Skip to content

Commit

Permalink
Implements API schema spec (#39)
Browse files Browse the repository at this point in the history
### 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.
  • Loading branch information
AmineAfia authored Sep 24, 2024
1 parent a379262 commit 10a83b8
Show file tree
Hide file tree
Showing 10 changed files with 340 additions and 77 deletions.
82 changes: 72 additions & 10 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,52 @@ package api
import (
"encoding/json"
"net/http"
"reflect"
"strconv"
"strings"

"github.com/go-chi/chi/v5"
"github.com/gorilla/schema"
"github.com/rs/zerolog/log"
)

type Error struct {
Code int
Message string
Code int `json:"code"`
Message string `json:"message"`
SupportId string `json:"support_id"`
}

type QueryParams struct {
ChainID string
FilterParams map[string]string `schema:"-"`
GroupBy string `schema:"group_by"`
SortBy string `schema:"sort_by"`
SortOrder string `schema:"sort_order"`
Page int `schema:"page"`
Limit int `schema:"limit"`
Aggregate []string `schema:"aggregate"`
}

type Meta struct {
ChainIdentifier string `json:"chain_identifier"`
ContractAddress string `json:"contract_address"`
FunctionSig string `json:"function_sig"`
Page int `json:"page"`
Limit int `json:"limit"`
TotalItems int `json:"total_items"`
TotalPages int `json:"total_pages"`
}

type QueryResponse struct {
Code int
Result string
Meta Meta `json:"meta"`
Data []interface{} `json:"data"`
Aggregations map[string]interface{} `json:"aggregations,omitempty"`
}

func writeError(w http.ResponseWriter, message string, code int) {
resp := Error{
Code: code,
Message: message,
Code: code,
Message: message,
SupportId: "TODO",
}

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

var (
RequestErrorHandler = func(w http.ResponseWriter, err error) {
BadRequestErrorHandler = func(w http.ResponseWriter, err error) {
writeError(w, err.Error(), http.StatusBadRequest)
}
InternalErrorHandler = func(w http.ResponseWriter) {
writeError(w, "An Unexpected Error Occurred.", http.StatusInternalServerError)
writeError(w, "An unexpected error occurred.", http.StatusInternalServerError)
}
UnauthorizedErrorHandler = func(w http.ResponseWriter, err error) {
writeError(w, err.Error(), http.StatusUnauthorized)
}
)

func ParseQueryParams(r *http.Request) (QueryParams, error) {
var params QueryParams
rawQueryParams := r.URL.Query()
params.FilterParams = make(map[string]string)
for key, values := range rawQueryParams {
if strings.HasPrefix(key, "filter_") {
params.FilterParams[key] = values[0]
delete(rawQueryParams, key)
}
}
)

decoder := schema.NewDecoder()
decoder.RegisterConverter(map[string]string{}, func(value string) reflect.Value {
return reflect.ValueOf(map[string]string{})
})
err := decoder.Decode(&params, rawQueryParams)
if err != nil {
log.Error().Err(err).Msg("Error parsing query params")
return QueryParams{}, err
}
return params, nil
}

func GetChainId(r *http.Request) (string, error) {
// TODO: check chainId agains the chain-service to ensure it's valid
chainId := chi.URLParam(r, "chainId")
if _, err := strconv.Atoi(chainId); err != nil {
log.Error().Err(err).Msg("Error getting chainId")
return "", err
}
return chainId, nil
}
Binary file added cmd/api/__debug_bin3998373950
Binary file not shown.
2 changes: 2 additions & 0 deletions cmd/api/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import (

"github.com/go-chi/chi/v5"
"github.com/rs/zerolog/log"
"github.com/thirdweb-dev/indexer/internal/env"
"github.com/thirdweb-dev/indexer/internal/handlers"
customLogger "github.com/thirdweb-dev/indexer/internal/log"
)

func main() {
env.Load()
customLogger.InitLogger()

var r *chi.Mux = chi.NewRouter()
Expand Down
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ go 1.22.0
require (
github.com/ClickHouse/clickhouse-go/v2 v2.28.3
github.com/ethereum/go-ethereum v1.14.8
github.com/go-chi/chi v1.5.4
github.com/go-chi/chi v1.5.5
github.com/go-chi/chi/v5 v5.1.0
github.com/google/uuid v1.6.0
github.com/gorilla/schema v1.2.0
github.com/gorilla/schema v1.4.1
github.com/hashicorp/golang-lru/v2 v2.0.7
github.com/joho/godotenv v1.5.1
github.com/rs/zerolog v1.33.0
Expand Down
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff h1:tY80oXqG
github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww=
github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps=
github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY=
github.com/go-chi/chi v1.5.4 h1:QHdzF2szwjqVV4wmByUnTcsbIg7UGaQ0tPF2t5GcAIs=
github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg=
github.com/go-chi/chi v1.5.5 h1:vOB/HbEMt9QqBqErz07QehcOKHaWFtuj87tTDVz2qXE=
github.com/go-chi/chi v1.5.5/go.mod h1:C9JqLr3tIYjDOZpzn+BCuxY8z8vmca43EeMgyZt7irw=
github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw=
Expand Down Expand Up @@ -95,8 +95,8 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN
github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc=
github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU=
github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E=
github.com/gorilla/schema v1.4.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/go-bexpr v0.1.10 h1:9kuI5PFotCboP3dkDYFr/wi0gg0QVbSNz5oFRpxn4uE=
Expand Down
26 changes: 21 additions & 5 deletions internal/handlers/api.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,34 @@
package handlers

import (
"net/http"

chimiddle "github.com/go-chi/chi/middleware"
"github.com/go-chi/chi/v5"
"github.com/thirdweb-dev/indexer/internal/middleware"
)

func Handler(r *chi.Mux) {
r.Use(chimiddle.StripSlashes)
r.Use(middleware.Authorization)
r.Route("/", func(router chi.Router) {
// might consolidate all variants to one handler function
// Wild card queries
router.Get("/{chainId}/transactions", GetTransactions)
router.Get("/{chainId}/events", GetLogs)

// contract scoped queries
router.Get("/{chainId}/transactions/{contractAddress}", GetTransactionsWithContract)
router.Get("/{chainId}/events/{contractAddress}", GetEventsWithContract)

// signature scoped queries
router.Get("/{chainId}/transactions/{contractAddress}/{functionSig}", GetTransactionsWithContractAndSignature)
router.Get("/{chainId}/events/{contractAddress}/{functionSig}", GetEventsWithContractAndSignature)
})

r.Route("/api", func(router chi.Router) {
router.Use(middleware.Authorization)
router.Route("/v1", func(r chi.Router) {
r.Get("/blocks", GetBlocks)
})
r.Get("/health", func(w http.ResponseWriter, r *http.Request) {
// TODO: implement a simple query before going live
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))
})
}
55 changes: 0 additions & 55 deletions internal/handlers/get_blocks.go

This file was deleted.

121 changes: 121 additions & 0 deletions internal/handlers/logs_handlers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package handlers

import (
"encoding/json"
"net/http"

"github.com/go-chi/chi/v5"
"github.com/rs/zerolog/log"
"github.com/thirdweb-dev/indexer/api"
)

func GetLogs(w http.ResponseWriter, r *http.Request) {
chainId, err := api.GetChainId(r)
if err != nil {
api.BadRequestErrorHandler(w, err)
return
}
queryParams, err := api.ParseQueryParams(r)
if err != nil {
api.BadRequestErrorHandler(w, err)
return
}

var response = api.QueryResponse{
Meta: api.Meta{
ChainIdentifier: chainId,
ContractAddress: "todo",
FunctionSig: "todo",
Page: 1,
Limit: 100,
TotalItems: 0,
TotalPages: 0,
},
Data: []interface{}{queryParams},
}

w.Header().Set("Content-Type", "application/json")
err = json.NewEncoder(w).Encode(response)
if err != nil {
log.Error().Err(err).Msg("Error encoding response")
api.InternalErrorHandler(w)
return
}
}

func GetEventsWithContract(w http.ResponseWriter, r *http.Request) {
chainId, err := api.GetChainId(r)
if err != nil {
api.BadRequestErrorHandler(w, err)
return
}
contractAddress := chi.URLParam(r, "contractAddress")
queryParams, err := api.ParseQueryParams(r)
if err != nil {
api.BadRequestErrorHandler(w, err)
return
}

var response = api.QueryResponse{
Meta: api.Meta{
ChainIdentifier: chainId,
ContractAddress: contractAddress,
FunctionSig: "todo",
Page: 1,
Limit: 100,
TotalItems: 0,
TotalPages: 0,
},
Data: []interface{}{queryParams},
}

w.Header().Set("Content-Type", "application/json")
err = json.NewEncoder(w).Encode(response)
if err != nil {
log.Error().Err(err).Msg("Error encoding response")
api.InternalErrorHandler(w)
return
}

}

func GetEventsWithContractAndSignature(w http.ResponseWriter, r *http.Request) {
chainId, err := api.GetChainId(r)
if err != nil {
api.BadRequestErrorHandler(w, err)
return
}
contractAddress := chi.URLParam(r, "contractAddress")
functionSig := chi.URLParam(r, "functionSig")
queryParams, err := api.ParseQueryParams(r)
if err != nil {
api.BadRequestErrorHandler(w, err)
return
}

var response = api.QueryResponse{
Meta: api.Meta{
ChainIdentifier: chainId,
ContractAddress: contractAddress,
FunctionSig: functionSig,
Page: 1,
Limit: 100,
TotalItems: 0,
TotalPages: 0,
},
Data: []interface{}{queryParams},
Aggregations: map[string]interface{}{
"count": 100,
"sum_value": "1000000000000000000000",
"avg_gas_price": "20000000000",
},
}

w.Header().Set("Content-Type", "application/json")
err = json.NewEncoder(w).Encode(response)
if err != nil {
log.Error().Err(err).Msg("Error encoding response")
api.InternalErrorHandler(w)
return
}
}
Loading

0 comments on commit 10a83b8

Please sign in to comment.