mirror of
https://github.com/luxfi/dex.git
synced 2026-09-24 00:26:31 +00:00
/v1/trade/swap asked the hosted provider registry for a price. This deployment
registers none on purpose, so it answered 500 while /v1/trade/quote priced the
same pair from the pools a line away — and then encoded whatever came back for
the PoolManager precompile at 0x9010 regardless of where the price came from. A
router on Ethereum quoted and a precompile on our chain addressed, in one
response. 0x9010 answers 0x on 96369, measured with the venue's own quoter
selector, so the calldata named a contract that is not there.
Venue has a Swap method now. Each one builds against the contract it read: the
V3 arm against the SwapRouter02 beside its quoter, the V2 arm against the same
Router02 that answered getAmountsOut. A venue that prices a market it cannot
settle returns nothing, and the surface says so naming it.
The selector was wrong in the way that spends money. calldata_lxrouter.go sent
04e45aaf under a signature it called ExactInputSingle(address,address,uint256,
uint256,uint160,bytes32); that hashes to f48f227e. 04e45aaf is SwapRouter02's
exactInputSingle((address,address,uint24,address,uint256,uint256,uint160)) — a
seven-field tuple. A router handed the six-field form reads amountIn where the
fee tier belongs and amountOutMinimum where the recipient belongs. Those
builders were reachable only from their own tests; the padding and decoding
survive in abi.go.
Proved end to end: the gateway's own calldata replayed through eth_call against
96369's real SwapRouter02, from the account holding the allowance, returns
997001483075056712 — the number the quote gave. The tier travels on
route[0].fee because a V3 quote asks four tiers and keeps one, and a swap that
does not name the same pool executes at a different price. amountOutMinimum is
99.5% of the quote by default, because a router handed the quoted amount as its
floor reverts on the movement the caller's own trade causes.
Permit2 is at one address everywhere it exists and does not exist everywhere:
9152 bytes on Ethereum, nothing on 96369. Both paths check first, so our chain
gets a sentence instead of calldata addressed to an empty account.
Nine more paths are gone because they could not work here. Five answered 500
"no providers available" — tokens, pools, pool/, positions, stats, price,
prices — because enumerating a chain is an index read and this reads one pool
by address, right now. api-explore.lux.cloud already answers all of it: 16
pools, $39.9M of TVL, real symbols and decimals for 96369. /v1/route is the
same read in a hat and returned {"routes":[]} forever, which a caller reads as
"no path exists" rather than "I cannot answer". The order paths were an
in-memory map behind two replicas, so an order placed on one pod was a 404 on
the other about half the time and every order died on restart — the D-Chain
venue is the order book and it is deployed beside this. The position builders
all addressed 0x9010.
Eight paths remain and all eight were exercised against the live chain.
Co-authored-by: Hanzo Dev <[email protected]>
262 lines
8.1 KiB
Go
262 lines
8.1 KiB
Go
// Package gateway provides an API gateway layer with pluggable providers.
|
|
// It supports delegation to external APIs (like Uniswap) and native Lux providers.
|
|
package gateway
|
|
|
|
import (
|
|
"context"
|
|
"math/big"
|
|
"time"
|
|
)
|
|
|
|
// ChainID represents a blockchain chain ID
|
|
type ChainID uint64
|
|
|
|
// Common chain IDs
|
|
const (
|
|
ChainIDEthereum ChainID = 1
|
|
ChainIDArbitrum ChainID = 42161
|
|
ChainIDOptimism ChainID = 10
|
|
ChainIDPolygon ChainID = 137
|
|
ChainIDBase ChainID = 8453
|
|
ChainIDBNB ChainID = 56
|
|
ChainIDLux ChainID = 96369
|
|
ChainIDZoo ChainID = 200200
|
|
)
|
|
|
|
// Token represents a token on a specific chain
|
|
type Token struct {
|
|
Address string `json:"address"`
|
|
ChainID ChainID `json:"chainId"`
|
|
Decimals int `json:"decimals"`
|
|
Symbol string `json:"symbol"`
|
|
Name string `json:"name"`
|
|
LogoURI string `json:"logoURI,omitempty"`
|
|
}
|
|
|
|
// TokenAmount represents a token with an amount
|
|
type TokenAmount struct {
|
|
Token Token `json:"token"`
|
|
Amount *big.Int `json:"amount"`
|
|
}
|
|
|
|
// SwapQuote represents a quote for a swap
|
|
type SwapQuote struct {
|
|
TokenIn TokenAmount `json:"tokenIn"`
|
|
TokenOut TokenAmount `json:"tokenOut"`
|
|
Route []PoolHop `json:"route"`
|
|
PriceImpact float64 `json:"priceImpact"`
|
|
GasEstimate *big.Int `json:"gasEstimate"`
|
|
QuoteID string `json:"quoteId,omitempty"`
|
|
ExpiresAt time.Time `json:"expiresAt,omitempty"`
|
|
ProviderName string `json:"providerName"`
|
|
}
|
|
|
|
// PoolHop represents a single hop in a swap route
|
|
type PoolHop struct {
|
|
PoolAddress string `json:"poolAddress"`
|
|
PoolType string `json:"poolType"` // "v2", "v3", "v4"
|
|
TokenIn Token `json:"tokenIn"`
|
|
TokenOut Token `json:"tokenOut"`
|
|
Fee int `json:"fee,omitempty"` // For V3/V4 pools
|
|
}
|
|
|
|
// SwapRequest represents a request to execute a swap
|
|
type SwapRequest struct {
|
|
TokenIn Token `json:"tokenIn"`
|
|
TokenOut Token `json:"tokenOut"`
|
|
Amount *big.Int `json:"amount"`
|
|
IsExactIn bool `json:"isExactIn"`
|
|
Slippage float64 `json:"slippage"`
|
|
Recipient string `json:"recipient"`
|
|
Deadline int64 `json:"deadline,omitempty"`
|
|
PermitSignature string `json:"permitSignature,omitempty"`
|
|
}
|
|
|
|
// SwapTransaction represents the transaction data for a swap
|
|
type SwapTransaction struct {
|
|
To string `json:"to"`
|
|
Data string `json:"data"`
|
|
Value *big.Int `json:"value"`
|
|
GasLimit *big.Int `json:"gasLimit"`
|
|
ChainID ChainID `json:"chainId"`
|
|
}
|
|
|
|
// Pool represents a liquidity pool
|
|
type Pool struct {
|
|
Address string `json:"address"`
|
|
ChainID ChainID `json:"chainId"`
|
|
Protocol string `json:"protocol"` // "uniswap_v2", "uniswap_v3", "lux_amm"
|
|
Token0 Token `json:"token0"`
|
|
Token1 Token `json:"token1"`
|
|
Fee int `json:"fee,omitempty"`
|
|
TVL *big.Int `json:"tvl,omitempty"`
|
|
Volume24h *big.Int `json:"volume24h,omitempty"`
|
|
APR float64 `json:"apr,omitempty"`
|
|
TickData []TickRange `json:"tickData,omitempty"` // For V3/V4 pools
|
|
Reserves *Reserves `json:"reserves,omitempty"` // For V2 pools
|
|
}
|
|
|
|
// Reserves represents V2 pool reserves
|
|
type Reserves struct {
|
|
Reserve0 *big.Int `json:"reserve0"`
|
|
Reserve1 *big.Int `json:"reserve1"`
|
|
}
|
|
|
|
// TickRange represents a tick range with liquidity (V3/V4)
|
|
type TickRange struct {
|
|
TickLower int `json:"tickLower"`
|
|
TickUpper int `json:"tickUpper"`
|
|
Liquidity *big.Int `json:"liquidity"`
|
|
}
|
|
|
|
// Position represents a liquidity position
|
|
type Position struct {
|
|
ID string `json:"id"`
|
|
Owner string `json:"owner"`
|
|
Pool Pool `json:"pool"`
|
|
Liquidity *big.Int `json:"liquidity"`
|
|
Token0Owed *big.Int `json:"token0Owed"`
|
|
Token1Owed *big.Int `json:"token1Owed"`
|
|
TickLower int `json:"tickLower,omitempty"`
|
|
TickUpper int `json:"tickUpper,omitempty"`
|
|
FeesEarned *FeesEarned `json:"feesEarned,omitempty"`
|
|
}
|
|
|
|
// FeesEarned represents fees earned from a position
|
|
type FeesEarned struct {
|
|
Token0Fees *big.Int `json:"token0Fees"`
|
|
Token1Fees *big.Int `json:"token1Fees"`
|
|
}
|
|
|
|
// ConversionLead represents a conversion tracking lead
|
|
type ConversionLead struct {
|
|
ID string `json:"id"`
|
|
Source string `json:"source"`
|
|
Medium string `json:"medium,omitempty"`
|
|
Campaign string `json:"campaign,omitempty"`
|
|
WalletAddr string `json:"walletAddr,omitempty"`
|
|
Timestamp time.Time `json:"timestamp"`
|
|
Metadata map[string]string `json:"metadata,omitempty"`
|
|
}
|
|
|
|
// ConversionEvent represents a conversion event
|
|
type ConversionEvent struct {
|
|
LeadID string `json:"leadId"`
|
|
EventType string `json:"eventType"` // "swap", "lp_add", "wallet_connect"
|
|
ChainID ChainID `json:"chainId"`
|
|
TxHash string `json:"txHash,omitempty"`
|
|
Value *big.Int `json:"value,omitempty"`
|
|
Timestamp time.Time `json:"timestamp"`
|
|
Metadata map[string]string `json:"metadata,omitempty"`
|
|
}
|
|
|
|
// TokenPrice represents token price data
|
|
type TokenPrice struct {
|
|
Token Token `json:"token"`
|
|
PriceUSD float64 `json:"priceUSD"`
|
|
PriceChange float64 `json:"priceChange24h,omitempty"`
|
|
Volume24h *big.Int `json:"volume24h,omitempty"`
|
|
MarketCap *big.Int `json:"marketCap,omitempty"`
|
|
UpdatedAt time.Time `json:"updatedAt"`
|
|
}
|
|
|
|
// ProviderInfo contains information about a provider
|
|
type ProviderInfo struct {
|
|
Name string `json:"name"`
|
|
Version string `json:"version"`
|
|
Description string `json:"description"`
|
|
SupportedChains []ChainID `json:"supportedChains"`
|
|
Priority int `json:"priority"` // Lower = higher priority
|
|
Healthy bool `json:"healthy"`
|
|
}
|
|
|
|
// HealthCheck represents health check result
|
|
type HealthCheck struct {
|
|
Provider string `json:"provider"`
|
|
Healthy bool `json:"healthy"`
|
|
Latency int64 `json:"latencyMs"`
|
|
LastCheck time.Time `json:"lastCheck"`
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
|
|
// QuoteRequest for getting swap quotes
|
|
type QuoteRequest struct {
|
|
TokenIn Token `json:"tokenIn"`
|
|
TokenOut Token `json:"tokenOut"`
|
|
Amount *big.Int `json:"amount"`
|
|
IsExactIn bool `json:"isExactIn"`
|
|
ChainID ChainID `json:"chainId"`
|
|
Slippage float64 `json:"slippage,omitempty"`
|
|
}
|
|
|
|
// PoolsRequest for querying pools
|
|
type PoolsRequest struct {
|
|
ChainID ChainID `json:"chainId"`
|
|
Token0 string `json:"token0,omitempty"`
|
|
Token1 string `json:"token1,omitempty"`
|
|
Protocol string `json:"protocol,omitempty"`
|
|
MinTVL *big.Int `json:"minTvl,omitempty"`
|
|
Limit int `json:"limit,omitempty"`
|
|
Offset int `json:"offset,omitempty"`
|
|
}
|
|
|
|
// PositionsRequest for querying positions
|
|
type PositionsRequest struct {
|
|
ChainID ChainID `json:"chainId"`
|
|
Owner string `json:"owner"`
|
|
PoolID string `json:"poolId,omitempty"`
|
|
}
|
|
|
|
// PoolStats represents aggregated pool statistics
|
|
type PoolStats struct {
|
|
TotalTVL float64 `json:"totalTvl"`
|
|
TotalVolume24h float64 `json:"totalVolume24h"`
|
|
PoolCount int `json:"poolCount"`
|
|
TxCount24h int `json:"txCount24h"`
|
|
}
|
|
|
|
// UnsignedTxResponse is calldata for a wallet to sign. Nothing here holds a
|
|
// key, so every path that ends in a transaction ends in one of these.
|
|
type UnsignedTxResponse struct {
|
|
To string `json:"to"`
|
|
Data string `json:"data"`
|
|
Value string `json:"value"`
|
|
GasLimit uint64 `json:"gasLimit"`
|
|
ChainID uint64 `json:"chainId"`
|
|
}
|
|
|
|
// Error types
|
|
type ProviderError struct {
|
|
Provider string
|
|
Err error
|
|
}
|
|
|
|
func (e *ProviderError) Error() string {
|
|
return e.Provider + ": " + e.Err.Error()
|
|
}
|
|
|
|
func (e *ProviderError) Unwrap() error {
|
|
return e.Err
|
|
}
|
|
|
|
// Context key type
|
|
type contextKey string
|
|
|
|
const (
|
|
ContextKeyRequestID contextKey = "requestID"
|
|
ContextKeyChainID contextKey = "chainID"
|
|
)
|
|
|
|
// WithRequestID adds a request ID to the context
|
|
func WithRequestID(ctx context.Context, requestID string) context.Context {
|
|
return context.WithValue(ctx, ContextKeyRequestID, requestID)
|
|
}
|
|
|
|
// GetRequestID gets the request ID from context
|
|
func GetRequestID(ctx context.Context) string {
|
|
if v := ctx.Value(ContextKeyRequestID); v != nil {
|
|
return v.(string)
|
|
}
|
|
return ""
|
|
}
|