Skip to content

Commit

Permalink
Browser support (with dist files) for Ledger.
Browse files Browse the repository at this point in the history
  • Loading branch information
ricmoo committed Jan 11, 2020
1 parent 4174095 commit 6f7fbf3
Show file tree
Hide file tree
Showing 5 changed files with 143 additions and 55 deletions.
14 changes: 6 additions & 8 deletions packages/hardware-wallets/package.json
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
{
"author": "Richard Moore <me@ricmoo.com>",
"browser": {
"./lib.esm/ledger-transport.js": "./lib.esm/browser-ledger-transport.js",
"ethers": "./lib.esm/browser-ethers.js"
},
"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"
"@ledgerhq/hw-transport-u2f": "5.3.0",
"ethers": "5.0.0-beta.166"
},
"description": "Hardware Wallet support for ethers.",
"devDependencies": {
Expand Down
12 changes: 12 additions & 0 deletions packages/hardware-wallets/src.ts/browser-ethers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"use strict";

let ethers: any = { };

const w = (window as any);
if (w._ethers == null) {
console.log("WARNING: @ethersproject/hardware-wallet requires ethers loaded first");
} else {
ethers = w._ethers;
}

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

import u2f from "@ledgerhq/hw-transport-u2f";

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

export const transports: { [ name: string ]: TransportCreator } = {
"u2f": u2f,
"default": u2f
};
107 changes: 60 additions & 47 deletions packages/hardware-wallets/src.ts/ledger.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
"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 { ethers } from "ethers";

import { version } from "./_version";
const logger = new ethers.utils.Logger(version);

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

Expand All @@ -17,25 +14,31 @@ import { transports } from "./ledger-transport";

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

export class LedgerSigner extends Signer {
function waiter(duration: number): Promise<void> {
return new Promise((resolve) => {
setTimeout(resolve, duration);
});
}

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

readonly _eth: Promise<Eth>;

constructor(provider?: Provider, type?: string, path?: string) {
constructor(provider?: ethers.providers.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);
ethers.utils.defineReadOnly(this, "path", path);
ethers.utils.defineReadOnly(this, "type", type);
ethers.utils.defineReadOnly(this, "provider", provider || null);

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

defineReadOnly(this, "_eth", transport.create().then((transport) => {
ethers.utils.defineReadOnly(this, "_eth", transport.create().then((transport) => {
const eth = new Eth(transport);
return eth.getAppConfiguration().then((config) => {
return eth;
Expand All @@ -47,53 +50,63 @@ export class LedgerSigner extends Signer {
}));
}

_retry<T = any>(callback: (eth: Eth) => Promise<T>, timeout?: number): Promise<T> {
return new Promise(async (resolve, reject) => {
if (timeout && timeout > 0) {
setTimeout(() => { reject(new Error("timeout")); }, timeout);
}

const eth = await this._eth;

// Wait up to 5 seconds
for (let i = 0; i < 50; i++) {
try {
const result = await callback(eth);
return resolve(result);
} catch (error) {
if (error.id !== "TransportLocked") {
return reject(error);
}
}
await waiter(100);
}

return reject(new Error("timeout"));
});
}

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);
const account = await this._retry((eth) => eth.getAddress(this.path));
return ethers.utils.getAddress(account.address);
}

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

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

const eth = await this._eth;
const sig = await eth.signPersonalMessage(this.path, messageHex);
const sig = await this._retry((eth) => eth.signPersonalMessage(this.path, messageHex));
sig.r = '0x' + sig.r;
sig.s = '0x' + sig.s;
return joinSignature(sig);
return ethers.utils.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),
});
});
async signTransaction(transaction: ethers.providers.TransactionRequest): Promise<string> {
const tx = transaction = await ethers.utils.resolveProperties(transaction);
const unsignedTx = ethers.utils.serializeTransaction(tx).substring(2);

const sig = await this._retry((eth) => eth.signTransaction(this.path, unsignedTx));

return ethers.utils.serializeTransaction(tx, {
v: sig.v,
r: ("0x" + sig.r),
s: ("0x" + sig.s),
});
}

connect(provider: Provider): Signer {
connect(provider: ethers.providers.Provider): ethers.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);
}
})();
53 changes: 53 additions & 0 deletions rollup-ancillary.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
"use strict";

import resolve from 'rollup-plugin-node-resolve';
import commonjs from 'rollup-plugin-commonjs';
import _globals from 'rollup-plugin-node-globals';

import { terser } from "rollup-plugin-terser";

function getConfig(project, minify) {

const suffix = [ "esm" ];

const plugins = [
resolve({
mainFields: [ "browser", "module", "main" ],
preferBuiltins: false
}),
commonjs({
namedExports: {
"bn.js": [ "BN" ],
"elliptic": [ "ec" ],
"scrypt-js": [ "scrypt" ],
"u2f-api": [ "isSupported", "sign" ],
"js-sha3": [ null ]
},
}),
_globals(),
];

if (minify) {
suffix.push("min");
plugins.push(terser());
}

return {
input: `packages/${ project }/lib.esm/index.js`,
output: {
file: `packages/${ project }/dist/hardware-wallets.${ suffix.join(".") }.js`,
format: "esm",
name: "_ethersAncillary",
exports: "named"
},
context: "window",
treeshake: false,
plugins: plugins
};
}

export default [
getConfig("hardware-wallets", false),
getConfig("hardware-wallets", true),
]

0 comments on commit 6f7fbf3

Please sign in to comment.