diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index d901068d..046b6352 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -102,6 +102,14 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile + - name: Build app for E2E + run: pnpm run build + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NODE_OPTIONS: --max-old-space-size=4096 + VITE_E2E: "true" + VITE_USE_HTTP: "true" + - name: Run Playwright tests run: pnpm run test:e2e --shard=${{ matrix.shard }}/3 diff --git a/ai/plugins/tempo/.codex-plugin/plugin.json b/ai/plugins/tempo/.codex-plugin/plugin.json index 64f24450..17eab950 100644 --- a/ai/plugins/tempo/.codex-plugin/plugin.json +++ b/ai/plugins/tempo/.codex-plugin/plugin.json @@ -6,7 +6,7 @@ "name": "Tempo", "url": "https://tempo.xyz" }, - "homepage": "https://docs.tempo.xyz/guide/using-tempo-with-ai", + "homepage": "https://docs.tempo.xyz/docs/guide/using-tempo-with-ai", "repository": "https://github.com/tempoxyz/docs", "license": "MIT", "keywords": ["tempo", "mcp", "payments", "wallet", "stablecoins", "mpp"], diff --git a/ai/plugins/tempo/skills/tempo/SKILL.md b/ai/plugins/tempo/skills/tempo/SKILL.md index f471c72b..51b9bb68 100644 --- a/ai/plugins/tempo/skills/tempo/SKILL.md +++ b/ai/plugins/tempo/skills/tempo/SKILL.md @@ -23,9 +23,9 @@ verify them from docs before writing code. | User is building... | Start with | |---|---| | A new Tempo app or network setup | `/quickstart/integrate-tempo`, `/quickstart/connection-details`, `/sdk` | -| Account or wallet UX | `/guide/use-accounts`, `/accounts`, `/quickstart/wallet-developers` | +| Wallet UX | `/quickstart/wallet-developers`, `/quickstart/connection-details`, `/sdk` | | Stablecoin payments | `/guide/payments`, especially send, accept, virtual addresses, memos, fees, sponsorship, and parallel transactions | -| Sponsored or gasless transactions | `/guide/payments/sponsor-user-fees`, `/developer-tools/fee-payer`, `/accounts/server/handler.relay` | +| Sponsored or gasless transactions | `/guide/payments/sponsor-user-fees`, `/developer-tools/fee-payer` | | MPP or paid APIs | `/guide/machine-payments`, then `/guide/machine-payments/client`, `/server`, or `/agent` | | Agent-paid service calls | Use the `tempo-wallet` skill for wallet login, service discovery, and `tempo request` | | Hosted indexer queries | `/developer-tools/indexer` | @@ -57,11 +57,11 @@ code. Use the docs page’s linked examples when available. ## Useful Docs -- Getting started: `https://docs.tempo.xyz/quickstart/integrate-tempo` -- Connection details: `https://docs.tempo.xyz/quickstart/connection-details` -- Accounts: `https://docs.tempo.xyz/guide/use-accounts` -- Payments: `https://docs.tempo.xyz/guide/payments` -- Machine payments: `https://docs.tempo.xyz/guide/machine-payments` -- Hosted services: `https://docs.tempo.xyz/hosted-services` -- SDKs: `https://docs.tempo.xyz/sdk` -- Protocol specs: `https://docs.tempo.xyz/protocol` +- Getting started: `https://docs.tempo.xyz/docs/quickstart/integrate-tempo` +- Connection details: `https://docs.tempo.xyz/docs/quickstart/connection-details` +- Wallet: `https://docs.tempo.xyz/docs/wallet` +- Payments: `https://docs.tempo.xyz/docs/guide/payments` +- Machine payments: `https://docs.tempo.xyz/docs/guide/machine-payments` +- Hosted services: `https://docs.tempo.xyz/docs/hosted-services` +- SDKs: `https://docs.tempo.xyz/docs/sdk` +- Protocol specs: `https://docs.tempo.xyz/docs/protocol` diff --git a/bun.lock b/bun.lock new file mode 100644 index 00000000..29f1b1c5 --- /dev/null +++ b/bun.lock @@ -0,0 +1,1850 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "docs-next", + "dependencies": { + "@iconify-json/lucide": "^1.2.102", + "@iconify-json/simple-icons": "^1.2.77", + "@monaco-editor/react": "^4.7.0", + "@tanstack/react-query": "^5.99.0", + "@vercel/analytics": "^1.6.1", + "@vercel/speed-insights": "^1.3.1", + "abitype": "^1.2.3", + "accounts": "^0.6.5", + "cva": "1.0.0-beta.4", + "mermaid": "^11.14.0", + "monaco-editor": "^0.55.1", + "ox": "0.14.18", + "posthog-js": "^1.367.0", + "posthog-node": "^5.29.2", + "prool": "^0.2.4", + "react": "^19.2.5", + "react-dom": "^19.2.5", + "sonner": "^2.0.7", + "sql-formatter": "^15.7.3", + "tailwind-merge": "^3.5.0", + "tailwindcss": "^4.2.2", + "unplugin-auto-import": "^21.0.0", + "unplugin-icons": "^23.0.1", + "viem": "2.48.0", + "vocs": "https://pkg.pr.new/wevm/vocs@2fb25c2", + "wagmi": "^3.6.1", + "waku": "1.0.0-alpha.4", + "zod": "^4.3.6", + }, + "devDependencies": { + "@biomejs/biome": "^2.3.11", + "@playwright/test": "^1.58.0", + "@types/node": "^25.6.0", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@typescript/native-preview": "7.0.0-dev.20260122.3", + "@vitejs/plugin-react": "^5.2.0", + "anser": "^2.3.5", + "tsx": "^4.21.0", + "typescript": "^5.9.3", + "vite": "^7.3.2", + "vite-plugin-mkcert": "^1.17.12", + }, + }, + }, + "packages": { + "@adraffy/ens-normalize": ["@adraffy/ens-normalize@1.11.1", "", {}, "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ=="], + + "@antfu/install-pkg": ["@antfu/install-pkg@1.1.0", "", { "dependencies": { "package-manager-detector": "^1.3.0", "tinyexec": "^1.0.1" } }, "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ=="], + + "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], + + "@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="], + + "@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="], + + "@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="], + + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="], + + "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], + + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="], + + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="], + + "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + + "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], + + "@babel/helpers": ["@babel/helpers@7.29.2", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.29.0" } }, "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw=="], + + "@babel/parser": ["@babel/parser@7.29.2", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="], + + "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="], + + "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="], + + "@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="], + + "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], + + "@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="], + + "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + + "@base-ui/react": ["@base-ui/react@1.4.1", "", { "dependencies": { "@babel/runtime": "^7.29.2", "@base-ui/utils": "0.2.8", "@floating-ui/react-dom": "^2.1.8", "@floating-ui/utils": "^0.2.11", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@date-fns/tz": "^1.2.0", "@types/react": "^17 || ^18 || ^19", "date-fns": "^4.0.0", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@date-fns/tz", "@types/react", "date-fns"] }, "sha512-Ab5/LIhcmL8BQcsBUYiOfkSDRdLpvgUBzMK30cu684JPcLclYlztharvCZyNNgzJtbAiREzI9q0pI5erHCMgCw=="], + + "@base-ui/utils": ["@base-ui/utils@0.2.8", "", { "dependencies": { "@babel/runtime": "^7.29.2", "@floating-ui/utils": "^0.2.11", "reselect": "^5.1.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@types/react": "^17 || ^18 || ^19", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-jvOi+c+ftGlGotNcKnzPVg2IhCaDTB6/6R3JeqdjdXktuAJi3wKH9T7+svuaKh1mmfVU11UWzUZVH74JDfi/wQ=="], + + "@biomejs/biome": ["@biomejs/biome@2.4.13", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.13", "@biomejs/cli-darwin-x64": "2.4.13", "@biomejs/cli-linux-arm64": "2.4.13", "@biomejs/cli-linux-arm64-musl": "2.4.13", "@biomejs/cli-linux-x64": "2.4.13", "@biomejs/cli-linux-x64-musl": "2.4.13", "@biomejs/cli-win32-arm64": "2.4.13", "@biomejs/cli-win32-x64": "2.4.13" }, "bin": { "biome": "bin/biome" } }, "sha512-gLXOwkOBBg0tr7bDsqlkIh4uFeKuMjxvqsrb1Tukww1iDmHcfr4Uu8MoQxp0Rcte+69+osRNWXwHsu/zxT6XqA=="], + + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.13", "", { "os": "darwin", "cpu": "arm64" }, "sha512-2KImO1jhNFBa2oWConyr0x6flxbQpGKv6902uGXpYM62Xyem8U80j441SyUJ8KyngsmKbQjeIv1q2CQfDkNnYg=="], + + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.13", "", { "os": "darwin", "cpu": "x64" }, "sha512-BKrJklbaFN4p1Ts4kPBczo+PkbsHQg57kmJ+vON9u2t6uN5okYHaSr7h/MutPCWQgg2lglaWoSmm+zhYW+oOkg=="], + + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.13", "", { "os": "linux", "cpu": "arm64" }, "sha512-NzkUDSqfvMBrPplKgVr3aXLHZ2NEELvvF4vZxXulEylKWIGqlvNEcwUcj9OLrn75TD3lJ/GIqCVlBwd1MZCuYQ=="], + + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.13", "", { "os": "linux", "cpu": "arm64" }, "sha512-U5MsuBQW25dXaYtqWWSPM3P96H6Y+fHuja3TQpMNnylocHW0tEbtFTDlUj6oM+YJLntvEkQy4grBvQNUD4+RCg=="], + + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.13", "", { "os": "linux", "cpu": "x64" }, "sha512-Az3ZZedYRBo9EQzNnD9SxFcR1G5QsGo6VEc2hIyVPZ1rdKwee/7E9oeBBZFpE8Z44ekxsDQBqbiWGW5ShOhUSQ=="], + + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.13", "", { "os": "linux", "cpu": "x64" }, "sha512-Z601MienRgTBDza/+u2CH3RSrWoXo9rtr8NK6A4KJzqGgfxx+H3VlyLgTJ4sRo40T3pIsqpTmiOQEvYzQvBRvQ=="], + + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.13", "", { "os": "win32", "cpu": "arm64" }, "sha512-Px9PS2B5/Q183bUwy/5VHqp3J2lzdOCeVGzMpphYfl8oSa7VDCqenBdqWpy6DCy/en4Rbf/Y1RieZF6dJPcc9A=="], + + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.13", "", { "os": "win32", "cpu": "x64" }, "sha512-tTcMkXyBrmHi9BfrD2VNHs/5rYIUKETqsBlYOvSAABwBkJhSDVb5e7wPukftsQbO3WzQkXe6kaztC6WtUOXSoQ=="], + + "@braintree/sanitize-url": ["@braintree/sanitize-url@7.1.2", "", {}, "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA=="], + + "@cfworker/json-schema": ["@cfworker/json-schema@4.1.1", "", {}, "sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og=="], + + "@chevrotain/cst-dts-gen": ["@chevrotain/cst-dts-gen@12.0.0", "", { "dependencies": { "@chevrotain/gast": "12.0.0", "@chevrotain/types": "12.0.0" } }, "sha512-fSL4KXjTl7cDgf0B5Rip9Q05BOrYvkJV/RrBTE/bKDN096E4hN/ySpcBK5B24T76dlQ2i32Zc3PAE27jFnFrKg=="], + + "@chevrotain/gast": ["@chevrotain/gast@12.0.0", "", { "dependencies": { "@chevrotain/types": "12.0.0" } }, "sha512-1ne/m3XsIT8aEdrvT33so0GUC+wkctpUPK6zU9IlOyJLUbR0rg4G7ZiApiJbggpgPir9ERy3FRjT6T7lpgetnQ=="], + + "@chevrotain/regexp-to-ast": ["@chevrotain/regexp-to-ast@12.0.0", "", {}, "sha512-p+EW9MaJwgaHguhoqwOtx/FwuGr+DnNn857sXWOi/mClXIkPGl3rn7hGNWvo31HA3vyeQxjqe+H36yZJwYU8cA=="], + + "@chevrotain/types": ["@chevrotain/types@12.0.0", "", {}, "sha512-S+04vjFQKeuYw0/eW3U52LkAHQsB1ASxsPGsLPUyQgrZ2iNNibQrsidruDzjEX2JYfespXMG0eZmXlhA6z7nWA=="], + + "@chevrotain/utils": ["@chevrotain/utils@12.0.0", "", {}, "sha512-lB59uJoaGIfOOL9knQqQRfhl9g7x8/wqFkp13zTdkRu1huG9kg6IJs1O8hqj9rs6h7orGxHJUKb+mX3rPbWGhA=="], + + "@codemirror/autocomplete": ["@codemirror/autocomplete@6.20.1", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0" } }, "sha512-1cvg3Vz1dSSToCNlJfRA2WSI4ht3K+WplO0UMOgmUYPivCyy2oueZY6Lx7M9wThm7SDUBViRmuT+OG/i8+ON9A=="], + + "@codemirror/commands": ["@codemirror/commands@6.10.3", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.6.0", "@codemirror/view": "^6.27.0", "@lezer/common": "^1.1.0" } }, "sha512-JFRiqhKu+bvSkDLI+rUhJwSxQxYb759W5GBezE8Uc8mHLqC9aV/9aTC7yJSqCtB3F00pylrLCwnyS91Ap5ej4Q=="], + + "@codemirror/lang-css": ["@codemirror/lang-css@6.3.1", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@lezer/common": "^1.0.2", "@lezer/css": "^1.1.7" } }, "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg=="], + + "@codemirror/lang-html": ["@codemirror/lang-html@6.4.11", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/lang-css": "^6.0.0", "@codemirror/lang-javascript": "^6.0.0", "@codemirror/language": "^6.4.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0", "@lezer/css": "^1.1.0", "@lezer/html": "^1.3.12" } }, "sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw=="], + + "@codemirror/lang-javascript": ["@codemirror/lang-javascript@6.2.5", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.6.0", "@codemirror/lint": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0", "@lezer/javascript": "^1.0.0" } }, "sha512-zD4e5mS+50htS7F+TYjBPsiIFGanfVqg4HyUz6WNFikgOPf2BgKlx+TQedI1w6n/IqRBVBbBWmGFdLB/7uxO4A=="], + + "@codemirror/language": ["@codemirror/language@6.12.3", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.23.0", "@lezer/common": "^1.5.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0", "style-mod": "^4.0.0" } }, "sha512-QwCZW6Tt1siP37Jet9Tb02Zs81TQt6qQrZR2H+eGMcFsL1zMrk2/b9CLC7/9ieP1fjIUMgviLWMmgiHoJrj+ZA=="], + + "@codemirror/lint": ["@codemirror/lint@6.9.5", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.35.0", "crelt": "^1.0.5" } }, "sha512-GElsbU9G7QT9xXhpUg1zWGmftA/7jamh+7+ydKRuT0ORpWS3wOSP0yT1FOlIZa7mIJjpVPipErsyvVqB9cfTFA=="], + + "@codemirror/state": ["@codemirror/state@6.6.0", "", { "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } }, "sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ=="], + + "@codemirror/view": ["@codemirror/view@6.41.1", "", { "dependencies": { "@codemirror/state": "^6.6.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-ToDnWKbBnke+ZLrP6vgTTDScGi5H37YYuZGniQaBzxMVdtCxMrslsmtnOvbPZk4RX9bvkQqnWR/WS/35tJA0qg=="], + + "@codesandbox/nodebox": ["@codesandbox/nodebox@0.1.8", "", { "dependencies": { "outvariant": "^1.4.0", "strict-event-emitter": "^0.4.3" } }, "sha512-2VRS6JDSk+M+pg56GA6CryyUSGPjBEe8Pnae0QL3jJF1mJZJVMDKr93gJRtBbLkfZN6LD/DwMtf+2L0bpWrjqg=="], + + "@codesandbox/sandpack-client": ["@codesandbox/sandpack-client@2.19.8", "", { "dependencies": { "@codesandbox/nodebox": "0.1.8", "buffer": "^6.0.3", "dequal": "^2.0.2", "mime-db": "^1.52.0", "outvariant": "1.4.0", "static-browser-server": "1.0.3" } }, "sha512-CMV4nr1zgKzVpx4I3FYvGRM5YT0VaQhALMW9vy4wZRhEyWAtJITQIqZzrTGWqB1JvV7V72dVEUCUPLfYz5hgJQ=="], + + "@codesandbox/sandpack-react": ["@codesandbox/sandpack-react@2.20.0", "", { "dependencies": { "@codemirror/autocomplete": "^6.4.0", "@codemirror/commands": "^6.1.3", "@codemirror/lang-css": "^6.0.1", "@codemirror/lang-html": "^6.4.0", "@codemirror/lang-javascript": "^6.1.2", "@codemirror/language": "^6.3.2", "@codemirror/state": "^6.2.0", "@codemirror/view": "^6.7.1", "@codesandbox/sandpack-client": "^2.19.8", "@lezer/highlight": "^1.1.3", "@react-hook/intersection-observer": "^3.1.1", "@stitches/core": "^1.2.6", "anser": "^2.1.1", "clean-set": "^1.1.2", "dequal": "^2.0.2", "escape-carriage": "^1.3.1", "lz-string": "^1.4.4", "react-devtools-inline": "4.4.0", "react-is": "^17.0.2" }, "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19", "react-dom": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-takd1YpW/PMQ6KPQfvseWLHWklJovGY8QYj8MtWnskGKbjOGJ6uZfyZbcJ6aCFLQMpNyjTqz9AKNbvhCOZ1TUQ=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.7", "", { "os": "aix", "cpu": "ppc64" }, "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.27.7", "", { "os": "android", "cpu": "arm" }, "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.7", "", { "os": "android", "cpu": "arm64" }, "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.27.7", "", { "os": "android", "cpu": "x64" }, "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.7", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.7", "", { "os": "freebsd", "cpu": "x64" }, "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.7", "", { "os": "linux", "cpu": "arm" }, "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.7", "", { "os": "linux", "cpu": "ia32" }, "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.7", "", { "os": "linux", "cpu": "ppc64" }, "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.7", "", { "os": "linux", "cpu": "s390x" }, "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.7", "", { "os": "linux", "cpu": "x64" }, "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.7", "", { "os": "none", "cpu": "x64" }, "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.7", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.7", "", { "os": "openbsd", "cpu": "x64" }, "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.7", "", { "os": "sunos", "cpu": "x64" }, "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.7", "", { "os": "win32", "cpu": "ia32" }, "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.7", "", { "os": "win32", "cpu": "x64" }, "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg=="], + + "@floating-ui/core": ["@floating-ui/core@1.7.5", "", { "dependencies": { "@floating-ui/utils": "^0.2.11" } }, "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ=="], + + "@floating-ui/dom": ["@floating-ui/dom@1.7.6", "", { "dependencies": { "@floating-ui/core": "^1.7.5", "@floating-ui/utils": "^0.2.11" } }, "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ=="], + + "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.8", "", { "dependencies": { "@floating-ui/dom": "^1.7.6" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A=="], + + "@floating-ui/utils": ["@floating-ui/utils@0.2.11", "", {}, "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg=="], + + "@hono/node-server": ["@hono/node-server@1.19.14", "", { "peerDependencies": { "hono": "^4" } }, "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw=="], + + "@iconify-json/lucide": ["@iconify-json/lucide@1.2.103", "", { "dependencies": { "@iconify/types": "*" } }, "sha512-A5WdDll62RPxYs1yeYa9b2mV1Vs+lK/wX0wYKR+xVnhwsfPGLBwo6qfyoAQLo7AaP7oHlkw3gnHdOCERegbwTA=="], + + "@iconify-json/simple-icons": ["@iconify-json/simple-icons@1.2.79", "", { "dependencies": { "@iconify/types": "*" } }, "sha512-aNyO7Fd1qej9oQfIyohYFRv0lhQLaZ+6UkK1c1qwax0MDPUOZOdq65MlU500kow97pD/W+b2u1And3e25eE24Q=="], + + "@iconify-json/vscode-icons": ["@iconify-json/vscode-icons@1.2.46", "", { "dependencies": { "@iconify/types": "*" } }, "sha512-ZuLQscdXzGfUy1BtpNE74rNRjhNkcT/BLUbclQpY7aNLS2ByBuF9RzSjJQ1c0nqRyyInBFWmEL8DbTufw6w5Vw=="], + + "@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="], + + "@iconify/utils": ["@iconify/utils@3.1.0", "", { "dependencies": { "@antfu/install-pkg": "^1.1.0", "@iconify/types": "^2.0.0", "mlly": "^1.8.0" } }, "sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw=="], + + "@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/source-map": ["@jridgewell/source-map@0.3.11", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" } }, "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@lezer/common": ["@lezer/common@1.5.2", "", {}, "sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ=="], + + "@lezer/css": ["@lezer/css@1.3.3", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.3.0" } }, "sha512-RzBo8r+/6QJeow7aPHIpGVIH59xTcJXp399820gZoMo9noQDRVpJLheIBUicYwKcsbOYoBRoLZlf2720dG/4Tg=="], + + "@lezer/highlight": ["@lezer/highlight@1.2.3", "", { "dependencies": { "@lezer/common": "^1.3.0" } }, "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g=="], + + "@lezer/html": ["@lezer/html@1.3.13", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-oI7n6NJml729m7pjm9lvLvmXbdoMoi2f+1pwSDJkl9d68zGr7a9Btz8NdHTGQZtW2DA25ybeuv/SyDb9D5tseg=="], + + "@lezer/javascript": ["@lezer/javascript@1.5.4", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.1.3", "@lezer/lr": "^1.3.0" } }, "sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA=="], + + "@lezer/lr": ["@lezer/lr@1.4.10", "", { "dependencies": { "@lezer/common": "^1.0.0" } }, "sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A=="], + + "@marijn/find-cluster-break": ["@marijn/find-cluster-break@1.0.2", "", {}, "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g=="], + + "@mdx-js/mdx": ["@mdx-js/mdx@3.1.1", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "acorn": "^8.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ=="], + + "@mdx-js/react": ["@mdx-js/react@3.1.1", "", { "dependencies": { "@types/mdx": "^2.0.0" }, "peerDependencies": { "@types/react": ">=16", "react": ">=16" } }, "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw=="], + + "@mdx-js/rollup": ["@mdx-js/rollup@3.1.1", "", { "dependencies": { "@mdx-js/mdx": "^3.0.0", "@rollup/pluginutils": "^5.0.0", "source-map": "^0.7.0", "vfile": "^6.0.0" }, "peerDependencies": { "rollup": ">=2" } }, "sha512-v8satFmBB+DqDzYohnm1u2JOvxx6Hl3pUvqzJvfs2Zk/ngZ1aRUhsWpXvwPkNeGN9c2NCm/38H29ZqXQUjf8dw=="], + + "@mermaid-js/parser": ["@mermaid-js/parser@1.1.0", "", { "dependencies": { "langium": "^4.0.0" } }, "sha512-gxK9ZX2+Fex5zu8LhRQoMeMPEHbc73UKZ0FQ54YrQtUxE1VVhMwzeNtKRPAu5aXks4FasbMe4xB4bWrmq6Jlxw=="], + + "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="], + + "@modelcontextprotocol/server": ["@modelcontextprotocol/server@2.0.0-alpha.2", "", { "dependencies": { "zod": "^4.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-gmLgdHzlYM8L7Aw/+VE0kxjT25WKamtUSLNhdOgrJq5CrESvqVSoAfWSJJeNPUXNTluQ+dYDGFbKVitdsJtbPA=="], + + "@monaco-editor/loader": ["@monaco-editor/loader@1.7.0", "", { "dependencies": { "state-local": "^1.0.6" } }, "sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA=="], + + "@monaco-editor/react": ["@monaco-editor/react@4.7.0", "", { "dependencies": { "@monaco-editor/loader": "^1.5.0" }, "peerDependencies": { "monaco-editor": ">= 0.25.0 < 1", "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" } }, "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA=="], + + "@noble/ciphers": ["@noble/ciphers@1.3.0", "", {}, "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw=="], + + "@noble/curves": ["@noble/curves@1.9.1", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA=="], + + "@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], + + "@open-draft/deferred-promise": ["@open-draft/deferred-promise@2.2.0", "", {}, "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA=="], + + "@opentelemetry/api": ["@opentelemetry/api@1.9.1", "", {}, "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q=="], + + "@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.208.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg=="], + + "@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="], + + "@opentelemetry/exporter-logs-otlp-http": ["@opentelemetry/exporter-logs-otlp-http@0.208.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.208.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-exporter-base": "0.208.0", "@opentelemetry/otlp-transformer": "0.208.0", "@opentelemetry/sdk-logs": "0.208.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-jOv40Bs9jy9bZVLo/i8FwUiuCvbjWDI+ZW13wimJm4LjnlwJxGgB+N/VWOZUTpM+ah/awXeQqKdNlpLf2EjvYg=="], + + "@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.208.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-transformer": "0.208.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-gMd39gIfVb2OgxldxUtOwGJYSH8P1kVFFlJLuut32L6KgUC4gl1dMhn+YC2mGn0bDOiQYSk/uHOdSjuKp58vvA=="], + + "@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.208.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.208.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.208.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-DCFPY8C6lAQHUNkzcNT9R+qYExvsk6C5Bto2pbNxgicpcSWbe2WHShLxkOxIdNcBiYPdVHv/e7vH7K6TI+C+fQ=="], + + "@opentelemetry/resources": ["@opentelemetry/resources@2.7.0", "", { "dependencies": { "@opentelemetry/core": "2.7.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-K+oi0hNMv94EpZbnW3eyu2X6SGVpD3O5DhG2NIp65Hc7lhAj9brRXTAVzh3wB82+q3ThakEf7Zd7RsFUqcTc7A=="], + + "@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.208.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.208.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-QlAyL1jRpOeaqx7/leG1vJMp84g0xKP6gJmfELBpnI4O/9xPX+Hu5m1POk9Kl+veNkyth5t19hRlN6tNY1sjbA=="], + + "@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw=="], + + "@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw=="], + + "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + + "@playwright/test": ["@playwright/test@1.59.1", "", { "dependencies": { "playwright": "1.59.1" }, "bin": { "playwright": "cli.js" } }, "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg=="], + + "@posthog/core": ["@posthog/core@1.27.3", "", { "dependencies": { "@posthog/types": "1.371.4" } }, "sha512-DFDKJ8tpuIZ3CNsoui+xCZsZqWDbaIBe1Kt0BNXQNWedkZ1AgO4shx11n9RkTmHGv1evObhhTDLmn0n4BwGzhw=="], + + "@posthog/types": ["@posthog/types@1.371.4", "", {}, "sha512-6jpn4ObQQVmMJTobJuWEaX1pjv0FtsAjhqzs0gFYbssGVsITl4TNjTfvdia07ILk1BsL4wELeMtd9peQyxkK7w=="], + + "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="], + + "@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="], + + "@protobufjs/codegen": ["@protobufjs/codegen@2.0.4", "", {}, "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="], + + "@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.0", "", {}, "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="], + + "@protobufjs/fetch": ["@protobufjs/fetch@1.1.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" } }, "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ=="], + + "@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="], + + "@protobufjs/inquire": ["@protobufjs/inquire@1.1.0", "", {}, "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="], + + "@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="], + + "@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="], + + "@protobufjs/utf8": ["@protobufjs/utf8@1.1.0", "", {}, "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="], + + "@react-hook/intersection-observer": ["@react-hook/intersection-observer@3.1.2", "", { "dependencies": { "@react-hook/passive-layout-effect": "^1.2.0", "intersection-observer": "^0.10.0" }, "peerDependencies": { "react": ">=16.8" } }, "sha512-mWU3BMkmmzyYMSuhO9wu3eJVP21N8TcgYm9bZnTrMwuM818bEk+0NRM3hP+c/TqA9Ln5C7qE53p1H0QMtzYdvQ=="], + + "@react-hook/passive-layout-effect": ["@react-hook/passive-layout-effect@1.2.1", "", { "peerDependencies": { "react": ">=16.8" } }, "sha512-IwEphTD75liO8g+6taS+4oqz+nnroocNfWVHWz7j+N+ZO2vYrc6PV1q7GQhuahL0IOR7JccFTsFKQ/mb6iZWAg=="], + + "@remix-run/node-fetch-server": ["@remix-run/node-fetch-server@0.13.0", "", {}, "sha512-1EsNo0ZpgXu/90AWoRZf/oE3RVTUS80tiTUpt+hv5pjtAkw7icN4WskDwz/KdAw5ARbJLMhZBrO1NqThmy/McA=="], + + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.3", "", {}, "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q=="], + + "@rollup/pluginutils": ["@rollup/pluginutils@5.3.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.2", "", { "os": "android", "cpu": "arm" }, "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.60.2", "", { "os": "android", "cpu": "arm64" }, "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.60.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.60.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.60.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.60.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.60.2", "", { "os": "linux", "cpu": "arm" }, "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.60.2", "", { "os": "linux", "cpu": "arm" }, "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.60.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.60.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.60.2", "", { "os": "linux", "cpu": "none" }, "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A=="], + + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.60.2", "", { "os": "linux", "cpu": "none" }, "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.60.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw=="], + + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.60.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.60.2", "", { "os": "linux", "cpu": "none" }, "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.60.2", "", { "os": "linux", "cpu": "none" }, "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.60.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.60.2", "", { "os": "linux", "cpu": "x64" }, "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.60.2", "", { "os": "linux", "cpu": "x64" }, "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw=="], + + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.60.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.60.2", "", { "os": "none", "cpu": "arm64" }, "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.60.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.60.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.60.2", "", { "os": "win32", "cpu": "x64" }, "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.60.2", "", { "os": "win32", "cpu": "x64" }, "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA=="], + + "@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="], + + "@scure/bip32": ["@scure/bip32@1.7.0", "", { "dependencies": { "@noble/curves": "~1.9.0", "@noble/hashes": "~1.8.0", "@scure/base": "~1.2.5" } }, "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw=="], + + "@scure/bip39": ["@scure/bip39@1.6.0", "", { "dependencies": { "@noble/hashes": "~1.8.0", "@scure/base": "~1.2.5" } }, "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A=="], + + "@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="], + + "@shikijs/core": ["@shikijs/core@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA=="], + + "@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA=="], + + "@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g=="], + + "@shikijs/langs": ["@shikijs/langs@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg=="], + + "@shikijs/rehype": ["@shikijs/rehype@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@types/hast": "^3.0.4", "hast-util-to-string": "^3.0.1", "shiki": "3.23.0", "unified": "^11.0.5", "unist-util-visit": "^5.1.0" } }, "sha512-GepKJxXHbXFfAkiZZZ+4V7x71Lw3s0ALYmydUxJRdvpKjSx9FOMSaunv6WRLFBXR6qjYerUq1YZQno+2gLEPwA=="], + + "@shikijs/themes": ["@shikijs/themes@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA=="], + + "@shikijs/transformers": ["@shikijs/transformers@3.23.0", "", { "dependencies": { "@shikijs/core": "3.23.0", "@shikijs/types": "3.23.0" } }, "sha512-F9msZVxdF+krQNSdQ4V+Ja5QemeAoTQ2jxt7nJCwhDsdF1JWS3KxIQXA3lQbyKwS3J61oHRUSv4jYWv3CkaKTQ=="], + + "@shikijs/twoslash": ["@shikijs/twoslash@3.23.0", "", { "dependencies": { "@shikijs/core": "3.23.0", "@shikijs/types": "3.23.0", "twoslash": "^0.3.6" }, "peerDependencies": { "typescript": ">=5.5.0" } }, "sha512-pNaLJWMA3LU7PhT8tm9OQBZ1epy0jmdgeJzntBtr1EVXLbHxGzTj3mnf9vOdcl84l96qnlJXkJ/NGXZYBpXl5g=="], + + "@shikijs/types": ["@shikijs/types@3.23.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="], + + "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="], + + "@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@4.0.0", "", {}, "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ=="], + + "@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="], + + "@stitches/core": ["@stitches/core@1.2.8", "", {}, "sha512-Gfkvwk9o9kE9r9XNBmJRfV8zONvXThnm1tcuojL04Uy5uRyqg93DC83lDebl0rocZCfKSjUv+fWYtMQmEDJldg=="], + + "@svgr/babel-plugin-add-jsx-attribute": ["@svgr/babel-plugin-add-jsx-attribute@8.0.0", "", { "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g=="], + + "@svgr/babel-plugin-remove-jsx-attribute": ["@svgr/babel-plugin-remove-jsx-attribute@8.0.0", "", { "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA=="], + + "@svgr/babel-plugin-remove-jsx-empty-expression": ["@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0", "", { "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA=="], + + "@svgr/babel-plugin-replace-jsx-attribute-value": ["@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0", "", { "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ=="], + + "@svgr/babel-plugin-svg-dynamic-title": ["@svgr/babel-plugin-svg-dynamic-title@8.0.0", "", { "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og=="], + + "@svgr/babel-plugin-svg-em-dimensions": ["@svgr/babel-plugin-svg-em-dimensions@8.0.0", "", { "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g=="], + + "@svgr/babel-plugin-transform-react-native-svg": ["@svgr/babel-plugin-transform-react-native-svg@8.1.0", "", { "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q=="], + + "@svgr/babel-plugin-transform-svg-component": ["@svgr/babel-plugin-transform-svg-component@8.0.0", "", { "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw=="], + + "@svgr/babel-preset": ["@svgr/babel-preset@8.1.0", "", { "dependencies": { "@svgr/babel-plugin-add-jsx-attribute": "8.0.0", "@svgr/babel-plugin-remove-jsx-attribute": "8.0.0", "@svgr/babel-plugin-remove-jsx-empty-expression": "8.0.0", "@svgr/babel-plugin-replace-jsx-attribute-value": "8.0.0", "@svgr/babel-plugin-svg-dynamic-title": "8.0.0", "@svgr/babel-plugin-svg-em-dimensions": "8.0.0", "@svgr/babel-plugin-transform-react-native-svg": "8.1.0", "@svgr/babel-plugin-transform-svg-component": "8.0.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug=="], + + "@svgr/core": ["@svgr/core@8.1.0", "", { "dependencies": { "@babel/core": "^7.21.3", "@svgr/babel-preset": "8.1.0", "camelcase": "^6.2.0", "cosmiconfig": "^8.1.3", "snake-case": "^3.0.4" } }, "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA=="], + + "@svgr/hast-util-to-babel-ast": ["@svgr/hast-util-to-babel-ast@8.0.0", "", { "dependencies": { "@babel/types": "^7.21.3", "entities": "^4.4.0" } }, "sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q=="], + + "@svgr/plugin-jsx": ["@svgr/plugin-jsx@8.1.0", "", { "dependencies": { "@babel/core": "^7.21.3", "@svgr/babel-preset": "8.1.0", "@svgr/hast-util-to-babel-ast": "8.0.0", "svg-parser": "^2.0.4" }, "peerDependencies": { "@svgr/core": "*" } }, "sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA=="], + + "@tailwindcss/node": ["@tailwindcss/node@4.2.4", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.4" } }, "sha512-Ai7+yQPxz3ddrDQzFfBKdHEVBg0w3Zl83jnjuwxnZOsnH9pGn93QHQtpU0p/8rYWxvbFZHneni6p1BSLK4DkGA=="], + + "@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.4", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.4", "@tailwindcss/oxide-darwin-arm64": "4.2.4", "@tailwindcss/oxide-darwin-x64": "4.2.4", "@tailwindcss/oxide-freebsd-x64": "4.2.4", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.4", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.4", "@tailwindcss/oxide-linux-arm64-musl": "4.2.4", "@tailwindcss/oxide-linux-x64-gnu": "4.2.4", "@tailwindcss/oxide-linux-x64-musl": "4.2.4", "@tailwindcss/oxide-wasm32-wasi": "4.2.4", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.4", "@tailwindcss/oxide-win32-x64-msvc": "4.2.4" } }, "sha512-9El/iI069DKDSXwTvB9J4BwdO5JhRrOweGaK25taBAvBXyXqJAX+Jqdvs8r8gKpsI/1m0LeJLyQYTf/WLrBT1Q=="], + + "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.2.4", "", { "os": "android", "cpu": "arm64" }, "sha512-e7MOr1SAn9U8KlZzPi1ZXGZHeC5anY36qjNwmZv9pOJ8E4Q6jmD1vyEHkQFmNOIN7twGPEMXRHmitN4zCMN03g=="], + + "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-tSC/Kbqpz/5/o/C2sG7QvOxAKqyd10bq+ypZNf+9Fi2TvbVbv1zNpcEptcsU7DPROaSbVgUXmrzKhurFvo5eDg=="], + + "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-yPyUXn3yO/ufR6+Kzv0t4fCg2qNr90jxXc5QqBpjlPNd0NqyDXcmQb/6weunH/MEDXW5dhyEi+agTDiqa3WsGg=="], + + "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.2.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-BoMIB4vMQtZsXdGLVc2z+P9DbETkiopogfWZKbWwM8b/1Vinbs4YcUwo+kM/KeLkX3Ygrf4/PsRndKaYhS8Eiw=="], + + "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-7pIHBLTHYRAlS7V22JNuTh33yLH4VElwKtB3bwchK/UaKUPpQ0lPQiOWcbm4V3WP2I6fNIJ23vABIvoy2izdwA=="], + + "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-+E4wxJ0ZGOzSH325reXTWB48l42i93kQqMvDyz5gqfRzRZ7faNhnmvlV4EPGJU3QJM/3Ab5jhJ5pCRUsKn6OQw=="], + + "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-bBADEGAbo4ASnppIziaQJelekCxdMaxisrk+fB7Thit72IBnALp9K6ffA2G4ruj90G9XRS2VQ6q2bCKbfFV82g=="], + + "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-7Mx25E4WTfnht0TVRTyC00j3i0M+EeFe7wguMDTlX4mRxafznw0CA8WJkFjWYH5BlgELd1kSjuU2JiPnNZbJDA=="], + + "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-2wwJRF7nyhOR0hhHoChc04xngV3iS+akccHTGtz965FwF0up4b2lOdo6kI1EbDaEXKgvcrFBYcYQQ/rrnWFVfA=="], + + "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.2.4", "", { "dependencies": { "@emnapi/core": "^1.8.1", "@emnapi/runtime": "^1.8.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.1", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-FQsqApeor8Fo6gUEklzmaa9994orJZZDBAlQpK2Mq+DslRKFJeD6AjHpBQ0kZFQohVr8o85PPh8eOy86VlSCmw=="], + + "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.2.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-L9BXqxC4ToVgwMFqj3pmZRqyHEztulpUJzCxUtLjobMCzTPsGt1Fa9enKbOpY2iIyVtaHNeNvAK8ERP/64sqGQ=="], + + "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.2.4", "", { "os": "win32", "cpu": "x64" }, "sha512-ESlKG0EpVJQwRjXDDa9rLvhEAh0mhP1sF7sap9dNZT0yyl9SAG6T7gdP09EH0vIv0UNTlo6jPWyujD6559fZvw=="], + + "@tailwindcss/vite": ["@tailwindcss/vite@4.2.4", "", { "dependencies": { "@tailwindcss/node": "4.2.4", "@tailwindcss/oxide": "4.2.4", "tailwindcss": "4.2.4" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "sha512-pCvohwOCspk3ZFn6eJzrrX3g4n2JY73H6MmYC87XfGPyTty4YsCjYTMArRZm/zOI8dIt3+EcrLHAFPe5A4bgtw=="], + + "@takumi-rs/core": ["@takumi-rs/core@0.62.8", "", { "optionalDependencies": { "@takumi-rs/core-darwin-arm64": "0.62.8", "@takumi-rs/core-darwin-x64": "0.62.8", "@takumi-rs/core-linux-arm64-gnu": "0.62.8", "@takumi-rs/core-linux-arm64-musl": "0.62.8", "@takumi-rs/core-linux-x64-gnu": "0.62.8", "@takumi-rs/core-linux-x64-musl": "0.62.8", "@takumi-rs/core-win32-arm64-msvc": "0.62.8", "@takumi-rs/core-win32-x64-msvc": "0.62.8" } }, "sha512-1KNknsY0r9+fhvMD4gijFuJ2qx63JIVII1dZLiz8/YuZ3mFd9IFW90RTeeAdsP83lNCn6nWCrR8W2hLQgjjCCg=="], + + "@takumi-rs/core-darwin-arm64": ["@takumi-rs/core-darwin-arm64@0.62.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TxVakpycL4g99i2mmLp7abKaQXMCirkrmIXOmQtOaezwsX+yKzDqfm8XH3fhhj5febqgtpHkNvKus8F5k81RHQ=="], + + "@takumi-rs/core-darwin-x64": ["@takumi-rs/core-darwin-x64@0.62.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-2a9TzXpPkpUUBc3dSU3BZAriBuLTPpVc0LzIvI72sp9qyYAVn5Itry+z1Xl+nt5As/xkFdOJccqQxe7tLLBtpQ=="], + + "@takumi-rs/core-linux-arm64-gnu": ["@takumi-rs/core-linux-arm64-gnu@0.62.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-e8Oe1aOFew+1rbu9y5JRaDvLqw4FqGJx1uPLo5mfdYQKWZ3ltdFseRUCCQI3rOXMXKz6uIWRuHd3Qw5sBp+wJg=="], + + "@takumi-rs/core-linux-arm64-musl": ["@takumi-rs/core-linux-arm64-musl@0.62.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-qR04H4DQfFsKmkl2kq/fh48zTC3QqBdvHjmHzJUckIktvJPKk1aK+jERIXeARkH6pfQuq+NI5Kbl1t9//qb8nQ=="], + + "@takumi-rs/core-linux-x64-gnu": ["@takumi-rs/core-linux-x64-gnu@0.62.8", "", { "os": "linux", "cpu": "x64" }, "sha512-/LouBAUVqK5/H3pwzOnitRz4JEJNrGJEN/L3TU0375M+uXFewWAl2iSwFqgMt9vCtKPrKa/oK+Iy3pdvFAnX2w=="], + + "@takumi-rs/core-linux-x64-musl": ["@takumi-rs/core-linux-x64-musl@0.62.8", "", { "os": "linux", "cpu": "x64" }, "sha512-FIt0fpowyh9dse8F29cYBPVhrdc7Zzjz1pEyJunNzIzCIpC0yG3eUwFnK0wsoYoyzbgVZkbLRFRRAFmUORLvDg=="], + + "@takumi-rs/core-win32-arm64-msvc": ["@takumi-rs/core-win32-arm64-msvc@0.62.8", "", { "os": "win32", "cpu": "arm64" }, "sha512-ucQmG2bp6yH+7oNQUev+0re+6FBHUOUcee4mwpQa+BKCtUSgkgQuw3ZAGdyXBQEfr3NrNsMqfc47b3Ljv3bIWA=="], + + "@takumi-rs/core-win32-x64-msvc": ["@takumi-rs/core-win32-x64-msvc@0.62.8", "", { "os": "win32", "cpu": "x64" }, "sha512-KxntZ5d7Cu87WQx0AUw0yzZ3AQuTjeTPZMe4MhYdQbgqw4y6hyOqUL3fz8z80/jh6XxLNHBgcBC0nn9FvJnvSw=="], + + "@takumi-rs/helpers": ["@takumi-rs/helpers@0.62.8", "", {}, "sha512-9qRFeuk0yTvAeVAaQoU2uW1XeigbTBta/fTReHxEsa0hYqyJe4gM4YicA0D29tfObhSRJUN/j2b2WmHuADAmcQ=="], + + "@takumi-rs/image-response": ["@takumi-rs/image-response@0.62.8", "", { "dependencies": { "@takumi-rs/core": "0.62.8", "@takumi-rs/helpers": "0.62.8", "@takumi-rs/wasm": "0.62.8" } }, "sha512-he79pvjif5psP8jyDiugUph3qbvKl7SaMetIuYUCI1asES5NMW3yw/nY2zQd73gqwqverhfnOEO/k4we6UxiIA=="], + + "@takumi-rs/wasm": ["@takumi-rs/wasm@0.62.8", "", {}, "sha512-V3L4LMoa1hqUxYel+j2oaIYhx7TdfSI/5iStU+7sTDru3o0gwJxw923jG66/F+lLSPGeY4rmYejcJCJN5VfpBA=="], + + "@tanstack/query-core": ["@tanstack/query-core@5.100.1", "", {}, "sha512-awvQhOO/2TrSCHE5LKKsXcvvj6WSBncwEcMFCB/ez0Qs0b17iyyivoGArNV3HFfXryZwCpnb/olsaBBKrIbtSw=="], + + "@tanstack/react-query": ["@tanstack/react-query@5.100.1", "", { "dependencies": { "@tanstack/query-core": "5.100.1" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-UgWRLhQKprC37SsO6y1zRabOqDmM2gsdTNPbqTT35yl7kOOhwXU4nyfOiGHXPwoEFJV1IpSk85hjIFjNFWVpzw=="], + + "@toon-format/toon": ["@toon-format/toon@2.1.0", "", {}, "sha512-JwWptdF5eOA0HaQxbKAzkpQtR4wSWTEfDlEy/y3/4okmOAX1qwnpLZMmtEWr+ncAhTTY1raCKH0kteHhSXnQqg=="], + + "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], + + "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="], + + "@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="], + + "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], + + "@types/d3": ["@types/d3@7.4.3", "", { "dependencies": { "@types/d3-array": "*", "@types/d3-axis": "*", "@types/d3-brush": "*", "@types/d3-chord": "*", "@types/d3-color": "*", "@types/d3-contour": "*", "@types/d3-delaunay": "*", "@types/d3-dispatch": "*", "@types/d3-drag": "*", "@types/d3-dsv": "*", "@types/d3-ease": "*", "@types/d3-fetch": "*", "@types/d3-force": "*", "@types/d3-format": "*", "@types/d3-geo": "*", "@types/d3-hierarchy": "*", "@types/d3-interpolate": "*", "@types/d3-path": "*", "@types/d3-polygon": "*", "@types/d3-quadtree": "*", "@types/d3-random": "*", "@types/d3-scale": "*", "@types/d3-scale-chromatic": "*", "@types/d3-selection": "*", "@types/d3-shape": "*", "@types/d3-time": "*", "@types/d3-time-format": "*", "@types/d3-timer": "*", "@types/d3-transition": "*", "@types/d3-zoom": "*" } }, "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww=="], + + "@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="], + + "@types/d3-axis": ["@types/d3-axis@3.0.6", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw=="], + + "@types/d3-brush": ["@types/d3-brush@3.0.6", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A=="], + + "@types/d3-chord": ["@types/d3-chord@3.0.6", "", {}, "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg=="], + + "@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="], + + "@types/d3-contour": ["@types/d3-contour@3.0.6", "", { "dependencies": { "@types/d3-array": "*", "@types/geojson": "*" } }, "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg=="], + + "@types/d3-delaunay": ["@types/d3-delaunay@6.0.4", "", {}, "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw=="], + + "@types/d3-dispatch": ["@types/d3-dispatch@3.0.7", "", {}, "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA=="], + + "@types/d3-drag": ["@types/d3-drag@3.0.7", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ=="], + + "@types/d3-dsv": ["@types/d3-dsv@3.0.7", "", {}, "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g=="], + + "@types/d3-ease": ["@types/d3-ease@3.0.2", "", {}, "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="], + + "@types/d3-fetch": ["@types/d3-fetch@3.0.7", "", { "dependencies": { "@types/d3-dsv": "*" } }, "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA=="], + + "@types/d3-force": ["@types/d3-force@3.0.10", "", {}, "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw=="], + + "@types/d3-format": ["@types/d3-format@3.0.4", "", {}, "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g=="], + + "@types/d3-geo": ["@types/d3-geo@3.1.0", "", { "dependencies": { "@types/geojson": "*" } }, "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ=="], + + "@types/d3-hierarchy": ["@types/d3-hierarchy@3.1.7", "", {}, "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg=="], + + "@types/d3-interpolate": ["@types/d3-interpolate@3.0.4", "", { "dependencies": { "@types/d3-color": "*" } }, "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA=="], + + "@types/d3-path": ["@types/d3-path@3.1.1", "", {}, "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg=="], + + "@types/d3-polygon": ["@types/d3-polygon@3.0.2", "", {}, "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA=="], + + "@types/d3-quadtree": ["@types/d3-quadtree@3.0.6", "", {}, "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg=="], + + "@types/d3-random": ["@types/d3-random@3.0.3", "", {}, "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ=="], + + "@types/d3-scale": ["@types/d3-scale@4.0.9", "", { "dependencies": { "@types/d3-time": "*" } }, "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw=="], + + "@types/d3-scale-chromatic": ["@types/d3-scale-chromatic@3.1.0", "", {}, "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ=="], + + "@types/d3-selection": ["@types/d3-selection@3.0.11", "", {}, "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w=="], + + "@types/d3-shape": ["@types/d3-shape@3.1.8", "", { "dependencies": { "@types/d3-path": "*" } }, "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w=="], + + "@types/d3-time": ["@types/d3-time@3.0.4", "", {}, "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="], + + "@types/d3-time-format": ["@types/d3-time-format@4.0.3", "", {}, "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg=="], + + "@types/d3-timer": ["@types/d3-timer@3.0.2", "", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="], + + "@types/d3-transition": ["@types/d3-transition@3.0.9", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg=="], + + "@types/d3-zoom": ["@types/d3-zoom@3.0.8", "", { "dependencies": { "@types/d3-interpolate": "*", "@types/d3-selection": "*" } }, "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw=="], + + "@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="], + + "@types/eslint": ["@types/eslint@9.6.1", "", { "dependencies": { "@types/estree": "*", "@types/json-schema": "*" } }, "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag=="], + + "@types/eslint-scope": ["@types/eslint-scope@3.7.7", "", { "dependencies": { "@types/eslint": "*", "@types/estree": "*" } }, "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg=="], + + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + + "@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="], + + "@types/geojson": ["@types/geojson@7946.0.16", "", {}, "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="], + + "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], + + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + + "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], + + "@types/mdx": ["@types/mdx@2.0.13", "", {}, "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw=="], + + "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], + + "@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="], + + "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], + + "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], + + "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], + + "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], + + "@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20260122.3", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260122.3", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20260122.3", "@typescript/native-preview-linux-arm": "7.0.0-dev.20260122.3", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20260122.3", "@typescript/native-preview-linux-x64": "7.0.0-dev.20260122.3", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20260122.3", "@typescript/native-preview-win32-x64": "7.0.0-dev.20260122.3" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-/G74DRDNrq2pkBjne3aGzycdy2bKa2Hs4DrkLMRq+cDODjyqvHJIZV7hjRRYph3cvGnd0bFd5G6LE5zNp+ap8w=="], + + "@typescript/native-preview-darwin-arm64": ["@typescript/native-preview-darwin-arm64@7.0.0-dev.20260122.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-VVr3SHMzQTeQhtSlLnSjMK8kaE3f2SmOOkvuC8qGHQ2W28RF2ZvIFhbJgCAI++8S/L0GufF+ISA50M41yRiNOA=="], + + "@typescript/native-preview-darwin-x64": ["@typescript/native-preview-darwin-x64@7.0.0-dev.20260122.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-3G2+xwNXu7cJtMavpuoJ3EdO0Dk1okrKQ28VNKrkV61+8Ei5Z6hNSU+rmEw7fn83EYGXMBpq8znO/U/Gvc3BDg=="], + + "@typescript/native-preview-linux-arm": ["@typescript/native-preview-linux-arm@7.0.0-dev.20260122.3", "", { "os": "linux", "cpu": "arm" }, "sha512-q/BqemIAcP7MjmtG8xXFnbC/MuCxzSL03kN2IrOybxetspCTEi5gKVLEm81LZUCLt3bVgBeVu1sSrQEGhunM6w=="], + + "@typescript/native-preview-linux-arm64": ["@typescript/native-preview-linux-arm64@7.0.0-dev.20260122.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-W5Ac4aYkKHf8ytEk969CjpWOT8WjVI7rbx5cwMCPKOLFnTyz4MJ2rK70IXWCssnZtz9KuSBdktAjhq7O1utF3w=="], + + "@typescript/native-preview-linux-x64": ["@typescript/native-preview-linux-x64@7.0.0-dev.20260122.3", "", { "os": "linux", "cpu": "x64" }, "sha512-N+yHKKuaMKjKMtxDBj7fK1nxbbVjme30kEoRQnYTPCwX7mwCbGy/8WffhgNV+qCxtdxp5P0+iQFr8X/dxjWS3w=="], + + "@typescript/native-preview-win32-arm64": ["@typescript/native-preview-win32-arm64@7.0.0-dev.20260122.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-QTT+YpIgPLl0TkVu2M/hGGR8MjVvfXDqA57R8ILS8+QjpzE+EB7P73MR7OAeCs+hI+Kw3OERoH3WtmdyHlYf5w=="], + + "@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20260122.3", "", { "os": "win32", "cpu": "x64" }, "sha512-Q6N5274oq3WiSlNoox9uC7YNEivkbwzYintiAzb45d1rsoknNgkCMRecdEC1+kMg6uNnWrmHELg1yJuLaaIinA=="], + + "@typescript/vfs": ["@typescript/vfs@1.6.4", "", { "dependencies": { "debug": "^4.4.3" }, "peerDependencies": { "typescript": "*" } }, "sha512-PJFXFS4ZJKiJ9Qiuix6Dz/OwEIqHD7Dme1UwZhTK11vR+5dqW2ACbdndWQexBzCx+CPuMe5WBYQWCsFyGlQLlQ=="], + + "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], + + "@upsetjs/venn.js": ["@upsetjs/venn.js@2.0.0", "", { "optionalDependencies": { "d3-selection": "^3.0.0", "d3-transition": "^3.0.1" } }, "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw=="], + + "@vercel/analytics": ["@vercel/analytics@1.6.1", "", { "peerDependencies": { "@remix-run/react": "^2", "@sveltejs/kit": "^1 || ^2", "next": ">= 13", "react": "^18 || ^19 || ^19.0.0-rc", "svelte": ">= 4", "vue": "^3", "vue-router": "^4" }, "optionalPeers": ["@remix-run/react", "@sveltejs/kit", "next", "react", "svelte", "vue", "vue-router"] }, "sha512-oH9He/bEM+6oKlv3chWuOOcp8Y6fo6/PSro8hEkgCW3pu9/OiCXiUpRUogDh3Fs3LH2sosDrx8CxeOLBEE+afg=="], + + "@vercel/speed-insights": ["@vercel/speed-insights@1.3.1", "", { "peerDependencies": { "@sveltejs/kit": "^1 || ^2", "next": ">= 13", "react": "^18 || ^19 || ^19.0.0-rc", "svelte": ">= 4", "vue": "^3", "vue-router": "^4" }, "optionalPeers": ["@sveltejs/kit", "next", "react", "svelte", "vue", "vue-router"] }, "sha512-PbEr7FrMkUrGYvlcLHGkXdCkxnylCWePx7lPxxq36DNdfo9mcUjLOmqOyPDHAOgnfqgGGdmE3XI9L/4+5fr+vQ=="], + + "@vitejs/plugin-react": ["@vitejs/plugin-react@5.2.0", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-rc.3", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw=="], + + "@vitejs/plugin-rsc": ["@vitejs/plugin-rsc@0.5.24", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.15", "es-module-lexer": "^2.0.0", "estree-walker": "^3.0.3", "magic-string": "^0.30.21", "srvx": "^0.11.15", "strip-literal": "^3.1.0", "turbo-stream": "^3.2.0", "vitefu": "^1.1.3" }, "peerDependencies": { "react": "*", "react-dom": "*", "react-server-dom-webpack": "*", "vite": "*" }, "optionalPeers": ["react-server-dom-webpack"] }, "sha512-FQ7o1Zf1GUB8L5qlIuV2mvIv/KahG2qUYW2gMpxyIN3zF7voDsfvA/t8w/TLjYC0T6p3JwMnK3N+YzMGf/m75A=="], + + "@wagmi/connectors": ["@wagmi/connectors@8.0.4", "", { "peerDependencies": { "@base-org/account": "^2.5.1", "@coinbase/wallet-sdk": "^4.3.6", "@metamask/connect-evm": "~0.9.0", "@safe-global/safe-apps-provider": "~0.18.6", "@safe-global/safe-apps-sdk": "^9.1.0", "@wagmi/core": "3.4.5", "@walletconnect/ethereum-provider": "^2.21.1", "accounts": "~0.6.7", "porto": "~0.2.35", "typescript": ">=5.7.3", "viem": "2.x" }, "optionalPeers": ["@base-org/account", "@coinbase/wallet-sdk", "@metamask/connect-evm", "@safe-global/safe-apps-provider", "@safe-global/safe-apps-sdk", "@walletconnect/ethereum-provider", "accounts", "porto", "typescript"] }, "sha512-zRxRmd4TnNv/LxYm/IcrUsXMFBECzEQAOAcUDRMUfRRKfneJFqmmv1kmUbWSIc/gBkBu6o3BAGPJjV5j3BUcLA=="], + + "@wagmi/core": ["@wagmi/core@3.4.5", "", { "dependencies": { "eventemitter3": "5.0.1", "mipd": "0.0.7", "zustand": "5.0.0" }, "peerDependencies": { "@tanstack/query-core": ">=5.0.0", "accounts": "~0.6.7", "typescript": ">=5.7.3", "viem": "2.x" }, "optionalPeers": ["@tanstack/query-core", "accounts", "typescript"] }, "sha512-rmqnLRlyFWcP2VvvQtS1XMmupaSruxCwSTfwB8v7pRyeVDywsOoJwIpLh4PI5o//b3ia4P0gND4vkQ1MmU8C3g=="], + + "@webassemblyjs/ast": ["@webassemblyjs/ast@1.14.1", "", { "dependencies": { "@webassemblyjs/helper-numbers": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2" } }, "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ=="], + + "@webassemblyjs/floating-point-hex-parser": ["@webassemblyjs/floating-point-hex-parser@1.13.2", "", {}, "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA=="], + + "@webassemblyjs/helper-api-error": ["@webassemblyjs/helper-api-error@1.13.2", "", {}, "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ=="], + + "@webassemblyjs/helper-buffer": ["@webassemblyjs/helper-buffer@1.14.1", "", {}, "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA=="], + + "@webassemblyjs/helper-numbers": ["@webassemblyjs/helper-numbers@1.13.2", "", { "dependencies": { "@webassemblyjs/floating-point-hex-parser": "1.13.2", "@webassemblyjs/helper-api-error": "1.13.2", "@xtuc/long": "4.2.2" } }, "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA=="], + + "@webassemblyjs/helper-wasm-bytecode": ["@webassemblyjs/helper-wasm-bytecode@1.13.2", "", {}, "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA=="], + + "@webassemblyjs/helper-wasm-section": ["@webassemblyjs/helper-wasm-section@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/wasm-gen": "1.14.1" } }, "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw=="], + + "@webassemblyjs/ieee754": ["@webassemblyjs/ieee754@1.13.2", "", { "dependencies": { "@xtuc/ieee754": "^1.2.0" } }, "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw=="], + + "@webassemblyjs/leb128": ["@webassemblyjs/leb128@1.13.2", "", { "dependencies": { "@xtuc/long": "4.2.2" } }, "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw=="], + + "@webassemblyjs/utf8": ["@webassemblyjs/utf8@1.13.2", "", {}, "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ=="], + + "@webassemblyjs/wasm-edit": ["@webassemblyjs/wasm-edit@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/helper-wasm-section": "1.14.1", "@webassemblyjs/wasm-gen": "1.14.1", "@webassemblyjs/wasm-opt": "1.14.1", "@webassemblyjs/wasm-parser": "1.14.1", "@webassemblyjs/wast-printer": "1.14.1" } }, "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ=="], + + "@webassemblyjs/wasm-gen": ["@webassemblyjs/wasm-gen@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/ieee754": "1.13.2", "@webassemblyjs/leb128": "1.13.2", "@webassemblyjs/utf8": "1.13.2" } }, "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg=="], + + "@webassemblyjs/wasm-opt": ["@webassemblyjs/wasm-opt@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", "@webassemblyjs/wasm-gen": "1.14.1", "@webassemblyjs/wasm-parser": "1.14.1" } }, "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw=="], + + "@webassemblyjs/wasm-parser": ["@webassemblyjs/wasm-parser@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-api-error": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/ieee754": "1.13.2", "@webassemblyjs/leb128": "1.13.2", "@webassemblyjs/utf8": "1.13.2" } }, "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ=="], + + "@webassemblyjs/wast-printer": ["@webassemblyjs/wast-printer@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" } }, "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw=="], + + "@xtuc/ieee754": ["@xtuc/ieee754@1.2.0", "", {}, "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA=="], + + "@xtuc/long": ["@xtuc/long@4.2.2", "", {}, "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ=="], + + "abitype": ["abitype@1.2.4", "", { "peerDependencies": { "typescript": ">=5.0.4", "zod": "^3.22.0 || ^4.0.0" }, "optionalPeers": ["typescript", "zod"] }, "sha512-dpKH+N27vRjarMVTFFkeY445VTKftzGWpL0FiT7xmVmzQRKazZexzC5uHG0f6XKsVLAuUlndnbGau6lRejClxg=="], + + "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], + + "accounts": ["accounts@0.6.7", "", { "dependencies": { "hono": "^4.12.12", "idb-keyval": "^6.2.2", "mipd": "^0.0.7", "mppx": "^0.5.10", "ox": "~0.14.15", "webauthx": "~0.1.0", "zod": "^4.3.6", "zustand": "^5.0.11" }, "peerDependencies": { "@react-native-async-storage/async-storage": "^3.0.2", "@wagmi/core": ">=2", "expo-secure-store": "^55.0.12", "expo-web-browser": "^55.0.13", "react": ">=18", "viem": ">=2.43.3" }, "optionalPeers": ["@react-native-async-storage/async-storage", "@wagmi/core", "expo-secure-store", "expo-web-browser", "react", "viem"] }, "sha512-bXTyx3AFoe98dnlavPsxp7Uoho+QXNdOeHNdsvzC5pzQ2idgK50yUiBTKXtI7+E8kSvvfzGQR8ZdwfgJS5bJHg=="], + + "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], + + "acorn-import-phases": ["acorn-import-phases@1.0.4", "", { "peerDependencies": { "acorn": "^8.14.0" } }, "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ=="], + + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + + "acorn-loose": ["acorn-loose@8.5.2", "", { "dependencies": { "acorn": "^8.15.0" } }, "sha512-PPvV6g8UGMGgjrMu+n/f9E/tCSkNQ2Y97eFvuVdJfG11+xdIeDcLyNdC8SHcrHbRqkfwLASdplyR6B6sKM1U4A=="], + + "ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], + + "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], + + "ajv-keywords": ["ajv-keywords@5.1.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3" }, "peerDependencies": { "ajv": "^8.8.2" } }, "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw=="], + + "anser": ["anser@2.3.5", "", {}, "sha512-vcZjxvvVoxTeR5XBNJB38oTu/7eDCZlwdz32N1eNgpyPF7j/Z7Idf+CUwQOkKKpJ7RJyjxgLHCM7vdIK0iCNMQ=="], + + "any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="], + + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "astring": ["astring@1.9.0", "", { "bin": { "astring": "bin/astring" } }, "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg=="], + + "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], + + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.21", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-Q+rUQ7Uz8AHM7DEaNdwvfFCTq7a43lNTzuS94eiWqwyxfV/wJv+oUivef51T91mmRY4d4A1u9rcSvkeufCVXlA=="], + + "body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], + + "browserslist": ["browserslist@4.28.2", "", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="], + + "buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], + + "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], + + "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], + + "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], + + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + + "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], + + "camelcase": ["camelcase@6.3.0", "", {}, "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001790", "", {}, "sha512-bOoxfJPyYo+ds6W0YfptaCWbFnJYjh2Y1Eow5lRv+vI2u8ganPZqNm1JwNh0t2ELQCqIWg4B3dWEusgAmsoyOw=="], + + "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], + + "change-case": ["change-case@5.4.4", "", {}, "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w=="], + + "character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="], + + "character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="], + + "character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="], + + "character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="], + + "chevrotain": ["chevrotain@12.0.0", "", { "dependencies": { "@chevrotain/cst-dts-gen": "12.0.0", "@chevrotain/gast": "12.0.0", "@chevrotain/regexp-to-ast": "12.0.0", "@chevrotain/types": "12.0.0", "@chevrotain/utils": "12.0.0" } }, "sha512-csJvb+6kEiQaqo1woTdSAuOWdN0WTLIydkKrBnS+V5gZz0oqBrp4kQ35519QgK6TpBThiG3V1vNSHlIkv4AglQ=="], + + "chevrotain-allstar": ["chevrotain-allstar@0.4.1", "", { "dependencies": { "lodash-es": "^4.17.21" }, "peerDependencies": { "chevrotain": "^12.0.0" } }, "sha512-PvVJm3oGqrveUVW2Vt/eZGeiAIsJszYweUcYwcskg9e+IubNYKKD+rHHem7A6XVO22eDAL+inxNIGAzZ/VIWlA=="], + + "chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="], + + "chrome-trace-event": ["chrome-trace-event@1.0.4", "", {}, "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ=="], + + "clean-set": ["clean-set@1.1.2", "", {}, "sha512-cA8uCj0qSoG9e0kevyOWXwPaELRPVg5Pxp6WskLMwerx257Zfnh8Nl0JBH59d7wQzij2CK7qEfJQK3RjuKKIug=="], + + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + + "collapse-white-space": ["collapse-white-space@2.1.0", "", {}, "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw=="], + + "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], + + "commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="], + + "confbox": ["confbox@0.2.4", "", {}, "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ=="], + + "content-disposition": ["content-disposition@1.1.0", "", {}, "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g=="], + + "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], + + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + + "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], + + "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], + + "core-js": ["core-js@3.49.0", "", {}, "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg=="], + + "cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="], + + "cose-base": ["cose-base@1.0.3", "", { "dependencies": { "layout-base": "^1.0.0" } }, "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg=="], + + "cosmiconfig": ["cosmiconfig@8.3.6", "", { "dependencies": { "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0", "path-type": "^4.0.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA=="], + + "crelt": ["crelt@1.0.6", "", {}, "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + + "cva": ["cva@1.0.0-beta.4", "", { "dependencies": { "clsx": "^2.1.1" }, "peerDependencies": { "typescript": ">= 4.5.5" }, "optionalPeers": ["typescript"] }, "sha512-F/JS9hScapq4DBVQXcK85l9U91M6ePeXoBMSp7vypzShoefUBxjQTo3g3935PUHgQd+IW77DjbPRIxugy4/GCQ=="], + + "cytoscape": ["cytoscape@3.33.2", "", {}, "sha512-sj4HXd3DokGhzZAdjDejGvTPLqlt84vNFN8m7bGsOzDY5DyVcxIb2ejIXat2Iy7HxWhdT/N1oKyheJ5YdpsGuw=="], + + "cytoscape-cose-bilkent": ["cytoscape-cose-bilkent@4.1.0", "", { "dependencies": { "cose-base": "^1.0.0" }, "peerDependencies": { "cytoscape": "^3.2.0" } }, "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ=="], + + "cytoscape-fcose": ["cytoscape-fcose@2.2.0", "", { "dependencies": { "cose-base": "^2.2.0" }, "peerDependencies": { "cytoscape": "^3.2.0" } }, "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ=="], + + "d": ["d@1.0.2", "", { "dependencies": { "es5-ext": "^0.10.64", "type": "^2.7.2" } }, "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw=="], + + "d3": ["d3@7.9.0", "", { "dependencies": { "d3-array": "3", "d3-axis": "3", "d3-brush": "3", "d3-chord": "3", "d3-color": "3", "d3-contour": "4", "d3-delaunay": "6", "d3-dispatch": "3", "d3-drag": "3", "d3-dsv": "3", "d3-ease": "3", "d3-fetch": "3", "d3-force": "3", "d3-format": "3", "d3-geo": "3", "d3-hierarchy": "3", "d3-interpolate": "3", "d3-path": "3", "d3-polygon": "3", "d3-quadtree": "3", "d3-random": "3", "d3-scale": "4", "d3-scale-chromatic": "3", "d3-selection": "3", "d3-shape": "3", "d3-time": "3", "d3-time-format": "4", "d3-timer": "3", "d3-transition": "3", "d3-zoom": "3" } }, "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA=="], + + "d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="], + + "d3-axis": ["d3-axis@3.0.0", "", {}, "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw=="], + + "d3-brush": ["d3-brush@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "3", "d3-transition": "3" } }, "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ=="], + + "d3-chord": ["d3-chord@3.0.1", "", { "dependencies": { "d3-path": "1 - 3" } }, "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g=="], + + "d3-color": ["d3-color@3.1.0", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="], + + "d3-contour": ["d3-contour@4.0.2", "", { "dependencies": { "d3-array": "^3.2.0" } }, "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA=="], + + "d3-delaunay": ["d3-delaunay@6.0.4", "", { "dependencies": { "delaunator": "5" } }, "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A=="], + + "d3-dispatch": ["d3-dispatch@3.0.1", "", {}, "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg=="], + + "d3-drag": ["d3-drag@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-selection": "3" } }, "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg=="], + + "d3-dsv": ["d3-dsv@3.0.1", "", { "dependencies": { "commander": "7", "iconv-lite": "0.6", "rw": "1" }, "bin": { "csv2json": "bin/dsv2json.js", "csv2tsv": "bin/dsv2dsv.js", "dsv2dsv": "bin/dsv2dsv.js", "dsv2json": "bin/dsv2json.js", "json2csv": "bin/json2dsv.js", "json2dsv": "bin/json2dsv.js", "json2tsv": "bin/json2dsv.js", "tsv2csv": "bin/dsv2dsv.js", "tsv2json": "bin/dsv2json.js" } }, "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q=="], + + "d3-ease": ["d3-ease@3.0.1", "", {}, "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="], + + "d3-fetch": ["d3-fetch@3.0.1", "", { "dependencies": { "d3-dsv": "1 - 3" } }, "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw=="], + + "d3-force": ["d3-force@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-quadtree": "1 - 3", "d3-timer": "1 - 3" } }, "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg=="], + + "d3-format": ["d3-format@3.1.2", "", {}, "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg=="], + + "d3-geo": ["d3-geo@3.1.1", "", { "dependencies": { "d3-array": "2.5.0 - 3" } }, "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q=="], + + "d3-hierarchy": ["d3-hierarchy@3.1.2", "", {}, "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA=="], + + "d3-interpolate": ["d3-interpolate@3.0.1", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="], + + "d3-path": ["d3-path@3.1.0", "", {}, "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="], + + "d3-polygon": ["d3-polygon@3.0.1", "", {}, "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg=="], + + "d3-quadtree": ["d3-quadtree@3.0.1", "", {}, "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw=="], + + "d3-random": ["d3-random@3.0.1", "", {}, "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ=="], + + "d3-sankey": ["d3-sankey@0.12.3", "", { "dependencies": { "d3-array": "1 - 2", "d3-shape": "^1.2.0" } }, "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ=="], + + "d3-scale": ["d3-scale@4.0.2", "", { "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", "d3-interpolate": "1.2.0 - 3", "d3-time": "2.1.1 - 3", "d3-time-format": "2 - 4" } }, "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="], + + "d3-scale-chromatic": ["d3-scale-chromatic@3.1.0", "", { "dependencies": { "d3-color": "1 - 3", "d3-interpolate": "1 - 3" } }, "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ=="], + + "d3-selection": ["d3-selection@3.0.0", "", {}, "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ=="], + + "d3-shape": ["d3-shape@3.2.0", "", { "dependencies": { "d3-path": "^3.1.0" } }, "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA=="], + + "d3-time": ["d3-time@3.1.0", "", { "dependencies": { "d3-array": "2 - 3" } }, "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q=="], + + "d3-time-format": ["d3-time-format@4.1.0", "", { "dependencies": { "d3-time": "1 - 3" } }, "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg=="], + + "d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="], + + "d3-transition": ["d3-transition@3.0.1", "", { "dependencies": { "d3-color": "1 - 3", "d3-dispatch": "1 - 3", "d3-ease": "1 - 3", "d3-interpolate": "1 - 3", "d3-timer": "1 - 3" }, "peerDependencies": { "d3-selection": "2 - 3" } }, "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w=="], + + "d3-zoom": ["d3-zoom@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "2 - 3", "d3-transition": "2 - 3" } }, "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw=="], + + "dagre-d3-es": ["dagre-d3-es@7.0.14", "", { "dependencies": { "d3": "^7.9.0", "lodash-es": "^4.17.21" } }, "sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg=="], + + "dayjs": ["dayjs@1.11.20", "", {}, "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="], + + "delaunator": ["delaunator@5.1.0", "", { "dependencies": { "robust-predicates": "^3.0.2" } }, "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ=="], + + "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], + + "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], + + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + + "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], + + "discontinuous-range": ["discontinuous-range@1.0.0", "", {}, "sha512-c68LpLbO+7kP/b1Hr1qs8/BJ09F5khZGTxqxZuhzxpmwJKOgRFHJWIb9/KmqnqHhLdO55aOxFH/EGBvUQbL/RQ=="], + + "dompurify": ["dompurify@3.4.1", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-JahakDAIg1gyOm7dlgWSDjV4n7Ip2PKR55NIT6jrMfIgLFgWo81vdr1/QGqWtFNRqXP9UV71oVePtjqS2ebnPw=="], + + "dot-case": ["dot-case@3.0.4", "", { "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w=="], + + "dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="], + + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], + + "electron-to-chromium": ["electron-to-chromium@1.5.344", "", {}, "sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg=="], + + "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], + + "enhanced-resolve": ["enhanced-resolve@5.21.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-otxSQPw4lkOZWkHpB3zaEQs6gWYEsmX4xQF68ElXC/TWvGxGMSGOvoNbaLXm6/cS/fSfHtsEdw90y20PCd+sCA=="], + + "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], + + "error-ex": ["error-ex@1.3.4", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ=="], + + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-module-lexer": ["es-module-lexer@2.0.0", "", {}, "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw=="], + + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + + "es5-ext": ["es5-ext@0.10.64", "", { "dependencies": { "es6-iterator": "^2.0.3", "es6-symbol": "^3.1.3", "esniff": "^2.0.1", "next-tick": "^1.1.0" } }, "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg=="], + + "es6-iterator": ["es6-iterator@2.0.3", "", { "dependencies": { "d": "1", "es5-ext": "^0.10.35", "es6-symbol": "^3.1.1" } }, "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g=="], + + "es6-symbol": ["es6-symbol@3.1.4", "", { "dependencies": { "d": "^1.0.2", "ext": "^1.7.0" } }, "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg=="], + + "esast-util-from-estree": ["esast-util-from-estree@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "unist-util-position-from-estree": "^2.0.0" } }, "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ=="], + + "esast-util-from-js": ["esast-util-from-js@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "acorn": "^8.0.0", "esast-util-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw=="], + + "esbuild": ["esbuild@0.27.7", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.7", "@esbuild/android-arm": "0.27.7", "@esbuild/android-arm64": "0.27.7", "@esbuild/android-x64": "0.27.7", "@esbuild/darwin-arm64": "0.27.7", "@esbuild/darwin-x64": "0.27.7", "@esbuild/freebsd-arm64": "0.27.7", "@esbuild/freebsd-x64": "0.27.7", "@esbuild/linux-arm": "0.27.7", "@esbuild/linux-arm64": "0.27.7", "@esbuild/linux-ia32": "0.27.7", "@esbuild/linux-loong64": "0.27.7", "@esbuild/linux-mips64el": "0.27.7", "@esbuild/linux-ppc64": "0.27.7", "@esbuild/linux-riscv64": "0.27.7", "@esbuild/linux-s390x": "0.27.7", "@esbuild/linux-x64": "0.27.7", "@esbuild/netbsd-arm64": "0.27.7", "@esbuild/netbsd-x64": "0.27.7", "@esbuild/openbsd-arm64": "0.27.7", "@esbuild/openbsd-x64": "0.27.7", "@esbuild/openharmony-arm64": "0.27.7", "@esbuild/sunos-x64": "0.27.7", "@esbuild/win32-arm64": "0.27.7", "@esbuild/win32-ia32": "0.27.7", "@esbuild/win32-x64": "0.27.7" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "escape-carriage": ["escape-carriage@1.3.1", "", {}, "sha512-GwBr6yViW3ttx1kb7/Oh+gKQ1/TrhYwxKqVmg5gS+BK+Qe2KrOa/Vh7w3HPBvgGf0LfcDGoY9I6NHKoA5Hozhw=="], + + "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], + + "escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], + + "eslint-scope": ["eslint-scope@5.1.1", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" } }, "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw=="], + + "esniff": ["esniff@2.0.1", "", { "dependencies": { "d": "^1.0.1", "es5-ext": "^0.10.62", "event-emitter": "^0.3.5", "type": "^2.7.2" } }, "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg=="], + + "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], + + "estraverse": ["estraverse@4.3.0", "", {}, "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw=="], + + "estree-util-attach-comments": ["estree-util-attach-comments@3.0.0", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw=="], + + "estree-util-build-jsx": ["estree-util-build-jsx@3.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-walker": "^3.0.0" } }, "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ=="], + + "estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="], + + "estree-util-scope": ["estree-util-scope@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0" } }, "sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ=="], + + "estree-util-to-js": ["estree-util-to-js@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "astring": "^1.8.0", "source-map": "^0.7.0" } }, "sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg=="], + + "estree-util-value-to-estree": ["estree-util-value-to-estree@3.5.0", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-aMV56R27Gv3QmfmF1MY12GWkGzzeAezAX+UplqHVASfjc9wNzI/X6hC0S9oxq61WT4aQesLGslWP9tKk6ghRZQ=="], + + "estree-util-visit": ["estree-util-visit@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/unist": "^3.0.0" } }, "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww=="], + + "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + + "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], + + "event-emitter": ["event-emitter@0.3.5", "", { "dependencies": { "d": "1", "es5-ext": "~0.10.14" } }, "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA=="], + + "eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], + + "events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="], + + "eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="], + + "eventsource-parser": ["eventsource-parser@3.0.8", "", {}, "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ=="], + + "execa": ["execa@9.6.1", "", { "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", "cross-spawn": "^7.0.6", "figures": "^6.1.0", "get-stream": "^9.0.0", "human-signals": "^8.0.1", "is-plain-obj": "^4.1.0", "is-stream": "^4.0.1", "npm-run-path": "^6.0.0", "pretty-ms": "^9.2.0", "signal-exit": "^4.1.0", "strip-final-newline": "^4.0.0", "yoctocolors": "^2.1.1" } }, "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA=="], + + "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], + + "express-rate-limit": ["express-rate-limit@8.4.0", "", { "dependencies": { "ip-address": "10.1.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-gDK8yiqKxrGta+3WtON59arrrw6GLmadA1qoFgYXzdcch8fmKDID2XqO8itsi3f1wufXYPT51387dN6cvVBS3Q=="], + + "exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="], + + "ext": ["ext@1.7.0", "", { "dependencies": { "type": "^2.7.2" } }, "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw=="], + + "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], + + "fault": ["fault@2.0.1", "", { "dependencies": { "format": "^0.2.0" } }, "sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "fflate": ["fflate@0.4.8", "", {}, "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA=="], + + "figures": ["figures@6.1.0", "", { "dependencies": { "is-unicode-supported": "^2.0.0" } }, "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg=="], + + "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], + + "follow-redirects": ["follow-redirects@1.16.0", "", {}, "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw=="], + + "format": ["format@0.2.2", "", {}, "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww=="], + + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], + + "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-port": ["get-port@7.2.0", "", {}, "sha512-afP4W205ONCuMoPBqcR6PSXnzX35KTcJygfJfcp+QY+uwm3p20p1YczWXhlICIzGMCxYBQcySEcOgsJcrkyobg=="], + + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + + "get-stream": ["get-stream@9.0.1", "", { "dependencies": { "@sec-ant/readable-stream": "^0.4.1", "is-stream": "^4.0.1" } }, "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA=="], + + "get-tsconfig": ["get-tsconfig@4.14.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA=="], + + "github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="], + + "glob-to-regexp": ["glob-to-regexp@0.4.1", "", {}, "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="], + + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "hachure-fill": ["hachure-fill@0.5.2", "", {}, "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg=="], + + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "hasown": ["hasown@2.0.3", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="], + + "hast-util-heading-rank": ["hast-util-heading-rank@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-EJKb8oMUXVHcWZTDepnr+WNbfnXKFNf9duMesmr4S8SXTJBJ9M4Yok08pu9vxdJwdlGRhVumk9mEhkEvKGifwA=="], + + "hast-util-is-element": ["hast-util-is-element@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g=="], + + "hast-util-to-estree": ["hast-util-to-estree@3.1.3", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-attach-comments": "^3.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w=="], + + "hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="], + + "hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="], + + "hast-util-to-string": ["hast-util-to-string@3.0.1", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A=="], + + "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="], + + "hono": ["hono@4.12.15", "", {}, "sha512-qM0jDhFEaCBb4TxoW7f53Qrpv9RBiayUHo0S52JudprkhvpjIrGoU1mnnr29Fvd1U335ZFPZQY1wlkqgfGXyLg=="], + + "html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="], + + "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], + + "http-proxy": ["http-proxy@1.18.1", "", { "dependencies": { "eventemitter3": "^4.0.0", "follow-redirects": "^1.0.0", "requires-port": "^1.0.0" } }, "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ=="], + + "human-signals": ["human-signals@8.0.1", "", {}, "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ=="], + + "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + + "idb-keyval": ["idb-keyval@6.2.2", "", {}, "sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg=="], + + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + + "image-size": ["image-size@2.0.2", "", { "bin": { "image-size": "bin/image-size.js" } }, "sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w=="], + + "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], + + "incur": ["incur@0.3.25", "", { "dependencies": { "@cfworker/json-schema": "^4.1.1", "@modelcontextprotocol/server": "^2.0.0-alpha.2", "@toon-format/toon": "^2.1.0", "tokenx": "^1.3.0", "yaml": "^2.8.2", "zod": "^4.3.6" }, "bin": { "incur": "dist/bin.js", "incur.src": "src/bin.ts" } }, "sha512-jrSkzauM42ilbQJ6THVkAY6dTulkyVW0sZpVHdA8gfiBwrLrLnLUf8U3bAOegAKBIMSOFgk1idchgu9xm9HMng=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="], + + "internmap": ["internmap@1.0.1", "", {}, "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw=="], + + "intersection-observer": ["intersection-observer@0.10.0", "", {}, "sha512-fn4bQ0Xq8FTej09YC/jqKZwtijpvARlRp6wxL5WTA6yPe2YWSJ5RJh7Nm79rK2qB0wr6iDQzH60XGq5V/7u8YQ=="], + + "ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="], + + "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], + + "is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="], + + "is-alphanumerical": ["is-alphanumerical@2.0.1", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="], + + "is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], + + "is-decimal": ["is-decimal@2.0.1", "", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="], + + "is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="], + + "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], + + "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], + + "is-stream": ["is-stream@4.0.1", "", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="], + + "is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "isows": ["isows@1.0.7", "", { "peerDependencies": { "ws": "*" } }, "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg=="], + + "jest-worker": ["jest-worker@27.5.1", "", { "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" } }, "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg=="], + + "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], + + "jose": ["jose@6.2.2", "", {}, "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ=="], + + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + + "json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="], + + "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="], + + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + + "katex": ["katex@0.16.45", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-pQpZbdBu7wCTmQUh7ufPmLr0pFoObnGUoL/yhtwJDgmmQpbkg/0HSVti25Fu4rmd1oCR6NGWe9vqTWuWv3GcNA=="], + + "khroma": ["khroma@2.1.0", "", {}, "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw=="], + + "langium": ["langium@4.2.2", "", { "dependencies": { "@chevrotain/regexp-to-ast": "~12.0.0", "chevrotain": "~12.0.0", "chevrotain-allstar": "~0.4.1", "vscode-languageserver": "~9.0.1", "vscode-languageserver-textdocument": "~1.0.11", "vscode-uri": "~3.1.0" } }, "sha512-JUshTRAfHI4/MF9dH2WupvjSXyn8JBuUEWazB8ZVJUtXutT0doDlAv1XKbZ1Pb5sMexa8FF4CFBc0iiul7gbUQ=="], + + "layout-base": ["layout-base@1.0.2", "", {}, "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg=="], + + "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], + + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], + + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], + + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="], + + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="], + + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="], + + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="], + + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="], + + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="], + + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="], + + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="], + + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], + + "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], + + "loader-runner": ["loader-runner@4.3.2", "", {}, "sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w=="], + + "local-pkg": ["local-pkg@1.1.2", "", { "dependencies": { "mlly": "^1.7.4", "pkg-types": "^2.3.0", "quansync": "^0.2.11" } }, "sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A=="], + + "lodash-es": ["lodash-es@4.18.1", "", {}, "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A=="], + + "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], + + "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], + + "lower-case": ["lower-case@2.0.2", "", { "dependencies": { "tslib": "^2.0.3" } }, "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg=="], + + "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + + "lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="], + + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "markdown-extensions": ["markdown-extensions@2.0.0", "", {}, "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q=="], + + "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="], + + "marked": ["marked@16.4.2", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA=="], + + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "mdast-util-directive": ["mdast-util-directive@3.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-I3fNFt+DHmpWCYAT7quoM6lHf9wuqtI+oCOfvILnoicNIqjh5E3dEJWiXuYME2gNe8vl1iMQwyUHa7bgFmak6Q=="], + + "mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="], + + "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.3", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="], + + "mdast-util-frontmatter": ["mdast-util-frontmatter@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "escape-string-regexp": "^5.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-extension-frontmatter": "^2.0.0" } }, "sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA=="], + + "mdast-util-gfm": ["mdast-util-gfm@3.1.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="], + + "mdast-util-gfm-autolink-literal": ["mdast-util-gfm-autolink-literal@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-find-and-replace": "^3.0.0", "micromark-util-character": "^2.0.0" } }, "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ=="], + + "mdast-util-gfm-footnote": ["mdast-util-gfm-footnote@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0" } }, "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ=="], + + "mdast-util-gfm-strikethrough": ["mdast-util-gfm-strikethrough@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg=="], + + "mdast-util-gfm-table": ["mdast-util-gfm-table@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "markdown-table": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg=="], + + "mdast-util-gfm-task-list-item": ["mdast-util-gfm-task-list-item@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ=="], + + "mdast-util-mdx": ["mdast-util-mdx@3.0.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w=="], + + "mdast-util-mdx-expression": ["mdast-util-mdx-expression@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ=="], + + "mdast-util-mdx-jsx": ["mdast-util-mdx-jsx@3.2.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-stringify-position": "^4.0.0", "vfile-message": "^4.0.0" } }, "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q=="], + + "mdast-util-mdxjs-esm": ["mdast-util-mdxjs-esm@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg=="], + + "mdast-util-phrasing": ["mdast-util-phrasing@4.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "unist-util-is": "^6.0.0" } }, "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w=="], + + "mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="], + + "mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA=="], + + "mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="], + + "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], + + "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], + + "merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="], + + "mermaid": ["mermaid@11.14.0", "", { "dependencies": { "@braintree/sanitize-url": "^7.1.1", "@iconify/utils": "^3.0.2", "@mermaid-js/parser": "^1.1.0", "@types/d3": "^7.4.3", "@upsetjs/venn.js": "^2.0.0", "cytoscape": "^3.33.1", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.2.0", "d3": "^7.9.0", "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.14", "dayjs": "^1.11.19", "dompurify": "^3.3.1", "katex": "^0.16.25", "khroma": "^2.1.0", "lodash-es": "^4.17.23", "marked": "^16.3.0", "roughjs": "^4.6.6", "stylis": "^4.3.6", "ts-dedent": "^2.2.0", "uuid": "^11.1.0" } }, "sha512-GSGloRsBs+JINmmhl0JDwjpuezCsHB4WGI4NASHxL3fHo3o/BRXTxhDLKnln8/Q0lRFRyDdEjmk1/d5Sn1Xz8g=="], + + "micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="], + + "micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="], + + "micromark-extension-directive": ["micromark-extension-directive@4.0.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "parse-entities": "^4.0.0" } }, "sha512-/C2nqVmXXmiseSSuCdItCMho7ybwwop6RrrRPk0KbOHW21JKoCldC+8rFOaundDoRBUWBnJJcxeA/Kvi34WQXg=="], + + "micromark-extension-frontmatter": ["micromark-extension-frontmatter@2.0.0", "", { "dependencies": { "fault": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg=="], + + "micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="], + + "micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@2.1.0", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="], + + "micromark-extension-gfm-footnote": ["micromark-extension-gfm-footnote@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw=="], + + "micromark-extension-gfm-strikethrough": ["micromark-extension-gfm-strikethrough@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw=="], + + "micromark-extension-gfm-table": ["micromark-extension-gfm-table@2.1.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg=="], + + "micromark-extension-gfm-tagfilter": ["micromark-extension-gfm-tagfilter@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg=="], + + "micromark-extension-gfm-task-list-item": ["micromark-extension-gfm-task-list-item@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw=="], + + "micromark-extension-mdx-expression": ["micromark-extension-mdx-expression@3.0.1", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q=="], + + "micromark-extension-mdx-jsx": ["micromark-extension-mdx-jsx@3.0.2", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ=="], + + "micromark-extension-mdx-md": ["micromark-extension-mdx-md@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ=="], + + "micromark-extension-mdxjs": ["micromark-extension-mdxjs@3.0.0", "", { "dependencies": { "acorn": "^8.0.0", "acorn-jsx": "^5.0.0", "micromark-extension-mdx-expression": "^3.0.0", "micromark-extension-mdx-jsx": "^3.0.0", "micromark-extension-mdx-md": "^2.0.0", "micromark-extension-mdxjs-esm": "^3.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ=="], + + "micromark-extension-mdxjs-esm": ["micromark-extension-mdxjs-esm@3.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-position-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A=="], + + "micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="], + + "micromark-factory-label": ["micromark-factory-label@2.0.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="], + + "micromark-factory-mdx-expression": ["micromark-factory-mdx-expression@2.0.3", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-position-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ=="], + + "micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="], + + "micromark-factory-title": ["micromark-factory-title@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw=="], + + "micromark-factory-whitespace": ["micromark-factory-whitespace@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ=="], + + "micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="], + + "micromark-util-chunked": ["micromark-util-chunked@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA=="], + + "micromark-util-classify-character": ["micromark-util-classify-character@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q=="], + + "micromark-util-combine-extensions": ["micromark-util-combine-extensions@2.0.1", "", { "dependencies": { "micromark-util-chunked": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg=="], + + "micromark-util-decode-numeric-character-reference": ["micromark-util-decode-numeric-character-reference@2.0.2", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw=="], + + "micromark-util-decode-string": ["micromark-util-decode-string@2.0.1", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ=="], + + "micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="], + + "micromark-util-events-to-acorn": ["micromark-util-events-to-acorn@2.0.3", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg=="], + + "micromark-util-html-tag-name": ["micromark-util-html-tag-name@2.0.1", "", {}, "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA=="], + + "micromark-util-normalize-identifier": ["micromark-util-normalize-identifier@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q=="], + + "micromark-util-resolve-all": ["micromark-util-resolve-all@2.0.1", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg=="], + + "micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="], + + "micromark-util-subtokenize": ["micromark-util-subtokenize@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA=="], + + "micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="], + + "micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="], + + "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], + + "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], + + "minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], + + "minisearch": ["minisearch@7.2.0", "", {}, "sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg=="], + + "minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="], + + "mipd": ["mipd@0.0.7", "", { "peerDependencies": { "typescript": ">=5.0.4" }, "optionalPeers": ["typescript"] }, "sha512-aAPZPNDQ3uMTdKbuO2YmAw2TxLHO0moa4YKAyETM/DTj5FloZo+a+8tU+iv4GmW+sOxKLSRwcSFuczk+Cpt6fg=="], + + "mkdirp": ["mkdirp@3.0.1", "", { "bin": { "mkdirp": "dist/cjs/src/bin.js" } }, "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg=="], + + "mlly": ["mlly@1.8.2", "", { "dependencies": { "acorn": "^8.16.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.3" } }, "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA=="], + + "monaco-editor": ["monaco-editor@0.55.1", "", { "dependencies": { "dompurify": "3.2.7", "marked": "14.0.0" } }, "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A=="], + + "moo": ["moo@0.5.3", "", {}, "sha512-m2fmM2dDm7GZQsY7KK2cme8agi+AAljILjQnof7p1ZMDe6dQ4bdnSMx0cPppudoeNv5hEFQirN6u+O4fDE0IWA=="], + + "mppx": ["mppx@0.5.17", "", { "dependencies": { "incur": "^0.3.25", "ox": "0.14.15", "zod": "^4.3.6" }, "peerDependencies": { "@modelcontextprotocol/sdk": ">=1.25.0", "elysia": ">=1", "express": ">=5", "hono": ">=4.12.14", "viem": ">=2.47.5" }, "optionalPeers": ["@modelcontextprotocol/sdk", "elysia", "express", "hono"], "bin": { "mppx": "dist/bin.js", "mppx.src": "src/bin.ts" } }, "sha512-4iZwc9XZclCsv8nzQyw32rdaWYg5eLRj4gNjq9l5d+6NuArazZSvjHsI5SQmtzDF6WsssI6E5hSIecCQ9LDA+w=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], + + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + + "nearley": ["nearley@2.20.1", "", { "dependencies": { "commander": "^2.19.0", "moo": "^0.5.0", "railroad-diagrams": "^1.0.0", "randexp": "0.4.6" }, "bin": { "nearleyc": "bin/nearleyc.js", "nearley-test": "bin/nearley-test.js", "nearley-unparse": "bin/nearley-unparse.js", "nearley-railroad": "bin/nearley-railroad.js" } }, "sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ=="], + + "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], + + "neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="], + + "next-tick": ["next-tick@1.1.0", "", {}, "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ=="], + + "no-case": ["no-case@3.0.4", "", { "dependencies": { "lower-case": "^2.0.2", "tslib": "^2.0.3" } }, "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg=="], + + "node-releases": ["node-releases@2.0.38", "", {}, "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw=="], + + "npm-run-path": ["npm-run-path@6.0.0", "", { "dependencies": { "path-key": "^4.0.0", "unicorn-magic": "^0.3.0" } }, "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA=="], + + "nuqs": ["nuqs@2.8.9", "", { "dependencies": { "@standard-schema/spec": "1.0.0" }, "peerDependencies": { "@remix-run/react": ">=2", "@tanstack/react-router": "^1", "next": ">=14.2.0", "react": ">=18.2.0 || ^19.0.0-0", "react-router": "^5 || ^6 || ^7", "react-router-dom": "^5 || ^6 || ^7" }, "optionalPeers": ["@remix-run/react", "@tanstack/react-router", "next", "react-router", "react-router-dom"] }, "sha512-8ou6AEwsxMWSYo2qkfZtYFVzngwbKmg4c00HVxC1fF6CEJv3Fwm6eoZmfVPALB+vw8Udo7KL5uy96PFcYe1BIQ=="], + + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + + "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], + + "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="], + + "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], + + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + + "oniguruma-parser": ["oniguruma-parser@0.12.2", "", {}, "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw=="], + + "oniguruma-to-es": ["oniguruma-to-es@4.3.6", "", { "dependencies": { "oniguruma-parser": "^0.12.2", "regex": "^6.1.0", "regex-recursion": "^6.0.2" } }, "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA=="], + + "outvariant": ["outvariant@1.4.0", "", {}, "sha512-AlWY719RF02ujitly7Kk/0QlV+pXGFDHrHf9O2OKqyqgBieaPOIeuSkL8sRK6j2WK+/ZAURq2kZsY0d8JapUiw=="], + + "ox": ["ox@0.14.18", "", { "dependencies": { "@adraffy/ens-normalize": "^1.11.0", "@noble/ciphers": "^1.3.0", "@noble/curves": "1.9.1", "@noble/hashes": "^1.8.0", "@scure/bip32": "^1.7.0", "@scure/bip39": "^1.6.0", "abitype": "^1.2.3", "eventemitter3": "5.0.1" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-1Irk/tvMsw7xJDuCTT/u9azSjz0YX9hrYFgJOacIuFwibaW2zZBXAMrpzegndYb5o8GLpxB6/0qro4/c40q6VQ=="], + + "package-manager-detector": ["package-manager-detector@1.6.0", "", {}, "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA=="], + + "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + + "parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="], + + "parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="], + + "parse-ms": ["parse-ms@4.0.0", "", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="], + + "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], + + "path-data-parser": ["path-data-parser@0.1.0", "", {}, "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="], + + "path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="], + + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], + + "pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="], + + "pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="], + + "pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="], + + "playwright": ["playwright@1.59.1", "", { "dependencies": { "playwright-core": "1.59.1" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw=="], + + "playwright-core": ["playwright-core@1.59.1", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg=="], + + "points-on-curve": ["points-on-curve@0.2.0", "", {}, "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A=="], + + "points-on-path": ["points-on-path@0.2.1", "", { "dependencies": { "path-data-parser": "0.1.0", "points-on-curve": "0.2.0" } }, "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g=="], + + "postcss": ["postcss@8.5.10", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ=="], + + "posthog-js": ["posthog-js@1.371.4", "", { "dependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/api-logs": "^0.208.0", "@opentelemetry/exporter-logs-otlp-http": "^0.208.0", "@opentelemetry/resources": "^2.2.0", "@opentelemetry/sdk-logs": "^0.208.0", "@posthog/core": "1.27.3", "@posthog/types": "1.371.4", "core-js": "^3.38.1", "dompurify": "^3.3.2", "fflate": "^0.4.8", "preact": "^10.28.2", "query-selector-shadow-dom": "^1.0.1", "web-vitals": "^5.1.0" } }, "sha512-ECyI1AlynGmLzKyXH3g8SHOSxCnpGbnwi5NRU98TOKrAY4NDCUj6+cxrb4XwFeXkZoXk54DV5sPqjnflAUNN3g=="], + + "posthog-node": ["posthog-node@5.30.2", "", { "dependencies": { "@posthog/core": "1.27.3" }, "peerDependencies": { "rxjs": "^7.0.0" }, "optionalPeers": ["rxjs"] }, "sha512-rgSwv1tAPz6sEycKfgGvfGeJc+xnPeYBFAX5BwePUt0Fi83V8wg1OaZ0NTP8yoxdBATO5kIbRJIL/OKp3KJeAA=="], + + "preact": ["preact@10.29.1", "", {}, "sha512-gQCLc/vWroE8lIpleXtdJhTFDogTdZG9AjMUpVkDf2iTCNwYNWA+u16dL41TqUDJO4gm2IgrcMv3uTpjd4Pwmg=="], + + "pretty-ms": ["pretty-ms@9.3.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ=="], + + "prool": ["prool@0.2.4", "", { "dependencies": { "change-case": "5.4.4", "eventemitter3": "^5.0.1", "execa": "^9.1.0", "get-port": "^7.1.0", "http-proxy": "^1.18.1", "tar": "7.2.0" }, "peerDependencies": { "@pimlico/alto": "*", "testcontainers": ">=11.10.0" }, "optionalPeers": ["@pimlico/alto", "testcontainers"] }, "sha512-KAGs6e++7MJNQ/vq8Xrk6akz0lRk6AmhuGzSHkluX3kwVj2XjNDDOYSINZwahRv3xfSD0rXYv3iA/2vXw7z47w=="], + + "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], + + "protobufjs": ["protobufjs@7.5.5", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-3wY1AxV+VBNW8Yypfd1yQY9pXnqTAN+KwQxL8iYm3/BjKYMNg4i0owhEe26PWDOMaIrzeeF98Lqd5NGz4omiIg=="], + + "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], + + "qs": ["qs@6.15.1", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg=="], + + "quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="], + + "query-selector-shadow-dom": ["query-selector-shadow-dom@1.0.1", "", {}, "sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw=="], + + "railroad-diagrams": ["railroad-diagrams@1.0.0", "", {}, "sha512-cz93DjNeLY0idrCNOH6PviZGRN9GJhsdm9hpn1YCS879fj4W+x5IFJhhkRZcwVgMmFF7R82UA/7Oh+R8lLZg6A=="], + + "randexp": ["randexp@0.4.6", "", { "dependencies": { "discontinuous-range": "1.0.0", "ret": "~0.1.10" } }, "sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ=="], + + "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], + + "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], + + "react": ["react@19.2.5", "", {}, "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA=="], + + "react-devtools-inline": ["react-devtools-inline@4.4.0", "", { "dependencies": { "es6-symbol": "^3" } }, "sha512-ES0GolSrKO8wsKbsEkVeiR/ZAaHQTY4zDh1UW8DImVmm8oaGLl3ijJDvSGe+qDRKPZdPRnDtWWnSvvrgxXdThQ=="], + + "react-dom": ["react-dom@19.2.5", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.5" } }, "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag=="], + + "react-error-boundary": ["react-error-boundary@6.1.1", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-BrYwPOdXi5mqkk5lw+Uvt0ThHx32rCt3BkukS4X23A2AIWDPSGX6iaWTc0y9TU/mHDA/6qOSGel+B2ERkOvD1w=="], + + "react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="], + + "react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="], + + "react-server-dom-webpack": ["react-server-dom-webpack@19.2.5", "", { "dependencies": { "acorn-loose": "^8.3.0", "neo-async": "^2.6.1", "webpack-sources": "^3.2.0" }, "peerDependencies": { "react": "^19.2.5", "react-dom": "^19.2.5", "webpack": "^5.59.0" } }, "sha512-bYhdd2cZJhXHqyJBoloYaJrn8MrL9Egf3ZZVn0OrIODCCORm2goFD7C+xszf6xgfsSJi0rtgB/ichcuHfkJ4yQ=="], + + "recma-build-jsx": ["recma-build-jsx@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-build-jsx": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew=="], + + "recma-jsx": ["recma-jsx@1.0.1", "", { "dependencies": { "acorn-jsx": "^5.0.0", "estree-util-to-js": "^2.0.0", "recma-parse": "^1.0.0", "recma-stringify": "^1.0.0", "unified": "^11.0.0" }, "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w=="], + + "recma-parse": ["recma-parse@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "esast-util-from-js": "^2.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ=="], + + "recma-stringify": ["recma-stringify@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-to-js": "^2.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g=="], + + "regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="], + + "regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="], + + "regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="], + + "rehype-autolink-headings": ["rehype-autolink-headings@7.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "hast-util-heading-rank": "^3.0.0", "hast-util-is-element": "^3.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-rItO/pSdvnvsP4QRB1pmPiNHUskikqtPojZKJPPPAVx9Hj8i8TwMBhofrrAYRhYOOBZH9tgmG5lPqDLuIWPWmw=="], + + "rehype-recma": ["rehype-recma@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "hast-util-to-estree": "^3.0.0" } }, "sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw=="], + + "rehype-slug": ["rehype-slug@6.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "github-slugger": "^2.0.0", "hast-util-heading-rank": "^3.0.0", "hast-util-to-string": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-lWyvf/jwu+oS5+hL5eClVd3hNdmwM1kAC0BUvEGD19pajQMIzcNUd/k9GsfQ+FfECvX+JE+e9/btsKH0EjJT6A=="], + + "rehype-stringify": ["rehype-stringify@10.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-to-html": "^9.0.0", "unified": "^11.0.0" } }, "sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA=="], + + "remark-directive": ["remark-directive@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-directive": "^3.0.0", "micromark-extension-directive": "^4.0.0", "unified": "^11.0.0" } }, "sha512-7sxn4RfF1o3izevPV1DheyGDD6X4c9hrGpfdUpm7uC++dqrnJxIZVkk7CoKqcLm0VUMAuOol7Mno3m6g8cfMuA=="], + + "remark-frontmatter": ["remark-frontmatter@5.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-frontmatter": "^2.0.0", "micromark-extension-frontmatter": "^2.0.0", "unified": "^11.0.0" } }, "sha512-XTFYvNASMe5iPN0719nPrdItC9aU0ssC4v14mH1BCi1u0n1gAocqcujWUrByftZTbLhRtiKRyjYTSIOcr69UVQ=="], + + "remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="], + + "remark-mdx": ["remark-mdx@3.1.1", "", { "dependencies": { "mdast-util-mdx": "^3.0.0", "micromark-extension-mdxjs": "^3.0.0" } }, "sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg=="], + + "remark-mdx-frontmatter": ["remark-mdx-frontmatter@5.2.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "estree-util-value-to-estree": "^3.0.0", "toml": "^3.0.0", "unified": "^11.0.0", "unist-util-mdx-define": "^1.0.0", "yaml": "^2.0.0" } }, "sha512-U/hjUYTkQqNjjMRYyilJgLXSPF65qbLPdoESOkXyrwz2tVyhAnm4GUKhfXqOOS9W34M3545xEMq+aMpHgVjEeQ=="], + + "remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="], + + "remark-rehype": ["remark-rehype@11.1.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw=="], + + "remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="], + + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + + "requires-port": ["requires-port@1.0.0", "", {}, "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="], + + "reselect": ["reselect@5.1.1", "", {}, "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="], + + "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + + "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], + + "ret": ["ret@0.1.15", "", {}, "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg=="], + + "robust-predicates": ["robust-predicates@3.0.3", "", {}, "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA=="], + + "rollup": ["rollup@4.60.2", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.2", "@rollup/rollup-android-arm64": "4.60.2", "@rollup/rollup-darwin-arm64": "4.60.2", "@rollup/rollup-darwin-x64": "4.60.2", "@rollup/rollup-freebsd-arm64": "4.60.2", "@rollup/rollup-freebsd-x64": "4.60.2", "@rollup/rollup-linux-arm-gnueabihf": "4.60.2", "@rollup/rollup-linux-arm-musleabihf": "4.60.2", "@rollup/rollup-linux-arm64-gnu": "4.60.2", "@rollup/rollup-linux-arm64-musl": "4.60.2", "@rollup/rollup-linux-loong64-gnu": "4.60.2", "@rollup/rollup-linux-loong64-musl": "4.60.2", "@rollup/rollup-linux-ppc64-gnu": "4.60.2", "@rollup/rollup-linux-ppc64-musl": "4.60.2", "@rollup/rollup-linux-riscv64-gnu": "4.60.2", "@rollup/rollup-linux-riscv64-musl": "4.60.2", "@rollup/rollup-linux-s390x-gnu": "4.60.2", "@rollup/rollup-linux-x64-gnu": "4.60.2", "@rollup/rollup-linux-x64-musl": "4.60.2", "@rollup/rollup-openbsd-x64": "4.60.2", "@rollup/rollup-openharmony-arm64": "4.60.2", "@rollup/rollup-win32-arm64-msvc": "4.60.2", "@rollup/rollup-win32-ia32-msvc": "4.60.2", "@rollup/rollup-win32-x64-gnu": "4.60.2", "@rollup/rollup-win32-x64-msvc": "4.60.2", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ=="], + + "roughjs": ["roughjs@4.6.6", "", { "dependencies": { "hachure-fill": "^0.5.2", "path-data-parser": "^0.1.0", "points-on-curve": "^0.2.0", "points-on-path": "^0.2.1" } }, "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ=="], + + "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], + + "rsc-html-stream": ["rsc-html-stream@0.0.7", "", {}, "sha512-v9+fuY7usTgvXdNl8JmfXCvSsQbq2YMd60kOeeMIqCJFZ69fViuIxztHei7v5mlMMa2h3SqS+v44Gu9i9xANZA=="], + + "rw": ["rw@1.3.3", "", {}, "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + + "schema-utils": ["schema-utils@4.3.3", "", { "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", "ajv-formats": "^2.1.1", "ajv-keywords": "^5.1.0" } }, "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA=="], + + "scule": ["scule@1.3.0", "", {}, "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g=="], + + "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], + + "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], + + "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "shiki": ["shiki@3.23.0", "", { "dependencies": { "@shikijs/core": "3.23.0", "@shikijs/engine-javascript": "3.23.0", "@shikijs/engine-oniguruma": "3.23.0", "@shikijs/langs": "3.23.0", "@shikijs/themes": "3.23.0", "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA=="], + + "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], + + "side-channel-list": ["side-channel-list@1.0.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4" } }, "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w=="], + + "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], + + "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], + + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "snake-case": ["snake-case@3.0.4", "", { "dependencies": { "dot-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg=="], + + "sonner": ["sonner@2.0.7", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w=="], + + "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], + + "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], + + "sql-formatter": ["sql-formatter@15.7.3", "", { "dependencies": { "argparse": "^2.0.1", "nearley": "^2.20.1" }, "bin": { "sql-formatter": "bin/sql-formatter-cli.cjs" } }, "sha512-5+zl9Nqg5aNjss0tb1G+StpC4dJKbjv3+g8CL/+V+00PfZop+2RKGyi53ScFl0dr+Dkx1LjmUO54Q3N7K3EtMw=="], + + "srvx": ["srvx@0.11.15", "", { "bin": { "srvx": "bin/srvx.mjs" } }, "sha512-iXsux0UcOjdvs0LCMa2Ws3WwcDUozA3JN3BquNXkaFPP7TpRqgunKdEgoZ/uwb1J6xaYHfxtz9Twlh6yzwM6Tg=="], + + "state-local": ["state-local@1.0.7", "", {}, "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w=="], + + "static-browser-server": ["static-browser-server@1.0.3", "", { "dependencies": { "@open-draft/deferred-promise": "^2.1.0", "dotenv": "^16.0.3", "mime-db": "^1.52.0", "outvariant": "^1.3.0" } }, "sha512-ZUyfgGDdFRbZGGJQ1YhiM930Yczz5VlbJObrQLlk24+qNHVQx4OlLcYswEUo3bIyNAbQUIUR9Yr5/Hqjzqb4zA=="], + + "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], + + "strict-event-emitter": ["strict-event-emitter@0.4.6", "", {}, "sha512-12KWeb+wixJohmnwNFerbyiBrAlq5qJLwIt38etRtKtmmHyDSoGlIqFE9wx+4IwG0aDjI7GV8tc8ZccjWZZtTg=="], + + "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], + + "strip-final-newline": ["strip-final-newline@4.0.0", "", {}, "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw=="], + + "strip-literal": ["strip-literal@3.1.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg=="], + + "style-mod": ["style-mod@4.1.3", "", {}, "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ=="], + + "style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="], + + "style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="], + + "stylis": ["stylis@4.4.0", "", {}, "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA=="], + + "sucrase": ["sucrase@3.35.1", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "tinyglobby": "^0.2.11", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw=="], + + "supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], + + "svg-parser": ["svg-parser@2.0.4", "", {}, "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ=="], + + "tailwind-merge": ["tailwind-merge@3.5.0", "", {}, "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A=="], + + "tailwindcss": ["tailwindcss@4.2.4", "", {}, "sha512-HhKppgO81FQof5m6TEnuBWCZGgfRAWbaeOaGT00KOy/Pf/j6oUihdvBpA7ltCeAvZpFhW3j0PTclkxsd4IXYDA=="], + + "tailwindcss-logical": ["tailwindcss-logical@4.2.0", "", { "peerDependencies": { "tailwindcss": ">=4.0.0" } }, "sha512-G+7NFU8xiMczeaykFRvaDbfbpqvFWv05YG6RtS2klW1wvTRkUs96P/e+Zy/Qse6y0HL8tLQ+da8M7trDfu12sw=="], + + "tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="], + + "tar": ["tar@7.2.0", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.0", "minizlib": "^3.0.1", "mkdirp": "^3.0.1", "yallist": "^5.0.0" } }, "sha512-hctwP0Nb4AB60bj8WQgRYaMOuJYRAPMGiQUAotms5igN8ppfQM+IvjQ5HcKu1MaZh2Wy2KWVTe563Yj8dfc14w=="], + + "terser": ["terser@5.46.2", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-uxfo9fPcSgLDYob/w1FuL0c99MWiJDnv+5qXSQc5+Ki5NjVNsYi66INnMFBjf6uFz6OnX12piJQPF4IpjJTNTw=="], + + "terser-webpack-plugin": ["terser-webpack-plugin@5.5.0", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", "schema-utils": "^4.3.0", "terser": "^5.31.1" }, "peerDependencies": { "webpack": "^5.1.0" } }, "sha512-UYhptBwhWvfIjKd/UuFo6D8uq9xpGLDK+z8EDsj/zWhrTaH34cKEbrkMKfV5YWqGBvAYA3tlzZbs2R+qYrbQJA=="], + + "thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="], + + "thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="], + + "tinyexec": ["tinyexec@1.1.1", "", {}, "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg=="], + + "tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], + + "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], + + "tokenx": ["tokenx@1.3.0", "", {}, "sha512-NLdXTEZkKiO0gZuLtMoZKjCXTREXeZZt8nnnNeyoXtNZAfG/GKGSbQtLU5STspc0rMSwcA+UJfWZkbNU01iKmQ=="], + + "toml": ["toml@3.0.0", "", {}, "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w=="], + + "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], + + "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="], + + "ts-dedent": ["ts-dedent@2.2.0", "", {}, "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ=="], + + "ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "tsx": ["tsx@4.21.0", "", { "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="], + + "turbo-stream": ["turbo-stream@3.2.0", "", {}, "sha512-EK+bZ9UVrVh7JLslVFOV0GEMsociOqVOvEMTAd4ixMyffN5YNIEdLZWXUx5PJqDbTxSIBWw04HS9gCY4frYQDQ=="], + + "twoslash": ["twoslash@0.3.8", "", { "dependencies": { "@typescript/vfs": "^1.6.4", "twoslash-protocol": "0.3.8" }, "peerDependencies": { "typescript": "^5.5.0 || ^6.0.0" } }, "sha512-OeDz0kDl8sqPUN3nr7gqcvOs70f5lZsdhKYTX3/SgB9OvdadzzoYJI/4SBXhXV1HG8E9fLc+e17itoRYTxmoig=="], + + "twoslash-protocol": ["twoslash-protocol@0.3.8", "", {}, "sha512-HmvAHoiEviK8LqvAQyc9/irkdvwTUiR1fHmNwH/0gq8EHxyBt4PWVPixjEXg6wJu1u6yBrILEWXGK9Kw58/8yQ=="], + + "type": ["type@2.7.3", "", {}, "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ=="], + + "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="], + + "undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], + + "unicorn-magic": ["unicorn-magic@0.3.0", "", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="], + + "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="], + + "unimport": ["unimport@5.7.0", "", { "dependencies": { "acorn": "^8.16.0", "escape-string-regexp": "^5.0.0", "estree-walker": "^3.0.3", "local-pkg": "^1.1.2", "magic-string": "^0.30.21", "mlly": "^1.8.0", "pathe": "^2.0.3", "picomatch": "^4.0.3", "pkg-types": "^2.3.0", "scule": "^1.3.0", "strip-literal": "^3.1.0", "tinyglobby": "^0.2.15", "unplugin": "^2.3.11", "unplugin-utils": "^0.3.1" } }, "sha512-njnL6sp8lEA8QQbZrt+52p/g4X0rw3bnGGmUcJnt1jeG8+iiqO779aGz0PirCtydAIVcuTBRlJ52F0u46z309Q=="], + + "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="], + + "unist-util-mdx-define": ["unist-util-mdx-define@1.1.2", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-9ncH7i7TN5Xn7/tzX5bE3rXgz1X/u877gYVAUB3mLeTKYJmQHmqKTDBi6BTGXV7AeolBCI9ErcVsOt2qryoD0g=="], + + "unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="], + + "unist-util-position-from-estree": ["unist-util-position-from-estree@2.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ=="], + + "unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="], + + "unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="], + + "unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="], + + "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], + + "unplugin": ["unplugin@2.3.11", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "acorn": "^8.15.0", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww=="], + + "unplugin-auto-import": ["unplugin-auto-import@21.0.0", "", { "dependencies": { "local-pkg": "^1.1.2", "magic-string": "^0.30.21", "picomatch": "^4.0.3", "unimport": "^5.6.0", "unplugin": "^2.3.11", "unplugin-utils": "^0.3.1" }, "peerDependencies": { "@nuxt/kit": "^4.0.0", "@vueuse/core": "*" }, "optionalPeers": ["@nuxt/kit", "@vueuse/core"] }, "sha512-vWuC8SwqJmxZFYwPojhOhOXDb5xFhNNcEVb9K/RFkyk/3VnfaOjzitWN7v+8DEKpMjSsY2AEGXNgt6I0yQrhRQ=="], + + "unplugin-icons": ["unplugin-icons@23.0.1", "", { "dependencies": { "@antfu/install-pkg": "^1.1.0", "@iconify/utils": "^3.1.0", "local-pkg": "^1.1.2", "obug": "^2.1.1", "unplugin": "^2.3.11" }, "peerDependencies": { "@svgr/core": ">=7.0.0", "@svgx/core": "^1.0.1", "@vue/compiler-sfc": "^3.0.2", "svelte": "^3.0.0 || ^4.0.0 || ^5.0.0" }, "optionalPeers": ["@svgr/core", "@svgx/core", "@vue/compiler-sfc", "svelte"] }, "sha512-rv0XEJepajKzDLvRUWASM8K+8+/CCfZn2jtogXqg6RIp7kpatRc/aFrVJn8ANQA09e++lPEEv9yX8cC9enc+QQ=="], + + "unplugin-utils": ["unplugin-utils@0.3.1", "", { "dependencies": { "pathe": "^2.0.3", "picomatch": "^4.0.3" } }, "sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog=="], + + "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], + + "urlpattern-polyfill": ["urlpattern-polyfill@10.1.0", "", {}, "sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw=="], + + "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], + + "uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="], + + "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], + + "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], + + "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], + + "viem": ["viem@2.48.0", "", { "dependencies": { "@noble/curves": "1.9.1", "@noble/hashes": "1.8.0", "@scure/bip32": "1.7.0", "@scure/bip39": "1.6.0", "abitype": "1.2.3", "isows": "1.0.7", "ox": "0.14.17", "ws": "8.18.3" }, "peerDependencies": { "typescript": ">=5.0.4" }, "optionalPeers": ["typescript"] }, "sha512-0uLzTAUNKPpY9Cf3OBCPdwClXx9CEHAkoVYnxMPdHt7cRI1DobMso+pHZvU7itD+hFwE4htmp9QfP+5lb+kn0g=="], + + "vite": ["vite@7.3.2", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg=="], + + "vite-plugin-arraybuffer": ["vite-plugin-arraybuffer@0.1.4", "", {}, "sha512-Ubfsw/g4PhdjlgsKYDm2Hlc0qmNJheX8VCeOMayq1k9fMVB1nNuqtWjDH1fj9hSlhlSnlndz/ehMKIuXpB0SeA=="], + + "vite-plugin-mkcert": ["vite-plugin-mkcert@1.17.12", "", { "dependencies": { "debug": "^4.4.3", "picocolors": "^1.1.1" }, "peerDependencies": { "vite": ">=3" } }, "sha512-HhLyUZnvkKLuk9o+HBaMlGIa6CqPTfLbVlLFBjrkN4mpZ4saYFRfFx5R12a46UxmzWT9dO8fijmblVvCYvCD3A=="], + + "vite-plugin-wasm": ["vite-plugin-wasm@3.6.0", "", { "peerDependencies": { "vite": "^2 || ^3 || ^4 || ^5 || ^6 || ^7 || ^8" } }, "sha512-mL/QPziiIA4RAA6DkaZZzOstdwbW5jO4Vz7Zenj0wieKWBlNvIvX5L5ljum9lcUX0ShNfBgCNLKTjNkRVVqcsw=="], + + "vitefu": ["vitefu@1.1.3", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["vite"] }, "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg=="], + + "vocs": ["vocs@https://pkg.pr.new/wevm/vocs@2fb25c2", { "dependencies": { "@base-ui/react": "^1.0.0", "@codesandbox/sandpack-react": "^2.20.0", "@iconify-json/lucide": "^1.2.82", "@iconify-json/simple-icons": "^1.2.65", "@iconify-json/vscode-icons": "^1.2.39", "@iconify/types": "^2.0.0", "@iconify/utils": "^3.1.0", "@mdx-js/mdx": "^3.1.1", "@mdx-js/react": "^3.1.1", "@mdx-js/rollup": "^3.1.1", "@modelcontextprotocol/sdk": "^1.25.2", "@remix-run/node-fetch-server": "^0.13.0", "@shikijs/rehype": "^3.20.0", "@shikijs/transformers": "^3.20.0", "@shikijs/twoslash": "^3.20.0", "@shikijs/types": "^3.20.0", "@svgr/core": "^8.1.0", "@svgr/plugin-jsx": "^8.1.0", "@tailwindcss/vite": "^4.1.8", "@takumi-rs/image-response": "^0.62.7", "@takumi-rs/wasm": "^0.62.7", "@vitejs/plugin-react": "5.1.2", "@vitejs/plugin-rsc": "^0.5.9", "cac": "^6.7.14", "cva": "npm:class-variance-authority@^0.7.1", "debug": "^4.4.0", "esast-util-from-js": "^2.0.1", "estree-util-value-to-estree": "^3.5.0", "estree-util-visit": "^2.0.0", "extend": "^3.0.2", "github-slugger": "^2.0.0", "hono": "^4.11.3", "image-size": "^2.0.2", "mdast-util-from-markdown": "^2.0.2", "mdast-util-gfm": "^3.1.0", "mdast-util-mdx": "^3.0.0", "mdast-util-to-hast": "^13.2.1", "mdast-util-to-markdown": "^2.1.2", "mdast-util-to-string": "^4.0.0", "micromark-extension-gfm": "^3.0.0", "minisearch": "^7.2.0", "nuqs": "^2.4.3", "react-error-boundary": "^6.0.2", "rehype-autolink-headings": "^7.1.0", "rehype-slug": "^6.0.0", "rehype-stringify": "^10.0.1", "remark-directive": "^4.0.0", "remark-frontmatter": "^5.0.0", "remark-gfm": "^4.0.1", "remark-mdx": "^3.1.1", "remark-mdx-frontmatter": "^5.2.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remark-stringify": "^11.0.0", "shiki": "^3.20.0", "sucrase": "^3.35.1", "tailwindcss": "^4.1.8", "tailwindcss-logical": "^4.1.0", "tsx": "^4.21.0", "twoslash": "^0.3.6", "unified": "^11.0.5", "unist-util-visit": "^5.0.0", "unplugin-icons": "^22.5.0", "urlpattern-polyfill": "^10.1.0", "vfile": "^6.0.3", "vite-plugin-arraybuffer": "^0.1.1", "vite-plugin-wasm": "^3.5.0", "yaml": "^2.8.2", "zod": "^4.3.5" }, "peerDependencies": { "@vocs/twoslash-rust": "^0.1.0", "mermaid": "^11", "react": "^19", "react-dom": "^19", "vite": "^7", "waku": "^1" }, "optionalPeers": ["@vocs/twoslash-rust", "mermaid", "vite", "waku"], "bin": { "vocs": "./dist/cli.js" } }, "sha512-ks2EN0bid4JPpFiHH8LDtKn4fRII9HrpyeHmD49VoOmZcgFvK6FAQ5iryi8HeS1f7uZfk/UoFdaPKC0mnR7ebw=="], + + "vscode-jsonrpc": ["vscode-jsonrpc@8.2.0", "", {}, "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA=="], + + "vscode-languageserver": ["vscode-languageserver@9.0.1", "", { "dependencies": { "vscode-languageserver-protocol": "3.17.5" }, "bin": { "installServerIntoExtension": "bin/installServerIntoExtension" } }, "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g=="], + + "vscode-languageserver-protocol": ["vscode-languageserver-protocol@3.17.5", "", { "dependencies": { "vscode-jsonrpc": "8.2.0", "vscode-languageserver-types": "3.17.5" } }, "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg=="], + + "vscode-languageserver-textdocument": ["vscode-languageserver-textdocument@1.0.12", "", {}, "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA=="], + + "vscode-languageserver-types": ["vscode-languageserver-types@3.17.5", "", {}, "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg=="], + + "vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="], + + "w3c-keyname": ["w3c-keyname@2.2.8", "", {}, "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="], + + "wagmi": ["wagmi@3.6.4", "", { "dependencies": { "@wagmi/connectors": "8.0.4", "@wagmi/core": "3.4.5", "use-sync-external-store": "1.4.0" }, "peerDependencies": { "@tanstack/react-query": ">=5.0.0", "react": ">=18", "typescript": ">=5.7.3", "viem": "2.x" }, "optionalPeers": ["typescript"] }, "sha512-aAvjKlRv1pMlw/fcZUFCCyeR4b32iCtcPqPH9cphuIygxag3wnzvGR2/kaXbY08qKeHZEkD8bOvb8acBOfy05A=="], + + "waku": ["waku@1.0.0-alpha.4", "", { "dependencies": { "@hono/node-server": "^1.19.0", "@vitejs/plugin-react": "^5.1.0", "@vitejs/plugin-rsc": "^0.5.19", "dotenv": "^17.3.1", "hono": "^4.10.0", "magic-string": "^0.30.21", "picocolors": "^1.1.1", "rsc-html-stream": "^0.0.7", "vite": "^7.2.0" }, "peerDependencies": { "react": "~19.2.4", "react-dom": "~19.2.4", "react-server-dom-webpack": "~19.2.4" }, "bin": { "waku": "cli.js" } }, "sha512-gZFEaaAL0YWEE55Z5GAggaDkwgpEmkL/ok9uUzq8exykNbXEDAuVd+H/ctXfQqW6VZGZb1aEyEMvjIamDv9igQ=="], + + "watchpack": ["watchpack@2.5.1", "", { "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" } }, "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg=="], + + "web-vitals": ["web-vitals@5.2.0", "", {}, "sha512-i2z98bEmaCqSDiHEDu+gHl/dmR4Q+TxFmG3/13KkMO+o8UxQzCqWaDRCiLgEa41nlO4VpXSI0ASa1xWmO9sBlA=="], + + "webauthx": ["webauthx@0.1.1", "", { "dependencies": { "ox": "~0.14.11" } }, "sha512-3jiskZl0jiTUoO8r3ySUYItdXNDBpfzxnxPx7XM0giF2dIY7S4KqHVbH04Qglz98nP8RaSV1/+3eDCx1s3WDiQ=="], + + "webpack": ["webpack@5.106.2", "", { "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", "@types/json-schema": "^7.0.15", "@webassemblyjs/ast": "^1.14.1", "@webassemblyjs/wasm-edit": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1", "acorn": "^8.16.0", "acorn-import-phases": "^1.0.3", "browserslist": "^4.28.1", "chrome-trace-event": "^1.0.2", "enhanced-resolve": "^5.20.0", "es-module-lexer": "^2.0.0", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.2.11", "loader-runner": "^4.3.1", "mime-db": "^1.54.0", "neo-async": "^2.6.2", "schema-utils": "^4.3.3", "tapable": "^2.3.0", "terser-webpack-plugin": "^5.3.17", "watchpack": "^2.5.1", "webpack-sources": "^3.3.4" }, "bin": { "webpack": "bin/webpack.js" } }, "sha512-wGN3qcrBQIFmQ/c0AiOAQBvrZ5lmY8vbbMv4Mxfgzqd/B6+9pXtLo73WuS1dSGXM5QYY3hZnIbvx+K1xxe6FyA=="], + + "webpack-sources": ["webpack-sources@3.4.0", "", {}, "sha512-gHwIe1cgBvvfLeu1Yz/dcFpmHfKDVxxyqI+kzqmuxZED81z2ChxpyqPaWcNqigPywhaEke7AjSGga+kxY55gjQ=="], + + "webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + + "ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], + + "yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], + + "yaml": ["yaml@2.8.3", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg=="], + + "yoctocolors": ["yoctocolors@2.1.2", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="], + + "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + + "zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="], + + "zustand": ["zustand@5.0.12", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g=="], + + "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], + + "@opentelemetry/otlp-transformer/@opentelemetry/resources": ["@opentelemetry/resources@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A=="], + + "@opentelemetry/resources/@opentelemetry/core": ["@opentelemetry/core@2.7.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-DT12SXVwV2eoJrGf4nnsvZojxxeQo+LlNAsoYGRRObPWTeN6APiqZ2+nqDCQDvQX40eLi1AePONS0onoASp3yQ=="], + + "@opentelemetry/sdk-logs/@opentelemetry/resources": ["@opentelemetry/resources@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A=="], + + "@opentelemetry/sdk-metrics/@opentelemetry/resources": ["@opentelemetry/resources@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A=="], + + "@opentelemetry/sdk-trace-base/@opentelemetry/resources": ["@opentelemetry/resources@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A=="], + + "@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" }, "bundled": true }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], + + "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" }, "bundled": true }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="], + + "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], + + "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "@vitejs/plugin-rsc/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.15", "", {}, "sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g=="], + + "@wagmi/core/zustand": ["zustand@5.0.0", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-LE+VcmbartOPM+auOjCCLQOsQ05zUTp8RkgwRzefUk+2jISdMMFnxvyTjA4YNWr5ZGXYbVsEMZosttuxUBkojQ=="], + + "body-parser/iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], + + "cytoscape-fcose/cose-base": ["cose-base@2.2.0", "", { "dependencies": { "layout-base": "^2.0.0" } }, "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g=="], + + "d3-dsv/commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="], + + "d3-sankey/d3-array": ["d3-array@2.12.1", "", { "dependencies": { "internmap": "^1.0.0" } }, "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ=="], + + "d3-sankey/d3-shape": ["d3-shape@1.3.7", "", { "dependencies": { "d3-path": "1" } }, "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw=="], + + "esrecurse/estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + + "http-proxy/eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="], + + "lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + + "mlly/pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], + + "monaco-editor/dompurify": ["dompurify@3.2.7", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw=="], + + "monaco-editor/marked": ["marked@14.0.0", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ=="], + + "mppx/ox": ["ox@0.14.15", "", { "dependencies": { "@adraffy/ens-normalize": "^1.11.0", "@noble/ciphers": "^1.3.0", "@noble/curves": "1.9.1", "@noble/hashes": "^1.8.0", "@scure/bip32": "^1.7.0", "@scure/bip39": "^1.6.0", "abitype": "^1.2.3", "eventemitter3": "5.0.1" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-3TubCmbKen/cuZQzX0qDbOS5lojjdSZ90lqKxWIDWd5siuJ0IJBaTXMYs8eMPLcraqnOwGZazz3apHPGiRCkGQ=="], + + "nearley/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], + + "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], + + "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], + + "playwright/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], + + "prool/eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="], + + "raw-body/iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], + + "schema-utils/ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="], + + "source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "static-browser-server/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], + + "strip-literal/js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="], + + "sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], + + "terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], + + "viem/abitype": ["abitype@1.2.3", "", { "peerDependencies": { "typescript": ">=5.0.4", "zod": "^3.22.0 || ^4.0.0" }, "optionalPeers": ["typescript", "zod"] }, "sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg=="], + + "viem/ox": ["ox@0.14.17", "", { "dependencies": { "@adraffy/ens-normalize": "^1.11.0", "@noble/ciphers": "^1.3.0", "@noble/curves": "1.9.1", "@noble/hashes": "^1.8.0", "@scure/bip32": "^1.7.0", "@scure/bip39": "^1.6.0", "abitype": "^1.2.3", "eventemitter3": "5.0.1" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-jOzNb2Wlfzsr8z/GoCtd1bf6OSRuWuysvbhnHGD+7fV1WRbcBR6B0RYoe3xWnUedF7zp4l5APmS7CzAhUok/lA=="], + + "vocs/@vitejs/plugin-react": ["@vitejs/plugin-react@5.1.2", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.53", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ=="], + + "vocs/cva": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], + + "vocs/unplugin-icons": ["unplugin-icons@22.5.0", "", { "dependencies": { "@antfu/install-pkg": "^1.1.0", "@iconify/utils": "^3.0.2", "debug": "^4.4.3", "local-pkg": "^1.1.2", "unplugin": "^2.3.10" }, "peerDependencies": { "@svgr/core": ">=7.0.0", "@svgx/core": "^1.0.1", "@vue/compiler-sfc": "^3.0.2 || ^2.7.0", "svelte": "^3.0.0 || ^4.0.0 || ^5.0.0", "vue-template-compiler": "^2.6.12", "vue-template-es2015-compiler": "^1.9.0" }, "optionalPeers": ["@svgr/core", "@svgx/core", "@vue/compiler-sfc", "svelte", "vue-template-compiler", "vue-template-es2015-compiler"] }, "sha512-MBlMtT5RuMYZy4TZgqUL2OTtOdTUVsS1Mhj6G1pEzMlFJlEnq6mhUfoIt45gBWxHcsOdXJDWLg3pRZ+YmvAVWQ=="], + + "wagmi/use-sync-external-store": ["use-sync-external-store@1.4.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw=="], + + "cytoscape-fcose/cose-base/layout-base": ["layout-base@2.0.1", "", {}, "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg=="], + + "d3-sankey/d3-shape/d3-path": ["d3-path@1.0.9", "", {}, "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="], + + "mlly/pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], + + "viem/ox/abitype": ["abitype@1.2.4", "", { "peerDependencies": { "typescript": ">=5.0.4", "zod": "^3.22.0 || ^4.0.0" }, "optionalPeers": ["typescript", "zod"] }, "sha512-dpKH+N27vRjarMVTFFkeY445VTKftzGWpL0FiT7xmVmzQRKazZexzC5uHG0f6XKsVLAuUlndnbGau6lRejClxg=="], + + "vocs/@vitejs/plugin-react/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.53", "", {}, "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ=="], + } +} diff --git a/e2e/create-a-stablecoin.test.ts b/e2e/create-a-stablecoin.test.ts index a5c82657..16324882 100644 --- a/e2e/create-a-stablecoin.test.ts +++ b/e2e/create-a-stablecoin.test.ts @@ -16,7 +16,7 @@ test('create a stablecoin', async ({ page }) => { }, }) - await page.goto('/guide/issuance/create-a-stablecoin') + await page.goto('/docs/guide/issuance/create-a-stablecoin') // Step 1: Sign in const signUpButton = page.getByRole('button', { name: 'Sign in' }).first() diff --git a/e2e/deposit-to-a-zone.test.ts b/e2e/deposit-to-a-zone.test.ts deleted file mode 100644 index 54cb4223..00000000 --- a/e2e/deposit-to-a-zone.test.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { expect, test } from '@playwright/test' -import { authorizeAndWaitForEnabledAction } from './private-zone-actions' - -test('prepare zone access and deposit to Zone A', async ({ page }) => { - test.setTimeout(180000) - - const client = await page.context().newCDPSession(page) - await client.send('WebAuthn.enable') - const { authenticatorId } = await client.send('WebAuthn.addVirtualAuthenticator', { - options: { - protocol: 'ctap2', - transport: 'internal', - hasResidentKey: true, - hasUserVerification: true, - isUserVerified: true, - }, - }) - - await page.goto('/guide/private-zones/deposit-to-a-zone') - - const signUpButton = page.getByRole('button', { name: 'Sign up' }).first() - await expect(signUpButton).toBeVisible({ timeout: 90000 }) - await signUpButton.click() - - await expect(page.getByRole('button', { name: 'Sign out' }).first()).toBeVisible({ - timeout: 30000, - }) - - const authorizeButton = page - .getByRole('button', { name: /^Authoriz(?:e|ing) Zone A reads$/i }) - .first() - - const getFundsButton = page.getByRole('button', { name: /^Get testnet pathUSD$/i }).first() - const depositButton = page.getByRole('button', { name: /^Deposit 100 pathUSD$/i }).first() - - const initialAction = await authorizeAndWaitForEnabledAction( - { locator: authorizeButton, name: 'authorize' }, - [ - { locator: getFundsButton, name: 'fund' }, - { locator: depositButton, name: 'deposit' }, - ], - ) - - if (initialAction.name === 'fund') { - await initialAction.locator.click() - await expect(depositButton).toBeEnabled({ timeout: 90000 }) - } - - await depositButton.click() - await expect(page.getByRole('link', { name: 'View receipt' }).first()).toBeVisible({ - timeout: 120000, - }) - await expect(page.getByText('Wait for Zone A to credit the deposit.').first()).toBeVisible() - - await client.send('WebAuthn.removeVirtualAuthenticator', { authenticatorId }) -}) diff --git a/e2e/distribute-rewards.test.ts b/e2e/distribute-rewards.test.ts index ee350775..3bf9c963 100644 --- a/e2e/distribute-rewards.test.ts +++ b/e2e/distribute-rewards.test.ts @@ -17,7 +17,7 @@ test('distribute rewards', async ({ page }) => { }, }) - await page.goto('/guide/issuance/distribute-rewards') + await page.goto('/docs/guide/issuance/distribute-rewards') // Step 1: Sign in const signUpButton = page.getByRole('button', { name: 'Sign in' }).first() diff --git a/e2e/executing-swaps.test.ts b/e2e/executing-swaps.test.ts index fafd6523..29580951 100644 --- a/e2e/executing-swaps.test.ts +++ b/e2e/executing-swaps.test.ts @@ -16,7 +16,7 @@ test('executing swaps', async ({ page }) => { }, }) - await page.goto('/guide/stablecoin-dex/executing-swaps') + await page.goto('/docs/guide/stablecoin-dex/executing-swaps') // Step 1: Sign in const signUpButton = page.getByRole('button', { name: 'Sign in' }).first() diff --git a/e2e/faucet.test.ts b/e2e/faucet.test.ts index b862fc8c..bbf3559f 100644 --- a/e2e/faucet.test.ts +++ b/e2e/faucet.test.ts @@ -3,7 +3,7 @@ import { expect, test } from '@playwright/test' test('fund an address via faucet', async ({ page }) => { test.setTimeout(120000) - await page.goto('/quickstart/faucet') + await page.goto('/docs/quickstart/faucet') // Switch to "Fund an address" tab const tab = page.getByRole('tab', { name: 'Fund an address' }) diff --git a/e2e/manage-stablecoin.test.ts b/e2e/manage-stablecoin.test.ts index dda7596f..836a65ca 100644 --- a/e2e/manage-stablecoin.test.ts +++ b/e2e/manage-stablecoin.test.ts @@ -17,7 +17,7 @@ test('manage stablecoin - grant and revoke roles', async ({ page }) => { }, }) - await page.goto('/guide/issuance/manage-stablecoin') + await page.goto('/docs/guide/issuance/manage-stablecoin') // Step 1: Sign in const signUpButton = page.getByRole('button', { name: 'Sign in' }).first() diff --git a/e2e/mint-stablecoins.test.ts b/e2e/mint-stablecoins.test.ts index f5388c3f..ff988658 100644 --- a/e2e/mint-stablecoins.test.ts +++ b/e2e/mint-stablecoins.test.ts @@ -17,7 +17,7 @@ test('mint stablecoins', async ({ page }) => { }, }) - await page.goto('/guide/issuance/mint-stablecoins') + await page.goto('/docs/guide/issuance/mint-stablecoins') // Step 1: Sign in const signUpButton = page.getByRole('button', { name: 'Sign in' }).first() diff --git a/e2e/passkey-accounts.test.ts b/e2e/passkey-accounts.test.ts deleted file mode 100644 index 0503f280..00000000 --- a/e2e/passkey-accounts.test.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { expect, test } from '@playwright/test' - -test('sign up, sign out, then sign in with passkey', async ({ page }) => { - // Set up virtual authenticator via CDP - const client = await page.context().newCDPSession(page) - await client.send('WebAuthn.enable') - const { authenticatorId } = await client.send('WebAuthn.addVirtualAuthenticator', { - options: { - protocol: 'ctap2', - transport: 'internal', - hasResidentKey: true, - hasUserVerification: true, - isUserVerified: true, - }, - }) - - await page.goto('/guide/use-accounts/embed-passkeys') - - // Wait for the demo to load - const signUpButton = page.getByRole('button', { name: 'Sign up' }).first() - await expect(signUpButton).toBeVisible({ timeout: 90000 }) - - // Sign up with passkey - await signUpButton.click() - - // Wait for sign out button (indicates successful sign up) - const signOutButton = page.getByRole('button', { name: 'Sign out' }).first() - await expect(signOutButton).toBeVisible({ timeout: 30000 }) - - // Sign out - await signOutButton.click() - - // Wait for sign in button to reappear - const signInButton = page.getByRole('button', { name: 'Sign in' }).first() - await expect(signInButton).toBeVisible({ timeout: 10000 }) - - // Sign in with the same passkey - await signInButton.click() - - // Confirm signed in again (sign out button visible) - await expect(page.getByRole('button', { name: 'Sign out' }).first()).toBeVisible({ - timeout: 30000, - }) - - // Clean up - await client.send('WebAuthn.removeVirtualAuthenticator', { authenticatorId }) -}) diff --git a/e2e/private-zone-actions.ts b/e2e/private-zone-actions.ts deleted file mode 100644 index 226841ea..00000000 --- a/e2e/private-zone-actions.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { expect, type Locator } from '@playwright/test' - -type Action = { - locator: Locator - name: string -} - -export async function waitForEnabledAction(actions: Action[], timeout = 120000) { - await expect - .poll( - async () => { - for (const action of actions) { - const visible = await action.locator.isVisible().catch(() => false) - if (!visible) continue - - const enabled = await action.locator.isEnabled().catch(() => false) - if (enabled) return action.name - } - - return '' - }, - { timeout }, - ) - .not.toBe('') - - for (const action of actions) { - const visible = await action.locator.isVisible().catch(() => false) - const enabled = await action.locator.isEnabled().catch(() => false) - if (visible && enabled) return action - } - - throw new Error('No enabled action found') -} - -export async function authorizeAndWaitForEnabledAction( - authorizeAction: Action, - actions: Action[], - timeout = 120000, -) { - const deadline = Date.now() + timeout - - await expect(authorizeAction.locator).toBeVisible({ timeout: 30000 }) - await expect(authorizeAction.locator).toBeEnabled({ - timeout: Math.min(90000, timeout), - }) - - while (Date.now() < deadline) { - const enabledAction = await getEnabledAction(actions) - if (enabledAction) return enabledAction - - const authorizeVisible = await authorizeAction.locator.isVisible().catch(() => false) - const authorizeEnabled = await authorizeAction.locator.isEnabled().catch(() => false) - if (authorizeVisible && authorizeEnabled) await authorizeAction.locator.click() - - await sleep(Math.min(1000, Math.max(deadline - Date.now(), 0))) - } - - const enabledAction = await getEnabledAction(actions) - if (enabledAction) return enabledAction - - throw new Error('No enabled action found after authorizing zone reads') -} - -async function getEnabledAction(actions: Action[]) { - for (const action of actions) { - const visible = await action.locator.isVisible().catch(() => false) - if (!visible) continue - - const enabled = await action.locator.isEnabled().catch(() => false) - if (enabled) return action - } - - return undefined -} - -async function sleep(ms: number) { - await new Promise((resolve) => setTimeout(resolve, ms)) -} diff --git a/e2e/providing-liquidity.test.ts b/e2e/providing-liquidity.test.ts index 6a40c75b..398ca853 100644 --- a/e2e/providing-liquidity.test.ts +++ b/e2e/providing-liquidity.test.ts @@ -16,7 +16,7 @@ test('providing liquidity - place and query order', async ({ page }) => { }, }) - await page.goto('/guide/stablecoin-dex/providing-liquidity') + await page.goto('/docs/guide/stablecoin-dex/providing-liquidity') // Step 1: Sign in const signUpButton = page.getByRole('button', { name: 'Sign in' }).first() diff --git a/e2e/send-a-payment.test.ts b/e2e/send-a-payment.test.ts index f6910fdd..cecdf51f 100644 --- a/e2e/send-a-payment.test.ts +++ b/e2e/send-a-payment.test.ts @@ -17,7 +17,7 @@ test('send a payment', async ({ page }) => { }, }) - await page.goto('/guide/payments/send-a-payment') + await page.goto('/docs/guide/payments/send-a-payment') // Step 1: Sign in const signUpButton = page.getByRole('button', { name: 'Sign in' }).first() diff --git a/e2e/send-tokens-across-zones.test.ts b/e2e/send-tokens-across-zones.test.ts deleted file mode 100644 index 1ea13a28..00000000 --- a/e2e/send-tokens-across-zones.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { expect, test } from '@playwright/test' -import { authorizeAndWaitForEnabledAction } from './private-zone-actions' - -test('send pathUSD from Zone A into Zone B', async ({ page }) => { - test.setTimeout(240000) - - const client = await page.context().newCDPSession(page) - await client.send('WebAuthn.enable') - const { authenticatorId } = await client.send('WebAuthn.addVirtualAuthenticator', { - options: { - protocol: 'ctap2', - transport: 'internal', - hasResidentKey: true, - hasUserVerification: true, - isUserVerified: true, - }, - }) - - await page.goto('/guide/private-zones/send-tokens-across-zones') - - const signUpButton = page.getByRole('button', { name: 'Sign up' }).first() - await expect(signUpButton).toBeVisible({ timeout: 90000 }) - await signUpButton.click() - - await expect(page.getByRole('button', { name: 'Sign out' }).first()).toBeVisible({ - timeout: 30000, - }) - - const authorizeSourceButton = page - .getByRole('button', { name: /^Authoriz(?:e|ing) Zone A reads$/i }) - .first() - - const getFundsButton = page.getByRole('button', { name: /^Get testnet pathUSD$/i }).first() - const topUpButton = page.getByRole('button', { name: /^Approve \+ top up Zone A$/i }).first() - - const initialAction = await authorizeAndWaitForEnabledAction( - { locator: authorizeSourceButton, name: 'authorize-source' }, - [ - { locator: getFundsButton, name: 'fund' }, - { locator: topUpButton, name: 'top-up' }, - ], - ) - - if (initialAction.name === 'fund') { - await initialAction.locator.click() - await expect(topUpButton).toBeEnabled({ timeout: 90000 }) - } - - if ((await topUpButton.isVisible()) && (await topUpButton.isEnabled())) { - await topUpButton.click() - } - - await expect(page.getByRole('link', { name: 'View receipt' }).first()).toBeVisible({ - timeout: 120000, - }) - await expect( - page.getByText('Withdraw 25 pathUSD from Zone A and route it into Zone B.').first(), - ).toBeVisible() - - await client.send('WebAuthn.removeVirtualAuthenticator', { authenticatorId }) -}) diff --git a/e2e/send-tokens-within-a-zone.test.ts b/e2e/send-tokens-within-a-zone.test.ts deleted file mode 100644 index d4a15d70..00000000 --- a/e2e/send-tokens-within-a-zone.test.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { expect, test } from '@playwright/test' -import { authorizeAndWaitForEnabledAction } from './private-zone-actions' - -test('prepare zone balance and send tokens within Zone A', async ({ page }) => { - test.setTimeout(180000) - - const client = await page.context().newCDPSession(page) - await client.send('WebAuthn.enable') - const { authenticatorId } = await client.send('WebAuthn.addVirtualAuthenticator', { - options: { - protocol: 'ctap2', - transport: 'internal', - hasResidentKey: true, - hasUserVerification: true, - isUserVerified: true, - }, - }) - - await page.goto('/guide/private-zones/send-tokens-within-a-zone') - - const signUpButton = page.getByRole('button', { name: 'Sign up' }).first() - await expect(signUpButton).toBeVisible({ timeout: 90000 }) - await signUpButton.click() - - await expect(page.getByRole('button', { name: 'Sign out' }).first()).toBeVisible({ - timeout: 30000, - }) - - const authorizeButton = page - .getByRole('button', { name: /^Authoriz(?:e|ing) Zone A reads$/i }) - .first() - - const getFundsButton = page.getByRole('button', { name: /^Get testnet pathUSD$/i }).first() - const topUpButton = page.getByRole('button', { name: /^Approve \+ top up Zone A$/i }).first() - - const initialAction = await authorizeAndWaitForEnabledAction( - { locator: authorizeButton, name: 'authorize' }, - [ - { locator: getFundsButton, name: 'fund' }, - { locator: topUpButton, name: 'top-up' }, - ], - ) - - if (initialAction.name === 'fund') { - await initialAction.locator.click() - await expect(topUpButton).toBeEnabled({ timeout: 90000 }) - } - - if ((await topUpButton.isVisible()) && (await topUpButton.isEnabled())) await topUpButton.click() - await expect(page.getByRole('link', { name: 'View receipt' }).first()).toBeVisible({ - timeout: 120000, - }) - await expect( - page.getByText('Send 25 pathUSD from Zone A to the demo recipient.').first(), - ).toBeVisible() - - await client.send('WebAuthn.removeVirtualAuthenticator', { authenticatorId }) -}) diff --git a/e2e/swap-across-zones.test.ts b/e2e/swap-across-zones.test.ts deleted file mode 100644 index e0dfc5e9..00000000 --- a/e2e/swap-across-zones.test.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { expect, test } from '@playwright/test' -import { authorizeAndWaitForEnabledAction } from './private-zone-actions' - -test('swap pathUSD from Zone A into betaUSD on Zone B', async ({ page }) => { - test.setTimeout(240000) - - const client = await page.context().newCDPSession(page) - await client.send('WebAuthn.enable') - const { authenticatorId } = await client.send('WebAuthn.addVirtualAuthenticator', { - options: { - protocol: 'ctap2', - transport: 'internal', - hasResidentKey: true, - hasUserVerification: true, - isUserVerified: true, - }, - }) - - await page.goto('/guide/private-zones/swap-across-zones') - - const signUpButton = page.getByRole('button', { name: 'Sign up' }).first() - await expect(signUpButton).toBeVisible({ timeout: 90000 }) - await signUpButton.click() - - await expect(page.getByRole('button', { name: 'Sign out' }).first()).toBeVisible({ - timeout: 30000, - }) - - const authorizeSourceButton = page - .getByRole('button', { name: /^Authoriz(?:e|ing) Zone A reads$/i }) - .first() - - const getFundsButton = page.getByRole('button', { name: /^Get testnet pathUSD$/i }).first() - const topUpButton = page.getByRole('button', { name: /^Approve \+ top up Zone A$/i }).first() - - const initialAction = await authorizeAndWaitForEnabledAction( - { locator: authorizeSourceButton, name: 'authorize-source' }, - [ - { locator: getFundsButton, name: 'fund' }, - { locator: topUpButton, name: 'top-up' }, - ], - ) - - if (initialAction.name === 'fund') { - await initialAction.locator.click() - await expect(topUpButton).toBeEnabled({ timeout: 90000 }) - } - - if ((await topUpButton.isVisible()) && (await topUpButton.isEnabled())) { - await topUpButton.click() - } - - await expect(page.getByRole('link', { name: 'View receipt' }).first()).toBeVisible({ - timeout: 120000, - }) - await expect( - page - .getByText('Withdraw 25 pathUSD from Zone A, swap it, and route betaUSD into Zone B.') - .first(), - ).toBeVisible() - - await client.send('WebAuthn.removeVirtualAuthenticator', { authenticatorId }) -}) diff --git a/e2e/use-for-fees.test.ts b/e2e/use-for-fees.test.ts index 2d848737..5a42866a 100644 --- a/e2e/use-for-fees.test.ts +++ b/e2e/use-for-fees.test.ts @@ -17,7 +17,7 @@ test('use stablecoin for fees', async ({ page }) => { }, }) - await page.goto('/guide/issuance/use-for-fees') + await page.goto('/docs/guide/issuance/use-for-fees') // Step 1: Sign in const signUpButton = page.getByRole('button', { name: 'Sign in' }).first() diff --git a/e2e/virtual-addresses.test.ts b/e2e/virtual-addresses.test.ts index d8f55711..05fe9e4f 100644 --- a/e2e/virtual-addresses.test.ts +++ b/e2e/virtual-addresses.test.ts @@ -1,7 +1,34 @@ -import { expect, test } from '@playwright/test' +import { expect, type Locator, type Page, test } from '@playwright/test' + +async function openVirtualAddressesGuide(page: Page): Promise { + let lastError: unknown + + for (let attempt = 0; attempt < 4; attempt++) { + try { + await page.goto('/docs/guide/payments/virtual-addresses', { waitUntil: 'domcontentloaded' }) + + await expect( + page.getByRole('heading', { name: 'Use virtual addresses for deposits' }), + ).toBeVisible({ timeout: 30000 }) + + const realRegistrationTab = page.getByRole('tab', { name: 'Real registration' }) + await expect(realRegistrationTab).toBeVisible({ timeout: 30000 }) + return realRegistrationTab + } catch (error) { + lastError = error + + if (attempt === 3) throw error + + // Vite can briefly serve SSR while client chunks are still re-optimizing in CI. + await page.waitForTimeout(1000) + } + } + + throw lastError +} test('virtual addresses guide signs in and starts master registration', async ({ page }) => { - test.setTimeout(150000) + test.setTimeout(240000) const client = await page.context().newCDPSession(page) await client.send('WebAuthn.enable') @@ -16,7 +43,8 @@ test('virtual addresses guide signs in and starts master registration', async ({ }) try { - await page.goto('/guide/use-accounts/embed-passkeys') + const realRegistrationTab = await openVirtualAddressesGuide(page) + await realRegistrationTab.click() const passkeySignUpButton = page.getByRole('button', { name: 'Sign up' }).first() await expect(passkeySignUpButton).toBeVisible({ timeout: 90000 }) @@ -26,12 +54,6 @@ test('virtual addresses guide signs in and starts master registration', async ({ timeout: 30000, }) - await page.goto('/guide/payments/virtual-addresses') - - await expect( - page.getByRole('heading', { name: 'Use virtual addresses for deposits' }), - ).toBeVisible() - await page.getByRole('tab', { name: 'Real registration' }).click() await expect(page.getByRole('button', { name: 'Sign out' }).first()).toBeVisible({ timeout: 30000, }) diff --git a/e2e/withdraw-from-a-zone.test.ts b/e2e/withdraw-from-a-zone.test.ts deleted file mode 100644 index 64c9953b..00000000 --- a/e2e/withdraw-from-a-zone.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { expect, test } from '@playwright/test' -import { authorizeAndWaitForEnabledAction } from './private-zone-actions' - -test.describe.configure({ retries: 0, timeout: 120000 }) - -test('prepare zone balance and withdraw from Zone A', async ({ page }) => { - test.setTimeout(180000) - - const client = await page.context().newCDPSession(page) - await client.send('WebAuthn.enable') - const { authenticatorId } = await client.send('WebAuthn.addVirtualAuthenticator', { - options: { - protocol: 'ctap2', - transport: 'internal', - hasResidentKey: true, - hasUserVerification: true, - isUserVerified: true, - }, - }) - - await page.goto('/guide/private-zones/withdraw-from-a-zone') - - const signUpButton = page.getByRole('button', { name: 'Sign up' }).first() - await expect(signUpButton).toBeVisible({ timeout: 45000 }) - await signUpButton.click() - - await expect(page.getByRole('button', { name: 'Sign out' }).first()).toBeVisible({ - timeout: 20000, - }) - - const authorizeButton = page - .getByRole('button', { name: /^Authoriz(?:e|ing) Zone A reads$/i }) - .first() - - const getFundsButton = page.getByRole('button', { name: /^Get testnet pathUSD$/i }).first() - const topUpButton = page.getByRole('button', { name: /^Approve \+ top up Zone A$/i }).first() - - const initialAction = await authorizeAndWaitForEnabledAction( - { locator: authorizeButton, name: 'authorize' }, - [ - { locator: getFundsButton, name: 'fund' }, - { locator: topUpButton, name: 'top-up' }, - ], - ) - - if (initialAction.name === 'fund') { - await initialAction.locator.click() - await expect(topUpButton).toBeEnabled({ timeout: 90000 }) - } - - if ((await topUpButton.isVisible()) && (await topUpButton.isEnabled())) { - await topUpButton.click() - } - - await expect(page.getByRole('link', { name: 'View receipt' }).first()).toBeVisible({ - timeout: 120000, - }) - await expect(page.getByText('Submit the withdrawal back from Zone A.').first()).toBeVisible() - - await client.send('WebAuthn.removeVirtualAuthenticator', { authenticatorId }) -}) diff --git a/lychee.toml b/lychee.toml index b51e9979..2fd902e9 100644 --- a/lychee.toml +++ b/lychee.toml @@ -33,8 +33,6 @@ exclude = [ "^https?://([a-z0-9-]+\\.)*example\\.(com|org|net)", # SVG xmlns reference, not a real link. "^http://www\\.w3\\.org/2000/svg$", - # Vocs/Vercel route template, not a literal URL. - "^https://accounts\\.tempo\\.xyz/docs/:path\\*?$", ] # Reuse cached results across runs. diff --git a/package.json b/package.json index 25e864ea..30fe7026 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "packageManager": "pnpm@10.28.1", "scripts": { "dev": "NODE_OPTIONS='--import tsx' vite", - "build": "tsgo --build && vite build", + "build": "tsgo --build && vite build && vite build --config vite.marketing.config.ts", "check": "biome check --write --unsafe .", "check:types": "tsgo --project tsconfig.json --noEmit", "preview": "node dist/preview.js", @@ -48,7 +48,7 @@ "tailwindcss": "^4.2.2", "unplugin-auto-import": "^21.0.0", "unplugin-icons": "^23.0.1", - "viem": "^2.48.8", + "viem": "^2.52.2", "vocs": "2.0.10", "wagmi": "3.6.14", "waku": "^1.0.0-beta.0", @@ -58,6 +58,7 @@ "devDependencies": { "@biomejs/biome": "^2.3.11", "@playwright/test": "^1.58.0", + "@tailwindcss/vite": "^4.3.0", "@types/node": "^25.6.0", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", @@ -66,7 +67,6 @@ "anser": "^2.3.5", "tsx": "^4.21.0", "typescript": "^5.9.3", - "use-sync-external-store": "^1.6.0", "vite": "^8.0.16", "vite-plugin-mkcert": "^1.17.12", "vitest": "^4.1.9" diff --git a/playwright.config.ts b/playwright.config.ts index 026aacf5..0859833a 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -24,7 +24,7 @@ export default defineConfig({ ], webServer: { command: isCI - ? 'NODE_OPTIONS="--max-old-space-size=4096 --import tsx" VITE_E2E=true VITE_USE_HTTP=true vite' + ? 'PORT=5173 VITE_E2E=true VITE_USE_HTTP=true node dist/preview.js' : 'pnpm run dev 2>/dev/null', url: webServerUrl, ignoreHTTPSErrors: true, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 42bd78b7..fc312661 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -47,13 +47,13 @@ importers: version: 1.3.1(react@19.2.6) '@wagmi/core': specifier: 3.4.11 - version: 3.4.11(@tanstack/query-core@5.99.0)(@types/react@19.2.14)(accounts@0.10.7)(react@19.2.6)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.6))(viem@2.48.8(typescript@5.9.3)(zod@4.3.6)) + version: 3.4.11(@tanstack/query-core@5.99.0)(@types/react@19.2.14)(accounts@0.10.7)(react@19.2.6)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.6))(viem@2.52.2(typescript@5.9.3)(zod@4.3.6)) abitype: specifier: ^1.2.3 version: 1.2.3(typescript@5.9.3)(zod@4.3.6) accounts: specifier: ^0.10.7 - version: 0.10.7(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6))(@types/react@19.2.14)(@wagmi/core@3.4.11)(express@5.2.1)(react@19.2.6)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.6))(viem@2.48.8(typescript@5.9.3)(zod@4.3.6))(wagmi@3.6.14) + version: 0.10.7(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6))(@types/react@19.2.14)(@wagmi/core@3.4.11)(express@5.2.1)(react@19.2.6)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.6))(viem@2.52.2(typescript@5.9.3)(zod@4.3.6))(wagmi@3.6.14) cva: specifier: 1.0.0-beta.4 version: 1.0.0-beta.4(typescript@5.9.3) @@ -106,14 +106,14 @@ importers: specifier: ^23.0.1 version: 23.0.1(@svgr/core@8.1.0(typescript@5.9.3)) viem: - specifier: ^2.48.8 - version: 2.48.8(typescript@5.9.3)(zod@4.3.6) + specifier: ^2.52.2 + version: 2.52.2(typescript@5.9.3)(zod@4.3.6) vocs: specifier: 2.0.10 version: 2.0.10(@cfworker/json-schema@4.1.1)(@types/react@19.2.14)(mermaid@11.14.0)(react-dom@19.2.6(react@19.2.6))(react-server-dom-webpack@19.2.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(webpack@5.104.1(esbuild@0.27.7)))(react@19.2.6)(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.16(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.4))(waku@1.0.0-beta.0(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(react-dom@19.2.6(react@19.2.6))(react-server-dom-webpack@19.2.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(webpack@5.104.1(esbuild@0.27.7)))(react@19.2.6)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.4)) wagmi: specifier: 3.6.14 - version: 3.6.14(@tanstack/query-core@5.99.0)(@tanstack/react-query@5.99.0(react@19.2.6))(@types/react@19.2.14)(accounts@0.10.7)(react@19.2.6)(typescript@5.9.3)(viem@2.48.8(typescript@5.9.3)(zod@4.3.6)) + version: 3.6.14(@tanstack/query-core@5.99.0)(@tanstack/react-query@5.99.0(react@19.2.6))(@types/react@19.2.14)(accounts@0.10.7)(react@19.2.6)(typescript@5.9.3)(viem@2.52.2(typescript@5.9.3)(zod@4.3.6)) waku: specifier: ^1.0.0-beta.0 version: 1.0.0-beta.0(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(react-dom@19.2.6(react@19.2.6))(react-server-dom-webpack@19.2.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(webpack@5.104.1(esbuild@0.27.7)))(react@19.2.6)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.4) @@ -130,6 +130,9 @@ importers: '@playwright/test': specifier: ^1.58.0 version: 1.58.0 + '@tailwindcss/vite': + specifier: ^4.3.0 + version: 4.3.0(vite@8.0.16(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.4)) '@types/node': specifier: ^25.6.0 version: 25.6.0 @@ -157,9 +160,6 @@ importers: typescript: specifier: ^5.9.3 version: 5.9.3 - use-sync-external-store: - specifier: ^1.6.0 - version: 1.6.0(react@19.2.6) vite: specifier: ^8.0.16 version: 8.0.16(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.4) @@ -3316,6 +3316,14 @@ packages: typescript: optional: true + ox@0.14.29: + resolution: {integrity: sha512-M5j87Ec4V99MQdRct/g09eWXW60g6zhHTUs1lr4deUtrPDnezBdCJTgKd7pxqTpSZBFveV0ALi9jMMuT1qKyNg==} + peerDependencies: + typescript: '>=5.4.0' + peerDependenciesMeta: + typescript: + optional: true + package-manager-detector@1.6.0: resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} @@ -4009,8 +4017,8 @@ packages: vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} - viem@2.48.8: - resolution: {integrity: sha512-Xj3Nrt66SKtn06kczU91ELn9Difr84ZM5A62BTlaisT5lpgt058i2mBkfMZCXHGb1ocOLjzC2ztPhD0Lvky7uQ==} + viem@2.52.2: + resolution: {integrity: sha512-HSU12p5aD/kAPZfrlbCUqdiP4P/c6hQ9AhfTS51VbLUQIjkWd1d5EjrCx/SCxZ0zhZVRn4Iv5X5WDqXPG8Ubew==} peerDependencies: typescript: '>=5.0.4' peerDependenciesMeta: @@ -4226,8 +4234,8 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - ws@8.18.3: - resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} + ws@8.20.1: + resolution: {integrity: sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -5758,23 +5766,23 @@ snapshots: convert-source-map: 2.0.0 tinyrainbow: 3.1.0 - '@wagmi/connectors@8.0.13(@wagmi/core@3.4.11)(accounts@0.10.7)(typescript@5.9.3)(viem@2.48.8(typescript@5.9.3)(zod@4.3.6))': + '@wagmi/connectors@8.0.13(@wagmi/core@3.4.11)(accounts@0.10.7)(typescript@5.9.3)(viem@2.52.2(typescript@5.9.3)(zod@4.3.6))': dependencies: - '@wagmi/core': 3.4.11(@tanstack/query-core@5.99.0)(@types/react@19.2.14)(accounts@0.10.7)(react@19.2.6)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.6))(viem@2.48.8(typescript@5.9.3)(zod@4.3.6)) - viem: 2.48.8(typescript@5.9.3)(zod@4.3.6) + '@wagmi/core': 3.4.11(@tanstack/query-core@5.99.0)(@types/react@19.2.14)(accounts@0.10.7)(react@19.2.6)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.6))(viem@2.52.2(typescript@5.9.3)(zod@4.3.6)) + viem: 2.52.2(typescript@5.9.3)(zod@4.3.6) optionalDependencies: - accounts: 0.10.7(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6))(@types/react@19.2.14)(@wagmi/core@3.4.11)(express@5.2.1)(react@19.2.6)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.6))(viem@2.48.8(typescript@5.9.3)(zod@4.3.6))(wagmi@3.6.14) + accounts: 0.10.7(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6))(@types/react@19.2.14)(@wagmi/core@3.4.11)(express@5.2.1)(react@19.2.6)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.6))(viem@2.52.2(typescript@5.9.3)(zod@4.3.6))(wagmi@3.6.14) typescript: 5.9.3 - '@wagmi/core@3.4.11(@tanstack/query-core@5.99.0)(@types/react@19.2.14)(accounts@0.10.7)(react@19.2.6)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.6))(viem@2.48.8(typescript@5.9.3)(zod@4.3.6))': + '@wagmi/core@3.4.11(@tanstack/query-core@5.99.0)(@types/react@19.2.14)(accounts@0.10.7)(react@19.2.6)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.6))(viem@2.52.2(typescript@5.9.3)(zod@4.3.6))': dependencies: eventemitter3: 5.0.1 mipd: 0.0.7(typescript@5.9.3) - viem: 2.48.8(typescript@5.9.3)(zod@4.3.6) + viem: 2.52.2(typescript@5.9.3)(zod@4.3.6) zustand: 5.0.0(@types/react@19.2.14)(react@19.2.6)(use-sync-external-store@1.4.0(react@19.2.6)) optionalDependencies: '@tanstack/query-core': 5.99.0 - accounts: 0.10.7(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6))(@types/react@19.2.14)(@wagmi/core@3.4.11)(express@5.2.1)(react@19.2.6)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.6))(viem@2.48.8(typescript@5.9.3)(zod@4.3.6))(wagmi@3.6.14) + accounts: 0.10.7(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6))(@types/react@19.2.14)(@wagmi/core@3.4.11)(express@5.2.1)(react@19.2.6)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.6))(viem@2.52.2(typescript@5.9.3)(zod@4.3.6))(wagmi@3.6.14) typescript: 5.9.3 transitivePeerDependencies: - '@types/react' @@ -5782,15 +5790,15 @@ snapshots: - react - use-sync-external-store - '@wagmi/core@3.4.11(@tanstack/query-core@5.99.0)(@types/react@19.2.14)(accounts@0.10.7)(react@19.2.6)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.6))(viem@2.48.8(typescript@5.9.3)(zod@4.3.6))': + '@wagmi/core@3.4.11(@tanstack/query-core@5.99.0)(@types/react@19.2.14)(accounts@0.10.7)(react@19.2.6)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.6))(viem@2.52.2(typescript@5.9.3)(zod@4.3.6))': dependencies: eventemitter3: 5.0.1 mipd: 0.0.7(typescript@5.9.3) - viem: 2.48.8(typescript@5.9.3)(zod@4.3.6) + viem: 2.52.2(typescript@5.9.3)(zod@4.3.6) zustand: 5.0.0(@types/react@19.2.14)(react@19.2.6)(use-sync-external-store@1.6.0(react@19.2.6)) optionalDependencies: '@tanstack/query-core': 5.99.0 - accounts: 0.10.7(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6))(@types/react@19.2.14)(@wagmi/core@3.4.11)(express@5.2.1)(react@19.2.6)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.6))(viem@2.48.8(typescript@5.9.3)(zod@4.3.6))(wagmi@3.6.14) + accounts: 0.10.7(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6))(@types/react@19.2.14)(@wagmi/core@3.4.11)(express@5.2.1)(react@19.2.6)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.6))(viem@2.52.2(typescript@5.9.3)(zod@4.3.6))(wagmi@3.6.14) typescript: 5.9.3 transitivePeerDependencies: - '@types/react' @@ -5893,21 +5901,21 @@ snapshots: mime-types: 3.0.2 negotiator: 1.0.0 - accounts@0.10.7(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6))(@types/react@19.2.14)(@wagmi/core@3.4.11)(express@5.2.1)(react@19.2.6)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.6))(viem@2.48.8(typescript@5.9.3)(zod@4.3.6))(wagmi@3.6.14): + accounts@0.10.7(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6))(@types/react@19.2.14)(@wagmi/core@3.4.11)(express@5.2.1)(react@19.2.6)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.6))(viem@2.52.2(typescript@5.9.3)(zod@4.3.6))(wagmi@3.6.14): dependencies: hono: 4.12.26 idb-keyval: 6.2.2 mipd: 0.0.7(typescript@5.9.3) - mppx: 0.6.20(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6))(express@5.2.1)(hono@4.12.26)(typescript@5.9.3)(viem@2.48.8(typescript@5.9.3)(zod@4.3.6)) + mppx: 0.6.20(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6))(express@5.2.1)(hono@4.12.26)(typescript@5.9.3)(viem@2.52.2(typescript@5.9.3)(zod@4.3.6)) ox: 0.14.20(typescript@5.9.3)(zod@4.3.6) webauthx: 0.1.1(typescript@5.9.3)(zod@4.3.6) zod: 4.3.6 zustand: 5.0.12(@types/react@19.2.14)(react@19.2.6)(use-sync-external-store@1.6.0(react@19.2.6)) optionalDependencies: - '@wagmi/core': 3.4.11(@tanstack/query-core@5.99.0)(@types/react@19.2.14)(accounts@0.10.7)(react@19.2.6)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.6))(viem@2.48.8(typescript@5.9.3)(zod@4.3.6)) + '@wagmi/core': 3.4.11(@tanstack/query-core@5.99.0)(@types/react@19.2.14)(accounts@0.10.7)(react@19.2.6)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.6))(viem@2.52.2(typescript@5.9.3)(zod@4.3.6)) react: 19.2.6 - viem: 2.48.8(typescript@5.9.3)(zod@4.3.6) - wagmi: 3.6.14(@tanstack/query-core@5.99.0)(@tanstack/react-query@5.99.0(react@19.2.6))(@types/react@19.2.14)(accounts@0.10.7)(react@19.2.6)(typescript@5.9.3)(viem@2.48.8(typescript@5.9.3)(zod@4.3.6)) + viem: 2.52.2(typescript@5.9.3)(zod@4.3.6) + wagmi: 3.6.14(@tanstack/query-core@5.99.0)(@tanstack/react-query@5.99.0(react@19.2.6))(@types/react@19.2.14)(accounts@0.10.7)(react@19.2.6)(typescript@5.9.3)(viem@2.52.2(typescript@5.9.3)(zod@4.3.6)) transitivePeerDependencies: - '@modelcontextprotocol/sdk' - '@types/react' @@ -6846,9 +6854,9 @@ snapshots: isexe@2.0.0: {} - isows@1.0.7(ws@8.18.3): + isows@1.0.7(ws@8.20.1): dependencies: - ws: 8.18.3 + ws: 8.20.1 jest-worker@27.5.1: dependencies: @@ -7523,11 +7531,11 @@ snapshots: moo@0.5.3: {} - mppx@0.6.20(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6))(express@5.2.1)(hono@4.12.26)(typescript@5.9.3)(viem@2.48.8(typescript@5.9.3)(zod@4.3.6)): + mppx@0.6.20(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6))(express@5.2.1)(hono@4.12.26)(typescript@5.9.3)(viem@2.52.2(typescript@5.9.3)(zod@4.3.6)): dependencies: incur: 0.4.5 ox: 0.14.20(typescript@5.9.3)(zod@4.4.3) - viem: 2.48.8(typescript@5.9.3)(zod@4.3.6) + viem: 2.52.2(typescript@5.9.3)(zod@4.3.6) zod: 4.4.3 optionalDependencies: '@modelcontextprotocol/sdk': 1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6) @@ -7632,6 +7640,21 @@ snapshots: transitivePeerDependencies: - zod + ox@0.14.29(typescript@5.9.3)(zod@4.3.6): + dependencies: + '@adraffy/ens-normalize': 1.11.1 + '@noble/ciphers': 1.3.0 + '@noble/curves': 1.9.1 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 + abitype: 1.2.3(typescript@5.9.3)(zod@4.3.6) + eventemitter3: 5.0.1 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - zod + package-manager-detector@1.6.0: {} parent-module@1.0.1: @@ -8471,16 +8494,16 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - viem@2.48.8(typescript@5.9.3)(zod@4.3.6): + viem@2.52.2(typescript@5.9.3)(zod@4.3.6): dependencies: '@noble/curves': 1.9.1 '@noble/hashes': 1.8.0 '@scure/bip32': 1.7.0 '@scure/bip39': 1.6.0 abitype: 1.2.3(typescript@5.9.3)(zod@4.3.6) - isows: 1.0.7(ws@8.18.3) - ox: 0.14.20(typescript@5.9.3)(zod@4.3.6) - ws: 8.18.3 + isows: 1.0.7(ws@8.20.1) + ox: 0.14.29(typescript@5.9.3)(zod@4.3.6) + ws: 8.20.1 optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: @@ -8669,14 +8692,14 @@ snapshots: w3c-keyname@2.2.8: {} - wagmi@3.6.14(@tanstack/query-core@5.99.0)(@tanstack/react-query@5.99.0(react@19.2.6))(@types/react@19.2.14)(accounts@0.10.7)(react@19.2.6)(typescript@5.9.3)(viem@2.48.8(typescript@5.9.3)(zod@4.3.6)): + wagmi@3.6.14(@tanstack/query-core@5.99.0)(@tanstack/react-query@5.99.0(react@19.2.6))(@types/react@19.2.14)(accounts@0.10.7)(react@19.2.6)(typescript@5.9.3)(viem@2.52.2(typescript@5.9.3)(zod@4.3.6)): dependencies: '@tanstack/react-query': 5.99.0(react@19.2.6) - '@wagmi/connectors': 8.0.13(@wagmi/core@3.4.11)(accounts@0.10.7)(typescript@5.9.3)(viem@2.48.8(typescript@5.9.3)(zod@4.3.6)) - '@wagmi/core': 3.4.11(@tanstack/query-core@5.99.0)(@types/react@19.2.14)(accounts@0.10.7)(react@19.2.6)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.6))(viem@2.48.8(typescript@5.9.3)(zod@4.3.6)) + '@wagmi/connectors': 8.0.13(@wagmi/core@3.4.11)(accounts@0.10.7)(typescript@5.9.3)(viem@2.52.2(typescript@5.9.3)(zod@4.3.6)) + '@wagmi/core': 3.4.11(@tanstack/query-core@5.99.0)(@types/react@19.2.14)(accounts@0.10.7)(react@19.2.6)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.6))(viem@2.52.2(typescript@5.9.3)(zod@4.3.6)) react: 19.2.6 use-sync-external-store: 1.4.0(react@19.2.6) - viem: 2.48.8(typescript@5.9.3)(zod@4.3.6) + viem: 2.52.2(typescript@5.9.3)(zod@4.3.6) optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: @@ -8783,7 +8806,7 @@ snapshots: wrappy@1.0.2: {} - ws@8.18.3: {} + ws@8.20.1: {} yallist@3.1.1: {} diff --git a/public/assets/Group.svg b/public/assets/Group.svg new file mode 100644 index 00000000..4e00be5f --- /dev/null +++ b/public/assets/Group.svg @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/assets/photo1.png b/public/assets/photo1.png new file mode 100644 index 00000000..e5ab9e22 Binary files /dev/null and b/public/assets/photo1.png differ diff --git a/public/assets/photo2.png b/public/assets/photo2.png new file mode 100644 index 00000000..10a1b779 Binary files /dev/null and b/public/assets/photo2.png differ diff --git a/public/assets/photo3.png b/public/assets/photo3.png new file mode 100644 index 00000000..2fab434a Binary files /dev/null and b/public/assets/photo3.png differ diff --git a/public/assets/reth.svg b/public/assets/reth.svg new file mode 100644 index 00000000..e29f6f10 --- /dev/null +++ b/public/assets/reth.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/fonts/hbset/HBSetv0.96-Light.woff2 b/public/fonts/hbset/HBSetv0.96-Light.woff2 new file mode 100644 index 00000000..d8c203a9 Binary files /dev/null and b/public/fonts/hbset/HBSetv0.96-Light.woff2 differ diff --git a/public/fonts/hbset/HBSetv0.96-LightItalic.woff2 b/public/fonts/hbset/HBSetv0.96-LightItalic.woff2 new file mode 100644 index 00000000..8b2bca50 Binary files /dev/null and b/public/fonts/hbset/HBSetv0.96-LightItalic.woff2 differ diff --git a/public/fonts/hbset/HBSetv0.96-Medium.woff2 b/public/fonts/hbset/HBSetv0.96-Medium.woff2 new file mode 100644 index 00000000..cb9578e6 Binary files /dev/null and b/public/fonts/hbset/HBSetv0.96-Medium.woff2 differ diff --git a/public/fonts/hbset/HBSetv0.96-MediumItalic.woff2 b/public/fonts/hbset/HBSetv0.96-MediumItalic.woff2 new file mode 100644 index 00000000..700e1bac Binary files /dev/null and b/public/fonts/hbset/HBSetv0.96-MediumItalic.woff2 differ diff --git a/public/fonts/hbset/HBSetv0.96-Regular2.woff2 b/public/fonts/hbset/HBSetv0.96-Regular2.woff2 new file mode 100644 index 00000000..f1641b51 Binary files /dev/null and b/public/fonts/hbset/HBSetv0.96-Regular2.woff2 differ diff --git a/public/fonts/hbset/HBSetv0.96-Regular2Italic.woff2 b/public/fonts/hbset/HBSetv0.96-Regular2Italic.woff2 new file mode 100644 index 00000000..5447bb31 Binary files /dev/null and b/public/fonts/hbset/HBSetv0.96-Regular2Italic.woff2 differ diff --git a/public/fonts/jetbrains-mono/JetBrainsMono-Medium.woff2 b/public/fonts/jetbrains-mono/JetBrainsMono-Medium.woff2 new file mode 100644 index 00000000..669d04cd Binary files /dev/null and b/public/fonts/jetbrains-mono/JetBrainsMono-Medium.woff2 differ diff --git a/public/fonts/jetbrains-mono/JetBrainsMono-Regular.woff2 b/public/fonts/jetbrains-mono/JetBrainsMono-Regular.woff2 new file mode 100644 index 00000000..40da4276 Binary files /dev/null and b/public/fonts/jetbrains-mono/JetBrainsMono-Regular.woff2 differ diff --git a/public/fonts/pilat/Pilat-Book.woff2 b/public/fonts/pilat/Pilat-Book.woff2 new file mode 100644 index 00000000..80f2bb71 Binary files /dev/null and b/public/fonts/pilat/Pilat-Book.woff2 differ diff --git a/public/learn/agentic-commerce-developer-benefits.png b/public/learn/agentic-commerce-developer-benefits.png deleted file mode 100644 index 116265a8..00000000 Binary files a/public/learn/agentic-commerce-developer-benefits.png and /dev/null differ diff --git a/public/learn/embedded-finance-beyond-payouts.png b/public/learn/embedded-finance-beyond-payouts.png deleted file mode 100644 index 1eac1aa3..00000000 Binary files a/public/learn/embedded-finance-beyond-payouts.png and /dev/null differ diff --git a/public/learn/embedded-finance-transformation.png b/public/learn/embedded-finance-transformation.png deleted file mode 100644 index ad453591..00000000 Binary files a/public/learn/embedded-finance-transformation.png and /dev/null differ diff --git a/public/learn/microtransactions-reconciliation-benefits.png b/public/learn/microtransactions-reconciliation-benefits.png deleted file mode 100644 index 44c144ff..00000000 Binary files a/public/learn/microtransactions-reconciliation-benefits.png and /dev/null differ diff --git a/public/learn/remittances-backend-settlement.png b/public/learn/remittances-backend-settlement.png deleted file mode 100644 index a1c84fc1..00000000 Binary files a/public/learn/remittances-backend-settlement.png and /dev/null differ diff --git a/public/learn/remittances-direct-transfers.png b/public/learn/remittances-direct-transfers.png deleted file mode 100644 index 4376eb65..00000000 Binary files a/public/learn/remittances-direct-transfers.png and /dev/null differ diff --git a/public/learn/remittances-orchestrated-payments.png b/public/learn/remittances-orchestrated-payments.png deleted file mode 100644 index 80537d69..00000000 Binary files a/public/learn/remittances-orchestrated-payments.png and /dev/null differ diff --git a/public/learn/stablecoin-mint-burn-process.svg b/public/learn/stablecoin-mint-burn-process.svg deleted file mode 100644 index 722a035b..00000000 --- a/public/learn/stablecoin-mint-burn-process.svg +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/public/learn/stablecoin-use-cases.svg b/public/learn/stablecoin-use-cases.svg deleted file mode 100644 index 06d2d3bb..00000000 --- a/public/learn/stablecoin-use-cases.svg +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/public/learn/stablecoins-payment-rails-comparison.png b/public/learn/stablecoins-payment-rails-comparison.png deleted file mode 100644 index 5419aae1..00000000 Binary files a/public/learn/stablecoins-payment-rails-comparison.png and /dev/null differ diff --git a/public/learn/tokenized-deposits-opportunity.png b/public/learn/tokenized-deposits-opportunity.png deleted file mode 100644 index b0c51901..00000000 Binary files a/public/learn/tokenized-deposits-opportunity.png and /dev/null differ diff --git a/public/learn/tokenized-deposits-treasury-management.png b/public/learn/tokenized-deposits-treasury-management.png deleted file mode 100644 index 9eee481b..00000000 Binary files a/public/learn/tokenized-deposits-treasury-management.png and /dev/null differ diff --git a/public/learn/wallet-add-funds.png b/public/learn/wallet-add-funds.png deleted file mode 100644 index 2e82115f..00000000 Binary files a/public/learn/wallet-add-funds.png and /dev/null differ diff --git a/public/learn/zones/diagram-deposit.svg b/public/learn/zones/diagram-deposit.svg deleted file mode 100644 index 4e9cc7ee..00000000 --- a/public/learn/zones/diagram-deposit.svg +++ /dev/null @@ -1,147 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -Locks funds - - - - - - - - - - - - - - - - - - - - - - - - - -Locks funds - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/public/learn/zones/diagram-node.svg b/public/learn/zones/diagram-node.svg deleted file mode 100644 index 26b1d28c..00000000 --- a/public/learn/zones/diagram-node.svg +++ /dev/null @@ -1,110 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/public/learn/zones/diagram-overview.svg b/public/learn/zones/diagram-overview.svg deleted file mode 100644 index b51029e3..00000000 --- a/public/learn/zones/diagram-overview.svg +++ /dev/null @@ -1,458 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/public/learn/zones/diagram-privacy.svg b/public/learn/zones/diagram-privacy.svg deleted file mode 100644 index d1282da3..00000000 --- a/public/learn/zones/diagram-privacy.svg +++ /dev/null @@ -1,179 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/public/learn/zones/diagram-swap.svg b/public/learn/zones/diagram-swap.svg deleted file mode 100644 index 336e5a33..00000000 --- a/public/learn/zones/diagram-swap.svg +++ /dev/null @@ -1,118 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/public/learn/zones/diagram-tip20.svg b/public/learn/zones/diagram-tip20.svg deleted file mode 100644 index bff69d5d..00000000 --- a/public/learn/zones/diagram-tip20.svg +++ /dev/null @@ -1,152 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/public/learn/zones/diagram-withdraw.svg b/public/learn/zones/diagram-withdraw.svg deleted file mode 100644 index a1a3c7c6..00000000 --- a/public/learn/zones/diagram-withdraw.svg +++ /dev/null @@ -1,140 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -Locks funds - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/public/partners/alloy.svg b/public/partners/alloy.svg new file mode 100644 index 00000000..62427f1f --- /dev/null +++ b/public/partners/alloy.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/public/partners/bitgo.svg b/public/partners/bitgo.svg new file mode 100644 index 00000000..e803de3e --- /dev/null +++ b/public/partners/bitgo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/partners/blockaid.svg b/public/partners/blockaid.svg new file mode 100644 index 00000000..22acc8a8 --- /dev/null +++ b/public/partners/blockaid.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/public/partners/bridge.svg b/public/partners/bridge.svg new file mode 100644 index 00000000..1e478df9 --- /dev/null +++ b/public/partners/bridge.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/public/partners/chainalysis.svg b/public/partners/chainalysis.svg new file mode 100644 index 00000000..b697ad89 --- /dev/null +++ b/public/partners/chainalysis.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/partners/circle.svg b/public/partners/circle.svg new file mode 100644 index 00000000..7ebfc43e --- /dev/null +++ b/public/partners/circle.svg @@ -0,0 +1,7 @@ + + + logo/licorice + + + + \ No newline at end of file diff --git a/public/partners/crates-io.svg b/public/partners/crates-io.svg new file mode 100644 index 00000000..ae88c29d --- /dev/null +++ b/public/partners/crates-io.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/public/partners/fireblocks.svg b/public/partners/fireblocks.svg new file mode 100644 index 00000000..824e4020 --- /dev/null +++ b/public/partners/fireblocks.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/public/partners/go.svg b/public/partners/go.svg new file mode 100644 index 00000000..da6ea83d --- /dev/null +++ b/public/partners/go.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/partners/graphql.svg b/public/partners/graphql.svg new file mode 100644 index 00000000..ba95925a --- /dev/null +++ b/public/partners/graphql.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/partners/layerzero.svg b/public/partners/layerzero.svg new file mode 100644 index 00000000..3463b517 --- /dev/null +++ b/public/partners/layerzero.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/public/partners/npm.svg b/public/partners/npm.svg new file mode 100644 index 00000000..33af4ba0 --- /dev/null +++ b/public/partners/npm.svg @@ -0,0 +1,9 @@ + + + + + diff --git a/public/partners/ondo.svg b/public/partners/ondo.svg new file mode 100644 index 00000000..f392d024 --- /dev/null +++ b/public/partners/ondo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/partners/parallel.svg b/public/partners/parallel.svg new file mode 100644 index 00000000..be8af65f --- /dev/null +++ b/public/partners/parallel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/partners/phantom.svg b/public/partners/phantom.svg new file mode 100644 index 00000000..dc3a0fb4 --- /dev/null +++ b/public/partners/phantom.svg @@ -0,0 +1 @@ +Logo Phantom \ No newline at end of file diff --git a/public/partners/privy.svg b/public/partners/privy.svg new file mode 100644 index 00000000..9237c214 --- /dev/null +++ b/public/partners/privy.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/partners/pypi.svg b/public/partners/pypi.svg new file mode 100644 index 00000000..3efd4b12 --- /dev/null +++ b/public/partners/pypi.svg @@ -0,0 +1 @@ +PyPI \ No newline at end of file diff --git a/public/partners/python.svg b/public/partners/python.svg new file mode 100644 index 00000000..e1b14ec6 --- /dev/null +++ b/public/partners/python.svg @@ -0,0 +1 @@ +Python \ No newline at end of file diff --git a/public/partners/reth.svg b/public/partners/reth.svg new file mode 100644 index 00000000..224fafd4 --- /dev/null +++ b/public/partners/reth.svg @@ -0,0 +1,43 @@ + + + + + + + + diff --git a/public/partners/rust.svg b/public/partners/rust.svg new file mode 100644 index 00000000..faa8c3a3 --- /dev/null +++ b/public/partners/rust.svg @@ -0,0 +1 @@ +Rust \ No newline at end of file diff --git a/public/partners/solidity.svg b/public/partners/solidity.svg new file mode 100644 index 00000000..19391843 --- /dev/null +++ b/public/partners/solidity.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/partners/stripe.svg b/public/partners/stripe.svg new file mode 100644 index 00000000..af09b91c --- /dev/null +++ b/public/partners/stripe.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/partners/trm.svg b/public/partners/trm.svg new file mode 100644 index 00000000..2e65f529 --- /dev/null +++ b/public/partners/trm.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/public/partners/typescript.svg b/public/partners/typescript.svg new file mode 100644 index 00000000..688ddcc2 --- /dev/null +++ b/public/partners/typescript.svg @@ -0,0 +1 @@ +TypeScript \ No newline at end of file diff --git a/public/partners/viem.svg b/public/partners/viem.svg new file mode 100644 index 00000000..aefc47b5 --- /dev/null +++ b/public/partners/viem.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/public/partners/visa.svg b/public/partners/visa.svg new file mode 100644 index 00000000..fc4104fa --- /dev/null +++ b/public/partners/visa.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/public/partners/wagmi.svg b/public/partners/wagmi.svg new file mode 100644 index 00000000..4e28590c --- /dev/null +++ b/public/partners/wagmi.svg @@ -0,0 +1,27 @@ + + + + + + + diff --git a/public/stickers/sticker4/bg-bar.svg b/public/stickers/sticker4/bg-bar.svg new file mode 100644 index 00000000..7e9d5c9e --- /dev/null +++ b/public/stickers/sticker4/bg-bar.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/stickers/sticker4/bg-body.svg b/public/stickers/sticker4/bg-body.svg new file mode 100644 index 00000000..a1059b9f --- /dev/null +++ b/public/stickers/sticker4/bg-body.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/stickers/sticker4/bg-top.svg b/public/stickers/sticker4/bg-top.svg new file mode 100644 index 00000000..5f8cb724 --- /dev/null +++ b/public/stickers/sticker4/bg-top.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/stickers/sticker4/orbit-back.svg b/public/stickers/sticker4/orbit-back.svg new file mode 100644 index 00000000..8e28c1e2 --- /dev/null +++ b/public/stickers/sticker4/orbit-back.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/public/stickers/sticker4/orbit-front-a.svg b/public/stickers/sticker4/orbit-front-a.svg new file mode 100644 index 00000000..99de9df6 --- /dev/null +++ b/public/stickers/sticker4/orbit-front-a.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/stickers/sticker4/orbit-front-b.svg b/public/stickers/sticker4/orbit-front-b.svg new file mode 100644 index 00000000..1dc30cfc --- /dev/null +++ b/public/stickers/sticker4/orbit-front-b.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/stickers/sticker4/tempo.svg b/public/stickers/sticker4/tempo.svg new file mode 100644 index 00000000..ee3ce415 --- /dev/null +++ b/public/stickers/sticker4/tempo.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/stickers/sticker5/oval-a.svg b/public/stickers/sticker5/oval-a.svg new file mode 100644 index 00000000..11cc59b1 --- /dev/null +++ b/public/stickers/sticker5/oval-a.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/public/stickers/sticker5/oval-b.svg b/public/stickers/sticker5/oval-b.svg new file mode 100644 index 00000000..4eb4bea7 --- /dev/null +++ b/public/stickers/sticker5/oval-b.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/public/stickers/sticker5/oval-ring.svg b/public/stickers/sticker5/oval-ring.svg new file mode 100644 index 00000000..34f79312 --- /dev/null +++ b/public/stickers/sticker5/oval-ring.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/stickers/sticker5/tempo.svg b/public/stickers/sticker5/tempo.svg new file mode 100644 index 00000000..dbc2eaeb --- /dev/null +++ b/public/stickers/sticker5/tempo.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/scripts/lighthouse.ts b/scripts/lighthouse.ts index fd3acb7f..89a59585 100644 --- a/scripts/lighthouse.ts +++ b/scripts/lighthouse.ts @@ -39,6 +39,27 @@ interface PageResult { tti: number } +type LighthouseAudit = { + numericValue?: number +} + +type LighthouseReport = { + runtimeError?: { + code?: string + message: string + } + categories?: { + performance?: { + score?: number + } + } + audits?: Record +} + +type ExecError = Error & { + stderr?: Buffer | string +} + function parseArgs(argv: string[]) { const args = argv.slice(2) const flags = { @@ -78,7 +99,7 @@ function parseArgs(argv: string[]) { const LH_OUTPUT = '/tmp/lighthouse-result.json' -function runLighthouse(url: string, mobile: boolean): any | null { +function runLighthouse(url: string, mobile: boolean): LighthouseReport | null { const preset = mobile ? 'perf' : 'desktop' // Run npx from /tmp to avoid devEngines conflicts in the docs package.json // Use --output-path to avoid pipe truncation on large JSON output @@ -92,7 +113,7 @@ function runLighthouse(url: string, mobile: boolean): any | null { cwd: '/tmp', }) const raw = readFileSync(LH_OUTPUT, 'utf-8') - const report = JSON.parse(raw) + const report = JSON.parse(raw) as LighthouseReport // Check for runtime errors (e.g., page returned 500) if (report.runtimeError?.code) { @@ -104,7 +125,7 @@ function runLighthouse(url: string, mobile: boolean): any | null { } catch (err) { console.error(` ✗ Lighthouse failed for ${url}`) if (err instanceof Error) { - const stderr = (err as any).stderr?.toString() || '' + const stderr = (err as ExecError).stderr?.toString() || '' const meaningful = stderr .split('\n') .filter((l: string) => !l.includes('npm warn') && l.trim()) @@ -116,7 +137,7 @@ function runLighthouse(url: string, mobile: boolean): any | null { } } -function extractMetrics(report: any, page: string): PageResult { +function extractMetrics(report: LighthouseReport, page: string): PageResult { const score = Math.round((report.categories?.performance?.score ?? 0) * 100) const audits = report.audits ?? {} diff --git a/scripts/probe-og.ts b/scripts/probe-og.ts index c0424ff8..b8c5b974 100644 --- a/scripts/probe-og.ts +++ b/scripts/probe-og.ts @@ -61,8 +61,6 @@ const subsectionMap: Record = { stablecoins: 'STABLECOINS', 'use-cases': 'USE CASES', tempo: 'TEMPO', - zones: 'ZONES', - 'private-zones': 'PRIVATE ZONES', upgrades: 'UPGRADES', api: 'API', guides: 'GUIDES', diff --git a/src/components/ConnectWallet.tsx b/src/components/ConnectWallet.tsx index 5bf029bd..806b4f95 100644 --- a/src/components/ConnectWallet.tsx +++ b/src/components/ConnectWallet.tsx @@ -18,7 +18,10 @@ const mainnetParams = { export function ConnectWallet({ showAddChain = true, network = 'testnet', -}: { showAddChain?: boolean; network?: 'mainnet' | 'testnet' }) { +}: { + showAddChain?: boolean + network?: 'mainnet' | 'testnet' +}) { const { address, chain, connector } = useConnection() const connect = useConnect() const connectors = useConnectors() diff --git a/src/components/DocsHeader.tsx b/src/components/DocsHeader.tsx new file mode 100644 index 00000000..008cd45d --- /dev/null +++ b/src/components/DocsHeader.tsx @@ -0,0 +1,1179 @@ +'use client' + +import { type ReactNode, useEffect, useLayoutEffect, useRef, useState } from 'react' +import { useConfig } from 'vocs' + +type MegaLink = { + label: string + desc: string + href: string + icon: ReactNode +} + +type MegaColumn = { title: string; items: MegaLink[] } +type MegaMenuData = { columns: MegaColumn[]; variant?: 'columns' | 'vertical' } +type MenuItem = { label: string; href: string; mega?: MegaMenuData } + +const DOCS_BASE_PATH = '/docs' +const TEMPO_AI_GUIDE_URL = `${DOCS_BASE_PATH}/guide/using-tempo-with-ai` +const TEMPO_DOCS_SKILL_URL = `${TEMPO_AI_GUIDE_URL}#docs-skill` +const TEMPO_PLUGIN_URL = `${TEMPO_AI_GUIDE_URL}#install-tempo-plugins` +const TEMPO_MCP_URL = 'https://mcp.tempo.xyz' +const TEMPO_SDK_DOCS_URL = `${DOCS_BASE_PATH}/sdk` + +function featurePath(slug: string) { + const featurePaths: Record = { + transactions: '/build/tempo-transactions', + tokens: '/build/tip20-tokens', + } + return featurePaths[slug] ?? '/build' +} + +function isExternal(href: string) { + return !href.startsWith('/') && !href.startsWith('#') +} + +function normalizePath(pathname: string) { + return pathname || '/' +} + +function pathMatches(pathname: string, href: string) { + return pathname === href || pathname.startsWith(`${href}/`) +} + +export function usePathname() { + const [pathname, setPathname] = useState('/') + + useLayoutEffect(() => { + const update = () => setPathname(normalizePath(window.location.pathname)) + update() + window.addEventListener('popstate', update) + window.addEventListener('hashchange', update) + return () => { + window.removeEventListener('popstate', update) + window.removeEventListener('hashchange', update) + } + }, []) + + return pathname +} + +function isActiveMenuItem(pathname: string, item: MenuItem) { + if (item.label === 'Build') return pathname === '/' || pathname.startsWith('/build') + if (item.label === 'Resources') + return pathname === '/docs/sdk' || pathname.startsWith('/docs/sdk/') + if (item.label === 'Docs') return pathname === '/docs' || pathname.startsWith('/docs/') + return !isExternal(item.href) && pathMatches(pathname, item.href) +} + +function Anchor({ + href, + children, + onFocus, + onPointerEnter, + ...props +}: React.AnchorHTMLAttributes) { + if (!href) return {children} + if (isExternal(href)) { + return ( + + {children} + + ) + } + return ( + { + prefetchPath(href) + onFocus?.(event) + }} + onPointerEnter={(event) => { + prefetchPath(href) + onPointerEnter?.(event) + }} + > + {children} + + ) +} + +const prefetchedPaths = new Set() + +function appendLink(rel: string, href: string, attributes: Record = {}) { + if (typeof document === 'undefined') return + const selector = `link[rel="${rel}"][href="${href}"]` + if (document.head.querySelector(selector)) return + + const link = document.createElement('link') + link.rel = rel + link.href = href + for (const [name, value] of Object.entries(attributes)) { + link.setAttribute(name, value) + } + document.head.appendChild(link) +} + +function prefetchPath(href: string) { + if (typeof document === 'undefined') return + if (!href.startsWith('/') || prefetchedPaths.has(href)) return + prefetchedPaths.add(href) + + appendLink('prefetch', href, { as: 'document' }) +} + +function warmMarketingApp() { + prefetchPath('/') +} + +function ArrowUpRight({ className }: { className?: string }) { + return ( + // biome-ignore lint/a11y/noSvgWithoutTitle: Decorative external-link icon. + + + + ) +} + +function TempoLogo({ className }: { className?: string }) { + return ( + + ) +} + +function Glyph({ children }: { children: ReactNode }) { + return ( + // biome-ignore lint/a11y/noSvgWithoutTitle: Decorative mega-menu icon. + + {children} + + ) +} + +function TransactionsIcon() { + return ( + + + + + ) +} + +function TokensIcon() { + return ( + + + + + ) +} + +function DocsIcon() { + return ( + + + + + ) +} + +function WalletIcon() { + return ( + + + + + + ) +} + +function ApiIcon() { + return ( + + + + + ) +} + +function ExplorerIcon() { + return ( + + + + + + ) +} + +function McpIcon() { + return ( + + + + + + ) +} + +function TerminalIcon() { + return ( + + + + + ) +} + +const protocolMenu: MegaMenuData = { + variant: 'vertical', + columns: [ + { + title: 'Transactions', + items: [ + { + label: 'Tempo Transactions', + desc: 'Flexible transactions for batching, fee sponsorship, scheduling, and more', + href: featurePath('transactions'), + icon: , + }, + ], + }, + { + title: 'Assets & privacy', + items: [ + { + label: 'TIP-20 tokens', + desc: 'Stablecoin-first token standard for payments', + href: featurePath('tokens'), + icon: , + }, + ], + }, + ], +} + +const developersMenu: MegaMenuData = { + columns: [ + { + title: 'Documentation', + items: [ + { + label: 'Docs', + desc: 'Guides, references & quickstart', + href: DOCS_BASE_PATH, + icon: , + }, + ], + }, + { + title: 'Tools', + items: [ + { + label: 'Wallet', + desc: 'A Tempo-first wallet for your agents', + href: 'https://wallet.tempo.xyz', + icon: , + }, + { + label: 'TIDX', + desc: 'Raw indexer queries & event streams', + href: `${DOCS_BASE_PATH}/developer-tools/indexer`, + icon: , + }, + { + label: 'Tempo Explorer', + desc: 'Search blocks, txs & tokens', + href: 'https://explorer.tempo.xyz', + icon: , + }, + ], + }, + { + title: 'Libraries', + items: [ + { + label: 'MPP', + desc: 'Open protocol for agentic payments', + href: 'https://mpp.dev/', + icon: , + }, + { + label: 'SDKs', + desc: 'TypeScript, Rust, Go & Foundry', + href: TEMPO_SDK_DOCS_URL, + icon: , + }, + ], + }, + ], +} + +const menu: MenuItem[] = [ + { label: 'Build', href: '/#protocol', mega: protocolMenu }, + { label: 'Resources', href: `${DOCS_BASE_PATH}/guide`, mega: developersMenu }, + { label: 'Performance', href: '/performance' }, + { label: 'Docs', href: DOCS_BASE_PATH }, +] + +function ActiveSquare({ activeKey }: { activeKey: string }) { + return ( + // biome-ignore lint/a11y/noSvgWithoutTitle: Decorative active-state indicator. + + {[0, 4, 8].flatMap((y) => + [0, 4, 8].map((x) => ( + + )), + )} + + ) +} + +function MegaItem({ link }: { link: MegaLink }) { + const external = isExternal(link.href) + return ( + + {external ? ( + + ) : null} + + {link.icon} + + + {link.label} + + {link.desc} + + + + ) +} + +function MegaMenu({ data }: { data: MegaMenuData }) { + const columns = data.columns + + if (data.variant === 'vertical') { + return ( +
+
    + {columns + .flatMap((column) => column.items) + .map((item) => ( +
  • + +
  • + ))} +
+
+ ) + } + + return ( +
+ {columns.map((column) => { + return ( +
+
    + {column.items.map((item) => ( +
  • + +
  • + ))} +
+
+ ) + })} +
+ ) +} + +function MenuIcon() { + return ( + // biome-ignore lint/a11y/noSvgWithoutTitle: Button provides the accessible label. + + + + ) +} + +function CloseIcon() { + return ( + // biome-ignore lint/a11y/noSvgWithoutTitle: Button provides the accessible label. + + + + ) +} + +function GearIcon() { + return ( + // biome-ignore lint/a11y/noSvgWithoutTitle: Decorative icon next to visible text. + + + + + ) +} + +function Chevron({ open }: { open: boolean }) { + return ( + // biome-ignore lint/a11y/noSvgWithoutTitle: Decorative disclosure icon; button exposes expanded state. + + + + ) +} + +function CopyIcon() { + return ( + // biome-ignore lint/a11y/noSvgWithoutTitle: Parent copy button provides the accessible label. + + + + + ) +} + +function CheckIcon() { + return ( + // biome-ignore lint/a11y/noSvgWithoutTitle: Parent copy button provides the accessible label. + + + + ) +} + +const mcpCommands = [ + { label: 'Claude', prefix: 'claude mcp add --transport http tempo ' }, + { label: 'Codex', prefix: 'codex mcp add tempo --url ' }, + { label: 'Amp', prefix: 'amp mcp add --transport http tempo ' }, +] + +function AgentMenuItem(props: { + href: string + label: string + desc: string + icon: ReactNode + onClick?: () => void +}) { + const { href, label, desc, icon, onClick } = props + const external = isExternal(href) + return ( + + {external ? ( + + ) : null} + + {icon} + + + {label} + + {desc} + + + + ) +} + +function AgentsPanel({ + variant = 'desktop', + onNavigate, +}: { + variant?: 'desktop' | 'mobile' + onNavigate?: () => void +}) { + const desktop = variant === 'desktop' + const [activeCommandIndex, setActiveCommandIndex] = useState(0) + const [copied, setCopied] = useState(false) + const activeCommand = mcpCommands[activeCommandIndex] + + const copyCommand = async () => { + try { + await navigator.clipboard.writeText(activeCommand.prefix + TEMPO_MCP_URL) + setCopied(true) + setTimeout(() => setCopied(false), 1500) + } catch {} + } + + return ( +
+ {desktop ? ( +

+ Use Tempo with AI +

+ ) : null} +
+
+
+ + + + + + Tempo MCP server + + + Give agents search and read tools for Tempo docs + + +
+
+
+ {mcpCommands.map((item, index) => { + const active = index === activeCommandIndex + return ( + + ) + })} +
+ +
+
+ } + onClick={onNavigate} + /> + } + onClick={onNavigate} + /> +
+
+ ) +} + +function megaLinks(data: MegaMenuData) { + return data.columns.flatMap((column) => column.items) +} + +type SidebarNode = { + text?: string + link?: string + collapsed?: boolean + items?: SidebarNode[] +} + +// The docs sidebar is configured in vocs.config.ts (keyed by path). Resolve the +// entry that best matches the current path so the mobile menu mirrors the +// desktop sidebar. +export function resolveSidebarItems(sidebar: unknown, pathname: string): SidebarNode[] { + if (!sidebar) return [] + if (Array.isArray(sidebar)) return sidebar as SidebarNode[] + if (typeof sidebar !== 'object') return [] + + const entries = sidebar as Record + let bestKey: string | null = null + for (const key of Object.keys(entries)) { + if (pathname === key || pathname.startsWith(key === '/' ? '/' : `${key}/`)) { + if (bestKey === null || key.length > bestKey.length) bestKey = key + } + } + const entry = entries[bestKey ?? '/docs'] ?? Object.values(entries)[0] + if (!entry) return [] + return Array.isArray(entry) ? entry : (entry.items ?? []) +} + +// Vocs treats a sidebar link as active only on an exact path match (ignoring +// trailing slashes), so an index link like `/docs` doesn't light up on every +// sub-page. +function pathIsExact(pathname: string, link: string) { + return pathname.replace(/\/+$/, '') === link.replace(/\/+$/, '') +} + +function nodeContainsActive(node: SidebarNode, pathname: string): boolean { + if (node.link && pathIsExact(pathname, node.link)) return true + return Boolean(node.items?.some((child) => nodeContainsActive(child, pathname))) +} + +function SidebarLeaf({ + node, + pathname, + depth, + onNavigate, +}: { + node: SidebarNode + pathname: string + depth: number + onNavigate: () => void +}) { + const active = node.link ? pathIsExact(pathname, node.link) : false + const external = node.link ? isExternal(node.link) : false + return ( + 1 ? `${(depth - 1) * 12}px` : undefined }} + className={`flex items-center gap-1.5 py-2 font-sans text-[15px] tracking-[0] transition-colors ${ + active ? 'text-foreground' : 'text-foreground/50 hover:text-foreground' + }`} + > + {active ? : null} + {node.text} + {external ? : null} + + ) +} + +export function SidebarNodes({ + nodes, + pathname, + depth, + onNavigate, +}: { + nodes: SidebarNode[] + pathname: string + depth: number + onNavigate: () => void +}) { + return ( + <> + {nodes.map((node, i) => { + const key = `${node.text ?? node.link ?? 'node'}-${i}` + const hasChildren = Array.isArray(node.items) && node.items.length > 0 + + // Leaf link. + if (!hasChildren) { + return ( + + ) + } + + // Non-collapsible section (e.g. "Build on Tempo"): a heading + children. + if (node.collapsed === undefined) { + return ( +
+

{node.text}

+ +
+ ) + } + + // Collapsible subgroup (e.g. "Make Payments"): expandable disclosure. + const open = !node.collapsed || nodeContainsActive(node, pathname) + return ( +
1 ? `${(depth - 1) * 12}px` : undefined }} + > + + {node.text} + + + +
+ ) + })} + + ) +} + +export default function DocsHeader() { + const pathname = usePathname() + const config = useConfig() + const docsSidebarItems = resolveSidebarItems(config?.sidebar, pathname) + const [open, setOpen] = useState(false) + const [expanded, setExpanded] = useState(null) + const [activeMenu, setActiveMenu] = useState(null) + const [geom, setGeom] = useState<{ x: number; w: number; h: number } | null>(null) + const [morphing, setMorphing] = useState(false) + const headerRef = useRef(null) + const triggerRefs = useRef(new Map()) + const panelRefs = useRef(new Map()) + const prevActive = useRef(null) + const closeTimer = useRef | null>(null) + + const cancelClose = () => { + if (closeTimer.current) clearTimeout(closeTimer.current) + } + const openMenu = (key: string) => { + cancelClose() + setActiveMenu(key) + } + const scheduleClose = () => { + cancelClose() + closeTimer.current = setTimeout(() => setActiveMenu(null), 120) + } + + useLayoutEffect(() => { + if (!activeMenu) { + prevActive.current = null + return + } + const panel = panelRefs.current.get(activeMenu) + const trigger = triggerRefs.current.get(activeMenu) + const header = headerRef.current + if (!panel || !trigger || !header) return + const w = panel.offsetWidth + const h = panel.offsetHeight + const triggerRect = trigger.getBoundingClientRect() + const headerRect = header.getBoundingClientRect() + const raw = + activeMenu === 'For agents' + ? triggerRect.right - headerRect.left - w + : triggerRect.left - headerRect.left + triggerRect.width / 2 - w / 2 + const x = Math.round(Math.min(Math.max(raw, 12), headerRect.width - w - 12)) + setMorphing(prevActive.current !== null) + setGeom({ x, w, h }) + prevActive.current = activeMenu + }, [activeMenu]) + + const dropdowns: { key: string; panel: ReactNode }[] = [ + ...menu.flatMap((item) => + item.mega ? [{ key: item.label, panel: }] : [], + ), + { key: 'For agents', panel: }, + ] + + const close = () => { + setOpen(false) + setExpanded(null) + } + + useEffect(() => { + warmMarketingApp() + }, []) + + return ( +
+
+ +
+ +
+
+
+ {/* biome-ignore lint/a11y/noStaticElementInteractions: Hover bridge keeps the shared menu surface open. */} +
+
+ {dropdowns.map(({ key, panel }) => ( +
{ + if (element) panelRefs.current.set(key, element) + else panelRefs.current.delete(key) + }} + className={`absolute top-0 left-0 w-max transition-opacity duration-150 ease-out motion-reduce:transition-none ${ + activeMenu === key ? 'opacity-100' : 'pointer-events-none opacity-0' + }`} + > + {panel} +
+ ))} +
+
+
+
+
+ +
+
+ {menu.map((item) => { + const active = isActiveMenuItem(pathname, item) + // The "Docs" item is the entry point to the docs sidebar: on docs + // pages it expands inline to reveal the page tree instead of being a + // plain link, matching the mega-menu accordion pattern. + const isDocsEntry = item.label === 'Docs' && docsSidebarItems.length > 0 + if (isDocsEntry) { + return ( +
+ +
+
+
+ +
+
+
+
+ ) + } + return item.mega ? ( +
+ +
+
+
+ {megaLinks(item.mega).map((sub) => ( + + {sub.label} + {isExternal(sub.href) ? : null} + + ))} +
+
+
+
+ ) : ( + + {active ? : null} + {item.label} + + ) + })} +
+ +
+
+ +
+
+
+
+
+
+ ) +} diff --git a/src/components/DocsSidebarDrawer.tsx b/src/components/DocsSidebarDrawer.tsx new file mode 100644 index 00000000..061d1950 --- /dev/null +++ b/src/components/DocsSidebarDrawer.tsx @@ -0,0 +1,160 @@ +'use client' + +import { useEffect, useState } from 'react' +import { createPortal } from 'react-dom' +import { useConfig } from 'vocs' +import { resolveSidebarItems, SidebarNodes, usePathname } from './DocsHeader' + +/** + * Mobile-only docs sidebar drawer. + * + * Injects a hamburger toggle into the sticky "On this page" outline row (Vocs' + * `[data-v-outline-mobile]` bar) so the docs sidebar stays reachable while + * scrolling, then slides the sidebar tree in from the left. On pages without an + * outline row the full-screen menu's "Docs" section remains the fallback. + */ +export default function DocsSidebarDrawer() { + const pathname = usePathname() + const config = useConfig() + const items = resolveSidebarItems(config?.sidebar, pathname) + const [host, setHost] = useState(null) + const [open, setOpen] = useState(false) + + // Mount a portal host inside the outline row, re-attaching whenever the route + // changes (Vocs re-creates the row per page). + useEffect(() => { + let span: HTMLElement | null = null + + const attach = () => { + const row = document.querySelector('[data-v-outline-mobile] > div') + if (!row || span) return Boolean(span) + span = document.createElement('span') + span.dataset.docsSidebarToggle = '' + span.style.display = 'inline-flex' + span.style.marginRight = '0.5rem' + row.prepend(span) + setHost(span) + return true + } + + if (attach()) { + return () => { + span?.remove() + setHost(null) + } + } + + const observer = new MutationObserver(() => { + if (attach()) observer.disconnect() + }) + observer.observe(document.body, { childList: true, subtree: true }) + return () => { + observer.disconnect() + span?.remove() + setHost(null) + } + }, []) + + // Close on route change. + // biome-ignore lint/correctness/useExhaustiveDependencies: close drawer when the path changes. + useEffect(() => { + setOpen(false) + }, [pathname]) + + // Close on Escape + lock body scroll while open. + useEffect(() => { + if (!open) return + const onKey = (event: KeyboardEvent) => { + if (event.key === 'Escape') setOpen(false) + } + document.addEventListener('keydown', onKey) + const prevOverflow = document.body.style.overflow + document.body.style.overflow = 'hidden' + return () => { + document.removeEventListener('keydown', onKey) + document.body.style.overflow = prevOverflow + } + }, [open]) + + if (items.length === 0) return null + + const toggle = host + ? createPortal( + , + host, + ) + : null + + return ( + <> + {toggle} +
+ {/* Backdrop */} + {/* biome-ignore lint/a11y/noStaticElementInteractions: backdrop dismiss. */} + {/* biome-ignore lint/a11y/useKeyWithClickEvents: Escape handled globally. */} +
setOpen(false)} + className={`absolute inset-0 bg-black/40 transition-opacity duration-200 ${ + open ? 'opacity-100' : 'opacity-0' + }`} + /> + {/* Panel */} +
+
+ + Documentation + + +
+
+ setOpen(false)} + /> +
+
+
+ + ) +} diff --git a/src/components/PostHogSetup.tsx b/src/components/PostHogSetup.tsx index c0afb351..cc9032f9 100644 --- a/src/components/PostHogSetup.tsx +++ b/src/components/PostHogSetup.tsx @@ -2,7 +2,7 @@ import { useEffect } from 'react' -function PostHogInitializer() { +function PostHogInitializer({ site }: { site: string }) { useEffect(() => { const posthogKey = import.meta.env.VITE_POSTHOG_KEY const posthogHost = import.meta.env.VITE_POSTHOG_HOST @@ -25,7 +25,7 @@ function PostHogInitializer() { }, }, }) - posthog.register({ site: 'docs' }) + posthog.register({ site }) } if ('requestIdleCallback' in window) { @@ -35,16 +35,16 @@ function PostHogInitializer() { const timeoutId = globalThis.setTimeout(init, 1) return () => globalThis.clearTimeout(timeoutId) - }, []) + }, [site]) return null } -export default function PostHogSetup() { +export default function PostHogSetup({ site = 'docs' }: { site?: string }) { const posthogKey = import.meta.env.VITE_POSTHOG_KEY const posthogHost = import.meta.env.VITE_POSTHOG_HOST if (!posthogKey || !posthogHost) return null - return + return } diff --git a/src/components/TipsList.tsx b/src/components/TipsList.tsx index 1a404d34..8df8e12e 100644 --- a/src/components/TipsList.tsx +++ b/src/components/TipsList.tsx @@ -1,7 +1,7 @@ import { Link } from 'vocs' import FileText from '~icons/lucide/file-text' -const modules = import.meta.glob('../pages/protocol/tips/tip-*.mdx', { +const modules = import.meta.glob('../pages/docs/protocol/tips/tip-*.mdx', { eager: true, }) as Record< string, diff --git a/src/components/guides/AccountsSignIn.tsx b/src/components/guides/AccountsSignIn.tsx deleted file mode 100644 index eef3aaca..00000000 --- a/src/components/guides/AccountsSignIn.tsx +++ /dev/null @@ -1,42 +0,0 @@ -'use client' -import { useConnect, useConnection, useConnectors, useDisconnect } from 'wagmi' -import { Button, TempoMarkBoxed } from './Demo' - -export function AccountsSignIn() { - const account = useConnection() - const connect = useConnect() - const disconnect = useDisconnect() - const connector = useTempoWalletConnector() - - if (!connector) return null - - if (account.address) - return ( -
- -
- ) - - if (connect.isPending) - return ( -
- -
- ) - - return ( -
- -
- ) -} - -function useTempoWalletConnector() { - const connectors = useConnectors() - return connectors.find((c: { id: string }) => c.id === 'xyz.tempo') -} diff --git a/src/components/guides/Demo.tsx b/src/components/guides/Demo.tsx index bd4741e6..a6cf33cb 100644 --- a/src/components/guides/Demo.tsx +++ b/src/components/guides/Demo.tsx @@ -1,12 +1,10 @@ 'use client' -import { useQuery, useQueryClient } from '@tanstack/react-query' +import { useQueryClient } from '@tanstack/react-query' import type { VariantProps } from 'cva' import * as React from 'react' -import { type Address, type BaseError, createClient, formatUnits } from 'viem' +import { type Address, type BaseError, formatUnits } from 'viem' import { tempoModerato } from 'viem/chains' -import { tempoActions } from 'viem/tempo' -import { http as zoneHttp, zoneModerato } from 'viem/tempo/zones' -import { useAccount, useConnect, useConnections, useConnectorClient, useDisconnect } from 'wagmi' +import { useAccount, useConnect, useConnections, useDisconnect } from 'wagmi' import { Hooks } from 'wagmi/tempo' import LucideCheck from '~icons/lucide/check' import LucideCopy from '~icons/lucide/copy' @@ -16,12 +14,6 @@ import LucideRotateCcw from '~icons/lucide/rotate-ccw' import LucideWalletCards from '~icons/lucide/wallet-cards' import { cva, cx } from '../../../cva.config' import { usePostHogTracking } from '../../lib/posthog' -import { - getZoneRpcHttpUrl, - getZoneRpcTransportConfig, - moderatoZoneRpcUrls, -} from '../../lib/private-zones.ts' -import { useRootWebAuthnAccount } from '../../lib/useRootWebAuthnAccount.ts' import { useTempoWalletConnector, useWebAuthnConnector } from '../../wagmi.config' import { Container as ParentContainer } from '../Container' import { alphaUsd } from './tokens' @@ -31,13 +23,6 @@ export { alphaUsd, betaUsd, pathUsd, thetaUsd } from './tokens' export const FAKE_RECIPIENT = '0xbeefcafe54750903ac1c8909323af7beb21ea2cb' export const FAKE_RECIPIENT_2 = '0xdeadbeef54750903ac1c8909323af7beb21ea2cb' -type ZoneBalance = { - label: string - token: Address - zone: number - feeToken?: Address | undefined -} - export function useHydrated() { const [hydrated, setHydrated] = React.useState(false) @@ -140,7 +125,6 @@ export function Container( footerVariant: 'balances' tokens: Address[] balanceSource?: 'webAuthn' | 'wallet' | undefined - zoneBalances?: ZoneBalance[] | undefined } | { footerVariant: 'source' @@ -183,11 +167,7 @@ export function Container( const footerElement = React.useMemo(() => { if (props.footerVariant === 'balances') return ( - + ) if (props.footerVariant === 'source') return return null @@ -229,12 +209,6 @@ export function Container( } export namespace Container { - type ZoneClientLike = { - token: { - getBalance: (parameters: { account: Address; token: Address }) => Promise - } - } - function BalancesFooterItem(props: { address: Address; token: Address }) { const queryClient = useQueryClient() const { address, token } = props @@ -289,74 +263,8 @@ export namespace Container { ) } - function ZoneBalancesFooterItem(props: ZoneBalance & { address: Address }) { - const { address, token, zone } = props - const { data: connectorClient } = useConnectorClient() - const { data: rootWebAuthnAccount } = useRootWebAuthnAccount() - const zoneRpcUrl = - moderatoZoneRpcUrls[zone as keyof typeof moderatoZoneRpcUrls] ?? - ( - connectorClient?.chain as - | { zones?: Record } - | undefined - )?.zones?.[zone]?.rpcUrls.default.http[0] - const zoneClient = React.useMemo( - () => - rootWebAuthnAccount && zoneRpcUrl - ? (createClient({ - account: rootWebAuthnAccount, - chain: zoneModerato(zone), - transport: zoneHttp( - getZoneRpcHttpUrl(zone, zoneRpcUrl), - getZoneRpcTransportConfig(zone, zoneRpcUrl), - ), - }).extend(tempoActions()) as unknown as ZoneClientLike) - : undefined, - [rootWebAuthnAccount, zone, zoneRpcUrl], - ) - const { data: metadata, isPending: metadataIsPending } = Hooks.token.useGetMetadata({ - token, - }) - const { data: balance, isPending: balanceIsPending } = useQuery({ - enabled: Boolean(address && zoneClient), - queryKey: ['demo-zone-balance', address, zone, token], - queryFn: async () => { - if (!zoneClient) throw new Error('zone client not ready') - - return zoneClient.token.getBalance({ - account: address, - token, - }) - }, - refetchInterval: (query) => { - if (query.state.error || query.state.data === undefined) return false - - return 1_500 - }, - refetchOnReconnect: false, - refetchOnWindowFocus: false, - retry: false, - staleTime: 1_000, - }) - - if (balanceIsPending || metadataIsPending || balance === undefined || metadata === undefined) { - return - } - - return ( - - {formatUnits(balance, metadata.decimals)} - {metadata.symbol} - - ) - } - - export function BalancesFooter(props: { - address?: string | undefined - tokens: Address[] - zoneBalances?: ZoneBalance[] | undefined - }) { - const { address, tokens, zoneBalances } = props + export function BalancesFooter(props: { address?: string | undefined; tokens: Address[] }) { + const { address, tokens } = props const personalBalanceLabel = tokens.length > 1 ? 'Personal balances' : 'Personal balance' return ( @@ -374,21 +282,6 @@ export namespace Container { )}
- {address && - zoneBalances && - zoneBalances.length > 0 && - zoneBalances.map((zoneBalance) => ( -
- {zoneBalance.label} balance -
-
- -
-
- ))}
) } diff --git a/src/components/guides/EmbedPasskeys.tsx b/src/components/guides/EmbedPasskeys.tsx deleted file mode 100644 index 6e8a1f53..00000000 --- a/src/components/guides/EmbedPasskeys.tsx +++ /dev/null @@ -1,99 +0,0 @@ -'use client' -import { useAccount, useConnect, useDisconnect } from 'wagmi' -import { useWebAuthnConnector } from '../../wagmi.config' -import { Button, useHydrated } from './Demo' - -export function EmbedPasskeys() { - const account = useAccount() - const connect = useConnect() - const disconnect = useDisconnect() - const hydrated = useHydrated() - const connector = useWebAuthnConnector() - const busy = connect.isPending || disconnect.isPending - - if (!hydrated || !connector) - return ( -
- -
- ) - - if (busy) - return ( -
- -
- ) - - if (account.address) - return ( -
- -
- ) - - return -} - -export function SignInButtons() { - const connect = useConnect() - const disconnect = useDisconnect() - const hydrated = useHydrated() - const connector = useWebAuthnConnector() - const busy = connect.isPending || disconnect.isPending - const isE2E = import.meta.env.VITE_E2E === 'true' - - if (!hydrated || !connector) - return ( -
- -
- ) - - if (busy) - return ( -
- -
- ) - - return ( -
- - -
- ) -} diff --git a/src/components/guides/VirtualAddressesLiveDemo.tsx b/src/components/guides/VirtualAddressesLiveDemo.tsx index 5e60dc2b..fdef1d32 100644 --- a/src/components/guides/VirtualAddressesLiveDemo.tsx +++ b/src/components/guides/VirtualAddressesLiveDemo.tsx @@ -98,6 +98,7 @@ function PasskeyLogin() { const connect = useConnect() const disconnect = useDisconnect() const connector = useWebAuthnConnector() + const isE2E = import.meta.env.VITE_E2E === 'true' return connect.isPending ? ( - ) - - let stepThreeAction: React.ReactNode - if (!hasRootBalance) { - stepThreeAction = ( - - ) - } else if (depositSetupQuery.isError) { - stepThreeAction = ( - - ) - } else if (depositSetupQuery.isPending || depositSetupQuery.data === undefined) { - stepThreeAction = ( - - ) - } else { - stepThreeAction = ( - - ) - } - - return ( - <> - - - - {rootReceipt && ( - - - - - )} - - - - -

- Your public-chain deposit is already submitted. This last step polls the private{' '} - {ZONE_LABEL} balance every 1.5 seconds until the post-fee amount appears. -

-
-
- - ) -} - -function DisconnectedZoneFlow(props: { mode: DepositMode }) { - const { mode } = props - - return ( - <> - - - - - ) -} - -function DepositModeSelector(props: { mode: DepositMode; onChange: (mode: DepositMode) => void }) { - const { mode, onChange } = props - - return ( -
-
-
-

Deposit mode

-

- Plaintext reveals both the recipient and memo of the deposit, while encrypted only lets - the sequencer see those details. -

-
-
- {[ - ['plaintext', 'Plaintext'], - ['encrypted', 'Encrypted'], - ].map(([value, label]) => { - const selected = mode === value - - return ( - - ) - })} -
-
-
- ) -} - -function StepBody(props: React.PropsWithChildren) { - return ( -
-
-
{props.children}
-
-
- ) -} - -function DetailLine(props: { label: string; value: string; dataTestId?: string | undefined }) { - const { dataTestId, label, value } = props - - return ( -
- {label} - - {value} - -
- ) -} - -function getDepositActionLabel(parameters: { isPending: boolean }) { - return parameters.isPending ? 'Depositing pathUSD' : 'Deposit 100 pathUSD' -} - -function getSubmitStepTitle(mode: DepositMode) { - return mode === 'encrypted' - ? `Fund and submit the encrypted deposit for 100 pathUSD into ${ZONE_LABEL}.` - : `Fund and submit the deposit for 100 pathUSD into ${ZONE_LABEL}.` -} - -function getConfirmationStepTitle(mode: DepositMode) { - return mode === 'encrypted' - ? `Wait for ${ZONE_LABEL} to credit the encrypted deposit.` - : `Wait for ${ZONE_LABEL} to credit the deposit.` -} - -function getNetZoneDepositAmount(amount: bigint, depositFee: bigint) { - if (depositFee > amount) { - throw new Error( - `Zone portal deposit fee ${depositFee.toString()} is greater than deposit amount ${amount.toString()}.`, - ) - } - - return amount - depositFee -} diff --git a/src/components/guides/zones/SendTokensAcrossZones.tsx b/src/components/guides/zones/SendTokensAcrossZones.tsx deleted file mode 100644 index 72947f90..00000000 --- a/src/components/guides/zones/SendTokensAcrossZones.tsx +++ /dev/null @@ -1,710 +0,0 @@ -'use client' -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' -import * as React from 'react' -import { createClient, encodeAbiParameters, type Hex, parseAbiItem, parseUnits } from 'viem' -import { Actions, tempoActions } from 'viem/tempo' -import { http as zoneHttp, zoneModerato } from 'viem/tempo/zones' -import { useConnection, useConnectorClient, usePublicClient } from 'wagmi' -import { Hooks } from 'wagmi/tempo' -import { - getZoneRpcHttpUrl, - getZoneRpcTransportConfig, - publicSettlementLookbackBlocks, - routerCallbackGasLimit, - swapAndDepositRouter, - ZONE_A, - ZONE_B, - zeroBytes32, - zoneRpcSyncTimeout, -} from '../../../lib/private-zones.ts' -import { useRootWebAuthnAccount } from '../../../lib/useRootWebAuthnAccount.ts' -import { useZoneAuthorization, type ZoneAuthClientLike } from '../../../lib/useZoneAuthorization.ts' -import { Button, ExplorerLink, Logout, ReceiptHash, Step } from '../Demo' -import { SignInButtons } from '../EmbedPasskeys' -import { pathUsd } from '../tokens' -import { useStickyStepCompletion } from './useStickyStepCompletion.ts' - -const TRANSFER_AMOUNT = parseUnits('25', 6) -const ZONE_GAS_BUFFER = parseUnits('1', 6) - -const portalAbi = [ - { - name: 'calculateDepositFee', - type: 'function', - stateMutability: 'view', - inputs: [], - outputs: [{ type: 'uint128' }], - }, - { - name: 'isTokenEnabled', - type: 'function', - stateMutability: 'view', - inputs: [{ name: 'token', type: 'address' }], - outputs: [{ type: 'bool' }], - }, -] as const - -const targetDepositEvent = parseAbiItem( - 'event DepositMade(bytes32 indexed newCurrentDepositQueueHash, address indexed sender, address token, address to, uint128 netAmount, uint128 fee, bytes32 memo)', -) - -type ZoneClientLike = { - token: { - getBalance: (parameters: { account: Hex; token: Hex }) => Promise - } - zone: { - getAuthorizationTokenInfo: ZoneAuthClientLike['zone']['getAuthorizationTokenInfo'] - requestWithdrawalSync: (parameters: { - account: unknown - amount: bigint - data?: Hex - feeToken: Hex - gas?: bigint - timeout: number - to: Hex - token: Hex - }) => Promise<{ receipt: { blockNumber: bigint; transactionHash: Hex } }> - getWithdrawalFee: (parameters?: { gasLimit?: bigint | undefined }) => Promise - signAuthorizationToken: ZoneAuthClientLike['zone']['signAuthorizationToken'] - } -} - -export function SendTokensAcrossZones() { - const { address } = useConnection() - const connected = Boolean(address) - - return ( - <> - : } - error={undefined} - number={1} - title="Create or use a passkey account on the public chain." - /> - - {address ? ( - - ) : ( - - )} - - ) -} - -function ConnectedZoneFlow(props: { address: Hex }) { - const { address } = props - const queryClient = useQueryClient() - const publicClient = usePublicClient() - const { data: connectorClient } = useConnectorClient() - const { data: rootWebAuthnAccount } = useRootWebAuthnAccount() - const { - data: rootBalance, - isPending: rootBalanceIsPending, - refetch: refetchRootBalance, - } = Hooks.token.useGetBalance({ - account: address, - token: pathUsd, - }) - - const sourceZoneClient = React.useMemo( - () => - rootWebAuthnAccount - ? (createClient({ - account: rootWebAuthnAccount, - chain: zoneModerato(ZONE_A.id), - transport: zoneHttp( - getZoneRpcHttpUrl(ZONE_A.id, ZONE_A.rpcUrl), - getZoneRpcTransportConfig(ZONE_A.id, ZONE_A.rpcUrl), - ), - }).extend(tempoActions()) as unknown as ZoneClientLike) - : undefined, - [rootWebAuthnAccount], - ) - const targetZoneClient = React.useMemo( - () => - rootWebAuthnAccount - ? (createClient({ - account: rootWebAuthnAccount, - chain: zoneModerato(ZONE_B.id), - transport: zoneHttp( - getZoneRpcHttpUrl(ZONE_B.id, ZONE_B.rpcUrl), - getZoneRpcTransportConfig(ZONE_B.id, ZONE_B.rpcUrl), - ), - }).extend(tempoActions()) as unknown as ZoneClientLike) - : undefined, - [rootWebAuthnAccount], - ) - - const sourceFooterQueryKey = React.useMemo( - () => ['demo-zone-balance', address, ZONE_A.id, pathUsd], - [address], - ) - const targetFooterQueryKey = React.useMemo( - () => ['demo-zone-balance', address, ZONE_B.id, pathUsd], - [address], - ) - - const sourceZoneAuthorization = useZoneAuthorization({ - address, - chainId: ZONE_A.chainId, - queryKey: ['guide-private-zones-cross-zone-send-source-auth', address, ZONE_A.id], - zoneClient: sourceZoneClient, - }) - - const sourceZoneBalanceQuery = useQuery({ - enabled: Boolean(sourceZoneClient && sourceZoneAuthorization.isAuthorized), - queryKey: ['guide-private-zones-cross-zone-send-source-balance', address, ZONE_A.id], - queryFn: async () => { - if (!sourceZoneClient) throw new Error('Zone A client not ready') - - return sourceZoneClient.token.getBalance({ - account: address, - token: pathUsd, - }) - }, - staleTime: 30_000, - }) - - const transferPrereqsQuery = useQuery({ - enabled: Boolean(connectorClient && publicClient && sourceZoneAuthorization.isAuthorized), - queryKey: ['guide-private-zones-cross-zone-send-prereqs', address, ZONE_A.id, ZONE_B.id], - queryFn: async () => { - if (!publicClient) throw new Error('public client not ready') - if (!sourceZoneClient) throw new Error('Zone A client not ready') - - const [routedWithdrawalFee, targetDepositFee, targetTokenEnabled] = await Promise.all([ - sourceZoneClient.zone.getWithdrawalFee({ - gasLimit: routerCallbackGasLimit, - }), - publicClient.readContract({ - address: ZONE_B.portalAddress, - abi: portalAbi, - functionName: 'calculateDepositFee', - }), - publicClient.readContract({ - address: ZONE_B.portalAddress, - abi: portalAbi, - functionName: 'isTokenEnabled', - args: [pathUsd], - }), - ]) - - if (!targetTokenEnabled) { - throw new Error(`${ZONE_B.label} is not ready for pathUSD deposits yet.`) - } - if (TRANSFER_AMOUNT <= targetDepositFee) { - throw new Error( - `The ${ZONE_B.label} deposit fee is currently too high for this 25 pathUSD send.`, - ) - } - - return { - minimumTargetIncrease: TRANSFER_AMOUNT - targetDepositFee, - routedWithdrawalFee, - targetDepositFee, - } - }, - staleTime: 30_000, - }) - - const requiredSourceZoneBalance = transferPrereqsQuery.data - ? TRANSFER_AMOUNT + transferPrereqsQuery.data.routedWithdrawalFee + ZONE_GAS_BUFFER - : undefined - const sourceZoneTopUpShortfall = - requiredSourceZoneBalance !== undefined && - sourceZoneBalanceQuery.data !== undefined && - sourceZoneBalanceQuery.data < requiredSourceZoneBalance - ? requiredSourceZoneBalance - sourceZoneBalanceQuery.data - : 0n - const hasEnoughSourceZoneBalance = Boolean( - requiredSourceZoneBalance !== undefined && - sourceZoneBalanceQuery.data !== undefined && - sourceZoneBalanceQuery.data >= requiredSourceZoneBalance, - ) - const sourceZoneBalanceStepComplete = useStickyStepCompletion(hasEnoughSourceZoneBalance) - - const fundMutation = useMutation({ - mutationFn: async () => { - if (!connectorClient) throw new Error('connector client not ready') - - await Actions.faucet.fundSync(connectorClient, { - account: address, - }) - }, - onSuccess: async () => { - await refetchRootBalance() - }, - }) - - const topUpMutation = useMutation({ - mutationFn: async () => { - if (!connectorClient) throw new Error('connector client not ready') - if (sourceZoneTopUpShortfall <= 0n) throw new Error('zone top-up is not required') - - const { receipt } = await Actions.zone.depositSync(connectorClient as never, { - account: connectorClient.account, - amount: sourceZoneTopUpShortfall, - chain: connectorClient.chain as never, - token: pathUsd, - zoneId: ZONE_A.id, - }) - - return { receipt } - }, - onSuccess: async () => { - await refetchRootBalance() - await sourceZoneBalanceQuery.refetch() - }, - }) - - const sendMutation = useMutation({ - mutationFn: async () => { - if (!connectorClient) throw new Error('connector client not ready') - if (!sourceZoneClient) throw new Error('Zone A client not ready') - if (!publicClient) throw new Error('public client not ready') - if (!rootWebAuthnAccount) throw new Error('root account not ready') - if (!transferPrereqsQuery.data) throw new Error('Send prerequisites are not ready') - - const currentSourceBalance = await sourceZoneClient.token.getBalance({ - account: address, - token: pathUsd, - }) - if ( - requiredSourceZoneBalance === undefined || - currentSourceBalance < requiredSourceZoneBalance - ) { - throw new Error('Zone A needs more pathUSD before the send can start.') - } - - const anchorBlock = await publicClient.getBlockNumber() - - const { receipt } = await sourceZoneClient.zone.requestWithdrawalSync({ - account: rootWebAuthnAccount, - amount: TRANSFER_AMOUNT, - data: encodeRouterCallback(address), - feeToken: pathUsd, - gas: routerCallbackGasLimit, - timeout: zoneRpcSyncTimeout, - to: swapAndDepositRouter, - token: pathUsd, - }) - - return { - anchorBlock, - minimumTargetIncrease: transferPrereqsQuery.data.minimumTargetIncrease, - receipt, - targetDepositFee: transferPrereqsQuery.data.targetDepositFee, - } - }, - onSuccess: async () => { - await sourceZoneBalanceQuery.refetch() - await queryClient.invalidateQueries({ queryKey: sourceFooterQueryKey }) - }, - }) - - const settlementQuery = useQuery({ - enabled: Boolean( - publicClient && sendMutation.isSuccess && sendMutation.data?.anchorBlock !== undefined, - ), - queryKey: [ - 'guide-private-zones-cross-zone-send-settlement', - address, - sendMutation.data?.anchorBlock?.toString(), - ], - queryFn: async () => { - if (!publicClient) throw new Error('public client not ready') - if (!sendMutation.data) throw new Error('send submission not ready') - - const fromBlock = - sendMutation.data.anchorBlock > publicSettlementLookbackBlocks - ? sendMutation.data.anchorBlock - publicSettlementLookbackBlocks - : 0n - const latest = await publicClient.getBlockNumber() - const logs = await publicClient.getLogs({ - address: ZONE_B.portalAddress, - event: targetDepositEvent, - fromBlock, - toBlock: latest, - }) - - const match = logs.find((log) => { - const sender = log.args.sender - const token = log.args.token - const recipient = log.args.to - const netAmount = log.args.netAmount - - return ( - typeof sender === 'string' && - typeof token === 'string' && - typeof recipient === 'string' && - typeof netAmount === 'bigint' && - sender.toLowerCase() === swapAndDepositRouter.toLowerCase() && - token.toLowerCase() === pathUsd.toLowerCase() && - recipient.toLowerCase() === address.toLowerCase() && - netAmount >= sendMutation.data.minimumTargetIncrease - ) - }) - - return match ? { txHash: match.transactionHash } : null - }, - refetchInterval: (query) => { - if (query.state.error || query.state.data) return false - - return 2_000 - }, - refetchOnReconnect: false, - refetchOnWindowFocus: false, - retry: false, - }) - - const targetZoneAuthorization = useZoneAuthorization({ - address, - chainId: ZONE_B.chainId, - queryKey: ['guide-private-zones-cross-zone-send-target-auth', address, ZONE_B.id], - zoneClient: targetZoneClient, - }) - - const targetZoneBalanceQuery = useQuery({ - enabled: Boolean( - targetZoneClient && targetZoneAuthorization.isAuthorized && settlementQuery.data, - ), - queryKey: ['guide-private-zones-cross-zone-send-target-balance', address, ZONE_B.id], - queryFn: async () => { - if (!targetZoneClient) throw new Error('Zone B client not ready') - - return targetZoneClient.token.getBalance({ - account: address, - token: pathUsd, - }) - }, - staleTime: 30_000, - refetchOnReconnect: false, - refetchOnWindowFocus: false, - retry: false, - }) - - const hasRootBalance = Boolean(rootBalance && rootBalance > 0n) - const topUpReceipt = topUpMutation.data?.receipt - const routedSendReceipt = sendMutation.data?.receipt - const settlementTxHash = settlementQuery.data?.txHash - const targetBalanceReady = Boolean( - settlementQuery.data && - targetZoneAuthorization.isAuthorized && - targetZoneBalanceQuery.isSuccess, - ) - const sourceAuthIsPreparing = - sourceZoneAuthorization.isChecking || sourceZoneAuthorization.authorizeMutation.isPending - const stepTwoAction = sourceZoneAuthorization.isAuthorized ? undefined : ( - - ) - - React.useEffect(() => { - if (!sourceZoneAuthorization.isAuthorized) return - - void queryClient.invalidateQueries({ queryKey: sourceFooterQueryKey }) - }, [queryClient, sourceFooterQueryKey, sourceZoneAuthorization.isAuthorized]) - - React.useEffect(() => { - if (!targetZoneAuthorization.isAuthorized) return - - void queryClient.invalidateQueries({ queryKey: targetFooterQueryKey }) - }, [queryClient, targetFooterQueryKey, targetZoneAuthorization.isAuthorized]) - - React.useEffect(() => { - if (!topUpMutation.isSuccess || sourceZoneBalanceStepComplete) return - - const interval = window.setInterval(() => { - void sourceZoneBalanceQuery.refetch() - }, 1_500) - - return () => window.clearInterval(interval) - }, [sourceZoneBalanceQuery, sourceZoneBalanceStepComplete, topUpMutation.isSuccess]) - - let stepThreeAction: React.ReactNode - if (sourceZoneBalanceStepComplete) { - stepThreeAction = undefined - } else if (sourceZoneBalanceQuery.isPending || transferPrereqsQuery.isPending) { - stepThreeAction = ( - - ) - } else if (!hasEnoughSourceZoneBalance && !hasRootBalance) { - stepThreeAction = ( - - ) - } else if (!hasEnoughSourceZoneBalance) { - stepThreeAction = ( - - ) - } - - let stepFourAction: React.ReactNode - if (!sourceZoneBalanceStepComplete || transferPrereqsQuery.isPending) { - stepFourAction = undefined - } else if (transferPrereqsQuery.isError) { - stepFourAction = ( - - ) - } else { - stepFourAction = ( - - ) - } - - let stepSixAction: React.ReactNode - if (!settlementQuery.data) { - stepSixAction = undefined - } else if (targetZoneBalanceQuery.isError) { - stepSixAction = ( - - ) - } else if (!targetZoneAuthorization.isAuthorized) { - stepSixAction = ( - - ) - } else if (targetZoneBalanceQuery.isPending) { - stepSixAction = ( - - ) - } - - return ( - <> - - - - {topUpReceipt && ( - - - - - )} - - - - {routedSendReceipt && sendMutation.data && ( - - - - - )} - - - - {settlementTxHash && ( - {settlementTxHash && } - )} - - - - - ) -} - -function DisconnectedZoneFlow() { - return ( - <> - - - - - - - ) -} - -function encodeRouterCallback(recipient: Hex) { - return encodeAbiParameters( - [ - { type: 'bool' }, - { type: 'address' }, - { type: 'address' }, - { type: 'address' }, - { type: 'bytes32' }, - { type: 'uint128' }, - ], - [false, pathUsd, ZONE_B.portalAddress, recipient, zeroBytes32, 0n], - ) -} - -function StepBody(props: React.PropsWithChildren) { - return ( -
-
-
{props.children}
-
-
- ) -} - -function DetailLine(props: { label: string; value: string; dataTestId?: string | undefined }) { - const { dataTestId, label, value } = props - - return ( -
- {label} - - {value} - -
- ) -} diff --git a/src/components/guides/zones/SendTokensWithinZone.tsx b/src/components/guides/zones/SendTokensWithinZone.tsx deleted file mode 100644 index fa0703c2..00000000 --- a/src/components/guides/zones/SendTokensWithinZone.tsx +++ /dev/null @@ -1,437 +0,0 @@ -'use client' -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' -import * as React from 'react' -import { createClient, type Hex, parseUnits } from 'viem' -import { Actions, tempoActions } from 'viem/tempo' -import { http as zoneHttp, zoneModerato } from 'viem/tempo/zones' -import { useConnection, useConnectorClient } from 'wagmi' -import { Hooks } from 'wagmi/tempo' -import { - getZoneRpcHttpUrl, - getZoneRpcTransportConfig, - moderatoZoneRpcUrls, -} from '../../../lib/private-zones.ts' -import { useRootWebAuthnAccount } from '../../../lib/useRootWebAuthnAccount.ts' -import { useZoneAuthorization, type ZoneAuthClientLike } from '../../../lib/useZoneAuthorization.ts' -import { Button, ExplorerLink, FAKE_RECIPIENT, Logout, ReceiptHash, Step } from '../Demo' -import { SignInButtons } from '../EmbedPasskeys' -import { pathUsd } from '../tokens' -import { useStickyStepCompletion } from './useStickyStepCompletion.ts' - -const ZONE_LABEL = 'Zone A' -const ZONE_ID = 6 as const -const TRANSFER_AMOUNT = parseUnits('25', 6) -const ZONE_GAS_BUFFER = parseUnits('1', 6) - -type ZoneClientLike = { - token: { - getBalance: (parameters: { account: Hex; token: Hex }) => Promise - } - zone: ZoneAuthClientLike['zone'] -} - -export function SendTokensWithinZone() { - const { address } = useConnection() - const connected = Boolean(address) - - return ( - <> - : } - error={undefined} - number={1} - title="Create or use a passkey account on the public chain." - /> - - {address ? ( - - ) : ( - - )} - - ) -} - -function ConnectedZoneFlow(props: { address: Hex }) { - const { address } = props - const queryClient = useQueryClient() - const { data: connectorClient } = useConnectorClient() - const { data: rootWebAuthnAccount } = useRootWebAuthnAccount() - const { - data: rootBalance, - isPending: rootBalanceIsPending, - refetch: refetchRootBalance, - } = Hooks.token.useGetBalance({ - account: address, - token: pathUsd, - }) - - const zoneClient = React.useMemo( - () => - rootWebAuthnAccount - ? (createClient({ - account: rootWebAuthnAccount, - chain: zoneModerato(ZONE_ID), - transport: zoneHttp( - getZoneRpcHttpUrl(ZONE_ID, moderatoZoneRpcUrls[ZONE_ID]), - getZoneRpcTransportConfig(ZONE_ID, moderatoZoneRpcUrls[ZONE_ID]), - ), - }).extend(tempoActions()) as unknown as ZoneClientLike) - : undefined, - [rootWebAuthnAccount], - ) - - const zoneAuthorization = useZoneAuthorization({ - address, - chainId: zoneModerato(ZONE_ID).id, - queryKey: ['guide-private-zones-send-auth', address, ZONE_ID], - zoneClient, - }) - - React.useEffect(() => { - if (!zoneAuthorization.isAuthorized) return - - void queryClient.invalidateQueries({ - queryKey: ['demo-zone-balance', address, ZONE_ID], - }) - }, [address, queryClient, zoneAuthorization.isAuthorized]) - - const zoneBalanceQuery = useQuery({ - enabled: Boolean(zoneClient && zoneAuthorization.isAuthorized), - queryKey: ['guide-private-zones-send-zone-balance', address, ZONE_ID], - queryFn: async () => { - if (!zoneClient) throw new Error('zone client not ready') - - return zoneClient.token.getBalance({ - account: address, - token: pathUsd, - }) - }, - staleTime: 30_000, - }) - - const requiredZoneBalance = TRANSFER_AMOUNT + ZONE_GAS_BUFFER - const zoneTopUpShortfall = - zoneBalanceQuery.data !== undefined && zoneBalanceQuery.data < requiredZoneBalance - ? requiredZoneBalance - zoneBalanceQuery.data - : 0n - const hasEnoughZoneBalance = Boolean( - zoneBalanceQuery.data !== undefined && zoneBalanceQuery.data >= requiredZoneBalance, - ) - const zoneBalanceStepComplete = useStickyStepCompletion(hasEnoughZoneBalance) - - const fundMutation = useMutation({ - mutationFn: async () => { - if (!connectorClient) throw new Error('connector client not ready') - - await Actions.faucet.fundSync(connectorClient, { - account: address, - }) - }, - onSuccess: async () => { - await refetchRootBalance() - }, - }) - - const topUpMutation = useMutation({ - mutationFn: async () => { - if (!connectorClient) throw new Error('connector client not ready') - if (zoneTopUpShortfall <= 0n) throw new Error('zone top-up is not required') - - const { receipt } = await Actions.zone.depositSync(connectorClient as never, { - account: connectorClient.account, - amount: zoneTopUpShortfall, - chain: connectorClient.chain as never, - token: pathUsd, - zoneId: ZONE_ID, - }) - - return { receipt } - }, - onSuccess: async () => { - await refetchRootBalance() - await zoneBalanceQuery.refetch() - }, - }) - - const transferMutation = useMutation({ - mutationFn: async () => { - if (!connectorClient) throw new Error('connector client not ready') - if (!zoneClient) throw new Error('zone client not ready') - if (!rootWebAuthnAccount) throw new Error('root account not ready') - - const currentZoneBalance = await zoneClient.token.getBalance({ - account: address, - token: pathUsd, - }) - if (currentZoneBalance < requiredZoneBalance) { - throw new Error('Zone A needs more pathUSD before sending.') - } - - const { receipt } = await Actions.token.transferSync(zoneClient as never, { - account: rootWebAuthnAccount, - amount: TRANSFER_AMOUNT, - chain: zoneModerato(ZONE_ID) as never, - feeToken: pathUsd, - to: FAKE_RECIPIENT as Hex, - token: pathUsd, - }) - - return { - receipt, - startingZoneBalance: currentZoneBalance, - } - }, - onSuccess: async () => { - await zoneBalanceQuery.refetch() - await transferConfirmationQuery.refetch() - }, - }) - - const transferConfirmationQuery = useQuery({ - enabled: Boolean(zoneClient && zoneAuthorization.isAuthorized && transferMutation.isSuccess), - queryKey: ['guide-private-zones-send-confirmation', address, ZONE_ID], - queryFn: async () => { - if (!zoneClient) throw new Error('zone client not ready') - - return zoneClient.token.getBalance({ - account: address, - token: pathUsd, - }) - }, - refetchInterval: (query) => { - if (query.state.error) return false - - const expectedMaxZoneBalance = transferMutation.data?.startingZoneBalance - ? transferMutation.data.startingZoneBalance - TRANSFER_AMOUNT - : undefined - if (expectedMaxZoneBalance === undefined) return false - - const currentZoneBalance = query.state.data as bigint | undefined - return currentZoneBalance !== undefined && currentZoneBalance <= expectedMaxZoneBalance - ? false - : 1_500 - }, - refetchOnReconnect: false, - refetchOnWindowFocus: false, - retry: false, - }) - - const hasRootBalance = Boolean(rootBalance && rootBalance > 0n) - const topUpReceipt = topUpMutation.data?.receipt - const expectedMaxZoneBalance = transferMutation.data?.startingZoneBalance - ? transferMutation.data.startingZoneBalance - TRANSFER_AMOUNT - : undefined - const transferConfirmed = Boolean( - expectedMaxZoneBalance !== undefined && - transferConfirmationQuery.data !== undefined && - transferConfirmationQuery.data <= expectedMaxZoneBalance, - ) - const transferReceipt = transferMutation.data?.receipt - const authIsPreparing = - zoneAuthorization.isChecking || zoneAuthorization.authorizeMutation.isPending - const stepTwoAction = zoneAuthorization.isAuthorized ? undefined : ( - - ) - - React.useEffect(() => { - if (!topUpMutation.isSuccess || zoneBalanceStepComplete) return - - const interval = window.setInterval(() => { - void zoneBalanceQuery.refetch() - }, 1_500) - - return () => window.clearInterval(interval) - }, [topUpMutation.isSuccess, zoneBalanceQuery, zoneBalanceStepComplete]) - - let stepThreeAction: React.ReactNode - if (zoneBalanceStepComplete) { - stepThreeAction = undefined - } else if (zoneBalanceQuery.isPending) { - stepThreeAction = ( - - ) - } else if (!hasEnoughZoneBalance && !hasRootBalance) { - stepThreeAction = ( - - ) - } else if (!hasEnoughZoneBalance) { - stepThreeAction = ( - - ) - } - - let stepFourAction: React.ReactNode - if (!zoneBalanceStepComplete) { - stepFourAction = undefined - } else { - stepFourAction = ( - - ) - } - - return ( - <> - - - - {topUpReceipt && ( - - - - - )} - - - - {transferReceipt && ( - - - - - )} - - - - - ) -} - -function DisconnectedZoneFlow() { - return ( - <> - - - - - - ) -} - -function StepBody(props: React.PropsWithChildren) { - return ( -
-
-
{props.children}
-
-
- ) -} - -function DetailLine(props: { label: string; value: string; dataTestId?: string | undefined }) { - const { dataTestId, label, value } = props - - return ( -
- {label} - - {value} - -
- ) -} diff --git a/src/components/guides/zones/SwapAcrossZones.tsx b/src/components/guides/zones/SwapAcrossZones.tsx deleted file mode 100644 index c6e1ed84..00000000 --- a/src/components/guides/zones/SwapAcrossZones.tsx +++ /dev/null @@ -1,773 +0,0 @@ -'use client' -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' -import * as React from 'react' -import { createClient, encodeAbiParameters, type Hex, parseAbiItem, parseUnits } from 'viem' -import { Actions, tempoActions } from 'viem/tempo' -import { http as zoneHttp, zoneModerato } from 'viem/tempo/zones' -import { useConnection, useConnectorClient, usePublicClient } from 'wagmi' -import { Hooks } from 'wagmi/tempo' -import { - getZoneRpcHttpUrl, - getZoneRpcTransportConfig, - moderatoZoneFactory, - publicSettlementLookbackBlocks, - routerCallbackGasLimit, - stablecoinDex, - swapAndDepositRouter, - ZONE_A, - ZONE_B, - zeroBytes32, - zoneRpcSyncTimeout, -} from '../../../lib/private-zones.ts' -import { useRootWebAuthnAccount } from '../../../lib/useRootWebAuthnAccount.ts' -import { useZoneAuthorization, type ZoneAuthClientLike } from '../../../lib/useZoneAuthorization.ts' -import { Button, ExplorerLink, Logout, ReceiptHash, Step } from '../Demo' -import { SignInButtons } from '../EmbedPasskeys' -import { betaUsd, pathUsd } from '../tokens' -import { useStickyStepCompletion } from './useStickyStepCompletion.ts' - -const SWAP_AMOUNT = parseUnits('25', 6) -const ZONE_GAS_BUFFER = parseUnits('1', 6) - -const portalAbi = [ - { - name: 'calculateDepositFee', - type: 'function', - stateMutability: 'view', - inputs: [], - outputs: [{ type: 'uint128' }], - }, - { - name: 'isTokenEnabled', - type: 'function', - stateMutability: 'view', - inputs: [{ name: 'token', type: 'address' }], - outputs: [{ type: 'bool' }], - }, -] as const - -const routerAbi = [ - { - name: 'stablecoinDEX', - type: 'function', - stateMutability: 'view', - inputs: [], - outputs: [{ type: 'address' }], - }, - { - name: 'zoneFactory', - type: 'function', - stateMutability: 'view', - inputs: [], - outputs: [{ type: 'address' }], - }, -] as const - -const targetDepositEvent = parseAbiItem( - 'event DepositMade(bytes32 indexed newCurrentDepositQueueHash, address indexed sender, address token, address to, uint128 netAmount, uint128 fee, bytes32 memo)', -) - -type ZoneClientLike = { - token: { - getAllowance: (parameters: { account: Hex; spender: Hex; token: Hex }) => Promise - getBalance: (parameters: { account: Hex; token: Hex }) => Promise - } - zone: { - getAuthorizationTokenInfo: ZoneAuthClientLike['zone']['getAuthorizationTokenInfo'] - requestWithdrawalSync: (parameters: { - account: unknown - amount: bigint - data?: Hex - feeToken: Hex - gas?: bigint - timeout: number - to: Hex - token: Hex - }) => Promise<{ receipt: { blockNumber: bigint; transactionHash: Hex } }> - getWithdrawalFee: (parameters?: { gasLimit?: bigint | undefined }) => Promise - signAuthorizationToken: ZoneAuthClientLike['zone']['signAuthorizationToken'] - } -} - -export function SwapAcrossZones() { - const { address } = useConnection() - const connected = Boolean(address) - - return ( - <> - : } - error={undefined} - number={1} - title="Create or use a passkey account on the public chain." - /> - - {address ? ( - - ) : ( - - )} - - ) -} - -function ConnectedZoneFlow(props: { address: Hex }) { - const { address } = props - const queryClient = useQueryClient() - const publicClient = usePublicClient() - const { data: connectorClient } = useConnectorClient() - const { data: rootWebAuthnAccount } = useRootWebAuthnAccount() - const { - data: rootBalance, - isPending: rootBalanceIsPending, - refetch: refetchRootBalance, - } = Hooks.token.useGetBalance({ - account: address, - token: pathUsd, - }) - - const sourceZoneClient = React.useMemo( - () => - rootWebAuthnAccount - ? (createClient({ - account: rootWebAuthnAccount, - chain: zoneModerato(ZONE_A.id), - transport: zoneHttp( - getZoneRpcHttpUrl(ZONE_A.id, ZONE_A.rpcUrl), - getZoneRpcTransportConfig(ZONE_A.id, ZONE_A.rpcUrl), - ), - }).extend(tempoActions()) as unknown as ZoneClientLike) - : undefined, - [rootWebAuthnAccount], - ) - const targetZoneClient = React.useMemo( - () => - rootWebAuthnAccount - ? (createClient({ - account: rootWebAuthnAccount, - chain: zoneModerato(ZONE_B.id), - transport: zoneHttp( - getZoneRpcHttpUrl(ZONE_B.id, ZONE_B.rpcUrl), - getZoneRpcTransportConfig(ZONE_B.id, ZONE_B.rpcUrl), - ), - }).extend(tempoActions()) as unknown as ZoneClientLike) - : undefined, - [rootWebAuthnAccount], - ) - - const sourceFooterQueryKey = React.useMemo( - () => ['demo-zone-balance', address, ZONE_A.id, pathUsd], - [address], - ) - const targetFooterQueryKey = React.useMemo( - () => ['demo-zone-balance', address, ZONE_B.id, betaUsd], - [address], - ) - - const sourceZoneAuthorization = useZoneAuthorization({ - address, - chainId: ZONE_A.chainId, - queryKey: ['guide-private-zones-swap-source-auth', address, ZONE_A.id], - zoneClient: sourceZoneClient, - }) - - const sourceZoneBalanceQuery = useQuery({ - enabled: Boolean(sourceZoneClient && sourceZoneAuthorization.isAuthorized), - queryKey: ['guide-private-zones-swap-source-balance', address, ZONE_A.id], - queryFn: async () => { - if (!sourceZoneClient) throw new Error('Zone A client not ready') - - return sourceZoneClient.token.getBalance({ - account: address, - token: pathUsd, - }) - }, - staleTime: 30_000, - }) - - const swapPrereqsQuery = useQuery({ - enabled: Boolean(connectorClient && publicClient && sourceZoneAuthorization.isAuthorized), - queryKey: ['guide-private-zones-swap-prereqs', address, ZONE_A.id, ZONE_B.id], - queryFn: async () => { - if (!connectorClient) throw new Error('connector client not ready') - if (!publicClient) throw new Error('public client not ready') - - const [ - routedWithdrawalFee, - quotedOutput, - targetDepositFee, - targetTokenEnabled, - routerDex, - routerFactory, - ] = await Promise.all([ - sourceZoneClient?.zone.getWithdrawalFee({ - gasLimit: routerCallbackGasLimit, - }), - Actions.dex.getSellQuote(publicClient as never, { - amountIn: SWAP_AMOUNT, - tokenIn: pathUsd, - tokenOut: betaUsd, - }), - publicClient.readContract({ - address: ZONE_B.portalAddress, - abi: portalAbi, - functionName: 'calculateDepositFee', - }), - publicClient.readContract({ - address: ZONE_B.portalAddress, - abi: portalAbi, - functionName: 'isTokenEnabled', - args: [betaUsd], - }), - publicClient.readContract({ - address: swapAndDepositRouter, - abi: routerAbi, - functionName: 'stablecoinDEX', - }), - publicClient.readContract({ - address: swapAndDepositRouter, - abi: routerAbi, - functionName: 'zoneFactory', - }), - ]) - - if (routedWithdrawalFee === undefined) throw new Error('Zone A withdrawal fee not ready') - if (routerDex.toLowerCase() !== stablecoinDex.toLowerCase()) { - throw new Error('The routed swap router is not pointing at the expected StablecoinDEX.') - } - if (routerFactory.toLowerCase() !== moderatoZoneFactory.toLowerCase()) { - throw new Error( - 'The routed swap router is not pointing at the current public-chain ZoneFactory.', - ) - } - if (!targetTokenEnabled) { - throw new Error(`${ZONE_B.label} is not ready for betaUSD deposits yet.`) - } - - const minimumOutput = applyOnePercentSlippageBuffer(quotedOutput) - if (minimumOutput <= targetDepositFee) { - throw new Error( - `The current pathUSD -> betaUSD quote is too small to cover the ${ZONE_B.label} deposit fee.`, - ) - } - - return { - minimumOutput, - minimumTargetIncrease: minimumOutput - targetDepositFee, - quotedOutput, - routedWithdrawalFee, - } - }, - staleTime: 30_000, - }) - - const requiredSourceZoneBalance = swapPrereqsQuery.data - ? SWAP_AMOUNT + swapPrereqsQuery.data.routedWithdrawalFee + ZONE_GAS_BUFFER - : undefined - const sourceZoneTopUpShortfall = - requiredSourceZoneBalance !== undefined && - sourceZoneBalanceQuery.data !== undefined && - sourceZoneBalanceQuery.data < requiredSourceZoneBalance - ? requiredSourceZoneBalance - sourceZoneBalanceQuery.data - : 0n - const hasEnoughSourceZoneBalance = Boolean( - requiredSourceZoneBalance !== undefined && - sourceZoneBalanceQuery.data !== undefined && - sourceZoneBalanceQuery.data >= requiredSourceZoneBalance, - ) - const sourceZoneBalanceStepComplete = useStickyStepCompletion(hasEnoughSourceZoneBalance) - - const fundMutation = useMutation({ - mutationFn: async () => { - if (!connectorClient) throw new Error('connector client not ready') - - await Actions.faucet.fundSync(connectorClient, { - account: address, - }) - }, - onSuccess: async () => { - await refetchRootBalance() - }, - }) - - const topUpMutation = useMutation({ - mutationFn: async () => { - if (!connectorClient) throw new Error('connector client not ready') - if (sourceZoneTopUpShortfall <= 0n) throw new Error('Zone A top-up is not required') - - const { receipt } = await Actions.zone.depositSync(connectorClient as never, { - account: connectorClient.account, - amount: sourceZoneTopUpShortfall, - chain: connectorClient.chain as never, - token: pathUsd, - zoneId: ZONE_A.id, - }) - - return { receipt } - }, - onSuccess: async () => { - await refetchRootBalance() - await sourceZoneBalanceQuery.refetch() - await queryClient.invalidateQueries({ queryKey: sourceFooterQueryKey }) - }, - }) - - const swapMutation = useMutation({ - mutationFn: async () => { - if (!connectorClient) throw new Error('connector client not ready') - if (!sourceZoneClient) throw new Error('Zone A client not ready') - if (!publicClient) throw new Error('public client not ready') - if (!rootWebAuthnAccount) throw new Error('root account not ready') - if (!swapPrereqsQuery.data) throw new Error('Swap prerequisites are not ready') - - const currentSourceBalance = await sourceZoneClient.token.getBalance({ - account: address, - token: pathUsd, - }) - if ( - requiredSourceZoneBalance === undefined || - currentSourceBalance < requiredSourceZoneBalance - ) { - throw new Error('Zone A needs more pathUSD before the swap can start.') - } - - const callbackData = encodeRouterCallback({ - minimumOutput: swapPrereqsQuery.data.minimumOutput, - recipient: address, - }) - - const anchorBlock = await publicClient.getBlockNumber() - - const { receipt } = await sourceZoneClient.zone.requestWithdrawalSync({ - account: rootWebAuthnAccount, - amount: SWAP_AMOUNT, - data: callbackData, - feeToken: pathUsd, - gas: routerCallbackGasLimit, - timeout: zoneRpcSyncTimeout, - to: swapAndDepositRouter, - token: pathUsd, - }) - - return { - anchorBlock, - minimumTargetIncrease: swapPrereqsQuery.data.minimumTargetIncrease, - receipt, - } - }, - onSuccess: async () => { - await sourceZoneBalanceQuery.refetch() - await queryClient.invalidateQueries({ queryKey: sourceFooterQueryKey }) - }, - }) - - const settlementQuery = useQuery({ - enabled: Boolean( - publicClient && swapMutation.isSuccess && swapMutation.data?.anchorBlock !== undefined, - ), - queryKey: [ - 'guide-private-zones-swap-settlement', - address, - swapMutation.data?.anchorBlock?.toString(), - ], - queryFn: async () => { - if (!publicClient) throw new Error('public client not ready') - if (!swapMutation.data) throw new Error('swap submission not ready') - - const fromBlock = - swapMutation.data.anchorBlock > publicSettlementLookbackBlocks - ? swapMutation.data.anchorBlock - publicSettlementLookbackBlocks - : 0n - const latest = await publicClient.getBlockNumber() - const logs = await publicClient.getLogs({ - address: ZONE_B.portalAddress, - event: targetDepositEvent, - fromBlock, - toBlock: latest, - }) - - const match = logs.find((log) => { - const sender = log.args.sender - const token = log.args.token - const recipient = log.args.to - const netAmount = log.args.netAmount - - return ( - typeof sender === 'string' && - typeof token === 'string' && - typeof recipient === 'string' && - typeof netAmount === 'bigint' && - sender.toLowerCase() === swapAndDepositRouter.toLowerCase() && - token.toLowerCase() === betaUsd.toLowerCase() && - recipient.toLowerCase() === address.toLowerCase() && - netAmount >= swapMutation.data.minimumTargetIncrease - ) - }) - - return match ? { txHash: match.transactionHash } : null - }, - refetchInterval: (query) => { - if (query.state.error || query.state.data) return false - - return 2_000 - }, - refetchOnReconnect: false, - refetchOnWindowFocus: false, - retry: false, - }) - - const targetZoneAuthorization = useZoneAuthorization({ - address, - chainId: ZONE_B.chainId, - queryKey: ['guide-private-zones-swap-target-auth', address, ZONE_B.id], - zoneClient: targetZoneClient, - }) - - const targetZoneBalanceQuery = useQuery({ - enabled: Boolean( - targetZoneClient && targetZoneAuthorization.isAuthorized && settlementQuery.data, - ), - queryKey: ['guide-private-zones-swap-target-balance', address, ZONE_B.id], - queryFn: async () => { - if (!targetZoneClient) throw new Error('Zone B client not ready') - - return targetZoneClient.token.getBalance({ - account: address, - token: betaUsd, - }) - }, - staleTime: 30_000, - refetchOnReconnect: false, - refetchOnWindowFocus: false, - retry: false, - }) - - const hasRootBalance = Boolean(rootBalance && rootBalance > 0n) - const topUpReceipt = topUpMutation.data?.receipt - const routedSwapReceipt = swapMutation.data?.receipt - const settlementTxHash = settlementQuery.data?.txHash - const targetBalanceReady = - settlementQuery.data && targetZoneAuthorization.isAuthorized && targetZoneBalanceQuery.isSuccess - const sourceAuthIsPreparing = - sourceZoneAuthorization.isChecking || sourceZoneAuthorization.authorizeMutation.isPending - const stepTwoAction = sourceZoneAuthorization.isAuthorized ? undefined : ( - - ) - - React.useEffect(() => { - if (!sourceZoneAuthorization.isAuthorized) return - - void queryClient.invalidateQueries({ queryKey: sourceFooterQueryKey }) - }, [queryClient, sourceFooterQueryKey, sourceZoneAuthorization.isAuthorized]) - - React.useEffect(() => { - if (!targetZoneAuthorization.isAuthorized) return - - void queryClient.invalidateQueries({ queryKey: targetFooterQueryKey }) - }, [queryClient, targetFooterQueryKey, targetZoneAuthorization.isAuthorized]) - - React.useEffect(() => { - if (!topUpMutation.isSuccess || sourceZoneBalanceStepComplete) return - - const interval = window.setInterval(() => { - void sourceZoneBalanceQuery.refetch() - }, 1_500) - - return () => window.clearInterval(interval) - }, [sourceZoneBalanceQuery, sourceZoneBalanceStepComplete, topUpMutation.isSuccess]) - - let stepThreeAction: React.ReactNode - if (sourceZoneBalanceStepComplete) { - stepThreeAction = undefined - } else if (sourceZoneBalanceQuery.isPending || swapPrereqsQuery.isPending) { - stepThreeAction = ( - - ) - } else if (!hasEnoughSourceZoneBalance && !hasRootBalance) { - stepThreeAction = ( - - ) - } else if (!hasEnoughSourceZoneBalance) { - stepThreeAction = ( - - ) - } - - let stepFourAction: React.ReactNode - if (!sourceZoneBalanceStepComplete || swapPrereqsQuery.isPending) { - stepFourAction = undefined - } else if (swapPrereqsQuery.isError) { - stepFourAction = ( - - ) - } else { - stepFourAction = ( - - ) - } - - let stepSixAction: React.ReactNode - if (!settlementQuery.data) { - stepSixAction = undefined - } else if (targetZoneBalanceQuery.isError) { - stepSixAction = ( - - ) - } else if (!targetZoneAuthorization.isAuthorized) { - stepSixAction = ( - - ) - } else if (targetZoneBalanceQuery.isPending) { - stepSixAction = ( - - ) - } - - return ( - <> - - - - {topUpReceipt && ( - - - - - )} - - - - {routedSwapReceipt && ( - - - - - )} - - - - {settlementTxHash && ( - {settlementTxHash && } - )} - - - - - ) -} - -function DisconnectedZoneFlow() { - return ( - <> - - - - - - - ) -} - -function encodeRouterCallback(parameters: { minimumOutput: bigint; recipient: Hex }) { - const { minimumOutput, recipient } = parameters - - return encodeAbiParameters( - [ - { type: 'bool' }, - { type: 'address' }, - { type: 'address' }, - { type: 'address' }, - { type: 'bytes32' }, - { type: 'uint128' }, - ], - [false, betaUsd, ZONE_B.portalAddress, recipient, zeroBytes32, minimumOutput], - ) -} - -function applyOnePercentSlippageBuffer(value: bigint) { - if (value <= 1n) return value - return value - value / 100n -} - -function StepBody(props: React.PropsWithChildren) { - return ( -
-
-
{props.children}
-
-
- ) -} - -function DetailLine(props: { label: string; value: string; dataTestId?: string | undefined }) { - const { dataTestId, label, value } = props - - return ( -
- {label} - - {value} - -
- ) -} diff --git a/src/components/guides/zones/WithdrawFromZone.tsx b/src/components/guides/zones/WithdrawFromZone.tsx deleted file mode 100644 index 1d2891d0..00000000 --- a/src/components/guides/zones/WithdrawFromZone.tsx +++ /dev/null @@ -1,608 +0,0 @@ -'use client' -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' -import * as React from 'react' -import { createClient, type Hex, parseAbiItem, parseUnits } from 'viem' -import { Actions, tempoActions } from 'viem/tempo' -import { http as zoneHttp, zoneModerato } from 'viem/tempo/zones' -import { useConnection, useConnectorClient, usePublicClient } from 'wagmi' -import { Hooks } from 'wagmi/tempo' -import { - getZoneRpcHttpUrl, - getZoneRpcTransportConfig, - moderatoZoneRpcUrls, - publicSettlementLookbackBlocks, - zoneRpcSyncTimeout, -} from '../../../lib/private-zones.ts' -import { useRootWebAuthnAccount } from '../../../lib/useRootWebAuthnAccount.ts' -import { useZoneAuthorization, type ZoneAuthClientLike } from '../../../lib/useZoneAuthorization.ts' -import { Button, ExplorerLink, Logout, Step } from '../Demo' -import { SignInButtons } from '../EmbedPasskeys' -import { pathUsd } from '../tokens' -import { useStickyStepCompletion } from './useStickyStepCompletion.ts' - -const ZONE_LABEL = 'Zone A' -const ZONE_ID = 6 as const -const AUTHENTICATED_WITHDRAWAL_REVEAL_TO = - '0x031dc147467e8f106eb22850fef549dc74b8f6634aeac554ebdd4ab896b67cdf68' as const -const WITHDRAWAL_AMOUNT = parseUnits('100', 6) -const ZONE_GAS_BUFFER = parseUnits('1', 6) - -const tip20TransferEvent = parseAbiItem( - 'event Transfer(address indexed from, address indexed to, uint256 value)', -) - -type WithdrawalMode = 'standard' | 'authenticated' - -type ZoneClientLike = { - token: { - getBalance: (parameters: { account: Hex; token: Hex }) => Promise - } - zone: { - requestVerifiableWithdrawalSync: (parameters: { - account: unknown - amount: bigint - feeToken: Hex - revealTo: Hex - timeout: number - to: Hex - token: Hex - }) => Promise<{ receipt: { blockNumber: bigint; transactionHash: Hex } }> - requestWithdrawalSync: (parameters: { - account: unknown - amount: bigint - feeToken: Hex - timeout: number - to: Hex - token: Hex - }) => Promise<{ receipt: { blockNumber: bigint; transactionHash: Hex } }> - getAuthorizationTokenInfo: ZoneAuthClientLike['zone']['getAuthorizationTokenInfo'] - signAuthorizationToken: ZoneAuthClientLike['zone']['signAuthorizationToken'] - getWithdrawalFee: () => Promise - } -} - -export function WithdrawFromZone() { - const { address } = useConnection() - const [mode, setMode] = React.useState('standard') - const connected = Boolean(address) - - return ( - <> - : } - error={undefined} - number={1} - title="Create or use a passkey account on the public chain." - /> - - - - {address ? ( - - ) : ( - - )} - - ) -} - -function ConnectedZoneFlow(props: { address: Hex; mode: WithdrawalMode }) { - const { address, mode } = props - const queryClient = useQueryClient() - const publicClient = usePublicClient() - const { data: connectorClient } = useConnectorClient() - const { data: rootWebAuthnAccount } = useRootWebAuthnAccount() - const { - data: rootBalance, - isPending: rootBalanceIsPending, - refetch: refetchRootBalance, - } = Hooks.token.useGetBalance({ - account: address, - token: pathUsd, - }) - - const zoneClient = React.useMemo( - () => - rootWebAuthnAccount - ? (createClient({ - account: rootWebAuthnAccount, - chain: zoneModerato(ZONE_ID), - transport: zoneHttp( - getZoneRpcHttpUrl(ZONE_ID, moderatoZoneRpcUrls[ZONE_ID]), - getZoneRpcTransportConfig(ZONE_ID, moderatoZoneRpcUrls[ZONE_ID]), - ), - }).extend(tempoActions()) as unknown as ZoneClientLike) - : undefined, - [rootWebAuthnAccount], - ) - - const zoneAuthorization = useZoneAuthorization({ - address, - chainId: zoneModerato(ZONE_ID).id, - queryKey: ['guide-private-zones-withdraw-auth', address, ZONE_ID], - zoneClient, - }) - - React.useEffect(() => { - if (!zoneAuthorization.isAuthorized) return - - void queryClient.invalidateQueries({ - queryKey: ['demo-zone-balance', address, ZONE_ID], - }) - }, [address, queryClient, zoneAuthorization.isAuthorized]) - - const withdrawalFeeQuery = useQuery({ - enabled: Boolean(zoneClient && zoneAuthorization.isAuthorized), - queryKey: ['guide-private-zones-withdraw-fee', address, ZONE_ID], - queryFn: async () => { - if (!zoneClient) throw new Error('zone client not ready') - - return zoneClient.zone.getWithdrawalFee() - }, - staleTime: 30_000, - }) - - const zoneBalanceQuery = useQuery({ - enabled: Boolean(zoneClient && zoneAuthorization.isAuthorized), - queryKey: ['guide-private-zones-withdraw-zone-balance', address, ZONE_ID], - queryFn: async () => { - if (!zoneClient) throw new Error('zone client not ready') - - return zoneClient.token.getBalance({ - account: address, - token: pathUsd, - }) - }, - staleTime: 30_000, - }) - - const zoneTopUpTarget = - withdrawalFeeQuery.data !== undefined - ? WITHDRAWAL_AMOUNT + withdrawalFeeQuery.data + ZONE_GAS_BUFFER - : undefined - const zoneTopUpShortfall = - zoneTopUpTarget !== undefined && - zoneBalanceQuery.data !== undefined && - zoneBalanceQuery.data < zoneTopUpTarget - ? zoneTopUpTarget - zoneBalanceQuery.data - : 0n - const hasEnoughZoneBalance = Boolean( - zoneTopUpTarget !== undefined && - zoneBalanceQuery.data !== undefined && - zoneBalanceQuery.data >= zoneTopUpTarget, - ) - const zoneBalanceStepComplete = useStickyStepCompletion(hasEnoughZoneBalance) - - const fundMutation = useMutation({ - mutationFn: async () => { - if (!connectorClient) throw new Error('connector client not ready') - - await Actions.faucet.fundSync(connectorClient, { - account: address, - }) - }, - onSuccess: async () => { - await refetchRootBalance() - }, - }) - - const topUpMutation = useMutation({ - mutationFn: async () => { - if (!connectorClient) throw new Error('connector client not ready') - if (zoneTopUpShortfall <= 0n) throw new Error('zone top-up is not required') - - const { receipt } = await Actions.zone.depositSync(connectorClient as never, { - account: connectorClient.account, - amount: zoneTopUpShortfall, - chain: connectorClient.chain as never, - token: pathUsd, - zoneId: ZONE_ID, - }) - - return { receipt } - }, - onSuccess: async () => { - await refetchRootBalance() - await zoneBalanceQuery.refetch() - }, - }) - - const withdrawMutation = useMutation({ - mutationFn: async () => { - if (!connectorClient) throw new Error('connector client not ready') - if (!publicClient) throw new Error('public client not ready') - if (!zoneClient) throw new Error('zone client not ready') - if (!rootWebAuthnAccount) throw new Error('root account not ready') - if (withdrawalFeeQuery.data === undefined) throw new Error('withdrawal fee not ready') - - const currentRootBalance = await Actions.token.getBalance(connectorClient as never, { - account: address, - token: pathUsd, - }) - const currentZoneBalance = await zoneClient.token.getBalance({ - account: address, - token: pathUsd, - }) - const anchorBlock = await publicClient.getBlockNumber() - const receipt = - mode === 'authenticated' - ? ( - await zoneClient.zone.requestVerifiableWithdrawalSync({ - account: rootWebAuthnAccount, - amount: WITHDRAWAL_AMOUNT, - feeToken: pathUsd, - revealTo: AUTHENTICATED_WITHDRAWAL_REVEAL_TO, - timeout: zoneRpcSyncTimeout, - to: address, - token: pathUsd, - }) - ).receipt - : ( - await zoneClient.zone.requestWithdrawalSync({ - account: rootWebAuthnAccount, - amount: WITHDRAWAL_AMOUNT, - feeToken: pathUsd, - timeout: zoneRpcSyncTimeout, - to: address, - token: pathUsd, - }) - ).receipt - - return { - anchorBlock, - receipt, - startingRootBalance: currentRootBalance, - startingZoneBalance: currentZoneBalance, - } - }, - onSuccess: async () => { - await refetchRootBalance() - await zoneBalanceQuery.refetch() - await withdrawalConfirmationQuery.refetch() - }, - }) - - // biome-ignore lint/correctness/useExhaustiveDependencies: switching modes should clear the previous submission state. - React.useEffect(() => { - withdrawMutation.reset() - }, [mode]) - - const withdrawalConfirmationQuery = useQuery({ - enabled: Boolean( - publicClient && - zoneClient && - connectorClient && - zoneAuthorization.isAuthorized && - withdrawMutation.isSuccess, - ), - queryKey: [ - 'guide-private-zones-withdraw-confirmation', - address, - ZONE_ID, - withdrawMutation.data?.anchorBlock?.toString(), - ], - queryFn: async () => { - if (!publicClient) throw new Error('public client not ready') - if (!zoneClient) throw new Error('zone client not ready') - if (!connectorClient) throw new Error('connector client not ready') - if (!withdrawMutation.data) throw new Error('withdrawal submission not ready') - - const fromBlock = - withdrawMutation.data.anchorBlock > publicSettlementLookbackBlocks - ? withdrawMutation.data.anchorBlock - publicSettlementLookbackBlocks - : 0n - - const [currentRootBalance, currentZoneBalance, latest] = await Promise.all([ - Actions.token.getBalance(connectorClient as never, { - account: address, - token: pathUsd, - }), - zoneClient.token.getBalance({ - account: address, - token: pathUsd, - }), - publicClient.getBlockNumber(), - ]) - - const logs = await publicClient.getLogs({ - address: pathUsd, - args: { to: address }, - event: tip20TransferEvent, - fromBlock, - toBlock: latest, - }) - - const settlement = logs.find((log) => log.args.value === WITHDRAWAL_AMOUNT) - - return { - rootBalance: currentRootBalance, - txHash: settlement?.transactionHash ?? null, - zoneBalance: currentZoneBalance, - } - }, - refetchInterval: (query) => { - if (query.state.error) return false - - const txHash = (query.state.data as { txHash: Hex | null } | undefined)?.txHash - - return txHash ? false : 1_500 - }, - refetchOnReconnect: false, - refetchOnWindowFocus: false, - retry: false, - }) - - const hasRootBalance = Boolean(rootBalance && rootBalance > 0n) - const settlementTxHash = withdrawalConfirmationQuery.data?.txHash - const withdrawalConfirmed = Boolean(settlementTxHash) - const topUpReceipt = topUpMutation.data?.receipt - const authIsPreparing = - zoneAuthorization.isChecking || zoneAuthorization.authorizeMutation.isPending - const stepTwoAction = zoneAuthorization.isAuthorized ? undefined : ( - - ) - - React.useEffect(() => { - if (!topUpMutation.isSuccess || zoneBalanceStepComplete) return - - const interval = window.setInterval(() => { - void zoneBalanceQuery.refetch() - }, 1_500) - - return () => window.clearInterval(interval) - }, [topUpMutation.isSuccess, zoneBalanceQuery, zoneBalanceStepComplete]) - - let stepThreeAction: React.ReactNode - if (zoneBalanceStepComplete) { - stepThreeAction = undefined - } else if (withdrawalFeeQuery.isPending || zoneBalanceQuery.isPending) { - stepThreeAction = ( - - ) - } else if (!hasEnoughZoneBalance && !hasRootBalance) { - stepThreeAction = ( - - ) - } else if (!hasEnoughZoneBalance) { - stepThreeAction = ( - - ) - } - - let stepFourAction: React.ReactNode - if (!zoneBalanceStepComplete) { - stepFourAction = undefined - } else { - stepFourAction = ( - - ) - } - - return ( - <> - - - - {topUpReceipt && ( - - - - - )} - - - - - - {settlementTxHash && ( - {settlementTxHash && } - )} - - - ) -} - -function DisconnectedZoneFlow(props: { mode: WithdrawalMode }) { - const { mode } = props - - return ( - <> - - - - - - ) -} - -function WithdrawalModeSelector(props: { - mode: WithdrawalMode - onChange: (mode: WithdrawalMode) => void -}) { - const { mode, onChange } = props - - return ( -
-
-
-

Withdrawal mode

-

- Standard withdrawals reveal the sender of the withdrawal, while authenticated - withdrawals only reveal sender details to the holder of the reveal key. -

-
-
- {[ - ['standard', 'Standard'], - ['authenticated', 'Authenticated'], - ].map(([value, label]) => { - const selected = mode === value - - return ( - - ) - })} -
-
-
- ) -} - -function StepBody(props: React.PropsWithChildren) { - return ( -
-
-
{props.children}
-
-
- ) -} - -function getWithdrawalActionLabel(parameters: { isPending: boolean; isSuccess: boolean }) { - const { isPending, isSuccess } = parameters - - if (isPending) return 'Withdrawing pathUSD' - - if (isSuccess) return 'Withdrawal submitted' - - return 'Withdraw 100 pathUSD' -} - -function getWithdrawalSubmitStepTitle(mode: WithdrawalMode) { - return mode === 'authenticated' - ? `Submit the authenticated withdrawal back from ${ZONE_LABEL}.` - : `Submit the withdrawal back from ${ZONE_LABEL}.` -} - -function DetailLine(props: { label: string; value: string; dataTestId?: string | undefined }) { - const { dataTestId, label, value } = props - - return ( -
- {label} - - {value} - -
- ) -} diff --git a/src/components/guides/zones/useStickyStepCompletion.ts b/src/components/guides/zones/useStickyStepCompletion.ts deleted file mode 100644 index 37cc28d8..00000000 --- a/src/components/guides/zones/useStickyStepCompletion.ts +++ /dev/null @@ -1,11 +0,0 @@ -import * as React from 'react' - -export function useStickyStepCompletion(isComplete: boolean) { - const [isStickyComplete, setIsStickyComplete] = React.useState(isComplete) - - React.useEffect(() => { - if (isComplete) setIsStickyComplete(true) - }, [isComplete]) - - return isStickyComplete -} diff --git a/src/lib/feedback.test.ts b/src/lib/feedback.test.ts index 97d12882..9a3d6a26 100644 --- a/src/lib/feedback.test.ts +++ b/src/lib/feedback.test.ts @@ -25,7 +25,7 @@ describe('normalizeFeedback', () => { helpful: false, category: ' Missing information ', message: ' Needs more detail about token setup. ', - pageUrl: 'https://docs.tempo.xyz/guide/payments#send', + pageUrl: 'https://docs.tempo.xyz/docs/guide/payments#send', timestamp: '2026-06-19T12:00:00.000Z', }) @@ -34,7 +34,7 @@ describe('normalizeFeedback', () => { sentiment: 'negative', category: 'Missing information', message: 'Needs more detail about token setup.', - pageUrl: 'https://docs.tempo.xyz/guide/payments', + pageUrl: 'https://docs.tempo.xyz/docs/guide/payments', path: '/guide/payments', timestamp: '2026-06-19T12:00:00.000Z', }) diff --git a/src/lib/posthog.ts b/src/lib/posthog.ts index 32878481..61d01c1b 100644 --- a/src/lib/posthog.ts +++ b/src/lib/posthog.ts @@ -1,6 +1,6 @@ 'use client' -import { usePostHog } from 'posthog-js/react' +import posthog from 'posthog-js' /** * PostHog event names @@ -85,8 +85,6 @@ export const POSTHOG_PROPERTIES = { * Hook to access PostHog instance */ export function usePostHogTracking() { - const posthog = usePostHog() - return { posthog, /** diff --git a/src/lib/private-zones.ts b/src/lib/private-zones.ts deleted file mode 100644 index a3cdb959..00000000 --- a/src/lib/private-zones.ts +++ /dev/null @@ -1,121 +0,0 @@ -type ZoneTransportConfig = { - onFetchRequest?: ( - request: Request, - init?: RequestInit, - ) => Promise | RequestInit | undefined -} - -export const feeToken = '0x20c0000000000000000000000000000000000001' as const -export const stablecoinDex = '0xDEc0000000000000000000000000000000000000' as const -export const moderatoZoneFactory = '0x7Cc496Dc634b718289c192b59CF90262C5228545' as const -export const zoneOutbox = '0x1c00000000000000000000000000000000000002' as const -export const swapAndDepositRouter = '0xf9b794e0dca9bc12ac90067df792d7aad33436e4' as const -// Private sequencers currently only accept the raw transaction param on eth_sendRawTransactionSync. -export const zoneRpcSyncTimeout = 0 -export const routerCallbackGasLimit = 2_000_000n -// Routed settlement can appear before the UI records a post-submission anchor block. -export const publicSettlementLookbackBlocks = 100n -export const zeroBytes32 = - '0x0000000000000000000000000000000000000000000000000000000000000000' as const - -const ZONE_A_RPC_URL = 'https://eng:bold-raman-silly-torvalds@rpc-zone-a.testnet.tempo.xyz' as const -const ZONE_B_RPC_URL = 'https://eng:bold-raman-silly-torvalds@rpc-zone-b.testnet.tempo.xyz' as const - -export const ZONE_A = { - chainId: 4217000006, - id: 6, - label: 'Zone A', - portalAddress: '0x7069DeC4E64Fd07334A0933eDe836C17259c9B23', - rpcUrl: ZONE_A_RPC_URL, - rpcUrls: { - default: { - http: [stripRpcBasicAuth(ZONE_A_RPC_URL)], - webSocket: [], - }, - }, -} as const - -export const ZONE_B = { - chainId: 4217000007, - id: 7, - label: 'Zone B', - portalAddress: '0x3F5296303400B56271b476F5A0B9cBF74350D6Ac', - rpcUrl: ZONE_B_RPC_URL, - rpcUrls: { - default: { - http: [stripRpcBasicAuth(ZONE_B_RPC_URL)], - webSocket: [], - }, - }, -} as const - -export const moderatoZoneRpcUrls = { - [ZONE_A.id]: ZONE_A.rpcUrl, - [ZONE_B.id]: ZONE_B.rpcUrl, -} as const - -export const moderatoZones = { - [ZONE_A.id]: { - chainId: ZONE_A.chainId, - name: ZONE_A.label, - portalAddress: ZONE_A.portalAddress, - rpcUrls: ZONE_A.rpcUrls, - }, - [ZONE_B.id]: { - chainId: ZONE_B.chainId, - name: ZONE_B.label, - portalAddress: ZONE_B.portalAddress, - rpcUrls: ZONE_B.rpcUrls, - }, -} as const - -export function stripRpcBasicAuth(url: string) { - const parsedUrl = new URL(url) - parsedUrl.username = '' - parsedUrl.password = '' - return parsedUrl.toString() -} - -export function getZoneTransportConfig(rpcUrl: string): ZoneTransportConfig | undefined { - const parsedUrl = new URL(rpcUrl) - const username = decodeURIComponent(parsedUrl.username) - const password = decodeURIComponent(parsedUrl.password) - - if (!username && !password) return undefined - - const authorization = `Basic ${encodeBase64(`${username}:${password}`)}` - - return { - async onFetchRequest(_request: Request, init?: RequestInit) { - const headers = new Headers(init?.headers) - headers.set('authorization', authorization) - - return { - ...init, - headers, - } - }, - } -} - -export function getZoneRpcHttpUrl(zoneId: number, rpcUrl: string) { - const location = (globalThis as { location?: { origin: string } }).location - - if (import.meta.env.VITE_E2E === 'true' && zoneId in moderatoZoneRpcUrls && location) { - return `${location.origin}/__e2e_zone_rpc/${zoneId}` - } - - return stripRpcBasicAuth(rpcUrl) -} - -export function getZoneRpcTransportConfig(zoneId: number, rpcUrl: string) { - if (import.meta.env.VITE_E2E === 'true' && zoneId in moderatoZoneRpcUrls) return undefined - - return getZoneTransportConfig(rpcUrl) -} - -function encodeBase64(value: string) { - if (typeof globalThis.btoa === 'function') return globalThis.btoa(value) - - return Buffer.from(value).toString('base64') -} diff --git a/src/lib/useRootWebAuthnAccount.ts b/src/lib/useRootWebAuthnAccount.ts deleted file mode 100644 index 968aab73..00000000 --- a/src/lib/useRootWebAuthnAccount.ts +++ /dev/null @@ -1,117 +0,0 @@ -'use client' - -import { useQuery } from '@tanstack/react-query' -import type { WebAuthnP256 } from 'viem/tempo' -import { Account } from 'viem/tempo' -import { useConfig, useConnection } from 'wagmi' -import { webAuthnRpId } from '../wagmi.config.ts' - -type RootWebAuthnAccount = ReturnType -type RootWebAuthnCredential = WebAuthnP256.P256Credential -type RootWebAuthnAccountProvider = { - getAccount: (options: { - accessKey?: boolean | undefined - address?: `0x${string}` | undefined - signable?: boolean | undefined - }) => RootWebAuthnAccount - request: (args: { method: 'eth_accounts' }) => Promise -} - -const rootWebAuthnAccountTimeoutMs = 30_000 - -export function useRootWebAuthnAccount() { - const config = useConfig() - const { address, connector } = useConnection() - - return useQuery({ - enabled: Boolean(address && connector?.id === 'webAuthn'), - queryKey: ['root-webauthn-account', address], - queryFn: async () => { - if (!address) throw new Error('account address not ready') - if (!connector) throw new Error('connector not ready') - - const provider = await connector.getProvider() - if (isRootWebAuthnAccountProvider(provider)) { - await waitForProviderAccount( - provider, - address as `0x${string}`, - rootWebAuthnAccountTimeoutMs, - ) - - return provider.getAccount({ - accessKey: false, - address: address as `0x${string}`, - signable: true, - }) - } - - const credential = await waitForStoredCredential( - config, - address as `0x${string}`, - rootWebAuthnAccountTimeoutMs, - ) - return accountFromCredential(credential) - }, - refetchOnReconnect: false, - refetchOnWindowFocus: false, - retry: 2, - retryDelay: 500, - staleTime: Number.POSITIVE_INFINITY, - }) -} - -function accountFromCredential(credential: RootWebAuthnCredential) { - return Account.fromWebAuthnP256(credential, webAuthnRpId ? { rpId: webAuthnRpId } : undefined) -} - -function isRootWebAuthnAccountProvider(value: unknown): value is RootWebAuthnAccountProvider { - return Boolean( - value && - typeof value === 'object' && - 'getAccount' in value && - typeof value.getAccount === 'function' && - 'request' in value && - typeof value.request === 'function', - ) -} - -async function waitForProviderAccount( - provider: RootWebAuthnAccountProvider, - address: `0x${string}`, - timeoutMs = 5_000, -) { - const deadline = Date.now() + timeoutMs - const normalizedAddress = address.toLowerCase() - - while (Date.now() < deadline) { - const accounts = await provider.request({ method: 'eth_accounts' }) - if (accounts.some((account) => account.toLowerCase() === normalizedAddress)) return - - await new Promise((resolve) => setTimeout(resolve, 100)) - } - - throw new Error(`webauthn account ${address} not ready`) -} - -async function waitForStoredCredential( - config: ReturnType, - address: `0x${string}`, - timeoutMs = 5_000, -): Promise { - const deadline = Date.now() + timeoutMs - const normalizedAddress = address.toLowerCase() - - while (Date.now() < deadline) { - const credential = await config.storage?.getItem('webAuthn.activeCredential') - if (credential) { - const account = accountFromCredential(credential as RootWebAuthnCredential) - if (account.address.toLowerCase() === normalizedAddress) { - return credential as RootWebAuthnCredential - } - } - - await new Promise((resolve) => setTimeout(resolve, 100)) - } - - throw new Error(`webauthn credential for ${address} not ready`) -} diff --git a/src/lib/useZoneAuthorization.ts b/src/lib/useZoneAuthorization.ts deleted file mode 100644 index dd145637..00000000 --- a/src/lib/useZoneAuthorization.ts +++ /dev/null @@ -1,181 +0,0 @@ -'use client' - -import { useMutation, useQuery } from '@tanstack/react-query' -import type { Hex } from 'viem' -import { Storage as ZoneStorage } from 'viem/tempo' - -const zoneAuthorizationInfoTimeoutMs = 5_000 - -export type ZoneAuthClientLike = { - zone: { - getAuthorizationTokenInfo: () => Promise<{ - account: Hex - expiresAt: bigint - }> - signAuthorizationToken: () => Promise<{ - authentication: { - expiresAt: number - zoneId: number - } - token: Hex - }> - } -} - -export function useZoneAuthorization(parameters: { - address: Hex | undefined - chainId: number - queryKey: readonly unknown[] - zoneClient: ZoneAuthClientLike | undefined -}) { - const { address, chainId, queryKey, zoneClient } = parameters - - const statusQuery = useQuery({ - enabled: Boolean(address && zoneClient), - queryKey, - queryFn: async () => { - if (!address) throw new Error('account address not ready') - if (!zoneClient) throw new Error('zone client not ready') - - const storage = ZoneStorage.defaultStorage() - const lowerAddress = address.toLowerCase() - const accountStorageKey = `auth:${lowerAddress}:${chainId}` - const chainStorageKey = `auth:token:${chainId}` - const accountToken = await storage.getItem(accountStorageKey) - - if (accountToken) await storage.setItem(chainStorageKey, accountToken) - - try { - const info = await withTimeout( - zoneClient.zone.getAuthorizationTokenInfo(), - zoneAuthorizationInfoTimeoutMs, - ) - const expired = info.expiresAt <= BigInt(Math.floor(Date.now() / 1000)) - const matchesAccount = info.account.toLowerCase() === lowerAddress - - if (!matchesAccount || expired) { - await storage.removeItem(chainStorageKey) - if (accountToken) await storage.removeItem(accountStorageKey) - return null - } - - if (!accountToken) { - const chainToken = await storage.getItem(chainStorageKey) - if (chainToken) await storage.setItem(accountStorageKey, chainToken) - } - - return info - } catch (error) { - if (!isZoneAuthorizationError(error)) throw error - - await storage.removeItem(chainStorageKey) - if (accountToken) await storage.removeItem(accountStorageKey) - return null - } - }, - refetchOnReconnect: false, - refetchOnWindowFocus: false, - retry: false, - staleTime: 30_000, - }) - - const authorizeMutation = useMutation({ - mutationFn: async () => { - if (!zoneClient) throw new Error('zone client not ready') - - return zoneClient.zone.signAuthorizationToken() - }, - onSuccess: async () => { - await statusQuery.refetch() - }, - }) - - return { - authorizeMutation, - error: authorizeMutation.error ?? statusQuery.error, - isAuthorized: statusQuery.data !== null && statusQuery.data !== undefined, - isChecking: statusQuery.fetchStatus === 'fetching', - statusQuery, - } -} - -function withTimeout(promise: Promise, timeoutMs: number) { - let timeout: ReturnType - - return Promise.race([ - promise.then( - (value) => { - clearTimeout(timeout) - return value - }, - (error) => { - clearTimeout(timeout) - throw error - }, - ), - new Promise((_, reject) => { - timeout = setTimeout(() => { - const error = new Error('zone authorization info request timed out') - error.name = 'TimeoutError' - reject(error) - }, timeoutMs) - }), - ]) -} - -function isZoneAuthorizationError(error: unknown) { - const status = getErrorStatus(error) - if (status === 401 || status === 403) return true - - const name = getErrorName(error) - if (name === 'HttpRequestError' || name === 'TimeoutError') return true - - const message = getErrorMessage(error) - return /authorization token/i.test(message) -} - -function getErrorMessage(error: unknown) { - if (typeof error === 'object' && error !== null) { - if ('shortMessage' in error && typeof error.shortMessage === 'string') { - return error.shortMessage - } - - if ('message' in error && typeof error.message === 'string') return error.message - } - - if (error instanceof Error) return error.message - - return '' -} - -function getErrorStatus(error: unknown): number | null { - if (typeof error !== 'object' || error === null) return null - - if ('status' in error && typeof error.status === 'number') { - return error.status - } - - if ('statusCode' in error && typeof error.statusCode === 'number') { - return error.statusCode - } - - if ('cause' in error) { - return getErrorStatus(error.cause) - } - - return null -} - -function getErrorName(error: unknown): string | null { - if (typeof error !== 'object' || error === null) return null - - if ('name' in error && typeof error.name === 'string') { - return error.name - } - - if ('cause' in error) { - return getErrorName(error.cause) - } - - return null -} diff --git a/src/marketing/DiagramsPage.tsx b/src/marketing/DiagramsPage.tsx new file mode 100644 index 00000000..e95e39ee --- /dev/null +++ b/src/marketing/DiagramsPage.tsx @@ -0,0 +1,3 @@ +import DiagramsPage from './app/diagrams/page' + +export default DiagramsPage diff --git a/src/marketing/FeaturePage.tsx b/src/marketing/FeaturePage.tsx new file mode 100644 index 00000000..d18b5153 --- /dev/null +++ b/src/marketing/FeaturePage.tsx @@ -0,0 +1,3 @@ +import FeaturePage from './app/features/[slug]/page' + +export default FeaturePage diff --git a/src/marketing/HomePage.tsx b/src/marketing/HomePage.tsx new file mode 100644 index 00000000..34a6ff76 --- /dev/null +++ b/src/marketing/HomePage.tsx @@ -0,0 +1,3 @@ +import HomePage from './app/page' + +export default HomePage diff --git a/src/marketing/MarketingRoute.tsx b/src/marketing/MarketingRoute.tsx new file mode 100644 index 00000000..c073f22f --- /dev/null +++ b/src/marketing/MarketingRoute.tsx @@ -0,0 +1,119 @@ +'use client' + +import { lazy, type ReactNode, Suspense, useEffect, useState } from 'react' + +const Analytics = lazy(() => + import('@vercel/analytics/react').then((module) => ({ default: module.Analytics })), +) +const SpeedInsights = lazy(() => + import('@vercel/speed-insights/react').then((module) => ({ default: module.SpeedInsights })), +) +const GoogleAnalytics = lazy(() => import('../components/GoogleAnalytics')) +const PostHogSetup = lazy(() => import('../components/PostHogSetup')) + +const routeMetadata: Record = { + '/': { + title: 'Tempo', + description: + 'The only blockchain designed for payments. Sub-second transactions, sub-cent fees.', + }, + '/build': { + title: 'Tempo', + description: + 'Build payment products on Tempo with stablecoins, fast settlement, and predictable fees.', + }, + '/build/tempo-transactions': { + title: 'Tempo Transactions', + description: 'Batch, sponsor, schedule, and parallelize payments with Tempo Transactions.', + }, + '/build/tip20-tokens': { + title: 'TIP-20 Tokens', + description: + 'Stablecoin-first Tempo Tokens for payments, fees, memos, policies, and liquidity.', + }, + '/performance': { + title: 'Tempo Performance', + description: + 'Nightly benchmarks on Tempo throughput, block times, execution rates, and uptime.', + }, + '/diagrams': { + title: 'Tempo Diagrams', + description: 'A playground for Tempo diagrams, product visuals, and house-style SVG exports.', + }, +} + +const prefetchedPaths = new Set() + +function prefetchPath(href: string) { + if (!href.startsWith('/') || prefetchedPaths.has(href)) return + prefetchedPaths.add(href) + + const link = document.createElement('link') + link.rel = 'prefetch' + link.href = href + link.as = 'document' + document.head.appendChild(link) +} + +function applyRouteMetadata(route: string) { + const metadata = routeMetadata[route] ?? routeMetadata['/'] + document.title = metadata.title + document.querySelector('meta[name="description"]')?.setAttribute('content', metadata.description) +} + +export default function MarketingRoute({ + children, + route, +}: { + children: ReactNode + route: keyof typeof routeMetadata +}) { + const [analyticsReady, setAnalyticsReady] = useState(false) + + useEffect(() => { + applyRouteMetadata(route) + }, [route]) + + useEffect(() => { + if ('requestIdleCallback' in window) { + const idleId = window.requestIdleCallback(() => setAnalyticsReady(true), { timeout: 2_000 }) + return () => window.cancelIdleCallback(idleId) + } + const timeoutId = globalThis.setTimeout(() => setAnalyticsReady(true), 1) + return () => globalThis.clearTimeout(timeoutId) + }, []) + + useEffect(() => { + prefetchPath('/docs') + + const prefetchAnchor = (event: Event) => { + const target = event.target + if (!(target instanceof Element)) return + const anchor = target.closest('a[href]') + if (!(anchor instanceof HTMLAnchorElement)) return + if (anchor.origin !== window.location.origin) return + prefetchPath(anchor.pathname) + } + + document.addEventListener('pointerover', prefetchAnchor, { passive: true }) + document.addEventListener('focusin', prefetchAnchor) + return () => { + document.removeEventListener('pointerover', prefetchAnchor) + document.removeEventListener('focusin', prefetchAnchor) + } + }, []) + + return ( + <> + {children} + {analyticsReady && ( + + + + + + + )} + + ) +} diff --git a/src/marketing/PerformancePage.tsx b/src/marketing/PerformancePage.tsx new file mode 100644 index 00000000..db2cb561 --- /dev/null +++ b/src/marketing/PerformancePage.tsx @@ -0,0 +1,3 @@ +import PerformancePage from './app/performance/page' + +export default PerformancePage diff --git a/src/marketing/app/_components/ArrowUpRight.tsx b/src/marketing/app/_components/ArrowUpRight.tsx new file mode 100644 index 00000000..7289bdaf --- /dev/null +++ b/src/marketing/app/_components/ArrowUpRight.tsx @@ -0,0 +1,18 @@ +type Props = { + className?: string +} + +export default function ArrowUpRight({ className }: Props) { + return ( + + ) +} diff --git a/src/marketing/app/_components/Button.tsx b/src/marketing/app/_components/Button.tsx new file mode 100644 index 00000000..025e76aa --- /dev/null +++ b/src/marketing/app/_components/Button.tsx @@ -0,0 +1,50 @@ +import Link from 'next/link' +import type { ReactNode } from 'react' +import ArrowUpRight from './ArrowUpRight' + +// Single button styling for the whole site. Two variants: +// - primary: filled (the prominent CTA) +// - secondary: outlined (supporting actions) +// Renders a Next for internal hrefs ("/", "#") and an otherwise. +type Props = { + href: string + children: ReactNode + variant?: 'primary' | 'secondary' + arrow?: boolean + className?: string +} + +const VARIANTS = { + primary: 'bg-surface-onyx text-on-surface-onyx hover:opacity-80', + secondary: 'border border-line bg-surface-shell text-foreground hover:bg-surface-block', +} as const + +export default function Button({ + href, + children, + variant = 'secondary', + arrow = false, + className = '', +}: Props) { + const classes = `inline-flex h-11 items-center justify-center gap-2 px-5 font-sans text-[14px] tracking-[0] whitespace-nowrap transition-colors ${VARIANTS[variant]} ${className}` + const inner = ( + <> + {children} + {arrow ? : null} + + ) + + if (href.startsWith('/') || href.startsWith('#')) { + return ( + + {inner} + + ) + } + + return ( + + {inner} + + ) +} diff --git a/src/marketing/app/_components/CodePanel.tsx b/src/marketing/app/_components/CodePanel.tsx new file mode 100644 index 00000000..c7f009cc --- /dev/null +++ b/src/marketing/app/_components/CodePanel.tsx @@ -0,0 +1,283 @@ +// biome-ignore-all lint/a11y/noSvgWithoutTitle: Copy-state SVGs are decorative inside labelled buttons. + +'use client' + +import { useState } from 'react' +import DotCanvas from './DotCanvas' + +const COLOR = { + keyword: 'var(--code-token-keyword)', + fn: 'var(--code-token-function)', + string: 'var(--code-token-string)', + number: 'var(--code-token-number)', + comment: 'var(--code-token-comment)', + punct: 'var(--code-token-punctuation)', +} + +const KEYWORDS = new Set([ + 'import', + 'from', + 'export', + 'const', + 'let', + 'var', + 'async', + 'await', + 'return', + 'new', + 'function', + 'if', + 'else', + 'for', + 'of', + 'in', + 'true', + 'false', + 'null', + 'undefined', +]) + +type Token = { text: string; color?: string; start: number } +type TokenRun = Token & { boxed: boolean } + +function uniqueKey(base: string, counts: Map) { + const count = counts.get(base) ?? 0 + counts.set(base, count + 1) + return count === 0 ? base : `${base}:${count}` +} + +// Lightweight TS tokenizer — good enough for short, controlled snippets. +function tokenize(line: string): Token[] { + const tokens: Token[] = [] + const re = + /(\/\/[^\n]*)|(`[^`]*`|"[^"]*"|'[^']*')|(\b\d[\d_]*n?\b)|([A-Za-z_$][\w$]*)|(\s+)|([^\s])/g + let match: RegExpExecArray | null + while (true) { + match = re.exec(line) + if (match === null) break + + const [text, comment, str, num, ident, ws, punct] = match + const start = match.index + if (comment) tokens.push({ text, color: COLOR.comment, start }) + else if (str) tokens.push({ text, color: COLOR.string, start }) + else if (num) tokens.push({ text, color: COLOR.number, start }) + else if (ident) { + if (KEYWORDS.has(ident)) tokens.push({ text, color: COLOR.keyword, start }) + else if (/^\s*\(/.test(line.slice(re.lastIndex))) + tokens.push({ text, color: COLOR.fn, start }) + else tokens.push({ text, start }) + } else if (ws) tokens.push({ text, start }) + else if (punct) tokens.push({ text, color: COLOR.punct, start }) + } + return tokens +} + +// Char ranges in `line` covered by any of the highlight substrings. +function highlightRanges(line: string, highlight: string[]): [number, number][] { + const ranges: [number, number][] = [] + for (const needle of highlight) { + let from = line.indexOf(needle) + while (from !== -1) { + ranges.push([from, from + needle.length]) + from = line.indexOf(needle, from + needle.length) + } + } + return ranges +} + +function splitTokenByHighlights(token: Token, ranges: [number, number][]): TokenRun[] { + const tokenStart = token.start + const tokenEnd = token.start + token.text.length + const bounds = new Set([tokenStart, tokenEnd]) + + for (const [start, end] of ranges) { + const from = Math.max(start, tokenStart) + const to = Math.min(end, tokenEnd) + if (from < to) { + bounds.add(from) + bounds.add(to) + } + } + + return [...bounds] + .sort((a, b) => a - b) + .slice(0, -1) + .map((from, index, sorted) => { + const to = sorted[index + 1] ?? tokenEnd + return { + ...token, + text: token.text.slice(from - tokenStart, to - tokenStart), + start: from, + boxed: ranges.some(([start, end]) => from < end && to > start), + } + }) + .filter((run) => run.text.length > 0) +} + +function renderCode(code: string[], highlight?: string[]) { + const lineCounts = new Map() + + return code.map((line) => { + const lineKey = uniqueKey(line || 'blank-line', lineCounts) + const tokens = tokenize(line) + if (tokens.length === 0) return
{'\u00A0'}
+ + const ranges = highlight?.length ? highlightRanges(line, highlight) : [] + // Group adjacent tokens that share the same highlight state so each run of + // emphasized code gets a single boxed container. + const groups: { boxed: boolean; toks: TokenRun[] }[] = [] + for (const tok of tokens) { + for (const run of splitTokenByHighlights(tok, ranges)) { + const last = groups[groups.length - 1] + if (last && last.boxed === run.boxed) last.toks.push(run) + else groups.push({ boxed: run.boxed, toks: [run] }) + } + } + + return ( +
+ {groups.map((group) => { + const groupStart = group.toks[0]?.start ?? 0 + const groupKey = `${group.boxed ? 'boxed' : 'plain'}:${groupStart}` + const inner = group.toks.map((tok) => ( + + {tok.text} + + )) + return group.boxed ? ( + + {inner} + + ) : ( + {inner} + ) + })} +
+ ) + }) +} + +function CopyIcon() { + return ( + + + + + ) +} + +function CheckIcon() { + return ( + + + + ) +} + +function CopyButton({ text }: { text: string }) { + const [copied, setCopied] = useState(false) + + const handleCopy = async () => { + try { + await navigator.clipboard.writeText(text) + setCopied(true) + setTimeout(() => setCopied(false), 1500) + } catch { + // Clipboard unavailable (e.g. insecure context) — fail silently. + } + } + + return ( + + ) +} + +export default function CodePanel({ + code, + sizer, + highlight, + inline, + bare, +}: { + code: string[] + // Longest line across all of the row's snippets; locks the panel width so the + // centered block never re-positions as snippets swap (font is monospaced). + sizer?: string + // Substrings of `code` to wrap in an emphasis container. + highlight?: string[] + // Mobile variant: a self-sizing, horizontally-scrollable code block (no dot + // canvas) for use inside the accordion under a selected item. + inline?: boolean + // Drop the inline variant's own border for callers that frame the panel + // themselves (e.g. window chrome on the API page). + bare?: boolean +}) { + if (inline) { + return ( +
+
+          {renderCode(code, highlight)}
+        
+
+ ) + } + + return ( +
+ +
+ +
+ {sizer !== undefined && ( +
+              {sizer}
+            
+ )} +
+            {renderCode(code, highlight)}
+          
+
+
+
+ ) +} diff --git a/src/marketing/app/_components/CodeWindow.tsx b/src/marketing/app/_components/CodeWindow.tsx new file mode 100644 index 00000000..07c903fc --- /dev/null +++ b/src/marketing/app/_components/CodeWindow.tsx @@ -0,0 +1,90 @@ +'use client' + +import { useState } from 'react' +import CodePanel from './CodePanel' + +export type CodeVariant = { + lang: string + code: string[] + highlight?: string[] +} + +// macOS-style window chrome around a syntax-highlighted snippet; the "code" +// half of the showcase visual/code toggle. +export default function CodeWindow({ + title, + code, + highlight, + variants, + activeIndex: activeIndexProp, + onActiveChange, + heightClassName = '', +}: { + title: string + code?: string[] + highlight?: string[] + variants?: CodeVariant[] + activeIndex?: number + onActiveChange?: (index: number) => void + // Lets callers cap or lock the window height when it sits inside a framed + // visual/code panel. + heightClassName?: string +}) { + const panels = variants ?? [{ lang: 'Code', code: code ?? [], highlight }] + const [uncontrolledActiveIndex, setUncontrolledActiveIndex] = useState(0) + const activeIndex = activeIndexProp ?? uncontrolledActiveIndex + const activePanelIndex = Math.min(activeIndex, panels.length - 1) + const active = panels[activePanelIndex] + const setActivePanelIndex = (index: number) => { + setUncontrolledActiveIndex(index) + onActiveChange?.(index) + } + + return ( +
+
+ + + + + {title} + +
+ {variants && variants.length > 1 ? ( +
+ {panels.map((panel, index) => ( + + ))} +
+ ) : null} +
+ {panels.map((panel, index) => ( +
+ +
+ ))} +
+
+ ) +} diff --git a/src/marketing/app/_components/DotCanvas.tsx b/src/marketing/app/_components/DotCanvas.tsx new file mode 100644 index 00000000..6a35f8c6 --- /dev/null +++ b/src/marketing/app/_components/DotCanvas.tsx @@ -0,0 +1,309 @@ +'use client' + +import { type ReactNode, useEffect, useRef } from 'react' +import type { DotPattern, LitCell } from './dotPatterns' + +// Grid geometry from Figma (node 561:2991): 4px squares on an 18px pitch. +const DOT_SIZE = 5 +const GAP = 5 +const PITCH = DOT_SIZE + GAP +// Faint base-grid opacity over the panel surface. Tunable. +const BASE_ALPHA = 0.02 +// Cursor spotlight: half-size (CSS px) of the square region that lights up. +const CURSOR_RADIUS = 0 +// Follow lag (ms) — higher trails the cursor more smoothly. +const CURSOR_RESPONSE_MS = 120 +// #d9d9d9 — cursor and default pattern color. +const BRIGHT_RGB = '255, 255, 255' +// Per-cell crossfade duration when the pattern changes. +const MORPH_MS = 1000 + +function readVar(el: Element, name: string, fallback: string): string { + return getComputedStyle(el).getPropertyValue(name).trim() || fallback +} + +function lerp(a: number, b: number, m: number): number { + return a + (b - a) * m +} + +function blendRgb(a: string, b: string, m: number): string { + const pa = a.split(',').map((s) => Number.parseFloat(s)) + const pb = b.split(',').map((s) => Number.parseFloat(s)) + return `${Math.round(lerp(pa[0], pb[0], m))}, ${Math.round(lerp(pa[1], pb[1], m))}, ${Math.round(lerp(pa[2], pb[2], m))}` +} + +type Props = { + className?: string + pattern?: DotPattern + // Overlays anchored to the dot grid. The container exposes --dot-off-x/y + // (the grid's centering offsets) so children can align to exact cells. + children?: ReactNode +} + +export default function DotCanvas({ className, pattern, children }: Props) { + const containerRef = useRef(null) + const canvasRef = useRef(null) + // Read the latest pattern from the loop without re-running the effect. + const patternRef = useRef(pattern) + // Restart the pattern's reveal animation when it re-enters the viewport. + const revealRef = useRef(true) + + useEffect(() => { + patternRef.current = pattern + }, [pattern]) + + useEffect(() => { + const container = containerRef.current + const canvas = canvasRef.current + if (!container || !canvas) return + const ctx = canvas.getContext('2d') + if (!ctx) return + + const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches + const baseRgb = readVar(container, '--canvas-dot-rgb', '125, 125, 125') + + let w = 0 + let h = 0 + // Centering offset so the grid block sits centered in the container instead + // of left/top-anchored with leftover space (most visible on mobile widths). + let offX = 0 + let offY = 0 + const cursor = { x: -9999, y: -9999, active: false } + + const gridSize = () => ({ + cols: Math.floor(w / PITCH), + rows: Math.floor(h / PITCH), + }) + + const recomputeOffset = () => { + const { cols, rows } = gridSize() + // Center the dot field itself (first dot's left edge → last dot's right + // edge), not the cell block, so the trailing cell gap doesn't pile up as + // extra padding on the right/bottom. + offX = (w - ((cols - 1) * PITCH + DOT_SIZE)) / 2 + offY = (h - ((rows - 1) * PITCH + DOT_SIZE)) / 2 + } + + // Paint a single dot (circle) at the cell's top-left pixel. fillStyle must + // be set by the caller. + const DOT_RADIUS = DOT_SIZE / 2.5 + const fillDot = (x: number, y: number) => { + ctx.beginPath() + ctx.arc(x + DOT_RADIUS, y + DOT_RADIUS, DOT_RADIUS, 0, Math.PI * 2) + ctx.fill() + } + + const drawBase = () => { + ctx.clearRect(0, 0, w, h) + ctx.fillStyle = `rgba(${baseRgb}, ${BASE_ALPHA})` + const { cols, rows } = gridSize() + for (let r = 0; r < rows; r++) { + for (let c = 0; c < cols; c++) { + fillDot(offX + c * PITCH, offY + r * PITCH) + } + } + } + + const drawCell = (col: number, row: number, color: string, alpha: number) => { + if (alpha <= 0.001) return + ctx.fillStyle = `rgba(${color}, ${alpha})` + fillDot(offX + col * PITCH, offY + row * PITCH) + } + + const drawPattern = (pat: DotPattern, t: number, mult: number) => { + const { cols, rows } = gridSize() + for (const cell of pat({ t, cols, rows })) { + drawCell(cell.col, cell.row, cell.color ?? BRIGHT_RGB, cell.alpha * mult) + } + } + + // Per-cell morph: shared cells lerp; cells only in `from` fade out; cells + // only in `to` fade in. + const drawMorph = (from: DotPattern, fromT: number, to: DotPattern, toT: number, m: number) => { + const { cols, rows } = gridSize() + const fromMap = new Map() + for (const c of from({ t: fromT, cols, rows })) { + fromMap.set(`${c.col},${c.row}`, c) + } + const toMap = new Map() + for (const c of to({ t: toT, cols, rows })) { + toMap.set(`${c.col},${c.row}`, c) + } + const keys = new Set([...fromMap.keys(), ...toMap.keys()]) + for (const key of keys) { + const f = fromMap.get(key) + const tc = toMap.get(key) + let color: string + let alpha: number + if (f && tc) { + color = blendRgb(f.color ?? BRIGHT_RGB, tc.color ?? BRIGHT_RGB, m) + alpha = lerp(f.alpha, tc.alpha, m) + } else if (f) { + color = f.color ?? BRIGHT_RGB + alpha = f.alpha * (1 - m) + } else if (tc) { + color = tc.color ?? BRIGHT_RGB + alpha = tc.alpha * m + } else { + continue + } + const [col, row] = key.split(',').map(Number) + drawCell(col, row, color, alpha) + } + } + + const drawCursor = () => { + if (!cursor.active) return + ctx.fillStyle = `rgba(${BRIGHT_RGB}, 1)` + const { cols, rows } = gridSize() + for (let r = 0; r < rows; r++) { + const y = offY + r * PITCH + if (Math.abs(y - cursor.y) > CURSOR_RADIUS) continue + for (let c = 0; c < cols; c++) { + const x = offX + c * PITCH + if (Math.abs(x - cursor.x) > CURSOR_RADIUS) continue + fillDot(x, y) + } + } + } + + const resize = () => { + const dpr = Math.min(window.devicePixelRatio || 1, 2) + w = container.clientWidth + h = container.clientHeight + if (w === 0 || h === 0) return + recomputeOffset() + container.style.setProperty('--dot-off-x', `${offX}px`) + container.style.setProperty('--dot-off-y', `${offY}px`) + canvas.width = Math.round(w * dpr) + canvas.height = Math.round(h * dpr) + ctx.setTransform(dpr, 0, 0, dpr, 0, 0) + if (reduced) { + drawBase() + if (patternRef.current) drawPattern(patternRef.current, 9999, 1) + } + } + resize() + const ro = new ResizeObserver(resize) + ro.observe(container) + + if (reduced) { + drawBase() + if (patternRef.current) drawPattern(patternRef.current, 9999, 1) + return () => ro.disconnect() + } + + let visible = true + const io = new IntersectionObserver( + ([entry]) => { + const nowVisible = entry?.isIntersecting ?? true + if (nowVisible && !visible) revealRef.current = true + visible = nowVisible + }, + { rootMargin: '200px' }, + ) + io.observe(container) + + const target = { x: -9999, y: -9999, active: false } + const onMove = (e: MouseEvent) => { + const rect = container.getBoundingClientRect() + const x = e.clientX - rect.left + const y = e.clientY - rect.top + const inside = x >= 0 && x <= rect.width && y >= 0 && y <= rect.height + if (inside) { + target.x = x + target.y = y + if (!target.active) { + cursor.x = x + cursor.y = y + } + target.active = true + } else { + target.active = false + } + } + const onLeave = () => { + target.active = false + } + window.addEventListener('mousemove', onMove) + window.addEventListener('mouseleave', onLeave) + window.addEventListener('blur', onLeave) + + let raf = 0 + let last = 0 + let t0 = performance.now() + let shown = patternRef.current + let from: DotPattern | null = null + let fromT = 0 + let morphStart = -1 + + const frame = (now: number) => { + raf = requestAnimationFrame(frame) + if (!visible) { + last = now + return + } + + if (revealRef.current) { + t0 = now + revealRef.current = false + } + + // Pattern changed → start a per-cell morph from the outgoing pattern. + const cur = patternRef.current + if (cur !== shown) { + from = shown ?? null + fromT = (now - t0) / 1000 + morphStart = now + t0 = now + shown = cur + } + + const dt = last ? Math.min(now - last, 100) : 16 + last = now + const damping = 1 - Math.exp(-dt / CURSOR_RESPONSE_MS) + cursor.x += (target.x - cursor.x) * damping + cursor.y += (target.y - cursor.y) * damping + cursor.active = target.active + + const tNew = (now - t0) / 1000 + const m = morphStart < 0 ? 1 : Math.min((now - morphStart) / MORPH_MS, 1) + + drawBase() + if (m < 1 && (from || cur)) { + if (from && cur) drawMorph(from, fromT, cur, tNew, m) + else if (from) drawPattern(from, fromT, 1 - m) + else if (cur) drawPattern(cur, tNew, m) + } else if (cur) { + drawPattern(cur, tNew, 1) + } + drawCursor() + + if (m >= 1) { + from = null + morphStart = -1 + } + } + raf = requestAnimationFrame(frame) + + return () => { + cancelAnimationFrame(raf) + ro.disconnect() + io.disconnect() + window.removeEventListener('mousemove', onMove) + window.removeEventListener('mouseleave', onLeave) + window.removeEventListener('blur', onLeave) + } + }, []) + + return ( +
+ + {children} +
+ ) +} diff --git a/src/marketing/app/_components/EdgeMarkers.tsx b/src/marketing/app/_components/EdgeMarkers.tsx new file mode 100644 index 00000000..fb913b62 --- /dev/null +++ b/src/marketing/app/_components/EdgeMarkers.tsx @@ -0,0 +1,10 @@ +type Props = { + edge?: 'top' | 'bottom' + wideOnly?: boolean +} + +export default function EdgeMarkers({ edge = 'top', wideOnly = false }: Props) { + const visibility = wideOnly ? 'hidden 2xl:grid' : 'grid' + + return +} diff --git a/src/marketing/app/_components/Footer.tsx b/src/marketing/app/_components/Footer.tsx new file mode 100644 index 00000000..0ab1514d --- /dev/null +++ b/src/marketing/app/_components/Footer.tsx @@ -0,0 +1,163 @@ +import Link from 'next/link' +import SimpleIconsGithub from '~icons/simple-icons/github' +import SimpleIconsX from '~icons/simple-icons/x' +import { featurePath } from '../_lib/featurePaths' +import { TEMPO_SDK_DOCS_URL } from '../_lib/links' +import EdgeMarkers from './EdgeMarkers' +import Reveal from './Reveal' +import TempoLogo from './TempoLogo' +import ThemeToggle from './ThemeToggle' + +type FooterLink = { + label: string + href: string +} +type FooterColumn = { header: string; links: FooterLink[] } + +const footerLinkClassName = + 'font-sans text-[14px] tracking-[0] text-foreground/50 transition-colors hover:text-foreground' + +const CONTACT_URL = 'https://tempo.xyz/contact' +const GITHUB_URL = 'https://github.com/tempoxyz' +const X_URL = 'https://twitter.com/tempo' + +function FooterLinkItem({ link }: { link: FooterLink }) { + return link.href.startsWith('/') ? ( + + {link.label} + + ) : ( + + {link.label} + + ) +} + +const columns: FooterColumn[] = [ + { + header: 'Protocol', + links: [ + { label: 'Transactions', href: featurePath('transactions') }, + { label: 'TIP-20 tokens', href: featurePath('tokens') }, + ], + }, + { + header: 'Documentation', + links: [ + { label: 'Docs', href: '/docs' }, + { label: 'Payments guide', href: '/docs/guide/payments' }, + { label: 'Token issuance', href: '/docs/guide/issuance' }, + ], + }, + { + header: 'Tools', + links: [ + { label: 'Tempo CLI', href: '/docs/wallet' }, + { label: 'TIDX', href: '/docs/developer-tools/indexer' }, + { label: 'Tempo Explorer', href: 'https://explorer.tempo.xyz' }, + { label: 'Tempo Faucet', href: 'https://faucet.tempo.xyz' }, + ], + }, + { + header: 'Libraries', + links: [ + { label: 'MPP', href: 'https://github.com/tempoxyz/mpp' }, + { label: 'SDKs', href: TEMPO_SDK_DOCS_URL }, + { label: 'GitHub', href: 'https://github.com/tempoxyz' }, + ], + }, + { + header: 'For agents', + links: [ + { + label: 'Tempo Docs skill', + href: '/docs/guide/using-tempo-with-ai#docs-skill', + }, + { label: 'Tempo MCP server', href: 'https://mcp.tempo.xyz' }, + { + label: 'Setup docs', + href: '/docs/guide/using-tempo-with-ai', + }, + ], + }, + { + header: 'Resources', + links: [ + { label: 'Performance', href: '/performance' }, + { label: 'Open source', href: '/#open-source' }, + { label: 'Contact', href: CONTACT_URL }, + ], + }, +] + +const socialLinks = [ + { label: 'GitHub', href: GITHUB_URL, Icon: SimpleIconsGithub }, + { label: 'X', href: X_URL, Icon: SimpleIconsX }, +] + +export default function Footer() { + return ( +
+ + +
+
+ + + +

+ Stablecoin payments infrastructure for developers, apps, and agents building on Tempo. +

+
+ + © {new Date().getFullYear()} Tempo + + + tempo.xyz + +
+
+ + +
+
+ + +
+
+
+ ) +} diff --git a/src/marketing/app/_components/Header.tsx b/src/marketing/app/_components/Header.tsx new file mode 100644 index 00000000..92bc6f2a --- /dev/null +++ b/src/marketing/app/_components/Header.tsx @@ -0,0 +1,802 @@ +// biome-ignore-all lint/a11y/noSvgWithoutTitle: Header SVGs are decorative icons paired with labels or button text. +// biome-ignore-all lint/a11y/noStaticElementInteractions: The dropdown surface tracks hover/focus while child controls keep semantic roles. + +'use client' + +import Link from 'next/link' +import { usePathname } from 'next/navigation' +import { type ReactNode, useLayoutEffect, useRef, useState } from 'react' +import { featurePath } from '../_lib/featurePaths' +import { TEMPO_SDK_DOCS_URL } from '../_lib/links' +import ArrowUpRight from './ArrowUpRight' +import MegaMenu, { type MegaLink, type MegaMenuData } from './MegaMenu' +import { + ApiIcon, + DocsIcon, + ExplorerIcon, + McpIcon, + TerminalIcon, + TokensIcon, + TransactionsIcon, + WalletIcon, +} from './menuIcons' +import TempoLogo from './TempoLogo' + +const protocolMenu: MegaMenuData = { + variant: 'vertical', + columns: [ + { + title: 'Transactions', + items: [ + { + label: 'Tempo Transactions', + desc: 'Flexible transactions for batching, fee sponsorship, scheduling, and more', + href: featurePath('transactions'), + icon: , + }, + ], + }, + { + title: 'Assets', + items: [ + { + label: 'TIP-20 tokens', + desc: 'Stablecoin-first token standard for payments', + href: featurePath('tokens'), + icon: , + }, + ], + }, + ], +} + +const developersMenu: MegaMenuData = { + columns: [ + { + title: 'Documentation', + items: [ + { + label: 'Docs', + desc: 'Guides, references & quickstart', + href: '/docs', + icon: , + }, + ], + }, + { + title: 'Tools', + items: [ + { + label: 'Wallet', + desc: 'A Tempo-first wallet for your agents', + href: 'https://wallet.tempo.xyz', + icon: , + }, + { + label: 'TIDX', + desc: 'Raw indexer queries & event streams', + href: '/docs/developer-tools/indexer', + icon: , + }, + { + label: 'Tempo Explorer', + desc: 'Search blocks, txs & tokens', + href: 'https://explorer.tempo.xyz', + icon: , + }, + ], + }, + { + title: 'Libraries', + items: [ + { + label: 'MPP', + desc: 'Open protocol for agentic payments', + href: 'https://mpp.dev/', + icon: , + }, + { + label: 'SDKs', + desc: 'TypeScript, Rust, Go & Foundry', + href: TEMPO_SDK_DOCS_URL, + icon: , + }, + ], + }, + ], +} + +const TEMPO_AI_GUIDE_URL = '/docs/guide/using-tempo-with-ai' +const TEMPO_DOCS_SKILL_URL = `${TEMPO_AI_GUIDE_URL}#docs-skill` +const TEMPO_PLUGIN_URL = `${TEMPO_AI_GUIDE_URL}#install-tempo-plugins` +const TEMPO_MCP_URL = 'https://mcp.tempo.xyz' + +type MenuItem = { label: string; href: string; mega?: MegaMenuData } + +function isExternal(href: string): boolean { + return !href.startsWith('/') && !href.startsWith('#') +} + +function pathMatches(pathname: string, href: string): boolean { + return pathname === href || pathname.startsWith(`${href}/`) +} + +function isActiveMenuItem(pathname: string, item: MenuItem): boolean { + if (item.label === 'Build') { + return pathname === '/' || pathname.startsWith('/build') + } + if (item.label === 'Resources') { + return pathname === TEMPO_SDK_DOCS_URL || pathname.startsWith(`${TEMPO_SDK_DOCS_URL}/`) + } + return !isExternal(item.href) && pathMatches(pathname, item.href) +} + +// 3x3 grid drawn as an SVG so all nine cells share identical geometry and stay +// evenly spaced at any size or device-pixel ratio (a div grid with gaps drifts +// from subpixel rounding at this small a scale). Cells are 3px on a 4px pitch. +function ActiveSquare({ activeKey }: { activeKey: string }) { + return ( + + {[0, 4, 8].flatMap((y) => + [0, 4, 8].map((x) => ( + + )), + )} + + ) +} + +const menu: MenuItem[] = [ + { label: 'Build', href: '/#protocol', mega: protocolMenu }, + { label: 'Resources', href: '/docs', mega: developersMenu }, + { label: 'Performance', href: '/performance' }, + { label: 'Docs', href: '/docs' }, +] + +// Flatten a mega menu into its leaf links for the mobile accordion. +function megaLinks(data: MegaMenuData): MegaLink[] { + return data.columns.flatMap((col) => col.items) +} + +function MenuIcon() { + return ( + + + + ) +} + +function CloseIcon() { + return ( + + + + ) +} + +function GearIcon() { + return ( + + + + + ) +} + +function Chevron({ open }: { open: boolean }) { + return ( + + + + ) +} + +// The server URL is appended at render time so it can be color-highlighted. +const mcpCommands = [ + { label: 'Claude', prefix: 'claude mcp add --transport http tempo ' }, + { label: 'Codex', prefix: 'codex mcp add tempo --url ' }, + { label: 'Amp', prefix: 'amp mcp add --transport http tempo ' }, +] + +function CopyIcon() { + return ( + + + + + ) +} + +function CheckIcon() { + return ( + + + + ) +} + +function AgentMenuItem({ + href, + label, + desc, + icon, + onClick, +}: { + href: string + label: string + desc: string + icon: ReactNode + onClick?: () => void +}) { + const external = isExternal(href) + + return ( + + {external ? ( + + ) : null} + + {icon} + + + {label} + + {desc} + + + + ) +} + +function AgentsPanel({ + variant = 'desktop', + onNavigate, +}: { + variant?: 'desktop' | 'mobile' + onNavigate?: () => void +}) { + const desktop = variant === 'desktop' + const [activeCommandIndex, setActiveCommandIndex] = useState(0) + const [copied, setCopied] = useState(false) + const activeCommand = mcpCommands[activeCommandIndex] + + const copyCommand = async () => { + try { + await navigator.clipboard.writeText(activeCommand.prefix + TEMPO_MCP_URL) + setCopied(true) + setTimeout(() => setCopied(false), 1500) + } catch { + // Clipboard unavailable (e.g. insecure context) — fail silently. + } + } + + return ( +
+ {desktop ? ( +

+ Use Tempo with AI +

+ ) : null} + +
+
+
+ + + + + + Tempo MCP server + + + Give agents search and read tools for Tempo docs + + +
+ + {/* Config block hangs under the item's text column (icon 40px + gap 12px). */} +
+
+ {mcpCommands.map((item, index) => { + const active = index === activeCommandIndex + return ( + + ) + })} +
+ + +
+
+ } + onClick={onNavigate} + /> + } + onClick={onNavigate} + /> +
+
+ ) +} + +export default function Header() { + const pathname = usePathname() + const [open, setOpen] = useState(false) + const [expanded, setExpanded] = useState(null) + // The mobile menu is a fixed overlay anchored just below the nav bar, so we + // track the bar's height to offset it (and keep it correct across resizes). + const [navH, setNavH] = useState(0) + + // Desktop dropdowns share one floating surface that morphs (slides & + // resizes) between panels instead of closing and reopening. + const [activeMenu, setActiveMenu] = useState(null) + const [geom, setGeom] = useState<{ x: number; w: number; h: number } | null>(null) + // Geometry only animates when moving between two open panels; a fresh open + // snaps into place and just fades in. + const [morphing, setMorphing] = useState(false) + const headerRef = useRef(null) + const navRef = useRef(null) + const triggerRefs = useRef(new Map()) + const panelRefs = useRef(new Map()) + const prevActive = useRef(null) + const closeTimer = useRef | null>(null) + + const cancelClose = () => { + if (closeTimer.current) clearTimeout(closeTimer.current) + } + const openMenu = (key: string) => { + cancelClose() + setActiveMenu(key) + } + // Delay lets the pointer cross the gap between trigger and panel (and hop + // between triggers) without the surface collapsing. + const scheduleClose = () => { + cancelClose() + closeTimer.current = setTimeout(() => setActiveMenu(null), 120) + } + + useLayoutEffect(() => { + if (!activeMenu) { + prevActive.current = null + return + } + const panel = panelRefs.current.get(activeMenu) + const trigger = triggerRefs.current.get(activeMenu) + const header = headerRef.current + if (!panel || !trigger || !header) return + const w = panel.offsetWidth + const h = panel.offsetHeight + const t = trigger.getBoundingClientRect() + const b = header.getBoundingClientRect() + const raw = + activeMenu === 'For agents' + ? t.right - b.left - w // right-align with its trigger + : t.left - b.left + t.width / 2 - w / 2 // center under trigger + const x = Math.round(Math.min(Math.max(raw, 12), b.width - w - 12)) + setMorphing(prevActive.current !== null) + setGeom({ x, w, h }) + prevActive.current = activeMenu + }, [activeMenu]) + + // Measure the nav bar so the mobile overlay can fill from its bottom edge to + // the bottom of the viewport (otherwise the page bleeds through beneath the + // short menu list, which looks broken on taller/wider screens). + useLayoutEffect(() => { + const measure = () => setNavH(navRef.current?.offsetHeight ?? 0) + measure() + window.addEventListener('resize', measure) + return () => window.removeEventListener('resize', measure) + }, []) + + // Lock background scroll while the mobile menu is open. + useLayoutEffect(() => { + if (!open) return + const prev = document.body.style.overflow + document.body.style.overflow = 'hidden' + return () => { + document.body.style.overflow = prev + } + }, [open]) + + const dropdowns: { key: string; panel: ReactNode }[] = [ + ...menu.flatMap((item) => + item.mega ? [{ key: item.label, panel: }] : [], + ), + { key: 'For agents', panel: }, + ] + + const close = () => { + setOpen(false) + setExpanded(null) + } + + return ( +
+ + + {/* Desktop dropdowns: one shared surface that slides & resizes between + panels while their content crossfades. */} +
+
+
+
+ {dropdowns.map(({ key, panel }) => ( +
{ + if (el) panelRefs.current.set(key, el) + else panelRefs.current.delete(key) + }} + className={`absolute top-0 left-0 w-max transition-opacity duration-150 ease-out motion-reduce:transition-none ${ + activeMenu === key ? 'opacity-100' : 'pointer-events-none opacity-0' + }`} + > + {panel} +
+ ))} +
+
+
+
+ + {/* Mobile menu: a fixed overlay that fills from the nav bar down to the + bottom of the viewport so page content never bleeds through beneath + the menu list (which looked broken on taller/wider screens). */} +
+
+ {menu.map((item) => { + const external = isExternal(item.href) + const active = isActiveMenuItem(pathname, item) + return item.mega ? ( +
+ +
+
+
+ {megaLinks(item.mega).map((sub) => + !isExternal(sub.href) ? ( + + {sub.label} + + ) : ( + + {sub.label} + + + ), + )} +
+
+
+
+ ) : ( + + {active ? : null} + {item.label} + + ) + })} +
+ +
+
+ +
+
+
+
+
+
+ ) +} diff --git a/src/marketing/app/_components/Hero.tsx b/src/marketing/app/_components/Hero.tsx new file mode 100644 index 00000000..8e92e93a --- /dev/null +++ b/src/marketing/app/_components/Hero.tsx @@ -0,0 +1,128 @@ +import Link from 'next/link' +import { featurePath } from '../_lib/featurePaths' +import ArrowUpRight from './ArrowUpRight' +import Button from './Button' +import EdgeMarkers from './EdgeMarkers' +import HeroPatternCanvas from './HeroPatternCanvas' +import { colorForIndex } from './palette' +import Reveal from './Reveal' + +const HERO_ACTIONS = [ + { + label: 'Integrate Tempo', + href: '/docs/quickstart/integrate-tempo', + variant: 'primary', + }, + { + label: 'Accept payments', + href: '/docs/guide/payments/accept-a-payment', + variant: 'secondary', + }, + { + label: 'Make agentic payments', + href: '/docs/guide/machine-payments', + variant: 'secondary', + }, +] as const + +const [primaryAction, ...secondaryActions] = HERO_ACTIONS + +const HERO_PATHS = [ + { + title: 'Stablecoin-native tokens', + desc: 'Stablecoins are first-class on Tempo, with TIP-20 and payments-first features.', + href: featurePath('tokens'), + }, + { + title: 'Transaction flows designed for payments', + desc: 'Batching, fee sponsorship, scheduling, and parallel transactions are built in.', + href: featurePath('transactions'), + }, + { + title: 'Performance at scale', + desc: 'Throughput that pushes the frontier, with predictably low fees at scale.', + href: '/performance', + }, +] as const + +export default function Hero() { + return ( +
+ + +
+ +

+ Engineered for payments from the ground up +

+

+ Accept payments, issue stablecoins, and build blockchain applications that scale globally + from day one. +

+ +
+ +
+ ) +} diff --git a/src/marketing/app/_components/HeroDots.tsx b/src/marketing/app/_components/HeroDots.tsx new file mode 100644 index 00000000..acaec3f3 --- /dev/null +++ b/src/marketing/app/_components/HeroDots.tsx @@ -0,0 +1,24 @@ +'use client' + +import DotCanvas from './DotCanvas' +import { heroAmbientPattern, heroAmbientPlusPattern } from './heroPattern' +import PlusCanvas from './PlusCanvas' + +// Client wrapper so server-rendered pages can use the canvas: pattern +// functions can't cross the server→client boundary as props. The top-down +// gradient mutes the upper dots, matching the homepage hero treatment. +export default function HeroDots({ plus = false }: { plus?: boolean }) { + return ( + <> + {plus ? ( + + ) : ( + + )} +
+ + ) +} diff --git a/src/marketing/app/_components/HeroPatternCanvas.tsx b/src/marketing/app/_components/HeroPatternCanvas.tsx new file mode 100644 index 00000000..cdc5d397 --- /dev/null +++ b/src/marketing/app/_components/HeroPatternCanvas.tsx @@ -0,0 +1,8 @@ +'use client' + +import { heroAmbientPlusPattern } from './heroPattern' +import PlusCanvas from './PlusCanvas' + +export default function HeroPatternCanvas() { + return +} diff --git a/src/marketing/app/_components/HomeShowcases.tsx b/src/marketing/app/_components/HomeShowcases.tsx new file mode 100644 index 00000000..13974a86 --- /dev/null +++ b/src/marketing/app/_components/HomeShowcases.tsx @@ -0,0 +1,21 @@ +'use client' + +import { useState } from 'react' +import type { ShowcaseMode } from './ModeToggle' +import TokensShowcase from './TokensShowcase' +import TransactionsShowcase from './TransactionsShowcase' + +export default function HomeShowcases() { + const [mode, setMode] = useState('visual') + + return ( + <> +
+ +
+
+ +
+ + ) +} diff --git a/src/marketing/app/_components/MegaMenu.tsx b/src/marketing/app/_components/MegaMenu.tsx new file mode 100644 index 00000000..d0e144d9 --- /dev/null +++ b/src/marketing/app/_components/MegaMenu.tsx @@ -0,0 +1,87 @@ +import Link from 'next/link' +import type { ReactNode } from 'react' +import ArrowUpRight from './ArrowUpRight' + +export type MegaLink = { + label: string + desc: string + href: string + icon: ReactNode +} + +export type MegaColumn = { title: string; items: MegaLink[] } +export type MegaMenuData = { columns: MegaColumn[]; variant?: 'columns' | 'vertical' } + +// One leaf link: an icon tile beside a stacked label + description. Internal +// hrefs (starting with "/") route through next/link; everything else opens in a +// new tab. Icon tiles use neutral surfaces so the nav stays monochrome. +function MegaItem({ link }: { link: MegaLink }) { + const external = !link.href.startsWith('/') && !link.href.startsWith('#') + const inner = ( + <> + {external ? ( + + ) : null} + + {link.icon} + + + {link.label} + + {link.desc} + + + + ) + + const className = + 'group/item relative flex items-start gap-3 rounded-[4px] px-3 py-2.5 transition-colors hover:bg-foreground/[0.04]' + + return link.href.startsWith('/') ? ( + + {inner} + + ) : ( + + {inner} + + ) +} + +// Chrome (border, bg, shadow) lives on the shared morphing surface in Header, +// so panels can crossfade inside one box. +export default function MegaMenu({ data }: { data: MegaMenuData }) { + if (data.variant === 'vertical') { + return ( +
+
    + {data.columns + .flatMap((col) => col.items) + .map((item) => ( +
  • + +
  • + ))} +
+
+ ) + } + + return ( +
+ {data.columns.map((col) => { + return ( +
+
    + {col.items.map((item) => ( +
  • + +
  • + ))} +
+
+ ) + })} +
+ ) +} diff --git a/src/marketing/app/_components/ModeToggle.tsx b/src/marketing/app/_components/ModeToggle.tsx new file mode 100644 index 00000000..22e0e1fd --- /dev/null +++ b/src/marketing/app/_components/ModeToggle.tsx @@ -0,0 +1,38 @@ +'use client' + +export type ShowcaseMode = 'visual' | 'code' + +export default function ModeToggle({ + mode, + setMode, +}: { + mode: ShowcaseMode + setMode: (mode: ShowcaseMode) => void +}) { + const labels = { + visual: 'Diagram', + code: 'Code', + } satisfies Record + + return ( +
+ + {(['visual', 'code'] as const).map((option) => ( + + ))} + +
+ ) +} diff --git a/src/marketing/app/_components/OpenSourceSection.tsx b/src/marketing/app/_components/OpenSourceSection.tsx new file mode 100644 index 00000000..b658f79f --- /dev/null +++ b/src/marketing/app/_components/OpenSourceSection.tsx @@ -0,0 +1,148 @@ +'use client' + +import Image from 'next/image' +import { useState } from 'react' +import ArrowUpRight from './ArrowUpRight' +import EdgeMarkers from './EdgeMarkers' +import Reveal from './Reveal' + +type Repo = { + name: string + desc: string + href: string + brandColor: string +} + +const repos: Repo[] = [ + { + name: 'Tempo', + desc: 'The chain itself: node, EVM, and protocol.', + href: 'https://github.com/tempoxyz', + brandColor: '#ffffff', + }, + { + name: 'MPP', + desc: 'The open machine-payments protocol, co-authored with Stripe.', + href: 'https://mpp.dev/', + brandColor: '#ffffff', + }, + { + name: 'Reth', + desc: 'The Rust execution client Tempo runs on.', + href: 'https://github.com/paradigmxyz/reth', + brandColor: '#F74C00', + }, + { + name: 'Foundry', + desc: 'The standard for testing and deploying contracts.', + href: 'https://www.getfoundry.sh/', + brandColor: '#04E100', + }, + { + name: 'Viem', + desc: 'TypeScript interfaces for Ethereum and Tempo apps.', + href: 'https://viem.sh/', + brandColor: '#FFC515', + }, + { + name: 'Wagmi', + desc: 'React hooks and app primitives for onchain interfaces.', + href: 'https://wagmi.sh/', + brandColor: '#455CB8', + }, +] + +// Decorative Reth chip straddling the shell's left boundary line. Hidden at +// rest; it scales into view only while the "Reth" repo tile below is hovered. +// Only rendered when the viewport is wide enough for it to hang outside the +// max-w-7xl shell without being clipped. +function RethBadge({ shown }: { shown: boolean }) { + const reveal = `transition-[opacity,scale] duration-350 ease-out motion-reduce:transition-none ${ + shown ? 'scale-100 opacity-100' : 'scale-40 opacity-0' + }` + + return ( +
+
+ {/* Ripple rings: invisible at rest, expanding outward while hovered. */} + + +
+ +
+
+
+ ) +} + +export default function OpenSourceSection() { + const [rethHovered, setRethHovered] = useState(false) + return ( +
+ + +

+ Open source +

+

+ All of Tempo's code is open source, built by the same team behind Reth, Foundry, + viem, and more. +

+
+ + +
+ ) +} diff --git a/src/marketing/app/_components/PerfSection.tsx b/src/marketing/app/_components/PerfSection.tsx new file mode 100644 index 00000000..aaf8ce4d --- /dev/null +++ b/src/marketing/app/_components/PerfSection.tsx @@ -0,0 +1,261 @@ +// biome-ignore-all lint/a11y/noSvgWithoutTitle: Section charts are decorative summaries with adjacent text labels. +// biome-ignore-all lint/suspicious/noArrayIndexKey: The uptime sparkline is a fixed static strip with no item state. + +import Link from 'next/link' +import type { ReactNode } from 'react' +import { linePath } from '../performance/_lib/chart' +import type { PerfRun } from '../performance/_lib/runs' +import ArrowUpRight from './ArrowUpRight' +import Button from './Button' +import EdgeMarkers from './EdgeMarkers' +import { PALETTE } from './palette' +import Reveal from './Reveal' +import type { Stat } from './stats' + +const PERFORMANCE_PAGE = '/performance' + +// Sparkline points in a fixed 0–100 viewBox (8% vertical padding). Rendered +// with preserveAspectRatio="none" + non-scaling strokes, so the same path +// fits any card size without client-side measuring. +const sparkPoints = (values: number[]): [number, number][] => { + const min = Math.min(...values) + const max = Math.max(...values) + const span = max - min || 1 + return values.map((v, i) => [(i / (values.length - 1)) * 100, 92 - ((v - min) / span) * 84]) +} + +function TpsSpark({ runs }: { runs: PerfRun[] }) { + const pts = sparkPoints(runs.map((r) => r.settledTps)) + return ( + + + + + + + + + + + ) +} + +// Compact retelling of the /performance payment-lanes chart: congested +// general blockspace carrying the real settled-TPS series, with the dedicated +// lane's near-flat fee line pulsing below. +function LaneSpark({ runs }: { runs: PerfRun[] }) { + const pts = sparkPoints(runs.map((r) => r.settledTps)) + const feePointCount = Math.max(runs.length, 16) + const feePts = Array.from( + { length: feePointCount }, + (_, i) => + [ + (i / (feePointCount - 1)) * 100, + 68 + Math.sin(i * 1.7) * 1.15 + Math.sin(i * 0.55) * 0.55, + ] as [number, number], + ) + + return ( +
+
+
+

+ GENERAL BLOCKSPACE +

+ + + +
+
+
+

+ DEDICATED PAYMENT LANE +

+ + + + +
+
+ ) +} + +// Compact form of the /performance uptime strip: one thin tick per night, +// solid green — the visual form of the uptime claim. +function UptimeSpark() { + return ( +
+ {Array.from({ length: 60 }, (_, i) => ( +
+ ))} +
+ ) +} + +function StatCard({ + href, + label, + value, + desc, + children, + className = '', + numberOnly = false, +}: { + href: string + label: string + value: string + desc: string + children: ReactNode + className?: string + numberOnly?: boolean +}) { + return ( + +
+
+

+ {label} +

+ {!numberOnly ? ( +

+ {value} +

+ ) : null} +
+ +
+ {numberOnly ? ( +
+

+ {value} +

+
+ ) : ( +
{children}
+ )} +

{desc}

+ + ) +} + +export default function PerfSection({ stats, runs }: { stats: Stat[]; runs: PerfRun[] }) { + const mainValue = (category: string, fallback: string) => + stats.find((s) => s.category === category)?.main.value ?? fallback + + // Headline numbers come from the live benchmark overlay where the API + // provides them (Speed, Reliability); the sparklines draw the full nightly + // feed. Cost and uptime claims stay static. + const hasFeed = runs.length >= 2 + const cards = [ + { + href: `${PERFORMANCE_PAGE}#settlement`, + label: 'Fast, guaranteed settlement', + value: `${mainValue('Speed', '508')} ms`, + desc: 'Average time between finalized blocks in the latest benchmark data.', + spark: null, + className: 'sm:border-r lg:col-span-2', + numberOnly: true, + }, + { + href: PERFORMANCE_PAGE, + label: 'High throughput', + value: `${mainValue('Reliability', '21,200')} TPS`, + desc: 'Settled transfers per second measured during benchmark runs.', + spark: hasFeed ? : null, + className: 'lg:col-span-4', + }, + { + href: `${PERFORMANCE_PAGE}#fees`, + label: 'Predictably low fees', + value: '$0.001', + desc: 'Base network fee for a standard stablecoin transfer.', + spark: hasFeed ? : null, + className: 'sm:border-r lg:col-span-3', + }, + { + href: `${PERFORMANCE_PAGE}#uptime`, + label: 'Reliable uptime guarantees', + value: '99.999%', + desc: 'Network availability target for production payment workloads.', + spark: , + className: 'lg:col-span-3', + }, + ] + + return ( +
+ + +

+ Pushing the frontier of blockchain performance. +

+ +
+ + +
+ {cards.map((card) => ( + + {card.spark} + + ))} +
+
+
+ ) +} diff --git a/src/marketing/app/_components/PlusCanvas.tsx b/src/marketing/app/_components/PlusCanvas.tsx new file mode 100644 index 00000000..0fa81cb4 --- /dev/null +++ b/src/marketing/app/_components/PlusCanvas.tsx @@ -0,0 +1,321 @@ +'use client' + +import { type ReactNode, useEffect, useRef } from 'react' +import type { DotPattern, LitCell } from './dotPatterns' + +// Same grid + interaction model as DotCanvas, but each cell is painted as a "+" +// glyph instead of a circle. Geometry from Figma (node 561:2991): 4px squares +// on an 18px pitch. +const DOT_SIZE = 14 +const GAP = 3 +const PITCH = DOT_SIZE + GAP +// Faint base-grid opacity over the panel surface. Tunable. +const BASE_ALPHA = 0.2 +// Cursor spotlight: half-size (CSS px) of the square region that lights up. +const CURSOR_RADIUS = 0 +// Follow lag (ms) — higher trails the cursor more smoothly. +const CURSOR_RESPONSE_MS = 100 +// #d9d9d9 — cursor and default pattern color. +const BRIGHT_RGB = '125, 125, 125' +// Per-cell crossfade duration when the pattern changes. +const MORPH_MS = 500 + +function readVar(el: Element, name: string, fallback: string): string { + return getComputedStyle(el).getPropertyValue(name).trim() || fallback +} + +function lerp(a: number, b: number, m: number): number { + return a + (b - a) * m +} + +function blendRgb(a: string, b: string, m: number): string { + const pa = a.split(',').map((s) => Number.parseFloat(s)) + const pb = b.split(',').map((s) => Number.parseFloat(s)) + return `${Math.round(lerp(pa[0], pb[0], m))}, ${Math.round(lerp(pa[1], pb[1], m))}, ${Math.round(lerp(pa[2], pb[2], m))}` +} + +type Props = { + className?: string + pattern?: DotPattern + // Overlays anchored to the dot grid. The container exposes --dot-off-x/y + // (the grid's centering offsets) so children can align to exact cells. + children?: ReactNode +} + +export default function PlusCanvas({ className, pattern, children }: Props) { + const containerRef = useRef(null) + const canvasRef = useRef(null) + // Read the latest pattern from the loop without re-running the effect. + const patternRef = useRef(pattern) + // Restart the pattern's reveal animation when it re-enters the viewport. + const revealRef = useRef(true) + + useEffect(() => { + patternRef.current = pattern + }, [pattern]) + + useEffect(() => { + const container = containerRef.current + const canvas = canvasRef.current + if (!container || !canvas) return + const ctx = canvas.getContext('2d') + if (!ctx) return + + const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches + const baseRgb = readVar(container, '--canvas-dot-rgb', '125, 125, 125') + + let w = 0 + let h = 0 + // Centering offset so the grid block sits centered in the container instead + // of left/top-anchored with leftover space (most visible on mobile widths). + let offX = 0 + let offY = 0 + const cursor = { x: -9999, y: -9999, active: false } + + const gridSize = () => ({ + cols: Math.floor(w / PITCH), + rows: Math.floor(h / PITCH), + }) + + const recomputeOffset = () => { + const { cols, rows } = gridSize() + // Center the dot field itself (first dot's left edge → last dot's right + // edge), not the cell block, so the trailing cell gap doesn't pile up as + // extra padding on the right/bottom. + offX = (w - ((cols - 1) * PITCH + DOT_SIZE)) / 2 + offY = (h - ((rows - 1) * PITCH + DOT_SIZE)) / 2 + } + + // Paint a single "+" centered in the cell at the cell's top-left pixel. + // fillStyle must be set by the caller. The plus is two bars sharing the + // cell's full extent, with a thinner arm so the cross reads clearly. + const PLUS_ARM = Math.max(1, Math.round(DOT_SIZE / 20)) + const fillDot = (x: number, y: number) => { + const off = (DOT_SIZE - PLUS_ARM) / 2 + // Vertical bar. + ctx.fillRect(x + off, y, PLUS_ARM, DOT_SIZE) + // Horizontal bar. + ctx.fillRect(x, y + off, DOT_SIZE, PLUS_ARM) + } + + const drawBase = () => { + ctx.clearRect(0, 0, w, h) + ctx.fillStyle = `rgba(${baseRgb}, ${BASE_ALPHA})` + const { cols, rows } = gridSize() + for (let r = 0; r < rows; r++) { + for (let c = 0; c < cols; c++) { + fillDot(offX + c * PITCH, offY + r * PITCH) + } + } + } + + const drawCell = (col: number, row: number, color: string, alpha: number) => { + if (alpha <= 0.001) return + ctx.fillStyle = `rgba(${color}, ${alpha})` + fillDot(offX + col * PITCH, offY + row * PITCH) + } + + const drawPattern = (pat: DotPattern, t: number, mult: number) => { + const { cols, rows } = gridSize() + for (const cell of pat({ t, cols, rows })) { + drawCell(cell.col, cell.row, cell.color ?? BRIGHT_RGB, cell.alpha * mult) + } + } + + // Per-cell morph: shared cells lerp; cells only in `from` fade out; cells + // only in `to` fade in. + const drawMorph = (from: DotPattern, fromT: number, to: DotPattern, toT: number, m: number) => { + const { cols, rows } = gridSize() + const fromMap = new Map() + for (const c of from({ t: fromT, cols, rows })) { + fromMap.set(`${c.col},${c.row}`, c) + } + const toMap = new Map() + for (const c of to({ t: toT, cols, rows })) { + toMap.set(`${c.col},${c.row}`, c) + } + const keys = new Set([...fromMap.keys(), ...toMap.keys()]) + for (const key of keys) { + const f = fromMap.get(key) + const tc = toMap.get(key) + let color: string + let alpha: number + if (f && tc) { + color = blendRgb(f.color ?? BRIGHT_RGB, tc.color ?? BRIGHT_RGB, m) + alpha = lerp(f.alpha, tc.alpha, m) + } else if (f) { + color = f.color ?? BRIGHT_RGB + alpha = f.alpha * (1 - m) + } else if (tc) { + color = tc.color ?? BRIGHT_RGB + alpha = tc.alpha * m + } else { + continue + } + const [col, row] = key.split(',').map(Number) + drawCell(col, row, color, alpha) + } + } + + const drawCursor = () => { + if (!cursor.active) return + const { cols, rows } = gridSize() + for (let r = 0; r < rows; r++) { + const y = offY + r * PITCH + const dy = y + DOT_SIZE / 2 - cursor.y + if (Math.abs(dy) > CURSOR_RADIUS) continue + for (let c = 0; c < cols; c++) { + const x = offX + c * PITCH + const dx = x + DOT_SIZE / 2 - cursor.x + const dist = Math.hypot(dx, dy) + if (dist > CURSOR_RADIUS) continue + // Smooth radial falloff: full glow at the center, fading to 0 at the edge. + const f = 1 - dist / CURSOR_RADIUS + const alpha = f * f + if (alpha <= 0.001) continue + ctx.fillStyle = `rgba(${BRIGHT_RGB}, ${alpha})` + fillDot(x, y) + } + } + } + + const resize = () => { + const dpr = Math.min(window.devicePixelRatio || 1, 2) + w = container.clientWidth + h = container.clientHeight + if (w === 0 || h === 0) return + recomputeOffset() + container.style.setProperty('--dot-off-x', `${offX}px`) + container.style.setProperty('--dot-off-y', `${offY}px`) + canvas.width = Math.round(w * dpr) + canvas.height = Math.round(h * dpr) + ctx.setTransform(dpr, 0, 0, dpr, 0, 0) + if (reduced) { + drawBase() + if (patternRef.current) drawPattern(patternRef.current, 9999, 1) + } + } + resize() + const ro = new ResizeObserver(resize) + ro.observe(container) + + if (reduced) { + drawBase() + if (patternRef.current) drawPattern(patternRef.current, 9999, 1) + return () => ro.disconnect() + } + + let visible = true + const io = new IntersectionObserver( + ([entry]) => { + const nowVisible = entry?.isIntersecting ?? true + if (nowVisible && !visible) revealRef.current = true + visible = nowVisible + }, + { rootMargin: '200px' }, + ) + io.observe(container) + + const target = { x: -9999, y: -9999, active: false } + const onMove = (e: MouseEvent) => { + const rect = container.getBoundingClientRect() + const x = e.clientX - rect.left + const y = e.clientY - rect.top + const inside = x >= 0 && x <= rect.width && y >= 0 && y <= rect.height + if (inside) { + target.x = x + target.y = y + if (!target.active) { + cursor.x = x + cursor.y = y + } + target.active = true + } else { + target.active = false + } + } + const onLeave = () => { + target.active = false + } + window.addEventListener('mousemove', onMove) + window.addEventListener('mouseleave', onLeave) + window.addEventListener('blur', onLeave) + + let raf = 0 + let last = 0 + let t0 = performance.now() + let shown = patternRef.current + let from: DotPattern | null = null + let fromT = 0 + let morphStart = -1 + + const frame = (now: number) => { + raf = requestAnimationFrame(frame) + if (!visible) { + last = now + return + } + + if (revealRef.current) { + t0 = now + revealRef.current = false + } + + // Pattern changed → start a per-cell morph from the outgoing pattern. + const cur = patternRef.current + if (cur !== shown) { + from = shown ?? null + fromT = (now - t0) / 1000 + morphStart = now + t0 = now + shown = cur + } + + const dt = last ? Math.min(now - last, 100) : 16 + last = now + const damping = 1 - Math.exp(-dt / CURSOR_RESPONSE_MS) + cursor.x += (target.x - cursor.x) * damping + cursor.y += (target.y - cursor.y) * damping + cursor.active = target.active + + const tNew = (now - t0) / 1000 + const m = morphStart < 0 ? 1 : Math.min((now - morphStart) / MORPH_MS, 1) + + drawBase() + if (m < 1 && (from || cur)) { + if (from && cur) drawMorph(from, fromT, cur, tNew, m) + else if (from) drawPattern(from, fromT, 1 - m) + else if (cur) drawPattern(cur, tNew, m) + } else if (cur) { + drawPattern(cur, tNew, 1) + } + drawCursor() + + if (m >= 1) { + from = null + morphStart = -1 + } + } + raf = requestAnimationFrame(frame) + + return () => { + cancelAnimationFrame(raf) + ro.disconnect() + io.disconnect() + window.removeEventListener('mousemove', onMove) + window.removeEventListener('mouseleave', onLeave) + window.removeEventListener('blur', onLeave) + } + }, []) + + return ( +
+ + {children} +
+ ) +} diff --git a/src/marketing/app/_components/Reveal.tsx b/src/marketing/app/_components/Reveal.tsx new file mode 100644 index 00000000..027b5a39 --- /dev/null +++ b/src/marketing/app/_components/Reveal.tsx @@ -0,0 +1,14 @@ +import type { ReactNode } from 'react' + +type Props = { + children: ReactNode + className?: string + // Kept for API compatibility; no longer used. + delay?: number +} + +// Renders content as-is. The scroll-in fade/lift/blur animation was removed +// site-wide; this remains a passthrough wrapper to preserve existing layout. +export default function Reveal({ children, className }: Props) { + return
{children}
+} diff --git a/src/marketing/app/_components/TempoLogo.tsx b/src/marketing/app/_components/TempoLogo.tsx new file mode 100644 index 00000000..6797660f --- /dev/null +++ b/src/marketing/app/_components/TempoLogo.tsx @@ -0,0 +1,23 @@ +type Props = { + className?: string +} + +export default function TempoLogo({ className }: Props) { + return ( + + ) +} diff --git a/src/marketing/app/_components/ThemeToggle.tsx b/src/marketing/app/_components/ThemeToggle.tsx new file mode 100644 index 00000000..16f4471a --- /dev/null +++ b/src/marketing/app/_components/ThemeToggle.tsx @@ -0,0 +1,175 @@ +// biome-ignore-all lint/a11y/noSvgWithoutTitle: Theme glyphs are decorative inside labelled radio buttons. +// biome-ignore-all lint/a11y/useSemanticElements: Segmented theme buttons expose radio state while applying changes immediately. + +'use client' + +import type { ReactNode } from 'react' +import { useSyncExternalStore } from 'react' + +type Theme = 'light' | 'dark' | 'system' + +const STORAGE_KEY = 'vocs-theme' + +function getStoredTheme(): Theme { + if (typeof window === 'undefined') return 'system' + const stored = localStorage.getItem(STORAGE_KEY) + if (stored === 'light' || stored === 'dark' || stored === 'system') { + return stored + } + return 'system' +} + +function getServerTheme(): Theme { + return 'system' +} + +function getSystemTheme(): 'light' | 'dark' { + if (typeof window === 'undefined') return 'dark' + return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light' +} + +const disableTransitionsCSS = + '*,*::before,*::after{-webkit-transition:none!important;-moz-transition:none!important;-o-transition:none!important;-ms-transition:none!important;transition:none!important}' + +function applyTheme(theme: Theme) { + const resolved = theme === 'system' ? getSystemTheme() : theme + const html = document.documentElement + + const style = document.createElement('style') + style.appendChild(document.createTextNode(disableTransitionsCSS)) + document.head.appendChild(style) + + html.setAttribute('data-vocs-theme', resolved) + if (resolved === 'light') { + html.dataset.theme = 'light' + } else { + delete html.dataset.theme + } + html.style.colorScheme = resolved + + window.getComputedStyle(document.body) + requestAnimationFrame(() => { + requestAnimationFrame(() => { + document.head.removeChild(style) + }) + }) +} + +function setStoredTheme(theme: Theme) { + try { + localStorage.setItem(STORAGE_KEY, theme) + } catch { + // The visible theme should still change if storage is blocked. + } + window.dispatchEvent(new Event('tempo-theme-change')) +} + +function subscribe(onStoreChange: () => void) { + window.addEventListener('storage', onStoreChange) + window.addEventListener('tempo-theme-change', onStoreChange) + const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)') + const onSystemThemeChange = () => { + if (getStoredTheme() === 'system') applyTheme('system') + } + mediaQuery.addEventListener('change', onSystemThemeChange) + return () => { + window.removeEventListener('storage', onStoreChange) + window.removeEventListener('tempo-theme-change', onStoreChange) + mediaQuery.removeEventListener('change', onSystemThemeChange) + } +} + +export default function ThemeToggle() { + const theme = useSyncExternalStore(subscribe, getStoredTheme, getServerTheme) + + return ( +
+ + + + + +
+ ) +} + +function Option({ + children, + label, + value, + theme, +}: { + children: ReactNode + label: string + value: Theme + theme: Theme +}) { + const checked = theme === value + + return ( + + ) +} + +function SunIcon() { + return ( + + + + + ) +} + +function MoonIcon() { + return ( + + + + ) +} + +function MonitorIcon() { + return ( + + + + + ) +} diff --git a/src/marketing/app/_components/TokensShowcase.tsx b/src/marketing/app/_components/TokensShowcase.tsx new file mode 100644 index 00000000..7736afac --- /dev/null +++ b/src/marketing/app/_components/TokensShowcase.tsx @@ -0,0 +1,354 @@ +'use client' + +import { Fragment, useState } from 'react' +import FeatureDiagram from '../diagrams/_components/FeatureDiagram' +import type { FeatureDiagramSpec } from '../diagrams/_lib/featureDiagram' +import Button from './Button' +import CodeWindow, { type CodeVariant } from './CodeWindow' +import EdgeMarkers from './EdgeMarkers' +import ModeToggle, { type ShowcaseMode } from './ModeToggle' +import { panelFadeClass } from './panelFade' +import Reveal from './Reveal' + +const VISUAL_HEIGHT = 'lg:h-[424px]' +const CODE_HEIGHT = 'max-h-[390px]' +const DIAGRAM_CONTAINER = 'p-6 lg:min-h-0 lg:p-10' +const SHOWCASE_HEIGHT = 'lg:min-h-[560px]' + +type Row = { + title: string + desc: string + href: string + spec: FeatureDiagramSpec + panelTitle: string + variants: CodeVariant[] +} + +const rows: Row[] = [ + { + title: 'Plugged into liquidity by default', + desc: 'TIP-20 tokens are tradable on an enshrined DEX with efficient liquidity.', + href: '/docs/guide/stablecoin-dex/executing-swaps', + spec: { + kind: 'dex', + input: { accent: 1, label: 'USDC', detail: '' }, + resolver: { accent: 2, label: 'pathUSD', detail: 'QUOTE TOKEN' }, + output: { accent: 2, label: 'USDT', detail: '' }, + asks: [0.45, 0.7, 0.95], + bids: [0.9, 0.65, 0.4], + bookLabel: 'ENSHRINED DEX', + caption: '', + }, + panelTitle: 'stablecoin-swap', + variants: [ + { + lang: 'TypeScript', + code: [ + 'import { parseUnits } from "viem";', + 'import { client } from "./viem.config";', + '', + 'const usdc = "0x20c0000000000000000000000000000000000001";', + 'const usdt = "0x20c0000000000000000000000000000000000002";', + 'const amountIn = parseUnits("100", 6);', + 'const minAmountOut = parseUnits("99.50", 6);', + '', + 'const { receipt } = await client.dex.sellSync({', + ' tokenIn: usdc,', + ' tokenOut: usdt,', + ' amountIn,', + ' minAmountOut,', + '});', + ], + highlight: ['client.dex.sellSync'], + }, + { + lang: 'Solidity', + code: [ + 'IStablecoinDex dex = IStablecoinDex(', + ' 0xdec0000000000000000000000000000000000000', + ');', + '', + 'uint128 amountIn = 100e6;', + 'uint128 quote = dex.quoteSwapExactAmountIn(', + ' USDC,', + ' USDT,', + ' amountIn', + ');', + '', + 'uint128 minOut = (quote * 995) / 1000;', + 'uint128 amountOut = dex.swapExactAmountIn(', + ' USDC,', + ' USDT,', + ' amountIn,', + ' minOut', + ');', + ], + highlight: ['quoteSwapExactAmountIn', 'swapExactAmountIn'], + }, + { + lang: 'CLI', + code: [ + 'DEX=0xdec0000000000000000000000000000000000000', + 'AMOUNT_IN=100000000', + 'MIN_USDT_OUT=99500000', + '', + 'cast send "$DEX" \\', + ' "swapExactAmountIn(address,address,uint128,uint128)(uint128)" \\', + ' "$USDC" "$USDT" "$AMOUNT_IN" "$MIN_USDT_OUT" \\', + ' --tempo.fee-token "$USDC" \\', + ' --rpc-url "$TEMPO_RPC_URL" \\', + ' --private-key "$PRIVATE_KEY"', + ], + highlight: ['swapExactAmountIn', '--tempo.fee-token'], + }, + ], + }, + { + title: 'Policies enforce compliance', + desc: 'Policies can enforce sender, recipient, and token rules for compliance.', + href: '/docs/guide/issuance/manage-stablecoin#configure-transfer-policies', + spec: { + kind: 'gate', + dest: 1, + destLabel: 'demoUSD', + destSub: 'TIP-403 POLICY', + sources: [ + { accent: 0, label: 'ALLOWED SENDER', detail: 'ON WHITELIST' }, + { accent: 2, label: 'APPROVED PAYEE', detail: 'POLICY OK' }, + { accent: 3, label: 'BLOCKED ADDRESS', detail: 'ON BLACKLIST', blocked: true }, + ], + }, + panelTitle: 'policy', + variants: [ + { + lang: 'TypeScript', + code: [ + 'import { client } from "./tempo";', + '', + '// One registry entry, enforced on every transfer', + 'const { policyId } = await client.policy.createSync({', + ' type: "blacklist",', + ' addresses: [blocked],', + '});', + '', + 'await client.token.changeTransferPolicySync({', + ' token: demoUsd,', + ' policyId,', + '});', + ], + highlight: ['client.policy.createSync', 'changeTransferPolicySync'], + }, + { + lang: 'Rust', + code: [ + 'use tempo_alloy::contracts::precompiles::{ITIP20, ITIP403Registry};', + '', + 'let registry = ITIP403Registry::new(TIP403_REGISTRY, provider);', + 'let policy_id = registry', + ' .createPolicyWithAccounts(admin, PolicyType::BLACKLIST, vec![blocked])', + ' .send()', + ' .await?;', + '', + 'let token = ITIP20::new(demo_usd, provider);', + 'token.changeTransferPolicyId(policy_id).send().await?;', + ], + highlight: ['createPolicyWithAccounts', 'changeTransferPolicyId'], + }, + { + lang: 'CLI', + code: [ + 'cast send 0x403c000000000000000000000000000000000000 \\', + ' "createPolicyWithAccounts(address,uint8,address[])" \\', + ' "$ADMIN" 1 "[$BLOCKED]" \\', + ' --tempo.fee-token 0x20c0000000000000000000000000000000000001 \\', + ' --rpc-url "$TEMPO_RPC_URL" \\', + ' --private-key "$PRIVATE_KEY"', + '', + 'cast send "$DEMO_USD" "changeTransferPolicyId(uint64)" "$POLICY_ID" \\', + ' --tempo.fee-token 0x20c0000000000000000000000000000000000001 \\', + ' --rpc-url "$TEMPO_RPC_URL" \\', + ' --private-key "$PRIVATE_KEY"', + ], + highlight: ['createPolicyWithAccounts', 'changeTransferPolicyId'], + }, + ], + }, + { + title: 'Optional memos for reconciliation', + desc: 'Attach invoice IDs to transfers and reconcile payments onchain.', + href: '/docs/guide/payments/transfer-memos', + spec: { + kind: 'memo', + paymentLabel: 'PAYMENT', + amount: '125.00 demoUSD', + amountAccent: 2, + memoLabel: 'MEMO', + memoValue: 'INV-12345', + memoAccent: 0, + explorerLabel: 'EXPLORER', + fields: [ + { label: 'TX', barW: 92 }, + { label: 'FROM / TO', barW: 64 }, + { label: 'AMOUNT', value: '125.00' }, + { label: 'MEMO', value: 'INV-12345', highlight: true }, + ], + caption: 'RECONCILE BY REFERENCE ONCHAIN', + }, + panelTitle: 'memo', + variants: [ + { + lang: 'TypeScript', + code: [ + 'import { parseUnits, stringToHex, pad } from "viem";', + 'import { client } from "./tempo";', + '', + '// A 32-byte reference travels with the transfer', + 'const memo = pad(stringToHex("INV-12345"), { size: 32 });', + '', + 'const { receipt } = await client.token.transferSync({', + ' token: demoUsd,', + ' to: merchant,', + ' amount: parseUnits("125", 6),', + ' memo,', + '});', + ], + highlight: ['memo,'], + }, + { + lang: 'Rust', + code: [ + 'use alloy::primitives::{B256, U256};', + 'use tempo_alloy::contracts::precompiles::ITIP20;', + '', + 'let memo = B256::right_padding_from(b"INV-12345");', + 'let token = ITIP20::new(demo_usd, provider);', + '', + 'token', + ' .transferWithMemo(merchant, U256::from(125_000_000), memo)', + ' .send()', + ' .await?;', + ], + highlight: ['transferWithMemo', 'memo'], + }, + { + lang: 'CLI', + code: [ + 'MEMO=$(cast format-bytes32-string "INV-12345")', + '', + 'cast send "$DEMO_USD" \\', + ' "transferWithMemo(address,uint256,bytes32)" \\', + ' "$MERCHANT" 125000000 "$MEMO" \\', + ' --rpc-url "$TEMPO_RPC_URL" \\', + ' --private-key "$PRIVATE_KEY"', + ], + highlight: ['transferWithMemo', 'MEMO'], + }, + ], + }, +] satisfies Row[] + +function VisualMock({ active }: { active: Row }) { + return ( + // Bleed over the visual column padding so the diagram sets the full frame. +
+ +
+ ) +} + +export default function TokensShowcase({ + mode, + setMode, +}: { + mode: ShowcaseMode + setMode: (mode: ShowcaseMode) => void +}) { + const [active, setActive] = useState(0) + const selectRow = (index: number) => { + if (index !== active) { + setActive(index) + } + } + + return ( +
+ {/* Mirror of TransactionsShowcase: content column first in the DOM so + mobile stacks heading-first, reversed on desktop so the visual sits + on the left. */} + + +
+

+ Stablecoin-native tokens. +

+
+ {rows.map((row, i) => ( + + ))} +
+
+ + +
+
+ +
+
+ +
+
+ {rows.map((row, i) => ( + +
+ +
+
+ +
+
+ ))} +
+
+
+
+ ) +} diff --git a/src/marketing/app/_components/TransactionsShowcase.tsx b/src/marketing/app/_components/TransactionsShowcase.tsx new file mode 100644 index 00000000..b54f2756 --- /dev/null +++ b/src/marketing/app/_components/TransactionsShowcase.tsx @@ -0,0 +1,183 @@ +'use client' + +import { Fragment, useState } from 'react' +import FeatureDiagram from '../diagrams/_components/FeatureDiagram' +import type { FeatureDiagramSpec } from '../diagrams/_lib/featureDiagram' +import Button from './Button' +import CodeWindow, { type CodeVariant } from './CodeWindow' +import EdgeMarkers from './EdgeMarkers' +import ModeToggle, { type ShowcaseMode } from './ModeToggle' +import { panelFadeClass } from './panelFade' +import Reveal from './Reveal' +import { + feeSponsorCodeVariants, + feeTokenCodeVariants, + paymentLaneCodeVariants, +} from './transactionCodeVariants' + +const VISUAL_HEIGHT = 'lg:h-[424px]' +const CODE_HEIGHT = 'max-h-[390px]' +const DIAGRAM_CONTAINER = 'p-6 lg:min-h-0 lg:p-10' +const SHOWCASE_HEIGHT = 'lg:min-h-[560px]' + +type Row = { + title: string + desc: string + href: string + spec: FeatureDiagramSpec + panelTitle: string + variants: CodeVariant[] +} + +const rows: Row[] = [ + { + title: 'Pay fees in stablecoins', + desc: 'Users can pay blockchain fees using any stablecoin they choose.', + href: '/docs/guide/payments/pay-fees-in-any-stablecoin', + spec: { + kind: 'feeamm', + user: { accent: 0, label: 'USER', detail: 'SELECTS FEE TOKEN' }, + selectedToken: { accent: 0, symbol: 'USDC' }, + receivedToken: { accent: 1, symbol: 'USDT' }, + ammLabel: 'FEE AMM', + validator: { accent: 1, label: 'VALIDATOR', detail: 'RECEIVES USDT' }, + }, + panelTitle: 'fee-token.ts', + variants: feeTokenCodeVariants, + }, + { + title: 'Predictable fees', + desc: 'Dedicated payment lanes keep payment and payout fees predictable.', + href: '/docs/protocol/blockspace/payment-lane-specification#motivation', + spec: { + kind: 'blockspace', + payments: [ + { accent: 3, label: 'PAYMENT', detail: 'FEE $0.001' }, + { accent: 1, label: 'PAYOUT', detail: 'FEE $0.001' }, + ], + general: { accent: 0, label: 'AIRDROP / TRADE', detail: 'FEE $0.01' }, + paymentLaneLabel: 'PAYMENT BLOCKSPACE', + generalLabel: 'GENERAL BLOCKSPACE', + }, + panelTitle: 'payment-lane.ts', + variants: paymentLaneCodeVariants, + }, + { + title: 'Fee sponsorship', + desc: 'Apps and agents can pay on behalf of users.', + href: '/docs/guide/payments/sponsor-user-fees', + spec: { + kind: 'sponsor', + user: { accent: 0, label: 'USER', detail: 'SENDS TX' }, + sponsor: { accent: 1, label: 'APP', detail: 'FEE PAYER' }, + txLabel: 'TEMPO TX', + actionLabel: 'PAYMENT', + gasLabel: 'APP', + hubLabel: 'EXECUTES', + caption: 'FEE PAYER BALANCE IS DEBITED', + }, + panelTitle: 'sponsor.ts', + variants: feeSponsorCodeVariants, + }, +] satisfies Row[] + +function VisualMock({ active }: { active: Row }) { + return ( + // Bleed over the visual column padding so the diagram sets the full frame. +
+ +
+ ) +} + +export default function TransactionsShowcase({ + mode, + setMode, +}: { + mode: ShowcaseMode + setMode: (mode: ShowcaseMode) => void +}) { + const [active, setActive] = useState(0) + const selectRow = (index: number) => { + if (index !== active) { + setActive(index) + } + } + + return ( +
+ + +
+

+ Flexible fees for apps using stablecoins. +

+
+ {rows.map((row, i) => ( + + ))} +
+
+ + +
+
+ +
+
+ +
+
+ {rows.map((row, i) => ( + +
+ +
+
+ +
+
+ ))} +
+
+
+
+ ) +} diff --git a/src/marketing/app/_components/dotPatterns/arrow.ts b/src/marketing/app/_components/dotPatterns/arrow.ts new file mode 100644 index 00000000..b03236e8 --- /dev/null +++ b/src/marketing/app/_components/dotPatterns/arrow.ts @@ -0,0 +1,35 @@ +import type { DotPattern, LitCell } from './types' + +// A bold static right-pointing arrow (→) drawn in highlighted dots, anchored +// toward the right edge. Used as the illustration on the "next feature" card. +const HEAD = 5 // arrowhead depth (cols) and half-height at its base +const SHAFT = 9 // shaft length, in dots +const THICK = 1 // shaft half-thickness → (2 * THICK + 1) rows tall +const RIGHT_GAP = 5 // dots between the tip and the right edge +const ALPHA = 0.85 + +export const arrowPattern: DotPattern = ({ cols, rows }) => { + const cells: LitCell[] = [] + const tip = cols - 1 - RIGHT_GAP + const mid = Math.floor(rows / 2) + + const add = (col: number, row: number) => { + if (col >= 0 && col < cols && row >= 0 && row < rows) { + cells.push({ col, row, alpha: ALPHA }) + } + } + + // Solid arrowhead — a filled triangle with its apex at the tip. + for (let k = 0; k <= HEAD; k++) { + const c = tip - k + for (let r = mid - k; r <= mid + k; r++) add(c, r) + } + + // Thick shaft running back from behind the head. + const shaftRight = tip - HEAD - 1 + for (let c = shaftRight - SHAFT + 1; c <= shaftRight; c++) { + for (let r = mid - THICK; r <= mid + THICK; r++) add(c, r) + } + + return cells +} diff --git a/src/marketing/app/_components/dotPatterns/build.ts b/src/marketing/app/_components/dotPatterns/build.ts new file mode 100644 index 00000000..da5b84da --- /dev/null +++ b/src/marketing/app/_components/dotPatterns/build.ts @@ -0,0 +1,342 @@ +import { clamp01, DOT_ALPHA, GREEN, hash, smoothstep, splat } from './helpers' +import type { DotPattern, LitCell } from './types' + +/** + * Dot patterns for the Build section. Hovering a capability row morphs the dot + * band into that row's pattern; `buildAmbient` is the resting state. + * + * Each pattern is a pure function of time + grid size that returns the lit + * cells. To tinker, edit the `controls` object at the top of each section — + * the grid is `cols` × `rows` cells and `t` is seconds since the pattern was + * revealed. + */ + +// ═══════════════════════════════════════════════════════════════════════════ +// Row 1 · "Assembly" (Create & Use Stablecoin Accounts) +// Scattered dots stream in, glide together into an account card that travels +// left → right, then disperse as it exits. Staggered cards keep it continuous. +// ═══════════════════════════════════════════════════════════════════════════ + +const ASSEMBLY = { + cardW: 12, // card width (cells) + cardH: 6, // card height (cells) + crossSeconds: 5, // time for one card to cross the band + scatter: 5, // how far dots spread out before converging + cards: 5, // how many cards travel the band at once + formStart: 0.05, // point in the trip (0..1) where a card starts forming + formSpan: 0.3, // fraction of the trip the form-up / break-up takes +} + +// Card shape as {dc, dr} offsets from its top-left: an outline + a chip. +function makeCard(w: number, h: number): { dc: number; dr: number }[] { + const cells: { dc: number; dr: number }[] = [] + for (let c = 0; c < w; c++) { + cells.push({ dc: c, dr: 0 }) // top edge + cells.push({ dc: c, dr: h - 1 }) // bottom edge + } + for (let r = 1; r < h - 1; r++) { + cells.push({ dc: 0, dr: r }) // left edge + cells.push({ dc: w - 1, dr: r }) // right edge + } + for (let r = 0; r < 2; r++) { + for (let c = 0; c < 3; c++) cells.push({ dc: 2 + c, dr: 2 + r }) // chip + } + return cells +} + +const cardCells = makeCard(ASSEMBLY.cardW, ASSEMBLY.cardH) + +const assembly: DotPattern = ({ t, cols, rows }) => { + const cells: LitCell[] = [] + const cardY = Math.floor((rows - ASSEMBLY.cardH) / 2) + const startX = -ASSEMBLY.cardW - 6 // fully off the left + const endX = cols + 6 // fully off the right + + for (let inst = 0; inst < ASSEMBLY.cards; inst++) { + const progress = (t / ASSEMBLY.crossSeconds + inst / ASSEMBLY.cards) % 1 + const cardX = startX + (endX - startX) * progress + + // 0 = scattered, 1 = fully formed. Ramps up after entering, holds across + // the middle, ramps back down before exiting. + const formIn = smoothstep(clamp01((progress - ASSEMBLY.formStart) / ASSEMBLY.formSpan)) + const formOut = smoothstep(clamp01((1 - ASSEMBLY.formStart - progress) / ASSEMBLY.formSpan)) + const formed = Math.min(formIn, formOut) + if (formed <= 0.001) continue + + const scatter = ASSEMBLY.scatter * (1 - formed) + cardCells.forEach((cell, i) => { + const angle = hash(i * 1.3 + inst * 11.7) * Math.PI * 2 + const x = cardX + cell.dc + Math.cos(angle) * scatter + const y = cardY + cell.dr + Math.sin(angle) * scatter + splat(cells, x, y, formed * DOT_ALPHA, cols, rows) + }) + } + + return cells +} + +// ═══════════════════════════════════════════════════════════════════════════ +// Row 2 · "Globes" (Issue tokenized assets) +// Small static globes staggered across the band, each emitting expanding +// ripples. Where ripples from neighbours meet (constructive interference) the +// dots light up green. +// ═══════════════════════════════════════════════════════════════════════════ + +const ISSUANCE = { + globes: 6, // number of static ripple sources + orbRadius: 1.4, // size of each globe marker (cells) + waveFreq: 0.85, // ripple ring density + waveSpeed: 3, // ripple expansion speed + falloff: 16, // distance over which a globe's ripples fade (cells) + crestMin: 0.25, // wave value needed to light a dot + greenAt: 1.1, // summed wave above this lights green (ripples meeting) +} + +// Soft-edged filled disc — the static globe marker. +function fillOrb(cells: LitCell[], cx: number, cy: number, r: number, cols: number, rows: number) { + for (let row = Math.floor(cy - r - 1); row <= Math.ceil(cy + r + 1); row++) { + if (row < 0 || row >= rows) continue + for (let col = Math.floor(cx - r - 1); col <= Math.ceil(cx + r + 1); col++) { + if (col < 0 || col >= cols) continue + const d = Math.hypot(col - cx, row - cy) + const a = (1 - clamp01(d - r)) * DOT_ALPHA + if (a > 0.01) cells.push({ col, row, alpha: a }) + } + } +} + +const issuance: DotPattern = ({ t, cols, rows }) => { + const cells: LitCell[] = [] + + const sources: [number, number][] = [] + const slot = cols / ISSUANCE.globes + for (let i = 0; i < ISSUANCE.globes; i++) { + // One globe per horizontal slot (keeps them spread, no clumping) but + // jittered within the slot and free across the full height. + const cx = slot * (i + 0.15 + hash(i * 5 + 1) * 0.7) + const cy = rows * (0.12 + hash(i * 3 + 2) * 0.76) + sources.push([cx, cy]) + } + + // Ripple interference field: sum each source's traveling wave per cell. + for (let row = 0; row < rows; row++) { + for (let col = 0; col < cols; col++) { + let sum = 0 + for (const [gx, gy] of sources) { + const d = Math.hypot(col - gx, row - gy) + const fall = clamp01(1 - d / ISSUANCE.falloff) + if (fall <= 0) continue + sum += Math.cos(d * ISSUANCE.waveFreq - t * ISSUANCE.waveSpeed) * fall + } + if (sum <= ISSUANCE.crestMin) continue + const color = sum > ISSUANCE.greenAt ? GREEN : undefined + const alpha = clamp01((sum - ISSUANCE.crestMin) / 1.3) * DOT_ALPHA + if (alpha > 0.01) cells.push({ col, row, alpha, color }) + } + } + + // Static globe markers on top. + for (const [gx, gy] of sources) { + fillOrb(cells, gx, gy, ISSUANCE.orbRadius, cols, rows) + } + + return cells +} + +// ═══════════════════════════════════════════════════════════════════════════ +// Row 3 · "Two-way lanes" (Send & receive payments) +// Stacked lanes of bold dashes; even lanes send right (white), odd lanes +// receive left (green). Fills the band, bidirectional. +// ═══════════════════════════════════════════════════════════════════════════ + +const PAYMENTS = { + topOffset: 2, // row of the first lane (balances the top/bottom margin) + laneGap: 3, // rows between lanes + dashLen: 4, // dash length (cells) + dashGap: 7, // gap between dashes (cells) + speed: 11, // travel speed (cells/sec) +} + +const payments: DotPattern = ({ t, cols, rows }) => { + const cells: LitCell[] = [] + const period = PAYMENTS.dashLen + PAYMENTS.dashGap + let lane = 0 + for (let row = PAYMENTS.topOffset; row < rows; row += PAYMENTS.laneGap) { + const dir = lane % 2 === 0 ? 1 : -1 // even: send →, odd: receive ← + const color = dir < 0 ? GREEN : undefined + const phase = (((t * PAYMENTS.speed * dir) % period) + period) % period + for (let k = -1; k <= Math.ceil(cols / period); k++) { + const x0 = k * period + phase + for (let c = 0; c < PAYMENTS.dashLen; c++) { + splat(cells, x0 + c, row, DOT_ALPHA, cols, rows, color) + } + } + lane += 1 + } + return cells +} + +// ═══════════════════════════════════════════════════════════════════════════ +// Row 4 · "Encryption sweep" (Enable private payments) +// A full ordered grid; a green band sweeps across, scrambling the dots it +// passes (privacy applied), which settle back to order behind it. +// ═══════════════════════════════════════════════════════════════════════════ + +const PRIVATE = { + speed: 18, // sweep speed (cells/sec) + bandWidth: 8, // width of the scramble band (cells) + base: 0.16, // ordered (clear) opacity + scrambleSpeed: 14, // flicker speed inside the band + lockAlpha: 0.95, // brightness of the padlock glyph + lockMargin: 6, // keep the padlock this many cells from the edges + lockFade: 5, // cells over which the padlock fades in/out near the margin +} + +// Padlock glyph (offsets from top-left): a shackle arch over a body with a +// keyhole. 8 wide × 9 tall — built so it reads clearly at dot scale. +const PADLOCK_W = 8 +const PADLOCK_H = 9 +const PADLOCK: { dc: number; dr: number }[] = (() => { + const cells: { dc: number; dr: number }[] = [] + // Shackle: top bar + two legs reaching down into the body. + for (let c = 2; c <= 5; c++) cells.push({ dc: c, dr: 0 }) + cells.push({ dc: 2, dr: 1 }, { dc: 5, dr: 1 }) + cells.push({ dc: 2, dr: 2 }, { dc: 5, dr: 2 }) + // Body rows 3–8, with a 2×2 keyhole gap in the middle. + for (let r = 3; r <= 8; r++) { + for (let c = 0; c <= 7; c++) { + const keyhole = (c === 3 || c === 4) && (r === 5 || r === 6) + if (!keyhole) cells.push({ dc: c, dr: r }) + } + } + return cells +})() + +const privacy: DotPattern = ({ t, cols, rows }) => { + const cells: LitCell[] = [] + const span = cols + PRIVATE.bandWidth * 2 + const sweep = ((t * PRIVATE.speed) % span) - PRIVATE.bandWidth + const tick = Math.floor(t * PRIVATE.scrambleSpeed) + + // The padlock rides the center of the sweep (snapped to whole cells so it + // stays crisp), but fades out near the edges so its quiet zone never touches + // them. Only when it's on, a 1-cell quiet zone keeps the shape readable. + const lockLeft = Math.round(sweep - PADLOCK_W / 2) + const lockTop = Math.round((rows - PADLOCK_H) / 2) + const lockFade = clamp01((Math.min(sweep, cols - sweep) - PRIVATE.lockMargin) / PRIVATE.lockFade) + const showLock = lockFade > 0.01 + const inQuietZone = (col: number, row: number) => + showLock && + col >= lockLeft - 1 && + col <= lockLeft + PADLOCK_W && + row >= lockTop - 1 && + row <= lockTop + PADLOCK_H + + for (let row = 0; row < rows; row++) { + for (let col = 0; col < cols; col++) { + if (inQuietZone(col, row)) { + cells.push({ col, row, alpha: PRIVATE.base * DOT_ALPHA }) // calm patch + continue + } + const near = clamp01(1 - Math.abs(col - sweep) / PRIVATE.bandWidth) + if (near <= 0) { + cells.push({ col, row, alpha: PRIVATE.base * DOT_ALPHA }) + } else { + // Scrambled: high-frequency per-cell flicker, green at the front. + const n = hash(col * 13 + row * 7 + tick) + const alpha = (0.25 + 0.6 * n) * near + const color = near > 0.5 ? GREEN : undefined + cells.push({ col, row, alpha, color }) + } + } + } + + // Padlock drawn crisply on top of its quiet patch, faded near the edges. + if (showLock) { + for (const { dc, dr } of PADLOCK) { + const col = lockLeft + dc + const row = lockTop + dr + if (col >= 0 && col < cols && row >= 0 && row < rows) { + cells.push({ col, row, alpha: PRIVATE.lockAlpha * lockFade }) + } + } + } + + return cells +} + +// ═══════════════════════════════════════════════════════════════════════════ +// Row 5 · "Swarm" (Accept agentic payments) +// Autonomous agents wander on smooth paths across the band; when two meet, a +// green handshake dot flashes between them. +// ═══════════════════════════════════════════════════════════════════════════ + +const AGENTIC = { + agents: 16, // number of agents + speed: 0.5, // wander speed + meetDist: 4, // distance (cells) that triggers a handshake +} + +const agentic: DotPattern = ({ t, cols, rows }) => { + const cells: LitCell[] = [] + const positions: [number, number][] = [] + + for (let i = 0; i < AGENTIC.agents; i++) { + const fx = 0.5 + hash(i) * 0.8 + const fy = 0.4 + hash(i * 2) * 0.7 + const x = (0.5 + 0.46 * Math.sin(t * AGENTIC.speed * fx + hash(i * 3) * 6.28)) * cols + const y = (0.5 + 0.42 * Math.cos(t * AGENTIC.speed * fy + hash(i * 5) * 6.28)) * rows + positions.push([x, y]) + // Bold agent blob. + splat(cells, x, y, DOT_ALPHA, cols, rows) + splat(cells, x + 1, y, DOT_ALPHA * 0.7, cols, rows) + splat(cells, x, y + 1, DOT_ALPHA * 0.7, cols, rows) + } + + // Handshakes: green dot at the midpoint of any close pair. + for (let i = 0; i < positions.length; i++) { + for (let j = i + 1; j < positions.length; j++) { + const dx = positions[i][0] - positions[j][0] + const dy = positions[i][1] - positions[j][1] + const d = Math.hypot(dx, dy) + if (d >= AGENTIC.meetDist) continue + const mx = (positions[i][0] + positions[j][0]) / 2 + const my = (positions[i][1] + positions[j][1]) / 2 + splat(cells, mx, my, clamp01(1 - d / AGENTIC.meetDist), cols, rows, GREEN) + } + } + return cells +} + +// ═══════════════════════════════════════════════════════════════════════════ +// Exports — one entry per capability row, in order. +// ═══════════════════════════════════════════════════════════════════════════ + +export const buildPatterns: DotPattern[] = [ + assembly, // Create & Use Stablecoin Accounts + issuance, // Issue tokenized assets + payments, // Send & receive payments + privacy, // Enable private payments + agentic, // Accept agentic payments +] + +// Resting state: a sparse field of dots that softly twinkle. +const AMBIENT = { + density: 0.14, // fraction of cells that participate + base: 0.18, // resting opacity + twinkle: 0.12, // opacity swing + speed: 1.5, // twinkle speed +} + +export const buildAmbient: DotPattern = ({ t, cols, rows }) => { + const cells: LitCell[] = [] + for (let row = 0; row < rows; row++) { + for (let col = 0; col < cols; col++) { + if (hash(col * 31 + row * 17) > AMBIENT.density) continue + const phase = hash(col * 7 + row * 53) * Math.PI * 2 + const alpha = AMBIENT.base + AMBIENT.twinkle * Math.sin(t * AMBIENT.speed + phase) + cells.push({ col, row, alpha }) + } + } + return cells +} diff --git a/src/marketing/app/_components/dotPatterns/cost.ts b/src/marketing/app/_components/dotPatterns/cost.ts new file mode 100644 index 00000000..8fe03330 --- /dev/null +++ b/src/marketing/app/_components/dotPatterns/cost.ts @@ -0,0 +1,108 @@ +import { clamp01, GREEN, smoothstep, splat } from './helpers' +import type { DotPattern, LitCell } from './types' + +// Fee comparison across chains, right-anchored: dim white bars (other chains) +// descend left -> right to a tiny green bar (Tempo). The bars rise in with a +// stagger, then a trend line traces down across the bar tops and lands on +// Tempo, which pulses — "fees drop to almost nothing here". Holds, fades, +// loops. Tune the constants below. + +const COST = { + // Relative fee height per chain, tallest -> cheapest. Last entry is Tempo. + bars: [0.92, 0.64, 0.44, 0.28, 0.1], + barCols: 4, // width of each bar + barGap: 2, // gap between bars + rightGap: 5, // gap from the grid's right edge + // Below this width (i.e. mobile) the right gap pushes the chart off the left + // edge behind the metric text, so we drop it to 0 there. + narrowCols: 28, + topPad: 4, // rows kept clear above the tallest bar (the line floats here) + lineLift: 2, // how far the trend line floats above the bar tops (rows) + growSeconds: 0.5, // time for one bar to rise + staggerSeconds: 0.12, // delay between consecutive bars + traceSeconds: 0.9, // time for the trend line to cross the chart + holdSeconds: 1.2, // dwell with the line landed and Tempo pulsing + fadeSeconds: 0.6, + barAlpha: 0.35, // other chains: dim, they're the backdrop + tempoAlpha: 0.9, // Tempo's bar: bright green + lineAlpha: 0.8, // trend line head; the trail is dimmer +} + +const easeOut = (x: number) => 1 - (1 - x) ** 3 + +export const costPattern: DotPattern = ({ t, cols, rows }) => { + const cells: LitCell[] = [] + const n = COST.bars.length + + const span = n * COST.barCols + (n - 1) * COST.barGap + const gap = cols <= COST.narrowCols ? 0 : COST.rightGap + const x0 = cols - 1 - gap - span + 1 + + const growEnd = (n - 1) * COST.staggerSeconds + COST.growSeconds + const traceEnd = growEnd + COST.traceSeconds + const holdEnd = traceEnd + COST.holdSeconds + const cycle = holdEnd + COST.fadeSeconds + const tc = t % cycle + const fade = tc < holdEnd ? 1 : 1 - smoothstep(clamp01((tc - holdEnd) / COST.fadeSeconds)) + + const maxH = rows - COST.topPad + // Tempo pulses once the trend line has landed on it. + const pulse = 0.75 + 0.25 * Math.sin((tc - traceEnd) * 6) + + // Final bar-top anchors (bar centers) the trend line connects. + const tops: { x: number; y: number }[] = [] + + for (let i = 0; i < n; i++) { + const isTempo = i === n - 1 + const target = COST.bars[i] * maxH + const barLeft = x0 + i * (COST.barCols + COST.barGap) + tops.push({ x: barLeft + (COST.barCols - 1) / 2, y: rows - target }) + + const growT = clamp01((tc - i * COST.staggerSeconds) / COST.growSeconds) + const h = target * easeOut(growT) + if (h <= 0) continue + + let alpha = isTempo ? COST.tempoAlpha : COST.barAlpha + if (isTempo && tc > traceEnd) alpha *= pulse + + const top = rows - h + for (let row = Math.floor(top); row < rows; row++) { + if (row < 0) continue + // Soft top edge so the bar rises smoothly instead of snapping per row. + const covY = clamp01(Math.min(row + 1, rows) - Math.max(row, top)) + const a = alpha * fade * covY + if (a <= 0.003) continue + for (let c = 0; c < COST.barCols; c++) { + const col = barLeft + c + if (col < 0 || col >= cols) continue + cells.push({ col, row, alpha: a, color: isTempo ? GREEN : undefined }) + } + } + } + + // Trend line: sweeps across the bar tops, dropping segment by segment, and + // lands on Tempo. Trail stays lit behind the bright head. + const traceT = smoothstep(clamp01((tc - growEnd) / COST.traceSeconds)) + if (traceT > 0) { + const xStart = tops[0].x + const xEnd = tops[n - 1].x + const headX = xStart + (xEnd - xStart) * traceT + + const yAt = (x: number) => { + let i = 0 + while (i < n - 2 && x > tops[i + 1].x) i++ + const f = clamp01((x - tops[i].x) / (tops[i + 1].x - tops[i].x)) + return tops[i].y + (tops[i + 1].y - tops[i].y) * f - COST.lineLift + } + + for (let col = Math.ceil(xStart); col <= Math.floor(headX); col++) { + splat(cells, col, yAt(col), COST.lineAlpha * 0.5 * fade, cols, rows) + } + + const landed = traceT >= 1 + const headAlpha = landed ? COST.lineAlpha * pulse : COST.lineAlpha + splat(cells, headX, yAt(headX), headAlpha * fade, cols, rows, landed ? GREEN : undefined) + } + + return cells +} diff --git a/src/marketing/app/_components/dotPatterns/helpers.ts b/src/marketing/app/_components/dotPatterns/helpers.ts new file mode 100644 index 00000000..75878835 --- /dev/null +++ b/src/marketing/app/_components/dotPatterns/helpers.ts @@ -0,0 +1,45 @@ +import type { LitCell } from './types' + +// Shared toolkit for the dot patterns: brightness/colors, easing, deterministic +// noise, and sub-cell "splat" rendering so motion glides between cells. + +// Brightness of a fully-lit dot (0..1). +export const DOT_ALPHA = 0.7 +// "r, g, b" used for green accents. +export const GREEN = '101, 255, 84' + +export const clamp01 = (x: number) => Math.min(Math.max(x, 0), 1) +export const smoothstep = (x: number) => x * x * (3 - 2 * x) + +// Deterministic pseudo-random in [0, 1). +export const hash = (n: number) => { + const x = Math.sin(n * 127.1) * 43758.5453 + return x - Math.floor(x) +} + +// Draw a dot at a fractional (x, y) by spreading its alpha across the four +// surrounding cells, so motion glides instead of snapping cell-to-cell. +export function splat( + cells: LitCell[], + x: number, + y: number, + alpha: number, + cols: number, + rows: number, + color?: string, +) { + if (alpha <= 0.003) return + const c0 = Math.floor(x) + const r0 = Math.floor(y) + const fx = x - c0 + const fy = y - r0 + const put = (c: number, r: number, a: number) => { + if (a > 0.003 && c >= 0 && c < cols && r >= 0 && r < rows) { + cells.push({ col: c, row: r, alpha: a, color }) + } + } + put(c0, r0, alpha * (1 - fx) * (1 - fy)) + put(c0 + 1, r0, alpha * fx * (1 - fy)) + put(c0, r0 + 1, alpha * (1 - fx) * fy) + put(c0 + 1, r0 + 1, alpha * fx * fy) +} diff --git a/src/marketing/app/_components/dotPatterns/index.ts b/src/marketing/app/_components/dotPatterns/index.ts new file mode 100644 index 00000000..1dd285fc --- /dev/null +++ b/src/marketing/app/_components/dotPatterns/index.ts @@ -0,0 +1,29 @@ +import { arrowPattern } from './arrow' +import { buildAmbient, buildPatterns } from './build' +import { costPattern } from './cost' +import { librariesPatterns } from './libraries' +import { performancePattern } from './performance' +import { reliabilityPattern } from './reliability' +import { speedPattern } from './speed' +import type { DotPattern } from './types' + +export type { DotPattern, LitCell, PatternCtx } from './types' +export { + arrowPattern, + buildAmbient, + buildPatterns, + costPattern, + librariesPatterns, + performancePattern, + reliabilityPattern, + speedPattern, +} + +// Wiring: which pattern each section/tab speaks. Add new patterns here as +// their sections are built. +export const patternByCategory: Record = { + Reliability: reliabilityPattern, + Cost: costPattern, + Speed: speedPattern, + Performance: performancePattern, +} diff --git a/src/marketing/app/_components/dotPatterns/libraries.ts b/src/marketing/app/_components/dotPatterns/libraries.ts new file mode 100644 index 00000000..c11d1c4c --- /dev/null +++ b/src/marketing/app/_components/dotPatterns/libraries.ts @@ -0,0 +1,284 @@ +import { clamp01, DOT_ALPHA, GREEN, hash, smoothstep, splat } from './helpers' +import type { DotPattern, LitCell } from './types' + +/** + * Dot patterns for the Libraries & SDKs section. Hovering an SDK row morphs the + * dot band into that library's pattern; the resting state reuses `buildAmbient`. + * + * Each pattern is a pure function of time + grid size returning lit cells. Tune + * via the `controls` object at the top of each block. + */ + +// ═══════════════════════════════════════════════════════════════════════════ +// Passkeys · "Passkey scan" +// Concentric fingerprint ridges; a horizontal scan line sweeps top→bottom, +// lighting the ridges green as it passes (biometric auth). +// ═══════════════════════════════════════════════════════════════════════════ + +const PASSKEY = { + freq: 0.85, // ridge density + drift: 1.2, // ridges drift outward at this speed + squash: 1.9, // horizontal stretch of the ovals + crest: 0.5, // ridge threshold + scanSeconds: 3, // time for the scan line to cross the band + scanBand: 2.4, // scan line thickness (rows) +} + +const passkey: DotPattern = ({ t, cols, rows }) => { + const cells: LitCell[] = [] + const cx = cols / 2 + const cy = (rows - 1) / 2 + const scanY = ((t / PASSKEY.scanSeconds) % 1) * rows + for (let row = 0; row < rows; row++) { + for (let col = 0; col < cols; col++) { + const dx = (col - cx) / PASSKEY.squash + const dy = row - cy + const d = Math.hypot(dx, dy) + const ridge = Math.cos(d * PASSKEY.freq - t * PASSKEY.drift) + if (ridge < PASSKEY.crest) continue + const base = ((ridge - PASSKEY.crest) / (1 - PASSKEY.crest)) * DOT_ALPHA + const near = clamp01(1 - Math.abs(row - scanY) / PASSKEY.scanBand) + const color = near > 0.35 ? GREEN : undefined + cells.push({ col, row, alpha: color ? Math.max(base, near * DOT_ALPHA) : base, color }) + } + } + return cells +} + +// ═══════════════════════════════════════════════════════════════════════════ +// MPPX · "Request / response" +// Pulsing machine nodes at both edges; white request packets travel right, +// green response packets travel left between them (machine payments over HTTP). +// ═══════════════════════════════════════════════════════════════════════════ + +const MPPX = { + packetLen: 3, // packet length (cells) + gap: 9, // gap between packets + speed: 12, // travel speed (cells/sec) + nodeW: 2, // endpoint node width +} + +const mppx: DotPattern = ({ t, cols, rows }) => { + const cells: LitCell[] = [] + const mid = Math.floor(rows / 2) + const period = MPPX.packetLen + MPPX.gap + const lanes = [ + { row: mid - 2, dir: 1, color: undefined as string | undefined }, + { row: mid + 2, dir: -1, color: GREEN }, + ] + for (const { row, dir, color } of lanes) { + const phase = (((t * MPPX.speed * dir) % period) + period) % period + for (let k = -1; k <= Math.ceil(cols / period); k++) { + const x0 = k * period + phase + for (let c = 0; c < MPPX.packetLen; c++) { + splat(cells, x0 + c, row, DOT_ALPHA, cols, rows, color) + } + } + } + // Endpoint machine nodes at both ends, pulsing on each request. + const pulse = 0.6 + 0.4 * Math.abs(Math.sin(t * 3)) + for (let r = mid - 3; r <= mid + 3; r++) { + for (let c = 0; c < MPPX.nodeW; c++) { + cells.push({ col: c, row: r, alpha: pulse * DOT_ALPHA }) + cells.push({ col: cols - 1 - c, row: r, alpha: pulse * DOT_ALPHA }) + } + } + return cells +} + +// ═══════════════════════════════════════════════════════════════════════════ +// TypeScript SDK · "Order from chaos" +// A field of dots breathes between a scattered cloud and a crisp ordered +// lattice — types bringing structure. At peak order the lattice glows green. +// ═══════════════════════════════════════════════════════════════════════════ + +const TS = { + colStep: 3, // lattice column spacing + rowStep: 2, // lattice row spacing + scatter: 6, // max displacement in the chaotic state (cells) + speed: 0.9, // breathing speed + greenAt: 0.82, // order level (0..1) at which dots turn green +} + +const typescript: DotPattern = ({ t, cols, rows }) => { + const cells: LitCell[] = [] + const order = smoothstep(0.5 + 0.5 * Math.sin(t * TS.speed)) // 0..1 + for (let row = 1; row < rows; row += TS.rowStep) { + for (let col = 1; col < cols; col += TS.colStep) { + const seed = col * 7 + row * 131 + const ang = hash(seed) * Math.PI * 2 + const mag = (0.4 + hash(seed + 1) * 0.6) * TS.scatter * (1 - order) + const alpha = (0.35 + 0.65 * order) * DOT_ALPHA + const color = order > TS.greenAt ? GREEN : undefined + splat(cells, col + Math.cos(ang) * mag, row + Math.sin(ang) * mag, alpha, cols, rows, color) + } + } + return cells +} + +// ═══════════════════════════════════════════════════════════════════════════ +// Go SDK · "Goroutines" +// Independent token streams flow right at varied speeds (concurrency); a green +// bar periodically sweeps across — a channel select syncing them. +// ═══════════════════════════════════════════════════════════════════════════ + +const GO = { + laneGap: 2, // rows between streams + tokenLen: 2, // token length (cells) + spacing: 11, // gap between tokens + baseSpeed: 7, // slowest stream speed + speedVar: 9, // extra speed range + syncSeconds: 3.5, // time for the sync bar to cross +} + +const go: DotPattern = ({ t, cols, rows }) => { + const cells: LitCell[] = [] + let lane = 0 + for (let row = 0; row < rows; row += GO.laneGap) { + const speed = GO.baseSpeed + hash(lane + 1) * GO.speedVar + const phase = (t * speed) % GO.spacing + for (let k = -1; k <= Math.ceil(cols / GO.spacing); k++) { + const x0 = k * GO.spacing + phase + for (let c = 0; c < GO.tokenLen; c++) { + splat(cells, x0 + c, row, DOT_ALPHA, cols, rows) + } + } + lane += 1 + } + const sx = ((t / GO.syncSeconds) % 1) * (cols + 4) - 2 + for (let row = 0; row < rows; row++) { + splat(cells, sx, row, DOT_ALPHA, cols, rows, GREEN) + splat(cells, sx + 1, row, DOT_ALPHA * 0.6, cols, rows, GREEN) + } + return cells +} + +// ═══════════════════════════════════════════════════════════════════════════ +// Python SDK · "Snake" +// A snake of dots winds across the band on a Lissajous path; head is green, +// the tail fades behind it. +// ═══════════════════════════════════════════════════════════════════════════ + +const PY = { + body: 22, // number of segments + lag: 0.06, // time delay between segments + ampX: 0.46, // horizontal swing (fraction of width) + ampY: 0.4, // vertical swing (fraction of height) + freqX: 0.8, + freqY: 1.7, +} + +const python: DotPattern = ({ t, cols, rows }) => { + const cells: LitCell[] = [] + const cx = cols / 2 + const cy = (rows - 1) / 2 + for (let s = 0; s < PY.body; s++) { + const tt = t - s * PY.lag + const x = cx + Math.sin(tt * PY.freqX) * cols * PY.ampX + const y = cy + Math.sin(tt * PY.freqY + 1.3) * rows * PY.ampY + const alpha = DOT_ALPHA * (1 - (s / PY.body) * 0.85) + splat(cells, x, y, alpha, cols, rows, s === 0 ? GREEN : undefined) + } + return cells +} + +// ═══════════════════════════════════════════════════════════════════════════ +// Rust SDK · "Signal" +// Two overlapping sine waves of dots sweep across at high frequency — a fast, +// low-level signal. Wave crests flash green. +// ═══════════════════════════════════════════════════════════════════════════ + +const RUST = { + freq: 0.45, // primary wavelength + speed: 6, // primary travel speed + amp: 0.32, // primary amplitude (fraction of height) + freq2: 0.9, // secondary (harmonic) wavelength + speed2: 9, // secondary travel speed + amp2: 0.16, // secondary amplitude + crest: 0.93, // |sin| above this lights green +} + +const rust: DotPattern = ({ t, cols, rows }) => { + const cells: LitCell[] = [] + const cy = (rows - 1) / 2 + for (let col = 0; col < cols; col++) { + const p1 = col * RUST.freq - t * RUST.speed + const y1 = cy + Math.sin(p1) * rows * RUST.amp + const color = Math.abs(Math.sin(p1)) > RUST.crest ? GREEN : undefined + splat(cells, col, y1, DOT_ALPHA, cols, rows, color) + + const y2 = cy + Math.sin(col * RUST.freq2 - t * RUST.speed2) * rows * RUST.amp2 + splat(cells, col, y2, DOT_ALPHA * 0.4, cols, rows) + } + return cells +} + +// ═══════════════════════════════════════════════════════════════════════════ +// Foundry support · "Forge" +// A hammer rises and falls onto an anvil; each strike throws green sparks that +// arc up and fall back (forging / compiling). +// ═══════════════════════════════════════════════════════════════════════════ + +const FOUNDRY = { + beat: 0.9, // seconds between strikes + sparks: 14, // sparks per strike + sparkLife: 0.5, // how long sparks live (sec) + sparkSpread: 12, // horizontal spark velocity + sparkLift: 10, // upward spark velocity + gravity: 20, // pulls sparks back down +} + +const foundry: DotPattern = ({ t, cols, rows }) => { + const cells: LitCell[] = [] + const cx = Math.floor(cols / 2) + const anvilTop = rows - 4 + + // Anvil: flat top tapering into a base. + const drawRow = (half: number, row: number) => { + for (let c = -half; c <= half; c++) { + cells.push({ col: cx + c, row, alpha: DOT_ALPHA }) + } + } + drawRow(6, anvilTop) + drawRow(4, anvilTop + 1) + drawRow(2, anvilTop + 2) + drawRow(2, anvilTop + 3) + + // Hammer head: descends to the anvil at mid-beat, then recoils. + const phase = (t % FOUNDRY.beat) / FOUNDRY.beat + const drop = phase < 0.5 ? smoothstep(phase / 0.5) : 1 - smoothstep((phase - 0.5) / 0.5) + const headRow = Math.round(drop * (anvilTop - 2)) + for (let c = -2; c <= 2; c++) { + cells.push({ col: cx + c, row: headRow, alpha: DOT_ALPHA }) + cells.push({ col: cx + c, row: Math.max(0, headRow - 1), alpha: DOT_ALPHA }) + } + + // Sparks from the most recent impact (impacts land at mid-beat). + const k = Math.floor(t / FOUNDRY.beat - 0.5) + const age = t - (k + 0.5) * FOUNDRY.beat + if (age >= 0 && age < FOUNDRY.sparkLife) { + const fade = 1 - age / FOUNDRY.sparkLife + for (let i = 0; i < FOUNDRY.sparks; i++) { + const vx = (hash(k * 13 + i) - 0.5) * 2 * FOUNDRY.sparkSpread + const vy = -(0.5 + hash(k * 7 + i) * 0.8) * FOUNDRY.sparkLift + const x = cx + vx * age + const y = anvilTop - 2 + vy * age + FOUNDRY.gravity * age * age + splat(cells, x, y, fade * DOT_ALPHA, cols, rows, GREEN) + } + } + return cells +} + +// ═══════════════════════════════════════════════════════════════════════════ +// Exports — one entry per SDK row, in order. +// ═══════════════════════════════════════════════════════════════════════════ + +export const librariesPatterns: DotPattern[] = [ + passkey, // Passkeys + mppx, // MPPX + typescript, // TypeScript SDK + go, // Go SDK + python, // Python SDK + rust, // Rust SDK + foundry, // Foundry support +] diff --git a/src/marketing/app/_components/dotPatterns/performance.ts b/src/marketing/app/_components/dotPatterns/performance.ts new file mode 100644 index 00000000..179fa931 --- /dev/null +++ b/src/marketing/app/_components/dotPatterns/performance.ts @@ -0,0 +1,68 @@ +import type { DotPattern, LitCell } from './types' + +// A growing bar graph: vertical dot bars rise from the bottom, staggered +// left -> right, hold, fade out, then loop. Reads as throughput "growth". +// Bar heights ascend to the right. Tune the constants below. + +const BAR_COUNT = 6 +const BAR_COLS = 4 // width of each bar +const BAR_GAP = 1 // gap between bars +const RIGHT_GAP_COLS = 5 // gap from the grid's right edge +// Below this width (i.e. mobile) the right gap pushes the bars off the left edge +// behind the metric text, so we drop it to 0 there. +const NARROW_COLS = 28 +const ALPHA = 0.8 + +// Target bar heights as a fraction of the grid height (ascending = growth). +const MIN_HEIGHT = 0.25 +const MAX_HEIGHT = 0.95 + +const GROW_SECONDS = 0.8 // time for one bar to grow to full +const STAGGER_SECONDS = 0.15 // delay between consecutive bars +const HOLD_SECONDS = 0.5 +const FADE_SECONDS = 0.6 + +const easeOut = (x: number) => 1 - (1 - x) ** 3 +const smoothstep = (x: number) => x * x * (3 - 2 * x) +const clamp01 = (x: number) => Math.min(Math.max(x, 0), 1) + +export const performancePattern: DotPattern = ({ t, cols, rows }) => { + const cells: LitCell[] = [] + + const span = BAR_COUNT * BAR_COLS + (BAR_COUNT - 1) * BAR_GAP + const gap = cols <= NARROW_COLS ? 0 : RIGHT_GAP_COLS + const x1 = cols - 1 - gap + const x0 = x1 - span + 1 + + const growEnd = (BAR_COUNT - 1) * STAGGER_SECONDS + GROW_SECONDS + const fadeStart = growEnd + HOLD_SECONDS + const cycle = fadeStart + FADE_SECONDS + const tc = t % cycle + const fade = tc < fadeStart ? 1 : 1 - smoothstep(clamp01((tc - fadeStart) / FADE_SECONDS)) + + for (let i = 0; i < BAR_COUNT; i++) { + const heightFraction = MIN_HEIGHT + (MAX_HEIGHT - MIN_HEIGHT) * (i / (BAR_COUNT - 1)) + const growT = clamp01((tc - i * STAGGER_SECONDS) / GROW_SECONDS) + const h = heightFraction * rows * easeOut(growT) + if (h <= 0) continue + + const top = rows - h // float top of the bar; bottom is the grid floor + const barLeft = x0 + i * (BAR_COLS + BAR_GAP) + + for (let row = Math.floor(top); row < rows; row++) { + if (row < 0) continue + // Soft top edge so the bar grows smoothly instead of snapping per row. + const covY = clamp01(Math.min(row + 1, rows) - Math.max(row, top)) + if (covY <= 0) continue + const alpha = ALPHA * fade * covY + if (alpha <= 0.001) continue + for (let c = 0; c < BAR_COLS; c++) { + const col = barLeft + c + if (col < 0 || col >= cols) continue + cells.push({ col, row, alpha }) + } + } + } + + return cells +} diff --git a/src/marketing/app/_components/dotPatterns/reliability.ts b/src/marketing/app/_components/dotPatterns/reliability.ts new file mode 100644 index 00000000..aff9baa0 --- /dev/null +++ b/src/marketing/app/_components/dotPatterns/reliability.ts @@ -0,0 +1,66 @@ +import type { DotPattern, LitCell } from './types' + +// A block anchored to the bottom-right of the grid. Its bottom SETTLED_FRACTION +// fills green ("settled"); the rest stays dim ("unsettled"). The green fills +// from the bottom up to the settled line, holds, fades out, then re-fills on a +// loop. The rising edge uses a soft per-row alpha so it glides instead of +// stepping. Tune the constants below to paint it. + +const GREEN = '101, 255, 84' +const DIM = '125, 125, 125' +const GREEN_ALPHA = 0.6 +const DIM_ALPHA = 0.12 +// How many cells the block spans, anchored to the bottom-right corner. +const BLOCK_COLS = 30 +const BLOCK_ROWS = 22 +// Fraction of the block height (from the bottom) that ends up green. +const SETTLED_FRACTION = 0.92 +// Looping fill: fill to the settled line over FILL_SECONDS, hold full for +// HOLD_SECONDS, fade out over FADE_SECONDS, then re-fill. +const FILL_SECONDS = 0.7 +const HOLD_SECONDS = 1 +const FADE_SECONDS = 0.6 +const CYCLE_SECONDS = FILL_SECONDS + HOLD_SECONDS + FADE_SECONDS +const easeOut = (x: number) => 1 - (1 - x) ** 3 +const smoothstep = (x: number) => x * x * (3 - 2 * x) + +export const reliabilityPattern: DotPattern = ({ t, cols, rows }) => { + const cells: LitCell[] = [] + + const c1 = cols - 1 + const c0 = Math.max(0, c1 - BLOCK_COLS + 1) + const r1 = rows - 1 + const r0 = Math.max(0, r1 - BLOCK_ROWS + 1) + const blockRows = r1 - r0 + 1 + + const settledRows = blockRows * SETTLED_FRACTION + const phase = t % CYCLE_SECONDS + let progress = 1 + let fade = 1 + if (phase < FILL_SECONDS) { + progress = easeOut(phase / FILL_SECONDS) + } else if (phase >= FILL_SECONDS + HOLD_SECONDS) { + fade = 1 - smoothstep((phase - FILL_SECONDS - HOLD_SECONDS) / FADE_SECONDS) + } + const filledExact = settledRows * progress + + // Dim base for the whole block. + for (let row = r0; row <= r1; row++) { + for (let col = c0; col <= c1; col++) { + cells.push({ col, row, alpha: DIM_ALPHA, color: DIM }) + } + } + + // Green overlay. `cover` (0..1) gives the leading row a partial alpha so the + // rising edge glides; `fade` dissolves the field before the loop resets. + for (let row = r0; row <= r1; row++) { + const cover = Math.min(Math.max(filledExact - (r1 - row), 0), 1) + const alpha = GREEN_ALPHA * cover * fade + if (alpha <= 0.001) continue + for (let col = c0; col <= c1; col++) { + cells.push({ col, row, alpha, color: GREEN }) + } + } + + return cells +} diff --git a/src/marketing/app/_components/dotPatterns/speed.ts b/src/marketing/app/_components/dotPatterns/speed.ts new file mode 100644 index 00000000..4b441a43 --- /dev/null +++ b/src/marketing/app/_components/dotPatterns/speed.ts @@ -0,0 +1,70 @@ +import { clamp01, GREEN, hash, smoothstep, splat } from './helpers' +import type { DotPattern, LitCell } from './types' + +// Transactions streaking through: green light beams enter at the left of a +// right-anchored band, race left -> right with a bright head and a fading +// tail, and dissolve as they exit. Each lane fires on its own jittered clock +// so arrivals feel live — boom, boom-boom — while staying deterministic in +// `t` (loops and morphs cleanly). Later the hash-based scheduler can be +// swapped for real Tempo transactions without touching the rendering. + +const TX = { + bandCols: 30, // width of the activity band (cells), right-anchored + rightGap: 5, // desktop gap between the band and the grid's right edge + // Below this width (i.e. mobile) the right gap pushes the band off the left + // edge behind the metric text, so we drop it to 0 there. + narrowCols: 28, + laneGap: 4, // rows between lane tops + beamRows: 2, // beam height (cells) + tailLen: 9, // tail length behind the head (cells) + travelSeconds: 1, // average time for a beam to cross the band + period: 0.8, // average seconds between beams per lane + fadeIn: 0.18, // fraction of the run spent fading in + fadeOut: 0.25, // fraction of the run spent fading out + alpha: 0.95, // head brightness +} + +export const speedPattern: DotPattern = ({ t, cols, rows }) => { + const cells: LitCell[] = [] + + const gap = cols <= TX.narrowCols ? 0 : TX.rightGap + const bandW = Math.min(TX.bandCols, cols) + const c0 = Math.max(0, cols - gap - bandW) + + let lane = 0 + for (let row = 1; row + TX.beamRows <= rows; row += TX.laneGap) { + // Per-lane cadence jitter keeps lanes from firing in lockstep. + const p = TX.period * (0.7 + hash(lane * 3.7) * 0.6) + const k = Math.floor(t / p) + + // Recent events only — older beams may still be mid-flight, so look back + // far enough to cover the longest travel time. + for (const ki of [k - 2, k - 1, k]) { + const seed = lane * 131 + ki * 17 + const start = ki * p + hash(seed) * p * 0.4 + const travel = TX.travelSeconds * (0.75 + hash(seed + 1) * 0.4) + const progress = (t - start) / travel + if (progress <= 0 || progress >= 1) continue + + // Fade in as the beam enters, out as it exits. + const env = + smoothstep(clamp01(progress / TX.fadeIn)) * smoothstep(clamp01((1 - progress) / TX.fadeOut)) + if (env <= 0.003) continue + + // The head runs past the band's right edge so the tail exits fully. + const headX = c0 + progress * (bandW + TX.tailLen) + + for (let i = 0; i <= TX.tailLen; i++) { + const x = headX - i + if (x < c0 - 0.5) break // keep the tail inside the band + const falloff = (1 - i / (TX.tailLen + 1)) ** 1.6 + for (let r = 0; r < TX.beamRows; r++) { + splat(cells, x, row + r, TX.alpha * env * falloff, cols, rows, GREEN) + } + } + } + lane += 1 + } + + return cells +} diff --git a/src/marketing/app/_components/dotPatterns/types.ts b/src/marketing/app/_components/dotPatterns/types.ts new file mode 100644 index 00000000..5c222918 --- /dev/null +++ b/src/marketing/app/_components/dotPatterns/types.ts @@ -0,0 +1,21 @@ +// The shared "language" every dot pattern speaks. A pattern is a pure function +// of time + grid size that returns which cells to light, with per-cell +// intensity and color. Static shapes ignore `t`; animated ones vary with it. + +export type LitCell = { + col: number + row: number + // 0..1 opacity for this dot. + alpha: number + // "r, g, b" string; defaults to the bright dot color when omitted. + color?: string +} + +export type PatternCtx = { + // Seconds since the pattern was last revealed (enters view / tab switch). + t: number + cols: number + rows: number +} + +export type DotPattern = (ctx: PatternCtx) => LitCell[] diff --git a/src/marketing/app/_components/features.tsx b/src/marketing/app/_components/features.tsx new file mode 100644 index 00000000..f56afb21 --- /dev/null +++ b/src/marketing/app/_components/features.tsx @@ -0,0 +1,292 @@ +import type { ReactNode } from 'react' + +export type FeatureItemData = { + label: string + desc: string + // When present, the row becomes interactive: hovering previews this snippet + // in the opposite half and clicking pins it. + code?: string[] + // Substrings of `code` to emphasize with a boxed container. + highlight?: string[] +} + +export type Feature = { + // Stable feature key used by feature-page routing and section selection. + slug: string + title: string + // Muted by default; wrap keywords in to highlight. + description: ReactNode + items: FeatureItemData[] + // Extra capabilities shown only on the dedicated feature page, where + // there's room to feature the full set. The homepage row stays curated. + extraItems?: FeatureItemData[] + readLabel: string + readHref: string + heroActions?: { + label: string + href: string + primary?: boolean + }[] +} + +const featurePrecedence: Record = { + tokens: 0, + transactions: 1, +} + +export const features: Feature[] = [ + { + slug: 'transactions', + title: 'Tempo Transactions', + description: ( + <> + Tempo Transactions let apps batch,{' '} + sponsor,{' '} + schedule, and{' '} + parallelize payments through a native transaction + type. + + ), + items: [ + { + label: 'Batch calls', + desc: 'Bundle multiple payments into a single transaction', + code: [ + 'import { client } from "./viem.config";', + '', + '// Calls execute atomically in one tx', + 'const receipt = await client.sendTransactionSync({', + ' calls: [', + ' { to: usdc, data: transferData(alice, 10n) },', + ' { to: usdc, data: transferData(bob, 25n) },', + ' ],', + '});', + ], + highlight: [ + '{ to: usdc, data: transferData(alice, 10n) }', + '{ to: usdc, data: transferData(bob, 25n) }', + ], + }, + { + label: 'Fee Sponsorship', + desc: 'Sponsor fees so users do not need to hold gas', + code: [ + 'import { parseUnits } from "viem";', + 'import { client } from "./viem.config";', + '', + '// App pays gas via a fee-payer signature', + 'const { receipt } = await client.token.transferSync({', + ' token: usdc,', + ' to: alice,', + ' amount: parseUnits("10", 6),', + ' feePayer: sponsor,', + '});', + ], + highlight: ['feePayer: sponsor'], + }, + { + label: 'Concurrent Transactions', + desc: 'Run parallel payment flows without nonce bottlenecks', + code: [ + 'import { client } from "./viem.config";', + '', + '// 2D nonce keys separate independent flows', + 'const opts = { token: usdc, nonceKey: 7 };', + '', + 'const [a, b] = await Promise.all([', + ' client.token.transferSync({ ...opts, to: alice, amount }),', + ' client.token.transferSync({ ...opts, to: bob, amount }),', + ']);', + ], + highlight: ['nonceKey: 7'], + }, + { + label: 'Scheduled Transactions', + desc: 'Sign now, execute within a defined time window', + code: [ + 'import { client } from "./viem.config";', + '', + 'const now = Math.floor(Date.now() / 1000);', + '', + '// Valid only inside this time window', + 'const receipt = await client.sendTransactionSync({', + ' to: usdc,', + ' data: transferData(alice, 10n),', + ' validAfter: now,', + ' validBefore: now + 3600,', + '});', + ], + highlight: ['validAfter: now', 'validBefore: now + 3600'], + }, + ], + extraItems: [ + { + label: 'Configurable fee tokens', + desc: 'Pay network fees in any USD stablecoin, auto-converted by the Fee AMM.', + code: [ + 'import { parseUnits } from "viem";', + 'import { client } from "./viem.config";', + '', + '// Pay the network fee in any TIP-20 stablecoin', + 'const { receipt } = await client.token.transferSync({', + ' token: usdc,', + ' to: alice,', + ' amount: parseUnits("100", 6),', + ' feeToken: eurc,', + '});', + ], + highlight: ['feeToken: eurc'], + }, + { + label: 'Access keys', + desc: 'Delegate signing to scoped keys with spending limits and expiry.', + code: [ + 'import { generatePrivateKey } from "viem/accounts";', + 'import { Account, Actions, Expiry } from "viem/tempo";', + 'import { client } from "./viem.config";', + '', + '// Authorize a scoped key to sign transactions', + 'const accessKey = Account.fromP256(generatePrivateKey(), {', + ' access: client.account,', + '});', + '', + 'const auth = await Actions.accessKey.signAuthorization(', + ' client,', + ' { accessKey, expiry: Expiry.days(7) },', + ');', + ], + highlight: ['Actions.accessKey.signAuthorization'], + }, + ], + readLabel: 'Read transaction docs', + readHref: '/docs/protocol/transactions', + heroActions: [ + { + label: 'Accept payments', + href: '/docs/guide/payments/accept-a-payment', + primary: true, + }, + { + label: 'Send payments', + href: '/docs/guide/payments/send-a-payment', + }, + { + label: 'Transaction guide', + href: '/docs/guide/tempo-transaction', + }, + ], + }, + { + slug: 'tokens', + title: 'TIP-20 Tokens', + description: ( + <> + TIP-20 gives stablecoins the primitives needed for payments:{' '} + fees,{' '} + memos,{' '} + lanes,{' '} + policies,{' '} + rewards, and{' '} + issuer controls. + + ), + items: [ + { + label: 'Stablecoin fees', + desc: 'Pay network fees directly in supported stablecoins.', + code: [ + 'import { parseUnits } from "viem";', + 'import { client } from "./viem.config";', + '', + '// Send alphaUSD, pay the fee in betaUSD', + 'const { receipt } = await client.token.transferSync({', + ' token: alphaUsd,', + ' to: alice,', + ' amount: parseUnits("100", 6),', + ' feeToken: betaUsd,', + '});', + ], + highlight: ['feeToken: betaUsd'], + }, + { + label: 'Transfer memos', + desc: 'Attach payment references, invoice IDs, or notes.', + code: [ + 'import { parseUnits, stringToHex, pad } from "viem";', + 'import { client } from "./viem.config";', + '', + '// Attach a 32-byte invoice reference', + 'const memo = pad(stringToHex("INV-12345"), { size: 32 });', + '', + 'const { receipt } = await client.token.transferSync({', + ' token: usdc,', + ' to: alice,', + ' amount: parseUnits("100", 6),', + ' memo,', + '});', + ], + highlight: ['pad(stringToHex("INV-12345"), { size: 32 })', 'memo,'], + }, + { + label: 'Payment lanes', + desc: 'Reserve blockspace for predictable payment execution.', + code: [ + 'import { parseUnits } from "viem";', + 'import { client } from "./viem.config";', + '', + '// TIP-20 transfers use the reserved payment', + '// lane automatically — predictable inclusion', + 'const { receipt } = await client.token.transferSync({', + ' token: usdc,', + ' to: alice,', + ' amount: parseUnits("100", 6),', + '});', + ], + highlight: ['client.token.transferSync'], + }, + { + label: 'Issuer controls', + desc: 'Mint, burn, pause, cap supply, and manage roles.', + code: [ + 'import { parseUnits } from "viem";', + 'import { client } from "./viem.config";', + '', + '// Issuer-only supply & emergency controls', + 'await client.token.mintSync({', + ' token: usdc,', + ' to: treasury,', + ' amount: parseUnits("1000000", 6),', + '});', + 'await client.token.setSupplyCapSync({ token: usdc, supplyCap });', + 'await client.token.pauseSync({ token: usdc });', + ], + highlight: [ + 'client.token.mintSync', + 'client.token.setSupplyCapSync', + 'client.token.pauseSync', + ], + }, + ], + readLabel: 'Read TIP-20 docs', + readHref: '/docs/protocol/tip20/overview', + heroActions: [ + { + label: 'Create a stablecoin', + href: '/docs/guide/issuance/create-a-stablecoin', + primary: true, + }, + { + label: 'Issuance guide', + href: '/docs/guide/issuance', + }, + { + label: 'TIP-20 spec', + href: '/docs/protocol/tip20/spec', + }, + ], + }, +].sort( + (a, b) => + (featurePrecedence[a.slug] ?? Number.MAX_SAFE_INTEGER) - + (featurePrecedence[b.slug] ?? Number.MAX_SAFE_INTEGER), +) diff --git a/src/marketing/app/_components/heroPattern.ts b/src/marketing/app/_components/heroPattern.ts new file mode 100644 index 00000000..73e3dfd7 --- /dev/null +++ b/src/marketing/app/_components/heroPattern.ts @@ -0,0 +1,130 @@ +import { clamp01, hash, smoothstep, splat } from './dotPatterns/helpers' +import type { DotPattern, LitCell } from './dotPatterns/types' +import { PALETTE } from './palette' + +// Hero ambience: the perf panel's "speed" payment beams, adapted to run along +// the bottom band of the hero grid. Bars enter at the left, race across +// the full width with a bright head and fading tail, and dissolve as they +// exit. Same jittered-clock scheduler as the v2 speed pattern, so arrivals +// feel live while staying deterministic in `t`. + +// Each beam keeps one palette color for its whole run; which color is hashed +// from the beam's seed, so the mix reshuffles while staying deterministic. +const hexToRgb = (hex: string) => [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16)).join(', ') +const BEAM_COLORS = PALETTE.map(hexToRgb) + +const TX = { + bandRows: 14, // height of the activity band above the grid's bottom edge + laneGap: 4, // rows between lane tops + beamRows: 2, // beam height (cells) + tailLen: 9, // tail length behind the head (cells) + travelSeconds: 2, // average time for a beam to cross the grid + period: 1.1, // average seconds between beams per lane + fadeIn: 0.18, // fraction of the run spent fading in + fadeOut: 0.25, // fraction of the run spent fading out + alpha: 0.95, // head brightness +} + +// Resting field behind the beams. Kept barely above the base grid so it reads +// as ambient texture, not a second pattern. Each cell breathes on its own slow +// clock — drifting in and out of visibility — so the lit set never settles and +// continuously reshuffles. +const AMBIENT = { + density: 0.16, // fraction of cells that ever light + floor: 0.02, // faint resting opacity (≈ base grid) + peak: 0.2, // brightest a dot gets at its crest + speed: 0.75, // breathing speed (lower = slower drift) +} + +export const heroAmbientPattern: DotPattern = ({ t, cols, rows }) => { + const cells: LitCell[] = [] + for (let row = 0; row < rows; row++) { + for (let col = 0; col < cols; col++) { + if (hash(col * 31 + row * 17) > AMBIENT.density) continue + // Per-cell phase + a slight per-cell frequency jitter so cells fall out + // of sync and the pattern keeps randomizing instead of pulsing together. + const phase = hash(col * 7 + row * 53) * Math.PI * 2 + const freq = AMBIENT.speed * (0.6 + hash(col * 5 + row * 3) * 0.8) + // Sharpened sine: dots sit dark most of the cycle and gently swell up. + const swell = (Math.sin(t * freq + phase) * 0.5 + 0.5) ** 2 + const alpha = AMBIENT.floor + (AMBIENT.peak - AMBIENT.floor) * swell + cells.push({ col, row, alpha }) + } + } + return cells +} + +// Same breathing field as heroAmbientPattern, but each lit point renders as a +// "+" (center plus its four orthogonal neighbours) instead of a single dot. The +// arms sit a touch dimmer than the center so the cross reads as a shape rather +// than five equal dots. +const PLUS_ARM = 0.55 // arm brightness relative to the center + +export const heroAmbientPlusPattern: DotPattern = ({ t, cols, rows }) => { + const cells: LitCell[] = [] + const put = (col: number, row: number, alpha: number) => { + if (alpha <= 0.003 || col < 0 || col >= cols || row < 0 || row >= rows) { + return + } + cells.push({ col, row, alpha }) + } + for (let row = 0; row < rows; row++) { + for (let col = 0; col < cols; col++) { + if (hash(col * 31 + row * 17) > AMBIENT.density) continue + const phase = hash(col * 7 + row * 53) * Math.PI * 2 + const freq = AMBIENT.speed * (0.6 + hash(col * 5 + row * 3) * 0.8) + const swell = (Math.sin(t * freq + phase) * 0.5 + 0.5) ** 2 + const alpha = AMBIENT.floor + (AMBIENT.peak - AMBIENT.floor) * swell + put(col, row, alpha) + put(col - 1, row, alpha * PLUS_ARM) + put(col + 1, row, alpha * PLUS_ARM) + put(col, row - 1, alpha * PLUS_ARM) + put(col, row + 1, alpha * PLUS_ARM) + } + } + return cells +} + +export const heroBeamsPattern: DotPattern = ({ t, cols, rows }) => { + const cells: LitCell[] = [] + + const rowStart = Math.max(1, rows - TX.bandRows) + + let lane = 0 + for (let row = rowStart; row + TX.beamRows <= rows; row += TX.laneGap) { + // Per-lane cadence jitter keeps lanes from firing in lockstep. + const p = TX.period * (0.7 + hash(lane * 3.7) * 0.6) + const k = Math.floor(t / p) + + // Recent events only — older beams may still be mid-flight, so look back + // far enough to cover the longest travel time. + for (const ki of [k - 2, k - 1, k]) { + const seed = lane * 131 + ki * 17 + const start = ki * p + hash(seed) * p * 0.4 + const travel = TX.travelSeconds * (0.75 + hash(seed + 1) * 0.4) + const color = BEAM_COLORS[Math.floor(hash(seed + 2) * BEAM_COLORS.length)] + const progress = (t - start) / travel + if (progress <= 0 || progress >= 1) continue + + // Fade in as the beam enters, out as it exits. + const env = + smoothstep(clamp01(progress / TX.fadeIn)) * smoothstep(clamp01((1 - progress) / TX.fadeOut)) + if (env <= 0.003) continue + + // The head runs past the right edge so the tail exits fully. + const headX = progress * (cols + TX.tailLen) + + for (let i = 0; i <= TX.tailLen; i++) { + const x = headX - i + if (x < -0.5) break + const falloff = (1 - i / (TX.tailLen + 1)) ** 1.6 + for (let r = 0; r < TX.beamRows; r++) { + splat(cells, x, row + r, TX.alpha * env * falloff, cols, rows, color) + } + } + } + lane += 1 + } + + return cells +} diff --git a/src/marketing/app/_components/menuIcons.tsx b/src/marketing/app/_components/menuIcons.tsx new file mode 100644 index 00000000..91ed3766 --- /dev/null +++ b/src/marketing/app/_components/menuIcons.tsx @@ -0,0 +1,132 @@ +// biome-ignore-all lint/a11y/noSvgWithoutTitle: Mega-menu glyphs are decorative and paired with visible menu labels. + +import type { ReactNode } from 'react' + +// Shared stroke style for the mega-menu glyphs. 24px viewbox to match +// ArrowUpRight; rendered at 18px inside the menu's icon tiles. +function Glyph({ children }: { children: ReactNode }) { + return ( + + {children} + + ) +} + +export function PoliciesIcon() { + return ( + + + + + ) +} + +export function TransactionsIcon() { + return ( + + + + + ) +} + +export function AgenticIcon() { + return ( + + + + ) +} + +export function TokensIcon() { + return ( + + + + + ) +} + +export function DocsIcon() { + return ( + + + + + ) +} + +export function QuickstartIcon() { + return ( + + + + ) +} + +export function WalletIcon() { + return ( + + + + + + ) +} + +export function ApiIcon() { + return ( + + + + + ) +} + +export function ExplorerIcon() { + return ( + + + + + + ) +} + +export function SdkIcon() { + return ( + + + + + ) +} + +export function McpIcon() { + return ( + + + + + + ) +} + +export function TerminalIcon() { + return ( + + + + + ) +} diff --git a/src/marketing/app/_components/palette.ts b/src/marketing/app/_components/palette.ts new file mode 100644 index 00000000..d86a4dc7 --- /dev/null +++ b/src/marketing/app/_components/palette.ts @@ -0,0 +1,10 @@ +// Agency palette (Figma node 60:132). Single source for the feature hover/select +// squares. +export const PALETTE = ['#d487f3', '#5d88ff', '#58b88a', '#cde769'] as const + +export type PaletteColor = (typeof PALETTE)[number] + +// A stable palette color for a given list index — each item keeps its own color +// for the life of the page instead of re-rolling on every hover. Cycles through +// the palette so adjacent items never share a color. +export const colorForIndex = (i: number): PaletteColor => PALETTE[i % PALETTE.length] diff --git a/src/marketing/app/_components/panelFade.ts b/src/marketing/app/_components/panelFade.ts new file mode 100644 index 00000000..fc541073 --- /dev/null +++ b/src/marketing/app/_components/panelFade.ts @@ -0,0 +1,4 @@ +// Stacked showcase panels share a grid cell. Hide inactive panels without +// animating so tab and Visual/Code switches feel immediate. +export const panelFadeClass = (visible: boolean) => + `self-center [grid-area:1/1] ${visible ? '' : 'hidden'}` diff --git a/src/marketing/app/_components/stats.ts b/src/marketing/app/_components/stats.ts new file mode 100644 index 00000000..1d162cbe --- /dev/null +++ b/src/marketing/app/_components/stats.ts @@ -0,0 +1,150 @@ +// Single source of truth for the four benchmark stats, shared by the +// PerformancePanel (tabs/metric) and the Hero's rotating StatTicker. +// `stats` holds the static copy and fallback values; `fetchStats()` overlays +// live metrics from the perf API's latest nightly run. + +export type Stat = { + category: string + title: string + // Footer caption shown under the panel when this stat is active. + caption: string + // Compact form shown in the panel's top-left box when another tab is active. + small: { label: string; value: string } + // Headline form shown as the panel's big metric when this tab is active. + main: { label: string; value: string; unit?: string } + // Muted suffix used in the Hero ticker after the bright value (e.g. "1.25 Ggas/s peak execution"). + tickerLabel: string +} + +export const stats: Stat[] = [ + { + category: 'Speed', + title: '< 500ms transaction time', + caption: 'Tempo sustained sub-second block times under continuous benchmark load', + small: { label: 'BLOCK TIME', value: '508ms' }, + main: { label: 'AVG BLOCK TIME', value: '508', unit: 'ms' }, + tickerLabel: 'average block time', + }, + { + category: 'Cost', + title: '0.001 USD base fee', + caption: 'Built for payment volumes where every fraction of a cent matters.', + small: { label: 'COST', value: '0.001 USD base fee' }, + main: { label: 'COST TO SPONSOR 10M USERS', value: '$1.8M' }, + tickerLabel: 'to sponsor 10M users', + }, + { + category: 'Performance', + title: '20k+ TPS', + caption: "Tempo's highest observed execution rate under peak benchmark load", + small: { label: 'PEAK TPS', value: '21,200' }, + main: { label: 'PEAK PERFORMANCE CAPACITY', value: '1.25', unit: 'Ggas/s' }, + tickerLabel: 'peak execution rate', + }, + { + category: 'Reliability', + title: '99.999 uptime', + caption: + 'Tempo settled ~92% of submitted benchmark load while sustaining high-volume throughput.', + small: { label: 'SETTLED TPS', value: '17,311' }, + main: { label: 'SETTLED TPS', value: '21,200', unit: 'TPS' }, + tickerLabel: 'settled at peak load', + }, +] + +// Bright value + unit as one string, e.g. "1.25 Ggas/s". +export function statValue(stat: Stat): string { + return stat.main.unit ? `${stat.main.value} ${stat.main.unit}` : stat.main.value +} + +// Nightly TIP-20 benchmark runs. PR-preview deployment for now — swap for the +// production URL when it lands. +const PERF_API_URL = + 'https://pr-77-tempo-apps-internal-perf-public.tempo-dev.workers.dev/api/perf/runs?feed=nightly&limit=1&scenario_id=tip20-50k' + +type PerfRuns = { + runs?: { + finishedAt?: string + metrics?: { + settledTps: number + avgGasPerSecond: number + peakGasPerSecond: number + avgBlockTimeMs: number + } + }[] +} + +export type PerfData = { + stats: Stat[] + // When the latest benchmark run finished, preformatted for display + // (e.g. "JUN 9, 03:28 UTC"); null when serving fallback values. + updatedAt: string | null +} + +// Fixed locale + UTC so the server-rendered string never depends on where it +// renders (avoids hydration mismatches). +const formatRunDate = (iso: string) => + `${new Date(iso) + .toLocaleString('en-US', { + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + hour12: false, + timeZone: 'UTC', + }) + .toUpperCase()} UTC` + +// Server-side only (call from a server component). Returns the static stats +// with live metrics overlaid where the API has them; Cost and all copy +// (titles, captions) stay static. Falls back to the static values wholesale +// if the API is down or the shape changes. +export async function fetchStats(): Promise { + const fallback: PerfData = { stats, updatedAt: null } + try { + const res = await fetch(PERF_API_URL) + if (!res.ok) return fallback + const data = (await res.json()) as PerfRuns + const run = data.runs?.[0] + const m = run?.metrics + if (!m) return fallback + + const blockMs = `${Math.round(m.avgBlockTimeMs)}` + const settled = Math.round(m.settledTps).toLocaleString('en-US') + const peakGgas = (m.peakGasPerSecond / 1e9).toFixed(2) + const avgGgas = (m.avgGasPerSecond / 1e9).toFixed(2) + + const live = stats.map((stat) => { + switch (stat.category) { + case 'Speed': + return { + ...stat, + small: { ...stat.small, value: `${blockMs}ms` }, + main: { ...stat.main, value: blockMs }, + } + case 'Performance': + // The API has no peak-TPS metric, so the compact stat shows the + // average execution rate alongside the headline peak. + return { + ...stat, + small: { label: 'AVG EXECUTION', value: `${avgGgas} Ggas/s` }, + main: { ...stat.main, value: peakGgas }, + } + case 'Reliability': + return { + ...stat, + small: { ...stat.small, value: settled }, + main: { ...stat.main, value: settled }, + } + default: + return stat + } + }) + return { + stats: live, + updatedAt: run.finishedAt ? formatRunDate(run.finishedAt) : null, + } + } catch { + return fallback + } +} diff --git a/src/marketing/app/_components/transactionCodeVariants.ts b/src/marketing/app/_components/transactionCodeVariants.ts new file mode 100644 index 00000000..c01a17e2 --- /dev/null +++ b/src/marketing/app/_components/transactionCodeVariants.ts @@ -0,0 +1,449 @@ +import type { CodeVariant } from './CodeWindow' + +export const feeTokenCodeVariants: CodeVariant[] = [ + { + lang: 'TypeScript', + code: [ + 'import { parseUnits } from "viem";', + 'import { client } from "./viem.config";', + '', + "const alphaUsd = '0x20c0000000000000000000000000000000000001';", + "const betaUsd = '0x20c0000000000000000000000000000000000002';", + '', + 'const receipt = await client.token.transferSync({', + " amount: parseUnits('100', 6),", + ' feeToken: betaUsd,', + " to: '0x0000000000000000000000000000000000000000',", + ' token: alphaUsd,', + '});', + ], + highlight: ['feeToken: betaUsd'], + }, + { + lang: 'Rust', + code: [ + 'use tempo_alloy::rpc::TempoTransactionRequest;', + '', + 'let pending = provider', + ' .send_transaction(TempoTransactionRequest {', + ' calls,', + ' fee_token: Some(beta_usd),', + ' ..Default::default()', + ' })', + ' .await?;', + ], + highlight: ['fee_token: Some(beta_usd)'], + }, + { + lang: 'CLI', + code: [ + 'cast erc20 transfer \\', + ' 0x20c0000000000000000000000000000000000001 \\', + ' 0x0000000000000000000000000000000000000000 \\', + ' 100000000 \\', + ' --tempo.fee-token 0x20c0000000000000000000000000000000000002 \\', + ' --rpc-url "$TEMPO_RPC_URL" \\', + ' --private-key "$PRIVATE_KEY"', + ], + highlight: ['--tempo.fee-token'], + }, +] + +export const feeSponsorCodeVariants: CodeVariant[] = [ + { + lang: 'TypeScript', + code: [ + 'import { parseUnits } from "viem";', + 'import { privateKeyToAccount } from "viem/accounts";', + 'import { client } from "./viem.config";', + '', + "const sender = privateKeyToAccount('0x...');", + "const sponsor = privateKeyToAccount('0x...');", + '', + 'const { receipt } = await client.token.transferSync({', + ' account: sender,', + " amount: parseUnits('10.5', 6),", + ' feePayer: sponsor,', + " to: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb',", + " token: '0x20c0000000000000000000000000000000000000',", + '});', + ], + highlight: ['feePayer: sponsor'], + }, + { + lang: 'Rust', + code: [ + 'use alloy::signers::{local::PrivateKeySigner, SignerSync};', + '', + 'let request = TempoTransactionRequest {', + ' calls,', + ' ..Default::default()', + '};', + '', + 'let mut tx = provider.fill(request).await?.build_aa()?;', + 'let sender = provider.default_signer_address();', + 'let hash = tx.fee_payer_signature_hash(sender);', + '', + 'let fee_payer: PrivateKeySigner = "0x...".parse()?;', + 'tx.fee_payer_signature = Some(fee_payer.sign_hash_sync(&hash)?);', + 'provider.send_transaction(tx).await?;', + ], + highlight: ['fee_payer_signature'], + }, + { + lang: 'CLI', + code: [ + 'FEE_PAYER_HASH=$(cast mktx $TOKEN \\', + ' "transfer(address,uint256)" $USER 10500000 \\', + ' --rpc-url "$TEMPO_RPC_URL" \\', + ' --private-key "$SENDER_KEY" \\', + ' --tempo.print-sponsor-hash)', + '', + 'SPONSOR_SIG=$(cast wallet sign --private-key "$SPONSOR_KEY" "$FEE_PAYER_HASH" --no-hash)', + '', + 'cast send $TOKEN \\', + ' "transfer(address,uint256)" $USER 10500000 \\', + ' --rpc-url "$TEMPO_RPC_URL" \\', + ' --private-key "$SENDER_KEY" \\', + ' --tempo.sponsor-signature "$SPONSOR_SIG"', + ], + highlight: ['--tempo.print-sponsor-hash', '--tempo.sponsor-signature'], + }, +] + +export const paymentLaneCodeVariants: CodeVariant[] = [ + { + lang: 'TypeScript', + code: [ + 'import { parseUnits } from "viem";', + 'import { client } from "./viem.config";', + '', + "const alphaUsd = '0x20c0000000000000000000000000000000000001';", + '', + 'const { receipt } = await client.token.transferSync({', + " amount: parseUnits('100', 6),", + " to: '0x0000000000000000000000000000000000000000',", + ' token: alphaUsd,', + '});', + '', + '// TIP-20 payments use the payment lane automatically.', + ], + highlight: ['client.token.transferSync'], + }, + { + lang: 'Rust', + code: [ + 'let call_data = ITIP20::transferCall {', + ' to: recipient,', + ' amount: U256::from(100_000_000),', + '}', + '.abi_encode();', + '', + 'let tx = TempoTransactionRequest::default()', + ' .with_to(alpha_usd)', + ' .with_input(call_data.into());', + '', + 'provider.send_transaction(tx).await?;', + ], + highlight: ['ITIP20::transferCall'], + }, + { + lang: 'CLI', + code: [ + 'cast erc20 transfer \\', + ' 0x20c0000000000000000000000000000000000001 \\', + ' 0x0000000000000000000000000000000000000000 \\', + ' 100000000 \\', + ' --rpc-url "$TEMPO_RPC_URL" \\', + ' --private-key "$PRIVATE_KEY"', + ], + highlight: ['transfer(address,uint256)'], + }, +] + +export const batchingCodeVariants: CodeVariant[] = [ + { + lang: 'TypeScript', + code: [ + 'import { client } from "./viem.config";', + '', + 'const { address, hash, id } = await client.sendTransaction({', + ' calls: [', + ' { to: tokenAddress, data: transferCalldata },', + ' { to: anotherTokenAddress, data: approveCalldata },', + ' { to: dexAddress, data: swapCalldata },', + ' ],', + '});', + ], + highlight: ['calls:'], + }, + { + lang: 'Rust', + code: [ + 'let tx = TempoTransactionRequest {', + ' calls: vec![transfer_call, approve_call, swap_call],', + ' ..Default::default()', + '};', + '', + 'let receipt = provider', + ' .send_transaction(tx)', + ' .await?', + ' .get_receipt()', + ' .await?;', + ], + highlight: ['calls: vec!'], + }, + { + lang: 'CLI', + code: [ + 'cast batch-send \\', + ' --call "$TOKEN::transfer(address,uint256):$ALICE,100000000" \\', + ' --call "$TOKEN::transfer(address,uint256):$BOB,50000000" \\', + ' --rpc-url "$TEMPO_RPC_URL" \\', + ' --private-key "$PRIVATE_KEY"', + ], + highlight: ['cast batch-send'], + }, +] + +export const parallelCodeVariants: CodeVariant[] = [ + { + lang: 'TypeScript', + code: [ + 'import { parseUnits } from "viem";', + 'import { client } from "./viem.config";', + '', + "const alphaUsd = '0x20c0000000000000000000000000000000000001';", + '', + 'const [receipt1, receipt2] = await Promise.all([', + ' client.token.transferSync({', + " amount: parseUnits('100', 6),", + ' nonceKey: 7,', + " to: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8',", + ' token: alphaUsd,', + ' }),', + ' client.token.transferSync({', + " amount: parseUnits('50', 6),", + ' nonceKey: 8,', + " to: '0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC',", + ' token: alphaUsd,', + ' }),', + ']);', + ], + highlight: ['Promise.all', 'nonceKey: 7', 'nonceKey: 8'], + }, + { + lang: 'Rust', + code: [ + 'let tx_a = TempoTransactionRequest {', + ' calls: vec![payment_a],', + ' nonce_key: U256::from(7),', + ' ..Default::default()', + '};', + 'let tx_b = TempoTransactionRequest {', + ' calls: vec![payment_b],', + ' nonce_key: U256::from(8),', + ' ..Default::default()', + '};', + '', + 'let (a, b) = tokio::try_join!(', + ' provider.send_transaction(tx_a),', + ' provider.send_transaction(tx_b),', + ')?;', + ], + highlight: ['nonce_key: U256::from(7)', 'nonce_key: U256::from(8)', 'try_join!'], + }, + { + lang: 'CLI', + code: [ + 'cast erc20 transfer "$TOKEN" "$ALICE" 100000000 \\', + ' --rpc-url "$TEMPO_RPC_URL" \\', + ' --private-key "$PRIVATE_KEY" \\', + ' --nonce 0 --tempo.nonce-key 7 &', + 'cast erc20 transfer "$TOKEN" "$BOB" 50000000 \\', + ' --rpc-url "$TEMPO_RPC_URL" \\', + ' --private-key "$PRIVATE_KEY" \\', + ' --nonce 0 --tempo.nonce-key 8 &', + 'wait', + ], + highlight: ['--tempo.nonce-key 7', '--tempo.nonce-key 8'], + }, +] + +export const schedulingCodeVariants: CodeVariant[] = [ + { + lang: 'TypeScript', + code: [ + 'import { client } from "./viem.config";', + '', + 'const now = Math.floor(Date.now() / 1000);', + '', + 'const receipt = await client.sendTransactionSync({', + ' to: tokenAddress,', + ' data: transferCalldata,', + ' validAfter: now,', + ' validBefore: now + 86_400,', + '});', + ], + highlight: ['validAfter: now', 'validBefore: now + 86_400'], + }, + { + lang: 'Rust', + code: [ + 'let now = SystemTime::now()', + ' .duration_since(UNIX_EPOCH)?', + ' .as_secs();', + '', + 'let tx = TempoTransactionRequest {', + ' calls,', + ' valid_after: Some(now),', + ' valid_before: Some(now + 86_400),', + ' ..Default::default()', + '};', + '', + 'provider.send_transaction(tx).await?;', + ], + highlight: ['valid_after: Some(now)', 'valid_before: Some(now + 86_400)'], + }, + { + lang: 'CLI', + code: [ + 'VALID_AFTER=$(($(date +%s) + 5))', + 'VALID_BEFORE=$(($(date +%s) + 25))', + '', + 'cast erc20 transfer "$TOKEN" "$RECIPIENT" 100000000 \\', + ' --rpc-url "$TEMPO_RPC_URL" \\', + ' --private-key "$PRIVATE_KEY" \\', + ' --tempo.expiring-nonce \\', + ' --tempo.valid-after "$VALID_AFTER" \\', + ' --tempo.valid-before "$VALID_BEFORE"', + ], + highlight: ['--tempo.expiring-nonce', '--tempo.valid-after', '--tempo.valid-before'], + }, +] + +export const accessKeyCodeVariants: CodeVariant[] = [ + { + lang: 'TypeScript', + code: [ + 'import { generatePrivateKey } from "viem/accounts";', + 'import { Account, Actions, Expiry } from "viem/tempo";', + 'import { client } from "./viem.config";', + '', + '// Create a scoped P256 access key.', + 'const accessKey = Account.fromP256(generatePrivateKey(), {', + ' access: client.account,', + '});', + '', + '// Authorize it for one week.', + 'const authorization = await Actions.accessKey.signAuthorization(', + ' client,', + ' { accessKey, expiry: Expiry.days(7) },', + ');', + '', + 'await client.account.authorizeAccessKey(authorization);', + ], + highlight: [ + 'const accessKey = Account.fromP256(generatePrivateKey(), {', + '{ accessKey, expiry: Expiry.days(7) }', + 'client.account.authorizeAccessKey(authorization)', + ], + }, + { + lang: 'Rust', + code: [ + 'use alloy::primitives::{Address, U256};', + 'use tempo_alloy::contracts::precompiles::{', + ' IAccountKeychain, TokenLimit, ACCOUNT_KEYCHAIN,', + '};', + '', + 'let keychain = IAccountKeychain::new(ACCOUNT_KEYCHAIN, provider.clone());', + 'let expiry = now + 7 * 24 * 60 * 60;', + '', + 'keychain', + ' .authorizeKey(', + ' access_key,', + ' 1u8,', + ' expiry,', + ' true,', + ' vec![TokenLimit {', + ' token: alpha_usd,', + ' amount: U256::from(500_000_000),', + ' }],', + ' )', + ' .send()', + ' .await?;', + ], + highlight: [ + 'IAccountKeychain::new(ACCOUNT_KEYCHAIN, provider.clone())', + '.authorizeKey(', + 'TokenLimit {', + 'amount: U256::from(500_000_000)', + ], + }, + { + lang: 'CLI', + code: [ + 'EXPIRY=$(($(date +%s) + 604800))', + '', + 'cast send $ACCOUNT_KEYCHAIN \\', + ' "authorizeKey(address,uint8,uint64,bool,(address,uint256)[])" \\', + ' "$ACCESS_KEY" \\', + ' 1 \\', + ' "$EXPIRY" \\', + ' true \\', + ' "[($TOKEN,500000000)]" \\', + ' --rpc-url "$TEMPO_RPC_URL" \\', + ' --private-key "$ROOT_KEY"', + ], + highlight: [ + 'authorizeKey(address,uint8,uint64,bool,(address,uint256)[])', + '"$ACCESS_KEY"', + '"$EXPIRY"', + '"[($TOKEN,500000000)]"', + ], + }, +] + +export const nonceKeyCodeVariants: CodeVariant[] = [ + { + lang: 'TypeScript', + code: [ + 'import { parseUnits } from "viem";', + 'import { client } from "./viem.config";', + '', + "const alphaUsd = '0x20c0000000000000000000000000000000000001';", + '', + 'await client.token.transferSync({', + " amount: parseUnits('100', 6),", + ' nonceKey: 9,', + " to: '0x0000000000000000000000000000000000000000',", + ' token: alphaUsd,', + '});', + ], + highlight: ['nonceKey: 9'], + }, + { + lang: 'Rust', + code: [ + 'let tx = TempoTransactionRequest {', + ' calls: vec![payment_call],', + ' nonce_key: U256::from(9),', + ' ..Default::default()', + '};', + '', + 'provider.send_transaction(tx).await?;', + ], + highlight: ['nonce_key: U256::from(9)'], + }, + { + lang: 'CLI', + code: [ + 'cast erc20 transfer "$TOKEN" "$RECIPIENT" 100000000 \\', + ' --rpc-url "$TEMPO_RPC_URL" \\', + ' --private-key "$PRIVATE_KEY" \\', + ' --nonce 0 --tempo.nonce-key 9', + ], + highlight: ['--tempo.nonce-key 9'], + }, +] diff --git a/src/marketing/app/_lib/featurePaths.ts b/src/marketing/app/_lib/featurePaths.ts new file mode 100644 index 00000000..b21d2a36 --- /dev/null +++ b/src/marketing/app/_lib/featurePaths.ts @@ -0,0 +1,8 @@ +const featurePaths: Record = { + transactions: '/build/tempo-transactions', + tokens: '/build/tip20-tokens', +} + +export function featurePath(slug: string): string { + return featurePaths[slug] ?? '/build' +} diff --git a/src/marketing/app/_lib/links.ts b/src/marketing/app/_lib/links.ts new file mode 100644 index 00000000..877c667b --- /dev/null +++ b/src/marketing/app/_lib/links.ts @@ -0,0 +1 @@ +export const TEMPO_SDK_DOCS_URL = '/docs/sdk' diff --git a/src/marketing/app/diagrams/_components/FeatureDiagram.tsx b/src/marketing/app/diagrams/_components/FeatureDiagram.tsx new file mode 100644 index 00000000..d3d82787 --- /dev/null +++ b/src/marketing/app/diagrams/_components/FeatureDiagram.tsx @@ -0,0 +1,1765 @@ +// biome-ignore-all lint/a11y/noSvgWithoutTitle: Feature diagrams are labelled by surrounding page content and exported visually. +// biome-ignore-all lint/suspicious/noArrayIndexKey: Static SVG diagram geometry uses positional keys for repeated primitives. + +import { PALETTE } from '../../_components/palette' +import { + type BatchSpec, + type BlockspaceSpec, + type BlockspaceTx, + DEFAULT_SPEC, + type DexSpec, + type FeatureDiagramSpec, + type FeeAmmSpec, + type FeeAmmToken, + type ForwardSpec, + type GateSpec, + type HubSpec, + type KeysSpec, + type LanesSpec, + type LinkSpec, + type MemoSpec, + type NoncesSpec, + rowCenters, + type SponsorSpec, +} from '../_lib/featureDiagram' + +const markPath = + 'M5.03996 14.8395H1.03534L4.74694 3.36361H0L1.03534 0H14.2604L13.225 3.36361H8.73202L5.03996 14.8395Z' + +const color = (accent: number) => PALETTE[accent % PALETTE.length] + +function NodeCard({ + x, + midY, + accent, + faded = false, +}: { + x: number + midY: number + accent: number + faded?: boolean +}) { + const boxY = midY - 24 + const detailX = x + 16 + return ( + + + + + + + + + ) +} + +// The Tempo mark, centered on (cx, cy). Doubles as hub and policy enforcer. +function Mark({ cx = 260, cy = 160 }: { cx?: number; cy?: number }) { + return ( + <> + + + + + + ) +} + +const MONO = 'var(--font-jetbrains-mono), ui-monospace, monospace' + +function Label({ + x, + y, + size = 11, + opacity = 0.85, + tracking = 0.08, + fill = 'var(--foreground)', + anchor = 'start', + children, +}: { + x: number + y: number + size?: number + opacity?: number + tracking?: number + fill?: string + anchor?: 'start' | 'middle' | 'end' + children: string +}) { + return ( + + {children} + + ) +} + +function Cross({ cx, cy }: { cx: number; cy: number }) { + return ( + + ) +} + +function Flow({ d, accent, faded = false }: { d: string; accent: number; faded?: boolean }) { + return ( + + ) +} + +// ── hub ───────────────────────────────────────────────────────────────────── +function HubShape({ spec }: { spec: HubSpec }) { + const leftRows = rowCenters(spec.left.length) + const rightRows = rowCenters(spec.right.length) + const leftArc = (y: number) => (y === 160 ? 'M160 160H248' : `M160 ${y}C208 ${y} 210 160 248 160`) + const rightArc = (y: number) => + y === 160 ? 'M272 160H360' : `M272 160C310 160 312 ${y} 360 ${y}` + return ( + <> + {spec.left.map((n, i) => ( + + ))} + {spec.right.map((n, i) => ( + + ))} + {spec.left.map((n, i) => ( + + ))} + {spec.right.map((n, i) => ( + + ))} + + + ) +} + +// ── gate ──────────────────────────────────────────────────────────────────── +// Inbound transfers (left) are screened by a policy gate. Allowed transfers pass +// through to the destination (right); blocked ones stop at the gate with an ✗. +// This is a rule at the edge, not a flow through Tempo — so there's no central +// mark. +const GATE_SRC_X = 24 +const GATE_SRC_W = 192 +const GATE_SRC_H = 54 +const GATE_X = 256 +const GATE_DEST_X = 360 +const GATE_DEST_W = 136 +function GateShape({ spec }: { spec: GateSpec }) { + const rows = rowCenters(spec.sources.length) + const srcRight = GATE_SRC_X + GATE_SRC_W + const destTop = 118 + return ( + <> + {/* Inbound transfer cards: token / sender + the rule they match. */} + {spec.sources.map((n, i) => { + const y = rows[i] + const top = y - GATE_SRC_H / 2 + const px = GATE_SRC_X + 16 + return ( + + + + + + ) + })} + + {/* The destination behind the policy. */} + + + + + + {/* The policy check: a dashed gate transfers are screened at. */} + + + + {spec.sources.map((n, i) => { + const y = rows[i] + if (n.blocked) { + return ( + + + + + + ) + } + const d = + y === 160 + ? `M${srcRight} 160H${GATE_DEST_X}` + : `M${srcRight} ${y}C${GATE_X - 12} ${y} ${GATE_X + 12} 160 ${GATE_DEST_X} 160` + return + })} + + ) +} + +// ── keys ──────────────────────────────────────────────────────────────────── +// A scoped key card: name, a spend-cap gauge filled to `cap`, and a scope/expiry +// line. The gauge + scope are what read as "scoped & capped". +const KEY_X = 292 +const KEY_W = 204 +const KEY_H = 66 +function KeyCard({ + midY, + accent, + cap, + name, + scope, + revoked = false, +}: { + midY: number + accent: number + cap: number + name: string + scope: string + revoked?: boolean +}) { + const top = midY - KEY_H / 2 + const px = KEY_X + 18 + const gaugeW = KEY_W - 36 + const fill = Math.max(0, Math.min(1, cap)) + return ( + + + + + {!revoked && ( + + )} + + + ) +} + +function KeysShape({ spec }: { spec: KeysSpec }) { + const rows = rowCenters(spec.keys.length) + const accX = 24 + const accW = 128 + const accTop = 128 + const accRight = accX + accW + return ( + <> + {/* Account: the root authority. It delegates straight to its keys — no hub + in the middle, since this is an account feature, not a flow through + Tempo. */} + + + + + + {spec.keys.map((k, i) => ( + + ))} + + {spec.keys.map((k, i) => { + const y = rows[i] + if (k.revoked) { + // Severed delegation: a short dim stub ending in an ✗. + const cutX = accRight + (KEY_X - accRight) * 0.42 + const cutY = 160 + (y - 160) * 0.42 + return ( + + + + + + ) + } + const d = + y === 160 + ? `M${accRight} 160H${KEY_X}` + : `M${accRight} 160C${accRight + 70} 160 ${KEY_X - 58} ${y} ${KEY_X} ${y}` + return + })} + + ) +} + +// ── link ──────────────────────────────────────────────────────────────────── +// Many credentials converge into one account identity; a recovery credential is +// a dormant standby path. +const CRED_X = 24 +const CRED_W = 184 +const CRED_H = 54 +function CredCard({ + midY, + name, + detail, + recovery = false, +}: { + midY: number + name: string + detail: string + recovery?: boolean +}) { + const top = midY - CRED_H / 2 + const px = CRED_X + 16 + return ( + + + + + + ) +} + +const LINK_ACC_X = 336 +const LINK_ACC_W = 160 +function LinkShape({ spec }: { spec: LinkSpec }) { + const rows = rowCenters(spec.credentials.length) + const credRight = CRED_X + CRED_W + const accTop = 122 + return ( + <> + {/* Account: many credentials, one identity. */} + + + + + + {spec.credentials.map((c, i) => ( + + ))} + + {spec.credentials.map((c, i) => { + const y = rows[i] + const d = + y === 160 + ? `M${credRight} 160H${LINK_ACC_X}` + : `M${credRight} ${y}C${credRight + 58} ${y} ${LINK_ACC_X - 62} 160 ${LINK_ACC_X} 160` + if (c.recovery) { + return ( + + ) + } + return + })} + + ) +} + +// ── forward ───────────────────────────────────────────────────────────────── +// Deposits land on per-customer virtual addresses (left) and auto-forward into +// one wallet (right). The inbound stubs + convergence read as "no sweep". +const FWD_SRC_X = 96 +const FWD_SRC_W = 172 +const FWD_SRC_H = 54 +const FWD_DEST_X = 340 +const FWD_DEST_W = 156 +function ForwardShape({ spec }: { spec: ForwardSpec }) { + const rows = rowCenters(spec.sources.length) + const srcRight = FWD_SRC_X + FWD_SRC_W + const destTop = 120 + return ( + <> + {/* Destination wallet. */} + + + + + + {/* Per-customer virtual addresses. */} + {spec.sources.map((s, i) => { + const y = rows[i] + const top = y - FWD_SRC_H / 2 + const px = FWD_SRC_X + 14 + return ( + + + + + + ) + })} + + {/* Inbound deposits landing on each address. */} + {spec.sources.map((s, i) => ( + + ))} + + {/* Auto-forward into the one wallet. */} + {spec.sources.map((s, i) => { + const y = rows[i] + const d = + y === 160 + ? `M${srcRight} 160H${FWD_DEST_X}` + : `M${srcRight} ${y}C${srcRight + 50} ${y} ${FWD_DEST_X - 58} 160 ${FWD_DEST_X} 160` + return + })} + + ) +} + +// ── lanes ─────────────────────────────────────────────────────────────────── +// Independent transactions run concurrently. Each tx (left) touches disjoint +// state, so it rides its own straight lane — parallel, never queued — and all +// lanes land together as packed slots in one block (right). Straight, aligned +// tracks are the signal: side-by-side, not single-file. +const LANE_TX_X = 24 +const LANE_TX_W = 188 +const LANE_TX_H = 54 +const LANE_BLK_X = 372 +const LANE_BLK_W = 124 +const LANE_BLK_TOP = 64 +const LANE_BLK_H = 192 +function LanesShape({ spec }: { spec: LanesSpec }) { + const rows = rowCenters(spec.txs.length) + const txRight = LANE_TX_X + LANE_TX_W + const blkBottom = LANE_BLK_TOP + LANE_BLK_H + const blkCx = LANE_BLK_X + LANE_BLK_W / 2 + return ( + <> + {/* Independent transactions, each touching disjoint state. */} + {spec.txs.map((t, i) => { + const y = rows[i] + const top = y - LANE_TX_H / 2 + const px = LANE_TX_X + 16 + return ( + + + + + + ) + })} + + {/* The block: every lane executes at once and lands here together. */} + + + {spec.txs.map((t, i) => { + const y = rows[i] + return ( + + + + + ) + })} + + + {/* Parallel lanes: straight + aligned = concurrent, non-blocking. */} + {spec.txs.map((t, i) => ( + + ))} + + ) +} + +// ── nonces ────────────────────────────────────────────────────────────────── +// One account, many transactions in flight at once. Each tx carries a nonce. A +// `pending` tx (the lowest nonce here) is still in flight, yet the higher +// nonces confirm anyway — on a sequential chain they'd be stuck behind it. That +// contrast is the point: no head-of-line blocking. +const NONCE_X = 296 +const NONCE_W = 200 +const NONCE_H = 58 +function NoncesShape({ spec }: { spec: NoncesSpec }) { + const rows = rowCenters(spec.txs.length) + const accX = 24 + const accW = 132 + const accTop = 128 + const accRight = accX + accW + return ( + <> + {/* The account: one signer, many txs in flight. */} + + + + + + {/* Transactions, each with a nonce + a confirmed/pending status dot. */} + {spec.txs.map((t, i) => { + const y = rows[i] + const top = y - NONCE_H / 2 + const px = NONCE_X + 16 + const dotX = NONCE_X + NONCE_W - 18 + return ( + + + + + {t.pending ? ( + + ) : ( + + )} + + ) + })} + + {/* Concurrent paths; the pending one is dormant but never blocks the rest. */} + {spec.txs.map((t, i) => { + const y = rows[i] + const d = + y === 160 + ? `M${accRight} 160H${NONCE_X}` + : `M${accRight} 160C${accRight + 70} 160 ${NONCE_X - 58} ${y} ${NONCE_X} ${y}` + if (t.pending) { + return ( + + ) + } + return + })} + + ) +} + +// ── blockspace ────────────────────────────────────────────────────────────── +// Payment lanes: every block is split into dedicated payment blockspace and +// general blockspace. Payment txs flow into the reserved lane with sub-cent +// fees; non-payment traffic lands in general blockspace at a higher fee. +const BS_CARD_X = 24 +const BS_CARD_W = 160 +const BS_CARD_H = 44 +const BS_BLK_X = 312 +const BS_BLK_W = 184 +const BS_BLK_TOP = 52 +const BS_BLK_H = 252 +const BS_DIV_Y = BS_BLK_TOP + 142 // payment lane is visibly dedicated. +function bsCard( + t: { accent: number; label: string; detail: string }, + y: number, + faded: boolean, + key: string, +) { + const top = y - BS_CARD_H / 2 + const px = BS_CARD_X + 14 + return ( + + + + + + ) +} +function BlockspaceSlot({ + x, + y, + width, + tx, +}: { + x: number + y: number + width: number + tx: BlockspaceTx +}) { + return ( + + + + + + ) +} +function BlockspaceShape({ spec }: { spec: BlockspaceSpec }) { + const cardRight = BS_CARD_X + BS_CARD_W + const blkCx = BS_BLK_X + BS_BLK_W / 2 + const slotX = BS_BLK_X + 16 + const slotW = BS_BLK_W - 32 + const payTop = 120 + const payGap = 48 + const payRows = spec.payments.map((_, i) => payTop + i * payGap) + const generalY = BS_DIV_Y + 60 + const bars = Array.from({ length: 4 }, (_, i) => BS_DIV_Y + 84 + i * 7) + return ( + <> + + + {/* The tinted payment band makes the dedicated reservation visible. */} + + + + + {spec.payments.map((t, i) => ( + + ))} + + + + {bars.map((by, i) => ( + + ))} + + {/* Inbound examples land in separate parts of the same block. */} + {spec.payments.map((t, i) => bsCard(t, payRows[i], false, `pc${i}`))} + {bsCard(spec.general, generalY, true, 'general')} + + {spec.payments.map((t, i) => ( + + ))} + + + {/* Tempo draws the line: the protocol enforces the reservation, so the + mark sits on the divider between the lanes. */} + + + ) +} + +// ── feeamm ────────────────────────────────────────────────────────────────── +// Fees in any stablecoin: the user selects a fee token, the Fee AMM converts it, +// and the validator receives the token they accept. +const FA_USER_X = 32 +const FA_USER_W = 136 +const FA_CARD_TOP = 108 +const FA_CARD_H = 104 +const FA_AMM_X = 208 +const FA_AMM_W = 128 +const FA_AMM_TOP = 64 +const FA_AMM_H = 192 +const FA_MID_Y = 160 +const FA_VAL_X = 384 +const FA_VAL_W = 110 +function FeeTokenBox({ + x, + y, + width, + height, + token, +}: { + x: number + y: number + width: number + height: number + token: FeeAmmToken +}) { + const labelX = x + 12 + return ( + + + + + + ) +} +function FeeAmmShape({ spec }: { spec: FeeAmmSpec }) { + const userRight = FA_USER_X + FA_USER_W + const ammCx = FA_AMM_X + FA_AMM_W / 2 + const ammRight = FA_AMM_X + FA_AMM_W + const selectedY = FA_AMM_TOP + 36 + const receivedY = FA_AMM_TOP + 120 + const valTop = FA_CARD_TOP + const cardLabelX = FA_USER_X + 18 + const valLabelX = FA_VAL_X + 18 + const ammTokenX = ammCx - 43 + const ammTokenW = 86 + const ammTokenH = 38 + return ( + <> + + + + + + + + + + + + + + + + + + + + + + + + ) +} + +// ── sponsor ────────────────────────────────────────────────────────────────── +// Fee sponsorship. The app signs the user's transaction as fee payer, the user +// sends it, and Tempo executes the action while debiting the fee payer balance. +const SP_PARTY_X = 36 +const SP_PARTY_W = 148 +const SP_PARTY_H = 54 +const SP_APP_CY = 92 +const SP_USER_CY = 218 +const SP_TX_X = 266 +const SP_TX_W = 140 +const SP_TX_TOP = 74 +const SP_TX_H = 172 +const SP_HUB_X = 464 +const SP_HUB_Y = 160 +function SponsorParty({ + cy, + accent, + label, + detail, +}: { + cy: number + accent: number + label: string + detail: string +}) { + const top = cy - SP_PARTY_H / 2 + const px = SP_PARTY_X + 16 + return ( + + + + + + + ) +} +function SponsorShape({ spec }: { spec: SponsorSpec }) { + const slotX = SP_TX_X + 14 + const slotW = SP_TX_W - 28 + const actionCy = 130 + const feeCy = 184 + const txRight = SP_TX_X + SP_TX_W + const txCx = SP_TX_X + SP_TX_W / 2 + const partyCx = SP_PARTY_X + SP_PARTY_W / 2 + const partyRight = SP_PARTY_X + SP_PARTY_W + return ( + <> + + + + {/* One transaction executes the action and charges the fee payer. */} + + + + + + + + + + + + + + + + + + {/* The assembled transaction executes on Tempo. */} + + + + + + ) +} + +// ── batch ──────────────────────────────────────────────────────────────────── +// Batch & schedule, told abstractly. A stack of payouts (left) bundle into one +// batch sealed by a single signature — atomic, all-or-nothing. That batch is +// scheduled: it executes on Tempo (the mark) only inside a time window (the +// shaded band on the timeline, right), bounded by an open and a close. +const BT_BOX_X = 40 +const BT_BOX_W = 176 +const BT_BOX_TOP = 64 +const BT_BOX_H = 192 +const BT_AXIS_Y = 160 +const BT_WIN_OPEN = 322 +const BT_WIN_CLOSE = 446 +const BT_HUB_X = 384 +function BatchShape({ spec }: { spec: BatchSpec }) { + const boxCx = BT_BOX_X + BT_BOX_W / 2 + const boxRight = BT_BOX_X + BT_BOX_W + const slotX = BT_BOX_X + 14 + const slotW = BT_BOX_W - 28 + const slotH = 30 + const slotGap = 12 + const n = spec.calls.length + const stackH = n * slotH + (n - 1) * slotGap + const stackTop = BT_BOX_TOP + 26 + const sealY = BT_BOX_TOP + BT_BOX_H - 22 + const winTop = 100 + const winBot = 220 + return ( + <> + {/* The batch: a stack of calls under one signature. */} + + + {spec.calls.map((c, i) => { + const top = stackTop + i * (slotH + slotGap) + return ( + + + + + ) + })} + + + + {/* The schedule: a timeline with the execution window. */} + + + + + + + + + {/* The sealed batch fires inside the window. */} + + + + ) +} + +// ── memo ───────────────────────────────────────────────────────────────────── +// Transfer memos, told abstractly. A payment (left) carries a memo reference. +// The reference is recorded on-chain with the money and surfaces in a low- +// fidelity explorer view of the transaction (right): most fields are skeleton +// bars, but the MEMO field is real and highlighted in the memo accent — the +// reference anyone can reconcile against. +const ME_PAY_X = 40 +const ME_PAY_W = 170 +const ME_PAY_TOP = 100 +const ME_PAY_H = 120 +const ME_EXP_X = 300 +const ME_EXP_W = 180 +const ME_EXP_TOP = 72 +const ME_EXP_H = 176 +function MemoShape({ spec }: { spec: MemoSpec }) { + const payCx = ME_PAY_X + ME_PAY_W / 2 + const payRight = ME_PAY_X + ME_PAY_W + const payPx = ME_PAY_X + 16 + const payInnerW = ME_PAY_W - 32 + const expCx = ME_EXP_X + ME_EXP_W / 2 + const expPx = ME_EXP_X + 16 + const expRight = ME_EXP_X + ME_EXP_W - 16 + const valX = ME_EXP_X + 74 + const n = spec.fields.length + const rowCy = (i: number) => ME_EXP_TOP + (ME_EXP_H * (i + 0.5)) / n + return ( + <> + {/* The payment, carrying its memo reference. */} + + + + + + + + + + {/* The reference is recorded on-chain with the money. */} + + + {/* Low-fidelity explorer view of the on-chain transaction. */} + + + {spec.fields.slice(1).map((_, i) => { + const y = ME_EXP_TOP + (ME_EXP_H * (i + 1)) / n + return ( + + ) + })} + {spec.fields.map((f, i) => { + const cy = rowCy(i) + return ( + + {f.highlight && ( + + )} + + {f.value !== undefined ? ( + + ) : ( + + )} + + ) + })} + + ) +} + +// ── dex ────────────────────────────────────────────────────────────────────── +// Enshrined stablecoin DEX. A token flows in (left), matches through a real +// order book (center) — asks stacked above the spread, bids below, the Tempo +// mark sitting on the spread as the matching engine — and taps shared +// protocol liquidity. No pool contract to deploy. +const DX_IN_X = 36 +const DX_OUT_X = 338 +const DX_CARD_W = 146 +const DX_CARD_H = 54 +const DX_BOOK_X = 206 +const DX_BOOK_W = 108 +const DX_MID_Y = 160 +const DX_ASK_FILL = 'var(--negative)' +const DX_BID_FILL = 'var(--indicator-green)' +const DX_RESOLVER_FILL = 'var(--surface-onyx)' +function DexCard({ + x, + accent, + label, + detail, +}: { + x: number + accent: number + label: string + detail: string +}) { + const top = DX_MID_Y - DX_CARD_H / 2 + const px = x + 16 + return ( + + + + + {detail && ( + + )} + + ) +} +function DexShape({ spec }: { spec: DexSpec }) { + const bookCx = DX_BOOK_X + DX_BOOK_W / 2 + const barX = DX_BOOK_X + 16 + const barMaxW = DX_BOOK_W - 32 + const inRight = DX_IN_X + DX_CARD_W + const outLeft = DX_OUT_X + const resolverTop = DX_MID_Y - 17 + const resolverH = 34 + return ( + <> + {/* The token you bring resolves through pathUSD, then routes to the output stablecoin. */} + + + + {/* Trade routes in through the book, resolves to the quote token, then routes out. */} + + + + {/* The enshrined order book. */} + + + + {/* Asks above the spread. */} + + {spec.asks.map((w, i) => ( + + ))} + + {/* The spread, where the matching engine sits. */} + + + {/* Bids below the spread. */} + {spec.bids.map((w, i) => ( + + ))} + + + {spec.resolver ? ( + + + + + + ) : ( + + )} + + ) +} + +export default function FeatureDiagram({ + spec = DEFAULT_SPEC, + compact = false, + hideSmallCaptions = false, + containerClassName, +}: { + spec?: FeatureDiagramSpec + // Tile sizing for the playground grid; default fills the feature-page panel. + compact?: boolean + hideSmallCaptions?: boolean + containerClassName?: string +}) { + return ( +
+ + + + + + + + + + + + {spec.kind === 'gate' ? ( + + ) : spec.kind === 'keys' ? ( + + ) : spec.kind === 'link' ? ( + + ) : spec.kind === 'forward' ? ( + + ) : spec.kind === 'lanes' ? ( + + ) : spec.kind === 'nonces' ? ( + + ) : spec.kind === 'blockspace' ? ( + + ) : spec.kind === 'feeamm' ? ( + + ) : spec.kind === 'sponsor' ? ( + + ) : spec.kind === 'batch' ? ( + + ) : spec.kind === 'memo' ? ( + + ) : spec.kind === 'dex' ? ( + + ) : ( + + )} + +
+ ) +} diff --git a/src/marketing/app/diagrams/_components/FeatureDiagramGallery.tsx b/src/marketing/app/diagrams/_components/FeatureDiagramGallery.tsx new file mode 100644 index 00000000..9cbc8332 --- /dev/null +++ b/src/marketing/app/diagrams/_components/FeatureDiagramGallery.tsx @@ -0,0 +1,42 @@ +import { FEATURE_CATALOG } from '../_lib/featureCatalog' +import FeatureDiagram from './FeatureDiagram' + +// The working board: every Tempo feature as a tile, grouped by product area. +// Each tile renders its current spec so we can design the purpose-built diagram +// for each feature directly against the real component. + +export default function FeatureDiagramGallery() { + return ( +
+ {FEATURE_CATALOG.map((group) => ( +
+
+

{group.area}

+

+ {group.blurb} +

+
+
+ {group.features.map((feature) => ( +
+
+ +
+
+ {feature.name} + + {feature.blurb} + +
+
+ ))} +
+
+ ))} +
+ ) +} diff --git a/src/marketing/app/diagrams/_components/Playground.tsx b/src/marketing/app/diagrams/_components/Playground.tsx new file mode 100644 index 00000000..58efd12a --- /dev/null +++ b/src/marketing/app/diagrams/_components/Playground.tsx @@ -0,0 +1,375 @@ +'use client' + +import { useMemo, useState } from 'react' +import { + buildBarChartSvg, + buildLaneDiagramSvg, + DEFAULT_STYLE, + type DiagramStyle, +} from '../_lib/diagramSvg' + +const DEFAULT_CHART = { + title: 'Sustained TPS by release', + subtitle: 'Live network, continuous load — perf.tempo.xyz', + values: '8700, 12800, 17000, 21200', + labels: 'v1.5, v1.6, v1.7, v1.8', +} + +const fieldLabel = 'font-mono text-[11px] tracking-[0.02em] text-foreground/40 uppercase' +const textInput = + 'h-9 w-full border border-line bg-surface-input px-3 font-mono text-[12px] text-foreground outline-none focus:border-line-strong' + +function ColorField({ + label, + value, + onChange, +}: { + label: string + value: string + onChange: (v: string) => void +}) { + return ( + + ) +} + +function SliderField({ + label, + value, + min, + max, + step, + onChange, +}: { + label: string + value: number + min: number + max: number + step: number + onChange: (v: number) => void +}) { + return ( + + ) +} + +function ActionButton({ label, onClick }: { label: string; onClick: () => void }) { + return ( + + ) +} + +function downloadSvg(filename: string, svg: string) { + const url = URL.createObjectURL(new Blob([svg], { type: 'image/svg+xml' })) + const a = document.createElement('a') + a.href = url + a.download = filename + a.click() + URL.revokeObjectURL(url) +} + +export default function Playground() { + const [style, setStyle] = useState(DEFAULT_STYLE) + const [chart, setChart] = useState(DEFAULT_CHART) + const [copied, setCopied] = useState(null) + + const set = (key: K, value: DiagramStyle[K]) => + setStyle((s) => ({ ...s, [key]: value })) + + const values = useMemo( + () => + chart.values + .split(',') + .map((v) => Number(v.trim())) + .filter((v) => Number.isFinite(v) && v > 0), + [chart.values], + ) + const labels = useMemo(() => chart.labels.split(',').map((l) => l.trim()), [chart.labels]) + + const barSvg = useMemo( + () => + buildBarChartSvg(style, { + title: chart.title, + subtitle: chart.subtitle, + values, + labels, + accentIndex: values.length - 1, + }), + [style, chart.title, chart.subtitle, values, labels], + ) + const laneSvg = useMemo(() => buildLaneDiagramSvg(style), [style]) + + const copy = (id: string, text: string) => { + navigator.clipboard.writeText(text) + setCopied(id) + setTimeout(() => setCopied(null), 1500) + } + + const tokensJson = JSON.stringify(style, null, 2) + + return ( +
+ {/* Controls */} +
+
+

Colors

+ set('background', v)} + /> + set('boxFill', v)} /> + set('boxStroke', v)} + /> + set('gridline', v)} + /> + set('baseline', v)} + /> + set('accentStroke', v)} + /> + set('accentFill', v)} + /> +
+ +
+

Text opacities

+ set('textPrimary', v)} + /> + set('textSecondary', v)} + /> + set('textLabel', v)} + /> + set('textMuted', v)} + /> +
+ +
+

Type & shape

+ set('titleSize', v)} + /> + set('subtitleSize', v)} + /> + set('labelSize', v)} + /> + set('letterSpacing', v)} + /> + set('strokeWidth', v)} + /> + set('cornerRadius', v)} + /> +
+ +
+

Chart data

+ + + + +

+ The last bar takes the accent — the diagram's one highlighted idea. +

+
+ +
+ copy('tokens', tokensJson)} + /> + setStyle(DEFAULT_STYLE)} /> +
+
+ + {/* Previews */} +
+ {[ + { id: 'bar', name: 'bar-chart', heading: 'Bar chart', svg: barSvg }, + { id: 'lane', name: 'lane-diagram', heading: 'Box / lane diagram', svg: laneSvg }, + ].map((preview) => ( +
+
+

{preview.heading}

+
+ copy(preview.id, preview.svg)} + /> + downloadSvg(`${preview.name}.svg`, preview.svg)} + /> +
+
+
+ {/* biome-ignore lint/security/noDangerouslySetInnerHtml: Playground previews render static SVG strings generated in this component. */} +
+
+
+ + View SVG source + +
+                {preview.svg}
+              
+
+
+ ))} + +
+

Style tokens

+

+ Tweak until it looks right, then copy this block alongside the exported SVG. +

+
+            {tokensJson}
+          
+
+
+
+ ) +} diff --git a/src/marketing/app/diagrams/_lib/diagramSvg.ts b/src/marketing/app/diagrams/_lib/diagramSvg.ts new file mode 100644 index 00000000..adba6a4e --- /dev/null +++ b/src/marketing/app/diagrams/_lib/diagramSvg.ts @@ -0,0 +1,161 @@ +// Pure builders for the house diagram style. The playground previews and exports +// come from the same string, so the preview matches the downloaded SVG. + +export type DiagramStyle = { + background: string + boxFill: string + boxStroke: string + gridline: string + baseline: string + accentStroke: string + accentFill: string + textPrimary: number + textSecondary: number + textLabel: number + textMuted: number + titleSize: number + subtitleSize: number + labelSize: number + letterSpacing: number + strokeWidth: number + cornerRadius: number +} + +export const DEFAULT_STYLE: DiagramStyle = { + background: '#0e0e0e', + boxFill: '#1c1c1c', + boxStroke: '#2e2e2e', + gridline: '#181818', + baseline: '#2e2e2e', + accentStroke: '#57B88A', + accentFill: '#143810', + textPrimary: 0.85, + textSecondary: 0.4, + textLabel: 0.6, + textMuted: 0.35, + titleSize: 13, + subtitleSize: 11, + labelSize: 11, + letterSpacing: 0.04, + strokeWidth: 1, + cornerRadius: 0, +} + +export type BarChartData = { + title: string + subtitle: string + values: number[] + labels: string[] + accentIndex: number +} + +const FONT = "ui-monospace, 'JetBrains Mono', monospace" + +function escapeXml(text: string): string { + return text + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') +} + +function white(opacity: number): string { + return `rgba(255,255,255,${opacity})` +} + +function titleBlock(s: DiagramStyle, title: string, subtitle: string): string { + return [ + ` ${escapeXml(title.toUpperCase())}`, + ` ${escapeXml(subtitle.toUpperCase())}`, + ].join('\n') +} + +function formatTick(value: number): string { + return value >= 950 ? `${Math.round(value / 1000)}K` : String(Math.round(value)) +} + +export function buildBarChartSvg(s: DiagramStyle, data: BarChartData): string { + const W = 840 + const H = 420 + const left = 40 + const right = 800 + const baseY = 360 + const span = 254 + + const values = data.values.length ? data.values : [1] + const n = values.length + const maxVal = Math.max(...values, 1) + const barW = Math.min(96, Math.floor((right - 120 - (n - 1) * 24) / n)) + const gap = n > 1 ? (right - 120 - n * barW) / (n - 1) : 0 + + const gridlines = [1, 2, 3, 4] + .map((k) => { + const y = baseY - k * 60 + const tick = formatTick((k * 60 * maxVal) / span) + return [ + ` `, + ` ${tick}`, + ].join('\n') + }) + .join('\n') + + const bars = values + .map((value, i) => { + const h = (value / maxVal) * span + const x = 120 + i * (barW + gap) + const y = baseY - h + const cx = x + barW / 2 + const accent = i === data.accentIndex + const label = escapeXml(data.labels[i] ?? '') + const fill = accent ? s.accentFill : s.boxFill + const stroke = accent ? s.accentStroke : s.boxStroke + const valueFill = accent ? s.accentStroke : white(0.45) + const labelFill = accent ? white(0.7) : white(s.textSecondary) + return [ + ` `, + ` ${value.toLocaleString('en-US')}`, + ` ${label}`, + ].join('\n') + }) + .join('\n') + + return ` + +${titleBlock(s, data.title, data.subtitle)} +${gridlines} + +${bars} + +` +} + +export function buildLaneDiagramSvg(s: DiagramStyle): string { + const W = 840 + const H = 260 + const rows = [0, 1, 2] + const boxH = 32 + const boxW = 110 + + const lanes = rows + .map((i) => { + const boxY = 96 + i * 44 + const textY = boxY + 21 + return [ + ` LANE ${i}`, + ` `, + ` tx ${i + 1}`, + ` `, + ` tx ${i + 4}`, + ].join('\n') + }) + .join('\n') + + return ` + +${titleBlock(s, 'Box / lane diagram', 'Neutral boxes, accent boxes & a dashed conceptual region')} +${lanes} + + RECLAIMED BLOCKSPACE + +` +} diff --git a/src/marketing/app/diagrams/_lib/featureCatalog.ts b/src/marketing/app/diagrams/_lib/featureCatalog.ts new file mode 100644 index 00000000..65491a0d --- /dev/null +++ b/src/marketing/app/diagrams/_lib/featureCatalog.ts @@ -0,0 +1,408 @@ +import type { FeatureDiagramSpec } from './featureDiagram' + +// Every Tempo feature that needs a diagram, grouped by product area (mirrors the +// site's feature pages and docs). Each entry starts from a hub-and- +// spoke `spec` whose shape roughly matches the feature's meaning — fan-in for +// things that aggregate, fan-out for things that distribute, one-to-one for +// direct flows. These are starting points: we refine each into a purpose-built +// diagram from the playground, one feature at a time. + +export type FeatureEntry = { + // kebab id, stable for linking/iteration. + id: string + name: string + blurb: string + spec: FeatureDiagramSpec +} + +export type FeatureArea = { + area: string + blurb: string + features: FeatureEntry[] +} + +const fanOut = (left: number, right: number[]): FeatureDiagramSpec => ({ + kind: 'hub', + left: [{ accent: left }], + right: right.map((accent) => ({ accent })), +}) + +const oneToOne = (left: number, right: number): FeatureDiagramSpec => ({ + kind: 'hub', + left: [{ accent: left }], + right: [{ accent: right }], +}) + +// A policy gate: inbound transfers pass, `blocked` ones are stopped before the +// account. Each source carries a token/sender label and the rule it matched. +const gate = ( + dest: { accent: number; label: string; sub: string }, + sources: { accent: number; label: string; detail: string; blocked?: boolean }[], +): FeatureDiagramSpec => ({ + kind: 'gate', + sources, + dest: dest.accent, + destLabel: dest.label, + destSub: dest.sub, +}) + +// Independent transactions on parallel lanes landing together in one block. +const lanes = ( + txs: { accent: number; label: string; detail: string }[], + blockLabel: string, + blockSub: string, +): FeatureDiagramSpec => ({ kind: 'lanes', txs, blockLabel, blockSub }) + +// One account with many nonced txs in flight; a `pending` tx never blocks the +// others from confirming. +const nonces = ( + account: { accent: number; label: string; sub: string }, + txs: { accent: number; label: string; detail: string; pending?: boolean }[], + note: string, +): FeatureDiagramSpec => ({ + kind: 'nonces', + account: account.accent, + accountLabel: account.label, + accountSub: account.sub, + txs, + note, +}) + +// A block split into a reserved payment lane and a congested general lane. +const blockspace = ( + payments: { accent: number; label: string; detail: string }[], + general: { accent: number; label: string; detail: string }, + paymentLaneLabel: string, + generalLabel: string, +): FeatureDiagramSpec => ({ + kind: 'blockspace', + payments, + general, + paymentLaneLabel, + generalLabel, +}) + +// A user-selected fee token is converted by the enshrined Fee AMM into the +// validator's preferred token. +type FeeAmmParty = { accent: number; label: string; detail: string } +type FeeAmmToken = { accent: number; symbol: string } +const feeamm = (cfg: { + user: FeeAmmParty + selectedToken: FeeAmmToken + receivedToken: FeeAmmToken + ammLabel: string + validator: FeeAmmParty +}): FeatureDiagramSpec => ({ kind: 'feeamm', ...cfg }) + +// Fee sponsorship: the app signs the transaction as fee payer; the user sends +// it, and Tempo debits the fee payer balance for fees. +type SponsorParty = { accent: number; label: string; detail: string } +const sponsor = (cfg: { + user: SponsorParty + sponsor: SponsorParty + txLabel: string + actionLabel: string + gasLabel: string + hubLabel: string + caption: string +}): FeatureDiagramSpec => ({ kind: 'sponsor', ...cfg }) + +// Batch & schedule: many calls bundle into one atomic, single-signature +// transaction (all-or-nothing) that executes inside a scheduled time window. +type BatchCall = { accent: number; label: string } +const batch = (cfg: { + batchLabel: string + calls: BatchCall[] + sealLabel: string + openLabel: string + closeLabel: string + hubLabel: string + caption: string +}): FeatureDiagramSpec => ({ kind: 'batch', ...cfg }) + +// Enshrined stablecoin DEX: a token trades natively through a real order book +// and taps protocol-level liquidity with no pool to deploy. +type DexParty = { accent: number; label: string; detail: string } +const dex = (cfg: { + input: DexParty + output: DexParty + asks: number[] + bids: number[] + bookLabel: string + caption: string +}): FeatureDiagramSpec => ({ kind: 'dex', ...cfg }) + +// Transfer memos: a payment carries a short reference that is recorded on-chain +// with the money, so it surfaces in the transaction record for reconciliation. +type MemoField = { label: string; value?: string; highlight?: boolean; barW?: number } +const memo = (cfg: { + paymentLabel: string + amount: string + amountAccent: number + memoLabel: string + memoValue: string + memoAccent: number + explorerLabel: string + fields: MemoField[] + caption: string +}): FeatureDiagramSpec => ({ kind: 'memo', ...cfg }) + +// An account delegating to scoped keys, each with a name, scope, and cap (0..1). +const keys = ( + account: { accent: number; label: string; sub: string }, + ks: { accent: number; cap: number; name: string; scope: string; revoked?: boolean }[], +): FeatureDiagramSpec => ({ + kind: 'keys', + account: account.accent, + accountLabel: account.label, + accountSub: account.sub, + keys: ks, +}) + +// Deposits land on per-customer virtual addresses, then auto-forward into one +// destination wallet — no sweep transactions. +const forward = ( + dest: { accent: number; label: string; sub: string }, + sources: { accent: number; label: string; detail: string }[], +): FeatureDiagramSpec => ({ + kind: 'forward', + dest: dest.accent, + destLabel: dest.label, + destSub: dest.sub, + sources, +}) + +// Many credentials linking to one account, with an optional recovery standby. +const link = ( + account: { accent: number; label: string; sub: string }, + creds: { accent: number; name: string; detail: string; recovery?: boolean }[], +): FeatureDiagramSpec => ({ + kind: 'link', + account: account.accent, + accountLabel: account.label, + accountSub: account.sub, + credentials: creds, +}) + +export const FEATURE_CATALOG: FeatureArea[] = [ + { + area: 'Accounts', + blurb: 'Self-custodial accounts, scoped keys, and virtual addresses.', + features: [ + { + id: 'passkey-accounts', + name: 'Passkey accounts', + blurb: 'Self-custody with WebAuthn — no seed phrase.', + spec: link({ accent: 2, label: 'ACCOUNT', sub: 'SELF-CUSTODY · NO SEED PHRASE' }, [ + { accent: 0, name: 'PASSKEY', detail: 'iPHONE · FACE ID' }, + { accent: 1, name: 'PASSKEY', detail: 'LAPTOP · TOUCH ID' }, + { accent: 3, name: 'PASSKEY', detail: 'SECURITY KEY · FIDO2' }, + ]), + }, + { + id: 'access-keys', + name: 'Access keys', + blurb: 'Scoped, capped keys for apps and agents.', + spec: keys({ accent: 1, label: 'ACCOUNT', sub: 'ROOT · PASSKEY' }, [ + { accent: 0, cap: 0.7, name: 'AGENT KEY', scope: 'PAY · ≤ $500/DAY · 7d' }, + { accent: 2, cap: 0.45, name: 'CHECKOUT KEY', scope: 'REFUND · ≤ $2K/DAY · 30d' }, + { accent: 3, cap: 0.2, name: 'BACKUP KEY', scope: 'READ-ONLY · NO EXPIRY' }, + ]), + }, + { + id: 'spend-limits', + name: 'Spend limits & revocation', + blurb: 'Periodic caps, instant revoke.', + spec: keys({ accent: 1, label: 'ACCOUNT', sub: 'OWNER' }, [ + { accent: 0, cap: 0.92, name: 'AGENT KEY', scope: 'DAILY · RESETS 00:00 UTC' }, + { accent: 2, cap: 0.5, name: 'PAYROLL KEY', scope: 'WEEKLY · ≤ $10K' }, + { accent: 3, cap: 0, name: 'OLD KEY', scope: 'REVOKED · 2d AGO', revoked: true }, + ]), + }, + { + id: 'linking-recovery', + name: 'Linking & recovery', + blurb: 'Link credentials, recover without seed phrases.', + spec: link({ accent: 1, label: 'ACCOUNT', sub: 'ONE IDENTITY' }, [ + { accent: 0, name: 'PASSKEY', detail: 'iPHONE · FACE ID' }, + { accent: 2, name: 'PASSKEY', detail: 'LAPTOP · TOUCH ID' }, + { accent: 3, name: 'RECOVERY', detail: 'GUARDIANS · 3 OF 5', recovery: true }, + ]), + }, + { + id: 'virtual-addresses', + name: 'Virtual addresses', + blurb: 'One master, many deposit addresses, auto-forwarded.', + spec: forward({ accent: 1, label: 'MASTER WALLET', sub: 'ONE ADDRESS · NO SWEEPS' }, [ + { accent: 0, label: 'CUSTOMER A', detail: 'VADDR · AUTO-FWD' }, + { accent: 2, label: 'CUSTOMER B', detail: 'VADDR · AUTO-FWD' }, + { accent: 3, label: 'CUSTOMER C', detail: 'VADDR · AUTO-FWD' }, + ]), + }, + ], + }, + { + area: 'Transactions', + blurb: 'Parallel throughput, stablecoin fees, and composable flows.', + features: [ + { + id: 'parallel-execution', + name: 'Parallel execution', + blurb: 'Independent transactions run concurrently.', + spec: lanes( + [ + { accent: 0, label: 'SWAP', detail: 'TOUCHES POOL A' }, + { accent: 1, label: 'PAYMENT', detail: 'TOUCHES ACCT B' }, + { accent: 2, label: 'MINT', detail: 'TOUCHES TOKEN C' }, + ], + 'ONE BLOCK', + 'EXECUTED IN PARALLEL', + ), + }, + { + id: 'concurrent-nonces', + name: 'Concurrent nonces', + blurb: 'Many transactions per account, no blocking.', + spec: nonces( + { accent: 1, label: 'ACCOUNT', sub: 'ONE SIGNER' }, + [ + { accent: 0, label: 'SWAP', detail: 'NONCE 41 · PENDING', pending: true }, + { accent: 2, label: 'PAYMENT', detail: 'NONCE 42 · CONFIRMED' }, + { accent: 3, label: 'TRANSFER', detail: 'NONCE 43 · CONFIRMED' }, + ], + 'NO HEAD-OF-LINE BLOCKING', + ), + }, + { + id: 'payment-lanes', + name: 'Payment lanes', + blurb: 'Dedicated blockspace keeps payments sub-cent.', + spec: blockspace( + [ + { accent: 3, label: 'PAYMENT', detail: 'FEE $0.001' }, + { accent: 1, label: 'PAYOUT', detail: 'FEE $0.001' }, + ], + { accent: 0, label: 'AIRDROP / TRADE', detail: 'FEE $0.01' }, + 'PAYMENT BLOCKSPACE', + 'GENERAL BLOCKSPACE', + ), + }, + { + id: 'any-stablecoin-fees', + name: 'Fees in any stablecoin', + blurb: 'Fee AMM converts fees automatically.', + spec: feeamm({ + user: { accent: 0, label: 'USER', detail: 'SELECTS FEE TOKEN' }, + selectedToken: { accent: 0, symbol: 'USDC' }, + receivedToken: { accent: 1, symbol: 'USDT' }, + ammLabel: 'FEE AMM', + validator: { accent: 1, label: 'VALIDATOR', detail: 'RECEIVES USDT' }, + }), + }, + { + id: 'fee-sponsorship', + name: 'Fee sponsorship', + blurb: 'A fee payer covers gas for users and agents.', + spec: sponsor({ + user: { accent: 0, label: 'USER', detail: 'SENDS TX' }, + sponsor: { accent: 3, label: 'APP', detail: 'FEE PAYER' }, + txLabel: 'TEMPO TX', + actionLabel: 'ACTION', + gasLabel: 'APP', + hubLabel: 'EXECUTES', + caption: 'FEE PAYER BALANCE IS DEBITED', + }), + }, + { + id: 'batch-schedule', + name: 'Batch & schedule', + blurb: 'Atomic batches and time windows under one signature.', + spec: batch({ + batchLabel: 'BATCH', + calls: [ + { accent: 0, label: 'PAYOUT 01' }, + { accent: 1, label: 'PAYOUT 02' }, + { accent: 3, label: 'PAYOUT 03' }, + ], + sealLabel: 'ONE SIGNATURE · ATOMIC', + openLabel: 'OPENS', + closeLabel: 'CLOSES', + hubLabel: 'EXECUTES', + caption: 'ALL OR NONE · RUNS INSIDE THE WINDOW', + }), + }, + ], + }, + { + area: 'Tokens', + blurb: 'TIP-20 stablecoins, policy, rewards, and the enshrined DEX.', + features: [ + { + id: 'tip20-standard', + name: 'TIP-20 standard', + blurb: 'Payment-native stablecoin token standard.', + spec: oneToOne(1, 2), + }, + { + id: 'transfer-memos', + name: 'Transfer memos', + blurb: '32-byte references for reconciliation.', + spec: memo({ + paymentLabel: 'PAYMENT', + amount: '100.00 USDC', + amountAccent: 2, + memoLabel: 'MEMO', + memoValue: 'INV-4021', + memoAccent: 0, + explorerLabel: 'EXPLORER', + fields: [ + { label: 'TX', barW: 92 }, + { label: 'FROM / TO', barW: 64 }, + { label: 'AMOUNT', value: '100.00 USDC' }, + { label: 'MEMO', value: 'INV-4021', highlight: true }, + ], + caption: 'RECORDED ON-CHAIN · RECONCILE BY REFERENCE', + }), + }, + { + id: 'token-policies', + name: 'Token policies (TIP-403)', + blurb: 'Shared whitelist / blacklist registry.', + spec: gate({ accent: 2, label: 'RECIPIENT', sub: 'TIP-403 REGISTRY' }, [ + { accent: 0, label: 'TRANSFER', detail: 'SENDER WHITELISTED' }, + { accent: 1, label: 'TRANSFER', detail: 'BLACKLISTED', blocked: true }, + { accent: 3, label: 'TRANSFER', detail: 'SENDER WHITELISTED' }, + ]), + }, + { + id: 'role-controls', + name: 'Role-based controls', + blurb: 'Issuer, pause, and burn roles.', + spec: keys({ accent: 2, label: 'STABLECOIN', sub: 'TIP-20 · ROLE REGISTRY' }, [ + { accent: 0, cap: 1, name: 'ISSUER', scope: 'MINT · MANAGE SUPPLY' }, + { accent: 1, cap: 1, name: 'PAUSER', scope: 'PAUSE · UNPAUSE TRANSFERS' }, + { accent: 3, cap: 1, name: 'BURNER', scope: 'BURN · REDUCE SUPPLY' }, + ]), + }, + { + id: 'tip20-rewards', + name: 'TIP-20 rewards', + blurb: 'Proportional rewards to holders at scale.', + spec: fanOut(2, [0, 1, 3]), + }, + { + id: 'dex-liquidity', + name: 'Stablecoin DEX liquidity', + blurb: 'Enshrined orderbook and shared liquidity.', + spec: dex({ + input: { accent: 0, label: 'USDC', detail: 'ANY TIP-20' }, + output: { accent: 1, label: 'USDT', detail: 'USD STABLECOIN' }, + asks: [0.45, 0.7, 0.95], + bids: [0.9, 0.65, 0.4], + bookLabel: 'ENSHRINED DEX', + caption: 'REAL ORDER BOOK · PRICE-TIME PRIORITY · NO POOL', + }), + }, + ], + }, +] diff --git a/src/marketing/app/diagrams/_lib/featureDiagram.ts b/src/marketing/app/diagrams/_lib/featureDiagram.ts new file mode 100644 index 00000000..babdbe72 --- /dev/null +++ b/src/marketing/app/diagrams/_lib/featureDiagram.ts @@ -0,0 +1,289 @@ +// The "feature diagram" language: a small vocabulary of SVG shapes that share +// one visual grammar — boxes, the Tempo mark, and animated dotted arcs (the +// `.diagram-flow` stroke in globals.css). Each shape says something different: +// • hub — nodes flowing through Tempo (aggregation, parallelism, exchange) +// • gate — incoming flows filtered by a policy before they land +// Specs are pure data so each feature instance composes the grammar to mean +// something specific. This directory is meant to be liftable into a standalone +// diagram package later. + +// Index into PALETTE (app/_components/palette.ts). Drives a node's accent bar +// and the color of its connecting arc. +export type DiagramNode = { + accent: number +} + +// Many nodes a side flow through the hub. The original placeholder. +export type HubSpec = { + kind: 'hub' + left: DiagramNode[] + right: DiagramNode[] +} + +// Inbound transfers on the left try to reach a destination; `blocked` ones are +// stopped at the policy gate, the rest pass through. Each carries a label and a +// detail line (the rule it matched). +export type GateNode = { + accent: number + label: string + detail: string + blocked?: boolean +} + +export type GateSpec = { + kind: 'gate' + sources: GateNode[] + dest: number + destLabel: string + destSub: string +} + +// An account delegates through its keychain to scoped keys. Each key has a name, +// a scope/expiry line, and a `cap` (0..1) drawn as a spend-limit gauge — what +// makes a key "scoped". +export type KeyNode = { + accent: number + cap: number + name: string + scope: string + // A revoked key: its delegation is severed and the card reads as cut off. + revoked?: boolean +} + +export type KeysSpec = { + kind: 'keys' + account: number + accountLabel: string + accountSub: string + keys: KeyNode[] +} + +// Many credentials link to one account identity. A `recovery` credential is a +// standby path that can restore access — drawn dormant until needed. +export type LinkCredential = { + accent: number + name: string + detail: string + recovery?: boolean +} + +export type LinkSpec = { + kind: 'link' + account: number + accountLabel: string + accountSub: string + credentials: LinkCredential[] +} + +// Deposits land on per-customer virtual addresses, then auto-forward into one +// destination wallet — no sweep transactions. +export type ForwardNode = { + accent: number + label: string + detail: string +} + +export type ForwardSpec = { + kind: 'forward' + dest: number + destLabel: string + destSub: string + sources: ForwardNode[] +} + +// Independent transactions run concurrently: each touches disjoint state, so +// they ride parallel lanes and land together in one block — no queue, no +// blocking. Each tx has a label and the state it touches (the detail). +export type LaneTx = { + accent: number + label: string + detail: string +} + +export type LanesSpec = { + kind: 'lanes' + txs: LaneTx[] + blockLabel: string + blockSub: string +} + +// One account, many transactions in flight at once. Each tx carries a nonce; a +// `pending` tx is still in flight, yet the others around it confirm anyway — +// the signal that there's no head-of-line blocking by nonce order. +export type NonceTx = { + accent: number + label: string + detail: string + pending?: boolean +} + +export type NoncesSpec = { + kind: 'nonces' + account: number + accountLabel: string + accountSub: string + txs: NonceTx[] + note: string +} + +// Every block reserves part of its space for payments and leaves the rest for +// general traffic. Payment txs flow into dedicated blockspace with sub-cent +// fees; non-payment activity lands in the general lane at a higher fee. +export type BlockspaceTx = { + accent: number + label: string + detail: string +} + +export type BlockspaceSpec = { + kind: 'blockspace' + payments: BlockspaceTx[] + general: BlockspaceTx + paymentLaneLabel: string + generalLabel: string +} + +// Fees in any stablecoin. The user chooses the fee token, the enshrined Fee AMM +// converts it, and the validator receives their preferred token. +export type FeeAmmParty = { + accent: number + label: string + detail: string +} + +export type FeeAmmToken = { + accent: number + symbol: string +} + +export type FeeAmmSpec = { + kind: 'feeamm' + user: FeeAmmParty + selectedToken: FeeAmmToken + receivedToken: FeeAmmToken + ammLabel: string + validator: FeeAmmParty +} + +// Fee sponsorship. A single Tempo Transaction can carry a fee-payer signature +// from the app. The user sends the transaction, Tempo executes the action, and +// the protocol debits the fee from the fee payer balance. +export type SponsorParty = { + accent: number + label: string + detail: string +} + +export type SponsorSpec = { + kind: 'sponsor' + user: SponsorParty + sponsor: SponsorParty + txLabel: string + actionLabel: string + gasLabel: string + hubLabel: string + caption: string +} + +// Batch & schedule. Many calls bundle into one atomic transaction under a single +// signature — all of them land or none do (a payroll run, say). That bundle is +// scheduled: it may only execute inside a future time window. Told abstractly as +// a stack of payouts sealed once, firing inside a window on a timeline. +export type BatchCall = { + accent: number + label: string +} + +export type BatchSpec = { + kind: 'batch' + batchLabel: string + calls: BatchCall[] + sealLabel: string + openLabel: string + closeLabel: string + hubLabel: string + caption: string +} + +// Transfer memos. Every payment can carry a short reference (TIP-20's +// transferWithMemo). The reference is recorded on-chain with the money, so it +// shows up in the transaction record — anyone can reconcile by reference, no +// guesswork. Told as a payment carrying a memo that surfaces as a highlighted +// field in a low-fidelity explorer view of the on-chain transaction. +export type MemoField = { + label: string + // When set, the field shows this value as text; otherwise it renders as a + // low-fidelity skeleton bar of width `barW`. + value?: string + highlight?: boolean + barW?: number +} + +export type MemoSpec = { + kind: 'memo' + paymentLabel: string + amount: string + amountAccent: number + memoLabel: string + memoValue: string + memoAccent: number + explorerLabel: string + fields: MemoField[] + caption: string +} + +// Enshrined stablecoin DEX. The exchange lives in the protocol — every TIP-20 +// trades natively against a real order book (price-time priority), with no pool +// contract to deploy. A token flows in (left), matches through the book (center, +// the Tempo mark sitting on the spread between asks and bids), and taps shared +// protocol liquidity. +export type DexParty = { + accent: number + label: string + detail: string +} + +export type DexSpec = { + kind: 'dex' + input: DexParty + resolver?: DexParty + output: DexParty + // Relative widths (0..1) of the order-book depth levels, asks above the spread + // and bids below it. + asks: number[] + bids: number[] + bookLabel: string + caption: string +} + +export type FeatureDiagramSpec = + | HubSpec + | GateSpec + | KeysSpec + | LinkSpec + | ForwardSpec + | LanesSpec + | NoncesSpec + | BlockspaceSpec + | FeeAmmSpec + | SponsorSpec + | BatchSpec + | MemoSpec + | DexSpec + +// Reproduces the original placeholder exactly: three nodes a side, accents +// stepping through the palette. +export const DEFAULT_SPEC: HubSpec = { + kind: 'hub', + left: [{ accent: 0 }, { accent: 1 }, { accent: 2 }], + right: [{ accent: 1 }, { accent: 2 }, { accent: 3 }], +} + +// Vertical center (mid-y) of each node box for a given side count, matching the +// 320-tall viewBox. The hub sits at y=160, so a single node aligns dead center +// and the three-node case keeps the original 88 / 160 / 232 rows. +export function rowCenters(count: number): number[] { + if (count <= 1) return [160] + if (count === 2) return [88, 232] + return [88, 160, 232] +} diff --git a/src/marketing/app/diagrams/page.tsx b/src/marketing/app/diagrams/page.tsx new file mode 100644 index 00000000..f599fec7 --- /dev/null +++ b/src/marketing/app/diagrams/page.tsx @@ -0,0 +1,58 @@ +import type { Metadata } from 'next' +import Footer from '../_components/Footer' +import Header from '../_components/Header' +import FeatureDiagramGallery from './_components/FeatureDiagramGallery' +import Playground from './_components/Playground' + +export const metadata: Metadata = { + title: 'Diagram playground — Tempo Developers', + description: + "Internal playground for Tempo's diagram language, including feature diagrams and static SVG exports.", +} + +export default function DiagramsPage() { + return ( +
+
+
+ +
+

+ Diagram playground +

+

+ Home of Tempo's diagram language. Author and preview specs here, then ship them — + the components live in app/diagrams/. +

+
+ +
+
+

Feature diagrams

+

+ One diagram per Tempo feature, grouped by product area. Each tile is a starting{' '} + spec in{' '} + featureCatalog.ts that we refine into a + purpose-built diagram, feature by feature. +

+
+ +
+ +
+
+

Static SVGs

+

+ Tweak tokens, drop in data, and copy or download a ready-to-ship SVG. +

+
+ +
+ +
+
+
+
+
+ ) +} diff --git a/src/marketing/app/features/[slug]/page.tsx b/src/marketing/app/features/[slug]/page.tsx new file mode 100644 index 00000000..86113f54 --- /dev/null +++ b/src/marketing/app/features/[slug]/page.tsx @@ -0,0 +1,127 @@ +import type { Metadata } from 'next' +import { notFound } from 'next/navigation' +import Button from '../../_components/Button' +import CodePanel from '../../_components/CodePanel' +import Footer from '../../_components/Footer' +import { features } from '../../_components/features' +import Header from '../../_components/Header' +import HeroDots from '../../_components/HeroDots' +import Reveal from '../../_components/Reveal' +import TokensSections from '../_components/TokensSections' +import TransactionsSections from '../_components/TransactionsSections' + +export function generateStaticParams() { + return features.map((feature) => ({ slug: feature.slug })) +} + +export async function generateMetadata({ + params, +}: { + params: Promise<{ slug: string }> +}): Promise { + const { slug } = await params + const feature = features.find((f) => f.slug === slug) + if (!feature) return {} + return { title: `${feature.title} — Tempo Developers` } +} + +type FeatureParams = { slug: string } | Promise<{ slug: string }> + +function resolveParams(params: FeatureParams) { + if ('then' in params) { + throw new Error('FeaturePage must receive resolved params in the Vite adapter') + } + return params +} + +export default function FeaturePage({ params }: { params: FeatureParams }) { + const { slug } = resolveParams(params) + const feature = features.find((f) => f.slug === slug) + if (!feature) notFound() + + // The dedicated page has room for the full capability set. + const items = [...feature.items, ...(feature.extraItems ?? [])] + + const heroActions = feature.heroActions ?? [ + { label: feature.readLabel, href: feature.readHref, primary: true }, + ] + const primaryAction = heroActions.find((a) => a.primary) ?? heroActions[0] + const secondaryActions = heroActions.filter((a) => a !== primaryAction) + + const page = ( +
+
+
+ +
+ + +

+ {feature.title} +

+

+ {feature.description} +

+
+ + {secondaryActions.length > 0 ? ( +
+ {secondaryActions.map((action) => ( + + ))} +
+ ) : null} +
+
+
+ + {/* Every snippet expanded — no select-to-reveal on the dedicated page. + Rows run full-bleed so their borders meet the shell's side borders; + the content is inset to match the section intros. */} + {feature.slug === 'transactions' ? ( + + ) : feature.slug === 'tokens' ? ( + + ) : ( +
+ {items.map((item, i) => ( + +
+
+

+ {item.label} +

+

+ {item.desc} +

+
+ {item.code ? ( + + ) : null} +
+
+ ))} +
+ )} + +
+
+
+ ) + + return page +} diff --git a/src/marketing/app/features/_components/FeatureFaq.tsx b/src/marketing/app/features/_components/FeatureFaq.tsx new file mode 100644 index 00000000..2b0616a3 --- /dev/null +++ b/src/marketing/app/features/_components/FeatureFaq.tsx @@ -0,0 +1,113 @@ +'use client' + +import { Fragment, useState } from 'react' +import EdgeMarkers from '../../_components/EdgeMarkers' +import Reveal from '../../_components/Reveal' + +type FaqAnswerPart = + | string + | { + text: string + href: string + } + +export type FaqItem = { + question: string + answer: FaqAnswerPart[] +} + +export default function FeatureFaq({ + title, + intro, + items, +}: { + title: string + intro: string + items: FaqItem[] +}) { + const [activeIndex, setActiveIndex] = useState(0) + + return ( +
+ + +
+
+

+ {title} +

+

+ {intro} +

+
+ +
+ {items.map((item, index) => { + const isActive = index === activeIndex + const answerId = `faq-answer-${index}` + + return ( +
+ +
+
+

+ {item.answer.map((part) => + typeof part === 'string' ? ( + {part} + ) : ( + + {part.text} + + ), + )} +

+
+
+
+ ) + })} +
+
+
+
+ ) +} diff --git a/src/marketing/app/features/_components/TokensSections.tsx b/src/marketing/app/features/_components/TokensSections.tsx new file mode 100644 index 00000000..e5920ca4 --- /dev/null +++ b/src/marketing/app/features/_components/TokensSections.tsx @@ -0,0 +1,701 @@ +'use client' + +import { useState } from 'react' +import Button from '../../_components/Button' +import CodeWindow, { type CodeVariant } from '../../_components/CodeWindow' +import EdgeMarkers from '../../_components/EdgeMarkers' +import ModeToggle, { type ShowcaseMode } from '../../_components/ModeToggle' +import { colorForIndex } from '../../_components/palette' +import Reveal from '../../_components/Reveal' +import { + feeTokenCodeVariants, + paymentLaneCodeVariants, +} from '../../_components/transactionCodeVariants' +import FeatureDiagram from '../../diagrams/_components/FeatureDiagram' +import type { FeatureDiagramSpec } from '../../diagrams/_lib/featureDiagram' +import FeatureFaq, { type FaqItem } from './FeatureFaq' + +type FeaturePoint = { + title: string + desc: string + panelTitle?: string + variants?: CodeVariant[] + spec?: FeatureDiagramSpec +} + +type Story = { + id: string + title: string + copy: string + ctas: { label: string; href: string; primary?: boolean }[] + points: FeaturePoint[] + panelTitle: string + variants: CodeVariant[] + // The hero diagram for this story. Stories without one fall back to the + // default placeholder until a semantically apt diagram is built. + spec?: FeatureDiagramSpec +} + +const CODE_WINDOW_HEIGHT = 'max-h-[392px] lg:max-h-[440px]' + +const dexCodeVariants: CodeVariant[] = [ + { + lang: 'TypeScript', + code: [ + 'import { parseUnits } from "viem";', + 'import { client } from "./viem.config";', + '', + 'const usdc = "0x20c0000000000000000000000000000000000001";', + 'const usdt = "0x20c0000000000000000000000000000000000002";', + 'const amountIn = parseUnits("100", 6);', + 'const minAmountOut = parseUnits("99.50", 6);', + '', + 'const { receipt } = await client.dex.sellSync({', + ' tokenIn: usdc,', + ' tokenOut: usdt,', + ' amountIn,', + ' minAmountOut,', + '});', + ], + highlight: ['client.dex.sellSync'], + }, + { + lang: 'Solidity', + code: [ + 'IStablecoinDex dex = IStablecoinDex(', + ' 0xdec0000000000000000000000000000000000000', + ');', + '', + 'uint128 amountIn = 100e6;', + 'uint128 quote = dex.quoteSwapExactAmountIn(', + ' USDC,', + ' USDT,', + ' amountIn', + ');', + '', + 'uint128 minOut = (quote * 995) / 1000;', + 'uint128 amountOut = dex.swapExactAmountIn(', + ' USDC,', + ' USDT,', + ' amountIn,', + ' minOut', + ');', + ], + highlight: ['quoteSwapExactAmountIn', 'swapExactAmountIn'], + }, + { + lang: 'CLI', + code: [ + 'DEX=0xdec0000000000000000000000000000000000000', + 'AMOUNT_IN=100000000', + 'MIN_USDT_OUT=99500000', + '', + 'cast send "$DEX" \\', + ' "swapExactAmountIn(address,address,uint128,uint128)(uint128)" \\', + ' "$USDC" "$USDT" "$AMOUNT_IN" "$MIN_USDT_OUT" \\', + ' --tempo.fee-token "$USDC" \\', + ' --rpc-url "$TEMPO_RPC_URL" \\', + ' --private-key "$PRIVATE_KEY"', + ], + highlight: ['swapExactAmountIn', '--tempo.fee-token'], + }, +] + +const memoCodeVariants: CodeVariant[] = [ + { + lang: 'TypeScript', + code: [ + 'import { parseUnits, stringToHex, pad } from "viem";', + 'import { client } from "./tempo";', + '', + '// A 32-byte reference travels with the transfer', + 'const memo = pad(stringToHex("INV-4021"), { size: 32 });', + '', + 'const { receipt } = await client.token.transferSync({', + ' token: usdc,', + ' to: merchant,', + ' amount: parseUnits("100", 6),', + ' memo,', + '});', + ], + highlight: ['memo,'], + }, + { + lang: 'Rust', + code: [ + 'use alloy::primitives::{B256, U256};', + 'use tempo_alloy::contracts::precompiles::ITIP20;', + '', + 'let memo = B256::right_padding_from(b"INV-4021");', + 'let token = ITIP20::new(usdc, provider);', + '', + 'token', + ' .transferWithMemo(merchant, U256::from(100_000_000), memo)', + ' .send()', + ' .await?;', + ], + highlight: ['transferWithMemo', 'memo'], + }, + { + lang: 'CLI', + code: [ + 'MEMO=$(cast format-bytes32-string "INV-4021")', + '', + 'cast send "$USDC" \\', + ' "transferWithMemo(address,uint256,bytes32)" \\', + ' "$MERCHANT" 100000000 "$MEMO" \\', + ' --rpc-url "$TEMPO_RPC_URL" \\', + ' --private-key "$PRIVATE_KEY"', + ], + highlight: ['transferWithMemo', 'MEMO'], + }, +] + +const feeTokenSpec: FeatureDiagramSpec = { + kind: 'feeamm', + user: { accent: 0, label: 'USER', detail: 'SELECTS FEE TOKEN' }, + selectedToken: { accent: 0, symbol: 'USDC' }, + receivedToken: { accent: 1, symbol: 'USDT' }, + ammLabel: 'FEE AMM', + validator: { accent: 1, label: 'VALIDATOR', detail: 'RECEIVES USDT' }, +} + +const paymentLaneSpec: FeatureDiagramSpec = { + kind: 'blockspace', + payments: [ + { accent: 3, label: 'PAYMENT', detail: 'FEE $0.001' }, + { accent: 1, label: 'PAYOUT', detail: 'FEE $0.001' }, + ], + general: { accent: 0, label: 'AIRDROP / TRADE', detail: 'FEE $0.01' }, + paymentLaneLabel: 'PAYMENT BLOCKSPACE', + generalLabel: 'GENERAL BLOCKSPACE', +} + +const transferMemoSpec: FeatureDiagramSpec = { + kind: 'memo', + paymentLabel: 'PAYMENT', + amount: '100.00 USDC', + amountAccent: 2, + memoLabel: 'MEMO', + memoValue: 'INV-4021', + memoAccent: 0, + explorerLabel: 'EXPLORER', + fields: [ + { label: 'TX', barW: 92 }, + { label: 'FROM / TO', barW: 64 }, + { label: 'AMOUNT', value: '100.00 USDC' }, + { label: 'MEMO', value: 'INV-4021', highlight: true }, + ], + caption: 'RECONCILIATION NATIVE TO THE TOKEN', +} + +const virtualAddressCodeVariants: CodeVariant[] = [ + { + lang: 'Derive', + code: [ + 'import { concatHex, getAddress, parseUnits } from "viem";', + 'import { client } from "./viem.config";', + '', + 'const masterId = "0x2612766c";', + 'const magic = "0xfdfdfdfdfdfdfdfdfdfd";', + 'const userTag = "0x000000000001";', + '', + 'const virtualAddress = getAddress(', + ' concatHex([masterId, magic, userTag]),', + ');', + '', + 'await client.token.transferSync({', + ' token: pathUsd,', + ' to: virtualAddress,', + ' amount: parseUnits("100", 6),', + '});', + ], + highlight: ['concatHex([masterId, magic, userTag])', 'to: virtualAddress'], + }, + { + lang: 'Attribute', + code: [ + '// The trailing 6 bytes are your routing key', + `const userTag = \`0x\${virtualAddress.slice(-12)}\`;`, + '', + 'await db.deposits.create({', + ' data: {', + ' userTag,', + ' customerId: customers.byTag[userTag],', + ' txHash: receipt.transactionHash,', + ' },', + '});', + ], + highlight: ['userTag', 'customers.byTag[userTag]'], + }, + { + lang: 'Balance', + code: [ + 'const masterBalance = await client.token.balanceOf({', + ' token: pathUsd,', + ' owner: masterWallet,', + '});', + '', + 'const virtualBalance = await client.token.balanceOf({', + ' token: pathUsd,', + ' owner: virtualAddress,', + '});', + '', + '// virtualBalance stays 0; funds settle on master', + ], + highlight: ['masterWallet', 'virtualBalance stays 0'], + }, +] + +const policyCodeVariants: CodeVariant[] = [ + { + lang: 'Whitelist', + code: [ + 'uint64 policyId = tip403Registry.createPolicyWithAccounts(', + ' admin,', + ' ITIP403Registry.PolicyType.WHITELIST,', + ' allowedHolders', + ');', + '', + 'tip20.changeTransferPolicyId(policyId);', + ], + highlight: ['PolicyType.WHITELIST', 'changeTransferPolicyId'], + }, + { + lang: 'Blocklist', + code: [ + 'uint64 policyId = tip403Registry.createPolicyWithAccounts(', + ' admin,', + ' ITIP403Registry.PolicyType.BLACKLIST,', + ' blockedAddresses', + ');', + '', + 'tip20.changeTransferPolicyId(policyId);', + ], + highlight: ['PolicyType.BLACKLIST', 'changeTransferPolicyId'], + }, + { + lang: 'Roles', + code: [ + 'bytes32 pauseRole = tip20.PAUSE_ROLE();', + '', + 'tip20.grantRole(pauseRole, complianceOps);', + 'tip20.pause();', + 'tip20.unpause();', + ], + highlight: ['grantRole', 'pause()'], + }, +] + +const STORIES: Story[] = [ + { + id: 'dex', + title: 'Optimized for onchain liquidity.', + copy: 'A stablecoin DEX is enshrined in the protocol. TIP-20 stablecoins trade against shared order-book liquidity, with no contract to deploy.', + spec: { + kind: 'dex', + input: { accent: 0, label: 'USDC', detail: 'ANY TIP-20' }, + resolver: { accent: 2, label: 'pathUSD', detail: 'QUOTE TOKEN' }, + output: { accent: 1, label: 'USDT', detail: 'USD STABLECOIN' }, + asks: [0.45, 0.7, 0.95], + bids: [0.9, 0.65, 0.4], + bookLabel: 'ENSHRINED DEX', + caption: 'REAL ORDER BOOK · PRICE-TIME PRIORITY · NO POOL', + }, + ctas: [ + { label: 'Explore the DEX', href: '/docs/protocol/exchange', primary: true }, + { label: 'Read the spec', href: '/docs/protocol/exchange/spec' }, + ], + panelTitle: 'stablecoin-swap', + variants: dexCodeVariants, + points: [ + { + title: 'Native stablecoin DEX', + desc: 'An enshrined orderbook lets every TIP-20 trade natively, with price-time priority and no pool contract to deploy.', + }, + { + title: 'Shared liquidity', + desc: 'Trade between USD stablecoins through protocol-level order books, so markets can tap existing liquidity.', + }, + ], + }, + { + id: 'standard', + title: 'A stablecoin-native token standard.', + copy: 'TIP-20 is the native token standard for stablecoins. Fee payment, payment lanes, and reconciliation are part of the token, not bolted on.', + spec: feeTokenSpec, + ctas: [ + { label: 'Explore TIP-20', href: '/docs/protocol/tip20/overview', primary: true }, + { label: 'Issue a token', href: '/docs/guide/issuance/create-a-stablecoin' }, + ], + panelTitle: 'fee-token.ts', + variants: feeTokenCodeVariants, + points: [ + { + title: 'Pay fees in any stablecoin', + desc: 'USD-denominated TIP-20 tokens can pay transaction fees directly, no native gas token required.', + spec: feeTokenSpec, + panelTitle: 'fee-token.ts', + variants: feeTokenCodeVariants, + }, + { + title: 'Dedicated payment lanes', + desc: 'Reserved blockspace keeps transfer fees sub-cent and predictable under load.', + spec: paymentLaneSpec, + panelTitle: 'payment-lanes.ts', + variants: paymentLaneCodeVariants, + }, + { + title: 'Transfer memos', + desc: 'Attach 32-byte references to payments for invoice matching and reconciliation.', + spec: transferMemoSpec, + panelTitle: 'memo.ts', + variants: memoCodeVariants, + }, + ], + }, + { + id: 'virtual-addresses', + title: 'Virtual addresses, native to TIP-20.', + copy: 'Give every customer, merchant, or invoice a unique deposit address without managing another wallet. TIP-20 transfers credit the registered master wallet at the protocol layer.', + spec: { + kind: 'forward', + dest: 1, + destLabel: 'MASTER WALLET', + destSub: 'ONE ADDRESS · NO SWEEPS', + sources: [ + { accent: 0, label: 'CUSTOMER A', detail: 'VADDR · AUTO-FWD' }, + { accent: 2, label: 'CUSTOMER B', detail: 'VADDR · AUTO-FWD' }, + { accent: 3, label: 'CUSTOMER C', detail: 'VADDR · AUTO-FWD' }, + ], + }, + ctas: [ + { + label: 'Explore virtual addresses', + href: '/docs/protocol/tip20/virtual-addresses', + primary: true, + }, + ], + panelTitle: 'virtual-address.ts', + variants: virtualAddressCodeVariants, + points: [ + { + title: 'Unique deposit endpoints', + desc: 'Each virtual address can represent a customer, merchant, corridor, or payment flow.', + }, + { + title: 'Protocol-level crediting', + desc: 'Funds sent to a virtual address are credited directly to the registered master wallet.', + }, + { + title: 'No sweep operations', + desc: 'Operators avoid separate sweep transactions, stranded balances, and extra wallet infrastructure.', + }, + ], + }, + { + id: 'compliance', + title: 'Enforce policies at the token level.', + copy: 'Attach issuer rules to a TIP-20 token so every transfer is checked before it settles.', + spec: { + kind: 'gate', + dest: 1, + destLabel: 'TOKEN', + destSub: 'ISSUER POLICY', + sources: [ + { accent: 0, label: 'ALLOWED HOLDER', detail: 'ON WHITELIST' }, + { accent: 2, label: 'APPROVED TRANSFER', detail: 'POLICY OK' }, + { accent: 3, label: 'BLOCKED ADDRESS', detail: 'ON BLACKLIST', blocked: true }, + ], + }, + ctas: [{ label: 'Read the spec', href: '/docs/protocol/tip403/overview', primary: true }], + panelTitle: 'token-policy.ts', + variants: policyCodeVariants, + points: [ + { + title: 'Whitelist holders', + desc: "Allow transfers only when senders and recipients satisfy the issuer's holder policy.", + }, + { + title: 'Block risky addresses', + desc: 'Reject sanctioned, compromised, or restricted addresses before funds can move.', + }, + { + title: 'Control issuer roles', + desc: 'Use admin and pause roles as operational guardrails for policy changes and emergency stops.', + }, + ], + }, +] + +const TOKEN_FAQS: FaqItem[] = [ + { + question: 'What is TIP-20?', + answer: [ + "TIP-20 is Tempo's ", + { + text: 'native token standard', + href: '/docs/protocol/tip20/overview#tip-20-token-standard', + }, + ' for stablecoins. It keeps familiar token operations, then adds ', + { + text: 'payment-specific primitives', + href: '/docs/protocol/tip20/overview#benefits--features-of-tip-20-tokens', + }, + ' like fee payment, memos, issuer controls, policy checks, virtual addresses, and payment lanes.', + ], + }, + { + question: 'How is TIP-20 different from ERC-20?', + answer: [ + 'ERC-20 gives apps a generic token interface. TIP-20 makes stablecoin payments first-class by building in ', + { + text: 'payment metadata', + href: '/docs/protocol/tip20/overview#transfer-memos', + }, + ', ', + { + text: 'compliance policies', + href: '/docs/protocol/tip20/overview#tip-403-transfer-policies', + }, + ', ', + { + text: 'fee handling', + href: '/docs/protocol/tip20/overview#pay-fees-in-any-stablecoin', + }, + ', and ', + { + text: 'predictable payment fees', + href: '/docs/protocol/tip20/overview#get-predictable-payment-fees', + }, + ' instead of making every token rebuild them.', + ], + }, + { + question: 'Can users pay network fees with TIP-20 tokens?', + answer: [ + 'Yes. Apps can ', + { + text: 'pass a feeToken', + href: '/docs/guide/payments/pay-fees-in-any-stablecoin#quick-snippet', + }, + " when sending a transaction, and Tempo's ", + { + text: 'Fee AMM', + href: '/docs/protocol/fees/fee-amm#fee-amm-overview', + }, + ' handles conversion for validators behind the scenes.', + ], + }, + { + question: 'What are payment lanes?', + answer: [ + { + text: 'Payment lanes', + href: '/docs/protocol/blockspace/payment-lane-specification#payment-lane-specification', + }, + ' reserve blockspace for ', + { + text: 'eligible TIP-20 transfers', + href: '/docs/protocol/blockspace/payment-lane-specification#1-transaction-classification', + }, + ', which keeps ordinary payment traffic predictable even when general-purpose activity spikes.', + ], + }, + { + question: 'What are virtual addresses for?', + answer: [ + 'Virtual addresses let an operator issue ', + { + text: 'unique deposit addresses', + href: '/docs/protocol/tip20/virtual-addresses#why-this-feature-exists', + }, + ' for customers, invoices, or partners while ', + { + text: 'funds route to one master account', + href: '/docs/protocol/tip20/virtual-addresses#what-happens-when-someone-sends-funds', + }, + ' during the TIP-20 transfer.', + ], + }, + { + question: 'How do compliance policies work?', + answer: [ + 'TIP-20 tokens can use ', + { + text: 'TIP-403 policies', + href: '/docs/protocol/tip403/spec#usage-with-tip-20-tokens', + }, + ' to enforce issuer rules before transfers settle. Policies can enforce ', + { + text: 'sender, recipient, and token rules', + href: '/docs/protocol/tip403/spec#authorization-logic', + }, + ' for compliance.', + ], + }, +] + +function StoryPointsList({ + activeIndex = 0, + onSelect, + points, + selectable = false, +}: { + activeIndex?: number + onSelect?: (index: number) => void + points: FeaturePoint[] + selectable?: boolean +}) { + const pointGridClass = points.length === 2 ? 'sm:grid-cols-2' : 'sm:grid-cols-3' + + return ( +
    + {points.map((point, i) => { + const content = ( + <> + + +

    + {point.title} +

    +

    + {point.desc} +

    +
    + + ) + + return ( +
  • + {selectable ? ( + + ) : ( +
    {content}
    + )} +
  • + ) + })} +
+ ) +} + +function StorySection({ story, index }: { story: Story; index: number }) { + const [mode, setMode] = useState('visual') + const [activePointIndex, setActivePointIndex] = useState(0) + + const pointsArePanels = story.points.every( + (point) => point.spec && point.panelTitle && point.variants, + ) + const activePoint = pointsArePanels ? story.points[activePointIndex] : undefined + const panelSpec = activePoint?.spec ?? story.spec + const panelTitle = activePoint?.panelTitle ?? story.panelTitle + const panelVariants = activePoint?.variants ?? story.variants + + return ( +
+ + +
+
+
+

+ {story.title} +

+

+ {story.copy} +

+
+ {story.ctas.map((cta) => ( + + ))} +
+
+ + +
+ +
+
+ +
+
+
+ +
+
+
+ +
+
+
+
+
+
+
+ ) +} + +export default function TokensSections() { + return ( + <> + {STORIES.map((story, i) => ( + + ))} + + + ) +} diff --git a/src/marketing/app/features/_components/TransactionsSections.tsx b/src/marketing/app/features/_components/TransactionsSections.tsx new file mode 100644 index 00000000..61a8cb91 --- /dev/null +++ b/src/marketing/app/features/_components/TransactionsSections.tsx @@ -0,0 +1,516 @@ +'use client' + +import Link from 'next/link' +import { Fragment, useState } from 'react' +import Button from '../../_components/Button' +import CodeWindow, { type CodeVariant } from '../../_components/CodeWindow' +import EdgeMarkers from '../../_components/EdgeMarkers' +import ModeToggle, { type ShowcaseMode } from '../../_components/ModeToggle' +import { colorForIndex } from '../../_components/palette' +import Reveal from '../../_components/Reveal' +import { + accessKeyCodeVariants, + batchingCodeVariants, + feeSponsorCodeVariants, + feeTokenCodeVariants, + parallelCodeVariants, + paymentLaneCodeVariants, + schedulingCodeVariants, +} from '../../_components/transactionCodeVariants' +import FeatureDiagram from '../../diagrams/_components/FeatureDiagram' +import type { FeatureDiagramSpec } from '../../diagrams/_lib/featureDiagram' +import FeatureFaq, { type FaqItem } from './FeatureFaq' + +type TransactionPrimitive = { + title: string + desc: string + href: string + panelTitle: string + spec: FeatureDiagramSpec + variants: CodeVariant[] +} + +type PrimitiveGroup = { + id: string + title: string + desc: string + ctas: { label: string; href: string; primary?: boolean }[] + items: TransactionPrimitive[] +} + +const feeItems: TransactionPrimitive[] = [ + { + title: 'Pay fees in stablecoins', + desc: 'Users can pay blockchain fees using any stablecoin they choose.', + href: '/docs/guide/payments/pay-fees-in-any-stablecoin', + panelTitle: 'fee-token.ts', + spec: { + kind: 'feeamm', + user: { accent: 0, label: 'USER', detail: 'SELECTS FEE TOKEN' }, + selectedToken: { accent: 0, symbol: 'USDC' }, + receivedToken: { accent: 1, symbol: 'USDT' }, + ammLabel: 'FEE AMM', + validator: { accent: 1, label: 'VALIDATOR', detail: 'RECEIVES USDT' }, + }, + variants: feeTokenCodeVariants, + }, + { + title: 'Predictable fees', + desc: 'Dedicated payment lanes keep payment and payout fees predictable.', + href: '/docs/protocol/blockspace/payment-lane-specification#motivation', + panelTitle: 'payment-lanes.ts', + spec: { + kind: 'blockspace', + payments: [ + { accent: 3, label: 'PAYMENT', detail: 'FEE $0.001' }, + { accent: 1, label: 'PAYOUT', detail: 'FEE $0.001' }, + ], + general: { accent: 0, label: 'AIRDROP / TRADE', detail: 'FEE $0.01' }, + paymentLaneLabel: 'PAYMENT BLOCKSPACE', + generalLabel: 'GENERAL BLOCKSPACE', + }, + variants: paymentLaneCodeVariants, + }, + { + title: 'Fee sponsorship', + desc: 'Apps and agents can pay on behalf of users.', + href: '/docs/guide/payments/sponsor-user-fees', + panelTitle: 'sponsor-fees.ts', + spec: { + kind: 'sponsor', + user: { accent: 0, label: 'USER', detail: 'SENDS TX' }, + sponsor: { accent: 1, label: 'APP', detail: 'FEE PAYER' }, + txLabel: 'TEMPO TX', + actionLabel: 'PAYMENT', + gasLabel: 'APP', + hubLabel: 'EXECUTES', + caption: 'FEE PAYER BALANCE IS DEBITED', + }, + variants: feeSponsorCodeVariants, + }, +] + +const flexibilityItems: TransactionPrimitive[] = [ + { + title: 'Batching', + desc: 'Bundle multiple calls into one atomic transaction.', + href: '/docs/protocol/transactions#batch-calls', + panelTitle: 'batch.ts', + spec: { + kind: 'batch', + batchLabel: 'BATCH', + calls: [ + { accent: 0, label: 'APPROVE' }, + { accent: 1, label: 'SWAP' }, + { accent: 2, label: 'TRANSFER' }, + ], + sealLabel: 'ONE SIGNATURE', + openLabel: 'OPEN', + closeLabel: 'CLOSE', + hubLabel: 'EXECUTES', + caption: 'ALL CALLS LAND OR NONE DO', + }, + variants: batchingCodeVariants, + }, + { + title: 'Parallelization', + desc: 'Nonce keys let independent transactions execute at the same time.', + href: '/docs/protocol/transactions#concurrent-transactions', + panelTitle: 'parallel.ts', + spec: { + kind: 'lanes', + txs: [ + { accent: 0, label: 'PAYMENT A', detail: 'TOUCHES ACCT A' }, + { accent: 1, label: 'PAYMENT B', detail: 'TOUCHES ACCT B' }, + { accent: 2, label: 'PAYOUT', detail: 'TOUCHES ACCT C' }, + ], + blockLabel: 'ONE BLOCK', + blockSub: 'EXECUTED IN PARALLEL', + }, + variants: parallelCodeVariants, + }, + { + title: 'Scheduling', + desc: 'Transactions can be valid only inside a defined execution window.', + href: '/docs/protocol/transactions#scheduled-transactions', + panelTitle: 'schedule.ts', + spec: { + kind: 'batch', + batchLabel: 'SIGNED TX', + calls: [ + { accent: 1, label: 'PAYROLL' }, + { accent: 2, label: 'INVOICE' }, + ], + sealLabel: 'SIGNED NOW', + openLabel: 'VALID AFTER', + closeLabel: 'VALID BEFORE', + hubLabel: 'EXECUTES', + caption: 'THE NETWORK HONORS THE TIME WINDOW', + }, + variants: schedulingCodeVariants, + }, +] + +const accessKeysSpec: FeatureDiagramSpec = { + kind: 'keys', + account: 1, + accountLabel: 'ACCOUNT', + accountSub: 'ROOT · PASSKEY', + keys: [ + { accent: 0, cap: 0.55, name: 'APP KEY', scope: 'PAYMENTS · ≤ $500/DAY · 7D' }, + { accent: 2, cap: 0.32, name: 'AGENT KEY', scope: 'CHECKOUT · ≤ $100/DAY' }, + { accent: 3, cap: 0, name: 'OLD KEY', scope: 'REVOKED · INSTANTLY', revoked: true }, + ], +} + +const accessKeyItems = [ + { + title: 'Scoped signing', + desc: 'Delegate one flow without delegating the account.', + href: '/docs/protocol/transactions/AccountKeychain#call-scope-enforcement', + }, + { + title: 'Spend limits', + desc: 'Cap what a key can move before it ever signs.', + href: '/docs/protocol/transactions/AccountKeychain#spending-limit-enforcement', + }, + { + title: 'Revocation', + desc: 'Turn off old keys without rotating the root.', + href: '/docs/protocol/transactions/AccountKeychain#key-revocation', + }, +] + +const CODE_WINDOW_HEIGHT = 'max-h-[412px] lg:max-h-[440px]' + +const groups: PrimitiveGroup[] = [ + { + id: 'fees', + title: 'Flexible fees for apps using stablecoins.', + desc: 'Pay fees in supported stablecoins, keep payment costs predictable with dedicated blockspace, and sponsor fees for users.', + ctas: [ + { label: 'Explore transactions', href: '/docs/protocol/transactions', primary: true }, + { label: 'Read docs', href: '/docs/protocol/transactions/spec-tempo-transaction' }, + ], + items: feeItems, + }, + { + id: 'flexibility', + title: 'Transaction controls for production throughput.', + desc: 'Batch calls, parallelize independent transactions, and schedule execution windows for high-volume services.', + ctas: [ + { label: 'Explore transactions', href: '/docs/protocol/transactions', primary: true }, + { label: 'Read docs', href: '/docs/protocol/transactions/spec-tempo-transaction' }, + ], + items: flexibilityItems, + }, +] + +const TRANSACTION_FAQS: FaqItem[] = [ + { + question: 'What is a Tempo Transaction?', + answer: [ + 'A Tempo Transaction is the ', + { + text: 'native transaction type', + href: '/docs/protocol/transactions/spec-tempo-transaction#transaction-type', + }, + ' for payments on Tempo. It combines ', + { + text: 'batching, fee tokens, fee sponsorship, scheduling, access keys, and nonce keys', + href: '/docs/protocol/transactions#properties', + }, + ' without requiring separate paymasters, relayers, or account layers.', + ], + }, + { + question: 'Do users need to hold a gas token?', + answer: [ + 'No. Users can pay fees with ', + { + text: 'configurable fee tokens', + href: '/docs/protocol/transactions#configurable-fee-tokens', + }, + ', or an app can ', + { + text: 'sponsor the fee', + href: '/docs/protocol/transactions#fee-sponsorship', + }, + ' with a fee-payer signature.', + ], + }, + { + question: 'How is fee sponsorship different from a paymaster?', + answer: [ + 'Fee sponsorship is part of the transaction itself. In the ', + { + text: 'fee sponsorship flow', + href: '/docs/protocol/fees/spec-fee#fee-sponsorship-flow', + }, + ', the user signs the action, the sponsor signs the fee-payer portion, and the protocol validates the ', + { + text: 'fee payer signature', + href: '/docs/protocol/transactions/spec-tempo-transaction#fee-payer-signature-details', + }, + ' before debiting the sponsor for fees.', + ], + }, + { + question: 'How does parallelization work?', + answer: [ + { + text: 'Nonce keys', + href: '/docs/protocol/transactions/spec-tempo-transaction#parallelizable-nonces', + }, + ' let ', + { + text: 'concurrent transactions', + href: '/docs/protocol/transactions#concurrent-transactions', + }, + ' advance separately, so one pending transaction does not block unrelated work from the same account.', + ], + }, + { + question: 'Can transactions be scheduled?', + answer: [ + 'Yes. A transaction can include ', + { + text: 'validAfter and validBefore', + href: '/docs/protocol/transactions#scheduled-transactions', + }, + ' fields, and ', + { + text: 'time window validation', + href: '/docs/protocol/transactions#scheduled-transactions', + }, + ' makes it executable only inside a defined time window.', + ], + }, + { + question: 'What are access keys for?', + answer: [ + { + text: 'Access keys', + href: '/docs/protocol/transactions/spec-tempo-transaction#access-keys', + }, + ' let users delegate scoped signing authority to apps or agents. Keys can be limited by ', + { + text: 'scope', + href: '/docs/protocol/transactions/AccountKeychain', + }, + ', ', + { + text: 'spending amount', + href: '/docs/protocol/transactions/AccountKeychain', + }, + ', and ', + { + text: 'expiry', + href: '/docs/protocol/transactions/AccountKeychain', + }, + ', then revoked without rotating the root account key.', + ], + }, +] + +function AccessKeysSection() { + const [mode, setMode] = useState('visual') + + return ( +
+ + + {/* Alternated layout: visual on the left, content on the right. */} +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+
+ +
+
+

+ Set spending limits using access keys. +

+

+ Authorize scoped keys with spending limits and expiry so apps and agents can move + approved funds without repeated user prompts. +

+
+ + +
+
+ +
+ {accessKeyItems.map((item) => ( + +

+ {item.title} +

+

+ {item.desc} +

+ + ))} +
+
+
+
+
+ ) +} + +function PrimitiveGroupSection({ group, index }: { group: PrimitiveGroup; index: number }) { + const [active, setActive] = useState(0) + const [mode, setMode] = useState('visual') + + const selectItem = (index: number) => { + if (index !== active) setActive(index) + } + + return ( +
+ + +
+

+ {group.title} +

+

+ {group.desc} +

+
+ {group.ctas.map((cta) => ( + + ))} +
+
+ + {/* Mirrors the SDK paired-grid: primitive cards on the left drive the + shared diagram/code panel on the right. The dotted icons tie each + card to its accent color in the diagram. */} +
+
    + {group.items.map((item, i) => ( +
  • + selectItem(i)} + onFocus={() => selectItem(i)} + onClick={() => selectItem(i)} + className={`group flex h-full w-full flex-col gap-3 p-7 text-left transition-colors lg:p-8 ${ + active === i + ? 'bg-surface-block text-foreground' + : 'text-foreground/55 hover:bg-surface-block hover:text-foreground/80' + }`} + > + + + + {item.title} + + + + {item.desc} + + +
  • + ))} +
+ +
+
+ +
+
+ {group.items.map((item, i) => ( + +
+ +
+
+
+ +
+
+
+ ))} +
+
+
+
+
+ ) +} + +export default function TransactionsSections() { + return ( + <> + + + + + + ) +} diff --git a/src/marketing/app/page.tsx b/src/marketing/app/page.tsx new file mode 100644 index 00000000..8c9ccf56 --- /dev/null +++ b/src/marketing/app/page.tsx @@ -0,0 +1,43 @@ +import { useEffect, useState } from 'react' +import Footer from './_components/Footer' +import Header from './_components/Header' +import Hero from './_components/Hero' +import HomeShowcases from './_components/HomeShowcases' +import OpenSourceSection from './_components/OpenSourceSection' +import PerfSection from './_components/PerfSection' +import { stats as fallbackStats, fetchStats } from './_components/stats' +import { fetchPerfRuns } from './performance/_lib/runs' + +export default function Home() { + const [stats, setStats] = useState(fallbackStats) + const [runs, setRuns] = useState>>([]) + + useEffect(() => { + let active = true + Promise.all([fetchStats(), fetchPerfRuns()]).then(([perfData, perfRuns]) => { + if (!active) return + setStats(perfData.stats) + setRuns(perfRuns) + }) + return () => { + active = false + } + }, []) + + return ( +
+
+
+ + +
+ +
+
+ +
+
+
+
+ ) +} diff --git a/src/marketing/app/performance/_components/ChartTooltip.tsx b/src/marketing/app/performance/_components/ChartTooltip.tsx new file mode 100644 index 00000000..872c8339 --- /dev/null +++ b/src/marketing/app/performance/_components/ChartTooltip.tsx @@ -0,0 +1,22 @@ +import type { ReactNode } from 'react' + +// Floating data card for chart hover states, clamped inside the chart width. +export default function ChartTooltip({ + x, + width, + children, +}: { + x: number + width: number + children: ReactNode +}) { + const clamped = Math.min(Math.max(x, 110), width - 110) + return ( +
+ {children} +
+ ) +} diff --git a/src/marketing/app/performance/_components/PaymentLanes.tsx b/src/marketing/app/performance/_components/PaymentLanes.tsx new file mode 100644 index 00000000..49bbf58c --- /dev/null +++ b/src/marketing/app/performance/_components/PaymentLanes.tsx @@ -0,0 +1,195 @@ +// biome-ignore-all lint/a11y/noSvgWithoutTitle: The chart SVG is decorative and described by nearby labels. + +'use client' + +import { useEffect, useState } from 'react' +import { linePath, scaleLinear } from '../_lib/chart' +import { fmtInt, type PerfRun } from '../_lib/runs' +import useMeasure from './useMeasure' + +// The payment-lanes story, told with real data: the chart is split into two +// always-visible bands. General blockspace carries the actual settled-TPS +// series from the nightly feed (tens of thousands of TPS); the dedicated +// payment lane below has tiny sub-cent fee movement, with pulses flowing along it. + +// No y-axis on this chart, so the bands run flush to the container's left +// edge; the right padding is the lane for the pinned end labels. On mobile that +// lane is collapsed and the end values move into the zone header rows instead. +const PAD = { l: 0, r: 150, t: 20, b: 24 } +const MOBILE_BP = 480 +const H = 300 +const DIVIDER = 190 +const FEE_Y = (DIVIDER + H - PAD.b) / 2 + 6 +const DRAW_MS = 900 + +export default function PaymentLanes({ runs }: { runs: PerfRun[] }) { + const { ref, width } = useMeasure() + // Draws the load line once the chart is properly on screen. + const [drawn, setDrawn] = useState( + () => + typeof window !== 'undefined' && + window.matchMedia('(prefers-reduced-motion: reduce)').matches, + ) + + useEffect(() => { + const el = ref.current + if (!el || drawn) return + const io = new IntersectionObserver( + ([entry]) => { + if (entry?.isIntersecting) { + setDrawn(true) + io.disconnect() + } + }, + { threshold: 0.5, rootMargin: '0px 0px -20% 0px' }, + ) + io.observe(el) + return () => io.disconnect() + }, [ref, drawn]) + + const mobile = width > 0 && width < MOBILE_BP + const padR = mobile ? 12 : PAD.r + const endX = Math.max(width - padR, PAD.l + 1) + + // Real settled TPS per nightly run, scaled into the general blockspace zone. + const values = runs.map((r) => r.settledTps) + const hasData = values.length >= 2 + const min = hasData ? Math.min(...values) : 0 + const max = hasData ? Math.max(...values) : 1 + const xAt = scaleLinear([0, Math.max(values.length - 1, 1)], [PAD.l, endX]) + const yAt = scaleLinear([min * 0.92, max * 1.08], [DIVIDER - 14, PAD.t + 34]) + const loadPts = values.map((v, i) => [xAt(i), yAt(v)] as [number, number]) + const loadEndY = hasData ? loadPts[loadPts.length - 1][1] : 0 + const feePointCount = Math.max(values.length, 16) + const feeXAt = scaleLinear([0, feePointCount - 1], [PAD.l, endX]) + const feePts = Array.from( + { length: feePointCount }, + (_, i) => + [feeXAt(i), FEE_Y + Math.sin(i * 1.7) * 1.15 + Math.sin(i * 0.55) * 0.55] as [number, number], + ) + const feeEndY = feePts[feePts.length - 1]?.[1] ?? FEE_Y + + return ( +
+ {width > 0 ? ( + + {/* General blockspace zone, carrying the benchmark load. */} + + + GENERAL BLOCKSPACE · SHARED LOAD + + + {/* Dedicated payment lane zone. */} + + + DEDICATED PAYMENT LANE + + + + + {/* Real benchmark load settling in general blockspace. */} + {hasData ? ( + <> + + + {fmtInt(values[values.length - 1])} TPS load + + + ) : null} + + {/* The fee line: near-flat, with micro fluctuations and pulses flowing along the lane. */} + + + + + $0.001 base fee + + + ) : null} +
+ ) +} diff --git a/src/marketing/app/performance/_components/SettlementStream.tsx b/src/marketing/app/performance/_components/SettlementStream.tsx new file mode 100644 index 00000000..5c6ce2e6 --- /dev/null +++ b/src/marketing/app/performance/_components/SettlementStream.tsx @@ -0,0 +1,188 @@ +'use client' + +import { useEffect, useState } from 'react' +import { createPublicClient, http } from 'viem' +import type { PerfRun } from '../_lib/runs' +import useMeasure from './useMeasure' + +// The settlement story as a live conveyor: Viem watches Tempo's finalized +// chain head and pushes each real block into the stream. Since finalized heads +// are already settled, every cell represents an observed finalized block; the +// per-block interval is measured from each block's on-chain millisecond +// timestamp (`timestampMillis`), so it stays accurate even when polling catches +// up several finalized blocks at once. + +// Tempo blocks carry a millisecond-precision Unix timestamp that standard EVM +// blocks lack. It is not part of viem's block type, so we read it off the raw +// block as a hex string. +type TempoBlock = { number: bigint | null; timestampMillis?: `0x${string}` } + +const TEMPO_RPC_URL = 'https://rpc.tempo.xyz' +const TEMPO_EXPLORER_BLOCK_URL = 'https://explore.tempo.xyz/block' +const POLL_MS = 500 + +const H = 180 +const CELL = 64 // square block cell, px +const GAP = 14 +const STEP = CELL + GAP +const TRACK_TOP = (H - CELL) / 2 +const TRACK_H = CELL + 24 // cells plus their height labels +const MAX_BLOCKS = 32 + +type StreamBlock = { + height: bigint + intervalMs: number | null +} + +const client = createPublicClient({ + transport: http(TEMPO_RPC_URL), + pollingInterval: POLL_MS, +}) + +export default function SettlementStream({ runs }: { runs: PerfRun[] }) { + const { ref, width } = useMeasure() + const latest = runs[runs.length - 1] + const intervalMs = Math.min(Math.max(Math.round(latest?.blockTimeMs || 500), 400), 1200) + + const [reducedMotion] = useState( + () => + typeof window !== 'undefined' && + window.matchMedia('(prefers-reduced-motion: reduce)').matches, + ) + const [blocks, setBlocks] = useState([]) + const [isLive, setIsLive] = useState(false) + + useEffect(() => { + let previousTimestampMs: number | null = null + const unwatch = client.watchBlocks({ + blockTag: 'finalized', + emitMissed: true, + emitOnBegin: true, + pollingInterval: POLL_MS, + onBlock: (block) => { + if (block.number === null) return + const rawTimestamp = (block as TempoBlock).timestampMillis + const timestampMs = rawTimestamp != null ? Number(rawTimestamp) : null + const measuredInterval = + timestampMs === null || previousTimestampMs === null + ? null + : Math.max(Math.round(timestampMs - previousTimestampMs), 0) + if (timestampMs !== null) previousTimestampMs = timestampMs + setIsLive(true) + setBlocks((prev) => { + if (prev.some(({ height }) => height === block.number)) return prev + return [...prev, { height: block.number, intervalMs: measuredInterval }].slice( + -MAX_BLOCKS, + ) + }) + }, + onError: () => setIsLive(false), + }) + + return () => unwatch() + }, []) + + // Enough cells to run flush past the container's left edge; the overflow + // is clipped by the track and softened by the fade mask. + const visible = Math.min(Math.max(Math.ceil(width / STEP) + 1, 3), MAX_BLOCKS) + const shown = blocks.slice(-visible) + const last = shown.length - 1 + const observedIntervals = blocks + .map(({ intervalMs }) => intervalMs) + .filter((value): value is number => value !== null) + const avgIntervalMs = + observedIntervals.length > 0 + ? Math.round( + observedIntervals.reduce((sum, value) => sum + value, 0) / observedIntervals.length, + ) + : intervalMs + + return ( +
+ {width > 0 ? ( +
+ {/* "Now" cursor: the live marker sits over the block being built. */} +

+ + FINALIZED +

+ + {/* The conveyor. Cells are anchored to the track's right edge and + positioned by index-from-newest, so when a block arrives every + older cell's transform changes and transitions one step left. */} +
+ {shown.map((b, i) => ( + + ))} + + {shown.length === 0 ? ( +

+ Waiting for finalized blocks… +

+ ) : null} + + {/* Mask the oldest block's exit at the track's left edge. */} +
+
+ + {/* Rolling average interval, measured from finalized block arrivals. */} +
+ + + +
+

+ AVG {observedIntervals.length > 0 ? avgIntervalMs : `~${avgIntervalMs}`} MS +

+
+ ) : null} +
+ ) +} diff --git a/src/marketing/app/performance/_components/TpsTrendChart.tsx b/src/marketing/app/performance/_components/TpsTrendChart.tsx new file mode 100644 index 00000000..c30d666b --- /dev/null +++ b/src/marketing/app/performance/_components/TpsTrendChart.tsx @@ -0,0 +1,223 @@ +// biome-ignore-all lint/a11y/noSvgWithoutTitle: The chart SVG is paired with visible axes, labels, and tooltip text. + +'use client' + +import { useEffect, useState } from 'react' +import { linePath, scaleLinear, ticks } from '../_lib/chart' +import { fmtInt, type PerfRun } from '../_lib/runs' +import ChartTooltip from './ChartTooltip' +import { + TPS_CHART_DEFAULT_DOMAIN, + TPS_CHART_DEFAULT_TICKS, + TPS_CHART_MOBILE_BP, + TpsChartGrid, + tpsChartPad, +} from './TpsTrendChartFrame' +import useMeasure from './useMeasure' + +// Vercel-hero-style throughput chart: settled TPS per nightly run, drawn with +// a theme-aware gradient stroke (userSpaceOnUse so the dots pick up the local +// gradient color) and a hover crosshair + data card. The line draws itself +// left-to-right on mount, with each dot popping in as the stroke reaches it. + +const DRAW_MS = 1600 + +export default function TpsTrendChart({ + runs, + height = 360, +}: { + runs: PerfRun[] + height?: number +}) { + const { ref, width } = useMeasure() + const [hover, setHover] = useState(null) + // Reduced-motion users start (and stay) fully drawn. The SVG only renders + // after the container is measured, so this never affects server HTML. + const [drawn, setDrawn] = useState( + () => + typeof window !== 'undefined' && + window.matchMedia('(prefers-reduced-motion: reduce)').matches, + ) + // True once the draw animation has finished — gates the pinned latest-value + // label so it appears with the final dot but still dims/undims instantly. + const [intro, setIntro] = useState( + () => + typeof window !== 'undefined' && + window.matchMedia('(prefers-reduced-motion: reduce)').matches, + ) + const ready = width > 0 + + // Flip to the drawn state one frame after first paint so the + // stroke-dashoffset transition runs. + useEffect(() => { + if (!ready || drawn) return + let raf2 = 0 + const raf1 = requestAnimationFrame(() => { + raf2 = requestAnimationFrame(() => setDrawn(true)) + }) + return () => { + cancelAnimationFrame(raf1) + cancelAnimationFrame(raf2) + } + }, [ready, drawn]) + + useEffect(() => { + if (!drawn || intro) return + const t = setTimeout(() => setIntro(true), DRAW_MS) + return () => clearTimeout(t) + }, [drawn, intro]) + + if (runs.length < 2) return null + + const PAD = tpsChartPad(width) + const mobile = width > 0 && width < TPS_CHART_MOBILE_BP + const n = runs.length + const values = runs.map((r) => r.settledTps) + const min = Math.min(...values) + const max = Math.max(...values) + const dynamicYDomain = [min * 0.9, max * 1.06] as [number, number] + const stableYDomainFits = + dynamicYDomain[0] >= TPS_CHART_DEFAULT_DOMAIN[0] && + dynamicYDomain[1] <= TPS_CHART_DEFAULT_DOMAIN[1] + const yDomain = stableYDomainFits ? TPS_CHART_DEFAULT_DOMAIN : dynamicYDomain + const yTicks = stableYDomainFits ? TPS_CHART_DEFAULT_TICKS : ticks(yDomain[0], yDomain[1], 4) + + const xAt = scaleLinear([0, n - 1], [PAD.l, Math.max(width - PAD.r, PAD.l + 1)]) + const yAt = scaleLinear(yDomain, [height - PAD.b, PAD.t]) + const points = values.map((v, i) => [xAt(i), yAt(v)] as [number, number]) + + const labelStep = Math.ceil(n / 6) + + const onMove = (e: React.PointerEvent) => { + const rect = e.currentTarget.getBoundingClientRect() + const px = e.clientX - rect.left + const i = Math.round(((px - PAD.l) / (width - PAD.l - PAD.r)) * (n - 1)) + const clamped = Math.min(Math.max(i, 0), n - 1) + setHover(clamped) + } + + const active = hover === null ? null : runs[hover] + const last = n - 1 + + return ( +
+ {width > 0 ? ( + + + + + + + + + + + + {runs.map((r, i) => + i === 0 || i === last || i % labelStep === 0 ? ( + + {r.dateLabel} + + ) : null, + )} + + {hover !== null ? ( + + ) : null} + + + + {points.map(([x, y], i) => ( + + ))} + + {/* Latest value, pinned beside the line's end on desktop (fades in + with the final dot, dims while a hover comparison is open). Hidden + on mobile, where the gutter is collapsed so the line goes wide. */} + {mobile ? null : ( + + {fmtInt(runs[last].settledTps)} + + )} + + setHover(null)} + /> + + ) : null} + + {active && hover !== null ? ( + +

+ {active.dateLabel} · {active.timeLabel} +

+

+ {fmtInt(active.settledTps)}{' '} + transactions per second +

+
+ ) : null} +
+ ) +} diff --git a/src/marketing/app/performance/_components/TpsTrendChartFrame.tsx b/src/marketing/app/performance/_components/TpsTrendChartFrame.tsx new file mode 100644 index 00000000..0b2d93b7 --- /dev/null +++ b/src/marketing/app/performance/_components/TpsTrendChartFrame.tsx @@ -0,0 +1,79 @@ +// biome-ignore-all lint/a11y/noSvgWithoutTitle: The loading SVG is decorative and paired with visible chart context. + +'use client' + +import { scaleLinear } from '../_lib/chart' +import useMeasure from './useMeasure' + +// Below this container width the chart reserves only a tiny right gutter and +// drops the pinned end-label, so the line itself stretches edge-to-edge on +// phones instead of being squeezed by the label lane. +export const TPS_CHART_MOBILE_BP = 480 + +// Right padding reserves room for the pinned latest-value label on desktop; on +// mobile that label is hidden, so the gutter shrinks and the line goes wide. +export function tpsChartPad(width: number) { + const mobile = width > 0 && width < TPS_CHART_MOBILE_BP + return { l: mobile ? 36 : 48, r: mobile ? 14 : 110, t: 20, b: 28 } +} + +export const TPS_CHART_DEFAULT_DOMAIN = [9500, 25_200] as [number, number] +export const TPS_CHART_DEFAULT_TICKS = [10_000, 15_000, 20_000, 25_000] + +export function TpsChartGrid({ + height, + showLabels = true, + width, + yDomain = TPS_CHART_DEFAULT_DOMAIN, + yTicks = TPS_CHART_DEFAULT_TICKS, +}: { + height: number + showLabels?: boolean + width: number + yDomain?: [number, number] + yTicks?: number[] +}) { + const pad = tpsChartPad(width) + const yAt = scaleLinear(yDomain, [height - pad.b, pad.t]) + + return ( + <> + {yTicks.map((t) => ( + + + {showLabels ? ( + + {Math.round(t / 1000)}K + + ) : null} + + ))} + + ) +} + +export default function TpsTrendChartFrame({ + height = 360, + showLabels = false, +}: { + height?: number + showLabels?: boolean +}) { + const { ref, width } = useMeasure() + + return ( +
+ {width > 0 ? ( + + Benchmark chart frame + + + ) : null} +
+ ) +} diff --git a/src/marketing/app/performance/_components/UptimeStrip.tsx b/src/marketing/app/performance/_components/UptimeStrip.tsx new file mode 100644 index 00000000..f01b7f0c --- /dev/null +++ b/src/marketing/app/performance/_components/UptimeStrip.tsx @@ -0,0 +1,162 @@ +// biome-ignore-all lint/a11y/noSvgWithoutTitle: The availability strip is decorative and backed by adjacent status text. + +'use client' + +import { useEffect, useState } from 'react' +import { fmtInt, type PerfRun } from '../_lib/runs' +import ChartTooltip from './ChartTooltip' +import useMeasure from './useMeasure' + +// Status-page-style availability strip: one thin cell per UTC night over the +// last 90 nights, all green — the visual form of the uptime claim. Hovering a +// night with a published benchmark observation shows what the network settled +// that night. Cells pop in left-to-right on scroll. The header badge carries the +// live aggregate state from status.tempo.xyz when the server has it. + +const DAYS = 90 +const H = 48 +const BAR_TOP = 6 +const BAR_BOTTOM = 42 +const GROW_MS = 250 +const STAGGER_MS = 400 + +const nightLabel = (date: string) => + new Date(`${date}T00:00:00Z`).toLocaleString('en-US', { + month: 'short', + day: 'numeric', + timeZone: 'UTC', + }) + +export default function UptimeStrip({ runs, status }: { runs: PerfRun[]; status: string | null }) { + const { ref, width } = useMeasure() + const [hover, setHover] = useState(null) + // Reduced-motion users start (and stay) fully grown; the SVG only renders + // after the container is measured, so this never affects server HTML. + const [grown, setGrown] = useState( + () => + typeof window !== 'undefined' && + window.matchMedia('(prefers-reduced-motion: reduce)').matches, + ) + + // Grow the cells the first time the strip is properly on screen. + useEffect(() => { + const el = ref.current + if (!el || grown) return + const io = new IntersectionObserver( + ([entry]) => { + if (entry?.isIntersecting) { + setGrown(true) + io.disconnect() + } + }, + { threshold: 0.5, rootMargin: '0px 0px -20% 0px' }, + ) + io.observe(el) + return () => io.disconnect() + }, [ref, grown]) + + if (runs.length === 0) return null + + // The strip ends on the latest observed night and runs DAYS back; nights + // with a published run carry its numbers in the tooltip. + const runByDay = new Map(runs.map((r) => [r.startedAt.slice(0, 10), r])) + const end = new Date(`${runs[runs.length - 1].startedAt.slice(0, 10)}T00:00:00Z`) + const nights = Array.from({ length: DAYS }, (_, i) => { + const d = new Date(end) + d.setUTCDate(d.getUTCDate() - (DAYS - 1 - i)) + const date = d.toISOString().slice(0, 10) + return { date, run: runByDay.get(date) ?? null } + }) + + const n = nights.length + const step = width / n + const barW = Math.max(Math.min(step - 2.5, 6), 1.5) + + const onMove = (e: React.PointerEvent) => { + const rect = e.currentTarget.getBoundingClientRect() + const i = Math.floor((e.clientX - rect.left) / step) + setHover(Math.min(Math.max(i, 0), n - 1)) + } + + const active = hover === null ? null : nights[hover] + + return ( +
+
+ {status ? ( +
+ +

+ {status === 'operational' ? 'All systems operational' : status.replace(/_/g, ' ')} +

+
+ ) : null} +

+ Last {DAYS} nights +

+
+ +
+ {width > 0 ? ( + + {nights.map((night, i) => ( + + ))} + + setHover(null)} + /> + + ) : null} + + {active && hover !== null ? ( + +

+ {nightLabel(active.date)} +

+

+ Operational +

+ {active.run ? ( +

+ {fmtInt(active.run.settledTps)} TPS settled · {fmtInt(active.run.blockCount)} blocks +

+ ) : null} +
+ ) : null} +
+ +
+ {nightLabel(nights[0].date)} + {nightLabel(nights[n - 1].date)} +
+
+ ) +} diff --git a/src/marketing/app/performance/_components/useMeasure.ts b/src/marketing/app/performance/_components/useMeasure.ts new file mode 100644 index 00000000..213be133 --- /dev/null +++ b/src/marketing/app/performance/_components/useMeasure.ts @@ -0,0 +1,22 @@ +'use client' + +import { useEffect, useRef, useState } from 'react' + +// Tracks the rendered width of a container so charts can draw at native pixel +// size (crisp text, pixel-perfect tooltips) instead of scaling a fixed viewBox. +export default function useMeasure() { + const ref = useRef(null) + const [width, setWidth] = useState(0) + + useEffect(() => { + const el = ref.current + if (!el) return + const ro = new ResizeObserver(([entry]) => { + if (entry) setWidth(entry.contentRect.width) + }) + ro.observe(el) + return () => ro.disconnect() + }, []) + + return { ref, width } +} diff --git a/src/marketing/app/performance/_lib/chart.ts b/src/marketing/app/performance/_lib/chart.ts new file mode 100644 index 00000000..6fbe3180 --- /dev/null +++ b/src/marketing/app/performance/_lib/chart.ts @@ -0,0 +1,38 @@ +// Minimal SVG chart math shared by the /performance charts: linear scales, +// path builders, and "nice" axis ticks. No rendering — components own that. + +export type Scale = (v: number) => number + +export const scaleLinear = ([d0, d1]: [number, number], [r0, r1]: [number, number]): Scale => { + const span = d1 - d0 || 1 + return (v) => r0 + ((v - d0) / span) * (r1 - r0) +} + +export const linePath = (points: [number, number][]) => + points.map(([x, y], i) => `${i === 0 ? 'M' : 'L'}${x.toFixed(2)} ${y.toFixed(2)}`).join(' ') + +export const areaPath = (points: [number, number][], baselineY: number) => { + const first = points[0] + const last = points[points.length - 1] + return `${linePath(points)} L${last[0].toFixed(2)} ${baselineY.toFixed(2)} L${first[0].toFixed(2)} ${baselineY.toFixed(2)} Z` +} + +// Closed band between an upper and lower series sharing x positions. +export const bandPath = (upper: [number, number][], lower: [number, number][]) => { + const reversed = [...lower].reverse() + return `${linePath(upper)} ${reversed + .map(([x, y]) => `L${x.toFixed(2)} ${y.toFixed(2)}`) + .join(' ')} Z` +} + +// Round-numbered ticks covering [min, max] with roughly `count` steps. +export function ticks(min: number, max: number, count = 4): number[] { + const span = max - min || 1 + const rawStep = span / count + const pow = 10 ** Math.floor(Math.log10(rawStep)) + const step = [1, 2, 2.5, 5, 10].map((m) => m * pow).find((s) => s >= rawStep) ?? 10 * pow + const start = Math.ceil(min / step) * step + const out: number[] = [] + for (let v = start; v <= max + step / 1e6; v += step) out.push(v) + return out +} diff --git a/src/marketing/app/performance/_lib/runs.ts b/src/marketing/app/performance/_lib/runs.ts new file mode 100644 index 00000000..4f110b52 --- /dev/null +++ b/src/marketing/app/performance/_lib/runs.ts @@ -0,0 +1,106 @@ +// Full nightly benchmark history from the perf API. The homepage's +// fetchStats() (app/_components/stats.ts) overlays only the latest run; +// this module fetches the whole feed for the /performance charts. + +const PERF_API_URL = + 'https://pr-77-tempo-apps-internal-perf-public.tempo-dev.workers.dev/api/perf/runs?feed=nightly&limit=100' + +type ApiRun = { + id?: string + scenario?: { id?: string; label?: string; workload?: string } + source?: { ref?: string; commitSha?: string } + startedAt?: string + finishedAt?: string + targetTps?: number + metrics?: { + settledTps?: number + avgGasPerSecond?: number + peakGasPerSecond?: number + avgBlockTimeMs?: number + blockCount?: number + } +} + +export type PerfRun = { + id: string + startedAt: string + dateLabel: string // "Jun 11" + timeLabel: string // "03:14 UTC" + commit: string // short sha + scenarioId: string + scenarioLabel: string + workload: string + targetTps: number + settledTps: number + avgGas: number // gas/s + peakGas: number // gas/s + blockTimeMs: number + blockCount: number + durationMin: number +} + +// Fixed locale + UTC so server-rendered strings never depend on where they +// render (avoids hydration mismatches). +const dateLabel = (iso: string) => + new Date(iso).toLocaleString('en-US', { + month: 'short', + day: 'numeric', + timeZone: 'UTC', + }) + +const timeLabel = (iso: string) => + `${new Date(iso).toLocaleString('en-US', { + hour: '2-digit', + minute: '2-digit', + hour12: false, + timeZone: 'UTC', + })} UTC` + +// Oldest → newest (the API returns newest first). Empty array when the API +// is down or the shape changes; callers render a fallback notice. +export async function fetchPerfRuns(): Promise { + try { + const res = await fetch(PERF_API_URL) + if (!res.ok) return [] + const data = (await res.json()) as { runs?: ApiRun[] } + return (data.runs ?? []) + .filter((r): r is Required> & ApiRun => + Boolean(r.startedAt && r.metrics?.settledTps), + ) + .map((r) => ({ + id: r.id ?? r.startedAt, + startedAt: r.startedAt, + dateLabel: dateLabel(r.startedAt), + timeLabel: timeLabel(r.startedAt), + commit: (r.source?.commitSha ?? '').slice(0, 7), + scenarioId: r.scenario?.id ?? 'unknown', + scenarioLabel: r.scenario?.label ?? 'Benchmark', + workload: r.scenario?.workload ?? '', + targetTps: r.targetTps ?? 0, + settledTps: r.metrics.settledTps ?? 0, + avgGas: r.metrics.avgGasPerSecond ?? 0, + peakGas: r.metrics.peakGasPerSecond ?? 0, + blockTimeMs: r.metrics.avgBlockTimeMs ?? 0, + blockCount: r.metrics.blockCount ?? 0, + durationMin: + r.finishedAt && r.startedAt + ? Math.round( + (new Date(r.finishedAt).getTime() - new Date(r.startedAt).getTime()) / 60_000, + ) + : 0, + })) + .reverse() + } catch { + return [] + } +} + +export const fmtInt = (n: number) => Math.round(n).toLocaleString('en-US') + +export const fmtGgas = (gasPerSecond: number) => (gasPerSecond / 1e9).toFixed(2) + +// Signed percent change, e.g. "+4.2%" / "−1.8%" (typographic minus). +export const fmtDelta = (current: number, previous: number) => { + const pct = ((current - previous) / previous) * 100 + return `${pct >= 0 ? '+' : '−'}${Math.abs(pct).toFixed(1)}%` +} diff --git a/src/marketing/app/performance/page.tsx b/src/marketing/app/performance/page.tsx new file mode 100644 index 00000000..d557eb50 --- /dev/null +++ b/src/marketing/app/performance/page.tsx @@ -0,0 +1,429 @@ +import type { Metadata } from 'next' +import { type ReactNode, useEffect, useState } from 'react' +import ArrowUpRight from '../_components/ArrowUpRight' +import Button from '../_components/Button' +import Footer from '../_components/Footer' +import Header from '../_components/Header' +import Reveal from '../_components/Reveal' +import PaymentLanes from './_components/PaymentLanes' +import SettlementStream from './_components/SettlementStream' +import TpsTrendChart from './_components/TpsTrendChart' +import TpsTrendChartFrame from './_components/TpsTrendChartFrame' +import UptimeStrip from './_components/UptimeStrip' +import { fetchPerfRuns, fmtInt, type PerfRun } from './_lib/runs' + +export const metadata: Metadata = { + title: 'Performance — Tempo Developers', + description: + 'Nightly benchmarks on a live Tempo network: throughput, block times, and execution rates, published as raw runs.', +} + +const STATUS_PAGE_URL = 'https://status.tempo.xyz' +const PERF_DASHBOARD_URL = 'https://perf.tempo.xyz/' +const DAY_MS = 24 * 60 * 60 * 1000 +const HERO_STAT_LABELS = ['Transactions per second', 'Avg block time', 'Base fee'] +const SETTLEMENT_SKELETON_CELLS = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'] +const UPTIME_SKELETON_BARS = Array.from({ length: 90 }, (_, i) => `bar-${i}`) +const prefetchedRuns = typeof window !== 'undefined' ? fetchPerfRuns() : null + +function formatChartRange(startedAt: string, endedAt: string) { + const start = new Date(startedAt) + const end = new Date(endedAt) + const daySpan = Math.max(1, Math.round((end.getTime() - start.getTime()) / DAY_MS)) + const startLabel = start.toLocaleString('en-US', { + month: 'short', + day: 'numeric', + timeZone: 'UTC', + }) + const endLabel = end.toLocaleString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + timeZone: 'UTC', + }) + + return `Benchmark runs · Past ${daySpan} ${ + daySpan === 1 ? 'day' : 'days' + } · ${startLabel} - ${endLabel} UTC` +} + +// Aggregate state ("operational", "downtime", …) from the BetterStack status +// page's public JSON; null when unreachable so the uptime strip falls back to +// a neutral header. +async function fetchStatusState(): Promise { + try { + const res = await fetch(`${STATUS_PAGE_URL}/index.json`) + if (!res.ok) return null + const data = (await res.json()) as { + data?: { attributes?: { aggregate_state?: string } } + } + return data.data?.attributes?.aggregate_state ?? null + } catch { + return null + } +} + +function Section({ + id, + title, + note, + children, +}: { + id: string + title: string + note: string + children: ReactNode +}) { + return ( +
+ +

{title}

+

+ {note} +

+
{children}
+
+
+ ) +} + +function SkeletonBlock({ className }: { className: string }) { + return ( + + ) +} + +function HeroChartSkeleton() { + return ( + + +
+

+ Transactions per second +

+ +
+
+ ) +} + +function HeroChartUnavailable() { + return ( + +
+

+ Benchmark feed unavailable. Charts will return when the API is reachable. +

+
+
+ ) +} + +function HeroStatsSkeleton() { + return ( + +
+ {HERO_STAT_LABELS.map((label, i) => ( +
0 ? 'border-line border-l' : ''}`} + > +

+ {label} +

+ +
+ ))} +
+
+ ) +} + +function SettlementStreamSkeleton() { + return ( +
+ +
+ {SETTLEMENT_SKELETON_CELLS.map((cell) => ( +
+ + +
+ ))} +
+
+
+ + + +
+
+ ) +} + +function PaymentLanesSkeleton() { + const gridLines = [70, 120, 240] + + return ( +
+
+ + + Payment lanes placeholder + {gridLines.map((y) => ( + + ))} + + +
+ +
+ ) +} + +function UptimeStripSkeleton() { + return ( +
+
+ + +
+
+ {UPTIME_SKELETON_BARS.map((bar) => ( + + ))} +
+
+ + +
+
+ ) +} + +function PerformanceSectionsSkeleton() { + return ( + <> +
+ +
+ +
+ + +
+ +
+ + +
+ + ) +} + +export default function PerformancePage() { + const [runs, setRuns] = useState([]) + const [statusState, setStatusState] = useState(null) + const [runsLoaded, setRunsLoaded] = useState(false) + + useEffect(() => { + let active = true + const runsRequest = prefetchedRuns ?? fetchPerfRuns() + + runsRequest + .catch(() => []) + .then((perfRuns) => { + if (!active) return + setRuns(perfRuns) + setRunsLoaded(true) + }) + + fetchStatusState().then((status) => { + if (!active) return + setStatusState(status) + }) + + return () => { + active = false + } + }, []) + + const latest = runs[runs.length - 1] + const hasRuns = runs.length >= 2 + const chartRange = latest && runs[0] ? formatChartRange(runs[0].startedAt, latest.startedAt) : '' + + const heroStats = latest + ? [ + { + label: 'Transactions per second', + value: fmtInt(latest.settledTps), + }, + { + label: 'Avg block time', + value: `${fmtInt(latest.blockTimeMs)} ms`, + }, + { + label: 'Base fee', + value: '$0.001', + }, + ] + : [] + + return ( +
+
+
+ + {/* Hero: headline + live gradient chart of the full nightly feed */} +
+ +

+ Pushing the frontier of blockchain performance. +

+
+ + {!runsLoaded ? ( + + ) : hasRuns ? ( + + +
+

+ Transactions per second +

+

+ {chartRange} +

+
+
+ ) : ( + + )} +
+ + {/* Headline metrics. */} + {!runsLoaded ? ( + + ) : heroStats.length > 0 ? ( + +
+ {heroStats.map((stat, i) => ( +
0 ? 'border-line border-l' : ''}`} + > +

+ {stat.label} +

+

+ {stat.value} +

+
+ ))} +
+
+ ) : null} + + {!runsLoaded ? ( + + ) : hasRuns ? ( + <> +
+ +
+ + {/* Payment lanes: the protocol feature behind the flat fee line. */} +
+ + +
+ +
+ + +
+ + ) : null} + +
+ + + + + Tempo is continuously evolving. + + + Tempo keeps pushing the limits of execution and transaction throughput. The public + performance dashboard has the details and updates nightly as the node software + underlying Tempo improves. + + + + Open performance dashboard + + + + +
+ +
+
+
+
+
+ ) +} diff --git a/src/marketing/index.html b/src/marketing/index.html new file mode 100644 index 00000000..044473f8 --- /dev/null +++ b/src/marketing/index.html @@ -0,0 +1,26 @@ + + + + + + Tempo + + + + + + + + + + + + + + + + +
+ + + diff --git a/src/marketing/main.tsx b/src/marketing/main.tsx new file mode 100644 index 00000000..050531ab --- /dev/null +++ b/src/marketing/main.tsx @@ -0,0 +1,284 @@ +import { + lazy, + type ReactNode, + Suspense, + startTransition, + useCallback, + useEffect, + useRef, + useState, +} from 'react' +import { createRoot } from 'react-dom/client' +import '../pages/_root.css' +import Header from './app/_components/Header' +import TpsTrendChartFrame from './app/performance/_components/TpsTrendChartFrame' +import HomePage from './HomePage' + +const loadDiagramsPage = () => import('./DiagramsPage') +const loadFeaturePage = () => import('./FeaturePage') +const loadPerformancePage = () => import('./PerformancePage') + +const Analytics = lazy(() => + import('@vercel/analytics/react').then((module) => ({ default: module.Analytics })), +) +const SpeedInsights = lazy(() => + import('@vercel/speed-insights/react').then((module) => ({ default: module.SpeedInsights })), +) +const GoogleAnalytics = lazy(() => import('../components/GoogleAnalytics')) +const PostHogSetup = lazy(() => import('../components/PostHogSetup')) +const PerformancePage = lazy(loadPerformancePage) +const DiagramsPage = lazy(loadDiagramsPage) +const FeaturePage = lazy(loadFeaturePage) + +function currentRoute() { + return normalizeRoutePath(window.location.pathname) +} + +function normalizeRoutePath(pathname: string) { + return pathname.replace(/\/$/, '') || '/' +} + +const prefetchedPaths = new Set() + +function prefetchPath(href: string) { + if (!href.startsWith('/') || prefetchedPaths.has(href)) return + prefetchedPaths.add(href) + + const link = document.createElement('link') + link.rel = 'prefetch' + link.href = href + link.as = 'document' + document.head.appendChild(link) +} + +const routeMetadata: Record = { + '/': { + title: 'Tempo', + description: + 'The only blockchain designed for payments. Sub-second transactions, sub-cent fees.', + }, + '/build': { + title: 'Tempo', + description: + 'Build payment products on Tempo with stablecoins, fast settlement, and predictable fees.', + }, + '/build/tempo-transactions': { + title: 'Tempo Transactions', + description: 'Batch, sponsor, schedule, and parallelize payments with Tempo Transactions.', + }, + '/build/tip20-tokens': { + title: 'TIP-20 Tokens', + description: + 'Stablecoin-first Tempo Tokens for payments, fees, memos, policies, and liquidity.', + }, + '/performance': { + title: 'Tempo Performance', + description: + 'Nightly benchmarks on Tempo throughput, block times, execution rates, and uptime.', + }, + '/diagrams': { + title: 'Tempo Diagrams', + description: 'A playground for Tempo diagrams, product visuals, and house-style SVG exports.', + }, +} + +function FallbackSkeleton({ className }: { className: string }) { + return ( + + ) +} + +function PerformanceRouteFallback() { + return ( +
+
+
+
+
+

+ Pushing the frontier of blockchain performance. +

+
+ +
+ +
+

+ Transactions per second +

+ +
+
+
+
+
+ ) +} + +function RouteFallback({ route }: { route: string }) { + if (route === '/performance') return + return null +} + +function isMarketingRoute(pathname: string) { + return normalizeRoutePath(pathname) in routeMetadata +} + +function preloadRoute(pathname: string) { + const route = normalizeRoutePath(pathname) + if (route === '/build/tempo-transactions' || route === '/build/tip20-tokens') { + void loadFeaturePage() + } else if (route === '/performance') { + void loadPerformancePage() + } else if (route === '/diagrams') { + void loadDiagramsPage() + } +} + +function idFromHash(hash: string) { + try { + return decodeURIComponent(hash.slice(1)) + } catch { + return hash.slice(1) + } +} + +function metadataForRoute(path: string) { + if (routeMetadata[path]) return routeMetadata[path] + return routeMetadata['/'] +} + +function applyRouteMetadata(path: string) { + const metadata = metadataForRoute(path) + document.title = metadata.title + document.querySelector('meta[name="description"]')?.setAttribute('content', metadata.description) +} + +function renderRoute(path: string): ReactNode { + if (path === '/' || path === '/build') return + if (path === '/build/tempo-transactions') return + if (path === '/build/tip20-tokens') return + if (path === '/performance') return + if (path === '/diagrams') return + return +} + +function MarketingApp() { + const [route, setRoute] = useState(currentRoute) + const [analyticsReady, setAnalyticsReady] = useState(false) + const routeRef = useRef(route) + const pendingScrollRef = useRef(null) + + const scrollToPendingTarget = useCallback(() => { + if (pendingScrollRef.current === null) return + const hash = pendingScrollRef.current + pendingScrollRef.current = null + + requestAnimationFrame(() => { + if (hash) { + document.getElementById(idFromHash(hash))?.scrollIntoView() + } else { + window.scrollTo({ top: 0, left: 0, behavior: 'instant' }) + } + }) + }, []) + + useEffect(() => { + routeRef.current = route + applyRouteMetadata(route) + scrollToPendingTarget() + }, [route, scrollToPendingTarget]) + + useEffect(() => { + setAnalyticsReady(false) + if ('requestIdleCallback' in window) { + const idleId = window.requestIdleCallback(() => setAnalyticsReady(true), { timeout: 2_000 }) + return () => window.cancelIdleCallback(idleId) + } + const timeoutId = globalThis.setTimeout(() => setAnalyticsReady(true), 1) + return () => globalThis.clearTimeout(timeoutId) + }, []) + + useEffect(() => { + prefetchPath('/docs') + + const update = () => { + startTransition(() => setRoute(currentRoute())) + } + const navigate = (url: URL) => { + const nextRoute = normalizeRoutePath(url.pathname) + pendingScrollRef.current = url.hash + preloadRoute(nextRoute) + window.history.pushState({}, '', `${url.pathname}${url.search}${url.hash}`) + if (nextRoute === routeRef.current) { + applyRouteMetadata(nextRoute) + scrollToPendingTarget() + window.dispatchEvent(new CustomEvent('tempo:navigation')) + return + } + startTransition(() => setRoute(nextRoute)) + window.dispatchEvent(new CustomEvent('tempo:navigation')) + } + const prefetchAnchor = (event: Event) => { + const target = event.target + if (!(target instanceof Element)) return + const anchor = target.closest('a[href]') + if (!(anchor instanceof HTMLAnchorElement)) return + if (anchor.origin !== window.location.origin) return + prefetchPath(anchor.pathname) + if (isMarketingRoute(anchor.pathname)) preloadRoute(anchor.pathname) + } + const clickAnchor = (event: MouseEvent) => { + if (event.defaultPrevented || event.button !== 0) return + if (event.metaKey || event.altKey || event.ctrlKey || event.shiftKey) return + + const target = event.target + if (!(target instanceof Element)) return + const anchor = target.closest('a[href]') + if (!(anchor instanceof HTMLAnchorElement)) return + if (anchor.target && anchor.target !== '_self') return + + const url = new URL(anchor.href) + if (url.origin !== window.location.origin || !isMarketingRoute(url.pathname)) return + + event.preventDefault() + navigate(url) + } + + window.addEventListener('popstate', update) + document.addEventListener('click', clickAnchor) + document.addEventListener('pointerover', prefetchAnchor, { passive: true }) + document.addEventListener('focusin', prefetchAnchor) + return () => { + window.removeEventListener('popstate', update) + document.removeEventListener('click', clickAnchor) + document.removeEventListener('pointerover', prefetchAnchor) + document.removeEventListener('focusin', prefetchAnchor) + } + }, [scrollToPendingTarget]) + + return ( + <> + }>{renderRoute(route)} + {analyticsReady && ( + + + + + + + )} + + ) +} + +const rootElement = document.getElementById('root') + +if (!rootElement) { + throw new Error('Marketing root element was not found') +} + +createRoot(rootElement).render() diff --git a/src/marketing/next-shims.tsx b/src/marketing/next-shims.tsx new file mode 100644 index 00000000..b619c3d4 --- /dev/null +++ b/src/marketing/next-shims.tsx @@ -0,0 +1,62 @@ +import type { AnchorHTMLAttributes, ImgHTMLAttributes, ReactNode } from 'react' + +export type Metadata = Record + +type LinkProps = AnchorHTMLAttributes & { + href: string + children?: ReactNode +} + +const prefetchedPaths = new Set() + +function prefetchPath(href: string) { + if (typeof document === 'undefined') return + if (!href.startsWith('/') || prefetchedPaths.has(href)) return + prefetchedPaths.add(href) + + const link = document.createElement('link') + link.rel = 'prefetch' + link.href = href + link.as = 'document' + document.head.appendChild(link) +} + +export default function Link({ href, children, onFocus, onPointerEnter, ...props }: LinkProps) { + return ( + { + prefetchPath(href) + onFocus?.(event) + }} + onPointerEnter={(event) => { + prefetchPath(href) + onPointerEnter?.(event) + }} + > + {children} + + ) +} + +type ImageProps = ImgHTMLAttributes & { + src: string + alt: string + width?: number + height?: number + priority?: boolean + fill?: boolean +} + +export function Image({ priority: _priority, fill: _fill, alt, ...props }: ImageProps) { + return {alt} +} + +export function usePathname() { + return typeof window === 'undefined' ? '/' : window.location.pathname +} + +export function notFound(): never { + throw new Error('Not found') +} diff --git a/src/marketing/next.d.ts b/src/marketing/next.d.ts new file mode 100644 index 00000000..caeff0f5 --- /dev/null +++ b/src/marketing/next.d.ts @@ -0,0 +1,16 @@ +declare module 'next' { + export type Metadata = Record +} + +declare module 'next/link' { + export { default } from './next-shims' +} + +declare module 'next/image' { + export { Image as default } from './next-shims' +} + +declare module 'next/navigation' { + export function notFound(): never + export function usePathname(): string +} diff --git a/src/pages/_layout.tsx b/src/pages/_layout.tsx index 76e56fdf..8ed446e7 100644 --- a/src/pages/_layout.tsx +++ b/src/pages/_layout.tsx @@ -1,27 +1,6 @@ 'use client' -import { lazy, type PropsWithChildren, Suspense } from 'react' -import { usePageSettled } from '../lib/pageSettled' - -const Analytics = lazy(() => - import('@vercel/analytics/react').then((module) => ({ default: module.Analytics })), -) -const SpeedInsights = lazy(() => - import('@vercel/speed-insights/react').then((module) => ({ default: module.SpeedInsights })), -) -const Toaster = lazy(() => import('sonner').then((module) => ({ default: module.Toaster }))) -const GoogleAnalytics = lazy(() => import('../components/GoogleAnalytics')) -const PostHogSetup = lazy(() => import('../components/PostHogSetup')) - -if (typeof window !== 'undefined') { - window.addEventListener('vite:preloadError', (event) => { - const key = `vite:preloadError:${(event as unknown as CustomEvent).detail?.message}` - if (!sessionStorage.getItem(key)) { - sessionStorage.setItem(key, '1') - window.location.reload() - } - }) -} +import type { PropsWithChildren } from 'react' export default function Layout( props: PropsWithChildren<{ @@ -29,36 +8,5 @@ export default function Layout( frontmatter?: { interactive?: boolean; mipd?: boolean } }>, ) { - const pageSettled = usePageSettled() - const needsToaster = Boolean(props.frontmatter?.interactive || props.frontmatter?.mipd) - - return ( - <> - {props.children} - - {needsToaster && ( - - )} - {pageSettled && ( - <> - - - - - - )} - - - ) + return <>{props.children} } diff --git a/src/pages/_root.css b/src/pages/_root.css index f85b5410..0cb094f0 100644 --- a/src/pages/_root.css +++ b/src/pages/_root.css @@ -1,8 +1,253 @@ -@import "tailwindcss" important; +@import "tailwindcss"; @source "./"; +@source "../marketing"; -@custom-variant dark (&:where([style*="color-scheme: dark"], [style*="color-scheme: dark"] *)); +@custom-variant dark (&:where( + [data-vocs-theme="dark"], + [data-vocs-theme="dark"] *, + [style*="color-scheme: dark"], + [style*="color-scheme: dark"] * + )); + +@font-face { + font-family: "Pilat"; + src: url("/fonts/pilat/Pilat-Book.woff2") format("woff2"); + font-weight: 400; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: "JetBrains Mono"; + src: url("/fonts/jetbrains-mono/JetBrainsMono-Regular.woff2") format("woff2"); + font-weight: 400; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: "JetBrains Mono"; + src: url("/fonts/jetbrains-mono/JetBrainsMono-Medium.woff2") format("woff2"); + font-weight: 500; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: "HBSet"; + src: url("/fonts/hbset/HBSetv0.96-Light.woff2") format("woff2"); + font-weight: 300; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: "HBSet"; + src: url("/fonts/hbset/HBSetv0.96-Regular2.woff2") format("woff2"); + font-weight: 400; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: "HBSet"; + src: url("/fonts/hbset/HBSetv0.96-Medium.woff2") format("woff2"); + font-weight: 500; + font-style: normal; + font-display: swap; +} + +@layer utilities { + :root { + --tempo-docs-outline-width: 280px; + --tempo-docs-shell-width: 100%; + --tempo-docs-sidebar-width: 280px; + --vocs-spacing-topNav: 65px; + } + + html, + body { + background: var(--vocs-background-color-primary); + color: var(--vocs-text-color-primary); + font-family: var(--font-pilat-book), ui-sans-serif, system-ui, sans-serif; + } + + [data-v-gutter-top], + [data-v-gutter-logo] { + display: none; + } + + [data-layout][data-v-sidebar] > [data-v-surface-bg] { + display: none; + } + + [data-layout][data-v-sidebar] > [data-v-gutter-left] { + @media (width >= 1080px) { + left: max(0px, calc((100% - var(--tempo-docs-shell-width)) * 0.5)); + width: var(--tempo-docs-sidebar-width); + justify-content: flex-start; + background: var(--vocs-background-color-primary); + border-right: 1px solid var(--vocs-border-color-primary); + border-left: 1px solid var(--vocs-border-color-primary); + } + } + + [data-v-sidebar-container], + [data-v-sidebar-footer-content], + [data-v-mobile-nav] { + background: var(--vocs-background-color-primary); + } + + [data-v-sidebar-container] { + padding-top: 24px; + scrollbar-width: none; + } + + [data-v-sidebar-container]::-webkit-scrollbar { + display: none; + } + + [data-v-sidebar-curtain] { + background: linear-gradient(to top, transparent, var(--vocs-background-color-primary)); + } + + [data-v-sidebar-footer-curtain] { + background: linear-gradient(to bottom, transparent, var(--vocs-background-color-primary)); + } + + [data-layout][data-v-sidebar] > [data-v-main] { + @media (width >= 1080px) { + width: calc(var(--tempo-docs-shell-width) - var(--tempo-docs-sidebar-width)); + max-width: calc(var(--tempo-docs-shell-width) - var(--tempo-docs-sidebar-width)); + margin-left: calc( + max(0px, calc((100% - var(--tempo-docs-shell-width)) * 0.5)) + + var(--tempo-docs-sidebar-width) + ); + background: var(--vocs-background-color-primary); + border-right: 1px solid var(--vocs-border-color-primary); + min-height: 100vh; + min-height: 100dvh; + } + + @media (width >= 1376px) { + padding-right: var(--tempo-docs-outline-width); + } + } + + [data-v-gutter-right] { + @media (width >= 1376px) { + right: max(0px, calc((100% - var(--tempo-docs-shell-width)) * 0.5)); + width: var(--tempo-docs-outline-width); + background: var(--vocs-background-color-primary); + border-right: 1px solid var(--vocs-border-color-primary); + } + } + + [data-v-sidebar] [data-v-content], + [data-v-sidebar] [data-v-footer] { + background: transparent; + + @media (width >= 1376px) { + margin-left: auto; + margin-right: auto; + } + } + + [data-v-ask-ai-container] { + @media (width < 1376px) { + display: none; + } + + @media (width >= 1376px) { + position: fixed; + top: auto; + bottom: 24px; + left: auto; + right: max(24px, calc((100% - var(--tempo-docs-shell-width)) * 0.5 + 24px)); + width: calc(var(--tempo-docs-outline-width) - 48px); + translate: 0; + transform: none; + } + } +} + +@layer vocs_utilities { + @media (width >= 1376px) { + [data-v-ask-ai-container] > button { + min-width: 0; + width: 100%; + max-width: 100%; + } + } +} + +.nav-active-square > rect { + opacity: 0; + transform-box: fill-box; + transform-origin: center; + animation: nav-active-pixel 460ms steps(3, end) both; +} + +.nav-active-square > rect:nth-child(1) { + animation-delay: 0ms; +} + +.nav-active-square > rect:nth-child(5) { + animation-delay: 45ms; +} + +.nav-active-square > rect:nth-child(9) { + animation-delay: 90ms; +} + +.nav-active-square > rect:nth-child(3) { + animation-delay: 135ms; +} + +.nav-active-square > rect:nth-child(7) { + animation-delay: 180ms; +} + +.nav-active-square > rect:nth-child(2) { + animation-delay: 225ms; +} + +.nav-active-square > rect:nth-child(4) { + animation-delay: 270ms; +} + +.nav-active-square > rect:nth-child(6) { + animation-delay: 315ms; +} + +.nav-active-square > rect:nth-child(8) { + animation-delay: 360ms; +} + +@keyframes nav-active-pixel { + 0% { + opacity: 0; + scale: 0; + } + + 55% { + opacity: 1; + scale: 1.4; + } + + 100% { + opacity: 1; + scale: 1; + } +} + +@media (prefers-reduced-motion: reduce) { + .nav-active-square > rect { + animation: none; + opacity: 1; + } +} @utility scrollbar-gutter-auto { scrollbar-gutter: auto; @@ -23,9 +268,22 @@ /* https://github.com/radix-ui/colors */ /* biome-ignore format: */ @theme { + --font-display: var(--font-hbset); + --font-sans: var(--font-pilat-book), ui-sans-serif, system-ui, sans-serif; + --font-mono: var(--font-jetbrains-mono), ui-monospace, monospace; + --color-black: #000000; --color-white: #ffffff; + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-info: var(--info); + --color-line: var(--line); + --color-on-accent: var(--on-accent); + --color-positive: var(--positive); + --color-surface-input: var(--surface-input); + --color-surface-page: var(--surface-page); + --color-gray1: light-dark(#fcfcfc, #111111); --color-gray2: light-dark(#f9f9f9, #191919); --color-gray3: light-dark(#f0f0f0, #222222); @@ -230,6 +488,647 @@ --radius-default: calc(infinity * 1px); } +/* Marketing surface tokens copied from the developers site. They override the + * Tailwind theme variables at runtime so the root marketing pages can switch + * between the same dark and light palettes while the docs remain Vocs pages. */ +:root { + color-scheme: dark; + + --font-hbset: "HBSet"; + --font-pilat-book: "Pilat"; + --font-jetbrains-mono: "JetBrains Mono"; + --vocs-font-family: var(--font-pilat-book), ui-sans-serif, system-ui, sans-serif; + --vocs-font-family-mono: var(--font-jetbrains-mono), ui-monospace, monospace; + + --color-background: #111111; + --color-foreground: oklch(94.66% 0 0); + --color-foreground-secondary: oklch(70.8% 0 0); + --color-foreground-secondary-hover: oklch(94.66% 0 0); + --color-negative: oklch(71.38% 0.2147 23.49); + --color-info: oklch(71.7% 0.1648 250.79); + --color-positive: oklch(81.51% 0.2258 148.1); + --color-warning: oklch(77.21% 0.1991 64.28); + --color-on-accent: #ffffff; + --color-on-negative: #ffffff; + --color-on-surface-onyx: oklch(94.66% 0 0); + --color-surface-block: #0e0e0e; + --color-surface-block-muted: #121212; + --color-surface-card: #131313; + --color-surface-card-elev: #141414; + --color-surface-input: #222222; + --color-surface-onyx: #000000; + --color-surface-deep: #050505; + --color-surface-skeleton: #292929; + --color-surface-panel: #181818; + --color-surface-page: #0a0a0a; + --color-surface-shell: #0c0c0c; + --color-line: #181818; + --color-line-strong: #2e2e2e; + --color-line-dashed: #888888; + --color-accent-blue: #5d88ff; + --color-indicator-green: #57b88a; + --color-indicator-green-dark: #1d6418; + --color-performance-tps-start: var(--color-accent-blue); + --color-performance-tps-mid: var(--color-indicator-green); + --color-performance-tps-end: var(--color-code-token-number); + --color-canvas-dot-bright: #d9d9d9; + --color-selection-bg: #ffffff; + --color-selection-fg: #000000; + --color-code-token-keyword: #d487f3; + --color-code-token-function: #5d88ff; + --color-code-token-string: #58b88a; + --color-code-token-number: #cde769; + --color-code-token-comment: rgb(255 255 255 / 0.3); + --color-code-token-punctuation: rgb(255 255 255 / 0.45); + --color-prose-body: rgba(255, 255, 255, 0.7); + --color-prose-link-decoration: rgba(255, 255, 255, 0.3); + --color-prose-marker: rgba(255, 255, 255, 0.35); + --color-prose-quote: rgba(255, 255, 255, 0.5); + --color-prose-caption: rgba(255, 255, 255, 0.4); + --color-scrollbar-thumb: #151515; + + --background: var(--color-background); + --foreground: var(--color-foreground); + --foreground-secondary: var(--color-foreground-secondary); + --foreground-secondary-hover: var(--color-foreground-secondary-hover); + --negative: var(--color-negative); + --info: var(--color-info); + --positive: var(--color-positive); + --warning: var(--color-warning); + --on-accent: var(--color-on-accent); + --on-negative: var(--color-on-negative); + --on-surface-onyx: var(--color-on-surface-onyx); + --surface-block: var(--color-surface-block); + --surface-block-muted: var(--color-surface-block-muted); + --surface-card: var(--color-surface-card); + --surface-card-elev: var(--color-surface-card-elev); + --surface-input: var(--color-surface-input); + --surface-onyx: var(--color-surface-onyx); + --surface-deep: var(--color-surface-deep); + --surface-skeleton: var(--color-surface-skeleton); + --surface-panel: var(--color-surface-panel); + --surface-page: var(--color-surface-page); + --surface-shell: var(--color-surface-shell); + --line: var(--color-line); + --line-strong: var(--color-line-strong); + --line-dashed: var(--color-line-dashed); + --accent-blue: var(--color-accent-blue); + --indicator-green: var(--color-indicator-green); + --indicator-green-dark: var(--color-indicator-green-dark); + --performance-tps-start: var(--color-performance-tps-start); + --performance-tps-mid: var(--color-performance-tps-mid); + --performance-tps-end: var(--color-performance-tps-end); + --canvas-dot-rgb: 125, 125, 125; + --canvas-dot-alpha-base: 0.05; + --canvas-dot-bright: var(--color-canvas-dot-bright); + --selection-bg: var(--color-selection-bg); + --selection-fg: var(--color-selection-fg); + --code-token-keyword: var(--color-code-token-keyword); + --code-token-function: var(--color-code-token-function); + --code-token-string: var(--color-code-token-string); + --code-token-number: var(--color-code-token-number); + --code-token-comment: var(--color-code-token-comment); + --code-token-punctuation: var(--color-code-token-punctuation); + --prose-body: var(--color-prose-body); + --prose-link-decoration: var(--color-prose-link-decoration); + --prose-marker: var(--color-prose-marker); + --prose-quote: var(--color-prose-quote); + --prose-caption: var(--color-prose-caption); + --scrollbar-thumb: var(--color-scrollbar-thumb); + + --vocs-color-accent: var(--accent-blue); + --vocs-color-blue: var(--info); + --vocs-color-green: var(--positive); + --vocs-color-red: var(--negative); + --vocs-color-yellow: var(--warning); + --vocs-color-background-primary: var(--surface-shell); + --vocs-background-color-primary: var(--surface-shell); + --vocs-background-color-surface: var(--surface-card); + --vocs-background-color-surfaceMuted: var(--surface-panel); + --vocs-background-color-surfaceTint: var(--surface-block); + --vocs-background-color-code-block: var(--surface-block); + --vocs-background-color-code-highlighted: var(--surface-panel); + --vocs-background-color-inline-code: var(--surface-block); + --vocs-text-color-primary: var(--foreground); + --vocs-text-color-secondary: var(--foreground-secondary); + --vocs-text-color-muted: var(--prose-caption); + --vocs-text-color-heading: var(--foreground); + --vocs-text-color-link: var(--foreground); + --vocs-text-color-link-hover: var(--foreground-secondary-hover); + --vocs-text-color-quote: var(--prose-quote); + --vocs-border-color-primary: var(--line); + --vocs-border-color-secondary: var(--line-strong); + --vocs-border-color-code-highlighted: var(--line-strong); + + --showcase-window-frame: var(--surface-block-muted); + --showcase-window-frame-border: var(--line-strong); + --showcase-window-bg: var(--surface-block); + --showcase-window-chrome: var(--surface-card); + --showcase-window-card: var(--surface-card-elev); + --showcase-window-input: var(--surface-input); + --showcase-window-active: color-mix(in srgb, var(--foreground) 8%, transparent); + --showcase-window-chip: color-mix(in srgb, var(--foreground) 6%, transparent); + --showcase-window-border: var(--line-strong); + --showcase-window-border-strong: color-mix(in srgb, var(--foreground) 28%, transparent); + --showcase-window-dot: var(--surface-skeleton); + --showcase-window-text: var(--foreground); + --showcase-window-muted: var(--foreground-secondary); + --showcase-window-accent: var(--accent-blue); + --showcase-window-accent-text: var(--on-accent); + --showcase-window-positive: var(--indicator-green); + --showcase-window-positive-bg: color-mix(in srgb, var(--indicator-green) 14%, transparent); + --showcase-window-warning: var(--negative); + --showcase-window-warning-bg: color-mix(in srgb, var(--negative) 14%, transparent); +} + +:root:where([data-theme="light"], [data-vocs-theme="light"]) { + color-scheme: light; + + --color-background: #fafafa; + --color-foreground: #111111; + --color-foreground-secondary: #737373; + --color-foreground-secondary-hover: #111111; + --color-on-accent: #ffffff; + --color-on-negative: #ffffff; + --color-on-surface-onyx: #ffffff; + --color-surface-block: #f5f5f5; + --color-surface-block-muted: #f7f7f7; + --color-surface-card: #ffffff; + --color-surface-card-elev: #ffffff; + --color-surface-input: #f5f5f5; + --color-surface-onyx: #111111; + --color-surface-deep: #f4f4f5; + --color-surface-skeleton: #e5e5e5; + --color-surface-panel: #f7f7f7; + --color-surface-page: #fafafa; + --color-surface-shell: #ffffff; + --color-line: #e5e5e5; + --color-line-strong: #d4d4d4; + --color-line-dashed: #a3a3a3; + --color-accent-blue: #3c66d8; + --color-indicator-green: #168f24; + --color-indicator-green-dark: #0f5f18; + --color-performance-tps-start: var(--color-indicator-green); + --color-performance-tps-mid: color-mix( + in srgb, + var(--color-indicator-green) 45%, + var(--color-accent-blue) + ); + --color-performance-tps-end: var(--color-accent-blue); + --color-canvas-dot-bright: #111111; + --color-selection-bg: #111111; + --color-selection-fg: #ffffff; + --color-code-token-keyword: #a93ad4; + --color-code-token-function: var(--color-accent-blue); + --color-code-token-string: #167a52; + --color-code-token-number: #6a7500; + --color-code-token-comment: color-mix(in srgb, var(--color-foreground) 38%, transparent); + --color-code-token-punctuation: color-mix(in srgb, var(--color-foreground) 48%, transparent); + --color-prose-body: color-mix(in srgb, var(--color-foreground) 70%, transparent); + --color-prose-link-decoration: color-mix(in srgb, var(--color-foreground) 28%, transparent); + --color-prose-marker: color-mix(in srgb, var(--color-foreground) 35%, transparent); + --color-prose-quote: color-mix(in srgb, var(--color-foreground) 52%, transparent); + --color-prose-caption: color-mix(in srgb, var(--color-foreground) 45%, transparent); + --color-scrollbar-thumb: #d4d4d4; + --canvas-dot-rgb: 17, 17, 17; + --canvas-dot-alpha-base: 0.045; + --showcase-window-frame: var(--surface-block); + --showcase-window-frame-border: var(--line-strong); + --showcase-window-bg: var(--surface-shell); + --showcase-window-chrome: var(--surface-panel); + --showcase-window-card: var(--surface-card); + --showcase-window-input: var(--surface-card); + --showcase-window-active: var(--surface-block); + --showcase-window-chip: var(--surface-input); + --showcase-window-border: var(--line); + --showcase-window-border-strong: var(--line-strong); + --showcase-window-dot: var(--surface-skeleton); + --showcase-window-text: var(--foreground); + --showcase-window-muted: var(--foreground-secondary); + --showcase-window-accent: var(--accent-blue); + --showcase-window-accent-text: var(--on-accent); + --showcase-window-positive: var(--indicator-green); + --showcase-window-positive-bg: color-mix(in srgb, var(--indicator-green) 12%, transparent); + --showcase-window-warning: var(--negative); + --showcase-window-warning-bg: color-mix(in srgb, var(--negative) 12%, transparent); +} + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-foreground-secondary: var(--foreground-secondary); + --color-foreground-secondary-hover: var(--foreground-secondary-hover); + --color-negative: var(--negative); + --color-info: var(--info); + --color-positive: var(--positive); + --color-warning: var(--warning); + --color-on-accent: var(--on-accent); + --color-on-negative: var(--on-negative); + --color-on-surface-onyx: var(--on-surface-onyx); + + --color-surface-block: var(--surface-block); + --color-surface-block-muted: var(--surface-block-muted); + --color-surface-card: var(--surface-card); + --color-surface-card-elev: var(--surface-card-elev); + --color-surface-input: var(--surface-input); + --color-surface-onyx: var(--surface-onyx); + --color-surface-deep: var(--surface-deep); + --color-surface-skeleton: var(--surface-skeleton); + --color-surface-panel: var(--surface-panel); + --color-surface-page: var(--surface-page); + --color-surface-shell: var(--surface-shell); + + --color-line: var(--line); + --color-line-strong: var(--line-strong); + --color-line-dashed: var(--line-dashed); + + --color-accent-blue: var(--accent-blue); + --color-indicator-green: var(--indicator-green); + + --font-display: var(--font-hbset); + --font-sans: var(--font-pilat-book); + --font-mono: var(--font-jetbrains-mono); +} + +html, +body { + background: var(--background); + color: var(--foreground); +} + +html { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body { + user-select: text; + font-family: var(--font-pilat-book), ui-sans-serif, system-ui, sans-serif; +} + +:where( + a[href], + button:not(:disabled), + summary, + [role="button"], + input[type="button"], + input[type="submit"], + input[type="reset"] + ) { + cursor: pointer; +} + +::selection { + background: var(--selection-bg); + color: var(--selection-fg); +} + +@media (prefers-reduced-motion: no-preference) { + html { + scroll-behavior: smooth; + } +} + +.edge-marker { + background-color: var(--line-dashed); +} + +.repo-brand-square-neutral { + background-color: var(--foreground); +} + +.showcase-visual-frame { + box-shadow: none; +} + +.showcase-visual-card { + border-color: var(--line); + background-color: var(--surface-card); + box-shadow: none; +} + +.showcase-visual-panel { + border-color: var(--line); + background-color: var(--surface-panel); + color: var(--foreground); +} + +.theme-preserve-dark { + color-scheme: dark; + --color-background: #111111; + --color-foreground: oklch(94.66% 0 0); + --color-foreground-secondary: oklch(70.8% 0 0); + --color-foreground-secondary-hover: oklch(94.66% 0 0); + --color-on-accent: #ffffff; + --color-surface-block: #0e0e0e; + --color-surface-card: #131313; + --color-surface-card-elev: #141414; + --color-surface-input: #222222; + --color-surface-onyx: #000000; + --color-surface-panel: #181818; + --color-line: #181818; + --color-line-strong: #2e2e2e; + --background: var(--color-background); + --foreground: var(--color-foreground); + --foreground-secondary: var(--color-foreground-secondary); + --foreground-secondary-hover: var(--color-foreground-secondary-hover); + --on-accent: var(--color-on-accent); + --surface-block: var(--color-surface-block); + --surface-card: var(--color-surface-card); + --surface-card-elev: var(--color-surface-card-elev); + --surface-input: var(--color-surface-input); + --surface-onyx: var(--color-surface-onyx); + --surface-panel: var(--color-surface-panel); + --line: var(--color-line); + --line-strong: var(--color-line-strong); + --code-token-keyword: #d487f3; + --code-token-function: #5d88ff; + --code-token-string: #58b88a; + --code-token-number: #cde769; + --code-token-comment: rgb(255 255 255 / 0.3); + --code-token-punctuation: rgb(255 255 255 / 0.45); +} + +.feature-diagram-mark { + filter: var(--feature-diagram-mark-filter, url("#feature-diagram-glow")); +} + +:root:where([data-theme="light"], [data-vocs-theme="light"]) { + --feature-diagram-mark-filter: none; +} + +/* Typography utilities — Regen design system. Each bundles font-size, + font-weight, line-height, and letter-spacing. font-family inherits, so + compose with `font-mono` etc. when a different family is needed. */ + +@layer utilities { + .heading-64 { + font-size: 64px; + font-weight: 600; + line-height: 1.1; + letter-spacing: 0; + } + .heading-48 { + font-size: 48px; + font-weight: 600; + line-height: 1.15; + letter-spacing: 0; + } + .heading-40 { + font-size: 40px; + font-weight: 600; + line-height: 1.2; + letter-spacing: 0; + } + .heading-32 { + font-size: 32px; + font-weight: 500; + line-height: 1.25; + letter-spacing: 0; + } + .heading-24 { + font-size: 24px; + font-weight: 500; + line-height: 1.35; + letter-spacing: 0; + } + .heading-20 { + font-size: 20px; + font-weight: 500; + line-height: 1.4; + letter-spacing: 0.01em; + } + .heading-16 { + font-size: 16px; + font-weight: 500; + line-height: 1.5; + letter-spacing: 0.01em; + } + + .copy-16 { + font-size: 16px; + font-weight: 400; + line-height: 1.6; + letter-spacing: 0.01em; + } + .copy-15 { + font-size: 15px; + font-weight: 400; + line-height: 1.6; + letter-spacing: 0.01em; + } + .copy-14 { + font-size: 14px; + font-weight: 400; + line-height: 1.6; + letter-spacing: 0.01em; + } + .copy-13 { + font-size: 13px; + font-weight: 400; + line-height: 1.55; + letter-spacing: 0.01em; + } + + .label-16 { + font-size: 16px; + font-weight: 400; + line-height: 1.5; + letter-spacing: 0.01em; + } + .label-15 { + font-size: 15px; + font-weight: 400; + line-height: 1.45; + letter-spacing: 0.01em; + } + .label-14 { + font-size: 14px; + font-weight: 400; + line-height: 1.45; + letter-spacing: 0.01em; + } + .label-13 { + font-size: 13px; + font-weight: 400; + line-height: 1.4; + letter-spacing: 0.01em; + } + .label-12 { + font-size: 12px; + font-weight: 400; + line-height: 1.35; + letter-spacing: 0.01em; + } + + .button-16 { + font-size: 16px; + font-weight: 500; + line-height: 1.5; + letter-spacing: 0.01em; + } + .button-14 { + font-size: 14px; + font-weight: 500; + line-height: 1.45; + letter-spacing: 0.01em; + } + .button-12 { + font-size: 12px; + font-weight: 500; + line-height: 1.35; + letter-spacing: 0.01em; + } + + /* "Live" indicator surface: a slow diagonal sweep between the dark and + bright green tokens. The 200% background-size leaves enough off-canvas + gradient for the sweep to read as continuous flow rather than a pulse. */ + .indicator-flow { + background-image: linear-gradient( + 135deg, + var(--indicator-green-dark), + var(--indicator-green), + var(--indicator-green-dark) + ); + background-size: 200% 200%; + animation: indicator-flow 4s ease-in-out infinite alternate; + } +} + +@keyframes indicator-flow { + 0% { + background-position: 0% 50%; + } + 100% { + background-position: 100% 50%; + } +} + +/* Ring outline expanding and fading out from the Reth badge border on hover. */ +@keyframes ripple { + from { + scale: 1; + opacity: 0.5; + } + to { + scale: 1.45; + opacity: 0; + } +} + +.feature-diagram-hide-small-captions [data-small-caption] { + display: none; +} + +/* Payment-lanes chart on /performance: pulses travel along the flat fee line + (offset matches the dash period 26 + 162) while the general-blockspace + zone's tint slowly breathes. */ +.lane-flow { + animation: lane-flow 2.4s linear infinite; +} +@keyframes lane-flow { + to { + stroke-dashoffset: -188; + } +} +.diagram-flow { + stroke-dasharray: 0.01 10; + stroke-dashoffset: 0; + stroke-linecap: round; +} +@media (prefers-reduced-motion: no-preference) { + .diagram-flow { + animation: diagram-flow 1.4s linear infinite; + } +} +@keyframes diagram-flow { + to { + stroke-dashoffset: -40; + } +} +.zone-breathe { + animation: zone-breathe 4s ease-in-out infinite alternate; +} +@keyframes zone-breathe { + from { + opacity: 0.07; + } + to { + opacity: 0.12; + } +} + +/* Settlement stream on /performance: a new block pops in, its fill bar runs + for one block interval (duration set via --build-ms), and the cell flashes + green once at the moment it settles. */ +.settled-cell { + background-color: color-mix(in srgb, var(--indicator-green) 12%, var(--surface-shell)); +} +.block-in { + animation: block-in 240ms ease-out; +} +@keyframes block-in { + from { + scale: 0.85; + } +} +.build-fill { + animation: build-fill var(--build-ms, 500ms) linear forwards; +} +@keyframes build-fill { + from { + transform: scaleX(0); + } + to { + transform: scaleX(1); + } +} +.settle-flash { + animation: settle-flash 600ms ease-out; +} +@keyframes settle-flash { + from { + background-color: color-mix(in srgb, var(--indicator-green) 28%, var(--surface-shell)); + } +} + +@media (prefers-reduced-motion: reduce) { + .indicator-flow { + animation: none; + } + .lane-flow { + animation: none; + } + .zone-breathe { + animation: none; + } + .block-in { + animation: none; + } + .build-fill { + animation: none; + } + .settle-flash { + animation: none; + } +} + +/* Thin scrollbar for the mobile inline code block. */ +.code-scroll { + scrollbar-width: thin; + scrollbar-color: var(--scrollbar-thumb) transparent; +} +.code-scroll::-webkit-scrollbar { + height: 8px; +} +.code-scroll::-webkit-scrollbar-track { + background: transparent; +} +.code-scroll::-webkit-scrollbar-thumb { + background: var(--scrollbar-thumb); + border-radius: 4px; +} + [data-v-logo] img { height: 20px; margin-top: 2px; diff --git a/src/pages/accounts/api/adapters.mdx b/src/pages/accounts/api/adapters.mdx deleted file mode 100644 index 4195d908..00000000 --- a/src/pages/accounts/api/adapters.mdx +++ /dev/null @@ -1,39 +0,0 @@ ---- -title: Adapters -description: Pluggable adapters for the Tempo Accounts SDK Provider. ---- - -import { Cards, Card } from 'vocs' - -# Adapters - -Adapters control how accounts are created and managed. Pass an adapter to `Provider.create()` to configure the account management strategy. - -| Adapter | Uses | Best for | -| --- | --- | --- | -| [`dialog`](/accounts/api/dialog) (default) | Hosted dialog | Apps wanting a universal account experience | -| [`webAuthn`](/accounts/api/webAuthn) | Domain-bound passkeys | Wallets integrating Passkey accounts, or Apps wanting to host and manage their own Passkey accounts | -| [`local`](/accounts/api/local) | Arbitrary keys | Custom signing and key management | - ---- - - - - - - diff --git a/src/pages/accounts/api/dialog.iframe.mdx b/src/pages/accounts/api/dialog.iframe.mdx deleted file mode 100644 index d780474f..00000000 --- a/src/pages/accounts/api/dialog.iframe.mdx +++ /dev/null @@ -1,20 +0,0 @@ ---- -title: Dialog.iframe -description: Embed the Tempo Wallet auth UI in an iframe dialog element. ---- - -# `Dialog.iframe` - -Creates an iframe dialog that embeds the auth app in a `` element. This is the default on most browsers in secure (HTTPS) contexts. - -Falls back to a popup on Safari (which does not support WebAuthn in cross-origin iframes) and insecure (HTTP) origins. - -## Usage - -```ts -import { dialog, Dialog, Provider } from 'accounts' - -const provider = Provider.create({ - adapter: dialog({ dialog: Dialog.iframe() }), -}) -``` diff --git a/src/pages/accounts/api/dialog.mdx b/src/pages/accounts/api/dialog.mdx deleted file mode 100644 index 2b2ace93..00000000 --- a/src/pages/accounts/api/dialog.mdx +++ /dev/null @@ -1,105 +0,0 @@ ---- -title: dialog -description: Adapter for the Tempo Wallet dialog, an embedded iframe or popup for account management. ---- - -# `dialog` - -Enables universal wallet experiences by delegating signing to an external origin dialog. Also exported as `tempoWallet`. - -## Usage - -```ts twoslash -import { dialog, Provider } from 'accounts' - -const provider = Provider.create({ - adapter: dialog(), -}) -``` - -## Parameters - -### dialog - -- **Type:** `Dialog` -- **Default:** `Dialog.iframe()` (or `Dialog.popup()` in Safari/insecure contexts) - -Dialog to use for the embed app. - -```ts twoslash -import { dialog, Dialog, Provider } from 'accounts' - -const provider = Provider.create({ - adapter: dialog({ - dialog: Dialog.popup(), // [!code focus] - }), -}) -``` - -### host - -- **Type:** `string` -- **Default:** `'https://wallet.tempo.xyz/embed'` - -URL of the embed app. - -```ts twoslash -import { dialog, Provider } from 'accounts' - -const provider = Provider.create({ - adapter: dialog({ - host: 'https://wallet.tempo.xyz/embed', // [!code focus] - }), -}) -``` - -### icon - -- **Type:** `` `data:image/${string}` `` -- **Optional** - -Data URI of the provider icon. - -```ts twoslash -import { dialog, Provider } from 'accounts' - -const provider = Provider.create({ - adapter: dialog({ - icon: 'data:image/svg+xml,...', // [!code focus] - }), -}) -``` - -### name - -- **Type:** `string` -- **Default:** `'Tempo Wallet'` - -Display name of the provider. - -```ts twoslash -import { dialog, Provider } from 'accounts' - -const provider = Provider.create({ - adapter: dialog({ - name: 'My Wallet', // [!code focus] - }), -}) -``` - -### rdns - -- **Type:** `string` -- **Default:** `'xyz.tempo'` - -Reverse DNS identifier for [EIP-6963](https://eips.ethereum.org/EIPS/eip-6963) provider discovery. - -```ts twoslash -import { dialog, Provider } from 'accounts' - -const provider = Provider.create({ - adapter: dialog({ - rdns: 'com.example.wallet', // [!code focus] - }), -}) -``` diff --git a/src/pages/accounts/api/dialog.popup.mdx b/src/pages/accounts/api/dialog.popup.mdx deleted file mode 100644 index 5d287442..00000000 --- a/src/pages/accounts/api/dialog.popup.mdx +++ /dev/null @@ -1,27 +0,0 @@ ---- -title: Dialog.popup -description: Open the Tempo Wallet auth UI in a popup window. ---- - -# `Dialog.popup` - -Opens the auth app in a new browser window. Used as the default on Safari and insecure (HTTP) origins. - -## Usage - -```ts -import { dialog, Dialog, Provider } from 'accounts' - -const provider = Provider.create({ - adapter: dialog({ dialog: Dialog.popup() }), -}) -``` - -## Parameters - -### size - -- **Type:** `{ width: number; height: number }` -- **Default:** `{ width: 360, height: 440 }` - -Popup window dimensions. diff --git a/src/pages/accounts/api/dialogs.mdx b/src/pages/accounts/api/dialogs.mdx deleted file mode 100644 index 8eb95ff5..00000000 --- a/src/pages/accounts/api/dialogs.mdx +++ /dev/null @@ -1,25 +0,0 @@ ---- -title: Dialog -description: Dialog modes for embedding the Tempo Wallet. ---- - -import { Cards, Card } from 'vocs' - -# Dialog - -Dialog modes control how the Tempo Wallet is embedded into your app. Pass a dialog to the `dialog()` adapter to configure the embed mode. - - - - - diff --git a/src/pages/accounts/api/expiry.mdx b/src/pages/accounts/api/expiry.mdx deleted file mode 100644 index 0c5170d6..00000000 --- a/src/pages/accounts/api/expiry.mdx +++ /dev/null @@ -1,47 +0,0 @@ ---- -title: Expiry -description: Utility functions for computing access key expiry timestamps. ---- - -# `Expiry` - -Utility functions that return a Unix timestamp (seconds) offset from the current time. Useful for setting access key expiry. - -## Usage - -```ts -import { Expiry } from 'accounts' - -// Access key that expires in 1 day -const expiry = Expiry.days(1) -``` - -## Functions - -### Expiry.seconds(n) - -Returns a Unix timestamp `n` seconds from now. - -### Expiry.minutes(n) - -Returns a Unix timestamp `n` minutes from now. - -### Expiry.hours(n) - -Returns a Unix timestamp `n` hours from now. - -### Expiry.days(n) - -Returns a Unix timestamp `n` days from now. - -### Expiry.weeks(n) - -Returns a Unix timestamp `n` weeks from now. - -### Expiry.months(n) - -Returns a Unix timestamp `n` months (30 days) from now. - -### Expiry.years(n) - -Returns a Unix timestamp `n` years (365 days) from now. diff --git a/src/pages/accounts/api/local.mdx b/src/pages/accounts/api/local.mdx deleted file mode 100644 index 48c76dde..00000000 --- a/src/pages/accounts/api/local.mdx +++ /dev/null @@ -1,117 +0,0 @@ ---- -title: local -description: Key-agnostic adapter for defining arbitrary account types and signing mechanisms. ---- - -# `local` - -Creates a local adapter where the app manages keys and signing in-process. Use this when you need full control over account creation and transaction signing. - -## Usage - -```ts -import { local, Provider } from 'accounts' - -const provider = Provider.create({ - adapter: local({ - loadAccounts: async () => ({ - accounts: [{ address: '0x...' }], - }), - }), -}) -``` - -## Parameters - -### createAccount - -- **Type:** `(params) => Promise<{ accounts: Account[] }>` -- **Optional** - -Create a new account. Omit for login-only flows. - -```ts twoslash -import { local, Provider } from 'accounts' - -const provider = Provider.create({ - adapter: local({ - loadAccounts: async () => ({ accounts: [{ address: '0x...' }] }), - createAccount: async () => ({ // [!code focus] - accounts: [{ address: '0x...' }], // [!code focus] - }), // [!code focus] - }), -}) -``` - -### icon - -- **Type:** `` `data:image/${string}` `` -- **Optional** - -Data URI of the provider icon. - -```ts twoslash -import { local, Provider } from 'accounts' - -const provider = Provider.create({ - adapter: local({ - loadAccounts: async () => ({ accounts: [{ address: '0x...' }] }), - icon: 'data:image/svg+xml,...', // [!code focus] - }), -}) -``` - -### loadAccounts - -- **Type:** `(params?) => Promise<{ accounts: Account[] }>` -- **Required** - -Discover existing accounts (e.g. via WebAuthn assertion or key lookup). - -```ts twoslash -import { local, Provider } from 'accounts' - -const provider = Provider.create({ - adapter: local({ - loadAccounts: async () => ({ // [!code focus] - accounts: [{ address: '0x...' }], // [!code focus] - }), // [!code focus] - }), -}) -``` - -### name - -- **Type:** `string` -- **Default:** `'Injected Wallet'` - -Display name of the provider. - -```ts twoslash -import { local, Provider } from 'accounts' - -const provider = Provider.create({ - adapter: local({ - loadAccounts: async () => ({ accounts: [{ address: '0x...' }] }), - name: 'My Wallet', // [!code focus] - }), -}) -``` - -### rdns - -- **Type:** `string` -- **Optional** - -Reverse DNS identifier. - -```ts twoslash -import { local, Provider } from 'accounts' - -const provider = Provider.create({ - adapter: local({ - loadAccounts: async () => ({ accounts: [{ address: '0x...' }] }), - rdns: 'com.example.wallet', // [!code focus] - }), -}) -``` diff --git a/src/pages/accounts/api/provider.mdx b/src/pages/accounts/api/provider.mdx deleted file mode 100644 index 60024c9d..00000000 --- a/src/pages/accounts/api/provider.mdx +++ /dev/null @@ -1,197 +0,0 @@ ---- -title: Provider -description: Create an EIP-1193 provider for managing accounts on Tempo. ---- - -# `Provider` - -Creates an EIP-1193 provider with a pluggable adapter for managing accounts on Tempo. - -## Usage - -```ts twoslash -import { Provider } from 'accounts' - -const provider = Provider.create() -``` - -### Using JSON-RPC Methods - -You can interact with accounts via the provider's `request` method. - -```ts twoslash -// @noErrors -import { Provider } from 'accounts' - -const provider = Provider.create() - -const { accounts } = await provider.request({ // [!code focus] - method: 'wallet_connect', // [!code focus] -}) // [!code focus] -``` - -### Using Viem Actions - -If you prefer to use Viem actions over raw JSON-RPC calls, the provider exposes a `getClient` accessor. - -```ts twoslash -// @noErrors -import { parseUnits } from 'viem' -import { tempoActions } from 'viem/tempo' -import { Provider } from 'accounts' - -const provider = Provider.create() - -const client = provider.getClient().extend(tempoActions()) // [!code focus] - -const { receipt } = await client.token.transferSync({ // [!code focus] - amount: parseUnits('100', 6), // [!code focus] - to: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb', // [!code focus] - token: '0x20c0000000000000000000000000000000000001', // [!code focus] -}) // [!code focus] -``` - -## Parameters - -### adapter - -- **Type:** `Adapter` -- **Default:** `dialog()` - -Adapter to use for account management. - -```ts twoslash -import { Provider, webAuthn } from 'accounts' - -const provider = Provider.create({ - adapter: webAuthn({ authUrl: '/auth' }), // [!code focus] -}) -``` - -### authorizeAccessKey - -- **Type:** `() => { expiry: number; limits?: { token: Address; limit: bigint }[] }` -- **Optional** - -Default access key parameters for `wallet_connect`. When set, `wallet_connect` will automatically authorize an access key. - -```ts twoslash -import { parseUnits } from 'viem' -import { Expiry, Provider } from 'accounts' - -const provider = Provider.create({ - authorizeAccessKey: () => ({ // [!code focus] - expiry: Expiry.days(7), // [!code focus] - limits: [{ // [!code focus] - token: '0x20c0000000000000000000000000000000000001', // [!code focus] - limit: parseUnits('500', 6), // [!code focus] - }], // [!code focus] - }), // [!code focus] -}) -``` - -### chains - -- **Type:** `readonly [Chain, ...Chain[]]` -- **Default:** `[tempo, tempoModerato]` - -Supported chains. The first chain is the default. - -```ts twoslash -import { Provider } from 'accounts' -import { tempo } from 'viem/chains' - -const provider = Provider.create({ - chains: [tempo], // [!code focus] -}) -``` - -### feePayer - -- **Type:** `string | { url: string; precedence?: 'fee-payer-first' | 'user-first' }` -- **Optional** - -Fee payer configuration for interacting with a service running [`Handler.relay`](/accounts/server/handler.relay) from `accounts/server`. Pass a URL string, or an object with `url` and optional `precedence` to control whether the fee payer or the user pays first. - -```ts twoslash -import { Provider } from 'accounts' - -const provider = Provider.create({ - feePayer: 'https://myapp.com/fee-payer', // [!code focus] -}) -``` - -Or with precedence: - -```ts twoslash -import { Provider } from 'accounts' - -const provider = Provider.create({ - feePayer: { // [!code focus] - url: 'https://myapp.com/fee-payer', // [!code focus] - precedence: 'user-first', // [!code focus] - }, // [!code focus] -}) -``` - -### mpp - -- **Type:** `boolean` -- **Default:** `false` - -Enable [Machine Payment Protocol](https://mpp.dev) support. - -```ts twoslash -import { Provider } from 'accounts' - -const provider = Provider.create({ - mpp: true, // [!code focus] -}) -``` - -### storage - -- **Type:** `Storage` -- **Default:** `Storage.idb()` in browser, `Storage.memory()` otherwise - -Storage adapter for persistence. - -```ts twoslash -import { Provider, Storage } from 'accounts' - -const provider = Provider.create({ - storage: Storage.memory(), // [!code focus] -}) -``` - -### testnet - -- **Type:** `boolean` -- **Default:** `false` - -Use testnet. When `true`, the default chain will be the first testnet chain in `chains`. - -```ts twoslash -import { Provider } from 'accounts' - -const provider = Provider.create({ - testnet: true, // [!code focus] -}) -``` - -## Return Type - -```ts -type ReturnType = { - /** EIP-1193 request method. */ - request(args: RequestArguments): Promise - /** Returns a Viem Client for the current (or specified) chain. */ - getClient(options?: { chainId?: number }): Client - /** Returns a Viem Account for the given address (or active account). */ - getAccount(options?: { address?: Address }): Account - /** Reactive state store with account and chain state. */ - store: Store - /** Configured chains. */ - chains: readonly [Chain, ...Chain[]] -} -``` diff --git a/src/pages/accounts/api/webAuthn.mdx b/src/pages/accounts/api/webAuthn.mdx deleted file mode 100644 index b475acba..00000000 --- a/src/pages/accounts/api/webAuthn.mdx +++ /dev/null @@ -1,116 +0,0 @@ ---- -title: webAuthn -description: Adapter for passkey-based accounts using WebAuthn registration and authentication. ---- - -# `webAuthn` - -Enables domain-bound passkey experiences by wrapping the `local` adapter with WebAuthn registration and authentication flows. - -## Usage - -```ts -import { webAuthn, Provider } from 'accounts' - -const provider = Provider.create({ - adapter: webAuthn({ authUrl: '/auth' }), -}) -``` - -## Parameters - -### authUrl - -- **Type:** `string` -- **Optional** - -URL of a [WebAuthn server handler](/accounts/server/handler.webAuthn) (shorthand for `WebAuthnCeremony.server({ url }){:js}`) - -:::warning -Cannot be used with `ceremony`. -::: - -```ts twoslash -import { webAuthn, Provider } from 'accounts' - -const provider = Provider.create({ - adapter: webAuthn({ - authUrl: '/auth', // [!code focus] - }), -}) -``` - -### ceremony - -- **Type:** `WebAuthnCeremony` -- **Default:** `WebAuthnCeremony.server({ url: authUrl }){:js}` - -Ceremony strategy for WebAuthn registration and authentication. [See more](/accounts/api/webauthnceremony). - -:::warning -Cannot be used with `authUrl`. -::: - -```ts twoslash -import { webAuthn, WebAuthnCeremony, Provider } from 'accounts' - -const provider = Provider.create({ - adapter: webAuthn({ - ceremony: WebAuthnCeremony.server({ url: '/auth' }), // [!code focus] - }), -}) -``` - -### icon - -- **Type:** `` `data:image/${string}` `` -- **Optional** - -Data URI of the provider icon. - -```ts twoslash -import { webAuthn, Provider } from 'accounts' - -const provider = Provider.create({ - adapter: webAuthn({ - authUrl: '/auth', - icon: 'data:image/svg+xml,...', // [!code focus] - }), -}) -``` - -### name - -- **Type:** `string` -- **Default:** `'Injected Wallet'` - -Display name of the provider. - -```ts twoslash -import { webAuthn, Provider } from 'accounts' - -const provider = Provider.create({ - adapter: webAuthn({ - authUrl: '/auth', - name: 'My Wallet', // [!code focus] - }), -}) -``` - -### rdns - -- **Type:** `string` -- **Optional** - -Reverse DNS identifier. - -```ts twoslash -import { webAuthn, Provider } from 'accounts' - -const provider = Provider.create({ - adapter: webAuthn({ - authUrl: '/auth', - rdns: 'com.example.wallet', // [!code focus] - }), -}) -``` diff --git a/src/pages/accounts/api/webauthnceremony.from.mdx b/src/pages/accounts/api/webauthnceremony.from.mdx deleted file mode 100644 index f8e6b4a2..00000000 --- a/src/pages/accounts/api/webauthnceremony.from.mdx +++ /dev/null @@ -1,47 +0,0 @@ ---- -title: WebAuthnCeremony.from -description: Create a WebAuthnCeremony from a custom implementation. ---- - -# `WebAuthnCeremony.from` - -Creates a `WebAuthnCeremony` from a custom implementation. - -## Usage - -```ts -import { WebAuthnCeremony } from 'accounts' - -const ceremony = WebAuthnCeremony.from({ - async getRegistrationOptions(params) { /* ... */ }, - async verifyRegistration(credential) { /* ... */ }, - async getAuthenticationOptions(params) { /* ... */ }, - async verifyAuthentication(response) { /* ... */ }, -}) -``` - -## Properties - -### getRegistrationOptions - -- **Type:** `(params: { name: string; userId?: string; excludeCredentialIds?: readonly string[] }) => Promise<{ options: Registration.Options }>` - -Get credential creation options for `navigator.credentials.create(){:js}`. - -### verifyRegistration - -- **Type:** `(credential: Registration.Credential, options?: { name?: string }) => Promise<{ credentialId: string; publicKey: Hex }>` - -Verify a registration response and extract the public key. - -### getAuthenticationOptions - -- **Type:** `(params?: { allowCredentialIds?: readonly string[]; challenge?: Hex; credentialId?: string; mediation?: 'conditional' | 'optional' | 'required' | 'silent' }) => Promise<{ options: Authentication.Options }>` - -Get credential request options for `navigator.credentials.get(){:js}`. - -### verifyAuthentication - -- **Type:** `(response: Authentication.Response) => Promise<{ credentialId: string; publicKey: Hex; userId?: string }>` - -Verify an authentication response and extract the public key. diff --git a/src/pages/accounts/api/webauthnceremony.mdx b/src/pages/accounts/api/webauthnceremony.mdx deleted file mode 100644 index d7002b1d..00000000 --- a/src/pages/accounts/api/webauthnceremony.mdx +++ /dev/null @@ -1,25 +0,0 @@ ---- -title: WebAuthnCeremony -description: Pluggable strategy for WebAuthn registration and authentication ceremonies. ---- - -import { Cards, Card } from 'vocs' - -# `WebAuthnCeremony` - -Pluggable strategy for WebAuthn registration and authentication ceremonies. A `WebAuthnCeremony` controls how challenges are generated and responses are verified during passkey registration and login. - - - - - diff --git a/src/pages/accounts/api/webauthnceremony.server.mdx b/src/pages/accounts/api/webauthnceremony.server.mdx deleted file mode 100644 index 9f64317b..00000000 --- a/src/pages/accounts/api/webauthnceremony.server.mdx +++ /dev/null @@ -1,25 +0,0 @@ ---- -title: WebAuthnCeremony.server -description: Server-backed WebAuthn ceremony that delegates to a remote handler. ---- - -# `WebAuthnCeremony.server` - -Creates a server-backed ceremony that delegates to a remote [WebAuthn server handler](/accounts/server/handler.webAuthn). All challenge generation, verification, and credential storage happen server-side. - -## Usage - -```ts -import { WebAuthnCeremony } from 'accounts' - -const ceremony = WebAuthnCeremony.server({ url: 'https://example.com/webauthn' }) -``` - -## Parameters - -### url - -- **Type:** `string` -- **Required** - -Base URL of the WebAuthn handler (e.g. `"https://example.com/webauthn"{:js}`). diff --git a/src/pages/accounts/faq.mdx b/src/pages/accounts/faq.mdx deleted file mode 100644 index c9038293..00000000 --- a/src/pages/accounts/faq.mdx +++ /dev/null @@ -1,54 +0,0 @@ ---- -title: FAQ -description: Frequently asked questions about the Tempo Accounts SDK. ---- - -# Frequently Asked Questions - -## What adapter should I use? - -Most apps should use [Tempo Wallet](/accounts/api/dialog) - it provides a universal account experience with a central account for users that includes embedded onramp, access keys, and transaction orchestration out of the box. - -Use [domain-bound passkeys](/accounts/api/webAuthn) when you want to build your own wallet experience or when your app needs to manage and own the WebAuthn ceremony directly. - -[Learn more](/accounts/guides/create-and-use-accounts). - -## Which browsers are supported? - -The Tempo Accounts SDK supports the following browsers: - -- Safari -- Chrome -- Firefox -- Brave -- Arc - -If a browser is not listed above, it likely works but is not officially supported. - -## Which password managers are supported? - -The Tempo Accounts SDK supports the following password managers: - -- iCloud Keychain -- Google Password Manager -- 1Password -- Bitwarden - -If a password manager is not listed above, it likely works but is not officially supported. - -## Which operating systems are supported? - -The Tempo Accounts SDK supports the following operating systems: - -- iOS -- iPadOS -- macOS -- Android -- Linux -- Windows - -If an operating system is not listed above, it likely works but is not officially supported. - -## How does the Tempo Accounts SDK work with my Content Security Policy? - -If you've deployed a Content Security Policy, see the [Deploying to Production](/accounts/production) page for the full set of required directives. diff --git a/src/pages/accounts/guides/create-and-use-accounts.mdx b/src/pages/accounts/guides/create-and-use-accounts.mdx deleted file mode 100644 index 00ee504a..00000000 --- a/src/pages/accounts/guides/create-and-use-accounts.mdx +++ /dev/null @@ -1,45 +0,0 @@ ---- -title: Create & Use Accounts -description: Choose between universal wallet experiences or domain-bound passkey accounts for your app. ---- - -import { Cards, Card } from 'vocs' - -# Create & Use Accounts - -Create and integrate Tempo accounts into your product with the universal Tempo Wallet or domain-bound passkeys. - -## What should I use? - -### Tempo Wallet - -Most apps should use [Tempo Wallet](/accounts/api/dialog) - it provides a universal account experience with a central account for users that includes embedded onramp, access keys, and transaction orchestration out of the box. - -### Domain-bound Passkeys - -Use [domain-bound passkeys](/accounts/api/webAuthn) when you want to build your own wallet experience or when your app needs to manage and own the WebAuthn ceremony directly. - -:::info -Tempo Wallet uses domain-bound passkeys (on `tempo.xyz`) under the hood. -::: - -### Other Wallets - -Offer users a range of existing wallets to connect from, such as MetaMask, Coinbase Wallet, and others. [See the guide](/guide/use-accounts/connect-to-wallets). - ---- - - - - - diff --git a/src/pages/accounts/index.mdx b/src/pages/accounts/index.mdx deleted file mode 100644 index 326c9d53..00000000 --- a/src/pages/accounts/index.mdx +++ /dev/null @@ -1,300 +0,0 @@ ---- -title: Accounts SDK – Getting Started -description: Set up the Tempo Accounts SDK to create, manage, and interact with accounts on Tempo. -interactive: true ---- - -import { Cards, Card } from 'vocs' -import * as Demo from '../../components/guides/Demo.tsx' -import { AddFunds } from '../../components/guides/steps/payments/AddFunds.tsx' -import { Connect } from '../../components/guides/steps/auth/Connect.tsx' -import { SendPayment } from '../../components/guides/steps/payments/SendPayment.tsx' -import IconGitHub from '~icons/simple-icons/github' -import { StaticMermaidDiagram } from '../../components/StaticMermaidDiagram' - -# Getting Started - -## Overview - -The Tempo Accounts SDK is a TypeScript library for applications and wallets to create, manage, and interact with accounts on Tempo. - -### Demo - -Try sign in and make a payment on Tempo using the Accounts SDK. - - - - - - - -### Quick Prompt - -You can integrate the Accounts SDK, and achieve a flow like the demo above by prompting your agent: - -``` -Read docs.tempo.xyz/accounts and integrate Tempo Wallet into my application -``` - -### Architecture - -The Accounts SDK is a router between your application and a signing [Adapter](/accounts/api/adapters) (e.g. the Tempo Wallet dialog), exposed as an [EIP-1193 Provider](https://eips.ethereum.org/EIPS/eip-1193). - -Adapters handle where keys live and who handles signing. The Accounts SDK currently ships with two adapters: - -| Adapter | Description | -| --- | --- | -| [**Tempo Wallet**](/guide/use-accounts/embed-tempo-wallet) | Delegates signing to [`wallet.tempo.xyz`](https://wallet.tempo.xyz) via an iframe dialog or popup. One passkey, one account, portable across apps. -| [**Domain-bound Passkeys**](/guide/use-accounts/embed-passkeys) | The app (or wallet) handles passkey ceremonies and signing in-process on its own domain. - -Your application interacts with the SDK through standard JSON-RPC methods (via [Wagmi](https://wagmi.sh/) or [Viem](https://viem.sh/)). - -An example of a high-level flow of an Application requesting for a user to perform an action with their Tempo Wallet is shown below: - ->S: action request - S->>W: routes request - note over W: User approves with Passkey - W-->>S: result - S-->>A: result -`} /> - -## Install - -The Tempo Accounts SDK is available as an [NPM package](https://www.npmjs.com/package/accounts) under `accounts` - -:::code-group -```bash [npm] -npm i accounts -``` -```bash [pnpm] -pnpm i accounts -``` -```bash [bun] -bun i accounts -``` -::: - -## Wagmi Usage - -The Tempo Accounts SDK is best used in conjunction with [Wagmi](https://wagmi.sh/) to provide a seamless experience for developers and end-users. - -::::steps - -### Set up Wagmi - -Get started with Wagmi by following the [official guide](https://wagmi.sh/react/getting-started). - -### Configure - -After you have set up Wagmi, you can configure the Tempo Accounts SDK with `tempoWallet` from `wagmi/connectors` or `webAuthn` from `wagmi/tempo`. - -:::code-group -```tsx twoslash [Tempo Wallet] -import { createConfig, http } from 'wagmi' -import { tempo } from 'wagmi/chains' -import { tempoWallet } from 'wagmi/connectors' // [!code hl] - -export const wagmiConfig = createConfig({ - chains: [tempo], - connectors: [tempoWallet()], // [!code hl] - transports: { - [tempo.id]: http(), - }, -}) -``` -```tsx twoslash [Domain-bound Passkeys] -import { createConfig, http } from 'wagmi' -import { tempo } from 'wagmi/chains' -import { webAuthn } from 'wagmi/tempo' // [!code hl] - -export const wagmiConfig = createConfig({ - chains: [tempo], - connectors: [webAuthn({ authUrl: '/auth' })], // [!code hl] - transports: { - [tempo.id]: http(), - }, -}) -``` -::: - -:::info[What connector should I use?] -Most apps should use **Tempo Wallet** - it provides a universal account experience with embedded onramp, access keys, and transaction orchestration. **Domain-bound Passkeys** is for apps that want to manage domain-bound passkey accounts directly. [Learn more](/accounts/faq#what-connector-should-i-use). -::: - -:::tip -If you are using a wallet connection library and cannot supply a custom connector, -you can use `Provider.create()` to create a new provider instance, and inject itself -into the wallet connection library via [EIP-6963](https://eips.ethereum.org/EIPS/eip-6963). - -```tsx -import { Provider } from 'accounts' -Provider.create() -``` -::: - -### Use Accounts - -You can now use [Wagmi Hooks](https://wagmi.sh/react/api/hooks) like `useConnect`, or [Tempo Hooks](https://wagmi.sh/tempo) like `useTransfer`. - -```tsx twoslash -// @noErrors -import { useConnect, useConnectors } from 'wagmi' - -function Connect() { - const connect = useConnect() - const connectors = useConnectors() - - return connectors?.map((connector) => ( - - )) -} -``` - -### Next Steps - - - - - - - - - - -:::: - -## Vanilla + Viem Usage - -You can get started with the Tempo Accounts SDK by creating a new `Provider` instance. -Once set up, you can use the provider to interact with accounts via JSON-RPC, or use [Viem Actions](https://viem.sh/tempo) with `.getClient(){:js}`. - -```tsx twoslash -// @noErrors -import { Provider } from 'accounts' -import { parseUnits } from 'viem' -import { tempoActions } from 'viem/tempo' - -const provider = Provider.create() - -const { accounts } = await provider.request({ - method: 'wallet_connect', -}) - -const client = provider.getClient().extend(tempoActions()) - -const { receipt } = await client.token.transferSync({ - token: '0x20c0000000000000000000000000000000000001', - to: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb', - amount: parseUnits('10', 6), -}) -``` - -## Examples - -Check out these examples to get started with the Tempo Accounts SDK. - -| Example | Description | | -| --- | --- | --- | -| [Embed Tempo Wallet](https://github.com/tempoxyz/accounts/tree/main/examples/basic) | Wagmi-based setup using the `tempoWallet` connector to connect to Tempo Wallets. | [Guide](/guide/use-accounts/embed-tempo-wallet) | -| [Embed Domain-bound Passkeys](https://github.com/tempoxyz/accounts/tree/main/examples/domain-bound-webauthn) | Domain-bound passkey example using Wagmi and the `webAuthn` connector. | [Guide](/guide/use-accounts/embed-passkeys) | -| [CLI + Tempo Wallets](https://github.com/tempoxyz/accounts/tree/main/examples/cli) | Minimal CLI setup to connect and authorize local keys using Tempo Wallets. | | -| [Access Keys + Tempo Wallets](https://github.com/tempoxyz/accounts/tree/main/examples/with-access-key) | Authorize access keys using Tempo Wallets to submit transactions without prompts. | | -| [Access Keys + Domain-bound Passkeys](https://github.com/tempoxyz/accounts/tree/main/examples/with-access-key-and-webauthn) | Authorize access keys using domain-bound Passkeys. | | -| [Sponsor Fees + Tempo Wallets](https://github.com/tempoxyz/accounts/tree/main/examples/with-fee-payer) | Sponsor transactions via Tempo Wallets. | [Guide](/guide/payments/sponsor-user-fees) | -| [Sponsor Fees + Domain-bound Passkeys](https://github.com/tempoxyz/accounts/tree/main/examples/with-fee-payer-and-webauthn) | Sponsor transactions via domain-bound Passkeys. | [Guide](/guide/payments/sponsor-user-fees) | - -## Secure Origins (HTTPS) - -The Tempo Accounts SDK is designed to be used on secure origins (HTTPS). If you are using HTTP, -it will fallback to using a popup instead of an iframe. This is because -WebAuthn is not supported on iframes embedded on insecure origins (HTTP). - -Web frameworks typically default to HTTP in development environments. You -will need to ensure to turn on HTTPS in development to leverage the iframe dialog. - -### Portless - -[Portless](https://github.com/vercel-labs/portless) replaces port numbers with stable, named `.localhost` URLs and can enable HTTPS with auto-generated certificates. - -```sh -npm install -g portless -portless run dev -# → https://myapp.localhost -``` - -### Next.js - -HTTPS can be enabled on Next.js' dev server by setting the `--experimental-https` flag on the `next dev` command. - -```bash -next dev --experimental-https -``` - -### Vite - -HTTPS can be enabled on Vite's dev server by installing and configuring the `vite-plugin-mkcert` plugin. - -```bash -npm i vite-plugin-mkcert -``` - -:::code-group -```ts [vite.config.ts] -import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react' -import mkcert from 'vite-plugin-mkcert' - -export default defineConfig({ - plugins: [ - mkcert(), - react(), - ], -}) -``` -::: - -## Getting Help - -Have questions or building something cool with the Accounts SDK? - -Join the Telegram group to chat with the team and other devs: [@mpp_devs](https://t.me/mpp_devs) diff --git a/src/pages/accounts/production.mdx b/src/pages/accounts/production.mdx deleted file mode 100644 index 52d8d25d..00000000 --- a/src/pages/accounts/production.mdx +++ /dev/null @@ -1,24 +0,0 @@ ---- -title: Deploying to Production -description: Things to consider before deploying your application with the Tempo Accounts SDK to production. ---- - -# Deploying to Production - -Below are some things to consider before deploying to production. - -## Trusted Hosts - -To enable the `iframe` dialog on all browsers in production, ensure your website hostname is added to the [trusted hosts list](https://github.com/tempoxyz/accounts/blob/main/src/trusted-hosts.json). - -Without this, the `iframe` dialog will fallback to a popup on browsers that do not support the [IntersectionObserver v2 API](https://web.dev/articles/intersectionobserver-v2). - -## Content Security Policy - -If you've deployed a Content Security Policy, ensure that you have the Tempo Accounts SDK configured with your CSPs. - -The full set of directives that the Tempo Accounts SDK requires are: - -| Directive | Value | -| --- | --- | -| `frame-src` | `https://wallet.tempo.xyz` | diff --git a/src/pages/accounts/rpc/eth_fillTransaction.mdx b/src/pages/accounts/rpc/eth_fillTransaction.mdx deleted file mode 100644 index 1fc6d4d0..00000000 --- a/src/pages/accounts/rpc/eth_fillTransaction.mdx +++ /dev/null @@ -1,414 +0,0 @@ ---- -title: eth_fillTransaction -description: Fills missing transaction fields and returns wallet-aware metadata. ---- - -# `eth_fillTransaction` - -Fills missing transaction fields (gas, nonce, fees) via the node, with wallet-aware enrichment — fee token resolution, simulation-based balance diffs, conditional sponsorship, and automatic AMM resolution for insufficient balances. - -When sent through [`Handler.relay`](/accounts/server/handler.relay), the response is enriched with a `capabilities` object containing balance diffs, fee estimates, sponsor info, and swap details. - -## Request - -```ts -type Request = { - method: 'eth_fillTransaction' - params: [{ - /** Access list entries. */ - accessList?: { address: `0x${string}`; storageKeys: `0x${string}`[] }[] - /** Batch of calls. */ - calls?: { - /** Calldata. */ - data?: `0x${string}` - /** Recipient. */ - to?: `0x${string}` - /** Value to transfer. */ - value?: `0x${string}` - }[] - /** Chain ID. */ - chainId?: `0x${string}` - /** Fee token to use. If omitted, the relay picks the user's best token. */ - feeToken?: `0x${string}` - /** Sender address. */ - from?: `0x${string}` - /** Gas limit. */ - gas?: `0x${string}` - /** Key authorization for access key transactions. */ - keyAuthorization?: KeyAuthorization - /** Max fee per gas. */ - maxFeePerGas?: `0x${string}` - /** Max priority fee per gas. */ - maxPriorityFeePerGas?: `0x${string}` - /** Nonce. */ - nonce?: `0x${string}` - /** Nonce key. */ - nonceKey?: `0x${string}` - /** Valid after timestamp. */ - validAfter?: number - /** Valid before timestamp. */ - validBefore?: number - }] -} -``` - -## Response - -```ts -type Response = { - /** Wallet-specific capabilities computed during fill. */ - capabilities: { - /** AMM swap injected to cover an insufficient balance. */ - autoSwap?: { - /** Max input amount with slippage applied. */ - maxIn: SwapAmount - /** Deficit amount that triggered the swap. */ - minOut: SwapAmount - /** Slippage tolerance (e.g. 0.05 = 5%). */ - slippage: number - } - - /** Per-account balance diffs from simulation (swap-related diffs excluded). */ - balanceDiffs?: { - [account: `0x${string}`]: BalanceDiff[] - } - - /** Fee estimate for the transaction. */ - fee?: { - /** Raw fee amount in token units. */ - amount: `0x${string}` - /** Token decimals (e.g. 6). */ - decimals: number - /** Human-readable fee (e.g. "0.028022"). */ - formatted: string - /** Token symbol (e.g. "AlphaUSD"). */ - symbol: string - } - - /** Structured error details when the fill fails. */ - error?: { - /** Revert error name (e.g. "InsufficientBalance"). */ - errorName: string - /** Human-readable error message. */ - message: string - } - - /** Funding requirement when InsufficientBalance is encountered. */ - requireFunds?: { - /** Deficit amount in token units. */ - amount: `0x${string}` - /** Token decimals (e.g. 6). */ - decimals: number - /** Human-readable deficit (e.g. "100.000000"). */ - formatted: string - /** Token address. */ - token: `0x${string}` - /** Token symbol (e.g. "USDC.e"). */ - symbol: string - } - - /** Sponsor details, present when `sponsored` is `true`. */ - sponsor?: { - /** Sponsor address. */ - address: `0x${string}` - /** Sponsor display name. */ - name?: string - /** Sponsor URL. */ - url?: string - } - - /** Whether the transaction is sponsored by a fee payer. */ - sponsored: boolean - } - /** Fully filled transaction. */ - tx: Record -} - -type BalanceDiff = { - /** Token address. */ - address: `0x${string}` - /** Token decimals (e.g. 6). */ - decimals: number - /** Direction relative to the user. */ - direction: 'incoming' | 'outgoing' - /** Human-readable formatted amount (e.g. "100.00"). */ - formatted: string - /** Token name (e.g. "USDC.e"). */ - name: string - /** Addresses receiving this asset. */ - recipients: readonly `0x${string}`[] - /** Token symbol (e.g. "USDC.e"). */ - symbol: string - /** Token amount. */ - value: `0x${string}` -} - -type SwapAmount = { - /** Token decimals. */ - decimals: number - /** Human-readable formatted amount. */ - formatted: string - /** Token name (e.g. "AlphaUSD"). */ - name: string - /** Token symbol (e.g. "AlphaUSD"). */ - symbol: string - /** Token address. */ - token: `0x${string}` - /** Amount. */ - value: `0x${string}` -} -``` - -## Example - -```ts twoslash -// @noErrors -import { Provider } from 'accounts' - -const provider = Provider.create() - -const [account] = await provider.request({ - method: 'eth_accounts', -}) - -// ---cut--- -const result = await provider.request({ - method: 'eth_fillTransaction', - params: [{ - from: account, - calls: [{ - to: '0x20c000000000000000000000b9537d11c60e8b50', // USDC.e - // transfer 100 USDC.e - data: '0xa9059cbb000000000000000000000000cafebabecafebabecafebabecafebabecafebabe0000000000000000000000000000000000000000000000000000000005f5e100', - }], - }], -}) - -result.capabilities -// { -// balanceDiffs: { -// '0x1234567890abcdef1234567890abcdef12345678': [{ -// address: '0x20c000000000000000000000b9537d11c60e8b50', -// decimals: 6, -// direction: 'outgoing', -// formatted: '100.000000', -// name: 'USDC.e', -// recipients: ['0xcafebabecafebabecafebabecafebabecafebabe'], -// symbol: 'USDC.e', -// value: '0x5f5e100', -// }], -// }, -// fee: { -// amount: '0x6b86', -// decimals: 6, -// formatted: '0.027526', -// symbol: 'pathUSD', -// }, -// sponsored: false, -// } -``` - -### With Fee Token - -Pay fees with a specific stablecoin. - -```ts twoslash -// @noErrors -import { Provider } from 'accounts' -const provider = Provider.create() -const [account] = await provider.request({ method: 'eth_accounts' }) -// ---cut--- -const result = await provider.request({ - method: 'eth_fillTransaction', - params: [{ - from: account, - calls: [{ - to: '0x20c000000000000000000000b9537d11c60e8b50', // USDC.e - // transfer 100 USDC.e - data: '0xa9059cbb000000000000000000000000cafebabecafebabecafebabecafebabecafebabe0000000000000000000000000000000000000000000000000000000005f5e100', - }], - feeToken: '0x20c000000000000000000000b9537d11c60e8b50', // USDC.e - }], -}) - -result.capabilities.fee -// { -// amount: '0x6b86', -// decimals: 6, -// formatted: '0.027526', -// symbol: 'USDC.e', -// } -``` - -### With Sponsorship - -When the relay is configured with a [`feePayer`](/accounts/server/handler.relay#feepayer), validated transactions are sponsored. The response includes `sponsored: true` and `sponsor` details. - -```ts twoslash -// @noErrors -import { Provider } from 'accounts' -const provider = Provider.create() -const [account] = await provider.request({ method: 'eth_accounts' }) -// ---cut--- -const result = await provider.request({ - method: 'eth_fillTransaction', - params: [{ - from: account, - calls: [{ - to: '0x20c000000000000000000000b9537d11c60e8b50', // USDC.e - // transfer 100 USDC.e - data: '0xa9059cbb000000000000000000000000cafebabecafebabecafebabecafebabecafebabe0000000000000000000000000000000000000000000000000000000005f5e100', - }], - }], -}) - -result.capabilities.sponsored -// true - -result.capabilities.sponsor -// { -// address: '0x1234567890abcdef1234567890abcdef12345678', -// name: 'My App', -// url: 'https://myapp.com', -// } -``` - -### AMM Resolution - -When the user has insufficient balance of a required token, the relay automatically injects swap calls (approve + buy) via the [Stablecoin DEX](/guide/stablecoin-dex). The swap details are reported in `meta.autoSwap`, and swap-related balance diffs are excluded from `meta.balanceDiffs`. - -```ts twoslash -// @noErrors -import { Provider } from 'accounts' -const provider = Provider.create() -const [account] = await provider.request({ method: 'eth_accounts' }) -// ---cut--- -const result = await provider.request({ - method: 'eth_fillTransaction', - params: [{ - from: account, - calls: [{ - to: '0x20c000000000000000000000b9537d11c60e8b50', // USDC.e - // transfer 100 USDC.e - data: '0xa9059cbb000000000000000000000000cafebabecafebabecafebabecafebabecafebabe0000000000000000000000000000000000000000000000000000000005f5e100', - }], - }], -}) - -result.capabilities.balanceDiffs -// { -// '0x1234567890abcdef1234567890abcdef12345678': [{ -// address: '0x20c0000000000000000000000000000000000001', -// decimals: 6, -// direction: 'outgoing', -// formatted: '100.000000', -// name: 'AlphaUSD', -// recipients: ['0xcafebabecafebabecafebabecafebabecafebabe'], -// symbol: 'AlphaUSD', -// value: '0x5f5e100', -// }], -// } - -result.capabilities.autoSwap -// { -// maxIn: { -// decimals: 6, -// formatted: '105.000000', -// name: 'AlphaUSD', -// symbol: 'AlphaUSD', -// token: '0x20c0000000000000000000000000000000000001', -// value: '0x6422c40', -// }, -// minOut: { -// decimals: 6, -// formatted: '100.000000', -// name: 'USDC.e', -// symbol: 'USDC.e', -// token: '0x20c000000000000000000000b9537d11c60e8b50', -// value: '0x5f5e100', -// }, -// slippage: 0.05, -// } - -result.capabilities.fee -// { -// amount: '0x6b86', -// decimals: 6, -// formatted: '0.027526', -// symbol: 'pathUSD', -// } -``` - -### Error Details - -When `eth_fillTransaction` fails, the relay returns structured error details in `capabilities.error` instead of throwing. - -```ts twoslash -// @noErrors -import { Provider } from 'accounts' -const provider = Provider.create() -const [account] = await provider.request({ method: 'eth_accounts' }) -// ---cut--- -const result = await provider.request({ - method: 'eth_fillTransaction', - params: [{ - from: account, - calls: [{ - to: '0x20c000000000000000000000b9537d11c60e8b50', // USDC.e - data: '0xa9059cbb000000000000000000000000cafebabecafebabecafebabecafebabecafebabe0000000000000000000000000000000000000000000000000000000005f5e100', - }], - }], -}) - -result.capabilities.error -// { -// errorName: 'BelowMinimumOrderSize', -// message: 'Below minimum order size: 1000000.', -// } -``` - -### Require Funds - -For `InsufficientBalance` errors, the relay also returns a `capabilities.requireFunds` object with the exact deficit amount and token metadata. - -```ts twoslash -// @noErrors -import { Provider } from 'accounts' -const provider = Provider.create() -const [account] = await provider.request({ method: 'eth_accounts' }) -// ---cut--- -const result = await provider.request({ - method: 'eth_fillTransaction', - params: [{ - from: account, - calls: [{ - to: '0x20c000000000000000000000b9537d11c60e8b50', // USDC.e - // transfer 100 USDC.e (but account has 0) - data: '0xa9059cbb000000000000000000000000cafebabecafebabecafebabecafebabecafebabe0000000000000000000000000000000000000000000000000000000005f5e100', - }], - }], -}) - -result.capabilities.requireFunds -// { -// amount: '0x5f5e100', -// decimals: 6, -// formatted: '100.000000', -// token: '0x20c000000000000000000000b9537d11c60e8b50', -// symbol: 'USDC.e', -// } - -result.capabilities.balanceDiffs -// { -// '0x1234567890abcdef1234567890abcdef12345678': [{ -// address: '0x20c000000000000000000000b9537d11c60e8b50', -// decimals: 6, -// direction: 'outgoing', -// formatted: '100.000000', -// name: 'USDC.e', -// recipients: ['0xcafebabecafebabecafebabecafebabecafebabe'], -// symbol: 'USDC.e', -// value: '0x5f5e100', -// }], -// } -``` diff --git a/src/pages/accounts/rpc/eth_sendTransaction.mdx b/src/pages/accounts/rpc/eth_sendTransaction.mdx deleted file mode 100644 index 7f1e57d8..00000000 --- a/src/pages/accounts/rpc/eth_sendTransaction.mdx +++ /dev/null @@ -1,36 +0,0 @@ ---- -title: eth_sendTransaction -description: Send a transaction from the connected account. ---- - -# `eth_sendTransaction` - -Signs and broadcasts a transaction, returning the transaction hash. - -## Request - -```ts -type Request = { - method: 'eth_sendTransaction' - params: [TransactionRequest] -} -``` - -## Response - -```ts -type Response = Hex // transaction hash -``` - -## Example - -```ts -const hash = await provider.request({ - method: 'eth_sendTransaction', - params: [{ - to: '0x...', - value: '0x0', - data: '0x...', - }], -}) -``` diff --git a/src/pages/accounts/rpc/eth_sendTransactionSync.mdx b/src/pages/accounts/rpc/eth_sendTransactionSync.mdx deleted file mode 100644 index 90b902c4..00000000 --- a/src/pages/accounts/rpc/eth_sendTransactionSync.mdx +++ /dev/null @@ -1,36 +0,0 @@ ---- -title: eth_sendTransactionSync -description: Send a transaction and wait for the receipt. ---- - -# `eth_sendTransactionSync` - -Signs, broadcasts, and waits for a transaction receipt. Returns the full receipt instead of just the hash. - -## Request - -```ts -type Request = { - method: 'eth_sendTransactionSync' - params: [TransactionRequest] -} -``` - -## Response - -```ts -type Response = TransactionReceipt -``` - -## Example - -```ts -const receipt = await provider.request({ - method: 'eth_sendTransactionSync', - params: [{ - to: '0x...', - value: '0x0', - data: '0x...', - }], -}) -``` diff --git a/src/pages/accounts/rpc/personal_sign.mdx b/src/pages/accounts/rpc/personal_sign.mdx deleted file mode 100644 index 3869d335..00000000 --- a/src/pages/accounts/rpc/personal_sign.mdx +++ /dev/null @@ -1,40 +0,0 @@ ---- -title: personal_sign -description: Sign a message with the connected account. ---- - -# `personal_sign` - -Signs an arbitrary message with the connected account's key. - -## Request - -```ts -type Request = { - method: 'personal_sign' - params: [ - Hex, // message - Address // signer address - ] -} -``` - -## Response - -```ts -type Response = Hex // signature -``` - -## Example - -```ts -import { Hex } from 'ox' - -const signature = await provider.request({ - method: 'personal_sign', - params: [ - Hex.fromString('Hello, Tempo!'), - '0x...', - ], -}) -``` diff --git a/src/pages/accounts/rpc/wallet_authorizeAccessKey.mdx b/src/pages/accounts/rpc/wallet_authorizeAccessKey.mdx deleted file mode 100644 index 58b48471..00000000 --- a/src/pages/accounts/rpc/wallet_authorizeAccessKey.mdx +++ /dev/null @@ -1,53 +0,0 @@ ---- -title: wallet_authorizeAccessKey -description: Authorize an access key for delegated transaction signing. ---- - -# `wallet_authorizeAccessKey` - -Authorizes an access key with an expiry and optional spending limits for delegated transaction signing. - -## Request - -```ts -type Request = { - method: 'wallet_authorizeAccessKey' - params: [{ - /** Unix timestamp (seconds) when the key expires. */ - expiry: number - /** TIP-20 spending limits. */ - limits?: { token: Address; limit: Hex }[] - /** Public key to authorize. */ - publicKey?: Hex - /** Key type. */ - keyType?: 'secp256k1' | 'p256' | 'webAuthn' - /** Address of the key (alternative to publicKey). */ - address?: Address - }] -} -``` - -## Response - -```ts -type Response = { - keyAuthorization: KeyAuthorization - rootAddress: Address -} -``` - -## Example - -```ts -import { Expiry } from 'accounts' - -const result = await provider.request({ - method: 'wallet_authorizeAccessKey', - params: [{ - expiry: Expiry.days(1), - limits: [ - { token: '0x20c0000000000000000000000000000000000001', limit: '0x5F5E100' }, - ], - }], -}) -``` diff --git a/src/pages/accounts/rpc/wallet_connect.mdx b/src/pages/accounts/rpc/wallet_connect.mdx deleted file mode 100644 index 85c16763..00000000 --- a/src/pages/accounts/rpc/wallet_connect.mdx +++ /dev/null @@ -1,54 +0,0 @@ ---- -title: wallet_connect -description: Connect account(s) with optional capabilities like access key authorization. ---- - -# `wallet_connect` - -Requests to connect account(s) with optional capabilities. - -## Request - -```ts -type Request = { - method: 'wallet_connect' - params?: [{ - capabilities?: { - /** Authorize an access key on connect. */ - authorizeAccessKey?: { - expiry: number - limits?: { token: Address; limit: Hex }[] - publicKey?: Hex - } - /** Authentication method. */ - method?: 'register' | 'login' - } - }] -} -``` - -## Response - -```ts -type Response = { - accounts: { - address: Address - capabilities: { - keyAuthorization?: KeyAuthorization - signature?: Hex - } - }[] -} -``` - -## Example - -```ts -import { Provider } from 'accounts' - -const provider = Provider.create() - -const { accounts } = await provider.request({ - method: 'wallet_connect', -}) -``` diff --git a/src/pages/accounts/rpc/wallet_disconnect.mdx b/src/pages/accounts/rpc/wallet_disconnect.mdx deleted file mode 100644 index 4dcc8b0d..00000000 --- a/src/pages/accounts/rpc/wallet_disconnect.mdx +++ /dev/null @@ -1,30 +0,0 @@ ---- -title: wallet_disconnect -description: Disconnect the connected account(s). ---- - -# `wallet_disconnect` - -Disconnects the connected account(s) and clears session state. - -## Request - -```ts -type Request = { - method: 'wallet_disconnect' -} -``` - -## Response - -```ts -type Response = undefined -``` - -## Example - -```ts -await provider.request({ - method: 'wallet_disconnect', -}) -``` diff --git a/src/pages/accounts/rpc/wallet_getBalances.mdx b/src/pages/accounts/rpc/wallet_getBalances.mdx deleted file mode 100644 index ad2c5557..00000000 --- a/src/pages/accounts/rpc/wallet_getBalances.mdx +++ /dev/null @@ -1,45 +0,0 @@ ---- -title: wallet_getBalances -description: Get token balances for an account. ---- - -# `wallet_getBalances` - -Returns token balances for the connected account. - -## Request - -```ts -type Request = { - method: 'wallet_getBalances' - params?: [{ - /** Account address. Defaults to connected account. */ - account?: Address - /** Chain ID. */ - chainId?: number - /** Filter to specific token addresses. */ - tokens?: Address[] - }] -} -``` - -## Response - -```ts -type Response = { - address: Address - balance: bigint - decimals: number - display: string - name: string - symbol: string -}[] -``` - -## Example - -```ts -const balances = await provider.request({ - method: 'wallet_getBalances', -}) -``` diff --git a/src/pages/accounts/rpc/wallet_getCallsStatus.mdx b/src/pages/accounts/rpc/wallet_getCallsStatus.mdx deleted file mode 100644 index 0a537ba9..00000000 --- a/src/pages/accounts/rpc/wallet_getCallsStatus.mdx +++ /dev/null @@ -1,39 +0,0 @@ ---- -title: wallet_getCallsStatus -description: Get the status of a batch of calls sent via wallet_sendCalls. ---- - -# `wallet_getCallsStatus` - -Returns the status and receipts of a previously submitted batch of calls. - -## Request - -```ts -type Request = { - method: 'wallet_getCallsStatus' - params?: [string] -} -``` - -## Response - -```ts -type Response = { - atomic: boolean - chainId: number - id: string - receipts?: TransactionReceipt[] - status: number - version: string -} -``` - -## Example - -```ts -const status = await provider.request({ - method: 'wallet_getCallsStatus', - params: ['0x...'], -}) -``` diff --git a/src/pages/accounts/rpc/wallet_getCapabilities.mdx b/src/pages/accounts/rpc/wallet_getCapabilities.mdx deleted file mode 100644 index 8a2f8f2f..00000000 --- a/src/pages/accounts/rpc/wallet_getCapabilities.mdx +++ /dev/null @@ -1,35 +0,0 @@ ---- -title: wallet_getCapabilities -description: Get account capabilities for specified chains. ---- - -# `wallet_getCapabilities` - -Returns capabilities for the connected account across chains. - -## Request - -```ts -type Request = { - method: 'wallet_getCapabilities' - params?: [Address] | [Address, Hex[]] -} -``` - -## Response - -```ts -type Response = Record -``` - -## Example - -```ts -const capabilities = await provider.request({ - method: 'wallet_getCapabilities', - params: ['0x...'], -}) -``` diff --git a/src/pages/accounts/rpc/wallet_revokeAccessKey.mdx b/src/pages/accounts/rpc/wallet_revokeAccessKey.mdx deleted file mode 100644 index a62cc6c6..00000000 --- a/src/pages/accounts/rpc/wallet_revokeAccessKey.mdx +++ /dev/null @@ -1,40 +0,0 @@ ---- -title: wallet_revokeAccessKey -description: Revoke a previously authorized access key. ---- - -# `wallet_revokeAccessKey` - -Revokes a previously authorized access key. - -## Request - -```ts -type Request = { - method: 'wallet_revokeAccessKey' - params: [{ - /** Address of the account. */ - address: Address - /** Address of the access key to revoke. */ - accessKeyAddress: Address - }] -} -``` - -## Response - -```ts -type Response = undefined -``` - -## Example - -```ts -await provider.request({ - method: 'wallet_revokeAccessKey', - params: [{ - address: '0x...', - accessKeyAddress: '0x...', - }], -}) -``` diff --git a/src/pages/accounts/rpc/wallet_sendCalls.mdx b/src/pages/accounts/rpc/wallet_sendCalls.mdx deleted file mode 100644 index dc907327..00000000 --- a/src/pages/accounts/rpc/wallet_sendCalls.mdx +++ /dev/null @@ -1,48 +0,0 @@ ---- -title: wallet_sendCalls -description: Send a batch of calls from the connected account. ---- - -# `wallet_sendCalls` - -Sends a batch of calls from the connected account. Supports atomic execution. - -## Request - -```ts -type Request = { - method: 'wallet_sendCalls' - params?: [{ - calls: { to: Address; data?: Hex; value?: Hex }[] - from?: Address - chainId?: number - capabilities?: { sync?: boolean } - atomicRequired?: boolean - }] -} -``` - -## Response - -```ts -type Response = { - id: string - status?: number - receipts?: TransactionReceipt[] - chainId?: number - atomic?: boolean -} -``` - -## Example - -```ts -const result = await provider.request({ - method: 'wallet_sendCalls', - params: [{ - calls: [ - { to: '0x...', data: '0x...' }, - ], - }], -}) -``` diff --git a/src/pages/accounts/server/handler.compose.mdx b/src/pages/accounts/server/handler.compose.mdx deleted file mode 100644 index e3d1c02c..00000000 --- a/src/pages/accounts/server/handler.compose.mdx +++ /dev/null @@ -1,54 +0,0 @@ ---- -title: Handler.compose -description: Compose multiple server handlers into a single handler. ---- - -# `Handler.compose` - -Composes multiple handlers into a single handler, routing requests to the handler that matches the request path. - -## Usage - -```ts -import { Handler } from 'accounts/server' - -const handler = Handler.compose([ - Handler.relay({ - feePayer: { account: privateKeyToAccount('0x...') }, - }), - Handler.webAuthn({ - kv: Kv.memory(), - origin: 'https://example.com', - rpId: 'example.com' - }), -]) -``` - -Then plug `handler` into your server framework of choice: - -```ts -createServer(handler.listener) // Node.js -Bun.serve(handler) // Bun -Deno.serve(handler) // Deno -app.all('*', c => handler.fetch(c.request)) // Elysia -app.use(handler.listener) // Express -app.use(c => handler.fetch(c.req.raw)) // Hono -export const GET = handler.fetch // Next.js -export const POST = handler.fetch // Next.js -``` - -## Parameters - -### handlers - -- **Type:** `Handler[]` -- **Required** - -Array of handlers to compose. Requests are routed to each handler in order. - -### path - -- **Type:** `string` -- **Default:** `'/'` - -Base path prefix for all composed handlers. diff --git a/src/pages/accounts/server/handler.feePayer.mdx b/src/pages/accounts/server/handler.feePayer.mdx deleted file mode 100644 index 4bbfcc4e..00000000 --- a/src/pages/accounts/server/handler.feePayer.mdx +++ /dev/null @@ -1,46 +0,0 @@ ---- -title: Handler.feePayer (Deprecated) -description: Deprecated — use Handler.relay with feePayer option instead. ---- - -# `Handler.feePayer` (Deprecated) - -:::warning -`Handler.feePayer` has been deprecated. Use [`Handler.relay`](/accounts/server/handler.relay) with the [`feePayer`](/accounts/server/handler.relay#feepayer) option instead. -::: - -:::tip -Looking for Tempo-hosted sponsorship endpoints? See [Hosted Fee Payer](/developer-tools/fee-payer). Use [`Handler.relay`](/accounts/server/handler.relay) when you need to run your own fee payer service. -::: - -## Migration - -Replace `Handler.feePayer` calls with `Handler.relay` and nest the `account` under `feePayer`: - -```diff -- const handler = Handler.feePayer({ -- account: privateKeyToAccount('0x...'), -- }) -+ const handler = Handler.relay({ -+ feePayer: { -+ account: privateKeyToAccount('0x...'), -+ }, -+ }) -``` - -All other options (`chains`, `transports`, `path`, `onRequest`) remain the same — `validate` moves inside `feePayer`: - -```diff -- const handler = Handler.feePayer({ -- account: privateKeyToAccount('0x...'), -- validate: (request) => request.from !== blocked, -- }) -+ const handler = Handler.relay({ -+ feePayer: { -+ account: privateKeyToAccount('0x...'), -+ validate: (request) => request.from !== blocked, -+ }, -+ }) -``` - -See [`Handler.relay`](/accounts/server/handler.relay) for the full API reference. diff --git a/src/pages/accounts/server/handler.relay.mdx b/src/pages/accounts/server/handler.relay.mdx deleted file mode 100644 index d3e33617..00000000 --- a/src/pages/accounts/server/handler.relay.mdx +++ /dev/null @@ -1,674 +0,0 @@ ---- -title: Handler.relay -description: Server handler that proxies certain RPC requests with wallet-aware enrichment. ---- - -import { Cards, Card } from 'vocs' - -# `Handler.relay` - -Creates a server handler that proxies certain RPC requests (like `eth_fillTransaction`) with wallet-aware enrichment — fee token resolution, simulation-based balance diffs, conditional sponsorship, and automatic AMM resolution for insufficient balances. - -## Usage - -```ts twoslash -import { Handler } from 'accounts/server' - -const handler = Handler.relay() -``` - -Then plug `handler` into your server framework of choice: - -```ts twoslash -// @noErrors -import { Handler } from 'accounts/server' -const handler = Handler.relay() -// ---cut--- -createServer(handler.listener) // Node.js -Bun.serve(handler) // Bun -Deno.serve(handler) // Deno -app.all('*', c => handler.fetch(c.request)) // Elysia -app.use(handler.listener) // Express -app.use(c => handler.fetch(c.req.raw)) // Hono -export const GET = handler.fetch // Next.js -export const POST = handler.fetch // Next.js -``` - -## Features - - - - - - - - - - -### Sponsorship - -Configure a [`feePayer`](#feepayer) to sponsor transactions. The relay signs `feePayerSignature` on the filled transaction and returns sponsor details in the response metadata. Use a [`validate`](#feepayervalidate) callback for conditional sponsorship — rejected transactions are re-filled for self-payment. - -:::code-group - -```ts twoslash [server.ts] -import { privateKeyToAccount } from 'viem/accounts' -import { Handler } from 'accounts/server' - -const blockedAddress = '0x...' -// ---cut--- -const handler = Handler.relay({ - feePayer: { - account: privateKeyToAccount('0x...'), - name: 'My App', - url: 'https://myapp.com', - // Optional — validate sponsorship approval. - validate: (request) => request.from !== blockedAddress, - }, -}) -``` - -```ts twoslash [client.ts] -// @noErrors -import { Provider } from 'accounts' -const provider = Provider.create() -const [account] = await provider.request({ method: 'eth_accounts' }) -// ---cut--- -const result = await provider.request({ - method: 'eth_fillTransaction', - params: [{ - from: account, - calls: [{ - to: '0x20c000000000000000000000b9537d11c60e8b50', // USDC.e - // transfer 100 USDC.e - data: '0xa9059cbb000000000000000000000000cafebabecafebabecafebabecafebabecafebabe0000000000000000000000000000000000000000000000000000000005f5e100', - }], - }], -}) - -result.capabilities.sponsored -// true - -result.capabilities.sponsor -// { -// address: '0x1234567890abcdef1234567890abcdef12345678', -// name: 'My App', -// url: 'https://myapp.com', -// } -``` - -::: - -### Auto Swap - -When a user has insufficient balance of a required token, the relay automatically injects swap calls (approve + buy) via the [Stablecoin DEX](/guide/stablecoin-dex). Swap details are reported in `capabilities.autoSwap`, and swap-related balance diffs are excluded from `capabilities.balanceDiffs`. Enabled by [`features: 'all'`](#features), configurable via [`autoSwap`](#autoswap). - -:::code-group - -```ts twoslash [client.ts] -// @noErrors -import { Provider } from 'accounts' -const provider = Provider.create() -const [account] = await provider.request({ method: 'eth_accounts' }) -// ---cut--- -const result = await provider.request({ - method: 'eth_fillTransaction', - params: [{ - from: account, - calls: [{ - to: '0x20c000000000000000000000b9537d11c60e8b50', // USDC.e - // transfer 100 USDC.e - data: '0xa9059cbb000000000000000000000000cafebabecafebabecafebabecafebabecafebabe0000000000000000000000000000000000000000000000000000000005f5e100', - }], - }], -}) - -result.capabilities.autoSwap -// { -// maxIn: { -// decimals: 6, -// formatted: '105.000000', -// name: 'AlphaUSD', -// symbol: 'AlphaUSD', -// token: '0x20c0000000000000000000000000000000000001', -// value: '0x6422c40', -// }, -// minOut: { -// decimals: 6, -// formatted: '100.000000', -// name: 'USDC.e', -// symbol: 'USDC.e', -// token: '0x20c000000000000000000000b9537d11c60e8b50', -// value: '0x5f5e100', -// }, -// slippage: 0.05, -// } -``` - -```ts [server.ts] -import { Handler } from 'accounts/server' - -const handler = Handler.relay({ - autoSwap: { slippage: 0.05 }, // 5% (default) -}) -``` - -::: - -### Best Fee Tokens - -Picks the user's optimal `feeToken` automatically: - -1. On-chain preference via `fee.getUserToken` (if set and has balance) -2. Highest-balance token from the [`resolveTokens`](#resolvetokens) list -3. Validator fallback - -```ts twoslash -// @noErrors -import { Provider } from 'accounts' -const provider = Provider.create() -const [account] = await provider.request({ method: 'eth_accounts' }) -// ---cut--- -const result = await provider.request({ - method: 'eth_fillTransaction', - params: [{ - from: account, - calls: [{ - to: '0x20c000000000000000000000b9537d11c60e8b50', // USDC.e - // transfer 100 USDC.e - data: '0xa9059cbb000000000000000000000000cafebabecafebabecafebabecafebabecafebabe0000000000000000000000000000000000000000000000000000000005f5e100', - }], - }], -}) - -result.capabilities.fee -// { -// amount: '0x6b86', -// decimals: 6, -// formatted: '0.027526', -// symbol: 'USDC.e', -// } -``` - -### Balance Diffs - -Simulates the transaction and returns per-account token balance diffs in `capabilities.balanceDiffs`. - -:::code-group - -```ts twoslash [client.ts] -// @noErrors -import { Provider } from 'accounts' -const provider = Provider.create() -const [account] = await provider.request({ method: 'eth_accounts' }) -// ---cut--- -const result = await provider.request({ - method: 'eth_fillTransaction', - params: [{ - from: account, - calls: [{ - to: '0x20c000000000000000000000b9537d11c60e8b50', // USDC.e - // transfer 100 USDC.e - data: '0xa9059cbb000000000000000000000000cafebabecafebabecafebabecafebabecafebabe0000000000000000000000000000000000000000000000000000000005f5e100', - }], - }], -}) - -result.capabilities.balanceDiffs -// { -// '0x1234567890abcdef1234567890abcdef12345678': [{ -// address: '0x20c000000000000000000000b9537d11c60e8b50', -// decimals: 6, -// direction: 'outgoing', -// formatted: '100.000000', -// name: 'USDC.e', -// recipients: ['0xcafebabecafebabecafebabecafebabecafebabe'], -// symbol: 'USDC.e', -// value: '0x5f5e100', -// }], -// } -``` - -```ts twoslash [server.ts] -import { Handler } from 'accounts/server' -// ---cut--- -// Balance diffs are included by default — no extra config needed. -const handler = Handler.relay() -``` - -::: - -### Fee Derivation - -Derives fee estimates from the filled transaction and returns them as both raw and human-readable values, so the UI can display costs without additional computation. - -:::code-group - -```ts twoslash [client.ts] -// @noErrors -import { Provider } from 'accounts' -const provider = Provider.create() -const [account] = await provider.request({ method: 'eth_accounts' }) -// ---cut--- -const result = await provider.request({ - method: 'eth_fillTransaction', - params: [{ - from: account, - calls: [{ - to: '0x20c000000000000000000000b9537d11c60e8b50', // USDC.e - // transfer 100 USDC.e - data: '0xa9059cbb000000000000000000000000cafebabecafebabecafebabecafebabecafebabe0000000000000000000000000000000000000000000000000000000005f5e100', - }], - }], -}) - -result.capabilities.fee -// { -// amount: '0x6b86', -// decimals: 6, -// formatted: '0.027526', -// symbol: 'pathUSD', -// } -``` - -```ts twoslash [server.ts] -import { Handler } from 'accounts/server' -// ---cut--- -// Formatted fees are included by default — no extra config needed. -const handler = Handler.relay() -``` - -::: - -### Require Funds - -For insufficient balance errors, the relay also returns a `capabilities.requireFunds` object with the exact deficit amount and token metadata — enabling UIs to prompt the user to fund their account. - -```ts twoslash -// @noErrors -import { Provider } from 'accounts' -const provider = Provider.create() -const [account] = await provider.request({ method: 'eth_accounts' }) -// ---cut--- -const result = await provider.request({ - method: 'eth_fillTransaction', - params: [{ - from: account, - calls: [{ - to: '0x20c000000000000000000000b9537d11c60e8b50', // USDC.e - // transfer 100 USDC.e (but account has 40) - data: '0xa9059cbb000000000000000000000000cafebabecafebabecafebabecafebabecafebabe0000000000000000000000000000000000000000000000000000000005f5e100', - }], - }], -}) - -result.capabilities.requireFunds -// { -// amount: '0x3938700', -// decimals: 6, -// formatted: '60.000000', -// token: '0x20c000000000000000000000b9537d11c60e8b50', -// symbol: 'USDC.e', -// } - -result.capabilities.balanceDiffs -// { -// '0x1234567890abcdef1234567890abcdef12345678': [{ -// address: '0x20c000000000000000000000b9537d11c60e8b50', -// decimals: 6, -// direction: 'outgoing', -// formatted: '100.000000', -// name: 'USDC.e', -// recipients: ['0xcafebabecafebabecafebabecafebabecafebabe'], -// symbol: 'USDC.e', -// value: '0x5f5e100', -// }], -// } -``` - -### Enabling Features - -By default, only a minimum set of features are enabled, given on what options you pass to `Handler.relay(){:js}` (e.g. `feePayer`, `autoSwap`, etc). - -Set [`features: 'all'`](#features) to enable all features by default such as: **fee token resolution**, **auto-swap**, and **simulation** (balance diffs + fee breakdown). This will come at the cost of slightly increased network latency. - -```ts twoslash -// @noErrors -import { privateKeyToAccount } from 'viem/accounts' -import { Handler } from 'accounts/server' - -const handler = Handler.relay({ - features: 'all', // [!code focus] -}) -``` - -## Parameters - -### autoSwap - -- **Type:** `false | { slippage?: number }` - -AMM swap options for automatic insufficient balance resolution. When a user doesn't hold enough of a token, the relay auto-swaps from their fee token via the [Stablecoin DEX](/guide/stablecoin-dex). Set to `false` to disable even when `features: 'all'` is set. - -```ts twoslash -import { Handler } from 'accounts/server' - -const handler = Handler.relay({ - autoSwap: { slippage: 0.02 }, // 2% slippage // [!code focus] -}) -``` - -#### autoSwap.slippage - -- **Type:** `number` -- **Default:** `0.05` (5%) - -Slippage tolerance for AMM swaps. The relay sets `maxAmountIn = deficit + deficit * slippage`. - -### chains - -- **Type:** `readonly [Chain, ...Chain[]]` -- **Default:** `[tempo, tempoModerato]` - -Supported chains. The handler resolves the client based on the `chainId` in the incoming transaction. - -```ts twoslash -import { tempo } from 'viem/chains' -import { Handler } from 'accounts/server' - -const handler = Handler.relay({ - chains: [tempo], // [!code focus] -}) -``` - -### features - -- **Type:** `'all'` -- **Optional** - -Controls which relay features are enabled. By default, only fee payer sponsorship is active. - -- `'all'`: enables all features (fee token resolution, auto-swap, balance diffs, fee breakdown, etc). -- `undefined` (default): enables only features that are configured via options (e.g. `feePayer`, `autoSwap`, etc). - -```ts twoslash -// @noErrors -import { Handler } from 'accounts/server' - -const handler = Handler.relay({ - features: 'all', // [!code focus] -}) -``` - -### feePayer - -- **Type:** `object` -- **Optional** - -Fee payer configuration. When provided, the relay will sign `feePayerSignature` on the filled transaction. - -```ts twoslash -import { privateKeyToAccount } from 'viem/accounts' -import { Handler } from 'accounts/server' - -const handler = Handler.relay({ - feePayer: { // [!code focus] - account: privateKeyToAccount('0x...'), // [!code focus] - name: 'My App', // [!code focus] - url: 'https://myapp.com', // [!code focus] - }, // [!code focus] -}) -``` - -#### feePayer.account - -- **Type:** `LocalAccount` -- **Required** - -The account to use as the fee payer. - -#### feePayer.name - -- **Type:** `string` -- **Optional** - -Sponsor display name returned in the response metadata. - -#### feePayer.url - -- **Type:** `string` -- **Optional** - -Sponsor URL returned in the response metadata. - -#### feePayer.validate - -- **Type:** `(request: TransactionRequest) => boolean | Promise` -- **Optional** - -Validates whether to sponsor a transaction. When omitted, all transactions are sponsored. Return `false` to reject sponsorship — the relay will re-fill without `feePayer` so gas/nonce are correct for self-payment. - -```ts twoslash -import { privateKeyToAccount } from 'viem/accounts' -import { Handler } from 'accounts/server' - -const blocked = '0x...' - -const handler = Handler.relay({ - feePayer: { - account: privateKeyToAccount('0x...'), - validate: (request) => request.from !== blocked, // [!code focus] - }, -}) -``` - -### onRequest - -- **Type:** `(request: RpcRequest) => Promise` -- **Optional** - -Callback called before processing each request. Useful for logging, rate limiting, or custom validation. - -```ts twoslash -import { Handler } from 'accounts/server' - -const handler = Handler.relay({ - onRequest: async (request) => { // [!code focus] - console.log('Processing request:', request.method) // [!code focus] - }, // [!code focus] -}) -``` - -### path - -- **Type:** `string` -- **Default:** `'/'` - -Path where the handler listens for requests. - -```ts twoslash -import { Handler } from 'accounts/server' - -const handler = Handler.relay({ - path: '/relay', // [!code focus] -}) -``` - -### resolveTokens - -- **Type:** `(chainId: number) => readonly Token[] | Promise` -- **Default:** Fetches `https://tokenlist.tempo.xyz/list/:chainId` - -Resolves the list of tokens to check balances for during fee token resolution. The relay checks `balanceOf` for each token and picks the one with the highest balance. - -```ts twoslash -import { Handler } from 'accounts/server' - -const handler = Handler.relay({ - resolveTokens: (chainId) => [ // [!code focus] - { // [!code focus] - address: '0x20c0000000000000000000000000000000000000', // [!code focus] - decimals: 6, // [!code focus] - name: 'pathUSD', // [!code focus] - symbol: 'pathUSD', // [!code focus] - }, // [!code focus] - { // [!code focus] - address: '0x20c000000000000000000000b9537d11c60e8b50', // [!code focus] - decimals: 6, // [!code focus] - name: 'USDC.e', // [!code focus] - symbol: 'USDC.e', // [!code focus] - }, // [!code focus] - ], // [!code focus] -}) -``` - -### transports - -- **Type:** `Record` -- **Default:** `http()` for each chain - -Transports keyed by chain ID. - -```ts twoslash -import { http } from 'viem' -import { tempo } from 'viem/chains' -import { Handler } from 'accounts/server' - -const handler = Handler.relay({ - transports: { // [!code focus] - [tempo.id]: http('https://rpc.tempo.xyz'), // [!code focus] - }, // [!code focus] -}) -``` - -The relay enriches the standard `eth_fillTransaction` response with a `capabilities` object: - -```ts -type Response = { - /** Wallet-specific capabilities computed during fill. */ - capabilities: { - /** Per-account balance diffs from simulation (swap-related diffs excluded). */ - balanceDiffs?: { - [account: Address]: BalanceDiff[] - } - /** Fee estimate for the transaction. */ - fee: { - /** Raw fee amount in token units (hex-encoded). */ - amount: Hex - /** Token decimals (e.g. 6). */ - decimals: number - /** Human-readable fee (e.g. "0.028022"). */ - formatted: string - /** Token symbol (e.g. "AlphaUSD"). */ - symbol: string - } | null - /** AMM swap injected to cover an insufficient balance. */ - autoSwap?: { - /** Max input amount with slippage. */ - maxIn: SwapAmount - /** Deficit amount that triggered the swap. */ - minOut: SwapAmount - /** Slippage tolerance (e.g. 0.05 = 5%). */ - slippage: number - } - /** Structured error details when the fill fails (e.g. InsufficientBalance). */ - error?: { - /** ABI item that caused the error. */ - abiItem: AbiItem - /** Data that caused the error. */ - data: Hex - /** Revert error name (e.g. "InsufficientBalance"). */ - errorName: string - /** Human-readable error message. */ - message: string - } - /** Funding requirement when InsufficientBalance is encountered. */ - requireFunds?: { - /** Deficit amount in token units (hex-encoded). */ - amount: Hex - /** Token decimals (e.g. 6). */ - decimals: number - /** Human-readable deficit (e.g. "100.000000"). */ - formatted: string - /** Token address. */ - token: Address - /** Token symbol (e.g. "USDC.e"). */ - symbol: string - } - /** Sponsor details (when sponsored). */ - sponsor?: { address: Address; name: string; url: string } - /** Whether the transaction is sponsored by a fee payer. */ - sponsored: boolean - } - /** Fully filled transaction. */ - tx: { - // ...filled tx fields - /** Resolved fee token used for this transaction. */ - feeToken: Address - } -} - -type BalanceDiff = { - /** Token address. */ - address: Address - /** Token decimals (e.g. 6). */ - decimals: number - /** Direction relative to the user. */ - direction: 'incoming' | 'outgoing' - /** Human-readable formatted amount (e.g. "100.00"). */ - formatted: string - /** Token name (e.g. "USDC.e"). */ - name: string - /** Addresses receiving this asset. */ - recipients: readonly Address[] - /** Token symbol (e.g. "USDC.e"). */ - symbol: string - /** Token amount (hex-encoded). */ - value: Hex -} - -type SwapAmount = { - /** Token decimals. */ - decimals: number - /** Human-readable formatted amount. */ - formatted: string - /** Token name (e.g. "AlphaUSD"). */ - name: string - /** Token symbol (e.g. "AlphaUSD"). */ - symbol: string - /** Token address. */ - token: Address - /** Amount (hex-encoded). */ - value: Hex -} -``` diff --git a/src/pages/accounts/server/handler.webAuthn.mdx b/src/pages/accounts/server/handler.webAuthn.mdx deleted file mode 100644 index 97a0eb1b..00000000 --- a/src/pages/accounts/server/handler.webAuthn.mdx +++ /dev/null @@ -1,180 +0,0 @@ ---- -title: Handler.webAuthn -description: Server-side WebAuthn ceremony handler for registration and authentication. ---- - -# `Handler.webAuthn` - -Creates a WebAuthn ceremony handler that manages registration and authentication flows server-side. - -:::info -[See the guide](/guide/use-accounts/embed-passkeys) -::: - -Exposes 4 POST endpoints: - -- `POST /register/options` — Generate credential creation options -- `POST /register` — Verify registration and store credential -- `POST /login/options` — Generate credential request options -- `POST /login` — Verify authentication - -## Usage - -```ts -import { Handler, Kv } from 'accounts/server' - -const handler = Handler.webAuthn({ - kv: Kv.memory(), - origin: 'https://example.com', - rpId: 'example.com', -}) -``` - -Then plug `handler` into your server framework of choice: - -```ts -createServer(handler.listener) // Node.js -Bun.serve(handler) // Bun -Deno.serve(handler) // Deno -app.all('*', c => handler.fetch(c.request)) // Elysia -app.use(handler.listener) // Express -app.use(c => handler.fetch(c.req.raw)) // Hono -export const GET = handler.fetch // Next.js -export const POST = handler.fetch // Next.js -``` - -:::warning -`Kv.memory()` is not recommended for production use. Instead, use a persistent store like Cloudflare or Vercel KV, or a Redis instance. See [`Kv`](/accounts/server/kv) for available adapters. -::: - -## Parameters - -### kv - -- **Type:** [`Kv`](/accounts/server/kv) -- **Required** - -Key-value store for challenges and credentials. [See `Kv`](/accounts/server/kv) for available adapters. - -```ts twoslash -import { Handler, Kv } from 'accounts/server' - -const handler = Handler.webAuthn({ - kv: Kv.memory(), // [!code focus] - origin: 'https://example.com', - rpId: 'example.com', -}) -``` - -### onAuthenticate - -- **Type:** `(params: { credentialId, publicKey, userId?, request }) => Response | void` -- **Optional** - -Called after a successful authentication. - -```ts twoslash -import { Handler, Kv } from 'accounts/server' - -const handler = Handler.webAuthn({ - kv: Kv.memory(), - origin: 'https://example.com', - rpId: 'example.com', - onAuthenticate: ({ credentialId, publicKey }) => { // [!code focus] - console.log('Authenticated:', credentialId) // [!code focus] - }, // [!code focus] -}) -``` - -### onRegister - -- **Type:** `(params: { credentialId, publicKey, request }) => Response | void` -- **Optional** - -Called after a successful registration. The returned response is merged onto the default JSON response. - -```ts twoslash -import { Handler, Kv } from 'accounts/server' - -const handler = Handler.webAuthn({ - kv: Kv.memory(), - origin: 'https://example.com', - rpId: 'example.com', - onRegister: ({ credentialId, publicKey }) => { // [!code focus] - console.log('Registered:', credentialId) // [!code focus] - }, // [!code focus] -}) -``` - -### origin - -- **Type:** `string | readonly string[]` -- **Required** - -Expected origin(s) for WebAuthn verification (e.g. `'https://example.com'`). - -```ts twoslash -import { Handler, Kv } from 'accounts/server' - -const handler = Handler.webAuthn({ - kv: Kv.memory(), - origin: 'https://example.com', // [!code focus] - rpId: 'example.com', -}) -``` - -### path - -- **Type:** `string` -- **Default:** `''` - -Path prefix for the WebAuthn endpoints. - -```ts twoslash -import { Handler, Kv } from 'accounts/server' - -const handler = Handler.webAuthn({ - kv: Kv.memory(), - origin: 'https://example.com', - rpId: 'example.com', - path: '/webauthn', // [!code focus] -}) -``` - -### rpId - -- **Type:** `string` -- **Required** - -Relying Party ID (e.g. `'example.com'`). - -```ts twoslash -import { Handler, Kv } from 'accounts/server' - -const handler = Handler.webAuthn({ - kv: Kv.memory(), - origin: 'https://example.com', - rpId: 'example.com', // [!code focus] -}) -``` - -### ttl - -- **Type:** `{ challenge?: number; session?: number }` -- **Default:** `{ challenge: 300, session: 86400 }` - -TTLs in seconds for challenges (5 minutes) and sessions (24 hours). - -```ts twoslash -import { Handler, Kv } from 'accounts/server' - -const handler = Handler.webAuthn({ - kv: Kv.memory(), - origin: 'https://example.com', - rpId: 'example.com', - ttl: { // [!code focus] - challenge: 600, // [!code focus] - session: 3600, // [!code focus] - }, // [!code focus] -}) -``` diff --git a/src/pages/accounts/server/index.mdx b/src/pages/accounts/server/index.mdx deleted file mode 100644 index 4ff31794..00000000 --- a/src/pages/accounts/server/index.mdx +++ /dev/null @@ -1,53 +0,0 @@ ---- -title: Tempo Accounts Server Handlers -description: Configure server-side Tempo Accounts SDK handlers for relaying wallet RPC requests, composing backends, and managing WebAuthn ceremonies. ---- - -import { Cards, Card } from 'vocs' - -# Handlers - -Server handlers are framework-agnostic handlers that run on your backend to manage protocol operations that require server-side logic. - -Handlers are compatible with any server framework that supports the: -- [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API), and is exposed via the `Handler#fetch` function -- [Node.js `RequestListener` API](https://nodejs.org/api/http.html#http_class_http_serverrequestlistener), and is exposed via the `Handler#listener` function - -```ts -import { Handler } from 'accounts/server' -import { privateKeyToAccount } from 'viem/accounts' - -const handler = Handler.relay({ - feePayer: { account: privateKeyToAccount('0x...') }, -}) - -createServer(handler.listener) // Node.js -Bun.serve(handler) // Bun -Deno.serve(handler) // Deno -app.all('*', c => handler.fetch(c.request)) // Elysia -app.use(handler.listener) // Express -app.use(c => handler.fetch(c.req.raw)) // Hono -export const GET = handler.fetch // Next.js -export const POST = handler.fetch // Next.js -``` - - - - - - diff --git a/src/pages/accounts/server/kv.mdx b/src/pages/accounts/server/kv.mdx deleted file mode 100644 index efa1252f..00000000 --- a/src/pages/accounts/server/kv.mdx +++ /dev/null @@ -1,54 +0,0 @@ ---- -title: Kv -description: Key-value store adapters for server-side persistence. ---- - -# `Kv` - -Key-value store interface used by server handlers for persistence (challenges, credentials, device codes). - -## Usage - -```ts -import { Kv } from 'accounts/server' - -// In-memory (for development/testing) -const kv = Kv.memory() - -// Cloudflare Workers KV -const kv = Kv.cloudflare(env.MY_KV_NAMESPACE) -``` - -## Adapters - -### Kv.memory() - -Creates an in-memory key-value store. Useful for development and testing. - -### Kv.cloudflare(namespace) - -Creates a key-value store backed by Cloudflare Workers KV. - -- **namespace** — Cloudflare KV namespace binding. - -### Kv.from(kv) - -Creates a key-value store from a custom implementation. - -```ts -const kv = Kv.from({ - async get(key) { /* ... */ }, - async set(key, value) { /* ... */ }, - async delete(key) { /* ... */ }, -}) -``` - -## Interface - -```ts -type Kv = { - get: (key: string) => Promise - set: (key: string, value: unknown) => Promise - delete: (key: string) => Promise -} -``` diff --git a/src/pages/accounts/wagmi/tempoWallet.mdx b/src/pages/accounts/wagmi/tempoWallet.mdx deleted file mode 100644 index 57a24d30..00000000 --- a/src/pages/accounts/wagmi/tempoWallet.mdx +++ /dev/null @@ -1,179 +0,0 @@ ---- -title: tempoWallet -description: Wagmi connector for the Tempo Wallet dialog. ---- - -# `tempoWallet` - -Creates a Wagmi connector backed by the Tempo Wallet dialog adapter. - -## Usage - -```ts -import { createConfig, http } from 'wagmi' -import { tempo } from 'wagmi/chains' -import { tempoWallet } from 'wagmi/connectors' - -export const config = createConfig({ - chains: [tempo], - connectors: [tempoWallet()], - transports: { - [tempo.id]: http(), - }, -}) -``` - -## Parameters - -Accepts all [`dialog`](/accounts/api/dialog) adapter options, plus all [`Provider`](/accounts/api/provider) options except `adapter`. - -### authorizeAccessKey - -- **Type:** `() => { expiry: number; limits?: { token: Address; limit: bigint }[] }` -- **Optional** - -Default access key parameters for `wallet_connect`. - -```ts twoslash -import { createConfig, http } from 'wagmi' -import { tempo } from 'wagmi/chains' -import { parseUnits } from 'viem' -import { Expiry } from 'accounts' -import { tempoWallet } from 'wagmi/connectors' - -export const config = createConfig({ - chains: [tempo], - connectors: [ - tempoWallet({ - authorizeAccessKey: () => ({ // [!code focus] - expiry: Expiry.days(7), // [!code focus] - limits: [{ // [!code focus] - token: '0x20c0000000000000000000000000000000000001', // [!code focus] - limit: parseUnits('500', 6), // [!code focus] - }], // [!code focus] - }), // [!code focus] - }), - ], - transports: { [tempo.id]: http() }, -}) -``` - -### dialog - -- **Type:** `Dialog` -- **Default:** `Dialog.iframe()` (or `Dialog.popup()` in Safari/insecure contexts) - -Dialog to use for the embed app. - -```ts twoslash -import { createConfig, http } from 'wagmi' -import { tempo } from 'wagmi/chains' -import { Dialog } from 'accounts' -import { tempoWallet } from 'wagmi/connectors' - -export const config = createConfig({ - chains: [tempo], - connectors: [ - tempoWallet({ - dialog: Dialog.popup(), // [!code focus] - }), - ], - transports: { [tempo.id]: http() }, -}) -``` - -### feePayer - -- **Type:** `string | { url: string; precedence?: 'fee-payer-first' | 'user-first' }` -- **Optional** - -Fee payer configuration for interacting with a service running [`Handler.relay`](/accounts/server/handler.relay) from `accounts/server`. Pass a URL string, or an object with `url` and optional `precedence`. - -:::info -[See the guide](/guide/payments/sponsor-user-fees) -::: - -```ts twoslash -import { createConfig, http } from 'wagmi' -import { tempo } from 'wagmi/chains' -import { tempoWallet } from 'wagmi/connectors' - -export const config = createConfig({ - chains: [tempo], - connectors: [ - tempoWallet({ - feePayer: 'https://myapp.com/fee-payer', // [!code focus] - }), - ], - transports: { [tempo.id]: http() }, -}) -``` - -### host - -- **Type:** `string` -- **Default:** `'https://wallet.tempo.xyz/embed'` - -URL of the embed app. - -```ts twoslash -import { createConfig, http } from 'wagmi' -import { tempo } from 'wagmi/chains' -import { tempoWallet } from 'wagmi/connectors' - -export const config = createConfig({ - chains: [tempo], - connectors: [ - tempoWallet({ - host: 'https://wallet.tempo.xyz/embed', // [!code focus] - }), - ], - transports: { [tempo.id]: http() }, -}) -``` - -### mpp - -- **Type:** `boolean` -- **Default:** `false` - -Enable [Machine Payment Protocol](https://mpp.dev) support. - -```ts twoslash -import { createConfig, http } from 'wagmi' -import { tempo } from 'wagmi/chains' -import { tempoWallet } from 'wagmi/connectors' - -export const config = createConfig({ - chains: [tempo], - connectors: [ - tempoWallet({ - mpp: true, // [!code focus] - }), - ], - transports: { [tempo.id]: http() }, -}) -``` - -### testnet - -- **Type:** `boolean` -- **Default:** `false` - -Use testnet. When `true`, the default chain will be the first testnet chain in `chains`. - -```ts twoslash -import { createConfig, http } from 'wagmi' -import { tempo } from 'wagmi/chains' -import { tempoWallet } from 'wagmi/connectors' - -export const config = createConfig({ - chains: [tempo], - connectors: [ - tempoWallet({ - testnet: true, // [!code focus] - }), - ], - transports: { [tempo.id]: http() }, -}) -``` diff --git a/src/pages/accounts/wagmi/webAuthn.mdx b/src/pages/accounts/wagmi/webAuthn.mdx deleted file mode 100644 index 7b37377d..00000000 --- a/src/pages/accounts/wagmi/webAuthn.mdx +++ /dev/null @@ -1,199 +0,0 @@ ---- -title: webAuthn -description: Wagmi connector for passkey-based WebAuthn accounts. ---- - -# `webAuthn` - -Creates a Wagmi connector backed by the WebAuthn adapter. - -## Usage - -```ts -import { createConfig, http } from 'wagmi' -import { tempo } from 'wagmi/chains' -import { webAuthn } from 'wagmi/tempo' - -export const config = createConfig({ - chains: [tempo], - connectors: [webAuthn({ authUrl: '/auth' })], - transports: { - [tempo.id]: http(), - }, -}) -``` - -## Parameters - -Accepts all [`webAuthn`](/accounts/api/webAuthn) adapter options, plus all [`Provider`](/accounts/api/provider) options except `adapter`. - -### authUrl - -- **Type:** `string` -- **Optional** - -URL of a [WebAuthn server handler](/accounts/server/handler.webAuthn). - -:::info -[See the guide](/guide/use-accounts/embed-passkeys) -::: - -:::warning -Cannot be used with `ceremony`. -::: - -```ts twoslash -import { createConfig, http } from 'wagmi' -import { tempo } from 'wagmi/chains' -import { webAuthn } from 'wagmi/tempo' - -export const config = createConfig({ - chains: [tempo], - connectors: [ - webAuthn({ - authUrl: '/auth', // [!code focus] - }), - ], - transports: { [tempo.id]: http() }, -}) -``` - -### authorizeAccessKey - -- **Type:** `() => { expiry: number; limits?: { token: Address; limit: bigint }[] }` -- **Optional** - -Default access key parameters for `wallet_connect`. - -```ts twoslash -import { createConfig, http } from 'wagmi' -import { tempo } from 'wagmi/chains' -import { parseUnits } from 'viem' -import { Expiry } from 'accounts' -import { webAuthn } from 'wagmi/tempo' - -export const config = createConfig({ - chains: [tempo], - connectors: [ - webAuthn({ - authUrl: '/auth', - authorizeAccessKey: () => ({ // [!code focus] - expiry: Expiry.days(7), // [!code focus] - limits: [{ // [!code focus] - token: '0x20c0000000000000000000000000000000000001', // [!code focus] - limit: parseUnits('500', 6), // [!code focus] - }], // [!code focus] - }), // [!code focus] - }), - ], - transports: { [tempo.id]: http() }, -}) -``` - -### ceremony - -- **Type:** `WebAuthnCeremony` -- **Default:** `WebAuthnCeremony.server({ url: authUrl }){:js}` - -Ceremony strategy for WebAuthn registration and authentication. [See more](/accounts/api/webauthnceremony). - -:::info -[See the guide](/guide/use-accounts/embed-passkeys) -::: - -:::warning -Cannot be used with `authUrl`. -::: - -```ts twoslash -import { createConfig, http } from 'wagmi' -import { tempo } from 'wagmi/chains' -import { WebAuthnCeremony } from 'accounts' -import { webAuthn } from 'wagmi/tempo' - -export const config = createConfig({ - chains: [tempo], - connectors: [ - webAuthn({ - ceremony: WebAuthnCeremony.server({ url: '/auth' }), // [!code focus] - }), - ], - transports: { [tempo.id]: http() }, -}) -``` - -### feePayer - -- **Type:** `string | { url: string; precedence?: 'fee-payer-first' | 'user-first' }` -- **Optional** - -Fee payer configuration for interacting with a service running [`Handler.relay`](/accounts/server/handler.relay) from `accounts/server`. Pass a URL string, or an object with `url` and optional `precedence`. - -:::info -[See the guide](/guide/payments/sponsor-user-fees) -::: - -```ts twoslash -import { createConfig, http } from 'wagmi' -import { tempo } from 'wagmi/chains' -import { webAuthn } from 'wagmi/tempo' - -export const config = createConfig({ - chains: [tempo], - connectors: [ - webAuthn({ - authUrl: '/auth', - feePayer: 'https://myapp.com/fee-payer', // [!code focus] - }), - ], - transports: { [tempo.id]: http() }, -}) -``` - -### mpp - -- **Type:** `boolean` -- **Default:** `false` - -Enable [Machine Payment Protocol](https://mpp.dev) support. - -```ts twoslash -import { createConfig, http } from 'wagmi' -import { tempo } from 'wagmi/chains' -import { webAuthn } from 'wagmi/tempo' - -export const config = createConfig({ - chains: [tempo], - connectors: [ - webAuthn({ - authUrl: '/auth', - mpp: true, // [!code focus] - }), - ], - transports: { [tempo.id]: http() }, -}) -``` - -### testnet - -- **Type:** `boolean` -- **Default:** `false` - -Use testnet. When `true`, the default chain will be the first testnet chain in `chains`. - -```ts twoslash -import { createConfig, http } from 'wagmi' -import { tempo } from 'wagmi/chains' -import { webAuthn } from 'wagmi/tempo' - -export const config = createConfig({ - chains: [tempo], - connectors: [ - webAuthn({ - authUrl: '/auth', - testnet: true, // [!code focus] - }), - ], - transports: { [tempo.id]: http() }, -}) -``` diff --git a/src/pages/build/index.tsx b/src/pages/build/index.tsx new file mode 100644 index 00000000..9a2c3730 --- /dev/null +++ b/src/pages/build/index.tsx @@ -0,0 +1,12 @@ +'use client' + +import HomePage from '../../marketing/HomePage' +import MarketingRoute from '../../marketing/MarketingRoute' + +export default function Page() { + return ( + + + + ) +} diff --git a/src/pages/build/tempo-transactions.tsx b/src/pages/build/tempo-transactions.tsx new file mode 100644 index 00000000..846f5fa3 --- /dev/null +++ b/src/pages/build/tempo-transactions.tsx @@ -0,0 +1,12 @@ +'use client' + +import FeaturePage from '../../marketing/FeaturePage' +import MarketingRoute from '../../marketing/MarketingRoute' + +export default function Page() { + return ( + + + + ) +} diff --git a/src/pages/build/tip20-tokens.tsx b/src/pages/build/tip20-tokens.tsx new file mode 100644 index 00000000..0163c497 --- /dev/null +++ b/src/pages/build/tip20-tokens.tsx @@ -0,0 +1,12 @@ +'use client' + +import FeaturePage from '../../marketing/FeaturePage' +import MarketingRoute from '../../marketing/MarketingRoute' + +export default function Page() { + return ( + + + + ) +} diff --git a/src/pages/diagrams.tsx b/src/pages/diagrams.tsx new file mode 100644 index 00000000..14e89770 --- /dev/null +++ b/src/pages/diagrams.tsx @@ -0,0 +1,12 @@ +'use client' + +import DiagramsPage from '../marketing/DiagramsPage' +import MarketingRoute from '../marketing/MarketingRoute' + +export default function Page() { + return ( + + + + ) +} diff --git a/src/pages/docs/_layout.tsx b/src/pages/docs/_layout.tsx new file mode 100644 index 00000000..fad4f330 --- /dev/null +++ b/src/pages/docs/_layout.tsx @@ -0,0 +1,68 @@ +'use client' + +import { lazy, type PropsWithChildren, Suspense } from 'react' +import DocsHeader from '../../components/DocsHeader' +import DocsSidebarDrawer from '../../components/DocsSidebarDrawer' +import { usePageSettled } from '../../lib/pageSettled' + +const Analytics = lazy(() => + import('@vercel/analytics/react').then((module) => ({ default: module.Analytics })), +) +const SpeedInsights = lazy(() => + import('@vercel/speed-insights/react').then((module) => ({ default: module.SpeedInsights })), +) +const Toaster = lazy(() => import('sonner').then((module) => ({ default: module.Toaster }))) +const GoogleAnalytics = lazy(() => import('../../components/GoogleAnalytics')) +const PostHogSetup = lazy(() => import('../../components/PostHogSetup')) + +if (typeof window !== 'undefined') { + window.addEventListener('vite:preloadError', (event) => { + const key = `vite:preloadError:${(event as unknown as CustomEvent).detail?.message}` + if (!sessionStorage.getItem(key)) { + sessionStorage.setItem(key, '1') + window.location.reload() + } + }) +} + +export default function DocsLayout( + props: PropsWithChildren<{ + path: string + frontmatter?: { interactive?: boolean; mipd?: boolean } + }>, +) { + const pageSettled = usePageSettled() + const needsToaster = Boolean(props.frontmatter?.interactive || props.frontmatter?.mipd) + + return ( + <> + + + {props.children} + + {needsToaster && ( + + )} + {pageSettled && ( + <> + + + + + + )} + + + ) +} diff --git a/src/pages/changelog.md b/src/pages/docs/changelog.md similarity index 100% rename from src/pages/changelog.md rename to src/pages/docs/changelog.md diff --git a/src/pages/cli/download.mdx b/src/pages/docs/cli/download.mdx similarity index 98% rename from src/pages/cli/download.mdx rename to src/pages/docs/cli/download.mdx index 161e2d7b..a2986960 100644 --- a/src/pages/cli/download.mdx +++ b/src/pages/docs/cli/download.mdx @@ -104,4 +104,4 @@ tempo download --chain mainnet --minimal --resumable=false Use the [snapshots viewer](https://snapshots.tempo.xyz/) to compare snapshot profiles and copy generated commands. -Then start your node with [`tempo node`](/cli/node). +Then start your node with [`tempo node`](/docs/cli/node). diff --git a/src/pages/cli/index.mdx b/src/pages/docs/cli/index.mdx similarity index 94% rename from src/pages/cli/index.mdx rename to src/pages/docs/cli/index.mdx index 68b35f20..aabe79e1 100644 --- a/src/pages/cli/index.mdx +++ b/src/pages/docs/cli/index.mdx @@ -49,25 +49,25 @@ Dive into each command: diff --git a/src/pages/cli/node.mdx b/src/pages/docs/cli/node.mdx similarity index 88% rename from src/pages/cli/node.mdx rename to src/pages/docs/cli/node.mdx index d0e55f73..ca3e2106 100644 --- a/src/pages/cli/node.mdx +++ b/src/pages/docs/cli/node.mdx @@ -7,7 +7,7 @@ import { Cards, Card } from 'vocs' # `tempo node` -Run a Tempo node. For faster initial sync, first download a snapshot with [`tempo download`](/cli/download). For operational setup guides — system requirements, systemd configs, monitoring, validator onboarding — see [Run a Tempo Node](/guide/node). +Run a Tempo node. For faster initial sync, first download a snapshot with [`tempo download`](/docs/cli/download). For operational setup guides — system requirements, systemd configs, monitoring, validator onboarding — see [Run a Tempo Node](/docs/guide/node). Flags grouped by function: @@ -38,7 +38,7 @@ Flags grouped by function: | `--consensus.datadir ` | Separate volume for consensus data | :::warning -`--consensus.fee-recipient` is deprecated as of `v1.5.2` and will be removed in an upcoming release. See [updating the fee recipient](/guide/node/validator-lifecycle#update-the-fee-recipient). +`--consensus.fee-recipient` is deprecated as of `v1.5.2` and will be removed in an upcoming release. See [updating the fee recipient](/docs/guide/node/validator-lifecycle#update-the-fee-recipient). ::: ### Observability @@ -76,19 +76,19 @@ tempo node --datadir /data/tempo \ diff --git a/src/pages/cli/wallet.mdx b/src/pages/docs/cli/wallet.mdx similarity index 86% rename from src/pages/cli/wallet.mdx rename to src/pages/docs/cli/wallet.mdx index 7176a39a..b2585940 100644 --- a/src/pages/cli/wallet.mdx +++ b/src/pages/docs/cli/wallet.mdx @@ -70,7 +70,7 @@ Once logged in, verify everything works: tempo wallet whoami ``` -If `ready=true`, the wallet is ready for [`tempo request`](/cli/request). +If `ready=true`, the wallet is ready for [`tempo request`](/docs/cli/request). To disconnect: @@ -108,7 +108,7 @@ To transfer tokens to another address: tempo wallet transfer ``` -For more options, see [Getting Funds on Tempo](/guide/getting-funds). +For more options, see [Getting Funds on Tempo](/docs/guide/getting-funds). ## Discover services @@ -118,13 +118,13 @@ tempo wallet services --search tempo wallet services ``` -The [Machine Payments Protocol](https://mpp.dev/overview) (MPP) lets any HTTP endpoint accept payments inline. The service directory indexes MPP-registered providers — each entry shows endpoint URLs, HTTP methods, pricing, and request schemas. Use it to find the right URL and payload for [`tempo request`](/cli/request). +The [Machine Payments Protocol](https://mpp.dev/overview) (MPP) lets any HTTP endpoint accept payments inline. The service directory indexes MPP-registered providers — each entry shows endpoint URLs, HTTP methods, pricing, and request schemas. Use it to find the right URL and payload for [`tempo request`](/docs/cli/request). -Agents that support MCP can also use the read-only services MCP server at `https://mpp.dev/mcp/services`. See [Discover MPP services](/guide/machine-payments/discover-services) for MCP setup, JSON-RPC examples, and the public catalog API. +Agents that support MCP can also use the read-only services MCP server at `https://mpp.dev/mcp/services`. See [Discover MPP services](/docs/guide/machine-payments/discover-services) for MCP setup, JSON-RPC examples, and the public catalog API. ## Manage payment sessions -When you use [pay-as-you-go](/guide/machine-payments/pay-as-you-go) services, MPP opens a [session](https://mpp.dev/payment-methods/tempo/session) — a payment channel where your wallet deposits funds into a reserve contract, then pays per request using signed [vouchers](https://mpp.dev/protocol/credentials) off-chain. This avoids an on-chain transaction for every request, giving sub-100ms latency and near-zero per-request fees. +When you use [pay-as-you-go](/docs/guide/machine-payments/pay-as-you-go) services, MPP opens a [session](https://mpp.dev/payment-methods/tempo/session) — a payment channel where your wallet deposits funds into a reserve contract, then pays per request using signed [vouchers](https://mpp.dev/protocol/credentials) off-chain. This avoids an on-chain transaction for every request, giving sub-100ms latency and near-zero per-request fees. The CLI tracks session state locally: @@ -201,7 +201,7 @@ tempo wallet sessions close --orphaned ` after your integration has been approved and you have been provided an API key. For production use outside the hosted policy, contact Tempo or run your own fee payer with [`Handler.relay`](/accounts/server/handler.relay). +Mainnet hosted fee payer access is intended for approved integrations and partner programs. Use `https://sponsor.tempo.xyz/tp_` after your integration has been approved and you have been provided an API key. For production use outside the hosted policy, contact Tempo or run your own fee payer. When provided an API key, you can directly pass it in the request path in order to sponsor requests. @@ -47,7 +47,7 @@ Run your own fee payer when you need: - Full control over availability, latency, and operational policy - Sponsorship for traffic that is not covered by Tempo's hosted policy -See [Sponsor User Fees](/guide/payments/sponsor-user-fees) to deploy your own fee payer service. +See [Sponsor User Fees](/docs/guide/payments/sponsor-user-fees) to deploy your own fee payer service. ## Configure clients @@ -151,9 +151,7 @@ No paymaster contract, bundler, or EntryPoint contract is required. ## Next steps - + - - - + diff --git a/src/pages/developer-tools/indexer.mdx b/src/pages/docs/developer-tools/indexer.mdx similarity index 97% rename from src/pages/developer-tools/indexer.mdx rename to src/pages/docs/developer-tools/indexer.mdx index 4c9ae67b..13bee8db 100644 --- a/src/pages/developer-tools/indexer.mdx +++ b/src/pages/docs/developer-tools/indexer.mdx @@ -5,7 +5,7 @@ interactive: true --- import { Card, Cards } from 'vocs' -import { TidxQuery } from '../../components/TidxQuery' +import { TidxQuery } from '../../../components/TidxQuery' # Indexer (`tidx`) @@ -126,12 +126,12 @@ The `/query` endpoint accepts SQL and can expose decoded events on demand throug icon="lucide:plug" title="Connection details" description="Find RPC URLs, chain IDs, explorers, and network metadata." - to="/quickstart/connection-details" + to="/docs/quickstart/connection-details" /> diff --git a/src/pages/ecosystem/block-explorers.mdx b/src/pages/docs/ecosystem/block-explorers.mdx similarity index 94% rename from src/pages/ecosystem/block-explorers.mdx rename to src/pages/docs/ecosystem/block-explorers.mdx index 643836b0..3f839d9d 100644 --- a/src/pages/ecosystem/block-explorers.mdx +++ b/src/pages/docs/ecosystem/block-explorers.mdx @@ -9,7 +9,7 @@ View transactions, blocks, accounts, and token activity on Tempo. ## Tempo Explorer -Tempo's official Mainnet block explorer is available at [explore.tempo.xyz](https://explore.tempo.xyz). View transactions, blocks, accounts, and token activity on the Tempo network. Testnet block explorer is available at [explore.testnet.tempo.xyz](https://explore.testnet.tempo.xyz). For more connection information, see [Connect to the Network](/quickstart/connection-details). +Tempo's official Mainnet block explorer is available at [explore.tempo.xyz](https://explore.tempo.xyz). View transactions, blocks, accounts, and token activity on the Tempo network. Testnet block explorer is available at [explore.testnet.tempo.xyz](https://explore.testnet.tempo.xyz). For more connection information, see [Connect to the Network](/docs/quickstart/connection-details). ## Tenderly diff --git a/src/pages/ecosystem/bridges.mdx b/src/pages/docs/ecosystem/bridges.mdx similarity index 95% rename from src/pages/ecosystem/bridges.mdx rename to src/pages/docs/ecosystem/bridges.mdx index ac144338..0258b35e 100644 --- a/src/pages/ecosystem/bridges.mdx +++ b/src/pages/docs/ecosystem/bridges.mdx @@ -17,7 +17,7 @@ Bridge assets to Tempo through the [Across app](https://app.across.to/) and expl [Bungee](https://bungee.exchange) enables seamless swaps within and between blockchains. Bungee aggregates bridge and DEX liquidity to deliver fast, cost-efficient cross-chain transfers and swaps to and from Tempo, with a simple integration path via link, widget, or API. -Get started with the [Tempo Bungee guide](/guide/bridge-bungee), read the [Bungee docs](https://docs.bungee.exchange/), or try the [Bungee app](https://bungee.exchange). +Get started with the [Tempo Bungee guide](/docs/guide/bridge-bungee), read the [Bungee docs](https://docs.bungee.exchange/), or try the [Bungee app](https://bungee.exchange). ## LayerZero diff --git a/src/pages/ecosystem/data-analytics.mdx b/src/pages/docs/ecosystem/data-analytics.mdx similarity index 100% rename from src/pages/ecosystem/data-analytics.mdx rename to src/pages/docs/ecosystem/data-analytics.mdx diff --git a/src/pages/ecosystem/index.mdx b/src/pages/docs/ecosystem/index.mdx similarity index 85% rename from src/pages/ecosystem/index.mdx rename to src/pages/docs/ecosystem/index.mdx index 3482c13d..14bc3a91 100644 --- a/src/pages/ecosystem/index.mdx +++ b/src/pages/docs/ecosystem/index.mdx @@ -12,55 +12,55 @@ Integrating with Tempo is easy by leveraging services provided by our infrastruc diff --git a/src/pages/ecosystem/node-infrastructure.mdx b/src/pages/docs/ecosystem/node-infrastructure.mdx similarity index 100% rename from src/pages/ecosystem/node-infrastructure.mdx rename to src/pages/docs/ecosystem/node-infrastructure.mdx diff --git a/src/pages/ecosystem/orchestration.mdx b/src/pages/docs/ecosystem/orchestration.mdx similarity index 100% rename from src/pages/ecosystem/orchestration.mdx rename to src/pages/docs/ecosystem/orchestration.mdx diff --git a/src/pages/ecosystem/security-compliance.mdx b/src/pages/docs/ecosystem/security-compliance.mdx similarity index 100% rename from src/pages/ecosystem/security-compliance.mdx rename to src/pages/docs/ecosystem/security-compliance.mdx diff --git a/src/pages/ecosystem/smart-contract-libraries.mdx b/src/pages/docs/ecosystem/smart-contract-libraries.mdx similarity index 100% rename from src/pages/ecosystem/smart-contract-libraries.mdx rename to src/pages/docs/ecosystem/smart-contract-libraries.mdx diff --git a/src/pages/ecosystem/wallets.mdx b/src/pages/docs/ecosystem/wallets.mdx similarity index 100% rename from src/pages/ecosystem/wallets.mdx rename to src/pages/docs/ecosystem/wallets.mdx diff --git a/src/pages/guide/bridge-bungee.mdx b/src/pages/docs/guide/bridge-bungee.mdx similarity index 99% rename from src/pages/guide/bridge-bungee.mdx rename to src/pages/docs/guide/bridge-bungee.mdx index 9c02de7b..fe3bae07 100644 --- a/src/pages/guide/bridge-bungee.mdx +++ b/src/pages/docs/guide/bridge-bungee.mdx @@ -263,4 +263,4 @@ while (true) { - [Bungee API reference](https://docs.bungee.exchange/api-reference) - [Bungee Link](https://docs.bungee.exchange/integrate/bungee-link) - [SOCKET Protocol documentation](https://docs.socket.tech/) -- [Getting Funds on Tempo](/guide/getting-funds) +- [Getting Funds on Tempo](/docs/guide/getting-funds) diff --git a/src/pages/guide/bridge-layerzero.mdx b/src/pages/docs/guide/bridge-layerzero.mdx similarity index 99% rename from src/pages/guide/bridge-layerzero.mdx rename to src/pages/docs/guide/bridge-layerzero.mdx index 93e995f0..b6708afd 100644 --- a/src/pages/guide/bridge-layerzero.mdx +++ b/src/pages/docs/guide/bridge-layerzero.mdx @@ -561,5 +561,5 @@ How fees flow: - [LayerZero V2 documentation](https://docs.layerzero.network/v2) - [Stargate documentation](https://stargateprotocol.gitbook.io/stargate/v2-developer-docs) -- [Bridges & Exchanges on Tempo](/ecosystem/bridges) -- [Getting Funds on Tempo](/guide/getting-funds) +- [Bridges & Exchanges on Tempo](/docs/ecosystem/bridges) +- [Getting Funds on Tempo](/docs/guide/getting-funds) diff --git a/src/pages/guide/bridge-relay.mdx b/src/pages/docs/guide/bridge-relay.mdx similarity index 98% rename from src/pages/guide/bridge-relay.mdx rename to src/pages/docs/guide/bridge-relay.mdx index 6edbd3a5..64f6f0fb 100644 --- a/src/pages/guide/bridge-relay.mdx +++ b/src/pages/docs/guide/bridge-relay.mdx @@ -343,5 +343,5 @@ Common routes include Ethereum, Base, Arbitrum, Optimism, Polygon, and more. See - [Relay documentation](https://docs.relay.link) - [Relay API reference](https://docs.relay.link/references/api/overview) -- [Bridges & Exchanges on Tempo](/ecosystem/bridges) -- [Getting Funds on Tempo](/guide/getting-funds) +- [Bridges & Exchanges on Tempo](/docs/ecosystem/bridges) +- [Getting Funds on Tempo](/docs/guide/getting-funds) diff --git a/src/pages/guide/getting-funds.mdx b/src/pages/docs/guide/getting-funds.mdx similarity index 66% rename from src/pages/guide/getting-funds.mdx rename to src/pages/docs/guide/getting-funds.mdx index 6c3e265f..bee20c34 100644 --- a/src/pages/guide/getting-funds.mdx +++ b/src/pages/docs/guide/getting-funds.mdx @@ -4,9 +4,9 @@ description: Bridge assets to Tempo, add funds in Tempo Wallet, or use the fauce interactive: true --- -import * as Demo from '../../components/guides/Demo.tsx' -import { DepositToTempoWallet } from '../../components/guides/steps/wallet/DepositToTempoWallet.tsx' -import { SignInWithTempo } from '../../components/guides/steps/wallet/SignInWithTempo.tsx' +import * as Demo from '../../../components/guides/Demo.tsx' +import { DepositToTempoWallet } from '../../../components/guides/steps/wallet/DepositToTempoWallet.tsx' +import { SignInWithTempo } from '../../../components/guides/steps/wallet/SignInWithTempo.tsx' # Getting Funds on Tempo @@ -21,7 +21,7 @@ import { SignInWithTempo } from '../../components/guides/steps/wallet/SignInWith ### With the CLI -Install the [Tempo CLI](/cli/wallet) and log in to your wallet: +Install the [Tempo CLI](/docs/cli/wallet) and log in to your wallet: ```bash curl -fsSL https://tempo.xyz/install | bash @@ -56,14 +56,14 @@ Use one of these supported bridges to move assets to Tempo: When you bridge USDC with LayerZero (Stargate), it appears on Tempo as USDC.e. Stargate does not charge its transfer fee on the direct route between Ethereum and Tempo, but other routes can vary, so check the quote before sending. -- **[LayerZero (Stargate)](/guide/bridge-layerzero)**: Bridge USDC to and from Tempo via Stargate. See the [full bridging guide](/guide/bridge-layerzero) for contract addresses, code examples, and EndpointDollar details. +- **[LayerZero (Stargate)](/docs/guide/bridge-layerzero)**: Bridge USDC to and from Tempo via Stargate. See the [full bridging guide](/docs/guide/bridge-layerzero) for contract addresses, code examples, and EndpointDollar details. - **[Squid](https://app.squidrouter.com/)**: Swap and bridge assets to Tempo in one flow. - **[Relay](https://relay.link/)**: Bridge assets to Tempo with low fees. - **[Across](https://app.across.to/)**: Bridge assets to Tempo quickly with competitive fees. -- **[Bungee](/guide/bridge-bungee)**: Swap and bridge assets to and from Tempo using Bungee Deposit. See the [full Bungee guide](/guide/bridge-bungee) for quote, transaction, and status examples. +- **[Bungee](/docs/guide/bridge-bungee)**: Swap and bridge assets to and from Tempo using Bungee Deposit. See the [full Bungee guide](/docs/guide/bridge-bungee) for quote, transaction, and status examples. ## Testnet Funds -For development and testing, use the [Tempo Faucet](/quickstart/faucet). +For development and testing, use the [Tempo Faucet](/docs/quickstart/faucet). The faucet provides `pathUSD`, `alphaUSD`, `betaUSD`, and `thetaUSD` test stablecoins. diff --git a/src/pages/guide/issuance/create-a-stablecoin.mdx b/src/pages/docs/guide/issuance/create-a-stablecoin.mdx similarity index 86% rename from src/pages/guide/issuance/create-a-stablecoin.mdx rename to src/pages/docs/guide/issuance/create-a-stablecoin.mdx index 0d2a295f..d3ca4d8d 100644 --- a/src/pages/guide/issuance/create-a-stablecoin.mdx +++ b/src/pages/docs/guide/issuance/create-a-stablecoin.mdx @@ -4,14 +4,14 @@ interactive: true --- import { Cards, Card } from 'vocs' -import * as Demo from '../../../components/guides/Demo.tsx' -import { AddFunds } from '../../../components/guides/steps/payments/AddFunds.tsx' -import { Connect } from '../../../components/guides/steps/auth/Connect.tsx' -import { CreateToken } from '../../../components/guides/steps/issuance/CreateToken.tsx' +import * as Demo from '../../../../components/guides/Demo.tsx' +import { AddFunds } from '../../../../components/guides/steps/payments/AddFunds.tsx' +import { Connect } from '../../../../components/guides/steps/auth/Connect.tsx' +import { CreateToken } from '../../../../components/guides/steps/issuance/CreateToken.tsx' # Create a Stablecoin -Create your own stablecoin on Tempo using [TIP-20 Tokens](/protocol/tip20/overview). +Create your own stablecoin on Tempo using [TIP-20 Tokens](/docs/protocol/tip20/overview). TIP-20 tokens are designed specifically for payments with built-in compliance features, role-based permissions, and integration with Tempo's payment infrastructure. @@ -29,19 +29,20 @@ By the end of this guide, you will be able to create a stablecoin on Tempo. ::::steps -### Set up Wagmi & integrate accounts +### Set up Wagmi -Ensure that you have set up your project with Wagmi and integrated accounts by following either of the guides: +Ensure that you have set up your project with Wagmi, a Tempo chain config, and a wallet connector: -- [Embed Passkey accounts](/guide/use-accounts/embed-passkeys) -- [Connect to wallets](/guide/use-accounts/connect-to-wallets) +- [Connection details](/docs/quickstart/connection-details) +- [TypeScript SDK](/docs/sdk/typescript) +- [Wallet integration](/docs/quickstart/wallet-developers) ### Add testnet funds¹ Before we send off a transaction to deploy our stablecoin to the Tempo testnet, we need to make sure our account is funded with a stablecoin to cover the transaction fee. As we have configured our project to use `AlphaUSD` (`0x20c000…0001`) -as the [default fee token](/quickstart/integrate-tempo#default-fee-token), we will need to add some `AlphaUSD` to our account. +as the [default fee token](/docs/quickstart/integrate-tempo#default-fee-token), we will need to add some `AlphaUSD` to our account. Luckily, the built-in Tempo testnet faucet supports funding accounts with `AlphaUSD`. @@ -153,7 +154,7 @@ After this step, your users will be able to create a stablecoin by clicking the Tokens can also carry an optional on-chain `logoURI` ([TIP-1026](https://tips.sh/1026)) that wallets and explorers read directly from the token contract. It's set on the token contract and is independent of this creation flow; for the recommended format, use a square, rasterized PNG or WebP (max 256 bytes; `https`, `http`, `ipfs`, or `data` scheme). :::warning -The `currency` field is **immutable** after token creation and affects fee payment eligibility, DEX routing, and quote token pairing. See [Currency Declaration](/protocol/tip20/overview#currency-declaration) for guidelines on choosing the right value. **Only `USD` stablecoins can be used to pay transaction fees on Tempo.** +The `currency` field is **immutable** after token creation and affects fee payment eligibility, DEX routing, and quote token pairing. See [Currency Declaration](/docs/protocol/tip20/overview#currency-declaration) for guidelines on choosing the right value. **Only `USD` stablecoins can be used to pay transaction fees on Tempo.** ::: @@ -281,9 +282,9 @@ export const config = createConfig({ ### Next steps Now that you have created your first stablecoin, you can now: -- [Add your token to the Token List](/quickstart/tokenlist#adding-a-new-token) so it appears in wallets, explorers, and other apps on Tempo +- [Add your token to the Token List](/docs/quickstart/tokenlist#adding-a-new-token) so it appears in wallets, explorers, and other apps on Tempo - learn the [Best Practices](#best-practices) below -- follow a guide on how to [mint](/guide/issuance/mint-stablecoins) and [more](/guide/issuance/manage-stablecoin) with your stablecoin. +- follow a guide on how to [mint](/docs/guide/issuance/mint-stablecoins) and [more](/docs/guide/issuance/manage-stablecoin) with your stablecoin. :::: @@ -327,7 +328,7 @@ export function CreateStablecoin() { diff --git a/src/pages/guide/issuance/distribute-rewards.mdx b/src/pages/docs/guide/issuance/distribute-rewards.mdx similarity index 87% rename from src/pages/guide/issuance/distribute-rewards.mdx rename to src/pages/docs/guide/issuance/distribute-rewards.mdx index 56e4f2c6..f00146de 100644 --- a/src/pages/guide/issuance/distribute-rewards.mdx +++ b/src/pages/docs/guide/issuance/distribute-rewards.mdx @@ -3,15 +3,15 @@ description: Distribute rewards to token holders using TIP-20's built-in reward interactive: true --- -import * as Demo from '../../../components/guides/Demo.tsx' -import { AddFunds } from '../../../components/guides/steps/payments/AddFunds.tsx' -import { ClaimReward } from '../../../components/guides/steps/rewards/ClaimReward.tsx' -import { Connect } from '../../../components/guides/steps/auth/Connect.tsx' -import { CreateOrLoadToken } from '../../../components/guides/steps/issuance/CreateOrLoadToken.tsx' -import { GrantTokenRoles } from '../../../components/guides/steps/issuance/GrantTokenRoles.tsx' -import { MintToken } from '../../../components/guides/steps/issuance/MintToken.tsx' -import { OptInToRewards } from '../../../components/guides/steps/rewards/OptInToRewards.tsx' -import { StartReward } from '../../../components/guides/steps/rewards/StartReward.tsx' +import * as Demo from '../../../../components/guides/Demo.tsx' +import { AddFunds } from '../../../../components/guides/steps/payments/AddFunds.tsx' +import { ClaimReward } from '../../../../components/guides/steps/rewards/ClaimReward.tsx' +import { Connect } from '../../../../components/guides/steps/auth/Connect.tsx' +import { CreateOrLoadToken } from '../../../../components/guides/steps/issuance/CreateOrLoadToken.tsx' +import { GrantTokenRoles } from '../../../../components/guides/steps/issuance/GrantTokenRoles.tsx' +import { MintToken } from '../../../../components/guides/steps/issuance/MintToken.tsx' +import { OptInToRewards } from '../../../../components/guides/steps/rewards/OptInToRewards.tsx' +import { StartReward } from '../../../../components/guides/steps/rewards/StartReward.tsx' import { Cards, Card } from 'vocs' # Distribute Rewards @@ -41,7 +41,7 @@ Try out the complete rewards flow: create a token, opt in to receive rewards on ### [Optional] Create a Stablecoin -If you would like to distribute rewards on a token you have created, follow the [Create a Stablecoin](/guide/issuance/create-a-stablecoin) guide to deploy your token. +If you would like to distribute rewards on a token you have created, follow the [Create a Stablecoin](/docs/guide/issuance/create-a-stablecoin) guide to deploy your token. ### Tell Your Users to Opt In to Rewards @@ -262,13 +262,13 @@ function WatchOptIns() { diff --git a/src/pages/guide/issuance/index.mdx b/src/pages/docs/guide/issuance/index.mdx similarity index 84% rename from src/pages/guide/issuance/index.mdx rename to src/pages/docs/guide/issuance/index.mdx index b35ce4c0..e29a3d8e 100644 --- a/src/pages/guide/issuance/index.mdx +++ b/src/pages/docs/guide/issuance/index.mdx @@ -11,28 +11,28 @@ Create and manage your own stablecoin on Tempo. Learn how to issue tokens, manag diff --git a/src/pages/guide/issuance/manage-stablecoin.mdx b/src/pages/docs/guide/issuance/manage-stablecoin.mdx similarity index 92% rename from src/pages/guide/issuance/manage-stablecoin.mdx rename to src/pages/docs/guide/issuance/manage-stablecoin.mdx index 2ca43443..8534f3b3 100644 --- a/src/pages/guide/issuance/manage-stablecoin.mdx +++ b/src/pages/docs/guide/issuance/manage-stablecoin.mdx @@ -3,25 +3,25 @@ description: Configure stablecoin permissions, supply limits, and compliance pol interactive: true --- -import * as Demo from '../../../components/guides/Demo.tsx' -import { AddFunds } from '../../../components/guides/steps/payments/AddFunds.tsx' -import { BurnTokenBlocked } from '../../../components/guides/steps/issuance/BurnTokenBlocked.tsx' -import { Connect } from '../../../components/guides/steps/auth/Connect.tsx' -import { CreateOrLoadToken } from '../../../components/guides/steps/issuance/CreateOrLoadToken.tsx' -import { CreateTokenPolicy } from '../../../components/guides/steps/issuance/CreateTokenPolicy.tsx' -import { GrantTokenRoles } from '../../../components/guides/steps/issuance/GrantTokenRoles.tsx' -import { LinkTokenPolicy } from '../../../components/guides/steps/issuance/LinkTokenPolicy.tsx' -import { MintToken } from '../../../components/guides/steps/issuance/MintToken.tsx' -import { PauseUnpauseTransfers } from '../../../components/guides/steps/issuance/PauseUnpauseTransfers.tsx' -import { RevokeTokenRoles } from '../../../components/guides/steps/issuance/RevokeTokenRoles.tsx' -import { SetSupplyCap } from '../../../components/guides/steps/issuance/SetSupplyCap.tsx' +import * as Demo from '../../../../components/guides/Demo.tsx' +import { AddFunds } from '../../../../components/guides/steps/payments/AddFunds.tsx' +import { BurnTokenBlocked } from '../../../../components/guides/steps/issuance/BurnTokenBlocked.tsx' +import { Connect } from '../../../../components/guides/steps/auth/Connect.tsx' +import { CreateOrLoadToken } from '../../../../components/guides/steps/issuance/CreateOrLoadToken.tsx' +import { CreateTokenPolicy } from '../../../../components/guides/steps/issuance/CreateTokenPolicy.tsx' +import { GrantTokenRoles } from '../../../../components/guides/steps/issuance/GrantTokenRoles.tsx' +import { LinkTokenPolicy } from '../../../../components/guides/steps/issuance/LinkTokenPolicy.tsx' +import { MintToken } from '../../../../components/guides/steps/issuance/MintToken.tsx' +import { PauseUnpauseTransfers } from '../../../../components/guides/steps/issuance/PauseUnpauseTransfers.tsx' +import { RevokeTokenRoles } from '../../../../components/guides/steps/issuance/RevokeTokenRoles.tsx' +import { SetSupplyCap } from '../../../../components/guides/steps/issuance/SetSupplyCap.tsx' import { Cards, Card } from 'vocs' # Manage Your Stablecoin Configure your stablecoin's permissions, supply limits, and compliance policies after deployment. This guide covers granting roles to manage token operations, setting supply caps, configuring transfer policies, and controlling token transfers through pause/unpause functionality. -TIP-20 tokens use a role-based access control system that allows you to delegate different administrative functions to different addresses. For detailed information about the role system, see the [TIP-20 specification](/protocol/tip20/spec#role-based-access-control). +TIP-20 tokens use a role-based access control system that allows you to delegate different administrative functions to different addresses. For detailed information about the role system, see the [TIP-20 specification](/docs/protocol/tip20/spec#role-based-access-control). ## Steps @@ -31,7 +31,7 @@ In this guide, we'll walk through how to assign and check the **`issuer`** role, ### Setup -Before you can manage roles on your stablecoin, you need to create one. Follow the [Create a Stablecoin](/guide/issuance/create-a-stablecoin) guide to deploy your token. +Before you can manage roles on your stablecoin, you need to create one. Follow the [Create a Stablecoin](/docs/guide/issuance/create-a-stablecoin) guide to deploy your token. Once you've created your token, you can proceed to grant roles to specific addresses. @@ -369,7 +369,7 @@ Transfer policies can be: - **Whitelist**: Only authorized addresses can send/receive - **Blacklist**: Blocked addresses cannot send/receive -Learn more about configuring transfer policies in the [TIP-403 specification](/protocol/tip403/spec). +Learn more about configuring transfer policies in the [TIP-403 specification](/docs/protocol/tip403/spec). @@ -635,13 +635,13 @@ Ensure pause and unpause roles are assigned to trusted addresses and that your t diff --git a/src/pages/guide/issuance/mint-stablecoins.mdx b/src/pages/docs/guide/issuance/mint-stablecoins.mdx similarity index 94% rename from src/pages/guide/issuance/mint-stablecoins.mdx rename to src/pages/docs/guide/issuance/mint-stablecoins.mdx index 3194f696..a294047f 100644 --- a/src/pages/guide/issuance/mint-stablecoins.mdx +++ b/src/pages/docs/guide/issuance/mint-stablecoins.mdx @@ -3,13 +3,13 @@ description: Mint new tokens to increase your stablecoin's total supply. Grant t interactive: true --- -import * as Demo from '../../../components/guides/Demo.tsx' -import { AddFunds } from '../../../components/guides/steps/payments/AddFunds.tsx' -import { BurnToken } from '../../../components/guides/steps/issuance/BurnToken.tsx' -import { Connect } from '../../../components/guides/steps/auth/Connect.tsx' -import { CreateOrLoadToken } from '../../../components/guides/steps/issuance/CreateOrLoadToken.tsx' -import { GrantTokenRoles } from '../../../components/guides/steps/issuance/GrantTokenRoles.tsx' -import { MintToken } from '../../../components/guides/steps/issuance/MintToken.tsx' +import * as Demo from '../../../../components/guides/Demo.tsx' +import { AddFunds } from '../../../../components/guides/steps/payments/AddFunds.tsx' +import { BurnToken } from '../../../../components/guides/steps/issuance/BurnToken.tsx' +import { Connect } from '../../../../components/guides/steps/auth/Connect.tsx' +import { CreateOrLoadToken } from '../../../../components/guides/steps/issuance/CreateOrLoadToken.tsx' +import { GrantTokenRoles } from '../../../../components/guides/steps/issuance/GrantTokenRoles.tsx' +import { MintToken } from '../../../../components/guides/steps/issuance/MintToken.tsx' import { Cards, Card } from 'vocs' # Mint Stablecoins @@ -22,7 +22,7 @@ Create new tokens by minting them to a specified address. Minting increases the ### Create a Stablecoin -Before you can mint tokens, you need to create a stablecoin. Follow the [Create a Stablecoin](/guide/issuance/create-a-stablecoin) guide to deploy your token. +Before you can mint tokens, you need to create a stablecoin. Follow the [Create a Stablecoin](/docs/guide/issuance/create-a-stablecoin) guide to deploy your token. Once you've created your token, you can proceed to grant the issuer role and mint tokens. @@ -427,13 +427,13 @@ Assign the issuer role to dedicated treasury or minting addresses separate from diff --git a/src/pages/guide/issuance/use-for-fees.mdx b/src/pages/docs/guide/issuance/use-for-fees.mdx similarity index 86% rename from src/pages/guide/issuance/use-for-fees.mdx rename to src/pages/docs/guide/issuance/use-for-fees.mdx index f2150e7f..c30abf49 100644 --- a/src/pages/guide/issuance/use-for-fees.mdx +++ b/src/pages/docs/guide/issuance/use-for-fees.mdx @@ -4,14 +4,14 @@ interactive: true --- import { Cards, Card } from 'vocs' -import * as Demo from '../../../components/guides/Demo.tsx' -import { AddFunds } from '../../../components/guides/steps/payments/AddFunds.tsx' -import { Connect } from '../../../components/guides/steps/auth/Connect.tsx' -import { CreateOrLoadToken } from '../../../components/guides/steps/issuance/CreateOrLoadToken.tsx' -import { GrantTokenRoles } from '../../../components/guides/steps/issuance/GrantTokenRoles.tsx' -import { MintFeeAmmLiquidity } from '../../../components/guides/steps/amm/MintFeeAmmLiquidity.tsx' -import { MintToken } from '../../../components/guides/steps/issuance/MintToken.tsx' -import { PayWithIssuedToken } from '../../../components/guides/steps/payments/PayWithIssuedToken.tsx' +import * as Demo from '../../../../components/guides/Demo.tsx' +import { AddFunds } from '../../../../components/guides/steps/payments/AddFunds.tsx' +import { Connect } from '../../../../components/guides/steps/auth/Connect.tsx' +import { CreateOrLoadToken } from '../../../../components/guides/steps/issuance/CreateOrLoadToken.tsx' +import { GrantTokenRoles } from '../../../../components/guides/steps/issuance/GrantTokenRoles.tsx' +import { MintFeeAmmLiquidity } from '../../../../components/guides/steps/amm/MintFeeAmmLiquidity.tsx' +import { MintToken } from '../../../../components/guides/steps/issuance/MintToken.tsx' +import { PayWithIssuedToken } from '../../../../components/guides/steps/payments/PayWithIssuedToken.tsx' # Use Your Stablecoin for Fees @@ -35,7 +35,7 @@ Enable users to pay transaction fees using your stablecoin. Tempo supports flexi ### Create your stablecoin -First, create and mint your stablecoin by following the [Create a Stablecoin](/guide/issuance/create-a-stablecoin) guide. +First, create and mint your stablecoin by following the [Create a Stablecoin](/docs/guide/issuance/create-a-stablecoin) guide. ### Add fee pool liquidity @@ -115,7 +115,7 @@ require(reserveValidator > 0, "No liquidity available for fee conversion"); ::: -If the pool has no liquidity (`reserveValidatorToken == 0`), you'll need to add liquidity to the fee pool before users can pay fees with your token. See the [Create a Stablecoin](/guide/issuance/create-a-stablecoin) guide for instructions on minting fee AMM liquidity. +If the pool has no liquidity (`reserveValidatorToken == 0`), you'll need to add liquidity to the fee pool before users can pay fees with your token. See the [Create a Stablecoin](/docs/guide/issuance/create-a-stablecoin) guide for instructions on minting fee AMM liquidity. ### Send payment with your token as fee @@ -279,20 +279,20 @@ async fn main() -> Result<(), Box> { ::: -Users can set your stablecoin as their default fee token at the account level, or specify it for individual transactions. Learn more about [how users pay fees in different stablecoins](/guide/payments/pay-fees-in-any-stablecoin). +Users can set your stablecoin as their default fee token at the account level, or specify it for individual transactions. Learn more about [how users pay fees in different stablecoins](/docs/guide/payments/pay-fees-in-any-stablecoin). :::: ## How It Works -When users pay transaction fees with your stablecoin, Tempo's fee system automatically handles the conversion if validators prefer a different token. The [Fee AMM](/protocol/fees/spec-fee-amm) ensures seamless fee payments across all supported stablecoins. +When users pay transaction fees with your stablecoin, Tempo's fee system automatically handles the conversion if validators prefer a different token. The [Fee AMM](/docs/protocol/fees/spec-fee-amm) ensures seamless fee payments across all supported stablecoins. Users can select your stablecoin as their fee token through: - **Account-level preference**: Set as default for all transactions - **Transaction-level preference**: Specify for individual transactions - **Automatic selection**: When directly interacting with your token contract -Learn more about [how users pay fees in different stablecoins](/guide/payments/pay-fees-in-any-stablecoin) and the complete [fee token preference hierarchy](/protocol/fees/spec-fee#fee-token-preferences). +Learn more about [how users pay fees in different stablecoins](/docs/guide/payments/pay-fees-in-any-stablecoin) and the complete [fee token preference hierarchy](/docs/protocol/fees/spec-fee#fee-token-preferences). ## Benefits @@ -310,7 +310,7 @@ Regularly check your token's fee pool reserves to ensure users can consistently Keep sufficient validator token reserves in your fee pool to handle expected transaction volume. Consider your user base size and typical transaction frequency when determining reserve levels. -As fees accrue in your token, the pool will run low on validator tokens and need to be rebalanced. Use [`rebalanceSwap`](/guide/stablecoin-dex/managing-fee-liquidity#rebalance-liquidity) to replenish validator token reserves when they become depleted. +As fees accrue in your token, the pool will run low on validator tokens and need to be rebalanced. Use [`rebalanceSwap`](/docs/guide/stablecoin-dex/managing-fee-liquidity#rebalance-liquidity) to replenish validator token reserves when they become depleted. ### Test before launch @@ -325,19 +325,19 @@ Before promoting fee payments with your token, thoroughly test the flow on testn diff --git a/src/pages/guide/machine-payments/agent.mdx b/src/pages/docs/guide/machine-payments/agent.mdx similarity index 87% rename from src/pages/guide/machine-payments/agent.mdx rename to src/pages/docs/guide/machine-payments/agent.mdx index a7b589bf..570bd1ce 100644 --- a/src/pages/guide/machine-payments/agent.mdx +++ b/src/pages/docs/guide/machine-payments/agent.mdx @@ -39,7 +39,7 @@ tempo wallet services The service directory shows endpoint URLs, HTTP methods, pricing, and request schemas — everything you need to construct a valid request. -For MCP-capable agents, see [Discover MPP services](/guide/machine-payments/discover-services) to connect the read-only services MCP server at `https://mpp.dev/mcp/services`. +For MCP-capable agents, see [Discover MPP services](/docs/guide/machine-payments/discover-services) to connect the read-only services MCP server at `https://mpp.dev/mcp/services`. ### Preview cost @@ -88,24 +88,24 @@ Once installed, the agent can discover services, preview costs, and make paid re icon="lucide:search" title="Discover MPP services" description="Connect agents to the mpp.dev catalog over MCP" - to="/guide/machine-payments/discover-services" + to="/docs/guide/machine-payments/discover-services" /> diff --git a/src/pages/guide/machine-payments/client.mdx b/src/pages/docs/guide/machine-payments/client.mdx similarity index 95% rename from src/pages/guide/machine-payments/client.mdx rename to src/pages/docs/guide/machine-payments/client.mdx index 5505a417..67015ef9 100644 --- a/src/pages/guide/machine-payments/client.mdx +++ b/src/pages/docs/guide/machine-payments/client.mdx @@ -34,7 +34,7 @@ const account = privateKeyToAccount('0xabc…123') ``` :::tip -With Tempo, you can also use [Passkey or WebCrypto accounts](https://viem.sh/tempo/accounts). +With Tempo, you can also use passkey or WebCrypto signing. ::: ### Create payment handler @@ -185,13 +185,13 @@ console.log(receipt.reference) icon="lucide:server" title="Server quickstart" description="Add payment gating to your HTTP endpoints" - to="/guide/machine-payments/server" + to="/docs/guide/machine-payments/server" /> @@ -112,7 +112,7 @@ Two [intents](https://mpp.dev/protocol#payment-intents) are available on Tempo: | Tool | Package | Install | |-----|---------|---------| -| CLI | [`tempo request`](/cli/request) | `curl -fsSL https://tempo.xyz/install \| bash` | +| CLI | [`tempo request`](/docs/cli/request) | `curl -fsSL https://tempo.xyz/install \| bash` | | TypeScript | [`mppx`](https://github.com/wevm/mppx) | `npm install mppx viem` | | Python | [`pympp`](https://github.com/tempoxyz/pympp) | `pip install pympp` | | Rust | [`mpp-rs`](https://github.com/tempoxyz/mpp-rs) | `cargo add mpp` | diff --git a/src/pages/guide/machine-payments/one-time-payments.mdx b/src/pages/docs/guide/machine-payments/one-time-payments.mdx similarity index 97% rename from src/pages/guide/machine-payments/one-time-payments.mdx rename to src/pages/docs/guide/machine-payments/one-time-payments.mdx index 3b19af3f..d04db71b 100644 --- a/src/pages/guide/machine-payments/one-time-payments.mdx +++ b/src/pages/docs/guide/machine-payments/one-time-payments.mdx @@ -161,13 +161,13 @@ $ npx mppx http://localhost:3000/api/photo icon="lucide:repeat" title="Accept pay-as-you-go payments" description="Session-based billing with payment channels" - to="/guide/machine-payments/pay-as-you-go" + to="/docs/guide/machine-payments/pay-as-you-go" /> diff --git a/src/pages/guide/node/installation.mdx b/src/pages/docs/guide/node/installation.mdx similarity index 97% rename from src/pages/guide/node/installation.mdx rename to src/pages/docs/guide/node/installation.mdx index afdb4847..28fc2bdd 100644 --- a/src/pages/guide/node/installation.mdx +++ b/src/pages/docs/guide/node/installation.mdx @@ -4,11 +4,11 @@ description: Install Tempo node using pre-built binaries, build from source with # Installation -We provide three different installation paths — installing a pre-built binary, building from source, or using our provided Docker image. For the full CLI command reference, see [`tempo node`](/cli/node). +We provide three different installation paths — installing a pre-built binary, building from source, or using our provided Docker image. For the full CLI command reference, see [`tempo node`](/docs/cli/node). ## Versions -The required node version may differ across networks. See [Network Upgrades](/guide/node/network-upgrades) for the current version for each network. +The required node version may differ across networks. See [Network Upgrades](/docs/guide/node/network-upgrades) for the current version for each network. ## Pre-built Binary diff --git a/src/pages/guide/node/network-upgrades.mdx b/src/pages/docs/guide/node/network-upgrades.mdx similarity index 94% rename from src/pages/guide/node/network-upgrades.mdx rename to src/pages/docs/guide/node/network-upgrades.mdx index b5681d29..b6d9edf0 100644 --- a/src/pages/guide/node/network-upgrades.mdx +++ b/src/pages/docs/guide/node/network-upgrades.mdx @@ -3,13 +3,13 @@ title: Network Upgrades and Releases description: Timeline and details for Tempo network upgrades and important releases for node operators. --- -import { Badge } from '../../../components/Badge' +import { Badge } from '../../../../components/Badge' # Network Upgrades and Releases Tempo uses scheduled network upgrades to introduce protocol changes. Each upgrade goes through testnet activation before mainnet. This page also tracks important releases that node operators should be aware of. -For detailed release notes and binaries, see the [Changelog](/changelog). +For detailed release notes and binaries, see the [Changelog](/docs/changelog). ## Node Operator Updates @@ -41,7 +41,7 @@ For detailed release notes and binaries, see the [Changelog](/changelog). |---|---| | **Scope** | Lower fees for work that is expected to expire; lower the base fee when gas is below the target threshold; remove TIP-20 rewards | | **TIPs** | [TIP-1060: Gas Credits Primitive](https://tips.sh/1060), [TIP-1064: Gas Credits for DEX](https://tips.sh/1064), [TIP-1066: Gas Credits for MPP](https://tips.sh/1066), [TIP-1067: Dynamic Base Fee](https://tips.sh/1067-1), [TIP-1075: Remove TIP-20 Rewards](https://github.com/tempoxyz/tempo/pull/5380) | -| **Details** | [T7 network upgrade](/protocol/upgrades/t7) | +| **Details** | [T7 network upgrade](/docs/protocol/upgrades/t7) | | **Release** | v1.10.0 (planned) | | **Release date** | Planned: June 25, 2026 | | **Testnet** | Planned: June 30, 2026 | @@ -58,7 +58,7 @@ T7 is planned for v1.10.0, with release publication planned for June 25, 2026. R |---|---| | **Scope** | Account-level receive policies for safer TIP-20 deposits; admin access keys for smoother passkey, device, and delegated-key management | | **TIPs** | [TIP-1028: Account-Level Receive Policies](https://tips.sh/1028), [TIP-1049: Admin Access Keys](https://tips.sh/1049) | -| **Details** | [T6 network upgrade](/protocol/upgrades/t6) | +| **Details** | [T6 network upgrade](/docs/protocol/upgrades/t6) | | **Release** | [v1.9.0](https://github.com/tempoxyz/tempo/releases/tag/v1.9.0) | | **Testnet** | June 18, 2026 4pm CEST (unix: 1781791200) | | **Mainnet** | June 23, 2026 4pm CEST (unix: 1782223200) | @@ -68,7 +68,7 @@ T7 is planned for v1.10.0, with release publication planned for June 25, 2026. R Testnet operators must run v1.9.0 to stay synced after the T6 activation timestamp. Mainnet operators need to upgrade before the mainnet activation timestamp. Non-upgraded nodes will fall out of consensus once T6 activates on their network. -Integrators, indexers, wallets, explorers, and SDK maintainers should review the [T6 network upgrade](/protocol/upgrades/t6) page for feature benefits and integration notes. +Integrators, indexers, wallets, explorers, and SDK maintainers should review the [T6 network upgrade](/docs/protocol/upgrades/t6) page for feature benefits and integration notes. --- @@ -78,7 +78,7 @@ Integrators, indexers, wallets, explorers, and SDK maintainers should review the |---|---| | **Scope** | Enshrined TIP-20 reserve channel precompile; payment lane classification; DEX same-tick flip orders and persistent order IDs across flips; multihop FeeAMM routing; optional on-chain TIP-20 `logoURI`; implicit approvals; and key authorization witnesses | | **TIPs** | [TIP-1034: Enshrined TIP-20 Reserve Channel](https://github.com/tempoxyz/tempo/blob/main/tips/tip-1034.md), [TIP-1045: Payment Lane Classification](https://github.com/tempoxyz/tempo/blob/main/tips/tip-1045.md), [TIP-1030: Allow Same-Tick Flip Orders](https://github.com/tempoxyz/tempo/blob/main/tips/tip-1030.md), [TIP-1056: Keep Order IDs Across Flips](https://github.com/tempoxyz/tempo/blob/main/tips/tip-1056.md), [TIP-1033: Multihop FeeAMM Routing](https://github.com/tempoxyz/tempo/blob/main/tips/tip-1033.md), [TIP-1026: Optional logoURI Field in TIP-20](https://github.com/tempoxyz/tempo/blob/main/tips/tip-1026.md), [TIP-1035: Implicit Approvals List](https://github.com/tempoxyz/tempo/blob/main/tips/tip-1035.md), [TIP-1053: Witness Digest in Key Authorizations](https://github.com/tempoxyz/tempo/blob/main/tips/tip-1053.md) | -| **Details** | [T5 network upgrade](/protocol/upgrades/t5) | +| **Details** | [T5 network upgrade](/docs/protocol/upgrades/t5) | | **Release** | [v1.8.1](https://github.com/tempoxyz/tempo/releases/tag/v1.8.1) | | **Testnet** | June 3, 2026 16:00 CEST (unix: 1780495200) | | **Mainnet** | June 9, 2026 16:00 CEST (unix: 1781013600) | @@ -88,7 +88,7 @@ Integrators, indexers, wallets, explorers, and SDK maintainers should review the All node operators needed to upgrade before the T5 activation timestamp. -Integrators, indexers, wallets, explorers, and SDK maintainers should review the [T5 network upgrade](/protocol/upgrades/t5) page for the T5 surfaces and migration notes. +Integrators, indexers, wallets, explorers, and SDK maintainers should review the [T5 network upgrade](/docs/protocol/upgrades/t5) page for the T5 surfaces and migration notes. --- @@ -98,7 +98,7 @@ Integrators, indexers, wallets, explorers, and SDK maintainers should review the |---|---| | **Scope** | Embed consensus context into the block header to unlock deferred verification, plus T4 bug fixes and security hardening | | **TIPs** | [TIP-1031: Embed Consensus Context in the Block Header](https://github.com/tempoxyz/tempo/blob/main/tips/tip-1031.md), [TIP-1046: T4 Hardfork Meta TIP](https://github.com/tempoxyz/tempo/blob/main/tips/tip-1046.md) | -| **Details** | [T4 network upgrade](/protocol/upgrades/t4) | +| **Details** | [T4 network upgrade](/docs/protocol/upgrades/t4) | | **Release** | [v1.7.0](https://github.com/tempoxyz/tempo/releases/tag/v1.7.0) | | **Testnet** | Moderato: May 14, 2026 16:00 CEST (unix: 1778767200) | | **Mainnet** | Presto: May 18, 2026 16:00 CEST (unix: 1779112800) | @@ -118,13 +118,13 @@ Smart contract developers and integrators are not directly affected by TIP-1031, |---|---| | **Scope** | Enhanced access keys with periodic limits, call scoping, and an authorization ABI update; signature verification precompile; and virtual addresses for TIP-20 deposit forwarding | | **TIPs** | [TIP-1011: Enhanced Access Key Permissions](https://github.com/tempoxyz/tempo/blob/main/tips/tip-1011.md), [TIP-1020: Signature Verification Precompile](https://github.com/tempoxyz/tempo/blob/main/tips/tip-1020.md), [TIP-1022: Virtual Addresses for TIP-20 Deposit Forwarding](https://github.com/tempoxyz/tempo/blob/main/tips/tip-1022.md) | -| **Details** | [T3 network upgrade](/protocol/upgrades/t3) | +| **Details** | [T3 network upgrade](/docs/protocol/upgrades/t3) | | **Release** | [v1.6.0](https://github.com/tempoxyz/tempo/releases/tag/v1.6.0) | | **Testnet** | Moderato: Apr 21, 2026 16:00 CEST (unix: 1776780000) | | **Mainnet** | Presto: Apr 27, 2026 16:00 CEST (unix: 1777298400) | | **Priority** | Required | -See the [T3 network upgrade](/protocol/upgrades/t3) page for breaking changes, migration checklist, and integration guidance. +See the [T3 network upgrade](/docs/protocol/upgrades/t3) page for breaking changes, migration checklist, and integration guidance. --- diff --git a/src/pages/guide/node/rpc.mdx b/src/pages/docs/guide/node/rpc.mdx similarity index 98% rename from src/pages/guide/node/rpc.mdx rename to src/pages/docs/guide/node/rpc.mdx index e0a2425a..c6b680e9 100644 --- a/src/pages/guide/node/rpc.mdx +++ b/src/pages/docs/guide/node/rpc.mdx @@ -98,7 +98,7 @@ sudo journalctl -u tempo -f ## Monitoring -Once you've set up your node (whether it's with Systemd or Docker), you can verify that it's running correctly using these commands (`cast` requires installation of [Foundry](/sdk/foundry)): +Once you've set up your node (whether it's with Systemd or Docker), you can verify that it's running correctly using these commands (`cast` requires installation of [Foundry](/docs/sdk/foundry)): ```bash /dev/null/monitor.sh#L1-11 # Check service status diff --git a/src/pages/guide/node/security.mdx b/src/pages/docs/guide/node/security.mdx similarity index 72% rename from src/pages/guide/node/security.mdx rename to src/pages/docs/guide/node/security.mdx index 8016a4e0..681b011d 100644 --- a/src/pages/guide/node/security.mdx +++ b/src/pages/docs/guide/node/security.mdx @@ -14,10 +14,10 @@ Your signing key is the most sensitive asset on your validator. Anyone with acce - **Restrict file permissions** — set `chmod 600` on key files so only the node process user can read them. - **Never share your private key** — the Tempo team will never ask for it. - **Use different keys for testnet and mainnet** — do not reuse signing keys or operator keys across networks; a testnet compromise should never put your mainnet validator at risk. -- **Rotate keys periodically** — use [key rotation](/guide/node/validator-lifecycle#rotate-validator-identity) to swap to a new ed25519 key without leaving the committee. +- **Rotate keys periodically** — use [key rotation](/docs/guide/node/validator-lifecycle#rotate-validator-identity) to swap to a new ed25519 key without leaving the committee. - **Separate the operator address** — the Ethereum address that controls on-chain operations (IP updates, rotation, ownership transfer) should be a dedicated address, not a general-purpose hot wallet. -See [Managing validator keys](/guide/node/validator-keys) for the full key hierarchy and generation instructions. +See [Managing validator keys](/docs/guide/node/validator-keys) for the full key hierarchy and generation instructions. ## Network configuration @@ -29,25 +29,25 @@ Tempo's networking layer includes built-in protections, making a cloud firewall ### Recommendations -- **Expose only the required ports** — see [Ports](/guide/node/system-requirements#ports) for which ports need public access vs. internal-only. +- **Expose only the required ports** — see [Ports](/docs/guide/node/system-requirements#ports) for which ports need public access vs. internal-only. - **Keep ingress addresses unique** — the on-chain contract enforces uniqueness, but verify your registered `IP:port` is correct after infrastructure changes. - **Use a dedicated machine** — avoid running other internet-facing services on the same host as your validator. ## Release verification -Always verify release artifacts before running them. Tempo signs all binaries and Docker images — see [Verifying Releases](/guide/node/installation#verifying-releases) for GPG, Cosign, and SHA256 verification instructions. +Always verify release artifacts before running them. Tempo signs all binaries and Docker images — see [Verifying Releases](/docs/guide/node/installation#verifying-releases) for GPG, Cosign, and SHA256 verification instructions. ## Time synchronization -An unsynchronized clock can cause your node to reject valid blocks or produce blocks that other validators reject. Use `chrony` or `ntpd` — not `systemd-timesyncd`. See [Time Synchronization](/guide/node/system-requirements#time-synchronization) for setup instructions. +An unsynchronized clock can cause your node to reject valid blocks or produce blocks that other validators reject. Use `chrony` or `ntpd` — not `systemd-timesyncd`. See [Time Synchronization](/docs/guide/node/system-requirements#time-synchronization) for setup instructions. ## Data integrity -- **Never delete the data directory and re-sync with the same signing key** — this risks double-signing and will require [rotating to a new identity](/guide/node/validator-lifecycle#resetting-your-validators-data). Deleting only the `consensus` subdirectory is safe; the signing share will be [automatically recovered](/guide/node/validator-keys#signing-share-recovery). +- **Never delete the data directory and re-sync with the same signing key** — this risks double-signing and will require [rotating to a new identity](/docs/guide/node/validator-lifecycle#resetting-your-validators-data). Deleting only the `consensus` subdirectory is safe; the signing share will be [automatically recovered](/docs/guide/node/validator-keys#signing-share-recovery). - **Back up your signing key** — if the key file is lost and no backup exists, you will need to rotate to a new key and coordinate with the Tempo team. ## Staying up to date - Subscribe to [Tempo GitHub releases](https://github.com/tempoxyz/tempo/releases) for security patches and upgrade announcements. -- Review the [Upgrade Cadence](/guide/node/upgrade-cadence) to understand notification timelines. +- Review the [Upgrade Cadence](/docs/guide/node/upgrade-cadence) to understand notification timelines. - Apply Required upgrades before the activation block — failing to do so will fork your node off the network. diff --git a/src/pages/guide/node/system-requirements.mdx b/src/pages/docs/guide/node/system-requirements.mdx similarity index 99% rename from src/pages/guide/node/system-requirements.mdx rename to src/pages/docs/guide/node/system-requirements.mdx index 2215eb17..fd03c1b5 100644 --- a/src/pages/guide/node/system-requirements.mdx +++ b/src/pages/docs/guide/node/system-requirements.mdx @@ -82,7 +82,7 @@ Most cloud providers (AWS, Hetzner, OVH) pre-configure NTP, but minimal VM image ## Security -For network configuration, key management, release verification, and other security best practices, see the dedicated [Node Security](/guide/node/security) page. +For network configuration, key management, release verification, and other security best practices, see the dedicated [Node Security](/docs/guide/node/security) page. ## Network Tuning diff --git a/src/pages/guide/node/upgrade-cadence.mdx b/src/pages/docs/guide/node/upgrade-cadence.mdx similarity index 87% rename from src/pages/guide/node/upgrade-cadence.mdx rename to src/pages/docs/guide/node/upgrade-cadence.mdx index 8de1f5ae..07e826f4 100644 --- a/src/pages/guide/node/upgrade-cadence.mdx +++ b/src/pages/docs/guide/node/upgrade-cadence.mdx @@ -3,11 +3,11 @@ title: Upgrade Cadence description: How Tempo schedules and communicates network upgrades, including timelines, notification windows, and what to expect as a node operator. --- -import { Badge } from '../../../components/Badge' +import { Badge } from '../../../../components/Badge' # Upgrade Cadence -Tempo ships protocol upgrades on a **bi-weekly to monthly cadence**. Each upgrade bundles one or more protocol changes ([TIPs](/protocol/tips)) into a named hardfork (T1, T2, T3, …). +Tempo ships protocol upgrades on a **bi-weekly to monthly cadence**. Each upgrade bundles one or more protocol changes ([TIPs](/docs/protocol/tips)) into a named hardfork (T1, T2, T3, …). ## Rollout timeline @@ -26,7 +26,7 @@ Between hardforks, Tempo publishes patch releases (e.g. v1.5.1, v1.5.2) for secu ## Upgrade priority levels -Each release on the [Network Upgrades and Releases](/guide/node/network-upgrades) page carries a priority badge: +Each release on the [Network Upgrades and Releases](/docs/guide/node/network-upgrades) page carries a priority badge: | Badge | Meaning | |-------|---------| @@ -38,7 +38,7 @@ Each release on the [Network Upgrades and Releases](/guide/node/network-upgrades - **GitHub releases** — every release is published to [tempoxyz/tempo releases](https://github.com/tempoxyz/tempo/releases) with a full changelog. - **Operator channels** — the Tempo team shares activation timestamps and migration checklists in dedicated operator channels ahead of each upgrade. -- **Docs** — the [Network Upgrades and Releases](/guide/node/network-upgrades) page is updated with dates, TIP references, and priority badges as soon as an upgrade is scheduled. +- **Docs** — the [Network Upgrades and Releases](/docs/guide/node/network-upgrades) page is updated with dates, TIP references, and priority badges as soon as an upgrade is scheduled. ## Operator checklist diff --git a/src/pages/guide/node/validator-failover.mdx b/src/pages/docs/guide/node/validator-failover.mdx similarity index 91% rename from src/pages/guide/node/validator-failover.mdx rename to src/pages/docs/guide/node/validator-failover.mdx index 55125bf9..1782cfee 100644 --- a/src/pages/guide/node/validator-failover.mdx +++ b/src/pages/docs/guide/node/validator-failover.mdx @@ -19,7 +19,7 @@ Only one node may run with a validator signing key at a time. Before promoting t Use a dedicated data directory for the follower. The upstream must be a validator or RPC node that exposes the `consensus` RPCs. -Trustless RPC-to-RPC following is currently only supported on Moderato testnet. If following on Mainnet, follow against the existing validator. See more about [trustless rpc nodes](/guide/node/rpc#trustless-rpc-nodes). +Trustless RPC-to-RPC following is currently only supported on Moderato testnet. If following on Mainnet, follow against the existing validator. See more about [trustless rpc nodes](/docs/guide/node/rpc#trustless-rpc-nodes). ::::code-group ```bash [Testnet] @@ -57,7 +57,7 @@ If `consensus_getLatest` is unavailable, the follower is not syncing consensus s The promoted node must be reachable at the ingress and egress addresses registered onchain. A setup that allows a floating IP or load balancer address that can move from the old validator to the standby will require no onchain updates. -If failover uses a different address, update the validator's onchain IP configuration after fencing the old validator. Follow the [update IP addresses](/guide/node/validator-lifecycle#update-ip-addresses) procedure. +If failover uses a different address, update the validator's onchain IP configuration after fencing the old validator. Follow the [update IP addresses](/docs/guide/node/validator-lifecycle#update-ip-addresses) procedure. ::::: @@ -88,7 +88,7 @@ If the follower is not managed by systemd, send `SIGINT` or `SIGTERM` and wait f Restart the same data directory without `--follow` or `--follow.experimental.certify`, and add the validator signing key. Use the same consensus signing key as the validator you are failing over from. This procedure moves an existing validator identity to the standby; it does not rotate the identity. To use a different signing key, -the validator's onchain configuration must be rotated. Follow the [rotate the validator identity](/guide/node/validator-lifecycle#rotate-validator-identity) procedure. +the validator's onchain configuration must be rotated. Follow the [rotate the validator identity](/docs/guide/node/validator-lifecycle#rotate-validator-identity) procedure. ::::code-group ```bash [Mainnet] @@ -138,4 +138,4 @@ The validator should remain `in_committee: true` if it was already active. If yo Treat the promoted node as the active validator until you intentionally move the role again. -Use the [validator status metrics](/guide/node/validator-status#checking-state-via-metrics) to confirm the promoted node is connected to peers and participating in consensus. +Use the [validator status metrics](/docs/guide/node/validator-status#checking-state-via-metrics) to confirm the promoted node is connected to peers and participating in consensus. diff --git a/src/pages/guide/node/validator-keys.mdx b/src/pages/docs/guide/node/validator-keys.mdx similarity index 94% rename from src/pages/guide/node/validator-keys.mdx rename to src/pages/docs/guide/node/validator-keys.mdx index 78f972a0..3bab9303 100644 --- a/src/pages/guide/node/validator-keys.mdx +++ b/src/pages/docs/guide/node/validator-keys.mdx @@ -17,9 +17,9 @@ Use different signing keys and operator keys for testnet and mainnet. A testnet | Key / Address | Type | What it does | Sensitivity | How to change | |---|---|---|---|---| -| **Signing key** | Ed25519 keypair | Identifies your validator in the consensus protocol. Used for DKG participation, block proposals, and voting. | **Critical** — anyone with this key can impersonate your validator. | [Rotate validator identity](/guide/node/validator-lifecycle#rotate-validator-identity) | -| **Validator operator address** | Ethereum address (`0x…`) | The control address that authorizes on-chain operations: IP updates, key rotation, ownership transfer, and deactivation. | **High** — controls all validator configuration. | [Transfer validator ownership](/guide/node/validator-lifecycle#transfer-validator-ownership) | -| **Fee recipient** | Ethereum address (`0x…`) | Receives transaction fees from blocks your validator proposes. | **Low** — changing it only redirects future fee revenue, no security impact. | [Update fee recipient](/guide/node/validator-lifecycle#update-the-fee-recipient) | +| **Signing key** | Ed25519 keypair | Identifies your validator in the consensus protocol. Used for DKG participation, block proposals, and voting. | **Critical** — anyone with this key can impersonate your validator. | [Rotate validator identity](/docs/guide/node/validator-lifecycle#rotate-validator-identity) | +| **Validator operator address** | Ethereum address (`0x…`) | The control address that authorizes on-chain operations: IP updates, key rotation, ownership transfer, and deactivation. | **High** — controls all validator configuration. | [Transfer validator ownership](/docs/guide/node/validator-lifecycle#transfer-validator-ownership) | +| **Fee recipient** | Ethereum address (`0x…`) | Receives transaction fees from blocks your validator proposes. | **Low** — changing it only redirects future fee revenue, no security impact. | [Update fee recipient](/docs/guide/node/validator-lifecycle#update-the-fee-recipient) | | **Signing share** | BLS12-381 key share | A share of the committee's threshold signing key, used to sign block notarizations and finalizations. | **Managed automatically** — updated every DKG ceremony (~3 hours). Lost shares are recovered from the network on restart. | Automatic (see [recovery](#signing-share-recovery)) | ## Generating a signing key @@ -108,7 +108,7 @@ tempo consensus generate-signing-key \ ## Signing key rotation -The ed25519 signing key can be rotated while preserving your validator index and committee slot. See [Rotate validator identity](/guide/node/validator-lifecycle#rotate-validator-identity) for the full procedure. +The ed25519 signing key can be rotated while preserving your validator index and committee slot. See [Rotate validator identity](/docs/guide/node/validator-lifecycle#rotate-validator-identity) for the full procedure. ## Signing share recovery diff --git a/src/pages/guide/node/validator-lifecycle.mdx b/src/pages/docs/guide/node/validator-lifecycle.mdx similarity index 95% rename from src/pages/guide/node/validator-lifecycle.mdx rename to src/pages/docs/guide/node/validator-lifecycle.mdx index 81d6b175..8bdf2034 100644 --- a/src/pages/guide/node/validator-lifecycle.mdx +++ b/src/pages/docs/guide/node/validator-lifecycle.mdx @@ -35,7 +35,7 @@ Self-service data resets are coming soon. Once available, you will be able to ro ## Rotate validator identity -The ed25519 key can be changed while keeping your validator index stable. This is useful for key rotation or recovery without leaving and re-joining the committee. [Generate a new signing key](/guide/node/validator-keys#generating-a-signing-key) first. +The ed25519 key can be changed while keeping your validator index stable. This is useful for key rotation or recovery without leaving and re-joining the committee. [Generate a new signing key](/docs/guide/node/validator-keys#generating-a-signing-key) first. :::danger[Do not shut down the old validator] Unlike Ethereum, you must **keep your old validator running** after rotation. The rotated-out validator is still a dealer in the committee for at least one more epoch. Shutting it down early will degrade network liveness. See the [exit timeline](#exit-timeline) for the full epoch-by-epoch breakdown. @@ -83,7 +83,7 @@ If self-service rotation is not yet enabled for your validator, use `tempo conse Rotation preserves your validator index and active validator count. The old entry is appended to history as deactivated, and the entry at your index is updated in place. You must use a different ingress address (changing the port is sufficient). ::: -After rotation, your validator goes through the [standard state transitions](/guide/node/validator-status#state-transitions) with the new identity. +After rotation, your validator goes through the [standard state transitions](/docs/guide/node/validator-status#state-transitions) with the new identity. ## Update IP addresses @@ -181,4 +181,4 @@ Deactivation is not instant — your validator is phased out over two epochs: | **E+1** | Exiting | `true` | `false` | `true` | | **E+2** | Exited | `false` | `false` | `false` | -Keep your node running until your validator shows `in_committee: false`. Use [validator lookup](/guide/node/validator-status#look-up-your-validator) to check — it is safe to shut down the node at that point. +Keep your node running until your validator shows `in_committee: false`. Use [validator lookup](/docs/guide/node/validator-status#look-up-your-validator) to check — it is safe to shut down the node at that point. diff --git a/src/pages/guide/node/validator-monitoring.mdx b/src/pages/docs/guide/node/validator-monitoring.mdx similarity index 100% rename from src/pages/guide/node/validator-monitoring.mdx rename to src/pages/docs/guide/node/validator-monitoring.mdx diff --git a/src/pages/guide/node/validator-setup.mdx b/src/pages/docs/guide/node/validator-setup.mdx similarity index 94% rename from src/pages/guide/node/validator-setup.mdx rename to src/pages/docs/guide/node/validator-setup.mdx index 19232298..6a36a67d 100644 --- a/src/pages/guide/node/validator-setup.mdx +++ b/src/pages/docs/guide/node/validator-setup.mdx @@ -6,10 +6,10 @@ description: Generate signing keys and run your Tempo validator node for the fir # Validator onboarding :::info -The active validator set is currently permissioned. If you are interested in becoming a validator, please [get in touch](mailto:partners@tempo.xyz) with the Tempo team. +The active validator set is currently permissioned. If you are interested in becoming a validator, please [get in touch](https://tempo.xyz/contact) with the Tempo team. ::: -This guide walks through registering, generating your signing key, and starting your validator node for the first time. Before proceeding, make sure you have completed [system requirements](/guide/node/system-requirements) and [installation](/guide/node/installation). +This guide walks through registering, generating your signing key, and starting your validator node for the first time. Before proceeding, make sure you have completed [system requirements](/docs/guide/node/system-requirements) and [installation](/docs/guide/node/installation). ## Initial registration @@ -25,7 +25,7 @@ Registering a validator takes three steps: Never share your private signing key. Anyone with access to it can impersonate your validator. The Tempo team will never ask for your private key. ::: -Generate an encrypted ed25519 keypair. The `--secret` argument points to a file-like input that contains the encryption key. Prefer a named pipe (FIFO) or shell process substitution for this path: a FIFO lets one process stream bytes directly to another process without storing those bytes as a regular file, and it keeps the secret out of environment variables and command-line arguments. See [Why FIFOs and not env vars](/guide/node/validator-keys#why-fifos-and-not-env-vars). +Generate an encrypted ed25519 keypair. The `--secret` argument points to a file-like input that contains the encryption key. Prefer a named pipe (FIFO) or shell process substitution for this path: a FIFO lets one process stream bytes directly to another process without storing those bytes as a regular file, and it keeps the secret out of environment variables and command-line arguments. See [Why FIFOs and not env vars](/docs/guide/node/validator-keys#why-fifos-and-not-env-vars). ```bash mkfifo /run/tempo/consensus-secret @@ -88,11 +88,11 @@ Provide the following values along with the signature to the Tempo team: | **Fee recipient** | Ethereum address (`0x…`) | The address that receives transaction fees from blocks your validator proposes. If you are not prepared to accept fees, use `0x0000000000000000000000000000000000000000`. | | **Signature** | `0x`-prefixed hex | The ed25519 signature proving you control the signing key (from [Step 2](#step-2-create-the-add-validator-signature)). | -Once the Tempo team adds your validator on-chain, it will enter the active set in the [next epoch](/guide/node/validator-status#state-transitions). +Once the Tempo team adds your validator on-chain, it will enter the active set in the [next epoch](/docs/guide/node/validator-status#state-transitions). ## Running the validator -The process for running a validator node is very similar to [running a full node](/guide/node/rpc). +The process for running a validator node is very similar to [running a full node](/docs/guide/node/rpc). You should start by downloading the latest snapshot. Validators should use `--minimal` when migrating to minimal snapshots, and should keep using `--minimal` for future validator replacements. diff --git a/src/pages/guide/node/validator-status.mdx b/src/pages/docs/guide/node/validator-status.mdx similarity index 95% rename from src/pages/guide/node/validator-status.mdx rename to src/pages/docs/guide/node/validator-status.mdx index af792670..4c07c377 100644 --- a/src/pages/guide/node/validator-status.mdx +++ b/src/pages/docs/guide/node/validator-status.mdx @@ -3,7 +3,7 @@ title: Checking validator status description: Understand validator state transitions, check your validator's participation status, and query on-chain status. --- -import { StaticMermaidDiagram } from '../../../components/StaticMermaidDiagram' +import { StaticMermaidDiagram } from '../../../../components/StaticMermaidDiagram' # Checking validator status @@ -11,7 +11,7 @@ Your validator moves through different states after registration. Understanding ## Look up your validator -You can query a single validator by its consensus public key (the ed25519 key generated during [initial setup](/guide/node/validator-setup#step-1-generate-a-signing-keypair) — see also [Managing validator keys](/guide/node/validator-keys#generating-a-signing-key)): +You can query a single validator by its consensus public key (the ed25519 key generated during [initial setup](/docs/guide/node/validator-setup#step-1-generate-a-signing-keypair) — see also [Managing validator keys](/docs/guide/node/validator-keys#generating-a-signing-key)): ::::code-group ```bash [Mainnet] diff --git a/src/pages/guide/node/validator-troubleshooting.mdx b/src/pages/docs/guide/node/validator-troubleshooting.mdx similarity index 72% rename from src/pages/guide/node/validator-troubleshooting.mdx rename to src/pages/docs/guide/node/validator-troubleshooting.mdx index a7d0ee3f..95499487 100644 --- a/src/pages/guide/node/validator-troubleshooting.mdx +++ b/src/pages/docs/guide/node/validator-troubleshooting.mdx @@ -14,7 +14,7 @@ INFO handle_propose{epoch=18 view=387213 parent.view=387212 parent.digest=0x4388 ``` If you do not see logs like these: -- Check that your validator is part of the active set, and is both [a player and a dealer](/guide/node/validator-status). +- Check that your validator is part of the active set, and is both [a player and a dealer](/docs/guide/node/validator-status). - Check that your node is synced up to the [latest block height](https://explorer.tempo.xyz/). - Check that your outgoing IP address matches the one whitelisted on the validator smart contract: @@ -33,14 +33,14 @@ If `consensus_engine_peer_manager_peers` remains at `0` for more than 3 hours af - Verify your firewall allows inbound connections on the ingress port you registered. - Verify your egress IP matches the one registered on-chain — check with `tempo consensus validator --rpc-url https://rpc.tempo.xyz`. -- If you have reset your validator's data without rotating to a new identity, your node may have been blocked due to double-signing. In that case, [reach out to the Tempo team](mailto:partners@tempo.xyz) to coordinate a new validator identity. +- If you have reset your validator's data without rotating to a new identity, your node may have been blocked due to double-signing. In that case, [reach out to the Tempo team](https://tempo.xyz/contact) to coordinate a new validator identity. ## My node's DKG metrics are not increasing If `how_often_dealer` and `how_often_player` are not increasing after 6 hours: - Confirm your node is connected to peers (see above). -- Check that your validator has progressed past the [Syncer state](/guide/node/validator-status#state-transitions) — it takes at least one full epoch (~3 hours) after on-chain addition before your node participates in DKG. +- Check that your validator has progressed past the [Syncer state](/docs/guide/node/validator-status#state-transitions) — it takes at least one full epoch (~3 hours) after on-chain addition before your node participates in DKG. - Check DKG failure count: if `consensus_engine_dkg_manager_ceremony_failures_total` is increasing, your node may be failing to complete ceremonies. Enable debug logging for more detail: ```bash RUST_LOG=info,tempo_commonware_node::dkg=debug @@ -72,19 +72,19 @@ To fix: 4. **Restart your node** after fixing the clock to clear any cached invalid block state. -See [Time Synchronization](/guide/node/system-requirements#time-synchronization) for full setup details. +See [Time Synchronization](/docs/guide/node/system-requirements#time-synchronization) for full setup details. ## I accidentally deleted my consensus data directory -If you deleted `/consensus`, your signing share is lost but will be [automatically recovered](/guide/node/validator-keys#signing-share-recovery) from the network when the node restarts. Your node will re-join the committee after the next successful DKG ceremony. +If you deleted `/consensus`, your signing share is lost but will be [automatically recovered](/docs/guide/node/validator-keys#signing-share-recovery) from the network when the node restarts. Your node will re-join the committee after the next successful DKG ceremony. :::danger -Do **not** delete the entire data directory and attempt to re-sync with the same signing key. This risks double-signing and will require [rotating to a new identity](/guide/node/validator-lifecycle#resetting-your-validators-data). +Do **not** delete the entire data directory and attempt to re-sync with the same signing key. This risks double-signing and will require [rotating to a new identity](/docs/guide/node/validator-lifecycle#resetting-your-validators-data). ::: ## How long does it take for my validator to become active? -After on-chain registration, your validator follows the [state transition timeline](/guide/node/validator-status#state-transitions): +After on-chain registration, your validator follows the [state transition timeline](/docs/guide/node/validator-status#state-transitions): 1. **Epoch E** (immediate) — registered on the p2p network, starts syncing. 2. **Epoch E+1** (~3 hours) — becomes a player, receives signing shares. @@ -100,28 +100,28 @@ After deactivation, your validator is phased out over two epochs: 2. **Epoch E+1** (~3 hours) — still a dealer but no longer a player. In the process of being removed. 3. **Epoch E+2** (~6 hours) — fully out of the committee (assuming no DKG failures). -Keep your node running until `in_committee: false` — check with [validator lookup](/guide/node/validator-status#look-up-your-validator). +Keep your node running until `in_committee: false` — check with [validator lookup](/docs/guide/node/validator-status#look-up-your-validator). ## Can I register my validator without the Tempo team? -No — the active validator set is currently permissioned. Only the contract owner can add validators on-chain. To get started, [contact the Tempo team](mailto:partners@tempo.xyz). +No — the active validator set is currently permissioned. Only the contract owner can add validators on-chain. To get started, [contact the Tempo team](https://tempo.xyz/contact). The onboarding process: -1. **You** generate your signing key and registration signature ([Steps 1–2](/guide/node/validator-setup#initial-registration)). -2. **You** provide the required values to the Tempo team ([Step 3](/guide/node/validator-setup#step-3-submit-registration-details)). +1. **You** generate your signing key and registration signature ([Steps 1–2](/docs/guide/node/validator-setup#initial-registration)). +2. **You** provide the required values to the Tempo team ([Step 3](/docs/guide/node/validator-setup#step-3-submit-registration-details)). 3. **The Tempo team** adds your validator on-chain. -4. **You** download a snapshot and start your node ([Running the validator](/guide/node/validator-setup#running-the-validator)). +4. **You** download a snapshot and start your node ([Running the validator](/docs/guide/node/validator-setup#running-the-validator)). ## What can I do without the Tempo team? Once your validator is registered, most operations are self-service: -- [Rotate your signing key](/guide/node/validator-lifecycle#rotate-validator-identity) -- [Update IP addresses](/guide/node/validator-lifecycle#update-ip-addresses) -- [Update your fee recipient](/guide/node/validator-lifecycle#update-the-fee-recipient) -- [Transfer validator ownership](/guide/node/validator-lifecycle#transfer-validator-ownership) -- [Deactivate your validator](/guide/node/validator-lifecycle#deactivate-your-validator) +- [Rotate your signing key](/docs/guide/node/validator-lifecycle#rotate-validator-identity) +- [Update IP addresses](/docs/guide/node/validator-lifecycle#update-ip-addresses) +- [Update your fee recipient](/docs/guide/node/validator-lifecycle#update-the-fee-recipient) +- [Transfer validator ownership](/docs/guide/node/validator-lifecycle#transfer-validator-ownership) +- [Deactivate your validator](/docs/guide/node/validator-lifecycle#deactivate-your-validator) Only **initial registration** and **reactivation** require the Tempo team. @@ -131,4 +131,4 @@ Only **initial registration** and **reactivation** require the Tempo team. tempo --version ``` -Compare with the latest release on the [network upgrades](/guide/node/network-upgrades) page to ensure you're on a supported version. +Compare with the latest release on the [network upgrades](/docs/guide/node/network-upgrades) page to ensure you're on a supported version. diff --git a/src/pages/guide/node/validator.mdx b/src/pages/docs/guide/node/validator.mdx similarity index 75% rename from src/pages/guide/node/validator.mdx rename to src/pages/docs/guide/node/validator.mdx index 3a77f38e..4cca9375 100644 --- a/src/pages/guide/node/validator.mdx +++ b/src/pages/docs/guide/node/validator.mdx @@ -9,48 +9,48 @@ import { Cards, Card } from 'vocs' Validator nodes secure Tempo by validating blocks and participating in consensus. :::info -The active validator set is currently permissioned. If you are interested in becoming a validator, please [get in touch](mailto:partners@tempo.xyz) with the Tempo team. See [Initial setup](/guide/node/validator-setup) for technical details. +The active validator set is currently permissioned. If you are interested in becoming a validator, please [get in touch](https://tempo.xyz/contact) with the Tempo team. See [Initial setup](/docs/guide/node/validator-setup) for technical details. ::: diff --git a/src/pages/guide/payments/accept-a-payment.mdx b/src/pages/docs/guide/payments/accept-a-payment.mdx similarity index 97% rename from src/pages/guide/payments/accept-a-payment.mdx rename to src/pages/docs/guide/payments/accept-a-payment.mdx index c01f4dca..4c79a465 100644 --- a/src/pages/guide/payments/accept-a-payment.mdx +++ b/src/pages/docs/guide/payments/accept-a-payment.mdx @@ -3,10 +3,10 @@ description: Accept stablecoin payments in your application. Verify transactions interactive: true --- -import * as Demo from '../../../components/guides/Demo.tsx' -import { AddFunds } from '../../../components/guides/steps/payments/AddFunds.tsx' -import { Connect } from '../../../components/guides/steps/auth/Connect.tsx' -import * as Token from '../../../components/guides/tokens' +import * as Demo from '../../../../components/guides/Demo.tsx' +import { AddFunds } from '../../../../components/guides/steps/payments/AddFunds.tsx' +import { Connect } from '../../../../components/guides/steps/auth/Connect.tsx' +import * as Token from '../../../../components/guides/tokens' import { Tabs, Tab } from 'vocs' # Accept a Payment @@ -615,5 +615,5 @@ const { receipt } = await client.dex.sellSync({ ## Next Steps -- **[Send a payment](/guide/payments/send-a-payment)** to learn how to send payments -- Learn more about [Exchange](/guide/stablecoin-dex) for cross-stablecoin payments +- **[Send a payment](/docs/guide/payments/send-a-payment)** to learn how to send payments +- Learn more about [Exchange](/docs/guide/stablecoin-dex) for cross-stablecoin payments diff --git a/src/pages/guide/payments/index.mdx b/src/pages/docs/guide/payments/index.mdx similarity index 81% rename from src/pages/guide/payments/index.mdx rename to src/pages/docs/guide/payments/index.mdx index 635b1203..38a1b751 100644 --- a/src/pages/guide/payments/index.mdx +++ b/src/pages/docs/guide/payments/index.mdx @@ -11,49 +11,49 @@ Send and receive payments using stablecoins on Tempo. Learn how to integrate pay diff --git a/src/pages/guide/payments/pay-fees-in-any-stablecoin.mdx b/src/pages/docs/guide/payments/pay-fees-in-any-stablecoin.mdx similarity index 94% rename from src/pages/guide/payments/pay-fees-in-any-stablecoin.mdx rename to src/pages/docs/guide/payments/pay-fees-in-any-stablecoin.mdx index d71a35fb..52566472 100644 --- a/src/pages/guide/payments/pay-fees-in-any-stablecoin.mdx +++ b/src/pages/docs/guide/payments/pay-fees-in-any-stablecoin.mdx @@ -4,11 +4,11 @@ showOutline: 1 interactive: true --- -import * as Demo from '../../../components/guides/Demo.tsx' -import { AddFunds } from '../../../components/guides/steps/payments/AddFunds.tsx' -import { Connect } from '../../../components/guides/steps/auth/Connect.tsx' -import { PayWithFeeToken } from '../../../components/guides/steps/payments/PayWithFeeToken.tsx' -import { betaUsd, thetaUsd, pathUsd } from '../../../components/guides/tokens.ts' +import * as Demo from '../../../../components/guides/Demo.tsx' +import { AddFunds } from '../../../../components/guides/steps/payments/AddFunds.tsx' +import { Connect } from '../../../../components/guides/steps/auth/Connect.tsx' +import { PayWithFeeToken } from '../../../../components/guides/steps/payments/PayWithFeeToken.tsx' +import { betaUsd, thetaUsd, pathUsd } from '../../../../components/guides/tokens.ts' import { Cards, Card, Tabs, Tab } from 'vocs' # Pay Fees in Any Stablecoin @@ -210,12 +210,13 @@ The fee token for a given transaction cannot be set from Solidity — it is a tr ::::steps -### Set up Wagmi & integrate accounts +### Set up Wagmi -Ensure that you have set up your project with Wagmi and integrated accounts by following either of the guides: +Ensure that you have set up your project with Wagmi, a Tempo chain config, and a wallet connector: -- [Embed Passkey accounts](/guide/use-accounts/embed-passkeys) -- [Connect to wallets](/guide/use-accounts/connect-to-wallets) +- [Connection details](/docs/quickstart/connection-details) +- [TypeScript SDK](/docs/sdk/typescript) +- [Wallet integration](/docs/quickstart/wallet-developers) ### Add testnet funds¹ @@ -377,8 +378,8 @@ function PayWithFeeToken() { ### Next steps Now that you have made a payment using a desired fee token, you can: -- Follow a guide on how to [sponsor user fees](/guide/payments/sponsor-user-fees) to enable gasless transactions -- Learn more about [transaction fees](/protocol/fees) +- Follow a guide on how to [sponsor user fees](/docs/guide/payments/sponsor-user-fees) to enable gasless transactions +- Learn more about [transaction fees](/docs/protocol/fees) :::: @@ -506,7 +507,7 @@ For Solidity integration, refer to the [Quick Snippet](#quick-snippet) above and ## Set user fee token -You can also set a persistent default fee token for an account, so users don't need to specify `feeToken` on every transaction. Learn more about fee token preferences [here](/protocol/fees/spec-fee#fee-token-preferences). +You can also set a persistent default fee token for an account, so users don't need to specify `feeToken` on every transaction. Learn more about fee token preferences [here](/docs/protocol/fees/spec-fee#fee-token-preferences). @@ -703,7 +704,7 @@ StdPrecompiles.TIP_FEE_MANAGER.setUserToken(0x20c0000000000000000000000000000000 diff --git a/src/pages/guide/payments/send-a-payment.mdx b/src/pages/docs/guide/payments/send-a-payment.mdx similarity index 96% rename from src/pages/guide/payments/send-a-payment.mdx rename to src/pages/docs/guide/payments/send-a-payment.mdx index 03d32124..2f7533e8 100644 --- a/src/pages/guide/payments/send-a-payment.mdx +++ b/src/pages/docs/guide/payments/send-a-payment.mdx @@ -3,10 +3,10 @@ description: Send stablecoin payments between accounts on Tempo. Include optiona interactive: true --- -import * as Demo from '../../../components/guides/Demo.tsx' -import { AddFunds } from '../../../components/guides/steps/payments/AddFunds.tsx' -import { Connect } from '../../../components/guides/steps/auth/Connect.tsx' -import { SendPayment } from '../../../components/guides/steps/payments/SendPayment.tsx' +import * as Demo from '../../../../components/guides/Demo.tsx' +import { AddFunds } from '../../../../components/guides/steps/payments/AddFunds.tsx' +import { Connect } from '../../../../components/guides/steps/auth/Connect.tsx' +import { SendPayment } from '../../../../components/guides/steps/payments/SendPayment.tsx' import { Cards, Card } from 'vocs' import { Tabs, Tab } from 'vocs' @@ -20,7 +20,7 @@ On T6 networks, a blocked TIP-20 `transfer` / `transferFrom` still succeeds, but - Before marking a payment, withdrawal, or deposit delivered, confirm the `Transfer` recipient is the intended receiver or its TIP-1022 master. - Index `ReceivePolicyGuard.TransferBlocked` so redirected funds can be surfaced and claimed later. -See [account-level receive policies on the T6 upgrade page](/protocol/upgrades/t6#account-level-receive-policies). +See [account-level receive policies on the T6 upgrade page](/docs/protocol/upgrades/t6#account-level-receive-policies). ::: ## Demo @@ -37,12 +37,13 @@ By the end of this guide you will be able to send payments on Tempo with an opti ::::steps -### Set up Wagmi & integrate accounts +### Set up Wagmi -Ensure that you have set up your project with Wagmi and integrated accounts by following either of the guides: +Ensure that you have set up your project with Wagmi, a Tempo chain config, and a wallet connector: -- [Embed Passkey accounts](/guide/use-accounts/embed-passkeys) -- [Connect to wallets](/guide/use-accounts/connect-to-wallets) +- [Connection details](/docs/quickstart/connection-details) +- [TypeScript SDK](/docs/sdk/typescript) +- [Wallet integration](/docs/quickstart/wallet-developers) ### Add testnet funds¹ @@ -185,10 +186,10 @@ function SendPaymentWithMemo() { ### Next steps Now that you have made a payment you can -- **[Accept a payment](/guide/payments/accept-a-payment)** to receive payments in your application -- Learn about [Batch Transactions](/guide/use-accounts/batch-transactions) -- Send a payment [with a specific fee token](/guide/payments/pay-fees-in-any-stablecoin) -:::: +- **[Accept a payment](/docs/guide/payments/accept-a-payment)** to receive payments in your application +- Learn about [Tempo Transactions](/docs/protocol/transactions) for batching, sponsorship, scheduling, and more +- Send a payment [with a specific fee token](/docs/guide/payments/pay-fees-in-any-stablecoin) +:::: ## Recipes @@ -1046,13 +1047,13 @@ function SendPayment() { diff --git a/src/pages/guide/payments/send-parallel-transactions.mdx b/src/pages/docs/guide/payments/send-parallel-transactions.mdx similarity index 63% rename from src/pages/guide/payments/send-parallel-transactions.mdx rename to src/pages/docs/guide/payments/send-parallel-transactions.mdx index f40eb12a..a153c2cc 100644 --- a/src/pages/guide/payments/send-parallel-transactions.mdx +++ b/src/pages/docs/guide/payments/send-parallel-transactions.mdx @@ -3,16 +3,16 @@ description: Submit multiple transactions concurrently using Tempo's expiring no interactive: true --- -import * as Demo from '../../../components/guides/Demo.tsx' -import { AddFunds } from '../../../components/guides/steps/payments/AddFunds.tsx' -import { Connect } from '../../../components/guides/steps/auth/Connect.tsx' -import { SendParallelPayments } from '../../../components/guides/steps/payments/SendParallelPayments.tsx' -import * as Token from '../../../components/guides/tokens' +import * as Demo from '../../../../components/guides/Demo.tsx' +import { AddFunds } from '../../../../components/guides/steps/payments/AddFunds.tsx' +import { Connect } from '../../../../components/guides/steps/auth/Connect.tsx' +import { SendParallelPayments } from '../../../../components/guides/steps/payments/SendParallelPayments.tsx' +import * as Token from '../../../../components/guides/tokens' import { Cards, Card } from 'vocs' # Send Parallel Transactions -Tempo enables concurrent transaction execution through its [expiring nonce](/guide/tempo-transaction#expiring-nonces) system. Unlike traditional sequential nonces that require transactions to be processed one at a time, expiring nonces allow multiple transactions to be submitted simultaneously without nonce conflicts. Each transaction uses an independent nonce that automatically expires after a set time window, enabling true parallel execution. +Tempo enables concurrent transaction execution through its [expiring nonce](/docs/guide/tempo-transaction#expiring-nonces) system. Unlike traditional sequential nonces that require transactions to be processed one at a time, expiring nonces allow multiple transactions to be submitted simultaneously without nonce conflicts. Each transaction uses an independent nonce that automatically expires after a set time window, enabling true parallel execution. ## Demo @@ -28,16 +28,17 @@ By the end of this guide you will understand how to send parallel payments using ::::steps -### Set up Wagmi & integrate accounts +### Set up Wagmi -Ensure that you have set up your project with Wagmi and integrated accounts by following either of the guides: +Ensure that you have set up your project with Wagmi, a Tempo chain config, and a wallet connector: -- [Embed Passkey accounts](/guide/use-accounts/embed-passkeys) -- [Connect to wallets](/guide/use-accounts/connect-to-wallets) +- [Connection details](/docs/quickstart/connection-details) +- [TypeScript SDK](/docs/sdk/typescript) +- [Wallet integration](/docs/quickstart/wallet-developers) ### Send concurrent transactions with nonce keys -To send multiple transactions in parallel, simply batch them together. [Expiring nonces](/guide/tempo-transaction#expiring-nonces) are attached to each transaction automatically. +To send multiple transactions in parallel, simply batch them together. [Expiring nonces](/docs/guide/tempo-transaction#expiring-nonces) are attached to each transaction automatically. :::code-group @@ -82,13 +83,13 @@ console.log('Transaction 2:', receipt2.transactionHash) // [!code focus] diff --git a/src/pages/guide/payments/sponsor-user-fees.mdx b/src/pages/docs/guide/payments/sponsor-user-fees.mdx similarity index 83% rename from src/pages/guide/payments/sponsor-user-fees.mdx rename to src/pages/docs/guide/payments/sponsor-user-fees.mdx index 5510745f..d7fbe6ba 100644 --- a/src/pages/guide/payments/sponsor-user-fees.mdx +++ b/src/pages/docs/guide/payments/sponsor-user-fees.mdx @@ -4,23 +4,23 @@ interactive: true --- import { Cards, Card, Tabs, Tab } from 'vocs' -import * as Demo from '../../../components/guides/Demo.tsx' -import { AddFunds } from '../../../components/guides/steps/payments/AddFunds.tsx' -import { Connect } from '../../../components/guides/steps/auth/Connect.tsx' -import { SendRelayerSponsoredPayment } from '../../../components/guides/steps/payments/SponsorUserFees.tsx' -import PublicTestnetSponsorTip from '../../../snippets/public-testnet-sponsor-tip.mdx' +import * as Demo from '../../../../components/guides/Demo.tsx' +import { AddFunds } from '../../../../components/guides/steps/payments/AddFunds.tsx' +import { Connect } from '../../../../components/guides/steps/auth/Connect.tsx' +import { SendRelayerSponsoredPayment } from '../../../../components/guides/steps/payments/SponsorUserFees.tsx' +import PublicTestnetSponsorTip from '../../../../snippets/public-testnet-sponsor-tip.mdx' # Sponsor User Fees Enable gasless transactions by sponsoring transaction fees for your users. Tempo's native fee sponsorship allows applications to pay fees on behalf of users, improving UX and removing friction from payment flows. :::tip[Hosted fee payer] -Use Tempo's [hosted fee payer endpoints](/developer-tools/fee-payer) for testnet development or approved mainnet integrations. Run your own fee payer when you need custom sponsorship policy, accounting, or operational control. +Use Tempo's [hosted fee payer endpoints](/docs/developer-tools/fee-payer) for testnet development or approved mainnet integrations. Run your own fee payer when you need custom sponsorship policy, accounting, or operational control. ::: ## Demo - + @@ -33,28 +33,12 @@ Use Tempo's [hosted fee payer endpoints](/developer-tools/fee-payer) for testnet ### Set up the fee payer service :::tip -Tempo provides a public testnet fee payer service at `https://sponsor.moderato.tempo.xyz` that you can use for development and testing. See [Hosted Fee Payer](/developer-tools/fee-payer) for endpoint details, or follow the instructions below to run your own. +Tempo provides a public testnet fee payer service at `https://sponsor.moderato.tempo.xyz` that you can use for development and testing. See [Hosted Fee Payer](/docs/developer-tools/fee-payer) for endpoint details, or follow the instructions below to run your own. ::: -You can stand up a minimal fee payer service using the [`Handler.relay`](/accounts/server/handler.relay) handler provided by the [Tempo Accounts SDK](/accounts). To sponsor transactions, you need a funded account that will act as the fee payer. +To sponsor transactions with your own infrastructure, run a JSON-RPC relay that validates incoming requests, fills Tempo Transactions, signs the fee payer payload, and broadcasts approved transactions. The fee payer account must be funded with the stablecoin it will use for fees. -```ts twoslash [server.ts] -// @noErrors -// [!include ~/snippets/unformatted/withFeePayer.ts:server] -``` - -Then plug `handler` into your server framework of choice: - -```ts -createServer(handler.listener) // Node.js -Bun.serve(handler) // Bun -Deno.serve(handler) // Deno -app.all('*', c => handler.fetch(c.request)) // Elysia -app.use(handler.listener) // Express -app.use(c => handler.fetch(c.req.raw)) // Hono -export const GET = handler.fetch // Next.js -export const POST = handler.fetch // Next.js -``` +Your relay should expose the same core JSON-RPC methods described in the [Hosted Fee Payer API reference](/docs/developer-tools/fee-payer#api-reference), then apply your own sponsorship policy before signing. ### Configure your client to use the fee payer service @@ -104,7 +88,7 @@ export const config = createConfig({ ### Sponsor your user's transactions -Now transactions will be sponsored by your relay. For more details on how to send a transaction, see the [Send a payment](/guide/payments/send-a-payment) guide. +Now transactions will be sponsored by your relay. For more details on how to send a transaction, see the [Send a payment](/docs/guide/payments/send-a-payment) guide. :::info You can also build your own fee paying service. See [Build your own fee paying service](#build-your-own-fee-paying-service) below. @@ -166,9 +150,9 @@ function SendSponsoredPayment() { ### Next Steps Now that you've implemented fee sponsorship, you can: -- Learn more about the [Tempo Transaction](/protocol/transactions/spec-tempo-transaction#fee-payer-signature-details) type and fee payer signature details -- Explore [Batch Transactions](/guide/use-accounts/batch-transactions) to sponsor multiple operations at once -- Learn how to [Pay Fees in Any Stablecoin](/guide/payments/pay-fees-in-any-stablecoin) +- Learn more about the [Tempo Transaction](/docs/protocol/transactions/spec-tempo-transaction#fee-payer-signature-details) type and fee payer signature details +- Use Tempo Transactions to sponsor multiple calls in a single signed operation +- Learn how to [Pay Fees in Any Stablecoin](/docs/guide/payments/pay-fees-in-any-stablecoin) :::: @@ -415,25 +399,19 @@ The SDKs handle this automatically. Here's how each SDK manages the dual-signing - diff --git a/src/pages/guide/payments/transfer-memos.mdx b/src/pages/docs/guide/payments/transfer-memos.mdx similarity index 85% rename from src/pages/guide/payments/transfer-memos.mdx rename to src/pages/docs/guide/payments/transfer-memos.mdx index a1b42f39..30a7dd8c 100644 --- a/src/pages/guide/payments/transfer-memos.mdx +++ b/src/pages/docs/guide/payments/transfer-memos.mdx @@ -4,14 +4,14 @@ description: Attach 32-byte reference memos to TIP-20 transfers for payment reco --- import { Cards, Card } from 'vocs' -import * as Demo from '../../../components/guides/Demo.tsx' -import { AddFunds } from '../../../components/guides/steps/payments/AddFunds.tsx' -import { Connect } from '../../../components/guides/steps/auth/Connect.tsx' -import { SendPaymentWithMemo } from '../../../components/guides/steps/payments/SendPaymentWithMemo.tsx' +import * as Demo from '../../../../components/guides/Demo.tsx' +import { AddFunds } from '../../../../components/guides/steps/payments/AddFunds.tsx' +import { Connect } from '../../../../components/guides/steps/auth/Connect.tsx' +import { SendPaymentWithMemo } from '../../../../components/guides/steps/payments/SendPaymentWithMemo.tsx' # Attach a Transfer Memo -Attach 32-byte references to [TIP-20](/protocol/tip20/overview) transfers for payment reconciliation. Use memos to link onchain transactions to your internal records—customer IDs, invoice numbers, or any identifier that helps you match payments to your database. +Attach 32-byte references to [TIP-20](/docs/protocol/tip20/overview) transfers for payment reconciliation. Use memos to link onchain transactions to your internal records—customer IDs, invoice numbers, or any identifier that helps you match payments to your database. ## Demo @@ -27,10 +27,11 @@ Attach 32-byte references to [TIP-20](/protocol/tip20/overview) transfers for pa ### Set up your project -Ensure you have Wagmi configured with Tempo. Follow either guide to get started: +Ensure you have Wagmi configured with Tempo: -- [Embed Passkey accounts](/guide/use-accounts/embed-passkeys) -- [Connect to wallets](/guide/use-accounts/connect-to-wallets) +- [Connection details](/docs/quickstart/connection-details) +- [TypeScript SDK](/docs/sdk/typescript) +- [Wallet integration](/docs/quickstart/wallet-developers) ### Send a transfer with memo @@ -198,19 +199,19 @@ const logs = await client.getLogs({ diff --git a/src/pages/guide/payments/virtual-addresses.mdx b/src/pages/docs/guide/payments/virtual-addresses.mdx similarity index 91% rename from src/pages/guide/payments/virtual-addresses.mdx rename to src/pages/docs/guide/payments/virtual-addresses.mdx index f5a26e39..ff9be5f0 100644 --- a/src/pages/guide/payments/virtual-addresses.mdx +++ b/src/pages/docs/guide/payments/virtual-addresses.mdx @@ -5,10 +5,10 @@ interactive: true --- import { Card, Cards, Tab, Tabs } from 'vocs' -import * as Demo from '../../../components/guides/Demo.tsx' -import { VirtualAddressesFastDemo } from '../../../components/guides/VirtualAddressesFastDemo.tsx' -import { VirtualAddressesLiveDemo } from '../../../components/guides/VirtualAddressesLiveDemo.tsx' -import { MermaidDiagram } from '../../../components/MermaidDiagram' +import * as Demo from '../../../../components/guides/Demo.tsx' +import { VirtualAddressesFastDemo } from '../../../../components/guides/VirtualAddressesFastDemo.tsx' +import { VirtualAddressesLiveDemo } from '../../../../components/guides/VirtualAddressesLiveDemo.tsx' +import { MermaidDiagram } from '../../../../components/MermaidDiagram' # Use virtual addresses for deposits @@ -128,7 +128,7 @@ A few things matter in production: diff --git a/src/pages/guide/stablecoin-dex/executing-swaps.mdx b/src/pages/docs/guide/stablecoin-dex/executing-swaps.mdx similarity index 96% rename from src/pages/guide/stablecoin-dex/executing-swaps.mdx rename to src/pages/docs/guide/stablecoin-dex/executing-swaps.mdx index 500c3911..13243e27 100644 --- a/src/pages/guide/stablecoin-dex/executing-swaps.mdx +++ b/src/pages/docs/guide/stablecoin-dex/executing-swaps.mdx @@ -4,10 +4,10 @@ interactive: true --- import { Cards, Card } from 'vocs' -import * as Demo from '../../../components/guides/Demo.tsx' -import { AddFunds } from '../../../components/guides/steps/payments/AddFunds.tsx' -import { Connect } from '../../../components/guides/steps/auth/Connect.tsx' -import { MakeSwaps } from '../../../components/guides/steps/exchange/MakeSwaps.tsx' +import * as Demo from '../../../../components/guides/Demo.tsx' +import { AddFunds } from '../../../../components/guides/steps/payments/AddFunds.tsx' +import { Connect } from '../../../../components/guides/steps/auth/Connect.tsx' +import { MakeSwaps } from '../../../../components/guides/steps/exchange/MakeSwaps.tsx' # Executing Swaps @@ -28,7 +28,7 @@ By the end of this guide you will be able to execute swaps, get price quotes, an ### Set up your client -Ensure that you have set up your client by following the [guide](/sdk/typescript). +Ensure that you have set up your client by following the [guide](/docs/sdk/typescript). ### Get a price quote @@ -436,13 +436,13 @@ Use `minAmountOut` or `maxAmountIn` to protect against unfavorable price movemen diff --git a/src/pages/guide/stablecoin-dex/index.mdx b/src/pages/docs/guide/stablecoin-dex/index.mdx similarity index 82% rename from src/pages/guide/stablecoin-dex/index.mdx rename to src/pages/docs/guide/stablecoin-dex/index.mdx index 037330ed..faeb3a5e 100644 --- a/src/pages/guide/stablecoin-dex/index.mdx +++ b/src/pages/docs/guide/stablecoin-dex/index.mdx @@ -11,28 +11,28 @@ Trade between stablecoins on Tempo's enshrined decentralized exchange (DEX). The diff --git a/src/pages/guide/stablecoin-dex/managing-fee-liquidity.mdx b/src/pages/docs/guide/stablecoin-dex/managing-fee-liquidity.mdx similarity index 94% rename from src/pages/guide/stablecoin-dex/managing-fee-liquidity.mdx rename to src/pages/docs/guide/stablecoin-dex/managing-fee-liquidity.mdx index e3b6b588..3a7dbea1 100644 --- a/src/pages/guide/stablecoin-dex/managing-fee-liquidity.mdx +++ b/src/pages/docs/guide/stablecoin-dex/managing-fee-liquidity.mdx @@ -4,13 +4,13 @@ interactive: true --- import { Cards, Card } from 'vocs' -import * as Demo from '../../../components/guides/Demo.tsx' -import { AddFunds } from '../../../components/guides/steps/payments/AddFunds.tsx' -import { BurnFeeAmmLiquidity } from '../../../components/guides/steps/amm/BurnFeeAmmLiquidity.tsx' -import { CheckFeeAmmPool } from '../../../components/guides/steps/amm/CheckFeeAmmPool.tsx' -import { Connect } from '../../../components/guides/steps/auth/Connect.tsx' -import { CreateOrLoadToken } from '../../../components/guides/steps/issuance/CreateOrLoadToken.tsx' -import { MintFeeAmmLiquidity } from '../../../components/guides/steps/amm/MintFeeAmmLiquidity.tsx' +import * as Demo from '../../../../components/guides/Demo.tsx' +import { AddFunds } from '../../../../components/guides/steps/payments/AddFunds.tsx' +import { BurnFeeAmmLiquidity } from '../../../../components/guides/steps/amm/BurnFeeAmmLiquidity.tsx' +import { CheckFeeAmmPool } from '../../../../components/guides/steps/amm/CheckFeeAmmPool.tsx' +import { Connect } from '../../../../components/guides/steps/auth/Connect.tsx' +import { CreateOrLoadToken } from '../../../../components/guides/steps/issuance/CreateOrLoadToken.tsx' +import { MintFeeAmmLiquidity } from '../../../../components/guides/steps/amm/MintFeeAmmLiquidity.tsx' # Managing Fee Liquidity @@ -82,7 +82,7 @@ export const config = createConfig({ ### Add liquidity -Add validator token to the pool to receive LP tokens representing your share. The first liquidity provider to a new pool must burn 1,000 units of liquidity. This costs approximately 0.002 USD and prevents attacks on pool reserves. Learn more in the [Fee AMM specification](/protocol/fees/spec-fee-amm). +Add validator token to the pool to receive LP tokens representing your share. The first liquidity provider to a new pool must burn 1,000 units of liquidity. This costs approximately 0.002 USD and prevents attacks on pool reserves. Learn more in the [Fee AMM specification](/docs/protocol/fees/spec-fee-amm). :::code-group @@ -366,7 +366,7 @@ export const config = createConfig({ ### Rebalance pools -You can rebalance pools by swapping validator tokens for accumulated user tokens at a fixed rate. Rebalancing restores validator token reserves and enables continued fee conversions. Learn more [here](/protocol/fees/spec-fee-amm#swap-mechanisms). +You can rebalance pools by swapping validator tokens for accumulated user tokens at a fixed rate. Rebalancing restores validator token reserves and enables continued fee conversions. Learn more [here](/docs/protocol/fees/spec-fee-amm#swap-mechanisms). :::code-group @@ -458,19 +458,19 @@ Focus liquidity on pools with: diff --git a/src/pages/guide/stablecoin-dex/providing-liquidity.mdx b/src/pages/docs/guide/stablecoin-dex/providing-liquidity.mdx similarity index 90% rename from src/pages/guide/stablecoin-dex/providing-liquidity.mdx rename to src/pages/docs/guide/stablecoin-dex/providing-liquidity.mdx index e7f4d16d..21fcb0e6 100644 --- a/src/pages/guide/stablecoin-dex/providing-liquidity.mdx +++ b/src/pages/docs/guide/stablecoin-dex/providing-liquidity.mdx @@ -4,17 +4,17 @@ interactive: true --- import { Cards, Card } from 'vocs' -import * as Demo from '../../../components/guides/Demo.tsx' -import { AddFunds } from '../../../components/guides/steps/payments/AddFunds.tsx' -import { ApproveSpend } from '../../../components/guides/steps/exchange/ApproveSpend.tsx' -import { CancelOrder } from '../../../components/guides/steps/exchange/CancelOrder.tsx' -import { Connect } from '../../../components/guides/steps/auth/Connect.tsx' -import { PlaceOrder } from '../../../components/guides/steps/exchange/PlaceOrder.tsx' -import { QueryOrder } from '../../../components/guides/steps/exchange/QueryOrder.tsx' +import * as Demo from '../../../../components/guides/Demo.tsx' +import { AddFunds } from '../../../../components/guides/steps/payments/AddFunds.tsx' +import { ApproveSpend } from '../../../../components/guides/steps/exchange/ApproveSpend.tsx' +import { CancelOrder } from '../../../../components/guides/steps/exchange/CancelOrder.tsx' +import { Connect } from '../../../../components/guides/steps/auth/Connect.tsx' +import { PlaceOrder } from '../../../../components/guides/steps/exchange/PlaceOrder.tsx' +import { QueryOrder } from '../../../../components/guides/steps/exchange/QueryOrder.tsx' # Providing Liquidity -Add liquidity for a token pair by placing orders on the Stablecoin DEX. You can provide liquidity on the `buy` or `sell` side of the orderbook, with `limit` or `flip` orders. To learn more about order types see the [documentation on order types](/protocol/exchange/providing-liquidity#order-types). +Add liquidity for a token pair by placing orders on the Stablecoin DEX. You can provide liquidity on the `buy` or `sell` side of the orderbook, with `limit` or `flip` orders. To learn more about order types see the [documentation on order types](/docs/protocol/exchange/providing-liquidity#order-types). In this guide you will learn how to place buy and sell orders to provide liquidity on the Stablecoin DEX orderbook. @@ -33,7 +33,7 @@ In this guide you will learn how to place buy and sell orders to provide liquidi ### Set up your client -Ensure that you have set up your client by following the [guide](/sdk/typescript). +Ensure that you have set up your client by following the [guide](/docs/sdk/typescript). ### Approve spend @@ -197,7 +197,7 @@ For more details on querying orders, see the [`Hooks.dex.useOrder`](https://wagm Cancel an order using its order ID. -When you cancel an order, any remaining funds are credited to your exchange balance (not directly to your wallet). To move funds back to your wallet, you can [withdraw them to your wallet](/protocol/exchange/exchange-balance#withdrawing-funds). +When you cancel an order, any remaining funds are credited to your exchange balance (not directly to your wallet). To move funds back to your wallet, you can [withdraw them to your wallet](/docs/protocol/exchange/exchange-balance#withdrawing-funds). @@ -310,7 +310,7 @@ console.log('Quote Token:', metadata?.quoteToken) // returns `pathUSD` address Flip orders automatically switch between buy and sell sides when filled, providing continuous liquidity. Use viem's [`dex.placeFlip`](https://viem.sh/tempo/actions/dex.placeFlip) to create a flip order call. -A flip order can re-list on the opposite side at the same tick (`flipTick == tick`), which is useful for pegged or near-1:1 pairs. When it fills, the order keeps the same `orderId` and emits an `OrderFlipped` event instead of a new `OrderPlaced`, so a single flip strategy stays trackable under one ID across its full lifecycle. Indexers, SDKs, and contract code that follow flip orders should treat `OrderFlipped` as the latest active state — see the [flip-order indexing notes](/protocol/exchange/providing-liquidity#flip-order-indexing). +A flip order can re-list on the opposite side at the same tick (`flipTick == tick`), which is useful for pegged or near-1:1 pairs. When it fills, the order keeps the same `orderId` and emits an `OrderFlipped` event instead of a new `OrderPlaced`, so a single flip strategy stays trackable under one ID across its full lifecycle. Indexers, SDKs, and contract code that follow flip orders should treat `OrderFlipped` as the latest active state — see the [flip-order indexing notes](/docs/protocol/exchange/providing-liquidity#flip-order-indexing). :::code-group @@ -395,31 +395,31 @@ const sellCall = Actions.dex.place.call({ // [!code hl] ``` -For more details including tick precision, limits, and calculation examples, see [Understanding Ticks](/protocol/exchange/providing-liquidity#understanding-ticks). +For more details including tick precision, limits, and calculation examples, see [Understanding Ticks](/docs/protocol/exchange/providing-liquidity#understanding-ticks). ## Best practices ### Batch calls -You can batch the calls to approve spend and place the order in a single transaction for efficiency. See the [guide on batch transactions](/guide/use-accounts/batch-transactions) for more details. +You can batch the calls to approve spend and place the order in a single transaction for efficiency. See the [Tempo Transactions guide](/docs/guide/tempo-transaction) for more details. ## Learning resources diff --git a/src/pages/guide/stablecoin-dex/view-the-orderbook.mdx b/src/pages/docs/guide/stablecoin-dex/view-the-orderbook.mdx similarity index 93% rename from src/pages/guide/stablecoin-dex/view-the-orderbook.mdx rename to src/pages/docs/guide/stablecoin-dex/view-the-orderbook.mdx index d7ee6b95..2ba0b509 100644 --- a/src/pages/guide/stablecoin-dex/view-the-orderbook.mdx +++ b/src/pages/docs/guide/stablecoin-dex/view-the-orderbook.mdx @@ -2,13 +2,13 @@ description: Inspect Tempo's onchain orderbook using SQL queries. View spreads, order depth, individual orders, and recent trade prices with indexed data. --- -import { IndexSupplyQuery } from '../../../components/IndexSupplyQuery' +import { IndexSupplyQuery } from '../../../../components/IndexSupplyQuery' # View the Orderbook :::warning[Recipes being updated] -These SQL recipes don't yet account for [flip orders](/protocol/exchange/providing-liquidity#flip-order-indexing), which keep the same `orderId` and re-list via `OrderFlipped` when filled — results may omit or misreport flipped orders. Updated queries are in progress. +These SQL recipes don't yet account for [flip orders](/docs/protocol/exchange/providing-liquidity#flip-order-indexing), which keep the same `orderId` and re-list via `OrderFlipped` when filled — results may omit or misreport flipped orders. Updated queries are in progress. ::: Query and inspect the orderbook to see available liquidity, price levels, and individual orders on Tempo's Stablecoin DEX. @@ -25,7 +25,7 @@ In this guide, we use [Index Supply](https://www.indexsupply.net) as our indexin Query the best bid and ask prices to calculate the current spread for a token pair. -These examples use the mainnet `USDC.e / pathUSD` orderbook; `pathUSD` is the quote token for `USDC.e` — see [Quote Tokens](/protocol/exchange/quote-tokens). +These examples use the mainnet `USDC.e / pathUSD` orderbook; `pathUSD` is the quote token for `USDC.e` — see [Quote Tokens](/docs/protocol/exchange/quote-tokens). Find the highest bid prices (buyers) for the `USDC.e / pathUSD` book. This query filters out fully filled and cancelled orders, groups by price level (tick), and shows the top 5 bid prices with their total liquidity. diff --git a/src/pages/guide/tempo-transaction/index.mdx b/src/pages/docs/guide/tempo-transaction/index.mdx similarity index 85% rename from src/pages/guide/tempo-transaction/index.mdx rename to src/pages/docs/guide/tempo-transaction/index.mdx index b034df68..d128b567 100644 --- a/src/pages/guide/tempo-transaction/index.mdx +++ b/src/pages/docs/guide/tempo-transaction/index.mdx @@ -4,7 +4,7 @@ description: Learn how to use Tempo Transactions for configurable fee tokens, fe --- import { Cards, Card } from 'vocs' -import TempoTxProperties from '../../../snippets/tempo-tx-properties.mdx' +import TempoTxProperties from '../../../../snippets/tempo-tx-properties.mdx' # Use Tempo Transactions @@ -14,7 +14,7 @@ Tempo Transactions are a new [EIP-2718](https://github.com/ethereum/EIPs/blob/ma Transaction [SDKs](#integration-guides) are available for TypeScript, Rust, Go, Python, and Foundry. ::: -If you're integrating with Tempo, we **strongly recommend** using Tempo Transactions, and not regular Ethereum transactions. Learn more about the benefits below, or follow the guide on issuance [here](/guide/issuance). +If you're integrating with Tempo, we **strongly recommend** using Tempo Transactions, and not regular Ethereum transactions. Learn more about the benefits below, or follow the guide on issuance [here](/docs/guide/issuance). diff --git a/src/pages/index.mdx b/src/pages/docs/index.mdx similarity index 81% rename from src/pages/index.mdx rename to src/pages/docs/index.mdx index 9d4b56cf..823a2bb5 100644 --- a/src/pages/index.mdx +++ b/src/pages/docs/index.mdx @@ -18,37 +18,31 @@ These docs cover everything from creating a wallet to building payment systems o - diff --git a/src/pages/docs/partners.mdx b/src/pages/docs/partners.mdx new file mode 100644 index 00000000..91c7930e --- /dev/null +++ b/src/pages/docs/partners.mdx @@ -0,0 +1,27 @@ +--- +title: Partners +description: Discover Tempo's ecosystem of stablecoin issuers, wallets, custody providers, compliance tools, ramps, orchestration services, and infrastructure partners. +--- + +import { Cards, Card } from 'vocs' + +# Partners + +Tempo works with partners across stablecoin issuance, wallets and custody, compliance tooling, fraud monitoring, interoperability protocols, analytics and monitoring, orchestration, ramps, and infrastructure. + +The ecosystem is designed to support production payment workloads from day one, with issuers across regions, broad local currency support, and infrastructure partners for developers building on Tempo. + + + + + diff --git a/src/pages/protocol/blockspace/consensus.mdx b/src/pages/docs/protocol/blockspace/consensus.mdx similarity index 100% rename from src/pages/protocol/blockspace/consensus.mdx rename to src/pages/docs/protocol/blockspace/consensus.mdx diff --git a/src/pages/protocol/blockspace/overview.mdx b/src/pages/docs/protocol/blockspace/overview.mdx similarity index 94% rename from src/pages/protocol/blockspace/overview.mdx rename to src/pages/docs/protocol/blockspace/overview.mdx index 12c8ecbc..e130780d 100644 --- a/src/pages/protocol/blockspace/overview.mdx +++ b/src/pages/docs/protocol/blockspace/overview.mdx @@ -25,7 +25,7 @@ pub struct Header { } ``` - `inner` is the canonical Ethereum header (parent_hash, state_root, gas_limit, etc.). -- `general_gas_limit` and `shared_gas_limit` partition the canonical `gas_limit` for payment and non-payment capacity (see [payment lane specification](/protocol/blockspace/payment-lane-specification)). +- `general_gas_limit` and `shared_gas_limit` partition the canonical `gas_limit` for payment and non-payment capacity (see [payment lane specification](/docs/protocol/blockspace/payment-lane-specification)). - `timestamp_millis_part` stores the sub‑second component; the full timestamp is `inner.timestamp * 1000 + timestamp_millis_part` . ### Block body @@ -39,4 +39,4 @@ The block body in Tempo retains the canonical Ethereum block body structure, wit ### System transactions A valid tempo block must contain the following system transaction: - - **Rewards Registry (start-of-block)** — must be the first transaction in the block body; refreshes validator rewards metadata before user transactions begin. Detailed specification [here](/protocol/tip20-rewards/spec). + - **Rewards Registry (start-of-block)** — must be the first transaction in the block body; refreshes validator rewards metadata before user transactions begin. Detailed specification [here](/docs/protocol/tip20-rewards/spec). diff --git a/src/pages/protocol/blockspace/payment-lane-specification.mdx b/src/pages/docs/protocol/blockspace/payment-lane-specification.mdx similarity index 100% rename from src/pages/protocol/blockspace/payment-lane-specification.mdx rename to src/pages/docs/protocol/blockspace/payment-lane-specification.mdx diff --git a/src/pages/protocol/exchange/exchange-balance.mdx b/src/pages/docs/protocol/exchange/exchange-balance.mdx similarity index 100% rename from src/pages/protocol/exchange/exchange-balance.mdx rename to src/pages/docs/protocol/exchange/exchange-balance.mdx diff --git a/src/pages/protocol/exchange/executing-swaps.mdx b/src/pages/docs/protocol/exchange/executing-swaps.mdx similarity index 97% rename from src/pages/protocol/exchange/executing-swaps.mdx rename to src/pages/docs/protocol/exchange/executing-swaps.mdx index 2d48fa54..668ae607 100644 --- a/src/pages/protocol/exchange/executing-swaps.mdx +++ b/src/pages/docs/protocol/exchange/executing-swaps.mdx @@ -147,4 +147,4 @@ Larger swaps that cross more orders will cost more gas, but the cost per unit of The DEX allows you to track token balances directly within the DEX contract, which saves gas by avoiding ERC-20 transfers on every trade. When you execute a swap, the contract first checks your DEX balance and only transfers from your wallet if needed. -For complete details on checking balances, depositing, withdrawing, and managing your DEX balance, see the [DEX Balance](/protocol/exchange/exchange-balance) page. \ No newline at end of file +For complete details on checking balances, depositing, withdrawing, and managing your DEX balance, see the [DEX Balance](/docs/protocol/exchange/exchange-balance) page. \ No newline at end of file diff --git a/src/pages/protocol/exchange/index.mdx b/src/pages/docs/protocol/exchange/index.mdx similarity index 56% rename from src/pages/protocol/exchange/index.mdx rename to src/pages/docs/protocol/exchange/index.mdx index be698ad4..c0c6d9fb 100644 --- a/src/pages/protocol/exchange/index.mdx +++ b/src/pages/docs/protocol/exchange/index.mdx @@ -10,36 +10,36 @@ Tempo features an enshrined decentralized exchange (DEX) designed specifically f The exchange operates as a singleton precompiled contract at address `0xdec0000000000000000000000000000000000000`. It maintains an orderbook with separate queues for each price tick, using price-time priority for order matching. -Trading pairs are determined by each token's quote token. All TIP-20 tokens specify a quote token for trading on the exchange. See [Quote Tokens](/protocol/exchange/quote-tokens) for more information on quote token selection and the optional [pathUSD](/protocol/exchange/quote-tokens#pathusd) stablecoin. See the [Stablecoin DEX Specification](/protocol/exchange/spec) for detailed information on the exchange structure. +Trading pairs are determined by each token's quote token. All TIP-20 tokens specify a quote token for trading on the exchange. See [Quote Tokens](/docs/protocol/exchange/quote-tokens) for more information on quote token selection and the optional [pathUSD](/docs/protocol/exchange/quote-tokens#pathusd) stablecoin. See the [Stablecoin DEX Specification](/docs/protocol/exchange/spec) for detailed information on the exchange structure. The exchange supports three types of orders, each with different execution behavior: | Order Type | Description | |------------|-------------| -| [**Limit Orders**](/protocol/exchange/providing-liquidity#limit-orders) | Place orders at specific price levels that wait in the book until matched or cancelled. Orders are added to the book immediately when placed. | -| [**Flip Orders**](/protocol/exchange/providing-liquidity#flip-orders) | Special orders that automatically reverse to the opposite side when completely filled, acting like a perpetual liquidity pool. When a flip order is fully filled, the same `orderId` is rewritten on the opposite side and emits `OrderFlipped`. | -| [**Market Orders**](/protocol/exchange/executing-swaps#swap-functions) | Execute immediately against the best available orders in the book (via swap functions). Swaps and cancellations execute immediately within the transaction. | +| [**Limit Orders**](/docs/protocol/exchange/providing-liquidity#limit-orders) | Place orders at specific price levels that wait in the book until matched or cancelled. Orders are added to the book immediately when placed. | +| [**Flip Orders**](/docs/protocol/exchange/providing-liquidity#flip-orders) | Special orders that automatically reverse to the opposite side when completely filled, acting like a perpetual liquidity pool. When a flip order is fully filled, the same `orderId` is rewritten on the opposite side and emits `OrderFlipped`. | +| [**Market Orders**](/docs/protocol/exchange/executing-swaps#swap-functions) | Execute immediately against the best available orders in the book (via swap functions). Swaps and cancellations execute immediately within the transaction. | -For the complete execution mechanics, see the [Stablecoin DEX Specification](/protocol/exchange/spec). +For the complete execution mechanics, see the [Stablecoin DEX Specification](/docs/protocol/exchange/spec). To get started with the exchange, explore these guides: @@ -47,5 +47,5 @@ To get started with the exchange, explore these guides: :::info -For a more complete technical specification including design decisions and details of execution semantics, see the [Stablecoin DEX Specification](/protocol/exchange/spec). +For a more complete technical specification including design decisions and details of execution semantics, see the [Stablecoin DEX Specification](/docs/protocol/exchange/spec). ::: diff --git a/src/pages/protocol/exchange/providing-liquidity.mdx b/src/pages/docs/protocol/exchange/providing-liquidity.mdx similarity index 96% rename from src/pages/protocol/exchange/providing-liquidity.mdx rename to src/pages/docs/protocol/exchange/providing-liquidity.mdx index cbea348a..a16d447e 100644 --- a/src/pages/protocol/exchange/providing-liquidity.mdx +++ b/src/pages/docs/protocol/exchange/providing-liquidity.mdx @@ -8,7 +8,7 @@ Provide liquidity to the DEX by placing limit orders or flip orders in the oncha When your orders are filled, you earn the spread between bid and ask prices while helping facilitate trades for other users. -You can only place orders on pairs between a token and its designated quote token. All TIP-20 tokens specify a quote token for trading pairs. [pathUSD](/protocol/exchange/quote-tokens#pathusd) can be used as a simple choice for a quote token. +You can only place orders on pairs between a token and its designated quote token. All TIP-20 tokens specify a quote token for trading pairs. [pathUSD](/docs/protocol/exchange/quote-tokens#pathusd) can be used as a simple choice for a quote token. ## Overview @@ -170,7 +170,7 @@ function cancel( exchange.cancel(12345); ``` -Cancellations execute immediately, and any unfilled portion of your order is refunded to your [DEX balance](/protocol/exchange/exchange-balance). +Cancellations execute immediately, and any unfilled portion of your order is refunded to your [DEX balance](/docs/protocol/exchange/exchange-balance). :::warning You can only cancel your own orders. Attempting to cancel another user's order will revert. diff --git a/src/pages/protocol/exchange/quote-tokens.mdx b/src/pages/docs/protocol/exchange/quote-tokens.mdx similarity index 95% rename from src/pages/protocol/exchange/quote-tokens.mdx rename to src/pages/docs/protocol/exchange/quote-tokens.mdx index 582b1665..b93c2f05 100644 --- a/src/pages/protocol/exchange/quote-tokens.mdx +++ b/src/pages/docs/protocol/exchange/quote-tokens.mdx @@ -22,7 +22,7 @@ pathUSD is not meant to compete as a consumer-facing stablecoin. Use of pathUSD ### Contract -pathUSD is a predeployed [TIP-20](/protocol/tip20/spec) at genesis. Since it is the first TIP-20 deployed, its quote token is the zero address. +pathUSD is a predeployed [TIP-20](/docs/protocol/tip20/spec) at genesis. Since it is the first TIP-20 deployed, its quote token is the zero address. | Property | Value | | -------------- | -------------------------------------------- | diff --git a/src/pages/protocol/exchange/spec.mdx b/src/pages/docs/protocol/exchange/spec.mdx similarity index 95% rename from src/pages/protocol/exchange/spec.mdx rename to src/pages/docs/protocol/exchange/spec.mdx index 22e78904..2312762a 100644 --- a/src/pages/protocol/exchange/spec.mdx +++ b/src/pages/docs/protocol/exchange/spec.mdx @@ -57,7 +57,7 @@ This forces liquidity into a tree structure, which in turn implies that there is USD tokens can only choose USD tokens as their quote token. Non-USD TIP-20 tokens can pick any token as their quote token, but currently there is no support for cross-currency trading, or same-currency trading of non-USD tokens, on the DEX. -The platform offers a neutral USD stablecoin, [`pathUSD`](/protocol/exchange/quote-tokens#pathusd), as an option for quote token. PathUSD is the first stablecoin deployed on the chain, which means it has no quote token. Use of pathUSD is optional. +The platform offers a neutral USD stablecoin, [`pathUSD`](/docs/protocol/exchange/quote-tokens#pathusd), as an option for quote token. PathUSD is the first stablecoin deployed on the chain, which means it has no quote token. Use of pathUSD is optional. #### Swaps @@ -191,7 +191,7 @@ Cancels an order owned by the caller. When canceled, the order is removed from t function cancelStaleOrder(uint128 orderId) external; ``` -Cancels an order where the maker is forbidden by the escrowed token's [TIP-403 transfer policy](/protocol/tip403/overview). Unlike `cancel`, this function can be called by anyone—not just the order maker—but only succeeds if the maker is no longer authorized to transfer the escrowed token (e.g., the maker has been blacklisted). This allows third parties to clean up stale orders from the book. +Cancels an order where the maker is forbidden by the escrowed token's [TIP-403 transfer policy](/docs/protocol/tip403/overview). Unlike `cancel`, this function can be called by anyone—not just the order maker—but only succeeds if the maker is no longer authorized to transfer the escrowed token (e.g., the maker has been blacklisted). This allows third parties to clean up stale orders from the book. When canceled, the order is removed from the tick queue, liquidity is decremented, and remaining escrow is refunded to the order maker's exchange balance. Reverts with `OrderNotStale` if the maker is still authorized. diff --git a/src/pages/protocol/fees/fee-amm/index.mdx b/src/pages/docs/protocol/fees/fee-amm/index.mdx similarity index 90% rename from src/pages/protocol/fees/fee-amm/index.mdx rename to src/pages/docs/protocol/fees/fee-amm/index.mdx index 62b25634..9730c73a 100644 --- a/src/pages/protocol/fees/fee-amm/index.mdx +++ b/src/pages/docs/protocol/fees/fee-amm/index.mdx @@ -23,19 +23,19 @@ This conversion happens automatically at the end of each block through batched s diff --git a/src/pages/protocol/fees/index.mdx b/src/pages/docs/protocol/fees/index.mdx similarity index 85% rename from src/pages/protocol/fees/index.mdx rename to src/pages/docs/protocol/fees/index.mdx index f7d48e28..a309d0d3 100644 --- a/src/pages/protocol/fees/index.mdx +++ b/src/pages/docs/protocol/fees/index.mdx @@ -17,31 +17,31 @@ Tempo uses a fixed base fee (rather than a variable base fee as in EIP-1559), se diff --git a/src/pages/protocol/fees/spec-fee-amm.mdx b/src/pages/docs/protocol/fees/spec-fee-amm.mdx similarity index 100% rename from src/pages/protocol/fees/spec-fee-amm.mdx rename to src/pages/docs/protocol/fees/spec-fee-amm.mdx diff --git a/src/pages/protocol/fees/spec-fee.mdx b/src/pages/docs/protocol/fees/spec-fee.mdx similarity index 82% rename from src/pages/protocol/fees/spec-fee.mdx rename to src/pages/docs/protocol/fees/spec-fee.mdx index 59e434e0..53861720 100644 --- a/src/pages/protocol/fees/spec-fee.mdx +++ b/src/pages/docs/protocol/fees/spec-fee.mdx @@ -13,7 +13,7 @@ This spec lays out how fees work on Tempo, including how fees are calculated, wh Tempo has no native token. Transaction fees are paid directly in USD-denominated stablecoins. This design removes the need for users or applications to hold volatile assets for gas, keeping the entire payment experience USD-native. -Users can pay gas fees in any [TIP-20](/protocol/tip20/spec) token whose currency is USD, as long as that stablecoin has sufficient liquidity on the enshrined [fee AMM](/protocol/fees/spec-fee-amm) against the token that the current validator wants to receive. +Users can pay gas fees in any [TIP-20](/docs/protocol/tip20/spec) token whose currency is USD, as long as that stablecoin has sufficient liquidity on the enshrined [fee AMM](/docs/protocol/fees/spec-fee-amm) against the token that the current validator wants to receive. In determining *which* token a user pays fees in, we want to maximize customizability (so that wallets or users can implement more sophisticated UX than is possible at the protocol layer), minimize surprise (particularly surprises in which a user pays fees in a stablecoin they did not expect to), and have sane default behavior so that users can begin using basic functions like payments even using wallets that are not customized for Tempo support. @@ -31,7 +31,7 @@ The fixed base fee combined with USD-denominated payment provides predictable un Congestion is managed through: -- **Payment lanes**: Reserved blockspace for TIP-20 transfers as specified in the [Payment Lane Specification](/protocol/blockspace/payment-lane-specification). Approximately 94% of blockspace is reserved for payment transactions, with the remaining 6% available for general computation. This allocation is conservative and the blockspace available for general computation may increase over time as total throughput scales. +- **Payment lanes**: Reserved blockspace for TIP-20 transfers as specified in the [Payment Lane Specification](/docs/protocol/blockspace/payment-lane-specification). Approximately 94% of blockspace is reserved for payment transactions, with the remaining 6% available for general computation. This allocation is conservative and the blockspace available for general computation may increase over time as total throughput scales. - **Priority fees**: The `max_priority_fee_per_gas` field allows transactions to bid for faster inclusion during periods of high demand. - **Block gas limits**: Standard per-block gas limits constrain total computation per block. @@ -45,13 +45,13 @@ Before the execution of each transaction, the protocol takes the following steps * Determine the `fee_token` of the transaction, according to the [rules for fee token preferences](#fee-token-preferences). If the fee token cannot be determined, the transaction is invalid. * Compute the `max_fee` of the transaction as `gas_limit * gas_price`. * Deduct `max_fee` from the `fee_payer`'s balance of `fee_token`. If `fee_payer` does not have sufficient balance in `fee_token`, the transaction is invalid. -* Reserve `max_fee` of liquidity on the [fee AMM](/protocol/fees/spec-fee-amm) between the `fee_token` and the validator's preferred fee token. If there is insufficient liquidity, the transaction is invalid. +* Reserve `max_fee` of liquidity on the [fee AMM](/docs/protocol/fees/spec-fee-amm) between the `fee_token` and the validator's preferred fee token. If there is insufficient liquidity, the transaction is invalid. After the execution of each transaction: * Compute the `refund_amount` as `(gas_limit - gas_used) * gas_price`. * Credit the `fee_payer`'s address with `refund_amount` of `fee_token`. -* Log a `Transfer` event from the user to the [fee manager contract](/protocol/fees/spec-fee-amm) for the net amount of the fee payment. +* Log a `Transfer` event from the user to the [fee manager contract](/docs/protocol/fees/spec-fee-amm) for the net amount of the fee payment. :::info[Atomic fee handling] The protocol executes the max fee deduction and refund atomically. If insufficient liquidity is encountered at any point during fee calculation (for example, if a previous transaction by the same sender in the same block exhausted the fee token reserves), the entire transaction reverts. @@ -59,11 +59,11 @@ The protocol executes the max fee deduction and refund atomically. If insufficie ## Fee payer -Tempo supports *sponsored transactions* in which the `fee_payer` is a different address from the `tx.origin` of the transaction. This is supported by Tempo's [new transaction type](/protocol/transactions/spec-tempo-transaction), which has a `fee_payer_signature` field. +Tempo supports *sponsored transactions* in which the `fee_payer` is a different address from the `tx.origin` of the transaction. This is supported by Tempo's [new transaction type](/docs/protocol/transactions/spec-tempo-transaction), which has a `fee_payer_signature` field. If no `fee_payer_signature` is provided, then the `fee_payer` of the transaction is its sender (`tx.origin`). -If the `fee_payer_signature` field is set, then it is used to derive the `fee_payer` for the transaction, as described in the [transaction spec](/protocol/transactions/spec-tempo-transaction). +If the `fee_payer_signature` field is set, then it is used to derive the `fee_payer` for the transaction, as described in the [transaction spec](/docs/protocol/transactions/spec-tempo-transaction). For purposes of [fee token preferences](#fee-token-preferences), the `fee_payer` is the account that chooses the fee token. @@ -107,13 +107,13 @@ The protocol checks preferences at each of these levels, stopping at the first o * The token must be a TIP-20 token whose currency is USD. * The user must have sufficient balance in that token to pay the `gasLimit` on the transaction at the transaction's `gasPrice`. -* There must be sufficient liquidity on the [fee AMM](/protocol/fees/spec-fee-amm), as discussed in that specification. +* There must be sufficient liquidity on the [fee AMM](/docs/protocol/fees/spec-fee-amm), as discussed in that specification. If no preference is specified at the transaction, account, or contract level, the protocol falls back to [pathUSD](#pathusd). ### Transaction level -Tempo's [new transaction type](/protocol/transactions/spec-tempo-transaction), allows transactions to specify a `fee_token` on the transaction. This overrides any preferences set at the account, contract, or validator level. +Tempo's [new transaction type](/docs/protocol/transactions/spec-tempo-transaction), allows transactions to specify a `fee_token` on the transaction. This overrides any preferences set at the account, contract, or validator level. For [sponsored transactions](#fee-payer), the `tx.origin` address does not sign over the `fee_token` field (allowing the `fee_payer` to choose the fee token). @@ -125,7 +125,7 @@ To set its preference, the account can call the `setUserToken` function on the F At this step, the protocol does one more check: -* If the transaction is not a [Tempo transaction](/protocol/transactions/spec-tempo-transaction) *and* the transaction is a top-level call to the `setUserToken` function on the FeeManager, then the protocol checks the `token` argument to the function: +* If the transaction is not a [Tempo transaction](/docs/protocol/transactions/spec-tempo-transaction) *and* the transaction is a top-level call to the `setUserToken` function on the FeeManager, then the protocol checks the `token` argument to the function: * If that token is a TIP-20 whose currency is USD, that token is used as the fee token (unless the transaction specifies a `fee_token` at the [transaction level](#transaction-level)). * If that token is not a TIP-20 or its currency is not USD, the transaction is invalid. @@ -139,18 +139,18 @@ If the top-level call of a transaction is to one of the following functions on a then that TIP-20 token is used as the user's fee token for that transaction (unless there is a preference specified at the [transaction](#transaction-level) or [account](#account-level) level). -For [Tempo Transactions](/protocol/transactions/spec-tempo-transaction), this rule applies only if _all_ top-level calls are to the same TIP-20 contract, and each such call is to one of the functions listed above, with `fee_payer == tx.origin`. +For [Tempo Transactions](/docs/protocol/transactions/spec-tempo-transaction), this rule applies only if _all_ top-level calls are to the same TIP-20 contract, and each such call is to one of the functions listed above, with `fee_payer == tx.origin`. ### Stablecoin DEX contract -If the top-level call of a transaction is to the [Stablecoin DEX](/protocol/exchange/spec) contract, the function being called is either `swapExactAmountIn` or `swapExactAmountOut`, and the `tokenIn` argument to that function is the address of a TIP-20 token for which the currency is USD, then the `tokenIn` argument is used as the user's fee token for the transaction (unless there is a preference specified at the [transaction](#transaction-level) or [account](#account-level) level). +If the top-level call of a transaction is to the [Stablecoin DEX](/docs/protocol/exchange/spec) contract, the function being called is either `swapExactAmountIn` or `swapExactAmountOut`, and the `tokenIn` argument to that function is the address of a TIP-20 token for which the currency is USD, then the `tokenIn` argument is used as the user's fee token for the transaction (unless there is a preference specified at the [transaction](#transaction-level) or [account](#account-level) level). -For [Tempo Transactions](/protocol/transactions/spec-tempo-transaction), this rule applies only if there is only one top-level call in the transaction. +For [Tempo Transactions](/docs/protocol/transactions/spec-tempo-transaction), this rule applies only if there is only one top-level call in the transaction. ### pathUSD -If no fee preference is set at the transaction, account, or contract level, the protocol falls back to [pathUSD](/protocol/exchange/quote-tokens#pathusd) as the user's fee token preference. +If no fee preference is set at the transaction, account, or contract level, the protocol falls back to [pathUSD](/docs/protocol/exchange/quote-tokens#pathusd) as the user's fee token preference. ## Validator preferences @@ -270,4 +270,4 @@ The fee conversion process adds minimal overhead to transactions: - **Post-transaction**: ~3,000 gas for refund and queue operations - **Block settlement**: Amortized across all transactions in the block -For complete technical specifications on the Fee AMM mechanism, see the [Fee AMM Protocol Specification](/protocol/fees/spec-fee-amm). +For complete technical specifications on the Fee AMM mechanism, see the [Fee AMM Protocol Specification](/docs/protocol/fees/spec-fee-amm). diff --git a/src/pages/protocol/index.mdx b/src/pages/docs/protocol/index.mdx similarity index 81% rename from src/pages/protocol/index.mdx rename to src/pages/docs/protocol/index.mdx index 6e5c49c2..e9410b75 100644 --- a/src/pages/protocol/index.mdx +++ b/src/pages/docs/protocol/index.mdx @@ -15,52 +15,46 @@ This section provides details on the Tempo Protocol specifications, and serves a - diff --git a/src/pages/protocol/tip20-rewards/spec.mdx b/src/pages/docs/protocol/tip20-rewards/spec.mdx similarity index 97% rename from src/pages/protocol/tip20-rewards/spec.mdx rename to src/pages/docs/protocol/tip20-rewards/spec.mdx index ef7e8209..e90275fd 100644 --- a/src/pages/protocol/tip20-rewards/spec.mdx +++ b/src/pages/docs/protocol/tip20-rewards/spec.mdx @@ -14,7 +14,7 @@ Many applications require pro-rata distribution of tokens to existing holders (i The rewards mechanism allows anyone to distribute token rewards to opted-in holders proportionally based on holdings. Users must opt in to receiving rewards and may delegate rewards to a recipient address. ## Tempo Token Rewards Functions -These functions are part of the [ITIP20](/protocol/tip20/spec) interface: +These functions are part of the [ITIP20](/docs/protocol/tip20/spec) interface: ```solidity /// @notice Distribute rewards to opted-in token holders diff --git a/src/pages/protocol/tip20/overview.mdx b/src/pages/docs/protocol/tip20/overview.mdx similarity index 77% rename from src/pages/protocol/tip20/overview.mdx rename to src/pages/docs/protocol/tip20/overview.mdx index 0e531126..654ea67f 100644 --- a/src/pages/protocol/tip20/overview.mdx +++ b/src/pages/docs/protocol/tip20/overview.mdx @@ -9,12 +9,12 @@ import { Cards, Card } from 'vocs' TIP-20 tokens are Tempo's native token standard for stablecoins and payment tokens. They are designed for stablecoin payments, and are the foundation for many token-related functions on Tempo including transaction fees, payment lanes, DEX quote tokens, optimized routing for DEX liquidity, optional on-chain token `logoURI` metadata, implicit approvals for listed precompiles, and enshrined payment-channel reserve flows. :::info[Live on testnet with T6] -The [T6 network upgrade](/protocol/upgrades/t6) adds account-level receive policies for TIP-20 transfers and mints ([TIP-1028](https://tips.sh/1028)). A receiver can choose which tokens and senders they accept, helping wallets and deposit addresses avoid unsupported assets, unwanted counterparties, and wrong-token deposits. If a receive policy blocks delivery, the transfer or mint still succeeds, but funds are redirected to `ReceivePolicyGuard` so they can be claimed later. +The [T6 network upgrade](/docs/protocol/upgrades/t6) adds account-level receive policies for TIP-20 transfers and mints ([TIP-1028](https://tips.sh/1028)). A receiver can choose which tokens and senders they accept, helping wallets and deposit addresses avoid unsupported assets, unwanted counterparties, and wrong-token deposits. If a receive policy blocks delivery, the transfer or mint still succeeds, but funds are redirected to `ReceivePolicyGuard` so they can be claimed later. -See the [T5 → T6 migration appendix on the TIP-20 spec](/protocol/tip20/spec#t5--t6-migration) for the TIP-20 surface area. +See the [T5 → T6 migration appendix on the TIP-20 spec](/docs/protocol/tip20/spec#t5--t6-migration) for the TIP-20 surface area. ::: -All TIP-20 tokens are created by interacting with the [TIP-20 Factory contract](/protocol/tip20/spec#tip20factory), calling the `createToken` function. If you're issuing a stablecoin on Tempo, we **strongly recommend** using the TIP-20 standard. Learn more about the benefits, or follow the guide on issuance [here](/guide/issuance). +All TIP-20 tokens are created by interacting with the [TIP-20 Factory contract](/docs/protocol/tip20/spec#tip20factory), calling the `createToken` function. If you're issuing a stablecoin on Tempo, we **strongly recommend** using the TIP-20 standard. Learn more about the benefits, or follow the guide on issuance [here](/docs/guide/issuance). ## Benefits & Features of TIP-20 Tokens @@ -93,32 +93,32 @@ Below are some of the key benefits and features of TIP-20 tokens: Any USD-denominated TIP-20 token can be used to pay transaction fees on Tempo. -The [Fee AMM](/protocol/fees/spec-fee-amm) automatically converts your token to the validator's preferred fee token, eliminating the need for users to hold a separate gas token. This feature works natively: no additional infrastructure or integration required. +The [Fee AMM](/docs/protocol/fees/spec-fee-amm) automatically converts your token to the validator's preferred fee token, eliminating the need for users to hold a separate gas token. This feature works natively: no additional infrastructure or integration required. -Full specification of this feature can be found in the [Payment Lanes Specification](/protocol/blockspace/payment-lane-specification). +Full specification of this feature can be found in the [Payment Lanes Specification](/docs/protocol/blockspace/payment-lane-specification). ### Get Predictable Payment Fees -Tempo has dedicated payment lanes: reserved blockspace for payment TIP-20 transactions that other applications cannot consume. Even if there are extremely popular applications on the chain competing for blockspace, payroll runs or customer disbursements execute predictably. Learn more about the [payments lane](/protocol/blockspace/payment-lane-specification). +Tempo has dedicated payment lanes: reserved blockspace for payment TIP-20 transactions that other applications cannot consume. Even if there are extremely popular applications on the chain competing for blockspace, payroll runs or customer disbursements execute predictably. Learn more about the [payments lane](/docs/protocol/blockspace/payment-lane-specification). ### Role-Based Access Control (RBAC) -TIP-20 includes a built-in [RBAC system](/protocol/tip403/spec#tip-20-token-roles) that separates administrative responsibilities: +TIP-20 includes a built-in [RBAC system](/docs/protocol/tip403/spec#tip-20-token-roles) that separates administrative responsibilities: - **ISSUER_ROLE**: Grants permission to mint and burn tokens, enabling controlled token issuance - **PAUSE_ROLE** / **UNPAUSE_ROLE**: Allows pausing and unpausing token transfers for emergency controls - **BURN_BLOCKED_ROLE**: Permits burning tokens from blocked addresses (e.g., for compliance actions) -Roles can be granted, revoked, and delegated without custom contract changes. This enables issuers to separate operational roles (e.g., who can mint) from administrative roles (e.g., who can pause). Learn more in the [TIP-20 specification](/protocol/tip20/spec#roles). +Roles can be granted, revoked, and delegated without custom contract changes. This enables issuers to separate operational roles (e.g., who can mint) from administrative roles (e.g., who can pause). Learn more in the [TIP-20 specification](/docs/protocol/tip20/spec#roles). ### Tempo Policy Registry (TIP-403) -TIP-20 tokens integrate with the [Tempo Policy Registry (TIP-403)](/protocol/tip403/overview) to enforce compliance policies. Each token can reference a policy that controls who can send and receive tokens: +TIP-20 tokens integrate with the [Tempo Policy Registry (TIP-403)](/docs/protocol/tip403/overview) to enforce compliance policies. Each token can reference a policy that controls who can send and receive tokens: - **Whitelist policies**: Only addresses in the whitelist can transfer tokens - **Blacklist policies**: Addresses in the blacklist are blocked from transferring tokens -Policies can be shared across multiple tokens, enabling consistent compliance enforcement across your token ecosystem. See the [TIP-403 specification](/protocol/tip403/spec) for details. +Policies can be shared across multiple tokens, enabling consistent compliance enforcement across your token ecosystem. See the [TIP-403 specification](/docs/protocol/tip403/spec) for details. ### Operational Controls @@ -132,11 +132,11 @@ TIP-20 tokens also have **pause/unpause** commands, which provide emergency cont ### Reward Distribution -TIP-20 supports an opt-in [reward distribution system](/protocol/tip20-rewards/overview) that allows issuers to distribute rewards to token holders. Rewards can be claimed by holders or automatically forwarded to designated recipient addresses. +TIP-20 supports an opt-in [reward distribution system](/docs/protocol/tip20-rewards/overview) that allows issuers to distribute rewards to token holders. Rewards can be claimed by holders or automatically forwarded to designated recipient addresses. ### Currency Declaration -A TIP-20 token can declare a currency identifier that identifies the reference asset whose price the token is designed to track. This enables proper routing and pricing in Tempo's [Stablecoin DEX](/protocol/exchange). Currently, **only `USD`-denominated stablecoins** can be used to pay transaction fees on Tempo or traded on the StablecoinDEX. +A TIP-20 token can declare a currency identifier that identifies the reference asset whose price the token is designed to track. This enables proper routing and pricing in Tempo's [Stablecoin DEX](/docs/protocol/exchange). Currently, **only `USD`-denominated stablecoins** can be used to pay transaction fees on Tempo or traded on the StablecoinDEX. #### General principle @@ -168,7 +168,7 @@ The currency code is **immutable** — it cannot be changed after token creation ### DEX Quote Tokens -TIP-20 tokens can serve as quote tokens in Tempo's decentralized exchange (DEX). When creating trading pairs on the [Stablecoin DEX](/protocol/exchange), TIP-20 tokens function as the quote currency against which other tokens are priced and traded. +TIP-20 tokens can serve as quote tokens in Tempo's decentralized exchange (DEX). When creating trading pairs on the [Stablecoin DEX](/docs/protocol/exchange), TIP-20 tokens function as the quote currency against which other tokens are priced and traded. This enables efficient stablecoin-to-stablecoin trading and provides optimized routing for liquidity. For example, a USDG TIP-20 token can be paired with other stablecoins, allowing traders to swap between different USD-denominated tokens with minimal slippage through concentrated liquidity pools. @@ -179,25 +179,25 @@ By using TIP-20 tokens as quote tokens, the DEX benefits from the same payment-o diff --git a/src/pages/protocol/tip20/spec.mdx b/src/pages/docs/protocol/tip20/spec.mdx similarity index 95% rename from src/pages/protocol/tip20/spec.mdx rename to src/pages/docs/protocol/tip20/spec.mdx index 36dfc7a5..02891cec 100644 --- a/src/pages/protocol/tip20/spec.mdx +++ b/src/pages/docs/protocol/tip20/spec.mdx @@ -13,7 +13,7 @@ TIP-20 extends ERC-20, building these features into precompiled contracts that a It also enables deeper integration with token-specific Tempo features like paying gas in stablecoins and payment lanes. ## Specification -TIP-20 tokens support standard fungible token operations such as transfers, mints, and burns. They also support transfers, mints, and burns with an attached 32-byte memo; a role-based access control system for token administrative operations; and a system for opt-in [reward distribution](/protocol/tip20-rewards/spec). +TIP-20 tokens support standard fungible token operations such as transfers, mints, and burns. They also support transfers, mints, and burns with an attached 32-byte memo; a role-based access control system for token administrative operations; and a system for opt-in [reward distribution](/docs/protocol/tip20-rewards/spec). ## TIP20 @@ -475,7 +475,7 @@ Recipient-bearing TIP-20 paths — `transfer`, `transferFrom`, `transferWithMemo Virtual addresses are valid TIP-20 recipients on those paths but remain forwarding aliases rather than canonical TIP-20 holders. Non-TIP-20 tokens sent to a virtual address do not forward. Forwarded deposits appear as two-hop standard `Transfer` events in the same transaction; indexers and explorers should collapse that pair into one logical deposit to the resolved master wallet. ## Currencies and Quote Tokens -Each TIP-20 token declares a [currency identifier](/protocol/tip20/overview#currency-declaration) and a corresponding `quoteToken` used for pricing and routing in the Stablecoin DEX. The currency is set at token creation and **cannot be changed afterward**. **Only tokens with `currency == "USD"` are eligible for paying transaction fees.** Tokens with `currency == "USD"` must pair with a USD-denominated TIP-20 token. +Each TIP-20 token declares a [currency identifier](/docs/protocol/tip20/overview#currency-declaration) and a corresponding `quoteToken` used for pricing and routing in the Stablecoin DEX. The currency is set at token creation and **cannot be changed afterward**. **Only tokens with `currency == "USD"` are eligible for paying transaction fees.** Tokens with `currency == "USD"` must pair with a USD-denominated TIP-20 token. Updating the quote token occurs in two phases: 1. `setNextQuoteToken` stages a new quote token. @@ -489,12 +489,12 @@ While quote tokens can be changed, choose carefully as the update process requir ::: ## Permit (TIP-1004) -TIP-20 tokens support [EIP-2612](https://eips.ethereum.org/EIPS/eip-2612) `permit`, added in the [T2 network upgrade](/protocol/upgrades/t2). A token owner signs an EIP-712 typed message off-chain authorizing a spender, and any third party can submit that signature on-chain — combining approve and action into a single transaction without the owner paying gas. +TIP-20 tokens support [EIP-2612](https://eips.ethereum.org/EIPS/eip-2612) `permit`, added in the [T2 network upgrade](/docs/protocol/upgrades/t2). A token owner signs an EIP-712 typed message off-chain authorizing a spender, and any third party can submit that signature on-chain — combining approve and action into a single transaction without the owner paying gas. The `DOMAIN_SEPARATOR` is computed dynamically on every call using `block.chainid`, so it remains correct after a chain fork. Each owner has a monotonically increasing `nonce` to prevent replay. Only `v = 27` or `v = 28` is accepted; `v = 0` or `v = 1` is intentionally **not** normalized (see [TIP-1004](https://github.com/tempoxyz/tempo/blob/main/tips/tip-1004.md) for rationale). ## Logo URI (TIP-1026) -Every TIP-20 exposes an optional on-chain `logoURI`, added in the [T5 network upgrade](/protocol/upgrades/t5) ([TIP-1026](https://tips.sh/1026)). Wallets and explorers read the icon directly from the token contract via `logoURI()`, without an off-chain registry round-trip. Tokens without a `logoURI` continue to work; the field is empty by default. +Every TIP-20 exposes an optional on-chain `logoURI`, added in the [T5 network upgrade](/docs/protocol/upgrades/t5) ([TIP-1026](https://tips.sh/1026)). Wallets and explorers read the icon directly from the token contract via `logoURI()`, without an off-chain registry round-trip. Tokens without a `logoURI` continue to work; the field is empty by default. The admin (`DEFAULT_ADMIN_ROLE`) sets or clears it via `setLogoURI(string newLogoURI)`, which emits `LogoURIUpdated(msg.sender, newLogoURI)`. An empty string is valid and clears the field. A non-empty value must be at most 256 bytes (`LogoURITooLong` otherwise) and a syntactically valid URI whose scheme is in the allowlist `{https, http, ipfs, data}`, matched case-insensitively (`InvalidLogoURI` otherwise). @@ -528,10 +528,10 @@ An Implicit Approval List names the precompiles that may pull TIP-20 tokens with This generalizes the system-only path described above (`systemTransferFrom`, `transferFeePreTx`, `transferFeePostTx`) to an allow-list of precompiles — the StablecoinDEX, FeeAMM, and the `TIP20ChannelReserve` precompile. Normal `approve`, `permit`, `allowance`, and `transferFrom` behavior is unchanged. ### Payment-channel reserve (TIP-1034) -The enshrined `TIP20ChannelReserve` precompile at [`0x4D50500000000000000000000000000000000000`](https://explore.tempo.xyz/address/0x4D50500000000000000000000000000000000000) (ASCII `MPP`) is a TIP-20 consumer rather than a change to the TIP-20 contract itself — it pulls funds via the implicit-approval path above and emits standard `Transfer` events from the host TIP-20. See the [enshrined TIP-20 reserve channel section of the T5 page](/protocol/upgrades/t5#enshrined-tip-20-reserve-channel) for the channel lifecycle, channel ID derivation, and event surface. +The enshrined `TIP20ChannelReserve` precompile at [`0x4D50500000000000000000000000000000000000`](https://explore.tempo.xyz/address/0x4D50500000000000000000000000000000000000) (ASCII `MPP`) is a TIP-20 consumer rather than a change to the TIP-20 contract itself — it pulls funds via the implicit-approval path above and emits standard `Transfer` events from the host TIP-20. See the [enshrined TIP-20 reserve channel section of the T5 page](/docs/protocol/upgrades/t5#enshrined-tip-20-reserve-channel) for the channel lifecycle, channel ID derivation, and event surface. ## Token Rewards Distribution -See [rewards distribution](/protocol/tip20-rewards/spec) for more information. +See [rewards distribution](/docs/protocol/tip20-rewards/spec) for more information. ## TIP20Factory The `TIP20Factory` contract is the canonical entrypoint for creating new TIP-20 tokens on Tempo. The factory derives deterministic deployment addresses using a caller-provided salt, combined with the caller's address, under a fixed 12-byte TIP-20 prefix. This ensures that every TIP-20 token exists at a predictable, collision-free address. The `TIP20Factory` precompile is deployed at `0x20Fc000000000000000000000000000000000000`. @@ -561,7 +561,7 @@ interface ITIP20Factory { /// @notice Creates and deploys a new TIP-20 token /// @param name The token's ERC-20 name /// @param symbol The token's ERC-20 symbol - /// @param currency The token's currency identifier (ISO 4217 code, when available). Immutable after creation. See Currency Declaration (https://docs.tempo.xyz/protocol/tip20/overview#currency-declaration). + /// @param currency The token's currency identifier (ISO 4217 code, when available). Immutable after creation. See Currency Declaration (https://docs.tempo.xyz/docs/protocol/tip20/overview#currency-declaration). /// @param quoteToken The TIP-20 quote token used for exchange pricing /// @param admin The address to receive DEFAULT_ADMIN_ROLE on the new token /// @param salt A unique salt for deterministic address derivation @@ -668,7 +668,7 @@ interface ITIP20Factory { ## T5 → T6 migration :::info[Migration appendix] -This section captures TIP-20 changes introduced by the [T6 network upgrade](/protocol/upgrades/t6). These changes are live on testnet as of June 18, 2026 4pm CEST and are scheduled for mainnet activation on June 23, 2026 4pm CEST. After mainnet activation, the items below will be folded into the body and this appendix will be removed. +This section captures TIP-20 changes introduced by the [T6 network upgrade](/docs/protocol/upgrades/t6). These changes are live on testnet as of June 18, 2026 4pm CEST and are scheduled for mainnet activation on June 23, 2026 4pm CEST. After mainnet activation, the items below will be folded into the body and this appendix will be removed. ::: T6 introduces one change that affects the TIP-20 transfer and mint surface: diff --git a/src/pages/protocol/tip20/virtual-addresses.mdx b/src/pages/docs/protocol/tip20/virtual-addresses.mdx similarity index 98% rename from src/pages/protocol/tip20/virtual-addresses.mdx rename to src/pages/docs/protocol/tip20/virtual-addresses.mdx index 11ccc651..3dba87d7 100644 --- a/src/pages/protocol/tip20/virtual-addresses.mdx +++ b/src/pages/docs/protocol/tip20/virtual-addresses.mdx @@ -4,7 +4,7 @@ description: Understand how TIP-20 virtual addresses work, why they remove sweep --- import { Cards, Card } from 'vocs' -import { MermaidDiagram } from '../../../components/MermaidDiagram' +import { MermaidDiagram } from '../../../../components/MermaidDiagram' # Virtual addresses for TIP-20 deposits @@ -160,7 +160,7 @@ Adopting virtual addresses is straightforward conceptually: - ongoing operations: derive deposit addresses offchain - reconciliation: decode the `userTag` from events and credit the right customer internally -If you want the exact transfer semantics, event shape, and validation rules, read [TIP-1022](https://github.com/tempoxyz/tempo/blob/main/tips/tip-1022.md) alongside the [TIP-20 specification](/protocol/tip20/spec). +If you want the exact transfer semantics, event shape, and validation rules, read [TIP-1022](https://github.com/tempoxyz/tempo/blob/main/tips/tip-1022.md) alongside the [TIP-20 specification](/docs/protocol/tip20/spec). ## Learn more @@ -168,7 +168,7 @@ If you want the exact transfer semantics, event shape, and validation rules, rea diff --git a/src/pages/protocol/tip403/overview.mdx b/src/pages/docs/protocol/tip403/overview.mdx similarity index 92% rename from src/pages/protocol/tip403/overview.mdx rename to src/pages/docs/protocol/tip403/overview.mdx index b8302951..879f0651 100644 --- a/src/pages/protocol/tip403/overview.mdx +++ b/src/pages/docs/protocol/tip403/overview.mdx @@ -15,13 +15,13 @@ The Tempo Policy Registry (TIP-403) enables TIP-20 tokens to enforce access cont diff --git a/src/pages/protocol/tip403/spec.mdx b/src/pages/docs/protocol/tip403/spec.mdx similarity index 100% rename from src/pages/protocol/tip403/spec.mdx rename to src/pages/docs/protocol/tip403/spec.mdx diff --git a/src/pages/protocol/tips/index.mdx b/src/pages/docs/protocol/tips/index.mdx similarity index 75% rename from src/pages/protocol/tips/index.mdx rename to src/pages/docs/protocol/tips/index.mdx index f712f7f0..b3f1962b 100644 --- a/src/pages/protocol/tips/index.mdx +++ b/src/pages/docs/protocol/tips/index.mdx @@ -3,10 +3,10 @@ title: Tempo Improvement Proposals description: Browse Tempo Improvement Proposals covering protocol changes, network upgrades, precompiles, transaction formats, and stablecoin standards. --- +import { TipsList } from '../../../../components/TipsList' + # Tempo Improvement Proposals (TIPs) TIPs are design documents that describe changes to the Tempo protocol. Each TIP provides a complete specification that serves as the source of truth for implementation. ---- - -TIP pages redirect to the canonical TIP markdown in the [Tempo repository](https://github.com/tempoxyz/tempo/tree/main/tips). + diff --git a/src/pages/docs/protocol/tips/tip-0000.mdx b/src/pages/docs/protocol/tips/tip-0000.mdx new file mode 100644 index 00000000..7ad74f96 --- /dev/null +++ b/src/pages/docs/protocol/tips/tip-0000.mdx @@ -0,0 +1,106 @@ +--- +id: TIP-0000 +title: TIP Process +description: Defines the Tempo Improvement Proposal lifecycle from draft to production. +authors: Internal +status: Draft +related: N/A +--- + +# TIP-0000: TIP Process + +## Abstract + +This TIP defines the lifecycle for a Tempo Improvement Proposal, from draft through mainnet activation. It sets clear decision gates, required reviews, and ownership expectations so proposals are evaluated consistently before they are scheduled and rolled out. + +**External TIP submissions are not accepted at this time.** + +## Motivation + +This process gives the team one shared path from idea to production. A consistent lifecycle improves decision quality, keeps security and ecosystem impact visible, and helps prioritize changes that solve real user problems. + +--- + +# Specification + +## Status Lifecycle + +`Draft` → `In Review` / `Rejected` + +`In Review` → `Ready for Consideration` / `Rejected` + +`Ready for Consideration` → `Approved` / `Backlog` / `Rejected` + +`Backlog` → `Ready for Consideration` / `Rejected` + +`Approved` → `Scheduled` → `Testnet` → `Mainnet` + +## Status Definitions + +- `Draft`: The idea is being developed into a complete TIP and is not yet in formal review. +- `In Review`: The TIP is under structured technical, security, and implications review. +- `Ready for Consideration`: Required review is complete and the TIP is ready for a network upgrade call decision. +- `Backlog`: The TIP is directionally supported, but deferred until there is clear product or customer pull. +- `Approved`: The TIP is accepted and eligible for upgrade scheduling. +- `Scheduled`: The TIP is assigned to a specific upgrade. +- `Testnet`: The TIP is released on testnet and monitored against success criteria. +- `Mainnet`: The TIP is released on mainnet. +- `Rejected`: The TIP does not move forward in its current form. + +## 1. Propose a TIP + +Goal: Turn an idea into a complete, reviewable specification. + +1. Share the problem and proposed direction early to gather feedback. +2. Assign one TIP owner who is accountable for moving the TIP forward. +3. Pick the lowest available TIP number and create branch `tip/xxxx`. +4. Create `tip-xxxx.md` using the [TIP Template](https://github.com/tempoxyz/tempo/blob/main/tips/tip_template.md) and open a draft PR. +5. Complete the draft with: problem statement, design, assumptions, alternatives considered, threat model, expected tooling/user impact, and success criteria. +6. When ready, set status to `In Review` and request stakeholder review. + +## 2. Review the TIP + +Goal: Validate the proposal's value, feasibility, and risk. + +1. Run stakeholder review in the PR and keep the TIP updated. +2. Provide evidence that the problem is real and worth solving now. +3. Run a whiteboard session if needed to align context. +4. Complete engineering review and confirm the design is feasible and robust. +5. Complete a security review. +6. Complete an implications review for tooling, integrations, and partners, and share outcomes with affected stakeholders. +7. Obtain engineering, research, and security approval. +8. Before merging, set status to `Ready for Consideration`, then merge. + +## 3. Consideration and Decision (Network Upgrade Call) + +Goal: Move each `Ready for Consideration` TIP to a clear decision. + +1. The TIP owner flags with the network upgrade call chair that a decision is needed ahead of the call. +2. The TIP owner presents review outcomes, key tradeoffs, and major spec changes. +3. The call records one outcome: `Approved`, `Backlog`, or `Rejected`. +4. If `Approved`, confirm a target upgrade when possible. +5. If `Backlog`, record what signal is needed to revisit it. + +## 4. Scheduling Criteria + +A TIP can move from `Approved` to `Scheduled` only if: + +1. Engineering capacity is available. +2. A target upgrade is identified. +3. Inclusion timing is explicit (`Why include? Why now?`). +4. The TIP spec is complete and implementation-ready. + +## 5. Ship a TIP + +Goal: Implement the approved TIP and prepare it for rollout. + +1. Implement the TIP and keep the spec aligned with what is built. +2. Implementation can still surface learnings; updates and scoped changes are welcome. +3. If implementation materially changes the TIP, request another approval from the same engineering, research, and security stakeholders. This second approval should be fast and focused on the implementation delta. + +## 6. Testnet Release Readiness + +Before moving an upgrade to `Testnet`: + +1. Publish technical communication, including tooling and user impact. +2. Ensure dashboards and alerting are in place for success criteria. diff --git a/src/pages/docs/protocol/tips/tip-1000.mdx b/src/pages/docs/protocol/tips/tip-1000.mdx new file mode 100644 index 00000000..ee59b6e4 --- /dev/null +++ b/src/pages/docs/protocol/tips/tip-1000.mdx @@ -0,0 +1,351 @@ +--- +id: TIP-1000 +title: State Creation Cost Increase +description: Increased gas costs for state creation operations to protect Tempo from adversarial state growth attacks. +authors: Dankrad Feist @dankrad +status: Mainnet +related: N/A +protocolVersion: T1 +--- + +# TIP-1000: State Creation Cost Increase + +- **Protocol Version**: T1 + +## Abstract + +This TIP increases the gas cost for creating new state elements, accounts, and contract code to provide economic protection against state growth spam attacks. The proposal increases the cost of writing a new state element from 20,000 gas to 250,000 gas, introduces a 250,000 gas charge for account creation (when the account's nonce is first written), and implements a new contract creation cost model: 1,000 gas per byte of contract code plus a fixed upfront contract creation cost of 500,000 gas. + +## Motivation + +Tempo's high throughput capability (approximately 20,000 transactions per second) creates a vulnerability where an adversary could create a massive amount of state with the intent of permanently slowing the chain down. If each transaction is used to create a new account, and each account requires approximately 200 bytes of storage, then over 120 TB of storage could be created in a single year. Even if this storage is technically feasible, the database performance implications are unknown and would likely require significant R&D on state management much earlier than needed for business requirements. + +The current EVM gas schedule charges 20,000 gas for writing a new state element and has no cost for creating an account. This makes state creation attacks economically viable for adversaries. By increasing these costs to 250,000 gas each, we create a meaningful economic barrier: creating 1 TB of state would cost approximately $50 million, and creating 10 TB would cost approximately $500 million (based on the assumption that a TIP-20 transfer costs 50,000 gas = 0.1 cent, implying 1 cent per 500,000 gas). + +### Alternatives Considered + +1. **Storage rent**: Implementing a periodic fee for holding state. This was rejected due to complexity and poor user experience. +2. **State expiry**: Automatically removing unused state after a time period. This was rejected due to technical complexity and breaking changes to existing applications. +3. **Lower cost increases**: Using smaller multipliers (e.g., 50,000 gas instead of 250,000 gas). This was rejected as it would not provide sufficient economic deterrent against well-funded attackers. + +## Terminology + +This TIP uses the following economic unit terminology: + +- **Microdollars**: TIP-20 token units at 10^-6 USD precision (6 decimals). One TIP-20 token unit = 1 microdollar = 0.000001 USD = 0.0001 cents. + +- **Attodollars**: Gas accounting units at 10^-18 USD precision. Gas prices (basefee) are denominated in attodollars. + +- **Conversion**: Gas cost in microdollars = (gas × basefee in attodollars) / 10^12 + +These units provide precise economic accounting while maintaining human-readable dollar relationships. + +--- + +# Specification + +## Gas Cost Changes + +### New State Element Creation + +**Current Behavior:** +- Writing a new state element (SSTORE to a zero slot) costs 20,000 gas + +**Proposed Behavior:** +- Writing a new state element (SSTORE to a zero slot) costs 250,000 gas for the state creation component (replacing 20,000 gas) +- The EIP-2929 access cost is charged separately: 2,100 gas for cold access, 100 gas for warm access +- Total cost for a cold zero-to-nonzero SSTORE: 2,100 + 250,000 = 252,100 gas +- Total cost for a warm zero-to-nonzero SSTORE: 100 + 250,000 = 250,100 gas + +This applies to all storage slot writes that transition from zero to non-zero, including: +- Contract storage slots +- TIP-20 token balances +- Nonce key storage in the Nonce precompile (when a new nonce key is first used) +- Rewards-related storage (userRewardInfo mappings, reward balances) +- Active key count tracking in the Nonce precompile +- Any other state elements stored in the EVM state trie + +**Note:** Since Tempo-specific operations (nonce keys, rewards processing, etc.) ultimately use EVM storage operations (SSTORE), they are automatically subject to the new state creation pricing. The implementation must ensure all new state element creation is correctly charged at 250,000 gas, regardless of which precompile or contract creates the state. + +### Account Creation + +**Current Behavior:** +- Account creation has no explicit gas cost +- The account is created implicitly when its nonce is first written + +**Proposed Behavior:** +- Account creation incurs a 250,000 gas charge when the account's nonce is first written +- This charge applies when the account is first used (e.g., sends its first transaction), not when it first receives tokens + +**Implementation Details:** +- The charge is applied when `account.nonce` transitions from 0 to 1 +- The charge also applies to other nonces with [nonce keys](/docs/protocol/transactions/spec-tempo-transaction#specification) (2D nonces) +- Transactions with a nonce value of 0 need to supply at least 271,000 gas and are otherwise invalid +- For EOA accounts: charged on the first transaction sent from that address (when the account is first used) +- For contract accounts: included in the fixed 500,000 gas CREATE cost (see Contract Creation); there is no separate account creation charge +- **Important:** When tokens are transferred TO a new address, the recipient's nonce remains 0, so no account creation cost is charged. The account creation cost only applies when the account is first used (sends a transaction). +- The charge is in addition to any other gas costs for the transaction + +### Contract Creation + +**Current Behavior:** +- Contract creation (CREATE/CREATE2) has a base cost of 32,000 gas plus 200 gas per byte of contract code +- Total cost formula: `32,000 + (code_size × 200)` gas +- Example: A 1,000 byte contract costs 32,000 + (1,000 × 200) = 232,000 gas + +**Proposed Behavior:** +- Contract creation replaces the existing EVM per-byte cost with a new pricing model: + - Each byte: 1,000 gas per byte (linear pricing) + - Fixed upfront contract creation cost: 500,000 gas +- This pricing applies to the contract code size (the bytecode being deployed) + +**Implementation Details:** +- The code storage cost is calculated as: `code_size × 1,000` +- Fixed upfront contract creation cost: 500,000 gas +- Total contract creation cost: `(code_size × 1,000) + 500,000` gas +- This replaces the existing EVM per-byte cost for contract creation (not an additional charge) +- Applies to both CREATE and CREATE2 operations +- The fixed 500,000 gas covers the contract account creation; there is no separate account creation charge for the contract + +### Intrinsic transaction gas + +A transaction is invalid if the minimal costs of a (reverting) transaction can't be covered by caller's balance. Those checks are done in the transaction pool as a DOS prevention measure as well as when a transaction is first executed as part of a block. + +* Transaction with `nonce == 0` require an additional 250,000 gas +* Tempo transactions with any `nonce_key` and `nonce == 0` require an additional 250,000 gas +* Changes to EIP-7702 authorization lists: + * The base cost per authorization is reduced to 12,500 gas + * EIP-7702 authorisation list entries with `auth_list.nonce == 0` require an additional 250,000 gas (account creation for the nonce field) + * EIP-7702 authorisation list entries going from no delegation to delegation require an additional 250,000 gas (state creation for the keccak/code hash field) + * There is no refund if the account already exists +* The additional initial cost for CREATE transactions that deploy a contract is increased to 500,000 from currently 32,000 (to reflect the upfront cost in contract creation) + * If the first transaction in a batch is a CREATE transaction, the additional cost of 500,000 needs to be charged + + +### Other changes + +The transaction gas cap is changed from 16M to 30M to accommodate the deployment of 24kb contracts. + +Tempo transaction key authorisations can't determine whether it is going to create new storage or not. If the transaction cannot pay for the key authorization storage costs, the transaction reverts any authorization key that has been set. + +## Gas Schedule Summary + +| Operation | Current Gas Cost | Proposed Gas Cost | Change | +|-----------|------------------|-------------------|--------| +| New state element (SSTORE zero → non-zero, state creation component) | 20,000 | 250,000 | +230,000 | +| Account creation (first nonce write) | 0 | 250,000 | +250,000 | +| Contract creation (per byte) | 200 | 1,000 | +800 | +| Contract creation (fixed upfront cost) | Included in base | 500,000 | +500,000 | +| Existing state element (SSTORE non-zero → non-zero) | 5,000 | 5,000 | No change | +| Existing state element (SSTORE non-zero → zero) | -15,000 (refund) | -15,000 (refund) | No change | + +## Economic Impact Analysis + +### Cost Calculations + +Based on the assumptions: +- TIP-20 transfer cost (to existing address, including base transaction and state update): 50,000 gas = 0.1 cent (1,000 microdollars) +- Implied gas price: 1 cent per 500,000 gas (10,000 microdollars per 500,000 gas) + +**New State Element Creation:** +- Gas cost: 250,000 gas +- Dollar cost: 250,000 / 500,000 = **0.5 cents (5,000 microdollars) per state element** + +**Account Creation:** +- Gas cost: 250,000 gas +- Dollar cost: 250,000 / 500,000 = **0.5 cents (5,000 microdollars) per account** + +**Contract Creation:** +- Per byte: 1,000 gas = **0.002 cents (20 microdollars) per byte** +- Fixed upfront cost: 500,000 gas = **1.0 cent (10,000 microdollars)** +- Example: 1,000 byte contract = (1,000 × 1,000) + 500,000 = 1,500,000 gas = **3.0 cents (30,000 microdollars)** + +### Attack Cost Analysis + +**Creating 1 TB of state:** +- 1 TB = 1,000,000,000,000 bytes +- Assuming ~100 bytes per state element: 10,000,000,000 state elements +- Cost: 10,000,000,000 × 0.5 cents = **$50,000,000** + +**Creating 10 TB of state:** +- 10 TB = 10,000,000,000,000 bytes +- Assuming ~100 bytes per state element: 100,000,000,000 state elements +- Cost: 100,000,000,000 × 0.5 cents = **$500,000,000** + +These costs serve as a significant economic deterrent against state growth spam attacks. + +## Impact on Normal Operations + +### Transfer to New Address + +**Current Cost:** +- TIP-20 transfer (base + operation): 50,000 gas +- New state element (balance): 20,000 gas +- **Total: ~70,000 gas ≈ 0.14 cents** +- Note: Account creation cost does not apply here because the recipient's nonce remains 0 + +**Proposed Cost:** +- TIP-20 transfer (base + operation): 50,000 gas +- New state element (balance): 250,000 gas +- **Total: ~300,000 gas ≈ 0.6 cents** +- Note: Account creation cost does not apply here because the recipient's nonce remains 0 + +**Impact:** A transfer to a new address increases from 0.14 cents to 0.6 cents, representing a 4.3x increase. The account creation cost (0.5 cents) will be charged separately when the recipient first uses their account. + +### First Use of New Account + +**Current Cost:** +- TIP-20 transfer (base + operation + state update): 50,000 gas +- Account creation: 0 gas +- **Total: 50,000 gas ≈ 0.1 cents** + +**Proposed Cost:** +- TIP-20 transfer (base + operation + state update): 50,000 gas +- Account creation (nonce 0 → 1): 250,000 gas +- **Total: ~300,000 gas ≈ 0.6 cents** + +**Impact:** The first transaction from a new account increases from 0.1 cents to 0.6 cents, representing a 6x increase. Combined with the initial transfer cost (0.6 cents), the total onboarding cost for a new user is approximately 1.2 cents. + +### Transfer to Existing Address + +**Current Cost:** +- TIP-20 transfer (base + operation + state update): 50,000 gas +- **Total: 50,000 gas ≈ 0.1 cents** + +**Proposed Cost:** +- TIP-20 transfer (base + operation + state update): 50,000 gas +- **Total: 50,000 gas ≈ 0.1 cents** + +**Impact:** No change for transfers to existing addresses. + +### Contract Deployment + +**Current Cost:** +- Contract code storage: 32,000 gas base + 200 gas per byte +- Example for 1,000 byte contract: 32,000 + (1,000 × 200) = 232,000 gas ≈ 0.46 cents + +**Proposed Cost:** +- Contract code storage: code_size × 1,000 gas +- Fixed upfront contract creation cost: 500,000 gas +- Example for 1,000 byte contract: (1,000 × 1,000) + 500,000 = 1,500,000 gas ≈ **3.0 cents** + +**Impact:** Contract deployment costs increase significantly, especially for larger contracts. A 100 byte contract costs (100 × 1,000) + 500,000 = 600,000 gas = 1.2 cents. + +## Implementation Requirements + +### Node Implementation + +The node implementation must: + +1. **Detect new state element creation:** + - Track SSTORE operations that write to a zero slot + - Charge 250,000 gas instead of 20,000 gas for these operations + +2. **Detect account creation:** + - Track when an EOA account's nonce transitions from 0 to 1 + - Charge 250,000 gas for this transition + - For contract accounts, the fixed 500,000 gas CREATE cost applies instead + +3. **Implement contract creation pricing:** + - Replace existing EVM per-byte cost for contract code storage + - Charge 1,000 gas per byte of contract code (linear pricing) + - Charge a fixed upfront contract creation cost of 500,000 gas + - Total formula: `(code_size × 1,000) + 500,000` + - Apply to both CREATE and CREATE2 operations + +4. **Maintain backward compatibility:** + - Existing state operations (non-zero to non-zero, non-zero to zero) remain unchanged + - Gas refunds for storage clearing remain unchanged + +### Test Suite Requirements + +The test suite must verify: + +1. **New state element creation:** + - SSTORE to zero slot charges 250,000 gas + - Multiple new state elements in one transaction are each charged 250,000 gas + - Existing state element updates (non-zero to non-zero) remain at 5,000 gas + +2. **Account creation:** + - First transaction from EOA charges 250,000 gas for account creation (when nonce transitions 0 → 1) + - Contract deployment does NOT charge a separate 250,000 gas for the contract's account creation (the nonce write is included in the 500,000 CREATE cost) + - Transfer TO a new address does NOT charge account creation fee (recipient's nonce remains 0) + - Subsequent transactions from the same account do not charge account creation fee + +3. **Contract creation:** + - Contract code storage replaces EVM per-byte cost with new pricing model + - Each byte of contract code costs 1,000 gas (linear pricing) + - Fixed upfront contract creation cost: 500,000 gas + - Total cost formula: `(code_size × 1,000) + 500,000` gas + - Example: 100 byte contract costs (100 × 1,000) + 500,000 = 600,000 gas + - Both CREATE and CREATE2 use the same pricing + +4. **Tempo-specific state creation operations:** + - Nonce key creation: First use of a new nonce key (nonce key > 0) creates storage in Nonce precompile + - Active key count tracking: First nonce key for an account creates active key count storage + - Rewards opt-in: `setRewardRecipient` creates new `userRewardInfo` mapping entry + - Rewards recipient delegation: Setting reward recipient for a new recipient creates storage + - Rewards balance creation: First reward accrual to a recipient creates storage if needed + - All Tempo-specific operations that create new state elements must charge 250,000 gas per new storage slot + +5. **Edge cases:** + - Self-destruct and recreation of account + - Contracts that create accounts via CREATE/CREATE2 + - Batch operations creating multiple accounts/state elements + - Contract deployment with various code sizes (small, medium, large) + - Multiple Tempo-specific operations in a single transaction + +6. **Economic calculations:** + - Verify gas costs match expected dollar amounts + - Verify attack cost calculations for large-scale state creation + - Verify contract creation costs match formula: `(code_size × 1,000) + 500,000` (nonce write included in CREATE cost) + - Verify Tempo-specific operations charge correctly for new state creation + +--- + +# Invariants + +The following invariants must always hold: + +1. **State Creation Cost Invariant:** Any SSTORE operation that writes a non-zero value to a zero slot MUST charge 250,000 gas for the state creation component (not 20,000 gas). The total gas charged also includes the EIP-2929 access cost: 2,100 gas for cold access or 100 gas for warm access, resulting in a total of 252,100 gas (cold) or 250,100 gas (warm). + +2. **Account Creation Cost Invariant:** The first transaction sent from an EOA (causing the sender's nonce to transition from 0 to 1) MUST charge exactly 250,000 gas for account creation. For contract accounts, the fixed 500,000 gas CREATE cost applies instead. + +3. **Existing State Invariant:** SSTORE operations that modify existing non-zero state (non-zero to non-zero) MUST continue to charge 5,000 gas and MUST NOT be affected by this change. + +4. **Storage Clearing Invariant:** SSTORE operations that clear storage (non-zero to zero) MUST continue to provide a 15,000 gas refund and MUST NOT be affected by this change. + +5. **Gas Accounting Invariant:** The total gas charged for a transaction creating N new state elements and M new accounts (where M is the number of accounts whose nonce transitions from 0 to 1 in this transaction) MUST equal: base_transaction_gas + operation_gas + (N × 250,000) + (M × 250,000). Note: Transferring tokens TO a new address does not create the account (nonce remains 0), so M = 0 in that case. + +6. **Contract Creation Cost Invariant:** Contract creation (CREATE/CREATE2) MUST charge exactly `(code_size × 1,000) + 500,000` gas for code storage, replacing the existing EVM per-byte cost. This includes: 1,000 gas per byte of contract code (linear pricing) and a fixed upfront contract creation cost of 500,000 gas. There is no separate account creation charge for the contract. + +7. **Economic Deterrent Invariant:** The cost to create 1 TB of state MUST be at least $50 million, and the cost to create 10 TB of state MUST be at least $500 million, based on the assumed gas price of 1 cent per 500,000 gas. + +## Critical Test Cases + +The test suite must cover: + +1. **Basic state creation:** Single SSTORE to zero slot charges 250,000 gas +2. **Multiple state creation:** Multiple SSTORE operations to zero slots each charge 250,000 gas +3. **Account creation (EOA):** First transaction from new EOA charges 250,000 gas +4. **Contract creation (CREATE):** Contract deployment via CREATE charges a fixed upfront cost of 500,000 gas (no separate account creation charge) +5. **Contract creation (CREATE2):** Contract deployment via CREATE2 charges a fixed upfront cost of 500,000 gas (no separate account creation charge) +6. **Contract creation (small):** Contract with 100 bytes charges (100 × 1,000) + 500,000 = 600,000 gas for code storage +7. **Contract creation (medium):** Contract with 1,000 bytes charges (1,000 × 1,000) + 500,000 = 1,500,000 gas for code storage +8. **Contract creation (large):** Contract with 10,000 bytes charges (10,000 × 1,000) + 500,000 = 10,500,000 gas for code storage +9. **Existing state updates:** SSTORE to existing non-zero slot charges 5,000 gas (unchanged) +10. **Storage clearing:** SSTORE clearing storage provides 15,000 gas refund (unchanged) +11. **Mixed operations:** Transaction creating both new accounts and new state elements charges correctly for both +12. **Transfer to new address:** Complete transaction cost matches expected ~300,000 gas (no account creation cost, only new state element cost) +13. **First use of new account:** Complete transaction cost matches expected ~300,000 gas (account creation cost applies) +14. **Transfer to existing address:** Complete transaction cost matches expected 50,000 gas (unchanged) +15. **Batch operations:** Multiple account creations in one transaction each charge 250,000 gas +16. **Self-destruct and recreate:** Account that self-destructs and is recreated charges account creation fee again +17. **Transfer to new address does not create account:** Transferring tokens to a new address does not charge account creation fee (only new state element fee applies) +18. **Nonce key creation:** First use of a new nonce key creates a new storage slot and charges 250,000 gas +19. **Active key count tracking:** First nonce key for an account creates storage for active key count and charges 250,000 gas +20. **Rewards opt-in:** First call to `setRewardRecipient` creates a new entry and charges 250,000 gas +21. **Rewards recipient delegation:** Setting a new reward recipient creates storage and charges 250,000 gas +22. **Rewards balance creation:** First reward accrual creates storage and charges 250,000 gas (if needed) +23. **Multiple nonce keys:** Creating multiple nonce keys in one transaction each charges 250,000 gas +24. **Nonce key and rewards combined:** Transaction creating both nonce key and rewards storage charges 250,000 gas for each new state element diff --git a/src/pages/docs/protocol/tips/tip-1001.mdx b/src/pages/docs/protocol/tips/tip-1001.mdx new file mode 100644 index 00000000..9a3c0934 --- /dev/null +++ b/src/pages/docs/protocol/tips/tip-1001.mdx @@ -0,0 +1,114 @@ +--- +id: TIP-1001 +title: Place-only mode for next quote token +description: A new DEX function for creating trading pairs against a token's staged next quote token, to allow orders to be placed on it. +authors: Dan Robinson +status: Draft +--- + +# TIP-1001: Place-only mode for next quote token + +## Abstract + +This TIP adds a `createNextPair` function to the Stablecoin DEX that creates a trading pair between a base token and its `nextQuoteToken()`, along with `place` and `placeFlip` overloads that accept a book key to target specific pairs. This enables market makers to place orders on the new pair before a quote token update is finalized, providing a smooth liquidity transition. + +## Motivation + +When a token issuer decides to change their quote token (via `setNextQuoteToken` and `completeQuoteTokenUpdate`), there is currently no way to establish liquidity on the new pair before the transition completes. This means that market makers will need to wait until the quote token has been updated before they can place orders, which could cause a period where there is no liquidity, or limited liquidity, for the token, which will interrupt swaps involving that token. + +By allowing pair creation against `nextQuoteToken()`, this change allows users and market makers to add liquidity to the DEX before it is used on swaps. Since swaps route through `quoteToken()` (not `nextQuoteToken()`), the new pair operates in "place-only" mode: orders can be placed and cancelled, but no swaps route through it until `completeQuoteTokenUpdate()` is called. + +--- + +# Specification + +## New functions + +Add the following functions to the Stablecoin DEX interface: + +```solidity +/// @notice Creates a trading pair between a base token and its next quote token +/// @param base The base token address +/// @return key The pair key for the created pair +/// @dev Reverts if: +/// - The base token has no next quote token staged (nextQuoteToken is zero) +/// - The pair already exists +/// - Either token is not USD-denominated +function createNextPair(address base) external returns (bytes32 key); + +/// @notice Places an order on a specific pair identified by book key +/// @param bookKey The pair key identifying the orderbook +/// @param token The base token of the pair +/// @param amount The order amount in base tokens +/// @param isBid True for buy orders, false for sell orders +/// @param tick The price tick for the order +/// @return orderId The ID of the placed order +function place(bytes32 bookKey, address token, uint128 amount, bool isBid, int16 tick) external returns (uint128 orderId); + +/// @notice Places a flip order on a specific pair identified by book key +/// @param bookKey The pair key identifying the orderbook +/// @param token The base token of the pair +/// @param amount The order amount in base tokens +/// @param isBid True for buy orders, false for sell orders +/// @param tick The price tick for the order +/// @param flipTick The price tick for the flipped order when filled +/// @param internalBalanceOnly If true, only use internal balance for the flipped order +/// @return orderId The ID of the placed order +function placeFlip(bytes32 bookKey, address token, uint128 amount, bool isBid, int16 tick, int16 flipTick, bool internalBalanceOnly) external returns (uint128 orderId); +``` + +## Behavior + +### Pair creation + +`createNextPair(base)` creates a pair between `base` and `base.nextQuoteToken()`. The function: + +1. Calls `nextQuoteToken()` on the base token +2. Reverts with `NO_NEXT_QUOTE_TOKEN` if the result is `address(0)` +3. Validates both tokens are USD-denominated (same as `createPair`) +4. Creates the pair using the same mechanism as `createPair` +5. Emits `PairCreated(key, base, nextQuoteToken)` + +### Place-only mode + +Once the pair exists, it supports the full order lifecycle: + +- `place(bookKey, ...)` and `placeFlip(bookKey, ...)` allow placing orders on the pair +- `cancel` and `cancelStaleOrder` work normally (they use order ID, not pair lookup) +- `books` returns accurate data (it takes the book key directly) + +The new `place` and `placeFlip` overloads are required because the existing functions derive the pair from `token.quoteToken()`, which would look up the wrong pair. The overloads accept a `bookKey` parameter to target the correct pair. + +Swap functions (`swapExactAmountIn`, `swapExactAmountOut`) and quote functions (`quoteSwapExactAmountIn`, `quoteSwapExactAmountOut`) do not route through this pair because routing uses `quoteToken()` to find paths between tokens. + +### After quote token update + +When the token issuer calls `completeQuoteTokenUpdate()`: + +1. The token's `quoteToken()` changes to what was `nextQuoteToken()` +2. The token's `nextQuoteToken()` becomes `address(0)` +3. The existing pair (created via `createNextPair`) is now the active pair +4. Swaps begin routing through the pair + +The old pair (against the previous quote token) remains but will no longer be used for routing swaps involving this base token. Orders on it can be canceled using their ID. + +## New error + +```solidity +/// @notice The base token has no next quote token staged +error NO_NEXT_QUOTE_TOKEN(); +``` + +## Events + +No new events. The existing `PairCreated` event is emitted by `createNextPair`, and the existing `OrderPlaced` event is emitted by the `place` and `placeFlip` overloads. + +--- + +# Invariants + +- A pair created via `createNextPair` must be identical to one created via `createPair` once `completeQuoteTokenUpdate` is called +- `createNextPair` must revert if `nextQuoteToken()` returns `address(0)` +- `createNextPair` must revert if the pair already exists (same as `createPair`) +- Orders placed on a next-quote-token pair must be executable via swaps after the quote token update completes +- Swap routing must not change until `completeQuoteTokenUpdate` is called on the base token diff --git a/src/pages/docs/protocol/tips/tip-1002.mdx b/src/pages/docs/protocol/tips/tip-1002.mdx new file mode 100644 index 00000000..034ab91c --- /dev/null +++ b/src/pages/docs/protocol/tips/tip-1002.mdx @@ -0,0 +1,83 @@ +--- +id: TIP-1002 +title: Prevent crossed orders and allow same-tick flip orders +description: Changes to the Stablecoin DEX that prevent placing orders that would cross existing orders on the opposite side of the book, and allow flip orders to flip to the same tick. +authors: Dan Robinson +status: Draft +--- + +# TIP-1002: Prevent crossed orders and allow same-tick flip orders + +## Abstract + +This TIP makes two related changes to the Stablecoin DEX: + +1. **Prevent crossed orders**: Modify `place` and `placeFlip` to reject orders that would cross existing orders on the opposite side of the book. An order "crosses" when a bid is placed at a tick higher than the best ask, or an ask is placed at a tick lower than the best bid. + +2. **Allow same-tick flip orders**: Relax the `placeFlip` validation to allow `flipTick` to equal `tick`, enabling flip orders that flip to the same price. + +## Motivation + +### Preventing crossed orders + +Currently, the Stablecoin DEX allows orders to be placed at any valid tick, even if they would cross existing orders. Since matching only occurs during swaps (not during order placement), crossed orders can accumulate in the order book. This is unusual behavior and could confuse market makers who are accustomed to books that do not allow crossing. + +By preventing crossed orders at placement time, the order book maintains a clean invariant: `best_bid_tick <= best_ask_tick`. + +### Allowing same-tick flip orders + +Currently, `placeFlip` requires `flipTick` to be strictly on the opposite side of `tick` (e.g., for a bid, `flipTick > tick`). This prevents use cases like instant token convertibility, where an issuer wants to place flip orders on both sides at the same tick to create a stable two-sided market that automatically replenishes when orders are filled. + +--- + +# Specification + +## Modified behavior + +The `place` and `placeFlip` functions (including the `bookKey` overloads from TIP-1001) are modified to check for crossing before accepting an order: + +- **For bids**: Revert if `tick > best_ask_tick` (when `best_ask_tick` exists) +- **For asks**: Revert if `tick < best_bid_tick` (when `best_bid_tick` exists) + +### Same-tick orders + +Orders at the same tick as the best order on the opposite side are **allowed**. This means: + +- A bid at `tick == best_ask_tick` is allowed +- An ask at `tick == best_bid_tick` is allowed + +While this is non-standard behavior for most order books (which would immediately match same-tick orders), it is intentionally permitted to support flip orders that flip to the same tick (see below). + +## Same-tick flip orders + +The `placeFlip` validation is relaxed to allow `flipTick == tick`: + +- **Current behavior**: For bids, `flipTick > tick` required; for asks, `flipTick < tick` required +- **New behavior**: For bids, `flipTick >= tick` required; for asks, `flipTick <= tick` required + +This enables use cases like instant token convertibility, where an issuer places flip orders on both sides at the same tick to create a stable two-sided market that automatically replenishes when orders are filled. + +## Interaction with TIP-1001 + +If TIP-1001 is accepted, the crossing check only applies when the pair is **active**—that is, when the pair's quote token equals the base token's current `quoteToken()`. + +For pairs created via `createNextPair` (where the quote token is the base token's `nextQuoteToken()`), the crossing check is skipped. This allows orders to accumulate freely during "place-only mode" before the quote token update is finalized. Such orders would likely be arbitraged nearly instantly once the pair launches, but this prevents someone from causing a denial-of-service to one side of the book by placing an extremely aggressive order on the other side. + +## New error + +```solidity +/// @notice The order would cross existing orders on the opposite side +error ORDER_WOULD_CROSS(); +``` + +## Events + +No new events. + +--- + +# Invariants + +- On active pairs, `best_bid_tick <= best_ask_tick` after any successful `place` or `placeFlip` call +- On inactive pairs (per TIP-1001), no crossing check is enforced +- Flip orders may create orders at the same tick as the opposite side, potentially resulting in `best_bid_tick == best_ask_tick` diff --git a/src/pages/docs/protocol/tips/tip-1003.mdx b/src/pages/docs/protocol/tips/tip-1003.mdx new file mode 100644 index 00000000..801bafe6 --- /dev/null +++ b/src/pages/docs/protocol/tips/tip-1003.mdx @@ -0,0 +1,174 @@ +--- +id: TIP-1003 +title: Client order IDs +description: Addition of client order IDs to the Stablecoin DEX, allowing users to specify their own order identifiers for idempotency and easier order tracking. +authors: Dan Robinson +status: Draft +--- + +# TIP-1003: Client order IDs + +## Abstract + +This TIP adds support for optional client order IDs (`clientOrderId`) to the Stablecoin DEX. Users can specify a `uint128` identifier when placing orders, which serves as an idempotency key and a predictable handle for the order. The system-generated `orderId` is not predictable before transaction execution, making client order IDs useful for order management. + +## Motivation + +Traditional exchanges allow users to specify a client order ID (called `ClOrdID` in FIX protocol, `cloid` in Hyperliquid) for several reasons: + +1. **Idempotency**: If a transaction is submitted twice (e.g., due to network issues), the duplicate can be detected and rejected +2. **Predictable reference**: Users know the order identifier before the transaction confirms, enabling them to prepare cancel requests or track orders without waiting for confirmation +3. **Integration**: External systems can use their own ID schemes to correlate orders + +--- + +# Specification + +## New storage + +A new mapping tracks active client order IDs per user: + +```solidity +mapping(address user => mapping(uint128 clientOrderId => uint128 orderId)) public clientOrderIds; +``` + +## Modified functions + +All order placement functions gain an optional `clientOrderId` parameter: + +```solidity +/// @notice Places an order with an optional client order ID +/// @param token The base token of the pair +/// @param amount The order amount in base tokens +/// @param isBid True for buy orders, false for sell orders +/// @param tick The price tick for the order +/// @param clientOrderId Optional client-specified ID (0 for none) +/// @return orderId The system-assigned order ID +function place( + address token, + uint128 amount, + bool isBid, + int16 tick, + uint128 clientOrderId +) external returns (uint128 orderId); + +/// @notice Places an order on a specific pair with an optional client order ID +/// @dev Overload from TIP-1001 +function place( + bytes32 bookKey, + address token, + uint128 amount, + bool isBid, + int16 tick, + uint128 clientOrderId +) external returns (uint128 orderId); + +/// @notice Places a flip order with an optional client order ID +function placeFlip( + address token, + uint128 amount, + bool isBid, + int16 tick, + int16 flipTick, + bool internalBalanceOnly, + uint128 clientOrderId +) external returns (uint128 orderId); + +/// @notice Places a flip order on a specific pair with an optional client order ID +/// @dev Overload from TIP-1001 +function placeFlip( + bytes32 bookKey, + address token, + uint128 amount, + bool isBid, + int16 tick, + int16 flipTick, + bool internalBalanceOnly, + uint128 clientOrderId +) external returns (uint128 orderId); +``` + +## New functions + +```solidity +/// @notice Cancels an order by its client order ID +/// @param clientOrderId The client-specified order ID +function cancelByClientOrderId(uint128 clientOrderId) external; + +/// @notice Gets the system order ID for a client order ID +/// @param user The user who placed the order +/// @param clientOrderId The client-specified order ID +/// @return orderId The system-assigned order ID, or 0 if not found +function getOrderByClientOrderId(address user, uint128 clientOrderId) external view returns (uint128 orderId); +``` + +## Behavior + +### Placing orders with clientOrderId + +When `clientOrderId` is non-zero: + +1. Check if `clientOrderIds[msg.sender][clientOrderId]` maps to an active order +2. If it does, revert with `DUPLICATE_CLIENT_ORDER_ID` +3. Otherwise, proceed with order placement and set `clientOrderIds[msg.sender][clientOrderId] = orderId` + +When `clientOrderId` is zero, no client order ID tracking occurs. + +### Uniqueness and reuse + +A `clientOrderId` must be unique among a user's **active orders**. Once an order is filled or cancelled, its `clientOrderId` can be reused. This matches the standard FIX protocol behavior where `ClOrdID` uniqueness is required only for working orders. + +When an order reaches a terminal state (filled or cancelled), the `clientOrderIds` mapping entry is cleared. + +### Flip orders + +When a flip order is filled and creates a new order on the opposite side: + +1. The new (flipped) order inherits the original order's `clientOrderId` +2. The `clientOrderIds` mapping is updated to point to the new order ID +3. This allows users to track their position across flips using a single `clientOrderId` + +If the original order had no `clientOrderId` (was zero), the flipped order also has no `clientOrderId`. + +### Cancellation + +`cancelByClientOrderId(clientOrderId)` looks up `clientOrderIds[msg.sender][clientOrderId]` and cancels that order. It reverts if no active order exists for that `clientOrderId`. + +## New event + +```solidity +/// @notice Emitted when an order is placed (V2 with clientOrderId) +/// @dev Replaces OrderPlaced for new orders +event OrderPlacedV2( + uint128 indexed orderId, + address indexed maker, + address token, + uint128 amount, + bool isBid, + int16 tick, + bool isFlipOrder, + int16 flipTick, + uint128 clientOrderId +); +``` + +`OrderPlacedV2` is identical to `OrderPlaced` but adds the `clientOrderId` field. When an order is placed, only `OrderPlacedV2` is emitted (not both events). + +## New errors + +```solidity +/// @notice The client order ID is already in use by an active order +error DUPLICATE_CLIENT_ORDER_ID(); + +/// @notice No active order found for the given client order ID +error CLIENT_ORDER_ID_NOT_FOUND(); +``` + +--- + +# Invariants + +- A non-zero `clientOrderId` maps to at most one active order per user +- `clientOrderIds[user][clientOrderId]` is cleared when the order is filled or cancelled +- Flip orders inherit `clientOrderId` and update the mapping atomically +- `clientOrderId = 0` is reserved to mean "no client order ID" diff --git a/src/pages/docs/protocol/tips/tip-1004.mdx b/src/pages/docs/protocol/tips/tip-1004.mdx new file mode 100644 index 00000000..638b74de --- /dev/null +++ b/src/pages/docs/protocol/tips/tip-1004.mdx @@ -0,0 +1,228 @@ +--- +id: TIP-1004 +title: Permit for TIP-20 +description: Addition of EIP-2612 permit functionality to TIP-20 tokens, enabling gasless approvals via off-chain signatures. +authors: Dan Robinson +status: Mainnet +related: N/A +protocolVersion: T2 +--- + +# TIP-1004: Permit for TIP-20 + +## Abstract + +TIP-1004 adds EIP-2612 compatible `permit()` functionality to TIP-20 tokens, enabling gasless approvals via off-chain signatures. This allows users to approve token spending without submitting an on-chain transaction, with the approval being executed by any third party who submits the signed permit. + +## Motivation + +The standard ERC-20 approval flow requires users to submit a transaction to approve a spender before that spender can transfer tokens on their behalf. Among other things, this makes it difficult for a transaction to "sweep" tokens from multiple addresses that have never sent a transaction onchain. + +EIP-2612 introduced the `permit()` function which allows approvals to be granted via a signed message rather than an on-chain transaction. This enables: + +- **Gasless approvals**: Users can sign a permit off-chain, and a relayer or the spender can submit the transaction +- **Single-transaction flows**: DApps can batch the permit with the subsequent action (e.g., approve + swap) in one transaction +- **Improved UX**: Users don't need to wait for or pay for a separate approval transaction + +Since TIP-20 aims to be a superset of ERC-20 with additional functionality, adding EIP-2612 permit support ensures TIP-20 tokens work seamlessly with existing DeFi protocols and tooling that expect permit functionality. + +### Alternatives + +While Tempo transactions provide solutions for most of the common problems that are solved by account abstraction, they do not provide a way to transfer tokens from an address that has never sent a transaction onchain, which means it does not provide an easy way for a batched transaction to "sweep" tokens from many addresses. + +While we plan to have Permit2 deployed on the chain, it, too, requires an initial transaction from the address being transferred from. + +Adding a function for `transferWithAuthorization`, which we are also considering, would also solve this problem. But `permit` is somewhat more flexible, and we think these functions are not mutually exclusive. + +--- + +# Specification + +## New functions + +The following functions are added to the TIP-20 interface: + +```solidity +interface ITIP20Permit { + /// @notice Approves `spender` to spend `value` tokens on behalf of `owner` via a signed permit + /// @param owner The address granting the approval + /// @param spender The address being approved to spend tokens + /// @param value The amount of tokens to approve + /// @param deadline Unix timestamp after which the permit is no longer valid + /// @param v The recovery byte of the signature + /// @param r Half of the ECDSA signature pair + /// @param s Half of the ECDSA signature pair + /// @dev The permit is valid only if: + /// - The current block timestamp is <= deadline + /// - The signature is valid and was signed by `owner` + /// - The nonce in the signature matches the current nonce for `owner` + /// Upon successful execution, increments the nonce for `owner` by 1. + /// Emits an {Approval} event. + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external; + + /// @notice Returns the current nonce for an address + /// @param owner The address to query + /// @return The current nonce, which must be included in any permit signature for this owner + /// @dev The nonce starts at 0 and increments by 1 each time a permit is successfully used + function nonces(address owner) external view returns (uint256); + + /// @notice Returns the EIP-712 domain separator for this token + /// @return The domain separator bytes32 value + /// @dev The domain separator is computed dynamically on each call as: + /// keccak256(abi.encode( + /// keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), + /// keccak256(bytes(name())), + /// keccak256(bytes("1")), + /// block.chainid, + /// address(this) + /// )) + /// Dynamic computation ensures correct behavior after chain forks where chainId changes. + function DOMAIN_SEPARATOR() external view returns (bytes32); +} +``` + +## EIP-712 Typed Data + +The permit signature must conform to EIP-712 typed structured data signing. The domain and message types are defined as follows: + +### Domain Separator + +The domain separator is computed using the following parameters: + +| Parameter | Value | +|-----------|-------| +| name | The token's `name()` | +| version | `"1"` | +| chainId | The chain ID where the token is deployed | +| verifyingContract | The TIP-20 token contract address | + +```solidity +bytes32 DOMAIN_SEPARATOR = keccak256(abi.encode( + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), + keccak256(bytes(name())), + keccak256(bytes("1")), + block.chainid, + address(this) +)); +``` + +### Permit Typehash + +The permit message type is: + +```solidity +bytes32 constant PERMIT_TYPEHASH = keccak256( + "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)" +); +``` + +### Signature Construction + +To create a valid permit signature, the signer must sign the following EIP-712 digest: + +```solidity +bytes32 structHash = keccak256(abi.encode( + PERMIT_TYPEHASH, + owner, + spender, + value, + nonces[owner], + deadline +)); + +bytes32 digest = keccak256(abi.encodePacked( + "\x19\x01", + DOMAIN_SEPARATOR, + structHash +)); +``` + +The signature `(v, r, s)` must be produced by signing `digest` with the private key of `owner`. + +## Behavior + +### Nonces + +Each address has an associated nonce that: +- Starts at `0` for all addresses +- Increments by `1` each time a permit is successfully executed for that address +- Must be included in the permit signature to prevent replay attacks + +### Deadline + +The `deadline` parameter is a Unix timestamp. The permit is only valid if `block.timestamp <= deadline`. This allows signers to limit the validity window of their permits. + +### Pause State + +The `permit()` function follows the same pause behavior as `approve()`. Since setting an allowance does not move tokens, `permit()` is allowed to execute even when the token is paused. + +### TIP-403 Transfer Policy + +The `permit()` function does not perform TIP-403 authorization checks, consistent with the behavior of `approve()`. Transfer policy checks are only enforced when tokens are actually transferred. + +### Signature Validation + +The implementation must: +1. Verify that `block.timestamp <= deadline`, otherwise revert with `PermitExpired` +2. Retrieve the current nonce for `owner` and use it to construct the `structHash` and `digest` +3. Increment `nonces[owner]` +4. Validate the signature: + - The `v` parameter must be `27` or `28`. Values of `0` or `1` are **not** normalized and will revert with `InvalidSignature`. Callers using signing libraries that produce `v ∈ {0, 1}` must add `27` before calling `permit`. + - Use `ecrecover` to recover a signer address from the digest + - If `ecrecover` returns a non-zero address that equals `owner`, the signature is valid (EOA case) + - Otherwise, revert with `InvalidSignature` +5. Set `allowance[owner][spender] = value` +6. Emit an `Approval(owner, spender, value)` event + +> **Note**: The nonce is included in the signed digest, so nonce verification is implicit in signature validation — if the wrong nonce was signed, `ecrecover` will return a different address. + +## New errors + +```solidity +/// @notice The permit signature has expired (block.timestamp > deadline) +error PermitExpired(); + +/// @notice The permit signature is invalid (wrong signer, malformed, or zero address recovered) +error InvalidSignature(); +``` + +## New events + +None. Successful permit execution emits the existing `Approval` event from TIP-20. + +--- + +# Invariants + +- `nonces(owner)` must only ever increase, never decrease +- `nonces(owner)` must increment by exactly 1 on each successful `permit()` call for that owner +- A permit signature can only be used once (enforced by nonce increment) +- A permit with a deadline in the past must always revert +- The recovered signer from a valid permit signature must exactly match the `owner` parameter +- After a successful `permit(owner, spender, value, ...)`, `allowance(owner, spender)` must equal `value` +- `DOMAIN_SEPARATOR()` must be computed dynamically and reflect the current `block.chainid` + +## Test Cases + +The test suite must cover: + +1. **Happy path**: Valid permit sets allowance correctly +2. **Expired permit**: Reverts with `PermitExpired` when `deadline < block.timestamp` +3. **Invalid signature**: Reverts with `InvalidSignature` for malformed signatures +4. **Wrong signer**: Reverts with `InvalidSignature` when signature is valid but signer ≠ owner +5. **Replay protection**: Second use of same signature reverts (nonce already incremented) +6. **Nonce tracking**: Verify nonce increments correctly after each permit +7. **Zero address recovery**: Reverts with `InvalidSignature` if ecrecover returns zero address +8. **Pause state**: Permit works when token is paused +9. **Domain separator**: Verify correct EIP-712 domain separator computation +10. **Domain separator chain ID**: Verify domain separator changes if chain ID changes +11. **Max allowance**: Permit with `type(uint256).max` value works correctly +12. **Allowance override**: Permit can override existing allowance (including to zero) diff --git a/src/pages/docs/protocol/tips/tip-1005.mdx b/src/pages/docs/protocol/tips/tip-1005.mdx new file mode 100644 index 00000000..d10492d6 --- /dev/null +++ b/src/pages/docs/protocol/tips/tip-1005.mdx @@ -0,0 +1,118 @@ +--- +id: TIP-1005 +title: Fix ask swap rounding loss +description: A fix for a rounding bug in the Stablecoin DEX where partial fills on ask orders can cause small amounts of quote tokens to be lost. +authors: Dan Robinson +status: Draft +--- + +# TIP-1005: Fix ask swap rounding loss + +## Abstract + +This TIP fixes a rounding bug in the `swapExactAmountIn` function when filling ask orders. Due to double-rounding, the maker can receive slightly less quote tokens than the taker paid, causing tokens to be lost. + +## Motivation + +When a taker swaps quote tokens for base tokens against an ask order, the following calculation occurs: + +1. Convert taker's `amountIn` (quote) to base: `base_out = floor(amountIn / price)` +2. Credit maker with quote: `makerReceives = ceil(base_out * price)` + +Due to the floor in step 1, `makerReceives` can be less than `amountIn`. For example: + +- Taker pays `amountIn = 102001` quote at price 1.02 (tick 2000) +- `base_out = floor(102001 / 1.02) = 100000` +- `makerReceives = ceil(100000 * 1.02) = 102000` +- **1 token is lost** + +This violates the zero-sum invariant: the taker pays more than the maker receives. It also means there is no canonical amount swapped—the trade for the maker is different from the trade for the taker. + +--- + +# Specification + +## Bug location + +The bug is in `_fillOrdersExactIn` when processing ask orders (the `baseForQuote = false` path). Specifically, when a partial fill occurs: + +1. `fillAmount` (base) is calculated by rounding down: `baseOut = (remainingIn * PRICE_SCALE) / price` +2. `_fillOrder` is called with `fillAmount` +3. Inside `_fillOrder`, the maker's quote credit is re-derived: `quoteAmount = ceil(fillAmount * price)` + +The re-derivation in step 3 loses the original `remainingIn` information. + +## Fix + +For partial fills in the ask path, pass the actual `remainingIn` (quote) to `_fillOrder` and use it directly for the maker's credit, rather than re-deriving it from `fillAmount`. + +The fix requires: + +1. Modify `_fillOrder` to accept an optional `quoteOverride` parameter for ask orders +2. In `_fillOrdersExactIn`, when partially filling an ask, pass `remainingIn` as the quote override +3. When `quoteOverride` is provided, use it directly for the maker's balance increment instead of computing `ceil(fillAmount * price)` + +## Reference implementation changes + +The fix requires changes to two functions in [`docs/specs/src/StablecoinDEX.sol`](https://github.com/tempoxyz/tempo/blob/main/docs/specs/src/StablecoinDEX.sol): + +### 1. `_fillOrder` ([line 551-556](https://github.com/tempoxyz/tempo/blob/main/docs/specs/src/StablecoinDEX.sol#L551-L556)) + +Add an optional `quoteOverride` parameter. When non-zero and the order is an ask, use `quoteOverride` directly for the maker's balance increment instead of computing `ceil(fillAmount * price)`. + +```solidity +// Before: +uint128 quoteAmount = + uint128((uint256(fillAmount) * uint256(price) + PRICE_SCALE - 1) / PRICE_SCALE); +balances[order.maker][book.quote] += quoteAmount; + +// After: +uint128 quoteAmount = quoteOverride > 0 + ? quoteOverride + : uint128((uint256(fillAmount) * uint256(price) + PRICE_SCALE - 1) / PRICE_SCALE); +balances[order.maker][book.quote] += quoteAmount; +``` + +### 2. `_fillOrdersExactIn` ([line 923-926](https://github.com/tempoxyz/tempo/blob/main/docs/specs/src/StablecoinDEX.sol#L923-L926)) + +In the partial fill branch for asks, pass `remainingIn` as the quote override: + +```solidity +// Before: +orderId = _fillOrder(orderId, fillAmount); + +// After (for partial fills where fillAmount == baseOut): +orderId = _fillOrder(orderId, fillAmount, remainingIn); +``` + +## Affected code paths + +- `_fillOrdersExactIn` with `baseForQuote = false` (ask path), partial fill case only +- Full fills are not affected because the quote amount is derived from `order.remaining`, not `remainingIn` +- Bid swaps are not affected because the taker pays base tokens directly + +## Example: Before and after + +**Before (buggy):** +``` +amountIn = 102001 quote +base_out = floor(102001 / 1.02) = 100000 +makerReceives = ceil(100000 * 1.02) = 102000 +Lost: 1 token +``` + +**After (fixed):** +``` +amountIn = 102001 quote +base_out = floor(102001 / 1.02) = 100000 +makerReceives = 102001 (passed directly) +Lost: 0 tokens +``` + +--- + +# Invariants + +- Zero-sum: for any swap, `takerPaid == makerReceived` (within the same token) +- Taker receives `floor(amountIn / price)` base tokens (rounds in favor of protocol) +- Maker receives exactly what taker paid in quote tokens diff --git a/src/pages/docs/protocol/tips/tip-1006.mdx b/src/pages/docs/protocol/tips/tip-1006.mdx new file mode 100644 index 00000000..893f8ad9 --- /dev/null +++ b/src/pages/docs/protocol/tips/tip-1006.mdx @@ -0,0 +1,117 @@ +--- +id: TIP-1006 +title: Burn At for TIP-20 Tokens +description: The burnAt function for TIP-20 tokens, enabling authorized administrators to burn tokens from any address. +authors: Dan Robinson +status: In Review +related: N/A +protocolVersion: TBD +--- + +# TIP-1006: Burn At for TIP-20 Tokens + +## Abstract + +This specification introduces a `burnAt` function to TIP-20 tokens, allowing holders of a new `BURN_AT_ROLE` to burn tokens from any address without transfer policy restrictions. This complements the existing `burnBlocked` function which is limited to burning from addresses blocked by the transfer policy. + +## Motivation + +The existing TIP-20 burn mechanisms have the following limitations: + +1. `burn()` - Only burns from the caller's own balance, requires `ISSUER_ROLE` +2. `burnBlocked()` - Can burn from other addresses, but only if the target address is blocked by the transfer policy + +There are legitimate use cases where token administrators may want a privileged caller to have the ability to burn tokens from any address regardless of their policy status, such as allowing a bridge contract to burn tokens that are being bridged out without requiring approval (as in the `crosschainBurn` function proposed in [ERC 7802](https://github.com/ethereum/ERCs/blob/master/ERCS/erc-7802.md)). + +The `burnAt` function provides this capability with appropriate access controls via a dedicated role. + +--- + +# Specification + +## New Role + +A new role constant is added to TIP-20: + +```solidity +bytes32 public constant BURN_AT_ROLE = keccak256("BURN_AT_ROLE"); +``` + +This role is administered by the `DEFAULT_ADMIN_ROLE` (same as other TIP-20 roles). + +## New Event + +```solidity +/// @notice Emitted when tokens are burned from any account. +/// @param from The address from which tokens were burned. +/// @param amount The amount of tokens burned. +event BurnAt(address indexed from, uint256 amount); +``` + +## New Function + +```solidity +/// @notice Burns tokens from any account. +/// @dev Requires BURN_AT_ROLE. Cannot burn from protected precompile addresses. +/// @param from The address to burn tokens from. +/// @param amount The amount of tokens to burn. +function burnAt(address from, uint256 amount) external; +``` + +### Behavior + +1. **Access Control**: Reverts with `Unauthorized` if caller does not have `BURN_AT_ROLE` +2. **Protected Addresses**: Reverts with `ProtectedAddress` if `from` is: + - `TIP_FEE_MANAGER_ADDRESS` (0xfeEC000000000000000000000000000000000000) + - `STABLECOIN_DEX_ADDRESS` (0xDEc0000000000000000000000000000000000000) +3. **Balance Check**: Reverts with `InsufficientBalance` if `from` has insufficient balance +4. **No Policy Check**: Unlike `burnBlocked`, this function does NOT check transfer policy authorization +5. **State Changes**: + - Decrements `balanceOf[from]` by `amount` + - Decrements `_totalSupply` by `amount` + - Updates reward accounting if `from` is opted into rewards +6. **Events**: Emits `Transfer(from, address(0), amount)` and `BurnAt(from, amount)` + +### Interface Addition + +The `ITIP20` interface is extended with: + +```solidity +/// @notice Returns the role identifier for burning tokens from any account. +/// @return The burn-at role identifier. +function BURN_AT_ROLE() external view returns (bytes32); + +/// @notice Burns tokens from any account. +/// @param from The address to burn tokens from. +/// @param amount The amount of tokens to burn. +function burnAt(address from, uint256 amount) external; +``` + +# Invariants + +1. **Role Required**: `burnAt` must always revert if caller lacks `BURN_AT_ROLE` +2. **Protected Addresses**: `burnAt` must never succeed when `from` is a protected precompile address +3. **Supply Conservation**: After `burnAt(from, amount)`: + - `totalSupply` decreases by exactly `amount` + - `balanceOf[from]` decreases by exactly `amount` +4. **Balance Constraint**: `burnAt` must revert if `amount > balanceOf[from]` +5. **Reward Accounting**: If `from` is opted into rewards: + - Pending rewards must be accrued to the reward recipient's `rewardBalance` before the balance changes + - `from`'s `rewardPerToken` snapshot must be synced to `globalRewardPerToken` + - `optedInSupply` must decrease by `amount` + - Any previously accrued `rewardBalance` remains claimable +6. **Policy Independence**: `burnAt` must succeed regardless of transfer policy status of `from` + +## Test Cases + +The test suite must verify: + +1. Successful burn with `BURN_AT_ROLE` +2. Revert without `BURN_AT_ROLE` (Unauthorized) +3. Revert when burning from `TIP_FEE_MANAGER_ADDRESS` (ProtectedAddress) +4. Revert when burning from `STABLECOIN_DEX_ADDRESS` (ProtectedAddress) +5. Successful burn from policy-blocked address (same behavior as `burnBlocked`) +6. Successful burn from policy-authorized address (differs from `burnBlocked`, which reverts) +7. Revert on insufficient balance +8. Correct event emissions (`Transfer` and `BurnAt`) +9. Correct reward accounting updates (pending rewards accrued before burn, `rewardBalance` remains claimable) diff --git a/src/pages/docs/protocol/tips/tip-1007.mdx b/src/pages/docs/protocol/tips/tip-1007.mdx new file mode 100644 index 00000000..fc838204 --- /dev/null +++ b/src/pages/docs/protocol/tips/tip-1007.mdx @@ -0,0 +1,134 @@ +--- +id: TIP-1007 +title: Fee Token Introspection +description: Addition of fee token introspection functionality to the FeeManager precompile, enabling smart contracts to query the fee token being used for the current transaction. +authors: Georgios Konstantopoulos +status: In Review +related: N/A +protocolVersion: TBD +--- + +# TIP-1007: Fee Token Introspection + +## Abstract + +TIP-1007 adds a `getFeeToken()` view function to the FeeManager precompile that returns the fee token address being used for the current transaction. This enables smart contracts to introspect which TIP-20 token is paying for gas fees during execution, allowing for dynamic logic based on the fee token choice. + +## Motivation + +Tempo transactions support paying gas fees in any USD-denominated TIP-20 token via the fee token preference system. However, prior to this TIP, there was no way for a smart contract to determine which fee token is being used for the current transaction during execution. + +This capability was requested by a partner. It could be useful for contracts that want to: + +- Adjust their internal logic based on which fee token is being used +- Provide fee token-aware pricing or routing decisions +- Emit events or logs that include the fee token for off-chain indexing +- Implement fee token-specific behavior in cross-chain messaging + +--- + +# Specification + +## New Function + +The following function is added to the `IFeeManager` interface: + +```solidity +interface IFeeManager { + // ... existing functions ... + + /// @notice Returns the fee token being used for the current transaction + /// @return The address of the TIP-20 token paying for gas fees + /// @dev This value is set by the protocol before transaction execution begins. + /// Returns address(0) if no fee token has been set (e.g., in eth_call + /// simulations where the transaction handler does not run). + function getFeeToken() external view returns (address); +} +``` + +## Behavior + +### Fee Token Resolution + +The fee token returned by `getFeeToken()` is the same token that was resolved by the protocol during transaction validation, following the [fee token preference rules](/docs/protocol/fees/spec-fee#fee-token-resolution). + +### Storage + +The fee token is stored in **transient storage** (EIP-1153) within the FeeManager precompile. This means: + +- The value is automatically cleared at the end of each transaction +- No persistent storage writes occur, minimizing gas costs +- The value is consistent across all calls within a transaction (including internal calls and subcalls) + +### Timing + +The fee token is set by the protocol in the `validate_against_state_and_deduct_caller` handler phase, before any user code executes. This ensures the value is available throughout the entire transaction execution. + +### Gas Cost + +Reading the fee token costs the standard warm transient storage read cost (100 gas for TLOAD). This is the cost of calling `getFeeToken()` itself; callers should account for additional gas used by the CALL opcode to invoke the precompile. + +### Edge Cases + +| Scenario | Return Value | +|----------|--------------| +| Normal transaction | The resolved fee token address | +| Free transaction (zero gas price) | The resolved fee token (may still be set) | +| `eth_call` simulation | `address(0)` (no transaction context) | + +The only case where `address(0)` is returned is in simulation contexts (e.g., `eth_call`) where the protocol handler does not execute. + +## Example Usage + +```solidity +import { IFeeManager } from "./interfaces/IFeeManager.sol"; + +contract FeeTokenAware { + IFeeManager constant FEE_MANAGER = IFeeManager(0xfeeC000000000000000000000000000000000000); + address constant PATH_USD = 0x20C0000000000000000000000000000000000000; + + function doSomething() external { + address feeToken = FEE_MANAGER.getFeeToken(); + + if (feeToken == PATH_USD) { + // User is paying fees in pathUSD + } else if (feeToken != address(0)) { + // User is paying fees in a different USD stablecoin + } else { + // No fee token context (e.g., eth_call simulation) + } + } +} +``` + +## Interface Addition + +The following function is added to `IFeeManager`: + +```solidity +/// @notice Returns the fee token being used for the current transaction +/// @return The address of the TIP-20 token paying for gas fees +function getFeeToken() external view returns (address); +``` + +--- + +# Invariants + +- `getFeeToken()` must return a consistent value across all calls within the same transaction +- `getFeeToken()` must return `address(0)` in simulation contexts (e.g., `eth_call`) where no transaction handler runs +- `getFeeToken()` must be callable from `staticcall` contexts without reverting +- The fee token returned must match the token used for actual fee deduction in `collectFeePreTx` and `collectFeePostTx` +- Reading the fee token must not modify any state (view function) + +## Test Cases + +The test suite must cover: + +1. **Basic functionality**: `getFeeToken()` returns the correct fee token address +2. **Zero when unset**: Returns `address(0)` when no fee token is set +3. **Consistency**: Same value returned from nested calls within a transaction +4. **Static call safety**: Works correctly when called via `staticcall` +5. **Transient storage**: Value is cleared between transactions +6. **Different fee tokens**: Works with various TIP-20 fee tokens (pathUSD, USDC, etc.) +7. **Dispatch coverage**: Function selector is correctly dispatched by the precompile diff --git a/src/pages/docs/protocol/tips/tip-1009.mdx b/src/pages/docs/protocol/tips/tip-1009.mdx new file mode 100644 index 00000000..c2260814 --- /dev/null +++ b/src/pages/docs/protocol/tips/tip-1009.mdx @@ -0,0 +1,331 @@ +--- +id: TIP-1009 +title: Expiring Nonces +description: Time-bounded replay protection using transaction hashes instead of sequential nonce management. +authors: Tempo Team +status: Mainnet +related: TIP-20, Transactions +protocolVersion: T1 +--- + +# TIP-1009: Expiring Nonces + +## Abstract + +TIP-1009 introduces expiring nonces, an alternative replay protection mechanism where transactions are valid only within a specified time window. Instead of tracking sequential nonces, the protocol uses transaction hashes with expiry timestamps to prevent replay attacks. This enables use cases like gasless transactions, meta-transactions, and simplified UX where users don't need to manage nonce ordering. + +## Motivation + +Traditional sequential nonces require careful ordering—if transaction N fails or is delayed, all subsequent transactions (N+1, N+2, ...) are blocked. This creates friction for: + +1. **Gasless/Meta-transactions**: Relayers need complex nonce management across multiple users +2. **Parallel submission**: Users cannot submit multiple independent transactions simultaneously +3. **Recovery from failures**: Stuck transactions require explicit cancellation with the same nonce + +Expiring nonces solve these problems by using time-based validity instead of sequence-based ordering. Each transaction is uniquely identified by its hash and is valid only until a specified `validBefore` timestamp. + +--- + +# Specification + +## Nonce Key + +Expiring nonce transactions use a reserved nonce key: + +``` +TEMPO_EXPIRING_NONCE_KEY = uint256.max (2^256 - 1) +``` + +When a Tempo transaction specifies `nonceKey = uint256.max`, the protocol treats it as an expiring nonce transaction. + +## Transaction Fields + +Expiring nonce transactions require: + +| Field | Type | Description | +|-------|------|-------------| +| `nonceKey` | `uint256` | Must be `uint256.max` to indicate expiring nonce mode | +| `nonce` | `uint64` | Must be `0` (unused, validated for consistency) | +| `validBefore` | `uint64` | Unix timestamp (seconds) after which the transaction is invalid | + +## Validity Window + +The `validBefore` timestamp must satisfy: + +``` +now < validBefore <= now + MAX_EXPIRY_SECS +``` + +Where: +- `now` is the current block timestamp +- `MAX_EXPIRY_SECS = 30` seconds + +Transactions with `validBefore` in the past or more than 30 seconds in the future are rejected. + +## Replay Protection + +Replay protection uses a **circular buffer** data structure in the Nonce precompile: + +### Storage Layout + +```solidity +contract Nonce { + // Existing 2D nonce storage + mapping(address => mapping(uint256 => uint64)) public nonces; // slot 0 + + // Expiring nonce storage + mapping(bytes32 => uint64) public expiringNonceSeen; // slot 1: txHash => expiry + mapping(uint32 => bytes32) public expiringNonceRing; // slot 2: circular buffer + uint32 public expiringNonceRingPtr; // slot 3: buffer pointer +} +``` + +### Circular Buffer Design + +The circular buffer has a fixed capacity: + +``` +EXPIRING_NONCE_SET_CAPACITY = 300,000 +``` + +This capacity is sized for 10,000 TPS × 30 seconds = 300,000 transactions, ensuring entries expire before being overwritten. + +### Algorithm + +When processing an expiring nonce transaction: + +1. **Validate expiry window**: Reject if `validBefore <= now` or `validBefore > now + 30` + +2. **Replay check**: Read `expiringNonceSeen[txHash]` + - If entry exists and `expiry > now`, reject as replay + +3. **Get buffer position**: Read `expiringNonceRingPtr`, compute `idx = ptr % CAPACITY` + +4. **Read existing entry**: Read `expiringNonceRing[idx]` to get `oldHash` + +5. **Eviction check** (safety): If `oldHash != 0`: + - Read `expiringNonceSeen[oldHash]` + - If `expiry > now`, reject (buffer full of valid entries) + - Clear `expiringNonceSeen[oldHash] = 0` + +6. **Insert new entry**: + - Write `expiringNonceRing[idx] = txHash` + - Write `expiringNonceSeen[txHash] = validBefore` + +7. **Advance pointer**: Write `expiringNonceRingPtr = ptr + 1` + +### Pseudocode + +```solidity +function checkAndMarkExpiringNonce( + bytes32 txHash, + uint64 validBefore, + uint64 now +) internal { + // 1. Validate expiry window + require(validBefore > now && validBefore <= now + 30, "InvalidExpiry"); + + // 2. Replay check + uint64 seenExpiry = expiringNonceSeen[txHash]; + require(seenExpiry == 0 || seenExpiry <= now, "Replay"); + + // 3-4. Get buffer position and existing entry + uint32 ptr = expiringNonceRingPtr; + uint32 idx = ptr % CAPACITY; + bytes32 oldHash = expiringNonceRing[idx]; + + // 5. Eviction check (safety) + if (oldHash != bytes32(0)) { + uint64 oldExpiry = expiringNonceSeen[oldHash]; + require(oldExpiry == 0 || oldExpiry <= now, "BufferFull"); + expiringNonceSeen[oldHash] = 0; + } + + // 6. Insert new entry + expiringNonceRing[idx] = txHash; + expiringNonceSeen[txHash] = validBefore; + + // 7. Advance pointer + expiringNonceRingPtr = ptr + 1; +} +``` + +## Gas Costs + +The intrinsic gas cost for expiring nonce transactions includes: + +``` +EXPIRING_NONCE_GAS = 2 * COLD_SLOAD_COST + WARM_SLOAD_COST + 3 * WARM_SSTORE_RESET + = 2 * 2100 + 100 + 3 * 2900 + = 13,000 gas +``` + +**Included operations:** +- 2 cold SLOADs: `seen[txHash]`, `ring[idx]` (unique slots per tx) +- 1 warm SLOAD: `seen[oldHash]` (warm because we just read `ring[idx]` which points to it) +- 3 SSTOREs at RESET price: `seen[oldHash]=0`, `ring[idx]`, `seen[txHash]` + +**Excluded operations (amortized):** +- `ring_ptr` SLOAD/SSTORE: Accessed by almost every expiring nonce tx in a block, so amortized cost approaches ~200 gas. May be moved out of EVM storage in the future. + +**Why SSTORE_RESET (2,900) instead of SSTORE_SET (20,000) for `seen[txHash]`:** +- SSTORE_SET cost exists to penalize permanent state growth +- Expiring nonce data is ephemeral: evicted within 30 seconds, fixed-size buffer (300k entries) +- No permanent state growth, so the 20k penalty doesn't apply + +## Transaction Pool Validation + +The transaction pool performs preliminary validation: + +1. Verify `nonceKey == uint256.max` +2. Verify `nonce == 0` +3. Verify `validBefore` is present +4. Verify `validBefore > currentTime` (not expired) +5. Verify `validBefore <= currentTime + MAX_EXPIRY_SECS` (within window) +6. Query `expiringNonceSeen[txHash]` storage slot to check for existing entry + +Transactions failing these checks are rejected before entering the pool. + +## Interaction with Other Features + +### 2D Nonces + +Expiring nonces and 2D nonces are mutually exclusive: +- `nonceKey = 0`: Protocol nonce (standard sequential) +- `nonceKey = 1..uint256.max-1`: 2D nonce keys +- `nonceKey = uint256.max`: Expiring nonce mode + +### Access Keys (Keychain) + +Expiring nonces work with access key signatures. The `validBefore` provides an additional security boundary—even if an access key is compromised, transactions signed with it become invalid after the expiry window. + +### Fee Tokens + +Expiring nonce transactions pay fees in TIP-20 fee tokens like any other Tempo transaction. + +--- + +# Invariants + +## Must Hold + +| ID | Invariant | Description | +|----|-----------|-------------| +| **E1** | No replay | A transaction hash can never be executed twice (changing `validBefore` produces a different hash) | +| **E2** | Expiry enforcement | Transactions with `validBefore <= now` must be rejected | +| **E3** | Window bounds | Transactions with `validBefore > now + MAX_EXPIRY_SECS` must be rejected | +| **E4** | Nonce must be zero | Expiring nonce transactions must have `nonce == 0` | +| **E5** | Valid before required | Expiring nonce transactions must have `validBefore` set | +| **E6** | No nonce mutation | Expiring nonce txs do not increment protocol nonce or any 2D nonce | +| **E7** | Concurrent independence | Multiple expiring nonce txs from same sender can execute in same block | + +## Invariant Tests + +These invariants are tested in the Foundry invariant test suite (`TempoTransactionInvariant.t.sol`): + +| Handler | Tests | Description | +|---------|-------|-------------| +| `handler_expiringNonceBasic` | Basic flow | Execute valid expiring nonce tx | +| `handler_expiringNonceReplay` | E1 | Replay must be rejected | +| `handler_expiringNonceExpired` | E2 | Tx with `validBefore <= now` must be rejected | +| `handler_expiringNonceWindowTooFar` | E3 | Tx with `validBefore > now + 30s` must be rejected | +| `handler_expiringNonceNonZeroNonce` | E4 | Tx with `nonce != 0` must be rejected | +| `handler_expiringNonceMissingValidBefore` | E5 | Tx without `validBefore` must be rejected | +| `handler_expiringNonceNoNonceMutation` | E6 | Protocol and 2D nonces unchanged after execution | +| `handler_expiringNonceConcurrent` | E7 | Multiple concurrent txs from same sender succeed | + +## Test Cases + +1. **Basic flow**: Submit transaction, verify execution, attempt replay (should fail) + +2. **Expiry validation**: + - `validBefore` in past → reject + - `validBefore = now` → reject + - `validBefore = now + 31` → reject + - `validBefore = now + 30` → accept + +3. **Nonce validation**: + - `nonce = 0` → accept + - `nonce > 0` → reject + +4. **Required fields**: + - `validBefore` missing → reject + - `nonceKey != uint256.max` → not expiring nonce (uses 2D nonce rules) + +5. **Post-expiry replay**: Submit tx, wait for expiry, submit same tx with new `validBefore` (should succeed) + +6. **Buffer eviction**: Fill buffer, verify old entries are evicted when expired + +7. **Concurrent transactions**: Submit multiple transactions with same `validBefore`, verify all succeed + +--- + +# Benchmark Results + +Benchmarks were run to measure state savings from expiring nonces compared to 2D nonces. + +## Key Findings + +| Metric | Value | +|--------|-------| +| Per-transaction state savings | ~100 bytes | +| Circular buffer capacity | 300,000 entries | +| Buffer fills at 5k TPS | ~60 seconds | + +## Controlled Benchmark (100k transactions at 5k TPS) + +| Nonce Type | Final DB Size | Transactions | +|------------|---------------|--------------| +| 2D Nonces | 4,342.85 MB | 100,000 | +| Expiring Nonces | 4,332.18 MB | 100,000 | +| **Difference** | **-10.67 MB** | - | + +The ~107 bytes per transaction overhead includes MPT node overhead, MDBX metadata, and RLP encoding. + +## Scaling Projections + +| TPS | Daily Transactions | Daily State Savings | +|-----|-------------------|---------------------| +| 5,000 | 432M | 43.2 GB | +| 10,000 | 864M | 86.4 GB | + +After the circular buffer fills, expiring nonces maintain constant storage while 2D nonces grow by ~100 bytes per transaction. + +--- + +# Open Questions + +## Safety Check for Buffer Eviction + +The current implementation includes a safety check that reads `expiringNonceSeen[oldHash]` before evicting an entry from the ring buffer. This check verifies the entry is actually expired before overwriting. + +**Rationale for keeping the check:** +- Protects against unexpected TPS spikes that could cause the buffer to fill with valid entries +- Defense-in-depth: prevents replay attacks if capacity assumptions are violated +- Cost is only incurred in the rare case when eviction is needed + +**Rationale for removing the check:** +- The buffer is sized (300k entries) to guarantee entries expire before being overwritten at 10k TPS +- Removes 1 SLOAD (2,100 gas) from the critical path +- Simplifies the algorithm + +**Current decision**: Keep the check but exclude it from gas accounting (charged as if it won't trigger in normal operation). + +**Question**: Should this safety check be: +1. Kept with current gas accounting (not charged for the extra SLOAD)? +2. Removed entirely, trusting the capacity sizing? +3. Kept and fully charged (add 2,100 gas to `EXPIRING_NONCE_GAS`)? + +## Buffer Capacity Sizing + +The current capacity of 300,000 assumes: +- Maximum 10,000 TPS sustained +- 30 second expiry window + +**Question**: Should the capacity be configurable per-chain or hardcoded? What happens if TPS requirements increase significantly? + +## Transaction Hash Computation + +The transaction hash used for replay protection must be computed before signature recovery. + +**Question**: Should the spec explicitly define the hash computation (which fields, encoding) or reference the Tempo Transaction spec? diff --git a/src/pages/docs/protocol/tips/tip-1010.mdx b/src/pages/docs/protocol/tips/tip-1010.mdx new file mode 100644 index 00000000..ef34bbbc --- /dev/null +++ b/src/pages/docs/protocol/tips/tip-1010.mdx @@ -0,0 +1,161 @@ +--- +id: TIP-1010 +title: Mainnet Gas Parameters +description: Initial gas parameters for Tempo mainnet launch including base fee pricing, payment lane capacity, and transaction gas limits. +authors: Dankrad Feist @dankrad +status: Mainnet +related: TIP-1000, Payment Lane Specification, Sub block Specification +protocolVersion: T1 +--- + +# TIP-1010: Mainnet Gas Parameters + +## Abstract + +This TIP specifies the initial gas parameters for Tempo mainnet, including base fee pricing, payment lane capacity, and main transaction gas limits. These parameters are calibrated to support Tempo's target of approximately 20,000 TPS for payment transactions while maintaining economically sustainable fee levels. + +## Motivation + +Tempo is designed as a high-throughput blockchain optimized for stablecoin payments. To achieve this, the gas parameters must be carefully calibrated to: + +1. **Enable high throughput**: Support ~20,000 TPS for payment transactions +2. **Maintain low fees**: Target 0.1 cent per standard TIP-20 transfer +3. **Prevent spam**: Ensure fees are high enough to deter abuse +4. **Balance capacity**: Allocate appropriate gas limits between payment lane and general transactions + +The parameters defined in this TIP represent the initial mainnet configuration and may be adjusted through future governance processes. + +--- + +# Specification + +## Base Fee + +**Value**: `2 × 10^10` attodollars (20 billion attodollars per gas) + +**Rationale**: +- A standard TIP-20 transfer costs approximately 50,000 gas +- At this basefee: 50,000 gas × 20 billion attodollars/gas = 10^15 attodollars = 1,000 microdollars = $0.001 +- This targets approximately **0.1 cent (1,000 microdollars) per TIP-20 transfer** + +**Note on units**: Attodollars (10^-18 USD) are the gas price unit. TIP-20 tokens use 6 decimals, so 1 token unit = 1 microdollar (10^-6 USD). Conversion: attodollars / 10^12 = microdollars. + +**Note**: The base fee is fixed per protocol version and does not adjust dynamically based on block utilization. Unlike EIP-1559, there is no in-protocol mechanism that raises or lowers the base fee in response to congestion. Changes to the base fee require a hardfork upgrade. + +## Block Gas Limit + +**Value**: 500,000,000 gas per block (total block gas limit) + +**Rationale**: +- At 50,000 gas per TIP-20 transfer: `500,000,000 / 50,000 = 10,000 transfers per block` +- With 500ms block time: `10,000 × 2 = 20,000 TPS` for payment transactions +- This capacity supports Tempo's target throughput for payment use cases + +**Gas Budget Breakdown**: +- **Total block gas limit**: 500,000,000 gas +- **Shared gas limit** (validator subblocks): 50,000,000 gas (`block_gas_limit / 10`) +- **Non-shared gas limit** (proposer pool): 450,000,000 gas (`block_gas_limit - shared_gas_limit`) +- **General gas limit** (non-payment cap): 30,000,000 gas (see below) + +:::info +**Shared capacity model**: The payment lane is non-dedicated. General and payment transactions selected by the proposer share the non-shared gas budget (450M). General transactions are capped at `general_gas_limit` (30M), guaranteeing that at least 420M gas remains available for proposer payment transactions. The remaining 50M (`shared_gas_limit`) is reserved for validator subblocks as defined in the Sub-block Specification. +::: + +**Constraints**: +- Only transactions qualifying for the payment lane (simple TIP-20 transfers, memos, etc.) may exceed the `general_gas_limit` +- Complex contract interactions use the general gas limit instead + +## Main Transaction Gas Limit + +**Value**: 30,000,000 gas per block (`general_gas_limit`) + +**Rationale**: +- Aligned with the transaction gas cap to ensure maximum-sized contract deployments can be included in a block +- Supports general smart contract interactions beyond simple payments +- Provides capacity for: + - Contract deployments (including max 24KB contracts) + - DEX swaps + - Complex multi-step transactions + - Other non-payment use cases + +:::warning +**Transactions exceeding 16,000,000 gas are not recommended.** The elevated gas limits (30M) exist solely to accommodate maximum-sized contract deployments under TIP-1000 state creation costs. Applications should not rely on transactions consuming more than 16M gas for normal operations. When storage pricing is moved to a separate mechanism (e.g., storage rent or state expiry), the transaction gas cap is expected to return to 16,000,000 gas. +::: + +## Transaction Gas Cap + +**Value**: 30,000,000 gas per transaction + +**Rationale**: +- Increased from the previous 16,000,000 gas limit +- Accommodates deployment of maximum-size contracts (24,576 bytes per EIP-170) under TIP-1000 state creation costs: + - Base transaction cost: 21,000 gas + - Calldata for initcode (up to 49,152 bytes per EIP-3860): ~500,000-800,000 gas + - CREATE base cost (TIP-1000, fixed upfront contract creation cost): 500,000 gas (replaces old 32,000) + - Initcode execution: variable (~3,000 gas minimum) + - Contract code storage (TIP-1000): `24,576 bytes × 1,000 gas/byte = 24,576,000 gas` + - **Total**: ~25,600,000-25,900,000 gas (fits within 30M limit) + +## Gas Schedule Summary + +| Parameter | Value | Purpose | +|-----------|-------|---------| +| Base fee | `2 × 10^10` attodollars | Target 0.1 cent (1,000 microdollars) per TIP-20 transfer | +| Total block gas limit | 500,000,000 gas/block | Total block capacity | +| Non-shared gas limit | 450,000,000 gas/block | Proposer pool transactions | +| Shared gas limit | 50,000,000 gas/block | Validator subblocks (see Sub-block Specification) | +| General gas limit | 30,000,000 gas/block | Cap for non-payment transactions | +| Transaction gas cap | 30,000,000 gas | Allow max-size contract deployment | + +## Economic Analysis + +### Fee Revenue Projections + +At full payment lane utilization: +- 10,000 transfers per block × 1,000 microdollars = 10,000,000 microdollars ($10) per block +- At 2 blocks/second: $20/second +- Daily: ~$1,728,000 in base fees from payment lane alone + +### Cost Per Operation + +| Operation | Gas Cost | USD Cost (at target base fee) | +|-----------|----------|-------------------------------| +| TIP-20 transfer (existing recipient) | 50,000 | $0.001 (0.1 cent / 1,000 microdollars) | +| TIP-20 transfer (new recipient) | 300,000 | $0.006 (0.6 cent / 6,000 microdollars) | +| First transaction from new account | 300,000 | $0.006 (0.6 cent / 6,000 microdollars) | +| Small contract deployment (1KB) | ~1,800,000 | $0.036 (3.6 cents / 36,000 microdollars) | +| Max contract deployment (24,576 bytes) | ~25,900,000 | $0.518 (~52 cents / 518,000 microdollars) | + +--- + +# Invariants + +1. **Base Fee Invariant**: The base fee is fixed at `2 × 10^10` attodollars per protocol version and can only be changed via a hardfork upgrade. At the current base fee, a TIP-20 transfer (50,000 gas) MUST cost approximately 0.1 cent (1,000 microdollars). + +2. **Payment Lane Priority**: Transactions qualifying for the payment lane MUST be able to consume up to the remaining block gas capacity (total gas limit minus gas already consumed by general transactions). + +3. **Shared Gas Pool**: Proposer pool transactions (payment and general) share the non-shared gas budget (450M). General transactions are additionally constrained by `general_gas_limit` (30M). The remaining 50M is reserved for validator subblocks. + +4. **Transaction Gas Cap**: No single transaction MUST be allowed to consume more than the transaction gas cap (30,000,000 gas). + +5. **Block Gas Validity**: A block MUST be invalid if any of the following hold: + - Total gas used by proposer pool transactions (payment + general) exceeds the non-shared gas limit (450M) + - Total gas used by non-payment (general) transactions exceeds the general gas limit (30M) + - Total gas used by validator subblock transactions exceeds the shared gas limit (50M) + +## Implementation Notes + +These parameters are configured at the chainspec level and applied during block validation. Future adjustments may be made through: + +1. Hard fork upgrades (for significant changes) +2. Governance proposals (if on-chain governance is implemented) +3. Emergency response procedures (for critical security issues) + +## Test Cases + +1. **Base fee targeting**: Verify that at equilibrium, TIP-20 transfers cost approximately 0.1 cent (1,000 microdollars) +2. **Payment lane capacity**: Verify that 10,000 TIP-20 transfers can be included in a single block +3. **General gas limit**: Verify that general transactions are correctly bounded by the 30M gas limit +4. **Transaction gas cap**: Verify that transactions exceeding 30M gas are rejected +5. **Contract deployment**: Verify that a 24KB contract can be deployed within the transaction gas cap +6. **Lane separation**: Verify that payment lane and general transactions are independently tracked diff --git a/src/pages/docs/protocol/tips/tip-1011.mdx b/src/pages/docs/protocol/tips/tip-1011.mdx new file mode 100644 index 00000000..0a98020c --- /dev/null +++ b/src/pages/docs/protocol/tips/tip-1011.mdx @@ -0,0 +1,628 @@ +--- +id: TIP-1011 +title: Enhanced Access Key Permissions +description: Extends Access Keys with periodic spending limits, destination/function scoping, and limited calldata recipient scoping. +authors: Tanishk Goyal +status: Testnet +related: Tempo Transaction Spec +protocolVersion: T3 +--- + +# TIP-1011: Enhanced Access Key Permissions + +## Abstract + +This TIP extends Access Keys with three permission features: + +1. **Periodic spending limits** that reset on fixed intervals. +2. **Call scoping** that limits what addresses a key can call and which selectors it can use. +3. **Limited calldata recipient scoping** for token transfer/approval selectors. + + +## Motivation + +Currently Access Keys support per-token limits and expiry, but miss two practical controls. + +### Periodic Spending Limits + +One-time limits cannot express recurring allowances. + +**Use cases:** + +1. Subscription billing (`10 USDC / month`). +2. Payroll schedules (monthly budgeted payouts). +3. Rate-limited agent/API budgets. + +### Call Scoping (Target + Selector Set) + +Users need finer controls than "any call". They want keys like: + +1. "Only call `swap()` and `exactInput()` on DEX X." +2. "Only call gameplay methods on contract Y." +3. "Only perform plain transfers, not token extension methods." +4. "Only vote() on governance contracts." + +**Current workaround**: Deploy a proxy contract that enforces destination/function restrictions, adding gas overhead and complexity. + +### Recipient-Bound Token Calls + +Target + selector scoping still allows an access key to move funds to arbitrary recipients for token methods like `transfer` and `approve`. + +Users need a narrower policy: the key may call transfer/approve selectors, but only when the recipient/spender matches a configured address. + +This TIP intentionally adds a narrow calldata rule (first ABI `address` argument equality) instead of a generic calldata policy language. + +--- + +# Specification + +## Extended Data Structures + +Conventions used in this section: + +1. Protocol/RLP structs are written with Rust-like `Option<...>` notation. +2. Solidity ABI structs are listed separately where ABI cannot directly represent protocol `Option` semantics. + +### TokenLimit + +**Current:** + +```solidity +struct TokenLimit { + address token; + uint256 amount; +} +``` + +**Proposed:** + +```solidity +struct TokenLimit { + address token; + uint256 amount; // One-time cap when period == 0, per-period cap when period > 0 + uint64 period; // Period duration in seconds (0 = one-time limit) +} +``` + +Design note: `period` is specified as an explicit field (instead of packed into `token`) to keep encoding/auditing straightforward and avoid migration risk for existing limit semantics. + +Runtime state is derived and stored by the precompile (not signed): + +```text +TokenLimitState { + remainingInPeriod: uint256, + periodEnd: uint64, +} +``` + +Initialization and persistence: + +1. `TokenLimitState` is initialized when the key is authorized (or a limit is created via root mutation), not lazily at first spend. +2. `period == 0` initializes `remainingInPeriod = limit` and `periodEnd = 0`. +3. `period > 0` initializes `remainingInPeriod = limit` and `periodEnd = authorize_time + period`. +4. For a given `(account,key,token)`, there is exactly one active `TokenLimit`; duplicate token entries in a single authorization MUST be rejected. + +### CallScope + +Call scoping uses explicit vectors in the protocol model: + +```text +CallScope { + target: address, + selector_rules: Vec, // [] => any selector on this target +} +``` + +Solidity ABI representation for precompile methods: + +```solidity +struct CallScope { + address target; + SelectorRule[] selectorRules; +} +``` + +Solidity ABI and protocol semantics match directly: + +1. `selectorRules = []` allows any selector on `target`. +2. `selectorRules = [r1, ...]` allows exactly the listed selectors. +3. To remove a target scope in the Solidity precompile API, callers MUST use `removeAllowedCalls(keyId, target)`. + +`selector_rules` behavior: + +1. `[]`: allow any selector. +2. `[r1, r2, ...]`: allow exactly the listed selector rules. + +In the Solidity precompile API, omitting a target scope blocks that target; `selectorRules = []` does not. + +### SelectorRule + +```text +SelectorRule { + selector: bytes4, + recipients: Vec
, // [] => any recipient, [a1, ...] => only listed recipients +} +``` + +Solidity ABI representation for precompile methods: + +```solidity +struct SelectorRule { + bytes4 selector; + address[] recipients; +} +``` + +Solidity ABI and protocol semantics match directly: + +1. `recipients = []` allows any recipient for that selector. +2. `recipients = [a1, ...]` constrains the selector to that recipient set. + +`SelectorRule.recipients` behavior: + +1. `[]` => no calldata recipient checks for this selector. +2. `[a1, a2, ...]` => enforce `arg0` recipient membership for this selector. +3. Selector rules MUST be unique per target (`selector` appears at most once). + +Supported constrained selectors in this TIP: + +1. `0xa9059cbb` => `transfer(address,uint256)` +2. `0x095ea7b3` => `approve(address,uint256)` +3. `0x95777d59` => `transferWithMemo(address,uint256,bytes32)` + +If a selector rule uses `recipients = [..]`, then: + +1. `target` MUST be a TIP-20 token address. +2. `selector` MUST be one of the constrained selectors above. +3. Otherwise, key authorization MUST be rejected. + +For these selectors, the constrained field is ABI argument `0` (the first `address` argument). + +Selector width is fixed at 4 bytes. + +1. Each `SelectorRule.selector` MUST be exactly 4 bytes. +2. Implementations MUST revert when decoding or accepting any selector whose length is not exactly 4 bytes. +3. Selectorless calls (`calldata.length < 4`) and fallback/receive routing are scope-matchable only for address-only scopes (`selector_rules = []`). They MUST be rejected when explicit selector matching is required. +4. Contracts with non-standard selector parsing are NOT supported. + +Examples: + +1. `{ target: 0x123, selector_rules: [{selector: 0xaabbccdd, recipients: []}, {selector: 0xeeff0011, recipients: []}] }`: allow two selectors on one target. +2. `{ target: 0x123, selector_rules: [] }`: address-only scoping (any calldata shape on `0x123`, including selectorless/fallback-style calls). +3. `allowedCalls = None`: unrestricted key. +4. `allowedCalls = Some([])`: key is authorized but cannot make scoped calls. +5. `{ target: tokenX, selector_rules: [{selector: 0xa9059cbb, recipients: [0xReceiver]}] }`: allow `transfer` only when `to == 0xReceiver`. +6. `{ target: tokenX, selector_rules: [{selector: 0xa9059cbb, recipients: [0xA, 0xB]}] }`: allow `transfer` only when `to` is in `{0xA, 0xB}`. +7. Distinct target scopes are independent: allowing selector `s` on target `A` never allows selector `s` on target `B`. + +### KeyAuthorization + +Existing fields remain, with a trailing optional call-scope field: + +```text +KeyAuthorization { + chain_id: u64, + key_type: SignatureType, + key_id: address, + expiry: Option, + limits: Option>, + allowed_calls: Option>, // New trailing field +} +``` + +## Interface Changes + +### Events + +```solidity +/// @notice Emitted when an access key spends tokens against a spending limit +/// @param account The account whose key was used +/// @param publicKey The public key (address) that initiated the spend +/// @param token The token address being spent +/// @param amount The amount spent in this transaction +/// @param remainingLimit The remaining spending limit after this spend +event AccessKeySpend( + address indexed account, + address indexed publicKey, + address indexed token, + uint256 amount, + uint256 remainingLimit +); +``` + +This event MUST be emitted whenever an access-key transaction deducts from a spending limit (one-time or periodic). + +### IAccountKeychain.sol + +```solidity +/// @notice Authorizes a key with enhanced permissions +/// @param keyId The key identifier (address derived from public key) +/// @param signatureType 0: secp256k1, 1: P256, 2: WebAuthn +/// @param expiry Block timestamp when key expires +/// @param enforceLimits Whether spending limits are enforced for this key +/// @param spendingLimits Token spending limits (may include periodic limits) +/// @param allowAnyCalls Whether the key is unrestricted (`true`) or scoped by `allowedCalls` (`false`) +/// @param allowedCalls Per-target call scopes for this key. +function authorizeKey( + address keyId, + SignatureType signatureType, + uint64 expiry, + bool enforceLimits, + TokenLimit[] calldata spendingLimits, + bool allowAnyCalls, + CallScope[] calldata allowedCalls +) external; + +/// @notice Creates or replaces one target scope for a key +/// @dev Root key only. If `target` does not exist, creates a new scope; otherwise replaces it atomically. +/// @dev `scope.selectorRules = []` allows any selector on `scope.target`; it does not block the target. +/// @dev If a selector rule has `recipients`, `target` MUST be TIP-20 and `selector` MUST be transfer/approve (+memo). +/// @dev For each selector rule, `recipients = []` means no recipient restriction. +function setAllowedCalls( + address keyId, + CallScope calldata scope +) external; + +/// @notice Removes one target scope for a key +function removeAllowedCalls(address keyId, address target) external; + +/// @notice Returns whether a key is call-scoped together with its configured call scopes +/// @dev `isScoped = false` means unrestricted. +/// @dev `isScoped = true && calls.length == 0` means scoped deny-all. +function getAllowedCalls( + address account, + address keyId +) external view returns (bool isScoped, CallScope[] memory calls); + +/// @notice Returns remaining limit for a token, accounting for period resets +function getRemainingLimit( + address account, + address keyId, + address token +) external view returns (uint256 remaining, uint64 periodEnd); +``` + +`getAllowedCalls(account, keyId)` semantics: + +1. `isScoped = false, calls = []`: unrestricted key. +2. `isScoped = true, calls = []`: scoped key with no allowed targets. +3. `isScoped = true, calls = [c1, ...]`: scoped key with the listed allowlist. +4. Missing, revoked, or expired access keys return `isScoped = true, calls = []`. + +## Semantic Behavior + +### Periodic Limit Reset Logic + +On each spend attempt for `(account, key, token)`: + +1. Implementations MUST load the configured `TokenLimit` and runtime `TokenLimitState`. +2. If `period == 0`, the limit is one-time and no period rollover is applied. +3. If `period > 0` and `block.timestamp >= periodEnd`, implementations MUST reset `remainingInPeriod` to `limit` and advance `periodEnd` by whole multiples of `period` so that `periodEnd > block.timestamp`. +4. If `amount > remainingInPeriod`, implementations MUST revert `SpendingLimitExceeded()`. +5. Otherwise, implementations MUST decrement `remainingInPeriod` by `amount`. + +`updateSpendingLimit(account,key,token,newLimit)` semantics: + +1. MUST update the configured `limit` for that `(account,key,token)`. +2. MUST set `remainingInPeriod = newLimit`. +3. MUST NOT change `period`. +4. MUST NOT change `periodEnd`. +5. Therefore changing `period` requires re-authorizing the key (or removing and recreating that token limit entry). + +### Call Validation Logic + +Call-scope checks use map lookups keyed by `(account_key, target, selector)` plus optional selector-level recipient allowlists. + +Scoped-call validation is performed in a metered pre-execution phase after transaction validation succeeds but before the first user call executes. It is not a transaction-validity condition. + +If any call fails scoped-call validation, the transaction execution MUST fail atomically before any user call in the batch begins. + + +#### Access-Key Contract Creation Ban + +If a transaction is signed with an access key (`key != Address::ZERO`), contract creation MUST be rejected as an invalid transaction in all configurations. + +This ban applies regardless of: + +1. Whether `allowed_calls` is `None` or `Some(...)`. +2. Whether any target scope has `selector_rules = []` (allow-any-selector). +3. Whether the creation call appears in a batch. + +Only the root key (`key == Address::ZERO`) may submit contract-creation calls; this is not a global create ban. + +#### Single Call Validation + +For each call: + +1. If the transaction uses an access key and the call is contract creation, implementations MUST reject the transaction as invalid before execution. +2. If `allowed_calls = None`, implementations MUST allow the call (subject to the contract-creation ban). +3. If `allowed_calls = Some(...)`, implementations MUST enforce target and selector matching. +4. If no target scope exists for `destination`, implementations MUST fail execution before the first user call begins. +5. If the target scope is `selector_rules = []`, implementations MUST allow the call, including selectorless/fallback-style calldata. +6. If the target scope has explicit selector rules and calldata does not provide at least 4 selector bytes, implementations MUST fail execution before the first user call begins. +7. If the target scope has explicit selector rules, there MUST be a rule for the selector; otherwise implementations MUST fail execution before the first user call begins. +8. If the matched rule has `recipients = []`, implementations MUST allow the call. +9. If the matched rule has `recipients = [a1, ...]`, implementations MUST decode ABI argument `0` as an `address` and require membership in that list. +10. For a selector rule with a non-empty `recipients` list, if calldata is shorter than `4 + 32` bytes, implementations MUST fail execution before the first user call begins. +11. For a selector rule with a non-empty `recipients` list, implementations MUST enforce canonical ABI `address` encoding for argument `0` (upper 12 bytes zero) before membership check; otherwise implementations MUST fail execution before the first user call begins. + +#### Batch Validation + +For AA transactions with multiple calls, each call MUST be validated independently in the metered pre-execution phase before execution of the first user call begins. + +If any call fails scope validation: + +1. The batch MUST fail atomically. +2. No user call in the batch may execute. +3. The failure MUST be reported as an execution failure rather than as an invalid transaction. + +### Root-Controlled Scope Updates + +`setAllowedCalls` MUST be root-key-only and MUST apply create-or-replace semantics per target. + +1. `setAllowedCalls(keyId, [])` MUST revert; an empty scope batch is ambiguous and MUST NOT act as a no-op or mode toggle. +2. `selectorRules = []` sets `selector_rules = None` semantics (any selector allowed on `target`). +3. `removeAllowedCalls(keyId, target)` disables that target scope. +4. Implementations MUST enforce at most one scope per target for each `(account, key)`. +5. Selector rules MUST be unique by `selector` within a target scope. +6. If any rule has `recipients = Some([..])`, `target` MUST be a TIP-20 token address. +7. If any rule has `recipients = Some([..])`, its `selector` MUST be one of: + 1. `0xa9059cbb` (`transfer(address,uint256)`) + 2. `0x095ea7b3` (`approve(address,uint256)`) + 3. `0x95777d59` (`transferWithMemo(address,uint256,bytes32)`) +8. If any rule has `recipients = Some([..])`, each recipient in that list MUST be non-zero. +9. If any rule has `recipients = Some([..])`, recipients in that list MUST be unique. +10. If any selector-rule validity rule is violated, implementations MUST reject the authorization (or revert `setAllowedCalls`). + +Rationale for rule 2 (`removeAllowedCalls` disables a target scope): + +1. This avoids unbounded gas from deletion-time slot iteration. +2. Prior selector rows may remain in state, but the removed target scope no longer participates in matching. + +### Interaction Rules + +1. Keys may mix one-time and periodic token limits. +2. Spending limits and call scopes are independent checks; both must pass. +3. `updateSpendingLimit()` updates limit and `remainingInPeriod`, but does not change `period` or `periodEnd`. +4. `allowed_calls = None` is unrestricted for non-create calls; `Some([])` is scoped mode with no allowed calls. +5. Every scope has an explicit target address, so there is no wildcard-target precedence ambiguity. +6. Per-target updates are create-or-replace and duplicate target scopes are not allowed. +7. Selector-level recipient allowlists are optional and only valid for TIP-20 targets and the constrained selectors above. +8. Selector-level recipient allowlists are checked after selector match and before call execution. +9. This TIP does not introduce generic calldata predicates, offset math, or wildcard argument matching. + +Wallet UX recommendation (non-consensus): + +1. Wallets SHOULD default to scoped keys (non-empty `selectorRules`) and require explicit user opt-in for unrestricted target scopes (`selectorRules = []`). + +## Gas And Complexity Bounds + +This TIP only specifies the additional intrinsic gas delta for call scopes in handler-side `key_authorization` charging. + +Existing key-authorization charging (signature verification, existing-key read, base key write, token-limit writes, and buffer) remains unchanged. + +Per-transaction scoped-call matching for access-key transactions is not charged as intrinsic gas. It is charged by normal metered execution in the scoped-call pre-execution phase described above. + +Definitions: + +1. `SSTORE_SET = sstore_set_without_load_cost`. +2. `S` = number of targets with configured call scope. +3. `K` = total selector rules across all configured targets. +4. `C` = total selector rules with a non-empty `recipients` list. +5. `W` = total recipient entries across all constrained selector rules. +Scoped-call storage writes counted for intrinsic gas: + +1. Restricted-mode marker: `1` slot when `allowed_calls` is `Some(...)`. +2. Each target scope writes `3` slots: target-set length, target-set value, and target-set position. +3. Each selector rule writes `3` slots: selector-set length, selector-set value, and selector-set position. +4. Each recipient-constrained selector writes `1` additional slot for recipient-set length. +5. Each recipient entry writes `2` slots: recipient-set value and recipient-set position. + +```text +gas_key_authorization_new = gas_key_authorization_existing + + SSTORE_SET * scope_slots + +scope_slots = 0 if allowed_calls is None + = 1 if allowed_calls is Some([]) // explicit restricted-mode marker + = 1 + 3*S + 3*K + C + 2*W if allowed_calls is Some(scopes) +``` + +Justification for `1 + 3*S + 3*K + C + 2*W`: `1` stores restricted mode, each target scope materializes as three set writes, each selector rule materializes as three set writes, each constrained selector writes one recipient-set length slot, and each recipient writes two set-membership slots. + +### Rounded Helper Overhead + +Implementations may also charge a small rounded helper overhead for scoped-key authorization bookkeeping that is not captured by raw storage-row counts alone. + +This overhead exists because fresh scope persistence includes additional bookkeeping such as clearing the empty scope tree, maintaining per-layer set metadata, and materializing recipient sets. + +Tempo's T4 implementation rounds this overhead upward using the same scope cardinalities: + +```text +extra_scope_gas = 5_000 + 7_000*S + 7_000*K + 5_000*W +``` + +This rounding is intentional. The design goal is to avoid materially underpricing larger scope trees while keeping pricing simple and predictable; slight overcharging is acceptable. + +Bounds: + +1. Implementations MUST reject any selector rule with a non-empty `recipients` list whose `target` is not a TIP-20 token address. +2. Implementations MUST reject any selector rule with a non-empty `recipients` list and selector outside the fixed constrained-selector set. +3. Implementations MUST reject duplicate selector rules for the same `(target, selector)`. +4. Implementations MUST reject duplicate recipients inside a selector rule. + +No additional flat gas is specified here for precompile methods (`setAllowedCalls`, `getAllowedCalls`, etc.); those are charged by normal EVM metering at execution time. + +## Encoding + +### Signing Format + +Authorization digest format: + +```text +key_auth_digest = keccak256(rlp([ + chain_id, + key_type, + key_id, + expiry?, + limits?, + allowed_calls? +])) + +limits = rlp([token, limit]) if period == 0 + = rlp([token, limit, period]) if period > 0 +``` + +RLP safety note: + +1. Implementations MUST use canonical RLP encoding for all fields. +2. The signed payload is a typed RLP list; distinct field tuples produce distinct canonical encodings (no cross-field preimage ambiguity under canonical RLP). + +### Transaction Authorization RLP + +```text +KeyAuthorization := RLP([ + chain_id: u64, + key_type: u8, + key_id: address, + expiry?: uint64, + limits?: [TokenLimit, ...], + allowed_calls?: [CallScope, ...] +]) + +TokenLimit := RLP([ + token: address, + limit: uint256, + period: uint64 +]) + +// Canonical one-time form omits `period` entirely. +// Omitted `period` decodes to `period = 0`, i.e. a non-periodic one-time spending limit. +TokenLimit(one-time) := RLP([ + token: address, + limit: uint256 +]) + +CallScope := RLP([ + target: address, + selector_rules: [SelectorRule, ...] | [] +]) + +SelectorRule := RLP([ + selector: bytes4, + recipients: [address, ...] | [] +]) +``` + +Optional encoding rules: + +1. Optional scalar fields (`expiry`) use `None => 0x80`. +2. `limits = None` uses `0x80`. +3. Top-level `allowed_calls = None` is canonically omitted on wire. Implementations MUST also accept explicit `0x80` for `allowed_calls = None` as equivalent non-canonical input. +4. Nested scope-list fields (`selector_rules` and `recipients`) are always encoded explicitly. Allow-all uses RLP empty list (`0xc0`). +5. Non-empty list values encode as normal lists. +6. For `TokenLimit`, one-time limits (`period == 0`) canonically use the two-field form. Implementations MUST also accept the explicit three-field form with `period = 0` as equivalent non-canonical input. +7. Each `SelectorRule.selector` MUST decode to exactly 4 bytes; otherwise the authorization MUST be rejected. + +--- + +## Precompile Storage Changes + +Current layout: + +1. `keys[account][keyId] -> AuthorizedKey` +2. `spending_limits[(account,keyId)][token] -> U256` + +Additive periodic-limit layout: + +| Mapping | Type | Description | +|---------|------|-------------| +| `spending_limits[account_key][token]` | `U256` | Remaining amount / `remainingInPeriod` | +| `spending_limit_period_state[account_key][token]` | struct `{ max, period, period_end }` | Periodic limit metadata | + +Call-scope storage is account-scoped and represented as nested scope membership, with a key-level scoped/unrestricted flag: + +| Path | Type | Description | +|------|------|-------------| +| `key_scopes[account_key].is_scoped` | `bool` | Whether the key is unrestricted or uses scoped target membership | +| `key_scopes[account_key].targets` | `Set
` | Scoped target membership | +| `key_scopes[account_key].target_scopes[target].selectors` | `Set` | Explicit selector membership | +| `key_scopes[account_key].target_scopes[target].selector_scopes[selector].recipients` | `Set
` | Selector-level recipient membership | + +Absent target or selector entries represent disabled inner scopes; implementations do not need separate target-level or selector-level mode bits. + +`account_key = keccak256(account || key_id)` to avoid cross-account collisions for shared key IDs. + +Implementations MAY maintain additional internal indexes or equivalent layouts so long as semantics remain unchanged. + +## Hardfork-Gated Features + +The following MUST be fork-gated: + +1. New `TokenLimit` decode/encode behavior. +2. `allowed_calls` decode/encode behavior. +3. `selector_rules` decode/encode behavior. +4. Periodic reset logic. +5. Call-scope validation logic. +6. Selector-rule recipient-allowlist calldata validation logic. +7. New precompile storage writes/reads for periodic + call-scope data. +8. New precompile storage writes/reads for selector-level recipient allowlists. +9. Updated precompile read APIs (`getAllowedCalls(account,key)`, richer `getRemainingLimit`). +10. New mutator function `setAllowedCalls`, which can only be called by root key. +11. Global ban on contract creation when using access keys. +12. Selector-width enforcement (`selector length == 4` only). +13. Constrained-selector allowlist and argument-0 canonical address checks. +14. TIP-20 target verification for selector rules with recipient allowlists. + +Pre-fork blocks MUST replay with pre-fork semantics to preserve state roots. + +--- + +# Invariants + +1. `periodEnd` is monotonic and never set to the past. +2. `remainingInPeriod <= limit` after any operation. +3. Expiry check runs before spending and call-scope checks. +4. If `key != Address::ZERO`, any contract-creation call MUST cause the transaction to be rejected as invalid before execution, regardless of `allowed_calls`. +5. `allowed_calls = None` allows all non-create calls; `allowed_calls = Some(...)` requires target+selector-rule match and otherwise causes the transaction to be rejected as invalid before execution. +6. In scoped mode, calldata must contain at least 4 selector bytes only when explicit selector matching is required; address-only scopes allow selectorless/fallback-style calldata. +7. For each `(account, key)`, target scopes are unique, selector rules are unique per target, and recipients are unique per selector rule. +8. `setAllowedCalls(..., scope.selectorRules = [])` allows any selector on that target; `removeAllowedCalls(keyId, target)` disables that target scope. +9. Selector rules with recipient allowlists are valid only for TIP-20 targets and only for the fixed constrained selector set. +10. For recipient-allowlisted rules, calldata argument `0` must be a canonically encoded ABI address and must be in the configured recipient set. +11. In the Solidity ABI, `selectorRules[i].recipients = []` means that selector has no recipient restriction. +## Test Cases + +1. Periodic reset after elapsed period. +2. No rollover of unused periodic allowance. +3. Address + multi-selector scope allow. +4. Address-only allow (`selector_rules=[]`). +5. Deny when no scope matches. +6. `allowed_calls=None` allows all non-create calls. +7. `allowed_calls=Some([])` denies all calls. +8. Mixed one-time and periodic token limits. +9. Existing keys continue to function after the fork. +10. Batch validation rejects the transaction before execution when any call is invalid. +11. Shared key IDs across accounts cannot overwrite each other’s scopes. +12. Reject calls that do not provide at least 4 selector bytes when explicit selector matching is required. +15. `setAllowedCalls(..., scope.selectorRules = [])` allows any selector on that target. +16. `setAllowedCalls` create-or-replace semantics are enforced. +17. `removeAllowedCalls(keyId, target)` removes that target scope; if no target scopes remain, the key stays scoped but matches no calls. +18. Address-only scopes allow selectorless/fallback-style calls to the scoped target. +19. Access-key transactions with CREATE as first call are rejected. +20. Access-key transactions with any CREATE in a batch are rejected. +21. For constrained TIP-20 selectors (`transfer`, `approve`, `transferWithMemo`), calls succeed iff calldata argument `0` is in the configured recipient set. +22. Single-recipient and multi-recipient selector rules both enforce the same membership rule. +23. Reject the transaction before execution when a selector rule with a recipient allowlist is matched and calldata is shorter than `4 + 32` bytes. +24. Reject the transaction before execution when a selector rule with a recipient allowlist is matched and ABI argument `0` is not canonically encoded as an address. +25. Reject selector rules with recipient allowlists for selectors outside the fixed constrained-selector set. +26. Reject duplicate selector rules for the same `(target, selector)`. +27. Reject duplicate recipients within a selector rule. +28. Reject key authorization when selector rules with recipient allowlists are used on a non-TIP-20 target. + +## References + +- [AccountKeychain docs](https://docs.tempo.xyz/docs/protocol/transactions/AccountKeychain) +- [Tempo Transactions](https://docs.tempo.xyz/docs/guide/tempo-transaction) +- [IAccountKeychain.sol](https://github.com/tempoxyz/tempo/blob/main/tips/verify/src/interfaces/IAccountKeychain.sol) +- [GitHub Issue #1865](https://github.com/tempoxyz/tempo/issues/1865) - Periodic spending limits +- [GitHub Issue #1491](https://github.com/tempoxyz/tempo/issues/1491) - Destination address scoping diff --git a/src/pages/docs/protocol/tips/tip-1015.mdx b/src/pages/docs/protocol/tips/tip-1015.mdx new file mode 100644 index 00000000..b9ee2ba5 --- /dev/null +++ b/src/pages/docs/protocol/tips/tip-1015.mdx @@ -0,0 +1,396 @@ +--- +id: TIP-1015 +title: Compound Transfer Policies +description: Extends TIP-403 with compound policies that specify different authorization rules for senders and recipients. +authors: Dan Robinson +status: Mainnet +related: TIP-403, TIP-20 +protocolVersion: T2 +--- + +# TIP-1015: Compound Transfer Policies + +## Abstract + +This TIP extends the TIP-403 policy registry to support **compound policies** that allow token issuers to specify different authorization rules for senders, recipients, and mint recipients. A compound policy references three simple policies: one for sender authorization, one for recipient authorization, and one for mint recipient authorization. Compound policies are structurally immutable once created — their constituent policy ID references cannot be changed. However, the referenced simple policies themselves remain mutable and can be modified by their respective admins, which will affect the compound policy's effective authorization behavior. + +## Motivation + +The current TIP-403 system applies the same policy to both senders and recipients of a token transfer. However, real-world requirements often differ between sending and receiving: + +- **Vendor credits**: A business may issue credits that can be minted to anyone and spent by holders to a specific vendor, but cannot be transferred peer-to-peer. This requires allowing all addresses as recipients (for minting) while restricting senders to only transfer to the vendor's address. +- **Sender restrictions**: An issuer may want to block sanctioned addresses from sending tokens, while allowing anyone to receive tokens (e.g., for refunds or seizure). +- **Recipient restrictions**: An issuer may require recipients to be KYC-verified, while allowing any holder to send tokens out. +- **Asymmetric compliance**: Different jurisdictions may have different requirements for inflows vs outflows. + +Compound policies enable these use cases while maintaining backward compatibility with existing simple policies. + +--- + +# Specification + +## Policy Types + +TIP-403 currently supports two policy types: `WHITELIST` and `BLACKLIST`. This TIP adds a third type: + +```solidity +enum PolicyType { + WHITELIST, + BLACKLIST, + COMPOUND +} +``` + +## Compound Policy Structure + +A compound policy references three existing simple policies by their policy IDs: + +```solidity +struct CompoundPolicyData { + uint64 senderPolicyId; // Policy checked for transfer senders + uint64 recipientPolicyId; // Policy checked for transfer recipients + uint64 mintRecipientPolicyId; // Policy checked for mint recipients +} +``` + +All three referenced policies MUST be simple policies (WHITELIST or BLACKLIST), not compound policies. This prevents circular references and unbounded recursion. + +## Storage Layout + +Policy data is stored in a unified `PolicyRecord` struct that contains both base policy data and compound policy data: + +```solidity +struct PolicyData { + uint8 policyType; // 0 = WHITELIST, 1 = BLACKLIST, 2 = COMPOUND + address admin; // Policy administrator (zero for compound policies — compound structure is immutable) +} + +struct PolicyRecord { + PolicyData base; // offset 0: base policy data + CompoundPolicyData compound; // offset 1: compound policy data (only used when policyType == COMPOUND) +} +``` + +The TIP403Registry storage layout: + +| Slot | Field | Description | +|------|-------|-------------| +| 0 | `policyIdCounter` | Counter for generating unique policy IDs | +| 1 | `policyRecords` (private) | `mapping(uint64 => PolicyRecord)` - Policy ID to policy record | +| 2 | `policySet` | `mapping(uint64 => mapping(address => bool))` - Whitelist/blacklist membership | + +The `policyRecords` mapping is private (not exposed in the ABI). The existing `policyData(uint64 policyId)` view function provides backwards-compatible access to `PolicyData`. + +For a given policy ID, storage locations are: +- **PolicyData**: `keccak256(policyId, 1)` (offset 0 within PolicyRecord) +- **CompoundPolicyData**: `keccak256(policyId, 1) + 1` (offset 1 within PolicyRecord) + +This unified layout requires only **1 keccak computation + 2 SLOADs** for compound policy authorization, compared to 2 keccak computations with separate mappings. + +## Interface Additions + +The TIP403Registry interface is extended with the following: + +```solidity +interface ITIP403Registry { + // ... existing interface ... + + // ========================================================================= + // Compound Policy Creation + // ========================================================================= + + /// @notice Creates a new compound policy (structurally immutable — references cannot be changed after creation) + /// @param senderPolicyId Policy ID to check for transfer senders + /// @param recipientPolicyId Policy ID to check for transfer recipients + /// @param mintRecipientPolicyId Policy ID to check for mint recipients + /// @return newPolicyId ID of the newly created compound policy + /// @dev All three policy IDs must reference existing simple policies (not compound). + /// Compound policy references are immutable — the constituent policy IDs cannot be changed after creation. + /// Note: the referenced simple policies themselves remain mutable by their admins. + /// Emits CompoundPolicyCreated event. + function createCompoundPolicy( + uint64 senderPolicyId, + uint64 recipientPolicyId, + uint64 mintRecipientPolicyId + ) external returns (uint64 newPolicyId); + + // ========================================================================= + // Sender/Recipient Authorization + // ========================================================================= + + /// @notice Checks if a user is authorized as a sender under the given policy + /// @param policyId Policy ID to check against + /// @param user Address to check + /// @return True if authorized to send, false otherwise + /// @dev For simple policies: equivalent to isAuthorized() + /// For compound policies: checks against the senderPolicyId + function isAuthorizedSender(uint64 policyId, address user) external view returns (bool); + + /// @notice Checks if a user is authorized as a recipient under the given policy + /// @param policyId Policy ID to check against + /// @param user Address to check + /// @return True if authorized to receive, false otherwise + /// @dev For simple policies: equivalent to isAuthorized() + /// For compound policies: checks against the recipientPolicyId + function isAuthorizedRecipient(uint64 policyId, address user) external view returns (bool); + + /// @notice Checks if a user is authorized as a mint recipient under the given policy + /// @param policyId Policy ID to check against + /// @param user Address to check + /// @return True if authorized to receive mints, false otherwise + /// @dev For simple policies: equivalent to isAuthorized() + /// For compound policies: checks against the mintRecipientPolicyId + function isAuthorizedMintRecipient(uint64 policyId, address user) external view returns (bool); + + // ========================================================================= + // Compound Policy Queries + // ========================================================================= + + /// @notice Returns the constituent policy IDs for a compound policy + /// @param policyId ID of the compound policy to query + /// @return senderPolicyId Policy ID for sender checks + /// @return recipientPolicyId Policy ID for recipient checks + /// @return mintRecipientPolicyId Policy ID for mint recipient checks + /// @dev Reverts if policyId is not a compound policy + function compoundPolicyData(uint64 policyId) external view returns ( + uint64 senderPolicyId, + uint64 recipientPolicyId, + uint64 mintRecipientPolicyId + ); + + // ========================================================================= + // Events + // ========================================================================= + + /// @notice Emitted when a new compound policy is created + /// @param policyId ID of the newly created compound policy + /// @param creator Address that created the policy + /// @param senderPolicyId Policy ID for sender checks + /// @param recipientPolicyId Policy ID for recipient checks + /// @param mintRecipientPolicyId Policy ID for mint recipient checks + event CompoundPolicyCreated( + uint64 indexed policyId, + address indexed creator, + uint64 senderPolicyId, + uint64 recipientPolicyId, + uint64 mintRecipientPolicyId + ); + + // ========================================================================= + // Errors + // ========================================================================= + + /// @notice The referenced policy is not a simple policy + error PolicyNotSimple(); + + /// @notice The referenced policy does not exist + error PolicyNotFound(); +} +``` + +## Authorization Logic + +### isAuthorizedSender + +```solidity +function isAuthorizedSender(uint64 policyId, address user) external view returns (bool) { + PolicyRecord storage record = policyRecords[policyId]; + + if (record.base.policyType == PolicyType.COMPOUND) { + return isAuthorized(record.compound.senderPolicyId, user); + } + + // For simple policies, sender authorization equals general authorization + return isAuthorized(policyId, user); +} +``` + +### isAuthorizedRecipient + +```solidity +function isAuthorizedRecipient(uint64 policyId, address user) external view returns (bool) { + PolicyRecord storage record = policyRecords[policyId]; + + if (record.base.policyType == PolicyType.COMPOUND) { + return isAuthorized(record.compound.recipientPolicyId, user); + } + + // For simple policies, recipient authorization equals general authorization + return isAuthorized(policyId, user); +} +``` + +### isAuthorizedMintRecipient + +```solidity +function isAuthorizedMintRecipient(uint64 policyId, address user) external view returns (bool) { + PolicyRecord storage record = policyRecords[policyId]; + + if (record.base.policyType == PolicyType.COMPOUND) { + return isAuthorized(record.compound.mintRecipientPolicyId, user); + } + + // For simple policies, mint recipient authorization equals general authorization + return isAuthorized(policyId, user); +} +``` + +### isAuthorized (updated) + +The existing `isAuthorized` function is updated to check both sender and recipient authorization: + +```solidity +function isAuthorized(uint64 policyId, address user) external view returns (bool) { + return isAuthorizedSender(policyId, user) && isAuthorizedRecipient(policyId, user); +} +``` + +This maintains backward compatibility: for simple policies both functions return the same result, so `isAuthorized` behaves identically to before. For compound policies, `isAuthorized` returns true only if the user is authorized as both sender and recipient. + +## Required Code Changes + +This TIP requires exactly 6 replacements of `isAuthorized` calls: + +### Direct Replacements + +| Location | Current | Replace With | +|----------|---------|--------------| +| TIP-20 `_mint` | `isAuthorized(to)` | `isAuthorizedMintRecipient(to)` | +| TIP-20 `burnBlocked` | `isAuthorized(from)` | `isAuthorizedSender(from)` | +| DEX `cancelStaleOrder` | `isAuthorized(maker)` | `isAuthorizedSender(maker)` | +| Fee payer `can_fee_payer_transfer` | `isAuthorized(fee_payer)` | `isAuthorizedSender(fee_payer)` | + +### Core Authorization Logic + +| Location | Current | Replace With | +|----------|---------|--------------| +| TIP-20 `isTransferAuthorized` | `isAuthorized(from)` | `isAuthorizedSender(from)` | +| TIP-20 `isTransferAuthorized` | `isAuthorized(to)` | `isAuthorizedRecipient(to)` | + +All other call sites use `ensureTransferAuthorized(from, to)` which delegates to `isTransferAuthorized`, so they automatically inherit the correct behavior: + +- **TIP-20**: `transfer`, `transferFrom`, `transferWithMemo`, `systemTransferFrom` +- **TIP-20 Rewards**: `distributeReward`, `setRewardRecipient`, `claimRewards` +- **Stablecoin DEX**: `decrementBalanceOrTransferFrom`, `placeLimitOrder`, `swapExactAmountIn` + +## TIP-20 Integration + +TIP-20 tokens MUST be updated to use the new sender/recipient authorization functions: + +### Transfer Authorization (isTransferAuthorized) + +```solidity +function isTransferAuthorized(address from, address to) internal view returns (bool) { + uint64 policyId = transferPolicyId; + + bool fromAuthorized = TIP403_REGISTRY.isAuthorizedSender(policyId, from); + bool toAuthorized = TIP403_REGISTRY.isAuthorizedRecipient(policyId, to); + + return fromAuthorized && toAuthorized; +} +``` + +### Mint Operations + +Mint operations check the mint recipient policy: + +```solidity +function _mint(address to, uint256 amount) internal { + if (!TIP403_REGISTRY.isAuthorizedMintRecipient(transferPolicyId, to)) { + revert PolicyForbids(); + } + // ... mint logic +} +``` + +### Burn Blocked Operations + +The `burnBlocked` function checks sender authorization to verify the address is blocked: + +```solidity +function burnBlocked(address from, uint256 amount) external { + require(hasRole(BURN_BLOCKED_ROLE, msg.sender)); + + // Only allow burning from addresses blocked from sending + if (TIP403_REGISTRY.isAuthorizedSender(transferPolicyId, from)) { + revert PolicyForbids(); + } + // ... burn logic +} +``` + +## Stablecoin DEX Integration + +### Cancel Stale Order + +The `cancelStaleOrder` function checks sender authorization on the token escrowed by the maker, since if the order is filled, the maker will have to send that token: + +```solidity +function cancelStaleOrder(uint128 orderId) external { + Order order = orders[orderId]; + address token = order.isBid() ? book.quote : book.base; + uint64 policyId = TIP20(token).transferPolicyId(); + + // Order is stale if maker can no longer send the escrowed token + if (TIP403_REGISTRY.isAuthorizedSender(policyId, order.maker())) { + revert OrderNotStale(); + } + + _cancelOrder(order); +} +``` + +## Mutability + +Compound policies are **structurally immutable** once created — their constituent policy ID references cannot be changed, and they have no admin. However, the referenced simple policies remain independently mutable by their respective admins. Modifications to a referenced simple policy's whitelist or blacklist will immediately affect the authorization behavior of any compound policy that references it. + +To change which simple policies a compound policy references, token issuers must: + +1. Create a new compound policy with the desired configuration +2. Update the token's `transferPolicyId` to the new policy + +To modify authorization behavior without changing the compound policy itself, the admin of a referenced simple policy can modify that simple policy's whitelist or blacklist directly. + +## Backward Compatibility + +This TIP is fully backward compatible: + +- Existing simple policies continue to work unchanged +- Tokens using simple policies will see identical behavior (since `isAuthorizedSender` and `isAuthorizedRecipient` return the same result for simple policies) +- The existing `isAuthorized` function continues to work for both simple and compound policies + +--- + +# Invariants + +1. **Simple Policy Constraint**: All three policy IDs in a compound policy MUST reference simple policies (WHITELIST or BLACKLIST). Compound policies cannot reference other compound policies. + +2. **Structural Immutability**: Once created, a compound policy's constituent policy ID references cannot be changed. The compound policy itself has no admin. Note that the referenced simple policies remain mutable by their respective admins. + +3. **Existence Check**: `createCompoundPolicy` MUST revert if any of the referenced policy IDs does not exist. + +4. **Delegation Correctness**: For simple policies, `isAuthorizedSender(p, u)` MUST equal `isAuthorizedRecipient(p, u)` MUST equal `isAuthorizedMintRecipient(p, u)`. + +5. **isAuthorized Equivalence**: `isAuthorized(p, u)` MUST equal `isAuthorizedSender(p, u) && isAuthorizedRecipient(p, u)`. + +6. **Built-in Policy Compatibility**: Compound policies MAY reference built-in policies (0 = always-reject, 1 = always-allow) as any of their constituent policies. + +7. **Non-existent Policy Revert**: All authorization functions (`isAuthorized`, `isAuthorizedSender`, `isAuthorizedRecipient`, `isAuthorizedMintRecipient`) MUST revert with `PolicyNotFound()` when called with a policy ID that does not exist. Built-in policies (0 and 1) always exist and are exempt from this check. + +## Test Cases + +1. **Simple policy equivalence**: Verify that for simple policies, all four authorization functions return the same result. + +2. **Compound policy creation**: Verify that compound policies can be created with valid simple policy references. + +3. **Invalid creation**: Verify that `createCompoundPolicy` reverts when referencing non-existent policies or compound policies. + +4. **Sender/recipient differentiation**: Verify that a compound policy with different sender/recipient policies correctly authorizes asymmetric transfers. + +5. **isAuthorized behavior**: Verify that `isAuthorized` on a compound policy returns `isAuthorizedSender() && isAuthorizedRecipient()`. + +6. **TIP-20 mint**: Verify that mints check `isAuthorizedMintRecipient`, not `isAuthorizedRecipient`. + +7. **TIP-20 burnBlocked**: Verify that burnBlocked checks sender authorization (and allows burning from blocked senders). + +8. **Vendor credits**: Verify that a compound policy with `mintRecipientPolicyId = 1` (always-allow), `senderPolicyId = 1` (always-allow), and `recipientPolicyId = vendor whitelist` allows minting to anyone but only transfers to vendors. diff --git a/src/pages/docs/protocol/tips/tip-1016.mdx b/src/pages/docs/protocol/tips/tip-1016.mdx new file mode 100644 index 00000000..70be37c3 --- /dev/null +++ b/src/pages/docs/protocol/tips/tip-1016.mdx @@ -0,0 +1,351 @@ +--- +id: TIP-1016 +title: Exempt Storage Creation from Gas Limits +description: Storage creation gas costs are charged but don't count against transaction or block gas limits, using a reservoir model aligned with EIP-8037 for correct GAS opcode semantics and EVM compatibility. +authors: Dankrad Feist @dankrad +status: Approved +related: TIP-1000, TIP-1010, EIP-8037, EIP-8011, EIP-7825, EIP-7623 +protocolVersion: T3 +--- + +# TIP-1016: Exempt Storage Creation from Gas Limits + +## Abstract + +Storage creation operations (new state elements, account creation, contract code storage) continue to consume and be charged for gas, this gas does not count against block gas limit but it is capped by max tx gas limit [EIP-7825](https://eips.ethereum.org/EIPS/eip-7825). Gas accounting uses a **reservoir model** (aligned with [EIP-8037](https://eips.ethereum.org/EIPS/eip-8037)) that splits gas into regular and reservoir gas, ensuring the `GAS` opcode accurately reflects the regular execution budget. This allows increasing contract code pricing to 2,500 gas/byte without preventing large contract deployments, and prevents new account creation from reducing effective throughput. + +## Motivation + +TIP-1000 increased storage creation costs to 250,000 gas per operation and 1,000 gas/byte for contract code. This created two problems: + +1. **Contract deployment constraints**: 24KB contracts require ~26M gas, forcing us to: + - Keep transaction gas cap at 30M (would prefer 16M) + - Keep general gas limit at 30M (would prefer lower) + - Limit contract code to 1,000 gas/byte (would prefer 2,500) + +2. **New account throughput penalty**: TIP-20 transfer to new address costs ~300,000 gas total (~70k regular + 230k state) vs ~50,000 gas to existing. At 500M payment lane gas limit: + - Without exemption (single dimension): only ~1,700 new account transfers/block = ~3,400 TPS + - With reservoir model (block limits apply to regular gas only): ~7,150 new account transfers/block = ~14,300 TPS + - Existing account transfers: ~10,000 transfers/block = ~20,000 TPS + - ~4x throughput improvement for new accounts by exempting state gas from block limits + +The root cause: state gas counts against limits designed for execution time constraints. Storage creation is permanent (disk) not ephemeral (CPU), and shouldn't be bounded by per-block execution limits. + +### Why a reservoir model + +Simply exempting state gas from protocol limits without changing EVM internals creates two problems: + +1. **`GAS` opcode inaccuracy**: The `GAS` opcode would return remaining gas from `tx.gas` minus all gas consumed (regular + state), which doesn't reflect the actual regular gas budget. A transaction with a high gas limit that has used 15.9M regular gas with a 16M EIP-7825 per-transaction gas limit would see `GAS` report millions of gas remaining, but OOG after just ~100k more regular gas. + +2. **Broken gas patterns**: Contracts relying on `gasleft()` for loop guards, subcall gas forwarding (63/64 rule), and relay/meta-transaction patterns would see incorrect values, potentially leading to unexpected OOG reverts. + +The reservoir model (from [EIP-8037](https://eips.ethereum.org/EIPS/eip-8037)) solves this by maintaining three internal counters: +* regular `remaining` gas is reflecting execution budget, used by cpu and state creation. Returned by `GAS` opcode. +* `reservoir` is holding overflow can be only be used for state creation +* `state_gas` is tracking cumulative state gas consumed during execution. + +--- + +# Specification + +## Gas Dimensions + +All operations consume gas in two dimensions: + +- **Regular gas** (`regular_gas`): Compute, memory, calldata, and the computational cost of storage operations (writing, hashing). This is the execution-time resource. + +- **State gas** (`state_gas`): The permanent storage burden of state creation operations. This is the long-term state growth resource. + +At the transaction level, the user pays for both. At the block level, only regular gas counts toward block and EIP-7825 max transaction gas limits; state gas is exempt. + +## Storage Gas Operations + +Storage creation operations split their cost between regular gas (computational overhead) and state gas (permanent storage burden): + +| Operation | Execution Gas | Storage Gas | Total | +|-----------|---------------|-------------|-------| +| Cold SSTORE (zero → non-zero) | 22,200 | 230,000 | 252,200 | +| Hot SSTORE (non-zero → non-zero) | 2,900 | 0 | 2,900 | +| Account creation (nonce 0 → 1) | 25,000 | 225,000 | 250,000 | +| Contract code storage (per byte) | 200 | 2,300 | 2,500 | +| Contract creation (fixed upfront cost) | 32,000 | 468,000 | 500,000 | +| EIP-7702 delegation (per auth) | 25,000 | 225,000 | 250,000 | + +For zero-to-non-zero `SSTORE`, Tempo keeps revm's decomposed Berlin accounting: `GAS_WARM_ACCESS` +(100) plus `sstore_set_without_load_cost` (20,000), for a 20,100 regular-gas write path. +When the slot is cold, the existing Berlin cold-slot access charge (`GAS_COLD_SLOAD = 2,100`) is +retained on top of that write component, for a total of 22,200 regular gas before state gas. + +### EIP-7702 Delegation Pricing + +Each EIP-7702 authorization writes a 23-byte delegation designator (`0xef0100 || address`) to the authority account's code field. This is permanent state: redelegation overwrites the account's code pointer but the old code entry persists in the code database. + +The base cost per authorization is **25,000 regular gas + 225,000 state gas = 250,000 total**, matching account creation. This reverts the TIP-1000 reduction to 12,500 gas per authorization. + +For authorizations where `auth.nonce == 0` (new account), the account creation cost (25,000 regular + 225,000 state) applies in addition to the delegation cost, for a total of 500,000 gas. + +### Keychain Authorization Pricing + +Keychain `authorize_key` is charged as intrinsic gas (T1B+). The SSTORE components use the same regular/state split as standard EVM SSTOREs: + +| Component | Regular Gas | State Gas | Notes | +|-----------|-------------|-----------|-------| +| Signature verification | 3,000+ | 0 | ecrecover + P256/WebAuthn if applicable | +| Existing key check (SLOAD) | 2,100 | 0 | Cold SLOAD | +| Key slot write (SSTORE) | 20,000 | 230,000 | Zero-to-non-zero write component only; cold-slot access charged separately | +| Per spending limit (SSTORE × N) | 20,000 × N | 230,000 × N | Zero-to-non-zero write component only per token limit; cold-slot access charged separately | +| Buffer (TSTORE, keccak, event) | 2,000 | 0 | Computational overhead | + +**Total per authorization:** ~27,100 + 20,000 × N regular gas, 230,000 × (1 + N) state gas. + +The table above isolates the write component itself. Any first access to a cold storage slot still +incurs the standard Berlin cold-access charge separately. + +### Precompile and Intrinsic Storage Operations + +The regular/state gas split applies uniformly to all SSTORE and code deposit operations regardless of call site. Precompile storage operations route through the same path as standard EVM SSTOREs and inherit the split automatically. Intrinsic gas charges that include SSTORE costs (e.g. keychain authorization) use the same split. + +**Exception:** Expiring nonce writes (TIP-1009) use `WARM_SSTORE_RESET` (2,900 gas) with zero state gas because they are ephemeral — entries are evicted from a fixed-size circular buffer and do not contribute to permanent state growth. + +**Notes:** +- Regular gas reflects computational cost (writing, hashing) and counts toward protocol limits +- State gas reflects permanent storage burden and does NOT count toward protocol limits +- All gas (regular + state) counts toward user's `gas_limit` and is charged at `base_fee_per_gas` +- All other operations (non-state-creating) are charged entirely as regular gas +- Regular gas is set to at least the pre-TIP-1000 (standard EVM) cost for each operation, ensuring that exempting state gas from limits never makes an operation cheaper against protocol limits than it was before TIP-1000 + +## Transaction Validation + +Before transaction execution, `calculate_intrinsic_cost` returns three values: + +- `intrinsic_regular_gas`: Base transaction cost, calldata, access lists, and other non-state-creating intrinsic costs +- `intrinsic_state_gas`: State gas components of intrinsic cost (e.g., account creation for contract deployment transactions) +- `calldata_floor_gas_cost`: The [EIP-7623](https://eips.ethereum.org/EIPS/eip-7623) calldata floor, defined as `TOTAL_COST_FLOOR_PER_TOKEN * tokens_in_calldata + 21000` + +`validate_transaction` rejects transactions where: + +``` +tx.gas < intrinsic_regular_gas + intrinsic_state_gas +``` + +or where: + +``` +max(intrinsic_regular_gas, calldata_floor_gas_cost) > max_transaction_gas_limit +``` + +The `max` ensures that calldata-heavy transactions cannot pass validation when their floor cost exceeds the per-transaction regular gas limit. The calldata floor is a regular gas concept — it does not interact with `intrinsic_state_gas` or `state_gas_reservoir`. + +`validate_transaction` also returns `intrinsic_regular_gas`, `intrinsic_state_gas`, and `calldata_floor_gas_cost`. + +## Transaction-Level Gas Accounting (Reservoir Model) + +Since transactions have a single gas limit parameter (`tx.gas`), gas accounting is enforced through a **reservoir model**, in which `gas_left` and `state_gas_reservoir` are initialized as follows: + +```python +intrinsic_gas = intrinsic_regular_gas + intrinsic_state_gas +execution_gas = tx.gas - intrinsic_gas +regular_gas_budget = max_transaction_gas_limit - intrinsic_regular_gas +gas_left = min(regular_gas_budget, execution_gas) +state_gas_reservoir = execution_gas - gas_left +``` + +The `state_gas_reservoir` holds gas that exceeds the per-transaction regular gas budget (`max_transaction_gas_limit`, per EIP-7825). The two counters operate as follows: + +- **Regular gas** charges deduct from `gas_left` only. +- **State gas** charges deduct from `state_gas_reservoir` first; when the reservoir is exhausted, from `gas_left`. +- When an opcode requires both regular and state gas, the regular gas charge MUST be applied first. If the regular gas charge triggers an out-of-gas error, the state gas charge is not applied. +- The **`GAS` opcode** returns `gas_left` only (excluding the reservoir). +- The reservoir is passed **in full** to child frames (no 63/64 rule). On child success, the remaining `state_gas_reservoir` is returned to the parent. +- On child **revert** or **exceptional halt**, all state gas consumed by the child, both from the reservoir and any that spilled into `gas_left`, is restored to the parent's reservoir. On child **exceptional halt**, only `gas_left` is consumed (zeroed). State gas is fully preserved on failure because state changes are reverted, so no state was actually grown. + - **Note**: State gas that originally spilled from the reservoir into `gas_left` is restored as reservoir gas, not as `gas_left`. A child frame that performs cold SSTOREs drawing from `gas_left` (because the reservoir was exhausted) and then reverts will return that gas to the parent's reservoir, where it can only be used for future state operations — not for regular execution. This is a known consequence of the EIP-8037 design that avoids tracking the original source of state gas charges per frame. The effect is bounded: it can only convert `gas_left` that was spent on state operations into reservoir gas, and only on child failure paths. +- On **exceptional halt**, remaining `gas_left` is attributed to `execution_regular_gas_used` and set to zero (all regular gas consumed), consistent with existing EVM out-of-gas semantics. The `state_gas_reservoir` is not consumed — it is returned to the parent frame or preserved at the top level, consistent with the principle that state gas pays for long-term state growth which does not occur on failure. +- **System transactions** are not subject to the `max_transaction_gas_limit` cap; their entire `execution_gas` is placed in `gas_left` with `state_gas_reservoir = 0`. + +The two counters are returned by the transaction output. Besides the two counters, the EVM also keeps track of `execution_state_gas_used` and `execution_regular_gas_used` during block execution. `state_gas` costs are added to `execution_state_gas_used` while `regular_gas` costs are added to `execution_regular_gas_used`. These two counters are also returned by the transaction output. + +## Transaction Gas Used + +At the end of transaction execution, the gas used before and after refunds is defined as: + +```python +tx_gas_used_before_refund = tx.gas - tx_output.gas_left - tx_output.state_gas_reservoir +tx_gas_refund = min(tx_gas_used_before_refund // 5, tx_output.refund_counter) +tx_gas_used_after_refund = max( + tx_gas_used_before_refund - tx_gas_refund, + calldata_floor_gas_cost +) +``` + +The refund cap remains at 20% of gas used. The `max` with `calldata_floor_gas_cost` ([EIP-7623](https://eips.ethereum.org/EIPS/eip-7623)) ensures the user always pays at least the calldata floor, even if refunds would bring the total below it. Refunds apply only to user-paid gas; block-level accounting uses `tx_regular_gas` (regular gas only, no refund subtracted) — see [Block-Level Gas Accounting](#block-level-gas-accounting). + +**Note**: EIP-8037 uses `tx_gas_used` in the refund and post-refund formulas, but that variable is not defined in the same code block. TIP-1016 uses `tx_gas_used_before_refund` consistently to avoid ambiguity. + +## Block-Level Gas Accounting + +At block level, only **regular gas** counts toward block gas limits. State gas is exempt — it is not tracked at the block level and does not constrain block capacity. + +```python +tx_regular_gas = intrinsic_regular_gas + tx_output.execution_regular_gas_used + +block_output.block_regular_gas_used += max(tx_regular_gas, calldata_floor_gas_cost) +``` + +The `max` with `calldata_floor_gas_cost` ([EIP-7623](https://eips.ethereum.org/EIPS/eip-7623)) ensures calldata-heavy transactions consume at least the floor cost worth of block capacity. The floor applies to regular gas only — state gas remains fully exempt from block limits. + +Per [EIP-7778](https://eips.ethereum.org/EIPS/eip-7778), `tx_regular_gas` is the pre-refund value: `tx_gas_refund` is **not** subtracted from block accounting. This prevents block gas limit circumvention via refundable operations while preserving user incentives to clean up state. + +The block header `gas_used` field is set to: + +```python +gas_used = block_output.block_regular_gas_used +``` + +The block validity condition uses this value: + +```python +assert gas_used <= block.gas_limit, 'invalid block: too much gas used' +``` + +The base fee update rule uses this same value: + +```python +gas_used_delta = parent.gas_used - parent.gas_target +``` + +**Note**: Tempo has two block limits — general gas limit (~25M) for contracts and payment lane limit (500M) for simple transfers. In both lanes, only regular gas counts toward the limit; state gas is exempt. + +**Divergence from EIP-8037**: EIP-8037 uses a bottleneck model where `gas_used = max(block_regular_gas, block_state_gas)`, effectively capping state gas at the block gas limit. TIP-1016 instead exempts state gas entirely from block limits, relying on fixed high prices (250,000 gas per state element) as the economic deterrent for state growth. + +## SSTORE Refund for Slot Restoration + +When a storage slot is set to a non-zero value and then restored to zero within the same transaction (0→X→0 pattern), the following are refunded via `refund_counter`: + +- State gas: 230,000 (the full state creation charge; EIP-8037 equivalent: `32 × cost_per_state_byte`) +- Regular gas: `GAS_STORAGE_UPDATE - GAS_COLD_SLOAD - GAS_WARM_ACCESS` (EIP-8037 equivalent: 2,800; Tempo: 20,000 − 2,100 − 100 = 17,800) + +The refund mechanism is identical to EIP-8037. The numeric values differ because Tempo uses fixed pricing (see Storage Gas Operations table) rather than EIP-8037's dynamic `cost_per_state_byte`. The net cost after refund is `GAS_WARM_ACCESS` (100), consistent with pre-EIP-8037 `SSTORE` restoration behavior. Refunds use `refund_counter` rather than direct gas accounting decrements, so that reverted frames do not benefit from the refund. + +## Revert Behavior for State Gas + +State gas charged for account creation (`CREATE`, `CALL` to new account, and EOA delegation) is consumed even if the frame reverts — state changes are rolled back but gas is not refunded. This is consistent with pre-EIP-8037 behavior where `GAS_NEW_ACCOUNT` was consumed on revert. + +This is achieved structurally: `GAS_NEW_ACCOUNT` state gas is charged in the **parent frame** before creating the child frame. On child revert, `handle_reservoir_remaining_gas` restores only the child's `state_gas_spent` to the parent's reservoir — the parent's prior charge is preserved. Similarly, `GAS_CREATE` state gas for contract deployment is charged in the parent before the child initcode runs. + +## Receipt Semantics + +Receipt `cumulative_gas_used` tracks the cumulative sum of `tx_gas_used_after_refund` (post-refund, post-floor) across transactions. This means `receipt[i].cumulative_gas_used - receipt[i-1].cumulative_gas_used` equals the gas paid by transaction `i`. + +## Contract Creation Pricing + +Contract code storage cost increases from 1,000 to **2,500 gas/byte** (200 regular + 2,300 state). + +### Contract Deployment Cost Calculation + +When a contract creation transaction or opcode (`CREATE`/`CREATE2`) is executed, gas is charged differently based on whether the deployment succeeds or fails. Given bytecode `B` (length `L`) returned by initcode and `H = keccak256(B)`: + +**When opcode execution starts:** Always charge `GAS_CREATE` (Tempo: 32,000 regular + 468,000 state; EIP-8037: 9,000 regular + `112 × cpsb` state) + +**During initcode execution:** Charge the actual gas consumed by the initcode execution + +**Success path** (no error, not reverted, and `L ≤ MAX_CODE_SIZE`): +- Charge `GAS_CODE_DEPOSIT * L` (200 regular + 2,300 state per byte) and persist `B` under `H`, then link `codeHash` to `H` + +**Failure paths** (REVERT, OOG/invalid during initcode, OOG during code deposit, or `L > MAX_CODE_SIZE`): +- Do NOT charge `GAS_CODE_DEPOSIT * L` +- No code is stored; no `codeHash` is linked to the account +- The account remains unchanged or non-existent + +This is aligned with EIP-8037's deployment flow, where `GAS_CODE_DEPOSIT` is charged only on the success path. + +### Example: 24KB Contract Deployment + +Operation | Regular | State gas +----------|---------|---------- +Contract code | `24,576 × 200 = 4,915,200` | `24,576 × 2,300 = 56,524,800` +Contract fixed upfront | `32,000` | `468,000` +Deployment logic | ~2M | 0 +----------|---------|---------- +**Totals:** | ~7M (counts toward protocol limits via `gas_left`) | ~57M (served from `state_gas_reservoir`, doesn't count toward protocol limits) + +Total gas: ~64M (user must authorize with `gas_limit >= 64M`) + +**Can deploy with protocol max_transaction_gas_limit = 16M** (only ~7M regular gas counts) + +## Examples + +### TIP-20 Transfer to New Address +- Transfer logic: ~50,000 regular gas +- New balance slot: 20,000 regular gas + 230,000 state gas +- **Total**: ~70,000 regular gas + 230,000 state gas = ~300,000 gas +- User must authorize: `gas_limit >= 300,000` +- Counts toward block limit: ~70,000 regular gas +- Reservoir initialization (assuming `max_transaction_gas_limit = 16M`): + - `intrinsic_gas = intrinsic_regular + intrinsic_state ≈ 21,000 + 0 = 21,000` + - `execution_gas = 300,000 - 21,000 = 279,000` + - `regular_gas_budget = 16M - 21,000 ≈ 15,979,000` + - `gas_left = min(15,979,000, 279,000) = 279,000` + - `state_gas_reservoir = 279,000 - 279,000 = 0` + - Since total < `max_transaction_gas_limit`, all gas fits in `gas_left`; state gas draws from `gas_left` +- `GAS` opcode accurately reflects execution budget (~279,000 before execution) +- Block accounting: adds ~70,000 to `block_regular_gas_used` (state gas is exempt from block limits) +- Total cost: ~300,000 gas + +### TIP-20 Transfer to Existing Address +- Transfer logic: ~50,000 regular gas +- Update existing slot: included in transfer logic +- **Total**: ~50,000 regular gas +- User must authorize: `gas_limit >= 50,000` +- Counts toward block limit: ~50,000 regular gas +- Total cost: ~50,000 gas + +### Block Throughput +At 500M payment lane gas limit (only regular gas counts toward block limits): + +- **New account transfers**: ~70k regular gas each → ~7,150 transfers/block ≈ 14,300 TPS +- **Existing account transfers**: ~50k regular gas each → ~10,000 transfers/block ≈ 20,000 TPS +- **Mixed workload**: Only regular gas constrains capacity. A block can contain any mix of new and existing transfers as long as total regular gas ≤ 500M. State gas doesn't reduce block capacity. +- **vs TIP-1000**: ~7,150 new account transfers/block vs ~1,700 without exemption (~4x improvement) + +--- + +# Invariants + +1. **User Authorization**: Total gas used (regular + state) MUST NOT exceed `transaction.gas_limit` (prevents surprise costs) +2. **Protocol Transaction Limit**: Regular gas (via `gas_left`) MUST NOT exceed `max_transaction_gas_limit` (EIP-7825 limit, e.g. 16M) +3. **Protocol Block Limits**: Block `regular_gas` MUST NOT exceed applicable limit: + - General transactions: `general_gas_limit` (25M target, currently 30M) + - Payment lane transactions: `payment_lane_limit` (500M) +4. **State Gas Exemption**: State gas MUST NOT count toward protocol limits (transaction or block). State gas is uncapped at the block level. +5. **Reservoir Model**: Gas accounting MUST use the reservoir model — `gas_left` and `state_gas_reservoir` initialized from `tx.gas`, with state gas drawing from reservoir first +6. **GAS Opcode**: The `GAS` opcode MUST return `gas_left` only (excluding `state_gas_reservoir`) +7. **Reservoir Passing**: The `state_gas_reservoir` MUST be passed in full to child frames (no 63/64 rule). Unused reservoir MUST be returned to parent on child completion +8. **Exceptional Halt**: On exceptional halt, `gas_left` MUST be set to zero; `state_gas_reservoir` MUST be preserved (returned to parent or kept for refund) +9. **Regular Gas Component**: Storage creation operations MUST charge regular gas for computational overhead (writing, hashing) +10. **Total Cost**: Transaction cost MUST equal `(regular_gas + state_gas) × (base_fee_per_gas + priority_fee)` +11. **Gas Split**: Storage creation operations MUST split cost into regular gas (computational) and state gas (permanent burden) +12. **Hot vs Cold**: Hot SSTORE (non-zero → non-zero) has NO state gas component; cold SSTORE (zero → non-zero) has both +13. **Refund via Counter**: SSTORE slot restoration refunds MUST use `refund_counter`, not direct gas decrements +14. **Revert Behavior**: On child revert or exceptional halt, all state gas consumed by the child MUST be restored to the parent's `state_gas_reservoir`, **except** state gas for account creation (`GAS_NEW_ACCOUNT`) which MUST be consumed even on revert +15. **Regular Gas Floor**: The regular gas component of each storage creation operation MUST be at least the pre-TIP-1000 (standard EVM) cost for that operation (SSTORE: 20,000, account creation: 25,000, CREATE base: 32,000, code deposit: 200/byte) +16. **EIP-7702 Delegation**: Each EIP-7702 authorization MUST charge 25,000 regular gas + 225,000 state gas (250,000 total). Authorizations with `auth.nonce == 0` MUST additionally charge the account creation cost (25,000 regular + 225,000 state) +17. **Precompile Consistency**: All precompile storage operations MUST use the same gas accounting path as standard EVM SSTORE, inheriting the regular/state gas split automatically +18. **Keychain Authorization**: Keychain `authorize_key` intrinsic gas MUST split SSTORE costs using the same regular/state ratio as standard EVM SSTOREs (20,000 regular + 230,000 state per new slot) +19. **Calldata Floor (EIP-7623)**: The calldata floor (`TOTAL_COST_FLOOR_PER_TOKEN * tokens_in_calldata + 21000`) MUST apply to regular gas only — it MUST NOT interact with `state_gas_reservoir`. Transaction validation MUST reject when `max(intrinsic_regular_gas, calldata_floor_gas_cost) > max_transaction_gas_limit`. Post-execution `tx_gas_used_after_refund` and block `regular_gas_used` MUST be at least `calldata_floor_gas_cost` + +--- + +# Alignment with EIP-8037 + +This TIP adopts the **reservoir model** from [EIP-8037](https://eips.ethereum.org/EIPS/eip-8037) for transaction-level gas accounting, with the following Tempo-specific differences: + +| Aspect | EIP-8037 | TIP-1016 | +|--------|----------|----------| +| State gas pricing | Dynamic `cost_per_state_byte` scaling with block gas limit | Fixed costs (e.g., 230,000 per slot) — Tempo uses fixed high prices for state growth protection | +| Gas cost harmonization | Harmonizes all state creation to uniform cost-per-byte | Maintains Tempo-specific pricing from TIP-1000 | +| Target state growth | 100 GiB/year dynamic target | Economic deterrence via fixed high costs | +| Block-level gas accounting | Bottleneck model: `max(block_regular_gas, block_state_gas)` | Regular gas only; state gas fully exempt from block limits | +| Block gas limit range | 60M–300M+ (Ethereum L1 scaling) | 25M general + 500M payment lane (Tempo dual-lane) | +| Quantization | Top-5 significant bits with offset for `cost_per_state_byte` | Not applicable (fixed costs) | + +The core EVM mechanism — reservoir model, `GAS` opcode semantics, SSTORE refund/revert behavior, contract deployment flow, and receipt semantics — is shared with EIP-8037, minimizing implementation divergence from upstream. The key divergence is at the block level: TIP-1016 exempts state gas entirely from block limits rather than using EIP-8037's bottleneck model. diff --git a/src/pages/docs/protocol/tips/tip-1017.mdx b/src/pages/docs/protocol/tips/tip-1017.mdx new file mode 100644 index 00000000..126efa5a --- /dev/null +++ b/src/pages/docs/protocol/tips/tip-1017.mdx @@ -0,0 +1,481 @@ +--- +id: TIP-1017 +title: Validator Config V2 precompile +description: Validator Config V2 precompile for improved management of consensus participants +authors: Janis (@superfluffy), Howy (@howydev) +status: Mainnet +protocolVersion: T2 +--- + +# ValidatorConfig V2 + +## Abstract + +TIP-1017 defines ValidatorConfig V2, a new precompile for managing consensus participants. V2 improves lifecycle tracking so validator sets can be reconstructed for any epoch, and adds stricter input validation. It is designed to safely support permissionless validator rotation, and additionally allows separation of fee custody from day-to-day validator operations. + +## Necessary background information + +In Tempo, validator information is stored on-chain. This includes which nodes +make up the current committee, which nodes are intended to join or leave the +committee, and their network information (ingress, egress). + +Each validator is uniquely identified by its ed25519 public key used for signing +all consensus p2p messages. For consensus itself, Tempo employs +bls12381 threshold cryptography, where each validator is assigned a private key +share corresponding to a section of the network public key. The network key itself +is undergoing a re-sharing Distributed Key Generation process every epoch, where +each epoch runs for a fixed number of blocks. The outcome of the DKG process is +written to last block of an epoch. + +The DKG outcome contains the validators that made up the committee in epoch +`E-1` (called dealers), the validators that will make up the committee in `E` +(called players during `E-1`), and the validators that will participate as players +in the DKG process during epoch `E` to become committee members in `E+1`. + +To determine the next players, validators read the contract state at the end +of the epoch and select all entries marked as active. The DKG outcome hence +determines who the committee members *are*, and the contract states who the +committee members *should be*. + +## Motivation + +The original ValidatorConfig precompile (frequently referred to V1 from here on), +was too permissive. It allowed addresses to arbitrarily change the values of +their entry in the contract, potentially breaking consensus. This and other +issues were: + +1. **Key ownership verification**: V1 does not verify that the caller controls + the private key corresponding to the public key being registered. A malicious + validator could hence grief another validator by using their public key, + breaking the consensus requirement that all keys be unique. +2. **Validator re-registration**: V1 allows deleted validators to be re-added + with different parameters, complicating historical queries. +3. **Historical state dependency**: Because V1 contained a warmup epoch for new + validators, and because these were not written to the DKG outcome, to sync + a node always needed to keep up to twice the epoch length of blocks around, + requiring bloated snapshots and preventing aggressive pruning. + +Tempo solved problems 1 and 2 by assigning validators entries anonymous addresses. +Thus, only the contract owner could change or deactivate entries. + +### How V2 solves these problems: + +- ed25519 signature verification proves key ownership at registration time +- fields `addedAtHeight` and `deactivatedAtHeight` are controlled by the contract and + cannot be mutated by the owner and allow historical state reconstruction. +- Public keys remain reserved forever (even after deactivation) +- Addresses are unique among current validators but can be reassigned via `transferValidatorOwnership` + +# Specification + +## Precompile Address +```solidity +address constant VALIDATOR_CONFIG_V2_ADDRESS = 0xCCCCCCCC00000000000000000000000000000001; +``` + +## Interface + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +/// @title IValidatorConfigV2 - Validator Config V2 Precompile Interface +/// @notice Interface for managing consensus validators with append-only, deactivate-once semantics +interface IValidatorConfigV2 { + + /// @notice Caller is not authorized. + error Unauthorized(); + + /// @notice Active validator address already exists. + error AddressAlreadyHasValidator(); + + /// @notice Public key already exists. + error PublicKeyAlreadyExists(); + + /// @notice Validator was not found. + error ValidatorNotFound(); + + /// @notice Validator is already deactivated. + error ValidatorAlreadyDeactivated(); + + /// @notice Public key is invalid. + error InvalidPublicKey(); + + /// @notice Validator address is invalid. + error InvalidValidatorAddress(); + + /// @notice Ed25519 signature verification failed. + error InvalidSignature(); + + /// @notice Contract is not initialized. + error NotInitialized(); + + /// @notice Contract is already initialized. + error AlreadyInitialized(); + + /// @notice Migration is not complete. + error MigrationNotComplete(); + + /// @notice V1 has no validators to migrate. + error EmptyV1ValidatorSet(); + + /// @notice Migration index is out of order. + error InvalidMigrationIndex(); + + /// @notice Address is not in valid `IP:port` format. + /// @param input Invalid input. + /// @param backtrace Additional error context. + error NotIpPort(string input, string backtrace); + + /// @notice Address is not a valid IP address. + /// @param input Invalid input. + /// @param backtrace Additional error context. + error NotIp(string input, string backtrace); + + /// @notice Ingress IP is already in use by an active validator. + /// @param ingress Conflicting ingress address. + error IngressAlreadyExists(string ingress); + + /// @notice Validator information + /// @param publicKey Ed25519 communication public key. + /// @param validatorAddress Validator address. + /// @param ingress Inbound address in `:` format. + /// @param egress Outbound address in `` format. + /// @param index Immutable validators-array position. + /// @param addedAtHeight Block height when entry was added. + /// @param deactivatedAtHeight Block height when entry was deactivated (`0` if active). + /// @param feeRecipient The fee recipient the node will set when proposing blocks as a leader. + struct Validator { + bytes32 publicKey; + address validatorAddress; + string ingress; + string egress; + uint64 index; + uint64 addedAtHeight; + uint64 deactivatedAtHeight; + address feeRecipient; + } + + /// @notice Get active validators. + /// @return validators Active validators (`deactivatedAtHeight == 0`). + function getActiveValidators() external view returns (Validator[] memory validators); + + /// @notice Get contract owner. + /// @return Owner address. + function owner() external view returns (address); + + /// @notice Get total validators, including deactivated entries. + /// @return count Validator count. + function validatorCount() external view returns (uint64); + + /// @notice Get validator by array index. + /// @param index Validators-array index. + /// @return validator Validator at `index`. + function validatorByIndex(uint64 index) external view returns (Validator memory); + + /// @notice Get validator by address. + /// @param validatorAddress Validator address. + /// @return validator Validator for `validatorAddress`. + function validatorByAddress(address validatorAddress) external view returns (Validator memory); + + /// @notice Get validator by public key. + /// @param publicKey Ed25519 public key. + /// @return validator Validator for `publicKey`. + function validatorByPublicKey(bytes32 publicKey) external view returns (Validator memory); + + /// @notice Get next epoch configured for a fresh DKG ceremony. + /// @return epoch Epoch number, or `0` if none is scheduled. + function getNextFullDkgCeremony() external view returns (uint64); + + /// @notice Add a new validator (owner only) + /// @dev Requires Ed25519 signature over a unique digest generated from inputs. + /// @param validatorAddress New validator address. + /// @param publicKey Validator Ed25519 communication public key. + /// @param ingress Inbound address `:`. + /// @param egress Outbound address ``. + /// @param feeRecipient The fee recipient the validator sets when proposing. + /// @param signature Ed25519 signature proving key ownership. + function addValidator( + address validatorAddress, + bytes32 publicKey, + string calldata ingress, + string calldata egress, + address feeRecipient, + bytes calldata signature + ) external returns (uint64); + + /// @notice Deactivate a validator (owner or validator only). + /// @dev Sets `deactivatedAtHeight` to current block height. + /// @param idx Validator index. + function deactivateValidator(uint64 idx) external; + + /// @notice Rotate a validator to a new identity (owner or validator only). + /// @dev Preserves index stability by appending a copy of the existing entry and updating the entry in-place. + /// @param idx Validator index to rotate. + /// @param publicKey New Ed25519 communication public key. + /// @param ingress New inbound address `:`. Must be different from the rotated-out validator (changing port is enough). + /// @param egress New outbound address ``. + /// @param signature Ed25519 signature proving new key ownership. + function rotateValidator( + uint64 idx, + bytes32 publicKey, + string calldata ingress, + string calldata egress, + bytes calldata signature + ) external; + + /// @notice Update validator IP addresses (owner or validator only). + /// @param idx Validator index. + /// @param ingress New inbound address `:`. + /// @param egress New outbound address ``. + function setIpAddresses( + uint64 idx, + string calldata ingress, + string calldata egress + ) external; + + /// @notice Update validator fee recipient (owner or validator only). + /// @param idx Validator index. + /// @param feeRecipient New fee recipient. + function setFeeRecipient( + uint64 idx, + address feeRecipient + ) external; + + /// @notice Transfer validator entry to a new address (owner or validator only). + /// @dev Reverts if `newAddress` conflicts with an active validator. + /// @param idx Validator index. + /// @param newAddress New validator address. + function transferValidatorOwnership(uint64 idx, address newAddress) external; + + /// @notice Transfer contract ownership (owner only). + /// @param newOwner New owner address. + function transferOwnership(address newOwner) external; + + /// @notice Set next fresh DKG ceremony epoch (owner only). + /// @param epoch Epoch where ceremony runs (`epoch + 1` uses new polynomial). + function setNextFullDkgCeremony(uint64 epoch) external; + + /// @notice Migrate one validator by V1 index (owner only). + /// @param idx V1 validator index. + function migrateValidator(uint64 idx) external; + + /// @notice Initialize V2 and enable reads (owner only). + /// @dev Requires all V1 indices to be processed. + function initializeIfMigrated() external; + + /// @notice Check initialization state. + /// @return initialized True if initialized. + function isInitialized() external view returns (bool); + + /// @notice Get initialization block height. + /// @return height Initialization height (`0` if not initialized). + function getInitializedAtHeight() external view returns (uint64); +} +``` + +## Overview + +- Migration incrementally reads and copies validator entries from V1 into V2. +- During migration, the consensus layer continues reading V1 until `initializeIfMigrated()` completes. +- Validator history are append-only, and deactivation is one-way. +- Historical validator sets are reconstructed from `addedAtHeight` and `deactivatedAtHeight`. +- Validator `index` is stable for the lifetime of an entry. +- Writes for post-migration operations are gated by `isInitialized()`. + +## State Model + +V2 stores validators in one append-only array, with lookup indexes by address and public key. + +- `addedAtHeight`: block height where the entry becomes visible to CL epoch filtering. +- `deactivatedAtHeight`: `0` means active; non-zero marks irreversible deactivation. +- `index`: immutable array position assigned at creation. +- `initialized`: one-way migration flag toggled by `initializeIfMigrated()`. + +### Fee Recipient Separation + +Each validator entry includes a `feeRecipient` that can differ from the validator's control address. This enables operators to route protocol fees to a dedicated treasury wallet, while retaining a separate validator or treasury-ops multisig for operational calls. This separation reduces blast radius during key compromise: operational key exposure does not cause historically collected fees held by the custody wallet to be lost. + +## Operation Semantics + +### Lifecycle Operations + +- `addValidator`: appends a new active entry after validation and signature verification. +- `deactivateValidator`: marks an existing active entry as deactivated at current block height. +- `rotateValidator`: to keep `index` stable, this updates the active entry in place and appends the entry to be deactivated. Active validator count is unchanged. + +### Network And Ownership Operations + +- `setIpAddresses`: updates ingress and egress for an active validator, enforcing address format and ingress uniqueness among active entries. +- `setFeeRecipient`: updates the destination address that receives network fees from block proposing. +- `transferValidatorOwnership`: rebinds a validator entry to a new address provided the address is not used by another active entry. + +### Migration And Phase-Gating Operations + +- `migrateValidator`: copies one V1 entry into V2 in descending index order. +- `initializeIfMigrated`: switches V2 to initialized state after all V1 indices have been processed. +- Mutators are phase-gated: migration mutators are blocked after init, and post-init mutators are blocked before init. + +### Input Validation And Safety Checks + +ValidatorConfig V2 enforces the following checks: + +1. Validator ed25519 public keys must be unique across all validators (active and inactive). +2. Validator addresses must be unique across active validators. +3. `ingress` must be a valid `IP:port`, and unique across active validators. +4. `egress` must be a valid IP. +5. `addValidator` and `rotateValidator` require a signature from the Ed25519 key being installed. + +### Ed25519 Signature Verification + +When adding or rotating a validator, the caller must provide an Ed25519 signature proving ownership of the public key. + +**Namespace:** `addValidator` uses `b"TEMPO_VALIDATOR_CONFIG_V2_ADD_VALIDATOR"` and `rotateValidator` uses `b"TEMPO_VALIDATOR_CONFIG_V2_ROTATE_VALIDATOR"`. + +**Messages:** + +``` +addValidatorMessage = keccak256( + bytes8(chainId) // uint64: Prevents cross-chain replay + || contractAddress // address: Prevents cross-contract replay + || validatorAddress // address: Binds to specific validator address + || uint8(ingress.length) // uint8: Length of ingress + || ingress // string: Binds network configuration + || uint8(egress.length) // uint8: Length of egress + || egress // string: Binds network configuration + || feeRecipient // address: Binds fee recipients when proposing. +) + +rotateValidatorMessage = keccak256( + bytes8(chainId) // uint64: Prevents cross-chain replay + || contractAddress // address: Prevents cross-contract replay + || validatorAddress // address: Binds to specific validator address + || uint8(ingress.length) // uint8: Length of ingress + || ingress // string: Binds network configuration + || uint8(egress.length) // uint8: Length of egress + || egress // string: Binds network configuration +) +``` + +The Ed25519 signature is computed over the operation-specific message with the namespace parameter (see commonware's [signing scheme](https://github.com/commonwarexyz/monorepo/blob/abb883b4a8b42b362d4003b510bd644361eb3953/cryptography/src/ed25519/scheme.rs#L38-L40) and [union format](https://github.com/commonwarexyz/monorepo/blob/abb883b4a8b42b362d4003b510bd644361eb3953/utils/src/lib.rs#L166-L174)). + +## Compatibility And Upgrade Behavior + +### Changes From V1 + +1. V2 preserves append-only history with irreversible deactivation instead of mutable active/inactive toggling. +2. V2 enforces stronger input checks in the precompile, including signature-backed key ownership. +3. V2 keeps validator index stable across lifecycle operations. + +### Consensus Layer Read Behavior + +The Consensus Layer checks `v2.isInitialized()` to determine which contract to read: + +- **`initialized == false`**: CL reads from V1. +- **`initialized == true`**: CL reads from V2. + +This read switch is implemented in CL logic. V2 does not proxy reads to V1. + +## Consensus Layer Integration + +**IP address changes**: `setIpAddresses` is expected to take effect in CL peer configuration on the next finalized block. + +**Validator addition and deactivation**: there is no warmup or cooldown in V2. Added validators are added to the DKG player set on the next epoch; deactivated validators leave on the next epoch. +(both in the case of successful DKG rounds; on failure DKG still falls back to its previous state, which might include validators that are marked inactive as per the contract). + +**Fee recipients**: Fee recipients are included now to be used in the future in a not yet determined hardfork. + +### DKG Player Selection + +The consensus layer determines DKG players for epoch `E+1` by reading state at `boundary(E) - 1` and filtering: + +``` +players(E+1) = validators.filter(v => + v.addedAtHeight < boundary(E) && + (v.deactivatedAtHeight == 0 || v.deactivatedAtHeight >= boundary(E)) +) +``` + +This enables node recovery and late joining without historical account state. + +## Migration from V1 + +On networks that start directly with V2 (no V1 state), `initializeIfMigrated` can be called immediately when the V1 validator count is zero. + +Because `SSTORE` cost is high under TIP-1000, migration is done one validator at a time to reduce out-of-gas risk on large sets. + +### Full Migration Steps + +1. At fork activation, the V2 precompile goes live. However, CL continues reading from the V1 precompile. +2. The owner calls `migrateValidator(n-1)` with `n` being the validator count in the V1 precompile. +3. On the first migration call, V2 copies owner from V1 if unset and snapshots the V1 validator count, then continues in descending index order. The snapshotted count is used for all subsequent index validation to prevent V1 mutations from breaking migration ordering. +4. After all indices are processed, owner calls `initializeIfMigrated()`, which flips `initialized` and activates CL reads from V2. + +### Migration Edge Cases + +1. **Validator goes offline during migration**: admin deactivates the validator in V1. If that index has already been migrated the admin will also deactivates it in V2. +2. **Invalid V1 entry encountered**: the index is still marked as processed (migrated, overwritten, or skipped by implementation rules) so global completion is not blocked. +3. **Zero validators in V1**: The migration flow requires at least 1 validator to be in the V1 precompile. +4. **Validator count overflow**: We use uint8s to cache the number of V1 validators and the number of skipped V1 validators. V1 currently has 14 validators, so a limit of 255 is sufficient. + +### Permitted Calls During Migration + +| Contract | Caller | Allowed calls | +| --- | --- | --- | +| V2 (pre-init) | owner | `deactivateValidator`, `migrateValidator`, `initializeIfMigrated` | +| V2 (pre-init) | validator | `deactivateValidator` (theoretically; in Tempo these addresses are unowned) | +| V2 (post-init) | any | `migrateValidator` and `initializeIfMigrated` are blocked | +| V1 (during migration window) | owner and validators | all V1 calls remain available (subject to V1 authorization and key ownership assumptions) | + +# Security + +## Considerations + +- **Migration timing**: migration and `initializeIfMigrated()` should complete before an epoch boundary to avoid DKG disruption. +- **Pre-migration validation**: admins should run a validation script against V1 state to detect entries that would fail V2 checks. +- **State parity before init**: admins should verify V1/V2 state consistency before finalizing with `initializeIfMigrated()`. +- **Signature domain separation**: signatures for `addValidator` and `rotateValidator` are bound to chain ID, precompile address, namespace, validator address, and endpoint payload. + +## Race And Griefing Risks + +- Stable `index` values prevent races between concurrent state-changing calls. +- Append-only history and permissionless rotation require query paths that remain safe as history grows. + +## Testing Requirements + +Unit tests should cover all control-flow branches in added functions, including initialization gating, migration completion checks, and index-based query behavior under large validator sets. + +## Invariants + +### Identity and Uniqueness + +1. **Unique active addresses**: No two active validators share the same `validatorAddress`. Deactivated addresses may be reused. +2. **Unique public keys**: No two validators (including deactivated) share the same `publicKey`. +3. **Ingress uniqueness across active validators**: In `getActiveValidators()`, no two validators share the same ingress `:`. +4. **Valid public keys**: All validators must have valid ed25519 `publicKey`s. + +### Lifecycle and Storage Behavior + +1. **Append-only validator array**: `validatorsArray` length can only increase. +2. **Entry index immutability**: Once a validator entry is created at index `i`, that entry can never move to another index. A previously deactivated operator may later be re-added as a new entry at a different index. +3. **Deactivate-once**: `deactivatedAtHeight` can only transition from 0 to a non-zero value, never back. +4. **Add increases exactly one entry**: A successful `addValidator` call increases `getActiveValidators().length` by exactly one. +5. **Rotation preserves active cardinality**: A successful `rotateValidator` call does not change `getActiveValidators().length`. +6. **Deactivation decrements active cardinality by one**: A successful `deactivateValidator` call decreases `getActiveValidators().length` by exactly one. + +### Query Correctness + +1. **Full-set reconstruction by index**: Reading `validatorByIndex(i)` for all `i` in `0..validatorCount()-1` must reconstruct exactly the ordered validator set. +2. **Validator activity consistency**: Filtering the reconstructed validator set by `deactivatedAtHeight == 0` must produce exactly `getActiveValidators()` (same members, order not important). +3. **Address round-trip for active entries**: For any `i` where `validatorByIndex(i).deactivatedAtHeight == 0`, `validatorByAddress(validatorByIndex(i).validatorAddress).index == i`. +4. **Public-key round-trip for all entries**: For any `i < validatorCount()`, `validatorByPublicKey(validatorByIndex(i).publicKey).index == i`. +5. **Index round-trip for all entries**: For any `i < validatorCount()`, `validatorByIndex(i).index == i`. + +### Migration and Initialization + +1. **Initialization phase gating**: Before initialization, post-init mutators are blocked; after initialization, migration mutators are blocked. +2. **Initialized once**: The `initialized` flag can only transition from `false` to `true`, never back. +3. **Migration completion gate**: Each V1 index must be processed exactly once (migrated or skipped), and `initializeIfMigrated()` stays blocked until all indices are processed. +4. **Skipped-index counter monotonicity**: `migrationSkippedCount` is monotonically non-decreasing and may only change during `migrateValidator`. +5. **DKG continuity at initialization**: On successful `initializeIfMigrated`, `getNextFullDkgCeremony()` in V2 equals the value read from V1 at that moment. +6. **Owner bootstrap during migration**: If V2 owner is unset on first migration call, owner is copied from V1 exactly once and then used for all migration authorization checks. diff --git a/src/pages/docs/protocol/tips/tip-1020.mdx b/src/pages/docs/protocol/tips/tip-1020.mdx new file mode 100644 index 00000000..baa9f5ea --- /dev/null +++ b/src/pages/docs/protocol/tips/tip-1020.mdx @@ -0,0 +1,135 @@ +--- +id: TIP-1020 +title: Signature Verification Precompile +description: A precompile for verifying Tempo signatures onchain. +authors: Jake Moxey (@jxom), Tanishk Goyal (@legion2002), Howy (@howydev) +status: Testnet +related: Tempo Transaction Spec +protocolVersion: T3 +--- + +# TIP-1020: Signature Verification Precompile + +## Abstract + +This TIP introduces a signature verification precompile that enables contracts to verify Tempo signature types (secp256k1, P256, WebAuthn) without relying on custom verifier contracts. + +## Motivation + +Tempo supports multiple signature schemes beyond standard secp256k1. Currently, contracts cannot verify Tempo signatures onchain without implementing custom verification logic for each signature type. + +Additionally, since smart contracts have to statically bind their verification logic at deployment time, developers cannot maintain forward compatibility with future Tempo account signature schemes introduced after deployment without making their contracts upgradeable. This precompile serves as a stable interface that smart contracts can use to maintain forward compatibility with future Tempo account types and signature schemes. + +## Specification + +The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119. + +### Precompile Address + +``` +0x5165300000000000000000000000000000000000 +``` + +### Interface + +```solidity +interface ISignatureVerifier { + error InvalidFormat(); + error InvalidSignature(); + + /// @notice Recovers the signer of a Tempo signature (secp256k1, P256, WebAuthn). + /// @param hash The message hash that was signed + /// @param signature The encoded signature (see Tempo Transaction spec for formats) + /// @return Address of the signer if valid, reverts otherwise + function recover(bytes32 hash, bytes calldata signature) external view returns (address signer); + + /// @notice Verifies a signer against a Tempo signature (secp256k1, P256, WebAuthn). + /// @param signer The input address verified against the recovered signer + /// @param hash The message hash that was signed + /// @param signature The encoded signature (see Tempo Transaction spec for formats) + /// @return True if the input address signed, false otherwise. Reverts on invalid signatures. + function verify(address signer, bytes32 hash, bytes calldata signature) external view returns (bool); +} +``` + +### Signature Encoding + +Signatures MUST be encoded using the same format as [Tempo Transaction signatures](https://docs.tempo.xyz/docs/protocol/transactions/spec-tempo-transaction#signature-types): + +| Type | Format | Length | +|------|--------|--------| +| secp256k1 | `r \|\| s \|\| v` | 65 bytes | +| P256 | `0x01 \|\| r \|\| s \|\| x \|\| y \|\| prehash` | 130 bytes | +| WebAuthn | `0x02 \|\| webauthn_data \|\| r \|\| s \|\| x \|\| y` | 129–2049 bytes | + +### Verification Logic + +The precompile MUST use the same verification logic as Tempo transaction signature validation. See the [Tempo Transaction Signature Validation spec](https://docs.tempo.xyz/docs/protocol/transactions/spec-tempo-transaction#signature-validation) for details. + +#### Keychain Signature Rejection + +The precompile MUST reject signatures with a Keychain type prefix (`0x03` or `0x04`). Keychain signatures are multi-step, stateful verification flows that cannot be reduced to a single pure cryptographic check. Thus, if a Keychain prefix is detected, the precompile MUST revert with `InvalidFormat()`. + +Contracts that need to verify Keychain-based signatures can do so by composing this precompile with the AccountKeychain precompile: first, use the AccountKeychain precompile to resolve and validate the access key for the account, then use this precompile to verify the inner signature against the resolved key. This two-step pattern separates key management (stateful, account-scoped) from cryptographic verification (stateless, type-scoped), allowing each precompile to remain single-purpose. + +### Calldata Limits + +The precompile MUST enforce strict size limits on the `signature` argument **before** any decoding or copying occurs. If the signature exceeds the limit for its type, the precompile MUST revert with `InvalidFormat()`. + +| Type | Exact / Max Length | +|------|-------------------| +| secp256k1 | exactly 65 bytes | +| P256 | exactly 130 bytes | +| WebAuthn | 129–2049 bytes | + +### Gas Costs + +The precompile MUST charge gas and verify sufficient gas is available **before** performing any cryptographic verification. The precompile MUST revert with out-of-gas if the call has insufficient gas for the signature type. + +All calls pay the standard Tempo precompile calldata cost of 6 gas per 32-byte word (rounded up) on the full ABI-encoded input, consistent with all other Tempo precompiles. The verification gas per signature type is in-line with the [Tempo Transaction Signature Gas Schedule](https://docs.tempo.xyz/docs/protocol/transactions/spec-tempo-transaction#signature-verification-gas-schedule): + +| Type | Verification Gas | +|------|-----------------| +| secp256k1 | 3,000 | +| P256 | 8,000 | +| WebAuthn | 8,000 | + +Total gas = `input_cost(calldata_len)` + verification gas for the signature type. + +## Compatibility + +This TIP is **additive**. It introduces a new precompile at `0x5165300000000000000000000000000000000000` and does **not** modify existing EVM opcodes, transaction formats, or any existing Ethereum precompiles. + +### Backward Compatibility + +#### Ethereum `ecrecover` (`0x01`) + +This TIP does not modify `ecrecover` or any existing Ethereum precompile. `ecrecover` remains the standard tool for Ethereum-style secp256k1 address recovery. + +#### Developer-Facing Differences vs. `ecrecover` + +Solidity developers commonly use `ecrecover(hash, v, r, s)` to recover an address and then compare it to an expected signer. The Tempo signature verification precompile follows the same recover-and-return pattern but differs in one key way: `ecrecover` returns `address(0)` on invalid input or failed recovery, but the TIP-1020 precompile **reverts** on invalid signatures. Contracts that want non-reverting behavior SHOULD wrap calls using `try/catch` (high-level) or `staticcall` (low-level) and treat failure as "invalid signature". + +#### `v` Value Normalization + +For secp256k1 signatures, the precompile normalizes the recovery identifier `v`: both Ethereum-style values (`27`, `28`) and raw values (`0`, `1`) are accepted. This is intentional — `recover()` is designed to be as close to a drop-in replacement for `ecrecover` as possible, so it accepts the same `v` values. This differs from TIP-1004 (`permit()`), which requires `v ∈ {27, 28}` and reverts on `0` or `1`. + +#### Existing secp256k1 Signature Payloads + +For backwards compatibility, secp256k1 signatures are encoded as **65 bytes `r || s || v` with no type prefix**. Callers who already produce 65-byte secp256k1 signatures can reuse them directly as the `signature` argument to this precompile. + +### Forward Compatibility + +It is expected that this precompile will be updated when other account types are introduced to maintain forward compatibility with Tempo accounts. + +## Invariants + +| ID | Invariant | Description | +|----|-----------|-------------| +| **SV1** | Transaction-equivalent verification | For any signature type supported by a given function, the precompile MUST use the same cryptographic verification rules as Tempo transaction signature validation. | +| **SV2** | P256 and ECDSA signature malleability resistance | P256 and ECDSA signatures MUST satisfy the low-s requirement (`s <= n/2`). Signatures with high-s values MUST be rejected. | +| **SV3** | Signature size enforcement | The precompile MUST enforce per-type size limits (65 bytes secp256k1, 130 bytes P256, 129–2049 bytes WebAuthn) before any decoding or copying, preventing out-of-bounds reads and pathological resource usage. | +| **SV4** | Revert on failure | On any invalid signature, invalid encoding, or unsupported type, the precompile MUST revert. | +| **SV5** | Gas schedule consistency | Gas charged MUST follow the gas schedule listed above. | +| **SV6** | Signature type disambiguation | Exactly 65 bytes MUST be interpreted as secp256k1 (no prefix). Any non-65-byte signature MUST be interpreted using the leading type byte. Unknown type identifiers MUST revert. | +| **SV7** | Keychain signature rejection | Signatures with a Keychain type prefix (`0x03` or `0x04`) MUST be rejected. Keychain verification is achieved by composing the AccountKeychain precompile (key resolution) with this precompile (inner signature verification). | diff --git a/src/pages/docs/protocol/tips/tip-1022.mdx b/src/pages/docs/protocol/tips/tip-1022.mdx new file mode 100644 index 00000000..efd0bd40 --- /dev/null +++ b/src/pages/docs/protocol/tips/tip-1022.mdx @@ -0,0 +1,490 @@ +--- +id: TIP-1022 +title: Virtual Addresses for TIP-20 Deposit Forwarding +description: Precompile-native virtual addresses that auto-forward TIP-20 deposits to a registered master wallet, eliminating sweep transactions. +authors: Dankrad Feist, Liam Horne, Mallesh Pai, Dan Robinson +status: Testnet +related: TIP-20, TIP-403, TIP-1028 +protocolVersion: T3 +--- + +# TIP-1022: Virtual Addresses for TIP-20 Deposit Forwarding + +## Abstract + +This TIP introduces **virtual addresses**: a reserved 20-byte address format that, when detected in TIP-20 recipient-bearing operations, causes the precompile to auto-credit a registered master wallet instead of the literal target address. This eliminates sweep transactions entirely for entities such as exchanges, ramps, and payment processors that generate per-user deposit addresses. Master registration is a one-time onchain call; deposit address derivation is fully offchain. + +## Motivation + +- **Eliminate sweep transactions.** Entities such as exchanges, ramps, and payment processors need to offer each customer a unique deposit address. Today, funds arriving at each address must be swept back to a central wallet in separate transactions, which is a large operational cost and burden at scale. Virtual addresses auto-credit the master wallet at the protocol level, making sweeps unnecessary. + +- **Avoid the 250,000 gas new-account cost.** Tempo charges 250,000 gas to create state for a new address on first use. With virtual addresses, deposit addresses never create onchain state, so the first transfer to a new deposit address costs the same as any other transfer. + +- **Prevent state bloat.** Without virtual addresses, each customer deposit address creates a new account in the state trie. At enterprise scale (millions of deposit addresses), this is significant and permanent state growth. Virtual addresses avoid this entirely: no accounts are created, regardless of how many deposit addresses a business generates. + +--- + +# Specification + +## Address Layout + +Virtual addresses are standard 20-byte EVM addresses with the following reserved format: + +``` +[4-byte masterId] [10-byte MAGIC] [6-byte userTag] += 20 bytes total +``` + +| Field | Bytes | Description | +|-------|-------|-------------| +| **masterId** | 4 | Deterministic identifier derived from `(masterAddress, salt)` via the registration hash. This is the registry lookup key. | +| **VIRTUAL_MAGIC** | 10 | Fixed magic value `0xFDFDFDFDFDFDFDFDFDFD`. Identifies the address as virtual. | +| **userTag** | 6 | Opaque per-user identifier derived offchain by the operator. 48 bits support ~2.8×10^14 unique deposit addresses per master. | + +### Why This Layout? + +TIP-1022 intentionally places the 10-byte magic sequence in the **middle** of the address instead of at the beginning. This preserves more visually useful bytes at the front and back of the address for operators and users comparing deposit addresses in wallets, explorers, etc. + +The 4-byte `masterId` is kept short to preserve room for a large `userTag`, while the 10-byte magic keeps the format highly unlikely to appear accidentally. The security implications of this tradeoff are discussed in **Security Considerations**. + +## Conformance and Scope + +TIP-1022 applies only to TIP-20 precompile recipient resolution for the entrypoints listed in **Transfer Path Modification**. + +TIP-1022 does **not** alter TIP-20 methods that do not carry a recipient in the TIP-20 transfer path (e.g. `approve`, `burn`, `permit`) and does not alter non-TIP-20 protocol behavior. + +Non-TIP-20 token transfers (e.g. ERC-20 contracts deployed on Tempo) to virtual addresses are **not** subject to TIP-1022 forwarding. Such transfers behave as standard EVM transfers to the literal address. Tokens sent this way may be irrecoverable — see **Risks and Limitations**. + +`setRewardRecipient` is **not** a TIP-20 transfer-path operation and is therefore not subject to TIP-1022 recipient resolution. Implementations MUST reject virtual addresses when setting reward recipients so that rewards remain tied to canonical accounts rather than aliases. + +## Reserved Virtual Address Format + +Any address whose bytes `[4:14]` equal `VIRTUAL_MAGIC` is treated as a virtual address by the TIP-20 precompile. + +If a TIP-20 transfer targets such an address: +- the precompile extracts the `masterId` from bytes `[0:4]` +- looks up the registered master +- credits the resolved master if registered +- otherwise reverts with `VirtualAddressUnregistered` + +The literal virtual address never accumulates TIP-20 balance through standard TIP-20 transfer paths. + +### Reserved Address Space + +Addresses matching the virtual-address format occupy a reserved TIP-20 recipient namespace. A user who happens to control an EOA or contract whose address matches this format can still exist on Tempo and can still originate ordinary EVM transactions. However, TIP-20 transfers to such an address will follow TIP-1022 recipient resolution semantics rather than crediting the literal address. + +Users who control such an address SHOULD NOT use it as a normal account on Tempo. + +## Master ID Derivation + +The `masterId` is deterministic and derived from the registration hash computed during `registerVirtualMaster()`: + +``` +registrationHash = keccak256(abi.encodePacked(msg.sender, salt)) +masterId = bytes4(registrationHash[4:8]) +``` + +The first 4 bytes of `registrationHash` are consumed by the proof-of-work check (see **Registration Proof of Work**); the `masterId` is extracted from bytes `[4:8]` of the same hash. + +The salt is a `bytes32` value chosen by the caller. Callers MUST grind the salt to satisfy the 32-bit proof-of-work requirement. The resulting `masterId` is permanently bound to the registration address. + +### Why `masterId` Registrations Are Immutable + +TIP-1022 intentionally does not provide a mechanism to rotate or update the master address bound to a `masterId`. Allowing rotation would interact poorly with TIP-403 policies: a blacklisted master could rotate to a fresh address and resume receiving deposits, requiring policy enforcement to track `masterId`s in addition to addresses. Operators who need to change their underlying key material can register their `masterId` to an upgradeable proxy contract or multisig, allowing the controlling keys to be rotated at the contract layer without any protocol-level change. Finally, any rotation mechanism would require a timelock or similar delay to prevent an attacker who compromises a master key from silently redirecting deposits before the legitimate owner can respond — complexity that is better handled by the operator's own key management infrastructure. + +In the event of a `masterId` collision (two `(address, salt)` pairs mapping to the same 4-byte `masterId`), the second registration reverts with `MasterIdCollision`. The caller can retry with a different valid salt. The probability of such a collision (and the resulting need to regrind another salt) is less than 0.1% even if 4 million masterId's have already been registered. + +## Registration Proof of Work + +Registration requires a 32-bit proof of work to make **targeted collisions against a chosen `masterId`** computationally expensive. + +The registration hash is computed as: + +``` +registrationHash = keccak256(abi.encodePacked(msg.sender, salt)) +``` + +The first 4 bytes of `registrationHash` MUST be zero: + +``` +require(bytes4(registrationHash[0:4]) == 0x00000000) // 32-bit PoW +masterId = bytes4(registrationHash[4:8]) +``` + +This requires the caller to grind ~2^32 salt values to find a valid registration. If the first 4 bytes are not zero, the call reverts with `ProofOfWorkFailed`. + +This proof of work is intended to make it expensive for an attacker who sees a pending registration transaction to compute a different `(attackerAddress, salt)` pair that lands on the same `masterId` and gets mined first. With a 4-byte `masterId` and a 32-bit proof-of-work requirement, that targeted attack costs ~2^64 work. + +## User Tag Derivation (Offchain) + +The `userTag` is an opaque 6-byte value generated offchain by the operator. The protocol does not interpret or validate it — all values including `0x000000000000` are valid. It exists solely so the operator can attribute deposits to specific users via the two-hop `Transfer` events described below. + +Operators maintain their own internal mapping `{internalUserId -> virtualAddress}`. No onchain transaction is needed to create a new deposit address. + +## Worked Example + +An exchange with master address `0xABCD...1234` registers with a salt that satisfies the 32-bit PoW: + +- `registrationHash = keccak256(abi.encodePacked(0xABCD...1234, salt))` +- `registrationHash[0:4] == 0x00000000` (PoW satisfied) +- `masterId = bytes4(registrationHash[4:8])` -> e.g. `0x07A3B1C2` +- For customer #103048, the exchange derives a `userTag` -> e.g. `0xD4E5A7C3F19E` + +``` +Virtual address = 0x07A3B1C2 FDFDFDFDFDFDFDFDFDFD D4E5A7C3F19E + ^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^ + masterId magic (10) userTag (6) +``` + +## Registry Precompile + +Virtual address resolution requires a registry that maps `masterId -> masterAddress`. This is managed through a new precompile deployed at `0xFDC0000000000000000000000000000000000000`. + +The registry MUST maintain the following mapping constraints: + +- each `masterId` maps to at most one registered master address (one-to-one from `masterId`) +- multiple `masterId`s MAY map to the same master address (many-to-one) + +This many-to-one design allows a single underlying wallet to register multiple `masterId`s (e.g. with different salts). + +A **valid master address** MUST satisfy TIP-20 recipient safety constraints: + +- MUST NOT be `address(0)` +- MUST NOT itself match the virtual-address format (`VIRTUAL_MAGIC` at bytes `[4:14]`) +- MUST NOT be a TIP-20 token address (`0x20c000....` at bytes `[0:12]`) + +### Registry Storage Layout + +Each `masterId` maps to a single 32-byte storage slot: + +``` +slot = keccak256(abi.encode(masterId, REGISTRY_SLOT)) +value = masterType | reserved | masterAddress + ^^^^^^^^^^ ^^^^^^^^ ^^^^^^^^^^^ + 1 byte 11 bytes 20 bytes +``` + +Here `REGISTRY_SLOT` means the storage slot of the `mapping(bytes4 => bytes32)` used to store registry entries, following standard Solidity mapping layout. + +| Field | Bytes | Description | +|-------|-------|-------------| +| `masterType` | 1 | Type discriminator for future extensibility. MUST be `0x00` in this version. | +| `reserved` | 11 | Reserved for future use. MUST be zeroed. | +| `masterAddress` | 20 | The registered master address for this `masterId`. `address(0)` if unregistered. | + +This layout packs all metadata for a `masterId` into a single storage slot, enabling one SLOAD during transfer-path resolution. + +### Interface + +```solidity +interface IAddressRegistry { + // ──────────────────── Events ──────────────────── + + /// @notice Emitted when a new master is registered. + event MasterRegistered( + bytes4 indexed masterId, + address indexed masterAddress + ); + + // ──────────────────── Errors ──────────────────── + + /// @notice The computed masterId is already registered to a different address. + error MasterIdCollision(); + + /// @notice The caller/new master address is invalid for virtual forwarding. + error InvalidMasterAddress(); + + /// @notice The registration hash does not satisfy the 32-bit proof-of-work requirement. + error ProofOfWorkFailed(); + + /// @notice The virtual address has a valid format but its masterId is not registered. + error VirtualAddressUnregistered(); + + // ──────────────── Registration ────────────────── + + /// @notice Registers msg.sender as a virtual address master. + /// @dev The registration hash is keccak256(abi.encodePacked(msg.sender, salt)). + /// The first 4 bytes of the hash MUST be zero (32-bit proof of work). + /// masterId is derived from bytes [4:8] of the registration hash. + /// Reverts with ProofOfWorkFailed if the first 4 bytes are not zero. + /// Reverts with InvalidMasterAddress if msg.sender is not a valid master address. + /// Reverts with MasterIdCollision if the derived masterId is already taken + /// by a different address. On collision, the caller can retry with a different salt. + /// The same address MAY register multiple masterIds using different salts. + /// @param salt Caller-chosen salt for masterId derivation. Must satisfy 32-bit PoW. + /// @return masterId The derived master identifier. + function registerVirtualMaster(bytes32 salt) external returns (bytes4 masterId); + + // ────────────────── Queries ───────────────────── + + /// @notice Returns the registered master address for a given masterId, or address(0) if unregistered. + function getMaster(bytes4 masterId) external view returns (address); + + /// @notice Resolves a transfer recipient using TIP-1022 execution semantics. + /// For non-virtual addresses, returns `to` unchanged. + /// For virtual addresses, returns the registered master or reverts with + /// VirtualAddressUnregistered. + function resolveRecipient(address to) external view returns (address effectiveRecipient); + + /// @notice Resolves a virtual address to its registered master. + /// Returns address(0) if the address does not match the virtual-address format. + /// Returns address(0) if the masterId is not registered. + function resolveVirtualAddress(address virtualAddr) external view returns (address master); + + /// @notice Returns true if the address matches the virtual-address format. + function isVirtualAddress(address addr) external pure returns (bool); + + /// @notice Decodes a virtual address into its components. + /// @return isVirtual True if the address matches the virtual-address format. + /// @return masterId The 4-byte master identifier (zero if not virtual). + /// @return userTag The 6-byte user tag (zero if not virtual). + function decodeVirtualAddress(address addr) + external pure returns (bool isVirtual, bytes4 masterId, bytes6 userTag); +} +``` + +### Constants + +| Name | Value | Description | +|------|-------|-------------| +| `VIRTUAL_MAGIC` | `0xFDFDFDFDFDFDFDFDFDFD` | 10-byte magic value identifying virtual addresses | +| `REGISTRY_ADDRESS` | `0xFDC0000000000000000000000000000000000000` | Precompile address for the virtual-address registry | + +## Transfer Path Modification + +The following existing TIP-20 entrypoints are modified to resolve the `to` (recipient) address before crediting: + +- `transfer` +- `transferFrom` +- `transferWithMemo` +- `transferFromWithMemo` +- `mint` +- `mintWithMemo` +- `systemTransferFrom` + +The `from` address on `transferFrom`, `transferFromWithMemo`, and `systemTransferFrom` is **not** affected by TIP-1022 resolution. + +### Resolution Logic + +```text +function resolveRecipient(to: address) -> address: + // Check bytes [4:14] against VIRTUAL_MAGIC + if to[4:14] != VIRTUAL_MAGIC: + return to + + masterId = to[0:4] + master = registry.getMaster(masterId) + + if master == address(0): + revert VirtualAddressUnregistered() + + return master +``` + +### Standard Transfer Entrypoints + +For `transfer`, `transferFrom`, `transferWithMemo`, `transferFromWithMemo`, and `systemTransferFrom`: + +1. **Resolve recipient**: compute `effectiveRecipient = resolveRecipient(to)`. If `to` is not virtual, `effectiveRecipient = to`. +2. **Token-level sender check**: apply the standard TIP-403 / TIP-1015 sender authorization rules. +3. **Token-level recipient check**: apply the standard TIP-403 / TIP-1015 recipient authorization rules to `effectiveRecipient`. +4. **Apply balance changes**: debit sender, credit `effectiveRecipient`. +5. **Emit events**: per **Event Emission** (two-hop `Transfer` if virtual, single `Transfer` otherwise). + +If any step reverts, the enclosing TIP-20 operation MUST revert atomically with no balance changes and no events. + +### Mint Entrypoints + +For `mint` and `mintWithMemo`: + +1. **Resolve recipient**: compute `effectiveRecipient = resolveRecipient(to)`. If `to` is not virtual, `effectiveRecipient = to`. +2. **Token-level mint-recipient check**: apply the standard TIP-1015 mint-recipient authorization rules to `effectiveRecipient`. +3. **Apply balance changes**: credit `effectiveRecipient`. +4. **Emit events**: per **Event Emission**. + +If any step reverts, the enclosing TIP-20 operation MUST revert atomically with no balance changes and no events. + +### Authorization Semantics + +TIP-1022 does not introduce new authorization logic in TIP-403 itself. Instead, TIP-20 transfer and mint logic MUST resolve virtual recipient addresses before invoking the existing TIP-403 / TIP-1015 checks. + +Concretely, for any TIP-20 entrypoint covered by TIP-1022: + +1. Compute `effectiveRecipient = resolveRecipient(to)`. +2. Apply the existing sender / recipient / mint-recipient authorization rules to `effectiveRecipient`, not the literal virtual address. + +This preserves view/execution symmetry with the TIP-20 authorization path defined by TIP-1015: any internal TIP-20 helper such as `isTransferAuthorized(from, to)` MUST evaluate recipient authorization against the resolved master address when `to` is virtual. + +`balanceOf(virtualAddress)` remains literal and MUST continue to return 0. + +Contracts or integrators that need explicit resolution behavior outside the TIP-20 transfer path MAY call `resolveRecipient` on the registry. + +## Event Emission + +TIP-1022 does **not** introduce new transfer-path events. The registry precompile emits `MasterRegistered`, but forwarding itself is represented using **two-hop standard `Transfer` events**: one hop showing funds arriving at the virtual address, and a second hop showing funds moving from the virtual address to the resolved master. Using standard `Transfer` events (rather than a new event type) preserves compatibility with existing indexers, block explorers, and wallets that already understand TIP-20 / ERC-20 `Transfer` events — no custom integration is required to track virtual address deposits. + +For transfers where the recipient is **not** a virtual address, event emission is unchanged from standard TIP-20 behavior — a single `Transfer(sender, to, amount)`. + +### Deposit Forwarding (Inbound) + +When a transfer targets a virtual address (`to` is virtual), the precompile MUST emit two `Transfer` events in sequence: + +1. `Transfer(sender, virtualAddress, amount)` — shows funds arriving at the virtual address +2. `Transfer(virtualAddress, masterAddress, amount)` — shows funds forwarding to the master + +The actual balance change is applied only to `masterAddress`. The virtual address never holds a balance; the first `Transfer` event is a logical representation of deposit attribution, not a real balance credit. + +Indexers that need deposit attribution SHOULD watch for pairs of `Transfer` events within the same transaction where the intermediate address matches the virtual-address format. The `userTag` can then be extracted from the virtual address to identify the depositor. + +### Entrypoint-Specific Event Ordering + +- `transfer`, `transferFrom`, `systemTransferFrom`: + 1. `Transfer(sender, virtualAddress, amount)` + 2. `Transfer(virtualAddress, masterAddress, amount)` + +- `transferWithMemo`, `transferFromWithMemo`: + 1. `Transfer(sender, virtualAddress, amount)` + 2. `TransferWithMemo(sender, virtualAddress, amount, memo)` + 3. `Transfer(virtualAddress, masterAddress, amount)` + +- `mint`: + 1. `Transfer(address(0), virtualAddress, amount)` + 2. `Mint(virtualAddress, amount)` + 3. `Transfer(virtualAddress, masterAddress, amount)` + +- `mintWithMemo`: + 1. `Transfer(address(0), virtualAddress, amount)` + 2. `TransferWithMemo(address(0), virtualAddress, amount, memo)` + 3. `Mint(virtualAddress, amount)` + 4. `Transfer(virtualAddress, masterAddress, amount)` + +## Self-Forwarding + +If the registered master sends tokens to one of its own virtual addresses, the transfer resolves back to the master, effectively a transfer to self. The standard TIP-20 self-transfer semantics apply (no net balance change). The two-hop `Transfer` events are still emitted: `Transfer(master, virtualAddress, amount)` followed by `Transfer(virtualAddress, master, amount)`. Indexers SHOULD NOT interpret this as net inflow when `from == masterAddress` in the first hop. + +## Interaction with TIP-403 + +Virtual address resolution happens **before** TIP-403 / TIP-1015 authorization checks. Policy evaluation uses the resolved `masterAddress`, not the literal virtual address. + +- If the **master address** is not authorized to receive the token, transfers to any of its virtual addresses revert. +- If the **sender** is not authorized to send the token, the transfer reverts. +- Policies configured on individual virtual addresses are ignored by the TIP-20 transfer path because virtual addresses have no independent canonical TIP-20 balance. + +### Rejection of Virtual Addresses in Policy Operations + +TIP-403 operations that accept addresses as policy members MUST reject virtual addresses rather than accepting them silently. + +Implementations SHOULD use a clear, informative error indicating that virtual addresses are aliases for TIP-20 forwarding and are not valid literal policy subjects. + +Rejecting these operations avoids the footgun where an operator configures policy on the virtual alias they see in logs or explorers instead of on the resolved master address that actually holds the funds. + +## Interaction with Account-Level Features + +- **`balanceOf(virtualAddress)`**: Always returns 0. Virtual addresses do not hold balances. +- **Nonce / transaction origination**: A contract or EOA whose address matches the virtual-address format can still exist and can still originate ordinary EVM transactions. TIP-1022 resolution applies only to the `to` field in TIP-20 precompile calls, not to transaction senders. + +## Security Considerations + +### 4-Byte `masterId` and 32-Bit Registration PoW + +A 4-byte `masterId` would be too small if its security relied only on raw namespace size. TIP-1022 does **not** rely on that. Instead, security comes from the combination of: + +- a 4-byte `masterId`, and +- a 32-bit proof-of-work requirement on registration + +An attacker who sees a pending registration transaction and wants to steal that `masterId` must compute a different `(attackerAddress, salt)` pair that: + +1. satisfies the 32-bit proof-of-work requirement, and +2. lands on the same 4-byte `masterId` + +That targeted attack costs roughly 2^64 work. Further, because registration requires proof-of-work grinding, deployment will typically happen via dedicated tooling or a managed service that: + +- performs the proof-of-work search, +- submits the registration transaction, and +- waits for confirmation or revert before the operator routes value through the resulting master ID. + +This does not eliminate the residual collision-risk entirely, but it substantially reduces the practical chance that an operator incorrectly believes they control a master ID that was actually registered first by an attacker. + +### Why the Magic Bytes Are in the Middle + +The middle `VIRTUAL_MAGIC` layout is a deliberate usability tradeoff: + +- it leaves the first 4 bytes available for `masterId` +- it leaves the last 6 bytes available for `userTag` +- it avoids spending the most visually important bytes of the address on static marker data + +This improves address comparison in UIs while still keeping a large reserved pattern that is highly unlikely to appear accidentally. We believe this layout is superior to the other permutations in terms of the prospect of address poisoning attacks (see below). + +### Contracts and EOAs Matching the Virtual Format + +A sufficiently resourced adversary could, in principle, grind a CREATE2 deployment or private key so that a contract or EOA lands at an address matching the virtual-address format in a `masterID` controlled by the adversary. We view this as unlikely in practice because the address must match a 10-byte fixed magic value in the middle of the address, while targeted theft of a specific registered namespace also requires colliding the 4-byte `masterId` under the registration proof-of-work design (i.e., 14 bytes totally). + +A stronger global reservation mechanism for problematic address ranges may still be desirable in the future. + +### Policy Configuration on Virtual Addresses + +Virtual addresses are forwarding aliases, not canonical TIP-20 holders. Using them directly in policy configuration is misleading and dangerous because the TIP-20 transfer path evaluates policies against the resolved master address. + +Accordingly, TIP-403 configuration operations SHOULD reject virtual addresses with explicit errors rather than accepting them. + +--- + +## Risks and Limitations + +### Address Poisoning and UI Confusion + +TIP-1022 still introduces a recognizable structured address format. Wallets, block explorers, and operational tooling that truncate addresses SHOULD display enough of the address to distinguish both the `masterId` and the `userTag`; ideally they SHOULD show the full address. + + +### Non-TIP-20 Token Loss + +TIP-1022 forwarding applies exclusively to TIP-20 precompile operations. Non-TIP-20 tokens (e.g. ERC-20 contracts deployed on Tempo) transferred to a virtual address are credited to the literal virtual address by the ERC-20 contract and are irrecoverable: no recovery mechanism is defined here. + +This risk is mitigated by the strong incentives for token issuers to use TIP-20 on Tempo (gas-payment eligibility, access to the payment lane, and policy support), but it remains a limitation of this design. + +### Non-TIP-20 Protocol Positions Minted to Virtual Addresses +TIP-1022 changes only the TIP-20 transfer and mint entrypoints listed in this document. It does not change other protocol logic that accepts an address parameter and records ownership against that literal address. + +This creates an edge case for protocols that mint LP shares, receipt tokens, or other redeemable positions to a user-supplied to address. If such a protocol later requires the recorded holder address to burn, redeem, or withdraw, a position minted to a virtual address can become stranded even though the corresponding master account controls that virtual namespace. The Fee AMM is one example of this pattern: LP shares minted can be mited to a virtual address, but are then permanently unburnable since `burn` checks that `msg_sender==lp_address`. + +In short, virtual-address forwarding is only defined for the TIP-20 paths enumerated by TIP-1022; other protocols remain literal-address systems unless they explicitly say otherwise. + +### Externally-Triggerable Revert on Unregistered Virtual Addresses + +TIP-1022 introduces a recipient-dependent revert: if the `to` address matches the virtual-address format but its `masterId` is not registered, the transfer reverts with `VirtualAddressUnregistered`. This is the first TIP-20 revert condition that an untrusted recipient address can induce — prior to TIP-1022, transfers could only revert due to sender-side conditions (insufficient balance, authorization failure). + +Contracts that perform batch transfers in a single transaction (e.g. payroll, airdrop, or distribution contracts) SHOULD validate recipient addresses before execution or wrap individual transfers in try/catch to prevent a single unregistered virtual address from reverting the entire batch. + +### Contracts and EOAs at virtual addresses + +It is theoretically possible to deploy a contract or control an EOA whose address matches `VIRTUAL_MAGIC`, including by grinding CREATE2 salts or private keys. Such addresses can still exist and originate ordinary EVM transactions, but we consider this unlikely in practice because targeting the 10-byte `VIRTUAL_MAGIC` requires roughly 2^80 work, with additional cost for targeted collisions against registered virtual namespaces. + +--- + +# Invariants + +## Core Invariants + +1. **No fund loss**: A TIP-20 transfer to a virtual address MUST either credit the registered master's balance by exactly the transfer amount, or revert. Funds MUST NOT be credited to the virtual address itself or lost. + +2. **Revert on unregistered**: A transfer to an address matching the virtual-address format whose `masterId` is not registered MUST revert. It MUST NOT credit any account. + +3. **Balance consistency**: After a successful virtual-forwarded transfer of amount `X`, `balanceOf(master)` MUST have increased by exactly `X`. + +4. **Zero-balance invariant**: For every virtual address, `balanceOf(virtualAddress)` MUST equal 0 from T3 activation onwards. Pre-T3, the TIP-20 precompile does not perform virtual-address resolution, so a transfer targeting an address that matches the virtual format will credit the literal address. Such pre-T3 balances are stranded (no party can claim them) and do not violate this invariant, which applies only to the T3-and-later transfer path. The probability of anyone controlling a private key for such an address is negligible. + +5. **Event consistency**: For virtual-forwarded entrypoints, the precompile MUST emit two `Transfer` events: `Transfer(sender, virtualAddress, amount)` followed by `Transfer(virtualAddress, masterAddress, amount)`. `TransferWithMemo` events MUST immediately follow their matching `Transfer` and MUST use `virtualAddress` as the recipient to preserve deposit attribution. `Mint` events MUST use `virtualAddress`. + +6. **Non-virtual path unaffected**: Transfers to addresses that do not match the virtual-address format MUST behave identically to pre-TIP-1022 semantics, with no registry lookup. + +7. **Deterministic masterId**: Given `registrationHash = keccak256(abi.encodePacked(registrationAddress, salt))`, the first 4 bytes of `registrationHash` MUST be zero, and `masterId` MUST equal `bytes4(registrationHash[4:8])`, where `registrationAddress` is the address that called `registerVirtualMaster()` and `salt` is the caller-supplied salt. If the PoW check fails, registration MUST revert with `ProofOfWorkFailed`. `masterId` MUST NOT depend on registration order or transaction ordering. + +8. **Master ID uniqueness**: Each `masterId` MUST map to at most one registered master address. Multiple `masterId`s MAY map to the same master address. + +9. **Atomic revert behavior**: If virtual resolution fails, the enclosing TIP-20 call MUST revert with no state changes and no events. + +10. **View/execution symmetry**: TIP-20 authorization logic MUST evaluate recipient authorization against the resolved master address when `to` is virtual, matching execution-time recipient resolution semantics. + +11. **Policy on master**: TIP-403 / TIP-1015 authorization for virtual-forwarded transfers and mints MUST check the resolved `masterAddress`. Policies set on individual virtual addresses MUST be ignored by the TIP-20 transfer path. + +12. **Policy-operation rejection**: TIP-403 configuration operations that accept literal addresses as policy subjects or members MUST reject virtual addresses. diff --git a/src/pages/docs/protocol/tips/tip-1030.mdx b/src/pages/docs/protocol/tips/tip-1030.mdx new file mode 100644 index 00000000..ad98b2cf --- /dev/null +++ b/src/pages/docs/protocol/tips/tip-1030.mdx @@ -0,0 +1,49 @@ +--- +id: TIP-1030 +title: Allow same-tick flip orders +description: Relaxes the placeFlip validation to allow flipTick to equal tick, enabling flip orders that flip to the same price. +authors: Dan Robinson +status: Draft +related: TIP-1002 +protocolVersion: T5 +supersedes: TIP-1002 +--- + +# TIP-1030: Allow same-tick flip orders + +## Abstract + +Relaxes the `placeFlip` validation to allow `flipTick == tick`, enabling flip orders that flip to the same price. This supersedes TIP-1002, extracting the "allow same-tick flip orders" portion without the "prevent crossed orders" change. + +## Motivation + +Currently, `placeFlip` requires `flipTick` to be strictly on the opposite side of `tick` (e.g., for a bid, `flipTick > tick`). This prevents use cases like instant token convertibility, where someone wants to place flip orders on both sides at the same tick to create a stable two-sided market that automatically replenishes when orders are filled. + +--- + +# Specification + +## Modified behavior + +The `placeFlip` validation is relaxed to allow `flipTick == tick`: + +- **Current behavior**: For bids, `flipTick > tick` required; for asks, `flipTick < tick` required +- **New behavior**: For bids, `flipTick >= tick` required; for asks, `flipTick <= tick` required + +## Events + +No new events. + +## New errors + +No new errors. + +# Implications + +- **Locked books**: Same-tick flip orders can result in `best_bid_tick == best_ask_tick`. This forecloses any future upgrade that would forbid bids and asks from resting at the same tick, since same-tick flip orders legitimately create that state. +- **MEV**: Tighter flip orders (where `flipTick` is closer to or equal to `tick`) increase the likelihood of certain kinds of MEV, such as backrunning, since the new opposite-side order appears at a better price for the backrunner. + +# Invariants + +- Flip orders with `flipTick == tick` are accepted and behave like any other flip order +- Flip orders with `flipTick` strictly on the wrong side of `tick` are still rejected diff --git a/src/pages/docs/protocol/tips/tip-1031.mdx b/src/pages/docs/protocol/tips/tip-1031.mdx new file mode 100644 index 00000000..ccde6ff0 --- /dev/null +++ b/src/pages/docs/protocol/tips/tip-1031.mdx @@ -0,0 +1,91 @@ +--- +id: TIP-1031 +title: Embed consensus context in the block Header +description: Embed consensus context into the block header +authors: Hamdi, Janis +status: Approved +related: N/A +protocolVersion: T4 +--- + +# TIP-1031: Embed Consensus Context into the Block Header + +## Abstract + +Embed consensus metadata into the `TempoHeader`. + +## Motivation + +Consensus context is a prerequisite to newer features in Commonware. + * [Deferred Verification](https://github.com/commonwarexyz/monorepo/blob/main/consensus/src/marshal/standard/deferred.rs#L487). A reduction in finalization latency by optimisitically notarizing blocks and verifying them async in the background. + +By embedding the context into the header, Tempo blocks can implement the required [CertifiableBlock](https://github.com/commonwarexyz/monorepo/blob/2a588e4e341548333a4bd753c016e814ae0ecca0/consensus/src/lib.rs#L57) trait. + +--- + +# Specification + +When activated, a new field, `Option` is added as the **last** field of `TempoHeader`, which must be set on every subsequent block containing consensus metadata. The field follows the trailing‑optional pattern used by Ethereum's fork‑activated header fields (e.g. `base_fee_per_gas`, `blob_gas_used`): when `None`, it is omitted entirely from the RLP stream, preserving existing block hashes for pre‑activation headers. + +```rust +#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, RlpEncodable, RlpDecodable)] +#[cfg_attr(feature = "reth-codec", derive(reth_codecs::Compact))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct Context { + pub epoch: u64, + pub view: u64, + pub parent_view: u64, + pub leader: B256, +} +``` + +The `Context` adds 3 fields: 3 `u64` values and 1 `B256` value, totaling 56 bytes raw. The field required to construct the [context required by simplex](https://github.com/commonwarexyz/monorepo/blob/main/consensus/src/simplex/types.rs#L22), not present in this struct can be computed using properties of the block, `parent_hash` -> `parent_digest`. + +**RLP**: Each `u64` encodes to at most 9 bytes (1‑byte prefix + 8 bytes); each `B256` encodes to 33 bytes (1‑byte prefix + 32 bytes). With a list header, the worst‑case overhead is **60 bytes** per block. Pre‑activation headers incur zero overhead. + +**Compact (DB)**: The `reth_codecs::Compact` representation is comparable, using bitflag‑compressed integer widths. + +## Block Production + +When activated the proposals must: + +1. Construct the `Context` from the information provided from the consensus engine. +2. **MUST** set the context field on the header. + +If not activated, the context field **MUST** be `None`. + +## Block Verification + +During `Automaton::verify()` step: + +1. If not activated, the context **MUST** be `None`. +2. If activated, the context **MUST** be set and match the information provided by the consensus engine. +3. Continue with verification as-is. + +Important to note the immediate switch to `Deferred` is __not strictly required__. For example a validator can choose an implementation that preserves synchronous verification. This validator simply does not contribute to the reduced latency in forming the notarization certificate. + +## Genesis Block + +The Genesis block MUST have the consensus context set to `None`. + +--- + +# Invariants + +1. **Context presence**: Every block beyond genesis when activated has a set `Context`. + +2. **Context correctness**: The encoded context MUST exactly match the `Context` provided by the consensus engine when the block was proposed. Validators MUST reject blocks where the embedded context does not match. + +3. **Context commitment**: Because the context is a part of the header, and the header hash is the block's digest, the context is transitively committed to by any notarization or finalization certificate over that digest. + +4. **Backward incompatibility**: This is a breaking change to block verification. All nodes must upgrade at the same protocol version. Blocks produced before the upgrade do not have the set context. Any blocks proposed by a non-upgraded node will have their proposals rejected, and will incorrectly notarize blocks with an invalid context. + +5. **Encoding backward compatibility**: The `context` field MUST be the last field in `TempoHeader`. When `None`, it MUST be omitted entirely from the RLP stream so that the encoded representation of pre‑activation headers remains unchanged and existing block hashes are preserved. + +6. **Round-trip fidelity**: Context serializes and de-serializes into the same values. + +### Test Coverage + +- `CertifiableBlock::context()` returns the correct context for a block built with known parameters. +- Block rejection when embedded context does not match the consensus engine's context. +- RLP round-trip for `TempoHeader` with `context: Some(...)` and `context: None`, and that a pre-activation header's hash is unchanged after the upgrade. \ No newline at end of file diff --git a/src/pages/docs/protocol/tips/tip-1036.mdx b/src/pages/docs/protocol/tips/tip-1036.mdx new file mode 100644 index 00000000..abeb9f8e --- /dev/null +++ b/src/pages/docs/protocol/tips/tip-1036.mdx @@ -0,0 +1,102 @@ +--- +id: TIP-1036 +title: T2 Hardfork Bug Fixes +description: Meta TIP collecting all audit-driven bug fixes and hardening changes gated behind the T2 hardfork. +authors: Tanishk (@legion2002), Rusowsky (@0xrusowsky), Jennifer (@jenpaff), Howy (@howydev), Kitsune (@0xKitsune) +status: In Review +related: N/A +protocolVersion: T2 +--- + +# TIP-1036: T2 Hardfork Bug Fixes + +## Abstract + +This meta TIP collects audit-driven bug fixes, security hardening, and correctness updates that activate at T2. Each item is small in isolation, but together they define the complete in-scope T2 bug-fix bundle for this TIP, while fixes already specified by other activated TIPs (such as TIP-1017) are intentionally excluded. + +## Motivation + +Internal and external review uncovered several T2-relevant correctness and security issues across core execution paths. Because these fixes alter state-function behavior at activation boundaries, they need hardfork gating and are grouped here as one coordinated rollout. This meta TIP tracks only fixes that are not already specified by another activated TIP (for example, TIP-1017). + +--- + +# Changes + +## 1. Require `tx.origin` for AccountKeychain admin ops + +**PRs**: [#3202](https://github.com/tempoxyz/tempo/pull/3202) · **Author**: @legion2002, [#3250](https://github.com/tempoxyz/tempo/pull/3250) · **Author**: @0xrusowsky + +With T2, `authorizeKey`, `revokeKey`, and `updateSpendingLimit` require direct owner calls by enforcing both `transaction_key == Address::ZERO` and `msg_sender == tx_origin`. This blocks indirect contract-call paths from being used to perform key-admin actions with owner-level authority. If `tx_origin` is not seeded, admin ops are rejected (failed-closed). + +## 2. Reject self-sponsored fee payer signatures + +**PR**: [#3200](https://github.com/tempoxyz/tempo/pull/3200) *(merged)* · **Author**: @legion2002 + +Rejects AA transactions where the `fee_payer_signature` resolves back to the sender, preventing self-sponsored signatures from bypassing fee-payer assumptions. Enforced in both txpool validation and EVM fee-payer resolution. + +## 3. Check token paused in internal DEX balance swaps + +**PR**: [#3204](https://github.com/tempoxyz/tempo/pull/3204) · **Author**: @0xrusowsky + +Adds a `check_not_paused()` call in `StablecoinDEX` internal balance transfers gated behind `is_t2()`. Previously, swaps using internal DEX balances could bypass the token pause state. + +## 4. Correct built-in policy type data for TIP403Registry + +**PR**: [#3203](https://github.com/tempoxyz/tempo/pull/3203) *(merged)* · **Author**: @0xrusowsky + +Built-in policies (`REJECT_ALL` / `ALLOW_ALL`) are virtual and not stored on-chain. On T2, `policyData()` now returns the correct `PolicyType` (`WHITELIST` / `BLACKLIST` respectively) and `Address::ZERO` admin for these built-in IDs instead of falling through to storage reads. + +## 5. Reject legacy invalid policy types in compound sub-policies + +**PR**: [#3188](https://github.com/tempoxyz/tempo/pull/3188) *(merged)* · **Author**: @howydev + +Uses `is_simple()` instead of `!is_compound()` to validate compound policy sub-policies, rejecting legacy type-255 policies that previously passed the negated check. + +## 6. Handle T2 policy errors in DEX + +**PR**: [#3015](https://github.com/tempoxyz/tempo/pull/3015) *(merged)* · **Author**: @0xrusowsky + +Updates DEX precompiles to handle the new `TIP403RegistryError::InvalidPolicyType` error returned by `policy_type()` post-T2, replacing the old `Panic(UnderOverflow)` sentinel. + +## 7. Return zero remaining limit for revoked keys + +**PR**: [#2553](https://github.com/tempoxyz/tempo/pull/2553) *(merged)* · **Author**: @0xrusowsky + +`getRemainingLimit()` now returns zero for revoked or non-existent access keys instead of a stale positive value. + +## 8. Nonce key gas repricing + +**PR**: [#2533](https://github.com/tempoxyz/tempo/pull/2533) *(merged)* · **Author**: @0xrusowsky + +Increases intrinsic gas costs for 2D nonce keys on T2 by adding `2 × WARM_SLOAD` to both existing-key and new-key gas to account for extended storage lookups. Base costs differ (`COLD_SLOAD + WARM_SSTORE_RESET` for existing, `COLD_SLOAD + SSTORE_SET` for new), but the T2 delta is the same. + +## 9. Error with `PolicyNotFound` for non-existent policy IDs + +**PR**: [#2618](https://github.com/tempoxyz/tempo/pull/2618) *(merged)* · **Author**: @0xrusowsky + +`get_policy_data()` now reverts with `PolicyNotFound` for non-existent policy IDs instead of silently returning default values. + +## 10. Refund spending limit for unused gas fees + +**PR**: [#2528](https://github.com/tempoxyz/tempo/pull/2528) *(merged)* · **Author**: @legion2002 + +Restores access key spending limits by the refunded gas amount in `transfer_fee_post_tx()`. Previously the full max fee was permanently deducted from the spending limit regardless of actual gas used. + +## 11. Tick spacing checks on DEX price conversion functions + +**PR**: [#2513](https://github.com/tempoxyz/tempo/pull/2513) *(merged)* · **Author**: @0xKitsune + +Adds tick spacing validation to `tick_to_price` and `price_to_tick`, rejecting ticks that don't align with the pool's configured spacing. + +## 12. Reserved liquidity transient storage check + +**PR**: [#2496](https://github.com/tempoxyz/tempo/pull/2496) *(merged)* · **Author**: @0xKitsune + +Adds a transient storage (`TSTORE`/`TLOAD`) guard to prevent reserved liquidity from being double-spent within the same transaction. + +## 13. Reject zero-address ecrecover in permit + +**PR**: [#2786](https://github.com/tempoxyz/tempo/pull/2786) *(merged)* · **Author**: @howydev + +`permit()` now explicitly rejects `recovered == address(0)` before comparing against `owner`. Previously, a crafted signature recovering to `address(0)` could have been accepted if `owner` was also `address(0)`. + diff --git a/src/pages/docs/protocol/tips/tip-1038.mdx b/src/pages/docs/protocol/tips/tip-1038.mdx new file mode 100644 index 00000000..edc3ef6e --- /dev/null +++ b/src/pages/docs/protocol/tips/tip-1038.mdx @@ -0,0 +1,56 @@ +--- +id: TIP-1038 +title: T3 Hardfork Meta TIP +description: Meta TIP collecting all bug fixes, security hardening, and gas correctness changes gated behind the T3 hardfork. +authors: Rusowsky (@0xrusowsky), Dragan Rakita (@rakita), Federico Gimenez (@fgimenez), Derek Cofausper (@decofe) +status: Testnet +protocolVersion: T3 +--- + +# TIP-1038: T3 Hardfork Meta TIP + +## Abstract + +This meta TIP collects bug fixes, gas correctness improvements, and security hardening changes that activate at T3. Each item is small in isolation, but together they define the complete in-scope T3 bug-fix bundle. + +## Motivation + +Ongoing internal review and audit follow-ups uncovered several correctness and performance issues that require hardfork gating. Because these fixes alter state-function behavior at activation boundaries, they are grouped here as one coordinated rollout under T3. + +--- + +# Changes + +## 1. Skip redundant `setUserToken` write and event when token unchanged + +**PR**: [#3272](https://github.com/tempoxyz/tempo/pull/3272) · **Author**: @fgimenez + +`setUserToken()` unconditionally writes to storage and emits `UserTokenSet` even when the token hasn't changed. Since the event triggers a full O(pool_size) txpool scan via `evict_invalidated_transactions`, any account can force network-wide CPU work each block by repeatedly calling `setUserToken` with the same value. T3+ adds a read-before-write guard that returns early when the stored token already matches, eliminating the redundant write, event, and pool scan. + +## 2. TIP-20: verify paused state before mint and burn + +**PR**: [#3411](https://github.com/tempoxyz/tempo/pull/3411) · **Author**: @0xrusowsky + +The TIP-20 pause mechanism was missing from `mint`, `mintWithMemo`, `burn`, `burnWithMemo`, and `burnBlocked` — an unintentional omission that left token-moving operations reachable while the contract was paused. The pause flag is meant to act as a universal kill switch; leaving mint/burn unguarded undermines that guarantee and, in particular, prevents the admin from stopping a compromised `BURN_BLOCKED_ROLE` key from seizing funds. + +T3+ adds `check_not_paused()` to all five functions. A new `validate_mint` helper consolidates the pause check, recipient validation, and TIP-403 policy check into a single call. Administrative functions (role management, unpausing) and `transferFeePostTx` remain intentionally exempt. + +## 3. Disambiguate optional AA expiry and validity timestamps + +**PRs**: [#3500](https://github.com/tempoxyz/tempo/pull/3500), [#3501](https://github.com/tempoxyz/tempo/pull/3501) · **Author**: @legion2002 + +Several AA timestamp fields were encoded as `Option` in RLP, but `None` and `Some(0)` both serialize as the empty string. That made `Some(0)` silently roundtrip to `None`, which could invert user intent by turning an immediately expired access key or transaction into one with no expiry bound. + +T3+ changes `key_authorization.expiry`, `valid_before`, and `valid_after` to `Option` at the primitives layer so zero-valued bounds are unrepresentable. Serde-backed JSON/request deserialization rejects `0x0` explicitly for these fields, while downstream execution and pool components convert back to plain `u64` only where comparisons or storage require it. + +## 4. StablecoinDEX: check token paused in internal balance swaps + +**PR**: [#3204](https://github.com/tempoxyz/tempo/pull/3204) · **Author**: @0xrusowsky + +The StablecoinDEX `swap_exact_amount_in` and `swap_exact_amount_out` paths operate on internal DEX balances and bypass TIP-20 `transfer`, so the pause check in the token contract is never hit. A paused token could still be swapped through the DEX — including as an intermediate hop in a multi-leg route. T3+ adds `check_not_paused()` to `validate_and_build_route` for every token in the swap path, ensuring paused tokens block DEX swaps the same way they block direct transfers. + +## 5. Account-keychain: clamp refunded spending limits to the configured max + +**PR**: [#3483](https://github.com/tempoxyz/tempo/pull/3483) · **Author**: @rakita + +`refund_spending_limit()` restored spending room with a saturating add, which could raise a T3 key's remaining allowance above the configured max during defensive refund paths. T3+ clamps refunded spending limits to the stored `max`, preserving the invariant that `remaining <= max` for T3 keys while leaving migrated pre-T3 rows on their legacy behavior because they do not persist a max bound. diff --git a/src/pages/docs/protocol/tips/tip-1047.mdx b/src/pages/docs/protocol/tips/tip-1047.mdx new file mode 100644 index 00000000..a848c09d --- /dev/null +++ b/src/pages/docs/protocol/tips/tip-1047.mdx @@ -0,0 +1,56 @@ +--- +id: TIP-1047 +title: Revert code creation and set code at addresses with TIP-20 prefix +description: Reject contract creation and EIP-7702 delegations that produce addresses in the TIP-20 reserved prefix range +authors: Howy (@howydev) +status: Draft +related: TIP-20 +protocolVersion: T5 +--- + +# TIP-1047: Revert code creation and set code at addresses with TIP-20 prefix + +## Abstract + +Reject any CREATE, CREATE2, or EIP-7702 authorization that would produce or delegate to an address starting with the TIP-20 token prefix (`0x20C000000000000000000000`). Without this guard, anyone can place arbitrary bytecode at an address that the rest of the system treats as a TIP-20 token. + +## Motivation + +Multiple system components use `is_tip20_prefix` to decide whether an address is a TIP-20 token: precompile routing, fee-token validation, stablecoin DEX, and transaction classification. Code or a delegation at a prefix-matching address would pass these checks despite not being a real token. + +CREATE and CREATE2 addresses are downstream of keccak; EIP-7702 authority addresses require secp256k1 point multiplication followed by keccak. Matching a 12-byte prefix requires ~2^96 work in all cases. This guard provides defense-in-depth by making such collisions fail-closed. + +--- + +# Specification + +## CREATE and CREATE2 + +Before setting up a create frame, compute the would-be contract address: + +- **CREATE**: `keccak256(rlp(caller, nonce))[12..]` +- **CREATE2**: `keccak256(0xff ++ caller ++ salt ++ keccak256(init_code))[12..]` + +If `is_tip20_prefix(address)` is true, revert the opcode. The caller's nonce is not bumped, no value is transferred, and no init-code is executed. Base opcode gas is consumed; init-code gas is not. + +## EIP-7702 Authorizations + +When processing EIP-7702 authorization lists, if `is_tip20_prefix(authority)` is true for a recovered authority address, skip the entry. The delegation is not applied. + +--- + +# Invariants + +1. **No new code at TIP-20 addresses**: No CREATE/CREATE2 produces a contract where `is_tip20_prefix` returns true. Applies to all depths (top-level and nested creates). + +2. **No delegations to TIP-20 addresses**: No EIP-7702 authorization sets a delegation for an address where `is_tip20_prefix` returns true. + +3. **Nonce preservation**: A rejected CREATE/CREATE2 does not bump the caller's nonce. + +### Test coverage + +- CREATE2 with a salt producing a TIP-20-prefixed address reverts without executing init-code. +- CREATE where the computed address has a TIP-20 prefix reverts with nonce unchanged. +- EIP-7702 authorization with a TIP-20-prefixed authority is skipped. +- Normal CREATE/CREATE2 producing non-TIP-20 addresses is unaffected. + diff --git a/src/pages/protocol/transactions/AccountKeychain.mdx b/src/pages/docs/protocol/transactions/AccountKeychain.mdx similarity index 98% rename from src/pages/protocol/transactions/AccountKeychain.mdx rename to src/pages/docs/protocol/transactions/AccountKeychain.mdx index d6deb6bf..c90999e1 100644 --- a/src/pages/protocol/transactions/AccountKeychain.mdx +++ b/src/pages/docs/protocol/transactions/AccountKeychain.mdx @@ -190,13 +190,13 @@ interface IAccountKeychain { - `key_authorization` includes an optional trailing `witness: bytes32` field. When present, the witness is included in the signing hash, checked against the account's burned-witness set, and emitted when the key authorization is registered. The protocol otherwise treats it as opaque and application-defined, so apps can bind a single access-key authorization signature to an offchain challenge. :::info[T6 testnet behavior — SDK encoders/decoders] -The [T6 network upgrade](/protocol/upgrades/t6) adds **admin access keys** ([TIP-1049](https://tips.sh/1049)). For partners maintaining transaction tooling, the wire-level change is two new trailing optional fields on the `KeyAuthorization` RLP payload: +The [T6 network upgrade](/docs/protocol/upgrades/t6) adds **admin access keys** ([TIP-1049](https://tips.sh/1049)). For partners maintaining transaction tooling, the wire-level change is two new trailing optional fields on the `KeyAuthorization` RLP payload: ```text rlp([chain_id, key_type, key_id, expiry?, limits?, allowed_calls?, witness?, is_admin?, account?]) ``` -SDK encoders/decoders and hardware-wallet firmware that strictly check field count MUST handle the new optionals before using T6 networks. The packed `AuthorizedKey` slot also gains an `is_admin` byte at offset 11. The existing `KeyAuthorized` event is **unchanged**; an additional `AdminKeyAuthorized` event is emitted alongside it when `is_admin == true`, so existing indexers continue to work without changes. Provisioning admin keys uses `authorizeAdminKey(keyId, signatureType, witness)`. See the [admin access keys section of the T6 page](/protocol/upgrades/t6#admin-access-keys) for the full surface. +SDK encoders/decoders and hardware-wallet firmware that strictly check field count MUST handle the new optionals before using T6 networks. The packed `AuthorizedKey` slot also gains an `is_admin` byte at offset 11. The existing `KeyAuthorized` event is **unchanged**; an additional `AdminKeyAuthorized` event is emitted alongside it when `is_admin == true`, so existing indexers continue to work without changes. Provisioning admin keys uses `authorizeAdminKey(keyId, signatureType, witness)`. See the [admin access keys section of the T6 page](/docs/protocol/upgrades/t6#admin-access-keys) for the full surface. ::: - Creates a new key entry with the specified `signatureType`, `config.expiry`, `config.enforceLimits`, and `isRevoked` set to `false` diff --git a/src/pages/protocol/transactions/eip-4337.mdx b/src/pages/docs/protocol/transactions/eip-4337.mdx similarity index 82% rename from src/pages/protocol/transactions/eip-4337.mdx rename to src/pages/docs/protocol/transactions/eip-4337.mdx index 535955a5..deb4fdd4 100644 --- a/src/pages/protocol/transactions/eip-4337.mdx +++ b/src/pages/docs/protocol/transactions/eip-4337.mdx @@ -24,7 +24,7 @@ Tempo Transactions provide these capabilities at the protocol level. EIP-4337 requires deploying a Paymaster contract, funding it with ETH, and running or paying for bundler infrastructure. The Paymaster validates sponsorship requests and the bundler aggregates UserOperations. -Tempo Transactions include a `fee_payer_signature` field directly in the transaction. A sponsor signs the transaction to agree to pay fees. The protocol validates both signatures and deducts fees from the sponsor. No contracts or infrastructure are required. See the [fee sponsorship guide](/guide/payments/sponsor-user-fees) for implementation details. +Tempo Transactions include a `fee_payer_signature` field directly in the transaction. A sponsor signs the transaction to agree to pay fees. The protocol validates both signatures and deducts fees from the sponsor. No contracts or infrastructure are required. See the [fee sponsorship guide](/docs/guide/payments/sponsor-user-fees) for implementation details. ### Batched Operations @@ -36,13 +36,13 @@ Tempo Transactions include a native `calls` array. Multiple contract calls execu EIP-4337 requires deploying a custom validation contract for each signature scheme. The contract must implement signature verification logic. -Tempo Transactions natively support secp256k1, P256, and WebAuthn signatures. The protocol verifies these signatures directly. Passkey authentication works without custom contracts. See the [passkey accounts guide](/guide/use-accounts/embed-passkeys) for implementation details. +Tempo Transactions natively support secp256k1, P256, and WebAuthn signatures. The protocol verifies these signatures directly, so passkey authentication works without custom contracts. ### Gas Token Flexibility EIP-4337 Paymasters can accept alternative tokens but must convert them to ETH internally. -Tempo Transactions pay fees directly in any USD stablecoin that has liquidity on the Fee AMM. No conversion infrastructure is needed. See the [stablecoin fees guide](/guide/payments/pay-fees-in-any-stablecoin) for implementation details. +Tempo Transactions pay fees directly in any USD stablecoin that has liquidity on the Fee AMM. No conversion infrastructure is needed. See the [stablecoin fees guide](/docs/guide/payments/pay-fees-in-any-stablecoin) for implementation details. ## Integration Comparison @@ -59,4 +59,4 @@ Tempo has the ERC-4337 EntryPoint contract deployed for projects that require co ## Getting Started -To start using Tempo Transactions, see the [Tempo Transactions guide](/guide/tempo-transaction) for SDK integration in TypeScript, Rust, Go, and Python. For the full technical specification, see the [Tempo Transaction Specification](/protocol/transactions/spec-tempo-transaction). +To start using Tempo Transactions, see the [Tempo Transactions guide](/docs/guide/tempo-transaction) for SDK integration in TypeScript, Rust, Go, and Python. For the full technical specification, see the [Tempo Transaction Specification](/docs/protocol/transactions/spec-tempo-transaction). diff --git a/src/pages/protocol/transactions/eip-7702.mdx b/src/pages/docs/protocol/transactions/eip-7702.mdx similarity index 85% rename from src/pages/protocol/transactions/eip-7702.mdx rename to src/pages/docs/protocol/transactions/eip-7702.mdx index c79b7a79..3d0b1c08 100644 --- a/src/pages/protocol/transactions/eip-7702.mdx +++ b/src/pages/docs/protocol/transactions/eip-7702.mdx @@ -31,10 +31,10 @@ EIP-7702 provides delegation as a building block. Applications must implement hi Tempo Transactions include these features natively: -- **Fee sponsorship**: Built into the transaction type, not requiring delegation. See the [fee sponsorship guide](/guide/payments/sponsor-user-fees). +- **Fee sponsorship**: Built into the transaction type, not requiring delegation. See the [fee sponsorship guide](/docs/guide/payments/sponsor-user-fees). - **Scheduled execution**: Native `validAfter` and `validBefore` timestamp windows for time-bounded transactions. -- **Parallelizable nonces**: Multiple nonce keys for concurrent transactions. See the [parallel transactions guide](/guide/payments/send-parallel-transactions). -- **Access keys**: Delegate signing to secondary keys with configurable permissions. See the [Account Keychain specification](/protocol/transactions/AccountKeychain). +- **Parallelizable nonces**: Multiple nonce keys for concurrent transactions. See the [parallel transactions guide](/docs/guide/payments/send-parallel-transactions). +- **Access keys**: Delegate signing to secondary keys with configurable permissions. See the [Account Keychain specification](/docs/protocol/transactions/AccountKeychain). ### Combining Delegation with Native Features @@ -55,8 +55,8 @@ Tempo Transactions can use EIP-7702 delegation alongside native features. An app The `aa_authorization_list` field in Tempo Transactions contains authorizations that follow EIP-7702 structure. Each authorization delegates an account to a specified implementation contract and is signed by the account authority. -For full details on the authorization format, see the [Tempo Transaction Specification](/protocol/transactions/spec-tempo-transaction). +For full details on the authorization format, see the [Tempo Transaction Specification](/docs/protocol/transactions/spec-tempo-transaction). ## Getting Started -To start using Tempo Transactions, see the [Tempo Transactions guide](/guide/tempo-transaction) for SDK integration in TypeScript, Rust, Go, and Python. +To start using Tempo Transactions, see the [Tempo Transactions guide](/docs/guide/tempo-transaction) for SDK integration in TypeScript, Rust, Go, and Python. diff --git a/src/pages/protocol/transactions/index.mdx b/src/pages/docs/protocol/transactions/index.mdx similarity index 74% rename from src/pages/protocol/transactions/index.mdx rename to src/pages/docs/protocol/transactions/index.mdx index a218f46b..2fb596c8 100644 --- a/src/pages/protocol/transactions/index.mdx +++ b/src/pages/docs/protocol/transactions/index.mdx @@ -3,13 +3,13 @@ description: Learn about Tempo Transactions, a new EIP-2718 transaction type wit --- import { Cards, Card } from 'vocs' -import TempoTxProperties from '../../../snippets/tempo-tx-properties.mdx' +import TempoTxProperties from '../../../../snippets/tempo-tx-properties.mdx' # Tempo Transactions Tempo Transactions are a new [EIP-2718](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-2718.md) transaction type, exclusively available on Tempo. -If you're integrating with Tempo, we **strongly recommend** using Tempo Transactions, and not regular Ethereum transactions. Learn more about the benefits below, or follow the guide on issuance [here](/guide/issuance). +If you're integrating with Tempo, we **strongly recommend** using Tempo Transactions, and not regular Ethereum transactions. Learn more about the benefits below, or follow the guide on issuance [here](/docs/guide/issuance). - - diff --git a/src/pages/protocol/transactions/spec-tempo-transaction.mdx b/src/pages/docs/protocol/transactions/spec-tempo-transaction.mdx similarity index 99% rename from src/pages/protocol/transactions/spec-tempo-transaction.mdx rename to src/pages/docs/protocol/transactions/spec-tempo-transaction.mdx index ea5f98f9..6c215811 100644 --- a/src/pages/protocol/transactions/spec-tempo-transaction.mdx +++ b/src/pages/docs/protocol/transactions/spec-tempo-transaction.mdx @@ -8,7 +8,7 @@ description: Technical specification for Tempo Transactions (EIP-2718) with WebA This spec introduces native protocol support for the following features, using Tempo Transactions: -* WebAuthn/P256 signature validation - enables passkey accounts +* WebAuthn/P256 signature validation - enables passkey signing * Parallelizable nonces - allows higher tx throughput for each account * Gas sponsorship - allows apps to pay for their users' transactions * Call Batching - allows users to multicall efficiently and atomically diff --git a/src/pages/protocol/upgrades/t2.mdx b/src/pages/docs/protocol/upgrades/t2.mdx similarity index 97% rename from src/pages/protocol/upgrades/t2.mdx rename to src/pages/docs/protocol/upgrades/t2.mdx index 50830604..79ebd0eb 100644 --- a/src/pages/protocol/upgrades/t2.mdx +++ b/src/pages/docs/protocol/upgrades/t2.mdx @@ -46,7 +46,7 @@ T2 built on T1 and introduced the following features: **Spec:** [TIP-1017](https://tips.sh/1017) -**Operator guide:** [Controlling validator lifecycle](/guide/node/validator-lifecycle) +**Operator guide:** [Controlling validator lifecycle](/docs/guide/node/validator-lifecycle) **TLDR:** New precompile for managing consensus validators. Adds Ed25519 signature verification at registration, append-only history, and stable validator indices. diff --git a/src/pages/protocol/upgrades/t3.mdx b/src/pages/docs/protocol/upgrades/t3.mdx similarity index 88% rename from src/pages/protocol/upgrades/t3.mdx rename to src/pages/docs/protocol/upgrades/t3.mdx index 4a6e74ca..67c0bc69 100644 --- a/src/pages/protocol/upgrades/t3.mdx +++ b/src/pages/docs/protocol/upgrades/t3.mdx @@ -26,7 +26,7 @@ Node operators needed to upgrade to the T3-compatible release before the testnet |-----|-------------|-------------------| | [TIP-1011](https://tips.sh/1011) | Periodic spending limits, call scoping, and a ban on access-key contract creation | Wallets, account SDKs, apps using connected apps or subscriptions | | [TIP-1020](https://tips.sh/1020) | Signature verification precompile for secp256k1, P256, and WebAuthn | Smart contract teams, account integrators, wallet SDKs | -| [TIP-1022](https://tips.sh/1022) | [Virtual addresses](/protocol/tip20/virtual-addresses) for TIP-20 deposit forwarding | Exchanges, ramps, custodians, payment processors, explorers, indexers | +| [TIP-1022](https://tips.sh/1022) | [Virtual addresses](/docs/protocol/tip20/virtual-addresses) for TIP-20 deposit forwarding | Exchanges, ramps, custodians, payment processors, explorers, indexers | ## Breaking changes @@ -36,7 +36,7 @@ These breaking changes only affect access-key integrations. You need to update y Integrations that directly call `AccountKeychain.authorizeKey(...)` or manually encode `key_authorization` must use the TIP-1011 tuple-form ABI. The legacy selector `0x54063a55` no longer works — legacy calls fail with `LegacyAuthorizeKeySelectorChanged(newSelector: 0x980a6025)`. -If you use an updated SDK, this is mostly a tooling upgrade. If you hand-encode calldata, use the exact tuple-form signature from the [Account Keychain precompile spec](/protocol/transactions/AccountKeychain). +If you use an updated SDK, this is mostly a tooling upgrade. If you hand-encode calldata, use the exact tuple-form signature from the [Account Keychain precompile spec](/docs/protocol/transactions/AccountKeychain). ### Access-key contract creation @@ -58,15 +58,15 @@ Most integrators only needed to upgrade tooling. Existing authorized access keys Each virtual-address deposit emits two `Transfer` events in one transaction: `Transfer(sender, virtualAddress, amount)` then `Transfer(virtualAddress, masterWallet, amount)`. Collapse these into one logical deposit to the master wallet. The virtual address is for attribution only — `balanceOf(virtualAddress)` is always zero. -See [Virtual addresses for TIP-20 deposits](/protocol/tip20/virtual-addresses) for the full routing model and event sequences. +See [Virtual addresses for TIP-20 deposits](/docs/protocol/tip20/virtual-addresses) for the full routing model and event sequences. ### Signature verification precompile -[TIP-1020](https://tips.sh/1020) is additive — existing verifier setups keep working. Teams that want a standard onchain verification surface can adopt the precompile instead of maintaining custom verifier contracts. See the [Signature Verification with Foundry](/sdk/foundry/signature-verifier) guide. +[TIP-1020](https://tips.sh/1020) is additive — existing verifier setups keep working. Teams that want a standard onchain verification surface can adopt the precompile instead of maintaining custom verifier contracts. See the [Signature Verification with Foundry](/docs/sdk/foundry/signature-verifier) guide. ## Compatible SDK releases -Tempo's broader tooling ecosystem is available in [Developer tools](/quickstart/developer-tools). +Tempo's broader tooling ecosystem is available in [Developer tools](/docs/quickstart/developer-tools). | SDK | T3-compatible release | |-----|-----------------------| @@ -77,6 +77,6 @@ Tempo's broader tooling ecosystem is available in [Developer tools](/quickstart/ | [Foundry](https://github.com/foundry-rs/foundry) | [`v1.7.0`](https://github.com/foundry-rs/foundry/releases/tag/v1.7.0) | ## Related docs -- [Virtual addresses for TIP-20 deposits](/protocol/tip20/virtual-addresses) +- [Virtual addresses for TIP-20 deposits](/docs/protocol/tip20/virtual-addresses) - [TIP-1022](https://tips.sh/1022) - Coordinating meta TIP: [tempoxyz/tempo#3273](https://github.com/tempoxyz/tempo/pull/3273) diff --git a/src/pages/protocol/upgrades/t4.mdx b/src/pages/docs/protocol/upgrades/t4.mdx similarity index 93% rename from src/pages/protocol/upgrades/t4.mdx rename to src/pages/docs/protocol/upgrades/t4.mdx index 4335da75..e53b8be2 100644 --- a/src/pages/protocol/upgrades/t4.mdx +++ b/src/pages/docs/protocol/upgrades/t4.mdx @@ -33,10 +33,10 @@ T4 introduced the following changes: | [Rust](https://github.com/tempoxyz/tempo) | [`tempo-alloy@1.7.0`](https://github.com/tempoxyz/tempo/releases/tag/tempo-alloy%401.7.0), [`tempo-primitives@1.7.0`](https://github.com/tempoxyz/tempo/releases/tag/tempo-primitives%401.7.0), [`tempo-contracts@1.7.0`](https://github.com/tempoxyz/tempo/releases/tag/tempo-contracts%401.7.0) | | [Foundry](https://github.com/foundry-rs/foundry) | nightly (T4 hardfork-aware decoding) | -See [Developer tools](/quickstart/developer-tools) for the broader SDK ecosystem. +See [Developer tools](/docs/quickstart/developer-tools) for the broader SDK ecosystem. ## Related docs -- [Network upgrades and releases](/guide/node/network-upgrades) +- [Network upgrades and releases](/docs/guide/node/network-upgrades) - [TIP-1031: Embed Consensus Context in the Block Header](https://tips.sh/1031) - [TIP-1046: T4 Hardfork Meta TIP](https://tips.sh/1046) diff --git a/src/pages/protocol/upgrades/t5.mdx b/src/pages/docs/protocol/upgrades/t5.mdx similarity index 97% rename from src/pages/protocol/upgrades/t5.mdx rename to src/pages/docs/protocol/upgrades/t5.mdx index 5a403c5a..9b319806 100644 --- a/src/pages/protocol/upgrades/t5.mdx +++ b/src/pages/docs/protocol/upgrades/t5.mdx @@ -113,7 +113,7 @@ The following releases support the full T5 feature set. New integrations should | Ecosystem | T5-compatible releases | | ---------- | ---------------------- | -| TypeScript | [`mppx@0.7.0`](https://github.com/wevm/mppx/releases/tag/mppx%400.7.0), [`viem@2.52.2`](https://github.com/wevm/viem/releases/tag/viem%402.52.2), [`accounts@0.14.9`](https://github.com/tempoxyz/accounts/releases/tag/accounts%400.14.9) | +| TypeScript | [`mppx@0.7.0`](https://github.com/wevm/mppx/releases/tag/mppx%400.7.0), [`viem@2.52.2`](https://github.com/wevm/viem/releases/tag/viem%402.52.2) | | Rust | [`tempo-alloy@1.8.0`](https://github.com/tempoxyz/tempo/releases/tag/tempo-alloy%401.8.0), [`tempo-primitives@1.8.0`](https://github.com/tempoxyz/tempo/releases/tag/tempo-primitives%401.8.0), [`tempo-contracts@1.8.0`](https://github.com/tempoxyz/tempo/releases/tag/tempo-contracts%401.8.0) | | Foundry | [nightly](https://getfoundry.sh) (T5 hardfork-aware decoding) | @@ -134,7 +134,7 @@ T5 is mostly additive. Integrators, indexers, wallets, explorers, and partner in - Treat `OrderFlipped` as the active state for the same `orderId`. - Do not assume a filled flip order receives a new `orderId`. - Remove checks that reject `flipTick == tick`. -- See the [flip-order indexing notes](/protocol/exchange/providing-liquidity#flip-order-indexing). +- See the [flip-order indexing notes](/docs/protocol/exchange/providing-liquidity#flip-order-indexing). ### For FeeAMM integrators diff --git a/src/pages/protocol/upgrades/t6.mdx b/src/pages/docs/protocol/upgrades/t6.mdx similarity index 100% rename from src/pages/protocol/upgrades/t6.mdx rename to src/pages/docs/protocol/upgrades/t6.mdx diff --git a/src/pages/protocol/upgrades/t7.mdx b/src/pages/docs/protocol/upgrades/t7.mdx similarity index 100% rename from src/pages/protocol/upgrades/t7.mdx rename to src/pages/docs/protocol/upgrades/t7.mdx diff --git a/src/pages/quickstart/connection-details.mdx b/src/pages/docs/quickstart/connection-details.mdx similarity index 84% rename from src/pages/quickstart/connection-details.mdx rename to src/pages/docs/quickstart/connection-details.mdx index 405f1039..59e92e0a 100644 --- a/src/pages/quickstart/connection-details.mdx +++ b/src/pages/docs/quickstart/connection-details.mdx @@ -4,8 +4,8 @@ mipd: true interactive: true --- -import * as Demo from '../../components/guides/Demo.tsx' -import { ConnectWallet } from '../../components/ConnectWallet.tsx' +import * as Demo from '../../../components/guides/Demo.tsx' +import { ConnectWallet } from '../../../components/ConnectWallet.tsx' # Connect to the Network @@ -18,12 +18,12 @@ Click on your browser wallet below to automatically connect it to the Tempo netw :::warning -Note that on some wallets, you might see an unusually high "balance". This is because, historically, blockchain wallets have always assumed that a blockchain has a "native gas token". On Tempo, there is no native gas token, and so the value shown is a placeholder. See [EVM Differences](/quickstart/evm-compatibility#handling-eth-balance-checks) for more information on this quirk. +Note that on some wallets, you might see an unusually high "balance". This is because, historically, blockchain wallets have always assumed that a blockchain has a "native gas token". On Tempo, there is no native gas token, and so the value shown is a placeholder. See [EVM Differences](/docs/quickstart/evm-compatibility#handling-eth-balance-checks) for more information on this quirk. ::: ## Connect via CLI -To connect via CLI, we recommend using [`cast`](https://getfoundry.sh/cast/overview/), which is a command-line tool for interacting with Ethereum networks. To install cast, you can read more in the [Foundry SDK docs](/sdk/foundry#get-started-with-foundry). +To connect via CLI, we recommend using [`cast`](https://getfoundry.sh/cast/overview/), which is a command-line tool for interacting with Ethereum networks. To install cast, you can read more in the [Foundry SDK docs](/docs/sdk/foundry#get-started-with-foundry). ```bash /dev/null/monitor.sh#L1-11 # Check block height (should be steadily increasing) diff --git a/src/pages/quickstart/developer-tools.mdx b/src/pages/docs/quickstart/developer-tools.mdx similarity index 98% rename from src/pages/quickstart/developer-tools.mdx rename to src/pages/docs/quickstart/developer-tools.mdx index 95767b69..8df1b1ef 100644 --- a/src/pages/quickstart/developer-tools.mdx +++ b/src/pages/docs/quickstart/developer-tools.mdx @@ -59,7 +59,7 @@ Integrating with Tempo is easy by leveraging services provided by our infrastruc /> @@ -95,9 +95,9 @@ Get started with the [Squid docs](https://docs.squidrouter.com/) or try the [bri ### Tempo Indexer (TIDX) -[TIDX](/developer-tools/indexer) is Tempo's hosted indexer for querying blocks, transactions, logs, token balances, and decoded events through SQL. Use the public mainnet endpoint at `https://indexer.tempo.xyz` or the testnet endpoint at `https://indexer.testnet.tempo.xyz`. +[TIDX](/docs/developer-tools/indexer) is Tempo's hosted indexer for querying blocks, transactions, logs, token balances, and decoded events through SQL. Use the public mainnet endpoint at `https://indexer.tempo.xyz` or the testnet endpoint at `https://indexer.testnet.tempo.xyz`. -The hosted indexer powers explorer-style reads and supports ClickHouse-backed analytical queries for expensive reads like holder lists and token activity. Try the [interactive TIDX example](/developer-tools/indexer#interactive-example) or read the [TIDX README](https://github.com/tempoxyz/tidx) to run your own indexer. +The hosted indexer powers explorer-style reads and supports ClickHouse-backed analytical queries for expensive reads like holder lists and token activity. Try the [interactive TIDX example](/docs/developer-tools/indexer#interactive-example) or read the [TIDX README](https://github.com/tempoxyz/tidx) to run your own indexer. ### Allium @@ -190,7 +190,7 @@ Get a free API key from the [Zerion dashboard](https://dashboard.zerion.io/) and ### Tempo Explorer -Tempo's official Mainnet block explorer is available at [explore.tempo.xyz](https://explore.tempo.xyz). View transactions, blocks, accounts, and token activity on the Tempo network. Testnet block explorer is available at [explore.testnet.tempo.xyz](https://explore.testnet.tempo.xyz). For more connection information, see [Connect to the Network](/quickstart/connection-details). +Tempo's official Mainnet block explorer is available at [explore.tempo.xyz](https://explore.tempo.xyz). View transactions, blocks, accounts, and token activity on the Tempo network. Testnet block explorer is available at [explore.testnet.tempo.xyz](https://explore.testnet.tempo.xyz). For more connection information, see [Connect to the Network](/docs/quickstart/connection-details). ### Tenderly diff --git a/src/pages/quickstart/evm-compatibility.mdx b/src/pages/docs/quickstart/evm-compatibility.mdx similarity index 93% rename from src/pages/quickstart/evm-compatibility.mdx rename to src/pages/docs/quickstart/evm-compatibility.mdx index 07fe827c..6430597e 100644 --- a/src/pages/quickstart/evm-compatibility.mdx +++ b/src/pages/docs/quickstart/evm-compatibility.mdx @@ -42,7 +42,7 @@ While the execution environment mirrors Ethereum's, Tempo introduces some differ By default, all existing functionality will work for EVM-compatible wallets, with only a few quirks. For developers of wallets, we strongly encourage you to implement support for Tempo Transactions over regular EVM transactions. See the [transaction differences](#transaction-differences) for more. :::tip -If you are building a wallet, read our [guide for wallet developers](/quickstart/wallet-developers). +If you are building a wallet, read our [guide for wallet developers](/docs/quickstart/wallet-developers). ::: ### Handling ETH (native token) Balance Checks @@ -71,11 +71,11 @@ Tempo does not have a native gas token. Instead, fees are denominated in USD and If your transactions are not using Tempo Transactions, there is a cascading fee token selection algorithm that determines the default fee token based on the user's preferences and the contract being called. -This preference system is specified [here](/protocol/fees/spec-fee#fee-token-preferences) in detail. +This preference system is specified [here](/docs/protocol/fees/spec-fee#fee-token-preferences) in detail. #### Consideration 1: Setting a user default fee token -As specified in the preference system above, the simplest way to specify the fee token for a user is to set the user default fee token. Read about how to do that [here](/protocol/fees/spec-fee#account-level) on behalf of an account. +As specified in the preference system above, the simplest way to specify the fee token for a user is to set the user default fee token. Read about how to do that [here](/docs/protocol/fees/spec-fee#account-level) on behalf of an account. #### Consideration 2: Paying fees in the TIP-20 contract being interacted with @@ -87,9 +87,9 @@ Importantly, note that the `amount` field in this case is sent in full. So, if t If the user is calling a contract that is not a TIP-20 token, the EVM transaction will default to the pathUSD token. Thus, in order to send transactions to non-TIP-20 contracts, the wallet must hold some balance of pathUSD. -On the Tempo Testnet, pathUSD is available from the [faucet](/quickstart/faucet). +On the Tempo Testnet, pathUSD is available from the [faucet](/docs/quickstart/faucet). -If a wallet wants to submit a non-TIP20 transaction without having to submit the above transaction, we recommend investing in using [Tempo Transactions](/quickstart/integrate-tempo#tempo-transactions) instead. +If a wallet wants to submit a non-TIP20 transaction without having to submit the above transaction, we recommend investing in using [Tempo Transactions](/docs/quickstart/integrate-tempo#tempo-transactions) instead. ## VM Layer Differences diff --git a/src/pages/quickstart/faucet.mdx b/src/pages/docs/quickstart/faucet.mdx similarity index 80% rename from src/pages/quickstart/faucet.mdx rename to src/pages/docs/quickstart/faucet.mdx index dbcf2520..f2e55671 100644 --- a/src/pages/quickstart/faucet.mdx +++ b/src/pages/docs/quickstart/faucet.mdx @@ -4,13 +4,13 @@ mipd: true interactive: true --- -import * as Demo from '../../components/guides/Demo.tsx' -import { AddFundsToOthers } from '../../components/guides/steps/payments/AddFundsToOthers.tsx' -import { AddFundsToWallet } from '../../components/guides/steps/wallet/AddFundsToWallet.tsx' -import { AddTokensToWallet } from '../../components/guides/steps/wallet/AddTokensToWallet.tsx' -import { ConnectWallet } from '../../components/guides/steps/wallet/ConnectWallet.tsx' -import { SetFeeToken } from '../../components/guides/steps/wallet/SetFeeToken.tsx' -import * as Token from '../../components/guides/tokens' +import * as Demo from '../../../components/guides/Demo.tsx' +import { AddFundsToOthers } from '../../../components/guides/steps/payments/AddFundsToOthers.tsx' +import { AddFundsToWallet } from '../../../components/guides/steps/wallet/AddFundsToWallet.tsx' +import { AddTokensToWallet } from '../../../components/guides/steps/wallet/AddTokensToWallet.tsx' +import { ConnectWallet } from '../../../components/guides/steps/wallet/ConnectWallet.tsx' +import { SetFeeToken } from '../../../components/guides/steps/wallet/SetFeeToken.tsx' +import * as Token from '../../../components/guides/tokens' import { Tabs, Tab } from 'vocs' diff --git a/src/pages/quickstart/integrate-tempo.mdx b/src/pages/docs/quickstart/integrate-tempo.mdx similarity index 73% rename from src/pages/quickstart/integrate-tempo.mdx rename to src/pages/docs/quickstart/integrate-tempo.mdx index 4cba2811..8fbe67c5 100644 --- a/src/pages/quickstart/integrate-tempo.mdx +++ b/src/pages/docs/quickstart/integrate-tempo.mdx @@ -1,15 +1,15 @@ --- -description: Build on Tempo Testnet. Connect to the network, explore SDKs, and follow guides for accounts, payments, and stablecoin issuance. +description: Build on Tempo Testnet. Connect to the network, explore SDKs, and follow guides for payments, stablecoin issuance, and exchange. interactive: true --- -import * as Demo from '../../components/guides/Demo.tsx' -import { ConnectWallet } from '../../components/ConnectWallet.tsx' +import * as Demo from '../../../components/guides/Demo.tsx' +import { ConnectWallet } from '../../../components/ConnectWallet.tsx' import { Cards, Card } from 'vocs' # Integrate Tempo -Tempo is fully compatible with the Ethereum Virtual Machine (EVM), targeting the **Osaka** EVM hard fork. So, everything you'd expect to work with Ethereum works on Tempo, with only a few exceptions which we detail on the [EVM Differences](/quickstart/evm-compatibility) page. +Tempo is fully compatible with the Ethereum Virtual Machine (EVM), targeting the **Osaka** EVM hard fork. So, everything you'd expect to work with Ethereum works on Tempo, with only a few exceptions which we detail on the [EVM Differences](/docs/quickstart/evm-compatibility) page. @@ -17,44 +17,38 @@ Tempo is fully compatible with the Ethereum Virtual Machine (EVM), targeting the icon="lucide:network" title="Connect to the Network" description="Learn how to connect to the Tempo Testnet with your wallet or programmatically." - to="/quickstart/connection-details" + to="/docs/quickstart/connection-details" /> ## Start Building -The guides below will help you get a sense for what is possible to do with Tempo. All of the guides below are fully-featured Tempo applications themselves, built with the [Tempo SDKs](/sdk). +The guides below will help you get a sense for what is possible to do with Tempo. All of the guides below are fully-featured Tempo applications themselves, built with the [Tempo SDKs](/docs/sdk). - @@ -69,13 +63,13 @@ If you're interested in learning more about how Tempo works under the hood, feel icon="lucide:server" title="Run a Tempo Node" description="System requirements, installation and usage instructions for running a Tempo node." - to="/guide/node" + to="/docs/guide/node" /> diff --git a/src/pages/quickstart/predeployed-contracts.mdx b/src/pages/docs/quickstart/predeployed-contracts.mdx similarity index 68% rename from src/pages/quickstart/predeployed-contracts.mdx rename to src/pages/docs/quickstart/predeployed-contracts.mdx index 1a90385a..559d60a3 100644 --- a/src/pages/quickstart/predeployed-contracts.mdx +++ b/src/pages/docs/quickstart/predeployed-contracts.mdx @@ -10,14 +10,14 @@ Core protocol contracts that power Tempo's features. | Contract | Address | Description | |----------|---------|-------------| -| [**TIP-20 Factory**](/protocol/tip20/overview) | [`0x20fc000000000000000000000000000000000000`](https://explore.tempo.xyz/address/0x20fc000000000000000000000000000000000000) | Create new TIP-20 tokens | -| [**Fee Manager**](/protocol/fees/spec-fee-amm#2-feemanager-contract) | [`0xfeec000000000000000000000000000000000000`](https://explore.tempo.xyz/address/0xfeec000000000000000000000000000000000000) | Handle fee payments and conversions | -| [**Stablecoin DEX**](/protocol/exchange) | [`0xdec0000000000000000000000000000000000000`](https://explore.tempo.xyz/address/0xdec0000000000000000000000000000000000000) | Enshrined DEX for stablecoin swaps | -| [**TIP-403 Registry**](/protocol/tip403/spec) | [`0x403c000000000000000000000000000000000000`](https://explore.tempo.xyz/address/0x403c000000000000000000000000000000000000) | Transfer policy registry | -| [**ReceivePolicyGuard**](/protocol/upgrades/t6#account-level-receive-policies) | [`0xB10C000000000000000000000000000000000000`](https://explore.testnet.tempo.xyz/address/0xB10C000000000000000000000000000000000000) | Holds TIP-20 transfers and mints blocked by account-level receive policies on T6 networks | +| [**TIP-20 Factory**](/docs/protocol/tip20/overview) | [`0x20fc000000000000000000000000000000000000`](https://explore.tempo.xyz/address/0x20fc000000000000000000000000000000000000) | Create new TIP-20 tokens | +| [**Fee Manager**](/docs/protocol/fees/spec-fee-amm#2-feemanager-contract) | [`0xfeec000000000000000000000000000000000000`](https://explore.tempo.xyz/address/0xfeec000000000000000000000000000000000000) | Handle fee payments and conversions | +| [**Stablecoin DEX**](/docs/protocol/exchange) | [`0xdec0000000000000000000000000000000000000`](https://explore.tempo.xyz/address/0xdec0000000000000000000000000000000000000) | Enshrined DEX for stablecoin swaps | +| [**TIP-403 Registry**](/docs/protocol/tip403/spec) | [`0x403c000000000000000000000000000000000000`](https://explore.tempo.xyz/address/0x403c000000000000000000000000000000000000) | Transfer policy registry | +| [**ReceivePolicyGuard**](/docs/protocol/upgrades/t6#account-level-receive-policies) | [`0xB10C000000000000000000000000000000000000`](https://explore.testnet.tempo.xyz/address/0xB10C000000000000000000000000000000000000) | Holds TIP-20 transfers and mints blocked by account-level receive policies on T6 networks | | [**Signature Verifier**](https://github.com/tempoxyz/tempo/blob/main/tips/tip-1020.md) | [`0x5165300000000000000000000000000000000000`](https://explore.tempo.xyz/address/0x5165300000000000000000000000000000000000) | Verify secp256k1, P256, and WebAuthn signatures onchain | | [**Address Registry**](https://github.com/tempoxyz/tempo/blob/main/tips/tip-1022.md) | [`0xFDC0000000000000000000000000000000000000`](https://explore.tempo.xyz/address/0xFDC0000000000000000000000000000000000000) | Resolve virtual TIP-20 deposit addresses to registered master wallets | -| [**pathUSD**](/protocol/exchange/quote-tokens#pathusd) | [`0x20c0000000000000000000000000000000000000`](https://explore.tempo.xyz/address/0x20c0000000000000000000000000000000000000) | First stablecoin deployed | +| [**pathUSD**](/docs/protocol/exchange/quote-tokens#pathusd) | [`0x20c0000000000000000000000000000000000000`](https://explore.tempo.xyz/address/0x20c0000000000000000000000000000000000000) | First stablecoin deployed | ## Standard Utilities diff --git a/src/pages/quickstart/tokenlist.mdx b/src/pages/docs/quickstart/tokenlist.mdx similarity index 98% rename from src/pages/quickstart/tokenlist.mdx rename to src/pages/docs/quickstart/tokenlist.mdx index f3aa1c05..d265cec8 100644 --- a/src/pages/quickstart/tokenlist.mdx +++ b/src/pages/docs/quickstart/tokenlist.mdx @@ -5,7 +5,7 @@ description: Query token metadata, icons, and prices on Tempo using the Uniswap --- import { Callout } from 'vocs' -import { TokenListDemo } from '../../components/TokenList.tsx' +import { TokenListDemo } from '../../../components/TokenList.tsx' # Tempo Token List Registry diff --git a/src/pages/quickstart/verify-contracts.mdx b/src/pages/docs/quickstart/verify-contracts.mdx similarity index 98% rename from src/pages/quickstart/verify-contracts.mdx rename to src/pages/docs/quickstart/verify-contracts.mdx index 226b5a99..130dc224 100644 --- a/src/pages/quickstart/verify-contracts.mdx +++ b/src/pages/docs/quickstart/verify-contracts.mdx @@ -233,7 +233,7 @@ View the full API documentation at [contracts.tempo.xyz/docs](https://contracts. ## Troubleshooting :::tip -If you encounter unexpected failures, you might be running an older version of Foundry/Forge. See the [Foundry setup guide](/sdk/foundry) for installation instructions. +If you encounter unexpected failures, you might be running an older version of Foundry/Forge. See the [Foundry setup guide](/docs/sdk/foundry) for installation instructions. ::: ### Verification Failed diff --git a/src/pages/quickstart/wallet-developers.mdx b/src/pages/docs/quickstart/wallet-developers.mdx similarity index 75% rename from src/pages/quickstart/wallet-developers.mdx rename to src/pages/docs/quickstart/wallet-developers.mdx index 13b382c3..4c0cc59d 100644 --- a/src/pages/quickstart/wallet-developers.mdx +++ b/src/pages/docs/quickstart/wallet-developers.mdx @@ -7,11 +7,11 @@ import { Cards, Card } from 'vocs' # Wallet Developer Guide -Tempo is EVM-compatible, so standard transactions work out of the box. However, Tempo has [no native gas token](/quickstart/evm-compatibility#handling-eth-native-token-balance-checks), which means wallet behaviors like balance display and gas quoting need adjustment. +Tempo is EVM-compatible, so standard transactions work out of the box. However, Tempo has [no native gas token](/docs/quickstart/evm-compatibility#handling-eth-native-token-balance-checks), which means wallet behaviors like balance display and gas quoting need adjustment. -To deliver the best experience for your users, integrate [Tempo Transactions](/guide/tempo-transaction) — a protocol-native [EIP-2718](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-2718.md) transaction type (type byte `0x76`) that provides fee token selection, fee sponsorship, call batching, concurrent nonces, passkey signing, and scheduled execution — without requiring a bundler, paymaster, or third-party vendor. +To deliver the best experience for your users, integrate [Tempo Transactions](/docs/guide/tempo-transaction) — a protocol-native [EIP-2718](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-2718.md) transaction type (type byte `0x76`) that provides fee token selection, fee sponsorship, call batching, concurrent nonces, passkey signing, and scheduled execution — without requiring a bundler, paymaster, or third-party vendor. -[SDKs](/guide/tempo-transaction#integration-guides) are available for TypeScript, Rust, Go, Python, and Foundry. Integration typically takes less than an hour. +[SDKs](/docs/guide/tempo-transaction#integration-guides) are available for TypeScript, Rust, Go, Python, and Foundry. Integration typically takes less than an hour. ## Steps @@ -19,7 +19,7 @@ To deliver the best experience for your users, integrate [Tempo Transactions](/g ### Integrate Tempo Transactions -Replace your wallet's transaction construction with Tempo Transactions. The minimum change is switching from a type-2 (EIP-1559) envelope to a type-`0x76` Tempo Transaction envelope using one of the [Tempo SDKs](/guide/tempo-transaction#integration-guides). +Replace your wallet's transaction construction with Tempo Transactions. The minimum change is switching from a type-2 (EIP-1559) envelope to a type-`0x76` Tempo Transaction envelope using one of the [Tempo SDKs](/docs/guide/tempo-transaction#integration-guides). :::code-group @@ -44,12 +44,10 @@ const { receipt } = await client.token.transferSync({ :::tip With Tempo Transactions, you can also: -- Set the fee token for your users' transactions ([guide](/guide/payments/pay-fees-in-any-stablecoin)) -- Sponsor transaction fees for your users ([guide](/guide/payments/sponsor-user-fees)) -- Send concurrent transactions with independent nonces ([guide](/guide/payments/send-parallel-transactions)) -- Batch multiple calls into a single atomic transaction ([guide](/guide/use-accounts/batch-transactions)) -- Sign with passkeys and P256 keys ([guide](/guide/use-accounts/webauthn-p256-signatures)) -- Use expiring nonces for cheaper transactions that don't require nonce tracking ([guide](/guide/tempo-transaction#expiring-nonces)) +- Set the fee token for your users' transactions ([guide](/docs/guide/payments/pay-fees-in-any-stablecoin)) +- Sponsor transaction fees for your users ([guide](/docs/guide/payments/sponsor-user-fees)) +- Send concurrent transactions with independent nonces ([guide](/docs/guide/payments/send-parallel-transactions)) +- Use expiring nonces for cheaper transactions that don't require nonce tracking ([guide](/docs/guide/tempo-transaction#expiring-nonces)) ::: ### Handle the absence of a native token @@ -57,7 +55,7 @@ With Tempo Transactions, you can also: If you use `eth_getBalance` to validate a user's balance, you should instead check the user's account fee token balance on Tempo. Additionally, you should not display any "native balance" in your UI for Tempo users. :::info -In testnet, `eth_getBalance` [returns a large placeholder value](/quickstart/evm-compatibility#handling-eth-native-token-balance-checks) for the native token balance to unblock existing assumptions wallets have about the native token balance. +In testnet, `eth_getBalance` [returns a large placeholder value](/docs/quickstart/evm-compatibility#handling-eth-native-token-balance-checks) for the native token balance to unblock existing assumptions wallets have about the native token balance. ::: :::code-group @@ -93,7 +91,7 @@ On Tempo, users can pay fees in any supported stablecoin. You should quote gas/f :::info As a wallet developer, you can set the fee token for your user at the account level. -If you don't, Tempo uses a cascading fee token selection algorithm to determine the fee token for a transaction – learn more about [Fee Token Preferences](/protocol/fees/spec-fee#fee-token-preferences). +If you don't, Tempo uses a cascading fee token selection algorithm to determine the fee token for a transaction – learn more about [Fee Token Preferences](/docs/protocol/fees/spec-fee#fee-token-preferences). ::: ### Add fee token selection to your UI @@ -131,7 +129,7 @@ await client.fee.setUserTokenSync({ }) ``` -See [Fee Token Preferences](/protocol/fees/spec-fee#fee-token-preferences) for the full cascading resolution order. +See [Fee Token Preferences](/docs/protocol/fees/spec-fee#fee-token-preferences) for the full cascading resolution order. ### Display token and network assets @@ -144,7 +142,7 @@ Tempo provides a public tokenlist service that hosts token and network assets. Y ## Already using EIP-7702 or EIP-4337? -If you've integrated a third-party account abstraction provider for batching, sponsorship, or smart accounts, Tempo Transactions provide these features natively at the protocol level. See the [feature comparison](/protocol/transactions/eip-7702#feature-comparison) for details. +If you've integrated a third-party account abstraction provider for batching, sponsorship, or smart accounts, Tempo Transactions provide these features natively at the protocol level. See the [feature comparison](/docs/protocol/transactions/eip-7702#feature-comparison) for details. ## Recipes @@ -202,8 +200,8 @@ Before launching Tempo support, ensure your wallet: - [ ] Quotes gas prices in the user's fee token - [ ] Provides fee token selection in the UI (dropdown or account setting) - [ ] Pulls token/network assets from Tempo's tokenlist -- [ ] (Recommended) Sponsors fees for your users via [fee sponsorship](/guide/payments/sponsor-user-fees) -- [ ] (Recommended) Uses [expiring nonces](/guide/tempo-transaction#expiring-nonces) for lower-cost transactions that don't require nonce management +- [ ] (Recommended) Sponsors fees for your users via [fee sponsorship](/docs/guide/payments/sponsor-user-fees) +- [ ] (Recommended) Uses [expiring nonces](/docs/guide/tempo-transaction#expiring-nonces) for lower-cost transactions that don't require nonce management ## Learning Resources @@ -212,30 +210,30 @@ Before launching Tempo support, ensure your wallet: description="Integrate Tempo Transactions for full control over transaction parameters" icon="lucide:coins" title="Tempo Transactions" - to="/guide/tempo-transaction" + to="/docs/guide/tempo-transaction" /> diff --git a/src/pages/sdk/foundry/index.mdx b/src/pages/docs/sdk/foundry/index.mdx similarity index 97% rename from src/pages/sdk/foundry/index.mdx rename to src/pages/docs/sdk/foundry/index.mdx index 093de9c2..436df4c2 100644 --- a/src/pages/sdk/foundry/index.mdx +++ b/src/pages/docs/sdk/foundry/index.mdx @@ -6,7 +6,7 @@ description: Build, test, and deploy smart contracts on Tempo using Foundry. Acc Tempo is supported as a first-class citizen in [Foundry](https://github.com/foundry-rs/foundry): the leading Ethereum development toolkit. -Install the latest Foundry release to access Tempo's [protocol-level features](/protocol) in `forge`, `cast`, `anvil`, and `chisel`, and to build, test, and deploy contracts that go [beyond the limits of standard EVM chains](/quickstart/evm-compatibility). +Install the latest Foundry release to access Tempo's [protocol-level features](/docs/protocol) in `forge`, `cast`, `anvil`, and `chisel`, and to build, test, and deploy contracts that go [beyond the limits of standard EVM chains](/docs/quickstart/evm-compatibility). :::warning[`tempo-foundry` is deprecated] `tempo-foundry` and `foundryup -n tempo` are deprecated. Switch to the latest upstream Foundry release with `foundryup`. @@ -199,7 +199,7 @@ forge script script/Deploy.s.sol \ Use a root key for `forge create`. Access keys can sign calls but not deployments. -For more verification options including verifying existing contracts and API verification, see [Contract Verification](/quickstart/verify-contracts). +For more verification options including verifying existing contracts and API verification, see [Contract Verification](/docs/quickstart/verify-contracts). :::warning[Batch Transaction Rules] - **Atomic execution**: If any call reverts, the entire batch reverts @@ -363,7 +363,7 @@ Ledger and Trezor wallets are not yet compatible with any `--tempo.*` option. ## cast keychain -`cast keychain` provides a CLI interface to Tempo's [Account Keychain precompile](/protocol/transactions/AccountKeychain). +`cast keychain` provides a CLI interface to Tempo's [Account Keychain precompile](/docs/protocol/transactions/AccountKeychain). Prefer this over hand-encoding `authorizeKey(...)` calldata when you are working from the CLI. diff --git a/src/pages/sdk/foundry/mpp.mdx b/src/pages/docs/sdk/foundry/mpp.mdx similarity index 97% rename from src/pages/sdk/foundry/mpp.mdx rename to src/pages/docs/sdk/foundry/mpp.mdx index df5ffa30..4d078a69 100644 --- a/src/pages/sdk/foundry/mpp.mdx +++ b/src/pages/docs/sdk/foundry/mpp.mdx @@ -22,7 +22,7 @@ Every Foundry tool works transparently with MPP-gated endpoints: ## How it works -When you point any Foundry tool at an MPP-gated RPC URL, the built-in transport intercepts `402` responses and resolves them using MPP's [session flow](/guide/machine-payments/pay-as-you-go): +When you point any Foundry tool at an MPP-gated RPC URL, the built-in transport intercepts `402` responses and resolves them using MPP's [session flow](/docs/guide/machine-payments/pay-as-you-go): 1. **First request** — Foundry sends a normal JSON-RPC request to the endpoint. 2. **402 challenge** — The server responds with `402 Payment Required` and a `WWW-Authenticate: Payment` header describing the price. @@ -237,18 +237,18 @@ Some MPP endpoints sponsor gas fees on behalf of the caller. When the server's c icon="lucide:user" title="Client quickstart" description="Handle payment-gated resources with the TypeScript SDK" - to="/guide/machine-payments/client" + to="/docs/guide/machine-payments/client" /> diff --git a/src/pages/sdk/foundry/signature-verifier.mdx b/src/pages/docs/sdk/foundry/signature-verifier.mdx similarity index 96% rename from src/pages/sdk/foundry/signature-verifier.mdx rename to src/pages/docs/sdk/foundry/signature-verifier.mdx index 23fdae40..4c2df048 100644 --- a/src/pages/sdk/foundry/signature-verifier.mdx +++ b/src/pages/docs/sdk/foundry/signature-verifier.mdx @@ -5,7 +5,7 @@ description: Verify secp256k1, P256, and WebAuthn signatures in smart contracts # Signature Verification with Foundry -The [TIP-1020](https://github.com/tempoxyz/tempo/blob/main/tips/tip-1020.md) `SignatureVerifier` precompile lets contracts verify secp256k1, P256, and WebAuthn signatures through a single interface — no custom verifier contracts needed. This is available after the [T3 network upgrade](/protocol/upgrades/t3). +The [TIP-1020](https://github.com/tempoxyz/tempo/blob/main/tips/tip-1020.md) `SignatureVerifier` precompile lets contracts verify secp256k1, P256, and WebAuthn signatures through a single interface — no custom verifier contracts needed. This is available after the [T3 network upgrade](/docs/protocol/upgrades/t3). The Foundry project template for Tempo ships with a working example that demonstrates signature verification in a relayed mail contract. Initialize it with: @@ -141,6 +141,6 @@ The secp256k1 and P256 relay tests run against the T3 hardfork automatically via ## Related - [TIP-1020: Signature Verification Precompile](https://github.com/tempoxyz/tempo/blob/main/tips/tip-1020.md) -- [T3 Network Upgrade](/protocol/upgrades/t3) -- [T6 Network Upgrade](/protocol/upgrades/t6) -- [Foundry for Tempo](/sdk/foundry) +- [T3 Network Upgrade](/docs/protocol/upgrades/t3) +- [T6 Network Upgrade](/docs/protocol/upgrades/t6) +- [Foundry for Tempo](/docs/sdk/foundry) diff --git a/src/pages/sdk/go/index.mdx b/src/pages/docs/sdk/go/index.mdx similarity index 96% rename from src/pages/sdk/go/index.mdx rename to src/pages/docs/sdk/go/index.mdx index 37dc9324..c5b1f4e3 100644 --- a/src/pages/sdk/go/index.mdx +++ b/src/pages/docs/sdk/go/index.mdx @@ -287,10 +287,10 @@ for _, resp := range responses { ## Account Keychain -The `keychain` package provides typed helpers for Tempo's [Account Keychain precompile](/protocol/transactions/AccountKeychain), enabling access key management and signing directly from Go. +The `keychain` package provides typed helpers for Tempo's [Account Keychain precompile](/docs/protocol/transactions/AccountKeychain), enabling access key management and signing directly from Go. :::info -Enhanced access key features — periodic spending limits and call scoping — require the [T3 network upgrade](/protocol/upgrades/t3). +Enhanced access key features — periodic spending limits and call scoping — require the [T3 network upgrade](/docs/protocol/upgrades/t3). ::: ```go [keychain_manage.go] @@ -420,5 +420,5 @@ fmt.Printf("Remaining: %s\n", remaining.String()) After setting up the Go SDK, you can: -- Follow a guide on how to [use accounts](/guide/use-accounts), [make payments](/guide/payments), [issue stablecoins](/guide/issuance), [exchange stablecoins](/guide/stablecoin-dex), and [more](/). +- Follow a guide on how to [make payments](/docs/guide/payments), [issue stablecoins](/docs/guide/issuance), [exchange stablecoins](/docs/guide/stablecoin-dex), and [more](/docs). - View the [examples on GitHub](https://github.com/tempoxyz/tempo-go/tree/main/examples) diff --git a/src/pages/sdk/index.mdx b/src/pages/docs/sdk/index.mdx similarity index 87% rename from src/pages/sdk/index.mdx rename to src/pages/docs/sdk/index.mdx index 0bda17cd..48ba8917 100644 --- a/src/pages/sdk/index.mdx +++ b/src/pages/docs/sdk/index.mdx @@ -11,28 +11,28 @@ Tempo is building clients in multiple languages to make integration as easy as p diff --git a/src/pages/sdk/python/index.mdx b/src/pages/docs/sdk/python/index.mdx similarity index 96% rename from src/pages/sdk/python/index.mdx rename to src/pages/docs/sdk/python/index.mdx index 7fdbbc53..f07aef54 100644 --- a/src/pages/sdk/python/index.mdx +++ b/src/pages/docs/sdk/python/index.mdx @@ -216,10 +216,10 @@ tx = TempoTransaction.create( ## Account Keychain -The `AccountKeychain` class provides typed helpers for Tempo's [Account Keychain precompile](/protocol/transactions/AccountKeychain), enabling access key management directly from Python. +The `AccountKeychain` class provides typed helpers for Tempo's [Account Keychain precompile](/docs/protocol/transactions/AccountKeychain), enabling access key management directly from Python. :::info -Enhanced access key features — periodic spending limits and call scoping — require the [T3 network upgrade](/protocol/upgrades/t3). +Enhanced access key features — periodic spending limits and call scoping — require the [T3 network upgrade](/docs/protocol/upgrades/t3). ::: ```python [keychain.py] @@ -354,6 +354,6 @@ tx_hash = w3.eth.send_raw_transaction(signed_tx.encode()) After setting up the Python SDK, you can: -- Follow a guide on how to [use accounts](/guide/use-accounts), [make payments](/guide/payments), [issue stablecoins](/guide/issuance), [exchange stablecoins](/guide/stablecoin-dex), and [more](/). +- Follow a guide on how to [make payments](/docs/guide/payments), [issue stablecoins](/docs/guide/issuance), [exchange stablecoins](/docs/guide/stablecoin-dex), and [more](/docs). - View the [source on GitHub](https://github.com/tempoxyz/pytempo) - View the [package on PyPI](https://pypi.org/project/pytempo/) diff --git a/src/pages/sdk/rust/index.mdx b/src/pages/docs/sdk/rust/index.mdx similarity index 100% rename from src/pages/sdk/rust/index.mdx rename to src/pages/docs/sdk/rust/index.mdx diff --git a/src/pages/sdk/typescript/index.mdx b/src/pages/docs/sdk/typescript/index.mdx similarity index 87% rename from src/pages/sdk/typescript/index.mdx rename to src/pages/docs/sdk/typescript/index.mdx index 82985acc..67d30a0d 100644 --- a/src/pages/sdk/typescript/index.mdx +++ b/src/pages/docs/sdk/typescript/index.mdx @@ -38,11 +38,7 @@ The Tempo extensions cover common chain operations such as querying state, sendi ## Viem -The T3-compatible `viem` release includes the updated access-key ABI, including periodic limits and call scoping. See the [T3 network upgrade](/protocol/upgrades/t3) for migration details. - -:::note -The Accounts SDK and wallet RPC docs still describe the legacy pre-T3 access-key request shape for now. Use the protocol specs or the T3-compatible `viem` release for direct post-T3 authorization encoding until those docs are updated. -::: +The T3-compatible `viem` release includes the updated access-key ABI, including periodic limits and call scoping. See the [T3 network upgrade](/docs/protocol/upgrades/t3) for migration details. tempo wallet fund ``` -If issues persist, continue with [Troubleshooting](/cli/wallet). +If issues persist, continue with [Troubleshooting](/docs/cli/wallet). ## End-to-End Script Pattern @@ -129,6 +129,6 @@ tempo request --json '{"input":"hello"}' ## See Also -1. [Reference](/wallet/reference) -2. [CLI Reference](/cli/wallet) -3. [Use with Agents](/wallet/use-with-agents) +1. [Reference](/docs/wallet/reference) +2. [CLI Reference](/docs/cli/wallet) +3. [Use with Agents](/docs/wallet/use-with-agents) diff --git a/src/pages/wallet/reference.mdx b/src/pages/docs/wallet/reference.mdx similarity index 94% rename from src/pages/wallet/reference.mdx rename to src/pages/docs/wallet/reference.mdx index 5d946948..81a89805 100644 --- a/src/pages/wallet/reference.mdx +++ b/src/pages/docs/wallet/reference.mdx @@ -44,7 +44,7 @@ The service directory indexes [MPP](https://mpp.dev)-registered providers. Each ### Sessions -Sessions are the local state for [pay-as-you-go](/guide/machine-payments/pay-as-you-go) payment channels. The CLI tracks them locally and can reconcile against onchain state. +Sessions are the local state for [pay-as-you-go](/docs/guide/machine-payments/pay-as-you-go) payment channels. The CLI tracks them locally and can reconcile against onchain state. | Command | Description | | --- | --- | diff --git a/src/pages/wallet/use-with-agents.mdx b/src/pages/docs/wallet/use-with-agents.mdx similarity index 96% rename from src/pages/wallet/use-with-agents.mdx rename to src/pages/docs/wallet/use-with-agents.mdx index 6ea8d57a..f9a92099 100644 --- a/src/pages/wallet/use-with-agents.mdx +++ b/src/pages/docs/wallet/use-with-agents.mdx @@ -41,4 +41,4 @@ This works in supported skill-enabled agents including **Claude Code**, **Amp**, ## Troubleshooting -If agent runs fail, continue with [Troubleshooting](/cli/wallet). +If agent runs fail, continue with [Troubleshooting](/docs/cli/wallet). diff --git a/src/pages/guide/_template.mdx b/src/pages/guide/_template.mdx deleted file mode 100644 index 00d7468a..00000000 --- a/src/pages/guide/_template.mdx +++ /dev/null @@ -1,135 +0,0 @@ ---- -searchable: false -interactive: true ---- - -import { Cards, Card } from 'vocs' -import * as Demo from '../../components/guides/Demo.tsx' -import { AddFunds } from '../../components/guides/steps/payments/AddFunds.tsx' -import { Connect } from '../../components/guides/steps/auth/Connect.tsx' -import { CreateToken } from '../../components/guides/steps/issuance/CreateToken.tsx' - -# !Replace Me! - -{/* Short description of the guide. What we are building, why we are building it, and what we will learn. */} - -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam elementum odio ante, sit amet tincidunt leo scelerisque vitae. - - - - - - - -## Steps - -{/* Steps to follow to get the demo working and fully functioning end-to-end */} - -### Lorem - -Lorem ipsum dolor sit amet, consectetur adipiscing elit. - -:::code-group - -```tsx twoslash [Component.tsx] -// @noErrors -export function Component() { - return ( -
- {/* Component code here */} -
- ) -} -``` - -```tsx twoslash [config.ts] filename="config.ts" -// @noErrors -import { createConfig, http } from 'wagmi' -import { tempo } from 'viem/chains' -import { tempoWallet } from 'wagmi/connectors' - -export const config = createConfig({ - chains: [tempo], - connectors: [tempoWallet()], - transports: { - [tempo.id]: http(), - }, -}) -``` - -::: - -### Ipsum - -Lorem ipsum dolor sit amet, consectetur adipiscing elit. - -:::code-group - -```tsx twoslash [Component.tsx] -// @noErrors -export function Component() { - // Component code here -} -``` - -```tsx twoslash [config.ts] filename="config.ts" -// @noErrors -import { createConfig, http } from 'wagmi' -import { tempo } from 'viem/chains' -import { tempoWallet } from 'wagmi/connectors' - -export const config = createConfig({ - chains: [tempo], - connectors: [tempoWallet()], - transports: { - [tempo.id]: http(), - }, -}) -``` - -::: - -:::: - -## Recipes - -{/* Any peripheral things you can do beyond the above steps */} - -### Foo - -Lorem ipsum dolor sit amet, consectetur adipiscing elit. - -### Bar - -Lorem ipsum dolor sit amet, consectetur adipiscing elit. - -## Best Practices - -{/* Any best practices or tips to follow when using this feature */} - -### Foo - -Lorem ipsum dolor sit amet, consectetur adipiscing elit. - -### Bar - -Lorem ipsum dolor sit amet, consectetur adipiscing elit. - -## Learning Resources - -{/* Outlink to any useful links to read in /documentation/* */} - - - - - diff --git a/src/pages/guide/node/questions.txt b/src/pages/guide/node/questions.txt deleted file mode 100644 index 587e5853..00000000 --- a/src/pages/guide/node/questions.txt +++ /dev/null @@ -1,7 +0,0 @@ -1. how do I know my validator's status -2. how do i know if my validator is syncing -3. why is my new validator stuck -4. what hardware do i need for my validator (e.g. what instance on AWS, OVH) -5. what metrics do I need to monitor -6. can I backup my validator's state? -7. how do I recover my validator from X state? diff --git a/src/pages/guide/private-zones/connect-to-a-zone.mdx b/src/pages/guide/private-zones/connect-to-a-zone.mdx deleted file mode 100644 index 6813ff78..00000000 --- a/src/pages/guide/private-zones/connect-to-a-zone.mdx +++ /dev/null @@ -1,59 +0,0 @@ ---- -title: Connect to a Zone -description: Connect to Tempo Zones on testnet using Zone A and Zone B RPC URLs, chain IDs, and a minimal viem client setup for private flows. ---- - -# Connect to a Zone - -:::info -Tempo Zones is still in early development and is available for testing purposes on Tempo Testnet only. While Tempo Zones are in this stage, expect breaking changes to the design and implementation. Do not use this in production. If you're interested in working with Tempo Labs as a design partner on the development of Tempo Zones, contact us at [tempo.xyz/contact](https://tempo.xyz/contact). -::: - -Use this page when you need the RPC endpoint and chain metadata for `Zone A` or `Zone B`. - -Account-scoped zone RPC methods require an `X-Authorization-Token` header signed by the Tempo account you are using. The interactive guides handle that for you automatically. If you are building your own integration, see the [Zone RPC specification](/protocol/zones/rpc) for the token format and the list of scoped methods. - -## Create a Viem client - -Use `zoneModerato(...)` from `viem/tempo/zones` so the client has the correct chain metadata for the zone you want to reach. - -```ts -import { createPublicClient } from 'viem' -import { http, zoneModerato } from 'viem/tempo/zones' - -const rpcUrl = 'https://rpc-zone-a.testnet.tempo.xyz' - -const zoneClient = createPublicClient({ - chain: zoneModerato(6), - transport: http(rpcUrl), -}) - -const blockNumber = await zoneClient.getBlockNumber() -console.log(blockNumber) -``` - -## Direct Connection Details - -### Zone A - -| **Property** | **Value** | -|-------------------|-------| -| **Network Name** | Zone A | -| **Zone ID** | `6` | -| **Chain ID** | `4217000006` | -| **HTTP URL** | `https://rpc-zone-a.testnet.tempo.xyz` | -| **Portal Address** | `0x7069DeC4E64Fd07334A0933eDe836C17259c9B23` | -| **Outbox Address** | `0x1c00000000000000000000000000000000000002` | - -### Zone B - -| **Property** | **Value** | -|-------------------|-------| -| **Network Name** | Zone B | -| **Zone ID** | `7` | -| **Chain ID** | `4217000007` | -| **HTTP URL** | `https://rpc-zone-b.testnet.tempo.xyz` | -| **Portal Address** | `0x3F5296303400B56271b476F5A0B9cBF74350D6Ac` | -| **Outbox Address** | `0x1c00000000000000000000000000000000000002` | - -Zones do not expose a public block explorer for private activity. Use authenticated RPC reads instead. diff --git a/src/pages/guide/private-zones/deposit-to-a-zone.mdx b/src/pages/guide/private-zones/deposit-to-a-zone.mdx deleted file mode 100644 index a66e318a..00000000 --- a/src/pages/guide/private-zones/deposit-to-a-zone.mdx +++ /dev/null @@ -1,94 +0,0 @@ ---- -title: Deposit to a Zone -description: Deposit pathUSD from your public-chain balance into Zone A and confirm the resulting zone balance. -interactive: true ---- - -import * as Demo from '../../../components/guides/Demo.tsx' -import { Tab, Tabs } from 'vocs' -import { DepositToZone } from '../../../components/guides/zones/DepositToZone.tsx' - -export const depositZoneBalances = [{ label: 'Zone A', token: Demo.pathUsd, zone: 6 }] - -# Deposit to a Zone - -:::info -Tempo Zones is still in early development and is available for testing purposes on Tempo Testnet only. While Tempo Zones are in this stage, expect breaking changes to the design and implementation. Do not use this in production. If you're interested in working with Tempo Labs as a design partner on the development of Tempo Zones, contact us at [tempo.xyz/contact](https://tempo.xyz/contact). -::: - -Use this guide when you want to move `pathUSD` from your public Tempo balance into `Zone A`. You will submit a public-chain deposit first, then wait for `Zone A` to credit the net amount after fees. - -![Zone contract architecture](/learn/zones/diagram-deposit.svg) - -The deposit is accepted through `ZonePortal` on the public chain. You need private zone authorization to read the resulting zone balance, because those reads are only exposed to the authenticated account. - -## Depositing pathUSD to Zone A - -By the end of this guide you will have deposited `pathUSD` into `Zone A` and confirmed the balance update. - - - - - -## Code examples - -These snippets assume you already have a signed-in `rootClient` on the public chain and the usual token and zone constants in scope. -Use the plaintext flow when revealing the recipient and memo is acceptable. Use the encrypted flow when only the zone sequencer should be able to read those fields. - - - - -```ts -import { parseUnits } from 'viem' -import { Actions } from 'viem/tempo' - -const depositAmount = parseUnits('100', 6) - -const { receipt } = await Actions.zone.depositSync(rootClient, { - account: rootClient.account, - amount: depositAmount, - token: pathUsd, - zoneId: ZONE_A.id, -}) - -console.log(receipt.blockNumber) -``` - - - - -```ts -import { parseUnits } from 'viem' -import { Actions } from 'viem/tempo' - -const depositAmount = parseUnits('100', 6) - -const { receipt } = await Actions.zone.encryptedDepositSync(rootClient, { // [!code focus] - account: rootClient.account, - amount: depositAmount, - token: pathUsd, - zoneId: ZONE_A.id, -}) - -console.log(receipt.blockNumber) -``` - - - - -## What Happens During a Deposit - -A zone deposit settles in two phases. - -First, you submit a public Tempo transaction depositing to the `ZonePortal`. The Zone Portal contract locks the token, deducts the deposit fee in the same token, and records the net deposit in its deposit queue. Later, the zone sequencer processes that queue and credits the recipient inside the zone. - -That means your public transaction receipt and your zone balance do not update at the same time. The Tempo transaction confirms that the deposit request was accepted. The zone balance changes only after the zone has processed that deposit, and it reflects the post-fee amount rather than the full amount you passed into `deposit(...)`. - -:::warning - If you need a specific net amount inside the zone, account for the portal deposit fee first. The amount minted on the zone is `amount - depositFee`. -::: diff --git a/src/pages/guide/private-zones/index.mdx b/src/pages/guide/private-zones/index.mdx deleted file mode 100644 index b0ed680b..00000000 --- a/src/pages/guide/private-zones/index.mdx +++ /dev/null @@ -1,74 +0,0 @@ ---- -title: Connect to Tempo Zones -description: Learn how Tempo Zones work alongside the public chain and follow guides for depositing, sending within a zone, routing pathUSD across zones, swapping into betaUSD, and withdrawing. ---- - -import { Card, Cards } from 'vocs' - -# Connect to Tempo Zones - -:::info -Tempo Zones is still in early development and is available for testing purposes on Tempo Testnet only. While Tempo Zones are in this stage, expect breaking changes to the design and implementation. Do not use this in production. If you're interested in working with Tempo Labs as a design partner on the development of Tempo Zones, contact us at [tempo.xyz/contact](https://tempo.xyz/contact). -::: - -Tempo Zones let you keep balances and transfers inside a private execution environment while still using the public Tempo chain when funds enter or leave. The important thing to remember is that most zone flows settle in stages: a public or zone transaction lands first, then the private balance update appears shortly after. - -![Tempo Zones overview](/learn/zones/diagram-overview.svg) - -## Before you start - -- Use a Tempo passkey account in the demo so the page can authorize private zone reads. -- Keep some `pathUSD` on the public chain if you want to try deposits, source-zone top-ups, routed sends, swaps, or withdrawals. -- Expect deposits, routed sends, routed swaps, and withdrawals to complete asynchronously rather than in a single balance update. - -These guides cover the current zone connection setup plus the baseline workflows used in the demos: deposits through `Actions.zone.depositSync(...)` and `Actions.zone.encryptedDepositSync(...)`, in-zone transfers, same-token routed sends through `Actions.zone.requestWithdrawalSync(...)`, routed swaps, direct withdrawals, and authenticated withdrawals through `Actions.zone.requestVerifiableWithdrawalSync(...)`. - -The deposit guide's demo lets you switch between plaintext and encrypted deposits, and the withdrawal guide lets you switch between standard and authenticated withdrawals, while keeping the transaction flow on the upstream `viem` zone actions. - -## Choose the right guide - -- **Connect to a zone** if you want the Zone A and Zone B RPC URLs, chain IDs, and a minimal `viem` client setup. -- **Deposit to a zone** if you want to move `pathUSD` from your public balance into `Zone A`. -- **Send tokens within a zone** if you want to transfer `pathUSD` between private accounts without leaving `Zone A`. -- **Send tokens across zones** if you want to leave `Zone A` with `pathUSD` and arrive in `Zone B` with the same token. -- **Swap across zones** if you want to leave `Zone A` with `pathUSD` and arrive in `Zone B` with `betaUSD`. -- **Withdraw from a zone** if you want to move `pathUSD` back from `Zone A` to your public balance. - - - - - - - - - diff --git a/src/pages/guide/private-zones/send-tokens-across-zones.mdx b/src/pages/guide/private-zones/send-tokens-across-zones.mdx deleted file mode 100644 index 1354e2b5..00000000 --- a/src/pages/guide/private-zones/send-tokens-across-zones.mdx +++ /dev/null @@ -1,91 +0,0 @@ ---- -title: Send tokens across zones -description: Send pathUSD from Zone A into Zone B by routing a same-token withdrawal through Tempo's L1 router and confirming the target deposit. -interactive: true ---- - -import * as Demo from '../../../components/guides/Demo.tsx' -import { SendTokensAcrossZones } from '../../../components/guides/zones/SendTokensAcrossZones.tsx' - -export const crossZoneBalances = [ - { label: 'Zone A', token: Demo.pathUsd, zone: 6 }, - { label: 'Zone B', token: Demo.pathUsd, zone: 7 }, -] - -# Send tokens across zones - -:::info -Tempo Zones is still in early development and is available for testing purposes on Tempo Testnet only. While Tempo Zones are in this stage, expect breaking changes to the design and implementation. Do not use this in production. If you're interested in working with Tempo Labs as a design partner on the development of Tempo Zones, contact us at [tempo.xyz/contact](https://tempo.xyz/contact). -::: - -Use this guide when you want to move `pathUSD` from `Zone A` into `Zone B` without changing the token. The route still touches the public chain, so the confirmation happens in stages rather than as a single balance update. - -The flow uses `swapAndDepositRouter` on the public chain in same-token mode: withdraw from `Zone A`, skip the swap because the asset stays as `pathUSD`, then deposit that `pathUSD` into `Zone B`. - -## Sending pathUSD from Zone A into Zone B - -By the end of this guide you will have sent **25 pathUSD** from **Zone A** into **Zone B** and confirmed the routed deposit. - - - - - -## Code example - -This snippet assumes you already have a signed-in `rootClient` on the public chain plus `zoneAClient`, and the shared token, router, and portal constants used throughout the zone guides. - -It shows the core routed send submission path; use the demo above when you want to watch the routed deposit settle into Zone B. - -```ts -import { encodeAbiParameters, parseUnits } from 'viem' -import { Actions } from 'viem/tempo' - -const transferAmount = parseUnits('25', 6) - -await zoneAClient.zone.signAuthorizationToken() - -const callbackData = encodeAbiParameters( - [ - { type: 'bool' }, - { type: 'address' }, - { type: 'address' }, - { type: 'address' }, - { type: 'bytes32' }, - { type: 'uint128' }, - ], - [false, pathUsd, ZONE_B.portalAddress, rootClient.account.address, zeroBytes32, 0n], -) - -const { receipt } = await Actions.zone.requestWithdrawalSync(zoneAClient, { - account: rootClient.account, - amount: transferAmount, - data: callbackData, - feeToken: pathUsd, - gas: routerCallbackGasLimit, - timeout: zoneRpcSyncTimeout, - to: swapAndDepositRouter, - token: pathUsd, -}) - -console.log(receipt.blockNumber) -``` - -## What this routed send does - -The cross-zone transfer path looks like this: the token leaves `Zone A`, briefly lands on the public chain, and is deposited back into `Zone B` as the same asset. - -1. Withdraws `pathUSD` from `Zone A` through `ZoneOutbox`. -2. Routes that withdrawal to `swapAndDepositRouter` on Tempo. -3. Skips the DEX swap because the input and output token are both `pathUSD`. -4. Deposits the routed `pathUSD` into `Zone B` through `ZonePortal`. - -The target deposit still pays the normal portal deposit fee, so the amount that arrives in `Zone B` is the routed `pathUSD` minus that fee. - -:::warning - If the routed withdrawal fails on Tempo—for example because the callback reverts or the target deposit cannot be completed—the amount is bounced back to the withdrawal's `fallbackRecipient` inside `Zone A`. The fee is still paid to the sequencer. -::: diff --git a/src/pages/guide/private-zones/send-tokens-within-a-zone.mdx b/src/pages/guide/private-zones/send-tokens-within-a-zone.mdx deleted file mode 100644 index 3b54f2c0..00000000 --- a/src/pages/guide/private-zones/send-tokens-within-a-zone.mdx +++ /dev/null @@ -1,57 +0,0 @@ ---- -title: Send tokens within a zone -description: Send pathUSD inside Zone A with a signed zone transfer and confirm the updated zone balance. -interactive: true ---- - -import * as Demo from '../../../components/guides/Demo.tsx' -import { SendTokensWithinZone } from '../../../components/guides/zones/SendTokensWithinZone.tsx' - -export const inZoneBalances = [{ label: 'Zone A', token: Demo.pathUsd, zone: 6 }] - -# Send tokens within a zone - -:::info -Tempo Zones is still in early development and is available for testing purposes on Tempo Testnet only. While Tempo Zones are in this stage, expect breaking changes to the design and implementation. Do not use this in production. If you're interested in working with Tempo Labs as a design partner on the development of Tempo Zones, contact us at [tempo.xyz/contact](https://tempo.xyz/contact). -::: - -Use this guide when you want to send `pathUSD` from one private `Zone A` balance to another without moving funds back through the public chain. - -Zone tokens use the `TIP20` token interface, so once you authorize private reads for the session, an in-zone transfer looks much like a normal token transfer. - -## Sending pathUSD within Zone A - -By the end of this guide you will have sent `25 pathUSD` inside `Zone A` and confirmed the updated balance. - - - - - -## Code example - -This snippet assumes you already have a signed-in `rootClient` on the public chain and a derived `zoneAClient`. - -It shows the core zone transfer path; use the demo above when you want to watch the updated zone balance. - -```ts -import { parseUnits, type Address } from 'viem' -import { Actions } from 'viem/tempo' - -const transferAmount = parseUnits('25', 6) -const demoRecipient = '0xbeefcafe54750903ac1c8909323af7beb21ea2cb' as Address - -await zoneAClient.zone.signAuthorizationToken() - -const { receipt } = await Actions.token.transferSync(zoneAClient, { - account: rootClient.account, - amount: transferAmount, - feeToken: pathUsd, - to: demoRecipient, - token: pathUsd, -}) -``` diff --git a/src/pages/guide/private-zones/swap-across-zones.mdx b/src/pages/guide/private-zones/swap-across-zones.mdx deleted file mode 100644 index d8b984a8..00000000 --- a/src/pages/guide/private-zones/swap-across-zones.mdx +++ /dev/null @@ -1,103 +0,0 @@ ---- -title: Swap stablecoins across zones -description: Swap pathUSD from Zone A into betaUSD on Zone B by routing a zone withdrawal through Tempo's L1 router and confirming the target deposit. -interactive: true ---- - -import * as Demo from '../../../components/guides/Demo.tsx' -import { SwapAcrossZones } from '../../../components/guides/zones/SwapAcrossZones.tsx' - -export const swapZoneBalances = [ - { label: 'Zone A', token: Demo.pathUsd, zone: 6 }, - { label: 'Zone B', token: Demo.betaUsd, zone: 7 }, -] - -# Swap across zones - -:::info -Tempo Zones is still in early development and is available for testing purposes on Tempo Testnet only. While Tempo Zones are in this stage, expect breaking changes to the design and implementation. Do not use this in production. If you're interested in working with Tempo Labs as a design partner on the development of Tempo Zones, contact us at [tempo.xyz/contact](https://tempo.xyz/contact). -::: - -Use this guide when you want to leave `Zone A` with `pathUSD` and arrive in `Zone B` with `betaUSD` in one routed flow. The trade briefly touches the public chain, so the confirmation happens in stages rather than as a single balance update. - -The route uses `swapAndDepositRouter` on the public chain: withdraw from `Zone A`, swap on the Stablecoin DEX, then deposit the output token into `Zone B`. - -![Cross-zone DEX swap flow](/learn/zones/diagram-swap.svg) - -## Swapping pathUSD from Zone A into betaUSD on Zone B - -By the end of this guide you will have swapped **25 pathUSD** from **Zone A** into **betaUSD** on **Zone B** and confirmed the routed deposit. - -## What this swap does - -1. Withdraws `pathUSD` from `Zone A`. -2. Routes it through the public chain and swaps it on the Stablecoin DEX. -3. Deposits the output token into `Zone B` through `ZonePortal`. -4. Lets you authorize private reads in `Zone B` so you can confirm the final `betaUSD` balance. - - - - - -## Code example - -This snippet assumes you already have a signed-in `rootClient` on the public chain plus `zoneAClient`, and the shared token, router, and portal constants used throughout the zone guides. -It shows the core routed swap submission path; use the demo above when you want to watch the output deposit settle into Zone B. - -```ts -import { encodeAbiParameters, parseUnits } from 'viem' -import { Actions } from 'viem/tempo' - -const swapAmount = parseUnits('25', 6) - -await zoneAClient.zone.signAuthorizationToken() - -const routedWithdrawalFee = await zoneAClient.zone.getWithdrawalFee({ gas: routerCallbackGasLimit }) -const quotedBetaOut = await rootClient.dex.getSellQuote({ - amountIn: swapAmount, - tokenIn: pathUsd, - tokenOut: betaUsd, -}) - -const minimumBetaOut = quotedBetaOut - quotedBetaOut / 100n - -const callbackData = encodeAbiParameters( - [ - { type: 'bool' }, - { type: 'address' }, - { type: 'address' }, - { type: 'address' }, - { type: 'bytes32' }, - { type: 'uint128' }, - ], - [false, betaUsd, ZONE_B.portalAddress, rootClient.account.address, zeroBytes32, minimumBetaOut], -) - -const { receipt } = await Actions.zone.requestWithdrawalSync(zoneAClient, { - account: rootClient.account, - amount: swapAmount, - data: callbackData, - feeToken: pathUsd, - gas: routerCallbackGasLimit, - timeout: zoneRpcSyncTimeout, - to: swapAndDepositRouter, - token: pathUsd, -}) - -console.log(receipt.blockNumber) -``` - -## How Routed Zone Swaps Settle - -This guide's swap flow is asynchronous because the trade temporarily leaves the zone. - -The source token is withdrawn through `ZoneOutbox`, transferred to `SwapAndDepositRouter` on Tempo, optionally swapped on the Stablecoin DEX, and then deposited back through a `ZonePortal` as the output token. That routed deposit pays the normal portal deposit fee, so the amount that arrives on the zone is the post-fee output. - -:::warning -If the routed withdrawal fails on Tempo - for example because the swap fails, the transfer fails, the router callback reverts, or the target deposit cannot be completed—the amount is bounced back to the withdrawal's `fallbackRecipient` inside the source zone. The fee is still paid to the sequencer, so a failed routed swap still results in fees for the sender. -::: diff --git a/src/pages/guide/private-zones/withdraw-from-a-zone.mdx b/src/pages/guide/private-zones/withdraw-from-a-zone.mdx deleted file mode 100644 index 3cdd3d6a..00000000 --- a/src/pages/guide/private-zones/withdraw-from-a-zone.mdx +++ /dev/null @@ -1,102 +0,0 @@ ---- -title: Withdraw from a Zone -description: Withdraw pathUSD from Zone A back to your public-chain balance with a direct zone outbox withdrawal. -interactive: true ---- - -import * as Demo from '../../../components/guides/Demo.tsx' -import { Tab, Tabs } from 'vocs' -import { WithdrawFromZone } from '../../../components/guides/zones/WithdrawFromZone.tsx' - -export const withdrawalZoneBalances = [{ label: 'Zone A', token: Demo.pathUsd, zone: 6 }] - -# Withdraw from a Zone - -:::info -Tempo Zones is still in early development and is available for testing purposes on Tempo Testnet only. While Tempo Zones are in this stage, expect breaking changes to the design and implementation. Do not use this in production. If you're interested in working with Tempo Labs as a design partner on the development of Tempo Zones, contact us at [tempo.xyz/contact](https://tempo.xyz/contact). -::: - -Use this guide when you want to move `pathUSD` out of `Zone A` and back to your public Tempo balance. - -![Zone contract architecture](/learn/zones/diagram-withdraw.svg) - -Direct withdrawals exit through `ZoneOutbox` on the zone chain. You submit the withdrawal request in the zone first, then wait for the public balance to increase after the batch settles. - -## Withdrawing pathUSD from Zone A - -By the end of this guide you will have withdrawn `pathUSD` from `Zone A` and confirmed the balance update on the public chain. - - - - - -## Code examples - -These snippets assume you already have a signed-in `rootClient` on the public chain, a derived `zoneAClient`, and the usual token constants in scope. -Use the plaintext flow when normal withdrawal visibility is fine. Use the authenticated flow when the sender details should only be revealed to the holder of a `revealTo` public key. - - - - -```ts -import { parseUnits } from 'viem' -import { Actions } from 'viem/tempo' - -const withdrawalAmount = parseUnits('100', 6) - -await zoneAClient.zone.signAuthorizationToken() - -const { receipt } = await Actions.zone.requestWithdrawalSync(zoneAClient, { - account: rootClient.account, - feeToken: pathUsd, - amount: withdrawalAmount, - token: pathUsd, - to: rootClient.account.address, -}) - -console.log(receipt.blockNumber) -``` - - - - -```ts -import { parseUnits } from 'viem' -import { Actions } from 'viem/tempo' - -const withdrawalAmount = parseUnits('100', 6) -const revealTo = '0x031dc147467e8f106eb22850fef549dc74b8f6634aeac554ebdd4ab896b67cdf68' // [!code focus] - -await zoneAClient.zone.signAuthorizationToken() - -const { receipt } = await Actions.zone.requestVerifiableWithdrawalSync(zoneAClient, { // [!code focus] - account: rootClient.account, - feeToken: pathUsd, - amount: withdrawalAmount, - revealTo, // [!code focus] - token: pathUsd, - to: rootClient.account.address, -}) - -console.log(receipt.blockNumber) -``` - - - - -## What a Direct Withdrawal Does - -A direct withdrawal is the simplest way to exit a zone. You ask `ZoneOutbox` on the zone to burn the zone balance, include the request in the next withdrawal batch, and settle the amount back to a public Tempo address. - -Like deposits, withdrawals settle in phases. The request is accepted on the zone first, and the public balance changes later when the sequencer submits and processes the corresponding batch on Tempo. - -If Tempo-side processing fails, the withdrawal does not stay stuck in limbo. The protocol re-deposits the withdrawal amount back into the zone to the request's `fallbackRecipient`. The fee is still consumed. - -:::warning - Even with `gasLimit: 0n`, a direct withdrawal can still fail on Tempo—for example because of token transfer or policy checks. In that case, the amount bounces back to `fallbackRecipient` on the zone instead of increasing the public balance. -::: diff --git a/src/pages/guide/use-accounts/add-funds.mdx b/src/pages/guide/use-accounts/add-funds.mdx deleted file mode 100644 index 2a059596..00000000 --- a/src/pages/guide/use-accounts/add-funds.mdx +++ /dev/null @@ -1,103 +0,0 @@ ---- -description: Get test stablecoins on Tempo Testnet using the faucet. Request pathUSD, AlphaUSD, BetaUSD, and ThetaUSD tokens for development and testing. -mipd: true -interactive: true ---- - -import * as Demo from '../../../components/guides/Demo.tsx' -import { AddFundsToOthers } from '../../../components/guides/steps/payments/AddFundsToOthers.tsx' -import { AddFundsToWallet } from '../../../components/guides/steps/wallet/AddFundsToWallet.tsx' -import { AddTokensToWallet } from '../../../components/guides/steps/wallet/AddTokensToWallet.tsx' -import { ConnectWallet } from '../../../components/guides/steps/wallet/ConnectWallet.tsx' -import { SetFeeToken } from '../../../components/guides/steps/wallet/SetFeeToken.tsx' -import * as Token from '../../../components/guides/tokens' -import { Tabs, Tab } from 'vocs' - -# Add Funds to Your Balance - -Get test tokens to build on Tempo testnet. - - - - -
-Send test stablecoins to any address. - -
- - - - - - - -
- -Connect your wallet to receive test stablecoins directly. - -
- - - - - - - - - - -
- -Request tokens programmatically via the faucet API. - -
- -```bash -curl -X POST https://docs.tempo.xyz/api/faucet \ - -H "Content-Type: application/json" \ - -d '{"address": ""}' -``` - -
- -Replace `` with a lowercase wallet address. - - - - -
- -Request tokens using the `tempo_fundAddress` RPC method. - -
- -```bash -cast rpc tempo_fundAddress \ - --rpc-url https://rpc.moderato.tempo.xyz -``` - -
- -Replace `` with your wallet address. - - - - -The faucet funds the following assets. - -| Asset | Address |Amount| -|-------|---------|----:| -| [pathUSD](https://explore.tempo.xyz/address/0x20c0000000000000000000000000000000000000) | `0x20c0000000000000000000000000000000000000` | `1M` | -| [AlphaUSD](https://explore.tempo.xyz/address/0x20c0000000000000000000000000000000000001) | `0x20c0000000000000000000000000000000000001` | `1M` | -| [BetaUSD](https://explore.tempo.xyz/address/0x20c0000000000000000000000000000000000002) | `0x20c0000000000000000000000000000000000002` | `1M` | -| [ThetaUSD](https://explore.tempo.xyz/address/0x20c0000000000000000000000000000000000003) | `0x20c0000000000000000000000000000000000003` | `1M` | - -## Verify Your Balance - -After requesting tokens, verify your balance: - -```bash -cast erc20 balance 0x20c0000000000000000000000000000000000001 \ - \ - --rpc-url https://rpc.moderato.tempo.xyz -``` diff --git a/src/pages/guide/use-accounts/authorize-access-keys.mdx b/src/pages/guide/use-accounts/authorize-access-keys.mdx deleted file mode 100644 index 186bddae..00000000 --- a/src/pages/guide/use-accounts/authorize-access-keys.mdx +++ /dev/null @@ -1,840 +0,0 @@ ---- -title: Authorize access keys -description: Authorize access keys on Tempo. Use a secondary signing key to send transactions without repeated passkey prompts, with spending limits and expiry for security. -interactive: true ---- - -import * as Demo from '../../../components/guides/Demo.tsx' -import { Connect } from '../../../components/guides/steps/auth/Connect.tsx' -import { AddFunds } from '../../../components/guides/steps/payments/AddFunds.tsx' -import { SendPayment } from '../../../components/guides/steps/payments/SendPayment.tsx' -import { Cards, Card } from 'vocs' -import { Tabs, Tab } from 'vocs' - -# Authorize access keys - -Send stablecoin payments using an access key: a secondary signing key that lets you transact without repeated passkey prompts. Access keys can be scoped with spending limits and expiry for security. - -## Demo - -By the end of this guide you will be able to send payments on Tempo using an access key. Notice that no passkey prompt appears when sending a payment. - - - - - - - -## Steps - -::::steps - -### Set up Wagmi & integrate accounts - -Ensure that you have set up your project with Wagmi and integrated accounts by following either of the guides: - -- [Embed Tempo Wallet](/guide/use-accounts/embed-tempo-wallet) -- [Embed domain-bound Passkeys](/guide/use-accounts/embed-passkeys) - -### Authorize an access key - -Configure your connector to authorize an access key when the user connects. The access key will be used to sign subsequent transactions without passkey prompts. - - - - - ```ts twoslash [wagmi.config.ts] - // @noErrors - import { createConfig, http } from 'wagmi' - import { tempo } from 'wagmi/chains' - import { parseUnits } from 'viem' - import { Expiry } from 'accounts' - import { tempoWallet } from 'accounts/wagmi' - - export const config = createConfig({ - chains: [tempo], - connectors: [ - tempoWallet({ - authorizeAccessKey: () => ({ // [!code hl] - // When the key expires // [!code hl] - expiry: Expiry.days(7), // [!code hl] - // Max spend per token // [!code hl] - limits: [{ // [!code hl] - token: '0x20c0000000000000000000000000000000000001', // [!code hl] - limit: parseUnits('100', 6), // 100 AlphaUSD // [!code hl] - }], // [!code hl] - // Tokens & functions the key can call // [!code hl] - scopes: [{ // [!code hl] - target: '0x20c0000000000000000000000000000000000001', // AlphaUSD // [!code hl] - selector: 'transfer(address,uint256)', // [!code hl] - }], // [!code hl] - }), // [!code hl] - }), - ], - transports: { [tempo.id]: http() }, - }) - ``` - - - - - - ```ts twoslash [wagmi.config.ts] - // @noErrors - import { createConfig, http } from 'wagmi' - import { tempo } from 'wagmi/chains' - import { parseUnits } from 'viem' - import { Expiry } from 'accounts' - import { webAuthn } from 'accounts/wagmi' - - export const config = createConfig({ - chains: [tempo], - connectors: [ - webAuthn({ - authUrl: '/auth', - authorizeAccessKey: () => ({ // [!code hl] - // When the key expires // [!code hl] - expiry: Expiry.days(7), // [!code hl] - // Max spend per token // [!code hl] - limits: [{ // [!code hl] - token: '0x20c0000000000000000000000000000000000001', // [!code hl] - limit: parseUnits('100', 6), // 100 AlphaUSD // [!code hl] - }], // [!code hl] - // Tokens & functions the key can call // [!code hl] - scopes: [{ // [!code hl] - target: '0x20c0000000000000000000000000000000000001', // AlphaUSD // [!code hl] - selector: 'transfer(address,uint256)', // [!code hl] - }], // [!code hl] - }), // [!code hl] - }), - ], - transports: { [tempo.id]: http() }, - }) - ``` - - - - -When the user connects, the connector will authorize an access key with: -- **7-day expiry**: the key automatically becomes invalid after 7 days -- **Scopes**: the key can only call `transfer` on the AlphaUSD token -- **100 AlphaUSD spending limit**: the key can spend up to 100 AlphaUSD - -### Add testnet funds - -Before you can send a payment, you need to fund your account with `AlphaUSD` (`0x20c000…0001`). - -```tsx twoslash [AddFunds.tsx] -// @noErrors -import { Hooks } from 'wagmi/tempo' -import { useConnection } from 'wagmi' - -function AddFunds() { - const { address } = useConnection() - const { mutate, isPending } = Hooks.faucet.useFundSync() - - return ( - - ) -} -``` - -:::warning -The `addFunds` Hook only works on testnets as a convenience feature to get -started quickly. For production, you will need to onramp & fund your account manually. -::: - -### Send a payment - -Now send a payment using `useTransferSync`. Because you authorized an access key in step 2, this transaction will be signed automatically: no passkey prompt. - -```tsx twoslash [SendPayment.tsx] -// @noErrors -import { Hooks } from 'wagmi/tempo' -import { parseUnits } from 'viem' - -function SendPayment() { - const transfer = Hooks.token.useTransferSync() // [!code hl] - - return ( -
{ - event.preventDefault() - const formData = new FormData(event.target as HTMLFormElement) - const recipient = formData.get('recipient') as `0x${string}` - - transfer.mutate({ // [!code hl] - amount: parseUnits('100', 6), // [!code hl] - to: recipient, // [!code hl] - token: '0x20c0000000000000000000000000000000000001', // [!code hl] - }) // [!code hl] - } - }> - - - -
- ) -} -``` - -### Display receipt - -Display the transaction receipt on success. - -```tsx twoslash [SendPayment.tsx] -// @noErrors -import { Hooks } from 'wagmi/tempo' -import { parseUnits } from 'viem' - -function SendPayment() { - const sendPayment = Hooks.token.useTransferSync() - - return ( - <> - {/* ... your payment form ... */} - {sendPayment.data && ( // [!code hl] - {/* [!code hl] */} - View receipt {/* [!code hl] */} - {/* [!code hl] */} - )} {/* [!code hl] */} - - ) -} -``` - -### Next steps - -Now that you can send payments with access keys: -- Learn about the [Account Keychain specification](/protocol/transactions/AccountKeychain) -- Send a payment [with a specific fee token](/guide/payments/pay-fees-in-any-stablecoin) - -:::: - -## SDKs - -### Basic transfer with access key - -Send a payment using an access key across different SDKs: - - - - - :::code-group - - ```ts twoslash [example.ts] - // @noErrors - import { parseUnits } from 'viem' - import { generatePrivateKey } from 'viem/accounts' - import { Account, Actions, Expiry } from 'viem/tempo' - import { client } from './viem.config' - - // 1. Create an access key. // [!code hl] - const accessKey = Account.fromP256(generatePrivateKey(), { // [!code hl] - access: client.account, // [!code hl] - }) // [!code hl] - - // 2. Sign a key authorization. // [!code hl] - const keyAuthorization = await Actions.accessKey.signAuthorization(client, { // [!code hl] - accessKey, // [!code hl] - expiry: Expiry.days(7), // [!code hl] - limits: [{ // [!code hl] - token: '0x20c0000000000000000000000000000000000001', // [!code hl] - limit: parseUnits('100', 6), // [!code hl] - }], // [!code hl] - }) // [!code hl] - - // 3. Send a transfer with the key authorization. // [!code hl] - const { receipt } = await client.token.transferSync({ - amount: parseUnits('100', 6), - to: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb', - token: '0x20c0000000000000000000000000000000000001', - keyAuthorization, // [!code hl] - }) - ``` - - ```ts twoslash [viem.config.ts] - // [!include ~/snippets/viem.config.ts:setup] - ``` - - ::: - -
- - :::info - Once you have broadcasted the transaction with `keyAuthorization`, you can reuse the same access key to sign subsequent transactions without the need to pass the key authorization again. - ::: - - - - - - :::code-group - - ```tsx twoslash [example.tsx] - // @noErrors - import { Hooks } from 'wagmi/tempo' - import { parseUnits } from 'viem' - - function SendPayment() { - const { mutate, isPending } = Hooks.token.useTransferSync() // [!code hl] - - return ( - - ) - } - ``` - - ```ts twoslash [wagmi.config.ts] - // @noErrors - import { createConfig, http } from 'wagmi' - import { tempo } from 'wagmi/chains' - import { parseUnits } from 'viem' - import { Expiry } from 'accounts' - import { tempoWallet } from 'accounts/wagmi' - - export const config = createConfig({ - chains: [tempo], - connectors: [ - tempoWallet({ - authorizeAccessKey: () => ({ // [!code hl] - // When the key expires // [!code hl] - expiry: Expiry.days(7), // [!code hl] - // Max spend per token // [!code hl] - limits: [{ // [!code hl] - token: '0x20c0000000000000000000000000000000000001', // [!code hl] - limit: parseUnits('100', 6), // 100 AlphaUSD // [!code hl] - }], // [!code hl] - // Tokens & functions the key can call // [!code hl] - scopes: [{ // [!code hl] - target: '0x20c0000000000000000000000000000000000001', // AlphaUSD // [!code hl] - selector: 'transfer(address,uint256)', // [!code hl] - }], // [!code hl] - }), // [!code hl] - }), - ], - transports: { [tempo.id]: http() }, - }) - ``` - - ::: - - - - - - :::code-group - - ```tsx twoslash [example.tsx] - // @noErrors - import { Hooks } from 'wagmi/tempo' - import { parseUnits } from 'viem' - - // Same API: the connector handles access key signing automatically - function SendPayment() { - const { mutate, isPending } = Hooks.token.useTransferSync() // [!code hl] - - return ( - - ) - } - ``` - - ```ts twoslash [wagmi.config.ts] - // @noErrors - import { createConfig, http } from 'wagmi' - import { tempo } from 'wagmi/chains' - import { parseUnits } from 'viem' - import { Expiry } from 'accounts' - import { webAuthn } from 'accounts/wagmi' - - export const config = createConfig({ - chains: [tempo], - connectors: [ - webAuthn({ - authUrl: '/auth', - authorizeAccessKey: () => ({ // [!code hl] - // When the key expires // [!code hl] - expiry: Expiry.days(7), // [!code hl] - // Max spend per token // [!code hl] - limits: [{ // [!code hl] - token: '0x20c0000000000000000000000000000000000001', // [!code hl] - limit: parseUnits('100', 6), // 100 AlphaUSD // [!code hl] - }], // [!code hl] - // Tokens & functions the key can call // [!code hl] - scopes: [{ // [!code hl] - target: '0x20c0000000000000000000000000000000000001', // AlphaUSD // [!code hl] - selector: 'transfer(address,uint256)', // [!code hl] - }], // [!code hl] - }), // [!code hl] - }), - ], - transports: { [tempo.id]: http() }, - }) - ``` - - ::: - - - - - - :::code-group - - ```rust [example.rs] - use alloy::primitives::{address, U256}; - use alloy::providers::Provider; - use alloy::signers::{SignerSync, local::PrivateKeySigner}; - use alloy::sol_types::SolCall; - use tempo_alloy::contracts::precompiles::ITIP20; - use tempo_alloy::primitives::transaction::key_authorization::{ - KeyAuthorization, SignedKeyAuthorization, TokenLimit, - }; - use tempo_alloy::primitives::transaction::tt_signature::{ - KeychainSignature, PrimitiveSignature, SignatureType, TempoSignature, - }; - use tempo_alloy::rpc::TempoTransactionRequest; - - mod provider; - - #[tokio::main] - async fn main() -> Result<(), Box> { - let provider = provider::get_provider().await?; - let root: PrivateKeySigner = std::env::var("PRIVATE_KEY")?.parse()?; - - let token = address!("0x20c0000000000000000000000000000000000001"); - let recipient = address!("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb"); - let amount = U256::from(100_000_000u64); // 100 AlphaUSD (6 decimals) - let chain_id = provider.get_chain_id().await?; - - // 1. Create an access key. // [!code hl] - let access_key = PrivateKeySigner::random(); // [!code hl] - - // 2. Sign a key authorization with the root account. // [!code hl] - let expiry = std::time::SystemTime::now() // [!code hl] - .duration_since(std::time::UNIX_EPOCH)? // [!code hl] - .as_secs() + 7 * 24 * 60 * 60; // 7 days // [!code hl] - - let authorization = KeyAuthorization { // [!code hl] - chain_id, // [!code hl] - key_type: SignatureType::Secp256k1, // [!code hl] - key_id: access_key.address(), // [!code hl] - expiry: Some(expiry), // [!code hl] - limits: Some(vec![TokenLimit { // [!code hl] - token, // [!code hl] - limit: amount, // [!code hl] - period: 0, // [!code hl] - }]), // [!code hl] - ..Default::default() // [!code hl] - }; // [!code hl] - - let sig = root.sign_hash_sync(&authorization.signature_hash())?; // [!code hl] - let key_authorization = SignedKeyAuthorization { // [!code hl] - authorization, // [!code hl] - signature: sig.into(), // [!code hl] - }; // [!code hl] - - // 3. Send a transfer with the key authorization. // [!code hl] - let call_data = ITIP20::transferCall { // [!code hl] - to: recipient, // [!code hl] - amount, // [!code hl] - }.abi_encode(); // [!code hl] - - let tx = TempoTransactionRequest { // [!code hl] - key_authorization: Some(key_authorization), // [!code hl] - ..Default::default() // [!code hl] - } // [!code hl] - .with_to(token) // [!code hl] - .with_input(call_data.into()); // [!code hl] - - let receipt = provider // [!code hl] - .send_transaction(tx) // [!code hl] - .await? // [!code hl] - .get_receipt() // [!code hl] - .await?; // [!code hl] - - println!("Transfer successful: {:?}", receipt.transaction_hash); - - Ok(()) - } - ``` - - ```rust [provider.rs] - // [!include ~/snippets/rust-signer-provider.rs:setup] - ``` - - ::: - - - - - - ```bash - # 1. Generate an access key. - $ cast wallet new - # Address: 0x9f3a7b2c1d4e5f6a8b0c9d2e3f4a5b6c7d8e9f0a - # Private key: 0x4c0883a69102937d6231471b5dbb6204fe512961708279f22a3c36f1e24b8e10 - - # 2. Authorize the access key on-chain with expiry and limits. - $ cast keychain auth \ - $ACCESS_KEY_ADDRESS \ - secp256k1 \ - $(date -v+7d +%s) \ # 7-day expiry - --limit 0x20c0000000000000000000000000000000000001:100000000 \ - --rpc-url $TEMPO_RPC_URL \ - --private-key $ROOT_PRIVATE_KEY - - # 3. Send a transfer with the access key. - $ cast send \ - 0x20c0000000000000000000000000000000000001 \ - "transfer(address,uint256)" \ - 0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb \ - 100000000 \ - --rpc-url $TEMPO_RPC_URL \ - --tempo.access-key $ACCESS_KEY_PRIVATE_KEY \ - --tempo.root-account $ROOT_ADDRESS - ``` - - - - -### Periodical payments - -Use `period` in spending limits to cap how much an access key can spend per time window. The limit resets automatically after each period, making it ideal for subscriptions and recurring billing. - - - - - :::code-group - - ```ts twoslash [client.ts] - // @noErrors - import { parseUnits } from 'viem' - import { Actions, Expiry } from 'viem/tempo' - import { client } from './viem.config' - - // Sign a key authorization with a periodic spending limit. - const keyAuthorization = await Actions.accessKey.signAuthorization(client, { - accessKey, - expiry: Expiry.days(30), - limits: [{ - token: '0x20c0000000000000000000000000000000000001', - limit: parseUnits('10', 6), // 10 AlphaUSD // [!code hl] - period: 60 * 60 * 24 * 7, // resets every 7 days // [!code hl] - }], - }) - ``` - - ```ts twoslash [server.ts] - // @noErrors - import { parseUnits } from 'viem' - import { client } from './viem.config' - - // Charge the user weekly from the server. - const { receipt } = await client.token.transferSync({ - account: accessKey, - amount: parseUnits('10', 6), - to: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb', - token: '0x20c0000000000000000000000000000000000001', - keyAuthorization, - }) - ``` - - ```ts twoslash [viem.config.ts] - // [!include ~/snippets/viem.config.ts:setup] - ``` - - ::: - - - - - - :::code-group - - ```tsx twoslash [client.tsx] - // @noErrors - import { parseUnits } from 'viem' - import { useConnectorClient } from 'wagmi' - import { Expiry } from 'accounts' - - function Subscribe() { - const { data: client } = useConnectorClient() - - async function handleSubscribe() { - await client.request({ // [!code hl] - method: 'wallet_authorizeAccessKey', // [!code hl] - params: [{ // [!code hl] - expiry: Expiry.days(30), // [!code hl] - limits: [{ // [!code hl] - token: '0x20c0000000000000000000000000000000000001', // [!code hl] - limit: parseUnits('10', 6), // 10 AlphaUSD // [!code hl] - period: 60 * 60 * 24 * 7, // resets every 7 days // [!code hl] - }], // [!code hl] - scopes: [{ // [!code hl] - target: '0x20c0000000000000000000000000000000000001', // [!code hl] - selector: 'transfer(address,uint256)', // [!code hl] - }], // [!code hl] - }], // [!code hl] - }) // [!code hl] - } - - return ( - - ) - } - ``` - - ```ts twoslash [server.ts] - // @noErrors - import { parseUnits } from 'viem' - import { Actions } from 'wagmi/tempo' - import { config } from './wagmi.config' - - // Charge the user weekly from the server. - const { receipt } = await Actions.token.transferSync(config, { - amount: parseUnits('10', 6), - to: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb', - token: '0x20c0000000000000000000000000000000000001', - }) - ``` - - ```ts twoslash [wagmi.config.ts] - // @noErrors - import { createConfig, http } from 'wagmi' - import { tempo } from 'wagmi/chains' - import { tempoWallet } from 'accounts/wagmi' - - export const config = createConfig({ - chains: [tempo], - connectors: [tempoWallet()], - transports: { [tempo.id]: http() }, - }) - ``` - - ::: - - - - - - :::code-group - - ```tsx twoslash [client.tsx] - // @noErrors - import { parseUnits } from 'viem' - import { useConnectorClient } from 'wagmi' - import { Expiry } from 'accounts' - - function Subscribe() { - const { data: client } = useConnectorClient() - - async function handleSubscribe() { - await client.request({ // [!code hl] - method: 'wallet_authorizeAccessKey', // [!code hl] - params: [{ // [!code hl] - expiry: Expiry.days(30), // [!code hl] - limits: [{ // [!code hl] - token: '0x20c0000000000000000000000000000000000001', // [!code hl] - limit: parseUnits('10', 6), // 10 AlphaUSD // [!code hl] - period: 60 * 60 * 24 * 7, // resets every 7 days // [!code hl] - }], // [!code hl] - }], // [!code hl] - }) // [!code hl] - } - - return ( - - ) - } - ``` - - ```ts twoslash [server.ts] - // @noErrors - import { parseUnits } from 'viem' - import { Actions } from 'wagmi/tempo' - import { config } from './wagmi.config' - - // Charge the user weekly from the server. - const { receipt } = await Actions.token.transferSync(config, { - amount: parseUnits('10', 6), - to: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb', - token: '0x20c0000000000000000000000000000000000001', - }) - ``` - - ```ts twoslash [wagmi.config.ts] - // @noErrors - import { createConfig, http } from 'wagmi' - import { tempo } from 'wagmi/chains' - import { webAuthn } from 'accounts/wagmi' - - export const config = createConfig({ - chains: [tempo], - connectors: [webAuthn({ authUrl: '/auth' })], - transports: { [tempo.id]: http() }, - }) - ``` - - ::: - - - - -### Authorize arbitrary public keys - -You can also directly authorize a public key: - - - - - :::code-group - - ```ts twoslash [example.ts] - // @noErrors - import { parseUnits } from 'viem' - import { Account, Actions, Expiry } from 'viem/tempo' - import { client } from './viem.config' - - // Sign authorization by public key - const keyAuthorization = await Actions.accessKey.signAuthorization(client, { - accessKey: { // [!code hl] - publicKey: '0x...', // [!code hl] - // address: '0x...', // or address // [!code hl] - type: 'p256', // [!code hl] - }, // [!code hl] - expiry: Expiry.days(7), - limits: [{ - token: '0x20c0000000000000000000000000000000000001', - limit: parseUnits('100', 6), - }], - }) - ``` - - ```ts twoslash [viem.config.ts] - // [!include ~/snippets/viem.config.ts:setup] - ``` - - ::: - - - - - - ```ts twoslash [wagmi.config.ts] - // @noErrors - import { createConfig, http } from 'wagmi' - import { tempo } from 'wagmi/chains' - import { parseUnits } from 'viem' - import { Expiry } from 'accounts' - import { tempoWallet } from 'accounts/wagmi' - - export const config = createConfig({ - chains: [tempo], - connectors: [ - tempoWallet({ - authorizeAccessKey: () => ({ - publicKey: '0x...', // [!code hl] - // address: '0x...', // or address // [!code hl] - expiry: Expiry.days(7), - limits: [{ - token: '0x20c0000000000000000000000000000000000001', - limit: parseUnits('100', 6), - }], - scopes: [{ - target: '0x20c0000000000000000000000000000000000001', - selector: 'transfer(address,uint256)', - }], - }), - }), - ], - transports: { [tempo.id]: http() }, - }) - ``` - - - - - - ```ts twoslash [wagmi.config.ts] - // @noErrors - import { createConfig, http } from 'wagmi' - import { tempo } from 'wagmi/chains' - import { parseUnits } from 'viem' - import { Expiry } from 'accounts' - import { webAuthn } from 'accounts/wagmi' - - export const config = createConfig({ - chains: [tempo], - connectors: [ - webAuthn({ - authUrl: '/auth', - authorizeAccessKey: () => ({ - publicKey: '0x...', // [!code hl] - // address: '0x...', // or address // [!code hl] - expiry: Expiry.days(7), - limits: [{ - token: '0x20c0000000000000000000000000000000000001', - limit: parseUnits('100', 6), - }], - }), - }), - ], - transports: { [tempo.id]: http() }, - }) - ``` - - - - -## Best practices - -### Scope to specific tokens and functions -Always define `scopes` to restrict which tokens and functions the access key can call. A key scoped to `transfer` on a specific token cannot be used to call other tokens or functions, even if compromised. - -### Set appropriate expiry -Access keys should have a reasonable expiry window. Use `Expiry.days(7)` for interactive sessions or `Expiry.hours(1)` for short-lived operations. - -### Use spending limits -Combine scopes with per-token spending limits for defense in depth. Spending limits cap the total amount a key can transfer, while scopes restrict which tokens it can interact with. - -## Learning resources - - - - diff --git a/src/pages/guide/use-accounts/batch-transactions.mdx b/src/pages/guide/use-accounts/batch-transactions.mdx deleted file mode 100644 index a858cd92..00000000 --- a/src/pages/guide/use-accounts/batch-transactions.mdx +++ /dev/null @@ -1,68 +0,0 @@ ---- -description: Execute multiple operations atomically in a single Tempo Transaction using native protocol-level batching with one signature and lower gas costs. ---- - -# Batch Transactions - -One of the most powerful features of the [Tempo Transaction](/protocol/transactions/spec-tempo-transaction) type is batching multiple operations into a single transaction. This allows you to execute several actions atomically (i.e., they all succeed or all fail together). This helps reduce gas costs, prevents partial failures, and creates better user experiences by combining multiple steps into one transaction. - -:::info -Batch transactions are a feature of Tempo's [TempoTransaction type](/protocol/transactions/spec-tempo-transaction). See the TempoTransaction spec for complete details on batching and other features. -::: - -## Example: Sending a Batch of Transactions - -:::code-group -```ts [batchTransactions.ts] -const { address, hash, id } = await client.sendTransaction({ // [!code focus] - calls: [{ // [!code focus] - to: tokenAddress, // [!code focus] - data: transferCalldata // Transfer tokens // [!code focus] - }, // [!code focus] - { // [!code focus] - to: anotherTokenAddress, // [!code focus] - data: approveCalldata // Approve spending // [!code focus] - }, // [!code focus] - { // [!code focus] - to: dexAddress, // [!code focus] - data: swapCalldata // Execute swap // [!code focus] - }], // [!code focus] -}) // [!code focus] -``` - -```ts [viem.config.ts] -// [!include ~/snippets/setup.ts:setup] -``` -::: - -## How It Works - -Unlike traditional EVM transactions which only support a single call per transaction, the [Tempo Transaction](/protocol/transactions/spec-tempo-transaction) type natively supports a `calls` vector—enabling multiple operations in a single atomic transaction. - -### Native Protocol Support - -Batching is built directly into the transaction type at the protocol level. This means: - -- **Atomic execution**: All calls succeed or all calls revert together—no partial failures -- **Single signature**: Sign once for the entire batch, reducing UX friction -- **Lower gas costs**: Avoid the overhead of multiple transaction submissions -- **No smart contract required**: Works with any EOA, no need to deploy a separate batching contract - -### Transaction Structure - -The `TempoTransaction` includes a `calls` field that accepts a list of operations: - -```rust -pub struct TempoTransaction { - // ... other fields - calls: Vec, // Batch of calls to execute atomically - // ... -} - -pub struct Call { - to: TxKind, // Target address or Create for contract deployment - value: U256, // ETH value to send - input: Bytes, // Calldata for the call -} -``` - diff --git a/src/pages/guide/use-accounts/connect-to-wallets.mdx b/src/pages/guide/use-accounts/connect-to-wallets.mdx deleted file mode 100644 index 496bfb78..00000000 --- a/src/pages/guide/use-accounts/connect-to-wallets.mdx +++ /dev/null @@ -1,311 +0,0 @@ ---- -description: Connect your application to EVM-compatible wallets like MetaMask on Tempo. Set up Wagmi connectors and add the Tempo network to user wallets. -mipd: true -interactive: true ---- - -import * as Demo from '../../../components/guides/Demo.tsx' -import { ConnectWallet as ConnectWalletStep } from '../../../components/guides/steps/wallet/ConnectWallet.tsx' -import { ConnectWallet } from '../../../components/ConnectWallet.tsx' - -# Connect to Wallets - -It is possible to use Tempo with EVM-compatible wallets that support the Tempo network, -or support adding custom networks (like MetaMask). - -You can use these wallets when building your application on Tempo. - -This guide will walk you through how to set up your application to connect to wallets. - - - - - -## Wagmi Setup - -::::steps - -### Set up Wagmi - -Ensure that you have set up your project with Wagmi by following the [guide](/sdk/typescript#wagmi-setup). - -### Configure Wagmi - -Next, let's ensure Wagmi is configured correctly to connect to wallets. - -Ensure we have [`multiInjectedProviderDiscovery`](https://wagmi.sh/react/api/createConfig#multiinjectedproviderdiscovery) set to `true` to display injected browser wallets. - -We can also utilize [wallet connectors](https://wagmi.sh/react/api/connectors) from Wagmi like `metaMask` to support mobile devices. - -```tsx twoslash [config.ts] -// @noErrors -import { createConfig, http } from 'wagmi' -import { tempo } from 'viem/chains' -import { metaMask } from 'wagmi/connectors' // [!code ++] - -export const config = createConfig({ - chains: [tempo], - connectors: [metaMask()], // [!code ++] - multiInjectedProviderDiscovery: true, // [!code ++] - transports: { - [tempo.id]: http(), - }, -}) -``` - -### Display Connect Buttons - -After that, we will set up "Connect" buttons so that the user can connect to their wallet. - -We will create a new `ConnectWallet.tsx` component to work in. - -:::code-group - -
- -
- -
-
-
- -::: - -:::code-group - -```tsx twoslash [Connect.tsx] -// @noErrors -import { useConnect, useConnectors } from 'wagmi' - -export function Connect() { - const connect = useConnect() - const connectors = useConnectors() - - return ( -
- {connectors.map((connector) => ( - - ))} -
- ) -} - -``` - -```tsx twoslash [config.ts] filename="config.ts" -// @noErrors -import { createConfig, http } from 'wagmi' -import { tempo } from 'viem/chains' -import { metaMask } from 'wagmi/connectors' // [!code ++] - -export const config = createConfig({ - chains: [tempo], - connectors: [metaMask()], // [!code ++] - multiInjectedProviderDiscovery: true, // [!code ++] - transports: { - [tempo.id]: http(), - }, -}) -``` - -::: - -### Display Account & Sign Out - -After the user has connected to their wallet, we can display the account information and a sign out button. - -We will create a new `Account.tsx` component to work in. - -:::code-group - -
- -
- -
-
-
- -::: - -:::code-group - -```tsx twoslash [Account.tsx] -// @noErrors -import { useConnection, useDisconnect } from 'wagmi' - -export function Account() { - const account = useConnection() - const disconnect = useDisconnect() - - return ( -
-
- {account.address?.slice(0, 6)}...{account.address?.slice(-4)} -
- -
- ) -} - -``` - -```tsx twoslash [config.ts] filename="config.ts" -// @noErrors -import { createConfig, http } from 'wagmi' -import { tempo } from 'viem/chains' -import { metaMask } from 'wagmi/connectors' // [!code ++] - -export const config = createConfig({ - chains: [tempo], - connectors: [metaMask()], // [!code ++] - multiInjectedProviderDiscovery: true, // [!code ++] - transports: { - [tempo.id]: http(), - }, -}) -``` - -::: - -### Display "Add Tempo" Button - -If the wallet is not on the Tempo network, we can display a "Add Tempo" button so that the user can add the network to their wallet. - -:::code-group - -
- -
- -
-
-
- -::: - -:::code-group - -```tsx twoslash [Account.tsx] -// @noErrors -import { useConnection, useDisconnect, useSwitchChain } from 'wagmi' -import { tempo } from 'viem/chains' // [!code ++] - -export function Account() { - const account = useConnection() - const disconnect = useDisconnect() - const switchChain = useSwitchChain() // [!code ++] - - return ( -
-
- {account.address?.slice(0, 6)}...{account.address?.slice(-4)} -
- - - {/* [!code ++] */} -
- ) -} - -``` - -```tsx twoslash [config.ts] filename="config.ts" -// @noErrors -import { createConfig, http } from 'wagmi' -import { tempo } from 'viem/chains' -import { metaMask } from 'wagmi/connectors' // [!code ++] - -export const config = createConfig({ - chains: [tempo], - connectors: [metaMask()], // [!code ++] - multiInjectedProviderDiscovery: true, // [!code ++] - transports: { - [tempo.id]: http(), - }, -}) -``` - -::: - -:::: - -## Third-Party Libraries - -You can also use a third-party Wallet Connection library to handle the onboarding & connection of wallets. - -Such libraries include: [Privy](https://privy.io), [ConnectKit](https://docs.family.co/connectkit), [AppKit](https://reown.com/appkit), [Dynamic](https://dynamic.xyz), and [RainbowKit](https://rainbowkit.com). - -The above libraries are all built on top of Wagmi, handle all the edge cases around wallet connection. - -:::warning -It is worth noting that some wallets that are included in the above libraries may not support Tempo yet. We are working on adding support for the majority of wallets. -::: - -## Add to Wallet Manually - -You can add Tempo to a wallet that supports custom networks (e.g. MetaMask) manually. - -For example, if you are using MetaMask: - -1. Open MetaMask and click on the menu in the top right and select "Networks" -2. Click "Add a custom network" -3. Enter the network details: - -#### Mainnet - -| **Property** | **Value** | -|-------------------|-------| -| **Network Name** | Tempo Mainnet | -| **Currency** | `USD` | -| **Chain ID** | `4217` | -| **HTTP URL** | `https://rpc.tempo.xyz` | -| **WebSocket URL** | `wss://rpc.tempo.xyz` | -| **Block Explorer** | [`https://explore.tempo.xyz`](https://explore.tempo.xyz) | - -#### Testnet - -| **Property** | **Value** | -|-------------------|-------| -| **Network Name** | Tempo Testnet (Moderato) | -| **Currency** | `USD` | -| **Chain ID** | `42431` | -| **HTTP URL** | `https://rpc.moderato.tempo.xyz` | -| **WebSocket URL** | `wss://rpc.moderato.tempo.xyz` | -| **Block Explorer** | [`https://explore.testnet.tempo.xyz`](https://explore.testnet.tempo.xyz) | - -The official documentation from MetaMask on this process is also available [here](https://support.metamask.io/configure/networks/how-to-add-a-custom-network-rpc#adding-a-network-manually). - -:::warning -Note that we recommend using the `symbol` "USD" for the currency symbol, despite there being no native gas token. Existing wallets like MetaMask don't natively support the Tempo network yet, so there are some quirks to the interface. -You might also need to set `nativeCurrency.decimals` to `18` instead of `6` in some wallets. -::: diff --git a/src/pages/guide/use-accounts/embed-passkeys.mdx b/src/pages/guide/use-accounts/embed-passkeys.mdx deleted file mode 100644 index c4fe2e66..00000000 --- a/src/pages/guide/use-accounts/embed-passkeys.mdx +++ /dev/null @@ -1,337 +0,0 @@ ---- -description: Create domain-bound passkey accounts on Tempo using WebAuthn for secure, passwordless authentication with biometrics like Face ID and Touch ID. -interactive: true ---- - -import { Cards, Card } from 'vocs' -import * as Demo from '../../../components/guides/Demo.tsx' -import { EmbedPasskeys, SignInButtons } from '../../../components/guides/EmbedPasskeys.tsx' - -# Embed Passkey Accounts - -Create a domain-bound passkey account on Tempo using the [Tempo Accounts SDK](/accounts) and WebAuthn signatures for secure, passwordless authentication with [Tempo Transactions](/protocol/transactions/spec-tempo-transaction). - -The [`webAuthn`](/accounts/wagmi/webAuthn) Wagmi connector is the easiest way to get started. Passkeys enable users to authenticate with biometrics (fingerprint, Face ID, Touch ID) they already use for other apps. Keys are stored in the device's secure enclave and sync across devices via iCloud Keychain or Google Password Manager. - -:::info - -**What does "domain-bound" mean?** - -WebAuthn credentials are bound to a specific domain (the [Relying Party](https://en.wikipedia.org/wiki/Relying_party)). - -This means that credentials created for one domain (e.g., `example.com`) will only work on that domain (and its subdomains) and cannot be used to authenticate on other domains. - -This means your users won't be able to use the same passkey account on other applications. If this is not what you want, head to the [Embed Tempo Wallet](/guide/use-accounts/embed-tempo-wallet) guide for a universal account experience. - -::: - -## Demo - -By the end of this guide, you will be able to embed passkey accounts into your application. - - - - - -## Steps - -::::steps - -### Set up Wagmi - -Ensure that you have set up your project with Wagmi by following the [guide](/sdk/typescript#wagmi-setup). - -### Set up the WebAuthn server - -Set up a [`Handler.webAuthn`](/accounts/server/handler.webAuthn) server to handle passkey registration and authentication ceremonies. - -```ts twoslash [server.ts] -// @noErrors -import { Handler, Kv } from 'accounts/server' - -const handler = Handler.webAuthn({ - kv: Kv.memory(), - origin: 'https://example.com', - rpId: 'example.com', -}) -``` - -Then plug `handler` into your server framework of choice: - -```ts -createServer(handler.listener) // Node.js -Bun.serve(handler) // Bun -Deno.serve(handler) // Deno -app.all('*', c => handler.fetch(c.request)) // Elysia -app.use(handler.listener) // Express -app.use(c => handler.fetch(c.req.raw)) // Hono -export const GET = handler.fetch // Next.js -export const POST = handler.fetch // Next.js -``` - -:::warning -`Kv.memory()` is not recommended for production use. Instead, use a persistent store like Cloudflare or Vercel KV, or a Redis instance. See [`Kv`](/accounts/server/kv) for available adapters. -::: - -### Configure the WebAuthn Connector - -Next, we will need to configure the `webAuthn` connector in our Wagmi config, passing the (relative or absolute) url of your WebAuthn server to `authUrl`. - -```tsx twoslash [config.ts] -// @noErrors -import { createConfig, http } from 'wagmi' -import { tempo } from 'viem/chains' -import { webAuthn } from 'wagmi/tempo' // [!code ++] - -export const config = createConfig({ - chains: [tempo], - connectors: [webAuthn({ authUrl: '/auth' })], // [!code ++] - multiInjectedProviderDiscovery: false, - transports: { - [tempo.id]: http(), - }, -}) -``` - -:::tip - -This Wagmi configuration sets `multiInjectedProviderDiscovery` to `false` to -prevent injected browser wallets from being detected, and to prefer the `webAuthn` connector. -If you would like to allow connection to other wallets, set this property to `true`. - -::: - - -### Display Sign In Buttons - -After that, we will set up "Sign in" and "Sign up" buttons so that the user can create -or use a passkey with the application. - -We will create a new `Example.tsx` component to work in. - -:::code-group - -
- -
- -::: - -:::code-group - -```tsx twoslash [Example.tsx] -// @noErrors -import { useConnect, useConnectors } from 'wagmi' - -export function Example() { - const connect = useConnect() - const [connector] = useConnectors() - - return ( -
- - - -
- ) -} - -``` - -```tsx twoslash [config.ts] filename="config.ts" -// @noErrors -import { createConfig, http } from 'wagmi' -import { tempo } from 'viem/chains' -import { webAuthn } from 'wagmi/tempo' // [!code ++] - -export const config = createConfig({ - chains: [tempo], - connectors: [webAuthn({ authUrl: '/auth' })], // [!code ++] - multiInjectedProviderDiscovery: false, - transports: { - [tempo.id]: http(), - }, -}) -``` - -::: - -### Display Account & Sign Out - -After the user has signed in, we can display the account information and a sign out button. - -:::code-group - -
- -
- -
-
-
- -::: - -:::code-group - -```tsx twoslash [Example.tsx] -// @noErrors -import { useConnection, useConnect, useConnectors, useDisconnect } from 'wagmi' - -export function Example() { - const account = useConnection() // [!code ++] - const connect = useConnect() - const [connector] = useConnectors() - const disconnect = useDisconnect() // [!code ++] - - if (account.address) // [!code ++] - return ( // [!code ++] -
{/* [!code ++] */} -
{account.address.slice(0, 6)}...{account.address.slice(-4)}
{/* [!code ++] */} - {/* [!code ++] */} -
{/* [!code ++] */} - ) // [!code ++] - - return ( -
- - - -
- ) -} - -``` - -```tsx twoslash [config.ts] filename="config.ts" -// @noErrors -import { createConfig, http } from 'wagmi' -import { tempo } from 'viem/chains' -import { webAuthn } from 'wagmi/tempo' // [!code ++] - -export const config = createConfig({ - chains: [tempo], - connectors: [webAuthn({ authUrl: '/auth' })], // [!code ++] - multiInjectedProviderDiscovery: false, - transports: { - [tempo.id]: http(), - }, -}) -``` - -::: - -### Next Steps - -Now that you have created your first passkey account, you can now: -- learn the [Best Practices](#best-practices) below -- follow a guide on how to [make a payment](#TODO), [create a stablecoin](#TODO), and [more](#TODO) with a passkey account. - -:::: - -{/* - -TODO: note on public key retention with credentialId -> pubKey KV service. - -## Notes for Production - -### Public Key Retention - - */} - -## Best Practices - -### Loading State - -When the user is logging in or signing out, we should show loading state to indicate that the process is happening. - -We can use the `isPending` property from the `useConnect` hook to show pending state to the user. - -```tsx twoslash [Example.tsx] -// @noErrors -import { useConnection, useConnect, useConnectors, useDisconnect } from 'wagmi' - -export function Example() { - const account = useConnection() - const connect = useConnect() - const [connector] = useConnectors() - const disconnect = useDisconnect() - - if (connect.isPending) // [!code ++] - return
Check prompt...
{/* [!code ++] */} - return (/* ... */) -} -``` - -:::tip - -Wagmi exposes [React Query's](https://tanstack.com/query/latest/docs/framework/react/overview) interfaces on all Hooks to extract asynchronous states such as loading (e.g. `isPending`) and error (e.g. `isError`, `error`) states. - -::: - -### Error Handling - -If an error unexpectedly occurs, we should display an error message to the user. - -We can use the `error` property from the `useConnect` hook to show error state to the user. - -```tsx twoslash [Example.tsx] -// @noErrors -import { useConnection, useConnect, useConnectors, useDisconnect } from 'wagmi' - -export function Example() { - const account = useConnection() - const connect = useConnect() - const [connector] = useConnectors() - const disconnect = useDisconnect() - - if (connect.error) // [!code ++] - return
Error: {connect.error.message}
{/* [!code ++] */} - return (/* ... */) -} -``` - -## Learning Resources - - - - - - diff --git a/src/pages/guide/use-accounts/embed-tempo-wallet.mdx b/src/pages/guide/use-accounts/embed-tempo-wallet.mdx deleted file mode 100644 index b3f0cfa2..00000000 --- a/src/pages/guide/use-accounts/embed-tempo-wallet.mdx +++ /dev/null @@ -1,287 +0,0 @@ ---- -description: Embed the Tempo Wallet dialog into your application for a universal wallet experience with account management, passkeys, and fee sponsorship. -interactive: true ---- - -import { Cards, Card } from 'vocs' -import * as Demo from '../../../components/guides/Demo.tsx' -import { AccountsSignIn } from '../../../components/guides/AccountsSignIn.tsx' - -# Embed Tempo Wallet - -Embed the [Tempo Wallet](https://wallet.tempo.xyz) dialog into your application using the [Tempo Accounts SDK](/accounts) for a universal wallet experience. - -The [`tempoWallet`](/accounts/wagmi/tempoWallet) Wagmi connector is the easiest way to get started. It wraps the Accounts SDK and provides a complete account management experience for your users, including sign-up, sign-in, balance management, and transaction signing, all within an embedded dialog. - -:::info - -**When should I use Tempo Wallet vs. domain-bound Passkeys?** - -The **Tempo Wallet** provides a universal wallet experience – users manage their account through the Tempo Wallet interface, and their account is portable across all applications that embed it. - -**Domain-bound Passkeys** are tied to your specific domain and cannot be used on other applications. This gives you full control over the authentication experience but requires more setup. - -If you want a quick integration with a full-featured wallet, use the [Tempo Wallet](/accounts/api/dialog). If you want a fully custom, domain-bound authentication experience, use [domain-bound Passkeys](/guide/use-accounts/embed-passkeys). - -::: - -## Demo - -By the end of this guide, you will be able to embed the Tempo Wallet into your application. - - - - - -## Steps - -::::steps - -### Set up Wagmi - -Ensure that you have set up your project with Wagmi by following the [guide](/sdk/typescript#wagmi-setup). - -### Configure the Tempo Wallet Connector - -Next, we will configure the `tempoWallet` connector in our Wagmi config. - -```tsx twoslash [config.ts] -// @noErrors -import { createConfig, http } from 'wagmi' -import { tempo } from 'viem/chains' -import { tempoWallet } from 'wagmi/connectors' // [!code ++] - -export const config = createConfig({ - chains: [tempo], - connectors: [tempoWallet()], // [!code ++] - multiInjectedProviderDiscovery: false, - transports: { - [tempo.id]: http(), - }, -}) -``` - -:::tip - -This Wagmi configuration sets `multiInjectedProviderDiscovery` to `false` to -prevent injected browser wallets from being detected, and to prefer the `tempoWallet` connector. -If you would like to allow connection to other wallets, set this property to `true`. - -::: - -### Display Sign In Button - -After that, we will set up a "Sign in" button that opens the Tempo Wallet dialog for the user. - -We will create a new `Example.tsx` component to work in. - -:::code-group - -```tsx twoslash [Example.tsx] -// @noErrors -import { useConnect, useConnectors } from 'wagmi' - -export function Example() { - const connect = useConnect() - const connectors = useConnectors() - const connector = connectors.find((c) => c.id === 'xyz.tempo') - - return ( -
- -
- ) -} -``` - -```tsx twoslash [config.ts] filename="config.ts" -// @noErrors -import { createConfig, http } from 'wagmi' -import { tempo } from 'viem/chains' -import { tempoWallet } from 'wagmi/connectors' // [!code ++] - -export const config = createConfig({ - chains: [tempo], - connectors: [tempoWallet()], // [!code ++] - multiInjectedProviderDiscovery: false, - transports: { - [tempo.id]: http(), - }, -}) -``` - -::: - -### Display Account & Sign Out - -After the user has signed in, we can display the account information and a sign out button. - -:::code-group - -
- -
- -
-
-
- -::: - -:::code-group - -```tsx twoslash [Example.tsx] -// @noErrors -import { useConnection, useConnect, useConnectors, useDisconnect } from 'wagmi' - -export function Example() { - const account = useConnection() // [!code ++] - const connect = useConnect() - const connectors = useConnectors() - const connector = connectors.find((c) => c.id === 'xyz.tempo') - const disconnect = useDisconnect() // [!code ++] - - if (account.address) // [!code ++] - return ( // [!code ++] -
{/* [!code ++] */} -
{account.address.slice(0, 6)}...{account.address.slice(-4)}
{/* [!code ++] */} - {/* [!code ++] */} -
{/* [!code ++] */} - ) // [!code ++] - - return ( -
- -
- ) -} - -``` - -```tsx twoslash [config.ts] filename="config.ts" -// @noErrors -import { createConfig, http } from 'wagmi' -import { tempo } from 'viem/chains' -import { tempoWallet } from 'wagmi/connectors' // [!code ++] - -export const config = createConfig({ - chains: [tempo], - connectors: [tempoWallet()], // [!code ++] - multiInjectedProviderDiscovery: false, - transports: { - [tempo.id]: http(), - }, -}) -``` - -::: - -### Deploy to Production - -When you're ready to go live, follow the [Deploying to Production](/accounts/production) guide to configure your application for production use. - -### Next Steps - -Now that you have embedded the Tempo Wallet, you can now: -- learn the [Best Practices](#best-practices) below -- follow a guide on how to [make a payment](/guide/payments), [create a stablecoin](/guide/issuance), or [exchange stablecoins](/guide/stablecoin-dex). - -:::: - -## Best Practices - -### Loading State - -When the user is logging in or signing out, we should show loading state to indicate that the process is happening. - -We can use the `isPending` property from the `useConnect` hook to show pending state to the user. - -```tsx twoslash [Example.tsx] -// @noErrors -import { useConnection, useConnect, useConnectors, useDisconnect } from 'wagmi' - -export function Example() { - const account = useConnection() - const connect = useConnect() - const connectors = useConnectors() - const connector = connectors.find((c) => c.id === 'xyz.tempo') - const disconnect = useDisconnect() - - if (connect.isPending) // [!code ++] - return
Check prompt...
{/* [!code ++] */} - return (/* ... */) -} -``` - -### Error Handling - -If an error unexpectedly occurs, we should display an error message to the user. - -We can use the `error` property from the `useConnect` hook to show error state to the user. - -```tsx twoslash [Example.tsx] -// @noErrors -import { useConnection, useConnect, useConnectors, useDisconnect } from 'wagmi' - -export function Example() { - const account = useConnection() - const connect = useConnect() - const connectors = useConnectors() - const connector = connectors.find((c) => c.id === 'xyz.tempo') - const disconnect = useDisconnect() - - if (connect.error) // [!code ++] - return
Error: {connect.error.message}
{/* [!code ++] */} - return (/* ... */) -} -``` - -### Fee Sponsorship - -The `tempoWallet` connector supports fee sponsorship via a `feePayer` option. This allows you to sponsor transaction fees for your users. - -```tsx twoslash [config.ts] -// @noErrors -import { createConfig, http } from 'wagmi' -import { tempo } from 'viem/chains' -import { tempoWallet } from 'wagmi/connectors' - -export const config = createConfig({ - chains: [tempo], - connectors: [tempoWallet({ - feePayer: 'https://sponsor.example.com', // [!code ++] - })], - transports: { - [tempo.id]: http(), - }, -}) -``` - -See the [Sponsor user fees](/guide/payments/sponsor-user-fees) guide for more details on setting up a fee payer server. - -## Learning Resources - - - - - - diff --git a/src/pages/guide/use-accounts/index.mdx b/src/pages/guide/use-accounts/index.mdx deleted file mode 100644 index bbd749ac..00000000 --- a/src/pages/guide/use-accounts/index.mdx +++ /dev/null @@ -1,56 +0,0 @@ ---- -description: Create and integrate Tempo accounts with the universal Tempo Wallet or domain-bound passkeys. ---- - -import { Cards, Card } from 'vocs' - -# Create & Use Accounts - -Create and integrate Tempo accounts into your product with the universal Tempo Wallet or domain-bound passkeys. - -## What should I use? - -### Tempo Wallet - -Most apps should use [Tempo Wallet](/accounts/api/dialog) - it provides a universal account experience with a central account for users that includes embedded onramp, access keys, and transaction orchestration out of the box. - -### Domain-bound Passkeys - -Use [domain-bound passkeys](/accounts/api/webAuthn) when you want to build your own wallet experience or when your app needs to manage and own the WebAuthn ceremony directly. - -:::info -Tempo Wallet uses domain-bound passkeys (on `tempo.xyz`) under the hood. -::: - -### Other Wallets - -Offer users a range of existing wallets to connect from, such as MetaMask, Coinbase Wallet, and others. - ---- - - - - - - - diff --git a/src/pages/guide/use-accounts/scheduled-transactions.mdx b/src/pages/guide/use-accounts/scheduled-transactions.mdx deleted file mode 100644 index 8b9696a4..00000000 --- a/src/pages/guide/use-accounts/scheduled-transactions.mdx +++ /dev/null @@ -1,89 +0,0 @@ ---- -description: Schedule Tempo Transactions to execute within a specific time window using validAfter and validBefore timestamps for vesting, offers, and delayed execution. ---- - -# Scheduled Transactions - -Execute transactions only within a specific time window using `validAfter` and `validBefore` timestamps. - -Scheduled transactions are enabled by the [Tempo Transaction](/protocol/transactions/spec-tempo-transaction) type, which includes optional timestamp fields. These fields are validated by the protocol when the transaction is submitted. - -## Setting Time Bounds - -You can add optional timestamp fields to your transaction to control when it can be executed. Both `validAfter` and `validBefore` accept Unix timestamps. - -For example, to create a transaction that can only execute between Jan 1, 2025 and Jan 2, 2025, you would set `validAfter: 1735689600` and `validBefore: 1735776000` when signing the transaction. - -## Time Window Validation - -The protocol validates timestamps against the block timestamp: - -- **`validAfter`**: Block timestamp must be greater than or equal to this value -- **`validBefore`**: Block timestamp must be less than or equal to this value -- **Both optional**: Omit either or both to not restrict that bound - -You can use these fields individually or in combination: - -- **Only `validAfter`**: Transaction can execute any time after the specified date (e.g., after Jan 1, 2025) -- **Only `validBefore`**: Transaction must execute by the specified date (e.g., before Jan 2, 2025) -- **Both**: Transaction has a specific time window for execution -- **Neither**: No time restrictions - -## Examples - -### Example: Vesting Schedule - -Release tokens at a specific future time by pre-signing a transaction with a future `validAfter` timestamp. - -You would sign a token transfer transaction that includes `validAfter: 1767225600` (Jan 1, 2026). The signed transaction can be stored off-chain or given to a third party. When Jan 1, 2026 arrives, anyone can submit the signed transaction to the network, and it will execute. If submitted before that date, validators will reject it. - -This enables trustless vesting schedules where the beneficiary holds a pre-signed transaction that becomes valid at a specific future date. - -### Example: Time-Limited Offer - -You can create an offer that expires if not accepted by setting both `validAfter` and `validBefore` timestamps. - -For example, to create an offer valid for 24 hours, you would set `validAfter` to the current timestamp and `validBefore` to 86400 seconds (24 hours) in the future. The signed transaction can only be executed within that 24-hour window. After the expiration time passes, the transaction becomes invalid and cannot be included in a block. - -### Example: Delayed Execution - -You can sign a transaction now that can only be executed at a future time by setting `validAfter` to a timestamp 7 days in the future. - -The signed transaction is then stored (in a database, onchain via a contract, or held by a party). After the 7-day delay period passes, anyone with access to the signed transaction can submit it to the network for execution. The protocol enforces that the transaction cannot be included in a block before the specified time. - -This is useful for governance proposals with timelock requirements or delayed contract upgrades. - -## Storing Scheduled Transactions - -:::info -**Important**: It is your responsibility to store and manage signed scheduled transactions until their execution time. The protocol validates the time constraints but does not store transactions for you. -::: - -When you create a scheduled transaction, you have several options for storage: - -- **Self-custody**: Store the signed transaction in your own database or secure storage until the execution time -- **Third-party services**: Work with a company that specializes in holding and submitting scheduled transactions at the appropriate time -- **Smart contracts**: Store the signed transaction onchain in a contract that can submit it when the time window is valid - -## Mempool Behavior - -Validators will reject transactions that are outside their validity window: - -- **Too early**: Transactions with `validAfter` in the future won't be included in blocks. The transaction will be rejected if you try to submit it before the specified time. -- **Too late**: Transactions with `validBefore` in the past will be rejected. Once the expiration time passes, the transaction can no longer be executed. - -For example, if you submit a transaction with `validAfter` set to Jan 1, 2026, validators will return an error until that timestamp is reached. - -## Combining with Other Features - -**Fee sponsorship**: You can combine scheduled transactions with fee sponsorship, where a sponsor pays the fees for a time-locked transaction. The fee payer signs their portion, and the combined transaction becomes valid only within the specified time window. - -**Batch transactions**: Multiple operations can be scheduled together to execute atomically within a specific time window. All operations in the batch must succeed or fail together, and the entire batch is subject to the time constraints. - -:::warning -The block timestamp is set by validators and may vary slightly. For critical timing, account for small time variations (±15 seconds). -::: - -## Technical Details - -For complete specifications on timestamp validation, see the [Tempo Transaction](/protocol/transactions/spec-tempo-transaction) type specification. diff --git a/src/pages/guide/use-accounts/webauthn-p256-signatures.mdx b/src/pages/guide/use-accounts/webauthn-p256-signatures.mdx deleted file mode 100644 index 6bd68b1f..00000000 --- a/src/pages/guide/use-accounts/webauthn-p256-signatures.mdx +++ /dev/null @@ -1,45 +0,0 @@ ---- -description: Sign Tempo Transactions with passkeys, Face ID, Touch ID, or hardware security keys using native WebAuthn and P256 signature support on EOA accounts. ---- - -# WebAuthn & P256 Signatures - -Tempo EOA addresses can be derived from multiple signature types. Allowing you to sign transactions with standard Ethereum wallets, hardware security keys, biometric authentication like Face ID and Touch ID, or even using [passkeys](https://passkeys.dev/). - -## Supported signature types - -### secp256k1 - -The standard Ethereum signature format. No type identifier needed, this is the default. - -### P256 - -Raw P256 signatures that include the public key coordinates. Identified by type `0x01`. - -The `preHash` flag indicates whether the digest should be hashed with SHA256 before verification. Set this to `true` if using Web Crypto API or similar implementations that require pre-hashing. - -### WebAuthn - -WebAuthn signatures include authenticator data from biometric devices. Identified by type `0x02`. - -The verification data contains information from the authenticator (device metadata, user presence flags, etc.). - -## Address derivation - -### secp256k1 - -Standard Ethereum address derivation: `keccak256(uncompressed_public_key)[12:32]`. - -### P256 and WebAuthn - -Both P256 and WebAuthn derive addresses from the public key coordinates: `keccak256(pub_key_x || pub_key_y)[12:32]`. - -:::info[Shared address space] -P256 and WebAuthn use identical address derivation, so the same key pair produces the same address regardless of signature type. A passkey can authorize transactions via raw P256 signatures and vice versa. -::: - ---- - -:::info -Additional signature types may be supported in the future. For complete technical specifications, see the [Tempo Transaction](/protocol/transactions/spec-tempo-transaction) protocol documentation. -::: diff --git a/src/pages/index.tsx b/src/pages/index.tsx new file mode 100644 index 00000000..6006a1d1 --- /dev/null +++ b/src/pages/index.tsx @@ -0,0 +1,12 @@ +'use client' + +import HomePage from '../marketing/HomePage' +import MarketingRoute from '../marketing/MarketingRoute' + +export default function Page() { + return ( + + + + ) +} diff --git a/src/pages/learn/index.mdx b/src/pages/learn/index.mdx deleted file mode 100644 index 741ff929..00000000 --- a/src/pages/learn/index.mdx +++ /dev/null @@ -1,69 +0,0 @@ ---- -description: Explore stablecoin use cases and Tempo's payments-optimized blockchain architecture for remittances, payouts, and embedded finance. ---- - -import { Cards, Card } from 'vocs' - -# Learn [Notes on stablecoin use cases and Tempo's architecture] - -Tempo is a general-purpose blockchain optimized for payments. Tempo is designed to be a low-cost, high-throughput blockchain with user and developer features that we believe should be core to a modern payment system. - -Tempo was designed in close collaboration with an exceptional group of [design partners](https://tempo.xyz/ecosystem) who are helping to validate the system against real payment workloads. Here, we have written about what stablecoins are and how Tempo is designed from the ground up for the use cases they enable. - - - - - - -## Stablecoin use cases - -Stablecoins enable a wide range of payment and financial use cases across industries: - - - - - - - - - diff --git a/src/pages/learn/partners.mdx b/src/pages/learn/partners.mdx deleted file mode 100644 index ab7886dc..00000000 --- a/src/pages/learn/partners.mdx +++ /dev/null @@ -1,26 +0,0 @@ ---- -description: Discover Tempo's ecosystem of stablecoin issuers, wallets, custody providers, compliance tools, and ramps. ---- - -import { Cards, Card } from 'vocs' - -# Partners - -Our partners power the infrastructure needed for stablecoin use cases like [remittances](/learn/use-cases/remittances), [global payouts](/learn/use-cases/global-payouts), [embedded finance](/learn/use-cases/embedded-finance), [tokenized deposits](/learn/use-cases/tokenized-deposits), [microtransactions](/learn/use-cases/microtransactions), and [agentic commerce](/learn/use-cases/agentic-commerce). - -The Tempo ecosystem is built with a diverse range of best-in-class partners across stablecoin issuance, wallets & custody, compliance tooling, fraud monitoring, interoperability protocols, analytics & monitoring, orchestration and ramps, and more. We're designing the ecosystem to be global from day one, with issuers across the globe and broad local currency support. - - - - - diff --git a/src/pages/learn/stablecoins.mdx b/src/pages/learn/stablecoins.mdx deleted file mode 100644 index 7b429114..00000000 --- a/src/pages/learn/stablecoins.mdx +++ /dev/null @@ -1,178 +0,0 @@ ---- -title: What are stablecoins? -description: Learn what stablecoins are, how they maintain value through reserves, and the payment use cases they enable for businesses globally. ---- - -import { Cards, Card } from 'vocs' -import { ZoomableImage } from "../../components/ZoomableImage.tsx" -import { FAQSchema } from "../../components/FAQSchema.tsx" - - -# Stablecoins - -Stablecoins let businesses move value globally with faster settlement and more predictable fees than legacy payment rails. Treasury and payments teams use them for cross-border payouts, vendor settlement, and automated liquidity operations that run 24/7. - -## What are stablecoins? - -Stablecoins are blockchain-based digital assets designed to maintain a stable value. Unlike volatile cryptocurrencies such as Bitcoin or Ether, stablecoins are pegged to established currencies like the US dollar or Euro. Examples of USD-pegged stablecoins include USDC and USDT. - -This page focuses on fully reserved, fiat-backed stablecoins. Other models (crypto-collateralized or algorithmic) have different risk profiles. - -Real-world utility, increasing regulatory clarity, and growing institutional adoption are driving rapid growth in stablecoin usage. The circulating supply of stablecoins has grown more than tenfold over the past five years, reaching roughly $300 billion today. The US Treasury projects this figure could rise to $3 trillion by 2030. - -## How stablecoins work - -Fully reserved fiat-backed stablecoins are issued by regulated entities and backed 1:1 with reserves such as cash and short-term government securities held at licensed financial institutions. Stablecoin issuers use a mint-and-burn process to keep the supply aligned with underlying reserves and ensure holders can always redeem stablecoins at face value. - -When a user deposits fiat currency with an issuer, the issuer mints the corresponding amount of stablecoins onchain and sends them to the user. When the user redeems stablecoins for fiat, the issuer burns the tokens and sends the equivalent fiat from reserves. - - - -Regulatory frameworks such as the EU's MiCA and the US GENIUS Act are establishing disclosure and reserve requirements for issuers. Requirements and enforcement still vary by jurisdiction, so businesses should evaluate each stablecoin and issuer on its own terms. - -## Why stablecoins matter for businesses - -For financial institutions, corporations, and payment providers, fully reserved fiat-backed stablecoins enable near-instant payments that operate 24/7, work across borders, and bypass friction found in legacy payment rails. - -They support established use cases such as [remittances](/learn/use-cases/remittances), [global payouts](/learn/use-cases/global-payouts), and [embedded finance](/learn/use-cases/embedded-finance), while enabling newer applications like [microtransactions](/learn/use-cases/microtransactions) and [agentic commerce](/learn/use-cases/agentic-commerce). - - - -## Programmable money - -Stablecoins open the door to programmable money. Smart contracts can execute payments automatically based on predefined rules, improving efficiency and transparency in payment processing, liquidity management, and reconciliation. - -For example, a global enterprise can use a smart contract to automatically top up or sweep subsidiary wallets based on balance thresholds. Routine treasury actions run automatically onchain, replacing custom integrations with multiple banking partners. - -On Tempo, stablecoins use [TIP-20 Tokens](/protocol/tip20/overview), which adds transfer memos, compliance controls, and reward distribution to standard token functionality. - -## Stablecoins on Tempo - -Tempo is purpose-built for stablecoin payments and issuance. Key resources: - -- [TIP-20 Tokens](/protocol/tip20/overview): How stablecoins and other tokens behave on Tempo, including transfer memos and compliance policies -- [Native stablecoins](/learn/tempo/native-stablecoins): How Tempo approaches stablecoin design, settlement, and fee payment -- [Stablecoin DEX](/guide/stablecoin-dex): On-chain exchange for stablecoin-to-stablecoin swaps -- [Mint stablecoins](/guide/issuance/mint-stablecoins): Create custom stablecoins with built-in compliance features - -## History of stablecoins - -Stablecoins emerged as a solution to cryptocurrency's volatility. Tether (USDT), launched in 2014 on the Bitcoin blockchain via the Omni Layer protocol, pioneered the concept of a dollar-pegged digital asset. MakerDAO introduced DAI in 2017, creating the first decentralized, crypto-collateralized stablecoin. Circle and Coinbase followed in 2018 with USD Coin (USDC), emphasizing regulatory compliance and transparent reserves. - -The 2020 "DeFi Summer" marked an inflection point. Total stablecoin market capitalization surged from under $10 billion to over $100 billion by 2021, as stablecoins became essential infrastructure for lending protocols, decentralized exchanges, and yield farming. - -Regulatory frameworks are now taking shape globally. The EU's Markets in Crypto-Assets (MiCA) regulation established comprehensive stablecoin requirements including reserve mandates and issuer licensing. In the US, the GENIUS Act represents ongoing legislative efforts to create a federal framework for stablecoin issuance and oversight. - -## Stablecoin use cases - -Stablecoins enable a wide range of payment and financial use cases: - - - - - - - - - - -## Frequently asked questions - -### What is a stablecoin? - -A stablecoin is a blockchain-based digital asset designed to maintain a stable value, often pegged 1:1 to a fiat currency such as the US dollar. Stablecoins combine blockchain settlement (24/7 transfers, programmability) with price stability useful for payments and treasury operations. - -### Are stablecoins safe? - -Safety depends on the stablecoin's backing, redemption model, and issuer disclosures. Widely used USD-pegged stablecoins including USDC and USDT publish reserve reports or attestations. The frequency, detail, and regulatory regime vary by issuer and jurisdiction. Review reserve composition, redemption terms, and disclosure history before use. - -### How are stablecoins different from other cryptocurrencies? - -Stablecoins target a fixed value (such as 1 USD), while cryptocurrencies like Bitcoin and Ether fluctuate based on market demand. This makes stablecoins practical for payments, settlement, and treasury workflows that require predictable amounts. - -### What are stablecoins used for? - -Businesses use stablecoins for cross-border payments, vendor settlement, global payouts, and treasury liquidity. Stablecoins settle in minutes and operate 24/7, often with more predictable fees than traditional international wires. - -### How do I get stablecoins? - -Businesses typically acquire stablecoins through regulated exchanges, direct onboarding with issuers like Circle or Tether, or payment providers that support stablecoin on-ramps. Enterprise teams often use licensed custodians for custody and settlement. - -### Are stablecoins regulated? - -Regulation varies by jurisdiction and continues to evolve. Some regions have introduced issuer requirements for reserves, disclosures, and licensing (for example, the EU's MiCA framework). Confirm the rules that apply to your entity, counterparties, and operating geographies. - -### What is counterparty risk in stablecoins? - -Counterparty risk is the risk that a stablecoin issuer cannot redeem tokens 1:1 due to insufficient or illiquid reserves, operational failures, or insolvency. Major issuers such as Circle (USDC) and Tether (USDT) publish reserve disclosures, but these are not real-time views of reserves. Businesses prefer reserves held in cash and short-term government securities. - -### What regulatory risks should I consider? - -A stablecoin compliant in one jurisdiction may face restrictions elsewhere, and rules can change. Verify issuer licensing and compliance status where you operate, and confirm how custody, reporting, and redemption requirements apply to your use case. - - diff --git a/src/pages/learn/tempo/fx.mdx b/src/pages/learn/tempo/fx.mdx deleted file mode 100644 index d3af39fd..00000000 --- a/src/pages/learn/tempo/fx.mdx +++ /dev/null @@ -1,40 +0,0 @@ ---- -description: Access foreign exchange liquidity directly onchain with regulated non-USD stablecoin issuers and multi-currency fee payments on Tempo. ---- - -import { Cards, Card } from 'vocs' - -# Onchain FX - -An overwhelming majority of cross-border payments facilitated by stablecoins use what is known as the stablecoin sandwich: the onchain leg is conducted in stablecoins, typically USD-denominated, with on- and off-ramps converting from/to a local currency. - -## Accessing FX Liquidity Onchain - -Tempo is exploring a design to enable stablecoin users to access FX liquidity directly onchain, including via a set of regulated non-USD stablecoin issuers. This removes the need for off-chain currency conversion and enables seamless multi-currency flows entirely onchain. - -When available, users will be able to: -- Exchange between USD and non-USD stablecoins directly onchain -- Access competitive FX rates through decentralized liquidity pools -- Execute cross-border payments without relying on traditional FX providers -- Reduce settlement times and costs associated with currency conversion - -## Multi-Currency Fee Payments - -When available, Tempo will also allow users to pay for network fees in currencies beyond USD. This means users would be able to interact with Tempo in their preferred currency without needing to hold USD stablecoins specifically for transaction fees. - -## Coming Soon - -Onchain FX is currently in development and will be available in a future release. This feature is being designed in close collaboration with regulated stablecoin issuers to ensure it meets compliance requirements while providing the flexibility and efficiency that global payment flows demand. - -If you're interested in multi-currency payment flows and want to explore how onchain FX can benefit your use case, reach out to our team. - -## Help design this feature - - - - diff --git a/src/pages/learn/tempo/index.mdx b/src/pages/learn/tempo/index.mdx deleted file mode 100644 index 494c2d77..00000000 --- a/src/pages/learn/tempo/index.mdx +++ /dev/null @@ -1,62 +0,0 @@ ---- -description: Discover Tempo, the payments-first blockchain with instant settlement, predictably low fees, and native stablecoin support. ---- - -import { Cards, Card } from 'vocs' - -# Tempo [The payments-first blockchain] - -Tempo is a general-purpose blockchain optimized for payments, built to deliver instant, deterministic settlement, predictably low fees, and a stablecoin-native experience. Tempo's public testnet launched on December 9, 2025, and is now available for anyone to start building on. - -Tempo was designed in close collaboration with an exceptional group of [design partners](https://tempo.xyz/ecosystem) who are helping to validate the system against real payment workloads. - -The general-purpose programmability of the blockchain provides functionality directly in support of our global payments mission: stablecoin interoperability, onchain FX, compliant privacy, and more. Tempo will be a permissionless, decentralized chain. The Tempo client is open source under the Apache license, and anyone can run a node or sync the chain. - -## Core Features - -Explore the key features that make Tempo purpose-built for payments: - - - - - - - - - - diff --git a/src/pages/learn/tempo/machine-payments.mdx b/src/pages/learn/tempo/machine-payments.mdx deleted file mode 100644 index a4489c48..00000000 --- a/src/pages/learn/tempo/machine-payments.mdx +++ /dev/null @@ -1,121 +0,0 @@ ---- -description: The Machine Payments Protocol (MPP) is an open standard for machine-to-machine payments, co-authored by Stripe and Tempo. ---- - -import { Cards, Card } from 'vocs' - -# Agentic Payments [Programmable payments for agents and services] - -Agents can write code, coordinate services, and execute complex workflows. What they can't do, until now, is pay for things. - -The [Machine Payments Protocol](https://mpp.dev) (MPP) is an open standard for machine-to-machine payments, co-authored by Stripe and Tempo. MPP lets any client (agents, apps, or humans) pay for any service inline with their HTTP request. No API keys, no billing accounts, no signup flows. - -MPP is live on Tempo Mainnet and works with stablecoins on Tempo, cards via Stripe, and Bitcoin via Lightning, with more payment methods coming. - -## How it works - -A client requests a paid resource. The server responds with `402 Payment Required` and a challenge describing the price. The client pays, retries with a credential, and the server returns the resource with a receipt. - -The entire flow happens in a single request cycle. No redirects, no webhooks, no out-of-band settlement. - -## What agents can do today - -Give your agent a wallet and it can transact autonomously across the internet: - - - - - - - - -## Sessions: OAuth for money - -Sessions let an agent authorize spending once, then make many payments without a separate on-chain transaction for each one. - -When an agent opens a session, it deposits funds into reserve. As the agent consumes resources (an API call, a model inference, a data query), payments stream continuously via signed vouchers off-chain. The server periodically settles the accumulated vouchers on-chain. - -Thousands of small transactions collapse into a single settlement. True pay-per-use at internet scale. - -| | **Charge** | **Session** | -|---|---|---| -| **Pattern** | One-time payment per request | Continuous pay-as-you-go | -| **Latency** | ~500ms (on-chain confirmation) | Near-zero (off-chain vouchers) | -| **Best for** | Single API calls, content access | LLM APIs, metered services, token streaming | -| **On-chain cost** | Per request | Amortized across many requests | - -## The Payments Directory - -The [Payments Directory](https://mpp.dev) catalogs MPP-compatible services that any agent can transact with automatically. At launch, the directory includes integrations with more than 100 services spanning model providers, developer infrastructure, compute platforms, and data services. - -Service providers can integrate MPP to monetize their APIs and become discoverable to agents. Use cases include pay-per-call APIs, monetized MCP servers, gated content, and multi-service agent workflows. - -## An open, rail-agnostic standard - -MPP is not tied to any single payment rail. The same protocol supports: - -- **Stablecoins on Tempo**: TIP-20 token transfers with instant finality -- **Cards via Stripe**: Visa, Mastercard, and other card networks -- **Bitcoin via Lightning**: Payments over the Lightning Network -- **Custom methods**: Anyone can build a [payment method](https://mpp.dev/payment-methods/custom) for their rail - -The core [Payment HTTP Authentication Scheme](https://paymentauth.org) is submitted to the IETF for standardization. Payment method specifications are separate documents that anyone can author and publish independently. - -## Why Tempo - -Tempo's infrastructure is purpose-built for the transaction patterns that agentic payments produce: - -- **~500ms deterministic finality**: Fast enough for synchronous request/response flows -- **Sub-cent fees**: Low enough for micropayments and per-request billing -- **Payment lanes**: Dedicated transaction routing that stays reliable under load -- **Fee sponsorship**: Servers cover gas so clients only need stablecoins -- **Parallel nonces**: Payment transactions don't block other account activity - -## Get started - - - - - - - diff --git a/src/pages/learn/tempo/modern-transactions.mdx b/src/pages/learn/tempo/modern-transactions.mdx deleted file mode 100644 index 7a83db45..00000000 --- a/src/pages/learn/tempo/modern-transactions.mdx +++ /dev/null @@ -1,96 +0,0 @@ ---- -description: Native support for gas sponsorship, batch transactions, scheduled payments, and passkey authentication built into Tempo's protocol. ---- - -import { Cards, Card } from 'vocs' - -# Tempo Transactions [Modern, optimized, and highly scalable blockchain transactions] - -Tempo has built-in support for gas sponsorship, batch transactions, scheduled payments, and modern authentication through passkeys. These features are often grouped together as "smart accounts" or "account abstraction." - -On other chains, even when available, these are generally add-on functionalities that require third-party providers to unlock. By natively enabling these features at the protocol level, developers on Tempo can deploy payment logic without managing additional middleware or custom contracts, and can build to enshrined standards. - -Tempo Transactions are built into the protocol so developers don't need custom smart contracts or third-party middleware to get batching, sponsorship, scheduling, and modern auth. This reduces integration risk and operational overhead for payment applications. - -## Batched Payments - -Payment processors and platforms often need to send thousands of payments at once (e.g., payroll runs, merchant settlements, customer refunds). Tempo supports batch transactions where multiple operations execute atomically in a single transaction. - -This unlocks high-volume use cases: orchestrators can submit thousands of payouts as a single operation, rather than submitting them one-by-one and tracking individual success or failure. If any operation in the batch fails, the entire batch reverts, ensuring atomic execution across all payments. This is critical for payment operators who need guaranteed settlement guarantees for their workflows. - -## Fee Sponsorship - -Applications often want to pay transaction fees on behalf of their users. For instance, to simplify onboarding, to improve the user experience, or to remove friction from their payment flows. - -Tempo's protocol-level fee sponsorship allows an account to sign a transaction while a separate sponsor (typically the application) pays the gas fee. This means end users can interact with your application without holding any tokens for fees, dramatically lowering the barrier to entry. - -## Configurable Fee Tokens - -Tempo supports paying transaction fees in USD-denominated stablecoins. This removes the need for users to acquire a volatile gas token and simplifies accounting for payment applications operating in dollars. Users can pay fees in any supported USD stablecoin, and the protocol handles conversion automatically. - -## Scheduled Payments - -The Tempo transaction type includes scheduling as a protocol feature. Users can specify a time window for transaction execution, and validators will include the transaction when it becomes valid. - -This enables "set and forget" payment operations directly at the protocol level, enabling recurring payments like subscriptions or scheduled disbursements. No need for external automation services or off-chain infrastructure to manage recurring transactions. - -## Modern Authentication - -Tempo supports passkey authentication through WebAuthn/P256 signature validation, built directly into the protocol. Users can authenticate with the same biometrics (fingerprint, Face ID) they already use for other apps. - -Their keys are stored in their device's secure enclave, and passkeys sync across devices via services they already use such as iCloud Keychain or Google Password Manager. This way, users don't need to secure a 12 or 24-word seed phrase for traditional wallets. For payment applications, this means onboarding flows can be as simple as existing consumer apps, without sacrificing security. - -Tempo uses an [EIP-2718](https://eips.ethereum.org/EIPS/eip-2718) transaction type with native support for these features. - -## Use Cases - -Tempo Transactions provide the features necessary to bring a wide range of payment use cases onchain: - -### Payroll and Merchant Payouts - -Batch transactions allow large payout runs to be processed efficiently and securely. Payments execute in a single atomic batch: the entire batch succeeds as a single transaction, eliminating partial failures and manual reruns. - -See: [Global payouts with stablecoins](/learn/use-cases/global-payouts) - -### Wallets and Neobanks - -Fee sponsorship removes gas from the user experience entirely. Combined with passkey authentication, users interact with a familiar fintech-grade interface with all of the blockchain complexity abstracted away. - -### Commerce - -As stablecoin adoption grows, platforms need flexibility in how fees are paid. Configurable fee tokens allow transaction fees to be paid in any USD stablecoin, while fee sponsorship enables merchants to offer seamless checkout experiences for users. - -See: [TIP-20 Tokens](/learn/tempo/native-stablecoins) for stable fees and payment memos. - -### Subscriptions - -Scheduled transactions automate recurring payments and enable true subscription services onchain. No need for external automation services or custom smart contracts. - -## Partner Ecosystem - -Infrastructure partners including [Crossmint](https://www.crossmint.com/), [Fireblocks](https://www.fireblocks.com/), [Privy](https://www.privy.io/), and [Turnkey](https://www.turnkey.com/) support Tempo Transactions and can help accelerate your development. Visit the [Ecosystem](https://tempo.xyz/ecosystem) to explore Tempo's growing network of partners. - -For partner testimonials and rollout context, see [Introducing Tempo Transactions](https://tempo.xyz/blog/tempo-transactions). - -## Next steps - - - - - - diff --git a/src/pages/learn/tempo/native-stablecoins.mdx b/src/pages/learn/tempo/native-stablecoins.mdx deleted file mode 100644 index 3f09d3dd..00000000 --- a/src/pages/learn/tempo/native-stablecoins.mdx +++ /dev/null @@ -1,125 +0,0 @@ ---- -description: Tempo's stablecoin token standard with payment lanes, stable fees, reconciliation memos, and built-in compliance for regulated issuers. ---- - -import { Cards, Card } from 'vocs' - -# TIP-20 Tokens [Tempo's native stablecoin token standard] - -Tempo extends the [ERC-20 standard](https://ethereum.org/en/developers/docs/standards/tokens/erc-20/) used on EVM chains (Ethereum, Base, Arbitrum) with additional features, and enshrines them in protocol as the TIP-20 contract suite. These features are purpose-built for payment use cases at scale. - -## Predictable Payment Throughput - -Tempo has dedicated payment lanes: reserved blockspace for payment transactions (specifically, TIP-20 token transfers) that other applications cannot consume. Payments have guaranteed blockspace reserved at the protocol level and don't compete with other traffic like NFT mints, liquidations, or high-frequency contract calls. - -Even if there are extremely popular applications on the chain competing for blockspace, payroll runs or customer disbursements execute predictably. Fees stay low and stable even when other network activity spikes, with a target of one-tenth of a cent per payment transaction. For payment processors, that means no "downtime" from congestion, and predictable economics for high-volume flows. - -There is no "noisy neighbor problem" like on traditional blockchains, where blockspace can fill up or cost orders of magnitude more during times of intense activity. - -## Low, Predictable Fees, in Stables - -Transaction fees can be paid directly in USD-denominated stablecoins. This removes the need for volatile gas tokens and lets payment applications operate entirely in the same currency as their underlying flows, ensuring predictable costs and simpler accounting. - -For wallets and custodians, this removes the need to hold a balance of new cryptoassets just to facilitate stablecoin payments. Users can pay fees in any USD stablecoin, and validators can receive fees in any USD stablecoin, with the protocol automatically converting between them using onchain liquidity through Tempo's built-in DEX. - -Costs are predictable and low: TIP-20 transfers target one-tenth of a cent per transaction. This removes a major barrier for mainstream adoption. Users can interact with Tempo using only the stablecoins they're already familiar with, without needing to understand or manage volatile crypto assets. - -For how applications sponsor fees and batch payments, see [Tempo Transactions](/learn/tempo/modern-transactions). - -## Native Reconciliation - -Tempo's TIP-20 tokens can natively attach a short memo directly to each transfer. This mirrors traditional payment systems, where every transaction carries context (e.g., an invoice number, a customer ID, a cost center, or a simple reference note) for backend systems to automatically match payments to internal records. - -For larger memos, Tempo supports flexible approaches where only a commitment (like a hash or locator) travels onchain while the full data (including PII) lives off-chain. This means finance teams can automatically match payments to invoices and payment processors can embed the structured data their systems need without custom solutions or integration with third-party reconciliation infrastructure. - -See stablecoin reconciliation in practice: [Global payouts](/learn/use-cases/global-payouts) and [Tokenized deposits](/learn/use-cases/tokenized-deposits). - -## Built-in Compliance - -Stablecoin (and other regulated asset) issuers operate under regulatory requirements. They need to enforce whitelists (only approved addresses can transact) or blacklists (sanctioned addresses cannot transact). - -Tempo provides a shared compliance infrastructure through the Tempo Policy Registry (TIP-403). An issuer can thus create a single policy with their compliance rules, and multiple tokens they issue can adopt that policy. When the compliance manager updates the policy, all tokens using it automatically enforce the new rules, unlike traditional blockchains where an issuer must update each contract one-by-one. - -## Built-in Stable Asset DEX - -Tempo includes a native decentralized exchange optimized for stablecoins and tokenized deposits. This means users can pay fees in any USD stablecoin, and validators can receive fees in any USD stablecoin, with the protocol automatically converting between them using onchain liquidity. - -## When TIP-20 matters - -Use TIP-20 when you need: - -- **Predictable throughput** for large payment runs without congestion-driven fee spikes. -- **Stable-denominated fees** for simpler unit economics. -- **Reconciliation metadata** to tie onchain transfers to invoices and ERP records. -- **Shared compliance enforcement** across multiple regulated assets. - -For basic token functionality without these requirements, standard ERC-20 contracts work on Tempo as they do on any EVM chain. - -## Use Cases for Custom Stablecoins - -Beyond peer-to-peer payments, custom stablecoins built on TIP-20 can be designed for specific treasury, settlement, and foreign exchange use cases: - -### Corporate Treasury Management - -Custom stablecoins can streamline global treasury operations, allowing funds to move instantly between subsidiaries. TIP-20's reward distribution enables reserve income to be shared across business units without complex off-chain accounting. - -### Cross-border Payments - -Stablecoins allow smaller banks and payment providers to participate in cross-border flows without correspondent banking networks. TIP-20's native transfer memos enable reconciliation between stablecoin transactions and existing payment engines that support SWIFT messaging. - -### Wholesale Settlement and Deposit Tokens - -Policy registries enable permissioned stablecoins and deposit tokens for bank-to-bank settlement. Issuers can enforce transfer rules directly at the token level, supporting regulated and wholesale payment systems. - -### Interest-bearing Stablecoins - -TIP-20 simplifies interest-bearing stablecoin issuance with native yield distribution. Issuers can programmatically share yield with users and intermediaries in real time, eliminating manual calculations and off-chain reconciliation. - -### Non-USD Stablecoins and Onchain FX - -A global stablecoin ecosystem requires currencies beyond USD. TIP-20 tokens include an optional currency identifier specifying the fiat currency they represent. Tempo's native DEX will enable onchain foreign exchange between stablecoins. - -## FAQs - -### Is TIP-20 compatible with ERC-20 tooling? - -TIP-20 extends ERC-20 semantics for payment-scale features while remaining familiar to EVM developers. Most indexers and wallet patterns still apply, but advanced features (memos, policy registry) require TIP-20-aware integrations. - -### How big can a reconciliation memo be? - -TIP-20 supports short onchain memos for common references (invoice ID, customer ID). For larger data, store details offchain and put only a commitment or locator onchain to avoid leaking PII. - -### How should issuers handle compliance updates? - -Use the Tempo Policy Registry (TIP-403) so multiple tokens can share a policy and inherit updates automatically, rather than updating each token contract individually. - -## Infrastructure Partners - -Work with infrastructure partners who are rolling out TIP-20 support to accelerate your development: - -- [AllUnity](https://allunity.com/) - Regulated euro, Swedish krona, and Swiss franc stablecoins for European corporates -- [Bridge](https://www.bridge.xyz/) - Custom regulated stablecoin issuance with off-the-shelf templates -- [LayerZero](https://layerzero.network/) - Cross-chain token extensions for Tempo's payment ecosystem - -## Related reading - - - - - - diff --git a/src/pages/learn/tempo/performance.mdx b/src/pages/learn/tempo/performance.mdx deleted file mode 100644 index 51921242..00000000 --- a/src/pages/learn/tempo/performance.mdx +++ /dev/null @@ -1,29 +0,0 @@ ---- -description: High throughput and sub-second finality built on Reth SDK and Simplex Consensus for payment applications requiring instant settlement. ---- - -import { Cards, Card } from 'vocs' - -# Performance [Tempo's performant blockchain architecture] - -All of Tempo's payments-first features are built on a foundation of a highly performant blockchain. Tempo delivers the throughput and finality characteristics that payment applications require. - -## High Throughput - -Tempo is built on the [Reth](https://reth.rs/) SDK, the most performant and flexible EVM (Ethereum Virtual Machine) execution client. Reth is also relied upon by several other blockchains including Ethereum, Base and Arc. - -Tempo houses the team that built and maintains Reth, and several other pieces of core blockchain developer software such as [Foundry](https://getfoundry.sh/). We are already benchmarking 20,000 TPS on testnet with a clear line of sight to an order of magnitude higher by mainnet. - -This throughput is critical for payment use cases. Whether processing payroll for thousands of employees, settling marketplace transactions, or handling microtransactions at scale, Tempo's performance ensures that your payment flows won't be bottlenecked by blockchain capacity. - -## Fast Finality - -Public blockchains that support financial activity need to balance two contradictory demands. - -On the one hand, the blockchain needs fast finality: a payment made on the blockchain should be quickly confirmed as final, with no risk of being "re-orged" out as can happen on many blockchains today. - -On the other hand, the blockchain also needs to degrade gracefully under suboptimal network conditions. Tempo uses Simplex Consensus (via [Commonware](https://www.commonware.xyz/)), which provides fast, sub-second finality in normal conditions while maintaining graceful degradation under adverse networks. - -Tempo uses Byzantine-fault tolerant consensus with four validators on testnet. Blocks are finalized every ~0.5 seconds, and once a block is marked as final, transactions in that block are guaranteed to be included in the chain. This cutting-edge consensus algorithm optimally balances speed with resilience. - -For payment applications, this gives payment operators the same settlement certainty they expect from existing financial systems, with speed that matches best-in-class blockchains. This is critical for use cases like point-of-sale systems, instant settlements, and cross-border transfers where users expect immediate, irreversible confirmation. diff --git a/src/pages/learn/tempo/privacy.mdx b/src/pages/learn/tempo/privacy.mdx deleted file mode 100644 index d3c62cce..00000000 --- a/src/pages/learn/tempo/privacy.mdx +++ /dev/null @@ -1,57 +0,0 @@ ---- -description: Explore Tempo's opt-in privacy features enabling private balances and confidential transfers while maintaining issuer compliance. ---- - -import { Cards, Card } from 'vocs' - -# Privacy - -Tempo is also developing opt-in privacy features designed to coexist with issuer compliance requirements. This functionality will enable private balances and transfers while maintaining the auditability and reporting capabilities that regulated issuers need. - -## Private Token Standard for Stablecoin Issuers - -Traditional blockchains make all transaction data publicly visible, which creates challenges for businesses and consumers who need confidentiality. At the same time, regulated stablecoin issuers must maintain compliance and auditability. - -We're currently working on multiple designs which provide: -- **Private balances**: Account balances that are hidden from public view while remaining auditable by authorized parties -- **Confidential transfers**: Transaction amounts and participants that can be shielded from the public ledger -- **Selective disclosure**: Issuers and regulators that can maintain the visibility they need for compliance without exposing sensitive business data publicly - -## Use Cases for Privacy - -When available, private tokens will enable: - -**Sensitive business transactions**: Companies will be able to conduct payroll, vendor payments, and treasury operations without revealing confidential financial information to competitors or the public. - -**Confidential consumer balances**: Users will be able to hold and transact with stablecoins without publicly broadcasting their account balances or spending patterns. - -**Privacy-preserving payment flows**: Platforms will be able to process payments while protecting user privacy and maintaining confidentiality in sensitive transactions. - -All of this is achieved while preserving the compliance features that make stablecoins viable in regulated markets. Issuers will be able to maintain the ability to monitor, report, and enforce policies as required by regulation. - -## Zones: Private Validium Chains - -Tempo has built-in support for privacy through [zones](/protocol/zones) — native validium chains anchored to Tempo. Instead of being visible to the entire world, balances and transactions on zones are only visible to the zone sequencer, the users involved, and anyone they choose to selectively disclose to. - -Read the full technical specification: - - - - - - diff --git a/src/pages/learn/tempo/receive-policies.mdx b/src/pages/learn/tempo/receive-policies.mdx deleted file mode 100644 index 3dd8979d..00000000 --- a/src/pages/learn/tempo/receive-policies.mdx +++ /dev/null @@ -1,111 +0,0 @@ ---- -title: Account-Level Receive Policies -description: Learn how Tempo account-level receive policies help wallets, custodians, and stablecoin platforms control accepted tokens, senders, and recovery paths. ---- - -import { Cards, Card } from 'vocs' - -# Account-level receive policies [Control what accounts can receive] - -Account-level receive policies give Tempo accounts a way to define which TIP-20 tokens they are willing to receive and which addresses are allowed to send to them. They are designed for teams that need more control over inbound funds: wallets, custodians, exchanges, on/off-ramps, payment processors, treasury systems, and stablecoin orchestration platforms. - -::::info[Live on testnet] -Account-level receive policies are live on testnet with the T6 network upgrade. Mainnet activation is scheduled for June 23, 2026 4pm CEST. -:::: - -Try the [receive policies demo](https://tempo.xyz/receive-policies) to see credited, held, and recovery flows in practice. - -## Why receiving needs policy - -On most blockchains, an address can send a compatible token to any other address without the recipient's permission. That flexibility is useful, but it creates operational problems for payment and financial applications: - -- A customer deposit address may receive a token that the platform does not support. -- A treasury or settlement wallet may receive funds from an address it is not allowed to interact with. -- A stablecoin orchestrator may need to prevent users from sending one issuer's token to a deposit address meant for another asset. -- A support team may need a clear way to identify, review, and recover funds that should not have landed in a user's balance. - -Receive policies provide a means to express for receivers to express such preferences at account level. Instead of only letting token issuers decide who can send or receive a token, a user can also define what it is willing to accept and from whom. - -## What a receive policy controls - -An account-level receive policy has three parts: - -- **Tokens**: an allowlist or blocklist of TIP-20 tokens the account can receive. -- **Senders**: an allowlist or blocklist of addresses that can send to the account. -- **Recovery authority**: who is allowed to move funds when an inbound send is held instead of credited. This could be the sender (default), the receiver, or a designated third-party address or smart contract. - - -## How inbound sends work - -A sender still starts a normal TIP-20 transfer or mint. Tempo first runs the usual token checks: balance, allowance, pause status, and issuer-level policies such as TIP-403 allowlists or blocklists. - -If those checks fail, the transaction reverts as usual. If they pass, Tempo checks whether the receiver has an account-level receive policy: - -- **No receive policy**: the transfer or mint is credited normally. -- **Policy accepts the token and sender**: the transfer or mint is credited normally. -- **Policy rejects the token or sender**: the transfer or mint can succeed, but the funds are held for recovery instead of being credited to the receiver. - -That held state is the key difference. The protocol records the details needed to review and recover the funds later, including the token, amount, sender, intended receiver, and reason the send was held. - -## Common use cases - -### Exchanges and custodians - -Deposit infrastructure can accept only supported assets for each customer or deposit flow. If a user sends an unsupported token, the platform can surface the held transfer and provide a structured support or recovery process instead of manually investigating a mismatch. - -### Wallets and wallet-as-a-service providers - -Wallet providers can offer business customers controls over which tokens and counterparties their accounts accept. This is useful for internal wallets, customer wallets, treasury wallets, and accounts that need to avoid unwanted tokens. - -### On/off-ramps and payment processors - -Receivers can limit inbound funds to known assets or known counterparties. That helps operational teams align onchain receipt behavior with compliance, risk, and reconciliation workflows. - -### Stablecoin orchestrators - -Platforms that route between stablecoins can reduce user error by preventing sends to deposit addresses that are not meant to receive a particular asset. - -## What builders should plan for - -Builders should plan to: - -- Let receivers configure token and sender allowlists or blocklists. -- Show recovery authority choices in plain language. -- Warn senders when a transfer is likely to be held. -- Index held-send events so support and operations teams can find them. -- Build recovery workflows for sender, receiver, or third-party controlled funds. - -## How this fits with TIP-20 - -Receive policies complement the issuer controls already built into TIP-20 and TIP-403. Token issuers can still enforce their own policies for regulated assets. Account-level receive policies add a receiver-side layer for businesses and users that need to decide what their own accounts are willing to accept. - -Together, these controls make Tempo more practical for payment applications that need the flexibility of open stablecoin rails and the operational safeguards expected by financial platforms. - -## Related reading - - - - - - - diff --git a/src/pages/learn/use-cases/agentic-commerce.mdx b/src/pages/learn/use-cases/agentic-commerce.mdx deleted file mode 100644 index 9e9c0653..00000000 --- a/src/pages/learn/use-cases/agentic-commerce.mdx +++ /dev/null @@ -1,61 +0,0 @@ ---- -description: Power autonomous AI agents with programmable stablecoin payments for goods, services, and digital resources in real time. ---- - -import { Cards, Card } from 'vocs' -import { ZoomableImage } from "../../../components/ZoomableImage.tsx" - -# Power AI agents with programmable money [Enable autonomous agents to purchase goods, services, and digital resources with real-time, programmable stablecoin payments.] - -## The rise of autonomous agents - -Autonomous agents are increasingly able to purchase goods and services, negotiate discounts, and manage everyday tasks without human input. An agent might reorder groceries when supplies run low, find better deals on household items, pay for digital services, manage streaming or app subscriptions, or book travel based on preferences and schedules. - -Further, the agentic commerce ecosystem is evolving quickly. New protocols are emerging that enable agents to interact directly with merchants: browsing offers, comparing terms, and purchasing goods or services autonomously. These protocols will allow businesses to sell directly to autonomous agents, whether they offer physical goods, digital items, compute, APIs, or data streams. - -## The challenge with traditional payment rails - - -However, for autonomous agents to function reliably, they need real-time, digitally native money that matches the speed and autonomy of their decisions. This money must be available 24/7, settle instantly, and support frequent microtransactions without friction. It also needs to work seamlessly across borders and offer in-built programmability so agents can operate without human intervention. - -Traditional payment rails were not designed to meet these requirements. Although card tokenization is being explored as a way for agents to pay, cards were never intended for machine-to-machine interactions. The entire card ecosystem is built around human cardholders and fraud models designed to stop bots, not support them. - -## How stablecoins solve this - - -Stablecoins fill this gap. They provide a stable, programmable representation of money that agents can use autonomously. Stablecoins combine the familiarity of fiat value with the instant, global settlement of public blockchains. Agents can hold stablecoins, pay for goods or services, and transact with other agents without relying on intermediaries. - - -Agents also need an easy way to access payment credentials. Creating cards or card tokens requires coordination with issuers, and, in many cases, manual workflows. In contrast, onchain wallets can be provisioned instantly, at massive scale, and without relying on banking partners. Wallets give agents a ready-to-use financial instrument the moment they are created. - -## Benefits beyond speed and cost - - -For developers building agentic platforms, stablecoins and wallets are far simpler and more flexible to work with than traditional payment instruments. Wallets can be created instantly and at scale, without issuer partnerships or card-provisioning workflows. Stablecoins also make it easy to top up agent balances instantly whenever additional funds are needed. - - -Developers can also set spending limits, enforce policy rules, and implement automatic top-ups directly in code, ensuring agents always have the funds they need without human intervention. Stablecoin balances can be monitored and rebalanced programmatically, enabling precise liquidity management and predictable spending across large fleets of autonomous agents. - - -As agentic commerce begins to scale, the need for a more native payment rail becomes unavoidable. Stablecoins provide exactly the properties agents require: real-time global settlement, programmable behavior, and the ability to operate without intermediaries or issuer dependencies. They offer payment infrastructure that aligns with how agents think, act, and transact. - -## Building with Tempo - -Autonomous agents require payment rails that are as programmable and tireless as they are. Tempo offers the throughput and sub-second settlement needed to support agent fleets at scale, without the friction of traditional card networks. - -If you are architecting payment flows for autonomous systems, we can help you understand how to implement automated wallet provisioning and policy-based spending controls directly into your agent's code. - - - - - diff --git a/src/pages/learn/use-cases/embedded-finance.mdx b/src/pages/learn/use-cases/embedded-finance.mdx deleted file mode 100644 index 63157f6c..00000000 --- a/src/pages/learn/use-cases/embedded-finance.mdx +++ /dev/null @@ -1,55 +0,0 @@ ---- -description: Enable platforms and marketplaces to streamline partner payouts, lower payment costs, and launch rewarding loyalty programs. ---- - -import { Cards, Card } from 'vocs' -import { ZoomableImage } from "../../../components/ZoomableImage.tsx" - -# Bring embedded finance to life with stablecoins [Enable platforms and marketplaces to streamline partner payouts, lower payment costs, and launch more rewarding loyalty programs.] - -## The challenge with traditional embedded finance - -Embedded finance was meant to help platforms and marketplaces offer smoother, more integrated user experiences. Yet its impact remains constrained by traditional payment rails, whose delays, fees, and operational complexity undermine much of the value embedded finance aims to provide. - -For example, gig economy and creator work happens around the clock, while traditional payout rails do not. ACH transfers, while inexpensive, operate only during weekday banking windows and cannot support real-time payouts. Even when platforms offer instant payouts via card networks, these transfers carry higher fees, and the costs are often passed on to partners. - -Merchant payouts on marketplaces face similar challenges. Settlement is often delayed by banking windows, regional cutoff times, and inconsistent payment rails across countries. Cross-border merchant payments frequently move through multiple intermediaries, adding cost and introducing unpredictability in when funds will arrive. - -## How stablecoins solve this - -By embedding blockchain wallets directly into their applications, these platforms can move onto modern, always-on payment rails. Partners, creators, and merchants can receive earnings in stablecoins far faster than through traditional methods, with settlement occurring onchain in seconds. Stablecoins also make these payouts inherently global, removing many of the barriers of regional banking systems. - - -Once partners, creators and merchants receive their earnings in the embedded wallet, they can manage those funds directly within the same application they use for work. The wallet can hold stablecoin balances, allow payouts to linked bank accounts, or support spending for goods and services through a stablecoin-linked payment card. - -Stablecoins and embedded wallets can support far more than payouts. Marketplaces can also use stablecoins to collect consumer payments, deliver instant refunds, and issue loyalty rewards. Stablecoin payments do not have the settlement delays and interchange fees of card networks, making them a faster and cheaper payment option. - - -## Benefits beyond speed and cost - -By significantly reducing the cost of collecting payments from consumers and paying out partners, creators, and merchants, marketplaces can redirect more value toward customer and merchant benefits. Lower payment costs give merchants more flexibility to reward loyal customers with better offers, reimagining their loyalty programs. After all, merchants ultimately fund card rewards through interchange fees. - -For platforms, embedded wallets and stablecoin rails replace a patchwork of payment processors, banking partners, and regional settlement rules with a single, consistent infrastructure. Reconciliation relies on unified onchain records rather than scattered bank statements, and operational teams spend far less time managing exceptions or coordinating across time zones. - -Faster payouts, lower-cost payment acceptance, and more dynamic loyalty programs are only the beginning of what stablecoins bring to embedded finance. As stablecoins become the foundation for agentic commerce, platforms will gain new opportunities to create value for customers and expand what embedded finance can offer. - -## Building with Tempo - -By providing a low-cost, high-speed settlement layer, Tempo enables platforms to offer financial products that were previously cost-prohibitive on legacy rails. Whether you are looking to embed wallets for gig workers or streamline complex marketplace settlements, our team can guide you through the wallet integration patterns that best fit your user experience. - -We are available to help you evaluate the technical lift and commercial impact of moving your platform's payments onchain. - - - - - diff --git a/src/pages/learn/use-cases/global-payouts.mdx b/src/pages/learn/use-cases/global-payouts.mdx deleted file mode 100644 index 35de62b7..00000000 --- a/src/pages/learn/use-cases/global-payouts.mdx +++ /dev/null @@ -1,192 +0,0 @@ ---- -description: Deliver instant, low-cost payouts to contractors, merchants, and partners worldwide with stablecoins, bypassing slow banking rails. ---- - -import { Cards, Card } from 'vocs' - -# Send global payouts instantly [Deliver faster, cheaper, and more predictable cross-border payouts to contractors, merchants, and partners around the world with stablecoins.] - -## The challenge with global payouts - -Managing global payouts is both complex and expensive. Businesses must either rely on slow, expensive, unpredictable cross-border transfers to pay recipients directly, or first move liquidity to local subsidiaries to access domestic payment rails. Each domestic rail, in turn, comes with its own rules, banking holidays, cutoff times, formats, and fees. - -This complexity results in a patchwork of manual workflows that is burdensome for finance teams and often frustrating for recipients waiting on delayed transfers. Stablecoins offer a way to simplify this process by reducing cost, improving predictability, and removing much of the operational overhead associated with traditional cross-border payments. - - -## The real cost of traditional payouts - -Consider an example: when you send $1,000 to a contractor in Brazil using traditional rails, the costs add up quickly. Your payment processor may charge a $10–$40 fee per payment, while intermediary banks take anywhere from 0.1% to 4.0% on the FX spread. By the time the payment clears five days later, the recipient has lost $80 to fees. If local currency moved adversely in that window, they would lose again. - -## Common pain points this removes - -Traditional cross-border payouts fail in predictable ways: - -- **High all-in cost:** Fees plus FX spread leakage compounds at scale. -- **Slow settlement:** 3-5 business days plus holidays and cutoffs. -- **Failures and reconciliation overhead:** Incorrect recipient details or intermediary issues create manual rework. -- **Currency volatility exposure:** Recipients can lose value between send and receive. - -## How stablecoins solve this - -Blockchains are global by design, allowing companies to send stablecoin payouts directly to recipients' wallets anywhere in the world without relying on local banks or intermediaries. These transfers typically cost less than a cent and arrive within seconds, offering far more consistency than traditional international payment rails. - -When you send $1,000 to a contractor in Brazil using stablecoin rails, the process is far more straightforward: your stablecoin partner may charge a flat fee of 0.1–0.4% for the onramp and transfer, delivering a **60–80% cost reduction** compared to traditional rails. Recipients receive dollar-denominated stablecoins and can convert to local currency when and how they choose, typically paying a transparent offramp fee in the 0.1–2.0% range. - -Cost reduction note: Traditional wire fees on a $1,000 payout typically range $10-$40 (1-4%) excluding FX spreads. Stablecoin onramp fees range from 0.1-0.4%. Comparing 0.4% vs 1.0% yields approximately 60% reduction; higher savings occur vs higher wire fees and FX spreads. - -Companies can also choose to distribute funds through their subsidiaries. In this model, a business can convert funds into stablecoins at the headquarters level, such as in the United States, and distribute those funds to subsidiary wallets around the world. Liquidity moves to subsidiaries in seconds rather than days, removing the need to pre-fund subsidiary accounts in advance. Subsidiaries can then handle payouts to local recipients. - -## Key benefits - - - - - - - -For structured payment references (invoice IDs, customer IDs), see [TIP-20 Tokens](/learn/tempo/native-stablecoins). - - - - - - -## Benefits beyond speed and cost - -Recipients receiving stablecoins can choose how they want to use their funds. They can hold stablecoins and spend them directly through stablecoin-linked payment cards, or convert them into fiat and withdraw the funds to a local bank account whenever they choose. This gives recipients flexibility and control over how they manage their income. - -Stablecoins also reduce foreign exchange complexity. Instead of converting funds into multiple local currencies and dealing with unpredictable spreads and fees, companies can make payouts in a single currency, such as USD stablecoins. Recipients can then convert to their desired currency when it's most convenient for them. This simplifies treasury operations and provides more options for recipients. - -Finally, reconciliation becomes significantly simpler. Because stablecoin payouts settle onchain, every transaction has a clear record. Companies no longer need to track separate reporting formats from multiple domestic banks or reconcile transactions that settle at different speeds. Instead, they rely on a single, unified ledger that provides immediate visibility into outgoing payments, balances, and settlement status. - -Global payouts with stablecoins already deliver meaningful benefits for both senders and recipients. As familiarity grows and merchant acceptance expands, stablecoins are likely to become a preferred payment method for international disbursements of all kinds. With lower costs, real-time settlement, and programmability, stablecoins bring payout systems closer to the always-on, digital nature of today's global economy. - -For native batching and scheduled payout execution, see [Tempo Transactions](/learn/tempo/modern-transactions). - -## Evaluating stablecoins for global payouts - -Stablecoin adoption starts with a clear business use case. Before evaluating vendors or integration paths, consider this framework: - -- **Do you pay people internationally?** If you are paying contractors, merchants, or employees across borders, you are already managing multiple currencies, banks, and regulatory regimes. -- **Do those payments create friction today?** Delays, failed transfers, opaque fees, FX leakage, and reconciliation errors are signals that your current payout rails are not keeping up. -- **Are you forced to pre-fund payouts?** Long settlement times often require holding excess balances across banks and currencies, tying up working capital. -- **Would reducing that friction materially impact your business?** Faster payouts, lower costs, and happier recipients can improve retention, reduce operational overhead, and unlock new geographies. -- **Can instant payouts become a new revenue stream?** Many platforms offer real-time payouts as a premium feature, turning speed and reliability into a paid upgrade rather than a cost center. - -If the answer to these questions is yes, you should be evaluating the stablecoin partner ecosystem. The infrastructure is ready, regulation is clearer, and the economics are hard to ignore. - -## Getting started - -Payouts are one of the cleanest entry points for stablecoins because you can pilot quickly and measure impact. - -:::::steps - -### Clarify the business case - -Pick your highest-friction corridor (e.g., U.S. to Brazil, Philippines, or Nigeria). Identify where fees, failed payments, or delays create real pain. - -### Choose an integration approach - -Decide whether to **buy** (provider abstracts custody, liquidity, compliance) or **build** (you assemble wallets, custody, bank rails, and potentially licensing). - -### Run a pilot - -Run stablecoin payouts alongside your existing system for approximately 60 days. Track delivery time, net recipient received, support tickets, and reconciliation effort. - -### Operationalize - -Define SLAs, exception handling, reconciliation processes, and support playbooks, then expand corridors and automate what was manual. - -::::: - -## Building with Tempo - -Designed for high-volume, global disbursement, Tempo allows organizations to bypass the fragmentation of local banking rails and reach recipients directly. We work closely with platforms, marketplaces, and payout operators to design stablecoin payout flows that are compliant, efficient, and user-friendly. - -If you are interested in seeing how a unified onchain ledger can simplify your global payout operations and remove FX friction, let's discuss the integration requirements and coverage capabilities. - - - - - - -## FAQs - -### Do I have to hold and manage stablecoins? - -No. Many infrastructure providers, such as BVNK, Bridge, and ZeroHash, abstract the complexity of managing stablecoins, wallets, or licensing. You connect to their API to instruct a stablecoin payout, send funds from your bank account in fiat currency, and they handle on- and off-ramp, custody, and liquidity management. - -### Will recipients understand stablecoins? - -They don't have to. Historically, stablecoin payouts were used primarily by crypto-native developers and contractors. That is changing as new fintech apps hide the complexity. Modern stablecoin infrastructure, like embedded wallets, can be invisible. The recipient gets a notification that payment arrived and sees a balance in US dollars on a mobile app. - -### What happens if a recipient wants local currency? - -Recipients can convert stablecoins to local currency through supported off-ramps as soon as funds arrive. This is often a one-click experience inside a fintech app or exchange, with transparent fees that are typically far lower than traditional bank FX spreads. Stablecoin orchestration platforms can also automate this process end-to-end. - -### Is this only useful for emerging markets? - -No. While the benefits are most visible in emerging markets with high fees or volatile currencies, stablecoin payouts are increasingly used in developed markets as well, especially for instant payouts, weekend payments, or premium payout features. - -### Is this regulated? - -Yes, increasingly so. Since the passage of the GENIUS Act in the United States, the compliance landscape for stablecoins has become clearer. Any money transmitter, financial institution, or nonbank money mover is subject to travel rule requirements. In practice, regulatory requirements like KYC/KYB and screening of senders over $1,000 are lower risk in payout use cases because the payer is the corporation itself. - -## Related reading - - - - - - - diff --git a/src/pages/learn/use-cases/microtransactions.mdx b/src/pages/learn/use-cases/microtransactions.mdx deleted file mode 100644 index 16303186..00000000 --- a/src/pages/learn/use-cases/microtransactions.mdx +++ /dev/null @@ -1,56 +0,0 @@ ---- -description: Enable true pay-per-use pricing with sub-cent payments for APIs, content, IoT services, and machine-to-machine commerce. ---- - -import { Cards, Card } from 'vocs' -import { ZoomableImage } from "../../../components/ZoomableImage.tsx" - -# Enable true pay-per-use pricing [Build usage-based billing, pay-per-use APIs, and machine-to-machine services with real-time, sub-cent stablecoin payments.] - -## The challenge with traditional payment rails - - -Digital services are increasingly shifting toward usage-based models, where users pay only for what they consume. This includes everything from API calls and AI inference requests to streaming content, and IoT services. Yet traditional payment rails were never designed for extremely small, frequent payments, making microtransactions impractical or impossible for most businesses today. - -Card networks apply fixed fees to every transaction, making any payment under a dollar uneconomical. Even instant rails like FedNow, while cheaper than cards, cannot support usage-based billing where each action costs only a few cents. This forces many digital products into subscriptions or prepaid balances instead of offering the more user-friendly and transparent pay-as-you-go model. - -## How stablecoins solve this - - -Stablecoins change this dynamic by enabling payments that cost a fraction of a cent and settle in seconds, making true microtransactions economically feasible at scale. They achieve this by removing the chain of intermediaries found in traditional payment rails, eliminating the delays, costs, and failure points that make small, frequent payments impractical today. - - -Programmability further expands what microtransactions can support. Smart contracts can meter usage, enforce payment rules, and top up wallet balances automatically. Developers can charge per API request, per compute cycle, or per any digital action, without building custom billing logic or relying on intermediaries. - -Microtransactions also unlock machine-to-machine payments. Autonomous agents, bots, and IoT devices can pay for data, services, or compute in real time without human intervention. A device can pay another device for sensor data; an AI agent can pay for inference or model access; a drone can pay for map segments or airspace permissions. - -Stablecoins also enable pay-as-you-go digital commerce for consumers. Instead of monthly subscriptions, consumers can pay per article, per minute of video, per chapter of a book, or per stream of a song. Creators and platforms can monetize content more flexibly, while users pay only for what they actually consume. - -## Benefits beyond speed and cost - -From a developer's perspective, building microtransaction-powered services is becoming increasingly straightforward. Stablecoins provide a simple, programmable payment primitive. At the same time, new protocols are emerging that standardize usage-based payments, authorization, and settlement, making it even easier to launch pay-per-use applications on top of stablecoin rails. - - -Finally, onchain microtransactions improve reconciliation. Instead of aggregating thousands of small events into batched invoices that must be reconciled manually, stablecoins provide a unified ledger of each individual payment. This reduces operational overhead and simplifies financial reporting for businesses handling large volumes of small-value transactions. - - -Ultimately, stablecoin-powered microtransactions are not simply about reducing payment friction. They unlock new business models built around real-time data and usage. By enabling instant, sub-cent payments, stablecoins let companies price, deliver, and monetize digital services in ways that were never feasible before, creating meaningful competitive advantages for those who adopt them early. - -## Building with Tempo - -True microtransactions require a blockchain that eliminates the economic floor of traditional payment fees. Tempo's architecture is designed to make sub-cent payments viable, unlocking entirely new business models for digital services, APIs, and content platforms. If you are calculating the economics of usage-based billing or machine-to-machine payments, we can help you run the numbers and explore the technical feasibility of building on Tempo. - - - - - diff --git a/src/pages/learn/use-cases/payroll.mdx b/src/pages/learn/use-cases/payroll.mdx deleted file mode 100644 index b384e298..00000000 --- a/src/pages/learn/use-cases/payroll.mdx +++ /dev/null @@ -1,138 +0,0 @@ ---- -title: Stablecoins for Payroll -description: Learn how payroll providers can use stablecoins for faster account funding, cheaper cross-border payouts, and embedded wallet revenue. ---- - -import { Cards, Card } from 'vocs' - -# Turn payroll into a platform with stablecoins [Faster funding, cheaper cross-border payouts, and new revenue streams for payroll providers.] - -## The challenge with traditional payroll infrastructure - -Payroll providers are starting to take stablecoins seriously. The reasons are familiar to anyone running global payroll: ACH funding takes 2-3 days and carries return risk, cross-border settlement routes through multiple intermediaries that each extract fees, and employees in emerging markets lose money to forced currency conversion. - -## How stablecoins solve this - -Stablecoins fix all of that, but the opportunity goes well beyond faster and cheaper payments. There are three distinct use cases, and they build on each other: faster payroll account funding, cheaper cross-border payouts to employee wallets, and embedded wallets that turn payroll into a financial services platform. Each stands on its own, and each increases in ambition and payoff. - -### 1. Faster payroll account funding - -Funding payroll accounts with stablecoins is the lowest-lift opportunity and the one most providers overlook. - -Today, employers fund payroll accounts via ACH, which takes 2-3 days to settle and can be returned after employees have already been paid. The provider carries that exposure. Stablecoin funding settles in seconds. Same-day payroll cycles become possible without the provider absorbing settlement risk. - -Nothing changes for employees here. They still receive payroll the same way. The improvement is entirely on the funding side: faster cycles, no float risk, simpler cash management. Banking platforms like Ramp, Brex, and Meow are already adding stablecoin support, which makes it increasingly easy for employers to fund payroll accounts in stablecoins. - -For providers processing significant volume, eliminating 2-3 days of settlement risk and the operational overhead of managing ACH returns is meaningful on its own. - -### 2. Cheaper cross-border payouts - -Cross-border payroll routes through payment processors, correspondent banking networks, and local receiving banks. Each intermediary takes a cut and employees absorb FX spreads of 1-4% when their payment is converted to local currency — a cost they can't negotiate and often can't see. - -Stablecoins cut through the intermediary chain. Payouts settle in seconds to wallets employees already have, regardless of time zone or bank holiday. Transaction costs drop to cents. The total [cost reduction for cross-border payouts runs 60-80%](/learn/use-cases/global-payouts). Providers don't need to build wallet infrastructure either; employees can bring their own. - -For employees, the experience also changes for the better. They receive dollar-denominated stablecoins and choose when (or whether) to convert to local currency. In markets with currency volatility — like Argentina, Nigeria, or Turkey — the ability to hold dollars and convert on their own terms preserves purchasing power. Workers stop losing money to exchange rates set by banks they didn't choose. - -This is where payroll providers start to differentiate. If your competitors are still routing through correspondent banking and your clients' contractors are getting paid in seconds at a fraction of the cost, that's a tangible advantage in competitive deals. - -For a deeper look at cross-border payout mechanics, cost comparisons, and implementation steps, see [Global Payouts](/learn/use-cases/global-payouts). - -### 3. Embedded wallets and financial services - -The first two use cases make payroll faster and cheaper. The third one changes the business model. - -Today, payroll providers handle the payout. Banks capture everything that happens after: employee deposits get monetized through cards, savings accounts, loans, and investment products. The payroll provider did the work of acquiring and paying the employee, but the bank captures the ongoing financial relationship. - -Embedded wallets flip this. When a payroll provider issues a wallet to each employee, the provider controls the user interface and maintains the relationship after funds land. That opens three revenue streams that don't exist in traditional payroll: - -- **Branded stablecoins and reserve yield.** Instead of paying employees in USDC, a provider can issue its own stablecoin backed 1:1 by reserves (US Treasuries and deposits). The provider earns the yield on those reserves for as long as employees hold balances. Partners like Bridge handle issuance and compliance. - -- **Card interchange.** Stablecoin-backed cards with automatic conversion at point of sale generate 1-2% interchange on spend. For a client with 1,000 employees spending $2,000/month on cards, that's $240K-$480K in annual interchange revenue. - -- **Tokenized investment products.** Wallets can offer tokenized money market funds yielding 3-3.5%, or structured credit products at 6-8%. This gives employees access to yields they wouldn't typically get through a traditional payroll deposit, and creates distribution revenue for the provider. - -The wallet approach also simplifies global rollout. Non-custodial wallets don't hold customer funds, so they don't require banking or payment institution licenses in every jurisdiction. A provider can launch wallets across dozens of countries with the same infrastructure. That's fundamentally different from custodial financial services, which need per-market licensing. - -This is the most ambitious path, and it requires investment in infrastructure, compliance, and product. However, it's also the path that transforms payroll from a low-margin transaction business into a platform with compounding revenue streams. The unit economics shift from "fee per payout" to "lifetime value of a financial relationship." - -For more on embedded wallet patterns for platforms, see [Embedded Finance](/learn/use-cases/embedded-finance). - -## What to evaluate before building - -Not every payroll provider needs to go to level three on day one. The right starting point depends on your client base and where the pain is sharpest. - -- **Where are your clients' employees?** If you're primarily in the US or Europe, the pain points are real but less acute. If you serve companies with contractors across Latin America, Africa, or Southeast Asia, the case for stablecoins is immediate and measurable. - -- **What corridors are most expensive or unreliable?** Focus stablecoin payouts where correspondent banking is slowest and most expensive. Start there. - -- **Do you want to capture value beyond the transaction fee?** If the answer is yes, the wallet path is worth serious evaluation. You can start with payroll account funding and cross-border payouts while building toward wallets. - -This is a multi-year build, and partner selection matters more than most providers expect. Choose partners who are thinking long term. - -## Other factors to consider - -Stablecoins solve real problems, but they create new considerations. - -- **Regulation and compliance don't disappear.** Stablecoins change the payment mechanism, not the regulatory framework. Tax withholding, labor law, and reporting requirements still apply in every jurisdiction. Some countries restrict or prohibit stablecoin payments entirely, so you need to verify the landscape in every market where you operate. - -- **Not all employees will want stablecoins.** Maintaining parallel payroll rails adds complexity, but forcing adoption creates friction. Offer stablecoins as an option, not a mandate. - -- **Employee education takes work.** Workers unfamiliar with digital wallets need clear onboarding. Support teams need training. This is operational investment that's easy to underestimate. - -## Building with Tempo - -The stablecoin you pay employees with is only as reliable as the infrastructure it runs on. At scale, the differences between chains become operationally significant. - -- **Privacy for sensitive payroll data.** Salary information on a public blockchain is a non-starter. Tempo's opt-in privacy solution keeps salary amounts, employee identities, and payment patterns shielded from the public while maintaining full auditability for compliance and internal controls. Employees and employers get privacy by default. Auditors and regulators can access data when required. - -- **Predictable delivery during congestion.** Dedicated payment lanes ensure payroll transactions process on schedule even when the network is busy. Missing a payroll cycle because of network congestion is not acceptable. - -- **Compliance and reconciliation built into the chain.** Native memo fields attach travel rule identifiers, invoice references, and reconciliation metadata directly to transactions, so payroll records map cleanly to onchain activity. Allow/blocklists for transaction control. These aren't add-ons; they're native to the chain. - -- **No account rent.** When you're provisioning wallets for hundreds of thousands of employees, account rent fees on other chains become a real cost line. Tempo doesn't charge account rent. - -- **Low, predictable fees paid in stablecoins.** No token management overhead. No gas price volatility. Fees are measured in cents and can be sponsored so employees never see them. - - - - - - -## Related reading - - - - - - - diff --git a/src/pages/learn/use-cases/remittances.mdx b/src/pages/learn/use-cases/remittances.mdx deleted file mode 100644 index 7b0b664d..00000000 --- a/src/pages/learn/use-cases/remittances.mdx +++ /dev/null @@ -1,62 +0,0 @@ ---- -description: Send cross-border payments faster and cheaper with stablecoins, eliminating correspondent banks and reducing transfer costs. ---- - -import { Cards, Card } from 'vocs' -import { ZoomableImage } from "../../../components/ZoomableImage.tsx" - -# Send money home faster and cheaper [Deliver instant cross-border payments to your customers around the world with stablecoins on Tempo.] - -## The challenge with traditional remittances - -Cross-border payments are notoriously slow, expensive, and unpredictable. A single transfer often passes through multiple intermediaries, each adding cost, introducing delays, and increasing the risk of errors. Newer "instant" cross-border services improve the user experience, but typically require remittance companies to hold liquidity on both sides of a payment corridor, which increases operational and capital costs. - -## How stablecoins make remittances better - -Stablecoins can address these challenges by enabling cheap and near-instant cross-border payments. Beyond peer-to-peer transfers, where users send stablecoins directly to each other, three stablecoin-based models are now widely used in the remittance industry: - -### Direct transfers - -A sender, for example, in the United States, can onramp to stablecoins within the mobile banking app and transfer them directly to a recipient abroad. The recipient can choose to hold the funds in stablecoins, spend them with a stablecoin-linked card, or off-ramp into local currency and deposit into a bank account. - - -### Orchestrated payments - -A sender initiates a cross-border payment from a mobile app, and the recipient receives funds directly into their bank account. Stablecoins are routed through a stablecoin orchestration platform that automatically converts them into local currency, preserving the speed and low cost of stablecoin transfers while delivering a familiar, traditional cross-border payment experience for both sender and recipient. - - -### Backend settlement - -Stablecoins operate entirely behind the scenes. A remittance company uses stablecoins to move liquidity between its own entities across jurisdictions, eliminating the need for correspondent banks and removing the requirement to maintain expensive, pre-funded liquidity on each side of the corridor. - - -In each case, blockchains and stablecoins help eliminate the patchwork of correspondent banks in between the sender and the recipient, or the need to hold liquidity to emulate "instant" payments. Stablecoin transfers typically cost less than a cent and arrive within seconds, introducing noticeable cost reduction and increasing speed. - -## Benefits beyond speed and cost - -Beyond the speed and cost improvements, stablecoin-based remittances offer a far smoother user experience for both senders and recipients. With no correspondent banks or intermediate hops, there are fewer points of failure and no unexpected holds, returns, or error paths. Transfers settle onchain in seconds, at any time of day, and the recipient sees the funds immediately. - -Further, users around the world increasingly value being able to hold USD-denominated stablecoins. These provide a simple and affordable way to send and receive remittances and pay for global digital services such as LLMs, software subscriptions, and online content platforms. - -Onchain settlement also delivers major reconciliation benefits. Each transaction is recorded in real time on a single ledger, giving remittance providers instant visibility into balances and flows. This reduces reliance on delayed bank statements, minimizes reconciliation errors from intermediary hops, and streamlines audit and compliance through a single, authoritative onchain record. - -## Building with Tempo - -Tempo is purpose-built to handle the volume and cost requirements of modern remittance corridors, enabling providers to move beyond the limitations of correspondent banking. We understand that transitioning to onchain settlement involves navigating complex liquidity and operational questions. - -If you are evaluating how to restructure your remittance corridors for higher speed and lower cost, our team can help you map out the architecture for your specific regions. We invite you to explore the technical and operational improvements stablecoins can bring to your flows. - - - - - diff --git a/src/pages/learn/use-cases/tokenized-deposits.mdx b/src/pages/learn/use-cases/tokenized-deposits.mdx deleted file mode 100644 index 08690998..00000000 --- a/src/pages/learn/use-cases/tokenized-deposits.mdx +++ /dev/null @@ -1,182 +0,0 @@ ---- -description: Move treasury liquidity instantly across borders with real-time visibility into global cash positions using tokenized deposits. ---- - -import { Cards, Card } from 'vocs' - -# Move treasury liquidity instantly across borders [Enable corporate treasury teams to improve liquidity management across banks, currencies, and regions with tokenized deposits.] - -## The challenge with traditional treasury management - - -Operating across multiple geographies requires companies to maintain relationships with multiple banks, manage balances in several currencies, and navigate an array of local settlement systems, cutoff times, and time zones. For global treasury teams, this results in a fragmented operational landscape with inconsistent processes across regions. - -Moving liquidity between a company's own accounts can take days and require subsidiaries to hold excess cash "just in case." Visibility into cash positions is incomplete, workflows vary by region, and reconciling activity across dozens of accounts becomes an ongoing operational burden. The result is inefficient use of capital and a patchwork of manual processes for controls and compliance. - -For companies operating across multiple jurisdictions, correspondent banking creates three specific problems: - -- **Higher borrowing costs** from trapped cash that cannot be deployed efficiently -- **Suboptimal hedging** based on incomplete or delayed data from disconnected systems -- **Excess bank fees** for accounts holding idle liquidity across regions - -## How tokenized deposits and stablecoins solve this - -Tokenized deposits and stablecoins offer a path toward simplifying this complexity. Instead of managing dozens of accounts across multiple banks, companies can operate a single wallet per subsidiary. Sitting alongside traditional bank accounts, these digital wallets become a natural extension of corporate treasury operations and provide a more efficient way to manage global liquidity. - - -Each wallet can hold stablecoins, tokenized deposits, and even tokenized money market funds on the same unified onchain infrastructure. Tokenized deposits and stablecoins introduce a shared set of benefits that transform how corporates store, move, and deploy liquidity: - -- **Liquidity can be moved 24/7**, across time zones, and across borders, removing the delays of local settlement systems and cutoff times; -- **Reconciliation becomes simpler** through onchain settlement records instead of disparate bank statements and siloed reporting systems; -- **Idle balances can be deployed more efficiently**, improving working-capital usage and enabling immediate access to interest-bearing tokenized assets; -- **Treasury workflows such as approvals, sweeps, and controls can be automated** with smart contracts, reducing manual processes and operational overhead. - -See also: [Global payouts with stablecoins](/learn/use-cases/global-payouts) for the outbound payments use case. For payment references and structured metadata, see [TIP-20 Tokens](/learn/tempo/native-stablecoins). For batching and scheduled treasury movements, see [Tempo Transactions](/learn/tempo/modern-transactions). - -## How stablecoins fit into treasury workflows - -Stablecoins are most useful when they remove timing and intermediary constraints (cutoff times, weekends, correspondent banks). Common first deployments: - -- **Intercompany settlements:** Move USD liquidity between parent and subsidiary entities instantly and settle back to bank accounts on your own schedule. -- **Cash positioning and forecasting:** Use onchain balances as a real-time view of global liquidity rather than end-of-day bank reports. -- **International payouts:** Use stablecoins as the settlement leg for high-volume payouts; recipients can receive stablecoins or be paid in local currency via offramps. - -## Current limitations - -Stablecoins complement bank accounts rather than replace them. Key constraints to plan for: - -- **Limited non-USD liquidity:** Most stablecoin liquidity is USD-denominated; EUR and GBP liquidity is improving but thinner. -- **Off-ramping coverage varies:** Not all markets have reliable bank offramps yet. -- **Custody and controls:** Decide between self-custody vs institutional custody (multi-sig, insurance, governance). -- **Systems integration:** Treasury systems may need adapters for onchain settlement data. -- **Operational risk differs by asset and network:** Prefer regulated, 1:1 reserve-backed assets; evaluate chain uptime and fee variability. - -## Reconciliation and operational risk - -### Automated GL reconciliation - -Fast settlement only matters if transactions reconcile cleanly. Use transfer metadata (e.g., invoice ID, cost center, GL code) so treasury movements can auto-post back into ERP/TMS. TIP-20 tokens support native transfer memos designed for reconciliation: see [TIP-20 Tokens](/learn/tempo/native-stablecoins). - -### Controls and auditability - -24/7 rails require strong approval policy. Enforce role-based permissions and programmable approvals so each movement is attributable and auditable end-to-end. - -## Understanding tokenized deposits vs. stablecoins - -As digital wallets become integrated into corporate treasury operations, it is important to understand how stablecoins and tokenized deposits differ. They are distinct instruments with different issuers, obligations, and regulatory treatment: - - - **Tokenized deposits** are commercial bank deposits issued on a blockchain. They remain a liability of the issuing bank and sit within the existing banking regulatory framework, behaving much like traditional deposits, but with the added benefits of programmability and real-time settlement. - - **Stablecoins**, by contrast, are issued by regulated non-bank stablecoin providers and backed 1:1 with reserves such as cash and short-term government securities. They are designed to maintain a stable value relative to currencies like the U.S. dollar or Euro and can move across blockchains unaffected by cutoff times, banking hours, or regional settlement windows. - -## The opportunity for financial institutions - -For financial institutions, tokenized deposits represent a major opportunity to strengthen existing corporate relationships and win market share as treasury infrastructure moves onchain. Large enterprises are already adopting stablecoins because they provide the speed, global reach, and programmability that traditional banking infrastructure cannot match. - -Tokenized deposits give financial institutions a way to offer many of these same benefits while maintaining the trust and regulatory certainty that corporates rely on. Tokenized deposits are also far more familiar to large corporations: they behave like traditional deposits, remain a liability of the issuing bank, and fit naturally into existing accounting and compliance models. - - -The main challenge for banks today is interoperability. Most tokenized deposit pilots run inside the closed perimeter of a single institution, limiting their usefulness in multi-bank, multi-region treasury setups. Corporations will only adopt tokenized deposits at scale if they can move funds seamlessly across banks and easily convert between tokenized deposits, stablecoins, and other tokenized assets. - -Public blockchains provide this missing bridge. They allow tokenized deposits from different banks to coexist on shared infrastructure, enabling frictionless swaps between tokenized deposits and stablecoins, and supporting cross-bank liquidity movement with the same speed as stablecoin rails. Banks that issue tokenized deposits on public chains gain the interoperability corporates require, while strengthening their role at the center of onchain financial flows. - -## Key questions to answer - -Before engaging providers, answer these questions: - -1. **What problem are you solving?** Identify your highest-value use case: reducing FX costs, accelerating intercompany transfers, or improving cash visibility in specific regions. -2. **Use existing stablecoins or issue your own?** USDC and USDT are liquid and widely accepted for off-ramping. Issuing your own stablecoin allows you to move value between entities while maintaining yield, without converting to money market funds. -3. **Manage internally or via third party?** Wallet setup, custody, and liquidity provider integration can be complex, so many companies outsource to infrastructure providers like Bridge, BVNK, or ZeroHash. Some, like Ant Group, build in-house. - -By answering these questions, you'll be ready to define the value more clearly and begin to meet providers. - -## Getting started - -- **Assess pain points:** Where is cash trapped (weekends, slow corridors, expensive FX)? -- **Build a business case:** Compare current fees, spreads, and delays vs stablecoin rails using your own data. -- **Choose build vs buy:** Many teams start with infrastructure providers that handle custody, on/offramps, and compliance. -- **Pilot a contained flow:** Start with one region or corridor and run in parallel before expanding. - -## FAQs - -### Do I have to hold and manage stablecoins? - -No. You can use stablecoins purely as a settlement rail without holding balances directly. Third parties can manage the stablecoins outside your organization, while your existing bank relationships remain central to day-to-day operations. Over time, you may choose to hold stablecoins directly and expand into other onchain assets such as tokenized treasuries. - -### Will my bank support this? - -Many banks are actively developing stablecoin and tokenized deposit capabilities, and many more are planning to launch through the coming year. In the meantime, infrastructure providers such as Bridge and BVNK enable seamless movement between traditional bank accounts and stablecoins. - -### Who do I need to integrate with? - -There are two common approaches. In a buy model, a provider manages licensing, custody, wallets, and blockchain connectivity, so you may never interact with stablecoins directly. In a build model, you integrate directly with custodians, wallet providers, and blockchain networks. The right choice depends on your timeline, internal resources, and how much control you need. - -### What about regulatory and audit concerns? - -Any treasury implementation must meet regulatory expectations and withstand internal and external audit scrutiny. - -- **Regulatory status:** In the US, the GENIUS Act provides a federal framework. The EU's MiCA regulations are in force. The UAE and Singapore have established frameworks, and many jurisdictions expect to publish theirs in the coming year. -- **Audit trail:** Onchain transactions are permanently recorded with timestamps and cryptographic proof. -- **Compliance:** Work with providers offering transaction monitoring, sanctions screening, and AML tools for blockchain. -- **External auditor comfort:** Major issuers like Circle (USDC) and Paxos (USDP) publish monthly attestations. Stablecoin-as-a-service providers like Agora, Bridge, and M0 offer similar capabilities for custom issuance. - -### What are the risk considerations? - -The primary risks associated with stablecoin-based treasury flows fall into three categories: - -- **Counterparty risk:** Stablecoin issuer solvency and infrastructure provider stability. Mitigation: Stablecoins backed 1:1 by HQLA under regulatory frameworks like GENIUS represent lower risk. -- **Operational risk:** Variable fee structures and network performance across blockchains. Mitigation: Select providers offering fee consistency and evaluate reliability alongside speed. -- **Regulatory risk:** Potential restrictions in certain jurisdictions. Mitigation: Start with dollar-based movements between your own entities before expanding to external use cases. - -### How does this integrate with our ERP/TMS? - -Most TMS platforms (e.g. Kyriba, TreasuryXpress, GTreasury) support native integrations or API connections. A typical flow would look like this: - -1. Payment instruction originates in ERP/TMS -2. Instruction routes to a stablecoin infrastructure provider -3. Settlement occurs onchain -4. Confirmation and settlement data flows back to TMS -5. Accounting entries are generated automatically - -Tempo offers ISO 20022-compatible messaging, simplifying integration for teams already working with SWIFT MT messages. - -## Building with Tempo - -Tempo provides the neutral, high-performance infrastructure required for tokenized deposits and stablecoins to operate side-by-side. For treasury teams looking to modernize their liquidity stack, the challenge is often interoperability between these assets and existing banking systems. We collaborate with financial institutions and enterprises to model how these assets can coexist within your current treasury operations. Let's examine how onchain rails can fit into your broader liquidity management strategy. - - - - - - -## Related reading - - - - - - diff --git a/src/pages/performance.tsx b/src/pages/performance.tsx new file mode 100644 index 00000000..3421a88b --- /dev/null +++ b/src/pages/performance.tsx @@ -0,0 +1,12 @@ +'use client' + +import MarketingRoute from '../marketing/MarketingRoute' +import PerformancePage from '../marketing/PerformancePage' + +export default function Page() { + return ( + + + + ) +} diff --git a/src/pages/protocol/tips/_tip_template.mdx b/src/pages/protocol/tips/_tip_template.mdx deleted file mode 100644 index 034444b1..00000000 --- a/src/pages/protocol/tips/_tip_template.mdx +++ /dev/null @@ -1,42 +0,0 @@ ---- -id: TIP-XXXX -title: TIP Title -description: Short description for SEO -authors: -status: Draft | In Review | Approved | In Progress | Devnet | QA/Integration | Testnet | Mainnet | Deprecated -related: -protocolVersion: -searchable: false ---- - -# TIP-XXXX: TIP Title - -## Abstract -Short 2–4 sentence high level summary - -## Motivation - -Explain what problem this solves/functionality this introduces, and any alternatives considered (if applicable). Add context or links to other specs/resources that serve as prerequisites to this spec. - ---- - -# Specification - - -This section should provide a complete description of the feature’s behavior and required interfaces. - -If the feature introduces a precompile, this section should include the full interface definition along with comprehensive NatSpec. Each function should clearly describe its parameters, return values, and error conditions. The goal is to define the intended functionality clearly enough that an engineer can implement the reference contract, test suite, and node implementation without needing to infer any implementation details. - -For features that do not introduce a precompile, this section should define the exact mechanics of the feature/system. Describe the relevant state transitions, data structures, encodings, etc. When the feature interacts with existing components, explain how they relate and how data moves between them each system component. - -Where a feature involves multiple processes, state diagrams / flowcharts should be considered when helpful. - -# Compatibility - -Describe the impact of this TIP on external systems (indexers, market makers, analytics, wallets, tooling, etc.) and on existing on-chain state, events, or interfaces. Call out any breaking changes and the rules external systems must adopt to remain compatible. - -If this TIP has no external compatibility impact, write `N/A`. - -# Invariants - -This section should describe invariants that must always hold, and outline the critical cases that the test suite must cover. diff --git a/src/pages/protocol/zones/accounts.mdx b/src/pages/protocol/zones/accounts.mdx deleted file mode 100644 index e320f75d..00000000 --- a/src/pages/protocol/zones/accounts.mdx +++ /dev/null @@ -1,45 +0,0 @@ ---- -title: Accounts -description: Account-scoped access control on Tempo Zones, including private balances, private allowances, and the two-layer privacy model. ---- - -# Accounts - -:::info -Tempo Zones is still in early development and is available for testing purposes on Tempo Testnet only. While Tempo Zones are in this stage, expect breaking changes to the design and implementation. Do not use this in production. If you're interested in working with Tempo Labs as a design partner on the development of Tempo Zones, contact us at [tempo.xyz/contact](https://tempo.xyz/contact). -::: - -Tempo Zones enforce account privacy at two complementary layers: the EVM execution level and the [RPC access control](/protocol/zones/rpc) level. Neither is sufficient alone. - -- **Execution alone is insufficient.** Without RPC restrictions, a caller could use `eth_getStorageAt` to read TIP-20 balance mapping slots directly, bypassing `balanceOf` access control. -- **RPC alone is insufficient.** Without execution-level changes, a caller could use `eth_call` to invoke a contract that reads another account's balance and returns it, bypassing RPC-level filtering. - -This page covers the execution-level protections. For the RPC layer, see the [RPC specification](/protocol/zones/rpc). - -## Private Balances - -On Tempo Mainnet, anyone can read any account's balance. On a Tempo Zone, `balanceOf(address)` enforces caller restrictions for TIP-20s: - -- If `msg.sender == account`, the call succeeds and returns the balance. -- If `msg.sender` is the sequencer, the call succeeds (required for block production and fee accounting). -- Otherwise, the call reverts with `Unauthorized()`. - -Enforcing this at the contract level (not just the RPC layer) ensures that even onchain composition cannot leak balances. A contract on the Tempo Zone cannot read and emit another account's balance. - -## Private Allowances - -The `allowance(owner, spender)` function is similarly restricted: - -- If `msg.sender == owner` or `msg.sender == spender`, the call succeeds. -- If `msg.sender` is the sequencer, the call succeeds. -- Otherwise, the call reverts with `Unauthorized()`. - -A non-zero allowance reveals that `owner` has interacted with `spender`, a relationship that should be private. Restricting reads to the two parties involved preserves standard TIP-20 (ERC-20) approval flows without leaking relationship information. - -Public views like `totalSupply()`, `name()`, `symbol()`, and `decimals()` remain unrestricted. - -## Related Specifications - -Tempo Zones also charge fixed gas costs for TIP-20 operations to prevent gas-based side channels. See [Execution & Gas](/protocol/zones/execution#fixed-gas-costs) for details. - -Tempo Zones currently disable contract creation (`CREATE` and `CREATE2`). See [Execution & Gas](/protocol/zones/execution#contract-creation-disabled) for details. diff --git a/src/pages/protocol/zones/architecture.mdx b/src/pages/protocol/zones/architecture.mdx deleted file mode 100644 index 33efeea0..00000000 --- a/src/pages/protocol/zones/architecture.mdx +++ /dev/null @@ -1,124 +0,0 @@ ---- -title: Tempo Zone Architecture -description: Architecture of Tempo Zones, including contract layout, sequencer management, chain IDs, and the trust model. ---- - -import { StaticMermaidDiagram } from '../../../components/StaticMermaidDiagram' - -# Tempo Zone Architecture - -:::info -Tempo Zones is still in early development and is available for testing purposes on Tempo Testnet only. While Tempo Zones are in this stage, expect breaking changes to the design and implementation. Do not use this in production. If you're interested in working with Tempo Labs as a design partner on the development of Tempo Zones, contact us at [tempo.xyz/contact](https://tempo.xyz/contact). -::: - -A Tempo Zone is a dedicated blockchain rooted to Tempo Mainnet where one sequencer controls block production and visibility. No zone data is published on Tempo Mainnet. Instead, the sequencer publishes commitments to the current zone state along with proofs of correct execution. These proofs allow funds to move in and out of the Tempo Zone. This scaling approach is known as a validium. - -The sequencer can enable any TIP-20 token on the Tempo Zone. Any enabled TIP-20 with USD currency can pay for zone gas. TIP-20 tokens bridge into the zone nearly instantly and bridge out as soon as a validity proof is posted (targeting under 10 seconds). - -Tempo Zones are tightly integrated with Tempo Mainnet. Withdrawals to Tempo Mainnet are processed by the sequencer and can trigger transfers, trades on Tempo's Stablecoin DEX, or deposits into other Tempo Zones, without further interaction from the user. - -Tempo Zones are designed for applications that want safe operation guaranteed by validity proofs and privacy from the rest of the world, where users are comfortable trusting a sequencer for liveness and privacy. - -## System Architecture - -Each Tempo Zone runs as a separate Tempo chain with its own Tempo node(s). Tempo Zones are tightly coupled with Tempo Mainnet and have direct, synchronous access to Tempo Mainnet state. Zone contracts can read certain Tempo Mainnet state without any message passing delay, such as deposit queues and TIP-403 policy information. - - Z1["Zone 1
USDX, USDY"] - TE --> Z2["Zone 2
pathUSD, ..."] - end -`} /> - -The sequencer runs a Tempo node with one or more zone nodes attached. Each zone node: - -- Synchronizes the zone's view of Tempo Mainnet each time a Tempo Mainnet block finalizes -- Executes zone transactions using privately submitted transactions and the zone's own state -- Produces batches proving state transitions on the zone and posts them to Tempo Mainnet -- Watches for deposits by monitoring Zone Portal events on Tempo Mainnet, and creates corresponding transactions on the zone once the block finalizes -- Watches for withdrawals on the zone and submits transactions to Tempo Mainnet processing them once the batch has been proven - - -## Contract Architecture - -The system consists of contracts on both Tempo Mainnet and within each Tempo Zone. - - ZI["ZoneInbox
(deposits)"] - ZO["ZoneOutbox
(withdrawals)"] -- "withdrawals" --> ZP - - subgraph TEMPO["Tempo Mainnet"] - ZF["ZoneFactory
(deploys)"] - ZP - ZM["ZoneMessenger
(callbacks)"] - end - subgraph ZONE["Zone"] - TS["TempoState
(Tempo Mainnet view)"] - ZI - ZO - end -`} /> - -### Tempo Contracts - -- **`ZoneFactory`** deploys new Tempo Zones. Each zone gets its own Zone Portal and Zone Messenger contracts with deterministic addresses. -- **`ZonePortal`** is the central bridge contract. It locks all deposited tokens, verifies validity proofs, and processes withdrawals. The Zone Portal contract maintains the authoritative state: which deposits have been made, which batches have been proven, and which withdrawals are pending. -- **`ZoneMessenger`** handles withdrawals that include callbacks. When a user wants to withdraw tokens and trigger a contract call atomically, the messenger executes both operations together. If the callback fails, the entire withdrawal reverts and funds bounce back to the zone. - -### Zone Predeploys - -Tempo Zones have four system contract predeploys at fixed addresses: - -| Contract | Address | Purpose | -|----------|---------|---------| -| `TempoState` | `0x1c00...0000` | Stores the zone's view of Tempo Mainnet. The sequencer updates this with Tempo Mainnet block headers, allowing zone contracts to read Tempo Mainnet state within proofs. | -| `ZoneInbox` | `0x1c00...0001` | Processes incoming deposits. Mints tokens to recipients and validates that processed deposits match what the Zone Portal contract expects. | -| `ZoneOutbox` | `0x1c00...0002` | Handles withdrawal requests. Users burn their zone tokens here and specify a Tempo Mainnet recipient. | -| `ZoneConfig` | `0x1c00...0003` | Central configuration. Reads sequencer and token registry from Tempo Mainnet. | - -## Creating a Zone - -Tempo Zones are created through the `ZoneFactory`: - -1. Choose a TIP-20 token to serve as the zone's initial asset. -2. Select a verifier contract (ZK prover or TEE attestor). -3. Designate a sequencer address. -4. Call `ZoneFactory.createZone()` with these parameters. - -The factory deploys a new `ZonePortal` and `ZoneMessenger` for the Tempo Zone. The zone itself runs as a separate chain with the system contracts deployed at genesis. The sequencer can enable additional TIP-20 tokens at any time via `ZonePortal.enableToken()`. - -### Chain ID - -Each Tempo Zone has a unique EIP-155 chain ID derived deterministically from its onchain zone ID: - -``` -chain_id = 421700000 + zone_id -``` - -The prefix `4217` corresponds to the Tempo Mainnet chain ID. This ensures replay protection between Tempo Zones. A transaction signed for one zone cannot be replayed on another. - -## Sequencer Transfer - -The sequencer can transfer control to a new address via a two-step process on Tempo Mainnet: - -1. Current sequencer calls `ZonePortal.transferSequencer(newSequencer)` to nominate a new sequencer. -2. New sequencer calls `ZonePortal.acceptSequencer()` to accept the transfer. - -Sequencer management occurs on Tempo Mainnet. Zone-side system contracts read the sequencer from Tempo Mainnet via `ZoneConfig`, which queries `TempoState` to get the sequencer address from the finalized `ZonePortal` storage. - -## Trust Model - -Tempo Zones make explicit tradeoffs between trust and performance: - -| What You Trust | What Could Go Wrong | -|---|---| -| Sequencer for liveness | The Tempo Zone halts if the sequencer stops. | -| Sequencer for inclusion and ordering | Transactions (including withdrawals) can be excluded or reordered. | -| Sequencer for privacy | The sequencer can see all transactions on the Tempo Zone. | -| Sequencer for data | Reconstructing the state of the Tempo Zone without the sequencer is impossible. | -| Sequencer + verifier for correctness | If a critical safety bug exists in the verifier or proving system, and the sequencer is malicious, they could exploit it to steal funds. | - -The sequencer cannot steal funds or forge state transitions. Validity proofs prevent this. However, the sequencer can halt the zone entirely, censor specific users, or reorder transactions for MEV. - -Failed withdrawals always bounce back to the zone `fallbackRecipient`, ensuring users retain their funds. TIP-403 policy changes or token pauses on Tempo Mainnet will cause affected withdrawals to bounce back rather than block the queue. diff --git a/src/pages/protocol/zones/bridging.mdx b/src/pages/protocol/zones/bridging.mdx deleted file mode 100644 index 34438824..00000000 --- a/src/pages/protocol/zones/bridging.mdx +++ /dev/null @@ -1,97 +0,0 @@ ---- -title: Zone Bridging -description: Deposit and withdraw TIP-20 tokens between Tempo Mainnet and Tempo Zones, including encrypted deposits and composable withdrawal callbacks. ---- - -# Zone Bridging - -:::info -Tempo Zones is still in early development and is available for testing purposes on Tempo Testnet only. While Tempo Zones are in this stage, expect breaking changes to the design and implementation. Do not use this in production. If you're interested in working with Tempo Labs as a design partner on the development of Tempo Zones, contact us at [tempo.xyz/contact](https://tempo.xyz/contact). -::: - -Tempo Zones use Tempo-centric bridging for cross-chain operations: deposits flow from Tempo into a zone, and withdrawals flow from a zone back to Tempo with optional callbacks for composability. - -![End-to-end privacy flow through a zone](/learn/zones/diagram-privacy.svg) - -Above is an example of the type of complex transaction that can remain privacy-preserving via Tempo Zones, while performing operations such as bridging, deposits & sends, and withdrawals. Learn more about [encrypted deposits](#encrypted-deposits) and [verifiable withdrawals](#verifiable-withdrawals) below. - -## Deposits (Tempo → Zone) - -1. User calls `ZonePortal.deposit(token, to, amount, memo)` on Tempo, specifying which enabled TIP-20 to deposit. -2. The Zone Portal contract validates the token is enabled and deposits are active, deducts the [deposit fee](/protocol/zones/execution#deposit-fees), locks the funds, and appends a deposit to the queue. -3. The sequencer observes `DepositMade` events and processes deposits in order via `ZoneInbox.advanceTempo()`, minting the corresponding zone-side TIP-20 to the recipient. -4. A batch proof must prove the zone correctly processed deposits by validating the Tempo state read inside the proof. - - -### Encrypted Deposits - -For privacy-sensitive use cases, users can make encrypted deposits where the recipient and memo are encrypted using the sequencer's public key. Only the sequencer can decrypt and credit the correct recipient on the zone. - -**What's public vs. private:** - -| Field | Visibility | Reason | -|-------|------------|--------| -| `token` | Public | Needed for locked token accounting | -| `sender` | Public | Needed for potential refunds if decryption fails | -| `amount` | Public | Needed for onchain locked token accounting | -| `to` | Encrypted | Only sequencer knows recipient | -| `memo` | Encrypted | Only sequencer knows payment context | - -The encryption uses ECIES with secp256k1: - -1. Sequencer publishes a secp256k1 encryption public key via `setSequencerEncryptionKey()` with a proof of possession. -2. User generates an ephemeral keypair and derives a shared secret via ECDH. -3. User encrypts `(to || memo)` with AES-256-GCM using the derived key. -4. User calls `depositEncrypted(token, amount, keyIndex, encryptedPayload)` on the Zone Portal contract. - -If decryption fails (invalid ciphertext, wrong key), the zone mints tokens to the `sender`'s address on the zone. The Tempo Mainnet funds remain locked in the Zone Portal contract. This ensures chain progress is never blocked by invalid encrypted deposits. - - -## Withdrawals (Zone → Tempo) - -Users withdraw by creating a withdrawal request on the zone. Withdrawals are processed in two steps: - -1. **Batch submission.** The sequencer calls `finalizeWithdrawalBatch()` at the end of the final block in a batch. This constructs the withdrawal hash chain and writes the `withdrawalQueueHash` and `withdrawalBatchIndex` to state. The proof validates this state and adds withdrawals to Tempo's queue. -2. **Withdrawal processing.** The sequencer calls `processWithdrawal()` on Tempo to process withdrawals from the queue's oldest slot. - -### Composable Withdrawals - -Withdrawals support callbacks to Tempo contracts via the `ZoneMessenger`. When `gasLimit > 0`, the messenger: - -1. Transfers tokens from the Zone Portal contract to the target via `transferFrom`. -2. Calls the target with the provided `callbackData`. - -Both operations are atomic. If the callback reverts, the transfer reverts too. Receiving contracts implement `IWithdrawalReceiver` and verify `msg.sender == zoneMessenger` to authenticate calls. This enables direct composition with DEX swaps, staking, or cross-zone deposits. - -```solidity -interface IWithdrawalReceiver { - function onWithdrawalReceived( - bytes32 senderTag, - address token, - uint128 amount, - bytes calldata callbackData - ) external returns (bytes4); -} -``` - -### Withdrawal Failure and Bounce-Back - -Withdrawals can fail if the token transfer or callback reverts (out of gas, TIP-403 policy, token pause, etc.). When a withdrawal fails, the Zone Portal contract bounces back the funds by re-depositing into the same zone to the withdrawal's `fallbackRecipient`: - -- The withdrawal is **popped unconditionally** from the queue, even on failure. -- A new deposit is enqueued for the `fallbackRecipient` on the zone. -- The sequencer keeps the processing fee regardless of success or failure. - -This ensures failed withdrawals never block the queue and users always retain their funds. - -### Verifiable Withdrawals - -Zone transactions are private: transaction data is not published on Tempo Mainnet. To protect sender privacy during withdrawal processing on Tempo Mainnet, the plaintext `sender` is replaced with a commitment: - -``` -senderTag = keccak256(abi.encodePacked(sender, txHash)) -``` - -The `txHash` acts as a blinding factor known only to the sender and sequencer. The sender can selectively disclose their identity by revealing `txHash` to any party, who verifies it against the `senderTag`. - -For automated disclosure, the sender can specify a `revealTo` public key. The sequencer encrypts `(sender, txHash)` to that key using ECDH, populating the `encryptedSender` field in the Tempo Mainnet-facing withdrawal struct. This enables cross-zone transfers where the destination zone's sequencer can automatically attribute incoming deposits. diff --git a/src/pages/protocol/zones/execution.mdx b/src/pages/protocol/zones/execution.mdx deleted file mode 100644 index 0dc64559..00000000 --- a/src/pages/protocol/zones/execution.mdx +++ /dev/null @@ -1,68 +0,0 @@ ---- -title: Execution & Gas -description: Specification for gas accounting, fee tokens, fixed TIP-20 gas costs, contract creation limits, and token management on Tempo Zones. ---- - -# Execution & Gas - -:::info -Tempo Zones is still in early development and is available for testing purposes on Tempo Testnet only. While Tempo Zones are in this stage, expect breaking changes to the design and implementation. Do not use this in production. If you're interested in working with Tempo Labs as a design partner on the development of Tempo Zones, contact us at [tempo.xyz/contact](https://tempo.xyz/contact). -::: - -This page specifies how Tempo Zones handle gas accounting, fee collection, and token management. For deposit and withdrawal flows, see the [bridging specification](/protocol/zones/bridging). For balance visibility and access control rules, see the [accounts specification](/protocol/zones/accounts). - -## Fee Tokens - -Tempo Zones reuse Tempo fee units and gas accounting. Each transaction includes a `feeToken` field. Any enabled TIP-20 token with USD currency is valid for gas payment. The sequencer accepts all enabled tokens directly, so no Fee AMM is needed. - -## Deposit Fees - -Deposits charge a fixed processing fee in the deposited token: - -``` -fee = FIXED_DEPOSIT_GAS × zoneGasRate -``` - -`FIXED_DEPOSIT_GAS` is fixed at 100,000 gas. The sequencer configures `zoneGasRate` through `ZonePortal.setZoneGasRate()`. The fee is deducted from the deposit amount and paid to the sequencer on Tempo Mainnet. - -## Withdrawal Fees - -Withdrawals charge a processing fee in the withdrawn token: - -``` -fee = gasLimit × tempoGasRate -``` - -The user specifies `gasLimit` to cover processing and any callback execution. The sequencer configures `tempoGasRate` through `ZoneOutbox.setTempoGasRate()`. - -## Fixed Gas Costs - -All user-facing TIP-20 transfer and approval operations cost exactly 100,000 gas. This removes gas-based information leaks tied to storage state. On a standard EVM chain, gas varies based on whether a transfer writes to a previously empty storage slot, revealing whether the recipient has received tokens before. Fixed costs eliminate that side channel. - -| Function | Gas Cost | -|----------|----------| -| `transfer(to, amount)` | 100,000 | -| `transferFrom(from, to, amount)` | 100,000 | -| `transferWithMemo(to, amount, memo)` | 100,000 | -| `transferFromWithMemo(from, to, amount, memo)` | 100,000 | -| `approve(spender, amount)` | 100,000 | - -System functions (`systemTransferFrom`, `transferFeePreTx`, `transferFeePostTx`) retain standard gas costs. Only restricted system callers can invoke them, so the gas side channel does not apply. - -## Contract Creation Disabled - -Tempo Zones currently disable the `CREATE` and `CREATE2` opcodes. Each Tempo Zone runs a fixed set of system contracts and predeploys. Any transaction that attempts contract creation reverts. - -## Token Management - -![Policy inheritance from mainnet to zone](/learn/zones/diagram-tip20.svg) - -The sequencer manages which TIP-20 tokens are available on a Tempo Zone: - -| Function | Behavior | -|----------|----------| -| `enableToken(token)` | Enables a TIP-20 token for bridging and gas payment. Irreversible. | -| `pauseDeposits(token)` | Stops new deposits for the token. Withdrawals continue. | -| `resumeDeposits(token)` | Restarts deposits for a previously paused token. | - -Once enabled, a token cannot be disabled. This preserves withdrawals for that token, subject to the token's own compliance policy. Tokens on the Tempo Zone use the same address as their Tempo Mainnet counterpart. `ZoneInbox` mints on deposit, `ZoneOutbox` burns on withdrawal. No mechanism exists to create new tokens on the Tempo Zone. diff --git a/src/pages/protocol/zones/index.mdx b/src/pages/protocol/zones/index.mdx deleted file mode 100644 index 386474c0..00000000 --- a/src/pages/protocol/zones/index.mdx +++ /dev/null @@ -1,89 +0,0 @@ ---- -title: Tempo Zones -description: Tempo Zones are private execution environments on Tempo Mainnet where balances, transfers, and transaction history are invisible to the public chain. ---- - -import { Cards, Card } from 'vocs' - -# Tempo Zones - -:::info -Tempo Zones are still in early development and available for testing purposes on Tempo Testnet only. While Tempo Zones are in this stage, expect breaking changes to the design and implementation. Do not use this in production. If you're interested in working with Tempo Labs as a design partner on the development of Tempo Zones, contact us at [tempo.xyz/contact](https://tempo.xyz/contact). -::: - -A Tempo Zone is a private execution environment attached to Tempo Mainnet. Inside a Tempo Zone, balances, transfers, and transaction history are invisible to block explorers, indexers, and other users on Tempo Mainnet. Each Tempo Zone runs its own sequencer and executes transactions independently. - -![Tempo Zones overview](/learn/zones/diagram-overview.svg) - -Funds deposited into a Tempo Zone are locked in the Zone Portal contract on Tempo Mainnet. [Validity proofs](/protocol/zones/proving) guarantee that the sequencer executed every transaction correctly. The sequencer orders and includes transactions, but cannot steal funds or forge state transitions. - -Each Tempo Zone operates as a separate chain, so adding more zones increases throughput without congesting Tempo Mainnet. Tempo Zones share liquidity through Tempo Mainnet. A zone can withdraw tokens, swap them on the Stablecoin DEX, and deposit the result into another zone without exposing who placed the trade. See [composable withdrawals](/protocol/zones/bridging#composable-withdrawals) for details. - -### Tempo Zones are private - -Tempo Zones make a key trade-off: Each zone has a sequencer who sees all activity on the zone. Privacy depends on the integrity of whoever is running the sequencer. Thanks to this trade-off, they achieve what few other privacy solutions do: Great privacy with good UX. - -Most privacy solutions offer either confidentiality (hide the amount) or anonymity (hide the sender). Tempo Zones provide both, and go further. Inside a Tempo Zone, balances, transaction history, and counterparty relationships are all invisible to outside observers. Block explorers and indexers see nothing. Other users cannot query your address. - -The [accounts specification](/protocol/zones/accounts) describes how balance and allowance reads are restricted at the contract level, and the [RPC specification](/protocol/zones/rpc) covers how the JSON-RPC interface is scoped per account. - -![End-to-end privacy flow through a zone](/learn/zones/diagram-privacy.svg) - -### Tempo Zones are compliant by design - -Every TIP-20 token carries its issuer's compliance policy (whitelists, blacklists, freeze controls) via the [TIP-403 registry](/protocol/tip403/overview). When deposited into a Tempo Zone, the policy is provably mirrored. The validity proof commits that every transaction in the batch followed the issuer's rules. - -![Policy inheritance from mainnet to zone](/learn/zones/diagram-tip20.svg) - -### Tempo Zones are safe from theft - -Validity proofs guarantee correct state transitions. Sequencers order transactions but cannot steal deposited funds. See the [proving specification](/protocol/zones/proving) for how proofs are constructed and verified. - -### Tempo Zones are interoperable - -Tempo Zones are interoperable with Tempo Mainnet and with each other. Deposits and withdrawals settle in seconds. A Tempo Zone can withdraw tokens, swap them on the Stablecoin DEX, and deposit the result into another Tempo Zone in a single operation. The [bridging specification](/protocol/zones/bridging) covers deposits, withdrawals, encrypted deposits for private on-ramps, and composable withdrawal callbacks for cross-zone transfers. - -### Github and Specifications - -The zones repository is available on [github](https://github.com/tempoxyz/zones), which also includes the full Tempo Zones [specifications](https://github.com/tempoxyz/zones/blob/main/docs/specs/zone_spec.md). - -## Reference - - - - - - - - - diff --git a/src/pages/protocol/zones/proving.mdx b/src/pages/protocol/zones/proving.mdx deleted file mode 100644 index c7f21908..00000000 --- a/src/pages/protocol/zones/proving.mdx +++ /dev/null @@ -1,136 +0,0 @@ ---- -title: Zone Proving -description: Batch submission and proof verification for Tempo zones, including the state transition function, ZK and TEE deployment modes, and ancestry proofs. ---- - -import { StaticMermaidDiagram } from '../../../components/StaticMermaidDiagram' - -# Zone Proving - -:::info -Tempo Zones is still in early development and is available for testing purposes on Tempo Testnet only. While Tempo Zones are in this stage, expect breaking changes to the design and implementation. Do not use this in production. If you're interested in working with Tempo Labs as a design partner on the development of Tempo Zones, contact us at [tempo.xyz/contact](https://tempo.xyz/contact). -::: - -:::warning -The zone prover is not yet live. This page describes the planned design. The prover will be added in a future release. -::: - -Zone settlement uses validity proofs to verify correct execution. The prover implements a pure state transition function in Rust with `no_std` compatibility, allowing it to run in both ZKVMs (SP1) and TEEs (SGX/TDX). - -## Batch Submission - -The sequencer posts batches to Tempo Mainnet via `submitBatch` on the portal. Each batch covers one or more zone blocks and includes: - -| Field | Description | -|-------|-------------| -| `tempoBlockNumber` | Tempo block the zone committed to (from zone's TempoState) | -| `recentTempoBlockNumber` | Optional recent block for ancestry proof (`0` = direct lookup) | -| `blockTransition` | Zone block hash transition (`prevBlockHash` → `nextBlockHash`) | -| `depositQueueTransition` | Deposit queue processing progress | -| `withdrawalQueueHash` | Hash chain of withdrawals for this batch (`0` if none) | -| `verifierConfig` | Opaque payload for the verifier (domain separation / attestation) | -| `proof` | Validity proof or TEE attestation | - -The portal verifies that `prevBlockHash` matches the stored `blockHash`, calls the verifier, and on success updates `withdrawalBatchIndex`, `blockHash`, `lastSyncedTempoBlockNumber`, and adds withdrawals to the queue. - -## Verifier Interface - -The verifier is abstracted behind a minimal interface. ZK systems and TEE attesters implement the same contract: - -```solidity -interface IVerifier { - function verify( - uint64 tempoBlockNumber, - uint64 anchorBlockNumber, - bytes32 anchorBlockHash, - uint64 expectedWithdrawalBatchIndex, - address sequencer, - BlockTransition calldata blockTransition, - DepositQueueTransition calldata depositQueueTransition, - bytes32 withdrawalQueueHash, - bytes calldata verifierConfig, - bytes calldata proof - ) external view returns (bool); -} -``` - -The proof verifies that: - -1. Valid state transition from `prevBlockHash` to `nextBlockHash`. -2. Zone committed to `tempoBlockNumber` via TempoState. -3. Anchor block hash matches (direct or ancestry mode). -4. `ZoneOutbox.lastBatch()` has the correct `withdrawalBatchIndex` and `withdrawalQueueHash`. -5. Deposit processing is correct (validated via Tempo state read inside proof). -6. Zone block `beneficiary` matches the registered sequencer. - -## State Transition Function - -The prover takes a complete witness of zone blocks and their dependencies, executes the EVM state transitions, and outputs commitments for on-chain verification: - -```rust -pub fn prove_zone_batch(witness: BatchWitness) -> Result -``` - -### Execution Flow - - B["Verify Tempo state proofs"] - B --> C["Initialize zone state from previous block hash"] - C --> D{"Next zone block"} - D --> E["Check parent hash and block number"] - E --> F["Verify beneficiary is the sequencer"] - F --> G["Execute advanceTempo system transaction if present"] - G --> H["Execute user transactions via revm"] - H --> I{"Final block in batch?"} - I -- No --> J["Compute simplified zone block hash"] - J --> D - I -- Yes --> K["Execute finalizeWithdrawalBatch"] - K --> L["Compute simplified zone block hash"] - L --> M["Extract output commitments"] - M --> N["Return batch output for verification"] -`} /> - -1. **Verify Tempo state proofs.** Validate MPT proofs for all Tempo storage reads against Tempo state roots. -2. **Initialize zone state.** Load the zone state from the witness, binding the initial state root to the previous block hash. -3. **Execute zone blocks.** For each block: - - Validate parent hash continuity and block number sequencing. - - Verify beneficiary matches the registered sequencer. - - Execute `advanceTempo()` system transaction (if present) to process deposits. - - Execute user transactions via revm. - - Execute `finalizeWithdrawalBatch()` in the final block only. - - Compute the zone block hash from the simplified header. -4. **Extract output commitments.** Block hash transition, deposit queue transition, withdrawal queue hash, and last batch parameters. - -### Deployment Modes - -**ZKVM (SP1):** The prover runs inside a ZKVM. The witness is read from the ZKVM IO, and the output is committed to the proof. - -**TEE (SGX/TDX):** The same function runs inside a trusted execution environment. The output is signed by the TEE attestation. - -## Ancestry Proofs - -EIP-2935 provides access to the last ~8,192 block hashes on Tempo. If a zone is inactive longer than this window, `tempoBlockNumber` rotates out of EIP-2935, which would prevent batch submission. - -The solution verifies ancestry inside the ZK circuit: - -1. The portal reads `recentTempoBlockNumber` hash from EIP-2935 (must be recent). -2. The prover includes Tempo headers from `tempoBlockNumber + 1` to `recentTempoBlockNumber` as witness data. -3. The proof verifies the parent hash chain: each header's parent hash must match the previous header's hash. -4. The portal verifies the constant-size proof against the recent block hash. - -| Mode | Condition | Behavior | -|------|-----------|----------| -| Direct | `recentTempoBlockNumber = 0` | Portal reads `tempoBlockNumber` hash from EIP-2935 | -| Ancestry | `recentTempoBlockNumber > tempoBlockNumber` | Portal reads `recentTempoBlockNumber` hash; proof verifies parent chain | - -Proving time increases linearly with the block gap (each gap block adds ~1 keccak operation), but on-chain verification cost remains constant. This prevents the zone from becoming stuck after an extended downtime. - -## Tempo State Access - -The zone accesses Tempo state via the TempoState predeploy (`0x1c00...0000`). During batch execution: - -1. `ZoneInbox` calls `TempoState.finalizeTempo(header)` to advance the zone's view of Tempo. -2. System contracts read Tempo storage via `TempoState.readTempoStorageSlot()`, restricted to zone system contracts only. -3. The proof includes Merkle proofs for each Tempo account and storage slot accessed during the batch. - -Tempo state staleness depends on how frequently the sequencer calls `advanceTempo()`. The zone client must only finalize Tempo headers after finality to avoid reorg risk. diff --git a/src/pages/protocol/zones/rpc.mdx b/src/pages/protocol/zones/rpc.mdx deleted file mode 100644 index ab80c60d..00000000 --- a/src/pages/protocol/zones/rpc.mdx +++ /dev/null @@ -1,145 +0,0 @@ ---- -title: Zone RPC -description: Authenticated JSON-RPC interface for Tempo Zones with per-account scoping, timing side channel mitigations, and event filtering. ---- - -# Zone RPC - -:::info -Tempo Zones is still in early development and is available for testing purposes on Tempo Testnet only. While Tempo Zones are in this stage, expect breaking changes to the design and implementation. Do not use this in production. If you're interested in working with Tempo Labs as a design partner on the development of Tempo Zones, contact us at [tempo.xyz/contact](https://tempo.xyz/contact). -::: - -The zone RPC starts from the standard Ethereum JSON-RPC and restricts it to enforce privacy guarantees. Every RPC request must include an authorization token that proves the caller controls a Tempo account and scopes all responses to that account. - -## Authorization Tokens - -Authorization tokens are short-lived credentials (maximum 1 month) signed by the caller's Tempo account key. Tempo accounts support multiple signature types (secp256k1, P256, WebAuthn), and accounts with Access Keys via the `AccountKeychain` precompile can use those keys to authenticate. - -The signed message includes: -- `"TempoZoneRPC"` magic prefix for domain separation -- Spec version, zone ID, and chain ID for replay protection (zone 0 can be used to allow access to all zones) -- Issuance and expiry timestamps - -Tokens are sent via the `X-Authorization-Token` HTTP header on every request. - -## Method Access Control - -Each JSON-RPC method falls into one of four categories: - -Available to any authenticated caller: - -| Method | Access Type | Notes | -|--------|-------------|-------| -| `eth_chainId` | Allowed | Zone chain ID | -| `eth_blockNumber` | Allowed | Latest block number | -| `eth_gasPrice` | Allowed | Current gas price | -| `eth_maxPriorityFeePerGas` | Allowed | Current priority fee | -| `eth_feeHistory` | Allowed | Fee history | -| `eth_getBlockByNumber` | Allowed | Block headers **without transaction details** | -| `eth_getBlockByHash` | Allowed | Block headers **without transaction details** | -| `eth_subscribe("newHeads")` | Allowed | Block headers with `logsBloom` zeroed | -| `eth_syncing` | Allowed | Sync status | -| `eth_coinbase` | Allowed | Sequencer address | -| `net_version` | Allowed | Network ID | -| `net_listening` | Allowed | Node status | -| `web3_clientVersion` | Allowed | Client version | -| `web3_sha3` | Allowed | Pure Keccak-256 hash | -| `eth_getBalance` | Scoped | Returns balance for the authenticated account only. Queries for other accounts return `0x0`. | -| `eth_getTransactionCount` | Scoped | Returns nonce for the authenticated account only. Other accounts return `0x0`. | -| `eth_call` | Scoped | Executes with `from` set to the authenticated account. [Execution-level privacy](/protocol/zones/accounts) enforces `balanceOf` access control at the contract level. | -| `eth_estimateGas` | Scoped | Only allowed when `from` equals the authenticated account. | -| `eth_getTransactionByHash` | Scoped | Returns the transaction only if the authenticated account is the sender. Returns `null` otherwise. | -| `eth_getTransactionReceipt` | Scoped | Returns the receipt only if the authenticated account is the sender. Logs are filtered (see [Event Filtering](#event-filtering)). | -| `eth_sendRawTransaction` | Scoped | Validates that the transaction sender matches the authenticated account. | -| `eth_getLogs` | Scoped | Filtered to TIP-20 events where the authenticated account is a relevant party (see [Event Filtering](#event-filtering)). | -| `eth_getFilterLogs` | Scoped | Same filtering as `eth_getLogs`. | -| `eth_getFilterChanges` | Scoped | Same filtering. Only returns new events since last poll. | -| `eth_newFilter` | Scoped | Creates a filter implicitly scoped to the authenticated account. | -| `eth_subscribe("logs")` | Scoped | Subscription scoped to the authenticated account. | -| `eth_newBlockFilter` | Scoped | Returns new block hashes. | -| `eth_uninstallFilter` | Scoped | Removes a previously created filter. | - -**Error vs. silent response**: Methods where the user explicitly provides a mismatched parameter (`eth_sendRawTransaction` with wrong sender, `eth_call` with wrong `from`) return explicit errors, since the user already knows the address they supplied and the error leaks nothing. Methods that query *about* other accounts return silent dummy values (`0x0`, `null`, empty results) instead of errors; an error would reveal "this data exists but you can't see it." - -### Restricted (sequencer-only) - -| Method | Reason | -|--------|--------| -| `eth_getStorageAt` | Raw storage reads bypass all access control | -| `eth_getCode` | No legitimate non-sequencer use case | -| `eth_createAccessList` | Reveals storage layout | -| `eth_getBlockByNumber` (with `true`) | Full block with all transactions | -| `eth_getBlockByHash` (with `true`) | Full block with all transactions | -| `eth_getBlockTransactionCountByNumber` | Transaction counts reveal activity levels | -| `eth_getBlockTransactionCountByHash` | Same as above | -| `eth_getTransactionByBlockNumberAndIndex` | Arbitrary transaction access | -| `eth_getTransactionByBlockHashAndIndex` | Same as above | -| `debug_*`, `admin_*`, `txpool_*` | All debug, admin, and txpool namespaces | - -### Disabled - -| Method | Reason | -|--------|--------| -| `eth_getProof` | Merkle proofs leak state trie structure | -| `eth_newPendingTransactionFilter` | Mempool observation | -| `eth_subscribe("newPendingTransactions")` | Mempool observation | -| Mining-related methods | Tempo Zones have no mining | - -Any method not explicitly listed returns error code `-32601` (method not found), ensuring new methods are not accidentally exposed. - -## Timing Side Channels - -Scoped methods that fetch data before checking authorization have a mandatory **100 ms minimum response time**. This ensures that `eth_getTransactionByHash` for a non-existent transaction hash and for another user's transaction have indistinguishable response times, preventing existence probing. - -Methods that need the speed bump: - -| Method | Reason | -|--------|--------| -| `eth_getTransactionByHash` | Must fetch the transaction to check if sender matches | -| `eth_getTransactionReceipt` | Must fetch the receipt to check the sender | -| `eth_getLogs` | Response time correlates with total log volume, not just the caller's logs | -| `eth_getFilterLogs` | Same as `eth_getLogs` | -| `eth_getFilterChanges` | Same as `eth_getLogs` | - -Methods that do **not** need the speed bump include `eth_getBalance` and `eth_getTransactionCount` (address checked before any data fetch), `eth_call` and `eth_estimateGas` (`from` validated before execution), and `eth_sendRawTransaction` (sender verified during decoding). - -## Block Responses - -Block headers returned to non-sequencer callers are sanitized: - -- `transactions` is always an empty array. -- `logsBloom` is replaced with a zero Bloom. The real Bloom filter would allow probing whether a specific address had activity in a block. -- All other header fields (`number`, `hash`, `gasUsed`, `stateRoot`, etc.) are returned normally. - -## Event Filtering - -Log queries are restricted to TIP-20 events where the authenticated account is a relevant party: - -| Event | Visible if | -|-------|-----------| -| `Transfer` | `from == caller` OR `to == caller` | -| `Approval` | `owner == caller` OR `spender == caller` | -| `TransferWithMemo` | `from == caller` OR `to == caller` | -| `Mint` | `to == caller` | -| `Burn` | `from == caller` | - -All other event topics (system events, role events, configuration events) are filtered out. - -## Zone-Specific RPC Methods - -| Method | Access | Description | -|--------|--------|-------------| -| `zone_getAuthorizationTokenInfo` | Any authenticated | Returns the authenticated account address and token expiry | -| `zone_getZoneInfo` | Any authenticated | Returns zone metadata: `zoneId`, `zoneTokens`, `sequencer`, `chainId` | -| `zone_getDepositStatus` | Scoped | Returns whether deposits from a given Tempo block have been processed, filtered to the caller's deposits | - -## Error Codes - -| Code | Message | Meaning | -|------|---------|---------| -| `-32001` | Authorization token required | No authorization token provided | -| `-32002` | Authorization token expired | The authorization token has expired | -| `-32003` | Transaction rejected | Transaction sender does not match authenticated account | -| `-32004` | Account mismatch | The `from` field does not match the authenticated account | -| `-32005` | Sequencer only | Method requires sequencer access | -| `-32006` | Method disabled | Method is not available on zones | diff --git a/src/snippets/public-testnet-sponsor-tip.mdx b/src/snippets/public-testnet-sponsor-tip.mdx index f93b28bf..b2eebf20 100644 --- a/src/snippets/public-testnet-sponsor-tip.mdx +++ b/src/snippets/public-testnet-sponsor-tip.mdx @@ -1,3 +1,3 @@ :::tip -Tempo provides a public testnet fee payer service at `https://sponsor.moderato.tempo.xyz` that you can use for development and testing. See [Hosted Fee Payer](/developer-tools/fee-payer) for endpoint details, or follow the instructions below to run your own. +Tempo provides a public testnet fee payer service at `https://sponsor.moderato.tempo.xyz` that you can use for development and testing. See [Hosted Fee Payer](/docs/developer-tools/fee-payer) for endpoint details, or follow the instructions below to run your own. ::: diff --git a/src/snippets/tempo-tx-properties.mdx b/src/snippets/tempo-tx-properties.mdx index ba81ed7e..0a0130fa 100644 --- a/src/snippets/tempo-tx-properties.mdx +++ b/src/snippets/tempo-tx-properties.mdx @@ -3,10 +3,10 @@ import PublicTestnetSponsorTip from './public-testnet-sponsor-tip.mdx' ### Configurable Fee Tokens -A fee token is a permissionless [TIP-20 token](/protocol/tip20/overview) that can be used to pay fees on Tempo. +A fee token is a permissionless [TIP-20 token](/docs/protocol/tip20/overview) that can be used to pay fees on Tempo. When a TIP-20 token is passed as the `fee_token` parameter in a transaction, -Tempo's [Fee AMM](/protocol/fees/spec-fee-amm) automatically facilitates conversion between the +Tempo's [Fee AMM](/docs/protocol/fees/spec-fee-amm) automatically facilitates conversion between the user's preferred fee token and the validator's preferred token.
@@ -235,7 +235,7 @@ user's preferred fee token and the validator's preferred token. :::info -See a full guide on [paying fees in any stablecoin](/guide/payments/pay-fees-in-any-stablecoin). +See a full guide on [paying fees in any stablecoin](/docs/guide/payments/pay-fees-in-any-stablecoin). ::: ### Fee Sponsorship @@ -546,13 +546,13 @@ over the transaction with a special "fee payer envelope" to commit to paying fee :::tip -It is also possible to use a remote [Fee Payer Relay](/guide/payments/sponsor-user-fees#fee-payer-relay) instead of a local account. +It is also possible to use a remote [Fee Payer Relay](/docs/guide/payments/sponsor-user-fees#fee-payer-relay) instead of a local account. ::: :::info -See a full guide on [sponsoring fees](/guide/payments/sponsor-user-fees). +See a full guide on [sponsoring fees](/docs/guide/payments/sponsor-user-fees). ::: ### Batch Calls @@ -1252,7 +1252,7 @@ transactions thereafter can be signed by the access key. :::info -Learn more about [Access Keys](/protocol/transactions/spec-tempo-transaction#access-keys). +Learn more about [Access Keys](/docs/protocol/transactions/spec-tempo-transaction#access-keys). ::: ### Concurrent Transactions diff --git a/src/snippets/unformatted/withFeePayer.ts b/src/snippets/unformatted/withFeePayer.ts deleted file mode 100644 index 108fca71..00000000 --- a/src/snippets/unformatted/withFeePayer.ts +++ /dev/null @@ -1,44 +0,0 @@ -// @ts-nocheck -// [!region client] -import { walletActions } from 'viem' -import { withRelay } from 'viem/tempo' - -const _client = createClient({ - account: privateKeyToAccount('0x...'), - chain: tempo, - transport: withRelay( - // [!code hl] - http(), // [!code hl] - http('http://localhost:3000'), // [!code hl] - { policy: 'sign-only' }, // [!code hl] - ), // [!code hl] -}).extend(walletActions) -// [!endregion client] - -// [!region usage] -// Sponsored transaction (automatic when relay has feePayer configured) -const _receipt1 = await client.sendTransactionSync({ - to: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb', -}) - -// Opt out of sponsorship // [!code hl] -const _receipt2 = await client.sendTransactionSync({ - // [!code hl] - feePayer: false, // [!code hl] - to: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb', // [!code hl] -}) // [!code hl] - -// [!endregion usage] - -// [!region server] -import { Handler } from 'accounts/server' -import { privateKeyToAccount } from 'viem/accounts' - -const handler = Handler.relay({ - // [!code hl] - feePayer: { account: privateKeyToAccount('0x...') }, // [!code hl] -}) // [!code hl] - -const server = createServer(handler.listener) -server.listen(3000) -// [!endregion server] diff --git a/src/wagmi.config.ts b/src/wagmi.config.ts index e50afe72..dfb523b1 100644 --- a/src/wagmi.config.ts +++ b/src/wagmi.config.ts @@ -1,4 +1,5 @@ import { QueryClient } from '@tanstack/react-query' +import { tempoWallet, webAuthn } from '@wagmi/core/tempo' import { Expiry } from 'accounts' import * as React from 'react' import { parseUnits } from 'viem' @@ -13,17 +14,17 @@ import { useConnectors, webSocket, } from 'wagmi' -import { tempoWallet, webAuthn } from 'wagmi/tempo' import { alphaUsd, betaUsd, pathUsd, thetaUsd } from './components/guides/tokens' -import { feeToken, moderatoZones } from './lib/private-zones.ts' import * as WebAuthnCeremony from './lib/webAuthnCeremony.ts' +const feeToken = '0x20c0000000000000000000000000000000000001' as const + const chain = import.meta.env.VITE_TEMPO_ENV === 'localnet' ? tempoLocalnet.extend({ feeToken }) : import.meta.env.VITE_TEMPO_ENV === 'devnet' ? tempoDevnet.extend({ feeToken }) - : tempoModerato.extend({ feeToken, zones: moderatoZones }) + : tempoModerato.extend({ feeToken }) const rpId = (() => { const hostname = globalThis.location?.hostname @@ -72,7 +73,7 @@ export function getConfig(options: getConfig.Options = {}) { ]), ], multiInjectedProviderDiscovery, - storage: createStorage({ + storage: createStorage>({ storage: typeof window !== 'undefined' ? localStorage : undefined, key: 'tempo-docs', }), diff --git a/vercel.json b/vercel.json index 79bae0c6..d080808c 100644 --- a/vercel.json +++ b/vercel.json @@ -8,7 +8,7 @@ "value": "faucet.tempo.xyz" } ], - "destination": "https://docs.tempo.xyz/quickstart/faucet", + "destination": "https://docs.tempo.xyz/docs/quickstart/faucet", "permanent": false }, { @@ -19,7 +19,7 @@ "value": "faucet.tempo.xyz" } ], - "destination": "https://docs.tempo.xyz/quickstart/faucet", + "destination": "https://docs.tempo.xyz/docs/quickstart/faucet", "permanent": false }, { @@ -30,7 +30,7 @@ "value": "bridge.tempo.xyz" } ], - "destination": "https://docs.tempo.xyz/guide/getting-funds", + "destination": "https://docs.tempo.xyz/docs/guide/getting-funds", "permanent": false }, { @@ -41,115 +41,171 @@ "value": "bridge.tempo.xyz" } ], - "destination": "https://docs.tempo.xyz/guide/getting-funds", + "destination": "https://docs.tempo.xyz/docs/guide/getting-funds", "permanent": false }, + { + "source": "/accounts", + "destination": "https://accounts.tempo.xyz", + "permanent": true + }, + { + "source": "/accounts/:path*", + "destination": "https://accounts.tempo.xyz/:path*", + "permanent": true + }, { "source": "/guide/bridge-usdc-stargate", - "destination": "/guide/bridge-layerzero", + "destination": "/docs/guide/bridge-layerzero", "permanent": true }, { "source": "/guide/bridge-usdc-relay", - "destination": "/guide/bridge-relay", + "destination": "/docs/guide/bridge-relay", "permanent": true }, { "source": "/network-upgrades", - "destination": "/guide/node/network-upgrades", + "destination": "/docs/guide/node/network-upgrades", "permanent": true }, { "source": "/AccountKeychain", - "destination": "/protocol/transactions/AccountKeychain", + "destination": "/docs/protocol/transactions/AccountKeychain", "permanent": true }, { "source": "/guide/node/validator-config-v2", - "destination": "/guide/node/network-upgrades", + "destination": "/docs/guide/node/network-upgrades", "permanent": true }, { - "source": "/accounts/server/compose", - "destination": "https://accounts.tempo.xyz/docs/server/handler.compose", + "source": "/tip-:id", + "destination": "https://github.com/tempoxyz/tempo/blob/main/tips/tip-:id.md", "permanent": true }, { - "source": "/accounts/server/relay", - "destination": "https://accounts.tempo.xyz/docs/server/handler.relay", + "source": "/protocol/tips/tip-:id", + "destination": "https://github.com/tempoxyz/tempo/blob/main/tips/tip-:id.md", "permanent": true }, { - "source": "/accounts/server/webAuthn", - "destination": "https://accounts.tempo.xyz/docs/server/handler.webAuthn", + "source": "/guide", + "destination": "/docs/guide", "permanent": true }, { - "source": "/accounts", - "destination": "https://accounts.tempo.xyz/docs", + "source": "/guide/:path*", + "destination": "/docs/guide/:path*", "permanent": true }, { - "source": "/accounts/:path*", - "destination": "https://accounts.tempo.xyz/docs/:path*", + "source": "/quickstart", + "destination": "/docs/quickstart", "permanent": true }, { - "source": "/tip-:id", - "destination": "https://github.com/tempoxyz/tempo/blob/main/tips/tip-:id.md", + "source": "/quickstart/:path*", + "destination": "/docs/quickstart/:path*", "permanent": true }, { - "source": "/protocol/tips/tip-:id", - "destination": "https://github.com/tempoxyz/tempo/blob/main/tips/tip-:id.md", + "source": "/protocol", + "destination": "/docs/protocol", + "permanent": true + }, + { + "source": "/protocol/:path*", + "destination": "/docs/protocol/:path*", + "permanent": true + }, + { + "source": "/sdk", + "destination": "/docs/sdk", + "permanent": true + }, + { + "source": "/sdk/:path*", + "destination": "/docs/sdk/:path*", "permanent": true }, { - "source": "/learn/stablecoins", - "destination": "https://tempo.xyz/learn/what-are-stablecoins", + "source": "/cli", + "destination": "/docs/cli", "permanent": true }, { - "source": "/learn/use-cases/global-payouts", - "destination": "https://tempo.xyz/learn/global-payouts", + "source": "/cli/:path*", + "destination": "/docs/cli/:path*", "permanent": true }, { - "source": "/learn/use-cases/remittances", - "destination": "https://tempo.xyz/learn/cross-border-payments", + "source": "/wallet", + "destination": "/docs/wallet", "permanent": true }, { - "source": "/learn/use-cases/embedded-finance", - "destination": "https://tempo.xyz/learn/stablecoin-payments", + "source": "/wallet/:path*", + "destination": "/docs/wallet/:path*", "permanent": true }, { - "source": "/learn/use-cases/agentic-commerce", - "destination": "https://tempo.xyz/learn/blockchain-payments", + "source": "/ecosystem", + "destination": "/docs/ecosystem", "permanent": true }, { - "source": "/learn/use-cases/payroll", - "destination": "https://tempo.xyz/learn/stablecoin-payroll", + "source": "/ecosystem/:path*", + "destination": "/docs/ecosystem/:path*", "permanent": true }, { - "source": "/learn/use-cases/tokenized-deposits", - "destination": "https://tempo.xyz/learn/tokenized-deposits", + "source": "/developer-tools", + "destination": "/docs/ecosystem", "permanent": true }, { - "source": "/learn/use-cases/microtransactions", - "destination": "https://tempo.xyz/learn/microtransactions", + "source": "/developer-tools/:path*", + "destination": "/docs/developer-tools/:path*", + "permanent": true + }, + { + "source": "/hosted-services", + "destination": "/docs/hosted-services", + "permanent": true + }, + { + "source": "/hosted-services/:path*", + "destination": "/docs/hosted-services/:path*", + "permanent": true + }, + { + "source": "/changelog", + "destination": "/docs/changelog", + "permanent": true + }, + { + "source": "/learn/partners", + "destination": "/docs/partners", + "permanent": true + }, + { + "source": "/docs/learn/partners", + "destination": "/docs/partners", + "permanent": true + }, + { + "source": "/docs/guide/using-tempo-with-ai/partners", + "destination": "/docs/partners", + "permanent": true + }, + { + "source": "/build/partners", + "destination": "/docs/partners", "permanent": true } ], "rewrites": [ - { - "source": "/api/mcp", - "destination": "https://mcp.tempo.xyz" - }, { "source": "/ingest/static/:path(.*)", "destination": "https://us-assets.i.posthog.com/static/:path" diff --git a/vite.config.ts b/vite.config.ts index b9523eb9..d69be3bd 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -2,10 +2,10 @@ import fs from 'node:fs/promises' import path from 'node:path' import react from '@vitejs/plugin-react' import { Instance } from 'prool' +import Icons from 'unplugin-icons/vite' import { defineConfig, loadEnv, type Plugin, type ResolvedConfig } from 'vite' import mkcert from 'vite-plugin-mkcert' import { vocs } from 'vocs/vite' -import { moderatoZoneRpcUrls } from './src/lib/private-zones.ts' // https://vite.dev/config/ export default defineConfig(({ mode }) => { @@ -14,9 +14,7 @@ export default defineConfig(({ mode }) => { if (!(key in process.env)) process.env[key] = env[key] } - const isE2E = process.env.VITE_E2E === 'true' || env.VITE_E2E === 'true' const useHttp = process.env.CI === 'true' || process.env.VITE_USE_HTTP === 'true' - const e2eZoneProxy = isE2E ? getE2EZoneProxy() : undefined const proxy = { '/api/mcp': { changeOrigin: true, @@ -24,11 +22,36 @@ export default defineConfig(({ mode }) => { secure: true, target: 'https://mcp.tempo.xyz', }, - ...e2eZoneProxy, } return { - plugins: [vocs(), react(), ...(useHttp ? [] : [mkcert()]), tempoNode(), llmsFeedbackPreamble()], + plugins: [ + marketingPages(), + crossAppPrefetch(), + vocs(), + Icons({ compiler: 'jsx', jsx: 'react' }), + react(), + ...(useHttp ? [] : [mkcert()]), + tempoNode(), + llmsFeedbackPreamble(), + ], + resolve: { + alias: [ + { + find: 'next/image', + replacement: path.resolve(process.cwd(), 'src/marketing/next-shims.tsx'), + }, + { + find: 'next/link', + replacement: path.resolve(process.cwd(), 'src/marketing/next-shims.tsx'), + }, + { + find: 'next/navigation', + replacement: path.resolve(process.cwd(), 'src/marketing/next-shims.tsx'), + }, + { find: 'next', replacement: path.resolve(process.cwd(), 'src/marketing/next-shims.tsx') }, + ], + }, server: { ...(useHttp ? { host: 'localhost' } : {}), proxy, @@ -36,28 +59,48 @@ export default defineConfig(({ mode }) => { } }) -function getE2EZoneProxy() { - return Object.fromEntries( - Object.entries(moderatoZoneRpcUrls).map(([zoneId, rpcUrl]) => { - const parsedUrl = new URL(rpcUrl) - const authorization = `Basic ${Buffer.from( - `${decodeURIComponent(parsedUrl.username)}:${decodeURIComponent(parsedUrl.password)}`, - ).toString('base64')}` - parsedUrl.username = '' - parsedUrl.password = '' - - return [ - `/__e2e_zone_rpc/${zoneId}`, - { - changeOrigin: true, - headers: { authorization }, - rewrite: () => '/', - secure: true, - target: parsedUrl.toString(), - }, - ] - }), - ) +const marketingRoutes = ['/', '/build', '/diagrams', '/performance'] + +function isMarketingPath(pathname: string) { + const normalized = pathname.replace(/\/$/, '') || '/' + return marketingRoutes.includes(normalized) || normalized.startsWith('/build/') +} + +async function marketingHtml() { + const html = await fs.readFile(path.resolve(process.cwd(), 'src/marketing/index.html'), 'utf-8') + return html.replace('src="./main.tsx"', 'src="/src/marketing/main.tsx"') +} + +function marketingPages(): Plugin { + return { + name: 'tempo-marketing-pages', + configureServer(server) { + server.middlewares.use(async (req, res, next) => { + if (!req.url) return next() + const url = new URL(req.url, 'http://localhost') + if (!isMarketingPath(url.pathname)) return next() + + const html = await server.transformIndexHtml(url.pathname, await marketingHtml()) + res.statusCode = 200 + res.setHeader('Content-Type', 'text/html') + res.end(html) + }) + }, + } +} + +function crossAppPrefetch(): Plugin { + return { + name: 'tempo-cross-app-prefetch', + enforce: 'post', + transformIndexHtml(html, context) { + const pathname = context.path?.replace(/\/$/, '') || '/' + const href = pathname === '/docs' || pathname.startsWith('/docs/') ? '/' : '/docs' + const prefetchLink = `` + if (html.includes(`href="${href}" rel="prefetch"`)) return html + return html.replace('', ` ${prefetchLink}\n `) + }, + } } const llmsFeedbackNotice = [ diff --git a/vite.marketing.config.ts b/vite.marketing.config.ts new file mode 100644 index 00000000..a283ca55 --- /dev/null +++ b/vite.marketing.config.ts @@ -0,0 +1,193 @@ +import fs from 'node:fs/promises' +import path from 'node:path' +import tailwindcss from '@tailwindcss/vite' +import react from '@vitejs/plugin-react' +import Icons from 'unplugin-icons/vite' +import { defineConfig, type Plugin } from 'vite' + +const staticRouteCopies = [ + 'build', + 'build/tempo-transactions', + 'build/tip20-tokens', + 'diagrams', + 'performance', +] + +function marketingRouteCopies(): Plugin { + return { + name: 'tempo-marketing-route-copies', + async closeBundle() { + const root = path.resolve(process.cwd(), 'dist/public') + const rootHtml = path.join(root, 'index.html') + const nestedHtml = path.join(root, 'src/marketing/index.html') + const html = await fs.readFile(rootHtml, 'utf-8').catch(async () => { + const nested = await fs.readFile(nestedHtml, 'utf-8') + await fs.writeFile(rootHtml, nested) + return nested + }) + const marketingHtml = applyMarketingMetadata( + injectHeadTags(html, ['']), + '/', + ) + await fs.writeFile(rootHtml, marketingHtml) + + await Promise.all( + (await marketingRouteCopiesForBuild()).map(async (route) => { + const routeDir = path.join(root, route) + await fs.mkdir(routeDir, { recursive: true }) + await fs.writeFile( + path.join(routeDir, 'index.html'), + applyMarketingMetadata(marketingHtml, route), + ) + }), + ) + }, + } +} + +async function marketingRouteCopiesForBuild() { + return staticRouteCopies +} + +const routeMetadata: Record = { + '/': { + title: 'Tempo', + description: + 'The only blockchain designed for payments. Sub-second transactions, sub-cent fees.', + }, + build: { + title: 'Tempo', + description: + 'Build payment products on Tempo with stablecoins, fast settlement, and predictable fees.', + }, + 'build/tempo-transactions': { + title: 'Tempo Transactions', + description: 'Batch, sponsor, schedule, and parallelize payments with Tempo Transactions.', + }, + 'build/tip20-tokens': { + title: 'TIP-20 Tokens', + description: + 'Stablecoin-first Tempo Tokens for payments, fees, memos, policies, and liquidity.', + }, + performance: { + title: 'Tempo Performance', + description: + 'Nightly benchmarks on Tempo throughput, block times, execution rates, and uptime.', + }, + diagrams: { + title: 'Tempo Diagrams', + description: 'A playground for Tempo diagrams, product visuals, and house-style SVG exports.', + }, +} + +function titleCaseRoute(route: string) { + const acronyms: Record = { api: 'API', mpp: 'MPP', sdk: 'SDK', sdks: 'SDKs' } + return route + .split('/') + .pop() + ?.split('-') + .filter(Boolean) + .map((word) => acronyms[word] ?? word[0]?.toUpperCase() + word.slice(1)) + .join(' ') +} + +function marketingMetadata(route: string) { + return ( + routeMetadata[route] ?? { + title: `${titleCaseRoute(route)} ⋅ Tempo`, + description: 'Build payment products on Tempo with stablecoins and predictable settlement.', + } + ) +} + +function escapeHtmlAttribute(value: string) { + return value.replaceAll('&', '&').replaceAll('"', '"') +} + +function applyMarketingMetadata(html: string, route: string) { + const metadata = marketingMetadata(route) + const ogImage = marketingOgImage(route, metadata) + return html + .replace(/.*?<\/title>/, `<title>${metadata.title}`) + .replace( + //, + ``, + ) + .replace( + //, + ``, + ) + .replace( + //, + ``, + ) + .replace( + //, + ``, + ) + .replace( + //, + ``, + ) + .replace( + //, + ``, + ) + .replace( + //, + ``, + ) +} + +function marketingOgImage(route: string, metadata: { title: string; description: string }) { + const sections: Record = { + performance: 'PERFORMANCE', + diagrams: 'DIAGRAMS', + } + const section = sections[route] || 'BUILD' + return `/api/og?${new URLSearchParams({ + title: metadata.title, + description: metadata.description, + section, + }).toString()}` +} + +function injectHeadTags(html: string, tags: string[]) { + const missing = tags.filter((tag) => !html.includes(tag)) + if (missing.length === 0) return html + return html.replace('', ` ${missing.join('\n ')}\n `) +} + +export default defineConfig({ + root: 'src/marketing', + plugins: [ + tailwindcss(), + Icons({ compiler: 'jsx', jsx: 'react' }), + react(), + marketingRouteCopies(), + ], + resolve: { + alias: [ + { + find: 'next/image', + replacement: path.resolve(process.cwd(), 'src/marketing/next-shims.tsx'), + }, + { + find: 'next/link', + replacement: path.resolve(process.cwd(), 'src/marketing/next-shims.tsx'), + }, + { + find: 'next/navigation', + replacement: path.resolve(process.cwd(), 'src/marketing/next-shims.tsx'), + }, + { find: 'next', replacement: path.resolve(process.cwd(), 'src/marketing/next-shims.tsx') }, + ], + }, + build: { + emptyOutDir: false, + outDir: '../../dist/public', + rollupOptions: { + input: 'index.html', + }, + }, +}) diff --git a/vocs.config.ts b/vocs.config.ts index 248346e8..fcb06eaf 100644 --- a/vocs.config.ts +++ b/vocs.config.ts @@ -5,9 +5,9 @@ import { createFeedbackAdapter } from './src/lib/feedback-adapter' // which causes all links to resolve to the absolute URL on preview deployments. const baseUrl = (() => { if (process.env.VERCEL_ENV && process.env.VERCEL_ENV !== 'production') return '' - if (URL.canParse(process.env.VITE_BASE_URL)) return process.env.VITE_BASE_URL - if (process.env.VERCEL_ENV === 'production') - return `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}` + if (URL.canParse(process.env.VITE_BASE_URL)) return process.env.VITE_BASE_URL.replace(/\/$/, '') + const productionUrl = process.env.VERCEL_PROJECT_PRODUCTION_URL + if (process.env.VERCEL_ENV === 'production' && productionUrl) return `https://${productionUrl}` return '' })() @@ -42,9 +42,11 @@ export default defineConfig({ ], }, baseUrl: baseUrl || undefined, - ogImageUrl: (path, { baseUrl } = { baseUrl: '' }) => { - const landingPaths = ['/', '/learn', '/changelog'] - if (landingPaths.includes(path)) return `${baseUrl}/og-docs.png` + ogImageUrl: (path, options = {}) => { + const urlBase = options.baseUrl?.replace(/\/$/, '') ?? '' + const docsPath = path.replace(/^\/docs(?=\/|$)/, '') || '/' + const landingPaths = ['/', '/changelog'] + if (landingPaths.includes(docsPath)) return `${urlBase}/og-docs.png` const sectionMap: Record = { quickstart: 'INTEGRATE', @@ -54,13 +56,10 @@ export default defineConfig({ cli: 'CLI', ecosystem: 'ECOSYSTEM', 'hosted-services': 'HOSTED SERVICES', - learn: 'LEARN', wallet: 'WALLET', - accounts: 'ACCOUNTS', } const subsectionMap: Record = { - 'use-accounts': 'ACCOUNTS', payments: 'PAYMENTS', issuance: 'ISSUANCE', 'stablecoin-dex': 'EXCHANGE', @@ -83,8 +82,6 @@ export default defineConfig({ stablecoins: 'STABLECOINS', 'use-cases': 'USE CASES', tempo: 'TEMPO', - zones: 'ZONES', - 'private-zones': 'PRIVATE ZONES', upgrades: 'UPGRADES', api: 'API', guides: 'GUIDES', @@ -93,7 +90,7 @@ export default defineConfig({ wagmi: 'WAGMI', } - const segments = path.split('/').filter(Boolean) + const segments = docsPath.split('/').filter(Boolean) const firstSeg = segments[0] || '' const secondSeg = segments[1] || '' const section = sectionMap[firstSeg] || firstSeg.toUpperCase().replace(/-/g, ' ') @@ -109,9 +106,8 @@ export default defineConfig({ ...(subsection ? { subsection } : {}), }).toString() - return `${baseUrl}/api/og?title=%title&description=%description&${extra}` + return `${urlBase}/api/og?title=%title&description=%description&${extra}` }, - // TODO: Change back to file paths (`/lockup-light.svg`, `/lockup-dark.svg`) once password protection is removed logoUrl: { light: 'data:image/svg+xml,%3Csvg%20width%3D%22184%22%20height%3D%2241%22%20viewBox%3D%220%200%20184%2041%22%20fill%3D%22none%22%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%3E%0A%3Cpath%20d%3D%22M13.6424%2040.3635H2.80251L12.8492%209.60026H0L2.80251%200.58344H38.6006L35.7981%209.60026H23.6362L13.6424%2040.3635Z%22%20fill%3D%22black%22/%3E%0A%3Cpath%20d%3D%22M53.9809%2040.3635H28.2824L41.1846%200.58344H66.7773L64.3449%208.16818H49.4863L46.7896%2016.7076H61.1723L58.7399%2024.1863H44.3043L41.6076%2032.7788H56.3604L53.9809%2040.3635Z%22%20fill%3D%22black%22/%3E%0A%3Cpath%20d%3D%22M65.6123%2040.3635H56.9933L69.9483%200.58344H84.331L83.8551%2022.0647L97.8676%200.58344H113.625L100.723%2040.3635H89.936L98.5021%2013.6313H98.3435L80.7353%2040.3635H74.3371L74.6015%2013.3131H74.4957L65.6123%2040.3635Z%22%20fill%3D%22black%22/%3E%0A%3Cpath%20d%3D%22M125.758%207.95602L121.581%2020.7917H122.744C125.388%2020.7917%20127.592%2020.1729%20129.354%2018.9353C131.117%2017.6624%20132.262%2015.859%20132.791%2013.5252C133.249%2011.5097%20133.003%2010.0776%20132.051%209.22898C131.099%208.38034%20129.513%207.95602%20127.292%207.95602H125.758ZM115.289%2040.3635H104.449L117.351%200.58344H130.517C133.549%200.58344%20136.158%201.07848%20138.343%202.06856C140.564%203.02328%20142.186%204.40233%20143.208%206.20569C144.266%207.97369%20144.618%2010.0423%20144.266%2012.4114C143.807%2015.5231%20142.609%2018.2635%20140.67%2020.6326C138.731%2023.0017%20136.211%2024.8405%20133.108%2026.1488C130.042%2027.4217%20126.604%2028.0582%20122.797%2028.0582H119.255L115.289%2040.3635Z%22%20fill%3D%22black%22/%3E%0A%3Cpath%20d%3D%22M170.103%2037.8176C166.507%2039.9392%20162.682%2041%20158.628%2041H158.523C154.927%2041%20151.895%2040.2044%20149.428%2038.6132C146.995%2036.9866%20145.25%2034.7943%20144.193%2032.0362C143.171%2029.2781%20142.924%2026.2549%20143.453%2022.9664C144.122%2018.8292%20145.656%2015.0103%20148.053%2011.5097C150.45%208.00906%20153.446%205.21561%20157.042%203.12937C160.638%201.04312%20164.48%200%20168.569%200H168.675C172.412%200%20175.496%200.795602%20177.929%202.38681C180.396%203.97801%20182.106%206.15265%20183.058%208.91074C184.045%2011.6335%20184.256%2014.6921%20183.692%2018.0867C183.023%2022.0824%20181.489%2025.8482%20179.092%2029.3842C176.695%2032.8849%20173.699%2035.696%20170.103%2037.8176ZM155.138%2030.9754C156.09%2032.7788%20157.747%2033.6805%20160.109%2033.6805H160.215C162.154%2033.6805%20163.951%2032.9556%20165.608%2031.5058C167.3%2030.0207%20168.728%2028.0405%20169.891%2025.5653C171.09%2023.0901%20171.971%2020.332%20172.535%2017.2911C173.064%2014.3208%20172.852%2011.934%20171.901%2010.1307C170.949%208.29194%20169.31%207.37257%20166.983%207.37257H166.877C165.079%207.37257%20163.335%208.11514%20161.642%209.60026C159.986%2011.0854%20158.54%2013.0832%20157.306%2015.5938C156.073%2018.1044%20155.174%2020.8271%20154.61%2023.762C154.046%2026.7322%20154.222%2029.1367%20155.138%2030.9754Z%22%20fill%3D%22black%22/%3E%0A%3C/svg%3E', @@ -133,51 +129,25 @@ export default defineConfig({ }, ], sidebar: { - '/': [ + '/docs': [ { text: 'Home', - link: '/', + link: '/docs', }, { text: 'Developing with LLMs', - link: '/guide/using-tempo-with-ai', + link: '/docs/guide/using-tempo-with-ai', + }, + { + text: 'Partners', + link: '/docs/partners', }, { text: 'Build on Tempo', items: [ { text: 'Getting Funds on Tempo', - link: '/guide/getting-funds', - }, - { - text: 'Create & Use Accounts', - collapsed: true, - items: [ - { - text: 'Overview', - link: '/guide/use-accounts', - }, - { - text: 'Embed Tempo Wallet', - link: '/guide/use-accounts/embed-tempo-wallet', - }, - { - text: 'Embed domain-bound Passkeys', - link: '/guide/use-accounts/embed-passkeys', - }, - { - text: 'Authorize access keys', - link: '/guide/use-accounts/authorize-access-keys', - }, - { - text: 'Connect to other wallets', - link: '/guide/use-accounts/connect-to-wallets', - }, - { - text: 'Add funds to your balance', - link: '/guide/use-accounts/add-funds', - }, - ], + link: '/docs/guide/getting-funds', }, { text: 'Make Payments', @@ -185,109 +155,75 @@ export default defineConfig({ items: [ { text: 'Overview', - link: '/guide/payments', + link: '/docs/guide/payments', }, { text: 'Send a payment', - link: '/guide/payments/send-a-payment', + link: '/docs/guide/payments/send-a-payment', }, { text: 'Accept a payment', - link: '/guide/payments/accept-a-payment', + link: '/docs/guide/payments/accept-a-payment', }, { text: 'Attach a transfer memo', - link: '/guide/payments/transfer-memos', + link: '/docs/guide/payments/transfer-memos', }, { text: 'Use virtual addresses', - link: '/guide/payments/virtual-addresses', + link: '/docs/guide/payments/virtual-addresses', }, { text: 'Pay fees in any stablecoin', - link: '/guide/payments/pay-fees-in-any-stablecoin', + link: '/docs/guide/payments/pay-fees-in-any-stablecoin', }, { text: 'Sponsor user fees', - link: '/guide/payments/sponsor-user-fees', + link: '/docs/guide/payments/sponsor-user-fees', }, { text: 'Send parallel transactions', - link: '/guide/payments/send-parallel-transactions', + link: '/docs/guide/payments/send-parallel-transactions', }, // { // text: 'Start a subscription 🚧', // disabled: true, - // link: '/guide/payments/start-a-subscription', + // link: '/docs/guide/payments/start-a-subscription', // }, // { // text: 'Private payments 🚧', // disabled: true, - // link: '/guide/payments/private-payments', + // link: '/docs/guide/payments/private-payments', // }, ], }, - { - text: 'Private Transactions', - collapsed: true, - items: [ - { - text: 'Overview', - link: '/guide/private-zones', - }, - { - text: 'Connect to a zone', - link: '/guide/private-zones/connect-to-a-zone', - }, - { - text: 'Deposit to a zone', - link: '/guide/private-zones/deposit-to-a-zone', - }, - { - text: 'Send tokens within a zone', - link: '/guide/private-zones/send-tokens-within-a-zone', - }, - { - text: 'Send tokens across zones', - link: '/guide/private-zones/send-tokens-across-zones', - }, - { - text: 'Swap across zones', - link: '/guide/private-zones/swap-across-zones', - }, - { - text: 'Withdraw from a zone', - link: '/guide/private-zones/withdraw-from-a-zone', - }, - ], - }, { text: 'Issue Stablecoins', collapsed: true, items: [ { text: 'Overview', - link: '/guide/issuance', + link: '/docs/guide/issuance', }, { text: 'Create a stablecoin', - link: '/guide/issuance/create-a-stablecoin', + link: '/docs/guide/issuance/create-a-stablecoin', }, { text: 'Mint stablecoins', - link: '/guide/issuance/mint-stablecoins', + link: '/docs/guide/issuance/mint-stablecoins', }, { text: 'Use your stablecoin for fees', - link: '/guide/issuance/use-for-fees', + link: '/docs/guide/issuance/use-for-fees', }, { text: 'Distribute rewards', - link: '/guide/issuance/distribute-rewards', + link: '/docs/guide/issuance/distribute-rewards', }, { text: 'Manage your stablecoin', - link: '/guide/issuance/manage-stablecoin', + link: '/docs/guide/issuance/manage-stablecoin', }, ], }, @@ -297,23 +233,23 @@ export default defineConfig({ items: [ { text: 'Overview', - link: '/guide/stablecoin-dex', + link: '/docs/guide/stablecoin-dex', }, { text: 'Managing fee liquidity', - link: '/guide/stablecoin-dex/managing-fee-liquidity', + link: '/docs/guide/stablecoin-dex/managing-fee-liquidity', }, { text: 'Executing swaps', - link: '/guide/stablecoin-dex/executing-swaps', + link: '/docs/guide/stablecoin-dex/executing-swaps', }, { text: 'View the orderbook', - link: '/guide/stablecoin-dex/view-the-orderbook', + link: '/docs/guide/stablecoin-dex/view-the-orderbook', }, { text: 'Providing liquidity', - link: '/guide/stablecoin-dex/providing-liquidity', + link: '/docs/guide/stablecoin-dex/providing-liquidity', }, ], }, @@ -323,35 +259,35 @@ export default defineConfig({ items: [ { text: 'Overview', - link: '/guide/machine-payments', + link: '/docs/guide/machine-payments', }, { text: 'Client quickstart', - link: '/guide/machine-payments/client', + link: '/docs/guide/machine-payments/client', }, { text: 'Agent quickstart', - link: '/guide/machine-payments/agent', + link: '/docs/guide/machine-payments/agent', }, { text: 'Discover MPP services', - link: '/guide/machine-payments/discover-services', + link: '/docs/guide/machine-payments/discover-services', }, { text: 'Server quickstart', - link: '/guide/machine-payments/server', + link: '/docs/guide/machine-payments/server', }, { text: 'Accept one-time payments', - link: '/guide/machine-payments/one-time-payments', + link: '/docs/guide/machine-payments/one-time-payments', }, { text: 'Accept pay-as-you-go payments', - link: '/guide/machine-payments/pay-as-you-go', + link: '/docs/guide/machine-payments/pay-as-you-go', }, { text: 'Accept streamed payments', - link: '/guide/machine-payments/streamed-payments', + link: '/docs/guide/machine-payments/streamed-payments', }, { text: 'Use Cases', @@ -359,55 +295,55 @@ export default defineConfig({ items: [ { text: 'Monetize Your API', - link: '/guide/machine-payments/use-cases/monetize-your-api', + link: '/docs/guide/machine-payments/use-cases/monetize-your-api', }, { text: 'AI Model Access', - link: '/guide/machine-payments/use-cases/ai-model-access', + link: '/docs/guide/machine-payments/use-cases/ai-model-access', }, { text: 'Web Search & Research', - link: '/guide/machine-payments/use-cases/web-search-and-research', + link: '/docs/guide/machine-payments/use-cases/web-search-and-research', }, { text: 'Image & Media Generation', - link: '/guide/machine-payments/use-cases/image-and-media-generation', + link: '/docs/guide/machine-payments/use-cases/image-and-media-generation', }, { text: 'Browser Automation', - link: '/guide/machine-payments/use-cases/browser-automation', + link: '/docs/guide/machine-payments/use-cases/browser-automation', }, { text: 'Compute & Code Execution', - link: '/guide/machine-payments/use-cases/compute-and-code-execution', + link: '/docs/guide/machine-payments/use-cases/compute-and-code-execution', }, { text: 'Storage', - link: '/guide/machine-payments/use-cases/storage', + link: '/docs/guide/machine-payments/use-cases/storage', }, { text: 'Blockchain Data & Analytics', - link: '/guide/machine-payments/use-cases/blockchain-data', + link: '/docs/guide/machine-payments/use-cases/blockchain-data', }, { text: 'Financial & Market Data', - link: '/guide/machine-payments/use-cases/financial-data', + link: '/docs/guide/machine-payments/use-cases/financial-data', }, { text: 'Data Enrichment & Leads', - link: '/guide/machine-payments/use-cases/data-enrichment-and-leads', + link: '/docs/guide/machine-payments/use-cases/data-enrichment-and-leads', }, { text: 'Translation & Language', - link: '/guide/machine-payments/use-cases/translation-and-language', + link: '/docs/guide/machine-payments/use-cases/translation-and-language', }, { text: 'Maps & Location Data', - link: '/guide/machine-payments/use-cases/location-and-maps', + link: '/docs/guide/machine-payments/use-cases/location-and-maps', }, { text: 'Agent-to-Agent Services', - link: '/guide/machine-payments/use-cases/agent-to-agent', + link: '/docs/guide/machine-payments/use-cases/agent-to-agent', }, ], }, @@ -420,39 +356,39 @@ export default defineConfig({ items: [ { text: 'Overview', - link: '/quickstart/integrate-tempo', + link: '/docs/quickstart/integrate-tempo', }, { text: 'Connect to the Network', - link: '/quickstart/connection-details', + link: '/docs/quickstart/connection-details', }, { text: 'Use Tempo Transactions', - link: '/guide/tempo-transaction', + link: '/docs/guide/tempo-transaction', }, { text: 'Get Testnet Faucet Funds', - link: '/quickstart/faucet', + link: '/docs/quickstart/faucet', }, { text: 'EVM Differences', - link: '/quickstart/evm-compatibility', + link: '/docs/quickstart/evm-compatibility', }, { text: 'Predeployed Contracts', - link: '/quickstart/predeployed-contracts', + link: '/docs/quickstart/predeployed-contracts', }, { text: 'Token List Registry', - link: '/quickstart/tokenlist', + link: '/docs/quickstart/tokenlist', }, { text: 'Wallet Developers', - link: '/quickstart/wallet-developers', + link: '/docs/quickstart/wallet-developers', }, { text: 'Contract Verification', - link: '/quickstart/verify-contracts', + link: '/docs/quickstart/verify-contracts', }, { text: 'Bridging', @@ -460,15 +396,15 @@ export default defineConfig({ items: [ { text: 'Bridge via LayerZero', - link: '/guide/bridge-layerzero', + link: '/docs/guide/bridge-layerzero', }, { text: 'Bridge via Bungee', - link: '/guide/bridge-bungee', + link: '/docs/guide/bridge-bungee', }, { text: 'Bridge via Relay', - link: '/guide/bridge-relay', + link: '/docs/guide/bridge-relay', }, ], }, @@ -478,39 +414,39 @@ export default defineConfig({ items: [ { text: 'Overview', - link: '/ecosystem', + link: '/docs/ecosystem', }, { text: 'Bridges & Exchanges', - link: '/ecosystem/bridges', + link: '/docs/ecosystem/bridges', }, { text: 'Data & Analytics', - link: '/ecosystem/data-analytics', + link: '/docs/ecosystem/data-analytics', }, { text: 'Block Explorers', - link: '/ecosystem/block-explorers', + link: '/docs/ecosystem/block-explorers', }, { text: 'Wallets', - link: '/ecosystem/wallets', + link: '/docs/ecosystem/wallets', }, { text: 'Smart Contract Libraries', - link: '/ecosystem/smart-contract-libraries', + link: '/docs/ecosystem/smart-contract-libraries', }, { text: 'Node Infrastructure', - link: '/ecosystem/node-infrastructure', + link: '/docs/ecosystem/node-infrastructure', }, { text: 'Security & Compliance', - link: '/ecosystem/security-compliance', + link: '/docs/ecosystem/security-compliance', }, { text: 'Issuance & Orchestration', - link: '/ecosystem/orchestration', + link: '/docs/ecosystem/orchestration', }, ], }, @@ -521,7 +457,7 @@ export default defineConfig({ items: [ { text: 'Overview', - link: '/protocol', + link: '/docs/protocol', }, { text: 'TIP-20 Tokens', @@ -529,15 +465,15 @@ export default defineConfig({ items: [ { text: 'Overview', - link: '/protocol/tip20/overview', + link: '/docs/protocol/tip20/overview', }, { text: 'Specification', - link: '/protocol/tip20/spec', + link: '/docs/protocol/tip20/spec', }, { text: 'Virtual addresses', - link: '/protocol/tip20/virtual-addresses', + link: '/docs/protocol/tip20/virtual-addresses', }, { text: 'Rust Implementation', @@ -551,11 +487,11 @@ export default defineConfig({ items: [ { text: 'Overview', - link: '/protocol/tip20-rewards/overview', + link: '/docs/protocol/tip20-rewards/overview', }, { text: 'Specification', - link: '/protocol/tip20-rewards/spec', + link: '/docs/protocol/tip20-rewards/spec', }, ], }, @@ -565,11 +501,11 @@ export default defineConfig({ items: [ { text: 'Overview', - link: '/protocol/tip403/overview', + link: '/docs/protocol/tip403/overview', }, { text: 'Specification', - link: '/protocol/tip403/spec', + link: '/docs/protocol/tip403/spec', }, { text: 'Rust Implementation', @@ -583,11 +519,11 @@ export default defineConfig({ items: [ { text: 'Overview', - link: '/protocol/fees', + link: '/docs/protocol/fees', }, { text: 'Specification', - link: '/protocol/fees/spec-fee', + link: '/docs/protocol/fees/spec-fee', }, { text: 'Fee AMM', @@ -595,11 +531,11 @@ export default defineConfig({ items: [ { text: 'Overview', - link: '/protocol/fees/fee-amm', + link: '/docs/protocol/fees/fee-amm', }, { text: 'Specification', - link: '/protocol/fees/spec-fee-amm', + link: '/docs/protocol/fees/spec-fee-amm', }, { text: 'Rust Implementation', @@ -615,23 +551,23 @@ export default defineConfig({ items: [ { text: 'Overview', - link: '/protocol/transactions', + link: '/docs/protocol/transactions', }, { text: 'Specification', - link: '/protocol/transactions/spec-tempo-transaction', + link: '/docs/protocol/transactions/spec-tempo-transaction', }, { text: 'EIP-4337 Comparison', - link: '/protocol/transactions/eip-4337', + link: '/docs/protocol/transactions/eip-4337', }, { text: 'EIP-7702 Comparison', - link: '/protocol/transactions/eip-7702', + link: '/docs/protocol/transactions/eip-7702', }, { text: 'Account Keychain Precompile Specification', - link: '/protocol/transactions/AccountKeychain', + link: '/docs/protocol/transactions/AccountKeychain', }, { text: 'Rust Implementation', @@ -645,15 +581,15 @@ export default defineConfig({ items: [ { text: 'Overview', - link: '/protocol/blockspace/overview', + link: '/docs/protocol/blockspace/overview', }, { text: 'Payment Lane Specification', - link: '/protocol/blockspace/payment-lane-specification', + link: '/docs/protocol/blockspace/payment-lane-specification', }, { text: 'Consensus and Finality', - link: '/protocol/blockspace/consensus', + link: '/docs/protocol/blockspace/consensus', }, ], }, @@ -663,27 +599,27 @@ export default defineConfig({ items: [ { text: 'Overview', - link: '/protocol/exchange', + link: '/docs/protocol/exchange', }, { text: 'Specification', - link: '/protocol/exchange/spec', + link: '/docs/protocol/exchange/spec', }, { text: 'Quote Tokens', - link: '/protocol/exchange/quote-tokens', + link: '/docs/protocol/exchange/quote-tokens', }, { text: 'Executing Swaps', - link: '/protocol/exchange/executing-swaps', + link: '/docs/protocol/exchange/executing-swaps', }, { text: 'Providing Liquidity', - link: '/protocol/exchange/providing-liquidity', + link: '/docs/protocol/exchange/providing-liquidity', }, { text: 'DEX Balance', - link: '/protocol/exchange/exchange-balance', + link: '/docs/protocol/exchange/exchange-balance', }, { text: 'Rust Implementation', @@ -691,45 +627,6 @@ export default defineConfig({ }, ], }, - { - text: 'Tempo Zones', - collapsed: true, - items: [ - { - text: 'Overview', - link: '/protocol/zones', - }, - { - text: 'Reference', - items: [ - { - text: 'Architecture', - link: '/protocol/zones/architecture', - }, - { - text: 'Accounts', - link: '/protocol/zones/accounts', - }, - { - text: 'Bridging', - link: '/protocol/zones/bridging', - }, - { - text: 'RPC', - link: '/protocol/zones/rpc', - }, - { - text: 'Execution & Gas', - link: '/protocol/zones/execution', - }, - { - text: 'Proving', - link: '/protocol/zones/proving', - }, - ], - }, - ], - }, { text: 'Network Upgrades', collapsed: true, @@ -737,35 +634,35 @@ export default defineConfig({ { text: 'T7', badge: { text: 'Planned', variant: 'note' }, - link: '/protocol/upgrades/t7', + link: '/docs/protocol/upgrades/t7', }, { text: 'T6', badge: { text: 'Testnet', variant: 'info' }, - link: '/protocol/upgrades/t6', + link: '/docs/protocol/upgrades/t6', }, { text: 'T5', badge: { text: 'Latest', variant: 'info' }, - link: '/protocol/upgrades/t5', + link: '/docs/protocol/upgrades/t5', }, { text: 'T4', - link: '/protocol/upgrades/t4', + link: '/docs/protocol/upgrades/t4', }, { text: 'T3', - link: '/protocol/upgrades/t3', + link: '/docs/protocol/upgrades/t3', }, { text: 'T2', - link: '/protocol/upgrades/t2', + link: '/docs/protocol/upgrades/t2', }, ], }, { text: 'TIPs', - link: 'https://tips.sh/', + link: '/docs/protocol/tips', }, ], }, @@ -774,54 +671,73 @@ export default defineConfig({ items: [ { text: 'Hosted Services', + collapsed: true, items: [ { text: 'Overview', - link: '/hosted-services', + link: '/docs/hosted-services', }, { text: 'Hosted Fee Payer', - link: '/developer-tools/fee-payer', + link: '/docs/developer-tools/fee-payer', }, { text: 'Indexer (tidx)', - link: '/developer-tools/indexer', + link: '/docs/developer-tools/indexer', }, ], }, - { - text: 'Accounts SDK', - link: 'https://accounts.tempo.xyz/docs', - }, { text: 'CLI', collapsed: true, items: [ { text: 'Overview', - link: '/cli', + link: '/docs/cli', }, { text: 'Wallet', - link: '/cli/wallet', + link: '/docs/cli/wallet', }, { text: 'Request', - link: '/cli/request', + link: '/docs/cli/request', }, { text: 'Download', - link: '/cli/download', + link: '/docs/cli/download', }, { text: 'Node', - link: '/cli/node', + link: '/docs/cli/node', + }, + ], + }, + { + text: 'Tempo Wallet', + collapsed: true, + items: [ + { + text: 'Overview', + link: '/docs/wallet', + }, + { + text: 'Recipes', + link: '/docs/wallet/recipes', + }, + { + text: 'Reference', + link: '/docs/wallet/reference', + }, + { + text: 'Use with agents', + link: '/docs/wallet/use-with-agents', }, ], }, { text: 'RPC Reference', - link: '/protocol/rpc', + link: '/docs/protocol/rpc', }, { text: 'SDKs', @@ -829,7 +745,7 @@ export default defineConfig({ items: [ { text: 'Overview', - link: '/sdk', + link: '/docs/sdk', }, { text: 'TypeScript', @@ -837,7 +753,7 @@ export default defineConfig({ items: [ { text: 'Overview', - link: '/sdk/typescript', + link: '/docs/sdk/typescript', }, { text: 'Viem Reference', @@ -852,7 +768,7 @@ export default defineConfig({ items: [ { text: 'Setup', - link: '/sdk/typescript/prool/setup', + link: '/docs/sdk/typescript/prool/setup', }, ], }, @@ -860,7 +776,7 @@ export default defineConfig({ }, { text: 'Go', - link: '/sdk/go', + link: '/docs/sdk/go', }, { text: 'Foundry', @@ -868,25 +784,25 @@ export default defineConfig({ items: [ { text: 'Overview', - link: '/sdk/foundry', + link: '/docs/sdk/foundry', }, { text: 'Use MPP with Foundry', - link: '/sdk/foundry/mpp', + link: '/docs/sdk/foundry/mpp', }, { text: 'Signature Verification', - link: '/sdk/foundry/signature-verifier', + link: '/docs/sdk/foundry/signature-verifier', }, ], }, { text: 'Python', - link: '/sdk/python', + link: '/docs/sdk/python', }, { text: 'Rust', - link: '/sdk/rust', + link: '/docs/sdk/rust', }, ], }, @@ -898,73 +814,77 @@ export default defineConfig({ items: [ { text: 'Overview', - link: '/guide/node', + link: '/docs/guide/node', }, { text: 'System Requirements', - link: '/guide/node/system-requirements', + link: '/docs/guide/node/system-requirements', }, { text: 'Installation', - link: '/guide/node/installation', + link: '/docs/guide/node/installation', }, { text: 'Running an RPC Node', - link: '/guide/node/rpc', + link: '/docs/guide/node/rpc', }, { text: 'Running a validator', items: [ { text: 'Overview', - link: '/guide/node/validator', + link: '/docs/guide/node/validator', }, { text: 'Validator Onboarding', - link: '/guide/node/validator-setup', + link: '/docs/guide/node/validator-setup', }, { text: 'Checking validator status', - link: '/guide/node/validator-status', + link: '/docs/guide/node/validator-status', }, { text: 'Controlling validator lifecycle', - link: '/guide/node/validator-lifecycle', + link: '/docs/guide/node/validator-lifecycle', }, { text: 'Managing validator keys', - link: '/guide/node/validator-keys', + link: '/docs/guide/node/validator-keys', + }, + { + text: 'Validator failover', + link: '/docs/guide/node/validator-failover', }, { text: 'Monitoring a validator', - link: '/guide/node/validator-monitoring', + link: '/docs/guide/node/validator-monitoring', }, { text: 'Troubleshooting and FAQ', - link: '/guide/node/validator-troubleshooting', + link: '/docs/guide/node/validator-troubleshooting', }, ], }, { text: 'Node Security', - link: '/guide/node/security', + link: '/docs/guide/node/security', }, { text: 'Network Upgrades and Releases', items: [ { text: 'Upgrade Cadence', - link: '/guide/node/upgrade-cadence', + link: '/docs/guide/node/upgrade-cadence', }, { text: 'Upgrades and Releases', - link: '/guide/node/network-upgrades', + link: '/docs/guide/node/network-upgrades', }, ], }, { text: 'Changelog', - link: '/changelog', + link: '/docs/changelog', }, ], }, @@ -973,506 +893,122 @@ export default defineConfig({ // items: [ // { // text: 'Overview', - // link: '/guide/infrastructure', + // link: '/docs/guide/infrastructure', // }, // { // text: 'Data Indexers', - // link: '/guide/infrastructure/data-indexers', + // link: '/docs/guide/infrastructure/data-indexers', // }, // { // text: 'Developer Tools', - // link: '/guide/infrastructure/developer-tools', + // link: '/docs/guide/infrastructure/developer-tools', // }, // { // text: 'Node Providers', - // link: '/guide/infrastructure/node-providers', + // link: '/docs/guide/infrastructure/node-providers', // }, // ], // }, ], - '/accounts': { - backLink: true, - items: [ - { - text: 'Accounts SDK', - items: [ - { - text: 'Getting Started', - link: '/accounts', - }, - { - text: 'Deploying to Production', - link: '/accounts/production', - }, - { - text: 'FAQ', - link: '/accounts/faq', - }, - { - text: 'GitHub', - link: 'https://github.com/tempoxyz/accounts', - }, - ], - }, - { - text: 'Guides', - items: [ - { - text: 'Create & Use Accounts', - link: '/guide/use-accounts', - external: true, - }, - { - text: 'Make Payments', - link: '/guide/payments', - external: true, - }, - { - text: 'Sponsor Fees', - link: '/guide/payments/sponsor-user-fees', - external: true, - }, - { - text: 'Issue Stablecoins', - link: '/guide/issuance', - external: true, - }, - { - text: 'Exchange Stablecoins', - link: '/guide/stablecoin-dex', - external: true, - }, - ], - }, - { - text: 'Core', - items: [ - { - text: 'Provider', - link: '/accounts/api/provider', - }, - { - text: 'Adapters', - collapsed: true, - items: [ - { - text: 'Overview', - link: '/accounts/api/adapters', - }, - { - text: 'dialog / tempoWallet', - link: '/accounts/api/dialog', - }, - { - text: 'webAuthn', - link: '/accounts/api/webAuthn', - }, - { - text: 'local', - link: '/accounts/api/local', - }, - ], - }, - { - text: 'Dialog', - collapsed: true, - items: [ - { - text: 'Overview', - link: '/accounts/api/dialogs', - }, - { - text: '.iframe', - link: '/accounts/api/dialog.iframe', - }, - { - text: '.popup', - link: '/accounts/api/dialog.popup', - }, - ], - }, - { - text: 'Expiry', - link: '/accounts/api/expiry', - }, - { - text: 'WebAuthnCeremony', - collapsed: true, - items: [ - { - text: 'Overview', - link: '/accounts/api/webauthnceremony', - }, - { - text: '.from', - link: '/accounts/api/webauthnceremony.from', - }, - { - text: '.server', - link: '/accounts/api/webauthnceremony.server', - }, - ], - }, - ], - }, - { - text: 'Wagmi', - items: [ - { - text: 'Connectors', - collapsed: true, - items: [ - { - text: 'tempoWallet', - link: '/accounts/wagmi/tempoWallet', - }, - { - text: 'webAuthn', - link: '/accounts/wagmi/webAuthn', - }, - ], - }, - ], - }, - { - text: 'Server', - items: [ - { - text: 'Handlers', - collapsed: true, - items: [ - { - text: 'Overview', - link: '/accounts/server', - }, - { - text: '.compose', - link: '/accounts/server/handler.compose', - }, - { - text: '.feePayer', - link: '/accounts/server/handler.feePayer', - }, - { - text: '.relay', - link: '/accounts/server/handler.relay', - }, - { - text: '.webAuthn', - link: '/accounts/server/handler.webAuthn', - }, - ], - }, - { - text: 'Kv', - link: '/accounts/server/kv', - }, - ], - }, - { - text: 'JSON-RPC', - items: [ - { - text: 'wallet_connect 🚧', - disabled: true, - link: '/accounts/rpc/wallet_connect', - }, - { - text: 'wallet_disconnect 🚧', - disabled: true, - link: '/accounts/rpc/wallet_disconnect', - }, - { - text: 'wallet_authorizeAccessKey 🚧', - disabled: true, - link: '/accounts/rpc/wallet_authorizeAccessKey', - }, - { - text: 'wallet_revokeAccessKey 🚧', - disabled: true, - link: '/accounts/rpc/wallet_revokeAccessKey', - }, - { - text: 'wallet_getBalances 🚧', - disabled: true, - link: '/accounts/rpc/wallet_getBalances', - }, - { - text: 'wallet_getCapabilities 🚧', - disabled: true, - link: '/accounts/rpc/wallet_getCapabilities', - }, - { - text: 'wallet_getCallsStatus 🚧', - disabled: true, - link: '/accounts/rpc/wallet_getCallsStatus', - }, - { - text: 'wallet_sendCalls 🚧', - disabled: true, - link: '/accounts/rpc/wallet_sendCalls', - }, - { - text: 'eth_sendTransaction 🚧', - disabled: true, - link: '/accounts/rpc/eth_sendTransaction', - }, - { - text: 'eth_sendTransactionSync 🚧', - disabled: true, - link: '/accounts/rpc/eth_sendTransactionSync', - }, - { - text: 'eth_fillTransaction', - link: '/accounts/rpc/eth_fillTransaction', - }, - { - text: 'personal_sign 🚧', - disabled: true, - link: '/accounts/rpc/personal_sign', - }, - ], - }, - ], - }, - '/learn': [ - { - text: 'Home', - link: '/learn', - }, - { - text: 'Partners', - link: '/learn/partners', - }, - { - text: 'Blog', - link: 'https://tempo.xyz/blog', - }, - { - text: 'Stablecoins', - items: [ - { - text: 'Overview', - link: '/learn/stablecoins', - }, - { - text: 'Remittances', - link: '/learn/use-cases/remittances', - }, - { - text: 'Global Payouts', - link: '/learn/use-cases/global-payouts', - }, - { - text: 'Payroll', - link: '/learn/use-cases/payroll', - }, - { - text: 'Embedded Finance', - link: '/learn/use-cases/embedded-finance', - }, - { - text: 'Tokenized Deposits', - link: '/learn/use-cases/tokenized-deposits', - }, - { - text: 'Microtransactions', - link: '/learn/use-cases/microtransactions', - }, - { - text: 'Agentic Commerce', - link: '/learn/use-cases/agentic-commerce', - }, - ], - }, - { - text: 'Tempo', - items: [ - { - text: 'Overview', - link: '/learn/tempo', - }, - { - text: 'Native Stablecoins', - link: '/learn/tempo/native-stablecoins', - }, - { - text: 'Receive Policies', - link: '/learn/tempo/receive-policies', - }, - { - text: 'Modern Transactions', - link: '/learn/tempo/modern-transactions', - }, - { - text: 'Performance', - link: '/learn/tempo/performance', - }, - { - text: 'Onchain FX', - link: '/learn/tempo/fx', - }, - { - text: 'Privacy', - link: '/learn/tempo/privacy', - }, - { - text: 'Agentic Payments', - link: '/learn/tempo/machine-payments', - }, - ], - }, - ], }, topNav: [ - { text: 'Learn', link: '/learn' }, { text: 'Docs', - link: '/', + link: '/docs', }, { text: 'Ecosystem', link: 'https://tempo.xyz/ecosystem' }, - { text: 'Blog', link: 'https://tempo.xyz/blog' }, { text: 'Wallet', link: 'https://wallet.tempo.xyz' }, ], redirects: [ { - source: '/documentation/protocol/:path*', - destination: '/protocol/:path*', + source: '/docs/documentation/protocol/:path*', + destination: '/docs/protocol/:path*', }, { - source: '/stablecoin-exchange/:path*', - destination: '/stablecoin-dex/:path*', + source: '/docs/stablecoin-exchange/:path*', + destination: '/docs/guide/stablecoin-dex/:path*', status: 301, }, { - source: '/quickstart/developer-tools', - destination: '/ecosystem', + source: '/docs/quickstart/developer-tools', + destination: '/docs/ecosystem', status: 301, }, { - source: '/guide/ai-support', - destination: '/guide/building-with-ai', - }, - { - source: '/guide/building-with-ai', - destination: '/guide/using-tempo-with-ai', - }, - { - source: '/guide', - destination: '/quickstart/integrate-tempo', - }, - { - source: '/quickstart', - destination: '/quickstart/integrate-tempo', - }, - { - source: '/protocol/zones/overview', - destination: '/protocol/zones', + source: '/docs/developer-tools', + destination: '/docs/ecosystem', status: 301, }, { - source: '/protocol/zones/privacy', - destination: '/protocol/zones/accounts', - status: 301, + source: '/docs/guide/ai-support', + destination: '/docs/guide/using-tempo-with-ai', }, { - source: '/protocol/blockspace', - destination: '/protocol/blockspace/overview', + source: '/docs/guide/building-with-ai', + destination: '/docs/guide/using-tempo-with-ai', }, { - source: '/protocol/tip20', - destination: '/protocol/tip20/overview', + source: '/docs/guide', + destination: '/docs/quickstart/integrate-tempo', }, { - source: '/protocol/tip20-rewards', - destination: '/protocol/tip20-rewards/overview', + source: '/docs/quickstart', + destination: '/docs/quickstart/integrate-tempo', }, { - source: '/protocol/tip403', - destination: '/protocol/tip403/overview', + source: '/docs/protocol/blockspace', + destination: '/docs/protocol/blockspace/overview', }, { - source: '/learn/use-cases', - destination: '/learn/use-cases/remittances', + source: '/docs/protocol/tip20', + destination: '/docs/protocol/tip20/overview', }, { - source: '/sdk/typescript/server', - destination: 'https://accounts.tempo.xyz/docs/server', - status: 301, - }, - { - source: '/sdk/typescript/server/handlers', - destination: 'https://accounts.tempo.xyz/docs/server', - status: 301, - }, - { - source: '/sdk/typescript/server/handler.compose', - destination: 'https://accounts.tempo.xyz/docs/server/handler.compose', - status: 301, - }, - { - source: '/sdk/typescript/server/handler.feePayer', - destination: 'https://accounts.tempo.xyz/docs/server/handler.relay', - status: 301, + source: '/docs/protocol/tip20-rewards', + destination: '/docs/protocol/tip20-rewards/overview', }, { - source: '/sdk/typescript/server/handler.keyManager', - destination: 'https://accounts.tempo.xyz/docs/server/handler.webAuthn', - status: 301, - }, - { - source: '/accounts', - destination: 'https://accounts.tempo.xyz/docs', - status: 301, - }, - { - source: '/accounts/:path*', - destination: 'https://accounts.tempo.xyz/docs/:path*', - status: 301, + source: '/docs/protocol/tip403', + destination: '/docs/protocol/tip403/overview', }, { - source: '/sdk/typescript/prool', - destination: '/sdk/typescript/prool/setup', - }, - { - source: '/wallet', - destination: '/cli', + source: '/docs/learn/partners', + destination: '/docs/partners', status: 301, }, { - source: '/wallet/reference', - destination: '/cli/wallet', + source: '/learn/partners', + destination: '/docs/partners', status: 301, }, { - source: '/wallet/:path*', - destination: '/cli/:path*', + source: '/docs/guide/using-tempo-with-ai/partners', + destination: '/docs/partners', status: 301, }, { - source: '/cli/reference', - destination: '/cli/wallet', + source: '/build/partners', + destination: '/docs/partners', status: 301, }, { - source: '/guide/use-accounts/fee-sponsorship', - destination: '/guide/payments/sponsor-user-fees', - status: 301, + source: '/docs/sdk/typescript/prool', + destination: '/docs/sdk/typescript/prool/setup', }, { - source: '/quickstart/tip20', - destination: '/protocol/tip20/overview', + source: '/docs/cli/reference', + destination: '/docs/cli/wallet', status: 301, }, { - source: '/protocol/exchange/pathUSD', - destination: '/protocol/exchange/quote-tokens#pathusd', + source: '/docs/quickstart/tip20', + destination: '/docs/protocol/tip20/overview', status: 301, }, { - source: '/protocol/zones/overview', - destination: '/protocol/zones', + source: '/docs/protocol/exchange/pathUSD', + destination: '/docs/protocol/exchange/quote-tokens#pathusd', status: 301, }, ],