@exodus/hw-ledger 1.3.3 → 2.1.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 ADDED
@@ -0,0 +1,83 @@
1
+ # Change Log
2
+
3
+ All notable changes to this project will be documented in this file.
4
+ See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
+
6
+ ## [2.1.0](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/hw-ledger@2.0.0...@exodus/hw-ledger@2.1.0) (2023-12-25)
7
+
8
+ ### Features
9
+
10
+ - add matic support ([#5172](https://github.com/ExodusMovement/exodus-hydra/issues/5172)) ([c8817c2](https://github.com/ExodusMovement/exodus-hydra/commit/c8817c250d42459b9583990bd6d0460d90f9a17a))
11
+
12
+ ## [2.0.0](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/hw-ledger@1.3.2...@exodus/hw-ledger@2.0.0) (2023-12-19)
13
+
14
+ ### ⚠ BREAKING CHANGES
15
+
16
+ - simplify transaction signing api for ledger (#4979)
17
+
18
+ ### Features
19
+
20
+ - add nested segwit to bitcoin ([#4981](https://github.com/ExodusMovement/exodus-hydra/issues/4981)) ([3721b60](https://github.com/ExodusMovement/exodus-hydra/commit/3721b607ea5176ad8461d4b97ac803e179f8a48f))
21
+ - add psbt tagging & sanitization to ledger ([#5059](https://github.com/ExodusMovement/exodus-hydra/issues/5059)) ([9e38c3b](https://github.com/ExodusMovement/exodus-hydra/commit/9e38c3b33c6beb67b1feee6eaf939c5a500da94a))
22
+ - simplify transaction signing api for ledger ([#4979](https://github.com/ExodusMovement/exodus-hydra/issues/4979)) ([30b1d45](https://github.com/ExodusMovement/exodus-hydra/commit/30b1d45602614c8b37cff926acaf830142fbf197))
23
+
24
+ ### Bug Fixes
25
+
26
+ - bump ledger packages ([#4990](https://github.com/ExodusMovement/exodus-hydra/issues/4990)) ([a3f60fc](https://github.com/ExodusMovement/exodus-hydra/commit/a3f60fc6dca2c0bbbdb37e6446d3b4f4438e2753))
27
+ - change polygon asset name to matic ([#4955](https://github.com/ExodusMovement/exodus-hydra/issues/4955)) ([e3d5ec3](https://github.com/ExodusMovement/exodus-hydra/commit/e3d5ec3e0d7e3f1a31082ca7bb77a13bb0ca3207))
28
+
29
+ ## [1.3.3](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/hw-ledger@1.3.2...@exodus/hw-ledger@1.3.3) (2023-12-11)
30
+
31
+ ### Bug Fixes
32
+
33
+ - bump ledger packages ([#4990](https://github.com/ExodusMovement/exodus-hydra/issues/4990)) ([a3f60fc](https://github.com/ExodusMovement/exodus-hydra/commit/a3f60fc6dca2c0bbbdb37e6446d3b4f4438e2753))
34
+ - change polygon asset name to matic ([#4955](https://github.com/ExodusMovement/exodus-hydra/issues/4955)) ([e3d5ec3](https://github.com/ExodusMovement/exodus-hydra/commit/e3d5ec3e0d7e3f1a31082ca7bb77a13bb0ca3207))
35
+
36
+ ## [1.3.2](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/hw-ledger@1.3.1...@exodus/hw-ledger@1.3.2) (2023-12-05)
37
+
38
+ ### Bug Fixes
39
+
40
+ - bump min required bitcoin firmware ([#4954](https://github.com/ExodusMovement/exodus-hydra/issues/4954)) ([ff19dfa](https://github.com/ExodusMovement/exodus-hydra/commit/ff19dfa3656ebf04d7f023f30a659a25a4f1c233))
41
+
42
+ ## [1.3.1](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/hw-ledger@1.3.0...@exodus/hw-ledger@1.3.1) (2023-11-28)
43
+
44
+ ### Bug Fixes
45
+
46
+ - ledger removed isEIP712Message export ([#4911](https://github.com/ExodusMovement/exodus-hydra/issues/4911)) ([dccbd2f](https://github.com/ExodusMovement/exodus-hydra/commit/dccbd2f7e53050e6132e2537f87571f70d445946))
47
+
48
+ ## [1.3.0](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/hw-ledger@1.2.0...@exodus/hw-ledger@1.3.0) (2023-11-28)
49
+
50
+ ### Features
51
+
52
+ - add ability to retrieve supported, installed and useable assets ([#4901](https://github.com/ExodusMovement/exodus-hydra/issues/4901)) ([d821590](https://github.com/ExodusMovement/exodus-hydra/commit/d82159062aa497c1ae5bbe4362cce8a2aff0cc9c))
53
+ - add generic `Message` type in `SignMessageParams` ([#4895](https://github.com/ExodusMovement/exodus-hydra/issues/4895)) ([fb688ea](https://github.com/ExodusMovement/exodus-hydra/commit/fb688ea071c18a1a041bbd53869f7afdce2c0b1d))
54
+ - add ledger `getFirstDevice` helper ([#4897](https://github.com/ExodusMovement/exodus-hydra/issues/4897)) ([6a2b362](https://github.com/ExodusMovement/exodus-hydra/commit/6a2b362e69b46e0d19be80394ff6f5533f6139ba))
55
+
56
+ ## [1.2.0](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/hw-ledger@1.1.0...@exodus/hw-ledger@1.2.0) (2023-11-27)
57
+
58
+ ### Features
59
+
60
+ - make hw-ledger a feature ([#4882](https://github.com/ExodusMovement/exodus-hydra/issues/4882)) ([33d93fb](https://github.com/ExodusMovement/exodus-hydra/commit/33d93fb0f4bb507e3788c0450a3e413e79815433))
61
+
62
+ ## [1.1.0](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/hw-ledger@1.0.0...@exodus/hw-ledger@1.1.0) (2023-08-21)
63
+
64
+ ### Features
65
+
66
+ - **hw-ledger:** add application management functions ([#2586](https://github.com/ExodusMovement/exodus-hydra/issues/2586)) ([125e9ef](https://github.com/ExodusMovement/exodus-hydra/commit/125e9efc835a56a66375e43f9072404af28f466d))
67
+ - **hw-ledger:** add bitcoin transaction & message signing support ([#2530](https://github.com/ExodusMovement/exodus-hydra/issues/2530)) ([5d17826](https://github.com/ExodusMovement/exodus-hydra/commit/5d17826335f6e85ba3b9179bb7a2343f686751fb))
68
+ - **hw-ledger:** add ethereum transaction signing ([#2315](https://github.com/ExodusMovement/exodus-hydra/issues/2315)) ([5b5c60e](https://github.com/ExodusMovement/exodus-hydra/commit/5b5c60e950948d24cea672e37dc171e1d7348343))
69
+ - **hw-ledger:** add polygon support ([#2293](https://github.com/ExodusMovement/exodus-hydra/issues/2293)) ([fd66005](https://github.com/ExodusMovement/exodus-hydra/commit/fd660055b0813917dd4055c408f8731ea448ad4f))
70
+ - **hw-ledger:** add prebuilt firmware ([#2212](https://github.com/ExodusMovement/exodus-hydra/issues/2212)) ([80667a4](https://github.com/ExodusMovement/exodus-hydra/commit/80667a4237a103c08e55dbf634a193dda0875ca0))
71
+ - **hw-ledger:** add solana transaction & message signing ([#2357](https://github.com/ExodusMovement/exodus-hydra/issues/2357)) ([2ca2a4e](https://github.com/ExodusMovement/exodus-hydra/commit/2ca2a4e93f813b1777953355c4d271033e7d98d0))
72
+ - **hw-ledger:** add Taproot address support ([#2442](https://github.com/ExodusMovement/exodus-hydra/issues/2442)) ([4d8350e](https://github.com/ExodusMovement/exodus-hydra/commit/4d8350ecbdca500e6b69a36ce83c7b4c63bb0e8f))
73
+ - **hw-ledger:** automatically open the application ([#2801](https://github.com/ExodusMovement/exodus-hydra/issues/2801)) ([80e04b4](https://github.com/ExodusMovement/exodus-hydra/commit/80e04b49ee886cc3ca9b65695a567d06b0909928))
74
+ - **hw-ledger:** bitcoin address derivation ([#2181](https://github.com/ExodusMovement/exodus-hydra/issues/2181)) ([19b77ec](https://github.com/ExodusMovement/exodus-hydra/commit/19b77ec223d998454843d82c4c47a4350e77299e))
75
+ - **hw-ledger:** ethereum address generation ([#2213](https://github.com/ExodusMovement/exodus-hydra/issues/2213)) ([1c5b326](https://github.com/ExodusMovement/exodus-hydra/commit/1c5b326d7fbbcf66ff6cb820c35c1e1d1d127821))
76
+ - **hw-ledger:** ethereum message signing ([#3058](https://github.com/ExodusMovement/exodus-hydra/issues/3058)) ([df07d2d](https://github.com/ExodusMovement/exodus-hydra/commit/df07d2d861cc20c1db366b47dc98802dd8e73f9c))
77
+ - **hw-ledger:** solana address derivation ([#2211](https://github.com/ExodusMovement/exodus-hydra/issues/2211)) ([1c73d28](https://github.com/ExodusMovement/exodus-hydra/commit/1c73d28798801016966a8034a3538b9abc4cbf16))
78
+ - **ledger:** add ethereum xpub support ([#3403](https://github.com/ExodusMovement/exodus-hydra/issues/3403)) ([7de98d2](https://github.com/ExodusMovement/exodus-hydra/commit/7de98d22080bcb46a9fe88e30d631a5ace3a923d))
79
+
80
+ ### Bug Fixes
81
+
82
+ - **hw-ledger:** support NFT transactions on Nano S ([#2961](https://github.com/ExodusMovement/exodus-hydra/issues/2961)) ([1161a80](https://github.com/ExodusMovement/exodus-hydra/commit/1161a807c581211132d56ec2912bc27b683207d1))
83
+ - missing dependencies ([#2408](https://github.com/ExodusMovement/exodus-hydra/issues/2408)) ([7791f2a](https://github.com/ExodusMovement/exodus-hydra/commit/7791f2aa10f318b0a3c8f932ec686fbe0cf16617))
@@ -1,45 +1,69 @@
1
1
  import bs58 from 'bs58';
2
- import { AppClient, DefaultWalletPolicy, PsbtV2 } from '@exodus/ledger-bitcoin';
2
+ import { AppClient, DefaultWalletPolicy, PsbtV2, } from '@exodus/ledger-bitcoin';
3
3
  import { splitBip44DerivationPath } from './utils';
4
4
  import assert from 'minimalistic-assert';
5
+ import { assertPsbtOnlyHasAllowedDerivationPaths, tagPsbtWithDerivationPaths } from './utils/psbt';
5
6
  async function createHandler(transport) {
6
7
  const app = new AppClient(transport);
7
8
  const getWalletPolicy = async (purpose, coin, account) => {
8
9
  const fpr = await app.getMasterFingerprint();
9
10
  const xpub = await app.getExtendedPubkey(`m/${purpose}/${coin}/${account}`);
10
- const desc = { "44'": 'pkh', "84'": 'wpkh', "86'": 'tr' }[purpose];
11
- return new DefaultWalletPolicy(`${desc}(@0/**)`, `[${fpr}/${purpose}/${coin}/${account}]${xpub}`);
11
+ const desc = {
12
+ "44'": 'pkh(@0/**)',
13
+ "49'": 'sh(wpkh(@0/**))',
14
+ "84'": 'wpkh(@0/**)',
15
+ "86'": 'tr(@0/**)',
16
+ }[purpose];
17
+ return new DefaultWalletPolicy(desc, `[${fpr}/${purpose}/${coin}/${account}]${xpub}`);
18
+ };
19
+ const getAddress = async (params) => {
20
+ const [purpose, coin, account, change, address] = splitBip44DerivationPath(params.derivationPath);
21
+ const walletPolicy = await getWalletPolicy(purpose, coin, account);
22
+ return app.getWalletAddress(walletPolicy, null, Number(change), Number(address), false);
23
+ };
24
+ const getXPub = async (params) => {
25
+ return app.getExtendedPubkey(params.derivationPath);
26
+ };
27
+ const getPublicKey = async (params) => {
28
+ return bs58.decode(await app.getExtendedPubkey(params.derivationPath)).slice(45, 45 + 33);
29
+ };
30
+ const signTransaction = async (params) => {
31
+ assert(Buffer.isBuffer(params.signableTransaction), 'signableTransaction must be PSBT buffer');
32
+ const psbtBuffer = params.signableTransaction;
33
+ const psbt = new PsbtV2();
34
+ psbt.deserialize(psbtBuffer);
35
+ const [purpose, coin, account] = splitBip44DerivationPath(params.derivationPaths[0]);
36
+ const fpr = await app.getMasterFingerprint();
37
+ const walletPolicy = await getWalletPolicy(purpose, coin, account);
38
+ const derivationPathToPublicKeyMap = Object.create(null);
39
+ for (const derivationPath of params.derivationPaths) {
40
+ derivationPathToPublicKeyMap[derivationPath] = await getPublicKey({
41
+ assetName: 'bitcoin',
42
+ derivationPath,
43
+ });
44
+ }
45
+ tagPsbtWithDerivationPaths(psbt, fpr, derivationPathToPublicKeyMap);
46
+ assertPsbtOnlyHasAllowedDerivationPaths(psbt, fpr, params.derivationPaths);
47
+ const signatures = await app.signPsbt(psbt.serialize(), walletPolicy, null);
48
+ const result = signatures.map(([inputIndex, partialSignature]) => ({
49
+ inputIndex,
50
+ publicKey: partialSignature.pubkey,
51
+ signature: partialSignature.signature,
52
+ tapleafHash: partialSignature.tapleafHash,
53
+ }));
54
+ return result;
55
+ };
56
+ const signMessage = async (params) => {
57
+ assert(Buffer.isBuffer(params.message.rawMessage), 'rawMessage must be buffer');
58
+ const signedMessageHex = await app.signMessage(params.message.rawMessage, params.derivationPath);
59
+ return Buffer.from(signedMessageHex, 'base64');
12
60
  };
13
61
  return {
14
- getAddress: async (params) => {
15
- const [purpose, coin, account, change, address] = splitBip44DerivationPath(params.derivationPath);
16
- const walletPolicy = await getWalletPolicy(purpose, coin, account);
17
- return app.getWalletAddress(walletPolicy, null, Number(change), Number(address), false);
18
- },
19
- getXPub: async (params) => {
20
- return app.getExtendedPubkey(params.derivationPath);
21
- },
22
- getPublicKey: async (params) => {
23
- return bs58.decode(await app.getExtendedPubkey(params.derivationPath)).slice(45, 45 + 33);
24
- },
25
- signTransaction: async (params) => {
26
- assert(Buffer.isBuffer(params.unsignedTx.txData), 'unsignedTx.txData must be PSBT buffer');
27
- const psbtBuffer = params.unsignedTx.txData;
28
- const psbt = new PsbtV2();
29
- psbt.deserialize(psbtBuffer);
30
- const [purpose, coin, account] = splitBip44DerivationPath(params.derivationPaths[0]);
31
- const walletPolicy = await getWalletPolicy(purpose, coin, account);
32
- const signatures = await app.signPsbt(psbtBuffer, walletPolicy, null);
33
- for (const [inputIndex, partialSignature] of signatures) {
34
- psbt.setInputPartialSig(inputIndex, partialSignature.pubkey, partialSignature.signature);
35
- }
36
- return psbt.serialize();
37
- },
38
- signMessage: async (params) => {
39
- assert(Buffer.isBuffer(params.message.rawMessage), 'rawMessage must be buffer');
40
- const signedMessageHex = await app.signMessage(params.message.rawMessage, params.derivationPath);
41
- return Buffer.from(signedMessageHex, 'base64');
42
- },
62
+ getAddress,
63
+ getPublicKey,
64
+ getXPub,
65
+ signTransaction,
66
+ signMessage,
43
67
  };
44
68
  }
45
69
  const metadata = {
@@ -44,10 +44,14 @@ async function createHandler(transport) {
44
44
  },
45
45
  signTransaction: async (params) => {
46
46
  assert(Array.isArray(params.derivationPaths) && params.derivationPaths.length === 1, 'derivationsPath array must be one element long');
47
- assert(params.unsignedTx.txData, 'txData must be defined');
48
- const deserializedTransaction = Buffer.isBuffer(params.unsignedTx.txData)
49
- ? ethers.parse(params.unsignedTx.txData)
50
- : params.unsignedTx.txData;
47
+ assert(Buffer.isBuffer(params.signableTransaction), 'signableTransaction must be Buffer');
48
+ const deserializedTransaction = ethers.parse(params.signableTransaction);
49
+ if (params.assetName === 'ethereum') {
50
+ deserializedTransaction.chainId = 1;
51
+ }
52
+ else if (params.assetName === 'matic') {
53
+ deserializedTransaction.chainId = 137;
54
+ }
51
55
  delete deserializedTransaction.r;
52
56
  delete deserializedTransaction.s;
53
57
  delete deserializedTransaction.v;
@@ -80,13 +84,16 @@ async function createHandler(transport) {
80
84
  throw err;
81
85
  }
82
86
  }
83
- const { r, s, v } = response;
84
- const serializedSignedTransaction = ethers.serialize(deserializedTransaction, {
85
- r: '0x' + r,
86
- s: '0x' + s,
87
- v: Number.parseInt(v, 16),
88
- });
89
- return Buffer.from(serializedSignedTransaction.slice(2), 'hex');
87
+ const { v, r, s } = response;
88
+ return [
89
+ {
90
+ signature: Buffer.concat([
91
+ Buffer.from(v, 'hex'),
92
+ Buffer.from(r, 'hex'),
93
+ Buffer.from(s, 'hex'),
94
+ ]),
95
+ },
96
+ ];
90
97
  },
91
98
  signMessage: async (params) => {
92
99
  const message = params.message;
@@ -4,28 +4,52 @@ import bs58 from 'bs58';
4
4
  import { XPubUnsupportedError } from '@exodus/hw-common';
5
5
  async function createHandler(transport) {
6
6
  const app = new AppSolana(transport);
7
+ const getAddress = async (params) => {
8
+ const { address } = await app.getAddress(params.derivationPath.replace('m/', ''));
9
+ return bs58.encode(address);
10
+ };
11
+ const getXPub = async (params) => {
12
+ throw new XPubUnsupportedError();
13
+ };
14
+ const getPublicKey = async (params) => {
15
+ const { address } = await app.getAddress(params.derivationPath.replace('m/', ''));
16
+ return address;
17
+ };
18
+ const signTransaction = async (params) => {
19
+ assert(Array.isArray(params.derivationPaths) && params.derivationPaths.length === 1, 'derivationsPath array must be one element long');
20
+ let serializedTransaction;
21
+ if (Buffer.isBuffer(params.signableTransaction)) {
22
+ serializedTransaction = params.signableTransaction;
23
+ }
24
+ else if (params.signableTransaction instanceof Uint8Array) {
25
+ serializedTransaction = Buffer.from(params.signableTransaction);
26
+ }
27
+ else if (params.signableTransaction.serialize) {
28
+ serializedTransaction = Buffer.from(params.signableTransaction.serialize());
29
+ }
30
+ if (!Buffer.isBuffer(serializedTransaction)) {
31
+ throw new TypeError(`signableTransaction must be Buffer or serializable to a Buffer.`);
32
+ }
33
+ const { signature } = await app.signTransaction(params.derivationPaths[0].replace('m/', ''), serializedTransaction);
34
+ const { address: publicKey } = await app.getAddress(params.derivationPaths[0].replace('m/', ''));
35
+ return [
36
+ {
37
+ publicKey,
38
+ signature,
39
+ },
40
+ ];
41
+ };
42
+ const signMessage = async (params) => {
43
+ assert(Buffer.isBuffer(params.message.rawMessage), 'rawMessage must be a buffer');
44
+ const { signature } = await app.signOffchainMessage(params.derivationPath.replace('m/', ''), params.message.rawMessage);
45
+ return signature;
46
+ };
7
47
  return {
8
- getAddress: async (params) => {
9
- const { address } = await app.getAddress(params.derivationPath.replace('m/', ''));
10
- return bs58.encode(address);
11
- },
12
- getXPub: async (params) => {
13
- throw new XPubUnsupportedError();
14
- },
15
- getPublicKey: async (params) => {
16
- const { address } = await app.getAddress(params.derivationPath.replace('m/', ''));
17
- return address;
18
- },
19
- signTransaction: async (params) => {
20
- assert(Array.isArray(params.derivationPaths) && params.derivationPaths.length === 1, 'derivationsPath array must be one element long');
21
- const { signature } = await app.signTransaction(params.derivationPaths[0].replace('m/', ''), params.unsignedTx.txData);
22
- return signature;
23
- },
24
- signMessage: async (params) => {
25
- assert(Buffer.isBuffer(params.message.rawMessage), 'rawMessage must be a buffer');
26
- const { signature } = await app.signOffchainMessage(params.derivationPath.replace('m/', ''), params.message.rawMessage);
27
- return signature;
28
- },
48
+ getAddress,
49
+ getPublicKey,
50
+ getXPub,
51
+ signTransaction,
52
+ signMessage,
29
53
  };
30
54
  }
31
55
  const metadata = {
@@ -0,0 +1,8 @@
1
+ export declare function splitDerivationPath(derivationPath: string): string[];
2
+ export type Bip44PathArray = [string, string, string, string, string];
3
+ export type Bip44PathArrayNumber = [number, number, number, number, number];
4
+ export declare function assertBip44Path(parts: string[]): asserts parts is Bip44PathArray;
5
+ export declare function splitBip44DerivationPath(derivationPath: string): Bip44PathArray;
6
+ export declare function assertBip44PathNumber(parts: readonly number[]): asserts parts is Bip44PathArrayNumber;
7
+ export declare function buildBip44PathFromNumber(parts: Bip44PathArrayNumber): string;
8
+ export declare function splitBip44PathToNumber(derivationPath: string): Bip44PathArrayNumber;
@@ -1,5 +1,4 @@
1
1
  import assert from 'minimalistic-assert';
2
- import bs58check from 'bs58check';
3
2
  export function splitDerivationPath(derivationPath) {
4
3
  assert(typeof derivationPath === 'string', 'derivationPath must be string');
5
4
  const parts = derivationPath.split('/');
@@ -7,7 +6,7 @@ export function splitDerivationPath(derivationPath) {
7
6
  assert(parts[0] === 'm', 'invalid derivationPath: must start with m');
8
7
  return parts.slice(1);
9
8
  }
10
- function assertBip44Path(parts) {
9
+ export function assertBip44Path(parts) {
11
10
  assert(parts.length === 5, 'invalid bip44 derivation path: length must be 5');
12
11
  assert(parts.every((part) => typeof part === 'string'));
13
12
  }
@@ -20,18 +19,23 @@ export function splitBip44DerivationPath(derivationPath) {
20
19
  assert(account.endsWith("'"), 'invalid derivationPath: account must be hardened');
21
20
  return [purpose, coin, account, change, address];
22
21
  }
23
- export function buildFauxXPUB(publicKey, chainCode) {
24
- const LEN = 78;
25
- const XPUB_PREFIX = 76067358;
26
- const buffer = Buffer.allocUnsafe(LEN);
27
- const fingerprint = 0;
28
- const depth = 0;
29
- const index = 0;
30
- buffer.writeUInt32BE(XPUB_PREFIX, 0);
31
- buffer.writeUInt8(depth, 4);
32
- buffer.writeUInt32BE(fingerprint, 5);
33
- buffer.writeUInt32BE(index, 9);
34
- chainCode.copy(buffer, 13);
35
- publicKey.copy(buffer, 45);
36
- return bs58check.encode(buffer);
22
+ export function assertBip44PathNumber(parts) {
23
+ assert(parts.length === 5, 'invalid bip44 derivation path: length must be 5');
24
+ assert(parts.every((part) => typeof part === 'number'));
25
+ }
26
+ export function buildBip44PathFromNumber(parts) {
27
+ const [purpose, coin, account, chainIndex, addressIndex] = parts;
28
+ const HARDENED_OFFSET = 2147483648;
29
+ return `m/${purpose - HARDENED_OFFSET}'/${coin - HARDENED_OFFSET}'/${account - HARDENED_OFFSET}'/${chainIndex}/${addressIndex}`;
30
+ }
31
+ export function splitBip44PathToNumber(derivationPath) {
32
+ const [purpose, coin, account, change, address] = splitBip44DerivationPath(derivationPath);
33
+ const HARDENED_OFFSET = 2147483648;
34
+ return [
35
+ Number(purpose.replaceAll(`'`, '')) + HARDENED_OFFSET,
36
+ Number(coin.replaceAll(`'`, '')) + HARDENED_OFFSET,
37
+ Number(account.replaceAll(`'`, '')) + HARDENED_OFFSET,
38
+ Number(change),
39
+ Number(address),
40
+ ];
37
41
  }
@@ -0,0 +1,3 @@
1
+ export * from './derivation-paths';
2
+ export * from './xpub';
3
+ export * from './psbt';
@@ -0,0 +1,3 @@
1
+ export * from './derivation-paths';
2
+ export * from './xpub';
3
+ export * from './psbt';
@@ -0,0 +1,4 @@
1
+ /// <reference types="node" />
2
+ import { PsbtV2 } from '@exodus/ledger-bitcoin';
3
+ export declare function assertPsbtOnlyHasAllowedDerivationPaths(psbt: PsbtV2, masterFingerprint: string, derivationPaths: string[]): void;
4
+ export declare function tagPsbtWithDerivationPaths(psbt: PsbtV2, masterFingerprint: string, derivationPathToPublicKeyMap: Record<string, Buffer>): void;
@@ -0,0 +1,96 @@
1
+ import assert from 'minimalistic-assert';
2
+ import { payments } from '@exodus/bitcoinjs-lib';
3
+ import { buildBip44PathFromNumber, assertBip44PathNumber, splitBip44PathToNumber, } from './derivation-paths';
4
+ export function assertPsbtOnlyHasAllowedDerivationPaths(psbt, masterFingerprint, derivationPaths) {
5
+ for (let i = 0; i < psbt.getGlobalInputCount(); i++) {
6
+ const BIP32_DERIVATION = 6;
7
+ const TAP_BIP32_DERIVATION = 22;
8
+ const publicKeysNormal = psbt.getInputKeyDatas(i, BIP32_DERIVATION);
9
+ const publicKeysTap = psbt.getInputKeyDatas(i, TAP_BIP32_DERIVATION);
10
+ for (const publicKey of publicKeysNormal) {
11
+ const bip32Derivation = psbt.getInputBip32Derivation(i, publicKey);
12
+ if (bip32Derivation) {
13
+ assertAllowedDerivation(bip32Derivation, masterFingerprint, derivationPaths);
14
+ }
15
+ }
16
+ for (const publicKey of publicKeysTap) {
17
+ const tapBip32Derivation = psbt.getInputTapBip32Derivation(i, publicKey);
18
+ if (tapBip32Derivation) {
19
+ assertAllowedDerivation(tapBip32Derivation, masterFingerprint, derivationPaths);
20
+ }
21
+ }
22
+ }
23
+ }
24
+ function assertAllowedDerivation(bip32Derivation, masterFingerprint, derivationPaths) {
25
+ const masterFingerprintToCheck = bip32Derivation.masterFingerprint.toString('hex');
26
+ const isOurMasterFingerprint = masterFingerprintToCheck === masterFingerprint;
27
+ if (isOurMasterFingerprint) {
28
+ assertBip44PathNumber(bip32Derivation.path);
29
+ const derivationPath = buildBip44PathFromNumber(bip32Derivation.path);
30
+ assert(derivationPaths.includes(derivationPath), `found unsupported derivation path: ${derivationPath}`);
31
+ }
32
+ }
33
+ export function tagPsbtWithDerivationPaths(psbt, masterFingerprint, derivationPathToPublicKeyMap) {
34
+ const masterFingerprintBuffer = Buffer.from(masterFingerprint, 'hex');
35
+ const scriptPubKeyToDerivationPathMap = computeScriptPubKeyToDerivationPathMap(derivationPathToPublicKeyMap);
36
+ for (let inputIndex = 0; inputIndex < psbt.getGlobalInputCount(); inputIndex++) {
37
+ const witnessUtxo = psbt.getInputWitnessUtxo(inputIndex);
38
+ assert(witnessUtxo, `witnessUtxo is currently the only supported type`);
39
+ const scriptPubKey = witnessUtxo.scriptPubKey.toString('hex');
40
+ const derivationPath = scriptPubKeyToDerivationPathMap[scriptPubKey];
41
+ if (derivationPath) {
42
+ const publicKey = derivationPathToPublicKeyMap[derivationPath];
43
+ assert(Buffer.isBuffer(publicKey), `no public key found for derivation path`);
44
+ const derivationPathNumerical = splitBip44PathToNumber(derivationPath);
45
+ if (isTaproot(witnessUtxo.scriptPubKey)) {
46
+ psbt.setInputTapBip32Derivation(inputIndex, publicKey.slice(1), [], masterFingerprintBuffer, derivationPathNumerical);
47
+ }
48
+ else {
49
+ psbt.setInputBip32Derivation(inputIndex, publicKey, masterFingerprintBuffer, derivationPathNumerical);
50
+ }
51
+ }
52
+ }
53
+ for (let outputIndex = 0; outputIndex < psbt.getGlobalOutputCount(); outputIndex++) {
54
+ const scriptPubKey = psbt.getOutputScript(outputIndex);
55
+ const derivationPath = scriptPubKeyToDerivationPathMap[scriptPubKey.toString('hex')];
56
+ if (derivationPath) {
57
+ const publicKey = derivationPathToPublicKeyMap[derivationPath];
58
+ assert(Buffer.isBuffer(publicKey), `no public key found for derivation path`);
59
+ const derivationPathNumerical = splitBip44PathToNumber(derivationPath);
60
+ if (isTaproot(scriptPubKey)) {
61
+ psbt.setOutputTapBip32Derivation(outputIndex, publicKey.slice(1), [], masterFingerprintBuffer, derivationPathNumerical);
62
+ }
63
+ else {
64
+ psbt.setOutputBip32Derivation(outputIndex, publicKey, masterFingerprintBuffer, derivationPathNumerical);
65
+ }
66
+ }
67
+ }
68
+ }
69
+ function computeScriptPubKeyToDerivationPathMap(derivationPathToPublicKeyMap) {
70
+ return Object.entries(derivationPathToPublicKeyMap).reduce((acc, [derivationPath, publicKey]) => {
71
+ if (derivationPath.startsWith('m/84')) {
72
+ const segwitV0 = payments.p2wpkh({
73
+ pubkey: publicKey,
74
+ }).output;
75
+ if (segwitV0) {
76
+ Object.assign(acc, {
77
+ [segwitV0.toString('hex')]: derivationPath,
78
+ });
79
+ }
80
+ }
81
+ else if (derivationPath.startsWith('m/86')) {
82
+ const taproot = payments.p2tr({
83
+ internalPubkey: publicKey.slice(1),
84
+ }).output;
85
+ if (taproot) {
86
+ Object.assign(acc, {
87
+ [taproot.toString('hex')]: derivationPath,
88
+ });
89
+ }
90
+ }
91
+ return acc;
92
+ }, Object.create(null));
93
+ }
94
+ function isTaproot(scriptPubKey) {
95
+ return scriptPubKey.length === 34 && scriptPubKey.at(0) === 0x51 && scriptPubKey.at(1) === 0x20;
96
+ }
@@ -0,0 +1,2 @@
1
+ /// <reference types="node" />
2
+ export declare function buildFauxXPUB(publicKey: Buffer, chainCode: Buffer): string;
@@ -0,0 +1,16 @@
1
+ import bs58check from 'bs58check';
2
+ export function buildFauxXPUB(publicKey, chainCode) {
3
+ const LEN = 78;
4
+ const XPUB_PREFIX = 76067358;
5
+ const buffer = Buffer.allocUnsafe(LEN);
6
+ const fingerprint = 0;
7
+ const depth = 0;
8
+ const index = 0;
9
+ buffer.writeUInt32BE(XPUB_PREFIX, 0);
10
+ buffer.writeUInt8(depth, 4);
11
+ buffer.writeUInt32BE(fingerprint, 5);
12
+ buffer.writeUInt32BE(index, 9);
13
+ chainCode.copy(buffer, 13);
14
+ publicKey.copy(buffer, 45);
15
+ return bs58check.encode(buffer);
16
+ }
@@ -14,6 +14,6 @@ export declare class LedgerDevice implements HardwareWalletDevice {
14
14
  getAddress: (params: GetAddressParams) => Promise<string>;
15
15
  getXPub: (params: GetXPubParams) => Promise<string>;
16
16
  getPublicKey: (params: GetPublicKeyParams) => Promise<Readonly<Uint8Array>>;
17
- signTransaction: (params: SignTransactionParams) => Promise<Readonly<Uint8Array>>;
17
+ signTransaction: (params: SignTransactionParams) => Promise<import("@exodus/hw-common").Signatures>;
18
18
  signMessage: (params: SignMessageParams) => Promise<Readonly<Uint8Array>>;
19
19
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/hw-ledger",
3
- "version": "1.3.3",
3
+ "version": "2.1.0",
4
4
  "description": "The ledger module",
5
5
  "author": "Exodus Movement Inc.",
6
6
  "repository": {
@@ -26,12 +26,14 @@
26
26
  "zemu:install": "docker pull zondax/builder-zemu:speculos-745fe46f81cfc18114b94a37beaefb58a09c0fcd",
27
27
  "test:integration": "yarn zemu:install && jest --runInBand",
28
28
  "lint": "eslint . --ignore-path ../../.gitignore",
29
- "lint:fix": "yarn lint --fix"
29
+ "lint:fix": "yarn lint --fix",
30
+ "prepublishOnly": "yarn run -T build --scope @exodus/hw-ledger"
30
31
  },
31
32
  "dependencies": {
33
+ "@exodus/bitcoinjs-lib": "^6.1.5-exodus.0",
32
34
  "@exodus/ethersproject-abi": "^5.4.2-exodus.2",
33
35
  "@exodus/ethersproject-transactions": "^5.4.2-exodus.0",
34
- "@exodus/hw-common": "^1.2.1",
36
+ "@exodus/hw-common": "^2.0.0",
35
37
  "@exodus/ledger-bitcoin": "^0.2.3-exodus.0",
36
38
  "@exodus/ledgerhq-hw-app-eth": "^6.33.7-exodus.3",
37
39
  "@exodus/secp256k1": "^4.0.2-exodus.0",
@@ -51,6 +53,7 @@
51
53
  "@ledgerhq/hw-transport-node-speculos": "^6.27.16",
52
54
  "@types/bs58": "^4.0.1",
53
55
  "@types/bs58check": "^2.1.0",
56
+ "@types/create-hash": "^1.2.6",
54
57
  "@types/jest-when": "^3.5.2",
55
58
  "@types/minimalistic-assert": "^1.0.1",
56
59
  "@types/secp256k1": "^4.0.3",
@@ -60,7 +63,9 @@
60
63
  "istanbul-combine": "^0.3.0",
61
64
  "jest": "^29.1.2",
62
65
  "jest-when": "^3.5.2",
66
+ "tiny-secp256k1": "^2.2.3",
63
67
  "ts-jest": "^29.0.3",
64
68
  "typescript": "^4.7.3"
65
- }
69
+ },
70
+ "gitHead": "85a22445ece0c52a1efd5d4323824e52fbfce116"
66
71
  }
@@ -1,6 +0,0 @@
1
- /// <reference types="node" />
2
- export declare function splitDerivationPath(derivationPath: string): string[];
3
- type Bip44PathArray = [string, string, string, string, string];
4
- export declare function splitBip44DerivationPath(derivationPath: string): Bip44PathArray;
5
- export declare function buildFauxXPUB(publicKey: Buffer, chainCode: Buffer): string;
6
- export {};