Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🐛 Fix: getFilterLog issue #1337

Merged
merged 6 commits into from
Aug 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions packages/actions/src/eth/ethGetLogsHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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)),
Expand Down
105 changes: 105 additions & 0 deletions packages/actions/src/eth/ethNewFilterHandler.js
Original file line number Diff line number Diff line change
@@ -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<import('@tevm/utils').Hex>]}*/ (
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
}
}
1 change: 1 addition & 0 deletions packages/actions/src/eth/index.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down
8 changes: 8 additions & 0 deletions packages/actions/src/eth/utils/generateRandomId.js
Original file line number Diff line number Diff line change
@@ -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('')}`
}
15 changes: 15 additions & 0 deletions packages/actions/src/eth/utils/generateRandomId.spec.ts
Original file line number Diff line number Diff line change
@@ -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)
})
})
14 changes: 14 additions & 0 deletions packages/actions/src/eth/utils/parseBlockTag.js
Original file line number Diff line number Diff line change
@@ -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
}
43 changes: 43 additions & 0 deletions packages/actions/src/eth/utils/parseBlockTag.spec.ts
Original file line number Diff line number Diff line change
@@ -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)
})
})
4 changes: 2 additions & 2 deletions packages/procedures/src/eth/ethGetFilterLogsProcedure.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand Down
100 changes: 100 additions & 0 deletions packages/procedures/src/eth/ethGetFilterLogsProcedure.spec.ts
Original file line number Diff line number Diff line change
@@ -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",
}
`)
})
})
Loading
Loading