Merge pull request #252 from 0xProject/feature/addSubproviders

Add Subproviders Subpackage
This commit is contained in:
Fabio Berger
2017-12-08 11:22:25 -06:00
committed by GitHub
25 changed files with 1270 additions and 233 deletions

View File

@@ -16,6 +16,7 @@
"lerna": "^2.5.1",
"async-child-process": "^1.1.1",
"semver-sort": "^0.0.4",
"publish-release": "0xproject/publish-release"
"publish-release": "0xproject/publish-release",
"ethereumjs-testrpc": "6.0.3"
}
}

View File

@@ -65,7 +65,6 @@
"copyfiles": "^1.2.0",
"coveralls": "^3.0.0",
"dirty-chai": "^2.0.1",
"ethereumjs-testrpc": "6.0.3",
"json-loader": "^0.5.4",
"mocha": "^4.0.1",
"npm-run-all": "^4.1.2",

View File

@@ -22,7 +22,7 @@
},
"homepage": "https://github.com/0xProject/0x.js/packages/abi-gen/README.md",
"dependencies": {
"bignumber.js": "^5.0.0",
"bignumber.js": "~4.1.0",
"chalk": "^2.3.0",
"glob": "^7.1.2",
"handlebars": "^4.0.11",
@@ -39,7 +39,7 @@
"@types/mkdirp": "^0.5.1",
"@types/node": "^8.0.53",
"@types/yargs": "^8.0.2",
"npm-run-all": "^4.1.1",
"npm-run-all": "^4.1.2",
"shx": "^0.2.2",
"tslint": "5.8.0",
"typescript": "~2.6.1",

View File

@@ -0,0 +1,39 @@
Subproviders
-----------
A few useful subproviders.
## Installation
```
npm install @0xproject/subproviders --save
```
## Subproviders
#### Ledger Nano S subprovider
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.
#### 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.
#### Injected Web3 subprovider
A subprovider that relays all signing related requests to a particular provider (in our case the provider injected onto the web page), while sending all other requests to a different provider (perhaps your own backing Ethereum node or Infura).
### Integration tests
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
Then run:
```
yarn test:integration
```

View File

@@ -0,0 +1,53 @@
{
"name": "@0xproject/subproviders",
"version": "0.0.1",
"main": "lib/src/index.js",
"types": "lib/src/index.d.ts",
"license": "Apache-2.0",
"scripts": {
"clean": "shx rm -rf lib",
"build": "tsc",
"lint": "tslint --project . 'src/**/*.ts' 'test/**/*.ts'",
"run_mocha_unit": "mocha lib/test/unit/**/*_test.js --timeout 10000 --bail --exit",
"run_mocha_integration": "mocha lib/test/integration/**/*_test.js --timeout 10000 --bail --exit",
"test": "npm run test:unit",
"test:circleci": "npm run test:unit",
"test:all": "run-s test:unit test:integration",
"test:unit": "run-s clean build run_mocha_unit",
"test:integration": "run-s clean build run_mocha_integration"
},
"dependencies": {
"@0xproject/assert": "^0.0.6",
"bn.js": "^4.11.8",
"es6-promisify": "^5.0.0",
"ethereum-address": "^0.0.4",
"ethereumjs-tx": "^1.3.3",
"ethereumjs-util": "^5.1.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"
},
"devDependencies": {
"@0xproject/tslint-config": "^0.2.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.3",
"chai-typescript-typings": "^0.0.1",
"dirty-chai": "^2.0.1",
"mocha": "^4.0.1",
"npm-run-all": "^4.1.2",
"shx": "^0.2.2",
"tslint": "5.8.0",
"types-bn": "^0.0.1",
"types-ethereumjs-util": "0xproject/types-ethereumjs-util",
"typescript": "~2.6.1",
"web3-typescript-typings": "^0.7.2",
"webpack": "^3.1.0"
}
}

97
packages/subproviders/src/globals.d.ts vendored Normal file
View File

@@ -0,0 +1,97 @@
/// <reference types='chai-typescript-typings' />
/// <reference types='chai-as-promised-typescript-typings' />
declare module 'dirty-chai';
declare module 'es6-promisify';
// tslint:disable:max-classes-per-file
// tslint:disable:class-name
// tslint:disable:completed-docs
// Ethereumjs-tx declarations
declare module 'ethereumjs-tx' {
class EthereumTx {
public raw: Buffer[];
public r: Buffer;
public s: Buffer;
public v: Buffer;
public serialize(): Buffer;
constructor(txParams: any);
}
export = EthereumTx;
}
// Ledgerco declarations
interface ECSignatureString {
v: string;
r: string;
s: string;
}
interface ECSignature {
v: number;
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(path: string, display?: boolean, chaincode?: boolean):
Promise<{publicKey: string; address: 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>;
}
}
// ethereum-address declarations
declare module 'ethereum-address' {
export const isAddress: (address: string) => boolean;
}
// Semaphore-async-await declarations
declare module 'semaphore-async-await' {
class Semaphore {
constructor(permits: number);
public wait(): Promise<void>;
public signal(): void;
}
export default Semaphore;
}
// web3-provider-engine declarations
declare module 'web3-provider-engine/subproviders/subprovider' {
class Subprovider {}
export = Subprovider;
}
declare module 'web3-provider-engine/subproviders/rpc' {
import * as Web3 from 'web3';
class RpcSubprovider {
constructor(options: {rpcUrl: string});
public handleRequest(
payload: Web3.JSONRPCRequestPayload, next: () => void, end: (err: Error|null, data?: any) => void,
): void;
}
export = RpcSubprovider;
}
declare module 'web3-provider-engine' {
class Web3ProviderEngine {
public on(event: string, handler: () => void): void;
public send(payload: any): void;
public sendAsync(payload: any, callback: (error: any, response: any) => void): void;
public addProvider(provider: any): void;
public start(): void;
public stop(): void;
}
export = Web3ProviderEngine;
}

View File

@@ -0,0 +1,38 @@
import {
comm_node as LedgerNodeCommunication,
comm_u2f as LedgerBrowserCommunication,
eth as LedgerEthereumClientFn,
} from 'ledgerco';
import {LedgerEthereumClient} from './types';
export {InjectedWeb3Subprovider} from './subproviders/injected_web3';
export {RedundantRPCSubprovider} from './subproviders/redundant_rpc';
export {
LedgerSubprovider,
} from './subproviders/ledger';
export {
ECSignature,
LedgerWalletSubprovider,
LedgerCommunicationClient,
} from './types';
/**
* A factory method for creating a LedgerEthereumClient usable in a browser context.
* @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);
return ledgerEthClient;
}

View File

@@ -1,6 +1,6 @@
import * as _ from 'lodash';
import {constants} from 'ts/utils/constants';
import Web3 = require('web3');
import Web3ProviderEngine = require('web3-provider-engine');
/*
* This class implements the web3-provider-engine subprovider interface and forwards
@@ -8,12 +8,14 @@ import Web3 = require('web3');
* web3 instance in their browser.
* Source: https://github.com/MetaMask/provider-engine/blob/master/subproviders/subprovider.js
*/
export class InjectedWeb3SubProvider {
export class InjectedWeb3Subprovider {
private injectedWeb3: Web3;
constructor(injectedWeb3: Web3) {
this.injectedWeb3 = injectedWeb3;
}
public handleRequest(payload: any, next: () => void, end: (err: Error, result: any) => void) {
public handleRequest(
payload: Web3.JSONRPCRequestPayload, next: () => void, end: (err: Error|null, result: any) => void,
) {
switch (payload.method) {
case 'web3_clientVersion':
this.injectedWeb3.version.getNode(end);
@@ -39,7 +41,7 @@ export class InjectedWeb3SubProvider {
}
// Required to implement this method despite not needing it for this subprovider
// tslint:disable-next-line:prefer-function-over-method
public setEngine(engine: any) {
public setEngine(engine: Web3ProviderEngine) {
// noop
}
}

View File

@@ -0,0 +1,306 @@
import {assert} from '@0xproject/assert';
import promisify = require('es6-promisify');
import {isAddress} from 'ethereum-address';
import EthereumTx = require('ethereumjs-tx');
import ethUtil = require('ethereumjs-util');
import * as ledger from 'ledgerco';
import * as _ from 'lodash';
import Semaphore from 'semaphore-async-await';
import Web3 = require('web3');
import {
LedgerEthereumClient,
LedgerEthereumClientFactoryAsync,
LedgerSubproviderConfigs,
LedgerSubproviderErrors,
PartialTxParams,
ResponseWithTxParams,
} from '../types';
import {Subprovider} from './subprovider';
const DEFAULT_DERIVATION_PATH = `44'/60'/0'`;
const NUM_ADDRESSES_TO_FETCH = 10;
const ASK_FOR_ON_DEVICE_CONFIRMATION = false;
const SHOULD_GET_CHAIN_CODE = false;
const HEX_REGEX = /^[0-9A-Fa-f]+$/g;
export class LedgerSubprovider extends Subprovider {
private _nonceLock: Semaphore;
private _connectionLock: Semaphore;
private _networkId: number;
private _derivationPath: string;
private _derivationPathIndex: number;
private _ledgerEthereumClientFactoryAsync: LedgerEthereumClientFactoryAsync;
private _ledgerClientIfExists?: LedgerEthereumClient;
private _shouldAlwaysAskForConfirmation: boolean;
private static isValidHex(data: string) {
if (!_.isString(data)) {
return false;
}
const isHexPrefixed = data.slice(0, 2) === '0x';
if (!isHexPrefixed) {
return false;
}
const nonPrefixed = data.slice(2);
const isValid = nonPrefixed.match(HEX_REGEX);
return isValid;
}
private static validateSender(sender: string) {
if (_.isUndefined(sender) || !isAddress(sender)) {
throw new Error(LedgerSubproviderErrors.SenderInvalidOrNotSupplied);
}
}
constructor(config: LedgerSubproviderConfigs) {
super();
this._nonceLock = new Semaphore(1);
this._connectionLock = new Semaphore(1);
this._networkId = config.networkId;
this._ledgerEthereumClientFactoryAsync = config.ledgerEthereumClientFactoryAsync;
this._derivationPath = config.derivationPath || DEFAULT_DERIVATION_PATH;
this._shouldAlwaysAskForConfirmation = !_.isUndefined(config.accountFetchingConfigs) &&
!_.isUndefined(
config.accountFetchingConfigs.shouldAskForOnDeviceConfirmation,
) ?
config.accountFetchingConfigs.shouldAskForOnDeviceConfirmation :
ASK_FOR_ON_DEVICE_CONFIRMATION;
this._derivationPathIndex = 0;
}
public getPath(): string {
return this._derivationPath;
}
public setPath(derivationPath: string) {
this._derivationPath = derivationPath;
}
public setPathIndex(pathIndex: number) {
this._derivationPathIndex = pathIndex;
}
public async handleRequest(
payload: Web3.JSONRPCRequestPayload, next: () => void, end: (err: Error|null, result?: any) => void,
) {
let accounts;
let txParams;
switch (payload.method) {
case 'eth_coinbase':
try {
accounts = await this.getAccountsAsync();
end(null, accounts[0]);
} catch (err) {
end(err);
}
return;
case 'eth_accounts':
try {
accounts = await this.getAccountsAsync();
end(null, accounts);
} catch (err) {
end(err);
}
return;
case 'eth_sendTransaction':
txParams = payload.params[0];
try {
LedgerSubprovider.validateSender(txParams.from);
const result = await this.sendTransactionAsync(txParams);
end(null, result);
} catch (err) {
end(err);
}
return;
case 'eth_signTransaction':
txParams = payload.params[0];
try {
const result = await this.signTransactionWithoutSendingAsync(txParams);
end(null, result);
} catch (err) {
end(err);
}
return;
case 'personal_sign':
const data = payload.params[0];
try {
if (_.isUndefined(data)) {
throw new Error(LedgerSubproviderErrors.DataMissingForSignPersonalMessage);
}
assert.isHexString('data', data);
const ecSignatureHex = await this.signPersonalMessageAsync(data);
end(null, ecSignatureHex);
} catch (err) {
end(err);
}
return;
default:
next();
return;
}
}
public async getAccountsAsync(): Promise<string[]> {
this._ledgerClientIfExists = await this.createLedgerClientAsync();
// TODO: replace with generating addresses without hitting Ledger
const accounts = [];
for (let i = 0; i < NUM_ADDRESSES_TO_FETCH; i++) {
try {
const derivationPath = `${this._derivationPath}/${i + this._derivationPathIndex}`;
const result = await this._ledgerClientIfExists.getAddress_async(
derivationPath, this._shouldAlwaysAskForConfirmation, SHOULD_GET_CHAIN_CODE,
);
accounts.push(result.address.toLowerCase());
} catch (err) {
await this.destoryLedgerClientAsync();
throw err;
}
}
await this.destoryLedgerClientAsync();
return accounts;
}
public async signTransactionAsync(txParams: PartialTxParams): Promise<string> {
this._ledgerClientIfExists = await this.createLedgerClientAsync();
const tx = new EthereumTx(txParams);
// Set the EIP155 bits
tx.raw[6] = Buffer.from([this._networkId]); // v
tx.raw[7] = Buffer.from([]); // r
tx.raw[8] = Buffer.from([]); // s
const txHex = tx.serialize().toString('hex');
try {
const derivationPath = this.getDerivationPath();
const result = await this._ledgerClientIfExists.signTransaction_async(derivationPath, txHex);
// Store signature in transaction
tx.r = Buffer.from(result.r, 'hex');
tx.s = Buffer.from(result.s, 'hex');
tx.v = Buffer.from(result.v, 'hex');
// EIP155: v should be chain_id * 2 + {35, 36}
const signedChainId = Math.floor((tx.v[0] - 35) / 2);
if (signedChainId !== this._networkId) {
await this.destoryLedgerClientAsync();
const err = new Error(LedgerSubproviderErrors.TooOldLedgerFirmware);
throw err;
}
const signedTxHex = `0x${tx.serialize().toString('hex')}`;
await this.destoryLedgerClientAsync();
return signedTxHex;
} catch (err) {
await this.destoryLedgerClientAsync();
throw err;
}
}
public async signPersonalMessageAsync(data: string): Promise<string> {
this._ledgerClientIfExists = await this.createLedgerClientAsync();
try {
const derivationPath = this.getDerivationPath();
const result = await this._ledgerClientIfExists.signPersonalMessage_async(
derivationPath, ethUtil.stripHexPrefix(data));
const v = result.v - 27;
let vHex = v.toString(16);
if (vHex.length < 2) {
vHex = `0${v}`;
}
const signature = `0x${result.r}${result.s}${vHex}`;
await this.destoryLedgerClientAsync();
return signature;
} catch (err) {
await this.destoryLedgerClientAsync();
throw err;
}
}
private getDerivationPath() {
const derivationPath = `${this.getPath()}/${this._derivationPathIndex}`;
return derivationPath;
}
private async createLedgerClientAsync(): Promise<LedgerEthereumClient> {
await this._connectionLock.wait();
if (!_.isUndefined(this._ledgerClientIfExists)) {
this._connectionLock.signal();
throw new Error(LedgerSubproviderErrors.MultipleOpenConnectionsDisallowed);
}
const ledgerEthereumClient = await this._ledgerEthereumClientFactoryAsync();
this._connectionLock.signal();
return ledgerEthereumClient;
}
private async destoryLedgerClientAsync() {
await this._connectionLock.wait();
if (_.isUndefined(this._ledgerClientIfExists)) {
this._connectionLock.signal();
return;
}
await this._ledgerClientIfExists.comm.close_async();
this._ledgerClientIfExists = undefined;
this._connectionLock.signal();
}
private async sendTransactionAsync(txParams: PartialTxParams): Promise<Web3.JSONRPCResponsePayload> {
await this._nonceLock.wait();
try {
// fill in the extras
const filledParams = await this.populateMissingTxParamsAsync(txParams);
// sign it
const signedTx = await this.signTransactionAsync(filledParams);
// emit a submit
const payload = {
method: 'eth_sendRawTransaction',
params: [signedTx],
};
const result = await this.emitPayloadAsync(payload);
this._nonceLock.signal();
return result;
} catch (err) {
this._nonceLock.signal();
throw err;
}
}
private async signTransactionWithoutSendingAsync(txParams: PartialTxParams): Promise<ResponseWithTxParams> {
await this._nonceLock.wait();
try {
// fill in the extras
const filledParams = await this.populateMissingTxParamsAsync(txParams);
// sign it
const signedTx = await this.signTransactionAsync(filledParams);
this._nonceLock.signal();
const result = {
raw: signedTx,
tx: txParams,
};
return result;
} catch (err) {
this._nonceLock.signal();
throw err;
}
}
private async populateMissingTxParamsAsync(txParams: PartialTxParams): Promise<PartialTxParams> {
if (_.isUndefined(txParams.gasPrice)) {
const gasPriceResult = await this.emitPayloadAsync({
method: 'eth_gasPrice',
params: [],
});
const gasPrice = gasPriceResult.result.toString();
txParams.gasPrice = gasPrice;
}
if (_.isUndefined(txParams.nonce)) {
const nonceResult = await this.emitPayloadAsync({
method: 'eth_getTransactionCount',
params: [txParams.from, 'pending'],
});
const nonce = nonceResult.result;
txParams.nonce = nonce;
}
if (_.isUndefined(txParams.gas)) {
const gasResult = await this.emitPayloadAsync({
method: 'eth_estimateGas',
params: [txParams],
});
const gas = gasResult.result.toString();
txParams.gas = gas;
}
return txParams;
}
}

View File

@@ -1,15 +1,17 @@
import {promisify} from '@0xproject/utils';
import * as _ from 'lodash';
import {JSONRPCPayload} from 'ts/types';
import RpcSubprovider = require('web3-provider-engine/subproviders/rpc');
import Subprovider = require('web3-provider-engine/subproviders/subprovider');
import {JSONRPCPayload} from '../types';
import {Subprovider} from './subprovider';
export class RedundantRPCSubprovider extends Subprovider {
private rpcs: RpcSubprovider[];
private static async firstSuccessAsync(
rpcs: RpcSubprovider[], payload: JSONRPCPayload, next: () => void,
): Promise<any> {
let lastErr;
let lastErr: Error|undefined;
for (const rpc of rpcs) {
try {
const data = await promisify(rpc.handleRequest.bind(rpc))(payload, next);
@@ -19,7 +21,9 @@ export class RedundantRPCSubprovider extends Subprovider {
continue;
}
}
throw Error(lastErr);
if (!_.isUndefined(lastErr)) {
throw lastErr;
}
}
constructor(endpoints: string[]) {
super();
@@ -30,7 +34,7 @@ export class RedundantRPCSubprovider extends Subprovider {
});
}
public async handleRequest(payload: JSONRPCPayload, next: () => void,
end: (err?: Error, data?: any) => void): Promise<void> {
end: (err: Error|null, data?: any) => void): Promise<void> {
const rpcsCopy = this.rpcs.slice();
try {
const data = await RedundantRPCSubprovider.firstSuccessAsync(rpcsCopy, payload, next);

View File

@@ -0,0 +1,46 @@
import promisify = require('es6-promisify');
import Web3 = require('web3');
import {
JSONRPCPayload,
} from '../types';
/*
* A version of the base class Subprovider found in providerEngine
* This one has an async/await `emitPayloadAsync` and also defined types.
* Altered version of: https://github.com/MetaMask/provider-engine/blob/master/subproviders/subprovider.js
*/
export class Subprovider {
private engine: any;
private currentBlock: any;
// Ported from: https://github.com/MetaMask/provider-engine/blob/master/util/random-id.js
private static getRandomId() {
const extraDigits = 3;
// 13 time digits
const datePart = new Date().getTime() * Math.pow(10, extraDigits);
// 3 random digits
const extraPart = Math.floor(Math.random() * Math.pow(10, extraDigits));
// 16 digits
return datePart + extraPart;
}
private static createFinalPayload(payload: JSONRPCPayload): Web3.JSONRPCRequestPayload {
const finalPayload = {
// defaults
id: Subprovider.getRandomId(),
jsonrpc: '2.0',
params: [],
...payload,
};
return finalPayload;
}
public setEngine(engine: any): void {
this.engine = engine;
engine.on('block', (block: any) => {
this.currentBlock = block;
});
}
public async emitPayloadAsync(payload: JSONRPCPayload): Promise<any> {
const finalPayload = Subprovider.createFinalPayload(payload);
const response = await promisify(this.engine.sendAsync, this.engine)(finalPayload);
return response;
}
}

View File

@@ -0,0 +1,108 @@
import * as _ from 'lodash';
import * as Web3 from 'web3';
export interface LedgerCommunicationClient {
close_async: () => Promise<void>;
}
/*
* The LedgerEthereumClient sends Ethereum-specific requests to the Ledger Nano S
* It uses an internal LedgerCommunicationClient to relay these requests. Currently
* NodeJs and Browser communication are supported.
*/
export interface LedgerEthereumClient {
getAddress_async: (derivationPath: string, askForDeviceConfirmation: boolean,
shouldGetChainCode: boolean) => Promise<LedgerGetAddressResult>;
signPersonalMessage_async: (derivationPath: string, messageHex: string) => Promise<ECSignature>;
signTransaction_async: (derivationPath: string, txHex: string) => Promise<ECSignatureString>;
comm: LedgerCommunicationClient;
}
export interface ECSignatureString {
v: string;
r: string;
s: string;
}
export interface ECSignature {
v: number;
r: string;
s: string;
}
export type LedgerEthereumClientFactoryAsync = () => Promise<LedgerEthereumClient>;
/*
* networkId: The ethereum networkId to set as the chainId from EIP155
* ledgerConnectionType: Environment in which you wish to connect to Ledger (nodejs or browser)
* derivationPath: Initial derivation path to use e.g 44'/60'/0'
* accountFetchingConfigs: configs related to fetching accounts from a Ledger
*/
export interface LedgerSubproviderConfigs {
networkId: number;
ledgerEthereumClientFactoryAsync: LedgerEthereumClientFactoryAsync;
derivationPath?: string;
accountFetchingConfigs?: AccountFetchingConfigs;
}
/*
* numAddressesToReturn: Number of addresses to return from 'eth_accounts' call
* shouldAskForOnDeviceConfirmation: Whether you wish to prompt the user on their Ledger
* before fetching their addresses
*/
export interface AccountFetchingConfigs {
numAddressesToReturn?: number;
shouldAskForOnDeviceConfirmation?: boolean;
}
export interface SignatureData {
hash: string;
r: string;
s: string;
v: number;
}
export interface LedgerGetAddressResult {
address: string;
}
export interface LedgerWalletSubprovider {
getPath: () => string;
setPath: (path: string) => void;
setPathIndex: (pathIndex: number) => void;
}
export interface PartialTxParams {
nonce: string;
gasPrice?: string;
gas: string;
to: string;
from?: string;
value?: string;
data?: string;
chainId: number; // EIP 155 chainId - mainnet: 1, ropsten: 3
}
export type DoneCallback = (err?: Error) => void;
export interface JSONRPCPayload {
params: any[];
method: string;
}
export interface LedgerCommunication {
close_async: () => Promise<void>;
}
export interface ResponseWithTxParams {
raw: string;
tx: PartialTxParams;
}
export enum LedgerSubproviderErrors {
TooOldLedgerFirmware = 'TOO_OLD_LEDGER_FIRMWARE',
FromAddressMissingOrInvalid = 'FROM_ADDRESS_MISSING_OR_INVALID',
DataMissingForSignPersonalMessage = 'DATA_MISSING_FOR_SIGN_PERSONAL_MESSAGE',
SenderInvalidOrNotSupplied = 'SENDER_INVALID_OR_NOT_SUPPLIED',
MultipleOpenConnectionsDisallowed = 'MULTIPLE_OPEN_CONNECTIONS_DISALLOWED',
}

View File

@@ -0,0 +1,11 @@
import * as chai from 'chai';
import chaiAsPromised = require('chai-as-promised');
import * as dirtyChai from 'dirty-chai';
export const chaiSetup = {
configure() {
chai.config.includeStack = true;
chai.use(dirtyChai);
chai.use(chaiAsPromised);
},
};

View File

@@ -0,0 +1,172 @@
import * as chai from 'chai';
import promisify = require('es6-promisify');
import * as ethUtils from 'ethereumjs-util';
import * as _ from 'lodash';
import * as mocha from 'mocha';
import Web3 = require('web3');
import Web3ProviderEngine = require('web3-provider-engine');
import RpcSubprovider = require('web3-provider-engine/subproviders/rpc');
import {
ECSignature,
ledgerEthereumNodeJsClientFactoryAsync,
LedgerSubprovider,
} from '../../src';
import {
DoneCallback,
LedgerGetAddressResult,
PartialTxParams,
} from '../../src/types';
import {chaiSetup} from '../chai_setup';
import {reportCallbackErrors} from '../utils/report_callback_errors';
chaiSetup.configure();
const expect = chai.expect;
const TEST_RPC_ACCOUNT_0 = '0x5409ed021d9299bf6814279a6a1411a7e866a631';
describe('LedgerSubprovider', () => {
let ledgerSubprovider: LedgerSubprovider;
const networkId: number = 42;
before(async () => {
ledgerSubprovider = new LedgerSubprovider({
networkId,
ledgerEthereumClientFactoryAsync: ledgerEthereumNodeJsClientFactoryAsync,
});
});
describe('direct method calls', () => {
it('returns a list of accounts', async () => {
const accounts = await ledgerSubprovider.getAccountsAsync();
expect(accounts[0]).to.not.be.an('undefined');
expect(accounts.length).to.be.equal(10);
});
it('signs a personal message', async () => {
const data = ethUtils.bufferToHex(ethUtils.toBuffer('hello world'));
const ecSignatureHex = await ledgerSubprovider.signPersonalMessageAsync(data);
expect(ecSignatureHex.length).to.be.equal(132);
expect(ecSignatureHex.substr(0, 2)).to.be.equal('0x');
});
it('signs a transaction', async () => {
const tx = {
nonce: '0x00',
gas: '0x2710',
to: '0x0000000000000000000000000000000000000000',
value: '0x00',
chainId: 3,
};
const txHex = await ledgerSubprovider.signTransactionAsync(tx);
// tslint:disable-next-line:max-line-length
expect(txHex).to.be.equal('0xf85f8080822710940000000000000000000000000000000000000000808077a088a95ef1378487bc82be558e82c8478baf840c545d5b887536bb1da63673a98ba0019f4a4b9a107d1e6752bf7f701e275f28c13791d6e76af895b07373462cefaa');
});
});
describe('calls through a provider', () => {
let defaultProvider: Web3ProviderEngine;
let ledgerProvider: Web3ProviderEngine;
before(() => {
ledgerProvider = new Web3ProviderEngine();
ledgerProvider.addProvider(ledgerSubprovider);
const httpProvider = new RpcSubprovider({
rpcUrl: 'http://localhost:8545',
});
ledgerProvider.addProvider(httpProvider);
ledgerProvider.start();
defaultProvider = new Web3ProviderEngine();
defaultProvider.addProvider(httpProvider);
defaultProvider.start();
});
it('returns a list of accounts', (done: DoneCallback) => {
const payload = {
jsonrpc: '2.0',
method: 'eth_accounts',
params: [],
id: 1,
};
const callback = reportCallbackErrors(done)((err: Error, response: Web3.JSONRPCResponsePayload) => {
expect(err).to.be.a('null');
expect(response.result.length).to.be.equal(10);
done();
});
ledgerProvider.sendAsync(payload, callback);
});
it('signs a personal message', (done: DoneCallback) => {
(async () => {
const messageHex = ethUtils.bufferToHex(ethUtils.toBuffer('hello world'));
const accounts = await ledgerSubprovider.getAccountsAsync();
const signer = accounts[0];
const payload = {
jsonrpc: '2.0',
method: 'personal_sign',
params: [messageHex, signer],
id: 1,
};
const callback = reportCallbackErrors(done)((err: Error, response: Web3.JSONRPCResponsePayload) => {
expect(err).to.be.a('null');
expect(response.result.length).to.be.equal(132);
expect(response.result.substr(0, 2)).to.be.equal('0x');
done();
});
ledgerProvider.sendAsync(payload, callback);
})().catch(done);
});
it('signs a transaction', (done: DoneCallback) => {
const tx = {
to: '0xafa3f8684e54059998bc3a7b0d2b0da075154d66',
value: '0x00',
};
const payload = {
jsonrpc: '2.0',
method: 'eth_signTransaction',
params: [tx],
id: 1,
};
const callback = reportCallbackErrors(done)((err: Error, response: Web3.JSONRPCResponsePayload) => {
expect(err).to.be.a('null');
expect(response.result.raw.length).to.be.equal(206);
expect(response.result.raw.substr(0, 2)).to.be.equal('0x');
done();
});
ledgerProvider.sendAsync(payload, callback);
});
it('signs and sends a transaction', (done: DoneCallback) => {
(async () => {
const accounts = await ledgerSubprovider.getAccountsAsync();
// Give first account on Ledger sufficient ETH to complete tx send
let tx = {
to: accounts[0],
from: TEST_RPC_ACCOUNT_0,
value: '0x8ac7230489e80000', // 10 ETH
};
let payload = {
jsonrpc: '2.0',
method: 'eth_sendTransaction',
params: [tx],
id: 1,
};
await promisify(defaultProvider.sendAsync, defaultProvider)(payload);
// Send transaction from Ledger
tx = {
to: '0xafa3f8684e54059998bc3a7b0d2b0da075154d66',
from: accounts[0],
value: '0xde0b6b3a7640000',
};
payload = {
jsonrpc: '2.0',
method: 'eth_sendTransaction',
params: [tx],
id: 1,
};
const callback = reportCallbackErrors(done)((err: Error, response: Web3.JSONRPCResponsePayload) => {
expect(err).to.be.a('null');
const result = response.result.result;
expect(result.length).to.be.equal(66);
expect(result.substr(0, 2)).to.be.equal('0x');
done();
});
ledgerProvider.sendAsync(payload, callback);
})().catch(done);
});
});
});

View File

@@ -0,0 +1,212 @@
import * as chai from 'chai';
import * as ethUtils from 'ethereumjs-util';
import * as _ from 'lodash';
import Web3 = require('web3');
import Web3ProviderEngine = require('web3-provider-engine');
import RpcSubprovider = require('web3-provider-engine/subproviders/rpc');
import {
ECSignature,
LedgerSubprovider,
} from '../../src';
import {
DoneCallback,
ECSignatureString,
LedgerCommunicationClient,
LedgerGetAddressResult,
LedgerSubproviderErrors,
} from '../../src/types';
import {chaiSetup} from '../chai_setup';
import {reportCallbackErrors} from '../utils/report_callback_errors';
chaiSetup.configure();
const expect = chai.expect;
const FAKE_ADDRESS = '0x9901c66f2d4b95f7074b553da78084d708beca70';
describe('LedgerSubprovider', () => {
const networkId: number = 42;
let ledgerSubprovider: LedgerSubprovider;
before(async () => {
const ledgerEthereumClientFactoryAsync = async () => {
// tslint:disable:no-object-literal-type-assertion
const ledgerEthClient = {
getAddress_async: async () => {
return {
address: FAKE_ADDRESS,
};
},
signPersonalMessage_async: async () => {
const ecSignature = {
v: 28,
r: 'a6cc284bff14b42bdf5e9286730c152be91719d478605ec46b3bebcd0ae49148',
s: '0652a1a7b742ceb0213d1e744316e285f41f878d8af0b8e632cbca4c279132d0',
};
return ecSignature;
},
signTransaction_async: async (derivationPath: string, txHex: string) => {
const ecSignature = {
v: '77',
r: '88a95ef1378487bc82be558e82c8478baf840c545d5b887536bb1da63673a98b',
s: '019f4a4b9a107d1e6752bf7f701e275f28c13791d6e76af895b07373462cefaa',
};
return ecSignature;
},
comm: {
close_async: _.noop,
} as LedgerCommunicationClient,
};
// tslint:enable:no-object-literal-type-assertion
return ledgerEthClient;
};
ledgerSubprovider = new LedgerSubprovider({
networkId,
ledgerEthereumClientFactoryAsync,
});
});
describe('direct method calls', () => {
describe('success cases', () => {
it('returns a list of accounts', async () => {
const accounts = await ledgerSubprovider.getAccountsAsync();
expect(accounts[0]).to.be.equal(FAKE_ADDRESS);
expect(accounts.length).to.be.equal(10);
});
it('signs a personal message', async () => {
const data = ethUtils.bufferToHex(ethUtils.toBuffer('hello world'));
const ecSignatureHex = await ledgerSubprovider.signPersonalMessageAsync(data);
// tslint:disable-next-line:max-line-length
expect(ecSignatureHex).to.be.equal('0xa6cc284bff14b42bdf5e9286730c152be91719d478605ec46b3bebcd0ae491480652a1a7b742ceb0213d1e744316e285f41f878d8af0b8e632cbca4c279132d001');
});
});
describe('failure cases', () => {
it('cannot open multiple simultaneous connections to the Ledger device', async () => {
const data = ethUtils.bufferToHex(ethUtils.toBuffer('hello world'));
return expect(Promise.all([
ledgerSubprovider.getAccountsAsync(),
ledgerSubprovider.signPersonalMessageAsync(data),
])).to.be.rejectedWith(LedgerSubproviderErrors.MultipleOpenConnectionsDisallowed);
});
});
});
describe('calls through a provider', () => {
let provider: Web3ProviderEngine;
before(() => {
provider = new Web3ProviderEngine();
provider.addProvider(ledgerSubprovider);
const httpProvider = new RpcSubprovider({
rpcUrl: 'http://localhost:8545',
});
provider.addProvider(httpProvider);
provider.start();
});
describe('success cases', () => {
it('returns a list of accounts', (done: DoneCallback) => {
const payload = {
jsonrpc: '2.0',
method: 'eth_accounts',
params: [],
id: 1,
};
const callback = reportCallbackErrors(done)((err: Error, response: Web3.JSONRPCResponsePayload) => {
expect(err).to.be.a('null');
expect(response.result.length).to.be.equal(10);
expect(response.result[0]).to.be.equal(FAKE_ADDRESS);
done();
});
provider.sendAsync(payload, callback);
});
it('signs a personal message', (done: DoneCallback) => {
const messageHex = ethUtils.bufferToHex(ethUtils.toBuffer('hello world'));
const payload = {
jsonrpc: '2.0',
method: 'personal_sign',
params: [messageHex, '0x0000000000000000000000000000000000000000'],
id: 1,
};
const callback = reportCallbackErrors(done)((err: Error, response: Web3.JSONRPCResponsePayload) => {
expect(err).to.be.a('null');
// tslint:disable-next-line:max-line-length
expect(response.result).to.be.equal('0xa6cc284bff14b42bdf5e9286730c152be91719d478605ec46b3bebcd0ae491480652a1a7b742ceb0213d1e744316e285f41f878d8af0b8e632cbca4c279132d001');
done();
});
provider.sendAsync(payload, callback);
});
it('signs a transaction', (done: DoneCallback) => {
const tx = {
to: '0xafa3f8684e54059998bc3a7b0d2b0da075154d66',
value: '0x00',
gasPrice: '0x00',
nonce: '0x00',
gas: '0x00',
};
const payload = {
jsonrpc: '2.0',
method: 'eth_signTransaction',
params: [tx],
id: 1,
};
const callback = reportCallbackErrors(done)((err: Error, response: Web3.JSONRPCResponsePayload) => {
expect(err).to.be.a('null');
expect(response.result.raw.length).to.be.equal(192);
expect(response.result.raw.substr(0, 2)).to.be.equal('0x');
done();
});
provider.sendAsync(payload, callback);
});
});
describe('failure cases', () => {
it('should throw if `data` param not hex when calling personal_sign', (done: DoneCallback) => {
const nonHexMessage = 'hello world';
const payload = {
jsonrpc: '2.0',
method: 'personal_sign',
params: [nonHexMessage, '0x0000000000000000000000000000000000000000'],
id: 1,
};
const callback = reportCallbackErrors(done)((err: Error, response: Web3.JSONRPCResponsePayload) => {
expect(err).to.not.be.a('null');
expect(err.message).to.be.equal('Expected data to be of type HexString, encountered: hello world');
done();
});
provider.sendAsync(payload, callback);
});
it('should throw if `from` param missing when calling eth_sendTransaction', (done: DoneCallback) => {
const tx = {
to: '0xafa3f8684e54059998bc3a7b0d2b0da075154d66',
value: '0xde0b6b3a7640000',
};
const payload = {
jsonrpc: '2.0',
method: 'eth_sendTransaction',
params: [tx],
id: 1,
};
const callback = reportCallbackErrors(done)((err: Error, response: Web3.JSONRPCResponsePayload) => {
expect(err).to.not.be.a('null');
expect(err.message).to.be.equal(LedgerSubproviderErrors.SenderInvalidOrNotSupplied);
done();
});
provider.sendAsync(payload, callback);
});
it('should throw if `from` param invalid address when calling eth_sendTransaction',
(done: DoneCallback) => {
const tx = {
to: '0xafa3f8684e54059998bc3a7b0d2b0da075154d66',
from: '0xIncorrectEthereumAddress',
value: '0xde0b6b3a7640000',
};
const payload = {
jsonrpc: '2.0',
method: 'eth_sendTransaction',
params: [tx],
id: 1,
};
const callback = reportCallbackErrors(done)((err: Error, response: Web3.JSONRPCResponsePayload) => {
expect(err).to.not.be.a('null');
expect(err.message).to.be.equal(LedgerSubproviderErrors.SenderInvalidOrNotSupplied);
done();
});
provider.sendAsync(payload, callback);
});
});
});
});

View File

@@ -0,0 +1,62 @@
import * as chai from 'chai';
import * as _ from 'lodash';
import Web3 = require('web3');
import Web3ProviderEngine = require('web3-provider-engine');
import {RedundantRPCSubprovider} from '../../src';
import {
DoneCallback,
} from '../../src/types';
import {chaiSetup} from '../chai_setup';
import {reportCallbackErrors} from '../utils/report_callback_errors';
const expect = chai.expect;
describe('RedundantRpcSubprovider', () => {
let provider: Web3ProviderEngine;
it('succeeds when supplied a healthy endpoint', (done: DoneCallback) => {
provider = new Web3ProviderEngine();
const endpoints = [
'http://localhost:8545',
];
const redundantSubprovider = new RedundantRPCSubprovider(endpoints);
provider.addProvider(redundantSubprovider);
provider.start();
const payload = {
jsonrpc: '2.0',
method: 'eth_accounts',
params: [],
id: 1,
};
const callback = reportCallbackErrors(done)((err: Error, response: Web3.JSONRPCResponsePayload) => {
expect(err).to.be.a('null');
expect(response.result.length).to.be.equal(10);
done();
});
provider.sendAsync(payload, callback);
});
it('succeeds when supplied at least one healthy endpoint', (done: DoneCallback) => {
provider = new Web3ProviderEngine();
const endpoints = [
'http://does-not-exist:3000',
'http://localhost:8545',
];
const redundantSubprovider = new RedundantRPCSubprovider(endpoints);
provider.addProvider(redundantSubprovider);
provider.start();
const payload = {
jsonrpc: '2.0',
method: 'eth_accounts',
params: [],
id: 1,
};
const callback = reportCallbackErrors(done)((err: Error, response: Web3.JSONRPCResponsePayload) => {
expect(err).to.be.a('null');
expect(response.result.length).to.be.equal(10);
done();
});
provider.sendAsync(payload, callback);
});
});

View File

@@ -0,0 +1,14 @@
import { DoneCallback } from '../../src/types';
export const reportCallbackErrors = (done: DoneCallback) => {
return (f: (...args: any[]) => void) => {
const wrapped = async (...args: any[]) => {
try {
f(...args);
} catch (err) {
done(err);
}
};
return wrapped;
};
};

View File

@@ -0,0 +1,22 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"lib": [ "es2015", "dom" ],
"outDir": "lib",
"sourceMap": true,
"declaration": true,
"noImplicitAny": true,
"experimentalDecorators": true,
"strictNullChecks": true
},
"include": [
"./src/**/*",
"./test/**/*",
"../../node_modules/web3-typescript-typings/index.d.ts",
"../../node_modules/chai-typescript-typings/index.d.ts",
"../../node_modules/types-bn/index.d.ts",
"../../node_modules/types-ethereumjs-util/index.d.ts",
"../../node_modules/chai-as-promised-typescript-typings/index.d.ts"
]
}

View File

@@ -0,0 +1,5 @@
{
"extends": [
"@0xproject/tslint-config"
]
}

View File

@@ -18,6 +18,7 @@
"author": "Fabio Berger",
"license": "Apache-2.0",
"dependencies": {
"@0xproject/subproviders": "0.0.1",
"0x.js": "0xproject/0x.js/packages/0x.js#0x.js@0.27.1",
"accounting": "^0.4.1",
"basscss": "^8.0.3",
@@ -61,7 +62,6 @@
"thenby": "^1.2.3",
"truffle-contract": "2.0.1",
"tslint-config-0xproject": "^0.0.2",
"typescript": "^2.4.1",
"web3": "^0.20.0",
"web3-provider-engine": "^13.0.1",
"whatwg-fetch": "^2.0.3",

View File

@@ -16,6 +16,13 @@ import {
ZeroEx,
ZeroExError,
} from '0x.js';
import {
InjectedWeb3Subprovider,
ledgerEthereumBrowserClientFactoryAsync,
LedgerSubprovider,
LedgerWalletSubprovider,
RedundantRPCSubprovider,
} from '@0xproject/subproviders';
import {promisify} from '@0xproject/utils';
import BigNumber from 'bignumber.js';
import compareVersions = require('compare-versions');
@@ -25,20 +32,16 @@ import * as _ from 'lodash';
import * as React from 'react';
import contract = require('truffle-contract');
import {TokenSendCompleted} from 'ts/components/flash_messages/token_send_completed';
import { TransactionSubmitted } from 'ts/components/flash_messages/transaction_submitted';
import {TransactionSubmitted} from 'ts/components/flash_messages/transaction_submitted';
import {trackedTokenStorage} from 'ts/local_storage/tracked_token_storage';
import {tradeHistoryStorage} from 'ts/local_storage/trade_history_storage';
import {Dispatcher} from 'ts/redux/dispatcher';
import {InjectedWeb3SubProvider} from 'ts/subproviders/injected_web3_subprovider';
import {ledgerWalletSubproviderFactory} from 'ts/subproviders/ledger_wallet_subprovider_factory';
import {RedundantRPCSubprovider} from 'ts/subproviders/redundant_rpc_subprovider';
import {
BlockchainCallErrs,
BlockchainErrs,
ContractInstance,
ContractResponse,
EtherscanLinkSuffixes,
LedgerWalletSubprovider,
ProviderType,
Side,
SignatureData,
@@ -71,7 +74,7 @@ export class Blockchain {
private tokenRegistry: ContractInstance;
private userAddress: string;
private cachedProvider: Web3.Provider;
private ledgerSubProvider: LedgerWalletSubprovider;
private ledgerSubprovider: LedgerWalletSubprovider;
private zrxPollIntervalId: number;
private static async onPageLoadAsync() {
if (document.readyState === 'complete') {
@@ -105,7 +108,7 @@ export class Blockchain {
// We catch all requests involving a users account and send it to the injectedWeb3
// instance. All other requests go to the public hosted node.
provider = new ProviderEngine();
provider.addProvider(new InjectedWeb3SubProvider(injectedWeb3));
provider.addProvider(new InjectedWeb3Subprovider(injectedWeb3));
provider.addProvider(new FilterSubprovider());
provider.addProvider(new RedundantRPCSubprovider(
publicNodeUrlsIfExistsForNetworkId,
@@ -168,23 +171,23 @@ export class Blockchain {
return !_.isUndefined(tokenIfExists);
}
public getLedgerDerivationPathIfExists(): string {
if (_.isUndefined(this.ledgerSubProvider)) {
if (_.isUndefined(this.ledgerSubprovider)) {
return undefined;
}
const path = this.ledgerSubProvider.getPath();
const path = this.ledgerSubprovider.getPath();
return path;
}
public updateLedgerDerivationPathIfExists(path: string) {
if (_.isUndefined(this.ledgerSubProvider)) {
if (_.isUndefined(this.ledgerSubprovider)) {
return; // noop
}
this.ledgerSubProvider.setPath(path);
this.ledgerSubprovider.setPath(path);
}
public updateLedgerDerivationIndex(pathIndex: number) {
if (_.isUndefined(this.ledgerSubProvider)) {
if (_.isUndefined(this.ledgerSubprovider)) {
return; // noop
}
this.ledgerSubProvider.setPathIndex(pathIndex);
this.ledgerSubprovider.setPathIndex(pathIndex);
}
public async providerTypeUpdatedFireAndForgetAsync(providerType: ProviderType) {
utils.assert(!_.isUndefined(this.zeroEx), 'ZeroEx must be instantiated.');
@@ -204,8 +207,12 @@ export class Blockchain {
this.dispatcher.updateUserAddress(''); // Clear old userAddress
provider = new ProviderEngine();
this.ledgerSubProvider = ledgerWalletSubproviderFactory(this.getBlockchainNetworkId.bind(this));
provider.addProvider(this.ledgerSubProvider);
const ledgerWalletConfigs = {
networkId: this.networkId,
ledgerEthereumClientFactoryAsync: ledgerEthereumBrowserClientFactoryAsync,
};
this.ledgerSubprovider = new LedgerSubprovider(ledgerWalletConfigs);
provider.addProvider(this.ledgerSubprovider);
provider.addProvider(new FilterSubprovider());
const networkId = configs.isMainnetEnabled ?
constants.MAINNET_NETWORK_ID :
@@ -231,7 +238,7 @@ export class Blockchain {
this.web3Wrapper = new Web3Wrapper(this.dispatcher, provider, this.networkId, shouldPollUserAddress);
this.zeroEx.setProvider(provider, this.networkId);
await this.postInstantiationOrUpdatingProviderZeroExAsync();
delete this.ledgerSubProvider;
delete this.ledgerSubprovider;
delete this.cachedProvider;
break;
}
@@ -657,11 +664,6 @@ export class Blockchain {
constants.PUBLIC_PROVIDER_NAME;
this.dispatcher.updateInjectedProviderName(providerName);
}
// This is only ever called by the LedgerWallet subprovider in order to retrieve
// the current networkId without this value going stale.
private getBlockchainNetworkId() {
return this.networkId;
}
private async fetchTokenInformationAsync() {
utils.assert(!_.isUndefined(this.networkId),
'Cannot call fetchTokenInformationAsync if disconnected from Ethereum node');

View File

@@ -3,7 +3,6 @@ declare module 'react-router-hash-link';
declare module 'truffle-contract';
declare module 'ethereumjs-util';
declare module 'keccak';
declare module 'web3-provider-engine';
declare module 'whatwg-fetch';
declare module 'react-html5video';
declare module 'web3-provider-engine/subproviders/filters';
@@ -21,6 +20,8 @@ declare module '*.json' {
/* tslint:enable */
}
// tslint:disable:max-classes-per-file
// find-version declarations
declare function findVersions(version: string): string[];
declare module 'find-versions' {
@@ -131,21 +132,26 @@ declare class Subprovider {}
declare module 'web3-provider-engine/subproviders/subprovider' {
export = Subprovider;
}
// tslint:disable-next-line:max-classes-per-file
declare class RpcSubprovider {
constructor(options: {rpcUrl: string});
public handleRequest(payload: any, next: any, end: (err?: Error, data?: any) => void): void;
}
declare module 'web3-provider-engine/subproviders/rpc' {
import * as Web3 from 'web3';
class RpcSubprovider {
constructor(options: {rpcUrl: string});
public handleRequest(
payload: Web3.JSONRPCRequestPayload, next: () => void, end: (err: Error|null, data?: any) => void,
): void;
}
export = RpcSubprovider;
}
// tslint:disable-next-line:max-classes-per-file
declare class HookedWalletSubprovider {
constructor(wallet: any);
}
declare module 'web3-provider-engine/subproviders/hooked-wallet' {
export = HookedWalletSubprovider;
declare module 'web3-provider-engine' {
class Web3ProviderEngine {
public on(event: string, handler: () => void): void;
public send(payload: any): void;
public sendAsync(payload: any, callback: (error: any, response: any) => void): void;
public addProvider(provider: any): void;
public start(): void;
public stop(): void;
}
export = Web3ProviderEngine;
}
declare interface Artifact {

View File

@@ -1,172 +0,0 @@
import * as EthereumTx from 'ethereumjs-tx';
import ethUtil = require('ethereumjs-util');
import * as ledger from 'ledgerco';
import * as _ from 'lodash';
import {LedgerEthConnection, SignPersonalMessageParams, TxParams} from 'ts/types';
import {constants} from 'ts/utils/constants';
import Web3 = require('web3');
import HookedWalletSubprovider = require('web3-provider-engine/subproviders/hooked-wallet');
const NUM_ADDRESSES_TO_FETCH = 10;
const ASK_FOR_ON_DEVICE_CONFIRMATION = false;
const SHOULD_GET_CHAIN_CODE = false;
export class LedgerWallet {
public isU2FSupported: boolean;
public getAccounts: (callback: (err: Error, accounts: string[]) => void) => void;
public signMessage: (msgParams: SignPersonalMessageParams,
callback: (err: Error, result?: string) => void) => void;
public signTransaction: (txParams: TxParams,
callback: (err: Error, result?: string) => void) => void;
private getNetworkId: () => number;
private path: string;
private pathIndex: number;
private ledgerEthConnection: LedgerEthConnection;
private accounts: string[];
constructor(getNetworkIdFn: () => number) {
this.path = constants.DEFAULT_DERIVATION_PATH;
this.pathIndex = 0;
this.isU2FSupported = false;
this.getNetworkId = getNetworkIdFn;
this.getAccounts = this.getAccountsAsync.bind(this);
this.signMessage = this.signPersonalMessageAsync.bind(this);
this.signTransaction = this.signTransactionAsync.bind(this);
}
public getPath(): string {
return this.path;
}
public setPath(derivationPath: string) {
this.path = derivationPath;
// HACK: Must re-assign getAccounts, signMessage and signTransaction since they were
// previously bound to old values of this.path
this.getAccounts = this.getAccountsAsync.bind(this);
this.signMessage = this.signPersonalMessageAsync.bind(this);
this.signTransaction = this.signTransactionAsync.bind(this);
}
public setPathIndex(pathIndex: number) {
this.pathIndex = pathIndex;
// HACK: Must re-assign signMessage & signTransaction since they it was previously bound to
// old values of this.path
this.signMessage = this.signPersonalMessageAsync.bind(this);
this.signTransaction = this.signTransactionAsync.bind(this);
}
public async getAccountsAsync(callback: (err: Error, accounts: string[]) => void) {
if (!_.isUndefined(this.ledgerEthConnection)) {
callback(null, []);
return;
}
this.ledgerEthConnection = await this.createLedgerConnectionAsync();
const accounts = [];
for (let i = 0; i < NUM_ADDRESSES_TO_FETCH; i++) {
try {
const derivationPath = `${this.path}/${i}`;
const result = await this.ledgerEthConnection.getAddress_async(
derivationPath, ASK_FOR_ON_DEVICE_CONFIRMATION, SHOULD_GET_CHAIN_CODE,
);
accounts.push(result.address.toLowerCase());
} catch (err) {
await this.closeLedgerConnectionAsync();
callback(err, null);
return;
}
}
await this.closeLedgerConnectionAsync();
callback(null, accounts);
}
public async signTransactionAsync(txParams: TxParams, callback: (err: Error, result?: string) => void) {
const tx = new EthereumTx(txParams);
const networkId = this.getNetworkId();
const chainId = networkId; // Same thing
// Set the EIP155 bits
tx.raw[6] = Buffer.from([chainId]); // v
tx.raw[7] = Buffer.from([]); // r
tx.raw[8] = Buffer.from([]); // s
const txHex = tx.serialize().toString('hex');
this.ledgerEthConnection = await this.createLedgerConnectionAsync();
try {
const derivationPath = this.getDerivationPath();
const result = await this.ledgerEthConnection.signTransaction_async(derivationPath, txHex);
// Store signature in transaction
tx.v = new Buffer(result.v, 'hex');
tx.r = new Buffer(result.r, 'hex');
tx.s = new Buffer(result.s, 'hex');
// EIP155: v should be chain_id * 2 + {35, 36}
const signedChainId = Math.floor((tx.v[0] - 35) / 2);
if (signedChainId !== chainId) {
const err = new Error('TOO_OLD_LEDGER_FIRMWARE');
callback(err, null);
return;
}
const signedTxHex = `0x${tx.serialize().toString('hex')}`;
await this.closeLedgerConnectionAsync();
callback(null, signedTxHex);
} catch (err) {
await this.closeLedgerConnectionAsync();
callback(err, null);
}
}
public async signPersonalMessageAsync(msgParams: SignPersonalMessageParams,
callback: (err: Error, result?: string) => void) {
if (!_.isUndefined(this.ledgerEthConnection)) {
callback(new Error('Another request is in progress.'));
return;
}
this.ledgerEthConnection = await this.createLedgerConnectionAsync();
try {
const derivationPath = this.getDerivationPath();
const result = await this.ledgerEthConnection.signPersonalMessage_async(
derivationPath, ethUtil.stripHexPrefix(msgParams.data),
);
const v = _.parseInt(result.v) - 27;
let vHex = v.toString(16);
if (vHex.length < 2) {
vHex = `0${v}`;
}
const signature = `0x${result.r}${result.s}${vHex}`;
await this.closeLedgerConnectionAsync();
callback(null, signature);
} catch (err) {
await this.closeLedgerConnectionAsync();
callback(err, null);
}
}
private async createLedgerConnectionAsync() {
if (!_.isUndefined(this.ledgerEthConnection)) {
throw new Error('Multiple open connections to the Ledger disallowed.');
}
const ledgerConnection = await ledger.comm_u2f.create_async();
const ledgerEthConnection = new ledger.eth(ledgerConnection);
return ledgerEthConnection;
}
private async closeLedgerConnectionAsync() {
if (_.isUndefined(this.ledgerEthConnection)) {
return;
}
await this.ledgerEthConnection.comm.close_async();
this.ledgerEthConnection = undefined;
}
private getDerivationPath() {
const derivationPath = `${this.path}/${this.pathIndex}`;
return derivationPath;
}
}
export const ledgerWalletSubproviderFactory = (getNetworkIdFn: () => number): LedgerWallet => {
const ledgerWallet = new LedgerWallet(getNetworkIdFn);
const ledgerWalletSubprovider = new HookedWalletSubprovider(ledgerWallet) as LedgerWallet;
ledgerWalletSubprovider.getPath = ledgerWallet.getPath.bind(ledgerWallet);
ledgerWalletSubprovider.setPath = ledgerWallet.setPath.bind(ledgerWallet);
ledgerWalletSubprovider.setPathIndex = ledgerWallet.setPathIndex.bind(ledgerWallet);
return ledgerWalletSubprovider;
};

View File

@@ -521,12 +521,6 @@ export interface SignPersonalMessageParams {
data: string;
}
export interface LedgerWalletSubprovider {
getPath: () => string;
setPath: (path: string) => void;
setPathIndex: (pathIndex: number) => void;
}
export interface TxParams {
nonce: string;
gasPrice?: number;

View File

@@ -2183,10 +2183,14 @@ conventional-recommended-bump@^1.0.1:
meow "^3.3.0"
object-assign "^4.0.1"
convert-source-map@^1.1.0, convert-source-map@^1.3.0, convert-source-map@^1.5.0:
convert-source-map@^1.1.0, convert-source-map@^1.5.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.0.tgz#9acd70851c6d5dfdd93d9282e5edf94a03ff46b5"
convert-source-map@^1.3.0:
version "1.5.1"
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.1.tgz#b8278097b9bc229365de5c62cf5fcaed8b5599e5"
cookie-signature@1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
@@ -4737,7 +4741,7 @@ lcov-parse@^0.0.10:
ledgerco@0xProject/ledger-node-js-api:
version "1.1.3"
resolved "https://codeload.github.com/0xProject/ledger-node-js-api/tar.gz/dc2024bac997bf023f12203f118d10ba84d15ded"
resolved "https://codeload.github.com/0xProject/ledger-node-js-api/tar.gz/24aed21b8b362f2afc86faa578f38955ae2319ba"
dependencies:
async "2.1.4"
node-hid "0.5.4"
@@ -5443,11 +5447,11 @@ mute-stream@0.0.7, mute-stream@~0.0.4:
version "0.0.7"
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab"
nan@^2.0.5, nan@^2.0.8, nan@^2.2.1, nan@^2.3.0, nan@^2.3.3:
nan@^2.0.5, nan@^2.2.1, nan@^2.3.0, nan@^2.3.3:
version "2.7.0"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.7.0.tgz#d95bf721ec877e08db276ed3fc6eb78f9083ad46"
nan@^2.4.0:
nan@^2.0.8, nan@^2.4.0:
version "2.8.0"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.8.0.tgz#ed715f3fe9de02b57a5e6252d90a96675e1f085a"
@@ -7298,6 +7302,10 @@ selfsigned@^1.9.1:
dependencies:
node-forge "0.6.33"
semaphore-async-await@^1.5.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/semaphore-async-await/-/semaphore-async-await-1.5.1.tgz#857bef5e3644601ca4b9570b87e9df5ca12974fa"
semaphore@>=1.0.1, semaphore@^1.0.3:
version "1.1.0"
resolved "https://registry.yarnpkg.com/semaphore/-/semaphore-1.1.0.tgz#aaad8b86b20fe8e9b32b16dc2ee682a8cd26a8aa"
@@ -8385,8 +8393,16 @@ types-bn@^0.0.1:
bn.js "4.11.7"
types-ethereumjs-util@0xProject/types-ethereumjs-util:
version "0.0.5"
resolved "https://codeload.github.com/0xProject/types-ethereumjs-util/tar.gz/b9ae55d2c2711d89f63f7fc53a78579f2d4fbd74"
version "0.0.6"
resolved "https://codeload.github.com/0xProject/types-ethereumjs-util/tar.gz/a3b236df39d9fbfcb3b832a1fea7110649eeb616"
dependencies:
bn.js "^4.11.7"
buffer "^5.0.6"
rlp "^2.0.0"
types-ethereumjs-util@0xproject/types-ethereumjs-util:
version "0.0.6"
resolved "https://codeload.github.com/0xproject/types-ethereumjs-util/tar.gz/a3b236df39d9fbfcb3b832a1fea7110649eeb616"
dependencies:
bn.js "^4.11.7"
buffer "^5.0.6"
@@ -8396,7 +8412,7 @@ typescript@2.4.1:
version "2.4.1"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.4.1.tgz#c3ccb16ddaa0b2314de031e7e6fee89e5ba346bc"
typescript@^2.4.1, typescript@~2.6.1:
typescript@~2.6.1:
version "2.6.1"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.6.1.tgz#ef39cdea27abac0b500242d6726ab90e0c846631"