-
-
Notifications
You must be signed in to change notification settings - Fork 28
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
## 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
1 parent
94aadff
commit 44f32db
Showing
10 changed files
with
304 additions
and
110 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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('')}` | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
100 changes: 100 additions & 0 deletions
100
packages/procedures/src/eth/ethGetFilterLogsProcedure.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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", | ||
} | ||
`) | ||
}) | ||
}) |
Oops, something went wrong.