-
Notifications
You must be signed in to change notification settings - Fork 33
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
Changes from 6 commits
bb18412
31e52d8
e8ab231
782cffd
6344a37
8aa16cb
4764926
afb2f19
e30cd63
3c10c02
bdae4ca
138297d
fc1ea97
6ae7a71
7cdcade
01d83dc
ee408a9
94a8f4d
c39d62c
6beb23a
fdf9d12
e23175f
4b99340
c557a28
888ce50
3293166
63f1a1e
594d6ea
7e7c5a1
7dcdf59
8cae8e4
94ee250
46d04e2
36701ba
2616b54
fe7a774
60db841
83b7f6d
32065ee
c5e9a00
e7d08e7
8e405e5
c6db31f
7812573
a01fb9a
c27ef32
b296da8
4384fb2
be695ea
5656bae
1c3870c
2051b30
f0928c4
4683974
33c24a3
7aee229
ff0aece
91c1bf5
cdee6ea
8ccbb3f
f2920e2
83a3603
f112235
292cd37
dd961c1
2368313
d7948d4
161ea2e
c240cd3
c8b5435
7fa8003
b05e6b7
f2a4be9
f203e7c
0835aae
2996aaa
89c565e
0a2b46a
8850cf0
2bae6b1
af384d4
3ae9552
d3f839f
925617e
7ff7c81
99c9d5c
6d6d172
c40dada
7878364
2c46bcb
1025c6c
65ddc92
16b3a5b
d71d686
a0591d6
3bc93ab
aa50d07
b4a25e1
26c6bbc
04ff76b
3324e53
cb7dde0
cbc6e18
5cb6050
e687ece
c8a5868
8bad457
7e88a97
526f2af
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 | ||||||
|
@@ -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 | ||||||
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 | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 Apply this diff: -// QuoteRequest represents a request for a quote
+// QuoteRequest represents a request for a quote. Committable suggestion
Suggested change
ToolsGitHub Check: Lint (services/rfq)
|
||||||
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 | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 Apply this diff: -// QuoteData represents the data within a quote request
+// QuoteData represents the data within a quote request. Committable suggestion
Suggested change
ToolsGitHub Check: Lint (services/rfq)
|
||||||
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"` | ||||||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
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: "e.RelayerAddr, | ||
} | ||
bestQuote = getBestQuote(bestQuote, quoteData) | ||
} | ||
|
||
return bestQuote, nil | ||
} |
There was a problem hiding this comment.
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:
Committable suggestion
Tools
GitHub Check: Lint (services/rfq)