@sidhujag/sysweb3-keyring 1.0.628 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +174 -0
  3. package/cjs/bip84-native.js +8 -0
  4. package/cjs/bip84-native.js.map +1 -0
  5. package/cjs/hardware-wallet-manager.js +302 -128
  6. package/cjs/hardware-wallet-manager.js.map +1 -1
  7. package/cjs/keyring-core.js +319 -0
  8. package/cjs/keyring-core.js.map +1 -1
  9. package/cjs/keyring-manager.js +9 -3
  10. package/cjs/keyring-manager.js.map +1 -1
  11. package/cjs/ledger/bitcoin_client/lib/appClient.js.map +1 -1
  12. package/cjs/ledger/host-types.js +10 -0
  13. package/cjs/ledger/host-types.js.map +1 -0
  14. package/cjs/ledger/index.js +243 -447
  15. package/cjs/ledger/index.js.map +1 -1
  16. package/cjs/ledger/prepare-psbt.js +177 -0
  17. package/cjs/ledger/prepare-psbt.js.map +1 -0
  18. package/cjs/public-bip84.js +157 -0
  19. package/cjs/public-bip84.js.map +1 -0
  20. package/cjs/software-hd-account.js +23 -0
  21. package/cjs/software-hd-account.js.map +1 -0
  22. package/cjs/software-utxo-account.js +12 -0
  23. package/cjs/software-utxo-account.js.map +1 -0
  24. package/cjs/software.js +24 -0
  25. package/cjs/software.js.map +1 -1
  26. package/cjs/transactions/bip84-native-plan.js +481 -0
  27. package/cjs/transactions/bip84-native-plan.js.map +1 -0
  28. package/cjs/transactions/ethereum.js +55 -10
  29. package/cjs/transactions/ethereum.js.map +1 -1
  30. package/cjs/transactions/hardware-psbt.js +36 -44
  31. package/cjs/transactions/hardware-psbt.js.map +1 -1
  32. package/cjs/transactions/prepared-bip84.js +214 -0
  33. package/cjs/transactions/prepared-bip84.js.map +1 -0
  34. package/cjs/transactions/prepared-evm.js +197 -0
  35. package/cjs/transactions/prepared-evm.js.map +1 -0
  36. package/cjs/trezor/connect.js +243 -0
  37. package/cjs/trezor/connect.js.map +1 -0
  38. package/cjs/trezor/index.js +96 -58
  39. package/cjs/trezor/index.js.map +1 -1
  40. package/cjs/trezor/witness.js +25 -0
  41. package/cjs/trezor/witness.js.map +1 -0
  42. package/cjs/utils/psbt-account-scope.js +5 -3
  43. package/cjs/utils/psbt-account-scope.js.map +1 -1
  44. package/esm/errorUtils.js +72 -0
  45. package/esm/errorUtils.js.map +1 -0
  46. package/esm/ethers-v6.js +207 -0
  47. package/esm/ethers-v6.js.map +1 -0
  48. package/esm/hardware-wallet-manager.js +643 -0
  49. package/esm/hardware-wallet-manager.js.map +1 -0
  50. package/esm/ledger/bitcoin_client/lib/appClient.js +279 -0
  51. package/esm/ledger/bitcoin_client/lib/appClient.js.map +1 -0
  52. package/esm/ledger/bitcoin_client/lib/bip32.js +49 -0
  53. package/esm/ledger/bitcoin_client/lib/bip32.js.map +1 -0
  54. package/esm/ledger/bitcoin_client/lib/buffertools.js +119 -0
  55. package/esm/ledger/bitcoin_client/lib/buffertools.js.map +1 -0
  56. package/esm/ledger/bitcoin_client/lib/clientCommands.js +269 -0
  57. package/esm/ledger/bitcoin_client/lib/clientCommands.js.map +1 -0
  58. package/esm/ledger/bitcoin_client/lib/merkelizedPsbt.js +50 -0
  59. package/esm/ledger/bitcoin_client/lib/merkelizedPsbt.js.map +1 -0
  60. package/esm/ledger/bitcoin_client/lib/merkle.js +111 -0
  61. package/esm/ledger/bitcoin_client/lib/merkle.js.map +1 -0
  62. package/esm/ledger/bitcoin_client/lib/merkleMap.js +42 -0
  63. package/esm/ledger/bitcoin_client/lib/merkleMap.js.map +1 -0
  64. package/esm/ledger/bitcoin_client/lib/policy.js +68 -0
  65. package/esm/ledger/bitcoin_client/lib/policy.js.map +1 -0
  66. package/esm/ledger/bitcoin_client/lib/psbtv2.js +645 -0
  67. package/esm/ledger/bitcoin_client/lib/psbtv2.js.map +1 -0
  68. package/esm/ledger/bitcoin_client/lib/varint.js +108 -0
  69. package/esm/ledger/bitcoin_client/lib/varint.js.map +1 -0
  70. package/esm/ledger/consts.js +4 -0
  71. package/esm/ledger/consts.js.map +1 -0
  72. package/esm/ledger/dmk-evm.js +258 -0
  73. package/esm/ledger/dmk-evm.js.map +1 -0
  74. package/esm/ledger/dmk-session.js +264 -0
  75. package/esm/ledger/dmk-session.js.map +1 -0
  76. package/esm/ledger/dmk-utxo.js +279 -0
  77. package/esm/ledger/dmk-utxo.js.map +1 -0
  78. package/esm/ledger/host-types.js +5 -0
  79. package/esm/ledger/host-types.js.map +1 -0
  80. package/esm/ledger/index.js +268 -0
  81. package/esm/ledger/index.js.map +1 -0
  82. package/esm/ledger/prepare-psbt.js +141 -0
  83. package/esm/ledger/prepare-psbt.js.map +1 -0
  84. package/esm/ledger/types.js +2 -0
  85. package/esm/ledger/types.js.map +1 -0
  86. package/esm/ledger-host.js +36 -0
  87. package/esm/ledger-host.js.map +1 -0
  88. package/esm/private-records.js +153 -0
  89. package/esm/private-records.js.map +1 -0
  90. package/esm/providers.js +588 -0
  91. package/esm/providers.js.map +1 -0
  92. package/esm/signers.js +28 -0
  93. package/esm/signers.js.map +1 -0
  94. package/esm/trezor/connect.js +206 -0
  95. package/esm/trezor/connect.js.map +1 -0
  96. package/esm/trezor/index.js +876 -0
  97. package/esm/trezor/index.js.map +1 -0
  98. package/esm/trezor/witness.js +21 -0
  99. package/esm/trezor/witness.js.map +1 -0
  100. package/esm/types.js +17 -0
  101. package/esm/types.js.map +1 -0
  102. package/esm/utils/derivation-paths.js +109 -0
  103. package/esm/utils/derivation-paths.js.map +1 -0
  104. package/esm/utils/psbt-account-scope.js +310 -0
  105. package/esm/utils/psbt-account-scope.js.map +1 -0
  106. package/package.json +34 -9
  107. package/types/bip84-native.d.ts +3 -0
  108. package/types/hardware-wallet-manager.d.ts +12 -19
  109. package/types/keyring-core.d.ts +14 -0
  110. package/types/keyring-manager.d.ts +2 -1
  111. package/types/ledger/bitcoin_client/lib/appClient.d.ts +6 -3
  112. package/types/ledger/dmk-evm.d.ts +8 -0
  113. package/types/ledger/dmk-session.d.ts +60 -0
  114. package/types/ledger/dmk-utxo.d.ts +26 -0
  115. package/types/ledger/host-types.d.ts +131 -0
  116. package/types/ledger/index.d.ts +21 -58
  117. package/types/ledger/prepare-psbt.d.ts +3 -0
  118. package/types/ledger/types.d.ts +3 -0
  119. package/types/ledger-host.d.ts +7 -0
  120. package/types/public-bip84.d.ts +39 -0
  121. package/types/software-hd-account.d.ts +16 -0
  122. package/types/software-utxo-account.d.ts +13 -0
  123. package/types/software.d.ts +26 -0
  124. package/types/transactions/bip84-native-plan.d.ts +83 -0
  125. package/types/transactions/ethereum.d.ts +1 -1
  126. package/types/transactions/prepared-bip84.d.ts +27 -0
  127. package/types/transactions/prepared-evm.d.ts +53 -0
  128. package/types/trezor/connect.d.ts +42 -0
  129. package/types/trezor/index.d.ts +22 -4
  130. package/types/trezor/witness.d.ts +3 -0
  131. package/types/utils/psbt-account-scope.d.ts +1 -0
@@ -1,475 +1,271 @@
1
1
  "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
- var __importDefault = (this && this.__importDefault) || function (mod) {
36
- return (mod && mod.__esModule) ? mod : { "default": mod };
37
- };
38
2
  Object.defineProperty(exports, "__esModule", { value: true });
39
3
  exports.LedgerKeyring = void 0;
40
- const bitcoin_client_1 = __importStar(require("./bitcoin_client"));
41
- const consts_1 = require("./consts");
42
- const sysweb3_network_1 = require("@sidhujag/sysweb3-network");
43
- const bip32_1 = __importDefault(require("bip32"));
44
- const secp256k1_1 = __importDefault(require("@bitcoinerlab/secp256k1"));
45
- const hw_app_eth_1 = __importStar(require("@ledgerhq/hw-app-eth"));
46
- const syscoinjs_lib_1 = require("syscoinjs-lib");
47
4
  const eth_sig_util_1 = require("@metamask/eth-sig-util");
48
- const derivation_paths_1 = require("../utils/derivation-paths");
5
+ const ethers_1 = require("ethers");
49
6
  const hardware_wallet_manager_1 = require("../hardware-wallet-manager");
50
- //
7
+ const host_types_1 = require("./host-types");
8
+ const invalidRequest = () => (0, host_types_1.ledgerHostError)('LEDGER_INVALID_REQUEST', 'Invalid Ledger signing request');
9
+ const invalidSignature = () => (0, host_types_1.ledgerHostError)('LEDGER_INVALID_SIGNATURE', 'Ledger returned an invalid signature for this request');
10
+ const accountIndex = (value) => {
11
+ if (!Number.isSafeInteger(value) || value < 0 || value > 0x7fffffff)
12
+ throw invalidRequest();
13
+ return value;
14
+ };
15
+ const hexBytes = (value) => {
16
+ if (typeof value !== 'string' || !/^(?:0x)?(?:[0-9a-f]{2})*$/i.test(value))
17
+ throw invalidRequest();
18
+ return Uint8Array.from(Buffer.from(value.replace(/^0x/i, ''), 'hex'));
19
+ };
20
+ const approvedAddress = (value) => {
21
+ try {
22
+ return (0, ethers_1.getAddress)(value);
23
+ }
24
+ catch {
25
+ throw invalidRequest();
26
+ }
27
+ };
28
+ const checkedSignature = (value, digest, expected) => {
29
+ try {
30
+ let signature;
31
+ if (typeof value === 'string') {
32
+ if (!/^0x[0-9a-f]{130}$/i.test(value))
33
+ throw invalidSignature();
34
+ signature = ethers_1.Signature.from(value);
35
+ }
36
+ else {
37
+ if (!value ||
38
+ !/^[0-9a-f]{64}$/i.test(value.r) ||
39
+ !/^[0-9a-f]{64}$/i.test(value.s) ||
40
+ !/^[0-9a-f]+$/i.test(value.v))
41
+ throw invalidSignature();
42
+ const v = Number.parseInt(value.v, 16);
43
+ if (!Number.isSafeInteger(v) || v < 0)
44
+ throw invalidSignature();
45
+ signature = ethers_1.Signature.from({ r: `0x${value.r}`, s: `0x${value.s}`, v });
46
+ }
47
+ if ((0, ethers_1.recoverAddress)(digest, signature) !== expected)
48
+ throw invalidSignature();
49
+ return signature;
50
+ }
51
+ catch {
52
+ throw invalidSignature();
53
+ }
54
+ };
55
+ // Detach the approved typed data before connection or host code can await/mutate it.
56
+ const copyTypedData = (value, seen = new WeakSet(), depth = 0) => {
57
+ if (depth > 64)
58
+ throw invalidRequest();
59
+ if (value === null || typeof value === 'string' || typeof value === 'boolean')
60
+ return value;
61
+ if (typeof value === 'bigint')
62
+ return value.toString();
63
+ if (typeof value === 'number') {
64
+ if (!Number.isFinite(value))
65
+ throw invalidRequest();
66
+ return value;
67
+ }
68
+ if (value instanceof Uint8Array)
69
+ return `0x${Buffer.from(value).toString('hex')}`;
70
+ if (!value || typeof value !== 'object' || seen.has(value))
71
+ throw invalidRequest();
72
+ const array = Array.isArray(value);
73
+ const prototype = Object.getPrototypeOf(value);
74
+ if (!array && prototype !== Object.prototype && prototype !== null)
75
+ throw invalidRequest();
76
+ seen.add(value);
77
+ try {
78
+ const result = array ? [] : Object.create(null);
79
+ for (const key of Object.keys(value)) {
80
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
81
+ if (!('value' in descriptor))
82
+ throw invalidRequest();
83
+ if (descriptor.value !== undefined)
84
+ result[key] = copyTypedData(descriptor.value, seen, depth + 1);
85
+ }
86
+ return Object.freeze(result);
87
+ }
88
+ finally {
89
+ seen.delete(value);
90
+ }
91
+ };
92
+ /** Small authority-side facade. Device SDKs belong exclusively in ledger-host. */
51
93
  class LedgerKeyring {
52
- /**
53
- * @param sharedHardwareWalletManager Optional shared HardwareWalletManager instance.
54
- * If provided, this will be used instead of creating
55
- * a new instance. This allows multiple KeyringManagers
56
- * to share the same Ledger connection.
57
- */
58
- constructor(sharedHardwareWalletManager) {
59
- this.hdPath = "m/44'/57'/0'/0/0";
60
- this.transport = null;
61
- // In-memory cache of registered wallet policy HMACs
62
- this.walletHmacCache = new Map();
63
- this.getUtxoAddress = async ({ coin, index, // account index
64
- slip44, showInLedger, }) => {
65
- return this.executeWithRetry(async () => {
66
- const fingerprint = await this.ledgerUtxoClient.getMasterFingerprint();
67
- const xpub = await this.getXpub({ index, coin, slip44 });
68
- this.setHdPath(coin, index, slip44);
69
- // xpub/tpub already comes from device; no conversion needed here
70
- const xpubWithDescriptor = `[${this.hdPath}]${xpub}`.replace('m', fingerprint);
71
- const walletPolicy = new bitcoin_client_1.WalletPolicy(coin, consts_1.DESCRIPTOR, [
72
- xpubWithDescriptor,
73
- ]);
74
- const hmac = await this.getOrRegisterHmac(walletPolicy, fingerprint);
75
- const address = await this.ledgerUtxoClient.getWalletAddress(walletPolicy, hmac, consts_1.RECEIVING_ADDRESS_INDEX, 0, // verify imported account address at first receive index
76
- !!showInLedger);
77
- return address;
78
- }, 'getUtxoAddress');
94
+ // Retain the first constructor position for existing shared-manager callers.
95
+ // A manager alone cannot grant a new device capability.
96
+ constructor(sharedHardwareWalletManager, host) {
97
+ this.sharedHardwareWalletManager = sharedHardwareWalletManager;
98
+ this.host = host;
99
+ this.generation = 0;
100
+ this.ensureConnection = async () => this.run(async () => undefined);
101
+ this.verifyUtxoAddress = (accountIndex, currency, slip44) => this.run((host) => host.utxo.getAddress({
102
+ accountIndex,
103
+ currency,
104
+ slip44,
105
+ checkOnDevice: true,
106
+ }));
107
+ this.signUtxoMessage = (request) => {
108
+ const approved = Object.freeze({
109
+ currency: request.currency,
110
+ slip44: request.slip44,
111
+ accountIndex: request.accountIndex,
112
+ message: request.message,
113
+ addressIndex: request.addressIndex,
114
+ change: request.change,
115
+ accountXpub: request.accountXpub,
116
+ });
117
+ return this.run((host) => host.utxo.signMessage(approved));
79
118
  };
80
- this.verifyUtxoAddress = async (accountIndex, currency, slip44) => await this.getUtxoAddress({
81
- coin: currency,
82
- index: accountIndex,
83
- slip44: slip44,
84
- showInLedger: true,
85
- });
86
- this.getXpub = async ({ index, coin, slip44, }) => {
87
- return this.executeWithRetry(async () => {
88
- this.setHdPath(coin, index, slip44);
89
- {
90
- // Try silent first, then fall back to on-device display for unusual paths
119
+ this.signPsbt = (request) => {
120
+ const approved = Object.freeze({
121
+ currency: request.currency,
122
+ slip44: request.slip44,
123
+ accountIndex: request.accountIndex,
124
+ accountXpub: request.accountXpub,
125
+ psbtBase64: request.psbtBase64,
126
+ });
127
+ return this.run((host) => host.utxo.signPsbt(approved));
128
+ };
129
+ this.evm = {
130
+ getEvmAddressAndPubKey: async (request) => {
131
+ const approved = { accountIndex: accountIndex(request.accountIndex) };
132
+ return this.run(async (host, assertCurrent) => {
133
+ const result = await host.evm.getAddress(approved);
134
+ assertCurrent();
91
135
  try {
92
- return await this.ledgerUtxoClient.getExtendedPubkey(this.hdPath, consts_1.WILL_NOT_DISPLAY);
136
+ const address = (0, ethers_1.getAddress)(result.address);
137
+ const publicKey = result.publicKey.replace(/^0x/i, '');
138
+ if ((0, ethers_1.computeAddress)(`0x${publicKey}`) !== address)
139
+ throw invalidRequest();
140
+ return { address, publicKey };
93
141
  }
94
- catch (err) {
95
- // Retry with display=true to allow unusual paths with user approval
96
- return await this.ledgerUtxoClient.getExtendedPubkey(this.hdPath, true);
142
+ catch {
143
+ throw (0, host_types_1.ledgerHostError)('LEDGER_INVALID_RESPONSE', 'Ledger returned an invalid address or public key');
97
144
  }
145
+ });
146
+ },
147
+ signEVMTransaction: async (request) => {
148
+ const index = accountIndex(request.accountIndex);
149
+ const expected = approvedAddress(request.expectedAddress);
150
+ let transaction;
151
+ try {
152
+ transaction = ethers_1.Transaction.from(`0x${Buffer.from(hexBytes(request.rawTx)).toString('hex')}`);
153
+ if (transaction.signature || transaction.chainId <= 0n)
154
+ throw invalidRequest();
98
155
  }
99
- }, 'getXpub');
100
- };
101
- /**
102
- * Sign a UTXO message - public method used by transaction classes
103
- */
104
- this.signUtxoMessage = async (path, message) => {
105
- return this.executeWithRetry(async () => {
106
- const bufferMessage = Buffer.from(message);
107
- const signature = await this.ledgerUtxoClient.signMessage(bufferMessage, path);
108
- return signature;
109
- }, 'signUtxoMessage');
110
- };
111
- this.signEVMTransaction = async ({ rawTx, accountIndex, }) => {
112
- return this.executeWithRetry(async () => {
113
- this.setHdPath('eth', accountIndex, 60);
114
- const resolution = await hw_app_eth_1.ledgerService.resolveTransaction(rawTx, {}, {});
115
- const signature = await this.ledgerEVMClient.signTransaction(this.hdPath.replace(/^m\//, ''), // Remove 'm/' prefix for EVM
116
- rawTx, resolution);
117
- return signature;
118
- }, 'signEVMTransaction');
119
- };
120
- this.signPersonalMessage = async ({ message, accountIndex, }) => {
121
- return this.executeWithRetry(async () => {
122
- this.setHdPath('eth', accountIndex, 60);
123
- const signature = await this.ledgerEVMClient.signPersonalMessage(this.hdPath.replace(/^m\//, ''), // Remove 'm/' prefix for EVM
124
- message);
125
- return `0x${signature.r}${signature.s}${signature.v.toString(16)}`;
126
- }, 'signPersonalMessage');
127
- };
128
- this.transformTypedData = (data, version) => {
129
- const { types, primaryType, domain, message } = this.sanitizeData(data);
130
- const domainSeparatorHash = eth_sig_util_1.TypedDataUtils.hashStruct('EIP712Domain', this.sanitizeData(domain), types, version).toString('hex');
131
- let messageHash = null;
132
- if (primaryType !== 'EIP712Domain') {
133
- messageHash = eth_sig_util_1.TypedDataUtils.hashStruct(primaryType, this.sanitizeData(message), types, version).toString('hex');
134
- }
135
- return {
136
- domain_separator_hash: domainSeparatorHash,
137
- message_hash: messageHash,
138
- ...data,
139
- };
140
- };
141
- this.getEvmAddressAndPubKey = async ({ accountIndex, }) => {
142
- return this.executeWithRetry(async () => {
143
- this.setHdPath('eth', accountIndex, 60);
144
- const { address, publicKey } = await this.ledgerEVMClient.getAddress(this.hdPath.replace(/^m\//, '') // Remove 'm/' prefix for EVM
145
- );
146
- return { address, publicKey };
147
- }, 'getEvmAddressAndPubKey');
148
- };
149
- this.signTypedData = async ({ version, data, accountIndex, }) => {
150
- return this.executeWithRetry(async () => {
151
- this.setHdPath('eth', accountIndex, 60);
152
- // V1 typed data is not supported by hardware wallets
153
- if (version === eth_sig_util_1.SignTypedDataVersion.V1) {
154
- throw new Error('Ledger: V1 typed data signing is not supported. Please use V3 or V4.');
156
+ catch {
157
+ throw invalidRequest();
155
158
  }
156
- const dataWithHashes = this.transformTypedData(data, version);
157
- const { domain_separator_hash, message_hash } = dataWithHashes;
158
- const signature = await this.ledgerEVMClient.signEIP712HashedMessage(this.hdPath.replace(/^m\//, ''), // Remove 'm/' prefix for EVM
159
- domain_separator_hash, message_hash ? message_hash : '');
160
- return `0x${signature.r}${signature.s}${signature.v.toString(16)}`;
161
- }, 'signTypedData');
162
- };
163
- this.getMasterFingerprint = async () => {
164
- try {
165
- const masterFingerprint = await this.ledgerUtxoClient.getMasterFingerprint();
166
- return masterFingerprint;
167
- }
168
- catch (error) {
169
- console.log('Fingerprint error: ', error);
170
- throw error;
171
- }
172
- };
173
- this.hardwareWalletManager =
174
- sharedHardwareWalletManager || new hardware_wallet_manager_1.HardwareWalletManager();
175
- // Set up event listeners
176
- this.hardwareWalletManager.on('connected', ({ type }) => {
177
- if (type === hardware_wallet_manager_1.HardwareWalletType.LEDGER) {
178
- console.log('Ledger connected');
179
- }
180
- });
181
- this.hardwareWalletManager.on('disconnected', ({ type }) => {
182
- if (type === hardware_wallet_manager_1.HardwareWalletType.LEDGER) {
183
- console.log('Ledger disconnected');
184
- this.transport = null;
185
- // Clear clients on disconnect
186
- this.ledgerEVMClient = null;
187
- this.ledgerUtxoClient = null;
188
- this.walletHmacCache.clear();
189
- }
190
- });
191
- this.hardwareWalletManager.on('connectionFailed', ({ type, error }) => {
192
- if (type === hardware_wallet_manager_1.HardwareWalletType.LEDGER) {
193
- console.error('Ledger connection failed:', error);
194
- }
195
- });
196
- this.evm = {
197
- getEvmAddressAndPubKey: this.getEvmAddressAndPubKey,
198
- signEVMTransaction: this.signEVMTransaction,
199
- signPersonalMessage: this.signPersonalMessage,
200
- signTypedData: this.signTypedData,
159
+ const approved = {
160
+ accountIndex: index,
161
+ expectedAddress: expected,
162
+ unsignedTxHex: transaction.unsignedSerialized.replace(/^0x/, ''),
163
+ };
164
+ const digest = transaction.unsignedHash;
165
+ return this.run(async (host, assertCurrent) => {
166
+ const result = await host.evm.signTransaction(approved);
167
+ assertCurrent();
168
+ const signature = checkedSignature(result, digest, expected);
169
+ if (signature.legacyChainId !== null &&
170
+ signature.legacyChainId !== transaction.chainId)
171
+ throw invalidSignature();
172
+ return {
173
+ r: signature.r.slice(2),
174
+ s: signature.s.slice(2),
175
+ v: signature.v.toString(16),
176
+ };
177
+ });
178
+ },
179
+ signPersonalMessage: async (request) => {
180
+ const expected = approvedAddress(request.expectedAddress);
181
+ const messageHex = Buffer.from(hexBytes(request.message)).toString('hex');
182
+ const digest = (0, ethers_1.hashMessage)(hexBytes(messageHex));
183
+ const approved = {
184
+ accountIndex: accountIndex(request.accountIndex),
185
+ expectedAddress: expected,
186
+ messageHex,
187
+ };
188
+ return this.run(async (host) => checkedSignature(await host.evm.signPersonalMessage(approved), digest, expected).serialized);
189
+ },
190
+ signTypedData: async (request) => {
191
+ const { version } = request;
192
+ if (version !== eth_sig_util_1.SignTypedDataVersion.V3 &&
193
+ version !== eth_sig_util_1.SignTypedDataVersion.V4)
194
+ throw invalidRequest();
195
+ const expected = approvedAddress(request.expectedAddress);
196
+ const index = accountIndex(request.accountIndex);
197
+ let data, digest;
198
+ try {
199
+ data = copyTypedData(request.data);
200
+ digest = `0x${eth_sig_util_1.TypedDataUtils.eip712Hash(data, version).toString('hex')}`;
201
+ }
202
+ catch {
203
+ throw invalidRequest();
204
+ }
205
+ const approved = {
206
+ accountIndex: index,
207
+ expectedAddress: expected,
208
+ data,
209
+ version,
210
+ };
211
+ return this.run(async (host) => checkedSignature(await host.evm.signTypedData(approved), digest, expected).serialized);
212
+ },
201
213
  };
202
214
  this.utxo = {
203
- getUtxoAddress: this.getUtxoAddress,
204
- getXpub: this.getXpub,
215
+ getUtxoAddress: ({ coin, index, slip44 }) => this.run((host) => host.utxo.getAddress({ currency: coin, accountIndex: index, slip44 })),
216
+ getXpub: ({ coin, index, slip44 }) => this.run((host) => host.utxo.getXpub({
217
+ currency: coin,
218
+ accountIndex: index,
219
+ slip44,
220
+ // Import must remain available for paths the device classifies as
221
+ // unusual. Ask for its confirmation once, without retrying denial.
222
+ checkOnDevice: true,
223
+ })),
205
224
  verifyUtxoAddress: this.verifyUtxoAddress,
206
225
  };
207
226
  }
208
- /**
209
- * Ensure Ledger is connected with automatic retry
210
- * Note: This is automatically called by all operations through executeWithRetry
211
- * External callers don't need to call this directly
212
- */
213
- async ensureConnection() {
214
- await this.hardwareWalletManager.ensureConnection(hardware_wallet_manager_1.HardwareWalletType.LEDGER);
215
- this.transport = await this.hardwareWalletManager.getLedgerConnection();
216
- // Create clients if transport is available
217
- if (this.transport && (!this.ledgerEVMClient || !this.ledgerUtxoClient)) {
218
- this.ledgerEVMClient = new hw_app_eth_1.default(this.transport);
219
- this.ledgerUtxoClient = new bitcoin_client_1.default(this.transport);
220
- }
221
- }
222
- sanitizeData(data) {
223
- switch (Object.prototype.toString.call(data)) {
224
- case '[object Object]': {
225
- const entries = Object.keys(data).map((k) => [
226
- k,
227
- this.sanitizeData(data[k]),
228
- ]);
229
- return Object.fromEntries(entries);
230
- }
231
- case '[object Array]':
232
- return data.map((v) => this.sanitizeData(v));
233
- case '[object BigInt]':
234
- return data.toString();
235
- default:
236
- return data;
237
- }
238
- }
239
- // Build a stable cache key for a policy bound to the device and derivation path
240
- buildWalletCacheKey(fingerprint, walletPolicy) {
241
- // Wallet HMACs are bound to a specific wallet policy id.
242
- // The id commits to: version, wallet name, descriptor template hash, and the merkle root of keys.
243
- // Therefore, caching by (fingerprint + walletId) avoids collisions across different networks
244
- // that may share the same derivation path (e.g., multiple slip44=1 testnets).
245
- return `${fingerprint}|${walletPolicy.getId().toString('hex')}`;
227
+ requireHost() {
228
+ if (!this.host)
229
+ throw (0, host_types_1.ledgerHostError)('HARDWARE_SIGNER_UNAVAILABLE', 'Ledger requires an authorized device host');
230
+ return this.host;
246
231
  }
247
- // Lazily register the wallet policy and cache HMAC in memory only
248
- async getOrRegisterHmac(walletPolicy, fingerprint) {
249
- const cacheKey = this.buildWalletCacheKey(fingerprint, walletPolicy);
250
- const cached = this.walletHmacCache.get(cacheKey);
251
- if (cached)
252
- return cached;
253
- // If registerWallet is unavailable (tests/mocks), fall back to null HMAC
254
- const registerWallet = this.ledgerUtxoClient?.registerWallet;
255
- if (typeof registerWallet !== 'function') {
256
- return null;
257
- }
232
+ async run(operation) {
233
+ const generation = this.generation;
234
+ const host = this.requireHost();
235
+ // A visible platform host owns discovery, permission and device selection.
236
+ if (!host.getStatus().connected)
237
+ await host.connect({ useGrantedDevices: true });
238
+ const assertCurrent = () => {
239
+ if (generation !== this.generation)
240
+ throw (0, host_types_1.ledgerCancelledError)();
241
+ };
242
+ assertCurrent();
258
243
  try {
259
- // Register once (device approval). If user cancels, error will propagate via retryOperation
260
- const result = await registerWallet.call(this.ledgerUtxoClient, walletPolicy);
261
- const walletHMAC = Array.isArray(result) ? result[1] : null;
262
- if (walletHMAC && Buffer.isBuffer(walletHMAC)) {
263
- this.walletHmacCache.set(cacheKey, walletHMAC);
264
- return walletHMAC;
265
- }
266
- return null;
244
+ const result = await operation(host, assertCurrent);
245
+ assertCurrent();
246
+ return result;
267
247
  }
268
- catch (e) {
269
- // On failure, proceed without HMAC (device may prompt)
270
- return null;
248
+ catch (error) {
249
+ assertCurrent();
250
+ throw error;
271
251
  }
272
252
  }
273
- setHdPath(coin, accountIndex, slip44) {
274
- if ((0, derivation_paths_1.isEvmCoin)(coin, slip44)) {
275
- // For EVM, the "accountIndex" parameter is actually used as the address index
276
- // EVM typically uses account 0, and different addresses are at different address indices
277
- this.hdPath = (0, derivation_paths_1.getAddressDerivationPath)(coin, slip44, 0, // account is always 0 for EVM
278
- false, // not a change address
279
- accountIndex // this is actually the address index for EVM
280
- );
281
- }
282
- else {
283
- // For UTXO, use account-level derivation path
284
- this.hdPath = (0, derivation_paths_1.getAccountDerivationPath)(coin, slip44, accountIndex);
285
- }
286
- }
287
- /**
288
- * Convert PSBT to Ledger format with retry logic
289
- */
290
- async convertToLedgerFormat(psbt, accountXpub, accountId, currency, slip44) {
291
- return this.executeWithRetry(async () => {
292
- // Ensure Ledger is connected before attempting operations
293
- // This is now handled by executeWithRetry
294
- // Build a bitcoinjs-bip32 network from slip44/currency so zpub/vpub parse directly
295
- const { networks, types } = (0, sysweb3_network_1.getNetworkConfig)(slip44, currency);
296
- const isTestnet = slip44 === 1;
297
- const pubTypes = isTestnet
298
- ? types.zPubType.testnet
299
- : types.zPubType.mainnet;
300
- const baseNetwork = isTestnet ? networks.testnet : networks.mainnet;
301
- const network = {
302
- ...baseNetwork,
303
- bip32: {
304
- public: parseInt(pubTypes.vpub || pubTypes.zpub, 16),
305
- private: parseInt(pubTypes.vprv || pubTypes.zprv, 16),
306
- },
307
- };
308
- const bip32 = (0, bip32_1.default)(secp256k1_1.default);
309
- const accountNode = bip32.fromBase58(accountXpub, network);
310
- // Get master fingerprint
311
- const fingerprint = await this.getMasterFingerprint();
312
- // Enhance each input with bip32Derivation
313
- const missingInputDerivations = [];
314
- for (let i = 0; i < psbt.inputCount; i++) {
315
- const dataInput = psbt.data.inputs[i];
316
- // External inputs are already authorized by their final scripts. Do
317
- // not invent wallet derivations or ask the device to sign them again.
318
- if (dataInput.finalScriptSig || dataInput.finalScriptWitness)
319
- continue;
320
- // Skip if already has bip32Derivation
321
- if (dataInput.bip32Derivation && dataInput.bip32Derivation.length > 0) {
322
- continue;
323
- }
324
- // Ensure witnessUtxo is present if nonWitnessUtxo exists
325
- if (!dataInput.witnessUtxo && dataInput.nonWitnessUtxo) {
326
- const txBuffer = dataInput.nonWitnessUtxo;
327
- const tx = syscoinjs_lib_1.Transaction.fromBuffer(txBuffer);
328
- const vout = psbt.txInputs[i].index;
329
- if (tx.outs[vout]) {
330
- dataInput.witnessUtxo = {
331
- script: tx.outs[vout].script,
332
- value: tx.outs[vout].value,
333
- };
334
- }
335
- }
336
- // Extract path from unknownKeyVals by searching for the key, not using hardcoded index
337
- let pathFromInput = null;
338
- if (dataInput.unknownKeyVals && dataInput.unknownKeyVals.length > 0) {
339
- for (const kv of dataInput.unknownKeyVals) {
340
- const keyStr = Buffer.from(kv.key).toString();
341
- if (keyStr === 'path') {
342
- pathFromInput = Buffer.from(kv.value).toString();
343
- break;
344
- }
345
- }
346
- }
347
- let bip32Derivation = null;
348
- if (pathFromInput) {
349
- const fullPath = pathFromInput;
350
- const accountPath = (0, derivation_paths_1.getAccountDerivationPath)(currency, slip44, accountId);
351
- const relativePath = fullPath
352
- .replace(accountPath, '')
353
- .replace(/^\//, '');
354
- const derivationTokens = relativePath.split('/').filter((t) => t);
355
- const derivedAccount = derivationTokens.reduce((acc, token) => {
356
- const index = parseInt(token);
357
- if (isNaN(index)) {
358
- return acc;
359
- }
360
- return acc.derive(index);
361
- }, accountNode);
362
- const rawPubkey = derivedAccount.publicKey;
363
- const pubkeyBuf = Buffer.isBuffer(rawPubkey)
364
- ? rawPubkey
365
- : Buffer.from(rawPubkey);
366
- if (pubkeyBuf && pubkeyBuf.length === 33) {
367
- // Add the bip32Derivation that Ledger needs
368
- bip32Derivation = {
369
- masterFingerprint: Buffer.from(fingerprint, 'hex'),
370
- path: fullPath,
371
- pubkey: pubkeyBuf,
372
- };
373
- psbt.updateInput(i, {
374
- bip32Derivation: [bip32Derivation],
375
- });
376
- }
377
- }
378
- // Check the updated PSBT state (avoid stale local references)
379
- const updatedInput = psbt.data.inputs[i];
380
- if (!updatedInput.bip32Derivation ||
381
- updatedInput.bip32Derivation.length === 0) {
382
- missingInputDerivations.push(i);
383
- }
384
- }
385
- // Enhance each output with bip32Derivation when it's a change/output owned by the wallet
386
- for (let i = 0; i < psbt.data.outputs.length; i++) {
387
- const dataOutput = psbt.data.outputs[i];
388
- // Skip if derivation already present
389
- if (dataOutput.bip32Derivation &&
390
- dataOutput.bip32Derivation.length > 0) {
391
- continue;
392
- }
393
- // Extract path from unknownKeyVals by searching for the key 'path'
394
- let pathFromOutput = null;
395
- if (dataOutput.unknownKeyVals && dataOutput.unknownKeyVals.length > 0) {
396
- for (const kv of dataOutput.unknownKeyVals) {
397
- const keyStr = Buffer.from(kv.key).toString();
398
- if (keyStr === 'path') {
399
- pathFromOutput = Buffer.from(kv.value).toString();
400
- break;
401
- }
402
- }
403
- }
404
- if (pathFromOutput) {
405
- let bip32Derivation = null;
406
- const fullPath = pathFromOutput;
407
- const accountPath = (0, derivation_paths_1.getAccountDerivationPath)(currency, slip44, accountId);
408
- const relativePath = fullPath
409
- .replace(accountPath, '')
410
- .replace(/^\//, '');
411
- const derivationTokens = relativePath.split('/').filter((t) => t);
412
- const derivedAccount = derivationTokens.reduce((acc, token) => {
413
- const index = parseInt(token);
414
- if (isNaN(index)) {
415
- return acc;
416
- }
417
- return acc.derive(index);
418
- }, accountNode);
419
- const rawOutPubkey = derivedAccount.publicKey;
420
- const outPubkeyBuf = Buffer.isBuffer(rawOutPubkey)
421
- ? rawOutPubkey
422
- : Buffer.from(rawOutPubkey);
423
- if (outPubkeyBuf && outPubkeyBuf.length === 33) {
424
- bip32Derivation = {
425
- masterFingerprint: Buffer.from(fingerprint, 'hex'),
426
- path: fullPath,
427
- pubkey: outPubkeyBuf,
428
- };
429
- psbt.updateOutput(i, {
430
- bip32Derivation: [bip32Derivation],
431
- });
432
- }
433
- // Outputs without valid derivation are allowed (external recipients)
434
- }
435
- }
436
- // If any wallet-owned inputs/outputs are missing bip32Derivation, fail early with a clear error
437
- if (missingInputDerivations.length > 0) {
438
- const parts = [];
439
- parts.push(`inputs [${missingInputDerivations.join(', ')}]`);
440
- throw new Error(`convertToLedgerFormat: Missing bip32Derivation for ${parts.join(' and ')}. Ensure PSBT includes a 'path' unknownKeyVal or BIP32_DERIVATION for wallet-owned entries.`);
441
- }
442
- return psbt;
443
- }, 'convertToLedgerFormat');
444
- }
445
- /**
446
- * Execute operation with automatic retry
447
- */
448
- async executeWithRetry(operation, operationName) {
449
- // Ensure connection first
450
- await this.ensureConnection();
451
- // Use hardware wallet manager's retry mechanism
452
- return this.hardwareWalletManager.retryOperation(operation, operationName, {
453
- maxRetries: 3,
454
- baseDelay: 1000,
455
- maxDelay: 5000,
456
- backoffMultiplier: 2,
457
- });
458
- }
459
- /**
460
- * Get hardware wallet status
461
- */
462
253
  getStatus() {
463
- return this.hardwareWalletManager
464
- .getStatus()
465
- .find((s) => s.type === hardware_wallet_manager_1.HardwareWalletType.LEDGER);
254
+ const connected = this.host?.getStatus().connected === true;
255
+ return {
256
+ type: hardware_wallet_manager_1.HardwareWalletType.LEDGER,
257
+ connected,
258
+ status: connected
259
+ ? hardware_wallet_manager_1.ConnectionStatus.CONNECTED
260
+ : hardware_wallet_manager_1.ConnectionStatus.DISCONNECTED,
261
+ };
466
262
  }
467
- /**
468
- * Clean up resources
469
- */
470
263
  async destroy() {
471
- await this.hardwareWalletManager.destroy();
472
- this.transport = null;
264
+ // Revoke pending responses synchronously, before platform teardown awaits.
265
+ this.generation++;
266
+ const cleanup = this.sharedHardwareWalletManager?.destroy();
267
+ const disconnect = this.host?.disconnect();
268
+ await Promise.all([cleanup, disconnect]);
473
269
  }
474
270
  }
475
271
  exports.LedgerKeyring = LedgerKeyring;