Nonce tracker subprovider
Caches the nonce when a request to getTransactionCount is made and increments the pending nonce after successful transactions
This commit is contained in:
21
packages/subproviders/src/globals.d.ts
vendored
21
packages/subproviders/src/globals.d.ts
vendored
@@ -1,3 +1,4 @@
|
||||
|
||||
declare module 'dirty-chai';
|
||||
declare module 'es6-promisify';
|
||||
|
||||
@@ -13,7 +14,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 +100,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' {
|
||||
|
||||
@@ -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';
|
||||
|
||||
/**
|
||||
|
||||
112
packages/subproviders/src/subproviders/nonce_tracker.ts
Normal file
112
packages/subproviders/src/subproviders/nonce_tracker.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import { promisify } from '@0xproject/utils';
|
||||
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 { JSONRPCPayload } 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];
|
||||
const transactionData = ethUtil.stripHexPrefix(raw);
|
||||
const rawData = new Buffer(transactionData, 'hex');
|
||||
return new EthereumTx(rawData);
|
||||
}
|
||||
private static _determineAddress(payload: JSONRPCPayload): string {
|
||||
switch (payload.method) {
|
||||
case 'eth_getTransactionCount':
|
||||
return payload.params[0].toLowerCase();
|
||||
case 'eth_sendRawTransaction':
|
||||
const transaction = NonceTrackerSubprovider._reconstructTransaction(payload);
|
||||
return `0x${transaction.getSenderAddress().toString('hex')}`.toLowerCase();
|
||||
default:
|
||||
throw new Error('Invalid Method');
|
||||
}
|
||||
}
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
// tslint:disable-next-line:async-suffix
|
||||
public async handleRequest(
|
||||
payload: JSONRPCPayload,
|
||||
next: (callback?: (err: Error | null, result: any, cb: any) => void) => void,
|
||||
end: (err: Error | null, data?: any) => void,
|
||||
): Promise<void> {
|
||||
switch (payload.method) {
|
||||
case 'eth_getTransactionCount':
|
||||
const blockTag = providerEngineUtils.blockTagForPayload(payload);
|
||||
if (!_.isNull(blockTag) && blockTag === 'pending') {
|
||||
const address = NonceTrackerSubprovider._determineAddress(payload);
|
||||
const cachedResult = this._nonceCache[address];
|
||||
if (cachedResult) {
|
||||
end(null, cachedResult);
|
||||
return;
|
||||
} else {
|
||||
next((requestError: Error | null, requestResult: any, cb: any) => {
|
||||
if (_.isNull(requestError)) {
|
||||
this._nonceCache[address] = requestResult as string;
|
||||
}
|
||||
cb();
|
||||
return;
|
||||
});
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
case 'eth_sendRawTransaction':
|
||||
return next(async (sendTransactionError: Error | null, txResult: any, cb: any) => {
|
||||
if (_.isNull(sendTransactionError)) {
|
||||
this._handleSuccessfulTransaction(payload);
|
||||
} else {
|
||||
await this._handleSendTransactionErrorAsync(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 async _handleSendTransactionErrorAsync(payload: JSONRPCPayload, err: Error): Promise<void> {
|
||||
const address = NonceTrackerSubprovider._determineAddress(payload);
|
||||
if (this._nonceCache[address]) {
|
||||
if (_.includes(err.message, NONCE_TOO_LOW_ERROR_MESSAGE)) {
|
||||
await this._handleNonceTooLowErrorAsync(address);
|
||||
}
|
||||
}
|
||||
}
|
||||
private async _handleNonceTooLowErrorAsync(address: string): Promise<void> {
|
||||
const oldNonceInt = ethUtil.bufferToInt(new Buffer(this._nonceCache[address], 'hex'));
|
||||
delete this._nonceCache[address];
|
||||
const nonceResult = await this.emitPayloadAsync({
|
||||
method: 'eth_getTransactionCount',
|
||||
params: [address, 'pending'],
|
||||
});
|
||||
const nonce = nonceResult.result;
|
||||
const latestNonceInt = ethUtil.bufferToInt(new Buffer(nonce, 'hex'));
|
||||
if (latestNonceInt > oldNonceInt) {
|
||||
this._nonceCache[address] = nonce;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
import * as chai from 'chai';
|
||||
import * as _ from 'lodash';
|
||||
import Web3 = require('web3');
|
||||
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 { DoneCallback } from '../../src/types';
|
||||
import { chaiSetup } from '../chai_setup';
|
||||
import { reportCallbackErrors } from '../utils/report_callback_errors';
|
||||
|
||||
import { Subprovider } from '../../src/subproviders/subprovider';
|
||||
|
||||
const expect = chai.expect;
|
||||
chaiSetup.configure();
|
||||
|
||||
describe('NonceTrackerSubprovider', () => {
|
||||
let provider: Web3ProviderEngine;
|
||||
it('successfully caches the transaction count', async () => {
|
||||
provider = new Web3ProviderEngine();
|
||||
let called = false;
|
||||
const nonceTrackerSubprovider = new NonceTrackerSubprovider();
|
||||
provider.addProvider(nonceTrackerSubprovider);
|
||||
provider.addProvider(new FixtureSubprovider({
|
||||
'eth_getBlockByNumber': '0x01',
|
||||
'eth_getTransactionCount': (data: any, next: any, end: any) => {
|
||||
if (called) {
|
||||
return end(null, '0x99');
|
||||
} else {
|
||||
called = true;
|
||||
end(null, '0x01');
|
||||
}
|
||||
},
|
||||
}));
|
||||
provider.start();
|
||||
|
||||
const payload = {
|
||||
jsonrpc: '2.0',
|
||||
method: 'eth_getTransactionCount',
|
||||
params: ['0x0', 'pending'],
|
||||
id: 1,
|
||||
};
|
||||
|
||||
const response = await promisify(provider.sendAsync, provider)(payload);
|
||||
expect(response.result).to.be.eq('0x01');
|
||||
const secondResponse = await promisify(provider.sendAsync, provider)(payload);
|
||||
expect(secondResponse.result).to.be.eq('0x01');
|
||||
});
|
||||
it('does not cache the result for latest transaction count', async () => {
|
||||
provider = new Web3ProviderEngine();
|
||||
let called = false;
|
||||
const nonceTrackerSubprovider = new NonceTrackerSubprovider();
|
||||
provider.addProvider(nonceTrackerSubprovider);
|
||||
provider.addProvider(new FixtureSubprovider({
|
||||
'eth_getBlockByNumber': '0x01',
|
||||
'eth_getTransactionCount': (data: any, next: any, end: any) => {
|
||||
if (called) {
|
||||
return end(null, '0x99');
|
||||
} else {
|
||||
called = true;
|
||||
end(null, '0x01');
|
||||
}
|
||||
},
|
||||
}));
|
||||
provider.start();
|
||||
|
||||
const payload = {
|
||||
jsonrpc: '2.0',
|
||||
method: 'eth_getTransactionCount',
|
||||
params: ['0x0', 'latest'],
|
||||
id: 1,
|
||||
};
|
||||
|
||||
const response = await promisify(provider.sendAsync, provider)(payload);
|
||||
expect(response.result).to.be.eq('0x01');
|
||||
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();
|
||||
let called = false;
|
||||
const nonceTrackerSubprovider = new NonceTrackerSubprovider();
|
||||
provider.addProvider(nonceTrackerSubprovider);
|
||||
provider.addProvider(new FixtureSubprovider({
|
||||
'eth_getBlockByNumber': '0x01',
|
||||
'eth_getTransactionCount': (data: any, next: any, end: any) => {
|
||||
if (called) {
|
||||
return end(null, '0x99');
|
||||
} else {
|
||||
called = true;
|
||||
end(null, '0x01');
|
||||
}
|
||||
},
|
||||
'eth_sendRawTransaction': (data: any, next: any, end: any) => {
|
||||
end(new Error('Transaction nonce is too low'));
|
||||
},
|
||||
}));
|
||||
provider.start();
|
||||
|
||||
const noncePayload = {
|
||||
jsonrpc: '2.0',
|
||||
method: 'eth_getTransactionCount',
|
||||
params: ['0x1f36f546477cda21bf2296c50976f2740247906f', 'pending'],
|
||||
id: 1,
|
||||
};
|
||||
const txParams = [
|
||||
'0x',
|
||||
'0x09184e72a000',
|
||||
'0x2710',
|
||||
'0x0000000000000000000000000000000000000000',
|
||||
'0x',
|
||||
'0x7f7465737432000000000000000000000000000000000000000000000000000000600057',
|
||||
'0x1c',
|
||||
'0x5e1d3a76fbf824220eafc8c79ad578ad2b67d01b0c2425eb1f1347e8f50882ab',
|
||||
'0x5bd428537f05f9830e93792f90ea6a3e2d1ee84952dd96edbae9f658f831ab13'
|
||||
];
|
||||
|
||||
const transaction = new EthereumTx(txParams);
|
||||
const txPayload = {
|
||||
jsonrpc: '2.0',
|
||||
method: 'eth_sendRawTransaction',
|
||||
params: [transaction.serialize()],
|
||||
id: 1,
|
||||
};
|
||||
|
||||
const response = await promisify(provider.sendAsync, provider)(noncePayload);
|
||||
expect(response.result).to.be.eq('0x01');
|
||||
const secondResponse = await promisify(provider.sendAsync, provider)(noncePayload);
|
||||
expect(secondResponse.result).to.be.eq('0x01');
|
||||
try {
|
||||
const txResponse = 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', async () => {
|
||||
provider = new Web3ProviderEngine();
|
||||
let called = false;
|
||||
const nonceTrackerSubprovider = new NonceTrackerSubprovider();
|
||||
provider.addProvider(nonceTrackerSubprovider);
|
||||
provider.addProvider(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');
|
||||
}
|
||||
},
|
||||
'eth_sendRawTransaction': (data: any, next: any, end: any) => {
|
||||
end(null);
|
||||
},
|
||||
}));
|
||||
provider.start();
|
||||
|
||||
const noncePayload = {
|
||||
jsonrpc: '2.0',
|
||||
method: 'eth_getTransactionCount',
|
||||
params: ['0x1f36f546477cda21bf2296c50976f2740247906f', 'pending'],
|
||||
id: 1,
|
||||
};
|
||||
const txParams = [
|
||||
'0x',
|
||||
'0x09184e72a000',
|
||||
'0x2710',
|
||||
'0x0000000000000000000000000000000000000000',
|
||||
'0x',
|
||||
'0x7f7465737432000000000000000000000000000000000000000000000000000000600057',
|
||||
'0x1c',
|
||||
'0x5e1d3a76fbf824220eafc8c79ad578ad2b67d01b0c2425eb1f1347e8f50882ab',
|
||||
'0x5bd428537f05f9830e93792f90ea6a3e2d1ee84952dd96edbae9f658f831ab13'
|
||||
];
|
||||
|
||||
const transaction = new EthereumTx(txParams);
|
||||
const txPayload = {
|
||||
jsonrpc: '2.0',
|
||||
method: 'eth_sendRawTransaction',
|
||||
params: [transaction.serialize()],
|
||||
id: 1,
|
||||
};
|
||||
|
||||
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');
|
||||
const txResponse = await promisify(provider.sendAsync, provider)(txPayload);
|
||||
const thirdResponse = await promisify(provider.sendAsync, provider)(noncePayload);
|
||||
expect(thirdResponse.result).to.be.eq('0x01');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user