From aca80878fbb5cadde8d0315c935f07cabc7d75e2 Mon Sep 17 00:00:00 2001 From: Ice Date: Thu, 20 Aug 2026 08:14:00 +0200 Subject: [PATCH 01/27] feat(wallet-hardware): migrate Ledger integrations to DMK --- bun.lock | 356 ++++++++++----- packages/wallet-hardware/build.ts | 13 +- packages/wallet-hardware/package.json | 20 +- .../src/ledger/clients/bitcoin.ts | 397 +++++++++++++++++ .../src/ledger/clients/cosmos.ts | 201 ++++++--- .../wallet-hardware/src/ledger/clients/evm.ts | 372 ++++++++-------- .../src/ledger/clients/near.ts | 53 ++- .../wallet-hardware/src/ledger/clients/sui.ts | 119 +++-- .../src/ledger/clients/thorchain/common.ts | 93 ---- .../src/ledger/clients/thorchain/helpers.ts | 120 ----- .../src/ledger/clients/thorchain/index.ts | 256 +++++++---- .../src/ledger/clients/thorchain/lib.ts | 258 ----------- .../src/ledger/clients/thorchain/protocol.ts | 163 +++++++ .../src/ledger/clients/thorchain/utils.ts | 69 --- .../src/ledger/clients/tron.ts | 84 ++-- .../src/ledger/clients/utxo.ts | 264 ++++++----- .../wallet-hardware/src/ledger/clients/xrp.ts | 48 +- .../src/ledger/clients/zcash.ts | 412 ++++++++++++++++++ .../wallet-hardware/src/ledger/helpers/dmk.ts | 139 ++++++ .../src/ledger/helpers/executeDeviceAction.ts | 59 +++ .../src/ledger/helpers/getLedgerClient.ts | 177 +++++--- .../src/ledger/helpers/index.ts | 6 + .../src/ledger/helpers/ledgerJsDmkBridge.ts | 167 +++++++ .../src/ledger/helpers/ledgerNetworkInfo.ts | 127 ------ packages/wallet-hardware/src/ledger/index.ts | 214 +++++++-- .../interfaces/CosmosLedgerInterface.ts | 67 --- packages/wallet-hardware/src/ledger/types.ts | 25 +- .../test/ledger-evm-clear-signing.test.ts | 361 --------------- tools/builder/index.ts | 24 +- 29 files changed, 2712 insertions(+), 1952 deletions(-) create mode 100644 packages/wallet-hardware/src/ledger/clients/bitcoin.ts delete mode 100644 packages/wallet-hardware/src/ledger/clients/thorchain/common.ts delete mode 100644 packages/wallet-hardware/src/ledger/clients/thorchain/helpers.ts delete mode 100644 packages/wallet-hardware/src/ledger/clients/thorchain/lib.ts create mode 100644 packages/wallet-hardware/src/ledger/clients/thorchain/protocol.ts delete mode 100644 packages/wallet-hardware/src/ledger/clients/thorchain/utils.ts create mode 100644 packages/wallet-hardware/src/ledger/clients/zcash.ts create mode 100644 packages/wallet-hardware/src/ledger/helpers/dmk.ts create mode 100644 packages/wallet-hardware/src/ledger/helpers/executeDeviceAction.ts create mode 100644 packages/wallet-hardware/src/ledger/helpers/ledgerJsDmkBridge.ts delete mode 100644 packages/wallet-hardware/src/ledger/helpers/ledgerNetworkInfo.ts delete mode 100644 packages/wallet-hardware/src/ledger/interfaces/CosmosLedgerInterface.ts delete mode 100644 packages/wallet-hardware/test/ledger-evm-clear-signing.test.ts diff --git a/bun.lock b/bun.lock index 90dd399..4496913 100644 --- a/bun.lock +++ b/bun.lock @@ -31,7 +31,7 @@ }, "packages/sdk": { "name": "@swapkit/sdk", - "version": "4.6.51", + "version": "4.6.52", "dependencies": { "@stricahq/typhonjs": "~3.0.1", "@swapkit/core": "^5.0.8", @@ -89,17 +89,23 @@ }, "packages/wallet-hardware": { "name": "@swapkit/wallet-hardware", - "version": "4.10.0", + "version": "4.11.0", "dependencies": { "@cosmjs/amino": "~0.37.0", "@cosmjs/crypto": "0.37.0", "@cosmjs/proto-signing": "~0.37.0", "@keepkey/keepkey-sdk": "~0.2.62", + "@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/devices": "~8.10.0", "@ledgerhq/errors": "~6.29.0", "@ledgerhq/hw-app-btc": "~10.17.0", "@ledgerhq/hw-app-cosmos": "~6.33.0", - "@ledgerhq/hw-app-eth": "~7.8.18", "@ledgerhq/hw-app-near": "~6.32.0", "@ledgerhq/hw-app-sui": "~1.5.0", "@ledgerhq/hw-app-trx": "~6.32.0", @@ -117,10 +123,12 @@ "@swapkit/utxo-signer": "^3.1.0", "@swapkit/wallet-core": "^5.0.8", "@trezor/connect-web": "~9.7.3", + "bitcoinjs-lib": "~6.1.7", "cosmjs-types": "~0.10.1", "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", }, @@ -129,11 +137,17 @@ "@cosmjs/crypto": "0.37.0", "@cosmjs/proto-signing": "0.37.0", "@keepkey/keepkey-sdk": "0.2.62", + "@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/devices": "8.10.0", "@ledgerhq/errors": "6.29.0", "@ledgerhq/hw-app-btc": "10.17.0", "@ledgerhq/hw-app-cosmos": "6.33.0", - "@ledgerhq/hw-app-eth": "7.8.18", "@ledgerhq/hw-app-near": "6.32.0", "@ledgerhq/hw-app-sui": "1.5.0", "@ledgerhq/hw-app-trx": "6.32.0", @@ -147,10 +161,12 @@ "@scure/base": "2.2.0", "@scure/bip32": "2.2.0", "@trezor/connect-web": "9.7.3", + "bitcoinjs-lib": "6.1.7", "cosmjs-types": "0.10.1", "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", }, @@ -166,7 +182,7 @@ }, "packages/wallets": { "name": "@swapkit/wallets", - "version": "4.13.0", + "version": "4.13.1", "dependencies": { "@coinbase/wallet-sdk": "~4.3.7", "@cosmjs/amino": "~0.37.0", @@ -485,42 +501,6 @@ "@ethereumjs/util": ["@ethereumjs/util@10.1.3", "", { "dependencies": { "@ethereumjs/rlp": "^10.1.3", "@noble/curves": "^2.0.1", "@noble/hashes": "^2.0.1" } }, "sha512-PhezOVe4DFaC+QLIsnK/tiOaXjpjS2F9AH4Ez0EfWevFw9NWNSg95l7I0dWuHF0B7v9odm2Ijqwo0EnARXtUmA=="], - "@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.8.0", "", { "dependencies": { "@floating-ui/utils": "^0.2.12" } }, "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ=="], @@ -549,6 +529,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=="], @@ -563,20 +553,28 @@ "@keplr-wallet/types": ["@keplr-wallet/types@0.13.24", "", { "dependencies": { "long": "^4.0.0" }, "peerDependencies": { "starknet": "^8" } }, "sha512-r5wXlvmVYqPS1PwRad0tQRhdRKJj3CpWcnYa78OzmyCHOJA17xHnkNW+TQJwiw3YGIq3r3laH3nYYEkhf/fFaQ=="], - "@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/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/domain-service": ["@ledgerhq/domain-service@1.8.18", "", { "dependencies": { "@ledgerhq/logs": "6.19.0", "@ledgerhq/types-live": "^6.123.0", "axios": "1.13.5", "eip55": "^2.1.1", "react": "19.1.4", "react-dom": "19.1.4" } }, "sha512-lJ+r7+hjH1C9uRq5Fkr7lMKY+z4o1kl2Huv4Su6iCasCmoWM0VDiyxhHV8Wm/4+qtlJbQkJw2gTjp0fTC8ekvA=="], + "@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/errors": ["@ledgerhq/errors@6.29.0", "", {}, "sha512-mmDsGN662zd0XGKyjzSKkg+5o1/l9pvV1HkVHtbzaydvHAtRypghmVoWMY9XAQDLXiUBXGIsLal84NgmGeuKWA=="], + "@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/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/evm-tools": ["@ledgerhq/evm-tools@1.14.3", "", { "dependencies": { "@ethersproject/constants": "^5.7.0", "@ethersproject/hash": "^5.7.0", "@ledgerhq/live-env": "^4.0.0", "axios": "1.13.5", "crypto-js": "4.2.0" } }, "sha512-pA18WpvtpM/MESCz4Tyg/8MD8kybZJyvYxtqK8lxvUcq6CHY+nxVb/D85URWGFmKkXQ3xmzRsR2bRdPsiwzEsg=="], + "@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/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.8.18", "", { "dependencies": { "@ethersproject/abi": "^5.7.0", "@ethersproject/rlp": "^5.7.0", "@ethersproject/transactions": "^5.7.0", "@ledgerhq/domain-service": "^1.8.18", "@ledgerhq/evm-tools": "^1.14.3", "@ledgerhq/hw-transport": "6.35.7", "@ledgerhq/hw-transport-mocker": "^6.34.7", "@ledgerhq/logs": "6.19.0", "@ledgerhq/types-live": "^6.123.0", "axios": "1.13.5", "bignumber.js": "^9.1.2", "semver": "7.7.3" } }, "sha512-wHjio1y/VRYqQDTscbGwQXRWqfk+GAbwGH4vM5Aerjq2UqXAt3+TG/k0BYjmdcyJ1mp+/EL1DecxhAkxKeKsfA=="], - "@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,19 +585,15 @@ "@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.7", "", { "dependencies": { "@ledgerhq/hw-transport": "6.35.7", "@ledgerhq/logs": "^6.17.0", "rxjs": "7.8.2" } }, "sha512-AyaO4unEHhZbyyo9y16z36uOi9pqY6kqVtWhTAp1PLZuQhXR/Y0m+Yd3yJrjUvlNpIOrrVN1rA+gHlmewl43Og=="], - "@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@4.0.0", "", {}, "sha512-H4U9UcaKp1+rg1CqQfzy430pxhSGlRE6B+kj4oMd0YYepvqTzOzb1eT+Nr9TLN3o4ZIK6yTy9baco6O6McJAiQ=="], - "@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.123.0", "", { "dependencies": { "bignumber.js": "^9.1.2", "rxjs": "7.8.2" } }, "sha512-XPP0t2ZZLbnA6PPP8+e97hPEZ0cxE0F7G2VqgRJD9ezrwBl1uovCrbysMg5o3RjFQqqQSQ/+l/sBZaxg6/DdZA=="], + "@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=="], "@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=="], @@ -985,8 +979,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.24", "", {}, "sha512-91up4t0BLjfkCZ4Ap/BcgaPrMaX6n0I2YRu+kc03xGhLy9G3N05nbTpnK8q7GXdv0PxwUoxi67A8o3Wr5U7j3w=="], @@ -1243,6 +1245,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.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], @@ -1377,7 +1381,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.5", "", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q=="], + "axios": ["axios@1.18.0", "", { "dependencies": { "follow-redirects": "^1.16.0", "form-data": "^4.0.5", "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } }, "sha512-E32NzpYKp++W7XRe52rHiXV2ehxmh3wbdgO7MHeFM+vqxLBYHzt0ElkiImtOBxtOmyp0yoC8C6uESVV84Y2/hw=="], "base-x": ["base-x@4.0.1", "", {}, "sha512-uAZ8x6r6S3aUM9rbHGVOIsR15U/ZSc82b3ymnCPsT45Gk1DDvhDPdIgB5MrhirZWt+5K0EEPQH985kNqZgNPFw=="], @@ -1627,8 +1631,6 @@ "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.412", "", {}, "sha512-z4rMe3esBzlzovKHj4gxJnsCGZRK5l4baUvm+gCGJBPE+gsyUMKsuU9tnEUtI1dOebXz1ytAPGjvXhmQ7rIPwA=="], "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=="], @@ -1831,6 +1833,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.5.0", "", {}, "sha512-R5SnVLJmgYYvf2F2ZgwSBnelz5G4q5AxIC277GDfUaNbrZKNANcBC7RHqYYePlszf4kBolVkJauG0ZjHHFh55g=="], "iron-webcrypto": ["iron-webcrypto@1.2.1", "", {}, "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg=="], @@ -1885,7 +1889,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=="], @@ -1899,8 +1903,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.3.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ=="], @@ -1929,8 +1931,6 @@ "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=="], @@ -2173,7 +2173,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=="], @@ -2181,6 +2181,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=="], @@ -2239,6 +2241,8 @@ "redeyed": ["redeyed@2.1.1", "", { "dependencies": { "esprima": "~4.0.0" } }, "sha512-FNpGGo1DycYAdnrKFxCMmKYgo/mILAqtRYbkdQD8Ep/Hk2PQ5+aEAEx+IU713RTDmuBaH0c8P5ZozurNu5ObRQ=="], + "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=="], "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], @@ -2473,7 +2477,7 @@ "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], - "uuid": ["uuid@11.1.1", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ=="], + "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 +2521,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=="], @@ -2575,19 +2581,37 @@ "@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/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/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/socket/isomorphic-ws": ["isomorphic-ws@4.0.1", "", { "peerDependencies": { "ws": "*" } }, "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w=="], + "@cosmjs/socket/ws": ["ws@7.5.13", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA=="], + "@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=="], - "@ledgerhq/devices/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "@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/domain-service/@ledgerhq/logs": ["@ledgerhq/logs@6.19.0", "", {}, "sha512-kmWj18C7eTtuytdaWygzVrQm9EoD4YWuYnd/kY8stVBBAzRdJzrGlm013ijQF6I/kGLKlGY7JKmll6j32KGpfQ=="], + "@ledgerhq/device-management-kit/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], - "@ledgerhq/domain-service/react": ["react@19.1.4", "", {}, "sha512-DHINL3PAmPUiK1uszfbKiXqfE03eszdt5BpVSuEAHb5nfmNPwnsy7g39h2t8aXFc/Bv99GH81s+j8dobtD+jOw=="], + "@ledgerhq/device-management-kit/ws": ["ws@8.21.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="], - "@ledgerhq/domain-service/react-dom": ["react-dom@19.1.4", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.4" } }, "sha512-s2868ab/xo2SI6H4106A7aFI8Mrqa4xC6HZT/pBzYyQ3cBLqa88hu47xYD8xf+uECleN698Awn7RCWlkTiKnqQ=="], + "@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/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/devices/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], "@ledgerhq/hw-app-btc/@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], @@ -2599,16 +2623,10 @@ "@ledgerhq/hw-app-btc/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], - "@ledgerhq/hw-app-eth/@ledgerhq/hw-transport": ["@ledgerhq/hw-transport@6.35.7", "", { "dependencies": { "@ledgerhq/devices": "8.17.0", "@ledgerhq/errors": "^7.0.0", "@ledgerhq/logs": "^6.17.0", "events": "^3.3.0" } }, "sha512-vVhAVQ56+7A5FY5Mr09HY+bmf3H6TXpwsj+/xbadNkjl//e/YzTWfpXk4eBnj3hwk1PQ9Mn6pKFH0BHjS2BKlg=="], - - "@ledgerhq/hw-app-eth/@ledgerhq/logs": ["@ledgerhq/logs@6.19.0", "", {}, "sha512-kmWj18C7eTtuytdaWygzVrQm9EoD4YWuYnd/kY8stVBBAzRdJzrGlm013ijQF6I/kGLKlGY7JKmll6j32KGpfQ=="], - - "@ledgerhq/hw-app-eth/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], - - "@ledgerhq/hw-transport-mocker/@ledgerhq/hw-transport": ["@ledgerhq/hw-transport@6.35.7", "", { "dependencies": { "@ledgerhq/devices": "8.17.0", "@ledgerhq/errors": "^7.0.0", "@ledgerhq/logs": "^6.17.0", "events": "^3.3.0" } }, "sha512-vVhAVQ56+7A5FY5Mr09HY+bmf3H6TXpwsj+/xbadNkjl//e/YzTWfpXk4eBnj3hwk1PQ9Mn6pKFH0BHjS2BKlg=="], - "@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/signer-utils/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + "@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=="], @@ -2623,10 +2641,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.1", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ=="], + "@metamask/connect-multichain/ws": ["ws@8.21.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="], + "@metamask/mobile-wallet-protocol-core/uuid": ["uuid@11.1.1", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ=="], + "@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.1", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ=="], + "@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=="], @@ -2725,6 +2749,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=="], @@ -2837,6 +2865,12 @@ "@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.3.0", "", {}, "sha512-Clu/xdfgVTf9o7ngLOURaxePwR0j8sjclKEtVij10/jGulwFsPWCvvRgG/XjUVf8Nei+jLG6uwyXzUTGY1DQrw=="], "@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=="], @@ -2907,8 +2941,6 @@ "@stellar/stellar-base/@stellar/js-xdr": ["@stellar/js-xdr@3.1.2", "", {}, "sha512-VVolPL5goVEIsvuGqDc5uiKxV03lzfWdvYg1KikvwheDmTBO68CKDji3bAZ/kppZrx5iTA8z3Ld5yuytcvhvOQ=="], - "@stellar/stellar-sdk/axios": ["axios@1.18.0", "", { "dependencies": { "follow-redirects": "^1.16.0", "form-data": "^4.0.5", "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } }, "sha512-E32NzpYKp++W7XRe52rHiXV2ehxmh3wbdgO7MHeFM+vqxLBYHzt0ElkiImtOBxtOmyp0yoC8C6uESVV84Y2/hw=="], - "@stellar/stellar-sdk/bignumber.js": ["bignumber.js@11.1.5", "", {}, "sha512-6WmzCNtUnfKpbozq+hOgWaZMMzORmYBwF1xZScyoIX3QRYWeKTtxxwDOW5tIz7C9BdjkIYHGTcelCLkXg0mndw=="], "@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=="], @@ -2959,8 +2991,26 @@ "@swapkit/ui/react-dom": ["react-dom@19.1.1", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.1" } }, "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw=="], + "@swapkit/wallet-extensions/@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-extensions/@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-extensions/@cosmjs/stargate": ["@cosmjs/stargate@0.37.1", "", { "dependencies": { "@cosmjs/amino": "^0.37.1", "@cosmjs/encoding": "^0.37.1", "@cosmjs/math": "^0.37.1", "@cosmjs/proto-signing": "^0.37.1", "@cosmjs/stream": "^0.37.1", "@cosmjs/tendermint-rpc": "^0.37.1", "@cosmjs/utils": "^0.37.1", "cosmjs-types": "^0.10.1" } }, "sha512-nQgaJB7A81cRYtmDrcIySq8hbd+QLXJTGFEs6R1xMdxascaABX0x86nS44ALk+EZojQQRnwupOsw1dKLlQmIFg=="], + "@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=="], + + "@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=="], + "@swc/helpers/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "@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=="], @@ -3121,14 +3171,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.13", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA=="], "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.3.0", "", {}, "sha512-Clu/xdfgVTf9o7ngLOURaxePwR0j8sjclKEtVij10/jGulwFsPWCvvRgG/XjUVf8Nei+jLG6uwyXzUTGY1DQrw=="], "micro-key-producer/micro-packed": ["micro-packed@0.8.0", "", { "dependencies": { "@scure/base": "2.0.0" } }, "sha512-AKb8znIvg9sooythbXzyFeChEY0SkW0C6iXECpy/ls0e5BtwXO45J9wD9SLzBztnS4XmF/5kwZknsq+jyynd/A=="], @@ -3247,6 +3297,8 @@ "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=="], @@ -3261,29 +3313,35 @@ "@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=="], + "@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=="], - "@ledgerhq/domain-service/react-dom/scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="], + "@ledgerhq/context-module/bs58/base-x": ["base-x@5.0.1", "", {}, "sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg=="], - "@ledgerhq/hw-app-btc/bitcoinjs-lib/bech32": ["bech32@1.1.4", "", {}, "sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ=="], + "@ledgerhq/context-module/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/context-module/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/context-module/ethers/@types/node": ["@types/node@22.7.5", "", { "dependencies": { "undici-types": "~6.19.2" } }, "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ=="], - "@ledgerhq/hw-app-eth/@ledgerhq/hw-transport/@ledgerhq/devices": ["@ledgerhq/devices@8.17.0", "", { "dependencies": { "semver": "7.7.3" } }, "sha512-l+rrVQEjR1hSWOLD00LFX4zbS8yB1M/Mb6UYPmSHGO7TmE1CFbZ4CmDJC/kZSl3hdrXCJ+YUNpvaLCcSWDwA+Q=="], + "@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-eth/@ledgerhq/hw-transport/@ledgerhq/errors": ["@ledgerhq/errors@7.0.0", "", {}, "sha512-+Q/vykUlNeIxiM+I3cu1B660WLkzlmIsHLTV9QNV5D2/Ocplx3QMg52NYq2X7OAfGQnfH1rQvhn/NrjT+t9wBA=="], + "@ledgerhq/device-signer-kit-ethereum/ethers/@noble/hashes": ["@noble/hashes@1.3.2", "", {}, "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ=="], - "@ledgerhq/hw-app-eth/@ledgerhq/hw-transport/@ledgerhq/logs": ["@ledgerhq/logs@6.17.0", "", {}, "sha512-yra33g5q/AU7+PwAws+GaVpQGUuxnDREjVBnviJjcaJLVKuLzI4pnj8Bd3nY3fypM5k1yZEYKEXfUuGFUjP2+w=="], + "@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.17.0", "", { "dependencies": { "semver": "7.7.3" } }, "sha512-l+rrVQEjR1hSWOLD00LFX4zbS8yB1M/Mb6UYPmSHGO7TmE1CFbZ4CmDJC/kZSl3hdrXCJ+YUNpvaLCcSWDwA+Q=="], + "@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@7.0.0", "", {}, "sha512-+Q/vykUlNeIxiM+I3cu1B660WLkzlmIsHLTV9QNV5D2/Ocplx3QMg52NYq2X7OAfGQnfH1rQvhn/NrjT+t9wBA=="], + "@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/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=="], @@ -3361,6 +3419,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=="], @@ -3405,8 +3473,6 @@ "@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=="], @@ -3475,13 +3541,27 @@ "@swapkit/ui/react-dom/scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="], - "@swapkit/wallet-extensions/@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=="], + "@swapkit/wallet-extensions/@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-extensions/@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/@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-extensions/@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=="], + "@swapkit/wallet-hardware/@near-js/crypto/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], - "@ton/ton/axios/proxy-from-env": ["proxy-from-env@2.1.0", "", {}, "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA=="], + "@swapkit/wallet-hardware/@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.18.0", "", { "dependencies": { "follow-redirects": "^1.16.0", "form-data": "^4.0.5", "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } }, "sha512-E32NzpYKp++W7XRe52rHiXV2ehxmh3wbdgO7MHeFM+vqxLBYHzt0ElkiImtOBxtOmyp0yoC8C6uESVV84Y2/hw=="], + "@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=="], + + "@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=="], + + "@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/eventsource": ["eventsource@2.0.2", "", {}, "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA=="], @@ -3493,8 +3573,6 @@ "@trezor/blockchain-link-utils/xrpl/@scure/bip39": ["@scure/bip39@1.6.0", "", { "dependencies": { "@noble/hashes": "~1.8.0", "@scure/base": "~1.2.5" } }, "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A=="], - "@trezor/blockchain-link/@stellar/stellar-sdk/axios": ["axios@1.18.0", "", { "dependencies": { "follow-redirects": "^1.16.0", "form-data": "^4.0.5", "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } }, "sha512-E32NzpYKp++W7XRe52rHiXV2ehxmh3wbdgO7MHeFM+vqxLBYHzt0ElkiImtOBxtOmyp0yoC8C6uESVV84Y2/hw=="], - "@trezor/blockchain-link/@stellar/stellar-sdk/eventsource": ["eventsource@2.0.2", "", {}, "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA=="], "@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=="], @@ -3607,11 +3685,25 @@ "@cosmjs/proto-signing/@cosmjs/crypto/@scure/bip39/@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="], + "@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=="], + + "@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=="], + "@cosmjs/tendermint-rpc/@cosmjs/crypto/@scure/bip39/@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="], - "@ledgerhq/hw-app-btc/bitcoinjs-lib/bip32/@types/node": ["@types/node@10.12.18", "", {}, "sha512-fh+pAqt4xRzPfqA6eh3Z2y6fyZavRIumvjhaCL753+TVkGKGhpPeyrJG2JftD0T9q4GF00KjefsQ+PQNDdWQaQ=="], + "@ledgerhq/context-module/ethers/@types/node/undici-types": ["undici-types@6.19.8", "", {}, "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="], - "@ledgerhq/hw-transport-mocker/@ledgerhq/hw-transport/@ledgerhq/devices/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "@ledgerhq/device-signer-kit-ethereum/ethers/@types/node/undici-types": ["undici-types@6.19.8", "", {}, "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="], + + "@ledgerhq/hw-app-btc/bitcoinjs-lib/bip32/@types/node": ["@types/node@10.12.18", "", {}, "sha512-fh+pAqt4xRzPfqA6eh3Z2y6fyZavRIumvjhaCL753+TVkGKGhpPeyrJG2JftD0T9q4GF00KjefsQ+PQNDdWQaQ=="], "@ledgerhq/psbtv2/bitcoinjs-lib/bip32/@types/node": ["@types/node@10.12.18", "", {}, "sha512-fh+pAqt4xRzPfqA6eh3Z2y6fyZavRIumvjhaCL753+TVkGKGhpPeyrJG2JftD0T9q4GF00KjefsQ+PQNDdWQaQ=="], @@ -3647,6 +3739,8 @@ "@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=="], @@ -3667,11 +3761,47 @@ "@swapkit/ui/@near-js/crypto/@noble/curves/@noble/hashes": ["@noble/hashes@1.7.1", "", {}, "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ=="], - "@swapkit/wallet-extensions/@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=="], + "@swapkit/wallet-extensions/@cosmjs/amino/@cosmjs/crypto/@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], + + "@swapkit/wallet-extensions/@cosmjs/amino/@cosmjs/crypto/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], + + "@swapkit/wallet-extensions/@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-extensions/@cosmjs/proto-signing/@cosmjs/crypto/@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], + + "@swapkit/wallet-extensions/@cosmjs/proto-signing/@cosmjs/crypto/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], + + "@swapkit/wallet-extensions/@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/@cosmjs/amino/@cosmjs/crypto/@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], + + "@swapkit/wallet-hardware/@cosmjs/amino/@cosmjs/crypto/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], + + "@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-extensions/@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=="], + "@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=="], - "@trezor/blockchain-link-utils/@stellar/stellar-sdk/axios/proxy-from-env": ["proxy-from-env@2.1.0", "", {}, "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA=="], + "@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=="], + + "@swapkit/wallets/@cosmjs/amino/@cosmjs/crypto/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], + + "@swapkit/wallets/@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/wallets/@cosmjs/proto-signing/@cosmjs/crypto/@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], + + "@swapkit/wallets/@cosmjs/proto-signing/@cosmjs/crypto/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], + + "@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/@trezor/protobuf/protobufjs/long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], @@ -3685,8 +3815,6 @@ "@trezor/blockchain-link-utils/xrpl/@scure/bip39/@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="], - "@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=="], @@ -3713,30 +3841,34 @@ "ripemd160/hash-base/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], + "@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/ws": ["ws@7.5.13", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA=="], + "@swapkit/ui/@cosmjs/stargate/@cosmjs/tendermint-rpc/@cosmjs/socket/isomorphic-ws": ["isomorphic-ws@4.0.1", "", { "peerDependencies": { "ws": "*" } }, "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w=="], - "@swapkit/wallet-extensions/@cosmjs/stargate/@cosmjs/amino/@cosmjs/crypto/@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], + "@swapkit/ui/@cosmjs/stargate/@cosmjs/tendermint-rpc/@cosmjs/socket/ws": ["ws@7.5.13", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA=="], - "@swapkit/wallet-extensions/@cosmjs/stargate/@cosmjs/amino/@cosmjs/crypto/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], + "@swapkit/wallet-extensions/@cosmjs/amino/@cosmjs/crypto/@scure/bip39/@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="], - "@swapkit/wallet-extensions/@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=="], + "@swapkit/wallet-extensions/@cosmjs/proto-signing/@cosmjs/crypto/@scure/bip39/@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="], - "@swapkit/wallet-extensions/@cosmjs/stargate/@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/amino/@cosmjs/crypto/@scure/bip39/@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="], - "@swapkit/wallet-extensions/@cosmjs/stargate/@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/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="], - "@swapkit/wallet-extensions/@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=="], + "@swapkit/wallets/@cosmjs/amino/@cosmjs/crypto/@scure/bip39/@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="], - "pkg-dir/find-up/locate-path/p-locate/p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], + "@swapkit/wallets/@cosmjs/proto-signing/@cosmjs/crypto/@scure/bip39/@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="], - "@swapkit/wallet-extensions/@cosmjs/stargate/@cosmjs/amino/@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=="], - "@swapkit/wallet-extensions/@cosmjs/stargate/@cosmjs/proto-signing/@cosmjs/crypto/@scure/bip39/@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="], + "pkg-dir/find-up/locate-path/p-locate/p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], } } diff --git a/packages/wallet-hardware/build.ts b/packages/wallet-hardware/build.ts index 392a109..ee426cd 100644 --- a/packages/wallet-hardware/build.ts +++ b/packages/wallet-hardware/build.ts @@ -1,3 +1,14 @@ import { buildPackage } from "../../tools/builder"; -void buildPackage(); +void buildPackage({ + bundlePackages: [ + "@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", + "@ledgerhq/hw-transport", + ], +}); diff --git a/packages/wallet-hardware/package.json b/packages/wallet-hardware/package.json index 5a1af8e..6f207c4 100644 --- a/packages/wallet-hardware/package.json +++ b/packages/wallet-hardware/package.json @@ -5,11 +5,17 @@ "@cosmjs/crypto": "0.37.0", "@cosmjs/proto-signing": "~0.37.0", "@keepkey/keepkey-sdk": "~0.2.62", + "@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/devices": "~8.10.0", "@ledgerhq/errors": "~6.29.0", "@ledgerhq/hw-app-btc": "~10.17.0", "@ledgerhq/hw-app-cosmos": "~6.33.0", - "@ledgerhq/hw-app-eth": "~7.8.18", "@ledgerhq/hw-app-near": "~6.32.0", "@ledgerhq/hw-app-sui": "~1.5.0", "@ledgerhq/hw-app-trx": "~6.32.0", @@ -27,10 +33,12 @@ "@swapkit/utxo-signer": "^3.1.0", "@swapkit/wallet-core": "^5.0.8", "@trezor/connect-web": "~9.7.3", + "bitcoinjs-lib": "~6.1.7", "cosmjs-types": "~0.10.1", "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" }, @@ -40,11 +48,17 @@ "@cosmjs/crypto": "0.37.0", "@cosmjs/proto-signing": "0.37.0", "@keepkey/keepkey-sdk": "0.2.62", + "@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/devices": "8.10.0", "@ledgerhq/errors": "6.29.0", "@ledgerhq/hw-app-btc": "10.17.0", "@ledgerhq/hw-app-cosmos": "6.33.0", - "@ledgerhq/hw-app-eth": "7.8.18", "@ledgerhq/hw-app-near": "6.32.0", "@ledgerhq/hw-app-sui": "1.5.0", "@ledgerhq/hw-app-trx": "6.32.0", @@ -58,10 +72,12 @@ "@scure/base": "2.2.0", "@scure/bip32": "2.2.0", "@trezor/connect-web": "9.7.3", + "bitcoinjs-lib": "6.1.7", "cosmjs-types": "0.10.1", "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..32a6db3 --- /dev/null +++ b/packages/wallet-hardware/src/ledger/clients/bitcoin.ts @@ -0,0 +1,397 @@ +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 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, ""); +} + +async function signedInputParts({ + rawInput, + purpose, +}: { + rawInput: ReturnType; + purpose: number; +}) { + if (purpose === 86) { + const [tapKeySig] = rawInput.finalScriptWitness ?? []; + if (!tapKeySig) throw new SwapKitError("wallet_ledger_invalid_response", { reason: "Missing taproot signature" }); + return { tapKeySig }; + } + + if (purpose === 49 || purpose === 84) { + const [signature, publicKey] = rawInput.finalScriptWitness ?? []; + if (!signature || !publicKey) { + throw new SwapKitError("wallet_ledger_invalid_response", { reason: "Missing SegWit signature or public key" }); + } + + if (purpose === 49) { + 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 }; + } + + return { partialSig: [[publicKey, signature]] as [[Uint8Array, Uint8Array]] }; + } + + 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]] }; +} + +async function restoreSignedPsbt({ + rawTransaction, + tx, + purpose, +}: { + rawTransaction: string; + tx: Transaction; + purpose: number; +}) { + 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() { + accountXpubPromise ??= getSigner().then(async (signer) => { + const { extendedPublicKey } = await executeLedgerDeviceAction({ + action: signer.getExtendedPublicKey(configuredPath.accountPath), + onDeviceActionState, + }); + return extendedPublicKey; + }); + return accountXpubPromise; + } + + function getMasterFingerprint() { + fingerprintPromise ??= getSigner().then(async (signer) => { + const { masterFingerprint } = await executeLedgerDeviceAction({ + action: signer.getMasterFingerprint(), + onDeviceActionState, + }); + return fingerprintToNumber(masterFingerprint); + }); + 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 d8f49cc..edf5023 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,126 +9,107 @@ import { import { AbstractSigner, type Provider, + type TransactionLike, type TransactionRequest, type TypedDataDomain, type TypedDataField, } from "ethers"; -import { getLedgerTransport } from "../helpers/getLedgerTransport"; -import { - fetchLedgerNetworkCertificate, - fetchLedgerNetworkDescriptor, - provideLedgerCertificate, - provideLedgerNetworkInformation, -} from "../helpers/ledgerNetworkInfo"; - -const LEDGER_INCORRECT_DATA = 0x6a80; - -const ARC_CHAIN_ID = Number(ChainId.Arc); - -type LedgerTransactionResolution = Awaited< - ReturnType ->; - -function isLedgerIncorrectDataError(error: unknown) { - return ( - error instanceof Error && - "statusCode" in error && - (error as { statusCode: number }).statusCode === LEDGER_INCORRECT_DATA - ); -} - -function hasClearSigningPayload(resolution: LedgerTransactionResolution | null) { - return Object.values(resolution ?? {}).some((entries) => Array.isArray(entries) && entries.length); -} +import type { LedgerDMKSession } from "../helpers/dmk"; +import { executeLedgerDeviceAction, type LedgerDeviceActionStateHandler } from "../helpers/executeDeviceAction"; -function parseLedgerSignatureV(v: number | string) { - if (typeof v === "number") return v; - - const hex = v.startsWith("0x") ? v.slice(2) : v; - return Number.parseInt(hex || "0", 16); +interface EVMLedgerParams { + dmkSession: LedgerDMKSession; + provider: Provider; + derivationPath?: DerivationPathArray | string; + chainId?: ChainId; + onDeviceActionState?: LedgerDeviceActionStateHandler; + originToken?: string; } class EVMLedgerInterface extends AbstractSigner { chainId: ChainId = ChainId.Ethereum; derivationPath = ""; - ledgerApp: InstanceType | null = null; - ledgerTimeout = 50000; - private transport?: Transport; - private readonly injectedTransport?: Transport; - private arcNetworkRegistered = false; + 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 ( @@ -139,152 +119,142 @@ 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 populated = await TypedDataEncoder.resolveNames(domain, filteredTypes, 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; - - try { - sig = await this.ledgerApp?.signEIP712Message(this.derivationPath, { - domain: domain as Record, - message: value, - primaryType, - types: { EIP712Domain: buildEIP712DomainType(domain), ...filteredTypes }, - }); - } 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); - - sig = await this.ledgerApp?.signEIP712HashedMessage(this.derivationPath, domainSeparator, messageHash); + if (chainId !== undefined && !Number.isSafeInteger(chainId)) { + throw new SwapKitError("wallet_ledger_invalid_params", { chainId: populated.domain.chainId }); } - if (!sig) throw new SwapKitError("wallet_ledger_signing_error"); + 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(populated.domain), ...filteredTypes }, + }), + onDeviceActionState: this.onDeviceActionState, + }); - sig.r = `0x${sig.r}`; - sig.s = `0x${sig.s}`; - return Signature.from(sig).serialized; + return Signature.from(signature).serialized; }; signTransaction = async (tx: TransactionRequest) => { - const { Transaction } = await import("ethers"); - await this.createTransportAndLedger(); - - const nonce = tx.nonce ?? undefined; - const transactionCount = - nonce === undefined ? await this.provider?.getTransactionCount(tx.from || (await this.getAddress())) : undefined; - - 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, - }; - - // ledger expects the tx to be serialized without the 0x prefix - const unsignedTx = Transaction.from(baseTx).unsignedSerialized.slice(2); - - const { ledgerService } = await import("@ledgerhq/hw-app-eth"); - - const chainId = Number(baseTx.chainId); - const isArc = chainId === ARC_CHAIN_ID; - - const resolution = await ledgerService - .resolveTransaction(unsignedTx, {}, { erc20: true, externalPlugins: true }) - .catch((error) => { - if (!isArc) throw error; - - console.warn("Ledger: could not fetch clear-signing metadata for Arc, signing blind"); - - return null; + 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", }); + } - if (isArc && !this.arcNetworkRegistered) await this.registerNetworkOnDevice(chainId); + 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; - const signature = await this.signWithLedgerApp(unsignedTx, resolution, chainId); + 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 (!signature) 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, + }); - const { r, s, v } = signature; + baseTx.signature = signature; - return Transaction.from({ ...baseTx, signature: { r: `0x${r}`, s: `0x${s}`, v: parseLedgerSignatureV(v) } }) - .serialized; + return baseTx.serialized; }; +} - private signWithLedgerApp = async ( - unsignedTx: string, - resolution: LedgerTransactionResolution | null, - chainId: number, - ) => { - try { - return await this.ledgerApp?.signTransaction(this.derivationPath, unsignedTx, resolution); - } catch (error) { - const canSignBlind = - chainId === ARC_CHAIN_ID && isLedgerIncorrectDataError(error) && hasClearSigningPayload(resolution); +interface LedgerParams { + dmkSession: LedgerDMKSession; + provider: Provider; + derivationPath?: DerivationPathArray; + onDeviceActionState?: LedgerDeviceActionStateHandler; + originToken?: string; +} - if (!canSignBlind) throw error; +export function ArbitrumLedger(params: LedgerParams) { + return new EVMLedgerInterface({ ...params, chainId: ChainId.Arbitrum }); +} - console.warn("Ledger: the app rejected the Arc metadata, signing blind"); +export function ArcLedger(params: LedgerParams) { + return new EVMLedgerInterface({ ...params, chainId: ChainId.Arc }); +} - return await this.ledgerApp?.signTransaction(this.derivationPath, unsignedTx, null); - } - }; +export function AuroraLedger(params: LedgerParams) { + return new EVMLedgerInterface({ ...params, chainId: ChainId.Aurora }); +} + +export function AvalancheLedger(params: LedgerParams) { + return new EVMLedgerInterface({ ...params, chainId: ChainId.Avalanche }); +} - private registerNetworkOnDevice = async (chainId: number) => { - const deviceModelId = this.transport?.deviceModel?.id; +export function BaseLedger(params: LedgerParams) { + return new EVMLedgerInterface({ ...params, chainId: ChainId.Base }); +} - if (!(this.transport && deviceModelId)) return; +export function EthereumLedger(params: LedgerParams) { + return new EVMLedgerInterface({ ...params, chainId: ChainId.Ethereum }); +} + +export function GnosisLedger(params: LedgerParams) { + return new EVMLedgerInterface({ ...params, chainId: ChainId.Gnosis }); +} - try { - const [descriptor, certificate] = await Promise.all([ - fetchLedgerNetworkDescriptor(chainId, deviceModelId), - fetchLedgerNetworkCertificate(deviceModelId), - ]); +export function OptimismLedger(params: LedgerParams) { + return new EVMLedgerInterface({ ...params, chainId: ChainId.Optimism }); +} - if (!descriptor) return; +export function PolygonLedger(params: LedgerParams) { + return new EVMLedgerInterface({ ...params, chainId: ChainId.Polygon }); +} - if (certificate) await provideLedgerCertificate(this.transport, certificate); +export function BinanceSmartChainLedger(params: LedgerParams) { + return new EVMLedgerInterface({ ...params, chainId: ChainId.BinanceSmartChain }); +} - await provideLedgerNetworkInformation(this.transport, descriptor); +export function MonadLedger(params: LedgerParams) { + return new EVMLedgerInterface({ ...params, chainId: ChainId.Monad }); +} - this.arcNetworkRegistered = true; - } catch { - // the app will reject the metadata and signWithLedgerApp falls back to blind signing - } - }; +export function XLayerLedger(params: LedgerParams) { + return new EVMLedgerInterface({ ...params, chainId: ChainId.XLayer }); } -type LedgerParams = { provider: Provider; derivationPath?: DerivationPathArray; transport?: Transport }; - -export const ArbitrumLedger = (params: LedgerParams) => - new EVMLedgerInterface({ ...params, chainId: ChainId.Arbitrum }); -export const ArcLedger = (params: LedgerParams) => new EVMLedgerInterface({ ...params, chainId: ChainId.Arc }); -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 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..9e55df8 100644 --- a/packages/wallet-hardware/src/ledger/clients/near.ts +++ b/packages/wallet-hardware/src/ledger/clients/near.ts @@ -1,18 +1,42 @@ +import { 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 { + 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 +44,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 +69,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: UserInteractionRequired.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..f229174 100644 --- a/packages/wallet-hardware/src/ledger/clients/sui.ts +++ b/packages/wallet-hardware/src/ledger/clients/sui.ts @@ -1,3 +1,4 @@ +import { 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,55 @@ import { SwapKitError, } from "@swapkit/helpers"; -import { getLedgerTransport } from "../helpers/getLedgerTransport"; +import { + 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 +65,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: UserInteractionRequired.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..40c4cea 100644 --- a/packages/wallet-hardware/src/ledger/clients/thorchain/index.ts +++ b/packages/wallet-hardware/src/ledger/clients/thorchain/index.ts @@ -1,11 +1,37 @@ import type { AccountData, AminoSignResponse, StdSignDoc } from "@cosmjs/amino"; +import { Secp256k1Signature } from "@cosmjs/crypto"; +import { + CallTaskInAppDeviceAction, + type DmkError, + DmkResultFactory, + isSuccessCommandResult, + UnknownDeviceExchangeError, + 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, type LedgerDeviceActionStateHandler } from "../../helpers/executeDeviceAction"; +import { + getThorAddressCommand, + getThorLegacyVersion, + getThorSignCommands, + invalidThorAppVersion, + 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 +44,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 +67,181 @@ 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.")) throw invalidThorAppVersion({ version }); + + let response = new Uint8Array(); + for (const command of commands) { + response = await sendThorLegacyCommand({ command, transport: this.transport }); + } + return response; + } + + const dmkSession = this.dmkSession; + if (!dmkSession) throw new SwapKitError("wallet_ledger_connection_error"); + + 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: invalidThorAppVersion({ version }), + }); + } + + let response: Uint8Array = new Uint8Array(); + for (const command of commands) { + const result = await internalApi.sendCommand(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), + }); + } + }, + }, + }); - this.pubKey = base64.encode(compressed_pk); + const result = await executeLedgerDeviceAction({ + action: dmkSession.dmk.executeDeviceAction({ deviceAction, sessionId: dmkSession.sessionId }), + onDeviceActionState: this.onDeviceActionState, + }); - return bech32_address; + return result.response; }; - getAddressAndPubKey = async () => { - await this.checkOrCreateTransportAndLedger(true); + 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 ? UserInteractionRequired.VerifyAddress : UserInteractionRequired.None, + }); - const response: GetAddressAndPubKeyResponse = await this.ledgerApp.getAddressAndPubKey( - this.derivationPath, - this.chain, - ); + return parseThorAddressResponse({ response }); + }; - this.validateResponse(response.return_code, response.error_message); + private signBytes = async ({ message }: { message: Uint8Array }) => { + const response = await this.executeCommands({ + commands: getThorSignCommands({ message, path: this.derivationPath }), + requiredUserInteraction: UserInteractionRequired.SignTransaction, + }); - return response; + return getFixedSignature({ signature: response }); }; - showAddressAndPubKey = async () => { - await this.checkOrCreateTransportAndLedger(true); - - const response: GetAddressAndPubKeyResponse = await this.ledgerApp.showAddressAndPubKey( - this.derivationPath, - this.chain, - ); + connect = async () => { + const { address, publicKey } = await this.getAddressData({ checkOnDevice: false }); + this.pubKey = publicKey; + return address; + }; - this.validateResponse(response.return_code, response.error_message); + 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..69d6724 --- /dev/null +++ b/packages/wallet-hardware/src/ledger/clients/thorchain/protocol.ts @@ -0,0 +1,163 @@ +import { + ApduBuilder, + type ApduResponse, + type Command, + CommandResultFactory, + InvalidResponseFormatError, + InvalidStatusWordError, +} from "@ledgerhq/device-management-kit"; +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; +} + +export class ThorCommand implements Command { + 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 new ApduBuilder({ cla: CLA, ins, p1, p2 }).addBufferToData(data).build(); + } + + parseResponse(response: ApduResponse) { + const status = (response.statusCode[0] ?? 0) * 0x100 + (response.statusCode[1] ?? 0); + if (status !== STATUS_OK) { + return CommandResultFactory({ + error: new InvalidStatusWordError(`THORChain app returned status 0x${status.toString(16).padStart(4, "0")}`), + }); + } + + return CommandResultFactory({ data: 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 invalidThorAppVersion({ version }: { version?: string }) { + return new InvalidResponseFormatError(`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..880e44f 100644 --- a/packages/wallet-hardware/src/ledger/clients/tron.ts +++ b/packages/wallet-hardware/src/ledger/clients/tron.ts @@ -1,3 +1,4 @@ +import { 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,81 @@ import { } from "@swapkit/helpers"; import type { TronSignedTransaction, TronSigner, TronTransaction } from "@swapkit/toolboxes/tron"; -import { getLedgerTransport } from "../helpers/getLedgerTransport"; +import { + 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: UserInteractionRequired.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: UserInteractionRequired.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 +91,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.ts b/packages/wallet-hardware/src/ledger/clients/utxo.ts index 63dbece..175e5c2 100644 --- a/packages/wallet-hardware/src/ledger/clients/utxo.ts +++ b/packages/wallet-hardware/src/ledger/clients/utxo.ts @@ -1,21 +1,44 @@ +import { 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 { + type LedgerJsClientParams, + normalizeLedgerJsClientParams, + runLedgerJsOperation, +} from "../helpers/ledgerJsDmkBridge"; const nonSegwitLedgerChains = ["bitcoin-cash", "dash", "dogecoin", "zcash"]; +type LedgerUTXOChain = "bitcoin-cash" | "bitcoin" | "litecoin" | "dogecoin" | "dash" | "zcash"; +type UTXOLedgerParams = LedgerJsClientParams; + +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 +46,35 @@ type MultiPathParams = Omit & { derivationPaths: string[]; }; +export async function resolveZcashPreviousTransaction({ + getRawTx, + input, + inputIndex, +}: { + getRawTx: (txid: string) => Promise; + input: { index: number; scriptPubkey: Uint8Array; txid: Uint8Array; value: bigint }; + inputIndex: number; +}) { + const txid = hex.encode(new Uint8Array([...input.txid].reverse())); + 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; +} + const signUTXOTransaction = ( { tx, inputUtxos, btcApp, derivationPath, chain }: Params, options?: Partial, @@ -110,52 +162,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 +215,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 +225,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(); @@ -207,13 +251,13 @@ const BaseLedgerUTXO = ({ 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); + inputUtxos.push( + await resolveZcashPreviousTransaction({ + getRawTx: (txid) => getUtxoApi(Chain.Zcash).getRawTx(txid), + input, + inputIndex: i, + }), + ); } for (let i = 0; i < pczt.outputsLength; i++) { @@ -221,18 +265,22 @@ const BaseLedgerUTXO = ({ 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: UserInteractionRequired.SignTransaction, + }); const signedTx = ZcashTransaction.fromHex(signedTxHex, { allowUnknownOutputs: true }); const signedPczt = pczt.clone(); @@ -250,9 +298,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: UserInteractionRequired.SignTransaction, + }); }, /** @@ -260,85 +310,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: UserInteractionRequired.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..a7ce80d 100644 --- a/packages/wallet-hardware/src/ledger/clients/xrp.ts +++ b/packages/wallet-hardware/src/ledger/clients/xrp.ts @@ -1,33 +1,50 @@ +import { 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 { + 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 +56,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: UserInteractionRequired.SignTransaction, + }); const tx_blob = encode({ ...transactionJSON, TxnSignature: txnSignature }); const hash = hashes.hashSignedTx(tx_blob); @@ -47,4 +67,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/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..ec94489 --- /dev/null +++ b/packages/wallet-hardware/src/ledger/helpers/executeDeviceAction.ts @@ -0,0 +1,59 @@ +import type { + DeviceActionIntermediateValue, + DeviceActionState, + ExecuteDeviceActionReturnType, +} from "@ledgerhq/device-management-kit"; +import { SwapKitError } from "@swapkit/helpers"; + +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; + } + + if (state.status === "completed") { + settle(() => resolve(state.output)); + } else if (state.status === "error") { + settle(() => reject(state.error)); + } else if (state.status === "stopped") { + settle(() => reject(new SwapKitError("wallet_ledger_connection_error"))); + } + }, + }); + + 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 ca1f5a6..53da31d 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, @@ -21,15 +29,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; @@ -61,75 +65,104 @@ 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.Arc, - 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.Arc, + 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.Arc, () => ArcLedger(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.Arc, () => ArcLedger(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/index.ts b/packages/wallet-hardware/src/ledger/helpers/index.ts index 00b51b9..db82607 100644 --- a/packages/wallet-hardware/src/ledger/helpers/index.ts +++ b/packages/wallet-hardware/src/ledger/helpers/index.ts @@ -1,3 +1,9 @@ +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..ea87603 --- /dev/null +++ b/packages/wallet-hardware/src/ledger/helpers/ledgerJsDmkBridge.ts @@ -0,0 +1,167 @@ +import { + Apdu, + type ApduResponse, + CallTaskInAppDeviceAction, + type Command, + CommandResultFactory, + DmkResultFactory, + type InternalApi, + isSuccessCommandResult, + 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, type LedgerDeviceActionStateHandler } from "./executeDeviceAction"; + +const STATUS_CODE_LENGTH = 2; + +interface RawApduResponse { + response: Uint8Array; +} + +class RawApduCommand implements Command { + readonly name = "ledgerJsRawApdu"; + + constructor(private readonly apdu: Apdu) {} + + getApdu() { + return this.apdu; + } + + parseResponse({ data, statusCode }: ApduResponse) { + if (statusCode.length !== STATUS_CODE_LENGTH) { + return CommandResultFactory({ + error: new 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 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 new Apdu(cla, ins, p1, p2, apdu.subarray(5)); +} + +export class LedgerJsDmkTransport extends Transport { + constructor(private readonly internalApi: InternalApi) { + super(); + } + + async exchange(apdu: Buffer, { abortTimeoutMs }: { abortTimeoutMs?: number } = {}) { + const command = new RawApduCommand(parseShortApdu(apdu)); + const result = await this.internalApi.sendCommand(command, abortTimeoutMs ?? this.exchangeTimeout); + + if (!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 = UserInteractionRequired.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 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/ledgerNetworkInfo.ts b/packages/wallet-hardware/src/ledger/helpers/ledgerNetworkInfo.ts deleted file mode 100644 index 8cc3255..0000000 --- a/packages/wallet-hardware/src/ledger/helpers/ledgerNetworkInfo.ts +++ /dev/null @@ -1,127 +0,0 @@ -import type Transport from "@ledgerhq/hw-transport"; - -const LEDGER_CAL_SERVICE_URL = "https://crypto-assets-service.api.ledger.com/v1"; - -const LEDGER_DEVICE_TO_CAL_MODEL: Record = { - apex: "apexp", - europa: "flex", - nanoS: "nanos", - nanoSP: "nanosp", - nanoX: "nanox", - stax: "stax", -}; - -const APDU_CLA = 0xe0; -const APDU_CLA_PKI = 0xb0; -const APDU_INS_LOAD_CERTIFICATE = 0x06; -const LEDGER_KEY_USAGE_NETWORK = 0x0c; -const APDU_INS_PROVIDE_NETWORK_INFORMATION = 0x30; -const APDU_P1_FIRST_CHUNK = 0x01; -const APDU_P1_FOLLOWING_CHUNK = 0x00; -const APDU_P2_NETWORK_CONFIG = 0x00; -const APDU_P2_NETWORK_ICON = 0x01; -const APDU_MAX_CHUNK_SIZE = 0xff; -const TLV_TAG_DER_SIGNATURE = 0x15; - -export type LedgerNetworkDescriptor = { data: string; icon?: string; signature: string }; - -type CalNetwork = { - chain_id: number; - descriptors?: Record; - icons?: Record; -}; - -export async function fetchLedgerNetworkCertificate( - deviceModelId: string, - fetchFn: typeof fetch = fetch, -): Promise { - const calModel = LEDGER_DEVICE_TO_CAL_MODEL[deviceModelId]; - if (!calModel) return null; - - const url = `${LEDGER_CAL_SERVICE_URL}/certificates?output=descriptor&public_key_usage=network&target_device=${calModel}`; - const response = await fetchFn(url); - if (!response.ok) return null; - - const certificates = (await response.json()) as Array<{ - descriptor?: { data?: string; signatures?: { prod?: string } }; - }>; - const descriptor = certificates[0]?.descriptor; - const signature = descriptor?.signatures?.prod; - - if (!(descriptor?.data && signature)) return null; - - return `${descriptor.data}${appendSignatureTlv(signature)}`; -} - -export async function provideLedgerCertificate(transport: Transport, payloadHex: string) { - await transport.send( - APDU_CLA_PKI, - APDU_INS_LOAD_CERTIFICATE, - LEDGER_KEY_USAGE_NETWORK, - 0x00, - Buffer.from(payloadHex, "hex"), - ); -} - -export async function fetchLedgerNetworkDescriptor( - chainId: number, - deviceModelId: string, - fetchFn: typeof fetch = fetch, -): Promise { - const calModel = LEDGER_DEVICE_TO_CAL_MODEL[deviceModelId]; - if (!calModel) return null; - - const url = `${LEDGER_CAL_SERVICE_URL}/networks?chain_id=${chainId}&output=chain_id,descriptors,icons`; - const response = await fetchFn(url); - if (!response.ok) return null; - - const networks = (await response.json()) as CalNetwork[]; - const network = networks.find((entry) => entry.chain_id === chainId); - const descriptor = network?.descriptors?.[calModel]; - const signature = descriptor?.signatures?.prod; - - if (!(descriptor?.data && signature)) return null; - - return { data: descriptor.data, icon: network?.icons?.[calModel], signature }; -} - -function appendSignatureTlv(signature: string) { - return encodeTlv(TLV_TAG_DER_SIGNATURE, Buffer.from(signature, "hex")).toString("hex"); -} - -function encodeTlv(tag: number, value: Buffer) { - return Buffer.concat([Buffer.from([tag, value.length]), value]); -} - -export function encodeNetworkInfoPayload({ data, signature }: LedgerNetworkDescriptor) { - const tlv = Buffer.concat([ - Buffer.from(data, "hex"), - encodeTlv(TLV_TAG_DER_SIGNATURE, Buffer.from(signature, "hex")), - ]); - const length = Buffer.alloc(2); - length.writeUInt16BE(tlv.length, 0); - return Buffer.concat([length, tlv]); -} - -async function sendChunked(transport: Transport, p2: number, payload: Buffer) { - for (let offset = 0; offset < payload.length; offset += APDU_MAX_CHUNK_SIZE) { - const p1 = offset === 0 ? APDU_P1_FIRST_CHUNK : APDU_P1_FOLLOWING_CHUNK; - const chunk = payload.subarray(offset, offset + APDU_MAX_CHUNK_SIZE); - await transport.send(APDU_CLA, APDU_INS_PROVIDE_NETWORK_INFORMATION, p1, p2, chunk); - } -} - -export async function provideLedgerNetworkInformation(transport: Transport, descriptor: LedgerNetworkDescriptor) { - const payload = encodeNetworkInfoPayload(descriptor); - await sendChunked(transport, APDU_P2_NETWORK_CONFIG, payload); - - if (!descriptor.icon) return { iconAccepted: false }; - - try { - await sendChunked(transport, APDU_P2_NETWORK_ICON, Buffer.from(descriptor.icon, "hex")); - return { iconAccepted: true }; - } catch { - await sendChunked(transport, APDU_P2_NETWORK_CONFIG, payload); - return { iconAccepted: false }; - } -} diff --git a/packages/wallet-hardware/src/ledger/index.ts b/packages/wallet-hardware/src/ledger/index.ts index 197e90f..9550b25 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,61 @@ 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 { + getLedgerAddress, + getLedgerClient, + type LedgerDeviceActionStateHandler, + type LedgerDMKSession, +} from "./helpers"; + +type BitcoinLedgerClient = ReturnType; +type LegacyUTXOLedgerClient = ReturnType; +type ZcashLedgerClient = ReturnType; /** * 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; +} 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 }); @@ -83,7 +111,7 @@ 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", @@ -131,14 +159,30 @@ function reduceMemo(memo?: string, affiliateAddress = "t") { 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 }); @@ -146,7 +190,12 @@ export async function getLedgerExtendedPublicKey( 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 = + chain === Chain.Bitcoin + ? await (signer as BitcoinLedgerClient).getExtendedPublicKey({ path: ledgerPath }) + : chain === Chain.Zcash + ? await (signer as ZcashLedgerClient).getExtendedPublicKey({ path: ledgerPath, xpubVersion }) + : await (signer as LegacyUTXOLedgerClient).getExtendedPublicKey(ledgerPath, xpubVersion); return { accountIndex: getUTXOAccountIndexFromPath(accountPath), path, xpub }; } finally { await (signer as { disconnect?: () => Promise }).disconnect?.(); @@ -156,7 +205,10 @@ export async function getLedgerExtendedPublicKey( async function getWalletMethods({ address: providedAddress, chain, + dmkSession, derivationPath, + onDeviceActionState, + originToken, transport, }: ConnectLedgerOptions & { chain: Chain; derivationPath?: DerivationPathArray }) { switch (chain) { @@ -169,25 +221,21 @@ async function getWalletMethods({ const { getUtxoToolbox } = await import("@swapkit/toolboxes/utxo"); const utxoChain = chain as UTXOChain; - const signer = await getLedgerClient({ chain, derivationPath, transport }); + const signer = await getLedgerClient({ chain, derivationPath, dmkSession, onDeviceActionState, 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; + let signAndBroadcastLedgerTransaction: ((tx: Transaction | ZcashTransaction) => 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 }; + const bitcoinSigner = signer as BitcoinLedgerClient; + toolboxSigner = { getAddress: bitcoinSigner.getAddress, signTransaction: bitcoinSigner.signTransaction }; + signAndBroadcastLedgerTransaction = async (tx) => { + const signedTxHex = await bitcoinSigner.signTransactionHex({ tx: tx as Transaction }); + return toolbox.broadcastTx(signedTxHex); + }; } else if ( chain === Chain.BitcoinCash || chain === Chain.Dogecoin || @@ -195,9 +243,49 @@ async function getWalletMethods({ 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 }); + const legacySigner = signer as LegacyUTXOLedgerClient; + toolboxSigner = createLegacyPsbtSigner({ address, chain: utxoChain, legacyClient: legacySigner }); + signAndBroadcastLedgerTransaction = async (tx) => { + const signedTxHex = await signLegacyPsbtTransaction({ + chain: utxoChain, + legacyClient: legacySigner, + tx: tx as Transaction, + }); + return toolbox.broadcastTx(signedTxHex); + }; + } else if (chain === Chain.Zcash) { + const zcashSigner = signer as ZcashLedgerClient; + signAndBroadcastLedgerTransaction = async (transaction) => { + if (!("consensusBranchId" in transaction)) { + throw new SwapKitError("wallet_ledger_method_not_supported", { + method: "signPCZT", + wallet: WalletOption.LEDGER, + }); + } + + const inputUtxos = await Promise.all( + Array.from({ length: transaction.inputsLength }, async (_, inputIndex) => { + const input = transaction.getInput(inputIndex); + const txid = hex.encode(input.txid); + const txHex = await getUtxoApi(Chain.Zcash).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; + }), + ); + const signedTxHex = await zcashSigner.signTransaction({ inputUtxos, tx: transaction }); return toolbox.broadcastTx(signedTxHex); }; } @@ -205,7 +293,7 @@ async function getWalletMethods({ const toolbox = toolboxSigner ? await getUtxoToolbox(utxoChain, { signer: toolboxSigner }) : getUtxoToolbox(utxoChain); - const signAndBroadcastTransaction = signAndBroadcastLegacyPsbtTransaction ?? toolbox.signAndBroadcastTransaction; + const signAndBroadcastTransaction = signAndBroadcastLedgerTransaction ?? toolbox.signAndBroadcastTransaction; const transfer = async (params: UTXOBuildTxParams) => { const feeRate = params.feeRate || (await toolbox.getFeeRates())[FeeOption.Average]; @@ -219,9 +307,12 @@ async function getWalletMethods({ sender: address, }); - // 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 txHex = + chain === Chain.Bitcoin + ? await (signer as BitcoinLedgerClient).signTransactionHex({ inputUtxos: inputs, tx: tx as Transaction }) + : chain === Chain.Zcash + ? await (signer as ZcashLedgerClient).signTransaction({ inputUtxos: inputs, tx: tx as ZcashTransaction }) + : await (signer as LegacyUTXOLedgerClient).signTransaction(tx as Transaction, inputs); const txHash = await toolbox.broadcastTx(txHex); return txHash; @@ -234,7 +325,12 @@ async function getWalletMethods({ 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); + const xpub = + chain === Chain.Bitcoin + ? await (signer as BitcoinLedgerClient).getExtendedPublicKey({ path: ledgerPath }) + : chain === Chain.Zcash + ? await (signer as ZcashLedgerClient).getExtendedPublicKey({ path: ledgerPath, xpubVersion }) + : await (signer as LegacyUTXOLedgerClient).getExtendedPublicKey(ledgerPath, xpubVersion); return { accountIndex: getUTXOAccountIndexFromPath(accountPath), path, xpub }; } @@ -255,7 +351,13 @@ async function getWalletMethods({ try { const fullPath = getUTXOAddressPath({ accountIndex, chain: utxoChain, change, derivationPath, index }); - const indexedSigner = await getLedgerClient({ chain: utxoChain, derivationPath: fullPath, transport }); + const indexedSigner = await getLedgerClient({ + chain: utxoChain, + derivationPath: fullPath, + dmkSession, + onDeviceActionState, + transport, + }); const derivedAddress = await getLedgerAddress({ chain: utxoChain, ledgerClient: indexedSigner }); return { @@ -318,6 +420,13 @@ async function getWalletMethods({ feeOptionKey?: FeeOption; changeAddress?: string; }) { + if (chain === Chain.Zcash) { + throw new SwapKitError("wallet_ledger_method_not_supported", { + method: "transferFromMultipleAddresses", + wallet: WalletOption.LEDGER, + }); + } + if (!utxos.length) { throw new SwapKitError("wallet_ledger_invalid_params", { message: "No UTXOs provided for multi-address transfer", @@ -381,7 +490,18 @@ async function getWalletMethods({ throw new SwapKitError("wallet_ledger_method_not_supported", { method: "signTransactionWithMultiplePaths" }); } - const txHex = await signer.signTransactionWithMultiplePaths(tx, selectedInputs, inputDerivationPaths); + const txHex = + chain === Chain.Bitcoin + ? await (signer as BitcoinLedgerClient).signTransactionWithMultiplePaths({ + derivationPaths: inputDerivationPaths, + inputUtxos: selectedInputs, + tx, + }) + : await (signer as LegacyUTXOLedgerClient).signTransactionWithMultiplePaths( + tx, + selectedInputs, + inputDerivationPaths, + ); return toolbox.broadcastTx(txHex); } @@ -412,8 +532,15 @@ async function getWalletMethods({ case Chain.Gnosis: case Chain.Monad: case Chain.XLayer: { + const signer = await getLedgerClient({ + chain, + derivationPath, + dmkSession, + onDeviceActionState, + originToken, + transport, + }); 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 }); @@ -424,7 +551,7 @@ async function getWalletMethods({ const { createSigningStargateClient, getDefaultChainFee, getMsgSendDenom, getCosmosToolbox } = await import( "@swapkit/toolboxes/cosmos" ); - const signer = await getLedgerClient({ chain, derivationPath, transport }); + const signer = await getLedgerClient({ chain, derivationPath, dmkSession, onDeviceActionState, transport }); const address = await getLedgerAddress({ chain, ledgerClient: signer }); const toolbox = await getCosmosToolbox(Chain.Cosmos, { signer }); @@ -459,7 +586,7 @@ async function getWalletMethods({ } case Chain.THORChain: { - const signer = await getLedgerClient({ chain, derivationPath, transport }); + 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 }); @@ -470,7 +597,7 @@ async function getWalletMethods({ case Chain.Near: { const { getNearToolbox } = await import("@swapkit/toolboxes/near"); - const signer = await getLedgerClient({ chain, derivationPath, transport }); + const signer = await getLedgerClient({ chain, derivationPath, dmkSession, onDeviceActionState, transport }); const accountId = await signer.getAddress(); const toolbox = getNearToolbox({ signer }); @@ -479,7 +606,7 @@ async function getWalletMethods({ case Chain.Ripple: { const { getRippleToolbox } = await import("@swapkit/toolboxes/ripple"); - const signer = await getLedgerClient({ chain, derivationPath, transport }); + const signer = await getLedgerClient({ chain, derivationPath, dmkSession, onDeviceActionState, transport }); const address = signer.getAddress(); const toolbox = getRippleToolbox({ signer }); @@ -488,7 +615,7 @@ async function getWalletMethods({ case Chain.Tron: { const { getTronToolbox } = await import("@swapkit/toolboxes/tron"); - const signer = await getLedgerClient({ chain, derivationPath, transport }); + const signer = await getLedgerClient({ chain, derivationPath, dmkSession, onDeviceActionState, transport }); const address = await getLedgerAddress({ chain, ledgerClient: signer }); const toolbox = getTronToolbox({ signer }); @@ -497,7 +624,7 @@ async function getWalletMethods({ case Chain.Sui: { const { getSuiToolbox } = await import("@swapkit/toolboxes/sui"); - const signer = await getLedgerClient({ chain, derivationPath, transport }); + const signer = await getLedgerClient({ chain, derivationPath, dmkSession, onDeviceActionState, transport }); const address = await getLedgerAddress({ chain, ledgerClient: signer }); const toolbox = getSuiToolbox({ signer }); @@ -508,3 +635,6 @@ async function getWalletMethods({ throw new SwapKitError("wallet_ledger_chain_not_supported", { chain }); } } + +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..d65ff1e 100644 --- a/packages/wallet-hardware/src/ledger/types.ts +++ b/packages/wallet-hardware/src/ledger/types.ts @@ -1,42 +1,47 @@ +import type { BitcoinLedger } from "./clients/bitcoin"; import type { CosmosLedger } from "./clients/cosmos"; import type { ArbitrumLedger, + ArcLedger, 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 + | 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/test/ledger-evm-clear-signing.test.ts b/packages/wallet-hardware/test/ledger-evm-clear-signing.test.ts deleted file mode 100644 index 330edcc..0000000 --- a/packages/wallet-hardware/test/ledger-evm-clear-signing.test.ts +++ /dev/null @@ -1,361 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test"; -import type Transport from "@ledgerhq/hw-transport"; -import { Transaction } from "ethers"; - -type Resolution = { - erc20Tokens: string[]; - externalPlugin: unknown[]; - plugin: unknown[]; - nfts: unknown[]; - domains: unknown[]; -}; - -type Apdu = { cla: number; ins: number; p1: number; p2: number; data: string }; - -const signInvocations: Array = []; -const apdus: Apdu[] = []; -let resolutionToReturn: Resolution | null = null; -let resolveShouldThrow = false; -let rejectClearSigningWith: number | null = null; -let acceptClearSigningOnceNetworkIsKnown = false; -let networkKnownByDevice = false; -let iconIsRejected = false; - -class MockTransportStatusError extends Error { - statusCode: number; - statusText: string; - constructor(statusCode: number, statusText: string) { - super(`Ledger device: ${statusText} (0x${statusCode.toString(16)})`); - this.name = "TransportStatusError"; - this.statusCode = statusCode; - this.statusText = statusText; - } -} - -mock.module("@ledgerhq/hw-app-eth", () => ({ - default: class MockEthereumApp { - getAddress = async () => ({ address: "0x0000000000000000000000000000000000000001" }); - getAppConfiguration = async () => ({ arbitraryDataEnabled: 1, version: "1.20.1" }); - signTransaction = (_path: string, _unsignedTx: string, resolution: Resolution | null | undefined) => { - signInvocations.push(resolution); - const networkAccepted = acceptClearSigningOnceNetworkIsKnown && networkKnownByDevice; - if (rejectClearSigningWith !== null && resolution && !networkAccepted) { - const code = rejectClearSigningWith; - throw new MockTransportStatusError( - code, - code === 0x6a80 ? "INCORRECT_DATA" : "CONDITIONS_OF_USE_NOT_SATISFIED", - ); - } - return { r: "1".padStart(64, "0"), s: "2".padStart(64, "0"), v: "01" }; - }; - }, - ledgerService: { - resolveTransaction: () => - resolveShouldThrow ? Promise.reject(new Error("CAL unreachable")) : Promise.resolve(resolutionToReturn), - }, -})); - -import { ArcLedger, AuroraLedger } from "../src/ledger/clients/evm"; -import { - encodeNetworkInfoPayload, - fetchLedgerNetworkDescriptor, - provideLedgerNetworkInformation, -} from "../src/ledger/helpers/ledgerNetworkInfo"; - -const USDC_ARC = "0x3600000000000000000000000000000000000000"; -const usdcArcDescriptor = { domains: [], erc20Tokens: ["04555344433600"], externalPlugin: [], nfts: [], plugin: [] }; - -const ARC_NANOX_DESCRIPTOR = { - data: "010108020101510101230800000000000013b252034172632404555344435320f220b08815631e275dba75dd9ae6a30630980a2784a2393edd55bdc3cfdca021", - icon: "0e000e0000190000001c0fe0ff8fcc783380ce03380ce001e003f003ff03fc0070", - signature: - "3044022062db1ce5c6df646e26c23725dbbe7e7e5ba1b54764dd5d9d512ebf4707bebf0e022026b79ea6799c1d676ca073f2c58be23a538532f0eea9a5d52d04f9a7789bf18b", -}; - -const ARC_CERTIFICATE = { - data: "0101010201023501023601011004030100001302000214010120076e6574776f726b3002000a31010c32012134010133210272e8e8c69a8d9cec8fa22ea676a45099bcbc0301eba78ccca639e8173feafbab", - signature: - "30440220754341b03ab7479badfeffeb800dd66f79d8ec5d1d60d727a6336c25ed410aca02206a68e8e63de56ea5e8256d27eb574652b173ab0e6ce8942de74b38a66e799de1", -}; - -const calCertificatesResponse = [ - { descriptor: { data: ARC_CERTIFICATE.data, signatures: { prod: ARC_CERTIFICATE.signature, test: "00" } } }, -]; - -const calNetworksResponse = [ - { - chain_id: 5042, - descriptors: { - nanox: { data: ARC_NANOX_DESCRIPTOR.data, signatures: { prod: ARC_NANOX_DESCRIPTOR.signature, test: "00" } }, - stax: { data: "aa", signatures: { prod: "bb" } }, - }, - icons: { nanox: ARC_NANOX_DESCRIPTOR.icon, stax: "cc" }, - }, -]; - -const originalFetch = globalThis.fetch; -const originalWarn = console.warn; -let fetchedUrls: string[] = []; -let warnings: string[] = []; -let calResponse: { ok: boolean; body: unknown } = { body: calNetworksResponse, ok: true }; - -function makeTransport(deviceModelId?: string) { - return { - deviceModel: deviceModelId ? { id: deviceModelId } : undefined, - send: (cla: number, ins: number, p1: number, p2: number, data?: Buffer) => { - apdus.push({ cla, data: data?.toString("hex") ?? "", ins, p1, p2 }); - - if (cla === 0xb0) return Promise.resolve(Buffer.from("9000", "hex")); - if (ins === 0x30 && p2 === 0x01 && iconIsRejected) { - networkKnownByDevice = false; - return Promise.reject(new MockTransportStatusError(0x6a80, "INCORRECT_DATA")); - } - if (ins === 0x30 && p2 === 0x00) networkKnownByDevice = true; - - return Promise.resolve(Buffer.from("9000", "hex")); - }, - } as unknown as Transport; -} - -function makeClient(deviceModelId?: string) { - const provider = {} as Parameters[0]["provider"]; - return ArcLedger({ provider, transport: makeTransport(deviceModelId) }); -} - -const approveTx = { - data: "0x095ea7b3000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000f4240", - gasLimit: 60000n, - maxFeePerGas: 2n, - maxPriorityFeePerGas: 1n, - nonce: 0, - to: USDC_ARC, - type: 2, - value: 0n, -}; - -describe("ledger EVM signer — chains the Ethereum app doesn't know", () => { - beforeEach(() => { - signInvocations.length = 0; - apdus.length = 0; - fetchedUrls = []; - warnings = []; - resolutionToReturn = null; - resolveShouldThrow = false; - rejectClearSigningWith = null; - acceptClearSigningOnceNetworkIsKnown = false; - networkKnownByDevice = false; - iconIsRejected = false; - calResponse = { body: calNetworksResponse, ok: true }; - console.warn = (...args: unknown[]) => warnings.push(args.map(String).join(" ")); - globalThis.fetch = ((url: string) => { - fetchedUrls.push(String(url)); - const body = String(url).includes("/certificates") ? calCertificatesResponse : calResponse.body; - return Promise.resolve({ json: () => Promise.resolve(body), ok: calResponse.ok }); - }) as unknown as typeof fetch; - }); - - afterEach(() => { - globalThis.fetch = originalFetch; - console.warn = originalWarn; - }); - - it("registers the chain up front so the app never rejects the metadata", async () => { - resolutionToReturn = usdcArcDescriptor; - rejectClearSigningWith = 0x6a80; - acceptClearSigningOnceNetworkIsKnown = true; - - const signedTx = await makeClient("nanoX").signTransaction(approveTx); - - expect(fetchedUrls.sort()).toEqual([ - "https://crypto-assets-service.api.ledger.com/v1/certificates?output=descriptor&public_key_usage=network&target_device=nanox", - "https://crypto-assets-service.api.ledger.com/v1/networks?chain_id=5042&output=chain_id,descriptors,icons", - ]); - expect(apdus.map(({ cla, ins, p1, p2 }) => ({ cla, ins, p1, p2 }))).toEqual([ - { cla: 0xb0, ins: 0x06, p1: 0x0c, p2: 0x00 }, - { cla: 0xe0, ins: 0x30, p1: 0x01, p2: 0x00 }, - { cla: 0xe0, ins: 0x30, p1: 0x01, p2: 0x01 }, - ]); - expect(apdus[0]?.data).toBe(`${ARC_CERTIFICATE.data}1546${ARC_CERTIFICATE.signature}`); - expect(apdus[1]?.data).toBe(encodeNetworkInfoPayload(ARC_NANOX_DESCRIPTOR).toString("hex")); - expect(apdus[2]?.data).toBe(ARC_NANOX_DESCRIPTOR.icon); - expect(signInvocations).toEqual([usdcArcDescriptor]); - expect(warnings).toHaveLength(0); - expect(Transaction.from(signedTx).chainId).toBe(5042n); - }); - - it("signs blind when Ledger publishes no descriptor for that device model", async () => { - resolutionToReturn = usdcArcDescriptor; - rejectClearSigningWith = 0x6a80; - acceptClearSigningOnceNetworkIsKnown = true; - - await makeClient("nanoS").signTransaction(approveTx); - - expect(apdus).toHaveLength(0); - expect(signInvocations).toEqual([usdcArcDescriptor, null]); - expect(warnings).toHaveLength(1); - expect(warnings[0]).toContain("signing blind"); - }); - - it("signs blind when the transport exposes no device model", async () => { - resolutionToReturn = usdcArcDescriptor; - rejectClearSigningWith = 0x6a80; - - await makeClient().signTransaction(approveTx); - - expect(fetchedUrls).toHaveLength(0); - expect(signInvocations).toEqual([usdcArcDescriptor, null]); - expect(warnings[0]).toContain("signing blind"); - }); - - it("signs blind when the device refuses the network descriptor, e.g. an app older than 1.13", async () => { - resolutionToReturn = usdcArcDescriptor; - rejectClearSigningWith = 0x6a80; - acceptClearSigningOnceNetworkIsKnown = true; - const provider = {} as Parameters[0]["provider"]; - const transport = { - deviceModel: { id: "nanoX" }, - send: () => Promise.reject(new MockTransportStatusError(0x6d00, "INS_NOT_SUPPORTED")), - } as unknown as Transport; - - await ArcLedger({ provider, transport }).signTransaction(approveTx); - - expect(signInvocations).toEqual([usdcArcDescriptor, null]); - expect(warnings[0]).toContain("signing blind"); - }); - - it("signs blind when the app still rejects metadata after the chain was registered", async () => { - resolutionToReturn = usdcArcDescriptor; - rejectClearSigningWith = 0x6a80; - - await makeClient("nanoX").signTransaction(approveTx); - - expect(signInvocations).toEqual([usdcArcDescriptor, null]); - expect(warnings[0]).toContain("signing blind"); - }); - - it("re-registers the chain when the device rejects the icon, which would drop the network", async () => { - resolutionToReturn = usdcArcDescriptor; - rejectClearSigningWith = 0x6a80; - acceptClearSigningOnceNetworkIsKnown = true; - iconIsRejected = true; - - await makeClient("nanoX").signTransaction(approveTx); - - expect(apdus.map(({ cla, p2 }) => (cla === 0xb0 ? "cert" : p2))).toEqual(["cert", 0x00, 0x01, 0x00]); - expect(signInvocations).toEqual([usdcArcDescriptor]); - expect(warnings).toHaveLength(0); - }); - - it("registers the chain once per client and reuses it on later signatures", async () => { - resolutionToReturn = usdcArcDescriptor; - - const client = makeClient("nanoX"); - await client.signTransaction(approveTx); - await client.signTransaction(approveTx); - - expect(fetchedUrls).toHaveLength(2); - expect(apdus).toHaveLength(3); - expect(signInvocations).toEqual([usdcArcDescriptor, usdcArcDescriptor]); - expect(warnings).toHaveLength(0); - }); - - it("does not sign blind when there was no metadata to blame", async () => { - resolutionToReturn = { domains: [], erc20Tokens: [], externalPlugin: [], nfts: [], plugin: [] }; - rejectClearSigningWith = 0x6a80; - - await expect(makeClient("nanoX").signTransaction(approveTx)).rejects.toMatchObject({ statusCode: 0x6a80 }); - expect(signInvocations).toHaveLength(1); - }); - - it("does not retry when the user rejected on device", async () => { - resolutionToReturn = usdcArcDescriptor; - rejectClearSigningWith = 0x6985; - - await expect(makeClient("nanoX").signTransaction(approveTx)).rejects.toMatchObject({ statusCode: 0x6985 }); - expect(signInvocations).toHaveLength(1); - }); - - it("falls back to blind signing when the Ledger asset list can't be fetched", async () => { - resolveShouldThrow = true; - - await makeClient("nanoX").signTransaction(approveTx); - - expect(signInvocations).toEqual([null]); - expect(warnings[0]).toContain("signing blind"); - }); - - it("keeps failing loudly on other chains when the Ledger asset list can't be fetched", async () => { - resolveShouldThrow = true; - const provider = {} as Parameters[0]["provider"]; - - await expect( - AuroraLedger({ provider, transport: makeTransport("nanoX") }).signTransaction(approveTx), - ).rejects.toThrow("CAL unreachable"); - - expect(signInvocations).toHaveLength(0); - }); - - it("never signs blind on other chains, even on INCORRECT_DATA", async () => { - resolutionToReturn = usdcArcDescriptor; - rejectClearSigningWith = 0x6a80; - const provider = {} as Parameters[0]["provider"]; - - await expect( - AuroraLedger({ provider, transport: makeTransport("nanoX") }).signTransaction(approveTx), - ).rejects.toMatchObject({ statusCode: 0x6a80 }); - - expect(signInvocations).toEqual([usdcArcDescriptor]); - expect(fetchedUrls).toHaveLength(0); - }); -}); - -describe("ledger network descriptor helpers", () => { - afterEach(() => { - globalThis.fetch = originalFetch; - }); - - it("encodes the network payload as <0x15 len signature>", () => { - const payload = encodeNetworkInfoPayload(ARC_NANOX_DESCRIPTOR); - const tlv = Buffer.from(ARC_NANOX_DESCRIPTOR.data, "hex"); - const signature = Buffer.from(ARC_NANOX_DESCRIPTOR.signature, "hex"); - - expect(payload.readUInt16BE(0)).toBe(tlv.length + 2 + signature.length); - expect(payload.subarray(2, 2 + tlv.length).toString("hex")).toBe(ARC_NANOX_DESCRIPTOR.data); - expect(payload[2 + tlv.length]).toBe(0x15); - expect(payload[3 + tlv.length]).toBe(signature.length); - expect(payload.subarray(4 + tlv.length).toString("hex")).toBe(ARC_NANOX_DESCRIPTOR.signature); - }); - - it("picks the descriptor and icon matching the connected device model", async () => { - const fetchFn = (() => - Promise.resolve({ json: () => Promise.resolve(calNetworksResponse), ok: true })) as unknown as typeof fetch; - - expect(await fetchLedgerNetworkDescriptor(5042, "nanoX", fetchFn)).toEqual(ARC_NANOX_DESCRIPTOR); - expect(await fetchLedgerNetworkDescriptor(5042, "stax", fetchFn)).toEqual({ - data: "aa", - icon: "cc", - signature: "bb", - }); - expect(await fetchLedgerNetworkDescriptor(5042, "nanoS", fetchFn)).toBeNull(); - expect(await fetchLedgerNetworkDescriptor(5042, "blue", fetchFn)).toBeNull(); - expect(await fetchLedgerNetworkDescriptor(1, "nanoX", fetchFn)).toBeNull(); - }); - - it("splits payloads larger than 255 bytes into first/following chunks", async () => { - const sent: Array<{ p1: number; p2: number; length: number }> = []; - const transport = { - send: (_cla: number, _ins: number, p1: number, p2: number, data: Buffer) => { - sent.push({ length: data.length, p1, p2 }); - return Promise.resolve(Buffer.from("9000", "hex")); - }, - } as unknown as Transport; - - await provideLedgerNetworkInformation(transport, { ...ARC_NANOX_DESCRIPTOR, icon: "ab".repeat(300) }); - - expect(sent).toEqual([ - { length: encodeNetworkInfoPayload(ARC_NANOX_DESCRIPTOR).length, p1: 0x01, p2: 0x00 }, - { length: 255, p1: 0x01, p2: 0x01 }, - { length: 45, p1: 0x00, p2: 0x01 }, - ]); - }); -}); diff --git a/tools/builder/index.ts b/tools/builder/index.ts index ef6ce85..77804a7 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, @@ -39,7 +55,7 @@ export async function buildPackage({ ? { logs: [], outputs: [], success: true } : 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"); } From bc132214e6e2c3d60ee82a0b504c6538c471ac8f Mon Sep 17 00:00:00 2001 From: Ice Date: Thu, 20 Aug 2026 08:14:15 +0200 Subject: [PATCH 02/27] test(wallet-hardware): cover Ledger DMK migration --- .../test/ledger-bitcoin-dsk.test.ts | 213 ++++++++++ .../test/ledger-cosmos-dsk.test.ts | 101 +++++ .../wallet-hardware/test/ledger-dmk.test.ts | 229 +++++++++++ .../test/ledger-js-dmk-bridge.test.ts | 221 +++++++++++ .../test/ledger-js-dmk-clients.test.ts | 255 ++++++++++++ .../test/ledger-thorchain.test.ts | 342 ++++++++++++++-- .../test/ledger-transport.test.ts | 369 +++++++++++++----- .../test/ledger-zcash-dsk.test.ts | 187 +++++++++ .../test/ledger-zcash-legacy-prevtx.test.ts | 50 +++ 9 files changed, 1851 insertions(+), 116 deletions(-) create mode 100644 packages/wallet-hardware/test/ledger-bitcoin-dsk.test.ts create mode 100644 packages/wallet-hardware/test/ledger-cosmos-dsk.test.ts create mode 100644 packages/wallet-hardware/test/ledger-dmk.test.ts create mode 100644 packages/wallet-hardware/test/ledger-js-dmk-bridge.test.ts create mode 100644 packages/wallet-hardware/test/ledger-js-dmk-clients.test.ts create mode 100644 packages/wallet-hardware/test/ledger-zcash-dsk.test.ts create mode 100644 packages/wallet-hardware/test/ledger-zcash-legacy-prevtx.test.ts 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..37e4c2d --- /dev/null +++ b/packages/wallet-hardware/test/ledger-bitcoin-dsk.test.ts @@ -0,0 +1,213 @@ +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 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-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 }); + return deviceAction({ extendedPublicKey: accountXpub }); + }, + getMasterFingerprint: () => 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; + signedRaw = ""; + }); + + 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("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..d43dc3c 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 { 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); - } - 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); +mock.module("@ledgerhq/device-signer-kit-ethereum", () => ({ + SignerEthBuilder: class MockSignerEthBuilder { + constructor(params: { dmk: unknown; originToken?: string; sessionId: string }) { + ethereumBuilderInvocations.push(params); } - 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,17 +220,169 @@ 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("BitcoinLedger: connect reuses the app initialized by getExtendedPublicKey", async () => { + 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: 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("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 () => { @@ -213,6 +397,5 @@ describe("wallet-hardware/ledger — closure isolation with injected transport", await walletMethods?.getExtendedPublicKey?.({ accountIndex: 0 }); expect(bitcoinAppXpubInvocations).toEqual([{ path: "m/84'/2'/0'", xpubVersion: 27108450 }]); - expect(psbtAppClientInvocations).toHaveLength(0); }); }); 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..5f90879 --- /dev/null +++ b/packages/wallet-hardware/test/ledger-zcash-legacy-prevtx.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, it, mock } from "bun:test"; + +import { resolveZcashPreviousTransaction } 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 input = { + index: 3, + scriptPubkey: new Uint8Array([0x76, 0xa9, 0x14, 0x88, 0xac]), + txid: new Uint8Array(Array.from({ length: 32 }, (_, index) => index)), + value: 123_456n, + }; + + const resolved = await resolveZcashPreviousTransaction({ getRawTx, input, inputIndex: 2 }); + + expect(getRawTx).toHaveBeenCalledWith( + Array.from({ length: 32 }, (_, index) => 31 - index) + .map((byte) => byte.toString(16).padStart(2, "0")) + .join(""), + ); + expect(resolved).toMatchObject({ + index: 3, + txHex: "050000800a27a726real-previous-transaction", + value: 123_456, + witnessUtxo: { script: input.scriptPubkey, value: 123_456 }, + }); + }); + + 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); + }); +}); From 9ef0c194fb2dccca5785614d0dd9e9d7f71be129 Mon Sep 17 00:00:00 2001 From: Ice Date: Thu, 20 Aug 2026 08:14:30 +0200 Subject: [PATCH 03/27] docs(wallet-hardware): document Ledger DMK migration --- .changeset/ledger-evm-dmk.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .changeset/ledger-evm-dmk.md 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. From 7787418113fec23aae42a7f16d60e76353c09a37 Mon Sep 17 00:00:00 2001 From: Ice Date: Thu, 20 Aug 2026 08:55:53 +0200 Subject: [PATCH 04/27] chore(wallet-hardware): remove obsolete Ledger dependencies --- bun.lock | 187 ++---------------- package.json | 1 - packages/wallet-hardware/build.ts | 23 ++- packages/wallet-hardware/package.json | 16 -- .../src/ledger/clients/utxo-psbt.ts | 171 ---------------- .../wallet-hardware/src/ledger/cosmosTypes.ts | 98 --------- .../src/ledger/helpers/getLedgerTransport.ts | 102 ---------- .../src/ledger/helpers/index.ts | 1 - .../src/ledger/helpers/ledgerJsDmkBridge.ts | 4 +- 9 files changed, 26 insertions(+), 577 deletions(-) delete mode 100644 packages/wallet-hardware/src/ledger/clients/utxo-psbt.ts delete mode 100644 packages/wallet-hardware/src/ledger/cosmosTypes.ts delete mode 100644 packages/wallet-hardware/src/ledger/helpers/getLedgerTransport.ts diff --git a/bun.lock b/bun.lock index 4496913..0d7397a 100644 --- a/bun.lock +++ b/bun.lock @@ -24,7 +24,6 @@ "@swapkit/wallet-keystore": "^5.2.0", "@types/bun": "1.3.13", "@types/node": "25.6.0", - "ledger-bitcoin": "0.3.0", "typescript": "6.0.3", "zod": "4.4.3", }, @@ -93,7 +92,6 @@ "dependencies": { "@cosmjs/amino": "~0.37.0", "@cosmjs/crypto": "0.37.0", - "@cosmjs/proto-signing": "~0.37.0", "@keepkey/keepkey-sdk": "~0.2.62", "@ledgerhq/context-module": "~2.4.0", "@ledgerhq/device-management-kit": "~1.8.0", @@ -102,8 +100,6 @@ "@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/devices": "~8.10.0", - "@ledgerhq/errors": "~6.29.0", "@ledgerhq/hw-app-btc": "~10.17.0", "@ledgerhq/hw-app-cosmos": "~6.33.0", "@ledgerhq/hw-app-near": "~6.32.0", @@ -111,9 +107,6 @@ "@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", @@ -124,9 +117,7 @@ "@swapkit/wallet-core": "^5.0.8", "@trezor/connect-web": "~9.7.3", "bitcoinjs-lib": "~6.1.7", - "cosmjs-types": "~0.10.1", "ethers": "^6.14.0", - "ledger-bitcoin": "~0.3.0", "ripple-binary-codec": "2.7.0", "rxjs": "~7.8.2", "ts-pattern": "^5.9.0", @@ -135,7 +126,6 @@ "devDependencies": { "@cosmjs/amino": "0.37.0", "@cosmjs/crypto": "0.37.0", - "@cosmjs/proto-signing": "0.37.0", "@keepkey/keepkey-sdk": "0.2.62", "@ledgerhq/context-module": "2.4.0", "@ledgerhq/device-management-kit": "1.8.0", @@ -144,8 +134,6 @@ "@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/devices": "8.10.0", - "@ledgerhq/errors": "6.29.0", "@ledgerhq/hw-app-btc": "10.17.0", "@ledgerhq/hw-app-cosmos": "6.33.0", "@ledgerhq/hw-app-near": "6.32.0", @@ -153,18 +141,13 @@ "@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", "bitcoinjs-lib": "6.1.7", - "cosmjs-types": "0.10.1", "ethers": "6.16.0", - "ledger-bitcoin": "0.3.0", "ripple-binary-codec": "2.7.0", "rxjs": "7.8.2", "ts-pattern": "^5.9.0", @@ -367,12 +350,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=="], @@ -585,20 +562,12 @@ "@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-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/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/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=="], - "@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=="], - "@lit-labs/ssr-dom-shim": ["@lit-labs/ssr-dom-shim@1.6.0", "", {}, "sha512-VHb0ALPMTlgKjM6yIxxoQNnpKyUKLD04VzeQdsiXkMqkvYlAHxq9glGLmgbb889/1GsohSOAjvQYoiBppXFqrQ=="], "@lit/reactive-element": ["@lit/reactive-element@2.1.2", "", { "dependencies": { "@lit-labs/ssr-dom-shim": "^1.5.0" } }, "sha512-pbCDiVMnne1lYUIaYNN5wrwQXDtHaYtg7YEFPeW+hws6U47WeFvISGUWekPGKWOP1ygrs0ef0o1VJMk1exos5A=="], @@ -1091,12 +1060,6 @@ "@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=="], @@ -1409,7 +1372,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=="], @@ -1419,8 +1382,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=="], @@ -1471,8 +1432,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=="], @@ -1629,8 +1588,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=="], - "electron-to-chromium": ["electron-to-chromium@1.5.412", "", {}, "sha512-z4rMe3esBzlzovKHj4gxJnsCGZRK5l4baUvm+gCGJBPE+gsyUMKsuU9tnEUtI1dOebXz1ytAPGjvXhmQ7rIPwA=="], "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=="], @@ -1935,8 +1892,6 @@ "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=="], @@ -1951,8 +1906,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=="], @@ -2569,8 +2522,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=="], @@ -2581,16 +2532,12 @@ "@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/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/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/socket/isomorphic-ws": ["isomorphic-ws@4.0.1", "", { "peerDependencies": { "ws": "*" } }, "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w=="], "@cosmjs/socket/ws": ["ws@7.5.13", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA=="], - "@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=="], @@ -2627,12 +2574,6 @@ "@ledgerhq/signer-utils/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], - "@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=="], - "@manypkg/find-root/@types/node": ["@types/node@12.20.55", "", {}, "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ=="], "@manypkg/find-root/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=="], @@ -2749,10 +2690,6 @@ "@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=="], @@ -2927,14 +2864,6 @@ "@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/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=="], @@ -2991,26 +2920,12 @@ "@swapkit/ui/react-dom": ["react-dom@19.1.1", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.1" } }, "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw=="], - "@swapkit/wallet-extensions/@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-extensions/@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-extensions/@cosmjs/stargate": ["@cosmjs/stargate@0.37.1", "", { "dependencies": { "@cosmjs/amino": "^0.37.1", "@cosmjs/encoding": "^0.37.1", "@cosmjs/math": "^0.37.1", "@cosmjs/proto-signing": "^0.37.1", "@cosmjs/stream": "^0.37.1", "@cosmjs/tendermint-rpc": "^0.37.1", "@cosmjs/utils": "^0.37.1", "cosmjs-types": "^0.10.1" } }, "sha512-nQgaJB7A81cRYtmDrcIySq8hbd+QLXJTGFEs6R1xMdxascaABX0x86nS44ALk+EZojQQRnwupOsw1dKLlQmIFg=="], - "@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=="], - "@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=="], - "@swc/helpers/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "@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=="], @@ -3109,9 +3024,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=="], @@ -3127,8 +3040,6 @@ "bs58check/bs58": ["bs58@4.0.1", "", { "dependencies": { "base-x": "^3.0.2" } }, "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw=="], - "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=="], @@ -3303,8 +3214,6 @@ "@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/crypto/@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], @@ -3313,7 +3222,7 @@ "@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=="], - "@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/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/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=="], @@ -3339,14 +3248,10 @@ "@ledgerhq/hw-app-btc/bitcoinjs-lib/bech32": ["bech32@1.1.4", "", {}, "sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ=="], - "@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/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=="], @@ -3419,16 +3324,6 @@ "@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=="], @@ -3457,8 +3352,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=="], @@ -3541,28 +3434,16 @@ "@swapkit/ui/react-dom/scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="], - "@swapkit/wallet-extensions/@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-extensions/@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=="], "@swapkit/wallet-extensions/@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/@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-extensions/@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=="], - "@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/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/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/@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/wallets/@near-js/transactions/@noble/hashes": ["@noble/hashes@1.7.1", "", {}, "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ=="], - "@trezor/blockchain-link-utils/@stellar/stellar-sdk/eventsource": ["eventsource@2.0.2", "", {}, "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA=="], "@trezor/blockchain-link-utils/@trezor/protobuf/long": ["long@5.2.5", "", {}, "sha512-e0r9YBBgNCq1D1o5Dp8FMH0N5hsFtXDBiVa0qoJPHpakvZkmDKPRoGffZJII/XsHvj9An9blm+cRJ01yQqU+Dw=="], @@ -3685,12 +3566,6 @@ "@cosmjs/proto-signing/@cosmjs/crypto/@scure/bip39/@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="], - "@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=="], @@ -3703,10 +3578,6 @@ "@ledgerhq/device-signer-kit-ethereum/ethers/@types/node/undici-types": ["undici-types@6.19.8", "", {}, "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="], - "@ledgerhq/hw-app-btc/bitcoinjs-lib/bip32/@types/node": ["@types/node@10.12.18", "", {}, "sha512-fh+pAqt4xRzPfqA6eh3Z2y6fyZavRIumvjhaCL753+TVkGKGhpPeyrJG2JftD0T9q4GF00KjefsQ+PQNDdWQaQ=="], - - "@ledgerhq/psbtv2/bitcoinjs-lib/bip32/@types/node": ["@types/node@10.12.18", "", {}, "sha512-fh+pAqt4xRzPfqA6eh3Z2y6fyZavRIumvjhaCL753+TVkGKGhpPeyrJG2JftD0T9q4GF00KjefsQ+PQNDdWQaQ=="], - "@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=="], "@metamask/mobile-wallet-protocol-dapp-client/@metamask/utils/@ethereumjs/tx/@ethereumjs/rlp": ["@ethereumjs/rlp@4.0.1", "", { "bin": { "rlp": "bin/rlp" } }, "sha512-tqsQiBQDQdmPWE1xkkBq4rlSW5QZpLOUJ5RJh2/9fug+q9tnUhuZoVLk7s0scUIKTOzEtR72DFBXI4WiZcMpvw=="], @@ -3739,8 +3610,6 @@ "@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=="], @@ -3761,37 +3630,13 @@ "@swapkit/ui/@near-js/crypto/@noble/curves/@noble/hashes": ["@noble/hashes@1.7.1", "", {}, "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ=="], - "@swapkit/wallet-extensions/@cosmjs/amino/@cosmjs/crypto/@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], - - "@swapkit/wallet-extensions/@cosmjs/amino/@cosmjs/crypto/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], - - "@swapkit/wallet-extensions/@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-extensions/@cosmjs/proto-signing/@cosmjs/crypto/@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], "@swapkit/wallet-extensions/@cosmjs/proto-signing/@cosmjs/crypto/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], "@swapkit/wallet-extensions/@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/@cosmjs/amino/@cosmjs/crypto/@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], - - "@swapkit/wallet-hardware/@cosmjs/amino/@cosmjs/crypto/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], - - "@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=="], - - "@swapkit/wallets/@cosmjs/amino/@cosmjs/crypto/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], - - "@swapkit/wallets/@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-extensions/@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=="], "@swapkit/wallets/@cosmjs/proto-signing/@cosmjs/crypto/@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], @@ -3799,10 +3644,6 @@ "@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/@trezor/protobuf/protobufjs/long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], "@trezor/blockchain-link-utils/xrpl/@scure/bip32/@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], @@ -3841,8 +3682,6 @@ "ripemd160/hash-base/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], - "@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=="], @@ -3855,20 +3694,18 @@ "@swapkit/ui/@cosmjs/stargate/@cosmjs/tendermint-rpc/@cosmjs/socket/ws": ["ws@7.5.13", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA=="], - "@swapkit/wallet-extensions/@cosmjs/amino/@cosmjs/crypto/@scure/bip39/@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="], - "@swapkit/wallet-extensions/@cosmjs/proto-signing/@cosmjs/crypto/@scure/bip39/@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="], - "@swapkit/wallet-hardware/@cosmjs/amino/@cosmjs/crypto/@scure/bip39/@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="], + "@swapkit/wallet-extensions/@cosmjs/stargate/@cosmjs/amino/@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/@scure/bip39/@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="], + "@swapkit/wallet-extensions/@cosmjs/stargate/@cosmjs/amino/@cosmjs/crypto/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], - "@swapkit/wallets/@cosmjs/amino/@cosmjs/crypto/@scure/bip39/@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="], + "@swapkit/wallet-extensions/@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=="], "@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=="], - "pkg-dir/find-up/locate-path/p-locate/p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], + + "@swapkit/wallet-extensions/@cosmjs/stargate/@cosmjs/amino/@cosmjs/crypto/@scure/bip39/@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="], } } diff --git a/package.json b/package.json index d5d4e0e..428d5ed 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,6 @@ "@swapkit/wallet-keystore": "^5.2.0", "@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-hardware/build.ts b/packages/wallet-hardware/build.ts index ee426cd..a8d78fb 100644 --- a/packages/wallet-hardware/build.ts +++ b/packages/wallet-hardware/build.ts @@ -1,14 +1,13 @@ import { buildPackage } from "../../tools/builder"; -void buildPackage({ - bundlePackages: [ - "@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", - "@ledgerhq/hw-transport", - ], -}); +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 6f207c4..1bbc5d5 100644 --- a/packages/wallet-hardware/package.json +++ b/packages/wallet-hardware/package.json @@ -3,7 +3,6 @@ "dependencies": { "@cosmjs/amino": "~0.37.0", "@cosmjs/crypto": "0.37.0", - "@cosmjs/proto-signing": "~0.37.0", "@keepkey/keepkey-sdk": "~0.2.62", "@ledgerhq/context-module": "~2.4.0", "@ledgerhq/device-management-kit": "~1.8.0", @@ -12,8 +11,6 @@ "@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/devices": "~8.10.0", - "@ledgerhq/errors": "~6.29.0", "@ledgerhq/hw-app-btc": "~10.17.0", "@ledgerhq/hw-app-cosmos": "~6.33.0", "@ledgerhq/hw-app-near": "~6.32.0", @@ -21,9 +18,6 @@ "@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", @@ -34,9 +28,7 @@ "@swapkit/wallet-core": "^5.0.8", "@trezor/connect-web": "~9.7.3", "bitcoinjs-lib": "~6.1.7", - "cosmjs-types": "~0.10.1", "ethers": "^6.14.0", - "ledger-bitcoin": "~0.3.0", "ripple-binary-codec": "2.7.0", "rxjs": "~7.8.2", "ts-pattern": "^5.9.0", @@ -46,7 +38,6 @@ "devDependencies": { "@cosmjs/amino": "0.37.0", "@cosmjs/crypto": "0.37.0", - "@cosmjs/proto-signing": "0.37.0", "@keepkey/keepkey-sdk": "0.2.62", "@ledgerhq/context-module": "2.4.0", "@ledgerhq/device-management-kit": "1.8.0", @@ -55,8 +46,6 @@ "@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/devices": "8.10.0", - "@ledgerhq/errors": "6.29.0", "@ledgerhq/hw-app-btc": "10.17.0", "@ledgerhq/hw-app-cosmos": "6.33.0", "@ledgerhq/hw-app-near": "6.32.0", @@ -64,18 +53,13 @@ "@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", "bitcoinjs-lib": "6.1.7", - "cosmjs-types": "0.10.1", "ethers": "6.16.0", - "ledger-bitcoin": "0.3.0", "ripple-binary-codec": "2.7.0", "rxjs": "7.8.2", "ts-pattern": "^5.9.0", 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/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/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 db82607..378fa66 100644 --- a/packages/wallet-hardware/src/ledger/helpers/index.ts +++ b/packages/wallet-hardware/src/ledger/helpers/index.ts @@ -6,4 +6,3 @@ export { } 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 index ea87603..f088bb0 100644 --- a/packages/wallet-hardware/src/ledger/helpers/ledgerJsDmkBridge.ts +++ b/packages/wallet-hardware/src/ledger/helpers/ledgerJsDmkBridge.ts @@ -17,6 +17,8 @@ import type { LedgerDMKSession } from "./dmk"; import { executeLedgerDeviceAction, type LedgerDeviceActionStateHandler } from "./executeDeviceAction"; const STATUS_CODE_LENGTH = 2; +const TransportBase = + typeof Transport === "function" ? Transport : (Transport as unknown as { default: typeof Transport }).default; interface RawApduResponse { response: Uint8Array; @@ -67,7 +69,7 @@ function parseShortApdu(apdu: Buffer) { return new Apdu(cla, ins, p1, p2, apdu.subarray(5)); } -export class LedgerJsDmkTransport extends Transport { +export class LedgerJsDmkTransport extends TransportBase { constructor(private readonly internalApi: InternalApi) { super(); } From 475c0c85048c67e523a904279cdb521fa2c7f047 Mon Sep 17 00:00:00 2001 From: Ice Date: Thu, 20 Aug 2026 09:08:30 +0200 Subject: [PATCH 05/27] refactor(wallet-hardware): simplify Ledger dispatch --- .../src/ledger/clients/bitcoin.ts | 64 ++++----- .../src/ledger/helpers/executeDeviceAction.ts | 24 ++-- packages/wallet-hardware/src/ledger/index.ts | 121 +++++++++--------- 3 files changed, 106 insertions(+), 103 deletions(-) diff --git a/packages/wallet-hardware/src/ledger/clients/bitcoin.ts b/packages/wallet-hardware/src/ledger/clients/bitcoin.ts index 32a6db3..34e4bb0 100644 --- a/packages/wallet-hardware/src/ledger/clients/bitcoin.ts +++ b/packages/wallet-hardware/src/ledger/clients/bitcoin.ts @@ -11,6 +11,7 @@ import { } 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"; @@ -143,43 +144,46 @@ function stripHexPrefix(value: string) { return value.replace(/^0x/i, ""); } -async function signedInputParts({ +function signedInputParts({ rawInput, purpose, }: { rawInput: ReturnType; - purpose: number; + purpose: ParsedBitcoinPath["purpose"]; }) { - if (purpose === 86) { - const [tapKeySig] = rawInput.finalScriptWitness ?? []; - if (!tapKeySig) throw new SwapKitError("wallet_ledger_invalid_response", { reason: "Missing taproot signature" }); - return { tapKeySig }; - } - - if (purpose === 49 || purpose === 84) { - const [signature, publicKey] = rawInput.finalScriptWitness ?? []; - if (!signature || !publicKey) { - throw new SwapKitError("wallet_ledger_invalid_response", { reason: "Missing SegWit signature or public key" }); - } + 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" }); + } - if (purpose === 49) { + 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 [redeemScript] = Script.decode(rawInput.finalScriptSig ?? new Uint8Array()); - if (!(redeemScript instanceof Uint8Array)) { - throw new SwapKitError("wallet_ledger_invalid_response", { reason: "Missing nested SegWit redeem script" }); + 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]], redeemScript }; - } - - return { partialSig: [[publicKey, signature]] as [[Uint8Array, Uint8Array]] }; - } - - 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]] }; + return { partialSig: [[publicKey, signature]] as [[Uint8Array, Uint8Array]] }; + }) + .exhaustive(); } async function restoreSignedPsbt({ @@ -189,7 +193,7 @@ async function restoreSignedPsbt({ }: { rawTransaction: string; tx: Transaction; - purpose: number; + purpose: ParsedBitcoinPath["purpose"]; }) { const { Transaction } = await import("@swapkit/utxo-signer"); const parsedRaw = Transaction.fromRaw(hex.decode(rawTransaction)); diff --git a/packages/wallet-hardware/src/ledger/helpers/executeDeviceAction.ts b/packages/wallet-hardware/src/ledger/helpers/executeDeviceAction.ts index ec94489..67d92fc 100644 --- a/packages/wallet-hardware/src/ledger/helpers/executeDeviceAction.ts +++ b/packages/wallet-hardware/src/ledger/helpers/executeDeviceAction.ts @@ -1,9 +1,11 @@ -import type { - DeviceActionIntermediateValue, - DeviceActionState, - ExecuteDeviceActionReturnType, +import { + type DeviceActionIntermediateValue, + type DeviceActionState, + DeviceActionStatus, + type ExecuteDeviceActionReturnType, } from "@ledgerhq/device-management-kit"; import { SwapKitError } from "@swapkit/helpers"; +import { match } from "ts-pattern"; export type LedgerDeviceActionState = DeviceActionState; export type LedgerDeviceActionStateHandler = (state: LedgerDeviceActionState) => void; @@ -44,13 +46,13 @@ export function executeLedgerDeviceAction< void error; } - if (state.status === "completed") { - settle(() => resolve(state.output)); - } else if (state.status === "error") { - settle(() => reject(state.error)); - } else if (state.status === "stopped") { - settle(() => reject(new SwapKitError("wallet_ledger_connection_error"))); - } + match(state) + .with({ status: DeviceActionStatus.Completed }, ({ output }) => settle(() => resolve(output))) + .with({ status: DeviceActionStatus.Error }, ({ error }) => settle(() => reject(error))) + .with({ status: DeviceActionStatus.Stopped }, () => + settle(() => reject(new SwapKitError("wallet_ledger_connection_error"))), + ) + .otherwise(() => undefined); }, }); diff --git a/packages/wallet-hardware/src/ledger/index.ts b/packages/wallet-hardware/src/ledger/index.ts index 9550b25..437a61c 100644 --- a/packages/wallet-hardware/src/ledger/index.ts +++ b/packages/wallet-hardware/src/ledger/index.ts @@ -30,6 +30,7 @@ import { } from "@swapkit/toolboxes/utxo"; import type { Transaction, ZcashTransaction } from "@swapkit/utxo-signer"; import { createWallet, getWalletSupportedChains, type HardwareExtendedPublicKeyInfo } from "@swapkit/wallet-core"; +import { match } from "ts-pattern"; import { getLedgerAddress, getLedgerClient, @@ -225,75 +226,71 @@ async function getWalletMethods({ const address = providedAddress ?? (await getLedgerAddress({ chain, ledgerClient: signer })); - let toolboxSigner: - | { getAddress: () => Promise; signTransaction: (tx: Transaction) => Promise } - | undefined; - let signAndBroadcastLedgerTransaction: ((tx: Transaction | ZcashTransaction) => Promise) | undefined; - if (chain === Chain.Bitcoin) { - const bitcoinSigner = signer as BitcoinLedgerClient; - toolboxSigner = { getAddress: bitcoinSigner.getAddress, signTransaction: bitcoinSigner.signTransaction }; - signAndBroadcastLedgerTransaction = async (tx) => { - const signedTxHex = await bitcoinSigner.signTransactionHex({ tx: tx as Transaction }); - return toolbox.broadcastTx(signedTxHex); - }; - } else if ( - chain === Chain.BitcoinCash || - chain === Chain.Dogecoin || - chain === Chain.Dash || - chain === Chain.Litecoin - ) { - const { createLegacyPsbtSigner, signLegacyPsbtTransaction } = await import("./clients/utxo-legacy-adapter"); - const legacySigner = signer as LegacyUTXOLedgerClient; - toolboxSigner = createLegacyPsbtSigner({ address, chain: utxoChain, legacyClient: legacySigner }); - signAndBroadcastLedgerTransaction = async (tx) => { - const signedTxHex = await signLegacyPsbtTransaction({ - chain: utxoChain, - legacyClient: legacySigner, - tx: tx as Transaction, - }); - return toolbox.broadcastTx(signedTxHex); - }; - } else if (chain === Chain.Zcash) { - const zcashSigner = signer as ZcashLedgerClient; - signAndBroadcastLedgerTransaction = async (transaction) => { - if (!("consensusBranchId" in transaction)) { - throw new SwapKitError("wallet_ledger_method_not_supported", { - method: "signPCZT", - wallet: WalletOption.LEDGER, - }); - } - - const inputUtxos = await Promise.all( - Array.from({ length: transaction.inputsLength }, async (_, inputIndex) => { - const input = transaction.getInput(inputIndex); - const txid = hex.encode(input.txid); - const txHex = await getUtxoApi(Chain.Zcash).getRawTx(txid); - if (!txHex) { - throw new SwapKitError("wallet_ledger_invalid_params", { - inputIndex, - reason: "Unable to resolve previous transaction hex for Ledger signing", - txid, + const { signLedgerTransaction, toolboxSigner } = await match(chain) + .with(Chain.Bitcoin, () => { + const bitcoinSigner = signer as BitcoinLedgerClient; + return { + signLedgerTransaction: (tx: Transaction | ZcashTransaction) => + bitcoinSigner.signTransactionHex({ tx: tx 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: (tx: Transaction | ZcashTransaction) => + signLegacyPsbtTransaction({ chain: utxoChain, legacyClient: legacySigner, tx: tx as Transaction }), + toolboxSigner: createLegacyPsbtSigner({ address, chain: utxoChain, legacyClient: legacySigner }), + }; + }) + .with(Chain.Zcash, () => { + const zcashSigner = signer as ZcashLedgerClient; + return { + signLedgerTransaction: async (transaction: Transaction | ZcashTransaction) => { + if (!("consensusBranchId" in transaction)) { + throw new SwapKitError("wallet_ledger_method_not_supported", { + method: "signPCZT", + wallet: WalletOption.LEDGER, }); } - return { - hash: txid, - index: input.index, - txHex, - value: Number(input.value), - witnessUtxo: input.script ? { script: input.script, value: Number(input.value) } : undefined, - } as UTXOType; - }), - ); - const signedTxHex = await zcashSigner.signTransaction({ inputUtxos, tx: transaction }); - return toolbox.broadcastTx(signedTxHex); - }; - } + const inputUtxos = await Promise.all( + Array.from({ length: transaction.inputsLength }, async (_, inputIndex) => { + const input = transaction.getInput(inputIndex); + const txid = hex.encode(input.txid); + const txHex = await getUtxoApi(Chain.Zcash).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; + }), + ); + return zcashSigner.signTransaction({ inputUtxos, tx: transaction }); + }, + toolboxSigner: undefined, + }; + }) + .exhaustive(); const toolbox = toolboxSigner ? await getUtxoToolbox(utxoChain, { signer: toolboxSigner }) : getUtxoToolbox(utxoChain); - const signAndBroadcastTransaction = signAndBroadcastLedgerTransaction ?? toolbox.signAndBroadcastTransaction; + const signAndBroadcastTransaction = signLedgerTransaction + ? async (transaction: Transaction | ZcashTransaction) => + toolbox.broadcastTx(await signLedgerTransaction(transaction)) + : toolbox.signAndBroadcastTransaction; const transfer = async (params: UTXOBuildTxParams) => { const feeRate = params.feeRate || (await toolbox.getFeeRates())[FeeOption.Average]; From 1531b2120397452a0c252713822c222176409230 Mon Sep 17 00:00:00 2001 From: Ice Date: Thu, 20 Aug 2026 09:08:36 +0200 Subject: [PATCH 06/27] fix(wallet-hardware): keep Trezor CJS entrypoint loadable --- packages/wallet-hardware/src/trezor/index.ts | 43 +++++++++++++------- 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/packages/wallet-hardware/src/trezor/index.ts b/packages/wallet-hardware/src/trezor/index.ts index c590633..f7dcabf 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; @@ -583,10 +591,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, From 02c09f3f28651afc7e299a2fc51f688d7ddbf7fe Mon Sep 17 00:00:00 2001 From: Ice Date: Thu, 20 Aug 2026 09:59:56 +0200 Subject: [PATCH 07/27] refactor(wallet-hardware): exhaust Ledger wallet routing --- .../src/ledger/clients/utxo-legacy-adapter.ts | 10 +- packages/wallet-hardware/src/ledger/index.ts | 945 ++++++++++-------- .../test/ledger-transport.test.ts | 45 +- .../test/ledger-utxo-legacy-adapter.test.ts | 22 + 4 files changed, 581 insertions(+), 441 deletions(-) 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..9ec0596 100644 --- a/packages/wallet-hardware/src/ledger/clients/utxo-legacy-adapter.ts +++ b/packages/wallet-hardware/src/ledger/clients/utxo-legacy-adapter.ts @@ -50,16 +50,18 @@ export async function extractInputsFromPsbt(tx: Transaction, chain: UTXOChain): } 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/index.ts b/packages/wallet-hardware/src/ledger/index.ts index 437a61c..0e1442a 100644 --- a/packages/wallet-hardware/src/ledger/index.ts +++ b/packages/wallet-hardware/src/ledger/index.ts @@ -30,7 +30,7 @@ import { } from "@swapkit/toolboxes/utxo"; import type { Transaction, ZcashTransaction } from "@swapkit/utxo-signer"; import { createWallet, getWalletSupportedChains, type HardwareExtendedPublicKeyInfo } from "@swapkit/wallet-core"; -import { match } from "ts-pattern"; +import { match, P } from "ts-pattern"; import { getLedgerAddress, getLedgerClient, @@ -41,6 +41,7 @@ import { type BitcoinLedgerClient = ReturnType; type LegacyUTXOLedgerClient = ReturnType; type ZcashLedgerClient = ReturnType; +type UTXOLedgerClient = BitcoinLedgerClient | LegacyUTXOLedgerClient | ZcashLedgerClient; /** * Options passed to `connectLedger` at call time. @@ -61,6 +62,84 @@ export interface ConnectLedgerOptions { 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.Arc, + 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.Arc, + 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( @@ -116,33 +195,7 @@ export const ledgerWallet = createWallet({ }, getExtendedPublicKey: getLedgerExtendedPublicKey, name: "connectLedger", - supportedChains: [ - Chain.Arbitrum, - Chain.Arc, - 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, }); @@ -157,6 +210,21 @@ 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(); +} + export async function getLedgerExtendedPublicKey( chain: Chain, derivationPath?: DerivationPathArray, @@ -188,449 +256,470 @@ export async function getLedgerExtendedPublicKey( 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 = - chain === Chain.Bitcoin - ? await (signer as BitcoinLedgerClient).getExtendedPublicKey({ path: ledgerPath }) - : chain === Chain.Zcash - ? await (signer as ZcashLedgerClient).getExtendedPublicKey({ path: ledgerPath, xpubVersion }) - : await (signer as LegacyUTXOLedgerClient).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, - originToken, 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, dmkSession, onDeviceActionState, transport }); - - const address = providedAddress ?? (await getLedgerAddress({ chain, ledgerClient: signer })); - - const { signLedgerTransaction, toolboxSigner } = await match(chain) - .with(Chain.Bitcoin, () => { - const bitcoinSigner = signer as BitcoinLedgerClient; - return { - signLedgerTransaction: (tx: Transaction | ZcashTransaction) => - bitcoinSigner.signTransactionHex({ tx: tx 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: (tx: Transaction | ZcashTransaction) => - signLegacyPsbtTransaction({ chain: utxoChain, legacyClient: legacySigner, tx: tx as Transaction }), - toolboxSigner: createLegacyPsbtSigner({ address, chain: utxoChain, legacyClient: legacySigner }), - }; - }) - .with(Chain.Zcash, () => { - const zcashSigner = signer as ZcashLedgerClient; - return { - signLedgerTransaction: async (transaction: Transaction | ZcashTransaction) => { - if (!("consensusBranchId" in transaction)) { - throw new SwapKitError("wallet_ledger_method_not_supported", { - method: "signPCZT", - wallet: WalletOption.LEDGER, - }); - } - - const inputUtxos = await Promise.all( - Array.from({ length: transaction.inputsLength }, async (_, inputIndex) => { - const input = transaction.getInput(inputIndex); - const txid = hex.encode(input.txid); - const txHex = await getUtxoApi(Chain.Zcash).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; - }), - ); - return zcashSigner.signTransaction({ inputUtxos, tx: transaction }); - }, - toolboxSigner: undefined, - }; - }) - .exhaustive(); - - const toolbox = toolboxSigner - ? await getUtxoToolbox(utxoChain, { signer: toolboxSigner }) - : getUtxoToolbox(utxoChain); - const signAndBroadcastTransaction = signLedgerTransaction - ? async (transaction: Transaction | ZcashTransaction) => - toolbox.broadcastTx(await signLedgerTransaction(transaction)) - : 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 txHex = - chain === Chain.Bitcoin - ? await (signer as BitcoinLedgerClient).signTransactionHex({ inputUtxos: inputs, tx: tx as Transaction }) - : chain === Chain.Zcash - ? await (signer as ZcashLedgerClient).signTransaction({ inputUtxos: inputs, tx: tx as ZcashTransaction }) - : await (signer as LegacyUTXOLedgerClient).signTransaction(tx as Transaction, inputs); - const txHash = await toolbox.broadcastTx(txHex); - - return txHash; - }; +}: WalletMethodsParams) { + const { getUtxoToolbox } = await import("@swapkit/toolboxes/utxo"); + const utxoChain = chain; - 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 = - chain === Chain.Bitcoin - ? await (signer as BitcoinLedgerClient).getExtendedPublicKey({ path: ledgerPath }) - : chain === Chain.Zcash - ? await (signer as ZcashLedgerClient).getExtendedPublicKey({ path: ledgerPath, xpubVersion }) - : await (signer as LegacyUTXOLedgerClient).getExtendedPublicKey(ledgerPath, xpubVersion); - - return { accountIndex: getUTXOAccountIndexFromPath(accountPath), path, xpub }; - } - - 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 }); - - return { - accountIndex: getUTXOAccountIndexFromPath(fullPath), - address: derivedAddress, - change, - index, - 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); - } - - const hdHelpers = createHDWalletHelpers({ - chain: utxoChain, - deriveAddress: deriveAddressAtIndex, - getBalance: toolbox.getBalance, - getUtxos: (addr: string) => getUtxoApi(utxoChain).getUtxos({ address: addr, fetchTxHex: true }), - }); + const signer = await getLedgerClient({ chain, derivationPath, dmkSession, onDeviceActionState, transport }); - 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, - }); - } - - 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 (!signer.signTransactionWithMultiplePaths) { - throw new SwapKitError("wallet_ledger_method_not_supported", { method: "signTransactionWithMultiplePaths" }); - } - - const txHex = - chain === Chain.Bitcoin - ? await (signer as BitcoinLedgerClient).signTransactionWithMultiplePaths({ - derivationPaths: inputDerivationPaths, - inputUtxos: selectedInputs, - tx, - }) - : await (signer as LegacyUTXOLedgerClient).signTransactionWithMultiplePaths( - tx, - selectedInputs, - inputDerivationPaths, - ); - return toolbox.broadcastTx(txHex); - } + const address = providedAddress ?? (await getLedgerAddress({ chain, ledgerClient: signer })); + const { signLedgerTransaction, toolboxSigner } = await match(chain) + .with(Chain.Bitcoin, () => { + const bitcoinSigner = signer as BitcoinLedgerClient; return { - ...toolbox, - ...hdHelpers, - address, - deriveAddressAtIndex, - deriveAddresses, - getExtendedPublicKey, - getExtendedPublicKeyInfo, - signAndBroadcastTransaction, - transfer, - transferFromMultipleAddresses, + 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 Promise.all( + Array.from({ length: transaction.inputsLength }, async (_, inputIndex) => { + const input = transaction.getInput(inputIndex); + const txid = hex.encode(input.txid); + const txHex = await getUtxoApi(Chain.Zcash).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; + }), + )); + 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 }); - case Chain.Ethereum: - case Chain.Avalanche: - case Chain.Arbitrum: - case Chain.Arc: - 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 signer = await getLedgerClient({ - chain, - derivationPath, + return { accountIndex: getUTXOAccountIndexFromPath(accountPath), path, xpub }; + } + + 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, - originToken, transport, }); - const { getEvmToolboxAsync } = await import("@swapkit/toolboxes/evm"); - const address = await getLedgerAddress({ chain, ledgerClient: signer }); - const toolbox = await getEvmToolboxAsync(chain, { signer }); + const derivedAddress = await getLedgerAddress({ chain: utxoChain, ledgerClient: indexedSigner }); - return { ...toolbox, address }; + return { + accountIndex: getUTXOAccountIndexFromPath(fullPath), + address: derivedAddress, + change, + index, + path: derivationPathToString(fullPath), + pubkey: "", + }; + } catch { + return undefined; } + } - case Chain.Cosmos: { - const { createSigningStargateClient, getDefaultChainFee, 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 }], - getDefaultChainFee(Chain.Cosmos), - memo, - ); - - return transactionHash; - }; + 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); + } - return { ...toolbox, address, transfer }; - } + const hdHelpers = createHDWalletHelpers({ + chain: utxoChain, + deriveAddress: deriveAddressAtIndex, + getBalance: toolbox.getBalance, + getUtxos: (addr: string) => getUtxoApi(utxoChain).getUtxos({ address: addr, fetchTxHex: true }), + }); - case Chain.THORChain: { - 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; + 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, + }); + } - return { ...toolbox, address, signMessage }; + if (!utxos.length) { + throw new SwapKitError("wallet_ledger_invalid_params", { + message: "No UTXOs provided for multi-address transfer", + }); } - case Chain.Near: { - 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 }); + const txFeeRate = feeRate || (await toolbox.getFeeRates())[feeOptionKey || FeeOption.Fast]; + const memoScript = memo ? compileMemo(memo) : null; - return { ...toolbox, address: accountId }; + 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 }); } - case Chain.Ripple: { - const { getRippleToolbox } = await import("@swapkit/toolboxes/ripple"); - const signer = await getLedgerClient({ chain, derivationPath, dmkSession, onDeviceActionState, transport }); - const address = signer.getAddress(); - const toolbox = getRippleToolbox({ signer }); + 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", + }); + } - 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.Tron: { - 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 }); + 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 }; - } + return { + ...toolbox, + ...hdHelpers, + address, + deriveAddressAtIndex, + deriveAddresses, + getExtendedPublicKey, + getExtendedPublicKeyInfo, + signAndBroadcastTransaction, + transfer, + transferFromMultipleAddresses, + }; +} - case Chain.Sui: { - 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 }); +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 }; +} + +async function getCosmosWalletMethods({ + chain, + derivationPath, + dmkSession, + onDeviceActionState, + transport, +}: WalletMethodsParams) { + const { createSigningStargateClient, getDefaultChainFee, 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 }], + getDefaultChainFee(Chain.Cosmos), + memo, + ); + + return transactionHash; + }; + + return { ...toolbox, address, transfer }; +} - default: - throw new SwapKitError("wallet_ledger_chain_not_supported", { chain }); +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 }; +} + +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 }); + + return { ...toolbox, address: accountId }; +} + +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 }); + + 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 }); + + 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 }; +} + +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"; diff --git a/packages/wallet-hardware/test/ledger-transport.test.ts b/packages/wallet-hardware/test/ledger-transport.test.ts index d43dc3c..bd76090 100644 --- a/packages/wallet-hardware/test/ledger-transport.test.ts +++ b/packages/wallet-hardware/test/ledger-transport.test.ts @@ -1,7 +1,7 @@ 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 { Chain, WalletOption } from "@swapkit/helpers"; import { hexlify, Signature, Transaction } from "ethers"; import { of } from "rxjs"; @@ -385,17 +385,44 @@ describe("wallet-hardware/ledger", () => { ]); }); - 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(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 98551e1..bf28ef7 100644 --- a/packages/wallet-hardware/test/ledger-utxo-legacy-adapter.test.ts +++ b/packages/wallet-hardware/test/ledger-utxo-legacy-adapter.test.ts @@ -91,6 +91,28 @@ describe("ledger legacy UTXO adapter", () => { expect(signedTxHex).toBe("0200000000"); }); + + it("reuses prepared inputs without fetching previous transactions again", async () => { + rawTxRequests.length = 0; + 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"); + }); }); afterAll(() => { From 50964c2f78714742833e7c11162aae99f33a755b Mon Sep 17 00:00:00 2001 From: Ice Date: Thu, 20 Aug 2026 10:28:08 +0200 Subject: [PATCH 08/27] fix(wallet-hardware): resolve Ledger prev txs concurrently --- .../src/ledger/clients/utxo-legacy-adapter.ts | 54 +++++++++---------- .../src/ledger/clients/utxo.ts | 45 +++++++++++----- .../src/ledger/helpers/rawTx.ts | 12 +++++ packages/wallet-hardware/src/ledger/index.ts | 54 ++++++++++--------- .../test/ledger-utxo-legacy-adapter.test.ts | 53 +++++++++++++++--- .../test/ledger-zcash-legacy-prevtx.test.ts | 45 +++++++++++++--- 6 files changed, 184 insertions(+), 79 deletions(-) create mode 100644 packages/wallet-hardware/src/ledger/helpers/rawTx.ts 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 9ec0596..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,37 +17,36 @@ 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({ diff --git a/packages/wallet-hardware/src/ledger/clients/utxo.ts b/packages/wallet-hardware/src/ledger/clients/utxo.ts index 175e5c2..61bcbad 100644 --- a/packages/wallet-hardware/src/ledger/clients/utxo.ts +++ b/packages/wallet-hardware/src/ledger/clients/utxo.ts @@ -18,12 +18,20 @@ import { 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", @@ -52,10 +60,10 @@ export async function resolveZcashPreviousTransaction({ inputIndex, }: { getRawTx: (txid: string) => Promise; - input: { index: number; scriptPubkey: Uint8Array; txid: Uint8Array; value: bigint }; + input: ZcashPreviousTransactionInput; inputIndex: number; }) { - const txid = hex.encode(new Uint8Array([...input.txid].reverse())); + const txid = hex.encode(input.txid); const txHex = await getRawTx(txid); if (!txHex) { throw new SwapKitError("wallet_ledger_invalid_params", { @@ -75,6 +83,19 @@ export async function resolveZcashPreviousTransaction({ } 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, @@ -238,11 +259,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(), @@ -250,16 +269,14 @@ const BaseLedgerUTXO = ({ txid: input.txid, value: input.value, }); - - inputUtxos.push( - await resolveZcashPreviousTransaction({ - getRawTx: (txid) => getUtxoApi(Chain.Zcash).getRawTx(txid), - input, - inputIndex: i, - }), - ); } + 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 }); 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 0e1442a..7ef805a 100644 --- a/packages/wallet-hardware/src/ledger/index.ts +++ b/packages/wallet-hardware/src/ledger/index.ts @@ -37,6 +37,7 @@ import { type LedgerDeviceActionStateHandler, type LedgerDMKSession, } from "./helpers"; +import { createCachedRawTxResolver } from "./helpers/rawTx"; type BitcoinLedgerClient = ReturnType; type LegacyUTXOLedgerClient = ReturnType; @@ -225,6 +226,34 @@ function getLedgerAccountXpub({ chain, path, signer, xpubVersion }: LedgerAccoun .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, @@ -314,30 +343,7 @@ async function getUTXOWalletMethods({ }); } - const resolvedInputUtxos = - inputUtxos ?? - (await Promise.all( - Array.from({ length: transaction.inputsLength }, async (_, inputIndex) => { - const input = transaction.getInput(inputIndex); - const txid = hex.encode(input.txid); - const txHex = await getUtxoApi(Chain.Zcash).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; - }), - )); + const resolvedInputUtxos = inputUtxos ?? (await resolveZcashInputUtxos(transaction)); return zcashSigner.signTransaction({ inputUtxos: resolvedInputUtxos, tx: transaction }); }, toolboxSigner: undefined, 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 bf28ef7..1c43b4c 100644 --- a/packages/wallet-hardware/test/ledger-utxo-legacy-adapter.test.ts +++ b/packages/wallet-hardware/test/ledger-utxo-legacy-adapter.test.ts @@ -1,10 +1,11 @@ -import { afterAll, describe, expect, it, mock } from "bun:test"; +import { afterAll, beforeEach, describe, expect, it, mock } from "bun:test"; import { hex } from "@scure/base"; import { Chain } from "@swapkit/helpers"; import * as realUtxoToolbox from "@swapkit/toolboxes/utxo"; import { RawTx, Transaction } from "@swapkit/utxo-signer"; const rawTxRequests: Array<{ chain: string; txid: string }> = []; +let getRawTxResponse = (_chain: string, _txid: string) => Promise.resolve("02000000000100"); const realUtxoToolboxSnapshot = { ...realUtxoToolbox }; @@ -12,7 +13,7 @@ mock.module("@swapkit/toolboxes/utxo", () => ({ getUtxoApi: (chain: string) => ({ getRawTx: (txid: string) => { rawTxRequests.push({ chain, txid }); - return Promise.resolve("02000000000100"); + return getRawTxResponse(chain, txid); }, }), })); @@ -20,9 +21,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: [ @@ -53,8 +57,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({ @@ -69,6 +71,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 }); @@ -93,7 +135,6 @@ describe("ledger legacy UTXO adapter", () => { }); it("reuses prepared inputs without fetching previous transactions again", async () => { - rawTxRequests.length = 0; 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) => { diff --git a/packages/wallet-hardware/test/ledger-zcash-legacy-prevtx.test.ts b/packages/wallet-hardware/test/ledger-zcash-legacy-prevtx.test.ts index 5f90879..8c22206 100644 --- a/packages/wallet-hardware/test/ledger-zcash-legacy-prevtx.test.ts +++ b/packages/wallet-hardware/test/ledger-zcash-legacy-prevtx.test.ts @@ -1,25 +1,25 @@ import { describe, expect, it, mock } from "bun:test"; +import { PCZT } from "@swapkit/utxo-signer"; -import { resolveZcashPreviousTransaction } from "../src/ledger/clients/utxo"; +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 input = { - index: 3, - scriptPubkey: new Uint8Array([0x76, 0xa9, 0x14, 0x88, 0xac]), - txid: new Uint8Array(Array.from({ length: 32 }, (_, index) => index)), - value: 123_456n, - }; + 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({ length: 32 }, (_, index) => 31 - index) + 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, @@ -27,6 +27,35 @@ describe("legacy Ledger Zcash previous transactions", () => { }); }); + 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("")); From 250f53035cfdcc35c0cfafd847981ee4dd9e8d63 Mon Sep 17 00:00:00 2001 From: Ice Date: Fri, 21 Aug 2026 11:50:54 +0200 Subject: [PATCH 09/27] fix(wallet-hardware): address Ledger review regressions --- .../src/ledger/clients/bitcoin.ts | 36 +++++++---- .../wallet-hardware/src/ledger/clients/evm.ts | 33 +++++++++- .../src/ledger/clients/near.ts | 5 +- .../wallet-hardware/src/ledger/clients/sui.ts | 5 +- .../src/ledger/clients/thorchain/index.ts | 60 ++++++++++++++----- .../src/ledger/clients/thorchain/protocol.ts | 29 ++++----- .../src/ledger/clients/tron.ts | 7 ++- .../src/ledger/clients/utxo.ts | 9 +-- .../wallet-hardware/src/ledger/clients/xrp.ts | 5 +- .../src/ledger/helpers/executeDeviceAction.ts | 27 ++++++--- .../src/ledger/helpers/ledgerJsDmkBridge.ts | 45 ++++++++------ .../test/ledger-bitcoin-dsk.test.ts | 51 +++++++++++++++- .../test/ledger-transport.test.ts | 22 +++++++ 13 files changed, 251 insertions(+), 83 deletions(-) diff --git a/packages/wallet-hardware/src/ledger/clients/bitcoin.ts b/packages/wallet-hardware/src/ledger/clients/bitcoin.ts index 34e4bb0..4f1d041 100644 --- a/packages/wallet-hardware/src/ledger/clients/bitcoin.ts +++ b/packages/wallet-hardware/src/ledger/clients/bitcoin.ts @@ -239,24 +239,36 @@ export function BitcoinLedger({ } function getAccountXpub() { - accountXpubPromise ??= getSigner().then(async (signer) => { - const { extendedPublicKey } = await executeLedgerDeviceAction({ - action: signer.getExtendedPublicKey(configuredPath.accountPath), - onDeviceActionState, + if (!accountXpubPromise) { + accountXpubPromise = getSigner().then(async (signer) => { + const { extendedPublicKey } = await executeLedgerDeviceAction({ + action: signer.getExtendedPublicKey(configuredPath.accountPath), + onDeviceActionState, + }); + return extendedPublicKey; }); - return extendedPublicKey; - }); + const pendingXpub = accountXpubPromise; + void pendingXpub.catch(() => { + if (accountXpubPromise === pendingXpub) accountXpubPromise = undefined; + }); + } return accountXpubPromise; } function getMasterFingerprint() { - fingerprintPromise ??= getSigner().then(async (signer) => { - const { masterFingerprint } = await executeLedgerDeviceAction({ - action: signer.getMasterFingerprint(), - onDeviceActionState, + if (!fingerprintPromise) { + fingerprintPromise = getSigner().then(async (signer) => { + const { masterFingerprint } = await executeLedgerDeviceAction({ + action: signer.getMasterFingerprint(), + onDeviceActionState, + }); + return fingerprintToNumber(masterFingerprint); }); - return fingerprintToNumber(masterFingerprint); - }); + const pendingFingerprint = fingerprintPromise; + void pendingFingerprint.catch(() => { + if (fingerprintPromise === pendingFingerprint) fingerprintPromise = undefined; + }); + } return fingerprintPromise; } diff --git a/packages/wallet-hardware/src/ledger/clients/evm.ts b/packages/wallet-hardware/src/ledger/clients/evm.ts index edf5023..b89c35d 100644 --- a/packages/wallet-hardware/src/ledger/clients/evm.ts +++ b/packages/wallet-hardware/src/ledger/clients/evm.ts @@ -27,6 +27,36 @@ interface EVMLedgerParams { 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", + }); + } + + return selectedTypes; +} + class EVMLedgerInterface extends AbstractSigner { chainId: ChainId = ChainId.Ethereum; derivationPath = ""; @@ -122,7 +152,8 @@ class EVMLedgerInterface extends AbstractSigner { const { hexlify, Signature, TypedDataEncoder } = await import("ethers"); const { EIP712Domain: _, ...filteredTypes } = types; const primaryType = explicitPrimaryType ?? TypedDataEncoder.from(filteredTypes).primaryType; - const populated = await TypedDataEncoder.resolveNames(domain, filteredTypes, value, async (name) => { + 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; diff --git a/packages/wallet-hardware/src/ledger/clients/near.ts b/packages/wallet-hardware/src/ledger/clients/near.ts index 9e55df8..5108e09 100644 --- a/packages/wallet-hardware/src/ledger/clients/near.ts +++ b/packages/wallet-hardware/src/ledger/clients/near.ts @@ -1,10 +1,11 @@ -import { UserInteractionRequired } from "@ledgerhq/device-management-kit"; +import type { UserInteractionRequired } from "@ledgerhq/device-management-kit"; import type Transport from "@ledgerhq/hw-transport"; import type { SignedTransaction, Transaction } from "@near-js/transactions"; import { Chain, type DerivationPathArray, NetworkDerivationPath, SwapKitError } from "@swapkit/helpers"; import type { NearSigner } from "@swapkit/toolboxes/near"; import { + LEDGER_USER_INTERACTION_REQUIRED, type LedgerJsClientParams, normalizeLedgerJsClientParams, runLedgerJsOperation, @@ -71,7 +72,7 @@ export async function getNearLedgerClient( try { const signatureArray = await runNearOperation({ operation: (app) => app.signTransaction(transaction.encode(), path), - requiredUserInteraction: UserInteractionRequired.SignTransaction, + requiredUserInteraction: LEDGER_USER_INTERACTION_REQUIRED.SignTransaction, }); if (!signatureArray) throw new Error("Signature undefined"); diff --git a/packages/wallet-hardware/src/ledger/clients/sui.ts b/packages/wallet-hardware/src/ledger/clients/sui.ts index f229174..eb5568e 100644 --- a/packages/wallet-hardware/src/ledger/clients/sui.ts +++ b/packages/wallet-hardware/src/ledger/clients/sui.ts @@ -1,4 +1,4 @@ -import { UserInteractionRequired } from "@ledgerhq/device-management-kit"; +import type { UserInteractionRequired } from "@ledgerhq/device-management-kit"; import type Sui from "@ledgerhq/hw-app-sui"; import type Transport from "@ledgerhq/hw-transport"; import { @@ -10,6 +10,7 @@ import { } from "@swapkit/helpers"; import { + LEDGER_USER_INTERACTION_REQUIRED, type LedgerJsClientParams, normalizeLedgerJsClientParams, runLedgerJsOperation, @@ -86,7 +87,7 @@ export class SuiLedgerInterface { const result = await this.runSuiOperation({ operation: (app) => app.signTransaction(ledgerPath, intentMessage), - requiredUserInteraction: UserInteractionRequired.SignTransaction, + requiredUserInteraction: LEDGER_USER_INTERACTION_REQUIRED.SignTransaction, }); if (!result?.signature) throw new SwapKitError("wallet_ledger_signing_error"); diff --git a/packages/wallet-hardware/src/ledger/clients/thorchain/index.ts b/packages/wallet-hardware/src/ledger/clients/thorchain/index.ts index 40c4cea..6061216 100644 --- a/packages/wallet-hardware/src/ledger/clients/thorchain/index.ts +++ b/packages/wallet-hardware/src/ledger/clients/thorchain/index.ts @@ -1,24 +1,21 @@ import type { AccountData, AminoSignResponse, StdSignDoc } from "@cosmjs/amino"; import { Secp256k1Signature } from "@cosmjs/crypto"; -import { - CallTaskInAppDeviceAction, - type DmkError, - DmkResultFactory, - isSuccessCommandResult, - UnknownDeviceExchangeError, - UserInteractionRequired, -} from "@ledgerhq/device-management-kit"; +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, SKConfig, SwapKitError } from "@swapkit/helpers"; import type { LedgerDMKSession } from "../../helpers/dmk"; -import { executeLedgerDeviceAction, type LedgerDeviceActionStateHandler } from "../../helpers/executeDeviceAction"; +import { + executeLedgerDeviceAction, + LEDGER_USER_INTERACTION_REQUIRED, + type LedgerDeviceActionStateHandler, +} from "../../helpers/executeDeviceAction"; import { getThorAddressCommand, getThorLegacyVersion, getThorSignCommands, - invalidThorAppVersion, + invalidThorAppVersionMessage, parseThorAddressResponse, sendThorLegacyCommand, type ThorCommand, @@ -115,7 +112,10 @@ export class THORChainLedger { }) => { if (this.transport) { const version = await getThorLegacyVersion({ transport: this.transport }); - if (!version.startsWith("2.")) throw invalidThorAppVersion({ version }); + 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) { @@ -127,6 +127,34 @@ export class THORChainLedger { 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 { + 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)), + }); + } + }, + }; + }; + const deviceAction = new CallTaskInAppDeviceAction<{ response: Uint8Array }, DmkError, UserInteractionRequired>({ input: { appName: THOR_APP_NAME, @@ -138,13 +166,13 @@ export class THORChainLedger { const version = "currentApp" in sessionState ? sessionState.currentApp.version : undefined; if (!version?.startsWith("2.")) { return DmkResultFactory<{ response: Uint8Array }, DmkError>({ - error: invalidThorAppVersion({ version }), + error: new InvalidResponseFormatError(invalidThorAppVersionMessage({ version })), }); } let response: Uint8Array = new Uint8Array(); for (const command of commands) { - const result = await internalApi.sendCommand(command); + const result = await internalApi.sendCommand(toDmkCommand(command)); if (!isSuccessCommandResult(result)) { return DmkResultFactory<{ response: Uint8Array }, DmkError>({ error: result.error }); } @@ -175,7 +203,9 @@ export class THORChainLedger { commands: [ getThorAddressCommand({ checkOnDevice, hrp: isStagenet ? "sthor" : "thor", path: this.derivationPath }), ], - requiredUserInteraction: checkOnDevice ? UserInteractionRequired.VerifyAddress : UserInteractionRequired.None, + requiredUserInteraction: checkOnDevice + ? LEDGER_USER_INTERACTION_REQUIRED.VerifyAddress + : LEDGER_USER_INTERACTION_REQUIRED.None, }); return parseThorAddressResponse({ response }); @@ -184,7 +214,7 @@ export class THORChainLedger { private signBytes = async ({ message }: { message: Uint8Array }) => { const response = await this.executeCommands({ commands: getThorSignCommands({ message, path: this.derivationPath }), - requiredUserInteraction: UserInteractionRequired.SignTransaction, + requiredUserInteraction: LEDGER_USER_INTERACTION_REQUIRED.SignTransaction, }); return getFixedSignature({ signature: response }); diff --git a/packages/wallet-hardware/src/ledger/clients/thorchain/protocol.ts b/packages/wallet-hardware/src/ledger/clients/thorchain/protocol.ts index 69d6724..a679be3 100644 --- a/packages/wallet-hardware/src/ledger/clients/thorchain/protocol.ts +++ b/packages/wallet-hardware/src/ledger/clients/thorchain/protocol.ts @@ -1,11 +1,3 @@ -import { - ApduBuilder, - type ApduResponse, - type Command, - CommandResultFactory, - InvalidResponseFormatError, - InvalidStatusWordError, -} from "@ledgerhq/device-management-kit"; import type Transport from "@ledgerhq/hw-transport"; import { SwapKitError } from "@swapkit/helpers"; @@ -26,7 +18,12 @@ interface ThorCommandParams { p2?: number; } -export class ThorCommand implements Command { +interface ThorApduResponse { + data: Uint8Array; + statusCode: Uint8Array; +} + +export class ThorCommand { readonly name = "THORChainCommand"; private readonly params: ThorCommandParams; @@ -36,18 +33,16 @@ export class ThorCommand implements Command { getApdu() { const { data = new Uint8Array(), ins, p1 = 0, p2 = 0 } = this.params; - return new ApduBuilder({ cla: CLA, ins, p1, p2 }).addBufferToData(data).build(); + return { cla: CLA, data, ins, p1, p2 }; } - parseResponse(response: ApduResponse) { + parseResponse(response: ThorApduResponse) { const status = (response.statusCode[0] ?? 0) * 0x100 + (response.statusCode[1] ?? 0); if (status !== STATUS_OK) { - return CommandResultFactory({ - error: new InvalidStatusWordError(`THORChain app returned status 0x${status.toString(16).padStart(4, "0")}`), - }); + throw new Error(`THORChain app returned status 0x${status.toString(16).padStart(4, "0")}`); } - return CommandResultFactory({ data: response.data }); + return response.data; } } @@ -158,6 +153,6 @@ export function parseThorAddressResponse({ response }: { response: Uint8Array }) return { address, publicKey }; } -export function invalidThorAppVersion({ version }: { version?: string }) { - return new InvalidResponseFormatError(`THORChain Ledger app major 2 is required, received ${version || "unknown"}`); +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/tron.ts b/packages/wallet-hardware/src/ledger/clients/tron.ts index 880e44f..5fa802a 100644 --- a/packages/wallet-hardware/src/ledger/clients/tron.ts +++ b/packages/wallet-hardware/src/ledger/clients/tron.ts @@ -1,4 +1,4 @@ -import { UserInteractionRequired } from "@ledgerhq/device-management-kit"; +import type { UserInteractionRequired } from "@ledgerhq/device-management-kit"; import type TronApp from "@ledgerhq/hw-app-trx"; import type Transport from "@ledgerhq/hw-transport"; import { @@ -10,6 +10,7 @@ import { import type { TronSignedTransaction, TronSigner, TronTransaction } from "@swapkit/toolboxes/tron"; import { + LEDGER_USER_INTERACTION_REQUIRED, type LedgerJsClientParams, normalizeLedgerJsClientParams, runLedgerJsOperation, @@ -72,7 +73,7 @@ export class TronLedgerInterface implements TronSigner { showAddressAndPubKey = async () => { return await this.runTronOperation({ operation: (app) => app.getAddress(this.derivationPath, true), - requiredUserInteraction: UserInteractionRequired.VerifyAddress, + requiredUserInteraction: LEDGER_USER_INTERACTION_REQUIRED.VerifyAddress, }); }; @@ -80,7 +81,7 @@ export class TronLedgerInterface implements TronSigner { try { const signature = await this.runTronOperation({ operation: (app) => app.signTransaction(this.derivationPath, transaction.raw_data_hex, []), - requiredUserInteraction: UserInteractionRequired.SignTransaction, + requiredUserInteraction: LEDGER_USER_INTERACTION_REQUIRED.SignTransaction, }); if (!signature) throw new SwapKitError("wallet_ledger_signing_error"); diff --git a/packages/wallet-hardware/src/ledger/clients/utxo.ts b/packages/wallet-hardware/src/ledger/clients/utxo.ts index 61bcbad..732ecaf 100644 --- a/packages/wallet-hardware/src/ledger/clients/utxo.ts +++ b/packages/wallet-hardware/src/ledger/clients/utxo.ts @@ -1,4 +1,4 @@ -import { UserInteractionRequired } from "@ledgerhq/device-management-kit"; +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"; @@ -14,6 +14,7 @@ import type { UTXOType } from "@swapkit/toolboxes/utxo"; import type { PCZT, Transaction } from "@swapkit/utxo-signer"; import { + LEDGER_USER_INTERACTION_REQUIRED, type LedgerJsClientParams, normalizeLedgerJsClientParams, runLedgerJsOperation, @@ -296,7 +297,7 @@ const BaseLedgerUTXO = ({ lockTime: global.lockTime, }, ), - requiredUserInteraction: UserInteractionRequired.SignTransaction, + requiredUserInteraction: LEDGER_USER_INTERACTION_REQUIRED.SignTransaction, }); const signedTx = ZcashTransaction.fromHex(signedTxHex, { allowUnknownOutputs: true }); @@ -318,7 +319,7 @@ const BaseLedgerUTXO = ({ return await runBtcOperation({ operation: (app) => signUTXOTransaction({ btcApp: app, chain, derivationPath, inputUtxos, tx }, additionalSignParams), - requiredUserInteraction: UserInteractionRequired.SignTransaction, + requiredUserInteraction: LEDGER_USER_INTERACTION_REQUIRED.SignTransaction, }); }, @@ -333,7 +334,7 @@ const BaseLedgerUTXO = ({ { btcApp: app, chain, derivationPaths, inputUtxos, tx }, additionalSignParams, ), - requiredUserInteraction: UserInteractionRequired.SignTransaction, + requiredUserInteraction: LEDGER_USER_INTERACTION_REQUIRED.SignTransaction, }); }, }; diff --git a/packages/wallet-hardware/src/ledger/clients/xrp.ts b/packages/wallet-hardware/src/ledger/clients/xrp.ts index a7ce80d..6dd7abd 100644 --- a/packages/wallet-hardware/src/ledger/clients/xrp.ts +++ b/packages/wallet-hardware/src/ledger/clients/xrp.ts @@ -1,4 +1,4 @@ -import { UserInteractionRequired } from "@ledgerhq/device-management-kit"; +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"; @@ -7,6 +7,7 @@ import { encode } from "ripple-binary-codec"; import type { Payment } from "xrpl"; import { + LEDGER_USER_INTERACTION_REQUIRED, type LedgerJsClientParams, normalizeLedgerJsClientParams, runLedgerJsOperation, @@ -58,7 +59,7 @@ export async function XRPLedger(paramsOrPath?: XRPLedgerParams | DerivationPathA const transactionToSignOnLedger = encode(transactionJSON); const txnSignature = await runXrpOperation({ operation: (app) => app.signTransaction(path, transactionToSignOnLedger), - requiredUserInteraction: UserInteractionRequired.SignTransaction, + requiredUserInteraction: LEDGER_USER_INTERACTION_REQUIRED.SignTransaction, }); const tx_blob = encode({ ...transactionJSON, TxnSignature: txnSignature }); const hash = hashes.hashSignedTx(tx_blob); diff --git a/packages/wallet-hardware/src/ledger/helpers/executeDeviceAction.ts b/packages/wallet-hardware/src/ledger/helpers/executeDeviceAction.ts index 67d92fc..b520a74 100644 --- a/packages/wallet-hardware/src/ledger/helpers/executeDeviceAction.ts +++ b/packages/wallet-hardware/src/ledger/helpers/executeDeviceAction.ts @@ -1,12 +1,25 @@ -import { - type DeviceActionIntermediateValue, - type DeviceActionState, +import type { + DeviceActionIntermediateValue, + DeviceActionState, DeviceActionStatus, - type ExecuteDeviceActionReturnType, + 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; @@ -47,9 +60,9 @@ export function executeLedgerDeviceAction< } match(state) - .with({ status: DeviceActionStatus.Completed }, ({ output }) => settle(() => resolve(output))) - .with({ status: DeviceActionStatus.Error }, ({ error }) => settle(() => reject(error))) - .with({ status: DeviceActionStatus.Stopped }, () => + .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); diff --git a/packages/wallet-hardware/src/ledger/helpers/ledgerJsDmkBridge.ts b/packages/wallet-hardware/src/ledger/helpers/ledgerJsDmkBridge.ts index f088bb0..cea2a1c 100644 --- a/packages/wallet-hardware/src/ledger/helpers/ledgerJsDmkBridge.ts +++ b/packages/wallet-hardware/src/ledger/helpers/ledgerJsDmkBridge.ts @@ -1,12 +1,8 @@ -import { +import type { Apdu, - type ApduResponse, - CallTaskInAppDeviceAction, - type Command, - CommandResultFactory, - DmkResultFactory, - type InternalApi, - isSuccessCommandResult, + ApduResponse, + Command, + InternalApi, UnknownDeviceExchangeError, UserInteractionRequired, } from "@ledgerhq/device-management-kit"; @@ -14,7 +10,13 @@ import Transport, { TransportError } from "@ledgerhq/hw-transport"; import { type DerivationPathArray, SwapKitError } from "@swapkit/helpers"; import type { LedgerDMKSession } from "./dmk"; -import { executeLedgerDeviceAction, type LedgerDeviceActionStateHandler } from "./executeDeviceAction"; +import { + executeLedgerDeviceAction, + LEDGER_USER_INTERACTION_REQUIRED, + type LedgerDeviceActionStateHandler, +} from "./executeDeviceAction"; + +export { LEDGER_USER_INTERACTION_REQUIRED }; const STATUS_CODE_LENGTH = 2; const TransportBase = @@ -27,7 +29,10 @@ interface RawApduResponse { class RawApduCommand implements Command { readonly name = "ledgerJsRawApdu"; - constructor(private readonly apdu: Apdu) {} + constructor( + private readonly apdu: Apdu, + private readonly dmkModule: typeof import("@ledgerhq/device-management-kit"), + ) {} getApdu() { return this.apdu; @@ -35,8 +40,8 @@ class RawApduCommand implements Command { parseResponse({ data, statusCode }: ApduResponse) { if (statusCode.length !== STATUS_CODE_LENGTH) { - return CommandResultFactory({ - error: new UnknownDeviceExchangeError(new Error("Ledger returned an invalid APDU status code")), + return this.dmkModule.CommandResultFactory({ + error: new this.dmkModule.UnknownDeviceExchangeError(new Error("Ledger returned an invalid APDU status code")), }); } @@ -44,7 +49,7 @@ class RawApduCommand implements Command { response.set(data); response.set(statusCode, data.length); - return CommandResultFactory({ data: { response } }); + return this.dmkModule.CommandResultFactory({ data: { response } }); } } @@ -66,7 +71,7 @@ function parseShortApdu(apdu: Buffer) { throw new TransportError("Ledger APDU header is incomplete", "InvalidAPDU"); } - return new Apdu(cla, ins, p1, p2, apdu.subarray(5)); + return { cla, data: apdu.subarray(5), ins, p1, p2 }; } export class LedgerJsDmkTransport extends TransportBase { @@ -75,10 +80,12 @@ export class LedgerJsDmkTransport extends TransportBase { } async exchange(apdu: Buffer, { abortTimeoutMs }: { abortTimeoutMs?: number } = {}) { - const command = new RawApduCommand(parseShortApdu(apdu)); + 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 (!isSuccessCommandResult(result)) throw result.error; + if (!dmkModule.isSuccessCommandResult(result)) throw result.error; return Buffer.from(result.data.response); } @@ -114,7 +121,7 @@ export async function runLedgerJsOperation({ connection, createApp, operation, - requiredUserInteraction = UserInteractionRequired.None, + requiredUserInteraction = LEDGER_USER_INTERACTION_REQUIRED.None, }: { appName: string; connection: LedgerJsClientConnection; @@ -138,6 +145,10 @@ export async function runLedgerJsOperation({ }); } + const { CallTaskInAppDeviceAction, DmkResultFactory, UnknownDeviceExchangeError } = await import( + "@ledgerhq/device-management-kit" + ); + const deviceAction = new CallTaskInAppDeviceAction< { output: Output }, UnknownDeviceExchangeError, diff --git a/packages/wallet-hardware/test/ledger-bitcoin-dsk.test.ts b/packages/wallet-hardware/test/ledger-bitcoin-dsk.test.ts index 37e4c2d..832dae6 100644 --- a/packages/wallet-hardware/test/ledger-bitcoin-dsk.test.ts +++ b/packages/wallet-hardware/test/ledger-bitcoin-dsk.test.ts @@ -14,7 +14,10 @@ const walletAddressCalls: Array<{ }> = []; 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 { @@ -26,6 +29,10 @@ function deviceAction(output: Output) { }; } +function failedDeviceAction(error: Error) { + return { cancel: mock(() => {}), observable: of({ error, status: DeviceActionStatus.Error }) }; +} + mock.module("@ledgerhq/device-signer-kit-bitcoin", () => ({ DefaultDescriptorTemplate: { LEGACY: "pkh(@0/**)", @@ -44,9 +51,20 @@ mock.module("@ledgerhq/device-signer-kit-bitcoin", () => ({ 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: () => deviceAction({ masterFingerprint: Uint8Array.of(0xde, 0xad, 0xbe, 0xef) }), + 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, @@ -96,7 +114,10 @@ describe("Ledger Bitcoin Device Signer Kit client", () => { 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 () => { @@ -144,6 +165,34 @@ describe("Ledger Bitcoin Device Signer Kit client", () => { 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); diff --git a/packages/wallet-hardware/test/ledger-transport.test.ts b/packages/wallet-hardware/test/ledger-transport.test.ts index bd76090..8b1baec 100644 --- a/packages/wallet-hardware/test/ledger-transport.test.ts +++ b/packages/wallet-hardware/test/ledger-transport.test.ts @@ -283,6 +283,28 @@ describe("wallet-hardware/ledger", () => { ]); }); + 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 }); From 6bba1fbf3ecc5975620e7471c4140d636f72b61f Mon Sep 17 00:00:00 2001 From: Ice Date: Fri, 21 Aug 2026 11:51:07 +0200 Subject: [PATCH 10/27] test: preserve ethers exports in Bun module mocks --- packages/wallet-extensions/test/evm-extensions.test.ts | 2 +- packages/wallets/test/metamask.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/wallet-extensions/test/evm-extensions.test.ts b/packages/wallet-extensions/test/evm-extensions.test.ts index 899497f..0a7c40b 100644 --- a/packages/wallet-extensions/test/evm-extensions.test.ts +++ b/packages/wallet-extensions/test/evm-extensions.test.ts @@ -63,7 +63,7 @@ class MockBrowserProvider { const realEthersSnapshot = { ...realEthers }; const realEvmToolboxSnapshot = { ...realEvmToolbox }; -mock.module("ethers", () => ({ BrowserProvider: MockBrowserProvider })); +mock.module("ethers", () => ({ ...realEthersSnapshot, BrowserProvider: MockBrowserProvider })); mock.module("@swapkit/toolboxes/evm", () => ({ getEvmToolboxAsync: async () => ({ diff --git a/packages/wallets/test/metamask.test.ts b/packages/wallets/test/metamask.test.ts index 4b61cc7..cbd84ff 100644 --- a/packages/wallets/test/metamask.test.ts +++ b/packages/wallets/test/metamask.test.ts @@ -61,7 +61,7 @@ mock.module("@metamask/connect-multichain", () => ({ }, })); -mock.module("ethers", () => ({ BrowserProvider: MockBrowserProvider })); +mock.module("ethers", () => ({ ...realEthersSnapshot, BrowserProvider: MockBrowserProvider })); mock.module("@swapkit/wallet-extensions/evm-extensions", () => ({ getWeb3WalletMethods: (options: Record) => { From 5b842ee312499161b9d361ae3d79b2987f600f9f Mon Sep 17 00:00:00 2001 From: Ice Date: Wed, 23 Sep 2026 13:31:14 +0200 Subject: [PATCH 11/27] fix(wallet-hardware): adapt Ledger clients to SDK 5.x - narrow LedgerJS client params with a type guard: Array.isArray does not exclude helpers 5.x readonly DerivationPathArray tuples - type CosmosLedger.signAmino against the toolbox OfflineAminoSigner so it satisfies createSigningStargateClient - load cosmjs lazily in the Cosmos and THORChain clients and their tests; Bun rejects cosmjs' CJS require of ESM-only @scure/base next to the ESM-only @swapkit/* graph --- .../src/ledger/clients/cosmos.ts | 18 +++++++++++++----- .../src/ledger/clients/thorchain/index.ts | 5 +++-- .../src/ledger/helpers/ledgerJsDmkBridge.ts | 9 ++++++++- packages/wallet-hardware/src/ledger/index.ts | 2 +- .../test/ledger-cosmos-dsk.test.ts | 5 ++++- .../test/ledger-thorchain.test.ts | 5 ++++- 6 files changed, 33 insertions(+), 11 deletions(-) diff --git a/packages/wallet-hardware/src/ledger/clients/cosmos.ts b/packages/wallet-hardware/src/ledger/clients/cosmos.ts index 8ef8bbb..24b5ae1 100644 --- a/packages/wallet-hardware/src/ledger/clients/cosmos.ts +++ b/packages/wallet-hardware/src/ledger/clients/cosmos.ts @@ -1,5 +1,4 @@ -import { encodeSecp256k1Signature, type StdSignDoc, serializeSignDoc } from "@cosmjs/amino"; -import { Secp256k1Signature } from "@cosmjs/crypto"; +import type { StdSignDoc } from "@cosmjs/amino"; import type { SignerCosmos } from "@ledgerhq/device-signer-kit-cosmos"; import type Transport from "@ledgerhq/hw-transport"; import { hex } from "@scure/base"; @@ -9,6 +8,7 @@ import { NetworkDerivationPath, SwapKitError, } from "@swapkit/helpers"; +import type { OfflineAminoSigner } from "@swapkit/toolboxes/cosmos"; import type { LedgerDMKSession } from "../helpers/dmk"; import { getLedgerDMKSession } from "../helpers/dmk"; @@ -41,7 +41,10 @@ function normalizeCosmosPath(path: DerivationPathArray | string) { return normalized; } -function normalizeCosmosSignature(signature: Uint8Array) { +// cosmjs is CJS and `require`s ESM-only `@scure/base`; loading it lazily keeps Bun from rejecting that +// require when the module graph also contains the ESM-only `@swapkit/*` packages. +async function normalizeCosmosSignature(signature: Uint8Array) { + const { Secp256k1Signature } = await import("@cosmjs/crypto"); try { return signature.length === 64 ? Secp256k1Signature.fromFixedLength(signature).toFixedLength() @@ -164,14 +167,19 @@ export class CosmosLedger { return [{ pub_key: { type: "tendermint/PubKeySecp256k1", value: this.pubKey }, sequence, signature }]; } - async signAmino(signerAddress: string, signDoc: StdSignDoc) { + // Typed against the toolbox signer contract so CosmosLedger satisfies `createSigningStargateClient`. + async signAmino( + signerAddress: string, + signDoc: Parameters[1], + ): ReturnType { const accounts = await this.getAccounts(); const account = accounts.find(({ address }) => address === signerAddress); if (!account) { throw new SwapKitError("wallet_ledger_address_not_found", { address: signerAddress }); } - const signature = await this.signBytes(serializeSignDoc(signDoc)); + const { encodeSecp256k1Signature, serializeSignDoc } = await import("@cosmjs/amino"); + const signature = await this.signBytes(serializeSignDoc(signDoc as StdSignDoc)); return { signature: encodeSecp256k1Signature(account.pubkey, signature), signed: signDoc }; } diff --git a/packages/wallet-hardware/src/ledger/clients/thorchain/index.ts b/packages/wallet-hardware/src/ledger/clients/thorchain/index.ts index 6061216..e5bc5ff 100644 --- a/packages/wallet-hardware/src/ledger/clients/thorchain/index.ts +++ b/packages/wallet-hardware/src/ledger/clients/thorchain/index.ts @@ -1,5 +1,4 @@ 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"; @@ -64,7 +63,9 @@ export function normalizeThorchainLedgerSignDoc(signDoc: StdSignDoc): StdSignDoc }; } -function getFixedSignature({ signature }: { signature: Uint8Array }) { +// cosmjs is loaded lazily: Bun rejects its CJS `require("@scure/base")` next to the ESM-only `@swapkit/*` graph. +async function getFixedSignature({ signature }: { signature: Uint8Array }) { + const { Secp256k1Signature } = await import("@cosmjs/crypto"); try { return Secp256k1Signature.fromDer(signature).toFixedLength(); } catch (error) { diff --git a/packages/wallet-hardware/src/ledger/helpers/ledgerJsDmkBridge.ts b/packages/wallet-hardware/src/ledger/helpers/ledgerJsDmkBridge.ts index cea2a1c..bc56f59 100644 --- a/packages/wallet-hardware/src/ledger/helpers/ledgerJsDmkBridge.ts +++ b/packages/wallet-hardware/src/ledger/helpers/ledgerJsDmkBridge.ts @@ -105,6 +105,13 @@ export interface LedgerJsClientParams( + value: LedgerJsClientParams | Path | undefined, +): value is LedgerJsClientParams { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + export function normalizeLedgerJsClientParams({ paramsOrPath, transport, @@ -112,7 +119,7 @@ export function normalizeLedgerJsClientParams | Path; transport?: Transport; }): LedgerJsClientParams { - if (paramsOrPath && typeof paramsOrPath === "object" && !Array.isArray(paramsOrPath)) return paramsOrPath; + if (isLedgerJsClientParams(paramsOrPath)) return paramsOrPath; return { derivationPath: paramsOrPath as Path | undefined, transport }; } diff --git a/packages/wallet-hardware/src/ledger/index.ts b/packages/wallet-hardware/src/ledger/index.ts index 7ef805a..1a67b10 100644 --- a/packages/wallet-hardware/src/ledger/index.ts +++ b/packages/wallet-hardware/src/ledger/index.ts @@ -602,7 +602,7 @@ async function getCosmosWalletMethods({ transport, }: WalletMethodsParams) { const { createSigningStargateClient, getDefaultChainFee, getMsgSendDenom, getCosmosToolbox } = await import( - "@swapkit/toolboxes/cosmos", + "@swapkit/toolboxes/cosmos" ); const signer = await getLedgerClient({ chain, derivationPath, dmkSession, onDeviceActionState, transport }); const address = await getLedgerAddress({ chain, ledgerClient: signer }); diff --git a/packages/wallet-hardware/test/ledger-cosmos-dsk.test.ts b/packages/wallet-hardware/test/ledger-cosmos-dsk.test.ts index f5b622c..e283081 100644 --- a/packages/wallet-hardware/test/ledger-cosmos-dsk.test.ts +++ b/packages/wallet-hardware/test/ledger-cosmos-dsk.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it, mock } from "bun:test"; -import { encodeSecp256k1Signature, type StdSignDoc, serializeSignDoc } from "@cosmjs/amino"; +import type { StdSignDoc } from "@cosmjs/amino"; import { DeviceActionStatus, type DeviceManagementKit } from "@ledgerhq/device-management-kit"; import { of } from "rxjs"; @@ -37,6 +37,9 @@ mock.module("@ledgerhq/device-signer-kit-cosmos", () => ({ import { CosmosLedger } from "../src/ledger/clients/cosmos"; +// cosmjs is CJS requiring ESM-only `@scure/base`; a static import next to `@swapkit/*` fails under Bun. +const { encodeSecp256k1Signature, serializeSignDoc } = await import("@cosmjs/amino"); + const dmkSession = { dmk: { id: "cosmos-dmk" } as unknown as DeviceManagementKit, sessionId: "cosmos-session" }; const signDoc: StdSignDoc = { account_number: "17", diff --git a/packages/wallet-hardware/test/ledger-thorchain.test.ts b/packages/wallet-hardware/test/ledger-thorchain.test.ts index a32f05f..8cdeab5 100644 --- a/packages/wallet-hardware/test/ledger-thorchain.test.ts +++ b/packages/wallet-hardware/test/ledger-thorchain.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, mock } from "bun:test"; -import { type StdSignDoc, serializeSignDoc } from "@cosmjs/amino"; +import type { StdSignDoc } from "@cosmjs/amino"; import { ApduResponse, CallTaskInAppDeviceAction, @@ -20,6 +20,9 @@ import { serializeThorPath, } from "../src/ledger/clients/thorchain/protocol"; +// cosmjs is CJS requiring ESM-only `@scure/base`; a static import next to `@swapkit/*` fails under Bun. +const { serializeSignDoc } = await import("@cosmjs/amino"); + interface ThorDmkAction { input: { appName: string; From c686376779e344ed5e4d2f54ab38a8467691148e Mon Sep 17 00:00:00 2001 From: Ice Date: Wed, 23 Sep 2026 13:32:00 +0200 Subject: [PATCH 12/27] chore: update @swapkit/* dependencies to latest SDK release --- .changeset/swapkit-sdk-1ice5rq4kml65.md | 17 +++ bun.lock | 174 +++++++++++++++--------- package.json | 14 +- packages/sdk/package.json | 14 +- packages/wallet-extensions/package.json | 6 +- packages/wallet-hardware/package.json | 6 +- packages/wallet-mobile/package.json | 6 +- packages/wallets/package.json | 6 +- 8 files changed, 154 insertions(+), 89 deletions(-) create mode 100644 .changeset/swapkit-sdk-1ice5rq4kml65.md diff --git a/.changeset/swapkit-sdk-1ice5rq4kml65.md b/.changeset/swapkit-sdk-1ice5rq4kml65.md new file mode 100644 index 0000000..4ffd754 --- /dev/null +++ b/.changeset/swapkit-sdk-1ice5rq4kml65.md @@ -0,0 +1,17 @@ +--- +"@swapkit/sdk": patch +"@swapkit/wallet-extensions": patch +"@swapkit/wallet-hardware": patch +"@swapkit/wallet-mobile": patch +"@swapkit/wallets": patch +--- + +Update SwapKit SDK dependencies: + +- @swapkit/core: 5.0.8 → 5.0.9 +- @swapkit/helpers: 5.4.1 → 5.4.2 +- @swapkit/plugins: 5.0.8 → 5.0.9 +- @swapkit/server: 5.2.1 → 5.2.2 +- @swapkit/toolboxes: 5.4.1 → 5.4.2 +- @swapkit/wallet-core: 5.0.8 → 5.0.9 +- @swapkit/wallet-keystore: 5.2.0 → 5.2.1 diff --git a/bun.lock b/bun.lock index 0d7397a..cb622d2 100644 --- a/bun.lock +++ b/bun.lock @@ -15,13 +15,13 @@ "@cosmjs/proto-signing": "0.37.0", "@cosmjs/stargate": "0.37.0", "@scure/bip32": "2.2.0", - "@swapkit/core": "^5.0.8", - "@swapkit/helpers": "^5.4.1", - "@swapkit/plugins": "^5.0.8", - "@swapkit/server": "^5.2.1", - "@swapkit/toolboxes": "^5.4.1", - "@swapkit/wallet-core": "^5.0.8", - "@swapkit/wallet-keystore": "^5.2.0", + "@swapkit/core": "^5.0.9", + "@swapkit/helpers": "^5.4.2", + "@swapkit/plugins": "^5.0.9", + "@swapkit/server": "^5.2.2", + "@swapkit/toolboxes": "^5.4.2", + "@swapkit/wallet-core": "^5.0.9", + "@swapkit/wallet-keystore": "^5.2.1", "@types/bun": "1.3.13", "@types/node": "25.6.0", "typescript": "6.0.3", @@ -33,13 +33,13 @@ "version": "4.6.52", "dependencies": { "@stricahq/typhonjs": "~3.0.1", - "@swapkit/core": "^5.0.8", - "@swapkit/helpers": "^5.4.1", - "@swapkit/plugins": "^5.0.8", - "@swapkit/server": "^5.2.1", - "@swapkit/toolboxes": "^5.4.1", - "@swapkit/wallet-core": "^5.0.8", - "@swapkit/wallet-keystore": "^5.2.0", + "@swapkit/core": "^5.0.9", + "@swapkit/helpers": "^5.4.2", + "@swapkit/plugins": "^5.0.9", + "@swapkit/server": "^5.2.2", + "@swapkit/toolboxes": "^5.4.2", + "@swapkit/wallet-core": "^5.0.9", + "@swapkit/wallet-keystore": "^5.2.1", "@swapkit/wallets": "workspace:*", "cosmjs-types": "0.10.1", }, @@ -58,10 +58,10 @@ "@near-js/transactions": "~2.5.0", "@scure/base": "~2.2.0", "@solana/web3.js": "~1.98.4", - "@swapkit/helpers": "^5.4.1", - "@swapkit/toolboxes": "^5.4.1", + "@swapkit/helpers": "^5.4.2", + "@swapkit/toolboxes": "^5.4.2", "@swapkit/utxo-signer": "^3.1.0", - "@swapkit/wallet-core": "^5.0.8", + "@swapkit/wallet-core": "^5.0.9", "@wallet-standard/app": "^1.1.1", "cosmjs-types": "0.10.1", "ethers": "^6.14.0", @@ -111,10 +111,10 @@ "@near-js/transactions": "~2.5.0", "@scure/base": "2.2.0", "@scure/bip32": "2.2.0", - "@swapkit/helpers": "^5.4.1", - "@swapkit/toolboxes": "^5.4.1", + "@swapkit/helpers": "^5.4.2", + "@swapkit/toolboxes": "^5.4.2", "@swapkit/utxo-signer": "^3.1.0", - "@swapkit/wallet-core": "^5.0.8", + "@swapkit/wallet-core": "^5.0.9", "@trezor/connect-web": "~9.7.3", "bitcoinjs-lib": "~6.1.7", "ethers": "^6.14.0", @@ -158,9 +158,9 @@ "name": "@swapkit/wallet-mobile", "version": "4.3.26", "dependencies": { - "@swapkit/helpers": "^5.4.1", - "@swapkit/toolboxes": "^5.4.1", - "@swapkit/wallet-core": "^5.0.8", + "@swapkit/helpers": "^5.4.2", + "@swapkit/toolboxes": "^5.4.2", + "@swapkit/wallet-core": "^5.0.9", }, }, "packages/wallets": { @@ -183,10 +183,10 @@ "@scure/base": "~2.2.0", "@scure/bip39": "~2.2.0", "@solana/web3.js": "~1.98.4", - "@swapkit/helpers": "^5.4.1", - "@swapkit/toolboxes": "^5.4.1", + "@swapkit/helpers": "^5.4.2", + "@swapkit/toolboxes": "^5.4.2", "@swapkit/utxo-signer": "^3.1.0", - "@swapkit/wallet-core": "^5.0.8", + "@swapkit/wallet-core": "^5.0.9", "@swapkit/wallet-extensions": "workspace:*", "@swapkit/wallet-hardware": "workspace:*", "@ton/core": "~0.63.1", @@ -1096,35 +1096,35 @@ "@swapkit/contracts": ["@swapkit/contracts@5.0.0", "", {}, "sha512-iqHjDIYr5dWiQRld7ks2sh/bxPQfkPrw96UpF135xePZJgcjufYPH4OBCoh6aY9JX0n5mr0QyIoe0PDrcBWWKw=="], - "@swapkit/core": ["@swapkit/core@5.0.8", "", { "dependencies": { "@swapkit/helpers": "5.4.1", "@swapkit/plugins": "5.0.8", "@swapkit/toolboxes": "5.4.1", "@swapkit/wallet-core": "5.0.8" }, "peerDependencies": { "@stricahq/typhonjs": "3.1.0", "@ton/core": "0.63.1", "cosmjs-types": "0.11.0", "ts-pattern": "5.9.0" } }, "sha512-NY9hsbEKqj8tYPyZlZRzM2TRYj0gFDu3ChfDi2h2VkadXw9xjlWm2JRHB6q2d8Nh4ZSutxeafraD1vKuRI6UIw=="], + "@swapkit/core": ["@swapkit/core@5.0.9", "", { "dependencies": { "@swapkit/helpers": "5.4.2", "@swapkit/plugins": "5.0.9", "@swapkit/toolboxes": "5.4.2", "@swapkit/wallet-core": "5.0.9" }, "peerDependencies": { "@stricahq/typhonjs": "3.1.0", "@ton/core": "0.63.1", "cosmjs-types": "0.11.0", "ts-pattern": "5.9.0" } }, "sha512-Jga+KMtu5eJhp84KAaRYxN4U2232v0z9tV631QBnW/IA/n5+sCz+krqL3X0/LpC2QnLlYnhL8wrUV2dFuJOyXg=="], "@swapkit/cosmos-signer": ["@swapkit/cosmos-signer@0.2.0", "", { "peerDependencies": { "@noble/curves": "2.2.0", "@noble/hashes": "2.2.0", "@scure/base": "2.2.0", "@scure/bip32": "2.2.0", "@scure/bip39": "2.2.0", "cosmjs-types": "0.11.0" } }, "sha512-++NFJuxvIzD9zZsbr69asbtZOgTsnuXHZm5/iKKup8SGdl0WeNlXk2LMDY8YNtEyNYwzzj3jTcAbVvDZQWzLmQ=="], - "@swapkit/helpers": ["@swapkit/helpers@5.4.1", "", { "dependencies": { "@noble/hashes": "2.2.0", "@swapkit/contracts": "5.0.0", "@swapkit/tokens": "5.0.8", "@swapkit/types": "1.2.1" }, "peerDependencies": { "ts-pattern": "5.9.0", "zod": "4.4.3", "zustand": "5.0.13" } }, "sha512-NhLQLr+9OovUSSih1APplP904y+EHeC/6A58I62g830B3Uy4bpokYWH4bXP5svmNtxkLVsrDqEso53ZrJxJsJw=="], + "@swapkit/helpers": ["@swapkit/helpers@5.4.2", "", { "dependencies": { "@noble/hashes": "2.2.0", "@swapkit/contracts": "5.0.0", "@swapkit/tokens": "5.0.9", "@swapkit/types": "1.2.2" }, "peerDependencies": { "ts-pattern": "5.9.0", "zod": "4.4.3", "zustand": "5.0.13" } }, "sha512-lOuUMvOd/tTrujzBviyCE5bI1W65HbThdym6ynf5Eu1IFsxBDfQIzr9+VWL2HbnThgm5OWaPn6LmcrXzz+YCiQ=="], - "@swapkit/plugins": ["@swapkit/plugins@5.0.8", "", { "dependencies": { "@swapkit/helpers": "5.4.1", "@swapkit/toolboxes": "5.4.1", "@swapkit/utxo-signer": "3.1.0" }, "peerDependencies": { "@mysten/sui": "2.17.0", "@near-js/transactions": "2.5.1", "@near-js/utils": "2.5.1", "@noble/hashes": "2.2.0", "@scure/base": "2.2.0", "@solana/web3.js": "1.98.4", "@stellar/stellar-sdk": "16.1.0", "ethers": "6.16.0", "ts-pattern": "5.9.0" } }, "sha512-yGebbLNiKQ1Weok1Aw55jvg1GvU+wH1ryA0BQnuVTRLcHGnT/E8An9GZc2KhrUJ5WfMcZeo1We0sANp49aNjKA=="], + "@swapkit/plugins": ["@swapkit/plugins@5.0.9", "", { "dependencies": { "@swapkit/helpers": "5.4.2", "@swapkit/toolboxes": "5.4.2", "@swapkit/utxo-signer": "3.1.0" }, "peerDependencies": { "@mysten/sui": "2.17.0", "@near-js/transactions": "2.5.1", "@near-js/utils": "2.5.1", "@noble/hashes": "2.2.0", "@scure/base": "2.2.0", "@solana/web3.js": "1.98.4", "@stellar/stellar-sdk": "16.1.0", "ethers": "6.16.0", "ts-pattern": "5.9.0" } }, "sha512-F9p4de8HubzxXa6Z9jnyeFL+FLbij3f/x+vgI1GYose7V0q1v4d9XIGQdoFDGnje3peh+CYZRNhy0MpXowSImQ=="], "@swapkit/sdk": ["@swapkit/sdk@workspace:packages/sdk"], - "@swapkit/server": ["@swapkit/server@5.2.1", "", { "dependencies": { "@swapkit/helpers": "5.4.1", "@swapkit/tokens": "5.0.8", "@swapkit/toolboxes": "5.4.1", "@swapkit/wallet-keystore": "5.2.0" }, "peerDependencies": { "ts-pattern": "5.9.0" } }, "sha512-z3hrJThs1DDfQawWkwNkxz3DY7Fyzf0Ys4khF5kx5AVek6I3LT850drSi5bpFsgEiSlByUiMH0uPnGP4PZeReQ=="], + "@swapkit/server": ["@swapkit/server@5.2.2", "", { "dependencies": { "@swapkit/helpers": "5.4.2", "@swapkit/tokens": "5.0.9", "@swapkit/toolboxes": "5.4.2", "@swapkit/wallet-keystore": "5.2.1" }, "peerDependencies": { "ts-pattern": "5.9.0" } }, "sha512-A6EhydGh+CdbmmpoawhGGORK4tYdGZJ2v50FHKklmXEwZNlI51zzq9WiPP720QjUTXaHBoo3KRjOUFaSWflUew=="], - "@swapkit/tokens": ["@swapkit/tokens@5.0.8", "", { "peerDependencies": { "ts-pattern": "5.9.0" } }, "sha512-LXJC4W+nPN82t9XH2cyH3ckIoTN+QcMZ5oKXRwBDHXEOzg8AZUAyQ113rUfgy9aUFzs+L3uu57nDeR1es2hORQ=="], + "@swapkit/tokens": ["@swapkit/tokens@5.0.9", "", { "peerDependencies": { "ts-pattern": "5.9.0" } }, "sha512-0agqoyUqNmXl7tha5cwfAMrdxOo6+l1QQNLxbr483pIZ9VhmU48Nz9yC61CpOHbJm0PvOEGpT4YAM6MUk607GQ=="], - "@swapkit/toolboxes": ["@swapkit/toolboxes@5.4.1", "", { "dependencies": { "@swapkit/helpers": "5.4.1", "@swapkit/utxo-signer": "3.1.0" }, "peerDependencies": { "@aptos-labs/ts-sdk": "7.0.1", "@mysten/sui": "2.17.0", "@near-js/accounts": "2.5.1", "@near-js/crypto": "2.5.1", "@near-js/providers": "2.5.1", "@near-js/signers": "2.5.1", "@near-js/transactions": "2.5.1", "@near-js/utils": "2.5.1", "@noble/curves": "2.2.0", "@noble/hashes": "2.2.0", "@orbs-network/ton-access": "2.3.3", "@polkadot/api": "16.5.6", "@polkadot/keyring": "14.0.3", "@polkadot/rpc-provider": "16.5.6", "@polkadot/util": "14.0.3", "@polkadot/util-crypto": "14.0.3", "@provablehq/sdk": "0.11.3", "@radixdlt/babylon-gateway-api-sdk": "1.10.1", "@radixdlt/radix-dapp-toolkit": "2.3.0", "@scure/base": "2.2.0", "@scure/bip32": "2.2.0", "@scure/bip39": "2.2.0", "@solana/spl-memo": "0.2.5", "@solana/spl-token": "0.4.14", "@solana/web3.js": "1.98.4", "@stellar/stellar-sdk": "16.1.0", "@stricahq/bip32ed25519": "1.1.2", "@stricahq/cbors": "1.0.4", "@stricahq/typhonjs": "3.1.0", "@swapkit/cosmos-signer": "0.2.0", "@ton/core": "0.63.1", "@ton/crypto": "3.3.0", "@ton/ton": "16.2.4", "bignumber.js": "9.3.1", "cosmjs-types": "0.11.0", "ethers": "6.16.0", "micro-key-producer": "0.8.6", "near-seed-phrase": "0.2.1", "protobufjs": "8.0.3", "ripple-address-codec": "5.0.1", "starknet": "10.0.2", "ts-pattern": "5.9.0", "xrpl": "4.6.0" } }, "sha512-H0R920vk7FuN6ncPsPiDRqZCKOGUk0ZPEF7XkvQ6UVcH3gxMp4hoc/Ei0iRKrE8JwyBZTo3PhF8pdph5YUQaRQ=="], + "@swapkit/toolboxes": ["@swapkit/toolboxes@5.4.2", "", { "dependencies": { "@swapkit/helpers": "5.4.2", "@swapkit/utxo-signer": "3.1.0" }, "peerDependencies": { "@aptos-labs/ts-sdk": "7.0.1", "@mysten/sui": "2.17.0", "@near-js/accounts": "2.5.1", "@near-js/crypto": "2.5.1", "@near-js/providers": "2.5.1", "@near-js/signers": "2.5.1", "@near-js/transactions": "2.5.1", "@near-js/utils": "2.5.1", "@noble/curves": "2.2.0", "@noble/hashes": "2.2.0", "@orbs-network/ton-access": "2.3.3", "@polkadot/api": "16.5.6", "@polkadot/keyring": "14.0.3", "@polkadot/rpc-provider": "16.5.6", "@polkadot/util": "14.0.3", "@polkadot/util-crypto": "14.0.3", "@provablehq/sdk": "0.11.3", "@radixdlt/babylon-gateway-api-sdk": "1.10.1", "@radixdlt/radix-dapp-toolkit": "2.3.0", "@scure/base": "2.2.0", "@scure/bip32": "2.2.0", "@scure/bip39": "2.2.0", "@solana/spl-memo": "0.2.5", "@solana/spl-token": "0.4.14", "@solana/web3.js": "1.98.4", "@stellar/stellar-sdk": "16.1.0", "@stricahq/bip32ed25519": "1.1.2", "@stricahq/cbors": "1.0.4", "@stricahq/typhonjs": "3.1.0", "@swapkit/cosmos-signer": "0.2.0", "@ton/core": "0.63.1", "@ton/crypto": "3.3.0", "@ton/ton": "16.2.4", "bignumber.js": "9.3.1", "cosmjs-types": "0.11.0", "ethers": "6.16.0", "micro-key-producer": "0.8.6", "near-seed-phrase": "0.2.1", "protobufjs": "8.0.3", "ripple-address-codec": "5.0.1", "starknet": "10.0.2", "ts-pattern": "5.9.0", "xrpl": "4.6.0" } }, "sha512-RuXvKWC7TWS069DuHe19mLgiRlh8yPXFoyj0tCvqJMmTMUJfuG3ubIQa4CuqIT/6ACjT1VtjpZ0q9BCDAXXmVg=="], - "@swapkit/types": ["@swapkit/types@1.2.1", "", {}, "sha512-WC+9ckx/zKGX1OwZXoZ5T11ajt0dv6gL0DNFzkgeGmZPTQbnaFV5nh/9slnJ+EPypsTjUjYNAFQpnYnCxx7VDw=="], + "@swapkit/types": ["@swapkit/types@1.2.2", "", {}, "sha512-f2ZN0KpupeEJmTbh4C3Inhx/1aGPpTMLF2LxEpOgaAY5gRKo/jI1YonIjuKYufvt2tOaFojO70J9WEfOM3zA2A=="], "@swapkit/ui": ["@swapkit/ui@0.23.0", "", { "dependencies": { "@aptos-labs/ts-sdk": "1.34.0", "@cosmjs/amino": "0.39.0", "@cosmjs/crypto": "0.39.0", "@cosmjs/proto-signing": "0.39.0", "@cosmjs/stargate": "0.39.0", "@hookform/resolvers": "5.2.2", "@mysten/sui": "1.44.0", "@near-js/accounts": "2.5.1", "@near-js/crypto": "2.5.1", "@near-js/providers": "2.5.1", "@near-js/signers": "2.5.1", "@near-js/transactions": "2.5.1", "@near-js/utils": "2.5.1", "@noble/curves": "2.2.0", "@noble/hashes": "2.2.0", "@orbs-network/ton-access": "2.3.3", "@polkadot/api": "15.10.2", "@polkadot/keyring": "13.5.7", "@polkadot/rpc-provider": "15.10.2", "@polkadot/util": "13.5.7", "@polkadot/util-crypto": "13.5.7", "@radix-ui/react-accordion": "1.2.12", "@radix-ui/react-alert-dialog": "1.1.15", "@radix-ui/react-checkbox": "1.3.3", "@radix-ui/react-collapsible": "1.1.12", "@radix-ui/react-dialog": "1.1.15", "@radix-ui/react-dropdown-menu": "2.1.16", "@radix-ui/react-hover-card": "1.1.15", "@radix-ui/react-label": "2.1.8", "@radix-ui/react-menubar": "1.1.16", "@radix-ui/react-select": "2.2.6", "@radix-ui/react-separator": "1.1.8", "@radix-ui/react-slot": "1.2.4", "@radix-ui/react-switch": "1.2.6", "@radix-ui/react-tabs": "1.1.13", "@radix-ui/react-tooltip": "1.2.8", "@radixdlt/babylon-gateway-api-sdk": "1.10.1", "@radixdlt/radix-dapp-toolkit": "2.3.0", "@scure/base": "2.2.0", "@scure/bip32": "2.2.0", "@scure/bip39": "2.2.0", "@sentry/react": "9.27.0", "@solana/spl-memo": "0.2.5", "@solana/spl-token": "0.4.14", "@solana/web3.js": "1.98.4", "@stellar/stellar-sdk": "16.1.0", "@stricahq/bip32ed25519": "1.1.2", "@stricahq/cbors": "1.0.4", "@stricahq/typhonjs": "3.1.0", "@swapkit/core": "4.6.4", "@swapkit/helpers": "4.20.1", "@swapkit/plugins": "4.7.4", "@swapkit/toolboxes": "4.28.0", "@swapkit/wallet-keystore": "4.5.5", "@swapkit/wallets": "4.10.0", "@ton/core": "0.63.1", "@ton/crypto": "3.3.0", "@ton/ton": "16.2.4", "bignumber.js": "9.3.1", "class-variance-authority": "0.7.1", "clsx": "2.1.1", "cosmjs-types": "0.11.0", "crypto-browserify": "3.12.1", "ethers": "6.16.0", "highlight.js": "^11.11.1", "lucide-react": "0.552.0", "micro-key-producer": "0.8.6", "micro-packed": "0.9.0", "near-seed-phrase": "0.2.1", "nuqs": "2.8.9", "protobufjs": "8.0.3", "react": "19.1.1", "react-dom": "19.1.1", "react-hook-form": "7.65.0", "sonner": "2.0.7", "starknet": "10.0.2", "tailwind-merge": "2.6.0", "tailwindcss": "3.4.18", "tailwindcss-animate": "1.0.7", "ts-pattern": "5.9.0", "xrpl": "4.6.0", "zod": "4.4.3", "zustand": "5.0.13" } }, "sha512-iUbOUr62iSz8/zc1IR8NZ796rfU6QeYMvRpc8CMW/fmutTIn2qrCmHCD2cfmURdHokiUEt3iulvB5hUyJyj6Qw=="], "@swapkit/utxo-signer": ["@swapkit/utxo-signer@3.1.0", "", { "peerDependencies": { "@noble/curves": "2.2.0", "@noble/hashes": "2.2.0", "@scure/base": "2.2.0", "micro-packed": "0.9.0" } }, "sha512-+5rsOdu81ld5UoYnm+vUh0hMWJoE6O3/+WpAOobM0XADeqBGqgrWFjB75Hk422bK1nXIax63ktsRZRV89M8E2A=="], - "@swapkit/wallet-core": ["@swapkit/wallet-core@5.0.8", "", { "dependencies": { "@swapkit/helpers": "5.4.1" } }, "sha512-3HDDuj+KEH0X8PeNqdEzaX3OTUaFH4WQ8gnOIF/twWIj2vKI22QLmvAlQXZrLVLimskLu06SXC6RA20jsPK4kw=="], + "@swapkit/wallet-core": ["@swapkit/wallet-core@5.0.9", "", { "dependencies": { "@swapkit/helpers": "5.4.2" } }, "sha512-7Abm+kbsDhNejnu15dcWdHYKzCe+mCxlqEQrskNpks5PuKubK1nR2QnUjhqENyBSy8P2+mdX94qmnxwYN/Aoow=="], "@swapkit/wallet-extensions": ["@swapkit/wallet-extensions@workspace:packages/wallet-extensions"], "@swapkit/wallet-hardware": ["@swapkit/wallet-hardware@workspace:packages/wallet-hardware"], - "@swapkit/wallet-keystore": ["@swapkit/wallet-keystore@5.2.0", "", { "dependencies": { "@swapkit/helpers": "5.4.1", "@swapkit/toolboxes": "5.4.1", "@swapkit/wallet-core": "5.0.8" }, "peerDependencies": { "@noble/hashes": "2.2.0", "@scure/bip39": "2.2.0" } }, "sha512-30AcpsSXXvk7ee9Gt4zPiHfscnOaDARukfgLaR14Z4gQgVg4d5HNeSP+bjEKq5G/bKtvh0YNlXpuKkFs2gK7WQ=="], + "@swapkit/wallet-keystore": ["@swapkit/wallet-keystore@5.2.1", "", { "dependencies": { "@swapkit/helpers": "5.4.2", "@swapkit/toolboxes": "5.4.2", "@swapkit/wallet-core": "5.0.9" }, "peerDependencies": { "@noble/hashes": "2.2.0", "@scure/bip39": "2.2.0" } }, "sha512-X0ekR2NtfefGjTwFfhdljsX2k/562IYMNvr4XCqiIUAhB/yP9fKo5LFHyezcTAP7f1MhtA4oIbw/spb67NRoPQ=="], "@swapkit/wallet-mobile": ["@swapkit/wallet-mobile@workspace:packages/wallet-mobile"], @@ -2532,13 +2532,15 @@ "@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/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/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/socket/isomorphic-ws": ["isomorphic-ws@4.0.1", "", { "peerDependencies": { "ws": "*" } }, "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w=="], "@cosmjs/socket/ws": ["ws@7.5.13", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA=="], - "@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/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/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=="], @@ -2690,6 +2692,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=="], @@ -2920,11 +2926,19 @@ "@swapkit/ui/react-dom": ["react-dom@19.1.1", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.1" } }, "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw=="], - "@swapkit/wallet-extensions/@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-extensions/@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-extensions/@cosmjs/stargate": ["@cosmjs/stargate@0.37.1", "", { "dependencies": { "@cosmjs/amino": "^0.37.1", "@cosmjs/encoding": "^0.37.1", "@cosmjs/math": "^0.37.1", "@cosmjs/proto-signing": "^0.37.1", "@cosmjs/stream": "^0.37.1", "@cosmjs/tendermint-rpc": "^0.37.1", "@cosmjs/utils": "^0.37.1", "cosmjs-types": "^0.10.1" } }, "sha512-nQgaJB7A81cRYtmDrcIySq8hbd+QLXJTGFEs6R1xMdxascaABX0x86nS44ALk+EZojQQRnwupOsw1dKLlQmIFg=="], - "@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=="], + "@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/@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/@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=="], "@swc/helpers/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], @@ -3222,9 +3236,7 @@ "@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=="], - "@cosmjs/stargate/@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/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/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/tendermint-rpc/@cosmjs/crypto/@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], @@ -3324,6 +3336,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=="], @@ -3434,15 +3456,23 @@ "@swapkit/ui/react-dom/scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="], - "@swapkit/wallet-extensions/@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=="], + "@swapkit/wallet-extensions/@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-extensions/@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-extensions/@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=="], - "@swapkit/wallet-extensions/@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=="], + "@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/wallets/@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=="], + "@swapkit/wallet-hardware/@near-js/crypto/@noble/curves": ["@noble/curves@1.8.1", "", { "dependencies": { "@noble/hashes": "1.7.1" } }, "sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ=="], - "@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=="], + "@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/@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/wallets/@near-js/transactions/@noble/hashes": ["@noble/hashes@1.7.1", "", {}, "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ=="], "@trezor/blockchain-link-utils/@stellar/stellar-sdk/eventsource": ["eventsource@2.0.2", "", {}, "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA=="], @@ -3566,11 +3596,11 @@ "@cosmjs/proto-signing/@cosmjs/crypto/@scure/bip39/@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="], - "@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/amino/@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=="], + "@cosmjs/stargate/@cosmjs/amino/@cosmjs/crypto/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], - "@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=="], + "@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/tendermint-rpc/@cosmjs/crypto/@scure/bip39/@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="], @@ -3610,6 +3640,8 @@ "@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=="], @@ -3630,19 +3662,31 @@ "@swapkit/ui/@near-js/crypto/@noble/curves/@noble/hashes": ["@noble/hashes@1.7.1", "", {}, "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ=="], - "@swapkit/wallet-extensions/@cosmjs/proto-signing/@cosmjs/crypto/@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], + "@swapkit/wallet-extensions/@cosmjs/amino/@cosmjs/crypto/@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], + + "@swapkit/wallet-extensions/@cosmjs/amino/@cosmjs/crypto/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], + + "@swapkit/wallet-extensions/@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-extensions/@cosmjs/proto-signing/@cosmjs/crypto/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], + "@swapkit/wallet-extensions/@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=="], - "@swapkit/wallet-extensions/@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/@cosmjs/amino/@cosmjs/crypto/@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], - "@swapkit/wallet-extensions/@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=="], + "@swapkit/wallet-hardware/@cosmjs/amino/@cosmjs/crypto/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], - "@swapkit/wallets/@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/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/wallets/@cosmjs/proto-signing/@cosmjs/crypto/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], + "@swapkit/wallet-hardware/@near-js/crypto/@noble/curves/@noble/hashes": ["@noble/hashes@1.7.1", "", {}, "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ=="], - "@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/@cosmjs/amino/@cosmjs/crypto/@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], + + "@swapkit/wallets/@cosmjs/amino/@cosmjs/crypto/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], + + "@swapkit/wallets/@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/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/@trezor/protobuf/protobufjs/long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], @@ -3682,7 +3726,7 @@ "ripemd160/hash-base/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], - "@cosmjs/stargate/@cosmjs/proto-signing/@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=="], "@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=="], @@ -3694,18 +3738,22 @@ "@swapkit/ui/@cosmjs/stargate/@cosmjs/tendermint-rpc/@cosmjs/socket/ws": ["ws@7.5.13", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA=="], - "@swapkit/wallet-extensions/@cosmjs/proto-signing/@cosmjs/crypto/@scure/bip39/@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="], + "@swapkit/wallet-extensions/@cosmjs/amino/@cosmjs/crypto/@scure/bip39/@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="], + + "@swapkit/wallet-extensions/@cosmjs/stargate/@cosmjs/proto-signing/@cosmjs/crypto/@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], + + "@swapkit/wallet-extensions/@cosmjs/stargate/@cosmjs/proto-signing/@cosmjs/crypto/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], - "@swapkit/wallet-extensions/@cosmjs/stargate/@cosmjs/amino/@cosmjs/crypto/@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], + "@swapkit/wallet-extensions/@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=="], - "@swapkit/wallet-extensions/@cosmjs/stargate/@cosmjs/amino/@cosmjs/crypto/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], + "@swapkit/wallet-hardware/@cosmjs/amino/@cosmjs/crypto/@scure/bip39/@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="], - "@swapkit/wallet-extensions/@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=="], + "@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=="], "pkg-dir/find-up/locate-path/p-locate/p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], - "@swapkit/wallet-extensions/@cosmjs/stargate/@cosmjs/amino/@cosmjs/crypto/@scure/bip39/@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="], + "@swapkit/wallet-extensions/@cosmjs/stargate/@cosmjs/proto-signing/@cosmjs/crypto/@scure/bip39/@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="], } } diff --git a/package.json b/package.json index 428d5ed..2c5dd8f 100644 --- a/package.json +++ b/package.json @@ -8,13 +8,13 @@ "@cosmjs/proto-signing": "0.37.0", "@cosmjs/stargate": "0.37.0", "@scure/bip32": "2.2.0", - "@swapkit/core": "^5.0.8", - "@swapkit/helpers": "^5.4.1", - "@swapkit/plugins": "^5.0.8", - "@swapkit/server": "^5.2.1", - "@swapkit/toolboxes": "^5.4.1", - "@swapkit/wallet-core": "^5.0.8", - "@swapkit/wallet-keystore": "^5.2.0", + "@swapkit/core": "^5.0.9", + "@swapkit/helpers": "^5.4.2", + "@swapkit/plugins": "^5.0.9", + "@swapkit/server": "^5.2.2", + "@swapkit/toolboxes": "^5.4.2", + "@swapkit/wallet-core": "^5.0.9", + "@swapkit/wallet-keystore": "^5.2.1", "@types/bun": "1.3.13", "@types/node": "25.6.0", "typescript": "6.0.3", diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 5f706e6..adcbe9a 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -2,13 +2,13 @@ "author": "swapkit-dev", "dependencies": { "@stricahq/typhonjs": "~3.0.1", - "@swapkit/core": "^5.0.8", - "@swapkit/helpers": "^5.4.1", - "@swapkit/plugins": "^5.0.8", - "@swapkit/server": "^5.2.1", - "@swapkit/toolboxes": "^5.4.1", - "@swapkit/wallet-core": "^5.0.8", - "@swapkit/wallet-keystore": "^5.2.0", + "@swapkit/core": "^5.0.9", + "@swapkit/helpers": "^5.4.2", + "@swapkit/plugins": "^5.0.9", + "@swapkit/server": "^5.2.2", + "@swapkit/toolboxes": "^5.4.2", + "@swapkit/wallet-core": "^5.0.9", + "@swapkit/wallet-keystore": "^5.2.1", "@swapkit/wallets": "workspace:*", "cosmjs-types": "0.10.1" }, diff --git a/packages/wallet-extensions/package.json b/packages/wallet-extensions/package.json index 9936f6d..7690920 100644 --- a/packages/wallet-extensions/package.json +++ b/packages/wallet-extensions/package.json @@ -11,10 +11,10 @@ "@near-js/transactions": "~2.5.0", "@scure/base": "~2.2.0", "@solana/web3.js": "~1.98.4", - "@swapkit/helpers": "^5.4.1", - "@swapkit/toolboxes": "^5.4.1", + "@swapkit/helpers": "^5.4.2", + "@swapkit/toolboxes": "^5.4.2", "@swapkit/utxo-signer": "^3.1.0", - "@swapkit/wallet-core": "^5.0.8", + "@swapkit/wallet-core": "^5.0.9", "@wallet-standard/app": "^1.1.1", "cosmjs-types": "0.10.1", "ethers": "^6.14.0", diff --git a/packages/wallet-hardware/package.json b/packages/wallet-hardware/package.json index 1bbc5d5..cdd5fea 100644 --- a/packages/wallet-hardware/package.json +++ b/packages/wallet-hardware/package.json @@ -22,10 +22,10 @@ "@near-js/transactions": "~2.5.0", "@scure/base": "2.2.0", "@scure/bip32": "2.2.0", - "@swapkit/helpers": "^5.4.1", - "@swapkit/toolboxes": "^5.4.1", + "@swapkit/helpers": "^5.4.2", + "@swapkit/toolboxes": "^5.4.2", "@swapkit/utxo-signer": "^3.1.0", - "@swapkit/wallet-core": "^5.0.8", + "@swapkit/wallet-core": "^5.0.9", "@trezor/connect-web": "~9.7.3", "bitcoinjs-lib": "~6.1.7", "ethers": "^6.14.0", diff --git a/packages/wallet-mobile/package.json b/packages/wallet-mobile/package.json index 6284ea6..b5ac248 100644 --- a/packages/wallet-mobile/package.json +++ b/packages/wallet-mobile/package.json @@ -1,9 +1,9 @@ { "author": "swapkit", "dependencies": { - "@swapkit/helpers": "^5.4.1", - "@swapkit/toolboxes": "^5.4.1", - "@swapkit/wallet-core": "^5.0.8" + "@swapkit/helpers": "^5.4.2", + "@swapkit/toolboxes": "^5.4.2", + "@swapkit/wallet-core": "^5.0.9" }, "description": "SwapKit - Wallet Mobile", "exports": { diff --git a/packages/wallets/package.json b/packages/wallets/package.json index 541fd6b..d9f76d5 100644 --- a/packages/wallets/package.json +++ b/packages/wallets/package.json @@ -16,10 +16,10 @@ "@scure/base": "~2.2.0", "@scure/bip39": "~2.2.0", "@solana/web3.js": "~1.98.4", - "@swapkit/helpers": "^5.4.1", - "@swapkit/toolboxes": "^5.4.1", + "@swapkit/helpers": "^5.4.2", + "@swapkit/toolboxes": "^5.4.2", "@swapkit/utxo-signer": "^3.1.0", - "@swapkit/wallet-core": "^5.0.8", + "@swapkit/wallet-core": "^5.0.9", "@swapkit/wallet-extensions": "workspace:*", "@swapkit/wallet-hardware": "workspace:*", "@ton/core": "~0.63.1", From 6067490d8771c56756499d1740c3d53706b925ea Mon Sep 17 00:00:00 2001 From: Ice Date: Wed, 23 Sep 2026 13:32:55 +0200 Subject: [PATCH 13/27] chore(wallet-hardware): update Ledger dependencies to latest Update DMK 1.9.1, context-module 2.6.0, Ethereum signer kit 1.18.1, Zcash signer kit 0.7.1 and the LedgerJS app clients to their latest releases. hw-app-btc stays on 10.x: 11.0 switches to Bitcoin app protocol v1, which the legacy LTC/BCH/DOGE/DASH apps behind the adapter have not been verified on. hw-transport follows the 6.35.7 pin of the app clients to avoid duplicates. --- bun.lock | 146 ++++++++++++++++---------- packages/wallet-hardware/package.json | 44 ++++---- 2 files changed, 114 insertions(+), 76 deletions(-) diff --git a/bun.lock b/bun.lock index cb622d2..55e5d68 100644 --- a/bun.lock +++ b/bun.lock @@ -93,20 +93,20 @@ "@cosmjs/amino": "~0.37.0", "@cosmjs/crypto": "0.37.0", "@keepkey/keepkey-sdk": "~0.2.62", - "@ledgerhq/context-module": "~2.4.0", - "@ledgerhq/device-management-kit": "~1.8.0", + "@ledgerhq/context-module": "~2.6.0", + "@ledgerhq/device-management-kit": "~1.9.1", "@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-signer-kit-ethereum": "~1.18.1", + "@ledgerhq/device-signer-kit-zcash": "~0.7.1", "@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-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-app-btc": "~10.22.1", + "@ledgerhq/hw-app-cosmos": "~6.36.7", + "@ledgerhq/hw-app-near": "~6.35.7", + "@ledgerhq/hw-app-sui": "~1.11.4", + "@ledgerhq/hw-app-trx": "~6.36.6", + "@ledgerhq/hw-app-xrp": "~6.37.7", + "@ledgerhq/hw-transport": "~6.35.7", "@near-js/crypto": "~2.5.0", "@near-js/transactions": "~2.5.0", "@scure/base": "2.2.0", @@ -127,20 +127,20 @@ "@cosmjs/amino": "0.37.0", "@cosmjs/crypto": "0.37.0", "@keepkey/keepkey-sdk": "0.2.62", - "@ledgerhq/context-module": "2.4.0", - "@ledgerhq/device-management-kit": "1.8.0", + "@ledgerhq/context-module": "2.6.0", + "@ledgerhq/device-management-kit": "1.9.1", "@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-signer-kit-ethereum": "1.18.1", + "@ledgerhq/device-signer-kit-zcash": "0.7.1", "@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-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-app-btc": "10.22.1", + "@ledgerhq/hw-app-cosmos": "6.36.7", + "@ledgerhq/hw-app-near": "6.35.7", + "@ledgerhq/hw-app-sui": "1.11.4", + "@ledgerhq/hw-app-trx": "6.36.6", + "@ledgerhq/hw-app-xrp": "6.37.7", + "@ledgerhq/hw-transport": "6.35.7", "@near-js/crypto": "2.5.0", "@near-js/transactions": "2.5.0", "@scure/base": "2.2.0", @@ -530,41 +530,53 @@ "@keplr-wallet/types": ["@keplr-wallet/types@0.13.24", "", { "dependencies": { "long": "^4.0.0" }, "peerDependencies": { "starknet": "^8" } }, "sha512-r5wXlvmVYqPS1PwRad0tQRhdRKJj3CpWcnYa78OzmyCHOJA17xHnkNW+TQJwiw3YGIq3r3laH3nYYEkhf/fFaQ=="], - "@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/context-module": ["@ledgerhq/context-module@2.6.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.9.1" } }, "sha512-jbzShfYygVEGxYiTdbOEjpSFeyOzLxJGxkey7GneUwbGi2QXvgCQGmkHc/hugNFX13jTqgP1HSvjQIfSN7QYDw=="], - "@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/device-contacts-kit": ["@ledgerhq/device-contacts-kit@0.5.0", "", { "dependencies": { "@ledgerhq/signer-utils": "^1.3.0", "inversify": "7.5.1", "purify-ts": "2.1.0", "reflect-metadata": "0.2.2", "semver": "7.7.2", "xstate": "5.19.2" }, "peerDependencies": { "@ledgerhq/device-management-kit": "^1.9.1" } }, "sha512-i8K0NSKalvIfg1mwrrOFEK00a5tPPFOqQBIHuSeTUUXWGH8VdnNulmTmAjNsw64Q+vskUs0+cd9DswAbtHHv9A=="], + + "@ledgerhq/device-management-kit": ["@ledgerhq/device-management-kit@1.9.1", "", { "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-sfPGBJQTEtmUYjdtMAOd7tuuvK5q9OuS0PsDqJcoSOYRGYyk8d8yIocFYgF/JCtvk1+0+NAqgb5V9eM0Zd9apw=="], "@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/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-ethereum": ["@ledgerhq/device-signer-kit-ethereum@1.18.1", "", { "dependencies": { "@ledgerhq/device-contacts-kit": "^0.5.0", "@ledgerhq/signer-utils": "^1.3.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.6.0", "@ledgerhq/device-management-kit": "^1.9.1" } }, "sha512-L/7ACvdQ37W5BgbfPgK0D+VTTnTIDMHNecBDdGJxzKJXli6mnsGZFTMPQuHkR/43qtxboNxSXkgqezUUMBy3nw=="], - "@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-signer-kit-zcash": ["@ledgerhq/device-signer-kit-zcash@0.7.1", "", { "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.9.0" } }, "sha512-WTim/MTeghnaShDr8IxKu+ZOc9NDhS5hJmHuN5np7McEfNuhrpnkVEVlGsjLn0+4D7ZZ8u+5Ry7C7Nur2oCkYA=="], "@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/devices": ["@ledgerhq/devices@8.17.0", "", { "dependencies": { "semver": "7.7.3" } }, "sha512-l+rrVQEjR1hSWOLD00LFX4zbS8yB1M/Mb6UYPmSHGO7TmE1CFbZ4CmDJC/kZSl3hdrXCJ+YUNpvaLCcSWDwA+Q=="], + + "@ledgerhq/errors": ["@ledgerhq/errors@7.0.0", "", {}, "sha512-+Q/vykUlNeIxiM+I3cu1B660WLkzlmIsHLTV9QNV5D2/Ocplx3QMg52NYq2X7OAfGQnfH1rQvhn/NrjT+t9wBA=="], + + "@ledgerhq/hw-app-btc": ["@ledgerhq/hw-app-btc@10.22.1", "", { "dependencies": { "@ledgerhq/hw-transport": "6.35.2", "@ledgerhq/logs": "^6.17.0", "@ledgerhq/psbtv2": "^0.5.0", "@noble/curves": "1.9.7", "@noble/hashes": "1.8.0", "bip32-path": "^0.4.2", "bitcoinjs-lib": "^6.1.7", "bs58": "^4.0.1", "bs58check": "^2.1.2", "invariant": "^2.2.4", "semver": "7.7.3" } }, "sha512-DH+E4+Nq7O9zDGhMjRStRQjk2UH4gBM2Pn9vR0OXnajmHpylCVwKRPW/cDLZ4HcNISKUMHq2yNrmRQq7JHHXDg=="], + + "@ledgerhq/hw-app-cosmos": ["@ledgerhq/hw-app-cosmos@6.36.7", "", { "dependencies": { "@ledgerhq/errors": "^7.0.0", "@ledgerhq/hw-transport": "6.35.7", "bip32-path": "^0.4.2" } }, "sha512-EiBTybtqILqvGBcB8j695KycRZhYPTFGK+Oy5/Xlcld9zbuWUidHxwadmYAQm8tuRqDneQd4Qi+7n/sIZzrKng=="], - "@ledgerhq/errors": ["@ledgerhq/errors@6.29.0", "", {}, "sha512-mmDsGN662zd0XGKyjzSKkg+5o1/l9pvV1HkVHtbzaydvHAtRypghmVoWMY9XAQDLXiUBXGIsLal84NgmGeuKWA=="], + "@ledgerhq/hw-app-near": ["@ledgerhq/hw-app-near@6.35.7", "", { "dependencies": { "@ledgerhq/hw-transport": "6.35.7", "near-api-js": "^3.0.2" } }, "sha512-CyY0QiG6SecuB4S+Kot6myO6gW57g+Lw1OWz+9Gl2ElxmZ04pnuiiuMeJ8g7yZ/fBILRhwkvL/v/RRkHAjuXBQ=="], - "@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-sui": ["@ledgerhq/hw-app-sui@1.11.4", "", { "dependencies": { "@ledgerhq/devices": "8.17.0", "@ledgerhq/errors": "^6.37.0", "@mysten/ledgerjs-hw-app-sui": "^0.8.0", "semver": "7.7.3" } }, "sha512-DsuCjonwOXwsNYvnOcZcw6SwiFTytwnCrXn4dt77CBONNg+5gnsU7d7PO06qJXmKEWJKnpl8p0gsXgg3Ep9zwQ=="], - "@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-trx": ["@ledgerhq/hw-app-trx@6.36.6", "", { "dependencies": { "@ledgerhq/hw-transport": "6.35.7" } }, "sha512-rlVlV7cGOuYAPwLWhJvueQFw5vuAsHpZTFziouY9oChIFCNwovAqrdIhW8r0A7wiICK88xVncVuG9i958qqZNA=="], - "@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-xrp": ["@ledgerhq/hw-app-xrp@6.37.7", "", { "dependencies": { "@ledgerhq/hw-transport": "6.35.7", "bip32-path": "0.4.2" } }, "sha512-haSWAUQgsqcn5kz0QQ6QO4rf6P6g+TdS2d5Tvj9nQFWEDvOplesd+MdBjnt5RZTJCB+vRBUoQkRG9ZYMGGGAdQ=="], - "@ledgerhq/hw-app-sui": ["@ledgerhq/hw-app-sui@1.5.0", "", { "dependencies": { "@mysten/ledgerjs-hw-app-sui": "0.6.0" } }, "sha512-LpLl4zPFZhnHrZ6kNC2SrhhdnC5F/cIPeXrhIKHTiRuj1SBGhfjTeNkN8siff9aUAbMdgaWlfv5fz/rWw8tkrw=="], + "@ledgerhq/hw-bolos": ["@ledgerhq/hw-bolos@6.36.7", "", { "dependencies": { "@ledgerhq/hw-transport": "6.35.7" } }, "sha512-ixtFCDYW59bT5KTR6EjxLMvzRKtVApkBoVZGwdXX4dJEQCwesGxVCfgUZdq9I8MwzkpSdawbB/smXvMDO3KQ5w=="], - "@ledgerhq/hw-app-trx": ["@ledgerhq/hw-app-trx@6.32.0", "", { "dependencies": { "@ledgerhq/hw-transport": "6.32.0" } }, "sha512-UKvGH24JK82pceDJA5PgrIUO5zxb50VvqcYpURoxElsi/L0ysIM2AS+CQ8iTMyicLW0tjkOGuanjVQWW2Rxeyw=="], + "@ledgerhq/hw-transport": ["@ledgerhq/hw-transport@6.35.7", "", { "dependencies": { "@ledgerhq/devices": "8.17.0", "@ledgerhq/errors": "^7.0.0", "@ledgerhq/logs": "^6.17.0", "events": "^3.3.0" } }, "sha512-vVhAVQ56+7A5FY5Mr09HY+bmf3H6TXpwsj+/xbadNkjl//e/YzTWfpXk4eBnj3hwk1PQ9Mn6pKFH0BHjS2BKlg=="], - "@ledgerhq/hw-app-xrp": ["@ledgerhq/hw-app-xrp@6.33.0", "", { "dependencies": { "@ledgerhq/hw-transport": "6.32.0", "bip32-path": "0.4.2" } }, "sha512-NMOC6BTR6Kma1Oj25tiOx654dta3mKf+G5JW9463/gCqKwvf7DVk4fRp+VtK+MYMMviUTjbjfEl6Ul12LrrcxQ=="], + "@ledgerhq/ledger-cal-service": ["@ledgerhq/ledger-cal-service@1.18.2", "", { "dependencies": { "@ledgerhq/live-env": "2.40.0", "@ledgerhq/live-network": "2.6.6" } }, "sha512-EtX6YmNutqhitMyES7iwXGi8jY79/HAZQYVwxYiPSsZW0F5ksM5/ZSxgTvP/c9hXfdVF5+gRcHjKT9HciFWnzw=="], - "@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/live-env": ["@ledgerhq/live-env@2.40.0", "", { "dependencies": { "rxjs": "7.8.2", "utility-types": "^3.10.0" } }, "sha512-TwiFiw+eLk/ONCp1v89j8fddzgohLqVlnclbLjDvkrkpZ6h2tigGstuzd+pgNfHi5ixEapg3WJF4naZgu40TSg=="], + + "@ledgerhq/live-network": ["@ledgerhq/live-network@2.6.6", "", { "dependencies": { "@ledgerhq/errors": "^6.37.0", "@ledgerhq/live-env": "^2.40.0", "@ledgerhq/live-promise": "^0.2.3", "@ledgerhq/logs": "^6.17.0", "axios": "1.13.5", "lru-cache": "^7.14.1" } }, "sha512-vPyrsSh0kwU6NMoXdFRryy9KQOG9kFDBK0OqHleydnyJq+Ok6cOiBwVwum/dDIo+pGjTSLN5bzo30180+lA0yg=="], + + "@ledgerhq/live-promise": ["@ledgerhq/live-promise@0.2.3", "", { "dependencies": { "@ledgerhq/logs": "^6.17.0" } }, "sha512-f673XOCKL/foDCbsqx2AWYfI1ehc5rGKLh3gObfTQSu3+MDJzhdd/pvyuVLJ0e6iUzUQlwLW27qEyvA7sNKvVQ=="], "@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/psbtv2": ["@ledgerhq/psbtv2@0.5.0", "", { "dependencies": { "bitcoinjs-lib": "^6.1.7", "varuint-bitcoin": "1.1.2" } }, "sha512-hKp6iIIvFX1KUZA6Uf7rvjBIjEqHBdqSsFzWGLNc9+F0qy+C5C0xLlPbw/lKR+x8Xuau1dhvmw52BvPMQQz41A=="], "@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=="], @@ -618,7 +630,7 @@ "@mysten/bcs": ["@mysten/bcs@1.9.2", "", { "dependencies": { "@mysten/utils": "0.2.0", "@scure/base": "^1.2.6" } }, "sha512-kBk5xrxV9OWR7i+JhL/plQrgQ2/KJhB2pB5gj+w6GXhbMQwS3DPpOvi/zN0Tj84jwPvHMllpEl0QHj6ywN7/eQ=="], - "@mysten/ledgerjs-hw-app-sui": ["@mysten/ledgerjs-hw-app-sui@0.6.0", "", { "dependencies": { "@ledgerhq/hw-transport": "^6.31.10", "fast-sha256": "^1.3.0" } }, "sha512-UopsSSI1gphfoh59feiPPhZUEybYIvYEcvmudGR2RAIN3IDJHkaA3Uu0HPH8BZQbQXhkgvFW4V2KNWIWj2IE2g=="], + "@mysten/ledgerjs-hw-app-sui": ["@mysten/ledgerjs-hw-app-sui@0.8.3", "", { "dependencies": { "@ledgerhq/devices": "^8.9.0", "@ledgerhq/errors": "^6.28.0", "@ledgerhq/hw-bolos": "^6.32.12", "@ledgerhq/hw-transport": "^6.31.16", "@ledgerhq/ledger-cal-service": "^1.10.0", "fast-sha256": "^1.3.0", "semver": ">=7.5.2" } }, "sha512-1WEkRWv0EjhtMKg/nxQNqXNdnLXbeclDXwiZkyPfGR70edygtVT0m2offmVL0TKOZsg+bNWsHWUYZpXt2/Vpuw=="], "@mysten/sui": ["@mysten/sui@1.44.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.2.0", "@mysten/bcs": "1.9.2", "@mysten/utils": "0.2.0", "@noble/curves": "=1.9.4", "@noble/hashes": "^1.8.0", "@protobuf-ts/grpcweb-transport": "^2.11.1", "@protobuf-ts/runtime": "^2.11.1", "@protobuf-ts/runtime-rpc": "^2.11.1", "@scure/base": "^1.2.6", "@scure/bip32": "^1.7.0", "@scure/bip39": "^1.6.0", "gql.tada": "^1.8.13", "graphql": "^16.11.0", "poseidon-lite": "0.2.1", "valibot": "^0.36.0" } }, "sha512-3p/y5gUhsG/MBXLOdX6qw1b8r1z/lKMvdfpacH+CjuEx8Y0U98R9fLmnFFd+6K+d+CE1gctBuMtcOqr+SppBug=="], @@ -1372,15 +1384,13 @@ "bip174": ["bip174@2.1.1", "", {}, "sha512-mdFV5+/v0XyNYXjBS6CQPLo9ekCx4gtKZFnJm5PMto7Fs9hTTDpkkzOB7/FtluRI6JbUUAu+snTYfJRgHLZbZQ=="], - "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=="], "bip39": ["bip39@3.0.2", "", { "dependencies": { "@types/node": "11.11.6", "create-hash": "^1.1.0", "pbkdf2": "^3.0.9", "randombytes": "^2.0.1" } }, "sha512-J4E1r2N0tUylTKt07ibXvhpT2c5pyAFgvuA5q1H9uDy6dEGpjV8jmymh3MTYJDLCNbIVClSB9FbND49I6N24MQ=="], "bip39-light": ["bip39-light@1.0.7", "", { "dependencies": { "create-hash": "^1.1.0", "pbkdf2": "^3.0.9" } }, "sha512-WDTmLRQUsiioBdTs9BmSEmkJza+8xfJmptsNJjxnoq3EydSa/ZBXT6rm66KoT3PJIRYMnhSKNR7S9YL1l7R40Q=="], - "bip66": ["bip66@1.1.5", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nemMHz95EmS38a26XbbdxIYj5csHd3RMP3H5bwQknX0WYHF01qhpufP42mLOwVICuH2JmhIhXiWs89MfUGL7Xw=="], + "bip66": ["bip66@2.0.0", "", {}, "sha512-kBG+hSpgvZBrkIm9dt5T1Hd/7xGCPEX2npoxAWZfsK1FvjgaxySEh2WizjyIstWXriKo9K9uJ4u0OnsyLDUPXQ=="], "bitcoin-ops": ["bitcoin-ops@1.4.1", "", {}, "sha512-pef6gxZFztEhaE9RY9HmWVmiIHqCb2OyS4HPKkpc6CIiiOa3Qmuoylxc5P2EkU3w+5eTSifI9SEZC88idAIGow=="], @@ -1930,8 +1940,6 @@ "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], - "merkle-lib": ["merkle-lib@2.0.10", "", {}, "sha512-XrNQvUbn1DL5hKNe46Ccs+Tu3/PYOlrcZILuGUhb95oKBPjc/nmIC8D462PQkipVDGKRvwhn+QFg2cCdIvmDJA=="], - "micro-ftch": ["micro-ftch@0.3.1", "", {}, "sha512-/0LLxhzP0tfiR5hcQebtudP56gUurs2CLkGarnCiB/OqEyUFQ6U3paQi/tgLv0hBJYt2rnr9MNpxz4fiiugstg=="], "micro-key-producer": ["micro-key-producer@0.8.6", "", { "dependencies": { "@noble/ciphers": "^2.2.0", "@noble/curves": "^2.2.0", "@noble/hashes": "^2.2.0", "@scure/base": "^2.2.0", "micro-packed": "^0.8.0" }, "bin": { "gpgkp": "bin/gpgkp.js" } }, "sha512-jlAjz5sZevIwLXHbcE9TmrPRjeNa+3cnXYeVlu97lLuq2yXmrrM46Ei8ZSMELHVMuziaQDne7wkVW8SYyPG94g=="], @@ -2430,6 +2438,8 @@ "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + "utility-types": ["utility-types@3.11.0", "", {}, "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw=="], + "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=="], @@ -2462,7 +2472,7 @@ "which-typed-array": ["which-typed-array@1.1.22", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.9", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw=="], - "wif": ["wif@2.0.6", "", { "dependencies": { "bs58check": "<3.0.0" } }, "sha512-HIanZn1zmduSF+BQhkE+YXIbEiH0xPr1012QbFEGB0xsKqJii0/SqJjyn8dFv6y36kOznMgMB+LGcbZTJ1xACQ=="], + "wif": ["wif@5.0.0", "", { "dependencies": { "bs58check": "^4.0.0" } }, "sha512-iFzrC/9ne740qFbNjTZ2FciSRJlHIXoxqk/Y5EnE08QOXu1WjJyCCswwDTYbohAOEnlCtLaAAQBhyaLRFh2hMA=="], "wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], @@ -2548,6 +2558,8 @@ "@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-contacts-kit/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + "@ledgerhq/device-management-kit/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], "@ledgerhq/device-management-kit/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], @@ -2562,17 +2574,31 @@ "@ledgerhq/devices/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "@ledgerhq/hw-app-btc/@ledgerhq/hw-transport": ["@ledgerhq/hw-transport@6.35.2", "", { "dependencies": { "@ledgerhq/devices": "8.14.2", "@ledgerhq/errors": "^6.34.1", "@ledgerhq/logs": "^6.17.0", "events": "^3.3.0" } }, "sha512-eSXFFqMDAB2Ra/5uqmlru0cUyc2XDQPEKf4ITWHeMms2fHhETw9lcgAEl61vszkiz0RLUVB4VQsLJjj7O8kCvg=="], + "@ledgerhq/hw-app-btc/@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], "@ledgerhq/hw-app-btc/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], - "@ledgerhq/hw-app-btc/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/hw-app-btc/bs58": ["bs58@4.0.1", "", { "dependencies": { "base-x": "^3.0.2" } }, "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw=="], "@ledgerhq/hw-app-btc/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], - "@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/hw-app-sui/@ledgerhq/errors": ["@ledgerhq/errors@6.37.0", "", {}, "sha512-T5yiKI5UX7ugeocdTF3TUsCIN2BH41Bio4ZeN410YFjFOf3es08n/5JyMzzKwzRgP0blG3HfBf7s7vJKqCSAeg=="], + + "@ledgerhq/hw-app-sui/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + + "@ledgerhq/hw-transport/@ledgerhq/logs": ["@ledgerhq/logs@6.19.0", "", {}, "sha512-kmWj18C7eTtuytdaWygzVrQm9EoD4YWuYnd/kY8stVBBAzRdJzrGlm013ijQF6I/kGLKlGY7JKmll6j32KGpfQ=="], + + "@ledgerhq/live-network/@ledgerhq/errors": ["@ledgerhq/errors@6.37.0", "", {}, "sha512-T5yiKI5UX7ugeocdTF3TUsCIN2BH41Bio4ZeN410YFjFOf3es08n/5JyMzzKwzRgP0blG3HfBf7s7vJKqCSAeg=="], + + "@ledgerhq/live-network/@ledgerhq/logs": ["@ledgerhq/logs@6.19.0", "", {}, "sha512-kmWj18C7eTtuytdaWygzVrQm9EoD4YWuYnd/kY8stVBBAzRdJzrGlm013ijQF6I/kGLKlGY7JKmll6j32KGpfQ=="], + + "@ledgerhq/live-network/axios": ["axios@1.13.5", "", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q=="], + + "@ledgerhq/live-network/lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="], + + "@ledgerhq/live-promise/@ledgerhq/logs": ["@ledgerhq/logs@6.19.0", "", {}, "sha512-kmWj18C7eTtuytdaWygzVrQm9EoD4YWuYnd/kY8stVBBAzRdJzrGlm013ijQF6I/kGLKlGY7JKmll6j32KGpfQ=="], "@ledgerhq/signer-utils/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], @@ -2618,6 +2644,10 @@ "@mysten/bcs/@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="], + "@mysten/ledgerjs-hw-app-sui/@ledgerhq/devices": ["@ledgerhq/devices@8.18.0", "", { "dependencies": { "semver": "7.7.3" } }, "sha512-90qS23wtqdviis+8pDWRbgyp4jE4YLK24CyAmH79rLySaPbgEJX5mZzbEKlDQ6cXvSA/51Z2/PtzmlAi26rzug=="], + + "@mysten/ledgerjs-hw-app-sui/@ledgerhq/errors": ["@ledgerhq/errors@6.37.0", "", {}, "sha512-T5yiKI5UX7ugeocdTF3TUsCIN2BH41Bio4ZeN410YFjFOf3es08n/5JyMzzKwzRgP0blG3HfBf7s7vJKqCSAeg=="], + "@mysten/sui/@noble/curves": ["@noble/curves@1.9.4", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-2bKONnuM53lINoDrSmK8qP8W271ms7pygDhZt4SiLOoLwBtoHqeCFi6RG42V8zd3mLHuJFhU/Bmaqo4nX0/kBw=="], "@mysten/sui/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], @@ -2978,16 +3008,12 @@ "@trezor/protobuf/protobufjs": ["protobufjs@7.5.5", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-3wY1AxV+VBNW8Yypfd1yQY9pXnqTAN+KwQxL8iYm3/BjKYMNg4i0owhEe26PWDOMaIrzeeF98Lqd5NGz4omiIg=="], - "@trezor/utxo-lib/bip66": ["bip66@2.0.0", "", {}, "sha512-kBG+hSpgvZBrkIm9dt5T1Hd/7xGCPEX2npoxAWZfsK1FvjgaxySEh2WizjyIstWXriKo9K9uJ4u0OnsyLDUPXQ=="], - "@trezor/utxo-lib/bs58": ["bs58@6.0.0", "", { "dependencies": { "base-x": "^5.0.0" } }, "sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw=="], "@trezor/utxo-lib/bs58check": ["bs58check@4.0.0", "", { "dependencies": { "@noble/hashes": "^1.2.0", "bs58": "^6.0.0" } }, "sha512-FsGDOnFg9aVI9erdriULkd/JjEWONV/lQE5aYziB5PoBsXRind56lh8doIZIc9X4HoxT5x4bLjMWN1/NB8Zp5g=="], "@trezor/utxo-lib/varuint-bitcoin": ["varuint-bitcoin@2.0.0", "", { "dependencies": { "uint8array-tools": "^0.0.8" } }, "sha512-6QZbU/rHO2ZQYpWFDALCDSRsXbAs1VOEmXAxtbtjLtKuMJ/FQ8YbhfxlaiKv5nklci0M6lZtlZyxo9Q+qNnyog=="], - "@trezor/utxo-lib/wif": ["wif@5.0.0", "", { "dependencies": { "bs58check": "^4.0.0" } }, "sha512-iFzrC/9ne740qFbNjTZ2FciSRJlHIXoxqk/Y5EnE08QOXu1WjJyCCswwDTYbohAOEnlCtLaAAQBhyaLRFh2hMA=="], - "@trezor/websocket-client/ws": ["ws@8.21.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="], "@walletconnect/environment/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], @@ -3038,8 +3064,6 @@ "async-mutex/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "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=="], "bitcoinjs-lib/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], @@ -3218,6 +3242,8 @@ "websocket/utf-8-validate": ["utf-8-validate@5.0.10", "", { "dependencies": { "node-gyp-build": "^4.3.0" } }, "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ=="], + "wif/bs58check": ["bs58check@4.0.0", "", { "dependencies": { "@noble/hashes": "^1.2.0", "bs58": "^6.0.0" } }, "sha512-FsGDOnFg9aVI9erdriULkd/JjEWONV/lQE5aYziB5PoBsXRind56lh8doIZIc9X4HoxT5x4bLjMWN1/NB8Zp5g=="], + "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=="], "xrpl/@scure/bip39": ["@scure/bip39@1.6.0", "", { "dependencies": { "@noble/hashes": "~1.8.0", "@scure/base": "~1.2.5" } }, "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A=="], @@ -3258,11 +3284,15 @@ "@ledgerhq/device-signer-kit-ethereum/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/hw-app-btc/@ledgerhq/hw-transport/@ledgerhq/devices": ["@ledgerhq/devices@8.14.2", "", { "dependencies": { "@ledgerhq/errors": "^6.34.1", "@ledgerhq/logs": "^6.17.0", "rxjs": "7.8.2", "semver": "7.7.3" } }, "sha512-T3pnfrsQEC/eJU0XHIqWI6qww+CL1k3NumR2XGWlMz8lVpoZ9HhcuGoCkMevp+8SWmymgyNIIFue3jlNA5KiMw=="], + + "@ledgerhq/hw-app-btc/@ledgerhq/hw-transport/@ledgerhq/errors": ["@ledgerhq/errors@6.37.0", "", {}, "sha512-T5yiKI5UX7ugeocdTF3TUsCIN2BH41Bio4ZeN410YFjFOf3es08n/5JyMzzKwzRgP0blG3HfBf7s7vJKqCSAeg=="], + + "@ledgerhq/hw-app-btc/@ledgerhq/hw-transport/@ledgerhq/logs": ["@ledgerhq/logs@6.19.0", "", {}, "sha512-kmWj18C7eTtuytdaWygzVrQm9EoD4YWuYnd/kY8stVBBAzRdJzrGlm013ijQF6I/kGLKlGY7JKmll6j32KGpfQ=="], "@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/live-network/axios/proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], "@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=="], @@ -3278,6 +3308,8 @@ "@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=="], + "@mysten/ledgerjs-hw-app-sui/@ledgerhq/devices/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "@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=="], @@ -3584,6 +3616,10 @@ "websocket/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + "wif/bs58check/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], + + "wif/bs58check/bs58": ["bs58@6.0.0", "", { "dependencies": { "base-x": "^5.0.0" } }, "sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw=="], + "xrpl/@scure/bip32/@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], "xrpl/@scure/bip32/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], @@ -3726,6 +3762,8 @@ "ripemd160/hash-base/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], + "wif/bs58check/bs58/base-x": ["base-x@5.0.1", "", {}, "sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg=="], + "@cosmjs/stargate/@cosmjs/amino/@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=="], diff --git a/packages/wallet-hardware/package.json b/packages/wallet-hardware/package.json index cdd5fea..bc339bf 100644 --- a/packages/wallet-hardware/package.json +++ b/packages/wallet-hardware/package.json @@ -4,20 +4,20 @@ "@cosmjs/amino": "~0.37.0", "@cosmjs/crypto": "0.37.0", "@keepkey/keepkey-sdk": "~0.2.62", - "@ledgerhq/context-module": "~2.4.0", - "@ledgerhq/device-management-kit": "~1.8.0", + "@ledgerhq/context-module": "~2.6.0", + "@ledgerhq/device-management-kit": "~1.9.1", "@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-signer-kit-ethereum": "~1.18.1", + "@ledgerhq/device-signer-kit-zcash": "~0.7.1", "@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-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-app-btc": "~10.22.1", + "@ledgerhq/hw-app-cosmos": "~6.36.7", + "@ledgerhq/hw-app-near": "~6.35.7", + "@ledgerhq/hw-app-sui": "~1.11.4", + "@ledgerhq/hw-app-trx": "~6.36.6", + "@ledgerhq/hw-app-xrp": "~6.37.7", + "@ledgerhq/hw-transport": "~6.35.7", "@near-js/crypto": "~2.5.0", "@near-js/transactions": "~2.5.0", "@scure/base": "2.2.0", @@ -39,20 +39,20 @@ "@cosmjs/amino": "0.37.0", "@cosmjs/crypto": "0.37.0", "@keepkey/keepkey-sdk": "0.2.62", - "@ledgerhq/context-module": "2.4.0", - "@ledgerhq/device-management-kit": "1.8.0", + "@ledgerhq/context-module": "2.6.0", + "@ledgerhq/device-management-kit": "1.9.1", "@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-signer-kit-ethereum": "1.18.1", + "@ledgerhq/device-signer-kit-zcash": "0.7.1", "@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-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-app-btc": "10.22.1", + "@ledgerhq/hw-app-cosmos": "6.36.7", + "@ledgerhq/hw-app-near": "6.35.7", + "@ledgerhq/hw-app-sui": "1.11.4", + "@ledgerhq/hw-app-trx": "6.36.6", + "@ledgerhq/hw-app-xrp": "6.37.7", + "@ledgerhq/hw-transport": "6.35.7", "@near-js/crypto": "2.5.0", "@near-js/transactions": "2.5.0", "@scure/base": "2.2.0", From 4d74e5c5290abed99dc3a0823c5bfda9d9296a7c Mon Sep 17 00:00:00 2001 From: Ice Date: Wed, 23 Sep 2026 13:36:47 +0200 Subject: [PATCH 14/27] fix(wallet-hardware): produce valid Bitcoin transactions from Ledger DSK signatures The signer kit's signTransaction extracts the transaction itself, writing the PSBT format version (2) where the transaction version belongs and dropping the witness of nested SegWit inputs. The SDK builds version 1 transactions, so every raw transaction from transfer and signAndBroadcastTransaction was invalid. Request signatures with signPsbt instead and apply them to our own PSBT, then finalise locally. Each signature must match the derived input key and every input must be signed. Also give the device what it needs to verify each input: the full previous transaction (checked against the input txid; fetched when the toolbox PSBT omits it) and, for SegWit, the spent output. This fixes nested SegWit signing and the unverified-inputs warning on native SegWit sends. Memo outputs no longer fail, as nothing re-parses the signed transaction. --- .../src/ledger/clients/bitcoin.ts | 238 +++++++++---- .../test/ledger-bitcoin-dsk.test.ts | 325 ++++++++++++------ 2 files changed, 386 insertions(+), 177 deletions(-) diff --git a/packages/wallet-hardware/src/ledger/clients/bitcoin.ts b/packages/wallet-hardware/src/ledger/clients/bitcoin.ts index 4f1d041..6e4cb5b 100644 --- a/packages/wallet-hardware/src/ledger/clients/bitcoin.ts +++ b/packages/wallet-hardware/src/ledger/clients/bitcoin.ts @@ -11,11 +11,11 @@ import { } 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 { createCachedRawTxResolver } from "../helpers/rawTx"; import { BitcoinLedger as LegacyBitcoinLedger } from "./utxo"; import { extractInputsFromPsbt, signLegacyPsbtTransaction } from "./utxo-legacy-adapter"; @@ -140,74 +140,51 @@ function descriptorTemplate(purpose: ParsedBitcoinPath["purpose"]) { }); } -function stripHexPrefix(value: string) { - return value.replace(/^0x/i, ""); +// Mirrors the signer kit's `PartialSignature`, which its package entry point does not export. +interface PartialSignature { + inputIndex: number; + pubkey: Uint8Array; + signature: Uint8Array; + tapleafHash?: Uint8Array; } -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" }); - } +function isPartialSignature(signature: object): signature is PartialSignature { + return "signature" in signature && "pubkey" in signature; +} - 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(); +function signatureError({ inputIndex, reason }: { inputIndex?: number; reason: string }) { + return new SwapKitError("wallet_ledger_invalid_response", { inputIndex, reason }); } -async function restoreSignedPsbt({ - rawTransaction, - tx, - purpose, +type PreviousTransaction = NonNullable["nonWitnessUtxo"]>; + +// Returns the output an input spends, after checking the previous transaction hashes to the input's +// txid: amounts shown on the device come from it, so it must not be substitutable. +async function verifiedSpentOutput({ + input, + inputIndex, + previousTransaction, }: { - rawTransaction: string; - tx: Transaction; - purpose: ParsedBitcoinPath["purpose"]; + input: ReturnType; + inputIndex: number; + previousTransaction?: PreviousTransaction; }) { - 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", - }); + if (!(previousTransaction && input.txid && input.index !== undefined)) { + throw signatureError({ inputIndex, reason: "PSBT input is missing its previous output" }); } - const signedPsbt = tx.clone(); - for (let inputIndex = 0; inputIndex < parsedRaw.inputsLength; inputIndex += 1) { - signedPsbt.updateInput(inputIndex, await signedInputParts({ purpose, rawInput: parsedRaw.getInput(inputIndex) })); + const { RawTx, utils } = await import("@swapkit/utxo-signer"); + const previousTxid = utils.sha256x2(RawTx.encode(previousTransaction)).reverse(); + if (hex.encode(previousTxid) !== hex.encode(input.txid)) { + throw new SwapKitError("wallet_ledger_invalid_params", { + inputIndex, + reason: "Previous transaction does not match the input txid", + }); } - return signedPsbt; + + const spentOutput = previousTransaction.outputs[input.index]; + if (!spentOutput) throw signatureError({ inputIndex, reason: "Previous transaction has no spent output" }); + return spentOutput; } export function BitcoinLedger({ @@ -280,15 +257,61 @@ export function BitcoinLedger({ return new DefaultWallet(configuredPath.accountPath, template); } - async function addInputDerivations({ paths, tx }: { paths: ParsedBitcoinPath[]; tx: Transaction }) { - const [accountXpub, fingerprint] = await Promise.all([getAccountXpub(), getMasterFingerprint()]); + async function resolvePreviousTransactions({ inputUtxos, tx }: { inputUtxos?: UTXOType[]; tx: Transaction }) { + const { RawTx } = await import("@swapkit/utxo-signer"); + const needsLookup = Array.from({ length: tx.inputsLength }, (_, index) => index).some( + (index) => !(tx.getInput(index).nonWitnessUtxo || inputUtxos?.[index]?.txHex), + ); + const getRawTx = needsLookup + ? await import("@swapkit/toolboxes/utxo").then(({ getUtxoApi }) => { + const utxoApi = getUtxoApi(Chain.Bitcoin); + return createCachedRawTxResolver((txid) => utxoApi.getRawTx(txid)); + }) + : undefined; + + return Promise.all( + Array.from({ length: tx.inputsLength }, async (_, inputIndex) => { + const input = tx.getInput(inputIndex); + if (input.nonWitnessUtxo) return input.nonWitnessUtxo; + if (!input.txid) throw signatureError({ inputIndex, reason: "PSBT input is missing its previous txid" }); + + const txHex = inputUtxos?.[inputIndex]?.txHex ?? (await getRawTx?.(hex.encode(input.txid))); + if (!txHex) { + throw new SwapKitError("wallet_ledger_invalid_params", { + inputIndex, + reason: "Unable to resolve the previous transaction for Ledger signing", + }); + } + return RawTx.decode(hex.decode(txHex)); + }), + ); + } + + /** + * Attach what the Bitcoin app needs to verify and sign each input: key origins, the full previous + * transaction (input amounts are only verifiable from it) and, for SegWit v0, the spent output. + * The previous transaction is checked against the input's txid so it cannot misstate the amount. + */ + async function addInputDerivations({ + inputUtxos, + paths, + tx, + }: { + inputUtxos?: UTXOType[]; + paths: ParsedBitcoinPath[]; + tx: Transaction; + }) { + const [accountXpub, fingerprint, previousTransactions] = await Promise.all([ + getAccountXpub(), + getMasterFingerprint(), + resolvePreviousTransactions({ inputUtxos, tx }), + ]); const { p2wpkh } = await import("@swapkit/utxo-signer"); const accountKey = HDKey.fromExtendedKey(accountXpub); const psbt = tx.clone(); + const publicKeys: Uint8Array[] = []; - for (let inputIndex = 0; inputIndex < paths.length; inputIndex += 1) { - const path = paths[inputIndex]; - if (!path) continue; + for (const [inputIndex, path] of paths.entries()) { const publicKey = accountKey.derive(`m/${path.change}/${path.addressIndex}`).publicKey; if (!publicKey) { throw new SwapKitError("wallet_ledger_invalid_response", { @@ -297,32 +320,91 @@ export function BitcoinLedger({ }); } + publicKeys[inputIndex] = publicKey; + + const input = psbt.getInput(inputIndex); + const previousTransaction = previousTransactions[inputIndex]; + const spentOutput = await verifiedSpentOutput({ input, inputIndex, previousTransaction }); + 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, + ...(input.witnessUtxo ? {} : { witnessUtxo: spentOutput }), }); } else { - const input = psbt.getInput(inputIndex); psbt.updateInput(inputIndex, { bip32Derivation: [[publicKey, derivation]], + nonWitnessUtxo: previousTransaction, + ...(path.purpose === 44 || input.witnessUtxo ? {} : { witnessUtxo: spentOutput }), ...(path.purpose === 49 && !input.redeemScript ? { redeemScript: p2wpkh(publicKey).script } : {}), }); } } - return psbt; + return { psbt, publicKeys }; } - 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)), + /** + * Ask the device for signatures only and apply them to our own PSBT. The signer kit's own + * transaction extraction writes the PSBT format version as the transaction version and drops + * the witness of nested SegWit inputs, which invalidates the signatures. + */ + async function signWithPaths({ + inputUtxos, + paths, + tx, + }: { + inputUtxos?: UTXOType[]; + paths: ParsedBitcoinPath[]; + tx: Transaction; + }) { + const [signer, wallet, { psbt, publicKeys }] = await Promise.all([ + getSigner(), + getWallet(), + addInputDerivations({ inputUtxos, paths, tx }), + ]); + const signatures = await executeLedgerDeviceAction({ + action: signer.signPsbt(wallet, psbt.toPSBT(0)), onDeviceActionState, }); - return stripHexPrefix(signedTransaction); + + const signedInputs = new Set(); + for (const signature of signatures) { + if (!isPartialSignature(signature)) throw signatureError({ reason: "Unexpected MuSig2 signature" }); + + const { inputIndex, pubkey } = signature; + const path = paths[inputIndex]; + const publicKey = publicKeys[inputIndex]; + if (!(path && publicKey) || signedInputs.has(inputIndex)) { + throw signatureError({ inputIndex, reason: "Unexpected or duplicate input signature" }); + } + signedInputs.add(inputIndex); + + if (path.purpose === 86) { + if (signature.tapleafHash) throw signatureError({ inputIndex, reason: "Unexpected taproot script signature" }); + psbt.updateInput(inputIndex, { tapKeySig: signature.signature }); + continue; + } + + if (hex.encode(pubkey) !== hex.encode(publicKey)) { + throw signatureError({ inputIndex, reason: "Signature public key does not match the input derivation" }); + } + psbt.updateInput(inputIndex, { partialSig: [[publicKey, signature.signature]] }); + } + + if (signedInputs.size !== psbt.inputsLength) { + throw signatureError({ reason: `Ledger signed ${signedInputs.size} of ${psbt.inputsLength} inputs` }); + } + + return psbt; + } + + function finalizeToHex(psbt: Transaction) { + psbt.finalize(); + return psbt.hex; } function signTransactionHex({ tx, inputUtxos }: SignBitcoinTransactionParams) { @@ -335,7 +417,7 @@ export function BitcoinLedger({ } const paths = Array.from({ length: tx.inputsLength }, () => configuredPath); - return signWithPaths({ paths, tx }); + return signWithPaths({ inputUtxos, paths, tx }).then(finalizeToHex); } return { @@ -386,8 +468,14 @@ export function BitcoinLedger({ }); }, signTransaction: async (tx: Transaction) => { - const signedTransaction = await signTransactionHex({ tx }); - return restoreSignedPsbt({ purpose: configuredPath.purpose, rawTransaction: signedTransaction, tx }); + if (legacyClient) { + // The legacy app returns a finalised transaction, as the other legacy UTXO chains do. + const signedTxHex = await signLegacyPsbtTransaction({ chain: Chain.Bitcoin, legacyClient, tx }); + const { Transaction: TransactionClass } = await import("@swapkit/utxo-signer"); + return TransactionClass.fromRaw(hex.decode(signedTxHex), { allowUnknownOutputs: true }); + } + const paths = Array.from({ length: tx.inputsLength }, () => configuredPath); + return await signWithPaths({ paths, tx }); }, signTransactionHex, signTransactionWithMultiplePaths: async ({ @@ -407,7 +495,7 @@ export function BitcoinLedger({ ); } - return signWithPaths({ paths, tx }); + return signWithPaths({ inputUtxos, paths, tx }).then(finalizeToHex); }, }; } diff --git a/packages/wallet-hardware/test/ledger-bitcoin-dsk.test.ts b/packages/wallet-hardware/test/ledger-bitcoin-dsk.test.ts index 832dae6..a5996a9 100644 --- a/packages/wallet-hardware/test/ledger-bitcoin-dsk.test.ts +++ b/packages/wallet-hardware/test/ledger-bitcoin-dsk.test.ts @@ -1,12 +1,14 @@ -import { beforeEach, describe, expect, it, mock } from "bun:test"; +import { afterAll, 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 * as realUtxoToolbox from "@swapkit/toolboxes/utxo"; 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 zeroAuxRand = new Uint8Array(32); const walletAddressCalls: Array<{ addressIndex: number; options: { change?: boolean; checkOnDevice?: boolean } | undefined; @@ -14,11 +16,21 @@ const walletAddressCalls: Array<{ }> = []; const xpubCalls: Array<{ options?: { checkOnDevice?: boolean }; path: string }> = []; const signCalls: Array<{ psbt: Uint8Array; wallet: { derivationPath: string; template: string } }> = []; +const rawTxRequests: string[] = []; +const rawTxs = new Map(); let fingerprintCalls = 0; let fingerprintFailuresRemaining = 0; -let signedRaw = ""; +let tamperSignatures: ((signatures: DevicePartialSignature[]) => DevicePartialSignature[]) | undefined; let xpubFailuresRemaining = 0; +type DevicePartialSignature = { inputIndex: number; pubkey: Uint8Array; signature: Uint8Array }; + +function leafKey(change: number, index: number) { + const key = accountKey.derive(`m/${change}/${index}`); + if (!(key.publicKey && key.privateKey)) throw new Error("Fixture account did not derive a key pair"); + return { privateKey: key.privateKey, publicKey: key.publicKey }; +} + function deviceAction(output: Output) { return { cancel: mock(() => {}), @@ -33,6 +45,25 @@ function failedDeviceAction(error: Error) { return { cancel: mock(() => {}), observable: of({ error, status: DeviceActionStatus.Error }) }; } +// Signs the PSBT the way the Bitcoin app does: one partial signature per input, from the key the +// PSBT's key origin points at, without touching the transaction itself. +function deviceSignatures(psbtBytes: Uint8Array) { + const psbt = Transaction.fromPSBT(psbtBytes); + return Array.from({ length: psbt.inputsLength }, (_, inputIndex) => { + const input = psbt.getInput(inputIndex); + const taprootPath = input.tapBip32Derivation?.[0]?.[1].der.path; + const path = taprootPath ?? input.bip32Derivation?.[0]?.[1].path; + if (!path) throw new Error(`Input ${inputIndex} has no key origin`); + const { privateKey, publicKey } = leafKey(path[3] ?? 0, path[4] ?? 0); + + psbt.signIdx(privateKey, inputIndex, undefined, zeroAuxRand); + const signed = psbt.getInput(inputIndex); + const signature = taprootPath ? signed.tapKeySig : signed.partialSig?.[0]?.[1]; + if (!signature) throw new Error(`Input ${inputIndex} was not signed`); + return { inputIndex, pubkey: taprootPath ? publicKey.slice(1) : publicKey, signature }; + }); +} + mock.module("@ledgerhq/device-signer-kit-bitcoin", () => ({ DefaultDescriptorTemplate: { LEGACY: "pkh(@0/**)", @@ -73,40 +104,85 @@ mock.module("@ledgerhq/device-signer-kit-bitcoin", () => ({ walletAddressCalls.push({ addressIndex, options, wallet }); return deviceAction({ address: "bc1qledgerdsk" }); }, - signTransaction: (wallet: { derivationPath: string; template: string }, psbt: Uint8Array) => { + signPsbt: (wallet: { derivationPath: string; template: string }, psbt: Uint8Array) => { signCalls.push({ psbt, wallet }); - return deviceAction(`0x${signedRaw}`); + const signatures = deviceSignatures(psbt); + return deviceAction(tamperSignatures ? tamperSignatures(signatures) : signatures); }, }; } }, })); +// mock.module replaces the whole export namespace process-wide; snapshot the real module so +// afterAll can restore it for test files that run later. +const realUtxoToolboxSnapshot = { ...realUtxoToolbox }; + +mock.module("@swapkit/toolboxes/utxo", () => ({ + ...realUtxoToolboxSnapshot, + getUtxoApi: () => ({ + getRawTx: (txid: string) => { + rawTxRequests.push(txid); + return Promise.resolve(rawTxs.get(txid) ?? ""); + }, + }), +})); + 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; +type Purpose = 44 | 49 | 84 | 86; + +function outputScript(purpose: Purpose, publicKey: Uint8Array) { + if (purpose === 44) return p2pkh(publicKey).script; + if (purpose === 49) return p2sh(p2wpkh(publicKey)).script; + if (purpose === 86) return p2tr(publicKey.slice(1)).script; + return p2wpkh(publicKey).script; +} + +// A funding transaction paying `amount` to each script, so inputs can reference a real txid. +function previousTransaction(scripts: Uint8Array[], amount = 10_000n) { + const funding = new Transaction({ allowLegacyWitnessUtxo: true }); + funding.addInput({ index: 0, txid: new Uint8Array(32).fill(9) }); + for (const script of scripts) funding.addOutput({ amount, script }); + return { txHex: hex.encode(funding.unsignedTx), txid: funding.id }; +} + +/** + * Mirrors what the SDK toolbox builds: version 1, SegWit v0 and taproot inputs carry only + * `witnessUtxo`, legacy and nested SegWit inputs carry only `nonWitnessUtxo`. + */ +function makeTransaction({ + keys = [{ change: 0, index: 0 }], + memo, + purpose = 84, +}: { + keys?: Array<{ change: number; index: number }>; + memo?: boolean; + purpose?: Purpose; +} = {}) { + const scripts = keys.map(({ change, index }) => outputScript(purpose, leafKey(change, index).publicKey)); + const funding = previousTransaction(scripts); + rawTxs.set(funding.txid, funding.txHex); + const tx = new Transaction({ allowLegacyWitnessUtxo: true, allowUnknownOutputs: memo, version: 1 }); + + scripts.forEach((script, index) => { + const prevout = { amount: 10_000n, script }; + tx.addInput({ + index, + txid: hex.decode(funding.txid), + ...(purpose === 84 || purpose === 86 ? { witnessUtxo: prevout } : { nonWitnessUtxo: hex.decode(funding.txHex) }), + }); + }); + tx.addOutput({ amount: BigInt(9_000 * keys.length), script: scripts[0] ?? new Uint8Array() }); + if (memo) tx.addOutput({ amount: 0n, script: Script.encode(["RETURN", new TextEncoder().encode("=:ETH.ETH:0x")]) }); + + return { funding, tx }; } -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()); +function version(rawHex: string) { + return new DataView(hex.decode(rawHex).buffer).getUint32(0, true); } describe("Ledger Bitcoin Device Signer Kit client", () => { @@ -114,9 +190,11 @@ describe("Ledger Bitcoin Device Signer Kit client", () => { walletAddressCalls.length = 0; xpubCalls.length = 0; signCalls.length = 0; + rawTxRequests.length = 0; + rawTxs.clear(); fingerprintCalls = 0; fingerprintFailuresRemaining = 0; - signedRaw = ""; + tamperSignatures = undefined; xpubFailuresRemaining = 0; }); @@ -137,42 +215,69 @@ describe("Ledger Bitcoin Device Signer Kit client", () => { 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); + it("signs only through signPsbt and keeps the transaction version the toolbox built", async () => { + const { funding, tx } = makeTransaction({ + keys: [ + { change: 0, index: 0 }, + { change: 0, index: 0 }, + ], + }); const client = BitcoinLedger({ derivationPath: "m/84'/0'/0'/0/0", dmkSession }); - const signed = await client.signTransaction(transaction); - signed.finalize(); + const raw = await client.signTransactionHex({ + inputUtxos: [0, 1].map((index) => ({ hash: funding.txid, index, txHex: funding.txHex, value: 10_000 })), + tx, + }); - expect(hex.encode(signed.extract())).toBe(signedRaw); - expect(signed.fee).toBe(2_000n); - expect(signCalls).toHaveLength(1); + expect(version(raw)).toBe(1); + const extracted = Transaction.fromRaw(hex.decode(raw)); + expect(extracted.getInput(0).finalScriptWitness?.[1]).toEqual(leafKey(0, 0).publicKey); + expect(rawTxRequests).toEqual([]); 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] }, - ], - ]); + + const devicePsbt = Transaction.fromPSBT(signCalls[0]?.psbt ?? new Uint8Array()); + for (const inputIndex of [0, 1]) { + const input = devicePsbt.getInput(inputIndex); + expect(input.bip32Derivation).toEqual([ + [leafKey(0, 0).publicKey, { fingerprint: 0xdeadbeef, path: [0x80000054, 0x80000000, 0x80000000, 0, 0] }], + ]); + expect(input.witnessUtxo?.amount).toBe(10_000n); + expect(input.nonWitnessUtxo).toBeDefined(); + } expect(xpubCalls).toEqual([{ options: undefined, path: "84'/0'/0'" }]); }); + it("fetches previous transactions the toolbox PSBT omits so the device can verify amounts", async () => { + const { funding, tx } = makeTransaction(); + const client = BitcoinLedger({ derivationPath: "84'/0'/0'/0/0", dmkSession }); + + const signed = await client.signTransaction(tx); + signed.finalize(); + + expect(rawTxRequests).toEqual([funding.txid]); + expect(version(signed.hex)).toBe(1); + expect(signed.fee).toBe(1_000n); + }); + + it("signs transactions carrying an OP_RETURN memo", async () => { + const { funding, tx } = makeTransaction({ memo: true }); + const client = BitcoinLedger({ derivationPath: "84'/0'/0'/0/0", dmkSession }); + + const raw = await client.signTransactionHex({ + inputUtxos: [{ hash: funding.txid, index: 0, txHex: funding.txHex, value: 10_000 }], + tx, + }); + + expect(Transaction.fromRaw(hex.decode(raw), { allowUnknownOutputs: true }).outputsLength).toBe(2); + }); + it("retries account xpub metadata after a transient device action failure", async () => { - const transaction = makeTransaction(); - signedRaw = signedTransactionRaw(transaction); + const { tx } = makeTransaction(); 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); + await expect(client.signTransaction(tx)).rejects.toThrow("Transient account xpub failure"); + await client.signTransaction(tx); expect(xpubCalls).toHaveLength(2); expect(fingerprintCalls).toBe(1); @@ -180,13 +285,12 @@ describe("Ledger Bitcoin Device Signer Kit client", () => { }); it("retries master fingerprint metadata after a transient device action failure", async () => { - const transaction = makeTransaction(); - signedRaw = signedTransactionRaw(transaction); + const { tx } = makeTransaction(); 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); + await expect(client.signTransaction(tx)).rejects.toThrow("Transient master fingerprint failure"); + await client.signTransaction(tx); expect(xpubCalls).toHaveLength(1); expect(fingerprintCalls).toBe(2); @@ -194,69 +298,86 @@ describe("Ledger Bitcoin Device Signer Kit client", () => { }); it("supports different leaf paths in one account and rejects account mixing", async () => { - const transaction = makeTransaction(); - signedRaw = signedTransactionRaw(transaction); + const { tx } = makeTransaction({ + keys: [ + { change: 0, index: 3 }, + { change: 1, index: 9 }, + ], + }); 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, + tx, }); - 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]); + const extracted = Transaction.fromRaw(hex.decode(raw)); + expect(extracted.getInput(0).finalScriptWitness?.[1]).toEqual(leafKey(0, 3).publicKey); + expect(extracted.getInput(1).finalScriptWitness?.[1]).toEqual(leafKey(1, 9).publicKey); + const devicePsbt = Transaction.fromPSBT(signCalls[0]?.psbt ?? new Uint8Array()); + expect(devicePsbt.getInput(0).bip32Derivation?.[0]?.[1].path).toEqual([0x80000054, 0x80000000, 0x80000000, 0, 3]); + expect(devicePsbt.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 }), + client.signTransactionWithMultiplePaths({ derivationPaths: ["84'/0'/0'/0/3", "84'/0'/1'/0/0"], tx }), ).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); + it("produces finalisable version-1 transactions for purposes 44, 49 and 86", async () => { + for (const purpose of [44, 49, 86] as const) { + const { tx } = makeTransaction({ purpose }); + const client = BitcoinLedger({ derivationPath: `${purpose}'/0'/0'/0/0`, dmkSession }); + + const signed = await client.signTransaction(tx); signed.finalize(); + const extracted = Transaction.fromRaw(signed.extract(), { allowLegacyWitnessUtxo: true }); - expect(hex.encode(signed.extract())).toBe(signedRaw); + expect(version(signed.hex)).toBe(1); expect(signed.fee).toBe(1_000n); + if (purpose === 49) { + // Nested SegWit needs both the redeem script push and the witness. + expect(extracted.getInput(0).finalScriptSig).toEqual(Script.encode([p2wpkh(leafKey(0, 0).publicKey).script])); + expect(extracted.getInput(0).finalScriptWitness).toHaveLength(2); + } + if (purpose === 86) expect(extracted.getInput(0).finalScriptWitness?.[0]).toHaveLength(64); } }); + + it("fails closed when the device signs with an unexpected key or skips an input", async () => { + const client = BitcoinLedger({ derivationPath: "84'/0'/0'/0/0", dmkSession }); + + tamperSignatures = (signatures) => + signatures.map((signature) => ({ ...signature, pubkey: leafKey(0, 1).publicKey })); + await expect(client.signTransaction(makeTransaction().tx)).rejects.toThrow("wallet_ledger_invalid_response"); + + tamperSignatures = (signatures) => signatures.slice(1); + await expect( + client.signTransaction( + makeTransaction({ + keys: [ + { change: 0, index: 0 }, + { change: 0, index: 0 }, + ], + }).tx, + ), + ).rejects.toThrow("wallet_ledger_invalid_response"); + }); + + it("rejects a previous transaction that does not match the input txid", async () => { + const { tx } = makeTransaction(); + const other = previousTransaction([outputScript(84, leafKey(0, 0).publicKey)], 99_999n); + const client = BitcoinLedger({ derivationPath: "84'/0'/0'/0/0", dmkSession }); + + await expect( + client.signTransactionHex({ + inputUtxos: [{ hash: other.txid, index: 0, txHex: other.txHex, value: 99_999 }], + tx, + }), + ).rejects.toThrow("wallet_ledger_invalid_params"); + expect(signCalls).toHaveLength(0); + }); +}); + +afterAll(() => { + mock.module("@swapkit/toolboxes/utxo", () => realUtxoToolboxSnapshot); }); From da7b6473d90384f58701325754942628a6f9f32d Mon Sep 17 00:00:00 2001 From: Ice Date: Wed, 23 Sep 2026 13:37:19 +0200 Subject: [PATCH 15/27] fix(wallet-hardware): refuse Zcash Ledger signing over a legacy transport hw-app-btc selects the NU6.1 branch id when no block height is given and parses v5 previous transactions without the zcash header, so a transaction signed through an injected LedgerJS transport is rejected by the network. Signing now requires a DMK session; addresses and xpubs still work over the legacy transport. --- .../wallet-hardware/src/ledger/clients/zcash.ts | 17 ++++++----------- .../test/ledger-zcash-dsk.test.ts | 12 ++++++++++++ 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/packages/wallet-hardware/src/ledger/clients/zcash.ts b/packages/wallet-hardware/src/ledger/clients/zcash.ts index e070134..418309d 100644 --- a/packages/wallet-hardware/src/ledger/clients/zcash.ts +++ b/packages/wallet-hardware/src/ledger/clients/zcash.ts @@ -8,7 +8,7 @@ import { SwapKitError, } from "@swapkit/helpers"; import type { UTXOType } from "@swapkit/toolboxes/utxo"; -import type { Transaction, ZcashTransaction } from "@swapkit/utxo-signer"; +import type { ZcashTransaction } from "@swapkit/utxo-signer"; import type { LedgerDMKSession } from "../helpers/dmk"; import { getLedgerDMKSession } from "../helpers/dmk"; @@ -350,17 +350,12 @@ export function ZcashLedger({ tx, }); + // hw-app-btc picks a pre-Ironwood branch id without a block height and misparses v5 previous + // transactions, so the legacy transport cannot produce a transaction the network accepts. 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); + throw new SwapKitError("wallet_ledger_invalid_params", { + reason: "Zcash signing requires a Ledger DMK session instead of a legacy transport", + }); } const blockHeight = await activationHeight(tx.consensusBranchId); diff --git a/packages/wallet-hardware/test/ledger-zcash-dsk.test.ts b/packages/wallet-hardware/test/ledger-zcash-dsk.test.ts index 5e3ca2c..2ea4de9 100644 --- a/packages/wallet-hardware/test/ledger-zcash-dsk.test.ts +++ b/packages/wallet-hardware/test/ledger-zcash-dsk.test.ts @@ -1,5 +1,6 @@ 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 { hex } from "@scure/base"; import type { UTXOType } from "@swapkit/toolboxes/utxo"; import { ZcashConsensusBranchId, ZcashTransaction, ZcashVersionGroupId } from "@swapkit/utxo-signer"; @@ -170,6 +171,17 @@ describe("Ledger Zcash Device Signer Kit client", () => { expect(signCalls).toHaveLength(0); }); + it("refuses to sign over a legacy transport", async () => { + const send = mock(() => Promise.reject(new Error("no APDU expected"))); + const client = ZcashLedger({ transport: { send } as unknown as Transport }); + + await expect(client.signTransaction({ inputUtxos: [inputUtxo()], tx: targetTransaction() })).rejects.toThrow( + "wallet_ledger_invalid_params", + ); + expect(send).not.toHaveBeenCalled(); + expect(signCalls).toHaveLength(0); + }); + it("returns address bytes and propagates address verification state", async () => { const states: string[] = []; const client = ZcashLedger({ From 4ab7ad8382dbdedb00dce6182b1035675dd79c44 Mon Sep 17 00:00:00 2001 From: Ice Date: Wed, 23 Sep 2026 13:37:43 +0200 Subject: [PATCH 16/27] refactor(wallet-hardware): remove unreachable legacy Zcash PCZT signing The Ledger wallet rejects PCZT signing and Zcash transfers go through the signer kit, so signPCZT and its previous-transaction resolvers had no callers left. --- .../src/ledger/clients/utxo.ts | 135 +----------------- .../test/ledger-zcash-legacy-prevtx.test.ts | 79 ---------- 2 files changed, 2 insertions(+), 212 deletions(-) delete mode 100644 packages/wallet-hardware/test/ledger-zcash-legacy-prevtx.test.ts diff --git a/packages/wallet-hardware/src/ledger/clients/utxo.ts b/packages/wallet-hardware/src/ledger/clients/utxo.ts index 732ecaf..8c93890 100644 --- a/packages/wallet-hardware/src/ledger/clients/utxo.ts +++ b/packages/wallet-hardware/src/ledger/clients/utxo.ts @@ -3,15 +3,9 @@ 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 { - Chain, - type DerivationPathArray, - derivationPathToString, - getWalletFormatFor, - SwapKitError, -} from "@swapkit/helpers"; +import { type DerivationPathArray, derivationPathToString, getWalletFormatFor, SwapKitError } from "@swapkit/helpers"; import type { UTXOType } from "@swapkit/toolboxes/utxo"; -import type { PCZT, Transaction } from "@swapkit/utxo-signer"; +import type { Transaction } from "@swapkit/utxo-signer"; import { LEDGER_USER_INTERACTION_REQUIRED, @@ -19,20 +13,12 @@ import { 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", @@ -55,48 +41,6 @@ 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, @@ -240,81 +184,6 @@ const BaseLedgerUTXO = ({ return await runBtcOperation({ operation: (app) => app.getWalletXpub({ path, xpubVersion }) }); }, - signPCZT: async (pczt: PCZT): Promise => { - if (chain !== "zcash") { - throw new SwapKitError("wallet_ledger_chain_not_supported", { - message: "PCZT signing is only supported for Zcash", - }); - } - - const { ZcashTransaction, Script } = await import("@swapkit/utxo-signer"); - const { getUtxoApi } = await import("@swapkit/toolboxes/utxo"); - - const global = pczt.getGlobal(); - - const unsignedTx = new ZcashTransaction({ - consensusBranchId: global.consensusBranchId, - expiryHeight: global.expiryHeight, - lockTime: global.lockTime, - version: global.txVersion, - versionGroupId: global.versionGroupId, - }); - - const inputs = Array.from({ length: pczt.inputsLength }, (_, inputIndex) => pczt.getInput(inputIndex)); - - for (const input of inputs) { - unsignedTx.addInput({ - index: input.index, - script: new Uint8Array(), - sequence: input.sequence ?? 0xffffffff, - txid: input.txid, - value: input.value, - }); - } - - 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 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(); - - for (let i = 0; i < signedTx.inputsLength; i++) { - const signedInput = signedTx.getInput(i); - if (signedInput.script && signedInput.script.length > 0) { - const scriptParts = Script.decode(signedInput.script); - if (scriptParts.length >= 2) { - signedPczt.addSignature(i, scriptParts[1] as Uint8Array, scriptParts[0] as Uint8Array); - } - } - } - - return signedPczt; - }, signTransaction: async (tx: Transaction, inputUtxos: UTXOType[]) => { return await runBtcOperation({ operation: (app) => diff --git a/packages/wallet-hardware/test/ledger-zcash-legacy-prevtx.test.ts b/packages/wallet-hardware/test/ledger-zcash-legacy-prevtx.test.ts deleted file mode 100644 index 8c22206..0000000 --- a/packages/wallet-hardware/test/ledger-zcash-legacy-prevtx.test.ts +++ /dev/null @@ -1,79 +0,0 @@ -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); - }); -}); From bb4e43b1b3618f8f8223fdfd59a06983892b0a66 Mon Sep 17 00:00:00 2001 From: Ice Date: Wed, 23 Sep 2026 13:38:18 +0200 Subject: [PATCH 17/27] fix(wallet-hardware): accept memo outputs in Ledger legacy UTXO signing The legacy LTC/BCH/DOGE/DASH toolbox signer re-parses the signed transaction and the multi-address transfer builds its own transaction; both rejected the OP_RETURN output of memo transactions such as THORChain swaps. --- .../src/ledger/clients/utxo-legacy-adapter.ts | 3 +- packages/wallet-hardware/src/ledger/index.ts | 2 +- .../test/ledger-utxo-legacy-adapter.test.ts | 32 ++++++++++++++++++- 3 files changed, 34 insertions(+), 3 deletions(-) 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 0add774..568789d 100644 --- a/packages/wallet-hardware/src/ledger/clients/utxo-legacy-adapter.ts +++ b/packages/wallet-hardware/src/ledger/clients/utxo-legacy-adapter.ts @@ -88,7 +88,8 @@ export function createLegacyPsbtSigner({ const { Transaction: TxClass } = await import("@swapkit/utxo-signer"); // `Transaction.fromRaw` parses a serialised tx (no PSBT envelope) — exactly // what `createPaymentTransaction` returns. - return TxClass.fromRaw(hex.decode(signedTxHex)); + // Memo transactions carry an OP_RETURN output, which the parser treats as unknown. + return TxClass.fromRaw(hex.decode(signedTxHex), { allowUnknownOutputs: true }); }, }; } diff --git a/packages/wallet-hardware/src/ledger/index.ts b/packages/wallet-hardware/src/ledger/index.ts index 1a67b10..a635216 100644 --- a/packages/wallet-hardware/src/ledger/index.ts +++ b/packages/wallet-hardware/src/ledger/index.ts @@ -517,7 +517,7 @@ async function getUTXOWalletMethods({ } const { Transaction } = await import("@swapkit/utxo-signer"); - const tx = new Transaction({ allowLegacyWitnessUtxo: true, version: 1 }); + const tx = new Transaction({ allowLegacyWitnessUtxo: true, allowUnknownOutputs: !!memoScript, version: 1 }); const senderAddress = changeAddress || utxos[0]?.address || recipient; addInputsAndOutputs({ 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 1c43b4c..8d655ab 100644 --- a/packages/wallet-hardware/test/ledger-utxo-legacy-adapter.test.ts +++ b/packages/wallet-hardware/test/ledger-utxo-legacy-adapter.test.ts @@ -18,7 +18,11 @@ mock.module("@swapkit/toolboxes/utxo", () => ({ }), })); -import { extractInputsFromPsbt, signLegacyPsbtTransaction } from "../src/ledger/clients/utxo-legacy-adapter"; +import { + createLegacyPsbtSigner, + extractInputsFromPsbt, + signLegacyPsbtTransaction, +} from "../src/ledger/clients/utxo-legacy-adapter"; describe("ledger legacy UTXO adapter", () => { beforeEach(() => { @@ -154,6 +158,32 @@ describe("ledger legacy UTXO adapter", () => { expect(signTransaction).toHaveBeenCalledTimes(1); expect(signedTxHex).toBe("0200000000"); }); + + it("parses signed transactions carrying an OP_RETURN memo", async () => { + const memo = new Uint8Array([0x6a, 0x04, ...new TextEncoder().encode("=:r:")]); + const signedTxHex = hex.encode( + RawTx.encode({ + inputs: [{ finalScriptSig: new Uint8Array([0x00]), index: 0, sequence: 0xffffffff, txid: new Uint8Array(32) }], + lockTime: 0, + outputs: [{ amount: 0n, script: memo }], + segwitFlag: undefined, + version: 1, + witnesses: undefined, + }), + ); + + const tx = new Transaction({ allowUnknownOutputs: true }); + tx.addInput({ index: 0, txid: new Uint8Array(32).fill(3) }); + const signer = createLegacyPsbtSigner({ + address: "ltc1qledger", + chain: Chain.Litecoin, + legacyClient: { signTransaction: () => Promise.resolve(signedTxHex) }, + }); + + const parsed = await signer.signTransaction(tx); + + expect(parsed.getOutput(0).script).toEqual(memo); + }); }); afterAll(() => { From ed9303a66549c3ec0ffb0490a27f77aff02fc6ce Mon Sep 17 00:00:00 2001 From: Ice Date: Wed, 23 Sep 2026 13:38:38 +0200 Subject: [PATCH 18/27] fix(wallet-hardware): send only the referenced EIP-712 types to Ledger Typed data with unrelated struct definitions reached the device unchanged. When the signer kit falls back to legacy signing it hashes with ethers' TypedDataEncoder, which throws on unused types, so the fallback failed. --- packages/wallet-hardware/src/ledger/clients/evm.ts | 4 +++- packages/wallet-hardware/test/ledger-transport.test.ts | 6 +++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/wallet-hardware/src/ledger/clients/evm.ts b/packages/wallet-hardware/src/ledger/clients/evm.ts index b89c35d..a8a2518 100644 --- a/packages/wallet-hardware/src/ledger/clients/evm.ts +++ b/packages/wallet-hardware/src/ledger/clients/evm.ts @@ -178,7 +178,9 @@ class EVMLedgerInterface extends AbstractSigner { domain: ledgerDomain, message: populated.value, primaryType, - types: { EIP712Domain: buildEIP712DomainType(populated.domain), ...filteredTypes }, + // Only the primary type and its dependencies: the signer kit's legacy fallback hashes with ethers, + // which rejects unused types. + types: { EIP712Domain: buildEIP712DomainType(populated.domain), ...resolutionTypes }, }), onDeviceActionState: this.onDeviceActionState, }); diff --git a/packages/wallet-hardware/test/ledger-transport.test.ts b/packages/wallet-hardware/test/ledger-transport.test.ts index 8b1baec..f64fddc 100644 --- a/packages/wallet-hardware/test/ledger-transport.test.ts +++ b/packages/wallet-hardware/test/ledger-transport.test.ts @@ -301,8 +301,12 @@ describe("wallet-hardware/ledger", () => { expect(ethereumSignTypedDataInvocations[0]?.typedData).toMatchObject({ message: { recipient: "0x0000000000000000000000000000000000000004" }, primaryType: "Message", - types: { Message: [{ name: "recipient", type: "address" }], Unrelated: [{ name: "contents", type: "string" }] }, + types: { + EIP712Domain: [{ name: "chainId", type: "uint256" }], + Message: [{ name: "recipient", type: "address" }], + }, }); + expect(ethereumSignTypedDataInvocations[0]?.typedData.types).not.toHaveProperty("Unrelated"); }); it("ArbitrumLedger: preserves EIP-2930 access lists", async () => { From 022d93b5d65e0089a6df5f146fa6666b74f83b29 Mon Sep 17 00:00:00 2001 From: Ice Date: Wed, 23 Sep 2026 13:38:53 +0200 Subject: [PATCH 19/27] fix(wallet-hardware): stop forwarding hw-transport's default timeout to DMK The LedgerJS bridge passed hw-transport's 30 s exchange timeout with every APDU. DMK 1.9 ignores it inside device actions, but honouring it would abort signing prompts the user has not confirmed yet. Only an explicit timeout is forwarded now. --- .../src/ledger/helpers/ledgerJsDmkBridge.ts | 4 +++- .../wallet-hardware/test/ledger-js-dmk-bridge.test.ts | 9 +++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/wallet-hardware/src/ledger/helpers/ledgerJsDmkBridge.ts b/packages/wallet-hardware/src/ledger/helpers/ledgerJsDmkBridge.ts index bc56f59..909255e 100644 --- a/packages/wallet-hardware/src/ledger/helpers/ledgerJsDmkBridge.ts +++ b/packages/wallet-hardware/src/ledger/helpers/ledgerJsDmkBridge.ts @@ -83,7 +83,9 @@ export class LedgerJsDmkTransport extends TransportBase { 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); + // Forward only an explicit timeout: hw-transport's 30 s default would cut off a user still confirming + // on the device once DMK honours the argument. + const result = await this.internalApi.sendCommand(command, abortTimeoutMs); if (!dmkModule.isSuccessCommandResult(result)) throw result.error; return Buffer.from(result.data.response); diff --git a/packages/wallet-hardware/test/ledger-js-dmk-bridge.test.ts b/packages/wallet-hardware/test/ledger-js-dmk-bridge.test.ts index 33f700b..5b66ae5 100644 --- a/packages/wallet-hardware/test/ledger-js-dmk-bridge.test.ts +++ b/packages/wallet-hardware/test/ledger-js-dmk-bridge.test.ts @@ -83,6 +83,15 @@ describe("LedgerJS DMK bridge transport", () => { expect([...response]).toEqual([0xde, 0xad, 0x90, 0x00]); }); + it("does not impose hw-transport's default exchange timeout", async () => { + const { abortTimeouts, internalApi } = createInternalApi({ responseData: new Uint8Array() }); + const transport = new LedgerJsDmkTransport(internalApi); + + await transport.exchange(Buffer.from([0xe0, 0x02, 0x00, 0x00, 0x00])); + + expect(abortTimeouts).toEqual([undefined]); + }); + it("rejects malformed short APDUs before touching DMK", async () => { const { internalApi, sendCommand } = createInternalApi(); const transport = new LedgerJsDmkTransport(internalApi); From 353389c90c47b44f30f099494ed1eb8a4ea4d0c2 Mon Sep 17 00:00:00 2001 From: Ice Date: Wed, 23 Sep 2026 13:40:33 +0200 Subject: [PATCH 20/27] fix(wallet-hardware): map Ledger device errors to typed SwapKit errors DMK rejects device actions with plain tagged objects, and the LedgerJS bridge nests hw-app status errors inside them, so a user rejection could not be told apart from a locked device, a wrong app or a lost session. Device action errors now become SwapKitError with wallet_connection_rejected_by_user, wallet_ledger_device_locked, wallet_ledger_app_not_open, wallet_ledger_connection_error or wallet_ledger_transport_error, keeping the DMK tag, status word and message in info. THORChain keeps the status word on its command errors so it maps the same way. --- .../src/ledger/clients/thorchain/index.ts | 8 ++- .../src/ledger/helpers/executeDeviceAction.ts | 68 ++++++++++++++++++- .../wallet-hardware/test/ledger-dmk.test.ts | 27 ++++++-- .../test/ledger-thorchain.test.ts | 32 +++++++-- 4 files changed, 120 insertions(+), 15 deletions(-) diff --git a/packages/wallet-hardware/src/ledger/clients/thorchain/index.ts b/packages/wallet-hardware/src/ledger/clients/thorchain/index.ts index e5bc5ff..e245f50 100644 --- a/packages/wallet-hardware/src/ledger/clients/thorchain/index.ts +++ b/packages/wallet-hardware/src/ledger/clients/thorchain/index.ts @@ -1,7 +1,7 @@ import type { AccountData, AminoSignResponse, StdSignDoc } from "@cosmjs/amino"; import type { Command, DmkError, UserInteractionRequired } from "@ledgerhq/device-management-kit"; import type Transport from "@ledgerhq/hw-transport"; -import { base64 } from "@scure/base"; +import { base64, hex } from "@scure/base"; import { type DerivationPathArray, NetworkDerivationPath, SKConfig, SwapKitError } from "@swapkit/helpers"; import type { LedgerDMKSession } from "../../helpers/dmk"; @@ -148,8 +148,12 @@ export class THORChainLedger { try { return CommandResultFactory({ data: command.parseResponse(response) }); } catch (error) { + // Keep the status word so rejections and wrong-app errors map to typed SwapKit errors. + const errorCode = hex.encode(response.statusCode); return CommandResultFactory({ - error: new InvalidStatusWordError(error instanceof Error ? error.message : String(error)), + error: Object.assign(new InvalidStatusWordError(error instanceof Error ? error.message : String(error)), { + errorCode, + }), }); } }, diff --git a/packages/wallet-hardware/src/ledger/helpers/executeDeviceAction.ts b/packages/wallet-hardware/src/ledger/helpers/executeDeviceAction.ts index b520a74..f2aef9a 100644 --- a/packages/wallet-hardware/src/ledger/helpers/executeDeviceAction.ts +++ b/packages/wallet-hardware/src/ledger/helpers/executeDeviceAction.ts @@ -23,6 +23,70 @@ export const LEDGER_USER_INTERACTION_REQUIRED = { export type LedgerDeviceActionState = DeviceActionState; export type LedgerDeviceActionStateHandler = (state: LedgerDeviceActionState) => void; +const USER_REFUSED_STATUS_WORDS = new Set(["6985", "5501"]); +const LOCKED_STATUS_WORDS = new Set(["5515"]); +const WRONG_APP_STATUS_WORDS = new Set(["6511", "6807", "6d00", "6e00"]); +const DISCONNECTED_ERROR_TAGS = new Set([ + "DeviceDisconnectedBeforeSendingApdu", + "DeviceDisconnectedWhileSendingError", + "DeviceNotRecognizedError", + "DeviceSessionNotFound", + "NoAccessibleDeviceError", + "ReconnectionFailedError", +]); + +type DeviceErrorLike = { + _tag?: unknown; + errorCode?: unknown; + message?: unknown; + originalError?: { message?: unknown; statusCode?: unknown }; +}; + +function statusWordOf(error: DeviceErrorLike) { + if (typeof error.errorCode === "string") return error.errorCode.toLowerCase(); + const statusCode = error.originalError?.statusCode; + return typeof statusCode === "number" ? statusCode.toString(16).padStart(4, "0") : undefined; +} + +/** + * DMK rejects with plain tagged objects (and the LedgerJS bridge nests hw-app status errors inside + * them), so callers could not tell a user rejection from a wrong app or a disconnected device. + */ +export function toLedgerDeviceError(error: unknown) { + if (error instanceof SwapKitError) return error; + + const deviceError: DeviceErrorLike = typeof error === "object" && error !== null ? error : {}; + const errorTag = typeof deviceError._tag === "string" ? deviceError._tag : undefined; + const statusWord = statusWordOf(deviceError); + const message = [deviceError.message, deviceError.originalError?.message].find( + (value): value is string => typeof value === "string" && value.length > 0, + ); + + const errorKey = match({ errorTag, statusWord }) + .when( + ({ errorTag, statusWord }) => + errorTag === "RefusedByUserDAError" || (!!statusWord && USER_REFUSED_STATUS_WORDS.has(statusWord)), + () => "wallet_connection_rejected_by_user" as const, + ) + .when( + ({ errorTag, statusWord }) => + errorTag === "DeviceLockedError" || (!!statusWord && LOCKED_STATUS_WORDS.has(statusWord)), + () => "wallet_ledger_device_locked" as const, + ) + .when( + ({ errorTag, statusWord }) => + errorTag === "UnsupportedApplicationDAError" || (!!statusWord && WRONG_APP_STATUS_WORDS.has(statusWord)), + () => "wallet_ledger_app_not_open" as const, + ) + .when( + ({ errorTag }) => !!errorTag && DISCONNECTED_ERROR_TAGS.has(errorTag), + () => "wallet_ledger_connection_error" as const, + ) + .otherwise(() => "wallet_ledger_transport_error" as const); + + return new SwapKitError({ errorKey, info: { errorTag, message, statusWord } }, error); +} + export function executeLedgerDeviceAction< Output, ActionError, @@ -50,7 +114,7 @@ export function executeLedgerDeviceAction< settle(() => reject(new SwapKitError("wallet_ledger_invalid_response"))); }, error: (error) => { - settle(() => reject(error)); + settle(() => reject(toLedgerDeviceError(error))); }, next: (state) => { try { @@ -61,7 +125,7 @@ export function executeLedgerDeviceAction< 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.Error }, ({ error }) => settle(() => reject(toLedgerDeviceError(error)))) .with({ status: DEVICE_ACTION_STATUS.Stopped }, () => settle(() => reject(new SwapKitError("wallet_ledger_connection_error"))), ) diff --git a/packages/wallet-hardware/test/ledger-dmk.test.ts b/packages/wallet-hardware/test/ledger-dmk.test.ts index 3b6da33..73b6c06 100644 --- a/packages/wallet-hardware/test/ledger-dmk.test.ts +++ b/packages/wallet-hardware/test/ledger-dmk.test.ts @@ -118,11 +118,28 @@ describe("wallet-hardware/ledger DMK", () => { 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 device action errors to typed SwapKit errors", async () => { + const cases = [ + { + error: { _tag: "EthAppCommandError", errorCode: "6985", message: "Denied" }, + errorKey: "wallet_connection_rejected_by_user", + }, + { error: { _tag: "RefusedByUserDAError" }, errorKey: "wallet_connection_rejected_by_user" }, + { + // hw-app status errors reach us nested by the LedgerJS bridge. + error: { _tag: "UnknownDeviceExchangeError", originalError: { statusCode: 0x6985 } }, + errorKey: "wallet_connection_rejected_by_user", + }, + { error: { _tag: "DeviceLockedError" }, errorKey: "wallet_ledger_device_locked" }, + { error: { _tag: "InvalidStatusWordError", errorCode: "6E00" }, errorKey: "wallet_ledger_app_not_open" }, + { error: { _tag: "DeviceSessionNotFound" }, errorKey: "wallet_ledger_connection_error" }, + { error: new Error("Ledger rejected the action"), errorKey: "wallet_ledger_transport_error" }, + ]; + + for (const { error, errorKey } of cases) { + const action = { cancel: mock(() => {}), observable: of({ error, status: DeviceActionStatus.Error }) }; + await expect(executeLedgerDeviceAction({ action })).rejects.toMatchObject({ errorKey }); + } }); it("maps stopped device actions to a SwapKit error", async () => { diff --git a/packages/wallet-hardware/test/ledger-thorchain.test.ts b/packages/wallet-hardware/test/ledger-thorchain.test.ts index 8cdeab5..284cb08 100644 --- a/packages/wallet-hardware/test/ledger-thorchain.test.ts +++ b/packages/wallet-hardware/test/ledger-thorchain.test.ts @@ -40,7 +40,13 @@ interface ThorDmkExchange { p2: number; } -function createThorDmkHarness({ version }: { version: string }) { +function createThorDmkHarness({ + signStatus = [0x90, 0x00], + version, +}: { + signStatus?: [number, number]; + 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"))); @@ -58,9 +64,10 @@ function createThorDmkHarness({ version }: { version: string }) { ? 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), - ); + const statusCode = + apdu.ins === 0x02 && apdu.p1 === 0x02 ? Uint8Array.from(signStatus) : Uint8Array.of(0x90, 0x00); + + return Promise.resolve(command.parseResponse(new ApduResponse({ data, statusCode }), undefined)); }) as InternalApi["sendCommand"]; const internalApi = { getDeviceSessionState: () => ({ currentApp: { name: "THORChain", version } }), @@ -98,6 +105,16 @@ function createThorDmkHarness({ version }: { version: string }) { } describe("ledger THORChain protocol", () => { + it("reports a signature rejected on the device as a user rejection", async () => { + const harness = createThorDmkHarness({ signStatus: [0x69, 0x85], version: "2.2.3" }); + const ledger = new THORChainLedger({ derivationPath: [44, 931, 0, 0, 0], dmkSession: harness.dmkSession }); + + await expect(ledger.sign("x")).rejects.toMatchObject({ + errorKey: "wallet_connection_rejected_by_user", + info: { statusWord: "6985" }, + }); + }); + 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, @@ -294,8 +311,11 @@ describe("ledger THORChain protocol", () => { }); await expect(v1Ledger.sign("x")).rejects.toMatchObject({ - _tag: "InvalidResponseFormatError", - originalError: { message: "THORChain Ledger app major 2 is required, received 1.9.0" }, + errorKey: "wallet_ledger_transport_error", + info: { + errorTag: "InvalidResponseFormatError", + message: "THORChain Ledger app major 2 is required, received 1.9.0", + }, }); expect(v1Harness.executeDeviceAction).toHaveBeenCalledTimes(1); expect(v1Harness.actions[0]?.deviceAction).toBeInstanceOf(CallTaskInAppDeviceAction); From 22c8d86e73d1b041ca927f9e59525c841e4954f0 Mon Sep 17 00:00:00 2001 From: Ice Date: Wed, 23 Sep 2026 13:41:57 +0200 Subject: [PATCH 21/27] fix(wallet-hardware): recover Ledger signers after the device session is lost Clients captured the default DMK session at connect time and cached signer kit instances built from it, so after an unplug or disconnectLedgerDMKSession() every later signature failed with DeviceSessionNotFound until the wallet was connected again. The default session is now resolved per operation (it is re-validated and re-discovered when gone) and signers are rebuilt when it changes. Caller-owned sessions are used as given. --- .../src/ledger/clients/bitcoin.ts | 18 +++++------ .../src/ledger/clients/cosmos.ts | 20 ++++++------- .../wallet-hardware/src/ledger/clients/evm.ts | 25 ++++++++-------- .../src/ledger/clients/thorchain/index.ts | 7 ++--- .../src/ledger/clients/zcash.ts | 17 ++++++----- .../wallet-hardware/src/ledger/helpers/dmk.ts | 28 +++++++++++++++++ .../src/ledger/helpers/getLedgerClient.ts | 12 +++----- .../src/ledger/helpers/ledgerJsDmkBridge.ts | 10 ++----- .../wallet-hardware/test/ledger-dmk.test.ts | 30 ++++++++++++++++++- 9 files changed, 106 insertions(+), 61 deletions(-) diff --git a/packages/wallet-hardware/src/ledger/clients/bitcoin.ts b/packages/wallet-hardware/src/ledger/clients/bitcoin.ts index 6e4cb5b..b0c704d 100644 --- a/packages/wallet-hardware/src/ledger/clients/bitcoin.ts +++ b/packages/wallet-hardware/src/ledger/clients/bitcoin.ts @@ -13,7 +13,7 @@ import type { UTXOType } from "@swapkit/toolboxes/utxo"; import type { Transaction } from "@swapkit/utxo-signer"; import type { LedgerDMKSession } from "../helpers/dmk"; -import { getLedgerDMKSession } from "../helpers/dmk"; +import { createLedgerSessionSigner } from "../helpers/dmk"; import { executeLedgerDeviceAction, type LedgerDeviceActionStateHandler } from "../helpers/executeDeviceAction"; import { createCachedRawTxResolver } from "../helpers/rawTx"; import { BitcoinLedger as LegacyBitcoinLedger } from "./utxo"; @@ -195,7 +195,13 @@ export function BitcoinLedger({ }: BitcoinLedgerParams = {}) { const configuredPath = parseBitcoinPath(derivationPath); const legacyClient = transport ? LegacyBitcoinLedger(derivationPath, transport) : undefined; - let signerPromise: Promise | undefined; + const getSessionSigner = createLedgerSessionSigner({ + build: async (session) => { + const { SignerBtcBuilder } = await import("@ledgerhq/device-signer-kit-bitcoin"); + return new SignerBtcBuilder(session).build(); + }, + dmkSession, + }); let accountXpubPromise: Promise | undefined; let fingerprintPromise: Promise | undefined; @@ -206,13 +212,7 @@ export function BitcoinLedger({ }); } - signerPromise ??= (async () => { - const session = dmkSession ?? (await getLedgerDMKSession()); - const { SignerBtcBuilder } = await import("@ledgerhq/device-signer-kit-bitcoin"); - return new SignerBtcBuilder(session).build(); - })(); - - return signerPromise; + return getSessionSigner(); } function getAccountXpub() { diff --git a/packages/wallet-hardware/src/ledger/clients/cosmos.ts b/packages/wallet-hardware/src/ledger/clients/cosmos.ts index 24b5ae1..e31666d 100644 --- a/packages/wallet-hardware/src/ledger/clients/cosmos.ts +++ b/packages/wallet-hardware/src/ledger/clients/cosmos.ts @@ -11,7 +11,7 @@ import { import type { OfflineAminoSigner } from "@swapkit/toolboxes/cosmos"; import type { LedgerDMKSession } from "../helpers/dmk"; -import { getLedgerDMKSession } from "../helpers/dmk"; +import { createLedgerSessionSigner } from "../helpers/dmk"; import { executeLedgerDeviceAction, type LedgerDeviceActionStateHandler } from "../helpers/executeDeviceAction"; interface CosmosLedgerParams { @@ -57,12 +57,11 @@ async function normalizeCosmosSignature(signature: Uint8Array) { 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; + private readonly getSessionSigner: () => Promise; constructor({ derivationPath = NetworkDerivationPath.GAIA, @@ -71,7 +70,13 @@ export class CosmosLedger { transport, }: CosmosLedgerParams = {}) { this.derivationPath = normalizeCosmosPath(derivationPath); - this.dmkSession = dmkSession; + this.getSessionSigner = createLedgerSessionSigner({ + build: async (session) => { + const { SignerCosmosBuilder } = await import("@ledgerhq/device-signer-kit-cosmos"); + return new SignerCosmosBuilder(session).build(); + }, + dmkSession, + }); this.onDeviceActionState = onDeviceActionState; this.transport = transport; } @@ -83,12 +88,7 @@ export class CosmosLedger { }); } - 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; + return this.getSessionSigner(); } private getLegacyApp() { diff --git a/packages/wallet-hardware/src/ledger/clients/evm.ts b/packages/wallet-hardware/src/ledger/clients/evm.ts index a8a2518..602d652 100644 --- a/packages/wallet-hardware/src/ledger/clients/evm.ts +++ b/packages/wallet-hardware/src/ledger/clients/evm.ts @@ -15,11 +15,11 @@ import { type TypedDataField, } from "ethers"; -import type { LedgerDMKSession } from "../helpers/dmk"; +import { createLedgerSessionSigner, type LedgerDMKSession } from "../helpers/dmk"; import { executeLedgerDeviceAction, type LedgerDeviceActionStateHandler } from "../helpers/executeDeviceAction"; interface EVMLedgerParams { - dmkSession: LedgerDMKSession; + dmkSession?: LedgerDMKSession; provider: Provider; derivationPath?: DerivationPathArray | string; chainId?: ChainId; @@ -60,8 +60,8 @@ function selectTypedDataTypes({ class EVMLedgerInterface extends AbstractSigner { chainId: ChainId = ChainId.Ethereum; derivationPath = ""; - private readonly dmkSession: LedgerDMKSession; - private ledgerSignerPromise?: Promise; + private readonly dmkSession?: LedgerDMKSession; + private readonly getLedgerSigner: () => Promise; private readonly onDeviceActionState?: LedgerDeviceActionStateHandler; private readonly originToken?: string; @@ -82,18 +82,17 @@ class EVMLedgerInterface extends AbstractSigner { this.dmkSession = dmkSession; this.onDeviceActionState = onDeviceActionState; this.originToken = originToken; + this.getLedgerSigner = createLedgerSessionSigner({ + build: async (session) => { + const { SignerEthBuilder } = await import("@ledgerhq/device-signer-kit-ethereum"); + return new SignerEthBuilder({ ...session, originToken }).build(); + }, + dmkSession, + }); 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, @@ -233,7 +232,7 @@ class EVMLedgerInterface extends AbstractSigner { } interface LedgerParams { - dmkSession: LedgerDMKSession; + dmkSession?: LedgerDMKSession; provider: Provider; derivationPath?: DerivationPathArray; onDeviceActionState?: LedgerDeviceActionStateHandler; diff --git a/packages/wallet-hardware/src/ledger/clients/thorchain/index.ts b/packages/wallet-hardware/src/ledger/clients/thorchain/index.ts index e245f50..62d1592 100644 --- a/packages/wallet-hardware/src/ledger/clients/thorchain/index.ts +++ b/packages/wallet-hardware/src/ledger/clients/thorchain/index.ts @@ -4,7 +4,7 @@ import type Transport from "@ledgerhq/hw-transport"; import { base64, hex } from "@scure/base"; import { type DerivationPathArray, NetworkDerivationPath, SKConfig, SwapKitError } from "@swapkit/helpers"; -import type { LedgerDMKSession } from "../../helpers/dmk"; +import { getLedgerDMKSession, type LedgerDMKSession } from "../../helpers/dmk"; import { executeLedgerDeviceAction, LEDGER_USER_INTERACTION_REQUIRED, @@ -92,8 +92,6 @@ export class THORChainLedger { }); } - if (!(dmkSession || transport)) throw new SwapKitError("wallet_ledger_connection_error"); - this.derivationPath = derivationPath; this.dmkSession = dmkSession; this.onDeviceActionState = onDeviceActionState; @@ -125,8 +123,7 @@ export class THORChainLedger { return response; } - const dmkSession = this.dmkSession; - if (!dmkSession) throw new SwapKitError("wallet_ledger_connection_error"); + const dmkSession = this.dmkSession ?? (await getLedgerDMKSession()); const { Apdu, diff --git a/packages/wallet-hardware/src/ledger/clients/zcash.ts b/packages/wallet-hardware/src/ledger/clients/zcash.ts index 418309d..7bf368c 100644 --- a/packages/wallet-hardware/src/ledger/clients/zcash.ts +++ b/packages/wallet-hardware/src/ledger/clients/zcash.ts @@ -11,7 +11,7 @@ import type { UTXOType } from "@swapkit/toolboxes/utxo"; import type { ZcashTransaction } from "@swapkit/utxo-signer"; import type { LedgerDMKSession } from "../helpers/dmk"; -import { getLedgerDMKSession } from "../helpers/dmk"; +import { createLedgerSessionSigner, getLedgerDMKSession } from "../helpers/dmk"; import { executeLedgerDeviceAction, type LedgerDeviceActionStateHandler } from "../helpers/executeDeviceAction"; import { runLedgerJsOperation } from "../helpers/ledgerJsDmkBridge"; import { ZcashLedger as LegacyZcashLedger } from "./utxo"; @@ -294,7 +294,13 @@ export function ZcashLedger({ }: ZcashLedgerParams = {}) { const configuredPath = parseZcashPath(derivationPath); const legacyClient = transport ? LegacyZcashLedger(derivationPath, transport) : undefined; - let signerPromise: Promise | undefined; + const getSessionSigner = createLedgerSessionSigner({ + build: async (session) => { + const { SignerZcashBuilder } = await import("@ledgerhq/device-signer-kit-zcash"); + return new SignerZcashBuilder(session).build(); + }, + dmkSession, + }); function getSigner() { if (legacyClient) { @@ -303,12 +309,7 @@ export function ZcashLedger({ }); } - signerPromise ??= (async () => { - const session = dmkSession ?? (await getLedgerDMKSession()); - const { SignerZcashBuilder } = await import("@ledgerhq/device-signer-kit-zcash"); - return new SignerZcashBuilder(session).build(); - })(); - return signerPromise; + return getSessionSigner(); } async function getDskAddress({ checkOnDevice = false, path = configuredPath.fullPath } = {}) { diff --git a/packages/wallet-hardware/src/ledger/helpers/dmk.ts b/packages/wallet-hardware/src/ledger/helpers/dmk.ts index fb56453..9162d97 100644 --- a/packages/wallet-hardware/src/ledger/helpers/dmk.ts +++ b/packages/wallet-hardware/src/ledger/helpers/dmk.ts @@ -100,6 +100,34 @@ export function getLedgerDMKSession({ dmk }: { dmk?: DeviceManagementKit } = {}) return getValidDefaultSession({ sessionPromise: cachedSessionPromise }); } +/** + * Builds a signer kit instance for the session an operation runs on. A caller-owned session is used + * as given; the default session is re-validated on every call and the signer is rebuilt when it was + * replaced, so a device that was unplugged and reconnected keeps working without a new connect. + */ +export function createLedgerSessionSigner({ + build, + dmkSession, +}: { + build: (session: LedgerDMKSession) => Signer | Promise; + dmkSession?: LedgerDMKSession; +}) { + let cached: { session: LedgerDMKSession; signer: Promise } | undefined; + + return async function getSessionSigner() { + const session = dmkSession ?? (await getLedgerDMKSession()); + if (cached?.session.dmk !== session.dmk || cached.session.sessionId !== session.sessionId) { + const signer = Promise.resolve().then(() => build(session)); + cached = { session, signer }; + const pending = cached; + void signer.catch(() => { + if (cached === pending) cached = undefined; + }); + } + return cached.signer; + }; +} + export function preloadLedgerDMK() { return getDefaultDMK(); } diff --git a/packages/wallet-hardware/src/ledger/helpers/getLedgerClient.ts b/packages/wallet-hardware/src/ledger/helpers/getLedgerClient.ts index 53da31d..d8464a7 100644 --- a/packages/wallet-hardware/src/ledger/helpers/getLedgerClient.ts +++ b/packages/wallet-hardware/src/ledger/helpers/getLedgerClient.ts @@ -32,7 +32,7 @@ import { TronLedger } from "../clients/tron"; 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 { LedgerDMKSession } from "./dmk"; import type { LedgerDeviceActionStateHandler } from "./executeDeviceAction"; type LedgerSignerMap = { @@ -94,10 +94,10 @@ export async function getLedgerClient({ }); } - const resolvedDMKSession = transport ? undefined : (dmkSession ?? (await getLedgerDMKSession())); + // The default session is resolved per operation by each client, so a reconnected device is picked up. const clientParams = { derivationPath: derivationPath ?? NetworkDerivationPath[chain], - dmkSession: resolvedDMKSession, + dmkSession, onDeviceActionState, transport, }; @@ -134,13 +134,9 @@ export async function getLedgerClient({ async () => { const { getProvider } = await import("@swapkit/toolboxes/evm"); - if (!resolvedDMKSession) { - throw new SwapKitError("wallet_ledger_connection_error"); - } - const params = { derivationPath, - dmkSession: resolvedDMKSession, + dmkSession, onDeviceActionState, originToken, provider: await getProvider(chain as EVMChain), diff --git a/packages/wallet-hardware/src/ledger/helpers/ledgerJsDmkBridge.ts b/packages/wallet-hardware/src/ledger/helpers/ledgerJsDmkBridge.ts index 909255e..caec26a 100644 --- a/packages/wallet-hardware/src/ledger/helpers/ledgerJsDmkBridge.ts +++ b/packages/wallet-hardware/src/ledger/helpers/ledgerJsDmkBridge.ts @@ -9,7 +9,7 @@ import type { import Transport, { TransportError } from "@ledgerhq/hw-transport"; import { type DerivationPathArray, SwapKitError } from "@swapkit/helpers"; -import type { LedgerDMKSession } from "./dmk"; +import { getLedgerDMKSession, type LedgerDMKSession } from "./dmk"; import { executeLedgerDeviceAction, LEDGER_USER_INTERACTION_REQUIRED, @@ -148,11 +148,7 @@ export async function runLedgerJsOperation({ 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 session = dmkSession ?? (await getLedgerDMKSession()); const { CallTaskInAppDeviceAction, DmkResultFactory, UnknownDeviceExchangeError } = await import( "@ledgerhq/device-management-kit" @@ -181,7 +177,7 @@ export async function runLedgerJsOperation({ }); const result = await executeLedgerDeviceAction({ - action: dmkSession.dmk.executeDeviceAction({ deviceAction, sessionId: dmkSession.sessionId }), + action: session.dmk.executeDeviceAction({ deviceAction, sessionId: session.sessionId }), onDeviceActionState, }); diff --git a/packages/wallet-hardware/test/ledger-dmk.test.ts b/packages/wallet-hardware/test/ledger-dmk.test.ts index 73b6c06..11b9460 100644 --- a/packages/wallet-hardware/test/ledger-dmk.test.ts +++ b/packages/wallet-hardware/test/ledger-dmk.test.ts @@ -2,7 +2,7 @@ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, mock import * as ledgerDMKModule from "@ledgerhq/device-management-kit"; import { of } from "rxjs"; -import { disconnectLedgerDMKSession, getLedgerDMKSession } from "../src/ledger/helpers/dmk"; +import { createLedgerSessionSigner, disconnectLedgerDMKSession, getLedgerDMKSession } from "../src/ledger/helpers/dmk"; import { executeLedgerDeviceAction } from "../src/ledger/helpers/executeDeviceAction"; type DeviceManagementKit = ledgerDMKModule.DeviceManagementKit; @@ -242,5 +242,33 @@ describe("wallet-hardware/ledger DMK", () => { expect(defaultDMK.disconnect).toHaveBeenCalledTimes(1); expect(defaultDMK.getConnectedDevice).toHaveBeenCalledTimes(2); }); + + it("rebuilds a default-session signer after the device session is lost", async () => { + const build = mock((session: { sessionId: string }) => ({ sessionId: session.sessionId })); + const getSigner = createLedgerSessionSigner({ build }); + + const first = await getSigner(); + expect(await getSigner()).toBe(first); + + // The device was unplugged: DMK no longer knows the session. + connectedSessions.clear(); + const second = await getSigner(); + + expect(first).toEqual({ sessionId: "default-ledger-session-1" }); + expect(second).toEqual({ sessionId: "default-ledger-session-2" }); + expect(build).toHaveBeenCalledTimes(2); + }); + + it("keeps a caller-owned session and builds its signer once", async () => { + const dmkSession = { dmk: defaultDMK as unknown as DeviceManagementKit, sessionId: "caller-session" }; + const build = mock(() => ({})); + const getSigner = createLedgerSessionSigner({ build, dmkSession }); + + await getSigner(); + await getSigner(); + + expect(build).toHaveBeenCalledTimes(1); + expect(lifecycleCalls).toEqual([]); + }); }); }); From bf653fef8c0d0665c1e81b792e02e9d93ef24c45 Mon Sep 17 00:00:00 2001 From: Ice Date: Wed, 23 Sep 2026 13:42:23 +0200 Subject: [PATCH 22/27] fix(wallet-hardware): reject hardened THORChain change and index segments The app-v2 path serialiser cleared the hardened bit on every segment and only re-hardened the first three, so a hardened change or address index silently derived a different key than requested. --- .../src/ledger/clients/thorchain/protocol.ts | 8 ++++++++ packages/wallet-hardware/test/ledger-thorchain.test.ts | 5 +++++ 2 files changed, 13 insertions(+) diff --git a/packages/wallet-hardware/src/ledger/clients/thorchain/protocol.ts b/packages/wallet-hardware/src/ledger/clients/thorchain/protocol.ts index a679be3..74044a4 100644 --- a/packages/wallet-hardware/src/ledger/clients/thorchain/protocol.ts +++ b/packages/wallet-hardware/src/ledger/clients/thorchain/protocol.ts @@ -59,6 +59,14 @@ export function serializeThorPath({ path }: { path: readonly (number | undefined throw new SwapKitError("wallet_ledger_invalid_params", { reason: `Invalid THORChain path segment ${index}` }); } + // The app hardens purpose, coin type and account; a hardened change or index would silently derive + // a different key if its bit were cleared, so reject it instead. + if (index >= 3 && segment >= 0x80000000) { + throw new SwapKitError("wallet_ledger_invalid_params", { + reason: `THORChain path segment ${index} must not be hardened`, + }); + } + const normalized = segment & 0x7fffffff; const value = index < 3 ? (normalized | 0x80000000) >>> 0 : normalized; view.setUint32(index * 4, value, true); diff --git a/packages/wallet-hardware/test/ledger-thorchain.test.ts b/packages/wallet-hardware/test/ledger-thorchain.test.ts index 284cb08..7ccba10 100644 --- a/packages/wallet-hardware/test/ledger-thorchain.test.ts +++ b/packages/wallet-hardware/test/ledger-thorchain.test.ts @@ -121,6 +121,11 @@ describe("ledger THORChain protocol", () => { ]); }); + it("rejects hardened change and address index segments", () => { + expect(() => serializeThorPath({ path: [44, 931, 0, 0x80000000, 0] })).toThrow("wallet_ledger_invalid_params"); + expect(() => serializeThorPath({ path: [44, 931, 0, 0, 0x80000001] })).toThrow("wallet_ledger_invalid_params"); + }); + 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(); From 9aac925e848fd81f4f4c0d4edc380e54d7b29680 Mon Sep 17 00:00:00 2001 From: Ice Date: Wed, 23 Sep 2026 13:43:05 +0200 Subject: [PATCH 23/27] fix(wallet-hardware): normalise Ledger Cosmos and THORChain signatures to low-S Cosmos SDK rejects high-S secp256k1 signatures. The Ledger clients converted the device's DER signature to fixed length without normalising S, relying on firmware behaviour; they now enforce low-S like the keystore signer does. --- .../src/ledger/clients/cosmos.ts | 9 ++++-- .../src/ledger/clients/thorchain/index.ts | 3 +- .../src/ledger/helpers/lowS.ts | 30 +++++++++++++++++++ .../wallet-hardware/test/ledger-low-s.test.ts | 28 +++++++++++++++++ 4 files changed, 66 insertions(+), 4 deletions(-) create mode 100644 packages/wallet-hardware/src/ledger/helpers/lowS.ts create mode 100644 packages/wallet-hardware/test/ledger-low-s.test.ts diff --git a/packages/wallet-hardware/src/ledger/clients/cosmos.ts b/packages/wallet-hardware/src/ledger/clients/cosmos.ts index e31666d..f258b4b 100644 --- a/packages/wallet-hardware/src/ledger/clients/cosmos.ts +++ b/packages/wallet-hardware/src/ledger/clients/cosmos.ts @@ -13,6 +13,7 @@ import type { OfflineAminoSigner } from "@swapkit/toolboxes/cosmos"; import type { LedgerDMKSession } from "../helpers/dmk"; import { createLedgerSessionSigner } from "../helpers/dmk"; import { executeLedgerDeviceAction, type LedgerDeviceActionStateHandler } from "../helpers/executeDeviceAction"; +import { toLowSSignature } from "../helpers/lowS"; interface CosmosLedgerParams { derivationPath?: DerivationPathArray | string; @@ -46,9 +47,11 @@ function normalizeCosmosPath(path: DerivationPathArray | string) { async function normalizeCosmosSignature(signature: Uint8Array) { const { Secp256k1Signature } = await import("@cosmjs/crypto"); try { - return signature.length === 64 - ? Secp256k1Signature.fromFixedLength(signature).toFixedLength() - : Secp256k1Signature.fromDer(signature).toFixedLength(); + return toLowSSignature( + signature.length === 64 + ? Secp256k1Signature.fromFixedLength(signature).toFixedLength() + : Secp256k1Signature.fromDer(signature).toFixedLength(), + ); } catch (error) { throw new SwapKitError("wallet_ledger_invalid_response", error); } diff --git a/packages/wallet-hardware/src/ledger/clients/thorchain/index.ts b/packages/wallet-hardware/src/ledger/clients/thorchain/index.ts index 62d1592..f79e0c7 100644 --- a/packages/wallet-hardware/src/ledger/clients/thorchain/index.ts +++ b/packages/wallet-hardware/src/ledger/clients/thorchain/index.ts @@ -10,6 +10,7 @@ import { LEDGER_USER_INTERACTION_REQUIRED, type LedgerDeviceActionStateHandler, } from "../../helpers/executeDeviceAction"; +import { toLowSSignature } from "../../helpers/lowS"; import { getThorAddressCommand, getThorLegacyVersion, @@ -67,7 +68,7 @@ export function normalizeThorchainLedgerSignDoc(signDoc: StdSignDoc): StdSignDoc async function getFixedSignature({ signature }: { signature: Uint8Array }) { const { Secp256k1Signature } = await import("@cosmjs/crypto"); try { - return Secp256k1Signature.fromDer(signature).toFixedLength(); + return toLowSSignature(Secp256k1Signature.fromDer(signature).toFixedLength()); } catch (error) { throw new SwapKitError("wallet_ledger_invalid_signature", error); } diff --git a/packages/wallet-hardware/src/ledger/helpers/lowS.ts b/packages/wallet-hardware/src/ledger/helpers/lowS.ts new file mode 100644 index 0000000..ed3dc8f --- /dev/null +++ b/packages/wallet-hardware/src/ledger/helpers/lowS.ts @@ -0,0 +1,30 @@ +const SECP256K1_ORDER = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141n; +const HALF_ORDER = SECP256K1_ORDER >> 1n; + +function bytesToBigInt(bytes: Uint8Array) { + return bytes.reduce((value, byte) => (value << 8n) | BigInt(byte), 0n); +} + +function bigIntTo32Bytes(value: bigint) { + const bytes = new Uint8Array(32); + let remaining = value; + for (let index = 31; index >= 0; index -= 1) { + bytes[index] = Number(remaining & 0xffn); + remaining >>= 8n; + } + return bytes; +} + +/** + * Cosmos SDK rejects high-S secp256k1 signatures. Ledger firmware should already return low-S, but + * normalising a fixed-length `r || s` signature here removes the dependency on it. + */ +export function toLowSSignature(signature: Uint8Array) { + const s = bytesToBigInt(signature.subarray(32, 64)); + if (s <= HALF_ORDER) return signature; + + const normalized = new Uint8Array(64); + normalized.set(signature.subarray(0, 32), 0); + normalized.set(bigIntTo32Bytes(SECP256K1_ORDER - s), 32); + return normalized; +} diff --git a/packages/wallet-hardware/test/ledger-low-s.test.ts b/packages/wallet-hardware/test/ledger-low-s.test.ts new file mode 100644 index 0000000..e599e31 --- /dev/null +++ b/packages/wallet-hardware/test/ledger-low-s.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, it } from "bun:test"; +import { hex } from "@scure/base"; + +import { toLowSSignature } from "../src/ledger/helpers/lowS"; + +const ORDER = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141n; + +function signature({ r, s }: { r: bigint; s: bigint }) { + return hex.decode(`${r.toString(16).padStart(64, "0")}${s.toString(16).padStart(64, "0")}`); +} + +describe("toLowSSignature", () => { + const r = 0x1234n; + + it("keeps low-S signatures unchanged, including s = n / 2", () => { + for (const s of [1n, ORDER >> 1n]) { + const lowS = signature({ r, s }); + expect(toLowSSignature(lowS)).toBe(lowS); + } + }); + + it("replaces a high s with n - s and keeps r", () => { + const s = 0xabcdefn; + + expect(toLowSSignature(signature({ r, s: ORDER - s }))).toEqual(signature({ r, s })); + expect(toLowSSignature(signature({ r, s: (ORDER >> 1n) + 1n }))).toEqual(signature({ r, s: ORDER >> 1n })); + }); +}); From d764f7624b84d9e25db8ddbfb3e88d8e1648e754 Mon Sep 17 00:00:00 2001 From: Ice Date: Wed, 23 Sep 2026 13:43:16 +0200 Subject: [PATCH 24/27] chore(playground): drop removed Ledger transports from Vite optimizeDeps @ledgerhq/hw-transport-webhid and -webusb are no longer installed after the DMK migration, so Vite could not resolve them on a fresh install. --- playgrounds/vite-lite/vite.config.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/playgrounds/vite-lite/vite.config.ts b/playgrounds/vite-lite/vite.config.ts index 929de7a..05c4381 100644 --- a/playgrounds/vite-lite/vite.config.ts +++ b/playgrounds/vite-lite/vite.config.ts @@ -47,8 +47,6 @@ export default defineConfig({ "@ledgerhq/devices", "@ledgerhq/errors", "@ledgerhq/hw-app-btc", - "@ledgerhq/hw-transport-webhid", - "@ledgerhq/hw-transport-webusb", // CJS deps dynamically imported with NAMED imports by the MetaMask connect // SDK; without listing them esbuild emits default-only interop and the // mobile/QR flow throws on undefined named members. From afa8dbc91f829cb7c83ebdf9e7a48c6aa73467ee Mon Sep 17 00:00:00 2001 From: Ice Date: Wed, 23 Sep 2026 13:43:55 +0200 Subject: [PATCH 25/27] docs(wallet-hardware): document Ledger DMK breaking changes and downstream bumps @swapkit/wallets and @swapkit/sdk re-export the Ledger wallet, so they now get a minor bump instead of an automatic patch. The changeset also lists the WebUSB removal, the typed device errors, the transport restrictions, the new exports and the signing fixes. --- .changeset/ledger-evm-dmk.md | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/.changeset/ledger-evm-dmk.md b/.changeset/ledger-evm-dmk.md index 6a72791..bdfde73 100644 --- a/.changeset/ledger-evm-dmk.md +++ b/.changeset/ledger-evm-dmk.md @@ -1,13 +1,24 @@ --- "@swapkit/wallet-hardware": major +"@swapkit/wallets": minor +"@swapkit/sdk": minor --- -Migrate Ledger connections to the Device Management Kit by default. +Migrate Ledger connections to the Device Management Kit by default. `@swapkit/wallets` and `@swapkit/sdk` re-export the Ledger wallet, so they carry these changes too. -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. +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. New exports: `getLedgerDMKSession`, `preloadLedgerDMK`, `disconnectLedgerDMKSession` and the `LedgerDMKSession`, `LedgerDeviceActionState` and `LedgerDeviceActionStateHandler` types. -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`. +Breaking changes: -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. +- WebUSB is no longer used. The default connection requires WebHID, so browsers without it (Android Chrome) can no longer connect a Ledger, and opening the device picker needs a user gesture. Call `preloadLedgerDMK()` ahead of the click to keep the gesture window. +- Device errors are now `SwapKitError`s: `wallet_connection_rejected_by_user`, `wallet_ledger_device_locked`, `wallet_ledger_app_not_open`, `wallet_ledger_connection_error` or `wallet_ledger_transport_error`, with the DMK tag, status word and message in `info`. Code that matched hw-transport's `TransportStatusError` or `statusCode` must switch to `errorKey`. +- EVM chains require a DMK session; passing a LedgerJS `transport` for them throws `wallet_ledger_invalid_params`. The injected `transport` remains a temporary compatibility path for other chains, cannot be combined with `dmkSession`, and no longer signs Zcash (hw-app-btc picks a pre-Ironwood branch id). Bitcoin `signTransaction` over it returns the finalised transaction from the legacy app, like the other legacy UTXO chains. +- Legacy Zcash PCZT v4 signing is no longer 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. +EVM, Bitcoin, Cosmos and transparent Zcash signing use the Ledger Device Signer Kits (the Zcash kit is still pre-1.0). THORChain uses its app-v2 protocol in one atomic DMK action, and chains without a signer kit run their existing Ledger app client through an operation-scoped DMK bridge. A default session that was lost (device unplugged, `disconnectLedgerDMKSession()`) is re-established on the next operation instead of failing until the wallet reconnects. + +EVM message, transaction and full EIP-712 signing use the current signer APIs, sending only the types the primary type references. Arc keeps Ledger support: the Ethereum signer kit loads the network certificate and Ledger-signed network descriptor itself, replacing the `PROVIDE_NETWORK_INFORMATION` APDUs added in 4.11.0. When clear-signing metadata cannot be loaded or is rejected, the signer kit falls back to blind signing on every chain rather than only on Arc; the device still requires blind signing to be enabled and the user to confirm. + +Bitcoin requests PSBT signatures from the signer kit and finalises the transaction locally, keeping the transaction version the toolbox built. Inputs carry the full previous transaction, checked against the input txid and fetched when missing, so the device can verify input amounts; nested SegWit inputs also carry the spent output. Zcash transfer signing produces the transparent v5 transaction required by the current Zcash app. + +Cosmos and THORChain signatures are normalised to low-S, THORChain rejects hardened change or address-index path segments instead of silently clearing them, and memo (OP_RETURN) outputs no longer break legacy UTXO signing. THORChain Amino signing keeps a compatibility normaliser for object-shaped deposit assets while signing the canonical string form expected on-chain. From e835e030cacdb2f8b91ec8ab91ef66626b84ab7c Mon Sep 17 00:00:00 2001 From: Ice Date: Wed, 23 Sep 2026 15:53:38 +0200 Subject: [PATCH 26/27] fix(wallet-hardware): disable Ledger blind-signing telemetry The Ethereum signer kit's default context module posts the chain, target address, device model and blind-signing status to Ledger on every signature. Build the same context module ourselves with a no-op blind-signing reporter. --- .changeset/ledger-evm-dmk.md | 2 +- .../wallet-hardware/src/ledger/clients/evm.ts | 23 ++++++- .../test/ledger-transport.test.ts | 67 ++++++++++++++++++- 3 files changed, 88 insertions(+), 4 deletions(-) diff --git a/.changeset/ledger-evm-dmk.md b/.changeset/ledger-evm-dmk.md index bdfde73..d4db712 100644 --- a/.changeset/ledger-evm-dmk.md +++ b/.changeset/ledger-evm-dmk.md @@ -17,7 +17,7 @@ Breaking changes: EVM, Bitcoin, Cosmos and transparent Zcash signing use the Ledger Device Signer Kits (the Zcash kit is still pre-1.0). THORChain uses its app-v2 protocol in one atomic DMK action, and chains without a signer kit run their existing Ledger app client through an operation-scoped DMK bridge. A default session that was lost (device unplugged, `disconnectLedgerDMKSession()`) is re-established on the next operation instead of failing until the wallet reconnects. -EVM message, transaction and full EIP-712 signing use the current signer APIs, sending only the types the primary type references. Arc keeps Ledger support: the Ethereum signer kit loads the network certificate and Ledger-signed network descriptor itself, replacing the `PROVIDE_NETWORK_INFORMATION` APDUs added in 4.11.0. When clear-signing metadata cannot be loaded or is rejected, the signer kit falls back to blind signing on every chain rather than only on Arc; the device still requires blind signing to be enabled and the user to confirm. +EVM message, transaction and full EIP-712 signing use the current signer APIs, sending only the types the primary type references. Arc keeps Ledger support: the Ethereum signer kit loads the network certificate and Ledger-signed network descriptor itself, replacing the `PROVIDE_NETWORK_INFORMATION` APDUs added in 4.11.0. When clear-signing metadata cannot be loaded or is rejected, the signer kit falls back to blind signing on every chain rather than only on Arc; the device still requires blind signing to be enabled and the user to confirm. The signer kit's blind-signing telemetry, which reports the chain, target address and device model to Ledger on every signature, is disabled. Bitcoin requests PSBT signatures from the signer kit and finalises the transaction locally, keeping the transaction version the toolbox built. Inputs carry the full previous transaction, checked against the input txid and fetched when missing, so the device can verify input amounts; nested SegWit inputs also carry the spent output. Zcash transfer signing produces the transparent v5 transaction required by the current Zcash app. diff --git a/packages/wallet-hardware/src/ledger/clients/evm.ts b/packages/wallet-hardware/src/ledger/clients/evm.ts index 602d652..8e5c13d 100644 --- a/packages/wallet-hardware/src/ledger/clients/evm.ts +++ b/packages/wallet-hardware/src/ledger/clients/evm.ts @@ -1,3 +1,4 @@ +import type { BlindSigningReporter } from "@ledgerhq/context-module"; import type { TypedDataDomain as LedgerTypedDataDomain, SignerEth } from "@ledgerhq/device-signer-kit-ethereum"; import { ChainId, @@ -57,6 +58,11 @@ function selectTypedDataTypes({ return selectedTypes; } +const disabledBlindSigningReporter: BlindSigningReporter = { + // The context module ignores the result; the cast avoids depending on purify-ts for an unused Either. + report: () => Promise.resolve(undefined as unknown as Awaited>), +}; + class EVMLedgerInterface extends AbstractSigner { chainId: ChainId = ChainId.Ethereum; derivationPath = ""; @@ -84,8 +90,21 @@ class EVMLedgerInterface extends AbstractSigner { this.originToken = originToken; this.getLedgerSigner = createLedgerSessionSigner({ build: async (session) => { - const { SignerEthBuilder } = await import("@ledgerhq/device-signer-kit-ethereum"); - return new SignerEthBuilder({ ...session, originToken }).build(); + const [{ ContextModuleBuilder, ContextModuleChainID }, { SignerEthBuilder }] = await Promise.all([ + import("@ledgerhq/context-module"), + import("@ledgerhq/device-signer-kit-ethereum"), + ]); + // Same context module the signer kit builds by default, minus the blind-signing reporter that + // would post chain, target address and device model to Ledger on every signature. + const contextModule = new ContextModuleBuilder({ + loggerFactory: (tag) => session.dmk.getLoggerFactory()(["ContextModule", tag]), + originToken, + }) + .setChain(ContextModuleChainID.Ethereum) + .setBlindSigningReporter(disabledBlindSigningReporter) + .build(); + + return new SignerEthBuilder({ ...session, originToken }).withContextModule(contextModule).build(); }, dmkSession, }); diff --git a/packages/wallet-hardware/test/ledger-transport.test.ts b/packages/wallet-hardware/test/ledger-transport.test.ts index f64fddc..99fe4bd 100644 --- a/packages/wallet-hardware/test/ledger-transport.test.ts +++ b/packages/wallet-hardware/test/ledger-transport.test.ts @@ -1,4 +1,5 @@ -import { beforeEach, describe, expect, it, mock } from "bun:test"; +import { afterAll, beforeEach, describe, expect, it, mock } from "bun:test"; +import * as realContextModule from "@ledgerhq/context-module"; import { DeviceActionStatus, type DeviceManagementKit } from "@ledgerhq/device-management-kit"; import type Transport from "@ledgerhq/hw-transport"; import { Chain, WalletOption } from "@swapkit/helpers"; @@ -57,11 +58,49 @@ mock.module("@ledgerhq/hw-app-btc", () => ({ }, })); +type MockContextModuleConfig = { + blindSigningReporter?: { report: (params: unknown) => Promise }; + chain?: unknown; + originToken?: string; +}; +const contextModuleConfigs: MockContextModuleConfig[] = []; +const ethereumContextModules: unknown[] = []; + +// mock.module replaces the whole export namespace process-wide; snapshot the real module so +// afterAll can restore it for test files that run later. +const realContextModuleSnapshot = { ...realContextModule }; + +mock.module("@ledgerhq/context-module", () => ({ + ...realContextModuleSnapshot, + ContextModuleBuilder: class MockContextModuleBuilder { + private readonly config: MockContextModuleConfig; + constructor({ originToken }: { originToken?: string }) { + this.config = { originToken }; + } + setChain(chain: unknown) { + this.config.chain = chain; + return this; + } + setBlindSigningReporter(reporter: MockContextModuleConfig["blindSigningReporter"]) { + this.config.blindSigningReporter = reporter; + return this; + } + build() { + contextModuleConfigs.push(this.config); + return this.config; + } + }, +})); + mock.module("@ledgerhq/device-signer-kit-ethereum", () => ({ SignerEthBuilder: class MockSignerEthBuilder { constructor(params: { dmk: unknown; originToken?: string; sessionId: string }) { ethereumBuilderInvocations.push(params); } + withContextModule(contextModule: unknown) { + ethereumContextModules.push(contextModule); + return this; + } build = () => ({ getAddress: (path: string, options?: { chainId?: number; checkOnDevice?: boolean }) => { ethereumGetAddressInvocations.push({ options, path }); @@ -110,6 +149,8 @@ describe("wallet-hardware/ledger", () => { bitcoinAppInvocations.length = 0; bitcoinAppXpubInvocations.length = 0; ethereumBuilderInvocations.length = 0; + contextModuleConfigs.length = 0; + ethereumContextModules.length = 0; ethereumGetAddressInvocations.length = 0; ethereumSignMessageInvocations.length = 0; ethereumSignTransactionInvocations.length = 0; @@ -186,6 +227,16 @@ describe("wallet-hardware/ledger", () => { expect(ethereumBuilderInvocations).toEqual([ { dmk: dmkSession.dmk, originToken: "ledger-origin-token", sessionId: "test-session" }, ]); + // Blind-signing telemetry is replaced with a reporter that never reaches the network. + expect(contextModuleConfigs).toEqual([ + { + blindSigningReporter: expect.any(Object), + chain: realContextModule.ContextModuleChainID.Ethereum, + originToken: "ledger-origin-token", + }, + ]); + expect(ethereumContextModules).toEqual([contextModuleConfigs[0]]); + await expect(contextModuleConfigs[0]?.blindSigningReporter?.report({})).resolves.toBeUndefined(); expect(ethereumGetAddressInvocations).toEqual([{ options: { chainId: 42161 }, path: "44'/60'/0'/0/0" }]); expect(ethereumSignTransactionInvocations).toHaveLength(1); expect(hexlify(ethereumSignTransactionInvocations[0]?.transaction ?? new Uint8Array())).toBe( @@ -449,6 +500,20 @@ describe("wallet-hardware/ledger", () => { expect(ethereumBuilderInvocations).toEqual([ { dmk: dmkSession.dmk, originToken: "ledger-origin-token", sessionId: "test-session" }, ]); + // Blind-signing telemetry is replaced with a reporter that never reaches the network. + expect(contextModuleConfigs).toEqual([ + { + blindSigningReporter: expect.any(Object), + chain: realContextModule.ContextModuleChainID.Ethereum, + originToken: "ledger-origin-token", + }, + ]); + expect(ethereumContextModules).toEqual([contextModuleConfigs[0]]); + await expect(contextModuleConfigs[0]?.blindSigningReporter?.report({})).resolves.toBeUndefined(); expect(ethereumGetAddressInvocations).toEqual([{ options: { chainId: 42161 }, path: "44'/60'/0'/0/0" }]); }); }); + +afterAll(() => { + mock.module("@ledgerhq/context-module", () => realContextModuleSnapshot); +}); From c00e582fea45a8b5023d3c2f4a5fffebcef666f6 Mon Sep 17 00:00:00 2001 From: Ice Date: Wed, 23 Sep 2026 16:09:21 +0200 Subject: [PATCH 27/27] fix(wallet-hardware): pay the toolbox Cosmos fee for Ledger transfers The Ledger ATOM transfer paid getDefaultChainFee(Chain.Cosmos), 500 uatom for 200000 gas (0.0025 uatom/gas), below the Cosmos Hub feemarket minimum of 0.005 uatom/gas, so the node rejected every transfer. Use the same fee as the toolbox transfer (SwapKit gas rate per fee option, or the caller's feeRate). The 0.007uatom gas price argument was ignored by createSigningStargateClient and is dropped. --- packages/wallet-hardware/src/ledger/index.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/wallet-hardware/src/ledger/index.ts b/packages/wallet-hardware/src/ledger/index.ts index a635216..f9cb2af 100644 --- a/packages/wallet-hardware/src/ledger/index.ts +++ b/packages/wallet-hardware/src/ledger/index.ts @@ -608,7 +608,13 @@ async function getCosmosWalletMethods({ const address = await getLedgerAddress({ chain, ledgerClient: signer }); const toolbox = await getCosmosToolbox(Chain.Cosmos, { signer }); - const transfer = async ({ assetValue, recipient, memo }: GenericTransferParams) => { + const transfer = async ({ + assetValue, + feeOptionKey = FeeOption.Average, + feeRate, + memo, + recipient, + }: GenericTransferParams) => { if (!assetValue) throw new SwapKitError("wallet_ledger_invalid_asset"); const sendCoinsMessage = { @@ -619,13 +625,16 @@ async function getCosmosWalletMethods({ toAddress: recipient, }; + // Same fee as the toolbox's own transfer. The static default (0.0025 uatom/gas) is below the Hub's + // feemarket minimum of 0.005 uatom/gas, so every transfer paying it was rejected. + const fee = feeRate ?? (await toolbox.getFees())[feeOptionKey].getBaseValue("number"); const rpcUrl = await getRPCUrl(chain); - const signingClient = await createSigningStargateClient(rpcUrl, signer, "0.007uatom"); + const signingClient = await createSigningStargateClient(rpcUrl, signer); const { transactionHash } = await signingClient.signAndBroadcast( address, [{ typeUrl: "/cosmos.bank.v1beta1.MsgSend", value: sendCoinsMessage }], - getDefaultChainFee(Chain.Cosmos), + { amount: [{ amount: fee.toString(), denom: "uatom" }], gas: getDefaultChainFee(Chain.Cosmos).gas }, memo, );