Skip to content

Commit

Permalink
🐛 Fix: getFilterLog issue (#1337)
Browse files Browse the repository at this point in the history
## Description

_Concise description of proposed changes_

## Testing

Explain the quality checks that have been done on the code changes

## Additional Information

- [ ] I read the [contributing docs](../docs/contributing.md) (if this
is your first contribution)

Your ENS/address:



<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **New Features**
- Introduced a new filter handler for Ethereum logs, allowing users to
create custom log filters based on various parameters.
	- Added a utility function to generate unique hexadecimal identifiers.
- Implemented a function to parse block tags, enhancing usability in
blockchain applications.

- **Bug Fixes**
- Improved error handling in the filter logging procedure to ensure
structured responses during exceptions.

- **Tests**
- Added unit tests for the new identifier generation and block tag
parsing functions to ensure reliability and expected behavior.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: William Cory <[email protected]>
  • Loading branch information
roninjin10 and William Cory committed Aug 12, 2024
1 parent 94aadff commit 44f32db
Show file tree
Hide file tree
Showing 10 changed files with 304 additions and 110 deletions.
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

0 comments on commit 44f32db

Please sign in to comment.