mirror of
https://github.com/luxfi/dex.git
synced 2026-10-30 16:58:56 +00:00
dexd is now the ONLY command in the repo. One binary, two run modes:
dexd run as a luxd rpcchainvm PLUGIN. luxd execs the plugin from
dexd plugin its PluginDir with NO positional args (engine address arrives
via the runtime.EngineAddressKey env var) and probes the
version with `dexd version`. No-subcommand == plugin is
REQUIRED to preserve that contract — byte-for-byte the old
cmd/dchain behavior (rpc.Serve over the plugin transport).
dexd run run the STANDALONE D-Chain venue: open the on-disk zapdb,
dexd standalone serve the frozen dex_* surface over ZAP, and drive the
consensus sealer loop (WaitForEvent->BuildBlock->Verify->
Accept). -addr/-db flags preserved from the old cmd/dvenue.
Relocated logic (engine code NOT duplicated — pkg/dchain + pkg/lx untouched):
cmd/dchain/{main,runner} -> internal/plugin (plugin.Run)
cmd/dvenue/{main,engine_*} -> internal/standalone (standalone.Run + engineName)
cmd/dexd/main.go = thin run-mode dispatcher
Removed cmd dirs (12): dchain, dvenue, demo, dex-api-server, dex-server,
dexfillproof, dexseed, fix-gateway, gateway, monitor, persistent-server,
price-feed. Removed committed build artifacts: gateway, dex-server-linux.
Build/ship ONLY dexd:
- Dockerfile: canonical CPU image (CGO=0, ghcr.io/luxfi/dex) running
`dexd run`; no GPU/MLX required.
- Dockerfile.dvenue: GPU operator bundle repointed cmd/dvenue -> cmd/dexd.
- Deleted Dockerfile.{gateway,devnet,minimal,dvenue-cpu} and
docker/{Dockerfile,Dockerfile.cuda,backend/Dockerfile,compose-simple.yml}.
- Makefile rewritten to build/run/ship dexd only.
- docker.yml: dropped the dex-gateway image; dex-venue-image.yml: dvenue->dexd.
- Deleted legacy scripts that built removed/long-gone cmds; fixed deploy.sh +
dev-setup.sh; .gitignore ignores root build binaries.
Also included (coupled, environment-applied, settled): the in-progress
clob -> dex/orderbook terminology refactor across pkg/* + sdk/* + tests/*
(identifier + file renames, e.g. RegisterCLOB->RegisterDEX,
source_clob.go->source_orderbook.go). The relocated standalone venue calls the
renamed API, so it lands in the same consistent snapshot.
Verified (GOWORK=off CGO_ENABLED=0): `go build ./cmd/dexd`, `go build ./...`,
`go vet ./...` all exit 0; `go test ./pkg/dchain` (renamed RegisterDEX socket
path) green. GPU/cgo path not compiled locally (no CUDA); its rename mirrors
the verified nocgo twins and preserves the C ABI symbol names.
848 lines
28 KiB
Go
848 lines
28 KiB
Go
// Package lux provides a native EVM DEX provider that queries on-chain
|
|
// pool data via LX-series precompiles (0x9010-0x9060).
|
|
package lux
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"math"
|
|
"math/big"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/luxfi/dex/pkg/gateway"
|
|
ethereum "github.com/luxfi/geth"
|
|
"github.com/luxfi/geth/common"
|
|
"github.com/luxfi/geth/ethclient"
|
|
)
|
|
|
|
var (
|
|
ErrNotImplemented = errors.New("not yet implemented - use Uniswap provider as fallback")
|
|
ErrNoRPCEndpoint = errors.New("no RPC endpoint configured")
|
|
)
|
|
|
|
// DEX precompile addresses (LX-series, activated at genesis via dexConfig)
|
|
const (
|
|
PoolManagerAddress = "0x0000000000000000000000000000000000009010" // LXPool — singleton AMM pool manager
|
|
OracleAddress = "0x0000000000000000000000000000000000009011" // LXOracle — multi-source price aggregation
|
|
SwapRouterAddress = "0x0000000000000000000000000000000000009012" // LXRouter — swap router
|
|
HooksRegistryAddr = "0x0000000000000000000000000000000000009013" // LXHooks — pool hooks registry
|
|
FlashLoanAddress = "0x0000000000000000000000000000000000009014" // LXFlash — flash loans
|
|
OrderBookAddress = "0x0000000000000000000000000000000000009020" // LXBook — central limit order book
|
|
VaultAddress = "0x0000000000000000000000000000000000009030" // LXVault — clearinghouse, margin, positions
|
|
FeedAddress = "0x0000000000000000000000000000000000009040" // LXFeed — computed price feeds
|
|
LendAddress = "0x0000000000000000000000000000000000009050" // LXLend — lending pool
|
|
LiquidAddress = "0x0000000000000000000000000000000000009060" // LXLiquid — self-repaying loans
|
|
)
|
|
|
|
// Provider implements native DEX functionality via LX precompiles.
|
|
// Connects to EVM nodes to query on-chain pool data.
|
|
type Provider struct {
|
|
name string
|
|
priority int
|
|
chains []gateway.ChainID
|
|
|
|
// RPC clients for each chain
|
|
clients map[gateway.ChainID]*ethclient.Client
|
|
endpoints map[gateway.ChainID]string
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
// ProviderConfig holds provider configuration
|
|
type ProviderConfig struct {
|
|
Name string
|
|
Priority int
|
|
Chains []gateway.ChainID
|
|
RPCEndpoints map[gateway.ChainID]string // Map of chainID to RPC endpoint URL
|
|
}
|
|
|
|
// DefaultConfig returns a minimal config — caller must provide chains and RPC endpoints.
|
|
func DefaultConfig() ProviderConfig {
|
|
return ProviderConfig{
|
|
Name: "native",
|
|
Priority: 10,
|
|
Chains: nil,
|
|
RPCEndpoints: nil,
|
|
}
|
|
}
|
|
|
|
// NewProvider creates a native DEX provider with RPC connectivity.
|
|
// Chains and RPCEndpoints must be provided by the caller.
|
|
func NewProvider(cfg ProviderConfig) *Provider {
|
|
if cfg.Name == "" {
|
|
cfg.Name = "native"
|
|
}
|
|
if cfg.Priority == 0 {
|
|
cfg.Priority = 10
|
|
}
|
|
|
|
p := &Provider{
|
|
name: cfg.Name,
|
|
priority: cfg.Priority,
|
|
chains: cfg.Chains,
|
|
clients: make(map[gateway.ChainID]*ethclient.Client),
|
|
endpoints: cfg.RPCEndpoints,
|
|
}
|
|
|
|
// Initialize RPC clients
|
|
for chainID, endpoint := range cfg.RPCEndpoints {
|
|
if endpoint != "" {
|
|
client, err := ethclient.Dial(endpoint)
|
|
if err != nil {
|
|
log.Printf("Warning: Failed to connect to %d RPC at %s: %v", chainID, endpoint, err)
|
|
continue
|
|
}
|
|
p.clients[chainID] = client
|
|
log.Printf("Connected to %d RPC at %s", chainID, endpoint)
|
|
}
|
|
}
|
|
|
|
return p
|
|
}
|
|
|
|
// Info returns provider information
|
|
func (p *Provider) Info() gateway.ProviderInfo {
|
|
p.mu.RLock()
|
|
healthy := len(p.clients) > 0
|
|
p.mu.RUnlock()
|
|
|
|
return gateway.ProviderInfo{
|
|
Name: p.name,
|
|
Version: "1.0.0",
|
|
Description: "Native Lux DEX provider with v4 precompile support",
|
|
SupportedChains: p.chains,
|
|
Priority: p.priority,
|
|
Healthy: healthy,
|
|
}
|
|
}
|
|
|
|
// HealthCheck performs a health check on RPC connections
|
|
func (p *Provider) HealthCheck(ctx context.Context) gateway.HealthCheck {
|
|
start := time.Now()
|
|
p.mu.RLock()
|
|
defer p.mu.RUnlock()
|
|
|
|
healthy := false
|
|
var latency time.Duration
|
|
|
|
// Check at least one RPC connection is working
|
|
for chainID, client := range p.clients {
|
|
if client != nil {
|
|
_, err := client.BlockNumber(ctx)
|
|
if err == nil {
|
|
healthy = true
|
|
latency = time.Since(start)
|
|
log.Printf("Health check passed for chain %d (latency: %v)", chainID, latency)
|
|
break
|
|
} else {
|
|
log.Printf("Health check failed for chain %d: %v", chainID, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
return gateway.HealthCheck{
|
|
Provider: p.name,
|
|
Healthy: healthy,
|
|
Latency: latency.Milliseconds(),
|
|
LastCheck: time.Now(),
|
|
}
|
|
}
|
|
|
|
// Close cleans up resources and closes RPC connections
|
|
func (p *Provider) Close() error {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
|
|
for chainID, client := range p.clients {
|
|
if client != nil {
|
|
client.Close()
|
|
log.Printf("Closed RPC connection for chain %d", chainID)
|
|
}
|
|
}
|
|
p.clients = make(map[gateway.ChainID]*ethclient.Client)
|
|
return nil
|
|
}
|
|
|
|
// getClient returns the RPC client for a chain
|
|
func (p *Provider) getClient(chainID gateway.ChainID) (*ethclient.Client, error) {
|
|
p.mu.RLock()
|
|
client, ok := p.clients[chainID]
|
|
p.mu.RUnlock()
|
|
|
|
if !ok || client == nil {
|
|
return nil, fmt.Errorf("%w for chain %d", ErrNoRPCEndpoint, chainID)
|
|
}
|
|
return client, nil
|
|
}
|
|
|
|
// getPoolManagerAddress returns the pool manager precompile address
|
|
func (p *Provider) getPoolManagerAddress() common.Address {
|
|
return common.HexToAddress(PoolManagerAddress)
|
|
}
|
|
|
|
// SupportsChain returns true if this provider supports the given chain
|
|
func (p *Provider) SupportsChain(chainID gateway.ChainID) bool {
|
|
for _, c := range p.chains {
|
|
if c == chainID {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// GetQuote returns a swap quote by querying on-chain DEX precompile
|
|
func (p *Provider) GetQuote(ctx context.Context, req gateway.QuoteRequest) (*gateway.SwapQuote, error) {
|
|
// Only handle Lux and Zoo chains
|
|
if !p.SupportsChain(req.ChainID) {
|
|
return nil, &gateway.ProviderError{Provider: p.name, Err: ErrNotImplemented}
|
|
}
|
|
|
|
if req.Amount == nil {
|
|
return nil, &gateway.ProviderError{Provider: p.name, Err: fmt.Errorf("amount required")}
|
|
}
|
|
|
|
// Get RPC client for chain
|
|
client, err := p.getClient(req.ChainID)
|
|
if err != nil {
|
|
// Fallback to estimate-based quote if no RPC
|
|
log.Printf("No RPC connection for chain %d, using estimate", req.ChainID)
|
|
return p.getEstimatedQuote(req)
|
|
}
|
|
|
|
// Get pool state from DEX precompile
|
|
poolState, err := p.queryPoolState(ctx, client, req.TokenIn.Address, req.TokenOut.Address, 3000)
|
|
if err != nil {
|
|
log.Printf("Failed to query pool state: %v, using estimate", err)
|
|
return p.getEstimatedQuote(req)
|
|
}
|
|
|
|
// Calculate swap output based on pool state
|
|
amountOut := p.calculateSwapOutput(poolState, req.Amount, req.IsExactIn)
|
|
|
|
tokenIn := req.TokenIn
|
|
if tokenIn.Symbol == "" {
|
|
tokenIn.Symbol = p.getNativeSymbol(req.ChainID)
|
|
tokenIn.Name = p.getNativeName(req.ChainID)
|
|
tokenIn.Decimals = 18
|
|
tokenIn.ChainID = req.ChainID
|
|
}
|
|
|
|
tokenOut := req.TokenOut
|
|
if tokenOut.Symbol == "" {
|
|
tokenOut.Symbol = "USDC"
|
|
tokenOut.Name = "USD Coin"
|
|
tokenOut.Decimals = 6
|
|
tokenOut.ChainID = req.ChainID
|
|
}
|
|
|
|
// Calculate price impact
|
|
priceImpact := p.calculatePriceImpact(poolState, req.Amount)
|
|
|
|
return &gateway.SwapQuote{
|
|
TokenIn: gateway.TokenAmount{
|
|
Token: tokenIn,
|
|
Amount: req.Amount,
|
|
},
|
|
TokenOut: gateway.TokenAmount{
|
|
Token: tokenOut,
|
|
Amount: amountOut,
|
|
},
|
|
Route: []gateway.PoolHop{
|
|
{
|
|
PoolAddress: PoolManagerAddress,
|
|
PoolType: "v4",
|
|
TokenIn: tokenIn,
|
|
TokenOut: tokenOut,
|
|
Fee: 3000,
|
|
},
|
|
},
|
|
PriceImpact: priceImpact,
|
|
GasEstimate: big.NewInt(150000),
|
|
QuoteID: fmt.Sprintf("lux-quote-%d", time.Now().UnixNano()),
|
|
ExpiresAt: time.Now().Add(30 * time.Second),
|
|
ProviderName: p.name,
|
|
}, nil
|
|
}
|
|
|
|
// poolState holds on-chain pool data
|
|
type poolState struct {
|
|
SqrtPriceX96 *big.Int
|
|
Liquidity *big.Int
|
|
Tick int32
|
|
FeeGrowth0 *big.Int
|
|
FeeGrowth1 *big.Int
|
|
}
|
|
|
|
// queryPoolState queries a V3 pool contract on-chain for live state via slot0() and liquidity()
|
|
func (p *Provider) queryPoolState(ctx context.Context, client *ethclient.Client, token0, token1 string, fee uint32) (*poolState, error) {
|
|
// First discover the pool address from V3 factory
|
|
poolAddr, err := p.getV3Pool(ctx, client, token0, token1, fee)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("pool not found: %w", err)
|
|
}
|
|
return p.queryV3PoolState(ctx, client, poolAddr)
|
|
}
|
|
|
|
// getV3Pool calls factory.getPool(tokenA, tokenB, fee) to find the pool address
|
|
func (p *Provider) getV3Pool(ctx context.Context, client *ethclient.Client, tokenA, tokenB string, fee uint32) (common.Address, error) {
|
|
factory := common.HexToAddress(V3FactoryAddress)
|
|
// getPool(address,address,uint24) selector: 0x1698ee82
|
|
data := common.FromHex("0x1698ee82")
|
|
data = append(data, common.LeftPadBytes(common.HexToAddress(tokenA).Bytes(), 32)...)
|
|
data = append(data, common.LeftPadBytes(common.HexToAddress(tokenB).Bytes(), 32)...)
|
|
feePad := make([]byte, 32)
|
|
new(big.Int).SetUint64(uint64(fee)).FillBytes(feePad)
|
|
data = append(data, feePad...)
|
|
|
|
msg := ethereum.CallMsg{To: &factory, Data: data}
|
|
result, err := client.CallContract(ctx, msg, nil)
|
|
if err != nil {
|
|
return common.Address{}, fmt.Errorf("getPool call failed: %w", err)
|
|
}
|
|
if len(result) < 32 {
|
|
return common.Address{}, fmt.Errorf("invalid getPool response")
|
|
}
|
|
addr := common.BytesToAddress(result[12:32])
|
|
if addr == (common.Address{}) {
|
|
return common.Address{}, fmt.Errorf("pool does not exist for %s/%s fee %d", tokenA, tokenB, fee)
|
|
}
|
|
return addr, nil
|
|
}
|
|
|
|
// queryV3PoolState calls slot0() and liquidity() on a V3 pool contract
|
|
func (p *Provider) queryV3PoolState(ctx context.Context, client *ethclient.Client, poolAddr common.Address) (*poolState, error) {
|
|
// slot0() = 0x3850c7bd
|
|
slot0Data := common.FromHex("0x3850c7bd")
|
|
msg := ethereum.CallMsg{To: &poolAddr, Data: slot0Data}
|
|
result, err := client.CallContract(ctx, msg, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("slot0 call failed: %w", err)
|
|
}
|
|
if len(result) < 64 {
|
|
return nil, fmt.Errorf("invalid slot0 response (len=%d)", len(result))
|
|
}
|
|
|
|
sqrtPriceX96 := new(big.Int).SetBytes(result[0:32])
|
|
tickBig := new(big.Int).SetBytes(result[32:64])
|
|
// Handle signed int24
|
|
if tickBig.Cmp(new(big.Int).Lsh(big.NewInt(1), 255)) >= 0 {
|
|
tickBig.Sub(tickBig, new(big.Int).Lsh(big.NewInt(1), 256))
|
|
}
|
|
|
|
// liquidity() = 0x1a686502
|
|
liqData := common.FromHex("0x1a686502")
|
|
msg2 := ethereum.CallMsg{To: &poolAddr, Data: liqData}
|
|
result2, err := client.CallContract(ctx, msg2, nil)
|
|
liquidity := big.NewInt(0)
|
|
if err == nil && len(result2) >= 32 {
|
|
liquidity = new(big.Int).SetBytes(result2[0:32])
|
|
}
|
|
|
|
return &poolState{
|
|
SqrtPriceX96: sqrtPriceX96,
|
|
Liquidity: liquidity,
|
|
Tick: int32(tickBig.Int64()),
|
|
FeeGrowth0: big.NewInt(0),
|
|
FeeGrowth1: big.NewInt(0),
|
|
}, nil
|
|
}
|
|
|
|
// priceFromSqrtX96 computes token1/token0 price from sqrtPriceX96, adjusted for decimals
|
|
func priceFromSqrtX96(sqrtPriceX96 *big.Int, decimals0, decimals1 int) float64 {
|
|
if sqrtPriceX96.Sign() == 0 {
|
|
return 0
|
|
}
|
|
sq := new(big.Float).SetInt(sqrtPriceX96)
|
|
q96 := new(big.Float).SetInt(new(big.Int).Lsh(big.NewInt(1), 96))
|
|
ratio := new(big.Float).Quo(sq, q96)
|
|
price := new(big.Float).Mul(ratio, ratio)
|
|
|
|
// Adjust for decimal difference
|
|
decAdj := decimals0 - decimals1
|
|
if decAdj != 0 {
|
|
adj := new(big.Float).SetFloat64(math.Pow(10, float64(decAdj)))
|
|
price.Mul(price, adj)
|
|
}
|
|
|
|
f, _ := price.Float64()
|
|
return f
|
|
}
|
|
|
|
// calculateSwapOutput calculates expected output for a swap
|
|
func (p *Provider) calculateSwapOutput(pool *poolState, amountIn *big.Int, exactIn bool) *big.Int {
|
|
if pool.Liquidity.Sign() == 0 {
|
|
return big.NewInt(0)
|
|
}
|
|
|
|
// Simplified AMM formula: amountOut = amountIn * liquidity / (liquidity + amountIn)
|
|
numerator := new(big.Int).Mul(amountIn, pool.Liquidity)
|
|
denominator := new(big.Int).Add(pool.Liquidity, amountIn)
|
|
return new(big.Int).Div(numerator, denominator)
|
|
}
|
|
|
|
// calculatePriceImpact calculates the price impact of a swap
|
|
func (p *Provider) calculatePriceImpact(pool *poolState, amountIn *big.Int) float64 {
|
|
if pool.Liquidity.Sign() == 0 {
|
|
return 100.0
|
|
}
|
|
|
|
// Price impact = amountIn / (liquidity * 2) * 100
|
|
impact := new(big.Float).SetInt(amountIn)
|
|
liq := new(big.Float).SetInt(pool.Liquidity)
|
|
liq.Mul(liq, big.NewFloat(2))
|
|
impact.Quo(impact, liq)
|
|
impact.Mul(impact, big.NewFloat(100))
|
|
|
|
result, _ := impact.Float64()
|
|
return result
|
|
}
|
|
|
|
// getEstimatedQuote returns a quote based on estimates when no RPC is available
|
|
func (p *Provider) getEstimatedQuote(req gateway.QuoteRequest) (*gateway.SwapQuote, error) {
|
|
// Simple estimate: 1:1000 ratio for native -> USDC-like
|
|
amountOut := new(big.Int).Mul(req.Amount, big.NewInt(1000))
|
|
amountOut = amountOut.Div(amountOut, big.NewInt(1e12))
|
|
|
|
tokenIn := req.TokenIn
|
|
if tokenIn.Symbol == "" {
|
|
tokenIn.Symbol = p.getNativeSymbol(req.ChainID)
|
|
tokenIn.Name = p.getNativeName(req.ChainID)
|
|
tokenIn.Decimals = 18
|
|
tokenIn.ChainID = req.ChainID
|
|
}
|
|
|
|
tokenOut := req.TokenOut
|
|
if tokenOut.Symbol == "" {
|
|
tokenOut.Symbol = "USDC"
|
|
tokenOut.Name = "USD Coin"
|
|
tokenOut.Decimals = 6
|
|
tokenOut.ChainID = req.ChainID
|
|
}
|
|
|
|
return &gateway.SwapQuote{
|
|
TokenIn: gateway.TokenAmount{
|
|
Token: tokenIn,
|
|
Amount: req.Amount,
|
|
},
|
|
TokenOut: gateway.TokenAmount{
|
|
Token: tokenOut,
|
|
Amount: amountOut,
|
|
},
|
|
Route: []gateway.PoolHop{
|
|
{
|
|
PoolAddress: PoolManagerAddress,
|
|
PoolType: "v4",
|
|
TokenIn: tokenIn,
|
|
TokenOut: tokenOut,
|
|
Fee: 3000,
|
|
},
|
|
},
|
|
PriceImpact: 0.1,
|
|
GasEstimate: big.NewInt(150000),
|
|
QuoteID: fmt.Sprintf("lux-estimate-%d", time.Now().UnixNano()),
|
|
ExpiresAt: time.Now().Add(30 * time.Second),
|
|
ProviderName: p.name,
|
|
}, nil
|
|
}
|
|
|
|
func (p *Provider) getNativeSymbol(chainID gateway.ChainID) string {
|
|
if chainID == gateway.ChainIDZoo {
|
|
return "ZOO"
|
|
}
|
|
return "LUX"
|
|
}
|
|
|
|
func (p *Provider) getNativeName(chainID gateway.ChainID) string {
|
|
if chainID == gateway.ChainIDZoo {
|
|
return "Zoo"
|
|
}
|
|
return "Lux"
|
|
}
|
|
|
|
// GetQuotes returns multiple quotes
|
|
func (p *Provider) GetQuotes(ctx context.Context, req gateway.QuoteRequest) ([]gateway.SwapQuote, error) {
|
|
// TODO: Implement native Lux multi-route quotes
|
|
return nil, &gateway.ProviderError{Provider: p.name, Err: ErrNotImplemented}
|
|
}
|
|
|
|
// BuildSwap builds a swap transaction
|
|
func (p *Provider) BuildSwap(ctx context.Context, quote gateway.SwapQuote, recipient string, deadline int64) (*gateway.SwapTransaction, error) {
|
|
// TODO: Implement native Lux swap transaction building
|
|
return nil, &gateway.ProviderError{Provider: p.name, Err: ErrNotImplemented}
|
|
}
|
|
|
|
// ExecuteSwap executes a swap
|
|
func (p *Provider) ExecuteSwap(ctx context.Context, req gateway.SwapRequest) (string, error) {
|
|
// TODO: Implement native Lux swap execution
|
|
return "", &gateway.ProviderError{Provider: p.name, Err: ErrNotImplemented}
|
|
}
|
|
|
|
// v3PoolDef defines a V3 token pair to check for pools
|
|
type v3PoolDef struct {
|
|
tokenA string
|
|
symbolA string
|
|
nameA string
|
|
decimalsA int
|
|
tokenB string
|
|
symbolB string
|
|
nameB string
|
|
decimalsB int
|
|
}
|
|
|
|
// knownV3Pairs lists token pairs to discover pools for via V3 factory
|
|
var knownV3Pairs = []v3PoolDef{
|
|
{WLUXAddress, "WLUX", "Wrapped LUX", 18, LUSDAddress, "LUSD", "Lux USD", 18},
|
|
{WLUXAddress, "WLUX", "Wrapped LUX", 18, LETHAddress, "LETH", "Lux Ether", 18},
|
|
{WLUXAddress, "WLUX", "Wrapped LUX", 18, LBTCAddress, "LBTC", "Lux Bitcoin", 8},
|
|
{WLUXAddress, "WLUX", "Wrapped LUX", 18, LSOLAddress, "LSOL", "Lux SOL", 18},
|
|
{LBTCAddress, "LBTC", "Lux Bitcoin", 8, LUSDAddress, "LUSD", "Lux USD", 18},
|
|
{LETHAddress, "LETH", "Lux Ether", 18, LUSDAddress, "LUSD", "Lux USD", 18},
|
|
{LBTCAddress, "LBTC", "Lux Bitcoin", 8, LETHAddress, "LETH", "Lux Ether", 18},
|
|
{LSOLAddress, "LSOL", "Lux SOL", 18, LUSDAddress, "LUSD", "Lux USD", 18},
|
|
}
|
|
|
|
// V3 fee tiers to check
|
|
var v3FeeTiers = []uint32{500, 3000, 10000}
|
|
|
|
// GetPools returns pools by querying on-chain V3 factory and pool contracts
|
|
func (p *Provider) GetPools(ctx context.Context, req gateway.PoolsRequest) ([]gateway.Pool, error) {
|
|
if !p.SupportsChain(req.ChainID) {
|
|
return nil, &gateway.ProviderError{Provider: p.name, Err: ErrNotImplemented}
|
|
}
|
|
|
|
client, err := p.getClient(req.ChainID)
|
|
if err != nil {
|
|
return nil, &gateway.ProviderError{Provider: p.name, Err: fmt.Errorf("no RPC: %w", err)}
|
|
}
|
|
|
|
blockNum, _ := client.BlockNumber(ctx)
|
|
log.Printf("Querying pools at block %d for chain %d", blockNum, req.ChainID)
|
|
|
|
var pools []gateway.Pool
|
|
|
|
if req.ChainID == gateway.ChainIDLux {
|
|
for _, pair := range knownV3Pairs {
|
|
for _, fee := range v3FeeTiers {
|
|
poolAddr, err := p.getV3Pool(ctx, client, pair.tokenA, pair.tokenB, fee)
|
|
if err != nil {
|
|
continue // Pool doesn't exist for this fee tier
|
|
}
|
|
|
|
state, err := p.queryV3PoolState(ctx, client, poolAddr)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
// Determine token order (V3 sorts by address)
|
|
var t0, t1 gateway.Token
|
|
addrA := common.HexToAddress(pair.tokenA)
|
|
addrB := common.HexToAddress(pair.tokenB)
|
|
if addrA.Hex() < addrB.Hex() {
|
|
t0 = gateway.Token{Address: pair.tokenA, ChainID: req.ChainID, Symbol: pair.symbolA, Name: pair.nameA, Decimals: pair.decimalsA}
|
|
t1 = gateway.Token{Address: pair.tokenB, ChainID: req.ChainID, Symbol: pair.symbolB, Name: pair.nameB, Decimals: pair.decimalsB}
|
|
} else {
|
|
t0 = gateway.Token{Address: pair.tokenB, ChainID: req.ChainID, Symbol: pair.symbolB, Name: pair.nameB, Decimals: pair.decimalsB}
|
|
t1 = gateway.Token{Address: pair.tokenA, ChainID: req.ChainID, Symbol: pair.symbolA, Name: pair.nameA, Decimals: pair.decimalsA}
|
|
}
|
|
|
|
pools = append(pools, gateway.Pool{
|
|
Address: poolAddr.Hex(),
|
|
ChainID: req.ChainID,
|
|
Protocol: "uniswap-v3",
|
|
Token0: t0,
|
|
Token1: t1,
|
|
Fee: int(fee),
|
|
TVL: state.Liquidity, // Live from chain
|
|
TickData: []gateway.TickRange{
|
|
{TickLower: int(state.Tick) - 1000, TickUpper: int(state.Tick) + 1000, Liquidity: state.Liquidity},
|
|
},
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
log.Printf("Found %d live pools for chain %d", len(pools), req.ChainID)
|
|
return pools, nil
|
|
}
|
|
|
|
// GetPool returns a specific pool
|
|
func (p *Provider) GetPool(ctx context.Context, chainID gateway.ChainID, address string) (*gateway.Pool, error) {
|
|
// TODO: Implement native Lux pool lookup
|
|
return nil, &gateway.ProviderError{Provider: p.name, Err: ErrNotImplemented}
|
|
}
|
|
|
|
// GetPositions returns positions for an owner
|
|
func (p *Provider) GetPositions(ctx context.Context, req gateway.PositionsRequest) ([]gateway.Position, error) {
|
|
// TODO: Implement native Lux position queries
|
|
return nil, &gateway.ProviderError{Provider: p.name, Err: ErrNotImplemented}
|
|
}
|
|
|
|
// GetPosition returns a specific position
|
|
func (p *Provider) GetPosition(ctx context.Context, chainID gateway.ChainID, positionID string) (*gateway.Position, error) {
|
|
// TODO: Implement native Lux position lookup
|
|
return nil, &gateway.ProviderError{Provider: p.name, Err: ErrNotImplemented}
|
|
}
|
|
|
|
// BuildAddLiquidity builds add liquidity transaction
|
|
func (p *Provider) BuildAddLiquidity(ctx context.Context, pool gateway.Pool, amount0, amount1 string, tickLower, tickUpper int) (*gateway.SwapTransaction, error) {
|
|
// TODO: Implement native Lux add liquidity
|
|
return nil, &gateway.ProviderError{Provider: p.name, Err: ErrNotImplemented}
|
|
}
|
|
|
|
// BuildRemoveLiquidity builds remove liquidity transaction
|
|
func (p *Provider) BuildRemoveLiquidity(ctx context.Context, position gateway.Position, percentage float64) (*gateway.SwapTransaction, error) {
|
|
// TODO: Implement native Lux remove liquidity
|
|
return nil, &gateway.ProviderError{Provider: p.name, Err: ErrNotImplemented}
|
|
}
|
|
|
|
// BuildCollectFees builds collect fees transaction
|
|
func (p *Provider) BuildCollectFees(ctx context.Context, position gateway.Position) (*gateway.SwapTransaction, error) {
|
|
// TODO: Implement native Lux collect fees
|
|
return nil, &gateway.ProviderError{Provider: p.name, Err: ErrNotImplemented}
|
|
}
|
|
|
|
// Real token addresses on Lux C-Chain (verified on-chain 2026-03-05)
|
|
const (
|
|
WLUXAddress = "0x4888e4a2ee0f03051c72d2bd3acf755ed3498b3e"
|
|
LBTCAddress = "0x1E48D32a4F5e9f08DB9aE4959163300FaF8A6C8e"
|
|
LETHAddress = "0x60E0a8167FC13dE89348978860466C9ceC24B9ba"
|
|
LSOLAddress = "0x26B40f650156C7EbF9e087Dd0dca181Fe87625B7"
|
|
LUSDAddress = "0x848Cff46eb323f323b6Bbe1Df274E40793d7f2c2"
|
|
LTONAddress = "0x3141b94b89691009b950c96e97Bff48e0C543E3C"
|
|
LAVAXAddress = "0x0e4bD0DD67c15dECfBBBdbbE07FC9d51D737693D"
|
|
|
|
// V3 Factory (CREATE2, confirmed on-chain)
|
|
V3FactoryAddress = "0x80bBc7C4C7a59C899D1B37BC14539A22D5830a84"
|
|
// V2 Factory (CREATE2)
|
|
V2FactoryAddress = "0xD173926A10A0C4eCd3A51B1422270b65Df0551c1"
|
|
|
|
// Price cache TTL
|
|
priceCacheTTL = 10 * time.Second
|
|
)
|
|
|
|
// tokenMeta defines known tokens with their decimals for price computation
|
|
var tokenMeta = map[string]struct {
|
|
address string
|
|
decimals int
|
|
}{
|
|
"WLUX": {address: WLUXAddress, decimals: 18},
|
|
"LUX": {address: WLUXAddress, decimals: 18},
|
|
"LBTC": {address: LBTCAddress, decimals: 8},
|
|
"LETH": {address: LETHAddress, decimals: 18},
|
|
"LSOL": {address: LSOLAddress, decimals: 18},
|
|
"LUSD": {address: LUSDAddress, decimals: 18},
|
|
"LTON": {address: LTONAddress, decimals: 9},
|
|
"LAVAX": {address: LAVAXAddress, decimals: 18},
|
|
}
|
|
|
|
// priceEntry is a cached price with timestamp
|
|
type priceEntry struct {
|
|
priceUSD float64
|
|
updatedAt time.Time
|
|
}
|
|
|
|
// priceCache stores live prices from on-chain queries
|
|
var (
|
|
priceCache = make(map[string]*priceEntry)
|
|
priceCacheMu sync.RWMutex
|
|
)
|
|
|
|
// GetTokenPrice returns live token price by querying on-chain V3 pools
|
|
func (p *Provider) GetTokenPrice(ctx context.Context, token gateway.Token) (*gateway.TokenPrice, error) {
|
|
if !p.SupportsChain(token.ChainID) {
|
|
return nil, &gateway.ProviderError{Provider: p.name, Err: ErrNotImplemented}
|
|
}
|
|
|
|
sym := token.Symbol
|
|
if sym == "" {
|
|
sym = p.getNativeSymbol(token.ChainID)
|
|
}
|
|
|
|
priceUSD := p.getLivePrice(ctx, sym)
|
|
|
|
return &gateway.TokenPrice{
|
|
Token: token,
|
|
PriceUSD: priceUSD,
|
|
UpdatedAt: time.Now(),
|
|
}, nil
|
|
}
|
|
|
|
// getLivePrice returns a live USD price for a token, querying on-chain if cache is stale
|
|
func (p *Provider) getLivePrice(ctx context.Context, symbol string) float64 {
|
|
// Check cache first
|
|
priceCacheMu.RLock()
|
|
if entry, ok := priceCache[symbol]; ok && time.Since(entry.updatedAt) < priceCacheTTL {
|
|
priceCacheMu.RUnlock()
|
|
return entry.priceUSD
|
|
}
|
|
priceCacheMu.RUnlock()
|
|
|
|
// LUSD is always $1 (stablecoin)
|
|
if symbol == "LUSD" {
|
|
p.cachePrice(symbol, 1.0)
|
|
return 1.0
|
|
}
|
|
|
|
client, err := p.getClient(gateway.ChainIDLux)
|
|
if err != nil {
|
|
log.Printf("No RPC for live price query: %v", err)
|
|
return 0
|
|
}
|
|
|
|
// Step 1: Get LUX price from WLUX/LUSD pool
|
|
luxPrice := p.queryLuxPrice(ctx, client)
|
|
if luxPrice == 0 {
|
|
return 0
|
|
}
|
|
p.cachePrice("LUX", luxPrice)
|
|
p.cachePrice("WLUX", luxPrice)
|
|
|
|
if symbol == "LUX" || symbol == "WLUX" {
|
|
return luxPrice
|
|
}
|
|
|
|
// Step 2: Get other token prices via their WLUX pools
|
|
meta, ok := tokenMeta[symbol]
|
|
if !ok {
|
|
return 0
|
|
}
|
|
|
|
// Try to find pool: token/WLUX
|
|
state, err := p.queryPoolState(ctx, client, meta.address, WLUXAddress, 3000)
|
|
if err != nil {
|
|
log.Printf("No pool found for %s/WLUX: %v", symbol, err)
|
|
return 0
|
|
}
|
|
|
|
// Determine token order (lower address = token0)
|
|
tokenAddr := common.HexToAddress(meta.address)
|
|
wluxAddr := common.HexToAddress(WLUXAddress)
|
|
|
|
var priceInWLUX float64
|
|
if tokenAddr.Hex() < wluxAddr.Hex() {
|
|
// token is token0, WLUX is token1
|
|
// price = token1/token0 = WLUX per token
|
|
priceInWLUX = priceFromSqrtX96(state.SqrtPriceX96, meta.decimals, 18)
|
|
} else {
|
|
// WLUX is token0, token is token1
|
|
// price = token1/token0 = token per WLUX
|
|
raw := priceFromSqrtX96(state.SqrtPriceX96, 18, meta.decimals)
|
|
if raw > 0 {
|
|
priceInWLUX = 1.0 / raw
|
|
}
|
|
}
|
|
|
|
priceUSD := priceInWLUX * luxPrice
|
|
p.cachePrice(symbol, priceUSD)
|
|
return priceUSD
|
|
}
|
|
|
|
// queryLuxPrice gets the LUX/USD price from the WLUX/LUSD V3 pool
|
|
func (p *Provider) queryLuxPrice(ctx context.Context, client *ethclient.Client) float64 {
|
|
state, err := p.queryPoolState(ctx, client, WLUXAddress, LUSDAddress, 3000)
|
|
if err != nil {
|
|
log.Printf("Failed to query WLUX/LUSD pool: %v", err)
|
|
return 0
|
|
}
|
|
|
|
// WLUX (0x4888...) < LUSD (0x848C...), so WLUX=token0, LUSD=token1
|
|
// price = LUSD per WLUX (both 18 decimals)
|
|
return priceFromSqrtX96(state.SqrtPriceX96, 18, 18)
|
|
}
|
|
|
|
func (p *Provider) cachePrice(symbol string, price float64) {
|
|
priceCacheMu.Lock()
|
|
priceCache[symbol] = &priceEntry{priceUSD: price, updatedAt: time.Now()}
|
|
priceCacheMu.Unlock()
|
|
}
|
|
|
|
// GetTokenPrices returns prices for multiple tokens
|
|
func (p *Provider) GetTokenPrices(ctx context.Context, tokens []gateway.Token) ([]gateway.TokenPrice, error) {
|
|
var prices []gateway.TokenPrice
|
|
for _, t := range tokens {
|
|
price, err := p.GetTokenPrice(ctx, t)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
prices = append(prices, *price)
|
|
}
|
|
if len(prices) == 0 {
|
|
return nil, &gateway.ProviderError{Provider: p.name, Err: fmt.Errorf("no prices available")}
|
|
}
|
|
return prices, nil
|
|
}
|
|
|
|
// CreateLead creates a conversion lead
|
|
func (p *Provider) CreateLead(ctx context.Context, lead gateway.ConversionLead) (*gateway.ConversionLead, error) {
|
|
// TODO: Implement native Lux conversion tracking
|
|
return nil, &gateway.ProviderError{Provider: p.name, Err: ErrNotImplemented}
|
|
}
|
|
|
|
// GetLead retrieves a lead
|
|
func (p *Provider) GetLead(ctx context.Context, leadID string) (*gateway.ConversionLead, error) {
|
|
// TODO: Implement lead lookup
|
|
return nil, &gateway.ProviderError{Provider: p.name, Err: ErrNotImplemented}
|
|
}
|
|
|
|
// TrackEvent tracks a conversion event
|
|
func (p *Provider) TrackEvent(ctx context.Context, event gateway.ConversionEvent) error {
|
|
// TODO: Implement native Lux event tracking
|
|
return &gateway.ProviderError{Provider: p.name, Err: ErrNotImplemented}
|
|
}
|
|
|
|
// GetLeadEvents gets events for a lead
|
|
func (p *Provider) GetLeadEvents(ctx context.Context, leadID string) ([]gateway.ConversionEvent, error) {
|
|
// TODO: Implement event queries
|
|
return nil, &gateway.ProviderError{Provider: p.name, Err: ErrNotImplemented}
|
|
}
|
|
|
|
// GetTokenList returns token list for a chain
|
|
func (p *Provider) GetTokenList(ctx context.Context, chainID gateway.ChainID) ([]gateway.Token, error) {
|
|
// Return real token list for Lux — all addresses verified on-chain
|
|
if chainID == gateway.ChainIDLux {
|
|
return []gateway.Token{
|
|
{Address: "0x0000000000000000000000000000000000000000", ChainID: gateway.ChainIDLux, Decimals: 18, Symbol: "LUX", Name: "Lux"},
|
|
{Address: WLUXAddress, ChainID: gateway.ChainIDLux, Decimals: 18, Symbol: "WLUX", Name: "Wrapped LUX"},
|
|
{Address: LUSDAddress, ChainID: gateway.ChainIDLux, Decimals: 18, Symbol: "LUSD", Name: "Lux USD"},
|
|
{Address: LETHAddress, ChainID: gateway.ChainIDLux, Decimals: 18, Symbol: "LETH", Name: "Lux Ether"},
|
|
{Address: LBTCAddress, ChainID: gateway.ChainIDLux, Decimals: 8, Symbol: "LBTC", Name: "Lux Bitcoin"},
|
|
{Address: LSOLAddress, ChainID: gateway.ChainIDLux, Decimals: 18, Symbol: "LSOL", Name: "Lux SOL"},
|
|
{Address: LTONAddress, ChainID: gateway.ChainIDLux, Decimals: 9, Symbol: "LTON", Name: "Lux TON"},
|
|
{Address: LAVAXAddress, ChainID: gateway.ChainIDLux, Decimals: 18, Symbol: "LAVAX", Name: "Lux AVAX"},
|
|
}, nil
|
|
}
|
|
|
|
if chainID == gateway.ChainIDZoo {
|
|
return []gateway.Token{
|
|
{Address: "0x0000000000000000000000000000000000000000", ChainID: gateway.ChainIDZoo, Decimals: 18, Symbol: "ZOO", Name: "Zoo"},
|
|
{Address: "0x5491216406daB99b7032b83765F36790E27F8A61", ChainID: gateway.ChainIDZoo, Decimals: 18, Symbol: "WZOO", Name: "Wrapped ZOO"},
|
|
{Address: "0xb2ee1CE7b84853b83AA08702aD0aD4D79711882D", ChainID: gateway.ChainIDZoo, Decimals: 18, Symbol: "LUSD", Name: "Lux USD"},
|
|
{Address: "0x4870621EA8be7a383eFCfdA225249d35888bD9f2", ChainID: gateway.ChainIDZoo, Decimals: 18, Symbol: "LETH", Name: "Lux Ether"},
|
|
{Address: "0x6fc44509a32E513bE1aa00d27bb298e63830C6A8", ChainID: gateway.ChainIDZoo, Decimals: 8, Symbol: "LBTC", Name: "Lux Bitcoin"},
|
|
}, nil
|
|
}
|
|
|
|
return nil, &gateway.ProviderError{Provider: p.name, Err: ErrNotImplemented}
|
|
}
|
|
|
|
// SearchTokens searches for tokens
|
|
func (p *Provider) SearchTokens(ctx context.Context, chainID gateway.ChainID, query string) ([]gateway.Token, error) {
|
|
// TODO: Implement token search
|
|
return nil, &gateway.ProviderError{Provider: p.name, Err: ErrNotImplemented}
|
|
}
|
|
|
|
// GetToken returns a specific token
|
|
func (p *Provider) GetToken(ctx context.Context, chainID gateway.ChainID, address string) (*gateway.Token, error) {
|
|
// TODO: Implement token lookup
|
|
return nil, &gateway.ProviderError{Provider: p.name, Err: ErrNotImplemented}
|
|
}
|
|
|
|
// parseBigInt parses a string to *big.Int
|
|
func parseBigInt(s string) *big.Int {
|
|
n := new(big.Int)
|
|
n.SetString(s, 10)
|
|
return n
|
|
}
|