From eb4c98ecd0e1c1fc3a903b5b2d7630556e49f8ac Mon Sep 17 00:00:00 2001 From: Daniel Wasserman Date: Mon, 8 Jan 2024 12:22:53 -0600 Subject: [PATCH 01/39] WIP: initial relayer api --- services/rfq/relayer/relapi/handler.go | 56 +++++++++ services/rfq/relayer/relapi/model.go | 8 ++ services/rfq/relayer/relapi/server.go | 159 +++++++++++++++++++++++++ 3 files changed, 223 insertions(+) create mode 100644 services/rfq/relayer/relapi/handler.go create mode 100644 services/rfq/relayer/relapi/model.go create mode 100644 services/rfq/relayer/relapi/server.go diff --git a/services/rfq/relayer/relapi/handler.go b/services/rfq/relayer/relapi/handler.go new file mode 100644 index 0000000000..d6d2d5d1db --- /dev/null +++ b/services/rfq/relayer/relapi/handler.go @@ -0,0 +1,56 @@ +package relapi + +import ( + "net/http" + + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/gin-gonic/gin" + "github.com/synapsecns/sanguine/services/rfq/relayer/reldb" +) + +// Handler is the REST API handler. +type Handler struct { + db reldb.Service +} + +// NewHandler creates a new REST API handler. +func NewHandler(db reldb.Service) *Handler { + return &Handler{ + db: db, // Store the database connection in the handler + } +} + +// GetQuoteRequestStatus gets the status of a quote request, given a tx id. +func (h *Handler) GetQuoteRequestStatus(c *gin.Context) { + txIDStr := c.Query("txID") + if txIDStr == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "Must specify txID"}) + return + } + + txIDBytes, err := hexutil.Decode(txIDStr) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid txID"}) + return + } + var txID [32]byte + copy(txID[:], txIDBytes) + + quoteRequest, err := h.db.GetQuoteRequestByID(c, txID) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + resp := GetQuoteRequestStatusResponse{ + Status: quoteRequest.Status.String(), + TxID: hexutil.Encode(txID[:]), + TxHash: quoteRequest.DestTxHash.String(), + } + c.JSON(http.StatusOK, resp) +} + +// PutTxRetry retries a transaction based on tx hash. +func (h *Handler) PutTxRetry(c *gin.Context) { + c.JSON(http.StatusOK, nil) +} diff --git a/services/rfq/relayer/relapi/model.go b/services/rfq/relayer/relapi/model.go new file mode 100644 index 0000000000..94bab4ba11 --- /dev/null +++ b/services/rfq/relayer/relapi/model.go @@ -0,0 +1,8 @@ +package relapi + +// GetQuoteRequestStatusResponse contains the schema for a GET /quote response. +type GetQuoteRequestStatusResponse struct { + Status string `json:"status"` + TxID string `json:"tx_id"` + TxHash string `json:"tx_hash"` +} diff --git a/services/rfq/relayer/relapi/server.go b/services/rfq/relayer/relapi/server.go new file mode 100644 index 0000000000..cab9869437 --- /dev/null +++ b/services/rfq/relayer/relapi/server.go @@ -0,0 +1,159 @@ +package relapi + +import ( + "context" + "fmt" + "net/http" + "time" + + "github.com/ipfs/go-log" + "github.com/synapsecns/sanguine/core/ginhelper" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/gin-gonic/gin" + "github.com/synapsecns/sanguine/core/metrics" + baseServer "github.com/synapsecns/sanguine/core/server" + omniClient "github.com/synapsecns/sanguine/services/omnirpc/client" + "github.com/synapsecns/sanguine/services/rfq/api/config" + "github.com/synapsecns/sanguine/services/rfq/api/model" + "github.com/synapsecns/sanguine/services/rfq/api/rest" + "github.com/synapsecns/sanguine/services/rfq/contracts/fastbridge" + "github.com/synapsecns/sanguine/services/rfq/relayer/reldb" +) + +// APIServer is a struct that holds the configuration, database connection, gin engine, RPC client, metrics handler, and fast bridge contracts. +// It is used to initialize and run the API server. +type APIServer struct { + cfg config.Config + db reldb.Service + engine *gin.Engine + omnirpcClient omniClient.RPCClient + handler metrics.Handler + fastBridgeContracts map[uint32]*fastbridge.FastBridge +} + +// NewAPI holds the configuration, database connection, gin engine, RPC client, metrics handler, and fast bridge contracts. +// It is used to initialize and run the API server. +func NewAPI( + ctx context.Context, + cfg config.Config, + handler metrics.Handler, + omniRPCClient omniClient.RPCClient, + store reldb.Service, +) (*APIServer, error) { + if ctx == nil { + return nil, fmt.Errorf("context is nil") + } + if handler == nil { + return nil, fmt.Errorf("handler is nil") + } + if omniRPCClient == nil { + return nil, fmt.Errorf("omniRPCClient is nil") + } + if store == nil { + return nil, fmt.Errorf("store is nil") + } + + bridges := make(map[uint32]*fastbridge.FastBridge) + for chainID, bridge := range cfg.Bridges { + chainClient, err := omniRPCClient.GetChainClient(ctx, int(chainID)) + if err != nil { + return nil, fmt.Errorf("could not create omnirpc client: %w", err) + } + bridges[chainID], err = fastbridge.NewFastBridge(common.HexToAddress(bridge), chainClient) + if err != nil { + return nil, fmt.Errorf("could not create bridge contract: %w", err) + } + } + + return &APIServer{ + cfg: cfg, + db: store, + omnirpcClient: omniRPCClient, + handler: handler, + fastBridgeContracts: bridges, + }, nil +} + +const ( + getQuoteStatusRoute = "/status" + putRetryRoute = "/retry" +) + +var logger = log.Logger("relayer-api") + +// Run runs the rest api server. +func (r *APIServer) Run(ctx context.Context) error { + // TODO: Use Gin Helper + engine := ginhelper.New(logger) + h := NewHandler(r.db) + + // Apply AuthMiddleware only to the PUT route + quotesPut := engine.Group(putRetryRoute) + quotesPut.Use(r.AuthMiddleware()) + quotesPut.PUT("", h.PutTxRetry) + + // GET routes without the AuthMiddleware + engine.GET(getQuoteStatusRoute, h.GetQuoteRequestStatus) + + r.engine = engine + + connection := baseServer.Server{} + fmt.Printf("starting api at http://localhost:%s\n", r.cfg.Port) + err := connection.ListenAndServe(ctx, fmt.Sprintf(":%s", r.cfg.Port), r.engine) + if err != nil { + return fmt.Errorf("could not start rest api server: %w", err) + } + + return nil +} + +// AuthMiddleware is the Gin authentication middleware that authenticates requests using EIP191. +func (r *APIServer) AuthMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + var req model.PutQuoteRequest + if err := c.BindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + c.Abort() + return + } + + bridge, ok := r.fastBridgeContracts[uint32(req.DestChainID)] + if !ok { + c.JSON(http.StatusBadRequest, gin.H{"msg": "dest chain id not supported"}) + c.Abort() + return + } + + ops := &bind.CallOpts{Context: c} + relayerRole := crypto.Keccak256Hash([]byte("RELAYER_ROLE")) + + // authenticate relayer signature with EIP191 + deadline := time.Now().Unix() - 1000 // TODO: Replace with some type of r.cfg.AuthExpiryDelta + addressRecovered, err := rest.EIP191Auth(c, deadline) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"msg": fmt.Sprintf("unable to authenticate relayer: %v", err)}) + c.Abort() + return + } + + has, err := bridge.HasRole(ops, relayerRole, addressRecovered) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"msg": "unable to check relayer role on-chain"}) + c.Abort() + return + } else if !has { + c.JSON(http.StatusBadRequest, gin.H{"msg": "q.Relayer not an on-chain relayer"}) + c.Abort() + return + } + + // Log and pass to the next middleware if authentication succeeds + // Store the request in context after binding and validation + c.Set("putRequest", &req) + c.Set("relayerAddr", addressRecovered.Hex()) + c.Next() + } +} From 0129b412630d91dd233eae1af3be8a001f40222a Mon Sep 17 00:00:00 2001 From: Daniel Wasserman Date: Mon, 8 Jan 2024 12:26:02 -0600 Subject: [PATCH 02/39] WIP: use ByTxID naming --- services/rfq/relayer/relapi/handler.go | 4 ++-- services/rfq/relayer/relapi/server.go | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/services/rfq/relayer/relapi/handler.go b/services/rfq/relayer/relapi/handler.go index d6d2d5d1db..f36705fd8e 100644 --- a/services/rfq/relayer/relapi/handler.go +++ b/services/rfq/relayer/relapi/handler.go @@ -20,8 +20,8 @@ func NewHandler(db reldb.Service) *Handler { } } -// GetQuoteRequestStatus gets the status of a quote request, given a tx id. -func (h *Handler) GetQuoteRequestStatus(c *gin.Context) { +// GetQuoteRequestStatusByTxID gets the status of a quote request, given a tx id. +func (h *Handler) GetQuoteRequestStatusByTxID(c *gin.Context) { txIDStr := c.Query("txID") if txIDStr == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "Must specify txID"}) diff --git a/services/rfq/relayer/relapi/server.go b/services/rfq/relayer/relapi/server.go index cab9869437..d59823390d 100644 --- a/services/rfq/relayer/relapi/server.go +++ b/services/rfq/relayer/relapi/server.go @@ -78,8 +78,8 @@ func NewAPI( } const ( - getQuoteStatusRoute = "/status" - putRetryRoute = "/retry" + getQuoteStatusByTxIDRoute = "/status/by_tx_id" + putRetryRoute = "/retry" ) var logger = log.Logger("relayer-api") @@ -96,7 +96,7 @@ func (r *APIServer) Run(ctx context.Context) error { quotesPut.PUT("", h.PutTxRetry) // GET routes without the AuthMiddleware - engine.GET(getQuoteStatusRoute, h.GetQuoteRequestStatus) + engine.GET(getQuoteStatusByTxIDRoute, h.GetQuoteRequestStatusByTxID) r.engine = engine From c740fbfbeb13a85d8261241c31c53a329583556c Mon Sep 17 00:00:00 2001 From: Daniel Wasserman Date: Mon, 8 Jan 2024 12:34:51 -0600 Subject: [PATCH 03/39] Feat: add GetQuoteRequestStatusByTxHash --- services/rfq/relayer/relapi/handler.go | 26 +++++++++++++++++++++++- services/rfq/relayer/reldb/base/quote.go | 21 ++++++++++++++++++- services/rfq/relayer/reldb/db.go | 6 +++++- 3 files changed, 50 insertions(+), 3 deletions(-) diff --git a/services/rfq/relayer/relapi/handler.go b/services/rfq/relayer/relapi/handler.go index f36705fd8e..03f581c417 100644 --- a/services/rfq/relayer/relapi/handler.go +++ b/services/rfq/relayer/relapi/handler.go @@ -3,6 +3,7 @@ package relapi import ( "net/http" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/gin-gonic/gin" "github.com/synapsecns/sanguine/services/rfq/relayer/reldb" @@ -44,7 +45,30 @@ func (h *Handler) GetQuoteRequestStatusByTxID(c *gin.Context) { resp := GetQuoteRequestStatusResponse{ Status: quoteRequest.Status.String(), - TxID: hexutil.Encode(txID[:]), + TxID: hexutil.Encode(quoteRequest.TransactionID[:]), + TxHash: quoteRequest.DestTxHash.String(), + } + c.JSON(http.StatusOK, resp) +} + +// GetQuoteRequestStatusByTxHash gets the status of a quote request, given a tx hash. +func (h *Handler) GetQuoteRequestStatusByTxHash(c *gin.Context) { + txHashStr := c.Query("hash") + if txHashStr == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "Must specify hash"}) + return + } + + txHash := common.HexToHash(txHashStr) + quoteRequest, err := h.db.GetQuoteRequestByTxHash(c, txHash) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + resp := GetQuoteRequestStatusResponse{ + Status: quoteRequest.Status.String(), + TxID: hexutil.Encode(quoteRequest.TransactionID[:]), TxHash: quoteRequest.DestTxHash.String(), } c.JSON(http.StatusOK, resp) diff --git a/services/rfq/relayer/reldb/base/quote.go b/services/rfq/relayer/reldb/base/quote.go index 7232b2474b..169caa8b3f 100644 --- a/services/rfq/relayer/reldb/base/quote.go +++ b/services/rfq/relayer/reldb/base/quote.go @@ -34,7 +34,26 @@ func (s Store) GetQuoteRequestByID(ctx context.Context, id [32]byte) (*reldb.Quo } if tx.Error != nil { - return nil, fmt.Errorf("could not get quote id") + return nil, fmt.Errorf("could not get quote") + } + + qr, err := modelResult.ToQuoteRequest() + if err != nil { + return nil, err + } + return qr, nil +} + +// GetQuoteRequestByTxHash gets a quote request by id. Should return ErrNoQuoteForID if not found. +func (s Store) GetQuoteRequestByTxHash(ctx context.Context, txHash common.Hash) (*reldb.QuoteRequest, error) { + var modelResult RequestForQuote + tx := s.DB().WithContext(ctx).Where(fmt.Sprintf("%s = ?", destTxHashFieldName), txHash.String()).First(&modelResult) + if errors.Is(tx.Error, gorm.ErrRecordNotFound) { + return nil, reldb.ErrNoQuoteForTxHash + } + + if tx.Error != nil { + return nil, fmt.Errorf("could not get quote") } qr, err := modelResult.ToQuoteRequest() diff --git a/services/rfq/relayer/reldb/db.go b/services/rfq/relayer/reldb/db.go index f4e178004d..a5a0fbbfed 100644 --- a/services/rfq/relayer/reldb/db.go +++ b/services/rfq/relayer/reldb/db.go @@ -32,6 +32,8 @@ type Reader interface { LatestBlockForChain(ctx context.Context, chainID uint64) (uint64, error) // GetQuoteRequestByID gets a quote request by id. Should return ErrNoQuoteForID if not found GetQuoteRequestByID(ctx context.Context, id [32]byte) (*QuoteRequest, error) + // GetQuoteRequestByTxHash gets a quote request by dest tx hash. Should return ErrNoQuoteForTxHash if not found + GetQuoteRequestByTxHash(ctx context.Context, txHash common.Hash) (*QuoteRequest, error) // GetQuoteResultsByStatus gets quote results by status GetQuoteResultsByStatus(ctx context.Context, matchStatuses ...QuoteRequestStatus) (res []QuoteRequest, _ error) } @@ -48,7 +50,9 @@ var ( // ErrNoLatestBlockForChainID is returned when no block exists for the chain. ErrNoLatestBlockForChainID = errors.New("no latest block for chainId") // ErrNoQuoteForID means the quote was not found. - ErrNoQuoteForID = errors.New("no quote found") + ErrNoQuoteForID = errors.New("no quote found for tx id") + // ErrNoQuoteForTxHash means the quote was not found. + ErrNoQuoteForTxHash = errors.New("no quote found for tx hash") ) // QuoteRequest is the quote request object. From 571e865b3f58b0cbf867a71ff7bf5941ca4647f1 Mon Sep 17 00:00:00 2001 From: Daniel Wasserman Date: Mon, 8 Jan 2024 14:46:42 -0600 Subject: [PATCH 04/39] WIP: initial impl for PUT /tx/retry --- services/rfq/relayer/relapi/handler.go | 62 +++++++++++++++++-- services/rfq/relayer/relapi/model.go | 8 +++ services/rfq/relayer/relapi/server.go | 39 +++++++----- services/rfq/relayer/service/handlers.go | 1 - services/rfq/relayer/service/statushandler.go | 21 +++++++ 5 files changed, 109 insertions(+), 22 deletions(-) diff --git a/services/rfq/relayer/relapi/handler.go b/services/rfq/relayer/relapi/handler.go index 03f581c417..52298b92f8 100644 --- a/services/rfq/relayer/relapi/handler.go +++ b/services/rfq/relayer/relapi/handler.go @@ -1,23 +1,31 @@ package relapi import ( + "fmt" + "math/big" "net/http" + "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core/types" "github.com/gin-gonic/gin" + "github.com/synapsecns/sanguine/core" "github.com/synapsecns/sanguine/services/rfq/relayer/reldb" + "github.com/synapsecns/sanguine/services/rfq/relayer/service" ) // Handler is the REST API handler. type Handler struct { - db reldb.Service + db reldb.Service + chains map[uint32]*service.Chain } // NewHandler creates a new REST API handler. -func NewHandler(db reldb.Service) *Handler { +func NewHandler(db reldb.Service, chains map[uint32]*service.Chain) *Handler { return &Handler{ - db: db, // Store the database connection in the handler + db: db, // Store the database connection in the handler + chains: chains, } } @@ -76,5 +84,51 @@ func (h *Handler) GetQuoteRequestStatusByTxHash(c *gin.Context) { // PutTxRetry retries a transaction based on tx hash. func (h *Handler) PutTxRetry(c *gin.Context) { - c.JSON(http.StatusOK, nil) + txHashStr := c.Query("hash") + if txHashStr == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "Must specify hash"}) + return + } + + txHash := common.HexToHash(txHashStr) + quoteRequest, err := h.db.GetQuoteRequestByTxHash(c, txHash) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + chainID := quoteRequest.Transaction.DestChainId + chain, ok := h.chains[uint32(chainID)] + if !ok { + c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("No contract found for chain: %d", chainID)}) + return + } + + // TODO: this can be deduped with handlers.go code + gasAmount := big.NewInt(0) + if quoteRequest.Transaction.SendChainGas { + gasAmount, err = chain.Bridge.ChainGasAmount(&bind.CallOpts{Context: c}) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("could not get chain gas amount: %s", err.Error())}) + return + } + } + nonce, err := chain.SubmitTransaction(c, func(transactor *bind.TransactOpts) (tx *types.Transaction, err error) { + transactor.Value = core.CopyBigInt(gasAmount) + + tx, err = chain.Bridge.Relay(transactor, quoteRequest.RawRequest) + if err != nil { + return nil, fmt.Errorf("could not relay: %w", err) + } + + return tx, nil + }) + + resp := PutTxRetryResponse{ + TxID: hexutil.Encode(quoteRequest.TransactionID[:]), + ChainID: chainID, + Nonce: nonce, + GasAmount: gasAmount.String(), + } + c.JSON(http.StatusOK, resp) } diff --git a/services/rfq/relayer/relapi/model.go b/services/rfq/relayer/relapi/model.go index 94bab4ba11..e5f8cad5a7 100644 --- a/services/rfq/relayer/relapi/model.go +++ b/services/rfq/relayer/relapi/model.go @@ -6,3 +6,11 @@ type GetQuoteRequestStatusResponse struct { TxID string `json:"tx_id"` TxHash string `json:"tx_hash"` } + +// PutTxRetryResponse contains the schema for a PUT /tx/retry response. +type PutTxRetryResponse struct { + TxID string `json:"tx_id"` + ChainID uint32 `json:"chain_id"` + Nonce uint64 `json:"nonce"` + GasAmount string `json:"gas_amount"` +} diff --git a/services/rfq/relayer/relapi/server.go b/services/rfq/relayer/relapi/server.go index d59823390d..3bd1a32766 100644 --- a/services/rfq/relayer/relapi/server.go +++ b/services/rfq/relayer/relapi/server.go @@ -8,6 +8,7 @@ import ( "github.com/ipfs/go-log" "github.com/synapsecns/sanguine/core/ginhelper" + "github.com/synapsecns/sanguine/ethergo/submitter" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" @@ -19,19 +20,19 @@ import ( "github.com/synapsecns/sanguine/services/rfq/api/config" "github.com/synapsecns/sanguine/services/rfq/api/model" "github.com/synapsecns/sanguine/services/rfq/api/rest" - "github.com/synapsecns/sanguine/services/rfq/contracts/fastbridge" + "github.com/synapsecns/sanguine/services/rfq/relayer/listener" "github.com/synapsecns/sanguine/services/rfq/relayer/reldb" + "github.com/synapsecns/sanguine/services/rfq/relayer/service" ) // APIServer is a struct that holds the configuration, database connection, gin engine, RPC client, metrics handler, and fast bridge contracts. // It is used to initialize and run the API server. type APIServer struct { - cfg config.Config - db reldb.Service - engine *gin.Engine - omnirpcClient omniClient.RPCClient - handler metrics.Handler - fastBridgeContracts map[uint32]*fastbridge.FastBridge + cfg config.Config + db reldb.Service + engine *gin.Engine + handler metrics.Handler + chains map[uint32]*service.Chain } // NewAPI holds the configuration, database connection, gin engine, RPC client, metrics handler, and fast bridge contracts. @@ -42,6 +43,7 @@ func NewAPI( handler metrics.Handler, omniRPCClient omniClient.RPCClient, store reldb.Service, + submitter submitter.TransactionSubmitter, ) (*APIServer, error) { if ctx == nil { return nil, fmt.Errorf("context is nil") @@ -56,24 +58,27 @@ func NewAPI( return nil, fmt.Errorf("store is nil") } - bridges := make(map[uint32]*fastbridge.FastBridge) + chains := make(map[uint32]*service.Chain) for chainID, bridge := range cfg.Bridges { chainClient, err := omniRPCClient.GetChainClient(ctx, int(chainID)) if err != nil { return nil, fmt.Errorf("could not create omnirpc client: %w", err) } - bridges[chainID], err = fastbridge.NewFastBridge(common.HexToAddress(bridge), chainClient) + chainListener, err := listener.NewChainListener(chainClient, store, common.HexToAddress(bridge), handler) if err != nil { - return nil, fmt.Errorf("could not create bridge contract: %w", err) + return nil, fmt.Errorf("could not get chain listener: %w", err) + } + chains[chainID], err = service.NewChain(ctx, chainClient, common.HexToAddress(bridge), chainListener, submitter) + if err != nil { + return nil, fmt.Errorf("could not create chain: %w", err) } } return &APIServer{ - cfg: cfg, - db: store, - omnirpcClient: omniRPCClient, - handler: handler, - fastBridgeContracts: bridges, + cfg: cfg, + db: store, + handler: handler, + chains: chains, }, nil } @@ -120,7 +125,7 @@ func (r *APIServer) AuthMiddleware() gin.HandlerFunc { return } - bridge, ok := r.fastBridgeContracts[uint32(req.DestChainID)] + chain, ok := r.chains[uint32(req.DestChainID)] if !ok { c.JSON(http.StatusBadRequest, gin.H{"msg": "dest chain id not supported"}) c.Abort() @@ -139,7 +144,7 @@ func (r *APIServer) AuthMiddleware() gin.HandlerFunc { return } - has, err := bridge.HasRole(ops, relayerRole, addressRecovered) + has, err := chain.Bridge.HasRole(ops, relayerRole, addressRecovered) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"msg": "unable to check relayer role on-chain"}) c.Abort() diff --git a/services/rfq/relayer/service/handlers.go b/services/rfq/relayer/service/handlers.go index b352ffddf4..f0a80cb267 100644 --- a/services/rfq/relayer/service/handlers.go +++ b/services/rfq/relayer/service/handlers.go @@ -243,7 +243,6 @@ func (r *Relayer) handleRelayLog(ctx context.Context, req *fastbridge.FastBridge func (q *QuoteRequestHandler) handleRelayCompleted(ctx context.Context, _ trace.Span, request reldb.QuoteRequest) (err error) { // relays been completed, it's time to go back to the origin chain and try to prove _, err = q.Origin.SubmitTransaction(ctx, func(transactor *bind.TransactOpts) (tx *types.Transaction, err error) { - // MAJO MAJOR TODO should be dest tx hash tx, err = q.Origin.Bridge.Prove(transactor, request.RawRequest, request.DestTxHash) if err != nil { return nil, fmt.Errorf("could not relay: %w", err) diff --git a/services/rfq/relayer/service/statushandler.go b/services/rfq/relayer/service/statushandler.go index e0cd069e71..7e46db056f 100644 --- a/services/rfq/relayer/service/statushandler.go +++ b/services/rfq/relayer/service/statushandler.go @@ -153,6 +153,27 @@ func (r *Relayer) chainIDToChain(ctx context.Context, chainID uint32) (*Chain, e }, nil } +// NewChain creates a new chain helper. +func NewChain(ctx context.Context, chainClient client.EVM, addr common.Address, chainListener listener.ContractListener, ts submitter.TransactionSubmitter) (*Chain, error) { + bridge, err := fastbridge.NewFastBridgeRef(addr, chainClient) + if err != nil { + return nil, fmt.Errorf("could not create bridge contract: %w", err) + } + chainID, err := chainClient.ChainID(ctx) + if err != nil { + return nil, fmt.Errorf("could not get chain id: %w", err) + } + return &Chain{ + ChainID: uint32(chainID.Int64()), + Bridge: bridge, + Client: chainClient, + // TODO: configure + Confirmations: 1, + listener: chainListener, + submitter: ts, + }, nil +} + // shouldCheckClaim checks if we should check the claim method. // if so it checks the claim method and updates the cache. func (q *QuoteRequestHandler) shouldCheckClaim(request reldb.QuoteRequest) bool { From 521e7fa60289efae7bf231352596867a476489a9 Mon Sep 17 00:00:00 2001 From: Daniel Wasserman Date: Mon, 8 Jan 2024 14:50:18 -0600 Subject: [PATCH 05/39] Cleanup: comments --- services/rfq/relayer/reldb/base/model.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/services/rfq/relayer/reldb/base/model.go b/services/rfq/relayer/reldb/base/model.go index 20b4ab1b53..3c1508c768 100644 --- a/services/rfq/relayer/reldb/base/model.go +++ b/services/rfq/relayer/reldb/base/model.go @@ -61,7 +61,8 @@ type RequestForQuote struct { // CreatedAt is the creation time CreatedAt time.Time // UpdatedAt is the update time - UpdatedAt time.Time + UpdatedAt time.Time + // TransactionID is the transaction id of the event TransactionID string `gorm:"column:transaction_id;primaryKey"` // OriginChainID is the origin chain for the transactions OriginChainID uint32 @@ -88,8 +89,10 @@ type RequestForQuote struct { DestAmountOriginal string // DestAmountOriginal is the original destination amount DestAmount decimal.Decimal `gorm:"index"` + // DestTxHash is the destination tx hash DestTxHash string - Deadline time.Time `gorm:"index"` + // Deadline is the deadline for the transaction + Deadline time.Time `gorm:"index"` // OriginNonce is the nonce on the origin chain in the app. // this is not effected by the message.sender nonce. OriginNonce int `gorm:"index"` From 2e2eba30404627867d5f4e91f02e4766b0a99fc9 Mon Sep 17 00:00:00 2001 From: Daniel Wasserman Date: Mon, 8 Jan 2024 14:57:01 -0600 Subject: [PATCH 06/39] Feat: add OriginTxHash to QuoteRequest, RequestForQuote models --- services/rfq/relayer/reldb/base/model.go | 8 ++++++-- services/rfq/relayer/reldb/db.go | 5 +++-- services/rfq/relayer/service/handlers.go | 1 + 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/services/rfq/relayer/reldb/base/model.go b/services/rfq/relayer/reldb/base/model.go index 3c1508c768..e03f208681 100644 --- a/services/rfq/relayer/reldb/base/model.go +++ b/services/rfq/relayer/reldb/base/model.go @@ -85,6 +85,8 @@ type RequestForQuote struct { // OriginAmount is the origin amount stored for sorting. // This is not the source of truth, but is approximate OriginAmount decimal.Decimal `gorm:"index"` + // OriginTxHash is the origin tx hash + OriginTxHash string // DestAmountOriginal is the original amount used for precision DestAmountOriginal string // DestAmountOriginal is the original destination amount @@ -118,6 +120,7 @@ func FromQuoteRequest(request reldb.QuoteRequest) RequestForQuote { DestRecipient: request.Transaction.DestRecipient.String(), OriginToken: request.Transaction.OriginToken.String(), OriginTokenDecimals: request.OriginTokenDecimals, + OriginTxHash: request.OriginTxHash.String(), RawRequest: hexutil.Encode(request.RawRequest), SendChainGas: request.Transaction.SendChainGas, DestTokenDecimals: request.DestTokenDecimals, @@ -172,8 +175,9 @@ func (r RequestForQuote) ToQuoteRequest() (*reldb.QuoteRequest, error) { Deadline: big.NewInt(r.Deadline.Unix()), Nonce: big.NewInt(int64(r.OriginNonce)), }, - Status: r.Status, - DestTxHash: common.HexToHash(r.DestTxHash), + Status: r.Status, + OriginTxHash: common.HexToHash(r.OriginTxHash), + DestTxHash: common.HexToHash(r.DestTxHash), }, nil } diff --git a/services/rfq/relayer/reldb/db.go b/services/rfq/relayer/reldb/db.go index f4e178004d..edd9feda87 100644 --- a/services/rfq/relayer/reldb/db.go +++ b/services/rfq/relayer/reldb/db.go @@ -61,8 +61,9 @@ type QuoteRequest struct { Sender common.Address Transaction fastbridge.IFastBridgeBridgeTransaction // Status is the quote request status - Status QuoteRequestStatus - DestTxHash common.Hash + Status QuoteRequestStatus + OriginTxHash common.Hash + DestTxHash common.Hash } // GetOriginIDPair gets the origin chain id and token address pair. diff --git a/services/rfq/relayer/service/handlers.go b/services/rfq/relayer/service/handlers.go index b352ffddf4..8b3b6ec4ad 100644 --- a/services/rfq/relayer/service/handlers.go +++ b/services/rfq/relayer/service/handlers.go @@ -81,6 +81,7 @@ func (r *Relayer) handleBridgeRequestedLog(parentCtx context.Context, req *fastb Sender: req.Sender, Transaction: bridgeTx, Status: reldb.Seen, + OriginTxHash: req.Raw.TxHash, }) if err != nil { return fmt.Errorf("could not get db: %w", err) From 20123b6c13615bba5ac790961af1c3ceaae9030f Mon Sep 17 00:00:00 2001 From: Daniel Wasserman Date: Mon, 8 Jan 2024 15:04:59 -0600 Subject: [PATCH 07/39] Feat: fetch request by origin tx hash instead of dest --- services/rfq/relayer/relapi/handler.go | 28 ++++++++++++++---------- services/rfq/relayer/relapi/model.go | 7 +++--- services/rfq/relayer/reldb/base/model.go | 3 +++ services/rfq/relayer/reldb/base/quote.go | 6 ++--- services/rfq/relayer/reldb/db.go | 4 ++-- 5 files changed, 28 insertions(+), 20 deletions(-) diff --git a/services/rfq/relayer/relapi/handler.go b/services/rfq/relayer/relapi/handler.go index 52298b92f8..61d1dcf603 100644 --- a/services/rfq/relayer/relapi/handler.go +++ b/services/rfq/relayer/relapi/handler.go @@ -33,7 +33,7 @@ func NewHandler(db reldb.Service, chains map[uint32]*service.Chain) *Handler { func (h *Handler) GetQuoteRequestStatusByTxID(c *gin.Context) { txIDStr := c.Query("txID") if txIDStr == "" { - c.JSON(http.StatusBadRequest, gin.H{"error": "Must specify txID"}) + c.JSON(http.StatusBadRequest, gin.H{"error": "Must specify 'txID'"}) return } @@ -52,32 +52,36 @@ func (h *Handler) GetQuoteRequestStatusByTxID(c *gin.Context) { } resp := GetQuoteRequestStatusResponse{ - Status: quoteRequest.Status.String(), - TxID: hexutil.Encode(quoteRequest.TransactionID[:]), - TxHash: quoteRequest.DestTxHash.String(), + Status: quoteRequest.Status.String(), + TxID: hexutil.Encode(quoteRequest.TransactionID[:]), + OriginTxHash: quoteRequest.OriginTxHash.String(), + DestTxHash: quoteRequest.DestTxHash.String(), } c.JSON(http.StatusOK, resp) } -// GetQuoteRequestStatusByTxHash gets the status of a quote request, given a tx hash. +const unspecifiedTxHash = "Must specify 'hash' (corresponding to origin tx)" + +// GetQuoteRequestStatusByTxHash gets the status of a quote request, given an origin tx hash. func (h *Handler) GetQuoteRequestStatusByTxHash(c *gin.Context) { txHashStr := c.Query("hash") if txHashStr == "" { - c.JSON(http.StatusBadRequest, gin.H{"error": "Must specify hash"}) + c.JSON(http.StatusBadRequest, gin.H{"error": unspecifiedTxHash}) return } txHash := common.HexToHash(txHashStr) - quoteRequest, err := h.db.GetQuoteRequestByTxHash(c, txHash) + quoteRequest, err := h.db.GetQuoteRequestByOriginTxHash(c, txHash) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } resp := GetQuoteRequestStatusResponse{ - Status: quoteRequest.Status.String(), - TxID: hexutil.Encode(quoteRequest.TransactionID[:]), - TxHash: quoteRequest.DestTxHash.String(), + Status: quoteRequest.Status.String(), + TxID: hexutil.Encode(quoteRequest.TransactionID[:]), + OriginTxHash: quoteRequest.OriginTxHash.String(), + DestTxHash: quoteRequest.DestTxHash.String(), } c.JSON(http.StatusOK, resp) } @@ -86,12 +90,12 @@ func (h *Handler) GetQuoteRequestStatusByTxHash(c *gin.Context) { func (h *Handler) PutTxRetry(c *gin.Context) { txHashStr := c.Query("hash") if txHashStr == "" { - c.JSON(http.StatusBadRequest, gin.H{"error": "Must specify hash"}) + c.JSON(http.StatusBadRequest, gin.H{"error": unspecifiedTxHash}) return } txHash := common.HexToHash(txHashStr) - quoteRequest, err := h.db.GetQuoteRequestByTxHash(c, txHash) + quoteRequest, err := h.db.GetQuoteRequestByOriginTxHash(c, txHash) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return diff --git a/services/rfq/relayer/relapi/model.go b/services/rfq/relayer/relapi/model.go index e5f8cad5a7..3f475e6455 100644 --- a/services/rfq/relayer/relapi/model.go +++ b/services/rfq/relayer/relapi/model.go @@ -2,9 +2,10 @@ package relapi // GetQuoteRequestStatusResponse contains the schema for a GET /quote response. type GetQuoteRequestStatusResponse struct { - Status string `json:"status"` - TxID string `json:"tx_id"` - TxHash string `json:"tx_hash"` + Status string `json:"status"` + TxID string `json:"tx_id"` + OriginTxHash string `json:"origin_tx_hash"` + DestTxHash string `json:"dest_tx_hash"` } // PutTxRetryResponse contains the schema for a PUT /tx/retry response. diff --git a/services/rfq/relayer/reldb/base/model.go b/services/rfq/relayer/reldb/base/model.go index e03f208681..87ecdcc4ec 100644 --- a/services/rfq/relayer/reldb/base/model.go +++ b/services/rfq/relayer/reldb/base/model.go @@ -22,6 +22,7 @@ func init() { blockNumberFieldName = namer.GetConsistentName("BlockNumber") statusFieldName = namer.GetConsistentName("Status") transactionIDFieldName = namer.GetConsistentName("TransactionID") + originTxHashFieldName = namer.GetConsistentName("OriginTxHash") destTxHashFieldName = namer.GetConsistentName("DestTxHash") } @@ -34,6 +35,8 @@ var ( statusFieldName string // transactionIDFieldName is the transactions id field name. transactionIDFieldName string + // originTxHashFieldName is the origin tx hash field name. + originTxHashFieldName string // destTxHashFieldName is the dest tx hash field name. destTxHashFieldName string ) diff --git a/services/rfq/relayer/reldb/base/quote.go b/services/rfq/relayer/reldb/base/quote.go index 169caa8b3f..b424a3f20a 100644 --- a/services/rfq/relayer/reldb/base/quote.go +++ b/services/rfq/relayer/reldb/base/quote.go @@ -44,10 +44,10 @@ func (s Store) GetQuoteRequestByID(ctx context.Context, id [32]byte) (*reldb.Quo return qr, nil } -// GetQuoteRequestByTxHash gets a quote request by id. Should return ErrNoQuoteForID if not found. -func (s Store) GetQuoteRequestByTxHash(ctx context.Context, txHash common.Hash) (*reldb.QuoteRequest, error) { +// GetQuoteRequestByOriginTxHash gets a quote request by tx hash. Should return ErrNoQuoteForID if not found. +func (s Store) GetQuoteRequestByOriginTxHash(ctx context.Context, txHash common.Hash) (*reldb.QuoteRequest, error) { var modelResult RequestForQuote - tx := s.DB().WithContext(ctx).Where(fmt.Sprintf("%s = ?", destTxHashFieldName), txHash.String()).First(&modelResult) + tx := s.DB().WithContext(ctx).Where(fmt.Sprintf("%s = ?", originTxHashFieldName), txHash.String()).First(&modelResult) if errors.Is(tx.Error, gorm.ErrRecordNotFound) { return nil, reldb.ErrNoQuoteForTxHash } diff --git a/services/rfq/relayer/reldb/db.go b/services/rfq/relayer/reldb/db.go index aa8af61e8b..723cce855d 100644 --- a/services/rfq/relayer/reldb/db.go +++ b/services/rfq/relayer/reldb/db.go @@ -32,8 +32,8 @@ type Reader interface { LatestBlockForChain(ctx context.Context, chainID uint64) (uint64, error) // GetQuoteRequestByID gets a quote request by id. Should return ErrNoQuoteForID if not found GetQuoteRequestByID(ctx context.Context, id [32]byte) (*QuoteRequest, error) - // GetQuoteRequestByTxHash gets a quote request by dest tx hash. Should return ErrNoQuoteForTxHash if not found - GetQuoteRequestByTxHash(ctx context.Context, txHash common.Hash) (*QuoteRequest, error) + // GetQuoteRequestByOriginTxHash gets a quote request by origin tx hash. Should return ErrNoQuoteForTxHash if not found + GetQuoteRequestByOriginTxHash(ctx context.Context, txHash common.Hash) (*QuoteRequest, error) // GetQuoteResultsByStatus gets quote results by status GetQuoteResultsByStatus(ctx context.Context, matchStatuses ...QuoteRequestStatus) (res []QuoteRequest, _ error) } From 29a27d78463eefff8f2ef3e29a5bddd27dc43907 Mon Sep 17 00:00:00 2001 From: Daniel Wasserman Date: Mon, 8 Jan 2024 16:25:57 -0600 Subject: [PATCH 08/39] WIP: initial relayer api suite --- services/rfq/relayer/relapi/server.go | 24 +-- services/rfq/relayer/relapi/server_test.go | 56 +++++++ services/rfq/relayer/relapi/suite_test.go | 177 +++++++++++++++++++++ 3 files changed, 246 insertions(+), 11 deletions(-) create mode 100644 services/rfq/relayer/relapi/server_test.go create mode 100644 services/rfq/relayer/relapi/suite_test.go diff --git a/services/rfq/relayer/relapi/server.go b/services/rfq/relayer/relapi/server.go index 3bd1a32766..f3ea197a37 100644 --- a/services/rfq/relayer/relapi/server.go +++ b/services/rfq/relayer/relapi/server.go @@ -25,9 +25,9 @@ import ( "github.com/synapsecns/sanguine/services/rfq/relayer/service" ) -// APIServer is a struct that holds the configuration, database connection, gin engine, RPC client, metrics handler, and fast bridge contracts. +// RelayerAPIServer is a struct that holds the configuration, database connection, gin engine, RPC client, metrics handler, and fast bridge contracts. // It is used to initialize and run the API server. -type APIServer struct { +type RelayerAPIServer struct { cfg config.Config db reldb.Service engine *gin.Engine @@ -35,16 +35,16 @@ type APIServer struct { chains map[uint32]*service.Chain } -// NewAPI holds the configuration, database connection, gin engine, RPC client, metrics handler, and fast bridge contracts. +// NewRelayerAPI holds the configuration, database connection, gin engine, RPC client, metrics handler, and fast bridge contracts. // It is used to initialize and run the API server. -func NewAPI( +func NewRelayerAPI( ctx context.Context, cfg config.Config, handler metrics.Handler, omniRPCClient omniClient.RPCClient, store reldb.Service, submitter submitter.TransactionSubmitter, -) (*APIServer, error) { +) (*RelayerAPIServer, error) { if ctx == nil { return nil, fmt.Errorf("context is nil") } @@ -74,7 +74,7 @@ func NewAPI( } } - return &APIServer{ + return &RelayerAPIServer{ cfg: cfg, db: store, handler: handler, @@ -83,17 +83,18 @@ func NewAPI( } const ( - getQuoteStatusByTxIDRoute = "/status/by_tx_id" - putRetryRoute = "/retry" + getQuoteStatusByTxHashRoute = "/status" + getQuoteStatusByTxIDRoute = "/status/by_tx_id" + putRetryRoute = "/retry" ) var logger = log.Logger("relayer-api") // Run runs the rest api server. -func (r *APIServer) Run(ctx context.Context) error { +func (r *RelayerAPIServer) Run(ctx context.Context) error { // TODO: Use Gin Helper engine := ginhelper.New(logger) - h := NewHandler(r.db) + h := NewHandler(r.db, r.chains) // Apply AuthMiddleware only to the PUT route quotesPut := engine.Group(putRetryRoute) @@ -101,6 +102,7 @@ func (r *APIServer) Run(ctx context.Context) error { quotesPut.PUT("", h.PutTxRetry) // GET routes without the AuthMiddleware + engine.GET(getQuoteStatusByTxHashRoute, h.GetQuoteRequestStatusByTxHash) engine.GET(getQuoteStatusByTxIDRoute, h.GetQuoteRequestStatusByTxID) r.engine = engine @@ -116,7 +118,7 @@ func (r *APIServer) Run(ctx context.Context) error { } // AuthMiddleware is the Gin authentication middleware that authenticates requests using EIP191. -func (r *APIServer) AuthMiddleware() gin.HandlerFunc { +func (r *RelayerAPIServer) AuthMiddleware() gin.HandlerFunc { return func(c *gin.Context) { var req model.PutQuoteRequest if err := c.BindJSON(&req); err != nil { diff --git a/services/rfq/relayer/relapi/server_test.go b/services/rfq/relayer/relapi/server_test.go new file mode 100644 index 0000000000..a608da722b --- /dev/null +++ b/services/rfq/relayer/relapi/server_test.go @@ -0,0 +1,56 @@ +package relapi_test + +import ( + "fmt" + "net/http" + "strconv" + "time" + + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/crypto" + "github.com/synapsecns/sanguine/ethergo/signer/wallet" +) + +func (c *RelayerServerSuite) TestNewAPIServer() { + // Start the API server in a separate goroutine and wait for it to initialize. + c.startAPIServer() + client := &http.Client{} + req, err := http.NewRequestWithContext(c.GetTestContext(), http.MethodGet, fmt.Sprintf("http://localhost:%d/status", c.port), nil) + c.Require().NoError(err) + _, err = client.Do(req) + c.Require().NoError(err) + c.GetTestContext().Done() +} + +func (c *RelayerServerSuite) TestPutAndGetQuote() { + c.startAPIServer() +} + +// startAPIServer starts the API server and waits for it to initialize. +func (c *RelayerServerSuite) startAPIServer() { + go func() { + err := c.RelayerAPIServer.Run(c.GetTestContext()) + c.Require().NoError(err) + }() + time.Sleep(2 * time.Second) // Wait for the server to start. +} + +// prepareAuthHeader generates an authorization header using EIP191 signature with the given private key. +func (c *RelayerServerSuite) prepareAuthHeader(wallet wallet.Wallet) (string, error) { + // Get the current Unix timestamp as a string. + now := strconv.Itoa(int(time.Now().Unix())) + + // Prepare the data to be signed. + data := "\x19Ethereum Signed Message:\n" + strconv.Itoa(len(now)) + now + digest := crypto.Keccak256([]byte(data)) + + // Sign the data with the provided private key. + sig, err := crypto.Sign(digest, wallet.PrivateKey()) + if err != nil { + return "", fmt.Errorf("failed to sign data: %w", err) + } + signature := hexutil.Encode(sig) + + // Return the combined header value. + return now + ":" + signature, nil +} diff --git a/services/rfq/relayer/relapi/suite_test.go b/services/rfq/relayer/relapi/suite_test.go new file mode 100644 index 0000000000..1d77b5a7f8 --- /dev/null +++ b/services/rfq/relayer/relapi/suite_test.go @@ -0,0 +1,177 @@ +package relapi_test + +import ( + "fmt" + "math/big" + "testing" + + "github.com/Flaque/filet" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/params" + "github.com/phayes/freeport" + "github.com/puzpuzpuz/xsync/v2" + "github.com/stretchr/testify/suite" + "github.com/synapsecns/sanguine/core/dbcommon" + "github.com/synapsecns/sanguine/core/metrics" + "github.com/synapsecns/sanguine/core/testsuite" + "github.com/synapsecns/sanguine/ethergo/backends" + "github.com/synapsecns/sanguine/ethergo/backends/geth" + "github.com/synapsecns/sanguine/ethergo/signer/wallet" + omniClient "github.com/synapsecns/sanguine/services/omnirpc/client" + omnirpcHelper "github.com/synapsecns/sanguine/services/omnirpc/testhelper" + "github.com/synapsecns/sanguine/services/rfq/api/config" + "github.com/synapsecns/sanguine/services/rfq/contracts/fastbridge" + "github.com/synapsecns/sanguine/services/rfq/relayer/relapi" + "github.com/synapsecns/sanguine/services/rfq/relayer/reldb" + "github.com/synapsecns/sanguine/services/rfq/relayer/reldb/connect" + "golang.org/x/sync/errgroup" +) + +// RelayerServer suite is the relayer API server test suite. +type RelayerServerSuite struct { + *testsuite.TestSuite + omniRPCClient omniClient.RPCClient + omniRPCTestBackends []backends.SimulatedTestBackend + testBackends map[uint64]backends.SimulatedTestBackend + fastBridgeAddressMap *xsync.MapOf[uint64, common.Address] + database reldb.Service + cfg config.Config + testWallet wallet.Wallet + handler metrics.Handler + RelayerAPIServer *relapi.RelayerAPIServer + port uint16 +} + +// NewRelayerServerSuite creates a end-to-end test suite. +func NewRelayerServerSuite(tb testing.TB) *RelayerServerSuite { + tb.Helper() + return &RelayerServerSuite{ + TestSuite: testsuite.NewTestSuite(tb), + } +} + +func (c *RelayerServerSuite) SetupTest() { + c.TestSuite.SetupTest() + + testOmnirpc := omnirpcHelper.NewOmnirpcServer(c.GetTestContext(), c.T(), c.omniRPCTestBackends...) + omniRPCClient := omniClient.NewOmnirpcClient(testOmnirpc, c.handler, omniClient.WithCaptureReqRes()) + c.omniRPCClient = omniRPCClient + + arbFastBridgeAddress, ok := c.fastBridgeAddressMap.Load(42161) + c.True(ok) + ethFastBridgeAddress, ok := c.fastBridgeAddressMap.Load(1) + c.True(ok) + port, err := freeport.GetFreePort() + c.port = uint16(port) + c.Require().NoError(err) + + testConfig := config.Config{ + Database: config.DatabaseConfig{ + Type: "sqlite", + DSN: filet.TmpFile(c.T(), "", "").Name(), + }, + OmniRPCURL: testOmnirpc, + Bridges: map[uint32]string{ + 1: ethFastBridgeAddress.Hex(), + 42161: arbFastBridgeAddress.Hex(), + }, + Port: fmt.Sprintf("%d", port), + } + c.cfg = testConfig + + server, err := relapi.NewRelayerAPI(c.GetTestContext(), c.cfg, c.handler, c.omniRPCClient, c.database, nil) + c.Require().NoError(err) + c.RelayerAPIServer = server +} + +func (c *RelayerServerSuite) SetupSuite() { + c.TestSuite.SetupSuite() + + // let's create 2 mock chains + chainIDs := []uint64{1, 42161} + + c.testBackends = make(map[uint64]backends.SimulatedTestBackend) + + g, _ := errgroup.WithContext(c.GetSuiteContext()) + for _, chainID := range chainIDs { + chainID := chainID // capture func literal + g.Go(func() error { + // Setup Anvil backend for the suite to have RPC support + // anvilOpts := anvil.NewAnvilOptionBuilder() + // anvilOpts.SetChainID(chainID) + // anvilOpts.SetBlockTime(1 * time.Second) + // backend := anvil.NewAnvilBackend(c.GetSuiteContext(), c.T(), anvilOpts) + backend := geth.NewEmbeddedBackendForChainID(c.GetSuiteContext(), c.T(), new(big.Int).SetUint64(chainID)) + + // add the backend to the list of backends + c.testBackends[chainID] = backend + c.omniRPCTestBackends = append(c.omniRPCTestBackends, backend) + return nil + }) + } + + // wait for all backends to be ready + if err := g.Wait(); err != nil { + c.T().Fatal(err) + } + + testWallet, err := wallet.FromRandom() + c.Require().NoError(err) + c.testWallet = testWallet + for _, backend := range c.testBackends { + backend.FundAccount(c.GetSuiteContext(), c.testWallet.Address(), *big.NewInt(params.Ether)) + } + + c.fastBridgeAddressMap = xsync.NewIntegerMapOf[uint64, common.Address]() + + g, _ = errgroup.WithContext(c.GetSuiteContext()) + for _, backend := range c.testBackends { + backend := backend + // TODO: functionalize me + g.Go(func() error { + chainID, err := backend.ChainID(c.GetSuiteContext()) + if err != nil { + return fmt.Errorf("could not get chain id: %w", err) + } + // Create an auth to interact with the blockchain + auth, err := bind.NewKeyedTransactorWithChainID(c.testWallet.PrivateKey(), chainID) + c.Require().NoError(err) + + // Deploy the FastBridge contract + fastBridgeAddress, tx, _, err := fastbridge.DeployFastBridge(auth, backend, c.testWallet.Address()) + c.Require().NoError(err) + backend.WaitForConfirmation(c.GetSuiteContext(), tx) + + // Save the contracts to the map + c.fastBridgeAddressMap.Store(chainID.Uint64(), fastBridgeAddress) + + fastBridgeInstance, err := fastbridge.NewFastBridge(fastBridgeAddress, backend) + c.Require().NoError(err) + tx, err = fastBridgeInstance.AddRelayer(auth, c.testWallet.Address()) + c.Require().NoError(err) + backend.WaitForConfirmation(c.GetSuiteContext(), tx) + + return nil + }) + } + + // wait for all backends to be ready + if err := g.Wait(); err != nil { + c.T().Fatal(err) + } + + dbType, err := dbcommon.DBTypeFromString("sqlite") + c.Require().NoError(err) + metricsHandler := metrics.NewNullHandler() + c.handler = metricsHandler + // TODO use temp file / in memory sqlite3 to not create in directory files + testDB, _ := connect.Connect(c.GetSuiteContext(), dbType, filet.TmpDir(c.T(), ""), metricsHandler) + c.database = testDB + // setup config +} + +// TestConfigSuite runs the integration test suite. +func TestRelayerServerSuite(t *testing.T) { + suite.Run(t, NewRelayerServerSuite(t)) +} From 90d1f5fbc79e1819351ecc82bcbabf05ac52291b Mon Sep 17 00:00:00 2001 From: Daniel Wasserman Date: Mon, 8 Jan 2024 16:56:59 -0600 Subject: [PATCH 09/39] Feat: add /health endpoint --- services/rfq/relayer/relapi/handler.go | 5 +++++ services/rfq/relayer/relapi/server.go | 2 ++ 2 files changed, 7 insertions(+) diff --git a/services/rfq/relayer/relapi/handler.go b/services/rfq/relayer/relapi/handler.go index 61d1dcf603..111869032b 100644 --- a/services/rfq/relayer/relapi/handler.go +++ b/services/rfq/relayer/relapi/handler.go @@ -62,6 +62,11 @@ func (h *Handler) GetQuoteRequestStatusByTxID(c *gin.Context) { const unspecifiedTxHash = "Must specify 'hash' (corresponding to origin tx)" +// GetHealth returns a successful response to signify the API is up and running. +func (h *Handler) GetHealth(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{"status": "ok"}) +} + // GetQuoteRequestStatusByTxHash gets the status of a quote request, given an origin tx hash. func (h *Handler) GetQuoteRequestStatusByTxHash(c *gin.Context) { txHashStr := c.Query("hash") diff --git a/services/rfq/relayer/relapi/server.go b/services/rfq/relayer/relapi/server.go index f3ea197a37..e33e9cb81b 100644 --- a/services/rfq/relayer/relapi/server.go +++ b/services/rfq/relayer/relapi/server.go @@ -83,6 +83,7 @@ func NewRelayerAPI( } const ( + getHealthRoute = "/health" getQuoteStatusByTxHashRoute = "/status" getQuoteStatusByTxIDRoute = "/status/by_tx_id" putRetryRoute = "/retry" @@ -102,6 +103,7 @@ func (r *RelayerAPIServer) Run(ctx context.Context) error { quotesPut.PUT("", h.PutTxRetry) // GET routes without the AuthMiddleware + engine.GET(getHealthRoute, h.GetHealth) engine.GET(getQuoteStatusByTxHashRoute, h.GetQuoteRequestStatusByTxHash) engine.GET(getQuoteStatusByTxIDRoute, h.GetQuoteRequestStatusByTxID) From 8645575dc30adef0b1b7b130099a5feb698b1aec Mon Sep 17 00:00:00 2001 From: Daniel Wasserman Date: Mon, 8 Jan 2024 16:57:12 -0600 Subject: [PATCH 10/39] Feat: add TestGetQuoteRequestByTxHash --- services/rfq/relayer/relapi/server_test.go | 70 +++++++++++++++++++++- 1 file changed, 68 insertions(+), 2 deletions(-) diff --git a/services/rfq/relayer/relapi/server_test.go b/services/rfq/relayer/relapi/server_test.go index a608da722b..b7af6a9ca8 100644 --- a/services/rfq/relayer/relapi/server_test.go +++ b/services/rfq/relayer/relapi/server_test.go @@ -1,14 +1,22 @@ package relapi_test import ( + "context" + "encoding/json" "fmt" + "math/big" "net/http" "strconv" "time" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/crypto" + "github.com/synapsecns/sanguine/core/retry" "github.com/synapsecns/sanguine/ethergo/signer/wallet" + "github.com/synapsecns/sanguine/services/rfq/contracts/fastbridge" + "github.com/synapsecns/sanguine/services/rfq/relayer/relapi" + "github.com/synapsecns/sanguine/services/rfq/relayer/reldb" ) func (c *RelayerServerSuite) TestNewAPIServer() { @@ -22,8 +30,58 @@ func (c *RelayerServerSuite) TestNewAPIServer() { c.GetTestContext().Done() } -func (c *RelayerServerSuite) TestPutAndGetQuote() { +func getTestQuoteRequest(status reldb.QuoteRequestStatus) reldb.QuoteRequest { + txIDRaw := hexutil.Encode(crypto.Keccak256([]byte("test"))) + var txID [32]byte + copy(txID[:], txIDRaw) + return reldb.QuoteRequest{ + OriginTokenDecimals: 6, + DestTokenDecimals: 6, + TransactionID: txID, + Status: status, + Transaction: fastbridge.IFastBridgeBridgeTransaction{ + OriginAmount: big.NewInt(100), + DestAmount: big.NewInt(100), + Deadline: big.NewInt(time.Now().Unix()), + Nonce: big.NewInt(1), + }, + OriginTxHash: common.HexToHash("0x0000000"), + DestTxHash: common.HexToHash("0x0000001"), + } +} + +func (c *RelayerServerSuite) TestGetQuoteRequestByTxHash() { c.startAPIServer() + + // Insert quote request to db + quoteRequest := getTestQuoteRequest(reldb.Seen) + err := c.database.StoreQuoteRequest(c.GetTestContext(), quoteRequest) + c.Require().NoError(err) + + // Fetch the quote request by tx hash + client := &http.Client{} + req, err := http.NewRequestWithContext(c.GetTestContext(), http.MethodGet, fmt.Sprintf("http://localhost:%d/status?hash=%s", c.port, quoteRequest.OriginTxHash), nil) + c.Require().NoError(err) + resp, err := client.Do(req) + c.Require().NoError(err) + defer func() { + err = resp.Body.Close() + c.Require().NoError(err) + }() + c.Equal(http.StatusOK, resp.StatusCode) + + // Compare to expected result + var result relapi.GetQuoteRequestStatusResponse + err = json.NewDecoder(resp.Body).Decode(&result) + c.Require().NoError(err) + expectedResult := relapi.GetQuoteRequestStatusResponse{ + Status: quoteRequest.Status.String(), + TxID: hexutil.Encode(quoteRequest.TransactionID[:]), + OriginTxHash: quoteRequest.OriginTxHash.String(), + DestTxHash: quoteRequest.DestTxHash.String(), + } + c.Equal(expectedResult, result) + c.GetTestContext().Done() } // startAPIServer starts the API server and waits for it to initialize. @@ -32,7 +90,15 @@ func (c *RelayerServerSuite) startAPIServer() { err := c.RelayerAPIServer.Run(c.GetTestContext()) c.Require().NoError(err) }() - time.Sleep(2 * time.Second) // Wait for the server to start. + + // Wait for the server to start + retry.WithBackoff(c.GetTestContext(), func(ctx context.Context) error { + client := &http.Client{} + req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("http://localhost:%d/health", c.port), nil) + c.Require().NoError(err) + _, err = client.Do(req) + return err + }, retry.WithMaxTotalTime(10*time.Second)) } // prepareAuthHeader generates an authorization header using EIP191 signature with the given private key. From 902f4aace855298b10e92be84bbf80600e8d0724 Mon Sep 17 00:00:00 2001 From: Daniel Wasserman Date: Mon, 8 Jan 2024 16:58:05 -0600 Subject: [PATCH 11/39] Fix: use /health in TestNewAPIServer --- services/rfq/relayer/relapi/server_test.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/services/rfq/relayer/relapi/server_test.go b/services/rfq/relayer/relapi/server_test.go index b7af6a9ca8..e6243b52f6 100644 --- a/services/rfq/relayer/relapi/server_test.go +++ b/services/rfq/relayer/relapi/server_test.go @@ -23,10 +23,15 @@ func (c *RelayerServerSuite) TestNewAPIServer() { // Start the API server in a separate goroutine and wait for it to initialize. c.startAPIServer() client := &http.Client{} - req, err := http.NewRequestWithContext(c.GetTestContext(), http.MethodGet, fmt.Sprintf("http://localhost:%d/status", c.port), nil) + req, err := http.NewRequestWithContext(c.GetTestContext(), http.MethodGet, fmt.Sprintf("http://localhost:%d/health", c.port), nil) c.Require().NoError(err) - _, err = client.Do(req) + resp, err := client.Do(req) c.Require().NoError(err) + defer func() { + err = resp.Body.Close() + c.Require().NoError(err) + }() + c.Equal(http.StatusOK, resp.StatusCode) c.GetTestContext().Done() } From 86d01e7189edb56e126c8bf4d5861aa1323e0fa7 Mon Sep 17 00:00:00 2001 From: Daniel Wasserman Date: Mon, 8 Jan 2024 17:01:36 -0600 Subject: [PATCH 12/39] Feat: add TestGetQuoteRequestByTxID --- services/rfq/relayer/relapi/handler.go | 2 +- services/rfq/relayer/relapi/server_test.go | 35 ++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/services/rfq/relayer/relapi/handler.go b/services/rfq/relayer/relapi/handler.go index 111869032b..dfe8d9adb0 100644 --- a/services/rfq/relayer/relapi/handler.go +++ b/services/rfq/relayer/relapi/handler.go @@ -31,7 +31,7 @@ func NewHandler(db reldb.Service, chains map[uint32]*service.Chain) *Handler { // GetQuoteRequestStatusByTxID gets the status of a quote request, given a tx id. func (h *Handler) GetQuoteRequestStatusByTxID(c *gin.Context) { - txIDStr := c.Query("txID") + txIDStr := c.Query("id") if txIDStr == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "Must specify 'txID'"}) return diff --git a/services/rfq/relayer/relapi/server_test.go b/services/rfq/relayer/relapi/server_test.go index e6243b52f6..a99e2d79e6 100644 --- a/services/rfq/relayer/relapi/server_test.go +++ b/services/rfq/relayer/relapi/server_test.go @@ -89,6 +89,41 @@ func (c *RelayerServerSuite) TestGetQuoteRequestByTxHash() { c.GetTestContext().Done() } +func (c *RelayerServerSuite) TestGetQuoteRequestByTxID() { + c.startAPIServer() + + // Insert quote request to db + quoteRequest := getTestQuoteRequest(reldb.Seen) + err := c.database.StoreQuoteRequest(c.GetTestContext(), quoteRequest) + c.Require().NoError(err) + + // Fetch the quote request by tx hash + client := &http.Client{} + txIDStr := hexutil.Encode(quoteRequest.TransactionID[:]) + req, err := http.NewRequestWithContext(c.GetTestContext(), http.MethodGet, fmt.Sprintf("http://localhost:%d/status/by_tx_id?id=%s", c.port, txIDStr), nil) + c.Require().NoError(err) + resp, err := client.Do(req) + c.Require().NoError(err) + defer func() { + err = resp.Body.Close() + c.Require().NoError(err) + }() + c.Equal(http.StatusOK, resp.StatusCode) + + // Compare to expected result + var result relapi.GetQuoteRequestStatusResponse + err = json.NewDecoder(resp.Body).Decode(&result) + c.Require().NoError(err) + expectedResult := relapi.GetQuoteRequestStatusResponse{ + Status: quoteRequest.Status.String(), + TxID: hexutil.Encode(quoteRequest.TransactionID[:]), + OriginTxHash: quoteRequest.OriginTxHash.String(), + DestTxHash: quoteRequest.DestTxHash.String(), + } + c.Equal(expectedResult, result) + c.GetTestContext().Done() +} + // startAPIServer starts the API server and waits for it to initialize. func (c *RelayerServerSuite) startAPIServer() { go func() { From 8689d376ddaff507439f7a77958b07cba5f5e117 Mon Sep 17 00:00:00 2001 From: Daniel Wasserman Date: Mon, 8 Jan 2024 17:02:13 -0600 Subject: [PATCH 13/39] Cleanup: reorder funcs --- services/rfq/relayer/relapi/handler.go | 52 +++++++++++++------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/services/rfq/relayer/relapi/handler.go b/services/rfq/relayer/relapi/handler.go index dfe8d9adb0..3c5c11d07e 100644 --- a/services/rfq/relayer/relapi/handler.go +++ b/services/rfq/relayer/relapi/handler.go @@ -29,23 +29,23 @@ func NewHandler(db reldb.Service, chains map[uint32]*service.Chain) *Handler { } } -// GetQuoteRequestStatusByTxID gets the status of a quote request, given a tx id. -func (h *Handler) GetQuoteRequestStatusByTxID(c *gin.Context) { - txIDStr := c.Query("id") - if txIDStr == "" { - c.JSON(http.StatusBadRequest, gin.H{"error": "Must specify 'txID'"}) - return - } +// GetHealth returns a successful response to signify the API is up and running. +func (h *Handler) GetHealth(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{"status": "ok"}) +} - txIDBytes, err := hexutil.Decode(txIDStr) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid txID"}) +const unspecifiedTxHash = "Must specify 'hash' (corresponding to origin tx)" + +// GetQuoteRequestStatusByTxHash gets the status of a quote request, given an origin tx hash. +func (h *Handler) GetQuoteRequestStatusByTxHash(c *gin.Context) { + txHashStr := c.Query("hash") + if txHashStr == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": unspecifiedTxHash}) return } - var txID [32]byte - copy(txID[:], txIDBytes) - quoteRequest, err := h.db.GetQuoteRequestByID(c, txID) + txHash := common.HexToHash(txHashStr) + quoteRequest, err := h.db.GetQuoteRequestByOriginTxHash(c, txHash) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return @@ -60,23 +60,23 @@ func (h *Handler) GetQuoteRequestStatusByTxID(c *gin.Context) { c.JSON(http.StatusOK, resp) } -const unspecifiedTxHash = "Must specify 'hash' (corresponding to origin tx)" - -// GetHealth returns a successful response to signify the API is up and running. -func (h *Handler) GetHealth(c *gin.Context) { - c.JSON(http.StatusOK, gin.H{"status": "ok"}) -} +// GetQuoteRequestStatusByTxID gets the status of a quote request, given a tx id. +func (h *Handler) GetQuoteRequestStatusByTxID(c *gin.Context) { + txIDStr := c.Query("id") + if txIDStr == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "Must specify 'txID'"}) + return + } -// GetQuoteRequestStatusByTxHash gets the status of a quote request, given an origin tx hash. -func (h *Handler) GetQuoteRequestStatusByTxHash(c *gin.Context) { - txHashStr := c.Query("hash") - if txHashStr == "" { - c.JSON(http.StatusBadRequest, gin.H{"error": unspecifiedTxHash}) + txIDBytes, err := hexutil.Decode(txIDStr) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid txID"}) return } + var txID [32]byte + copy(txID[:], txIDBytes) - txHash := common.HexToHash(txHashStr) - quoteRequest, err := h.db.GetQuoteRequestByOriginTxHash(c, txHash) + quoteRequest, err := h.db.GetQuoteRequestByID(c, txID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return From a7a9cf2e886d0160b62817ea0ade2b0732547720 Mon Sep 17 00:00:00 2001 From: Daniel Wasserman Date: Mon, 8 Jan 2024 17:10:23 -0600 Subject: [PATCH 14/39] WIP: add TestPutTxRetry --- services/rfq/relayer/relapi/server_test.go | 34 ++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/services/rfq/relayer/relapi/server_test.go b/services/rfq/relayer/relapi/server_test.go index a99e2d79e6..afc1385128 100644 --- a/services/rfq/relayer/relapi/server_test.go +++ b/services/rfq/relayer/relapi/server_test.go @@ -124,6 +124,40 @@ func (c *RelayerServerSuite) TestGetQuoteRequestByTxID() { c.GetTestContext().Done() } +func (c *RelayerServerSuite) TestPutTxRetry() { + c.startAPIServer() + + // Insert quote request to db + quoteRequest := getTestQuoteRequest(reldb.Seen) + err := c.database.StoreQuoteRequest(c.GetTestContext(), quoteRequest) + c.Require().NoError(err) + + // Send a retry request + client := &http.Client{} + req, err := http.NewRequestWithContext(c.GetTestContext(), http.MethodPut, fmt.Sprintf("http://localhost:%d/retry?hash=%s", c.port, quoteRequest.OriginTxHash), nil) + c.Require().NoError(err) + resp, err := client.Do(req) + c.Require().NoError(err) + defer func() { + err = resp.Body.Close() + c.Require().NoError(err) + }() + c.Equal(http.StatusOK, resp.StatusCode) + + // Compare to expected result + var result relapi.PutTxRetryResponse + err = json.NewDecoder(resp.Body).Decode(&result) + c.Require().NoError(err) + expectedResult := relapi.PutTxRetryResponse{ + TxID: hexutil.Encode(quoteRequest.TransactionID[:]), + ChainID: quoteRequest.Transaction.DestChainId, + Nonce: uint64(quoteRequest.Transaction.Nonce.Int64()), + GasAmount: "0", + } + c.Equal(expectedResult, result) + c.GetTestContext().Done() +} + // startAPIServer starts the API server and waits for it to initialize. func (c *RelayerServerSuite) startAPIServer() { go func() { From 165f46fdddc477d7571a604d3ccbf9b14600985a Mon Sep 17 00:00:00 2001 From: Daniel Wasserman Date: Mon, 8 Jan 2024 17:12:19 -0600 Subject: [PATCH 15/39] Feat: PutTxRetry -> GetTxRetry, remove auth --- services/rfq/relayer/relapi/handler.go | 6 +- services/rfq/relayer/relapi/model.go | 4 +- services/rfq/relayer/relapi/server.go | 66 ++-------------------- services/rfq/relayer/relapi/server_test.go | 6 +- 4 files changed, 12 insertions(+), 70 deletions(-) diff --git a/services/rfq/relayer/relapi/handler.go b/services/rfq/relayer/relapi/handler.go index 3c5c11d07e..b4a41e3be6 100644 --- a/services/rfq/relayer/relapi/handler.go +++ b/services/rfq/relayer/relapi/handler.go @@ -91,8 +91,8 @@ func (h *Handler) GetQuoteRequestStatusByTxID(c *gin.Context) { c.JSON(http.StatusOK, resp) } -// PutTxRetry retries a transaction based on tx hash. -func (h *Handler) PutTxRetry(c *gin.Context) { +// GetTxRetry retries a transaction based on tx hash. +func (h *Handler) GetTxRetry(c *gin.Context) { txHashStr := c.Query("hash") if txHashStr == "" { c.JSON(http.StatusBadRequest, gin.H{"error": unspecifiedTxHash}) @@ -133,7 +133,7 @@ func (h *Handler) PutTxRetry(c *gin.Context) { return tx, nil }) - resp := PutTxRetryResponse{ + resp := GetTxRetryResponse{ TxID: hexutil.Encode(quoteRequest.TransactionID[:]), ChainID: chainID, Nonce: nonce, diff --git a/services/rfq/relayer/relapi/model.go b/services/rfq/relayer/relapi/model.go index 3f475e6455..721c83d6c0 100644 --- a/services/rfq/relayer/relapi/model.go +++ b/services/rfq/relayer/relapi/model.go @@ -8,8 +8,8 @@ type GetQuoteRequestStatusResponse struct { DestTxHash string `json:"dest_tx_hash"` } -// PutTxRetryResponse contains the schema for a PUT /tx/retry response. -type PutTxRetryResponse struct { +// GetTxRetryResponse contains the schema for a PUT /tx/retry response. +type GetTxRetryResponse struct { TxID string `json:"tx_id"` ChainID uint32 `json:"chain_id"` Nonce uint64 `json:"nonce"` diff --git a/services/rfq/relayer/relapi/server.go b/services/rfq/relayer/relapi/server.go index e33e9cb81b..8f09fa398e 100644 --- a/services/rfq/relayer/relapi/server.go +++ b/services/rfq/relayer/relapi/server.go @@ -3,23 +3,17 @@ package relapi import ( "context" "fmt" - "net/http" - "time" "github.com/ipfs/go-log" "github.com/synapsecns/sanguine/core/ginhelper" "github.com/synapsecns/sanguine/ethergo/submitter" - "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/crypto" "github.com/gin-gonic/gin" "github.com/synapsecns/sanguine/core/metrics" baseServer "github.com/synapsecns/sanguine/core/server" omniClient "github.com/synapsecns/sanguine/services/omnirpc/client" "github.com/synapsecns/sanguine/services/rfq/api/config" - "github.com/synapsecns/sanguine/services/rfq/api/model" - "github.com/synapsecns/sanguine/services/rfq/api/rest" "github.com/synapsecns/sanguine/services/rfq/relayer/listener" "github.com/synapsecns/sanguine/services/rfq/relayer/reldb" "github.com/synapsecns/sanguine/services/rfq/relayer/service" @@ -86,7 +80,7 @@ const ( getHealthRoute = "/health" getQuoteStatusByTxHashRoute = "/status" getQuoteStatusByTxIDRoute = "/status/by_tx_id" - putRetryRoute = "/retry" + getRetryRoute = "/retry" ) var logger = log.Logger("relayer-api") @@ -97,15 +91,11 @@ func (r *RelayerAPIServer) Run(ctx context.Context) error { engine := ginhelper.New(logger) h := NewHandler(r.db, r.chains) - // Apply AuthMiddleware only to the PUT route - quotesPut := engine.Group(putRetryRoute) - quotesPut.Use(r.AuthMiddleware()) - quotesPut.PUT("", h.PutTxRetry) - - // GET routes without the AuthMiddleware + // Assign GET routes engine.GET(getHealthRoute, h.GetHealth) engine.GET(getQuoteStatusByTxHashRoute, h.GetQuoteRequestStatusByTxHash) engine.GET(getQuoteStatusByTxIDRoute, h.GetQuoteRequestStatusByTxID) + engine.GET(getRetryRoute, h.GetTxRetry) r.engine = engine @@ -113,56 +103,8 @@ func (r *RelayerAPIServer) Run(ctx context.Context) error { fmt.Printf("starting api at http://localhost:%s\n", r.cfg.Port) err := connection.ListenAndServe(ctx, fmt.Sprintf(":%s", r.cfg.Port), r.engine) if err != nil { - return fmt.Errorf("could not start rest api server: %w", err) + return fmt.Errorf("could not start relayer api server: %w", err) } return nil } - -// AuthMiddleware is the Gin authentication middleware that authenticates requests using EIP191. -func (r *RelayerAPIServer) AuthMiddleware() gin.HandlerFunc { - return func(c *gin.Context) { - var req model.PutQuoteRequest - if err := c.BindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - c.Abort() - return - } - - chain, ok := r.chains[uint32(req.DestChainID)] - if !ok { - c.JSON(http.StatusBadRequest, gin.H{"msg": "dest chain id not supported"}) - c.Abort() - return - } - - ops := &bind.CallOpts{Context: c} - relayerRole := crypto.Keccak256Hash([]byte("RELAYER_ROLE")) - - // authenticate relayer signature with EIP191 - deadline := time.Now().Unix() - 1000 // TODO: Replace with some type of r.cfg.AuthExpiryDelta - addressRecovered, err := rest.EIP191Auth(c, deadline) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"msg": fmt.Sprintf("unable to authenticate relayer: %v", err)}) - c.Abort() - return - } - - has, err := chain.Bridge.HasRole(ops, relayerRole, addressRecovered) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"msg": "unable to check relayer role on-chain"}) - c.Abort() - return - } else if !has { - c.JSON(http.StatusBadRequest, gin.H{"msg": "q.Relayer not an on-chain relayer"}) - c.Abort() - return - } - - // Log and pass to the next middleware if authentication succeeds - // Store the request in context after binding and validation - c.Set("putRequest", &req) - c.Set("relayerAddr", addressRecovered.Hex()) - c.Next() - } -} diff --git a/services/rfq/relayer/relapi/server_test.go b/services/rfq/relayer/relapi/server_test.go index afc1385128..ed2102d7c1 100644 --- a/services/rfq/relayer/relapi/server_test.go +++ b/services/rfq/relayer/relapi/server_test.go @@ -124,7 +124,7 @@ func (c *RelayerServerSuite) TestGetQuoteRequestByTxID() { c.GetTestContext().Done() } -func (c *RelayerServerSuite) TestPutTxRetry() { +func (c *RelayerServerSuite) TestGetTxRetry() { c.startAPIServer() // Insert quote request to db @@ -145,10 +145,10 @@ func (c *RelayerServerSuite) TestPutTxRetry() { c.Equal(http.StatusOK, resp.StatusCode) // Compare to expected result - var result relapi.PutTxRetryResponse + var result relapi.GetTxRetryResponse err = json.NewDecoder(resp.Body).Decode(&result) c.Require().NoError(err) - expectedResult := relapi.PutTxRetryResponse{ + expectedResult := relapi.GetTxRetryResponse{ TxID: hexutil.Encode(quoteRequest.TransactionID[:]), ChainID: quoteRequest.Transaction.DestChainId, Nonce: uint64(quoteRequest.Transaction.Nonce.Int64()), From c35248152666f94267f3d259cdc5dff739018e67 Mon Sep 17 00:00:00 2001 From: Daniel Wasserman Date: Mon, 8 Jan 2024 17:21:15 -0600 Subject: [PATCH 16/39] WIP: set chain IDs on test request --- services/rfq/relayer/relapi/server_test.go | 66 ++++++++-------------- services/rfq/relayer/relapi/suite_test.go | 33 +++++------ 2 files changed, 40 insertions(+), 59 deletions(-) diff --git a/services/rfq/relayer/relapi/server_test.go b/services/rfq/relayer/relapi/server_test.go index ed2102d7c1..65b8bf4f49 100644 --- a/services/rfq/relayer/relapi/server_test.go +++ b/services/rfq/relayer/relapi/server_test.go @@ -6,14 +6,12 @@ import ( "fmt" "math/big" "net/http" - "strconv" "time" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/crypto" "github.com/synapsecns/sanguine/core/retry" - "github.com/synapsecns/sanguine/ethergo/signer/wallet" "github.com/synapsecns/sanguine/services/rfq/contracts/fastbridge" "github.com/synapsecns/sanguine/services/rfq/relayer/relapi" "github.com/synapsecns/sanguine/services/rfq/relayer/reldb" @@ -35,31 +33,11 @@ func (c *RelayerServerSuite) TestNewAPIServer() { c.GetTestContext().Done() } -func getTestQuoteRequest(status reldb.QuoteRequestStatus) reldb.QuoteRequest { - txIDRaw := hexutil.Encode(crypto.Keccak256([]byte("test"))) - var txID [32]byte - copy(txID[:], txIDRaw) - return reldb.QuoteRequest{ - OriginTokenDecimals: 6, - DestTokenDecimals: 6, - TransactionID: txID, - Status: status, - Transaction: fastbridge.IFastBridgeBridgeTransaction{ - OriginAmount: big.NewInt(100), - DestAmount: big.NewInt(100), - Deadline: big.NewInt(time.Now().Unix()), - Nonce: big.NewInt(1), - }, - OriginTxHash: common.HexToHash("0x0000000"), - DestTxHash: common.HexToHash("0x0000001"), - } -} - func (c *RelayerServerSuite) TestGetQuoteRequestByTxHash() { c.startAPIServer() // Insert quote request to db - quoteRequest := getTestQuoteRequest(reldb.Seen) + quoteRequest := c.getTestQuoteRequest(reldb.Seen) err := c.database.StoreQuoteRequest(c.GetTestContext(), quoteRequest) c.Require().NoError(err) @@ -93,7 +71,7 @@ func (c *RelayerServerSuite) TestGetQuoteRequestByTxID() { c.startAPIServer() // Insert quote request to db - quoteRequest := getTestQuoteRequest(reldb.Seen) + quoteRequest := c.getTestQuoteRequest(reldb.Seen) err := c.database.StoreQuoteRequest(c.GetTestContext(), quoteRequest) c.Require().NoError(err) @@ -128,13 +106,13 @@ func (c *RelayerServerSuite) TestGetTxRetry() { c.startAPIServer() // Insert quote request to db - quoteRequest := getTestQuoteRequest(reldb.Seen) + quoteRequest := c.getTestQuoteRequest(reldb.Seen) err := c.database.StoreQuoteRequest(c.GetTestContext(), quoteRequest) c.Require().NoError(err) // Send a retry request client := &http.Client{} - req, err := http.NewRequestWithContext(c.GetTestContext(), http.MethodPut, fmt.Sprintf("http://localhost:%d/retry?hash=%s", c.port, quoteRequest.OriginTxHash), nil) + req, err := http.NewRequestWithContext(c.GetTestContext(), http.MethodGet, fmt.Sprintf("http://localhost:%d/retry?hash=%s", c.port, quoteRequest.OriginTxHash), nil) c.Require().NoError(err) resp, err := client.Do(req) c.Require().NoError(err) @@ -175,22 +153,24 @@ func (c *RelayerServerSuite) startAPIServer() { }, retry.WithMaxTotalTime(10*time.Second)) } -// prepareAuthHeader generates an authorization header using EIP191 signature with the given private key. -func (c *RelayerServerSuite) prepareAuthHeader(wallet wallet.Wallet) (string, error) { - // Get the current Unix timestamp as a string. - now := strconv.Itoa(int(time.Now().Unix())) - - // Prepare the data to be signed. - data := "\x19Ethereum Signed Message:\n" + strconv.Itoa(len(now)) + now - digest := crypto.Keccak256([]byte(data)) - - // Sign the data with the provided private key. - sig, err := crypto.Sign(digest, wallet.PrivateKey()) - if err != nil { - return "", fmt.Errorf("failed to sign data: %w", err) +func (c *RelayerServerSuite) getTestQuoteRequest(status reldb.QuoteRequestStatus) reldb.QuoteRequest { + txIDRaw := hexutil.Encode(crypto.Keccak256([]byte("test"))) + var txID [32]byte + copy(txID[:], txIDRaw) + return reldb.QuoteRequest{ + OriginTokenDecimals: 6, + DestTokenDecimals: 6, + TransactionID: txID, + Status: status, + Transaction: fastbridge.IFastBridgeBridgeTransaction{ + OriginChainId: c.originChainID, + DestChainId: c.destChainID, + OriginAmount: big.NewInt(100), + DestAmount: big.NewInt(100), + Deadline: big.NewInt(time.Now().Unix()), + Nonce: big.NewInt(1), + }, + OriginTxHash: common.HexToHash("0x0000000"), + DestTxHash: common.HexToHash("0x0000001"), } - signature := hexutil.Encode(sig) - - // Return the combined header value. - return now + ":" + signature, nil } diff --git a/services/rfq/relayer/relapi/suite_test.go b/services/rfq/relayer/relapi/suite_test.go index 1d77b5a7f8..948896fd77 100644 --- a/services/rfq/relayer/relapi/suite_test.go +++ b/services/rfq/relayer/relapi/suite_test.go @@ -34,6 +34,8 @@ type RelayerServerSuite struct { omniRPCClient omniClient.RPCClient omniRPCTestBackends []backends.SimulatedTestBackend testBackends map[uint64]backends.SimulatedTestBackend + originChainID uint32 + destChainID uint32 fastBridgeAddressMap *xsync.MapOf[uint64, common.Address] database reldb.Service cfg config.Config @@ -66,6 +68,9 @@ func (c *RelayerServerSuite) SetupTest() { c.port = uint16(port) c.Require().NoError(err) + c.originChainID = 1 + c.destChainID = 42161 + testConfig := config.Config{ Database: config.DatabaseConfig{ Type: "sqlite", @@ -73,8 +78,8 @@ func (c *RelayerServerSuite) SetupTest() { }, OmniRPCURL: testOmnirpc, Bridges: map[uint32]string{ - 1: ethFastBridgeAddress.Hex(), - 42161: arbFastBridgeAddress.Hex(), + c.originChainID: ethFastBridgeAddress.Hex(), + c.destChainID: arbFastBridgeAddress.Hex(), }, Port: fmt.Sprintf("%d", port), } @@ -95,20 +100,16 @@ func (c *RelayerServerSuite) SetupSuite() { g, _ := errgroup.WithContext(c.GetSuiteContext()) for _, chainID := range chainIDs { - chainID := chainID // capture func literal - g.Go(func() error { - // Setup Anvil backend for the suite to have RPC support - // anvilOpts := anvil.NewAnvilOptionBuilder() - // anvilOpts.SetChainID(chainID) - // anvilOpts.SetBlockTime(1 * time.Second) - // backend := anvil.NewAnvilBackend(c.GetSuiteContext(), c.T(), anvilOpts) - backend := geth.NewEmbeddedBackendForChainID(c.GetSuiteContext(), c.T(), new(big.Int).SetUint64(chainID)) - - // add the backend to the list of backends - c.testBackends[chainID] = backend - c.omniRPCTestBackends = append(c.omniRPCTestBackends, backend) - return nil - }) + // Setup Anvil backend for the suite to have RPC support + // anvilOpts := anvil.NewAnvilOptionBuilder() + // anvilOpts.SetChainID(chainID) + // anvilOpts.SetBlockTime(1 * time.Second) + // backend := anvil.NewAnvilBackend(c.GetSuiteContext(), c.T(), anvilOpts) + backend := geth.NewEmbeddedBackendForChainID(c.GetSuiteContext(), c.T(), new(big.Int).SetUint64(chainID)) + + // add the backend to the list of backends + c.testBackends[chainID] = backend + c.omniRPCTestBackends = append(c.omniRPCTestBackends, backend) } // wait for all backends to be ready From 5e3f0a781fe7e13933c105bff60cd9375ebf2c43 Mon Sep 17 00:00:00 2001 From: Daniel Wasserman Date: Mon, 8 Jan 2024 17:25:30 -0600 Subject: [PATCH 17/39] Fix: set transaction submitter in suite --- services/rfq/relayer/relapi/server_test.go | 2 +- services/rfq/relayer/relapi/suite_test.go | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/services/rfq/relayer/relapi/server_test.go b/services/rfq/relayer/relapi/server_test.go index 65b8bf4f49..f4eb587206 100644 --- a/services/rfq/relayer/relapi/server_test.go +++ b/services/rfq/relayer/relapi/server_test.go @@ -168,7 +168,7 @@ func (c *RelayerServerSuite) getTestQuoteRequest(status reldb.QuoteRequestStatus OriginAmount: big.NewInt(100), DestAmount: big.NewInt(100), Deadline: big.NewInt(time.Now().Unix()), - Nonce: big.NewInt(1), + Nonce: big.NewInt(0), }, OriginTxHash: common.HexToHash("0x0000000"), DestTxHash: common.HexToHash("0x0000001"), diff --git a/services/rfq/relayer/relapi/suite_test.go b/services/rfq/relayer/relapi/suite_test.go index 948896fd77..45fda92224 100644 --- a/services/rfq/relayer/relapi/suite_test.go +++ b/services/rfq/relayer/relapi/suite_test.go @@ -17,7 +17,10 @@ import ( "github.com/synapsecns/sanguine/core/testsuite" "github.com/synapsecns/sanguine/ethergo/backends" "github.com/synapsecns/sanguine/ethergo/backends/geth" + "github.com/synapsecns/sanguine/ethergo/signer/signer/localsigner" "github.com/synapsecns/sanguine/ethergo/signer/wallet" + "github.com/synapsecns/sanguine/ethergo/submitter" + submitterConfig "github.com/synapsecns/sanguine/ethergo/submitter/config" omniClient "github.com/synapsecns/sanguine/services/omnirpc/client" omnirpcHelper "github.com/synapsecns/sanguine/services/omnirpc/testhelper" "github.com/synapsecns/sanguine/services/rfq/api/config" @@ -85,7 +88,13 @@ func (c *RelayerServerSuite) SetupTest() { } c.cfg = testConfig - server, err := relapi.NewRelayerAPI(c.GetTestContext(), c.cfg, c.handler, c.omniRPCClient, c.database, nil) + wall, err := wallet.FromRandom() + c.Require().NoError(err) + signer := localsigner.NewSigner(wall.PrivateKey()) + submitterCfg := &submitterConfig.Config{} + ts := submitter.NewTransactionSubmitter(c.handler, signer, omniRPCClient, c.database.SubmitterDB(), submitterCfg) + + server, err := relapi.NewRelayerAPI(c.GetTestContext(), c.cfg, c.handler, c.omniRPCClient, c.database, ts) c.Require().NoError(err) c.RelayerAPIServer = server } From ee218b2981ff90660f045e3c16908e01b21327b3 Mon Sep 17 00:00:00 2001 From: Daniel Wasserman Date: Mon, 8 Jan 2024 17:31:28 -0600 Subject: [PATCH 18/39] Feat: check for submitted tx in TestGetTxRetry --- services/rfq/relayer/relapi/server_test.go | 6 ++++++ services/rfq/relayer/relapi/suite_test.go | 5 +++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/services/rfq/relayer/relapi/server_test.go b/services/rfq/relayer/relapi/server_test.go index f4eb587206..9503fc089e 100644 --- a/services/rfq/relayer/relapi/server_test.go +++ b/services/rfq/relayer/relapi/server_test.go @@ -12,6 +12,7 @@ import ( "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/crypto" "github.com/synapsecns/sanguine/core/retry" + submitterdb "github.com/synapsecns/sanguine/ethergo/submitter/db" "github.com/synapsecns/sanguine/services/rfq/contracts/fastbridge" "github.com/synapsecns/sanguine/services/rfq/relayer/relapi" "github.com/synapsecns/sanguine/services/rfq/relayer/reldb" @@ -134,6 +135,11 @@ func (c *RelayerServerSuite) TestGetTxRetry() { } c.Equal(expectedResult, result) c.GetTestContext().Done() + + // Verify that a transaction was submitted + status, err := c.database.SubmitterDB().GetNonceStatus(c.GetTestContext(), c.wallet.Address(), big.NewInt(int64(quoteRequest.Transaction.DestChainId)), result.Nonce) + c.Require().NoError(err) + c.Equal(status, submitterdb.Stored) } // startAPIServer starts the API server and waits for it to initialize. diff --git a/services/rfq/relayer/relapi/suite_test.go b/services/rfq/relayer/relapi/suite_test.go index 45fda92224..2860013ef9 100644 --- a/services/rfq/relayer/relapi/suite_test.go +++ b/services/rfq/relayer/relapi/suite_test.go @@ -46,6 +46,7 @@ type RelayerServerSuite struct { handler metrics.Handler RelayerAPIServer *relapi.RelayerAPIServer port uint16 + wallet wallet.Wallet } // NewRelayerServerSuite creates a end-to-end test suite. @@ -88,9 +89,9 @@ func (c *RelayerServerSuite) SetupTest() { } c.cfg = testConfig - wall, err := wallet.FromRandom() + c.wallet, err = wallet.FromRandom() c.Require().NoError(err) - signer := localsigner.NewSigner(wall.PrivateKey()) + signer := localsigner.NewSigner(c.wallet.PrivateKey()) submitterCfg := &submitterConfig.Config{} ts := submitter.NewTransactionSubmitter(c.handler, signer, omniRPCClient, c.database.SubmitterDB(), submitterCfg) From a5b254fd3f85ceeb1720c798f31724d397d8cd19 Mon Sep 17 00:00:00 2001 From: Daniel Wasserman Date: Mon, 8 Jan 2024 17:42:48 -0600 Subject: [PATCH 19/39] Cleanup: add SubmitRelay func to deduplicate code --- services/rfq/relayer/relapi/handler.go | 27 +++------------- services/rfq/relayer/service/handlers.go | 39 +++++++++++++++--------- 2 files changed, 30 insertions(+), 36 deletions(-) diff --git a/services/rfq/relayer/relapi/handler.go b/services/rfq/relayer/relapi/handler.go index b4a41e3be6..444639f953 100644 --- a/services/rfq/relayer/relapi/handler.go +++ b/services/rfq/relayer/relapi/handler.go @@ -2,15 +2,11 @@ package relapi import ( "fmt" - "math/big" "net/http" - "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/ethereum/go-ethereum/core/types" "github.com/gin-gonic/gin" - "github.com/synapsecns/sanguine/core" "github.com/synapsecns/sanguine/services/rfq/relayer/reldb" "github.com/synapsecns/sanguine/services/rfq/relayer/service" ) @@ -113,25 +109,12 @@ func (h *Handler) GetTxRetry(c *gin.Context) { return } - // TODO: this can be deduped with handlers.go code - gasAmount := big.NewInt(0) - if quoteRequest.Transaction.SendChainGas { - gasAmount, err = chain.Bridge.ChainGasAmount(&bind.CallOpts{Context: c}) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("could not get chain gas amount: %s", err.Error())}) - return - } + // `quoteRequest == nil` case should be handled by the db query above + nonce, gasAmount, err := service.SubmitRelay(c, chain, *quoteRequest) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("could not submit relay: %s", err.Error())}) + return } - nonce, err := chain.SubmitTransaction(c, func(transactor *bind.TransactOpts) (tx *types.Transaction, err error) { - transactor.Value = core.CopyBigInt(gasAmount) - - tx, err = chain.Bridge.Relay(transactor, quoteRequest.RawRequest) - if err != nil { - return nil, fmt.Errorf("could not relay: %w", err) - } - - return tx, nil - }) resp := GetTxRetryResponse{ TxID: hexutil.Encode(quoteRequest.TransactionID[:]), diff --git a/services/rfq/relayer/service/handlers.go b/services/rfq/relayer/service/handlers.go index 565998c967..cb2a42c887 100644 --- a/services/rfq/relayer/service/handlers.go +++ b/services/rfq/relayer/service/handlers.go @@ -175,19 +175,38 @@ func (q *QuoteRequestHandler) handleCommitPending(ctx context.Context, span trac // This is the fourth step in the bridge process. Here we submit the relay transaction to the destination chain. // TODO: just to be safe, we should probably check if another relayer has already relayed this. func (q *QuoteRequestHandler) handleCommitConfirmed(ctx context.Context, _ trace.Span, request reldb.QuoteRequest) (err error) { + + err = q.db.UpdateQuoteRequestStatus(ctx, request.TransactionID, reldb.RelayStarted) + + // TODO: store the dest txhash connected to the nonce + nonce, _, err := SubmitRelay(ctx, &q.Dest, request) + if err != nil { + return err + } + _ = nonce + + if err != nil { + return fmt.Errorf("could not update request status: %w", err) + } + return nil +} + +// SubmitRelay submits a relay transaction to the destination chain after evaluating gas amount. +func SubmitRelay(ctx context.Context, chain *Chain, request reldb.QuoteRequest) (uint64, *big.Int, error) { gasAmount := big.NewInt(0) + var err error if request.Transaction.SendChainGas { - gasAmount, err = q.Dest.Bridge.ChainGasAmount(&bind.CallOpts{Context: ctx}) + gasAmount, err = chain.Bridge.ChainGasAmount(&bind.CallOpts{Context: ctx}) if err != nil { - return fmt.Errorf("could not get chain gas amount: %w", err) + return 0, nil, fmt.Errorf("could not get chain gas amount: %w", err) } } - nonce, err := q.Dest.SubmitTransaction(ctx, func(transactor *bind.TransactOpts) (tx *types.Transaction, err error) { + nonce, err := chain.SubmitTransaction(ctx, func(transactor *bind.TransactOpts) (tx *types.Transaction, err error) { transactor.Value = core.CopyBigInt(gasAmount) - tx, err = q.Dest.Bridge.Relay(transactor, request.RawRequest) + tx, err = chain.Bridge.Relay(transactor, request.RawRequest) if err != nil { return nil, fmt.Errorf("could not relay: %w", err) } @@ -195,18 +214,10 @@ func (q *QuoteRequestHandler) handleCommitConfirmed(ctx context.Context, _ trace return tx, nil }) if err != nil { - return fmt.Errorf("could not submit transaction: %w", err) + return 0, nil, fmt.Errorf("could not submit transaction: %w", err) } - err = q.db.UpdateQuoteRequestStatus(ctx, request.TransactionID, reldb.RelayStarted) - - // TODO: store the dest txhash connected to the nonce - _ = nonce - - if err != nil { - return fmt.Errorf("could not update request status: %w", err) - } - return nil + return nonce, gasAmount, nil } // handleRelayStarted handles the relay started status and marks the relay as completed. From b99e4fa7076efd0e586b1d034ebac6d4a39f4a46 Mon Sep 17 00:00:00 2001 From: Daniel Wasserman Date: Mon, 8 Jan 2024 18:09:19 -0600 Subject: [PATCH 20/39] Cleanup: lints --- services/rfq/relayer/relapi/server.go | 1 + services/rfq/relayer/relapi/server_test.go | 9 +++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/services/rfq/relayer/relapi/server.go b/services/rfq/relayer/relapi/server.go index 8f09fa398e..6e12d3e9e3 100644 --- a/services/rfq/relayer/relapi/server.go +++ b/services/rfq/relayer/relapi/server.go @@ -1,3 +1,4 @@ +// Package relapi provides RESTful API services for the RFQ relayer package relapi import ( diff --git a/services/rfq/relayer/relapi/server_test.go b/services/rfq/relayer/relapi/server_test.go index 9503fc089e..abdf993a8f 100644 --- a/services/rfq/relayer/relapi/server_test.go +++ b/services/rfq/relayer/relapi/server_test.go @@ -150,13 +150,18 @@ func (c *RelayerServerSuite) startAPIServer() { }() // Wait for the server to start - retry.WithBackoff(c.GetTestContext(), func(ctx context.Context) error { + err := retry.WithBackoff(c.GetTestContext(), func(ctx context.Context) error { client := &http.Client{} req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("http://localhost:%d/health", c.port), nil) c.Require().NoError(err) - _, err = client.Do(req) + resp, err := client.Do(req) + defer func() { + err = resp.Body.Close() + c.Require().NoError(err) + }() return err }, retry.WithMaxTotalTime(10*time.Second)) + c.Require().NoError(err) } func (c *RelayerServerSuite) getTestQuoteRequest(status reldb.QuoteRequestStatus) reldb.QuoteRequest { From 27b7998800f784bd0ad63e7e9935a66736a0af42 Mon Sep 17 00:00:00 2001 From: Daniel Wasserman Date: Mon, 8 Jan 2024 19:41:18 -0600 Subject: [PATCH 21/39] Cleanup: lints --- services/rfq/relayer/relapi/handler.go | 2 +- services/rfq/relayer/relapi/server_test.go | 2 +- services/rfq/relayer/service/handlers.go | 4 +++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/services/rfq/relayer/relapi/handler.go b/services/rfq/relayer/relapi/handler.go index 444639f953..762f4f7b67 100644 --- a/services/rfq/relayer/relapi/handler.go +++ b/services/rfq/relayer/relapi/handler.go @@ -103,7 +103,7 @@ func (h *Handler) GetTxRetry(c *gin.Context) { } chainID := quoteRequest.Transaction.DestChainId - chain, ok := h.chains[uint32(chainID)] + chain, ok := h.chains[chainID] if !ok { c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("No contract found for chain: %d", chainID)}) return diff --git a/services/rfq/relayer/relapi/server_test.go b/services/rfq/relayer/relapi/server_test.go index abdf993a8f..e18746f9d9 100644 --- a/services/rfq/relayer/relapi/server_test.go +++ b/services/rfq/relayer/relapi/server_test.go @@ -159,7 +159,7 @@ func (c *RelayerServerSuite) startAPIServer() { err = resp.Body.Close() c.Require().NoError(err) }() - return err + return fmt.Errorf("server not ready: %w", err) }, retry.WithMaxTotalTime(10*time.Second)) c.Require().NoError(err) } diff --git a/services/rfq/relayer/service/handlers.go b/services/rfq/relayer/service/handlers.go index cb2a42c887..df6862c924 100644 --- a/services/rfq/relayer/service/handlers.go +++ b/services/rfq/relayer/service/handlers.go @@ -175,8 +175,10 @@ func (q *QuoteRequestHandler) handleCommitPending(ctx context.Context, span trac // This is the fourth step in the bridge process. Here we submit the relay transaction to the destination chain. // TODO: just to be safe, we should probably check if another relayer has already relayed this. func (q *QuoteRequestHandler) handleCommitConfirmed(ctx context.Context, _ trace.Span, request reldb.QuoteRequest) (err error) { - err = q.db.UpdateQuoteRequestStatus(ctx, request.TransactionID, reldb.RelayStarted) + if err != nil { + return fmt.Errorf("could not update quote request status: %w", err) + } // TODO: store the dest txhash connected to the nonce nonce, _, err := SubmitRelay(ctx, &q.Dest, request) From 32db813cb6c7af701adc26f87f114fe68ae807fd Mon Sep 17 00:00:00 2001 From: Daniel Wasserman Date: Tue, 9 Jan 2024 00:17:20 -0600 Subject: [PATCH 22/39] Fix: bump server start timeout --- services/rfq/relayer/relapi/server_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/rfq/relayer/relapi/server_test.go b/services/rfq/relayer/relapi/server_test.go index e18746f9d9..84ff47ac88 100644 --- a/services/rfq/relayer/relapi/server_test.go +++ b/services/rfq/relayer/relapi/server_test.go @@ -160,7 +160,7 @@ func (c *RelayerServerSuite) startAPIServer() { c.Require().NoError(err) }() return fmt.Errorf("server not ready: %w", err) - }, retry.WithMaxTotalTime(10*time.Second)) + }, retry.WithMaxTotalTime(60*time.Second)) c.Require().NoError(err) } From 833fca4c2a3c70998ad83577f733bf4472595ac9 Mon Sep 17 00:00:00 2001 From: Daniel Wasserman Date: Tue, 9 Jan 2024 10:27:54 -0600 Subject: [PATCH 23/39] Lint: disable depguard --- .golangci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.golangci.yml b/.golangci.yml index 478786580d..0097498b03 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -92,6 +92,7 @@ linters: - nonamedreturns - contextcheck - nosnakecase + - depguard fast: false issues: From acd3f44295798c741afbc096ae6caf928b025c94 Mon Sep 17 00:00:00 2001 From: Daniel Wasserman Date: Tue, 9 Jan 2024 10:31:44 -0600 Subject: [PATCH 24/39] Fix: check nil tx in TestListenForEvents --- services/rfq/relayer/listener/listener_test.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/services/rfq/relayer/listener/listener_test.go b/services/rfq/relayer/listener/listener_test.go index cf7031063c..fe5ac62687 100644 --- a/services/rfq/relayer/listener/listener_test.go +++ b/services/rfq/relayer/listener/listener_test.go @@ -2,13 +2,14 @@ package listener_test import ( "context" + "math/big" + "sync" + "github.com/brianvoe/gofakeit/v6" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/synapsecns/sanguine/services/rfq/relayer/listener" - "math/big" - "sync" ) func (l *ListenerTestSuite) TestListenForEvents() { @@ -27,11 +28,13 @@ func (l *ListenerTestSuite) TestListenForEvents() { txID := [32]byte(crypto.Keccak256(testAddress.Bytes())) bridgeRequestTX, err := handle.MockBridgeRequestRaw(auth.TransactOpts, txID, testAddress, []byte(gofakeit.Sentence(10))) l.NoError(err) + l.NotNil(bridgeRequestTX) l.backend.WaitForConfirmation(l.GetTestContext(), bridgeRequestTX) bridgeResponseTX, err := handle.MockBridgeRelayer(auth.TransactOpts, txID, testAddress, testAddress, testAddress, new(big.Int).SetUint64(gofakeit.Uint64()), new(big.Int).SetUint64(gofakeit.Uint64())) l.NoError(err) + l.NotNil(bridgeResponseTX) l.backend.WaitForConfirmation(l.GetTestContext(), bridgeResponseTX) }(i) } From aa743d24f39a671e4c3cd5b4200dbc92ba073931 Mon Sep 17 00:00:00 2001 From: Daniel Wasserman Date: Tue, 9 Jan 2024 10:50:41 -0600 Subject: [PATCH 25/39] Cleanup: use itoa instead of sprintf --- services/rfq/relayer/relapi/suite_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/rfq/relayer/relapi/suite_test.go b/services/rfq/relayer/relapi/suite_test.go index 2860013ef9..13a2ac0562 100644 --- a/services/rfq/relayer/relapi/suite_test.go +++ b/services/rfq/relayer/relapi/suite_test.go @@ -3,6 +3,7 @@ package relapi_test import ( "fmt" "math/big" + "strconv" "testing" "github.com/Flaque/filet" @@ -85,7 +86,7 @@ func (c *RelayerServerSuite) SetupTest() { c.originChainID: ethFastBridgeAddress.Hex(), c.destChainID: arbFastBridgeAddress.Hex(), }, - Port: fmt.Sprintf("%d", port), + Port: strconv.Itoa(port), } c.cfg = testConfig From 831c50d86bb262bac30e642c9ca0a1477e611bce Mon Sep 17 00:00:00 2001 From: Daniel Wasserman Date: Tue, 9 Jan 2024 12:01:31 -0600 Subject: [PATCH 26/39] [goreleaser] Bump timeout on server startup --- services/rfq/relayer/relapi/server_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/rfq/relayer/relapi/server_test.go b/services/rfq/relayer/relapi/server_test.go index 84ff47ac88..a740db7dce 100644 --- a/services/rfq/relayer/relapi/server_test.go +++ b/services/rfq/relayer/relapi/server_test.go @@ -160,7 +160,7 @@ func (c *RelayerServerSuite) startAPIServer() { c.Require().NoError(err) }() return fmt.Errorf("server not ready: %w", err) - }, retry.WithMaxTotalTime(60*time.Second)) + }, retry.WithMaxTotalTime(120*time.Second)) c.Require().NoError(err) } From 1cca179d16927c4b6b66184932bf3bc2245c2864 Mon Sep 17 00:00:00 2001 From: Daniel Wasserman Date: Tue, 9 Jan 2024 13:23:45 -0600 Subject: [PATCH 27/39] Fix: server_test --- services/rfq/relayer/relapi/server_test.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/services/rfq/relayer/relapi/server_test.go b/services/rfq/relayer/relapi/server_test.go index a740db7dce..2d1551f852 100644 --- a/services/rfq/relayer/relapi/server_test.go +++ b/services/rfq/relayer/relapi/server_test.go @@ -156,11 +156,13 @@ func (c *RelayerServerSuite) startAPIServer() { c.Require().NoError(err) resp, err := client.Do(req) defer func() { - err = resp.Body.Close() - c.Require().NoError(err) + resp.Body.Close() }() - return fmt.Errorf("server not ready: %w", err) - }, retry.WithMaxTotalTime(120*time.Second)) + if err != nil { + return fmt.Errorf("server not ready: %w", err) + } + return nil + }, retry.WithMaxTotalTime(60*time.Second)) c.Require().NoError(err) } From f42f2ce100ac8bd919b6b471deefdfdb044a9897 Mon Sep 17 00:00:00 2001 From: Daniel Wasserman Date: Tue, 9 Jan 2024 16:22:13 -0600 Subject: [PATCH 28/39] [goreleaser] Cleanup: lint --- services/rfq/relayer/relapi/server_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/rfq/relayer/relapi/server_test.go b/services/rfq/relayer/relapi/server_test.go index 2d1551f852..713dc3600d 100644 --- a/services/rfq/relayer/relapi/server_test.go +++ b/services/rfq/relayer/relapi/server_test.go @@ -156,7 +156,8 @@ func (c *RelayerServerSuite) startAPIServer() { c.Require().NoError(err) resp, err := client.Do(req) defer func() { - resp.Body.Close() + closeErr := resp.Body.Close() + c.NoError(closeErr) }() if err != nil { return fmt.Errorf("server not ready: %w", err) From dbed1e3533c9693c133387883aad5db044a672a0 Mon Sep 17 00:00:00 2001 From: Daniel Wasserman Date: Tue, 9 Jan 2024 17:07:51 -0600 Subject: [PATCH 29/39] WIP: add relayer api config --- services/rfq/relayer/relapi/server.go | 16 ++++++++-------- services/rfq/relayer/relconfig/config.go | 9 +++++++++ services/rfq/relayer/service/relayer.go | 16 ++++++++++++++++ 3 files changed, 33 insertions(+), 8 deletions(-) diff --git a/services/rfq/relayer/relapi/server.go b/services/rfq/relayer/relapi/server.go index 6e12d3e9e3..1be5228d00 100644 --- a/services/rfq/relayer/relapi/server.go +++ b/services/rfq/relayer/relapi/server.go @@ -14,8 +14,8 @@ import ( "github.com/synapsecns/sanguine/core/metrics" baseServer "github.com/synapsecns/sanguine/core/server" omniClient "github.com/synapsecns/sanguine/services/omnirpc/client" - "github.com/synapsecns/sanguine/services/rfq/api/config" "github.com/synapsecns/sanguine/services/rfq/relayer/listener" + "github.com/synapsecns/sanguine/services/rfq/relayer/relconfig" "github.com/synapsecns/sanguine/services/rfq/relayer/reldb" "github.com/synapsecns/sanguine/services/rfq/relayer/service" ) @@ -23,7 +23,7 @@ import ( // RelayerAPIServer is a struct that holds the configuration, database connection, gin engine, RPC client, metrics handler, and fast bridge contracts. // It is used to initialize and run the API server. type RelayerAPIServer struct { - cfg config.Config + cfg relconfig.Config db reldb.Service engine *gin.Engine handler metrics.Handler @@ -34,7 +34,7 @@ type RelayerAPIServer struct { // It is used to initialize and run the API server. func NewRelayerAPI( ctx context.Context, - cfg config.Config, + cfg relconfig.Config, handler metrics.Handler, omniRPCClient omniClient.RPCClient, store reldb.Service, @@ -54,16 +54,16 @@ func NewRelayerAPI( } chains := make(map[uint32]*service.Chain) - for chainID, bridge := range cfg.Bridges { + for chainID, chainCfg := range cfg.Chains { chainClient, err := omniRPCClient.GetChainClient(ctx, int(chainID)) if err != nil { return nil, fmt.Errorf("could not create omnirpc client: %w", err) } - chainListener, err := listener.NewChainListener(chainClient, store, common.HexToAddress(bridge), handler) + chainListener, err := listener.NewChainListener(chainClient, store, common.HexToAddress(chainCfg.Bridge), handler) if err != nil { return nil, fmt.Errorf("could not get chain listener: %w", err) } - chains[chainID], err = service.NewChain(ctx, chainClient, common.HexToAddress(bridge), chainListener, submitter) + chains[uint32(chainID)], err = service.NewChain(ctx, chainClient, common.HexToAddress(chainCfg.Bridge), chainListener, submitter) if err != nil { return nil, fmt.Errorf("could not create chain: %w", err) } @@ -101,8 +101,8 @@ func (r *RelayerAPIServer) Run(ctx context.Context) error { r.engine = engine connection := baseServer.Server{} - fmt.Printf("starting api at http://localhost:%s\n", r.cfg.Port) - err := connection.ListenAndServe(ctx, fmt.Sprintf(":%s", r.cfg.Port), r.engine) + fmt.Printf("starting api at http://localhost:%s\n", r.cfg.APIConfig.Port) + err := connection.ListenAndServe(ctx, fmt.Sprintf(":%s", r.cfg.APIConfig.Port), r.engine) if err != nil { return fmt.Errorf("could not start relayer api server: %w", err) } diff --git a/services/rfq/relayer/relconfig/config.go b/services/rfq/relayer/relconfig/config.go index 31e3d79ab0..06962b4efd 100644 --- a/services/rfq/relayer/relconfig/config.go +++ b/services/rfq/relayer/relconfig/config.go @@ -35,6 +35,8 @@ type Config struct { SubmitterConfig submitterConfig.Config `yaml:"submitter_config"` // FeePricer is the fee pricer config. FeePricer FeePricerConfig `yaml:"fee_pricer"` + // APIConfig is the relayer API config. + APIConfig APIConfig `yaml:"api_config"` } // ChainConfig represents the configuration for a chain. @@ -65,6 +67,13 @@ type DatabaseConfig struct { DSN string `yaml:"dsn"` // Data Source Name } +// APIConfig is the configuration for the relayer API server. +type APIConfig struct { + Database DatabaseConfig `yaml:"database"` + OmniRPCURL string `yaml:"omnirpc_url"` + Port string `yaml:"port"` +} + // FeePricerConfig represents the configuration for the fee pricer. type FeePricerConfig struct { // OriginGasEstimate is the gas required to execute prove + claim transactions on origin chain. diff --git a/services/rfq/relayer/service/relayer.go b/services/rfq/relayer/service/relayer.go index 152fb496d7..ba6e98836e 100644 --- a/services/rfq/relayer/service/relayer.go +++ b/services/rfq/relayer/service/relayer.go @@ -19,6 +19,7 @@ import ( "github.com/synapsecns/sanguine/services/rfq/relayer/listener" "github.com/synapsecns/sanguine/services/rfq/relayer/pricer" "github.com/synapsecns/sanguine/services/rfq/relayer/quoter" + "github.com/synapsecns/sanguine/services/rfq/relayer/relapi" "github.com/synapsecns/sanguine/services/rfq/relayer/relconfig" "github.com/synapsecns/sanguine/services/rfq/relayer/reldb" "github.com/synapsecns/sanguine/services/rfq/relayer/reldb/connect" @@ -32,6 +33,7 @@ type Relayer struct { db reldb.Service client omnirpcClient.RPCClient chainListeners map[int]listener.ContractListener + apiServer *relapi.RelayerAPIServer inventory inventory.Manager quoter quoter.Quoter submitter submitter.TransactionSubmitter @@ -94,6 +96,11 @@ func NewRelayer(ctx context.Context, metricHandler metrics.Handler, cfg relconfi sm := submitter.NewTransactionSubmitter(metricHandler, sg, omniClient, store.SubmitterDB(), &cfg.SubmitterConfig) + apiServer, err := relapi.NewRelayerAPI(ctx, cfg, metricHandler, omniClient, store, sm) + if err != nil { + return nil, fmt.Errorf("could not get api server: %w", err) + } + cache := ttlcache.New[common.Hash, bool](ttlcache.WithTTL[common.Hash, bool](time.Second * 30)) rel := Relayer{ db: store, @@ -106,6 +113,7 @@ func NewRelayer(ctx context.Context, metricHandler metrics.Handler, cfg relconfi submitter: sm, signer: sg, chainListeners: chainListeners, + apiServer: apiServer, } return &rel, nil } @@ -178,6 +186,14 @@ func (r *Relayer) Start(ctx context.Context) error { return nil }) + g.Go(func() error { + err := r.apiServer.Run(ctx) + if err != nil { + return fmt.Errorf("could not start api server: %w", err) + } + return nil + }) + err = g.Wait() if err != nil { return fmt.Errorf("could not start: %w", err) From 2c57477dd0d14be902afe301ccc16848f20b1315 Mon Sep 17 00:00:00 2001 From: Daniel Wasserman Date: Tue, 9 Jan 2024 17:45:59 -0600 Subject: [PATCH 30/39] WIP: add new chain pkg, add APIConfig section for relayer api --- services/rfq/e2e/rfq_test.go | 15 +-- services/rfq/e2e/setup_test.go | 12 ++- services/rfq/relayer/chain/chain.go | 91 +++++++++++++++++++ services/rfq/relayer/relapi/handler.go | 8 +- services/rfq/relayer/relapi/server.go | 8 +- services/rfq/relayer/relapi/suite_test.go | 28 +++--- services/rfq/relayer/relconfig/config.go | 2 + services/rfq/relayer/service/handlers.go | 34 +------ services/rfq/relayer/service/statushandler.go | 69 +------------- 9 files changed, 143 insertions(+), 124 deletions(-) create mode 100644 services/rfq/relayer/chain/chain.go diff --git a/services/rfq/e2e/rfq_test.go b/services/rfq/e2e/rfq_test.go index 1f216f873d..8648fcf5da 100644 --- a/services/rfq/e2e/rfq_test.go +++ b/services/rfq/e2e/rfq_test.go @@ -27,13 +27,14 @@ type IntegrationSuite struct { originBackend backends.SimulatedTestBackend destBackend backends.SimulatedTestBackend //omniserver is the omnirpc server address - omniServer string - omniClient omnirpcClient.RPCClient - metrics metrics.Handler - apiServer string - relayer *service.Relayer - relayerWallet wallet.Wallet - userWallet wallet.Wallet + omniServer string + omniClient omnirpcClient.RPCClient + metrics metrics.Handler + apiServer string + relayerApiServer string + relayer *service.Relayer + relayerWallet wallet.Wallet + userWallet wallet.Wallet } func NewIntegrationSuite(tb testing.TB) *IntegrationSuite { diff --git a/services/rfq/e2e/setup_test.go b/services/rfq/e2e/setup_test.go index 7dcbd04a85..b92bea5dc4 100644 --- a/services/rfq/e2e/setup_test.go +++ b/services/rfq/e2e/setup_test.go @@ -189,6 +189,8 @@ func (i *IntegrationSuite) setupRelayer() { wg.Wait() // construct the config + relayerApiPort, err := freeport.GetFreePort() + i.NoError(err) dsn := filet.TmpDir(i.T(), "") cfg := relconfig.Config{ // generated ex-post facto @@ -229,6 +231,15 @@ func (i *IntegrationSuite) setupRelayer() { Type: signerConfig.FileType.String(), File: filet.TmpFile(i.T(), "", i.relayerWallet.PrivateKeyHex()).Name(), }, + RelayerAPIURL: fmt.Sprintf("http://localhost:%d", relayerApiPort), + APIConfig: relconfig.APIConfig{ + Database: relconfig.DatabaseConfig{ + Type: dbcommon.Sqlite.String(), + DSN: dsn, + }, + OmniRPCURL: i.omniServer, + Port: strconv.Itoa(relayerApiPort), + }, FeePricer: relconfig.FeePricerConfig{ GasPriceCacheTTLSeconds: 60, TokenPriceCacheTTLSeconds: 60, @@ -277,7 +288,6 @@ func (i *IntegrationSuite) setupRelayer() { } // TODO: good chance we wanna leave actually starting this up to the indiividual test. - var err error i.relayer, err = service.NewRelayer(i.GetTestContext(), i.metrics, cfg) i.NoError(err) go func() { diff --git a/services/rfq/relayer/chain/chain.go b/services/rfq/relayer/chain/chain.go new file mode 100644 index 0000000000..13c9585473 --- /dev/null +++ b/services/rfq/relayer/chain/chain.go @@ -0,0 +1,91 @@ +// Package chain defines the interface for interacting with a blockchain. +package chain + +import ( + "context" + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/synapsecns/sanguine/core" + "github.com/synapsecns/sanguine/ethergo/client" + "github.com/synapsecns/sanguine/ethergo/submitter" + "github.com/synapsecns/sanguine/services/rfq/contracts/fastbridge" + "github.com/synapsecns/sanguine/services/rfq/relayer/listener" + "github.com/synapsecns/sanguine/services/rfq/relayer/reldb" +) + +// Chain is a chain helper for relayer. +// lowercase fields are private, uppercase are public. +// the plan is to move this out of relayer which is when this distinction will matter. +type Chain struct { + ChainID uint32 + Bridge *fastbridge.FastBridgeRef + Client client.EVM + Confirmations uint64 + listener listener.ContractListener + submitter submitter.TransactionSubmitter +} + +// NewChain creates a new chain. +func NewChain(ctx context.Context, chainClient client.EVM, addr common.Address, chainListener listener.ContractListener, ts submitter.TransactionSubmitter) (*Chain, error) { + bridge, err := fastbridge.NewFastBridgeRef(addr, chainClient) + if err != nil { + return nil, fmt.Errorf("could not create bridge contract: %w", err) + } + chainID, err := chainClient.ChainID(ctx) + if err != nil { + return nil, fmt.Errorf("could not get chain id: %w", err) + } + return &Chain{ + ChainID: uint32(chainID.Int64()), + Bridge: bridge, + Client: chainClient, + // TODO: configure + Confirmations: 1, + listener: chainListener, + submitter: ts, + }, nil +} + +// SubmitTransaction submits a transaction to the chain. +func (c Chain) SubmitTransaction(ctx context.Context, call submitter.ContractCallType) (nonce uint64, _ error) { + //nolint: wrapcheck + return c.submitter.SubmitTransaction(ctx, big.NewInt(int64(c.ChainID)), call) +} + +// LatestBlock returns the latest block. +func (c Chain) LatestBlock() uint64 { + return c.listener.LatestBlock() +} + +// SubmitRelay submits a relay transaction to the destination chain after evaluating gas amount. +func (c Chain) SubmitRelay(ctx context.Context, request reldb.QuoteRequest) (uint64, *big.Int, error) { + gasAmount := big.NewInt(0) + var err error + + if request.Transaction.SendChainGas { + gasAmount, err = c.Bridge.ChainGasAmount(&bind.CallOpts{Context: ctx}) + if err != nil { + return 0, nil, fmt.Errorf("could not get chain gas amount: %w", err) + } + } + + nonce, err := c.SubmitTransaction(ctx, func(transactor *bind.TransactOpts) (tx *types.Transaction, err error) { + transactor.Value = core.CopyBigInt(gasAmount) + + tx, err = c.Bridge.Relay(transactor, request.RawRequest) + if err != nil { + return nil, fmt.Errorf("could not relay: %w", err) + } + + return tx, nil + }) + if err != nil { + return 0, nil, fmt.Errorf("could not submit transaction: %w", err) + } + + return nonce, gasAmount, nil +} diff --git a/services/rfq/relayer/relapi/handler.go b/services/rfq/relayer/relapi/handler.go index 762f4f7b67..2182db44e8 100644 --- a/services/rfq/relayer/relapi/handler.go +++ b/services/rfq/relayer/relapi/handler.go @@ -7,18 +7,18 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/gin-gonic/gin" + "github.com/synapsecns/sanguine/services/rfq/relayer/chain" "github.com/synapsecns/sanguine/services/rfq/relayer/reldb" - "github.com/synapsecns/sanguine/services/rfq/relayer/service" ) // Handler is the REST API handler. type Handler struct { db reldb.Service - chains map[uint32]*service.Chain + chains map[uint32]*chain.Chain } // NewHandler creates a new REST API handler. -func NewHandler(db reldb.Service, chains map[uint32]*service.Chain) *Handler { +func NewHandler(db reldb.Service, chains map[uint32]*chain.Chain) *Handler { return &Handler{ db: db, // Store the database connection in the handler chains: chains, @@ -110,7 +110,7 @@ func (h *Handler) GetTxRetry(c *gin.Context) { } // `quoteRequest == nil` case should be handled by the db query above - nonce, gasAmount, err := service.SubmitRelay(c, chain, *quoteRequest) + nonce, gasAmount, err := chain.SubmitRelay(c, *quoteRequest) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("could not submit relay: %s", err.Error())}) return diff --git a/services/rfq/relayer/relapi/server.go b/services/rfq/relayer/relapi/server.go index 1be5228d00..ab210481f7 100644 --- a/services/rfq/relayer/relapi/server.go +++ b/services/rfq/relayer/relapi/server.go @@ -14,10 +14,10 @@ import ( "github.com/synapsecns/sanguine/core/metrics" baseServer "github.com/synapsecns/sanguine/core/server" omniClient "github.com/synapsecns/sanguine/services/omnirpc/client" + "github.com/synapsecns/sanguine/services/rfq/relayer/chain" "github.com/synapsecns/sanguine/services/rfq/relayer/listener" "github.com/synapsecns/sanguine/services/rfq/relayer/relconfig" "github.com/synapsecns/sanguine/services/rfq/relayer/reldb" - "github.com/synapsecns/sanguine/services/rfq/relayer/service" ) // RelayerAPIServer is a struct that holds the configuration, database connection, gin engine, RPC client, metrics handler, and fast bridge contracts. @@ -27,7 +27,7 @@ type RelayerAPIServer struct { db reldb.Service engine *gin.Engine handler metrics.Handler - chains map[uint32]*service.Chain + chains map[uint32]*chain.Chain } // NewRelayerAPI holds the configuration, database connection, gin engine, RPC client, metrics handler, and fast bridge contracts. @@ -53,7 +53,7 @@ func NewRelayerAPI( return nil, fmt.Errorf("store is nil") } - chains := make(map[uint32]*service.Chain) + chains := make(map[uint32]*chain.Chain) for chainID, chainCfg := range cfg.Chains { chainClient, err := omniRPCClient.GetChainClient(ctx, int(chainID)) if err != nil { @@ -63,7 +63,7 @@ func NewRelayerAPI( if err != nil { return nil, fmt.Errorf("could not get chain listener: %w", err) } - chains[uint32(chainID)], err = service.NewChain(ctx, chainClient, common.HexToAddress(chainCfg.Bridge), chainListener, submitter) + chains[uint32(chainID)], err = chain.NewChain(ctx, chainClient, common.HexToAddress(chainCfg.Bridge), chainListener, submitter) if err != nil { return nil, fmt.Errorf("could not create chain: %w", err) } diff --git a/services/rfq/relayer/relapi/suite_test.go b/services/rfq/relayer/relapi/suite_test.go index 13a2ac0562..d07276c146 100644 --- a/services/rfq/relayer/relapi/suite_test.go +++ b/services/rfq/relayer/relapi/suite_test.go @@ -24,9 +24,9 @@ import ( submitterConfig "github.com/synapsecns/sanguine/ethergo/submitter/config" omniClient "github.com/synapsecns/sanguine/services/omnirpc/client" omnirpcHelper "github.com/synapsecns/sanguine/services/omnirpc/testhelper" - "github.com/synapsecns/sanguine/services/rfq/api/config" "github.com/synapsecns/sanguine/services/rfq/contracts/fastbridge" "github.com/synapsecns/sanguine/services/rfq/relayer/relapi" + "github.com/synapsecns/sanguine/services/rfq/relayer/relconfig" "github.com/synapsecns/sanguine/services/rfq/relayer/reldb" "github.com/synapsecns/sanguine/services/rfq/relayer/reldb/connect" "golang.org/x/sync/errgroup" @@ -42,7 +42,7 @@ type RelayerServerSuite struct { destChainID uint32 fastBridgeAddressMap *xsync.MapOf[uint64, common.Address] database reldb.Service - cfg config.Config + cfg relconfig.Config testWallet wallet.Wallet handler metrics.Handler RelayerAPIServer *relapi.RelayerAPIServer @@ -76,17 +76,23 @@ func (c *RelayerServerSuite) SetupTest() { c.originChainID = 1 c.destChainID = 42161 - testConfig := config.Config{ - Database: config.DatabaseConfig{ - Type: "sqlite", - DSN: filet.TmpFile(c.T(), "", "").Name(), + testConfig := relconfig.Config{ + Chains: map[int]relconfig.ChainConfig{ + int(c.originChainID): { + Bridge: ethFastBridgeAddress.Hex(), + }, + int(c.destChainID): { + Bridge: arbFastBridgeAddress.Hex(), + }, }, - OmniRPCURL: testOmnirpc, - Bridges: map[uint32]string{ - c.originChainID: ethFastBridgeAddress.Hex(), - c.destChainID: arbFastBridgeAddress.Hex(), + APIConfig: relconfig.APIConfig{ + Database: relconfig.DatabaseConfig{ + Type: "sqlite", + DSN: filet.TmpFile(c.T(), "", "").Name(), + }, + OmniRPCURL: testOmnirpc, + Port: strconv.Itoa(port), }, - Port: strconv.Itoa(port), } c.cfg = testConfig diff --git a/services/rfq/relayer/relconfig/config.go b/services/rfq/relayer/relconfig/config.go index 06962b4efd..054420dc21 100644 --- a/services/rfq/relayer/relconfig/config.go +++ b/services/rfq/relayer/relconfig/config.go @@ -25,6 +25,8 @@ type Config struct { OmniRPCURL string `yaml:"omnirpc_url"` // RfqAPIURL is the URL of the RFQ API. RfqAPIURL string `yaml:"rfq_url"` + // RelayerAPIURL is the URL of the relayer API. + RelayerAPIURL string `yaml:"relayer_url"` // Database is the database config. Database DatabaseConfig `yaml:"database"` // QuotableTokens is a map of token -> list of quotable tokens. diff --git a/services/rfq/relayer/service/handlers.go b/services/rfq/relayer/service/handlers.go index df6862c924..6d82a264e3 100644 --- a/services/rfq/relayer/service/handlers.go +++ b/services/rfq/relayer/service/handlers.go @@ -4,11 +4,8 @@ import ( "context" "errors" "fmt" - "math/big" "strings" - "github.com/synapsecns/sanguine/core" - "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core/types" @@ -181,7 +178,7 @@ func (q *QuoteRequestHandler) handleCommitConfirmed(ctx context.Context, _ trace } // TODO: store the dest txhash connected to the nonce - nonce, _, err := SubmitRelay(ctx, &q.Dest, request) + nonce, _, err := q.Dest.SubmitRelay(ctx, request) if err != nil { return err } @@ -193,35 +190,6 @@ func (q *QuoteRequestHandler) handleCommitConfirmed(ctx context.Context, _ trace return nil } -// SubmitRelay submits a relay transaction to the destination chain after evaluating gas amount. -func SubmitRelay(ctx context.Context, chain *Chain, request reldb.QuoteRequest) (uint64, *big.Int, error) { - gasAmount := big.NewInt(0) - var err error - - if request.Transaction.SendChainGas { - gasAmount, err = chain.Bridge.ChainGasAmount(&bind.CallOpts{Context: ctx}) - if err != nil { - return 0, nil, fmt.Errorf("could not get chain gas amount: %w", err) - } - } - - nonce, err := chain.SubmitTransaction(ctx, func(transactor *bind.TransactOpts) (tx *types.Transaction, err error) { - transactor.Value = core.CopyBigInt(gasAmount) - - tx, err = chain.Bridge.Relay(transactor, request.RawRequest) - if err != nil { - return nil, fmt.Errorf("could not relay: %w", err) - } - - return tx, nil - }) - if err != nil { - return 0, nil, fmt.Errorf("could not submit transaction: %w", err) - } - - return nonce, gasAmount, nil -} - // handleRelayStarted handles the relay started status and marks the relay as completed. // Step 5: RelayCompleted // diff --git a/services/rfq/relayer/service/statushandler.go b/services/rfq/relayer/service/statushandler.go index 7e46db056f..4cdba04db1 100644 --- a/services/rfq/relayer/service/statushandler.go +++ b/services/rfq/relayer/service/statushandler.go @@ -10,11 +10,8 @@ import ( "github.com/ethereum/go-ethereum/common/hexutil" "github.com/jellydator/ttlcache/v3" "github.com/synapsecns/sanguine/core/metrics" - "github.com/synapsecns/sanguine/ethergo/client" - "github.com/synapsecns/sanguine/ethergo/submitter" - "github.com/synapsecns/sanguine/services/rfq/contracts/fastbridge" + "github.com/synapsecns/sanguine/services/rfq/relayer/chain" "github.com/synapsecns/sanguine/services/rfq/relayer/inventory" - "github.com/synapsecns/sanguine/services/rfq/relayer/listener" "github.com/synapsecns/sanguine/services/rfq/relayer/quoter" "github.com/synapsecns/sanguine/services/rfq/relayer/reldb" "go.opentelemetry.io/otel/attribute" @@ -28,9 +25,9 @@ import ( // the plan is to move this out of relayer which is when this distinction will matter. type QuoteRequestHandler struct { // Origin is the origin chain. - Origin Chain + Origin chain.Chain // Dest is the destination chain. - Dest Chain + Dest chain.Chain // db is the database. db reldb.Service // Inventory is the inventory. @@ -50,29 +47,6 @@ type QuoteRequestHandler struct { // Handler is the handler for a quote request. type Handler func(ctx context.Context, span trace.Span, req reldb.QuoteRequest) error -// Chain is a chain helper for relayer. -// lowercase fields are private, uppercase are public. -// the plan is to move this out of relayer which is when this distinction will matter. -type Chain struct { - ChainID uint32 - Bridge *fastbridge.FastBridgeRef - Client client.EVM - Confirmations uint64 - listener listener.ContractListener - submitter submitter.TransactionSubmitter -} - -// SubmitTransaction submits a transaction to the chain. -func (c Chain) SubmitTransaction(ctx context.Context, call submitter.ContractCallType) (nonce uint64, _ error) { - //nolint: wrapcheck - return c.submitter.SubmitTransaction(ctx, big.NewInt(int64(c.ChainID)), call) -} - -// LatestBlock returns the latest block. -func (c Chain) LatestBlock() uint64 { - return c.listener.LatestBlock() -} - func (r *Relayer) requestToHandler(ctx context.Context, req reldb.QuoteRequest) (*QuoteRequestHandler, error) { origin, err := r.chainIDToChain(ctx, req.Transaction.OriginChainId) if err != nil { @@ -130,7 +104,7 @@ func (r *Relayer) deadlineMiddleware(next func(ctx context.Context, span trace.S } } -func (r *Relayer) chainIDToChain(ctx context.Context, chainID uint32) (*Chain, error) { +func (r *Relayer) chainIDToChain(ctx context.Context, chainID uint32) (*chain.Chain, error) { id := int(chainID) chainClient, err := r.client.GetChainClient(ctx, id) @@ -138,40 +112,7 @@ func (r *Relayer) chainIDToChain(ctx context.Context, chainID uint32) (*Chain, e return nil, fmt.Errorf("could not get origin client: %w", err) } - fastBridge, err := fastbridge.NewFastBridgeRef(common.HexToAddress(r.cfg.GetChains()[id].Bridge), chainClient) - if err != nil { - return nil, fmt.Errorf("could not get origin fast bridge: %w", err) - } - - return &Chain{ - ChainID: chainID, - Bridge: fastBridge, - Client: chainClient, - Confirmations: r.cfg.GetChains()[id].Confirmations, - listener: r.chainListeners[id], - submitter: r.submitter, - }, nil -} - -// NewChain creates a new chain helper. -func NewChain(ctx context.Context, chainClient client.EVM, addr common.Address, chainListener listener.ContractListener, ts submitter.TransactionSubmitter) (*Chain, error) { - bridge, err := fastbridge.NewFastBridgeRef(addr, chainClient) - if err != nil { - return nil, fmt.Errorf("could not create bridge contract: %w", err) - } - chainID, err := chainClient.ChainID(ctx) - if err != nil { - return nil, fmt.Errorf("could not get chain id: %w", err) - } - return &Chain{ - ChainID: uint32(chainID.Int64()), - Bridge: bridge, - Client: chainClient, - // TODO: configure - Confirmations: 1, - listener: chainListener, - submitter: ts, - }, nil + return chain.NewChain(ctx, chainClient, common.HexToAddress(r.cfg.GetChains()[id].Bridge), r.chainListeners[id], r.submitter) } // shouldCheckClaim checks if we should check the claim method. From 0d0615c520475dfbe8e0e0de7e334cd192a73a43 Mon Sep 17 00:00:00 2001 From: Daniel Wasserman Date: Tue, 9 Jan 2024 21:58:05 -0600 Subject: [PATCH 31/39] Cleanup: lint --- services/rfq/relayer/relapi/server.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/rfq/relayer/relapi/server.go b/services/rfq/relayer/relapi/server.go index ab210481f7..0fbe9d5536 100644 --- a/services/rfq/relayer/relapi/server.go +++ b/services/rfq/relayer/relapi/server.go @@ -55,7 +55,7 @@ func NewRelayerAPI( chains := make(map[uint32]*chain.Chain) for chainID, chainCfg := range cfg.Chains { - chainClient, err := omniRPCClient.GetChainClient(ctx, int(chainID)) + chainClient, err := omniRPCClient.GetChainClient(ctx, chainID) if err != nil { return nil, fmt.Errorf("could not create omnirpc client: %w", err) } From 4e4e4d490545d2585f21ba5c2adc54fff5185c16 Mon Sep 17 00:00:00 2001 From: Daniel Wasserman Date: Tue, 9 Jan 2024 22:09:50 -0600 Subject: [PATCH 32/39] Cleanup: APIConfig -> RelayerAPIConfig --- services/rfq/relayer/relapi/server.go | 4 ++-- services/rfq/relayer/relapi/suite_test.go | 2 +- services/rfq/relayer/relconfig/config.go | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/services/rfq/relayer/relapi/server.go b/services/rfq/relayer/relapi/server.go index 0fbe9d5536..ae256c8569 100644 --- a/services/rfq/relayer/relapi/server.go +++ b/services/rfq/relayer/relapi/server.go @@ -101,8 +101,8 @@ func (r *RelayerAPIServer) Run(ctx context.Context) error { r.engine = engine connection := baseServer.Server{} - fmt.Printf("starting api at http://localhost:%s\n", r.cfg.APIConfig.Port) - err := connection.ListenAndServe(ctx, fmt.Sprintf(":%s", r.cfg.APIConfig.Port), r.engine) + fmt.Printf("starting api at http://localhost:%s\n", r.cfg.RelayerAPIConfig.Port) + err := connection.ListenAndServe(ctx, fmt.Sprintf(":%s", r.cfg.RelayerAPIConfig.Port), r.engine) if err != nil { return fmt.Errorf("could not start relayer api server: %w", err) } diff --git a/services/rfq/relayer/relapi/suite_test.go b/services/rfq/relayer/relapi/suite_test.go index d07276c146..a1df56c70d 100644 --- a/services/rfq/relayer/relapi/suite_test.go +++ b/services/rfq/relayer/relapi/suite_test.go @@ -85,7 +85,7 @@ func (c *RelayerServerSuite) SetupTest() { Bridge: arbFastBridgeAddress.Hex(), }, }, - APIConfig: relconfig.APIConfig{ + RelayerAPIConfig: relconfig.RelayerAPIConfig{ Database: relconfig.DatabaseConfig{ Type: "sqlite", DSN: filet.TmpFile(c.T(), "", "").Name(), diff --git a/services/rfq/relayer/relconfig/config.go b/services/rfq/relayer/relconfig/config.go index 054420dc21..e0793e0d23 100644 --- a/services/rfq/relayer/relconfig/config.go +++ b/services/rfq/relayer/relconfig/config.go @@ -37,8 +37,8 @@ type Config struct { SubmitterConfig submitterConfig.Config `yaml:"submitter_config"` // FeePricer is the fee pricer config. FeePricer FeePricerConfig `yaml:"fee_pricer"` - // APIConfig is the relayer API config. - APIConfig APIConfig `yaml:"api_config"` + // RelayerAPIConfig is the relayer API config. + RelayerAPIConfig RelayerAPIConfig `yaml:"api_config"` } // ChainConfig represents the configuration for a chain. @@ -69,8 +69,8 @@ type DatabaseConfig struct { DSN string `yaml:"dsn"` // Data Source Name } -// APIConfig is the configuration for the relayer API server. -type APIConfig struct { +// RelayerAPIConfig is the configuration for the relayer API server. +type RelayerAPIConfig struct { Database DatabaseConfig `yaml:"database"` OmniRPCURL string `yaml:"omnirpc_url"` Port string `yaml:"port"` From 9abe98cee51f073789c5ed8fc15463941bbe603a Mon Sep 17 00:00:00 2001 From: Daniel Wasserman Date: Tue, 9 Jan 2024 22:13:00 -0600 Subject: [PATCH 33/39] Cleanup: APIServer -> QuoterAPIServer --- services/rfq/api/client/suite_test.go | 8 ++++---- services/rfq/api/rest/server.go | 14 +++++++------- services/rfq/api/rest/server_test.go | 20 ++++++++++---------- services/rfq/api/rest/suite_test.go | 8 ++++---- services/rfq/e2e/rfq_test.go | 2 +- services/rfq/e2e/setup_test.go | 2 +- services/rfq/relayer/relapi/server_test.go | 14 +++++++------- 7 files changed, 34 insertions(+), 34 deletions(-) diff --git a/services/rfq/api/client/suite_test.go b/services/rfq/api/client/suite_test.go index f5c549eca1..779098d974 100644 --- a/services/rfq/api/client/suite_test.go +++ b/services/rfq/api/client/suite_test.go @@ -42,7 +42,7 @@ type ClientSuite struct { cfg config.Config testWallet wallet.Wallet handler metrics.Handler - APIServer *rest.APIServer + QuoterAPIServer *rest.QuoterAPIServer port uint16 client client.AuthenticatedClient } @@ -84,12 +84,12 @@ func (c *ClientSuite) SetupTest() { } c.cfg = testConfig - APIServer, err := rest.NewAPI(c.GetTestContext(), c.cfg, c.handler, c.omniRPCClient, c.database) + QuoterAPIServer, err := rest.NewAPI(c.GetTestContext(), c.cfg, c.handler, c.omniRPCClient, c.database) c.Require().NoError(err) - c.APIServer = APIServer + c.QuoterAPIServer = QuoterAPIServer go func() { - err := c.APIServer.Run(c.GetTestContext()) + err := c.QuoterAPIServer.Run(c.GetTestContext()) c.Require().NoError(err) }() time.Sleep(2 * time.Second) // Wait for the server to start. diff --git a/services/rfq/api/rest/server.go b/services/rfq/api/rest/server.go index e7963ce5b2..caec48421f 100644 --- a/services/rfq/api/rest/server.go +++ b/services/rfq/api/rest/server.go @@ -23,9 +23,9 @@ import ( "github.com/synapsecns/sanguine/services/rfq/contracts/fastbridge" ) -// APIServer is a struct that holds the configuration, database connection, gin engine, RPC client, metrics handler, and fast bridge contracts. +// QuoterAPIServer is a struct that holds the configuration, database connection, gin engine, RPC client, metrics handler, and fast bridge contracts. // It is used to initialize and run the API server. -type APIServer struct { +type QuoterAPIServer struct { cfg config.Config db db.APIDB engine *gin.Engine @@ -42,7 +42,7 @@ func NewAPI( handler metrics.Handler, omniRPCClient omniClient.RPCClient, store db.APIDB, -) (*APIServer, error) { +) (*QuoterAPIServer, error) { if ctx == nil { return nil, fmt.Errorf("context is nil") } @@ -68,7 +68,7 @@ func NewAPI( } } - return &APIServer{ + return &QuoterAPIServer{ cfg: cfg, db: store, omnirpcClient: omniRPCClient, @@ -84,8 +84,8 @@ const ( var logger = log.Logger("rfq-api") -// Run runs the rest api server. -func (r *APIServer) Run(ctx context.Context) error { +// Run runs the quoter api server. +func (r *QuoterAPIServer) Run(ctx context.Context) error { // TODO: Use Gin Helper engine := ginhelper.New(logger) h := NewHandler(r.db) @@ -112,7 +112,7 @@ func (r *APIServer) Run(ctx context.Context) error { } // AuthMiddleware is the Gin authentication middleware that authenticates requests using EIP191. -func (r *APIServer) AuthMiddleware() gin.HandlerFunc { +func (r *QuoterAPIServer) AuthMiddleware() gin.HandlerFunc { return func(c *gin.Context) { var req model.PutQuoteRequest if err := c.BindJSON(&req); err != nil { diff --git a/services/rfq/api/rest/server_test.go b/services/rfq/api/rest/server_test.go index 406cce6c74..16e6e11d30 100644 --- a/services/rfq/api/rest/server_test.go +++ b/services/rfq/api/rest/server_test.go @@ -15,9 +15,9 @@ import ( "github.com/synapsecns/sanguine/services/rfq/api/model" ) -func (c *ServerSuite) TestNewAPIServer() { +func (c *ServerSuite) TestNewQuoterAPIServer() { // Start the API server in a separate goroutine and wait for it to initialize. - c.startAPIServer() + c.startQuoterAPIServer() client := &http.Client{} req, err := http.NewRequestWithContext(c.GetTestContext(), http.MethodGet, fmt.Sprintf("http://localhost:%d/quotes", c.port), nil) c.Require().NoError(err) @@ -34,7 +34,7 @@ func (c *ServerSuite) TestNewAPIServer() { // TestEIP191_SuccessfulSignature tests the EIP191 signature process for successful authentication. func (c *ServerSuite) TestEIP191_SuccessfulSignature() { // Start the API server in a separate goroutine and wait for it to initialize. - c.startAPIServer() + c.startQuoterAPIServer() // Prepare the authorization header with a signed timestamp. header, err := c.prepareAuthHeader(c.testWallet) @@ -65,7 +65,7 @@ func (c *ServerSuite) TestEIP191_SuccessfulSignature() { // TestEIP191_UnsuccessfulSignature tests the EIP191 signature process with an incorrect wallet signature. func (c *ServerSuite) TestEIP191_UnsuccessfulSignature() { // Start the API server in a separate goroutine and wait for it to initialize. - c.startAPIServer() + c.startQuoterAPIServer() // Prepare the authorization header with a signed timestamp using an incorrect wallet. randomWallet, err := wallet.FromRandom() @@ -97,7 +97,7 @@ func (c *ServerSuite) TestEIP191_UnsuccessfulSignature() { // TestEIP191_SuccessfulPutSubmission tests a successful PUT request submission. func (c *ServerSuite) TestEIP191_SuccessfulPutSubmission() { // Start the API server in a separate goroutine and wait for it to initialize. - c.startAPIServer() + c.startQuoterAPIServer() // Prepare the authorization header with a signed timestamp. header, err := c.prepareAuthHeader(c.testWallet) @@ -120,7 +120,7 @@ func (c *ServerSuite) TestEIP191_SuccessfulPutSubmission() { } func (c *ServerSuite) TestPutAndGetQuote() { - c.startAPIServer() + c.startQuoterAPIServer() header, err := c.prepareAuthHeader(c.testWallet) c.Require().NoError(err) @@ -162,7 +162,7 @@ func (c *ServerSuite) TestPutAndGetQuote() { } func (c *ServerSuite) TestPutAndGetQuoteByRelayer() { - c.startAPIServer() + c.startQuoterAPIServer() header, err := c.prepareAuthHeader(c.testWallet) c.Require().NoError(err) @@ -203,10 +203,10 @@ func (c *ServerSuite) TestPutAndGetQuoteByRelayer() { c.Assert().True(found, "Newly added quote not found") } -// startAPIServer starts the API server and waits for it to initialize. -func (c *ServerSuite) startAPIServer() { +// startQuoterAPIServer starts the API server and waits for it to initialize. +func (c *ServerSuite) startQuoterAPIServer() { go func() { - err := c.APIServer.Run(c.GetTestContext()) + err := c.QuoterAPIServer.Run(c.GetTestContext()) c.Require().NoError(err) }() time.Sleep(2 * time.Second) // Wait for the server to start. diff --git a/services/rfq/api/rest/suite_test.go b/services/rfq/api/rest/suite_test.go index eb287b5b59..903093b1fb 100644 --- a/services/rfq/api/rest/suite_test.go +++ b/services/rfq/api/rest/suite_test.go @@ -39,7 +39,7 @@ type ServerSuite struct { cfg config.Config testWallet wallet.Wallet handler metrics.Handler - APIServer *rest.APIServer + QuoterAPIServer *rest.QuoterAPIServer port uint16 } @@ -80,13 +80,13 @@ func (c *ServerSuite) SetupTest() { } c.cfg = testConfig - APIServer, err := rest.NewAPI(c.GetTestContext(), c.cfg, c.handler, c.omniRPCClient, c.database) + QuoterAPIServer, err := rest.NewAPI(c.GetTestContext(), c.cfg, c.handler, c.omniRPCClient, c.database) c.Require().NoError(err) - c.APIServer = APIServer + c.QuoterAPIServer = QuoterAPIServer // go func() { - // err := c.APIServer.Run(c.GetTestContext()) + // err := c.QuoterAPIServer.Run(c.GetTestContext()) // c.Require().NoError(err) // }() // time.Sleep(2 * time.Second) // Wait for the server to start. diff --git a/services/rfq/e2e/rfq_test.go b/services/rfq/e2e/rfq_test.go index 8648fcf5da..7be36fae5f 100644 --- a/services/rfq/e2e/rfq_test.go +++ b/services/rfq/e2e/rfq_test.go @@ -69,7 +69,7 @@ func (i *IntegrationSuite) SetupTest() { i.setupBackends() // setup the api server - i.setupAPI() + i.setupQuoterAPI() i.setupRelayer() } diff --git a/services/rfq/e2e/setup_test.go b/services/rfq/e2e/setup_test.go index b92bea5dc4..ffdaee84bf 100644 --- a/services/rfq/e2e/setup_test.go +++ b/services/rfq/e2e/setup_test.go @@ -36,7 +36,7 @@ import ( "github.com/synapsecns/sanguine/services/rfq/testutil" ) -func (i *IntegrationSuite) setupAPI() { +func (i *IntegrationSuite) setupQuoterAPI() { dbPath := filet.TmpDir(i.T(), "") apiPort, err := freeport.GetFreePort() i.NoError(err) diff --git a/services/rfq/relayer/relapi/server_test.go b/services/rfq/relayer/relapi/server_test.go index 713dc3600d..faf78cefcc 100644 --- a/services/rfq/relayer/relapi/server_test.go +++ b/services/rfq/relayer/relapi/server_test.go @@ -18,9 +18,9 @@ import ( "github.com/synapsecns/sanguine/services/rfq/relayer/reldb" ) -func (c *RelayerServerSuite) TestNewAPIServer() { +func (c *RelayerServerSuite) TestNewQuoterAPIServer() { // Start the API server in a separate goroutine and wait for it to initialize. - c.startAPIServer() + c.startQuoterAPIServer() client := &http.Client{} req, err := http.NewRequestWithContext(c.GetTestContext(), http.MethodGet, fmt.Sprintf("http://localhost:%d/health", c.port), nil) c.Require().NoError(err) @@ -35,7 +35,7 @@ func (c *RelayerServerSuite) TestNewAPIServer() { } func (c *RelayerServerSuite) TestGetQuoteRequestByTxHash() { - c.startAPIServer() + c.startQuoterAPIServer() // Insert quote request to db quoteRequest := c.getTestQuoteRequest(reldb.Seen) @@ -69,7 +69,7 @@ func (c *RelayerServerSuite) TestGetQuoteRequestByTxHash() { } func (c *RelayerServerSuite) TestGetQuoteRequestByTxID() { - c.startAPIServer() + c.startQuoterAPIServer() // Insert quote request to db quoteRequest := c.getTestQuoteRequest(reldb.Seen) @@ -104,7 +104,7 @@ func (c *RelayerServerSuite) TestGetQuoteRequestByTxID() { } func (c *RelayerServerSuite) TestGetTxRetry() { - c.startAPIServer() + c.startQuoterAPIServer() // Insert quote request to db quoteRequest := c.getTestQuoteRequest(reldb.Seen) @@ -142,8 +142,8 @@ func (c *RelayerServerSuite) TestGetTxRetry() { c.Equal(status, submitterdb.Stored) } -// startAPIServer starts the API server and waits for it to initialize. -func (c *RelayerServerSuite) startAPIServer() { +// startQuoterAPIServer starts the API server and waits for it to initialize. +func (c *RelayerServerSuite) startQuoterAPIServer() { go func() { err := c.RelayerAPIServer.Run(c.GetTestContext()) c.Require().NoError(err) From 49b6ce46a0afc50ccc474884d84e90f312ff8ebf Mon Sep 17 00:00:00 2001 From: Daniel Wasserman Date: Tue, 9 Jan 2024 22:19:42 -0600 Subject: [PATCH 34/39] Fix: build --- services/rfq/e2e/setup_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/rfq/e2e/setup_test.go b/services/rfq/e2e/setup_test.go index ffdaee84bf..ef869e0558 100644 --- a/services/rfq/e2e/setup_test.go +++ b/services/rfq/e2e/setup_test.go @@ -232,7 +232,7 @@ func (i *IntegrationSuite) setupRelayer() { File: filet.TmpFile(i.T(), "", i.relayerWallet.PrivateKeyHex()).Name(), }, RelayerAPIURL: fmt.Sprintf("http://localhost:%d", relayerApiPort), - APIConfig: relconfig.APIConfig{ + RelayerAPIConfig: relconfig.RelayerAPIConfig{ Database: relconfig.DatabaseConfig{ Type: dbcommon.Sqlite.String(), DSN: dsn, From 83aa7d9fc9a0d50866f60cc45da49ade0236a1dd Mon Sep 17 00:00:00 2001 From: Daniel Wasserman Date: Wed, 10 Jan 2024 10:34:16 -0600 Subject: [PATCH 35/39] Cleanup: lint --- services/rfq/relayer/service/statushandler.go | 1 + 1 file changed, 1 insertion(+) diff --git a/services/rfq/relayer/service/statushandler.go b/services/rfq/relayer/service/statushandler.go index 4cdba04db1..714130328e 100644 --- a/services/rfq/relayer/service/statushandler.go +++ b/services/rfq/relayer/service/statushandler.go @@ -112,6 +112,7 @@ func (r *Relayer) chainIDToChain(ctx context.Context, chainID uint32) (*chain.Ch return nil, fmt.Errorf("could not get origin client: %w", err) } + //nolint: wrapcheck return chain.NewChain(ctx, chainClient, common.HexToAddress(r.cfg.GetChains()[id].Bridge), r.chainListeners[id], r.submitter) } From 51f719d520966962a7067a95acaa3230f16b673d Mon Sep 17 00:00:00 2001 From: Daniel Wasserman Date: Wed, 10 Jan 2024 10:36:55 -0600 Subject: [PATCH 36/39] Fix: build --- services/rfq/relayer/listener/listener_test.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/services/rfq/relayer/listener/listener_test.go b/services/rfq/relayer/listener/listener_test.go index 5c4c3e9c98..0797bc9833 100644 --- a/services/rfq/relayer/listener/listener_test.go +++ b/services/rfq/relayer/listener/listener_test.go @@ -2,9 +2,6 @@ package listener_test import ( "context" - "math/big" - "sync" - "math/big" "sync" "time" From 497bdffd9d108c963ad76f1574c4e0492ef02f51 Mon Sep 17 00:00:00 2001 From: Daniel Wasserman Date: Wed, 10 Jan 2024 11:50:58 -0600 Subject: [PATCH 37/39] Cleanup: lint --- services/rfq/relayer/service/handlers.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/rfq/relayer/service/handlers.go b/services/rfq/relayer/service/handlers.go index 6d82a264e3..9fdfae20eb 100644 --- a/services/rfq/relayer/service/handlers.go +++ b/services/rfq/relayer/service/handlers.go @@ -180,7 +180,7 @@ func (q *QuoteRequestHandler) handleCommitConfirmed(ctx context.Context, _ trace // TODO: store the dest txhash connected to the nonce nonce, _, err := q.Dest.SubmitRelay(ctx, request) if err != nil { - return err + return fmt.Errorf("could not submit relay: %w", err) } _ = nonce From 5dbd12e56d10393b524e0fb5c26d4615d33cb0dc Mon Sep 17 00:00:00 2001 From: Daniel Wasserman Date: Wed, 10 Jan 2024 12:04:53 -0600 Subject: [PATCH 38/39] Cleanup: lint --- services/rfq/relayer/pricer/fee_pricer.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/rfq/relayer/pricer/fee_pricer.go b/services/rfq/relayer/pricer/fee_pricer.go index 8fcf5b6f92..f090dc3154 100644 --- a/services/rfq/relayer/pricer/fee_pricer.go +++ b/services/rfq/relayer/pricer/fee_pricer.go @@ -77,7 +77,7 @@ func (f *feePricer) GetOriginFee(ctx context.Context, origin, destination uint32 return f.getFee(ctx, origin, destination, f.config.GetFeePricer().OriginGasEstimate, denomToken, useMultiplier) } -func (f *feePricer) GetDestinationFee(ctx context.Context, origin, destination uint32, denomToken string, useMultiplier bool) (*big.Int, error) { +func (f *feePricer) GetDestinationFee(ctx context.Context, _, destination uint32, denomToken string, useMultiplier bool) (*big.Int, error) { return f.getFee(ctx, destination, destination, f.config.GetFeePricer().DestinationGasEstimate, denomToken, useMultiplier) } From 5fa359818485019840d888cd8a956da9f5812df0 Mon Sep 17 00:00:00 2001 From: Daniel Wasserman Date: Wed, 10 Jan 2024 14:28:24 -0600 Subject: [PATCH 39/39] [goreleaser] Remove redundant RelayerAPIConfig section --- services/rfq/e2e/rfq_test.go | 3 --- services/rfq/e2e/setup_test.go | 10 +--------- services/rfq/relayer/relapi/server.go | 4 ++-- services/rfq/relayer/relapi/suite_test.go | 11 ++++------- services/rfq/relayer/relconfig/config.go | 13 ++----------- 5 files changed, 9 insertions(+), 32 deletions(-) diff --git a/services/rfq/e2e/rfq_test.go b/services/rfq/e2e/rfq_test.go index 7be36fae5f..a608b6f54a 100644 --- a/services/rfq/e2e/rfq_test.go +++ b/services/rfq/e2e/rfq_test.go @@ -113,7 +113,6 @@ func (i *IntegrationSuite) TestUSDCtoUSDC() { i.NoError(err) // let's figure out the amount of usdc we need - for _, quote := range allQuotes { if common.HexToAddress(quote.DestTokenAddr) == destUSDC.Address() { destAmountBigInt, _ := new(big.Int).SetString(quote.DestAmount, 10) @@ -177,7 +176,6 @@ func (i *IntegrationSuite) TestUSDCtoUSDC() { i.NoError(err) // let's figure out the amount of usdc we need - for _, quote := range allQuotes { if common.HexToAddress(quote.DestTokenAddr) == originUSDC.Address() && quote.DestChainID == originBackendChainID { @@ -193,5 +191,4 @@ func (i *IntegrationSuite) TestUSDCtoUSDC() { } return false }) - } diff --git a/services/rfq/e2e/setup_test.go b/services/rfq/e2e/setup_test.go index ef869e0558..71737c4d0a 100644 --- a/services/rfq/e2e/setup_test.go +++ b/services/rfq/e2e/setup_test.go @@ -231,15 +231,7 @@ func (i *IntegrationSuite) setupRelayer() { Type: signerConfig.FileType.String(), File: filet.TmpFile(i.T(), "", i.relayerWallet.PrivateKeyHex()).Name(), }, - RelayerAPIURL: fmt.Sprintf("http://localhost:%d", relayerApiPort), - RelayerAPIConfig: relconfig.RelayerAPIConfig{ - Database: relconfig.DatabaseConfig{ - Type: dbcommon.Sqlite.String(), - DSN: dsn, - }, - OmniRPCURL: i.omniServer, - Port: strconv.Itoa(relayerApiPort), - }, + RelayerAPIPort: strconv.Itoa(relayerApiPort), FeePricer: relconfig.FeePricerConfig{ GasPriceCacheTTLSeconds: 60, TokenPriceCacheTTLSeconds: 60, diff --git a/services/rfq/relayer/relapi/server.go b/services/rfq/relayer/relapi/server.go index ae256c8569..7be4fed499 100644 --- a/services/rfq/relayer/relapi/server.go +++ b/services/rfq/relayer/relapi/server.go @@ -101,8 +101,8 @@ func (r *RelayerAPIServer) Run(ctx context.Context) error { r.engine = engine connection := baseServer.Server{} - fmt.Printf("starting api at http://localhost:%s\n", r.cfg.RelayerAPIConfig.Port) - err := connection.ListenAndServe(ctx, fmt.Sprintf(":%s", r.cfg.RelayerAPIConfig.Port), r.engine) + fmt.Printf("starting api at http://localhost:%s\n", r.cfg.RelayerAPIPort) + err := connection.ListenAndServe(ctx, fmt.Sprintf(":%s", r.cfg.RelayerAPIPort), r.engine) if err != nil { return fmt.Errorf("could not start relayer api server: %w", err) } diff --git a/services/rfq/relayer/relapi/suite_test.go b/services/rfq/relayer/relapi/suite_test.go index a1df56c70d..3522f96596 100644 --- a/services/rfq/relayer/relapi/suite_test.go +++ b/services/rfq/relayer/relapi/suite_test.go @@ -85,13 +85,10 @@ func (c *RelayerServerSuite) SetupTest() { Bridge: arbFastBridgeAddress.Hex(), }, }, - RelayerAPIConfig: relconfig.RelayerAPIConfig{ - Database: relconfig.DatabaseConfig{ - Type: "sqlite", - DSN: filet.TmpFile(c.T(), "", "").Name(), - }, - OmniRPCURL: testOmnirpc, - Port: strconv.Itoa(port), + RelayerAPIPort: strconv.Itoa(port), + Database: relconfig.DatabaseConfig{ + Type: "sqlite", + DSN: filet.TmpFile(c.T(), "", "").Name(), }, } c.cfg = testConfig diff --git a/services/rfq/relayer/relconfig/config.go b/services/rfq/relayer/relconfig/config.go index e0793e0d23..bc3ad4f2fd 100644 --- a/services/rfq/relayer/relconfig/config.go +++ b/services/rfq/relayer/relconfig/config.go @@ -25,8 +25,8 @@ type Config struct { OmniRPCURL string `yaml:"omnirpc_url"` // RfqAPIURL is the URL of the RFQ API. RfqAPIURL string `yaml:"rfq_url"` - // RelayerAPIURL is the URL of the relayer API. - RelayerAPIURL string `yaml:"relayer_url"` + // RelayerAPIPort is the port of the relayer API. + RelayerAPIPort string `yaml:"relayer_api_port"` // Database is the database config. Database DatabaseConfig `yaml:"database"` // QuotableTokens is a map of token -> list of quotable tokens. @@ -37,8 +37,6 @@ type Config struct { SubmitterConfig submitterConfig.Config `yaml:"submitter_config"` // FeePricer is the fee pricer config. FeePricer FeePricerConfig `yaml:"fee_pricer"` - // RelayerAPIConfig is the relayer API config. - RelayerAPIConfig RelayerAPIConfig `yaml:"api_config"` } // ChainConfig represents the configuration for a chain. @@ -69,13 +67,6 @@ type DatabaseConfig struct { DSN string `yaml:"dsn"` // Data Source Name } -// RelayerAPIConfig is the configuration for the relayer API server. -type RelayerAPIConfig struct { - Database DatabaseConfig `yaml:"database"` - OmniRPCURL string `yaml:"omnirpc_url"` - Port string `yaml:"port"` -} - // FeePricerConfig represents the configuration for the fee pricer. type FeePricerConfig struct { // OriginGasEstimate is the gas required to execute prove + claim transactions on origin chain.