@exodus/hardware-wallets 3.4.0 → 3.5.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.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,22 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [3.5.0](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/hardware-wallets@3.4.0...@exodus/hardware-wallets@3.5.0) (2025-12-11)
7
+
8
+ ### Features
9
+
10
+ - feat: throw device unininitialized error for hw (#14601)
11
+
12
+ ### Bug Fixes
13
+
14
+ - fix: various trezor and hardware-wallets fixes (#14502)
15
+
16
+ ## [3.4.1](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/hardware-wallets@3.4.0...@exodus/hardware-wallets@3.4.1) (2025-11-28)
17
+
18
+ ### Bug Fixes
19
+
20
+ - fix: various trezor and hardware-wallets fixes (#14502)
21
+
6
22
  ## [3.4.0](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/hardware-wallets@3.3.0...@exodus/hardware-wallets@3.4.0) (2025-11-13)
7
23
 
8
24
  ### Features
@@ -3,7 +3,7 @@ import { WalletAccount } from '@exodus/models';
3
3
  import Emitter from '@exodus/wild-emitter';
4
4
  import { randomBytes } from '@exodus/crypto/randomBytes';
5
5
  import delay from 'delay';
6
- import { NoDeviceFoundError, UserRefusedError } from '@exodus/hw-common';
6
+ import { DeviceUninitializedError, NoDeviceFoundError, UserRefusedError } from '@exodus/hw-common';
7
7
  import restrictConcurrency from 'make-concurrent';
8
8
  import pDefer from 'p-defer';
9
9
  export class HardwareWallets {
@@ -18,6 +18,7 @@ export class HardwareWallets {
18
18
  #walletAccounts;
19
19
  #syncedKeysMap = new Map();
20
20
  #signingRequest;
21
+ #isRetrying = false;
21
22
  events = new Emitter();
22
23
  constructor({ assetsModule, ledgerDiscovery, trezorDiscovery, logger, hardwareWalletSigningRequestsAtom, publicKeyStore, wallet, walletAccountsAtom, walletAccounts, }) {
23
24
  this.#assetsModule = assetsModule;
@@ -30,12 +31,41 @@ export class HardwareWallets {
30
31
  this.#walletAccountsAtom = walletAccountsAtom;
31
32
  this.#walletAccounts = walletAccounts;
32
33
  }
33
- #getSelectedDevice = async () => {
34
- const [trezors, ledgers] = await Promise.all([
35
- this.#trezorDiscovery.list().catch(() => []),
36
- this.#ledgerDiscovery.list().catch(() => []),
37
- ]);
38
- const descriptors = [...trezors, ...ledgers];
34
+ #listTrezorDevicesOrEmpty = async () => {
35
+ try {
36
+ return await this.#trezorDiscovery.list();
37
+ }
38
+ catch (error) {
39
+ if (error instanceof DeviceUninitializedError) {
40
+ throw error;
41
+ }
42
+ return [];
43
+ }
44
+ };
45
+ #listLedgerDevicesOrEmpty = async () => {
46
+ try {
47
+ return await this.#ledgerDiscovery.list();
48
+ }
49
+ catch {
50
+ return [];
51
+ }
52
+ };
53
+ #getSelectedDevice = async (walletAccount) => {
54
+ let descriptors;
55
+ const manufacturer = walletAccount?.source;
56
+ if (manufacturer === 'ledger') {
57
+ descriptors = await this.#listLedgerDevicesOrEmpty();
58
+ }
59
+ else if (manufacturer === 'trezor') {
60
+ descriptors = await this.#listTrezorDevicesOrEmpty();
61
+ }
62
+ else {
63
+ const [trezors, ledgers] = await Promise.all([
64
+ this.#listTrezorDevicesOrEmpty(),
65
+ this.#listLedgerDevicesOrEmpty(),
66
+ ]);
67
+ descriptors = [...trezors, ...ledgers];
68
+ }
39
69
  if (descriptors[0]) {
40
70
  return { device: await descriptors[0].get() };
41
71
  }
@@ -62,9 +92,14 @@ export class HardwareWallets {
62
92
  this.#logger.warn(`No signing request found for id: ${id}`);
63
93
  return;
64
94
  }
95
+ if (this.#isRetrying) {
96
+ this.#logger.debug(`Retry already in progress for id: ${id}, ignoring duplicate call`);
97
+ return;
98
+ }
99
+ this.#isRetrying = true;
65
100
  try {
66
101
  this.#logger.debug(`Attempting to get selected device for signing request with id: ${id}`);
67
- const { device } = await this.#getSelectedDevice();
102
+ const { device } = await this.#getSelectedDevice(request.walletAccount);
68
103
  this.#logger.debug(`Attempting to sign for signing request with id: ${id}`);
69
104
  const result = await request.sign({ device });
70
105
  await this.#deleteSigningRequest(id);
@@ -79,6 +114,7 @@ export class HardwareWallets {
79
114
  if (['DisconnectedDevice', 'DisconnectedDeviceDuringOperation'].includes(_error.name)) {
80
115
  this.#logger.debug(`Device disconnected during signing request, likely due to app opening: ${id}`, _error);
81
116
  await delay(300);
117
+ this.#isRetrying = false;
82
118
  await this.retrySigningRequest(id);
83
119
  return;
84
120
  }
@@ -93,6 +129,9 @@ export class HardwareWallets {
93
129
  baseAssetName: this.#signingRequest.baseAssetName,
94
130
  });
95
131
  }
132
+ finally {
133
+ this.#isRetrying = false;
134
+ }
96
135
  };
97
136
  cancelSigningRequest = async (id, fromUI) => {
98
137
  const request = this.#signingRequest;
@@ -105,7 +144,7 @@ export class HardwareWallets {
105
144
  if (fromUI) {
106
145
  this.#logger.debug(`Cancelling signing request on device for id: ${id}`);
107
146
  try {
108
- const { device } = await this.#getSelectedDevice();
147
+ const { device } = await this.#getSelectedDevice(request.walletAccount);
109
148
  await device.cancelAction();
110
149
  this.#logger.debug(`Succesfully cancelled signing request on device for id: ${id}`);
111
150
  }
@@ -115,13 +154,14 @@ export class HardwareWallets {
115
154
  }
116
155
  request.reject(new UserRefusedError(!fromUI));
117
156
  };
118
- #signGeneric = restrictConcurrency(async ({ baseAssetName, scenario, sign }) => {
157
+ #signGeneric = restrictConcurrency(async ({ baseAssetName, scenario, sign, walletAccount }) => {
119
158
  const id = randomBytes(16).toString('hex');
120
159
  this.#logger.debug(`Starting signing request for ${baseAssetName} with scenario: ${scenario} and id: ${id}`);
121
160
  const deferred = pDefer();
122
161
  this.#signingRequest = {
123
162
  id,
124
163
  baseAssetName,
164
+ walletAccount,
125
165
  sign: async ({ device }) => {
126
166
  await this.#updateSigningRequest({
127
167
  id,
@@ -148,7 +188,12 @@ export class HardwareWallets {
148
188
  multisigData,
149
189
  });
150
190
  };
151
- return this.#signGeneric({ baseAssetName, scenario: 'signTransaction', sign });
191
+ return this.#signGeneric({
192
+ baseAssetName,
193
+ scenario: 'signTransaction',
194
+ sign,
195
+ walletAccount,
196
+ });
152
197
  };
153
198
  signMessage = async ({ assetName, derivationPath, message }) => {
154
199
  const baseAssetName = this.#assetsModule.getAsset(assetName).baseAsset.name;
@@ -168,7 +213,11 @@ export class HardwareWallets {
168
213
  return true;
169
214
  }
170
215
  }
171
- catch { }
216
+ catch (error) {
217
+ if (error instanceof DeviceUninitializedError) {
218
+ throw error;
219
+ }
220
+ }
172
221
  try {
173
222
  const devices = await this.#ledgerDiscovery.list();
174
223
  return devices.length > 0;
@@ -189,8 +238,8 @@ export class HardwareWallets {
189
238
  };
190
239
  getAvailableDevices = async () => {
191
240
  const [ledgers, trezors] = await Promise.all([
192
- this.#ledgerDiscovery.list().catch(() => []),
193
- this.#trezorDiscovery.list().catch(() => []),
241
+ this.#listLedgerDevicesOrEmpty(),
242
+ this.#listTrezorDevicesOrEmpty(),
194
243
  ]);
195
244
  return [...ledgers, ...trezors].map((device) => ({
196
245
  model: device.model,
@@ -227,10 +276,10 @@ export class HardwareWallets {
227
276
  getAddress = async ({ assetName, accountIndex, addressIndex, multisigData, displayOnDevice, }) => {
228
277
  const asset = this.#assetsModule.getAsset(assetName);
229
278
  const { device } = await this.#getSelectedDevice();
230
- const supportedPurposes = asset.baseAsset.api.getSupportedPurposes({
279
+ const supportedPurposes = asset.baseAsset.api.getSupportedPurposes?.({
231
280
  compatibilityMode: device.descriptor.manufacturer,
232
281
  isMultisig: !!multisigData,
233
- });
282
+ }) ?? [44];
234
283
  const { derivationPath } = asset.baseAsset.api.getKeyIdentifier({
235
284
  compatibilityMode: device.descriptor.manufacturer,
236
285
  purpose: supportedPurposes[0],
@@ -348,10 +397,10 @@ export class HardwareWallets {
348
397
  continue;
349
398
  }
350
399
  const baseAsset = asset.baseAsset;
351
- const supportedPurposes = baseAsset.api.getSupportedPurposes({
400
+ const supportedPurposes = baseAsset.api.getSupportedPurposes?.({
352
401
  compatibilityMode: source,
353
402
  isMultisig,
354
- });
403
+ }) ?? [44];
355
404
  for (const purpose of supportedPurposes) {
356
405
  const { keyIdentifier, xpub, publicKey, } = (await this.#getXPUB({ device, baseAsset, purpose, accountIndex })) ||
357
406
  (await this.#getPublicKey({ device, baseAsset, purpose, accountIndex }));
@@ -381,6 +430,10 @@ export class HardwareWallets {
381
430
  const { device } = await this.#getSelectedDevice();
382
431
  const source = device.descriptor.manufacturer;
383
432
  const label = source === WalletAccount.LEDGER_SRC ? 'Ledger' : 'Trezor';
433
+ const id = source === WalletAccount.TREZOR_SRC
434
+ ? device.descriptor.internalDescriptor
435
+ :
436
+ randomBytes(32).toString('hex');
384
437
  const { accountIndex, model } = this.#syncedKeysMap.get(syncedKeysId);
385
438
  const walletAccount = new WalletAccount({
386
439
  label: `${label}${accountIndex === 0 ? '' : '_' + accountIndex}`,
@@ -388,7 +441,7 @@ export class HardwareWallets {
388
441
  source,
389
442
  model,
390
443
  index: accountIndex,
391
- id: randomBytes(32).toString('hex'),
444
+ id,
392
445
  isMultisig: !!isMultisig,
393
446
  });
394
447
  const walletAccountName = walletAccount.toString();
@@ -84,6 +84,7 @@ export interface GenericSignParams {
84
84
  baseAssetName: string;
85
85
  scenario: 'signTransaction' | 'signMessage';
86
86
  sign: GenericSignCallback;
87
+ walletAccount?: WalletAccount;
87
88
  }
88
89
  export interface SigningRequestState {
89
90
  id: string;
@@ -94,6 +95,7 @@ export interface SigningRequestState {
94
95
  export interface SigningRequest {
95
96
  id: string;
96
97
  baseAssetName?: string;
98
+ walletAccount?: WalletAccount;
97
99
  sign: GenericSignCallback;
98
100
  resolve: (result: any) => void;
99
101
  reject: (error: Error) => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/hardware-wallets",
3
- "version": "3.4.0",
3
+ "version": "3.5.0",
4
4
  "description": "An Exodus SDK feature that provides a high level abstraction for interacting with hardware wallet devices",
5
5
  "author": "Exodus Movement, Inc.",
6
6
  "repository": {
@@ -32,7 +32,7 @@
32
32
  "@exodus/basic-utils": "^3.2.0",
33
33
  "@exodus/bip32": "^4.0.2",
34
34
  "@exodus/crypto": "^1.0.0-rc.14",
35
- "@exodus/hw-common": "^3.2.0",
35
+ "@exodus/hw-common": "^3.3.0",
36
36
  "@exodus/models": "^12.18.0",
37
37
  "@exodus/redux-dependency-injection": "^4.0.0",
38
38
  "@exodus/wild-emitter": "^1.1.0",
@@ -53,5 +53,5 @@
53
53
  "access": "public",
54
54
  "provenance": false
55
55
  },
56
- "gitHead": "781bdadc2bb8d8a749ade2dd01f3f7f62792363d"
56
+ "gitHead": "91c5651b997e904755c35158697523f816611d71"
57
57
  }