diff --git a/.changeset/ledger-evm-dmk.md b/.changeset/ledger-evm-dmk.md new file mode 100644 index 0000000..6a72791 --- /dev/null +++ b/.changeset/ledger-evm-dmk.md @@ -0,0 +1,13 @@ +--- +"@swapkit/wallet-hardware": major +--- + +Migrate Ledger connections to the Device Management Kit by default. + +Browser connections now discover and reuse one WebHID DMK session. Consumers that manage the device lifecycle can pass a caller-owned `dmkSession`; `originToken` enables EVM Transaction Checks, and `onDeviceActionState` exposes device prompts to the UI. + +EVM, Bitcoin, Cosmos, and transparent Zcash signing use stable Device Signer Kits. THORChain uses its app-v2 protocol in one atomic DMK action, while chains without a stable signer kit run their existing Ledger app client through an operation-scoped DMK bridge. An explicitly injected LedgerJS `transport` remains available as a temporary non-EVM compatibility path, but cannot be combined with `dmkSession`. + +EVM message, transaction, and full EIP-712 signing now use the current signer APIs. Bitcoin signs PSBTs with the Bitcoin signer kit, and Zcash transfer signing produces the transparent v5 transaction required by the current Zcash app. Legacy Zcash PCZT v4 signing is no longer exposed as a supported Ledger path. + +THORChain Amino signing keeps a compatibility normalizer for object-shaped deposit assets emitted by toolboxes 4.x while signing the canonical string form expected on-chain. diff --git a/bun.lock b/bun.lock index 147b356..35b21dd 100644 --- a/bun.lock +++ b/bun.lock @@ -21,7 +21,6 @@ "@swapkit/wallet-keystore": "^4.5.2", "@types/bun": "1.3.13", "@types/node": "25.6.0", - "ledger-bitcoin": "0.3.0", "typescript": "6.0.3", "zod": "4.4.3", }, @@ -90,21 +89,21 @@ "dependencies": { "@cosmjs/amino": "~0.37.0", "@cosmjs/crypto": "0.37.0", - "@cosmjs/proto-signing": "~0.37.0", "@keepkey/keepkey-sdk": "~0.2.62", - "@ledgerhq/devices": "~8.10.0", - "@ledgerhq/errors": "~6.29.0", + "@ledgerhq/context-module": "~2.4.0", + "@ledgerhq/device-management-kit": "~1.8.0", + "@ledgerhq/device-signer-kit-bitcoin": "~1.3.3", + "@ledgerhq/device-signer-kit-cosmos": "~1.0.1", + "@ledgerhq/device-signer-kit-ethereum": "~1.16.0", + "@ledgerhq/device-signer-kit-zcash": "~0.7.0", + "@ledgerhq/device-transport-kit-web-hid": "~1.2.4", "@ledgerhq/hw-app-btc": "~10.17.0", "@ledgerhq/hw-app-cosmos": "~6.33.0", - "@ledgerhq/hw-app-eth": "~7.4.2", "@ledgerhq/hw-app-near": "~6.32.0", "@ledgerhq/hw-app-sui": "~1.5.0", "@ledgerhq/hw-app-trx": "~6.32.0", "@ledgerhq/hw-app-xrp": "~6.33.0", "@ledgerhq/hw-transport": "~6.32.0", - "@ledgerhq/hw-transport-webhid": "~6.31.0", - "@ledgerhq/hw-transport-webusb": "~6.30.0", - "@ledgerhq/wallet-api-client": "~1.13.1", "@near-js/crypto": "~2.5.0", "@near-js/transactions": "~2.5.0", "@scure/base": "2.2.0", @@ -114,40 +113,40 @@ "@swapkit/utxo-signer": "^2.3.0", "@swapkit/wallet-core": "^4.3.19", "@trezor/connect-web": "~9.7.3", - "cosmjs-types": "~0.10.1", + "bitcoinjs-lib": "~6.1.7", "ethers": "^6.14.0", - "ledger-bitcoin": "~0.3.0", "ripple-binary-codec": "2.7.0", + "rxjs": "~7.8.2", "ts-pattern": "^5.9.0", "xrpl": "~4.6.0", }, "devDependencies": { "@cosmjs/amino": "0.37.0", "@cosmjs/crypto": "0.37.0", - "@cosmjs/proto-signing": "0.37.0", "@keepkey/keepkey-sdk": "0.2.62", - "@ledgerhq/devices": "8.10.0", - "@ledgerhq/errors": "6.29.0", + "@ledgerhq/context-module": "2.4.0", + "@ledgerhq/device-management-kit": "1.8.0", + "@ledgerhq/device-signer-kit-bitcoin": "1.3.3", + "@ledgerhq/device-signer-kit-cosmos": "1.0.1", + "@ledgerhq/device-signer-kit-ethereum": "1.16.0", + "@ledgerhq/device-signer-kit-zcash": "0.7.0", + "@ledgerhq/device-transport-kit-web-hid": "1.2.4", "@ledgerhq/hw-app-btc": "10.17.0", "@ledgerhq/hw-app-cosmos": "6.33.0", - "@ledgerhq/hw-app-eth": "7.4.2", "@ledgerhq/hw-app-near": "6.32.0", "@ledgerhq/hw-app-sui": "1.5.0", "@ledgerhq/hw-app-trx": "6.32.0", "@ledgerhq/hw-app-xrp": "6.33.0", "@ledgerhq/hw-transport": "6.32.0", - "@ledgerhq/hw-transport-webhid": "6.31.0", - "@ledgerhq/hw-transport-webusb": "6.30.0", - "@ledgerhq/wallet-api-client": "1.13.1", "@near-js/crypto": "2.5.0", "@near-js/transactions": "2.5.0", "@scure/base": "2.2.0", "@scure/bip32": "2.2.0", "@trezor/connect-web": "9.7.3", - "cosmjs-types": "0.10.1", + "bitcoinjs-lib": "6.1.7", "ethers": "6.16.0", - "ledger-bitcoin": "0.3.0", "ripple-binary-codec": "2.7.0", + "rxjs": "7.8.2", "ts-pattern": "^5.9.0", "xrpl": "4.6.0", }, @@ -349,12 +348,6 @@ "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.13", "", { "os": "win32", "cpu": "x64" }, "sha512-tTcMkXyBrmHi9BfrD2VNHs/5rYIUKETqsBlYOvSAABwBkJhSDVb5e7wPukftsQbO3WzQkXe6kaztC6WtUOXSoQ=="], - "@bitcoinerlab/descriptors": ["@bitcoinerlab/descriptors@1.1.1", "", { "dependencies": { "@bitcoinerlab/miniscript": "^1.2.1", "@bitcoinerlab/secp256k1": "^1.0.5", "bip32": "^4.0.0", "bitcoinjs-lib": "^6.1.3", "ecpair": "^2.1.0" }, "peerDependencies": { "ledger-bitcoin": "^0.2.2" }, "optionalPeers": ["ledger-bitcoin"] }, "sha512-AMFkbBBg9T1iWtEmWB23oADk7zaOQix6wUPLXalhTyFDjhkFXDd6pCRfto/HAdaPg/ccM4GMTVgYLee9WdYFyQ=="], - - "@bitcoinerlab/miniscript": ["@bitcoinerlab/miniscript@1.4.3", "", { "dependencies": { "bip68": "^1.0.4" } }, "sha512-gf7WK4dKJJJl+IgLGmkTOxXQLiCje9c9y4wTLC+cyt0tBDTiSXgsG0X8FFaXf4d+34b8B5p/EJGvRd7mEYB6mQ=="], - - "@bitcoinerlab/secp256k1": ["@bitcoinerlab/secp256k1@1.2.0", "", { "dependencies": { "@noble/curves": "^1.7.0" } }, "sha512-jeujZSzb3JOZfmJYI0ph1PVpCRV5oaexCgy+RvCXV8XlY+XFB/2n3WOcvBsKLsOw78KYgnQrQWb2HrKE4be88Q=="], - "@changesets/apply-release-plan": ["@changesets/apply-release-plan@7.1.1", "", { "dependencies": { "@changesets/config": "^3.1.4", "@changesets/get-version-range-type": "^0.4.0", "@changesets/git": "^3.0.4", "@changesets/should-skip-package": "^0.1.2", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", "detect-indent": "^6.0.0", "fs-extra": "^7.0.1", "lodash.startcase": "^4.4.0", "outdent": "^0.5.0", "prettier": "^2.7.1", "resolve-from": "^5.0.0", "semver": "^7.5.3" } }, "sha512-9qPCm/rLx/xoOFXIHGB229+4GOL76S4MC+7tyOuTsR6+1jYlfFDQORdvwR5hDA6y4FL2BPt3qpbcQIS+dW85LA=="], "@changesets/assemble-release-plan": ["@changesets/assemble-release-plan@6.0.10", "", { "dependencies": { "@changesets/errors": "^0.2.0", "@changesets/get-dependents-graph": "^2.1.4", "@changesets/should-skip-package": "^0.1.2", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", "semver": "^7.5.3" } }, "sha512-rSDcqdJ9KbVyjpBIuCidhvZNIiVt1XaIYp73ycVQRIA5n/j6wQaEk0ChRLMUQ1vkxZe51PTQ9OIhbg6HQMW45A=="], @@ -483,42 +476,6 @@ "@ethereumjs/util": ["@ethereumjs/util@10.1.1", "", { "dependencies": { "@ethereumjs/rlp": "^10.1.1", "@noble/curves": "^2.0.1", "@noble/hashes": "^2.0.1" } }, "sha512-r2EhaeEmLZXVs1dT2HJFQysAkr63ZWATu/9tgYSp1IlvjvwyC++DLg5kCDwMM49HBq3sOAhrPnXkoqf9DV2gbw=="], - "@ethersproject/abi": ["@ethersproject/abi@5.8.0", "", { "dependencies": { "@ethersproject/address": "^5.8.0", "@ethersproject/bignumber": "^5.8.0", "@ethersproject/bytes": "^5.8.0", "@ethersproject/constants": "^5.8.0", "@ethersproject/hash": "^5.8.0", "@ethersproject/keccak256": "^5.8.0", "@ethersproject/logger": "^5.8.0", "@ethersproject/properties": "^5.8.0", "@ethersproject/strings": "^5.8.0" } }, "sha512-b9YS/43ObplgyV6SlyQsG53/vkSal0MNA1fskSC4mbnCMi8R+NkcH8K9FPYNESf6jUefBUniE4SOKms0E/KK1Q=="], - - "@ethersproject/abstract-provider": ["@ethersproject/abstract-provider@5.8.0", "", { "dependencies": { "@ethersproject/bignumber": "^5.8.0", "@ethersproject/bytes": "^5.8.0", "@ethersproject/logger": "^5.8.0", "@ethersproject/networks": "^5.8.0", "@ethersproject/properties": "^5.8.0", "@ethersproject/transactions": "^5.8.0", "@ethersproject/web": "^5.8.0" } }, "sha512-wC9SFcmh4UK0oKuLJQItoQdzS/qZ51EJegK6EmAWlh+OptpQ/npECOR3QqECd8iGHC0RJb4WKbVdSfif4ammrg=="], - - "@ethersproject/abstract-signer": ["@ethersproject/abstract-signer@5.8.0", "", { "dependencies": { "@ethersproject/abstract-provider": "^5.8.0", "@ethersproject/bignumber": "^5.8.0", "@ethersproject/bytes": "^5.8.0", "@ethersproject/logger": "^5.8.0", "@ethersproject/properties": "^5.8.0" } }, "sha512-N0XhZTswXcmIZQdYtUnd79VJzvEwXQw6PK0dTl9VoYrEBxxCPXqS0Eod7q5TNKRxe1/5WUMuR0u0nqTF/avdCA=="], - - "@ethersproject/address": ["@ethersproject/address@5.8.0", "", { "dependencies": { "@ethersproject/bignumber": "^5.8.0", "@ethersproject/bytes": "^5.8.0", "@ethersproject/keccak256": "^5.8.0", "@ethersproject/logger": "^5.8.0", "@ethersproject/rlp": "^5.8.0" } }, "sha512-GhH/abcC46LJwshoN+uBNoKVFPxUuZm6dA257z0vZkKmU1+t8xTn8oK7B9qrj8W2rFRMch4gbJl6PmVxjxBEBA=="], - - "@ethersproject/base64": ["@ethersproject/base64@5.8.0", "", { "dependencies": { "@ethersproject/bytes": "^5.8.0" } }, "sha512-lN0oIwfkYj9LbPx4xEkie6rAMJtySbpOAFXSDVQaBnAzYfB4X2Qr+FXJGxMoc3Bxp2Sm8OwvzMrywxyw0gLjIQ=="], - - "@ethersproject/bignumber": ["@ethersproject/bignumber@5.8.0", "", { "dependencies": { "@ethersproject/bytes": "^5.8.0", "@ethersproject/logger": "^5.8.0", "bn.js": "^5.2.1" } }, "sha512-ZyaT24bHaSeJon2tGPKIiHszWjD/54Sz8t57Toch475lCLljC6MgPmxk7Gtzz+ddNN5LuHea9qhAe0x3D+uYPA=="], - - "@ethersproject/bytes": ["@ethersproject/bytes@5.8.0", "", { "dependencies": { "@ethersproject/logger": "^5.8.0" } }, "sha512-vTkeohgJVCPVHu5c25XWaWQOZ4v+DkGoC42/TS2ond+PARCxTJvgTFUNDZovyQ/uAQ4EcpqqowKydcdmRKjg7A=="], - - "@ethersproject/constants": ["@ethersproject/constants@5.8.0", "", { "dependencies": { "@ethersproject/bignumber": "^5.8.0" } }, "sha512-wigX4lrf5Vu+axVTIvNsuL6YrV4O5AXl5ubcURKMEME5TnWBouUh0CDTWxZ2GpnRn1kcCgE7l8O5+VbV9QTTcg=="], - - "@ethersproject/hash": ["@ethersproject/hash@5.8.0", "", { "dependencies": { "@ethersproject/abstract-signer": "^5.8.0", "@ethersproject/address": "^5.8.0", "@ethersproject/base64": "^5.8.0", "@ethersproject/bignumber": "^5.8.0", "@ethersproject/bytes": "^5.8.0", "@ethersproject/keccak256": "^5.8.0", "@ethersproject/logger": "^5.8.0", "@ethersproject/properties": "^5.8.0", "@ethersproject/strings": "^5.8.0" } }, "sha512-ac/lBcTbEWW/VGJij0CNSw/wPcw9bSRgCB0AIBz8CvED/jfvDoV9hsIIiWfvWmFEi8RcXtlNwp2jv6ozWOsooA=="], - - "@ethersproject/keccak256": ["@ethersproject/keccak256@5.8.0", "", { "dependencies": { "@ethersproject/bytes": "^5.8.0", "js-sha3": "0.8.0" } }, "sha512-A1pkKLZSz8pDaQ1ftutZoaN46I6+jvuqugx5KYNeQOPqq+JZ0Txm7dlWesCHB5cndJSu5vP2VKptKf7cksERng=="], - - "@ethersproject/logger": ["@ethersproject/logger@5.8.0", "", {}, "sha512-Qe6knGmY+zPPWTC+wQrpitodgBfH7XoceCGL5bJVejmH+yCS3R8jJm8iiWuvWbG76RUmyEG53oqv6GMVWqunjA=="], - - "@ethersproject/networks": ["@ethersproject/networks@5.8.0", "", { "dependencies": { "@ethersproject/logger": "^5.8.0" } }, "sha512-egPJh3aPVAzbHwq8DD7Po53J4OUSsA1MjQp8Vf/OZPav5rlmWUaFLiq8cvQiGK0Z5K6LYzm29+VA/p4RL1FzNg=="], - - "@ethersproject/properties": ["@ethersproject/properties@5.8.0", "", { "dependencies": { "@ethersproject/logger": "^5.8.0" } }, "sha512-PYuiEoQ+FMaZZNGrStmN7+lWjlsoufGIHdww7454FIaGdbe/p5rnaCXTr5MtBYl3NkeoVhHZuyzChPeGeKIpQw=="], - - "@ethersproject/rlp": ["@ethersproject/rlp@5.8.0", "", { "dependencies": { "@ethersproject/bytes": "^5.8.0", "@ethersproject/logger": "^5.8.0" } }, "sha512-LqZgAznqDbiEunaUvykH2JAoXTT9NV0Atqk8rQN9nx9SEgThA/WMx5DnW8a9FOufo//6FZOCHZ+XiClzgbqV9Q=="], - - "@ethersproject/signing-key": ["@ethersproject/signing-key@5.8.0", "", { "dependencies": { "@ethersproject/bytes": "^5.8.0", "@ethersproject/logger": "^5.8.0", "@ethersproject/properties": "^5.8.0", "bn.js": "^5.2.1", "elliptic": "6.6.1", "hash.js": "1.1.7" } }, "sha512-LrPW2ZxoigFi6U6aVkFN/fa9Yx/+4AtIUe4/HACTvKJdhm0eeb107EVCIQcrLZkxaSIgc/eCrX8Q1GtbH+9n3w=="], - - "@ethersproject/strings": ["@ethersproject/strings@5.8.0", "", { "dependencies": { "@ethersproject/bytes": "^5.8.0", "@ethersproject/constants": "^5.8.0", "@ethersproject/logger": "^5.8.0" } }, "sha512-qWEAk0MAvl0LszjdfnZ2uC8xbR2wdv4cDabyHiBh3Cldq/T8dPH3V4BbBsAYJUeonwD+8afVXld274Ls+Y1xXg=="], - - "@ethersproject/transactions": ["@ethersproject/transactions@5.8.0", "", { "dependencies": { "@ethersproject/address": "^5.8.0", "@ethersproject/bignumber": "^5.8.0", "@ethersproject/bytes": "^5.8.0", "@ethersproject/constants": "^5.8.0", "@ethersproject/keccak256": "^5.8.0", "@ethersproject/logger": "^5.8.0", "@ethersproject/properties": "^5.8.0", "@ethersproject/rlp": "^5.8.0", "@ethersproject/signing-key": "^5.8.0" } }, "sha512-UglxSDjByHG0TuU17bDfCemZ3AnKO2vYrL5/2n2oXvKzvb7Cz+W9gOWXKARjp2URVwcWlQlPOEQyAviKwT4AHg=="], - - "@ethersproject/web": ["@ethersproject/web@5.8.0", "", { "dependencies": { "@ethersproject/base64": "^5.8.0", "@ethersproject/bytes": "^5.8.0", "@ethersproject/logger": "^5.8.0", "@ethersproject/properties": "^5.8.0", "@ethersproject/strings": "^5.8.0" } }, "sha512-j7+Ksi/9KfGviws6Qtf9Q7KCqRhpwrYKQPs+JBA/rKVFF/yaWLHJEH3zfVP2plVu+eys0d2DlFmhoQJayFewcw=="], - "@fivebinaries/coin-selection": ["@fivebinaries/coin-selection@3.0.0", "", { "dependencies": { "@emurgo/cardano-serialization-lib-browser": "^13.2.0", "@emurgo/cardano-serialization-lib-nodejs": "13.2.0" } }, "sha512-h25Pn1ZA7oqQBQDodGAgIsQt66T2wDge9onBKNqE66WNWL0KJiKJbpij8YOLo5AAlEIg5IS7EB1QjBgDOIg6DQ=="], "@floating-ui/core": ["@floating-ui/core@1.7.5", "", { "dependencies": { "@floating-ui/utils": "^0.2.11" } }, "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ=="], @@ -547,6 +504,16 @@ "@internal/tools-typescript": ["@internal/tools-typescript@workspace:tools/typescript"], + "@inversifyjs/common": ["@inversifyjs/common@1.5.0", "", {}, "sha512-Qj5BELk11AfI2rgZEAaLPmOftmQRLLmoCXgAjmaF0IngQN5vHomVT5ML7DZ3+CA5fgGcEVMcGbUDAun+Rz+oNg=="], + + "@inversifyjs/container": ["@inversifyjs/container@1.9.1", "", { "dependencies": { "@inversifyjs/common": "1.5.0", "@inversifyjs/core": "5.2.0", "@inversifyjs/reflect-metadata-utils": "1.1.0" }, "peerDependencies": { "reflect-metadata": "~0.2.2" } }, "sha512-8EhjFoAx1LoD7TpCuP9ptitwc+7kVXEpZB0IeoEsCQb9sWaNYYaBi+7Lommu3/1nYxdMbX2t7AJhQnryT1EEsg=="], + + "@inversifyjs/core": ["@inversifyjs/core@5.2.0", "", { "dependencies": { "@inversifyjs/common": "1.5.0", "@inversifyjs/prototype-utils": "0.1.0", "@inversifyjs/reflect-metadata-utils": "1.1.0" } }, "sha512-MMaHGGRjQWT9uAof63sbZyEhRl4XptEAE17DJwrgUcN9LsdIEvecKP3r53pE9VF88PFR41knUKKzagUBvWLHmw=="], + + "@inversifyjs/prototype-utils": ["@inversifyjs/prototype-utils@0.1.0", "", { "dependencies": { "@inversifyjs/common": "1.5.0" } }, "sha512-lNz1yyajMRDXBHLvJsYYX81FcmeD15e5Qz1zAZ/3zeYTl+u7ZF/GxNRKJzNOloeMPMtuR8BnvzHA1SZxjR+J9w=="], + + "@inversifyjs/reflect-metadata-utils": ["@inversifyjs/reflect-metadata-utils@1.1.0", "", { "peerDependencies": { "reflect-metadata": "0.2.2" } }, "sha512-jmuAuC3eL1GnFAYfJGJOMKRDL9q1mgzOyrban6zxfM8Yg1FUHsj25h27bW2G7p8X1Amvhg3MLkaOuogszkrofA=="], + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], @@ -561,22 +528,28 @@ "@keplr-wallet/types": ["@keplr-wallet/types@0.13.24", "", { "dependencies": { "long": "^4.0.0" }, "peerDependencies": { "starknet": "^8" } }, "sha512-r5wXlvmVYqPS1PwRad0tQRhdRKJj3CpWcnYa78OzmyCHOJA17xHnkNW+TQJwiw3YGIq3r3laH3nYYEkhf/fFaQ=="], - "@ledgerhq/client-ids": ["@ledgerhq/client-ids@0.8.2", "", { "dependencies": { "@ledgerhq/live-env": "^2.32.0", "@reduxjs/toolkit": "2.11.2", "uuid": "^9.0.0" } }, "sha512-mfds5IyuJLDEwzIzHrYwii1YjP3EkY+txMInWDeektnafzC0nkKOirhluoCKUqtetwBuTUBYLDCf2eyfbmdtrQ=="], + "@ledgerhq/context-module": ["@ledgerhq/context-module@2.4.0", "", { "dependencies": { "bs58": "^6.0.0", "crypto-js": "^4.2.0", "ethers": "6.14.1", "inversify": "7.5.1", "purify-ts": "2.1.0", "reflect-metadata": "0.2.2" }, "peerDependencies": { "@ledgerhq/device-management-kit": "^1.8.0" } }, "sha512-/rjrIXLPYBvhhrtE7rs0rNsh1LUhQzWknLxCxHwz0V8rWwTsy+IFrUOAFfx2T8T/fwfNoFCoK5zRGN8uiF5wnQ=="], - "@ledgerhq/devices": ["@ledgerhq/devices@8.10.0", "", { "dependencies": { "@ledgerhq/errors": "^6.29.0", "@ledgerhq/logs": "^6.14.0", "rxjs": "7.8.2", "semver": "7.7.3" } }, "sha512-ytT66KI8MizFX6dGJKthOzPDw5uNRmmg+RaMta62jbFePKYqfXtYTp6Wc0ErTXaL8nFS3IujHENwKthPmsj6jw=="], + "@ledgerhq/device-management-kit": ["@ledgerhq/device-management-kit@1.8.0", "", { "dependencies": { "@noble/hashes": "^1.8.0", "@sentry/minimal": "6.19.7", "inversify": "7.5.1", "isomorphic-ws": "^5.0.0", "purify-ts": "2.1.0", "reflect-metadata": "0.2.2", "semver": "7.7.2", "uuid": "11.0.3", "ws": "^8.18.0", "xstate": "5.19.2" }, "peerDependencies": { "rxjs": "7.8.2" } }, "sha512-yRYt4a0/tuYjoYDePBMlhCm3Bej0F/TiVu09Hl5fjO4f64Q6jbI2/YthzRWrgP7CEs93EgN7hxJ0WSErtzvEwA=="], - "@ledgerhq/domain-service": ["@ledgerhq/domain-service@1.7.4", "", { "dependencies": { "@ledgerhq/errors": "^6.33.0", "@ledgerhq/logs": "^6.17.0", "@ledgerhq/types-live": "^6.104.0", "axios": "1.13.2", "eip55": "^2.1.1", "react": "19.0.0", "react-dom": "19.0.0" } }, "sha512-7QYkml4MBRMzQsj92xsafI6xum8xuyztTKKRGWMlV9kN47EdMukiOnr5Ywt6SZD3cDJ6cBWxtwj8n853LcdBcQ=="], + "@ledgerhq/device-signer-kit-bitcoin": ["@ledgerhq/device-signer-kit-bitcoin@1.3.3", "", { "dependencies": { "@ledgerhq/signer-utils": "^1.3.0", "@types/crypto-js": "^4.2.0", "crypto-js": "^4.2.0", "inversify": "7.5.1", "purify-ts": "2.1.0", "reflect-metadata": "0.2.2", "xstate": "5.19.2" }, "peerDependencies": { "@ledgerhq/device-management-kit": "^1.7.1", "bitcoinjs-lib": "^6.1.6" } }, "sha512-jLdZsq3eQKli7zOYsP4NxETY40FzA3WlMP7aMWsrajOShVNJmj/p8RLoURA77zM8Fxhr88qMz8ziJm3XuzIjkA=="], - "@ledgerhq/errors": ["@ledgerhq/errors@6.29.0", "", {}, "sha512-mmDsGN662zd0XGKyjzSKkg+5o1/l9pvV1HkVHtbzaydvHAtRypghmVoWMY9XAQDLXiUBXGIsLal84NgmGeuKWA=="], + "@ledgerhq/device-signer-kit-cosmos": ["@ledgerhq/device-signer-kit-cosmos@1.0.1", "", { "dependencies": { "@ledgerhq/signer-utils": "^1.2.0", "inversify": "7.5.1", "purify-ts": "2.1.0", "reflect-metadata": "0.2.2", "xstate": "5.19.2" }, "peerDependencies": { "@ledgerhq/device-management-kit": "^1.5.1" } }, "sha512-822hk0ef7/ELTTi+mIPO+hX5MHLiZ6M8oMWz1eQOfoemcGiVxtiqLsodDKJ60GVmOVP6X6eoqihdrAomS6EvZQ=="], + + "@ledgerhq/device-signer-kit-ethereum": ["@ledgerhq/device-signer-kit-ethereum@1.16.0", "", { "dependencies": { "@ledgerhq/signer-utils": "^1.2.0", "ethers": "6.14.1", "inversify": "7.5.1", "purify-ts": "2.1.0", "reflect-metadata": "0.2.2", "semver": "7.7.2", "xstate": "5.19.2" }, "peerDependencies": { "@ledgerhq/context-module": "^2.0.0", "@ledgerhq/device-management-kit": "^1.5.0" } }, "sha512-lRXCAHlNcZObazX4sQrD2fiVssolzJYjcyzH+9b2AKnV1bfxN5JCY4coIge/L+23LjDVhqibVK6jg8ioUJ4znQ=="], + + "@ledgerhq/device-signer-kit-zcash": ["@ledgerhq/device-signer-kit-zcash@0.7.0", "", { "dependencies": { "@ledgerhq/signer-utils": "^1.3.0", "@noble/hashes": "^1.8.0", "inversify": "7.5.1", "purify-ts": "2.1.0", "reflect-metadata": "0.2.2", "xstate": "5.19.2" }, "peerDependencies": { "@ledgerhq/device-management-kit": "^1.8.0" } }, "sha512-EzHkAjW63IywQl42M+P97+WZflLACszcIVqnG+5/Jpe1G1M3VUB1YLfr1N3PLUqqDltzaoRzMlgqIEqNlBVV6Q=="], + + "@ledgerhq/device-transport-kit-web-hid": ["@ledgerhq/device-transport-kit-web-hid@1.2.4", "", { "dependencies": { "@sentry/minimal": "6.19.7", "purify-ts": "2.1.0", "uuid": "11.0.3" }, "peerDependencies": { "@ledgerhq/device-management-kit": "^1.7.0", "rxjs": "7.8.2" } }, "sha512-VMwkZ6QTUJpj4fS45ihnqdcNrJFDI58kg/KGKCDVhsknSZAe52R0JoH9yRqryemjL0eeo11Yj2KifXM8dNpsDA=="], + + "@ledgerhq/devices": ["@ledgerhq/devices@8.10.0", "", { "dependencies": { "@ledgerhq/errors": "^6.29.0", "@ledgerhq/logs": "^6.14.0", "rxjs": "7.8.2", "semver": "7.7.3" } }, "sha512-ytT66KI8MizFX6dGJKthOzPDw5uNRmmg+RaMta62jbFePKYqfXtYTp6Wc0ErTXaL8nFS3IujHENwKthPmsj6jw=="], - "@ledgerhq/evm-tools": ["@ledgerhq/evm-tools@1.12.3", "", { "dependencies": { "@ethersproject/constants": "^5.7.0", "@ethersproject/hash": "^5.7.0", "@ledgerhq/live-env": "^2.32.0", "axios": "1.13.2", "crypto-js": "4.2.0" } }, "sha512-ZDD4lqwIpMA0ZrwYLA+D6JZoJUDA2E6Uk4hqNnFj1K5h6XKJstj3t8HFRvjKNubMOctQPjsVZ1I5yFPLAPRG4w=="], + "@ledgerhq/errors": ["@ledgerhq/errors@6.29.0", "", {}, "sha512-mmDsGN662zd0XGKyjzSKkg+5o1/l9pvV1HkVHtbzaydvHAtRypghmVoWMY9XAQDLXiUBXGIsLal84NgmGeuKWA=="], "@ledgerhq/hw-app-btc": ["@ledgerhq/hw-app-btc@10.17.0", "", { "dependencies": { "@ledgerhq/hw-transport": "6.32.0", "@ledgerhq/logs": "^6.14.0", "@ledgerhq/psbtv2": "^0.1.0", "@noble/curves": "1.9.7", "@noble/hashes": "1.8.0", "bip32-path": "^0.4.2", "bitcoinjs-lib": "^5.2.0", "bs58": "^4.0.1", "bs58check": "^2.1.2", "invariant": "^2.2.4", "semver": "7.7.3" } }, "sha512-zJMp7lQ5UfV2ZoWCx3Jt1kPGVKI0IQQBnXpOd0Cx354S420I9WXsaNMz2Fn7C7kkYbpE3n1ZeQwccKjRWvFWgQ=="], "@ledgerhq/hw-app-cosmos": ["@ledgerhq/hw-app-cosmos@6.33.0", "", { "dependencies": { "@ledgerhq/errors": "^6.29.0", "@ledgerhq/hw-transport": "6.32.0", "bip32-path": "^0.4.2" } }, "sha512-0CxN9y1U0OmPm5yNX1dcOzYBPsPP+TCBul+uCn7BDvaY11IB6jptxhZRoTtthBFKwBzZbkv+BSdumPRnKIScDQ=="], - "@ledgerhq/hw-app-eth": ["@ledgerhq/hw-app-eth@7.4.2", "", { "dependencies": { "@ethersproject/abi": "^5.7.0", "@ethersproject/rlp": "^5.7.0", "@ethersproject/transactions": "^5.7.0", "@ledgerhq/domain-service": "^1.6.6", "@ledgerhq/errors": "^6.29.0", "@ledgerhq/evm-tools": "^1.11.2", "@ledgerhq/hw-transport": "6.32.0", "@ledgerhq/hw-transport-mocker": "^6.31.0", "@ledgerhq/logs": "^6.14.0", "@ledgerhq/types-live": "^6.99.0", "axios": "1.13.2", "bignumber.js": "^9.1.2", "semver": "7.7.3" } }, "sha512-vSzd976GV/hapXch8944s3dw0xof9nzKVLAsHXPiS5skOTZgCwHXXEQuOnloAW5uVu66fJ2k2z6batlaTNbtmg=="], - "@ledgerhq/hw-app-near": ["@ledgerhq/hw-app-near@6.32.0", "", { "dependencies": { "@ledgerhq/hw-transport": "6.32.0", "near-api-js": "^3.0.2" } }, "sha512-CeQmeKYmut+TMGQ+IGjycRnKSU+FyzbMCha+G7ZuVKyLJ6QK+3VqNbtOELNIWaUmU2Ct1RbkQf2DqRhQ1o+ijA=="], "@ledgerhq/hw-app-sui": ["@ledgerhq/hw-app-sui@1.5.0", "", { "dependencies": { "@mysten/ledgerjs-hw-app-sui": "0.6.0" } }, "sha512-LpLl4zPFZhnHrZ6kNC2SrhhdnC5F/cIPeXrhIKHTiRuj1SBGhfjTeNkN8siff9aUAbMdgaWlfv5fz/rWw8tkrw=="], @@ -587,23 +560,11 @@ "@ledgerhq/hw-transport": ["@ledgerhq/hw-transport@6.32.0", "", { "dependencies": { "@ledgerhq/devices": "8.10.0", "@ledgerhq/errors": "^6.29.0", "@ledgerhq/logs": "^6.14.0", "events": "^3.3.0" } }, "sha512-bf2nxzDQ21DV/bsmExfWI0tatoVeoqhu/ePWalD/nPgPnTn/ZIDq7VBU+TY5p0JZaE87NQwmRUAjm6C1Exe61A=="], - "@ledgerhq/hw-transport-mocker": ["@ledgerhq/hw-transport-mocker@6.34.0", "", { "dependencies": { "@ledgerhq/hw-transport": "6.35.0", "@ledgerhq/logs": "^6.17.0", "rxjs": "7.8.2" } }, "sha512-gFxz0oZXNIoye6qKHddjyprBTErvvEPic2SZbbZHXMc4KHkZuSmpkLOrzqs+iPMiT8OqykuYHV3wAzU2Mu1+Ww=="], - - "@ledgerhq/hw-transport-webhid": ["@ledgerhq/hw-transport-webhid@6.31.0", "", { "dependencies": { "@ledgerhq/devices": "8.10.0", "@ledgerhq/errors": "^6.29.0", "@ledgerhq/hw-transport": "6.32.0", "@ledgerhq/logs": "^6.14.0" } }, "sha512-XtCFOJ1ooaqCefB6WDfV/ie+GaO/7xQgezwRpyLroMBYboSpnEd3Diu3BAoptXKTNklwZEiUVRLVXlltTBzNvw=="], - - "@ledgerhq/hw-transport-webusb": ["@ledgerhq/hw-transport-webusb@6.30.0", "", { "dependencies": { "@ledgerhq/devices": "8.10.0", "@ledgerhq/errors": "^6.29.0", "@ledgerhq/hw-transport": "6.32.0", "@ledgerhq/logs": "^6.14.0" } }, "sha512-GX5OJvNl0cRZyadxQjS+cnAxiD2irw4Cponp46r50WmirXOLR7hJYKfd1plmGlZOAgT6ayggOZkzahunYerPXw=="], - - "@ledgerhq/live-env": ["@ledgerhq/live-env@2.32.0", "", { "dependencies": { "rxjs": "7.8.2", "utility-types": "^3.10.0" } }, "sha512-5/6gQDRJCxluC3dy8WdtpfE13xLhwOOi07FF6uU57jtx4Pe2C8qKtvA4wnZD5I7R3RAKS4o84meSshGXNplcPA=="], - "@ledgerhq/logs": ["@ledgerhq/logs@6.17.0", "", {}, "sha512-yra33g5q/AU7+PwAws+GaVpQGUuxnDREjVBnviJjcaJLVKuLzI4pnj8Bd3nY3fypM5k1yZEYKEXfUuGFUjP2+w=="], "@ledgerhq/psbtv2": ["@ledgerhq/psbtv2@0.1.0", "", { "dependencies": { "bitcoinjs-lib": "^5.2.0", "varuint-bitcoin": "1.1.2" } }, "sha512-1NWThlGMGDwzmrB5UHNY2E/shIQ/n1yWo+CXdbwDDIWHFkKIU/mhgwv/A1cly0FfCn1m+zEbw4NU0TgvGqaEWQ=="], - "@ledgerhq/types-live": ["@ledgerhq/types-live@6.104.0", "", { "dependencies": { "@ledgerhq/client-ids": "0.8.2", "bignumber.js": "^9.1.2", "rxjs": "7.8.2" } }, "sha512-5WLoFeKaassNM9dDKkiv8FCn5JRntghq208IV/MEDNj02oTkaZSHFUVuBFQEw3d6m2svkF40aMlI2gnf0tNMoQ=="], - - "@ledgerhq/wallet-api-client": ["@ledgerhq/wallet-api-client@1.13.1", "", { "dependencies": { "@ledgerhq/hw-transport": "^6.31.13", "@ledgerhq/wallet-api-core": "1.29.0", "bignumber.js": "^9.1.2" } }, "sha512-uohDyB+KdN3vtF0HpviElWOyfsLBmS4bwmblrNkurATaWWFGCXE0oCB9k9vjIz0gRKIhjEvbccTlRWKQhyRnqA=="], - - "@ledgerhq/wallet-api-core": ["@ledgerhq/wallet-api-core@1.29.0", "", { "dependencies": { "@ledgerhq/errors": "^6.27.0", "@stacks/transactions": "^6.13.0", "@ton/core": "^0.62.0", "bignumber.js": "^9.1.2", "uuid": "^9.0.1", "zod": "^3.22.4" } }, "sha512-xpIQAkMaQOKlPFQV50GzEjiG6xFBPn8NNxkyJKbKWMrF4WuNdbU2pK9VTgN4Cxnx4Nm8uR2G2GRjQ/zbJU888A=="], + "@ledgerhq/signer-utils": ["@ledgerhq/signer-utils@1.3.0", "", { "dependencies": { "semver": "7.7.2" }, "peerDependencies": { "@ledgerhq/device-management-kit": "^1.7.1" } }, "sha512-qe+HQMgZxq6SPv8ghvlq4c33RchUwlf1qE/qp7MtUGPbKGAqD/0PKNyzx05sKLpmA8m6YZdtndHH4CEkikzqSQ=="], "@lit-labs/ssr-dom-shim": ["@lit-labs/ssr-dom-shim@1.5.1", "", {}, "sha512-Aou5UdlSpr5whQe8AA/bZG0jMj96CoJIWbGfZ91qieWu5AWUMKw8VR/pAkQkJYvBNhmCcWnZlyyk5oze8JIqYA=="], @@ -663,7 +624,7 @@ "@near-js/accounts": ["@near-js/accounts@2.5.1", "", { "dependencies": { "@near-js/crypto": "2.5.1", "@near-js/keystores": "2.5.1", "@near-js/providers": "2.5.1", "@near-js/signers": "2.5.1", "@near-js/tokens": "2.5.1", "@near-js/transactions": "2.5.1", "@near-js/types": "2.5.1", "@near-js/utils": "2.5.1", "@noble/hashes": "1.7.1", "borsh": "1.0.0", "depd": "2.0.0", "is-my-json-valid": "^2.20.6", "lru_map": "0.4.1", "near-abi": "0.2.0" } }, "sha512-L4/Z9Ujwnz8/uvOExESP6h0t/plu4/0SFrL+UJBnfO5LObVwnJH2KG6MCLONy8rKh1FZHOlT9LPhtG/6nUrvOQ=="], - "@near-js/crypto": ["@near-js/crypto@2.5.1", "", { "dependencies": { "@near-js/types": "2.5.1", "@near-js/utils": "2.5.1", "@noble/curves": "1.8.1", "@noble/hashes": "^1.7.1", "borsh": "1.0.0", "secp256k1": "5.0.1" } }, "sha512-Kb+bbnUrfvuzzed9hpLRpcIlCMOaQlw/7BxlZPCq8DggVaK1m0nKR1DHQs2cV0Q0WSKSk2UrFJho1dxeKL5alg=="], + "@near-js/crypto": ["@near-js/crypto@2.5.0", "", { "dependencies": { "@near-js/types": "2.5.0", "@near-js/utils": "2.5.0", "@noble/curves": "1.8.1", "@noble/hashes": "^1.7.1", "borsh": "1.0.0", "secp256k1": "5.0.1" } }, "sha512-bsMCasnjRne9j0u5DuKeO0/xwS49GgRrVxzmW9WRZ7yWWh0U8IidTxNHTL1QCDFJNwwzt9HDnuj8XeVhlpixww=="], "@near-js/keystores": ["@near-js/keystores@2.5.1", "", { "dependencies": { "@near-js/crypto": "2.5.1", "@near-js/types": "2.5.1" } }, "sha512-UlP3GXeDzRyUlL3qKZUkwSwbABDarhOqRRR0vcMBOOu+063GDULGio9GTWDvKDDi11nmfbhOzbiY5vXEW4jR8g=="], @@ -677,7 +638,7 @@ "@near-js/tokens": ["@near-js/tokens@2.5.1", "", {}, "sha512-doDtOaSA2FOsYzzwocg0L02z6ORNL7HFAuL455y2O38//F4C2jp+C9De9fWrItkrim2jKjoSR3UvO2AuVbwmWA=="], - "@near-js/transactions": ["@near-js/transactions@2.5.1", "", { "dependencies": { "@near-js/crypto": "2.5.1", "@near-js/types": "2.5.1", "@near-js/utils": "2.5.1", "@noble/hashes": "1.7.1", "borsh": "1.0.0" } }, "sha512-0svK5K6VqOciSIn9mxreyS0uQf8T1UZc71JBA6yrstpHJhltz/dvNUMY0FV+xHx3H/KAdvCMjX0VLEpyi08yWQ=="], + "@near-js/transactions": ["@near-js/transactions@2.5.0", "", { "dependencies": { "@near-js/crypto": "2.5.0", "@near-js/types": "2.5.0", "@near-js/utils": "2.5.0", "@noble/hashes": "1.7.1", "borsh": "1.0.0" } }, "sha512-gVhWS4T5VcLjyVaW8nxWxK7kcgbGgyJ0YVS+VmaFzQbSZKuYgNnQXxynTycFc34awTNbp3a4/MqAd8RgX9VIcg=="], "@near-js/types": ["@near-js/types@2.5.1", "", {}, "sha512-lUJJmbV6qcilIwswldeP7FIjkjTWvmtgI+yQHVmiBNRVjYvPMiQ/e9vs3SXE/cYtRluAdq0JHJwYoqhFb69mwA=="], @@ -905,8 +866,6 @@ "@radixdlt/radix-dapp-toolkit": ["@radixdlt/radix-dapp-toolkit@2.3.0", "", { "dependencies": { "@noble/curves": "^1.4.0", "base64url": "^3.0.1", "blakejs": "^1.2.1", "buffer": "^6.0.3", "immer": "^10.0.4", "lit": "^3.1.2", "lit-html": "^3.1.2", "neverthrow": "^8.0.0", "rxjs": "^7.8.1", "tslog": ">=4.8.0", "uuid": "^10.0.0", "valibot": "0.42.1" } }, "sha512-sBC5h58WBESfjI7w5UW/3ltasEF6MYCtbgAdTA/g7E1fAbCQ7Zeai6id8TIE97L7hxv7g+uiOwNnpruN+TJJEw=="], - "@reduxjs/toolkit": ["@reduxjs/toolkit@2.11.2", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@standard-schema/utils": "^0.3.0", "immer": "^11.0.0", "redux": "^5.0.1", "redux-thunk": "^3.1.0", "reselect": "^5.1.0" }, "peerDependencies": { "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" }, "optionalPeers": ["react", "react-redux"] }, "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ=="], - "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.43", "", {}, "sha512-5Uxg7fQUCmfhax7FJke2+8B6cqgeUJUD9o2uXIKXhD+mG0mL6NObmVoi9wXEU1tY89mZKgAYA6fTbftx3q2ZPQ=="], "@rollup/plugin-inject": ["@rollup/plugin-inject@5.0.5", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "estree-walker": "^2.0.2", "magic-string": "^0.30.3" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-2+DEJbNBoPROPkgTDNe8/1YXWcqxbN5DTjASVIOx8HS+pITXushyNiBV56RB08zuptzz8gT3YfkqriTBVycepg=="], @@ -983,8 +942,16 @@ "@sentry/core": ["@sentry/core@9.27.0", "", {}, "sha512-Zb2SSAdWXQjTem+sVWrrAq9L6YYfxyoTwtapaE6C6qZBR5C8Uak0wcYww8StaCFH7dDA/PSW+VxOwjNXocrQHQ=="], + "@sentry/hub": ["@sentry/hub@6.19.7", "", { "dependencies": { "@sentry/types": "6.19.7", "@sentry/utils": "6.19.7", "tslib": "^1.9.3" } }, "sha512-y3OtbYFAqKHCWezF0EGGr5lcyI2KbaXW2Ik7Xp8Mu9TxbSTuwTe4rTntwg8ngPjUQU3SUHzgjqVB8qjiGqFXCA=="], + + "@sentry/minimal": ["@sentry/minimal@6.19.7", "", { "dependencies": { "@sentry/hub": "6.19.7", "@sentry/types": "6.19.7", "tslib": "^1.9.3" } }, "sha512-wcYmSJOdvk6VAPx8IcmZgN08XTXRwRtB1aOLZm+MVHjIZIhHoBGZJYTVQS/BWjldsamj2cX3YGbGXNunaCfYJQ=="], + "@sentry/react": ["@sentry/react@9.27.0", "", { "dependencies": { "@sentry/browser": "9.27.0", "@sentry/core": "9.27.0", "hoist-non-react-statics": "^3.3.2" }, "peerDependencies": { "react": "^16.14.0 || 17.x || 18.x || 19.x" } }, "sha512-UT7iaGEwTqe06O4mgHfKGTRBHg+U0JSI/id+QxrOji6ksosOsSnSC3Vdq+gPs9pzCCFE+6+DkH6foYNNLIN0lw=="], + "@sentry/types": ["@sentry/types@6.19.7", "", {}, "sha512-jH84pDYE+hHIbVnab3Hr+ZXr1v8QABfhx39KknxqKWr2l0oEItzepV0URvbEhB446lk/S/59230dlUUIBGsXbg=="], + + "@sentry/utils": ["@sentry/utils@6.19.7", "", { "dependencies": { "@sentry/types": "6.19.7", "tslib": "^1.9.3" } }, "sha512-z95ECmE3i9pbWoXQrD/7PgkBAzJYR+iXtPuTkpBjDKs86O3mT+PXOT3BAn79w2wkn7/i3vOGD2xVr1uiMl26dA=="], + "@serenity-kit/noble-sodium": ["@serenity-kit/noble-sodium@0.2.3", "", { "dependencies": { "@noble/ciphers": "^2.1.1", "@noble/curves": "^2.0.1", "@noble/hashes": "^2.0.1" } }, "sha512-/IQuvJHJXg8M8SvrZexB/kI5vP0jkoMhNglFpm12h1SitJldncoOA0aF5asvtybJ8nldnkA/HwGEvEL7t+VIyw=="], "@sinclair/typebox": ["@sinclair/typebox@0.33.22", "", {}, "sha512-auUj4k+f4pyrIVf4GW5UKquSZFHJWri06QgARy9C0t9ZTjJLIuNIrr1yl9bWcJWJ1Gz1vOvYN1D+QPaIlNMVkQ=="], @@ -1087,20 +1054,12 @@ "@solana/web3.js": ["@solana/web3.js@1.98.4", "", { "dependencies": { "@babel/runtime": "^7.25.0", "@noble/curves": "^1.4.2", "@noble/hashes": "^1.4.0", "@solana/buffer-layout": "^4.0.1", "@solana/codecs-numbers": "^2.1.0", "agentkeepalive": "^4.5.0", "bn.js": "^5.2.1", "borsh": "^0.7.0", "bs58": "^4.0.1", "buffer": "6.0.3", "fast-stable-stringify": "^1.0.0", "jayson": "^4.1.1", "node-fetch": "^2.7.0", "rpc-websockets": "^9.0.2", "superstruct": "^2.0.2" } }, "sha512-vv9lfnvjUsRiq//+j5pBdXig0IQdtzA0BRZ3bXEP4KaIyF1CcaydWqgyzQgfZMNIsWNWmG+AUHwPy4AHOD6gpw=="], - "@stacks/common": ["@stacks/common@6.16.0", "", { "dependencies": { "@types/bn.js": "^5.1.0", "@types/node": "^18.0.4" } }, "sha512-PnzvhrdGRMVZvxTulitlYafSK4l02gPCBBoI9QEoTqgSnv62oaOXhYAUUkTMFKxdHW1seVEwZsrahuXiZPIAwg=="], - - "@stacks/network": ["@stacks/network@6.17.0", "", { "dependencies": { "@stacks/common": "^6.16.0", "cross-fetch": "^3.1.5" } }, "sha512-numHbfKjwco/rbkGPOEz8+FcJ2nBnS/tdJ8R422Q70h3SiA9eqk9RjSzB8p4JP8yW1SZvW+eihADHfMpBuZyfw=="], - - "@stacks/transactions": ["@stacks/transactions@6.17.0", "", { "dependencies": { "@noble/hashes": "1.1.5", "@noble/secp256k1": "1.7.1", "@stacks/common": "^6.16.0", "@stacks/network": "^6.17.0", "c32check": "^2.0.0", "lodash.clonedeep": "^4.5.0" } }, "sha512-FUah2BRgV66ApLcEXGNGhwyFTRXqX5Zco3LpiM3essw8PF0NQlHwwdPgtDko5RfrJl3LhGXXe/30nwsfNnB3+g=="], - "@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="], "@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="], "@starknet-io/get-starknet-wallet-standard": ["@starknet-io/get-starknet-wallet-standard@5.0.0", "", { "dependencies": { "@starknet-io/types-js": "^0.7.10", "@wallet-standard/base": "^1.1.0", "@wallet-standard/features": "^1.1.0", "ox": "^0.4.4" } }, "sha512-isDNGDlp16W24HE4IuweYXLDRZN0JbsDnazAieeKXE87Mn+jqhsjgTsMxcwWTjX7v906Bjz39FiDjGUddnr36g=="], - "@starknet-io/starknet-types-010": ["@starknet-io/types-js@0.10.0", "", {}, "sha512-7ALSydz6pq3YIOpq5a7OkkxqwJciMc9Nlph0OGjhcC3xX0xH30XgizmziLyYVN10oO9+BJk8M9KbJjpzdbtRSw=="], - "@starknet-io/starknet-types-0101": ["@starknet-io/types-js@0.10.2", "", {}, "sha512-AtUFPYdmo9DqVus++aBSoY9W13/2PZmillPr8/mXZjc+V0iYJ/QTmkTsbw+es2mnLeLhYWSymW9ivQzyyyKdog=="], "@starknet-io/starknet-types-09": ["@starknet-io/types-js@0.9.2", "", {}, "sha512-vWOc0FVSn+RmabozIEWcEny1I73nDGTvOrLYJsR1x7LGA3AZmqt4i/aW69o/3i2NN5CVP8Ok6G1ayRQJKye3Wg=="], @@ -1241,6 +1200,8 @@ "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], + "@types/crypto-js": ["@types/crypto-js@4.2.2", "", {}, "sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ=="], + "@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="], "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], @@ -1377,7 +1338,7 @@ "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], - "axios": ["axios@1.13.2", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA=="], + "axios": ["axios@1.15.0", "", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^2.1.0" } }, "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q=="], "base-x": ["base-x@4.0.1", "", {}, "sha512-uAZ8x6r6S3aUM9rbHGVOIsR15U/ZSc82b3ymnCPsT45Gk1DDvhDPdIgB5MrhirZWt+5K0EEPQH985kNqZgNPFw=="], @@ -1405,7 +1366,7 @@ "bip174": ["bip174@2.1.1", "", {}, "sha512-mdFV5+/v0XyNYXjBS6CQPLo9ekCx4gtKZFnJm5PMto7Fs9hTTDpkkzOB7/FtluRI6JbUUAu+snTYfJRgHLZbZQ=="], - "bip32": ["bip32@4.0.0", "", { "dependencies": { "@noble/hashes": "^1.2.0", "@scure/base": "^1.1.1", "typeforce": "^1.11.5", "wif": "^2.0.6" } }, "sha512-aOGy88DDlVUhspIXJN+dVEtclhIsfAUppD43V0j40cPTld3pv/0X/MlrZSZ6jowIaQQzFwP8M6rFU2z2mVYjDQ=="], + "bip32": ["bip32@2.0.6", "", { "dependencies": { "@types/node": "10.12.18", "bs58check": "^2.1.1", "create-hash": "^1.2.0", "create-hmac": "^1.1.7", "tiny-secp256k1": "^1.1.3", "typeforce": "^1.11.5", "wif": "^2.0.6" } }, "sha512-HpV5OMLLGTjSVblmrtYRfFFKuQB+GArM0+XP8HGWfJ5vxYBqo+DesvJwOdC2WJ3bCkZShGf0QIfoIpeomVzVdA=="], "bip32-path": ["bip32-path@0.4.2", "", {}, "sha512-ZBMCELjJfcNMkz5bDuJ1WrYvjlhEF5k6mQ8vUr4N7MbVRsXei7ZOg8VhhwMfNiW68NWmLkgkc6WvTickrLGprQ=="], @@ -1415,8 +1376,6 @@ "bip66": ["bip66@1.1.5", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nemMHz95EmS38a26XbbdxIYj5csHd3RMP3H5bwQknX0WYHF01qhpufP42mLOwVICuH2JmhIhXiWs89MfUGL7Xw=="], - "bip68": ["bip68@1.0.4", "", {}, "sha512-O1htyufFTYy3EO0JkHg2CLykdXEtV2ssqw47Gq9A0WByp662xpJnMEB9m43LZjsSDjIAOozWRExlFQk2hlV1XQ=="], - "bitcoin-ops": ["bitcoin-ops@1.4.1", "", {}, "sha512-pef6gxZFztEhaE9RY9HmWVmiIHqCb2OyS4HPKkpc6CIiiOa3Qmuoylxc5P2EkU3w+5eTSifI9SEZC88idAIGow=="], "bitcoinjs-lib": ["bitcoinjs-lib@6.1.7", "", { "dependencies": { "@noble/hashes": "^1.2.0", "bech32": "^2.0.0", "bip174": "^2.1.1", "bs58check": "^3.0.1", "typeforce": "^1.11.3", "varuint-bitcoin": "^1.1.2" } }, "sha512-tlf/r2DGMbF7ky1MgUqXHzypYHakkEnm0SZP23CJKIqNY/5uNAnMbFhMJdhjrL/7anfb/U8+AlpdjPWjPnAalg=="], @@ -1467,8 +1426,6 @@ "bun-types": ["bun-types@1.3.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA=="], - "c32check": ["c32check@2.0.0", "", { "dependencies": { "@noble/hashes": "^1.1.2", "base-x": "^4.0.0" } }, "sha512-rpwfAcS/CMqo0oCqDf3r9eeLgScRE3l/xHDCXhM3UyrfvIn7PrLq63uHh7yYbv8NzaZn5MVsVhIRpQ+5GZ5HyA=="], - "cacheable-lookup": ["cacheable-lookup@5.0.4", "", {}, "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA=="], "cacheable-request": ["cacheable-request@7.0.4", "", { "dependencies": { "clone-response": "^1.0.2", "get-stream": "^5.1.0", "http-cache-semantics": "^4.0.0", "keyv": "^4.0.0", "lowercase-keys": "^2.0.0", "normalize-url": "^6.0.1", "responselike": "^2.0.0" } }, "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg=="], @@ -1625,10 +1582,6 @@ "eciesjs": ["eciesjs@0.4.17", "", { "dependencies": { "@ecies/ciphers": "^0.2.5", "@noble/ciphers": "^1.3.0", "@noble/curves": "^1.9.7", "@noble/hashes": "^1.8.0" } }, "sha512-TOOURki4G7sD1wDCjj7NfLaXZZ49dFOeEb5y39IXpb8p0hRzVvfvzZHOi5JcT+PpyAbi/Y+lxPb8eTag2WYH8w=="], - "ecpair": ["ecpair@2.1.0", "", { "dependencies": { "randombytes": "^2.1.0", "typeforce": "^1.18.0", "wif": "^2.0.6" } }, "sha512-cL/mh3MtJutFOvFc27GPZE2pWL3a3k4YvzUWEOvilnfZVlH3Jwgx/7d6tlD7/75tNk8TG2m+7Kgtz0SI1tWcqw=="], - - "eip55": ["eip55@2.1.1", "", { "dependencies": { "keccak": "^3.0.3" } }, "sha512-WcagVAmNu2Ww2cDUfzuWVntYwFxbvZ5MvIyLZpMjTTkjD6sCvkGOiS86jTppzu9/gWsc8isLHAeMBWK02OnZmA=="], - "electron-to-chromium": ["electron-to-chromium@1.5.340", "", {}, "sha512-908qahOGocRMinT2nM3ajCEM99H4iPdv84eagPP3FfZy/1ZGeOy2CZYzjhms81ckOPCXPlW7LkY4XpxD8r1DrA=="], "elliptic": ["elliptic@6.6.1", "", { "dependencies": { "bn.js": "^4.11.9", "brorand": "^1.1.0", "hash.js": "^1.0.0", "hmac-drbg": "^1.0.1", "inherits": "^2.0.4", "minimalistic-assert": "^1.0.1", "minimalistic-crypto-utils": "^1.0.1" } }, "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g=="], @@ -1827,6 +1780,8 @@ "invariant": ["invariant@2.2.4", "", { "dependencies": { "loose-envify": "^1.0.0" } }, "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA=="], + "inversify": ["inversify@7.5.1", "", { "dependencies": { "@inversifyjs/common": "1.5.0", "@inversifyjs/container": "1.9.1", "@inversifyjs/core": "5.2.0" }, "peerDependencies": { "reflect-metadata": "~0.2.2" } }, "sha512-oWWFDU2BXiuqZynGcLDDD4tqiWAeaEUCALlCj0vk/QwHGgZY/tzDFPuTmWMY3jwRmDIHxVDkcj9SL3MFTlCEDw=="], + "ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="], "iron-webcrypto": ["iron-webcrypto@1.2.1", "", {}, "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg=="], @@ -1881,7 +1836,7 @@ "isomorphic-timers-promises": ["isomorphic-timers-promises@1.0.1", "", {}, "sha512-u4sej9B1LPSxTGKB/HiuzvEQnXH0ECYkSVQU39koSwmFAxhlEAFl9RdTvLv4TOTQUgBS5O3O5fwUxk6byBZ+IQ=="], - "isomorphic-ws": ["isomorphic-ws@4.0.1", "", { "peerDependencies": { "ws": "*" } }, "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w=="], + "isomorphic-ws": ["isomorphic-ws@5.0.0", "", { "peerDependencies": { "ws": "*" } }, "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw=="], "isows": ["isows@1.0.7", "", { "peerDependencies": { "ws": "*" } }, "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg=="], @@ -1895,8 +1850,6 @@ "js-sha256": ["js-sha256@0.9.0", "", {}, "sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA=="], - "js-sha3": ["js-sha3@0.8.0", "", {}, "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q=="], - "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], @@ -1925,14 +1878,10 @@ "jwt-decode": ["jwt-decode@4.0.0", "", {}, "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA=="], - "keccak": ["keccak@3.0.4", "", { "dependencies": { "node-addon-api": "^2.0.0", "node-gyp-build": "^4.2.0", "readable-stream": "^3.6.0" } }, "sha512-3vKuW0jV8J3XNTzvfyicFR5qvxrSAGl7KIhvgOu5cmWwM7tZRj3fMbj/pfIf4be7aznbc+prBWGjywox/g2Y6Q=="], - "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], "keyvaluestorage-interface": ["keyvaluestorage-interface@1.0.0", "", {}, "sha512-8t6Q3TclQ4uZynJY9IGr2+SsIGwK9JHcO6ootkHCGA0CrQCRy+VkouYNO2xicET6b9al7QKzpebNow+gkpCL8g=="], - "ledger-bitcoin": ["ledger-bitcoin@0.3.0", "", { "dependencies": { "@bitcoinerlab/descriptors": "^1.1.1", "@bitcoinerlab/secp256k1": "^1.2.0", "@ledgerhq/hw-transport": "^6.31.13", "bip32-path": "^0.4.2", "bitcoinjs-lib": "^6.1.7" } }, "sha512-gaTCdvnqFMvK6i6112MCcsNC+DDhUx1EKPDgqYoLswgwWLyb+EoZjXI0VxxkW9m7VszQdpOv210xNdohmQ6knw=="], - "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], @@ -1947,8 +1896,6 @@ "lodash": ["lodash@4.18.1", "", {}, "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q=="], - "lodash.clonedeep": ["lodash.clonedeep@4.5.0", "", {}, "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ=="], - "lodash.startcase": ["lodash.startcase@4.4.0", "", {}, "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg=="], "long": ["long@4.0.0", "", {}, "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA=="], @@ -2169,7 +2116,7 @@ "proxy-compare": ["proxy-compare@2.5.1", "", {}, "sha512-oyfc0Tx87Cpwva5ZXezSp5V9vht1c7dZBhvuV/y3ctkgMVUmiAGDVeeB0dKhGSyT0v1ZTEQYpe/RXlBVBNuCLA=="], - "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], + "proxy-from-env": ["proxy-from-env@2.1.0", "", {}, "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA=="], "public-encrypt": ["public-encrypt@4.0.3", "", { "dependencies": { "bn.js": "^4.1.0", "browserify-rsa": "^4.0.0", "create-hash": "^1.1.0", "parse-asn1": "^5.0.0", "randombytes": "^2.0.1", "safe-buffer": "^5.1.2" } }, "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q=="], @@ -2177,6 +2124,8 @@ "punycode": ["punycode@1.4.1", "", {}, "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ=="], + "purify-ts": ["purify-ts@2.1.0", "", { "dependencies": { "@types/json-schema": "7.0.15" } }, "sha512-+KNUHV9FxB9BbjadFdvxa+LNJIaqZmSF7CQH5Rv6+f0rBzsxm9FEqrvkALQbWYJobAja2ZCbBDUY7O4fH2znMA=="], + "pushdata-bitcoin": ["pushdata-bitcoin@1.0.1", "", { "dependencies": { "bitcoin-ops": "^1.3.0" } }, "sha512-hw7rcYTJRAl4olM8Owe8x0fBuJJ+WGbMhQuLWOXEMN3PxPCKQHRkhfL+XG0+iXUmSHjkMmb3Ba55Mt21cZc9kQ=="], "qr-code-styling": ["qr-code-styling@1.9.2", "", { "dependencies": { "qrcode-generator": "^1.4.4" } }, "sha512-RgJaZJ1/RrXJ6N0j7a+pdw3zMBmzZU4VN2dtAZf8ZggCfRB5stEQ3IoDNGaNhYY3nnZKYlYSLl5YkfWN5dPutg=="], @@ -2235,9 +2184,7 @@ "redeyed": ["redeyed@2.1.1", "", { "dependencies": { "esprima": "~4.0.0" } }, "sha512-FNpGGo1DycYAdnrKFxCMmKYgo/mILAqtRYbkdQD8Ep/Hk2PQ5+aEAEx+IU713RTDmuBaH0c8P5ZozurNu5ObRQ=="], - "redux": ["redux@5.0.1", "", {}, "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w=="], - - "redux-thunk": ["redux-thunk@3.1.0", "", { "peerDependencies": { "redux": "^5.0.0" } }, "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw=="], + "reflect-metadata": ["reflect-metadata@0.2.2", "", {}, "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q=="], "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], @@ -2245,8 +2192,6 @@ "require-main-filename": ["require-main-filename@2.0.0", "", {}, "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="], - "reselect": ["reselect@5.1.1", "", {}, "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="], - "resolve": ["resolve@1.22.12", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="], "resolve-alpn": ["resolve-alpn@1.2.1", "", {}, "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g=="], @@ -2471,9 +2416,7 @@ "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], - "utility-types": ["utility-types@3.11.0", "", {}, "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw=="], - - "uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="], + "uuid": ["uuid@11.0.3", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg=="], "valibot": ["valibot@0.42.1", "", { "peerDependencies": { "typescript": ">=5" }, "optionalPeers": ["typescript"] }, "sha512-3keXV29Ar5b//Hqi4MbSdV7lfVp6zuYLZuA9V1PvQUsXqogr+u5lvLPLk3A4f74VUXDnf/JfWMN6sB+koJ/FFw=="], @@ -2517,6 +2460,8 @@ "xrpl": ["xrpl@4.6.0", "", { "dependencies": { "@scure/bip32": "^1.3.1", "@scure/bip39": "^1.2.1", "@xrplf/isomorphic": "^1.0.1", "@xrplf/secret-numbers": "^2.0.0", "bignumber.js": "^9.0.0", "eventemitter3": "^5.0.1", "fast-json-stable-stringify": "^2.1.0", "ripple-address-codec": "^5.0.0", "ripple-binary-codec": "^2.7.0", "ripple-keypairs": "^2.0.0" } }, "sha512-0nXZfqDHRJ6bsDv1WtA9MdCYalMtXuxVa9mtLdqT3xypRKf2LwT5DbuGL/kHcVfuqk3B+ly+SFARlrnX+LHtRQ=="], + "xstate": ["xstate@5.19.2", "", {}, "sha512-B8fL2aP0ogn5aviAXFzI5oZseAMqN00fg/TeDa3ZtatyDcViYLIfuQl4y8qmHCiKZgGEzmnTyNtNQL9oeJE2gw=="], + "xstream": ["xstream@11.14.0", "", { "dependencies": { "globalthis": "^1.0.1", "symbol-observable": "^2.0.3" } }, "sha512-1bLb+kKKtKPbgTK6i/BaoAn03g47PpFstlbe1BA+y3pNS/LfvcaghS5BFf9+EE1J+KwSQsEpfJvFN5GqFtiNmw=="], "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="], @@ -2563,8 +2508,6 @@ "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "@bitcoinerlab/secp256k1/@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], - "@coinbase/wallet-sdk/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], "@coinbase/wallet-sdk/clsx": ["clsx@1.2.1", "", {}, "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg=="], @@ -2577,8 +2520,16 @@ "@cosmjs/encoding/@scure/base": ["@scure/base@2.0.0", "", {}, "sha512-3E1kpuZginKkek01ovG8krQ0Z44E3DHPjc5S2rjJw9lZn3KSQOs8S7wqikF/AH7iRanHypj85uGyxk0XAyC37w=="], + "@cosmjs/proto-signing/@cosmjs/amino": ["@cosmjs/amino@0.37.1", "", { "dependencies": { "@cosmjs/crypto": "^0.37.1", "@cosmjs/encoding": "^0.37.1", "@cosmjs/math": "^0.37.1", "@cosmjs/utils": "^0.37.1" } }, "sha512-z3QSfw2S2kGbi5XBahOdAIu2Nnb7XiLgTOphZQoOJXJcTQemAG/wYHLJD9TcQ48SX+eDTJ4i7XRyJaIdg0ClcA=="], + + "@cosmjs/socket/isomorphic-ws": ["isomorphic-ws@4.0.1", "", { "peerDependencies": { "ws": "*" } }, "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w=="], + "@cosmjs/socket/ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="], + "@cosmjs/stargate/@cosmjs/amino": ["@cosmjs/amino@0.37.1", "", { "dependencies": { "@cosmjs/crypto": "^0.37.1", "@cosmjs/encoding": "^0.37.1", "@cosmjs/math": "^0.37.1", "@cosmjs/utils": "^0.37.1" } }, "sha512-z3QSfw2S2kGbi5XBahOdAIu2Nnb7XiLgTOphZQoOJXJcTQemAG/wYHLJD9TcQ48SX+eDTJ4i7XRyJaIdg0ClcA=="], + + "@cosmjs/stargate/@cosmjs/proto-signing": ["@cosmjs/proto-signing@0.37.1", "", { "dependencies": { "@cosmjs/amino": "^0.37.1", "@cosmjs/crypto": "^0.37.1", "@cosmjs/encoding": "^0.37.1", "@cosmjs/math": "^0.37.1", "@cosmjs/utils": "^0.37.1", "cosmjs-types": "^0.10.1" } }, "sha512-cCUfejHOfjx6L/noF9QnXIy4dKoTKl56NFIqz2cizx+kZKszdL0yOtQlu0dD9nR+uMafRHU7aok6v5HH4RZIDw=="], + "@cosmjs/tendermint-rpc/@cosmjs/crypto": ["@cosmjs/crypto@0.37.1", "", { "dependencies": { "@cosmjs/encoding": "^0.37.1", "@cosmjs/math": "^0.37.1", "@cosmjs/utils": "^0.37.1", "@noble/ciphers": "^1.3.0", "@noble/curves": "^1.9.2", "@noble/hashes": "^1.8.0", "@scure/bip39": "^1.6.0", "hash-wasm": "^4.12.0" } }, "sha512-CdLOKQVJM422UegKpi4T/xkdNwvLZwJwSC7NyYi7tAyty0NzP67u+vNqar7Ok22ECkKLj6N9uRcIl4g62oZThA=="], "@ethereumjs/tx/@noble/curves": ["@noble/curves@2.0.1", "", { "dependencies": { "@noble/hashes": "2.0.1" } }, "sha512-vs1Az2OOTBiP4q0pwjW5aF0xp9n4MxVrmkFBxc6EKZc6ddYx5gaZiAsZoq0uRRXWbi3AT/sBqn05eRPtn1JCPw=="], @@ -2589,15 +2540,21 @@ "@ethereumjs/util/@noble/hashes": ["@noble/hashes@2.0.1", "", {}, "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw=="], - "@keplr-wallet/types/starknet": ["starknet@9.2.2", "", { "dependencies": { "@noble/curves": "~1.7.0", "@noble/hashes": "~1.6.0", "@scure/base": "~1.2.1", "@scure/starknet": "1.1.0", "@starknet-io/get-starknet-wallet-standard": "^5.0.0", "@starknet-io/starknet-types-010": "npm:@starknet-io/types-js@0.10.0", "@starknet-io/starknet-types-09": "npm:@starknet-io/types-js@~0.9.1", "abi-wan-kanabi": "2.2.4", "lossless-json": "^4.2.0", "pako": "^2.0.4", "ts-mixer": "^6.0.3" } }, "sha512-wYgszbsEWowlc8/PSJGs38kQ54fMYrP5UwN/HpiOUwI1wRFJm7gbTOyrs3fA2UhnScawtBU5besuYkb/nxWm/A=="], + "@ledgerhq/context-module/bs58": ["bs58@6.0.0", "", { "dependencies": { "base-x": "^5.0.0" } }, "sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw=="], + + "@ledgerhq/context-module/ethers": ["ethers@6.14.1", "", { "dependencies": { "@adraffy/ens-normalize": "1.10.1", "@noble/curves": "1.2.0", "@noble/hashes": "1.3.2", "@types/node": "22.7.5", "aes-js": "4.0.0-beta.5", "tslib": "2.7.0", "ws": "8.17.1" } }, "sha512-JnFiPFi3sK2Z6y7jZ3qrafDMwiXmU+6cNZ0M+kPq+mTy9skqEzwqAdFW3nb/em2xjlIVXX6Lz8ID6i3LmS4+fQ=="], + + "@ledgerhq/device-management-kit/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], - "@ledgerhq/client-ids/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="], + "@ledgerhq/device-management-kit/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], - "@ledgerhq/domain-service/@ledgerhq/errors": ["@ledgerhq/errors@6.33.0", "", {}, "sha512-9iMdjB0Hfxfd8HGMVM0TDPfxBXhJ4MtGKyFlm8aveSHSXjcTjGtCkcnk4OUZ6ZHopBb69BFR+HC2N/ob9xuNgA=="], + "@ledgerhq/device-management-kit/ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], - "@ledgerhq/domain-service/react": ["react@19.0.0", "", {}, "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ=="], + "@ledgerhq/device-signer-kit-ethereum/ethers": ["ethers@6.14.1", "", { "dependencies": { "@adraffy/ens-normalize": "1.10.1", "@noble/curves": "1.2.0", "@noble/hashes": "1.3.2", "@types/node": "22.7.5", "aes-js": "4.0.0-beta.5", "tslib": "2.7.0", "ws": "8.17.1" } }, "sha512-JnFiPFi3sK2Z6y7jZ3qrafDMwiXmU+6cNZ0M+kPq+mTy9skqEzwqAdFW3nb/em2xjlIVXX6Lz8ID6i3LmS4+fQ=="], - "@ledgerhq/domain-service/react-dom": ["react-dom@19.0.0", "", { "dependencies": { "scheduler": "^0.25.0" }, "peerDependencies": { "react": "^19.0.0" } }, "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ=="], + "@ledgerhq/device-signer-kit-ethereum/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + + "@ledgerhq/device-signer-kit-zcash/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], "@ledgerhq/hw-app-btc/@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], @@ -2607,19 +2564,9 @@ "@ledgerhq/hw-app-btc/bs58": ["bs58@4.0.1", "", { "dependencies": { "base-x": "^3.0.2" } }, "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw=="], - "@ledgerhq/hw-app-eth/@ledgerhq/errors": ["@ledgerhq/errors@6.33.0", "", {}, "sha512-9iMdjB0Hfxfd8HGMVM0TDPfxBXhJ4MtGKyFlm8aveSHSXjcTjGtCkcnk4OUZ6ZHopBb69BFR+HC2N/ob9xuNgA=="], - - "@ledgerhq/hw-transport-mocker/@ledgerhq/hw-transport": ["@ledgerhq/hw-transport@6.35.0", "", { "dependencies": { "@ledgerhq/devices": "8.14.0", "@ledgerhq/errors": "^6.33.0", "@ledgerhq/logs": "^6.17.0", "events": "^3.3.0" } }, "sha512-BFk4Zr5gfzT40RT6nj0NQTNB25bxWD3QWhdrOA6Mi/CChFTdGomjOuAhWHla2kH50ft8M5UsiiUeHoR+56J69g=="], - "@ledgerhq/psbtv2/bitcoinjs-lib": ["bitcoinjs-lib@5.2.0", "", { "dependencies": { "bech32": "^1.1.2", "bip174": "^2.0.1", "bip32": "^2.0.4", "bip66": "^1.1.0", "bitcoin-ops": "^1.4.0", "bs58check": "^2.0.0", "create-hash": "^1.1.0", "create-hmac": "^1.1.3", "merkle-lib": "^2.0.10", "pushdata-bitcoin": "^1.0.1", "randombytes": "^2.0.1", "tiny-secp256k1": "^1.1.1", "typeforce": "^1.11.3", "varuint-bitcoin": "^1.0.4", "wif": "^2.0.1" } }, "sha512-5DcLxGUDejgNBYcieMIUfjORtUeNWl828VWLHJGVKZCb4zIS1oOySTUr0LGmcqJBQgTBz3bGbRQla4FgrdQEIQ=="], - "@ledgerhq/wallet-api-core/@ledgerhq/errors": ["@ledgerhq/errors@6.33.0", "", {}, "sha512-9iMdjB0Hfxfd8HGMVM0TDPfxBXhJ4MtGKyFlm8aveSHSXjcTjGtCkcnk4OUZ6ZHopBb69BFR+HC2N/ob9xuNgA=="], - - "@ledgerhq/wallet-api-core/@ton/core": ["@ton/core@0.62.1", "", { "peerDependencies": { "@ton/crypto": ">=3.2.0" } }, "sha512-RaEGBo9gCf6ZHyS8SKq1K53pswvYW9E5A6vwUuzFBTRX14g4qMDlB9F+fq4aBE5kN7XyVr8ScQtOJfQj41usCw=="], - - "@ledgerhq/wallet-api-core/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="], - - "@ledgerhq/wallet-api-core/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "@ledgerhq/signer-utils/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], "@manypkg/find-root/@types/node": ["@types/node@12.20.55", "", {}, "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ=="], @@ -2629,10 +2576,16 @@ "@manypkg/get-packages/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], + "@metamask/connect-multichain/uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="], + "@metamask/connect-multichain/ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], + "@metamask/mobile-wallet-protocol-core/uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="], + "@metamask/mobile-wallet-protocol-dapp-client/@metamask/utils": ["@metamask/utils@9.3.0", "", { "dependencies": { "@ethereumjs/tx": "^4.2.0", "@metamask/superstruct": "^3.1.0", "@noble/hashes": "^1.3.1", "@scure/base": "^1.1.3", "@types/debug": "^4.1.7", "debug": "^4.3.4", "pony-cause": "^2.1.10", "semver": "^7.5.4", "uuid": "^9.0.1" } }, "sha512-w8CVbdkDrVXFJbfBSlDfafDR6BAkpDmv1bC1UJVCoVny5tW2RKAdn9i68Xf7asYT4TnUhl/hN4zfUiKQq9II4g=="], + "@metamask/mobile-wallet-protocol-dapp-client/uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="], + "@metamask/utils/@ethereumjs/tx": ["@ethereumjs/tx@4.2.0", "", { "dependencies": { "@ethereumjs/common": "^3.2.0", "@ethereumjs/rlp": "^4.0.1", "@ethereumjs/util": "^8.1.0", "ethereum-cryptography": "^2.0.0" } }, "sha512-1nc6VO4jtFd172BbSnTnDQVr9IYBFl1y4xPzZdtkrkKIncBCkdbgfdRV+MiTkJYAtTxvV12GRZLqBFT1PNK6Yw=="], "@metamask/utils/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], @@ -2671,12 +2624,22 @@ "@mysten/utils/@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="], + "@near-js/accounts/@near-js/crypto": ["@near-js/crypto@2.5.1", "", { "dependencies": { "@near-js/types": "2.5.1", "@near-js/utils": "2.5.1", "@noble/curves": "1.8.1", "@noble/hashes": "^1.7.1", "borsh": "1.0.0", "secp256k1": "5.0.1" } }, "sha512-Kb+bbnUrfvuzzed9hpLRpcIlCMOaQlw/7BxlZPCq8DggVaK1m0nKR1DHQs2cV0Q0WSKSk2UrFJho1dxeKL5alg=="], + + "@near-js/accounts/@near-js/transactions": ["@near-js/transactions@2.5.1", "", { "dependencies": { "@near-js/crypto": "2.5.1", "@near-js/types": "2.5.1", "@near-js/utils": "2.5.1", "@noble/hashes": "1.7.1", "borsh": "1.0.0" } }, "sha512-0svK5K6VqOciSIn9mxreyS0uQf8T1UZc71JBA6yrstpHJhltz/dvNUMY0FV+xHx3H/KAdvCMjX0VLEpyi08yWQ=="], + "@near-js/accounts/@noble/hashes": ["@noble/hashes@1.7.1", "", {}, "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ=="], + "@near-js/crypto/@near-js/types": ["@near-js/types@2.5.0", "", {}, "sha512-U0HgAnRwN/XKJ1O4Htac+G5bvkMalBJ8i/kwAPJpoOI1MUtP9dhbCG4GBErADCpczukhrAWlDBHHdfhPnhgyzQ=="], + + "@near-js/crypto/@near-js/utils": ["@near-js/utils@2.5.0", "", { "dependencies": { "@near-js/types": "2.5.0", "@scure/base": "^1.2.4", "depd": "2.0.0", "mustache": "4.0.0" } }, "sha512-hifdeZVzV9TFcgLNekRVVnB6q7zxdCQPmC9cybT1SjfeiIP/5jPabOZXGWNJPSloRcrTMluCTtaWinIw1HXanQ=="], + "@near-js/crypto/@noble/curves": ["@noble/curves@1.8.1", "", { "dependencies": { "@noble/hashes": "1.7.1" } }, "sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ=="], "@near-js/crypto/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], + "@near-js/keystores/@near-js/crypto": ["@near-js/crypto@2.5.1", "", { "dependencies": { "@near-js/types": "2.5.1", "@near-js/utils": "2.5.1", "@noble/curves": "1.8.1", "@noble/hashes": "^1.7.1", "borsh": "1.0.0", "secp256k1": "5.0.1" } }, "sha512-Kb+bbnUrfvuzzed9hpLRpcIlCMOaQlw/7BxlZPCq8DggVaK1m0nKR1DHQs2cV0Q0WSKSk2UrFJho1dxeKL5alg=="], + "@near-js/keystores-browser/@near-js/crypto": ["@near-js/crypto@1.2.1", "", { "dependencies": { "@near-js/types": "0.0.4", "@near-js/utils": "0.1.0", "@noble/curves": "1.2.0", "bn.js": "5.2.1", "borsh": "1.0.0", "randombytes": "2.1.0" } }, "sha512-iJOHaGKvdudYfR8nEtRhGlgcTEHeVmxMoT0JVXmuP3peG96v/sSnA03CE6MZBeCC8txKAQOffagxE7oU6hJp9g=="], "@near-js/keystores-browser/@near-js/keystores": ["@near-js/keystores@0.0.9", "", { "dependencies": { "@near-js/crypto": "1.2.1", "@near-js/types": "0.0.4" } }, "sha512-j8ySgVEcm2Gg6zxkSdadNtPlIqhJZdPGfWWM3tPtEoowNS9snhwZn5NRFPrgmX0+MzpF7E091CRcY90MvRVhsg=="], @@ -2685,10 +2648,22 @@ "@near-js/keystores-node/@near-js/keystores": ["@near-js/keystores@0.0.9", "", { "dependencies": { "@near-js/crypto": "1.2.1", "@near-js/types": "0.0.4" } }, "sha512-j8ySgVEcm2Gg6zxkSdadNtPlIqhJZdPGfWWM3tPtEoowNS9snhwZn5NRFPrgmX0+MzpF7E091CRcY90MvRVhsg=="], + "@near-js/providers/@near-js/crypto": ["@near-js/crypto@2.5.1", "", { "dependencies": { "@near-js/types": "2.5.1", "@near-js/utils": "2.5.1", "@noble/curves": "1.8.1", "@noble/hashes": "^1.7.1", "borsh": "1.0.0", "secp256k1": "5.0.1" } }, "sha512-Kb+bbnUrfvuzzed9hpLRpcIlCMOaQlw/7BxlZPCq8DggVaK1m0nKR1DHQs2cV0Q0WSKSk2UrFJho1dxeKL5alg=="], + + "@near-js/providers/@near-js/transactions": ["@near-js/transactions@2.5.1", "", { "dependencies": { "@near-js/crypto": "2.5.1", "@near-js/types": "2.5.1", "@near-js/utils": "2.5.1", "@noble/hashes": "1.7.1", "borsh": "1.0.0" } }, "sha512-0svK5K6VqOciSIn9mxreyS0uQf8T1UZc71JBA6yrstpHJhltz/dvNUMY0FV+xHx3H/KAdvCMjX0VLEpyi08yWQ=="], + "@near-js/providers/node-fetch": ["node-fetch@2.6.7", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ=="], + "@near-js/signers/@near-js/crypto": ["@near-js/crypto@2.5.1", "", { "dependencies": { "@near-js/types": "2.5.1", "@near-js/utils": "2.5.1", "@noble/curves": "1.8.1", "@noble/hashes": "^1.7.1", "borsh": "1.0.0", "secp256k1": "5.0.1" } }, "sha512-Kb+bbnUrfvuzzed9hpLRpcIlCMOaQlw/7BxlZPCq8DggVaK1m0nKR1DHQs2cV0Q0WSKSk2UrFJho1dxeKL5alg=="], + + "@near-js/signers/@near-js/transactions": ["@near-js/transactions@2.5.1", "", { "dependencies": { "@near-js/crypto": "2.5.1", "@near-js/types": "2.5.1", "@near-js/utils": "2.5.1", "@noble/hashes": "1.7.1", "borsh": "1.0.0" } }, "sha512-0svK5K6VqOciSIn9mxreyS0uQf8T1UZc71JBA6yrstpHJhltz/dvNUMY0FV+xHx3H/KAdvCMjX0VLEpyi08yWQ=="], + "@near-js/signers/@noble/hashes": ["@noble/hashes@1.7.1", "", {}, "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ=="], + "@near-js/transactions/@near-js/types": ["@near-js/types@2.5.0", "", {}, "sha512-U0HgAnRwN/XKJ1O4Htac+G5bvkMalBJ8i/kwAPJpoOI1MUtP9dhbCG4GBErADCpczukhrAWlDBHHdfhPnhgyzQ=="], + + "@near-js/transactions/@near-js/utils": ["@near-js/utils@2.5.0", "", { "dependencies": { "@near-js/types": "2.5.0", "@scure/base": "^1.2.4", "depd": "2.0.0", "mustache": "4.0.0" } }, "sha512-hifdeZVzV9TFcgLNekRVVnB6q7zxdCQPmC9cybT1SjfeiIP/5jPabOZXGWNJPSloRcrTMluCTtaWinIw1HXanQ=="], + "@near-js/transactions/@noble/hashes": ["@noble/hashes@1.7.1", "", {}, "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ=="], "@near-js/utils/@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="], @@ -2709,6 +2684,10 @@ "@near-js/wallet-account/bn.js": ["bn.js@5.2.1", "", {}, "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ=="], + "@near-wallet-selector/core/@near-js/crypto": ["@near-js/crypto@2.5.1", "", { "dependencies": { "@near-js/types": "2.5.1", "@near-js/utils": "2.5.1", "@noble/curves": "1.8.1", "@noble/hashes": "^1.7.1", "borsh": "1.0.0", "secp256k1": "5.0.1" } }, "sha512-Kb+bbnUrfvuzzed9hpLRpcIlCMOaQlw/7BxlZPCq8DggVaK1m0nKR1DHQs2cV0Q0WSKSk2UrFJho1dxeKL5alg=="], + + "@near-wallet-selector/core/@near-js/transactions": ["@near-js/transactions@2.5.1", "", { "dependencies": { "@near-js/crypto": "2.5.1", "@near-js/types": "2.5.1", "@near-js/utils": "2.5.1", "@noble/hashes": "1.7.1", "borsh": "1.0.0" } }, "sha512-0svK5K6VqOciSIn9mxreyS0uQf8T1UZc71JBA6yrstpHJhltz/dvNUMY0FV+xHx3H/KAdvCMjX0VLEpyi08yWQ=="], + "@near-wallet-selector/core/borsh": ["borsh@2.0.0", "", {}, "sha512-kc9+BgR3zz9+cjbwM8ODoUB4fs3X3I5A/HtX7LZKxCLaMrEeDFoBpnhZY//DTS1VZBSs6S5v46RZRbZjRFspEg=="], "@near-wallet-selector/core/rxjs": ["rxjs@7.8.1", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg=="], @@ -2811,12 +2790,16 @@ "@radixdlt/radix-dapp-toolkit/uuid": ["uuid@10.0.0", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="], - "@reduxjs/toolkit/immer": ["immer@11.1.4", "", {}, "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw=="], - "@scure/starknet/@noble/curves": ["@noble/curves@1.7.0", "", { "dependencies": { "@noble/hashes": "1.6.0" } }, "sha512-UTMhXK9SeDhFJVrHeUJ5uZlI6ajXg10O6Ddocf9S6GjbSBVZsJo88HzKwXznNfGpMTRDyJkqMjNDPYgf0qFWnw=="], "@scure/starknet/@noble/hashes": ["@noble/hashes@1.6.1", "", {}, "sha512-pq5D8h10hHBjyqX+cfBm0i8JUXJ0UhczFc4r74zbuT9XgewFo2E3J1cOaGtdZynILNmQ685YWGzGE1Zv6io50w=="], + "@sentry/hub/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], + + "@sentry/minimal/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], + + "@sentry/utils/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], + "@serenity-kit/noble-sodium/@noble/ciphers": ["@noble/ciphers@2.2.0", "", {}, "sha512-Z6pjIZ/8IJcCGzb2S/0Px5J81yij85xASuk1teLNeg75bfT07MV3a/O2Mtn1I2se43k3lkVEcFaR10N4cgQcZA=="], "@solana/accounts/@solana/codecs-strings": ["@solana/codecs-strings@2.3.0", "", { "dependencies": { "@solana/codecs-core": "2.3.0", "@solana/codecs-numbers": "2.3.0", "@solana/errors": "2.3.0" }, "peerDependencies": { "fastestsmallesttextencoderdecoder": "^1.0.22", "typescript": ">=5.3.3" } }, "sha512-y5pSBYwzVziXu521hh+VxqUtp0hYGTl1eWGoc1W+8mdvBdC1kTqm/X7aYQw33J42hw03JjryvYOvmGgk3Qz/Ug=="], @@ -2875,32 +2858,28 @@ "@solana/web3.js/bs58": ["bs58@4.0.1", "", { "dependencies": { "base-x": "^3.0.2" } }, "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw=="], - "@stacks/common/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], - - "@stacks/network/cross-fetch": ["cross-fetch@3.2.0", "", { "dependencies": { "node-fetch": "^2.7.0" } }, "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q=="], - - "@stacks/transactions/@noble/hashes": ["@noble/hashes@1.1.5", "", {}, "sha512-LTMZiiLc+V4v1Yi16TD6aX2gmtKszNye0pQgbaLqkvhIqP7nVsSaJsWloGQjJfJ8offaoP5GtX3yY5swbcJxxQ=="], - - "@stacks/transactions/@noble/secp256k1": ["@noble/secp256k1@1.7.1", "", {}, "sha512-hOUk6AyBFmqVrv7k5WAw/LpszxVbj9gGN4JRkIX52fdFAj1UA61KXmZDvqVEm+pOyec3+fIeZB02LYa/pWOArw=="], - "@starknet-io/get-starknet-wallet-standard/@wallet-standard/base": ["@wallet-standard/base@1.1.0", "", {}, "sha512-DJDQhjKmSNVLKWItoKThJS+CsJQjR9AOBOirBVT1F9YpRyC9oYHE+ZnSf8y8bxUphtKqdQMPVQ2mHohYdRvDVQ=="], "@starknet-io/get-starknet-wallet-standard/ox": ["ox@0.4.4", "", { "dependencies": { "@adraffy/ens-normalize": "^1.10.1", "@noble/curves": "^1.6.0", "@noble/hashes": "^1.5.0", "@scure/bip32": "^1.5.0", "@scure/bip39": "^1.4.0", "abitype": "^1.0.6", "eventemitter3": "5.0.1" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-oJPEeCDs9iNiPs6J0rTx+Y0KGeCGyCAA3zo94yZhm8G5WpOxrwUtn2Ie/Y8IyARSqqY/j9JTKA3Fc1xs1DvFnw=="], "@stellar/stellar-base/@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], - "@stellar/stellar-sdk/axios": ["axios@1.15.0", "", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^2.1.0" } }, "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q=="], - "@stricahq/typhonjs/@stricahq/cbors": ["@stricahq/cbors@1.0.2", "", { "dependencies": { "bignumber.js": "^9.0.2", "buffer": "^6.0.3" } }, "sha512-6ePsEiq7EGHA5IiPn9poA7sF5iXPqt30kKw3pjR/BhP7S+XHZNu/OPumESWnVl4AM+IEYC2x9eL+4qRPsTPVww=="], "@swapkit/core/@stricahq/typhonjs": ["@stricahq/typhonjs@3.1.0", "", { "dependencies": { "@stricahq/cbors": "1.0.2", "bech32": "^2.0.0", "bignumber.js": "^9.0.1", "blakejs": "^1.2.1", "bs58": "^5.0.0", "buffer": "^6.0.3", "lodash": "^4.17.21" } }, "sha512-Eaw0qqNIz+6sA9zD/9pFCdQ+gNbCv8PKFMhXu4Vq6KSR1XsH40jNPFkrqgKaq8q+gbO569U1+tu41dHk40JNbw=="], "@swapkit/core/cosmjs-types": ["cosmjs-types@0.11.0", "", {}, "sha512-kDSkgHpRTrg1413jCNehT3P21+EBxZWFMBr9JEzVfmPiNdtuwAoLAkCYo7c7i/pTakAwyHsXbxOg8kkD+AN33w=="], + "@swapkit/plugins/@near-js/transactions": ["@near-js/transactions@2.5.1", "", { "dependencies": { "@near-js/crypto": "2.5.1", "@near-js/types": "2.5.1", "@near-js/utils": "2.5.1", "@noble/hashes": "1.7.1", "borsh": "1.0.0" } }, "sha512-0svK5K6VqOciSIn9mxreyS0uQf8T1UZc71JBA6yrstpHJhltz/dvNUMY0FV+xHx3H/KAdvCMjX0VLEpyi08yWQ=="], + "@swapkit/server/@swapkit/tokens": ["@swapkit/tokens@4.4.1", "", { "peerDependencies": { "ts-pattern": "5.9.0" } }, "sha512-PVHduHv3RTc4aY9pHDvge9t0rrChJA5xGkoYd34E/PO9t/3gWRb4ABHVmakLxtLJnh5hb/McSha5y1HGzvzVEQ=="], "@swapkit/toolboxes/@cosmjs/crypto": ["@cosmjs/crypto@0.39.0", "", { "dependencies": { "@cosmjs/encoding": "^0.39.0", "@cosmjs/math": "^0.39.0", "@cosmjs/utils": "^0.39.0", "@noble/ciphers": "^2.1.1", "@noble/curves": "^2.0.1", "@noble/hashes": "^2.0.1", "@scure/bip39": "^2.0.1" } }, "sha512-ATRhSXN8w3fvUkj9xzLHwvzylDvvn4f3cC1CQwhQc2OxyzpEEFACS3wHS6iwdJJS99acV8dq+oVOYflUqI0brQ=="], + "@swapkit/toolboxes/@near-js/crypto": ["@near-js/crypto@2.5.1", "", { "dependencies": { "@near-js/types": "2.5.1", "@near-js/utils": "2.5.1", "@noble/curves": "1.8.1", "@noble/hashes": "^1.7.1", "borsh": "1.0.0", "secp256k1": "5.0.1" } }, "sha512-Kb+bbnUrfvuzzed9hpLRpcIlCMOaQlw/7BxlZPCq8DggVaK1m0nKR1DHQs2cV0Q0WSKSk2UrFJho1dxeKL5alg=="], + + "@swapkit/toolboxes/@near-js/transactions": ["@near-js/transactions@2.5.1", "", { "dependencies": { "@near-js/crypto": "2.5.1", "@near-js/types": "2.5.1", "@near-js/utils": "2.5.1", "@noble/hashes": "1.7.1", "borsh": "1.0.0" } }, "sha512-0svK5K6VqOciSIn9mxreyS0uQf8T1UZc71JBA6yrstpHJhltz/dvNUMY0FV+xHx3H/KAdvCMjX0VLEpyi08yWQ=="], + "@swapkit/toolboxes/@stricahq/typhonjs": ["@stricahq/typhonjs@3.1.0", "", { "dependencies": { "@stricahq/cbors": "1.0.2", "bech32": "^2.0.0", "bignumber.js": "^9.0.1", "blakejs": "^1.2.1", "bs58": "^5.0.0", "buffer": "^6.0.3", "lodash": "^4.17.21" } }, "sha512-Eaw0qqNIz+6sA9zD/9pFCdQ+gNbCv8PKFMhXu4Vq6KSR1XsH40jNPFkrqgKaq8q+gbO569U1+tu41dHk40JNbw=="], "@swapkit/toolboxes/cosmjs-types": ["cosmjs-types@0.11.0", "", {}, "sha512-kDSkgHpRTrg1413jCNehT3P21+EBxZWFMBr9JEzVfmPiNdtuwAoLAkCYo7c7i/pTakAwyHsXbxOg8kkD+AN33w=="], @@ -2913,6 +2892,10 @@ "@swapkit/ui/@cosmjs/stargate": ["@cosmjs/stargate@0.39.0", "", { "dependencies": { "@cosmjs/amino": "^0.39.0", "@cosmjs/encoding": "^0.39.0", "@cosmjs/math": "^0.39.0", "@cosmjs/proto-signing": "^0.39.0", "@cosmjs/stream": "^0.39.0", "@cosmjs/tendermint-rpc": "^0.39.0", "@cosmjs/utils": "^0.39.0", "cosmjs-types": "^0.11.0" } }, "sha512-dQtucU9czF2NUUbEKs19PMfv+EusDFKWHMVK6Hinytmpq9sdk5JUIln+g/8k+UjKcV4HPv6S0AywbhE+tl84kQ=="], + "@swapkit/ui/@near-js/crypto": ["@near-js/crypto@2.5.1", "", { "dependencies": { "@near-js/types": "2.5.1", "@near-js/utils": "2.5.1", "@noble/curves": "1.8.1", "@noble/hashes": "^1.7.1", "borsh": "1.0.0", "secp256k1": "5.0.1" } }, "sha512-Kb+bbnUrfvuzzed9hpLRpcIlCMOaQlw/7BxlZPCq8DggVaK1m0nKR1DHQs2cV0Q0WSKSk2UrFJho1dxeKL5alg=="], + + "@swapkit/ui/@near-js/transactions": ["@near-js/transactions@2.5.1", "", { "dependencies": { "@near-js/crypto": "2.5.1", "@near-js/types": "2.5.1", "@near-js/utils": "2.5.1", "@noble/hashes": "1.7.1", "borsh": "1.0.0" } }, "sha512-0svK5K6VqOciSIn9mxreyS0uQf8T1UZc71JBA6yrstpHJhltz/dvNUMY0FV+xHx3H/KAdvCMjX0VLEpyi08yWQ=="], + "@swapkit/ui/@polkadot/keyring": ["@polkadot/keyring@13.5.7", "", { "dependencies": { "@polkadot/util": "13.5.7", "@polkadot/util-crypto": "13.5.7", "tslib": "^2.8.0" } }, "sha512-S75K2m2AoiTMnns7ko3t72jvyJRmrqdFFPldLdPdjRuds+E8OFewcwms/aXHGn9IwViWHFX6PSx0QAzWN/qWzQ=="], "@swapkit/ui/@polkadot/util": ["@polkadot/util@13.5.7", "", { "dependencies": { "@polkadot/x-bigint": "13.5.7", "@polkadot/x-global": "13.5.7", "@polkadot/x-textdecoder": "13.5.7", "@polkadot/x-textencoder": "13.5.7", "@types/bn.js": "^5.1.6", "bn.js": "^5.2.1", "tslib": "^2.8.0" } }, "sha512-5Rhp6/FDI55iCJcGd/9bMQaF0E26OE+uZwz68JuRW75DW8v7zsN3bnjnVqk3KO/c4u5EgLSqbhXPuyW24BP1+Q=="], @@ -2943,15 +2926,17 @@ "@swapkit/wallet-hardware/@cosmjs/amino": ["@cosmjs/amino@0.37.1", "", { "dependencies": { "@cosmjs/crypto": "^0.37.1", "@cosmjs/encoding": "^0.37.1", "@cosmjs/math": "^0.37.1", "@cosmjs/utils": "^0.37.1" } }, "sha512-z3QSfw2S2kGbi5XBahOdAIu2Nnb7XiLgTOphZQoOJXJcTQemAG/wYHLJD9TcQ48SX+eDTJ4i7XRyJaIdg0ClcA=="], - "@swapkit/wallet-hardware/@cosmjs/proto-signing": ["@cosmjs/proto-signing@0.37.1", "", { "dependencies": { "@cosmjs/amino": "^0.37.1", "@cosmjs/crypto": "^0.37.1", "@cosmjs/encoding": "^0.37.1", "@cosmjs/math": "^0.37.1", "@cosmjs/utils": "^0.37.1", "cosmjs-types": "^0.10.1" } }, "sha512-cCUfejHOfjx6L/noF9QnXIy4dKoTKl56NFIqz2cizx+kZKszdL0yOtQlu0dD9nR+uMafRHU7aok6v5HH4RZIDw=="], + "@swapkit/wallet-hardware/@near-js/crypto": ["@near-js/crypto@2.5.1", "", { "dependencies": { "@near-js/types": "2.5.1", "@near-js/utils": "2.5.1", "@noble/curves": "1.8.1", "@noble/hashes": "^1.7.1", "borsh": "1.0.0", "secp256k1": "5.0.1" } }, "sha512-Kb+bbnUrfvuzzed9hpLRpcIlCMOaQlw/7BxlZPCq8DggVaK1m0nKR1DHQs2cV0Q0WSKSk2UrFJho1dxeKL5alg=="], + + "@swapkit/wallet-hardware/@near-js/transactions": ["@near-js/transactions@2.5.1", "", { "dependencies": { "@near-js/crypto": "2.5.1", "@near-js/types": "2.5.1", "@near-js/utils": "2.5.1", "@noble/hashes": "1.7.1", "borsh": "1.0.0" } }, "sha512-0svK5K6VqOciSIn9mxreyS0uQf8T1UZc71JBA6yrstpHJhltz/dvNUMY0FV+xHx3H/KAdvCMjX0VLEpyi08yWQ=="], "@swapkit/wallets/@cosmjs/amino": ["@cosmjs/amino@0.37.1", "", { "dependencies": { "@cosmjs/crypto": "^0.37.1", "@cosmjs/encoding": "^0.37.1", "@cosmjs/math": "^0.37.1", "@cosmjs/utils": "^0.37.1" } }, "sha512-z3QSfw2S2kGbi5XBahOdAIu2Nnb7XiLgTOphZQoOJXJcTQemAG/wYHLJD9TcQ48SX+eDTJ4i7XRyJaIdg0ClcA=="], "@swapkit/wallets/@cosmjs/proto-signing": ["@cosmjs/proto-signing@0.37.1", "", { "dependencies": { "@cosmjs/amino": "^0.37.1", "@cosmjs/crypto": "^0.37.1", "@cosmjs/encoding": "^0.37.1", "@cosmjs/math": "^0.37.1", "@cosmjs/utils": "^0.37.1", "cosmjs-types": "^0.10.1" } }, "sha512-cCUfejHOfjx6L/noF9QnXIy4dKoTKl56NFIqz2cizx+kZKszdL0yOtQlu0dD9nR+uMafRHU7aok6v5HH4RZIDw=="], - "@swc/helpers/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "@swapkit/wallets/@near-js/transactions": ["@near-js/transactions@2.5.1", "", { "dependencies": { "@near-js/crypto": "2.5.1", "@near-js/types": "2.5.1", "@near-js/utils": "2.5.1", "@noble/hashes": "1.7.1", "borsh": "1.0.0" } }, "sha512-0svK5K6VqOciSIn9mxreyS0uQf8T1UZc71JBA6yrstpHJhltz/dvNUMY0FV+xHx3H/KAdvCMjX0VLEpyi08yWQ=="], - "@ton/ton/axios": ["axios@1.15.0", "", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^2.1.0" } }, "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q=="], + "@swc/helpers/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "@ton/ton/dataloader": ["dataloader@2.2.3", "", {}, "sha512-y2krtASINtPFS1rSDjacrFgn1dcUuoREVabwlOGOe4SdxenREqwjwjElAdwvbGM7kgZz9a3KVicWR7vcz8rnzA=="], @@ -3061,9 +3046,7 @@ "async-mutex/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "bip32/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], - - "bip32/@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="], + "bip32/@types/node": ["@types/node@10.12.18", "", {}, "sha512-fh+pAqt4xRzPfqA6eh3Z2y6fyZavRIumvjhaCL753+TVkGKGhpPeyrJG2JftD0T9q4GF00KjefsQ+PQNDdWQaQ=="], "bip39/@types/node": ["@types/node@11.11.6", "", {}, "sha512-Exw4yUWMBXM3X+8oqzJNRqZSwUAaS4+7NdvHqQuFi/d+synz++xmX3QIf+BFqneW8N31R8Ky+sikfZUXq07ggQ=="], @@ -3081,8 +3064,6 @@ "bun-types/@types/node": ["@types/node@24.10.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A=="], - "c32check/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], - "centrifuge/protobufjs": ["protobufjs@7.6.5", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.1", "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", "long": "^5.3.2" } }, "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw=="], "chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], @@ -3125,14 +3106,14 @@ "jayson/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], + "jayson/isomorphic-ws": ["isomorphic-ws@4.0.1", "", { "peerDependencies": { "ws": "*" } }, "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w=="], + "jayson/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], "jayson/ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="], "jsontokens/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], - "keccak/node-addon-api": ["node-addon-api@2.0.2", "", {}, "sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA=="], - "micro-key-producer/@noble/ciphers": ["@noble/ciphers@2.2.0", "", {}, "sha512-Z6pjIZ/8IJcCGzb2S/0Px5J81yij85xASuk1teLNeg75bfT07MV3a/O2Mtn1I2se43k3lkVEcFaR10N4cgQcZA=="], "micro-key-producer/micro-packed": ["micro-packed@0.8.0", "", { "dependencies": { "@scure/base": "2.0.0" } }, "sha512-AKb8znIvg9sooythbXzyFeChEY0SkW0C6iXECpy/ls0e5BtwXO45J9wD9SLzBztnS4XmF/5kwZknsq+jyynd/A=="], @@ -3209,6 +3190,8 @@ "rpc-websockets/@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], + "rpc-websockets/uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="], + "rpc-websockets/ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], "smoldot/ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], @@ -3235,8 +3218,6 @@ "use-sidecar/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "use-sync-external-store/react": ["react@19.1.1", "", {}, "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ=="], - "viem/@noble/curves": ["@noble/curves@1.9.1", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA=="], "viem/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], @@ -3255,42 +3236,46 @@ "xrpl/@scure/bip39": ["@scure/bip39@1.6.0", "", { "dependencies": { "@noble/hashes": "~1.8.0", "@scure/base": "~1.2.5" } }, "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A=="], + "@aptos-labs/aptos-client/axios/proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], + "@aptos-labs/ts-sdk/@scure/bip32/@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="], "@aptos-labs/ts-sdk/@scure/bip39/@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="], - "@bitcoinerlab/secp256k1/@noble/curves/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], - "@cosmjs/crypto/@scure/bip39/@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="], + "@cosmjs/proto-signing/@cosmjs/amino/@cosmjs/crypto": ["@cosmjs/crypto@0.37.1", "", { "dependencies": { "@cosmjs/encoding": "^0.37.1", "@cosmjs/math": "^0.37.1", "@cosmjs/utils": "^0.37.1", "@noble/ciphers": "^1.3.0", "@noble/curves": "^1.9.2", "@noble/hashes": "^1.8.0", "@scure/bip39": "^1.6.0", "hash-wasm": "^4.12.0" } }, "sha512-CdLOKQVJM422UegKpi4T/xkdNwvLZwJwSC7NyYi7tAyty0NzP67u+vNqar7Ok22ECkKLj6N9uRcIl4g62oZThA=="], + + "@cosmjs/stargate/@cosmjs/amino/@cosmjs/crypto": ["@cosmjs/crypto@0.37.1", "", { "dependencies": { "@cosmjs/encoding": "^0.37.1", "@cosmjs/math": "^0.37.1", "@cosmjs/utils": "^0.37.1", "@noble/ciphers": "^1.3.0", "@noble/curves": "^1.9.2", "@noble/hashes": "^1.8.0", "@scure/bip39": "^1.6.0", "hash-wasm": "^4.12.0" } }, "sha512-CdLOKQVJM422UegKpi4T/xkdNwvLZwJwSC7NyYi7tAyty0NzP67u+vNqar7Ok22ECkKLj6N9uRcIl4g62oZThA=="], + + "@cosmjs/stargate/@cosmjs/proto-signing/@cosmjs/crypto": ["@cosmjs/crypto@0.37.1", "", { "dependencies": { "@cosmjs/encoding": "^0.37.1", "@cosmjs/math": "^0.37.1", "@cosmjs/utils": "^0.37.1", "@noble/ciphers": "^1.3.0", "@noble/curves": "^1.9.2", "@noble/hashes": "^1.8.0", "@scure/bip39": "^1.6.0", "hash-wasm": "^4.12.0" } }, "sha512-CdLOKQVJM422UegKpi4T/xkdNwvLZwJwSC7NyYi7tAyty0NzP67u+vNqar7Ok22ECkKLj6N9uRcIl4g62oZThA=="], + "@cosmjs/tendermint-rpc/@cosmjs/crypto/@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], "@cosmjs/tendermint-rpc/@cosmjs/crypto/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], "@cosmjs/tendermint-rpc/@cosmjs/crypto/@scure/bip39": ["@scure/bip39@1.6.0", "", { "dependencies": { "@noble/hashes": "~1.8.0", "@scure/base": "~1.2.5" } }, "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A=="], - "@keplr-wallet/types/starknet/@noble/curves": ["@noble/curves@1.7.0", "", { "dependencies": { "@noble/hashes": "1.6.0" } }, "sha512-UTMhXK9SeDhFJVrHeUJ5uZlI6ajXg10O6Ddocf9S6GjbSBVZsJo88HzKwXznNfGpMTRDyJkqMjNDPYgf0qFWnw=="], + "@ledgerhq/context-module/bs58/base-x": ["base-x@5.0.1", "", {}, "sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg=="], - "@keplr-wallet/types/starknet/@noble/hashes": ["@noble/hashes@1.6.1", "", {}, "sha512-pq5D8h10hHBjyqX+cfBm0i8JUXJ0UhczFc4r74zbuT9XgewFo2E3J1cOaGtdZynILNmQ685YWGzGE1Zv6io50w=="], + "@ledgerhq/context-module/ethers/@noble/curves": ["@noble/curves@1.2.0", "", { "dependencies": { "@noble/hashes": "1.3.2" } }, "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw=="], - "@keplr-wallet/types/starknet/@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="], + "@ledgerhq/context-module/ethers/@noble/hashes": ["@noble/hashes@1.3.2", "", {}, "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ=="], - "@ledgerhq/domain-service/react-dom/scheduler": ["scheduler@0.25.0", "", {}, "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA=="], + "@ledgerhq/context-module/ethers/@types/node": ["@types/node@22.7.5", "", { "dependencies": { "undici-types": "~6.19.2" } }, "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ=="], - "@ledgerhq/hw-app-btc/bitcoinjs-lib/bech32": ["bech32@1.1.4", "", {}, "sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ=="], + "@ledgerhq/device-signer-kit-ethereum/ethers/@noble/curves": ["@noble/curves@1.2.0", "", { "dependencies": { "@noble/hashes": "1.3.2" } }, "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw=="], - "@ledgerhq/hw-app-btc/bitcoinjs-lib/bip32": ["bip32@2.0.6", "", { "dependencies": { "@types/node": "10.12.18", "bs58check": "^2.1.1", "create-hash": "^1.2.0", "create-hmac": "^1.1.7", "tiny-secp256k1": "^1.1.3", "typeforce": "^1.11.5", "wif": "^2.0.6" } }, "sha512-HpV5OMLLGTjSVblmrtYRfFFKuQB+GArM0+XP8HGWfJ5vxYBqo+DesvJwOdC2WJ3bCkZShGf0QIfoIpeomVzVdA=="], + "@ledgerhq/device-signer-kit-ethereum/ethers/@noble/hashes": ["@noble/hashes@1.3.2", "", {}, "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ=="], - "@ledgerhq/hw-app-btc/bs58/base-x": ["base-x@3.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA=="], + "@ledgerhq/device-signer-kit-ethereum/ethers/@types/node": ["@types/node@22.7.5", "", { "dependencies": { "undici-types": "~6.19.2" } }, "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ=="], - "@ledgerhq/hw-transport-mocker/@ledgerhq/hw-transport/@ledgerhq/devices": ["@ledgerhq/devices@8.14.0", "", { "dependencies": { "@ledgerhq/errors": "^6.33.0", "@ledgerhq/logs": "^6.17.0", "rxjs": "7.8.2", "semver": "7.7.3" } }, "sha512-eMGOaagkMJrqGtqemYNraPg38FA0I/UUEPrbrC+vOZu1qeoG+Sek3gjuJih6rMK4efUfcI4Zs19w/Hv/58fwkQ=="], + "@ledgerhq/hw-app-btc/bitcoinjs-lib/bech32": ["bech32@1.1.4", "", {}, "sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ=="], - "@ledgerhq/hw-transport-mocker/@ledgerhq/hw-transport/@ledgerhq/errors": ["@ledgerhq/errors@6.33.0", "", {}, "sha512-9iMdjB0Hfxfd8HGMVM0TDPfxBXhJ4MtGKyFlm8aveSHSXjcTjGtCkcnk4OUZ6ZHopBb69BFR+HC2N/ob9xuNgA=="], + "@ledgerhq/hw-app-btc/bs58/base-x": ["base-x@3.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA=="], "@ledgerhq/psbtv2/bitcoinjs-lib/bech32": ["bech32@1.1.4", "", {}, "sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ=="], - "@ledgerhq/psbtv2/bitcoinjs-lib/bip32": ["bip32@2.0.6", "", { "dependencies": { "@types/node": "10.12.18", "bs58check": "^2.1.1", "create-hash": "^1.2.0", "create-hmac": "^1.1.7", "tiny-secp256k1": "^1.1.3", "typeforce": "^1.11.5", "wif": "^2.0.6" } }, "sha512-HpV5OMLLGTjSVblmrtYRfFFKuQB+GArM0+XP8HGWfJ5vxYBqo+DesvJwOdC2WJ3bCkZShGf0QIfoIpeomVzVdA=="], - "@metamask/mobile-wallet-protocol-dapp-client/@metamask/utils/@ethereumjs/tx": ["@ethereumjs/tx@4.2.0", "", { "dependencies": { "@ethereumjs/common": "^3.2.0", "@ethereumjs/rlp": "^4.0.1", "@ethereumjs/util": "^8.1.0", "ethereum-cryptography": "^2.0.0" } }, "sha512-1nc6VO4jtFd172BbSnTnDQVr9IYBFl1y4xPzZdtkrkKIncBCkdbgfdRV+MiTkJYAtTxvV12GRZLqBFT1PNK6Yw=="], "@metamask/mobile-wallet-protocol-dapp-client/@metamask/utils/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], @@ -3307,6 +3292,12 @@ "@mysten/sui/@scure/bip32/@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], + "@near-js/accounts/@near-js/crypto/@noble/curves": ["@noble/curves@1.8.1", "", { "dependencies": { "@noble/hashes": "1.7.1" } }, "sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ=="], + + "@near-js/accounts/@near-js/crypto/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], + + "@near-js/crypto/@near-js/utils/@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="], + "@near-js/crypto/@noble/curves/@noble/hashes": ["@noble/hashes@1.7.1", "", {}, "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ=="], "@near-js/keystores-browser/@near-js/crypto/@near-js/types": ["@near-js/types@0.0.4", "", { "dependencies": { "bn.js": "5.2.1" } }, "sha512-8TTMbLMnmyG06R5YKWuS/qFG1tOA3/9lX4NgBqQPsvaWmDsa+D+QwOkrEHDegped0ZHQwcjAXjKML1S1TyGYKg=="], @@ -3329,6 +3320,22 @@ "@near-js/keystores-node/@near-js/keystores/@near-js/types": ["@near-js/types@0.0.4", "", { "dependencies": { "bn.js": "5.2.1" } }, "sha512-8TTMbLMnmyG06R5YKWuS/qFG1tOA3/9lX4NgBqQPsvaWmDsa+D+QwOkrEHDegped0ZHQwcjAXjKML1S1TyGYKg=="], + "@near-js/keystores/@near-js/crypto/@noble/curves": ["@noble/curves@1.8.1", "", { "dependencies": { "@noble/hashes": "1.7.1" } }, "sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ=="], + + "@near-js/keystores/@near-js/crypto/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], + + "@near-js/providers/@near-js/crypto/@noble/curves": ["@noble/curves@1.8.1", "", { "dependencies": { "@noble/hashes": "1.7.1" } }, "sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ=="], + + "@near-js/providers/@near-js/crypto/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], + + "@near-js/providers/@near-js/transactions/@noble/hashes": ["@noble/hashes@1.7.1", "", {}, "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ=="], + + "@near-js/signers/@near-js/crypto/@noble/curves": ["@noble/curves@1.8.1", "", { "dependencies": { "@noble/hashes": "1.7.1" } }, "sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ=="], + + "@near-js/signers/@near-js/crypto/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], + + "@near-js/transactions/@near-js/utils/@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="], + "@near-js/wallet-account/@near-js/accounts/@near-js/providers": ["@near-js/providers@0.1.1", "", { "dependencies": { "@near-js/transactions": "1.1.2", "@near-js/types": "0.0.4", "@near-js/utils": "0.1.0", "bn.js": "5.2.1", "borsh": "1.0.0", "http-errors": "1.7.2" }, "optionalDependencies": { "node-fetch": "2.6.7" } }, "sha512-0M/Vz2Ac34ShKVoe2ftVJ5Qg4eSbEqNXDbCDOdVj/2qbLWZa7Wpe+me5ei4TMY2ZhGdawhgJUPrYwdJzOCyf8w=="], "@near-js/wallet-account/@near-js/accounts/near-abi": ["near-abi@0.1.1", "", { "dependencies": { "@types/json-schema": "^7.0.11" } }, "sha512-RVDI8O+KVxRpC3KycJ1bpfVj9Zv+xvq9PlW1yIFl46GhrnLw83/72HqHGjGDjQ8DtltkcpSjY9X3YIGZ+1QyzQ=="], @@ -3341,6 +3348,16 @@ "@near-js/wallet-account/@near-js/utils/bs58": ["bs58@4.0.0", "", { "dependencies": { "base-x": "^2.0.1" } }, "sha512-/jcGuUuSebyxwLLfKrbKnCJttxRf9PM51EnHTwmFKBxl4z1SGkoAhrfd6uZKE0dcjQTfm6XzTP8DPr1tzE4KIw=="], + "@near-wallet-selector/core/@near-js/crypto/@noble/curves": ["@noble/curves@1.8.1", "", { "dependencies": { "@noble/hashes": "1.7.1" } }, "sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ=="], + + "@near-wallet-selector/core/@near-js/crypto/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], + + "@near-wallet-selector/core/@near-js/crypto/borsh": ["borsh@1.0.0", "", {}, "sha512-fSVWzzemnyfF89EPwlUNsrS5swF5CrtiN4e+h0/lLf4dz2he4L3ndM20PS9wj7ICSkXJe/TQUHdaPTq15b1mNQ=="], + + "@near-wallet-selector/core/@near-js/transactions/@noble/hashes": ["@noble/hashes@1.7.1", "", {}, "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ=="], + + "@near-wallet-selector/core/@near-js/transactions/borsh": ["borsh@1.0.0", "", {}, "sha512-fSVWzzemnyfF89EPwlUNsrS5swF5CrtiN4e+h0/lLf4dz2he4L3ndM20PS9wj7ICSkXJe/TQUHdaPTq15b1mNQ=="], + "@radixdlt/radix-dapp-toolkit/@noble/curves/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], "@scure/starknet/@noble/curves/@noble/hashes": ["@noble/hashes@1.6.0", "", {}, "sha512-YUULf0Uk4/mAA89w+k3+yUYh6NrEvxZa5T6SY3wlMvE2chHkxFUUIDI8/XW1QSC357iA5pSnqt7XEhvFOqmDyQ=="], @@ -3369,8 +3386,6 @@ "@solana/web3.js/bs58/base-x": ["base-x@3.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA=="], - "@stacks/common/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], - "@starknet-io/get-starknet-wallet-standard/ox/@adraffy/ens-normalize": ["@adraffy/ens-normalize@1.11.1", "", {}, "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ=="], "@starknet-io/get-starknet-wallet-standard/ox/@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], @@ -3385,10 +3400,12 @@ "@stellar/stellar-base/@noble/curves/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], - "@stellar/stellar-sdk/axios/proxy-from-env": ["proxy-from-env@2.1.0", "", {}, "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA=="], - "@swapkit/core/@stricahq/typhonjs/@stricahq/cbors": ["@stricahq/cbors@1.0.2", "", { "dependencies": { "bignumber.js": "^9.0.2", "buffer": "^6.0.3" } }, "sha512-6ePsEiq7EGHA5IiPn9poA7sF5iXPqt30kKw3pjR/BhP7S+XHZNu/OPumESWnVl4AM+IEYC2x9eL+4qRPsTPVww=="], + "@swapkit/plugins/@near-js/transactions/@near-js/crypto": ["@near-js/crypto@2.5.1", "", { "dependencies": { "@near-js/types": "2.5.1", "@near-js/utils": "2.5.1", "@noble/curves": "1.8.1", "@noble/hashes": "^1.7.1", "borsh": "1.0.0", "secp256k1": "5.0.1" } }, "sha512-Kb+bbnUrfvuzzed9hpLRpcIlCMOaQlw/7BxlZPCq8DggVaK1m0nKR1DHQs2cV0Q0WSKSk2UrFJho1dxeKL5alg=="], + + "@swapkit/plugins/@near-js/transactions/@noble/hashes": ["@noble/hashes@1.7.1", "", {}, "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ=="], + "@swapkit/toolboxes/@cosmjs/crypto/@cosmjs/encoding": ["@cosmjs/encoding@0.39.0", "", { "dependencies": { "@scure/base": "^2.0.0", "base64-js": "^1.3.0", "readonly-date-esm": "^2.0.0" } }, "sha512-+poEaeM8YjGNVtrHLQNWqkhEeDxapjrdpnPCT+JCRh8YNbeHEdftzZLCH5VCBSOtvo7PF0gK1B7sbQbBl6q5pQ=="], "@swapkit/toolboxes/@cosmjs/crypto/@cosmjs/math": ["@cosmjs/math@0.39.0", "", {}, "sha512-FSLy/oDF+BtOP/J60RsjL5W4MCKiCfBjSoeV5xj6qg2g8N884Ue853iuWanjqGkQJwkXCp+JbeK8Mv9j6AaYHw=="], @@ -3397,6 +3414,12 @@ "@swapkit/toolboxes/@cosmjs/crypto/@noble/ciphers": ["@noble/ciphers@2.2.0", "", {}, "sha512-Z6pjIZ/8IJcCGzb2S/0Px5J81yij85xASuk1teLNeg75bfT07MV3a/O2Mtn1I2se43k3lkVEcFaR10N4cgQcZA=="], + "@swapkit/toolboxes/@near-js/crypto/@noble/curves": ["@noble/curves@1.8.1", "", { "dependencies": { "@noble/hashes": "1.7.1" } }, "sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ=="], + + "@swapkit/toolboxes/@near-js/crypto/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], + + "@swapkit/toolboxes/@near-js/transactions/@noble/hashes": ["@noble/hashes@1.7.1", "", {}, "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ=="], + "@swapkit/toolboxes/@stricahq/typhonjs/@stricahq/cbors": ["@stricahq/cbors@1.0.2", "", { "dependencies": { "bignumber.js": "^9.0.2", "buffer": "^6.0.3" } }, "sha512-6ePsEiq7EGHA5IiPn9poA7sF5iXPqt30kKw3pjR/BhP7S+XHZNu/OPumESWnVl4AM+IEYC2x9eL+4qRPsTPVww=="], "@swapkit/ui/@cosmjs/amino/@cosmjs/encoding": ["@cosmjs/encoding@0.39.0", "", { "dependencies": { "@scure/base": "^2.0.0", "base64-js": "^1.3.0", "readonly-date-esm": "^2.0.0" } }, "sha512-+poEaeM8YjGNVtrHLQNWqkhEeDxapjrdpnPCT+JCRh8YNbeHEdftzZLCH5VCBSOtvo7PF0gK1B7sbQbBl6q5pQ=="], @@ -3429,6 +3452,12 @@ "@swapkit/ui/@cosmjs/stargate/@cosmjs/utils": ["@cosmjs/utils@0.39.0", "", {}, "sha512-h7fy7Tbcl9v8ABntp8+kqw2VmUus2HbnRJFyzTkM7byRktLtECHYNMsztwyl1rdHkzc8Nc2xs6K/56d7a+75aw=="], + "@swapkit/ui/@near-js/crypto/@noble/curves": ["@noble/curves@1.8.1", "", { "dependencies": { "@noble/hashes": "1.7.1" } }, "sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ=="], + + "@swapkit/ui/@near-js/crypto/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], + + "@swapkit/ui/@near-js/transactions/@noble/hashes": ["@noble/hashes@1.7.1", "", {}, "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ=="], + "@swapkit/ui/@polkadot/keyring/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "@swapkit/ui/@polkadot/util/@polkadot/x-bigint": ["@polkadot/x-bigint@13.5.7", "", { "dependencies": { "@polkadot/x-global": "13.5.7", "tslib": "^2.8.0" } }, "sha512-NbN4EPbMBhjOXoWj0BVcT49/obzusFWPKbSyBxbZi8ITBaIIgpncgcCfXY4rII6Fqh74khx9jdevWge/6ycepQ=="], @@ -3473,17 +3502,21 @@ "@swapkit/wallet-hardware/@cosmjs/amino/@cosmjs/crypto": ["@cosmjs/crypto@0.37.1", "", { "dependencies": { "@cosmjs/encoding": "^0.37.1", "@cosmjs/math": "^0.37.1", "@cosmjs/utils": "^0.37.1", "@noble/ciphers": "^1.3.0", "@noble/curves": "^1.9.2", "@noble/hashes": "^1.8.0", "@scure/bip39": "^1.6.0", "hash-wasm": "^4.12.0" } }, "sha512-CdLOKQVJM422UegKpi4T/xkdNwvLZwJwSC7NyYi7tAyty0NzP67u+vNqar7Ok22ECkKLj6N9uRcIl4g62oZThA=="], - "@swapkit/wallet-hardware/@cosmjs/proto-signing/@cosmjs/crypto": ["@cosmjs/crypto@0.37.1", "", { "dependencies": { "@cosmjs/encoding": "^0.37.1", "@cosmjs/math": "^0.37.1", "@cosmjs/utils": "^0.37.1", "@noble/ciphers": "^1.3.0", "@noble/curves": "^1.9.2", "@noble/hashes": "^1.8.0", "@scure/bip39": "^1.6.0", "hash-wasm": "^4.12.0" } }, "sha512-CdLOKQVJM422UegKpi4T/xkdNwvLZwJwSC7NyYi7tAyty0NzP67u+vNqar7Ok22ECkKLj6N9uRcIl4g62oZThA=="], + "@swapkit/wallet-hardware/@near-js/crypto/@noble/curves": ["@noble/curves@1.8.1", "", { "dependencies": { "@noble/hashes": "1.7.1" } }, "sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ=="], + + "@swapkit/wallet-hardware/@near-js/crypto/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], + + "@swapkit/wallet-hardware/@near-js/transactions/@noble/hashes": ["@noble/hashes@1.7.1", "", {}, "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ=="], "@swapkit/wallets/@cosmjs/amino/@cosmjs/crypto": ["@cosmjs/crypto@0.37.1", "", { "dependencies": { "@cosmjs/encoding": "^0.37.1", "@cosmjs/math": "^0.37.1", "@cosmjs/utils": "^0.37.1", "@noble/ciphers": "^1.3.0", "@noble/curves": "^1.9.2", "@noble/hashes": "^1.8.0", "@scure/bip39": "^1.6.0", "hash-wasm": "^4.12.0" } }, "sha512-CdLOKQVJM422UegKpi4T/xkdNwvLZwJwSC7NyYi7tAyty0NzP67u+vNqar7Ok22ECkKLj6N9uRcIl4g62oZThA=="], "@swapkit/wallets/@cosmjs/proto-signing/@cosmjs/crypto": ["@cosmjs/crypto@0.37.1", "", { "dependencies": { "@cosmjs/encoding": "^0.37.1", "@cosmjs/math": "^0.37.1", "@cosmjs/utils": "^0.37.1", "@noble/ciphers": "^1.3.0", "@noble/curves": "^1.9.2", "@noble/hashes": "^1.8.0", "@scure/bip39": "^1.6.0", "hash-wasm": "^4.12.0" } }, "sha512-CdLOKQVJM422UegKpi4T/xkdNwvLZwJwSC7NyYi7tAyty0NzP67u+vNqar7Ok22ECkKLj6N9uRcIl4g62oZThA=="], - "@ton/ton/axios/proxy-from-env": ["proxy-from-env@2.1.0", "", {}, "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA=="], + "@swapkit/wallets/@near-js/transactions/@near-js/crypto": ["@near-js/crypto@2.5.1", "", { "dependencies": { "@near-js/types": "2.5.1", "@near-js/utils": "2.5.1", "@noble/curves": "1.8.1", "@noble/hashes": "^1.7.1", "borsh": "1.0.0", "secp256k1": "5.0.1" } }, "sha512-Kb+bbnUrfvuzzed9hpLRpcIlCMOaQlw/7BxlZPCq8DggVaK1m0nKR1DHQs2cV0Q0WSKSk2UrFJho1dxeKL5alg=="], - "@trezor/blockchain-link-utils/@stellar/stellar-sdk/@stellar/stellar-base": ["@stellar/stellar-base@14.1.0", "", { "dependencies": { "@noble/curves": "^1.9.6", "@stellar/js-xdr": "^3.1.2", "base32.js": "^0.1.0", "bignumber.js": "^9.3.1", "buffer": "^6.0.3", "sha.js": "^2.4.12" } }, "sha512-A8kFli6QGy22SRF45IjgPAJfUNGjnI+R7g4DF5NZYVsD1kGf7B4ITyc4OPclLV9tqNI4/lXxafGEw0JEUbHixw=="], + "@swapkit/wallets/@near-js/transactions/@noble/hashes": ["@noble/hashes@1.7.1", "", {}, "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ=="], - "@trezor/blockchain-link-utils/@stellar/stellar-sdk/axios": ["axios@1.15.0", "", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^2.1.0" } }, "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q=="], + "@trezor/blockchain-link-utils/@stellar/stellar-sdk/@stellar/stellar-base": ["@stellar/stellar-base@14.1.0", "", { "dependencies": { "@noble/curves": "^1.9.6", "@stellar/js-xdr": "^3.1.2", "base32.js": "^0.1.0", "bignumber.js": "^9.3.1", "buffer": "^6.0.3", "sha.js": "^2.4.12" } }, "sha512-A8kFli6QGy22SRF45IjgPAJfUNGjnI+R7g4DF5NZYVsD1kGf7B4ITyc4OPclLV9tqNI4/lXxafGEw0JEUbHixw=="], "@trezor/blockchain-link-utils/@trezor/protobuf/long": ["long@5.2.5", "", {}, "sha512-e0r9YBBgNCq1D1o5Dp8FMH0N5hsFtXDBiVa0qoJPHpakvZkmDKPRoGffZJII/XsHvj9An9blm+cRJ01yQqU+Dw=="], @@ -3497,8 +3530,6 @@ "@trezor/blockchain-link/@stellar/stellar-sdk/@stellar/stellar-base": ["@stellar/stellar-base@14.1.0", "", { "dependencies": { "@noble/curves": "^1.9.6", "@stellar/js-xdr": "^3.1.2", "base32.js": "^0.1.0", "bignumber.js": "^9.3.1", "buffer": "^6.0.3", "sha.js": "^2.4.12" } }, "sha512-A8kFli6QGy22SRF45IjgPAJfUNGjnI+R7g4DF5NZYVsD1kGf7B4ITyc4OPclLV9tqNI4/lXxafGEw0JEUbHixw=="], - "@trezor/blockchain-link/@stellar/stellar-sdk/axios": ["axios@1.15.0", "", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^2.1.0" } }, "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q=="], - "@trezor/blockchain-link/xrpl/@scure/bip32": ["@scure/bip32@1.7.0", "", { "dependencies": { "@noble/curves": "~1.9.0", "@noble/hashes": "~1.8.0", "@scure/base": "~1.2.5" } }, "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw=="], "@trezor/blockchain-link/xrpl/@scure/bip39": ["@scure/bip39@1.6.0", "", { "dependencies": { "@noble/hashes": "~1.8.0", "@scure/base": "~1.2.5" } }, "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A=="], @@ -3637,13 +3668,29 @@ "xrpl/@scure/bip39/@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="], - "@cosmjs/tendermint-rpc/@cosmjs/crypto/@scure/bip39/@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="], + "@cosmjs/proto-signing/@cosmjs/amino/@cosmjs/crypto/@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], + + "@cosmjs/proto-signing/@cosmjs/amino/@cosmjs/crypto/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], + + "@cosmjs/proto-signing/@cosmjs/amino/@cosmjs/crypto/@scure/bip39": ["@scure/bip39@1.6.0", "", { "dependencies": { "@noble/hashes": "~1.8.0", "@scure/base": "~1.2.5" } }, "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A=="], + + "@cosmjs/stargate/@cosmjs/amino/@cosmjs/crypto/@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], + + "@cosmjs/stargate/@cosmjs/amino/@cosmjs/crypto/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], + + "@cosmjs/stargate/@cosmjs/amino/@cosmjs/crypto/@scure/bip39": ["@scure/bip39@1.6.0", "", { "dependencies": { "@noble/hashes": "~1.8.0", "@scure/base": "~1.2.5" } }, "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A=="], + + "@cosmjs/stargate/@cosmjs/proto-signing/@cosmjs/crypto/@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], + + "@cosmjs/stargate/@cosmjs/proto-signing/@cosmjs/crypto/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], - "@keplr-wallet/types/starknet/@noble/curves/@noble/hashes": ["@noble/hashes@1.6.0", "", {}, "sha512-YUULf0Uk4/mAA89w+k3+yUYh6NrEvxZa5T6SY3wlMvE2chHkxFUUIDI8/XW1QSC357iA5pSnqt7XEhvFOqmDyQ=="], + "@cosmjs/stargate/@cosmjs/proto-signing/@cosmjs/crypto/@scure/bip39": ["@scure/bip39@1.6.0", "", { "dependencies": { "@noble/hashes": "~1.8.0", "@scure/base": "~1.2.5" } }, "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A=="], - "@ledgerhq/hw-app-btc/bitcoinjs-lib/bip32/@types/node": ["@types/node@10.12.18", "", {}, "sha512-fh+pAqt4xRzPfqA6eh3Z2y6fyZavRIumvjhaCL753+TVkGKGhpPeyrJG2JftD0T9q4GF00KjefsQ+PQNDdWQaQ=="], + "@cosmjs/tendermint-rpc/@cosmjs/crypto/@scure/bip39/@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="], + + "@ledgerhq/context-module/ethers/@types/node/undici-types": ["undici-types@6.19.8", "", {}, "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="], - "@ledgerhq/psbtv2/bitcoinjs-lib/bip32/@types/node": ["@types/node@10.12.18", "", {}, "sha512-fh+pAqt4xRzPfqA6eh3Z2y6fyZavRIumvjhaCL753+TVkGKGhpPeyrJG2JftD0T9q4GF00KjefsQ+PQNDdWQaQ=="], + "@ledgerhq/device-signer-kit-ethereum/ethers/@types/node/undici-types": ["undici-types@6.19.8", "", {}, "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="], "@metamask/mobile-wallet-protocol-dapp-client/@metamask/utils/@ethereumjs/tx/@ethereumjs/common": ["@ethereumjs/common@3.2.0", "", { "dependencies": { "@ethereumjs/util": "^8.1.0", "crc-32": "^1.2.0" } }, "sha512-pksvzI0VyLgmuEF2FA/JR/4/y6hcPq8OUail3/AvycBaW1d5VSauOZzqGvJ3RTmR4MU35lWE8KseKOsEhrFRBA=="], @@ -3651,6 +3698,8 @@ "@metamask/mobile-wallet-protocol-dapp-client/@metamask/utils/@ethereumjs/tx/@ethereumjs/util": ["@ethereumjs/util@8.1.0", "", { "dependencies": { "@ethereumjs/rlp": "^4.0.1", "ethereum-cryptography": "^2.0.0", "micro-ftch": "^0.3.1" } }, "sha512-zQ0IqbdX8FZ9aw11vP+dZkKDkS+kgIvQPHnSAXzP9pLu+Rfu3D3XEeLbicvoXJTYnhZiPmsZUxgdzXwNKxRPbA=="], + "@near-js/accounts/@near-js/crypto/@noble/curves/@noble/hashes": ["@noble/hashes@1.7.1", "", {}, "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ=="], + "@near-js/keystores-browser/@near-js/crypto/@near-js/utils/bs58": ["bs58@4.0.0", "", { "dependencies": { "base-x": "^2.0.1" } }, "sha512-/jcGuUuSebyxwLLfKrbKnCJttxRf9PM51EnHTwmFKBxl4z1SGkoAhrfd6uZKE0dcjQTfm6XzTP8DPr1tzE4KIw=="], "@near-js/keystores-browser/@near-js/crypto/@noble/curves/@noble/hashes": ["@noble/hashes@1.3.2", "", {}, "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ=="], @@ -3663,12 +3712,20 @@ "@near-js/keystores-node/@near-js/keystores/@near-js/types/bn.js": ["bn.js@5.2.1", "", {}, "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ=="], + "@near-js/keystores/@near-js/crypto/@noble/curves/@noble/hashes": ["@noble/hashes@1.7.1", "", {}, "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ=="], + + "@near-js/providers/@near-js/crypto/@noble/curves/@noble/hashes": ["@noble/hashes@1.7.1", "", {}, "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ=="], + + "@near-js/signers/@near-js/crypto/@noble/curves/@noble/hashes": ["@noble/hashes@1.7.1", "", {}, "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ=="], + "@near-js/wallet-account/@near-js/accounts/@near-js/providers/node-fetch": ["node-fetch@2.6.7", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ=="], "@near-js/wallet-account/@near-js/crypto/@noble/curves/@noble/hashes": ["@noble/hashes@1.3.2", "", {}, "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ=="], "@near-js/wallet-account/@near-js/utils/bs58/base-x": ["base-x@2.0.6", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-UAmjxz9KbK+YIi66xej+pZVo/vxUOh49ubEvZW5egCbxhur05pBb+hwuireQwKO4nDpsNm64/jEei17LEpsr5g=="], + "@near-wallet-selector/core/@near-js/crypto/@noble/curves/@noble/hashes": ["@noble/hashes@1.7.1", "", {}, "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ=="], + "@solana/codecs/@solana/codecs-core/@solana/errors/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], "@solana/codecs/@solana/codecs-numbers/@solana/errors/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], @@ -3677,10 +3734,18 @@ "@starknet-io/get-starknet-wallet-standard/ox/@scure/bip39/@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="], + "@swapkit/plugins/@near-js/transactions/@near-js/crypto/@noble/curves": ["@noble/curves@1.8.1", "", { "dependencies": { "@noble/hashes": "1.7.1" } }, "sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ=="], + + "@swapkit/plugins/@near-js/transactions/@near-js/crypto/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], + + "@swapkit/toolboxes/@near-js/crypto/@noble/curves/@noble/hashes": ["@noble/hashes@1.7.1", "", {}, "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ=="], + "@swapkit/ui/@cosmjs/stargate/@cosmjs/tendermint-rpc/@cosmjs/json-rpc": ["@cosmjs/json-rpc@0.39.0", "", { "dependencies": { "@cosmjs/stream": "^0.39.0", "xstream": "^11.14.0" } }, "sha512-slyo76IYkTuSxrzxvF1s1ScFPIGnW7PbNSx8lr++NzdyPluqe053+cSqsxLUioXb5qI9Nxny2fWxOK8isSn+cg=="], "@swapkit/ui/@cosmjs/stargate/@cosmjs/tendermint-rpc/@cosmjs/socket": ["@cosmjs/socket@0.39.0", "", { "dependencies": { "@cosmjs/stream": "^0.39.0", "isomorphic-ws": "^4.0.1", "ws": "^7", "xstream": "^11.14.0" } }, "sha512-mvA+/ycMn7dnjjdooSPq516poFpSARRXhFhKjCXxGUmBZA+74vLDLbtD6VGzHd08A2JSZBSiAhVOZpjYqgV78A=="], + "@swapkit/ui/@near-js/crypto/@noble/curves/@noble/hashes": ["@noble/hashes@1.7.1", "", {}, "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ=="], + "@swapkit/ui/@polkadot/util-crypto/@polkadot/x-bigint/@polkadot/x-global": ["@polkadot/x-global@13.5.7", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-TkBxLfeKtj0laCzXp2lvRhwSIeXSxIu7LAWpfAUW4SYNFQvtgIS0x0Bq70CUW3lcy0wqTrSG2cqzfnbomB0Djw=="], "@swapkit/ui/@polkadot/util-crypto/@polkadot/x-randomvalues/@polkadot/x-global": ["@polkadot/x-global@13.5.7", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-TkBxLfeKtj0laCzXp2lvRhwSIeXSxIu7LAWpfAUW4SYNFQvtgIS0x0Bq70CUW3lcy0wqTrSG2cqzfnbomB0Djw=="], @@ -3703,11 +3768,7 @@ "@swapkit/wallet-hardware/@cosmjs/amino/@cosmjs/crypto/@scure/bip39": ["@scure/bip39@1.6.0", "", { "dependencies": { "@noble/hashes": "~1.8.0", "@scure/base": "~1.2.5" } }, "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A=="], - "@swapkit/wallet-hardware/@cosmjs/proto-signing/@cosmjs/crypto/@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], - - "@swapkit/wallet-hardware/@cosmjs/proto-signing/@cosmjs/crypto/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], - - "@swapkit/wallet-hardware/@cosmjs/proto-signing/@cosmjs/crypto/@scure/bip39": ["@scure/bip39@1.6.0", "", { "dependencies": { "@noble/hashes": "~1.8.0", "@scure/base": "~1.2.5" } }, "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A=="], + "@swapkit/wallet-hardware/@near-js/crypto/@noble/curves/@noble/hashes": ["@noble/hashes@1.7.1", "", {}, "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ=="], "@swapkit/wallets/@cosmjs/amino/@cosmjs/crypto/@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], @@ -3721,12 +3782,14 @@ "@swapkit/wallets/@cosmjs/proto-signing/@cosmjs/crypto/@scure/bip39": ["@scure/bip39@1.6.0", "", { "dependencies": { "@noble/hashes": "~1.8.0", "@scure/base": "~1.2.5" } }, "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A=="], + "@swapkit/wallets/@near-js/transactions/@near-js/crypto/@noble/curves": ["@noble/curves@1.8.1", "", { "dependencies": { "@noble/hashes": "1.7.1" } }, "sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ=="], + + "@swapkit/wallets/@near-js/transactions/@near-js/crypto/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], + "@trezor/blockchain-link-utils/@stellar/stellar-sdk/@stellar/stellar-base/@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], "@trezor/blockchain-link-utils/@stellar/stellar-sdk/@stellar/stellar-base/@stellar/js-xdr": ["@stellar/js-xdr@3.1.2", "", {}, "sha512-VVolPL5goVEIsvuGqDc5uiKxV03lzfWdvYg1KikvwheDmTBO68CKDji3bAZ/kppZrx5iTA8z3Ld5yuytcvhvOQ=="], - "@trezor/blockchain-link-utils/@stellar/stellar-sdk/axios/proxy-from-env": ["proxy-from-env@2.1.0", "", {}, "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA=="], - "@trezor/blockchain-link-utils/@trezor/protobuf/protobufjs/@types/node": ["@types/node@24.10.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A=="], "@trezor/blockchain-link-utils/@trezor/protobuf/protobufjs/long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], @@ -3745,8 +3808,6 @@ "@trezor/blockchain-link/@stellar/stellar-sdk/@stellar/stellar-base/@stellar/js-xdr": ["@stellar/js-xdr@3.1.2", "", {}, "sha512-VVolPL5goVEIsvuGqDc5uiKxV03lzfWdvYg1KikvwheDmTBO68CKDji3bAZ/kppZrx5iTA8z3Ld5yuytcvhvOQ=="], - "@trezor/blockchain-link/@stellar/stellar-sdk/axios/proxy-from-env": ["proxy-from-env@2.1.0", "", {}, "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA=="], - "@trezor/blockchain-link/xrpl/@scure/bip32/@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], "@trezor/blockchain-link/xrpl/@scure/bip32/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], @@ -3779,10 +3840,20 @@ "rpc-websockets/@types/ws/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + "@cosmjs/proto-signing/@cosmjs/amino/@cosmjs/crypto/@scure/bip39/@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="], + + "@cosmjs/stargate/@cosmjs/amino/@cosmjs/crypto/@scure/bip39/@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="], + + "@cosmjs/stargate/@cosmjs/proto-signing/@cosmjs/crypto/@scure/bip39/@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="], + "@near-js/keystores-browser/@near-js/crypto/@near-js/utils/bs58/base-x": ["base-x@2.0.6", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-UAmjxz9KbK+YIi66xej+pZVo/vxUOh49ubEvZW5egCbxhur05pBb+hwuireQwKO4nDpsNm64/jEei17LEpsr5g=="], "@near-js/keystores-node/@near-js/crypto/@near-js/utils/bs58/base-x": ["base-x@2.0.6", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-UAmjxz9KbK+YIi66xej+pZVo/vxUOh49ubEvZW5egCbxhur05pBb+hwuireQwKO4nDpsNm64/jEei17LEpsr5g=="], + "@swapkit/plugins/@near-js/transactions/@near-js/crypto/@noble/curves/@noble/hashes": ["@noble/hashes@1.7.1", "", {}, "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ=="], + + "@swapkit/ui/@cosmjs/stargate/@cosmjs/tendermint-rpc/@cosmjs/socket/isomorphic-ws": ["isomorphic-ws@4.0.1", "", { "peerDependencies": { "ws": "*" } }, "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w=="], + "@swapkit/ui/@cosmjs/stargate/@cosmjs/tendermint-rpc/@cosmjs/socket/ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="], "@swapkit/wallet-extensions/@cosmjs/amino/@cosmjs/crypto/@scure/bip39/@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="], @@ -3791,12 +3862,12 @@ "@swapkit/wallet-hardware/@cosmjs/amino/@cosmjs/crypto/@scure/bip39/@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="], - "@swapkit/wallet-hardware/@cosmjs/proto-signing/@cosmjs/crypto/@scure/bip39/@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="], - "@swapkit/wallets/@cosmjs/amino/@cosmjs/crypto/@scure/bip39/@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="], "@swapkit/wallets/@cosmjs/proto-signing/@cosmjs/crypto/@scure/bip39/@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="], + "@swapkit/wallets/@near-js/transactions/@near-js/crypto/@noble/curves/@noble/hashes": ["@noble/hashes@1.7.1", "", {}, "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ=="], + "@trezor/blockchain-link-utils/@stellar/stellar-sdk/@stellar/stellar-base/@noble/curves/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], "@trezor/blockchain-link-utils/@trezor/protobuf/protobufjs/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], diff --git a/package.json b/package.json index 2430b16..8b97c93 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,6 @@ "@swapkit/wallet-keystore": "^4.5.2", "@types/bun": "1.3.13", "@types/node": "25.6.0", - "ledger-bitcoin": "0.3.0", "typescript": "6.0.3", "zod": "4.4.3" }, diff --git a/packages/wallet-extensions/test/evm-extensions.test.ts b/packages/wallet-extensions/test/evm-extensions.test.ts index 0ddb81f..b1d82ec 100644 --- a/packages/wallet-extensions/test/evm-extensions.test.ts +++ b/packages/wallet-extensions/test/evm-extensions.test.ts @@ -1,5 +1,6 @@ import { beforeEach, describe, expect, mock, test } from "bun:test"; import { Chain, WalletOption } from "@swapkit/helpers"; +import * as ethersModule from "ethers"; let balanceAddressCalls: string[] = []; @@ -56,7 +57,7 @@ class MockBrowserProvider { } } -mock.module("ethers", () => ({ BrowserProvider: MockBrowserProvider })); +mock.module("ethers", () => ({ ...ethersModule, BrowserProvider: MockBrowserProvider })); mock.module("@swapkit/toolboxes/evm", () => ({ getEvmToolboxAsync: async () => ({ diff --git a/packages/wallet-hardware/build.ts b/packages/wallet-hardware/build.ts index 392a109..a8d78fb 100644 --- a/packages/wallet-hardware/build.ts +++ b/packages/wallet-hardware/build.ts @@ -1,3 +1,13 @@ import { buildPackage } from "../../tools/builder"; -void buildPackage(); +const nodeIncompatibleLedgerPackages = [ + "@ledgerhq/context-module", + "@ledgerhq/device-management-kit", + "@ledgerhq/device-signer-kit-bitcoin", + "@ledgerhq/device-signer-kit-cosmos", + "@ledgerhq/device-signer-kit-ethereum", + "@ledgerhq/device-signer-kit-zcash", + "@ledgerhq/device-transport-kit-web-hid", +]; + +void buildPackage({ bundlePackages: nodeIncompatibleLedgerPackages }); diff --git a/packages/wallet-hardware/package.json b/packages/wallet-hardware/package.json index 8f7a03b..ac9058f 100644 --- a/packages/wallet-hardware/package.json +++ b/packages/wallet-hardware/package.json @@ -3,21 +3,21 @@ "dependencies": { "@cosmjs/amino": "~0.37.0", "@cosmjs/crypto": "0.37.0", - "@cosmjs/proto-signing": "~0.37.0", "@keepkey/keepkey-sdk": "~0.2.62", - "@ledgerhq/devices": "~8.10.0", - "@ledgerhq/errors": "~6.29.0", + "@ledgerhq/context-module": "~2.4.0", + "@ledgerhq/device-management-kit": "~1.8.0", + "@ledgerhq/device-signer-kit-bitcoin": "~1.3.3", + "@ledgerhq/device-signer-kit-cosmos": "~1.0.1", + "@ledgerhq/device-signer-kit-ethereum": "~1.16.0", + "@ledgerhq/device-signer-kit-zcash": "~0.7.0", + "@ledgerhq/device-transport-kit-web-hid": "~1.2.4", "@ledgerhq/hw-app-btc": "~10.17.0", "@ledgerhq/hw-app-cosmos": "~6.33.0", - "@ledgerhq/hw-app-eth": "~7.4.2", "@ledgerhq/hw-app-near": "~6.32.0", "@ledgerhq/hw-app-sui": "~1.5.0", "@ledgerhq/hw-app-trx": "~6.32.0", "@ledgerhq/hw-app-xrp": "~6.33.0", "@ledgerhq/hw-transport": "~6.32.0", - "@ledgerhq/hw-transport-webhid": "~6.31.0", - "@ledgerhq/hw-transport-webusb": "~6.30.0", - "@ledgerhq/wallet-api-client": "~1.13.1", "@near-js/crypto": "~2.5.0", "@near-js/transactions": "~2.5.0", "@scure/base": "2.2.0", @@ -27,10 +27,10 @@ "@swapkit/utxo-signer": "^2.3.0", "@swapkit/wallet-core": "^4.3.19", "@trezor/connect-web": "~9.7.3", - "cosmjs-types": "~0.10.1", + "bitcoinjs-lib": "~6.1.7", "ethers": "^6.14.0", - "ledger-bitcoin": "~0.3.0", "ripple-binary-codec": "2.7.0", + "rxjs": "~7.8.2", "ts-pattern": "^5.9.0", "xrpl": "~4.6.0" }, @@ -38,30 +38,30 @@ "devDependencies": { "@cosmjs/amino": "0.37.0", "@cosmjs/crypto": "0.37.0", - "@cosmjs/proto-signing": "0.37.0", "@keepkey/keepkey-sdk": "0.2.62", - "@ledgerhq/devices": "8.10.0", - "@ledgerhq/errors": "6.29.0", + "@ledgerhq/context-module": "2.4.0", + "@ledgerhq/device-management-kit": "1.8.0", + "@ledgerhq/device-signer-kit-bitcoin": "1.3.3", + "@ledgerhq/device-signer-kit-cosmos": "1.0.1", + "@ledgerhq/device-signer-kit-ethereum": "1.16.0", + "@ledgerhq/device-signer-kit-zcash": "0.7.0", + "@ledgerhq/device-transport-kit-web-hid": "1.2.4", "@ledgerhq/hw-app-btc": "10.17.0", "@ledgerhq/hw-app-cosmos": "6.33.0", - "@ledgerhq/hw-app-eth": "7.4.2", "@ledgerhq/hw-app-near": "6.32.0", "@ledgerhq/hw-app-sui": "1.5.0", "@ledgerhq/hw-app-trx": "6.32.0", "@ledgerhq/hw-app-xrp": "6.33.0", "@ledgerhq/hw-transport": "6.32.0", - "@ledgerhq/hw-transport-webhid": "6.31.0", - "@ledgerhq/hw-transport-webusb": "6.30.0", - "@ledgerhq/wallet-api-client": "1.13.1", "@near-js/crypto": "2.5.0", "@near-js/transactions": "2.5.0", "@scure/base": "2.2.0", "@scure/bip32": "2.2.0", "@trezor/connect-web": "9.7.3", - "cosmjs-types": "0.10.1", + "bitcoinjs-lib": "6.1.7", "ethers": "6.16.0", - "ledger-bitcoin": "0.3.0", "ripple-binary-codec": "2.7.0", + "rxjs": "7.8.2", "ts-pattern": "^5.9.0", "xrpl": "4.6.0" }, diff --git a/packages/wallet-hardware/src/ledger/clients/bitcoin.ts b/packages/wallet-hardware/src/ledger/clients/bitcoin.ts new file mode 100644 index 0000000..4f1d041 --- /dev/null +++ b/packages/wallet-hardware/src/ledger/clients/bitcoin.ts @@ -0,0 +1,413 @@ +import type { SignerBtc } from "@ledgerhq/device-signer-kit-bitcoin"; +import type Transport from "@ledgerhq/hw-transport"; +import { hex } from "@scure/base"; +import { HDKey } from "@scure/bip32"; +import { + Chain, + type DerivationPathArray, + derivationPathToString, + NetworkDerivationPath, + SwapKitError, +} from "@swapkit/helpers"; +import type { UTXOType } from "@swapkit/toolboxes/utxo"; +import type { Transaction } from "@swapkit/utxo-signer"; +import { match, P } from "ts-pattern"; + +import type { LedgerDMKSession } from "../helpers/dmk"; +import { getLedgerDMKSession } from "../helpers/dmk"; +import { executeLedgerDeviceAction, type LedgerDeviceActionStateHandler } from "../helpers/executeDeviceAction"; +import { BitcoinLedger as LegacyBitcoinLedger } from "./utxo"; +import { extractInputsFromPsbt, signLegacyPsbtTransaction } from "./utxo-legacy-adapter"; + +interface BitcoinLedgerParams { + derivationPath?: DerivationPathArray | string; + dmkSession?: LedgerDMKSession; + onDeviceActionState?: LedgerDeviceActionStateHandler; + transport?: Transport; +} + +interface GetExtendedPublicKeyParams { + checkOnDevice?: boolean; + path?: string; +} + +interface SignBitcoinTransactionParams { + inputUtxos?: UTXOType[]; + tx: Transaction; +} + +interface SignBitcoinTransactionWithMultiplePathsParams extends SignBitcoinTransactionParams { + derivationPaths: string[]; +} + +interface ParsedBitcoinPath { + accountPath: string; + addressIndex: number; + change: number; + fullPath: string; + purpose: 44 | 49 | 84 | 86; +} + +function normalizePath(path: DerivationPathArray | string) { + return (typeof path === "string" ? path : derivationPathToString(path)).replace(/^m\//, "").replace(/^\/+/, ""); +} + +function invalidPath({ path, reason }: { path: string; reason: string }) { + return new SwapKitError("wallet_ledger_invalid_params", { path, reason }); +} + +function parseBitcoinPath(path: DerivationPathArray | string): ParsedBitcoinPath { + const normalized = normalizePath(path); + const match = /^(44|49|84|86)'\/0'\/(\d+)'(?:\/(0|1)\/(\d+))?$/.exec(normalized); + + if (!match) { + throw invalidPath({ + path: normalized, + reason: "Expected a Bitcoin path with purpose 44, 49, 84, or 86 and coin type 0", + }); + } + + const purpose = Number(match[1]) as ParsedBitcoinPath["purpose"]; + const accountPath = `${purpose}'/0'/${match[2]}'`; + const change = Number(match[3] ?? 0); + const addressIndex = Number(match[4] ?? 0); + + return { accountPath, addressIndex, change, fullPath: `${accountPath}/${change}/${addressIndex}`, purpose }; +} + +function pathToNumberArray(path: string) { + return path.split("/").map((segment) => { + const hardened = segment.endsWith("'"); + const value = Number.parseInt(hardened ? segment.slice(0, -1) : segment, 10); + return hardened ? (value | 0x80000000) >>> 0 : value; + }); +} + +function fingerprintToNumber(fingerprint: Uint8Array) { + if (fingerprint.length !== 4) { + throw new SwapKitError("wallet_ledger_invalid_response", { + reason: `Expected a 4-byte master fingerprint, received ${fingerprint.length}`, + }); + } + + return new DataView(fingerprint.buffer, fingerprint.byteOffset, fingerprint.byteLength).getUint32(0, false); +} + +function validateInputCount({ count, tx }: { count: number; tx: Transaction }) { + if (count !== tx.inputsLength) { + throw new SwapKitError("wallet_ledger_invalid_params", { + reason: `Input metadata count (${count}) must match transaction inputs count (${tx.inputsLength})`, + }); + } +} + +function validateAccountPaths({ + accountPath, + derivationPaths, + tx, +}: { + accountPath: string; + derivationPaths: string[]; + tx: Transaction; +}) { + validateInputCount({ count: derivationPaths.length, tx }); + const parsedPaths = derivationPaths.map(parseBitcoinPath); + const mismatchedPath = parsedPaths.find((path) => path.accountPath !== accountPath); + + if (mismatchedPath) { + throw new SwapKitError("wallet_ledger_invalid_params", { + accountPath, + path: mismatchedPath.fullPath, + reason: "All input derivation paths must belong to the configured Bitcoin account", + }); + } + + return parsedPaths; +} + +function descriptorTemplate(purpose: ParsedBitcoinPath["purpose"]) { + return import("@ledgerhq/device-signer-kit-bitcoin").then(({ DefaultDescriptorTemplate }) => { + switch (purpose) { + case 44: + return DefaultDescriptorTemplate.LEGACY; + case 49: + return DefaultDescriptorTemplate.NESTED_SEGWIT; + case 84: + return DefaultDescriptorTemplate.NATIVE_SEGWIT; + case 86: + return DefaultDescriptorTemplate.TAPROOT; + } + }); +} + +function stripHexPrefix(value: string) { + return value.replace(/^0x/i, ""); +} + +function signedInputParts({ + rawInput, + purpose, +}: { + rawInput: ReturnType; + purpose: ParsedBitcoinPath["purpose"]; +}) { + return match(purpose) + .with(86, () => { + const [tapKeySig] = rawInput.finalScriptWitness ?? []; + if (!tapKeySig) throw new SwapKitError("wallet_ledger_invalid_response", { reason: "Missing taproot signature" }); + return { tapKeySig }; + }) + .with(P.union(49, 84), (segwitPurpose) => { + const [signature, publicKey] = rawInput.finalScriptWitness ?? []; + if (!signature || !publicKey) { + throw new SwapKitError("wallet_ledger_invalid_response", { reason: "Missing SegWit signature or public key" }); + } + + return match(segwitPurpose) + .with(49, async () => { + const { Script } = await import("@swapkit/utxo-signer"); + const [redeemScript] = Script.decode(rawInput.finalScriptSig ?? new Uint8Array()); + if (!(redeemScript instanceof Uint8Array)) { + throw new SwapKitError("wallet_ledger_invalid_response", { reason: "Missing nested SegWit redeem script" }); + } + return { partialSig: [[publicKey, signature]] as [[Uint8Array, Uint8Array]], redeemScript }; + }) + .with(84, () => ({ partialSig: [[publicKey, signature]] as [[Uint8Array, Uint8Array]] })) + .exhaustive(); + }) + .with(44, async () => { + const { Script } = await import("@swapkit/utxo-signer"); + const [signature, publicKey] = Script.decode(rawInput.finalScriptSig ?? new Uint8Array()); + if (!(signature instanceof Uint8Array) || !(publicKey instanceof Uint8Array)) { + throw new SwapKitError("wallet_ledger_invalid_response", { reason: "Missing legacy signature or public key" }); + } + return { partialSig: [[publicKey, signature]] as [[Uint8Array, Uint8Array]] }; + }) + .exhaustive(); +} + +async function restoreSignedPsbt({ + rawTransaction, + tx, + purpose, +}: { + rawTransaction: string; + tx: Transaction; + purpose: ParsedBitcoinPath["purpose"]; +}) { + const { Transaction } = await import("@swapkit/utxo-signer"); + const parsedRaw = Transaction.fromRaw(hex.decode(rawTransaction)); + if (parsedRaw.inputsLength !== tx.inputsLength || parsedRaw.outputsLength !== tx.outputsLength) { + throw new SwapKitError("wallet_ledger_invalid_response", { + reason: "Signed Bitcoin transaction shape does not match the requested transaction", + }); + } + + const signedPsbt = tx.clone(); + for (let inputIndex = 0; inputIndex < parsedRaw.inputsLength; inputIndex += 1) { + signedPsbt.updateInput(inputIndex, await signedInputParts({ purpose, rawInput: parsedRaw.getInput(inputIndex) })); + } + return signedPsbt; +} + +export function BitcoinLedger({ + derivationPath = NetworkDerivationPath.BTC, + dmkSession, + onDeviceActionState, + transport, +}: BitcoinLedgerParams = {}) { + const configuredPath = parseBitcoinPath(derivationPath); + const legacyClient = transport ? LegacyBitcoinLedger(derivationPath, transport) : undefined; + let signerPromise: Promise | undefined; + let accountXpubPromise: Promise | undefined; + let fingerprintPromise: Promise | undefined; + + function getSigner() { + if (legacyClient) { + throw new SwapKitError("wallet_ledger_invalid_params", { + reason: "A Device Signer Kit operation is unavailable when a legacy transport is supplied", + }); + } + + signerPromise ??= (async () => { + const session = dmkSession ?? (await getLedgerDMKSession()); + const { SignerBtcBuilder } = await import("@ledgerhq/device-signer-kit-bitcoin"); + return new SignerBtcBuilder(session).build(); + })(); + + return signerPromise; + } + + function getAccountXpub() { + if (!accountXpubPromise) { + accountXpubPromise = getSigner().then(async (signer) => { + const { extendedPublicKey } = await executeLedgerDeviceAction({ + action: signer.getExtendedPublicKey(configuredPath.accountPath), + onDeviceActionState, + }); + return extendedPublicKey; + }); + const pendingXpub = accountXpubPromise; + void pendingXpub.catch(() => { + if (accountXpubPromise === pendingXpub) accountXpubPromise = undefined; + }); + } + return accountXpubPromise; + } + + function getMasterFingerprint() { + if (!fingerprintPromise) { + fingerprintPromise = getSigner().then(async (signer) => { + const { masterFingerprint } = await executeLedgerDeviceAction({ + action: signer.getMasterFingerprint(), + onDeviceActionState, + }); + return fingerprintToNumber(masterFingerprint); + }); + const pendingFingerprint = fingerprintPromise; + void pendingFingerprint.catch(() => { + if (fingerprintPromise === pendingFingerprint) fingerprintPromise = undefined; + }); + } + return fingerprintPromise; + } + + async function getWallet() { + const [{ DefaultWallet }, template] = await Promise.all([ + import("@ledgerhq/device-signer-kit-bitcoin"), + descriptorTemplate(configuredPath.purpose), + ]); + return new DefaultWallet(configuredPath.accountPath, template); + } + + async function addInputDerivations({ paths, tx }: { paths: ParsedBitcoinPath[]; tx: Transaction }) { + const [accountXpub, fingerprint] = await Promise.all([getAccountXpub(), getMasterFingerprint()]); + const { p2wpkh } = await import("@swapkit/utxo-signer"); + const accountKey = HDKey.fromExtendedKey(accountXpub); + const psbt = tx.clone(); + + for (let inputIndex = 0; inputIndex < paths.length; inputIndex += 1) { + const path = paths[inputIndex]; + if (!path) continue; + const publicKey = accountKey.derive(`m/${path.change}/${path.addressIndex}`).publicKey; + if (!publicKey) { + throw new SwapKitError("wallet_ledger_invalid_response", { + inputIndex, + reason: "Could not derive a public key from the Ledger account xpub", + }); + } + + const derivation = { fingerprint, path: pathToNumberArray(path.fullPath) }; + if (path.purpose === 86) { + const xOnlyPublicKey = publicKey.slice(1); + psbt.updateInput(inputIndex, { + tapBip32Derivation: [[xOnlyPublicKey, { der: derivation, hashes: [] }]], + tapInternalKey: xOnlyPublicKey, + }); + } else { + const input = psbt.getInput(inputIndex); + psbt.updateInput(inputIndex, { + bip32Derivation: [[publicKey, derivation]], + ...(path.purpose === 49 && !input.redeemScript ? { redeemScript: p2wpkh(publicKey).script } : {}), + }); + } + } + + return psbt; + } + + async function signWithPaths({ paths, tx }: { paths: ParsedBitcoinPath[]; tx: Transaction }) { + const [signer, wallet, psbt] = await Promise.all([getSigner(), getWallet(), addInputDerivations({ paths, tx })]); + const signedTransaction = await executeLedgerDeviceAction({ + action: signer.signTransaction(wallet, psbt.toPSBT(0)), + onDeviceActionState, + }); + return stripHexPrefix(signedTransaction); + } + + function signTransactionHex({ tx, inputUtxos }: SignBitcoinTransactionParams) { + if (inputUtxos) validateInputCount({ count: inputUtxos.length, tx }); + + if (legacyClient) { + return inputUtxos + ? legacyClient.signTransaction(tx, inputUtxos) + : signLegacyPsbtTransaction({ chain: Chain.Bitcoin, legacyClient, tx }); + } + + const paths = Array.from({ length: tx.inputsLength }, () => configuredPath); + return signWithPaths({ paths, tx }); + } + + return { + connect: async () => { + if (legacyClient) return legacyClient.connect(); + await getSigner(); + }, + disconnect: async () => legacyClient?.disconnect(), + getAddress: async () => { + if (legacyClient) return legacyClient.getAddress(); + const signer = await getSigner(); + const wallet = await getWallet(); + const { address } = await executeLedgerDeviceAction({ + action: signer.getWalletAddress(wallet, configuredPath.addressIndex, { change: configuredPath.change === 1 }), + onDeviceActionState, + }); + if (!address) { + throw new SwapKitError("wallet_ledger_get_address_error", { path: configuredPath.fullPath }); + } + return address; + }, + getExtendedPublicKey: async ({ + checkOnDevice = false, + path = configuredPath.accountPath, + }: GetExtendedPublicKeyParams = {}) => { + if (legacyClient) return legacyClient.getExtendedPublicKey(path); + const signer = await getSigner(); + const normalizedPath = normalizePath(path); + const { extendedPublicKey } = await executeLedgerDeviceAction({ + action: signer.getExtendedPublicKey(normalizedPath, { checkOnDevice }), + onDeviceActionState, + }); + return extendedPublicKey; + }, + showAddressAndPubKey: async () => { + if (legacyClient) { + const address = await legacyClient.getAddress(); + return { address }; + } + const signer = await getSigner(); + const wallet = await getWallet(); + return executeLedgerDeviceAction({ + action: signer.getWalletAddress(wallet, configuredPath.addressIndex, { + change: configuredPath.change === 1, + checkOnDevice: true, + }), + onDeviceActionState, + }); + }, + signTransaction: async (tx: Transaction) => { + const signedTransaction = await signTransactionHex({ tx }); + return restoreSignedPsbt({ purpose: configuredPath.purpose, rawTransaction: signedTransaction, tx }); + }, + signTransactionHex, + signTransactionWithMultiplePaths: async ({ + derivationPaths, + inputUtxos, + tx, + }: SignBitcoinTransactionWithMultiplePathsParams) => { + if (inputUtxos) validateInputCount({ count: inputUtxos.length, tx }); + const paths = validateAccountPaths({ accountPath: configuredPath.accountPath, derivationPaths, tx }); + + if (legacyClient) { + const inputs = inputUtxos ?? (await extractInputsFromPsbt(tx, Chain.Bitcoin)); + return legacyClient.signTransactionWithMultiplePaths( + tx, + inputs, + paths.map((path) => `m/${path.fullPath}`), + ); + } + + return signWithPaths({ paths, tx }); + }, + }; +} diff --git a/packages/wallet-hardware/src/ledger/clients/cosmos.ts b/packages/wallet-hardware/src/ledger/clients/cosmos.ts index e78cd2d..8ef8bbb 100644 --- a/packages/wallet-hardware/src/ledger/clients/cosmos.ts +++ b/packages/wallet-hardware/src/ledger/clients/cosmos.ts @@ -1,85 +1,182 @@ +import { encodeSecp256k1Signature, type StdSignDoc, serializeSignDoc } from "@cosmjs/amino"; +import { Secp256k1Signature } from "@cosmjs/crypto"; +import type { SignerCosmos } from "@ledgerhq/device-signer-kit-cosmos"; import type Transport from "@ledgerhq/hw-transport"; +import { hex } from "@scure/base"; import { type DerivationPathArray, derivationPathToString, NetworkDerivationPath, SwapKitError, } from "@swapkit/helpers"; -import { CosmosLedgerInterface } from "../interfaces/CosmosLedgerInterface"; -export class CosmosLedger extends CosmosLedgerInterface { - private pubKey: string | null = null; +import type { LedgerDMKSession } from "../helpers/dmk"; +import { getLedgerDMKSession } from "../helpers/dmk"; +import { executeLedgerDeviceAction, type LedgerDeviceActionStateHandler } from "../helpers/executeDeviceAction"; - derivationPath: string; +interface CosmosLedgerParams { + derivationPath?: DerivationPathArray | string; + dmkSession?: LedgerDMKSession; + onDeviceActionState?: LedgerDeviceActionStateHandler; + transport?: Transport; +} - constructor(derivationPath: DerivationPathArray = NetworkDerivationPath.GAIA, transport?: Transport) { - super(transport); - this.chain = "cosmos"; - this.derivationPath = derivationPathToString(derivationPath); - } +interface CosmosAddressAndPublicKey { + address: string; + publicKey: string; +} - connect = async () => { - await this.checkOrCreateTransportAndLedger(true); - const { publicKey, address } = await this.getAddressAndPubKey(); +function normalizeCosmosPath(path: DerivationPathArray | string) { + const normalized = (typeof path === "string" ? path : derivationPathToString(path)) + .replace(/^m\//, "") + .replace(/^\/+/, ""); - this.pubKey = Buffer.from(publicKey, "hex").toString("base64"); + if (!/^44'\/118'\/\d+'\/(0|1)\/\d+$/.test(normalized)) { + throw new SwapKitError("wallet_ledger_invalid_params", { + path: normalized, + reason: "Expected a five-level Cosmos derivation path with coin type 118", + }); + } - return address; - }; + return normalized; +} - getAddressAndPubKey = async () => { - await this.checkOrCreateTransportAndLedger(true); +function normalizeCosmosSignature(signature: Uint8Array) { + try { + return signature.length === 64 + ? Secp256k1Signature.fromFixedLength(signature).toFixedLength() + : Secp256k1Signature.fromDer(signature).toFixedLength(); + } catch (error) { + throw new SwapKitError("wallet_ledger_invalid_response", error); + } +} - const response = await this.ledgerApp.getAddress(this.derivationPath, this.chain); +export class CosmosLedger { + readonly chain = "cosmos"; + readonly derivationPath: string; + private readonly dmkSession?: LedgerDMKSession; + private readonly onDeviceActionState?: LedgerDeviceActionStateHandler; + private readonly transport?: Transport; + private legacyAppPromise?: Promise; + private pubKey: string | null = null; + private signerPromise?: Promise; + + constructor({ + derivationPath = NetworkDerivationPath.GAIA, + dmkSession, + onDeviceActionState, + transport, + }: CosmosLedgerParams = {}) { + this.derivationPath = normalizeCosmosPath(derivationPath); + this.dmkSession = dmkSession; + this.onDeviceActionState = onDeviceActionState; + this.transport = transport; + } - return response; - }; + private getSigner() { + if (this.transport) { + throw new SwapKitError("wallet_ledger_invalid_params", { + reason: "A Device Signer Kit operation is unavailable when a legacy transport is supplied", + }); + } - signTransaction = async (rawTx: string, sequence = "0") => { - await this.checkOrCreateTransportAndLedger(true); + this.signerPromise ??= (async () => { + const session = this.dmkSession ?? (await getLedgerDMKSession()); + const { SignerCosmosBuilder } = await import("@ledgerhq/device-signer-kit-cosmos"); + return new SignerCosmosBuilder(session).build(); + })(); + return this.signerPromise; + } - const { return_code, error_message, signature } = await this.ledgerApp.sign(this.derivationPath, rawTx); + private getLegacyApp() { + if (!this.transport) { + throw new SwapKitError("wallet_ledger_invalid_params", { reason: "Legacy Cosmos transport is unavailable" }); + } - if (!this.pubKey) throw new SwapKitError("wallet_ledger_pubkey_not_found"); + this.legacyAppPromise ??= import("@ledgerhq/hw-app-cosmos").then( + ({ default: CosmosApp }) => new CosmosApp(this.transport as Transport), + ); + return this.legacyAppPromise; + } - this.validateResponse(return_code, error_message); + private async signBytes(message: Uint8Array) { + if (this.transport) { + const app = await this.getLegacyApp(); + const response = await app.sign(this.derivationPath, new TextDecoder().decode(message)); + if (response.return_code !== 0x9000 || !response.signature) { + throw new SwapKitError("wallet_ledger_invalid_response", { returnCode: response.return_code }); + } + return normalizeCosmosSignature(response.signature); + } - return [{ pub_key: { type: "tendermint/PubKeySecp256k1", value: this.pubKey }, sequence, signature }]; - }; + const signer = await this.getSigner(); + const signature = await executeLedgerDeviceAction({ + action: signer.signTransaction(this.derivationPath, this.chain, message), + onDeviceActionState: this.onDeviceActionState, + }); + return normalizeCosmosSignature(signature); + } - signAmino = async (signerAddress: string, signDoc: any): Promise => { - await this.checkOrCreateTransportAndLedger(true); + async connect() { + if (this.transport) await this.getLegacyApp(); + else await this.getSigner(); - const accounts = await this.getAccounts(); - const accountIndex = accounts.findIndex((account) => account.address === signerAddress); + const { address, publicKey } = await this.getAddressAndPubKey(); + this.pubKey = Buffer.from(publicKey, "hex").toString("base64"); + return address; + } - if (accountIndex === -1) { - throw new SwapKitError("wallet_ledger_address_not_found", { address: signerAddress }); + async disconnect() {} + + async getAddressAndPubKey(): Promise { + if (this.transport) { + return this.getLegacyApp().then((app) => app.getAddress(this.derivationPath, this.chain)); } - const importedAmino = await import("@cosmjs/amino"); - const encodeSecp256k1Signature = - importedAmino.encodeSecp256k1Signature ?? importedAmino.default?.encodeSecp256k1Signature; - const serializeSignDoc = importedAmino.serializeSignDoc ?? importedAmino.default?.serializeSignDoc; - const importedCrypto = await import("@cosmjs/crypto"); - const Secp256k1Signature = importedCrypto.Secp256k1Signature ?? importedCrypto.default?.Secp256k1Signature; + const signer = await this.getSigner(); + const { address, publicKey } = await executeLedgerDeviceAction({ + action: signer.getAddress(this.derivationPath, this.chain), + onDeviceActionState: this.onDeviceActionState, + }); + return { address, publicKey: hex.encode(publicKey) }; + } + + async showAddressAndPubKey(): Promise { + if (this.transport) { + return this.getLegacyApp().then((app) => app.getAddress(this.derivationPath, this.chain, true)); + } - const message = serializeSignDoc(signDoc); - const signature = await this.ledgerApp.sign(this.derivationPath, message); + const signer = await this.getSigner(); + const { address, publicKey } = await executeLedgerDeviceAction({ + action: signer.getAddress(this.derivationPath, this.chain, { checkOnDevice: true }), + onDeviceActionState: this.onDeviceActionState, + }); + return { address, publicKey: hex.encode(publicKey) }; + } - this.validateResponse(signature.return_code, signature.error_message); + async signTransaction(rawTx: string | Uint8Array, sequence = "0") { + const signature = await this.signBytes(typeof rawTx === "string" ? new TextEncoder().encode(rawTx) : rawTx); + if (!this.pubKey) { + const { publicKey } = await this.getAddressAndPubKey(); + this.pubKey = Buffer.from(publicKey, "hex").toString("base64"); + } - const secpSignature = Secp256k1Signature.fromDer(signature.signature).toFixedLength(); + return [{ pub_key: { type: "tendermint/PubKeySecp256k1", value: this.pubKey }, sequence, signature }]; + } - return { signature: encodeSecp256k1Signature(accounts[0].pubkey, secpSignature), signed: signDoc }; - }; + async signAmino(signerAddress: string, signDoc: StdSignDoc) { + const accounts = await this.getAccounts(); + const account = accounts.find(({ address }) => address === signerAddress); + if (!account) { + throw new SwapKitError("wallet_ledger_address_not_found", { address: signerAddress }); + } - getAccounts = async () => { - await this.checkOrCreateTransportAndLedger(true); + const signature = await this.signBytes(serializeSignDoc(signDoc)); + return { signature: encodeSecp256k1Signature(account.pubkey, signature), signed: signDoc }; + } - const addressAndPubKey = await this.getAddressAndPubKey(); - return [ - { address: addressAndPubKey.address, algo: "secp256k1", pubkey: Buffer.from(addressAndPubKey.publicKey, "hex") }, - ] as any[]; - }; + async getAccounts() { + const { address, publicKey } = await this.getAddressAndPubKey(); + return [{ address, algo: "secp256k1" as const, pubkey: hex.decode(publicKey) }]; + } } diff --git a/packages/wallet-hardware/src/ledger/clients/evm.ts b/packages/wallet-hardware/src/ledger/clients/evm.ts index 7a97c19..c248003 100644 --- a/packages/wallet-hardware/src/ledger/clients/evm.ts +++ b/packages/wallet-hardware/src/ledger/clients/evm.ts @@ -1,5 +1,4 @@ -import type EthereumApp from "@ledgerhq/hw-app-eth"; -import type Transport from "@ledgerhq/hw-transport"; +import type { TypedDataDomain as LedgerTypedDataDomain, SignerEth } from "@ledgerhq/device-signer-kit-ethereum"; import { ChainId, type DerivationPathArray, @@ -10,99 +9,137 @@ import { import { AbstractSigner, type Provider, + type TransactionLike, type TransactionRequest, type TypedDataDomain, type TypedDataField, } from "ethers"; -import { getLedgerTransport } from "../helpers/getLedgerTransport"; +import type { LedgerDMKSession } from "../helpers/dmk"; +import { executeLedgerDeviceAction, type LedgerDeviceActionStateHandler } from "../helpers/executeDeviceAction"; -function parseLedgerSignatureV(v: number | string) { - if (typeof v === "number") return v; +interface EVMLedgerParams { + dmkSession: LedgerDMKSession; + provider: Provider; + derivationPath?: DerivationPathArray | string; + chainId?: ChainId; + onDeviceActionState?: LedgerDeviceActionStateHandler; + originToken?: string; +} + +function selectTypedDataTypes({ + primaryType, + types, +}: { + primaryType: string; + types: Record; +}) { + const selectedTypes: Record = {}; + + function addType(typeName: string) { + if (Object.hasOwn(selectedTypes, typeName) || !Object.hasOwn(types, typeName)) return; + const fields = types[typeName]; + if (!fields) return; + + selectedTypes[typeName] = fields; + for (const field of fields) addType(field.type.replace(/\[[0-9]*\]/g, "")); + } + + addType(primaryType); + + if (!Object.hasOwn(selectedTypes, primaryType)) { + throw new SwapKitError("wallet_ledger_invalid_params", { + primaryType, + reason: "The EIP-712 primary type is not defined", + }); + } - const hex = v.startsWith("0x") ? v.slice(2) : v; - return Number.parseInt(hex || "0", 16); + return selectedTypes; } class EVMLedgerInterface extends AbstractSigner { chainId: ChainId = ChainId.Ethereum; derivationPath = ""; - ledgerApp: InstanceType | null = null; - ledgerTimeout = 50000; - private transport?: Transport; - private readonly injectedTransport?: Transport; + private readonly dmkSession: LedgerDMKSession; + private ledgerSignerPromise?: Promise; + private readonly onDeviceActionState?: LedgerDeviceActionStateHandler; + private readonly originToken?: string; constructor({ provider, + dmkSession, derivationPath = NetworkDerivationPath.OP, chainId = ChainId.Optimism, - transport, - }: { provider: Provider; derivationPath?: DerivationPathArray | string; chainId?: ChainId; transport?: Transport }) { + onDeviceActionState, + originToken, + }: EVMLedgerParams) { super(provider); this.chainId = chainId || ChainId.Ethereum; - this.derivationPath = typeof derivationPath === "string" ? derivationPath : derivationPathToString(derivationPath); - this.injectedTransport = transport; + this.derivationPath = ( + typeof derivationPath === "string" ? derivationPath : derivationPathToString(derivationPath) + ).replace(/^m\//, ""); + this.dmkSession = dmkSession; + this.onDeviceActionState = onDeviceActionState; + this.originToken = originToken; Object.defineProperty(this, "provider", { enumerable: true, value: provider || null, writable: false }); } + private getLedgerSigner = () => { + this.ledgerSignerPromise ??= import("@ledgerhq/device-signer-kit-ethereum").then(({ SignerEthBuilder }) => + new SignerEthBuilder({ ...this.dmkSession, originToken: this.originToken }).build(), + ); + + return this.ledgerSignerPromise; + }; + connect = (provider: Provider) => new EVMLedgerInterface({ chainId: this.chainId, derivationPath: this.derivationPath, + dmkSession: this.dmkSession, + onDeviceActionState: this.onDeviceActionState, + originToken: this.originToken, provider, - transport: this.transport ?? this.injectedTransport, }); - checkOrCreateTransportAndLedger = async () => { - await this.createTransportAndLedger(); - }; - - createTransportAndLedger = async () => { - if (this.ledgerApp) return; - - this.transport ||= this.injectedTransport ?? (await getLedgerTransport()); - const EthereumApp = (await import("@ledgerhq/hw-app-eth")).default; - - this.ledgerApp = new EthereumApp(this.transport); - }; - getAddress = async () => { - const response = await this.getAddressAndPubKey(); - if (!response) throw new SwapKitError("wallet_ledger_failed_to_get_address"); - return response.address; + const { address } = await this.getAddressAndPubKey(); + if (!address) throw new SwapKitError("wallet_ledger_failed_to_get_address"); + return address; }; getAddressAndPubKey = async () => { - await this.createTransportAndLedger(); - return this.ledgerApp?.getAddress(this.derivationPath); - }; + const ledgerSigner = await this.getLedgerSigner(); - showAddressAndPubKey = async () => { - await this.createTransportAndLedger(); - return this.ledgerApp?.getAddress(this.derivationPath, true); + return executeLedgerDeviceAction({ + action: ledgerSigner.getAddress(this.derivationPath, { chainId: Number(this.chainId) }), + onDeviceActionState: this.onDeviceActionState, + }); }; - signMessage = async (messageHex: string) => { - const { Signature } = await import("ethers"); - await this.createTransportAndLedger(); - - const sig = await this.ledgerApp?.signPersonalMessage(this.derivationPath, messageHex); - - if (!sig) throw new SwapKitError("wallet_ledger_signing_error"); + showAddressAndPubKey = async () => { + const ledgerSigner = await this.getLedgerSigner(); - sig.r = `0x${sig.r}`; - sig.s = `0x${sig.s}`; - return Signature.from(sig).serialized; + return executeLedgerDeviceAction({ + action: ledgerSigner.getAddress(this.derivationPath, { chainId: Number(this.chainId), checkOnDevice: true }), + onDeviceActionState: this.onDeviceActionState, + }); }; - sendTransaction = async (tx: TransactionRequest): Promise => { - if (!this.provider) throw new SwapKitError("wallet_ledger_no_provider"); - - const signedTxHex = await this.signTransaction(tx); + signMessage = async (message: string | Uint8Array) => { + const { Signature, toUtf8Bytes } = await import("ethers"); + const ledgerSigner = await this.getLedgerSigner(); + const signature = await executeLedgerDeviceAction({ + action: ledgerSigner.signMessage( + this.derivationPath, + typeof message === "string" ? toUtf8Bytes(message) : message, + ), + onDeviceActionState: this.onDeviceActionState, + }); - return await this.provider.broadcastTransaction(signedTxHex); + return Signature.from(signature).serialized; }; signTypedData = async ( @@ -112,96 +149,139 @@ class EVMLedgerInterface extends AbstractSigner { explicitPrimaryType?: string, ) => { const { buildEIP712DomainType } = await import("@swapkit/toolboxes/evm"); - const { Signature, TypedDataEncoder } = await import("ethers"); - await this.createTransportAndLedger(); - + const { hexlify, Signature, TypedDataEncoder } = await import("ethers"); const { EIP712Domain: _, ...filteredTypes } = types; const primaryType = explicitPrimaryType ?? TypedDataEncoder.from(filteredTypes).primaryType; + const resolutionTypes = selectTypedDataTypes({ primaryType, types: filteredTypes }); + const populated = await TypedDataEncoder.resolveNames(domain, resolutionTypes, value, async (name) => { + const resolvedAddress = await this.resolveName(name); + if (!resolvedAddress) throw new SwapKitError("wallet_ledger_invalid_params", { name }); + return resolvedAddress; + }); + const chainId = populated.domain.chainId == null ? undefined : Number(populated.domain.chainId); - let sig: { v: number; s: string; r: string } | undefined; + if (chainId !== undefined && !Number.isSafeInteger(chainId)) { + throw new SwapKitError("wallet_ledger_invalid_params", { chainId: populated.domain.chainId }); + } - try { - sig = await this.ledgerApp?.signEIP712Message(this.derivationPath, { - domain: domain as Record, - message: value, + const ledgerDomain = { + ...(populated.domain.chainId != null && { chainId }), + ...(populated.domain.name != null && { name: populated.domain.name }), + ...(populated.domain.salt != null && { salt: hexlify(populated.domain.salt) }), + ...(populated.domain.verifyingContract != null && { verifyingContract: populated.domain.verifyingContract }), + ...(populated.domain.version != null && { version: populated.domain.version }), + } satisfies LedgerTypedDataDomain; + + const ledgerSigner = await this.getLedgerSigner(); + const signature = await executeLedgerDeviceAction({ + action: ledgerSigner.signTypedData(this.derivationPath, { + domain: ledgerDomain, + message: populated.value, primaryType, - types: { EIP712Domain: buildEIP712DomainType(domain), ...filteredTypes }, + types: { EIP712Domain: buildEIP712DomainType(populated.domain), ...filteredTypes }, + }), + onDeviceActionState: this.onDeviceActionState, + }); + + return Signature.from(signature).serialized; + }; + + signTransaction = async (tx: TransactionRequest) => { + const { copyRequest, getAddress, getBytes, resolveAddress, resolveProperties, Transaction } = await import( + "ethers" + ); + const request = copyRequest(tx); + + if (request.type === 4 || request.authorizationList?.length) { + throw new SwapKitError("wallet_ledger_invalid_params", { + message: "Ledger Ethereum DSK does not support EIP-7702 transactions", }); - } catch (error) { - const isLedgerDeviceError = error instanceof Error && "statusCode" in error; - if (!isLedgerDeviceError || (isLedgerDeviceError && (error as { statusCode: number }).statusCode === 0x6985)) { - throw error; - } + } - const domainSeparator = TypedDataEncoder.hashDomain(domain).slice(2); - const messageHash = TypedDataEncoder.from(filteredTypes).hash(value).slice(2); + const { from, to } = await resolveProperties({ + from: request.from ? resolveAddress(request.from, this) : undefined, + to: request.to ? resolveAddress(request.to, this) : undefined, + }); + const signerAddress = from || request.nonce == null ? await this.getAddress() : undefined; - sig = await this.ledgerApp?.signEIP712HashedMessage(this.derivationPath, domainSeparator, messageHash); + if (from && signerAddress && getAddress(from) !== getAddress(signerAddress)) { + throw new SwapKitError("wallet_ledger_invalid_params", { + message: "Transaction from address does not match the Ledger account", + }); } - if (!sig) throw new SwapKitError("wallet_ledger_signing_error"); + if (to) request.to = to; + delete request.from; + delete request.authorizationList; + request.chainId ??= BigInt(this.chainId); + request.nonce ??= await this.provider?.getTransactionCount(signerAddress ?? (await this.getAddress())); + + const baseTx = Transaction.from(request as TransactionLike); + const unsignedTransaction = getBytes(baseTx.unsignedSerialized); + const ledgerSigner = await this.getLedgerSigner(); + const signature = await executeLedgerDeviceAction({ + action: ledgerSigner.signTransaction(this.derivationPath, unsignedTransaction), + onDeviceActionState: this.onDeviceActionState, + }); + + baseTx.signature = signature; - sig.r = `0x${sig.r}`; - sig.s = `0x${sig.s}`; - return Signature.from(sig).serialized; + return baseTx.serialized; }; +} - signTransaction = async (tx: TransactionRequest) => { - const { Transaction } = await import("ethers"); - await this.createTransportAndLedger(); +interface LedgerParams { + dmkSession: LedgerDMKSession; + provider: Provider; + derivationPath?: DerivationPathArray; + onDeviceActionState?: LedgerDeviceActionStateHandler; + originToken?: string; +} - const nonce = tx.nonce ?? undefined; - const transactionCount = - nonce === undefined ? await this.provider?.getTransactionCount(tx.from || (await this.getAddress())) : undefined; +export function ArbitrumLedger(params: LedgerParams) { + return new EVMLedgerInterface({ ...params, chainId: ChainId.Arbitrum }); +} - const baseTx = { - chainId: tx.chainId || this.chainId, - data: tx.data, - gasLimit: tx.gasLimit, - ...(tx.gasPrice && { gasPrice: tx.gasPrice }), - ...(!tx.gasPrice && - tx.maxFeePerGas && { maxFeePerGas: tx.maxFeePerGas, maxPriorityFeePerGas: tx.maxPriorityFeePerGas }), - nonce: nonce !== undefined ? Number(nonce.toString()) : transactionCount, - to: tx.to?.toString(), - type: tx.type && !Number.isNaN(tx.type) ? tx.type : tx.maxFeePerGas ? 2 : 0, - value: tx.value, - }; +export function AuroraLedger(params: LedgerParams) { + return new EVMLedgerInterface({ ...params, chainId: ChainId.Aurora }); +} - // ledger expects the tx to be serialized without the 0x prefix - const unsignedTx = Transaction.from(baseTx).unsignedSerialized.slice(2); +export function AvalancheLedger(params: LedgerParams) { + return new EVMLedgerInterface({ ...params, chainId: ChainId.Avalanche }); +} - const { ledgerService } = await import("@ledgerhq/hw-app-eth"); +export function BaseLedger(params: LedgerParams) { + return new EVMLedgerInterface({ ...params, chainId: ChainId.Base }); +} - const resolution = await ledgerService.resolveTransaction(unsignedTx, {}, { erc20: true, externalPlugins: true }); +export function EthereumLedger(params: LedgerParams) { + return new EVMLedgerInterface({ ...params, chainId: ChainId.Ethereum }); +} - const signature = await this.ledgerApp?.signTransaction(this.derivationPath, unsignedTx, resolution); +export function GnosisLedger(params: LedgerParams) { + return new EVMLedgerInterface({ ...params, chainId: ChainId.Gnosis }); +} - if (!signature) throw new SwapKitError("wallet_ledger_signing_error"); +export function OptimismLedger(params: LedgerParams) { + return new EVMLedgerInterface({ ...params, chainId: ChainId.Optimism }); +} - const { r, s, v } = signature; +export function PolygonLedger(params: LedgerParams) { + return new EVMLedgerInterface({ ...params, chainId: ChainId.Polygon }); +} - return Transaction.from({ ...baseTx, signature: { r: `0x${r}`, s: `0x${s}`, v: parseLedgerSignatureV(v) } }) - .serialized; - }; +export function BinanceSmartChainLedger(params: LedgerParams) { + return new EVMLedgerInterface({ ...params, chainId: ChainId.BinanceSmartChain }); +} + +export function MonadLedger(params: LedgerParams) { + return new EVMLedgerInterface({ ...params, chainId: ChainId.Monad }); } -type LedgerParams = { provider: Provider; derivationPath?: DerivationPathArray; transport?: Transport }; - -export const ArbitrumLedger = (params: LedgerParams) => - new EVMLedgerInterface({ ...params, chainId: ChainId.Arbitrum }); -export const AuroraLedger = (params: LedgerParams) => new EVMLedgerInterface({ ...params, chainId: ChainId.Aurora }); -export const AvalancheLedger = (params: LedgerParams) => - new EVMLedgerInterface({ ...params, chainId: ChainId.Avalanche }); -export const BaseLedger = (params: LedgerParams) => new EVMLedgerInterface({ ...params, chainId: ChainId.Base }); -export const EthereumLedger = (params: LedgerParams) => - new EVMLedgerInterface({ ...params, chainId: ChainId.Ethereum }); -export const GnosisLedger = (params: LedgerParams) => new EVMLedgerInterface({ ...params, chainId: ChainId.Gnosis }); -export const OptimismLedger = (params: LedgerParams) => - new EVMLedgerInterface({ ...params, chainId: ChainId.Optimism }); -export const PolygonLedger = (params: LedgerParams) => new EVMLedgerInterface({ ...params, chainId: ChainId.Polygon }); -export const BinanceSmartChainLedger = (params: LedgerParams) => - new EVMLedgerInterface({ ...params, chainId: ChainId.BinanceSmartChain }); -export const MonadLedger = (params: LedgerParams) => new EVMLedgerInterface({ ...params, chainId: ChainId.Monad }); -export const XLayerLedger = (params: LedgerParams) => new EVMLedgerInterface({ ...params, chainId: ChainId.XLayer }); -export const BerachainLedger = (params: LedgerParams) => - new EVMLedgerInterface({ ...params, chainId: ChainId.Berachain }); +export function XLayerLedger(params: LedgerParams) { + return new EVMLedgerInterface({ ...params, chainId: ChainId.XLayer }); +} + +export function BerachainLedger(params: LedgerParams) { + return new EVMLedgerInterface({ ...params, chainId: ChainId.Berachain }); +} diff --git a/packages/wallet-hardware/src/ledger/clients/near.ts b/packages/wallet-hardware/src/ledger/clients/near.ts index 6e049db..5108e09 100644 --- a/packages/wallet-hardware/src/ledger/clients/near.ts +++ b/packages/wallet-hardware/src/ledger/clients/near.ts @@ -1,18 +1,43 @@ +import type { UserInteractionRequired } from "@ledgerhq/device-management-kit"; import type Transport from "@ledgerhq/hw-transport"; import type { SignedTransaction, Transaction } from "@near-js/transactions"; -import type { DerivationPathArray } from "@swapkit/helpers"; +import { Chain, type DerivationPathArray, NetworkDerivationPath, SwapKitError } from "@swapkit/helpers"; import type { NearSigner } from "@swapkit/toolboxes/near"; -import { getLedgerTransport } from "../helpers/getLedgerTransport"; -export async function getNearLedgerClient(derivationPath?: DerivationPathArray, injectedTransport?: Transport) { - const Near = (await import("@ledgerhq/hw-app-near")).default; - const { Chain, NetworkDerivationPath, SwapKitError } = await import("@swapkit/helpers"); - const transport = injectedTransport ?? (await getLedgerTransport()); - const nearApp = new Near(transport); +import { + LEDGER_USER_INTERACTION_REQUIRED, + type LedgerJsClientParams, + normalizeLedgerJsClientParams, + runLedgerJsOperation, +} from "../helpers/ledgerJsDmkBridge"; +type NearLedgerParams = LedgerJsClientParams; + +export async function getNearLedgerClient( + paramsOrPath?: NearLedgerParams | DerivationPathArray, + transport?: Transport, +) { + const { derivationPath, ...connection } = normalizeLedgerJsClientParams({ paramsOrPath, transport }); const path = (derivationPath || NetworkDerivationPath[Chain.Near]).join("'/").concat("'"); - const { address, publicKey: pubKeyHex } = await nearApp.getAddress(path); + async function runNearOperation({ + operation, + requiredUserInteraction, + }: { + operation: (app: InstanceType) => Promise; + requiredUserInteraction?: UserInteractionRequired; + }) { + const Near = (await import("@ledgerhq/hw-app-near")).default; + return runLedgerJsOperation({ + appName: "NEAR", + connection, + createApp: (ledgerTransport) => new Near(ledgerTransport), + operation, + requiredUserInteraction, + }); + } + + const { address, publicKey } = await runNearOperation({ operation: (app) => app.getAddress(path) }); const signer = { getAddress() { @@ -20,7 +45,8 @@ export async function getNearLedgerClient(derivationPath?: DerivationPathArray, }, async getPublicKey() { const { PublicKey } = await import("@near-js/crypto"); - return PublicKey.fromString(`ed25519:${pubKeyHex}`); + const encodedPublicKey = publicKey.startsWith("ed25519:") ? publicKey : `ed25519:${publicKey}`; + return PublicKey.fromString(encodedPublicKey); }, signDelegateAction(_delegateAction: any) { @@ -44,13 +70,13 @@ export async function getNearLedgerClient(derivationPath?: DerivationPathArray, async signTransaction(transaction: Transaction) { const { Signature, SignedTransaction } = await import("@near-js/transactions"); try { - const signatureArray = await nearApp.signTransaction(transaction.encode(), path); - if (!signatureArray) { - throw new Error("Signature undefined"); - } + const signatureArray = await runNearOperation({ + operation: (app) => app.signTransaction(transaction.encode(), path), + requiredUserInteraction: LEDGER_USER_INTERACTION_REQUIRED.SignTransaction, + }); + if (!signatureArray) throw new Error("Signature undefined"); const signature = new Signature({ data: signatureArray, keyType: 0 }); - const signedTransaction = new SignedTransaction({ signature, transaction }); return [signatureArray, signedTransaction] as [Uint8Array, SignedTransaction]; diff --git a/packages/wallet-hardware/src/ledger/clients/sui.ts b/packages/wallet-hardware/src/ledger/clients/sui.ts index 99c6bac..eb5568e 100644 --- a/packages/wallet-hardware/src/ledger/clients/sui.ts +++ b/packages/wallet-hardware/src/ledger/clients/sui.ts @@ -1,3 +1,4 @@ +import type { UserInteractionRequired } from "@ledgerhq/device-management-kit"; import type Sui from "@ledgerhq/hw-app-sui"; import type Transport from "@ledgerhq/hw-transport"; import { @@ -8,55 +9,56 @@ import { SwapKitError, } from "@swapkit/helpers"; -import { getLedgerTransport } from "../helpers/getLedgerTransport"; +import { + LEDGER_USER_INTERACTION_REQUIRED, + type LedgerJsClientParams, + normalizeLedgerJsClientParams, + runLedgerJsOperation, +} from "../helpers/ledgerJsDmkBridge"; + +type SuiLedgerParams = LedgerJsClientParams; export class SuiLedgerInterface { - derivationPath: string; - ledgerApp: InstanceType | null = null; + readonly derivationPath: string; address: string | null = null; publicKey: Uint8Array | null = null; - private readonly injectedTransport?: Transport; + private readonly connection: Omit; - constructor(derivationPath?: DerivationPathArray | string, transport?: Transport) { + constructor(paramsOrPath?: SuiLedgerParams | DerivationPathArray | string, transport?: Transport) { + const { derivationPath, ...connection } = normalizeLedgerJsClientParams({ paramsOrPath, transport }); this.derivationPath = typeof derivationPath === "string" ? derivationPath : derivationPathToString(derivationPath || NetworkDerivationPath[Chain.Sui]); - this.injectedTransport = transport; + this.connection = connection; } - /** - * Transform derivation path for SUI Ledger format. - * SUI Ledger expects: 44'/784'/0'/0'/0' (all hardened, no 'm/' prefix) - * Standard format is: m/44'/784'/0'/0/0 (only first 3 hardened, has 'm/' prefix) - */ - private getLedgerPath(): string { - return this.derivationPath - .replace(/^m\//, "") // Remove 'm/' prefix - .replace(/\/(\d+)\/(\d+)$/, "/$1'/$2'"); // Make last two components hardened + private getLedgerPath() { + return this.derivationPath.replace(/^m\//, "").replace(/\/(\d+)\/(\d+)$/, "/$1'/$2'"); } - private async createTransportAndLedger() { - if (this.ledgerApp) return; - - const transport = this.injectedTransport ?? (await getLedgerTransport()); + private async runSuiOperation({ + operation, + requiredUserInteraction, + }: { + operation: (app: InstanceType) => Promise; + requiredUserInteraction?: UserInteractionRequired; + }) { const SuiApp = (await import("@ledgerhq/hw-app-sui")).default; - this.ledgerApp = new SuiApp(transport); + return runLedgerJsOperation({ + appName: "Sui", + connection: this.connection, + createApp: (ledgerTransport) => new SuiApp(ledgerTransport), + operation, + requiredUserInteraction, + }); } - async connect(): Promise { - await this.createTransportAndLedger(); - - if (!this.ledgerApp) { - throw new SwapKitError("wallet_ledger_transport_error"); - } - + async connect() { const ledgerPath = this.getLedgerPath(); - const result = await this.ledgerApp.getPublicKey(ledgerPath); + const result = await this.runSuiOperation({ operation: (app) => app.getPublicKey(ledgerPath) }); - if (!result?.publicKey) { - throw new SwapKitError("wallet_ledger_failed_to_get_address"); - } + if (!result?.publicKey) throw new SwapKitError("wallet_ledger_failed_to_get_address"); this.publicKey = result.publicKey; this.address = `0x${Buffer.from(result.address).toString("hex")}`; @@ -64,71 +66,51 @@ export class SuiLedgerInterface { return this.address; } - toSuiAddress(): string { - if (!this.address) { - throw new SwapKitError("wallet_ledger_failed_to_get_address"); - } + toSuiAddress() { + if (!this.address) throw new SwapKitError("wallet_ledger_failed_to_get_address"); return this.address; } - async getAddress(): Promise { + async getAddress() { if (this.address) return this.address; return await this.connect(); } - async signTransaction( - input: Uint8Array | { transaction: Uint8Array }, - ): Promise<{ bytes: string; signature: string }> { + async signTransaction(input: Uint8Array | { transaction: Uint8Array }) { const txBytes = input instanceof Uint8Array ? input : input.transaction; - await this.createTransportAndLedger(); - - if (!this.ledgerApp) { - throw new SwapKitError("wallet_ledger_transport_error"); - } - - if (!this.publicKey) { - throw new SwapKitError("wallet_ledger_failed_to_get_address"); - } + if (!this.publicKey) throw new SwapKitError("wallet_ledger_failed_to_get_address"); try { const ledgerPath = this.getLedgerPath(); - - // SUI intent message format for TransactionData: - // [intent_scope=0 (TransactionData), intent_version=0, app_id=0] + transaction_bytes - // The Ledger SUI app expects the INTENT MESSAGE, not raw transaction bytes const intentMessage = new Uint8Array(3 + txBytes.length); - intentMessage[0] = 0; // IntentScope: TransactionData - intentMessage[1] = 0; // IntentVersion: V0 - intentMessage[2] = 0; // AppId: Sui intentMessage.set(txBytes, 3); - const result = await this.ledgerApp.signTransaction(ledgerPath, intentMessage); + const result = await this.runSuiOperation({ + operation: (app) => app.signTransaction(ledgerPath, intentMessage), + requiredUserInteraction: LEDGER_USER_INTERACTION_REQUIRED.SignTransaction, + }); - if (!result?.signature) { - throw new SwapKitError("wallet_ledger_signing_error"); - } + if (!result?.signature) throw new SwapKitError("wallet_ledger_signing_error"); - // SUI signature format: [scheme_flag (1 byte)] + [signature (64 bytes)] + [public_key (32 bytes)] - // Scheme flag 0x00 = Ed25519 const pubKey = this.publicKey.length === 33 ? this.publicKey.slice(1) : this.publicKey; if (pubKey.length !== 32) { throw new SwapKitError("wallet_ledger_signing_error", { error: "Invalid public key length" }); } - const serializedSignature = new Uint8Array(1 + 64 + 32); - serializedSignature[0] = 0x00; // Ed25519 scheme flag + const serializedSignature = new Uint8Array(97); serializedSignature.set(result.signature, 1); serializedSignature.set(pubKey, 65); - const signatureBase64 = Buffer.from(serializedSignature).toString("base64"); - const bytesBase64 = Buffer.from(txBytes).toString("base64"); - - return { bytes: bytesBase64, signature: signatureBase64 }; + return { + bytes: Buffer.from(txBytes).toString("base64"), + signature: Buffer.from(serializedSignature).toString("base64"), + }; } catch (error) { throw new SwapKitError("wallet_ledger_signing_error", { error }); } } } -export const SuiLedger = (derivationPath?: DerivationPathArray, transport?: Transport) => - new SuiLedgerInterface(derivationPath, transport); +export function SuiLedger(paramsOrPath?: SuiLedgerParams | DerivationPathArray, transport?: Transport) { + return new SuiLedgerInterface(paramsOrPath, transport); +} diff --git a/packages/wallet-hardware/src/ledger/clients/thorchain/common.ts b/packages/wallet-hardware/src/ledger/clients/thorchain/common.ts deleted file mode 100644 index 147be87..0000000 --- a/packages/wallet-hardware/src/ledger/clients/thorchain/common.ts +++ /dev/null @@ -1,93 +0,0 @@ -export const CLA = 0x55; -export const CHUNK_SIZE = 250; -export const APP_KEY = "CSM"; - -export const INS = { - GET_ADDR_SECP256K1: 0x04, - GET_VERSION: 0x00, - INS_PUBLIC_KEY_SECP256K1: 0x01, // Obsolete - SIGN_SECP256K1: 0x02, -}; - -export const PAYLOAD_TYPE = { ADD: 0x01, INIT: 0x00, LAST: 0x02 }; - -export const P1_VALUES = { ONLY_RETRIEVE: 0x00, SHOW_ADDRESS_IN_DEVICE: 0x01 }; - -export const P2_VALUES = { JSON: 0x0 }; - -export const ERROR_CODE = { NoError: 0x9000 }; - -const ERROR_DESCRIPTION: any = { - 1: "U2F: Unknown", - 2: "U2F: Bad request", - 3: "U2F: Configuration unsupported", - 4: "U2F: Device Ineligible", - 5: "U2F: Timeout", - 14: "Timeout", - 25600: "Execution Error", - 26368: "Wrong Length", - 26626: "Error deriving keys", - 27010: "Empty Buffer", - 27011: "Output buffer too small", - 27012: "Data is invalid", - 27013: "Conditions not satisfied", - 27014: "Transaction rejected", - 27264: "Bad key handle", - 27392: "Invalid P1/P2", - 27904: "Instruction not supported", - 28160: "App does not seem to be open", - 28416: "Unknown error", - 28417: "Sign/verify error", - 36864: "No errors", - 36865: "Device is busy", -}; - -export function errorCodeToString(statusCode: number) { - if (statusCode in ERROR_DESCRIPTION) return ERROR_DESCRIPTION[statusCode]; - return `Unknown Status Code: ${statusCode}`; -} - -function isDict(v: any) { - return typeof v === "object" && v !== null && !Array.isArray(v) && !(v instanceof Date); -} - -export function processErrorResponse(response: any) { - if (response) { - if (isDict(response)) { - if (Object.hasOwn(response, "statusCode")) { - return { error_message: errorCodeToString(response.statusCode), return_code: response.statusCode }; - } - - if (Object.hasOwn(response, "return_code") && Object.hasOwn(response, "error_message")) { - return response; - } - } - return { error_message: response.toString(), return_code: 0xffff }; - } - - return { error_message: response.toString(), return_code: 0xffff }; -} - -export function getVersion(transport: any) { - return transport.send(CLA, INS.GET_VERSION, 0, 0).then((response: any) => { - const errorCodeData = response.slice(-2); - const returnCode = errorCodeData[0] * 256 + errorCodeData[1]; - - let targetId = 0; - if (response.length >= 9) { - targetId = (response[5] << 24) + (response[6] << 16) + (response[7] << 8) + (response[8] << 0); - } - - return { - device_locked: response[4] === 1, - error_message: errorCodeToString(returnCode), - major: response[1], - minor: response[2], - patch: response[3], - return_code: returnCode, - target_id: targetId.toString(16), - // /// - test_mode: response[0] !== 0, - }; - }, processErrorResponse); -} diff --git a/packages/wallet-hardware/src/ledger/clients/thorchain/helpers.ts b/packages/wallet-hardware/src/ledger/clients/thorchain/helpers.ts deleted file mode 100644 index c81d777..0000000 --- a/packages/wallet-hardware/src/ledger/clients/thorchain/helpers.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { SwapKitError } from "@swapkit/helpers"; -import { CLA, ERROR_CODE, errorCodeToString, INS, P2_VALUES, PAYLOAD_TYPE, processErrorResponse } from "./common"; - -export function serializePathv1(path: number[]) { - if (path == null || path.length < 3) { - throw new SwapKitError("wallet_ledger_invalid_params", { reason: "Path too short" }); - } - if (path.length > 10) { - throw new SwapKitError("wallet_ledger_invalid_params", { reason: "Path too long" }); - } - const buf = Buffer.alloc(1 + 4 * path.length); - buf.writeUInt8(path.length, 0); - for (let i = 0; i < path.length; i += 1) { - let v = path[i] || 0; - if (i < 3) { - // eslint-disable-next-line no-bitwise - v |= 0x80000000; // Harden - } - buf.writeInt32LE(v, 1 + i * 4); - } - return buf; -} - -export function signSendChunkv1(app: any, chunkIdx: number, _chunkNum: number, chunk: Buffer, txType = P2_VALUES.JSON) { - return app.transport - .send(CLA, INS.SIGN_SECP256K1, chunkIdx, txType, chunk, [ERROR_CODE.NoError, 0x6984, 0x6a80]) - .then((response: any) => { - const errorCodeData = response.slice(-2); - const returnCode = errorCodeData[0] * 256 + errorCodeData[1]; - let errorMessage = errorCodeToString(returnCode); - - if (returnCode === 0x6a80 || returnCode === 0x6984) { - errorMessage = `${errorMessage} : ${response.slice(0, response.length - 2).toString("ascii")}`; - } - - let signature: any = null; - if (response.length > 2) { - signature = response.slice(0, response.length - 2); - } - - return { error_message: errorMessage, return_code: returnCode, signature }; - }, processErrorResponse); -} - -function compressPublicKey(publicKey: Buffer) { - if (publicKey.length !== 65) { - throw new SwapKitError("wallet_ledger_invalid_params", { - reason: "decompressed public key length should be 65 bytes", - }); - } - const y = publicKey.slice(33, 65); - - // @ts-expect-error - const z = Buffer.from([2 + (y[y.length - 1] & 1)]); - return Buffer.concat([z, publicKey.slice(1, 33)]); -} - -export function publicKeyv1(app: any, data: Buffer) { - return app.transport - .send(CLA, INS.INS_PUBLIC_KEY_SECP256K1, 0, 0, data, [ERROR_CODE.NoError]) - .then((response: any) => { - const errorCodeData = response.slice(-2); - const returnCode = errorCodeData[0] * 256 + errorCodeData[1]; - const pk = Buffer.from(response.slice(0, 65)); - - return { - compressed_pk: compressPublicKey(pk), - error_message: errorCodeToString(returnCode), - pk, - return_code: returnCode, - }; - }, processErrorResponse); -} - -export function serializePathv2(path: number[]) { - if (!path || path.length !== 5) { - throw new SwapKitError("wallet_ledger_invalid_params", { reason: "Path must be exactly 5 elements" }); - } - - const buf = Buffer.alloc(20); - // @ts-expect-error - buf.writeUInt32LE(0x80000000 + path[0], 0); - // @ts-expect-error - buf.writeUInt32LE(0x80000000 + path[1], 4); - // @ts-expect-error - buf.writeUInt32LE(0x80000000 + path[2], 8); - // @ts-expect-error - buf.writeUInt32LE(path[3], 12); - // @ts-expect-error - buf.writeUInt32LE(path[4], 16); - - return buf; -} - -export function signSendChunkv2(app: any, chunkIdx: number, chunkNum: number, chunk: Buffer, txType = P2_VALUES.JSON) { - let payloadType = PAYLOAD_TYPE.ADD; - if (chunkIdx === 1) { - payloadType = PAYLOAD_TYPE.INIT; - } - if (chunkIdx === chunkNum) { - payloadType = PAYLOAD_TYPE.LAST; - } - - return signSendChunkv1(app, payloadType, 0, chunk, txType); -} - -export function publicKeyv2(app: any, data: Buffer) { - return app.transport.send(CLA, INS.GET_ADDR_SECP256K1, 0, 0, data, [ERROR_CODE.NoError]).then((response: any) => { - const errorCodeData = response.slice(-2); - const returnCode = errorCodeData[0] * 256 + errorCodeData[1]; - const compressedPk = Buffer.from(response.slice(0, 33)); - - return { - compressed_pk: compressedPk, - error_message: errorCodeToString(returnCode), - pk: "OBSOLETE PROPERTY", - return_code: returnCode, - }; - }, processErrorResponse); -} diff --git a/packages/wallet-hardware/src/ledger/clients/thorchain/index.ts b/packages/wallet-hardware/src/ledger/clients/thorchain/index.ts index cfa3e83..6061216 100644 --- a/packages/wallet-hardware/src/ledger/clients/thorchain/index.ts +++ b/packages/wallet-hardware/src/ledger/clients/thorchain/index.ts @@ -1,11 +1,34 @@ import type { AccountData, AminoSignResponse, StdSignDoc } from "@cosmjs/amino"; +import { Secp256k1Signature } from "@cosmjs/crypto"; +import type { Command, DmkError, UserInteractionRequired } from "@ledgerhq/device-management-kit"; import type Transport from "@ledgerhq/hw-transport"; import { base64 } from "@scure/base"; -import { type DerivationPathArray, NetworkDerivationPath, SwapKitError } from "@swapkit/helpers"; - -import { CosmosLedgerInterface } from "../../interfaces/CosmosLedgerInterface"; -import type { GetAddressAndPubKeyResponse } from "../../types"; -import { getSignature } from "./utils"; +import { type DerivationPathArray, NetworkDerivationPath, SKConfig, SwapKitError } from "@swapkit/helpers"; + +import type { LedgerDMKSession } from "../../helpers/dmk"; +import { + executeLedgerDeviceAction, + LEDGER_USER_INTERACTION_REQUIRED, + type LedgerDeviceActionStateHandler, +} from "../../helpers/executeDeviceAction"; +import { + getThorAddressCommand, + getThorLegacyVersion, + getThorSignCommands, + invalidThorAppVersionMessage, + parseThorAddressResponse, + sendThorLegacyCommand, + type ThorCommand, +} from "./protocol"; + +const THOR_APP_NAME = "THORChain"; + +interface THORChainLedgerParams { + derivationPath?: DerivationPathArray; + dmkSession?: LedgerDMKSession; + onDeviceActionState?: LedgerDeviceActionStateHandler; + transport?: Transport; +} type ThorchainAssetObject = { chain?: string; symbol?: string; synth?: boolean; ticker?: string }; @@ -18,9 +41,7 @@ function getAminoAssetDenom(asset: string | ThorchainAssetObject) { const symbol = (asset.symbol || asset.ticker || "").toUpperCase(); if (!(chain && symbol)) return symbol || chain || ""; - if (asset.synth) return `${chain}/${symbol}`; - - return `${chain}.${symbol}`; + return `${chain}${asset.synth ? "/" : "."}${symbol}`; } export function normalizeThorchainLedgerSignDoc(signDoc: StdSignDoc): StdSignDoc { @@ -43,123 +64,214 @@ export function normalizeThorchainLedgerSignDoc(signDoc: StdSignDoc): StdSignDoc }; } -export class THORChainLedger extends CosmosLedgerInterface { - private pubKey: string | null = null; +function getFixedSignature({ signature }: { signature: Uint8Array }) { + try { + return Secp256k1Signature.fromDer(signature).toFixedLength(); + } catch (error) { + throw new SwapKitError("wallet_ledger_invalid_signature", error); + } +} + +export class THORChainLedger { + readonly derivationPath: DerivationPathArray; + private readonly dmkSession?: LedgerDMKSession; + private readonly onDeviceActionState?: LedgerDeviceActionStateHandler; + private pubKey: Uint8Array | undefined; + private readonly transport?: Transport; + + constructor({ + derivationPath = NetworkDerivationPath.THOR, + dmkSession, + onDeviceActionState, + transport, + }: THORChainLedgerParams = {}) { + if (dmkSession && transport) { + throw new SwapKitError("wallet_ledger_invalid_params", { + message: "Provide either a Ledger DMK session or a LedgerJS transport, not both", + }); + } - derivationPath: DerivationPathArray; + if (!(dmkSession || transport)) throw new SwapKitError("wallet_ledger_connection_error"); - constructor(derivationPath: DerivationPathArray = NetworkDerivationPath.THOR, transport?: Transport) { - super(transport); - this.chain = "thor"; this.derivationPath = derivationPath; + this.dmkSession = dmkSession; + this.onDeviceActionState = onDeviceActionState; + this.transport = transport; } get pubkey() { - return this.pubKey; + return this.pubKey ? base64.encode(this.pubKey) : null; } - connect = async () => { - await this.checkOrCreateTransportAndLedger(); - const { compressed_pk, bech32_address }: GetAddressAndPubKeyResponse = await this.getAddressAndPubKey(); + private executeCommands = async ({ + commands, + requiredUserInteraction, + }: { + commands: ThorCommand[]; + requiredUserInteraction: UserInteractionRequired; + }) => { + if (this.transport) { + const version = await getThorLegacyVersion({ transport: this.transport }); + if (!version.startsWith("2.")) { + const { InvalidResponseFormatError } = await import("@ledgerhq/device-management-kit"); + throw new InvalidResponseFormatError(invalidThorAppVersionMessage({ version })); + } + + let response = new Uint8Array(); + for (const command of commands) { + response = await sendThorLegacyCommand({ command, transport: this.transport }); + } + return response; + } - this.pubKey = base64.encode(compressed_pk); + const dmkSession = this.dmkSession; + if (!dmkSession) throw new SwapKitError("wallet_ledger_connection_error"); + + const { + Apdu, + CallTaskInAppDeviceAction, + CommandResultFactory, + DmkResultFactory, + InvalidResponseFormatError, + InvalidStatusWordError, + isSuccessCommandResult, + UnknownDeviceExchangeError, + } = await import("@ledgerhq/device-management-kit"); + const toDmkCommand = (command: ThorCommand): Command => { + const apdu = command.getApdu(); - return bech32_address; - }; + return { + getApdu: () => new Apdu(apdu.cla, apdu.ins, apdu.p1, apdu.p2, apdu.data), + name: command.name, + parseResponse: (response) => { + try { + return CommandResultFactory({ data: command.parseResponse(response) }); + } catch (error) { + return CommandResultFactory({ + error: new InvalidStatusWordError(error instanceof Error ? error.message : String(error)), + }); + } + }, + }; + }; - getAddressAndPubKey = async () => { - await this.checkOrCreateTransportAndLedger(true); + const deviceAction = new CallTaskInAppDeviceAction<{ response: Uint8Array }, DmkError, UserInteractionRequired>({ + input: { + appName: THOR_APP_NAME, + requiredUserInteraction, + skipOpenApp: false, + task: async (internalApi) => { + try { + const sessionState = internalApi.getDeviceSessionState(); + const version = "currentApp" in sessionState ? sessionState.currentApp.version : undefined; + if (!version?.startsWith("2.")) { + return DmkResultFactory<{ response: Uint8Array }, DmkError>({ + error: new InvalidResponseFormatError(invalidThorAppVersionMessage({ version })), + }); + } + + let response: Uint8Array = new Uint8Array(); + for (const command of commands) { + const result = await internalApi.sendCommand(toDmkCommand(command)); + if (!isSuccessCommandResult(result)) { + return DmkResultFactory<{ response: Uint8Array }, DmkError>({ error: result.error }); + } + response = result.data; + } + + return DmkResultFactory<{ response: Uint8Array }, DmkError>({ data: { response } }); + } catch (error) { + return DmkResultFactory<{ response: Uint8Array }, DmkError>({ + error: new UnknownDeviceExchangeError(error), + }); + } + }, + }, + }); - const response: GetAddressAndPubKeyResponse = await this.ledgerApp.getAddressAndPubKey( - this.derivationPath, - this.chain, - ); + const result = await executeLedgerDeviceAction({ + action: dmkSession.dmk.executeDeviceAction({ deviceAction, sessionId: dmkSession.sessionId }), + onDeviceActionState: this.onDeviceActionState, + }); - this.validateResponse(response.return_code, response.error_message); + return result.response; + }; - return response; + private getAddressData = async ({ checkOnDevice }: { checkOnDevice: boolean }) => { + const { isStagenet } = SKConfig.get("envs"); + const response = await this.executeCommands({ + commands: [ + getThorAddressCommand({ checkOnDevice, hrp: isStagenet ? "sthor" : "thor", path: this.derivationPath }), + ], + requiredUserInteraction: checkOnDevice + ? LEDGER_USER_INTERACTION_REQUIRED.VerifyAddress + : LEDGER_USER_INTERACTION_REQUIRED.None, + }); + + return parseThorAddressResponse({ response }); }; - showAddressAndPubKey = async () => { - await this.checkOrCreateTransportAndLedger(true); + private signBytes = async ({ message }: { message: Uint8Array }) => { + const response = await this.executeCommands({ + commands: getThorSignCommands({ message, path: this.derivationPath }), + requiredUserInteraction: LEDGER_USER_INTERACTION_REQUIRED.SignTransaction, + }); - const response: GetAddressAndPubKeyResponse = await this.ledgerApp.showAddressAndPubKey( - this.derivationPath, - this.chain, - ); + return getFixedSignature({ signature: response }); + }; - this.validateResponse(response.return_code, response.error_message); + connect = async () => { + const { address, publicKey } = await this.getAddressData({ checkOnDevice: false }); + this.pubKey = publicKey; + return address; + }; + + getAddressAndPubKey = async () => { + const { address, publicKey } = await this.getAddressData({ checkOnDevice: false }); + this.pubKey = publicKey; - return response; + return { bech32_address: address, compressed_pk: publicKey, error_message: "No errors", return_code: 0x9000 }; }; - signTransaction = async (rawTx: string, sequence = "0") => { - await this.checkOrCreateTransportAndLedger(true); + showAddressAndPubKey = async () => { + const { address, publicKey } = await this.getAddressData({ checkOnDevice: true }); + this.pubKey = publicKey; - const { return_code, error_message, signature } = await this.ledgerApp.sign(this.derivationPath, rawTx); + return { bech32_address: address, compressed_pk: publicKey, error_message: "No errors", return_code: 0x9000 }; + }; - if (!this.pubKey) throw new SwapKitError("wallet_ledger_pubkey_not_found"); + signTransaction = async ({ rawTx, sequence = "0" }: { rawTx: string; sequence?: string }) => { + if (!this.pubKey) await this.connect(); + const pubKey = this.pubKey; + if (!pubKey) throw new SwapKitError("wallet_ledger_pubkey_not_found"); - this.validateResponse(return_code, error_message); + const signature = await this.signBytes({ message: new TextEncoder().encode(rawTx) }); return [ { - pub_key: { type: "tendermint/PubKeySecp256k1", value: this.pubKey }, + pub_key: { type: "tendermint/PubKeySecp256k1", value: base64.encode(pubKey) }, sequence, - signature: getSignature(signature), + signature: base64.encode(signature), }, ]; }; signAmino = async (signerAddress: string, signDoc: StdSignDoc): Promise => { - await this.checkOrCreateTransportAndLedger(true); + const account = (await this.getAccounts()).find(({ address }) => address === signerAddress); + if (!account) throw new SwapKitError("wallet_ledger_address_not_found", { address: signerAddress }); - const account = (await this.getAccounts()).find((item) => item.address === signerAddress); - if (!account) { - throw new SwapKitError("wallet_ledger_address_not_found", { address: signerAddress }); - } + const { encodeSecp256k1Signature, serializeSignDoc } = await import("@cosmjs/amino"); - const importedAmino = await import("@cosmjs/amino"); - const encodeSecp256k1Signature = - importedAmino.encodeSecp256k1Signature ?? importedAmino.default?.encodeSecp256k1Signature; - const serializeSignDoc = importedAmino.serializeSignDoc ?? importedAmino.default?.serializeSignDoc; - const normalizedSignDoc = normalizeThorchainLedgerSignDoc(signDoc); - - const { return_code, error_message, signature } = await this.ledgerApp.sign( - this.derivationPath, - serializeSignDoc(normalizedSignDoc), - ); - - this.validateResponse(return_code, error_message); - - return { - signature: encodeSecp256k1Signature(account.pubkey, base64.decode(getSignature(signature))), - // CosmJS rebuilds bodyBytes from the returned `signed` doc. Keep the - // original direct-shaped doc there so MsgDeposit protobuf assets do not - // get round-tripped through the incomplete Amino converter. - signed: signDoc, - }; + const signature = await this.signBytes({ message: serializeSignDoc(normalizeThorchainLedgerSignDoc(signDoc)) }); + + return { signature: encodeSecp256k1Signature(account.pubkey, signature), signed: signDoc }; }; getAccounts = async (): Promise => { - await this.checkOrCreateTransportAndLedger(true); - - const { bech32_address, compressed_pk }: GetAddressAndPubKeyResponse = await this.getAddressAndPubKey(); - - this.pubKey = base64.encode(compressed_pk); - - return [{ address: bech32_address, algo: "secp256k1", pubkey: compressed_pk }]; + const { address, publicKey } = await this.getAddressData({ checkOnDevice: false }); + this.pubKey = publicKey; + return [{ address, algo: "secp256k1", pubkey: publicKey }]; }; - sign = async (message: string) => { - await this.checkOrCreateTransportAndLedger(true); - - const { return_code, error_message, signature } = await this.ledgerApp.sign(this.derivationPath, message); - - if (!this.pubKey) throw new SwapKitError("wallet_ledger_pubkey_not_found"); - - this.validateResponse(return_code, error_message); - - return getSignature(signature); - }; + sign = async (message: string) => base64.encode(await this.signBytes({ message: new TextEncoder().encode(message) })); } diff --git a/packages/wallet-hardware/src/ledger/clients/thorchain/lib.ts b/packages/wallet-hardware/src/ledger/clients/thorchain/lib.ts deleted file mode 100644 index f3fbb81..0000000 --- a/packages/wallet-hardware/src/ledger/clients/thorchain/lib.ts +++ /dev/null @@ -1,258 +0,0 @@ -import type Transport from "@ledgerhq/hw-transport"; -import { SwapKitError } from "@swapkit/helpers"; - -import { - CHUNK_SIZE, - CLA, - ERROR_CODE, - errorCodeToString, - getVersion, - INS, - P1_VALUES, - P2_VALUES, - processErrorResponse, -} from "./common"; -import { - publicKeyv1, - publicKeyv2, - serializePathv1, - serializePathv2, - signSendChunkv1, - signSendChunkv2, -} from "./helpers"; - -export class THORChainApp { - transport: Transport; - versionResponse: any; - - constructor(transport: any) { - if (!transport) { - throw new SwapKitError("wallet_ledger_transport_not_defined"); - } - - this.transport = transport; - } - - static serializeHRP(hrp: string) { - if (hrp == null || hrp.length < 3 || hrp.length > 83) { - throw new SwapKitError("wallet_ledger_invalid_params", { reason: "Invalid HRP" }); - } - const buf = Buffer.alloc(1 + hrp.length); - buf.writeUInt8(hrp.length, 0); - buf.write(hrp, 1); - return buf; - } - - async serializePath(path: number[]) { - this.versionResponse = await getVersion(this.transport); - - if (this.versionResponse.return_code !== ERROR_CODE.NoError) { - throw this.versionResponse; - } - - switch (this.versionResponse.major) { - case 1: - return serializePathv1(path); - case 2: - return serializePathv2(path); - default: - return Buffer.alloc(0); - } - } - - async signGetChunks(path: number[], buffer: Buffer) { - const serializedPath = await this.serializePath(path); - - const chunks = []; - chunks.push(serializedPath); - - for (let i = 0; i < buffer.length; i += CHUNK_SIZE) { - let end = i + CHUNK_SIZE; - if (i > buffer.length) { - end = buffer.length; - } - chunks.push(buffer.slice(i, end)); - } - - return chunks; - } - - async getVersion() { - try { - this.versionResponse = await getVersion(this.transport); - return this.versionResponse; - } catch (e) { - return processErrorResponse(e); - } - } - - appInfo() { - return this.transport.send(0xb0, 0x01, 0, 0).then((response: any) => { - const returnCode = response.readUInt16BE(response.length - 2); - - let appName = ""; - let appVersion = ""; - let flagLen = 0; - let flagsValue = 0; - - if (response[0] !== 1) { - // Ledger responds with format ID 1. There is no spec for any format != 1 - return { error_message: "response format ID not recognized", return_code: 0x9001 }; - } - - const appNameLen = response[1]; - appName = response.slice(2, 2 + appNameLen).toString("ascii"); - let idx = 2 + appNameLen; - const appVersionLen = response[idx]; - idx += 1; - appVersion = response.slice(idx, idx + appVersionLen).toString("ascii"); - idx += appVersionLen; - const appFlagsLen = response[idx]; - idx += 1; - flagLen = appFlagsLen; - flagsValue = response[idx]; - - return { - appName, - appVersion, - error_message: errorCodeToString(returnCode), - flag_onboarded: (flagsValue & 4) !== 0, - flag_pin_validated: (flagsValue & 128) !== 0, - flag_recovery: (flagsValue & 1) !== 0, - flag_signed_mcu_code: (flagsValue & 2) !== 0, - flagLen, - flagsValue, - return_code: returnCode, - }; - }, processErrorResponse); - } - - deviceInfo() { - return this.transport - .send(0xe0, 0x01, 0, 0, Buffer.from([]), [ERROR_CODE.NoError, 0x6e00]) - .then((response: any) => { - const errorCodeData = response.slice(-2); - const returnCode = errorCodeData[0] * 256 + errorCodeData[1]; - - if (returnCode === 0x6e00) { - return { error_message: "This command is only available in the Dashboard", return_code: returnCode }; - } - - const targetId = response.slice(0, 4).toString("hex"); - - let pos = 4; - const secureElementVersionLen = response[pos]; - pos += 1; - const seVersion = response.slice(pos, pos + secureElementVersionLen).toString(); - pos += secureElementVersionLen; - - const flagsLen = response[pos]; - pos += 1; - const flag = response.slice(pos, pos + flagsLen).toString("hex"); - pos += flagsLen; - - const mcuVersionLen = response[pos]; - pos += 1; - // Patch issue in mcu version - let tmp = response.slice(pos, pos + mcuVersionLen); - if (tmp[mcuVersionLen - 1] === 0) { - tmp = response.slice(pos, pos + mcuVersionLen - 1); - } - const mcuVersion = tmp.toString(); - - return { - error_message: errorCodeToString(returnCode), - flag, - mcuVersion, - return_code: returnCode, - seVersion, - // // - targetId, - }; - }, processErrorResponse); - } - - async publicKey(path: number[]) { - try { - const serializedPath = await this.serializePath(path); - - switch (this.versionResponse.major) { - case 1: - return publicKeyv1(this, serializedPath); - case 2: { - const data = Buffer.concat([THORChainApp.serializeHRP("thor"), serializedPath]); - return publicKeyv2(this, data); - } - default: - return { error_message: "App Version is not supported", return_code: 0x6400 }; - } - } catch (e) { - return processErrorResponse(e); - } - } - - async getAddressAndPubKey(path: number[], hrp: string, showInDevice = false) { - try { - const serializedPath = await this.serializePath(path); - const data = Buffer.concat([THORChainApp.serializeHRP(hrp), serializedPath]); - const response = await this.transport.send( - CLA, - INS.GET_ADDR_SECP256K1, - showInDevice ? P1_VALUES.SHOW_ADDRESS_IN_DEVICE : P1_VALUES.ONLY_RETRIEVE, - 0, - data, - [ERROR_CODE.NoError], - ); - - const compressedPk = Buffer.from(response.slice(0, 33)); - const bech32Address = Buffer.from(response.slice(33, -2)).toString(); - const returnCode = response.readUInt16BE(response.length - 2); - - return { - bech32_address: bech32Address, - compressed_pk: compressedPk, - error_message: errorCodeToString(returnCode), - return_code: returnCode, - }; - } catch (err) { - return processErrorResponse(err); - } - } - - showAddressAndPubKey(path: number[], hrp: string) { - return this.getAddressAndPubKey(path, hrp, true); - } - - signSendChunk(chunkIdx: number, chunkNum: number, chunk: Buffer, txType = P2_VALUES.JSON) { - switch (this.versionResponse.major) { - case 1: - return signSendChunkv1(this, chunkIdx, chunkNum, chunk, txType); - case 2: - return signSendChunkv2(this, chunkIdx, chunkNum, chunk, txType); - default: - return { error_message: "App Version is not supported", return_code: 0x6400 }; - } - } - - async sign(path: number[], message: string, txType = P2_VALUES.JSON) { - const buffer = Buffer.from(message); - let chunks: Buffer[] = []; - let response: any; - try { - chunks = await this.signGetChunks(path, buffer); - response = await this.signSendChunk(1, chunks.length, chunks[0] as Buffer, txType); - } catch (error) { - processErrorResponse(error); - } - let result = { error_message: response.error_message, return_code: response.return_code, signature: null }; - - for (let i = 1; i < chunks.length; i += 1) { - result = await this.signSendChunk(1 + i, chunks.length, chunks[i] as Buffer, txType); - if (result.return_code !== ERROR_CODE.NoError) { - break; - } - } - - return { error_message: result.error_message, return_code: result.return_code, signature: result.signature }; - } -} diff --git a/packages/wallet-hardware/src/ledger/clients/thorchain/protocol.ts b/packages/wallet-hardware/src/ledger/clients/thorchain/protocol.ts new file mode 100644 index 0000000..a679be3 --- /dev/null +++ b/packages/wallet-hardware/src/ledger/clients/thorchain/protocol.ts @@ -0,0 +1,158 @@ +import type Transport from "@ledgerhq/hw-transport"; +import { SwapKitError } from "@swapkit/helpers"; + +const CLA = 0x55; +const INS_GET_ADDRESS = 0x04; +const INS_GET_VERSION = 0x00; +const INS_SIGN = 0x02; +const P1_INIT = 0x00; +const P1_ADD = 0x01; +const P1_LAST = 0x02; +const STATUS_OK = 0x9000; +const SIGN_CHUNK_SIZE = 250; + +interface ThorCommandParams { + data?: Uint8Array; + ins: number; + p1?: number; + p2?: number; +} + +interface ThorApduResponse { + data: Uint8Array; + statusCode: Uint8Array; +} + +export class ThorCommand { + readonly name = "THORChainCommand"; + private readonly params: ThorCommandParams; + + constructor(params: ThorCommandParams) { + this.params = params; + } + + getApdu() { + const { data = new Uint8Array(), ins, p1 = 0, p2 = 0 } = this.params; + return { cla: CLA, data, ins, p1, p2 }; + } + + parseResponse(response: ThorApduResponse) { + const status = (response.statusCode[0] ?? 0) * 0x100 + (response.statusCode[1] ?? 0); + if (status !== STATUS_OK) { + throw new Error(`THORChain app returned status 0x${status.toString(16).padStart(4, "0")}`); + } + + return response.data; + } +} + +export function serializeThorPath({ path }: { path: readonly (number | undefined)[] }) { + if (path.length !== 5) { + throw new SwapKitError("wallet_ledger_invalid_params", { reason: "THORChain path must contain five elements" }); + } + + const serialized = new Uint8Array(20); + const view = new DataView(serialized.buffer); + + path.forEach((segment, index) => { + if (typeof segment !== "number" || !Number.isInteger(segment) || segment < 0 || segment > 0xffffffff) { + throw new SwapKitError("wallet_ledger_invalid_params", { reason: `Invalid THORChain path segment ${index}` }); + } + + const normalized = segment & 0x7fffffff; + const value = index < 3 ? (normalized | 0x80000000) >>> 0 : normalized; + view.setUint32(index * 4, value, true); + }); + + return serialized; +} + +export function serializeThorAddressPayload({ hrp, path }: { hrp: string; path: readonly (number | undefined)[] }) { + const encodedHrp = new TextEncoder().encode(hrp); + if (encodedHrp.length < 1 || encodedHrp.length > 83) { + throw new SwapKitError("wallet_ledger_invalid_params", { reason: "Invalid THORChain address prefix" }); + } + + const serializedPath = serializeThorPath({ path }); + return Uint8Array.from([encodedHrp.length, ...encodedHrp, ...serializedPath]); +} + +export function getThorAddressCommand({ + checkOnDevice, + hrp, + path, +}: { + checkOnDevice: boolean; + hrp: string; + path: readonly (number | undefined)[]; +}) { + return new ThorCommand({ + data: serializeThorAddressPayload({ hrp, path }), + ins: INS_GET_ADDRESS, + p1: checkOnDevice ? 1 : 0, + }); +} + +export function getThorSignCommands({ message, path }: { message: Uint8Array; path: readonly (number | undefined)[] }) { + const commands = [new ThorCommand({ data: serializeThorPath({ path }), ins: INS_SIGN, p1: P1_INIT })]; + + if (message.length === 0) { + commands.push(new ThorCommand({ data: new Uint8Array(), ins: INS_SIGN, p1: P1_LAST })); + return commands; + } + + for (let offset = 0; offset < message.length; offset += SIGN_CHUNK_SIZE) { + const end = Math.min(offset + SIGN_CHUNK_SIZE, message.length); + commands.push( + new ThorCommand({ + data: message.slice(offset, end), + ins: INS_SIGN, + p1: end === message.length ? P1_LAST : P1_ADD, + }), + ); + } + + return commands; +} + +function parseTransportResponse({ response }: { response: Uint8Array }) { + if (response.length < 2) { + throw new SwapKitError("wallet_ledger_invalid_response", { reason: "THORChain response is missing status bytes" }); + } + + return response.slice(0, -2); +} + +export async function getThorLegacyVersion({ transport }: { transport: Transport }) { + const response = await transport.send(CLA, INS_GET_VERSION, 0, 0, Buffer.alloc(0), [STATUS_OK]); + const data = parseTransportResponse({ response }); + if (data.length < 4) { + throw new SwapKitError("wallet_ledger_invalid_response", { reason: "Invalid THORChain version response" }); + } + + return `${data[1]}.${data[2]}.${data[3]}`; +} + +export async function sendThorLegacyCommand({ command, transport }: { command: ThorCommand; transport: Transport }) { + const apdu = command.getApdu(); + const response = await transport.send(apdu.cla, apdu.ins, apdu.p1, apdu.p2, Buffer.from(apdu.data), [STATUS_OK]); + return parseTransportResponse({ response }); +} + +export function parseThorAddressResponse({ response }: { response: Uint8Array }) { + if (response.length < 34) { + throw new SwapKitError("wallet_ledger_invalid_response", { reason: "Invalid THORChain address response" }); + } + + const publicKey = response.slice(0, 33); + const address = new TextDecoder().decode(response.slice(33)); + if (!address) { + throw new SwapKitError("wallet_ledger_invalid_response", { reason: "THORChain address is empty" }); + } + + return { address, publicKey }; +} + +export function invalidThorAppVersionMessage({ version }: { version?: string }) { + return `THORChain Ledger app major 2 is required, received ${version || "unknown"}`; +} diff --git a/packages/wallet-hardware/src/ledger/clients/thorchain/utils.ts b/packages/wallet-hardware/src/ledger/clients/thorchain/utils.ts deleted file mode 100644 index d20ffee..0000000 --- a/packages/wallet-hardware/src/ledger/clients/thorchain/utils.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { base64 } from "@scure/base"; -import { SwapKitError } from "@swapkit/helpers"; - -export const getSignature = (signatureArray: any) => { - // Check Type Length Value encoding - if (signatureArray.length < 64) { - throw new SwapKitError("wallet_ledger_invalid_signature", { reason: "Too short" }); - } - if (signatureArray[0] !== 0x30) { - throw new SwapKitError("wallet_ledger_invalid_signature", { reason: "TLV encoding: expected first byte 0x30" }); - } - if (signatureArray[1] + 2 !== signatureArray.length) { - throw new SwapKitError("wallet_ledger_invalid_signature", { reason: "signature length does not match TLV" }); - } - if (signatureArray[2] !== 0x02) { - throw new SwapKitError("wallet_ledger_invalid_signature", { reason: "TLV encoding: expected length type 0x02" }); - } - - // r signature - const rLength = signatureArray[3]; - let rSignature = signatureArray.slice(4, rLength + 4); - - // Drop leading zero on some 'r' signatures that are 33 bytes. - if (rSignature.length === 33 && rSignature[0] === 0) { - rSignature = rSignature.slice(1, 33); - } else if (rSignature.length === 33) { - throw new SwapKitError("wallet_ledger_invalid_signature", { reason: "r too long" }); - } - - // add leading zero's to pad to 32 bytes - while (rSignature.length < 32) { - rSignature.unshift(0); - } - - // s signature - if (signatureArray[rLength + 4] !== 0x02) { - throw new SwapKitError("wallet_ledger_invalid_signature", { - reason: "TLV encoding: expected length type 0x02 for s", - }); - } - - const sLength = signatureArray[rLength + 5]; - - if (4 + rLength + 2 + sLength !== signatureArray.length) { - throw new SwapKitError("wallet_ledger_invalid_signature", { - reason: "TLV byte lengths do not match message length", - }); - } - - let sSignature = signatureArray.slice(rLength + 6, signatureArray.length); - - // Drop leading zero on 's' signatures that are 33 bytes. This shouldn't occur since ledger signs using "Small s" math. But just to be sure... - if (sSignature.length === 33 && sSignature[0] === 0) { - sSignature = sSignature.slice(1, 33); - } else if (sSignature.length === 33) { - throw new SwapKitError("wallet_ledger_invalid_signature", { reason: "s too long" }); - } - - // add leading zero's to pad to 32 bytes - while (sSignature.length < 32) { - sSignature.unshift(0); - } - - if (rSignature.length !== 32 || sSignature.length !== 32) { - throw new SwapKitError("wallet_ledger_invalid_signature", { reason: "must be 32 bytes each" }); - } - - return base64.encode(Buffer.concat([rSignature, sSignature])); -}; diff --git a/packages/wallet-hardware/src/ledger/clients/tron.ts b/packages/wallet-hardware/src/ledger/clients/tron.ts index 09dfc61..5fa802a 100644 --- a/packages/wallet-hardware/src/ledger/clients/tron.ts +++ b/packages/wallet-hardware/src/ledger/clients/tron.ts @@ -1,3 +1,4 @@ +import type { UserInteractionRequired } from "@ledgerhq/device-management-kit"; import type TronApp from "@ledgerhq/hw-app-trx"; import type Transport from "@ledgerhq/hw-transport"; import { @@ -8,73 +9,82 @@ import { } from "@swapkit/helpers"; import type { TronSignedTransaction, TronSigner, TronTransaction } from "@swapkit/toolboxes/tron"; -import { getLedgerTransport } from "../helpers/getLedgerTransport"; +import { + LEDGER_USER_INTERACTION_REQUIRED, + type LedgerJsClientParams, + normalizeLedgerJsClientParams, + runLedgerJsOperation, +} from "../helpers/ledgerJsDmkBridge"; + +type TronLedgerParams = LedgerJsClientParams; export class TronLedgerInterface implements TronSigner { - derivationPath: string; - ledgerApp: InstanceType | null = null; - ledgerTimeout = 50000; - private readonly injectedTransport?: Transport; + readonly derivationPath: string; + readonly ledgerTimeout = 50000; + private readonly connection: Omit; - constructor(derivationPath?: DerivationPathArray | string, transport?: Transport) { + constructor(paramsOrPath?: TronLedgerParams | DerivationPathArray | string, transport?: Transport) { + const { derivationPath, ...connection } = normalizeLedgerJsClientParams({ paramsOrPath, transport }); this.derivationPath = typeof derivationPath === "string" ? derivationPath : derivationPathToString(derivationPath || NetworkDerivationPath.TRON); - this.injectedTransport = transport; + this.connection = connection; + } + + private async runTronOperation({ + operation, + requiredUserInteraction, + }: { + operation: (app: InstanceType) => Promise; + requiredUserInteraction?: UserInteractionRequired; + }) { + const TronApp = (await import("@ledgerhq/hw-app-trx")).default; + return runLedgerJsOperation({ + appName: "Tron", + connection: this.connection, + createApp: (ledgerTransport) => new TronApp(ledgerTransport), + operation, + requiredUserInteraction, + }); } checkOrCreateTransportAndLedger = async () => { - if (this.ledgerApp) return; - await this.createTransportAndLedger(); + await this.runTronOperation({ operation: async () => true }); }; createTransportAndLedger = async () => { - const transport = this.injectedTransport ?? (await getLedgerTransport()); - const TronApp = (await import("@ledgerhq/hw-app-trx")).default; - - this.ledgerApp = new TronApp(transport); + await this.runTronOperation({ operation: async () => true }); }; - getAddress = async (): Promise => { + getAddress = async () => { const response = await this.getAddressAndPubKey(); if (!response) throw new SwapKitError("wallet_ledger_failed_to_get_address"); return response.address; }; getAddressAndPubKey = async () => { - await this.createTransportAndLedger(); - const result = await this.ledgerApp?.getAddress(this.derivationPath); - + const result = await this.runTronOperation({ operation: (app) => app.getAddress(this.derivationPath) }); if (!result) throw new SwapKitError("wallet_ledger_failed_to_get_address"); return { address: result.address, publicKey: result.publicKey }; }; showAddressAndPubKey = async () => { - await this.createTransportAndLedger(); - return this.ledgerApp?.getAddress(this.derivationPath, true); + return await this.runTronOperation({ + operation: (app) => app.getAddress(this.derivationPath, true), + requiredUserInteraction: LEDGER_USER_INTERACTION_REQUIRED.VerifyAddress, + }); }; signTransaction = async (transaction: TronTransaction): Promise => { - await this.createTransportAndLedger(); - - if (!this.ledgerApp) { - throw new SwapKitError("wallet_ledger_transport_error"); - } - try { - const signature = await this.ledgerApp.signTransaction( - this.derivationPath, - transaction.raw_data_hex, - [], // Token signatures array - empty for native TRX transfers - ); - - if (!signature) { - throw new SwapKitError("wallet_ledger_signing_error"); - } + const signature = await this.runTronOperation({ + operation: (app) => app.signTransaction(this.derivationPath, transaction.raw_data_hex, []), + requiredUserInteraction: LEDGER_USER_INTERACTION_REQUIRED.SignTransaction, + }); - // Return the signed transaction in Tron's expected format + if (!signature) throw new SwapKitError("wallet_ledger_signing_error"); return { ...transaction, signature: [signature] }; } catch (error) { throw new SwapKitError("wallet_ledger_signing_error", { error }); @@ -82,5 +92,6 @@ export class TronLedgerInterface implements TronSigner { }; } -export const TronLedger = (derivationPath?: DerivationPathArray, transport?: Transport) => - new TronLedgerInterface(derivationPath, transport); +export function TronLedger(paramsOrPath?: TronLedgerParams | DerivationPathArray, transport?: Transport) { + return new TronLedgerInterface(paramsOrPath, transport); +} diff --git a/packages/wallet-hardware/src/ledger/clients/utxo-legacy-adapter.ts b/packages/wallet-hardware/src/ledger/clients/utxo-legacy-adapter.ts index 49e11a5..0add774 100644 --- a/packages/wallet-hardware/src/ledger/clients/utxo-legacy-adapter.ts +++ b/packages/wallet-hardware/src/ledger/clients/utxo-legacy-adapter.ts @@ -2,6 +2,7 @@ import { hex } from "@scure/base"; import { SwapKitError, type UTXOChain } from "@swapkit/helpers"; import type { UTXOType } from "@swapkit/toolboxes/utxo"; import type { Transaction } from "@swapkit/utxo-signer"; +import { createCachedRawTxResolver } from "../helpers/rawTx"; /** * Extract per-input metadata from a V3 PSBT in the shape the legacy @@ -16,50 +17,51 @@ import type { Transaction } from "@swapkit/utxo-signer"; export async function extractInputsFromPsbt(tx: Transaction, chain: UTXOChain): Promise { const { RawTx } = await import("@swapkit/utxo-signer"); const { getUtxoApi } = await import("@swapkit/toolboxes/utxo"); - const inputs: UTXOType[] = []; + const utxoApi = getUtxoApi(chain); + const getRawTx = createCachedRawTxResolver((txid) => utxoApi.getRawTx(txid)); - for (let i = 0; i < tx.inputsLength; i++) { - const input = tx.getInput(i); + return Promise.all( + Array.from({ length: tx.inputsLength }, async (_, inputIndex) => { + const input = tx.getInput(inputIndex); - if (!input.txid || input.index === undefined) { - throw new Error(`PSBT input ${i} is missing txid/index`); - } + if (!input.txid || input.index === undefined) { + throw new Error(`PSBT input ${inputIndex} is missing txid/index`); + } - const txid = hex.encode(input.txid); - const txHex = input.nonWitnessUtxo - ? hex.encode(RawTx.encode(input.nonWitnessUtxo)) - : await getUtxoApi(chain).getRawTx(txid); - if (!txHex) { - throw new SwapKitError("wallet_ledger_invalid_params", { - chain, - inputIndex: i, - reason: "Unable to resolve previous transaction hex for Ledger signing", - txid, - }); - } - const witnessUtxo = input.witnessUtxo - ? { script: input.witnessUtxo.script, value: Number(input.witnessUtxo.amount) } - : undefined; - const nonWitnessPrevout = input.index !== undefined ? input.nonWitnessUtxo?.outputs?.[input.index] : undefined; - const value = witnessUtxo?.value ?? (nonWitnessPrevout ? Number(nonWitnessPrevout.amount) : 0); + const txid = hex.encode(input.txid); + const txHex = input.nonWitnessUtxo ? hex.encode(RawTx.encode(input.nonWitnessUtxo)) : await getRawTx(txid); + if (!txHex) { + throw new SwapKitError("wallet_ledger_invalid_params", { + chain, + inputIndex, + reason: "Unable to resolve previous transaction hex for Ledger signing", + txid, + }); + } + const witnessUtxo = input.witnessUtxo + ? { script: input.witnessUtxo.script, value: Number(input.witnessUtxo.amount) } + : undefined; + const nonWitnessPrevout = input.nonWitnessUtxo?.outputs?.[input.index]; + const value = witnessUtxo?.value ?? (nonWitnessPrevout ? Number(nonWitnessPrevout.amount) : 0); - inputs.push({ hash: txid, index: input.index, txHex, value, witnessUtxo } as UTXOType); - } - - return inputs; + return { hash: txid, index: input.index, txHex, value, witnessUtxo } as UTXOType; + }), + ); } export async function signLegacyPsbtTransaction({ - legacyClient, chain, + inputUtxos, + legacyClient, tx, }: { - legacyClient: { signTransaction: (tx: Transaction, inputUtxos: UTXOType[]) => Promise }; chain: UTXOChain; + inputUtxos?: UTXOType[]; + legacyClient: { signTransaction: (tx: Transaction, inputUtxos: UTXOType[]) => Promise }; tx: Transaction; }): Promise { - const inputUtxos = await extractInputsFromPsbt(tx, chain); - return legacyClient.signTransaction(tx, inputUtxos); + const resolvedInputUtxos = inputUtxos ?? (await extractInputsFromPsbt(tx, chain)); + return legacyClient.signTransaction(tx, resolvedInputUtxos); } /** diff --git a/packages/wallet-hardware/src/ledger/clients/utxo-psbt.ts b/packages/wallet-hardware/src/ledger/clients/utxo-psbt.ts deleted file mode 100644 index 30fcd37..0000000 --- a/packages/wallet-hardware/src/ledger/clients/utxo-psbt.ts +++ /dev/null @@ -1,171 +0,0 @@ -import type Transport from "@ledgerhq/hw-transport"; -import { base64 } from "@scure/base"; -import { HDKey } from "@scure/bip32"; -import { type DerivationPathArray, derivationPathToString, getWalletFormatFor, SwapKitError } from "@swapkit/helpers"; -import type { Transaction } from "@swapkit/utxo-signer"; - -import { getLedgerTransport } from "../helpers/getLedgerTransport"; - -type SupportedCoin = "bitcoin" | "litecoin"; - -type DefaultDescriptorTemplate = "wpkh(@0/**)" | "tr(@0/**)" | "sh(wpkh(@0/**))" | "pkh(@0/**)"; - -function templateForFormat(format: ReturnType): DefaultDescriptorTemplate { - switch (format) { - case "bech32": - return "wpkh(@0/**)"; - case "p2sh": - return "sh(wpkh(@0/**))"; - case "legacy": - return "pkh(@0/**)"; - default: - return "wpkh(@0/**)"; - } -} - -function pathToString(path: DerivationPathArray | string): string { - return typeof path === "string" ? path : derivationPathToString(path); -} - -function normalizeLedgerPath(path: string): string { - return path.replace(/^m\//, "").replace(/^\/+/, ""); -} - -function pathToNumberArray(path: string): number[] { - return path - .replace(/^m\//, "") - .split("/") - .filter(Boolean) - .map((p) => { - const hardened = p.endsWith("'"); - const num = Number.parseInt(hardened ? p.slice(0, -1) : p, 10); - return hardened ? (num | 0x80000000) >>> 0 : num; - }); -} - -function hasBip32Derivation(tx: Transaction, inputIndex: number) { - const input = tx.getInput(inputIndex) as { bip32Derivation?: Array }; - - return Boolean(input.bip32Derivation?.length); -} - -const BaseLedgerPsbtUTXO = ({ chain }: { chain: SupportedCoin }) => { - return (derivationPathArray?: DerivationPathArray | string, injectedTransport?: Transport) => { - // Per-call state — each BitcoinPsbtLedger/LitecoinPsbtLedger invocation has its own - // AppClient and master fingerprint so different consumers (e.g. concurrent MCP - // sessions on different devices) cannot inherit each other's ledger bindings or xpub. - let appClient: import("ledger-bitcoin").AppClient | undefined; - let masterFingerprint: string | undefined; - - async function getAppClient() { - if (!appClient) { - const transport = injectedTransport ?? (await getLedgerTransport()); - const { AppClient } = await import("ledger-bitcoin"); - appClient = new AppClient(transport); - } - return appClient; - } - - async function getFingerprint() { - if (!masterFingerprint) { - const app = await getAppClient(); - masterFingerprint = await app.getMasterFingerprint(); - } - return masterFingerprint; - } - - // Single-address account: change == index == 0 by default. - const derivationPath = normalizeLedgerPath( - derivationPathArray ? pathToString(derivationPathArray) : "84'/0'/0'/0/0", - ); - const pathSegments = derivationPath.split("/").filter(Boolean); - const accountPath = pathSegments.slice(0, 3).join("/"); - const leafSegments = pathSegments.slice(3); - const change = Number(leafSegments[0] ?? 0); - const addressIndex = Number(leafSegments[1] ?? 0); - // getWalletFormatFor expects an m/-prefixed path (it reads the purpose - // from the second segment); derivationPath is normalized without it. - const format = getWalletFormatFor(`m/${derivationPath}`); - const template = templateForFormat(format); - - let cachedAccountXpub: string | undefined; - let cachedLeafPubkey: Uint8Array | undefined; - - async function buildPolicy() { - const app = await getAppClient(); - const fpr = await getFingerprint(); - if (!cachedAccountXpub) { - cachedAccountXpub = await app.getExtendedPubkey(`m/${accountPath}`); - } - const { DefaultWalletPolicy } = await import("ledger-bitcoin"); - const policy = new DefaultWalletPolicy(template, `[${fpr}/${accountPath}]${cachedAccountXpub}`); - return { app, fpr, policy, xpub: cachedAccountXpub }; - } - - async function getLeafPubkey() { - if (!cachedLeafPubkey) { - const { xpub } = await buildPolicy(); - const accountKey = HDKey.fromExtendedKey(xpub); - const leaf = accountKey.derive(`m/${change}/${addressIndex}`); - if (!leaf.publicKey) { - throw new SwapKitError("wallet_ledger_get_address_error", { - message: `Cannot derive leaf pubkey for ${chain}`, - }); - } - cachedLeafPubkey = leaf.publicKey; - } - return cachedLeafPubkey; - } - - return { - connect: async () => { - await getAppClient(); - }, - getAddress: async () => { - const { app, policy } = await buildPolicy(); - const address = await app.getWalletAddress(policy, null, change, addressIndex, false); - if (!address) { - throw new SwapKitError("wallet_ledger_get_address_error", { - message: `Cannot get ${chain} address from ledger derivation path: ${derivationPath}`, - }); - } - return address; - }, - getExtendedPublicKey: async (path = `m/${accountPath}`) => { - const app = await getAppClient(); - return app.getExtendedPubkey(`m/${normalizeLedgerPath(path)}`); - }, - signTransaction: async (tx: Transaction): Promise => { - const { app, policy, fpr } = await buildPolicy(); - const fingerprintBE = Number.parseInt(fpr, 16) >>> 0; - const pathNumbers = pathToNumberArray(derivationPath); - const missingDerivationIndexes = Array.from({ length: tx.inputsLength }, (_, inputIndex) => inputIndex).filter( - (inputIndex) => !hasBip32Derivation(tx, inputIndex), - ); - - if (missingDerivationIndexes.length > 0) { - const leafPubkey = await getLeafPubkey(); - - // Fallback for PSBTs that do not include per-input HD key origins. - for (const inputIndex of missingDerivationIndexes) { - tx.updateInput(inputIndex, { - bip32Derivation: [[leafPubkey, { fingerprint: fingerprintBE, path: pathNumbers }]], - }); - } - } - - const psbtB64 = base64.encode(tx.toPSBT(0)); - const sigs = await app.signPsbt(psbtB64, policy, null); - - for (const [idx, partial] of sigs) { - tx.updateInput(idx, { partialSig: [[new Uint8Array(partial.pubkey), new Uint8Array(partial.signature)]] }); - } - - return tx; - }, - }; - }; -}; - -export const BitcoinPsbtLedger = BaseLedgerPsbtUTXO({ chain: "bitcoin" }); -export const LitecoinPsbtLedger = BaseLedgerPsbtUTXO({ chain: "litecoin" }); diff --git a/packages/wallet-hardware/src/ledger/clients/utxo.ts b/packages/wallet-hardware/src/ledger/clients/utxo.ts index 63dbece..732ecaf 100644 --- a/packages/wallet-hardware/src/ledger/clients/utxo.ts +++ b/packages/wallet-hardware/src/ledger/clients/utxo.ts @@ -1,21 +1,53 @@ +import type { UserInteractionRequired } from "@ledgerhq/device-management-kit"; import type BitcoinApp from "@ledgerhq/hw-app-btc"; import type { CreateTransactionArg } from "@ledgerhq/hw-app-btc/lib-es/createTransaction"; import type Transport from "@ledgerhq/hw-transport"; import { hex } from "@scure/base"; -import { type DerivationPathArray, derivationPathToString, getWalletFormatFor, SwapKitError } from "@swapkit/helpers"; +import { + Chain, + type DerivationPathArray, + derivationPathToString, + getWalletFormatFor, + SwapKitError, +} from "@swapkit/helpers"; import type { UTXOType } from "@swapkit/toolboxes/utxo"; import type { PCZT, Transaction } from "@swapkit/utxo-signer"; -import { getLedgerTransport } from "../helpers/getLedgerTransport"; +import { + LEDGER_USER_INTERACTION_REQUIRED, + type LedgerJsClientParams, + normalizeLedgerJsClientParams, + runLedgerJsOperation, +} from "../helpers/ledgerJsDmkBridge"; +import { createCachedRawTxResolver } from "../helpers/rawTx"; const nonSegwitLedgerChains = ["bitcoin-cash", "dash", "dogecoin", "zcash"]; +type LedgerUTXOChain = "bitcoin-cash" | "bitcoin" | "litecoin" | "dogecoin" | "dash" | "zcash"; +type UTXOLedgerParams = LedgerJsClientParams; + +interface ZcashPreviousTransactionInput { + index: number; + scriptPubkey: Uint8Array; + txid: Uint8Array; + value: bigint; +} + +const ledgerAppNames: Record = { + bitcoin: "Bitcoin", + "bitcoin-cash": "Bitcoin Cash", + dash: "Dash", + dogecoin: "Dogecoin", + litecoin: "Litecoin", + zcash: "Zcash", +}; + type Params = { tx: Transaction; inputUtxos: UTXOType[]; btcApp: BitcoinApp; derivationPath: string; - chain: "bitcoin-cash" | "bitcoin" | "litecoin" | "dogecoin" | "dash" | "zcash"; + chain: LedgerUTXOChain; }; type MultiPathParams = Omit & { @@ -23,6 +55,48 @@ type MultiPathParams = Omit & { derivationPaths: string[]; }; +export async function resolveZcashPreviousTransaction({ + getRawTx, + input, + inputIndex, +}: { + getRawTx: (txid: string) => Promise; + input: ZcashPreviousTransactionInput; + inputIndex: number; +}) { + const txid = hex.encode(input.txid); + const txHex = await getRawTx(txid); + if (!txHex) { + throw new SwapKitError("wallet_ledger_invalid_params", { + chain: Chain.Zcash, + inputIndex, + reason: "Unable to resolve previous transaction hex for Ledger signing", + txid, + }); + } + + return { + hash: txid, + index: input.index, + txHex, + value: Number(input.value), + witnessUtxo: { script: input.scriptPubkey, value: Number(input.value) }, + } as UTXOType; +} + +export function resolveZcashPreviousTransactions({ + getRawTx, + inputs, +}: { + getRawTx: (txid: string) => Promise; + inputs: ZcashPreviousTransactionInput[]; +}) { + const cachedGetRawTx = createCachedRawTxResolver(getRawTx); + return Promise.all( + inputs.map((input, inputIndex) => resolveZcashPreviousTransaction({ getRawTx: cachedGetRawTx, input, inputIndex })), + ); +} + const signUTXOTransaction = ( { tx, inputUtxos, btcApp, derivationPath, chain }: Params, options?: Partial, @@ -110,52 +184,47 @@ const BaseLedgerUTXO = ({ chain, additionalSignParams, }: { - chain: "bitcoin-cash" | "bitcoin" | "litecoin" | "dogecoin" | "dash" | "zcash"; + chain: LedgerUTXOChain; additionalSignParams?: Partial; }) => { - return (derivationPathArray?: DerivationPathArray | string, injectedTransport?: Transport) => { - // Per-call state — each BitcoinLedger/LitecoinLedger/... invocation has its own - // transport + btcApp so different consumers (e.g. concurrent MCP sessions) cannot - // cross-contaminate each other's Ledger device handle. - let btcApp: InstanceType | undefined; - let transport: any = null; - - async function createTransportWebUSB() { - transport ||= injectedTransport ?? (await getLedgerTransport()); - const BitcoinApp = (await import("@ledgerhq/hw-app-btc")).default; - - btcApp ||= new BitcoinApp({ currency: chain, transport }); - return btcApp; - } - - async function getBtcApp() { - return btcApp || (await createTransportWebUSB()); - } - - async function disconnect() { - if (!injectedTransport) await transport?.close?.(); - btcApp = undefined; - transport = null; - } - + return (paramsOrPath?: UTXOLedgerParams | DerivationPathArray | string, transport?: Transport) => { + const { derivationPath: derivationPathParam, ...connection } = normalizeLedgerJsClientParams({ + paramsOrPath, + transport, + }); const derivationPath = - typeof derivationPathArray === "string" - ? derivationPathArray - : derivationPathToString(derivationPathArray as DerivationPathArray); - + typeof derivationPathParam === "string" + ? derivationPathParam + : derivationPathToString(derivationPathParam as DerivationPathArray); const format = getWalletFormatFor(derivationPath); + async function runBtcOperation({ + operation, + requiredUserInteraction, + }: { + operation: (app: InstanceType) => Promise | Output; + requiredUserInteraction?: UserInteractionRequired; + }) { + const BitcoinApp = (await import("@ledgerhq/hw-app-btc")).default; + return runLedgerJsOperation({ + appName: ledgerAppNames[chain], + connection, + createApp: (ledgerTransport) => new BitcoinApp({ currency: chain, transport: ledgerTransport }), + operation, + requiredUserInteraction, + }); + } + return { connect: async () => { - await getBtcApp(); + await runBtcOperation({ operation: async () => true }); }, - disconnect, + disconnect: async () => {}, getAddress: async () => { const { toCashAddress } = await import("@swapkit/toolboxes/utxo"); - - const app = await getBtcApp(); - - const { bitcoinAddress: address } = await app.getWalletPublicKey(derivationPath, { format }); + const { bitcoinAddress: address } = await runBtcOperation({ + operation: (app) => app.getWalletPublicKey(derivationPath, { format }), + }); if (!address) { throw new SwapKitError("wallet_ledger_get_address_error", { @@ -168,9 +237,7 @@ const BaseLedgerUTXO = ({ : address; }, getExtendedPublicKey: async (path = "84'/0'/0'", xpubVersion = 76067358) => { - const app = await getBtcApp(); - - return app.getWalletXpub({ path, xpubVersion }); + return await runBtcOperation({ operation: (app) => app.getWalletXpub({ path, xpubVersion }) }); }, signPCZT: async (pczt: PCZT): Promise => { @@ -180,9 +247,8 @@ const BaseLedgerUTXO = ({ }); } - const app = await getBtcApp(); - const { ZcashTransaction, Script } = await import("@swapkit/utxo-signer"); + const { getUtxoApi } = await import("@swapkit/toolboxes/utxo"); const global = pczt.getGlobal(); @@ -194,11 +260,9 @@ const BaseLedgerUTXO = ({ versionGroupId: global.versionGroupId, }); - const inputUtxos: UTXOType[] = []; - - for (let i = 0; i < pczt.inputsLength; i++) { - const input = pczt.getInput(i); + const inputs = Array.from({ length: pczt.inputsLength }, (_, inputIndex) => pczt.getInput(inputIndex)); + for (const input of inputs) { unsignedTx.addInput({ index: input.index, script: new Uint8Array(), @@ -206,33 +270,35 @@ const BaseLedgerUTXO = ({ txid: input.txid, value: input.value, }); - - inputUtxos.push({ - hash: hex.encode(new Uint8Array([...input.txid].reverse())), - index: input.index, - txHex: buildMinimalPrevTxHex(input, global), - value: Number(input.value), - witnessUtxo: { script: input.scriptPubkey, value: Number(input.value) }, - } as UTXOType); } + const zcashApi = getUtxoApi(Chain.Zcash); + const inputUtxos = await resolveZcashPreviousTransactions({ + getRawTx: (txid) => zcashApi.getRawTx(txid), + inputs, + }); + for (let i = 0; i < pczt.outputsLength; i++) { const output = pczt.getOutput(i); unsignedTx.addOutput({ amount: output.value, script: output.scriptPubkey }); } - const signedTxHex = await signUTXOTransaction( - { btcApp: app, chain, derivationPath, inputUtxos, tx: unsignedTx as unknown as Transaction }, - { - ...additionalSignParams, - expiryHeight: (() => { - const buf = Buffer.alloc(4); - buf.writeUInt32LE(global.expiryHeight); - return buf; - })(), - lockTime: global.lockTime, - }, - ); + const signedTxHex = await runBtcOperation({ + operation: (app) => + signUTXOTransaction( + { btcApp: app, chain, derivationPath, inputUtxos, tx: unsignedTx as unknown as Transaction }, + { + ...additionalSignParams, + expiryHeight: (() => { + const buf = Buffer.alloc(4); + buf.writeUInt32LE(global.expiryHeight); + return buf; + })(), + lockTime: global.lockTime, + }, + ), + requiredUserInteraction: LEDGER_USER_INTERACTION_REQUIRED.SignTransaction, + }); const signedTx = ZcashTransaction.fromHex(signedTxHex, { allowUnknownOutputs: true }); const signedPczt = pczt.clone(); @@ -250,9 +316,11 @@ const BaseLedgerUTXO = ({ return signedPczt; }, signTransaction: async (tx: Transaction, inputUtxos: UTXOType[]) => { - const app = await getBtcApp(); - - return signUTXOTransaction({ btcApp: app, chain, derivationPath, inputUtxos, tx }, additionalSignParams); + return await runBtcOperation({ + operation: (app) => + signUTXOTransaction({ btcApp: app, chain, derivationPath, inputUtxos, tx }, additionalSignParams), + requiredUserInteraction: LEDGER_USER_INTERACTION_REQUIRED.SignTransaction, + }); }, /** @@ -260,85 +328,19 @@ const BaseLedgerUTXO = ({ * Each input can be signed with its own derivation path. */ signTransactionWithMultiplePaths: async (tx: Transaction, inputUtxos: UTXOType[], derivationPaths: string[]) => { - const app = await getBtcApp(); - - return signUTXOTransactionWithMultiplePaths( - { btcApp: app, chain, derivationPaths, inputUtxos, tx }, - additionalSignParams, - ); + return await runBtcOperation({ + operation: (app) => + signUTXOTransactionWithMultiplePaths( + { btcApp: app, chain, derivationPaths, inputUtxos, tx }, + additionalSignParams, + ), + requiredUserInteraction: LEDGER_USER_INTERACTION_REQUIRED.SignTransaction, + }); }, }; }; }; -function buildMinimalPrevTxHex( - input: { txid: Uint8Array; index: number; scriptPubkey: Uint8Array; value: bigint }, - global: { txVersion: number; versionGroupId: number; expiryHeight: number; lockTime: number }, -): string { - const parts: number[] = []; - - const version = (global.txVersion | 0x80000000) >>> 0; - parts.push(version & 0xff, (version >> 8) & 0xff, (version >> 16) & 0xff, (version >> 24) & 0xff); - - const vgid = global.versionGroupId; - parts.push(vgid & 0xff, (vgid >> 8) & 0xff, (vgid >> 16) & 0xff, (vgid >> 24) & 0xff); - - parts.push(0); - - const outputCount = input.index + 1; - if (outputCount < 0xfd) { - parts.push(outputCount); - } else { - parts.push(0xfd, outputCount & 0xff, (outputCount >> 8) & 0xff); - } - - for (let i = 0; i < input.index; i++) { - parts.push(0, 0, 0, 0, 0, 0, 0, 0); - parts.push(0); - } - - const value = input.value; - parts.push( - Number(value & 0xffn), - Number((value >> 8n) & 0xffn), - Number((value >> 16n) & 0xffn), - Number((value >> 24n) & 0xffn), - Number((value >> 32n) & 0xffn), - Number((value >> 40n) & 0xffn), - Number((value >> 48n) & 0xffn), - Number((value >> 56n) & 0xffn), - ); - - const script = input.scriptPubkey; - if (script.length < 0xfd) { - parts.push(script.length); - } else { - parts.push(0xfd, script.length & 0xff, (script.length >> 8) & 0xff); - } - for (const byte of script) { - parts.push(byte); - } - - parts.push( - global.lockTime & 0xff, - (global.lockTime >> 8) & 0xff, - (global.lockTime >> 16) & 0xff, - (global.lockTime >> 24) & 0xff, - ); - parts.push( - global.expiryHeight & 0xff, - (global.expiryHeight >> 8) & 0xff, - (global.expiryHeight >> 16) & 0xff, - (global.expiryHeight >> 24) & 0xff, - ); - parts.push(0, 0, 0, 0, 0, 0, 0, 0); // value balance - parts.push(0); // empty sapling spends - parts.push(0); // empty sapling outputs - parts.push(0); // empty joinsplits - - return hex.encode(new Uint8Array(parts)); -} - export const BitcoinLedger = BaseLedgerUTXO({ chain: "bitcoin" }); export const LitecoinLedger = BaseLedgerUTXO({ chain: "litecoin" }); diff --git a/packages/wallet-hardware/src/ledger/clients/xrp.ts b/packages/wallet-hardware/src/ledger/clients/xrp.ts index c7c95c2..6dd7abd 100644 --- a/packages/wallet-hardware/src/ledger/clients/xrp.ts +++ b/packages/wallet-hardware/src/ledger/clients/xrp.ts @@ -1,33 +1,51 @@ +import type { UserInteractionRequired } from "@ledgerhq/device-management-kit"; import Xrp from "@ledgerhq/hw-app-xrp"; import type Transport from "@ledgerhq/hw-transport"; import { Chain, type DerivationPathArray, derivationPathToString, NetworkDerivationPath } from "@swapkit/helpers"; import type { RippleTransaction } from "@swapkit/toolboxes/ripple"; import { encode } from "ripple-binary-codec"; import type { Payment } from "xrpl"; -import { getLedgerTransport } from "../helpers/getLedgerTransport"; + +import { + LEDGER_USER_INTERACTION_REQUIRED, + type LedgerJsClientParams, + normalizeLedgerJsClientParams, + runLedgerJsOperation, +} from "../helpers/ledgerJsDmkBridge"; const TF_FULLY_CANONICAL_SIG = 2147483648; +type XRPLedgerParams = LedgerJsClientParams; + function cleanTransactionObject(obj: Record) { const cleaned: Record = {}; for (const key in obj) { - if (obj[key] !== null && obj[key] !== undefined) { - cleaned[key] = obj[key]; - } + if (obj[key] !== null && obj[key] !== undefined) cleaned[key] = obj[key]; } return cleaned; } -function establishConnection(transport: Transport) { - return new Xrp(transport); -} - -export const XRPLedger = async (derivationPath?: DerivationPathArray, injectedTransport?: Transport) => { +export async function XRPLedger(paramsOrPath?: XRPLedgerParams | DerivationPathArray, transport?: Transport) { + const { derivationPath, ...connection } = normalizeLedgerJsClientParams({ paramsOrPath, transport }); const path = derivationPathToString(derivationPath || NetworkDerivationPath[Chain.Ripple]); - const transport = injectedTransport ?? (await getLedgerTransport()); - const xrpInstance = establishConnection(transport); - const { address, publicKey } = await xrpInstance.getAddress(path); + function runXrpOperation({ + operation, + requiredUserInteraction, + }: { + operation: (app: Xrp) => Promise; + requiredUserInteraction?: UserInteractionRequired; + }) { + return runLedgerJsOperation({ + appName: "XRP", + connection, + createApp: (ledgerTransport) => new Xrp(ledgerTransport), + operation, + requiredUserInteraction, + }); + } + + const { address, publicKey } = await runXrpOperation({ operation: (app) => app.getAddress(path) }); async function signTransaction(transaction: Payment | RippleTransaction) { const { hashes } = await import("xrpl"); @@ -39,7 +57,10 @@ export const XRPLedger = async (derivationPath?: DerivationPathArray, injectedTr }; const transactionToSignOnLedger = encode(transactionJSON); - const txnSignature = await xrpInstance.signTransaction(path, transactionToSignOnLedger); + const txnSignature = await runXrpOperation({ + operation: (app) => app.signTransaction(path, transactionToSignOnLedger), + requiredUserInteraction: LEDGER_USER_INTERACTION_REQUIRED.SignTransaction, + }); const tx_blob = encode({ ...transactionJSON, TxnSignature: txnSignature }); const hash = hashes.hashSignedTx(tx_blob); @@ -47,4 +68,4 @@ export const XRPLedger = async (derivationPath?: DerivationPathArray, injectedTr } return { getAddress: () => address, signTransaction }; -}; +} diff --git a/packages/wallet-hardware/src/ledger/clients/zcash.ts b/packages/wallet-hardware/src/ledger/clients/zcash.ts new file mode 100644 index 0000000..e070134 --- /dev/null +++ b/packages/wallet-hardware/src/ledger/clients/zcash.ts @@ -0,0 +1,412 @@ +import type { LegacyCreateTransactionArg, LegacyTransaction, SignerZcash } from "@ledgerhq/device-signer-kit-zcash"; +import type Transport from "@ledgerhq/hw-transport"; +import { hex } from "@scure/base"; +import { + type DerivationPathArray, + derivationPathToString, + NetworkDerivationPath, + SwapKitError, +} from "@swapkit/helpers"; +import type { UTXOType } from "@swapkit/toolboxes/utxo"; +import type { Transaction, ZcashTransaction } from "@swapkit/utxo-signer"; + +import type { LedgerDMKSession } from "../helpers/dmk"; +import { getLedgerDMKSession } from "../helpers/dmk"; +import { executeLedgerDeviceAction, type LedgerDeviceActionStateHandler } from "../helpers/executeDeviceAction"; +import { runLedgerJsOperation } from "../helpers/ledgerJsDmkBridge"; +import { ZcashLedger as LegacyZcashLedger } from "./utxo"; + +const NU6_2_ACTIVATION_HEIGHT = 3_364_600; +const IRONWOOD_ACTIVATION_HEIGHT = 3_428_143; + +interface ZcashLedgerParams { + derivationPath?: DerivationPathArray | string; + dmkSession?: LedgerDMKSession; + onDeviceActionState?: LedgerDeviceActionStateHandler; + transport?: Transport; +} + +interface GetExtendedPublicKeyParams { + path?: string; + xpubVersion?: number; +} + +interface SignZcashTransactionParams { + changePath?: string; + derivationPaths?: string[]; + inputUtxos: UTXOType[]; + tx: ZcashTransaction; +} + +interface SignZcashTransactionWithMultiplePathsParams extends Omit { + derivationPaths: string[]; +} + +interface ParsedZcashPath { + account: number; + accountPath: string; + fullPath: string; +} + +function normalizePath(path: DerivationPathArray | string) { + return (typeof path === "string" ? path : derivationPathToString(path)).replace(/^m\//, "").replace(/^\/+/, ""); +} + +function parseZcashPath(path: DerivationPathArray | string): ParsedZcashPath { + const normalized = normalizePath(path); + const match = /^44'\/133'\/(\d+)'(?:\/(0|1)\/(\d+))?$/.exec(normalized); + + if (!match) { + throw new SwapKitError("wallet_ledger_invalid_params", { + path: normalized, + reason: "Expected a Zcash BIP44 path with coin type 133", + }); + } + + const account = Number(match[1]); + const accountPath = `44'/133'/${account}'`; + return { account, accountPath, fullPath: `${accountPath}/${match[2] ?? 0}/${match[3] ?? 0}` }; +} + +function uint32LE(value: number) { + const bytes = new Uint8Array(4); + new DataView(bytes.buffer).setUint32(0, value >>> 0, true); + return bytes; +} + +function uint64LE(value: bigint) { + const bytes = new Uint8Array(8); + new DataView(bytes.buffer).setBigUint64(0, value, true); + return bytes; +} + +function compactSize(value: number) { + if (!Number.isSafeInteger(value) || value < 0) { + throw new SwapKitError("wallet_ledger_invalid_params", { reason: `Invalid compact-size value: ${value}` }); + } + if (value < 0xfd) return Uint8Array.of(value); + if (value <= 0xffff) return Uint8Array.of(0xfd, value & 0xff, (value >>> 8) & 0xff); + if (value <= 0xffffffff) { + return Uint8Array.of(0xfe, value & 0xff, (value >>> 8) & 0xff, (value >>> 16) & 0xff, value >>> 24); + } + + const bytes = new Uint8Array(9); + bytes[0] = 0xff; + new DataView(bytes.buffer).setBigUint64(1, BigInt(value), true); + return bytes; +} + +function readCompactSize({ bytes, offset }: { bytes: Uint8Array; offset: number }) { + const prefix = bytes[offset]; + if (prefix === undefined) throw new Error("Unexpected end of previous transaction"); + if (prefix < 0xfd) return { offset: offset + 1, value: prefix }; + + const byteLength = prefix === 0xfd ? 2 : prefix === 0xfe ? 4 : 8; + if (offset + 1 + byteLength > bytes.length) throw new Error("Truncated compact-size value"); + const view = new DataView(bytes.buffer, bytes.byteOffset + offset + 1, byteLength); + const value = + byteLength === 2 + ? view.getUint16(0, true) + : byteLength === 4 + ? view.getUint32(0, true) + : Number(view.getBigUint64(0, true)); + if (!Number.isSafeInteger(value)) throw new Error("Previous transaction compact-size value exceeds safe range"); + return { offset: offset + 1 + byteLength, value }; +} + +function readUint32LE({ bytes, offset }: { bytes: Uint8Array; offset: number }) { + if (offset + 4 > bytes.length) throw new Error("Unexpected end of previous transaction"); + return new DataView(bytes.buffer, bytes.byteOffset + offset, 4).getUint32(0, true); +} + +function parsePreviousTransactionOutput({ bytes, outputIndex }: { bytes: Uint8Array; outputIndex: number }) { + const version = readUint32LE({ bytes, offset: 0 }) & 0x7fffffff; + const overwintered = (readUint32LE({ bytes, offset: 0 }) & 0x80000000) !== 0; + let offset = overwintered ? 8 : 4; + let consensusBranchId = new Uint8Array(); + let expiryHeight = new Uint8Array(); + let locktime = new Uint8Array(); + + if (version >= 5) { + if (bytes.length < 20) throw new Error("Truncated Zcash v5 transaction header"); + consensusBranchId = bytes.slice(8, 12); + locktime = bytes.slice(12, 16); + expiryHeight = bytes.slice(16, 20); + offset = 20; + } + + const inputCount = readCompactSize({ bytes, offset }); + offset = inputCount.offset; + for (let index = 0; index < inputCount.value; index += 1) { + if (offset + 36 > bytes.length) throw new Error("Truncated previous transaction input"); + offset += 36; + const scriptLength = readCompactSize({ bytes, offset }); + offset = scriptLength.offset + scriptLength.value + 4; + if (offset > bytes.length) throw new Error("Truncated previous transaction input script"); + } + + const outputCount = readCompactSize({ bytes, offset }); + offset = outputCount.offset; + if (outputIndex < 0 || outputIndex >= outputCount.value) { + throw new Error("Zcash input references an output outside its previous transaction"); + } + + let referencedOutput: { amount: Uint8Array; script: Uint8Array } | undefined; + for (let index = 0; index < outputCount.value; index += 1) { + if (offset + 8 > bytes.length) throw new Error("Truncated previous transaction output amount"); + const amount = bytes.slice(offset, offset + 8); + offset += 8; + const scriptLength = readCompactSize({ bytes, offset }); + offset = scriptLength.offset; + if (offset + scriptLength.value > bytes.length) throw new Error("Truncated previous transaction output script"); + const script = bytes.slice(offset, offset + scriptLength.value); + offset += scriptLength.value; + if (index === outputIndex) referencedOutput = { amount, script }; + } + + if (version < 5) { + if (offset + 4 > bytes.length) throw new Error("Previous transaction is missing locktime"); + locktime = bytes.slice(offset, offset + 4); + offset += 4; + expiryHeight = overwintered ? bytes.slice(offset, offset + 4) : new Uint8Array(); + } + + if (!referencedOutput) throw new Error("Previous transaction output could not be parsed"); + return { consensusBranchId, expiryHeight, locktime, outputCount: outputCount.value, referencedOutput }; +} + +function concatBytes(parts: Uint8Array[]) { + const result = new Uint8Array(parts.reduce((length, part) => length + part.length, 0)); + let offset = 0; + for (const part of parts) { + result.set(part, offset); + offset += part.length; + } + return result; +} + +function serializeOutputs(tx: ZcashTransaction) { + const outputs = Array.from({ length: tx.outputsLength }, (_, index) => tx.getOutput(index)); + return concatBytes([ + compactSize(outputs.length), + ...outputs.flatMap(({ amount, script }) => [uint64LE(amount), compactSize(script.length), script]), + ]); +} + +async function activationHeight(consensusBranchId: number) { + const { ZcashConsensusBranchId } = await import("@swapkit/utxo-signer"); + + switch (consensusBranchId) { + case ZcashConsensusBranchId.NU6_2: + return NU6_2_ACTIVATION_HEIGHT; + case ZcashConsensusBranchId.IRONWOOD: + return IRONWOOD_ACTIVATION_HEIGHT; + default: + throw new SwapKitError("wallet_ledger_invalid_params", { + consensusBranchId, + reason: "Ledger Zcash DSK signing supports NU6.2 and Ironwood target branches", + }); + } +} + +function previousTransaction({ inputIndex, utxo }: { inputIndex: number; utxo: UTXOType }): LegacyTransaction { + if (!utxo.txHex) { + throw new SwapKitError("wallet_ledger_invalid_params", { + inputIndex, + reason: "Zcash Ledger signing requires the full previous transaction txHex for every input", + }); + } + + const raw = hex.decode(utxo.txHex.replace(/^0x/i, "")); + const parsed = (() => { + try { + return parsePreviousTransactionOutput({ bytes: raw, outputIndex: utxo.index }); + } catch (error) { + throw new SwapKitError("wallet_ledger_invalid_params", error); + } + })(); + + const outputs = Array.from({ length: utxo.index + 1 }, (_, outputIndex) => + outputIndex === utxo.index ? parsed.referencedOutput : { amount: new Uint8Array(8), script: new Uint8Array() }, + ); + + return { + consensusBranchId: parsed.consensusBranchId, + inputs: [], + locktime: parsed.locktime, + nExpiryHeight: parsed.expiryHeight, + nVersionGroupId: (readUint32LE({ bytes: raw, offset: 0 }) & 0x80000000) !== 0 ? raw.slice(4, 8) : undefined, + outputs, + serializedPreviousTransactionOverride: raw, + version: raw.slice(0, 4), + }; +} + +function validateSigningParams({ + accountPath, + derivationPaths, + inputUtxos, + tx, +}: { + accountPath: string; + derivationPaths: string[]; + inputUtxos: UTXOType[]; + tx: ZcashTransaction; +}) { + if (inputUtxos.length !== tx.inputsLength || derivationPaths.length !== tx.inputsLength) { + throw new SwapKitError("wallet_ledger_invalid_params", { + derivationPathCount: derivationPaths.length, + inputCount: tx.inputsLength, + inputUtxoCount: inputUtxos.length, + reason: "Zcash input UTXOs and derivation paths must match the transaction input count", + }); + } + + return derivationPaths.map((path) => { + const parsedPath = parseZcashPath(path); + if (parsedPath.accountPath !== accountPath) { + throw new SwapKitError("wallet_ledger_invalid_params", { + accountPath, + path: parsedPath.fullPath, + reason: "All Zcash input derivation paths must belong to the configured account", + }); + } + return parsedPath.fullPath; + }); +} + +function assertRawV5(rawTransaction: string) { + const normalized = rawTransaction.replace(/^0x/i, ""); + const bytes = hex.decode(normalized); + if (bytes.length < 4 || (new DataView(bytes.buffer, bytes.byteOffset, 4).getUint32(0, true) & 0x7fffffff) !== 5) { + throw new SwapKitError("wallet_ledger_invalid_response", { + reason: "Ledger Zcash signer did not return a version 5 transaction", + }); + } + return normalized; +} + +export function ZcashLedger({ + derivationPath = NetworkDerivationPath.ZEC, + dmkSession, + onDeviceActionState, + transport, +}: ZcashLedgerParams = {}) { + const configuredPath = parseZcashPath(derivationPath); + const legacyClient = transport ? LegacyZcashLedger(derivationPath, transport) : undefined; + let signerPromise: Promise | undefined; + + function getSigner() { + if (legacyClient) { + throw new SwapKitError("wallet_ledger_invalid_params", { + reason: "A Device Signer Kit operation is unavailable when a legacy transport is supplied", + }); + } + + signerPromise ??= (async () => { + const session = dmkSession ?? (await getLedgerDMKSession()); + const { SignerZcashBuilder } = await import("@ledgerhq/device-signer-kit-zcash"); + return new SignerZcashBuilder(session).build(); + })(); + return signerPromise; + } + + async function getDskAddress({ checkOnDevice = false, path = configuredPath.fullPath } = {}) { + const signer = await getSigner(); + return executeLedgerDeviceAction({ action: signer.getAddress(path, { checkOnDevice }), onDeviceActionState }); + } + + async function getExtendedPublicKey({ + path = configuredPath.accountPath, + xpubVersion = 0x0488b21e, + }: GetExtendedPublicKeyParams = {}) { + const parsedAccount = parseZcashPath(path); + if (parsedAccount.fullPath !== `${parsedAccount.accountPath}/0/0` || normalizePath(path).split("/").length !== 3) { + throw new SwapKitError("wallet_ledger_invalid_params", { + path, + reason: "Zcash extended public keys are exported at account level", + }); + } + + const connection = { + dmkSession: transport ? undefined : (dmkSession ?? (await getLedgerDMKSession())), + onDeviceActionState, + transport, + }; + const { default: BitcoinApp } = await import("@ledgerhq/hw-app-btc"); + return runLedgerJsOperation({ + appName: "Zcash", + connection, + createApp: (operationTransport) => new BitcoinApp({ currency: "zcash", transport: operationTransport }), + operation: (app) => app.getWalletXpub({ path: parsedAccount.accountPath, xpubVersion }), + }); + } + + async function signTransaction({ changePath, derivationPaths, inputUtxos, tx }: SignZcashTransactionParams) { + const paths = validateSigningParams({ + accountPath: configuredPath.accountPath, + derivationPaths: derivationPaths ?? Array.from({ length: tx.inputsLength }, () => configuredPath.fullPath), + inputUtxos, + tx, + }); + + if (legacyClient) { + const legacyTx = { unsignedTx: tx.toBytes() } as Transaction; + const signed = + derivationPaths === undefined + ? await legacyClient.signTransaction(legacyTx, inputUtxos) + : await legacyClient.signTransactionWithMultiplePaths( + legacyTx, + inputUtxos, + paths.map((path) => `m/${path}`), + ); + return assertRawV5(signed); + } + + const blockHeight = await activationHeight(tx.consensusBranchId); + const args: LegacyCreateTransactionArg = { + additionals: ["zcash", "sapling"], + associatedKeysets: paths, + blockHeight, + changePath: changePath ? parseZcashPath(changePath).fullPath : configuredPath.fullPath, + expiryHeight: uint32LE(tx.expiryHeight), + inputs: inputUtxos.map((utxo, inputIndex) => [ + previousTransaction({ inputIndex, utxo }), + utxo.index, + null, + tx.getInput(inputIndex).sequence ?? 0xffffffff, + ]), + lockTime: tx.lockTime, + outputScriptHex: hex.encode(serializeOutputs(tx)), + }; + const signer = await getSigner(); + const signed = await executeLedgerDeviceAction({ action: signer.signTransaction(args), onDeviceActionState }); + return assertRawV5(signed); + } + + return { + connect: async () => { + if (legacyClient) return legacyClient.connect(); + await getSigner(); + }, + disconnect: async () => legacyClient?.disconnect(), + getAddress: async () => { + if (legacyClient) return legacyClient.getAddress(); + return (await getDskAddress()).address; + }, + getAddressAndPubKey: async () => { + if (legacyClient) return { address: await legacyClient.getAddress() }; + const { address, chainCode, publicKey } = await getDskAddress(); + return { address, chainCode: hex.encode(chainCode), publicKey: hex.encode(publicKey) }; + }, + getExtendedPublicKey, + showAddressAndPubKey: async () => { + if (legacyClient) return { address: await legacyClient.getAddress() }; + const { address, chainCode, publicKey } = await getDskAddress({ checkOnDevice: true }); + return { address, chainCode: hex.encode(chainCode), publicKey: hex.encode(publicKey) }; + }, + signTransaction, + signTransactionWithMultiplePaths: async (params: SignZcashTransactionWithMultiplePathsParams) => + signTransaction(params), + }; +} diff --git a/packages/wallet-hardware/src/ledger/cosmosTypes.ts b/packages/wallet-hardware/src/ledger/cosmosTypes.ts deleted file mode 100644 index 3ff7c7b..0000000 --- a/packages/wallet-hardware/src/ledger/cosmosTypes.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { SwapKitError } from "@swapkit/helpers"; - -export interface Coin { - readonly denom: string; - readonly amount: string; -} - -export interface EncodeObject { - readonly typeUrl: string; - readonly value: any; -} - -export interface AminoMsg { - readonly type: string; - readonly value: any; -} - -export interface AminoMsgSend extends AminoMsg { - readonly type: "cosmos-sdk/MsgSend"; - readonly value: { - /** Bech32 account address */ - readonly from_address: string; - /** Bech32 account address */ - readonly to_address: string; - readonly amount: readonly Coin[]; - }; -} - -export interface AminoConverter { - readonly aminoType: string; - readonly toAmino: (value: any) => any; - readonly fromAmino: (value: any) => any; -} - -/** A map from protobuf type URL to the AminoConverter implementation if supported on chain */ -export type AminoConverters = Record; - -function isAminoConverter( - converter: [string, AminoConverter | "not_supported_by_chain"], -): converter is [string, AminoConverter] { - return typeof converter[1] !== "string"; -} - -/** - * A map from Stargate message types as used in the messages's `Any` type - * to Amino types. - */ -export class AminoTypes { - // The map type here ensures uniqueness of the protobuf type URL in the key. - // There is no uniqueness guarantee of the Amino type identifier in the type - // system or constructor. Instead it's the user's responsibility to ensure - // there is no overlap when fromAmino is called. - private readonly register: Record; - - constructor(types: AminoConverters) { - this.register = types; - } - - toAmino({ typeUrl, value }: EncodeObject): AminoMsg { - const converter = this.register[typeUrl]; - if (converter === "not_supported_by_chain") { - throw new SwapKitError("wallet_ledger_chain_not_supported", { - reason: `The message type '${typeUrl}' cannot be signed using the Amino JSON sign mode because this is not supported by chain.`, - }); - } - if (!converter) { - throw new SwapKitError("wallet_ledger_invalid_params", { - reason: `Type URL '${typeUrl}' does not exist in the Amino message type register.`, - }); - } - return { type: converter.aminoType, value: converter.toAmino(value) }; - } - - fromAmino({ type, value }: AminoMsg): EncodeObject { - const matches = Object.entries(this.register) - .filter(isAminoConverter) - .filter(([_typeUrl, { aminoType }]) => aminoType === type); - - if (matches.length === 0) { - throw new SwapKitError("wallet_ledger_invalid_params", { - reason: `Amino type identifier '${type}' does not exist in the Amino message type register.`, - }); - } - - if (matches.length > 1) { - throw new SwapKitError("wallet_ledger_invalid_params", { - reason: `Multiple types are registered with Amino type identifier '${type}': '${matches - .map(([key, _value]) => key) - .sort() - .join("', '")}'. Thus fromAmino cannot be performed.`, - }); - } - - const [typeUrl, converter] = matches[0] as [string, AminoConverter]; - - return { typeUrl, value: converter.fromAmino(value) }; - } -} diff --git a/packages/wallet-hardware/src/ledger/helpers/dmk.ts b/packages/wallet-hardware/src/ledger/helpers/dmk.ts new file mode 100644 index 0000000..fb56453 --- /dev/null +++ b/packages/wallet-hardware/src/ledger/helpers/dmk.ts @@ -0,0 +1,139 @@ +import type { DeviceManagementKit, DeviceSessionId } from "@ledgerhq/device-management-kit"; +import { SwapKitError } from "@swapkit/helpers"; +import { firstValueFrom } from "rxjs"; + +export interface LedgerDMKSession { + dmk: DeviceManagementKit; + sessionId: DeviceSessionId; +} + +let defaultDMKPromise: Promise | undefined; +let defaultSessionPromise: Promise | undefined; + +function createDefaultDMKPromise() { + if (typeof navigator === "undefined" || !("hid" in navigator)) { + return Promise.reject(new SwapKitError("wallet_ledger_webhid_not_supported")); + } + + return Promise.all([import("@ledgerhq/device-management-kit"), import("@ledgerhq/device-transport-kit-web-hid")]) + .then(([{ DeviceManagementKitBuilder }, { webHidTransportFactory }]) => + new DeviceManagementKitBuilder().addTransport(webHidTransportFactory).build(), + ) + .catch((error) => { + defaultDMKPromise = undefined; + if (error instanceof SwapKitError) throw error; + throw new SwapKitError("wallet_ledger_connection_error", error); + }); +} + +function getDefaultDMK() { + defaultDMKPromise ??= createDefaultDMKPromise(); + return defaultDMKPromise; +} + +function createDefaultSessionPromise() { + return getDefaultDMK().then((dmk) => connectLedgerDMK({ dmk })); +} + +function isSessionConnected({ dmk, sessionId }: LedgerDMKSession) { + try { + dmk.getConnectedDevice({ sessionId }); + return true; + } catch (error) { + void error; + return false; + } +} + +async function getValidDefaultSession({ sessionPromise }: { sessionPromise: Promise }) { + let activeSessionPromise = sessionPromise; + + try { + const session = await activeSessionPromise; + if (isSessionConnected(session)) return session; + + if (defaultSessionPromise === activeSessionPromise) { + defaultSessionPromise = createDefaultSessionPromise(); + } + + defaultSessionPromise ??= createDefaultSessionPromise(); + activeSessionPromise = defaultSessionPromise; + + return await activeSessionPromise; + } catch (error) { + if (defaultSessionPromise === activeSessionPromise) defaultSessionPromise = undefined; + if (error instanceof SwapKitError) throw error; + throw new SwapKitError("wallet_ledger_connection_error", error); + } +} + +async function connectLedgerDMK({ dmk }: { dmk: DeviceManagementKit }) { + let discoveryStarted = false; + + try { + const discoveredDevices = dmk.startDiscovering({}); + discoveryStarted = true; + const device = await firstValueFrom(discoveredDevices); + const sessionId = await dmk.connect({ device }); + + return { dmk, sessionId }; + } catch (error) { + if (error instanceof SwapKitError) throw error; + throw new SwapKitError("wallet_ledger_connection_error", error); + } finally { + if (discoveryStarted) { + try { + await dmk.stopDiscovering(); + } catch (error) { + void error; + } + } + } +} + +export function getLedgerDMKSession({ dmk }: { dmk?: DeviceManagementKit } = {}) { + if (dmk) return connectLedgerDMK({ dmk }); + + defaultSessionPromise ??= createDefaultSessionPromise(); + const cachedSessionPromise = defaultSessionPromise; + + return getValidDefaultSession({ sessionPromise: cachedSessionPromise }); +} + +export function preloadLedgerDMK() { + return getDefaultDMK(); +} + +export async function disconnectLedgerDMKSession({ dmkSession }: { dmkSession?: LedgerDMKSession } = {}) { + const cachedSessionPromise = defaultSessionPromise; + let sessionToDisconnect = dmkSession; + + if (!sessionToDisconnect) { + if (!cachedSessionPromise) return; + if (defaultSessionPromise === cachedSessionPromise) defaultSessionPromise = undefined; + + try { + sessionToDisconnect = await cachedSessionPromise; + } catch { + return; + } + } + + if (cachedSessionPromise) { + try { + const cachedSession = await cachedSessionPromise; + if ( + defaultSessionPromise === cachedSessionPromise && + cachedSession.dmk === sessionToDisconnect.dmk && + cachedSession.sessionId === sessionToDisconnect.sessionId + ) { + defaultSessionPromise = undefined; + } + } catch (error) { + void error; + if (defaultSessionPromise === cachedSessionPromise) defaultSessionPromise = undefined; + } + } + + await sessionToDisconnect.dmk.disconnect({ sessionId: sessionToDisconnect.sessionId }); +} diff --git a/packages/wallet-hardware/src/ledger/helpers/executeDeviceAction.ts b/packages/wallet-hardware/src/ledger/helpers/executeDeviceAction.ts new file mode 100644 index 0000000..b520a74 --- /dev/null +++ b/packages/wallet-hardware/src/ledger/helpers/executeDeviceAction.ts @@ -0,0 +1,74 @@ +import type { + DeviceActionIntermediateValue, + DeviceActionState, + DeviceActionStatus, + ExecuteDeviceActionReturnType, + UserInteractionRequired, +} from "@ledgerhq/device-management-kit"; +import { SwapKitError } from "@swapkit/helpers"; +import { match } from "ts-pattern"; + +const DEVICE_ACTION_STATUS = { + Completed: "completed" as DeviceActionStatus.Completed, + Error: "error" as DeviceActionStatus.Error, + Stopped: "stopped" as DeviceActionStatus.Stopped, +} as const; + +export const LEDGER_USER_INTERACTION_REQUIRED = { + None: "none" as UserInteractionRequired.None, + SignTransaction: "sign-transaction" as UserInteractionRequired.SignTransaction, + VerifyAddress: "verify-address" as UserInteractionRequired.VerifyAddress, +} as const; + +export type LedgerDeviceActionState = DeviceActionState; +export type LedgerDeviceActionStateHandler = (state: LedgerDeviceActionState) => void; + +export function executeLedgerDeviceAction< + Output, + ActionError, + IntermediateValue extends DeviceActionIntermediateValue, +>({ + action, + onDeviceActionState, +}: { + action: ExecuteDeviceActionReturnType; + onDeviceActionState?: LedgerDeviceActionStateHandler; +}) { + return new Promise((resolve, reject) => { + let settled = false; + let subscription: { unsubscribe: () => void } | undefined; + + function settle(callback: () => void) { + if (settled) return; + settled = true; + subscription?.unsubscribe(); + callback(); + } + + subscription = action.observable.subscribe({ + complete: () => { + settle(() => reject(new SwapKitError("wallet_ledger_invalid_response"))); + }, + error: (error) => { + settle(() => reject(error)); + }, + next: (state) => { + try { + onDeviceActionState?.(state); + } catch (error) { + void error; + } + + match(state) + .with({ status: DEVICE_ACTION_STATUS.Completed }, ({ output }) => settle(() => resolve(output))) + .with({ status: DEVICE_ACTION_STATUS.Error }, ({ error }) => settle(() => reject(error))) + .with({ status: DEVICE_ACTION_STATUS.Stopped }, () => + settle(() => reject(new SwapKitError("wallet_ledger_connection_error"))), + ) + .otherwise(() => undefined); + }, + }); + + if (settled) subscription.unsubscribe(); + }); +} diff --git a/packages/wallet-hardware/src/ledger/helpers/getLedgerClient.ts b/packages/wallet-hardware/src/ledger/helpers/getLedgerClient.ts index 6f76bdd..96668ed 100644 --- a/packages/wallet-hardware/src/ledger/helpers/getLedgerClient.ts +++ b/packages/wallet-hardware/src/ledger/helpers/getLedgerClient.ts @@ -1,6 +1,14 @@ import type Transport from "@ledgerhq/hw-transport"; -import { Chain, type DerivationPathArray, type EVMChain, SwapKitError, WalletOption } from "@swapkit/helpers"; - +import { + Chain, + type DerivationPathArray, + type EVMChain, + EVMChains, + NetworkDerivationPath, + SwapKitError, + WalletOption, +} from "@swapkit/helpers"; +import { BitcoinLedger } from "../clients/bitcoin"; import { CosmosLedger } from "../clients/cosmos"; import { ArbitrumLedger, @@ -20,15 +28,11 @@ import { getNearLedgerClient } from "../clients/near"; import { SuiLedger } from "../clients/sui"; import { THORChainLedger } from "../clients/thorchain"; import { TronLedger } from "../clients/tron"; -import { - BitcoinCashLedger, - BitcoinLedger, - DashLedger, - DogecoinLedger, - LitecoinLedger, - ZcashLedger, -} from "../clients/utxo"; +import { BitcoinCashLedger, DashLedger, DogecoinLedger, LitecoinLedger } from "../clients/utxo"; import { XRPLedger } from "../clients/xrp"; +import { ZcashLedger } from "../clients/zcash"; +import { getLedgerDMKSession, type LedgerDMKSession } from "./dmk"; +import type { LedgerDeviceActionStateHandler } from "./executeDeviceAction"; type LedgerSignerMap = { [Chain.Arbitrum]: ReturnType; @@ -59,73 +63,102 @@ type LedgerSignerMap = { type LedgerSupportedChain = keyof LedgerSignerMap; -export const getLedgerClient = async ({ +export async function getLedgerClient({ chain, + dmkSession, derivationPath, + onDeviceActionState, + originToken, transport, }: { chain: T; + dmkSession?: LedgerDMKSession; derivationPath?: DerivationPathArray; + onDeviceActionState?: LedgerDeviceActionStateHandler; + originToken?: string; transport?: Transport; -}): Promise => { +}): Promise { + const isEVMChain = EVMChains.includes(chain as EVMChain); + + if (dmkSession && transport) { + throw new SwapKitError("wallet_ledger_invalid_params", { + message: "Provide either a Ledger DMK session or a LedgerJS transport, not both", + }); + } + + if (isEVMChain && transport) { + throw new SwapKitError("wallet_ledger_invalid_params", { + message: "Ledger EVM clients require a DMK session instead of a LedgerJS transport", + }); + } + + const resolvedDMKSession = transport ? undefined : (dmkSession ?? (await getLedgerDMKSession())); + const clientParams = { + derivationPath: derivationPath ?? NetworkDerivationPath[chain], + dmkSession: resolvedDMKSession, + onDeviceActionState, + transport, + }; const { match } = await import("ts-pattern"); - return ( - match(chain as LedgerSupportedChain) - .returnType>() - .with(Chain.THORChain, () => - Promise.resolve(new THORChainLedger(derivationPath, transport) as LedgerSignerMap[T]), - ) - .with(Chain.Cosmos, () => Promise.resolve(new CosmosLedger(derivationPath, transport) as LedgerSignerMap[T])) - .with(Chain.Bitcoin, () => Promise.resolve(BitcoinLedger(derivationPath, transport) as LedgerSignerMap[T])) - .with(Chain.BitcoinCash, () => - Promise.resolve(BitcoinCashLedger(derivationPath, transport) as LedgerSignerMap[T]), - ) - .with(Chain.Dash, () => Promise.resolve(DashLedger(derivationPath, transport) as LedgerSignerMap[T])) - .with(Chain.Dogecoin, () => Promise.resolve(DogecoinLedger(derivationPath, transport) as LedgerSignerMap[T])) - .with(Chain.Litecoin, () => Promise.resolve(LitecoinLedger(derivationPath, transport) as LedgerSignerMap[T])) - .with(Chain.Zcash, () => Promise.resolve(ZcashLedger(derivationPath, transport) as LedgerSignerMap[T])) - .with(Chain.Ripple, () => Promise.resolve(XRPLedger(derivationPath, transport) as LedgerSignerMap[T])) - .with(Chain.Tron, () => Promise.resolve(TronLedger(derivationPath, transport) as LedgerSignerMap[T])) - .with(Chain.Sui, () => Promise.resolve(SuiLedger(derivationPath, transport) as LedgerSignerMap[T])) - // @ts-expect-error - .with(Chain.Near, () => { - return Promise.resolve(getNearLedgerClient(derivationPath, transport)); - }) - .with( - Chain.Arbitrum, - Chain.Aurora, - Chain.Avalanche, - Chain.Berachain, - Chain.BinanceSmartChain, - Chain.Ethereum, - Chain.Gnosis, - Chain.Monad, - Chain.Optimism, - Chain.Polygon, - Chain.Base, - Chain.XLayer, - async () => { - const { getProvider } = await import("@swapkit/toolboxes/evm"); - const params = { derivationPath, provider: await getProvider(chain as EVMChain), transport }; + return match(chain as LedgerSupportedChain) + .returnType>() + .with(Chain.THORChain, () => Promise.resolve(new THORChainLedger(clientParams) as LedgerSignerMap[T])) + .with(Chain.Cosmos, () => Promise.resolve(new CosmosLedger(clientParams) as LedgerSignerMap[T])) + .with(Chain.Bitcoin, () => Promise.resolve(BitcoinLedger(clientParams) as LedgerSignerMap[T])) + .with(Chain.BitcoinCash, () => Promise.resolve(BitcoinCashLedger(clientParams) as LedgerSignerMap[T])) + .with(Chain.Dash, () => Promise.resolve(DashLedger(clientParams) as LedgerSignerMap[T])) + .with(Chain.Dogecoin, () => Promise.resolve(DogecoinLedger(clientParams) as LedgerSignerMap[T])) + .with(Chain.Litecoin, () => Promise.resolve(LitecoinLedger(clientParams) as LedgerSignerMap[T])) + .with(Chain.Zcash, () => Promise.resolve(ZcashLedger(clientParams) as LedgerSignerMap[T])) + .with(Chain.Ripple, () => Promise.resolve(XRPLedger(clientParams) as LedgerSignerMap[T])) + .with(Chain.Tron, () => Promise.resolve(TronLedger(clientParams) as LedgerSignerMap[T])) + .with(Chain.Sui, () => Promise.resolve(SuiLedger(clientParams) as LedgerSignerMap[T])) + .with(Chain.Near, () => Promise.resolve(getNearLedgerClient(clientParams) as LedgerSignerMap[T])) + .with( + Chain.Arbitrum, + Chain.Aurora, + Chain.Avalanche, + Chain.Berachain, + Chain.BinanceSmartChain, + Chain.Ethereum, + Chain.Gnosis, + Chain.Monad, + Chain.Optimism, + Chain.Polygon, + Chain.Base, + Chain.XLayer, + async () => { + const { getProvider } = await import("@swapkit/toolboxes/evm"); - return match(chain as Chain) - .with(Chain.BinanceSmartChain, () => BinanceSmartChainLedger(params) as LedgerSignerMap[T]) - .with(Chain.Avalanche, () => AvalancheLedger(params) as LedgerSignerMap[T]) - .with(Chain.Arbitrum, () => ArbitrumLedger(params) as LedgerSignerMap[T]) - .with(Chain.Berachain, () => BerachainLedger(params) as LedgerSignerMap[T]) - .with(Chain.Optimism, () => OptimismLedger(params) as LedgerSignerMap[T]) - .with(Chain.Polygon, () => PolygonLedger(params) as LedgerSignerMap[T]) - .with(Chain.Base, () => BaseLedger(params) as LedgerSignerMap[T]) - .with(Chain.Aurora, () => AuroraLedger(params) as LedgerSignerMap[T]) - .with(Chain.Gnosis, () => GnosisLedger(params) as LedgerSignerMap[T]) - .with(Chain.Monad, () => MonadLedger(params) as LedgerSignerMap[T]) - .with(Chain.XLayer, () => XLayerLedger(params) as LedgerSignerMap[T]) - .otherwise(() => EthereumLedger(params) as LedgerSignerMap[T]); - }, - ) - .otherwise(() => { - throw new SwapKitError("wallet_chain_not_supported", { chain, wallet: WalletOption.LEDGER }); - }) - ); -}; + if (!resolvedDMKSession) { + throw new SwapKitError("wallet_ledger_connection_error"); + } + + const params = { + derivationPath, + dmkSession: resolvedDMKSession, + onDeviceActionState, + originToken, + provider: await getProvider(chain as EVMChain), + }; + + return match(chain as Chain) + .with(Chain.BinanceSmartChain, () => BinanceSmartChainLedger(params) as LedgerSignerMap[T]) + .with(Chain.Avalanche, () => AvalancheLedger(params) as LedgerSignerMap[T]) + .with(Chain.Arbitrum, () => ArbitrumLedger(params) as LedgerSignerMap[T]) + .with(Chain.Berachain, () => BerachainLedger(params) as LedgerSignerMap[T]) + .with(Chain.Optimism, () => OptimismLedger(params) as LedgerSignerMap[T]) + .with(Chain.Polygon, () => PolygonLedger(params) as LedgerSignerMap[T]) + .with(Chain.Base, () => BaseLedger(params) as LedgerSignerMap[T]) + .with(Chain.Aurora, () => AuroraLedger(params) as LedgerSignerMap[T]) + .with(Chain.Gnosis, () => GnosisLedger(params) as LedgerSignerMap[T]) + .with(Chain.Monad, () => MonadLedger(params) as LedgerSignerMap[T]) + .with(Chain.XLayer, () => XLayerLedger(params) as LedgerSignerMap[T]) + .otherwise(() => EthereumLedger(params) as LedgerSignerMap[T]); + }, + ) + .otherwise(() => { + throw new SwapKitError("wallet_chain_not_supported", { chain, wallet: WalletOption.LEDGER }); + }); +} diff --git a/packages/wallet-hardware/src/ledger/helpers/getLedgerTransport.ts b/packages/wallet-hardware/src/ledger/helpers/getLedgerTransport.ts deleted file mode 100644 index a2aa610..0000000 --- a/packages/wallet-hardware/src/ledger/helpers/getLedgerTransport.ts +++ /dev/null @@ -1,102 +0,0 @@ -import type Transport from "@ledgerhq/hw-transport"; -import { SwapKitError } from "@swapkit/helpers"; - -const getNavigatorUsb = () => - navigator?.usb as unknown as { - getDevices: () => Promise; - requestDevice: (requestObject: any) => Promise; - removeEventListener: (event: string, callback: (e: any) => void) => void; - addEventListener: (event: string, callback: (e: any) => void) => void; - }; - -const getLedgerDevices = async (): Promise => { - const navigatorUsb = getNavigatorUsb(); - - if (typeof navigatorUsb?.getDevices !== "function") return {} as USBDevice; - const { ledgerUSBVendorId } = await import("@ledgerhq/devices"); - - const devices = await navigatorUsb?.getDevices(); - const existingDevices = devices.filter((d) => d.vendorId === ledgerUSBVendorId); - if (existingDevices.length > 0) return existingDevices[0]; - - return navigatorUsb?.requestDevice({ filters: [{ vendorId: ledgerUSBVendorId }] }); -}; - -export const getLedgerTransport = async (): Promise => { - const device = await getLedgerDevices(); - - if (!device) { - throw new SwapKitError("wallet_ledger_device_not_found"); - } - - device.opened || (await device.open()); - if (device.configuration === null) await device.selectConfiguration(1); - - try { - await device.reset(); - } catch { - // reset fails on devices that are already open - } - - const configuration = device.configuration ?? device.configurations?.[0]; - - const iface = - configuration?.interfaces.find(({ alternates }: { alternates: { interfaceClass: number }[] }) => - alternates.some(({ interfaceClass }) => interfaceClass === 0xff), - ) || - configuration?.interfaces.find(({ alternates }: { alternates: { interfaceClass: number }[] }) => - alternates.some(({ interfaceClass }) => interfaceClass === 0x03), - ); - - if (!iface) { - await device.close(); - throw new SwapKitError("wallet_ledger_connection_error"); - } - - const klass0x03 = (iface.alternates as any[])?.find( - ({ interfaceClass }: { interfaceClass: number }) => interfaceClass === 0x03, - )?.interfaceClass as number; - - const klass0xff = (iface.alternates as any[])?.find( - ({ interfaceClass }: { interfaceClass: number }) => interfaceClass === 0xff, - )?.interfaceClass as number; - - if (klass0x03 && !klass0xff) { - // -------- HID class (NEAR, ETH, SOL, etc.) -> WebHID transport -------- - const TransportWebHID = (await import("@ledgerhq/hw-transport-webhid")).default; - const supported = await TransportWebHID.isSupported(); - if (!supported) { - await device.close(); - throw new SwapKitError("wallet_ledger_webhid_not_supported"); - } - const transport = await TransportWebHID.create(); - return transport; - } - - try { - await device.claimInterface(iface.interfaceNumber); - } catch (error: unknown) { - await device.close(); - - throw new SwapKitError("wallet_ledger_connection_claimed", error); - } - - const Transport = (await import("@ledgerhq/hw-transport-webusb")).default; - const isSupported = await Transport.isSupported(); - if (!isSupported) throw new SwapKitError("wallet_ledger_webusb_not_supported"); - - const { DisconnectedDevice } = await import("@ledgerhq/errors"); - - const transport = new Transport(device, iface.interfaceNumber); - - const onDisconnect = (e: any) => { - if (device === e.device) { - getNavigatorUsb()?.removeEventListener("disconnect", onDisconnect); - - transport._emitDisconnect(new DisconnectedDevice()); - } - }; - getNavigatorUsb()?.addEventListener("disconnect", onDisconnect); - - return transport as Transport; -}; diff --git a/packages/wallet-hardware/src/ledger/helpers/index.ts b/packages/wallet-hardware/src/ledger/helpers/index.ts index 00b51b9..378fa66 100644 --- a/packages/wallet-hardware/src/ledger/helpers/index.ts +++ b/packages/wallet-hardware/src/ledger/helpers/index.ts @@ -1,3 +1,8 @@ +export { disconnectLedgerDMKSession, getLedgerDMKSession, type LedgerDMKSession, preloadLedgerDMK } from "./dmk"; +export { + executeLedgerDeviceAction, + type LedgerDeviceActionState, + type LedgerDeviceActionStateHandler, +} from "./executeDeviceAction"; export { getLedgerAddress } from "./getLedgerAddress"; export { getLedgerClient } from "./getLedgerClient"; -export { getLedgerTransport } from "./getLedgerTransport"; diff --git a/packages/wallet-hardware/src/ledger/helpers/ledgerJsDmkBridge.ts b/packages/wallet-hardware/src/ledger/helpers/ledgerJsDmkBridge.ts new file mode 100644 index 0000000..cea2a1c --- /dev/null +++ b/packages/wallet-hardware/src/ledger/helpers/ledgerJsDmkBridge.ts @@ -0,0 +1,180 @@ +import type { + Apdu, + ApduResponse, + Command, + InternalApi, + UnknownDeviceExchangeError, + UserInteractionRequired, +} from "@ledgerhq/device-management-kit"; +import Transport, { TransportError } from "@ledgerhq/hw-transport"; +import { type DerivationPathArray, SwapKitError } from "@swapkit/helpers"; + +import type { LedgerDMKSession } from "./dmk"; +import { + executeLedgerDeviceAction, + LEDGER_USER_INTERACTION_REQUIRED, + type LedgerDeviceActionStateHandler, +} from "./executeDeviceAction"; + +export { LEDGER_USER_INTERACTION_REQUIRED }; + +const STATUS_CODE_LENGTH = 2; +const TransportBase = + typeof Transport === "function" ? Transport : (Transport as unknown as { default: typeof Transport }).default; + +interface RawApduResponse { + response: Uint8Array; +} + +class RawApduCommand implements Command { + readonly name = "ledgerJsRawApdu"; + + constructor( + private readonly apdu: Apdu, + private readonly dmkModule: typeof import("@ledgerhq/device-management-kit"), + ) {} + + getApdu() { + return this.apdu; + } + + parseResponse({ data, statusCode }: ApduResponse) { + if (statusCode.length !== STATUS_CODE_LENGTH) { + return this.dmkModule.CommandResultFactory({ + error: new this.dmkModule.UnknownDeviceExchangeError(new Error("Ledger returned an invalid APDU status code")), + }); + } + + const response = new Uint8Array(data.length + statusCode.length); + response.set(data); + response.set(statusCode, data.length); + + return this.dmkModule.CommandResultFactory({ data: { response } }); + } +} + +function parseShortApdu(apdu: Buffer) { + if (apdu.length < 5) { + throw new TransportError("Ledger APDU must contain a five-byte short-APDU header", "InvalidAPDU"); + } + + const dataLength = apdu[4]; + if (dataLength === undefined || apdu.length !== dataLength + 5) { + throw new TransportError("Ledger APDU payload length does not match its short-APDU header", "InvalidAPDU"); + } + + const cla = apdu[0]; + const ins = apdu[1]; + const p1 = apdu[2]; + const p2 = apdu[3]; + if (cla === undefined || ins === undefined || p1 === undefined || p2 === undefined) { + throw new TransportError("Ledger APDU header is incomplete", "InvalidAPDU"); + } + + return { cla, data: apdu.subarray(5), ins, p1, p2 }; +} + +export class LedgerJsDmkTransport extends TransportBase { + constructor(private readonly internalApi: InternalApi) { + super(); + } + + async exchange(apdu: Buffer, { abortTimeoutMs }: { abortTimeoutMs?: number } = {}) { + const dmkModule = await import("@ledgerhq/device-management-kit"); + const { cla, data, ins, p1, p2 } = parseShortApdu(apdu); + const command = new RawApduCommand(new dmkModule.Apdu(cla, ins, p1, p2, data), dmkModule); + const result = await this.internalApi.sendCommand(command, abortTimeoutMs ?? this.exchangeTimeout); + + if (!dmkModule.isSuccessCommandResult(result)) throw result.error; + return Buffer.from(result.data.response); + } + + close() { + return Promise.resolve(); + } +} + +export interface LedgerJsClientConnection { + dmkSession?: LedgerDMKSession; + onDeviceActionState?: LedgerDeviceActionStateHandler; + transport?: Transport; +} + +export interface LedgerJsClientParams + extends LedgerJsClientConnection { + derivationPath?: Path; +} + +export function normalizeLedgerJsClientParams({ + paramsOrPath, + transport, +}: { + paramsOrPath?: LedgerJsClientParams | Path; + transport?: Transport; +}): LedgerJsClientParams { + if (paramsOrPath && typeof paramsOrPath === "object" && !Array.isArray(paramsOrPath)) return paramsOrPath; + return { derivationPath: paramsOrPath as Path | undefined, transport }; +} + +export async function runLedgerJsOperation({ + appName, + connection, + createApp, + operation, + requiredUserInteraction = LEDGER_USER_INTERACTION_REQUIRED.None, +}: { + appName: string; + connection: LedgerJsClientConnection; + createApp: (transport: Transport) => App; + operation: (app: App) => Promise | Output; + requiredUserInteraction?: UserInteractionRequired; +}) { + const { dmkSession, onDeviceActionState, transport } = connection; + + if (dmkSession && transport) { + throw new SwapKitError("wallet_ledger_invalid_params", { + message: "Provide either a Ledger DMK session or a LedgerJS transport, not both", + }); + } + + if (transport) return operation(createApp(transport)); + + if (!dmkSession) { + throw new SwapKitError("wallet_ledger_connection_error", { + message: "A Ledger DMK session or an injected LedgerJS transport is required", + }); + } + + const { CallTaskInAppDeviceAction, DmkResultFactory, UnknownDeviceExchangeError } = await import( + "@ledgerhq/device-management-kit" + ); + + const deviceAction = new CallTaskInAppDeviceAction< + { output: Output }, + UnknownDeviceExchangeError, + UserInteractionRequired + >({ + input: { + appName, + requiredUserInteraction, + skipOpenApp: false, + task: async (internalApi) => { + try { + const output = await operation(createApp(new LedgerJsDmkTransport(internalApi))); + return DmkResultFactory<{ output: Output }, UnknownDeviceExchangeError>({ data: { output } }); + } catch (error) { + return DmkResultFactory<{ output: Output }, UnknownDeviceExchangeError>({ + error: new UnknownDeviceExchangeError(error), + }); + } + }, + }, + }); + + const result = await executeLedgerDeviceAction({ + action: dmkSession.dmk.executeDeviceAction({ deviceAction, sessionId: dmkSession.sessionId }), + onDeviceActionState, + }); + + return result.output; +} diff --git a/packages/wallet-hardware/src/ledger/helpers/rawTx.ts b/packages/wallet-hardware/src/ledger/helpers/rawTx.ts new file mode 100644 index 0000000..9ef2440 --- /dev/null +++ b/packages/wallet-hardware/src/ledger/helpers/rawTx.ts @@ -0,0 +1,12 @@ +export function createCachedRawTxResolver(getRawTx: (txid: string) => Promise) { + const requests = new Map>(); + + return function resolveRawTx(txid: string) { + const cachedRequest = requests.get(txid); + if (cachedRequest) return cachedRequest; + + const request = getRawTx(txid); + requests.set(txid, request); + return request; + }; +} diff --git a/packages/wallet-hardware/src/ledger/index.ts b/packages/wallet-hardware/src/ledger/index.ts index 742aba1..81ee95e 100644 --- a/packages/wallet-hardware/src/ledger/index.ts +++ b/packages/wallet-hardware/src/ledger/index.ts @@ -1,4 +1,5 @@ import type Transport from "@ledgerhq/hw-transport"; +import { hex } from "@scure/base"; import { type AssetValue, Chain, @@ -25,34 +26,140 @@ import { getUtxoApi, type UTXOBuildTxParams, type UTXOForMultiAddressTransfer, + type UTXOType, } from "@swapkit/toolboxes/utxo"; -import type { Transaction } from "@swapkit/utxo-signer"; +import type { Transaction, ZcashTransaction } from "@swapkit/utxo-signer"; import { createWallet, getWalletSupportedChains, type HardwareExtendedPublicKeyInfo } from "@swapkit/wallet-core"; -import { getLedgerAddress, getLedgerClient } from "./helpers"; +import { match, P } from "ts-pattern"; +import { + getLedgerAddress, + getLedgerClient, + type LedgerDeviceActionStateHandler, + type LedgerDMKSession, +} from "./helpers"; +import { createCachedRawTxResolver } from "./helpers/rawTx"; + +type BitcoinLedgerClient = ReturnType; +type LegacyUTXOLedgerClient = ReturnType; +type ZcashLedgerClient = ReturnType; +type UTXOLedgerClient = BitcoinLedgerClient | LegacyUTXOLedgerClient | ZcashLedgerClient; /** * Options passed to `connectLedger` at call time. * - * When `transport` is supplied, the consumer opens and owns its lifecycle — - * wallet-hardware will use that exact instance for every per-chain Ledger - * client and will NOT recreate it on `forceReconnect`. When omitted, the - * default browser flow (WebHID / WebUSB via `navigator.usb`) is used. + * When `dmkSession` is omitted, the default browser WebHID flow discovers a + * device and reuses one session across chains. Lazy-loaded apps can call + * `preloadLedgerDMK` before the user gesture that starts connection. */ -export type ConnectLedgerOptions = { address?: string; transport?: Transport }; +export interface ConnectLedgerOptions { + address?: string; + /** Caller-owned DMK session used by every supported chain. */ + dmkSession?: LedgerDMKSession; + /** Receives DMK progress and device-interaction states. */ + onDeviceActionState?: LedgerDeviceActionStateHandler; + /** Ledger-provided token required for Transaction Checks. */ + originToken?: string; + /** @deprecated Temporary non-EVM fallback. Cannot be combined with `dmkSession`. */ + transport?: Transport; +} + +interface WalletMethodsParams extends ConnectLedgerOptions { + chain: C; + derivationPath?: DerivationPathArray; +} + +interface LedgerAccountXpubParams { + chain: UTXOChain; + path: string; + signer: UTXOLedgerClient; + xpubVersion: number; +} + +interface SignLedgerTransactionParams { + inputUtxos?: UTXOType[]; + transaction: Transaction | ZcashTransaction; +} + +const LEDGER_SUPPORTED_CHAINS_CONFIG = [ + Chain.Arbitrum, + Chain.Aurora, + Chain.Avalanche, + Chain.Base, + Chain.Berachain, + Chain.BinanceSmartChain, + Chain.Bitcoin, + Chain.BitcoinCash, + Chain.Cosmos, + Chain.Dash, + Chain.Dogecoin, + Chain.Ethereum, + Chain.Gnosis, + Chain.Litecoin, + Chain.Monad, + Chain.Near, + Chain.Optimism, + Chain.Polygon, + Chain.Ripple, + Chain.Sui, + Chain.THORChain, + Chain.XLayer, + Chain.Tron, + Chain.Zcash, +] as const; + +type LedgerSupportedChain = (typeof LEDGER_SUPPORTED_CHAINS_CONFIG)[number]; + +const LEDGER_UTXO_CHAINS = [ + Chain.BitcoinCash, + Chain.Bitcoin, + Chain.Dash, + Chain.Dogecoin, + Chain.Litecoin, + Chain.Zcash, +] as const satisfies readonly UTXOChain[]; + +const LEDGER_EVM_CHAINS = [ + Chain.Ethereum, + Chain.Avalanche, + Chain.Arbitrum, + Chain.Berachain, + Chain.Optimism, + Chain.Polygon, + Chain.BinanceSmartChain, + Chain.Base, + Chain.Aurora, + Chain.Gnosis, + Chain.Monad, + Chain.XLayer, +] as const satisfies readonly LedgerSupportedChain[]; + +type LedgerEVMChain = (typeof LEDGER_EVM_CHAINS)[number]; + +function isLedgerSupportedChain(chain: Chain): chain is LedgerSupportedChain { + return LEDGER_SUPPORTED_CHAINS_CONFIG.includes(chain as LedgerSupportedChain); +} export const ledgerWallet = createWallet({ connect: ({ addChain, supportedChains, walletType }) => async function connectLedger( chains: Chain[], derivationPath?: DerivationPathArray, - { address, transport }: ConnectLedgerOptions = {}, + { address, dmkSession, onDeviceActionState, originToken, transport }: ConnectLedgerOptions = {}, ) { const [chain] = filterSupportedChains({ chains, supportedChains, walletType }); if (!chain) return false; const resolvedPath = derivationPath ?? (NetworkDerivationPath[chain] as DerivationPathArray | undefined); - const walletMethods = await getWalletMethods({ address, chain, derivationPath: resolvedPath, transport }); + const walletMethods = await getWalletMethods({ + address, + chain, + derivationPath: resolvedPath, + dmkSession, + onDeviceActionState, + originToken, + transport, + }); addChain({ ...walletMethods, chain, walletType: WalletOption.LEDGER }); @@ -82,36 +189,11 @@ export const ledgerWallet = createWallet({ [Chain.THORChain]: true, [Chain.Tron]: true, [Chain.XLayer]: true, - // ZEC: still on bespoke signPCZT path + // ZEC transparent transfers use DSK, but route-level PCZT signing remains unsupported. }, getExtendedPublicKey: getLedgerExtendedPublicKey, name: "connectLedger", - supportedChains: [ - Chain.Arbitrum, - Chain.Aurora, - Chain.Avalanche, - Chain.Base, - Chain.Berachain, - Chain.BinanceSmartChain, - Chain.Bitcoin, - Chain.BitcoinCash, - Chain.Cosmos, - Chain.Dash, - Chain.Dogecoin, - Chain.Ethereum, - Chain.Gnosis, - Chain.Litecoin, - Chain.Monad, - Chain.Near, - Chain.Optimism, - Chain.Polygon, - Chain.Ripple, - Chain.Sui, - Chain.THORChain, - Chain.XLayer, - Chain.Tron, - Chain.Zcash, - ], + supportedChains: [...LEDGER_SUPPORTED_CHAINS_CONFIG], walletType: WalletOption.LEDGER, }); @@ -126,382 +208,520 @@ function reduceMemo(memo?: string, affiliateAddress = "t") { return removedAffiliate?.substring(0, removedAffiliate.lastIndexOf(":")); } +function getLedgerAccountXpub({ chain, path, signer, xpubVersion }: LedgerAccountXpubParams) { + const ledgerPath = match(chain) + .with(Chain.Bitcoin, Chain.Litecoin, () => path) + .with(Chain.BitcoinCash, Chain.Dash, Chain.Dogecoin, Chain.Zcash, () => path.replace(/^m\//, "")) + .exhaustive(); + + return match(chain) + .with(Chain.Bitcoin, () => (signer as BitcoinLedgerClient).getExtendedPublicKey({ path: ledgerPath })) + .with(Chain.Zcash, () => (signer as ZcashLedgerClient).getExtendedPublicKey({ path: ledgerPath, xpubVersion })) + .with(Chain.BitcoinCash, Chain.Dash, Chain.Dogecoin, Chain.Litecoin, () => + (signer as LegacyUTXOLedgerClient).getExtendedPublicKey(ledgerPath, xpubVersion), + ) + .exhaustive(); +} + +function resolveZcashInputUtxos(transaction: ZcashTransaction) { + const zcashApi = getUtxoApi(Chain.Zcash); + const getRawTx = createCachedRawTxResolver((txid) => zcashApi.getRawTx(txid)); + + return Promise.all( + Array.from({ length: transaction.inputsLength }, async (_, inputIndex) => { + const input = transaction.getInput(inputIndex); + const txid = hex.encode(input.txid); + const txHex = await getRawTx(txid); + if (!txHex) { + throw new SwapKitError("wallet_ledger_invalid_params", { + inputIndex, + reason: "Unable to resolve previous transaction hex for Ledger signing", + txid, + }); + } + + return { + hash: txid, + index: input.index, + txHex, + value: Number(input.value), + witnessUtxo: input.script ? { script: input.script, value: Number(input.value) } : undefined, + } as UTXOType; + }), + ); +} + export async function getLedgerExtendedPublicKey( chain: Chain, derivationPath?: DerivationPathArray, - { accountIndex }: { accountIndex?: number } = {}, + { + accountIndex, + dmkSession, + onDeviceActionState, + transport, + }: { + accountIndex?: number; + dmkSession?: LedgerDMKSession; + onDeviceActionState?: LedgerDeviceActionStateHandler; + transport?: Transport; + } = {}, ): Promise { if (![Chain.BitcoinCash, Chain.Bitcoin, Chain.Dash, Chain.Dogecoin, Chain.Litecoin, Chain.Zcash].includes(chain)) { throw new SwapKitError("wallet_chain_not_supported", { chain, wallet: WalletOption.LEDGER }); } const utxoChain = chain as UTXOChain; - const signer = await getLedgerClient({ chain: utxoChain, derivationPath }); + const signer = await getLedgerClient({ + chain: utxoChain, + derivationPath, + dmkSession, + onDeviceActionState, + transport, + }); if (!signer.getExtendedPublicKey) return undefined; const accountPath = getUTXOAccountPath({ accountIndex, chain: utxoChain, derivationPath }); const path = derivationPathToString(accountPath); - const ledgerPath = chain === Chain.Bitcoin || chain === Chain.Litecoin ? path : path.replace(/^m\//, ""); const xpubVersion = getNetworkForChain(utxoChain).bip32.public; try { - const xpub = await signer.getExtendedPublicKey(ledgerPath, xpubVersion); + const xpub = await getLedgerAccountXpub({ chain: utxoChain, path, signer, xpubVersion }); return { accountIndex: getUTXOAccountIndexFromPath(accountPath), path, xpub }; } finally { await (signer as { disconnect?: () => Promise }).disconnect?.(); } } -async function getWalletMethods({ +async function getUTXOWalletMethods({ address: providedAddress, chain, + dmkSession, derivationPath, + onDeviceActionState, transport, -}: ConnectLedgerOptions & { chain: Chain; derivationPath?: DerivationPathArray }) { - switch (chain) { - case Chain.BitcoinCash: - case Chain.Bitcoin: - case Chain.Dash: - case Chain.Dogecoin: - case Chain.Litecoin: - case Chain.Zcash: { - const { getUtxoToolbox } = await import("@swapkit/toolboxes/utxo"); - const utxoChain = chain as UTXOChain; - - const signer = await getLedgerClient({ chain, derivationPath, transport }); - - const address = providedAddress ?? (await getLedgerAddress({ chain, ledgerClient: signer })); - - // V3 toolbox signer: - // - BTC uses the modern `ledger-bitcoin` AppClient with native PSBT signing. - // - LTC/BCH/DOGE/DASH use the legacy `hw-app-btc` adapter that pulls - // `nonWitnessUtxo` (full prev-tx hex) out of the API PSBT. - // The Litecoin Ledger app does not support the `ledger-bitcoin` - // policy APDUs and returns CLA_NOT_SUPPORTED. - // - ZEC stays on the bespoke `signPCZT` flow for now. - let toolboxSigner: - | { getAddress: () => Promise; signTransaction: (tx: Transaction) => Promise } - | undefined; - let signAndBroadcastLegacyPsbtTransaction: ((tx: Transaction) => Promise) | undefined; - if (chain === Chain.Bitcoin) { - const { BitcoinPsbtLedger } = await import("./clients/utxo-psbt"); - const psbtClient = BitcoinPsbtLedger(derivationPath, transport); - toolboxSigner = { getAddress: psbtClient.getAddress, signTransaction: psbtClient.signTransaction }; - } else if ( - chain === Chain.BitcoinCash || - chain === Chain.Dogecoin || - chain === Chain.Dash || - chain === Chain.Litecoin - ) { - const { createLegacyPsbtSigner, signLegacyPsbtTransaction } = await import("./clients/utxo-legacy-adapter"); - toolboxSigner = createLegacyPsbtSigner({ address, chain: utxoChain, legacyClient: signer }); - signAndBroadcastLegacyPsbtTransaction = async (tx) => { - const signedTxHex = await signLegacyPsbtTransaction({ chain: utxoChain, legacyClient: signer, tx }); - return toolbox.broadcastTx(signedTxHex); - }; - } +}: WalletMethodsParams) { + const { getUtxoToolbox } = await import("@swapkit/toolboxes/utxo"); + const utxoChain = chain; - const toolbox = toolboxSigner - ? await getUtxoToolbox(utxoChain, { signer: toolboxSigner }) - : getUtxoToolbox(utxoChain); - const signAndBroadcastTransaction = signAndBroadcastLegacyPsbtTransaction ?? toolbox.signAndBroadcastTransaction; - - const transfer = async (params: UTXOBuildTxParams) => { - const feeRate = params.feeRate || (await toolbox.getFeeRates())[FeeOption.Average]; - const memo = [Chain.Bitcoin].includes(chain as typeof Chain.Bitcoin) ? params.memo : reduceMemo(params.memo); - - const { tx, inputs } = await toolbox.createTransaction({ - ...params, - feeRate, - fetchTxHex: true, - memo, - sender: address, - }); + const signer = await getLedgerClient({ chain, derivationPath, dmkSession, onDeviceActionState, transport }); - // Legacy Ledger UTXO signing returns finalized raw tx hex, so transfer - // broadcasts directly instead of routing through toolbox finalization. - const txHex = await signer.signTransaction(tx as Transaction, inputs); - const txHash = await toolbox.broadcastTx(txHex); + const address = providedAddress ?? (await getLedgerAddress({ chain, ledgerClient: signer })); - return txHash; + const { signLedgerTransaction, toolboxSigner } = await match(chain) + .with(Chain.Bitcoin, () => { + const bitcoinSigner = signer as BitcoinLedgerClient; + return { + signLedgerTransaction: ({ inputUtxos, transaction }: SignLedgerTransactionParams) => + bitcoinSigner.signTransactionHex({ inputUtxos, tx: transaction as Transaction }), + toolboxSigner: { getAddress: bitcoinSigner.getAddress, signTransaction: bitcoinSigner.signTransaction }, }; + }) + .with(Chain.BitcoinCash, Chain.Dogecoin, Chain.Dash, Chain.Litecoin, async () => { + const { createLegacyPsbtSigner, signLegacyPsbtTransaction } = await import("./clients/utxo-legacy-adapter"); + const legacySigner = signer as LegacyUTXOLedgerClient; + return { + signLedgerTransaction: ({ inputUtxos, transaction }: SignLedgerTransactionParams) => + signLegacyPsbtTransaction({ + chain: utxoChain, + inputUtxos, + legacyClient: legacySigner, + tx: transaction as Transaction, + }), + toolboxSigner: createLegacyPsbtSigner({ address, chain: utxoChain, legacyClient: legacySigner }), + }; + }) + .with(Chain.Zcash, () => { + const zcashSigner = signer as ZcashLedgerClient; + return { + signLedgerTransaction: async ({ inputUtxos, transaction }: SignLedgerTransactionParams) => { + if (!("consensusBranchId" in transaction)) { + throw new SwapKitError("wallet_ledger_method_not_supported", { + method: "signPCZT", + wallet: WalletOption.LEDGER, + }); + } + + const resolvedInputUtxos = inputUtxos ?? (await resolveZcashInputUtxos(transaction)); + return zcashSigner.signTransaction({ inputUtxos: resolvedInputUtxos, tx: transaction }); + }, + toolboxSigner: undefined, + }; + }) + .exhaustive(); + + const toolbox = toolboxSigner + ? await getUtxoToolbox(utxoChain, { signer: toolboxSigner }) + : getUtxoToolbox(utxoChain); + const signAndBroadcastTransaction = async (transaction: Transaction | ZcashTransaction) => + toolbox.broadcastTx(await signLedgerTransaction({ transaction })); + + const transfer = async (params: UTXOBuildTxParams) => { + const feeRate = params.feeRate || (await toolbox.getFeeRates())[FeeOption.Average]; + const memo = match(chain) + .with(Chain.Bitcoin, () => params.memo) + .with(Chain.BitcoinCash, Chain.Dash, Chain.Dogecoin, Chain.Litecoin, Chain.Zcash, () => reduceMemo(params.memo)) + .exhaustive(); + + const { tx, inputs } = await toolbox.createTransaction({ + ...params, + feeRate, + fetchTxHex: true, + memo, + sender: address, + }); + + const txHex = await signLedgerTransaction({ inputUtxos: inputs, transaction: tx }); + const txHash = await toolbox.broadcastTx(txHex); + + return txHash; + }; + + async function getExtendedPublicKeyInfo({ accountIndex }: { accountIndex?: number } = {}) { + if (!signer.getExtendedPublicKey) return undefined; + + const accountPath = getUTXOAccountPath({ accountIndex, chain: utxoChain, derivationPath }); + const path = derivationPathToString(accountPath); + const xpubVersion = getNetworkForChain(utxoChain).bip32.public; + const xpub = await getLedgerAccountXpub({ chain, path, signer, xpubVersion }); - async function getExtendedPublicKeyInfo({ accountIndex }: { accountIndex?: number } = {}) { - if (!signer.getExtendedPublicKey) return undefined; - - const accountPath = getUTXOAccountPath({ accountIndex, chain: utxoChain, derivationPath }); - const path = derivationPathToString(accountPath); - const ledgerPath = chain === Chain.Bitcoin || chain === Chain.Litecoin ? path : path.replace(/^m\//, ""); - const xpubVersion = getNetworkForChain(utxoChain).bip32.public; - const xpub = await signer.getExtendedPublicKey(ledgerPath, xpubVersion); + return { accountIndex: getUTXOAccountIndexFromPath(accountPath), path, xpub }; + } - return { accountIndex: getUTXOAccountIndexFromPath(accountPath), path, xpub }; - } + function getExtendedPublicKey(params: { accountIndex?: number } = {}) { + return getExtendedPublicKeyInfo(params); + } - function getExtendedPublicKey(params: { accountIndex?: number } = {}) { - return getExtendedPublicKeyInfo(params); - } + async function deriveAddressAtIndex({ + accountIndex, + index, + change = false, + }: { + accountIndex?: number; + index: number; + change?: boolean; + }) { + try { + const fullPath = getUTXOAddressPath({ accountIndex, chain: utxoChain, change, derivationPath, index }); + + const indexedSigner = await getLedgerClient({ + chain: utxoChain, + derivationPath: fullPath, + dmkSession, + onDeviceActionState, + transport, + }); + const derivedAddress = await getLedgerAddress({ chain: utxoChain, ledgerClient: indexedSigner }); - async function deriveAddressAtIndex({ - accountIndex, + return { + accountIndex: getUTXOAccountIndexFromPath(fullPath), + address: derivedAddress, + change, index, - change = false, - }: { - accountIndex?: number; - index: number; - change?: boolean; - }) { - try { - const fullPath = getUTXOAddressPath({ accountIndex, chain: utxoChain, change, derivationPath, index }); - - const indexedSigner = await getLedgerClient({ chain: utxoChain, derivationPath: fullPath, transport }); - const derivedAddress = await getLedgerAddress({ chain: utxoChain, ledgerClient: indexedSigner }); - - return { - accountIndex: getUTXOAccountIndexFromPath(fullPath), - address: derivedAddress, - change, - index, - path: derivationPathToString(fullPath), - pubkey: "", - }; - } catch { - return undefined; - } - } + path: derivationPathToString(fullPath), + pubkey: "", + }; + } catch { + return undefined; + } + } - async function deriveAddresses({ - accountIndex, - count, - startIndex = 0, - change = false, - }: { - accountIndex?: number; - count: number; - startIndex?: number; - change?: boolean; - }) { - assertDerivationIndex("count", count); - assertDerivationIndex("startIndex", startIndex); - - const addresses = await Promise.all( - Array.from({ length: count }, (_, i) => - deriveAddressAtIndex({ accountIndex, change, index: startIndex + i }), - ), - ); - - return addresses.filter((address) => !!address); - } + async function deriveAddresses({ + accountIndex, + count, + startIndex = 0, + change = false, + }: { + accountIndex?: number; + count: number; + startIndex?: number; + change?: boolean; + }) { + assertDerivationIndex("count", count); + assertDerivationIndex("startIndex", startIndex); + + const addresses = await Promise.all( + Array.from({ length: count }, (_, i) => deriveAddressAtIndex({ accountIndex, change, index: startIndex + i })), + ); + + return addresses.filter((address) => !!address); + } - const hdHelpers = createHDWalletHelpers({ - chain: utxoChain, - deriveAddress: deriveAddressAtIndex, - getBalance: toolbox.getBalance, - getUtxos: (addr: string) => getUtxoApi(utxoChain).getUtxos({ address: addr, fetchTxHex: true }), + const hdHelpers = createHDWalletHelpers({ + chain: utxoChain, + deriveAddress: deriveAddressAtIndex, + getBalance: toolbox.getBalance, + getUtxos: (addr: string) => getUtxoApi(utxoChain).getUtxos({ address: addr, fetchTxHex: true }), + }); + + async function transferFromMultipleAddresses({ + utxos, + recipient, + assetValue, + memo, + feeRate, + feeOptionKey, + changeAddress, + }: { + utxos: UTXOForMultiAddressTransfer[]; + recipient: string; + assetValue: AssetValue; + memo?: string; + feeRate?: number; + feeOptionKey?: FeeOption; + changeAddress?: string; + }) { + if (chain === Chain.Zcash) { + throw new SwapKitError("wallet_ledger_method_not_supported", { + method: "transferFromMultipleAddresses", + wallet: WalletOption.LEDGER, }); + } - async function transferFromMultipleAddresses({ - utxos, - recipient, - assetValue, - memo, - feeRate, - feeOptionKey, - changeAddress, - }: { - utxos: UTXOForMultiAddressTransfer[]; - recipient: string; - assetValue: AssetValue; - memo?: string; - feeRate?: number; - feeOptionKey?: FeeOption; - changeAddress?: string; - }) { - if (!utxos.length) { - throw new SwapKitError("wallet_ledger_invalid_params", { - message: "No UTXOs provided for multi-address transfer", - }); - } - - const txFeeRate = feeRate || (await toolbox.getFeeRates())[feeOptionKey || FeeOption.Fast]; - const memoScript = memo ? compileMemo(memo) : null; - - const targetOutputs: Array<{ address: string; value: number } | { script: Uint8Array; value: number }> = [ - { address: recipient, value: assetValue.getBaseValue("number") }, - ]; - if (memoScript) { - targetOutputs.push({ script: memoScript, value: 0 }); - } - - const basicUtxos = utxos.map(({ hash, index, value, txHex, witnessUtxo }) => ({ - hash, - index, - txHex, - value, - witnessUtxo, - })); - - const { inputs: selectedInputs, outputs } = toolbox.accumulative({ - chain: utxoChain, - feeRate: txFeeRate, - inputs: basicUtxos, - outputs: targetOutputs, - }); - - if (!(selectedInputs && outputs)) { - throw new SwapKitError("wallet_ledger_connection_error", { - message: "Insufficient balance for multi-address transfer", - }); - } - - const { Transaction } = await import("@swapkit/utxo-signer"); - const tx = new Transaction({ allowLegacyWitnessUtxo: true, version: 1 }); - const senderAddress = changeAddress || utxos[0]?.address || recipient; - - addInputsAndOutputs({ - chain: utxoChain, - compiledMemo: memoScript, - inputs: selectedInputs, - outputs, - sender: senderAddress, - tx, - }); - - const basePath = getUTXOAccountPath({ chain: utxoChain, derivationPath }); - const inputDerivationPaths = selectedInputs.map((input: { hash: string; index: number }) => { - const utxoInfo = utxos.find((u) => u.hash === input.hash && u.index === input.index); - const derivationIndex = utxoInfo?.derivationIndex ?? 0; - const isChange = utxoInfo?.isChange ?? false; - const fullPath = [...basePath, Number(isChange), derivationIndex] as unknown as DerivationPathArray; - return derivationPathToString(fullPath); - }); + if (!utxos.length) { + throw new SwapKitError("wallet_ledger_invalid_params", { + message: "No UTXOs provided for multi-address transfer", + }); + } - if (!signer.signTransactionWithMultiplePaths) { - throw new SwapKitError("wallet_ledger_method_not_supported", { method: "signTransactionWithMultiplePaths" }); - } + const txFeeRate = feeRate || (await toolbox.getFeeRates())[feeOptionKey || FeeOption.Fast]; + const memoScript = memo ? compileMemo(memo) : null; - const txHex = await signer.signTransactionWithMultiplePaths(tx, selectedInputs, inputDerivationPaths); - return toolbox.broadcastTx(txHex); - } + const targetOutputs: Array<{ address: string; value: number } | { script: Uint8Array; value: number }> = [ + { address: recipient, value: assetValue.getBaseValue("number") }, + ]; + if (memoScript) { + targetOutputs.push({ script: memoScript, value: 0 }); + } - return { - ...toolbox, - ...hdHelpers, - address, - deriveAddressAtIndex, - deriveAddresses, - getExtendedPublicKey, - getExtendedPublicKeyInfo, - signAndBroadcastTransaction, - transfer, - transferFromMultipleAddresses, - }; + const basicUtxos = utxos.map(({ hash, index, value, txHex, witnessUtxo }) => ({ + hash, + index, + txHex, + value, + witnessUtxo, + })); + + const { inputs: selectedInputs, outputs } = toolbox.accumulative({ + chain: utxoChain, + feeRate: txFeeRate, + inputs: basicUtxos, + outputs: targetOutputs, + }); + + if (!(selectedInputs && outputs)) { + throw new SwapKitError("wallet_ledger_connection_error", { + message: "Insufficient balance for multi-address transfer", + }); } - case Chain.Ethereum: - case Chain.Avalanche: - case Chain.Arbitrum: - case Chain.Berachain: - case Chain.Optimism: - case Chain.Polygon: - case Chain.BinanceSmartChain: - case Chain.Base: - case Chain.Aurora: - case Chain.Gnosis: - case Chain.Monad: - case Chain.XLayer: { - const { getEvmToolboxAsync } = await import("@swapkit/toolboxes/evm"); - const signer = await getLedgerClient({ chain, derivationPath, transport }); - const address = await getLedgerAddress({ chain, ledgerClient: signer }); - const toolbox = await getEvmToolboxAsync(chain, { signer }); - - return { ...toolbox, address }; + const { Transaction } = await import("@swapkit/utxo-signer"); + const tx = new Transaction({ allowLegacyWitnessUtxo: true, version: 1 }); + const senderAddress = changeAddress || utxos[0]?.address || recipient; + + addInputsAndOutputs({ + chain: utxoChain, + compiledMemo: memoScript, + inputs: selectedInputs, + outputs, + sender: senderAddress, + tx, + }); + + const basePath = getUTXOAccountPath({ chain: utxoChain, derivationPath }); + const inputDerivationPaths = selectedInputs.map((input: { hash: string; index: number }) => { + const utxoInfo = utxos.find((u) => u.hash === input.hash && u.index === input.index); + const derivationIndex = utxoInfo?.derivationIndex ?? 0; + const isChange = utxoInfo?.isChange ?? false; + const fullPath = [...basePath, Number(isChange), derivationIndex] as unknown as DerivationPathArray; + return derivationPathToString(fullPath); + }); + + if (!signer.signTransactionWithMultiplePaths) { + throw new SwapKitError("wallet_ledger_method_not_supported", { method: "signTransactionWithMultiplePaths" }); } - case Chain.Cosmos: { - const { createSigningStargateClient, getMsgSendDenom, getCosmosToolbox } = await import( - "@swapkit/toolboxes/cosmos" - ); - const signer = await getLedgerClient({ chain, derivationPath, transport }); - const address = await getLedgerAddress({ chain, ledgerClient: signer }); - const toolbox = await getCosmosToolbox(Chain.Cosmos, { signer }); - - const transfer = async ({ assetValue, recipient, memo }: GenericTransferParams) => { - if (!assetValue) throw new SwapKitError("wallet_ledger_invalid_asset"); - - const sendCoinsMessage = { - amount: [ - { - amount: assetValue.getBaseValue("string"), - denom: getMsgSendDenom(`u${assetValue.symbol}`).toLowerCase(), - }, - ], - fromAddress: address, - toAddress: recipient, - }; - - const rpcUrl = await getRPCUrl(chain); - const signingClient = await createSigningStargateClient(rpcUrl, signer, "0.007uatom"); - - const { transactionHash } = await signingClient.signAndBroadcast( - address, - [{ typeUrl: "/cosmos.bank.v1beta1.MsgSend", value: sendCoinsMessage }], - 2, - memo, - ); - - return transactionHash; - }; + const txHex = await match(chain) + .with(Chain.Bitcoin, () => + (signer as BitcoinLedgerClient).signTransactionWithMultiplePaths({ + derivationPaths: inputDerivationPaths, + inputUtxos: selectedInputs, + tx, + }), + ) + .with(Chain.BitcoinCash, Chain.Dash, Chain.Dogecoin, Chain.Litecoin, () => + (signer as LegacyUTXOLedgerClient).signTransactionWithMultiplePaths(tx, selectedInputs, inputDerivationPaths), + ) + .exhaustive(); + return toolbox.broadcastTx(txHex); + } - return { ...toolbox, address, transfer }; - } + return { + ...toolbox, + ...hdHelpers, + address, + deriveAddressAtIndex, + deriveAddresses, + getExtendedPublicKey, + getExtendedPublicKeyInfo, + signAndBroadcastTransaction, + transfer, + transferFromMultipleAddresses, + }; +} - case Chain.THORChain: { - const signer = await getLedgerClient({ chain, derivationPath, transport }); - const { getCosmosToolbox } = await import("@swapkit/toolboxes/cosmos"); - const toolbox = getCosmosToolbox(chain, { signer }); - const address = await getLedgerAddress({ chain, ledgerClient: signer }); - const { sign: signMessage } = signer; +async function getEVMWalletMethods({ + chain, + derivationPath, + dmkSession, + onDeviceActionState, + originToken, + transport, +}: WalletMethodsParams) { + const signer = await getLedgerClient({ + chain, + derivationPath, + dmkSession, + onDeviceActionState, + originToken, + transport, + }); + const { getEvmToolboxAsync } = await import("@swapkit/toolboxes/evm"); + const address = await getLedgerAddress({ chain, ledgerClient: signer }); + const toolbox = await getEvmToolboxAsync(chain, { signer }); + + return { ...toolbox, address }; +} - return { ...toolbox, address, signMessage }; - } +async function getCosmosWalletMethods({ + chain, + derivationPath, + dmkSession, + onDeviceActionState, + transport, +}: WalletMethodsParams) { + const { createSigningStargateClient, getMsgSendDenom, getCosmosToolbox } = await import("@swapkit/toolboxes/cosmos"); + const signer = await getLedgerClient({ chain, derivationPath, dmkSession, onDeviceActionState, transport }); + const address = await getLedgerAddress({ chain, ledgerClient: signer }); + const toolbox = await getCosmosToolbox(Chain.Cosmos, { signer }); + + const transfer = async ({ assetValue, recipient, memo }: GenericTransferParams) => { + if (!assetValue) throw new SwapKitError("wallet_ledger_invalid_asset"); + + const sendCoinsMessage = { + amount: [ + { amount: assetValue.getBaseValue("string"), denom: getMsgSendDenom(`u${assetValue.symbol}`).toLowerCase() }, + ], + fromAddress: address, + toAddress: recipient, + }; + + const rpcUrl = await getRPCUrl(chain); + const signingClient = await createSigningStargateClient(rpcUrl, signer, "0.007uatom"); + + const { transactionHash } = await signingClient.signAndBroadcast( + address, + [{ typeUrl: "/cosmos.bank.v1beta1.MsgSend", value: sendCoinsMessage }], + 2, + memo, + ); + + return transactionHash; + }; + + return { ...toolbox, address, transfer }; +} - case Chain.Near: { - const { getNearToolbox } = await import("@swapkit/toolboxes/near"); - const signer = await getLedgerClient({ chain, derivationPath, transport }); - const accountId = await signer.getAddress(); - const toolbox = getNearToolbox({ signer }); +async function getTHORChainWalletMethods({ + chain, + derivationPath, + dmkSession, + onDeviceActionState, + transport, +}: WalletMethodsParams) { + const signer = await getLedgerClient({ chain, derivationPath, dmkSession, onDeviceActionState, transport }); + const { getCosmosToolbox } = await import("@swapkit/toolboxes/cosmos"); + const toolbox = getCosmosToolbox(chain, { signer }); + const address = await getLedgerAddress({ chain, ledgerClient: signer }); + const { sign: signMessage } = signer; + + return { ...toolbox, address, signMessage }; +} - return { ...toolbox, address: accountId }; - } +async function getNearWalletMethods({ + chain, + derivationPath, + dmkSession, + onDeviceActionState, + transport, +}: WalletMethodsParams) { + const { getNearToolbox } = await import("@swapkit/toolboxes/near"); + const signer = await getLedgerClient({ chain, derivationPath, dmkSession, onDeviceActionState, transport }); + const accountId = await signer.getAddress(); + const toolbox = getNearToolbox({ signer }); - case Chain.Ripple: { - const { getRippleToolbox } = await import("@swapkit/toolboxes/ripple"); - const signer = await getLedgerClient({ chain, derivationPath, transport }); - const address = signer.getAddress(); - const toolbox = getRippleToolbox({ signer }); + return { ...toolbox, address: accountId }; +} - return { ...toolbox, address }; - } +async function getRippleWalletMethods({ + chain, + derivationPath, + dmkSession, + onDeviceActionState, + transport, +}: WalletMethodsParams) { + const { getRippleToolbox } = await import("@swapkit/toolboxes/ripple"); + const signer = await getLedgerClient({ chain, derivationPath, dmkSession, onDeviceActionState, transport }); + const address = signer.getAddress(); + const toolbox = getRippleToolbox({ signer }); - case Chain.Tron: { - const { getTronToolbox } = await import("@swapkit/toolboxes/tron"); - const signer = await getLedgerClient({ chain, derivationPath, transport }); - const address = await getLedgerAddress({ chain, ledgerClient: signer }); - const toolbox = getTronToolbox({ signer }); + return { ...toolbox, address }; +} - return { ...toolbox, address }; - } +async function getTronWalletMethods({ + chain, + derivationPath, + dmkSession, + onDeviceActionState, + transport, +}: WalletMethodsParams) { + const { getTronToolbox } = await import("@swapkit/toolboxes/tron"); + const signer = await getLedgerClient({ chain, derivationPath, dmkSession, onDeviceActionState, transport }); + const address = await getLedgerAddress({ chain, ledgerClient: signer }); + const toolbox = getTronToolbox({ signer }); - case Chain.Sui: { - const { getSuiToolbox } = await import("@swapkit/toolboxes/sui"); - const signer = await getLedgerClient({ chain, derivationPath, transport }); - const address = await getLedgerAddress({ chain, ledgerClient: signer }); - const toolbox = getSuiToolbox({ signer }); + return { ...toolbox, address }; +} - return { ...toolbox, address }; - } +async function getSuiWalletMethods({ + chain, + derivationPath, + dmkSession, + onDeviceActionState, + transport, +}: WalletMethodsParams) { + const { getSuiToolbox } = await import("@swapkit/toolboxes/sui"); + const signer = await getLedgerClient({ chain, derivationPath, dmkSession, onDeviceActionState, transport }); + const address = await getLedgerAddress({ chain, ledgerClient: signer }); + const toolbox = getSuiToolbox({ signer }); + + return { ...toolbox, address }; +} - default: - throw new SwapKitError("wallet_ledger_chain_not_supported", { chain }); +function getWalletMethods(params: WalletMethodsParams) { + if (!isLedgerSupportedChain(params.chain)) { + throw new SwapKitError("wallet_ledger_chain_not_supported", { chain: params.chain }); } + + return match(params.chain) + .with(P.union(...LEDGER_UTXO_CHAINS), (chain) => getUTXOWalletMethods({ ...params, chain })) + .with(P.union(...LEDGER_EVM_CHAINS), (chain) => getEVMWalletMethods({ ...params, chain })) + .with(Chain.Cosmos, (chain) => getCosmosWalletMethods({ ...params, chain })) + .with(Chain.THORChain, (chain) => getTHORChainWalletMethods({ ...params, chain })) + .with(Chain.Near, (chain) => getNearWalletMethods({ ...params, chain })) + .with(Chain.Ripple, (chain) => getRippleWalletMethods({ ...params, chain })) + .with(Chain.Tron, (chain) => getTronWalletMethods({ ...params, chain })) + .with(Chain.Sui, (chain) => getSuiWalletMethods({ ...params, chain })) + .exhaustive(); } + +export type { LedgerDeviceActionState, LedgerDeviceActionStateHandler, LedgerDMKSession } from "./helpers"; +export { disconnectLedgerDMKSession, getLedgerDMKSession, preloadLedgerDMK } from "./helpers"; diff --git a/packages/wallet-hardware/src/ledger/interfaces/CosmosLedgerInterface.ts b/packages/wallet-hardware/src/ledger/interfaces/CosmosLedgerInterface.ts deleted file mode 100644 index eab50d3..0000000 --- a/packages/wallet-hardware/src/ledger/interfaces/CosmosLedgerInterface.ts +++ /dev/null @@ -1,67 +0,0 @@ -import type Transport from "@ledgerhq/hw-transport"; -import { type DerivationPathArray, LedgerErrorCode, NetworkDerivationPath, SwapKitError } from "@swapkit/helpers"; - -import { THORChainApp } from "../clients/thorchain/lib"; -import { getLedgerTransport } from "../helpers/getLedgerTransport"; - -export abstract class CosmosLedgerInterface { - ledgerTimeout = 50000; - derivationPath: DerivationPathArray | string = NetworkDerivationPath.GAIA; - transport: any; - ledgerApp: any; - chain: "thor" | "cosmos" = "thor"; - - private readonly injectedTransport?: Transport; - - constructor(transport?: Transport) { - this.injectedTransport = transport; - if (transport) this.transport = transport; - } - - checkOrCreateTransportAndLedger = async (forceReconnect = false) => { - if (!forceReconnect && this.transport && this.ledgerApp) return; - - try { - // Consumer owns the lifecycle of an injected transport, so forceReconnect - // only refreshes ledgerApp — the transport itself stays as passed in. - const needsNewTransport = !this.transport || (forceReconnect && !this.injectedTransport); - if (needsNewTransport) { - this.transport = this.injectedTransport ?? (await getLedgerTransport()); - } - - switch (this.chain) { - case "thor": { - this.ledgerApp = forceReconnect || !this.ledgerApp ? new THORChainApp(this.transport) : this.ledgerApp; - - break; - } - - case "cosmos": { - const CosmosApp = (await import("@ledgerhq/hw-app-cosmos")).default; - this.ledgerApp = forceReconnect || !this.ledgerApp ? new CosmosApp(this.transport) : this.ledgerApp; - } - } - - return this.ledgerApp; - } catch (error: unknown) { - throw new SwapKitError("wallet_ledger_connection_error", error); - } - }; - - validateResponse = (errorCode: LedgerErrorCode, message?: string) => { - switch (errorCode) { - case LedgerErrorCode.NoError: - return; - - case LedgerErrorCode.LockedDevice: - throw new SwapKitError("wallet_ledger_device_locked", { message: `Ledger is locked: ${message}` }); - - case LedgerErrorCode.TC_NotFound: - throw new SwapKitError("wallet_ledger_device_not_found"); - - default: { - break; - } - } - }; -} diff --git a/packages/wallet-hardware/src/ledger/types.ts b/packages/wallet-hardware/src/ledger/types.ts index ab56cac..b6effa9 100644 --- a/packages/wallet-hardware/src/ledger/types.ts +++ b/packages/wallet-hardware/src/ledger/types.ts @@ -1,42 +1,45 @@ +import type { BitcoinLedger } from "./clients/bitcoin"; import type { CosmosLedger } from "./clients/cosmos"; import type { ArbitrumLedger, AuroraLedger, AvalancheLedger, BaseLedger, + BerachainLedger, BinanceSmartChainLedger, EthereumLedger, GnosisLedger, + MonadLedger, OptimismLedger, PolygonLedger, + XLayerLedger, } from "./clients/evm"; import type { SuiLedger } from "./clients/sui"; import type { THORChainLedger } from "./clients/thorchain"; import type { TronLedger } from "./clients/tron"; -import type { BitcoinCashLedger, BitcoinLedger, DogecoinLedger, LitecoinLedger } from "./clients/utxo"; +import type { BitcoinCashLedger, DashLedger, DogecoinLedger, LitecoinLedger } from "./clients/utxo"; +import type { ZcashLedger } from "./clients/zcash"; export type UTXOLedgerClients = | ReturnType | ReturnType + | ReturnType | ReturnType - | ReturnType; + | ReturnType + | ReturnType; export type CosmosLedgerClients = CosmosLedger | THORChainLedger; export type EVMLedgerClients = | ReturnType | ReturnType | ReturnType | ReturnType + | ReturnType | ReturnType | ReturnType | ReturnType + | ReturnType | ReturnType - | ReturnType; + | ReturnType + | ReturnType; export type TronLedgerClient = ReturnType; export type SuiLedgerClient = ReturnType; - -export type GetAddressAndPubKeyResponse = { - bech32_address: string; - compressed_pk: any; - error_message: string; - return_code: number; -}; diff --git a/packages/wallet-hardware/src/trezor/index.ts b/packages/wallet-hardware/src/trezor/index.ts index 544a31e..c5e6a71 100644 --- a/packages/wallet-hardware/src/trezor/index.ts +++ b/packages/wallet-hardware/src/trezor/index.ts @@ -23,7 +23,6 @@ import { type UTXOType, } from "@swapkit/toolboxes/utxo"; import type { BTCNetwork, PCZT, Transaction, ZcashPSBT, ZcashTransaction } from "@swapkit/utxo-signer"; -import { BCHSigHash, NETWORKS, ZcashVersionGroupId } from "@swapkit/utxo-signer"; import { createWallet, getWalletSupportedChains, type HardwareExtendedPublicKeyInfo } from "@swapkit/wallet-core"; type TrezorBip32Derivation = [Uint8Array, { fingerprint: number; path: number[] }]; @@ -49,15 +48,12 @@ const DEFAULT_TREZOR_CORE_MODE: TrezorCoreMode = "auto"; const DEFAULT_TREZOR_MANIFEST = { appName: "SwapKit", appUrl: "https://swapkit.dev", email: "support@swapkit.dev" }; const DEFAULT_TREZOR_TRANSPORTS = ["WebUsbTransport" as const]; const TREZOR_KEEP_SESSION_PARAMS = { keepSession: true } as const; +const BITCOIN_CASH_SIGHASH_ALL = 0x41; const trezorXpubCache = new Map(); let trezorSessionDispose: Promise | undefined; -const EXTENDED_KEY_VERSION_CANDIDATES = [ - NETWORKS.bitcoin.bip32, - NETWORKS.bitcoinCash.bip32, - NETWORKS.dash.bip32, - NETWORKS.dogecoin.bip32, - NETWORKS.litecoin.bip32, -]; +const EXTENDED_KEY_VERSION_CANDIDATES = ( + [Chain.Bitcoin, Chain.BitcoinCash, Chain.Dash, Chain.Dogecoin, Chain.Litecoin] as const satisfies readonly UTXOChain[] +).map((chain) => getNetworkForChain(chain).bip32); async function disconnectTrezorSession() { trezorXpubCache.clear(); @@ -260,7 +256,7 @@ export function normalizeTrezorSignature(signatureHex: string, chain: Chain) { const derLength = signature[1] !== undefined ? signature[1] + 2 : undefined; if (derLength !== undefined && signature.length === derLength) { - return new Uint8Array([...signature, chain === Chain.BitcoinCash ? BCHSigHash.ALL : 0x01]); + return new Uint8Array([...signature, chain === Chain.BitcoinCash ? BITCOIN_CASH_SIGHASH_ALL : 0x01]); } return new Uint8Array(signature); @@ -325,7 +321,7 @@ async function buildPCZTOutputsForTrezor(pczt: PCZT, address_n: number[], myAddr async function decodeOutputAddress(script: Uint8Array): Promise { try { - const { OutScript, Address } = await import("@swapkit/utxo-signer"); + const { OutScript, Address, NETWORKS } = await import("@swapkit/utxo-signer"); const decoded = OutScript.decode(script); if (decoded.type === "pkh" || decoded.type === "pk") { return Address(NETWORKS.zcash).encode(decoded); @@ -375,11 +371,23 @@ function buildZcashTxInputsForTrezor( return inputs; } -function buildZcashTxOutputsForTrezor(tx: ZcashTransaction, address_n: number[], myAddress: string, chain: Chain) { +function buildZcashTxOutputsForTrezor({ + addressN, + chain, + myAddress, + network, + tx, +}: { + addressN: number[]; + chain: Chain; + myAddress: string; + network: BTCNetwork; + tx: ZcashTransaction; +}) { const outputs = []; for (let i = 0; i < tx.outputsLength; i++) { const output = tx.getOutput(i); - const outputAddress = tx.getOutputAddress(i, NETWORKS.zcash); + const outputAddress = tx.getOutputAddress(i, network); const script = output.script; if (output.amount === 0n && script?.length > 0 && script[0] === 0x6a) { @@ -399,7 +407,7 @@ function buildZcashTxOutputsForTrezor(tx: ZcashTransaction, address_n: number[], } const isChangeAddress = outputAddress === myAddress; - const outputParam = isChangeAddress || !outputAddress ? { address_n } : { address: outputAddress }; + const outputParam = isChangeAddress || !outputAddress ? { address_n: addressN } : { address: outputAddress }; outputs.push({ ...outputParam, amount: output.amount?.toString() || "0", script_type: "PAYTOADDRESS" as const }); } return outputs; @@ -582,10 +590,17 @@ async function getTrezorWallet({ const signZcashTransactionWithSerializedTx = async (tx: ZcashTransaction) => { const TrezorConnect = (await import("@trezor/connect-web")).default; const { hex: hexEncode } = await import("@scure/base"); + const { NETWORKS, ZcashVersionGroupId } = await import("@swapkit/utxo-signer"); const address_n = hardenDerivationPath(derivationPath); const inputs = buildZcashTxInputsForTrezor(tx, [], address_n, hexEncode); - const outputs = buildZcashTxOutputsForTrezor(tx, address_n, address, chain); + const outputs = buildZcashTxOutputsForTrezor({ + addressN: address_n, + chain, + myAddress: address, + network: NETWORKS.zcash, + tx, + }); const result = await TrezorConnect.signTransaction({ branchId: tx.consensusBranchId, diff --git a/packages/wallet-hardware/test/ledger-bitcoin-dsk.test.ts b/packages/wallet-hardware/test/ledger-bitcoin-dsk.test.ts new file mode 100644 index 0000000..832dae6 --- /dev/null +++ b/packages/wallet-hardware/test/ledger-bitcoin-dsk.test.ts @@ -0,0 +1,262 @@ +import { beforeEach, describe, expect, it, mock } from "bun:test"; +import { DeviceActionStatus, type DeviceManagementKit } from "@ledgerhq/device-management-kit"; +import { hex } from "@scure/base"; +import { HDKey } from "@scure/bip32"; +import { p2pkh, p2sh, p2tr, p2wpkh, Script, Transaction } from "@swapkit/utxo-signer"; +import { of } from "rxjs"; + +const accountKey = HDKey.fromMasterSeed(new Uint8Array(32).fill(7)).derive("m/84'/0'/0'"); +const accountXpub = accountKey.publicExtendedKey; +const walletAddressCalls: Array<{ + addressIndex: number; + options: { change?: boolean; checkOnDevice?: boolean } | undefined; + wallet: { derivationPath: string; template: string }; +}> = []; +const xpubCalls: Array<{ options?: { checkOnDevice?: boolean }; path: string }> = []; +const signCalls: Array<{ psbt: Uint8Array; wallet: { derivationPath: string; template: string } }> = []; +let fingerprintCalls = 0; +let fingerprintFailuresRemaining = 0; +let signedRaw = ""; +let xpubFailuresRemaining = 0; + +function deviceAction(output: Output) { + return { + cancel: mock(() => {}), + observable: of( + { intermediateValue: { requiredUserInteraction: "confirm-on-device" }, status: DeviceActionStatus.Pending }, + { output, status: DeviceActionStatus.Completed }, + ), + }; +} + +function failedDeviceAction(error: Error) { + return { cancel: mock(() => {}), observable: of({ error, status: DeviceActionStatus.Error }) }; +} + +mock.module("@ledgerhq/device-signer-kit-bitcoin", () => ({ + DefaultDescriptorTemplate: { + LEGACY: "pkh(@0/**)", + NATIVE_SEGWIT: "wpkh(@0/**)", + NESTED_SEGWIT: "sh(wpkh(@0/**))", + TAPROOT: "tr(@0/**)", + }, + DefaultWallet: class MockDefaultWallet { + constructor( + readonly derivationPath: string, + readonly template: string, + ) {} + }, + SignerBtcBuilder: class MockSignerBtcBuilder { + build() { + return { + getExtendedPublicKey: (path: string, options?: { checkOnDevice?: boolean }) => { + xpubCalls.push({ options, path }); + if (xpubFailuresRemaining > 0) { + xpubFailuresRemaining -= 1; + return failedDeviceAction(new Error("Transient account xpub failure")); + } + return deviceAction({ extendedPublicKey: accountXpub }); + }, + getMasterFingerprint: () => { + fingerprintCalls += 1; + if (fingerprintFailuresRemaining > 0) { + fingerprintFailuresRemaining -= 1; + return failedDeviceAction(new Error("Transient master fingerprint failure")); + } + return deviceAction({ masterFingerprint: Uint8Array.of(0xde, 0xad, 0xbe, 0xef) }); + }, + getWalletAddress: ( + wallet: { derivationPath: string; template: string }, + addressIndex: number, + options?: { change?: boolean; checkOnDevice?: boolean }, + ) => { + walletAddressCalls.push({ addressIndex, options, wallet }); + return deviceAction({ address: "bc1qledgerdsk" }); + }, + signTransaction: (wallet: { derivationPath: string; template: string }, psbt: Uint8Array) => { + signCalls.push({ psbt, wallet }); + return deviceAction(`0x${signedRaw}`); + }, + }; + } + }, +})); + +import { BitcoinLedger } from "../src/ledger/clients/bitcoin"; + +const dmkSession = { dmk: { id: "bitcoin-dmk" } as unknown as DeviceManagementKit, sessionId: "btc-session" }; + +function makeTransaction(inputCount = 2) { + const transaction = new Transaction(); + const leafPublicKey = accountKey.derive("m/0/0").publicKey; + if (!leafPublicKey) throw new Error("Fixture account did not derive a public key"); + const script = p2wpkh(leafPublicKey).script; + + for (let index = 0; index < inputCount; index += 1) { + transaction.addInput({ index, txid: new Uint8Array(32).fill(index + 1), witnessUtxo: { amount: 10_000n, script } }); + } + transaction.addOutput({ amount: BigInt(9_000 * inputCount), script }); + return transaction; +} + +function signedTransactionRaw(transaction: Transaction) { + const publicKey = accountKey.derive("m/0/0").publicKey; + if (!publicKey) throw new Error("Fixture account did not derive a public key"); + const signed = transaction.clone(); + for (let inputIndex = 0; inputIndex < signed.inputsLength; inputIndex += 1) { + signed.updateInput(inputIndex, { finalScriptWitness: [Uint8Array.of(0x30, 1, 1), publicKey] }); + } + return hex.encode(signed.extract()); +} + +describe("Ledger Bitcoin Device Signer Kit client", () => { + beforeEach(() => { + walletAddressCalls.length = 0; + xpubCalls.length = 0; + signCalls.length = 0; + fingerprintCalls = 0; + fingerprintFailuresRemaining = 0; + signedRaw = ""; + xpubFailuresRemaining = 0; + }); + + it("normalizes the address path and maps purpose 86 to a taproot wallet", async () => { + const states: string[] = []; + const client = BitcoinLedger({ + derivationPath: "m/86'/0'/2'/1/7", + dmkSession, + onDeviceActionState: ({ status }) => states.push(status), + }); + + const address = await client.getAddress(); + + expect(address).toBe("bc1qledgerdsk"); + expect(walletAddressCalls).toEqual([ + { addressIndex: 7, options: { change: true }, wallet: { derivationPath: "86'/0'/2'", template: "tr(@0/**)" } }, + ]); + expect(states).toEqual(["pending", "completed"]); + }); + + it("adds exact per-input key origins and returns a parsed raw transaction", async () => { + const transaction = makeTransaction(); + signedRaw = signedTransactionRaw(transaction); + const client = BitcoinLedger({ derivationPath: "m/84'/0'/0'/0/0", dmkSession }); + + const signed = await client.signTransaction(transaction); + signed.finalize(); + + expect(hex.encode(signed.extract())).toBe(signedRaw); + expect(signed.fee).toBe(2_000n); + expect(signCalls).toHaveLength(1); + expect(signCalls[0]?.wallet).toEqual({ derivationPath: "84'/0'/0'", template: "wpkh(@0/**)" }); + const signedPsbt = Transaction.fromPSBT(signCalls[0]?.psbt ?? new Uint8Array()); + expect(signedPsbt.getInput(0).bip32Derivation).toEqual([ + [ + accountKey.derive("m/0/0").publicKey, + { fingerprint: 0xdeadbeef, path: [0x80000054, 0x80000000, 0x80000000, 0, 0] }, + ], + ]); + expect(signedPsbt.getInput(1).bip32Derivation).toEqual([ + [ + accountKey.derive("m/0/0").publicKey, + { fingerprint: 0xdeadbeef, path: [0x80000054, 0x80000000, 0x80000000, 0, 0] }, + ], + ]); + expect(xpubCalls).toEqual([{ options: undefined, path: "84'/0'/0'" }]); + }); + + it("retries account xpub metadata after a transient device action failure", async () => { + const transaction = makeTransaction(); + signedRaw = signedTransactionRaw(transaction); + xpubFailuresRemaining = 1; + const client = BitcoinLedger({ derivationPath: "m/84'/0'/0'/0/0", dmkSession }); + + await expect(client.signTransaction(transaction)).rejects.toThrow("Transient account xpub failure"); + await client.signTransaction(transaction); + + expect(xpubCalls).toHaveLength(2); + expect(fingerprintCalls).toBe(1); + expect(signCalls).toHaveLength(1); + }); + + it("retries master fingerprint metadata after a transient device action failure", async () => { + const transaction = makeTransaction(); + signedRaw = signedTransactionRaw(transaction); + fingerprintFailuresRemaining = 1; + const client = BitcoinLedger({ derivationPath: "m/84'/0'/0'/0/0", dmkSession }); + + await expect(client.signTransaction(transaction)).rejects.toThrow("Transient master fingerprint failure"); + await client.signTransaction(transaction); + + expect(xpubCalls).toHaveLength(1); + expect(fingerprintCalls).toBe(2); + expect(signCalls).toHaveLength(1); + }); + + it("supports different leaf paths in one account and rejects account mixing", async () => { + const transaction = makeTransaction(); + signedRaw = signedTransactionRaw(transaction); + const client = BitcoinLedger({ derivationPath: "84'/0'/0'/0/0", dmkSession }); + + const raw = await client.signTransactionWithMultiplePaths({ + derivationPaths: ["m/84'/0'/0'/0/3", "84'/0'/0'/1/9"], + tx: transaction, + }); + + expect(raw).toBe(signedRaw); + const signedPsbt = Transaction.fromPSBT(signCalls[0]?.psbt ?? new Uint8Array()); + expect(signedPsbt.getInput(0).bip32Derivation?.[0]?.[1].path).toEqual([0x80000054, 0x80000000, 0x80000000, 0, 3]); + expect(signedPsbt.getInput(1).bip32Derivation?.[0]?.[1].path).toEqual([0x80000054, 0x80000000, 0x80000000, 1, 9]); + + await expect( + client.signTransactionWithMultiplePaths({ derivationPaths: ["84'/0'/0'/0/3", "84'/0'/1'/0/0"], tx: transaction }), + ).rejects.toThrow("wallet_ledger_invalid_params"); + }); + + it("restores finalizable signatures for purposes 44, 49, and 86", async () => { + const publicKey = accountKey.derive("m/0/0").publicKey; + if (!publicKey) throw new Error("Fixture account did not derive a public key"); + const cases = [ + { + finalizeRaw: (raw: Transaction) => + raw.updateInput(0, { finalScriptSig: Script.encode([Uint8Array.of(0x30, 1, 1), publicKey]) }), + path: "44'/0'/0'/0/0", + script: p2pkh(publicKey).script, + }, + { + finalizeRaw: (raw: Transaction) => + raw.updateInput(0, { + finalScriptSig: Script.encode([p2wpkh(publicKey).script]), + finalScriptWitness: [Uint8Array.of(0x30, 1, 1), publicKey], + }), + path: "49'/0'/0'/0/0", + script: p2sh(p2wpkh(publicKey)).script, + }, + { + finalizeRaw: (raw: Transaction) => raw.updateInput(0, { finalScriptWitness: [new Uint8Array(64).fill(6)] }), + path: "86'/0'/0'/0/0", + script: p2tr(publicKey.slice(1)).script, + }, + ]; + + for (const testCase of cases) { + const transaction = new Transaction({ allowLegacyWitnessUtxo: true }); + transaction.addInput({ + index: 0, + txid: new Uint8Array(32).fill(4), + witnessUtxo: { amount: 10_000n, script: testCase.script }, + }); + transaction.addOutput({ amount: 9_000n, script: testCase.script }); + const raw = transaction.clone(); + testCase.finalizeRaw(raw); + signedRaw = hex.encode(raw.extract()); + + const client = BitcoinLedger({ derivationPath: testCase.path, dmkSession }); + const signed = await client.signTransaction(transaction); + signed.finalize(); + + expect(hex.encode(signed.extract())).toBe(signedRaw); + expect(signed.fee).toBe(1_000n); + } + }); +}); diff --git a/packages/wallet-hardware/test/ledger-cosmos-dsk.test.ts b/packages/wallet-hardware/test/ledger-cosmos-dsk.test.ts new file mode 100644 index 0000000..f5b622c --- /dev/null +++ b/packages/wallet-hardware/test/ledger-cosmos-dsk.test.ts @@ -0,0 +1,101 @@ +import { beforeEach, describe, expect, it, mock } from "bun:test"; +import { encodeSecp256k1Signature, type StdSignDoc, serializeSignDoc } from "@cosmjs/amino"; +import { DeviceActionStatus, type DeviceManagementKit } from "@ledgerhq/device-management-kit"; +import { of } from "rxjs"; + +const publicKey = Uint8Array.from([2, ...new Uint8Array(32).fill(9)]); +const addressCalls: Array<{ hrp: string; options?: { checkOnDevice?: boolean }; path: string }> = []; +const signCalls: Array<{ hrp: string; message: Uint8Array; path: string }> = []; +let signatureOutput = new Uint8Array(64); + +function deviceAction(output: Output) { + return { + cancel: mock(() => {}), + observable: of( + { intermediateValue: { requiredUserInteraction: "confirm-on-device" }, status: DeviceActionStatus.Pending }, + { output, status: DeviceActionStatus.Completed }, + ), + }; +} + +mock.module("@ledgerhq/device-signer-kit-cosmos", () => ({ + SignerCosmosBuilder: class MockSignerCosmosBuilder { + build() { + return { + getAddress: (path: string, hrp: string, options?: { checkOnDevice?: boolean }) => { + addressCalls.push({ hrp, options, path }); + return deviceAction({ address: "cosmos1ledgerdsk", publicKey }); + }, + signTransaction: (path: string, hrp: string, message: Uint8Array) => { + signCalls.push({ hrp, message, path }); + return deviceAction(signatureOutput); + }, + }; + } + }, +})); + +import { CosmosLedger } from "../src/ledger/clients/cosmos"; + +const dmkSession = { dmk: { id: "cosmos-dmk" } as unknown as DeviceManagementKit, sessionId: "cosmos-session" }; +const signDoc: StdSignDoc = { + account_number: "17", + chain_id: "cosmoshub-4", + fee: { amount: [{ amount: "123", denom: "uatom" }], gas: "200000" }, + memo: "Ledger DSK", + msgs: [{ type: "cosmos-sdk/MsgSend", value: { amount: [], from_address: "a", to_address: "b" } }], + sequence: "4", +}; + +describe("Ledger Cosmos Device Signer Kit client", () => { + beforeEach(() => { + addressCalls.length = 0; + signCalls.length = 0; + signatureOutput = new Uint8Array(64); + }); + + it("serializes the amino sign doc and normalizes a DER signature", async () => { + signatureOutput = Uint8Array.of(0x30, 0x06, 0x02, 0x01, 0x01, 0x02, 0x01, 0x02); + const states: string[] = []; + const client = new CosmosLedger({ + derivationPath: "m/44'/118'/3'/0/7", + dmkSession, + onDeviceActionState: ({ status }) => states.push(status), + }); + + const response = await client.signAmino("cosmos1ledgerdsk", signDoc); + const fixedSignature = Uint8Array.from([...new Uint8Array(31), 1, ...new Uint8Array(31), 2]); + + expect(signCalls).toEqual([{ hrp: "cosmos", message: serializeSignDoc(signDoc), path: "44'/118'/3'/0/7" }]); + expect(response).toEqual({ signature: encodeSecp256k1Signature(publicKey, fixedSignature), signed: signDoc }); + expect(addressCalls).toEqual([{ hrp: "cosmos", options: undefined, path: "44'/118'/3'/0/7" }]); + expect(states).toEqual(["pending", "completed", "pending", "completed"]); + }); + + it("preserves a fixed-length signature for the legacy transaction result", async () => { + signatureOutput = Uint8Array.from({ length: 64 }, (_, index) => index + 1); + const client = new CosmosLedger({ dmkSession }); + + const result = await client.signTransaction('{"account_number":"1"}', "9"); + + expect(signCalls).toEqual([ + { hrp: "cosmos", message: new TextEncoder().encode('{"account_number":"1"}'), path: "44'/118'/0'/0/0" }, + ]); + expect(result).toEqual([ + { + pub_key: { type: "tendermint/PubKeySecp256k1", value: Buffer.from(publicKey).toString("base64") }, + sequence: "9", + signature: signatureOutput, + }, + ]); + }); + + it("requests on-device address verification with the normalized path", async () => { + const client = new CosmosLedger({ derivationPath: "m/44'/118'/0'/1/12", dmkSession }); + + const response = await client.showAddressAndPubKey(); + + expect(response).toEqual({ address: "cosmos1ledgerdsk", publicKey: Buffer.from(publicKey).toString("hex") }); + expect(addressCalls).toEqual([{ hrp: "cosmos", options: { checkOnDevice: true }, path: "44'/118'/0'/1/12" }]); + }); +}); diff --git a/packages/wallet-hardware/test/ledger-dmk.test.ts b/packages/wallet-hardware/test/ledger-dmk.test.ts new file mode 100644 index 0000000..3b6da33 --- /dev/null +++ b/packages/wallet-hardware/test/ledger-dmk.test.ts @@ -0,0 +1,229 @@ +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, mock } from "bun:test"; +import * as ledgerDMKModule from "@ledgerhq/device-management-kit"; +import { of } from "rxjs"; + +import { disconnectLedgerDMKSession, getLedgerDMKSession } from "../src/ledger/helpers/dmk"; +import { executeLedgerDeviceAction } from "../src/ledger/helpers/executeDeviceAction"; + +type DeviceManagementKit = ledgerDMKModule.DeviceManagementKit; + +const { DeviceActionStatus } = ledgerDMKModule; +const actualLedgerDMKModule = { ...ledgerDMKModule }; +const discoveredDevice = { id: "default-ledger-device" }; +const lifecycleCalls: string[] = []; +const connectedSessions = new Set(); +let connectionAttempts = 0; +let connectionFailures = 0; + +const defaultDMK = { + connect: mock(({ device }: { device: unknown }) => { + connectionAttempts += 1; + lifecycleCalls.push(`connect:${device === discoveredDevice}:${connectionAttempts}`); + + if (connectionAttempts <= connectionFailures) { + return Promise.reject(new Error(`Connection attempt ${connectionAttempts} failed`)); + } + + const sessionId = `default-ledger-session-${connectionAttempts}`; + connectedSessions.add(sessionId); + return Promise.resolve(sessionId); + }), + disconnect: mock(({ sessionId }: { sessionId: string }) => { + lifecycleCalls.push(`disconnect:${sessionId}`); + connectedSessions.delete(sessionId); + return Promise.resolve(); + }), + getConnectedDevice: mock(({ sessionId }: { sessionId: string }) => { + if (!connectedSessions.has(sessionId)) throw new Error(`Session ${sessionId} is disconnected`); + return { sessionId }; + }), + startDiscovering: mock(() => { + lifecycleCalls.push("discover"); + return of(discoveredDevice); + }), + stopDiscovering: mock(() => { + lifecycleCalls.push("stop"); + return Promise.resolve(); + }), +}; + +class MockDeviceManagementKitBuilder { + addTransport() { + return this; + } + + build() { + return defaultDMK as unknown as DeviceManagementKit; + } +} + +mock.module("@ledgerhq/device-management-kit", () => ({ + ...actualLedgerDMKModule, + DeviceManagementKitBuilder: MockDeviceManagementKitBuilder, +})); + +function resetDefaultDMK({ failures = 0 }: { failures?: number } = {}) { + connectedSessions.clear(); + connectionAttempts = 0; + connectionFailures = failures; + lifecycleCalls.length = 0; + defaultDMK.connect.mockClear(); + defaultDMK.disconnect.mockClear(); + defaultDMK.getConnectedDevice.mockClear(); + defaultDMK.startDiscovering.mockClear(); + defaultDMK.stopDiscovering.mockClear(); +} + +describe("wallet-hardware/ledger DMK", () => { + it("discovers, connects, and stops discovery for an injected DMK", async () => { + const device = { id: "ledger-device" }; + const calls: string[] = []; + const dmk = { + connect: mock(({ device: selectedDevice }: { device: unknown }) => { + calls.push(`connect:${selectedDevice === device}`); + return Promise.resolve("ledger-session"); + }), + startDiscovering: mock(() => { + calls.push("discover"); + return of(device); + }), + stopDiscovering: mock(() => { + calls.push("stop"); + return Promise.resolve(); + }), + } as unknown as DeviceManagementKit; + + const session = await getLedgerDMKSession({ dmk }); + + expect(session).toEqual({ dmk, sessionId: "ledger-session" }); + expect(calls).toEqual(["discover", "connect:true", "stop"]); + }); + + it("forwards intermediate states and resolves completed device actions", async () => { + const states: string[] = []; + const action = { + cancel: mock(() => {}), + observable: of( + { intermediateValue: { requiredUserInteraction: "confirm-on-device" }, status: DeviceActionStatus.Pending }, + { output: { address: "0xledger" }, status: DeviceActionStatus.Completed }, + ), + }; + + const output = await executeLedgerDeviceAction({ + action, + onDeviceActionState: ({ status }) => states.push(status), + }); + + expect(output).toEqual({ address: "0xledger" }); + expect(states).toEqual(["pending", "completed"]); + }); + + it("rejects the error emitted by a device action", async () => { + const ledgerError = new Error("Ledger rejected the action"); + const action = { cancel: mock(() => {}), observable: of({ error: ledgerError, status: DeviceActionStatus.Error }) }; + + await expect(executeLedgerDeviceAction({ action })).rejects.toBe(ledgerError); + }); + + it("maps stopped device actions to a SwapKit error", async () => { + const action = { cancel: mock(() => {}), observable: of({ status: DeviceActionStatus.Stopped }) }; + + await expect(executeLedgerDeviceAction({ action })).rejects.toMatchObject({ + errorKey: "wallet_ledger_connection_error", + }); + }); + + it("isolates consumer progress-handler errors", async () => { + const action = { + cancel: mock(() => {}), + observable: of({ output: "signed", status: DeviceActionStatus.Completed }), + }; + + const output = await executeLedgerDeviceAction({ + action, + onDeviceActionState: () => { + throw new Error("UI callback failed"); + }, + }); + + expect(output).toBe("signed"); + }); + + it("disconnects an explicitly released DMK session", async () => { + const disconnect = mock((_params: { sessionId: string }) => Promise.resolve()); + const dmk = { disconnect } as unknown as DeviceManagementKit; + + await disconnectLedgerDMKSession({ dmkSession: { dmk, sessionId: "ledger-session" } }); + + expect(disconnect).toHaveBeenCalledWith({ sessionId: "ledger-session" }); + }); + + describe("default session lifecycle", () => { + const originalHidDescriptor = Object.getOwnPropertyDescriptor(globalThis.navigator, "hid"); + + beforeAll(() => { + Object.defineProperty(globalThis.navigator, "hid", { configurable: true, value: {} }); + }); + + beforeEach(() => { + resetDefaultDMK(); + }); + + afterEach(async () => { + await disconnectLedgerDMKSession(); + }); + + afterAll(() => { + if (originalHidDescriptor) { + Object.defineProperty(globalThis.navigator, "hid", originalHidDescriptor); + } else { + Reflect.deleteProperty(globalThis.navigator, "hid"); + } + }); + + it("shares one discovery and connection across concurrent callers", async () => { + const [firstSession, secondSession] = await Promise.all([getLedgerDMKSession(), getLedgerDMKSession()]); + + expect(firstSession).toBe(secondSession); + expect(firstSession).toMatchObject({ sessionId: "default-ledger-session-1" }); + expect(firstSession.dmk).toBe(defaultDMK); + expect(lifecycleCalls).toEqual(["discover", "connect:true:1", "stop"]); + expect(defaultDMK.getConnectedDevice).toHaveBeenCalledTimes(2); + }); + + it("clears a rejected connection promise so the next call retries", async () => { + resetDefaultDMK({ failures: 1 }); + + await expect(getLedgerDMKSession()).rejects.toMatchObject({ errorKey: "wallet_ledger_connection_error" }); + + const retriedSession = await getLedgerDMKSession(); + + expect(retriedSession).toMatchObject({ sessionId: "default-ledger-session-2" }); + expect(retriedSession.dmk).toBe(defaultDMK); + expect(lifecycleCalls).toEqual(["discover", "connect:true:1", "stop", "discover", "connect:true:2", "stop"]); + expect(defaultDMK.getConnectedDevice).toHaveBeenCalledTimes(1); + }); + + it("invalidates the cached session on disconnect and discovers again", async () => { + const firstSession = await getLedgerDMKSession(); + + await disconnectLedgerDMKSession(); + const secondSession = await getLedgerDMKSession(); + + expect(firstSession).toMatchObject({ sessionId: "default-ledger-session-1" }); + expect(secondSession).toMatchObject({ sessionId: "default-ledger-session-2" }); + expect(secondSession).not.toBe(firstSession); + expect(lifecycleCalls).toEqual([ + "discover", + "connect:true:1", + "stop", + "disconnect:default-ledger-session-1", + "discover", + "connect:true:2", + "stop", + ]); + expect(defaultDMK.disconnect).toHaveBeenCalledTimes(1); + expect(defaultDMK.getConnectedDevice).toHaveBeenCalledTimes(2); + }); + }); +}); diff --git a/packages/wallet-hardware/test/ledger-js-dmk-bridge.test.ts b/packages/wallet-hardware/test/ledger-js-dmk-bridge.test.ts new file mode 100644 index 0000000..33f700b --- /dev/null +++ b/packages/wallet-hardware/test/ledger-js-dmk-bridge.test.ts @@ -0,0 +1,221 @@ +import { describe, expect, it, mock } from "bun:test"; +import { + ApduResponse, + DeviceActionStatus, + type DeviceManagementKit, + DmkResultStatus, + type InternalApi, + UserInteractionRequired, +} from "@ledgerhq/device-management-kit"; +import type Transport from "@ledgerhq/hw-transport"; +import { concat, from, of } from "rxjs"; + +import { LedgerJsDmkTransport, runLedgerJsOperation } from "../src/ledger/helpers/ledgerJsDmkBridge"; + +function createInternalApi({ + responseData = new Uint8Array([0xaa]), + statusCode = new Uint8Array([0x90, 0x00]), +}: { + responseData?: Uint8Array; + statusCode?: Uint8Array; +} = {}) { + const apdus: Uint8Array[] = []; + const abortTimeouts: Array = []; + const sendCommand = mock((command: Parameters[0], abortTimeout?: number) => { + apdus.push(command.getApdu().getRawApdu()); + abortTimeouts.push(abortTimeout); + return Promise.resolve(command.parseResponse(new ApduResponse({ data: responseData, statusCode }), undefined)); + }) as InternalApi["sendCommand"]; + + return { abortTimeouts, apdus, internalApi: { sendCommand } as unknown as InternalApi, sendCommand }; +} + +function createDmkHarness(internalApi: InternalApi) { + const actions: Array<{ + input: { + appName: string; + requiredUserInteraction: UserInteractionRequired; + skipOpenApp: boolean; + task: (api: InternalApi) => Promise<{ data?: unknown; error?: unknown; status: DmkResultStatus }>; + }; + }> = []; + const publicSendCommand = mock(() => Promise.reject(new Error("Public DMK sendCommand must not be called"))); + const executeDeviceAction = mock(({ deviceAction }: { deviceAction: (typeof actions)[number] }) => { + actions.push(deviceAction); + const completedState = async () => { + const result = await deviceAction.input.task(internalApi); + return result.status === DmkResultStatus.Success + ? { output: result.data, status: DeviceActionStatus.Completed } + : { error: result.error, status: DeviceActionStatus.Error }; + }; + + return { + cancel: mock(() => {}), + observable: concat( + of({ + intermediateValue: { requiredUserInteraction: deviceAction.input.requiredUserInteraction }, + status: DeviceActionStatus.Pending, + }), + from(completedState()), + ), + }; + }); + + return { + actions, + dmk: { executeDeviceAction, sendCommand: publicSendCommand } as unknown as DeviceManagementKit, + executeDeviceAction, + publicSendCommand, + }; +} + +describe("LedgerJS DMK bridge transport", () => { + it("validates and forwards a short APDU with its abort timeout", async () => { + const { abortTimeouts, apdus, internalApi } = createInternalApi({ responseData: new Uint8Array([0xde, 0xad]) }); + const transport = new LedgerJsDmkTransport(internalApi); + + const response = await transport.exchange(Buffer.from([0xe0, 0x02, 0x01, 0x03, 0x02, 0xbe, 0xef]), { + abortTimeoutMs: 12_345, + }); + + expect(apdus.map((apdu) => [...apdu])).toEqual([[0xe0, 0x02, 0x01, 0x03, 0x02, 0xbe, 0xef]]); + expect(abortTimeouts).toEqual([12_345]); + expect([...response]).toEqual([0xde, 0xad, 0x90, 0x00]); + }); + + it("rejects malformed short APDUs before touching DMK", async () => { + const { internalApi, sendCommand } = createInternalApi(); + const transport = new LedgerJsDmkTransport(internalApi); + + await expect(transport.exchange(Buffer.from([0xe0, 0x02, 0x00, 0x00]))).rejects.toMatchObject({ + id: "InvalidAPDU", + }); + await expect(transport.exchange(Buffer.from([0xe0, 0x02, 0x00, 0x00, 0x02, 0xaa]))).rejects.toMatchObject({ + id: "InvalidAPDU", + }); + expect(sendCommand).toHaveBeenCalledTimes(0); + }); + + it("returns data plus status so LedgerJS applies its accepted-status list", async () => { + const { internalApi } = createInternalApi({ + responseData: new Uint8Array([0x01]), + statusCode: new Uint8Array([0x6a, 0x80]), + }); + const transport = new LedgerJsDmkTransport(internalApi); + + await expect(transport.send(0xe0, 0x02, 0x00, 0x00)).rejects.toMatchObject({ statusCode: 0x6a80 }); + const accepted = await transport.send(0xe0, 0x02, 0x00, 0x00, Buffer.alloc(0), [0x6a80]); + + expect([...accepted]).toEqual([0x01, 0x6a, 0x80]); + }); + + it("has a no-op close that does not alter the DMK session", async () => { + const { internalApi, sendCommand } = createInternalApi(); + const transport = new LedgerJsDmkTransport(internalApi); + + await transport.close(); + + expect(sendCommand).toHaveBeenCalledTimes(0); + }); +}); + +describe("operation-scoped LedgerJS DMK bridge", () => { + it("keeps a multi-APDU app call in one action and forwards progress", async () => { + const { internalApi, sendCommand } = createInternalApi(); + const harness = createDmkHarness(internalApi); + const transports: Transport[] = []; + const states: string[] = []; + + const output = await runLedgerJsOperation({ + appName: "Litecoin", + connection: { + dmkSession: { dmk: harness.dmk, sessionId: "ledger-session" }, + onDeviceActionState: ({ status }) => states.push(status), + }, + createApp: (transport) => { + transports.push(transport); + return { + async sign() { + await transport.send(0xe1, 0x01, 0x00, 0x00, Buffer.from([0x01])); + await transport.send(0xe1, 0x02, 0x00, 0x00, Buffer.from([0x02])); + return "signed"; + }, + }; + }, + operation: (app) => app.sign(), + requiredUserInteraction: UserInteractionRequired.SignTransaction, + }); + + expect(output).toBe("signed"); + expect(harness.executeDeviceAction).toHaveBeenCalledTimes(1); + expect(sendCommand).toHaveBeenCalledTimes(2); + expect(transports).toHaveLength(1); + expect(harness.actions[0]?.input).toMatchObject({ + appName: "Litecoin", + requiredUserInteraction: UserInteractionRequired.SignTransaction, + skipOpenApp: false, + }); + expect(states).toEqual([DeviceActionStatus.Pending, DeviceActionStatus.Completed]); + expect(harness.publicSendCommand).toHaveBeenCalledTimes(0); + }); + + it("uses a fresh bridge transport for each operation and preserves falsy output", async () => { + const { internalApi } = createInternalApi(); + const harness = createDmkHarness(internalApi); + const transports: Transport[] = []; + const connection = { dmkSession: { dmk: harness.dmk, sessionId: "ledger-session" } }; + const createApp = (transport: Transport) => { + transports.push(transport); + return { run: async () => "" }; + }; + + const first = await runLedgerJsOperation({ appName: "NEAR", connection, createApp, operation: (app) => app.run() }); + const second = await runLedgerJsOperation({ + appName: "NEAR", + connection, + createApp, + operation: (app) => app.run(), + }); + + expect([first, second]).toEqual(["", ""]); + expect(harness.executeDeviceAction).toHaveBeenCalledTimes(2); + expect(transports).toHaveLength(2); + expect(transports[0]).not.toBe(transports[1]); + }); + + it("keeps an explicitly injected LedgerJS transport on the direct fallback path", async () => { + const transport = { id: "legacy-ledgerjs" } as unknown as Transport; + const createAppTransports: Transport[] = []; + + const output = await runLedgerJsOperation({ + appName: "XRP", + connection: { transport }, + createApp: (appTransport) => { + createAppTransports.push(appTransport); + return { getAddress: async () => "rLedger" }; + }, + operation: (app) => app.getAddress(), + }); + + expect(output).toBe("rLedger"); + expect(createAppTransports).toEqual([transport]); + }); + + it("rejects ambiguous DMK plus LedgerJS transport ownership", async () => { + const { internalApi } = createInternalApi(); + const harness = createDmkHarness(internalApi); + + await expect( + runLedgerJsOperation({ + appName: "Sui", + connection: { + dmkSession: { dmk: harness.dmk, sessionId: "ledger-session" }, + transport: { id: "legacy" } as unknown as Transport, + }, + createApp: () => ({ run: async () => true }), + operation: (app) => app.run(), + }), + ).rejects.toMatchObject({ errorKey: "wallet_ledger_invalid_params" }); + expect(harness.executeDeviceAction).toHaveBeenCalledTimes(0); + }); +}); diff --git a/packages/wallet-hardware/test/ledger-js-dmk-clients.test.ts b/packages/wallet-hardware/test/ledger-js-dmk-clients.test.ts new file mode 100644 index 0000000..48ba989 --- /dev/null +++ b/packages/wallet-hardware/test/ledger-js-dmk-clients.test.ts @@ -0,0 +1,255 @@ +import { describe, expect, it, mock } from "bun:test"; +import { + ApduResponse, + DeviceActionStatus, + type DeviceManagementKit, + DmkResultStatus, + type InternalApi, + UserInteractionRequired, +} from "@ledgerhq/device-management-kit"; +import type { Transaction as NearTransaction } from "@near-js/transactions"; +import { decode } from "ripple-binary-codec"; +import { from } from "rxjs"; +import type { Payment } from "xrpl"; + +import { getNearLedgerClient } from "../src/ledger/clients/near"; +import { SuiLedger } from "../src/ledger/clients/sui"; +import { TronLedger } from "../src/ledger/clients/tron"; +import { BitcoinCashLedger, DashLedger, DogecoinLedger, LitecoinLedger } from "../src/ledger/clients/utxo"; +import { XRPLedger } from "../src/ledger/clients/xrp"; + +interface ExchangeRecord { + actionIndex: number; + appName: string; + cla: number; + ins: number; + p1: number; + p2: number; + data: Uint8Array; +} + +function ledgerAddressResponse({ address, publicKey }: { address: string; publicKey: Uint8Array }) { + return new Uint8Array([ + publicKey.length, + ...publicKey, + address.length, + ...Buffer.from(address), + ...new Uint8Array(32), + ]); +} + +function getResponseData({ appName, ins, p1 }: Pick) { + if (appName === "NEAR") { + if (ins === 0x04) return new Uint8Array(32); + if (ins === 0x02 && p1 === 0x80) return new Uint8Array(64).fill(0x71); + return new Uint8Array(); + } + + if (appName === "XRP") { + if (ins === 0x02) { + return ledgerAddressResponse({ + address: "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + publicKey: new Uint8Array([0x02, ...new Uint8Array(32).fill(0x11)]), + }); + } + if (ins === 0x04) return new Uint8Array([0x30, 0x06, 0x02, 0x01, 0x01, 0x02, 0x01, 0x01]); + } + + if (appName === "Tron") { + if (ins === 0x02) { + return ledgerAddressResponse({ + address: "TR9VSvyaqPs5whY6oeYMTVbyAH27hoNFUW", + publicKey: new Uint8Array([0x04, ...new Uint8Array(64).fill(0x22)]), + }); + } + if (ins === 0x04) return new Uint8Array(65).fill(0xab); + } + + if (appName === "Sui") { + if (ins === 0x00) return new Uint8Array([0x01, 0x01, 0x00, 0x00]); + if (ins === 0x02) { + return new Uint8Array([0x01, 32, ...new Uint8Array(32).fill(0x33), 32, ...new Uint8Array(32).fill(0x44)]); + } + if (ins === 0x03) return new Uint8Array([0x01, ...new Uint8Array(64).fill(0x55)]); + } + + if (["Bitcoin Cash", "Dash", "Dogecoin", "Litecoin"].includes(appName) && ins === 0x40) { + return ledgerAddressResponse({ + address: `${appName.toLowerCase().replace(" ", "-")}-ledger-address`, + publicKey: new Uint8Array([0x02, ...new Uint8Array(32).fill(0x66)]), + }); + } + + throw new Error(`Unexpected ${appName} APDU instruction 0x${ins.toString(16)}`); +} + +function createDmkHarness() { + const actions: Array<{ appName: string; requiredUserInteraction: UserInteractionRequired }> = []; + const exchanges: ExchangeRecord[] = []; + const executeDeviceAction = mock( + ({ + deviceAction, + }: { + deviceAction: { + input: { + appName: string; + requiredUserInteraction: UserInteractionRequired; + task: (internalApi: InternalApi) => Promise<{ data?: unknown; error?: unknown; status: DmkResultStatus }>; + }; + }; + }) => { + const actionIndex = actions.length; + actions.push({ + appName: deviceAction.input.appName, + requiredUserInteraction: deviceAction.input.requiredUserInteraction, + }); + + const completedState = async () => { + const sendCommand = (command: Parameters[0]) => { + const apdu = command.getApdu(); + const exchange = { + actionIndex, + appName: deviceAction.input.appName, + cla: apdu.cla, + data: apdu.data, + ins: apdu.ins, + p1: apdu.p1, + p2: apdu.p2, + }; + exchanges.push(exchange); + return Promise.resolve( + command.parseResponse( + new ApduResponse({ data: getResponseData(exchange), statusCode: new Uint8Array([0x90, 0x00]) }), + undefined, + ), + ); + }; + const result = await deviceAction.input.task({ sendCommand } as unknown as InternalApi); + return result.status === DmkResultStatus.Success + ? { output: result.data, status: DeviceActionStatus.Completed } + : { error: result.error, status: DeviceActionStatus.Error }; + }; + + return { cancel: mock(() => {}), observable: from(completedState()) }; + }, + ); + const dmk = { executeDeviceAction } as unknown as DeviceManagementKit; + + return { actions, dmkSession: { dmk, sessionId: "ledger-session" }, exchanges, executeDeviceAction }; +} + +describe("LedgerJS clients over the operation-scoped DMK bridge", () => { + it("keeps the NEAR address and multi-chunk signature formats without duplicating the key prefix", async () => { + const harness = createDmkHarness(); + const client = await getNearLedgerClient({ derivationPath: [44, 397, 0, 0, 0], dmkSession: harness.dmkSession }); + + const publicKey = await client.getPublicKey(); + const transaction = { encode: () => new Uint8Array(600).fill(0x42) } as unknown as NearTransaction; + const [signature] = await client.signTransaction(transaction); + + expect(publicKey.toString()).toBe(`ed25519:${"1".repeat(32)}`); + expect([...signature]).toEqual(new Array(64).fill(0x71)); + expect(harness.actions).toEqual([ + { appName: "NEAR", requiredUserInteraction: UserInteractionRequired.None }, + { appName: "NEAR", requiredUserInteraction: UserInteractionRequired.SignTransaction }, + ]); + expect(harness.exchanges.map(({ actionIndex, ins, p1 }) => ({ actionIndex, ins, p1 }))).toEqual([ + { actionIndex: 0, ins: 0x04, p1: 0x01 }, + { actionIndex: 1, ins: 0x02, p1: 0x00 }, + { actionIndex: 1, ins: 0x02, p1: 0x00 }, + { actionIndex: 1, ins: 0x02, p1: 0x80 }, + ]); + }); + + it("preserves the XRP public key, DER signature, canonical flag, and signed blob", async () => { + const harness = createDmkHarness(); + const client = await XRPLedger({ dmkSession: harness.dmkSession }); + const transaction: Payment = { + Account: client.getAddress(), + Amount: "1000", + Destination: "rTooLkitCksh5mQa67eaa2JaWHDBnHkpy", + Fee: "12", + Sequence: 1, + TransactionType: "Payment", + }; + + const signed = await client.signTransaction(transaction); + const decoded = decode(signed.tx_blob); + + expect(signed.hash).toMatch(/^[A-F0-9]{64}$/); + expect(decoded).toMatchObject({ + Flags: 2147483648, + SigningPubKey: `02${"11".repeat(32)}`, + TxnSignature: "3006020101020101", + }); + expect(harness.actions).toEqual([ + { appName: "XRP", requiredUserInteraction: UserInteractionRequired.None }, + { appName: "XRP", requiredUserInteraction: UserInteractionRequired.SignTransaction }, + ]); + expect(harness.exchanges.filter(({ actionIndex }) => actionIndex === 1).every(({ ins }) => ins === 0x04)).toBe( + true, + ); + }); + + it("preserves Tron's raw transaction and 65-byte signature array", async () => { + const harness = createDmkHarness(); + const client = TronLedger({ dmkSession: harness.dmkSession }); + const transaction = { raw_data_hex: "0a00", txID: "ledger-tron-tx" } as Parameters< + typeof client.signTransaction + >[0]; + + const signed = await client.signTransaction(transaction); + + expect(signed).toMatchObject({ raw_data_hex: "0a00", signature: ["ab".repeat(65)], txID: "ledger-tron-tx" }); + expect(harness.actions).toEqual([ + { appName: "Tron", requiredUserInteraction: UserInteractionRequired.SignTransaction }, + ]); + }); + + it("preserves Sui's intent bytes and serialized signature layout", async () => { + const harness = createDmkHarness(); + const client = SuiLedger({ dmkSession: harness.dmkSession }); + + const address = await client.connect(); + const signed = await client.signTransaction(new Uint8Array([0xaa, 0xbb])); + const serializedSignature = Buffer.from(signed.signature, "base64"); + + expect(address).toBe(`0x${"44".repeat(32)}`); + expect(signed.bytes).toBe("qrs="); + expect([...serializedSignature]).toEqual([0, ...new Array(64).fill(0x55), ...new Array(32).fill(0x33)]); + expect(harness.actions).toEqual([ + { appName: "Sui", requiredUserInteraction: UserInteractionRequired.None }, + { appName: "Sui", requiredUserInteraction: UserInteractionRequired.SignTransaction }, + ]); + expect(harness.exchanges.filter(({ actionIndex }) => actionIndex === 1).map(({ ins }) => ins)).toEqual([ + 0x00, 0x03, + ]); + }); + + it("opens each alt-UTXO app through its own operation and keeps address output intact", async () => { + const harness = createDmkHarness(); + const clients = [ + BitcoinCashLedger({ derivationPath: "84'/145'/0'/0/0", dmkSession: harness.dmkSession }), + LitecoinLedger({ derivationPath: "84'/2'/0'/0/0", dmkSession: harness.dmkSession }), + DogecoinLedger({ derivationPath: "84'/3'/0'/0/0", dmkSession: harness.dmkSession }), + DashLedger({ derivationPath: "84'/5'/0'/0/0", dmkSession: harness.dmkSession }), + ]; + + const addresses: string[] = []; + for (const client of clients) addresses.push(await client.getAddress()); + + expect(addresses).toEqual([ + "bitcoin-cash-ledger-address", + "litecoin-ledger-address", + "dogecoin-ledger-address", + "dash-ledger-address", + ]); + expect(harness.actions.map(({ appName }) => appName)).toEqual(["Bitcoin Cash", "Litecoin", "Dogecoin", "Dash"]); + expect(harness.exchanges.map(({ actionIndex, ins }) => ({ actionIndex, ins }))).toEqual([ + { actionIndex: 0, ins: 0x40 }, + { actionIndex: 1, ins: 0x40 }, + { actionIndex: 2, ins: 0x40 }, + { actionIndex: 3, ins: 0x40 }, + ]); + }); +}); diff --git a/packages/wallet-hardware/test/ledger-thorchain.test.ts b/packages/wallet-hardware/test/ledger-thorchain.test.ts index bda7d3c..a32f05f 100644 --- a/packages/wallet-hardware/test/ledger-thorchain.test.ts +++ b/packages/wallet-hardware/test/ledger-thorchain.test.ts @@ -1,48 +1,344 @@ -import { describe, expect, it } from "bun:test"; -import type { StdSignDoc } from "@cosmjs/amino"; +import { describe, expect, it, mock } from "bun:test"; +import { type StdSignDoc, serializeSignDoc } from "@cosmjs/amino"; +import { + ApduResponse, + CallTaskInAppDeviceAction, + DeviceActionStatus, + type DeviceManagementKit, + DmkResultStatus, + type InternalApi, + UserInteractionRequired, +} from "@ledgerhq/device-management-kit"; +import type Transport from "@ledgerhq/hw-transport"; +import { base64, hex } from "@scure/base"; +import { concat, from, of } from "rxjs"; -import { normalizeThorchainLedgerSignDoc } from "../src/ledger/clients/thorchain"; +import { normalizeThorchainLedgerSignDoc, THORChainLedger } from "../src/ledger/clients/thorchain"; +import { + getThorAddressCommand, + getThorSignCommands, + serializeThorPath, +} from "../src/ledger/clients/thorchain/protocol"; -describe("ledger THORChain signer", () => { - it("normalizes MsgDeposit asset objects back to Amino denoms before Ledger signing", () => { - const signDoc: StdSignDoc = { - account_number: "75542", +interface ThorDmkAction { + input: { + appName: string; + requiredUserInteraction: UserInteractionRequired; + skipOpenApp: boolean; + task: (internalApi: InternalApi) => Promise<{ data?: unknown; error?: unknown; status: DmkResultStatus }>; + }; +} + +interface ThorDmkExchange { + cla: number; + data: Uint8Array; + ins: number; + p1: number; + p2: number; +} + +function createThorDmkHarness({ version }: { version: string }) { + const actions: Array<{ deviceAction: ThorDmkAction; sessionId: string }> = []; + const exchanges: ThorDmkExchange[] = []; + const publicSendCommand = mock(() => Promise.reject(new Error("Public DMK sendCommand must not be called"))); + const disconnect = mock(() => Promise.resolve()); + const executeDeviceAction = mock( + ({ deviceAction, sessionId }: { deviceAction: ThorDmkAction; sessionId: string }) => { + actions.push({ deviceAction, sessionId }); + + const completedState = async () => { + const sendCommand = mock((command: Parameters[0]) => { + const apdu = command.getApdu(); + exchanges.push({ cla: apdu.cla, data: apdu.data, ins: apdu.ins, p1: apdu.p1, p2: apdu.p2 }); + const data = + apdu.ins === 0x02 && apdu.p1 === 0x02 + ? Uint8Array.of(0x30, 0x06, 0x02, 0x01, 0x01, 0x02, 0x01, 0x02) + : new Uint8Array(); + + return Promise.resolve( + command.parseResponse(new ApduResponse({ data, statusCode: Uint8Array.of(0x90, 0x00) }), undefined), + ); + }) as InternalApi["sendCommand"]; + const internalApi = { + getDeviceSessionState: () => ({ currentApp: { name: "THORChain", version } }), + sendCommand, + } as unknown as InternalApi; + const result = await deviceAction.input.task(internalApi); + + return result.status === DmkResultStatus.Success + ? { output: result.data, status: DeviceActionStatus.Completed } + : { error: result.error, status: DeviceActionStatus.Error }; + }; + + return { + cancel: mock(() => {}), + observable: concat( + of({ + intermediateValue: { requiredUserInteraction: deviceAction.input.requiredUserInteraction }, + status: DeviceActionStatus.Pending, + }), + from(completedState()), + ), + }; + }, + ); + const dmk = { disconnect, executeDeviceAction, sendCommand: publicSendCommand } as unknown as DeviceManagementKit; + + return { + actions, + disconnect, + dmkSession: { dmk, sessionId: "thor-session" }, + exchanges, + executeDeviceAction, + publicSendCommand, + }; +} + +describe("ledger THORChain protocol", () => { + it("serializes the app v2 derivation path as five little-endian integers", () => { + expect(Array.from(serializeThorPath({ path: [44, 931, 0, 0, 7] }))).toEqual([ + 44, 0, 0, 128, 163, 3, 0, 128, 0, 0, 0, 128, 0, 0, 0, 0, 7, 0, 0, 0, + ]); + }); + + it("builds the address APDU with the selected HRP and device confirmation flag", () => { + const apdu = getThorAddressCommand({ checkOnDevice: true, hrp: "sthor", path: [44, 931, 0, 0, 0] }).getApdu(); + + expect({ cla: apdu.cla, data: Array.from(apdu.data), ins: apdu.ins, p1: apdu.p1, p2: apdu.p2 }).toEqual({ + cla: 0x55, + data: [5, 115, 116, 104, 111, 114, 44, 0, 0, 128, 163, 3, 0, 128, 0, 0, 0, 128, 0, 0, 0, 0, 0, 0, 0, 0], + ins: 0x04, + p1: 1, + p2: 0, + }); + }); + + it("keeps every signing chunk inside one app-v2 command sequence", () => { + const message = Uint8Array.from({ length: 501 }, (_, index) => index % 256); + const commands = getThorSignCommands({ message, path: [44, 931, 0, 0, 0] }).map((command) => command.getApdu()); + + expect(commands.map(({ cla, data, ins, p1, p2 }) => ({ cla, dataLength: data.length, ins, p1, p2 }))).toEqual([ + { cla: 0x55, dataLength: 20, ins: 0x02, p1: 0, p2: 0 }, + { cla: 0x55, dataLength: 250, ins: 0x02, p1: 1, p2: 0 }, + { cla: 0x55, dataLength: 250, ins: 0x02, p1: 1, p2: 0 }, + { cla: 0x55, dataLength: 1, ins: 0x02, p1: 2, p2: 0 }, + ]); + }); + + it("rejects derivation paths that app v2 cannot consume", () => { + expect(() => serializeThorPath({ path: [44, 931, 0, 0] })).toThrow("wallet_ledger_invalid_params"); + expect(() => serializeThorPath({ path: [44, 931, undefined, 0, 0] })).toThrow("wallet_ledger_invalid_params"); + }); + + it("normalizes legacy toolbox deposit assets to canonical Ledger Amino bytes", () => { + const signDoc = { + account_number: "1", chain_id: "thorchain-1", - fee: { amount: [], gas: "500000000" }, - memo: "=:f:0xfb30fEbB4A9Fa5801aDA640e6b14bd56d0a5457E:150363:-_/nc:15/0", + fee: { amount: [], gas: "500000" }, + memo: "", msgs: [ { type: "thorchain/MsgDeposit", value: { - coins: [{ amount: "680000000", asset: { chain: "THOR", symbol: "RUNE", synth: false, ticker: "RUNE" } }], - memo: "=:f:0xfb30fEbB4A9Fa5801aDA640e6b14bd56d0a5457E:150363:-_/nc:15/0", - signer: "thor1mznwgn90zfzpj9zug3xk2llyk8yxq2sg6hgsnn", + coins: [{ amount: "123", asset: { chain: "THOR", symbol: "RUNE", synth: false, ticker: "RUNE" } }], + memo: "=:BTC.BTC:bc1recipient", + signer: "thor1signer", }, }, ], - sequence: "147", + sequence: "2", }; - expect(normalizeThorchainLedgerSignDoc(signDoc).msgs[0]?.value.coins).toEqual([ - { amount: "680000000", asset: "THOR.RUNE" }, - ]); + const normalized = normalizeThorchainLedgerSignDoc(signDoc); + + expect(normalized.msgs[0]).toMatchObject({ + type: "thorchain/MsgDeposit", + value: { coins: [{ amount: "123", asset: "THOR.RUNE" }] }, + }); + expect(hex.encode(serializeSignDoc(normalized))).toBe( + "7b226163636f756e745f6e756d626572223a2231222c22636861696e5f6964223a2274686f72636861696e2d31222c22666565223a7b22616d6f756e74223a5b5d2c22676173223a22353030303030227d2c226d656d6f223a22222c226d736773223a5b7b2274797065223a2274686f72636861696e2f4d73674465706f736974222c2276616c7565223a7b22636f696e73223a5b7b22616d6f756e74223a22313233222c226173736574223a2254484f522e52554e45227d5d2c226d656d6f223a223d3a4254432e4254433a626331726563697069656e74222c227369676e6572223a2274686f72317369676e6572227d7d5d2c2273657175656e6365223a2232227d", + ); }); - it("leaves MsgSend denoms untouched", () => { + it("normalizes native and synth assets only at the signAmino serialization boundary", async () => { + const signerAddress = "thor1qypqxpq9qcrsszg2pvxq6rs0zqg3yyc5e949nr"; + const publicKey = Uint8Array.from([0x02, ...new Uint8Array(32).fill(0x11)]); + const signingChunks: Uint8Array[] = []; + const signingCalls: Array<{ dataLength: number; p1: number }> = []; const signDoc: StdSignDoc = { - account_number: "1", + account_number: "7", chain_id: "thorchain-1", fee: { amount: [], gas: "500000000" }, - memo: "", + memo: "outer", msgs: [ { - type: "thorchain/MsgSend", - value: { amount: [{ amount: "1", denom: "rune" }], from_address: "thor1from", to_address: "thor1to" }, + type: "thorchain/MsgDeposit", + value: { + coins: [ + { amount: "123", asset: { chain: "THOR", symbol: "RUNE", synth: false, ticker: "RUNE" } }, + { amount: "456", asset: { chain: "BTC", symbol: "BTC", synth: true, ticker: "BTC" } }, + ], + memo: "=:BTC.BTC:bc1recipient", + signer: signerAddress, + }, }, ], - sequence: "2", + sequence: "9", }; + const expectedPayload = + `{"account_number":"7","chain_id":"thorchain-1","fee":{"amount":[],"gas":"500000000"},` + + `"memo":"outer","msgs":[{"type":"thorchain/MsgDeposit","value":{"coins":[` + + `{"amount":"123","asset":"THOR.RUNE"},{"amount":"456","asset":"BTC/BTC"}],` + + `"memo":"=:BTC.BTC:bc1recipient","signer":"${signerAddress}"}}],"sequence":"9"}`; + const transport = { + send: (_cla: number, ins: number, p1: number, _p2: number, data = new Uint8Array()) => { + if (ins === 0x00) return Buffer.from([0x00, 0x02, 0x05, 0x01, 0x90, 0x00]); + if (ins === 0x04) return Buffer.from([...publicKey, ...new TextEncoder().encode(signerAddress), 0x90, 0x00]); + + signingCalls.push({ dataLength: data.length, p1 }); + if (p1 !== 0x00) signingChunks.push(Uint8Array.from(data)); + if (p1 === 0x02) { + return Buffer.from([0x30, 0x06, 0x02, 0x01, 0x01, 0x02, 0x01, 0x02, 0x90, 0x00]); + } + return Buffer.from([0x90, 0x00]); + }, + } as unknown as Transport; + const ledger = new THORChainLedger({ derivationPath: [44, 931, 0, 0, 0], transport }); + + const response = await ledger.signAmino(signerAddress, signDoc); + const serializedPayload = Uint8Array.from(signingChunks.flatMap((chunk) => Array.from(chunk))); + const fixedSignature = Uint8Array.from([...new Uint8Array(31), 0x01, ...new Uint8Array(31), 0x02]); + + expect(new TextDecoder().decode(serializedPayload)).toBe(expectedPayload); + expect(signingCalls).toEqual([ + { dataLength: 20, p1: 0x00 }, + { dataLength: 250, p1: 0x01 }, + { dataLength: 85, p1: 0x02 }, + ]); + expect(response.signature).toEqual({ + pub_key: { type: "tendermint/PubKeySecp256k1", value: base64.encode(publicKey) }, + signature: base64.encode(fixedSignature), + }); + expect(response.signed).toBe(signDoc); + expect(signDoc.msgs[0]?.value.coins).toEqual([ + { amount: "123", asset: { chain: "THOR", symbol: "RUNE", synth: false, ticker: "RUNE" } }, + { amount: "456", asset: { chain: "BTC", symbol: "BTC", synth: true, ticker: "BTC" } }, + ]); + }); + + it("executes every app-v2 signing chunk atomically through one real DMK action", async () => { + const v2Harness = createThorDmkHarness({ version: "2.5.1" }); + const v2Progress: Array<{ requiredUserInteraction: string | null; status: string }> = []; + const v2Ledger = new THORChainLedger({ + derivationPath: [44, 931, 0, 0, 0], + dmkSession: v2Harness.dmkSession, + onDeviceActionState: (state) => { + v2Progress.push({ + requiredUserInteraction: + state.status === DeviceActionStatus.Pending ? state.intermediateValue.requiredUserInteraction : null, + status: state.status, + }); + }, + }); - expect(normalizeThorchainLedgerSignDoc(signDoc)).toEqual(signDoc); + const signature = await v2Ledger.sign("x".repeat(501)); + + expect(Buffer.from(base64.decode(signature)).toString("hex")).toBe(`${"00".repeat(31)}01${"00".repeat(31)}02`); + expect(v2Harness.executeDeviceAction).toHaveBeenCalledTimes(1); + expect(v2Harness.actions[0]?.deviceAction).toBeInstanceOf(CallTaskInAppDeviceAction); + expect( + v2Harness.actions.map(({ deviceAction, sessionId }) => ({ + appName: deviceAction.input.appName, + requiredUserInteraction: deviceAction.input.requiredUserInteraction, + sessionId, + skipOpenApp: deviceAction.input.skipOpenApp, + })), + ).toEqual([ + { + appName: "THORChain", + requiredUserInteraction: UserInteractionRequired.SignTransaction, + sessionId: "thor-session", + skipOpenApp: false, + }, + ]); + expect( + v2Harness.exchanges.map(({ cla, data, ins, p1, p2 }) => ({ cla, dataLength: data.length, ins, p1, p2 })), + ).toEqual([ + { cla: 0x55, dataLength: 20, ins: 0x02, p1: 0x00, p2: 0x00 }, + { cla: 0x55, dataLength: 250, ins: 0x02, p1: 0x01, p2: 0x00 }, + { cla: 0x55, dataLength: 250, ins: 0x02, p1: 0x01, p2: 0x00 }, + { cla: 0x55, dataLength: 1, ins: 0x02, p1: 0x02, p2: 0x00 }, + ]); + expect(v2Progress).toEqual([ + { requiredUserInteraction: UserInteractionRequired.SignTransaction, status: DeviceActionStatus.Pending }, + { requiredUserInteraction: null, status: DeviceActionStatus.Completed }, + ]); + expect(v2Harness.publicSendCommand).toHaveBeenCalledTimes(0); + expect(v2Harness.disconnect).toHaveBeenCalledTimes(0); + + const v1Harness = createThorDmkHarness({ version: "1.9.0" }); + const v1Progress: Array<{ requiredUserInteraction: string | null; status: string }> = []; + const v1Ledger = new THORChainLedger({ + derivationPath: [44, 931, 0, 0, 0], + dmkSession: v1Harness.dmkSession, + onDeviceActionState: (state) => { + v1Progress.push({ + requiredUserInteraction: + state.status === DeviceActionStatus.Pending ? state.intermediateValue.requiredUserInteraction : null, + status: state.status, + }); + }, + }); + + await expect(v1Ledger.sign("x")).rejects.toMatchObject({ + _tag: "InvalidResponseFormatError", + originalError: { message: "THORChain Ledger app major 2 is required, received 1.9.0" }, + }); + expect(v1Harness.executeDeviceAction).toHaveBeenCalledTimes(1); + expect(v1Harness.actions[0]?.deviceAction).toBeInstanceOf(CallTaskInAppDeviceAction); + expect( + v1Harness.actions.map(({ deviceAction, sessionId }) => ({ + appName: deviceAction.input.appName, + requiredUserInteraction: deviceAction.input.requiredUserInteraction, + sessionId, + skipOpenApp: deviceAction.input.skipOpenApp, + })), + ).toEqual([ + { + appName: "THORChain", + requiredUserInteraction: UserInteractionRequired.SignTransaction, + sessionId: "thor-session", + skipOpenApp: false, + }, + ]); + expect(v1Harness.exchanges).toEqual([]); + expect(v1Progress).toEqual([ + { requiredUserInteraction: UserInteractionRequired.SignTransaction, status: DeviceActionStatus.Pending }, + { requiredUserInteraction: null, status: DeviceActionStatus.Error }, + ]); + expect(v1Harness.publicSendCommand).toHaveBeenCalledTimes(0); + expect(v1Harness.disconnect).toHaveBeenCalledTimes(0); + }); + + it("keeps the complete legacy fallback signing exchange scoped to app v2", async () => { + const calls: Array<{ data: number[]; ins: number; p1: number }> = []; + const transport = { + send: (_cla: number, ins: number, p1: number, _p2: number, data = new Uint8Array()) => { + calls.push({ data: Array.from(data), ins, p1 }); + if (ins === 0) return Buffer.from([0, 2, 5, 1, 0x90, 0x00]); + if (p1 === 0) return Buffer.from([0x90, 0x00]); + return Buffer.from([0x30, 0x06, 0x02, 0x01, 0x01, 0x02, 0x01, 0x02, 0x90, 0x00]); + }, + } as unknown as Transport; + const ledger = new THORChainLedger({ derivationPath: [44, 931, 0, 0, 0], transport }); + + const signature = await ledger.sign("hello"); + + expect(Buffer.from(base64.decode(signature)).toString("hex")).toBe(`${"00".repeat(31)}01${"00".repeat(31)}02`); + expect(calls).toEqual([ + { data: [], ins: 0, p1: 0 }, + { data: [44, 0, 0, 128, 163, 3, 0, 128, 0, 0, 0, 128, 0, 0, 0, 0, 0, 0, 0, 0], ins: 2, p1: 0 }, + { data: [104, 101, 108, 108, 111], ins: 2, p1: 2 }, + ]); }); }); diff --git a/packages/wallet-hardware/test/ledger-transport.test.ts b/packages/wallet-hardware/test/ledger-transport.test.ts index b5f2243..8b1baec 100644 --- a/packages/wallet-hardware/test/ledger-transport.test.ts +++ b/packages/wallet-hardware/test/ledger-transport.test.ts @@ -1,26 +1,55 @@ import { beforeEach, describe, expect, it, mock } from "bun:test"; +import { DeviceActionStatus, type DeviceManagementKit } from "@ledgerhq/device-management-kit"; import type Transport from "@ledgerhq/hw-transport"; -import { Chain } from "@swapkit/helpers"; -import { Transaction } from "ethers"; +import { Chain, WalletOption } from "@swapkit/helpers"; +import { hexlify, Signature, Transaction } from "ethers"; +import { of } from "rxjs"; // Registries that capture constructor invocations on the mocked Ledger apps. // Tests assert that each inner-factory call wires through its own transport // rather than cross-contaminating via a shared outer closure. const bitcoinAppInvocations: Array<{ currency: string; transport: unknown }> = []; const bitcoinAppXpubInvocations: Array<{ path: string; xpubVersion?: number }> = []; -const psbtAppClientInvocations: Array = []; -const psbtExtendedPubkeyInvocations: string[] = []; -const psbtWalletAddressInvocations: Array<{ addressIndex: number; change: number }> = []; -const ethereumAppInvocations: Array = []; -const ethereumSignTransactionInvocations: string[] = []; -let ethereumSignatureV = "014985"; +const ethereumBuilderInvocations: Array<{ dmk: unknown; originToken?: string; sessionId: string }> = []; +const ethereumGetAddressInvocations: Array<{ options?: { chainId?: number; checkOnDevice?: boolean }; path: string }> = + []; +const ethereumSignMessageInvocations: Array<{ message: string | Uint8Array; path: string }> = []; +const ethereumSignTransactionInvocations: Array<{ path: string; transaction: Uint8Array }> = []; +const ethereumSignTypedDataInvocations: Array<{ + path: string; + typedData: { + domain: Record; + message: Record; + primaryType: string; + types: Record>; + }; +}> = []; +let ethereumTransactionSignatureV = 84357; + +function deviceAction(output: Output) { + return { + cancel: mock(() => {}), + observable: of( + { intermediateValue: { requiredUserInteraction: "confirm-on-device" }, status: DeviceActionStatus.Pending }, + { output, status: DeviceActionStatus.Completed }, + ), + }; +} mock.module("@ledgerhq/hw-app-btc", () => ({ default: class MockBitcoinApp { + private readonly currency: string; + private readonly transport: { send?: (...params: unknown[]) => Promise }; + constructor(opts: { currency: string; transport: unknown }) { bitcoinAppInvocations.push(opts); + this.currency = opts.currency; + this.transport = opts.transport as { send?: (...params: unknown[]) => Promise }; } - getWalletPublicKey = async () => ({ bitcoinAddress: "ltc1qtestaddress" }); + getWalletPublicKey = async () => { + await this.transport.send?.(0xe0, 0x40, 0, 0, Buffer.alloc(0)); + return { bitcoinAddress: `${this.currency.toLowerCase().replace(" ", "-")}-ledger-address` }; + }; getWalletXpub = ({ path, xpubVersion }: { path: string; xpubVersion?: number }) => { bitcoinAppXpubInvocations.push({ path, xpubVersion }); return "Ltub2SSUS19CirucV6jZg6pTzmtZtxhX1JZJYK7Uq16czQkfFb5m1zf6KV24enP679G9gYHDBYSjbgHn6CJK7VTqDEEnRSsUgJGQWhhmLQV5foV"; @@ -28,64 +57,72 @@ mock.module("@ledgerhq/hw-app-btc", () => ({ }, })); -mock.module("ledger-bitcoin", () => ({ - AppClient: class MockAppClient { - constructor(transport: unknown) { - psbtAppClientInvocations.push(transport); +mock.module("@ledgerhq/device-signer-kit-ethereum", () => ({ + SignerEthBuilder: class MockSignerEthBuilder { + constructor(params: { dmk: unknown; originToken?: string; sessionId: string }) { + ethereumBuilderInvocations.push(params); } - getMasterFingerprint = async () => "deadbeef"; - getExtendedPubkey = (path: string) => { - psbtExtendedPubkeyInvocations.push(path); - return "xpub661MyMwAqRbcF8SxkT6wT9y6rL4n9wBEmc6kAMPxQ4vYXvyfZ87Z84qxdjQbaAWkj2rW6zGyFNR7fsRG3Gzdhvj1io8GZF1dgNpTiFqouBZ"; - }; - getWalletAddress = (_policy: unknown, _hmac: unknown, change: number, addressIndex: number) => { - psbtWalletAddressInvocations.push({ addressIndex, change }); - return "bc1qtestaddress"; - }; - }, - DefaultWalletPolicy: class MockDefaultWalletPolicy { - // biome-ignore lint/complexity/noUselessConstructor: skip for tests - constructor(_template: string, _key: string) {} - }, -})); - -mock.module("@ledgerhq/hw-app-eth", () => ({ - default: class MockEthereumApp { - constructor(transport: unknown) { - ethereumAppInvocations.push(transport); - } - getAddress = async () => ({ address: "0x0000000000000000000000000000000000000001" }); - signTransaction = (_path: string, unsignedTx: string) => { - ethereumSignTransactionInvocations.push(unsignedTx); - return { r: "1".padStart(64, "0"), s: "2".padStart(64, "0"), v: ethereumSignatureV }; - }; + build = () => ({ + getAddress: (path: string, options?: { chainId?: number; checkOnDevice?: boolean }) => { + ethereumGetAddressInvocations.push({ options, path }); + return deviceAction({ + address: "0x0000000000000000000000000000000000000001", + publicKey: "0xledger-public-key", + }); + }, + signMessage: (path: string, message: string | Uint8Array) => { + ethereumSignMessageInvocations.push({ message, path }); + return deviceAction({ r: `0x${"1".padStart(64, "0")}`, s: `0x${"2".padStart(64, "0")}`, v: 27 }); + }, + signTransaction: (path: string, transaction: Uint8Array) => { + ethereumSignTransactionInvocations.push({ path, transaction }); + return deviceAction({ + r: `0x${"1".padStart(64, "0")}`, + s: `0x${"2".padStart(64, "0")}`, + v: ethereumTransactionSignatureV, + }); + }, + signTypedData: ( + path: string, + typedData: { + domain: Record; + message: Record; + primaryType: string; + types: Record>; + }, + ) => { + ethereumSignTypedDataInvocations.push({ path, typedData }); + return deviceAction({ r: `0x${"1".padStart(64, "0")}`, s: `0x${"2".padStart(64, "0")}`, v: 27 }); + }, + }); }, - ledgerService: { resolveTransaction: async () => null }, })); +import { BitcoinLedger } from "../src/ledger/clients/bitcoin"; import { ArbitrumLedger, BinanceSmartChainLedger } from "../src/ledger/clients/evm"; -import { BitcoinLedger } from "../src/ledger/clients/utxo"; -import { BitcoinPsbtLedger } from "../src/ledger/clients/utxo-psbt"; +import { getLedgerClient } from "../src/ledger/helpers"; import { ledgerWallet } from "../src/ledger/index"; -describe("wallet-hardware/ledger — closure isolation with injected transport", () => { +const dmkSession = { dmk: { id: "test-dmk" } as unknown as DeviceManagementKit, sessionId: "test-session" }; + +describe("wallet-hardware/ledger", () => { beforeEach(() => { bitcoinAppInvocations.length = 0; bitcoinAppXpubInvocations.length = 0; - psbtAppClientInvocations.length = 0; - psbtExtendedPubkeyInvocations.length = 0; - psbtWalletAddressInvocations.length = 0; - ethereumAppInvocations.length = 0; + ethereumBuilderInvocations.length = 0; + ethereumGetAddressInvocations.length = 0; + ethereumSignMessageInvocations.length = 0; ethereumSignTransactionInvocations.length = 0; - ethereumSignatureV = "014985"; + ethereumSignTypedDataInvocations.length = 0; + ethereumTransactionSignatureV = 84357; }); it("BitcoinLedger: two invocations with different transports get their own BitcoinApp each", async () => { const transportA = { id: "A" } as unknown as Transport; const transportB = { id: "B" } as unknown as Transport; - const clientA = BitcoinLedger("84'/0'/0'/0/0", transportA); - const clientB = BitcoinLedger("84'/0'/0'/0/1", transportB); + const clientA = BitcoinLedger({ derivationPath: "84'/0'/0'/0/0", transport: transportA }); + const clientB = BitcoinLedger({ derivationPath: "84'/0'/0'/0/1", transport: transportB }); await clientA.connect(); await clientB.connect(); @@ -95,38 +132,13 @@ describe("wallet-hardware/ledger — closure isolation with injected transport", expect(bitcoinAppInvocations[1]?.transport).toBe(transportB); }); - it("BitcoinPsbtLedger: two invocations with different transports get their own AppClient each", async () => { - const transportA = { id: "A" } as unknown as Transport; - const transportB = { id: "B" } as unknown as Transport; - - const clientA = BitcoinPsbtLedger("84'/0'/0'/0/0", transportA); - const clientB = BitcoinPsbtLedger("84'/0'/0'/0/1", transportB); - - await clientA.connect(); - await clientB.connect(); - - expect(psbtAppClientInvocations).toHaveLength(2); - expect(psbtAppClientInvocations[0]).toBe(transportA); - expect(psbtAppClientInvocations[1]).toBe(transportB); - }); - - it("BitcoinPsbtLedger: normalizes m/ derivation paths before requesting account xpubs", async () => { - const transport = { id: "A" } as unknown as Transport; - const client = BitcoinPsbtLedger("m/84'/0'/0'/0/0", transport); - - await client.getAddress(); - - expect(psbtExtendedPubkeyInvocations).toEqual(["m/84'/0'/0'"]); - expect(psbtWalletAddressInvocations).toEqual([{ addressIndex: 0, change: 0 }]); - }); - it("BitcoinLedger: reusing the same transport across calls does not deduplicate state", async () => { // Same transport instance passed twice should still produce two distinct // BitcoinApp instances — each call's inner closure owns its btcApp. const transport = { id: "shared" } as unknown as Transport; - const clientA = BitcoinLedger("84'/0'/0'/0/0", transport); - const clientB = BitcoinLedger("84'/0'/0'/0/1", transport); + const clientA = BitcoinLedger({ derivationPath: "84'/0'/0'/0/0", transport }); + const clientB = BitcoinLedger({ derivationPath: "84'/0'/0'/0/1", transport }); await clientA.connect(); await clientB.connect(); @@ -138,9 +150,9 @@ describe("wallet-hardware/ledger — closure isolation with injected transport", it("BitcoinLedger: getExtendedPublicKey initializes the Bitcoin app before connect", async () => { const transport = { id: "xpub" } as unknown as Transport; - const client = BitcoinLedger("84'/0'/0'/0/0", transport); + const client = BitcoinLedger({ derivationPath: "84'/0'/0'/0/0", transport }); - const xpub = await client.getExtendedPublicKey("84'/0'/0'", 76067358); + const xpub = await client.getExtendedPublicKey({ path: "84'/0'/0'" }); expect(xpub).toBe( "Ltub2SSUS19CirucV6jZg6pTzmtZtxhX1JZJYK7Uq16czQkfFb5m1zf6KV24enP679G9gYHDBYSjbgHn6CJK7VTqDEEnRSsUgJGQWhhmLQV5foV", @@ -150,10 +162,15 @@ describe("wallet-hardware/ledger — closure isolation with injected transport", expect(bitcoinAppXpubInvocations).toEqual([{ path: "84'/0'/0'", xpubVersion: 76067358 }]); }); - it("ArbitrumLedger: reuses the Ethereum app initialized during connect for later transaction signing", async () => { + it("ArbitrumLedger: reuses one DSK signer and passes transaction bytes", async () => { const provider = {} as Parameters[0]["provider"]; - const transport = { id: "ARB" } as unknown as Transport; - const client = ArbitrumLedger({ provider, transport }); + const actionStatuses: string[] = []; + const client = ArbitrumLedger({ + dmkSession, + onDeviceActionState: ({ status }) => actionStatuses.push(status), + originToken: "ledger-origin-token", + provider, + }); await client.getAddress(); const signedTx = await client.signTransaction({ @@ -166,16 +183,31 @@ describe("wallet-hardware/ledger — closure isolation with injected transport", value: 0n, }); - expect(ethereumAppInvocations).toEqual([transport]); + expect(ethereumBuilderInvocations).toEqual([ + { dmk: dmkSession.dmk, originToken: "ledger-origin-token", sessionId: "test-session" }, + ]); + expect(ethereumGetAddressInvocations).toEqual([{ options: { chainId: 42161 }, path: "44'/60'/0'/0/0" }]); expect(ethereumSignTransactionInvocations).toHaveLength(1); - expect(Transaction.from(signedTx).chainId).toBe(42161n); + expect(hexlify(ethereumSignTransactionInvocations[0]?.transaction ?? new Uint8Array())).toBe( + Transaction.from({ + chainId: 42161, + data: "0x", + gasLimit: 21000n, + gasPrice: 1n, + nonce: 0, + to: "0x0000000000000000000000000000000000000002", + type: 0, + value: 0n, + }).unsignedSerialized, + ); + expect(Transaction.from(signedTx)).toMatchObject({ chainId: 42161n, type: 0 }); + expect(actionStatuses).toEqual(["pending", "completed", "pending", "completed"]); }); - it("BinanceSmartChainLedger: parses single-byte legacy Ledger v as hex", async () => { - ethereumSignatureV = "93"; + it("BinanceSmartChainLedger: accepts the EIP-155 v returned by DSK", async () => { + ethereumTransactionSignatureV = 147; const provider = {} as Parameters[0]["provider"]; - const transport = { id: "BSC" } as unknown as Transport; - const client = BinanceSmartChainLedger({ provider, transport }); + const client = BinanceSmartChainLedger({ dmkSession, provider }); const signedTx = await client.signTransaction({ data: "0x", @@ -188,31 +220,231 @@ describe("wallet-hardware/ledger — closure isolation with injected transport", }); expect(Transaction.from(signedTx).chainId).toBe(56n); + expect(Transaction.from(signedTx).signature?.networkV).toBe(147n); + }); + + it("ArbitrumLedger: UTF-8 encodes personal messages for DSK", async () => { + const provider = {} as Parameters[0]["provider"]; + const client = ArbitrumLedger({ dmkSession, provider }); + + const signature = await client.signMessage("Zażółć 💩"); + + expect(ethereumSignMessageInvocations).toHaveLength(1); + expect(hexlify(ethereumSignMessageInvocations[0]?.message as Uint8Array)).toBe( + hexlify(new TextEncoder().encode("Zażółć 💩")), + ); + expect(ethereumSignMessageInvocations[0]?.path).toBe("44'/60'/0'/0/0"); + expect(Signature.from(signature).toJSON()).toMatchObject({ + r: `0x${"1".padStart(64, "0")}`, + s: `0x${"2".padStart(64, "0")}`, + v: 27, + }); + }); + + it("ArbitrumLedger: sends complete typed data to DSK", async () => { + const provider = {} as Parameters[0]["provider"]; + const client = ArbitrumLedger({ dmkSession, provider }); + + await client.signTypedData( + { + chainId: 42161n, + name: "SwapKit", + verifyingContract: "0x0000000000000000000000000000000000000002", + version: "1", + }, + { Message: [{ name: "contents", type: "string" }] }, + { contents: "full typed data" }, + "Message", + ); + + expect(ethereumSignTypedDataInvocations).toEqual([ + { + path: "44'/60'/0'/0/0", + typedData: { + domain: { + chainId: 42161, + name: "SwapKit", + verifyingContract: "0x0000000000000000000000000000000000000002", + version: "1", + }, + message: { contents: "full typed data" }, + primaryType: "Message", + types: { + EIP712Domain: [ + { name: "name", type: "string" }, + { name: "version", type: "string" }, + { name: "chainId", type: "uint256" }, + { name: "verifyingContract", type: "address" }, + ], + Message: [{ name: "contents", type: "string" }], + }, + }, + }, + ]); + }); + + it("ArbitrumLedger: honors an explicit primary type when resolving names with an unrelated root", async () => { + const resolveName = mock((name: string) => + Promise.resolve(name === "vault.eth" ? "0x0000000000000000000000000000000000000004" : null), + ); + const provider = { resolveName } as unknown as Parameters[0]["provider"]; + const client = ArbitrumLedger({ dmkSession, provider }); + + await client.signTypedData( + { chainId: 42161n }, + { Message: [{ name: "recipient", type: "address" }], Unrelated: [{ name: "contents", type: "string" }] }, + { recipient: "vault.eth" }, + "Message", + ); + + expect(resolveName.mock.calls).toEqual([["vault.eth"]]); + expect(ethereumSignTypedDataInvocations[0]?.typedData).toMatchObject({ + message: { recipient: "0x0000000000000000000000000000000000000004" }, + primaryType: "Message", + types: { Message: [{ name: "recipient", type: "address" }], Unrelated: [{ name: "contents", type: "string" }] }, + }); + }); + + it("ArbitrumLedger: preserves EIP-2930 access lists", async () => { + const provider = {} as Parameters[0]["provider"]; + const client = ArbitrumLedger({ dmkSession, provider }); + const request = { + accessList: [{ address: "0x0000000000000000000000000000000000000003", storageKeys: [`0x${"4".repeat(64)}`] }], + data: "0x", + gasLimit: 25000n, + gasPrice: 1n, + nonce: 1, + to: "0x0000000000000000000000000000000000000002", + type: 1, + value: 0n, + }; + + await client.signTransaction(request); + + expect(hexlify(ethereumSignTransactionInvocations[0]?.transaction ?? new Uint8Array())).toBe( + Transaction.from({ ...request, chainId: 42161 }).unsignedSerialized, + ); + }); + + it("ArbitrumLedger: preserves EIP-4844 transaction fields", async () => { + const provider = {} as Parameters[0]["provider"]; + const client = ArbitrumLedger({ dmkSession, provider }); + const request = { + accessList: [], + blobVersionedHashes: [`0x01${"1".repeat(62)}`], + data: "0x", + gasLimit: 25000n, + maxFeePerBlobGas: 2n, + maxFeePerGas: 3n, + maxPriorityFeePerGas: 1n, + nonce: 2, + to: "0x0000000000000000000000000000000000000002", + type: 3, + value: 0n, + }; + + await client.signTransaction(request); + + expect(hexlify(ethereumSignTransactionInvocations[0]?.transaction ?? new Uint8Array())).toBe( + Transaction.from({ ...request, chainId: 42161 }).unsignedSerialized, + ); + }); + + it("ArbitrumLedger: resolves typed-data address names", async () => { + const resolveName = mock((name: string) => + Promise.resolve(name === "vault.eth" ? "0x0000000000000000000000000000000000000004" : null), + ); + const provider = { resolveName } as unknown as Parameters[0]["provider"]; + const client = ArbitrumLedger({ dmkSession, provider }); + + await client.signTypedData( + { chainId: 42161n, verifyingContract: "vault.eth" }, + { Message: [{ name: "recipient", type: "address" }] }, + { recipient: "vault.eth" }, + ); + + expect(resolveName.mock.calls).toEqual([["vault.eth"]]); + expect(ethereumSignTypedDataInvocations[0]?.typedData).toMatchObject({ + domain: { verifyingContract: "0x0000000000000000000000000000000000000004" }, + message: { recipient: "0x0000000000000000000000000000000000000004" }, + }); + }); + + it("ArbitrumLedger: rejects a transaction from another account", async () => { + const provider = {} as Parameters[0]["provider"]; + const client = ArbitrumLedger({ dmkSession, provider }); + + await expect( + client.signTransaction({ + from: "0x0000000000000000000000000000000000000002", + gasLimit: 21000n, + gasPrice: 1n, + nonce: 0, + to: "0x0000000000000000000000000000000000000003", + type: 0, + }), + ).rejects.toThrow("wallet_ledger_invalid_params"); + expect(ethereumSignTransactionInvocations).toHaveLength(0); }); - it("BitcoinLedger: connect reuses the app initialized by getExtendedPublicKey", async () => { + it("getLedgerClient: rejects LedgerJS transport injection for EVM", async () => { + const transport = { id: "legacy-evm" } as unknown as Transport; + + await expect(getLedgerClient({ chain: Chain.Arbitrum, transport })).rejects.toThrow("wallet_ledger_invalid_params"); + }); + + it("BitcoinLedger: explicit transport creates a fresh app for each complete operation", async () => { const transport = { id: "reused-xpub" } as unknown as Transport; - const client = BitcoinLedger("84'/0'/0'/0/0", transport); + const client = BitcoinLedger({ derivationPath: "84'/0'/0'/0/0", transport }); - await client.getExtendedPublicKey("84'/0'/0'", 76067358); + await client.getExtendedPublicKey({ path: "84'/0'/0'" }); await client.connect(); - expect(bitcoinAppInvocations).toHaveLength(1); - expect(bitcoinAppInvocations[0]?.transport).toBe(transport); + expect(bitcoinAppInvocations).toHaveLength(2); + expect(bitcoinAppInvocations.map(({ transport: invokedTransport }) => invokedTransport)).toEqual([ + transport, + transport, + ]); }); - it("connectLedger: requests Litecoin account xpubs with the Litecoin version byte", async () => { + it("connectLedger: routes every UTXO account xpub path and version", async () => { + for (const chain of [Chain.Bitcoin, Chain.BitcoinCash, Chain.Dash, Chain.Dogecoin, Chain.Litecoin, Chain.Zcash]) { + const addChain = mock(() => {}); + const connectLedger = ledgerWallet.connectLedger.connectWallet({ addChain }); + const transport = { id: chain } as unknown as Transport; + + await connectLedger([chain], undefined, { address: "provided-ledger-address", transport }); + const walletMethods = addChain.mock.calls[0]?.[0] as + | { getExtendedPublicKey?: (params?: { accountIndex?: number }) => Promise<{ xpub: string }> } + | undefined; + await walletMethods?.getExtendedPublicKey?.({ accountIndex: 0 }); + } + + expect(bitcoinAppXpubInvocations).toEqual([ + { path: "m/84'/0'/0'", xpubVersion: 76067358 }, + { path: "44'/145'/0'", xpubVersion: 76067358 }, + { path: "44'/5'/0'", xpubVersion: 76067358 }, + { path: "44'/3'/0'", xpubVersion: 49990397 }, + { path: "m/84'/2'/0'", xpubVersion: 27108450 }, + { path: "44'/133'/0'", xpubVersion: 76067358 }, + ]); + }); + + it("connectLedger: routes EVM chains through the EVM wallet methods", async () => { const addChain = mock(() => {}); const connectLedger = ledgerWallet.connectLedger.connectWallet({ addChain }); - const transport = { id: "LTC" } as unknown as Transport; - await connectLedger([Chain.Litecoin], undefined, { transport }); - const walletMethods = addChain.mock.calls[0]?.[0] as - | { getExtendedPublicKey?: (params?: { accountIndex?: number }) => Promise<{ xpub: string }> } - | undefined; - await walletMethods?.getExtendedPublicKey?.({ accountIndex: 0 }); + await connectLedger([Chain.Arbitrum], undefined, { dmkSession, originToken: "ledger-origin-token" }); - expect(bitcoinAppXpubInvocations).toEqual([{ path: "m/84'/2'/0'", xpubVersion: 27108450 }]); - expect(psbtAppClientInvocations).toHaveLength(0); + expect(addChain).toHaveBeenCalledTimes(1); + expect(addChain.mock.calls[0]?.[0]).toMatchObject({ + address: "0x0000000000000000000000000000000000000001", + chain: Chain.Arbitrum, + walletType: WalletOption.LEDGER, + }); + expect(ethereumBuilderInvocations).toEqual([ + { dmk: dmkSession.dmk, originToken: "ledger-origin-token", sessionId: "test-session" }, + ]); + expect(ethereumGetAddressInvocations).toEqual([{ options: { chainId: 42161 }, path: "44'/60'/0'/0/0" }]); }); }); diff --git a/packages/wallet-hardware/test/ledger-utxo-legacy-adapter.test.ts b/packages/wallet-hardware/test/ledger-utxo-legacy-adapter.test.ts index a81c493..4aa58fe 100644 --- a/packages/wallet-hardware/test/ledger-utxo-legacy-adapter.test.ts +++ b/packages/wallet-hardware/test/ledger-utxo-legacy-adapter.test.ts @@ -1,15 +1,16 @@ -import { describe, expect, it, mock } from "bun:test"; +import { beforeEach, describe, expect, it, mock } from "bun:test"; import { hex } from "@scure/base"; import { Chain } from "@swapkit/helpers"; import { RawTx, Transaction } from "@swapkit/utxo-signer"; const rawTxRequests: Array<{ chain: string; txid: string }> = []; +let getRawTxResponse = (_chain: string, _txid: string) => Promise.resolve("02000000000100"); mock.module("@swapkit/toolboxes/utxo", () => ({ getUtxoApi: (chain: string) => ({ getRawTx: (txid: string) => { rawTxRequests.push({ chain, txid }); - return Promise.resolve("02000000000100"); + return getRawTxResponse(chain, txid); }, }), })); @@ -17,9 +18,12 @@ mock.module("@swapkit/toolboxes/utxo", () => ({ import { extractInputsFromPsbt, signLegacyPsbtTransaction } from "../src/ledger/clients/utxo-legacy-adapter"; describe("ledger legacy UTXO adapter", () => { - it("uses embedded previous tx hex for legacy Ledger UTXO chains", async () => { + beforeEach(() => { rawTxRequests.length = 0; + getRawTxResponse = (_chain, _txid) => Promise.resolve("02000000000100"); + }); + it("uses embedded previous tx hex for legacy Ledger UTXO chains", async () => { const previousTxHex = hex.encode( RawTx.encode({ inputs: [ @@ -50,8 +54,6 @@ describe("ledger legacy UTXO adapter", () => { }); it("fetches previous tx hex for witness-only PSBT inputs", async () => { - rawTxRequests.length = 0; - const txid = "11".repeat(32); const tx = new Transaction({ allowUnknownOutputs: true }); tx.addInput({ @@ -66,6 +68,46 @@ describe("ledger legacy UTXO adapter", () => { expect(input).toMatchObject({ hash: txid, index: 1, txHex: "02000000000100", value: 12_345 }); }); + it("fetches distinct previous transactions concurrently and reuses duplicate requests", async () => { + let activeRequests = 0; + let maxActiveRequests = 0; + getRawTxResponse = async (_chain, txid) => { + activeRequests += 1; + maxActiveRequests = Math.max(maxActiveRequests, activeRequests); + await Promise.resolve(); + activeRequests -= 1; + return `raw-${txid}`; + }; + + const firstTxid = "11".repeat(32); + const secondTxid = "22".repeat(32); + const tx = new Transaction({ allowUnknownOutputs: true }); + for (const [txid, index] of [ + [firstTxid, 0], + [secondTxid, 1], + [firstTxid, 2], + ] as const) { + tx.addInput({ + index, + txid: hex.decode(txid), + witnessUtxo: { amount: BigInt(index + 1), script: new Uint8Array([0, 20, ...Array(20).fill(index)]) }, + }); + } + + const inputs = await extractInputsFromPsbt(tx, Chain.Litecoin); + + expect(rawTxRequests).toEqual([ + { chain: Chain.Litecoin, txid: firstTxid }, + { chain: Chain.Litecoin, txid: secondTxid }, + ]); + expect(inputs).toMatchObject([ + { hash: firstTxid, index: 0, txHex: `raw-${firstTxid}` }, + { hash: secondTxid, index: 1, txHex: `raw-${secondTxid}` }, + { hash: firstTxid, index: 2, txHex: `raw-${firstTxid}` }, + ]); + expect(maxActiveRequests).toBe(2); + }); + it("returns finalized raw tx hex from legacy Ledger signing without PSBT finalization", async () => { const txid = "11".repeat(32); const tx = new Transaction({ allowUnknownOutputs: true }); @@ -88,4 +130,25 @@ describe("ledger legacy UTXO adapter", () => { expect(signedTxHex).toBe("0200000000"); }); + + it("reuses prepared inputs without fetching previous transactions again", async () => { + const tx = new Transaction({ allowUnknownOutputs: true }); + const inputUtxos = [{ hash: "11".repeat(32), index: 1, txHex: "02000000000100", value: 12_345 }]; + const signTransaction = mock((receivedTx: Transaction, receivedInputs: typeof inputUtxos) => { + expect(receivedTx).toBe(tx); + expect(receivedInputs).toEqual(inputUtxos); + return Promise.resolve("0200000000"); + }); + + const signedTxHex = await signLegacyPsbtTransaction({ + chain: Chain.Litecoin, + inputUtxos, + legacyClient: { signTransaction }, + tx, + }); + + expect(rawTxRequests).toEqual([]); + expect(signTransaction).toHaveBeenCalledTimes(1); + expect(signedTxHex).toBe("0200000000"); + }); }); diff --git a/packages/wallet-hardware/test/ledger-zcash-dsk.test.ts b/packages/wallet-hardware/test/ledger-zcash-dsk.test.ts new file mode 100644 index 0000000..5e3ca2c --- /dev/null +++ b/packages/wallet-hardware/test/ledger-zcash-dsk.test.ts @@ -0,0 +1,187 @@ +import { beforeEach, describe, expect, it, mock } from "bun:test"; +import { DeviceActionStatus, type DeviceManagementKit } from "@ledgerhq/device-management-kit"; +import { hex } from "@scure/base"; +import type { UTXOType } from "@swapkit/toolboxes/utxo"; +import { ZcashConsensusBranchId, ZcashTransaction, ZcashVersionGroupId } from "@swapkit/utxo-signer"; +import { of } from "rxjs"; + +const addressCalls: Array<{ options?: { checkOnDevice?: boolean }; path: string }> = []; +const signCalls: unknown[] = []; +let signedRaw = ""; + +function deviceAction(output: Output) { + return { + cancel: mock(() => {}), + observable: of( + { intermediateValue: { requiredUserInteraction: "confirm-on-device" }, status: DeviceActionStatus.Pending }, + { output, status: DeviceActionStatus.Completed }, + ), + }; +} + +mock.module("@ledgerhq/device-signer-kit-zcash", () => ({ + SignerZcashBuilder: class MockSignerZcashBuilder { + build() { + return { + getAddress: (path: string, options?: { checkOnDevice?: boolean }) => { + addressCalls.push({ options, path }); + return deviceAction({ + address: "t1ledgerdsk", + chainCode: new Uint8Array(32).fill(3), + publicKey: Uint8Array.from([4, ...new Uint8Array(64).fill(5)]), + }); + }, + signTransaction: (args: unknown) => { + signCalls.push(args); + return deviceAction(`0x${signedRaw}`); + }, + }; + } + }, +})); + +import type { LegacyCreateTransactionArg } from "@ledgerhq/device-signer-kit-zcash"; +import { ZcashLedger } from "../src/ledger/clients/zcash"; + +const dmkSession = { dmk: { id: "zcash-dmk" } as unknown as DeviceManagementKit, sessionId: "zec-session" }; + +function uint32LE(value: number) { + const bytes = new Uint8Array(4); + new DataView(bytes.buffer).setUint32(0, value >>> 0, true); + return bytes; +} + +function uint64LE(value: bigint) { + const bytes = new Uint8Array(8); + new DataView(bytes.buffer).setBigUint64(0, value, true); + return bytes; +} + +function realV5Transaction({ + branchId = ZcashConsensusBranchId.NU6_2, + expiryHeight = 3_364_700, + outputAmount = 5_000n, + outputScript = Uint8Array.of(0x51), +}: { + branchId?: number; + expiryHeight?: number; + outputAmount?: bigint; + outputScript?: Uint8Array; +} = {}) { + return hex.encode( + Uint8Array.from([ + ...uint32LE(0x80000005), + ...uint32LE(ZcashVersionGroupId.NU5), + ...uint32LE(branchId), + ...uint32LE(0), + ...uint32LE(expiryHeight), + 0, + 1, + ...uint64LE(outputAmount), + outputScript.length, + ...outputScript, + 0, + 0, + 0, + ]), + ); +} + +function targetTransaction(consensusBranchId = ZcashConsensusBranchId.NU6_2) { + const tx = new ZcashTransaction({ + consensusBranchId, + expiryHeight: consensusBranchId === ZcashConsensusBranchId.NU6_2 ? 3_364_701 : 3_428_200, + lockTime: 12, + version: 5, + versionGroupId: ZcashVersionGroupId.NU5, + }); + tx.addInput({ index: 0, sequence: 0xfffffffd, txid: new Uint8Array(32).fill(7), value: 5_000n }); + tx.addOutput({ amount: 4_500n, script: Uint8Array.of(0x51) }); + return tx; +} + +function inputUtxo(txHex = realV5Transaction()): UTXOType { + return { hash: "ab".repeat(32), index: 0, txHex, value: 5_000 }; +} + +describe("Ledger Zcash Device Signer Kit client", () => { + beforeEach(() => { + addressCalls.length = 0; + signCalls.length = 0; + signedRaw = realV5Transaction({ outputAmount: 4_500n }); + }); + + it("maps a real v5 previous transaction into the legacy DSK argument", async () => { + const previousRaw = realV5Transaction(); + const tx = targetTransaction(); + const states: string[] = []; + const client = ZcashLedger({ + derivationPath: "m/44'/133'/0'/0/0", + dmkSession, + onDeviceActionState: ({ status }) => states.push(status), + }); + + const raw = await client.signTransaction({ inputUtxos: [inputUtxo(previousRaw)], tx }); + + expect(raw).toBe(signedRaw); + expect(signCalls).toHaveLength(1); + const args = signCalls[0] as LegacyCreateTransactionArg; + expect(args).toMatchObject({ + additionals: ["zcash", "sapling"], + associatedKeysets: ["44'/133'/0'/0/0"], + blockHeight: 3_364_600, + changePath: "44'/133'/0'/0/0", + lockTime: 12, + outputScriptHex: "0194110000000000000151", + }); + expect(args.expiryHeight).toEqual(uint32LE(3_364_701)); + expect(args.inputs[0]?.[1]).toBe(0); + expect(args.inputs[0]?.[3]).toBe(0xfffffffd); + expect(args.inputs[0]?.[0].serializedPreviousTransactionOverride).toEqual(hex.decode(previousRaw)); + expect(args.inputs[0]?.[0].outputs?.[0]).toEqual({ amount: uint64LE(5_000n), script: Uint8Array.of(0x51) }); + expect(states).toEqual(["pending", "completed"]); + }); + + it("uses the Ironwood activation height and exact multi-input paths", async () => { + const tx = targetTransaction(ZcashConsensusBranchId.IRONWOOD); + tx.addInput({ index: 0, txid: new Uint8Array(32).fill(8), value: 5_000n }); + const client = ZcashLedger({ derivationPath: "44'/133'/4'/0/0", dmkSession }); + + await client.signTransactionWithMultiplePaths({ + derivationPaths: ["m/44'/133'/4'/0/2", "44'/133'/4'/1/9"], + inputUtxos: [inputUtxo(), inputUtxo()], + tx, + }); + + expect(signCalls).toHaveLength(1); + expect(signCalls[0]).toMatchObject({ + associatedKeysets: ["44'/133'/4'/0/2", "44'/133'/4'/1/9"], + blockHeight: 3_428_143, + }); + }); + + it("fails before device signing when an input has no real txHex", async () => { + const tx = targetTransaction(); + const client = ZcashLedger({ dmkSession }); + + await expect( + client.signTransaction({ inputUtxos: [{ hash: "ab".repeat(32), index: 0, value: 5_000 }], tx }), + ).rejects.toThrow("wallet_ledger_invalid_params"); + expect(signCalls).toHaveLength(0); + }); + + it("returns address bytes and propagates address verification state", async () => { + const states: string[] = []; + const client = ZcashLedger({ + derivationPath: "m/44'/133'/1'/1/3", + dmkSession, + onDeviceActionState: ({ status }) => states.push(status), + }); + + const address = await client.showAddressAndPubKey(); + + expect(address).toEqual({ address: "t1ledgerdsk", chainCode: "03".repeat(32), publicKey: `04${"05".repeat(64)}` }); + expect(addressCalls).toEqual([{ options: { checkOnDevice: true }, path: "44'/133'/1'/1/3" }]); + expect(states).toEqual(["pending", "completed"]); + }); +}); diff --git a/packages/wallet-hardware/test/ledger-zcash-legacy-prevtx.test.ts b/packages/wallet-hardware/test/ledger-zcash-legacy-prevtx.test.ts new file mode 100644 index 0000000..8c22206 --- /dev/null +++ b/packages/wallet-hardware/test/ledger-zcash-legacy-prevtx.test.ts @@ -0,0 +1,79 @@ +import { describe, expect, it, mock } from "bun:test"; +import { PCZT } from "@swapkit/utxo-signer"; + +import { resolveZcashPreviousTransaction, resolveZcashPreviousTransactions } from "../src/ledger/clients/utxo"; + +describe("legacy Ledger Zcash previous transactions", () => { + it("requires and preserves the full raw previous transaction", async () => { + const getRawTx = mock((_txid: string) => Promise.resolve("050000800a27a726real-previous-transaction")); + const txid = new Uint8Array(Array.from({ length: 32 }, (_, index) => index)); + const pczt = new PCZT(); + pczt.addInput({ index: 3, scriptPubkey: new Uint8Array([0x76, 0xa9, 0x14, 0x88, 0xac]), txid, value: 123_456n }); + const input = PCZT.fromHex(pczt.toHex()).getInput(0); + + const resolved = await resolveZcashPreviousTransaction({ getRawTx, input, inputIndex: 2 }); + + expect(getRawTx).toHaveBeenCalledWith( + Array.from(txid) + .map((byte) => byte.toString(16).padStart(2, "0")) + .join(""), + ); + expect(resolved).toMatchObject({ + hash: "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", + index: 3, + txHex: "050000800a27a726real-previous-transaction", + value: 123_456, + witnessUtxo: { script: input.scriptPubkey, value: 123_456 }, + }); + }); + + it("resolves distinct inputs concurrently and fetches a shared previous transaction once", async () => { + let activeRequests = 0; + let maxActiveRequests = 0; + const getRawTx = mock(async (txid: string) => { + activeRequests += 1; + maxActiveRequests = Math.max(maxActiveRequests, activeRequests); + await Promise.resolve(); + activeRequests -= 1; + return `raw-${txid}`; + }); + const firstTxid = new Uint8Array(32).fill(0x11); + const secondTxid = new Uint8Array(32).fill(0x22); + const inputs = [ + { index: 0, scriptPubkey: new Uint8Array([0x51]), txid: firstTxid, value: 1n }, + { index: 1, scriptPubkey: new Uint8Array([0x52]), txid: secondTxid, value: 2n }, + { index: 2, scriptPubkey: new Uint8Array([0x53]), txid: firstTxid, value: 3n }, + ]; + + const resolved = await resolveZcashPreviousTransactions({ getRawTx, inputs }); + + expect(getRawTx.mock.calls.map(([txid]) => txid)).toEqual(["11".repeat(32), "22".repeat(32)]); + expect(resolved).toMatchObject([ + { hash: "11".repeat(32), index: 0, txHex: `raw-${"11".repeat(32)}`, value: 1 }, + { hash: "22".repeat(32), index: 1, txHex: `raw-${"22".repeat(32)}`, value: 2 }, + { hash: "11".repeat(32), index: 2, txHex: `raw-${"11".repeat(32)}`, value: 3 }, + ]); + expect(maxActiveRequests).toBe(2); + }); + + it("fails before signing when the API cannot provide a real previous transaction", async () => { + const getRawTx = mock((_txid: string) => Promise.resolve("")); + + await expect( + resolveZcashPreviousTransaction({ + getRawTx, + input: { index: 0, scriptPubkey: new Uint8Array([0x51]), txid: new Uint8Array(32).fill(0xaa), value: 1n }, + inputIndex: 4, + }), + ).rejects.toMatchObject({ + cause: { + chain: "ZEC", + inputIndex: 4, + reason: "Unable to resolve previous transaction hex for Ledger signing", + txid: "aa".repeat(32), + }, + errorKey: "wallet_ledger_invalid_params", + }); + expect(getRawTx).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/wallets/test/metamask.test.ts b/packages/wallets/test/metamask.test.ts index a8f5c2e..277ec1d 100644 --- a/packages/wallets/test/metamask.test.ts +++ b/packages/wallets/test/metamask.test.ts @@ -1,5 +1,6 @@ import { beforeEach, describe, expect, mock, test } from "bun:test"; import { Chain, SwapKitError, WalletOption } from "@swapkit/helpers"; +import * as ethersModule from "ethers"; const SOLANA_MAINNET_CAIP2 = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp"; const ETHEREUM_ADDRESS = "0x1111111111111111111111111111111111111111"; @@ -52,7 +53,7 @@ mock.module("@metamask/connect-multichain", () => ({ }, })); -mock.module("ethers", () => ({ BrowserProvider: MockBrowserProvider })); +mock.module("ethers", () => ({ ...ethersModule, BrowserProvider: MockBrowserProvider })); mock.module("@swapkit/wallet-extensions/evm-extensions", () => ({ getWeb3WalletMethods: (options: Record) => { diff --git a/tools/builder/index.ts b/tools/builder/index.ts index ed95abc..6d1da91 100644 --- a/tools/builder/index.ts +++ b/tools/builder/index.ts @@ -4,11 +4,19 @@ const isDebug = process.env.DEBUG === "true"; const sizeData: Record = {}; export async function buildPackage({ + bundlePackages, entrypoints: packageEntrypoints, evmOnly, + external = [], + packages, ...rest -}: Omit & { evmOnly?: boolean; entrypoints?: string[] } = {}) { - const { exports, name: pkgName } = (await Bun.file("package.json").json()) as { +}: Omit & { bundlePackages?: string[]; evmOnly?: boolean; entrypoints?: string[] } = {}) { + const { + dependencies = {}, + exports, + name: pkgName, + } = (await Bun.file("package.json").json()) as { + dependencies?: Record; exports: Record; name: string; }; @@ -21,12 +29,20 @@ export async function buildPackage({ console.info("Entrypoints:", entrypoints); } + const bundledDependencies = new Set(bundlePackages); + const packageExternals = bundlePackages + ? Object.keys(dependencies) + .filter((dependency) => !bundledDependencies.has(dependency)) + .flatMap((dependency) => [dependency, `${dependency}/*`]) + : []; + const buildOptions: BuildConfig = { define: { "process.env.NODE_ENV": JSON.stringify(isDebug ? "development" : "production") }, entrypoints, + external: [...packageExternals, ...external], minify: !isDebug, outdir: "./dist", - packages: "external", + packages: bundlePackages ? "bundle" : (packages ?? "external"), sourcemap: "linked", splitting: !pkgName.includes("toolboxes"), ...rest, @@ -35,9 +51,9 @@ export async function buildPackage({ const buildESM = await Bun.build(buildOptions); const buildCJS = evmOnly ? { logs: [], outputs: [], success: true } - : await Bun.build({ ...buildOptions, format: "cjs", naming: "[dir]/[name].cjs" }); + : await Bun.build({ ...buildOptions, format: "cjs", naming: "[dir]/[name].cjs", splitting: false }); - if (!(buildESM.success || buildCJS.success)) { + if (!buildESM.success || !buildCJS.success) { throw new AggregateError(buildESM.logs.concat(buildCJS.logs), "Build failed"); }