Skip to content

Commit

Permalink
Initial code drop of new hardware wallet package.
Browse files Browse the repository at this point in the history
  • Loading branch information
ricmoo committed Jan 10, 2020
1 parent 381a72d commit 2e8f5ca
Show file tree
Hide file tree
Showing 9 changed files with 244 additions and 0 deletions.
2 changes: 2 additions & 0 deletions packages/hardware-wallets/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
lib/bin/*.d.ts
lib.esm/bin/*.d.ts
3 changes: 3 additions & 0 deletions packages/hardware-wallets/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
tsconfig.json
src.ts/
tsconfig.tsbuildinfo
29 changes: 29 additions & 0 deletions packages/hardware-wallets/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
Hardware Wallets
================

Thid is still very experimental.

I only have 1 ledger nano, and testing is done locally (CirlceCI doesn't have
ledgers plugged in ;)).

API
===

```
import { LedgerSigner } from "@ethersproject/hardware-wallets";
const signer = new LedgerSigner(provider, type, path);
// By default:
// - in node, type = "usb"
// - path is the default Ethereum path (i.e. `m/44'/60'/0'/0/0`)
```

License
=======

All ethers code is MIT License.

Each hardware wallet manufacturer may impose additional license
requirements so please check the related abstraction libraries
they provide.

All Firefly abstraction is also MIT Licensed.
42 changes: 42 additions & 0 deletions packages/hardware-wallets/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"author": "Richard Moore <[email protected]>",
"dependencies": {
"@ethersproject/abstract-provider": ">=5.0.0-beta.136",
"@ethersproject/abstract-signer": ">=5.0.0-beta.137",
"@ethersproject/address": ">=5.0.0-beta.134",
"@ethersproject/bytes": ">=5.0.0-beta.134",
"@ethersproject/properties": ">=5.0.0-beta.136",
"@ethersproject/strings": ">=5.0.0-beta.135",
"@ethersproject/transactions": ">=5.0.0-beta.133",
"@ledgerhq/hw-app-eth": "5.3.0",
"@ledgerhq/hw-transport": "5.3.0",
"@ledgerhq/hw-transport-node-hid": "5.3.0",
"@ledgerhq/hw-transport-u2f": "5.3.0"
},
"description": "Hardware Wallet support for ethers.",
"devDependencies": {
"@types/node": "^12.7.4"
},
"ethereum": "donations.ethers.eth",
"keywords": [
"Ethereum",
"ethers",
"cli"
],
"license": "MIT",
"main": "./lib/index.js",
"module": "./lib.esm/index.js",
"name": "@ethersproject/hardware-wallets",
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "git://github.com/ethers-io/ethers.js.git"
},
"scripts": {
"test": "exit 1"
},
"types": "./lib/index.d.ts",
"version": "5.0.0-beta.1"
}
7 changes: 7 additions & 0 deletions packages/hardware-wallets/src.ts/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"use strict";

import { LedgerSigner } from "./ledger";

export {
LedgerSigner
};
12 changes: 12 additions & 0 deletions packages/hardware-wallets/src.ts/ledger-transport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"use strict";

import hid from "@ledgerhq/hw-transport-node-hid";

export type TransportCreator = {
create: () => Promise<Transport>;
};

export const transports: { [ name: string ]: TransportCreator } = {
"hid": hid,
"default": hid
};
99 changes: 99 additions & 0 deletions packages/hardware-wallets/src.ts/ledger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
"use strict";

import { getAddress } from "@ethersproject/address";
import { Bytes, hexlify, joinSignature } from "@ethersproject/bytes";
import { Signer } from "@ethersproject/abstract-signer";
import { Provider, TransactionRequest } from "@ethersproject/abstract-provider";
import { defineReadOnly, resolveProperties } from "@ethersproject/properties";
import { toUtf8Bytes } from "@ethersproject/strings";
import { serialize as serializeTransaction } from "@ethersproject/transactions";

import Eth from "@ledgerhq/hw-app-eth";

// We store these in a separated import so it is easier to swap them out
// at bundle time; browsers do not get HID, for example. This maps a string
// "type" to a Transport with create.
import { transports } from "./ledger-transport";

const defaultPath = "m/44'/60'/0'/0/0";

export class LedgerSigner extends Signer {
readonly type: string;
readonly path: string

readonly _eth: Promise<Eth>;

constructor(provider?: Provider, type?: string, path?: string) {
super();
if (path == null) { path = defaultPath; }
if (type == null) { type = "default"; }

defineReadOnly(this, "path", path);
defineReadOnly(this, "type", type);
defineReadOnly(this, "provider", provider || null);

const transport = transports[type];
if (!transport) { throw new Error("unknown or unsupport type"); }

defineReadOnly(this, "_eth", transport.create().then((transport) => {
const eth = new Eth(transport);
return eth.getAppConfiguration().then((config) => {
return eth;
}, (error) => {
return Promise.reject(error);
});
}, (error) => {
return Promise.reject(error);
}));
}

async getAddress(): Promise<string> {
const eth = await this._eth;
if (eth == null) { throw new Error("failed to connect"); }
const o = await eth.getAddress(this.path);
return getAddress(o.address);
}

async signMessage(message: Bytes | string): Promise<string> {
if (typeof(message) === 'string') {
message = toUtf8Bytes(message);
}

const messageHex = hexlify(message).substring(2);

const eth = await this._eth;
const sig = await eth.signPersonalMessage(this.path, messageHex);
sig.r = '0x' + sig.r;
sig.s = '0x' + sig.s;
return joinSignature(sig);
}

async signTransaction(transaction: TransactionRequest): Promise<string> {
const eth = await this._eth;
return resolveProperties(transaction).then((tx) => {
const unsignedTx = serializeTransaction(tx).substring(2);
return eth.signTransaction(this.path, unsignedTx).then((sig) => {
return serializeTransaction(tx, {
v: sig.v,
r: ("0x" + sig.r),
s: ("0x" + sig.s),
});
});
});
}

connect(provider: Provider): Signer {
return new LedgerSigner(provider, this.type, this.path);
}
}

(async function() {
const signer = new LedgerSigner();
console.log(signer);
try {
const sig = await signer.signMessage("Hello World");
console.log(sig);
} catch (error) {
console.log("ERR", error);
}
})();
38 changes: 38 additions & 0 deletions packages/hardware-wallets/thirdparty.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
declare module "@ledgerhq/hw-app-eth" {
export type PublicAccount = {
publicKey: string;
address: string;
chainCode: string;
};

export type Config = {
arbitraryDataEnabled: number,
version: string
};

export type Signature = {
r: string,
s: string,
v: number
};

export class Transport { }

export class Eth {
constructor(transport: Transport);
getAppConfiguration(): Promise<Config>;
getAddress(path: string): Promise<PublicAccount>;
signPersonalMessage(path: string, message: string): Promise<Signature>;
signTransaction(path: string, unsignedTx: string): Promise<Signature>;
}

export default Eth;
}

declare module "@ledgerhq/hw-transport-node-hid" {
export function create(): Promise<Transport>;
}

declare module "@ledgerhq/hw-transport-u2f" {
export function create(): Promise<Transport>;
}
12 changes: 12 additions & 0 deletions packages/hardware-wallets/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"extends": "../../tsconfig.package.json",
"compilerOptions": {
"rootDir": "./src.ts",
"outDir": "./lib/"
},
"include": [
"./thirdparty.d.ts",
"./src.ts/*"
],
"exclude": []
}

0 comments on commit 2e8f5ca

Please sign in to comment.