mirror of
https://github.com/luxfi/dex.git
synced 2026-09-24 00:26:31 +00:00
The trading surface is served, and each chain is asked of whoever holds it
pkg/gateway has carried the whole thing for a while — quote, swap, approval, orders, pools, prices, tokens, stats, history — with a Uniswap provider that speaks the shapes at trade-api.gateway.uniswap.org, and nothing ever started it. `dexd run -http` mounts the D-Chain venue's own handlers under /v1/dex, which is a different surface for a different reader, so an interface asking for a quote reached nothing on any chain. It has an entrypoint now. Two things it did wrong the moment it ran, both the same mistake in different places. The router asked every provider about every chain, so a quote for a Lux market went to Uniswap and came back ACCESS_DENIED — somebody else's API error dressed as a fact about our liquidity. A provider is asked only about the chains it lists now, and one that lists none is taken to serve any, which is the shape a single-chain deployment has. And the venues were attached to the server and never read: `s.venues` was stored by an option nothing consulted, so the chain we settle ourselves was quoted by the chain we do not. Our own venues answer first — an upstream provider that also lists the chain is a second opinion about a market we settle — and a venue holding no liquidity returns nothing rather than an error, so falling through to the providers stays the honest next question. A chain of ours with no provider behind it then says no venue holds the pair, rather than "no providers available", which sends a reader looking for a misconfiguration that is not there. Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
// The trading surface, served.
|
||||
//
|
||||
// `pkg/gateway` has carried the whole thing for a while — the quote, swap,
|
||||
// approval and order paths, a router over venues, and a Uniswap provider that
|
||||
// speaks the shapes at trade-api.gateway.uniswap.org — and nothing started it.
|
||||
// `dexd run -http` mounts the D-Chain venue's own handlers under /v1/dex, which
|
||||
// is a different surface for a different reader. So an interface asking for a
|
||||
// quote reached nothing, on any chain.
|
||||
//
|
||||
// One binary, and one rule about what it answers: a market on a Lux chain is
|
||||
// quoted from our own contracts, and a market anywhere else is quoted upstream.
|
||||
// The response is the same shape either way, because a client should not have
|
||||
// to know which arm served it — that is the whole reason a gateway exists
|
||||
// rather than two clients in the interface.
|
||||
//
|
||||
// Everything it needs comes from the environment, because the addresses differ
|
||||
// per chain and per deployment and a binary that carries them is a binary that
|
||||
// is wrong somewhere.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/luxfi/dex/pkg/gateway"
|
||||
"github.com/luxfi/dex/pkg/gateway/uniswap"
|
||||
)
|
||||
|
||||
func env(name, fallback string) string {
|
||||
if v := strings.TrimSpace(os.Getenv(name)); v != "" {
|
||||
return v
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func main() {
|
||||
addr := env("GATEWAY_ADDR", ":8080")
|
||||
rpc := env("LUX_RPC", "https://api.lux.cloud/v1/chain/C/rpc")
|
||||
|
||||
chain, err := strconv.ParseUint(env("LUX_CHAIN_ID", "96369"), 10, 64)
|
||||
if err != nil {
|
||||
log.Fatalf("LUX_CHAIN_ID: %v", err)
|
||||
}
|
||||
|
||||
// Our own chain, quoted from our own contracts. The native venue reads the
|
||||
// DEX precompiles; a V2 router is added beside it when one is deployed,
|
||||
// which is a fact about the chain rather than about this binary — an
|
||||
// unset address means the venue is not offered, not that it is broken.
|
||||
venues := []gateway.Venue{
|
||||
gateway.NewNativeDEXVenue(gateway.NativeDEXConfig{RPCURL: rpc, UseDEX: true}),
|
||||
}
|
||||
if router := strings.TrimSpace(os.Getenv("LUX_V2_ROUTER")); router != "" {
|
||||
venues = append(venues, gateway.NewUniswapV2Venue(gateway.UniswapV2Config{
|
||||
RPCURL: rpc,
|
||||
RouterAddress: router,
|
||||
Name: "lux_v2",
|
||||
}))
|
||||
}
|
||||
|
||||
gw := gateway.New(gateway.GatewayConfig{
|
||||
DefaultChainID: gateway.ChainID(chain),
|
||||
EnableFallback: true,
|
||||
CacheEnabled: true,
|
||||
CacheTTLSeconds: func() int {
|
||||
n, err := strconv.Atoi(env("GATEWAY_CACHE_TTL", "10"))
|
||||
if err != nil {
|
||||
return 10
|
||||
}
|
||||
return n
|
||||
}(),
|
||||
})
|
||||
|
||||
// Every other chain, quoted upstream. The provider already names the six
|
||||
// Uniswap serves — Ethereum, Arbitrum, Optimism, Polygon, Base, BNB — and
|
||||
// an API key is optional: without one the public endpoints answer, with one
|
||||
// they answer under a quota that is ours.
|
||||
up := uniswap.DefaultConfig()
|
||||
up.APIKey = os.Getenv("UNISWAP_API_KEY")
|
||||
up.Timeout = 30 * time.Second
|
||||
if err := gw.RegisterProvider(uniswap.NewProvider(up)); err != nil {
|
||||
log.Fatalf("uniswap provider: %v", err)
|
||||
}
|
||||
|
||||
cfg := gateway.DefaultServerConfig()
|
||||
cfg.Addr = addr
|
||||
server := gateway.NewServer(gw.GetRouter(), cfg, gateway.WithVenues(gateway.NewVenueRouter(venues...)))
|
||||
|
||||
stop := make(chan os.Signal, 1)
|
||||
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
|
||||
go func() {
|
||||
<-stop
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
_ = server.Shutdown(ctx)
|
||||
}()
|
||||
|
||||
log.Printf("trading gateway on %s — chain %d at %s, %d native venue(s), upstream for %v",
|
||||
addr, chain, rpc, len(venues), up.Chains)
|
||||
if err := server.Start(); err != nil {
|
||||
log.Fatalf("gateway: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -114,6 +114,43 @@ func (r *Registry) GetQuoteProviders() []QuoteProvider {
|
||||
return providers
|
||||
}
|
||||
|
||||
// GetQuoteProvidersFor returns the quote providers that serve one chain.
|
||||
//
|
||||
// Asking every provider about every chain is how a request for a Lux market
|
||||
// reached Uniswap and came back ACCESS_DENIED: a provider that does not list
|
||||
// the chain cannot answer for it, and its refusal is not a fact about the
|
||||
// market. A provider naming no chains at all is taken to serve any — that is
|
||||
// the shape a single-chain deployment has.
|
||||
func (r *Registry) GetQuoteProvidersFor(chain ChainID) []QuoteProvider {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
providers := make([]QuoteProvider, 0)
|
||||
for _, name := range r.order {
|
||||
provider, exists := r.providers[name]
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
qp, ok := provider.(QuoteProvider)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
chains := provider.Info().SupportedChains
|
||||
if len(chains) == 0 {
|
||||
providers = append(providers, qp)
|
||||
continue
|
||||
}
|
||||
for _, c := range chains {
|
||||
if c == chain {
|
||||
providers = append(providers, qp)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return providers
|
||||
}
|
||||
|
||||
// GetLiquidityProviders returns all liquidity providers
|
||||
func (r *Registry) GetLiquidityProviders() []LiquidityProvider {
|
||||
r.mu.RLock()
|
||||
|
||||
@@ -22,7 +22,7 @@ func NewRouter(registry *Registry, enableFallback bool) *Router {
|
||||
|
||||
// GetBestQuote gets the best quote from all providers
|
||||
func (r *Router) GetBestQuote(ctx context.Context, req QuoteRequest) (*SwapQuote, error) {
|
||||
providers := r.registry.GetQuoteProviders()
|
||||
providers := r.registry.GetQuoteProvidersFor(req.ChainID)
|
||||
if len(providers) == 0 {
|
||||
return nil, ErrNoProvidersAvailable
|
||||
}
|
||||
@@ -83,7 +83,7 @@ func (r *Router) GetBestQuote(ctx context.Context, req QuoteRequest) (*SwapQuote
|
||||
|
||||
// GetAllQuotes gets quotes from all providers
|
||||
func (r *Router) GetAllQuotes(ctx context.Context, req QuoteRequest) ([]SwapQuote, error) {
|
||||
providers := r.registry.GetQuoteProviders()
|
||||
providers := r.registry.GetQuoteProvidersFor(req.ChainID)
|
||||
if len(providers) == 0 {
|
||||
return nil, ErrNoProvidersAvailable
|
||||
}
|
||||
|
||||
+73
-1
@@ -3,6 +3,7 @@ package gateway
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net/http"
|
||||
@@ -253,8 +254,33 @@ func (s *Server) handleQuote(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
ctx := s.requestContext(r)
|
||||
quote, err := s.router.GetBestQuote(ctx, s.convertQuoteRequest(req))
|
||||
asked := s.convertQuoteRequest(req)
|
||||
|
||||
// Our own markets are quoted from our own contracts, and everything else is
|
||||
// quoted upstream. The venues were attached to this server and never read,
|
||||
// so every chain went to a provider — which is how a Lux market came back
|
||||
// with an upstream access error, a sentence about somebody else's API
|
||||
// dressed as a fact about our liquidity.
|
||||
//
|
||||
// The venues answer first because they are the venue: an upstream provider
|
||||
// that also lists the chain is a second opinion about a market we settle
|
||||
// ourselves. A venue with no liquidity returns nothing rather than an
|
||||
// error, so falling through to the providers is the honest next question.
|
||||
if quote := s.venueQuote(ctx, asked); quote != nil {
|
||||
s.writeJSON(w, http.StatusOK, quote)
|
||||
return
|
||||
}
|
||||
|
||||
quote, err := s.router.GetBestQuote(ctx, asked)
|
||||
if err != nil {
|
||||
// A chain we settle ourselves, with no provider behind it, is not a
|
||||
// deployment missing a provider — it is a pair no venue here holds.
|
||||
// Saying "no providers available" of our own chain sends a reader
|
||||
// looking for a misconfiguration that is not there.
|
||||
if errors.Is(err, ErrNoProvidersAvailable) && s.venues != nil && len(s.venues.Venues()) > 0 {
|
||||
s.writeError(w, http.StatusNotFound, fmt.Errorf("no venue here holds this pair"))
|
||||
return
|
||||
}
|
||||
s.writeError(w, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
@@ -262,6 +288,52 @@ func (s *Server) handleQuote(w http.ResponseWriter, r *http.Request) {
|
||||
s.writeJSON(w, http.StatusOK, quote)
|
||||
}
|
||||
|
||||
// venueQuote asks the venues this deployment settles on, and returns nil when
|
||||
// none of them holds the pair — which is a question for the providers and not
|
||||
// an answer of its own.
|
||||
func (s *Server) venueQuote(ctx context.Context, req QuoteRequest) *SwapQuote {
|
||||
if s.venues == nil || len(s.venues.Venues()) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
quotes, routing := s.venues.QueryAllVenues(ctx, VenueQuoteRequest{
|
||||
TokenIn: req.TokenIn.Address,
|
||||
TokenOut: req.TokenOut.Address,
|
||||
Amount: req.Amount.String(),
|
||||
Type: map[bool]string{true: "EXACT_INPUT", false: "EXACT_OUTPUT"}[req.IsExactIn],
|
||||
})
|
||||
|
||||
best := -1
|
||||
for i, q := range quotes {
|
||||
if !q.Executable || q.AmountOut == "" || q.AmountOut == "0" {
|
||||
continue
|
||||
}
|
||||
if best < 0 {
|
||||
best = i
|
||||
continue
|
||||
}
|
||||
a, aok := new(big.Int).SetString(q.AmountOut, 10)
|
||||
b, bok := new(big.Int).SetString(quotes[best].AmountOut, 10)
|
||||
if aok && bok && a.Cmp(b) > 0 {
|
||||
best = i
|
||||
}
|
||||
}
|
||||
if best < 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
won := quotes[best]
|
||||
out, _ := new(big.Int).SetString(won.AmountOut, 10)
|
||||
gas, _ := new(big.Int).SetString(won.GasEstimate, 10)
|
||||
return &SwapQuote{
|
||||
TokenIn: TokenAmount{Token: req.TokenIn, Amount: req.Amount},
|
||||
TokenOut: TokenAmount{Token: req.TokenOut, Amount: out},
|
||||
Route: []PoolHop{{PoolType: routing, TokenIn: req.TokenIn, TokenOut: req.TokenOut}},
|
||||
GasEstimate: gas,
|
||||
ProviderName: won.Venue,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) handleQuotes(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
s.writeError(w, http.StatusMethodNotAllowed, fmt.Errorf("method not allowed"))
|
||||
|
||||
Reference in New Issue
Block a user