Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
18 changes: 17 additions & 1 deletion packages/relay/src/lib/eth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { EventEmitter } from 'events';
import { Logger } from 'pino';
import { RedisClientType } from 'redis';

import { Eth } from '../index';
import { MirrorNodeClient } from './clients';
Expand All @@ -19,13 +20,16 @@ import {
IBlockService,
ICommonService,
IContractService,
LocalPendingTransactionStorage,
TransactionPoolService,
TransactionService,
} from './services';
import type { CacheService } from './services/cacheService/cacheService';
import { FeeService } from './services/ethService/feeService/FeeService';
import { IFeeService } from './services/ethService/feeService/IFeeService';
import { ITransactionService } from './services/ethService/transactionService/ITransactionService';
import HAPIService from './services/hapiService/hapiService';
import { RedisPendingTransactionStorage } from './services/transactionPoolService/RedisPendingTransactionStorage';
import {
IContractCallRequest,
IFeeHistory,
Expand Down Expand Up @@ -123,6 +127,7 @@ export class EthImpl implements Eth {
logger: Logger,
chain: string,
public readonly cacheService: CacheService,
public readonly redisClient: RedisClientType | undefined,
) {
this.chain = chain;
this.logger = logger;
Expand All @@ -132,8 +137,11 @@ export class EthImpl implements Eth {
this.filterService = new FilterService(mirrorNodeClient, logger, cacheService, this.common);
this.feeService = new FeeService(mirrorNodeClient, this.common, logger);
this.contractService = new ContractService(cacheService, this.common, hapiService, logger, mirrorNodeClient);
this.accountService = new AccountService(cacheService, this.common, logger, mirrorNodeClient);
this.blockService = new BlockService(cacheService, chain, this.common, mirrorNodeClient, logger);
const storage = this.redisClient
? new RedisPendingTransactionStorage(this.redisClient)
: new LocalPendingTransactionStorage();
const transactionPoolService = new TransactionPoolService(storage, logger);
this.transactionService = new TransactionService(
cacheService,
chain,
Expand All @@ -142,6 +150,14 @@ export class EthImpl implements Eth {
hapiService,
logger,
mirrorNodeClient,
transactionPoolService,
);
this.accountService = new AccountService(
cacheService,
this.common,
logger,
mirrorNodeClient,
transactionPoolService,
);
}

Expand Down
28 changes: 18 additions & 10 deletions packages/relay/src/lib/precheck.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { prepend0x } from '../formatters';
import { MirrorNodeClient } from './clients';
import constants from './constants';
import { JsonRpcError, predefined } from './errors/JsonRpcError';
import { CommonService } from './services';
import { CommonService, TransactionPoolService } from './services';
import { RequestDetails } from './types';

/**
Expand All @@ -18,17 +18,24 @@ export class Precheck {
private readonly mirrorNodeClient: MirrorNodeClient;
private readonly chain: string;
private readonly logger: Logger;
private readonly transactionPoolService: TransactionPoolService;

/**
* Creates an instance of Precheck.
* @param {MirrorNodeClient} mirrorNodeClient - The MirrorNodeClient instance.
* @param {Logger} logger - The logger instance.
* @param {string} chainId - The chain ID.
*/
constructor(mirrorNodeClient: MirrorNodeClient, logger: Logger, chainId: string) {
constructor(
mirrorNodeClient: MirrorNodeClient,
logger: Logger,
chainId: string,
transactionPoolService: TransactionPoolService,
) {
this.mirrorNodeClient = mirrorNodeClient;
this.logger = logger;
this.chain = chainId;
this.transactionPoolService = transactionPoolService;
}

/**
Expand Down Expand Up @@ -70,7 +77,10 @@ export class Precheck {
this.transactionType(parsedTx);
this.gasLimit(parsedTx);
const mirrorAccountInfo = await this.verifyAccount(parsedTx, requestDetails);
this.nonce(parsedTx, mirrorAccountInfo.ethereum_nonce);
this.nonce(
parsedTx,
mirrorAccountInfo.ethereum_nonce + (await this.transactionPoolService.getPendingCount(parsedTx.from!)),
);
this.chainId(parsedTx);
this.value(parsedTx);
this.gasPrice(parsedTx, networkGasPriceInWeiBars);
Expand Down Expand Up @@ -105,17 +115,15 @@ export class Precheck {
/**
* Checks the nonce of the transaction.
* @param tx - The transaction.
* @param accountInfoNonce - The nonce of the account.
* @param signerNonce - The nonce of the account.
*/
nonce(tx: Transaction, accountInfoNonce: number): void {
nonce(tx: Transaction, signerNonce: number): void {
if (this.logger.isLevelEnabled('trace')) {
this.logger.trace(
`Nonce precheck for sendRawTransaction(tx.nonce=${tx.nonce}, accountInfoNonce=${accountInfoNonce})`,
);
this.logger.trace(`Nonce precheck for sendRawTransaction(tx.nonce=${tx.nonce}, signerNonce=${signerNonce})`);
}

if (accountInfoNonce > tx.nonce) {
throw predefined.NONCE_TOO_LOW(tx.nonce, accountInfoNonce);
if (signerNonce > tx.nonce) {
throw predefined.NONCE_TOO_LOW(tx.nonce, signerNonce);
}
}

Expand Down
1 change: 1 addition & 0 deletions packages/relay/src/lib/relay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ export class Relay {
logger.child({ name: 'relay-eth' }),
chainId,
this.cacheService,
this.redisClient,
);

(this.ethImpl as EthImpl).eventEmitter.on('eth_execution', (args: IEthExecutionEventPayload) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { JsonRpcError, predefined } from '../../../errors/JsonRpcError';
import { RequestDetails } from '../../../types';
import { LatestBlockNumberTimestamp } from '../../../types/mirrorNode';
import { CacheService } from '../../cacheService/cacheService';
import { TransactionPoolService } from '../../transactionPoolService/transactionPoolService';
import { ICommonService } from '../ethCommonService/ICommonService';
import { IAccountService } from './IAccountService';

Expand Down Expand Up @@ -70,18 +71,31 @@ export class AccountService implements IAccountService {
*/
private readonly mirrorNodeClient: MirrorNodeClient;

/**
* The interface through which we interact with the transaction pool.
* @private
*/
private readonly transactionPoolService: TransactionPoolService;

/**
* @constructor
* @param cacheService
* @param common
* @param logger
* @param mirrorNodeClient
*/
constructor(cacheService: CacheService, common: ICommonService, logger: Logger, mirrorNodeClient: MirrorNodeClient) {
constructor(
cacheService: CacheService,
common: ICommonService,
logger: Logger,
mirrorNodeClient: MirrorNodeClient,
transactionPoolService: TransactionPoolService,
) {
this.cacheService = cacheService;
this.common = common;
this.logger = logger;
this.mirrorNodeClient = mirrorNodeClient;
this.transactionPoolService = transactionPoolService;
}

/**
Expand Down Expand Up @@ -303,8 +317,11 @@ export class AccountService implements IAccountService {
// previewnet and testnet bug have a genesis blockNumber of 1 but non system account were yet to be created
return constants.ZERO_HEX;
} else if (this.common.blockTagIsLatestOrPending(blockNumOrTag)) {
// if latest or pending, get latest ethereumNonce from mirror node account API
return await this.getAccountLatestEthereumNonce(address, requestDetails);
const mnNonce = await this.getAccountLatestEthereumNonce(address, requestDetails);
if (blockNumOrTag == constants.BLOCK_PENDING) {
return numberTo0x(Number(mnNonce) + (await this.transactionPoolService.getPendingCount(address)));
}
return mnNonce;
} else if (blockNumOrTag === constants.BLOCK_EARLIEST) {
return await this.getAccountNonceForEarliestBlock(requestDetails);
} else if (!isNaN(blockNum) && blockNumOrTag.length != constants.BLOCK_HASH_LENGTH && blockNum > 0) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { Precheck } from '../../../precheck';
import { ITransactionReceipt, RequestDetails, TypedEvents } from '../../../types';
import { CacheService } from '../../cacheService/cacheService';
import HAPIService from '../../hapiService/hapiService';
import { ICommonService } from '../../index';
import { ICommonService, TransactionPoolService } from '../../index';
import { ITransactionService } from './ITransactionService';

export class TransactionService implements ITransactionService {
Expand Down Expand Up @@ -66,6 +66,8 @@ export class TransactionService implements ITransactionService {
*/
private readonly precheck: Precheck;

private readonly transactionPoolService: TransactionPoolService;

/**
* The ID of the chain, as a hex string, as it would be returned in a JSON-RPC call.
* @private
Expand All @@ -83,6 +85,7 @@ export class TransactionService implements ITransactionService {
hapiService: HAPIService,
logger: Logger,
mirrorNodeClient: MirrorNodeClient,
transactionPoolService: TransactionPoolService,
) {
this.cacheService = cacheService;
this.chain = chain;
Expand All @@ -91,7 +94,8 @@ export class TransactionService implements ITransactionService {
this.hapiService = hapiService;
this.logger = logger;
this.mirrorNodeClient = mirrorNodeClient;
this.precheck = new Precheck(mirrorNodeClient, logger, chain);
this.precheck = new Precheck(mirrorNodeClient, logger, chain, transactionPoolService);
this.transactionPoolService = transactionPoolService;
}

/**
Expand Down Expand Up @@ -455,6 +459,15 @@ export class TransactionService implements ITransactionService {
return input.startsWith(constants.EMPTY_HEX) ? input.substring(2) : input;
}

/**
* Narrows an ethers Transaction to one that definitely has a non-null hash.
*/
private assertSignedTransaction(tx: EthersTransaction): asserts tx is EthersTransaction & { hash: string } {
if (tx.hash == null) {
throw predefined.INVALID_ARGUMENTS('Expected a signed transaction with a non-null hash');
}
}

/**
* Asynchronously processes a raw transaction by submitting it to the network, managing HFS, polling the MN, handling errors, and returning the transaction hash.
*
Expand All @@ -471,6 +484,9 @@ export class TransactionService implements ITransactionService {
networkGasPriceInWeiBars: number,
requestDetails: RequestDetails,
): Promise<string | JsonRpcError> {
// although we validdate on earlier stages that we have
// a signed transaction, we need to assert it again here in order to satisfy the type checker
this.assertSignedTransaction(parsedTx);
let sendRawTransactionError: any;

const originalCallerAddress = parsedTx.from?.toString() || '';
Expand All @@ -479,6 +495,9 @@ export class TransactionService implements ITransactionService {
method: constants.ETH_SEND_RAW_TRANSACTION,
});

// Save the transaction to the transaction pool before submitting it to the network
await this.transactionPoolService.saveTransaction(originalCallerAddress, parsedTx);

const { txSubmitted, submittedTransactionId, error } = await this.submitTransaction(
transactionBuffer,
originalCallerAddress,
Expand Down Expand Up @@ -508,6 +527,9 @@ export class TransactionService implements ITransactionService {
this.mirrorNodeClient.getMirrorNodeRequestRetryCount(),
);

// Remove the transaction from the transaction pool after successful submission
await this.transactionPoolService.removeTransaction(originalCallerAddress, contractResult.hash);

if (!contractResult) {
if (
sendRawTransactionError instanceof SDKClientError &&
Expand Down Expand Up @@ -536,6 +558,9 @@ export class TransactionService implements ITransactionService {
}
}

// Remove the transaction from the transaction pool after unsuccessful submission
await this.transactionPoolService.removeTransaction(originalCallerAddress, parsedTx.hash);

// If this point is reached, it means that no valid transaction hash was returned. Therefore, an error must have occurred.
return await this.sendRawTransactionErrorHandler(
sendRawTransactionError,
Expand Down
8 changes: 5 additions & 3 deletions packages/relay/tests/lib/eth/eth_getTransactionCount.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import MockAdapter from 'axios-mock-adapter';
import { expect, use } from 'chai';
import chaiAsPromised from 'chai-as-promised';
import sinon from 'sinon';
import sinon, { stub } from 'sinon';

import { Eth, predefined } from '../../../src';
import { numberTo0x } from '../../../src/formatters';
Expand Down Expand Up @@ -141,11 +141,13 @@ describe('@ethGetTransactionCount eth_getTransactionCount spec', async function
expect(nonce).to.equal(numberTo0x(mockData.account.ethereum_nonce));
});

it('should return latest nonce for pending block', async () => {
it('should return pending nonce for pending block', async () => {
const pendingTxs: number = 2;
stub(ethImpl['accountService']['transactionPoolService'], 'getPendingCount').returns(pendingTxs);
restMock.onGet(accountPath).reply(200, JSON.stringify(mockData.account));
const nonce = await ethImpl.getTransactionCount(MOCK_ACCOUNT_ADDR, constants.BLOCK_PENDING, requestDetails);
expect(nonce).to.exist;
expect(nonce).to.equal(numberTo0x(mockData.account.ethereum_nonce));
expect(nonce).to.equal(numberTo0x(mockData.account.ethereum_nonce + pendingTxs));
});

it('should return 0x0 nonce for earliest block with valid block', async () => {
Expand Down
4 changes: 2 additions & 2 deletions packages/relay/tests/lib/precheck.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -605,7 +605,7 @@ describe('Precheck', async function () {
mock.onGet(`accounts/${parsedTx.from}${limitOrderPostFix}`).reply(200, JSON.stringify(mirrorAccount));

try {
precheck.nonce(parsedTx, mirrorAccount.ethereum_nonce, requestDetails);
precheck.nonce(parsedTx, mirrorAccount.ethereum_nonce);
expectedError();
} catch (e: any) {
expect(e).to.eql(predefined.NONCE_TOO_LOW(parsedTx.nonce, mirrorAccount.ethereum_nonce));
Expand All @@ -622,7 +622,7 @@ describe('Precheck', async function () {

mock.onGet(`accounts/${parsedTx.from}${limitOrderPostFix}`).reply(200, JSON.stringify(mirrorAccount));

precheck.nonce(parsedTx, mirrorAccount.ethereum_nonce, requestDetails);
precheck.nonce(parsedTx, mirrorAccount.ethereum_nonce);
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import * as sinon from 'sinon';

import { LocalPendingTransactionStorage } from '../../../../src/lib/services/transactionPoolService/LocalPendingTransactionStorage';

describe.only('LocalPendingTransactionStorage Test Suite', function () {
describe('LocalPendingTransactionStorage Test Suite', function () {
let storage: LocalPendingTransactionStorage;

const testAddress1 = '0x742d35cc6db9027d0e0ba7d3c9e5a96f';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { TransactionPoolService } from '../../../../src/lib/services/transaction
import { IExecuteTransactionEventPayload } from '../../../../src/lib/types/events';
import { AddToListResult, PendingTransactionStorage } from '../../../../src/lib/types/transactionPool';

describe.only('TransactionPoolService Test Suite', function () {
describe('TransactionPoolService Test Suite', function () {
this.timeout(10000);

let logger: Logger;
Expand Down
Loading