@exodus/hw-ledger 2.9.4 → 2.10.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,19 @@
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
+ ## [2.10.0](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/hw-ledger@2.9.5...@exodus/hw-ledger@2.10.0) (2024-09-16)
7
+
8
+ ### Features
9
+
10
+ - add bitcoin multisig support to ledger ([#9218](https://github.com/ExodusMovement/exodus-hydra/issues/9218)) ([1b63e8a](https://github.com/ExodusMovement/exodus-hydra/commit/1b63e8a6ac7bab9cd580f602f2fd0cbff48ed01c))
11
+
12
+ ## [2.9.5](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/hw-ledger@2.9.4...@exodus/hw-ledger@2.9.5) (2024-08-16)
13
+
14
+ ### Bug Fixes
15
+
16
+ - bluetooth on scan ([#8457](https://github.com/ExodusMovement/exodus-hydra/issues/8457)) ([c6cef7a](https://github.com/ExodusMovement/exodus-hydra/commit/c6cef7a19dad315bdbe8e01ea353f108348d48e3))
17
+ - use safe alloc ([#8467](https://github.com/ExodusMovement/exodus-hydra/issues/8467)) ([926ef36](https://github.com/ExodusMovement/exodus-hydra/commit/926ef36d1478e8fea290b56eabab5c807e7e6695))
18
+
6
19
  ## [2.9.4](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/hw-ledger@2.9.3...@exodus/hw-ledger@2.9.4) (2024-08-14)
7
20
 
8
21
  ### Bug Fixes
@@ -1,7 +1,12 @@
1
+ import type { Storage } from 'adapters/storage-interface';
2
+ export type BluetoothStatus = {
3
+ type: 'Unknown' | 'Resetting' | 'Unsupported' | 'Unauthorized' | 'PoweredOff' | 'PoweredOn';
4
+ available: boolean;
5
+ };
1
6
  export declare const bluetoothStatusAtomDefinition: {
2
7
  readonly id: "bluetoothStatusAtom";
3
8
  readonly type: "atom";
4
- readonly factory: () => import("@exodus/atoms").Atom<boolean>;
9
+ readonly factory: () => import("@exodus/atoms").Atom<BluetoothStatus>;
5
10
  readonly dependencies: readonly [];
6
11
  readonly public: true;
7
12
  };
@@ -12,3 +17,14 @@ export declare const bluetoothScanningAtomDefinition: {
12
17
  readonly dependencies: readonly [];
13
18
  readonly public: true;
14
19
  };
20
+ type Params = {
21
+ storage: Storage<Record<string, Buffer>>;
22
+ };
23
+ export declare const walletPolicyAtomDefinition: {
24
+ readonly id: "walletPolicyAtom";
25
+ readonly type: "atom";
26
+ readonly factory: ({ storage }: Params) => import("@exodus/atoms").Atom<Record<string, Buffer> | {}>;
27
+ readonly dependencies: readonly ["storage"];
28
+ readonly public: true;
29
+ };
30
+ export {};
@@ -1,8 +1,10 @@
1
- import { createInMemoryAtom } from '@exodus/atoms';
1
+ import { createInMemoryAtom, createStorageAtomFactory } from '@exodus/atoms';
2
2
  export const bluetoothStatusAtomDefinition = {
3
3
  id: 'bluetoothStatusAtom',
4
4
  type: 'atom',
5
- factory: () => createInMemoryAtom({ defaultValue: false }),
5
+ factory: () => createInMemoryAtom({
6
+ defaultValue: { type: 'Unknown', available: false },
7
+ }),
6
8
  dependencies: [],
7
9
  public: true,
8
10
  };
@@ -13,3 +15,17 @@ export const bluetoothScanningAtomDefinition = {
13
15
  dependencies: [],
14
16
  public: true,
15
17
  };
18
+ export const walletPolicyAtomDefinition = {
19
+ id: 'walletPolicyAtom',
20
+ type: 'atom',
21
+ factory: ({ storage }) => {
22
+ const createStorageAtom = createStorageAtomFactory({ storage });
23
+ return createStorageAtom({
24
+ key: 'walletPolicy',
25
+ defaultValue: {},
26
+ isSoleWriter: true,
27
+ });
28
+ },
29
+ dependencies: ['storage'],
30
+ public: true,
31
+ };
package/lib/index.d.ts CHANGED
@@ -5,7 +5,7 @@ declare const hardwareWalletLedger: ({ bluetoothScannerTimeout, }?: Partial<HwLe
5
5
  readonly definition: {
6
6
  readonly id: "bluetoothStatusAtom";
7
7
  readonly type: "atom";
8
- readonly factory: () => import("libraries/atoms/lib").Atom<boolean>;
8
+ readonly factory: () => import("libraries/atoms/lib").Atom<import("./atoms").BluetoothStatus>;
9
9
  readonly dependencies: readonly [];
10
10
  readonly public: true;
11
11
  };
@@ -17,22 +17,31 @@ declare const hardwareWalletLedger: ({ bluetoothScannerTimeout, }?: Partial<HwLe
17
17
  readonly dependencies: readonly [];
18
18
  readonly public: true;
19
19
  };
20
+ }, {
21
+ readonly definition: {
22
+ readonly id: "walletPolicyAtom";
23
+ readonly type: "atom";
24
+ readonly factory: ({ storage }: {
25
+ storage: import("adapters/storage-interface").Storage<Record<string, Buffer>>;
26
+ }) => import("libraries/atoms/lib").Atom<Record<string, Buffer> | {}>;
27
+ readonly dependencies: readonly ["storage"];
28
+ readonly public: true;
29
+ };
20
30
  }, {
21
31
  readonly definition: {
22
32
  readonly id: "hardwareWalletLedgerPlugin";
23
33
  readonly type: "plugin";
24
- readonly factory: ({ logger, transportsFactories, bluetoothStatusAtom, bluetoothScanningAtom, port, }: {
25
- logger: import("./shared/types").Logger;
26
- transportsFactories: import("./module/types").TransportFactories;
27
- bluetoothStatusAtom: import("libraries/atoms/lib").Atom<import("./redux/initial-state").BluetoothStatus>;
34
+ readonly factory: ({ bluetoothStatusAtom, bluetoothScanningAtom, ledgerDiscovery, port, }: {
35
+ bluetoothStatusAtom: import("libraries/atoms/lib").Atom<import("./atoms").BluetoothStatus>;
28
36
  bluetoothScanningAtom: import("libraries/atoms/lib").Atom<boolean>;
37
+ ledgerDiscovery: import("./module").LedgerDiscovery;
29
38
  port: import("./shared/types").Port;
30
39
  }) => {
31
40
  onUnlock: () => void;
32
41
  onLoad: () => void;
33
42
  onStop: () => void;
34
43
  };
35
- readonly dependencies: readonly ["logger", "transportsFactories", "bluetoothStatusAtom", "bluetoothScanningAtom", "port"];
44
+ readonly dependencies: readonly ["bluetoothStatusAtom", "bluetoothScanningAtom", "ledgerDiscovery", "port"];
36
45
  readonly public: true;
37
46
  };
38
47
  }, {
@@ -40,7 +49,7 @@ declare const hardwareWalletLedger: ({ bluetoothScannerTimeout, }?: Partial<HwLe
40
49
  readonly id: "ledgerDiscovery";
41
50
  readonly type: "module";
42
51
  readonly factory: (deps: import("./module").Dependencies) => import("./module").LedgerDiscovery;
43
- readonly dependencies: readonly ["config", "logger", "transportsFactories", "bluetoothScanningAtom"];
52
+ readonly dependencies: readonly ["config", "logger", "transportsFactories", "bluetoothStatusAtom", "bluetoothScanningAtom", "walletPolicyAtom"];
44
53
  readonly public: true;
45
54
  };
46
55
  readonly config: {
package/lib/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { bluetoothStatusAtomDefinition, bluetoothScanningAtomDefinition } from './atoms';
1
+ import { bluetoothStatusAtomDefinition, bluetoothScanningAtomDefinition, walletPolicyAtomDefinition, } from './atoms';
2
2
  import ledgerDiscoveryDefinition from './module';
3
3
  import hardwareWalletLedgerPluginDefinition from './plugin';
4
4
  const hardwareWalletLedger = ({ bluetoothScannerTimeout = 10_000, } = {}) => {
@@ -11,6 +11,9 @@ const hardwareWalletLedger = ({ bluetoothScannerTimeout = 10_000, } = {}) => {
11
11
  {
12
12
  definition: bluetoothScanningAtomDefinition,
13
13
  },
14
+ {
15
+ definition: walletPolicyAtomDefinition,
16
+ },
14
17
  {
15
18
  definition: hardwareWalletLedgerPluginDefinition,
16
19
  },
@@ -1,4 +1,5 @@
1
1
  import type Transport from '@ledgerhq/hw-transport';
2
+ import type { Atom } from '@exodus/atoms';
2
3
  import type { Bytes, HardwareWalletAssetHandler } from '@exodus/hw-common';
3
4
  export interface BitcoinMessage {
4
5
  rawMessage?: Bytes;
@@ -7,7 +8,7 @@ export interface BitcoinMessage {
7
8
  message: Bytes;
8
9
  };
9
10
  }
10
- declare function createHandler(transport: Transport): Promise<HardwareWalletAssetHandler<BitcoinMessage>>;
11
+ declare function createHandler(transport: Transport, walletPolicyAtom?: Atom<Record<string, Buffer>>): Promise<HardwareWalletAssetHandler<BitcoinMessage>>;
11
12
  declare const metadata: {
12
13
  applications: {
13
14
  applicationName: string;
@@ -3,17 +3,25 @@ import { AppClient, PsbtV2 } from '@exodus/ledger-bitcoin';
3
3
  import { BIP322 } from '@exodus/bip322-js';
4
4
  import { splitBip44DerivationPath } from './utils';
5
5
  import assert from 'minimalistic-assert';
6
- import { assertPsbtOnlyHasAllowedDerivationPaths, tagPsbtWithDerivationPaths, getWalletPolicy, derivationPathsToWalletPolicy, addDummyInputs, } from './utils/psbt';
6
+ import { assertPsbtOnlyHasAllowedDerivationPaths, tagPsbtWithDerivationPaths, getWalletPolicy, getMultisigWalletPolicy, derivationPathsToWalletPolicy, addDummyInputs, } from './utils/psbt';
7
7
  import { buildBip322PSBT } from './utils/bip322';
8
8
  import applications from './applications';
9
- async function createHandler(transport) {
9
+ async function createHandler(transport, walletPolicyAtom) {
10
10
  const app = new AppClient(transport);
11
11
  const getAddress = async (params) => {
12
12
  const [purpose, coin, account, change, address] = splitBip44DerivationPath(params.derivationPath);
13
13
  const fpr = await app.getMasterFingerprint();
14
14
  const xpub = await app.getExtendedPubkey(`m/${purpose}/${coin}/${account}`);
15
- const walletPolicy = getWalletPolicy(fpr, xpub, purpose, coin, account);
16
- return app.getWalletAddress(walletPolicy, null, Number(change), Number(address), false);
15
+ let walletPolicy;
16
+ let hmac = null;
17
+ if (params.multisigData) {
18
+ walletPolicy = getMultisigWalletPolicy(fpr, xpub, purpose, coin, account, params.multisigData);
19
+ hmac = await registerWalletPolicy(walletPolicy);
20
+ }
21
+ else {
22
+ walletPolicy = getWalletPolicy(fpr, xpub, purpose, coin, account);
23
+ }
24
+ return app.getWalletAddress(walletPolicy, hmac, Number(change), Number(address), params.displayOnDevice ?? false);
17
25
  };
18
26
  const getXPub = async (params) => {
19
27
  return app.getExtendedPubkey(params.derivationPath);
@@ -28,19 +36,30 @@ async function createHandler(transport) {
28
36
  psbt.deserialize(psbtBuffer);
29
37
  const fpr = await app.getMasterFingerprint();
30
38
  const derivationPathToPublicKeyMap = Object.create(null);
39
+ const addressToDerivationPathMap = Object.create(null);
31
40
  for (const derivationPath of params.derivationPaths) {
32
- derivationPathToPublicKeyMap[derivationPath] = await getPublicKey({
41
+ if (params.multisigData) {
42
+ const address = await getAddress({
43
+ assetName: 'bitcoin',
44
+ derivationPath,
45
+ multisigData: params.multisigData,
46
+ });
47
+ addressToDerivationPathMap[address] = derivationPath;
48
+ }
49
+ const publicKey = await getPublicKey({
33
50
  assetName: 'bitcoin',
34
51
  derivationPath,
35
52
  });
53
+ derivationPathToPublicKeyMap[derivationPath] = publicKey;
36
54
  }
37
- tagPsbtWithDerivationPaths(psbt, fpr, derivationPathToPublicKeyMap);
55
+ tagPsbtWithDerivationPaths(psbt, fpr, derivationPathToPublicKeyMap, addressToDerivationPathMap);
38
56
  addDummyInputs(psbt);
39
57
  assertPsbtOnlyHasAllowedDerivationPaths(psbt, fpr, params.derivationPaths);
40
58
  const signatures = [];
41
- const walletPolicies = await derivationPathsToWalletPolicy(fpr, app, params.derivationPaths);
59
+ const walletPolicies = await derivationPathsToWalletPolicy(fpr, app, params.derivationPaths, params.multisigData);
42
60
  for (const walletPolicy of walletPolicies) {
43
- const _signatures = await app.signPsbt(psbt.serialize(), walletPolicy, null);
61
+ const hmac = params.multisigData ? await registerWalletPolicy(walletPolicy) : null;
62
+ const _signatures = await app.signPsbt(psbt.serialize(), walletPolicy, hmac);
44
63
  signatures.push(..._signatures);
45
64
  }
46
65
  const result = signatures.map(([inputIndex, partialSignature]) => ({
@@ -95,6 +114,18 @@ async function createHandler(transport) {
95
114
  }
96
115
  throw new Error('rawMessage or bip322Message must be defined');
97
116
  };
117
+ const registerWalletPolicy = async (walletPolicy) => {
118
+ const walletPolicies = (await walletPolicyAtom?.get()) ?? {};
119
+ const id = walletPolicy.getId().toString('hex');
120
+ let hmac = walletPolicies[id];
121
+ if (!hmac) {
122
+ const [, h] = await app.registerWallet(walletPolicy);
123
+ hmac = h;
124
+ const updatedPolicies = { ...walletPolicies, [id]: hmac };
125
+ await walletPolicyAtom?.set(updatedPolicies);
126
+ }
127
+ return hmac;
128
+ };
98
129
  return {
99
130
  getAddress,
100
131
  getPublicKey,
@@ -1,7 +1,7 @@
1
1
  import secp256k1 from '@exodus/secp256k1';
2
2
  import * as ethers from '@exodus/ethersproject-transactions';
3
3
  import AppEth, { ledgerService } from '@exodus/ledgerhq-hw-app-eth';
4
- import { TypedDataUtils, SignTypedDataVersion, } from '@metamask/eth-sig-util';
4
+ import { TypedDataUtils, SignTypedDataVersion } from '@metamask/eth-sig-util';
5
5
  import assert from 'minimalistic-assert';
6
6
  import { buildFauxXPUB } from './utils';
7
7
  import { RequiresBlindSigningError } from '../errors';
@@ -1,6 +1,7 @@
1
1
  import type Transport from '@ledgerhq/hw-transport';
2
2
  import type { HardwareWalletAssetHandler } from '@exodus/hw-common';
3
- type HandlerFactory = (transport: Transport) => Promise<HardwareWalletAssetHandler>;
3
+ import type { Atom } from '@exodus/atoms';
4
+ type HandlerFactory = (transport: Transport, walletPolicyAtom?: Atom<Record<string, Buffer>>) => Promise<HardwareWalletAssetHandler>;
4
5
  type AssetMetadata = {
5
6
  applications: {
6
7
  applicationName: string;
@@ -7,6 +7,7 @@ import applications from './applications';
7
7
  export const assetApplications = {
8
8
  basemainnet,
9
9
  bitcoin,
10
+ bitcoinregtest: bitcoin,
10
11
  matic,
11
12
  solana,
12
13
  ethereum,
@@ -1,6 +1,10 @@
1
- import { DefaultWalletPolicy, PsbtV2, WalletPolicy } from '@exodus/ledger-bitcoin';
1
+ import type { PsbtV2 } from '@exodus/ledger-bitcoin';
2
+ import { DefaultWalletPolicy, WalletPolicy } from '@exodus/ledger-bitcoin';
3
+ import type { MultisigData } from 'libraries/hw-common/lib';
2
4
  export declare function assertPsbtOnlyHasAllowedDerivationPaths(psbt: PsbtV2, masterFingerprint: string, derivationPaths: string[]): void;
3
- export declare function tagPsbtWithDerivationPaths(psbt: PsbtV2, masterFingerprint: string, derivationPathToPublicKeyMap: Record<string, Buffer>): void;
5
+ export declare function tagPsbtWithDerivationPaths(psbt: PsbtV2, masterFingerprint: string, derivationPathToPublicKeyMap: Record<string, Buffer>, addressToDerivationPathMap?: Record<string, string>): void;
4
6
  export declare const getWalletPolicy: (fpr: string, xpub: string, purpose: string, coin: string, account: string) => DefaultWalletPolicy;
5
- export declare function derivationPathsToWalletPolicy(fpr: string, app: any, derivationPaths: string[]): Promise<WalletPolicy[]>;
7
+ export declare const DUMMY_TAPROOT_XPUB = "xpub661MyMwAqRbcEYS8w7XLSVeEsBXy79zSzH1J8vCdxAZningWLdN3zgtU6QgnecKFpJFPpdzxKrwoaZoV44qAJewsc4kX9vGaCaBExuvJH57";
8
+ export declare const getMultisigWalletPolicy: (fpr: string, xpub: string, purpose: string, coin: string, account: string, multisigData: MultisigData) => WalletPolicy;
9
+ export declare function derivationPathsToWalletPolicy(fpr: string, app: any, derivationPaths: string[], multisigData?: MultisigData): Promise<WalletPolicy[]>;
6
10
  export declare function addDummyInputs(psbt: PsbtV2): void;
@@ -1,6 +1,6 @@
1
1
  import assert from 'minimalistic-assert';
2
- import { DefaultWalletPolicy, } from '@exodus/ledger-bitcoin';
3
- import { payments } from '@exodus/bitcoinjs-lib';
2
+ import { DefaultWalletPolicy, WalletPolicy } from '@exodus/ledger-bitcoin';
3
+ import { payments, address as baddress } from '@exodus/bitcoinjs-lib';
4
4
  import { buildBip44PathFromNumber, assertBip44PathNumber, splitBip44PathToNumber, splitBip44DerivationPath, } from './derivation-paths';
5
5
  export function assertPsbtOnlyHasAllowedDerivationPaths(psbt, masterFingerprint, derivationPaths) {
6
6
  for (let i = 0; i < psbt.getGlobalInputCount(); i++) {
@@ -31,14 +31,21 @@ function assertAllowedDerivation(bip32Derivation, masterFingerprint, derivationP
31
31
  assert(derivationPaths.includes(derivationPath), `found unsupported derivation path: ${derivationPath}`);
32
32
  }
33
33
  }
34
- export function tagPsbtWithDerivationPaths(psbt, masterFingerprint, derivationPathToPublicKeyMap) {
34
+ export function tagPsbtWithDerivationPaths(psbt, masterFingerprint, derivationPathToPublicKeyMap, addressToDerivationPathMap) {
35
35
  const masterFingerprintBuffer = Buffer.from(masterFingerprint, 'hex');
36
36
  const scriptPubKeyToDerivationPathMap = computeScriptPubKeyToDerivationPathMap(derivationPathToPublicKeyMap);
37
37
  for (let inputIndex = 0; inputIndex < psbt.getGlobalInputCount(); inputIndex++) {
38
38
  const witnessUtxo = psbt.getInputWitnessUtxo(inputIndex);
39
39
  assert(witnessUtxo, `witnessUtxo is currently the only supported type`);
40
40
  const scriptPubKey = witnessUtxo.scriptPubKey.toString('hex');
41
- const derivationPath = scriptPubKeyToDerivationPathMap[scriptPubKey];
41
+ let derivationPath = scriptPubKeyToDerivationPathMap[scriptPubKey];
42
+ if (addressToDerivationPathMap && !derivationPath) {
43
+ try {
44
+ const address = baddress.fromOutputScript(witnessUtxo.scriptPubKey);
45
+ derivationPath = addressToDerivationPathMap[address];
46
+ }
47
+ catch { }
48
+ }
42
49
  if (derivationPath) {
43
50
  const publicKey = derivationPathToPublicKeyMap[derivationPath];
44
51
  assert(Buffer.isBuffer(publicKey), `no public key found for derivation path`);
@@ -53,7 +60,14 @@ export function tagPsbtWithDerivationPaths(psbt, masterFingerprint, derivationPa
53
60
  }
54
61
  for (let outputIndex = 0; outputIndex < psbt.getGlobalOutputCount(); outputIndex++) {
55
62
  const scriptPubKey = psbt.getOutputScript(outputIndex);
56
- const derivationPath = scriptPubKeyToDerivationPathMap[scriptPubKey.toString('hex')];
63
+ let derivationPath = scriptPubKeyToDerivationPathMap[scriptPubKey.toString('hex')];
64
+ if (addressToDerivationPathMap && !derivationPath) {
65
+ try {
66
+ const address = baddress.fromOutputScript(scriptPubKey);
67
+ derivationPath = addressToDerivationPathMap[address];
68
+ }
69
+ catch { }
70
+ }
57
71
  if (derivationPath) {
58
72
  const publicKey = derivationPathToPublicKeyMap[derivationPath];
59
73
  assert(Buffer.isBuffer(publicKey), `no public key found for derivation path`);
@@ -104,7 +118,23 @@ export const getWalletPolicy = (fpr, xpub, purpose, coin, account) => {
104
118
  }[purpose];
105
119
  return new DefaultWalletPolicy(desc, `[${fpr}/${purpose}/${coin}/${account}]${xpub}`);
106
120
  };
107
- export async function derivationPathsToWalletPolicy(fpr, app, derivationPaths) {
121
+ export const DUMMY_TAPROOT_XPUB = 'xpub661MyMwAqRbcEYS8w7XLSVeEsBXy79zSzH1J8vCdxAZningWLdN3zgtU6QgnecKFpJFPpdzxKrwoaZoV44qAJewsc4kX9vGaCaBExuvJH57';
122
+ export const getMultisigWalletPolicy = (fpr, xpub, purpose, coin, account, multisigData) => {
123
+ let template = `tr(@0/**,sortedmulti_a(${multisigData.threshold}`;
124
+ const keys = [DUMMY_TAPROOT_XPUB];
125
+ multisigData.xpubs.forEach((xpub2, i) => {
126
+ if (xpub === xpub2) {
127
+ keys.push(`[${fpr}/${purpose}/${coin}/${account}]${xpub}`);
128
+ }
129
+ else {
130
+ keys.push(xpub2);
131
+ }
132
+ template += `,@${i + 1}/**`;
133
+ });
134
+ template += '))';
135
+ return new WalletPolicy('Multisig', template, keys);
136
+ };
137
+ export async function derivationPathsToWalletPolicy(fpr, app, derivationPaths, multisigData) {
108
138
  const splittedDerivationPaths = derivationPaths.map(splitBip44DerivationPath);
109
139
  const dedupedPaths = Object.values(splittedDerivationPaths.reduce((prev, splittedDerivationPath) => {
110
140
  const [purpose, coin, account] = splittedDerivationPath;
@@ -115,7 +145,12 @@ export async function derivationPathsToWalletPolicy(fpr, app, derivationPaths) {
115
145
  for (const dedupedPath of dedupedPaths) {
116
146
  const [purpose, coin, account] = dedupedPath;
117
147
  const xpub = await app.getExtendedPubkey(`m/${purpose}/${coin}/${account}`);
118
- walletPolicies.push(getWalletPolicy(fpr, xpub, purpose, coin, account));
148
+ if (multisigData) {
149
+ walletPolicies.push(getMultisigWalletPolicy(fpr, xpub, purpose, coin, account, multisigData));
150
+ }
151
+ else {
152
+ walletPolicies.push(getWalletPolicy(fpr, xpub, purpose, coin, account));
153
+ }
119
154
  }
120
155
  return walletPolicies;
121
156
  }
@@ -2,7 +2,7 @@ import bs58check from 'bs58check';
2
2
  export function buildFauxXPUB(publicKey, chainCode) {
3
3
  const LEN = 78;
4
4
  const XPUB_PREFIX = 0x04_88_b2_1e;
5
- const buffer = Buffer.allocUnsafe(LEN);
5
+ const buffer = Buffer.alloc(LEN);
6
6
  const fingerprint = 0x00_00_00_00;
7
7
  const depth = 0;
8
8
  const index = 0;
@@ -1,13 +1,15 @@
1
1
  import type { GetAddressParams, GetPublicKeyParams, GetXPubParams, HardwareWalletDevice, SignMessageParams, SignTransactionParams } from '@exodus/hw-common';
2
2
  import type { TransportFactory } from './types';
3
+ import type { Atom } from '@exodus/atoms';
3
4
  export type Descriptor = unknown;
4
5
  export type Dependencies = {
5
6
  transportFactory: TransportFactory;
6
7
  descriptor: Descriptor;
8
+ walletPolicyAtom: Atom<Record<string, Buffer>>;
7
9
  };
8
10
  export declare class LedgerDevice implements HardwareWalletDevice {
9
11
  #private;
10
- constructor({ transportFactory, descriptor }: Dependencies);
12
+ constructor({ transportFactory, descriptor, walletPolicyAtom }: Dependencies);
11
13
  ensureApplicationIsOpened: (baseAssetName: string) => Promise<void>;
12
14
  listSupportedAssetNames: () => Promise<string[]>;
13
15
  listInstalledAssetNames: () => Promise<string[]>;
@@ -6,9 +6,11 @@ import { UnsupportedVersionError, ApplicationNotFoundError } from './errors';
6
6
  export class LedgerDevice {
7
7
  #transportFactory;
8
8
  #descriptor;
9
- constructor({ transportFactory, descriptor }) {
9
+ #walletPolicyAtom;
10
+ constructor({ transportFactory, descriptor, walletPolicyAtom }) {
10
11
  this.#transportFactory = transportFactory;
11
12
  this.#descriptor = descriptor;
13
+ this.#walletPolicyAtom = walletPolicyAtom;
12
14
  }
13
15
  #runInSession = async (operation, expectErrorWhenClosing = false) => {
14
16
  const transport = await this.#transportFactory.open(this.#descriptor);
@@ -119,7 +121,7 @@ export class LedgerDevice {
119
121
  getAddress = async (params) => {
120
122
  await this.ensureApplicationIsOpened(params.assetName);
121
123
  return this.#runInSession(async (transport) => {
122
- const handler = await this.#getAssetApplication(params.assetName).handler(transport);
124
+ const handler = await this.#getAssetApplication(params.assetName).handler(transport, this.#walletPolicyAtom);
123
125
  return handler.getAddress(params);
124
126
  });
125
127
  };
@@ -140,7 +142,7 @@ export class LedgerDevice {
140
142
  signTransaction = async (params) => {
141
143
  await this.ensureApplicationIsOpened(params.assetName);
142
144
  return this.#runInSession(async (transport) => {
143
- const handler = await this.#getAssetApplication(params.assetName).handler(transport);
145
+ const handler = await this.#getAssetApplication(params.assetName).handler(transport, this.#walletPolicyAtom);
144
146
  return handler.signTransaction(params);
145
147
  });
146
148
  };
@@ -1,26 +1,31 @@
1
- import { Logger } from '../shared/types';
1
+ import type { Logger } from '../shared/types';
2
2
  import type { Atom } from '@exodus/atoms';
3
3
  import type { HardwareWalletDescriptor, HardwareWalletDiscovery } from '@exodus/hw-common';
4
4
  import type { HwLedgerConfig, TransportFactories } from './types';
5
+ import type { BluetoothStatus } from '../atoms';
5
6
  export type Dependencies = {
6
7
  config: HwLedgerConfig;
7
8
  logger: Logger;
8
9
  transportsFactories: TransportFactories;
10
+ bluetoothStatusAtom: Atom<BluetoothStatus>;
9
11
  bluetoothScanningAtom: Atom<boolean>;
12
+ walletPolicyAtom: Atom<Record<string, Buffer>>;
10
13
  };
11
14
  export declare class LedgerDiscovery implements HardwareWalletDiscovery {
12
15
  #private;
13
- constructor({ config, logger, transportsFactories, bluetoothScanningAtom }: Dependencies);
16
+ constructor({ config, logger, transportsFactories, bluetoothStatusAtom, bluetoothScanningAtom, walletPolicyAtom, }: Dependencies);
17
+ start: () => void;
14
18
  scan: () => Promise<void>;
15
19
  stopScan: (clearDevices?: boolean) => void;
16
20
  list: () => Promise<HardwareWalletDescriptor[]>;
17
21
  getFirstDevice: () => Promise<import("@exodus/hw-common").HardwareWalletDevice<import("@exodus/hw-common").Message>>;
22
+ stop: () => Promise<void>;
18
23
  }
19
24
  declare const _default: {
20
25
  readonly id: "ledgerDiscovery";
21
26
  readonly type: "module";
22
27
  readonly factory: (deps: Dependencies) => LedgerDiscovery;
23
- readonly dependencies: readonly ["config", "logger", "transportsFactories", "bluetoothScanningAtom"];
28
+ readonly dependencies: readonly ["config", "logger", "transportsFactories", "bluetoothStatusAtom", "bluetoothScanningAtom", "walletPolicyAtom"];
24
29
  readonly public: true;
25
30
  };
26
31
  export default _default;
@@ -7,12 +7,17 @@ export class LedgerDiscovery {
7
7
  #logger;
8
8
  #transportsFactories;
9
9
  #scannerTimeout;
10
+ #statusBluetooth;
11
+ #bluetoothStatusAtom;
10
12
  #scannerBluetooth;
11
13
  #bluetoothScanningAtom;
14
+ #walletPolicyAtom;
12
15
  #discoveredBluetoothDevices = Object.create(null);
13
- constructor({ config, logger, transportsFactories, bluetoothScanningAtom }) {
16
+ constructor({ config, logger, transportsFactories, bluetoothStatusAtom, bluetoothScanningAtom, walletPolicyAtom, }) {
14
17
  this.#logger = logger;
18
+ this.#bluetoothStatusAtom = bluetoothStatusAtom;
15
19
  this.#bluetoothScanningAtom = bluetoothScanningAtom;
20
+ this.#walletPolicyAtom = walletPolicyAtom;
16
21
  if (!(typeof transportsFactories === 'object' &&
17
22
  Object.keys(transportsFactories).length > 0 &&
18
23
  Object.entries(transportsFactories).every(([transportName, transportFactory]) => ['usb', 'bluetooth', 'tcp'].includes(transportName) &&
@@ -26,7 +31,26 @@ export class LedgerDiscovery {
26
31
  this.#transportsFactories = transportsFactories;
27
32
  this.#scannerTimeout = config.bluetoothScannerTimeout;
28
33
  }
34
+ start = () => {
35
+ if (!this.#transportsFactories.bluetooth)
36
+ return;
37
+ if (this.#statusBluetooth)
38
+ return;
39
+ if (this.#transportsFactories.bluetooth.observeState) {
40
+ this.#statusBluetooth = this.#transportsFactories.bluetooth.observeState({
41
+ next: ({ type, available }) => this.#bluetoothStatusAtom.set({ type, available }),
42
+ error: (error) => this.#logger.error(error),
43
+ });
44
+ }
45
+ else if (this.#transportsFactories.bluetooth.observeAvailability) {
46
+ this.#statusBluetooth = this.#transportsFactories.bluetooth.observeAvailability({
47
+ next: (available) => this.#bluetoothStatusAtom.set({ type: 'Unknown', available }),
48
+ error: (error) => this.#logger.error(error),
49
+ });
50
+ }
51
+ };
29
52
  scan = async () => {
53
+ this.start();
30
54
  return new Promise((resolve, reject) => {
31
55
  if (this.#transportsFactories.bluetooth) {
32
56
  this.stopScan(true);
@@ -79,7 +103,11 @@ export class LedgerDiscovery {
79
103
  manufacturer: 'ledger',
80
104
  transport: 'usb',
81
105
  model: deviceModel?.id ?? 'unknown',
82
- get: async () => new LedgerDevice({ transportFactory, descriptor: hidDevice }),
106
+ get: async () => new LedgerDevice({
107
+ transportFactory,
108
+ descriptor: hidDevice,
109
+ walletPolicyAtom: this.#walletPolicyAtom,
110
+ }),
83
111
  };
84
112
  });
85
113
  descriptors.push(...usbDeviceList);
@@ -93,7 +121,11 @@ export class LedgerDiscovery {
93
121
  manufacturer: 'ledger',
94
122
  transport: 'bluetooth',
95
123
  model: deviceModel?.id ?? 'unknown',
96
- get: async () => new LedgerDevice({ transportFactory, descriptor: bluetoothDevice }),
124
+ get: async () => new LedgerDevice({
125
+ transportFactory,
126
+ descriptor: bluetoothDevice,
127
+ walletPolicyAtom: this.#walletPolicyAtom,
128
+ }),
97
129
  };
98
130
  });
99
131
  descriptors.push(...bluetoothDeviceList);
@@ -107,6 +139,7 @@ export class LedgerDiscovery {
107
139
  get: async () => new LedgerDevice({
108
140
  transportFactory,
109
141
  descriptor: { apduPort: APDU_PORT },
142
+ walletPolicyAtom: this.#walletPolicyAtom,
110
143
  }),
111
144
  });
112
145
  break;
@@ -123,12 +156,22 @@ export class LedgerDiscovery {
123
156
  }
124
157
  throw new NoDeviceFoundError();
125
158
  };
159
+ stop = async () => {
160
+ this.#statusBluetooth?.unsubscribe();
161
+ };
126
162
  }
127
163
  const createLedgerDiscovery = (deps) => new LedgerDiscovery({ ...deps });
128
164
  export default {
129
165
  id: MODULE_ID,
130
166
  type: 'module',
131
167
  factory: createLedgerDiscovery,
132
- dependencies: ['config', 'logger', 'transportsFactories', 'bluetoothScanningAtom'],
168
+ dependencies: [
169
+ 'config',
170
+ 'logger',
171
+ 'transportsFactories',
172
+ 'bluetoothStatusAtom',
173
+ 'bluetoothScanningAtom',
174
+ 'walletPolicyAtom',
175
+ ],
133
176
  public: true,
134
177
  };
@@ -1,22 +1,21 @@
1
1
  import type { Atom } from '@exodus/atoms';
2
- import type { TransportFactories } from '../module/types';
3
- import type { Logger, Port } from '../shared/types';
4
- import { BluetoothStatus } from '../redux/initial-state';
2
+ import type { Port } from '../shared/types';
3
+ import type { BluetoothStatus } from '../atoms';
4
+ import type { LedgerDiscovery } from '../module';
5
5
  declare const hardwareWalletLedgerPluginDefinition: {
6
6
  readonly id: "hardwareWalletLedgerPlugin";
7
7
  readonly type: "plugin";
8
- readonly factory: ({ logger, transportsFactories, bluetoothStatusAtom, bluetoothScanningAtom, port, }: {
9
- logger: Logger;
10
- transportsFactories: TransportFactories;
8
+ readonly factory: ({ bluetoothStatusAtom, bluetoothScanningAtom, ledgerDiscovery, port, }: {
11
9
  bluetoothStatusAtom: Atom<BluetoothStatus>;
12
10
  bluetoothScanningAtom: Atom<boolean>;
11
+ ledgerDiscovery: LedgerDiscovery;
13
12
  port: Port;
14
13
  }) => {
15
14
  onUnlock: () => void;
16
15
  onLoad: () => void;
17
16
  onStop: () => void;
18
17
  };
19
- readonly dependencies: readonly ["logger", "transportsFactories", "bluetoothStatusAtom", "bluetoothScanningAtom", "port"];
18
+ readonly dependencies: readonly ["bluetoothStatusAtom", "bluetoothScanningAtom", "ledgerDiscovery", "port"];
20
19
  readonly public: true;
21
20
  };
22
21
  export default hardwareWalletLedgerPluginDefinition;
@@ -1,33 +1,16 @@
1
1
  import { createAtomObserver } from '@exodus/atoms';
2
- const createHardwareWalletLedgerPlugin = ({ logger, transportsFactories, bluetoothStatusAtom, bluetoothScanningAtom, port, }) => {
3
- let subscription;
2
+ const createHardwareWalletLedgerPlugin = ({ bluetoothStatusAtom, bluetoothScanningAtom, ledgerDiscovery, port, }) => {
4
3
  const observers = [
5
4
  createAtomObserver({ atom: bluetoothStatusAtom, port, event: 'bluetoothStatus' }),
6
5
  createAtomObserver({ atom: bluetoothScanningAtom, port, event: 'bluetoothScanning' }),
7
6
  ];
8
7
  observers.forEach((observer) => observer.register());
9
8
  const start = () => {
10
- if (!transportsFactories.bluetooth)
11
- return;
12
- if (subscription)
13
- return;
14
- if (transportsFactories.bluetooth.observeState) {
15
- subscription = transportsFactories.bluetooth.observeState({
16
- next: ({ type, available }) => bluetoothStatusAtom.set({ type, available }),
17
- error: (error) => logger.error(error),
18
- });
19
- }
20
- else if (transportsFactories.bluetooth.observeAvailability) {
21
- subscription = transportsFactories.bluetooth.observeAvailability({
22
- next: (available) => bluetoothStatusAtom.set({ type: 'Unknown', available }),
23
- error: (error) => logger.error(error),
24
- });
25
- }
26
9
  observers.forEach((observer) => observer.start());
27
10
  };
28
11
  const stop = () => {
29
12
  observers.forEach((observer) => observer.unregister());
30
- subscription?.unsubscribe();
13
+ ledgerDiscovery.stop();
31
14
  };
32
15
  const onUnlock = () => {
33
16
  start();
@@ -44,13 +27,7 @@ const hardwareWalletLedgerPluginDefinition = {
44
27
  id: 'hardwareWalletLedgerPlugin',
45
28
  type: 'plugin',
46
29
  factory: createHardwareWalletLedgerPlugin,
47
- dependencies: [
48
- 'logger',
49
- 'transportsFactories',
50
- 'bluetoothStatusAtom',
51
- 'bluetoothScanningAtom',
52
- 'port',
53
- ],
30
+ dependencies: ['bluetoothStatusAtom', 'bluetoothScanningAtom', 'ledgerDiscovery', 'port'],
54
31
  public: true,
55
32
  };
56
33
  export default hardwareWalletLedgerPluginDefinition;
@@ -10,12 +10,12 @@ declare const exchangeReduxDefinition: {
10
10
  };
11
11
  readonly bluetoothScanning: (state: LedgerHardwareWalletState, payload: boolean) => {
12
12
  bluetoothScanning: boolean;
13
- bluetoothStatus: import("./initial-state").BluetoothStatus;
13
+ bluetoothStatus: import("../atoms").BluetoothStatus;
14
14
  };
15
15
  };
16
16
  readonly selectorDefinitions: ({
17
17
  readonly id: "bluetoothStatus";
18
- readonly resultFunction: (state: LedgerHardwareWalletState) => import("./initial-state").BluetoothStatus;
18
+ readonly resultFunction: (state: LedgerHardwareWalletState) => import("../atoms").BluetoothStatus;
19
19
  readonly dependencies: readonly [{
20
20
  readonly selector: string;
21
21
  }];
@@ -1,7 +1,4 @@
1
- export type BluetoothStatus = {
2
- type: 'Unknown' | 'Resetting' | 'Unsupported' | 'Unauthorized' | 'PoweredOff' | 'PoweredOn';
3
- available: boolean;
4
- };
1
+ import type { BluetoothStatus } from '../atoms';
5
2
  export type LedgerHardwareWalletState = {
6
3
  bluetoothStatus: BluetoothStatus;
7
4
  bluetoothScanning: boolean;
@@ -1,7 +1,7 @@
1
1
  import type { LedgerHardwareWalletState } from '../initial-state';
2
2
  declare const bluetoothStatusSelectorDefinition: {
3
3
  readonly id: "bluetoothStatus";
4
- readonly resultFunction: (state: LedgerHardwareWalletState) => import("../initial-state").BluetoothStatus;
4
+ readonly resultFunction: (state: LedgerHardwareWalletState) => import("../../atoms").BluetoothStatus;
5
5
  readonly dependencies: readonly [{
6
6
  readonly selector: string;
7
7
  }];
@@ -1,6 +1,6 @@
1
1
  declare const ledgerSelectors: ({
2
2
  readonly id: "bluetoothStatus";
3
- readonly resultFunction: (state: import("../initial-state").LedgerHardwareWalletState) => import("../initial-state").BluetoothStatus;
3
+ readonly resultFunction: (state: import("../initial-state").LedgerHardwareWalletState) => import("../../atoms").BluetoothStatus;
4
4
  readonly dependencies: readonly [{
5
5
  readonly selector: string;
6
6
  }];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/hw-ledger",
3
- "version": "2.9.4",
3
+ "version": "2.10.0",
4
4
  "description": "The ledger module",
5
5
  "author": "Exodus Movement, Inc.",
6
6
  "repository": {
@@ -25,7 +25,7 @@
25
25
  "test": "run -T jest --testMatch='**/*.unit.test.js' --coverageDirectory coverage/unit",
26
26
  "zemu:install": "docker pull zondax/builder-zemu:speculos-745fe46f81cfc18114b94a37beaefb58a09c0fcd",
27
27
  "test:integration": "yarn zemu:install && run -T jest --runInBand",
28
- "lint": "run -T eslint . --ignore-path ../../.gitignore",
28
+ "lint": "run -T eslint .",
29
29
  "lint:fix": "yarn lint --fix",
30
30
  "prepublishOnly": "yarn run -T build --scope @exodus/hw-ledger"
31
31
  },
@@ -34,7 +34,7 @@
34
34
  "@exodus/bip322-js": "^1.1.0-exodus.4",
35
35
  "@exodus/bitcoinjs-lib": "^6.1.5-exodus.0",
36
36
  "@exodus/ethersproject-transactions": "^5.4.2-exodus.0",
37
- "@exodus/hw-common": "^2.1.0",
37
+ "@exodus/hw-common": "^2.4.0",
38
38
  "@exodus/ledger-bitcoin": "^0.2.3-exodus.3",
39
39
  "@exodus/ledgerhq-devices": "^8.1.0-exodus.3",
40
40
  "@exodus/ledgerhq-hw-app-eth": "^6.33.7-exodus.8",
@@ -61,6 +61,7 @@
61
61
  "@types/jest-when": "^3.5.2",
62
62
  "@types/minimalistic-assert": "^1.0.1",
63
63
  "@types/secp256k1": "^4.0.3",
64
+ "@types/semver": "^7.3.5",
64
65
  "@zondax/zemu": "^0.43.1",
65
66
  "bitcoinjs-lib": "^6.1.3",
66
67
  "concurrently": "^7.6.0",
@@ -71,5 +72,5 @@
71
72
  "tiny-secp256k1": "^2.2.3",
72
73
  "ts-jest": "^29.0.3"
73
74
  },
74
- "gitHead": "02011f9404194769108cec052ec27c714c19ad64"
75
+ "gitHead": "d035797666e173e9cec145ddce8e137fe84dfb83"
75
76
  }