From 1814fea45caef74ac5f3d007bb3e512f93cbb2df Mon Sep 17 00:00:00 2001 From: Gui Iribarren Date: Wed, 24 Jul 2024 13:28:38 +0200 Subject: [PATCH] api: /chain/transactions endpoint --- api/api_types.go | 8 +- api/chain.go | 100 ++++++++++----------- vochain/indexer/db/db.go | 20 ++--- vochain/indexer/db/transactions.sql.go | 110 ++++++++++++++--------- vochain/indexer/indexer_test.go | 4 +- vochain/indexer/queries/transactions.sql | 21 +++-- vochain/indexer/transaction.go | 45 ++++++---- 7 files changed, 167 insertions(+), 141 deletions(-) diff --git a/api/api_types.go b/api/api_types.go index 216fdaa6d..c88d533c6 100644 --- a/api/api_types.go +++ b/api/api_types.go @@ -44,6 +44,7 @@ type AccountParams struct { type TransactionParams struct { PaginationParams Height uint64 `json:"height,omitempty"` + Type string `json:"type,omitempty"` } // FeesParams allows the client to filter fees @@ -269,13 +270,6 @@ type FeesList struct { Pagination *Pagination `json:"pagination"` } -// TODO: this struct should be deprecated, why blockNumber instead of blockHeight?? -type BlockTransactionsInfo struct { - BlockNumber uint64 `json:"blockNumber"` - TransactionsCount uint32 `json:"transactionCount"` - Transactions []TransactionMetadata `json:"transactions"` -} - type GenericTransactionWithInfo struct { TxContent json.RawMessage `json:"tx"` TxInfo indexertypes.Transaction `json:"txInfo"` diff --git a/api/chain.go b/api/chain.go index 8547c46c4..f16c493e3 100644 --- a/api/chain.go +++ b/api/chain.go @@ -18,8 +18,6 @@ import ( "go.vocdoni.io/dvote/vochain" "go.vocdoni.io/dvote/vochain/genesis" "go.vocdoni.io/dvote/vochain/indexer" - "go.vocdoni.io/proto/build/go/models" - "google.golang.org/protobuf/proto" ) const ( @@ -139,6 +137,14 @@ func (a *API) enableChainHandlers() error { ); err != nil { return err } + if err := a.Endpoint.RegisterMethod( + "/chain/transactions", + "GET", + apirest.MethodAccessTypePublic, + a.chainTxListHandler, + ); err != nil { + return err + } if err := a.Endpoint.RegisterMethod( "/chain/transactions/page/{page}", "GET", @@ -696,6 +702,32 @@ func (a *API) chainTxRefByIndexHandler(_ *apirest.APIdata, ctx *httprouter.HTTPC return ctx.Send(data, apirest.HTTPstatusOK) } +// chainTxListHandler +// +// @Summary List transactions +// @Description To get full transaction information use [/chain/transaction/{blockHeight}/{txIndex}](transaction-by-block-index).\nWhere transactionIndex is the index of the transaction on the containing block. +// @Tags Chain +// @Accept json +// @Produce json +// @Param page query number false "Page" +// @Param limit query number false "Items per page" +// @Param height query number false "Block height" +// @Success 200 {object} TransactionsList "List of transactions references" +// @Router /chain/transactions [get] +func (a *API) chainTxListHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext) error { + params, err := parseTransactionParams( + ctx.QueryParam(ParamPage), + ctx.QueryParam(ParamLimit), + ctx.QueryParam(ParamHeight), + ctx.QueryParam(ParamType), + ) + if err != nil { + return err + } + + return a.sendTransactionList(ctx, params) +} + // chainTxListByPageHandler // // @Summary List transactions @@ -713,6 +745,7 @@ func (a *API) chainTxListByPageHandler(_ *apirest.APIdata, ctx *httprouter.HTTPC ctx.URLParam(ParamPage), "", "", + "", ) if err != nil { return err @@ -725,71 +758,27 @@ func (a *API) chainTxListByPageHandler(_ *apirest.APIdata, ctx *httprouter.HTTPC // // @Summary Transactions in a block // @Description Given a block returns the list of transactions for that block +// @Deprecated +// @Description (deprecated, in favor of /chain/transactions?page=xxx&height=xxx) // @Tags Chain // @Accept json // @Produce json // @Param height path number true "Block height" // @Param page path number true "Page" -// @Success 200 {object} []TransactionMetadata +// @Success 200 {object} TransactionsList // @Router /chain/blocks/{height}/transactions/page/{page} [get] func (a *API) chainTxListByHeightAndPageHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext) error { params, err := parseTransactionParams( ctx.URLParam(ParamPage), "", ctx.URLParam(ParamHeight), + "", ) if err != nil { return err } - // TODO: replace all of this with the indexer method, - // from the other PR, TransactionListByHeight - block := a.vocapp.GetBlockByHeight(int64(params.Height)) - if block == nil { - return ErrBlockNotFound - } - blockTxs := &BlockTransactionsInfo{ - BlockNumber: params.Height, - TransactionsCount: uint32(len(block.Txs)), - Transactions: make([]TransactionMetadata, 0), - } - - count := 0 - for i := params.Page * params.Limit; i < len(block.Txs); i++ { - if count >= params.Limit { - break - } - signedTx := new(models.SignedTx) - tx := new(models.Tx) - var err error - if err := proto.Unmarshal(block.Txs[i], signedTx); err != nil { - return ErrUnmarshalingServerProto.WithErr(err) - } - if err := proto.Unmarshal(signedTx.Tx, tx); err != nil { - return ErrUnmarshalingServerProto.WithErr(err) - } - txType := string( - tx.ProtoReflect().WhichOneof( - tx.ProtoReflect().Descriptor().Oneofs().Get(0)).Name()) - - // TODO: can we avoid indexer Get calls in a loop? - txRef, err := a.indexer.GetTxHashReference(block.Txs[i].Hash()) - if err != nil { - return ErrTransactionNotFound - } - blockTxs.Transactions = append(blockTxs.Transactions, TransactionMetadata{ - Type: txType, - Index: int32(i), - Number: uint32(txRef.Index), - Hash: block.Txs[i].Hash(), - }) - count++ - } - data, err := json.Marshal(blockTxs) - if err != nil { - return err - } - return ctx.Send(data, apirest.HTTPstatusOK) + return a.sendTransactionList(ctx, params) } // sendTransactionList produces a filtered, paginated TransactionList, @@ -798,8 +787,10 @@ func (a *API) chainTxListByHeightAndPageHandler(_ *apirest.APIdata, ctx *httprou // Errors returned are always of type APIerror. func (a *API) sendTransactionList(ctx *httprouter.HTTPContext, params *TransactionParams) error { txs, total, err := a.indexer.SearchTransactions( - int32(params.Limit), - int32(params.Page*params.Limit), + params.Limit, + params.Page*params.Limit, + params.Height, + params.Type, ) if err != nil { return ErrIndexerQueryFailed.WithErr(err) @@ -1148,7 +1139,7 @@ func parseFeesParams(paramPage, paramLimit, paramReference, paramType, paramAcco } // parseTransactionParams returns an TransactionParams filled with the passed params -func parseTransactionParams(paramPage, paramLimit, paramHeight string) (*TransactionParams, error) { +func parseTransactionParams(paramPage, paramLimit, paramHeight, paramType string) (*TransactionParams, error) { pagination, err := parsePaginationParams(paramPage, paramLimit) if err != nil { return nil, err @@ -1162,5 +1153,6 @@ func parseTransactionParams(paramPage, paramLimit, paramHeight string) (*Transac return &TransactionParams{ PaginationParams: pagination, Height: uint64(height), + Type: paramType, }, nil } diff --git a/vochain/indexer/db/db.go b/vochain/indexer/db/db.go index 98ddaa9ad..f84c11994 100644 --- a/vochain/indexer/db/db.go +++ b/vochain/indexer/db/db.go @@ -66,9 +66,6 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) { if q.getEntityCountStmt, err = db.PrepareContext(ctx, getEntityCount); err != nil { return nil, fmt.Errorf("error preparing query GetEntityCount: %w", err) } - if q.getLastTransactionsStmt, err = db.PrepareContext(ctx, getLastTransactions); err != nil { - return nil, fmt.Errorf("error preparing query GetLastTransactions: %w", err) - } if q.getListAccountsStmt, err = db.PrepareContext(ctx, getListAccounts); err != nil { return nil, fmt.Errorf("error preparing query GetListAccounts: %w", err) } @@ -114,6 +111,9 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) { if q.searchTokenFeesStmt, err = db.PrepareContext(ctx, searchTokenFees); err != nil { return nil, fmt.Errorf("error preparing query SearchTokenFees: %w", err) } + if q.searchTransactionsStmt, err = db.PrepareContext(ctx, searchTransactions); err != nil { + return nil, fmt.Errorf("error preparing query SearchTransactions: %w", err) + } if q.searchVotesStmt, err = db.PrepareContext(ctx, searchVotes); err != nil { return nil, fmt.Errorf("error preparing query SearchVotes: %w", err) } @@ -210,11 +210,6 @@ func (q *Queries) Close() error { err = fmt.Errorf("error closing getEntityCountStmt: %w", cerr) } } - if q.getLastTransactionsStmt != nil { - if cerr := q.getLastTransactionsStmt.Close(); cerr != nil { - err = fmt.Errorf("error closing getLastTransactionsStmt: %w", cerr) - } - } if q.getListAccountsStmt != nil { if cerr := q.getListAccountsStmt.Close(); cerr != nil { err = fmt.Errorf("error closing getListAccountsStmt: %w", cerr) @@ -290,6 +285,11 @@ func (q *Queries) Close() error { err = fmt.Errorf("error closing searchTokenFeesStmt: %w", cerr) } } + if q.searchTransactionsStmt != nil { + if cerr := q.searchTransactionsStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing searchTransactionsStmt: %w", cerr) + } + } if q.searchVotesStmt != nil { if cerr := q.searchVotesStmt.Close(); cerr != nil { err = fmt.Errorf("error closing searchVotesStmt: %w", cerr) @@ -378,7 +378,6 @@ type Queries struct { createVoteStmt *sql.Stmt getBlockStmt *sql.Stmt getEntityCountStmt *sql.Stmt - getLastTransactionsStmt *sql.Stmt getListAccountsStmt *sql.Stmt getProcessStmt *sql.Stmt getProcessCountStmt *sql.Stmt @@ -394,6 +393,7 @@ type Queries struct { searchEntitiesStmt *sql.Stmt searchProcessesStmt *sql.Stmt searchTokenFeesStmt *sql.Stmt + searchTransactionsStmt *sql.Stmt searchVotesStmt *sql.Stmt setProcessResultsCancelledStmt *sql.Stmt setProcessResultsReadyStmt *sql.Stmt @@ -421,7 +421,6 @@ func (q *Queries) WithTx(tx *sql.Tx) *Queries { createVoteStmt: q.createVoteStmt, getBlockStmt: q.getBlockStmt, getEntityCountStmt: q.getEntityCountStmt, - getLastTransactionsStmt: q.getLastTransactionsStmt, getListAccountsStmt: q.getListAccountsStmt, getProcessStmt: q.getProcessStmt, getProcessCountStmt: q.getProcessCountStmt, @@ -437,6 +436,7 @@ func (q *Queries) WithTx(tx *sql.Tx) *Queries { searchEntitiesStmt: q.searchEntitiesStmt, searchProcessesStmt: q.searchProcessesStmt, searchTokenFeesStmt: q.searchTokenFeesStmt, + searchTransactionsStmt: q.searchTransactionsStmt, searchVotesStmt: q.searchVotesStmt, setProcessResultsCancelledStmt: q.setProcessResultsCancelledStmt, setProcessResultsReadyStmt: q.setProcessResultsReadyStmt, diff --git a/vochain/indexer/db/transactions.sql.go b/vochain/indexer/db/transactions.sql.go index c3d6e0b45..f329b4393 100644 --- a/vochain/indexer/db/transactions.sql.go +++ b/vochain/indexer/db/transactions.sql.go @@ -13,8 +13,6 @@ import ( ) const countTransactions = `-- name: CountTransactions :one -; - SELECT COUNT(*) FROM transactions ` @@ -49,47 +47,6 @@ func (q *Queries) CreateTransaction(ctx context.Context, arg CreateTransactionPa ) } -const getLastTransactions = `-- name: GetLastTransactions :many -SELECT id, hash, block_height, block_index, type FROM transactions -ORDER BY id DESC -LIMIT ? -OFFSET ? -` - -type GetLastTransactionsParams struct { - Limit int64 - Offset int64 -} - -func (q *Queries) GetLastTransactions(ctx context.Context, arg GetLastTransactionsParams) ([]Transaction, error) { - rows, err := q.query(ctx, q.getLastTransactionsStmt, getLastTransactions, arg.Limit, arg.Offset) - if err != nil { - return nil, err - } - defer rows.Close() - var items []Transaction - for rows.Next() { - var i Transaction - if err := rows.Scan( - &i.ID, - &i.Hash, - &i.BlockHeight, - &i.BlockIndex, - &i.Type, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Close(); err != nil { - return nil, err - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - const getTransaction = `-- name: GetTransaction :one SELECT id, hash, block_height, block_index, type FROM transactions WHERE id = ? @@ -151,3 +108,70 @@ func (q *Queries) GetTxReferenceByBlockHeightAndBlockIndex(ctx context.Context, ) return i, err } + +const searchTransactions = `-- name: SearchTransactions :many +WITH results AS ( + SELECT id, hash, block_height, block_index, type + FROM transactions + WHERE ( + (?3 = 0 OR block_height = ?3) + AND (?4 = '' OR LOWER(tx_type) = LOWER(?4)) + ) +) +SELECT id, hash, block_height, block_index, type, COUNT(*) OVER() AS total_count +FROM results +ORDER BY id DESC +LIMIT ?2 +OFFSET ?1 +` + +type SearchTransactionsParams struct { + Offset int64 + Limit int64 + Height interface{} + TxType interface{} +} + +type SearchTransactionsRow struct { + ID int64 + Hash []byte + BlockHeight int64 + BlockIndex int64 + Type string + TotalCount int64 +} + +func (q *Queries) SearchTransactions(ctx context.Context, arg SearchTransactionsParams) ([]SearchTransactionsRow, error) { + rows, err := q.query(ctx, q.searchTransactionsStmt, searchTransactions, + arg.Offset, + arg.Limit, + arg.Height, + arg.TxType, + ) + if err != nil { + return nil, err + } + defer rows.Close() + var items []SearchTransactionsRow + for rows.Next() { + var i SearchTransactionsRow + if err := rows.Scan( + &i.ID, + &i.Hash, + &i.BlockHeight, + &i.BlockIndex, + &i.Type, + &i.TotalCount, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/vochain/indexer/indexer_test.go b/vochain/indexer/indexer_test.go index dd5cbc08b..e3f74f502 100644 --- a/vochain/indexer/indexer_test.go +++ b/vochain/indexer/indexer_test.go @@ -1419,7 +1419,7 @@ func TestTxIndexer(t *testing.T) { } } - txs, err := idx.GetLastTransactions(15, 0) + txs, _, err := idx.SearchTransactions(15, 0, 0, "") qt.Assert(t, err, qt.IsNil) for i, tx := range txs { // Index is between 1 and totalCount. @@ -1430,7 +1430,7 @@ func TestTxIndexer(t *testing.T) { qt.Assert(t, tx.TxType, qt.Equals, "setAccount") } - txs, err = idx.GetLastTransactions(1, 5) + txs, _, err = idx.SearchTransactions(1, 5, 0, "") qt.Assert(t, err, qt.IsNil) qt.Assert(t, txs, qt.HasLen, 1) qt.Assert(t, txs[0].Index, qt.Equals, uint64(95)) diff --git a/vochain/indexer/queries/transactions.sql b/vochain/indexer/queries/transactions.sql index 5b5ec4698..302dd57e3 100644 --- a/vochain/indexer/queries/transactions.sql +++ b/vochain/indexer/queries/transactions.sql @@ -15,13 +15,6 @@ SELECT * FROM transactions WHERE hash = ? LIMIT 1; --- name: GetLastTransactions :many -SELECT * FROM transactions -ORDER BY id DESC -LIMIT ? -OFFSET ? -; - -- name: CountTransactions :one SELECT COUNT(*) FROM transactions; @@ -30,3 +23,17 @@ SELECT * FROM transactions WHERE block_height = ? AND block_index = ? LIMIT 1; +-- name: SearchTransactions :many +WITH results AS ( + SELECT * + FROM transactions + WHERE ( + (sqlc.arg(height) = 0 OR block_height = sqlc.arg(height)) + AND (sqlc.arg(tx_type) = '' OR LOWER(tx_type) = LOWER(sqlc.arg(tx_type))) + ) +) +SELECT *, COUNT(*) OVER() AS total_count +FROM results +ORDER BY id DESC +LIMIT sqlc.arg(limit) +OFFSET sqlc.arg(offset); diff --git a/vochain/indexer/transaction.go b/vochain/indexer/transaction.go index 7abd56cf3..f1f9fbde7 100644 --- a/vochain/indexer/transaction.go +++ b/vochain/indexer/transaction.go @@ -61,30 +61,39 @@ func (idx *Indexer) GetTxHashReference(hash types.HexBytes) (*indexertypes.Trans return indexertypes.TransactionFromDB(&sqlTxRef), nil } -// SearchTransactions TBD, doesn't return the real totalItems -func (idx *Indexer) SearchTransactions(limit, offset int32) ([]*indexertypes.Transaction, uint64, error) { - list, err := idx.GetLastTransactions(limit, offset) - return list, uint64(len(list)), err -} - -// GetLastTransactions fetches a number of the latest indexed transactions. +// SearchTransactions returns the list of transactions indexed. +// height and txType are optional, if declared as zero-value will be ignored. // The first one returned is the newest, so they are in descending order. -func (idx *Indexer) GetLastTransactions(limit, offset int32) ([]*indexertypes.Transaction, error) { - sqlTxRefs, err := idx.readOnlyQuery.GetLastTransactions(context.TODO(), indexerdb.GetLastTransactionsParams{ +func (idx *Indexer) SearchTransactions(limit, offset int, height uint64, txType string) ([]*indexertypes.Transaction, uint64, error) { + if offset < 0 { + return nil, 0, fmt.Errorf("invalid value: offset cannot be %d", offset) + } + if limit <= 0 { + return nil, 0, fmt.Errorf("invalid value: limit cannot be %d", limit) + } + results, err := idx.readOnlyQuery.SearchTransactions(context.TODO(), indexerdb.SearchTransactionsParams{ Limit: int64(limit), Offset: int64(offset), + Height: height, + TxType: txType, }) - if err != nil || len(sqlTxRefs) == 0 { - if errors.Is(err, sql.ErrNoRows) || len(sqlTxRefs) == 0 { - return nil, ErrTransactionNotFound - } - return nil, fmt.Errorf("could not get last %d tx refs: %v", limit, err) + if err != nil { + return nil, 0, err + } + list := []*indexertypes.Transaction{} + for _, row := range results { + list = append(list, &indexertypes.Transaction{ + Index: uint64(row.ID), + Hash: row.Hash, + BlockHeight: uint32(row.BlockHeight), + TxBlockIndex: int32(row.BlockIndex), + TxType: row.Type, + }) } - txRefs := make([]*indexertypes.Transaction, len(sqlTxRefs)) - for i, sqlTxRef := range sqlTxRefs { - txRefs[i] = indexertypes.TransactionFromDB(&sqlTxRef) + if len(results) == 0 { + return list, 0, nil } - return txRefs, nil + return list, uint64(results[0].TotalCount), nil } func (idx *Indexer) OnNewTx(tx *vochaintx.Tx, blockHeight uint32, txIndex int32) {