Files
zeekayandHanzo Dev e0fde794da The venue that quotes is the venue that builds, and every path answers
/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]>
2026-09-09 01:01:50 -07:00

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 ""
}