mirror of
https://github.com/luxfi/dex.git
synced 2026-09-24 00:26:31 +00:00
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]>
321 lines
7.1 KiB
Go
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 "es[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()
|
|
}
|