Port subproviders over to mono repo, refactor LedgerSubprovider to no longer rely on hookedWalletSubprovider. Added unit and integration tests.
This commit is contained in:
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 Ethereum app
|
||||
- Make sure "browser support" is disabled
|
||||
|
||||
Then run:
|
||||
|
||||
```
|
||||
yarn test:integration
|
||||
```
|
||||
60
packages/subproviders/package.json
Normal file
60
packages/subproviders/package.json
Normal file
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"name": "@0xproject/subproviders",
|
||||
"version": "0.0.0",
|
||||
"main": "lib/src/index.js",
|
||||
"types": "lib/src/index.d.ts",
|
||||
"license": "Apache-2.0",
|
||||
"scripts": {
|
||||
"prebuild": "npm run clean",
|
||||
"build": "run-p build:umd:dev build:commonjs; exit 0;",
|
||||
"clean": "shx rm -rf lib",
|
||||
"build:umd:dev": "webpack",
|
||||
"build:umd:prod": "NODE_ENV=production webpack",
|
||||
"build:commonjs": "tsc",
|
||||
"lint": "tslint --project . 'src/**/*.ts' 'test/**/*.ts'",
|
||||
"pretest:umd": "run-s clean build:umd:dev build:commonjs",
|
||||
"substitute_umd_bundle": "shx mv _bundles/* lib/src",
|
||||
"run_mocha_all": "mocha lib/test/**/*_test.js --timeout 10000 --bail --exit",
|
||||
"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": "run-s clean build:commonjs run_mocha_all",
|
||||
"test:unit": "run-s clean build:commonjs run_mocha_unit",
|
||||
"test:integration": "run-s clean build:commonjs 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",
|
||||
"sinon": "^4.0.0",
|
||||
"web3": "^0.20.0",
|
||||
"web3-provider-engine": "^13.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@0xproject/tslint-config": "^0.2.0",
|
||||
"@types/lodash": "^4.14.64",
|
||||
"@types/mocha": "^2.2.44",
|
||||
"@types/node": "^8.0.1",
|
||||
"@types/sinon": "^2.2.2",
|
||||
"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.0",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
68
packages/subproviders/src/globals.d.ts
vendored
Normal file
68
packages/subproviders/src/globals.d.ts
vendored
Normal file
@@ -0,0 +1,68 @@
|
||||
/// <reference types='chai-typescript-typings' />
|
||||
/// <reference types='chai-as-promised-typescript-typings' />
|
||||
declare module 'bn.js';
|
||||
declare module 'dirty-chai';
|
||||
declare module 'ledgerco';
|
||||
declare module 'ethereumjs-tx';
|
||||
declare module 'es6-promisify';
|
||||
declare module 'ethereum-address';
|
||||
declare module 'debug';
|
||||
|
||||
// tslint:disable:max-classes-per-file
|
||||
// tslint:disable:class-name
|
||||
// tslint:disable:completed-docs
|
||||
declare module 'ledgerco' {
|
||||
interface comm {
|
||||
close_async: Promise<void>;
|
||||
create_async: Promise<void>;
|
||||
}
|
||||
export class comm_node implements comm {
|
||||
public create_async: Promise<void>;
|
||||
public close_async: Promise<void>;
|
||||
}
|
||||
export class comm_u2f implements comm {
|
||||
public create_async: Promise<void>;
|
||||
public close_async: Promise<void>;
|
||||
}
|
||||
}
|
||||
|
||||
// Semaphore-async-await declarations
|
||||
declare module 'semaphore-async-await' {
|
||||
class Semaphore {
|
||||
constructor(permits: number);
|
||||
public wait(): 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;
|
||||
}
|
||||
// tslint:enable:max-classes-per-file
|
||||
// tslint:enable:class-name
|
||||
// tslint:enable:completed-docs
|
||||
30
packages/subproviders/src/index.ts
Normal file
30
packages/subproviders/src/index.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
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';
|
||||
|
||||
export async function ledgerEthereumBrowserClientFactoryAsync(): Promise<LedgerEthereumClient> {
|
||||
const ledgerConnection = await LedgerBrowserCommunication.create_async();
|
||||
const ledgerEthClient = new LedgerEthereumClientFn(ledgerConnection);
|
||||
return ledgerEthClient;
|
||||
}
|
||||
|
||||
export async function ledgerEthereumNodeJsClientFactoryAsync(): Promise<LedgerEthereumClient> {
|
||||
const ledgerConnection = await LedgerNodeCommunication.create_async();
|
||||
const ledgerEthClient = new LedgerEthereumClientFn(ledgerConnection);
|
||||
return ledgerEthClient;
|
||||
}
|
||||
47
packages/subproviders/src/subproviders/injected_web3.ts
Normal file
47
packages/subproviders/src/subproviders/injected_web3.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import * as _ from 'lodash';
|
||||
import Web3 = require('web3');
|
||||
import Web3ProviderEngine = require('web3-provider-engine');
|
||||
|
||||
/*
|
||||
* This class implements the web3-provider-engine subprovider interface and forwards
|
||||
* requests involving user accounts (getAccounts, sendTransaction, etc...) to the injected
|
||||
* web3 instance in their browser.
|
||||
* Source: https://github.com/MetaMask/provider-engine/blob/master/subproviders/subprovider.js
|
||||
*/
|
||||
export class InjectedWeb3Subprovider {
|
||||
private injectedWeb3: Web3;
|
||||
constructor(injectedWeb3: Web3) {
|
||||
this.injectedWeb3 = injectedWeb3;
|
||||
}
|
||||
public handleRequest(
|
||||
payload: Web3.JSONRPCRequestPayload, next: () => void, end: (err: Error, result: any) => void,
|
||||
) {
|
||||
switch (payload.method) {
|
||||
case 'web3_clientVersion':
|
||||
this.injectedWeb3.version.getNode(end);
|
||||
return;
|
||||
case 'eth_accounts':
|
||||
this.injectedWeb3.eth.getAccounts(end);
|
||||
return;
|
||||
|
||||
case 'eth_sendTransaction':
|
||||
const [txParams] = payload.params;
|
||||
this.injectedWeb3.eth.sendTransaction(txParams, end);
|
||||
return;
|
||||
|
||||
case 'eth_sign':
|
||||
const [address, message] = payload.params;
|
||||
this.injectedWeb3.eth.sign(address, message, end);
|
||||
return;
|
||||
|
||||
default:
|
||||
next();
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Required to implement this method despite not needing it for this subprovider
|
||||
// tslint:disable-next-line:prefer-function-over-method
|
||||
public setEngine(engine: Web3ProviderEngine) {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
320
packages/subproviders/src/subproviders/ledger.ts
Normal file
320
packages/subproviders/src/subproviders/ledger.ts
Normal file
@@ -0,0 +1,320 @@
|
||||
import promisify = require('es6-promisify');
|
||||
import {isAddress} from 'ethereum-address';
|
||||
import * as EthereumTx from '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,
|
||||
SignPersonalMessageParams,
|
||||
} 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 validatePersonalMessage(msgParams: PartialTxParams) {
|
||||
if (_.isUndefined(msgParams.from) || !isAddress(msgParams.from)) {
|
||||
throw new Error(LedgerSubproviderErrors.FromAddressMissingOrInvalid);
|
||||
}
|
||||
if (_.isUndefined(msgParams.data)) {
|
||||
throw new Error(LedgerSubproviderErrors.DataMissingForSignPersonalMessage);
|
||||
}
|
||||
if (!LedgerSubprovider.isValidHex(msgParams.data)) {
|
||||
throw new Error(LedgerSubproviderErrors.DataNotValidHexForSignPersonalMessage);
|
||||
}
|
||||
}
|
||||
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':
|
||||
// non-standard "extraParams" to be appended to our "msgParams" obj
|
||||
// good place for metadata
|
||||
const extraParams = payload.params[2] || {};
|
||||
const msgParams = _.assign({}, extraParams, {
|
||||
from: payload.params[1],
|
||||
data: payload.params[0],
|
||||
});
|
||||
|
||||
try {
|
||||
LedgerSubprovider.validatePersonalMessage(msgParams);
|
||||
const ecSignatureHex = await this.signPersonalMessageAsync(msgParams);
|
||||
end(null, ecSignatureHex);
|
||||
} catch (err) {
|
||||
end(err);
|
||||
}
|
||||
return;
|
||||
|
||||
default:
|
||||
next();
|
||||
return;
|
||||
}
|
||||
}
|
||||
public async getAccountsAsync(): Promise<string[]> {
|
||||
this._ledgerClientIfExists = await this.createLedgerClientAsync();
|
||||
|
||||
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(msgParams: SignPersonalMessageParams): Promise<string> {
|
||||
this._ledgerClientIfExists = await this.createLedgerClientAsync();
|
||||
try {
|
||||
const derivationPath = this.getDerivationPath();
|
||||
const result = await this._ledgerClientIfExists.signPersonalMessage_async(
|
||||
derivationPath, ethUtil.stripHexPrefix(msgParams.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<any> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
46
packages/subproviders/src/subproviders/redundant_rpc.ts
Normal file
46
packages/subproviders/src/subproviders/redundant_rpc.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import promisify = require('es6-promisify');
|
||||
import * as _ from 'lodash';
|
||||
import RpcSubprovider = require('web3-provider-engine/subproviders/rpc');
|
||||
import Subprovider = require('web3-provider-engine/subproviders/subprovider');
|
||||
|
||||
import {JSONRPCPayload} from '../types';
|
||||
|
||||
export class RedundantRPCSubprovider extends Subprovider {
|
||||
private rpcs: RpcSubprovider[];
|
||||
private static async firstSuccessAsync(
|
||||
rpcs: RpcSubprovider[], payload: JSONRPCPayload, next: () => void,
|
||||
): Promise<any> {
|
||||
let lastErr: Error|undefined;
|
||||
for (const rpc of rpcs) {
|
||||
try {
|
||||
const data = await promisify(rpc.handleRequest.bind(rpc))(payload, next);
|
||||
return data;
|
||||
} catch (err) {
|
||||
lastErr = err;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (!_.isUndefined(lastErr)) {
|
||||
throw lastErr;
|
||||
}
|
||||
}
|
||||
constructor(endpoints: string[]) {
|
||||
super();
|
||||
this.rpcs = _.map(endpoints, endpoint => {
|
||||
return new RpcSubprovider({
|
||||
rpcUrl: endpoint,
|
||||
});
|
||||
});
|
||||
}
|
||||
public async handleRequest(payload: JSONRPCPayload, next: () => void,
|
||||
end: (err?: Error, data?: any) => void): Promise<void> {
|
||||
const rpcsCopy = this.rpcs.slice();
|
||||
try {
|
||||
const data = await RedundantRPCSubprovider.firstSuccessAsync(rpcsCopy, payload, next);
|
||||
end(undefined, data);
|
||||
} catch (err) {
|
||||
end(err);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
45
packages/subproviders/src/subproviders/subprovider.ts
Normal file
45
packages/subproviders/src/subproviders/subprovider.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
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;
|
||||
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;
|
||||
}
|
||||
}
|
||||
115
packages/subproviders/src/types.ts
Normal file
115
packages/subproviders/src/types.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as Web3 from 'web3';
|
||||
|
||||
export interface LedgerCommunicationClient {
|
||||
exchange: (apduHex: string, statusList: number[]) => Promise<any[]>;
|
||||
setScrambleKey: (key: string) => void;
|
||||
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 SignPersonalMessageParams {
|
||||
data: string;
|
||||
}
|
||||
|
||||
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',
|
||||
DataNotValidHexForSignPersonalMessage = 'DATA_NOT_VALID_HEX_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,171 @@
|
||||
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';
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
229
packages/subproviders/test/unit/ledger_subprovider_test.ts
Normal file
229
packages/subproviders/test/unit/ledger_subprovider_test.ts
Normal file
@@ -0,0 +1,229 @@
|
||||
import * as chai from 'chai';
|
||||
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,
|
||||
LedgerSubprovider,
|
||||
} from '../../src';
|
||||
import {
|
||||
DoneCallback,
|
||||
ECSignatureString,
|
||||
LedgerCommunicationClient,
|
||||
LedgerGetAddressResult,
|
||||
LedgerSubproviderErrors,
|
||||
} from '../../src/types';
|
||||
import { chaiSetup } from '../chai_setup';
|
||||
import {reportCallbackErrors} from '../utils/report_callback_errors';
|
||||
|
||||
const expect = chai.expect;
|
||||
const FAKE_ADDRESS = '0x9901c66f2d4b95f7074b553da78084d708beca70';
|
||||
|
||||
describe('LedgerSubprovider', () => {
|
||||
const networkId: number = 42;
|
||||
let ledgerSubprovider: LedgerSubprovider;
|
||||
before(async () => {
|
||||
const ledgerEthereumClientFactoryAsync = async () => {
|
||||
const ledgerEthClient = {
|
||||
getAddress_async: async () => {
|
||||
return {
|
||||
address: FAKE_ADDRESS,
|
||||
};
|
||||
},
|
||||
signPersonalMessage_async: async () => {
|
||||
const ecSignature: ECSignature = {
|
||||
v: 28,
|
||||
r: 'a6cc284bff14b42bdf5e9286730c152be91719d478605ec46b3bebcd0ae49148',
|
||||
s: '0652a1a7b742ceb0213d1e744316e285f41f878d8af0b8e632cbca4c279132d0',
|
||||
};
|
||||
return ecSignature;
|
||||
},
|
||||
signTransaction_async: async (derivationPath: string, txHex: string) => {
|
||||
const ecSignature: ECSignatureString = {
|
||||
v: '77',
|
||||
r: '88a95ef1378487bc82be558e82c8478baf840c545d5b887536bb1da63673a98b',
|
||||
s: '019f4a4b9a107d1e6752bf7f701e275f28c13791d6e76af895b07373462cefaa',
|
||||
};
|
||||
return ecSignature;
|
||||
},
|
||||
comm: {
|
||||
close_async: _.noop,
|
||||
} as LedgerCommunicationClient,
|
||||
};
|
||||
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 msgParams = {data};
|
||||
const ecSignatureHex = await ledgerSubprovider.signPersonalMessageAsync(msgParams);
|
||||
// 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'));
|
||||
const msgParams = {data};
|
||||
try {
|
||||
const result = await Promise.all([
|
||||
ledgerSubprovider.getAccountsAsync(),
|
||||
ledgerSubprovider.signPersonalMessageAsync(msgParams),
|
||||
]);
|
||||
throw new Error('Multiple simultaneous calls succeeded when they should have failed');
|
||||
} catch (err) {
|
||||
expect(err.message).to.be.equal(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',
|
||||
};
|
||||
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();
|
||||
});
|
||||
provider.sendAsync(payload, callback);
|
||||
});
|
||||
});
|
||||
describe('failure cases', () => {
|
||||
it('should throw if `from` param missing when calling personal_sign', (done: DoneCallback) => {
|
||||
const messageHex = ethUtils.bufferToHex(ethUtils.toBuffer('hello world'));
|
||||
const payload = {
|
||||
jsonrpc: '2.0',
|
||||
method: 'personal_sign',
|
||||
params: [messageHex], // Missing from param
|
||||
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.FromAddressMissingOrInvalid);
|
||||
done();
|
||||
});
|
||||
provider.sendAsync(payload, callback);
|
||||
});
|
||||
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(LedgerSubproviderErrors.DataNotValidHexForSignPersonalMessage);
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
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"
|
||||
]
|
||||
}
|
||||
56
packages/subproviders/webpack.config.js
Normal file
56
packages/subproviders/webpack.config.js
Normal file
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* This is to generate the umd bundle only
|
||||
*/
|
||||
const _ = require('lodash');
|
||||
const webpack = require('webpack');
|
||||
const path = require('path');
|
||||
const production = process.env.NODE_ENV === 'production';
|
||||
|
||||
let entry = {
|
||||
'index': './src/index.ts',
|
||||
};
|
||||
if (production) {
|
||||
entry = _.assign({}, entry, {'index.min': './src/index.ts'});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
entry,
|
||||
output: {
|
||||
path: path.resolve(__dirname, '_bundles'),
|
||||
filename: '[name].js',
|
||||
libraryTarget: 'umd',
|
||||
library: '0x Subproviders',
|
||||
umdNamedDefine: true,
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.ts', '.js', '.json'],
|
||||
},
|
||||
devtool: 'source-map',
|
||||
plugins: [
|
||||
new webpack.optimize.UglifyJsPlugin({
|
||||
minimize: true,
|
||||
sourceMap: true,
|
||||
include: /\.min\.js$/,
|
||||
}),
|
||||
],
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.ts$/,
|
||||
use: [
|
||||
{
|
||||
loader: 'awesome-typescript-loader',
|
||||
query: {
|
||||
declaration: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
exclude: /node_modules/,
|
||||
},
|
||||
{
|
||||
test: /\.json$/,
|
||||
loader: 'json-loader',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
14
yarn.lock
14
yarn.lock
@@ -1285,7 +1285,7 @@ bn.js@4.11.7:
|
||||
version "4.11.7"
|
||||
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.7.tgz#ddb048e50d9482790094c13eb3fcfc833ce7ab46"
|
||||
|
||||
bn.js@4.11.8, bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.10.0, bn.js@^4.11.3, bn.js@^4.11.7, bn.js@^4.4.0, bn.js@^4.8.0:
|
||||
bn.js@4.11.8, bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.10.0, bn.js@^4.11.3, bn.js@^4.11.7, bn.js@^4.11.8, bn.js@^4.4.0, bn.js@^4.8.0:
|
||||
version "4.11.8"
|
||||
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f"
|
||||
|
||||
@@ -8066,8 +8066,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"
|
||||
|
||||
Reference in New Issue
Block a user