diff --git a/packages/actions/src/eth/ethGetLogsHandler.js b/packages/actions/src/eth/ethGetLogsHandler.js index d8a5aedc6b..d7649efcb9 100644 --- a/packages/actions/src/eth/ethGetLogsHandler.js +++ b/packages/actions/src/eth/ethGetLogsHandler.js @@ -60,9 +60,6 @@ const parseBlockParam = async (blockchain, blockParam) => { * @returns {import('./EthHandler.js').EthGetLogsHandler} */ export const ethGetLogsHandler = (client) => async (params) => { - params.filterParams.topics - params.filterParams.address - client.logger.debug(params, 'blockNumberHandler called with params') const vm = await client.getVm() const receiptsManager = await client.getReceiptsManager() @@ -172,7 +169,7 @@ export const ethGetLogsHandler = (client) => async (params) => { } const cachedLogs = await receiptsManager.getLogs( - fetchFromRpc ? fromBlock : /** @type {import('@tevm/block').Block}*/ (forkedBlock), + fetchFromRpc ? /** @type {import('@tevm/block').Block}*/ (forkedBlock) : fromBlock, toBlock, params.filterParams.address !== undefined ? [createAddress(params.filterParams.address).bytes] : [], params.filterParams.topics?.map((topic) => hexToBytes(topic)), diff --git a/packages/actions/src/eth/ethNewFilterHandler.js b/packages/actions/src/eth/ethNewFilterHandler.js new file mode 100644 index 0000000000..fa0e84e12b --- /dev/null +++ b/packages/actions/src/eth/ethNewFilterHandler.js @@ -0,0 +1,105 @@ +import { createAddress } from '@tevm/address' +import { InvalidBlockError, UnknownBlockError } from '@tevm/errors' +import { bytesToHex, hexToBytes } from '@tevm/utils' +import { generateRandomId } from './utils/generateRandomId.js' +import { parseBlockTag } from './utils/parseBlockTag.js' + +/** + * @typedef {UnknownBlockError | InvalidBlockError} EthNewFilterError + */ + +/** + * @param {import('@tevm/node').TevmNode} tevmNode + * @returns {import('./EthHandler.js').EthNewFilterHandler} ethNewFilterHandler + */ +export const ethNewFilterHandler = (tevmNode) => { + return async (params) => { + const { topics, address, toBlock = 'latest', fromBlock } = params + const vm = await tevmNode.getVm() + /** + * @param {typeof toBlock} tag + */ + const getBlock = async (tag) => { + const parsedTag = parseBlockTag(tag) + if ( + parsedTag === 'safe' || + parsedTag === 'latest' || + parsedTag === 'finalized' || + parsedTag === 'earliest' || + parsedTag === 'pending' || + parsedTag === /** @type any*/ ('forked') + ) { + return vm.blockchain.blocksByTag.get(parsedTag) + } + if (typeof parsedTag === 'string') { + return vm.blockchain.getBlock(hexToBytes(parsedTag)) + } + if (typeof tag === 'bigint') { + return vm.blockchain.getBlock(tag) + } + throw new InvalidBlockError(`Invalid block tag ${tag}`) + } + const _toBlock = await getBlock(toBlock) + if (!_toBlock) { + throw new UnknownBlockError(`Unknown block tag ${toBlock}`) + } + const _fromBlock = await getBlock(fromBlock ?? 'latest') + if (!_fromBlock) { + throw new UnknownBlockError(`Unknown block tag ${fromBlock}`) + } + + const id = generateRandomId() + /** + * @param {import('@tevm/node').Filter['logs'][number]} log + */ + const listener = (log) => { + const filter = tevmNode.getFilters().get(id) + if (!filter) { + return + } + filter.logs.push(log) + } + tevmNode.on('newLog', listener) + // populate with past blocks + const receiptsManager = await tevmNode.getReceiptsManager() + const pastLogs = await receiptsManager.getLogs( + _fromBlock, + _toBlock, + address !== undefined ? [createAddress(address).bytes] : [], + topics?.map((topic) => hexToBytes(topic)), + ) + tevmNode.setFilter({ + id, + type: 'Log', + created: Date.now(), + logs: pastLogs.map((log) => { + const [address, topics, data] = log.log + return { + topics: /** @type {[import('@tevm/utils').Hex, ...Array]}*/ ( + topics.map((topic) => bytesToHex(topic)) + ), + address: bytesToHex(address), + data: bytesToHex(data), + blockNumber: log.block.header.number, + transactionHash: bytesToHex(log.tx.hash()), + removed: false, + logIndex: log.logIndex, + blockHash: bytesToHex(log.block.hash()), + transactionIndex: log.txIndex, + } + }), + tx: [], + blocks: [], + logsCriteria: { + topics, + address, + toBlock: toBlock, + fromBlock: fromBlock ?? _fromBlock.header.number, + }, + installed: {}, + err: undefined, + registeredListeners: [listener], + }) + return id + } +} diff --git a/packages/actions/src/eth/index.ts b/packages/actions/src/eth/index.ts index 416d459150..8f758d09f2 100644 --- a/packages/actions/src/eth/index.ts +++ b/packages/actions/src/eth/index.ts @@ -1,6 +1,7 @@ export * from './chainIdHandler.js' export * from './getCodeHandler.js' export * from './gasPriceHandler.js' +export * from './ethNewFilterHandler.js' export * from './blockNumberHandler.js' export * from './getBalanceHandler.js' export * from './getStorageAtHandler.js' diff --git a/packages/actions/src/eth/utils/generateRandomId.js b/packages/actions/src/eth/utils/generateRandomId.js new file mode 100644 index 0000000000..31efee1df0 --- /dev/null +++ b/packages/actions/src/eth/utils/generateRandomId.js @@ -0,0 +1,8 @@ +/** + * @returns {import("@tevm/utils").Hex} + */ +export const generateRandomId = () => { + return `0x${Array.from(crypto.getRandomValues(new Uint8Array(16))) + .map((b) => b.toString(16).padStart(2, '0')) + .join('')}` +} diff --git a/packages/actions/src/eth/utils/generateRandomId.spec.ts b/packages/actions/src/eth/utils/generateRandomId.spec.ts new file mode 100644 index 0000000000..9e0c85dd00 --- /dev/null +++ b/packages/actions/src/eth/utils/generateRandomId.spec.ts @@ -0,0 +1,15 @@ +import { describe, expect, it } from 'vitest' +import { generateRandomId } from './generateRandomId.js' + +describe('generateRandomId', () => { + it('should generate a valid hex string of length 34', () => { + const id = generateRandomId() + expect(id).toMatch(/^0x[a-f0-9]{32}$/) + }) + + it('should generate different ids on multiple calls', () => { + const id1 = generateRandomId() + const id2 = generateRandomId() + expect(id1).not.toBe(id2) + }) +}) diff --git a/packages/actions/src/eth/utils/parseBlockTag.js b/packages/actions/src/eth/utils/parseBlockTag.js new file mode 100644 index 0000000000..4ef6ea65a0 --- /dev/null +++ b/packages/actions/src/eth/utils/parseBlockTag.js @@ -0,0 +1,14 @@ +import { hexToBigInt } from '@tevm/utils' + +/** + * @param {import('@tevm/utils').Hex | import('@tevm/utils').BlockTag | bigint} blockTag + * @returns {bigint | import('@tevm/utils').Hex | import('@tevm/utils').BlockTag} + */ +export const parseBlockTag = (blockTag) => { + const blockHashLength = 64 + '0x'.length + const isBlockNumber = typeof blockTag === 'string' && blockTag.startsWith('0x') && blockTag.length !== blockHashLength + if (isBlockNumber) { + return hexToBigInt(/** @type {import('@tevm/utils').Hex}*/ (blockTag)) + } + return blockTag +} diff --git a/packages/actions/src/eth/utils/parseBlockTag.spec.ts b/packages/actions/src/eth/utils/parseBlockTag.spec.ts new file mode 100644 index 0000000000..f885577380 --- /dev/null +++ b/packages/actions/src/eth/utils/parseBlockTag.spec.ts @@ -0,0 +1,43 @@ +import { hexToBigInt } from '@tevm/utils' +import { describe, expect, it } from 'vitest' +import { parseBlockTag } from './parseBlockTag.js' + +describe('parseBlockTag', () => { + it('should parse hex block numbers to bigint', () => { + const blockTag = '0x10' + const result = parseBlockTag(blockTag) + expect(result).toBe(hexToBigInt(blockTag)) + }) + + it('should return block hash as is', () => { + const blockHash = `0x${'a'.repeat(64)}` as const + const result = parseBlockTag(blockHash) + expect(result).toBe(blockHash) + }) + + it('should return special block tags as is', () => { + const tags = ['latest', 'earliest', 'pending'] as const + tags.forEach((tag) => { + const result = parseBlockTag(tag) + expect(result).toBe(tag) + }) + }) + + it('should return block number as bigint for valid hex strings', () => { + const blockTag = '0x1a' + const result = parseBlockTag(blockTag) + expect(result).toBe(26n) + }) + + it('should handle block tag as a number string correctly', () => { + const blockTag = '0x10' + const result = parseBlockTag(blockTag) + expect(result).toBe(16n) + }) + + it('should return blockTag unchanged if it is a non-hex string', () => { + const blockTag = 'pending' + const result = parseBlockTag(blockTag) + expect(result).toBe(blockTag) + }) +}) diff --git a/packages/procedures/src/eth/ethGetFilterLogsProcedure.js b/packages/procedures/src/eth/ethGetFilterLogsProcedure.js index 1146a11e30..0d4ffccc56 100644 --- a/packages/procedures/src/eth/ethGetFilterLogsProcedure.js +++ b/packages/procedures/src/eth/ethGetFilterLogsProcedure.js @@ -23,8 +23,8 @@ export const ethGetFilterLogsProcedure = (client) => { try { const ethGetLogsResult = await ethGetLogsHandler(client)({ filterParams: { - fromBlock: filter.logsCriteria.fromBlock, - toBlock: filter.logsCriteria.toBlock, + fromBlock: filter.logsCriteria.fromBlock?.header?.number ?? 0n, + toBlock: filter.logsCriteria.toBlock?.header?.number ?? 'latest', address: filter.logsCriteria.address, topics: filter.logsCriteria.topics, }, diff --git a/packages/procedures/src/eth/ethGetFilterLogsProcedure.spec.ts b/packages/procedures/src/eth/ethGetFilterLogsProcedure.spec.ts new file mode 100644 index 0000000000..5f58c3a0a5 --- /dev/null +++ b/packages/procedures/src/eth/ethGetFilterLogsProcedure.spec.ts @@ -0,0 +1,100 @@ +import { createAddress, createContractAddress } from '@tevm/address' +import { SimpleContract } from '@tevm/contract' +import { type TevmNode, createTevmNode } from '@tevm/node' +import { PREFUNDED_ACCOUNTS, encodeDeployData, encodeFunctionData, isHex, numberToHex } from '@tevm/utils' +import { beforeEach, describe, expect, it } from 'vitest' +import { callProcedure } from '../call/callProcedure.js' +import { mineProcedure } from '../mine/mineProcedure.js' +import { ethGetFilterLogsProcedure } from './ethGetFilterLogsProcedure.js' +import { ethNewFilterJsonRpcProcedure } from './ethNewFilterProcedure.js' + +describe(ethGetFilterLogsProcedure.name, () => { + let client: TevmNode + + const INITIAL_BALANCE = 20n + const contract = SimpleContract.withAddress( + createContractAddress(createAddress(PREFUNDED_ACCOUNTS[0].address), 0n).toString(), + ) + + const doMine = () => { + return mineProcedure(client)({ + jsonrpc: '2.0', + params: [numberToHex(1n), numberToHex(1n)], + method: 'tevm_mine', + }) + } + + beforeEach(async () => { + client = createTevmNode() + + expect( + ( + await callProcedure(client)({ + method: 'tevm_call', + jsonrpc: '2.0', + params: [ + { + data: encodeDeployData(contract.deploy(INITIAL_BALANCE)), + createTransaction: true, + }, + ], + }) + ).error, + ).toBeUndefined() + + expect((await doMine()).error).toBeUndefined() + }) + + it('should return logs', async () => { + const { result: filterId } = await ethNewFilterJsonRpcProcedure(client)({ + jsonrpc: '2.0', + method: 'eth_newFilter', + params: [{}], + }) + if (!filterId) throw new Error('Expected filter') + + expect( + ( + await callProcedure(client)({ + method: 'tevm_call', + jsonrpc: '2.0', + params: [ + { + to: contract.address, + data: encodeFunctionData(contract.write.set(69n)), + createTransaction: true, + }, + ], + }) + ).error, + ).toBeUndefined() + expect((await doMine()).error).toBeUndefined() + + const { result, error } = await ethGetFilterLogsProcedure(client)({ + jsonrpc: '2.0', + method: 'eth_getFilterLogs', + params: [filterId], + }) + + expect(error).toBeUndefined() + + expect(result).toHaveLength(1) + const { blockHash, ...deterministicResult } = result?.[0] ?? {} + expect(isHex(blockHash)).toBe(true) + expect(blockHash).toHaveLength(66) + expect(deterministicResult).toMatchInlineSnapshot(` + { + "address": "0x5fbdb2315678afecb367f032d93f642f64180aa3", + "blockNumber": "0x2", + "data": "0x0000000000000000000000000000000000000000000000000000000000000045", + "logIndex": "0x0", + "removed": false, + "topics": [ + "0x012c78e2b84325878b1bd9d250d772cfe5bda7722d795f45036fa5e1e6e303fc", + ], + "transactionHash": "0x26de6f137bcebaa05e276447f69158f66910b461e47afca6fe67360833698708", + "transactionIndex": "0x0", + } + `) + }) +}) diff --git a/packages/procedures/src/eth/ethNewFilterProcedure.js b/packages/procedures/src/eth/ethNewFilterProcedure.js index 016dc16e03..f24bc88f25 100644 --- a/packages/procedures/src/eth/ethNewFilterProcedure.js +++ b/packages/procedures/src/eth/ethNewFilterProcedure.js @@ -1,121 +1,32 @@ -import { createAddress } from '@tevm/address' -import { bytesToHex, hexToBytes } from '@tevm/utils' -import { generateRandomId } from '../utils/generateRandomId.js' -import { parseBlockTag } from '../utils/parseBlockTag.js' +import { ethNewFilterHandler } from '@tevm/actions' /** * Request handler for eth_newFilter JSON-RPC requests. - * @param {import('@tevm/node').TevmNode} client + * @param {import('@tevm/node').TevmNode} tevmNode * @returns {import('./EthProcedure.js').EthNewFilterJsonRpcProcedure} */ -export const ethNewFilterJsonRpcProcedure = (client) => { +export const ethNewFilterJsonRpcProcedure = (tevmNode) => { return async (request) => { const newFilterRequest = /** @type {import('./EthJsonRpcRequest.js').EthNewFilterJsonRpcRequest}*/ (request) - - const { topics, address, toBlock = 'latest', fromBlock = 'latest' } = newFilterRequest.params[0] - const id = generateRandomId() - const vm = await client.getVm() - /** - * @param {typeof toBlock} tag - */ - const getBlock = async (tag) => { - const parsedTag = parseBlockTag(tag) - if ( - parsedTag === 'safe' || - parsedTag === 'latest' || - parsedTag === 'finalized' || - parsedTag === 'earliest' || - parsedTag === 'pending' || - parsedTag === /** @type any*/ ('forked') - ) { - return vm.blockchain.blocksByTag.get(parsedTag) - } - if (typeof parsedTag === 'string') { - return vm.blockchain.getBlock(hexToBytes(parsedTag)) - } - return vm.blockchain.getBlock(parsedTag) - } - const _toBlock = await getBlock(toBlock) - if (!_toBlock) { + try { return { - ...(request.id ? { id: request.id } : {}), - method: request.method, jsonrpc: request.jsonrpc, - error: { - code: -32602, - message: `Invalid block tag ${toBlock}`, - }, + method: request.method, + result: await ethNewFilterHandler(tevmNode)(newFilterRequest.params[0]), + ...(request.id !== undefined ? { id: request.id } : {}), } - } - const _fromBlock = await getBlock(fromBlock) - if (!_fromBlock) { + } catch (e) { + tevmNode.logger.error(e) + const { code, message } = /** @type {import('@tevm/actions').EthNewFilterError}*/ (e) return { - ...(request.id ? { id: request.id } : {}), - method: request.method, - jsonrpc: request.jsonrpc, error: { - code: -32602, - message: `Invalid block tag ${fromBlock}`, + code, + message, }, + method: request.method, + jsonrpc: request.jsonrpc, + ...(request.id !== undefined ? { id: request.id } : {}), } } - - /** - * @param {import('@tevm/node').Filter['logs'][number]} log - */ - const listener = (log) => { - const filter = client.getFilters().get(id) - if (!filter) { - return - } - filter.logs.push(log) - } - client.on('newLog', listener) - // populate with past blocks - const receiptsManager = await client.getReceiptsManager() - const pastLogs = await receiptsManager.getLogs( - _fromBlock, - _toBlock, - address !== undefined ? [createAddress(address).bytes] : [], - topics?.map((topic) => hexToBytes(topic)), - ) - client.setFilter({ - id, - type: 'Log', - created: Date.now(), - logs: pastLogs.map((log) => { - const [address, topics, data] = log.log - return { - topics: /** @type {[import('@tevm/utils').Hex, ...Array]}*/ ( - topics.map((topic) => bytesToHex(topic)) - ), - address: bytesToHex(address), - data: bytesToHex(data), - blockNumber: log.block.header.number, - transactionHash: bytesToHex(log.tx.hash()), - removed: false, - logIndex: log.logIndex, - blockHash: bytesToHex(log.block.hash()), - transactionIndex: log.txIndex, - } - }), - tx: [], - blocks: [], - logsCriteria: { - topics, - address, - toBlock: toBlock, - fromBlock: fromBlock, - }, - installed: {}, - err: undefined, - registeredListeners: [listener], - }) - return { - ...(request.id ? { id: request.id } : {}), - method: request.method, - jsonrpc: request.jsonrpc, - result: id, - } } }