From 7be2d775571c31a782a669dae0612c4e82de93cb Mon Sep 17 00:00:00 2001 From: Amir Date: Tue, 21 Apr 2026 12:59:41 -0700 Subject: [PATCH 1/4] BACKPORT-CONFLICT --- evmrpc/filter.go | 31 +++++++++++++--- evmrpc/filter_test.go | 86 ++++++++++++++++++++++++++++++++++--------- 2 files changed, 95 insertions(+), 22 deletions(-) diff --git a/evmrpc/filter.go b/evmrpc/filter.go index 7a0541e105..43156e939e 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,18 @@ func (a *FilterAPI) NewBlockFilter( return curFilterID, nil } +<<<<<<< HEAD +======= +func (a *FilterAPI) NewPendingTransactionFilter( + _ *bool, +) (id ethrpc.ID, err error) { + defer recordMetricsWithError(fmt.Sprintf("%s_newPendingTransactionFilter", a.namespace), a.connectionType, time.Now(), err) + return "", &ErrEVMNotSupported{Msg: "eth_newPendingTransactionFilter is not supported on Sei EVM RPC"} +} + +const GetFilterChangesMethod = "getFilterChanges" + +>>>>>>> 0ad5733 (fix(evmrpc): return empty array instead of null for eth_getFilterLogs and eth_getFilterChanges (#3292)) func (a *FilterAPI) GetFilterChanges( ctx context.Context, filterID ethrpc.ID, @@ -448,6 +460,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 +480,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 +508,8 @@ func (a *FilterAPI) GetFilterChanges( } } +const GetFilterLogsMethod = "getFilterLogs" + func (a *FilterAPI) GetFilterLogs( ctx context.Context, filterID ethrpc.ID, @@ -514,6 +532,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 +956,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..8823dee8eb 100644 --- a/evmrpc/filter_test.go +++ b/evmrpc/filter_test.go @@ -52,7 +52,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 +61,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 +75,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 +294,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 +307,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 +317,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 +329,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 +338,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 +346,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 +360,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 +376,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 +393,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 +583,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) +} From dc613188a5d5010d87797e10538d2dd71a00cbe6 Mon Sep 17 00:00:00 2001 From: Amir Deris Date: Tue, 28 Apr 2026 09:57:18 -0700 Subject: [PATCH 2/4] resolving merge conflict --- evmrpc/filter.go | 3 --- evmrpc/rpc_unsupported.go | 21 +++++++++++++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 evmrpc/rpc_unsupported.go diff --git a/evmrpc/filter.go b/evmrpc/filter.go index 43156e939e..09b5a86c85 100644 --- a/evmrpc/filter.go +++ b/evmrpc/filter.go @@ -430,8 +430,6 @@ func (a *FilterAPI) NewBlockFilter( return curFilterID, nil } -<<<<<<< HEAD -======= func (a *FilterAPI) NewPendingTransactionFilter( _ *bool, ) (id ethrpc.ID, err error) { @@ -441,7 +439,6 @@ func (a *FilterAPI) NewPendingTransactionFilter( const GetFilterChangesMethod = "getFilterChanges" ->>>>>>> 0ad5733 (fix(evmrpc): return empty array instead of null for eth_getFilterLogs and eth_getFilterChanges (#3292)) func (a *FilterAPI) GetFilterChanges( ctx context.Context, filterID ethrpc.ID, diff --git a/evmrpc/rpc_unsupported.go b/evmrpc/rpc_unsupported.go new file mode 100644 index 0000000000..6ca4a6a01a --- /dev/null +++ b/evmrpc/rpc_unsupported.go @@ -0,0 +1,21 @@ +package evmrpc + +// ErrCodeEVMNotSupported is the JSON-RPC error code (-32000) for EVM RPC methods that +// Sei exposes explicitly but does not implement (clear client feedback vs -32601 method missing). +const ErrCodeEVMNotSupported = -32000 + +// ErrCodeBlobsNotSupported is the historical name for ErrCodeEVMNotSupported (eth_blobBaseFee). +const ErrCodeBlobsNotSupported = ErrCodeEVMNotSupported + +// ErrEVMNotSupported is returned for such methods; the JSON-RPC layer maps it to code ErrCodeEVMNotSupported. +type ErrEVMNotSupported struct { + Msg string +} + +func (e *ErrEVMNotSupported) Error() string { + return e.Msg +} + +func (e *ErrEVMNotSupported) ErrorCode() int { + return ErrCodeEVMNotSupported +} From 95f0c1819b72e6e799d91cdad846c60b119e1860 Mon Sep 17 00:00:00 2001 From: Amir Deris Date: Tue, 28 Apr 2026 10:08:52 -0700 Subject: [PATCH 3/4] fixed missing import --- evmrpc/filter_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/evmrpc/filter_test.go b/evmrpc/filter_test.go index 8823dee8eb..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) { From d4d9455c691a6eaf6c42fb3c5a29aef9d487d937 Mon Sep 17 00:00:00 2001 From: Amir Deris Date: Tue, 28 Apr 2026 11:04:27 -0700 Subject: [PATCH 4/4] removed extra code not in pr 3292 --- evmrpc/filter.go | 7 ------- evmrpc/rpc_unsupported.go | 21 --------------------- 2 files changed, 28 deletions(-) delete mode 100644 evmrpc/rpc_unsupported.go diff --git a/evmrpc/filter.go b/evmrpc/filter.go index 09b5a86c85..2b8e20fbe8 100644 --- a/evmrpc/filter.go +++ b/evmrpc/filter.go @@ -430,13 +430,6 @@ func (a *FilterAPI) NewBlockFilter( return curFilterID, nil } -func (a *FilterAPI) NewPendingTransactionFilter( - _ *bool, -) (id ethrpc.ID, err error) { - defer recordMetricsWithError(fmt.Sprintf("%s_newPendingTransactionFilter", a.namespace), a.connectionType, time.Now(), err) - return "", &ErrEVMNotSupported{Msg: "eth_newPendingTransactionFilter is not supported on Sei EVM RPC"} -} - const GetFilterChangesMethod = "getFilterChanges" func (a *FilterAPI) GetFilterChanges( diff --git a/evmrpc/rpc_unsupported.go b/evmrpc/rpc_unsupported.go deleted file mode 100644 index 6ca4a6a01a..0000000000 --- a/evmrpc/rpc_unsupported.go +++ /dev/null @@ -1,21 +0,0 @@ -package evmrpc - -// ErrCodeEVMNotSupported is the JSON-RPC error code (-32000) for EVM RPC methods that -// Sei exposes explicitly but does not implement (clear client feedback vs -32601 method missing). -const ErrCodeEVMNotSupported = -32000 - -// ErrCodeBlobsNotSupported is the historical name for ErrCodeEVMNotSupported (eth_blobBaseFee). -const ErrCodeBlobsNotSupported = ErrCodeEVMNotSupported - -// ErrEVMNotSupported is returned for such methods; the JSON-RPC layer maps it to code ErrCodeEVMNotSupported. -type ErrEVMNotSupported struct { - Msg string -} - -func (e *ErrEVMNotSupported) Error() string { - return e.Msg -} - -func (e *ErrEVMNotSupported) ErrorCode() int { - return ErrCodeEVMNotSupported -}