@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,13 +1,42 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
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
+ })();
5
35
  Object.defineProperty(exports, "__esModule", { value: true });
6
36
  exports.HardwareWalletManager = exports.ConnectionStatus = exports.HardwareWalletType = void 0;
7
- const hw_transport_webhid_1 = __importDefault(require("@ledgerhq/hw-transport-webhid"));
8
- const logs_1 = require("@ledgerhq/logs");
9
- const connect_webextension_1 = __importDefault(require("@trezor/connect-webextension"));
10
37
  const events_1 = require("events");
38
+ const host_types_1 = require("./ledger/host-types");
39
+ const connect_1 = __importStar(require("./trezor/connect"));
11
40
  var HardwareWalletType;
12
41
  (function (HardwareWalletType) {
13
42
  HardwareWalletType["LEDGER"] = "ledger";
@@ -24,11 +53,12 @@ var ConnectionStatus;
24
53
  * Hardware Wallet Manager with connection pooling, status monitoring, and error recovery
25
54
  */
26
55
  class HardwareWalletManager extends events_1.EventEmitter {
27
- constructor() {
56
+ constructor(ledgerHost) {
28
57
  super();
58
+ this.ledgerHost = ledgerHost;
29
59
  this.connectionPool = new Map();
30
60
  this.statusMonitorInterval = null;
31
- this.CONNECTION_TIMEOUT = 30000; // 30 seconds
61
+ this.lifecycleGeneration = 0;
32
62
  this.IDLE_TIMEOUT = 5 * 60 * 1000; // 5 minutes
33
63
  this.STATUS_CHECK_INTERVAL = 5000; // 5 seconds
34
64
  this.retryConfig = {
@@ -37,92 +67,109 @@ class HardwareWalletManager extends events_1.EventEmitter {
37
67
  maxDelay: 10000, // 10 seconds
38
68
  backoffMultiplier: 2,
39
69
  };
70
+ this.observeTrezor();
40
71
  this.startStatusMonitoring();
41
- // Set up Ledger debug logging
42
- if (process.env.NODE_ENV === 'development') {
43
- (0, logs_1.listen)((log) => {
44
- console.log(`[Ledger] ${log.type}: ${log.message}`);
45
- });
46
- }
47
72
  }
48
- /**
49
- * Get or create a Ledger connection with retry logic
50
- */
73
+ observeTrezor() {
74
+ if (this.unsubscribeTrezor)
75
+ return;
76
+ this.unsubscribeTrezor = (0, connect_1.subscribeTrezorEvents)((event) => {
77
+ if (event.type === 'disconnected') {
78
+ // Our pre-init disposal and Connect initialization invalidate the
79
+ // adapter too. Keep this attempt owned until it settles or is revoked.
80
+ if (this.connectionPool.get('trezor-default')?.status ===
81
+ ConnectionStatus.CONNECTING)
82
+ return;
83
+ this.handleDisconnect('trezor-default', HardwareWalletType.TREZOR);
84
+ }
85
+ });
86
+ }
87
+ /** Raw transports are deliberately unavailable through the semantic host boundary. */
51
88
  async getLedgerConnection() {
89
+ throw (0, host_types_1.ledgerHostError)('HARDWARE_SIGNER_UNAVAILABLE', 'Ledger requires a semantic LedgerHost; raw Ledger transports are not supported');
90
+ }
91
+ async initializeLedger() {
92
+ const host = this.ledgerHost;
93
+ if (!host) {
94
+ throw (0, host_types_1.ledgerHostError)('HARDWARE_SIGNER_UNAVAILABLE', 'A semantic LedgerHost is required to connect Ledger');
95
+ }
96
+ if (this.destruction)
97
+ throw (0, host_types_1.ledgerCancelledError)();
98
+ this.startStatusMonitoring();
52
99
  const key = `${HardwareWalletType.LEDGER}-default`;
53
- const existing = this.connectionPool.get(key);
54
- if (existing?.transport && existing.status === ConnectionStatus.CONNECTED) {
55
- existing.lastActivity = Date.now();
56
- return existing.transport;
100
+ if (host.getStatus().connected) {
101
+ const existing = this.connectionPool.get(key);
102
+ if (existing)
103
+ existing.lastActivity = Date.now();
104
+ return;
57
105
  }
58
- return this.createLedgerConnectionWithRetry(key);
59
- }
60
- /**
61
- * Create Ledger connection with exponential backoff retry
62
- */
63
- async createLedgerConnectionWithRetry(key) {
106
+ if (this.ledgerInitialization)
107
+ return this.ledgerInitialization;
108
+ const generation = this.lifecycleGeneration;
64
109
  const entry = {
65
110
  status: ConnectionStatus.CONNECTING,
66
111
  lastActivity: Date.now(),
67
112
  retryCount: 0,
68
113
  };
69
114
  this.connectionPool.set(key, entry);
70
- let lastError;
71
- for (let attempt = 0; attempt <= this.retryConfig.maxRetries; attempt++) {
72
- try {
73
- this.emit('connectionAttempt', {
115
+ const isCurrent = () => generation === this.lifecycleGeneration &&
116
+ this.connectionPool.get(key) === entry &&
117
+ !this.destruction;
118
+ const assertCurrent = () => {
119
+ if (!isCurrent())
120
+ throw (0, host_types_1.ledgerCancelledError)();
121
+ };
122
+ // Install the shared promise before any observer can re-enter this method.
123
+ const pending = Promise.resolve().then(async () => {
124
+ assertCurrent();
125
+ this.emit('connectionAttempt', {
126
+ type: HardwareWalletType.LEDGER,
127
+ attempt: 1,
128
+ maxAttempts: 1,
129
+ });
130
+ assertCurrent();
131
+ // Device selection happens in the visible host, never by a background chooser.
132
+ await host.connect({ useGrantedDevices: true });
133
+ assertCurrent();
134
+ if (!host.getStatus().connected)
135
+ throw (0, host_types_1.ledgerHostError)('HARDWARE_SIGNER_UNAVAILABLE', 'Connect a Ledger device in the Ledger window');
136
+ entry.status = ConnectionStatus.CONNECTED;
137
+ entry.lastActivity = Date.now();
138
+ this.emit('connected', { type: HardwareWalletType.LEDGER });
139
+ assertCurrent();
140
+ });
141
+ this.ledgerInitialization = pending;
142
+ try {
143
+ await pending;
144
+ }
145
+ catch (error) {
146
+ if (isCurrent()) {
147
+ entry.status = ConnectionStatus.ERROR;
148
+ entry.error = error;
149
+ this.emit('connectionFailed', {
74
150
  type: HardwareWalletType.LEDGER,
75
- attempt: attempt + 1,
76
- maxAttempts: this.retryConfig.maxRetries + 1,
151
+ error: entry.error.message,
77
152
  });
78
- const transport = await this.createLedgerTransport();
79
- entry.transport = transport;
80
- entry.status = ConnectionStatus.CONNECTED;
81
- entry.retryCount = 0;
82
- entry.error = undefined;
83
- this.emit('connected', { type: HardwareWalletType.LEDGER });
84
- // Set up disconnect handler
85
- transport.on('disconnect', () => {
86
- this.handleDisconnect(key, HardwareWalletType.LEDGER);
87
- });
88
- return transport;
89
- }
90
- catch (error) {
91
- lastError = error;
92
- entry.retryCount = attempt + 1;
93
- entry.error = lastError;
94
- if (attempt < this.retryConfig.maxRetries) {
95
- const delay = this.calculateBackoffDelay(attempt);
96
- this.emit('retrying', {
97
- type: HardwareWalletType.LEDGER,
98
- attempt: attempt + 1,
99
- nextRetryIn: delay,
100
- error: lastError.message,
101
- });
102
- await this.delay(delay);
103
- }
104
153
  }
154
+ throw error;
155
+ }
156
+ finally {
157
+ if (this.ledgerInitialization === pending)
158
+ this.ledgerInitialization = undefined;
105
159
  }
106
- entry.status = ConnectionStatus.ERROR;
107
- this.emit('connectionFailed', {
108
- type: HardwareWalletType.LEDGER,
109
- error: lastError?.message,
110
- });
111
- throw new Error(`Failed to connect to Ledger after ${this.retryConfig.maxRetries + 1} attempts: ${lastError?.message || 'Unknown error'}`);
112
- }
113
- /**
114
- * Create Ledger transport with timeout
115
- */
116
- async createLedgerTransport() {
117
- return Promise.race([
118
- hw_transport_webhid_1.default.create(),
119
- this.createTimeoutPromise(this.CONNECTION_TIMEOUT, 'Ledger connection timeout'),
120
- ]);
121
160
  }
122
161
  /**
123
162
  * Initialize Trezor with retry logic
124
163
  */
125
164
  async initializeTrezor() {
165
+ if (this.destruction) {
166
+ throw Object.assign(new Error('Trezor connection is closing'), {
167
+ code: 'Method_Interrupted',
168
+ });
169
+ }
170
+ const generation = this.lifecycleGeneration;
171
+ this.observeTrezor();
172
+ this.startStatusMonitoring();
126
173
  const key = `${HardwareWalletType.TREZOR}-default`;
127
174
  const existing = this.connectionPool.get(key);
128
175
  if (existing?.status === ConnectionStatus.CONNECTED) {
@@ -130,25 +177,40 @@ class HardwareWalletManager extends events_1.EventEmitter {
130
177
  return true;
131
178
  }
132
179
  // Check if we're already trying to connect
133
- if (existing?.status === ConnectionStatus.CONNECTING) {
180
+ if (this.trezorInitialization ||
181
+ existing?.status === ConnectionStatus.CONNECTING) {
134
182
  console.log('[HardwareWalletManager] Trezor connection already in progress');
135
183
  // Wait a bit and check again
136
184
  await this.delay(1000);
185
+ if (generation !== this.lifecycleGeneration || this.destruction)
186
+ return false;
137
187
  const updated = this.connectionPool.get(key);
138
188
  return updated?.status === ConnectionStatus.CONNECTED || false;
139
189
  }
140
- return this.initializeTrezorWithRetry(key);
190
+ const owner = {};
191
+ this.trezorInitialization = owner;
192
+ try {
193
+ return await this.initializeTrezorWithRetry(key, owner, generation);
194
+ }
195
+ finally {
196
+ if (this.trezorInitialization === owner)
197
+ this.trezorInitialization = undefined;
198
+ }
141
199
  }
142
200
  /**
143
201
  * Initialize Trezor with exponential backoff retry
144
202
  */
145
- async initializeTrezorWithRetry(key) {
203
+ async initializeTrezorWithRetry(key, owner, generation) {
146
204
  const entry = {
147
205
  status: ConnectionStatus.CONNECTING,
148
206
  lastActivity: Date.now(),
149
207
  retryCount: 0,
150
208
  };
151
209
  this.connectionPool.set(key, entry);
210
+ const isCurrent = () => generation === this.lifecycleGeneration &&
211
+ this.trezorInitialization === owner &&
212
+ this.connectionPool.get(key) === entry &&
213
+ !this.destruction;
152
214
  let lastError;
153
215
  // Reduce retry attempts for Trezor to prevent repeated popups
154
216
  const trezorRetryConfig = {
@@ -156,73 +218,93 @@ class HardwareWalletManager extends events_1.EventEmitter {
156
218
  maxRetries: 1, // Only retry once for Trezor
157
219
  };
158
220
  for (let attempt = 0; attempt <= trezorRetryConfig.maxRetries; attempt++) {
221
+ if (!isCurrent())
222
+ return false;
159
223
  try {
160
224
  this.emit('connectionAttempt', {
161
225
  type: HardwareWalletType.TREZOR,
162
226
  attempt: attempt + 1,
163
227
  maxAttempts: trezorRetryConfig.maxRetries + 1,
164
228
  });
165
- // Dispose any existing iframe before initialization
229
+ if (!isCurrent())
230
+ return false;
231
+ // Dispose the previous Suite connection before initialization
166
232
  try {
167
- await connect_webextension_1.default.dispose();
233
+ await connect_1.default.dispose();
168
234
  }
169
235
  catch (disposeError) {
170
236
  console.log('[HardwareWalletManager] Dispose error (safe to ignore):', disposeError);
171
237
  }
172
- await connect_webextension_1.default.init({
238
+ if (!isCurrent())
239
+ return false;
240
+ await connect_1.default.init({
173
241
  manifest: {
174
242
  appName: 'Pali Wallet',
175
243
  appUrl: 'https://paliwallet.com/',
176
244
  email: 'support@syscoin.org',
177
245
  },
178
- lazyLoad: true,
179
- popup: true,
180
- connectSrc: 'https://connect.trezor.io/9/',
181
- _extendWebextensionLifetime: true,
182
- transports: ['BridgeTransport', 'WebUsbTransport'],
183
246
  debug: false, // Disable debug mode to prevent extra logs
184
- // Suite desktop handles newer devices; older devices retain the popup fallback.
247
+ // Connect chooses Suite Desktop, then the supported Suite Web fallback.
185
248
  coreMode: 'auto',
186
249
  });
250
+ if (!isCurrent())
251
+ return false;
187
252
  entry.status = ConnectionStatus.CONNECTED;
188
253
  entry.retryCount = 0;
189
254
  entry.error = undefined;
190
255
  this.emit('connected', { type: HardwareWalletType.TREZOR });
191
- // Set up device event listeners
192
- connect_webextension_1.default.on('DEVICE_EVENT', (event) => {
193
- if (event.type === 'device-disconnect') {
194
- this.handleDisconnect(key, HardwareWalletType.TREZOR);
195
- }
196
- });
197
- return true;
256
+ return isCurrent();
198
257
  }
199
258
  catch (error) {
259
+ // Teardown or a newer owner revoked this attempt. Its continuation
260
+ // must not mutate status, retry, or dispose the newer connection.
261
+ if (!isCurrent())
262
+ return false;
200
263
  lastError = error;
264
+ if (lastError.code === 'HARDWARE_SIGNER_BUSY') {
265
+ entry.status = ConnectionStatus.ERROR;
266
+ entry.error = lastError;
267
+ this.emit('connectionFailed', {
268
+ type: HardwareWalletType.TREZOR,
269
+ error: lastError.message,
270
+ });
271
+ throw lastError;
272
+ }
273
+ if (lastError.code === 'HARDWARE_SIGNER_UNAVAILABLE')
274
+ break;
201
275
  // Check if already initialized - this is actually OK
202
276
  if (lastError.message.includes('TrezorConnect has been already initialized')) {
203
277
  // Instead of just marking as connected, verify the connection
204
278
  try {
205
279
  // Test the connection with a simple call
206
- const testResponse = await connect_webextension_1.default.getFeatures();
280
+ const testResponse = await connect_1.default.getFeatures();
281
+ if (!isCurrent())
282
+ return false;
207
283
  if (testResponse.success) {
208
284
  entry.status = ConnectionStatus.CONNECTED;
209
285
  entry.retryCount = 0;
210
286
  entry.error = undefined;
211
287
  this.emit('connected', { type: HardwareWalletType.TREZOR });
212
- return true;
288
+ return isCurrent();
213
289
  }
214
290
  }
215
291
  catch (testError) {
216
292
  console.log('[HardwareWalletManager] Trezor test connection failed:', testError);
217
293
  }
294
+ if (!isCurrent())
295
+ return false;
218
296
  // If test failed, dispose and retry
219
297
  try {
220
- await connect_webextension_1.default.dispose();
298
+ await connect_1.default.dispose();
299
+ if (!isCurrent())
300
+ return false;
221
301
  await this.delay(500);
222
302
  }
223
303
  catch (disposeError) {
224
304
  console.log('[HardwareWalletManager] Error disposing for retry:', disposeError);
225
305
  }
306
+ if (!isCurrent())
307
+ return false;
226
308
  }
227
309
  entry.retryCount = attempt + 1;
228
310
  entry.error = lastError;
@@ -232,17 +314,19 @@ class HardwareWalletManager extends events_1.EventEmitter {
232
314
  console.log('[HardwareWalletManager] Trezor device is in use by another application');
233
315
  // Try to dispose and reinitialize
234
316
  try {
235
- await connect_webextension_1.default.dispose();
317
+ await connect_1.default.dispose();
318
+ if (!isCurrent())
319
+ return false;
236
320
  await this.delay(2000); // Wait 2 seconds before retry
237
321
  }
238
322
  catch (disposeError) {
239
323
  console.log('[HardwareWalletManager] Error during dispose:', disposeError);
240
324
  }
325
+ if (!isCurrent())
326
+ return false;
241
327
  }
242
328
  // Don't retry if user cancelled
243
- if (lastError.message.includes('Popup closed') ||
244
- lastError.message.includes('cancelled') ||
245
- lastError.message.includes('denied')) {
329
+ if (this.isUserCancellationError(lastError)) {
246
330
  console.log('[HardwareWalletManager] User cancelled Trezor connection');
247
331
  break; // Exit retry loop
248
332
  }
@@ -255,17 +339,23 @@ class HardwareWalletManager extends events_1.EventEmitter {
255
339
  error: lastError.message,
256
340
  });
257
341
  await this.delay(delay);
342
+ if (!isCurrent())
343
+ return false;
258
344
  }
259
345
  }
260
346
  }
347
+ if (!isCurrent())
348
+ return false;
261
349
  entry.status = ConnectionStatus.ERROR;
262
350
  this.emit('connectionFailed', {
263
351
  type: HardwareWalletType.TREZOR,
264
352
  error: lastError?.message,
265
353
  });
354
+ if (!isCurrent())
355
+ return false;
266
356
  // Clean up on failure
267
357
  try {
268
- await connect_webextension_1.default.dispose();
358
+ await connect_1.default.dispose();
269
359
  }
270
360
  catch (disposeError) {
271
361
  console.log('[HardwareWalletManager] Failed to dispose on error:', disposeError);
@@ -279,7 +369,6 @@ class HardwareWalletManager extends events_1.EventEmitter {
279
369
  const entry = this.connectionPool.get(key);
280
370
  if (entry) {
281
371
  entry.status = ConnectionStatus.DISCONNECTED;
282
- entry.transport = null;
283
372
  this.emit('disconnected', { type });
284
373
  }
285
374
  }
@@ -298,16 +387,29 @@ class HardwareWalletManager extends events_1.EventEmitter {
298
387
  * Check all connections and clean up idle ones
299
388
  */
300
389
  async checkAllConnections() {
390
+ const generation = this.lifecycleGeneration;
391
+ const isCurrent = () => generation === this.lifecycleGeneration && !this.destruction;
392
+ if (!isCurrent())
393
+ return;
301
394
  const now = Date.now();
302
395
  const statuses = [];
303
396
  for (const [key, entry] of this.connectionPool.entries()) {
397
+ if (!isCurrent())
398
+ return;
304
399
  const type = key.startsWith(HardwareWalletType.LEDGER)
305
400
  ? HardwareWalletType.LEDGER
306
401
  : HardwareWalletType.TREZOR;
402
+ if (type === HardwareWalletType.LEDGER &&
403
+ entry.status === ConnectionStatus.CONNECTED &&
404
+ !this.ledgerHost?.getStatus().connected) {
405
+ this.handleDisconnect(key, type);
406
+ }
307
407
  // Clean up idle connections
308
408
  if (entry.status === ConnectionStatus.CONNECTED &&
309
409
  now - entry.lastActivity > this.IDLE_TIMEOUT) {
310
- await this.closeConnection(key);
410
+ await this.closeConnection(key, entry);
411
+ if (!isCurrent())
412
+ return;
311
413
  continue;
312
414
  }
313
415
  // Build status
@@ -320,7 +422,8 @@ class HardwareWalletManager extends events_1.EventEmitter {
320
422
  };
321
423
  statuses.push(status);
322
424
  }
323
- this.emit('statusUpdate', statuses);
425
+ if (isCurrent())
426
+ this.emit('statusUpdate', statuses);
324
427
  }
325
428
  /**
326
429
  * Get current status of all hardware wallets
@@ -329,10 +432,19 @@ class HardwareWalletManager extends events_1.EventEmitter {
329
432
  const statuses = [];
330
433
  // Check Ledger
331
434
  const ledgerEntry = this.connectionPool.get(`${HardwareWalletType.LEDGER}-default`);
435
+ const ledgerConnected = !this.destruction && this.ledgerHost?.getStatus().connected === true;
332
436
  statuses.push({
333
437
  type: HardwareWalletType.LEDGER,
334
- status: ledgerEntry?.status || ConnectionStatus.DISCONNECTED,
335
- connected: ledgerEntry?.status === ConnectionStatus.CONNECTED,
438
+ status: this.destruction
439
+ ? ConnectionStatus.DISCONNECTED
440
+ : ledgerConnected
441
+ ? ConnectionStatus.CONNECTED
442
+ : ledgerEntry?.status === ConnectionStatus.CONNECTING
443
+ ? ConnectionStatus.CONNECTING
444
+ : ledgerEntry?.status === ConnectionStatus.ERROR
445
+ ? ConnectionStatus.ERROR
446
+ : ConnectionStatus.DISCONNECTED,
447
+ connected: ledgerConnected,
336
448
  lastSeen: ledgerEntry?.lastActivity,
337
449
  error: ledgerEntry?.error?.message,
338
450
  });
@@ -351,6 +463,8 @@ class HardwareWalletManager extends events_1.EventEmitter {
351
463
  * Check if a specific device is connected
352
464
  */
353
465
  isConnected(type) {
466
+ if (type === HardwareWalletType.LEDGER)
467
+ return (!this.destruction && this.ledgerHost?.getStatus().connected === true);
354
468
  const key = `${type}-default`;
355
469
  const entry = this.connectionPool.get(key);
356
470
  return entry?.status === ConnectionStatus.CONNECTED;
@@ -360,7 +474,7 @@ class HardwareWalletManager extends events_1.EventEmitter {
360
474
  */
361
475
  async ensureConnection(type) {
362
476
  if (type === HardwareWalletType.LEDGER) {
363
- await this.getLedgerConnection();
477
+ await this.initializeLedger();
364
478
  }
365
479
  else if (type === HardwareWalletType.TREZOR) {
366
480
  const initialized = await this.initializeTrezor();
@@ -372,42 +486,88 @@ class HardwareWalletManager extends events_1.EventEmitter {
372
486
  /**
373
487
  * Close a specific connection
374
488
  */
375
- async closeConnection(key) {
376
- const entry = this.connectionPool.get(key);
489
+ async closeConnection(key, entry = this.connectionPool.get(key)) {
490
+ if (!entry || this.connectionPool.get(key) !== entry)
491
+ return;
377
492
  // Handle Trezor connections
378
493
  if (key.startsWith(HardwareWalletType.TREZOR)) {
379
494
  try {
380
- await connect_webextension_1.default.dispose();
495
+ await connect_1.default.dispose();
381
496
  }
382
497
  catch (error) {
383
498
  console.error('Error disposing Trezor:', error);
384
499
  }
385
500
  }
386
- // Handle Ledger connections
387
- if (entry?.transport) {
501
+ if (key.startsWith(HardwareWalletType.LEDGER)) {
388
502
  try {
389
- await entry.transport.close();
503
+ await this.ledgerHost?.disconnect();
390
504
  }
391
505
  catch (error) {
392
- console.error('Error closing transport:', error);
506
+ console.error('Error disconnecting Ledger host:', error);
393
507
  }
394
508
  }
395
- this.connectionPool.delete(key);
509
+ if (this.connectionPool.get(key) === entry)
510
+ this.connectionPool.delete(key);
396
511
  }
397
512
  /**
398
513
  * Close all connections and stop monitoring
399
514
  */
400
- async destroy() {
515
+ destroy() {
516
+ if (this.destruction)
517
+ return this.destruction;
518
+ // Revoke pending initialization synchronously, before closing any transport.
519
+ this.lifecycleGeneration++;
520
+ this.trezorInitialization = undefined;
521
+ this.ledgerInitialization = undefined;
522
+ this.unsubscribeTrezor?.();
523
+ this.unsubscribeTrezor = undefined;
401
524
  // Stop monitoring
402
525
  if (this.statusMonitorInterval) {
403
526
  clearInterval(this.statusMonitorInterval);
404
527
  this.statusMonitorInterval = null;
405
528
  }
406
- // Close all connections
407
- for (const key of this.connectionPool.keys()) {
408
- await this.closeConnection(key);
529
+ const entries = [...this.connectionPool.entries()];
530
+ const trezor = this.connectionPool.get('trezor-default');
531
+ if (trezor)
532
+ trezor.status = ConnectionStatus.DISCONNECTED;
533
+ let resolve;
534
+ let reject;
535
+ const destruction = new Promise((done, failed) => {
536
+ resolve = done;
537
+ reject = failed;
538
+ });
539
+ this.destruction = destruction;
540
+ // Revoke Trezor first, then Ledger in this same call stack. Awaiting either
541
+ // cleanup before revoking the other could admit an already-queued response.
542
+ const trezorCleanup = entries
543
+ .filter(([key]) => key.startsWith(HardwareWalletType.TREZOR))
544
+ .map(([key, entry]) => this.closeConnection(key, entry));
545
+ let ledgerCleanup;
546
+ try {
547
+ ledgerCleanup = Promise.resolve(this.ledgerHost?.disconnect()).catch((error) => {
548
+ console.error('Error disconnecting Ledger host:', error);
549
+ });
550
+ }
551
+ catch (error) {
552
+ console.error('Error disconnecting Ledger host:', error);
553
+ ledgerCleanup = Promise.resolve();
409
554
  }
410
- this.removeAllListeners();
555
+ const close = async () => {
556
+ try {
557
+ await Promise.all([...trezorCleanup, ledgerCleanup]);
558
+ for (const [key, entry] of entries) {
559
+ if (this.connectionPool.get(key) === entry)
560
+ this.connectionPool.delete(key);
561
+ }
562
+ this.removeAllListeners();
563
+ }
564
+ finally {
565
+ if (this.destruction === destruction)
566
+ this.destruction = undefined;
567
+ }
568
+ };
569
+ void close().then(resolve, reject);
570
+ return destruction;
411
571
  }
412
572
  /**
413
573
  * Calculate exponential backoff delay
@@ -419,14 +579,6 @@ class HardwareWalletManager extends events_1.EventEmitter {
419
579
  const jitter = delay * 0.2 * (Math.random() - 0.5);
420
580
  return Math.round(delay + jitter);
421
581
  }
422
- /**
423
- * Create a timeout promise
424
- */
425
- createTimeoutPromise(ms, message) {
426
- return new Promise((_, reject) => {
427
- setTimeout(() => reject(new Error(message)), ms);
428
- });
429
- }
430
582
  /**
431
583
  * Delay helper
432
584
  */
@@ -470,6 +622,27 @@ class HardwareWalletManager extends events_1.EventEmitter {
470
622
  const message = (err?.message || '').toLowerCase();
471
623
  const name = err?.name || '';
472
624
  const statusCode = err?.statusCode;
625
+ const code = err?.code;
626
+ if (code === 'HARDWARE_REQUEST_CANCELLED' ||
627
+ code === 'HARDWARE_CLEAR_SIGNING_UNAVAILABLE' ||
628
+ code === 'HARDWARE_ACCOUNT_MISMATCH' ||
629
+ code === 'LEDGER_INVALID_REQUEST' ||
630
+ code === 'LEDGER_INVALID_RESPONSE' ||
631
+ code === 'LEDGER_INVALID_SIGNATURE' ||
632
+ code === 'HARDWARE_SIGNER_UNAVAILABLE' ||
633
+ code === 'HARDWARE_SIGNER_BUSY' ||
634
+ code === 'Method_Cancel' ||
635
+ code === 'Method_Interrupted' ||
636
+ code === 'Method_PermissionsNotGranted' ||
637
+ code === 'Method_Unsupported' ||
638
+ code === 'Method_UnsupportedCoinForHost' ||
639
+ code === 'Device_Disconnected' ||
640
+ code === 'Device_NotFound' ||
641
+ code === 'Device_CallInProgress' ||
642
+ code === 'Transport_Missing' ||
643
+ code === 'Desktop_ConnectionMissing' ||
644
+ code === 'Browser_LocalNetworkPermissionMissing')
645
+ return true;
473
646
  // Ledger: 0x6985 (Conditions of use not satisfied) on rejection
474
647
  if (statusCode === 0x6985)
475
648
  return true;
@@ -480,6 +653,7 @@ class HardwareWalletManager extends events_1.EventEmitter {
480
653
  message.includes('satisf')) ||
481
654
  message.includes('not allowed') ||
482
655
  message.includes('permission denied') ||
656
+ message.includes('permissions not granted') ||
483
657
  message.includes('user rejected') ||
484
658
  message.includes('denied by the user') ||
485
659
  message.includes('failure_actioncancelled') ||