Skip to content

Commit 3c5daeb

Browse files
committed
Merge branch 'feat/transaction-pool' into 4394-update-nonce-precheck-method
# Conflicts: # packages/relay/src/lib/eth.ts # packages/relay/src/lib/services/ethService/transactionService/TransactionService.ts
2 parents b769116 + 02ba6bd commit 3c5daeb

File tree

6 files changed

+70
-129
lines changed

6 files changed

+70
-129
lines changed

packages/relay/src/lib/relay.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,6 @@ export class Relay {
201201

202202
hapiService.eventEmitter.on('execute_transaction', (args: IExecuteTransactionEventPayload) => {
203203
this.metricService.captureTransactionMetrics(args).then();
204-
// TODO: Call transactionPoolService.onConsensusResult(args) when service is available
205204
});
206205

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

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

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -484,20 +484,20 @@ export class TransactionService implements ITransactionService {
484484
networkGasPriceInWeiBars: number,
485485
requestDetails: RequestDetails,
486486
): Promise<string | JsonRpcError> {
487-
// although we validdate on earlier stages that we have
487+
// although we validate in earlier stages that we have
488488
// a signed transaction, we need to assert it again here in order to satisfy the type checker
489489
this.assertSignedTransaction(parsedTx);
490490
let sendRawTransactionError: any;
491491

492492
const originalCallerAddress = parsedTx.from?.toString() || '';
493493

494+
// Save the transaction to the transaction pool before submitting it to the network
495+
await this.transactionPoolService.saveTransaction(originalCallerAddress, parsedTx);
496+
494497
this.eventEmitter.emit('eth_execution', {
495498
method: constants.ETH_SEND_RAW_TRANSACTION,
496499
});
497500

498-
// Save the transaction to the transaction pool before submitting it to the network
499-
await this.transactionPoolService.saveTransaction(originalCallerAddress, parsedTx);
500-
501501
const { txSubmitted, submittedTransactionId, error } = await this.submitTransaction(
502502
transactionBuffer,
503503
originalCallerAddress,
@@ -527,9 +527,6 @@ export class TransactionService implements ITransactionService {
527527
this.mirrorNodeClient.getMirrorNodeRequestRetryCount(),
528528
);
529529

530-
// Remove the transaction from the transaction pool after successful submission
531-
await this.transactionPoolService.removeTransaction(originalCallerAddress, contractResult.hash);
532-
533530
if (!contractResult) {
534531
if (
535532
sendRawTransactionError instanceof SDKClientError &&
@@ -552,6 +549,9 @@ export class TransactionService implements ITransactionService {
552549
);
553550
}
554551

552+
// Remove the transaction from the transaction pool after successful submission
553+
await this.transactionPoolService.removeTransaction(originalCallerAddress, contractResult.hash);
554+
555555
return contractResult.hash;
556556
} catch (e: any) {
557557
sendRawTransactionError = e;

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

packages/relay/tests/lib/services/transactionPoolService/transactionPoolService.spec.ts

Lines changed: 0 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -130,87 +130,6 @@ describe('TransactionPoolService Test Suite', function () {
130130
});
131131
});
132132

133-
describe('onConsensusResult', () => {
134-
it('should successfully remove transaction from pool when transaction hash is provided', async () => {
135-
const remainingCount = 1;
136-
const payload = createTestEventPayload();
137-
138-
mockStorage.removeFromList.resolves(remainingCount);
139-
140-
await transactionPoolService.onConsensusResult(payload);
141-
142-
expect(mockStorage.removeFromList.calledOnceWith(testAddress, testTxHash)).to.be.true;
143-
});
144-
145-
it('should handle missing transaction hash gracefully', async () => {
146-
const payload = createTestEventPayload({ transactionHash: undefined });
147-
148-
await transactionPoolService.onConsensusResult(payload);
149-
150-
expect(mockStorage.removeFromList.called).to.be.false;
151-
});
152-
153-
it('should handle empty transaction hash gracefully', async () => {
154-
const payload = createTestEventPayload({ transactionHash: '' });
155-
156-
await transactionPoolService.onConsensusResult(payload);
157-
158-
expect(mockStorage.removeFromList.called).to.be.false;
159-
});
160-
161-
it('should throw storage errors during transaction removal', async () => {
162-
const payload = createTestEventPayload();
163-
const storageError = new Error('Storage removal failed');
164-
mockStorage.removeFromList.rejects(storageError);
165-
166-
// Should throw - errors are no longer caught
167-
await expect(transactionPoolService.onConsensusResult(payload)).to.be.rejectedWith('Storage removal failed');
168-
169-
expect(mockStorage.removeFromList.calledOnceWith(testAddress, testTxHash)).to.be.true;
170-
});
171-
172-
it('should throw when removal fails', async () => {
173-
const payload = createTestEventPayload();
174-
mockStorage.removeFromList.rejects(new Error('Storage error'));
175-
176-
// Should throw when storage fails
177-
await expect(transactionPoolService.onConsensusResult(payload)).to.be.rejectedWith('Storage error');
178-
});
179-
180-
it('should use originalCallerAddress from payload for transaction removal', async () => {
181-
const customAddress = '0x999999999999999999999999999999999999999';
182-
const payload = createTestEventPayload({ originalCallerAddress: customAddress });
183-
184-
mockStorage.removeFromList.resolves(0);
185-
186-
await transactionPoolService.onConsensusResult(payload);
187-
188-
expect(mockStorage.removeFromList.calledOnceWith(customAddress, testTxHash)).to.be.true;
189-
});
190-
191-
it('should handle multiple transaction removals correctly', async () => {
192-
const payload1 = createTestEventPayload();
193-
const payload2 = createTestEventPayload({
194-
transactionHash: '0xabcdef1234567890',
195-
originalCallerAddress: '0x1111111111111111111111111111111111111111',
196-
});
197-
198-
mockStorage.removeFromList.resolves(0);
199-
200-
await transactionPoolService.onConsensusResult(payload1);
201-
await transactionPoolService.onConsensusResult(payload2);
202-
203-
expect(mockStorage.removeFromList.calledTwice).to.be.true;
204-
expect(mockStorage.removeFromList.firstCall.calledWith(testAddress, testTxHash)).to.be.true;
205-
expect(
206-
mockStorage.removeFromList.secondCall.calledWith(
207-
'0x1111111111111111111111111111111111111111',
208-
'0xabcdef1234567890',
209-
),
210-
).to.be.true;
211-
});
212-
});
213-
214133
describe('removeTransaction', () => {
215134
it('should successfully remove transaction from pool', async () => {
216135
const remainingCount = 1;

0 commit comments

Comments
 (0)