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

Implement the eth_getFilterLogs endpoint #217

Merged
merged 4 commits into from
Apr 26, 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
48 changes: 45 additions & 3 deletions api/pull.go
Original file line number Diff line number Diff line change
Expand Up @@ -259,9 +259,51 @@ func (api *PullAPI) NewFilter(criteria filters.FilterCriteria) (rpc.ID, error) {
}

// GetFilterLogs returns the logs for the filter with the given id.
// If the filter could not be found an empty array of logs is returned.
func (api *PullAPI) GetFilterLogs(ctx context.Context, id rpc.ID) ([]*gethTypes.Log, error) {
panic("not implemented")
// If the filter could not be found, `nil` is returned.
func (api *PullAPI) GetFilterLogs(
ctx context.Context,
id rpc.ID,
) ([]*gethTypes.Log, error) {
api.mux.Lock()
defer api.mux.Unlock()

filter, ok := api.filters[id]
if !ok {
return nil, errors.Join(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

according to the above description it should return empty array if not found, I suggest adding a test where the filter is not found as well

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point regarding the test case, I will add it right away.
Good catch as well regarding the description. This is actually a wrong description copied from geth, See https://github.com/ethereum/go-ethereum/blob/8d42e115b1cae4f09fd02b71c06ec9c85f22ad4f/eth/filters/api.go#L383-L417 😅 I will update the description, the geth behavior is to always return nil, instead of an empty array, at least for this specific endpoint.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated in d2952b8

errs.ErrNotFound,
fmt.Errorf("filted by id %s does not exist", id),
)
}

if filter.expired() {
api.UninstallFilter(id)
return nil, errors.Join(
errs.ErrNotFound,
fmt.Errorf("filted by id %s has expired", id),
)
}

logsFilter, ok := filter.(*logsFilter)
if !ok {
return nil, fmt.Errorf("filted by id %s is not a logs filter", id)
}

current, err := api.blocks.LatestEVMHeight()
if err != nil {
return nil, err
}

result, err := api.getLogs(current, logsFilter)
if err != nil {
return nil, err
}

logs, ok := result.([]*gethTypes.Log)
if !ok {
return nil, fmt.Errorf("logs filter returned incorrect type: %T", logs)
}

return logs, nil
}

// GetFilterChanges returns the logs for the filter with the given id since
Expand Down
4 changes: 4 additions & 0 deletions tests/e2e_web3js_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ func TestWeb3_E2E(t *testing.T) {
runWeb3Test(t, "eth_logs_filtering_test")
})

t.Run("eth_getFilterLogs", func(t *testing.T) {
runWeb3Test(t, "eth_get_filter_logs_test")
})

t.Run("streaming of entities and subscription", func(t *testing.T) {
runWeb3Test(t, "eth_streaming_test")
})
Expand Down
77 changes: 77 additions & 0 deletions tests/web3js/eth_get_filter_logs_test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
const { assert } = require('chai')
const conf = require('./config')
const helpers = require('./helpers')
const web3 = conf.web3

it('returns a null result for missing filter', async () => {
// check that null is returned when the filter could not be found.
let response = await helpers.callRPCMethod('eth_getFilterLogs', ['0xffa1'])

assert.equal(200, response.status)
assert.isUndefined(response.body['result'])
})

it('create logs filter and call eth_getFilterLogs', async () => {
// deploy contract
let deployed = await helpers.deployContract('storage')
let contractAddress = deployed.receipt.contractAddress

// create logs filter on the address of the deployed contract
let response = await helpers.callRPCMethod('eth_newFilter', [{ 'address': contractAddress }])

assert.equal(200, response.status)
assert.isDefined(response.body['result'])
let filterID = response.body['result']

// make contract function call that emits a log
let res = await helpers.signAndSend({
from: conf.eoa.address,
to: contractAddress,
data: deployed.contract.methods.sum(15, 20).encodeABI(),
gas: 1000000,
gasPrice: 0
})
assert.equal(res.receipt.status, conf.successStatus)

// check the matching items from the logs filter
response = await helpers.callRPCMethod('eth_getFilterLogs', [filterID])

assert.equal(200, response.status)
assert.equal(1, response.body['result'].length)

let logs = response.body['result']
assert.equal(contractAddress.toLowerCase(), logs[0].address)
assert.lengthOf(logs[0].topics, 4)
assert.equal(
'35',
web3.eth.abi.decodeParameter("int256", logs[0].data)
)

// make contract function call that emits another log
res = await helpers.signAndSend({
from: conf.eoa.address,
to: contractAddress,
data: deployed.contract.methods.sum(30, 20).encodeABI(),
gas: 1000000,
gasPrice: 0
})

assert.equal(res.receipt.status, conf.successStatus)

// check the matching items from the logs filter include both logs
// from the above 2 contract function calls
response = await helpers.callRPCMethod('eth_getFilterLogs', [filterID])

assert.equal(200, response.status)
assert.equal(2, response.body['result'].length)
logs = response.body['result']
console.log()

assert.equal(contractAddress.toLowerCase(), logs[1].address)
assert.lengthOf(logs[1].topics, 4)
assert.equal(
'50',
web3.eth.abi.decodeParameter("int256", logs[1].data)
)

}).timeout(10 * 1000)
78 changes: 49 additions & 29 deletions tests/web3js/helpers.js
Original file line number Diff line number Diff line change
@@ -1,50 +1,70 @@
const fs = require("fs")
const conf = require("./config")
const {assert} = require("chai");
const chai = require("chai")
const chaiHttp = require('chai-http')
const web3 = conf.web3

chai.use(chaiHttp);

// deployContract deploys a contract by name, the contract files must be saved in
// fixtures folder, each contract must have two files: ABI and bytecode,
// the ABI file must be named {name}ABI.json and contain ABI definition for the contract
// and bytecode file must be named {name}.byte and must contain compiled byte code of the contract.
//
// Returns the contract object as well as the receipt deploying the contract.
async function deployContract(name) {
const abi = require(`../fixtures/${name}ABI.json`)
const code = await fs.promises.readFile(`${__dirname}/../fixtures/${name}.byte`, 'utf8')
const contractABI = new web3.eth.Contract(abi)

let data = contractABI
.deploy({ data: `0x${code}` })
.encodeABI()

let signed = await conf.eoa.signTransaction({
from: conf.eoa.address,
data: data,
value: '0',
gasPrice: '0',
})
const abi = require(`../fixtures/${name}ABI.json`)
const code = await fs.promises.readFile(`${__dirname}/../fixtures/${name}.byte`, 'utf8')
const contractABI = new web3.eth.Contract(abi)

let data = contractABI
.deploy({ data: `0x${code}` })
.encodeABI()

let signed = await conf.eoa.signTransaction({
from: conf.eoa.address,
data: data,
value: '0',
gasPrice: '0',
})

let receipt = await web3.eth.sendSignedTransaction(signed.rawTransaction)
let receipt = await web3.eth.sendSignedTransaction(signed.rawTransaction)

return {
contract: new web3.eth.Contract(abi, receipt.contractAddress),
receipt: receipt
}
return {
contract: new web3.eth.Contract(abi, receipt.contractAddress),
receipt: receipt
}
Comment on lines +16 to +36
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding explicit error handling for operations like file reading and contract deployment to enhance robustness.

}

// signAndSend signs a transactions and submits it to the network,
// returning a transaction hash and receipt
async function signAndSend(tx) {
const signedTx = await conf.eoa.signTransaction(tx)
// send transaction and make sure interaction was success
const receipt = await web3.eth.sendSignedTransaction(signedTx.rawTransaction)

return {
hash: signedTx.transactionHash,
receipt: receipt,
}
const signedTx = await conf.eoa.signTransaction(tx)
// send transaction and make sure interaction was success
const receipt = await web3.eth.sendSignedTransaction(signedTx.rawTransaction)

return {
hash: signedTx.transactionHash,
receipt: receipt,
}
Comment on lines +42 to +49
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add explicit error handling for the network operations to ensure robustness, especially in case of network failures.

}

// callRPCMethod accepts a method name and its params and
// makes a POST request to the JSON-RPC API server.
// Returns a promise for the response.
async function callRPCMethod(methodName, params) {
return chai.request('http://127.0.0.1:8545')
.post('/')
.set('Content-Type', 'application/json')
.set('Accept', 'application/json')
.send({
'jsonrpc': '2.0',
'method': methodName,
'id': 1,
'params': params
})
Comment on lines +52 to +65
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding error handling for the HTTP requests to handle potential issues like network errors or invalid responses.

}

exports.signAndSend = signAndSend
exports.deployContract = deployContract
exports.deployContract = deployContract
exports.callRPCMethod = callRPCMethod
Loading
Loading