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

RFQ API: add bulk quotes endpoint #2846

Merged
merged 14 commits into from
Jul 5, 2024
14 changes: 14 additions & 0 deletions services/rfq/api/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
// It provides methods for creating, retrieving and updating quotes.
type AuthenticatedClient interface {
PutQuote(ctx context.Context, q *model.PutQuoteRequest) error
PutBulkQuotes(ctx context.Context, q *model.PutBulkQuotesRequest) error
PutRelayAck(ctx context.Context, req *model.PutAckRequest) (*model.PutRelayAckResponse, error)
UnauthenticatedClient
}
Expand Down Expand Up @@ -125,6 +126,19 @@ func (c *clientImpl) PutQuote(ctx context.Context, q *model.PutQuoteRequest) err
return err
}

// PutBulkQuotes puts multiple new quotes in the RFQ quoting API.
func (c *clientImpl) PutBulkQuotes(ctx context.Context, q *model.PutBulkQuotesRequest) error {
res, err := c.rClient.R().
SetContext(ctx).
SetBody(q).
Put(rest.BulkQuotesRoute)

// TODO: Figure out if there's anything to do with the response, right now it's result: Status Code 200 OK
_ = res

return err
}
Comment on lines +129 to +140
Copy link
Contributor

Choose a reason for hiding this comment

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

Ensure proper error handling for the response.

The function PutBulkQuotes is correctly implemented to handle multiple quotes. However, it would be beneficial to handle the response to check for errors or log relevant information.

-	// TODO: Figure out if there's anything to do with the response, right now it's result: Status Code 200 OK
-	_ = res
+	if res.IsError() {
+		return fmt.Errorf("error from server: %s", res.Status())
+	}
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// PutBulkQuotes puts multiple new quotes in the RFQ quoting API.
func (c *clientImpl) PutBulkQuotes(ctx context.Context, q *model.PutBulkQuotesRequest) error {
res, err := c.rClient.R().
SetContext(ctx).
SetBody(q).
Put(rest.BulkQuotesRoute)
// TODO: Figure out if there's anything to do with the response, right now it's result: Status Code 200 OK
_ = res
return err
}
// PutBulkQuotes puts multiple new quotes in the RFQ quoting API.
func (c *clientImpl) PutBulkQuotes(ctx context.Context, q *model.PutBulkQuotesRequest) error {
res, err := c.rClient.R().
SetContext(ctx).
SetBody(q).
Put(rest.BulkQuotesRoute)
// TODO: Figure out if there's anything to do with the response, right now it's result: Status Code 200 OK
if res.IsError() {
return fmt.Errorf("error from server: %s", res.Status())
}
return err
}


func (c *clientImpl) PutRelayAck(ctx context.Context, req *model.PutAckRequest) (*model.PutRelayAckResponse, error) {
var ack *model.PutRelayAckResponse
resp, err := c.rClient.R().
Expand Down
59 changes: 59 additions & 0 deletions services/rfq/api/client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,65 @@ func (c *ClientSuite) TestPutAndGetQuote() {
c.Equal(expectedResp, *quotes[0])
}

func (c *ClientSuite) TestPutAndGetBulkQuotes() {
req := model.PutBulkQuotesRequest{
Quotes: []model.PutQuoteRequest{
{
OriginChainID: 1,
OriginTokenAddr: "0xOriginTokenAddr",
DestChainID: 42161,
DestTokenAddr: "0xDestTokenAddr",
DestAmount: "100",
MaxOriginAmount: "200",
FixedFee: "10",
},
{
OriginChainID: 42161,
OriginTokenAddr: "0xOriginTokenAddr",
DestChainID: 1,
DestTokenAddr: "0xDestTokenAddr",
DestAmount: "100",
MaxOriginAmount: "200",
FixedFee: "10",
},
},
}

err := c.client.PutBulkQuotes(c.GetTestContext(), &req)
c.Require().NoError(err)

quotes, err := c.client.GetAllQuotes(c.GetTestContext())
c.Require().NoError(err)

expectedResp := []model.GetQuoteResponse{
{
OriginChainID: 1,
OriginTokenAddr: "0xOriginTokenAddr",
DestChainID: 42161,
DestTokenAddr: "0xDestTokenAddr",
DestAmount: "100",
MaxOriginAmount: "200",
FixedFee: "10",
RelayerAddr: c.testWallet.Address().String(),
UpdatedAt: quotes[0].UpdatedAt,
},
{
OriginChainID: 42161,
OriginTokenAddr: "0xOriginTokenAddr",
DestChainID: 1,
DestTokenAddr: "0xDestTokenAddr",
DestAmount: "100",
MaxOriginAmount: "200",
FixedFee: "10",
RelayerAddr: c.testWallet.Address().String(),
UpdatedAt: quotes[0].UpdatedAt,
},
}
c.Len(quotes, 2)
c.Equal(expectedResp[0], *quotes[0])
c.Equal(expectedResp[1], *quotes[1])
}
Comment on lines +41 to +98
Copy link
Contributor

Choose a reason for hiding this comment

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

Address implicit memory aliasing.

The loop in TestPutAndGetBulkQuotes contains implicit memory aliasing. To avoid this, create a new variable inside the loop.

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)
}
for i := range putRequest.Quotes {
	quoteReq := putRequest.Quotes[i]
	dbQuote, err := parseDBQuote(&quoteReq, relayerAddr)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid quote request"})
		return
	}
	dbQuotes = append(dbQuotes, dbQuote)
}


func (c *ClientSuite) TestGetSpecificQuote() {
req := model.PutQuoteRequest{
OriginChainID: 1,
Expand Down
2 changes: 2 additions & 0 deletions services/rfq/api/db/api_db.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ type APIDBReader interface {
type APIDBWriter interface {
// UpsertQuote upserts a quote in the database.
UpsertQuote(ctx context.Context, quote *Quote) error
// UpsertQuotes upserts multiple quotes in the database.
UpsertQuotes(ctx context.Context, quotes []*Quote) error
}

// APIDB is the interface for the database service.
Expand Down
14 changes: 14 additions & 0 deletions services/rfq/api/db/sql/base/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package base
import (
"context"
"fmt"

"gorm.io/gorm/clause"

"github.com/synapsecns/sanguine/services/rfq/api/db"
Expand Down Expand Up @@ -63,3 +64,16 @@ func (s *Store) UpsertQuote(ctx context.Context, quote *db.Quote) error {
}
return nil
}

// UpsertQuotes inserts multiple quotes into the database or updates existing ones.
func (s *Store) UpsertQuotes(ctx context.Context, quotes []*db.Quote) error {
dbTx := s.DB().WithContext(ctx).
Clauses(clause.OnConflict{
UpdateAll: true,
}).Create(quotes)

if dbTx.Error != nil {
return fmt.Errorf("could not update quotes: %w", dbTx.Error)
}
return nil
}
5 changes: 5 additions & 0 deletions services/rfq/api/model/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ type PutQuoteRequest struct {
DestFastBridgeAddress string `json:"dest_fast_bridge_address"`
}

// PutBulkQuotesRequest contains the schema for a PUT /quote request.
type PutBulkQuotesRequest struct {
Quotes []PutQuoteRequest `json:"quotes"`
}

// PutAckRequest contains the schema for a PUT /ack request.
type PutAckRequest struct {
TxID string `json:"tx_id"`
Expand Down
84 changes: 70 additions & 14 deletions services/rfq/api/rest/handler.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package rest

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

Expand Down Expand Up @@ -54,23 +55,84 @@ func (h *Handler) ModifyQuote(c *gin.Context) {
return
}

destAmount, err := decimal.NewFromString(putRequest.DestAmount)
dbQuote, err := parseDBQuote(*putRequest, relayerAddr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid DestAmount"})
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
maxOriginAmount, err := decimal.NewFromString(putRequest.MaxOriginAmount)
err = h.db.UpsertQuote(c, dbQuote)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid DestAmount"})
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
fixedFee, err := decimal.NewFromString(putRequest.FixedFee)
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
// @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.StatusBadRequest, gin.H{"error": "Invalid FixedFee"})
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.Status(http.StatusOK)
}

func parseDBQuote(putRequest model.PutQuoteRequest, 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
quote := &db.Quote{
return &db.Quote{
OriginChainID: uint64(putRequest.OriginChainID),
OriginTokenAddr: putRequest.OriginTokenAddr,
DestChainID: uint64(putRequest.DestChainID),
Expand All @@ -82,13 +144,7 @@ func (h *Handler) ModifyQuote(c *gin.Context) {
RelayerAddr: relayerAddr.(string),
OriginFastBridgeAddress: putRequest.OriginFastBridgeAddress,
DestFastBridgeAddress: putRequest.DestFastBridgeAddress,
}
err = h.db.UpsertQuote(c, quote)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.Status(http.StatusOK)
}, nil
}

// GetQuotes retrieves all quotes from the database.
Expand Down
40 changes: 32 additions & 8 deletions services/rfq/api/rest/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,8 @@ func NewAPI(
const (
// QuoteRoute is the API endpoint for handling quote related requests.
QuoteRoute = "/quotes"
// BulkQuotesRoute is the API endpoint for handling bulk quote related requests.
BulkQuotesRoute = "/bulk_quotes"
// AckRoute is the API endpoint for handling relay ack related requests.
AckRoute = "/ack"
cacheInterval = time.Minute
Expand All @@ -160,6 +162,9 @@ func (r *QuoterAPIServer) Run(ctx context.Context) error {
quotesPut := engine.Group(QuoteRoute)
quotesPut.Use(r.AuthMiddleware())
quotesPut.PUT("", h.ModifyQuote)
bulkQuotesPut := engine.Group(BulkQuotesRoute)
bulkQuotesPut.Use(r.AuthMiddleware())
bulkQuotesPut.PUT("", h.ModifyBulkQuotes)
ackPut := engine.Group(AckRoute)
ackPut.Use(r.AuthMiddleware())
ackPut.PUT("", r.PutRelayAck)
Expand Down Expand Up @@ -187,23 +192,32 @@ func (r *QuoterAPIServer) Run(ctx context.Context) error {
func (r *QuoterAPIServer) AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
var loggedRequest interface{}
var destChainID uint32
var err error
destChainIDs := []uint32{}

// Parse the dest chain id from the request
switch c.Request.URL.Path {
case QuoteRoute:
var req model.PutQuoteRequest
err = c.BindJSON(&req)
if err == nil {
destChainID = uint32(req.DestChainID)
destChainIDs = append(destChainIDs, uint32(req.DestChainID))
loggedRequest = &req
}
case BulkQuotesRoute:
var req model.PutBulkQuotesRequest
err = c.BindJSON(&req)
if err == nil {
for _, quote := range req.Quotes {
destChainIDs = append(destChainIDs, uint32(quote.DestChainID))
}
loggedRequest = &req
}
case AckRoute:
var req model.PutAckRequest
err = c.BindJSON(&req)
if err == nil {
destChainID = uint32(req.DestChainID)
destChainIDs = append(destChainIDs, uint32(req.DestChainID))
loggedRequest = &req
}
default:
Expand All @@ -216,11 +230,21 @@ func (r *QuoterAPIServer) AuthMiddleware() gin.HandlerFunc {
}

// Authenticate and fetch the address from the request
addressRecovered, err := r.checkRole(c, destChainID)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"msg": err.Error()})
c.Abort()
return
var addressRecovered *common.Address
for _, destChainID := range destChainIDs {
addr, err := r.checkRole(c, destChainID)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"msg": err.Error()})
c.Abort()
return
}
if addressRecovered == nil {
addressRecovered = &addr
} else if *addressRecovered != addr {
c.JSON(http.StatusBadRequest, gin.H{"msg": "relayer address mismatch"})
c.Abort()
return
}
}

// Log and pass to the next middleware if authentication succeeds
Expand Down
Loading
Loading