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:
2026-09-09 01:09:39 -07:00
co-authored by Hanzo Dev
parent fb30703324
commit d789ffebb3
4 changed files with 80 additions and 36 deletions
+34 -13
View File
@@ -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
+39
View File
@@ -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 -9
View File
@@ -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 {
-14
View File
@@ -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 {