@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 +16 -0
- package/lib/module/hardware-wallets.js +72 -19
- package/lib/module/interfaces.d.ts +2 -0
- package/package.json +3 -3
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
|
-
#
|
|
34
|
-
|
|
35
|
-
this.#trezorDiscovery.list()
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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({
|
|
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.#
|
|
193
|
-
this.#
|
|
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
|
|
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.
|
|
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.
|
|
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": "
|
|
56
|
+
"gitHead": "91c5651b997e904755c35158697523f816611d71"
|
|
57
57
|
}
|