@exodus/hw-ledger 1.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/README.md +92 -0
- package/lib/assets/bitcoin.d.ts +3 -0
- package/lib/assets/bitcoin.js +21 -0
- package/lib/assets/index.d.ts +5 -0
- package/lib/assets/index.js +4 -0
- package/lib/constants.d.ts +1 -0
- package/lib/constants.js +1 -0
- package/lib/device.d.ts +13 -0
- package/lib/device.js +33 -0
- package/lib/discovery.d.ts +25 -0
- package/lib/discovery.js +52 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +2 -0
- package/package.json +46 -0
package/README.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# @exodus/hw-ledger
|
|
2
|
+
|
|
3
|
+
```mermaid
|
|
4
|
+
flowchart LR
|
|
5
|
+
AA[TransportFactory] --> A
|
|
6
|
+
A[LedgerDiscovery] -->|"list()"| B(HardwareWalletDescriptor)
|
|
7
|
+
B --> |"get()"| C(LedgerDevice)
|
|
8
|
+
C --> |"getAddress()"| D{Pick by \nasset}
|
|
9
|
+
D -->|bitcoin| E[BitcoinHandlers]
|
|
10
|
+
D -->|ethereum| F[EthereumHandlers]
|
|
11
|
+
D -->|solana| G[SolanaHandlers]
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Usage
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
import createLedgerDiscover from '@exodus/hw-ledger'
|
|
18
|
+
import Transport from '@ledgerhq/hw-transport-node-speculos'
|
|
19
|
+
|
|
20
|
+
// Initialize transport factories depending on the platform
|
|
21
|
+
const ledgerDiscovery = createLedgerDiscover({
|
|
22
|
+
logger,
|
|
23
|
+
transports: {
|
|
24
|
+
tcp: Transport, // emulator transport over tcp
|
|
25
|
+
},
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
const ledgerDescriptors = await ledgerDiscovery.list()
|
|
29
|
+
// [
|
|
30
|
+
// {
|
|
31
|
+
// manufacturer: 'ledger',
|
|
32
|
+
// transportType: 'tcp',
|
|
33
|
+
// get: function () => { ... }
|
|
34
|
+
// }
|
|
35
|
+
// ]
|
|
36
|
+
|
|
37
|
+
let ledgerDevice = null
|
|
38
|
+
try {
|
|
39
|
+
ledgerDevice = await ledgerDescriptors[0].get()
|
|
40
|
+
} catch (err) {
|
|
41
|
+
// Instantiating the device may fail, deal with this.
|
|
42
|
+
// e.g unplugged, udev issues, no networking
|
|
43
|
+
logger.error(`Unable to connect to device.`, err)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (ledgerDevice) {
|
|
47
|
+
const address = await ledgerDevice.getAddress({
|
|
48
|
+
assetName: 'bitcoin',
|
|
49
|
+
derivationPath: `m/86'/0'/0'/0/0`,
|
|
50
|
+
})
|
|
51
|
+
logger.log(`bitcoin address is ${address}`)
|
|
52
|
+
// bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Development
|
|
57
|
+
|
|
58
|
+
If you're on Linux then it's highly likely that you'll want to setup your udev rules.
|
|
59
|
+
|
|
60
|
+
### Installing Ledger Emulator (Recommended)
|
|
61
|
+
|
|
62
|
+
Get the Docker images
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
docker pull ghcr.io/ledgerhq/speculos
|
|
66
|
+
docker image tag ghcr.io/ledgerhq/speculos speculos
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Clone the speculos repository & change your current working directory
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
git clone https://github.com/LedgerHQ/speculos.git
|
|
73
|
+
cd speculos
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Launch the emulator while within the `speculos` directory
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
docker run --rm -it -v "$(pwd)"/apps:/speculos/apps \
|
|
80
|
+
-p 1234:1234 -p 5000:5000 -p 40000:40000 -p 41000:41000 speculos \
|
|
81
|
+
--model nanos ./apps/btc.elf --sdk 2.0 --seed "secret" --display headless --apdu-port 40000 --vnc-port 41000
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Now visit http://127.0.0.1:5000 to interact with the emulator.
|
|
85
|
+
|
|
86
|
+
### Install udev rules for real devices (Linux only)
|
|
87
|
+
|
|
88
|
+
If you want to talk with real devices then you might require setting up udev rules.
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
sudo ./scripts/setup_udev.sh
|
|
92
|
+
```
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import AppBtc from '@ledgerhq/hw-app-btc';
|
|
2
|
+
export default async function (transport) {
|
|
3
|
+
const app = new AppBtc({ transport });
|
|
4
|
+
return {
|
|
5
|
+
getAddress: async (params) => {
|
|
6
|
+
throw new Error('implement me');
|
|
7
|
+
},
|
|
8
|
+
getXPub: async (params) => {
|
|
9
|
+
throw new Error('implement me');
|
|
10
|
+
},
|
|
11
|
+
getPublicKey: async (params) => {
|
|
12
|
+
throw new Error('implement me');
|
|
13
|
+
},
|
|
14
|
+
signTransaction: async (params) => {
|
|
15
|
+
throw new Error('implement me');
|
|
16
|
+
},
|
|
17
|
+
signMessage: async (params) => {
|
|
18
|
+
throw new Error('implement me');
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type Transport from '@ledgerhq/hw-transport';
|
|
2
|
+
import type { HardwareWalletDevice } from '@exodus/hw-common';
|
|
3
|
+
type HandlerFactory = (transport: Transport) => Promise<HardwareWalletDevice>;
|
|
4
|
+
export declare const assetHandlerFactories: Record<string, HandlerFactory>;
|
|
5
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const APDU_PORT = 40000;
|
package/lib/constants.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const APDU_PORT = 40000;
|
package/lib/device.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { GetAddressParams, GetPublicKeyParams, GetXPubParams, HardwareWalletDevice, SignMessageParams, SignTransactionParams } from '@exodus/hw-common';
|
|
2
|
+
import type Transport from '@ledgerhq/hw-transport';
|
|
3
|
+
export declare class LedgerDevice implements HardwareWalletDevice {
|
|
4
|
+
#private;
|
|
5
|
+
constructor({ transport }: {
|
|
6
|
+
transport: Transport;
|
|
7
|
+
});
|
|
8
|
+
getAddress: (params: GetAddressParams) => Promise<string>;
|
|
9
|
+
getXPub: (params: GetXPubParams) => Promise<string>;
|
|
10
|
+
getPublicKey: (params: GetPublicKeyParams) => Promise<Readonly<Uint8Array>>;
|
|
11
|
+
signTransaction: (params: SignTransactionParams) => Promise<Readonly<Uint8Array>>;
|
|
12
|
+
signMessage: (params: SignMessageParams) => Promise<Readonly<Uint8Array>>;
|
|
13
|
+
}
|
package/lib/device.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import assert from 'minimalistic-assert';
|
|
2
|
+
import { assetHandlerFactories } from './assets';
|
|
3
|
+
export class LedgerDevice {
|
|
4
|
+
#transport;
|
|
5
|
+
constructor({ transport }) {
|
|
6
|
+
this.#transport = transport;
|
|
7
|
+
}
|
|
8
|
+
#getAssetHandler = async (assetName) => {
|
|
9
|
+
const handlerFactory = assetHandlerFactories[assetName];
|
|
10
|
+
assert(handlerFactory, `ledger does not support ${assetName}`);
|
|
11
|
+
return handlerFactory(this.#transport);
|
|
12
|
+
};
|
|
13
|
+
getAddress = async (params) => {
|
|
14
|
+
const handler = await this.#getAssetHandler(params.assetName);
|
|
15
|
+
return handler.getAddress(params);
|
|
16
|
+
};
|
|
17
|
+
getXPub = async (params) => {
|
|
18
|
+
const handler = await this.#getAssetHandler(params.assetName);
|
|
19
|
+
return handler.getXPub(params);
|
|
20
|
+
};
|
|
21
|
+
getPublicKey = async (params) => {
|
|
22
|
+
const handler = await this.#getAssetHandler(params.assetName);
|
|
23
|
+
return handler.getPublicKey(params);
|
|
24
|
+
};
|
|
25
|
+
signTransaction = async (params) => {
|
|
26
|
+
const handler = await this.#getAssetHandler(params.assetName);
|
|
27
|
+
return handler.signTransaction(params);
|
|
28
|
+
};
|
|
29
|
+
signMessage = async (params) => {
|
|
30
|
+
const handler = await this.#getAssetHandler(params.assetName);
|
|
31
|
+
return handler.signMessage(params);
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import ExodusModule from '@exodus/module';
|
|
2
|
+
import type Transport from '@ledgerhq/hw-transport';
|
|
3
|
+
import { Logger } from '~/shared/types';
|
|
4
|
+
import type { HardwareWalletDescriptor, HardwareWalletDiscovery } from '@exodus/hw-common';
|
|
5
|
+
type TransportFactory = typeof Transport;
|
|
6
|
+
export type Dependencies = {
|
|
7
|
+
logger: Logger;
|
|
8
|
+
transportsFactories: {
|
|
9
|
+
usb: TransportFactory;
|
|
10
|
+
bluetooth: TransportFactory;
|
|
11
|
+
tcp: TransportFactory;
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
export declare class LedgerDiscovery extends ExodusModule implements HardwareWalletDiscovery {
|
|
15
|
+
#private;
|
|
16
|
+
constructor({ logger, transportsFactories }: Dependencies);
|
|
17
|
+
list: () => Promise<HardwareWalletDescriptor[]>;
|
|
18
|
+
}
|
|
19
|
+
declare const _default: {
|
|
20
|
+
id: string;
|
|
21
|
+
type: string;
|
|
22
|
+
factory: (deps: Dependencies) => LedgerDiscovery;
|
|
23
|
+
dependencies: string[];
|
|
24
|
+
};
|
|
25
|
+
export default _default;
|
package/lib/discovery.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import ExodusModule from '@exodus/module';
|
|
2
|
+
import assert from 'minimalistic-assert';
|
|
3
|
+
import { APDU_PORT } from './constants';
|
|
4
|
+
import { LedgerDevice } from './device';
|
|
5
|
+
const MODULE_ID = 'hardwareWalletLedger';
|
|
6
|
+
export class LedgerDiscovery extends ExodusModule {
|
|
7
|
+
#transportsFactories;
|
|
8
|
+
constructor({ logger, transportsFactories }) {
|
|
9
|
+
super({ name: MODULE_ID, logger });
|
|
10
|
+
assert(typeof transportsFactories === 'object' &&
|
|
11
|
+
Object.keys(transportsFactories).length > 0 &&
|
|
12
|
+
Object.entries(transportsFactories).every(([transportName, transportFactory]) => ['usb', 'bluetooth', 'tcp'].includes(transportName) &&
|
|
13
|
+
typeof transportFactory === 'object'), `invalid transports`);
|
|
14
|
+
this.#transportsFactories = transportsFactories;
|
|
15
|
+
}
|
|
16
|
+
list = async () => {
|
|
17
|
+
const descriptors = [];
|
|
18
|
+
for (const [transportName, transportFactory] of Object.entries(this.#transportsFactories)) {
|
|
19
|
+
const deviceList = await transportFactory.list();
|
|
20
|
+
switch (transportName) {
|
|
21
|
+
case 'usb':
|
|
22
|
+
const usbDeviceList = deviceList.map((hidDevice) => ({
|
|
23
|
+
manufacturer: 'ledger',
|
|
24
|
+
transport: 'usb',
|
|
25
|
+
get: async () => new LedgerDevice({ transport: await transportFactory.open(hidDevice) }),
|
|
26
|
+
}));
|
|
27
|
+
descriptors.push(...usbDeviceList);
|
|
28
|
+
break;
|
|
29
|
+
case 'bluetooth':
|
|
30
|
+
this._logger.error(`bluetooth is not supported yet!`);
|
|
31
|
+
break;
|
|
32
|
+
case 'tcp':
|
|
33
|
+
descriptors.push({
|
|
34
|
+
manufacturer: 'ledger',
|
|
35
|
+
transport: 'tcp',
|
|
36
|
+
get: async () => new LedgerDevice({ transport: await transportFactory.open({ apduPort: APDU_PORT }) }),
|
|
37
|
+
});
|
|
38
|
+
break;
|
|
39
|
+
default:
|
|
40
|
+
throw new Error(`unknown transport type ${transportName}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return descriptors;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const createLedgerDiscovery = (deps) => new LedgerDiscovery({ ...deps });
|
|
47
|
+
export default {
|
|
48
|
+
id: MODULE_ID,
|
|
49
|
+
type: 'module',
|
|
50
|
+
factory: createLedgerDiscovery,
|
|
51
|
+
dependencies: ['logger'],
|
|
52
|
+
};
|
package/lib/index.d.ts
ADDED
package/lib/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@exodus/hw-ledger",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "The ledger module",
|
|
5
|
+
"author": "Exodus Movement Inc",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/ExodusMovement/exodus-hydra.git"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/ExodusMovement/exodus-hydra/tree/master/modules/hw-ledger",
|
|
11
|
+
"license": "UNLICENSED",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/ExodusMovement/exodus-hydra/issues?q=is%3Aissue+is%3Aopen+label%3Ahw-ledger"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"lib",
|
|
17
|
+
"README.md",
|
|
18
|
+
"CHANGELOG.md"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsc --build tsconfig.build.json",
|
|
22
|
+
"build:watch": "tsc --build tsconfig.build.json && (concurrently 'tsc --build -w tsconfig.build.json')",
|
|
23
|
+
"clean": "tsc --build --clean",
|
|
24
|
+
"test": "jest --runInBand",
|
|
25
|
+
"lint": "eslint . --ignore-path ../../.gitignore",
|
|
26
|
+
"lint:fix": "yarn lint --fix"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@ledgerhq/hw-app-btc": "^10.0.5",
|
|
30
|
+
"@ledgerhq/hw-transport": "^6.28.5",
|
|
31
|
+
"minimalistic-assert": "^1.0.1"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@exodus/hw-common": "^1.0.0",
|
|
35
|
+
"@exodus/module": "^1.2.0",
|
|
36
|
+
"@ledgerhq/hw-transport-node-hid": "^6.27.16",
|
|
37
|
+
"@ledgerhq/hw-transport-node-speculos": "^6.27.16",
|
|
38
|
+
"@types/jest-when": "^3.5.2",
|
|
39
|
+
"@types/minimalistic-assert": "^1.0.1",
|
|
40
|
+
"concurrently": "^7.6.0",
|
|
41
|
+
"eslint": "^8.33.0",
|
|
42
|
+
"jest": "^29.1.2",
|
|
43
|
+
"jest-when": "^3.5.2"
|
|
44
|
+
},
|
|
45
|
+
"gitHead": "3772b0c9e6727aa1ed14f0a3d703e93c08b65c8a"
|
|
46
|
+
}
|