@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 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,3 @@
1
+ import type Transport from '@ledgerhq/hw-transport';
2
+ import type { HardwareWalletDevice } from '@exodus/hw-common';
3
+ export default function (transport: Transport): Promise<HardwareWalletDevice>;
@@ -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,4 @@
1
+ import bitcoin from './bitcoin';
2
+ export const assetHandlerFactories = {
3
+ bitcoin,
4
+ };
@@ -0,0 +1 @@
1
+ export declare const APDU_PORT = 40000;
@@ -0,0 +1 @@
1
+ export const APDU_PORT = 40000;
@@ -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;
@@ -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
@@ -0,0 +1,2 @@
1
+ export * from './discovery';
2
+ export { default } from './discovery';
package/lib/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export * from './discovery';
2
+ export { default } from './discovery';
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
+ }