diff --git a/docs/bridge/docs/rfq/API/upsert-quotes.api.mdx b/docs/bridge/docs/rfq/API/upsert-quotes.api.mdx new file mode 100644 index 0000000000..c001ef8ea1 --- /dev/null +++ b/docs/bridge/docs/rfq/API/upsert-quotes.api.mdx @@ -0,0 +1,244 @@ +--- +id: upsert-quotes +title: "Upsert quotes" +description: "upsert bulk quotes from relayer." +sidebar_label: "Upsert quotes" +hide_title: true +hide_table_of_contents: true +api: eJyNVMGOmzAQ/RXkc9pEPea4VU89tKo2p3SFBhjAG8DesdkNQvx7ZzDZ0IRFCxIy4+fx87w37lWGLiVtvTaN2qvWOiQfJW11il5a49FFOZk6IqygQ/qqNorwpUXnH0zWqX2vUtN4bLwMwdpKpyCpts9O8vXKpSXWICNLxnJujU7+QnIZaY+1uwcwLx9DbdqQ23cWmZ/zpJtCDZswn5agm1hnM4RmOgXSOyQH/iSkswJjyDJC5z7O580JmxG2iMn1GbM4R1ycreEcG9IFM1rhPSHWmU+gz3Kf4KvsGTZFTPKMqVfXABBBt4QYQl1m/mDpqYssELBmwQuakE/hqcVBAs6axgUJv+12k5KzDL9+hp2gYMzx4oMnLh/60nAmZVvPqS34kn+2YsV4Qm2U2OrP1YA/zlDbCueGOt5Y51KAO8fs1gxys2pe2evUzA7X4IILrpN34u/Wtb5buURkeOJ66iY3l3aEVKwnRdZeiqN4/SuSCwKI8NY4X8PYoA3UAjmExn8v9H+izbr8MzfE5CKPZ7+1FR9VtmypGpt8lPWo5rKy+CUTknDfJ+DwQNUwSHi0G8d5+AqkIZHjHOW8JULGvSJyn5Ah6ntg+OVRNhd41Y7evr2ThrnVfh8eGZtMV1ltMllC8CbW5u9e/eVXqj8WYjT1GO9VBU3RQiH4kFaefxaR3uo= +sidebar_class_name: "put api-method" +custom_edit_url: null +--- + +import ApiTabs from "@theme/ApiTabs"; +import DiscriminatorTabs from "@theme/DiscriminatorTabs"; +import MethodEndpoint from "@theme/ApiExplorer/MethodEndpoint"; +import SecuritySchemes from "@theme/ApiExplorer/SecuritySchemes"; +import MimeTabs from "@theme/MimeTabs"; +import ParamsItem from "@theme/ParamsItem"; +import ResponseSamples from "@theme/ResponseSamples"; +import SchemaItem from "@theme/SchemaItem"; +import SchemaTabs from "@theme/SchemaTabs"; +import Markdown from "@theme/Markdown"; +import Heading from "@theme/Heading"; +import OperationTabs from "@theme/OperationTabs"; +import TabItem from "@theme/TabItem"; + + + + + + + + + + +upsert bulk quotes from relayer. + + + + + + +
+ +

+ Body +

+ required + +
+
+ + + query params + + +
+
    + +
    + + + + quotes + + object[] + + +
    +
  • +
    + Array [ +
    +
  • + + + + + + + + + + + + + + + + + +
  • +
    + ] +
    +
  • +
    +
    +
    +
+
+
+
+
+ + +
+ + + OK + + +
+ +
+
+
+
+
+ \ No newline at end of file diff --git a/services/rfq/api/client/client.go b/services/rfq/api/client/client.go index 2e878afaf2..38a16ced13 100644 --- a/services/rfq/api/client/client.go +++ b/services/rfq/api/client/client.go @@ -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 } @@ -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 +} + func (c *clientImpl) PutRelayAck(ctx context.Context, req *model.PutAckRequest) (*model.PutRelayAckResponse, error) { var ack *model.PutRelayAckResponse resp, err := c.rClient.R(). diff --git a/services/rfq/api/client/client_test.go b/services/rfq/api/client/client_test.go index f5d5f12364..bfc8dc3483 100644 --- a/services/rfq/api/client/client_test.go +++ b/services/rfq/api/client/client_test.go @@ -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]) +} + func (c *ClientSuite) TestGetSpecificQuote() { req := model.PutQuoteRequest{ OriginChainID: 1, diff --git a/services/rfq/api/client/suite_test.go b/services/rfq/api/client/suite_test.go index faf9e6c35d..e87436fcca 100644 --- a/services/rfq/api/client/suite_test.go +++ b/services/rfq/api/client/suite_test.go @@ -58,6 +58,15 @@ func NewTestClientSuite(tb testing.TB) *ClientSuite { func (c *ClientSuite) SetupTest() { c.TestSuite.SetupTest() + metricsHandler := metrics.NewNullHandler() + c.handler = metricsHandler + dbType, err := dbcommon.DBTypeFromString("sqlite") + c.Require().NoError(err) + // TODO use temp file / in memory sqlite3 to not create in directory files + testDB, err := sql.Connect(c.GetSuiteContext(), dbType, filet.TmpDir(c.T(), ""), metricsHandler) + c.Require().NoError(err) + c.database = testDB + testOmnirpc := omnirpcHelper.NewOmnirpcServer(c.GetTestContext(), c.T(), c.omniRPCTestBackends...) omniRPCClient := omniClient.NewOmnirpcClient(testOmnirpc, c.handler, omniClient.WithCaptureReqRes()) c.omniRPCClient = omniRPCClient @@ -175,14 +184,6 @@ func (c *ClientSuite) SetupSuite() { c.T().Fatal(err) } - dbType, err := dbcommon.DBTypeFromString("sqlite") - c.Require().NoError(err) - metricsHandler := metrics.NewNullHandler() - c.handler = metricsHandler - // TODO use temp file / in memory sqlite3 to not create in directory files - testDB, err := sql.Connect(c.GetSuiteContext(), dbType, filet.TmpDir(c.T(), ""), metricsHandler) - c.Require().NoError(err) - c.database = testDB // setup config } diff --git a/services/rfq/api/db/api_db.go b/services/rfq/api/db/api_db.go index dc84aa7f65..48c7344484 100644 --- a/services/rfq/api/db/api_db.go +++ b/services/rfq/api/db/api_db.go @@ -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. diff --git a/services/rfq/api/db/sql/base/store.go b/services/rfq/api/db/sql/base/store.go index 0c44c59930..8ef37607e8 100644 --- a/services/rfq/api/db/sql/base/store.go +++ b/services/rfq/api/db/sql/base/store.go @@ -3,6 +3,7 @@ package base import ( "context" "fmt" + "gorm.io/gorm/clause" "github.com/synapsecns/sanguine/services/rfq/api/db" @@ -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 +} diff --git a/services/rfq/api/docs/docs.go b/services/rfq/api/docs/docs.go index fdd79b976b..1edb775079 100644 --- a/services/rfq/api/docs/docs.go +++ b/services/rfq/api/docs/docs.go @@ -46,6 +46,37 @@ const docTemplate = `{ } } }, + "/bulk_quotes": { + "put": { + "description": "upsert bulk quotes from relayer.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "quotes" + ], + "summary": "Upsert quotes", + "parameters": [ + { + "description": "query params", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/model.PutBulkQuotesRequest" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, "/quotes": { "get": { "description": "get quotes from all relayers.", @@ -184,6 +215,17 @@ const docTemplate = `{ } } }, + "model.PutBulkQuotesRequest": { + "type": "object", + "properties": { + "quotes": { + "type": "array", + "items": { + "$ref": "#/definitions/model.PutQuoteRequest" + } + } + } + }, "model.PutQuoteRequest": { "type": "object", "properties": { diff --git a/services/rfq/api/docs/swagger.json b/services/rfq/api/docs/swagger.json index 0f4707ab3f..45ee72b695 100644 --- a/services/rfq/api/docs/swagger.json +++ b/services/rfq/api/docs/swagger.json @@ -35,6 +35,37 @@ } } }, + "/bulk_quotes": { + "put": { + "description": "upsert bulk quotes from relayer.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "quotes" + ], + "summary": "Upsert quotes", + "parameters": [ + { + "description": "query params", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/model.PutBulkQuotesRequest" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, "/quotes": { "get": { "description": "get quotes from all relayers.", @@ -173,6 +204,17 @@ } } }, + "model.PutBulkQuotesRequest": { + "type": "object", + "properties": { + "quotes": { + "type": "array", + "items": { + "$ref": "#/definitions/model.PutQuoteRequest" + } + } + } + }, "model.PutQuoteRequest": { "type": "object", "properties": { diff --git a/services/rfq/api/docs/swagger.yaml b/services/rfq/api/docs/swagger.yaml index a95f1dcddc..e2b9376218 100644 --- a/services/rfq/api/docs/swagger.yaml +++ b/services/rfq/api/docs/swagger.yaml @@ -43,6 +43,13 @@ definitions: description: UpdatedAt is the time that the quote was last upserted type: string type: object + model.PutBulkQuotesRequest: + properties: + quotes: + items: + $ref: '#/definitions/model.PutQuoteRequest' + type: array + type: object model.PutQuoteRequest: properties: dest_amount: @@ -87,6 +94,26 @@ paths: summary: Relay ack tags: - ack + /bulk_quotes: + put: + consumes: + - application/json + description: upsert bulk quotes from relayer. + parameters: + - description: query params + in: body + name: request + required: true + schema: + $ref: '#/definitions/model.PutBulkQuotesRequest' + produces: + - application/json + responses: + "200": + description: OK + summary: Upsert quotes + tags: + - quotes /quotes: get: consumes: diff --git a/services/rfq/api/model/request.go b/services/rfq/api/model/request.go index f3ce466ce1..cff2db2161 100644 --- a/services/rfq/api/model/request.go +++ b/services/rfq/api/model/request.go @@ -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"` diff --git a/services/rfq/api/rest/handler.go b/services/rfq/api/rest/handler.go index 7722294b6b..7f982a5949 100644 --- a/services/rfq/api/rest/handler.go +++ b/services/rfq/api/rest/handler.go @@ -1,6 +1,7 @@ package rest import ( + "fmt" "net/http" "strconv" @@ -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), @@ -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. diff --git a/services/rfq/api/rest/server.go b/services/rfq/api/rest/server.go index 886c7d075c..03cfcdeed5 100644 --- a/services/rfq/api/rest/server.go +++ b/services/rfq/api/rest/server.go @@ -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 @@ -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) @@ -187,8 +192,8 @@ 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 { @@ -196,14 +201,23 @@ func (r *QuoterAPIServer) AuthMiddleware() gin.HandlerFunc { 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: @@ -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 diff --git a/services/rfq/relayer/quoter/quoter.go b/services/rfq/relayer/quoter/quoter.go index 6e182426dd..b607e2c2c6 100644 --- a/services/rfq/relayer/quoter/quoter.go +++ b/services/rfq/relayer/quoter/quoter.go @@ -270,21 +270,31 @@ func (m *Manager) prepareAndSubmitQuotes(ctx context.Context, inv map[int]map[co span.SetAttributes(attribute.Int("num_quotes", len(allQuotes))) // Now, submit all the generated quotes - for _, quote := range allQuotes { - if err := m.submitQuote(ctx, quote); err != nil { - span.AddEvent("error submitting quote; setting relayPaused to true", trace.WithAttributes( - attribute.String("error", err.Error()), - attribute.Int(metrics.Origin, quote.OriginChainID), - attribute.Int(metrics.Destination, quote.DestChainID), - attribute.String("origin_token_addr", quote.OriginTokenAddr), - attribute.String("dest_token_addr", quote.DestTokenAddr), - attribute.String("max_origin_amount", quote.MaxOriginAmount), - attribute.String("dest_amount", quote.DestAmount), - )) + if m.config.SubmitSingleQuotes { + for _, quote := range allQuotes { + if err := m.submitQuote(ctx, quote); err != nil { + span.AddEvent("error submitting quote; setting relayPaused to true", trace.WithAttributes( + attribute.String("error", err.Error()), + attribute.Int(metrics.Origin, quote.OriginChainID), + attribute.Int(metrics.Destination, quote.DestChainID), + attribute.String("origin_token_addr", quote.OriginTokenAddr), + attribute.String("dest_token_addr", quote.DestTokenAddr), + attribute.String("max_origin_amount", quote.MaxOriginAmount), + attribute.String("dest_amount", quote.DestAmount), + )) + m.relayPaused.Store(true) + + // Suppress error so that we can continue submitting quotes + return nil + } + } + } else { + err = m.submitBulkQuotes(ctx, allQuotes) + if err != nil { + span.AddEvent("error submitting bulk quotes; setting relayPaused to true", trace.WithAttributes( + attribute.String("error", err.Error()))) m.relayPaused.Store(true) - - // Suppress error so that we can continue submitting quotes - return nil + return fmt.Errorf("error submitting bulk quotes: %w", err) } } @@ -600,3 +610,18 @@ func (m *Manager) submitQuote(ctx context.Context, quote model.PutQuoteRequest) } return nil } + +// Submits multiple quotes. +func (m *Manager) submitBulkQuotes(ctx context.Context, quotes []model.PutQuoteRequest) error { + quoteCtx, quoteCancel := context.WithTimeout(ctx, m.config.GetQuoteSubmissionTimeout()) + defer quoteCancel() + + req := model.PutBulkQuotesRequest{ + Quotes: quotes, + } + err := m.rfqClient.PutBulkQuotes(quoteCtx, &req) + if err != nil { + return fmt.Errorf("error submitting bulk quotes: %w", err) + } + return nil +} diff --git a/services/rfq/relayer/relconfig/config.go b/services/rfq/relayer/relconfig/config.go index d0cdce49c6..344f2c7b07 100644 --- a/services/rfq/relayer/relconfig/config.go +++ b/services/rfq/relayer/relconfig/config.go @@ -55,6 +55,8 @@ type Config struct { EnableAPIWithdrawals bool `yaml:"enable_api_withdrawals"` // WithdrawalWhitelist is a list of addresses that are allowed to withdraw. WithdrawalWhitelist []string `yaml:"withdrawal_whitelist"` + // SubmitSingleQuotes enables submitting single quotes. + SubmitSingleQuotes bool `yaml:"submit_single_quotes"` } // ChainConfig represents the configuration for a chain.