@exodus/hw-ledger 2.3.0 → 2.4.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,18 @@
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.4.0](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/hw-ledger@2.3.1...@exodus/hw-ledger@2.4.0) (2024-03-06)
7
+
8
+ ### Features
9
+
10
+ - add bluetooth support ([#5998](https://github.com/ExodusMovement/exodus-hydra/issues/5998)) ([6160b9e](https://github.com/ExodusMovement/exodus-hydra/commit/6160b9eec796afe0109699b6e1c3291fbaeeede1))
11
+
12
+ ## [2.3.1](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/hw-ledger@2.3.0...@exodus/hw-ledger@2.3.1) (2024-02-13)
13
+
14
+ ### Bug Fixes
15
+
16
+ - add dummy inputs when signing psbt on ledger ([#5680](https://github.com/ExodusMovement/exodus-hydra/issues/5680)) ([b11c6c6](https://github.com/ExodusMovement/exodus-hydra/commit/b11c6c67db196d7b970ded3228e7482ff1881c57))
17
+
6
18
  ## [2.3.0](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/hw-ledger@2.2.6...@exodus/hw-ledger@2.3.0) (2024-02-08)
7
19
 
8
20
  ### Features
package/lib/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- declare const hardwareWalletLedger: () => {
1
+ import type { HwLedgerConfig } from './module/types';
2
+ declare const hardwareWalletLedger: ({ bluetoothScannerTimeout, }?: Partial<HwLedgerConfig>) => {
2
3
  id: string;
3
4
  definitions: {
4
5
  definition: {
@@ -7,6 +8,9 @@ declare const hardwareWalletLedger: () => {
7
8
  factory: (deps: import("./module").Dependencies) => import("./module").LedgerDiscovery;
8
9
  dependencies: string[];
9
10
  };
11
+ config: {
12
+ bluetoothScannerTimeout: number;
13
+ };
10
14
  }[];
11
15
  };
12
16
  export default hardwareWalletLedger;
package/lib/index.js CHANGED
@@ -1,10 +1,11 @@
1
1
  import ledgerDiscoveryDefinition from './module';
2
- const hardwareWalletLedger = () => {
2
+ const hardwareWalletLedger = ({ bluetoothScannerTimeout = 10000, } = {}) => {
3
3
  return {
4
4
  id: 'hardwareWalletLedger',
5
5
  definitions: [
6
6
  {
7
7
  definition: ledgerDiscoveryDefinition,
8
+ config: { bluetoothScannerTimeout },
8
9
  },
9
10
  ],
10
11
  };
@@ -3,7 +3,7 @@ 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, } from './utils/psbt';
6
+ import { assertPsbtOnlyHasAllowedDerivationPaths, tagPsbtWithDerivationPaths, getWalletPolicy, derivationPathsToWalletPolicy, addDummyInputs, } from './utils/psbt';
7
7
  import { buildBip322PSBT } from './utils/bip322';
8
8
  async function createHandler(transport) {
9
9
  const app = new AppClient(transport);
@@ -34,6 +34,7 @@ async function createHandler(transport) {
34
34
  });
35
35
  }
36
36
  tagPsbtWithDerivationPaths(psbt, fpr, derivationPathToPublicKeyMap);
37
+ addDummyInputs(psbt);
37
38
  assertPsbtOnlyHasAllowedDerivationPaths(psbt, fpr, params.derivationPaths);
38
39
  const signatures = [];
39
40
  const walletPolicies = await derivationPathsToWalletPolicy(fpr, app, params.derivationPaths);
@@ -4,3 +4,4 @@ export declare function assertPsbtOnlyHasAllowedDerivationPaths(psbt: PsbtV2, ma
4
4
  export declare function tagPsbtWithDerivationPaths(psbt: PsbtV2, masterFingerprint: string, derivationPathToPublicKeyMap: Record<string, Buffer>): void;
5
5
  export declare const getWalletPolicy: (fpr: string, xpub: string, purpose: string, coin: string, account: string) => DefaultWalletPolicy;
6
6
  export declare function derivationPathsToWalletPolicy(fpr: string, app: any, derivationPaths: string[]): Promise<WalletPolicy[]>;
7
+ export declare function addDummyInputs(psbt: PsbtV2): void;
@@ -119,3 +119,40 @@ export async function derivationPathsToWalletPolicy(fpr, app, derivationPaths) {
119
119
  }
120
120
  return walletPolicies;
121
121
  }
122
+ function canModifyInputs(psbt) {
123
+ const SIGHASH_ALL = 0x01;
124
+ const SIGHASH_NONE = 0x02;
125
+ const SIGHASH_SINGLE = 0x03;
126
+ for (let i = 0; i < psbt.getGlobalInputCount(); i++) {
127
+ if ([SIGHASH_ALL, SIGHASH_NONE, SIGHASH_SINGLE, undefined].includes(psbt.getInputSighashType(i))) {
128
+ return false;
129
+ }
130
+ }
131
+ return true;
132
+ }
133
+ export function addDummyInputs(psbt) {
134
+ const canAddDummy = canModifyInputs(psbt);
135
+ if (!canAddDummy) {
136
+ return;
137
+ }
138
+ let outputAmount = 0;
139
+ for (let i = 0; i < psbt.getGlobalOutputCount(); i++) {
140
+ outputAmount += psbt.getOutputAmount(i);
141
+ }
142
+ let inputAmount = 0;
143
+ for (let i = 0; i < psbt.getGlobalInputCount(); i++) {
144
+ const witnessUtxo = psbt.getInputWitnessUtxo(i);
145
+ if (witnessUtxo) {
146
+ inputAmount += witnessUtxo.amount;
147
+ }
148
+ }
149
+ if (outputAmount > inputAmount) {
150
+ const newInputIndex = psbt.getGlobalInputCount();
151
+ const amount = outputAmount - inputAmount;
152
+ psbt.setGlobalInputCount(newInputIndex + 1);
153
+ const FIRST_BITCOIN_TXID = '4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b';
154
+ psbt.setInputPreviousTxId(newInputIndex, Buffer.from(FIRST_BITCOIN_TXID, 'hex'));
155
+ psbt.setInputOutputIndex(newInputIndex, 0);
156
+ psbt.setInputWitnessUtxo(newInputIndex, amount, Buffer.from('01', 'hex'));
157
+ }
158
+ }
@@ -1,8 +1,9 @@
1
1
  import ExodusModule from '@exodus/module';
2
2
  import { Logger } from '../shared/types';
3
3
  import type { HardwareWalletDescriptor, HardwareWalletDiscovery } from '@exodus/hw-common';
4
- import type { TransportFactory } from './types';
4
+ import type { TransportFactory, HwLedgerConfig } from './types';
5
5
  export type Dependencies = {
6
+ config: HwLedgerConfig;
6
7
  logger: Logger;
7
8
  transportsFactories: {
8
9
  usb: TransportFactory;
@@ -12,7 +13,9 @@ export type Dependencies = {
12
13
  };
13
14
  export declare class LedgerDiscovery extends ExodusModule implements HardwareWalletDiscovery {
14
15
  #private;
15
- constructor({ logger, transportsFactories }: Dependencies);
16
+ constructor({ config, logger, transportsFactories }: Dependencies);
17
+ scan: () => Promise<void>;
18
+ stopScan: (clearDevices?: boolean) => void;
16
19
  list: () => Promise<HardwareWalletDescriptor[]>;
17
20
  getFirstDevice: () => Promise<import("@exodus/hw-common").HardwareWalletDevice<import("@exodus/hw-common").Message>>;
18
21
  }
@@ -5,7 +5,10 @@ import { NoDeviceFoundError, UnknownTransportError } from './errors';
5
5
  const MODULE_ID = 'ledgerDiscovery';
6
6
  export class LedgerDiscovery extends ExodusModule {
7
7
  #transportsFactories;
8
- constructor({ logger, transportsFactories }) {
8
+ #scannerTimeout;
9
+ #scannerBluetooth;
10
+ #discoveredBluetoothDevices = Object.create(null);
11
+ constructor({ config, logger, transportsFactories }) {
9
12
  super({ name: MODULE_ID, logger });
10
13
  if (!(typeof transportsFactories === 'object' &&
11
14
  Object.keys(transportsFactories).length > 0 &&
@@ -14,14 +17,55 @@ export class LedgerDiscovery extends ExodusModule {
14
17
  typeof transportFactory.list === 'function' &&
15
18
  typeof transportFactory.open === 'function')))
16
19
  throw new UnknownTransportError();
20
+ if (!Number.isInteger(config.bluetoothScannerTimeout)) {
21
+ throw new TypeError(`Invalid bluetoothScannerTimeout passed through config`);
22
+ }
17
23
  this.#transportsFactories = transportsFactories;
24
+ this.#scannerTimeout = config.bluetoothScannerTimeout;
18
25
  }
26
+ scan = async () => {
27
+ return new Promise((resolve, reject) => {
28
+ if (this.#transportsFactories.bluetooth) {
29
+ this.stopScan(true);
30
+ this.#scannerBluetooth = this.#transportsFactories.bluetooth.listen({
31
+ next: (evnt) => {
32
+ if (evnt.type === 'add') {
33
+ const descriptor = evnt.descriptor;
34
+ this.#discoveredBluetoothDevices[descriptor.id] = descriptor;
35
+ }
36
+ },
37
+ error: (error) => {
38
+ this._logger.error(`an error occured while scanning for bluetooth devices "${error.message}"`);
39
+ this.stopScan();
40
+ reject(error);
41
+ },
42
+ complete: () => { },
43
+ });
44
+ setTimeout(() => {
45
+ this.stopScan();
46
+ resolve();
47
+ }, this.#scannerTimeout);
48
+ }
49
+ else {
50
+ this._logger.error(`Attempted to perform scan without having bluetooth transport factory`);
51
+ }
52
+ });
53
+ };
54
+ stopScan = (clearDevices = false) => {
55
+ if (clearDevices) {
56
+ this.#discoveredBluetoothDevices = Object.create(null);
57
+ }
58
+ if (this.#scannerBluetooth) {
59
+ this.#scannerBluetooth.unsubscribe();
60
+ this.#scannerBluetooth = undefined;
61
+ }
62
+ };
19
63
  list = async () => {
20
64
  const descriptors = [];
21
65
  for (const [transportName, transportFactory] of Object.entries(this.#transportsFactories)) {
22
- const deviceList = await transportFactory.list();
23
66
  switch (transportName) {
24
67
  case 'usb':
68
+ const deviceList = await transportFactory.list();
25
69
  const usbDeviceList = deviceList.map((hidDevice) => ({
26
70
  manufacturer: 'ledger',
27
71
  transport: 'usb',
@@ -30,7 +74,12 @@ export class LedgerDiscovery extends ExodusModule {
30
74
  descriptors.push(...usbDeviceList);
31
75
  break;
32
76
  case 'bluetooth':
33
- this._logger.error('bluetooth is not supported yet!');
77
+ const bluetoothDeviceList = Object.values(this.#discoveredBluetoothDevices).map((bluetoothDevice) => ({
78
+ manufacturer: 'ledger',
79
+ transport: 'bluetooth',
80
+ get: async () => new LedgerDevice({ transportFactory, descriptor: bluetoothDevice }),
81
+ }));
82
+ descriptors.push(...bluetoothDeviceList);
34
83
  break;
35
84
  case 'tcp':
36
85
  descriptors.push({
@@ -61,5 +110,5 @@ export default {
61
110
  id: MODULE_ID,
62
111
  type: 'module',
63
112
  factory: createLedgerDiscovery,
64
- dependencies: ['logger', 'transportsFactories'],
113
+ dependencies: ['config', 'logger', 'transportsFactories'],
65
114
  };
@@ -1,2 +1,5 @@
1
1
  import type Transport from '@ledgerhq/hw-transport';
2
2
  export type TransportFactory = typeof Transport;
3
+ export type HwLedgerConfig = {
4
+ bluetoothScannerTimeout: number;
5
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/hw-ledger",
3
- "version": "2.3.0",
3
+ "version": "2.4.0",
4
4
  "description": "The ledger module",
5
5
  "author": "Exodus Movement Inc.",
6
6
  "repository": {
@@ -22,10 +22,10 @@
22
22
  "build": "tsc --build tsconfig.build.json",
23
23
  "build:watch": "tsc --build tsconfig.build.json && (concurrently 'tsc --build -w tsconfig.build.json')",
24
24
  "clean": "tsc --build --clean",
25
- "test": "jest --testMatch='**/*.unit.test.js' --coverageDirectory coverage/unit",
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 && jest --runInBand",
28
- "lint": "eslint . --ignore-path ../../.gitignore",
28
+ "lint": "run -T eslint . --ignore-path ../../.gitignore",
29
29
  "lint:fix": "yarn lint --fix",
30
30
  "prepublishOnly": "yarn run -T build --scope @exodus/hw-ledger"
31
31
  },
@@ -60,13 +60,11 @@
60
60
  "@types/secp256k1": "^4.0.3",
61
61
  "@zondax/zemu": "^0.43.1",
62
62
  "concurrently": "^7.6.0",
63
- "eslint": "^8.44.0",
64
63
  "istanbul-combine": "^0.3.0",
65
- "jest": "^29.1.2",
66
64
  "jest-when": "^3.5.2",
67
65
  "tiny-secp256k1": "^2.2.3",
68
66
  "ts-jest": "^29.0.3",
69
67
  "typescript": "^4.7.3"
70
68
  },
71
- "gitHead": "c066fd85fa5cf0090a3d1b573403c79c21a84102"
69
+ "gitHead": "f4899f51920c3103416a11db9fd9d5a8ed4af07c"
72
70
  }