From b42fb6d3d541e500ef3a9a40eefa11d22e41212f Mon Sep 17 00:00:00 2001 From: janniks Date: Tue, 14 Jan 2025 22:13:39 +0100 Subject: [PATCH 1/4] chore: partial --- .vscode/settings.json | 12 +- README.md | 14 + package-lock.json | 1360 ++++++++++++++++- package.json | 3 +- packages/connect-ui/src/providers.ts | 19 + packages/connect/package.json | 8 +- packages/connect/src/auth.ts | 86 +- packages/connect/src/bitcoin/index.ts | 2 + packages/connect/src/bitcoin/psbt.ts | 141 +- packages/connect/src/errors.ts | 43 + packages/connect/src/index.ts | 14 +- packages/connect/src/methods.ts | 218 +++ packages/connect/src/request.ts | 160 ++ packages/connect/src/signature/index.ts | 103 +- .../connect/src/signature/structuredData.ts | 116 +- packages/connect/src/types/network.ts | 2 +- packages/connect/src/types/provider.ts | 52 +- packages/connect/src/types/signature.ts | 2 +- packages/connect/src/types/typebox.ts | 201 +++ packages/connect/src/types/zod.ts | 0 packages/connect/src/ui.ts | 169 +- packages/connect/src/utils.ts | 60 +- packages/connect/tests/typebox.test.ts | 184 +++ packages/connect/tsconfig.json | 3 +- packages/connect/tsconfig.test.json | 8 + sip-030.md | 563 +++++++ 26 files changed, 3049 insertions(+), 494 deletions(-) create mode 100644 packages/connect/src/errors.ts create mode 100644 packages/connect/src/methods.ts create mode 100644 packages/connect/src/request.ts create mode 100644 packages/connect/src/types/typebox.ts create mode 100644 packages/connect/src/types/zod.ts create mode 100644 packages/connect/tests/typebox.test.ts create mode 100644 packages/connect/tsconfig.test.json create mode 100644 sip-030.md diff --git a/.vscode/settings.json b/.vscode/settings.json index 25fa6215..020e3902 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,13 @@ { - "typescript.tsdk": "node_modules/typescript/lib" + "typescript.tsdk": "node_modules/typescript/lib", + + // Jest -> Vitest: Run + "jestrunner.jestCommand": "npx vitest run", + + // Jest -> Vitest: Debug (Hack) + "jestrunner.jestPath": "node_modules/.bin/vitest", + "jestrunner.runOptions": ["--"], + "jestrunner.debugOptions": { + "args": ["run"] + } } diff --git a/README.md b/README.md index 91de42a4..ff98560c 100644 --- a/README.md +++ b/README.md @@ -59,3 +59,17 @@ Join our community and stay connected with the latest updates and discussions: - [Join our Discord community chat](https://stacks.chat/) to engage with other users, ask questions, and participate in discussions. - [Visit hiro.so](https://www.hiro.so/) for updates and subcribing to the mailing list. - Follow [Hiro on Twitter.](https://twitter.com/hirosystems) + +### MIGRATION TODO + +#### BREAKING + +- REMOVED BlockstackProvider, StacksProvider +- UPDATED StacksProvider to only have request +- ADDED requestRaw, and similar +- REMOVED shouldUsePopup + +#### CONTINUE + +- Make old things compatible with new things +- diff --git a/package-lock.json b/package-lock.json index b321d469..2f0d857c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,7 +33,8 @@ "react-dom": "^18.3.1", "tsup": "^8.3.5", "typedoc": "^0.26.10", - "typescript": "^5.3.2" + "typescript": "^5.3.2", + "vitest": "^2.1.8" } }, "node_modules/@alloc/quick-lru": { @@ -12057,6 +12058,92 @@ "node": ">=12" } }, + "node_modules/@vitest/expect": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.8.tgz", + "integrity": "sha512-8ytZ/fFHq2g4PJVAtDX57mayemKgDR6X3Oa2Foro+EygiOJHUXhCqBAAKQYYajZpFoIfvBCF1j6R6IYRSIUFuw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.8", + "@vitest/utils": "2.1.8", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.8.tgz", + "integrity": "sha512-9HiSZ9zpqNLKlbIDRWOnAWqgcA7xu+8YxXSekhr0Ykab7PAYFkhkwoqVArPOtJhPmYeE2YHgKZlj3CP36z2AJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.8.tgz", + "integrity": "sha512-17ub8vQstRnRlIU5k50bG+QOMLHRhYPAna5tw8tYbj+jzjcspnwnwtPtiOlkuKC4+ixDPTuLZiqiWWQ2PSXHVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.8", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.8.tgz", + "integrity": "sha512-20T7xRFbmnkfcmgVEz+z3AU/3b0cEzZOt/zmnvZEctg64/QZbSDJEVm9fLnnlSi74KibmRsO9/Qabi+t0vCRPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.8", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.8.tgz", + "integrity": "sha512-5swjf2q95gXeYPevtW0BLk6H8+bPlMb4Vw/9Em4hFxDcaOxS+e0LOX4yqNxoHzMR2akEB2xfpnWUzkZokmgWDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.8.tgz", + "integrity": "sha512-dwSoui6djdwbfFmIgbIjX2ZhIoG7Ex/+xpxyiEgIGzjliY8xGkcpITKTlp6B4MgtGkF2ilvm97cPM96XZaAgcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.8", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/@webassemblyjs/ast": { "version": "1.12.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", @@ -12921,6 +13008,16 @@ "util": "^0.12.5" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/ast-types": { "version": "0.16.1", "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.1.tgz", @@ -13905,6 +14002,23 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/chai": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.2.tgz", + "integrity": "sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/chalk": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", @@ -13947,6 +14061,16 @@ "dev": true, "license": "MIT" }, + "node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -16168,6 +16292,16 @@ } } }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/deep-equal": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz", @@ -18122,6 +18256,16 @@ "node": ">=6" } }, + "node_modules/expect-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.1.0.tgz", + "integrity": "sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/exponential-backoff": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.1.tgz", @@ -23242,6 +23386,13 @@ "loose-envify": "cli.js" } }, + "node_modules/loupe": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.2.tgz", + "integrity": "sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==", + "dev": true, + "license": "MIT" + }, "node_modules/lower-case": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", @@ -25790,6 +25941,16 @@ "dev": true, "license": "MIT" }, + "node_modules/pathval": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", + "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, "node_modules/peek-stream": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/peek-stream/-/peek-stream-1.1.3.tgz", @@ -28945,6 +29106,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -29288,6 +29456,13 @@ "dev": true, "license": "MIT" }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, "node_modules/stackframe": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", @@ -29305,6 +29480,13 @@ "node": ">= 0.8" } }, + "node_modules/std-env": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.8.0.tgz", + "integrity": "sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==", + "dev": true, + "license": "MIT" + }, "node_modules/stencil-tailwind-plugin": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/stencil-tailwind-plugin/-/stencil-tailwind-plugin-1.8.0.tgz", @@ -30500,6 +30682,13 @@ "dev": true, "license": "MIT" }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, "node_modules/tinyexec": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.1.tgz", @@ -30549,6 +30738,36 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/tinypool": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.2.tgz", + "integrity": "sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", @@ -31784,6 +32003,7 @@ "integrity": "sha512-ifW3Lb2sMdX+WU91s3R0FyQlAyLxOzCSCP37ujw0+r5POeHPwe6udWVIElKQq8gk3t7b8rkmvqC6IHBpCff4GQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.18.10", "postcss": "^8.4.27", @@ -31834,6 +32054,526 @@ } } }, + "node_modules/vite-node": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.8.tgz", + "integrity": "sha512-uPAwSr57kYjAUux+8E2j0q0Fxpn8M9VoyfGiRI8Kfktz9NcYMCenwY5RnZxnF1WTu3TGiYipirIzacLL3VVGFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite-node/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/es-module-lexer": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz", + "integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite-node/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vite-node/node_modules/vite": { + "version": "5.4.11", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz", + "integrity": "sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, "node_modules/vite/node_modules/@esbuild/android-arm": { "version": "0.18.20", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", @@ -31847,6 +32587,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": ">=12" } @@ -31864,6 +32605,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": ">=12" } @@ -31881,6 +32623,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": ">=12" } @@ -31898,6 +32641,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">=12" } @@ -31915,6 +32659,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">=12" } @@ -31932,6 +32677,7 @@ "os": [ "freebsd" ], + "peer": true, "engines": { "node": ">=12" } @@ -31949,6 +32695,7 @@ "os": [ "freebsd" ], + "peer": true, "engines": { "node": ">=12" } @@ -31966,6 +32713,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=12" } @@ -31983,6 +32731,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=12" } @@ -32000,6 +32749,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=12" } @@ -32017,6 +32767,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=12" } @@ -32034,6 +32785,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=12" } @@ -32051,6 +32803,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=12" } @@ -32068,6 +32821,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=12" } @@ -32085,6 +32839,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=12" } @@ -32102,6 +32857,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=12" } @@ -32119,6 +32875,7 @@ "os": [ "netbsd" ], + "peer": true, "engines": { "node": ">=12" } @@ -32136,6 +32893,7 @@ "os": [ "openbsd" ], + "peer": true, "engines": { "node": ">=12" } @@ -32153,6 +32911,7 @@ "os": [ "sunos" ], + "peer": true, "engines": { "node": ">=12" } @@ -32170,6 +32929,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">=12" } @@ -32187,6 +32947,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">=12" } @@ -32204,6 +32965,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">=12" } @@ -32215,6 +32977,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -32252,6 +33015,7 @@ "integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==", "dev": true, "license": "MIT", + "peer": true, "bin": { "rollup": "dist/bin/rollup" }, @@ -32263,6 +33027,571 @@ "fsevents": "~2.3.2" } }, + "node_modules/vitest": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.8.tgz", + "integrity": "sha512-1vBKTZskHw/aosXqQUlVWWlGUxSJR8YtiyZDJAFeW2kPAeX6S3Sool0mjspO+kXLuxVWlEDDowBAeqeAQefqLQ==", + "dev": true, + "dependencies": { + "@vitest/expect": "2.1.8", + "@vitest/mocker": "2.1.8", + "@vitest/pretty-format": "^2.1.8", + "@vitest/runner": "2.1.8", + "@vitest/snapshot": "2.1.8", + "@vitest/spy": "2.1.8", + "@vitest/utils": "2.1.8", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.8", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.8", + "@vitest/ui": "2.1.8", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@vitest/mocker": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.8.tgz", + "integrity": "sha512-7guJ/47I6uqfttp33mgo6ga5Gr1VnL58rcqYKyShoRK9ebu8T5Rs6HN3s1NABiBeVTdWNrwUMcHH54uXZBN4zA==", + "dev": true, + "dependencies": { + "@vitest/spy": "2.1.8", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vitest/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/vitest/node_modules/vite": { + "version": "5.4.11", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz", + "integrity": "sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==", + "dev": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, "node_modules/w3c-xmlserializer": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", @@ -32682,6 +34011,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wide-align": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", @@ -33099,12 +34445,12 @@ }, "packages/connect": { "name": "@stacks/connect", - "version": "7.8.0", + "version": "7.10.0", "license": "MIT", "dependencies": { "@stacks/auth": "^7.0.0", "@stacks/common": "^7.0.0", - "@stacks/connect-ui": "6.5.0", + "@stacks/connect-ui": "6.6.0", "@stacks/network": "^7.0.0", "@stacks/network-v6": "npm:@stacks/network@^6.16.0", "@stacks/profile": "^7.0.0", @@ -33129,15 +34475,15 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "storybook": "^7.6.0-alpha.5", - "vite": "^4.5.0" + "vitest": "^2.1.8" } }, "packages/connect-react": { "name": "@stacks/connect-react", - "version": "22.5.0", + "version": "22.6.1", "license": "MIT", "dependencies": { - "@stacks/connect": "7.8.0", + "@stacks/connect": "7.10.0", "jsontokens": "^4.0.1" }, "devDependencies": { @@ -33152,7 +34498,7 @@ }, "packages/connect-ui": { "name": "@stacks/connect-ui", - "version": "6.5.0", + "version": "6.6.0", "license": "MIT", "dependencies": { "@stencil/core": "^2.17.1" diff --git a/package.json b/package.json index 7be01531..a7dc6baf 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "react-dom": "^18.3.1", "tsup": "^8.3.5", "typedoc": "^0.26.10", - "typescript": "^5.3.2" + "typescript": "^5.3.2", + "vitest": "^2.1.8" } } diff --git a/packages/connect-ui/src/providers.ts b/packages/connect-ui/src/providers.ts index f2d9f27e..a454663b 100644 --- a/packages/connect-ui/src/providers.ts +++ b/packages/connect-ui/src/providers.ts @@ -1,5 +1,7 @@ // AUTO REGISTERED PROVIDERS +import { getSelectedProviderId } from './session'; + export interface WebBTCProvider { /** The global "path" of the provider (e.g. `"MyProvider"` if registered at `window.MyProvider`) */ id: string; @@ -55,6 +57,23 @@ export const getInstalledProviders = (defaultProviders: WebBTCProvider[] = []) = return registeredProviders.concat(additionalInstalledProviders); }; +/** + * Check if a wallet provider was previously selected via Connect. + * @returns `true` if a provider was selected, `false` otherwise. + */ +export const isProviderSelected = () => { + return !!getSelectedProviderId(); +}; + +/** + * Get the currently selected wallet provider. + * @returns The wallet provider object, or null if no provider is selected. + */ +export const getProvider = () => { + const providerId = getSelectedProviderId(); + return getProviderFromId(providerId); +}; + export const getProviderFromId = (id: string | undefined) => { return id?.split('.').reduce((acc, part) => acc?.[part], window); }; diff --git a/packages/connect/package.json b/packages/connect/package.json index f506548d..e2cb6550 100644 --- a/packages/connect/package.json +++ b/packages/connect/package.json @@ -4,11 +4,15 @@ "license": "MIT", "scripts": { "build": "concurrently 'tsup src/index.ts' 'npm run types'", + "dev": "tsc --project tsconfig.json --watch", "prepublishOnly": "npm run build", "typecheck": "tsc --project tsconfig.json --noEmit", "types": "tsc --project tsconfig.json --emitDeclarationOnly", "storybook": "storybook dev -p 6006", - "build-storybook": "storybook build" + "build-storybook": "storybook build", + "test": "vitest run", + "test:watch": "vitest", + "test:coverage": "vitest run --coverage" }, "dependencies": { "@stacks/auth": "^7.0.0", @@ -53,6 +57,6 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "storybook": "^7.6.0-alpha.5", - "vite": "^4.5.0" + "vitest": "^2.1.8" } } diff --git a/packages/connect/src/auth.ts b/packages/connect/src/auth.ts index a2037dc1..bb8f5282 100644 --- a/packages/connect/src/auth.ts +++ b/packages/connect/src/auth.ts @@ -1,15 +1,11 @@ -import { AppConfig, UserSession } from '@stacks/auth'; -import { decodeToken } from 'jsontokens'; -import type { AuthOptions, AuthResponsePayload, StacksProvider } from './types'; - -import { getStacksProvider } from './utils'; - +/** @deprecated Not used anymore. */ export const defaultAuthURL = 'https://app.blockstack.org'; if (typeof window !== 'undefined') { window.__CONNECT_VERSION__ = '__VERSION__'; // replaced via tsup esbuildOptions } +/** @deprecated Will be marked as internal going forward. */ export const isMobile = () => { const ua = navigator.userAgent; if (/android/i.test(ua)) { @@ -20,81 +16,3 @@ export const isMobile = () => { } return /windows phone/i.test(ua); }; - -/** - * mobile should not use a 'popup' type of window. - */ -export const shouldUsePopup = () => { - return !isMobile(); -}; - -export const getOrCreateUserSession = (userSession?: UserSession): UserSession => { - if (!userSession) { - const appConfig = new AppConfig(['store_write'], document.location.href); - userSession = new UserSession({ appConfig }); - } - return userSession; -}; - -export const authenticate = async ( - authOptions: AuthOptions, - provider: StacksProvider = getStacksProvider() -) => { - if (!provider) throw new Error('[Connect] No installed Stacks wallet found'); - - const { - redirectTo = '/', - manifestPath, - onFinish, - onCancel, - sendToSignIn = false, - userSession: _userSession, - appDetails, - } = authOptions; - const userSession = getOrCreateUserSession(_userSession); - if (userSession.isUserSignedIn()) { - userSession.signUserOut(); - } - const transitKey = userSession.generateAndStoreTransitKey(); - const authRequest = userSession.makeAuthRequest( - transitKey, - `${document.location.origin}${redirectTo}`, - `${document.location.origin}${manifestPath}`, - userSession.appConfig.scopes, - undefined, - undefined, - { - sendToSignIn, - appDetails, - connectVersion: '__VERSION__', // replaced via tsup esbuildOptions, - } - ); - - try { - const authResponse = await provider.authenticationRequest(authRequest); - await userSession.handlePendingSignIn(authResponse); - const token = decodeToken(authResponse); - const payload = token?.payload; - const authResponsePayload = payload as unknown as AuthResponsePayload; - onFinish?.({ - authResponse, - authResponsePayload, - userSession, - }); - } catch (error) { - console.error('[Connect] Error during auth request', error); - onCancel?.(); - } -}; - -// eslint-disable-next-line @typescript-eslint/require-await -export const getUserData = async (userSession?: UserSession) => { - userSession = getOrCreateUserSession(userSession); - if (userSession.isUserSignedIn()) { - return userSession.loadUserData(); - } - if (userSession.isSignInPending()) { - return userSession.handlePendingSignIn(); - } - return null; -}; diff --git a/packages/connect/src/bitcoin/index.ts b/packages/connect/src/bitcoin/index.ts index 76b841ed..662dad95 100644 --- a/packages/connect/src/bitcoin/index.ts +++ b/packages/connect/src/bitcoin/index.ts @@ -1 +1,3 @@ export * from './psbt'; + +// todo: add at least psbt before merge diff --git a/packages/connect/src/bitcoin/psbt.ts b/packages/connect/src/bitcoin/psbt.ts index 51dee332..7944e99b 100644 --- a/packages/connect/src/bitcoin/psbt.ts +++ b/packages/connect/src/bitcoin/psbt.ts @@ -1,79 +1,80 @@ -import { createUnsecuredToken, Json, TokenSigner } from 'jsontokens'; -import { getKeys, getUserSession, hasAppPrivateKey } from '../transactions'; -import { StacksProvider } from '../types'; -import { PsbtPayload, PsbtPopup, PsbtRequestOptions } from '../types/bitcoin'; -import { getStacksProvider, legacyNetworkFromConnectNetwork } from '../utils'; +// TODO +// import { createUnsecuredToken, Json, TokenSigner } from 'jsontokens'; +// import { getKeys, getUserSession, hasAppPrivateKey } from '../transactions'; +// import { StacksProvider } from '../types'; +// import { PsbtPayload, PsbtPopup, PsbtRequestOptions } from '../types/bitcoin'; +// import { getStacksProvider, legacyNetworkFromConnectNetwork } from '../utils'; -// eslint-disable-next-line @typescript-eslint/require-await -async function signPayload(payload: PsbtPayload, privateKey: string) { - const tokenSigner = new TokenSigner('ES256k', privateKey); - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion - return tokenSigner.signAsync({ ...payload } as any); -} +// // eslint-disable-next-line @typescript-eslint/require-await +// async function signPayload(payload: PsbtPayload, privateKey: string) { +// const tokenSigner = new TokenSigner('ES256k', privateKey); +// // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion +// return tokenSigner.signAsync({ ...payload } as any); +// } -export function getDefaultPsbtRequestOptions(options: PsbtRequestOptions) { - const network = legacyNetworkFromConnectNetwork(options.network); - const userSession = getUserSession(options.userSession); - const defaults: PsbtRequestOptions = { - ...options, - network, - userSession, - }; - return { - ...defaults, - }; -} +// export function getDefaultPsbtRequestOptions(options: PsbtRequestOptions) { +// const network = legacyNetworkFromConnectNetwork(options.network); +// const userSession = getUserSession(options.userSession); +// const defaults: PsbtRequestOptions = { +// ...options, +// network, +// userSession, +// }; +// return { +// ...defaults, +// }; +// } -async function openPsbtPopup({ token, options }: PsbtPopup, provider: StacksProvider) { - if (!provider) throw new Error('[Connect] No installed Stacks wallet found'); +// async function openPsbtPopup({ token, options }: PsbtPopup, provider: StacksProvider) { +// if (!provider) throw new Error('[Connect] No installed Stacks wallet found'); - try { - const psbtResponse = await provider.psbtRequest(token); - options.onFinish?.(psbtResponse); - } catch (error) { - console.error('[Connect] Error during psbt request', error); - options.onCancel?.(); - } -} +// try { +// const psbtResponse = await provider.psbtRequest(token); +// options.onFinish?.(psbtResponse); +// } catch (error) { +// console.error('[Connect] Error during psbt request', error); +// options.onCancel?.(); +// } +// } -// eslint-disable-next-line @typescript-eslint/require-await -export const makePsbtToken = async (options: PsbtRequestOptions) => { - const { allowedSighash, hex, signAtIndex, userSession, ..._options } = options; - if (hasAppPrivateKey(userSession)) { - const { privateKey, publicKey } = getKeys(userSession); +// // eslint-disable-next-line @typescript-eslint/require-await +// export const makePsbtToken = async (options: PsbtRequestOptions) => { +// const { allowedSighash, hex, signAtIndex, userSession, ..._options } = options; +// if (hasAppPrivateKey(userSession)) { +// const { privateKey, publicKey } = getKeys(userSession); - const payload: PsbtPayload = { - ..._options, - allowedSighash, - hex, - signAtIndex, - publicKey, - }; +// const payload: PsbtPayload = { +// ..._options, +// allowedSighash, +// hex, +// signAtIndex, +// publicKey, +// }; - return signPayload(payload, privateKey); - } - const payload = { ..._options }; - return createUnsecuredToken(payload as Json); -}; +// return signPayload(payload, privateKey); +// } +// const payload = { ..._options }; +// return createUnsecuredToken(payload as Json); +// }; -async function generateTokenAndOpenPopup( - options: T, - makeTokenFn: (options: T) => Promise, - provider: StacksProvider -) { - const token = await makeTokenFn({ - ...getDefaultPsbtRequestOptions(options), - ...options, - } as T); - return openPsbtPopup({ token, options }, provider); -} +// async function generateTokenAndOpenPopup( +// options: T, +// makeTokenFn: (options: T) => Promise, +// provider: StacksProvider +// ) { +// const token = await makeTokenFn({ +// ...getDefaultPsbtRequestOptions(options), +// ...options, +// } as T); +// return openPsbtPopup({ token, options }, provider); +// } -/** - * @experimental - */ -export function openPsbtRequestPopup( - options: PsbtRequestOptions, - provider: StacksProvider = getStacksProvider() -) { - return generateTokenAndOpenPopup(options, makePsbtToken, provider); -} +// /** +// * @experimental +// */ +// export function openPsbtRequestPopup( +// options: PsbtRequestOptions, +// provider: StacksProvider = getStacksProvider() +// ) { +// return generateTokenAndOpenPopup(options, makePsbtToken, provider); +// } diff --git a/packages/connect/src/errors.ts b/packages/connect/src/errors.ts new file mode 100644 index 00000000..17bdd943 --- /dev/null +++ b/packages/connect/src/errors.ts @@ -0,0 +1,43 @@ +export class JsonRpcError extends Error { + constructor( + message: string, + public code: number, + public data?: string, + public cause?: Error + ) { + super(data ? `${message} (${data})` : message); + this.name = 'JsonRpcError'; + this.cause = cause; + } +} + +export class ConnectCanceledError extends JsonRpcError { + constructor(message = 'User canceled provider selection') { + super(message, ConnectErrorCode.Canceled); + this.name = 'ConnectCanceledError'; + } +} + +export enum ConnectErrorCode { + Canceled = 32_001, +} + +export enum JsonRpcErrorCode { + /** Invalid JSON received by server while parsing */ + ParseError = -32_700, + + /** Invalid Request object */ + InvalidRequest = -32_600, + + /** Method not found/available */ + MethodNotFound = -32_601, + + /** Invalid method params */ + InvalidParams = -32_602, + + /** Internal JSON-RPC error */ + InternalError = -32_603, + + /** Implementation-defined server errors (-32_000 to -32_099) */ + ServerError = -32_000, +} diff --git a/packages/connect/src/index.ts b/packages/connect/src/index.ts index 6edb16e1..99df795c 100644 --- a/packages/connect/src/index.ts +++ b/packages/connect/src/index.ts @@ -1,19 +1,27 @@ -export * from './auth'; +export * from './auth'; // file may be renamed in the future + export * from './bitcoin'; export * from './transactions'; export * from './signature'; export * from './signature/structuredData'; export * from './profile'; export * from './types'; -export * from './utils'; export * from './ui'; export * from './providers'; +export { request, requestRaw } from './request'; + +export { getStacksProvider, isStacksWalletInstalled } from './utils'; + +// typebox +// only export the outermost typebox schemas +export { ClarityValueTypeBoxSchema, PostConditionTypeBoxSchema } from './types/typebox'; + // re-exports -export * from '@stacks/auth'; export { clearSelectedProviderId, getSelectedProviderId, setSelectedProviderId, + isProviderSelected, } from '@stacks/connect-ui'; diff --git a/packages/connect/src/methods.ts b/packages/connect/src/methods.ts new file mode 100644 index 00000000..0f222ef5 --- /dev/null +++ b/packages/connect/src/methods.ts @@ -0,0 +1,218 @@ +import type { + AddressString, + ClarityValue, + ContractIdString, + PostCondition, + PostConditionModeName, + TupleCV, +} from '@stacks/transactions'; + +// Re-export types from Stacks.js +export type { + AddressString, + AssetString, + ClarityValue, + ContractIdString, + FungibleComparator, + FungiblePostCondition, + NonFungibleComparator, + NonFungiblePostCondition, + PostCondition, + PostConditionModeName, + StxPostCondition, +} from '@stacks/transactions'; + +// TYPES + +export type NetworkString = 'mainnet' | 'testnet' | 'regtest' | 'devnet' | string; + +export type PrincipalString = AddressString | ContractIdString; + +export type Integer = number | bigint | string; + +export interface AddressEntry { + address: string; + publicKey: string; +} + +export interface AccountEntry extends AddressEntry { + gaiaHubUrl: string; + gaiaAppKey: string; +} + +// PARAMS + +interface CommonTxParams { + /** + * The recommended address to use for the method. + * + * ⚠︎ Warning: Wallets may not implement this for privacy reasons. + * */ + address?: AddressString; + + network?: NetworkString; + + fee?: Integer; + nonce?: Integer; + + sponsored?: boolean; + + postConditions?: PostCondition[]; + postConditionMode?: PostConditionModeName; +} + +export interface TransferStxParams + extends Omit { + recipient: string; + amount: Integer; + memo?: string; +} + +export interface TransferFungibleParams extends CommonTxParams { + recipient: string; + asset: string; + amount: Integer; +} + +export interface TransferNonFungibleParams extends CommonTxParams { + recipient: string; + asset: string; + assetId: ClarityValue; +} + +export interface CallContractParams extends CommonTxParams { + contract: ContractIdString; + functionName: string; + functionArgs?: ClarityValue[]; +} + +export interface DeployContractParams extends CommonTxParams { + name: string; + clarityCode: string; + clarityVersion?: number; +} + +export interface SignTransactionParams { + transaction: string; + broadcast?: boolean; // todo: check before merging +} + +export interface SignMessageParams { + message: string; +} + +export interface SignStructuredMessageParams { + message: ClarityValue; + domain: TupleCV; +} + +export interface GetAddressesParams { + network?: NetworkString; +} + +export interface GetAccountsParams { + network?: NetworkString; +} + +export interface UpdateProfileParams { + profile: Record; +} + +// RESULTS + +export interface TransactionResult { + txid?: string; + transaction?: string; // todo: check before merging +} + +export interface SignTransactionResult { + txid?: string; + transaction: string; +} + +export interface SignMessageResult { + signature: string; + publicKey: string; +} + +export interface GetAddressesResult { + addresses: AddressEntry[]; +} + +export interface GetAccountsResult { + accounts: AccountEntry[]; +} + +export interface UpdateProfileResult { + profile: Record; +} + +// ERROR RESPONSES + +// todo: add error responses + +// JSON RPC METHODS + +export type Methods = keyof StxMethods; + +export type MethodParams = StxMethods[M]['params']; + +export type MethodResult = StxMethods[M]['result']; + +export type JsonRpcResponse = { + jsonrpc: '2.0'; + id: number; + result: MethodResult; + // todo: add error +}; + +export type StxMethods = { + stx_transferStx: { + params: TransferStxParams; + result: TransactionResult; + }; + stx_transferSip10Ft: { + params: TransferFungibleParams; + result: TransactionResult; + }; + stx_transferSip10Nft: { + params: TransferNonFungibleParams; + result: TransactionResult; + }; + stx_callContract: { + params: CallContractParams; + result: TransactionResult; + }; + stx_deployContract: { + params: DeployContractParams; + result: TransactionResult; + }; + stx_signTransaction: { + params: SignTransactionParams; + result: SignTransactionResult; + }; + stx_signMessage: { + params: SignMessageParams; + result: SignMessageResult; + }; + stx_signStructuredMessage: { + params: SignStructuredMessageParams; + result: SignMessageResult; + }; + stx_getAddresses: { + params: GetAddressesParams; + result: GetAddressesResult; + }; + stx_getAccounts: { + params: GetAccountsParams; + result: GetAccountsResult; + }; + stx_updateProfile: { + params: UpdateProfileParams; + result: UpdateProfileResult; + }; +}; + +export type GlobalMethods = { + getAddresses: StxMethods['stx_getAddresses']; // todo: might differ later +}; diff --git a/packages/connect/src/request.ts b/packages/connect/src/request.ts new file mode 100644 index 00000000..d07519b9 --- /dev/null +++ b/packages/connect/src/request.ts @@ -0,0 +1,160 @@ +import { getProvider, WebBTCProvider, getInstalledProviders } from '@stacks/connect-ui'; +import { defineCustomElements } from '@stacks/connect-ui/loader'; +import { StacksProvider } from './types'; +import { DEFAULT_PROVIDERS } from './providers'; +import { ConnectCanceledError } from './errors'; +import { Methods, MethodParams, MethodResult } from './methods'; + +export interface ConnectRequestOptions { + defaultProviders?: WebBTCProvider[]; + provider?: StacksProvider; + persistSelection?: boolean; + forceSelection?: boolean; + + // todo: maybe add callbacks, if set use them instead of throwing errors +} + +export async function requestRaw( + provider: StacksProvider, + method: M, + params?: MethodParams +): Promise> { + const response = await provider.request(method, params); + if (response.error) { + // todo: add typed error handling (before merge) + throw new Error(response.error.message); + } + return response.result; +} + +export async function request( + method: M, + params?: MethodParams +): Promise>; +export async function request( + options: ConnectRequestOptions, + method: M, + params?: MethodParams +): Promise>; +export async function request( + ...args: + | [method: M, params?: MethodParams] + | [options: ConnectRequestOptions, method: M, params?: MethodParams] +): Promise> { + const { options, method, params } = requestArgs(args); + + const opts = Object.assign( + { + defaultProviders: DEFAULT_PROVIDERS, + persistSelection: true, + forceSelection: false, + provider: getProvider(), + }, + options + ); + + // WITHOUT UI + if (opts.provider && !opts.forceSelection) return requestRaw(opts.provider, method, params); + + // WITH UI + if (typeof window === 'undefined') return; // todo: throw error + + void defineCustomElements(window); + + const defaultProviders = options?.defaultProviders ?? DEFAULT_PROVIDERS; + const installedProviders = getInstalledProviders(defaultProviders); + + return new Promise((resolve, reject) => { + const element = document.createElement('connect-modal'); + element.defaultProviders = defaultProviders; + element.installedProviders = installedProviders; + element.persistSelection = opts.persistSelection; + + const originalOverflow = document.body.style.overflow; + document.body.style.overflow = 'hidden'; + + const closeModal = () => { + element.remove(); + document.body.style.overflow = originalOverflow; + }; + + element.callback = (selectedProvider: StacksProvider | undefined) => { + closeModal(); + resolve(requestRaw(selectedProvider, method, params)); + }; + + element.cancelCallback = () => { + closeModal(); + reject(new ConnectCanceledError()); + }; + + document.body.appendChild(element); + + const handleEsc = (ev: KeyboardEvent) => { + if (ev.key === 'Escape') { + document.removeEventListener('keydown', handleEsc); + element.remove(); + reject(new ConnectCanceledError()); + } + }; + document.addEventListener('keydown', handleEsc); + }); +} + +/** @internal */ +function requestArgs( + args: + | [method: M, params?: MethodParams] + | [options: ConnectRequestOptions, method: M, params?: MethodParams] +): { + method: M; + params?: MethodParams; + options?: ConnectRequestOptions; +} { + if (typeof args[0] === 'string') return { method: args[0], params: args[1] as MethodParams }; + return { options: args[0], method: args[1] as M, params: args[2] }; +} + +// /** @internal */ +// export function requestOpenLegacy( +// provider: StacksProvider, +// method: M, +// params: MethodParams, +// hooks: { +// onFinish: (response: JsonRpcResponse) => void; +// onCancel: () => void; +// } +// ) { +// if (!provider) throw new Error('[Connect] No installed Stacks wallet found'); +// provider.request(method, params).then(hooks.onFinish).catch(hooks.onCancel); +// } + +/** + * **Note:** Higher order function! + * @internal Legacy non-UI request. + */ +export function requestRawLegacy< + M extends Methods, + O extends { + onCancel?: () => void; + onFinish?: (response: R) => void; + }, + R, +>( + method: M, + mapOptions: (options: O) => MethodParams, + mapResponse: (response: MethodResult) => R +) { + return (options: O, provider?: StacksProvider) => { + if (!provider) throw new Error('[Connect] No installed Stacks wallet found'); + + const params = mapOptions(options); + + void requestRaw(provider, method, params) + .then(response => { + const r = mapResponse(response); + options.onFinish?.(r); + }) + .catch(options.onCancel); + }; +} diff --git a/packages/connect/src/signature/index.ts b/packages/connect/src/signature/index.ts index 09c3e6db..b4b25a4f 100644 --- a/packages/connect/src/signature/index.ts +++ b/packages/connect/src/signature/index.ts @@ -1,99 +1,36 @@ -import { ChainId } from '@stacks/network'; -import { createUnsecuredToken, TokenSigner } from 'jsontokens'; -import { getKeys, getUserSession, hasAppPrivateKey } from '../transactions'; +import { MethodParams, MethodResult } from '../methods'; +import { requestRawLegacy } from '../request'; import { StacksProvider } from '../types'; -import { - CommonSignatureRequestOptions, - SignatureOptions, - SignaturePayload, - SignaturePopup, - SignatureRequestOptions, -} from '../types/signature'; -import { getStacksProvider, legacyNetworkFromConnectNetwork } from '../utils'; +import { CommonSignatureRequestOptions, SignatureRequestOptions } from '../types/signature'; +import { getStacksProvider } from '../utils'; -function getStxAddress(options: CommonSignatureRequestOptions) { - const { userSession, network: _network } = options; - - if (!userSession || !_network) return undefined; - const stxAddresses = userSession?.loadUserData().profile?.stxAddress; - const chainIdToKey = { - [ChainId.Mainnet]: 'mainnet', - [ChainId.Testnet]: 'testnet', - }; - const network = legacyNetworkFromConnectNetwork(_network); - const address: string | undefined = stxAddresses?.[chainIdToKey[network.chainId]]; - return address; -} - -// eslint-disable-next-line @typescript-eslint/require-await -async function signPayload(payload: SignaturePayload, privateKey: string) { - const tokenSigner = new TokenSigner('ES256k', privateKey); - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion - return tokenSigner.signAsync({ ...payload } as any); -} - -export function getDefaultSignatureRequestOptions(options: CommonSignatureRequestOptions) { - const network = legacyNetworkFromConnectNetwork(options.network); - const userSession = getUserSession(options.userSession); - const defaults: CommonSignatureRequestOptions = { - ...options, - network, - userSession, - }; - return { - stxAddress: getStxAddress(defaults), - ...defaults, - }; -} - -async function openSignaturePopup({ token, options }: SignaturePopup, provider: StacksProvider) { - try { - const signatureResponse = await provider.signatureRequest(token); - options.onFinish?.(signatureResponse); - } catch (error) { - console.error('[Connect] Error during signature request', error); - options.onCancel?.(); - } -} +/** @deprecated No-op. Tokens are not needed for latest RPC endpoints. */ +export function getDefaultSignatureRequestOptions(_options: CommonSignatureRequestOptions) {} export interface SignatureRequestPayload { message: string; } -// eslint-disable-next-line @typescript-eslint/require-await -export const signMessage = async (options: SignatureRequestOptions) => { - const { userSession, ..._options } = options; - if (hasAppPrivateKey(userSession)) { - const { privateKey, publicKey } = getKeys(userSession); +/** @deprecated No-op. Tokens are not needed for latest RPC endpoints. */ +export const signMessage = async (_options: SignatureRequestOptions) => {}; - const payload: SignaturePayload = { - ..._options, - publicKey, - }; +const METHOD = 'stx_signMessage' as const; - return signPayload(payload, privateKey); - } - const payload = { ..._options }; - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion - return createUnsecuredToken(payload as any); -}; +/** @internal */ +export const LEGACY_SIGN_MESSAGE_OPTIONS_MAP = ( + options: SignatureRequestOptions +): MethodParams => options; -async function generateTokenAndOpenPopup( - options: T, - makeTokenFn: (options: T) => Promise, - provider: StacksProvider -) { - const token = await makeTokenFn({ - ...getDefaultSignatureRequestOptions(options), - ...options, - } as T); - return openSignaturePopup({ token, options }, provider); -} +/** @internal */ +export const LEGACY_SIGN_MESSAGE_RESPONSE_MAP = (response: MethodResult) => response; export function openSignatureRequestPopup( options: SignatureRequestOptions, provider: StacksProvider = getStacksProvider() ) { - if (!provider) throw new Error('[Connect] No installed Stacks wallet found'); - return generateTokenAndOpenPopup(options, signMessage, provider); + requestRawLegacy( + METHOD, + LEGACY_SIGN_MESSAGE_OPTIONS_MAP, + LEGACY_SIGN_MESSAGE_RESPONSE_MAP + )(options, provider); } diff --git a/packages/connect/src/signature/structuredData.ts b/packages/connect/src/signature/structuredData.ts index f661cc1d..4b7a7f29 100644 --- a/packages/connect/src/signature/structuredData.ts +++ b/packages/connect/src/signature/structuredData.ts @@ -1,94 +1,42 @@ -import { bytesToHex } from '@stacks/common'; -import { - serializeCV as legacySerializeCV, - ClarityValue as LegacyClarityValue, - TupleCV as LegacyTupleCV, -} from '@stacks/transactions-v6'; -import { serializeCV } from '@stacks/transactions'; -import { createUnsecuredToken, Json, TokenSigner } from 'jsontokens'; -import { getDefaultSignatureRequestOptions } from '.'; -import { getKeys, hasAppPrivateKey } from '../transactions'; -import { - StructuredDataSignatureOptions, - StructuredDataSignaturePayload, - StructuredDataSignaturePopup, - StructuredDataSignatureRequestOptions, -} from '../types/structuredDataSignature'; -import { getStacksProvider } from '../utils'; +import { TupleCV } from '@stacks/transactions'; +import { ClarityType as LegacyClarityType } from '@stacks/transactions-v6'; +import { MethodParams, MethodResult } from '../methods'; +import { requestRawLegacy } from '../request'; import { StacksProvider } from '../types'; +import { StructuredDataSignatureRequestOptions } from '../types/structuredDataSignature'; +import { getStacksProvider, legacyCVToCV } from '../utils'; -async function generateTokenAndOpenPopup( - options: T, - makeTokenFn: (options: T) => Promise, - provider: StacksProvider -) { - const token = await makeTokenFn({ - ...getDefaultSignatureRequestOptions(options), - ...options, - } as T); - return openStructuredDataSignaturePopup({ token, options }, provider); -} - -function parseUnserializableBigIntValues(payload: StructuredDataSignaturePayload) { - const { message, domain } = payload; - - if (typeof message.type === 'string' && typeof domain.type === 'string') { - // new readable types - return { - ...payload, - message: serializeCV(message), - domain: serializeCV(domain), - } as Json; - } - - // legacy types - return { - ...payload, - message: bytesToHex(legacySerializeCV(message as LegacyClarityValue)), - domain: bytesToHex(legacySerializeCV(domain as LegacyTupleCV)), - } as Json; -} - -// eslint-disable-next-line @typescript-eslint/require-await -async function signPayload(payload: StructuredDataSignaturePayload, privateKey: string) { - const tokenSigner = new TokenSigner('ES256k', privateKey); - return tokenSigner.signAsync(parseUnserializableBigIntValues(payload)); -} +/** @deprecated No-op. Tokens are not needed for latest RPC endpoints. */ +export async function signStructuredMessage(_options: StructuredDataSignatureRequestOptions) {} -// eslint-disable-next-line @typescript-eslint/require-await -export async function signStructuredMessage(options: StructuredDataSignatureRequestOptions) { - const { userSession, ..._options } = options; - if (hasAppPrivateKey(userSession)) { - const { privateKey, publicKey } = getKeys(userSession); - const payload: StructuredDataSignaturePayload = { - ..._options, - publicKey, - }; - return signPayload(payload, privateKey); - } - return createUnsecuredToken( - parseUnserializableBigIntValues(options as StructuredDataSignaturePayload) - ); -} +const METHOD = 'stx_signStructuredMessage' as const; -async function openStructuredDataSignaturePopup( - { token, options }: StructuredDataSignaturePopup, - provider: StacksProvider -) { - try { - const signatureResponse = await provider.structuredDataSignatureRequest(token); +/** @internal */ +export const LEGACY_SIGN_STRUCTURED_MESSAGE_OPTIONS_MAP = ( + options: StructuredDataSignatureRequestOptions +): MethodParams => ({ + // todo: also make sure that cvs don't have bigint unserializable values + message: legacyCVToCV(options.message), + domain: legacyCVToCV(options.domain) as TupleCV, // safe cast, because of below check +}); - options.onFinish?.(signatureResponse); - } catch (error) { - console.error('[Connect] Error during signature request', error); - options.onCancel?.(); - } -} +/** @internal */ +export const LEGACY_SIGN_STRUCTURED_MESSAGE_RESPONSE_MAP = ( + response: MethodResult +) => response; +/** Compatible interface with previous Connect `openStructuredDataSignatureRequestPopup` version, but using new SIP-030 RPC method. */ export function openStructuredDataSignatureRequestPopup( options: StructuredDataSignatureRequestOptions, provider: StacksProvider = getStacksProvider() -) { - if (!provider) throw new Error('[Connect] No installed Stacks wallet found'); - return generateTokenAndOpenPopup(options, signStructuredMessage, provider); +): void { + if (options.domain.type !== LegacyClarityType.Tuple) { + throw new Error('Domain must be a tuple'); // check, ensures domain is a tuple + } + + requestRawLegacy( + METHOD, + LEGACY_SIGN_STRUCTURED_MESSAGE_OPTIONS_MAP, + LEGACY_SIGN_STRUCTURED_MESSAGE_RESPONSE_MAP + )(options, provider); } diff --git a/packages/connect/src/types/network.ts b/packages/connect/src/types/network.ts index e0447be4..8bbe058a 100644 --- a/packages/connect/src/types/network.ts +++ b/packages/connect/src/types/network.ts @@ -2,7 +2,7 @@ import { StacksNetwork } from '@stacks/network'; import { StacksNetwork as LegacyNetwork, StacksNetworkName } from '@stacks/network-v6'; /** - * ⚠️ Warning: The new Stacks.js v7 network type is still experimental. + * ⚠︎ Warning: The new Stacks.js v7 network type is still experimental. */ export type ConnectNetwork = | StacksNetworkName diff --git a/packages/connect/src/types/provider.ts b/packages/connect/src/types/provider.ts index 25e34053..2017547c 100644 --- a/packages/connect/src/types/provider.ts +++ b/packages/connect/src/types/provider.ts @@ -1,53 +1,5 @@ -import { PublicProfile } from '@stacks/profile'; - -import { PsbtData } from './bitcoin'; -import { SignatureData } from './signature'; -import { FinishedTxPayload, SponsoredFinishedTxPayload } from './transactions'; +import { JsonRpcResponse, MethodParams, Methods } from '../methods'; export interface StacksProvider { - /** @deprecated */ - getURL: () => Promise; - /** - * Make a transaction request - * - * @param payload - a JSON web token representing a transaction request - */ - transactionRequest(payload: string): Promise; - /** - * Make an authentication request - * - * @param payload - a JSON web token representing an auth request - * - * @returns an authResponse string in the form of a JSON web token - */ - authenticationRequest(payload: string): Promise; - signatureRequest(payload: string): Promise; - structuredDataSignatureRequest(payload: string): Promise; - /** - * @experimental - */ - psbtRequest(payload: string): Promise; - profileUpdateRequest(payload: string): Promise; - request(method: string, params?: any[]): Promise>; - getProductInfo: - | undefined - | (() => { - version: string; - name: string; - meta?: { - tag?: string; - commit?: string; - [key: string]: any; - }; - [key: string]: any; - }); -} - -export type BlockstackProvider = StacksProvider; - -declare global { - interface Window { - BlockstackProvider?: BlockstackProvider; - StacksProvider?: StacksProvider; - } + request(method: M, params?: MethodParams): Promise>; } diff --git a/packages/connect/src/types/signature.ts b/packages/connect/src/types/signature.ts index b44b102f..c71a9a17 100644 --- a/packages/connect/src/types/signature.ts +++ b/packages/connect/src/types/signature.ts @@ -16,7 +16,7 @@ export interface CommonSignatureRequestOptions { } export interface SignatureRequestOptions extends CommonSignatureRequestOptions { - message: string; + message: string; // todo: check before merge if we only sign strings or also clarity values. } export interface SignatureOptions { diff --git a/packages/connect/src/types/typebox.ts b/packages/connect/src/types/typebox.ts new file mode 100644 index 00000000..a5ae18ce --- /dev/null +++ b/packages/connect/src/types/typebox.ts @@ -0,0 +1,201 @@ +import { Type } from '@sinclair/typebox'; + +// SUB-TYPES + +const IntegerTypeBoxSchema = Type.Union([Type.Number(), Type.BigInt(), Type.String()]); + +const HexTypeBoxSchema = Type.RegEx(/^(?:0x)?[A-Fa-f0-9]+$/); + +const AddressNameTypeBoxSchema = Type.RegEx(/^[A-Za-z0-9]+$/); + +const ContractNameTypeBoxSchema = Type.RegEx(/^[A-Za-z0-9]+\.[A-Za-z0-9-]+$/); + +const PostConditionAddressTypeBoxSchema = Type.Union([ + Type.Literal('origin'), + AddressNameTypeBoxSchema, + ContractNameTypeBoxSchema, +]); + +const PostConditionAssetTypeBoxSchema = Type.RegEx(/^[A-Z0-9]+\.[A-Za-z0-9]+::[A-Za-z0-9]+$/); + +const ConditionTypeBoxSchema = Type.Union([ + Type.Literal('eq'), + Type.Literal('gt'), + Type.Literal('gte'), + Type.Literal('lt'), + Type.Literal('lte'), +]); + +// CLARITY VALUES + +const UIntTypeBoxSchema = Type.Object( + { + type: Type.Literal('uint'), + value: IntegerTypeBoxSchema, + }, + { $id: 'UInt' } +); + +const IntTypeBoxSchema = Type.Object( + { + type: Type.Literal('int'), + value: IntegerTypeBoxSchema, + }, + { $id: 'Int' } +); + +const BufferTypeBoxSchema = Type.Object( + { + type: Type.Literal('buffer'), + value: HexTypeBoxSchema, + }, + { $id: 'Buffer' } +); + +const TrueTypeBoxSchema = Type.Object( + { + type: Type.Literal('true'), + }, + { $id: 'True' } +); + +const FalseTypeBoxSchema = Type.Object( + { + type: Type.Literal('false'), + }, + { $id: 'False' } +); + +const BooleanTypeBoxSchema = Type.Union([TrueTypeBoxSchema, FalseTypeBoxSchema], { + $id: 'Boolean', +}); + +const StandardPrincipalTypeBoxSchema = Type.Object( + { + type: Type.Literal('address'), + value: AddressNameTypeBoxSchema, + }, + { $id: 'StandardPrincipal' } +); + +const ContractPrincipalTypeBoxSchema = Type.Object( + { + type: Type.Literal('contract'), + value: ContractNameTypeBoxSchema, + }, + { $id: 'ContractPrincipal' } +); + +const StringAsciiTypeBoxSchema = Type.Object( + { + type: Type.Literal('ascii'), + value: Type.String(), + }, + { $id: 'StringAscii' } +); + +const StringUtf8TypeBoxSchema = Type.Object( + { + type: Type.Literal('utf8'), + value: Type.String(), + }, + { $id: 'StringUtf8' } +); + +const NoneTypeBoxSchema = Type.Object( + { + type: Type.Literal('none'), + }, + { $id: 'None' } +); + +export const ClarityValueTypeBoxSchema = Type.Recursive(self => + Type.Union( + [ + UIntTypeBoxSchema, + IntTypeBoxSchema, + BufferTypeBoxSchema, + BooleanTypeBoxSchema, + StandardPrincipalTypeBoxSchema, + ContractPrincipalTypeBoxSchema, + StringAsciiTypeBoxSchema, + StringUtf8TypeBoxSchema, + NoneTypeBoxSchema, + Type.Object( + { + type: Type.Literal('some'), + value: self, + }, + { $id: 'Some' } + ), + Type.Object( + { + type: Type.Literal('tuple'), + value: Type.Record(Type.String(), self), + }, + { $id: 'Tuple' } + ), + Type.Object( + { + type: Type.Literal('list'), + value: Type.Array(self), + }, + { $id: 'List' } + ), + Type.Union( + [ + Type.Object({ + type: Type.Literal('ok'), + value: self, + }), + Type.Object({ + type: Type.Literal('err'), + value: self, + }), + ], + { $id: 'Response' } + ), + ], + { $id: 'ClarityValue' } + ) +); + +// POST-CONDITIONS + +const StxPostConditionTypeBoxSchema = Type.Object( + { + type: Type.Literal('stx-postcondition'), + address: PostConditionAddressTypeBoxSchema, + condition: ConditionTypeBoxSchema, + amount: IntegerTypeBoxSchema, + }, + { $id: 'StxPostCondition' } +); + +const FungiblePostConditionTypeBoxSchema = Type.Object( + { + type: Type.Literal('ft-postcondition'), + address: PostConditionAddressTypeBoxSchema, + condition: ConditionTypeBoxSchema, + amount: IntegerTypeBoxSchema, + asset: PostConditionAssetTypeBoxSchema, + }, + { $id: 'FungiblePostCondition' } +); + +const NonFungiblePostConditionTypeBoxSchema = Type.Object( + { + type: Type.Literal('nft-postcondition'), + address: PostConditionAddressTypeBoxSchema, + condition: Type.Union([Type.Literal('sent'), Type.Literal('not-sent')]), + asset: PostConditionAssetTypeBoxSchema, + assetId: ClarityValueTypeBoxSchema, + }, + { $id: 'NonFungiblePostCondition' } +); + +export const PostConditionTypeBoxSchema = Type.Union([ + StxPostConditionTypeBoxSchema, + FungiblePostConditionTypeBoxSchema, + NonFungiblePostConditionTypeBoxSchema, +]); diff --git a/packages/connect/src/types/zod.ts b/packages/connect/src/types/zod.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/connect/src/ui.ts b/packages/connect/src/ui.ts index a19a781f..6b6ff7a7 100644 --- a/packages/connect/src/ui.ts +++ b/packages/connect/src/ui.ts @@ -1,122 +1,89 @@ +import { clearSelectedProviderId } from '@stacks/connect-ui'; +import { ConnectRequestOptions, request } from './request'; +import { Methods, MethodParams, MethodResult } from './methods'; +import { StacksProvider } from './types'; import { - WebBTCProvider, - clearSelectedProviderId, - getInstalledProviders, - getSelectedProviderId, -} from '@stacks/connect-ui'; -import { defineCustomElements } from '@stacks/connect-ui/loader'; -import { authenticate } from './auth'; -import { openPsbtRequestPopup } from './bitcoin'; -import { openProfileUpdateRequestPopup } from './profile'; -import { openSignatureRequestPopup } from './signature'; -import { openStructuredDataSignatureRequestPopup } from './signature/structuredData'; -import { - openContractCall, - openContractDeploy, - openSTXTransfer, - openSignTransaction, -} from './transactions'; -import { - ContractCallOptions, - ContractDeployOptions, - ProfileUpdateRequestOptions, - PsbtRequestOptions, - STXTransferOptions, - SignatureRequestOptions, - StacksProvider, - StructuredDataSignatureRequestOptions, - TransactionOptions, -} from './types'; -import type { AuthOptions } from './types/auth'; -import { getStacksProvider } from './utils'; -import { DEFAULT_PROVIDERS } from './providers'; - -export type ActionOptions = ( - | AuthOptions - | STXTransferOptions - | ContractCallOptions - | ContractDeployOptions - | TransactionOptions - | PsbtRequestOptions - | ProfileUpdateRequestOptions - | SignatureRequestOptions - | StructuredDataSignatureRequestOptions -) & { - defaultProviders?: WebBTCProvider[]; -}; + LEGACY_SIGN_STRUCTURED_MESSAGE_OPTIONS_MAP, + LEGACY_SIGN_STRUCTURED_MESSAGE_RESPONSE_MAP, +} from './signature/structuredData'; +import { LEGACY_SIGN_MESSAGE_OPTIONS_MAP, LEGACY_SIGN_MESSAGE_RESPONSE_MAP } from './signature'; + +// /** @internal */ +// function requestShowLegacy( +// method: M, +// options: ConnectRequestOptions = { +// forceSelection: true, +// } +// ) { +// return (params?: MethodParams): Promise> => request(options, method, params); +// } -/** Helper higher-order function for creating connect methods that allow for wallet selection */ -function wrapConnectCall( - action: (options: O, provider?: StacksProvider) => any, - persistSelection = true +/** + * **Note:** Higher order function! + * @internal Legacy UI request. + */ +function requestLegacy< + M extends Methods, + O extends { + onCancel?: () => void; + onFinish?: (response: R) => void; + }, + R, +>( + method: M, + mapOptions: (options: O) => MethodParams, + mapResponse: (response: MethodResult) => R, + uiOptions: ConnectRequestOptions = { + forceSelection: true, + } ) { - return function wrapped(options: O, provider?: StacksProvider) { - if (provider) return action(options, provider); // if a provider is passed, use it - - const selectedId = getSelectedProviderId(); - const selectedProvider = getStacksProvider(); - if (selectedId && selectedProvider) return action(options, selectedProvider); // if a provider is selected, use it - - if (typeof window === 'undefined') return; - void defineCustomElements(window); - - const defaultProviders = options?.defaultProviders ?? DEFAULT_PROVIDERS; - const installedProviders = getInstalledProviders(defaultProviders); - - const element = document.createElement('connect-modal'); - element.defaultProviders = defaultProviders; - element.installedProviders = installedProviders; - element.persistSelection = persistSelection; + return (options: O, provider?: StacksProvider) => { + if (!provider) throw new Error('[Connect] No installed Stacks wallet found'); - const originalOverflow = document.body.style.overflow; - document.body.style.overflow = 'hidden'; + const params = mapOptions(options); - const closeModal = () => { - element.remove(); - document.body.style.overflow = originalOverflow; - }; - - element.callback = (selectedProvider: StacksProvider | undefined) => { - closeModal(); - action(options, selectedProvider); - }; - element.cancelCallback = () => { - closeModal(); - options.onCancel?.(); - }; - - document.body.appendChild(element); - - const handleEsc = (ev: KeyboardEvent) => { - if (ev.key === 'Escape') { - document.removeEventListener('keydown', handleEsc); - element.remove(); - } - }; - document.addEventListener('keydown', handleEsc); + void request(uiOptions, method, params) + .then(response => { + const r = mapResponse(response); + options.onFinish?.(r); + }) + .catch(options.onCancel); }; } +// BACKWARDS COMPATIBILITY + /** A wrapper for selecting a wallet (if none is selected) and then calling the {@link authenticate} action. */ -export const showConnect = wrapConnectCall(authenticate, false); +export const showConnect = requestShowLegacy('stx_getAddresses'); /** A wrapper for selecting a wallet (if none is selected) and then calling the {@link openSTXTransfer} action. */ -export const showSTXTransfer = wrapConnectCall(openSTXTransfer); +export const showSTXTransfer = requestShowLegacy('stx_transferStx'); + /** A wrapper for selecting a wallet (if none is selected) and then calling the {@link openContractCall} action. */ -export const showContractCall = wrapConnectCall(openContractCall); +export const showContractCall = requestShowLegacy('stx_callContract'); + /** A wrapper for selecting a wallet (if none is selected) and then calling the {@link openContractDeploy} action. */ -export const showContractDeploy = wrapConnectCall(openContractDeploy); +export const showContractDeploy = requestShowLegacy('stx_deployContract'); + /** A wrapper for selecting a wallet (if none is selected) and then calling the {@link openSignTransaction} action. */ -export const showSignTransaction = wrapConnectCall(openSignTransaction); +export const showSignTransaction = requestShowLegacy('stx_signTransaction'); -/** A wrapper for selecting a wallet (if none is selected) and then calling the {@link openPsbtRequestPopup} action. */ -export const showPsbt = wrapConnectCall(openPsbtRequestPopup); /** A wrapper for selecting a wallet (if none is selected) and then calling the {@link openProfileUpdateRequestPopup} action. */ -export const showProfileUpdate = wrapConnectCall(openProfileUpdateRequestPopup); +export const showProfileUpdate = requestShowLegacy('stx_updateProfile'); + /** A wrapper for selecting a wallet (if none is selected) and then calling the {@link openSignatureRequestPopup} action. */ -export const showSignMessage = wrapConnectCall(openSignatureRequestPopup); +export const showSignMessage = requestLegacy( + 'stx_signMessage', + LEGACY_SIGN_MESSAGE_OPTIONS_MAP, + LEGACY_SIGN_MESSAGE_RESPONSE_MAP +); + /** A wrapper for selecting a wallet (if none is selected) and then calling the {@link openStructuredDataSignatureRequestPopup} action. */ -export const showSignStructuredMessage = wrapConnectCall(openStructuredDataSignatureRequestPopup); +export const showSignStructuredMessage = requestLegacy( + 'stx_signStructuredMessage', + LEGACY_SIGN_STRUCTURED_MESSAGE_OPTIONS_MAP, + LEGACY_SIGN_STRUCTURED_MESSAGE_RESPONSE_MAP +); /** Disconnect selected wallet. Alias for {@link clearSelectedProviderId} */ export const disconnect = clearSelectedProviderId; diff --git a/packages/connect/src/utils.ts b/packages/connect/src/utils.ts index 6ea84a32..ae03e53a 100644 --- a/packages/connect/src/utils.ts +++ b/packages/connect/src/utils.ts @@ -1,15 +1,21 @@ -import { getSelectedProviderId, getProviderFromId } from '@stacks/connect-ui'; +import { getProviderFromId, getSelectedProviderId } from '@stacks/connect-ui'; +import { TransactionVersion } from '@stacks/network'; import { - StacksNetwork as LegacyStacksNetwork, StacksMainnet as LegacyStacksMainnet, + StacksNetwork as LegacyStacksNetwork, StacksTestnet as LegacyStacksTestnet, } from '@stacks/network-v6'; +import { Address, Cl, ClarityValue } from '@stacks/transactions'; +import { + ClarityType as LegacyClarityType, + ClarityValue as LegacyClarityValue, +} from '@stacks/transactions-v6'; import { ConnectNetwork } from './types'; -import { TransactionVersion } from '@stacks/network'; +/** @deprecated This will default to the legacy provider. The behavior may be undefined with competing wallets. */ export function getStacksProvider() { const provider = getProviderFromId(getSelectedProviderId()); - return provider || window.StacksProvider || window.BlockstackProvider; + return provider || (window as any).StacksProvider || (window as any).BlockstackProvider; } export function isStacksWalletInstalled() { @@ -28,3 +34,49 @@ export function legacyNetworkFromConnectNetwork(network?: ConnectNetwork): Legac ? new LegacyStacksMainnet({ url: network.client.baseUrl }) : new LegacyStacksTestnet({ url: network.client.baseUrl }); } + +/** + * @internal + * This may be moved to Stacks.js in the future. + */ +export function legacyCVToCV(cv: LegacyClarityValue | ClarityValue): ClarityValue { + if (typeof cv.type === 'string') return cv; + + switch (cv.type) { + case LegacyClarityType.BoolFalse: + return Cl.bool(false); + case LegacyClarityType.BoolTrue: + return Cl.bool(true); + case LegacyClarityType.Int: + return Cl.int(cv.value); + case LegacyClarityType.UInt: + return Cl.uint(cv.value); + case LegacyClarityType.Buffer: + return Cl.buffer(cv.buffer); + case LegacyClarityType.StringASCII: + return Cl.stringAscii(cv.data); + case LegacyClarityType.StringUTF8: + return Cl.stringUtf8(cv.data); + case LegacyClarityType.List: + return Cl.list(cv.list.map(legacyCVToCV)); + case LegacyClarityType.Tuple: + return Cl.tuple( + Object.fromEntries(Object.entries(cv.data).map(([k, v]) => [k, legacyCVToCV(v)])) + ); + case LegacyClarityType.OptionalNone: + return Cl.none(); + case LegacyClarityType.OptionalSome: + return Cl.some(legacyCVToCV(cv.value)); + case LegacyClarityType.ResponseErr: + return Cl.error(legacyCVToCV(cv.value)); + case LegacyClarityType.ResponseOk: + return Cl.ok(legacyCVToCV(cv.value)); + case LegacyClarityType.PrincipalContract: + return Cl.contractPrincipal(Address.stringify(cv.address), cv.contractName.content); + case LegacyClarityType.PrincipalStandard: + return Cl.standardPrincipal(Address.stringify(cv.address)); + default: + const _exhaustiveCheck: never = cv; + throw new Error(`Unknown clarity type: ${_exhaustiveCheck}`); + } +} diff --git a/packages/connect/tests/typebox.test.ts b/packages/connect/tests/typebox.test.ts new file mode 100644 index 00000000..b9cdc995 --- /dev/null +++ b/packages/connect/tests/typebox.test.ts @@ -0,0 +1,184 @@ +import { describe, it, expect } from 'vitest'; +import { Value } from '@sinclair/typebox/value'; +import { ClarityValueTypeBoxSchema, PostConditionTypeBoxSchema } from '../src/types/typebox'; + +describe('Clarity Value TypeBox Schemas', () => { + it('should validate uint representation', () => { + const validUint = { type: 'uint', value: '12' }; + const invalidUint = { type: 'uint', value: {} }; + expect(Value.Check(ClarityValueTypeBoxSchema, validUint)).toBe(true); + expect(Value.Check(ClarityValueTypeBoxSchema, invalidUint)).toBe(false); + }); + + it('should validate int representation', () => { + const validInt = { type: 'int', value: '42' }; + const invalidInt = { type: 'int', value: true }; + expect(Value.Check(ClarityValueTypeBoxSchema, validInt)).toBe(true); + expect(Value.Check(ClarityValueTypeBoxSchema, invalidInt)).toBe(false); + }); + + it('should validate buffer representation', () => { + const validBuffer = { type: 'buffer', value: 'beaf' }; + const invalidBuffer = { type: 'buffer', value: 'xyz!' }; + expect(Value.Check(ClarityValueTypeBoxSchema, validBuffer)).toBe(true); + expect(Value.Check(ClarityValueTypeBoxSchema, invalidBuffer)).toBe(false); + }); + + it('should validate boolean representations', () => { + const validTrue = { type: 'true' }; + const validFalse = { type: 'false' }; + const invalidBoolean = { type: 'boolean', value: true }; + expect(Value.Check(ClarityValueTypeBoxSchema, validTrue)).toBe(true); + expect(Value.Check(ClarityValueTypeBoxSchema, validFalse)).toBe(true); + expect(Value.Check(ClarityValueTypeBoxSchema, invalidBoolean)).toBe(false); + }); + + it('should validate principal representations', () => { + const validStandardPrincipal = { + type: 'address', + value: 'STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6', + }; + const validContractPrincipal = { + type: 'contract', + value: 'STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6.contract-name', + }; + const invalidPrincipal = { + type: 'address', + value: 'invalid-address', + }; + expect(Value.Check(ClarityValueTypeBoxSchema, validStandardPrincipal)).toBe(true); + expect(Value.Check(ClarityValueTypeBoxSchema, validContractPrincipal)).toBe(true); + expect(Value.Check(ClarityValueTypeBoxSchema, invalidPrincipal)).toBe(false); + }); + + it('should validate optional representations', () => { + const validNone = { type: 'none' }; + const validSome = { + type: 'some', + value: { type: 'uint', value: '12' }, + }; + const invalidSome = { + type: 'some', + value: 12, + }; + expect(Value.Check(ClarityValueTypeBoxSchema, validNone)).toBe(true); + expect(Value.Check(ClarityValueTypeBoxSchema, validSome)).toBe(true); + expect(Value.Check(ClarityValueTypeBoxSchema, invalidSome)).toBe(false); + }); + + it('should validate string representations', () => { + const validAscii = { type: 'ascii', value: 'hello there' }; + const validUtf8 = { type: 'utf8', value: 'hello 👋' }; + const invalidString = { type: 'string', value: 'wrong type' }; + expect(Value.Check(ClarityValueTypeBoxSchema, validAscii)).toBe(true); + expect(Value.Check(ClarityValueTypeBoxSchema, validUtf8)).toBe(true); + expect(Value.Check(ClarityValueTypeBoxSchema, invalidString)).toBe(false); + }); + + it('should validate tuple representation', () => { + const validTuple = { + type: 'tuple', + value: { + amount: { type: 'uint', value: '10' }, + recipient: { type: 'address', value: 'STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6' }, + }, + }; + const invalidTuple = { + type: 'tuple', + value: ['not', 'an', 'object'], + }; + expect(Value.Check(ClarityValueTypeBoxSchema, validTuple)).toBe(true); + expect(Value.Check(ClarityValueTypeBoxSchema, invalidTuple)).toBe(false); + }); + + it('should validate list representation', () => { + const validList = { + type: 'list', + value: [ + { type: 'int', value: '4' }, + { type: 'int', value: '8' }, + ], + }; + const invalidList = { + type: 'list', + value: { key: 'not an array' }, + }; + expect(Value.Check(ClarityValueTypeBoxSchema, validList)).toBe(true); + expect(Value.Check(ClarityValueTypeBoxSchema, invalidList)).toBe(false); + }); + + it('should validate response representation', () => { + const validOk = { + type: 'ok', + value: { type: 'uint', value: '4' }, + }; + const validErr = { + type: 'err', + value: { type: 'uint', value: '4' }, + }; + const invalidResponse = { + type: 'ok', + value: 'not a clarity value', + }; + expect(Value.Check(ClarityValueTypeBoxSchema, validOk)).toBe(true); + expect(Value.Check(ClarityValueTypeBoxSchema, validErr)).toBe(true); + expect(Value.Check(ClarityValueTypeBoxSchema, invalidResponse)).toBe(false); + }); +}); + +describe('Post-Condition TypeBox Schemas', () => { + it('should validate STX post-condition', () => { + const validStxPostCondition = { + type: 'stx-postcondition', + address: 'STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6', + amount: '10000', + condition: 'gt', + }; + const invalidStxPostCondition = { + type: 'stx-postcondition', + address: 'invalid-address', + amount: { type: 'utf8', value: 'not a number' }, + condition: 'invalid', + }; + expect(Value.Check(PostConditionTypeBoxSchema, validStxPostCondition)).toBe(true); + expect(Value.Check(PostConditionTypeBoxSchema, invalidStxPostCondition)).toBe(false); + }); + + it('should validate FT post-condition', () => { + const validFtPostCondition = { + type: 'ft-postcondition', + address: 'STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6', + asset: 'STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6.tokencoin::tkn', + amount: '1000', + condition: 'gt', + }; + const invalidFtPostCondition = { + type: 'ft-postcondition', + address: 'STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6', + asset: 'invalid-asset-format', + amount: '12000', + condition: 'invalid', + }; + expect(Value.Check(PostConditionTypeBoxSchema, validFtPostCondition)).toBe(true); + expect(Value.Check(PostConditionTypeBoxSchema, invalidFtPostCondition)).toBe(false); + }); + + it('should validate NFT post-condition', () => { + const validNftPostCondition = { + type: 'nft-postcondition', + address: 'STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6.vault', + asset: 'STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6.tokencoin::tkn', + assetId: { type: 'uint', value: '12' }, + condition: 'not-sent', + }; + const invalidNftPostCondition = { + type: 'nft-postcondition', + address: 'invalid-address', + asset: 'invalid-asset', + assetId: '12', + condition: 'invalid', + }; + expect(Value.Check(PostConditionTypeBoxSchema, validNftPostCondition)).toBe(true); + expect(Value.Check(PostConditionTypeBoxSchema, invalidNftPostCondition)).toBe(false); + }); +}); diff --git a/packages/connect/tsconfig.json b/packages/connect/tsconfig.json index 9119b074..7b97f0cf 100644 --- a/packages/connect/tsconfig.json +++ b/packages/connect/tsconfig.json @@ -4,10 +4,9 @@ "baseUrl": ".", "outDir": "./dist/types", "rootDir": "./src", - "jsx": "preserve" }, - "include": ["src"], + "include": ["src/**/*.ts"], "typedocOptions": { "entryPointStrategy": "resolve", "entryPoints": ["src/index.ts"] diff --git a/packages/connect/tsconfig.test.json b/packages/connect/tsconfig.test.json new file mode 100644 index 00000000..638a289f --- /dev/null +++ b/packages/connect/tsconfig.test.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": ".", + "types": ["vitest/globals"] + }, + "include": ["tests/**/*.ts", "src/**/*.ts"] +} diff --git a/sip-030.md b/sip-030.md new file mode 100644 index 00000000..9213a809 --- /dev/null +++ b/sip-030.md @@ -0,0 +1,563 @@ +## Preamble + +SIP Number: `030` + +Title: Integration of a Modern Stacks Wallet Interface Standard + +Authors: [aryzing](https://github.com/aryzing), [janniks](https://github.com/janniks), [kyranjamie](https://github.com/kyranjamie), [m-aboelenein](https://github.com/m-aboelenein) + +Consideration: Technical + +Type: Standard + +Status: Draft + +Created: 10 October 2023 + +License: BSD 2-Clause + +## Abstract + +This SIP proposes common RPC methods to use for the Stacks blockchain's "Connect" and "Auth" systems. +The goal is to replace the current Connect interface, primarily used by web applications to connect with browser extensions and mobile apps with a more straightforward protocol. +This proposal's goal is to standardize JSON compatible interfaces for use with wallet interfaces. + +## Introduction + +The current Connect system[^15], which has existed for several years, is primarily utilized by web applications for interfacing with wallets. +However, many aspects of the existing "Connect" and "Auth" libraries are no longer required, leading to unnecessary complexity (e.g., wrapping RPC payloads in jsontokens) and lack of clear definitions (e.g., undefined serialization for non-JSON compatible data structures) in wallet connectivity. + +Recent attempts[^21][^22][^23][^24][^25][^26] to standardize the interface have sparked valuable discussions but have not culminated in a ratified standard, largely due to the stable state of the existing system. +This SIP aims to address these issues by adopting the WBIPs standards[^11], which offer a more suitable RPC-style interface for modern web applications. +The simplified protocol will allow integration without heavy dependencies (like Auth) and provide a more extendable interface for wallets. + +Additionally, this SIP is motivated by the increased traffic of Ordinal inscriptions on Bitcoin and the Stacks ecosystem growing closer to Bitcoin. +The community has recognized the need for a more unified approach to wallet connectivity (e.g. Bitcoin and PSBTs for previously Stacks-only wallets). +By adopting the new standard, we aim to align the community towards a common and modern protocol for wallet interaction in web applications. +Importantly, the decision to use an existing standard (rather than designing a new one or reworking Auth) is intentional — to avoid further division or split ownership within the community. + +There was an attempt to re-use existing standards/protocols from other ecosystems via the WBIPs working group[^11][^26] — but no consensus was found that was a perfect fit or had enough traction for the larger layer-2 ecosystem. +So this SIP aims to capture the important features for the Stacks ecosystem, with a focus on extensibility. + +## Specification + +The proposed changes are listed as follows: + +Specify [JSON-RPC 2.0](https://www.jsonrpc.org/specification) compatible methods and payloads for wallet interaction. +These can be used via a browser object (i.e., via the `window.WalletProvider.request` method) or similar interfaces like WalletConnect. + +## Backwards Compatibility + +The implementation of this proposal is not necessarily backward compatible. +Wallets implementing the new standard may maintain the previous system to support legacy applications during a transition period or indefinitely. +Existing applications using the current Auth system should continue to operate, but immediate changes are recommended once this SIP is ratified. + +## Reference Implementations + +### Notes on Serialization + +To ensure serializability, consider these notes: + +- Enums are serialized as human-readable strings. +- BigInts are serialized as numbers, strings, or anything that can be parsed by the JavaScript BigInt constructor. +- Bytes are serialized as hex-encoded strings (without a 0x prefix). +- Predefined formats from previous SIPs are used where applicable. +- Addresses are serialized as Stacks c32-encoded strings. +- Clarity values, post-conditions, and transactions are serialized to bytes (defined by SIP-005) and used as hex-encoded strings. + +### Methods + +This section defines the available methods, their parameters, and result structure. +Parameters should be considered only as recommendations for the wallet, and the user/wallet may choose to ignore or override them. + +> Note: Optional params are marked with a `?`. + +Methods can be namespaced under `stx_` if used in more generic settings and other more Ethereum inspired domains. +In other cases (e.g. `WalletConnect`), the namespace may already be given by metadata (e.g. a `chainId` field) and can be omitted. +On the predominant `StacksProvider` global object, the methods can be used without a namespace, but wallets may add namespaced aliases for convenience. + +##### General parameters (for transaction methods) + +The following definitions can be used in the transaction methods. + +Parameter properties + +// add notes here on privacy + +- `address?`: `string` address, Stacks c32-encoded, defaults to wallets current address +- `network?`: `'mainnet' | 'testnet' | 'regtest' | 'devnet' | 'mocknet' | NetworkId` + - where `NetworkId extends string` and can be a network identifier, which already exists in the wallet (e.g. a URL or network name) +- `fee?`: `number | string` BigInt constructor compatible value +- `nonce?`: `number | string` BigInt constructor compatible value +- `postConditions?`: `PostCondition[]`, defaults to `[]` + - where `PostCondition` is `string | object` hex-encoded or JSON representation +- `postConditionMode?`: `'allow' | 'deny'` +- `sponsored?`: `boolean`, defaults to `false` +- ~~`attachment?`~~ _removed_ +- ~~`appDetails`~~ _removed_ +- ~~`onFinish`~~ _removed_ +- ~~`onCancel`~~ _removed_ + +--- + +#### Method `stx_transferStx` + +> **Comment**: This method doesn't take post-conditions. + +Parameter properties + +- `recipient`: `string` address, Stacks c32-encoded +- `amount`: `number | string` BigInt constructor compatible value +- `memo?`: `string`, defaults to `''` + +Result properties + +- `txid`: `string` hex-encoded +- `transaction`: `string` hex-encoded raw transaction + +#### Method `stx_transferSip10Ft` + +Parameter properties + +- `recipient`: `string` address, Stacks c32-encoded +- `asset`: `string` address, Stacks c32-encoded, with contract name suffix +- `amount`: `number | string` BigInt constructor compatible value + +Result properties + +- `txid`: `string` hex-encoded +- `transaction`: `string` hex-encoded raw transaction + +#### Method `stx_transferSip10Nft` + +Parameter properties + +- `recipient`: `string` address, Stacks c32-encoded +- `asset`: `string` address, Stacks c32-encoded, with contract name suffix +- `assetId`: `ClarityValue` + +`where` + +- `ClarityValue`: `string | object` hex-encoded or JSON representation + +Result properties + +- `txid`: `string` hex-encoded +- `transaction`: `string` hex-encoded raw transaction + +#### Method `stx_callContract` + +Parameter properties + +- `contract`: `string.string` address with contract name suffix, Stacks c32-encoded +- `functionName`: `string` +- `functionArgs`: `ClarityValue[]`, defaults to `[]` + - where `ClarityValue` is `string | object` hex-encoded or JSON representation + +Result properties + +- `txid`: `string` hex-encoded +- `transaction`: `string` hex-encoded raw transaction + +#### Method `stx_deployContract` + +Parameter properties + +- `name`: `string` +- `clarityCode`: `string` Clarity contract code +- `clarityVersion?`: `number` + +Result properties + +- `txid`: `string` hex-encoded +- `transaction`: `string` hex-encoded raw transaction + +#### Method `stx_signTransaction` + +Parameter properties + +- `transaction`: `string` hex-encoded raw transaction + +Result properties + +- `transaction`: `string` hex-encoded raw transaction (signed) + +#### Method `stx_signMessage` + +Parameter properties + +- `message`: `string` + +Result properties + +- `signature`: `string` hex-encoded +- `publicKey`: `string` hex-encoded + +#### Method `stx_signStructuredMessage` + +Parameter properties + +- `message`: `string` Clarity value, hex-encoded +- `domain`: `string` hex-encoded (defined by SIP-018) + +Result properties + +- `signature`: `string` hex-encoded +- `publicKey`: `string` hex-encoded + +#### Method `stx_getAddresses` + +> **Note**: This method can be used similarly to legacy "connect" methods, where the first account selection also acts as an approval of the site/domain. + +Result properties + +- `addresses`: `{}[]` + - `address`: `string` address, Stacks c32-encoded + - `publicKey`: `string` hex-encoded + +#### Method `stx_getAccounts` + +> **Comment**: This method is similar to `stx_getAddresses`. +> It was added to provide better backwards compatibility for applications using Gaia. + +> **Note**: This method can be used similarly to legacy "connect" methods, where the first account selection also acts as an approval of the site/domain. + +Result properties + +- `accounts`: `{}[]` + - `address`: `string` address, Stacks c32-encoded + - `publicKey`: `string` hex-encoded + - `gaiaHubUrl`: `string` URL + - `gaiaAppKey`: `string` hex-encoded + +#### Method `stx_getNetworks` + +Result properties + +- `active`: `string` network identifier +- `networks`: `{}[]` + - `id`: `string` + - `rpcUrl`: `string` + - `chainId`: `string` + +#### Method `stx_updateProfile` + +Parameter properties + +- `profile`: `object` Schema.org Person object[^13] + +Result properties + +- `profile`: `object` updated Schema.org Person object[^13] + +### Listeners + +In addition to the request interface, event listeners may be provided via the `.listen` method. + +- `provider.listen(event: string, listener: (...args: any[]) => void): Function` + +> `provider.listen` should return a "unlisten" function, which can be called to remove the listener. + +The event name should be closer to nouns than verbs and doesn't use the `on` prefix from DOM naming conventions. + +#### Event `stx_accountChange` + +`listener: (accounts: {}[]) => void` + +> `accounts` as defined above in `stx_getAccounts`. +> The first account is considered the default account (and may be the only "active" account in a wallet). + +// add network change event + +### Error Codes + +Errors thrown by request methods should match existing JSON-RPC 2.0 error codes. +This way, the user or an intermediary library can handle them in a standardized way. +Otherwise, no additional error codes are defined in this SIP. + +### JSON Representations + +For historical reasons, a Stacks.js internal representation, based on the Stacks core code, has been used in serialized payloads to wallets. +These representations are not human-readable and thus make debugging difficult. +A better solution would be to rely on string literal enumeration, rather than magic values, which need additional lookups. +Relying on solely on hex-encoding also poses difficulties when building Stacks enabled web applications. + +#### Clarity values + +Proposed below is an updated interface representation for Clarity primitives for use in Stacks.js and JSON compatible environments. + +> **Comment**: For encoding larger than JS `Number` big integers, `string` is used. + +`0x00` `int` + +```ts +{ + type: 'int', + value: string // `bigint` compatible +} +``` + +`0x01` `uint` + +```ts +{ + type: 'uint', + value: string // `bigint` compatible +} +``` + +`0x02` `buffer` + +```ts +{ + type: 'buffer', + value: string // hex-encoded string +} +``` + +`0x03` `bool` `true` + +```ts +{ + type: 'true', +} +``` + +`0x04` `bool` `false` + +```ts +{ + type: 'false', +} +``` + +`0x05` `address` (aka "standard principal") + +```ts +{ + type: 'address', + value: string // Stacks c32-encoded +} +``` + +`0x06` `contract` (aka "contract principal") + +```ts +{ + type: 'contract', + value: `${string}.${string}` // Stacks c32-encoded, with contract name suffix +} +``` + +`0x07` `ok` (aka "response ok") + +```ts +{ + type: 'ok', + value: object // Clarity value +} +``` + +`0x08` `err` (aka "response err") + +```ts +{ + type: 'err', + value: object // Clarity value +} +``` + +`0x09` `none` (aka "optional none") + +```ts +{ + type: 'none', +} +``` + +`0x0a` `some` (aka "optional some") + +```ts +{ + type: 'some', + value: object // Clarity value +} +``` + +`0x0b` `list` + +```ts +{ + type: 'list', + value: object[] // Array of Clarity values +} +``` + +`0x0c` `tuple` + +```ts +{ + type: 'tuple', + value: Record // Record of Clarity values +} +``` + +`0x0d` `ascii` + +```ts +{ + type: 'ascii', + value: string // ASCII-compatible string +} +``` + +`0x0e` `utf8` + +```ts +{ + type: 'utf8', + value: string +} +``` + +#### Post-conditions + +`0x00` STX + +```ts +{ + type: 'stx-postcondition', + address: 'origin' | string | `${string}.${string}`, // Stacks c32-encoded, with optional contract name suffix + condition: 'eq' | 'gt' | 'gte' | 'lt' | 'lte', + amount: string // `bigint` compatible, amount in micro-STX +} +``` + +`0x01` Fungible token + +```ts +{ + type: 'ft-postcondition', + address: 'origin' | string | `${string}.${string}`, // Stacks c32-encoded, with optional contract name suffix + condition: 'eq' | 'gt' | 'gte' | 'lt' | 'lte', + asset: `${string}.${string}::${string}` // Stacks c32-encoded address, with contract name suffix, with asset suffix + amount: string // `bigint` compatible, amount in lowest integer denomination of fungible token +} +``` + +`0x02` Non-fungible token + +```ts +{ + type: 'nft-postcondition', + address: 'origin' | string | `${string}.${string}`, // Stacks c32-encoded, with optional contract name suffix + condition: 'sent' | 'not-sent', + asset: `${string}.${string}::${string}` // address with contract name suffix with asset suffix, Stacks c32-encoded + assetId: object, // Clarity value +} +``` + +#### Test vectors + +Listed below are some examples of the potentially unclear representations: + +- `u12` = `{ type: "uint", value: "12" }` +- `0xbeaf` = `{ type: "ascii", value: "hello there" }` +- `"hello there"` = `{ type: "ascii", value: "hello there" }` +- `(list 4 8)` = + ``` + { + type: "list", + value: [ + { type: "int", value: "4"}, + { type: "int", value: "8"}, + ] + } + ``` +- `(err u4)` = + ``` + { + type: "err", + value: { type: "uint", value: "4"}, + } + ``` +- "sends more than 10000 uSTX" = + ``` + { + type: "stx-postcondition", + address: "STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6", + amount: "10000", + condition: "gt" + } + ``` +- "does not send the `12` TKN non-fungible token" = + ``` + { + type: "ntf-postcondition", + address: "STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6.vault" + asset: "STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6.tokencoin::tkn", + assetId: { type: "uint", value: "12" } + condition: "not-sent" + } + ``` + +### Provider registration + +Wallets can register their aliased provider objects however they see fit. +For example, using the WBIP-004[^3] standard or Wallet Standard[^14]. + +## Activation + +This SIP is considered _Ratified_ after Xverse and Leather (currently the largest wallets in the Stacks ecosystem) have implemented and launched the new standard. + +Once wallets have implemented the new standard, tooling (e.g. Stacks Connect[^15]) can be updated to support the new standard as well. +This SIP is not consensus breaking, thus the timeline for activation is not tied to Stacks releases. + +## Related Work + +This SIP is designed as a replacement for the existing Connect system[^15], due to the issues mentioned above. + +The standard builds on top of the following work: the webbtc `.request` standard[^10], Wallet Standard[^14], and WBIPs[^11]. +This SIP is meant to be compatible with various use cases and is meant as a formal specification to unify and drive forward the wallet RPC ecosystem. + +## Appendix + + + +> WBIPs documents partially worked on in the working group with Leather, Xverse, and others. + +[^1]: [WBIP-001: Wallet API JSON RPC](https://wbips.netlify.app/wbips/WBIP001) + +[^2]: [WBIP-002: Namespaces](https://wbips.netlify.app/wbips/WBIP002) + +[^3]: [WBIP-004: Registration](https://wbips.netlify.app/wbips/WBIP004) + +[^4]: [WBIP-007: Batching](https://wbips.netlify.app/wbips/WBIP007) + + + +[^21]: [Wallet JSON RPC API, Request Accounts #2378](https://github.com/leather-wallet/extension/pull/2378) + +[^22]: [Sign-in with stacks #70](https://github.com/stacksgov/sips/pull/70) + +[^23]: [Add API to request addresses #2371](https://github.com/leather-wallet/extension/issues/2371) + +[^24]: [SIP for Wallet Protocol #59](https://github.com/stacksgov/sips/pull/59) + +[^25]: [SIP for Authentication Protocol #50](https://github.com/stacksgov/sips/pull/50) + +[^26]: [Wallet client API](https://github.com/stacksgov/sips/issues/117) + + + +[^10]: [WebBTC Request Standard](https://balls.dev/webbtc/extendability/extending/) + +[^11]: [WBIPs](https://wbips.netlify.app) + +[^12]: [Xverse WalletConnect JSON API](https://docs.xverse.app/wallet-connect/reference/api_reference) + +[^13]: [Schema.org Person](https://schema.org/Person) + +[^14]: [Wallet Standard](https://github.com/wallet-standard/wallet-standard) + +[^15]: [Stacks Connect](https://github.com/hirosystems/connect) From f7fbea294105619a10323c9b6a2c23ce284a1251 Mon Sep 17 00:00:00 2001 From: janniks Date: Tue, 14 Jan 2025 23:04:07 +0100 Subject: [PATCH 2/4] chore: partial --- README.md | 4 +- packages/connect/src/profile/index.ts | 94 +++++-------------- packages/connect/src/request.ts | 9 ++ packages/connect/src/signature/index.ts | 11 ++- .../connect/src/signature/structuredData.ts | 4 +- packages/connect/src/types/profile.ts | 11 ++- packages/connect/src/types/signature.ts | 9 ++ .../src/types/structuredDataSignature.ts | 4 + 8 files changed, 66 insertions(+), 80 deletions(-) diff --git a/README.md b/README.md index ff98560c..a20392cb 100644 --- a/README.md +++ b/README.md @@ -71,5 +71,5 @@ Join our community and stay connected with the latest updates and discussions: #### CONTINUE -- Make old things compatible with new things -- +- Done: signature, structured signature, profile +- Continue: txs, bitcoin diff --git a/packages/connect/src/profile/index.ts b/packages/connect/src/profile/index.ts index e013bcf1..29cb8d58 100644 --- a/packages/connect/src/profile/index.ts +++ b/packages/connect/src/profile/index.ts @@ -1,81 +1,35 @@ -import { createUnsecuredToken, Json, TokenSigner } from 'jsontokens'; -import { getKeys, getUserSession, hasAppPrivateKey } from '../transactions'; -import { - ProfileUpdatePayload, - ProfileUpdatePopup, - ProfileUpdateRequestOptions, - StacksProvider, -} from '../types'; +import { ProfileUpdateRequestOptions, StacksProvider } from '../types'; +import { MethodParams, MethodResult, UpdateProfileResult } from '../methods'; +import { requestRawLegacy } from '../request'; +import { getStacksProvider } from '../utils'; +import { PublicPersonProfile } from '@stacks/profile'; -import { getStacksProvider, legacyNetworkFromConnectNetwork } from '../utils'; +/** @deprecated No-op. Tokens are not needed for latest RPC endpoints. */ +export function getDefaultProfileUpdateRequestOptions(_options: ProfileUpdateRequestOptions) {} -// eslint-disable-next-line @typescript-eslint/require-await -async function signPayload(payload: ProfileUpdatePayload, privateKey: string) { - const tokenSigner = new TokenSigner('ES256k', privateKey); - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion - return tokenSigner.signAsync({ ...payload } as any); -} - -export function getDefaultProfileUpdateRequestOptions(options: ProfileUpdateRequestOptions) { - const network = legacyNetworkFromConnectNetwork(options.network); - const userSession = getUserSession(options.userSession); - const defaults: ProfileUpdateRequestOptions = { - ...options, - network, - userSession, - }; - return { - ...defaults, - }; -} +/** @deprecated No-op. Tokens are not needed for latest RPC endpoints. */ +export const makeProfileUpdateToken = async (_options: ProfileUpdateRequestOptions) => {}; -async function openProfileUpdatePopup( - { token, options }: ProfileUpdatePopup, - provider: StacksProvider -) { - try { - const profileUpdateResponse = await provider.profileUpdateRequest(token); - options.onFinish?.(profileUpdateResponse); - } catch (error) { - console.error('[Connect] Error during signature request', error); - options.onCancel?.(); - } -} +const METHOD = 'stx_updateProfile' as const; -// eslint-disable-next-line @typescript-eslint/require-await -export const makeProfileUpdateToken = async (options: ProfileUpdateRequestOptions) => { - const { userSession, profile, ..._options } = options; - if (hasAppPrivateKey(userSession)) { - const { privateKey, publicKey } = getKeys(userSession); +/** @internal */ +export const LEGACY_UPDATE_PROFILE_OPTIONS_MAP = ( + options: ProfileUpdateRequestOptions +): MethodParams => options; - const payload: ProfileUpdatePayload = { - ..._options, - profile, - publicKey, - }; - - return signPayload(payload, privateKey); - } - const payload = { ..._options }; - return createUnsecuredToken(payload as Json); -}; - -async function generateTokenAndOpenPopup( - options: T, - makeTokenFn: (options: T) => Promise, - provider: StacksProvider -) { - const token = await makeTokenFn({ - ...getDefaultProfileUpdateRequestOptions(options), - ...options, - } as T); - return openProfileUpdatePopup({ token, options }, provider); -} +/** @internal */ +export const LEGACY_UPDATE_PROFILE_RESPONSE_MAP = ( + response: MethodResult +): PublicPersonProfile => response.profile as PublicPersonProfile; +/** Compatible interface with previous Connect `openProfileUpdateRequestPopup` version, but using new SIP-030 RPC method. */ export function openProfileUpdateRequestPopup( options: ProfileUpdateRequestOptions, provider: StacksProvider = getStacksProvider() ) { - if (!provider) throw new Error('[Connect] No installed Stacks wallet found'); - return generateTokenAndOpenPopup(options, makeProfileUpdateToken, provider); + requestRawLegacy( + METHOD, + LEGACY_UPDATE_PROFILE_OPTIONS_MAP, + LEGACY_UPDATE_PROFILE_RESPONSE_MAP + )(options, provider); } diff --git a/packages/connect/src/request.ts b/packages/connect/src/request.ts index d07519b9..ed0c805c 100644 --- a/packages/connect/src/request.ts +++ b/packages/connect/src/request.ts @@ -158,3 +158,12 @@ export function requestRawLegacy< .catch(options.onCancel); }; } + +// todo: strip params that might be unserializable or privacy sensitive, e.g. +// appDetails?: AuthOptions['appDetails']; +// authOrigin?: string; +// network?: ConnectNetwork; +// stxAddress?: string; +// userSession?: UserSession; +// onFinish?: ProfileUpdateFinished; +// onCancel?: ProfileUpdateCanceled; diff --git a/packages/connect/src/signature/index.ts b/packages/connect/src/signature/index.ts index b4b25a4f..5e67058c 100644 --- a/packages/connect/src/signature/index.ts +++ b/packages/connect/src/signature/index.ts @@ -1,7 +1,11 @@ import { MethodParams, MethodResult } from '../methods'; import { requestRawLegacy } from '../request'; import { StacksProvider } from '../types'; -import { CommonSignatureRequestOptions, SignatureRequestOptions } from '../types/signature'; +import { + CommonSignatureRequestOptions, + SignatureData, + SignatureRequestOptions, +} from '../types/signature'; import { getStacksProvider } from '../utils'; /** @deprecated No-op. Tokens are not needed for latest RPC endpoints. */ @@ -22,8 +26,11 @@ export const LEGACY_SIGN_MESSAGE_OPTIONS_MAP = ( ): MethodParams => options; /** @internal */ -export const LEGACY_SIGN_MESSAGE_RESPONSE_MAP = (response: MethodResult) => response; +export const LEGACY_SIGN_MESSAGE_RESPONSE_MAP = ( + response: MethodResult +): SignatureData => response; +/** Compatible interface with previous Connect `openSignatureRequestPopup` version, but using new SIP-030 RPC method. */ export function openSignatureRequestPopup( options: SignatureRequestOptions, provider: StacksProvider = getStacksProvider() diff --git a/packages/connect/src/signature/structuredData.ts b/packages/connect/src/signature/structuredData.ts index 4b7a7f29..2553e24a 100644 --- a/packages/connect/src/signature/structuredData.ts +++ b/packages/connect/src/signature/structuredData.ts @@ -2,7 +2,7 @@ import { TupleCV } from '@stacks/transactions'; import { ClarityType as LegacyClarityType } from '@stacks/transactions-v6'; import { MethodParams, MethodResult } from '../methods'; import { requestRawLegacy } from '../request'; -import { StacksProvider } from '../types'; +import { SignatureData, StacksProvider } from '../types'; import { StructuredDataSignatureRequestOptions } from '../types/structuredDataSignature'; import { getStacksProvider, legacyCVToCV } from '../utils'; @@ -23,7 +23,7 @@ export const LEGACY_SIGN_STRUCTURED_MESSAGE_OPTIONS_MAP = ( /** @internal */ export const LEGACY_SIGN_STRUCTURED_MESSAGE_RESPONSE_MAP = ( response: MethodResult -) => response; +): SignatureData => response; /** Compatible interface with previous Connect `openStructuredDataSignatureRequestPopup` version, but using new SIP-030 RPC method. */ export function openStructuredDataSignatureRequestPopup( diff --git a/packages/connect/src/types/profile.ts b/packages/connect/src/types/profile.ts index b03b7c10..ffd85515 100644 --- a/packages/connect/src/types/profile.ts +++ b/packages/connect/src/types/profile.ts @@ -3,9 +3,12 @@ import { AuthOptions } from './auth'; import { PublicPersonProfile } from '@stacks/profile'; import { ConnectNetwork } from './network'; +/** @deprecated Update to the latest `request` RPC methods. */ export type ProfileUpdateFinished = (data: PublicPersonProfile) => void; +/** @deprecated Update to the latest `request` RPC methods. */ export type ProfileUpdateCanceled = () => void; +/** @deprecated Update to the latest `request` RPC methods. */ export interface ProfileUpdateBase { appDetails?: AuthOptions['appDetails']; authOrigin?: string; @@ -16,22 +19,22 @@ export interface ProfileUpdateBase { onCancel?: ProfileUpdateCanceled; } +/** @deprecated Update to the latest `request` RPC methods. */ export interface CommonProfileUpdatePayload extends ProfileUpdateBase { publicKey: string; } +/** @deprecated Update to the latest `request` RPC methods. */ export interface ProfileUpdatePayload extends CommonProfileUpdatePayload { profile: PublicPersonProfile; } -// same as ProfileUpdatePayload without publicKey +/** @deprecated Update to the latest `request` RPC methods. */ export interface ProfileUpdateRequestOptions extends ProfileUpdateBase { profile: PublicPersonProfile; } -/** - * Transaction Popup - */ +/** @deprecated Update to the latest `request` RPC methods. */ export interface ProfileUpdatePopup { token: string; options: ProfileUpdateRequestOptions; diff --git a/packages/connect/src/types/signature.ts b/packages/connect/src/types/signature.ts index c71a9a17..5a0d295c 100644 --- a/packages/connect/src/types/signature.ts +++ b/packages/connect/src/types/signature.ts @@ -2,9 +2,12 @@ import { UserSession } from '@stacks/auth'; import type { AuthOptions } from '../types/auth'; import { ConnectNetwork } from './network'; +/** @deprecated Update to the latest `request` RPC methods. */ export type SignatureFinished = (data: SignatureData) => void; +/** @deprecated Update to the latest `request` RPC methods. */ export type SignatureCanceled = () => void; +/** @deprecated Update to the latest `request` RPC methods. */ export interface CommonSignatureRequestOptions { appDetails?: AuthOptions['appDetails']; authOrigin?: string; @@ -15,25 +18,30 @@ export interface CommonSignatureRequestOptions { onCancel?: SignatureCanceled; } +/** @deprecated Update to the latest `request` RPC methods. */ export interface SignatureRequestOptions extends CommonSignatureRequestOptions { message: string; // todo: check before merge if we only sign strings or also clarity values. } +/** @deprecated Update to the latest `request` RPC methods. */ export interface SignatureOptions { message: string; onFinish?: SignatureFinished; onCancel?: SignatureCanceled; } +/** @deprecated Update to the latest `request` RPC methods. */ export interface SignaturePopup { token: string; options: SignatureOptions; } +/** @deprecated Update to the latest `request` RPC methods. */ export interface SignaturePayload extends CommonSignaturePayload { message: string; } +/** @deprecated Update to the latest `request` RPC methods. */ export interface SignatureData { /* Hex encoded DER signature */ signature: string; @@ -41,6 +49,7 @@ export interface SignatureData { publicKey: string; } +/** @deprecated Update to the latest `request` RPC methods. */ export interface CommonSignaturePayload { publicKey: string; /** diff --git a/packages/connect/src/types/structuredDataSignature.ts b/packages/connect/src/types/structuredDataSignature.ts index ba4e1a09..7a1e1b72 100644 --- a/packages/connect/src/types/structuredDataSignature.ts +++ b/packages/connect/src/types/structuredDataSignature.ts @@ -10,11 +10,13 @@ import { SignatureFinished, } from './signature'; +/** @deprecated Update to the latest `request` RPC methods. */ export interface StructuredDataSignatureRequestOptions extends CommonSignatureRequestOptions { message: LegacyClarityValue | ClarityValue; domain: LegacyTupleCV | TupleCV; } +/** @deprecated Update to the latest `request` RPC methods. */ export interface StructuredDataSignatureOptions { message: LegacyClarityValue | ClarityValue; domain: LegacyTupleCV | TupleCV; @@ -22,11 +24,13 @@ export interface StructuredDataSignatureOptions { onCancel?: SignatureCanceled; } +/** @deprecated Update to the latest `request` RPC methods. */ export type StructuredDataSignaturePopup = { token: string; options: StructuredDataSignatureOptions; }; +/** @deprecated Update to the latest `request` RPC methods. */ export interface StructuredDataSignaturePayload extends CommonSignaturePayload { message: LegacyClarityValue | ClarityValue; domain: LegacyTupleCV | TupleCV; From e5b85f91d2d5690f7ed92febd54d7714d0d3b5ac Mon Sep 17 00:00:00 2001 From: janniks Date: Thu, 16 Jan 2025 00:23:22 +0100 Subject: [PATCH 3/4] chore: add more partial, all methods --- README.md | 14 + packages/connect/src/bitcoin/psbt.ts | 133 +++---- packages/connect/src/index.ts | 48 ++- packages/connect/src/methods.ts | 51 ++- packages/connect/src/profile/index.ts | 6 +- packages/connect/src/request.ts | 4 +- packages/connect/src/transactions/index.ts | 403 ++++++++++----------- packages/connect/src/types/provider.ts | 5 +- packages/connect/src/types/transactions.ts | 3 +- packages/connect/src/utils.ts | 28 +- 10 files changed, 373 insertions(+), 322 deletions(-) diff --git a/README.md b/README.md index a20392cb..6116e152 100644 --- a/README.md +++ b/README.md @@ -73,3 +73,17 @@ Join our community and stay connected with the latest updates and discussions: - Done: signature, structured signature, profile - Continue: txs, bitcoin + +#### TODO + +- Add `PostConditionModeName` to all options (new and old) — This might have been missing since the v7 release. +- Try to make address to user session work. +- Strip unserializable fields from RawLegacy wrapper just in case. +- Remove exports from LEGACY_XYZ + +Search for the below and replace with inline return. + +``` +=> { + return { +``` diff --git a/packages/connect/src/bitcoin/psbt.ts b/packages/connect/src/bitcoin/psbt.ts index 7944e99b..261fedeb 100644 --- a/packages/connect/src/bitcoin/psbt.ts +++ b/packages/connect/src/bitcoin/psbt.ts @@ -1,80 +1,53 @@ -// TODO -// import { createUnsecuredToken, Json, TokenSigner } from 'jsontokens'; -// import { getKeys, getUserSession, hasAppPrivateKey } from '../transactions'; -// import { StacksProvider } from '../types'; -// import { PsbtPayload, PsbtPopup, PsbtRequestOptions } from '../types/bitcoin'; -// import { getStacksProvider, legacyNetworkFromConnectNetwork } from '../utils'; - -// // eslint-disable-next-line @typescript-eslint/require-await -// async function signPayload(payload: PsbtPayload, privateKey: string) { -// const tokenSigner = new TokenSigner('ES256k', privateKey); -// // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion -// return tokenSigner.signAsync({ ...payload } as any); -// } - -// export function getDefaultPsbtRequestOptions(options: PsbtRequestOptions) { -// const network = legacyNetworkFromConnectNetwork(options.network); -// const userSession = getUserSession(options.userSession); -// const defaults: PsbtRequestOptions = { -// ...options, -// network, -// userSession, -// }; -// return { -// ...defaults, -// }; -// } - -// async function openPsbtPopup({ token, options }: PsbtPopup, provider: StacksProvider) { -// if (!provider) throw new Error('[Connect] No installed Stacks wallet found'); - -// try { -// const psbtResponse = await provider.psbtRequest(token); -// options.onFinish?.(psbtResponse); -// } catch (error) { -// console.error('[Connect] Error during psbt request', error); -// options.onCancel?.(); -// } -// } - -// // eslint-disable-next-line @typescript-eslint/require-await -// export const makePsbtToken = async (options: PsbtRequestOptions) => { -// const { allowedSighash, hex, signAtIndex, userSession, ..._options } = options; -// if (hasAppPrivateKey(userSession)) { -// const { privateKey, publicKey } = getKeys(userSession); - -// const payload: PsbtPayload = { -// ..._options, -// allowedSighash, -// hex, -// signAtIndex, -// publicKey, -// }; - -// return signPayload(payload, privateKey); -// } -// const payload = { ..._options }; -// return createUnsecuredToken(payload as Json); -// }; - -// async function generateTokenAndOpenPopup( -// options: T, -// makeTokenFn: (options: T) => Promise, -// provider: StacksProvider -// ) { -// const token = await makeTokenFn({ -// ...getDefaultPsbtRequestOptions(options), -// ...options, -// } as T); -// return openPsbtPopup({ token, options }, provider); -// } - -// /** -// * @experimental -// */ -// export function openPsbtRequestPopup( -// options: PsbtRequestOptions, -// provider: StacksProvider = getStacksProvider() -// ) { -// return generateTokenAndOpenPopup(options, makePsbtToken, provider); -// } +import { createUnsecuredToken, Json, TokenSigner } from 'jsontokens'; +import { getKeys, getUserSession, hasAppPrivateKey } from '../transactions'; +import { StacksProvider } from '../types'; +import { + PsbtData, + PsbtPayload, + PsbtPopup, + PsbtRequestOptions, + SignatureHash, +} from '../types/bitcoin'; +import { getStacksProvider, legacyNetworkFromConnectNetwork } from '../utils'; +import { requestRawLegacy } from '../request'; +import { MethodParams, MethodResult, SigHash, SignPsbtResult } from '../methods'; + +/** @deprecated No-op. Tokens are not needed for latest RPC endpoints. */ +export function getDefaultPsbtRequestOptions(_options: PsbtRequestOptions) {} + +/** @deprecated No-op. Tokens are not needed for latest RPC endpoints. */ +export const makePsbtToken = async (_options: PsbtRequestOptions) => {}; + +const METHOD = 'signPsbt' as const; + +/** @internal */ +export const LEGACY_SIGN_PSBT_OPTIONS_MAP = ( + options: PsbtRequestOptions +): MethodParams => { + return { + psbt: options.hex, + signInputs: + typeof options.signAtIndex === 'number' ? [options.signAtIndex] : options.signAtIndex, + allowedSigHash: options.allowedSighash?.map(hash => SignatureHash[hash] as SigHash), + }; +}; + +/** @internal */ +export const LEGACY_SIGN_PSBT_RESPONSE_MAP = (response: MethodResult): PsbtData => ({ + hex: response.psbt, +}); + +/** + * @experimental + * Compatible interface with previous Connect `openPsbtRequestPopup` version, but using new SIP-030 RPC method. + */ +export function openPsbtRequestPopup( + options: PsbtRequestOptions, + provider: StacksProvider = getStacksProvider() +) { + requestRawLegacy( + METHOD, + LEGACY_SIGN_PSBT_OPTIONS_MAP, + LEGACY_SIGN_PSBT_RESPONSE_MAP + )(options, provider); +} diff --git a/packages/connect/src/index.ts b/packages/connect/src/index.ts index 99df795c..41a54aea 100644 --- a/packages/connect/src/index.ts +++ b/packages/connect/src/index.ts @@ -1,24 +1,48 @@ -export * from './auth'; // file may be renamed in the future +export * from './auth'; // File may be renamed in the future -export * from './bitcoin'; -export * from './transactions'; -export * from './signature'; -export * from './signature/structuredData'; -export * from './profile'; +export * from './providers'; export * from './types'; export * from './ui'; -export * from './providers'; - +// Manual exports to avoid exporting internals (e.g. `LEGACY_XYZ`) +export { getDefaultPsbtRequestOptions, makePsbtToken, openPsbtRequestPopup } from './bitcoin'; +export { + getDefaultSignatureRequestOptions, + SignatureRequestPayload, + signMessage, + openSignatureRequestPopup, +} from './signature'; +export { + signStructuredMessage, + openStructuredDataSignatureRequestPopup, +} from './signature/structuredData'; +export { + getDefaultProfileUpdateRequestOptions, + makeProfileUpdateToken, + openProfileUpdateRequestPopup, +} from './profile'; +export { + getUserSession, + hasAppPrivateKey, + getKeys, + getStxAddress, + makeContractCallToken, + makeContractDeployToken, + makeSTXTransferToken, + makeSignTransaction, + openContractCall, + openContractDeploy, + openSTXTransfer, + openSignTransaction, +} from './transactions'; export { request, requestRaw } from './request'; - export { getStacksProvider, isStacksWalletInstalled } from './utils'; -// typebox -// only export the outermost typebox schemas +// TypeBox +// Only export the outermost typebox schemas export { ClarityValueTypeBoxSchema, PostConditionTypeBoxSchema } from './types/typebox'; -// re-exports +// Re-exports export { clearSelectedProviderId, getSelectedProviderId, diff --git a/packages/connect/src/methods.ts b/packages/connect/src/methods.ts index 0f222ef5..c6b372fe 100644 --- a/packages/connect/src/methods.ts +++ b/packages/connect/src/methods.ts @@ -47,7 +47,7 @@ interface CommonTxParams { * The recommended address to use for the method. * * ⚠︎ Warning: Wallets may not implement this for privacy reasons. - * */ + */ address?: AddressString; network?: NetworkString; @@ -57,7 +57,7 @@ interface CommonTxParams { sponsored?: boolean; - postConditions?: PostCondition[]; + postConditions?: (string | PostCondition)[]; // hex-encoded string or JSON PostCondition postConditionMode?: PostConditionModeName; } @@ -77,13 +77,13 @@ export interface TransferFungibleParams extends CommonTxParams { export interface TransferNonFungibleParams extends CommonTxParams { recipient: string; asset: string; - assetId: ClarityValue; + assetId: ClarityValue; // todo: add string (hex-encoded), add string (clarity syntax) } export interface CallContractParams extends CommonTxParams { contract: ContractIdString; functionName: string; - functionArgs?: ClarityValue[]; + functionArgs?: ClarityValue[]; // todo: add string (hex-encoded), add string (clarity syntax) } export interface DeployContractParams extends CommonTxParams { @@ -153,20 +153,45 @@ export interface UpdateProfileResult { // JSON RPC METHODS -export type Methods = keyof StxMethods; +/// BITCOIN METHODS -export type MethodParams = StxMethods[M]['params']; +export type SigHash = 'ALL' | 'NONE' | 'SINGLE' | 'ANYONECANPAY'; -export type MethodResult = StxMethods[M]['result']; +export interface SignInputsByAddress { + [address: string]: number[]; +} + +export interface SignPsbtParams { + psbt: string; + signInputs?: number[] | SignInputsByAddress; + /** @experimental Might need a rename, when SIPs/WBIPs are finalized. */ + allowedSigHash?: SigHash[]; +} + +export interface SignPsbtResult { + txid?: string; + psbt: string; +} -export type JsonRpcResponse = { +// todo: double check spec +export type JsonRpcResponse = { jsonrpc: '2.0'; id: number; - result: MethodResult; + result: Methods[M]['result']; // todo: add error }; -export type StxMethods = { +export type Methods = { + // BTC + signPsbt: { + params: SignPsbtParams; + result: SignPsbtResult; + }; + getAddresses: { + params: GetAddressesParams; + result: GetAddressesResult; + }; + // STX stx_transferStx: { params: TransferStxParams; result: TransactionResult; @@ -213,6 +238,6 @@ export type StxMethods = { }; }; -export type GlobalMethods = { - getAddresses: StxMethods['stx_getAddresses']; // todo: might differ later -}; +export type MethodParams = Methods[M]['params']; + +export type MethodResult = Methods[M]['result']; diff --git a/packages/connect/src/profile/index.ts b/packages/connect/src/profile/index.ts index 29cb8d58..bbc5cda1 100644 --- a/packages/connect/src/profile/index.ts +++ b/packages/connect/src/profile/index.ts @@ -1,8 +1,8 @@ -import { ProfileUpdateRequestOptions, StacksProvider } from '../types'; -import { MethodParams, MethodResult, UpdateProfileResult } from '../methods'; +import { PublicPersonProfile } from '@stacks/profile'; +import { MethodParams, MethodResult } from '../methods'; import { requestRawLegacy } from '../request'; +import { ProfileUpdateRequestOptions, StacksProvider } from '../types'; import { getStacksProvider } from '../utils'; -import { PublicPersonProfile } from '@stacks/profile'; /** @deprecated No-op. Tokens are not needed for latest RPC endpoints. */ export function getDefaultProfileUpdateRequestOptions(_options: ProfileUpdateRequestOptions) {} diff --git a/packages/connect/src/request.ts b/packages/connect/src/request.ts index ed0c805c..a0150426 100644 --- a/packages/connect/src/request.ts +++ b/packages/connect/src/request.ts @@ -14,7 +14,7 @@ export interface ConnectRequestOptions { // todo: maybe add callbacks, if set use them instead of throwing errors } -export async function requestRaw( +export async function requestRaw( provider: StacksProvider, method: M, params?: MethodParams @@ -134,7 +134,7 @@ function requestArgs( * @internal Legacy non-UI request. */ export function requestRawLegacy< - M extends Methods, + M extends keyof Methods, O extends { onCancel?: () => void; onFinish?: (response: R) => void; diff --git a/packages/connect/src/transactions/index.ts b/packages/connect/src/transactions/index.ts index 1ce29ef8..9a12c63a 100644 --- a/packages/connect/src/transactions/index.ts +++ b/packages/connect/src/transactions/index.ts @@ -1,44 +1,43 @@ import { AppConfig, UserSession } from '@stacks/auth'; -import { bytesToHex, hexToBytes } from '@stacks/common'; +import { bytesToHex } from '@stacks/common'; import { ChainId } from '@stacks/network'; import { + Cl, deserializeTransaction, PostCondition, - postConditionToHex, - serializeCV, + PostConditionMode, + PostConditionModeName, } from '@stacks/transactions'; import { PostCondition as LegacyPostCondition, - serializeCV as legacySerializeCV, serializePostCondition as legacySerializePostCondition, } from '@stacks/transactions-v6'; -import { createUnsecuredToken, Json, SECP256K1Client, TokenSigner } from 'jsontokens'; +import { SECP256K1Client } from 'jsontokens'; +import { MethodParams, MethodResult } from '../methods'; +import { requestRawLegacy } from '../request'; import { StacksProvider } from '../types'; import { ContractCallOptions, - ContractCallPayload, ContractCallRegularOptions, ContractCallSponsoredOptions, ContractDeployOptions, - ContractDeployPayload, - ContractDeployRegularOptions, - ContractDeploySponsoredOptions, - FinishedTxPayload, + FinishedTxData, + SignTransactionFinishedTxData, SignTransactionOptions, - SignTransactionPayload, - SponsoredFinishedTxPayload, + SponsoredFinishedTxData, STXTransferOptions, - STXTransferPayload, STXTransferRegularOptions, STXTransferSponsoredOptions, TransactionOptions, - TransactionPayload, - TransactionPopup, - TransactionTypes, } from '../types/transactions'; -import { getStacksProvider, legacyNetworkFromConnectNetwork } from '../utils'; +import { + connectNetworkToString, + getStacksProvider, + legacyCVToCV, + legacyNetworkFromConnectNetwork, +} from '../utils'; -// TODO extract out of transactions +/** @deprecated Update to the latest `request` RPC methods. It's not recommended to use the UserSession. */ export const getUserSession = (_userSession?: UserSession) => { let userSession = _userSession; @@ -49,6 +48,7 @@ export const getUserSession = (_userSession?: UserSession) => { return userSession; }; +/** @deprecated Update to the latest `request` RPC methods. It's not recommended to use the UserSession. */ export function hasAppPrivateKey(userSession?: UserSession) { try { const session = getUserSession(userSession).loadUserData(); @@ -58,6 +58,7 @@ export function hasAppPrivateKey(userSession?: UserSession) { } } +/** @deprecated Update to the latest `request` RPC methods. It's not recommended to use the UserSession. */ export const getKeys = (_userSession?: UserSession) => { const userSession = getUserSession(_userSession); const privateKey = userSession.loadUserData().appPrivateKey; @@ -66,7 +67,7 @@ export const getKeys = (_userSession?: UserSession) => { return { privateKey, publicKey }; }; -// TODO extract out of transactions +/** @deprecated Update to the latest `request` RPC methods. It's not recommended to use the UserSession. */ export function getStxAddress(options: TransactionOptions) { const { stxAddress, userSession, network: _network } = options; @@ -83,230 +84,214 @@ export function getStxAddress(options: TransactionOptions) { return address; } -function getDefaults(options: TransactionOptions) { - const network = legacyNetworkFromConnectNetwork(options.network); +/** @deprecated No-op. Tokens are not needed for latest RPC endpoints. */ +export const makeContractCallToken = async (_options: ContractCallOptions) => {}; - const userSession = getUserSession(options.userSession); - const defaults: TransactionOptions = { - ...options, - network, - userSession, - }; - - return { - stxAddress: getStxAddress(defaults), - ...defaults, - }; -} +/** @deprecated No-op. Tokens are not needed for latest RPC endpoints. */ +export const makeContractDeployToken = async (_options: ContractDeployOptions) => {}; -// eslint-disable-next-line @typescript-eslint/require-await -async function signPayload(payload: TransactionPayload, privateKey: string) { - let { postConditions } = payload; - if (postConditions && postConditions.length > 0 && typeof postConditions[0] !== 'string') { - if (typeof postConditions[0].type === 'string') { - // new readable types - postConditions = (postConditions as PostCondition[]).map(postConditionToHex); - } else { - // legacy types - postConditions = (postConditions as LegacyPostCondition[]).map(pc => { - return bytesToHex(legacySerializePostCondition(pc)); - }); - } - } - const tokenSigner = new TokenSigner('ES256k', privateKey); - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion - return tokenSigner.signAsync({ ...payload, postConditions } as any); -} +/** @deprecated No-op. Tokens are not needed for latest RPC endpoints. */ +export const makeSTXTransferToken = async (_options: STXTransferOptions) => {}; -function createUnsignedTransactionPayload(payload: Partial) { - let { postConditions } = payload; - if (postConditions && postConditions.length > 0 && typeof postConditions[0] !== 'string') { - if (typeof postConditions[0].type === 'string') { - // new readable types - postConditions = (postConditions as PostCondition[]).map(postConditionToHex); - } else { - // legacy types - postConditions = (postConditions as LegacyPostCondition[]).map(pc => { - return bytesToHex(legacySerializePostCondition(pc)); - }); - } - } - return createUnsecuredToken({ ...payload, postConditions } as unknown as Json); -} +/** @deprecated No-op. Tokens are not needed for latest RPC endpoints. */ +export const makeSignTransaction = async (_options: SignTransactionOptions) => {}; -const openTransactionPopup = async ( - { token, options }: TransactionPopup, - provider: StacksProvider -) => { - try { - const txResponse = await provider.transactionRequest(token); - const { txRaw } = txResponse; - const txBytes = hexToBytes(txRaw.replace(/^0x/, '')); - const stacksTransaction = deserializeTransaction(txBytes); - - if ('sponsored' in options && options.sponsored) { - options.onFinish?.({ - ...(txResponse as SponsoredFinishedTxPayload), - stacksTransaction, - }); - return; - } - options.onFinish?.({ - ...(txResponse as FinishedTxPayload), - stacksTransaction, - }); - } catch (error) { - console.error('[Connect] Error during transaction request', error); - options.onCancel?.(); - } -}; +// # TRANSACTION METHODS -// eslint-disable-next-line @typescript-eslint/require-await -export const makeContractCallToken = async (options: ContractCallOptions) => { - const { functionArgs, appDetails, userSession, ..._options } = options; +// ## Contract Call - const args: string[] = functionArgs.map(arg => { - if (typeof arg === 'string') { - return arg; - } - if (typeof arg.type === 'string') { - // new readable types - return serializeCV(arg); - } +const METHOD_CALL_CONTRACT = 'stx_callContract' as const; - // legacy types - return bytesToHex(legacySerializeCV(arg)); +/** @internal */ +export const LEGACY_CALL_CONTRACT_OPTIONS_MAP = ( + options: ContractCallOptions +): MethodParams => { + const functionArgs = options.functionArgs?.map(arg => { + if (typeof arg === 'string') return Cl.deserialize(arg); + return legacyCVToCV(arg); }); - if (hasAppPrivateKey(userSession)) { - const { privateKey, publicKey } = getKeys(userSession); - const payload: ContractCallPayload = { - ..._options, - functionArgs: args, - txType: TransactionTypes.ContractCall, - publicKey, - }; - if (appDetails) payload.appDetails = appDetails; - return signPayload(payload, privateKey); - } - const payload: Partial = { - ..._options, - functionArgs: args, - txType: TransactionTypes.ContractCall, - }; - if (appDetails) payload.appDetails = appDetails; - return createUnsignedTransactionPayload(payload); -}; - -// eslint-disable-next-line @typescript-eslint/require-await -export const makeContractDeployToken = async (options: ContractDeployOptions) => { - const { appDetails, userSession, ..._options } = options; - if (hasAppPrivateKey(userSession)) { - const { privateKey, publicKey } = getKeys(userSession); - const payload: ContractDeployPayload = { - ..._options, - publicKey, - txType: TransactionTypes.ContractDeploy, - }; - if (appDetails) payload.appDetails = appDetails; - return signPayload(payload, privateKey); - } - - const payload: Partial = { - ..._options, - txType: TransactionTypes.ContractDeploy, - }; - if (appDetails) payload.appDetails = appDetails; - return createUnsignedTransactionPayload(payload); -}; -// eslint-disable-next-line @typescript-eslint/require-await -export const makeSTXTransferToken = async (options: STXTransferOptions) => { - const { amount, appDetails, userSession, ..._options } = options; - - if (hasAppPrivateKey(userSession)) { - const { privateKey, publicKey } = getKeys(userSession); - const payload: STXTransferPayload = { - ..._options, - amount: amount.toString(10), - publicKey, - txType: TransactionTypes.STXTransfer, - }; - if (appDetails) payload.appDetails = appDetails; - return signPayload(payload, privateKey); - } - - const payload: Partial = { - ..._options, - amount: amount.toString(10), - txType: TransactionTypes.STXTransfer, + return { + ...options, + contract: `${options.contractAddress}.${options.contractName}`, + functionArgs, + network: connectNetworkToString(options.network), + postConditionMode: optPostConditionMode(options.postConditionMode), + postConditions: optPostCondition(options.postConditions), + address: options.stxAddress, }; - if (appDetails) payload.appDetails = appDetails; - return createUnsignedTransactionPayload(payload); }; -export const makeSignTransaction = async (options: SignTransactionOptions) => { - const { txHex, appDetails, userSession, ..._options } = options; - - if (hasAppPrivateKey(userSession)) { - const { privateKey, publicKey } = getKeys(userSession); - const payload: SignTransactionPayload = { - ..._options, - txHex, - publicKey, - }; - if (appDetails) payload.appDetails = appDetails; - return signPayload(payload, privateKey); - } - - const payload: Partial = { - ..._options, - txHex, +/** @internal */ +export const LEGACY_CALL_CONTRACT_RESPONSE_MAP = ( + response: MethodResult +): FinishedTxData | SponsoredFinishedTxData => { + return { + txId: response.txid, + txRaw: response.transaction, + stacksTransaction: deserializeTransaction(response.transaction), }; - if (appDetails) payload.appDetails = appDetails; - return createUnsignedTransactionPayload(payload); }; -async function generateTokenAndOpenPopup( - options: T, - makeTokenFn: (options: T) => Promise, - provider: StacksProvider -) { - const token = await makeTokenFn({ - ...getDefaults(options), - ...options, - network: legacyNetworkFromConnectNetwork(options.network), // ensure network is legacy compatible - } as T); - return openTransactionPopup({ token, options }, provider); -} - +/** Compatible interface with previous Connect `openContractCall` version, but using new SIP-030 RPC method. */ export function openContractCall( options: ContractCallOptions | ContractCallRegularOptions | ContractCallSponsoredOptions, provider: StacksProvider = getStacksProvider() ) { - if (!provider) throw new Error('[Connect] No installed Stacks wallet found'); - return generateTokenAndOpenPopup(options, makeContractCallToken, provider); + requestRawLegacy( + METHOD_CALL_CONTRACT, + LEGACY_CALL_CONTRACT_OPTIONS_MAP, + LEGACY_CALL_CONTRACT_RESPONSE_MAP + )(options, provider); } +// ## Contract Deploy + +const METHOD_DEPLOY_CONTRACT = 'stx_deployContract' as const; + +/** @internal */ +export const LEGACY_DEPLOY_CONTRACT_OPTIONS_MAP = ( + options: ContractDeployOptions +): MethodParams => { + return { + ...options, + name: options.contractName, + clarityCode: options.codeBody, + network: connectNetworkToString(options.network), + postConditionMode: optPostConditionMode(options.postConditionMode), + postConditions: optPostCondition(options.postConditions), + address: options.stxAddress, + }; +}; + +/** @internal */ +export const LEGACY_DEPLOY_CONTRACT_RESPONSE_MAP = ( + response: MethodResult +): FinishedTxData | SponsoredFinishedTxData => { + return { + txId: response.txid, + txRaw: response.transaction, + stacksTransaction: deserializeTransaction(response.transaction), + }; +}; + +/** Compatible interface with previous Connect `openContractDeploy` version, but using new SIP-030 RPC method. */ export function openContractDeploy( - options: ContractDeployOptions | ContractDeployRegularOptions | ContractDeploySponsoredOptions, + options: ContractDeployOptions, provider: StacksProvider = getStacksProvider() ) { - if (!provider) throw new Error('[Connect] No installed Stacks wallet found'); - return generateTokenAndOpenPopup(options, makeContractDeployToken, provider); + requestRawLegacy( + METHOD_DEPLOY_CONTRACT, + LEGACY_DEPLOY_CONTRACT_OPTIONS_MAP, + LEGACY_DEPLOY_CONTRACT_RESPONSE_MAP + )(options, provider); } +// ## STX Transfer + +const METHOD_TRANSFER_STX = 'stx_transferStx' as const; + +/** @internal */ +export const LEGACY_TRANSFER_STX_OPTIONS_MAP = ( + options: STXTransferOptions +): MethodParams => { + return { + ...options, + amount: options.amount.toString(), + network: connectNetworkToString(options.network), + address: options.stxAddress, + }; +}; + +/** @internal */ +export const LEGACY_TRANSFER_STX_RESPONSE_MAP = ( + response: MethodResult +): FinishedTxData | SponsoredFinishedTxData => { + return { + txId: response.txid, + txRaw: response.transaction, + stacksTransaction: deserializeTransaction(response.transaction), + }; +}; + +/** Compatible interface with previous Connect `openSTXTransfer` version, but using new SIP-030 RPC method. */ export function openSTXTransfer( options: STXTransferOptions | STXTransferRegularOptions | STXTransferSponsoredOptions, provider: StacksProvider = getStacksProvider() ) { - if (!provider) throw new Error('[Connect] No installed Stacks wallet found'); - return generateTokenAndOpenPopup(options, makeSTXTransferToken, provider); + requestRawLegacy( + METHOD_TRANSFER_STX, + LEGACY_TRANSFER_STX_OPTIONS_MAP, + LEGACY_TRANSFER_STX_RESPONSE_MAP + )(options, provider); } +// ## Sign Transaction + +const METHOD_SIGN_TRANSACTION = 'stx_signTransaction' as const; + +/** @internal */ +export const LEGACY_SIGN_TRANSACTION_OPTIONS_MAP = ( + options: SignTransactionOptions +): MethodParams => { + return { + ...options, + transaction: options.txHex, + }; +}; + +/** @internal */ +export const LEGACY_SIGN_TRANSACTION_RESPONSE_MAP = ( + response: MethodResult +): SignTransactionFinishedTxData => { + return { + ...response, // additional fields, in case previous type was incorrect + stacksTransaction: deserializeTransaction(response.transaction), + }; +}; + +/** Compatible interface with previous Connect `openSignTransaction` version, but using new SIP-030 RPC method. */ export function openSignTransaction( options: SignTransactionOptions, provider: StacksProvider = getStacksProvider() ) { - if (!provider) throw new Error('[Connect] No installed Stacks wallet found'); - return generateTokenAndOpenPopup(options, makeSignTransaction, provider); + requestRawLegacy( + METHOD_SIGN_TRANSACTION, + LEGACY_SIGN_TRANSACTION_OPTIONS_MAP, + LEGACY_SIGN_TRANSACTION_RESPONSE_MAP + )(options, provider); +} + +// ## Helpers + +/** @internal */ +function optPostCondition(pcs?: (string | LegacyPostCondition | PostCondition)[]) { + if (typeof pcs === 'undefined') return undefined; + return pcs.map(pc => { + if (typeof pc === 'string') return pc; + if (typeof pc.type === 'string') { + return { + ...pc, + amount: 'amount' in pc ? pc.amount.toString() : undefined, // ensure amount is not bigint + }; + } + return bytesToHex(legacySerializePostCondition(pc)); + }); +} + +/** @internal */ +function optPostConditionMode(mode?: PostConditionModeName | PostConditionMode) { + if (typeof mode === 'undefined') return undefined; + if (typeof mode === 'string') return mode; + switch (mode) { + case PostConditionMode.Allow: + return 'allow'; + case PostConditionMode.Deny: + return 'deny'; + default: + const _exhaustiveCheck: never = mode; + throw new Error( + `Unknown post condition mode: ${_exhaustiveCheck}. Should be one of: 'allow', 'deny'` + ); + } } diff --git a/packages/connect/src/types/provider.ts b/packages/connect/src/types/provider.ts index 2017547c..02bef260 100644 --- a/packages/connect/src/types/provider.ts +++ b/packages/connect/src/types/provider.ts @@ -1,5 +1,8 @@ import { JsonRpcResponse, MethodParams, Methods } from '../methods'; export interface StacksProvider { - request(method: M, params?: MethodParams): Promise>; + request( + method: M, + params?: MethodParams + ): Promise>; } diff --git a/packages/connect/src/types/transactions.ts b/packages/connect/src/types/transactions.ts index e3ee1c3a..df244b3f 100644 --- a/packages/connect/src/types/transactions.ts +++ b/packages/connect/src/types/transactions.ts @@ -3,6 +3,7 @@ import type { AuthOptions } from '../types/auth'; import { ClarityValue as LegacyClarityValue, PostCondition as LegacyPostCondition, + PostConditionMode as LegacyPostConditionMode, } from '@stacks/transactions-v6'; import { PostConditionMode, @@ -15,7 +16,7 @@ import { ConnectNetwork } from './network'; export interface TxBase { appDetails?: AuthOptions['appDetails']; - postConditionMode?: PostConditionMode; + postConditionMode?: LegacyPostConditionMode | PostConditionMode; postConditions?: (string | LegacyPostCondition | PostCondition)[]; network?: ConnectNetwork; anchorMode?: AnchorMode; diff --git a/packages/connect/src/utils.ts b/packages/connect/src/utils.ts index ae03e53a..3f2ba0da 100644 --- a/packages/connect/src/utils.ts +++ b/packages/connect/src/utils.ts @@ -4,11 +4,15 @@ import { StacksMainnet as LegacyStacksMainnet, StacksNetwork as LegacyStacksNetwork, StacksTestnet as LegacyStacksTestnet, + StacksDevnet as LegacyStacksDevnet, + StacksMocknet as LegacyStacksMocknet, } from '@stacks/network-v6'; -import { Address, Cl, ClarityValue } from '@stacks/transactions'; +import { Address, Cl, ClarityValue, PostCondition } from '@stacks/transactions'; import { ClarityType as LegacyClarityType, ClarityValue as LegacyClarityValue, + PostCondition as LegacyPostCondition, + serializePostCondition as legacySerializePostCondition, } from '@stacks/transactions-v6'; import { ConnectNetwork } from './types'; @@ -35,6 +39,28 @@ export function legacyNetworkFromConnectNetwork(network?: ConnectNetwork): Legac : new LegacyStacksTestnet({ url: network.client.baseUrl }); } +function isInstance(object: any, clazz: { new (...args: any[]): T }): object is T { + return object instanceof clazz || object?.constructor?.name?.toLowerCase() === clazz.name; +} + +/** @internal */ +export function connectNetworkToString(network: ConnectNetwork): string { + // not perfect, but good enough to identify the legacy network in most cases + if (typeof network === 'string') return network; + if (isInstance(network, LegacyStacksMainnet)) return 'mainnet'; + if (isInstance(network, LegacyStacksTestnet)) return 'testnet'; + if (isInstance(network, LegacyStacksDevnet)) return 'devnet'; + if (isInstance(network, LegacyStacksMocknet)) return 'devnet'; + if ('coreApiUrl' in (network as any)) return (network as any).coreApiUrl; // in case alternate network was used + if ('url' in network) return network.url; + if ('transactionVersion' in network) { + return network.transactionVersion === (TransactionVersion.Mainnet as number) + ? 'mainnet' + : 'testnet'; + } + return 'mainnet'; // todo: what should the fallback be? +} + /** * @internal * This may be moved to Stacks.js in the future. From 98173fa71b28c7eb1c09d45ae6c6063962aa6ac7 Mon Sep 17 00:00:00 2001 From: janniks Date: Thu, 16 Jan 2025 20:24:38 +0100 Subject: [PATCH 4/4] chore: partial compiling --- packages/connect/src/auth.ts | 13 +++++++ packages/connect/src/bitcoin/psbt.ts | 16 ++------ packages/connect/src/index.ts | 3 +- packages/connect/src/request.ts | 18 ++++----- packages/connect/src/ui.ts | 56 +++++++++++++++++++++++----- packages/connect/src/utils.ts | 8 ++-- 6 files changed, 76 insertions(+), 38 deletions(-) diff --git a/packages/connect/src/auth.ts b/packages/connect/src/auth.ts index bb8f5282..ec686d39 100644 --- a/packages/connect/src/auth.ts +++ b/packages/connect/src/auth.ts @@ -1,3 +1,6 @@ +import { MethodParams, MethodResult } from './methods'; +import { AuthOptions, FinishedAuthData } from './types'; + /** @deprecated Not used anymore. */ export const defaultAuthURL = 'https://app.blockstack.org'; @@ -16,3 +19,13 @@ export const isMobile = () => { } return /windows phone/i.test(ua); }; + +/** @internal */ +export const LEGACY_GET_ADDRESSES_OPTIONS_MAP = ( + _options: AuthOptions +): MethodParams<'stx_getAddresses'> => ({}); + +/** @internal */ +export const LEGACY_GET_ADDRESSES_RESPONSE_MAP = ( + response: MethodResult<'stx_getAddresses'> +): FinishedAuthData => response as unknown as FinishedAuthData; diff --git a/packages/connect/src/bitcoin/psbt.ts b/packages/connect/src/bitcoin/psbt.ts index 261fedeb..142f980b 100644 --- a/packages/connect/src/bitcoin/psbt.ts +++ b/packages/connect/src/bitcoin/psbt.ts @@ -1,16 +1,8 @@ -import { createUnsecuredToken, Json, TokenSigner } from 'jsontokens'; -import { getKeys, getUserSession, hasAppPrivateKey } from '../transactions'; -import { StacksProvider } from '../types'; -import { - PsbtData, - PsbtPayload, - PsbtPopup, - PsbtRequestOptions, - SignatureHash, -} from '../types/bitcoin'; -import { getStacksProvider, legacyNetworkFromConnectNetwork } from '../utils'; +import { MethodParams, MethodResult, SigHash } from '../methods'; import { requestRawLegacy } from '../request'; -import { MethodParams, MethodResult, SigHash, SignPsbtResult } from '../methods'; +import { StacksProvider } from '../types'; +import { PsbtData, PsbtRequestOptions, SignatureHash } from '../types/bitcoin'; +import { getStacksProvider } from '../utils'; /** @deprecated No-op. Tokens are not needed for latest RPC endpoints. */ export function getDefaultPsbtRequestOptions(_options: PsbtRequestOptions) {} diff --git a/packages/connect/src/index.ts b/packages/connect/src/index.ts index 41a54aea..1f512f1f 100644 --- a/packages/connect/src/index.ts +++ b/packages/connect/src/index.ts @@ -1,10 +1,9 @@ -export * from './auth'; // File may be renamed in the future - export * from './providers'; export * from './types'; export * from './ui'; // Manual exports to avoid exporting internals (e.g. `LEGACY_XYZ`) +export { defaultAuthURL, isMobile } from './auth'; export { getDefaultPsbtRequestOptions, makePsbtToken, openPsbtRequestPopup } from './bitcoin'; export { getDefaultSignatureRequestOptions, diff --git a/packages/connect/src/request.ts b/packages/connect/src/request.ts index a0150426..d32b41c0 100644 --- a/packages/connect/src/request.ts +++ b/packages/connect/src/request.ts @@ -20,23 +20,23 @@ export async function requestRaw( params?: MethodParams ): Promise> { const response = await provider.request(method, params); - if (response.error) { - // todo: add typed error handling (before merge) - throw new Error(response.error.message); - } + // if (response.error) { + // // todo: add typed error handling (before merge) + // throw new Error(response.error.message); + // } return response.result; } -export async function request( +export async function request( method: M, params?: MethodParams ): Promise>; -export async function request( +export async function request( options: ConnectRequestOptions, method: M, params?: MethodParams ): Promise>; -export async function request( +export async function request( ...args: | [method: M, params?: MethodParams] | [options: ConnectRequestOptions, method: M, params?: MethodParams] @@ -57,7 +57,7 @@ export async function request( if (opts.provider && !opts.forceSelection) return requestRaw(opts.provider, method, params); // WITH UI - if (typeof window === 'undefined') return; // todo: throw error + if (typeof window === 'undefined') return undefined; // don't throw for SSR contexts void defineCustomElements(window); @@ -102,7 +102,7 @@ export async function request( } /** @internal */ -function requestArgs( +function requestArgs( args: | [method: M, params?: MethodParams] | [options: ConnectRequestOptions, method: M, params?: MethodParams] diff --git a/packages/connect/src/ui.ts b/packages/connect/src/ui.ts index 6b6ff7a7..29f67744 100644 --- a/packages/connect/src/ui.ts +++ b/packages/connect/src/ui.ts @@ -1,12 +1,24 @@ import { clearSelectedProviderId } from '@stacks/connect-ui'; +import { LEGACY_GET_ADDRESSES_OPTIONS_MAP, LEGACY_GET_ADDRESSES_RESPONSE_MAP } from './auth'; +import { MethodParams, MethodResult, Methods } from './methods'; +import { LEGACY_UPDATE_PROFILE_OPTIONS_MAP, LEGACY_UPDATE_PROFILE_RESPONSE_MAP } from './profile'; import { ConnectRequestOptions, request } from './request'; -import { Methods, MethodParams, MethodResult } from './methods'; -import { StacksProvider } from './types'; +import { LEGACY_SIGN_MESSAGE_OPTIONS_MAP, LEGACY_SIGN_MESSAGE_RESPONSE_MAP } from './signature'; import { LEGACY_SIGN_STRUCTURED_MESSAGE_OPTIONS_MAP, LEGACY_SIGN_STRUCTURED_MESSAGE_RESPONSE_MAP, } from './signature/structuredData'; -import { LEGACY_SIGN_MESSAGE_OPTIONS_MAP, LEGACY_SIGN_MESSAGE_RESPONSE_MAP } from './signature'; +import { + LEGACY_CALL_CONTRACT_OPTIONS_MAP, + LEGACY_CALL_CONTRACT_RESPONSE_MAP, + LEGACY_DEPLOY_CONTRACT_OPTIONS_MAP, + LEGACY_DEPLOY_CONTRACT_RESPONSE_MAP, + LEGACY_SIGN_TRANSACTION_OPTIONS_MAP, + LEGACY_SIGN_TRANSACTION_RESPONSE_MAP, + LEGACY_TRANSFER_STX_OPTIONS_MAP, + LEGACY_TRANSFER_STX_RESPONSE_MAP, +} from './transactions'; +import { StacksProvider } from './types'; // /** @internal */ // function requestShowLegacy( @@ -23,7 +35,7 @@ import { LEGACY_SIGN_MESSAGE_OPTIONS_MAP, LEGACY_SIGN_MESSAGE_RESPONSE_MAP } fro * @internal Legacy UI request. */ function requestLegacy< - M extends Methods, + M extends keyof Methods, O extends { onCancel?: () => void; onFinish?: (response: R) => void; @@ -54,22 +66,46 @@ function requestLegacy< // BACKWARDS COMPATIBILITY /** A wrapper for selecting a wallet (if none is selected) and then calling the {@link authenticate} action. */ -export const showConnect = requestShowLegacy('stx_getAddresses'); +export const showConnect = requestLegacy( + 'stx_getAddresses', + LEGACY_GET_ADDRESSES_OPTIONS_MAP, + LEGACY_GET_ADDRESSES_RESPONSE_MAP +); /** A wrapper for selecting a wallet (if none is selected) and then calling the {@link openSTXTransfer} action. */ -export const showSTXTransfer = requestShowLegacy('stx_transferStx'); +export const showSTXTransfer = requestLegacy( + 'stx_transferStx', + LEGACY_TRANSFER_STX_OPTIONS_MAP, + LEGACY_TRANSFER_STX_RESPONSE_MAP +); /** A wrapper for selecting a wallet (if none is selected) and then calling the {@link openContractCall} action. */ -export const showContractCall = requestShowLegacy('stx_callContract'); +export const showContractCall = requestLegacy( + 'stx_callContract', + LEGACY_CALL_CONTRACT_OPTIONS_MAP, + LEGACY_CALL_CONTRACT_RESPONSE_MAP +); /** A wrapper for selecting a wallet (if none is selected) and then calling the {@link openContractDeploy} action. */ -export const showContractDeploy = requestShowLegacy('stx_deployContract'); +export const showContractDeploy = requestLegacy( + 'stx_deployContract', + LEGACY_DEPLOY_CONTRACT_OPTIONS_MAP, + LEGACY_DEPLOY_CONTRACT_RESPONSE_MAP +); /** A wrapper for selecting a wallet (if none is selected) and then calling the {@link openSignTransaction} action. */ -export const showSignTransaction = requestShowLegacy('stx_signTransaction'); +export const showSignTransaction = requestLegacy( + 'stx_signTransaction', + LEGACY_SIGN_TRANSACTION_OPTIONS_MAP, + LEGACY_SIGN_TRANSACTION_RESPONSE_MAP +); /** A wrapper for selecting a wallet (if none is selected) and then calling the {@link openProfileUpdateRequestPopup} action. */ -export const showProfileUpdate = requestShowLegacy('stx_updateProfile'); +export const showProfileUpdate = requestLegacy( + 'stx_updateProfile', + LEGACY_UPDATE_PROFILE_OPTIONS_MAP, + LEGACY_UPDATE_PROFILE_RESPONSE_MAP +); /** A wrapper for selecting a wallet (if none is selected) and then calling the {@link openSignatureRequestPopup} action. */ export const showSignMessage = requestLegacy( diff --git a/packages/connect/src/utils.ts b/packages/connect/src/utils.ts index 3f2ba0da..281b82cf 100644 --- a/packages/connect/src/utils.ts +++ b/packages/connect/src/utils.ts @@ -1,18 +1,16 @@ import { getProviderFromId, getSelectedProviderId } from '@stacks/connect-ui'; import { TransactionVersion } from '@stacks/network'; import { + StacksDevnet as LegacyStacksDevnet, StacksMainnet as LegacyStacksMainnet, + StacksMocknet as LegacyStacksMocknet, StacksNetwork as LegacyStacksNetwork, StacksTestnet as LegacyStacksTestnet, - StacksDevnet as LegacyStacksDevnet, - StacksMocknet as LegacyStacksMocknet, } from '@stacks/network-v6'; -import { Address, Cl, ClarityValue, PostCondition } from '@stacks/transactions'; +import { Address, Cl, ClarityValue } from '@stacks/transactions'; import { ClarityType as LegacyClarityType, ClarityValue as LegacyClarityValue, - PostCondition as LegacyPostCondition, - serializePostCondition as legacySerializePostCondition, } from '@stacks/transactions-v6'; import { ConnectNetwork } from './types';