Files
zeekayandHanzo Dev 45bff3f5b6 fix(dex): close price sources synchronously; build pkg/dpdk and pkg/orderbook on linux
`go test ./...` failed three ways on the runner. Two were build failures that
cannot happen on a mac, and one was a race that only loses under load.

pkg/dpdk — `undefined: syscall.SO_NOSIGPIPE`. DarwinOptimizations guarded itself
with `if runtime.GOOS != "darwin" { return }`, but SO_NOSIGPIPE is a COMPILE-time
constant that exists only on Darwin; a runtime check cannot save a reference the
linux build still has to resolve. Split into _darwin.go / _nodarwin.go. Verified
by cross-compiling both ways -- it failed for GOOS=linux before, both pass now.

pkg/orderbook — the cgo preamble uses uint64_t while including only <stdlib.h>.
macOS pulls stdint.h in transitively and glibc does not, so this built here and
never on the runner. Added the include.

pkg/price — TestXChainSourceStalePrice: "Expected price to be stale".

Not a flaky test; Close() was not a barrier. It closed the done channel and
returned WITHOUT waiting for the polling goroutine. A loop selecting on done
against a ticker picks a ready case AT RANDOM, so a poll could still be running
after Close() returned, and poll writes last[symbol] = time.Now(). The test
closes the source, backdates that timestamp by 10s, and asserts staleness -- an
in-flight poll overwrote it. Wide window on a loaded runner, near-zero on a
laptop, which is why it reproduces 0/30 here.

All EIGHT sources had it, so the fix is one shared `lifecycle` (embedded, so
`s.done` still resolves) whose stop() joins before returning and is safe to call
twice. Oracle had the same defect written as

    // Give time for goroutines to exit
    time.Sleep(100 * time.Millisecond)

which could not work either -- calcAverages blocks on a 1s ticker, so it ran up
to a full second after Stop() returned. Now on the same lifecycle, and its loops
select on done instead of polling a flag, so they leave promptly rather than
after one more tick. Nothing restarts a stopped source (checked: 0 tests), and
the other sources already could not.

    go build ./...                        OK
    GOOS=linux go build ./...             OK   (failed before)
    go vet price/dpdk/orderbook           OK
    go test ./pkg/price/ -race            ok, 3/3 runs
    go test -run StalePrice -count=30     ok

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-02 03:19:43 -07:00

291 lines
6.9 KiB
Go

package price
import (
"context"
"math/big"
"sync"
"time"
)
// CChainSource provides prices from Lux C-Chain AMMs.
// On-chain price discovery for cross-market arbitrage.
type CChainSource struct {
rpcURL string
wsURL string
routers map[string]string
tokens map[string]TokenPair
prices map[string]*Data
last map[string]time.Time
reserves map[string]*Reserves
interval time.Duration
healthy bool
mu sync.RWMutex
lifecycle
polling bool
}
// TokenPair defines a trading pair.
type TokenPair struct {
Token0 string
Token1 string
Decimals [2]uint8
Pool string
}
// Reserves tracks AMM pool state.
type Reserves struct {
R0 *big.Int
R1 *big.Int
Block uint64
Time time.Time
}
// NewCChainSource creates a C-Chain price source.
func NewCChainSource(rpcURL, wsURL string) *CChainSource {
return &CChainSource{
rpcURL: rpcURL,
wsURL: wsURL,
routers: cchainRouters(),
tokens: cchainTokens(),
prices: make(map[string]*Data),
last: make(map[string]time.Time),
reserves: make(map[string]*Reserves),
interval: 100 * time.Millisecond,
healthy: true,
lifecycle: newLifecycle(),
}
}
func cchainRouters() map[string]string {
return map[string]string{
"v3_swap_router": "0xE8fb25086C8652c92f5AF90D730Bac7C63Fc9A58", // Lux V3 SwapRouter
"v3_factory": "0x80bBc7C4C7a59C899D1B37BC14539A22D5830a84", // Lux V3 Factory
"v2_router": "0xAe2cf1E403aAFE6C05A5b8Ef63EB19ba591d8511", // Lux V2 Router
}
}
func cchainTokens() map[string]TokenPair {
// Lux C-Chain V3 pool pairs — real on-chain addresses verified 2026-03-05
return map[string]TokenPair{
"WLUX-LUSD": {
Token0: "0x4888e4a2ee0f03051c72d2bd3acf755ed3498b3e", // WLUX
Token1: "0x848Cff46eb323f323b6Bbe1Df274E40793d7f2c2", // LUSD
Decimals: [2]uint8{18, 18},
Pool: "0x37011bb281676f85962fb35c674f7e9eb7584452", // V3 3000 fee
},
"LBTC-WLUX": {
Token0: "0x1E48D32a4F5e9f08DB9aE4959163300FaF8A6C8e", // LBTC
Token1: "0x4888e4a2ee0f03051c72d2bd3acf755ed3498b3e", // WLUX
Decimals: [2]uint8{8, 18},
Pool: "0xa06e94ed014034e804c674f64c6f4c9313f2a6d7",
},
"WLUX-LETH": {
Token0: "0x4888e4a2ee0f03051c72d2bd3acf755ed3498b3e", // WLUX
Token1: "0x60E0a8167FC13dE89348978860466C9ceC24B9ba", // LETH
Decimals: [2]uint8{18, 18},
Pool: "0xfafbf1a0c5d6604f48378872c534545612a17aec",
},
"LSOL-WLUX": {
Token0: "0x26B40f650156C7EbF9e087Dd0dca181Fe87625B7", // LSOL
Token1: "0x4888e4a2ee0f03051c72d2bd3acf755ed3498b3e", // WLUX
Decimals: [2]uint8{18, 18},
Pool: "0xa9330ee37bba7139c7697d82253b3d5fbb430004",
},
"LBTC-LUSD": {
Token0: "0x1E48D32a4F5e9f08DB9aE4959163300FaF8A6C8e", // LBTC
Token1: "0x848Cff46eb323f323b6Bbe1Df274E40793d7f2c2", // LUSD
Decimals: [2]uint8{8, 18},
Pool: "0x328229d8872d04c8383edbced3e89c0ec5d63a9b",
},
"LETH-LUSD": {
Token0: "0x60E0a8167FC13dE89348978860466C9ceC24B9ba", // LETH
Token1: "0x848Cff46eb323f323b6Bbe1Df274E40793d7f2c2", // LUSD
Decimals: [2]uint8{18, 18},
Pool: "0x1f997d4b8a9be816e3009bdbc1c7062ab4271268",
},
"LBTC-LETH": {
Token0: "0x1E48D32a4F5e9f08DB9aE4959163300FaF8A6C8e", // LBTC
Token1: "0x60E0a8167FC13dE89348978860466C9ceC24B9ba", // LETH
Decimals: [2]uint8{8, 18},
Pool: "0x6521618978f083e83e912aab242ca8fc11493349",
},
"LSOL-LUSD": {
Token0: "0x26B40f650156C7EbF9e087Dd0dca181Fe87625B7", // LSOL
Token1: "0x848Cff46eb323f323b6Bbe1Df274E40793d7f2c2", // LUSD
Decimals: [2]uint8{18, 18},
},
}
}
// Start begins polling C-Chain pools.
func (s *CChainSource) Start() error {
s.mu.Lock()
if s.polling {
s.mu.Unlock()
return nil
}
s.polling = true
s.mu.Unlock()
s.run(s.loop)
return nil
}
func (s *CChainSource) loop() {
ticker := time.NewTicker(s.interval)
defer ticker.Stop()
for {
select {
case <-s.done:
return
case <-ticker.C:
s.poll()
}
}
}
func (s *CChainSource) poll() {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
var wg sync.WaitGroup
for symbol, pair := range s.tokens {
wg.Add(1)
go func(sym string, p TokenPair) {
defer wg.Done()
s.fetch(ctx, sym, p)
}(symbol, pair)
}
wg.Wait()
}
func (s *CChainSource) fetch(ctx context.Context, symbol string, pair TokenPair) {
// In production: call getReserves() on the pool
// For now, simulate
price := s.simulate(symbol)
s.mu.Lock()
defer s.mu.Unlock()
s.prices[symbol] = &Data{
Symbol: symbol,
Price: price,
Confidence: 0.95,
Timestamp: time.Now(),
Source: "c-chain",
}
s.last[symbol] = time.Now()
s.healthy = true
}
func (s *CChainSource) simulate(symbol string) float64 {
// Real market prices from Lux C-Chain AMM
base := map[string]float64{
// Native LUX
"LUX-USDC": 0.00232,
"LUX-USDT": 0.00232,
// Liquid tokens (from market data)
"LZOO-USDC": 0.000107,
"LZOO-LUX": 0.0461, // LZOO/LUX ratio
"LETH-USDC": 4078.55,
"LETH-LUX": 1757996.0, // LETH/LUX ratio
"LSOL-USDC": 325.69,
"LUSD-USDC": 1.0,
"LBTC-USDC": 124217.54,
"LBTC-LUX": 53542906.0, // LBTC/LUX ratio
"LTON-USDC": 10.67,
"LAVAX-USDC": 72.26,
"LAVAX-LUX": 31146.0, // LAVAX/LUX ratio
"LBNB-USDC": 1065.75,
"LBLAST-USDC": 0.0178,
"LPOL-USDC": 0.436,
}
b, ok := base[symbol]
if !ok {
return 0
}
// Slight AMM variation (slippage simulation)
now := time.Now().UnixNano()
v := float64(now%1000) / 100000.0
return b * (1.0 + v)
}
// Price returns the latest price for a symbol.
func (s *CChainSource) Price(symbol string) (*Data, error) {
s.mu.RLock()
defer s.mu.RUnlock()
p, ok := s.prices[symbol]
if !ok {
return nil, ErrNotFound
}
if time.Since(s.last[symbol]) > 5*time.Second {
copy := *p
copy.Stale = true
return &copy, nil
}
copy := *p
return &copy, nil
}
// Prices returns prices for multiple symbols.
func (s *CChainSource) Prices(symbols []string) (map[string]*Data, error) {
result := make(map[string]*Data)
for _, sym := range symbols {
if p, err := s.Price(sym); err == nil {
result[sym] = p
}
}
return result, nil
}
// Subscribe is a no-op (uses polling).
func (s *CChainSource) Subscribe(symbol string) error { return nil }
// Unsubscribe is a no-op.
func (s *CChainSource) Unsubscribe(symbol string) error { return nil }
// Healthy returns health status.
func (s *CChainSource) Healthy() bool {
s.mu.RLock()
defer s.mu.RUnlock()
return s.healthy
}
// Name returns "c-chain".
func (s *CChainSource) Name() string { return "c-chain" }
// Weight returns 1.8 (on-chain AMM - high trust).
func (s *CChainSource) Weight() float64 { return 1.8 }
// Close stops the source.
func (s *CChainSource) Close() error {
s.stop()
s.mu.Lock()
s.polling = false
s.mu.Unlock()
return nil
}
// Reserves returns pool reserves for a symbol.
func (s *CChainSource) GetReserves(symbol string) (*Reserves, error) {
s.mu.RLock()
defer s.mu.RUnlock()
r, ok := s.reserves[symbol]
if !ok {
return nil, ErrNotFound
}
return r, nil
}