diff --git a/src/chains/ethereum/ethereum/src/api.ts b/src/chains/ethereum/ethereum/src/api.ts index 5c9a8d8576..e5fcb9f79d 100644 --- a/src/chains/ethereum/ethereum/src/api.ts +++ b/src/chains/ethereum/ethereum/src/api.ts @@ -3426,39 +3426,83 @@ export default class EthereumApi implements Api { */ @assertArgLength(0) async txpool_content(): Promise> { - const { transactions, common } = this.#blockchain; + const { transactions } = this.#blockchain; const { - transactionPool: { executables, origins } + transactionPool: { executables, origins, processMap } } = transactions; - const processMap = (map: Map>) => { - let res: Record< - string, - Record> - > = {}; - for (let [_, { array, length }] of map) { - for (let i = 0; i < length; ++i) { - const transaction = array[i]; - const from = transaction.from.toString(); - if (res[from] === undefined) { - res[from] = {}; - } - // The nonce keys are actual decimal numbers (as strings) and not - // hex literals (based on what geth returns). - const nonce = transaction.nonce.toBigInt().toString(); - res[from][nonce] = transaction.toJSON( - common - ) as Ethereum.Pool.Transaction<"private">; - } - } - return res; - }; - return { pending: processMap(executables.pending), queued: processMap(origins) }; } + + /** + * Returns transactions created by the specified address currently pending inclusion in the next blocks, as well as the ones that are scheduled for future execution. + * + * @param address - The account address + * @returns The transactions currently pending or queued in the transaction pool by address. + * @example + * ```javascript + * const [from] = await provider.request({ method: "eth_accounts", params: [] }); + * const pendingTx = await provider.request({ method: "eth_sendTransaction", params: [{ from, gas: "0x5b8d80", nonce:"0x0" }] }); + * const queuedTx = await provider.request({ method: "eth_sendTransaction", params: [{ from, gas: "0x5b8d80", nonce:"0x2" }] }); + * const pool = await provider.send("txpool_contentFrom", [from]); + * console.log(pool); + * ``` + */ + @assertArgLength(1) + async txpool_contentFrom(address: DATA): Promise> { + const { transactions } = this.#blockchain; + const { + transactionPool: { executables, origins, processMap } + } = transactions; + + const fromAddress = Address.from(address); + + return { + pending: processMap(executables.pending, fromAddress), + queued: processMap(origins, fromAddress) + }; + } + + /** + * Returns the number of transactions currently pending for inclusion in the next block(s), as well as the ones that are being scheduled for future execution only. + * + * @returns The count of transactions currently pending or queued in the transaction pool. + * @example + * ```javascript + * const [from] = await provider.request({ method: "eth_accounts", params: [] }); + * const pendingTx = await provider.request({ method: "eth_sendTransaction", params: [{ from, gas: "0x5b8d80", nonce:"0x0" }] }); + * const queuedTx = await provider.request({ method: "eth_sendTransaction", params: [{ from, gas: "0x5b8d80", nonce:"0x2" }] }); + * const pool = await provider.send("txpool_status"); + * console.log(pool); + * ``` + */ + @assertArgLength(0) + async txpool_status(): Promise<{pending: number, queued: number}> { + const { transactions } = this.#blockchain; + const { + transactionPool: { executables, origins } + } = transactions; + + let pending = 0; + let queued = 0; + + for (const [_, transactions] of executables.pending) { + pending += transactions?.array.length || 0; + } + + for (const [_, transactions] of origins) { + queued += transactions?.array.length || 0; + } + + return { + pending, + queued + }; + } + //#endregion } diff --git a/src/chains/ethereum/ethereum/src/transaction-pool.ts b/src/chains/ethereum/ethereum/src/transaction-pool.ts index 035e9dc140..4f7586ffd0 100644 --- a/src/chains/ethereum/ethereum/src/transaction-pool.ts +++ b/src/chains/ethereum/ethereum/src/transaction-pool.ts @@ -14,6 +14,8 @@ import { import { EthereumInternalOptions } from "@ganache/ethereum-options"; import { Executables } from "./miner/executables"; import { TypedTransaction } from "@ganache/ethereum-transaction"; +import { Ethereum } from "./api-types"; +import { Address } from "@ganache/ethereum-address"; /** * Checks if the `replacer` is eligible to replace the `replacee` transaction @@ -453,6 +455,33 @@ export default class TransactionPool extends Emittery<{ drain: undefined }> { return null; } + public processMap(map: Map>, address?: Address) { + let res: Record< + string, + Record> + > = {}; + + for (let [_, { array, length }] of map) { + for (let i = 0; i < length; ++i) { + const transaction = array[i]; + + if(address && transaction.from.toString() != address.toString()) { + continue; + } + + const from = transaction.from.toString(); + if (res[from] === undefined) { + res[from] = {}; + } + // The nonce keys are actual decimal numbers (as strings) and not + // hex literals (based on what geth returns). + const nonce = transaction.nonce.toBigInt().toString(); + res[from][nonce] = transaction.toJSON() as Ethereum.Pool.Transaction<"private">; + } + } + return res; + }; + readonly drain = () => { // notify listeners (the blockchain, then the miner, eventually) that we // have executable transactions ready diff --git a/src/chains/ethereum/ethereum/tests/api/txpool/content.test.ts b/src/chains/ethereum/ethereum/tests/api/txpool/txpool.test.ts similarity index 55% rename from src/chains/ethereum/ethereum/tests/api/txpool/content.test.ts rename to src/chains/ethereum/ethereum/tests/api/txpool/txpool.test.ts index f01ff0b46d..e23abec1ad 100644 --- a/src/chains/ethereum/ethereum/tests/api/txpool/content.test.ts +++ b/src/chains/ethereum/ethereum/tests/api/txpool/txpool.test.ts @@ -120,4 +120,110 @@ describe("txpool", () => { assert.deepStrictEqual(queued, {}); }); }); + + describe("contentFrom", () => { + let provider: EthereumProvider; + let accounts: string[]; + + beforeEach(async () => { + provider = await getProvider({ + miner: { blockTime: 1000 } + }); + accounts = await provider.send("eth_accounts"); + }); + + it("handles pending and queued transactions", async () => { + const pendingTransactions = await Promise.all([ + provider.send("eth_sendTransaction", [ + { + from: accounts[1], + to: accounts[2] + } + ]), + provider.send("eth_sendTransaction", [ + { + from: accounts[2], + to: accounts[3] + } + ]), + provider.send("eth_sendTransaction", [ + { + from: accounts[1], + to: accounts[2] + } + ]) + ]); + + const queuedTransactions = await Promise.all([ + provider.send("eth_sendTransaction", [ + { + from: accounts[1], + to: accounts[2], + nonce: "0x123", + } + ]) + ]); + + const { pending, queued } = await provider.send("txpool_contentFrom", [accounts[1]]); + + const pendingAddresses = Object.keys(pending); + const queuedAddresses = Object.keys(queued); + + assert(pendingAddresses.findIndex((value) => value === accounts[1]) == 0) + assert(pendingAddresses.findIndex((value) => value === accounts[2]) == -1) + assert(pendingAddresses.length == 1) + + assert(queuedAddresses.findIndex((value) => value === accounts[1]) == 0) + assert(queuedAddresses.findIndex((value) => value === accounts[2]) == -1) + assert(queuedAddresses.length == 1) + }) + }) + + describe("status", () => { + let provider: EthereumProvider; + let accounts: string[]; + beforeEach(async () => { + provider = await getProvider({ + miner: { blockTime: 1000 } + }); + accounts = await provider.send("eth_accounts"); + }); + + it("handles pending and queued transactions", async () => { + const pendingTransactions = await Promise.all([ + provider.send("eth_sendTransaction", [ + { + from: accounts[1], + to: accounts[2] + } + ]), + provider.send("eth_sendTransaction", [ + { + from: accounts[2], + to: accounts[3] + } + ]), + provider.send("eth_sendTransaction", [ + { + from: accounts[1], + to: accounts[2] + } + ]) + ]); + + const queuedTransactions = await Promise.all([ + provider.send("eth_sendTransaction", [ + { + from: accounts[1], + to: accounts[2], + nonce: "0x123", + } + ]) + ]); + + const { pending, queued } = await provider.send("txpool_status"); + assert.strictEqual(pending, pendingTransactions.length); + assert.strictEqual(queued, queuedTransactions.length); + }); + }); });