-
Notifications
You must be signed in to change notification settings - Fork 32
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
Add relayer API #1785
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 0129b41
WIP: use ByTxID naming
dwasse c740fbf
Feat: add GetQuoteRequestStatusByTxHash
dwasse 571e865
WIP: initial impl for PUT /tx/retry
dwasse 521e7fa
Cleanup: comments
dwasse 2e2eba3
Feat: add OriginTxHash to QuoteRequest, RequestForQuote models
dwasse de99481
Merge branch 'feat/rfq-origin-tx-hash' into feat/rfq-status
dwasse 20123b6
Feat: fetch request by origin tx hash instead of dest
dwasse 29a27d7
WIP: initial relayer api suite
dwasse 90d1f5f
Feat: add /health endpoint
dwasse 8645575
Feat: add TestGetQuoteRequestByTxHash
dwasse 902f4aa
Fix: use /health in TestNewAPIServer
dwasse 86d01e7
Feat: add TestGetQuoteRequestByTxID
dwasse 8689d37
Cleanup: reorder funcs
dwasse a7a9cf2
WIP: add TestPutTxRetry
dwasse 165f46f
Feat: PutTxRetry -> GetTxRetry, remove auth
dwasse c352481
WIP: set chain IDs on test request
dwasse 5e3f0a7
Fix: set transaction submitter in suite
dwasse ee218b2
Feat: check for submitted tx in TestGetTxRetry
dwasse a5b254f
Cleanup: add SubmitRelay func to deduplicate code
dwasse b99e4fa
Cleanup: lints
dwasse 27b7998
Cleanup: lints
dwasse d75654b
Merge branch 'master' into feat/rfq-status update w/ working test [go…
trajan0x 32db813
Fix: bump server start timeout
dwasse 833fca4
Lint: disable depguard
dwasse acd3f44
Fix: check nil tx in TestListenForEvents
dwasse aa743d2
Cleanup: use itoa instead of sprintf
dwasse 831c50d
[goreleaser] Bump timeout on server startup
dwasse 1cca179
Fix: server_test
dwasse 5ca7680
Merge branch 'master' into feat/rfq-status
dwasse f42f2ce
[goreleaser] Cleanup: lint
dwasse dbed1e3
WIP: add relayer api config
dwasse 2c57477
WIP: add new chain pkg, add APIConfig section for relayer api
dwasse 0d0615c
Cleanup: lint
dwasse 4e4e4d4
Cleanup: APIConfig -> RelayerAPIConfig
dwasse 9abe98c
Cleanup: APIServer -> QuoterAPIServer
dwasse 49b6ce4
Fix: build
dwasse 98dcf6b
Merge branch 'master' into feat/rfq-status
dwasse 83aa7d9
Cleanup: lint
dwasse 51f719d
Fix: build
dwasse 497bdff
Cleanup: lint
dwasse 6a297fb
[goreleaser] Merge branch 'master' into feat/rfq-status
dwasse 5dbd12e
Cleanup: lint
dwasse 5fa3598
[goreleaser] Remove redundant RelayerAPIConfig section
dwasse 6beb0f7
Merge branch 'master' into feat/rfq-status [goreleaser]
trajan0x File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -92,6 +92,7 @@ linters: | |
- nonamedreturns | ||
- contextcheck | ||
- nosnakecase | ||
- depguard | ||
fast: false | ||
|
||
issues: | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
|
||
txHash := common.HexToHash(txHashStr) | ||
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[:]), | ||
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 | ||
} | ||
|
||
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(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 | ||
} | ||
|
||
txHash := common.HexToHash(txHashStr) | ||
quoteRequest, err := h.db.GetQuoteRequestByOriginTxHash(c, txHash) | ||
if err != nil { | ||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) | ||
return | ||
} | ||
|
||
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 | ||
} | ||
|
||
// `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 | ||
} | ||
|
||
resp := GetTxRetryResponse{ | ||
TxID: hexutil.Encode(quoteRequest.TransactionID[:]), | ||
ChainID: chainID, | ||
Nonce: nonce, | ||
GasAmount: gasAmount.String(), | ||
} | ||
c.JSON(http.StatusOK, resp) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"` | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | ||
} | ||
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") | ||
} | ||
|
||
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) | ||
} | ||
chainListener, err := listener.NewChainListener(chainClient, store, common.HexToAddress(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) | ||
if err != nil { | ||
return nil, fmt.Errorf("could not create chain: %w", err) | ||
} | ||
} | ||
|
||
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) | ||
} | ||
|
||
return nil | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test now includes concurrent execution with goroutines to simulate event listening. However, there is a potential issue with the
sync.WaitGroup
usage. Thewg.Add(1)
call should be outside the goroutine to prevent a race condition where thewg.Wait()
could potentially be called before allwg.Add(1)
calls are made.