diff options
Diffstat (limited to 'packages/subproviders')
-rw-r--r-- | packages/subproviders/CHANGELOG.md | 9 | ||||
-rw-r--r-- | packages/subproviders/README.md | 42 | ||||
-rw-r--r-- | packages/subproviders/package.json | 26 | ||||
-rw-r--r-- | packages/subproviders/src/globals.d.ts | 54 | ||||
-rw-r--r-- | packages/subproviders/src/index.ts | 21 | ||||
-rw-r--r-- | packages/subproviders/src/subproviders/ledger.ts | 14 | ||||
-rw-r--r-- | packages/subproviders/src/types.ts | 10 | ||||
-rw-r--r-- | packages/subproviders/test/integration/ledger_subprovider_test.ts | 33 | ||||
-rw-r--r-- | packages/subproviders/test/unit/ledger_subprovider_test.ts | 18 |
9 files changed, 148 insertions, 79 deletions
diff --git a/packages/subproviders/CHANGELOG.md b/packages/subproviders/CHANGELOG.md index c2d590a35..8e7321d4a 100644 --- a/packages/subproviders/CHANGELOG.md +++ b/packages/subproviders/CHANGELOG.md @@ -1,5 +1,14 @@ # CHANGELOG +## v0.7.0 - _TBD_ + + * Updated legerco packages. Removed node-hid package as a dependency and make it an optional dependency. It is still used in integration tests but is causing problems for users on Linux distros. (#437) + +## v0.6.0 - _March 4, 2018_ + + * Move web3 types from being a devDep to a dep since one cannot use this package without it (#429) + * Add `numberOfAccounts` param to `LedgerSubprovider` method `getAccountsAsync` (#432) + ## v0.5.0 - _February 16, 2018_ * Add EmptyWalletSubprovider and FakeGasEstimateSubprovider (#392) diff --git a/packages/subproviders/README.md b/packages/subproviders/README.md index 954729713..4614342b2 100644 --- a/packages/subproviders/README.md +++ b/packages/subproviders/README.md @@ -10,6 +10,14 @@ We have written up a [Wiki](https://0xproject.com/wiki#Web3-Provider-Examples) a yarn add @0xproject/subproviders ``` +If your project is in [TypeScript](https://www.typescriptlang.org/), add the following to your `tsconfig.json`: + +``` +"include": [ + "./node_modules/web3-typescript-typings/index.d.ts", +] +``` + ## Usage Simply import the subprovider you are interested in using: @@ -34,6 +42,28 @@ const accounts = await ledgerSubprovider.getAccountsAsync(); A subprovider that enables your dApp to send signing requests to a user's Ledger Nano S hardware wallet. These can be requests to sign transactions or messages. +Ledger Nano (and this library) by default uses a derivation path of `44'/60'/0'`. This is different to TestRPC which by default uses `m/44'/60'/0'/0`. This is a configuration option in the Ledger Subprovider package. + +##### Ledger Nano S + Node-hid (usb) + +By default, node-hid transport support is an optional dependency. This is due to the requirement of native usb developer packages on the host system. If these aren't installed the entire `npm install` fails. We also no longer export node-hid transport client factories. To re-create this see our integration tests or follow the example below: + +```typescript +import Eth from '@ledgerhq/hw-app-eth'; +import TransportNodeHid from '@ledgerhq/hw-transport-node-hid'; +async function ledgerEthereumNodeJsClientFactoryAsync(): Promise<LedgerEthereumClient> { + const ledgerConnection = await TransportNodeHid.create(); + const ledgerEthClient = new Eth(ledgerConnection); + return ledgerEthClient; +} + +// Create a LedgerSubprovider with the node-hid transport +ledgerSubprovider = new LedgerSubprovider({ + networkId, + ledgerEthereumClientFactoryAsync: ledgerEthereumNodeJsClientFactoryAsync, +}); +``` + #### Redundant RPC subprovider A subprovider which attempts to send an RPC call to a list of RPC endpoints sequentially, until one of them returns a successful response. @@ -96,10 +126,12 @@ yarn run test:unit In order to run the integration tests, make sure you have a Ledger Nano S available. -* Plug it into your computer -* Unlock the device -* Open the on-device Ethereum app -* Make sure "browser support" is disabled +* Setup your Ledger with the development mnemonic seed: `concert load couple harbor equip island argue ramp clarify fence smart topic` +* Plug it into your computer +* Unlock the device +* Open the on-device Ethereum app +* Make sure "browser support" and "contract data" are disabled +* Start [TestRPC](https://github.com/trufflesuite/ganache-cli) locally at port `8545` Then run: @@ -107,6 +139,8 @@ Then run: yarn test:integration ``` +**Note:** We assume a derivation path of `m/44'/60'/0'/0` which is already configured in the tests. With this setup and derivation path, your first account should be `0x5409ed021d9299bf6814279a6a1411a7e866a631`, exactly like TestRPC. + #### All tests ```bash diff --git a/packages/subproviders/package.json b/packages/subproviders/package.json index 5e9c2ed89..a3e865d24 100644 --- a/packages/subproviders/package.json +++ b/packages/subproviders/package.json @@ -1,6 +1,6 @@ { "name": "@0xproject/subproviders", - "version": "0.5.0", + "version": "0.6.0", "main": "lib/src/index.js", "types": "lib/src/index.d.ts", "license": "Apache-2.0", @@ -18,31 +18,33 @@ "test:integration": "run-s clean build run_mocha_integration" }, "dependencies": { - "@0xproject/assert": "^0.0.20", - "@0xproject/types": "^0.2.3", - "@0xproject/utils": "^0.3.4", + "@0xproject/assert": "^0.1.0", + "@0xproject/types": "^0.3.0", + "@0xproject/utils": "^0.4.0", + "@ledgerhq/hw-app-eth": "^4.3.0", + "@ledgerhq/hw-transport-u2f": "^4.3.0", "bn.js": "^4.11.8", "es6-promisify": "^5.0.0", "ethereumjs-tx": "^1.3.3", "ethereumjs-util": "^5.1.1", "hdkey": "^0.7.1", - "ledgerco": "0xProject/ledger-node-js-api", "lodash": "^4.17.4", "semaphore-async-await": "^1.5.1", "web3": "^0.20.0", - "web3-provider-engine": "^13.0.1" + "web3-provider-engine": "^13.0.1", + "web3-typescript-typings": "^0.10.0" }, "devDependencies": { - "@0xproject/tslint-config": "^0.4.9", - "@0xproject/utils": "^0.3.4", + "@0xproject/tslint-config": "^0.4.10", + "@0xproject/utils": "^0.4.0", "@types/lodash": "^4.14.86", "@types/mocha": "^2.2.42", "@types/node": "^8.0.53", "awesome-typescript-loader": "^3.1.3", "chai": "^4.0.1", "chai-as-promised": "^7.1.0", - "chai-as-promised-typescript-typings": "^0.0.9", - "chai-typescript-typings": "^0.0.3", + "chai-as-promised-typescript-typings": "^0.0.10", + "chai-typescript-typings": "^0.0.4", "dirty-chai": "^2.0.1", "mocha": "^4.0.1", "npm-run-all": "^4.1.2", @@ -51,7 +53,9 @@ "types-bn": "^0.0.1", "types-ethereumjs-util": "0xProject/types-ethereumjs-util", "typescript": "2.7.1", - "web3-typescript-typings": "^0.9.11", "webpack": "^3.1.0" + }, + "optionalDependencies": { + "@ledgerhq/hw-transport-node-hid": "^4.3.0" } } diff --git a/packages/subproviders/src/globals.d.ts b/packages/subproviders/src/globals.d.ts index 6f344dcd3..d59ee9e67 100644 --- a/packages/subproviders/src/globals.d.ts +++ b/packages/subproviders/src/globals.d.ts @@ -32,32 +32,38 @@ interface ECSignature { r: string; s: string; } -declare module 'ledgerco' { - interface comm { - close_async(): Promise<void>; - } - export class comm_node implements comm { - public static create_async(timeoutMilliseconds?: number): Promise<comm_node>; - public close_async(): Promise<void>; - } - export class comm_u2f implements comm { - public static create_async(): Promise<comm_u2f>; - public close_async(): Promise<void>; - } - export class eth { - public comm: comm; - constructor(comm: comm); - public getAddress_async( + +interface LedgerTransport { + close(): Promise<void>; +} + +declare module '@ledgerhq/hw-app-eth' { + class Eth { + public transport: LedgerTransport; + constructor(transport: LedgerTransport); + public getAddress( path: string, - display?: boolean, - chaincode?: boolean, + boolDisplay?: boolean, + boolChaincode?: boolean, ): Promise<{ publicKey: string; address: string; chainCode: string }>; - public signTransaction_async(path: string, rawTxHex: string): Promise<ECSignatureString>; - public getAppConfiguration_async(): Promise<{ - arbitraryDataEnabled: number; - version: string; - }>; - public signPersonalMessage_async(path: string, messageHex: string): Promise<ECSignature>; + public signTransaction(path: string, rawTxHex: string): Promise<ECSignatureString>; + public getAppConfiguration(): Promise<{ arbitraryDataEnabled: number; version: string }>; + public signPersonalMessage(path: string, messageHex: string): Promise<ECSignature>; + } + export default Eth; +} + +declare module '@ledgerhq/hw-transport-u2f' { + export default class TransportU2F implements LedgerTransport { + public static create(): Promise<LedgerTransport>; + public close(): Promise<void>; + } +} + +declare module '@ledgerhq/hw-transport-node-hid' { + export default class TransportNodeHid implements LedgerTransport { + public static create(): Promise<LedgerTransport>; + public close(): Promise<void>; } } diff --git a/packages/subproviders/src/index.ts b/packages/subproviders/src/index.ts index 4da405ec0..e22b6f5f3 100644 --- a/packages/subproviders/src/index.ts +++ b/packages/subproviders/src/index.ts @@ -1,8 +1,5 @@ -import { - comm_node as LedgerNodeCommunication, - comm_u2f as LedgerBrowserCommunication, - eth as LedgerEthereumClientFn, -} from 'ledgerco'; +import Eth from '@ledgerhq/hw-app-eth'; +import TransportU2F from '@ledgerhq/hw-transport-u2f'; import { LedgerEthereumClient } from './types'; @@ -19,17 +16,7 @@ export { ECSignature, LedgerWalletSubprovider, LedgerCommunicationClient, NonceS * @return LedgerEthereumClient A browser client */ export async function ledgerEthereumBrowserClientFactoryAsync(): Promise<LedgerEthereumClient> { - const ledgerConnection = await LedgerBrowserCommunication.create_async(); - const ledgerEthClient = new LedgerEthereumClientFn(ledgerConnection); - return ledgerEthClient; -} - -/** - * A factory for creating a LedgerEthereumClient usable in a Node.js context. - * @return LedgerEthereumClient A Node.js client - */ -export async function ledgerEthereumNodeJsClientFactoryAsync(): Promise<LedgerEthereumClient> { - const ledgerConnection = await LedgerNodeCommunication.create_async(); - const ledgerEthClient = new LedgerEthereumClientFn(ledgerConnection); + const ledgerConnection = await TransportU2F.create(); + const ledgerEthClient = new Eth(ledgerConnection); return ledgerEthClient; } diff --git a/packages/subproviders/src/subproviders/ledger.ts b/packages/subproviders/src/subproviders/ledger.ts index 5966a88bb..0a84caae3 100644 --- a/packages/subproviders/src/subproviders/ledger.ts +++ b/packages/subproviders/src/subproviders/ledger.ts @@ -19,7 +19,7 @@ import { import { Subprovider } from './subprovider'; const DEFAULT_DERIVATION_PATH = `44'/60'/0'`; -const NUM_ADDRESSES_TO_FETCH = 10; +const DEFAULT_NUM_ADDRESSES_TO_FETCH = 10; const ASK_FOR_ON_DEVICE_CONFIRMATION = false; const SHOULD_GET_CHAIN_CODE = true; @@ -129,12 +129,12 @@ export class LedgerSubprovider extends Subprovider { return; } } - public async getAccountsAsync(): Promise<string[]> { + public async getAccountsAsync(numberOfAccounts: number = DEFAULT_NUM_ADDRESSES_TO_FETCH): Promise<string[]> { this._ledgerClientIfExists = await this._createLedgerClientAsync(); let ledgerResponse; try { - ledgerResponse = await this._ledgerClientIfExists.getAddress_async( + ledgerResponse = await this._ledgerClientIfExists.getAddress( this._derivationPath, this._shouldAlwaysAskForConfirmation, SHOULD_GET_CHAIN_CODE, @@ -148,7 +148,7 @@ export class LedgerSubprovider extends Subprovider { hdKey.chainCode = new Buffer(ledgerResponse.chainCode, 'hex'); const accounts = []; - for (let i = 0; i < NUM_ADDRESSES_TO_FETCH; i++) { + for (let i = 0; i < numberOfAccounts; i++) { const derivedHDNode = hdKey.derive(`m/${i + this._derivationPathIndex}`); const derivedPublicKey = derivedHDNode.publicKey; const shouldSanitizePublicKey = true; @@ -173,7 +173,7 @@ export class LedgerSubprovider extends Subprovider { const txHex = tx.serialize().toString('hex'); try { const derivationPath = this._getDerivationPath(); - const result = await this._ledgerClientIfExists.signTransaction_async(derivationPath, txHex); + const result = await this._ledgerClientIfExists.signTransaction(derivationPath, txHex); // Store signature in transaction tx.r = Buffer.from(result.r, 'hex'); tx.s = Buffer.from(result.s, 'hex'); @@ -199,7 +199,7 @@ export class LedgerSubprovider extends Subprovider { this._ledgerClientIfExists = await this._createLedgerClientAsync(); try { const derivationPath = this._getDerivationPath(); - const result = await this._ledgerClientIfExists.signPersonalMessage_async( + const result = await this._ledgerClientIfExists.signPersonalMessage( derivationPath, ethUtil.stripHexPrefix(data), ); @@ -236,7 +236,7 @@ export class LedgerSubprovider extends Subprovider { this._connectionLock.signal(); return; } - await this._ledgerClientIfExists.comm.close_async(); + await this._ledgerClientIfExists.transport.close(); this._ledgerClientIfExists = undefined; this._connectionLock.signal(); } diff --git a/packages/subproviders/src/types.ts b/packages/subproviders/src/types.ts index 65b7f6c8f..f49ac6107 100644 --- a/packages/subproviders/src/types.ts +++ b/packages/subproviders/src/types.ts @@ -1,7 +1,7 @@ import * as _ from 'lodash'; export interface LedgerCommunicationClient { - close_async: () => Promise<void>; + close: () => Promise<void>; } /* @@ -12,14 +12,14 @@ export interface LedgerCommunicationClient { export interface LedgerEthereumClient { // shouldGetChainCode is defined as `true` instead of `boolean` because other types rely on the assumption // that we get back the chain code and we don't have dependent types to express it properly - getAddress_async: ( + getAddress: ( derivationPath: string, askForDeviceConfirmation: boolean, shouldGetChainCode: true, ) => Promise<LedgerGetAddressResult>; - signPersonalMessage_async: (derivationPath: string, messageHex: string) => Promise<ECSignature>; - signTransaction_async: (derivationPath: string, txHex: string) => Promise<ECSignatureString>; - comm: LedgerCommunicationClient; + signTransaction: (derivationPath: string, rawTxHex: string) => Promise<ECSignatureString>; + signPersonalMessage: (derivationPath: string, messageHex: string) => Promise<ECSignature>; + transport: LedgerCommunicationClient; } export interface ECSignatureString { diff --git a/packages/subproviders/test/integration/ledger_subprovider_test.ts b/packages/subproviders/test/integration/ledger_subprovider_test.ts index 628b532d7..a94cfbe3a 100644 --- a/packages/subproviders/test/integration/ledger_subprovider_test.ts +++ b/packages/subproviders/test/integration/ledger_subprovider_test.ts @@ -1,3 +1,7 @@ +import Eth from '@ledgerhq/hw-app-eth'; +// HACK: This depdency is optional and tslint skips optional depdencies +// tslint:disable-next-line:no-implicit-dependencies +import TransportNodeHid from '@ledgerhq/hw-transport-node-hid'; import * as chai from 'chai'; import promisify = require('es6-promisify'); import * as ethUtils from 'ethereumjs-util'; @@ -6,14 +10,21 @@ import Web3 = require('web3'); import Web3ProviderEngine = require('web3-provider-engine'); import RpcSubprovider = require('web3-provider-engine/subproviders/rpc'); -import { ledgerEthereumNodeJsClientFactoryAsync, LedgerSubprovider } from '../../src'; -import { DoneCallback } from '../../src/types'; +import { LedgerSubprovider } from '../../src'; +import { DoneCallback, LedgerEthereumClient } from '../../src/types'; import { chaiSetup } from '../chai_setup'; import { reportCallbackErrors } from '../utils/report_callback_errors'; chaiSetup.configure(); const expect = chai.expect; +async function ledgerEthereumNodeJsClientFactoryAsync(): Promise<LedgerEthereumClient> { + const ledgerConnection = await TransportNodeHid.create(); + const ledgerEthClient = new Eth(ledgerConnection); + return ledgerEthClient; +} + +const TESTRPC_DERIVATION_PATH = `m/44'/60'/0'/0`; const TEST_RPC_ACCOUNT_0 = '0x5409ed021d9299bf6814279a6a1411a7e866a631'; describe('LedgerSubprovider', () => { @@ -23,14 +34,25 @@ describe('LedgerSubprovider', () => { ledgerSubprovider = new LedgerSubprovider({ networkId, ledgerEthereumClientFactoryAsync: ledgerEthereumNodeJsClientFactoryAsync, + derivationPath: TESTRPC_DERIVATION_PATH, }); }); describe('direct method calls', () => { - it('returns a list of accounts', async () => { + it('returns default number of accounts', async () => { const accounts = await ledgerSubprovider.getAccountsAsync(); expect(accounts[0]).to.not.be.an('undefined'); expect(accounts.length).to.be.equal(10); }); + it('returns the expected first account from a ledger set up with the test mnemonic', async () => { + const accounts = await ledgerSubprovider.getAccountsAsync(); + expect(accounts[0]).to.be.equal(TEST_RPC_ACCOUNT_0); + }); + it('returns requested number of accounts', async () => { + const numberOfAccounts = 20; + const accounts = await ledgerSubprovider.getAccountsAsync(numberOfAccounts); + expect(accounts[0]).to.not.be.an('undefined'); + expect(accounts.length).to.be.equal(numberOfAccounts); + }); it('signs a personal message', async () => { const data = ethUtils.bufferToHex(ethUtils.toBuffer('hello world')); const ecSignatureHex = await ledgerSubprovider.signPersonalMessageAsync(data); @@ -44,10 +66,11 @@ describe('LedgerSubprovider', () => { to: '0x0000000000000000000000000000000000000000', value: '0x00', chainId: 3, + from: TEST_RPC_ACCOUNT_0, }; const txHex = await ledgerSubprovider.signTransactionAsync(tx); expect(txHex).to.be.equal( - '0xf85f8080822710940000000000000000000000000000000000000000808077a088a95ef1378487bc82be558e82c8478baf840c545d5b887536bb1da63673a98ba0019f4a4b9a107d1e6752bf7f701e275f28c13791d6e76af895b07373462cefaa', + '0xf85f8080822710940000000000000000000000000000000000000000808078a0712854c73c69445cc1b22a7c3d7312ff9a97fe4ffba35fd636e8236b211b6e7ca0647cee031615e52d916c7c707025bc64ad525d8f1b9876c3435a863b42743178', ); }); }); @@ -172,7 +195,7 @@ describe('LedgerSubprovider', () => { }; const callback = reportCallbackErrors(done)((err: Error, response: Web3.JSONRPCResponsePayload) => { expect(err).to.be.a('null'); - const result = response.result.result; + const result = response.result; expect(result.length).to.be.equal(66); expect(result.substr(0, 2)).to.be.equal('0x'); done(); diff --git a/packages/subproviders/test/unit/ledger_subprovider_test.ts b/packages/subproviders/test/unit/ledger_subprovider_test.ts index 1c70dd3a6..4c0803a29 100644 --- a/packages/subproviders/test/unit/ledger_subprovider_test.ts +++ b/packages/subproviders/test/unit/ledger_subprovider_test.ts @@ -21,7 +21,7 @@ describe('LedgerSubprovider', () => { const ledgerEthereumClientFactoryAsync = async () => { // tslint:disable:no-object-literal-type-assertion const ledgerEthClient = { - getAddress_async: async () => { + getAddress: async () => { const publicKey = '04f428290f4c5ed6a198f71b8205f488141dbb3f0840c923bbfa798ecbee6370986c03b5575d94d506772fb48a6a44e345e4ebd4f028a6f609c44b655d6d3e71a1'; const chainCode = 'ac055a5537c0c7e9e02d14a197cad6b857836da2a12043b46912a37d959b5ae8'; @@ -32,7 +32,7 @@ describe('LedgerSubprovider', () => { chainCode, }; }, - signPersonalMessage_async: async () => { + signPersonalMessage: async () => { const ecSignature = { v: 28, r: 'a6cc284bff14b42bdf5e9286730c152be91719d478605ec46b3bebcd0ae49148', @@ -40,7 +40,7 @@ describe('LedgerSubprovider', () => { }; return ecSignature; }, - signTransaction_async: async (derivationPath: string, txHex: string) => { + signTransaction: async (derivationPath: string, txHex: string) => { const ecSignature = { v: '77', r: '88a95ef1378487bc82be558e82c8478baf840c545d5b887536bb1da63673a98b', @@ -48,8 +48,8 @@ describe('LedgerSubprovider', () => { }; return ecSignature; }, - comm: { - close_async: _.noop, + transport: { + close: _.noop, } as LedgerCommunicationClient, }; // tslint:enable:no-object-literal-type-assertion @@ -62,11 +62,17 @@ describe('LedgerSubprovider', () => { }); describe('direct method calls', () => { describe('success cases', () => { - it('returns a list of accounts', async () => { + it('returns default number of accounts', async () => { const accounts = await ledgerSubprovider.getAccountsAsync(); expect(accounts[0]).to.be.equal(FAKE_ADDRESS); expect(accounts.length).to.be.equal(10); }); + it('returns requested number of accounts', async () => { + const numberOfAccounts = 20; + const accounts = await ledgerSubprovider.getAccountsAsync(numberOfAccounts); + expect(accounts[0]).to.be.equal(FAKE_ADDRESS); + expect(accounts.length).to.be.equal(numberOfAccounts); + }); it('signs a personal message', async () => { const data = ethUtils.bufferToHex(ethUtils.toBuffer('hello world')); const ecSignatureHex = await ledgerSubprovider.signPersonalMessageAsync(data); |