diff --git a/serve/filters/filter/tx.go b/serve/filters/filter/tx.go new file mode 100644 index 00000000..a740be98 --- /dev/null +++ b/serve/filters/filter/tx.go @@ -0,0 +1,107 @@ +package filter + +import ( + "github.com/gnolang/gno/tm2/pkg/bft/types" +) + +type Options struct { + GasUsed struct{ Min, Max *int64 } + GasWanted struct{ Min, Max *int64 } + GasLimit struct{ Min, Max *int64 } +} + +// TxFilter holds a slice of transaction results. +// It provides methods to manipulate and query the transactions. +type TxFilter struct { + opts Options + *baseFilter + txs []*types.TxResult +} + +// NewTxFilter creates a new TxFilter object. +func NewTxFilter(opts Options) *TxFilter { + return &TxFilter{ + baseFilter: newBaseFilter(TxFilterType), + txs: make([]*types.TxResult, 0), + opts: opts, + } +} + +// GetHashes iterates over all transactions in the filter and returns their hashes. +func (tf *TxFilter) GetHashes() [][]byte { + tf.Lock() + defer tf.Unlock() + + hashes := make([][]byte, 0, len(tf.txs)) + + for _, txr := range tf.txs { + var hash []byte + + if txr != nil && txr.Tx != nil { + hash = txr.Tx.Hash() + } + + hashes = append(hashes, hash) + } + + return hashes +} + +func (tf *TxFilter) UpdateWithTx(tx *types.TxResult) { + tf.Lock() + defer tf.Unlock() + + tf.txs = append(tf.txs, tx) +} + +// Apply applies all added conditions to the transactions in the filter. +// +// It returns a slice of `TxResult` that satisfy all the conditions. If no conditions are set, +// it returns all transactions in the filter. Also, if the filter value is invalid filter will not +// be applied. +func (tf *TxFilter) Apply() []*types.TxResult { + tf.Lock() + defer tf.Unlock() + + return checkOpts(tf.txs, tf.opts) +} + +func checkOpts(txs []*types.TxResult, opts Options) []*types.TxResult { + filtered := make([]*types.TxResult, 0, len(txs)) + + for _, tx := range txs { + if checkFilterCondition(tx, opts) { + filtered = append(filtered, tx) + } + } + + return filtered +} + +func checkFilterCondition(tx *types.TxResult, opts Options) bool { + if opts.GasLimit.Max != nil && tx.Response.GasUsed > *opts.GasLimit.Max { + return false + } + + if opts.GasLimit.Min != nil && tx.Response.GasUsed < *opts.GasLimit.Min { + return false + } + + if opts.GasUsed.Max != nil && tx.Response.GasUsed > *opts.GasUsed.Max { + return false + } + + if opts.GasUsed.Min != nil && tx.Response.GasUsed < *opts.GasUsed.Min { + return false + } + + if opts.GasWanted.Max != nil && tx.Response.GasWanted > *opts.GasWanted.Max { + return false + } + + if opts.GasWanted.Min != nil && tx.Response.GasWanted < *opts.GasWanted.Min { + return false + } + + return true +} diff --git a/serve/filters/filter/tx_test.go b/serve/filters/filter/tx_test.go new file mode 100644 index 00000000..0dc02015 --- /dev/null +++ b/serve/filters/filter/tx_test.go @@ -0,0 +1,320 @@ +package filter + +import ( + "fmt" + "testing" + + abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" + "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetHashes(t *testing.T) { + t.Parallel() + + txs := []*types.TxResult{ + {Tx: []byte(`c25dda249cdece9d908cc33adcd16aa05e20290f`)}, + {Tx: []byte(`71ac9eed6a76a285ae035fe84a251d56ae9485a4`)}, + {Tx: []byte(`356a192b7913b04c54574d18c28d46e6395428ab`)}, + {Tx: []byte(`da4b9237bacccdf19c0760cab7aec4a8359010b0`)}, + {Tx: []byte(`77de68daecd823babbb58edb1c8e14d7106e83bb`)}, + {Tx: []byte(`1b6453892473a467d07372d45eb05abc2031647a`)}, + {Tx: []byte(`ac3478d69a3c81fa62e60f5c3696165a4e5e6ac4`)}, + {Tx: []byte(`c1dfd96eea8cc2b62785275bca38ac261256e278`)}, + } + + f := NewTxFilter(Options{}) + + for _, tx := range txs { + f.UpdateWithTx(tx) + } + + hashes := f.GetHashes() + require.Len( + t, hashes, 8, + fmt.Sprintf("There should be 8 hashes in the filter: %v", len(hashes)), + ) + + for i, hs := range hashes { + assert.Equal( + t, txs[i].Tx.Hash(), hs, + fmt.Sprintf("The hash should match the expected hash: %v", txs[i].Tx.Hash()), + ) + } +} + +func TestApplyFilters(t *testing.T) { + t.Parallel() + + txs := []*types.TxResult{ + { + Height: 100, + Index: 0, + Tx: []byte(`sampleTx0`), + Response: abci.ResponseDeliverTx{ + GasWanted: 1000, + GasUsed: 900, + ResponseBase: abci.ResponseBase{ + Data: []byte(`data0`), + }, + }, + }, + { + Height: 101, + Index: 1, + Tx: []byte(`sampleTx1`), + Response: abci.ResponseDeliverTx{ + GasWanted: 1200, + GasUsed: 1100, + ResponseBase: abci.ResponseBase{ + Data: []byte(`data1`), + }, + }, + }, + { + Height: 102, + Index: 2, + Tx: []byte(`sampleTx2`), + Response: abci.ResponseDeliverTx{ + GasWanted: 1000, + GasUsed: 1400, + ResponseBase: abci.ResponseBase{ + Data: []byte(`data2`), + }, + }, + }, + { + Height: 103, + Index: 3, + Tx: []byte(`sampleTx3`), + Response: abci.ResponseDeliverTx{ + GasWanted: 1200, + GasUsed: 900, + ResponseBase: abci.ResponseBase{ + Data: []byte(`data3`), + }, + }, + }, + { + Height: 104, + Index: 4, + Tx: []byte(`sampleTx4`), + Response: abci.ResponseDeliverTx{ + GasWanted: 1100, + GasUsed: 1000, + ResponseBase: abci.ResponseBase{ + Data: []byte(`data4`), + }, + }, + }, + } + + tests := []struct { + options Options + name string + expected []*types.TxResult + }{ + { + name: "no filter", + options: Options{}, + expected: txs, + }, + { + name: "min gas used is 0", + options: Options{ + GasUsed: struct{ Min, Max *int64 }{int64Ptr(0), int64Ptr(1000)}, + }, + expected: []*types.TxResult{txs[0], txs[3], txs[4]}, + }, + { + name: "invalid gas used", + options: Options{ + GasUsed: struct{ Min, Max *int64 }{int64Ptr(1000), int64Ptr(900)}, + }, + expected: []*types.TxResult{}, + }, + { + name: "filter by gas wanted 1", + options: Options{ + GasWanted: struct{ Min, Max *int64 }{int64Ptr(1100), int64Ptr(1200)}, + }, + expected: []*types.TxResult{txs[1], txs[3], txs[4]}, + }, + { + name: "gas wanted min, max is same value", + options: Options{ + GasWanted: struct{ Min, Max *int64 }{int64Ptr(1000), int64Ptr(1000)}, + }, + expected: []*types.TxResult{txs[0], txs[2]}, + }, + { + name: "filter by gas used 2", + options: Options{ + GasUsed: struct{ Min, Max *int64 }{int64Ptr(900), int64Ptr(1000)}, + }, + expected: []*types.TxResult{txs[0], txs[3], txs[4]}, + }, + { + name: "gas used min, max is same value", + options: Options{ + GasUsed: struct{ Min, Max *int64 }{int64Ptr(1000), int64Ptr(1000)}, + }, + expected: []*types.TxResult{txs[4]}, + }, + { + name: "filter by gas wanted is invalid", + options: Options{ + GasWanted: struct{ Min, Max *int64 }{int64Ptr(1200), int64Ptr(1100)}, + }, + expected: []*types.TxResult{}, + }, + { + name: "gas used filter is invalid", + options: Options{ + GasUsed: struct{ Min, Max *int64 }{int64Ptr(1000), int64Ptr(900)}, + }, + expected: []*types.TxResult{}, + }, + { + name: "gas limit min value is nil", + options: Options{ + GasLimit: struct{ Min, Max *int64 }{nil, int64Ptr(1000)}, + }, + expected: []*types.TxResult{txs[0], txs[3], txs[4]}, + }, + { + name: "gas limit max value is nil", + options: Options{ + GasLimit: struct{ Min, Max *int64 }{int64Ptr(1100), nil}, + }, + expected: []*types.TxResult{txs[1], txs[2]}, + }, + { + name: "gas limit range is valid", + options: Options{ + GasLimit: struct{ Min, Max *int64 }{int64Ptr(900), int64Ptr(1000)}, + }, + expected: []*types.TxResult{txs[0], txs[3], txs[4]}, + }, + { + name: "gas limit both min and max are nil", + options: Options{ + GasLimit: struct{ Min, Max *int64 }{nil, nil}, + }, + expected: txs, + }, + { + name: "gas limit min is larger than max", + options: Options{ + GasLimit: struct{ Min, Max *int64 }{int64Ptr(1000), int64Ptr(900)}, + }, + expected: []*types.TxResult{}, + }, + { + name: "gas used min is nil", + options: Options{ + GasUsed: struct{ Min, Max *int64 }{nil, int64Ptr(1000)}, + }, + expected: []*types.TxResult{txs[0], txs[3], txs[4]}, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + f := NewTxFilter(tt.options) + + for _, tx := range txs { + f.UpdateWithTx(tx) + } + + filtered := f.Apply() + require.Len( + t, filtered, len(tt.expected), + fmt.Sprintf( + "There should be one transaction after applying filters: %v", + len(tt.expected), + ), + ) + + for i, tx := range filtered { + assert.Equal( + t, tt.expected[i], tx, + fmt.Sprintf( + "The filtered transaction should match the expected transaction: %v", + tt.expected[i], + ), + ) + } + }) + } +} + +func TestApplyFiltersWithLargeData(t *testing.T) { + t.Parallel() + + const txCount = 100000 + + txs := make([]*types.TxResult, txCount) + + for i := 0; i < txCount; i++ { + txs[i] = &types.TxResult{ + Height: int64(i / 10000), + Index: uint32(i), + Tx: []byte(fmt.Sprintf("sampleTx%d", i)), + Response: abci.ResponseDeliverTx{ + GasWanted: int64(1000 + i%200), + GasUsed: int64(900 + i%100), + ResponseBase: abci.ResponseBase{ + Data: []byte(fmt.Sprintf("data%d", i)), + }, + }, + } + } + + tests := []struct { + options Options + name string + expected int + }{ + { + name: "no filter", + options: Options{}, + expected: txCount, + }, + { + name: "filter by gas used", + options: Options{ + GasUsed: struct{ Min, Max *int64 }{int64Ptr(950), int64Ptr(1000)}, + }, + expected: txCount / 2, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + f := NewTxFilter(tt.options) + + for _, tx := range txs { + f.UpdateWithTx(tx) + } + + filtered := f.Apply() + require.Len( + t, filtered, tt.expected, + fmt.Sprintf( + "There should be %d transactions after applying filters. got %d", + tt.expected, len(filtered), + ), + ) + }) + } +} + +func int64Ptr(i int64) *int64 { return &i } diff --git a/serve/filters/filter/types.go b/serve/filters/filter/types.go index 4d6519c3..a3fc5c43 100644 --- a/serve/filters/filter/types.go +++ b/serve/filters/filter/types.go @@ -4,4 +4,5 @@ type Type string const ( BlockFilterType Type = "BlockFilter" + TxFilterType Type = "TxFilter" )