mirror of
https://github.com/luxfi/graph.git
synced 2026-09-24 00:26:07 +00:00
price every token on the wire, not just in the pass
The valuation pass has always priced every token in USD — that is where TVL and volume come from — but it published none of it. The subgraph format carries a price only as the product `token.derivedETH * bundle.ethPriceUSD`, and both sides were left unwritten: mainnet served derivedETH="" for all 8 tokens against a null bundle. lux.exchange and the explorer therefore rendered real TVL and real volume beside a blank price and no market cap, on every market. "ETH" is the wire's name for the chain's own coin, so the bundle now carries the native coin's USD price and each token its price in native units. The two multiply back to exactly the USD price the pass computed, and the wrapped native quotes 1 of itself by construction. No second price source, no oracle: one relaxation, two projections of it. Unpriced stays unpriced. A token no pool connects to a stable publishes an empty derivedETH and no bundle is written at all — a zero there would render every token free rather than unquoted. fmtUSD could not carry these numbers: it is a CURRENCY format, and at two decimals LUX at $0.00041334 renders "0.00". fmtPrice keeps 12 significant digits. Wrapped-native address is per-chain (Zoo's differs), so it follows the factories' existing pattern — Config field, Lux mainnet default, env override. Tests run under -tags nosqlite: the default store links two sqlite C implementations (mattn + sqlite3vfs) and 283 duplicate symbols make the test binary unlinkable, which is why the swap-count tests added earlier had never once executed. They pass now, along with the new ones. Removing the SeedBundle call fails TestPricesReachTheWire with "bundle absent", so the test tracks the publication and not a reimplementation of it. Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
@@ -102,6 +102,7 @@ func main() {
|
||||
FactoryV2: os.Getenv("FACTORY_V2"),
|
||||
FactoryV3: os.Getenv("FACTORY_V3"),
|
||||
PoolManager: os.Getenv("POOL_MANAGER"),
|
||||
Native: os.Getenv("WRAPPED_NATIVE"),
|
||||
}, store)
|
||||
// One-shot enrichment of Token rows persisted by an older build with the
|
||||
// address placeholder (symbol == shortAddr). Opt-in via BACKFILL_TOKENS=1 so
|
||||
|
||||
@@ -45,6 +45,12 @@ type Config struct {
|
||||
// FactoryV3 is the canonical Uniswap-v3 factory — same trust-root role as
|
||||
// FactoryV2 for PoolCreated and V3 Swaps. Lower-cased on construction.
|
||||
FactoryV3 string
|
||||
// Native is the wrapped native token (WLUX on Lux, WZOO on Zoo). The wire
|
||||
// format quotes every token against it — `token.derivedETH` is a price in
|
||||
// native units and `bundle.ethPriceUSD` is what one native unit is worth —
|
||||
// so this address is what makes a price expressible at all. Lower-cased on
|
||||
// construction. Empty => Lux mainnet's.
|
||||
Native string
|
||||
// StartBlock is the genesis-relative block to (re)index from. On a clean
|
||||
// chain relaunch the indexer rewinds to this value (see poll's reorg guard).
|
||||
StartBlock uint64
|
||||
@@ -94,6 +100,7 @@ type Indexer struct {
|
||||
poolManager string // lower-cased 0x9999 settlement address
|
||||
factoryV2 string // lower-cased canonical V2 factory (PairCreated trust root)
|
||||
factoryV3 string // lower-cased canonical V3 factory (PoolCreated trust root)
|
||||
native string // lower-cased wrapped native token — the unit `derivedETH` is denominated in
|
||||
label string // log prefix identifying this indexer (see Config.Label)
|
||||
startBlock uint64
|
||||
store *storage.Store
|
||||
@@ -141,11 +148,16 @@ func NewWithConfig(cfg Config, store *storage.Store) *Indexer {
|
||||
if fv3 == "" {
|
||||
fv3 = LuxMainnet.FactoryV3
|
||||
}
|
||||
native := cfg.Native
|
||||
if native == "" {
|
||||
native = LuxMainnet.WETH
|
||||
}
|
||||
idx := &Indexer{
|
||||
rpc: cfg.RPC,
|
||||
poolManager: strings.ToLower(pm),
|
||||
factoryV2: strings.ToLower(fv2),
|
||||
factoryV3: strings.ToLower(fv3),
|
||||
native: strings.ToLower(native),
|
||||
label: cfg.Label,
|
||||
startBlock: cfg.StartBlock,
|
||||
store: store,
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
package indexer
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/luxfi/graph/storage"
|
||||
)
|
||||
|
||||
// A price this pass computes must reach the WIRE, not just the arithmetic.
|
||||
//
|
||||
// The valuation pass priced every token in USD and published none of it: the
|
||||
// subgraph format carries a price only as the product
|
||||
// `token.derivedETH * bundle.ethPriceUSD`, and both were left unwritten. Live
|
||||
// mainnet served TVL and volume beside an empty derivedETH and a null bundle,
|
||||
// so lux.exchange and the explorer rendered blank prices and no market cap.
|
||||
//
|
||||
// These assert the published product, not the internal float — the internal
|
||||
// number was always right.
|
||||
|
||||
const (
|
||||
wlux = "0x4888e4a2ee0f03051c72d2bd3acf755ed3498b3e" // the native unit
|
||||
lusd = "0x848cff46eb323f323b6bbe1df274e40793d7f2c2" // stable anchor, $1
|
||||
lzoo = "0x5e5290f350352768bd2bfc59c2da15dd04a7cb88"
|
||||
)
|
||||
|
||||
// priced builds an indexer holding the three tokens the relaxation walks:
|
||||
// a stable anchor, the native unit, and one token priced a hop out.
|
||||
func priced(t *testing.T) *Indexer {
|
||||
t.Helper()
|
||||
store, err := storage.New(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatalf("storage.New: %v", err)
|
||||
}
|
||||
idx := NewWithConfig(Config{RPC: "http://127.0.0.1:0", Native: wlux}, store)
|
||||
idx.store.SeedToken(wlux, &storage.SeedTokenData{Symbol: "WLUX", Decimals: 18})
|
||||
idx.store.SeedToken(lusd, &storage.SeedTokenData{Symbol: "LUSD", Decimals: 18})
|
||||
idx.store.SeedToken(lzoo, &storage.SeedTokenData{Symbol: "LZOO", Decimals: 18})
|
||||
return idx
|
||||
}
|
||||
|
||||
func wireDerivedETH(t *testing.T, idx *Indexer, addr string) string {
|
||||
t.Helper()
|
||||
tk, ok := idx.store.TokensRaw()[addr]
|
||||
if !ok {
|
||||
t.Fatalf("token %s absent", addr)
|
||||
}
|
||||
return tk.DerivedETH
|
||||
}
|
||||
|
||||
// The whole point: derivedETH * ethPriceUSD is the USD price a client shows.
|
||||
func TestPricesReachTheWire(t *testing.T) {
|
||||
const luxUSD, zooUSD = 0.00041334, 0.0000190053
|
||||
|
||||
idx := priced(t)
|
||||
vps := []valuedPool{
|
||||
// 1 LUSD ($1) per 1/luxUSD WLUX fixes WLUX at luxUSD.
|
||||
{id: "p0", t0: lusd, t1: wlux, bal0: 1000, bal1: 1000 / luxUSD, held0: true, held1: true},
|
||||
// LZOO priced one hop out, through WLUX.
|
||||
{id: "p1", t0: wlux, t1: lzoo, bal0: 5000, bal1: 5000 * (luxUSD / zooUSD), held0: true, held1: true},
|
||||
}
|
||||
prices := priceTokens(vps, idx.store.TokensRaw())
|
||||
|
||||
// The PRODUCTION writers, not a copy of them: a test that recomputes the
|
||||
// projection itself passes just as happily when the pass publishes nothing.
|
||||
nativeUSD := idx.publishAnchor(prices)
|
||||
if math.Abs(nativeUSD-luxUSD)/luxUSD > 1e-9 {
|
||||
t.Fatalf("native price = %g, want %g", nativeUSD, luxUSD)
|
||||
}
|
||||
for addr, tk := range idx.store.TokensRaw() {
|
||||
p, ok := prices[addr]
|
||||
tk.DerivedETH = derivedNative(p, nativeUSD, ok)
|
||||
idx.store.SeedToken(addr, tk)
|
||||
}
|
||||
|
||||
b, err := idx.store.GetBundle(nil, "1")
|
||||
if err != nil {
|
||||
t.Fatalf("GetBundle: %v", err)
|
||||
}
|
||||
m, ok := b.(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatal("bundle absent — a client has no USD anchor and every price renders blank")
|
||||
}
|
||||
eth, err := strconv.ParseFloat(m["ethPriceUSD"].(string), 64)
|
||||
if err != nil {
|
||||
t.Fatalf("ethPriceUSD unparseable: %v", err)
|
||||
}
|
||||
|
||||
// The wrapped native quotes exactly 1 of itself.
|
||||
if got := wireDerivedETH(t, idx, wlux); got != "1" {
|
||||
t.Errorf("WLUX derivedETH = %q, want \"1\"", got)
|
||||
}
|
||||
|
||||
for _, c := range []struct {
|
||||
addr string
|
||||
want float64
|
||||
}{{wlux, luxUSD}, {lzoo, zooUSD}} {
|
||||
d, err := strconv.ParseFloat(wireDerivedETH(t, idx, c.addr), 64)
|
||||
if err != nil {
|
||||
t.Fatalf("%s derivedETH unparseable: %v", c.addr, err)
|
||||
}
|
||||
if got := d * eth; math.Abs(got-c.want)/c.want > 1e-6 {
|
||||
t.Errorf("%s: derivedETH*ethPriceUSD = %g, want %g", c.addr, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fmtUSD is a currency format; using it for a price rounds every sub-cent token
|
||||
// to "0.00", which reads as free rather than as small.
|
||||
func TestSubCentPriceSurvivesFormatting(t *testing.T) {
|
||||
if got := fmtUSD(0.00041334); got != "0.00" {
|
||||
t.Fatalf("fmtUSD(0.00041334) = %q — this test's premise is stale", got)
|
||||
}
|
||||
v, err := strconv.ParseFloat(fmtPrice(0.00041334), 64)
|
||||
if err != nil {
|
||||
t.Fatalf("fmtPrice unparseable: %v", err)
|
||||
}
|
||||
if math.Abs(v-0.00041334)/0.00041334 > 1e-9 {
|
||||
t.Errorf("fmtPrice(0.00041334) = %g, want 0.00041334", v)
|
||||
}
|
||||
}
|
||||
|
||||
// No anchor means no price. A zero here would be a fabricated quote.
|
||||
func TestUnpricedStaysEmpty(t *testing.T) {
|
||||
if got := fmtPrice(0); got != "" {
|
||||
t.Errorf("fmtPrice(0) = %q, want empty", got)
|
||||
}
|
||||
if got := fmtPrice(math.NaN()); got != "" {
|
||||
t.Errorf("fmtPrice(NaN) = %q, want empty", got)
|
||||
}
|
||||
}
|
||||
+57
-3
@@ -156,6 +156,24 @@ func (idx *Indexer) revalue(parent context.Context) {
|
||||
}
|
||||
prices := priceTokens(vps, tokens)
|
||||
|
||||
// ── The USD anchor, published in the shape clients read ────────────
|
||||
//
|
||||
// This pass prices every token in USD directly, but the subgraph wire
|
||||
// format has no such field: a client reads a price as the PRODUCT
|
||||
// `token.derivedETH * bundle.ethPriceUSD`. Both were left unwritten, so
|
||||
// every token served an empty derivedETH against a null bundle and the
|
||||
// whole site rendered TVL and volume beside a blank price and a blank
|
||||
// market cap.
|
||||
//
|
||||
// "ETH" is the wire format's name for the chain's own coin — here LUX. So
|
||||
// the bundle carries the native coin's USD price and each token its price
|
||||
// in native units, which multiply back to exactly the USD price computed
|
||||
// above. The wrapped native token quotes 1 by construction.
|
||||
//
|
||||
// Unpriced stays unpriced: no anchor means no bundle and no derivedETH,
|
||||
// never a zero standing in for a number nobody knows.
|
||||
nativeUSD := idx.publishAnchor(prices)
|
||||
|
||||
// ── Pools: TVL, spot prices, volume ────────────────────────────────
|
||||
tokenTVL := map[string]float64{}
|
||||
tokenVol := map[string]float64{}
|
||||
@@ -207,13 +225,16 @@ func (idx *Indexer) revalue(parent context.Context) {
|
||||
|
||||
// ── Tokens: TVL and volume across every pool holding them ──────────
|
||||
for addr, t := range tokens {
|
||||
tvl, hasTVL := tokenTVL[strings.ToLower(addr)]
|
||||
vol, hasVol := tokenVol[strings.ToLower(addr)]
|
||||
if !hasTVL && !hasVol {
|
||||
low := strings.ToLower(addr)
|
||||
tvl, hasTVL := tokenTVL[low]
|
||||
vol, hasVol := tokenVol[low]
|
||||
price, hasPrice := prices[low]
|
||||
if !hasTVL && !hasVol && !hasPrice {
|
||||
continue
|
||||
}
|
||||
t.TotalValueLockedUSD = fmtUSD(tvl)
|
||||
t.VolumeUSD = fmtUSD(vol)
|
||||
t.DerivedETH = derivedNative(price, nativeUSD, hasPrice)
|
||||
idx.store.SeedToken(addr, t)
|
||||
}
|
||||
|
||||
@@ -554,6 +575,39 @@ func fmtUSD(v float64) string {
|
||||
return strconv.FormatFloat(v, 'f', 2, 64)
|
||||
}
|
||||
|
||||
// publishAnchor writes the native coin's USD price — the anchor every quoted
|
||||
// price is read against — and returns it. Zero when no pool connects the native
|
||||
// coin to a stable: then nothing is quotable and nothing is published, rather
|
||||
// than a bundle of zero that would render every token free.
|
||||
func (idx *Indexer) publishAnchor(prices map[string]float64) float64 {
|
||||
usd := prices[idx.native]
|
||||
if usd <= 0 {
|
||||
return 0
|
||||
}
|
||||
idx.store.SeedBundle("1", &storage.SeedBundleData{EthPriceUSD: fmtPrice(usd)})
|
||||
return usd
|
||||
}
|
||||
|
||||
// derivedNative converts a USD price into the wire's native-denominated one, so
|
||||
// that derivedETH * bundle.ethPriceUSD is exactly the USD price again. Empty
|
||||
// when either side is unknown — an absent quote, not a zero one.
|
||||
func derivedNative(priceUSD, nativeUSD float64, priced bool) string {
|
||||
if !priced || nativeUSD <= 0 {
|
||||
return ""
|
||||
}
|
||||
return fmtPrice(priceUSD / nativeUSD)
|
||||
}
|
||||
|
||||
// fmtPrice renders a price at full significance. fmtUSD's two decimals are a
|
||||
// CURRENCY format and wrong for a price: LUX at $0.00041334 renders as "0.00"
|
||||
// there, which reads as free rather than as small.
|
||||
func fmtPrice(v float64) string {
|
||||
if v <= 0 || math.IsNaN(v) || math.IsInf(v, 0) {
|
||||
return ""
|
||||
}
|
||||
return strconv.FormatFloat(v, 'g', 12, 64)
|
||||
}
|
||||
|
||||
// fmtRatio renders a spot price as num/den, matching the Uniswap convention
|
||||
// that Pair.token0Price is reserve0/reserve1. An undefined ratio renders "0".
|
||||
func fmtRatio(num, den float64) string {
|
||||
|
||||
Reference in New Issue
Block a user