Files
zeekayandHanzo Dev 01f5bc490e 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]>
2026-09-08 22:57:42 -07:00

321 lines
7.1 KiB
Go

package gateway
import (
"context"
"sort"
"sync"
)
// Router aggregates multiple providers and routes requests appropriately
type Router struct {
registry *Registry
enableFallback bool
}
// NewRouter creates a new router
func NewRouter(registry *Registry, enableFallback bool) *Router {
return &Router{
registry: registry,
enableFallback: enableFallback,
}
}
// GetBestQuote gets the best quote from all providers
func (r *Router) GetBestQuote(ctx context.Context, req QuoteRequest) (*SwapQuote, error) {
providers := r.registry.GetQuoteProvidersFor(req.ChainID)
if len(providers) == 0 {
return nil, ErrNoProvidersAvailable
}
// Get quotes from all providers in parallel
type result struct {
quote *SwapQuote
err error
}
results := make(chan result, len(providers))
var wg sync.WaitGroup
for _, provider := range providers {
wg.Add(1)
go func(p QuoteProvider) {
defer wg.Done()
quote, err := p.GetQuote(ctx, req)
results <- result{quote: quote, err: err}
}(provider)
}
go func() {
wg.Wait()
close(results)
}()
// Collect quotes
var quotes []SwapQuote
var lastErr error
for res := range results {
if res.err != nil {
lastErr = res.err
continue
}
quotes = append(quotes, *res.quote)
}
if len(quotes) == 0 {
if lastErr != nil {
return nil, lastErr
}
return nil, ErrAllProvidersFailed
}
// Sort by output amount (descending) for EXACT_INPUT
// or input amount (ascending) for EXACT_OUTPUT
sort.Slice(quotes, func(i, j int) bool {
if req.IsExactIn {
return quotes[i].TokenOut.Amount.Cmp(quotes[j].TokenOut.Amount) > 0
}
return quotes[i].TokenIn.Amount.Cmp(quotes[j].TokenIn.Amount) < 0
})
return &quotes[0], nil
}
// GetAllQuotes gets quotes from all providers
func (r *Router) GetAllQuotes(ctx context.Context, req QuoteRequest) ([]SwapQuote, error) {
providers := r.registry.GetQuoteProvidersFor(req.ChainID)
if len(providers) == 0 {
return nil, ErrNoProvidersAvailable
}
type result struct {
quotes []SwapQuote
err error
}
results := make(chan result, len(providers))
var wg sync.WaitGroup
for _, provider := range providers {
wg.Add(1)
go func(p QuoteProvider) {
defer wg.Done()
quotes, err := p.GetQuotes(ctx, req)
results <- result{quotes: quotes, err: err}
}(provider)
}
go func() {
wg.Wait()
close(results)
}()
var allQuotes []SwapQuote
for res := range results {
if res.err != nil {
continue
}
allQuotes = append(allQuotes, res.quotes...)
}
if len(allQuotes) == 0 {
return nil, ErrAllProvidersFailed
}
// Sort by output amount
sort.Slice(allQuotes, func(i, j int) bool {
if req.IsExactIn {
return allQuotes[i].TokenOut.Amount.Cmp(allQuotes[j].TokenOut.Amount) > 0
}
return allQuotes[i].TokenIn.Amount.Cmp(allQuotes[j].TokenIn.Amount) < 0
})
return allQuotes, nil
}
// GetPools gets pools from providers with fallback
func (r *Router) GetPools(ctx context.Context, req PoolsRequest) ([]Pool, error) {
providers := r.registry.GetLiquidityProviders()
if len(providers) == 0 {
return nil, ErrNoProvidersAvailable
}
for _, provider := range providers {
pools, err := provider.GetPools(ctx, req)
if err == nil && len(pools) > 0 {
return pools, nil
}
if !r.enableFallback {
return nil, err
}
}
return nil, ErrAllProvidersFailed
}
// GetPool gets a specific pool with fallback
func (r *Router) GetPool(ctx context.Context, chainID ChainID, address string) (*Pool, error) {
providers := r.registry.GetLiquidityProviders()
if len(providers) == 0 {
return nil, ErrNoProvidersAvailable
}
var lastErr error
for _, provider := range providers {
pool, err := provider.GetPool(ctx, chainID, address)
if err == nil {
return pool, nil
}
lastErr = err
if !r.enableFallback {
return nil, err
}
}
return nil, lastErr
}
// GetPositions gets positions with fallback
func (r *Router) GetPositions(ctx context.Context, req PositionsRequest) ([]Position, error) {
providers := r.registry.GetLiquidityProviders()
if len(providers) == 0 {
return nil, ErrNoProvidersAvailable
}
var lastErr error
for _, provider := range providers {
positions, err := provider.GetPositions(ctx, req)
if err == nil {
return positions, nil
}
lastErr = err
if !r.enableFallback {
return nil, err
}
}
return nil, lastErr
}
// GetTokenPrice gets token price from the first available provider
func (r *Router) GetTokenPrice(ctx context.Context, token Token) (*TokenPrice, error) {
providers := r.registry.GetPriceProviders()
if len(providers) == 0 {
return nil, ErrNoProvidersAvailable
}
var lastErr error
for _, provider := range providers {
price, err := provider.GetTokenPrice(ctx, token)
if err == nil {
return price, nil
}
lastErr = err
if !r.enableFallback {
return nil, err
}
}
return nil, lastErr
}
// GetTokenPrices gets token prices from all providers, aggregating results
func (r *Router) GetTokenPrices(ctx context.Context, tokens []Token) ([]TokenPrice, error) {
providers := r.registry.GetPriceProviders()
if len(providers) == 0 {
return nil, ErrNoProvidersAvailable
}
// Try first provider
for _, provider := range providers {
prices, err := provider.GetTokenPrices(ctx, tokens)
if err == nil && len(prices) > 0 {
return prices, nil
}
if !r.enableFallback {
return nil, err
}
}
return nil, ErrAllProvidersFailed
}
// CreateLead creates a conversion lead
func (r *Router) CreateLead(ctx context.Context, lead ConversionLead) (*ConversionLead, error) {
providers := r.registry.GetConversionProviders()
if len(providers) == 0 {
return nil, ErrNoProvidersAvailable
}
var lastErr error
for _, provider := range providers {
created, err := provider.CreateLead(ctx, lead)
if err == nil {
return created, nil
}
lastErr = err
if !r.enableFallback {
return nil, err
}
}
return nil, lastErr
}
// TrackEvent tracks a conversion event to all providers
func (r *Router) TrackEvent(ctx context.Context, event ConversionEvent) error {
providers := r.registry.GetConversionProviders()
if len(providers) == 0 {
return ErrNoProvidersAvailable
}
// Track to all providers (don't fail if one fails)
var lastErr error
for _, provider := range providers {
if err := provider.TrackEvent(ctx, event); err != nil {
lastErr = err
}
}
return lastErr
}
// GetTokenList gets the token list for a chain from the first available provider
func (r *Router) GetTokenList(ctx context.Context, chainID ChainID) ([]Token, error) {
providers := r.registry.GetTokenListProviders()
if len(providers) == 0 {
return nil, ErrNoProvidersAvailable
}
var lastErr error
for _, provider := range providers {
tokens, err := provider.GetTokenList(ctx, chainID)
if err == nil && len(tokens) > 0 {
return tokens, nil
}
lastErr = err
if !r.enableFallback {
return nil, err
}
}
return nil, lastErr
}
// HealthCheck performs health checks on all providers
func (r *Router) HealthCheck(ctx context.Context) []HealthCheck {
return r.registry.HealthCheckAll(ctx)
}
// ListProviders lists all registered providers
func (r *Router) ListProviders() []ProviderInfo {
return r.registry.ListProviders()
}