aboutsummaryrefslogtreecommitdiffstats
path: root/packages/subproviders
diff options
context:
space:
mode:
Diffstat (limited to 'packages/subproviders')
-rw-r--r--packages/subproviders/CHANGELOG.md6
-rw-r--r--packages/subproviders/src/globals.d.ts20
-rw-r--r--packages/subproviders/src/index.ts1
-rw-r--r--packages/subproviders/src/subproviders/ledger.ts2
-rw-r--r--packages/subproviders/src/subproviders/nonce_tracker.ts97
-rw-r--r--packages/subproviders/src/subproviders/redundant_rpc.ts1
-rw-r--r--packages/subproviders/src/types.ts13
-rw-r--r--packages/subproviders/test/unit/nonce_tracker_subprovider_test.ts150
8 files changed, 289 insertions, 1 deletions
diff --git a/packages/subproviders/CHANGELOG.md b/packages/subproviders/CHANGELOG.md
index 0469150c0..8adaa1c08 100644
--- a/packages/subproviders/CHANGELOG.md
+++ b/packages/subproviders/CHANGELOG.md
@@ -1,6 +1,10 @@
# CHANGELOG
-## v0.4.0 - _January 28, 2017_
+## v0.4.1 - _Febuary 2, 2018_
+
+ * Added NonceTrackerSubprovider (#355)
+
+## v0.4.0 - _January 28, 2018_
* Return a transaction hash from `_sendTransactionAsync` (#303)
diff --git a/packages/subproviders/src/globals.d.ts b/packages/subproviders/src/globals.d.ts
index 53457fa24..6f344dcd3 100644
--- a/packages/subproviders/src/globals.d.ts
+++ b/packages/subproviders/src/globals.d.ts
@@ -13,7 +13,9 @@ declare module 'ethereumjs-tx' {
public r: Buffer;
public s: Buffer;
public v: Buffer;
+ public nonce: Buffer;
public serialize(): Buffer;
+ public getSenderAddress(): Buffer;
constructor(txParams: any);
}
export = EthereumTx;
@@ -97,6 +99,24 @@ declare module 'web3-provider-engine' {
}
export = Web3ProviderEngine;
}
+declare module 'web3-provider-engine/util/rpc-cache-utils' {
+ class ProviderEngineRpcUtils {
+ public static blockTagForPayload(payload: any): string | null;
+ }
+ export = ProviderEngineRpcUtils;
+}
+declare module 'web3-provider-engine/subproviders/fixture' {
+ import * as Web3 from 'web3';
+ class FixtureSubprovider {
+ constructor(staticResponses: any);
+ public handleRequest(
+ payload: Web3.JSONRPCRequestPayload,
+ next: () => void,
+ end: (err: Error | null, data?: any) => void,
+ ): void;
+ }
+ export = FixtureSubprovider;
+}
// hdkey declarations
declare module 'hdkey' {
diff --git a/packages/subproviders/src/index.ts b/packages/subproviders/src/index.ts
index 720c4362f..4b3be4efd 100644
--- a/packages/subproviders/src/index.ts
+++ b/packages/subproviders/src/index.ts
@@ -9,6 +9,7 @@ import { LedgerEthereumClient } from './types';
export { InjectedWeb3Subprovider } from './subproviders/injected_web3';
export { RedundantRPCSubprovider } from './subproviders/redundant_rpc';
export { LedgerSubprovider } from './subproviders/ledger';
+export { NonceTrackerSubprovider } from './subproviders/nonce_tracker';
export { ECSignature, LedgerWalletSubprovider, LedgerCommunicationClient } from './types';
/**
diff --git a/packages/subproviders/src/subproviders/ledger.ts b/packages/subproviders/src/subproviders/ledger.ts
index 7267a793e..5966a88bb 100644
--- a/packages/subproviders/src/subproviders/ledger.ts
+++ b/packages/subproviders/src/subproviders/ledger.ts
@@ -60,6 +60,8 @@ export class LedgerSubprovider extends Subprovider {
public setPathIndex(pathIndex: number) {
this._derivationPathIndex = pathIndex;
}
+ // Required to implement this public interface which doesn't conform to our linting rule.
+ // tslint:disable-next-line:async-suffix
public async handleRequest(
payload: Web3.JSONRPCRequestPayload,
next: () => void,
diff --git a/packages/subproviders/src/subproviders/nonce_tracker.ts b/packages/subproviders/src/subproviders/nonce_tracker.ts
new file mode 100644
index 000000000..2f94ea581
--- /dev/null
+++ b/packages/subproviders/src/subproviders/nonce_tracker.ts
@@ -0,0 +1,97 @@
+import * as _ from 'lodash';
+
+import EthereumTx = require('ethereumjs-tx');
+import ethUtil = require('ethereumjs-util');
+import providerEngineUtils = require('web3-provider-engine/util/rpc-cache-utils');
+
+import {
+ BlockParamLiteral,
+ ErrorCallback,
+ JSONRPCPayload,
+ NonceSubproviderErrors,
+ OptionalNextCallback,
+} from '../types';
+
+import { Subprovider } from './subprovider';
+
+const NONCE_TOO_LOW_ERROR_MESSAGE = 'Transaction nonce is too low';
+export class NonceTrackerSubprovider extends Subprovider {
+ private _nonceCache: { [address: string]: string } = {};
+ private static _reconstructTransaction(payload: JSONRPCPayload): EthereumTx {
+ const raw = payload.params[0];
+ if (_.isUndefined(raw)) {
+ throw new Error(NonceSubproviderErrors.EmptyParametersFound);
+ }
+ const rawData = ethUtil.toBuffer(raw);
+ const transaction = new EthereumTx(rawData);
+ return transaction;
+ }
+ private static _determineAddress(payload: JSONRPCPayload): string {
+ let address: string;
+ switch (payload.method) {
+ case 'eth_getTransactionCount':
+ address = payload.params[0].toLowerCase();
+ return address;
+ case 'eth_sendRawTransaction':
+ const transaction = NonceTrackerSubprovider._reconstructTransaction(payload);
+ address = `0x${transaction.getSenderAddress().toString('hex')}`.toLowerCase();
+ return address;
+ default:
+ throw new Error(NonceSubproviderErrors.CannotDetermineAddressFromPayload);
+ }
+ }
+ // Required to implement this public interface which doesn't conform to our linting rule.
+ // tslint:disable-next-line:async-suffix
+ public async handleRequest(payload: JSONRPCPayload, next: OptionalNextCallback, end: ErrorCallback): Promise<void> {
+ switch (payload.method) {
+ case 'eth_getTransactionCount':
+ const requestDefaultBlock = providerEngineUtils.blockTagForPayload(payload);
+ if (requestDefaultBlock === BlockParamLiteral.Pending) {
+ const address = NonceTrackerSubprovider._determineAddress(payload);
+ const cachedResult = this._nonceCache[address];
+ if (!_.isUndefined(cachedResult)) {
+ return end(null, cachedResult);
+ } else {
+ return next((requestError: Error | null, requestResult: any, cb: any) => {
+ if (_.isNull(requestError)) {
+ this._nonceCache[address] = requestResult as string;
+ }
+ cb();
+ });
+ }
+ } else {
+ return next();
+ }
+ case 'eth_sendRawTransaction':
+ return next((sendTransactionError: Error | null, txResult: any, cb: any) => {
+ if (_.isNull(sendTransactionError)) {
+ this._handleSuccessfulTransaction(payload);
+ } else {
+ this._handleSendTransactionError(payload, sendTransactionError);
+ }
+ cb();
+ });
+ default:
+ return next();
+ }
+ }
+ private _handleSuccessfulTransaction(payload: JSONRPCPayload): void {
+ const address = NonceTrackerSubprovider._determineAddress(payload);
+ const transaction = NonceTrackerSubprovider._reconstructTransaction(payload);
+ // Increment the nonce from the previous successfully submitted transaction
+ let nonce = ethUtil.bufferToInt(transaction.nonce);
+ nonce++;
+ let nextHexNonce = nonce.toString(16);
+ if (nextHexNonce.length % 2) {
+ nextHexNonce = `0${nextHexNonce}`;
+ }
+ nextHexNonce = `0x${nextHexNonce}`;
+ this._nonceCache[address] = nextHexNonce;
+ }
+ private _handleSendTransactionError(payload: JSONRPCPayload, err: Error): void {
+ const address = NonceTrackerSubprovider._determineAddress(payload);
+ if (this._nonceCache[address] && _.includes(err.message, NONCE_TOO_LOW_ERROR_MESSAGE)) {
+ delete this._nonceCache[address];
+ }
+ }
+}
diff --git a/packages/subproviders/src/subproviders/redundant_rpc.ts b/packages/subproviders/src/subproviders/redundant_rpc.ts
index a3cb463a8..5a94f93d7 100644
--- a/packages/subproviders/src/subproviders/redundant_rpc.ts
+++ b/packages/subproviders/src/subproviders/redundant_rpc.ts
@@ -35,6 +35,7 @@ export class RedundantRPCSubprovider extends Subprovider {
});
});
}
+ // Required to implement this public interface which doesn't conform to our linting rule.
// tslint:disable-next-line:async-suffix
public async handleRequest(
payload: JSONRPCPayload,
diff --git a/packages/subproviders/src/types.ts b/packages/subproviders/src/types.ts
index 3db8be943..86b118767 100644
--- a/packages/subproviders/src/types.ts
+++ b/packages/subproviders/src/types.ts
@@ -112,3 +112,16 @@ export enum LedgerSubproviderErrors {
SenderInvalidOrNotSupplied = 'SENDER_INVALID_OR_NOT_SUPPLIED',
MultipleOpenConnectionsDisallowed = 'MULTIPLE_OPEN_CONNECTIONS_DISALLOWED',
}
+
+export enum NonceSubproviderErrors {
+ EmptyParametersFound = 'EMPTY_PARAMETERS_FOUND',
+ CannotDetermineAddressFromPayload = 'CANNOT_DETERMINE_ADDRESS_FROM_PAYLOAD',
+}
+
+// Re-defined BlockParamLiteral here, rather than import it from 0x.js.
+export enum BlockParamLiteral {
+ Pending = 'pending',
+}
+
+export type OptionalNextCallback = (callback?: (err: Error | null, result: any, cb: any) => void) => void;
+export type ErrorCallback = (err: Error | null, data?: any) => void;
diff --git a/packages/subproviders/test/unit/nonce_tracker_subprovider_test.ts b/packages/subproviders/test/unit/nonce_tracker_subprovider_test.ts
new file mode 100644
index 000000000..cffb4cdee
--- /dev/null
+++ b/packages/subproviders/test/unit/nonce_tracker_subprovider_test.ts
@@ -0,0 +1,150 @@
+import * as chai from 'chai';
+import * as _ from 'lodash';
+import Web3ProviderEngine = require('web3-provider-engine');
+import FixtureSubprovider = require('web3-provider-engine/subproviders/fixture');
+
+import promisify = require('es6-promisify');
+import EthereumTx = require('ethereumjs-tx');
+
+import { NonceTrackerSubprovider } from '../../src';
+import { chaiSetup } from '../chai_setup';
+
+const expect = chai.expect;
+chaiSetup.configure();
+
+describe('NonceTrackerSubprovider', () => {
+ let provider: Web3ProviderEngine;
+ const getTransactionCountPayload = {
+ jsonrpc: '2.0',
+ method: 'eth_getTransactionCount',
+ params: ['0x0', 'pending'],
+ id: 1,
+ };
+ const sendTransactionPayload = {
+ jsonrpc: '2.0',
+ method: 'eth_sendRawTransaction',
+ params: [],
+ id: 1,
+ };
+ const txParams = [
+ '0x',
+ '0x09184e72a000',
+ '0x2710',
+ '0x0000000000000000000000000000000000000000',
+ '0x',
+ '0x7f7465737432000000000000000000000000000000000000000000000000000000600057',
+ '0x1c',
+ '0x5e1d3a76fbf824220eafc8c79ad578ad2b67d01b0c2425eb1f1347e8f50882ab',
+ '0x5bd428537f05f9830e93792f90ea6a3e2d1ee84952dd96edbae9f658f831ab13',
+ ];
+ function createFixtureSubprovider() {
+ let called = false;
+ const fixedBlockNumberAndTransactionCountProvider = new FixtureSubprovider({
+ eth_getBlockByNumber: '0x01',
+ eth_getTransactionCount: (data: any, next: any, end: any) => {
+ if (called) {
+ return end(null, '0x99');
+ } else {
+ called = true;
+ end(null, '0x00');
+ }
+ },
+ });
+ return fixedBlockNumberAndTransactionCountProvider;
+ }
+ it('successfully caches the transaction count', async () => {
+ provider = new Web3ProviderEngine();
+ const nonceTrackerSubprovider = new NonceTrackerSubprovider();
+ provider.addProvider(nonceTrackerSubprovider);
+ provider.addProvider(createFixtureSubprovider());
+ provider.start();
+
+ const payload = { ...getTransactionCountPayload, params: ['0x0', 'pending'] };
+
+ const response = await promisify(provider.sendAsync, provider)(payload);
+ expect(response.result).to.be.eq('0x00');
+ const secondResponse = await promisify(provider.sendAsync, provider)(payload);
+ expect(secondResponse.result).to.be.eq('0x00');
+ });
+ it('does not cache the result for latest transaction count', async () => {
+ provider = new Web3ProviderEngine();
+ const nonceTrackerSubprovider = new NonceTrackerSubprovider();
+ provider.addProvider(nonceTrackerSubprovider);
+ provider.addProvider(createFixtureSubprovider());
+ provider.start();
+
+ const payload = { ...getTransactionCountPayload, params: ['0x0', 'latest'] };
+
+ const response = await promisify(provider.sendAsync, provider)(payload);
+ expect(response.result).to.be.eq('0x00');
+ const secondResponse = await promisify(provider.sendAsync, provider)(payload);
+ expect(secondResponse.result).to.be.eq('0x99');
+ });
+ it('clears the cache on a Nonce Too Low Error', async () => {
+ provider = new Web3ProviderEngine();
+ const nonceTrackerSubprovider = new NonceTrackerSubprovider();
+ provider.addProvider(nonceTrackerSubprovider);
+ provider.addProvider(createFixtureSubprovider());
+ provider.addProvider(
+ new FixtureSubprovider({
+ eth_sendRawTransaction: (data: any, next: any, end: any) => {
+ end(new Error('Transaction nonce is too low'));
+ },
+ }),
+ );
+ provider.start();
+
+ const noncePayload = {
+ ...getTransactionCountPayload,
+ params: ['0x1f36f546477cda21bf2296c50976f2740247906f', 'pending'],
+ };
+ const transaction = new EthereumTx(txParams);
+ const txPayload = {
+ ...sendTransactionPayload,
+ params: [transaction.serialize()],
+ };
+
+ const response = await promisify(provider.sendAsync, provider)(noncePayload);
+ expect(response.result).to.be.eq('0x00');
+ const secondResponse = await promisify(provider.sendAsync, provider)(noncePayload);
+ expect(secondResponse.result).to.be.eq('0x00');
+ try {
+ await promisify(provider.sendAsync, provider)(txPayload);
+ } catch (err) {
+ const thirdResponse = await promisify(provider.sendAsync, provider)(noncePayload);
+ expect(thirdResponse.result).to.be.eq('0x99');
+ }
+ });
+ it('increments the used nonce when a transaction successfully submits', async () => {
+ provider = new Web3ProviderEngine();
+ const nonceTrackerSubprovider = new NonceTrackerSubprovider();
+ provider.addProvider(nonceTrackerSubprovider);
+ provider.addProvider(createFixtureSubprovider());
+ provider.addProvider(
+ new FixtureSubprovider({
+ eth_sendRawTransaction: (data: any, next: any, end: any) => {
+ end(null);
+ },
+ }),
+ );
+ provider.start();
+
+ const noncePayload = {
+ ...getTransactionCountPayload,
+ params: ['0x1f36f546477cda21bf2296c50976f2740247906f', 'pending'],
+ };
+ const transaction = new EthereumTx(txParams);
+ const txPayload = {
+ ...sendTransactionPayload,
+ params: [transaction.serialize()],
+ };
+
+ const response = await promisify(provider.sendAsync, provider)(noncePayload);
+ expect(response.result).to.be.eq('0x00');
+ const secondResponse = await promisify(provider.sendAsync, provider)(noncePayload);
+ expect(secondResponse.result).to.be.eq('0x00');
+ await promisify(provider.sendAsync, provider)(txPayload);
+ const thirdResponse = await promisify(provider.sendAsync, provider)(noncePayload);
+ expect(thirdResponse.result).to.be.eq('0x01');
+ });
+});