Commit c725718c authored by Will Cory's avatar Will Cory

feat(indexer): Add go-chi api

parent 2fdcbd5a
package api
import (
"encoding/json"
"net/http"
"strconv"
"time"
"github.com/go-chi/chi"
)
// Database interface for deposits
type DepositDB interface {
GetDeposits(limit int, cursor string, sortDirection string) ([]Deposit, string, bool, error)
}
// Database interface for withdrawals
type WithdrawalDB interface {
GetWithdrawals(limit int, cursor string, sortDirection string, sortBy string) ([]Withdrawal, string, bool, error)
}
// Deposit data structure
type Deposit struct {
Guid string `json:"guid"`
Amount string `json:"amount"`
BlockNumber int `json:"blockNumber"`
BlockTimestamp time.Time `json:"blockTimestamp"`
From string `json:"from"`
To string `json:"to"`
TransactionHash string `json:"transactionHash"`
L1Token TokenListItem `json:"l1Token"`
L2Token TokenListItem `json:"l2Token"`
}
// Withdrawal data structure
type Withdrawal struct {
Guid string `json:"guid"`
Amount string `json:"amount"`
BlockNumber int `json:"blockNumber"`
BlockTimestamp time.Time `json:"blockTimestamp"`
From string `json:"from"`
To string `json:"to"`
TransactionHash string `json:"transactionHash"`
WithdrawalState string `json:"withdrawalState"`
Proof *ProofClaim `json:"proof"`
Claim *ProofClaim `json:"claim"`
L1Token TokenListItem `json:"l1Token"`
L2Token TokenListItem `json:"l2Token"`
}
// TokenListItem data structure
type TokenListItem struct {
ChainId int `json:"chainId"`
Address string `json:"address"`
Name string `json:"name"`
Symbol string `json:"symbol"`
Decimals int `json:"decimals"`
LogoURI string `json:"logoURI"`
Extensions Extensions `json:"extensions"`
}
// Extensions data structure
type Extensions struct {
OptimismBridgeAddress string `json:"optimismBridgeAddress"`
BridgeType string `json:"bridgeType"`
}
// ProofClaim data structure
type ProofClaim struct {
TransactionHash string `json:"transactionHash"`
BlockTimestamp time.Time `json:"blockTimestamp"`
BlockNumber int `json:"blockNumber"`
}
// PaginationResponse for paginated responses
type PaginationResponse struct {
// TODO type this better
Data interface{} `json:"data"`
Cursor string `json:"cursor"`
HasNextPage bool `json:"hasNextPage"`
}
func getDepositsHandler(depositDB DepositDB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
limit := getIntFromQuery(r, "limit", 10)
cursor := r.URL.Query().Get("cursor")
sortDirection := r.URL.Query().Get("sortDirection")
deposits, nextCursor, hasNextPage, err := depositDB.GetDeposits(limit, cursor, sortDirection)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
response := PaginationResponse{
Data: deposits,
Cursor: nextCursor,
HasNextPage: hasNextPage,
}
jsonResponse(w, response, http.StatusOK)
}
}
func getWithdrawalsHandler(withdrawalDB WithdrawalDB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
limit := getIntFromQuery(r, "limit", 10)
cursor := r.URL.Query().Get("cursor")
sortDirection := r.URL.Query().Get("sortDirection")
sortBy := r.URL.Query().Get("sortBy")
withdrawals, nextCursor, hasNextPage, err := withdrawalDB.GetWithdrawals(limit, cursor, sortDirection, sortBy)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
response := PaginationResponse{
Data: withdrawals,
Cursor: nextCursor,
HasNextPage: hasNextPage,
}
jsonResponse(w, response, http.StatusOK)
}
}
func getIntFromQuery(r *http.Request, key string, defaultValue int) int {
valueStr := r.URL.Query().Get(key)
if valueStr == "" {
return defaultValue
}
value, err := strconv.Atoi(valueStr)
if err != nil {
return defaultValue
}
return value
}
func jsonResponse(w http.ResponseWriter, data interface{}, statusCode int) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
json.NewEncoder(w).Encode(data)
}
type Api struct {
Router *chi.Mux
DepositDB DepositDB
WithdrawalDB WithdrawalDB
}
func NewApi(depositDB DepositDB, withdrawalDB WithdrawalDB) *Api {
r := chi.NewRouter()
r.Get("/api/v0/deposits", getDepositsHandler(depositDB))
r.Get("/api/v0/withdrawals", getWithdrawalsHandler(withdrawalDB))
return &Api{
Router: r,
DepositDB: depositDB,
WithdrawalDB: withdrawalDB,
}
}
func (a *Api) Listen(port string) {
http.ListenAndServe(port, a.Router)
}
package api_test
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/ethereum-optimism/optimism/indexer/api"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
type MockDB struct {
mock.Mock
}
func (db *MockDB) GetDeposits(limit int, cursor string, sortDirection string) ([]api.Deposit, string, bool, error) {
args := db.Called(limit, cursor, sortDirection)
return args.Get(0).([]api.Deposit), args.String(1), args.Bool(2), args.Error(3)
}
func (db *MockDB) GetWithdrawals(limit int, cursor string, sortDirection string, sortBy string) ([]api.Withdrawal, string, bool, error) {
args := db.Called(limit, cursor, sortDirection, sortBy)
return args.Get(0).([]api.Withdrawal), args.String(1), args.Bool(2), args.Error(3)
}
func TestApi(t *testing.T) {
mockDB := new(MockDB)
mockDeposits := []api.Deposit{
{
Guid: "test-guid",
Amount: "1000",
BlockNumber: 123,
BlockTimestamp: time.Unix(123456, 0),
From: "0x1",
To: "0x2",
TransactionHash: "0x3",
},
}
mockWithdrawals := []api.Withdrawal{
{
Guid: "test-guid",
Amount: "1000",
BlockNumber: 123,
BlockTimestamp: time.Unix(123456, 0),
From: "0x1",
To: "0x2",
TransactionHash: "0x3",
},
}
mockDB.On("GetDeposits", 10, "", "").Return(mockDeposits, "nextCursor", false, nil)
mockDB.On("GetWithdrawals", 10, "", "", "").Return(mockWithdrawals, "nextCursor", false, nil)
testApi := api.NewApi(mockDB, mockDB)
req, _ := http.NewRequest("GET", "/api/v0/deposits", nil)
rr := httptest.NewRecorder()
testApi.Router.ServeHTTP(rr, req)
assert.Equal(t, http.StatusOK, rr.Code, "status code should be 200")
// TODO make this type exist
var depositsResponse api.DepositsResponse
err := json.Unmarshal(rr.Body.Bytes(), &depositsResponse)
assert.NoError(t, err)
assert.Equal(t, mockDeposits, depositsResponse.Data)
req, _ = http.NewRequest("GET", "/api/v0/withdrawals", nil)
rr = httptest.NewRecorder()
testApi.Router.ServeHTTP(rr, req)
assert.Equal(t, http.StatusOK, rr.Code, "status code should be 200")
// TODO make this type exist
var withdrawalsResponse WithdrawalsResponse
err = json
err = json.Unmarshal(rr.Body.Bytes(), &withdrawalsResponse)
assert.NoError(t, err)
// Assert response data
assert.Equal(t, mockWithdrawals, withdrawalsResponse.Data)
// Finally, assert that the methods were called with the expected parameters
mockDB.AssertCalled(t, "GetDeposits", 10, "", "")
mockDB.AssertCalled(t, "GetWithdrawals", 10, "", "", "")
}
...@@ -5,8 +5,10 @@ go 1.19 ...@@ -5,8 +5,10 @@ go 1.19
replace github.com/ethereum/go-ethereum v1.11.4 => github.com/ethereum-optimism/op-geth v1.11.2-de8c5df46.0.20230321002540-11f0554a4313 replace github.com/ethereum/go-ethereum v1.11.4 => github.com/ethereum-optimism/op-geth v1.11.2-de8c5df46.0.20230321002540-11f0554a4313
require ( require (
github.com/BurntSushi/toml v1.3.0
github.com/ethereum-optimism/optimism v0.2.1-0.20230326215719-b8e2fa58359a github.com/ethereum-optimism/optimism v0.2.1-0.20230326215719-b8e2fa58359a
github.com/ethereum/go-ethereum v1.11.4 github.com/ethereum/go-ethereum v1.11.4
github.com/go-chi/chi v1.5.4
github.com/google/uuid v1.3.0 github.com/google/uuid v1.3.0
github.com/gorilla/mux v1.8.0 github.com/gorilla/mux v1.8.0
github.com/lib/pq v1.10.4 github.com/lib/pq v1.10.4
...@@ -17,7 +19,6 @@ require ( ...@@ -17,7 +19,6 @@ require (
) )
require ( require (
github.com/BurntSushi/toml v1.3.0 // indirect
github.com/DataDog/zstd v1.5.2 // indirect github.com/DataDog/zstd v1.5.2 // indirect
github.com/VictoriaMetrics/fastcache v1.10.0 // indirect github.com/VictoriaMetrics/fastcache v1.10.0 // indirect
github.com/benbjohnson/clock v1.3.0 // indirect github.com/benbjohnson/clock v1.3.0 // indirect
......
...@@ -221,6 +221,8 @@ github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aev ...@@ -221,6 +221,8 @@ github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aev
github.com/glycerine/go-unsnap-stream v0.0.0-20180323001048-9f0cb55181dd/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE= github.com/glycerine/go-unsnap-stream v0.0.0-20180323001048-9f0cb55181dd/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE=
github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24= github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24=
github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98= github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98=
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/v5 v5.0.0/go.mod h1:BBug9lr0cqtdAhsu6R4AAdvufI0/XBzAQSsUqJpoZOs= github.com/go-chi/chi/v5 v5.0.0/go.mod h1:BBug9lr0cqtdAhsu6R4AAdvufI0/XBzAQSsUqJpoZOs=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment