Skip to content

Commit 765a06f

Browse files
committed
chore: add poc
Signed-off-by: nikolay <[email protected]>
1 parent c04db62 commit 765a06f

File tree

4 files changed

+133
-5
lines changed

4 files changed

+133
-5
lines changed

packages/relay/src/lib/precheck.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -104,9 +104,9 @@ export class Precheck {
104104
throw predefined.RESOURCE_NOT_FOUND(`Account nonce unavailable for address: ${tx.from}.`);
105105
}
106106

107-
if (accountNonce > tx.nonce) {
108-
throw predefined.NONCE_TOO_LOW(tx.nonce, accountNonce);
109-
}
107+
// if (accountNonce > tx.nonce) {
108+
// throw predefined.NONCE_TOO_LOW(tx.nonce, accountNonce);
109+
// }
110110
}
111111

112112
/**

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,10 @@ export class TransactionService implements ITransactionService {
271271
await this.validateRawTransaction(parsedTx, networkGasPriceInWeiBars, requestDetails);
272272

273273
// Save the transaction to the transaction pool before submitting it to the network
274-
await this.transactionPoolService.saveTransaction(parsedTx.from!, parsedTx);
274+
const isUpdated = await this.transactionPoolService.saveOrUpdate(parsedTx.from!, parsedTx);
275+
if (isUpdated) {
276+
return Utils.computeTransactionHash(transactionBuffer);
277+
}
275278

276279
/**
277280
* Note: If the USE_ASYNC_TX_PROCESSING feature flag is enabled,
@@ -498,6 +501,10 @@ export class TransactionService implements ITransactionService {
498501

499502
const sessionKey = await this.lockService.acquireLock(parsedTx.from!);
500503

504+
const foundTx = await this.transactionPoolService.getBySenderAndNonce(parsedTx.from!.toLowerCase(), parsedTx.nonce);
505+
506+
transactionBuffer = Buffer.from(this.prune0x(foundTx!), 'hex');
507+
501508
this.eventEmitter.emit('eth_execution', {
502509
method: constants.ETH_SEND_RAW_TRANSACTION,
503510
});
@@ -510,7 +517,7 @@ export class TransactionService implements ITransactionService {
510517
);
511518

512519
// Remove the transaction from the transaction pool after submission
513-
await this.transactionPoolService.removeTransaction(originalCallerAddress, parsedTx.serialized);
520+
await this.transactionPoolService.removeTransaction(originalCallerAddress, foundTx!);
514521
await this.lockService.releaseLock(parsedTx.from!, sessionKey);
515522

516523
sendRawTransactionError = error;

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

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,52 @@ export class TransactionPoolService implements ITransactionPoolService {
7373
}
7474
}
7575

76+
async saveOrUpdate(address: string, tx: Transaction): Promise<boolean> {
77+
const addr = address.toLowerCase();
78+
const nonce = tx.nonce;
79+
80+
// @ts-ignore
81+
const pending = this.storage.pendingTransactions.get(addr) ?? new Set<string>();
82+
83+
let existingTx: string | null = null;
84+
const updatedPending = new Set<string>();
85+
86+
// Replace or keep list entries
87+
for (const item of pending) {
88+
const parsed = Transaction.from(item);
89+
if (parsed.nonce === nonce) {
90+
existingTx = item;
91+
updatedPending.add(tx.serialized);
92+
} else {
93+
updatedPending.add(item);
94+
}
95+
}
96+
97+
// If no existing tx with same nonce → just save new
98+
if (!existingTx) {
99+
await this.saveTransaction(addr, tx);
100+
return false;
101+
}
102+
103+
// Update pending set
104+
// @ts-ignore
105+
this.storage.pendingTransactions.set(addr, updatedPending);
106+
107+
// Update global index
108+
// @ts-ignore
109+
const globalIndex = this.storage.globalTransactionIndex;
110+
const updatedGlobal = new Set<string>();
111+
112+
for (const item of globalIndex) {
113+
updatedGlobal.add(item === existingTx ? tx.serialized : item);
114+
}
115+
116+
// @ts-ignore
117+
this.storage.globalTransactionIndex = updatedGlobal;
118+
119+
return true;
120+
}
121+
76122
/**
77123
* Removes a specific transaction from the pending pool.
78124
* This is typically called when a transaction is confirmed or fails on the consensus layer.
@@ -136,4 +182,19 @@ export class TransactionPoolService implements ITransactionPoolService {
136182

137183
return payloads;
138184
}
185+
186+
async getBySenderAndNonce(sender: string, nonce: number): Promise<string> {
187+
// TODO: create a method in the storage layers
188+
// @ts-ignore
189+
const txs = this.storage.pendingTransactions.get(sender.toLowerCase());
190+
191+
let foundTx = '';
192+
txs?.forEach((txIt) => {
193+
if (Transaction.from(txIt).nonce == nonce) {
194+
foundTx = txIt;
195+
}
196+
});
197+
198+
return foundTx;
199+
}
139200
}

packages/server/tests/acceptance/rpc_batch2.spec.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,66 @@ describe('@api-batch-2 RPC Server Acceptance Tests', function () {
143143
}
144144
});
145145

146+
describe('Replace TX pool transaction', function () {
147+
it('should replace a transaction with the same nonce', async function () {
148+
const gasPrice = await relay.gasPrice();
149+
const nonce = await relay.getAccountNonce(accounts[0].address);
150+
151+
const txHashes = [];
152+
let txForOverrideHash;
153+
let txForOverrideNonce;
154+
for (let i = 0; i < 6; i++) {
155+
const itNonce = nonce + i;
156+
const transaction = {
157+
type: 2,
158+
chainId: Number(CHAIN_ID),
159+
nonce: itNonce,
160+
maxPriorityFeePerGas: gasPrice,
161+
maxFeePerGas: gasPrice,
162+
gasLimit: 1_000_000,
163+
to: accounts[1].address,
164+
value: Utils.add0xPrefix(Utils.toHex(ethers.parseUnits('1', 10))),
165+
};
166+
167+
const signedTx = await accounts[0].wallet.signTransaction(transaction);
168+
const transactionHash = await relay.sendRawTransaction(signedTx);
169+
170+
if (i == 4) {
171+
txForOverrideHash = transactionHash;
172+
txForOverrideNonce = itNonce;
173+
} else {
174+
txHashes.push(transactionHash);
175+
}
176+
}
177+
178+
const signedTx = await accounts[0].wallet.signTransaction({
179+
type: 2,
180+
chainId: Number(CHAIN_ID),
181+
nonce: txForOverrideNonce,
182+
maxPriorityFeePerGas: gasPrice,
183+
maxFeePerGas: gasPrice,
184+
gasLimit: 1_000_000,
185+
to: accounts[1].address,
186+
value: Utils.add0xPrefix(Utils.toHex(ethers.parseUnits('2', 10))),
187+
});
188+
const transactionHash = await relay.sendRawTransaction(signedTx);
189+
190+
expect(txHashes).to.have.length(5);
191+
for (const txHash of txHashes) {
192+
const receipt = await relay.pollForValidTransactionReceipt(txHash);
193+
expect(receipt.status).to.equal('0x1');
194+
}
195+
196+
const txForOverrideReceipt = await relay.call(RelayCalls.ETH_ENDPOINTS.ETH_GET_TRANSACTION_RECEIPT, [
197+
txForOverrideHash,
198+
]);
199+
expect(txForOverrideReceipt).to.be.null;
200+
201+
const receipt = await relay.pollForValidTransactionReceipt(transactionHash);
202+
expect(receipt.status).to.equal('0x1');
203+
});
204+
});
205+
146206
describe('eth_estimateGas', async function () {
147207
let basicContract: ethers.Contract;
148208
let basicContractAddress: string;

0 commit comments

Comments
 (0)