synapsecns/sanguine

View on GitHub
services/rfq/api/rest/handler.go

Summary

Maintainability
A
1 hr
Test Coverage
package rest

import (
    "fmt"
    "net/http"
    "strconv"
    "time"

    "github.com/synapsecns/sanguine/services/rfq/api/config"

    "github.com/gin-gonic/gin"
    "github.com/shopspring/decimal"
    "github.com/synapsecns/sanguine/services/rfq/api/db"
    "github.com/synapsecns/sanguine/services/rfq/api/model"
)

// Handler is the REST API handler.
type Handler struct {
    db  db.APIDB
    cfg config.Config
}

// NewHandler creates a new REST API handler.
func NewHandler(db db.APIDB, cfg config.Config) *Handler {
    return &Handler{
        db:  db, // Store the database connection in the handler
        cfg: cfg,
    }
}

// APIVersionMiddleware adds the X-API-Version header to the response with the current version # from versions.json file.
func APIVersionMiddleware(serverVersion string) gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Writer.Header().Set("X-Api-Version", serverVersion)
        c.Next()
    }
}

// ModifyQuote upserts a quote
//
// PUT /quotes
// @dev Protected Method: Authentication is handled through middleware in server.go.
// nolint: cyclop
// @Summary Upsert quote
// @Schemes
// @Description upsert a quote from relayer.
// @Param request body model.PutRelayerQuoteRequest true "query params"
// @Tags quotes
// @Accept json
// @Produce json
// @Success 200
// @Header 200 {string} X-Api-Version "API Version Number - See docs for more info"
// @Router /quotes [put].
func (h *Handler) ModifyQuote(c *gin.Context) {
    // Retrieve the request from context
    req, exists := c.Get("putRequest")
    if !exists {
        c.JSON(http.StatusBadRequest, gin.H{"error": "Request not found"})
        return
    }
    relayerAddr, exists := c.Get("relayerAddr")
    if !exists {
        c.JSON(http.StatusBadRequest, gin.H{"error": "No relayer address recovered from signature"})
        return
    }
    putRequest, ok := req.(*model.PutRelayerQuoteRequest)
    if !ok {
        c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request type"})
        return
    }

    dbQuote, err := parseDBQuote(*putRequest, relayerAddr)
    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }
    err = h.db.UpsertQuote(c, dbQuote)
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }
    c.Status(http.StatusOK)
}

// ModifyBulkQuotes upserts multiple quotes
//
// PUT /bulk_quotes
// @dev Protected Method: Authentication is handled through middleware in server.go.
// nolint: cyclop
// @Summary Upsert quotes
// @Schemes
// @Description upsert bulk quotes from relayer.
// @Param request body model.PutBulkQuotesRequest true "query params"
// @Tags quotes
// @Accept json
// @Produce json
// @Success 200
// @Header 200 {string} X-Api-Version "API Version Number - See docs for more info"
// @Router /bulk_quotes [put].
func (h *Handler) ModifyBulkQuotes(c *gin.Context) {
    // Retrieve the request from context
    req, exists := c.Get("putRequest")
    if !exists {
        c.JSON(http.StatusBadRequest, gin.H{"error": "Request not found"})
        return
    }
    relayerAddr, exists := c.Get("relayerAddr")
    if !exists {
        c.JSON(http.StatusBadRequest, gin.H{"error": "No relayer address recovered from signature"})
        return
    }
    putRequest, ok := req.(*model.PutBulkQuotesRequest)
    if !ok {
        c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request type"})
        return
    }

    dbQuotes := []*db.Quote{}
    for _, quoteReq := range putRequest.Quotes {
        dbQuote, err := parseDBQuote(quoteReq, relayerAddr)
        if err != nil {
            c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid quote request"})
            return
        }
        dbQuotes = append(dbQuotes, dbQuote)
    }

    err := h.db.UpsertQuotes(c, dbQuotes)
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }
    c.Status(http.StatusOK)
}

//nolint:gosec
func parseDBQuote(putRequest model.PutRelayerQuoteRequest, relayerAddr interface{}) (*db.Quote, error) {
    destAmount, err := decimal.NewFromString(putRequest.DestAmount)
    if err != nil {
        return nil, fmt.Errorf("invalid DestAmount")
    }
    maxOriginAmount, err := decimal.NewFromString(putRequest.MaxOriginAmount)
    if err != nil {
        return nil, fmt.Errorf("invalid MaxOriginAmount")
    }
    fixedFee, err := decimal.NewFromString(putRequest.FixedFee)
    if err != nil {
        return nil, fmt.Errorf("invalid FixedFee")
    }
    // nolint: forcetypeassert
    return &db.Quote{
        OriginChainID:   uint64(putRequest.OriginChainID),
        OriginTokenAddr: putRequest.OriginTokenAddr,
        DestChainID:     uint64(putRequest.DestChainID),
        DestTokenAddr:   putRequest.DestTokenAddr,
        DestAmount:      destAmount,
        MaxOriginAmount: maxOriginAmount,
        FixedFee:        fixedFee,
        //nolint: forcetypeassert
        RelayerAddr:             relayerAddr.(string),
        OriginFastBridgeAddress: putRequest.OriginFastBridgeAddress,
        DestFastBridgeAddress:   putRequest.DestFastBridgeAddress,
    }, nil
}

//nolint:gosec
func quoteResponseFromDBQuote(dbQuote *db.Quote) *model.GetQuoteResponse {
    return &model.GetQuoteResponse{
        OriginChainID:           int(dbQuote.OriginChainID),
        OriginTokenAddr:         dbQuote.OriginTokenAddr,
        DestChainID:             int(dbQuote.DestChainID),
        DestTokenAddr:           dbQuote.DestTokenAddr,
        DestAmount:              dbQuote.DestAmount.String(),
        MaxOriginAmount:         dbQuote.MaxOriginAmount.String(),
        FixedFee:                dbQuote.FixedFee.String(),
        RelayerAddr:             dbQuote.RelayerAddr,
        OriginFastBridgeAddress: dbQuote.OriginFastBridgeAddress,
        DestFastBridgeAddress:   dbQuote.DestFastBridgeAddress,
        UpdatedAt:               dbQuote.UpdatedAt.Format(time.RFC3339),
    }
}

// GetQuotes retrieves all quotes from the database.
// GET /quotes.
// nolint: cyclop
// PingExample godoc
// @Summary Get quotes
// @Schemes
// @Param   originChainID     path    int     false        "origin chain id to filter quotes by"
// @Param   originTokenAddr   path    string     false        "origin chain id to filter quotes by"
// @Param   destChainID     path    int     false        "destination chain id to filter quotes by"
// @Param   destTokenAddr   path    string     false        "destination token address to filter quotes by"
// @Param   relayerAddr   path    string     false        "relayer address to filter quotes by"
// @Description get quotes from all relayers.
// @Tags quotes
// @Accept json
// @Produce json
// @Success 200 {array} model.GetQuoteResponse
// @Header 200 {string} X-Api-Version "API Version Number - See docs for more info"
// @Router /quotes [get].
func (h *Handler) GetQuotes(c *gin.Context) {
    originChainIDStr := c.Query("originChainID")
    originTokenAddr := c.Query("originTokenAddr")
    destChainIDStr := c.Query("destChainId")
    destTokenAddr := c.Query("destTokenAddr")
    relayerAddr := c.Query("relayerAddr")

    // TODO (aureliusbtc): rewrite this if
    //nolint: gocritic, nestif
    var dbQuotes []*db.Quote
    var err error
    if originChainIDStr != "" && originTokenAddr != "" && destChainIDStr != "" && destTokenAddr != "" {
        destChainID, err := strconv.ParseUint(destChainIDStr, 10, 64)
        if err != nil {
            c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid destChainId"})
            return
        }

        originChainID, err := strconv.ParseUint(originChainIDStr, 10, 64)
        if err != nil {
            c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid originChainID"})
            return
        }

        dbQuotes, err = h.db.GetQuotesByOriginAndDestination(c, originChainID, originTokenAddr, destChainID, destTokenAddr)
        if err != nil {
            c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
            return
        }
    } else if relayerAddr != "" {
        dbQuotes, err = h.db.GetQuotesByRelayerAddress(c, relayerAddr)
        if err != nil {
            c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
            return
        }
    } else {
        dbQuotes, err = h.db.GetAllQuotes(c)
        if err != nil {
            c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
            return
        }
    }

    // Filter quotes
    dbQuotes = filterQuoteAge(h.cfg, dbQuotes)

    // Convert quotes from db model to api model
    quotes := make([]*model.GetQuoteResponse, len(dbQuotes))
    for i, dbQuote := range dbQuotes {
        quotes[i] = quoteResponseFromDBQuote(dbQuote)
    }
    c.JSON(http.StatusOK, quotes)
}

// GetOpenQuoteRequests retrieves all open quote requests.
// GET /open_quote_requests
// @Summary Get open quote requests
// @Description Get all open quote requests that are currently in Received or Pending status.
// @Tags quotes
// @Accept json
// @Produce json
// @Success 200 {array} model.GetOpenQuoteRequestsResponse
// @Header 200 {string} X-Api-Version "API Version Number - See docs for more info"
// @Router /open_quote_requests [get].
func (h *Handler) GetOpenQuoteRequests(c *gin.Context) {
    dbQuotes, err := h.db.GetActiveQuoteRequests(c, db.Received, db.Pending)
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }

    quotes := make([]*model.GetOpenQuoteRequestsResponse, len(dbQuotes))
    for i, dbQuote := range dbQuotes {
        quotes[i] = dbActiveQuoteRequestToModel(dbQuote)
    }
    c.JSON(http.StatusOK, quotes)
}

func dbActiveQuoteRequestToModel(dbQuote *db.ActiveQuoteRequest) *model.GetOpenQuoteRequestsResponse {
    return &model.GetOpenQuoteRequestsResponse{
        UserAddress:       dbQuote.UserAddress,
        OriginChainID:     dbQuote.OriginChainID,
        OriginTokenAddr:   dbQuote.OriginTokenAddr,
        DestChainID:       dbQuote.DestChainID,
        DestTokenAddr:     dbQuote.DestTokenAddr,
        OriginAmountExact: dbQuote.OriginAmountExact.String(),
        ExpirationWindow:  int(dbQuote.ExpirationWindow.Milliseconds()),
        CreatedAt:         dbQuote.CreatedAt,
    }
}

// GetContracts retrieves all contracts api is currently enabled on.
// GET /contracts.
// PingExample godoc
// @Summary Get contract addresses
// @Description get quotes from all relayers.
// @Tags quotes
// @Accept json
// @Produce json
// @Success 200 {array} model.GetContractsResponse
// @Header 200 {string} X-Api-Version "API Version Number - See docs for more info"
// @Router /contracts [get].
func (h *Handler) GetContracts(c *gin.Context) {
    // Convert quotes from db model to api model
    contracts := make(map[uint32]string)
    for chainID, address := range h.cfg.Bridges {
        contracts[chainID] = address
    }
    c.JSON(http.StatusOK, model.GetContractsResponse{Contracts: contracts})
}

func filterQuoteAge(cfg config.Config, dbQuotes []*db.Quote) []*db.Quote {
    maxAge := cfg.GetMaxQuoteAge()

    thresh := time.Now().Add(-maxAge)
    var filteredQuotes []*db.Quote
    for _, quote := range dbQuotes {
        if quote.UpdatedAt.After(thresh) {
            filteredQuotes = append(filteredQuotes, quote)
        }
    }

    return filteredQuotes
}