Merge pull request #252 from 0xProject/feature/addSubproviders
Add Subproviders Subpackage
This commit is contained in:
@@ -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"
|
||||
}
|
||||
}
|
||||
|
@@ -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",
|
||||
|
@@ -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",
|
||||
|
39
packages/subproviders/README.md
Normal file
39
packages/subproviders/README.md
Normal 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
|
||||
```
|
53
packages/subproviders/package.json
Normal file
53
packages/subproviders/package.json
Normal 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
97
packages/subproviders/src/globals.d.ts
vendored
Normal 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;
|
||||
}
|
38
packages/subproviders/src/index.ts
Normal file
38
packages/subproviders/src/index.ts
Normal 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;
|
||||
}
|
@@ -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
|
||||
}
|
||||
}
|
306
packages/subproviders/src/subproviders/ledger.ts
Normal file
306
packages/subproviders/src/subproviders/ledger.ts
Normal 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;
|
||||
}
|
||||
}
|
@@ -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);
|
46
packages/subproviders/src/subproviders/subprovider.ts
Normal file
46
packages/subproviders/src/subproviders/subprovider.ts
Normal 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;
|
||||
}
|
||||
}
|
108
packages/subproviders/src/types.ts
Normal file
108
packages/subproviders/src/types.ts
Normal 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',
|
||||
}
|
11
packages/subproviders/test/chai_setup.ts
Normal file
11
packages/subproviders/test/chai_setup.ts
Normal 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);
|
||||
},
|
||||
};
|
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
212
packages/subproviders/test/unit/ledger_subprovider_test.ts
Normal file
212
packages/subproviders/test/unit/ledger_subprovider_test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@@ -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);
|
||||
});
|
||||
});
|
14
packages/subproviders/test/utils/report_callback_errors.ts
Normal file
14
packages/subproviders/test/utils/report_callback_errors.ts
Normal 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;
|
||||
};
|
||||
};
|
22
packages/subproviders/tsconfig.json
Normal file
22
packages/subproviders/tsconfig.json
Normal 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"
|
||||
]
|
||||
}
|
5
packages/subproviders/tslint.json
Normal file
5
packages/subproviders/tslint.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": [
|
||||
"@0xproject/tslint-config"
|
||||
]
|
||||
}
|
@@ -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",
|
||||
|
@@ -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');
|
||||
|
32
packages/website/ts/globals.d.ts
vendored
32
packages/website/ts/globals.d.ts
vendored
@@ -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 {
|
||||
|
@@ -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;
|
||||
};
|
@@ -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;
|
||||
|
30
yarn.lock
30
yarn.lock
@@ -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"
|
||||
|
||||
|
Reference in New Issue
Block a user