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

feat(rfq): active quoting API #3128

Merged
merged 109 commits into from
Oct 2, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
109 commits
Select commit Hold shift + click to select a range
bb18412
WIP: initial websocket wiring
dwasse Sep 13, 2024
31e52d8
WIP: add ws client and handling
dwasse Sep 13, 2024
e8ab231
Fix: receive respsects context
dwasse Sep 13, 2024
782cffd
Cleanup: split into rfq.go
dwasse Sep 13, 2024
6344a37
Fix: build
dwasse Sep 13, 2024
8aa16cb
Feat: add mocked ws client
dwasse Sep 13, 2024
4764926
Fix: build
dwasse Sep 16, 2024
afb2f19
Feat: add SubscribeActiveQuotes() to client
dwasse Sep 16, 2024
e30cd63
Feat: add PutUserQuoteRequest() to api client
dwasse Sep 16, 2024
3c10c02
Fix: build
dwasse Sep 16, 2024
bdae4ca
WIP: rfq tests with ws auth
dwasse Sep 16, 2024
138297d
WIP: test fixes
dwasse Sep 17, 2024
fc1ea97
Feat: working TestHandleActiveRFQ
dwasse Sep 17, 2024
6ae7a71
Feat: add expired request case
dwasse Sep 17, 2024
7cdcade
WIP: functionalize test relayer resps
dwasse Sep 17, 2024
01d83dc
Feat: add runMockRelayer with multiple relayers
dwasse Sep 17, 2024
ee408a9
Feat: add MultipleRelayers case
dwasse Sep 17, 2024
94a8f4d
Feat: add FallbackToPassive case
dwasse Sep 17, 2024
c39d62c
Fix: bigint ptr issue
dwasse Sep 17, 2024
6beb23a
Cleanup: bump expiration window
dwasse Sep 17, 2024
fdf9d12
WIP: logs
dwasse Sep 17, 2024
e23175f
Feat: split into separate tests
dwasse Sep 17, 2024
4b99340
Cleanup: logs
dwasse Sep 17, 2024
c557a28
Feat: add PassiveBestQuote case
dwasse Sep 17, 2024
888ce50
WIP: update db interface with new models
dwasse Sep 17, 2024
3293166
Feat: impl new db funcs
dwasse Sep 17, 2024
63f1a1e
Feat: insert models within api server
dwasse Sep 17, 2024
594d6ea
Feat: update quote request / response statuses
dwasse Sep 17, 2024
7e7c5a1
Fix: db error handling
dwasse Sep 17, 2024
7dcdf59
Fix: api tests
dwasse Sep 17, 2024
8cae8e4
Feat: add initial response validation
dwasse Sep 17, 2024
94ee250
Feat: impl pingpong
dwasse Sep 18, 2024
46d04e2
Fix: register models
dwasse Sep 18, 2024
36701ba
Feat: verify quote request in SingleRelayer case
dwasse Sep 18, 2024
2616b54
Feat: verify more db requests
dwasse Sep 18, 2024
fe7a774
Cleanup: common vars
dwasse Sep 18, 2024
60db841
Cleanup: break down handleActiveRFQ into sub funcs
dwasse Sep 18, 2024
83b7f6d
Cleanup: comments
dwasse Sep 18, 2024
32065ee
Cleanup: remove unused mock
dwasse Sep 18, 2024
c5e9a00
Fix: builds
dwasse Sep 18, 2024
e7d08e7
Feat: make relayer response data optional to signify null resp
dwasse Sep 19, 2024
8e405e5
Fix: response primary key on quote id
dwasse Sep 19, 2024
c6db31f
Fix: build
dwasse Sep 19, 2024
7812573
Feat: update swagger docs
dwasse Sep 19, 2024
a01fb9a
WIP: generic pubsub
dwasse Sep 20, 2024
c27ef32
Feat: add basic PubSubManager
dwasse Sep 20, 2024
b296da8
Feat: implement subscription / unsubscription operations
dwasse Sep 20, 2024
4384fb2
Feat: respond to subscribe operation
dwasse Sep 20, 2024
be695ea
Feat: add runWsListener helper
dwasse Sep 20, 2024
5656bae
Cleanup: reduce chan buffer
dwasse Sep 20, 2024
1c3870c
Cleanup: lints
dwasse Sep 20, 2024
2051b30
Cleanup: break down into smaller funcs
dwasse Sep 20, 2024
f0928c4
Cleanup: refactor ws client
dwasse Sep 20, 2024
4683974
Cleanup: more lints
dwasse Sep 20, 2024
33c24a3
Fix: build
dwasse Sep 20, 2024
7aee229
Cleanup: lints
dwasse Sep 20, 2024
ff0aece
Feat: mark as fulfilled when updating request status
dwasse Sep 20, 2024
91c1bf5
Cleanup: lint
dwasse Sep 20, 2024
cdee6ea
Skip broken test for now
dwasse Sep 20, 2024
8ccbb3f
Cleanup: lint
dwasse Sep 20, 2024
f2920e2
Feat: add open_quote_requests endpoint with test
dwasse Sep 20, 2024
83a3603
Feat: add new open request model
dwasse Sep 20, 2024
f112235
Update swagger
dwasse Sep 20, 2024
292cd37
go mod tidy
trajan0x Sep 23, 2024
dd961c1
fix error
trajan0x Sep 23, 2024
2368313
Fix: respecting context
dwasse Sep 24, 2024
d7948d4
Replace: Fulfilled -> Closed
dwasse Sep 24, 2024
161ea2e
Cleanup: use errors.New()
dwasse Sep 24, 2024
c240cd3
Feat: ReceiveQuoteResponse specifies request id
dwasse Sep 25, 2024
c8b5435
Cleanup: remove logs
dwasse Sep 25, 2024
7fa8003
Feat: add some tracing
dwasse Sep 25, 2024
b05e6b7
Feat: add IntegratorID
dwasse Sep 25, 2024
f2a4be9
Feat: remove repetitive fields from relayer quote response, move requ…
dwasse Sep 25, 2024
f203e7c
Cleanup: use new routes
dwasse Sep 25, 2024
0835aae
Cleanup: migrate req/res struct naming
dwasse Sep 25, 2024
2996aaa
Cleanup: update swagger
dwasse Sep 25, 2024
89c565e
Cleanup: lint
dwasse Sep 25, 2024
0a2b46a
[goreleaser]
dwasse Sep 25, 2024
8850cf0
Feat: run ws endpoint within existing server
dwasse Sep 27, 2024
2bae6b1
[goreleaser]
dwasse Sep 27, 2024
af384d4
Fix: build
dwasse Sep 27, 2024
3ae9552
[goreleaser]
dwasse Sep 27, 2024
d3f839f
Feat: add more tracing
dwasse Sep 27, 2024
925617e
[goreleaser]
dwasse Sep 27, 2024
7ff7c81
feat(rfq-relayer): relayer supports active quoting (#3198)
dwasse Sep 30, 2024
99c9d5c
Fix: build
dwasse Sep 30, 2024
6d6d172
Cleanup: lint
dwasse Sep 30, 2024
c40dada
Cleanup: lint
dwasse Oct 1, 2024
7878364
Cleanup: update swagger
dwasse Oct 1, 2024
2c46bcb
Feat: client sends pings, server sends pongs
dwasse Oct 1, 2024
1025c6c
[goreleaser]
dwasse Oct 1, 2024
65ddc92
Cleanup: remove unused func
dwasse Oct 1, 2024
16b3a5b
WIP: ws error handling
dwasse Oct 1, 2024
d71d686
[goreleaser]
dwasse Oct 1, 2024
a0591d6
Feat: ws client uses errgroup
dwasse Oct 1, 2024
3bc93ab
Cleanup: remove log
dwasse Oct 1, 2024
aa50d07
[goreleaser]
dwasse Oct 1, 2024
b4a25e1
Replace: PutUserQuoteResponse -> PutRFQResponse
dwasse Oct 1, 2024
26c6bbc
Feat: add QuoteID to PutRFQResponse
dwasse Oct 1, 2024
04ff76b
[goreleaser]
dwasse Oct 1, 2024
3324e53
Cleanup: lint
dwasse Oct 2, 2024
cb7dde0
Fix: build
dwasse Oct 2, 2024
cbc6e18
Cleanup: lint
dwasse Oct 2, 2024
5cb6050
[goreleaser]
dwasse Oct 2, 2024
e687ece
Add logs
dwasse Oct 2, 2024
c8a5868
[goreleaser]
dwasse Oct 2, 2024
8bad457
Add logs
dwasse Oct 2, 2024
7e88a97
[goreleaser]
dwasse Oct 2, 2024
526f2af
Cleanup: remove logs
dwasse Oct 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions services/rfq/api/model/request.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package model

// PutQuoteRequest contains the schema for a PUT /quote request.
type PutQuoteRequest struct {
// PutRelayerQuoteRequest contains the schema for a PUT /quote request.
type PutRelayerQuoteRequest struct {
OriginChainID int `json:"origin_chain_id"`
OriginTokenAddr string `json:"origin_token_addr"`
DestChainID int `json:"dest_chain_id"`
Expand All @@ -15,7 +15,7 @@ type PutQuoteRequest struct {

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

// PutAckRequest contains the schema for a PUT /ack request.
Expand Down
72 changes: 72 additions & 0 deletions services/rfq/api/model/response.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
package model

import (
"time"

"github.com/google/uuid"
)

// GetQuoteResponse contains the schema for a GET /quote response.
type GetQuoteResponse struct {
// OriginChainID is the chain which the relayer is willing to relay from
Expand Down Expand Up @@ -41,3 +47,69 @@ type GetContractsResponse struct {
// Contracts is a map of chain id to contract address
Contracts map[uint32]string `json:"contracts"`
}

// ActiveRFQMessage represents the general structure of WebSocket messages for Active RFQ
Copy link
Contributor

Choose a reason for hiding this comment

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

Add a period at the end of the comment.

Per the linting rule godot, comments should end with a period.

Apply this diff to fix the comment:

-// ActiveRFQMessage represents the general structure of WebSocket messages for Active RFQ
+// ActiveRFQMessage represents the general structure of WebSocket messages for Active RFQ.
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
// ActiveRFQMessage represents the general structure of WebSocket messages for Active RFQ
// ActiveRFQMessage represents the general structure of WebSocket messages for Active RFQ.
Tools
GitHub Check: Lint (services/rfq)

[failure] 50-50:
Comment should end in a period (godot)

type ActiveRFQMessage struct {
Op string `json:"op"`
Content interface{} `json:"content"`
Success bool `json:"success"`
}

// PutUserQuoteRequest represents a user request for quote.
type PutUserQuoteRequest struct {
UserAddress string `json:"user_address"`
QuoteTypes []string `json:"quote_types"`
Data QuoteData `json:"data"`
}

// PutUserQuoteResponse represents a response to a user quote request.
type PutUserQuoteResponse struct {
Success bool `json:"success"`
Reason string `json:"reason"`
UserAddress string `json:"user_address"`
QuoteType string `json:"quote_type"`
Data QuoteData `json:"data"`
}

// QuoteRequest represents a request for a quote
Copy link
Contributor

Choose a reason for hiding this comment

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

Add a period at the end of the comment.

Per the linting rule godot, comments should end with a period.

Apply this diff:

-// QuoteRequest represents a request for a quote
+// QuoteRequest represents a request for a quote.
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
// QuoteRequest represents a request for a quote
// QuoteRequest represents a request for a quote.
Tools
GitHub Check: Lint (services/rfq)

[failure] 73-73:
Comment should end in a period (godot)

type QuoteRequest struct {
RequestID string `json:"request_id"`
Data QuoteData `json:"data"`
CreatedAt time.Time `json:"created_at"`
}

// QuoteData represents the data within a quote request
Copy link
Contributor

Choose a reason for hiding this comment

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

Add a period at the end of the comment.

Per the linting rule godot, comments should end with a period.

Apply this diff:

-// QuoteData represents the data within a quote request
+// QuoteData represents the data within a quote request.
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
// QuoteData represents the data within a quote request
// QuoteData represents the data within a quote request.
Tools
GitHub Check: Lint (services/rfq)

[failure] 80-80:
Comment should end in a period (godot)

type QuoteData struct {
OriginChainID int `json:"origin_chain_id"`
DestChainID int `json:"dest_chain_id"`
OriginTokenAddr string `json:"origin_token_addr"`
DestTokenAddr string `json:"dest_token_addr"`
OriginAmount string `json:"origin_amount"`
ExpirationWindow int64 `json:"expiration_window"`
DestAmount *string `json:"dest_amount"`
RelayerAddress *string `json:"relayer_address"`
}

// RelayerWsQuoteRequest represents a request for a quote to a relayer
type RelayerWsQuoteRequest struct {
RequestID string `json:"request_id"`
Data QuoteData `json:"data"`
CreatedAt time.Time `json:"created_at"`
}

// NewRelayerWsQuoteRequest creates a new RelayerWsQuoteRequest
func NewRelayerWsQuoteRequest(data QuoteData) *RelayerWsQuoteRequest {
return &RelayerWsQuoteRequest{
RequestID: uuid.New().String(),
Data: data,
CreatedAt: time.Now(),
}
}

// RelayerWsQuoteResponse represents a response to a quote request
type RelayerWsQuoteResponse struct {
RequestID string `json:"request_id"`
QuoteID string `json:"quote_id"`
Data QuoteData `json:"data"`
UpdatedAt time.Time `json:"updated_at"`
}
4 changes: 2 additions & 2 deletions services/rfq/api/rest/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ func (h *Handler) ModifyQuote(c *gin.Context) {
c.JSON(http.StatusBadRequest, gin.H{"error": "No relayer address recovered from signature"})
return
}
putRequest, ok := req.(*model.PutQuoteRequest)
putRequest, ok := req.(*model.PutRelayerQuoteRequest)
if !ok {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request type"})
return
Expand Down Expand Up @@ -133,7 +133,7 @@ func (h *Handler) ModifyBulkQuotes(c *gin.Context) {
c.Status(http.StatusOK)
}

func parseDBQuote(putRequest model.PutQuoteRequest, relayerAddr interface{}) (*db.Quote, error) {
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")
Expand Down
81 changes: 81 additions & 0 deletions services/rfq/api/rest/mocks/ws_client.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

129 changes: 129 additions & 0 deletions services/rfq/api/rest/rfq.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package rest

import (
"context"
"fmt"
"math/big"
"sync"
"time"

"github.com/synapsecns/sanguine/services/rfq/api/model"
)

func getBestQuote(a, b *model.QuoteData) *model.QuoteData {
if a == nil && b == nil {
return nil
}
if a == nil {
return b
}
if b == nil {
return a
}
aAmount, _ := new(big.Int).SetString(*a.DestAmount, 10)
bAmount, _ := new(big.Int).SetString(*b.DestAmount, 10)
if aAmount.Cmp(bAmount) > 0 {
return a
}
return b
}

func (r *QuoterAPIServer) handleActiveRFQ(ctx context.Context, request *model.PutUserQuoteRequest) (quote *model.QuoteData) {
rfqCtx, _ := context.WithTimeout(ctx, time.Duration(request.Data.ExpirationWindow)*time.Millisecond)

// publish the quote request to all connected clients
relayerReq := model.NewRelayerWsQuoteRequest(request.Data)
r.wsClients.Range(func(key string, client WsClient) bool {
client.SendQuoteRequest(rfqCtx, relayerReq)
return true
})

// collect responses from all clients until expiration window closes
wg := sync.WaitGroup{}
respMux := sync.Mutex{}
responses := map[string]*model.RelayerWsQuoteResponse{}
r.wsClients.Range(func(key string, client WsClient) bool {
wg.Add(1)
go func(client WsClient) {
defer wg.Done()
resp, err := client.ReceiveQuoteResponse(rfqCtx)
if err != nil {
logger.Error("Error receiving quote response", "error", err)
return
}
respMux.Lock()
responses[key] = resp
respMux.Unlock()
}(client)
return true
})

select {
case <-rfqCtx.Done():
// Context expired before all responses were received
case <-func() chan struct{} {
ch := make(chan struct{})
go func() {
wg.Wait()
close(ch)
}()
return ch
}():
// All responses received
}

// construct the response
// at this point, all responses should have been validated
for _, resp := range responses {
quote = getBestQuote(quote, &resp.Data)
}

return quote
}

func (r *QuoterAPIServer) handlePassiveRFQ(ctx context.Context, request *model.PutUserQuoteRequest) (*model.QuoteData, error) {
quotes, err := r.db.GetQuotesByOriginAndDestination(ctx, uint64(request.Data.OriginChainID), request.Data.OriginTokenAddr, uint64(request.Data.DestChainID), request.Data.DestTokenAddr)
if err != nil {
return nil, fmt.Errorf("failed to get quotes: %w", err)
}

originAmount, ok := new(big.Int).SetString(request.Data.OriginAmount, 10)
if !ok {
return nil, fmt.Errorf("invalid origin amount")
}
dwasse marked this conversation as resolved.
Show resolved Hide resolved

var bestQuote *model.QuoteData
for _, quote := range quotes {
quoteOriginAmount, ok := new(big.Int).SetString(quote.MaxOriginAmount.String(), 10)
if !ok {
continue
}
if quoteOriginAmount.Cmp(originAmount) < 0 {
continue
}
quotePrice := new(big.Float).Quo(
new(big.Float).SetInt(quote.DestAmount.BigInt()),
new(big.Float).SetInt(quote.MaxOriginAmount.BigInt()),
)

rawDestAmount := new(big.Float).Mul(
new(big.Float).SetInt(originAmount),
quotePrice,
)

rawDestAmountInt, _ := rawDestAmount.Int(nil)
destAmount := new(big.Int).Sub(rawDestAmountInt, quote.FixedFee.BigInt()).String()
quoteData := &model.QuoteData{
OriginChainID: int(quote.OriginChainID),
DestChainID: int(quote.DestChainID),
OriginTokenAddr: quote.OriginTokenAddr,
DestTokenAddr: quote.DestTokenAddr,
OriginAmount: quote.MaxOriginAmount.String(),
DestAmount: &destAmount,
RelayerAddress: &quote.RelayerAddr,
}
bestQuote = getBestQuote(bestQuote, quoteData)
}

return bestQuote, nil
}
Loading
Loading