@exodus/hw-ledger 1.3.3 → 2.0.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 +77 -0
- package/lib/module/assets/bitcoin.js +56 -32
- package/lib/module/assets/ethereum.js +12 -11
- package/lib/module/assets/solana.js +45 -21
- package/lib/module/assets/utils/derivation-paths.d.ts +8 -0
- package/lib/module/assets/{utils.js → utils/derivation-paths.js} +20 -16
- package/lib/module/assets/utils/index.d.ts +3 -0
- package/lib/module/assets/utils/index.js +3 -0
- package/lib/module/assets/utils/psbt.d.ts +4 -0
- package/lib/module/assets/utils/psbt.js +96 -0
- package/lib/module/assets/utils/xpub.d.ts +2 -0
- package/lib/module/assets/utils/xpub.js +16 -0
- package/lib/module/device.d.ts +1 -1
- package/package.json +9 -4
- package/lib/module/assets/utils.d.ts +0 -6
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
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.0.0](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/hw-ledger@1.3.2...@exodus/hw-ledger@2.0.0) (2023-12-19)
|
|
7
|
+
|
|
8
|
+
### ⚠ BREAKING CHANGES
|
|
9
|
+
|
|
10
|
+
- simplify transaction signing api for ledger (#4979)
|
|
11
|
+
|
|
12
|
+
### Features
|
|
13
|
+
|
|
14
|
+
- add nested segwit to bitcoin ([#4981](https://github.com/ExodusMovement/exodus-hydra/issues/4981)) ([3721b60](https://github.com/ExodusMovement/exodus-hydra/commit/3721b607ea5176ad8461d4b97ac803e179f8a48f))
|
|
15
|
+
- add psbt tagging & sanitization to ledger ([#5059](https://github.com/ExodusMovement/exodus-hydra/issues/5059)) ([9e38c3b](https://github.com/ExodusMovement/exodus-hydra/commit/9e38c3b33c6beb67b1feee6eaf939c5a500da94a))
|
|
16
|
+
- simplify transaction signing api for ledger ([#4979](https://github.com/ExodusMovement/exodus-hydra/issues/4979)) ([30b1d45](https://github.com/ExodusMovement/exodus-hydra/commit/30b1d45602614c8b37cff926acaf830142fbf197))
|
|
17
|
+
|
|
18
|
+
### Bug Fixes
|
|
19
|
+
|
|
20
|
+
- bump ledger packages ([#4990](https://github.com/ExodusMovement/exodus-hydra/issues/4990)) ([a3f60fc](https://github.com/ExodusMovement/exodus-hydra/commit/a3f60fc6dca2c0bbbdb37e6446d3b4f4438e2753))
|
|
21
|
+
- change polygon asset name to matic ([#4955](https://github.com/ExodusMovement/exodus-hydra/issues/4955)) ([e3d5ec3](https://github.com/ExodusMovement/exodus-hydra/commit/e3d5ec3e0d7e3f1a31082ca7bb77a13bb0ca3207))
|
|
22
|
+
|
|
23
|
+
## [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)
|
|
24
|
+
|
|
25
|
+
### Bug Fixes
|
|
26
|
+
|
|
27
|
+
- bump ledger packages ([#4990](https://github.com/ExodusMovement/exodus-hydra/issues/4990)) ([a3f60fc](https://github.com/ExodusMovement/exodus-hydra/commit/a3f60fc6dca2c0bbbdb37e6446d3b4f4438e2753))
|
|
28
|
+
- change polygon asset name to matic ([#4955](https://github.com/ExodusMovement/exodus-hydra/issues/4955)) ([e3d5ec3](https://github.com/ExodusMovement/exodus-hydra/commit/e3d5ec3e0d7e3f1a31082ca7bb77a13bb0ca3207))
|
|
29
|
+
|
|
30
|
+
## [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)
|
|
31
|
+
|
|
32
|
+
### Bug Fixes
|
|
33
|
+
|
|
34
|
+
- bump min required bitcoin firmware ([#4954](https://github.com/ExodusMovement/exodus-hydra/issues/4954)) ([ff19dfa](https://github.com/ExodusMovement/exodus-hydra/commit/ff19dfa3656ebf04d7f023f30a659a25a4f1c233))
|
|
35
|
+
|
|
36
|
+
## [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)
|
|
37
|
+
|
|
38
|
+
### Bug Fixes
|
|
39
|
+
|
|
40
|
+
- ledger removed isEIP712Message export ([#4911](https://github.com/ExodusMovement/exodus-hydra/issues/4911)) ([dccbd2f](https://github.com/ExodusMovement/exodus-hydra/commit/dccbd2f7e53050e6132e2537f87571f70d445946))
|
|
41
|
+
|
|
42
|
+
## [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)
|
|
43
|
+
|
|
44
|
+
### Features
|
|
45
|
+
|
|
46
|
+
- 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))
|
|
47
|
+
- add generic `Message` type in `SignMessageParams` ([#4895](https://github.com/ExodusMovement/exodus-hydra/issues/4895)) ([fb688ea](https://github.com/ExodusMovement/exodus-hydra/commit/fb688ea071c18a1a041bbd53869f7afdce2c0b1d))
|
|
48
|
+
- add ledger `getFirstDevice` helper ([#4897](https://github.com/ExodusMovement/exodus-hydra/issues/4897)) ([6a2b362](https://github.com/ExodusMovement/exodus-hydra/commit/6a2b362e69b46e0d19be80394ff6f5533f6139ba))
|
|
49
|
+
|
|
50
|
+
## [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)
|
|
51
|
+
|
|
52
|
+
### Features
|
|
53
|
+
|
|
54
|
+
- make hw-ledger a feature ([#4882](https://github.com/ExodusMovement/exodus-hydra/issues/4882)) ([33d93fb](https://github.com/ExodusMovement/exodus-hydra/commit/33d93fb0f4bb507e3788c0450a3e413e79815433))
|
|
55
|
+
|
|
56
|
+
## [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)
|
|
57
|
+
|
|
58
|
+
### Features
|
|
59
|
+
|
|
60
|
+
- **hw-ledger:** add application management functions ([#2586](https://github.com/ExodusMovement/exodus-hydra/issues/2586)) ([125e9ef](https://github.com/ExodusMovement/exodus-hydra/commit/125e9efc835a56a66375e43f9072404af28f466d))
|
|
61
|
+
- **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))
|
|
62
|
+
- **hw-ledger:** add ethereum transaction signing ([#2315](https://github.com/ExodusMovement/exodus-hydra/issues/2315)) ([5b5c60e](https://github.com/ExodusMovement/exodus-hydra/commit/5b5c60e950948d24cea672e37dc171e1d7348343))
|
|
63
|
+
- **hw-ledger:** add polygon support ([#2293](https://github.com/ExodusMovement/exodus-hydra/issues/2293)) ([fd66005](https://github.com/ExodusMovement/exodus-hydra/commit/fd660055b0813917dd4055c408f8731ea448ad4f))
|
|
64
|
+
- **hw-ledger:** add prebuilt firmware ([#2212](https://github.com/ExodusMovement/exodus-hydra/issues/2212)) ([80667a4](https://github.com/ExodusMovement/exodus-hydra/commit/80667a4237a103c08e55dbf634a193dda0875ca0))
|
|
65
|
+
- **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))
|
|
66
|
+
- **hw-ledger:** add Taproot address support ([#2442](https://github.com/ExodusMovement/exodus-hydra/issues/2442)) ([4d8350e](https://github.com/ExodusMovement/exodus-hydra/commit/4d8350ecbdca500e6b69a36ce83c7b4c63bb0e8f))
|
|
67
|
+
- **hw-ledger:** automatically open the application ([#2801](https://github.com/ExodusMovement/exodus-hydra/issues/2801)) ([80e04b4](https://github.com/ExodusMovement/exodus-hydra/commit/80e04b49ee886cc3ca9b65695a567d06b0909928))
|
|
68
|
+
- **hw-ledger:** bitcoin address derivation ([#2181](https://github.com/ExodusMovement/exodus-hydra/issues/2181)) ([19b77ec](https://github.com/ExodusMovement/exodus-hydra/commit/19b77ec223d998454843d82c4c47a4350e77299e))
|
|
69
|
+
- **hw-ledger:** ethereum address generation ([#2213](https://github.com/ExodusMovement/exodus-hydra/issues/2213)) ([1c5b326](https://github.com/ExodusMovement/exodus-hydra/commit/1c5b326d7fbbcf66ff6cb820c35c1e1d1d127821))
|
|
70
|
+
- **hw-ledger:** ethereum message signing ([#3058](https://github.com/ExodusMovement/exodus-hydra/issues/3058)) ([df07d2d](https://github.com/ExodusMovement/exodus-hydra/commit/df07d2d861cc20c1db366b47dc98802dd8e73f9c))
|
|
71
|
+
- **hw-ledger:** solana address derivation ([#2211](https://github.com/ExodusMovement/exodus-hydra/issues/2211)) ([1c73d28](https://github.com/ExodusMovement/exodus-hydra/commit/1c73d28798801016966a8034a3538b9abc4cbf16))
|
|
72
|
+
- **ledger:** add ethereum xpub support ([#3403](https://github.com/ExodusMovement/exodus-hydra/issues/3403)) ([7de98d2](https://github.com/ExodusMovement/exodus-hydra/commit/7de98d22080bcb46a9fe88e30d631a5ace3a923d))
|
|
73
|
+
|
|
74
|
+
### Bug Fixes
|
|
75
|
+
|
|
76
|
+
- **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))
|
|
77
|
+
- 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 = {
|
|
11
|
-
|
|
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
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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,8 @@ 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.
|
|
48
|
-
const deserializedTransaction =
|
|
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);
|
|
51
49
|
delete deserializedTransaction.r;
|
|
52
50
|
delete deserializedTransaction.s;
|
|
53
51
|
delete deserializedTransaction.v;
|
|
@@ -80,13 +78,16 @@ async function createHandler(transport) {
|
|
|
80
78
|
throw err;
|
|
81
79
|
}
|
|
82
80
|
}
|
|
83
|
-
const { r, s
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
81
|
+
const { v, r, s } = response;
|
|
82
|
+
return [
|
|
83
|
+
{
|
|
84
|
+
signature: Buffer.concat([
|
|
85
|
+
Buffer.from(v, 'hex'),
|
|
86
|
+
Buffer.from(r, 'hex'),
|
|
87
|
+
Buffer.from(s, 'hex'),
|
|
88
|
+
]),
|
|
89
|
+
},
|
|
90
|
+
];
|
|
90
91
|
},
|
|
91
92
|
signMessage: async (params) => {
|
|
92
93
|
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
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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,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,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
|
+
}
|
package/lib/module/device.d.ts
CHANGED
|
@@ -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<
|
|
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": "
|
|
3
|
+
"version": "2.0.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": "^
|
|
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": "bd3c7a6093ef581c8c0d694761cde1114a486c75"
|
|
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 {};
|