Browser support (with dist files) for Ledger.

This commit is contained in:
Richard Moore 2020-01-10 19:59:20 -05:00
parent 41740956df
commit 6f7fbf3858
No known key found for this signature in database
GPG Key ID: 665176BE8E9DC651
5 changed files with 151 additions and 63 deletions

View File

@ -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": {

View File

@ -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 }

View File

@ -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
};

View File

@ -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";
@ -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;
@ -47,53 +50,63 @@ export class LedgerSigner extends Signer {
}));
}
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);
}
_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);
}
async signMessage(message: Bytes | string): Promise<string> {
if (typeof(message) === 'string') {
message = toUtf8Bytes(message);
}
const eth = await this._eth;
const messageHex = hexlify(message).substring(2);
// 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);
}
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),
});
});
return reject(new Error("timeout"));
});
}
connect(provider: Provider): Signer {
async getAddress(): Promise<string> {
const account = await this._retry((eth) => eth.getAddress(this.path));
return ethers.utils.getAddress(account.address);
}
async signMessage(message: ethers.utils.Bytes | string): Promise<string> {
if (typeof(message) === 'string') {
message = ethers.utils.toUtf8Bytes(message);
}
const messageHex = ethers.utils.hexlify(message).substring(2);
const sig = await this._retry((eth) => eth.signPersonalMessage(this.path, messageHex));
sig.r = '0x' + sig.r;
sig.s = '0x' + sig.s;
return ethers.utils.joinSignature(sig);
}
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: 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);
}
})();

View File

@ -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),
]