diff --git a/packages/hardware-wallets/.gitignore b/packages/hardware-wallets/.gitignore new file mode 100644 index 000000000..99ffa823b --- /dev/null +++ b/packages/hardware-wallets/.gitignore @@ -0,0 +1,2 @@ +lib/bin/*.d.ts +lib.esm/bin/*.d.ts diff --git a/packages/hardware-wallets/.npmignore b/packages/hardware-wallets/.npmignore new file mode 100644 index 000000000..afda00963 --- /dev/null +++ b/packages/hardware-wallets/.npmignore @@ -0,0 +1,3 @@ +tsconfig.json +src.ts/ +tsconfig.tsbuildinfo diff --git a/packages/hardware-wallets/README.md b/packages/hardware-wallets/README.md new file mode 100644 index 000000000..eaecb8792 --- /dev/null +++ b/packages/hardware-wallets/README.md @@ -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. diff --git a/packages/hardware-wallets/package.json b/packages/hardware-wallets/package.json new file mode 100644 index 000000000..14312901c --- /dev/null +++ b/packages/hardware-wallets/package.json @@ -0,0 +1,42 @@ +{ + "author": "Richard Moore ", + "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" +} diff --git a/packages/hardware-wallets/src.ts/index.ts b/packages/hardware-wallets/src.ts/index.ts new file mode 100644 index 000000000..16a21b18d --- /dev/null +++ b/packages/hardware-wallets/src.ts/index.ts @@ -0,0 +1,7 @@ +"use strict"; + +import { LedgerSigner } from "./ledger"; + +export { + LedgerSigner +}; diff --git a/packages/hardware-wallets/src.ts/ledger-transport.ts b/packages/hardware-wallets/src.ts/ledger-transport.ts new file mode 100644 index 000000000..b1ec18f42 --- /dev/null +++ b/packages/hardware-wallets/src.ts/ledger-transport.ts @@ -0,0 +1,12 @@ +"use strict"; + +import hid from "@ledgerhq/hw-transport-node-hid"; + +export type TransportCreator = { + create: () => Promise; +}; + +export const transports: { [ name: string ]: TransportCreator } = { + "hid": hid, + "default": hid +}; diff --git a/packages/hardware-wallets/src.ts/ledger.ts b/packages/hardware-wallets/src.ts/ledger.ts new file mode 100644 index 000000000..faf58df84 --- /dev/null +++ b/packages/hardware-wallets/src.ts/ledger.ts @@ -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; + + 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 { + 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 { + 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 { + 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); + } +})(); diff --git a/packages/hardware-wallets/thirdparty.d.ts b/packages/hardware-wallets/thirdparty.d.ts new file mode 100644 index 000000000..6c6367904 --- /dev/null +++ b/packages/hardware-wallets/thirdparty.d.ts @@ -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; + getAddress(path: string): Promise; + signPersonalMessage(path: string, message: string): Promise; + signTransaction(path: string, unsignedTx: string): Promise; + } + + export default Eth; +} + +declare module "@ledgerhq/hw-transport-node-hid" { + export function create(): Promise; +} + +declare module "@ledgerhq/hw-transport-u2f" { + export function create(): Promise; +} diff --git a/packages/hardware-wallets/tsconfig.json b/packages/hardware-wallets/tsconfig.json new file mode 100644 index 000000000..0a9d2f16c --- /dev/null +++ b/packages/hardware-wallets/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.package.json", + "compilerOptions": { + "rootDir": "./src.ts", + "outDir": "./lib/" + }, + "include": [ + "./thirdparty.d.ts", + "./src.ts/*" + ], + "exclude": [] +}