@exodus/hw-ledger 1.3.2 → 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 +24 -0
- package/README.md +21 -10
- package/lib/module/assets/bitcoin.js +56 -32
- package/lib/module/assets/ethereum.js +12 -11
- package/lib/module/assets/index.js +2 -2
- 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 +10 -5
- package/lib/module/assets/utils.d.ts +0 -6
- /package/lib/module/assets/{polygon.d.ts → matic.d.ts} +0 -0
- /package/lib/module/assets/{polygon.js → matic.js} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,30 @@
|
|
|
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.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
|
+
|
|
6
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)
|
|
7
31
|
|
|
8
32
|
### Bug Fixes
|
package/README.md
CHANGED
|
@@ -59,29 +59,40 @@ If you're on Linux then it's highly likely that you'll want to setup your udev r
|
|
|
59
59
|
|
|
60
60
|
### Installing Ledger Emulator (Recommended)
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
Clone the speculos repository & change your current working directory
|
|
63
63
|
|
|
64
64
|
```bash
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
git clone https://github.com/LedgerHQ/speculos.git --branch v0.3.5 --single-branch --depth 1 speculos-0.3.5
|
|
66
|
+
cd speculos-0.3.5
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
-
|
|
69
|
+
Get the Docker image
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
docker image pull ghcr.io/ledgerhq/speculos:sha-8241dc2 # v0.3.5
|
|
73
|
+
docker image tag ghcr.io/ledgerhq/speculos:sha-8241dc2 speculos
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Get the Docker image for ARM (M1/M2)
|
|
70
77
|
|
|
71
78
|
```bash
|
|
72
|
-
|
|
73
|
-
|
|
79
|
+
docker image build -t ghcr.io/ledgerhq/speculos-builder:latest -f build.Dockerfile .
|
|
80
|
+
docker image build -t speculos:latest .
|
|
74
81
|
```
|
|
75
82
|
|
|
76
83
|
Launch the emulator while within the `speculos` directory
|
|
77
84
|
|
|
78
85
|
```bash
|
|
79
|
-
docker run --rm -it -v "$(pwd)"/apps:/speculos/apps \
|
|
80
|
-
-p
|
|
81
|
-
--
|
|
86
|
+
docker container run --rm -it -v "$(pwd)"/apps:/speculos/apps:ro \
|
|
87
|
+
-p 127.0.0.1:4000:4000 -p 127.0.0.1:40000:40000 speculos \
|
|
88
|
+
--display headless --api-port 4000 --apdu-port 40000 --seed "secret" --model nanos --sdk 2.0 ./apps/btc.elf
|
|
82
89
|
```
|
|
83
90
|
|
|
84
|
-
Now visit http://127.0.0.1:
|
|
91
|
+
Now visit http://127.0.0.1:4000 to interact with the emulator.
|
|
92
|
+
|
|
93
|
+
### MacOS launch issues:
|
|
94
|
+
|
|
95
|
+
- `port XXXX is already in use`: re-map any available host port ([doc](https://docs.docker.com/network/#published-ports))
|
|
85
96
|
|
|
86
97
|
### Install udev rules for real devices (Linux only)
|
|
87
98
|
|
|
@@ -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;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import bitcoin from './bitcoin';
|
|
2
2
|
import ethereum from './ethereum';
|
|
3
|
-
import
|
|
3
|
+
import matic from './matic';
|
|
4
4
|
import solana from './solana';
|
|
5
5
|
export const assetApplications = {
|
|
6
6
|
bitcoin,
|
|
7
7
|
ethereum,
|
|
8
|
-
|
|
8
|
+
matic,
|
|
9
9
|
solana,
|
|
10
10
|
};
|
|
11
11
|
export const supportedBaseAssetNames = Object.keys(assetApplications);
|
|
@@ -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,14 +26,16 @@
|
|
|
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
|
-
"@exodus/ledgerhq-hw-app-eth": "^6.33.7-exodus.
|
|
38
|
+
"@exodus/ledgerhq-hw-app-eth": "^6.33.7-exodus.3",
|
|
37
39
|
"@exodus/secp256k1": "^4.0.2-exodus.0",
|
|
38
40
|
"@ledgerhq/hw-app-solana": "^7.0.10",
|
|
39
41
|
"@ledgerhq/hw-transport": "^6.28.5",
|
|
@@ -45,11 +47,13 @@
|
|
|
45
47
|
},
|
|
46
48
|
"devDependencies": {
|
|
47
49
|
"@exodus/bip32": "^2.0.0",
|
|
50
|
+
"@exodus/ledgerhq-devices": "^8.1.0-exodus.0",
|
|
48
51
|
"@exodus/module": "^1.2.2",
|
|
49
52
|
"@ledgerhq/hw-transport-node-hid": "^6.27.17",
|
|
50
53
|
"@ledgerhq/hw-transport-node-speculos": "^6.27.16",
|
|
51
54
|
"@types/bs58": "^4.0.1",
|
|
52
55
|
"@types/bs58check": "^2.1.0",
|
|
56
|
+
"@types/create-hash": "^1.2.6",
|
|
53
57
|
"@types/jest-when": "^3.5.2",
|
|
54
58
|
"@types/minimalistic-assert": "^1.0.1",
|
|
55
59
|
"@types/secp256k1": "^4.0.3",
|
|
@@ -59,8 +63,9 @@
|
|
|
59
63
|
"istanbul-combine": "^0.3.0",
|
|
60
64
|
"jest": "^29.1.2",
|
|
61
65
|
"jest-when": "^3.5.2",
|
|
66
|
+
"tiny-secp256k1": "^2.2.3",
|
|
62
67
|
"ts-jest": "^29.0.3",
|
|
63
68
|
"typescript": "^4.7.3"
|
|
64
69
|
},
|
|
65
|
-
"gitHead": "
|
|
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 {};
|
|
File without changes
|
|
File without changes
|