Skip to content

Commit 488e3a4

Browse files
authored
feat: get blocks API handle (#140)
### TL;DR Added a new `/blocks` endpoint to retrieve block data with filtering, sorting, and aggregation capabilities. ### What changed? - Created a new `/blocks` endpoint in the API - Implemented block data retrieval with support for: - Filtering by chain ID and other block parameters - Sorting and pagination - Aggregation functions - Group by operations - Updated the storage interface to return QueryResult for block operations - Added block scanning functionality to handle database rows ### How to test? 1. Start the API server 2. Make GET requests to `/blocks` with optional query parameters: - `chainId`: Specify the blockchain network - `filter`: Apply filters to block data - `sort_by`: Sort results by specific fields - `sort_order`: Choose ascending or descending order - `page` and `limit`: Control pagination - `aggregate`: Apply aggregation functions - `group_by`: Group results by specific fields ### Why make this change? To provide a standardized way to query and analyze block data from the blockchain, enabling users to retrieve block information with flexible filtering and aggregation options. This enhancement aligns with the existing transaction and event query capabilities of the API.
2 parents 4bc1da4 + 7a4e899 commit 488e3a4

File tree

6 files changed

+180
-70
lines changed

6 files changed

+180
-70
lines changed

cmd/api.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ func RunApi(cmd *cobra.Command, args []string) {
7373
// signature scoped queries
7474
root.GET("/transactions/:to/:signature", handlers.GetTransactionsByContractAndSignature)
7575
root.GET("/events/:contract/:signature", handlers.GetLogsByContractAndSignature)
76+
77+
// blocks table queries
78+
root.GET("/blocks", handlers.GetBlocks)
7679
}
7780

7881
r.GET("/health", func(c *gin.Context) {
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
package handlers
2+
3+
import (
4+
"github.com/gin-gonic/gin"
5+
"github.com/rs/zerolog/log"
6+
"github.com/thirdweb-dev/indexer/api"
7+
"github.com/thirdweb-dev/indexer/internal/storage"
8+
)
9+
10+
// BlockModel represents a simplified Block structure for Swagger documentation
11+
type BlockModel struct {
12+
ChainId string `json:"chain_id"`
13+
Number string `json:"number"`
14+
Hash string `json:"hash"`
15+
ParentHash string `json:"parent_hash"`
16+
Timestamp uint64 `json:"timestamp"`
17+
Nonce string `json:"nonce"`
18+
Sha3Uncles string `json:"sha3_uncles"`
19+
LogsBloom string `json:"logs_bloom"`
20+
ReceiptsRoot string `json:"receipts_root"`
21+
Difficulty string `json:"difficulty"`
22+
TotalDifficulty string `json:"total_difficulty"`
23+
Size uint64 `json:"size"`
24+
ExtraData string `json:"extra_data"`
25+
GasLimit uint64 `json:"gas_limit"`
26+
GasUsed uint64 `json:"gas_used"`
27+
BaseFeePerGas string `json:"base_fee_per_gas"`
28+
WithdrawalsRoot string `json:"withdrawals_root"`
29+
}
30+
31+
// @Summary Get all blocks
32+
// @Description Retrieve all blocks
33+
// @Tags blocks
34+
// @Accept json
35+
// @Produce json
36+
// @Security BasicAuth
37+
// @Param chainId path string true "Chain ID"
38+
// @Param filter query string false "Filter parameters"
39+
// @Param group_by query string false "Field to group results by"
40+
// @Param sort_by query string false "Field to sort results by"
41+
// @Param sort_order query string false "Sort order (asc or desc)"
42+
// @Param page query int false "Page number for pagination"
43+
// @Param limit query int false "Number of items per page" default(5)
44+
// @Param aggregate query []string false "List of aggregate functions to apply"
45+
// @Success 200 {object} api.QueryResponse{data=[]BlockModel}
46+
// @Failure 400 {object} api.Error
47+
// @Failure 401 {object} api.Error
48+
// @Failure 500 {object} api.Error
49+
// @Router /{chainId}/blocks [get]
50+
func GetBlocks(c *gin.Context) {
51+
handleBlocksRequest(c)
52+
}
53+
54+
func handleBlocksRequest(c *gin.Context) {
55+
chainId, err := api.GetChainId(c)
56+
if err != nil {
57+
api.BadRequestErrorHandler(c, err)
58+
return
59+
}
60+
61+
queryParams, err := api.ParseQueryParams(c.Request)
62+
if err != nil {
63+
api.BadRequestErrorHandler(c, err)
64+
return
65+
}
66+
67+
mainStorage, err := getMainStorage()
68+
if err != nil {
69+
log.Error().Err(err).Msg("Error getting main storage")
70+
api.InternalErrorHandler(c)
71+
return
72+
}
73+
74+
// Prepare the QueryFilter
75+
qf := storage.QueryFilter{
76+
FilterParams: queryParams.FilterParams,
77+
ChainId: chainId,
78+
SortBy: queryParams.SortBy,
79+
SortOrder: queryParams.SortOrder,
80+
Page: queryParams.Page,
81+
Limit: queryParams.Limit,
82+
}
83+
84+
// Initialize the QueryResult
85+
queryResult := api.QueryResponse{
86+
Meta: api.Meta{
87+
ChainId: chainId.Uint64(),
88+
Page: queryParams.Page,
89+
Limit: queryParams.Limit,
90+
TotalItems: 0,
91+
TotalPages: 0, // TODO: Implement total pages count
92+
},
93+
Data: nil,
94+
Aggregations: nil,
95+
}
96+
97+
// If aggregates or groupings are specified, retrieve them
98+
if len(queryParams.Aggregates) > 0 || len(queryParams.GroupBy) > 0 {
99+
qf.Aggregates = queryParams.Aggregates
100+
qf.GroupBy = queryParams.GroupBy
101+
102+
aggregatesResult, err := mainStorage.GetAggregations("blocks", qf)
103+
if err != nil {
104+
log.Error().Err(err).Msg("Error querying aggregates")
105+
// TODO: might want to choose BadRequestError if it's due to not-allowed functions
106+
api.InternalErrorHandler(c)
107+
return
108+
}
109+
queryResult.Aggregations = aggregatesResult.Aggregates
110+
queryResult.Meta.TotalItems = len(aggregatesResult.Aggregates)
111+
} else {
112+
// Retrieve blocks data
113+
blocksResult, err := mainStorage.GetBlocks(qf)
114+
if err != nil {
115+
log.Error().Err(err).Msg("Error querying blocks")
116+
// TODO: might want to choose BadRequestError if it's due to not-allowed functions
117+
api.InternalErrorHandler(c)
118+
return
119+
}
120+
121+
queryResult.Data = blocksResult.Data
122+
queryResult.Meta.TotalItems = len(blocksResult.Data)
123+
}
124+
125+
sendJSONResponse(c, queryResult)
126+
}

internal/storage/clickhouse.go

Lines changed: 31 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -291,55 +291,9 @@ func (c *ClickHouseConnector) StoreBlockFailures(failures []common.BlockFailure)
291291
return batch.Send()
292292
}
293293

294-
func (c *ClickHouseConnector) GetBlocks(qf QueryFilter) (blocks []common.Block, err error) {
294+
func (c *ClickHouseConnector) GetBlocks(qf QueryFilter) (QueryResult[common.Block], error) {
295295
columns := "chain_id, number, hash, parent_hash, timestamp, nonce, sha3_uncles, logs_bloom, receipts_root, difficulty, total_difficulty, size, extra_data, gas_limit, gas_used, transaction_count, base_fee_per_gas, withdrawals_root"
296-
query := fmt.Sprintf("SELECT %s FROM %s.blocks WHERE number IN (%s) AND is_deleted = 0",
297-
columns, c.cfg.Database, getBlockNumbersStringArray(qf.BlockNumbers))
298-
299-
if qf.ChainId.Sign() > 0 {
300-
query += fmt.Sprintf(" AND chain_id = %s", qf.ChainId.String())
301-
}
302-
303-
query += getLimitClause(int(qf.Limit))
304-
305-
if err := common.ValidateQuery(query); err != nil {
306-
return nil, err
307-
}
308-
rows, err := c.conn.Query(context.Background(), query)
309-
if err != nil {
310-
return nil, err
311-
}
312-
defer rows.Close()
313-
314-
for rows.Next() {
315-
var block common.Block
316-
err := rows.Scan(
317-
&block.ChainId,
318-
&block.Number,
319-
&block.Hash,
320-
&block.ParentHash,
321-
&block.Timestamp,
322-
&block.Nonce,
323-
&block.Sha3Uncles,
324-
&block.LogsBloom,
325-
&block.ReceiptsRoot,
326-
&block.Difficulty,
327-
&block.TotalDifficulty,
328-
&block.Size,
329-
&block.ExtraData,
330-
&block.GasLimit,
331-
&block.GasUsed,
332-
&block.TransactionCount,
333-
&block.BaseFeePerGas,
334-
&block.WithdrawalsRoot,
335-
)
336-
if err != nil {
337-
zLog.Error().Err(err).Msg("Error scanning block")
338-
return nil, err
339-
}
340-
blocks = append(blocks, block)
341-
}
342-
return blocks, nil
296+
return executeQuery[common.Block](c, "blocks", columns, qf, scanBlock)
343297
}
344298

345299
func (c *ClickHouseConnector) GetTransactions(qf QueryFilter) (QueryResult[common.Transaction], error) {
@@ -613,6 +567,35 @@ func scanLog(rows driver.Rows) (common.Log, error) {
613567
return log, nil
614568
}
615569

570+
func scanBlock(rows driver.Rows) (common.Block, error) {
571+
var block common.Block
572+
err := rows.Scan(
573+
&block.ChainId,
574+
&block.Number,
575+
&block.Hash,
576+
&block.ParentHash,
577+
&block.Timestamp,
578+
&block.Nonce,
579+
&block.Sha3Uncles,
580+
&block.LogsBloom,
581+
&block.ReceiptsRoot,
582+
&block.Difficulty,
583+
&block.TotalDifficulty,
584+
&block.Size,
585+
&block.ExtraData,
586+
&block.GasLimit,
587+
&block.GasUsed,
588+
&block.TransactionCount,
589+
&block.BaseFeePerGas,
590+
&block.WithdrawalsRoot,
591+
)
592+
if err != nil {
593+
return common.Block{}, fmt.Errorf("error scanning block: %w", err)
594+
}
595+
596+
return block, nil
597+
}
598+
616599
func (c *ClickHouseConnector) GetMaxBlockNumber(chainId *big.Int) (maxBlockNumber *big.Int, err error) {
617600
query := fmt.Sprintf("SELECT number FROM %s.blocks WHERE is_deleted = 0", c.cfg.Database)
618601
if chainId.Sign() > 0 {

internal/storage/connector.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ type IStagingStorage interface {
5353
type IMainStorage interface {
5454
InsertBlockData(data *[]common.BlockData) error
5555

56-
GetBlocks(qf QueryFilter) (blocks []common.Block, err error)
56+
GetBlocks(qf QueryFilter) (blocks QueryResult[common.Block], err error)
5757
GetTransactions(qf QueryFilter) (transactions QueryResult[common.Transaction], err error)
5858
GetLogs(qf QueryFilter) (logs QueryResult[common.Log], err error)
5959
GetAggregations(table string, qf QueryFilter) (QueryResult[interface{}], error)

internal/storage/memory.go

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ func (m *MemoryConnector) insertBlocks(blocks *[]common.Block) error {
8585
return nil
8686
}
8787

88-
func (m *MemoryConnector) GetBlocks(qf QueryFilter) ([]common.Block, error) {
88+
func (m *MemoryConnector) GetBlocks(qf QueryFilter) (QueryResult[common.Block], error) {
8989
blocks := []common.Block{}
9090
limit := getLimit(qf)
9191
blockNumbersToCheck := getBlockNumbersToCheck(qf)
@@ -100,13 +100,13 @@ func (m *MemoryConnector) GetBlocks(qf QueryFilter) ([]common.Block, error) {
100100
block := common.Block{}
101101
err := json.Unmarshal([]byte(value), &block)
102102
if err != nil {
103-
return nil, err
103+
return QueryResult[common.Block]{}, err
104104
}
105105
blocks = append(blocks, block)
106106
}
107107
}
108108
}
109-
return blocks, nil
109+
return QueryResult[common.Block]{Data: blocks}, nil
110110
}
111111

112112
func (m *MemoryConnector) insertTransactions(txs *[]common.Transaction) error {
@@ -120,7 +120,7 @@ func (m *MemoryConnector) insertTransactions(txs *[]common.Transaction) error {
120120
return nil
121121
}
122122

123-
func (m *MemoryConnector) GetTransactions(qf QueryFilter) ([]common.Transaction, error) {
123+
func (m *MemoryConnector) GetTransactions(qf QueryFilter) (QueryResult[common.Transaction], error) {
124124
txs := []common.Transaction{}
125125
limit := getLimit(qf)
126126
blockNumbersToCheck := getBlockNumbersToCheck(qf)
@@ -134,13 +134,13 @@ func (m *MemoryConnector) GetTransactions(qf QueryFilter) ([]common.Transaction,
134134
tx := common.Transaction{}
135135
err := json.Unmarshal([]byte(value), &tx)
136136
if err != nil {
137-
return nil, err
137+
return QueryResult[common.Transaction]{}, err
138138
}
139139
txs = append(txs, tx)
140140
}
141141
}
142142
}
143-
return txs, nil
143+
return QueryResult[common.Transaction]{Data: txs}, nil
144144
}
145145

146146
func (m *MemoryConnector) insertLogs(logs *[]common.Log) error {
@@ -154,7 +154,7 @@ func (m *MemoryConnector) insertLogs(logs *[]common.Log) error {
154154
return nil
155155
}
156156

157-
func (m *MemoryConnector) GetLogs(qf QueryFilter) ([]common.Log, error) {
157+
func (m *MemoryConnector) GetLogs(qf QueryFilter) (QueryResult[common.Log], error) {
158158
logs := []common.Log{}
159159
limit := getLimit(qf)
160160
blockNumbersToCheck := getBlockNumbersToCheck(qf)
@@ -168,13 +168,13 @@ func (m *MemoryConnector) GetLogs(qf QueryFilter) ([]common.Log, error) {
168168
log := common.Log{}
169169
err := json.Unmarshal([]byte(value), &log)
170170
if err != nil {
171-
return nil, err
171+
return QueryResult[common.Log]{}, err
172172
}
173173
logs = append(logs, log)
174174
}
175175
}
176176
}
177-
return logs, nil
177+
return QueryResult[common.Log]{Data: logs}, nil
178178
}
179179

180180
func (m *MemoryConnector) GetMaxBlockNumber(chainId *big.Int) (*big.Int, error) {
@@ -314,7 +314,7 @@ func (m *MemoryConnector) insertTraces(traces *[]common.Trace) error {
314314
return nil
315315
}
316316

317-
func (m *MemoryConnector) GetTraces(qf QueryFilter) ([]common.Trace, error) {
317+
func (m *MemoryConnector) GetTraces(qf QueryFilter) (QueryResult[common.Trace], error) {
318318
traces := []common.Trace{}
319319
limit := getLimit(qf)
320320
blockNumbersToCheck := getBlockNumbersToCheck(qf)
@@ -328,13 +328,13 @@ func (m *MemoryConnector) GetTraces(qf QueryFilter) ([]common.Trace, error) {
328328
trace := common.Trace{}
329329
err := json.Unmarshal([]byte(value), &trace)
330330
if err != nil {
331-
return nil, err
331+
return QueryResult[common.Trace]{}, err
332332
}
333333
traces = append(traces, trace)
334334
}
335335
}
336336
}
337-
return traces, nil
337+
return QueryResult[common.Trace]{Data: traces}, nil
338338
}
339339

340340
func traceAddressToString(traceAddress []uint64) string {

test/mocks/MockIMainStorage.go

Lines changed: 7 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)