mirror of
https://github.com/luxfi/dex.git
synced 2026-09-24 00:26:31 +00:00
One way to find a chain's arms, and one request conversion
The swap looked for the venue that had priced the trade in the per-chain set alone, while the quote falls back to the single set a one-chain deployment is handed. Configured the second way, this surface priced a pair and then reported the venue that had just priced it as unregistered. Both go through Server.arms now, and Server.arm is that set looked up by the name a venue calls itself. The swap also rebuilt the quote request by hand beside convertQuoteRequest, which does the same thing for the quote path — six of seven fields, twice, free to drift about what slippage or isExactIn mean. swapRequest embeds quoteRequest; encoding/json flattens it, so the wire shape is unchanged and there is one conversion. Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
+34
-13
@@ -338,6 +338,33 @@ func (s *Server) whyNothingHeld(asked QuoteRequest) error {
|
||||
return fmt.Errorf("no venue here holds this pair")
|
||||
}
|
||||
|
||||
// arms is the venues that read one chain: the per-chain set where there is
|
||||
// one, and otherwise the single set a one-chain deployment was handed.
|
||||
//
|
||||
// Every path that needs a venue asks this. The swap used to look only at the
|
||||
// per-chain set, so a deployment configured the other way could price a pair
|
||||
// and then report the venue that had just priced it as unregistered.
|
||||
func (s *Server) arms(chain ChainID) *VenueRouter {
|
||||
if v := s.chains.For(chain); v != nil {
|
||||
return v
|
||||
}
|
||||
return s.venues
|
||||
}
|
||||
|
||||
// arm is one of them, by the name it calls itself.
|
||||
func (s *Server) arm(chain ChainID, name string) Venue {
|
||||
venues := s.arms(chain)
|
||||
if venues == nil {
|
||||
return nil
|
||||
}
|
||||
for _, v := range venues.Venues() {
|
||||
if v.Name() == name {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// settles reports whether this deployment reads a chain from its own pools.
|
||||
// Where it does, "no providers available" is never the true answer — it names
|
||||
// an upstream registry the caller did not ask about.
|
||||
@@ -349,10 +376,7 @@ func (s *Server) settles(chain ChainID) bool {
|
||||
// one holds, best first. Empty when none of them holds the pair — which is a
|
||||
// question for the providers and not an answer of its own.
|
||||
func (s *Server) venueQuotes(ctx context.Context, req QuoteRequest) []SwapQuote {
|
||||
venues := s.chains.For(req.ChainID)
|
||||
if venues == nil {
|
||||
venues = s.venues
|
||||
}
|
||||
venues := s.arms(req.ChainID)
|
||||
if venues == nil || len(venues.Venues()) == 0 {
|
||||
return nil
|
||||
}
|
||||
@@ -474,15 +498,12 @@ func (s *Server) handleSwap(w http.ResponseWriter, r *http.Request) {
|
||||
// registry for the price and then encoding it for a fixed precompile —
|
||||
// which is what this did — quotes a router on one chain and addresses a
|
||||
// contract on another, in one answer.
|
||||
//
|
||||
// The same conversion /v1/trade/quote uses, on the same fields, so a swap
|
||||
// can never be priced by a question shaped differently from the one a
|
||||
// caller could have asked directly.
|
||||
ctx := s.requestContext(r)
|
||||
asked := QuoteRequest{
|
||||
TokenIn: Token{Address: req.TokenIn, ChainID: ChainID(req.ChainID)},
|
||||
TokenOut: Token{Address: req.TokenOut, ChainID: ChainID(req.ChainID)},
|
||||
Amount: amount,
|
||||
IsExactIn: req.IsExactIn,
|
||||
ChainID: ChainID(req.ChainID),
|
||||
Slippage: req.Slippage,
|
||||
}
|
||||
asked := s.convertQuoteRequest(req.quoteRequest)
|
||||
held := s.venueQuotes(ctx, asked)
|
||||
if len(held) == 0 {
|
||||
s.writeError(w, http.StatusNotFound, s.whyNothingHeld(asked))
|
||||
@@ -494,7 +515,7 @@ func (s *Server) handleSwap(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
venue := s.chains.Venue(asked.ChainID, best.ProviderName)
|
||||
venue := s.arm(asked.ChainID, best.ProviderName)
|
||||
if venue == nil {
|
||||
s.writeError(w, http.StatusInternalServerError, fmt.Errorf("venue %q priced this and is not registered", best.ProviderName))
|
||||
return
|
||||
|
||||
@@ -373,3 +373,42 @@ func TestExactOutputSaysItIsTheDirection(t *testing.T) {
|
||||
t.Fatalf("the pair is held after all: %d %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// A deployment handed one venue set for everything — which is what a
|
||||
// single-chain run does — must be able to swap on it, not only quote. The swap
|
||||
// used to look at the per-chain set alone, so it priced the pair and then
|
||||
// reported the venue that had just priced it as unregistered.
|
||||
func TestOneVenueSetCanSwapAsWellAsQuote(t *testing.T) {
|
||||
chain := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
ID uint64 `json:"id"`
|
||||
}
|
||||
json.NewDecoder(r.Body).Decode(&req)
|
||||
json.NewEncoder(w).Encode(map[string]any{
|
||||
"jsonrpc": "2.0", "id": req.ID, "result": "0x" + fmt.Sprintf("%064x", 997_000),
|
||||
})
|
||||
}))
|
||||
t.Cleanup(chain.Close)
|
||||
|
||||
s := NewServer(NewRouter(NewRegistry(), true), DefaultServerConfig(),
|
||||
WithVenues(NewVenueRouter(NewUniswapV3Venue(UniswapV3Config{
|
||||
RPCURL: chain.URL,
|
||||
QuoterAddress: testOther,
|
||||
RouterAddress: "0xbbD8d9A1E6bf5627A2F6D2aF38664bbe1cEF63Bb",
|
||||
}))))
|
||||
|
||||
body := map[string]any{
|
||||
"chainId": uint64(ChainIDLux), "tokenIn": testWETH, "tokenOut": testLUSD,
|
||||
"amount": "1000000000000000000", "isExactIn": true, "recipient": testOther,
|
||||
}
|
||||
if w := ask(s, http.MethodPost, "/v1/trade/quote", body); w.Code != http.StatusOK {
|
||||
t.Fatalf("quote: %d %s", w.Code, w.Body.String())
|
||||
}
|
||||
w := ask(s, http.MethodPost, "/v1/trade/swap", body)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("swap: %d %s", w.Code, w.Body.String())
|
||||
}
|
||||
if bytes.Contains(w.Body.Bytes(), []byte("not registered")) {
|
||||
t.Errorf("the venue that priced it was not found: %s", w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,16 +7,14 @@ package gateway
|
||||
// gone. Each venue builds its own transaction now (venue_swap.go), because the
|
||||
// venue that read the price is the only thing that knows what executes it.
|
||||
|
||||
// A swap is a quote plus where the proceeds go, so it is that request plus two
|
||||
// fields rather than a second copy of the same six. Embedding flattens in JSON,
|
||||
// so the shape on the wire is unchanged — and the two cannot drift about what
|
||||
// slippage or isExactIn mean.
|
||||
type swapRequest struct {
|
||||
TokenIn string `json:"tokenIn"`
|
||||
TokenOut string `json:"tokenOut"`
|
||||
ChainID uint64 `json:"chainId"`
|
||||
Amount string `json:"amount"`
|
||||
IsExactIn bool `json:"isExactIn"`
|
||||
// Slippage is a percentage. Zero means half a percent.
|
||||
Slippage float64 `json:"slippage,omitempty"`
|
||||
Recipient string `json:"recipient"`
|
||||
Deadline int64 `json:"deadline,omitempty"`
|
||||
quoteRequest
|
||||
Recipient string `json:"recipient"`
|
||||
Deadline int64 `json:"deadline,omitempty"`
|
||||
}
|
||||
|
||||
type swapResponse struct {
|
||||
|
||||
@@ -113,20 +113,6 @@ func (c *ChainRouters) RPC(chain ChainID) string {
|
||||
return c.rpc[chain]
|
||||
}
|
||||
|
||||
// Venue returns one chain's arm by the name it calls itself, or nil.
|
||||
func (c *ChainRouters) Venue(chain ChainID, name string) Venue {
|
||||
r := c.For(chain)
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
for _, v := range r.Venues() {
|
||||
if v.Name() == name {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// For returns the venues that read one chain, or nil.
|
||||
func (c *ChainRouters) For(chain ChainID) *VenueRouter {
|
||||
if c == nil {
|
||||
|
||||
Reference in New Issue
Block a user