Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 40 additions & 25 deletions src/NonceTracker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@ import assert from 'assert';
import { Mutex } from 'async-mutex';
import type { PollingBlockTracker } from '@metamask/eth-block-tracker';

// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports
const { Web3Provider } = require('@ethersproject/providers');
import { Web3Provider } from '@ethersproject/providers';

/**
* @property opts.provider - An ethereum provider
* @property opts.blockTracker - An instance of eth-block-tracker
* @property opts.blockTracker - An instance of @metamask/eth-block-tracker
* @property opts.getPendingTransactions - A function that returns an array of txMeta
* whose status is `submitted`
* @property opts.getConfirmedTransactions - A function that returns an array of txMeta
Expand Down Expand Up @@ -89,25 +88,42 @@ export interface Transaction {
}

export class NonceTracker {
private provider: Record<string, unknown>;
#provider: Record<string, unknown>;

private blockTracker: PollingBlockTracker;
#blockTracker: PollingBlockTracker;

private web3: typeof Web3Provider;
readonly #getPendingTransactions: (address: string) => Transaction[];

private getPendingTransactions: (address: string) => Transaction[];
readonly #getConfirmedTransactions: (address: string) => Transaction[];

private getConfirmedTransactions: (address: string) => Transaction[];

private lockMap: Record<string, Mutex>;
readonly #lockMap: Record<string, Mutex>;

constructor(opts: NonceTrackerOptions) {
this.provider = opts.provider;
this.blockTracker = opts.blockTracker;
this.web3 = new Web3Provider(opts.provider);
this.getPendingTransactions = opts.getPendingTransactions;
this.getConfirmedTransactions = opts.getConfirmedTransactions;
this.lockMap = {};
this.#provider = opts.provider;
this.#blockTracker = opts.blockTracker;
this.#getPendingTransactions = opts.getPendingTransactions;
this.#getConfirmedTransactions = opts.getConfirmedTransactions;
this.#lockMap = {};
}

/**
* Allows changing the provider and block tracker after insrtantiation. As the blockTracker also has a provider, they are updated atomically.
*
* @param opts - new props
* @param opts.provider - An ethereum provider
* @param opts.blockTracker - An instance of @metamask/eth-block-tracker
*/
setProvider({
Copy link
Contributor Author

@legobeat legobeat May 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am very easily convinced to remove the setProvider function alltogether, if there is no current use for it. Put it in to make sure there are no compatibility concerns by default.

provider,
blockTracker,
}: {
provider: Record<string, unknown>;
blockTracker: PollingBlockTracker;
}): void {
assert(typeof provider === 'object', 'missing or invalid provider');
assert(typeof blockTracker === 'object', 'missing or invalid blockTracker');
this.#provider = provider;
this.#blockTracker = blockTracker;
}

/**
Expand Down Expand Up @@ -144,7 +160,7 @@ export class NonceTracker {
highestLocallyConfirmed,
);

const pendingTxs: Transaction[] = this.getPendingTransactions(address);
const pendingTxs: Transaction[] = this.#getPendingTransactions(address);
const localNonceResult: HighestContinuousFrom =
this._getHighestContinuousFrom(pendingTxs, highestSuggested);

Expand Down Expand Up @@ -189,10 +205,10 @@ export class NonceTracker {
}

_lookupMutex(lockId: string): Mutex {
let mutex: Mutex = this.lockMap[lockId];
let mutex: Mutex = this.#lockMap[lockId];
if (!mutex) {
mutex = new Mutex();
this.lockMap[lockId] = mutex;
this.#lockMap[lockId] = mutex;
}
return mutex;
}
Expand All @@ -208,11 +224,10 @@ export class NonceTracker {
// calculate next nonce
// we need to make sure our base count
// and pending count are from the same block
const blockNumber = await this.blockTracker.getLatestBlock();
const baseCount: number = await this.web3.getTransactionCount(
address,
blockNumber,
);
const blockNumber = await this.#blockTracker.getLatestBlock();
const baseCount: number = await new Web3Provider(
this.#provider,
).getTransactionCount(address, blockNumber);
Comment on lines -211 to +230

This comment was marked as off-topic.

assert(
Number.isInteger(baseCount),
`nonce-tracker - baseCount is not an integer - got: (${typeof baseCount}) "${baseCount}"`,
Expand All @@ -231,7 +246,7 @@ export class NonceTracker {
*/
_getHighestLocallyConfirmed(address: string): number {
const confirmedTransactions: Transaction[] =
this.getConfirmedTransactions(address);
this.#getConfirmedTransactions(address);
const highest: number = this._getHighestNonce(confirmedTransactions);
return Number.isInteger(highest) ? highest + 1 : 0;
}
Expand Down
48 changes: 40 additions & 8 deletions test/nonce-tracker-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ const assert = require('assert');
const { NonceTracker } = require('../dist');
const MockTxGen = require('./lib/mock-tx-gen');

const providerResultStub = {};

describe('Nonce Tracker', function () {
let nonceTracker, pendingTxs, confirmedTxs;

Expand Down Expand Up @@ -199,6 +197,30 @@ describe('Nonce Tracker', function () {
});
});

describe('when switching provider', function () {
it('should discard previous nonce tracking', async function () {
const txGen = new MockTxGen();
pendingTxs = txGen.generate({ status: 'submitted' }, { count: 2 });
nonceTracker = generateNonceTrackerWith(pendingTxs, [], '0x07');

this.timeout(15000);
nonceTracker.setProvider({
provider: generateProviderWith('0x22'),
blockTracker: generateBlockTracker(),
});
this.timeout(15000);
const nonceLock = await nonceTracker.getNonceLock(
'0x7d3517b0d011698406d6e0aed8453f0be2697926',
);
assert.equal(
nonceLock.nextNonce,
'0x22',
`nonce should be 0x22 got ${nonceLock.nextNonce}`,
);
await nonceLock.releaseLock();
});
});

describe('when there are some pending nonces below the remote one and some over.', function () {
it('should return nonce after those', async function () {
const txGen = new MockTxGen();
Expand Down Expand Up @@ -299,19 +321,29 @@ describe('Nonce Tracker', function () {
});
});

function generateNonceTrackerWith(pending, confirmed, providerStub = '0x0') {
const getPendingTransactions = () => pending;
const getConfirmedTransactions = () => confirmed;
providerResultStub.result = providerStub;
const provider = {
function generateProviderWith(nonce) {
const providerResultStub = {};

providerResultStub.result = nonce;
return {
sendAsync: (_, cb) => {
cb(undefined, providerResultStub);
},
};
const blockTracker = {
}

function generateBlockTracker() {
return {
getCurrentBlock: () => '0x11b568',
getLatestBlock: async () => '0x11b568',
};
}

function generateNonceTrackerWith(pending, confirmed, providerStub = '0x0') {
const getPendingTransactions = () => pending;
const getConfirmedTransactions = () => confirmed;
const provider = generateProviderWith(providerStub);
const blockTracker = generateBlockTracker();
return new NonceTracker({
provider,
blockTracker,
Expand Down