Files
maker/provider_v3_test.go
zeekay e5f5aa12fc feat(maker): live on-chain AMM price feed — track V3 spot, not static mid
The maker quoted LUX/* at a static config mid (12.5) — fictional; the real WLUX/LUSD V3 spot is ~0.00061 LUSD. provider_v3.go adds V3ProviderSource: reads the MARGINAL spot from factory.getPool + pool.slot0 (sqrtPriceX96), NOT quoteExactInputSingle (a fixed-size quote slips a thin pool). Decomplected: PriceFeed (what to track) is orthogonal to the DEX venue (where to quote) — seeder composes an optional MidProvider (*Aggregator) via UseFeed; currentMid consults feed -> DEX book -> static. LUX lists only on native V2/V3/V4, so the AMM IS its reference. Opt-in via MAKER_V3_RPC/FACTORY/WLUX; absent = prior behavior. Selectors computed at init (getPool 1698ee82, slot0 3850c7bd), pinned in tests; spot orientation + decimals verified; live mainnet read confirms 0.00061. Verified: go test ./... green; raw eth_call to live QuoterV2/pool returns the price.
2026-06-26 19:40:45 -07:00

102 lines
3.7 KiB
Go

package maker
import (
"encoding/hex"
"math/big"
"testing"
"github.com/luxfi/geth/common"
)
// TestV3Selectors pins the factory.getPool and pool.slot0 selectors to their
// canonical Uniswap-V3 values. They are computed at init from the signature
// strings, so this guards both the spelling and the keccak primitive — a wrong
// selector silently fails every on-chain read.
func TestV3Selectors(t *testing.T) {
if got := hex.EncodeToString(sigGetPool); got != "1698ee82" {
t.Fatalf("getPool selector = %s, want 1698ee82", got)
}
if got := hex.EncodeToString(sigSlot0); got != "3850c7bd" {
t.Fatalf("slot0 selector = %s, want 3850c7bd", got)
}
}
// sqrtX96For returns sqrtPriceX96 encoding a raw token1/token0 wei ratio: it sets
// sqrtPriceX96 = sqrt(raw)·2^96 (the V3 fixed-point spot encoding).
func sqrtX96For(raw float64) *big.Int {
q96 := new(big.Float).SetInt(new(big.Int).Lsh(big.NewInt(1), 96))
s := new(big.Float).Mul(new(big.Float).Sqrt(big.NewFloat(raw)), q96)
out, _ := s.Int(nil)
return out
}
// TestV3SpotOrientation verifies marginal-spot decoding from sqrtPriceX96 with
// correct token0/token1 orientation: when base is token0 the price is the raw
// token1/token0; when base is token1 it is inverted. Equal decimals.
func TestV3SpotOrientation(t *testing.T) {
low := common.HexToAddress("0x1111111111111111111111111111111111111111")
high := common.HexToAddress("0x2222222222222222222222222222222222222222")
sqrt := sqrtX96For(4.0) // token1/token0 = 4
// base = low (token0), quote = high (token1): quote per base = 4.
base0 := V3Pair{Base: low, Quote: high, BaseDecimals: 18, QuoteDecimals: 18}
if got := v3SpotFromSqrt(sqrt, base0); !approx(got, 4.0) {
t.Fatalf("base=token0 spot = %v, want 4", got)
}
// base = high (token1), quote = low (token0): quote per base = 1/4 = 0.25.
base1 := V3Pair{Base: high, Quote: low, BaseDecimals: 18, QuoteDecimals: 18}
if got := v3SpotFromSqrt(sqrt, base1); !approx(got, 0.25) {
t.Fatalf("base=token1 spot = %v, want 0.25", got)
}
}
// TestV3SpotDecimals verifies the decimal adjustment: token0 has 6 decimals,
// token1 has 18, raw wei ratio 4 → human token1/token0 = 4·10^6/10^18 = 4e-12.
func TestV3SpotDecimals(t *testing.T) {
low := common.HexToAddress("0x1111111111111111111111111111111111111111") // token0, 6 dec
high := common.HexToAddress("0x2222222222222222222222222222222222222222") // token1, 18 dec
p := V3Pair{Base: low, Quote: high, BaseDecimals: 6, QuoteDecimals: 18}
if got := v3SpotFromSqrt(sqrtX96For(4.0), p); !approx(got, 4e-12) {
t.Fatalf("decimal-adjusted spot = %v, want 4e-12", got)
}
}
// TestBytesLess pins the V3 token0/token1 (lower-address) ordering.
func TestBytesLess(t *testing.T) {
a := common.HexToAddress("0x4888e4a2ee0f03051c72d2bd3acf755ed3498b3e") // WLUX
b := common.HexToAddress("0x848Cff46eb323f323b6Bbe1Df274E40793d7f2c2") // LUSD
if !bytesLess(a, b) {
t.Fatal("WLUX should sort before LUSD (token0)")
}
if bytesLess(b, a) {
t.Fatal("LUSD should not sort before WLUX")
}
}
// TestNewV3FeedNilWithoutEnv confirms the feed is nil (opt-in) when any required
// address is absent, and that a real quote token id round-trips to its address.
func TestNewV3FeedNilWithoutEnv(t *testing.T) {
pairs := []Pair{{Symbol: "LUX/LUSD", Quote: AssetAddress("0x848Cff46eb323f323b6Bbe1Df274E40793d7f2c2")}}
if NewV3Feed(pairs, "", "0xfactory", "0xwlux", 18, nil) != nil {
t.Fatal("expected nil feed when rpcURL is empty")
}
if got := common.BytesToAddress(pairs[0].Quote[12:]); got != common.HexToAddress("0x848Cff46eb323f323b6Bbe1Df274E40793d7f2c2") {
t.Fatalf("quote address extraction = %s", got.Hex())
}
}
func approx(a, b float64) bool {
d := a - b
if d < 0 {
d = -d
}
tol := b * 1e-9
if tol < 0 {
tol = -tol
}
if tol < 1e-18 {
tol = 1e-18
}
return d <= tol
}