@exodus/hardware-wallets 1.4.0 → 1.5.1

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,23 @@
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
+ ## [1.5.1](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/hardware-wallets@1.5.0...@exodus/hardware-wallets@1.5.1) (2024-10-17)
7
+
8
+ ### Bug Fixes
9
+
10
+ - support cancelling transaction through ui ([#10058](https://github.com/ExodusMovement/exodus-hydra/issues/10058)) ([beb7d2d](https://github.com/ExodusMovement/exodus-hydra/commit/beb7d2d1aafba17678a34febcf2458163d9182d2))
11
+
12
+ ## [1.5.0](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/hardware-wallets@1.4.0...@exodus/hardware-wallets@1.5.0) (2024-10-17)
13
+
14
+ ### Features
15
+
16
+ - add `getSelectedDevice` ([#9822](https://github.com/ExodusMovement/exodus-hydra/issues/9822)) ([2c18c22](https://github.com/ExodusMovement/exodus-hydra/commit/2c18c22d419ba9136cd8b264e946b151df438c59))
17
+ - support adding model to wallet account ([#9936](https://github.com/ExodusMovement/exodus-hydra/issues/9936)) ([8c858f6](https://github.com/ExodusMovement/exodus-hydra/commit/8c858f6e08e41bee3261f444c3d25e8bdd385014))
18
+
19
+ ### Bug Fixes
20
+
21
+ - unify user refused error handling ([#10052](https://github.com/ExodusMovement/exodus-hydra/issues/10052)) ([db85d10](https://github.com/ExodusMovement/exodus-hydra/commit/db85d108333630d09bad545c5ec1169b937e08fe))
22
+
6
23
  ## [1.4.0](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/hardware-wallets@1.3.0...@exodus/hardware-wallets@1.4.0) (2024-10-07)
7
24
 
8
25
  ### Features
@@ -10,7 +10,7 @@ declare const hardwareWalletsApiDefinition: {
10
10
  hardwareWallets: {
11
11
  isDeviceConnected: () => Promise<boolean>;
12
12
  getAvailableDevices: () => Promise<{
13
- model: ("blue" | "nanoS" | "nanoSP" | "nanoX" | "stax" | "europa") | ("1" | "t" | "Safe 3" | "Safe 5") | "unknown";
13
+ model: import("libraries/hw-common/lib/types.js").HardwareWalletDeviceModels;
14
14
  name: string;
15
15
  }[]>;
16
16
  listUseableAssetNames: () => Promise<string[]>;
package/lib/index.d.ts CHANGED
@@ -13,7 +13,7 @@ declare const hardwareWallets: () => {
13
13
  hardwareWallets: {
14
14
  isDeviceConnected: () => Promise<boolean>;
15
15
  getAvailableDevices: () => Promise<{
16
- model: ("blue" | "nanoS" | "nanoSP" | "nanoX" | "stax" | "europa") | ("1" | "t" | "Safe 3" | "Safe 5") | "unknown";
16
+ model: import("libraries/hw-common/lib/types.js").HardwareWalletDeviceModels;
17
17
  name: string;
18
18
  }[]>;
19
19
  listUseableAssetNames: () => Promise<string[]>;
@@ -26,7 +26,7 @@ export declare class HardwareWallets implements HardwareSignerProvider {
26
26
  isDeviceConnected: () => Promise<boolean>;
27
27
  scanForDevices: () => Promise<void>;
28
28
  getAvailableDevices: () => Promise<{
29
- model: ("blue" | "nanoS" | "nanoSP" | "nanoX" | "stax" | "europa") | ("1" | "t" | "Safe 3" | "Safe 5") | "unknown";
29
+ model: import("@exodus/hw-common").HardwareWalletDeviceModels;
30
30
  name: string;
31
31
  }[]>;
32
32
  canAccessAsset: ({ assetName }: CanAccessAssetParams) => Promise<boolean>;
@@ -3,7 +3,7 @@ import { WalletAccount } from '@exodus/models';
3
3
  import Emitter from '@exodus/wild-emitter';
4
4
  import randomBytes from 'randombytes';
5
5
  import delay from 'delay';
6
- import { UserCancelledError } from './errors.js';
6
+ import { NoDeviceFoundError, UserRefusedError } from '@exodus/hw-common';
7
7
  export class HardwareWallets {
8
8
  #assetsModule;
9
9
  #ledgerDiscovery;
@@ -25,26 +25,35 @@ export class HardwareWallets {
25
25
  this.#walletAccountsAtom = walletAccountsAtom;
26
26
  this.#walletAccounts = walletAccounts;
27
27
  }
28
+ #getSelectedDevice = async () => {
29
+ const descriptors = await this.#ledgerDiscovery.list();
30
+ if (descriptors[0]) {
31
+ return { device: await descriptors[0].get() };
32
+ }
33
+ throw new NoDeviceFoundError();
34
+ };
28
35
  #requestUserAction = async (params) => {
29
36
  const rpc = this.#userInterface.getRPC();
30
37
  return rpc.callMethod('handle-hardware-wallet', params);
31
38
  };
32
39
  #signGeneric = async ({ baseAssetName, scenario, sign }) => {
33
- let needUserApproval = true;
34
40
  let attempts = 0;
35
41
  const MAX_ATTEMPTS = 50;
36
42
  while (attempts < MAX_ATTEMPTS) {
37
43
  try {
38
- if (needUserApproval) {
39
- this.#requestUserAction({
40
- scenario,
41
- baseAssetName,
42
- });
43
- needUserApproval = false;
44
+ const approvePromise = this.#requestUserAction({
45
+ scenario,
46
+ baseAssetName,
47
+ });
48
+ const { device } = await this.#getSelectedDevice();
49
+ const runSign = async () => {
50
+ await device.ensureApplicationIsOpened(baseAssetName);
51
+ return sign({ device });
52
+ };
53
+ const result = await Promise.race([approvePromise, runSign()]);
54
+ if (typeof result === 'object' && result.tryAgain === false) {
55
+ throw new UserRefusedError(false);
44
56
  }
45
- const hardwareDevice = await this.#ledgerDiscovery.getFirstDevice();
46
- await hardwareDevice.ensureApplicationIsOpened(baseAssetName);
47
- const result = await sign({ hardwareDevice });
48
57
  this.#requestUserAction({ scenario: 'completed' });
49
58
  return result;
50
59
  }
@@ -52,11 +61,10 @@ export class HardwareWallets {
52
61
  if (error.message.includes('timeout')) {
53
62
  break;
54
63
  }
55
- if (error.name === 'DisconnectedDeviceDuringOperation') {
56
- this.#requestUserAction({
57
- scenario,
58
- baseAssetName,
59
- });
64
+ else if (error.name === 'UserRefusedError') {
65
+ throw error;
66
+ }
67
+ if (['DisconnectedDevice', 'DisconnectedDeviceDuringOperation'].includes(error.name)) {
60
68
  continue;
61
69
  }
62
70
  try {
@@ -80,15 +88,15 @@ export class HardwareWallets {
80
88
  }
81
89
  }
82
90
  this.#requestUserAction({ scenario: 'completed' });
83
- throw new UserCancelledError();
91
+ throw new UserRefusedError(false);
84
92
  };
85
93
  signTransaction = async ({ baseAssetName, unsignedTx, walletAccount, multisigData, }) => {
86
94
  const baseAsset = this.#assetsModule.getAsset(baseAssetName);
87
95
  const accountIndex = walletAccount.index;
88
- const sign = async ({ hardwareDevice }) => {
96
+ const sign = async ({ device }) => {
89
97
  return baseAsset.api.signHardware({
90
98
  unsignedTx,
91
- hardwareDevice,
99
+ hardwareDevice: device,
92
100
  accountIndex,
93
101
  multisigData,
94
102
  });
@@ -97,8 +105,8 @@ export class HardwareWallets {
97
105
  };
98
106
  signMessage = async ({ assetName, derivationPath, message }) => {
99
107
  const baseAssetName = this.#assetsModule.getAsset(assetName).baseAsset.name;
100
- const sign = async ({ hardwareDevice }) => {
101
- return hardwareDevice.signMessage({
108
+ const sign = async ({ device }) => {
109
+ return device.signMessage({
102
110
  assetName: baseAssetName,
103
111
  derivationPath,
104
112
  message,
@@ -128,12 +136,12 @@ export class HardwareWallets {
128
136
  };
129
137
  canAccessAsset = async ({ assetName }) => {
130
138
  const asset = this.#assetsModule.getAsset(assetName);
131
- const device = await this.#ledgerDiscovery.getFirstDevice();
139
+ const { device } = await this.#getSelectedDevice();
132
140
  const useableAssetNames = new Set(await device.listUseableAssetNames());
133
141
  return useableAssetNames.has(asset.baseAsset.name);
134
142
  };
135
143
  listUseableAssetNames = async () => {
136
- const device = await this.#ledgerDiscovery.getFirstDevice();
144
+ const { device } = await this.#getSelectedDevice();
137
145
  return device.listUseableAssetNames();
138
146
  };
139
147
  ensureApplicationIsOpened = async ({ assetName }) => {
@@ -141,8 +149,8 @@ export class HardwareWallets {
141
149
  let i = 0;
142
150
  while (i < 3) {
143
151
  try {
144
- const hardwareDevice = await this.#ledgerDiscovery.getFirstDevice();
145
- await hardwareDevice.ensureApplicationIsOpened(asset.baseAsset.name);
152
+ const { device } = await this.#getSelectedDevice();
153
+ await device.ensureApplicationIsOpened(asset.baseAsset.name);
146
154
  }
147
155
  catch (error) {
148
156
  this.#logger.log(error);
@@ -166,7 +174,7 @@ export class HardwareWallets {
166
174
  chainIndex: 0,
167
175
  addressIndex,
168
176
  });
169
- const device = await this.#ledgerDiscovery.getFirstDevice();
177
+ const { device } = await this.#getSelectedDevice();
170
178
  return device.getAddress({
171
179
  assetName,
172
180
  derivationPath,
@@ -259,7 +267,7 @@ export class HardwareWallets {
259
267
  };
260
268
  sync = async ({ accountIndex: index, isMultisig } = {}) => {
261
269
  const keysToSync = [];
262
- const device = await this.#ledgerDiscovery.getFirstDevice();
270
+ const { device } = await this.#getSelectedDevice();
263
271
  const accountIndex = index ?? this.#walletAccounts.getNextIndex({ source: WalletAccount.LEDGER_SRC });
264
272
  const useableAssetNames = new Set(await device.listUseableAssetNames());
265
273
  for (const assetName of useableAssetNames) {
@@ -279,6 +287,7 @@ export class HardwareWallets {
279
287
  const id = randomBytes(16).toString('hex');
280
288
  this.#syncedKeysMap.set(id, {
281
289
  accountIndex,
290
+ model: device.descriptor.model,
282
291
  assetNames: useableAssetNames,
283
292
  keysToSync,
284
293
  });
@@ -295,11 +304,12 @@ export class HardwareWallets {
295
304
  };
296
305
  create = async ({ syncedKeysId, isMultisig }) => {
297
306
  assert(this.#syncedKeysMap.has(syncedKeysId), `no synchronized keys found for id ${syncedKeysId}`);
298
- const { accountIndex } = this.#syncedKeysMap.get(syncedKeysId);
307
+ const { accountIndex, model } = this.#syncedKeysMap.get(syncedKeysId);
299
308
  const walletAccount = new WalletAccount({
300
309
  label: `Ledger${accountIndex === 0 ? '' : ' ' + accountIndex}`,
301
310
  icon: 'ledger',
302
311
  source: WalletAccount.LEDGER_SRC,
312
+ model,
303
313
  index: accountIndex,
304
314
  id: randomBytes(32).toString('hex'),
305
315
  isMultisig: !!isMultisig,
@@ -1,5 +1,5 @@
1
1
  import type { WalletAccount } from '@exodus/models';
2
- import type { HardwareWalletDevice, MultisigData, SignMessageParams } from '@exodus/hw-common';
2
+ import type { HardwareWalletDeviceModels, HardwareWalletDevice, MultisigData, SignMessageParams } from '@exodus/hw-common';
3
3
  import type KeyIdentifier from '@exodus/key-identifier';
4
4
  export interface HardwareSignerProvider {
5
5
  isDeviceConnected: () => Promise<boolean>;
@@ -59,6 +59,7 @@ export interface SyncParams {
59
59
  export type SyncedKeysId = string;
60
60
  export interface SyncedKeysData {
61
61
  accountIndex: number;
62
+ model: HardwareWalletDeviceModels;
62
63
  assetNames: Set<string>;
63
64
  keysToSync: KeyToSyncData[];
64
65
  }
@@ -74,8 +75,8 @@ export interface CreateParams {
74
75
  syncedKeysId: SyncedKeysId;
75
76
  isMultisig?: boolean;
76
77
  }
77
- export type GenericSignCallback = ({ hardwareDevice, }: {
78
- hardwareDevice: HardwareWalletDevice;
78
+ export type GenericSignCallback = ({ device }: {
79
+ device: HardwareWalletDevice;
79
80
  }) => Promise<any>;
80
81
  export interface GenericSignParams {
81
82
  baseAssetName: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/hardware-wallets",
3
- "version": "1.4.0",
3
+ "version": "1.5.1",
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": {
@@ -24,10 +24,11 @@
24
24
  "clean": "run -T tsc --build --clean",
25
25
  "lint": "run -T eslint .",
26
26
  "lint:fix": "yarn lint --fix",
27
- "test": "run -T jest",
27
+ "test": "run -T exodus-test --jest --esbuild",
28
28
  "prepublishOnly": "yarn run -T build --scope @exodus/hardware-wallets"
29
29
  },
30
30
  "dependencies": {
31
+ "@exodus/hw-common": "^3.0.0",
31
32
  "@exodus/models": "^12.0.1",
32
33
  "@exodus/wild-emitter": "^1.1.0",
33
34
  "delay": "^5.0.0",
@@ -37,11 +38,11 @@
37
38
  "devDependencies": {
38
39
  "@exodus/atoms": "^9.0.0",
39
40
  "@exodus/dependency-types": "^2.1.0",
40
- "@exodus/hw-common": "^2.5.0",
41
41
  "@exodus/key-identifier": "^1.3.0",
42
42
  "@exodus/logger": "^1.2.2",
43
43
  "@exodus/public-key-provider": "^3.0.0",
44
- "@types/randombytes": "^2.0.3"
44
+ "@types/randombytes": "^2.0.3",
45
+ "p-defer": "^4.0.1"
45
46
  },
46
- "gitHead": "c60b8443608e3f1d4673fff692f4aa03b816badd"
47
+ "gitHead": "c74a9968861467c805398c25b4017985e976ba31"
47
48
  }
@@ -1,4 +0,0 @@
1
- export declare class UserCancelledError extends Error {
2
- name: string;
3
- constructor();
4
- }
@@ -1,6 +0,0 @@
1
- export class UserCancelledError extends Error {
2
- name = 'UserCancelledError';
3
- constructor() {
4
- super('User cancelled action');
5
- }
6
- }