Skip to content

Commit 10a83b8

Browse files
authored
Implements API schema spec (#39)
### TL;DR Implemented new API endpoints for transactions and events, enhanced query parameter handling, and improved error responses. ### What changed? - Added new API endpoints for transactions and events, including chain-specific, contract-specific, and function signature-specific routes. - Implemented `ParseQueryParams` function to handle complex query parameters, including filtering, grouping, sorting, and pagination. - Enhanced error handling with more specific error types (BadRequest, Unauthorized, Internal). - Updated the `QueryResponse` and `Meta` structs to include more detailed information about the query results. - Added a health check endpoint. - Removed the `GetBlocks` handler and replaced it with more specific transaction and event handlers. - Updated dependencies, including upgrading chi and gorilla/schema. ### How to test? 1. Run the API server locally. 2. Test the new endpoints: - GET /api/transactions/{chainId} - GET /api/events/{chainId} - GET /api/transactions/{chainId}/{contractAddress} - GET /api/events/{chainId}/{contractAddress} - GET /api/transactions/{chainId}/{contractAddress}/{functionSig} - GET /api/events/{chainId}/{contractAddress}/{functionSig} 3. Test the health check endpoint: GET /health 4. Verify that query parameters are correctly parsed and included in the response. 5. Test error scenarios, including unauthorized access and invalid chain IDs. ### Why make this change? This change aims to provide a more flexible and powerful API for querying blockchain data. The new endpoints allow for more granular queries, while the enhanced query parameter handling supports advanced filtering, sorting, and aggregation. The improved error handling and response structure will make it easier for clients to consume and understand the API responses.
1 parent a379262 commit 10a83b8

File tree

10 files changed

+340
-77
lines changed

10 files changed

+340
-77
lines changed

api/api.go

+72-10
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,52 @@ package api
33
import (
44
"encoding/json"
55
"net/http"
6+
"reflect"
7+
"strconv"
8+
"strings"
9+
10+
"github.com/go-chi/chi/v5"
11+
"github.com/gorilla/schema"
12+
"github.com/rs/zerolog/log"
613
)
714

815
type Error struct {
9-
Code int
10-
Message string
16+
Code int `json:"code"`
17+
Message string `json:"message"`
18+
SupportId string `json:"support_id"`
1119
}
1220

1321
type QueryParams struct {
14-
ChainID string
22+
FilterParams map[string]string `schema:"-"`
23+
GroupBy string `schema:"group_by"`
24+
SortBy string `schema:"sort_by"`
25+
SortOrder string `schema:"sort_order"`
26+
Page int `schema:"page"`
27+
Limit int `schema:"limit"`
28+
Aggregate []string `schema:"aggregate"`
29+
}
30+
31+
type Meta struct {
32+
ChainIdentifier string `json:"chain_identifier"`
33+
ContractAddress string `json:"contract_address"`
34+
FunctionSig string `json:"function_sig"`
35+
Page int `json:"page"`
36+
Limit int `json:"limit"`
37+
TotalItems int `json:"total_items"`
38+
TotalPages int `json:"total_pages"`
1539
}
1640

1741
type QueryResponse struct {
18-
Code int
19-
Result string
42+
Meta Meta `json:"meta"`
43+
Data []interface{} `json:"data"`
44+
Aggregations map[string]interface{} `json:"aggregations,omitempty"`
2045
}
2146

2247
func writeError(w http.ResponseWriter, message string, code int) {
2348
resp := Error{
24-
Code: code,
25-
Message: message,
49+
Code: code,
50+
Message: message,
51+
SupportId: "TODO",
2652
}
2753

2854
w.Header().Set("Content-Type", "application/json")
@@ -32,10 +58,46 @@ func writeError(w http.ResponseWriter, message string, code int) {
3258
}
3359

3460
var (
35-
RequestErrorHandler = func(w http.ResponseWriter, err error) {
61+
BadRequestErrorHandler = func(w http.ResponseWriter, err error) {
3662
writeError(w, err.Error(), http.StatusBadRequest)
3763
}
3864
InternalErrorHandler = func(w http.ResponseWriter) {
39-
writeError(w, "An Unexpected Error Occurred.", http.StatusInternalServerError)
65+
writeError(w, "An unexpected error occurred.", http.StatusInternalServerError)
66+
}
67+
UnauthorizedErrorHandler = func(w http.ResponseWriter, err error) {
68+
writeError(w, err.Error(), http.StatusUnauthorized)
69+
}
70+
)
71+
72+
func ParseQueryParams(r *http.Request) (QueryParams, error) {
73+
var params QueryParams
74+
rawQueryParams := r.URL.Query()
75+
params.FilterParams = make(map[string]string)
76+
for key, values := range rawQueryParams {
77+
if strings.HasPrefix(key, "filter_") {
78+
params.FilterParams[key] = values[0]
79+
delete(rawQueryParams, key)
80+
}
4081
}
41-
)
82+
83+
decoder := schema.NewDecoder()
84+
decoder.RegisterConverter(map[string]string{}, func(value string) reflect.Value {
85+
return reflect.ValueOf(map[string]string{})
86+
})
87+
err := decoder.Decode(&params, rawQueryParams)
88+
if err != nil {
89+
log.Error().Err(err).Msg("Error parsing query params")
90+
return QueryParams{}, err
91+
}
92+
return params, nil
93+
}
94+
95+
func GetChainId(r *http.Request) (string, error) {
96+
// TODO: check chainId agains the chain-service to ensure it's valid
97+
chainId := chi.URLParam(r, "chainId")
98+
if _, err := strconv.Atoi(chainId); err != nil {
99+
log.Error().Err(err).Msg("Error getting chainId")
100+
return "", err
101+
}
102+
return chainId, nil
103+
}

cmd/api/__debug_bin3998373950

8.79 MB
Binary file not shown.

cmd/api/main.go

+2
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ import (
55

66
"github.com/go-chi/chi/v5"
77
"github.com/rs/zerolog/log"
8+
"github.com/thirdweb-dev/indexer/internal/env"
89
"github.com/thirdweb-dev/indexer/internal/handlers"
910
customLogger "github.com/thirdweb-dev/indexer/internal/log"
1011
)
1112

1213
func main() {
14+
env.Load()
1315
customLogger.InitLogger()
1416

1517
var r *chi.Mux = chi.NewRouter()

go.mod

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ go 1.22.0
55
require (
66
github.com/ClickHouse/clickhouse-go/v2 v2.28.3
77
github.com/ethereum/go-ethereum v1.14.8
8-
github.com/go-chi/chi v1.5.4
8+
github.com/go-chi/chi v1.5.5
99
github.com/go-chi/chi/v5 v5.1.0
1010
github.com/google/uuid v1.6.0
11-
github.com/gorilla/schema v1.2.0
11+
github.com/gorilla/schema v1.4.1
1212
github.com/hashicorp/golang-lru/v2 v2.0.7
1313
github.com/joho/godotenv v1.5.1
1414
github.com/rs/zerolog v1.33.0

go.sum

+4-4
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,8 @@ github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff h1:tY80oXqG
6464
github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww=
6565
github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps=
6666
github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY=
67-
github.com/go-chi/chi v1.5.4 h1:QHdzF2szwjqVV4wmByUnTcsbIg7UGaQ0tPF2t5GcAIs=
68-
github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg=
67+
github.com/go-chi/chi v1.5.5 h1:vOB/HbEMt9QqBqErz07QehcOKHaWFtuj87tTDVz2qXE=
68+
github.com/go-chi/chi v1.5.5/go.mod h1:C9JqLr3tIYjDOZpzn+BCuxY8z8vmca43EeMgyZt7irw=
6969
github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
7070
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
7171
github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw=
@@ -95,8 +95,8 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN
9595
github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
9696
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
9797
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
98-
github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc=
99-
github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU=
98+
github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E=
99+
github.com/gorilla/schema v1.4.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM=
100100
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
101101
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
102102
github.com/hashicorp/go-bexpr v0.1.10 h1:9kuI5PFotCboP3dkDYFr/wi0gg0QVbSNz5oFRpxn4uE=

internal/handlers/api.go

+21-5
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,34 @@
11
package handlers
22

33
import (
4+
"net/http"
5+
46
chimiddle "github.com/go-chi/chi/middleware"
57
"github.com/go-chi/chi/v5"
68
"github.com/thirdweb-dev/indexer/internal/middleware"
79
)
810

911
func Handler(r *chi.Mux) {
1012
r.Use(chimiddle.StripSlashes)
13+
r.Use(middleware.Authorization)
14+
r.Route("/", func(router chi.Router) {
15+
// might consolidate all variants to one handler function
16+
// Wild card queries
17+
router.Get("/{chainId}/transactions", GetTransactions)
18+
router.Get("/{chainId}/events", GetLogs)
19+
20+
// contract scoped queries
21+
router.Get("/{chainId}/transactions/{contractAddress}", GetTransactionsWithContract)
22+
router.Get("/{chainId}/events/{contractAddress}", GetEventsWithContract)
23+
24+
// signature scoped queries
25+
router.Get("/{chainId}/transactions/{contractAddress}/{functionSig}", GetTransactionsWithContractAndSignature)
26+
router.Get("/{chainId}/events/{contractAddress}/{functionSig}", GetEventsWithContractAndSignature)
27+
})
1128

12-
r.Route("/api", func(router chi.Router) {
13-
router.Use(middleware.Authorization)
14-
router.Route("/v1", func(r chi.Router) {
15-
r.Get("/blocks", GetBlocks)
16-
})
29+
r.Get("/health", func(w http.ResponseWriter, r *http.Request) {
30+
// TODO: implement a simple query before going live
31+
w.WriteHeader(http.StatusOK)
32+
w.Write([]byte("ok"))
1733
})
1834
}

internal/handlers/get_blocks.go

-55
This file was deleted.

internal/handlers/logs_handlers.go

+121
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
package handlers
2+
3+
import (
4+
"encoding/json"
5+
"net/http"
6+
7+
"github.com/go-chi/chi/v5"
8+
"github.com/rs/zerolog/log"
9+
"github.com/thirdweb-dev/indexer/api"
10+
)
11+
12+
func GetLogs(w http.ResponseWriter, r *http.Request) {
13+
chainId, err := api.GetChainId(r)
14+
if err != nil {
15+
api.BadRequestErrorHandler(w, err)
16+
return
17+
}
18+
queryParams, err := api.ParseQueryParams(r)
19+
if err != nil {
20+
api.BadRequestErrorHandler(w, err)
21+
return
22+
}
23+
24+
var response = api.QueryResponse{
25+
Meta: api.Meta{
26+
ChainIdentifier: chainId,
27+
ContractAddress: "todo",
28+
FunctionSig: "todo",
29+
Page: 1,
30+
Limit: 100,
31+
TotalItems: 0,
32+
TotalPages: 0,
33+
},
34+
Data: []interface{}{queryParams},
35+
}
36+
37+
w.Header().Set("Content-Type", "application/json")
38+
err = json.NewEncoder(w).Encode(response)
39+
if err != nil {
40+
log.Error().Err(err).Msg("Error encoding response")
41+
api.InternalErrorHandler(w)
42+
return
43+
}
44+
}
45+
46+
func GetEventsWithContract(w http.ResponseWriter, r *http.Request) {
47+
chainId, err := api.GetChainId(r)
48+
if err != nil {
49+
api.BadRequestErrorHandler(w, err)
50+
return
51+
}
52+
contractAddress := chi.URLParam(r, "contractAddress")
53+
queryParams, err := api.ParseQueryParams(r)
54+
if err != nil {
55+
api.BadRequestErrorHandler(w, err)
56+
return
57+
}
58+
59+
var response = api.QueryResponse{
60+
Meta: api.Meta{
61+
ChainIdentifier: chainId,
62+
ContractAddress: contractAddress,
63+
FunctionSig: "todo",
64+
Page: 1,
65+
Limit: 100,
66+
TotalItems: 0,
67+
TotalPages: 0,
68+
},
69+
Data: []interface{}{queryParams},
70+
}
71+
72+
w.Header().Set("Content-Type", "application/json")
73+
err = json.NewEncoder(w).Encode(response)
74+
if err != nil {
75+
log.Error().Err(err).Msg("Error encoding response")
76+
api.InternalErrorHandler(w)
77+
return
78+
}
79+
80+
}
81+
82+
func GetEventsWithContractAndSignature(w http.ResponseWriter, r *http.Request) {
83+
chainId, err := api.GetChainId(r)
84+
if err != nil {
85+
api.BadRequestErrorHandler(w, err)
86+
return
87+
}
88+
contractAddress := chi.URLParam(r, "contractAddress")
89+
functionSig := chi.URLParam(r, "functionSig")
90+
queryParams, err := api.ParseQueryParams(r)
91+
if err != nil {
92+
api.BadRequestErrorHandler(w, err)
93+
return
94+
}
95+
96+
var response = api.QueryResponse{
97+
Meta: api.Meta{
98+
ChainIdentifier: chainId,
99+
ContractAddress: contractAddress,
100+
FunctionSig: functionSig,
101+
Page: 1,
102+
Limit: 100,
103+
TotalItems: 0,
104+
TotalPages: 0,
105+
},
106+
Data: []interface{}{queryParams},
107+
Aggregations: map[string]interface{}{
108+
"count": 100,
109+
"sum_value": "1000000000000000000000",
110+
"avg_gas_price": "20000000000",
111+
},
112+
}
113+
114+
w.Header().Set("Content-Type", "application/json")
115+
err = json.NewEncoder(w).Encode(response)
116+
if err != nil {
117+
log.Error().Err(err).Msg("Error encoding response")
118+
api.InternalErrorHandler(w)
119+
return
120+
}
121+
}

0 commit comments

Comments
 (0)