@exodus/hardware-wallets 3.4.1 → 3.6.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.6.0](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/hardware-wallets@3.5.0...@exodus/hardware-wallets@3.6.0) (2026-01-21)
7
+
8
+ ### Features
9
+
10
+ - feat: trezor passphrase support (#14965)
11
+
12
+ ## [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)
13
+
14
+ ### Features
15
+
16
+ - feat: throw device unininitialized error for hw (#14601)
17
+
18
+ ### Bug Fixes
19
+
20
+ - fix: various trezor and hardware-wallets fixes (#14502)
21
+
6
22
  ## [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)
7
23
 
8
24
  ### Bug Fixes
@@ -16,7 +16,6 @@ declare const hardwareWalletsApiDefinition: {
16
16
  listUseableAssetNames: () => Promise<string[]>;
17
17
  scanForDevices: (deviceType?: import("libraries/hw-common/lib/types.js").HardwareWalletManufacturer) => Promise<void>;
18
18
  canAccessAsset: ({ assetName }: import("../module/interfaces.js").CanAccessAssetParams) => Promise<boolean>;
19
- ensureApplicationIsOpened: ({ assetName }: import("../module/interfaces.js").EnsureApplicationIsOpenedParams) => Promise<void>;
20
19
  scan: ({ assetName, accountIndexes, addressLimit, addressOffset, }: import("../module/interfaces.js").ScanParams) => Promise<import("../module/interfaces.js").ScanResult>;
21
20
  sync: ({ accountIndex: index, isMultisig }?: import("../module/interfaces.js").SyncParams) => Promise<import("../module/interfaces.js").SyncedKeysId>;
22
21
  addPublicKeysToWalletAccount: ({ walletAccount, syncedKeysId }: import("../module/interfaces.js").StoreSyncedKeysParams) => Promise<void>;
@@ -25,6 +24,7 @@ declare const hardwareWalletsApiDefinition: {
25
24
  cancelSigningRequest: (id: string, fromUI: boolean) => Promise<void>;
26
25
  submitPairingCode: ({ code }: import("../module/interfaces.js").ProcessPairingCodeParams) => Promise<any>;
27
26
  clearPairingRequest: () => () => Promise<void>;
27
+ setPassphraseMode: ({ enabled }: import("../module/interfaces.js").SetPassphraseModeParams) => Promise<void>;
28
28
  };
29
29
  };
30
30
  readonly dependencies: readonly ["hardwareWallets", "txLogMonitors", "restoreProgressTracker"];
package/lib/api/index.js CHANGED
@@ -23,7 +23,6 @@ const createHardwareWalletsApi = ({ hardwareWallets, restoreProgressTracker, txL
23
23
  listUseableAssetNames: hardwareWallets.listUseableAssetNames,
24
24
  scanForDevices: hardwareWallets.scanForDevices,
25
25
  canAccessAsset: hardwareWallets.canAccessAsset,
26
- ensureApplicationIsOpened: hardwareWallets.ensureApplicationIsOpened,
27
26
  scan: hardwareWallets.scan,
28
27
  sync: hardwareWallets.sync,
29
28
  addPublicKeysToWalletAccount: hardwareWallets.addPublicKeysToWalletAccount,
@@ -32,6 +31,7 @@ const createHardwareWalletsApi = ({ hardwareWallets, restoreProgressTracker, txL
32
31
  cancelSigningRequest: hardwareWallets.cancelSigningRequest,
33
32
  submitPairingCode: hardwareWallets.submitPairingCode,
34
33
  clearPairingRequest: () => hardwareWallets.clearPairingRequest,
34
+ setPassphraseMode: hardwareWallets.setPassphraseMode,
35
35
  },
36
36
  };
37
37
  };
package/lib/index.d.ts CHANGED
@@ -19,7 +19,6 @@ declare const hardwareWallets: () => {
19
19
  listUseableAssetNames: () => Promise<string[]>;
20
20
  scanForDevices: (deviceType?: import("libraries/hw-common/lib/types.js").HardwareWalletManufacturer) => Promise<void>;
21
21
  canAccessAsset: ({ assetName }: import("./module/interfaces.js").CanAccessAssetParams) => Promise<boolean>;
22
- ensureApplicationIsOpened: ({ assetName }: import("./module/interfaces.js").EnsureApplicationIsOpenedParams) => Promise<void>;
23
22
  scan: ({ assetName, accountIndexes, addressLimit, addressOffset, }: import("./module/interfaces.js").ScanParams) => Promise<import("./module/interfaces.js").ScanResult>;
24
23
  sync: ({ accountIndex: index, isMultisig }?: import("./module/interfaces.js").SyncParams) => Promise<import("./module/interfaces.js").SyncedKeysId>;
25
24
  addPublicKeysToWalletAccount: ({ walletAccount, syncedKeysId }: import("./module/interfaces.js").StoreSyncedKeysParams) => Promise<void>;
@@ -28,6 +27,7 @@ declare const hardwareWallets: () => {
28
27
  cancelSigningRequest: (id: string, fromUI: boolean) => Promise<void>;
29
28
  submitPairingCode: ({ code }: import("./module/interfaces.js").ProcessPairingCodeParams) => Promise<any>;
30
29
  clearPairingRequest: () => () => Promise<void>;
30
+ setPassphraseMode: ({ enabled }: import("./module/interfaces.js").SetPassphraseModeParams) => Promise<void>;
31
31
  };
32
32
  };
33
33
  readonly dependencies: readonly ["hardwareWallets", "txLogMonitors", "restoreProgressTracker"];
@@ -1,6 +1,6 @@
1
1
  import { WalletAccount } from '@exodus/models';
2
2
  import Emitter from '@exodus/wild-emitter';
3
- import type { HardwareSignerProvider, CanAccessAssetParams, CreateParams, StoreSyncedKeysParams, ScanParams, SyncParams, EnsureApplicationIsOpenedParams, SignTransactionParams, ScanResult, SyncedKeysId, GetAddressParams, RequireDeviceForParams, ProcessPairingCodeParams, SigningRequestState } from './interfaces.js';
3
+ import type { HardwareSignerProvider, CanAccessAssetParams, CreateParams, StoreSyncedKeysParams, ScanParams, SyncParams, SignTransactionParams, ScanResult, SyncedKeysId, GetAddressParams, RequireDeviceForParams, ProcessPairingCodeParams, SigningRequestState, SetPassphraseModeParams } from './interfaces.js';
4
4
  import type { HardwareWalletDiscovery, HardwareWalletManufacturer, SignMessageParams } from '@exodus/hw-common';
5
5
  import type { Atom } from '@exodus/atoms';
6
6
  import type { IPublicKeyStore } from '@exodus/public-key-provider/lib/module/store/types';
@@ -38,7 +38,6 @@ export declare class HardwareWallets implements HardwareSignerProvider {
38
38
  }[]>;
39
39
  canAccessAsset: ({ assetName }: CanAccessAssetParams) => Promise<boolean>;
40
40
  listUseableAssetNames: () => Promise<string[]>;
41
- ensureApplicationIsOpened: ({ assetName }: EnsureApplicationIsOpenedParams) => Promise<void>;
42
41
  getAddress: ({ assetName, accountIndex, addressIndex, multisigData, displayOnDevice, }: GetAddressParams) => Promise<string>;
43
42
  scan: ({ assetName, accountIndexes, addressLimit, addressOffset, }: ScanParams) => Promise<ScanResult>;
44
43
  sync: ({ accountIndex: index, isMultisig }?: SyncParams) => Promise<SyncedKeysId>;
@@ -46,6 +45,7 @@ export declare class HardwareWallets implements HardwareSignerProvider {
46
45
  create: ({ syncedKeysId, isMultisig }: CreateParams) => Promise<WalletAccount>;
47
46
  clearPairingRequest: () => Promise<void>;
48
47
  submitPairingCode: ({ code }: ProcessPairingCodeParams) => Promise<any>;
48
+ setPassphraseMode: ({ enabled }: SetPassphraseModeParams) => Promise<void>;
49
49
  requireDeviceFor: ({ walletAccount }: RequireDeviceForParams) => Promise<{
50
50
  signTransaction: ({ baseAssetName, unsignedTx, walletAccount, multisigData, }: SignTransactionParams) => Promise<any>;
51
51
  signMessage: ({ assetName, derivationPath, message }: SignMessageParams) => Promise<any>;
@@ -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, HardwareWalletManufacturerMismatchError, UserRefusedError, } from '@exodus/hw-common';
7
7
  import restrictConcurrency from 'make-concurrent';
8
8
  import pDefer from 'p-defer';
9
9
  export class HardwareWallets {
@@ -20,6 +20,15 @@ export class HardwareWallets {
20
20
  #signingRequest;
21
21
  #isRetrying = false;
22
22
  events = new Emitter();
23
+ #requireTrezorDevice = (device) => {
24
+ if (device.descriptor.manufacturer !== WalletAccount.TREZOR_SRC) {
25
+ throw new HardwareWalletManufacturerMismatchError({
26
+ expected: WalletAccount.TREZOR_SRC,
27
+ actual: String(device.descriptor.manufacturer),
28
+ });
29
+ }
30
+ return device;
31
+ };
23
32
  constructor({ assetsModule, ledgerDiscovery, trezorDiscovery, logger, hardwareWalletSigningRequestsAtom, publicKeyStore, wallet, walletAccountsAtom, walletAccounts, }) {
24
33
  this.#assetsModule = assetsModule;
25
34
  this.#ledgerDiscovery = ledgerDiscovery;
@@ -31,12 +40,41 @@ export class HardwareWallets {
31
40
  this.#walletAccountsAtom = walletAccountsAtom;
32
41
  this.#walletAccounts = walletAccounts;
33
42
  }
34
- #getSelectedDevice = async () => {
35
- const [trezors, ledgers] = await Promise.all([
36
- this.#trezorDiscovery.list().catch(() => []),
37
- this.#ledgerDiscovery.list().catch(() => []),
38
- ]);
39
- const descriptors = [...trezors, ...ledgers];
43
+ #listTrezorDevicesOrEmpty = async () => {
44
+ try {
45
+ return await this.#trezorDiscovery.list();
46
+ }
47
+ catch (error) {
48
+ if (error instanceof DeviceUninitializedError) {
49
+ throw error;
50
+ }
51
+ return [];
52
+ }
53
+ };
54
+ #listLedgerDevicesOrEmpty = async () => {
55
+ try {
56
+ return await this.#ledgerDiscovery.list();
57
+ }
58
+ catch {
59
+ return [];
60
+ }
61
+ };
62
+ #getSelectedDevice = async (walletAccount) => {
63
+ let descriptors;
64
+ const manufacturer = walletAccount?.source;
65
+ if (manufacturer === 'ledger') {
66
+ descriptors = await this.#listLedgerDevicesOrEmpty();
67
+ }
68
+ else if (manufacturer === 'trezor') {
69
+ descriptors = await this.#listTrezorDevicesOrEmpty();
70
+ }
71
+ else {
72
+ const [trezors, ledgers] = await Promise.all([
73
+ this.#listTrezorDevicesOrEmpty(),
74
+ this.#listLedgerDevicesOrEmpty(),
75
+ ]);
76
+ descriptors = [...trezors, ...ledgers];
77
+ }
40
78
  if (descriptors[0]) {
41
79
  return { device: await descriptors[0].get() };
42
80
  }
@@ -70,7 +108,7 @@ export class HardwareWallets {
70
108
  this.#isRetrying = true;
71
109
  try {
72
110
  this.#logger.debug(`Attempting to get selected device for signing request with id: ${id}`);
73
- const { device } = await this.#getSelectedDevice();
111
+ const { device } = await this.#getSelectedDevice(request.walletAccount);
74
112
  this.#logger.debug(`Attempting to sign for signing request with id: ${id}`);
75
113
  const result = await request.sign({ device });
76
114
  await this.#deleteSigningRequest(id);
@@ -115,7 +153,7 @@ export class HardwareWallets {
115
153
  if (fromUI) {
116
154
  this.#logger.debug(`Cancelling signing request on device for id: ${id}`);
117
155
  try {
118
- const { device } = await this.#getSelectedDevice();
156
+ const { device } = await this.#getSelectedDevice(request.walletAccount);
119
157
  await device.cancelAction();
120
158
  this.#logger.debug(`Succesfully cancelled signing request on device for id: ${id}`);
121
159
  }
@@ -125,20 +163,21 @@ export class HardwareWallets {
125
163
  }
126
164
  request.reject(new UserRefusedError(!fromUI));
127
165
  };
128
- #signGeneric = restrictConcurrency(async ({ baseAssetName, scenario, sign }) => {
166
+ #signGeneric = restrictConcurrency(async ({ baseAssetName, scenario, sign, walletAccount }) => {
129
167
  const id = randomBytes(16).toString('hex');
130
168
  this.#logger.debug(`Starting signing request for ${baseAssetName} with scenario: ${scenario} and id: ${id}`);
131
169
  const deferred = pDefer();
132
170
  this.#signingRequest = {
133
171
  id,
134
172
  baseAssetName,
173
+ walletAccount,
135
174
  sign: async ({ device }) => {
136
175
  await this.#updateSigningRequest({
137
176
  id,
138
177
  baseAssetName,
139
178
  scenario,
140
179
  });
141
- await device.ensureApplicationIsOpened(baseAssetName);
180
+ await device.ensureDeviceReady({ baseAssetName, walletAccount });
142
181
  return sign({ device });
143
182
  },
144
183
  resolve: deferred.resolve,
@@ -158,7 +197,12 @@ export class HardwareWallets {
158
197
  multisigData,
159
198
  });
160
199
  };
161
- return this.#signGeneric({ baseAssetName, scenario: 'signTransaction', sign });
200
+ return this.#signGeneric({
201
+ baseAssetName,
202
+ scenario: 'signTransaction',
203
+ sign,
204
+ walletAccount,
205
+ });
162
206
  };
163
207
  signMessage = async ({ assetName, derivationPath, message }) => {
164
208
  const baseAssetName = this.#assetsModule.getAsset(assetName).baseAsset.name;
@@ -178,7 +222,11 @@ export class HardwareWallets {
178
222
  return true;
179
223
  }
180
224
  }
181
- catch { }
225
+ catch (error) {
226
+ if (error instanceof DeviceUninitializedError) {
227
+ throw error;
228
+ }
229
+ }
182
230
  try {
183
231
  const devices = await this.#ledgerDiscovery.list();
184
232
  return devices.length > 0;
@@ -199,8 +247,8 @@ export class HardwareWallets {
199
247
  };
200
248
  getAvailableDevices = async () => {
201
249
  const [ledgers, trezors] = await Promise.all([
202
- this.#ledgerDiscovery.list().catch(() => []),
203
- this.#trezorDiscovery.list().catch(() => []),
250
+ this.#listLedgerDevicesOrEmpty(),
251
+ this.#listTrezorDevicesOrEmpty(),
204
252
  ]);
205
253
  return [...ledgers, ...trezors].map((device) => ({
206
254
  model: device.model,
@@ -217,23 +265,6 @@ export class HardwareWallets {
217
265
  const { device } = await this.#getSelectedDevice();
218
266
  return device.listUseableAssetNames();
219
267
  };
220
- ensureApplicationIsOpened = async ({ assetName }) => {
221
- const asset = this.#assetsModule.getAsset(assetName);
222
- let i = 0;
223
- while (i < 3) {
224
- try {
225
- const { device } = await this.#getSelectedDevice();
226
- await device.ensureApplicationIsOpened(asset.baseAsset.name);
227
- }
228
- catch (error) {
229
- this.#logger.log(error);
230
- }
231
- finally {
232
- await delay(1000);
233
- i++;
234
- }
235
- }
236
- };
237
268
  getAddress = async ({ assetName, accountIndex, addressIndex, multisigData, displayOnDevice, }) => {
238
269
  const asset = this.#assetsModule.getAsset(assetName);
239
270
  const { device } = await this.#getSelectedDevice();
@@ -350,6 +381,7 @@ export class HardwareWallets {
350
381
  const { device } = await this.#getSelectedDevice();
351
382
  const source = device.descriptor.manufacturer;
352
383
  const accountIndex = index ?? this.#walletAccounts.getNextIndex({ source });
384
+ const walletId = (await device.getWalletId?.()) ?? randomBytes(32).toString('hex');
353
385
  const useableAssetNames = new Set(await device.listUseableAssetNames());
354
386
  for (const assetName of useableAssetNames) {
355
387
  const asset = this.#assetsModule.getAsset(assetName);
@@ -374,6 +406,7 @@ export class HardwareWallets {
374
406
  model: device.descriptor.model,
375
407
  assetNames: useableAssetNames,
376
408
  keysToSync,
409
+ walletId,
377
410
  });
378
411
  return id;
379
412
  };
@@ -391,18 +424,14 @@ export class HardwareWallets {
391
424
  const { device } = await this.#getSelectedDevice();
392
425
  const source = device.descriptor.manufacturer;
393
426
  const label = source === WalletAccount.LEDGER_SRC ? 'Ledger' : 'Trezor';
394
- const id = source === WalletAccount.TREZOR_SRC
395
- ? device.descriptor.internalDescriptor
396
- :
397
- randomBytes(32).toString('hex');
398
- const { accountIndex, model } = this.#syncedKeysMap.get(syncedKeysId);
427
+ const { accountIndex, model, walletId } = this.#syncedKeysMap.get(syncedKeysId);
399
428
  const walletAccount = new WalletAccount({
400
429
  label: `${label}${accountIndex === 0 ? '' : '_' + accountIndex}`,
401
430
  icon: source,
402
431
  source,
403
432
  model,
404
433
  index: accountIndex,
405
- id,
434
+ id: walletId,
406
435
  isMultisig: !!isMultisig,
407
436
  });
408
437
  const walletAccountName = walletAccount.toString();
@@ -417,6 +446,11 @@ export class HardwareWallets {
417
446
  submitPairingCode = async ({ code }) => {
418
447
  return this.#trezorDiscovery.submitPairingCode(code);
419
448
  };
449
+ setPassphraseMode = async ({ enabled }) => {
450
+ const { device } = await this.#getSelectedDevice();
451
+ const trezorDevice = this.#requireTrezorDevice(device);
452
+ trezorDevice.setPassphraseMode(enabled ? 'hidden' : 'standard');
453
+ };
420
454
  requireDeviceFor = async ({ walletAccount }) => {
421
455
  return {
422
456
  signTransaction: this.signTransaction,
@@ -6,7 +6,6 @@ export interface HardwareSignerProvider {
6
6
  scanForDevices: (deviceType: HardwareWalletManufacturer) => Promise<void>;
7
7
  canAccessAsset: ({ assetName }: CanAccessAssetParams) => Promise<boolean>;
8
8
  listUseableAssetNames: () => Promise<string[]>;
9
- ensureApplicationIsOpened: ({ assetName }: EnsureApplicationIsOpenedParams) => Promise<void>;
10
9
  scan: ({ assetName, accountIndexes, addressOffset }: ScanParams) => Promise<ScanResult>;
11
10
  sync: ({ accountIndex }: SyncParams) => Promise<SyncedKeysId>;
12
11
  addPublicKeysToWalletAccount: ({ walletAccount, syncedKeysId, }: StoreSyncedKeysParams) => Promise<void>;
@@ -14,6 +13,7 @@ export interface HardwareSignerProvider {
14
13
  signTransaction: ({ baseAssetName, unsignedTx, walletAccount, }: SignTransactionParams) => Promise<any>;
15
14
  signMessage: ({ assetName, derivationPath, message }: SignMessageParams) => Promise<any>;
16
15
  submitPairingCode: ({ code }: ProcessPairingCodeParams) => Promise<any>;
16
+ setPassphraseMode: ({ enabled }: SetPassphraseModeParams) => Promise<void>;
17
17
  }
18
18
  type Asset = {
19
19
  name: string;
@@ -64,6 +64,7 @@ export interface SyncedKeysData {
64
64
  model: HardwareWalletDeviceModels;
65
65
  assetNames: Set<string>;
66
66
  keysToSync: KeyToSyncData[];
67
+ walletId: string;
67
68
  }
68
69
  export type KeyToSyncData = [KeyIdentifier, {
69
70
  xpub?: string;
@@ -84,6 +85,7 @@ export interface GenericSignParams {
84
85
  baseAssetName: string;
85
86
  scenario: 'signTransaction' | 'signMessage';
86
87
  sign: GenericSignCallback;
88
+ walletAccount?: WalletAccount;
87
89
  }
88
90
  export interface SigningRequestState {
89
91
  id: string;
@@ -94,6 +96,7 @@ export interface SigningRequestState {
94
96
  export interface SigningRequest {
95
97
  id: string;
96
98
  baseAssetName?: string;
99
+ walletAccount?: WalletAccount;
97
100
  sign: GenericSignCallback;
98
101
  resolve: (result: any) => void;
99
102
  reject: (error: Error) => void;
@@ -146,4 +149,7 @@ export interface RequireDeviceForParams {
146
149
  export interface ProcessPairingCodeParams {
147
150
  code: string;
148
151
  }
152
+ export interface SetPassphraseModeParams {
153
+ enabled: boolean;
154
+ }
149
155
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/hardware-wallets",
3
- "version": "3.4.1",
3
+ "version": "3.6.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.4.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": "7c1b8b88571d94cec15b9512e5301922ac1b6017"
56
+ "gitHead": "43e7fa9467acd5e681d99473eb54d9e790130e04"
57
57
  }