Skip to content

Commit 02ba6bd

Browse files
feat: updates sendrawtransaction methods (#4444)
Signed-off-by: Konstantina Blazhukova <[email protected]>
1 parent a7125e1 commit 02ba6bd

File tree

7 files changed

+100
-123
lines changed

7 files changed

+100
-123
lines changed

packages/relay/src/lib/eth.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import { EventEmitter } from 'events';
44
import { Logger } from 'pino';
5+
import { RedisClientType } from 'redis';
56

67
import { Eth } from '../index';
78
import { MirrorNodeClient } from './clients';
@@ -19,13 +20,16 @@ import {
1920
IBlockService,
2021
ICommonService,
2122
IContractService,
23+
LocalPendingTransactionStorage,
24+
TransactionPoolService,
2225
TransactionService,
2326
} from './services';
2427
import type { CacheService } from './services/cacheService/cacheService';
2528
import { FeeService } from './services/ethService/feeService/FeeService';
2629
import { IFeeService } from './services/ethService/feeService/IFeeService';
2730
import { ITransactionService } from './services/ethService/transactionService/ITransactionService';
2831
import HAPIService from './services/hapiService/hapiService';
32+
import { RedisPendingTransactionStorage } from './services/transactionPoolService/RedisPendingTransactionStorage';
2933
import {
3034
IContractCallRequest,
3135
IFeeHistory,
@@ -123,6 +127,7 @@ export class EthImpl implements Eth {
123127
logger: Logger,
124128
chain: string,
125129
public readonly cacheService: CacheService,
130+
public readonly redisClient: RedisClientType | undefined,
126131
) {
127132
this.chain = chain;
128133
this.logger = logger;
@@ -134,6 +139,10 @@ export class EthImpl implements Eth {
134139
this.contractService = new ContractService(cacheService, this.common, hapiService, logger, mirrorNodeClient);
135140
this.accountService = new AccountService(cacheService, this.common, logger, mirrorNodeClient);
136141
this.blockService = new BlockService(cacheService, chain, this.common, mirrorNodeClient, logger);
142+
const storage = this.redisClient
143+
? new RedisPendingTransactionStorage(this.redisClient)
144+
: new LocalPendingTransactionStorage();
145+
const transactionPoolService = new TransactionPoolService(storage, logger);
137146
this.transactionService = new TransactionService(
138147
cacheService,
139148
chain,
@@ -142,6 +151,7 @@ export class EthImpl implements Eth {
142151
hapiService,
143152
logger,
144153
mirrorNodeClient,
154+
transactionPoolService,
145155
);
146156
}
147157

packages/relay/src/lib/relay.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,7 @@ export class Relay {
192192
logger.child({ name: 'relay-eth' }),
193193
chainId,
194194
this.cacheService,
195+
this.redisClient,
195196
);
196197

197198
(this.ethImpl as EthImpl).eventEmitter.on('eth_execution', (args: IEthExecutionEventPayload) => {
@@ -200,7 +201,6 @@ export class Relay {
200201

201202
hapiService.eventEmitter.on('execute_transaction', (args: IExecuteTransactionEventPayload) => {
202203
this.metricService.captureTransactionMetrics(args).then();
203-
// TODO: Call transactionPoolService.onConsensusResult(args) when service is available
204204
});
205205

206206
hapiService.eventEmitter.on('execute_query', (args: IExecuteQueryEventPayload) => {

packages/relay/src/lib/services/ethService/transactionService/TransactionService.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { Precheck } from '../../../precheck';
2121
import { ITransactionReceipt, RequestDetails, TypedEvents } from '../../../types';
2222
import { CacheService } from '../../cacheService/cacheService';
2323
import HAPIService from '../../hapiService/hapiService';
24-
import { ICommonService } from '../../index';
24+
import { ICommonService, TransactionPoolService } from '../../index';
2525
import { ITransactionService } from './ITransactionService';
2626

2727
export class TransactionService implements ITransactionService {
@@ -66,6 +66,8 @@ export class TransactionService implements ITransactionService {
6666
*/
6767
private readonly precheck: Precheck;
6868

69+
private readonly transactionPoolService: TransactionPoolService;
70+
6971
/**
7072
* The ID of the chain, as a hex string, as it would be returned in a JSON-RPC call.
7173
* @private
@@ -83,6 +85,7 @@ export class TransactionService implements ITransactionService {
8385
hapiService: HAPIService,
8486
logger: Logger,
8587
mirrorNodeClient: MirrorNodeClient,
88+
transactionPoolService: TransactionPoolService,
8689
) {
8790
this.cacheService = cacheService;
8891
this.chain = chain;
@@ -92,6 +95,7 @@ export class TransactionService implements ITransactionService {
9295
this.logger = logger;
9396
this.mirrorNodeClient = mirrorNodeClient;
9497
this.precheck = new Precheck(mirrorNodeClient, logger, chain);
98+
this.transactionPoolService = transactionPoolService;
9599
}
96100

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

462+
/**
463+
* Narrows an ethers Transaction to one that definitely has a non-null hash.
464+
*/
465+
private assertSignedTransaction(tx: EthersTransaction): asserts tx is EthersTransaction & { hash: string } {
466+
if (tx.hash == null) {
467+
throw predefined.INVALID_ARGUMENTS('Expected a signed transaction with a non-null hash');
468+
}
469+
}
470+
458471
/**
459472
* Asynchronously processes a raw transaction by submitting it to the network, managing HFS, polling the MN, handling errors, and returning the transaction hash.
460473
*
@@ -471,10 +484,16 @@ export class TransactionService implements ITransactionService {
471484
networkGasPriceInWeiBars: number,
472485
requestDetails: RequestDetails,
473486
): Promise<string | JsonRpcError> {
487+
// although we validate in earlier stages that we have
488+
// a signed transaction, we need to assert it again here in order to satisfy the type checker
489+
this.assertSignedTransaction(parsedTx);
474490
let sendRawTransactionError: any;
475491

476492
const originalCallerAddress = parsedTx.from?.toString() || '';
477493

494+
// Save the transaction to the transaction pool before submitting it to the network
495+
await this.transactionPoolService.saveTransaction(originalCallerAddress, parsedTx);
496+
478497
this.eventEmitter.emit('eth_execution', {
479498
method: constants.ETH_SEND_RAW_TRANSACTION,
480499
});
@@ -530,12 +549,18 @@ export class TransactionService implements ITransactionService {
530549
);
531550
}
532551

552+
// Remove the transaction from the transaction pool after successful submission
553+
await this.transactionPoolService.removeTransaction(originalCallerAddress, contractResult.hash);
554+
533555
return contractResult.hash;
534556
} catch (e: any) {
535557
sendRawTransactionError = e;
536558
}
537559
}
538560

561+
// Remove the transaction from the transaction pool after unsuccessful submission
562+
await this.transactionPoolService.removeTransaction(originalCallerAddress, parsedTx.hash);
563+
539564
// If this point is reached, it means that no valid transaction hash was returned. Therefore, an error must have occurred.
540565
return await this.sendRawTransactionErrorHandler(
541566
sendRawTransactionError,

packages/relay/src/lib/services/transactionPoolService/transactionPoolService.ts

Lines changed: 6 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,13 @@ export class TransactionPoolService implements ITransactionPoolService {
4848
*/
4949
async saveTransaction(address: string, tx: Transaction): Promise<void> {
5050
const txHash = tx.hash;
51+
const addressLowerCased = address.toLowerCase();
5152

5253
if (!txHash) {
5354
throw new Error('Transaction hash is required for storage');
5455
}
5556

56-
const result = await this.storage.addToList(address, txHash);
57+
const result = await this.storage.addToList(addressLowerCased, txHash);
5758

5859
if (!result.ok) {
5960
throw new Error('Failed to add transaction to list');
@@ -62,33 +63,6 @@ export class TransactionPoolService implements ITransactionPoolService {
6263
this.logger.debug({ address, txHash, pendingCount: result.newValue }, 'Transaction saved to pool');
6364
}
6465

65-
/**
66-
* Handles consensus results and updates the pool state accordingly.
67-
*
68-
* @param payload - The transaction execution event payload containing transaction details.
69-
* @returns A promise that resolves when the consensus result has been processed.
70-
*/
71-
async onConsensusResult(payload: IExecuteTransactionEventPayload): Promise<void> {
72-
const { transactionHash, originalCallerAddress, transactionId } = payload;
73-
74-
if (!transactionHash) {
75-
this.logger.warn({ transactionId }, 'Transaction hash not available in execution event');
76-
return;
77-
}
78-
79-
const remainingCount = await this.removeTransaction(originalCallerAddress, transactionHash);
80-
81-
this.logger.debug(
82-
{
83-
transactionHash,
84-
address: originalCallerAddress,
85-
transactionId,
86-
remainingCount,
87-
},
88-
'Transaction removed from pool after consensus',
89-
);
90-
}
91-
9266
/**
9367
* Removes a specific transaction from the pending pool.
9468
* This is typically called when a transaction is confirmed or fails on the consensus layer.
@@ -98,7 +72,8 @@ export class TransactionPoolService implements ITransactionPoolService {
9872
* @returns A promise that resolves to the new pending transaction count for the address.
9973
*/
10074
async removeTransaction(address: string, txHash: string): Promise<number> {
101-
return await this.storage.removeFromList(address, txHash);
75+
const addressLowerCased = address.toLowerCase();
76+
return await this.storage.removeFromList(addressLowerCased, txHash);
10277
}
10378

10479
/**
@@ -108,7 +83,8 @@ export class TransactionPoolService implements ITransactionPoolService {
10883
* @returns A promise that resolves to the number of pending transactions.
10984
*/
11085
async getPendingCount(address: string): Promise<number> {
111-
return await this.storage.getList(address);
86+
const addressLowerCased = address.toLowerCase();
87+
return await this.storage.getList(addressLowerCased);
11288
}
11389

11490
/**

packages/relay/src/lib/types/transactionPool.ts

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22

33
import { Transaction } from 'ethers';
44

5-
import { IExecuteTransactionEventPayload } from './events';
6-
75
/**
86
* Result of attempting to add a transaction to the pending list.
97
*
@@ -26,14 +24,6 @@ export interface TransactionPoolService {
2624
*/
2725
saveTransaction(address: string, tx: Transaction): Promise<void>;
2826

29-
/**
30-
* Handles consensus results and updates the pool state accordingly.
31-
*
32-
* @param payload - The transaction execution event payload containing transaction details.
33-
* @returns A promise that resolves when the consensus result has been received.
34-
*/
35-
onConsensusResult(payload: IExecuteTransactionEventPayload): Promise<void>;
36-
3727
/**
3828
* Retrieves the number of pending transactions for a given address.
3929
*

packages/relay/tests/lib/eth/eth_sendRawTransaction.spec.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,34 @@ describe('@ethSendRawTransaction eth_sendRawTransaction spec', async function ()
288288
);
289289
});
290290

291+
it('should save and remove transaction from transaction pool on success path', async function () {
292+
const signed = await signTransaction(transaction);
293+
const txPool = ethImpl['transactionService']['transactionPoolService'] as any;
294+
295+
const saveStub = sinon.stub(txPool, 'saveTransaction').resolves();
296+
const removeStub = sinon.stub(txPool, 'removeTransaction').resolves();
297+
298+
restMock.onGet(contractResultEndpoint).reply(200, JSON.stringify({ hash: ethereumHash }));
299+
sdkClientStub.submitEthereumTransaction.resolves({
300+
txResponse: {
301+
transactionId: TransactionId.fromString(transactionIdServicesFormat),
302+
} as unknown as TransactionResponse,
303+
fileId: null,
304+
});
305+
306+
const result = await ethImpl.sendRawTransaction(signed, requestDetails);
307+
308+
expect(result).to.equal(ethereumHash);
309+
sinon.assert.calledOnce(saveStub);
310+
sinon.assert.calledWithMatch(saveStub, accountAddress, sinon.match.object);
311+
312+
sinon.assert.calledOnce(removeStub);
313+
sinon.assert.calledWith(removeStub, accountAddress, ethereumHash);
314+
315+
saveStub.restore();
316+
removeStub.restore();
317+
});
318+
291319
withOverriddenEnvsInMochaTest({ USE_ASYNC_TX_PROCESSING: false }, () => {
292320
it('[USE_ASYNC_TX_PROCESSING=true] should throw internal error when transaction returned from mirror node is null', async function () {
293321
const signed = await signTransaction(transaction);
@@ -307,6 +335,35 @@ describe('@ethSendRawTransaction eth_sendRawTransaction spec', async function ()
307335
expect(`Error invoking RPC: ${response.message}`).to.equal(predefined.INTERNAL_ERROR(response.message).message);
308336
});
309337

338+
it('should save and remove transaction (fallback path uses parsedTx.hash)', async function () {
339+
const signed = await signTransaction(transaction);
340+
const expectedTxHash = Utils.computeTransactionHash(Buffer.from(signed.replace('0x', ''), 'hex'));
341+
const txPool = ethImpl['transactionService']['transactionPoolService'] as any;
342+
343+
const saveStub = sinon.stub(txPool, 'saveTransaction').resolves();
344+
const removeStub = sinon.stub(txPool, 'removeTransaction').resolves();
345+
346+
// Cause MN polling to fail, forcing fallback
347+
restMock.onGet(contractResultEndpoint).reply(404, JSON.stringify(mockData.notFound));
348+
sdkClientStub.submitEthereumTransaction.resolves({
349+
txResponse: {
350+
transactionId: TransactionId.fromString(transactionIdServicesFormat),
351+
} as unknown as TransactionResponse,
352+
fileId: null,
353+
});
354+
355+
await ethImpl.sendRawTransaction(signed, requestDetails);
356+
357+
sinon.assert.calledOnce(saveStub);
358+
sinon.assert.calledWithMatch(saveStub, accountAddress, sinon.match.object);
359+
360+
sinon.assert.calledOnce(removeStub);
361+
sinon.assert.calledWith(removeStub, accountAddress, expectedTxHash);
362+
363+
saveStub.restore();
364+
removeStub.restore();
365+
});
366+
310367
it('[USE_ASYNC_TX_PROCESSING=false] should throw internal error when transactionID is invalid', async function () {
311368
const signed = await signTransaction(transaction);
312369

0 commit comments

Comments
 (0)