Commit 87c7bcea authored by Hamdi Allam's avatar Hamdi Allam Committed by GitHub

Merge pull request #8160 from epociask/indexer.withdrawal_type_supplies

feat(indexer) - API type filtering (init, proven, finalized) for withdrawal values in `/supply` endpoint
parents d240329d f5796140
...@@ -3,6 +3,11 @@ ...@@ -3,6 +3,11 @@
////////// //////////
// source: models.go // source: models.go
export interface QueryParams {
Address: any /* common.Address */;
Limit: number /* int */;
Cursor: string;
}
/** /**
* DepositItem ... Deposit item model for API responses * DepositItem ... Deposit item model for API responses
*/ */
...@@ -54,4 +59,6 @@ export interface WithdrawalResponse { ...@@ -54,4 +59,6 @@ export interface WithdrawalResponse {
export interface BridgeSupplyView { export interface BridgeSupplyView {
l1DepositSum: number /* float64 */; l1DepositSum: number /* float64 */;
l2WithdrawalSum: number /* float64 */; l2WithdrawalSum: number /* float64 */;
provenSum: number /* float64 */;
finalizedSum: number /* float64 */;
} }
...@@ -17,6 +17,7 @@ import ( ...@@ -17,6 +17,7 @@ import (
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/ethereum-optimism/optimism/indexer/api/routes" "github.com/ethereum-optimism/optimism/indexer/api/routes"
"github.com/ethereum-optimism/optimism/indexer/api/service"
"github.com/ethereum-optimism/optimism/indexer/config" "github.com/ethereum-optimism/optimism/indexer/config"
"github.com/ethereum-optimism/optimism/indexer/database" "github.com/ethereum-optimism/optimism/indexer/database"
"github.com/ethereum-optimism/optimism/op-service/httputil" "github.com/ethereum-optimism/optimism/op-service/httputil"
...@@ -137,8 +138,11 @@ func (a *APIService) initDB(ctx context.Context, connector DBConnector) error { ...@@ -137,8 +138,11 @@ func (a *APIService) initDB(ctx context.Context, connector DBConnector) error {
} }
func (a *APIService) initRouter(apiConfig config.ServerConfig) { func (a *APIService) initRouter(apiConfig config.ServerConfig) {
v := new(service.Validator)
svc := service.New(v, a.bv, a.log)
apiRouter := chi.NewRouter() apiRouter := chi.NewRouter()
h := routes.NewRoutes(a.log, a.bv, apiRouter) h := routes.NewRoutes(a.log, apiRouter, svc)
promRecorder := metrics.NewPromHTTPRecorder(a.metricsRegistry, MetricsNamespace) promRecorder := metrics.NewPromHTTPRecorder(a.metricsRegistry, MetricsNamespace)
......
...@@ -96,10 +96,10 @@ func (mbv *MockBridgeTransfersView) L2BridgeWithdrawalsByAddress(address common. ...@@ -96,10 +96,10 @@ func (mbv *MockBridgeTransfersView) L2BridgeWithdrawalsByAddress(address common.
}, nil }, nil
} }
func (mbv *MockBridgeTransfersView) L1BridgeDepositSum() (float64, error) { func (mbv *MockBridgeTransfersView) L1TxDepositSum() (float64, error) {
return 69, nil return 69, nil
} }
func (mbv *MockBridgeTransfersView) L2BridgeWithdrawalSum() (float64, error) { func (mbv *MockBridgeTransfersView) L2BridgeWithdrawalSum(database.WithdrawFilter) (float64, error) {
return 420, nil return 420, nil
} }
......
package models package models
import ( import (
"github.com/ethereum-optimism/optimism/indexer/database"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
) )
type QueryParams struct {
Address common.Address
Limit int
Cursor string
}
// DepositItem ... Deposit item model for API responses // DepositItem ... Deposit item model for API responses
type DepositItem struct { type DepositItem struct {
Guid string `json:"guid"` Guid string `json:"guid"`
...@@ -50,41 +55,8 @@ type WithdrawalResponse struct { ...@@ -50,41 +55,8 @@ type WithdrawalResponse struct {
} }
type BridgeSupplyView struct { type BridgeSupplyView struct {
L1DepositSum float64 `json:"l1DepositSum"` L1DepositSum float64 `json:"l1DepositSum"`
L2WithdrawalSum float64 `json:"l2WithdrawalSum"` InitWithdrawalSum float64 `json:"l2WithdrawalSum"`
} ProvenWithdrawSum float64 `json:"provenSum"`
FinalizedWithdrawSum float64 `json:"finalizedSum"`
// FIXME make a pure function that returns a struct instead of newWithdrawalResponse
// newWithdrawalResponse ... Converts a database.L2BridgeWithdrawalsResponse to an api.WithdrawalResponse
func CreateWithdrawalResponse(withdrawals *database.L2BridgeWithdrawalsResponse) WithdrawalResponse {
items := make([]WithdrawalItem, len(withdrawals.Withdrawals))
for i, withdrawal := range withdrawals.Withdrawals {
cdh := withdrawal.L2BridgeWithdrawal.CrossDomainMessageHash
if cdh == nil { // Zero value indicates that the withdrawal didn't have a cross domain message
cdh = &common.Hash{0}
}
item := WithdrawalItem{
Guid: withdrawal.L2BridgeWithdrawal.TransactionWithdrawalHash.String(),
L2BlockHash: withdrawal.L2BlockHash.String(),
Timestamp: withdrawal.L2BridgeWithdrawal.Tx.Timestamp,
From: withdrawal.L2BridgeWithdrawal.Tx.FromAddress.String(),
To: withdrawal.L2BridgeWithdrawal.Tx.ToAddress.String(),
TransactionHash: withdrawal.L2TransactionHash.String(),
Amount: withdrawal.L2BridgeWithdrawal.Tx.Amount.String(),
CrossDomainMessageHash: cdh.String(),
L1ProvenTxHash: withdrawal.ProvenL1TransactionHash.String(),
L1FinalizedTxHash: withdrawal.FinalizedL1TransactionHash.String(),
L1TokenAddress: withdrawal.L2BridgeWithdrawal.TokenPair.RemoteTokenAddress.String(),
L2TokenAddress: withdrawal.L2BridgeWithdrawal.TokenPair.LocalTokenAddress.String(),
}
items[i] = item
}
return WithdrawalResponse{
Cursor: withdrawals.Cursor,
HasNextPage: withdrawals.HasNextPage,
Items: items,
}
} }
package models_test package models_test
import (
"fmt"
"reflect"
"testing"
"github.com/ethereum-optimism/optimism/indexer/api/models"
"github.com/ethereum-optimism/optimism/indexer/database"
"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/require"
)
func TestCreateWithdrawal(t *testing.T) {
// (1) Create a dummy database response object
cdh := common.HexToHash("0x2")
dbWithdrawals := &database.L2BridgeWithdrawalsResponse{
Withdrawals: []database.L2BridgeWithdrawalWithTransactionHashes{
{
L2BridgeWithdrawal: database.L2BridgeWithdrawal{
TransactionWithdrawalHash: common.HexToHash("0x1"),
BridgeTransfer: database.BridgeTransfer{
CrossDomainMessageHash: &cdh,
Tx: database.Transaction{
FromAddress: common.HexToAddress("0x3"),
ToAddress: common.HexToAddress("0x4"),
Timestamp: 5,
},
TokenPair: database.TokenPair{
LocalTokenAddress: common.HexToAddress("0x6"),
RemoteTokenAddress: common.HexToAddress("0x7"),
},
},
},
},
},
}
// (2) Create and validate response object
response := models.CreateWithdrawalResponse(dbWithdrawals)
require.NotEmpty(t, response.Items)
require.Len(t, response.Items, 1)
// (3) Use reflection to check that all fields in WithdrawalItem are populated correctly
item := response.Items[0]
structType := reflect.TypeOf(item)
structVal := reflect.ValueOf(item)
fieldNum := structVal.NumField()
for i := 0; i < fieldNum; i++ {
field := structVal.Field(i)
fieldName := structType.Field(i).Name
isSet := field.IsValid() && !field.IsZero()
require.True(t, isSet, fmt.Sprintf("%s in not set", fieldName))
}
}
...@@ -7,9 +7,6 @@ import ( ...@@ -7,9 +7,6 @@ import (
const ( const (
InternalServerError = "Internal server error" InternalServerError = "Internal server error"
// defaultPageLimit ... Default page limit for pagination
defaultPageLimit = 100
) )
// jsonResponse ... Marshals and writes a JSON response provided arbitrary data // jsonResponse ... Marshals and writes a JSON response provided arbitrary data
......
...@@ -3,76 +3,32 @@ package routes ...@@ -3,76 +3,32 @@ package routes
import ( import (
"net/http" "net/http"
"github.com/ethereum-optimism/optimism/indexer/api/models"
"github.com/ethereum-optimism/optimism/indexer/database"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
) )
// newDepositResponse ... Converts a database.L1BridgeDepositsResponse to an api.DepositResponse
func newDepositResponse(deposits *database.L1BridgeDepositsResponse) models.DepositResponse {
items := make([]models.DepositItem, len(deposits.Deposits))
for i, deposit := range deposits.Deposits {
item := models.DepositItem{
Guid: deposit.L1BridgeDeposit.TransactionSourceHash.String(),
L1BlockHash: deposit.L1BlockHash.String(),
Timestamp: deposit.L1BridgeDeposit.Tx.Timestamp,
L1TxHash: deposit.L1TransactionHash.String(),
L2TxHash: deposit.L2TransactionHash.String(),
From: deposit.L1BridgeDeposit.Tx.FromAddress.String(),
To: deposit.L1BridgeDeposit.Tx.ToAddress.String(),
Amount: deposit.L1BridgeDeposit.Tx.Amount.String(),
L1TokenAddress: deposit.L1BridgeDeposit.TokenPair.LocalTokenAddress.String(),
L2TokenAddress: deposit.L1BridgeDeposit.TokenPair.RemoteTokenAddress.String(),
}
items[i] = item
}
return models.DepositResponse{
Cursor: deposits.Cursor,
HasNextPage: deposits.HasNextPage,
Items: items,
}
}
// L1DepositsHandler ... Handles /api/v0/deposits/{address} GET requests // L1DepositsHandler ... Handles /api/v0/deposits/{address} GET requests
func (h Routes) L1DepositsHandler(w http.ResponseWriter, r *http.Request) { func (h Routes) L1DepositsHandler(w http.ResponseWriter, r *http.Request) {
addressValue := chi.URLParam(r, "address") address := chi.URLParam(r, "address")
cursor := r.URL.Query().Get("cursor") cursor := r.URL.Query().Get("cursor")
limitQuery := r.URL.Query().Get("limit") limit := r.URL.Query().Get("limit")
address, err := h.v.ParseValidateAddress(addressValue)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
h.logger.Error("Invalid address param", "param", addressValue)
h.logger.Error(err.Error())
return
}
err = h.v.ValidateCursor(cursor) params, err := h.svc.QueryParams(address, cursor, limit)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest) http.Error(w, "invalid query params", http.StatusBadRequest)
h.logger.Error("Invalid cursor param", "param", cursor, "err", err.Error()) h.logger.Error("error reading request params", "err", err.Error())
}
limit, err := h.v.ParseValidateLimit(limitQuery)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
h.logger.Error("Invalid limit param", "param", limitQuery, "err", err.Error())
return return
} }
deposits, err := h.view.L1BridgeDepositsByAddress(address, cursor, limit) deposits, err := h.svc.GetDeposits(params)
if err != nil { if err != nil {
http.Error(w, "Internal server error reading deposits", http.StatusInternalServerError) http.Error(w, "internal server error", http.StatusInternalServerError)
h.logger.Error("Unable to read deposits from DB", "err", err.Error()) h.logger.Error("error fetching deposits", "err", err.Error())
return return
} }
response := newDepositResponse(deposits) resp := h.svc.DepositResponse(deposits)
err = jsonResponse(w, resp, http.StatusOK)
err = jsonResponse(w, response, http.StatusOK)
if err != nil { if err != nil {
h.logger.Error("Error writing response", "err", err) h.logger.Error("error writing response", "err", err)
} }
} }
package routes package routes
import ( import (
"github.com/ethereum-optimism/optimism/indexer/database" "github.com/ethereum-optimism/optimism/indexer/api/service"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
) )
// Routes ... Route handler struct
type Routes struct { type Routes struct {
logger log.Logger logger log.Logger
view database.BridgeTransfersView
router *chi.Mux router *chi.Mux
v *Validator svc service.Service
} }
// NewRoutes ... Construct a new route handler instance // NewRoutes ... Construct a new route handler instance
func NewRoutes(logger log.Logger, bv database.BridgeTransfersView, r *chi.Mux) Routes { func NewRoutes(l log.Logger, r *chi.Mux, svc service.Service) Routes {
return Routes{ return Routes{
logger: logger, logger: l,
view: bv,
router: r, router: r,
svc: svc,
} }
} }
...@@ -2,32 +2,18 @@ package routes ...@@ -2,32 +2,18 @@ package routes
import ( import (
"net/http" "net/http"
"github.com/ethereum-optimism/optimism/indexer/api/models"
) )
// SupplyView ... Handles /api/v0/supply GET requests // SupplyView ... Handles /api/v0/supply GET requests
func (h Routes) SupplyView(w http.ResponseWriter, r *http.Request) { func (h Routes) SupplyView(w http.ResponseWriter, r *http.Request) {
depositSum, err := h.view.L1BridgeDepositSum() view, err := h.svc.GetSupplyInfo()
if err != nil { if err != nil {
http.Error(w, "internal server error reading deposits", http.StatusInternalServerError) http.Error(w, "Internal server error", http.StatusInternalServerError)
h.logger.Error("unable to read deposits from DB", "err", err.Error()) h.logger.Error("error getting supply info", "err", err)
return return
} }
withdrawalSum, err := h.view.L2BridgeWithdrawalSum()
if err != nil {
http.Error(w, "internal server error reading withdrawals", http.StatusInternalServerError)
h.logger.Error("unable to read withdrawals from DB", "err", err.Error())
return
}
view := models.BridgeSupplyView{
L1DepositSum: depositSum,
L2WithdrawalSum: withdrawalSum,
}
err = jsonResponse(w, view, http.StatusOK) err = jsonResponse(w, view, http.StatusOK)
if err != nil { if err != nil {
h.logger.Error("error writing response", "err", err) h.logger.Error("error writing response", "err", err)
......
...@@ -3,46 +3,31 @@ package routes ...@@ -3,46 +3,31 @@ package routes
import ( import (
"net/http" "net/http"
"github.com/ethereum-optimism/optimism/indexer/api/models"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
) )
// L2WithdrawalsHandler ... Handles /api/v0/withdrawals/{address} GET requests // L2WithdrawalsHandler ... Handles /api/v0/withdrawals/{address} GET requests
func (h Routes) L2WithdrawalsHandler(w http.ResponseWriter, r *http.Request) { func (h Routes) L2WithdrawalsHandler(w http.ResponseWriter, r *http.Request) {
addressValue := chi.URLParam(r, "address") address := chi.URLParam(r, "address")
cursor := r.URL.Query().Get("cursor") cursor := r.URL.Query().Get("cursor")
limitQuery := r.URL.Query().Get("limit") limit := r.URL.Query().Get("limit")
address, err := h.v.ParseValidateAddress(addressValue) params, err := h.svc.QueryParams(address, cursor, limit)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest) http.Error(w, "Invalid query params", http.StatusBadRequest)
h.logger.Error("Invalid address param", "param", addressValue, "err", err) h.logger.Error("Invalid query params", "err", err.Error())
return return
} }
err = h.v.ValidateCursor(cursor) withdrawals, err := h.svc.GetWithdrawals(params)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest) http.Error(w, "Internal server error", http.StatusInternalServerError)
h.logger.Error("Invalid cursor param", "param", cursor, "err", err) h.logger.Error("Error getting withdrawals", "err", err.Error())
return return
} }
limit, err := h.v.ParseValidateLimit(limitQuery) resp := h.svc.WithdrawResponse(withdrawals)
if err != nil { err = jsonResponse(w, resp, http.StatusOK)
http.Error(w, err.Error(), http.StatusBadRequest)
h.logger.Error("Invalid query params", "err", err)
return
}
withdrawals, err := h.view.L2BridgeWithdrawalsByAddress(address, cursor, limit)
if err != nil {
http.Error(w, "Internal server error reading withdrawals", http.StatusInternalServerError)
h.logger.Error("Unable to read withdrawals from DB", "err", err.Error())
return
}
response := models.CreateWithdrawalResponse(withdrawals)
err = jsonResponse(w, response, http.StatusOK)
if err != nil { if err != nil {
h.logger.Error("Error writing response", "err", err.Error()) h.logger.Error("Error writing response", "err", err.Error())
} }
......
package service
import (
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum-optimism/optimism/indexer/api/models"
"github.com/ethereum-optimism/optimism/indexer/database"
"github.com/ethereum/go-ethereum/common"
)
type Service interface {
GetDeposits(*models.QueryParams) (*database.L1BridgeDepositsResponse, error)
DepositResponse(*database.L1BridgeDepositsResponse) models.DepositResponse
GetWithdrawals(params *models.QueryParams) (*database.L2BridgeWithdrawalsResponse, error)
WithdrawResponse(*database.L2BridgeWithdrawalsResponse) models.WithdrawalResponse
GetSupplyInfo() (*models.BridgeSupplyView, error)
QueryParams(address, cursor, limit string) (*models.QueryParams, error)
}
type HandlerSvc struct {
v *Validator
db database.BridgeTransfersView
logger log.Logger
}
func New(v *Validator, db database.BridgeTransfersView, l log.Logger) Service {
return &HandlerSvc{
logger: l,
v: v,
db: db,
}
}
func (svc *HandlerSvc) QueryParams(a, c, l string) (*models.QueryParams, error) {
address, err := svc.v.ParseValidateAddress(a)
if err != nil {
svc.logger.Error("invalid address param", "param", a, "err", err)
return nil, err
}
err = svc.v.ValidateCursor(c)
if err != nil {
svc.logger.Error("invalid cursor param", "cursor", c, "err", err)
return nil, err
}
limit, err := svc.v.ParseValidateLimit(l)
if err != nil {
svc.logger.Error("invalid query param", "cursor", c, "err", err)
return nil, err
}
return &models.QueryParams{
Address: address,
Cursor: c,
Limit: limit,
}, nil
}
func (svc *HandlerSvc) GetWithdrawals(params *models.QueryParams) (*database.L2BridgeWithdrawalsResponse, error) {
withdrawals, err := svc.db.L2BridgeWithdrawalsByAddress(params.Address, params.Cursor, params.Limit)
if err != nil {
svc.logger.Error("error getting withdrawals", "err", err.Error(), "address", params.Address.String())
return nil, err
}
svc.logger.Debug("read withdrawals from db", "count", len(withdrawals.Withdrawals), "address", params.Address.String())
return withdrawals, nil
}
func (svc *HandlerSvc) WithdrawResponse(withdrawals *database.L2BridgeWithdrawalsResponse) models.WithdrawalResponse {
items := make([]models.WithdrawalItem, len(withdrawals.Withdrawals))
for i, withdrawal := range withdrawals.Withdrawals {
cdh := withdrawal.L2BridgeWithdrawal.CrossDomainMessageHash
if cdh == nil { // Zero value indicates that the withdrawal didn't have a cross domain message
cdh = &common.Hash{0}
}
item := models.WithdrawalItem{
Guid: withdrawal.L2BridgeWithdrawal.TransactionWithdrawalHash.String(),
L2BlockHash: withdrawal.L2BlockHash.String(),
Timestamp: withdrawal.L2BridgeWithdrawal.Tx.Timestamp,
From: withdrawal.L2BridgeWithdrawal.Tx.FromAddress.String(),
To: withdrawal.L2BridgeWithdrawal.Tx.ToAddress.String(),
TransactionHash: withdrawal.L2TransactionHash.String(),
Amount: withdrawal.L2BridgeWithdrawal.Tx.Amount.String(),
CrossDomainMessageHash: cdh.String(),
L1ProvenTxHash: withdrawal.ProvenL1TransactionHash.String(),
L1FinalizedTxHash: withdrawal.FinalizedL1TransactionHash.String(),
L1TokenAddress: withdrawal.L2BridgeWithdrawal.TokenPair.RemoteTokenAddress.String(),
L2TokenAddress: withdrawal.L2BridgeWithdrawal.TokenPair.LocalTokenAddress.String(),
}
items[i] = item
}
return models.WithdrawalResponse{
Cursor: withdrawals.Cursor,
HasNextPage: withdrawals.HasNextPage,
Items: items,
}
}
func (svc *HandlerSvc) GetDeposits(params *models.QueryParams) (*database.L1BridgeDepositsResponse, error) {
deposits, err := svc.db.L1BridgeDepositsByAddress(params.Address, params.Cursor, params.Limit)
if err != nil {
svc.logger.Error("error getting deposits", "err", err.Error(), "address", params.Address.String())
return nil, err
}
svc.logger.Debug("read deposits from db", "count", len(deposits.Deposits), "address", params.Address.String())
return deposits, nil
}
// DepositResponse ... Converts a database.L1BridgeDepositsResponse to an api.DepositResponse
func (svc *HandlerSvc) DepositResponse(deposits *database.L1BridgeDepositsResponse) models.DepositResponse {
items := make([]models.DepositItem, len(deposits.Deposits))
for i, deposit := range deposits.Deposits {
item := models.DepositItem{
Guid: deposit.L1BridgeDeposit.TransactionSourceHash.String(),
L1BlockHash: deposit.L1BlockHash.String(),
Timestamp: deposit.L1BridgeDeposit.Tx.Timestamp,
L1TxHash: deposit.L1TransactionHash.String(),
L2TxHash: deposit.L2TransactionHash.String(),
From: deposit.L1BridgeDeposit.Tx.FromAddress.String(),
To: deposit.L1BridgeDeposit.Tx.ToAddress.String(),
Amount: deposit.L1BridgeDeposit.Tx.Amount.String(),
L1TokenAddress: deposit.L1BridgeDeposit.TokenPair.LocalTokenAddress.String(),
L2TokenAddress: deposit.L1BridgeDeposit.TokenPair.RemoteTokenAddress.String(),
}
items[i] = item
}
return models.DepositResponse{
Cursor: deposits.Cursor,
HasNextPage: deposits.HasNextPage,
Items: items,
}
}
// GetSupplyInfo ... Fetch native bridge supply info
func (svc *HandlerSvc) GetSupplyInfo() (*models.BridgeSupplyView, error) {
depositSum, err := svc.db.L1TxDepositSum()
if err != nil {
svc.logger.Error("error getting deposit sum", "err", err)
return nil, err
}
initSum, err := svc.db.L2BridgeWithdrawalSum(database.All)
if err != nil {
svc.logger.Error("error getting init sum", "err", err)
return nil, err
}
provenSum, err := svc.db.L2BridgeWithdrawalSum(database.Proven)
if err != nil {
svc.logger.Error("error getting proven sum", "err", err)
return nil, err
}
finalizedSum, err := svc.db.L2BridgeWithdrawalSum(database.Finalized)
if err != nil {
svc.logger.Error("error getting finalized sum", "err", err)
return nil, err
}
return &models.BridgeSupplyView{
L1DepositSum: depositSum,
InitWithdrawalSum: initSum,
ProvenWithdrawSum: provenSum,
FinalizedWithdrawSum: finalizedSum,
}, nil
}
package service_test
import (
"fmt"
"reflect"
"testing"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum-optimism/optimism/indexer/api/service"
"github.com/ethereum-optimism/optimism/indexer/database"
"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/require"
)
func assertFieldsAreSet(t *testing.T, item any) {
structType := reflect.TypeOf(item)
structVal := reflect.ValueOf(item)
fieldNum := structVal.NumField()
for i := 0; i < fieldNum; i++ {
field := structVal.Field(i)
fieldName := structType.Field(i).Name
isSet := field.IsValid() && !field.IsZero()
require.True(t, isSet, fmt.Sprintf("%s in not set", fieldName))
}
}
func TestWithdrawalResponse(t *testing.T) {
svc := service.New(nil, nil, nil)
cdh := common.HexToHash("0x2")
withdraws := &database.L2BridgeWithdrawalsResponse{
Withdrawals: []database.L2BridgeWithdrawalWithTransactionHashes{
{
L2BridgeWithdrawal: database.L2BridgeWithdrawal{
TransactionWithdrawalHash: common.HexToHash("0x1"),
BridgeTransfer: database.BridgeTransfer{
CrossDomainMessageHash: &cdh,
Tx: database.Transaction{
FromAddress: common.HexToAddress("0x3"),
ToAddress: common.HexToAddress("0x4"),
Timestamp: 5,
},
TokenPair: database.TokenPair{
LocalTokenAddress: common.HexToAddress("0x6"),
RemoteTokenAddress: common.HexToAddress("0x7"),
},
},
},
},
},
}
response := svc.WithdrawResponse(withdraws)
require.NotEmpty(t, response.Items)
require.Len(t, response.Items, 1)
assertFieldsAreSet(t, response.Items[0])
}
func TestDepositResponse(t *testing.T) {
cdh := common.HexToHash("0x2")
svc := service.New(nil, nil, nil)
deposits := &database.L1BridgeDepositsResponse{
Deposits: []database.L1BridgeDepositWithTransactionHashes{
{
L1BridgeDeposit: database.L1BridgeDeposit{
BridgeTransfer: database.BridgeTransfer{
CrossDomainMessageHash: &cdh,
Tx: database.Transaction{
FromAddress: common.HexToAddress("0x3"),
ToAddress: common.HexToAddress("0x4"),
Timestamp: 5,
},
TokenPair: database.TokenPair{
LocalTokenAddress: common.HexToAddress("0x6"),
RemoteTokenAddress: common.HexToAddress("0x7"),
},
},
},
},
},
}
response := svc.DepositResponse(deposits)
require.NotEmpty(t, response.Items)
require.Len(t, response.Items, 1)
assertFieldsAreSet(t, response.Items[0])
}
func TestQueryParams(t *testing.T) {
var tests = []struct {
name string
test func(*testing.T, service.Service)
}{
{
name: "empty params",
test: func(t *testing.T, svc service.Service) {
params, err := svc.QueryParams("", "", "")
require.Error(t, err)
require.Nil(t, params)
},
},
{
name: "empty params except address",
test: func(t *testing.T, svc service.Service) {
addr := common.HexToAddress("0x420")
params, err := svc.QueryParams(addr.String(), "", "")
require.NoError(t, err)
require.NotNil(t, params)
require.Equal(t, addr, params.Address)
require.Equal(t, 100, params.Limit)
require.Equal(t, "", params.Cursor)
},
},
}
v := new(service.Validator)
svc := service.New(v, nil, log.New())
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.test(t, svc)
})
}
}
package routes package service
import ( import (
"strconv"
"errors" "errors"
"strconv"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
) )
...@@ -11,25 +10,6 @@ import ( ...@@ -11,25 +10,6 @@ import (
// Validator ... Validates API user request parameters // Validator ... Validates API user request parameters
type Validator struct{} type Validator struct{}
// ParseValidateLimit ... Validates and parses the limit query parameters
func (v *Validator) ParseValidateLimit(limit string) (int, error) {
if limit == "" {
return defaultPageLimit, nil
}
val, err := strconv.Atoi(limit)
if err != nil {
return 0, errors.New("limit must be an integer value")
}
if val <= 0 {
return 0, errors.New("limit must be greater than 0")
}
// TODO - Add a check against a max limit value
return val, nil
}
// ParseValidateAddress ... Validates and parses the address query parameter // ParseValidateAddress ... Validates and parses the address query parameter
func (v *Validator) ParseValidateAddress(addr string) (common.Address, error) { func (v *Validator) ParseValidateAddress(addr string) (common.Address, error) {
if !common.IsHexAddress(addr) { if !common.IsHexAddress(addr) {
...@@ -60,3 +40,22 @@ func (v *Validator) ValidateCursor(cursor string) error { ...@@ -60,3 +40,22 @@ func (v *Validator) ValidateCursor(cursor string) error {
return nil return nil
} }
// ParseValidateLimit ... Validates and parses the limit query parameters
func (v *Validator) ParseValidateLimit(limit string) (int, error) {
if limit == "" {
return 100, nil
}
val, err := strconv.Atoi(limit)
if err != nil {
return 0, errors.New("limit must be an integer value")
}
if val <= 0 {
return 0, errors.New("limit must be greater than 0")
}
// TODO - Add a check against a max limit value
return val, nil
}
package routes package service
import ( import (
"testing" "testing"
......
...@@ -125,7 +125,7 @@ func (c *Client) HealthCheck() error { ...@@ -125,7 +125,7 @@ func (c *Client) HealthCheck() error {
// GetDepositsByAddress ... Gets a deposit response object provided an L1 address and cursor // GetDepositsByAddress ... Gets a deposit response object provided an L1 address and cursor
func (c *Client) GetDepositsByAddress(l1Address common.Address, cursor string) (*models.DepositResponse, error) { func (c *Client) GetDepositsByAddress(l1Address common.Address, cursor string) (*models.DepositResponse, error) {
var dResponse *models.DepositResponse var response models.DepositResponse
url := c.cfg.BaseURL + api.DepositsPath + l1Address.String() + urlParams url := c.cfg.BaseURL + api.DepositsPath + l1Address.String() + urlParams
endpoint := fmt.Sprintf(url, cursor, c.cfg.PaginationLimit) endpoint := fmt.Sprintf(url, cursor, c.cfg.PaginationLimit)
...@@ -134,11 +134,11 @@ func (c *Client) GetDepositsByAddress(l1Address common.Address, cursor string) ( ...@@ -134,11 +134,11 @@ func (c *Client) GetDepositsByAddress(l1Address common.Address, cursor string) (
return nil, err return nil, err
} }
if err := json.Unmarshal(resp, &dResponse); err != nil { if err := json.Unmarshal(resp, &response); err != nil {
return nil, err return nil, err
} }
return dResponse, nil return &response, nil
} }
// GetAllDepositsByAddress ... Gets all deposits provided a L1 address // GetAllDepositsByAddress ... Gets all deposits provided a L1 address
......
...@@ -3,6 +3,7 @@ package database ...@@ -3,6 +3,7 @@ package database
import ( import (
"errors" "errors"
"fmt" "fmt"
"strings"
"gorm.io/gorm" "gorm.io/gorm"
"gorm.io/gorm/clause" "gorm.io/gorm/clause"
...@@ -61,12 +62,12 @@ type L2BridgeWithdrawalWithTransactionHashes struct { ...@@ -61,12 +62,12 @@ type L2BridgeWithdrawalWithTransactionHashes struct {
type BridgeTransfersView interface { type BridgeTransfersView interface {
L1BridgeDeposit(common.Hash) (*L1BridgeDeposit, error) L1BridgeDeposit(common.Hash) (*L1BridgeDeposit, error)
L1BridgeDepositSum() (float64, error) L1TxDepositSum() (float64, error)
L1BridgeDepositWithFilter(BridgeTransfer) (*L1BridgeDeposit, error) L1BridgeDepositWithFilter(BridgeTransfer) (*L1BridgeDeposit, error)
L1BridgeDepositsByAddress(common.Address, string, int) (*L1BridgeDepositsResponse, error) L1BridgeDepositsByAddress(common.Address, string, int) (*L1BridgeDepositsResponse, error)
L2BridgeWithdrawal(common.Hash) (*L2BridgeWithdrawal, error) L2BridgeWithdrawal(common.Hash) (*L2BridgeWithdrawal, error)
L2BridgeWithdrawalSum() (float64, error) L2BridgeWithdrawalSum(filter WithdrawFilter) (float64, error)
L2BridgeWithdrawalWithFilter(BridgeTransfer) (*L2BridgeWithdrawal, error) L2BridgeWithdrawalWithFilter(BridgeTransfer) (*L2BridgeWithdrawal, error)
L2BridgeWithdrawalsByAddress(common.Address, string, int) (*L2BridgeWithdrawalsResponse, error) L2BridgeWithdrawalsByAddress(common.Address, string, int) (*L2BridgeWithdrawalsResponse, error)
} }
...@@ -138,10 +139,10 @@ type L1BridgeDepositsResponse struct { ...@@ -138,10 +139,10 @@ type L1BridgeDepositsResponse struct {
HasNextPage bool HasNextPage bool
} }
// L1BridgeDepositSum ... returns the sum of all l1 bridge deposit mints in gwei // L1TxDepositSum ... returns the sum of all l1 tx deposit mints in gwei
func (db *bridgeTransfersDB) L1BridgeDepositSum() (float64, error) { func (db *bridgeTransfersDB) L1TxDepositSum() (float64, error) {
var sum float64 var sum float64
result := db.gorm.Model(&L1TransactionDeposit{}).Select("sum(amount)").Scan(&sum) result := db.gorm.Model(&L1TransactionDeposit{}).Select("SUM(amount)").Scan(&sum)
if result.Error != nil { if result.Error != nil {
return 0, result.Error return 0, result.Error
} }
...@@ -246,14 +247,44 @@ func (db *bridgeTransfersDB) L2BridgeWithdrawal(txWithdrawalHash common.Hash) (* ...@@ -246,14 +247,44 @@ func (db *bridgeTransfersDB) L2BridgeWithdrawal(txWithdrawalHash common.Hash) (*
return &withdrawal, nil return &withdrawal, nil
} }
func (db *bridgeTransfersDB) L2BridgeWithdrawalSum() (float64, error) { type WithdrawFilter uint8
const (
All WithdrawFilter = iota // Same as "initialized"
Proven
Finalized
)
func (db *bridgeTransfersDB) L2BridgeWithdrawalSum(filter WithdrawFilter) (float64, error) {
// Determine where filter
var clause string
switch filter {
case All:
clause = ""
case Finalized:
clause = "finalized_l1_event_guid IS NOT NULL"
case Proven:
clause = "proven_l1_event_guid IS NOT NULL"
default:
return 0, fmt.Errorf("unknown filter argument: %d", filter)
}
// NOTE - Scanning to float64 reduces precision versus scanning to big.Int since amount is a uint256
// This is ok though given all bridges will never exceed max float64 (10^308 || 1.7E+308) in wei value locked
// since that would require 10^308 / 10^18 = 10^290 ETH locked in the bridge
var sum float64 var sum float64
result := db.gorm.Model(&L2TransactionWithdrawal{}).Select("sum(amount)").Scan(&sum) result := db.gorm.Model(&L2TransactionWithdrawal{}).Where(clause).Select("SUM(amount)").Scan(&sum)
if result.Error != nil { if result.Error != nil && strings.Contains(result.Error.Error(), "converting NULL to float64 is unsupported") {
// no rows found
return 0, nil
} else if result.Error != nil {
return 0, result.Error return 0, result.Error
} else {
return sum, nil
} }
return sum, nil
} }
// L2BridgeWithdrawalWithFilter queries for a bridge withdrawal with set fields in the `BridgeTransfer` filter // L2BridgeWithdrawalWithFilter queries for a bridge withdrawal with set fields in the `BridgeTransfer` filter
......
...@@ -460,33 +460,45 @@ func TestClientBridgeFunctions(t *testing.T) { ...@@ -460,33 +460,45 @@ func TestClientBridgeFunctions(t *testing.T) {
// (2) Create test actors that will deposit and withdraw using the standard bridge // (2) Create test actors that will deposit and withdraw using the standard bridge
aliceAddr := testSuite.OpCfg.Secrets.Addresses().Alice aliceAddr := testSuite.OpCfg.Secrets.Addresses().Alice
bobAddr := testSuite.OpCfg.Secrets.Addresses().Bob bobAddr := testSuite.OpCfg.Secrets.Addresses().Bob
malAddr := testSuite.OpCfg.Secrets.Addresses().Mallory
type actor struct { type actor struct {
addr common.Address addr common.Address
priv *ecdsa.PrivateKey priv *ecdsa.PrivateKey
amt *big.Int
receipt *types.Receipt
} }
mintSum := bigint.Zero mintSum := bigint.Zero
withdrawSum := bigint.Zero
actors := []actor{ actors := []actor{
{ {
addr: aliceAddr, addr: aliceAddr,
priv: testSuite.OpCfg.Secrets.Alice, priv: testSuite.OpCfg.Secrets.Alice,
amt: big.NewInt(0),
}, },
{ {
addr: bobAddr, addr: bobAddr,
priv: testSuite.OpCfg.Secrets.Bob, priv: testSuite.OpCfg.Secrets.Bob,
amt: big.NewInt(0),
}, },
{ }
addr: malAddr,
priv: testSuite.OpCfg.Secrets.Mallory, type supplies struct {
}, all *big.Int
proven *big.Int
finalized *big.Int
}
s := supplies{
all: big.NewInt(0),
proven: big.NewInt(0),
finalized: big.NewInt(0),
} }
// (3) Iterate over each actor and deposit / withdraw // (3) Iterate over each actor and deposit / withdraw
for _, actor := range actors { for i, actor := range actors {
t.Logf("%d - simulating deposit/withdrawal flow for %s", i, actor.addr.String())
l2Opts, err := bind.NewKeyedTransactorWithChainID(actor.priv, testSuite.OpCfg.L2ChainIDBig()) l2Opts, err := bind.NewKeyedTransactorWithChainID(actor.priv, testSuite.OpCfg.L2ChainIDBig())
require.NoError(t, err) require.NoError(t, err)
l2Opts.Value = big.NewInt(params.Ether) l2Opts.Value = big.NewInt(params.Ether)
...@@ -517,7 +529,9 @@ func TestClientBridgeFunctions(t *testing.T) { ...@@ -517,7 +529,9 @@ func TestClientBridgeFunctions(t *testing.T) {
return seenL1 && seenL2, nil return seenL1 && seenL2, nil
})) }))
withdrawSum = new(big.Int).Add(withdrawSum, l2ToL1MessagePasserWithdrawTx.Value()) s.all = new(big.Int).Add(s.all, l2ToL1MessagePasserWithdrawTx.Value())
actors[i].receipt = l2ToL1WithdrawReceipt
actors[i].amt = l2ToL1MessagePasserWithdrawTx.Value()
// (3.d) Ensure that withdrawal and deposit txs are retrievable via API // (3.d) Ensure that withdrawal and deposit txs are retrievable via API
deposits, err := testSuite.Client.GetAllDepositsByAddress(actor.addr) deposits, err := testSuite.Client.GetAllDepositsByAddress(actor.addr)
...@@ -539,7 +553,42 @@ func TestClientBridgeFunctions(t *testing.T) { ...@@ -539,7 +553,42 @@ func TestClientBridgeFunctions(t *testing.T) {
mintFloat, _ := mintSum.Float64() mintFloat, _ := mintSum.Float64()
require.Equal(t, mintFloat, assessment.L1DepositSum) require.Equal(t, mintFloat, assessment.L1DepositSum)
withdrawFloat, _ := withdrawSum.Float64() withdrawFloat, _ := s.all.Float64()
require.Equal(t, withdrawFloat, assessment.L2WithdrawalSum) require.Equal(t, withdrawFloat, assessment.InitWithdrawalSum)
require.Equal(t, assessment.ProvenWithdrawSum, float64(0))
require.Equal(t, assessment.FinalizedWithdrawSum, float64(0))
// (5) Prove & finalize withdrawals on L1
for _, actor := range actors {
params, proveReceipt := op_e2e.ProveWithdrawal(t, *testSuite.OpCfg, testSuite.L1Client, testSuite.OpSys.EthInstances["sequencer"], actor.priv, actor.receipt)
require.NoError(t, wait.For(context.Background(), 500*time.Millisecond, func() (bool, error) {
l1Header := testSuite.Indexer.BridgeProcessor.LastL1Header
seen := l1Header != nil && l1Header.Number.Uint64() >= proveReceipt.BlockNumber.Uint64()
return seen, nil
}))
s.proven = new(big.Int).Add(s.proven, actor.amt)
finalReceipt := op_e2e.FinalizeWithdrawal(t, *testSuite.OpCfg, testSuite.L1Client, actor.priv, proveReceipt, params)
require.NoError(t, wait.For(context.Background(), 500*time.Millisecond, func() (bool, error) {
l1Header := testSuite.Indexer.BridgeProcessor.LastFinalizedL1Header
seen := l1Header != nil && l1Header.Number.Uint64() >= finalReceipt.BlockNumber.Uint64()
return seen, nil
}))
s.finalized = new(big.Int).Add(s.finalized, actor.amt)
}
// (6) Validate assessment for proven & finalized withdrawals
assessment, err = testSuite.Client.GetSupplyAssessment()
require.NoError(t, err)
proven, acc := s.proven.Float64()
require.Zero(t, acc)
require.Equal(t, proven, assessment.ProvenWithdrawSum)
finalized, acc := s.finalized.Float64()
require.Zero(t, acc)
require.Equal(t, finalized, assessment.FinalizedWithdrawSum)
} }
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