services/rfq/relayer/relapi/handler.go
package relapi
import (
"encoding/json"
"fmt"
"github.com/synapsecns/sanguine/core"
"math/big"
"net/http"
"github.com/synapsecns/sanguine/core/metrics"
"go.opentelemetry.io/otel/attribute"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/core/types"
"github.com/synapsecns/sanguine/ethergo/submitter"
"github.com/synapsecns/sanguine/services/rfq/contracts/ierc20"
"github.com/synapsecns/sanguine/services/rfq/relayer/relconfig"
"github.com/synapsecns/sanguine/services/rfq/util"
"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"
)
// Handler is the REST API handler.
type Handler struct {
metrics metrics.Handler
db reldb.Service
chains map[uint32]*chain.Chain
cfg relconfig.Config
submitter submitter.TransactionSubmitter
}
// NewHandler creates a new REST API handler.
func NewHandler(metricsHelper metrics.Handler, db reldb.Service, chains map[uint32]*chain.Chain, cfg relconfig.Config, txSubmitter submitter.TransactionSubmitter) *Handler {
return &Handler{
metrics: metricsHelper,
db: db, // Store the database connection in the handler
chains: chains,
cfg: cfg,
submitter: txSubmitter,
}
}
// 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)"
// 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
chainHandler, 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 := chainHandler.SubmitRelay(c, *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)
}
// GetQuoteRequestByTxID gets the quote request by tx id.
func (h *Handler) GetQuoteRequestByTxID(c *gin.Context) {
txIDStr := c.Query("id")
if txIDStr == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Must specify 'id'"})
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 := GetQuoteRequestResponse{
Sender: quoteRequest.Sender.String(),
Status: quoteRequest.Status.String(),
TxID: hexutil.Encode(quoteRequest.TransactionID[:]),
QuoteRequestRaw: common.Bytes2Hex(quoteRequest.RawRequest),
OriginTxHash: quoteRequest.OriginTxHash.String(),
DestTxHash: quoteRequest.DestTxHash.String(),
OriginChainID: quoteRequest.Transaction.OriginChainId,
DestChainID: quoteRequest.Transaction.DestChainId,
OriginToken: quoteRequest.Transaction.OriginToken.Hex(),
DestToken: quoteRequest.Transaction.DestToken.Hex(),
}
c.JSON(http.StatusOK, resp)
}
// GetQuoteRequestByTxHash gets the quote request by tx hash.
func (h *Handler) GetQuoteRequestByTxHash(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 := GetQuoteRequestResponse{
Sender: quoteRequest.Sender.String(),
Status: quoteRequest.Status.String(),
TxID: hexutil.Encode(quoteRequest.TransactionID[:]),
QuoteRequestRaw: common.Bytes2Hex(quoteRequest.RawRequest),
OriginTxHash: quoteRequest.OriginTxHash.String(),
DestTxHash: quoteRequest.DestTxHash.String(),
OriginChainID: quoteRequest.Transaction.OriginChainId,
DestChainID: quoteRequest.Transaction.DestChainId,
OriginToken: quoteRequest.Transaction.OriginToken.Hex(),
DestToken: quoteRequest.Transaction.DestToken.Hex(),
}
c.JSON(http.StatusOK, resp)
}
// Withdraw withdraws tokens from the relayer.
//
//nolint:cyclop
func (h *Handler) Withdraw(c *gin.Context) {
ctx, span := h.metrics.Tracer().Start(c, "withdraw")
var err error
defer func() {
metrics.EndSpanWithErr(span, err)
}()
var req WithdrawRequest
if err = c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// validate the token address
if !tokenIDExists(h.cfg, req.TokenAddress, int(req.ChainID)) {
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("invalid token address %s for chain %d", req.TokenAddress.Hex(), req.ChainID)})
return
}
// validate the withdrawal address
if !toAddressIsWhitelisted(h.cfg, req.To) {
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("withdrawal address %s is not whitelisted", req.To.Hex())})
return
}
var nonce uint64
value, ok := new(big.Int).SetString(req.Amount, 10)
if !ok {
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("invalid amount %s", req.Amount)})
return
}
//nolint: nestif
if util.IsGasToken(req.TokenAddress) {
nonce, err = h.submitter.SubmitTransaction(ctx, big.NewInt(int64(req.ChainID)), func(transactor *bind.TransactOpts) (tx *types.Transaction, err error) {
bc := bind.NewBoundContract(req.To, abi.ABI{}, h.chains[req.ChainID].Client, h.chains[req.ChainID].Client, h.chains[req.ChainID].Client)
transactor.Value = core.CopyBigInt(value)
return bc.Transfer(transactor)
})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("could not submit transaction: %s", err.Error())})
return
}
} else {
var erc20Contract *ierc20.IERC20
erc20Contract, err = ierc20.NewIERC20(req.TokenAddress, h.chains[req.ChainID].Client)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("could not create erc20 contract: %s", err.Error())})
return
}
nonce, err = h.submitter.SubmitTransaction(ctx, big.NewInt(int64(req.ChainID)), func(transactor *bind.TransactOpts) (tx *types.Transaction, err error) {
// nolint: wrapcheck
return erc20Contract.Transfer(transactor, req.To, value)
})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("could not submit transaction: %s", err.Error())})
return
}
}
c.JSON(http.StatusOK, gin.H{"nonce": nonce})
}
// GetTxByNonceRequest is the request for getting a transaction hash by nonce.
type GetTxByNonceRequest struct {
ChainID uint32 `json:"chain_id"`
Nonce uint64 `json:"nonce"`
}
// GetTxHashByNonce gets the transaction hash by submitter nonce.
func (h *Handler) GetTxHashByNonce(c *gin.Context) {
ctx, span := h.metrics.Tracer().Start(c, "txByNonce")
var err error
defer func() {
metrics.EndSpanWithErr(span, err)
}()
chainIDStr := c.Query("chain_id")
nonceStr := c.Query("nonce")
chainID, ok := new(big.Int).SetString(chainIDStr, 10)
if !ok {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid chainID"})
return
}
span.SetAttributes(attribute.Int("chain_id", int(chainID.Uint64())))
nonce, ok := new(big.Int).SetString(nonceStr, 10)
if !ok {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid nonce"})
return
}
span.SetAttributes(attribute.Int("nonce", int(nonce.Uint64())))
tx, err := h.submitter.GetSubmissionStatus(ctx, chainID, nonce.Uint64())
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("could not get tx hash: %s", err.Error())})
return
}
if tx.HasTx() {
c.JSON(http.StatusOK, gin.H{"withdrawTxHash": tx.TxHash().String()})
return
}
c.JSON(http.StatusNotFound, gin.H{"error": "transaction not found"})
}
// tokenIDExists checks if a token ID exists in the config.
// note: this method assumes that SanitizeTokenID is a method of relconfig.Config
func tokenIDExists(cfg relconfig.Config, tokenAddress common.Address, chainID int) bool {
for quotableToken := range cfg.QuotableTokens {
prospectiveChainID, prospectiveAddress, err := relconfig.DecodeTokenID(quotableToken)
if err != nil {
continue
}
if prospectiveChainID == chainID && prospectiveAddress == tokenAddress {
return true
}
}
return false
}
func toAddressIsWhitelisted(cfg relconfig.Config, to common.Address) bool {
for _, addr := range cfg.WithdrawalWhitelist {
if common.HexToAddress(addr) == to {
return true
}
}
return false
}
// WithdrawRequest is the request to withdraw tokens from the relayer.
type WithdrawRequest struct {
// ChainID is the chain ID of the chain to withdraw from.
ChainID uint32 `json:"chain_id"`
// Amount is the amount to withdraw, in wei.
Amount string `json:"amount"`
// TokenAddress is the address of the token to withdraw.
TokenAddress common.Address `json:"token_address"`
// To is the address to withdraw to.
To common.Address `json:"to"`
}
// MarshalJSON handles JSON marshaling for WithdrawRequest.
func (wr *WithdrawRequest) MarshalJSON() ([]byte, error) {
type Alias WithdrawRequest
// nolint: wrapcheck
return json.Marshal(&struct {
TokenAddress string `json:"token_address"`
To string `json:"to"`
*Alias
}{
TokenAddress: wr.TokenAddress.Hex(),
To: wr.To.Hex(),
Alias: (*Alias)(wr),
})
}
// UnmarshalJSON has JSON unmarshalling for WithdrawRequest.
func (wr *WithdrawRequest) UnmarshalJSON(data []byte) error {
type Alias WithdrawRequest
aux := &struct {
TokenAddress string `json:"token_address"`
To string `json:"to"`
*Alias
}{
Alias: (*Alias)(wr),
}
if err := json.Unmarshal(data, aux); err != nil {
//nolint: wrapcheck
return err
}
wr.TokenAddress = common.HexToAddress(aux.TokenAddress)
wr.To = common.HexToAddress(aux.To)
return nil
}