Commit 250d80c5 authored by Ethen Pociask's avatar Ethen Pociask

[indexer.client] Fixed DB query bugs

parent 1946ecf6
...@@ -7,6 +7,18 @@ import ( ...@@ -7,6 +7,18 @@ import (
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
) )
const (
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
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)
......
...@@ -2,7 +2,6 @@ package routes ...@@ -2,7 +2,6 @@ package routes
import ( import (
"net/http" "net/http"
"strconv"
"github.com/ethereum-optimism/optimism/indexer/database" "github.com/ethereum-optimism/optimism/indexer/database"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
...@@ -58,16 +57,12 @@ func (h Routes) L1DepositsHandler(w http.ResponseWriter, r *http.Request) { ...@@ -58,16 +57,12 @@ func (h Routes) L1DepositsHandler(w http.ResponseWriter, r *http.Request) {
cursor := r.URL.Query().Get("cursor") cursor := r.URL.Query().Get("cursor")
limitQuery := r.URL.Query().Get("limit") limitQuery := r.URL.Query().Get("limit")
defaultLimit := 100 limit, err := h.v.ValidateLimit(limitQuery)
limit := defaultLimit if err != nil {
if limitQuery != "" { http.Error(w, err.Error(), http.StatusBadRequest)
parsedLimit, err := strconv.Atoi(limitQuery) h.Logger.Error("Invalid limit param")
if err != nil { h.Logger.Error(err.Error())
http.Error(w, "Limit could not be parsed into a number", http.StatusBadRequest) return
h.Logger.Error("Invalid limit")
h.Logger.Error(err.Error())
}
limit = parsedLimit
} }
deposits, err := h.BridgeTransfersView.L1BridgeDepositsByAddress(address, cursor, limit) deposits, err := h.BridgeTransfersView.L1BridgeDepositsByAddress(address, cursor, limit)
......
...@@ -10,6 +10,7 @@ type Routes struct { ...@@ -10,6 +10,7 @@ type Routes struct {
Logger log.Logger Logger log.Logger
BridgeTransfersView database.BridgeTransfersView BridgeTransfersView database.BridgeTransfersView
Router *chi.Mux Router *chi.Mux
v *Validator
} }
func NewRoutes(logger log.Logger, bv database.BridgeTransfersView, r *chi.Mux) Routes { func NewRoutes(logger log.Logger, bv database.BridgeTransfersView, r *chi.Mux) Routes {
......
package routes
import (
"strconv"
"errors"
)
// Validator ... Validates API user request parameters
type Validator struct {
}
// ValidateQueryParams ... Validates the limit and cursor query parameters
func (v *Validator) ValidateLimit(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
}
...@@ -2,7 +2,6 @@ package routes ...@@ -2,7 +2,6 @@ package routes
import ( import (
"net/http" "net/http"
"strconv"
"github.com/ethereum-optimism/optimism/indexer/database" "github.com/ethereum-optimism/optimism/indexer/database"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
...@@ -60,22 +59,20 @@ func (h Routes) L2WithdrawalsHandler(w http.ResponseWriter, r *http.Request) { ...@@ -60,22 +59,20 @@ func (h Routes) L2WithdrawalsHandler(w http.ResponseWriter, r *http.Request) {
cursor := r.URL.Query().Get("cursor") cursor := r.URL.Query().Get("cursor")
limitQuery := r.URL.Query().Get("limit") limitQuery := r.URL.Query().Get("limit")
defaultLimit := 100 limit, err := h.v.ValidateLimit(limitQuery)
limit := defaultLimit if err != nil {
if limitQuery != "" { http.Error(w, err.Error(), http.StatusBadRequest)
parsedLimit, err := strconv.Atoi(limitQuery) h.Logger.Error("Invalid query params")
if err != nil { h.Logger.Error(err.Error())
http.Error(w, "Limit could not be parsed into a number", http.StatusBadRequest) return
h.Logger.Error("Invalid limit")
h.Logger.Error(err.Error())
}
limit = parsedLimit
} }
withdrawals, err := h.BridgeTransfersView.L2BridgeWithdrawalsByAddress(address, cursor, limit) withdrawals, err := h.BridgeTransfersView.L2BridgeWithdrawalsByAddress(address, cursor, limit)
if err != nil { if err != nil {
http.Error(w, "Internal server error reading withdrawals", http.StatusInternalServerError) http.Error(w, "Internal server error reading withdrawals", http.StatusInternalServerError)
h.Logger.Error("Unable to read withdrawals from DB") h.Logger.Error("Unable to read withdrawals from DB")
h.Logger.Error(err.Error()) h.Logger.Error(err.Error())
return
} }
response := newWithdrawalResponse(withdrawals) response := newWithdrawalResponse(withdrawals)
......
...@@ -2,7 +2,7 @@ package client ...@@ -2,7 +2,7 @@ package client
import ( import (
"fmt" "fmt"
"io/ioutil" "io"
"net/http" "net/http"
"encoding/json" "encoding/json"
...@@ -31,8 +31,8 @@ type IndexerClient struct { ...@@ -31,8 +31,8 @@ type IndexerClient struct {
m node.Metricer m node.Metricer
} }
// DialIndexerClient ... Dials indexer API URL // NewClient ... Construct a new indexer client
func DialIndexerClient(cfg *Config, m node.Metricer) (*IndexerClient, error) { func NewClient(cfg *Config, m node.Metricer) (*IndexerClient, error) {
if cfg.PaginationLimit == 0 { if cfg.PaginationLimit == 0 {
cfg.PaginationLimit = defaultPagingLimit cfg.PaginationLimit = defaultPagingLimit
} }
...@@ -74,7 +74,7 @@ func (ic *IndexerClient) GetWithdrawalsByAddress(l2Address string, cursor string ...@@ -74,7 +74,7 @@ func (ic *IndexerClient) GetWithdrawalsByAddress(l2Address string, cursor string
return nil, err return nil, err
} }
body, err := ioutil.ReadAll(resp.Body) body, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err) return nil, fmt.Errorf("failed to read response body: %w", err)
} }
......
package client
import "testing"
func Test_Client(t *testing.T) {
cfg := &Config{
PaginationLimit: 100,
URL: "https://localhost:8080",
}
ic, err := DialIndexerClient(cfg, nil)
if err != nil {
t.Fatal(err)
}
// Get all withdrawals by address
withdrawals, err := ic.GetAllWithdrawalsByAddress("0xC64c9c88F28072F9DAa60d371acc08cB5FDb9952")
if err != nil {
t.Fatal(err)
}
t.Logf("withdrawals: %+v", withdrawals)
}
...@@ -128,7 +128,7 @@ type L1BridgeDepositsResponse struct { ...@@ -128,7 +128,7 @@ type L1BridgeDepositsResponse struct {
HasNextPage bool HasNextPage bool
} }
// L1BridgeDepositsByAddress retrieves a list of deposits intiated 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 defaultLimit := 100
...@@ -152,7 +152,7 @@ func (db *bridgeTransfersDB) L1BridgeDepositsByAddress(address common.Address, c ...@@ -152,7 +152,7 @@ func (db *bridgeTransfersDB) L1BridgeDepositsByAddress(address common.Address, c
// Coalesce l1 transaction deposits that are simply ETH sends // Coalesce l1 transaction deposits that are simply ETH sends
ethTransactionDeposits := db.gorm.Model(&L1TransactionDeposit{}) ethTransactionDeposits := db.gorm.Model(&L1TransactionDeposit{})
ethTransactionDeposits = ethTransactionDeposits.Where(Transaction{FromAddress: address}).Where("data = '0x' AND amount > 0") ethTransactionDeposits = ethTransactionDeposits.Where(&Transaction{FromAddress: address}).Where("data = '0x' AND amount > 0")
ethTransactionDeposits = ethTransactionDeposits.Joins("INNER JOIN l1_contract_events ON l1_contract_events.guid = initiated_l1_event_guid") ethTransactionDeposits = ethTransactionDeposits.Joins("INNER JOIN l1_contract_events ON l1_contract_events.guid = initiated_l1_event_guid")
ethTransactionDeposits = ethTransactionDeposits.Select(` ethTransactionDeposits = ethTransactionDeposits.Select(`
from_address, to_address, amount, data, source_hash AS transaction_source_hash, from_address, to_address, amount, data, source_hash AS transaction_source_hash,
...@@ -164,6 +164,7 @@ l1_transaction_deposits.timestamp, NULL AS cross_domain_message_hash, ? AS local ...@@ -164,6 +164,7 @@ l1_transaction_deposits.timestamp, NULL AS cross_domain_message_hash, ? AS local
} }
depositsQuery := db.gorm.Model(&L1BridgeDeposit{}) depositsQuery := db.gorm.Model(&L1BridgeDeposit{})
depositsQuery = depositsQuery.Where(&Transaction{FromAddress: address})
depositsQuery = depositsQuery.Joins("INNER JOIN l1_transaction_deposits ON l1_transaction_deposits.source_hash = transaction_source_hash") depositsQuery = depositsQuery.Joins("INNER JOIN l1_transaction_deposits ON l1_transaction_deposits.source_hash = transaction_source_hash")
depositsQuery = depositsQuery.Joins("INNER JOIN l1_contract_events ON l1_contract_events.guid = l1_transaction_deposits.initiated_l1_event_guid") depositsQuery = depositsQuery.Joins("INNER JOIN l1_contract_events ON l1_contract_events.guid = l1_transaction_deposits.initiated_l1_event_guid")
depositsQuery = depositsQuery.Select(` depositsQuery = depositsQuery.Select(`
...@@ -241,13 +242,9 @@ type L2BridgeWithdrawalsResponse struct { ...@@ -241,13 +242,9 @@ type L2BridgeWithdrawalsResponse struct {
HasNextPage bool HasNextPage bool
} }
// L2BridgeDepositsByAddress retrieves a list of deposits intiated 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 to with the Bedrock multistep 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) {
defaultLimit := 100
if limit <= 0 {
limit = defaultLimit
}
cursorClause := "" cursorClause := ""
if cursor != "" { if cursor != "" {
...@@ -265,7 +262,7 @@ func (db *bridgeTransfersDB) L2BridgeWithdrawalsByAddress(address common.Address ...@@ -265,7 +262,7 @@ func (db *bridgeTransfersDB) L2BridgeWithdrawalsByAddress(address common.Address
// Coalesce l2 transaction withdrawals that are simply ETH sends // Coalesce l2 transaction withdrawals that are simply ETH sends
ethTransactionWithdrawals := db.gorm.Model(&L2TransactionWithdrawal{}) ethTransactionWithdrawals := db.gorm.Model(&L2TransactionWithdrawal{})
ethTransactionWithdrawals = ethTransactionWithdrawals.Where(Transaction{FromAddress: address}).Where(`data = '0x' AND amount > 0`) ethTransactionWithdrawals = ethTransactionWithdrawals.Where(&Transaction{FromAddress: address}).Where(`data = '0x' AND amount > 0`)
ethTransactionWithdrawals = ethTransactionWithdrawals.Joins("INNER JOIN l2_contract_events ON l2_contract_events.guid = l2_transaction_withdrawals.initiated_l2_event_guid") ethTransactionWithdrawals = ethTransactionWithdrawals.Joins("INNER JOIN l2_contract_events ON l2_contract_events.guid = l2_transaction_withdrawals.initiated_l2_event_guid")
ethTransactionWithdrawals = ethTransactionWithdrawals.Joins("LEFT JOIN l1_contract_events AS proven_l1_events ON proven_l1_events.guid = l2_transaction_withdrawals.proven_l1_event_guid") ethTransactionWithdrawals = ethTransactionWithdrawals.Joins("LEFT JOIN l1_contract_events AS proven_l1_events ON proven_l1_events.guid = l2_transaction_withdrawals.proven_l1_event_guid")
ethTransactionWithdrawals = ethTransactionWithdrawals.Joins("LEFT JOIN l1_contract_events AS finalized_l1_events ON finalized_l1_events.guid = l2_transaction_withdrawals.finalized_l1_event_guid") ethTransactionWithdrawals = ethTransactionWithdrawals.Joins("LEFT JOIN l1_contract_events AS finalized_l1_events ON finalized_l1_events.guid = l2_transaction_withdrawals.finalized_l1_event_guid")
...@@ -278,7 +275,13 @@ l2_transaction_withdrawals.timestamp, NULL AS cross_domain_message_hash, ? AS lo ...@@ -278,7 +275,13 @@ l2_transaction_withdrawals.timestamp, NULL AS cross_domain_message_hash, ? AS lo
ethTransactionWithdrawals = ethTransactionWithdrawals.Where(cursorClause) ethTransactionWithdrawals = ethTransactionWithdrawals.Where(cursorClause)
} }
withdrawalsQuery := db.gorm.Model(&L2BridgeWithdrawal{}) ethTransactionWithdrawals.DryRun = true
ethTransactionWithdrawals.Find(&[]L2BridgeWithdrawalWithTransactionHashes{})
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")
...@@ -292,11 +295,19 @@ l2_bridge_withdrawals.timestamp, cross_domain_message_hash, local_token_address, ...@@ -292,11 +295,19 @@ 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{}
result := query.Find(&withdrawals) result := query.Find(&withdrawals)
if result.Error != nil { if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) { if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return nil, nil return nil, nil
......
...@@ -173,4 +173,11 @@ func TestE2EBridgeL2CrossDomainMessenger(t *testing.T) { ...@@ -173,4 +173,11 @@ func TestE2EBridgeL2CrossDomainMessenger(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
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))
} }
...@@ -10,7 +10,10 @@ import ( ...@@ -10,7 +10,10 @@ import (
"testing" "testing"
"time" "time"
"github.com/ethereum-optimism/optimism/indexer/api"
"github.com/ethereum-optimism/optimism/indexer" "github.com/ethereum-optimism/optimism/indexer"
"github.com/ethereum-optimism/optimism/indexer/client"
"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"
...@@ -30,6 +33,10 @@ type E2ETestSuite struct { ...@@ -30,6 +33,10 @@ type E2ETestSuite struct {
DB *database.DB DB *database.DB
Indexer *indexer.Indexer Indexer *indexer.Indexer
// API
API *api.Api
IClient *client.IndexerClient
// Rollup // Rollup
OpCfg *op_e2e.SystemConfig OpCfg *op_e2e.SystemConfig
OpSys *op_e2e.System OpSys *op_e2e.System
...@@ -84,7 +91,7 @@ func createE2ETestSuite(t *testing.T) E2ETestSuite { ...@@ -84,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: 0}, HTTPServer: config.ServerConfig{Host: "127.0.0.1", Port: 8080},
MetricsServer: config.ServerConfig{Host: "127.0.0.1", Port: 0}, MetricsServer: config.ServerConfig{Host: "127.0.0.1", Port: 0},
} }
...@@ -97,15 +104,36 @@ func createE2ETestSuite(t *testing.T) E2ETestSuite { ...@@ -97,15 +104,36 @@ func createE2ETestSuite(t *testing.T) E2ETestSuite {
require.NoError(t, err) require.NoError(t, err)
indexerCtx, indexerStop := context.WithCancel(context.Background()) indexerCtx, indexerStop := context.WithCancel(context.Background())
t.Cleanup(func() { indexerStop() })
go func() { go func() {
err := indexer.Run(indexerCtx) err := indexer.Run(indexerCtx)
require.NoError(t, err) require.NoError(t, err)
}() }()
api := api.NewApi(indexerLog, db.BridgeTransfers, indexerCfg.HTTPServer, indexerCfg.MetricsServer)
go func() {
err := api.Start(indexerCtx)
require.NoError(t, err)
}()
t.Cleanup(func() {
indexerStop()
})
cfg := &client.Config{
PaginationLimit: 100,
URL: fmt.Sprintf("http://%s:%d", indexerCfg.HTTPServer.Host, indexerCfg.HTTPServer.Port),
}
ic, err := client.NewClient(cfg, nil)
require.NoError(t, err)
return E2ETestSuite{ return E2ETestSuite{
t: t, t: t,
DB: db, DB: db,
API: api,
IClient: ic,
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