Commit a5436689 authored by Ethen Pociask's avatar Ethen Pociask

[indexer.client] E2E tests and better documentation

parent a4f00f61
...@@ -20,9 +20,11 @@ import ( ...@@ -20,9 +20,11 @@ import (
const ethereumAddressRegex = `^0x[a-fA-F0-9]{40}$` const ethereumAddressRegex = `^0x[a-fA-F0-9]{40}$`
// Api ... Indexer API struct
// TODO : Structured error responses
type Api struct { type Api struct {
log log.Logger log log.Logger
Router *chi.Mux router *chi.Mux
serverConfig config.ServerConfig serverConfig config.ServerConfig
metricsConfig config.ServerConfig metricsConfig config.ServerConfig
metricsRegistry *prometheus.Registry metricsRegistry *prometheus.Registry
...@@ -31,37 +33,48 @@ type Api struct { ...@@ -31,37 +33,48 @@ type Api struct {
const ( const (
MetricsNamespace = "op_indexer" MetricsNamespace = "op_indexer"
addressParam = "{address:%s}" addressParam = "{address:%s}"
DepositsPath = "/api/v0/deposits/"
WithdrawalsPath = "/api/v0/withdrawals/" // Endpoint paths
HealthPath = "/healthz"
DepositsPath = "/api/v0/deposits/"
WithdrawalsPath = "/api/v0/withdrawals/"
) )
// chiMetricsMiddleware ... Injects a metrics recorder into request processing middleware
func chiMetricsMiddleware(rec metrics.HTTPRecorder) func(http.Handler) http.Handler { func chiMetricsMiddleware(rec metrics.HTTPRecorder) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler {
return metrics.NewHTTPRecordingMiddleware(rec, next) return metrics.NewHTTPRecordingMiddleware(rec, next)
} }
} }
// NewApi ... Construct a new api instance
func NewApi(logger log.Logger, bv database.BridgeTransfersView, serverConfig config.ServerConfig, metricsConfig config.ServerConfig) *Api { func NewApi(logger log.Logger, bv database.BridgeTransfersView, serverConfig config.ServerConfig, metricsConfig config.ServerConfig) *Api {
// (1) Initialize dependencies
apiRouter := chi.NewRouter() apiRouter := chi.NewRouter()
h := routes.NewRoutes(logger, bv, apiRouter) h := routes.NewRoutes(logger, bv, apiRouter)
mr := metrics.NewRegistry() mr := metrics.NewRegistry()
promRecorder := metrics.NewPromHTTPRecorder(mr, MetricsNamespace) promRecorder := metrics.NewPromHTTPRecorder(mr, MetricsNamespace)
// (2) Inject routing middleware
apiRouter.Use(chiMetricsMiddleware(promRecorder)) apiRouter.Use(chiMetricsMiddleware(promRecorder))
apiRouter.Use(middleware.Recoverer) apiRouter.Use(middleware.Recoverer)
apiRouter.Use(middleware.Heartbeat("/healthz")) apiRouter.Use(middleware.Heartbeat(HealthPath))
// (3) Set GET routes
apiRouter.Get(fmt.Sprintf(DepositsPath+addressParam, ethereumAddressRegex), h.L1DepositsHandler) apiRouter.Get(fmt.Sprintf(DepositsPath+addressParam, ethereumAddressRegex), h.L1DepositsHandler)
apiRouter.Get(fmt.Sprintf(WithdrawalsPath+addressParam, ethereumAddressRegex), h.L2WithdrawalsHandler) apiRouter.Get(fmt.Sprintf(WithdrawalsPath+addressParam, ethereumAddressRegex), h.L2WithdrawalsHandler)
return &Api{log: logger, Router: apiRouter, metricsRegistry: mr, serverConfig: serverConfig, metricsConfig: metricsConfig} return &Api{log: logger, router: apiRouter, metricsRegistry: mr, serverConfig: serverConfig, metricsConfig: metricsConfig}
} }
// Start ... Starts the API server routines
func (a *Api) Start(ctx context.Context) error { func (a *Api) Start(ctx context.Context) error {
var wg sync.WaitGroup var wg sync.WaitGroup
errCh := make(chan error, 2) errCh := make(chan error, 2)
// (1) Construct an inner function that will start a goroutine
// and handle any panics that occur on a shared error channel
processCtx, processCancel := context.WithCancel(ctx) processCtx, processCancel := context.WithCancel(ctx)
runProcess := func(start func(ctx context.Context) error) { runProcess := func(start func(ctx context.Context) error) {
wg.Add(1) wg.Add(1)
...@@ -81,9 +94,11 @@ func (a *Api) Start(ctx context.Context) error { ...@@ -81,9 +94,11 @@ func (a *Api) Start(ctx context.Context) error {
}() }()
} }
// (2) Start the API and metrics servers
runProcess(a.startServer) runProcess(a.startServer)
runProcess(a.startMetricsServer) runProcess(a.startMetricsServer)
// (3) Wait for all processes to complete
wg.Wait() wg.Wait()
err := <-errCh err := <-errCh
...@@ -96,9 +111,10 @@ func (a *Api) Start(ctx context.Context) error { ...@@ -96,9 +111,10 @@ func (a *Api) Start(ctx context.Context) error {
return err return err
} }
// startServer ... Starts the API server
func (a *Api) startServer(ctx context.Context) error { func (a *Api) startServer(ctx context.Context) error {
a.log.Info("api server listening...", "port", a.serverConfig.Port) a.log.Info("api server listening...", "port", a.serverConfig.Port)
server := http.Server{Addr: fmt.Sprintf(":%d", a.serverConfig.Port), Handler: a.Router} server := http.Server{Addr: fmt.Sprintf(":%d", a.serverConfig.Port), Handler: a.router}
err := httputil.ListenAndServeContext(ctx, &server) err := httputil.ListenAndServeContext(ctx, &server)
if err != nil { if err != nil {
a.log.Error("api server stopped", "err", err) a.log.Error("api server stopped", "err", err)
...@@ -108,6 +124,7 @@ func (a *Api) startServer(ctx context.Context) error { ...@@ -108,6 +124,7 @@ func (a *Api) startServer(ctx context.Context) error {
return err return err
} }
// startMetricsServer ... Starts the metrics server
func (a *Api) startMetricsServer(ctx context.Context) error { func (a *Api) startMetricsServer(ctx context.Context) error {
a.log.Info("starting metrics server...", "port", a.metricsConfig.Port) a.log.Info("starting metrics server...", "port", a.metricsConfig.Port)
err := metrics.ListenAndServe(ctx, a.metricsRegistry, a.metricsConfig.Host, a.metricsConfig.Port) err := metrics.ListenAndServe(ctx, a.metricsRegistry, a.metricsConfig.Host, a.metricsConfig.Port)
......
...@@ -100,7 +100,7 @@ func TestHealthz(t *testing.T) { ...@@ -100,7 +100,7 @@ func TestHealthz(t *testing.T) {
assert.Nil(t, err) assert.Nil(t, err)
responseRecorder := httptest.NewRecorder() responseRecorder := httptest.NewRecorder()
api.Router.ServeHTTP(responseRecorder, request) api.router.ServeHTTP(responseRecorder, request)
assert.Equal(t, http.StatusOK, responseRecorder.Code) assert.Equal(t, http.StatusOK, responseRecorder.Code)
} }
...@@ -112,7 +112,7 @@ func TestL1BridgeDepositsHandler(t *testing.T) { ...@@ -112,7 +112,7 @@ func TestL1BridgeDepositsHandler(t *testing.T) {
assert.Nil(t, err) assert.Nil(t, err)
responseRecorder := httptest.NewRecorder() responseRecorder := httptest.NewRecorder()
api.Router.ServeHTTP(responseRecorder, request) api.router.ServeHTTP(responseRecorder, request)
assert.Equal(t, http.StatusOK, responseRecorder.Code) assert.Equal(t, http.StatusOK, responseRecorder.Code)
...@@ -135,7 +135,7 @@ func TestL2BridgeWithdrawalsByAddressHandler(t *testing.T) { ...@@ -135,7 +135,7 @@ func TestL2BridgeWithdrawalsByAddressHandler(t *testing.T) {
assert.Nil(t, err) assert.Nil(t, err)
responseRecorder := httptest.NewRecorder() responseRecorder := httptest.NewRecorder()
api.Router.ServeHTTP(responseRecorder, request) api.router.ServeHTTP(responseRecorder, request)
var resp routes.WithdrawalResponse var resp routes.WithdrawalResponse
err = json.Unmarshal(responseRecorder.Body.Bytes(), &resp) err = json.Unmarshal(responseRecorder.Body.Bytes(), &resp)
......
...@@ -8,22 +8,18 @@ import ( ...@@ -8,22 +8,18 @@ import (
) )
const ( const (
InternalServerError = "Internal server error"
// defaultPageLimit ... Default page limit for pagination
defaultPageLimit = 100 defaultPageLimit = 100
) )
// // errorToJson ... Converts an error to a JSON map
// func errorToJson(err error) map[string]interface{} {
// return map[string]interface{}{
// "error": err.Error(),
// }
// }
// jsonResponse ... Marshals and writes a JSON response provided arbitrary data // jsonResponse ... Marshals and writes a JSON response provided arbitrary data
func jsonResponse(w http.ResponseWriter, logger log.Logger, data interface{}, statusCode int) { func jsonResponse(w http.ResponseWriter, logger log.Logger, data interface{}, statusCode int) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
jsonData, err := json.Marshal(data) jsonData, err := json.Marshal(data)
if err != nil { if err != nil {
http.Error(w, "Internal server error", http.StatusInternalServerError) http.Error(w, InternalServerError, http.StatusInternalServerError)
logger.Error("Failed to marshal JSON: %v", err) logger.Error("Failed to marshal JSON: %v", err)
return return
} }
...@@ -31,7 +27,7 @@ func jsonResponse(w http.ResponseWriter, logger log.Logger, data interface{}, st ...@@ -31,7 +27,7 @@ func jsonResponse(w http.ResponseWriter, logger log.Logger, data interface{}, st
w.WriteHeader(statusCode) w.WriteHeader(statusCode)
_, err = w.Write(jsonData) _, err = w.Write(jsonData)
if err != nil { if err != nil {
http.Error(w, "Internal server error", http.StatusInternalServerError) http.Error(w, InternalServerError, http.StatusInternalServerError)
logger.Error("Failed to write JSON data", err) logger.Error("Failed to write JSON data", err)
return return
} }
......
...@@ -8,6 +8,7 @@ import ( ...@@ -8,6 +8,7 @@ import (
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
) )
// DepositItem ... Deposit item model for API responses
type DepositItem struct { type DepositItem struct {
Guid string `json:"guid"` Guid string `json:"guid"`
From string `json:"from"` From string `json:"from"`
...@@ -27,6 +28,7 @@ type DepositResponse struct { ...@@ -27,6 +28,7 @@ type DepositResponse struct {
Items []DepositItem `json:"items"` Items []DepositItem `json:"items"`
} }
// newDepositResponse ... Converts a database.L1BridgeDepositsResponse to an api.DepositResponse
func newDepositResponse(deposits *database.L1BridgeDepositsResponse) DepositResponse { func newDepositResponse(deposits *database.L1BridgeDepositsResponse) DepositResponse {
items := make([]DepositItem, len(deposits.Deposits)) items := make([]DepositItem, len(deposits.Deposits))
for i, deposit := range deposits.Deposits { for i, deposit := range deposits.Deposits {
...@@ -52,6 +54,7 @@ func newDepositResponse(deposits *database.L1BridgeDepositsResponse) DepositResp ...@@ -52,6 +54,7 @@ func newDepositResponse(deposits *database.L1BridgeDepositsResponse) DepositResp
} }
} }
// 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) {
address := common.HexToAddress(chi.URLParam(r, "address")) address := common.HexToAddress(chi.URLParam(r, "address"))
cursor := r.URL.Query().Get("cursor") cursor := r.URL.Query().Get("cursor")
...@@ -60,7 +63,7 @@ func (h Routes) L1DepositsHandler(w http.ResponseWriter, r *http.Request) { ...@@ -60,7 +63,7 @@ func (h Routes) L1DepositsHandler(w http.ResponseWriter, r *http.Request) {
limit, err := h.v.ValidateLimit(limitQuery) limit, err := h.v.ValidateLimit(limitQuery)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest) http.Error(w, err.Error(), http.StatusBadRequest)
h.Logger.Error("Invalid limit param") h.Logger.Error("Invalid limit param", "param", limitQuery)
h.Logger.Error(err.Error()) h.Logger.Error(err.Error())
return return
} }
......
...@@ -6,6 +6,7 @@ import ( ...@@ -6,6 +6,7 @@ import (
"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
BridgeTransfersView database.BridgeTransfersView BridgeTransfersView database.BridgeTransfersView
...@@ -13,6 +14,7 @@ type Routes struct { ...@@ -13,6 +14,7 @@ type Routes struct {
v *Validator v *Validator
} }
// NewRoutes ... Construct a new route handler instance
func NewRoutes(logger log.Logger, bv database.BridgeTransfersView, r *chi.Mux) Routes { func NewRoutes(logger log.Logger, bv database.BridgeTransfersView, r *chi.Mux) Routes {
return Routes{ return Routes{
Logger: logger, Logger: logger,
......
package routes
import (
"testing"
"github.com/stretchr/testify/require"
)
func Test_ValidateLimit(t *testing.T) {
validator := Validator{}
// (1)
limit := "100"
_, err := validator.ValidateLimit(limit)
require.NoError(t, err, "limit should be valid")
// (2)
limit = "0"
_, err = validator.ValidateLimit(limit)
require.Error(t, err, "limit must be greater than 0")
// (3)
limit = "abc"
_, err = validator.ValidateLimit(limit)
require.Error(t, err, "limit must be an integer value")
}
...@@ -54,6 +54,7 @@ func newWithdrawalResponse(withdrawals *database.L2BridgeWithdrawalsResponse) Wi ...@@ -54,6 +54,7 @@ func newWithdrawalResponse(withdrawals *database.L2BridgeWithdrawalsResponse) Wi
} }
} }
// 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) {
address := common.HexToAddress(chi.URLParam(r, "address")) address := common.HexToAddress(chi.URLParam(r, "address"))
cursor := r.URL.Query().Get("cursor") cursor := r.URL.Query().Get("cursor")
......
...@@ -10,6 +10,7 @@ import ( ...@@ -10,6 +10,7 @@ import (
"github.com/ethereum-optimism/optimism/indexer/api" "github.com/ethereum-optimism/optimism/indexer/api"
"github.com/ethereum-optimism/optimism/indexer/database" "github.com/ethereum-optimism/optimism/indexer/database"
"github.com/ethereum-optimism/optimism/indexer/node" "github.com/ethereum-optimism/optimism/indexer/node"
"github.com/ethereum/go-ethereum/common"
) )
const ( const (
...@@ -24,30 +25,97 @@ type Config struct { ...@@ -24,30 +25,97 @@ type Config struct {
URL string URL string
} }
// IndexerClient ... Indexer client struct // Client ... Indexer client struct
type IndexerClient struct { // TODO: Add metrics
// TODO: Add injectable context support
type Client struct {
cfg *Config cfg *Config
c *http.Client c *http.Client
m node.Metricer m node.Metricer
} }
// NewClient ... Construct a new indexer client // NewClient ... Construct a new indexer client
func NewClient(cfg *Config, m node.Metricer) (*IndexerClient, error) { func NewClient(cfg *Config, m node.Metricer) (*Client, error) {
if cfg.PaginationLimit == 0 { if cfg.PaginationLimit <= 0 {
cfg.PaginationLimit = defaultPagingLimit cfg.PaginationLimit = defaultPagingLimit
} }
c := &http.Client{} c := &http.Client{}
return &IndexerClient{cfg: cfg, c: c, m: m}, nil return &Client{cfg: cfg, c: c, m: m}, nil
} }
// GetAllWithdrawalsByAddress ... Gets all withdrawals by address // HealthCheck ... Checks the health of the indexer
func (ic *IndexerClient) GetAllWithdrawalsByAddress(l2Address string) ([]database.L2BridgeWithdrawalWithTransactionHashes, error) { func (c *Client) HealthCheck() error {
resp, err := c.c.Get(c.cfg.URL + api.HealthPath)
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("health check failed with status code %d", resp.StatusCode)
}
return nil
}
// GetDepositsByAddress ... Gets a deposit response object provided an L1 address and cursor
func (c *Client) GetDepositsByAddress(l1Address common.Address, cursor string) (*database.L1BridgeDepositsResponse, error) {
var dResponse *database.L1BridgeDepositsResponse
url := c.cfg.URL + api.DepositsPath + l1Address.String() + urlParams
endpoint := fmt.Sprintf(url, cursor, c.cfg.PaginationLimit)
resp, err := c.c.Get(endpoint)
if err != nil {
return nil, err
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err)
}
defer func() {
_ = resp.Body.Close()
}()
if err := json.Unmarshal(body, &dResponse); err != nil {
return nil, err
}
return dResponse, nil
}
// GetAllDepositsByAddress ... Gets all deposits provided a L1 address
func (c *Client) GetAllDepositsByAddress(l1Address common.Address) ([]database.L1BridgeDepositWithTransactionHashes, error) {
var deposits []database.L1BridgeDepositWithTransactionHashes
cursor := ""
for {
dResponse, err := c.GetDepositsByAddress(l1Address, cursor)
if err != nil {
return nil, err
}
deposits = append(deposits, dResponse.Deposits...)
if !dResponse.HasNextPage {
break
}
cursor = dResponse.Cursor
}
return deposits, nil
}
// GetAllWithdrawalsByAddress ... Gets all withdrawals provided a L2 address
func (c *Client) GetAllWithdrawalsByAddress(l2Address common.Address) ([]database.L2BridgeWithdrawalWithTransactionHashes, error) {
var withdrawals []database.L2BridgeWithdrawalWithTransactionHashes var withdrawals []database.L2BridgeWithdrawalWithTransactionHashes
cursor := "" cursor := ""
for { for {
wResponse, err := ic.GetWithdrawalsByAddress(l2Address, cursor) wResponse, err := c.GetWithdrawalsByAddress(l2Address, cursor)
if err != nil { if err != nil {
return nil, err return nil, err
} }
...@@ -64,12 +132,13 @@ func (ic *IndexerClient) GetAllWithdrawalsByAddress(l2Address string) ([]databas ...@@ -64,12 +132,13 @@ func (ic *IndexerClient) GetAllWithdrawalsByAddress(l2Address string) ([]databas
return withdrawals, nil return withdrawals, nil
} }
// GetWithdrawalsByAddress ... Gets a withdrawal response object provided an L2 address // GetWithdrawalsByAddress ... Gets a withdrawal response object provided an L2 address and cursor
func (ic *IndexerClient) GetWithdrawalsByAddress(l2Address string, cursor string) (*database.L2BridgeWithdrawalsResponse, error) { func (c *Client) GetWithdrawalsByAddress(l2Address common.Address, cursor string) (*database.L2BridgeWithdrawalsResponse, error) {
var wResponse *database.L2BridgeWithdrawalsResponse var wResponse *database.L2BridgeWithdrawalsResponse
url := c.cfg.URL + api.WithdrawalsPath + l2Address.String() + urlParams
endpoint := fmt.Sprintf(ic.cfg.URL+api.WithdrawalsPath+l2Address+urlParams, cursor, ic.cfg.PaginationLimit) endpoint := fmt.Sprintf(url, cursor, c.cfg.PaginationLimit)
resp, err := ic.c.Get(endpoint) resp, err := c.c.Get(endpoint)
if err != nil { if err != nil {
return nil, err return nil, err
} }
......
...@@ -131,9 +131,8 @@ type L1BridgeDepositsResponse struct { ...@@ -131,9 +131,8 @@ type L1BridgeDepositsResponse struct {
// L1BridgeDepositsByAddress retrieves a list of deposits initiated by the specified address, // L1BridgeDepositsByAddress retrieves a list of deposits initiated by the specified address,
// coupled with the L1/L2 transaction hashes that complete the bridge transaction. // coupled with the L1/L2 transaction hashes that complete the bridge transaction.
func (db *bridgeTransfersDB) L1BridgeDepositsByAddress(address common.Address, cursor string, limit int) (*L1BridgeDepositsResponse, error) { func (db *bridgeTransfersDB) L1BridgeDepositsByAddress(address common.Address, cursor string, limit int) (*L1BridgeDepositsResponse, error) {
defaultLimit := 100
if limit <= 0 { if limit <= 0 {
limit = defaultLimit return nil, fmt.Errorf("limit must be greater than 0")
} }
cursorClause := "" cursorClause := ""
...@@ -245,7 +244,11 @@ type L2BridgeWithdrawalsResponse struct { ...@@ -245,7 +244,11 @@ type L2BridgeWithdrawalsResponse struct {
// L2BridgeDepositsByAddress retrieves a list of deposits initiated by the specified address, coupled with the L1/L2 transaction hashes // L2BridgeDepositsByAddress retrieves a list of deposits initiated by the specified address, coupled with the L1/L2 transaction hashes
// that complete the bridge transaction. The hashes that correspond with the Bedrock multi-step withdrawal process are also surfaced // that complete the bridge transaction. The hashes that correspond with the Bedrock multi-step withdrawal process are also surfaced
func (db *bridgeTransfersDB) L2BridgeWithdrawalsByAddress(address common.Address, cursor string, limit int) (*L2BridgeWithdrawalsResponse, error) { func (db *bridgeTransfersDB) L2BridgeWithdrawalsByAddress(address common.Address, cursor string, limit int) (*L2BridgeWithdrawalsResponse, error) {
if limit <= 0 {
return nil, fmt.Errorf("limit must be greater than 0")
}
// (1) Generate cursor clause provided a cursor tx hash
cursorClause := "" cursorClause := ""
if cursor != "" { if cursor != "" {
withdrawalHash := common.HexToHash(cursor) withdrawalHash := common.HexToHash(cursor)
...@@ -257,6 +260,11 @@ func (db *bridgeTransfersDB) L2BridgeWithdrawalsByAddress(address common.Address ...@@ -257,6 +260,11 @@ func (db *bridgeTransfersDB) L2BridgeWithdrawalsByAddress(address common.Address
cursorClause = fmt.Sprintf("l2_transaction_withdrawals.timestamp <= %d", txWithdrawal.Tx.Timestamp) cursorClause = fmt.Sprintf("l2_transaction_withdrawals.timestamp <= %d", txWithdrawal.Tx.Timestamp)
} }
// (2) Generate query for fetching ETH withdrawal data
// This query is a UNION (A | B) of two sub-queries:
// - (A) ETH sends from L2 to L1
// - (B) Bridge withdrawals from L2 to L1
// TODO join with l1_bridged_tokens and l2_bridged_tokens // TODO join with l1_bridged_tokens and l2_bridged_tokens
ethAddressString := predeploys.LegacyERC20ETHAddr.String() ethAddressString := predeploys.LegacyERC20ETHAddr.String()
...@@ -275,13 +283,8 @@ l2_transaction_withdrawals.timestamp, NULL AS cross_domain_message_hash, ? AS lo ...@@ -275,13 +283,8 @@ l2_transaction_withdrawals.timestamp, NULL AS cross_domain_message_hash, ? AS lo
ethTransactionWithdrawals = ethTransactionWithdrawals.Where(cursorClause) ethTransactionWithdrawals = ethTransactionWithdrawals.Where(cursorClause)
} }
ethTransactionWithdrawals.DryRun = true withdrawalsQuery := db.gorm.Model(&L2BridgeWithdrawal{})
ethTransactionWithdrawals.Find(&[]L2BridgeWithdrawalWithTransactionHashes{}) withdrawalsQuery = withdrawalsQuery.Where(&Transaction{FromAddress: address})
x := ethTransactionWithdrawals.Statement.SQL.String()
ethTransactionWithdrawals.DryRun = false
println(x)
withdrawalsQuery := db.gorm.Model(&L2BridgeWithdrawal{}).Where(&Transaction{FromAddress: address})
withdrawalsQuery = withdrawalsQuery.Joins("INNER JOIN l2_transaction_withdrawals ON withdrawal_hash = l2_bridge_withdrawals.transaction_withdrawal_hash") withdrawalsQuery = withdrawalsQuery.Joins("INNER JOIN l2_transaction_withdrawals ON withdrawal_hash = l2_bridge_withdrawals.transaction_withdrawal_hash")
withdrawalsQuery = withdrawalsQuery.Joins("INNER JOIN l2_contract_events ON l2_contract_events.guid = l2_transaction_withdrawals.initiated_l2_event_guid") withdrawalsQuery = withdrawalsQuery.Joins("INNER JOIN l2_contract_events ON l2_contract_events.guid = l2_transaction_withdrawals.initiated_l2_event_guid")
withdrawalsQuery = withdrawalsQuery.Joins("LEFT JOIN l1_contract_events AS proven_l1_events ON proven_l1_events.guid = l2_transaction_withdrawals.proven_l1_event_guid") withdrawalsQuery = withdrawalsQuery.Joins("LEFT JOIN l1_contract_events AS proven_l1_events ON proven_l1_events.guid = l2_transaction_withdrawals.proven_l1_event_guid")
...@@ -295,17 +298,12 @@ l2_bridge_withdrawals.timestamp, cross_domain_message_hash, local_token_address, ...@@ -295,17 +298,12 @@ l2_bridge_withdrawals.timestamp, cross_domain_message_hash, local_token_address,
withdrawalsQuery = withdrawalsQuery.Where(cursorClause) withdrawalsQuery = withdrawalsQuery.Where(cursorClause)
} }
withdrawalsQuery.DryRun = true
withdrawalsQuery.Find(&[]L2BridgeWithdrawalWithTransactionHashes{})
x = withdrawalsQuery.Statement.SQL.String()
withdrawalsQuery.DryRun = false
println(x)
query := db.gorm.Table("(?) AS withdrawals", withdrawalsQuery) query := db.gorm.Table("(?) AS withdrawals", withdrawalsQuery)
query = query.Joins("UNION (?)", ethTransactionWithdrawals) query = query.Joins("UNION (?)", ethTransactionWithdrawals)
query = query.Select("*").Order("timestamp DESC").Limit(limit + 1) query = query.Select("*").Order("timestamp DESC").Limit(limit + 1)
withdrawals := []L2BridgeWithdrawalWithTransactionHashes{} withdrawals := []L2BridgeWithdrawalWithTransactionHashes{}
// (3) Execute query and process results
result := query.Find(&withdrawals) result := query.Find(&withdrawals)
if result.Error != nil { if result.Error != nil {
......
...@@ -174,10 +174,4 @@ func TestE2EBridgeL2CrossDomainMessenger(t *testing.T) { ...@@ -174,10 +174,4 @@ func TestE2EBridgeL2CrossDomainMessenger(t *testing.T) {
require.NotNil(t, event) require.NotNil(t, event)
require.Equal(t, event.TransactionHash, finalizedReceipt.TxHash) require.Equal(t, event.TransactionHash, finalizedReceipt.TxHash)
// (3) Validate that withdrawal is extractable via API
withdrawalsResponse, err := testSuite.IClient.GetWithdrawalsByAddress(aliceAddr.String(), "")
require.NoError(t, err)
require.Equal(t, 1, len(withdrawalsResponse.Withdrawals))
} }
...@@ -2,6 +2,7 @@ package e2e_tests ...@@ -2,6 +2,7 @@ package e2e_tests
import ( import (
"context" "context"
"crypto/ecdsa"
"fmt" "fmt"
"math/big" "math/big"
"testing" "testing"
...@@ -16,6 +17,7 @@ import ( ...@@ -16,6 +17,7 @@ import (
"github.com/ethereum-optimism/optimism/op-bindings/bindings" "github.com/ethereum-optimism/optimism/op-bindings/bindings"
"github.com/ethereum-optimism/optimism/op-bindings/predeploys" "github.com/ethereum-optimism/optimism/op-bindings/predeploys"
"github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/params"
...@@ -252,7 +254,7 @@ func TestE2EBridgeTransfersStandardBridgeETHWithdrawal(t *testing.T) { ...@@ -252,7 +254,7 @@ func TestE2EBridgeTransfersStandardBridgeETHWithdrawal(t *testing.T) {
return l2Header != nil && l2Header.Number.Uint64() >= withdrawReceipt.BlockNumber.Uint64(), nil return l2Header != nil && l2Header.Number.Uint64() >= withdrawReceipt.BlockNumber.Uint64(), nil
})) }))
aliceWithdrawals, err := testSuite.DB.BridgeTransfers.L2BridgeWithdrawalsByAddress(aliceAddr, "", 0) aliceWithdrawals, err := testSuite.DB.BridgeTransfers.L2BridgeWithdrawalsByAddress(aliceAddr, "", 3)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, aliceWithdrawals.Withdrawals, 1) require.Len(t, aliceWithdrawals.Withdrawals, 1)
require.Equal(t, withdrawTx.Hash().String(), aliceWithdrawals.Withdrawals[0].L2TransactionHash.String()) require.Equal(t, withdrawTx.Hash().String(), aliceWithdrawals.Withdrawals[0].L2TransactionHash.String())
...@@ -290,7 +292,7 @@ func TestE2EBridgeTransfersStandardBridgeETHWithdrawal(t *testing.T) { ...@@ -290,7 +292,7 @@ func TestE2EBridgeTransfersStandardBridgeETHWithdrawal(t *testing.T) {
return l1Header != nil && l1Header.Number.Uint64() >= finalizeReceipt.BlockNumber.Uint64(), nil return l1Header != nil && l1Header.Number.Uint64() >= finalizeReceipt.BlockNumber.Uint64(), nil
})) }))
aliceWithdrawals, err = testSuite.DB.BridgeTransfers.L2BridgeWithdrawalsByAddress(aliceAddr, "", 0) aliceWithdrawals, err = testSuite.DB.BridgeTransfers.L2BridgeWithdrawalsByAddress(aliceAddr, "", 100)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, proveReceipt.TxHash, aliceWithdrawals.Withdrawals[0].ProvenL1TransactionHash) require.Equal(t, proveReceipt.TxHash, aliceWithdrawals.Withdrawals[0].ProvenL1TransactionHash)
require.Equal(t, finalizeReceipt.TxHash, aliceWithdrawals.Withdrawals[0].FinalizedL1TransactionHash) require.Equal(t, finalizeReceipt.TxHash, aliceWithdrawals.Withdrawals[0].FinalizedL1TransactionHash)
...@@ -337,7 +339,7 @@ func TestE2EBridgeTransfersL2ToL1MessagePasserETHReceive(t *testing.T) { ...@@ -337,7 +339,7 @@ func TestE2EBridgeTransfersL2ToL1MessagePasserETHReceive(t *testing.T) {
return l2Header != nil && l2Header.Number.Uint64() >= l2ToL1WithdrawReceipt.BlockNumber.Uint64(), nil return l2Header != nil && l2Header.Number.Uint64() >= l2ToL1WithdrawReceipt.BlockNumber.Uint64(), nil
})) }))
aliceWithdrawals, err := testSuite.DB.BridgeTransfers.L2BridgeWithdrawalsByAddress(aliceAddr, "", 0) aliceWithdrawals, err := testSuite.DB.BridgeTransfers.L2BridgeWithdrawalsByAddress(aliceAddr, "", 100)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, aliceWithdrawals.Withdrawals, 1) require.Len(t, aliceWithdrawals.Withdrawals, 1)
require.Equal(t, l2ToL1MessagePasserWithdrawTx.Hash().String(), aliceWithdrawals.Withdrawals[0].L2TransactionHash.String()) require.Equal(t, l2ToL1MessagePasserWithdrawTx.Hash().String(), aliceWithdrawals.Withdrawals[0].L2TransactionHash.String())
...@@ -370,7 +372,7 @@ func TestE2EBridgeTransfersL2ToL1MessagePasserETHReceive(t *testing.T) { ...@@ -370,7 +372,7 @@ func TestE2EBridgeTransfersL2ToL1MessagePasserETHReceive(t *testing.T) {
return l1Header != nil && l1Header.Number.Uint64() >= finalizeReceipt.BlockNumber.Uint64(), nil return l1Header != nil && l1Header.Number.Uint64() >= finalizeReceipt.BlockNumber.Uint64(), nil
})) }))
aliceWithdrawals, err = testSuite.DB.BridgeTransfers.L2BridgeWithdrawalsByAddress(aliceAddr, "", 0) aliceWithdrawals, err = testSuite.DB.BridgeTransfers.L2BridgeWithdrawalsByAddress(aliceAddr, "", 100)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, proveReceipt.TxHash, aliceWithdrawals.Withdrawals[0].ProvenL1TransactionHash) require.Equal(t, proveReceipt.TxHash, aliceWithdrawals.Withdrawals[0].ProvenL1TransactionHash)
require.Equal(t, finalizeReceipt.TxHash, aliceWithdrawals.Withdrawals[0].FinalizedL1TransactionHash) require.Equal(t, finalizeReceipt.TxHash, aliceWithdrawals.Withdrawals[0].FinalizedL1TransactionHash)
...@@ -414,20 +416,20 @@ func TestE2EBridgeTransfersCursoredWithdrawals(t *testing.T) { ...@@ -414,20 +416,20 @@ func TestE2EBridgeTransfersCursoredWithdrawals(t *testing.T) {
})) }))
// Get All // Get All
aliceWithdrawals, err := testSuite.DB.BridgeTransfers.L2BridgeWithdrawalsByAddress(aliceAddr, "", 0) aliceWithdrawals, err := testSuite.DB.BridgeTransfers.L2BridgeWithdrawalsByAddress(aliceAddr, "", 100)
require.NotNil(t, aliceWithdrawals) require.NotNil(t, aliceWithdrawals)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, aliceWithdrawals.Withdrawals, 3) require.Len(t, aliceWithdrawals.Withdrawals, 3)
require.False(t, aliceWithdrawals.HasNextPage) require.False(t, aliceWithdrawals.HasNextPage)
// Respects Limits & Supplied Cursors // Respects Limits & Supplied Cursors
aliceWithdrawals, err = testSuite.DB.BridgeTransfers.L2BridgeWithdrawalsByAddress(aliceAddr, "", 2) aliceWithdrawals, err = testSuite.DB.BridgeTransfers.L2BridgeWithdrawalsByAddress(aliceAddr, "", 100)
require.NotNil(t, aliceWithdrawals) require.NotNil(t, aliceWithdrawals)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, aliceWithdrawals.Withdrawals, 2) require.Len(t, aliceWithdrawals.Withdrawals, 2)
require.True(t, aliceWithdrawals.HasNextPage) require.True(t, aliceWithdrawals.HasNextPage)
aliceWithdrawals, err = testSuite.DB.BridgeTransfers.L2BridgeWithdrawalsByAddress(aliceAddr, aliceWithdrawals.Cursor, 2) aliceWithdrawals, err = testSuite.DB.BridgeTransfers.L2BridgeWithdrawalsByAddress(aliceAddr, aliceWithdrawals.Cursor, 100)
require.NotNil(t, aliceWithdrawals) require.NotNil(t, aliceWithdrawals)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, aliceWithdrawals.Withdrawals, 1) require.Len(t, aliceWithdrawals.Withdrawals, 1)
...@@ -445,3 +447,76 @@ func TestE2EBridgeTransfersCursoredWithdrawals(t *testing.T) { ...@@ -445,3 +447,76 @@ func TestE2EBridgeTransfersCursoredWithdrawals(t *testing.T) {
require.Equal(t, int64(3-i)*params.Ether, withdrawal.L2BridgeWithdrawal.Tx.Amount.Int64()) require.Equal(t, int64(3-i)*params.Ether, withdrawal.L2BridgeWithdrawal.Tx.Amount.Int64())
} }
} }
func Test_ClientGetWithdrawals(t *testing.T) {
testSuite := createE2ETestSuite(t)
optimismPortal, err := bindings.NewOptimismPortal(testSuite.OpCfg.L1Deployments.OptimismPortalProxy, testSuite.L1Client)
require.NoError(t, err)
l2ToL1MessagePasser, err := bindings.NewOptimismPortal(predeploys.L2ToL1MessagePasserAddr, testSuite.L2Client)
require.NoError(t, err)
// Use alice and bob to generate two deposits / withdrawals
aliceAddr := testSuite.OpCfg.Secrets.Addresses().Alice
bobAddr := testSuite.OpCfg.Secrets.Addresses().Bob
// Actor represents a user that will deposit and withdraw
type actor struct {
addr common.Address
priv *ecdsa.PrivateKey
hash common.Hash
}
actors := []actor{
{
addr: aliceAddr,
priv: testSuite.OpCfg.Secrets.Alice,
},
{
addr: bobAddr,
priv: testSuite.OpCfg.Secrets.Bob,
},
}
// Iterate over each actor and deposit / withdraw
for _, actor := range actors {
l2Opts, err := bind.NewKeyedTransactorWithChainID(actor.priv, testSuite.OpCfg.L2ChainIDBig())
require.NoError(t, err)
l2Opts.Value = big.NewInt(params.Ether)
// (1) Deposit user funds into L2 via OptimismPortal contract
l1Opts, err := bind.NewKeyedTransactorWithChainID(actor.priv, testSuite.OpCfg.L1ChainIDBig())
require.NoError(t, err)
l1Opts.Value = l2Opts.Value
depositTx, err := optimismPortal.Receive(l1Opts)
require.NoError(t, err)
_, err = wait.ForReceiptOK(context.Background(), testSuite.L1Client, depositTx.Hash())
require.NoError(t, err)
// (2) Initiate withdrawal transaction via L2ToL1MessagePasser contract
l2ToL1MessagePasserWithdrawTx, err := l2ToL1MessagePasser.Receive(l2Opts)
require.NoError(t, err)
l2ToL1WithdrawReceipt, err := wait.ForReceiptOK(context.Background(), testSuite.L2Client, l2ToL1MessagePasserWithdrawTx.Hash())
require.NoError(t, err)
// wait for processor catchup
require.NoError(t, wait.For(context.Background(), 500*time.Millisecond, func() (bool, error) {
l2Header := testSuite.Indexer.BridgeProcessor.LatestL2Header
return l2Header != nil && l2Header.Number.Uint64() >= l2ToL1WithdrawReceipt.BlockNumber.Uint64(), nil
}))
actor.hash = l2ToL1MessagePasserWithdrawTx.Hash()
}
// (2) Test that Alice's tx can be retrieved via API client
aliceWithdrawals, err := testSuite.Client.GetAllWithdrawalsByAddress(aliceAddr)
require.NoError(t, err)
require.Len(t, aliceWithdrawals, 1)
require.Equal(t, actors[0], aliceWithdrawals[0].L2TransactionHash[0])
// (3) Test that Bob's tx can be retrieved via API client
bobWithdrawals, err := testSuite.Client.GetAllWithdrawalsByAddress(bobAddr)
require.NoError(t, err)
require.Len(t, bobWithdrawals, 1)
require.Equal(t, actors[1], bobWithdrawals[0].L2TransactionHash[0])
}
...@@ -34,8 +34,8 @@ type E2ETestSuite struct { ...@@ -34,8 +34,8 @@ type E2ETestSuite struct {
Indexer *indexer.Indexer Indexer *indexer.Indexer
// API // API
API *api.Api API *api.Api
IClient *client.IndexerClient Client *client.Client
// Rollup // Rollup
OpCfg *op_e2e.SystemConfig OpCfg *op_e2e.SystemConfig
...@@ -91,7 +91,7 @@ func createE2ETestSuite(t *testing.T) E2ETestSuite { ...@@ -91,7 +91,7 @@ func createE2ETestSuite(t *testing.T) E2ETestSuite {
L1ERC721BridgeProxy: opCfg.L1Deployments.L1ERC721BridgeProxy, L1ERC721BridgeProxy: opCfg.L1Deployments.L1ERC721BridgeProxy,
}, },
}, },
HTTPServer: config.ServerConfig{Host: "127.0.0.1", Port: 8080}, HTTPServer: config.ServerConfig{Host: "http://localhost", Port: 8777},
MetricsServer: config.ServerConfig{Host: "127.0.0.1", Port: 0}, MetricsServer: config.ServerConfig{Host: "127.0.0.1", Port: 0},
} }
...@@ -123,17 +123,17 @@ func createE2ETestSuite(t *testing.T) E2ETestSuite { ...@@ -123,17 +123,17 @@ func createE2ETestSuite(t *testing.T) E2ETestSuite {
cfg := &client.Config{ cfg := &client.Config{
PaginationLimit: 100, PaginationLimit: 100,
URL: fmt.Sprintf("http://%s:%d", indexerCfg.HTTPServer.Host, indexerCfg.HTTPServer.Port), URL: fmt.Sprintf("%s:%d", indexerCfg.HTTPServer.Host, indexerCfg.HTTPServer.Port),
} }
client, err := client.NewClient(cfg, nil)
ic, err := client.NewClient(cfg, nil)
require.NoError(t, err) require.NoError(t, err)
return E2ETestSuite{ return E2ETestSuite{
t: t, t: t,
DB: db, DB: db,
API: api, API: api,
IClient: ic, Client: client,
Indexer: indexer, Indexer: indexer,
OpCfg: &opCfg, OpCfg: &opCfg,
OpSys: opSys, OpSys: opSys,
......
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