diff --git a/apps/playground/src/pages/bitcoin/index.tsx b/apps/playground/src/pages/bitcoin/index.tsx index 7a0630cf4..41d30121a 100644 --- a/apps/playground/src/pages/bitcoin/index.tsx +++ b/apps/playground/src/pages/bitcoin/index.tsx @@ -19,7 +19,7 @@ const ReactPage: NextPage = () => { const expectAddress = "tb1q3x7c8nuew6ayzmy3fnfx6ydnr8a4kf2267za7y"; const wallet = new EmbeddedWallet({ - testnet: false, + network: "Mainnet", key: { type: "mnemonic", words: mnemonic.split(" "), @@ -27,18 +27,20 @@ const ReactPage: NextPage = () => { provider: provider, }); - const address = wallet.getAddress(); - console.log("address", address); - console.log("expectAddress", expectAddress === address.address); + const addresses = await wallet.getAddresses(); + const address = addresses[0]?.address!; + console.log("address", addresses); + console.log("expectAddress", expectAddress === address); console.log("network", wallet.getNetworkId()); console.log("publicKey", wallet.getPublicKey()); // console.log("utxos", await wallet.getUtxos()); // console.log("brew", EmbeddedWallet.brew()); + const message = "test message"; - const signature = await wallet.signData(message); - console.log("signature", signature); - const isValid = verifySignature(message, signature, address.publicKey!); + const signedMessage = await wallet.signMessage({ address, message }); + console.log("signature", signedMessage.signature); + const isValid = verifySignature(message, signedMessage.signature, addresses[0]?.publicKey!); console.log("isValid", isValid); } diff --git a/package-lock.json b/package-lock.json index eacff5029..88fefead5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,9 @@ "scripts/*" ], "dependencies": { + "@bitcoinerlab/coinselect": "^1.3.2", + "@bitcoinerlab/descriptors": "^2.3.1", + "@bitcoinerlab/secp256k1": "^1.2.0", "npm": "^10.8.0" }, "devDependencies": { @@ -1306,6 +1309,65 @@ "node": ">=14.0.0" } }, + "node_modules/@bitcoinerlab/coinselect": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@bitcoinerlab/coinselect/-/coinselect-1.3.2.tgz", + "integrity": "sha512-Y8AX3moqJ06MZ8XeGDsi9gAvLpnY7fQ5ICEnmm2XppSBuDQk9nf4bfYJ7KfS8687z68Ss2Cj9l5Ufsm0rBx9oQ==", + "license": "MIT", + "dependencies": { + "@bitcoinerlab/descriptors": "^2.3.0" + } + }, + "node_modules/@bitcoinerlab/descriptors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@bitcoinerlab/descriptors/-/descriptors-2.3.1.tgz", + "integrity": "sha512-e53NE+vMolNusJ9bLQR/Tcu4Wnf9SXKb64+u30pboJozVFtl7brtcCL6IwjB5iW6QYNr1DYGocai8qtgp34p4g==", + "license": "MIT", + "dependencies": { + "@bitcoinerlab/miniscript": "^1.4.0", + "@bitcoinerlab/secp256k1": "^1.2.0", + "bip32": "^4.0.0", + "bitcoinjs-lib": "^6.1.3", + "ecpair": "^2.1.0", + "lodash.memoize": "^4.1.2", + "varuint-bitcoin": "^1.1.2" + }, + "peerDependencies": { + "ledger-bitcoin": "^0.2.2" + }, + "peerDependenciesMeta": { + "ledger-bitcoin": { + "optional": true + } + } + }, + "node_modules/@bitcoinerlab/descriptors/node_modules/varuint-bitcoin": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/varuint-bitcoin/-/varuint-bitcoin-1.1.2.tgz", + "integrity": "sha512-4EVb+w4rx+YfVM32HQX42AbbT7/1f5zwAYhIujKXKk8NQK+JfRVl3pqT3hjNn/L+RstigmGGKVwHA/P0wgITZw==", + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.1" + } + }, + "node_modules/@bitcoinerlab/miniscript": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@bitcoinerlab/miniscript/-/miniscript-1.4.0.tgz", + "integrity": "sha512-BsG3dmwQmgKHnRZecDgUsPjwcpnf1wgaZbolcMTByS10k1zYzIx97W51LzG7GvokRJ+wnzTX/GhC8Y3L2X0CQA==", + "license": "MIT", + "dependencies": { + "bip68": "^1.0.4" + } + }, + "node_modules/@bitcoinerlab/secp256k1": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@bitcoinerlab/secp256k1/-/secp256k1-1.2.0.tgz", + "integrity": "sha512-jeujZSzb3JOZfmJYI0ph1PVpCRV5oaexCgy+RvCXV8XlY+XFB/2n3WOcvBsKLsOw78KYgnQrQWb2HrKE4be88Q==", + "license": "MIT", + "dependencies": { + "@noble/curves": "^1.7.0" + } + }, "node_modules/@bufbuild/protobuf": { "version": "1.10.1", "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-1.10.1.tgz", @@ -12229,6 +12291,16 @@ } } }, + "node_modules/@jest/diff-sequences": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz", + "integrity": "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/@jest/environment": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", @@ -12290,6 +12362,16 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/@jest/get-type": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", + "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/@jest/globals": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", @@ -12306,6 +12388,30 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/@jest/pattern": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", + "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/pattern/node_modules/jest-regex-util": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", + "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/@jest/reporters": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", @@ -12417,6 +12523,61 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/@jest/snapshot-utils": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.2.0.tgz", + "integrity": "sha512-0aVxM3RH6DaiLcjj/b0KrIBZhSX1373Xci4l3cW5xiUWPctZ59zQ7jj4rqcJQ/Z8JuN/4wX3FpJSa3RssVvCug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "natural-compare": "^1.4.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/snapshot-utils/node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/snapshot-utils/node_modules/@jest/types": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", + "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/snapshot-utils/node_modules/@sinclair/typebox": { + "version": "0.34.41", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz", + "integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==", + "dev": true, + "license": "MIT" + }, "node_modules/@jest/source-map": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", @@ -13003,11 +13164,6 @@ "ts-custom-error": "^3.3.1" } }, - "node_modules/@midnight-ntwrk/ledger": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@midnight-ntwrk/ledger/-/ledger-4.0.0.tgz", - "integrity": "sha512-cnJisY8uQ4NO54miDnjs/ov6n102ZPfKEPxtiDKVEdlKijcc70jz/CI2yBCc0itlwwaqma7FM2Ou5N4pWpbwbg==" - }, "node_modules/@midnight-ntwrk/midnight-js-contracts": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@midnight-ntwrk/midnight-js-contracts/-/midnight-js-contracts-2.0.2.tgz", @@ -13124,7 +13280,8 @@ "node_modules/@midnight-ntwrk/zswap": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@midnight-ntwrk/zswap/-/zswap-4.0.0.tgz", - "integrity": "sha512-yKfzp4M/wUkqxUJv1D2SizojYdWYnF3FDeKaxPehLPOFC4BrR9KnLZsNR2Y/occ8a4yFDtQXf7bN1QvK5k7Grw==" + "integrity": "sha512-yKfzp4M/wUkqxUJv1D2SizojYdWYnF3FDeKaxPehLPOFC4BrR9KnLZsNR2Y/occ8a4yFDtQXf7bN1QvK5k7Grw==", + "peer": true }, "node_modules/@multiformats/dns": { "version": "1.0.9", @@ -13403,6 +13560,21 @@ "node": ">=4.0" } }, + "node_modules/@noble/curves": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", + "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@noble/hashes": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", @@ -14129,30 +14301,6 @@ } } }, - "node_modules/@radix-ui/react-progress": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.7.tgz", - "integrity": "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-roving-focus": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", @@ -17910,6 +18058,15 @@ "@noble/hashes": "^1.2.0" } }, + "node_modules/bip68": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/bip68/-/bip68-1.0.4.tgz", + "integrity": "sha512-O1htyufFTYy3EO0JkHg2CLykdXEtV2ssqw47Gq9A0WByp662xpJnMEB9m43LZjsSDjIAOozWRExlFQk2hlV1XQ==", + "license": "ISC", + "engines": { + "node": ">=4.5.0" + } + }, "node_modules/bitcoinjs-lib": { "version": "6.1.7", "resolved": "https://registry.npmjs.org/bitcoinjs-lib/-/bitcoinjs-lib-6.1.7.tgz", @@ -21937,6 +22094,16 @@ "node": ">= 0.8.0" } }, + "node_modules/exit-x": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz", + "integrity": "sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -22478,7 +22645,8 @@ "version": "2.16.11", "resolved": "https://registry.npmjs.org/fp-ts/-/fp-ts-2.16.11.tgz", "integrity": "sha512-LaI+KaX2NFkfn1ZGHoKCmcfv7yrZsC3b8NtWsTVQeHkq4F27vI5igUuO53sxqDEa2gNQMHFPmpojDw/1zmUK7w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/fraction.js": { "version": "4.3.7", @@ -25780,7 +25948,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", - "dev": true, "license": "MIT" }, "node_modules/lodash.merge": { @@ -25891,15 +26058,6 @@ "node": ">=12.20.0" } }, - "node_modules/lucide-react": { - "version": "0.523.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.523.0.tgz", - "integrity": "sha512-rUjQoy7egZT9XYVXBK1je9ckBnNp7qzRZOhLQx5RcEp2dCGlXo+mv6vf7Am4LimEcFBJIIZzSGfgTqc9QCrPSw==", - "license": "ISC", - "peerDependencies": { - "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, "node_modules/lunr": { "version": "2.3.9", "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", @@ -34757,9 +34915,9 @@ } }, "node_modules/ts-jest": { - "version": "29.4.4", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.4.tgz", - "integrity": "sha512-ccVcRABct5ZELCT5U0+DZwkXMCcOCLi2doHRrKy1nK/s7J7bch6TzJMsrY09WxgUUIP/ITfmcDS8D2yl63rnXw==", + "version": "29.4.5", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.5.tgz", + "integrity": "sha512-HO3GyiWn2qvTQA4kTgjDcXiMwYQt68a1Y8+JuLRVpdIzm+UOLSHgl/XqR4c6nzJkq5rOkjc02O2I7P7l/Yof0Q==", "dev": true, "license": "MIT", "dependencies": { @@ -34769,7 +34927,7 @@ "json5": "^2.2.3", "lodash.memoize": "^4.1.2", "make-error": "^1.3.6", - "semver": "^7.7.2", + "semver": "^7.7.3", "type-fest": "^4.41.0", "yargs-parser": "^21.1.1" }, @@ -37389,10 +37547,10 @@ }, "packages/bitcoin": { "name": "@meshsdk/bitcoin", - "version": "1.9.0-beta.80", + "version": "1.9.0-beta.81", "dependencies": { "@bitcoin-js/tiny-secp256k1-asmjs": "^2.2.3", - "bip174": "^3.0.0-rc.1", + "bip174": "^3.0.0", "bip32": "^4.0.0", "bip39": "^3.1.0", "bitcoinjs-lib": "^6.1.7", @@ -37401,11 +37559,323 @@ "devDependencies": { "@meshsdk/configs": "*", "@swc/core": "^1.10.7", + "dotenv": "^17.2.3", "eslint": "^8.57.0", + "jest": "^30.2.0", + "ts-jest": "^29.4.5", "tsup": "^8.0.2", "typescript": "^5.3.3" } }, + "packages/bitcoin/node_modules/@jest/console": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.2.0.tgz", + "integrity": "sha512-+O1ifRjkvYIkBqASKWgLxrpEhQAAE7hY77ALLUufSk5717KfOShg6IbqLmdsLMPdUiFvA2kTs0R7YZy+l0IzZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "jest-message-util": "30.2.0", + "jest-util": "30.2.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "packages/bitcoin/node_modules/@jest/core": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.2.0.tgz", + "integrity": "sha512-03W6IhuhjqTlpzh/ojut/pDB2LPRygyWX8ExpgHtQA8H/3K7+1vKmcINx5UzeOX1se6YEsBsOHQ1CRzf3fOwTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.2.0", + "@jest/pattern": "30.0.1", + "@jest/reporters": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-changed-files": "30.2.0", + "jest-config": "30.2.0", + "jest-haste-map": "30.2.0", + "jest-message-util": "30.2.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.2.0", + "jest-resolve-dependencies": "30.2.0", + "jest-runner": "30.2.0", + "jest-runtime": "30.2.0", + "jest-snapshot": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", + "jest-watcher": "30.2.0", + "micromatch": "^4.0.8", + "pretty-format": "30.2.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "packages/bitcoin/node_modules/@jest/environment": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.2.0.tgz", + "integrity": "sha512-/QPTL7OBJQ5ac09UDRa3EQes4gt1FTEG/8jZ/4v5IVzx+Cv7dLxlVIvfvSVRiiX2drWyXeBjkMSR8hvOWSog5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "jest-mock": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "packages/bitcoin/node_modules/@jest/expect": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.2.0.tgz", + "integrity": "sha512-V9yxQK5erfzx99Sf+7LbhBwNWEZ9eZay8qQ9+JSC0TrMR1pMDHLMY+BnVPacWU6Jamrh252/IKo4F1Xn/zfiqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "30.2.0", + "jest-snapshot": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "packages/bitcoin/node_modules/@jest/expect-utils": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.2.0.tgz", + "integrity": "sha512-1JnRfhqpD8HGpOmQp180Fo9Zt69zNtC+9lR+kT7NVL05tNXIi+QC8Csz7lfidMoVLPD3FnOtcmp0CEFnxExGEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "packages/bitcoin/node_modules/@jest/fake-timers": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.2.0.tgz", + "integrity": "sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@sinonjs/fake-timers": "^13.0.0", + "@types/node": "*", + "jest-message-util": "30.2.0", + "jest-mock": "30.2.0", + "jest-util": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "packages/bitcoin/node_modules/@jest/globals": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.2.0.tgz", + "integrity": "sha512-b63wmnKPaK+6ZZfpYhz9K61oybvbI1aMcIs80++JI1O1rR1vaxHUCNqo3ITu6NU0d4V34yZFoHMn/uoKr/Rwfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.2.0", + "@jest/expect": "30.2.0", + "@jest/types": "30.2.0", + "jest-mock": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "packages/bitcoin/node_modules/@jest/reporters": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.2.0.tgz", + "integrity": "sha512-DRyW6baWPqKMa9CzeiBjHwjd8XeAyco2Vt8XbcLFjiwCOEKOvy82GJ8QQnJE9ofsxCMPjH4MfH8fCWIHHDKpAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "@jridgewell/trace-mapping": "^0.3.25", + "@types/node": "*", + "chalk": "^4.1.2", + "collect-v8-coverage": "^1.0.2", + "exit-x": "^0.2.2", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^5.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "30.2.0", + "jest-util": "30.2.0", + "jest-worker": "30.2.0", + "slash": "^3.0.0", + "string-length": "^4.0.2", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "packages/bitcoin/node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "packages/bitcoin/node_modules/@jest/source-map": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-30.0.1.tgz", + "integrity": "sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "callsites": "^3.1.0", + "graceful-fs": "^4.2.11" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "packages/bitcoin/node_modules/@jest/test-result": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.2.0.tgz", + "integrity": "sha512-RF+Z+0CCHkARz5HT9mcQCBulb1wgCP3FBvl9VFokMX27acKphwyQsNuWH3c+ojd1LeWBLoTYoxF0zm6S/66mjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.2.0", + "@jest/types": "30.2.0", + "@types/istanbul-lib-coverage": "^2.0.6", + "collect-v8-coverage": "^1.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "packages/bitcoin/node_modules/@jest/test-sequencer": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.2.0.tgz", + "integrity": "sha512-wXKgU/lk8fKXMu/l5Hog1R61bL4q5GCdT6OJvdAFz1P+QrpoFuLU68eoKuVc4RbrTtNnTL5FByhWdLgOPSph+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "30.2.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.2.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "packages/bitcoin/node_modules/@jest/transform": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.2.0.tgz", + "integrity": "sha512-XsauDV82o5qXbhalKxD7p4TZYYdwcaEXC77PPD2HixEFF+6YGppjrAAQurTl2ECWcEomHBMMNS9AH3kcCFx8jA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/types": "30.2.0", + "@jridgewell/trace-mapping": "^0.3.25", + "babel-plugin-istanbul": "^7.0.1", + "chalk": "^4.1.2", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.2.0", + "jest-regex-util": "30.0.1", + "jest-util": "30.2.0", + "micromatch": "^4.0.8", + "pirates": "^4.0.7", + "slash": "^3.0.0", + "write-file-atomic": "^5.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "packages/bitcoin/node_modules/@jest/types": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", + "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "packages/bitcoin/node_modules/@sinclair/typebox": { + "version": "0.34.41", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz", + "integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==", + "dev": true, + "license": "MIT" + }, + "packages/bitcoin/node_modules/@sinonjs/fake-timers": { + "version": "13.0.5", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", + "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, "packages/bitcoin/node_modules/@swc/core": { "version": "1.13.5", "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.13.5.tgz", @@ -37445,6 +37915,809 @@ } } }, + "packages/bitcoin/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "packages/bitcoin/node_modules/babel-jest": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.2.0.tgz", + "integrity": "sha512-0YiBEOxWqKkSQWL9nNGGEgndoeL0ZpWrbLMNL5u/Kaxrli3Eaxlt3ZtIDktEvXt4L/R9r3ODr2zKwGM/2BjxVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "30.2.0", + "@types/babel__core": "^7.20.5", + "babel-plugin-istanbul": "^7.0.1", + "babel-preset-jest": "30.2.0", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0 || ^8.0.0-0" + } + }, + "packages/bitcoin/node_modules/babel-plugin-istanbul": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.1.tgz", + "integrity": "sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==", + "dev": true, + "license": "BSD-3-Clause", + "workspaces": [ + "test/babel-8" + ], + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-instrument": "^6.0.2", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "packages/bitcoin/node_modules/babel-plugin-jest-hoist": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.2.0.tgz", + "integrity": "sha512-ftzhzSGMUnOzcCXd6WHdBGMyuwy15Wnn0iyyWGKgBDLxf9/s5ABuraCSpBX2uG0jUg4rqJnxsLc5+oYBqoxVaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/babel__core": "^7.20.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "packages/bitcoin/node_modules/babel-preset-jest": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.2.0.tgz", + "integrity": "sha512-US4Z3NOieAQumwFnYdUWKvUKh8+YSnS/gB3t6YBiz0bskpu7Pine8pPCheNxlPEW4wnUkma2a94YuW2q3guvCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "30.2.0", + "babel-preset-current-node-syntax": "^1.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0 || ^8.0.0-beta.1" + } + }, + "packages/bitcoin/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/bitcoin/node_modules/ci-info": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", + "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "packages/bitcoin/node_modules/cjs-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.1.0.tgz", + "integrity": "sha512-UX0OwmYRYQQetfrLEZeewIFFI+wSTofC+pMBLNuH3RUuu/xzG1oz84UCEDOSoQlN3fZ4+AzmV50ZYvGqkMh9yA==", + "dev": true, + "license": "MIT" + }, + "packages/bitcoin/node_modules/dotenv": { + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", + "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "packages/bitcoin/node_modules/expect": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.2.0.tgz", + "integrity": "sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "30.2.0", + "@jest/get-type": "30.1.0", + "jest-matcher-utils": "30.2.0", + "jest-message-util": "30.2.0", + "jest-mock": "30.2.0", + "jest-util": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "packages/bitcoin/node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "packages/bitcoin/node_modules/jest": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-30.2.0.tgz", + "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "30.2.0", + "@jest/types": "30.2.0", + "import-local": "^3.2.0", + "jest-cli": "30.2.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "packages/bitcoin/node_modules/jest-changed-files": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.2.0.tgz", + "integrity": "sha512-L8lR1ChrRnSdfeOvTrwZMlnWV8G/LLjQ0nG9MBclwWZidA2N5FviRki0Bvh20WRMOX31/JYvzdqTJrk5oBdydQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.1.1", + "jest-util": "30.2.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "packages/bitcoin/node_modules/jest-circus": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.2.0.tgz", + "integrity": "sha512-Fh0096NC3ZkFx05EP2OXCxJAREVxj1BcW/i6EWqqymcgYKWjyyDpral3fMxVcHXg6oZM7iULer9wGRFvfpl+Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.2.0", + "@jest/expect": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "co": "^4.6.0", + "dedent": "^1.6.0", + "is-generator-fn": "^2.1.0", + "jest-each": "30.2.0", + "jest-matcher-utils": "30.2.0", + "jest-message-util": "30.2.0", + "jest-runtime": "30.2.0", + "jest-snapshot": "30.2.0", + "jest-util": "30.2.0", + "p-limit": "^3.1.0", + "pretty-format": "30.2.0", + "pure-rand": "^7.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "packages/bitcoin/node_modules/jest-cli": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.2.0.tgz", + "integrity": "sha512-Os9ukIvADX/A9sLt6Zse3+nmHtHaE6hqOsjQtNiugFTbKRHYIYtZXNGNK9NChseXy7djFPjndX1tL0sCTlfpAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/types": "30.2.0", + "chalk": "^4.1.2", + "exit-x": "^0.2.2", + "import-local": "^3.2.0", + "jest-config": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", + "yargs": "^17.7.2" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "packages/bitcoin/node_modules/jest-config": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.2.0.tgz", + "integrity": "sha512-g4WkyzFQVWHtu6uqGmQR4CQxz/CH3yDSlhzXMWzNjDx843gYjReZnMRanjRCq5XZFuQrGDxgUaiYWE8BRfVckA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/get-type": "30.1.0", + "@jest/pattern": "30.0.1", + "@jest/test-sequencer": "30.2.0", + "@jest/types": "30.2.0", + "babel-jest": "30.2.0", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "deepmerge": "^4.3.1", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "jest-circus": "30.2.0", + "jest-docblock": "30.2.0", + "jest-environment-node": "30.2.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.2.0", + "jest-runner": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", + "micromatch": "^4.0.8", + "parse-json": "^5.2.0", + "pretty-format": "30.2.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "esbuild-register": ">=3.4.0", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "esbuild-register": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "packages/bitcoin/node_modules/jest-diff": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.2.0.tgz", + "integrity": "sha512-dQHFo3Pt4/NLlG5z4PxZ/3yZTZ1C7s9hveiOj+GCN+uT109NC2QgsoVZsVOAvbJ3RgKkvyLGXZV9+piDpWbm6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/diff-sequences": "30.0.1", + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "packages/bitcoin/node_modules/jest-docblock": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.2.0.tgz", + "integrity": "sha512-tR/FFgZKS1CXluOQzZvNH3+0z9jXr3ldGSD8bhyuxvlVUwbeLOGynkunvlTMxchC5urrKndYiwCFC0DLVjpOCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "packages/bitcoin/node_modules/jest-each": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.2.0.tgz", + "integrity": "sha512-lpWlJlM7bCUf1mfmuqTA8+j2lNURW9eNafOy99knBM01i5CQeY5UH1vZjgT9071nDJac1M4XsbyI44oNOdhlDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "@jest/types": "30.2.0", + "chalk": "^4.1.2", + "jest-util": "30.2.0", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "packages/bitcoin/node_modules/jest-environment-node": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.2.0.tgz", + "integrity": "sha512-ElU8v92QJ9UrYsKrxDIKCxu6PfNj4Hdcktcn0JX12zqNdqWHB0N+hwOnnBBXvjLd2vApZtuLUGs1QSY+MsXoNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.2.0", + "@jest/fake-timers": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "jest-mock": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "packages/bitcoin/node_modules/jest-haste-map": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.2.0.tgz", + "integrity": "sha512-sQA/jCb9kNt+neM0anSj6eZhLZUIhQgwDt7cPGjumgLM4rXsfb9kpnlacmvZz3Q5tb80nS+oG/if+NBKrHC+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "anymatch": "^3.1.3", + "fb-watchman": "^2.0.2", + "graceful-fs": "^4.2.11", + "jest-regex-util": "30.0.1", + "jest-util": "30.2.0", + "jest-worker": "30.2.0", + "micromatch": "^4.0.8", + "walker": "^1.0.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.3" + } + }, + "packages/bitcoin/node_modules/jest-leak-detector": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.2.0.tgz", + "integrity": "sha512-M6jKAjyzjHG0SrQgwhgZGy9hFazcudwCNovY/9HPIicmNSBuockPSedAP9vlPK6ONFJ1zfyH/M2/YYJxOz5cdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "packages/bitcoin/node_modules/jest-matcher-utils": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.2.0.tgz", + "integrity": "sha512-dQ94Nq4dbzmUWkQ0ANAWS9tBRfqCrn0bV9AMYdOi/MHW726xn7eQmMeRTpX2ViC00bpNaWXq+7o4lIQ3AX13Hg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "jest-diff": "30.2.0", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "packages/bitcoin/node_modules/jest-message-util": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.2.0.tgz", + "integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.2.0", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "micromatch": "^4.0.8", + "pretty-format": "30.2.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "packages/bitcoin/node_modules/jest-mock": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.2.0.tgz", + "integrity": "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "jest-util": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "packages/bitcoin/node_modules/jest-regex-util": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", + "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "packages/bitcoin/node_modules/jest-resolve": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.2.0.tgz", + "integrity": "sha512-TCrHSxPlx3tBY3hWNtRQKbtgLhsXa1WmbJEqBlTBrGafd5fiQFByy2GNCEoGR+Tns8d15GaL9cxEzKOO3GEb2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.2.0", + "jest-pnp-resolver": "^1.2.3", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", + "slash": "^3.0.0", + "unrs-resolver": "^1.7.11" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "packages/bitcoin/node_modules/jest-resolve-dependencies": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.2.0.tgz", + "integrity": "sha512-xTOIGug/0RmIe3mmCqCT95yO0vj6JURrn1TKWlNbhiAefJRWINNPgwVkrVgt/YaerPzY3iItufd80v3lOrFJ2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "30.0.1", + "jest-snapshot": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "packages/bitcoin/node_modules/jest-runner": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.2.0.tgz", + "integrity": "sha512-PqvZ2B2XEyPEbclp+gV6KO/F1FIFSbIwewRgmROCMBo/aZ6J1w8Qypoj2pEOcg3G2HzLlaP6VUtvwCI8dM3oqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.2.0", + "@jest/environment": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-docblock": "30.2.0", + "jest-environment-node": "30.2.0", + "jest-haste-map": "30.2.0", + "jest-leak-detector": "30.2.0", + "jest-message-util": "30.2.0", + "jest-resolve": "30.2.0", + "jest-runtime": "30.2.0", + "jest-util": "30.2.0", + "jest-watcher": "30.2.0", + "jest-worker": "30.2.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "packages/bitcoin/node_modules/jest-runtime": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.2.0.tgz", + "integrity": "sha512-p1+GVX/PJqTucvsmERPMgCPvQJpFt4hFbM+VN3n8TMo47decMUcJbt+rgzwrEme0MQUA/R+1de2axftTHkKckg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.2.0", + "@jest/fake-timers": "30.2.0", + "@jest/globals": "30.2.0", + "@jest/source-map": "30.0.1", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "cjs-module-lexer": "^2.1.0", + "collect-v8-coverage": "^1.0.2", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.2.0", + "jest-message-util": "30.2.0", + "jest-mock": "30.2.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.2.0", + "jest-snapshot": "30.2.0", + "jest-util": "30.2.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "packages/bitcoin/node_modules/jest-snapshot": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.2.0.tgz", + "integrity": "sha512-5WEtTy2jXPFypadKNpbNkZ72puZCa6UjSr/7djeecHWOu7iYhSXSnHScT8wBz3Rn8Ena5d5RYRcsyKIeqG1IyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@babel/generator": "^7.27.5", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1", + "@babel/types": "^7.27.3", + "@jest/expect-utils": "30.2.0", + "@jest/get-type": "30.1.0", + "@jest/snapshot-utils": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "babel-preset-current-node-syntax": "^1.2.0", + "chalk": "^4.1.2", + "expect": "30.2.0", + "graceful-fs": "^4.2.11", + "jest-diff": "30.2.0", + "jest-matcher-utils": "30.2.0", + "jest-message-util": "30.2.0", + "jest-util": "30.2.0", + "pretty-format": "30.2.0", + "semver": "^7.7.2", + "synckit": "^0.11.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "packages/bitcoin/node_modules/jest-util": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz", + "integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "packages/bitcoin/node_modules/jest-validate": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.2.0.tgz", + "integrity": "sha512-FBGWi7dP2hpdi8nBoWxSsLvBFewKAg0+uSQwBaof4Y4DPgBabXgpSYC5/lR7VmnIlSpASmCi/ntRWPbv7089Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "@jest/types": "30.2.0", + "camelcase": "^6.3.0", + "chalk": "^4.1.2", + "leven": "^3.1.0", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "packages/bitcoin/node_modules/jest-watcher": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.2.0.tgz", + "integrity": "sha512-PYxa28dxJ9g777pGm/7PrbnMeA0Jr7osHP9bS7eJy9DuAjMgdGtxgf0uKMyoIsTWAkIbUW5hSDdJ3urmgXBqxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "jest-util": "30.2.0", + "string-length": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "packages/bitcoin/node_modules/jest-worker": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.2.0.tgz", + "integrity": "sha512-0Q4Uk8WF7BUwqXHuAjc23vmopWJw5WH7w2tqBoUOZpOjW/ZnR44GXXd1r82RvnmI2GZge3ivrYXk/BE2+VtW2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@ungap/structured-clone": "^1.3.0", + "jest-util": "30.2.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "packages/bitcoin/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "packages/bitcoin/node_modules/pretty-format": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", + "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "packages/bitcoin/node_modules/pure-rand": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", + "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "packages/bitcoin/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "packages/bitcoin/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "packages/bitcoin/node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "packages/bitcoin/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "packages/bitcoin/node_modules/write-file-atomic": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "packages/configs": { "name": "@meshsdk/configs", "version": "0.0.0", @@ -37664,7 +38937,7 @@ }, "packages/mesh-common": { "name": "@meshsdk/common", - "version": "1.9.0-beta.80", + "version": "1.9.0-beta.81", "license": "Apache-2.0", "dependencies": { "bech32": "^2.0.0", @@ -37682,11 +38955,11 @@ }, "packages/mesh-contract": { "name": "@meshsdk/contract", - "version": "1.9.0-beta.80", + "version": "1.9.0-beta.81", "license": "Apache-2.0", "dependencies": { - "@meshsdk/common": "1.9.0-beta.80", - "@meshsdk/core": "1.9.0-beta.80" + "@meshsdk/common": "1.9.0-beta.81", + "@meshsdk/core": "1.9.0-beta.81" }, "devDependencies": { "@meshsdk/configs": "*", @@ -37697,15 +38970,15 @@ }, "packages/mesh-core": { "name": "@meshsdk/core", - "version": "1.9.0-beta.80", + "version": "1.9.0-beta.81", "license": "Apache-2.0", "dependencies": { - "@meshsdk/common": "1.9.0-beta.80", - "@meshsdk/core-cst": "1.9.0-beta.80", - "@meshsdk/provider": "1.9.0-beta.80", - "@meshsdk/react": "1.9.0-beta.80", - "@meshsdk/transaction": "1.9.0-beta.80", - "@meshsdk/wallet": "1.9.0-beta.80" + "@meshsdk/common": "1.9.0-beta.81", + "@meshsdk/core-cst": "1.9.0-beta.81", + "@meshsdk/provider": "1.9.0-beta.81", + "@meshsdk/react": "1.9.0-beta.81", + "@meshsdk/transaction": "1.9.0-beta.81", + "@meshsdk/wallet": "1.9.0-beta.81" }, "devDependencies": { "@meshsdk/configs": "*", @@ -37716,10 +38989,10 @@ }, "packages/mesh-core-csl": { "name": "@meshsdk/core-csl", - "version": "1.9.0-beta.80", + "version": "1.9.0-beta.81", "license": "Apache-2.0", "dependencies": { - "@meshsdk/common": "1.9.0-beta.80", + "@meshsdk/common": "1.9.0-beta.81", "@sidan-lab/whisky-js-browser": "^1.0.11", "@sidan-lab/whisky-js-nodejs": "^1.0.11", "@types/base32-encoding": "^1.0.2", @@ -37729,7 +39002,7 @@ }, "devDependencies": { "@meshsdk/configs": "*", - "@meshsdk/provider": "1.9.0-beta.80", + "@meshsdk/provider": "1.9.0-beta.81", "@types/json-bigint": "^1.0.4", "eslint": "^8.57.0", "ts-jest": "^29.1.4", @@ -37739,7 +39012,7 @@ }, "packages/mesh-core-cst": { "name": "@meshsdk/core-cst", - "version": "1.9.0-beta.80", + "version": "1.9.0-beta.81", "license": "Apache-2.0", "dependencies": { "@cardano-sdk/core": "^0.45.5", @@ -37750,7 +39023,7 @@ "@harmoniclabs/pair": "^1.0.0", "@harmoniclabs/plutus-data": "1.2.4", "@harmoniclabs/uplc": "1.2.4", - "@meshsdk/common": "1.9.0-beta.80", + "@meshsdk/common": "1.9.0-beta.81", "@types/base32-encoding": "^1.0.2", "base32-encoding": "^1.0.0", "bech32": "^2.0.0", @@ -37769,11 +39042,11 @@ }, "packages/mesh-hydra": { "name": "@meshsdk/hydra", - "version": "1.9.0-beta.80", + "version": "1.9.0-beta.81", "dependencies": { - "@meshsdk/common": "1.9.0-beta.80", - "@meshsdk/core": "1.9.0-beta.80", - "@meshsdk/core-cst": "1.9.0-beta.80", + "@meshsdk/common": "1.9.0-beta.81", + "@meshsdk/core": "1.9.0-beta.81", + "@meshsdk/core-cst": "1.9.0-beta.81", "axios": "^1.7.2" }, "devDependencies": { @@ -37825,12 +39098,12 @@ }, "packages/mesh-provider": { "name": "@meshsdk/provider", - "version": "1.9.0-beta.80", + "version": "1.9.0-beta.81", "license": "Apache-2.0", "dependencies": { - "@meshsdk/bitcoin": "1.9.0-beta.80", - "@meshsdk/common": "1.9.0-beta.80", - "@meshsdk/core-cst": "1.9.0-beta.80", + "@meshsdk/bitcoin": "1.9.0-beta.81", + "@meshsdk/common": "1.9.0-beta.81", + "@meshsdk/core-cst": "1.9.0-beta.81", "@utxorpc/sdk": "^0.6.7", "@utxorpc/spec": "^0.16.0", "axios": "^1.7.2", @@ -37846,14 +39119,14 @@ }, "packages/mesh-react": { "name": "@meshsdk/react", - "version": "1.9.0-beta.80", + "version": "1.9.0-beta.81", "license": "Apache-2.0", "dependencies": { "@fabianbormann/cardano-peer-connect": "^1.2.18", - "@meshsdk/bitcoin": "1.9.0-beta.80", - "@meshsdk/common": "1.9.0-beta.80", - "@meshsdk/transaction": "1.9.0-beta.80", - "@meshsdk/wallet": "1.9.0-beta.80", + "@meshsdk/bitcoin": "1.9.0-beta.81", + "@meshsdk/common": "1.9.0-beta.81", + "@meshsdk/transaction": "1.9.0-beta.81", + "@meshsdk/wallet": "1.9.0-beta.81", "@meshsdk/web3-sdk": "0.0.50", "@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-dropdown-menu": "^2.1.2", @@ -37891,10 +39164,10 @@ }, "packages/mesh-svelte": { "name": "@meshsdk/svelte", - "version": "1.9.0-beta.80", + "version": "1.9.0-beta.81", "license": "Apache-2.0", "dependencies": { - "@meshsdk/core": "1.9.0-beta.80", + "@meshsdk/core": "1.9.0-beta.81", "bits-ui": "1.0.0-next.65" }, "devDependencies": { @@ -37920,14 +39193,14 @@ }, "packages/mesh-transaction": { "name": "@meshsdk/transaction", - "version": "1.9.0-beta.80", + "version": "1.9.0-beta.81", "license": "Apache-2.0", "dependencies": { "@cardano-sdk/core": "^0.45.5", "@cardano-sdk/input-selection": "^0.13.33", "@cardano-sdk/util": "^0.15.5", - "@meshsdk/common": "1.9.0-beta.80", - "@meshsdk/core-cst": "1.9.0-beta.80", + "@meshsdk/common": "1.9.0-beta.81", + "@meshsdk/core-cst": "1.9.0-beta.81", "json-bigint": "^1.0.0" }, "devDependencies": { @@ -37940,12 +39213,12 @@ }, "packages/mesh-wallet": { "name": "@meshsdk/wallet", - "version": "1.9.0-beta.80", + "version": "1.9.0-beta.81", "license": "Apache-2.0", "dependencies": { - "@meshsdk/common": "1.9.0-beta.80", - "@meshsdk/core-cst": "1.9.0-beta.80", - "@meshsdk/transaction": "1.9.0-beta.80", + "@meshsdk/common": "1.9.0-beta.81", + "@meshsdk/core-cst": "1.9.0-beta.81", + "@meshsdk/transaction": "1.9.0-beta.81", "@simplewebauthn/browser": "^13.0.0" }, "devDependencies": { @@ -37958,35 +39231,19 @@ }, "packages/midnight-setup": { "name": "@meshsdk/midnight-setup", - "version": "1.9.0-beta.79", - "license": "MIT", + "version": "1.9.0-beta.81", + "license": "Apache-2.0", "dependencies": { "@midnight-ntwrk/compact-runtime": "^0.8.1", - "@midnight-ntwrk/ledger": "^4.0.0", "@midnight-ntwrk/midnight-js-contracts": "2.0.2", "@midnight-ntwrk/midnight-js-types": "2.0.2", - "@midnight-ntwrk/zswap": "4.0.0", - "@radix-ui/react-dialog": "^1.1.14", - "@radix-ui/react-label": "^2.1.7", - "@radix-ui/react-progress": "^1.1.7", - "@radix-ui/react-slot": "^1.2.3", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", - "fp-ts": "^2.16.1", - "lucide-react": "^0.523.0", "pino": "^8.16.1", - "rxjs": "^7.8.1", - "semver": "^7.6.0", - "tailwind-merge": "^3.3.1" + "rxjs": "^7.8.1" }, "devDependencies": { "@meshsdk/configs": "*", - "@types/react": "^18.2.61", - "@types/react-dom": "^18.2.19", "@types/ws": "^8.5.9", "eslint": "^8.57.0", - "react": "^18.2.0", - "react-dom": "^18.2.0", "tsup": "^8.0.2", "typescript": "^5.3.3" }, @@ -37996,32 +39253,12 @@ "@midnight-ntwrk/midnight-js-http-client-proof-provider": "^2.0.2", "@midnight-ntwrk/midnight-js-indexer-public-data-provider": "^2.0.2", "@midnight-ntwrk/midnight-js-level-private-state-provider": "^2.0.2", - "@midnight-ntwrk/midnight-js-network-id": "^2.0.2", - "react": ">=18.0.0", - "react-dom": ">=18.0.0" - }, - "peerDependenciesMeta": { - "react": { - "optional": true - }, - "react-dom": { - "optional": true - } - } - }, - "packages/midnight-setup/node_modules/tailwind-merge": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz", - "integrity": "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/dcastil" + "@midnight-ntwrk/midnight-js-network-id": "^2.0.2" } }, "scripts/mesh-cli": { "name": "meshjs", - "version": "1.9.0-beta.80", + "version": "1.9.0-beta.81", "license": "Apache-2.0", "dependencies": { "@sidan-lab/cardano-bar": "^0.0.7", diff --git a/package.json b/package.json index ce0042bd0..3afc72dcf 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,9 @@ "scripts/*" ], "dependencies": { + "@bitcoinerlab/coinselect": "^1.3.2", + "@bitcoinerlab/descriptors": "^2.3.1", + "@bitcoinerlab/secp256k1": "^1.2.0", "npm": "^10.8.0" } -} \ No newline at end of file +} diff --git a/packages/bitcoin/.env.example b/packages/bitcoin/.env.example new file mode 100644 index 000000000..1d0f58d07 --- /dev/null +++ b/packages/bitcoin/.env.example @@ -0,0 +1,2 @@ +# API keys for running unit test cases +MAESTRO_BITCOIN_API_KEY_TESTNET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx diff --git a/packages/bitcoin/jest.config.ts b/packages/bitcoin/jest.config.ts new file mode 100644 index 000000000..b2d4dafa8 --- /dev/null +++ b/packages/bitcoin/jest.config.ts @@ -0,0 +1,20 @@ +import type { Config } from "jest"; + +const jestConfig: Config = { + clearMocks: true, + maxWorkers: 1, + testEnvironment: "node", + testMatch: ["**/test/**/*.test.ts"], + setupFiles: ["dotenv/config"], + preset: "ts-jest", + moduleNameMapper: { + "^(\\.{1,2}/.*)\\.js$": "$1", + }, + transform: { + "^.+\\.[jt]s?$": "ts-jest", + }, + transformIgnorePatterns: ["/node_modules/(?!@meshsdk/.*)"], + passWithNoTests: true, +}; + +export default jestConfig; \ No newline at end of file diff --git a/packages/bitcoin/package.json b/packages/bitcoin/package.json index 320ece08a..d0e5477f6 100644 --- a/packages/bitcoin/package.json +++ b/packages/bitcoin/package.json @@ -25,7 +25,7 @@ }, "dependencies": { "@bitcoin-js/tiny-secp256k1-asmjs": "^2.2.3", - "bip174": "^3.0.0-rc.1", + "bip174": "^3.0.0", "bip32": "^4.0.0", "bip39": "^3.1.0", "bitcoinjs-lib": "^6.1.7", @@ -34,7 +34,10 @@ "devDependencies": { "@meshsdk/configs": "*", "@swc/core": "^1.10.7", + "dotenv": "^17.2.3", "eslint": "^8.57.0", + "jest": "^30.2.0", + "ts-jest": "^29.4.5", "tsup": "^8.0.2", "typescript": "^5.3.3" } diff --git a/packages/bitcoin/src/interfaces/index.ts b/packages/bitcoin/src/interfaces/index.ts index b479fdc13..1fbbad599 100644 --- a/packages/bitcoin/src/interfaces/index.ts +++ b/packages/bitcoin/src/interfaces/index.ts @@ -1 +1,2 @@ export * from "./wallet"; +export * from "./provider"; diff --git a/packages/bitcoin/src/interfaces/provider.ts b/packages/bitcoin/src/interfaces/provider.ts index 7735bd912..2b5bc01a0 100644 --- a/packages/bitcoin/src/interfaces/provider.ts +++ b/packages/bitcoin/src/interfaces/provider.ts @@ -18,5 +18,6 @@ export interface IBitcoinProvider { ): Promise; fetchScriptUTxOs(hash: string): Promise; fetchTransactionStatus(txid: string): Promise; + fetchFeeEstimates(blocks: number): Promise; submitTx(tx: string): Promise; } diff --git a/packages/bitcoin/src/providers/blockstream.ts b/packages/bitcoin/src/providers/blockstream.ts index 2c0ceb34e..b93dd720a 100644 --- a/packages/bitcoin/src/providers/blockstream.ts +++ b/packages/bitcoin/src/providers/blockstream.ts @@ -162,6 +162,31 @@ export class BlockstreamProvider implements IBitcoinProvider { } } + /** + * Get fee estimates for confirmation within a specified number of blocks. + * Returns fee rate in sat/vB based on Blockstream's fee estimates API. + * @param blocks - Confirmation target in blocks (1-25, 144, 504, 1008) + * @returns Fee rate in sat/vB + */ + async fetchFeeEstimates(blocks: number): Promise { + try { + const { data, status } = await this._axiosInstance.get("/fee-estimates"); + + if (status === 200) { + const feeEstimates = data as Record; + + // Find the closest available confirmation target + const availableTargets = Object.keys(feeEstimates).map(Number).sort((a, b) => a - b); + const closestTarget = availableTargets.find(target => target >= blocks) || availableTargets[availableTargets.length - 1]; + + return feeEstimates[closestTarget.toString()] || 10; + } + throw parseHttpError(data); + } catch (error) { + throw parseHttpError(error); + } + } + /** * Broadcast a raw transaction to the network. * The transaction should be provided as hex in the request body. The txid will be returned on success. diff --git a/packages/bitcoin/src/providers/common.ts b/packages/bitcoin/src/providers/common.ts index f6038199c..e893891ee 100644 --- a/packages/bitcoin/src/providers/common.ts +++ b/packages/bitcoin/src/providers/common.ts @@ -13,7 +13,7 @@ export const parseHttpError = (error: unknown): string => { }); } - if (error.request && !(error.request instanceof XMLHttpRequest)) { + if (error.request) { return JSON.stringify(error.request); } diff --git a/packages/bitcoin/src/providers/maestro.ts b/packages/bitcoin/src/providers/maestro.ts index 85e25ed90..2ccdd3a83 100644 --- a/packages/bitcoin/src/providers/maestro.ts +++ b/packages/bitcoin/src/providers/maestro.ts @@ -1,37 +1,9 @@ import axios, { AxiosInstance } from "axios"; import { IBitcoinProvider } from "../interfaces/provider"; -import { UTxO } from "../types"; -import { AddressInfo } from "../types/address-info"; -import { ScriptInfo } from "../types/script-info"; -import { TransactionsInfo } from "../types/transactions-info"; -import { TransactionsStatus } from "../types/transactions-status"; +import { UTxO, AddressInfo, ScriptInfo, TransactionsInfo, TransactionsStatus } from "../types"; +import { MaestroSupportedNetworks, MaestroConfig, SatoshiActivityResponse, BalanceResponse } from "../types/maestro"; import { parseHttpError } from "./common"; -export type MaestroSupportedNetworks = "mainnet" | "testnet"; - -export interface MaestroConfig { - network: MaestroSupportedNetworks; - apiKey: string; -} - -type FeeEstimateResponse = { - feerate: number; -}; - -type SatoshiActivityResponse = { - data: Array<{ - tx_hash: string; - block_height: number; - block_time: number; - value: number; - }>; - cursor?: string; -}; - -type BalanceResponse = { - data: number; -}; - /** * Maestro provider for Bitcoin operations. */ @@ -39,18 +11,37 @@ export class MaestroProvider implements IBitcoinProvider { private readonly _axiosInstance: AxiosInstance; private readonly _network: MaestroSupportedNetworks; + /** + * Create provider with custom base URL (for proxy endpoints). + * @param baseUrl - The base URL of the proxy endpoint. + * @param apiKey - The API key for the proxy. + */ + constructor(baseUrl: string, apiKey: string); + /** * Create provider with Maestro configuration. * @param config - The Maestro configuration object. - * @param config.network - The network to use (mainnet or testnet). - * @param config.apiKey - The Maestro API key. */ - constructor({ network, apiKey }: MaestroConfig) { - this._axiosInstance = axios.create({ - baseURL: `https://xbt-${network}.gomaestro-api.org/v0`, - headers: { "api-key": apiKey }, - }); - this._network = network; + constructor(config: MaestroConfig); + + constructor(...args: unknown[]) { + if ( + typeof args[0] === "string" && + (args[0].startsWith("http") || args[0].startsWith("/")) + ) { + this._axiosInstance = axios.create({ + baseURL: args[0], + headers: { "api-key": args[1] as string }, + }); + this._network = args[0].includes("testnet") ? "testnet" : "mainnet"; + } else { + const { network, apiKey } = args[0] as MaestroConfig; + this._axiosInstance = axios.create({ + baseURL: `https://xbt-${network}.gomaestro-api.org/v0`, + headers: { "api-key": apiKey }, + }); + this._network = network; + } } /** @@ -60,7 +51,9 @@ export class MaestroProvider implements IBitcoinProvider { * @note Maestro does not have any endpoint available for this yet */ async fetchScript(hash: string): Promise { - throw new Error("fetchScript is not implemented - Maestro does not have any endpoint available for this yet"); + throw new Error( + "fetchScript is not implemented - Maestro does not have any endpoint available for this yet", + ); } /** @@ -70,8 +63,13 @@ export class MaestroProvider implements IBitcoinProvider { * @returns TransactionsInfo[] * @note Maestro does not have any endpoint available for this yet */ - async fetchScriptTransactions(hash: string, last_seen_txid?: string): Promise { - throw new Error("fetchScriptTransactions is not implemented - Maestro does not have any endpoint available for this yet"); + async fetchScriptTransactions( + hash: string, + last_seen_txid?: string, + ): Promise { + throw new Error( + "fetchScriptTransactions is not implemented - Maestro does not have any endpoint available for this yet", + ); } /** @@ -81,7 +79,9 @@ export class MaestroProvider implements IBitcoinProvider { * @note Maestro does not have any endpoint available for this yet */ async fetchScriptUTxOs(hash: string): Promise { - throw new Error("fetchScriptUTxOs is not implemented - Maestro does not have any endpoint available for this yet"); + throw new Error( + "fetchScriptUTxOs is not implemented - Maestro does not have any endpoint available for this yet", + ); } /** @@ -91,8 +91,10 @@ export class MaestroProvider implements IBitcoinProvider { */ async fetchAddress(address: string): Promise { try { - const { data, status } = await this._axiosInstance.get(`/esplora/address/${address}`); - + const { data, status } = await this._axiosInstance.get( + `/esplora/address/${address}`, + ); + if (status === 200) return data as AddressInfo; throw parseHttpError(data); } catch (error) { @@ -107,13 +109,16 @@ export class MaestroProvider implements IBitcoinProvider { * @param last_seen_txid - The last seen transaction ID (optional). * @returns TransactionsInfo[] */ - async fetchAddressTransactions(address: string, last_seen_txid?: string): Promise { + async fetchAddressTransactions( + address: string, + last_seen_txid?: string, + ): Promise { try { const url = last_seen_txid ? `/esplora/address/${address}/txs/chain/${last_seen_txid}` : `/esplora/address/${address}/txs`; const { data, status } = await this._axiosInstance.get(url); - + if (status === 200) return data as TransactionsInfo[]; throw parseHttpError(data); } catch (error) { @@ -128,8 +133,10 @@ export class MaestroProvider implements IBitcoinProvider { */ async fetchAddressUTxOs(address: string): Promise { try { - const { data, status } = await this._axiosInstance.get(`/esplora/address/${address}/utxo`); - + const { data, status } = await this._axiosInstance.get( + `/esplora/address/${address}/utxo`, + ); + if (status === 200) return data as UTxO[]; throw parseHttpError(data); } catch (error) { @@ -144,8 +151,10 @@ export class MaestroProvider implements IBitcoinProvider { */ async fetchTransactionStatus(txid: string): Promise { try { - const { data, status } = await this._axiosInstance.get(`/esplora/tx/${txid}/status`); - + const { data, status } = await this._axiosInstance.get( + `/esplora/tx/${txid}/status`, + ); + if (status === 200) return data as TransactionsStatus; throw parseHttpError(data); } catch (error) { @@ -160,12 +169,16 @@ export class MaestroProvider implements IBitcoinProvider { */ async submitTx(txHex: string): Promise { try { - const { data, status } = await this._axiosInstance.post('/esplora/tx', txHex, { - headers: { - 'Content-Type': 'application/json', + const { data, status } = await this._axiosInstance.post( + "/esplora/tx", + txHex, + { + headers: { + "Content-Type": "application/json", + }, }, - }); - + ); + if (status === 200) return data.txid || data; throw parseHttpError(data); } catch (error) { @@ -173,23 +186,25 @@ export class MaestroProvider implements IBitcoinProvider { } } - // Additional Bitcoin-specific methods (beyond IBitcoinProvider) /** * Get fee estimates for Bitcoin transactions. * @param blocks - The number of blocks to estimate fees for (default: 6). * @returns FeeEstimateResponse containing the estimated fee rate. */ - async fetchFeeEstimates(blocks: number = 6): Promise { + async fetchFeeEstimates(blocks: number = 6): Promise { try { - const { data, status } = await this._axiosInstance.get(`/rpc/transaction/estimatefee/${blocks}`); - - if (status === 200) return data as FeeEstimateResponse; + const { data, status } = await this._axiosInstance.get( + `/rpc/transaction/estimatefee/${blocks}`, + ); + + if (status === 200) return data.data.feerate; throw parseHttpError(data); } catch (error) { throw parseHttpError(error); } } + // Additional Bitcoin-specific methods (beyond IBitcoinProvider) /** * Fetch satoshi activity for a Bitcoin address (transaction history). * @param address - The Bitcoin address. @@ -204,12 +219,12 @@ export class MaestroProvider implements IBitcoinProvider { async fetchSatoshiActivity( address: string, options: { - order?: 'asc' | 'desc'; + order?: "asc" | "desc"; count?: number; from?: number; to?: number; cursor?: string; - } = {} + } = {}, ): Promise { const params = new URLSearchParams(); @@ -220,11 +235,11 @@ export class MaestroProvider implements IBitcoinProvider { }); const queryString = params.toString(); - const url = `/addresses/${address}/activity${queryString ? `?${queryString}` : ''}`; + const url = `/addresses/${address}/activity${queryString ? `?${queryString}` : ""}`; try { const { data, status } = await this._axiosInstance.get(url); - + if (status === 200) return data as SatoshiActivityResponse; throw parseHttpError(data); } catch (error) { @@ -239,8 +254,10 @@ export class MaestroProvider implements IBitcoinProvider { */ async fetchTxInfo(hash: string): Promise { try { - const { data, status } = await this._axiosInstance.get(`/esplora/tx/${hash}`); - + const { data, status } = await this._axiosInstance.get( + `/esplora/tx/${hash}`, + ); + if (status === 200) return data as TransactionsInfo; throw parseHttpError(data); } catch (error) { @@ -255,8 +272,10 @@ export class MaestroProvider implements IBitcoinProvider { */ async fetchAddressBalance(address: string): Promise { try { - const { data, status } = await this._axiosInstance.get(`/addresses/${address}/balance`); - + const { data, status } = await this._axiosInstance.get( + `/addresses/${address}/balance`, + ); + if (status === 200) return data as BalanceResponse; throw parseHttpError(data); } catch (error) { @@ -282,7 +301,7 @@ export class MaestroProvider implements IBitcoinProvider { async get(url: string): Promise { try { const { data, status } = await this._axiosInstance.get(url); - + if (status === 200) return data; throw parseHttpError(data); } catch (error) { @@ -300,10 +319,10 @@ export class MaestroProvider implements IBitcoinProvider { try { const { data, status } = await this._axiosInstance.post(url, body, { headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, }); - + if (status === 200) return data; throw parseHttpError(data); } catch (error) { @@ -318,4 +337,4 @@ export class MaestroProvider implements IBitcoinProvider { getNetwork(): MaestroSupportedNetworks { return this._network; } -} \ No newline at end of file +} diff --git a/packages/bitcoin/src/types/address.ts b/packages/bitcoin/src/types/address.ts index a038c4686..f7952856f 100644 --- a/packages/bitcoin/src/types/address.ts +++ b/packages/bitcoin/src/types/address.ts @@ -1,6 +1,6 @@ export type Address = { address: string; publicKey?: string; - purpose: "payment" | "ordinals" | "stacks"; - addressType: "p2tr" | "p2wpkh" | "p2sh" | "stacks"; + purpose: "payment" | "ordinals"; + addressType: "p2tr" | "p2wpkh" | "p2sh"; } \ No newline at end of file diff --git a/packages/bitcoin/src/types/index.ts b/packages/bitcoin/src/types/index.ts index c2e16b7d2..b325f0fe7 100644 --- a/packages/bitcoin/src/types/index.ts +++ b/packages/bitcoin/src/types/index.ts @@ -5,4 +5,6 @@ export * from './transactions-info'; export * from './transactions-status'; export * from './script-info'; export * from './chain-stats'; -export * from './mempool-stats'; \ No newline at end of file +export * from './mempool-stats'; +export * from './wallet'; +export * from './maestro'; \ No newline at end of file diff --git a/packages/bitcoin/src/types/maestro.ts b/packages/bitcoin/src/types/maestro.ts new file mode 100644 index 000000000..3585ef447 --- /dev/null +++ b/packages/bitcoin/src/types/maestro.ts @@ -0,0 +1,29 @@ +export type MaestroSupportedNetworks = "mainnet" | "testnet"; + +export interface MaestroConfig { + network: MaestroSupportedNetworks; + apiKey: string; +} + +export interface SatoshiActivity { + confirmations: number; + height: number; + tx_hash: string; + sat_activity: { + kind: "self_transfer" | "increase" | "decrease"; + amount: string; + }; +} + +export interface SatoshiActivityResponse { + data: SatoshiActivity[]; + last_updated: { + block_hash: string; + block_height: number; + }; + next_cursor: string | null; +} + +export type BalanceResponse = { + data: number; +}; \ No newline at end of file diff --git a/packages/bitcoin/src/types/wallet.ts b/packages/bitcoin/src/types/wallet.ts new file mode 100644 index 000000000..1319fc1f6 --- /dev/null +++ b/packages/bitcoin/src/types/wallet.ts @@ -0,0 +1,93 @@ +import { IBitcoinProvider } from "../interfaces/provider"; + +export type NetworkType = "Mainnet" | "Regtest" | "Testnet"; + +export type CreateWalletOptions = { + network: NetworkType; + key: + | { + type: "mnemonic"; + words: string[]; + } + | { + type: "address"; + address: string; + }; + path?: string; + provider?: IBitcoinProvider; +}; + +export type GetAddressResult = { + address: string; + publicKey: string; + purpose: "payment" | "ordinals"; + addressType: "p2tr" | "p2wpkh" | "p2sh"; + network: "mainnet" | "testnet" | "regtest"; + walletType: "software" | "ledger"; +}; + +export type SignMessageParams = { + address: string; + message: string; + protocol?: "ECDSA" | "BIP322"; +}; + +export type SignMessageResult = { + signature: string; + messageHash: string; + address: string; +}; + +export type SignPsbtParams = { + psbt: string; + signInputs: Record; + broadcast?: boolean; +}; + +export type SignPsbtResult = { + psbt: string; + txid?: string; +}; + +export type SendTransferParams = { + recipients: Array<{ + address: string; + amount: number; + }>; +}; + +export type SendTransferResult = { + txid: string; +}; + +export type GetBalanceResult = { + confirmed: string; + unconfirmed: string; + total: string; +}; + +export type SignMultipleTransactionsParams = { + network: { type: NetworkType }; + message: string; + psbts: Array<{ + psbtBase64: string; + inputsToSign: Array<{ + address: string; + signingIndexes: number[]; + sigHash?: number; + }>; + }>; +}; + +// Keep legacy type for backward compatibility +export type TransactionPayload = { + inputs: { + txid: string; + vout: number; + value: number; + }[]; + outputs: { + address: string; + value: number; + }[]; +}; \ No newline at end of file diff --git a/packages/bitcoin/src/wallets/embedded/index.ts b/packages/bitcoin/src/wallets/embedded/index.ts index d326bf97d..1ed42414b 100644 --- a/packages/bitcoin/src/wallets/embedded/index.ts +++ b/packages/bitcoin/src/wallets/embedded/index.ts @@ -1,39 +1,23 @@ -import { bitcoin, bip32, bip39, ECPair } from "../../core"; -import { BIP32Interface } from "bip32"; -import { UTxO } from "../../types/utxo"; -import { Address } from "../../types/address"; -import { IBitcoinProvider } from "../../interfaces/provider"; import type { Network } from "bitcoinjs-lib"; +import { BIP32Interface } from "bip32"; import { mnemonicToSeedSync, validateMnemonic } from "bip39"; +import { bip32, bip39, bitcoin, ECPair } from "../../core"; +import { IBitcoinProvider } from "../../interfaces/provider"; +import { UTxO } from "../../types/utxo"; +import { + CreateWalletOptions, + GetAddressResult, + GetBalanceResult, + SendTransferParams, + SendTransferResult, + SignMessageParams, + SignMessageResult, + SignMultipleTransactionsParams, + SignPsbtParams, + SignPsbtResult, +} from "../../types/wallet"; import { resolveAddress } from "../../utils"; -export type CreateWalletOptions = { - testnet: boolean; - key: - | { - type: "mnemonic"; - words: string[]; - } - | { - type: "address"; - address: string; - }; - path?: string; - provider?: IBitcoinProvider; -}; - -export type TransactionPayload = { - inputs: { - txid: string; - vout: number; - value: number; - }[]; - outputs: { - address: string; - value: number; - }[]; -}; - /** * EmbeddedWallet is a class that provides a simple interface to interact with Bitcoin wallets. */ @@ -45,15 +29,24 @@ export class EmbeddedWallet { private readonly _address?: string; constructor(options: CreateWalletOptions) { - this._network = options.testnet - ? bitcoin.networks.testnet - : bitcoin.networks.bitcoin; + switch (options.network) { + case "Testnet": + this._network = bitcoin.networks.testnet; + break; + case "Regtest": + this._network = bitcoin.networks.regtest; + break; + case "Mainnet": + default: + this._network = bitcoin.networks.bitcoin; + break; + } if (options.key.type === "mnemonic") { this._wallet = _derive( options.key.words, options.path ?? "m/84'/0'/0'/0/0", - this._network + this._network, ); this._isReadOnly = false; } else { @@ -66,41 +59,58 @@ export class EmbeddedWallet { } /** - * Returns the wallet's SegWit (P2WPKH) address and associated public key. + * Returns the wallet's Bitcoin addresses following Xverse API format. * - * @returns {Address} The wallet address object including address, public key, and metadata. - * @throws {Error} If internal address or public key is not properly initialized. + * @param purposes Array of address purposes to request: + * - `'ordinals'` for managing ordinals (should return P2TR taproot address) + * - `'payment'` for managing bitcoin (returns P2WPKH address) + * TODO: Should follow Xverse docs to return taproot address for ordinals purpose + * @param message Optional message to display in request prompt (ignored for embedded wallets) + * @returns {Promise} Array of address objects with address, publicKey, purpose, addressType, network, and walletType + * @throws {Error} If wallet is not properly initialized. */ - getAddress(): Address { + async getAddresses( + purposes?: Array<"payment" | "ordinals">, + message?: string, + ): Promise { + // TODO: Implement full Xverse API compliance + // - Default to both purposes if not specified: ["payment", "ordinals"] + // - Generate payment address (P2WPKH) when "payment" is requested + // - Generate ordinals address (P2TR - Taproot) when "ordinals" is requested + // - Filter addresses based on requested purposes + // + // Note: `message` parameter is used by Xverse to show custom prompts to users, + // but embedded wallets don't have UI prompts, so this parameter is ignored here + if (this._isReadOnly && this._address) { - return { - address: this._address, - purpose: "payment", - addressType: "p2wpkh", - }; + return [ + { + address: this._address, + publicKey: "", // Not available for read-only wallets + purpose: "payment", + addressType: "p2wpkh", + network: this._getNetworkString(), + walletType: "software", + }, + ]; } if (!this._wallet) { throw new Error("Wallet not initialized properly."); } - return resolveAddress(this._wallet.publicKey, this._network); - - // const p2wpkh = bitcoin.payments.p2wpkh({ - // pubkey: this._wallet.publicKey, - // network: this._network, - // }); - - // if (!p2wpkh?.address) { - // throw new Error("Address is not initialized."); - // } - - // return { - // address: p2wpkh.address, - // publicKey: this._wallet.publicKey.toString("hex"), - // purpose: "payment", - // addressType: "p2wpkh", - // }; + const addressInfo = resolveAddress(this._wallet.publicKey, this._network); + return [ + { + address: addressInfo.address, + publicKey: + addressInfo.publicKey || this._wallet.publicKey.toString("hex"), + purpose: "payment", + addressType: addressInfo.addressType, + network: this._getNetworkString(), + walletType: "software", + }, + ]; } /** @@ -123,13 +133,25 @@ export class EmbeddedWallet { /** * Returns the network identifier of the wallet. - * 0': Indicates the Bitcoin mainnet. - * 1': Indicates the Bitcoin testnet. + * 0: Indicates the Bitcoin testnet. + * 1: Indicates the Bitcoin mainnet. + * 2: Indicates the Bitcoin regtest. * - * @returns {0 | 1} The Bitcoin network ID. + * @returns {0 | 1 | 2} The Bitcoin network ID. + */ + getNetworkId(): 0 | 1 | 2 { + if (this._network === bitcoin.networks.testnet) return 0; + if (this._network === bitcoin.networks.regtest) return 2; + return 1; // mainnet + } + + /** + * Returns the network type as a string for API responses. */ - getNetworkId(): 0 | 1 { - return this._network === bitcoin.networks.testnet ? 1 : 0; + private _getNetworkString(): "mainnet" | "testnet" | "regtest" { + if (this._network === bitcoin.networks.testnet) return "testnet"; + if (this._network === bitcoin.networks.regtest) return "regtest"; + return "mainnet"; } /** @@ -137,22 +159,31 @@ export class EmbeddedWallet { * @returns An array of UTXOs. */ async getUTxOs(): Promise { - const address = this.getAddress(); + const address = await this.getAddresses(); if (this._provider === undefined) { throw new Error("`provider` is not defined. Provide a BitcoinProvider."); } - return await this._provider.fetchAddressUTxOs(address.address); + return await this._provider.fetchAddressUTxOs(address[0].address); } /** - * Signs a given message using the wallet's private key. + * Signs a message following Xverse API format. * - * @param message - The message to be signed. - * @returns The signature of the message as a string. + * @param params - Object containing address, message, and optional protocol + * @returns Promise resolving to signature result * @throws {Error} If the wallet is read-only or private key is not available. */ - async signData(message: string): Promise { + async signMessage(params: SignMessageParams): Promise { + const { address, message, protocol = "ECDSA" } = params; + + // TODO: Implement BIP322 support for message signing + if (protocol === "BIP322") { + throw new Error( + "BIP322 protocol is not yet supported. Only ECDSA is currently available.", + ); + } + if (this._isReadOnly) { throw new Error("Cannot sign data with a read-only wallet."); } @@ -161,6 +192,10 @@ export class EmbeddedWallet { throw new Error("Private key is not available for signing."); } + // TODO: This uses legacy message signing format which may not be appropriate for SegWit addresses + // Since we're using BIP84 derivation path (m/84'/0'/0'/0/0) for native SegWit (P2WPKH), + // we should implement BIP322 message signing for proper SegWit address ownership proof + // Create ECPair from private key const keyPair = ECPair.fromPrivateKey(this._wallet.privateKey, { compressed: true, @@ -176,17 +211,22 @@ export class EmbeddedWallet { const hash = bitcoin.crypto.hash256(bufferToHash); // Sign the hash const signature = keyPair.sign(hash); - // DER encode and return as base64 - return signature.toString("base64"); + + return { + signature: signature.toString("base64"), + messageHash: hash.toString("hex"), + address: address, + }; } /** - * Sign a transaction payload. - * @param payload - The transaction payload to sign. - * @returns The signed transaction in hex format. + * Sign a PSBT following Xverse API format. + * @param params - Object containing PSBT and signing instructions + * @returns Promise resolving to signed PSBT result * @throws {Error} If the wallet is read-only or private key is not available. */ - async signTx(payload: TransactionPayload): Promise { + async signPsbt(params: SignPsbtParams): Promise { + const { psbt: psbtBase64, signInputs, broadcast = false } = params; if (this._isReadOnly) { throw new Error("Cannot sign transactions with a read-only wallet."); } @@ -195,44 +235,244 @@ export class EmbeddedWallet { throw new Error("Private key is not available for signing."); } - const psbt = new bitcoin.Psbt({ network: this._network }); - const p2wpkh = bitcoin.payments.p2wpkh({ - pubkey: this._wallet.publicKey, + const psbt = bitcoin.Psbt.fromBase64(psbtBase64, { network: this._network, }); + const ecPair = ECPair.fromPrivateKey(this._wallet.privateKey, { network: this._network, }); - for (const input of payload.inputs) { - psbt.addInput({ - hash: input.txid, - index: input.vout, - witnessUtxo: { - script: p2wpkh.output!, - value: input.value, - }, - }); + // Sign the specified inputs + const allInputIndexes = Object.values(signInputs).flat(); + allInputIndexes.forEach((inputIndex) => { + psbt.signInput(inputIndex, this._wallet!); + psbt.validateSignaturesOfInput( + inputIndex, + (pubkey, hash, signature) => + ecPair.publicKey.equals(pubkey) && ecPair.verify(hash, signature), + ); + }); + + const signedPsbt = psbt.toBase64(); + + if (broadcast) { + if (!this._provider) { + throw new Error( + "`provider` is not defined. Provide a BitcoinProvider for broadcasting.", + ); + } + + psbt.finalizeAllInputs(); + const txHex = psbt.extractTransaction().toHex(); + const txid = await this._provider.submitTx(txHex); + + return { + psbt: signedPsbt, + txid: txid, + }; + } + + return { + psbt: signedPsbt, + }; + } + + /** + * Send Bitcoin to recipients following Xverse API format. + * @param params - Object containing recipients array + * @returns Promise resolving to transaction result + * @throws {Error} If the wallet is read-only or provider is not available. + */ + async sendTransfer(params: SendTransferParams): Promise { + if (this._isReadOnly) { + throw new Error("Cannot send transactions with a read-only wallet."); + } + + if (!this._provider) { + throw new Error("`provider` is not defined. Provide a BitcoinProvider for sending."); + } + + if (!this._wallet) { + throw new Error("Wallet not initialized properly."); + } + + const { recipients } = params; + + const [addresses, utxos] = await Promise.all([ + this.getAddresses(), + this.getUTxOs(), + ]); + + const walletAddress = addresses[0].address; + const psbt = await this._buildTransferPsbt( + utxos, + recipients, + walletAddress, + ); + + // Sign and broadcast using signPsbt + const inputCount = psbt.inputCount; + const signInputs: Record = { + [walletAddress]: Array.from({ length: inputCount }, (_, i) => i), + }; + + const signResult = await this.signPsbt({ + psbt: psbt.toBase64(), + signInputs, + broadcast: true, + }); + + return { txid: signResult.txid! }; + } + + /** + * Get wallet balance following Xverse API format. + * @returns Promise resolving to balance information + * @throws {Error} If provider is not available. + */ + async getBalance(): Promise { + if (!this._provider) { + throw new Error( + "`provider` is not defined. Provide a BitcoinProvider for balance.", + ); + } + + const addresses = await this.getAddresses(); + const address = addresses[0].address; + + const addressInfo = await this._provider.fetchAddress(address); + const confirmed = addressInfo.chain_stats.funded_txo_sum - addressInfo.chain_stats.spent_txo_sum; + const unconfirmed = addressInfo.mempool_stats.funded_txo_sum - addressInfo.mempool_stats.spent_txo_sum; + const total = confirmed + unconfirmed; + + return { + confirmed: confirmed.toString(), + unconfirmed: unconfirmed.toString(), + total: total.toString(), + }; + } + + /** + * Sign multiple transactions (Xverse custom method). + * @param params - Object containing network, message, and PSBTs array + * @returns Promise resolving to signed PSBTs + * @throws {Error} If the wallet is read-only or private key is not available. + */ + async signMultipleTransactions( + params: SignMultipleTransactionsParams, + ): Promise { + if (!this._wallet || !this._wallet.privateKey) { + throw new Error("Private key is not available for signing."); } - for (const output of payload.outputs) { - psbt.addOutput({ - address: output.address, - value: output.value, + const { psbts } = params; + const results: SignPsbtResult[] = []; + + for (const psbtInfo of psbts) { + const psbt = bitcoin.Psbt.fromBase64(psbtInfo.psbtBase64, { + network: this._network, + }); + + // Sign all specified input indexes + const inputIndexes = psbtInfo.inputsToSign.flatMap( + (input) => input.signingIndexes, + ); + inputIndexes.forEach((index) => psbt.signInput(index, this._wallet!)); + + results.push({ + psbt: psbt.toBase64(), }); } - for (let i = 0; i < payload.inputs.length; i++) { - psbt.signInput(i, this._wallet); - psbt.validateSignaturesOfInput(i, (pubkey, hash, signature) => { - return ( - ecPair.publicKey.equals(pubkey) && ecPair.verify(hash, signature) - ); + return results; + } + + /** + * Simple largest-first coin selection algorithm. + * Selects UTXOs in descending order by value until target amount + fees is reached. + * + * @param utxos Available UTXOs + * @param targetAmount Amount needed in satoshis + * @param feeRate Fee rate in sat/vByte + * @returns Selected UTXOs and change amount + */ + private _selectUtxosLargestFirst(utxos: UTxO[], targetAmount: number, feeRate: number): + { selectedUtxos: UTxO[], change: number } { + + // Sort UTXOs by value (descending) - largest first + const sortedUtxos = [...utxos].sort((a, b) => b.value - a.value); + + let selectedValue = 0; + const selectedUtxos: UTxO[] = []; + + // Accumulate UTXOs until we have enough + for (const utxo of sortedUtxos) { + selectedUtxos.push(utxo); + selectedValue += utxo.value; + + // Calculate fee with current selection (rough estimate) + const estimatedTxSize = selectedUtxos.length * 150 + 2 * 34 + 10; // inputs + outputs + overhead + const fee = Math.ceil(estimatedTxSize * feeRate); + + // Check if we have enough + if (selectedValue >= targetAmount + fee) { + const finalFee = Math.ceil((selectedUtxos.length * 150 + 2 * 34 + 10) * feeRate); + const change = selectedValue - targetAmount - finalFee; + return { selectedUtxos, change }; + } + } + + throw new Error("Insufficient funds for transaction."); + } + + /** + * Build PSBT for transfer using optimal coin selection. + * @param utxos Available UTXOs + * @param recipients Transfer recipients + * @param walletAddress Wallet address for change + * @returns Built PSBT ready for signing + */ + private async _buildTransferPsbt( + utxos: UTxO[], + recipients: any[], + walletAddress: string, + ): Promise { + let feeRate = 10; // Default fallback + if (this._provider) { + try { + feeRate = await this._provider.fetchFeeEstimates(6); + } catch (error) { + console.warn("Fee estimation failed, using default rate:", error); + } + } + + // Use simple largest-first coin selection + const targetAmount = recipients.reduce((sum, r) => sum + r.amount, 0); + const { selectedUtxos, change } = this._selectUtxosLargestFirst(utxos, targetAmount, feeRate); + const psbt = new bitcoin.Psbt({ network: this._network }); + const p2wpkh = bitcoin.payments.p2wpkh({ + pubkey: this._wallet!.publicKey, + network: this._network, + }); + + selectedUtxos.forEach(utxo => { + psbt.addInput({ + hash: utxo.txid, + index: utxo.vout, + witnessUtxo: { script: p2wpkh.output!, value: utxo.value }, }); + }); + + recipients.forEach(recipient => { + psbt.addOutput({ address: recipient.address, value: recipient.amount }); + }); + + if (change > 0) { + psbt.addOutput({ address: walletAddress, value: change }); } - psbt.finalizeAllInputs(); - return psbt.extractTransaction().toHex(); + return psbt; } /** @@ -245,7 +485,7 @@ export class EmbeddedWallet { static brew(strength: number = 128): string[] { if (![128, 160, 192, 224, 256].includes(strength)) { throw new Error( - "Invalid strength. Must be one of: 128, 160, 192, 224, 256." + "Invalid strength. Must be one of: 128, 160, 192, 224, 256.", ); } @@ -257,7 +497,7 @@ export class EmbeddedWallet { function _derive( words: string[], path: string = "m/84'/0'/0'/0/0", - network?: Network + network?: Network, ): BIP32Interface { const mnemonic = words.join(" "); @@ -296,7 +536,7 @@ function varIntBuffer(n: number): Buffer { export function verifySignature( message: string, signatureBase64: string, - publicKeyHex: string + publicKeyHex: string, ): boolean { try { const messageBuffer = Buffer.from(message, "utf8"); diff --git a/packages/bitcoin/test/providers/blockstream/blockstream-integration.test.ts b/packages/bitcoin/test/providers/blockstream/blockstream-integration.test.ts new file mode 100644 index 000000000..d5c9ff9f4 --- /dev/null +++ b/packages/bitcoin/test/providers/blockstream/blockstream-integration.test.ts @@ -0,0 +1,172 @@ +import { BlockstreamProvider } from "../../../src/providers/blockstream"; + +describe("Blockstream Integration Tests", () => { + let provider: BlockstreamProvider; + + // Known testnet addresses for testing + const testAddressWithHistory = "tb1q6rz28mcfaxtmd6v789l9rrlrusdprr9pqcpvkl"; + const testAddressEmpty = "tb1qcr8te4kr609gcawutmrza0j4xv80jy8zmfp6l0"; + + beforeAll(() => { + provider = new BlockstreamProvider("testnet"); + }); + + describe("Address operations", () => { + it("should fetch address info successfully", async () => { + const addressInfo = await provider.fetchAddress(testAddressWithHistory); + + expect(addressInfo).toMatchObject({ + address: testAddressWithHistory, + chain_stats: expect.objectContaining({ + funded_txo_count: expect.any(Number), + funded_txo_sum: expect.any(Number), + spent_txo_count: expect.any(Number), + spent_txo_sum: expect.any(Number), + tx_count: expect.any(Number), + }), + mempool_stats: expect.objectContaining({ + funded_txo_count: expect.any(Number), + funded_txo_sum: expect.any(Number), + spent_txo_count: expect.any(Number), + spent_txo_sum: expect.any(Number), + tx_count: expect.any(Number), + }), + }); + }); + + it("should fetch address UTxOs", async () => { + const utxos = await provider.fetchAddressUTxOs(testAddressWithHistory); + + expect(Array.isArray(utxos)).toBe(true); + // UTxOs may be empty, but should return valid array + if (utxos.length > 0) { + expect(utxos[0]).toMatchObject({ + txid: expect.any(String), + vout: expect.any(Number), + value: expect.any(Number), + status: expect.objectContaining({ + confirmed: expect.any(Boolean), + }), + }); + } + }); + + it("should fetch address transactions", async () => { + const transactions = await provider.fetchAddressTransactions(testAddressWithHistory); + + expect(Array.isArray(transactions)).toBe(true); + // May have no transactions, but should return valid array + if (transactions.length > 0) { + expect(transactions[0]).toMatchObject({ + txid: expect.any(String), + version: expect.any(Number), + vin: expect.any(Array), + vout: expect.any(Array), + }); + } + }); + + it("should handle empty address gracefully", async () => { + const addressInfo = await provider.fetchAddress(testAddressEmpty); + + expect(addressInfo).toMatchObject({ + address: testAddressEmpty, + chain_stats: expect.objectContaining({ + tx_count: expect.any(Number), + }), + }); + }); + }); + + describe("Fee estimation", () => { + it("should fetch fee estimates", async () => { + const feeEstimates = await provider.fetchFeeEstimates(6); + + expect(typeof feeEstimates).toBe("number"); + expect(feeEstimates).toBeGreaterThan(0); + }); + + it("should handle different block targets", async () => { + const fee6 = await provider.fetchFeeEstimates(6); + const fee12 = await provider.fetchFeeEstimates(12); + const fee24 = await provider.fetchFeeEstimates(24); + + expect(typeof fee6).toBe("number"); + expect(typeof fee12).toBe("number"); + expect(typeof fee24).toBe("number"); + + // Generally, longer confirmation times have lower fees + expect(fee6).toBeGreaterThanOrEqual(fee24); + }); + }); + + describe("Transaction operations", () => { + it("should handle non-existent transaction status requests", async () => { + const nonExistentTxId = "0000000000000000000000000000000000000000000000000000000000000000"; + + // Blockstream returns {confirmed: false} for non-existent transactions + const status = await provider.fetchTransactionStatus(nonExistentTxId); + expect(status).toMatchObject({ + confirmed: false, + }); + }); + + it("should return error for invalid transaction hex", async () => { + const invalidTxHex = "invalid-hex-data"; + + try { + await provider.submitTx(invalidTxHex); + fail("Expected API to throw error for invalid transaction hex"); + } catch (error) { + expect(typeof error).toBe("string"); + expect(error).toContain("error"); + } + }); + }); + + describe("Error handling", () => { + it("should return base58 error for invalid address format", async () => { + const invalidAddress = "invalid-address-format"; + try { + await provider.fetchAddress(invalidAddress); + fail("Expected API to throw error for invalid address"); + } catch (error) { + // parseHttpError returns JSON strings (mesh standard) + expect(typeof error).toBe("string"); + const errorObj = JSON.parse(error as string); + expect(errorObj).toMatchObject({ + data: "base58 error", + status: 400, + }); + } + }); + + it("should return error for malformed addresses", async () => { + // Test with malformed but valid-length address + const malformedAddress = "tb1qinvalidbutvalidlength000000000000000000"; + + try { + await provider.fetchAddress(malformedAddress); + fail("Expected API to throw error for malformed address"); + } catch (error) { + expect(typeof error).toBe("string"); + const errorObj = JSON.parse(error as string); + expect(errorObj).toMatchObject({ + data: "base58 error", + status: 400, + }); + } + }); + }); + + describe("Network configuration", () => { + it("should work with testnet configuration", () => { + expect(provider).toBeDefined(); + }); + + it("should handle mainnet configuration", () => { + const mainnetProvider = new BlockstreamProvider("mainnet"); + expect(mainnetProvider).toBeDefined(); + }); + }); +}); \ No newline at end of file diff --git a/packages/bitcoin/test/providers/blockstream/blockstream.test.ts b/packages/bitcoin/test/providers/blockstream/blockstream.test.ts new file mode 100644 index 000000000..a644e26f2 --- /dev/null +++ b/packages/bitcoin/test/providers/blockstream/blockstream.test.ts @@ -0,0 +1,70 @@ +import { BlockstreamProvider } from "../../../src/providers/blockstream"; + +describe("BlockstreamProvider", () => { + describe("Constructor", () => { + it("should create provider for mainnet", () => { + const provider = new BlockstreamProvider("mainnet"); + expect(provider).toBeDefined(); + }); + + it("should create provider for testnet", () => { + const provider = new BlockstreamProvider("testnet"); + expect(provider).toBeDefined(); + }); + + it("should default to mainnet when no network specified", () => { + const provider = new BlockstreamProvider(); + expect(provider).toBeDefined(); + }); + }); + + describe("IBitcoinProvider interface", () => { + let provider: BlockstreamProvider; + + beforeEach(() => { + provider = new BlockstreamProvider("testnet"); + }); + + it("should have all required IBitcoinProvider methods", () => { + expect(typeof provider.fetchAddress).toBe("function"); + expect(typeof provider.fetchAddressTransactions).toBe("function"); + expect(typeof provider.fetchAddressUTxOs).toBe("function"); + expect(typeof provider.fetchTransactionStatus).toBe("function"); + expect(typeof provider.submitTx).toBe("function"); + expect(typeof provider.fetchFeeEstimates).toBe("function"); + }); + + it("should have script methods available", () => { + // Test that script methods exist (implementation may vary) + expect(typeof provider.fetchScript).toBe("function"); + expect(typeof provider.fetchScriptTransactions).toBe("function"); + expect(typeof provider.fetchScriptUTxOs).toBe("function"); + }); + }); + + describe("Network configuration", () => { + it("should handle mainnet configuration", () => { + const provider = new BlockstreamProvider("mainnet"); + expect(provider).toBeDefined(); + }); + + it("should handle testnet configuration", () => { + const provider = new BlockstreamProvider("testnet"); + expect(provider).toBeDefined(); + }); + }); + + describe("API endpoint validation", () => { + let provider: BlockstreamProvider; + + beforeEach(() => { + provider = new BlockstreamProvider("testnet"); + }); + + it("should construct correct API URLs internally", () => { + // We can't directly test private axios instance, but we can verify + // the provider was created successfully with the correct network + expect(provider).toBeDefined(); + }); + }); +}); \ No newline at end of file diff --git a/packages/bitcoin/test/providers/maestro/maestro-integration.test.ts b/packages/bitcoin/test/providers/maestro/maestro-integration.test.ts new file mode 100644 index 000000000..fdc4f7db0 --- /dev/null +++ b/packages/bitcoin/test/providers/maestro/maestro-integration.test.ts @@ -0,0 +1,228 @@ +import dotenv from "dotenv"; +import { MaestroProvider } from "../../../src/providers/maestro"; + +dotenv.config(); +const apiKey = process.env.MAESTRO_BITCOIN_API_KEY_TESTNET; + +// Skip tests if API key is not provided +const testCondition = apiKey ? describe : describe.skip; + +testCondition("Maestro Integration Tests", () => { + let provider: MaestroProvider; + + // Known testnet addresses with transaction history for testing + const testAddressWithHistory = "tb1q6rz28mcfaxtmd6v789l9rrlrusdprr9pqcpvkl"; + const testAddressEmpty = "tb1qcr8te4kr609gcawutmrza0j4xv80jy8zmfp6l0"; + + beforeAll(() => { + provider = new MaestroProvider({ + network: "testnet", + apiKey: apiKey!, + }); + }); + + describe("Address operations", () => { + it("should fetch address info successfully", async () => { + const addressInfo = await provider.fetchAddress(testAddressWithHistory); + + expect(addressInfo).toMatchObject({ + address: testAddressWithHistory, + chain_stats: expect.objectContaining({ + funded_txo_count: expect.any(Number), + funded_txo_sum: expect.any(Number), + spent_txo_count: expect.any(Number), + spent_txo_sum: expect.any(Number), + tx_count: expect.any(Number), + }), + mempool_stats: expect.objectContaining({ + funded_txo_count: expect.any(Number), + funded_txo_sum: expect.any(Number), + spent_txo_count: expect.any(Number), + spent_txo_sum: expect.any(Number), + tx_count: expect.any(Number), + }), + }); + }); + + it("should fetch address UTxOs", async () => { + const utxos = await provider.fetchAddressUTxOs(testAddressWithHistory); + + expect(Array.isArray(utxos)).toBe(true); + // UTxOs may be empty, but should return valid array + if (utxos.length > 0) { + expect(utxos[0]).toMatchObject({ + txid: expect.any(String), + vout: expect.any(Number), + value: expect.any(Number), + status: expect.objectContaining({ + confirmed: expect.any(Boolean), + }), + }); + } + }); + + it("should fetch address transactions", async () => { + const transactions = await provider.fetchAddressTransactions(testAddressWithHistory); + + expect(Array.isArray(transactions)).toBe(true); + // May have no transactions, but should return valid array + if (transactions.length > 0) { + expect(transactions[0]).toMatchObject({ + txid: expect.any(String), + version: expect.any(Number), + vin: expect.any(Array), + vout: expect.any(Array), + }); + } + }); + }); + + describe("Balance operations", () => { + it("should fetch address balance", async () => { + const balance = await provider.fetchAddressBalance(testAddressWithHistory); + + expect(balance).toMatchObject({ + data: expect.any(String), // Balance as string + }); + }); + + it("should get balance as BigInt", async () => { + const balance = await provider.getBalance(testAddressWithHistory); + + expect(typeof balance).toBe("bigint"); + expect(balance).toBeGreaterThanOrEqual(BigInt(0)); + }); + }); + + describe("Fee estimation", () => { + it("should fetch fee estimates with default blocks", async () => { + const feeRate = await provider.fetchFeeEstimates(); + expect(typeof feeRate).toBe("number"); + expect(feeRate).toBeGreaterThanOrEqual(0); // Can be 0 for testnet + }); + + it("should fetch fee estimates with custom blocks", async () => { + const feeRate = await provider.fetchFeeEstimates(12); + expect(typeof feeRate).toBe("number"); + expect(feeRate).toBeGreaterThanOrEqual(0); // Can be 0 for testnet + }); + }); + + describe("Satoshi Activity", () => { + it("should fetch satoshi activity without options", async () => { + const activity = await provider.fetchSatoshiActivity(testAddressWithHistory); + + expect(activity).toMatchObject({ + data: expect.any(Array), + }); + + if (activity.data.length > 0) { + expect(activity.data[0]).toMatchObject({ + tx_hash: expect.any(String), + }); + } + }); + + it("should fetch satoshi activity with pagination options", async () => { + const activity = await provider.fetchSatoshiActivity(testAddressWithHistory, { + order: "desc", + count: 5, + }); + + expect(activity).toMatchObject({ + data: expect.any(Array), + }); + + // Should respect count limit + expect(activity.data.length).toBeLessThanOrEqual(5); + }); + }); + + describe("Transaction operations", () => { + it("should return status for non-existent transactions", async () => { + const nonExistentTxId = "0000000000000000000000000000000000000000000000000000000000000000"; + + // Maestro returns valid status response for non-existent transactions + const status = await provider.fetchTransactionStatus(nonExistentTxId); + expect(status).toMatchObject({ + confirmed: false, + }); + }); + + it("should handle invalid transaction info requests", async () => { + const invalidTxId = "0000000000000000000000000000000000000000000000000000000000000000"; + + // May return error or empty response for non-existent transactions + try { + await provider.fetchTxInfo(invalidTxId); + } catch (error) { + expect(typeof error).toBe("string"); + } + }); + + it("should handle invalid transaction submission", async () => { + const invalidTxHex = "invalid-hex-data"; + + try { + await provider.submitTx(invalidTxHex); + fail("Expected error for invalid transaction hex"); + } catch (error) { + expect(typeof error).toBe("string"); + } + }); + }); + + describe("Generic HTTP methods", () => { + it("should handle GET requests", async () => { + // Test a valid endpoint + const result = await provider.get("/esplora/blocks/tip/height"); + expect(typeof result).toBe("number"); + expect(result).toBeGreaterThan(0); + }); + + it("should handle invalid GET requests", async () => { + try { + await provider.get("/invalid/endpoint"); + fail("Expected error for invalid endpoint"); + } catch (error) { + expect(typeof error).toBe("string"); + } + }); + + it("should handle invalid POST requests", async () => { + try { + await provider.post("/invalid/endpoint", {}); + fail("Expected error for invalid endpoint"); + } catch (error) { + expect(typeof error).toBe("string"); + } + }); + }); + + describe("Error handling", () => { + it("should handle invalid addresses gracefully", async () => { + const invalidAddress = "invalid-address-format"; + + try { + await provider.fetchAddress(invalidAddress); + fail("Expected error for invalid address"); + } catch (error) { + expect(typeof error).toBe("string"); + const errorObj = JSON.parse(error as string); + expect(errorObj.data).toBe("Invalid Bitcoin address"); + expect(errorObj.status).toBe(400); + } + }); + + it("should handle network timeouts and errors", async () => { + // Test with a malformed but syntactically valid address + const malformedAddress = "tb1qinvalidbutvalidlength000000000000000000"; + + try { + await provider.fetchAddress(malformedAddress); + } catch (error) { + expect(typeof error).toBe("string"); + } + }); + }); +}); \ No newline at end of file diff --git a/packages/bitcoin/test/providers/maestro/maestro.test.ts b/packages/bitcoin/test/providers/maestro/maestro.test.ts new file mode 100644 index 000000000..821302660 --- /dev/null +++ b/packages/bitcoin/test/providers/maestro/maestro.test.ts @@ -0,0 +1,77 @@ +import { MaestroProvider } from "../../../src/providers/maestro"; +import { MaestroConfig } from "../../../src/types/maestro"; + +describe("MaestroProvider", () => { + describe("Constructor", () => { + it("should create provider with config", () => { + const config: MaestroConfig = { + network: "testnet", + apiKey: "test-key", + }; + const provider = new MaestroProvider(config); + expect(provider.getNetwork()).toBe("testnet"); + }); + + it("should create provider with custom baseURL", () => { + const provider = new MaestroProvider("https://custom-api.com", "custom-key"); + expect(provider.getNetwork()).toBe("mainnet"); + }); + + it("should detect testnet from URL", () => { + const provider = new MaestroProvider("https://testnet-api.com", "test-key"); + expect(provider.getNetwork()).toBe("testnet"); + }); + }); + + describe("IBitcoinProvider interface", () => { + let provider: MaestroProvider; + + beforeEach(() => { + provider = new MaestroProvider({ + network: "mainnet", + apiKey: "test-key", + }); + }); + + it("should have all required IBitcoinProvider methods", () => { + expect(typeof provider.fetchAddress).toBe("function"); + expect(typeof provider.fetchAddressTransactions).toBe("function"); + expect(typeof provider.fetchAddressUTxOs).toBe("function"); + expect(typeof provider.fetchTransactionStatus).toBe("function"); + expect(typeof provider.submitTx).toBe("function"); + expect(typeof provider.fetchFeeEstimates).toBe("function"); + }); + + it("should throw error for unimplemented script methods", async () => { + await expect(provider.fetchScript("test-hash")).rejects.toThrow( + "fetchScript is not implemented" + ); + await expect(provider.fetchScriptTransactions("test-hash")).rejects.toThrow( + "fetchScriptTransactions is not implemented" + ); + await expect(provider.fetchScriptUTxOs("test-hash")).rejects.toThrow( + "fetchScriptUTxOs is not implemented" + ); + }); + }); + + describe("Additional methods", () => { + let provider: MaestroProvider; + + beforeEach(() => { + provider = new MaestroProvider({ + network: "mainnet", + apiKey: "test-key", + }); + }); + + it("should have additional Bitcoin-specific methods", () => { + expect(typeof provider.fetchSatoshiActivity).toBe("function"); + expect(typeof provider.fetchTxInfo).toBe("function"); + expect(typeof provider.fetchAddressBalance).toBe("function"); + expect(typeof provider.getBalance).toBe("function"); + expect(typeof provider.get).toBe("function"); + expect(typeof provider.post).toBe("function"); + }); + }); +}); \ No newline at end of file diff --git a/packages/bitcoin/test/wallets/embedded-core.test.ts b/packages/bitcoin/test/wallets/embedded-core.test.ts new file mode 100644 index 000000000..82d002542 --- /dev/null +++ b/packages/bitcoin/test/wallets/embedded-core.test.ts @@ -0,0 +1,216 @@ +import { EmbeddedWallet } from "../../src/wallets/embedded"; +import { MaestroProvider } from "../../src/providers/maestro"; +import { UTxO, AddressInfo } from "../../src/types"; +import { SendTransferParams } from "../../src/types/wallet"; + +jest.mock("../../src/providers/maestro"); +const MockedMaestroProvider = MaestroProvider as jest.MockedClass; + +describe("EmbeddedWallet - Core Functionality", () => { + let mockProvider: jest.Mocked; + const testMnemonic = ["abandon", "abandon", "abandon", "abandon", "abandon", "abandon", "abandon", "abandon", "abandon", "abandon", "abandon", "about"]; + const testAddressTestnet = "tb1qcr8te4kr609gcawutmrza0j4xv80jy8zmfp6l0"; + const readOnlyAddress = "tb1qcr8te4kr609gcawutmrza0j4xv80jy8zmfp6l0"; + + beforeEach(() => { + jest.clearAllMocks(); + mockProvider = new MockedMaestroProvider({ + network: "mainnet", + apiKey: "test-key", + }) as jest.Mocked; + }); + + describe("getUTxOs", () => { + const mockUTxOs: UTxO[] = [ + { + txid: "d6ac4a5fcb9b8b4e5f9b7ac8b6d4c2b1a3f5e7d9c8b6a4f2e1d3c5b7a9", + vout: 0, + value: 100000, + status: { + confirmed: true, + block_height: 700000, + block_hash: "block-hash", + block_time: 1640000000, + }, + }, + ]; + + it("should fetch UTxOs from provider", async () => { + mockProvider.fetchAddressUTxOs.mockResolvedValue(mockUTxOs); + + const wallet = new EmbeddedWallet({ + network: "Testnet", + key: { type: "mnemonic", words: testMnemonic }, + provider: mockProvider, + }); + + const utxos = await wallet.getUTxOs(); + + expect(mockProvider.fetchAddressUTxOs).toHaveBeenCalledWith(testAddressTestnet); + expect(utxos).toEqual(mockUTxOs); + }); + + it("should throw error if provider is not defined", async () => { + const wallet = new EmbeddedWallet({ + network: "Testnet", + key: { type: "mnemonic", words: testMnemonic }, + }); + + await expect(wallet.getUTxOs()).rejects.toThrow( + "`provider` is not defined. Provide a BitcoinProvider." + ); + }); + }); + + describe("getBalance", () => { + const mockAddressInfo: AddressInfo = { + address: testAddressTestnet, + chain_stats: { + funded_txo_count: 5, + funded_txo_sum: 1000000, + spent_txo_count: 3, + spent_txo_sum: 500000, + tx_count: 8, + }, + mempool_stats: { + funded_txo_count: 1, + funded_txo_sum: 100000, + spent_txo_count: 0, + spent_txo_sum: 0, + tx_count: 1, + }, + }; + + it("should return balance information", async () => { + mockProvider.fetchAddress.mockResolvedValue(mockAddressInfo); + + const wallet = new EmbeddedWallet({ + network: "Testnet", + key: { type: "mnemonic", words: testMnemonic }, + provider: mockProvider, + }); + + const balance = await wallet.getBalance(); + + expect(balance).toEqual({ + confirmed: "500000", // funded_txo_sum - spent_txo_sum + unconfirmed: "100000", // mempool funded_txo_sum - spent_txo_sum + total: "600000", // confirmed + unconfirmed + }); + }); + + it("should throw error when provider missing", async () => { + const wallet = new EmbeddedWallet({ + network: "Testnet", + key: { type: "mnemonic", words: testMnemonic }, + }); + + await expect(wallet.getBalance()).rejects.toThrow( + "`provider` is not defined. Provide a BitcoinProvider for balance." + ); + }); + }); + + describe("sendTransfer", () => { + const mockUTxOs: UTxO[] = [ + { + txid: "d6ac4a5fcb9b8b4e5f9b7ac8b6d4c2b1a3f5e7d9c8b6a4f2e1d3c5b7a9", + vout: 0, + value: 1000000, + status: { + confirmed: true, + block_height: 700000, + block_hash: "block-hash", + block_time: 1640000000, + }, + }, + ]; + + const transferParams: SendTransferParams = { + recipients: [ + { + address: "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4", + amount: 500000, + }, + ], + }; + + it("should attempt to build and send transfer", async () => { + mockProvider.fetchAddressUTxOs.mockResolvedValue(mockUTxOs); + mockProvider.fetchFeeEstimates.mockResolvedValue(10); + mockProvider.submitTx.mockResolvedValue("transfer-txid"); + + const wallet = new EmbeddedWallet({ + network: "Testnet", + key: { type: "mnemonic", words: testMnemonic }, + provider: mockProvider, + }); + + // This will likely throw due to PSBT construction, but we're testing the flow + await expect(wallet.sendTransfer(transferParams)).rejects.toThrow(); + }); + + it("should throw error for read-only wallet", async () => { + const wallet = new EmbeddedWallet({ + network: "Testnet", + key: { type: "address", address: readOnlyAddress }, + provider: mockProvider, + }); + + await expect(wallet.sendTransfer(transferParams)).rejects.toThrow( + "Cannot send transactions with a read-only wallet." + ); + }); + + it("should throw error when provider missing", async () => { + const wallet = new EmbeddedWallet({ + network: "Testnet", + key: { type: "mnemonic", words: testMnemonic }, + }); + + await expect(wallet.sendTransfer(transferParams)).rejects.toThrow( + "`provider` is not defined. Provide a BitcoinProvider for sending." + ); + }); + }); + + describe("Coin selection", () => { + const mockUtxos: UTxO[] = [ + { txid: "tx1", vout: 0, value: 100000, status: { confirmed: true, block_height: 700000, block_hash: "hash1", block_time: 1640000000 } }, + { txid: "tx2", vout: 0, value: 50000, status: { confirmed: true, block_height: 700001, block_hash: "hash2", block_time: 1640000001 } }, + { txid: "tx3", vout: 0, value: 200000, status: { confirmed: true, block_height: 700002, block_hash: "hash3", block_time: 1640000002 } }, + ]; + + it("should select UTxOs using largest-first algorithm", () => { + const wallet = new EmbeddedWallet({ + network: "Testnet", + key: { type: "mnemonic", words: testMnemonic }, + provider: mockProvider, + }); + + // Test private method through reflection + const selectUtxos = (wallet as any)._selectUtxosLargestFirst; + if (selectUtxos) { + const result = selectUtxos.call(wallet, mockUtxos, 150000, 10); + expect(result.selectedUtxos).toHaveLength(1); + expect(result.selectedUtxos[0].value).toBe(200000); + expect(result.change).toBeGreaterThan(0); + } + }); + + it("should throw error for insufficient funds", () => { + const wallet = new EmbeddedWallet({ + network: "Testnet", + key: { type: "mnemonic", words: testMnemonic }, + provider: mockProvider, + }); + + const selectUtxos = (wallet as any)._selectUtxosLargestFirst; + if (selectUtxos) { + expect(() => selectUtxos.call(wallet, mockUtxos, 500000, 10)).toThrow( + "Insufficient funds for transaction." + ); + } + }); + }); +}); \ No newline at end of file diff --git a/packages/bitcoin/test/wallets/embedded-derivation.test.ts b/packages/bitcoin/test/wallets/embedded-derivation.test.ts new file mode 100644 index 000000000..241a31984 --- /dev/null +++ b/packages/bitcoin/test/wallets/embedded-derivation.test.ts @@ -0,0 +1,181 @@ +import { EmbeddedWallet } from "../../src/wallets/embedded"; +import { MaestroProvider } from "../../src/providers/maestro"; +import { CreateWalletOptions } from "../../src/types/wallet"; + +jest.mock("../../src/providers/maestro"); +const MockedMaestroProvider = MaestroProvider as jest.MockedClass; + +describe("EmbeddedWallet - Address Derivation", () => { + let mockProvider: jest.Mocked; + const testMnemonic = ["abandon", "abandon", "abandon", "abandon", "abandon", "abandon", "abandon", "abandon", "abandon", "abandon", "abandon", "about"]; + const testAddressTestnet = "tb1qcr8te4kr609gcawutmrza0j4xv80jy8zmfp6l0"; + const testAddressMainnet = "bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu"; + const readOnlyAddress = "tb1qcr8te4kr609gcawutmrza0j4xv80jy8zmfp6l0"; + + beforeEach(() => { + jest.clearAllMocks(); + mockProvider = new MockedMaestroProvider({ + network: "mainnet", + apiKey: "test-key", + }) as jest.Mocked; + }); + + describe("getAddresses", () => { + it("should return correct testnet address", async () => { + const wallet = new EmbeddedWallet({ + network: "Testnet", + key: { type: "mnemonic", words: testMnemonic }, + provider: mockProvider, + }); + + const addresses = await wallet.getAddresses(); + + expect(addresses).toHaveLength(1); + expect(addresses[0]).toMatchObject({ + address: testAddressTestnet, + publicKey: expect.any(String), + purpose: "payment", + addressType: "p2wpkh", + network: "testnet", + walletType: "software", + }); + }); + + it("should return correct mainnet address", async () => { + const wallet = new EmbeddedWallet({ + network: "Mainnet", + key: { type: "mnemonic", words: testMnemonic }, + provider: mockProvider, + }); + + const addresses = await wallet.getAddresses(); + + expect(addresses).toHaveLength(1); + expect(addresses[0]).toMatchObject({ + address: testAddressMainnet, + publicKey: expect.any(String), + purpose: "payment", + addressType: "p2wpkh", + network: "mainnet", + walletType: "software", + }); + }); + + it("should handle read-only wallet", async () => { + const wallet = new EmbeddedWallet({ + network: "Testnet", + key: { type: "address", address: readOnlyAddress }, + provider: mockProvider, + }); + + const addresses = await wallet.getAddresses(); + + expect(addresses).toHaveLength(1); + expect(addresses[0]).toMatchObject({ + address: readOnlyAddress, + publicKey: "", + purpose: "payment", + addressType: "p2wpkh", + network: "testnet", + walletType: "software", + }); + }); + + it("should work with custom derivation path", async () => { + const wallet = new EmbeddedWallet({ + network: "Testnet", + key: { type: "mnemonic", words: testMnemonic }, + path: "m/84'/1'/0'/0/5", // Custom path + provider: mockProvider, + }); + + const addresses = await wallet.getAddresses(); + expect(addresses).toHaveLength(1); + expect(addresses[0]?.addressType).toBe("p2wpkh"); + }); + }); + + describe("getPublicKey", () => { + it("should return public key for mnemonic wallet", () => { + const wallet = new EmbeddedWallet({ + network: "Testnet", + key: { type: "mnemonic", words: testMnemonic }, + provider: mockProvider, + }); + + const publicKey = wallet.getPublicKey(); + expect(publicKey).toMatch(/^[0-9a-f]{66}$/i); + }); + + it("should throw error for read-only wallet", () => { + const wallet = new EmbeddedWallet({ + network: "Testnet", + key: { type: "address", address: readOnlyAddress }, + provider: mockProvider, + }); + + expect(() => wallet.getPublicKey()).toThrow( + "Public key is not available for read-only wallets." + ); + }); + }); + + describe("getNetworkId", () => { + it("should return 1 for mainnet", () => { + const wallet = new EmbeddedWallet({ + network: "Mainnet", + key: { type: "mnemonic", words: testMnemonic }, + provider: mockProvider, + }); + + expect(wallet.getNetworkId()).toBe(1); + }); + + it("should return 0 for testnet", () => { + const wallet = new EmbeddedWallet({ + network: "Testnet", + key: { type: "mnemonic", words: testMnemonic }, + provider: mockProvider, + }); + + expect(wallet.getNetworkId()).toBe(0); + }); + + it("should return 2 for regtest", () => { + const wallet = new EmbeddedWallet({ + network: "Regtest", + key: { type: "mnemonic", words: testMnemonic }, + provider: mockProvider, + }); + + expect(wallet.getNetworkId()).toBe(2); + }); + }); + + describe("Static methods", () => { + describe("brew", () => { + it("should generate mnemonic with default strength", () => { + const mnemonic = EmbeddedWallet.brew(); + expect(mnemonic).toHaveLength(12); + expect(mnemonic.every(word => typeof word === "string")).toBe(true); + }); + + it("should generate mnemonic with custom strength", () => { + const mnemonic = EmbeddedWallet.brew(256); + expect(mnemonic).toHaveLength(24); + }); + + it("should throw error for invalid strength", () => { + expect(() => EmbeddedWallet.brew(100)).toThrow( + "Invalid strength. Must be one of: 128, 160, 192, 224, 256." + ); + }); + + it("should generate different mnemonics on each call", () => { + const mnemonic1 = EmbeddedWallet.brew(); + const mnemonic2 = EmbeddedWallet.brew(); + expect(mnemonic1).not.toEqual(mnemonic2); + }); + }); + }); +}); \ No newline at end of file diff --git a/packages/bitcoin/test/wallets/embedded-signing.test.ts b/packages/bitcoin/test/wallets/embedded-signing.test.ts new file mode 100644 index 000000000..22400971e --- /dev/null +++ b/packages/bitcoin/test/wallets/embedded-signing.test.ts @@ -0,0 +1,225 @@ +import { EmbeddedWallet, verifySignature } from "../../src/wallets/embedded"; +import { MaestroProvider } from "../../src/providers/maestro"; +import { SignPsbtParams } from "../../src/types/wallet"; + +jest.mock("../../src/providers/maestro"); +const MockedMaestroProvider = MaestroProvider as jest.MockedClass; + +describe("EmbeddedWallet - Message Signing", () => { + let mockProvider: jest.Mocked; + const testMnemonic = ["abandon", "abandon", "abandon", "abandon", "abandon", "abandon", "abandon", "abandon", "abandon", "abandon", "abandon", "about"]; + const testAddressTestnet = "tb1qcr8te4kr609gcawutmrza0j4xv80jy8zmfp6l0"; + const testAddressMainnet = "bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu"; + const readOnlyAddress = "tb1qcr8te4kr609gcawutmrza0j4xv80jy8zmfp6l0"; + + beforeEach(() => { + jest.clearAllMocks(); + mockProvider = new MockedMaestroProvider({ + network: "mainnet", + apiKey: "test-key", + }) as jest.Mocked; + + mockProvider.submitTx.mockResolvedValue("mock-tx-id"); + }); + + describe("signMessage", () => { + it("should sign message with real values", async () => { + const wallet = new EmbeddedWallet({ + network: "Mainnet", + key: { type: "mnemonic", words: testMnemonic }, + provider: mockProvider, + }); + + const result = await wallet.signMessage({ + address: testAddressMainnet, + message: "abc", + protocol: "ECDSA" + }); + + expect(result).toMatchObject({ + address: testAddressMainnet, + messageHash: "caaad4da7a8a3b6e7437e933603a9ee4bf338ecdda896e06cb9a4d07660e83d1", + signature: "INXaCK0Dsd5ykeKtMvRY/Wo2Nnse1hasn4DdujqxKVwQVfif0wPMIP8nyfid7chB/Y+P1nUPnMdBFRNWnqVtJA==", + }); + + // Verify signature is cryptographically valid + const publicKey = wallet.getPublicKey(); + const isValid = verifySignature("abc", result.signature, publicKey); + expect(isValid).toBe(true); + }); + + it("should use default ECDSA protocol when not specified", async () => { + const wallet = new EmbeddedWallet({ + network: "Mainnet", + key: { type: "mnemonic", words: testMnemonic }, + provider: mockProvider, + }); + + const result = await wallet.signMessage({ + address: testAddressMainnet, + message: "test message", + }); + + expect(result).toMatchObject({ + address: testAddressMainnet, + messageHash: expect.any(String), + signature: expect.any(String), + }); + + // Verify signature format + expect(() => Buffer.from(result.signature, "base64")).not.toThrow(); + expect(result.messageHash).toMatch(/^[0-9a-f]+$/i); + }); + + it("should throw error for BIP322 protocol", async () => { + const wallet = new EmbeddedWallet({ + network: "Mainnet", + key: { type: "mnemonic", words: testMnemonic }, + provider: mockProvider, + }); + + await expect(wallet.signMessage({ + address: testAddressMainnet, + message: "test", + protocol: "BIP322", + })).rejects.toThrow("BIP322 protocol is not yet supported"); + }); + + it("should throw error for read-only wallet", async () => { + const wallet = new EmbeddedWallet({ + network: "Testnet", + key: { type: "address", address: readOnlyAddress }, + provider: mockProvider, + }); + + await expect(wallet.signMessage({ + address: readOnlyAddress, + message: "test", + })).rejects.toThrow("Cannot sign data with a read-only wallet."); + }); + + it("should handle different message types", async () => { + const wallet = new EmbeddedWallet({ + network: "Mainnet", + key: { type: "mnemonic", words: testMnemonic }, + provider: mockProvider, + }); + + const messages = ["hello", "123", "special chars: !@#$%", ""]; + + for (const message of messages) { + const result = await wallet.signMessage({ + address: testAddressMainnet, + message, + }); + + expect(result).toMatchObject({ + address: testAddressMainnet, + messageHash: expect.any(String), + signature: expect.any(String), + }); + + // Verify each signature + const publicKey = wallet.getPublicKey(); + const isValid = verifySignature(message, result.signature, publicKey); + expect(isValid).toBe(true); + } + }); + + it("should sign message on testnet", async () => { + const wallet = new EmbeddedWallet({ + network: "Testnet", + key: { type: "mnemonic", words: testMnemonic }, + provider: mockProvider, + }); + + const result = await wallet.signMessage({ + address: testAddressTestnet, + message: "testnet message", + protocol: "ECDSA" + }); + + expect(result).toMatchObject({ + address: testAddressTestnet, + messageHash: expect.any(String), + signature: expect.any(String), + }); + + // Verify signature works + const publicKey = wallet.getPublicKey(); + const isValid = verifySignature("testnet message", result.signature, publicKey); + expect(isValid).toBe(true); + }); + }); + + describe("signPsbt", () => { + it("should handle invalid PSBT format", async () => { + const wallet = new EmbeddedWallet({ + network: "Mainnet", + key: { type: "mnemonic", words: testMnemonic }, + provider: mockProvider, + }); + + await expect(wallet.signPsbt({ + psbt: "invalid-base64-data", + signInputs: { [testAddressMainnet]: [0] }, + broadcast: false, + })).rejects.toThrow(); + }); + + it("should handle real PSBT from Bitcoin Core", async () => { + const wallet = new EmbeddedWallet({ + network: "Testnet", + key: { type: "mnemonic", words: testMnemonic }, + provider: mockProvider, + }); + + // Real PSBT from Bitcoin Core multisig example + await expect(wallet.signPsbt({ + psbt: "cHNidP8BAFMCAAAAAQfhbKNA1FRjpH+onW13OEqKIWZCpiF4g1orGXVsH8pEAQAAAAD+////AV1VAAAAAAAAF6kUNgjRCMLYCa5FpfX/Hiio6FjPlemHAAAAAAABASvwVQAAAAAAACIAIP2mSKAKgi8Fe1ssH+zSHj+bdc64GY+QI1re719EAgYdAQVpUiECEXI48hpauJFQc/7pBoGupdsLt2CFBMdqBMmErS/C774hAkkKCbBnfVx28cGq4alyzpHNgadhXhvx+hdqv9xrQQM9IQK08CnGk58nlXP4i/7R3wl5qz3Ldi6akUHA5PNUGBLyllOuAAEAFgAUL/cVRY4VRc6TSlXq0wnca5QjxnUA", + signInputs: { [testAddressTestnet]: [0] }, + broadcast: false, + })).rejects.toThrow(); // Expected to fail due to key mismatch + }); + + it("should throw error for read-only wallet", async () => { + const wallet = new EmbeddedWallet({ + network: "Testnet", + key: { type: "address", address: readOnlyAddress }, + provider: mockProvider, + }); + + await expect(wallet.signPsbt({ + psbt: "cHNidP8BADM...", + signInputs: { [readOnlyAddress]: [0] }, + broadcast: false, + })).rejects.toThrow("Cannot sign transactions with a read-only wallet."); + }); + }); + + describe("verifySignature utility", () => { + it("should verify valid signature", () => { + // Test with known good values + const message = "abc"; + const signature = "INXaCK0Dsd5ykeKtMvRY/Wo2Nnse1hasn4DdujqxKVwQVfif0wPMIP8nyfid7chB/Y+P1nUPnMdBFRNWnqVtJA=="; + const publicKey = "03ba1cf3b34a9f1c5d8e1e8a6d2b3c4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f"; + + const result = verifySignature(message, signature, publicKey); + expect(typeof result).toBe("boolean"); + }); + + it("should return false for invalid signature", () => { + const message = "abc"; + const signature = "invalid-signature"; + const publicKey = "invalid-public-key"; + + const result = verifySignature(message, signature, publicKey); + expect(result).toBe(false); + }); + + it("should handle exceptions gracefully", () => { + const result = verifySignature("", "", ""); + expect(result).toBe(false); + }); + }); +}); \ No newline at end of file diff --git a/packages/bitcoin/test/wallets/embedded.test.ts b/packages/bitcoin/test/wallets/embedded.test.ts new file mode 100644 index 000000000..c14075888 --- /dev/null +++ b/packages/bitcoin/test/wallets/embedded.test.ts @@ -0,0 +1,104 @@ +import { EmbeddedWallet } from "../../src/wallets/embedded"; +import { MaestroProvider } from "../../src/providers/maestro"; + +jest.mock("../../src/providers/maestro"); +const MockedMaestroProvider = MaestroProvider as jest.MockedClass; + +describe("EmbeddedWallet - Constructor & Basic Functionality", () => { + let mockProvider: jest.Mocked; + const testMnemonic = ["abandon", "abandon", "abandon", "abandon", "abandon", "abandon", "abandon", "abandon", "abandon", "abandon", "abandon", "about"]; + const readOnlyAddress = "tb1qcr8te4kr609gcawutmrza0j4xv80jy8zmfp6l0"; + + beforeEach(() => { + jest.clearAllMocks(); + mockProvider = new MockedMaestroProvider({ + network: "mainnet", + apiKey: "test-key", + }) as jest.Mocked; + }); + + describe("Constructor", () => { + it("should create wallet with mnemonic for mainnet", () => { + const wallet = new EmbeddedWallet({ + network: "Mainnet", + key: { type: "mnemonic", words: testMnemonic }, + provider: mockProvider, + }); + + expect(wallet).toBeDefined(); + expect(wallet.getNetworkId()).toBe(1); + }); + + it("should create wallet with mnemonic for testnet", () => { + const wallet = new EmbeddedWallet({ + network: "Testnet", + key: { type: "mnemonic", words: testMnemonic }, + provider: mockProvider, + }); + + expect(wallet).toBeDefined(); + expect(wallet.getNetworkId()).toBe(0); + }); + + it("should create wallet with mnemonic for regtest", () => { + const wallet = new EmbeddedWallet({ + network: "Regtest", + key: { type: "mnemonic", words: testMnemonic }, + provider: mockProvider, + }); + + expect(wallet).toBeDefined(); + expect(wallet.getNetworkId()).toBe(2); + }); + + it("should create read-only wallet with address", () => { + const wallet = new EmbeddedWallet({ + network: "Testnet", + key: { type: "address", address: readOnlyAddress }, + provider: mockProvider, + }); + + expect(wallet).toBeDefined(); + expect(() => wallet.getPublicKey()).toThrow( + "Public key is not available for read-only wallets." + ); + }); + + it("should create wallet with custom derivation path", () => { + const wallet = new EmbeddedWallet({ + network: "Testnet", + key: { type: "mnemonic", words: testMnemonic }, + path: "m/84'/1'/0'/0/5", + provider: mockProvider, + }); + + expect(wallet).toBeDefined(); + }); + + it("should create wallet without provider", () => { + const wallet = new EmbeddedWallet({ + network: "Testnet", + key: { type: "mnemonic", words: testMnemonic }, + }); + + expect(wallet).toBeDefined(); + }); + }); + + describe("Network validation", () => { + it("should handle all supported networks", () => { + const networks = ["Mainnet", "Testnet", "Regtest"] as const; + const expectedNetworkIds = [1, 0, 2]; + + networks.forEach((network, index) => { + const wallet = new EmbeddedWallet({ + network, + key: { type: "mnemonic", words: testMnemonic }, + provider: mockProvider, + }); + + expect(wallet.getNetworkId()).toBe(expectedNetworkIds[index]); + }); + }); + }); +}); \ No newline at end of file