Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add relayer API #1785

Merged
merged 45 commits into from
Jan 12, 2024
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
eb4c98e
WIP: initial relayer api
dwasse Jan 8, 2024
0129b41
WIP: use ByTxID naming
dwasse Jan 8, 2024
c740fbf
Feat: add GetQuoteRequestStatusByTxHash
dwasse Jan 8, 2024
571e865
WIP: initial impl for PUT /tx/retry
dwasse Jan 8, 2024
521e7fa
Cleanup: comments
dwasse Jan 8, 2024
2e2eba3
Feat: add OriginTxHash to QuoteRequest, RequestForQuote models
dwasse Jan 8, 2024
de99481
Merge branch 'feat/rfq-origin-tx-hash' into feat/rfq-status
dwasse Jan 8, 2024
20123b6
Feat: fetch request by origin tx hash instead of dest
dwasse Jan 8, 2024
29a27d7
WIP: initial relayer api suite
dwasse Jan 8, 2024
90d1f5f
Feat: add /health endpoint
dwasse Jan 8, 2024
8645575
Feat: add TestGetQuoteRequestByTxHash
dwasse Jan 8, 2024
902f4aa
Fix: use /health in TestNewAPIServer
dwasse Jan 8, 2024
86d01e7
Feat: add TestGetQuoteRequestByTxID
dwasse Jan 8, 2024
8689d37
Cleanup: reorder funcs
dwasse Jan 8, 2024
a7a9cf2
WIP: add TestPutTxRetry
dwasse Jan 8, 2024
165f46f
Feat: PutTxRetry -> GetTxRetry, remove auth
dwasse Jan 8, 2024
c352481
WIP: set chain IDs on test request
dwasse Jan 8, 2024
5e3f0a7
Fix: set transaction submitter in suite
dwasse Jan 8, 2024
ee218b2
Feat: check for submitted tx in TestGetTxRetry
dwasse Jan 8, 2024
a5b254f
Cleanup: add SubmitRelay func to deduplicate code
dwasse Jan 8, 2024
b99e4fa
Cleanup: lints
dwasse Jan 9, 2024
27b7998
Cleanup: lints
dwasse Jan 9, 2024
d75654b
Merge branch 'master' into feat/rfq-status update w/ working test [go…
trajan0x Jan 9, 2024
32db813
Fix: bump server start timeout
dwasse Jan 9, 2024
833fca4
Lint: disable depguard
dwasse Jan 9, 2024
acd3f44
Fix: check nil tx in TestListenForEvents
dwasse Jan 9, 2024
aa743d2
Cleanup: use itoa instead of sprintf
dwasse Jan 9, 2024
831c50d
[goreleaser] Bump timeout on server startup
dwasse Jan 9, 2024
1cca179
Fix: server_test
dwasse Jan 9, 2024
5ca7680
Merge branch 'master' into feat/rfq-status
dwasse Jan 9, 2024
f42f2ce
[goreleaser] Cleanup: lint
dwasse Jan 9, 2024
dbed1e3
WIP: add relayer api config
dwasse Jan 9, 2024
2c57477
WIP: add new chain pkg, add APIConfig section for relayer api
dwasse Jan 9, 2024
0d0615c
Cleanup: lint
dwasse Jan 10, 2024
4e4e4d4
Cleanup: APIConfig -> RelayerAPIConfig
dwasse Jan 10, 2024
9abe98c
Cleanup: APIServer -> QuoterAPIServer
dwasse Jan 10, 2024
49b6ce4
Fix: build
dwasse Jan 10, 2024
98dcf6b
Merge branch 'master' into feat/rfq-status
dwasse Jan 10, 2024
83aa7d9
Cleanup: lint
dwasse Jan 10, 2024
51f719d
Fix: build
dwasse Jan 10, 2024
497bdff
Cleanup: lint
dwasse Jan 10, 2024
6a297fb
[goreleaser] Merge branch 'master' into feat/rfq-status
dwasse Jan 10, 2024
5dbd12e
Cleanup: lint
dwasse Jan 10, 2024
5fa3598
[goreleaser] Remove redundant RelayerAPIConfig section
dwasse Jan 10, 2024
6beb0f7
Merge branch 'master' into feat/rfq-status [goreleaser]
trajan0x Jan 11, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ linters:
- nonamedreturns
- contextcheck
- nosnakecase
- depguard
fast: false

issues:
Expand Down
7 changes: 5 additions & 2 deletions services/rfq/relayer/listener/listener_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: This review was outside the patches, so it was mapped to the patch with the greatest overlap. Original lines [10-40]

The test now includes concurrent execution with goroutines to simulate event listening. However, there is a potential issue with the sync.WaitGroup usage. The wg.Add(1) call should be outside the goroutine to prevent a race condition where the wg.Wait() could potentially be called before all wg.Add(1) calls are made.

- go func(num int) {
-     wg.Add(1)
+ go func(num int) {
+     // Moved outside the goroutine
+ }
+ wg.Add(1)

Expand Down
126 changes: 126 additions & 0 deletions services/rfq/relayer/relapi/handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package relapi

import (
"fmt"
"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"
"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
}

// NewHandler creates a new REST API handler.
func NewHandler(db reldb.Service, chains map[uint32]*service.Chain) *Handler {
return &Handler{
db: db, // Store the database connection in the handler
chains: chains,
}
}

// 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"})
}

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
}

Check warning on line 41 in services/rfq/relayer/relapi/handler.go

View check run for this annotation

Codecov / codecov/patch

services/rfq/relayer/relapi/handler.go#L39-L41

Added lines #L39 - L41 were not covered by tests

txHash := common.HexToHash(txHashStr)
quoteRequest, err := h.db.GetQuoteRequestByOriginTxHash(c, txHash)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}

Check warning on line 48 in services/rfq/relayer/relapi/handler.go

View check run for this annotation

Codecov / codecov/patch

services/rfq/relayer/relapi/handler.go#L46-L48

Added lines #L46 - L48 were not covered by tests

resp := GetQuoteRequestStatusResponse{
Status: quoteRequest.Status.String(),
TxID: hexutil.Encode(quoteRequest.TransactionID[:]),
OriginTxHash: quoteRequest.OriginTxHash.String(),
DestTxHash: quoteRequest.DestTxHash.String(),
}
c.JSON(http.StatusOK, resp)
}

// 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
}

Check warning on line 65 in services/rfq/relayer/relapi/handler.go

View check run for this annotation

Codecov / codecov/patch

services/rfq/relayer/relapi/handler.go#L63-L65

Added lines #L63 - L65 were not covered by tests

txIDBytes, err := hexutil.Decode(txIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid txID"})
return
}

Check warning on line 71 in services/rfq/relayer/relapi/handler.go

View check run for this annotation

Codecov / codecov/patch

services/rfq/relayer/relapi/handler.go#L69-L71

Added lines #L69 - L71 were not covered by tests
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
}

Check warning on line 79 in services/rfq/relayer/relapi/handler.go

View check run for this annotation

Codecov / codecov/patch

services/rfq/relayer/relapi/handler.go#L77-L79

Added lines #L77 - L79 were not covered by tests

resp := GetQuoteRequestStatusResponse{
Status: quoteRequest.Status.String(),
TxID: hexutil.Encode(quoteRequest.TransactionID[:]),
OriginTxHash: quoteRequest.OriginTxHash.String(),
DestTxHash: quoteRequest.DestTxHash.String(),
}
c.JSON(http.StatusOK, resp)
}

// 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})
return
}

Check warning on line 96 in services/rfq/relayer/relapi/handler.go

View check run for this annotation

Codecov / codecov/patch

services/rfq/relayer/relapi/handler.go#L94-L96

Added lines #L94 - L96 were not covered by tests

txHash := common.HexToHash(txHashStr)
quoteRequest, err := h.db.GetQuoteRequestByOriginTxHash(c, txHash)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}

Check warning on line 103 in services/rfq/relayer/relapi/handler.go

View check run for this annotation

Codecov / codecov/patch

services/rfq/relayer/relapi/handler.go#L101-L103

Added lines #L101 - L103 were not covered by tests

chainID := quoteRequest.Transaction.DestChainId
chain, ok := h.chains[chainID]
if !ok {
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("No contract found for chain: %d", chainID)})
return
}

Check warning on line 110 in services/rfq/relayer/relapi/handler.go

View check run for this annotation

Codecov / codecov/patch

services/rfq/relayer/relapi/handler.go#L108-L110

Added lines #L108 - L110 were not covered by tests

// `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
}

Check warning on line 117 in services/rfq/relayer/relapi/handler.go

View check run for this annotation

Codecov / codecov/patch

services/rfq/relayer/relapi/handler.go#L115-L117

Added lines #L115 - L117 were not covered by tests

resp := GetTxRetryResponse{
TxID: hexutil.Encode(quoteRequest.TransactionID[:]),
ChainID: chainID,
Nonce: nonce,
GasAmount: gasAmount.String(),
}
c.JSON(http.StatusOK, resp)
}
17 changes: 17 additions & 0 deletions services/rfq/relayer/relapi/model.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package relapi

// GetQuoteRequestStatusResponse contains the schema for a GET /quote response.
type GetQuoteRequestStatusResponse struct {
Status string `json:"status"`
TxID string `json:"tx_id"`
OriginTxHash string `json:"origin_tx_hash"`
DestTxHash string `json:"dest_tx_hash"`
}

// 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"`
GasAmount string `json:"gas_amount"`
}
111 changes: 111 additions & 0 deletions services/rfq/relayer/relapi/server.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// Package relapi provides RESTful API services for the RFQ relayer
package relapi

import (
"context"
"fmt"

"github.com/ipfs/go-log"
"github.com/synapsecns/sanguine/core/ginhelper"
"github.com/synapsecns/sanguine/ethergo/submitter"

"github.com/ethereum/go-ethereum/common"
"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/relayer/listener"
"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.
// It is used to initialize and run the API server.
type RelayerAPIServer struct {
cfg config.Config
db reldb.Service
engine *gin.Engine
handler metrics.Handler
chains map[uint32]*service.Chain
}

// 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 NewRelayerAPI(
ctx context.Context,
cfg config.Config,
handler metrics.Handler,
omniRPCClient omniClient.RPCClient,
store reldb.Service,
submitter submitter.TransactionSubmitter,
) (*RelayerAPIServer, error) {
if ctx == nil {
return nil, fmt.Errorf("context is nil")
}

Check warning on line 45 in services/rfq/relayer/relapi/server.go

View check run for this annotation

Codecov / codecov/patch

services/rfq/relayer/relapi/server.go#L44-L45

Added lines #L44 - L45 were not covered by tests
if handler == nil {
return nil, fmt.Errorf("handler is nil")
}

Check warning on line 48 in services/rfq/relayer/relapi/server.go

View check run for this annotation

Codecov / codecov/patch

services/rfq/relayer/relapi/server.go#L47-L48

Added lines #L47 - L48 were not covered by tests
if omniRPCClient == nil {
return nil, fmt.Errorf("omniRPCClient is nil")
}

Check warning on line 51 in services/rfq/relayer/relapi/server.go

View check run for this annotation

Codecov / codecov/patch

services/rfq/relayer/relapi/server.go#L50-L51

Added lines #L50 - L51 were not covered by tests
if store == nil {
return nil, fmt.Errorf("store is nil")
}

Check warning on line 54 in services/rfq/relayer/relapi/server.go

View check run for this annotation

Codecov / codecov/patch

services/rfq/relayer/relapi/server.go#L53-L54

Added lines #L53 - L54 were not covered by tests

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)
}

Check warning on line 61 in services/rfq/relayer/relapi/server.go

View check run for this annotation

Codecov / codecov/patch

services/rfq/relayer/relapi/server.go#L60-L61

Added lines #L60 - L61 were not covered by tests
chainListener, err := listener.NewChainListener(chainClient, store, common.HexToAddress(bridge), handler)
if err != nil {
return nil, fmt.Errorf("could not get chain listener: %w", err)
}

Check warning on line 65 in services/rfq/relayer/relapi/server.go

View check run for this annotation

Codecov / codecov/patch

services/rfq/relayer/relapi/server.go#L64-L65

Added lines #L64 - L65 were not covered by tests
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)
}

Check warning on line 69 in services/rfq/relayer/relapi/server.go

View check run for this annotation

Codecov / codecov/patch

services/rfq/relayer/relapi/server.go#L68-L69

Added lines #L68 - L69 were not covered by tests
}

return &RelayerAPIServer{
cfg: cfg,
db: store,
handler: handler,
chains: chains,
}, nil
}

const (
getHealthRoute = "/health"
getQuoteStatusByTxHashRoute = "/status"
getQuoteStatusByTxIDRoute = "/status/by_tx_id"
getRetryRoute = "/retry"
)

var logger = log.Logger("relayer-api")

// Run runs the rest api server.
func (r *RelayerAPIServer) Run(ctx context.Context) error {
// TODO: Use Gin Helper
engine := ginhelper.New(logger)
h := NewHandler(r.db, r.chains)

// 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

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 relayer api server: %w", err)
}

Check warning on line 108 in services/rfq/relayer/relapi/server.go

View check run for this annotation

Codecov / codecov/patch

services/rfq/relayer/relapi/server.go#L107-L108

Added lines #L107 - L108 were not covered by tests

return nil
}
Loading
Loading