diff --git a/evmrpc/filter.go b/evmrpc/filter.go index 7a0541e105..2b8e20fbe8 100644 --- a/evmrpc/filter.go +++ b/evmrpc/filter.go @@ -32,8 +32,6 @@ import ( var logger = seilog.NewLogger("evmrpc") -const TxSearchPerPage = 10 - const ( // DB Concurrency Read Limit MaxDBReadConcurrency = 16 @@ -388,6 +386,8 @@ func (a *FilterAPI) updateFilterAccess(filterID ethrpc.ID) { } } +const NewFilterMethod = "newFilter" + func (a *FilterAPI) NewFilter( ctx context.Context, crit filters.FilterCriteria, @@ -430,6 +430,8 @@ func (a *FilterAPI) NewBlockFilter( return curFilterID, nil } +const GetFilterChangesMethod = "getFilterChanges" + func (a *FilterAPI) GetFilterChanges( ctx context.Context, filterID ethrpc.ID, @@ -448,6 +450,7 @@ func (a *FilterAPI) GetFilterChanges( // Update access time a.updateFilterAccess(filterID) + result := []*ethtypes.Log{} switch filter.typ { case BlocksSubscription: hashes, cursor, err := a.getBlockHeadersAfter(ctx, filter.blockCursor) @@ -467,16 +470,19 @@ func (a *FilterAPI) GetFilterChanges( case LogsSubscription: // filter by hash would have no updates if it has previously queried for this crit if filter.fc.BlockHash != nil && filter.lastToHeight > 0 { - return nil, nil + return result, nil } // filter with a ToBlock would have no updates if it has previously queried for this crit if filter.fc.ToBlock != nil && filter.lastToHeight >= filter.fc.ToBlock.Int64() { - return nil, nil + return result, nil } logs, lastToHeight, err := a.logFetcher.GetLogsByFilters(ctx, filter.fc, filter.lastToHeight) if err != nil { return nil, err } + if logs == nil { + logs = result + } // Update filter with write lock a.filtersMu.Lock() @@ -492,6 +498,8 @@ func (a *FilterAPI) GetFilterChanges( } } +const GetFilterLogsMethod = "getFilterLogs" + func (a *FilterAPI) GetFilterLogs( ctx context.Context, filterID ethrpc.ID, @@ -514,6 +522,9 @@ func (a *FilterAPI) GetFilterLogs( if err != nil { return nil, err } + if logs == nil { + logs = []*ethtypes.Log{} + } // Update filter with write lock a.filtersMu.Lock() @@ -935,7 +946,7 @@ func (f *LogFetcher) tryFilterLogsRange(ctx context.Context, fromBlock, toBlock } if len(logs) == 0 { - return logs, nil + return []*ethtypes.Log{}, nil } return f.normalizeRangeQueryLogs(ctx, logs, crit) diff --git a/evmrpc/filter_test.go b/evmrpc/filter_test.go index 43dd26006d..bef8df7ddf 100644 --- a/evmrpc/filter_test.go +++ b/evmrpc/filter_test.go @@ -7,6 +7,8 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/require" + + "github.com/sei-protocol/sei-chain/evmrpc" ) func TestFilterNew(t *testing.T) { @@ -52,7 +54,7 @@ func TestFilterNew(t *testing.T) { filterCriteria["fromBlock"] = tt.fromBlock filterCriteria["toBlock"] = tt.toBlock } - resObj := sendRequestGood(t, "newFilter", filterCriteria) + resObj := sendRequestGood(t, evmrpc.NewFilterMethod, filterCriteria) _, errExists := resObj["error"] if tt.wantErr { @@ -61,7 +63,7 @@ func TestFilterNew(t *testing.T) { require.False(t, errExists, "error should not exist") got := resObj["result"].(string) // make sure next filter id is not equal to this one - resObj := sendRequestGood(t, "newFilter", filterCriteria) + resObj := sendRequestGood(t, evmrpc.NewFilterMethod, filterCriteria) got2 := resObj["result"].(string) require.NotEqual(t, got, got2) } @@ -75,7 +77,7 @@ func TestFilterUninstall(t *testing.T) { filterCriteria := map[string]interface{}{ "fromBlock": "0x1", } - resObj := sendRequestGood(t, "newFilter", filterCriteria) + resObj := sendRequestGood(t, evmrpc.NewFilterMethod, filterCriteria) filterId := resObj["result"].(string) require.NotEmpty(t, filterId) @@ -294,10 +296,10 @@ func TestFilterGetFilterLogs(t *testing.T) { "fromBlock": "0x2", "toBlock": "0x2", } - resObj := sendRequestGood(t, "newFilter", filterCriteria) + resObj := sendRequestGood(t, evmrpc.NewFilterMethod, filterCriteria) filterId := resObj["result"].(string) - resObj = sendRequest(t, TestPort, "getFilterLogs", filterId) + resObj = sendRequest(t, TestPort, evmrpc.GetFilterLogsMethod, filterId) logs := resObj["result"].([]interface{}) require.Equal(t, 4, len(logs)) for _, log := range logs { @@ -307,7 +309,7 @@ func TestFilterGetFilterLogs(t *testing.T) { // error: filter id does not exist nonexistentFilterId := 1000 - resObj = sendRequest(t, TestPort, "getFilterLogs", nonexistentFilterId) + resObj = sendRequest(t, TestPort, evmrpc.GetFilterLogsMethod, nonexistentFilterId) _, ok := resObj["error"] require.True(t, ok) } @@ -317,10 +319,10 @@ func TestFilterGetFilterChanges(t *testing.T) { filterCriteria := map[string]interface{}{ "fromBlock": "0x2", } - resObj := sendRequest(t, TestPort, "newFilter", filterCriteria) + resObj := sendRequest(t, TestPort, evmrpc.NewFilterMethod, filterCriteria) filterId := resObj["result"].(string) - resObj = sendRequest(t, TestPort, "getFilterChanges", filterId) + resObj = sendRequest(t, TestPort, evmrpc.GetFilterChangesMethod, filterId) logs := resObj["result"].([]interface{}) // After tightening block/receipt matching, fromBlock=0x2 now yields 5 logs total require.Equal(t, 5, len(logs)) @@ -329,7 +331,7 @@ func TestFilterGetFilterChanges(t *testing.T) { // error: filter id does not exist nonExistingFilterId := 1000 - resObj = sendRequest(t, TestPort, "getFilterChanges", nonExistingFilterId) + resObj = sendRequest(t, TestPort, evmrpc.GetFilterChangesMethod, nonExistingFilterId) _, ok := resObj["error"] require.True(t, ok) } @@ -338,7 +340,7 @@ func TestFilterBlockFilter(t *testing.T) { t.Parallel() resObj := sendRequestGood(t, "newBlockFilter") blockFilterId := resObj["result"].(string) - resObj = sendRequestGood(t, "getFilterChanges", blockFilterId) + resObj = sendRequestGood(t, evmrpc.GetFilterChangesMethod, blockFilterId) hashesInterface := resObj["result"].([]interface{}) for _, hashInterface := range hashesInterface { hash := hashInterface.(string) @@ -346,7 +348,7 @@ func TestFilterBlockFilter(t *testing.T) { require.Equal(t, "0x", hash[:2]) } // query again to make sure cursor is updated - resObj = sendRequestGood(t, "getFilterChanges", blockFilterId) + resObj = sendRequestGood(t, evmrpc.GetFilterChangesMethod, blockFilterId) hashesInterface = resObj["result"].([]interface{}) for _, hashInterface := range hashesInterface { hash := hashInterface.(string) @@ -360,13 +362,13 @@ func TestFilterExpiration(t *testing.T) { filterCriteria := map[string]interface{}{ "fromBlock": "0x1", } - resObj := sendRequestGood(t, "newFilter", filterCriteria) + resObj := sendRequestGood(t, evmrpc.NewFilterMethod, filterCriteria) filterId := resObj["result"].(string) // wait for filter to expire time.Sleep(2 * filterTimeoutDuration) - resObj = sendRequest(t, TestPort, "getFilterLogs", filterId) + resObj = sendRequest(t, TestPort, evmrpc.GetFilterLogsMethod, filterId) _, ok := resObj["error"] require.True(t, ok) } @@ -376,12 +378,12 @@ func TestFilterGetFilterLogsKeepsFilterAlive(t *testing.T) { filterCriteria := map[string]interface{}{ "fromBlock": "0x1", } - resObj := sendRequestGood(t, "newFilter", filterCriteria) + resObj := sendRequestGood(t, evmrpc.NewFilterMethod, filterCriteria) filterId := resObj["result"].(string) for i := 0; i < 5; i++ { // should keep filter alive - resObj = sendRequestGood(t, "getFilterLogs", filterId) + resObj = sendRequestGood(t, evmrpc.GetFilterLogsMethod, filterId) _, ok := resObj["error"] require.False(t, ok) time.Sleep(filterTimeoutDuration / 2) @@ -393,12 +395,12 @@ func TestFilterGetFilterChangesKeepsFilterAlive(t *testing.T) { filterCriteria := map[string]interface{}{ "fromBlock": "0x1", } - resObj := sendRequestGood(t, "newFilter", filterCriteria) + resObj := sendRequestGood(t, evmrpc.NewFilterMethod, filterCriteria) filterId := resObj["result"].(string) for i := 0; i < 5; i++ { // should keep filter alive - resObj = sendRequestGood(t, "getFilterChanges", filterId) + resObj = sendRequestGood(t, evmrpc.GetFilterChangesMethod, filterId) _, ok := resObj["error"] require.False(t, ok) time.Sleep(filterTimeoutDuration / 2) @@ -583,3 +585,55 @@ func TestCollectLogsEvmTransactionIndex(t *testing.T) { require.Len(t, seen, len(expectedByHash), "should observe logs for all expected EVM txs in block 2") } + +// TestFilterGetFilterChangesEmptyOnExhaustedBoundedFilter asserts that +// eth_getFilterChanges returns [] (not null) once a bounded filter's range is +// fully consumed (Ethereum JSON-RPC spec requires an array, never null). +func TestFilterGetFilterChangesEmptyOnExhaustedBoundedFilter(t *testing.T) { + t.Parallel() + filterCriteria := map[string]interface{}{ + "fromBlock": "0x2", + "toBlock": "0x2", + } + resObj := sendRequestGood(t, evmrpc.NewFilterMethod, filterCriteria) + filterId := resObj["result"].(string) + + // First call: consumes the range; result may be non-empty, but must be an array. + resObj = sendRequest(t, TestPort, evmrpc.GetFilterChangesMethod, filterId) + _, hasErr := resObj["error"] + require.False(t, hasErr) + require.NotNil(t, resObj["result"], "first getFilterChanges should return [] not null") + _, ok := resObj["result"].([]interface{}) + require.True(t, ok, "first getFilterChanges result should be an array") + + // Second call: range is exhausted; must return [] not null. + resObj = sendRequest(t, TestPort, evmrpc.GetFilterChangesMethod, filterId) + _, hasErr = resObj["error"] + require.False(t, hasErr) + require.NotNil(t, resObj["result"], "exhausted getFilterChanges should return [] not null") + logs, ok := resObj["result"].([]interface{}) + require.True(t, ok, "exhausted getFilterChanges result should be an array") + require.Empty(t, logs) +} + +// TestFilterGetFilterLogsEmptyResultIsArray asserts that eth_getFilterLogs +// returns [] (not null) when no logs match the filter criteria. +func TestFilterGetFilterLogsEmptyResultIsArray(t *testing.T) { + t.Parallel() + // Use an address that is guaranteed to have no logs in the mock data. + filterCriteria := map[string]interface{}{ + "fromBlock": "0x1", + "toBlock": "0x1", + "address": "0x0000000000000000000000000000000000000000", + } + resObj := sendRequestGood(t, evmrpc.NewFilterMethod, filterCriteria) + filterId := resObj["result"].(string) + + resObj = sendRequest(t, TestPort, evmrpc.GetFilterLogsMethod, filterId) + _, hasErr := resObj["error"] + require.False(t, hasErr) + require.NotNil(t, resObj["result"], "getFilterLogs with no matching logs should return [] not null") + logs, ok := resObj["result"].([]interface{}) + require.True(t, ok, "getFilterLogs result should be an array") + require.Empty(t, logs) +}