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

feat: add gas price RPC methods #120

Merged
merged 14 commits into from
Nov 28, 2024
3 changes: 3 additions & 0 deletions cmd/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,9 @@ func setupJSONRPC(
// Transaction handlers
j.RegisterTxEndpoints(db)

// Gas handlers
j.RegisterGasEndpoints(db)

// Block handlers
j.RegisterBlockEndpoints(db)

Expand Down
10 changes: 10 additions & 0 deletions serve/filters/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ func (f *Manager) NewTransactionSubscription(conn conns.WSConnection) string {
return f.newSubscription(filterSubscription.NewTransactionSubscription(conn))
}

// NewGasPriceSubscription creates gas fee subscriptions for blocks with transactions (over WS)
func (f *Manager) NewGasPriceSubscription(conn conns.WSConnection) string {
return f.newSubscription(filterSubscription.NewGasPriceSubscription(conn))
}

// newSubscription adds new subscription to the subscription map
func (f *Manager) newSubscription(subscription subscription) string {
return f.subscriptions.addSubscription(subscription)
Expand Down Expand Up @@ -123,6 +128,11 @@ func (f *Manager) subscribeToEvents() {
// Send events to all `newHeads` subscriptions
f.subscriptions.sendEvent(filterSubscription.NewHeadsEvent, newBlock.Block)

// Send an event to the `newGasPrice` subscription when creating a block with transactions
if len(newBlock.Block.Txs) > 0 {
f.subscriptions.sendEvent(filterSubscription.NewGasPriceEvent, newBlock.Block)
}

for _, txResult := range newBlock.Results {
// Apply transaction to filters
f.updateFiltersWithTxResult(txResult)
Expand Down
45 changes: 45 additions & 0 deletions serve/filters/subscription/gas.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package subscription

import (
"fmt"

"github.com/gnolang/gno/tm2/pkg/bft/types"
"github.com/gnolang/tx-indexer/events"
"github.com/gnolang/tx-indexer/serve/conns"
"github.com/gnolang/tx-indexer/serve/methods"
"github.com/gnolang/tx-indexer/serve/spec"
)

const (
NewGasPriceEvent = "newGasPrice"
)

// GasPriceSubscription is the new-transactions type
// subscription
type GasPriceSubscription struct {
*baseSubscription
}

func NewGasPriceSubscription(conn conns.WSConnection) *GasPriceSubscription {
return &GasPriceSubscription{
baseSubscription: newBaseSubscription(conn),
}
}

func (b *GasPriceSubscription) GetType() events.Type {
return NewGasPriceEvent
}

func (b *GasPriceSubscription) WriteResponse(id string, data any) error {
block, ok := data.(*types.Block)
if !ok {
return fmt.Errorf("unable to cast txResult, %s", data)
}

gasPrices, err := methods.GetGasPricesByBlock(block)
if err != nil {
return err
}

return b.conn.WriteData(spec.NewJSONSubscribeResponse(id, gasPrices))
}
45 changes: 45 additions & 0 deletions serve/filters/subscription/gas_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package subscription

import (
"testing"

"github.com/gnolang/gno/tm2/pkg/bft/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/gnolang/tx-indexer/internal/mock"
"github.com/gnolang/tx-indexer/serve/methods"
"github.com/gnolang/tx-indexer/serve/spec"
)

func TestGasPriceSubscription_WriteResponse(t *testing.T) {
t.Parallel()

var (
capturedWrite any

mockBlock = &types.Block{}
mockGasPrices = []*methods.GasPrice{}
)

expectedGasPricesResponse := spec.NewJSONSubscribeResponse("", mockGasPrices)

mockConn := &mock.Conn{
WriteDataFn: func(data any) error {
capturedWrite = data

return nil
},
}

// Create the block subscription
gasPriceSubscription := NewGasPriceSubscription(mockConn)

// Write the response
require.NoError(t, gasPriceSubscription.WriteResponse("", mockBlock))

// Make sure the captured data matches
require.NotNil(t, capturedWrite)

assert.Equal(t, expectedGasPricesResponse, capturedWrite)
}
117 changes: 117 additions & 0 deletions serve/handlers/gas/gas.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package gas

import (
"fmt"
"strconv"

"github.com/gnolang/gno/tm2/pkg/bft/types"
"github.com/vektah/gqlparser/v2/gqlerror"

"github.com/gnolang/tx-indexer/serve/metadata"
"github.com/gnolang/tx-indexer/serve/methods"
"github.com/gnolang/tx-indexer/serve/spec"
)

const DefaultBlockRangeSize = 1_000

type Handler struct {
storage Storage
}

func NewHandler(storage Storage) *Handler {
return &Handler{
storage: storage,
}
}

func (h *Handler) GetGasPriceHandler(
_ *metadata.Metadata,
params []any,
) (any, *spec.BaseJSONError) {
// Check the params
if len(params) != 0 && len(params) != 2 {
return nil, spec.GenerateInvalidParamCountError()
}

var toBlockNum, fromBlockNum uint64

if len(params) == 0 {
latestHeight, err := h.storage.GetLatestHeight()
if err != nil {
return nil, spec.GenerateResponseError(err)
}

fromBlockNum, toBlockNum = initializeDefaultBlockRangeByHeight(latestHeight)
} else {
fromBlockNum, toBlockNum = parseBlockRangeByParams(params)
}

response, err := h.getGasPriceBy(fromBlockNum, toBlockNum)
if err != nil {
return nil, spec.GenerateResponseError(err)
}

return response, nil
}

func (h *Handler) getGasPriceBy(fromBlockNum, toBlockNum uint64) ([]*methods.GasPrice, error) {
it, err := h.
storage.
BlockIterator(
zivkovicmilos marked this conversation as resolved.
Show resolved Hide resolved
fromBlockNum,
toBlockNum,
)
if err != nil {
return nil, gqlerror.Wrap(err)
}

defer it.Close()

blocks := make([]*types.Block, 0)

for {
if !it.Next() {
break
}

block, itErr := it.Value()
if itErr != nil {
return nil, err
}

blocks = append(blocks, block)
}

gasPrices, err := methods.GetGasPricesByBlocks(blocks)
if err != nil {
return nil, err
}

return gasPrices, nil
}

func initializeDefaultBlockRangeByHeight(latestHeight uint64) (uint64, uint64) {
toBlockNum := latestHeight

var fromBlockNum uint64

if latestHeight > DefaultBlockRangeSize {
fromBlockNum = latestHeight - DefaultBlockRangeSize
}

return fromBlockNum, toBlockNum
}

func parseBlockRangeByParams(params []any) (uint64, uint64) {
fromBlockNum, err := strconv.ParseUint(fmt.Sprintf("%v", params[0]), 10, 64)
if err != nil {
fromBlockNum = 0
}

toBlockNum, err := strconv.ParseUint(fmt.Sprintf("%v", params[1]), 10, 64)
if err != nil {
toBlockNum = 0
}

return fromBlockNum, toBlockNum
}
41 changes: 41 additions & 0 deletions serve/handlers/gas/gas_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package gas

import (
"testing"

"github.com/gnolang/tx-indexer/serve/spec"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestGetGasPriceHandler_InvalidParams(t *testing.T) {
t.Parallel()

testTable := []struct {
name string
params []any
}{
{
"invalid param length",
[]any{1},
},
{
"invalid param type",
[]any{"totally invalid param type"},
},
}

for _, testCase := range testTable {
t.Run(testCase.name, func(t *testing.T) {
t.Parallel()

h := NewHandler(&mockStorage{})

response, err := h.GetGasPriceHandler(nil, testCase.params)
assert.Nil(t, response)

require.NotNil(t, err)
assert.Equal(t, spec.InvalidParamsErrorCode, err.Code)
})
}
}
34 changes: 34 additions & 0 deletions serve/handlers/gas/mocks_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package gas

import (
"github.com/gnolang/gno/tm2/pkg/bft/types"
"github.com/gnolang/tx-indexer/storage"
)

type getLatestHeight func() (uint64, error)

type blockIterator func(uint64, uint64) (storage.Iterator[*types.Block], error)

type mockStorage struct {
getLatestHeightFn getLatestHeight
blockIteratorFn blockIterator
}

func (m *mockStorage) GetLatestHeight() (uint64, error) {
if m.getLatestHeightFn != nil {
return m.getLatestHeightFn()
}

return 0, nil
}

func (m *mockStorage) BlockIterator(
fromBlockNum,
toBlockNum uint64,
) (storage.Iterator[*types.Block], error) {
if m.blockIteratorFn != nil {
return m.blockIteratorFn(fromBlockNum, toBlockNum)
}

return nil, nil
}
14 changes: 14 additions & 0 deletions serve/handlers/gas/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package gas

import (
"github.com/gnolang/gno/tm2/pkg/bft/types"
"github.com/gnolang/tx-indexer/storage"
)

type Storage interface {
// GetLatestHeight returns the latest block height from the storage
GetLatestHeight() (uint64, error)

// BlockIterator iterates over Blocks, limiting the results to be between the provided block numbers
BlockIterator(fromBlockNum, toBlockNum uint64) (storage.Iterator[*types.Block], error)
}
2 changes: 2 additions & 0 deletions serve/handlers/subs/subs.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@ func (h *Handler) subscribe(connID, eventType string) (string, error) {
return h.filterManager.NewBlockSubscription(conn), nil
case subscription.NewTransactionsEvent:
return h.filterManager.NewTransactionSubscription(conn), nil
case subscription.NewGasPriceEvent:
return h.filterManager.NewGasPriceSubscription(conn), nil
default:
return "", fmt.Errorf("invalid event type: %s", eventType)
}
Expand Down
11 changes: 11 additions & 0 deletions serve/jsonrpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/gnolang/tx-indexer/serve/conns/wsconn"
"github.com/gnolang/tx-indexer/serve/filters"
"github.com/gnolang/tx-indexer/serve/handlers/block"
"github.com/gnolang/tx-indexer/serve/handlers/gas"
"github.com/gnolang/tx-indexer/serve/handlers/subs"
"github.com/gnolang/tx-indexer/serve/handlers/tx"
"github.com/gnolang/tx-indexer/serve/metadata"
Expand Down Expand Up @@ -127,6 +128,16 @@ func (j *JSONRPC) RegisterTxEndpoints(db tx.Storage) {
)
}

// RegisterGasPriceEndpoints registers the gas price endpoints
func (j *JSONRPC) RegisterGasEndpoints(db gas.Storage) {
gasPriceHandler := gas.NewHandler(db)

j.RegisterHandler(
"getGasPrice",
gasPriceHandler.GetGasPriceHandler,
)
}

// RegisterBlockEndpoints registers the block endpoints
func (j *JSONRPC) RegisterBlockEndpoints(db block.Storage) {
blockHandler := block.NewHandler(db)
Expand Down
Loading
Loading