Files
zeekay 8142506538 Value pools only at a head that has reached the index, and read stake where luxd serves it
A chain re-imported from genesis reports a head far under the indexed cursor;
its state there predates the events, so the pass now leaves the aggregates as
they are until the head catches up.

Stake is GET /v1/chain/p/ops/stake/total, in microLUX. The read used the
removed /bc/ path, JSON-RPC and a 10^9 divisor, so it always reported zero.
go.sum records hanzoai/replicate v0.6.0 as sum.golang.org does.
2026-09-23 00:35:11 -07:00

177 lines
6.0 KiB
Go

package indexer
import (
"context"
"encoding/json"
"math/big"
"net/http"
"strconv"
"strings"
"github.com/luxfi/graph/storage"
)
// What a native token is worth depends on three numbers, and they live in three
// different places. Getting the first one wrong is how a two-trillion-supply
// token reported a market cap of $4.8M.
//
// minted — every unit created at block 0. A fact of genesis, not a
// query: the alloc is not exposed over RPC and summing every
// account is not something a node will do for you.
// undistributed — what the treasury still holds, read live, because it falls
// as tokens reach holders. Lux minted 2T to one address and
// that address holds 994.7B today; the trillion that left is
// exactly the point.
// staked — bonded to a validator on the primary network. It has reached
// holders but cannot be sold without unstaking first.
//
// Circulating is what is left, and it is the only sound basis for a market cap.
// Fully diluted value uses the whole minted supply, which is why they differ.
//
// The trap worth naming: the P-Chain's current-supply counter is NOT the token's
// supply. On Lux it reads 13.27 billion against a real supply of two trillion.
// Trusting it put the market cap out by 150x.
//
// The P-Chain counts in microLUX: 1 LUX is 10^6 (luxd utils/units).
const microLUX = 1e6
// fmtUnits writes a whole-token amount without exponent or separators, so the
// client parses it as a number rather than guessing at a format.
func fmtUnits(v float64) string { return strconv.FormatFloat(v, 'f', -1, 64) }
// NativeSupply is what the chain says about its own token, in whole units.
type NativeSupply struct {
// Minted is every unit created at genesis.
Minted float64
// Undistributed is what the treasury still holds.
Undistributed float64
// Staked is bonded to a validator.
Staked float64
}
// Circulating is what can actually change hands.
func (n NativeSupply) Circulating() float64 {
c := n.Minted - n.Undistributed - n.Staked
if c < 0 {
return 0
}
return c
}
// readNativeSupply gathers the three figures.
func (idx *Indexer) readNativeSupply(ctx context.Context) (NativeSupply, bool) {
minted, err := strconv.ParseFloat(idx.genesisSupply, 64)
if err != nil || minted <= 0 {
return NativeSupply{}, false // no declared supply ⇒ say nothing
}
out := NativeSupply{Minted: minted}
if idx.treasury != "" {
if bal, ok := idx.nativeBalance(ctx, idx.treasury); ok {
out.Undistributed = bal
}
}
if base, ok := platformEndpoint(idx.rpc); ok {
// A chain with no validators of its own answers zero, which is honest.
out.Staked, _ = idx.totalStake(ctx, base)
}
return out, true
}
// nativeBalance reads an account's native balance in whole units.
func (idx *Indexer) nativeBalance(ctx context.Context, addr string) (float64, bool) {
raw, err := idx.rpcCall(ctx, "eth_getBalance", []interface{}{addr, "latest"})
if err != nil {
return 0, false
}
var hex string
if json.Unmarshal(raw, &hex) != nil {
return 0, false
}
n, ok := new(big.Int).SetString(strings.TrimPrefix(hex, "0x"), 16)
if !ok {
return 0, false
}
f, _ := new(big.Float).Quo(new(big.Float).SetInt(n), big.NewFloat(1e18)).Float64()
return f, true
}
// platformEndpoint turns a C-Chain RPC URL into its P-Chain sibling. The two
// chains sit beside each other under the same node, so what is staked is one
// derived URL away and nothing needs configuring.
func platformEndpoint(evmRPC string) (string, bool) {
i := strings.Index(evmRPC, "/chain/")
if i < 0 {
return "", false
}
return evmRPC[:i] + "/chain/p", true
}
// totalStake reads the primary network's bonded stake in whole LUX:
// GET <p-chain>/ops/stake/total, answered in microLUX.
func (idx *Indexer) totalStake(ctx context.Context, platform string) (float64, bool) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, platform+"/ops/stake/total", nil)
if err != nil {
return 0, false
}
resp, err := idx.client.Do(req)
if err != nil {
return 0, false
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return 0, false
}
var out struct {
Stake string `json:"stake"`
}
if json.NewDecoder(resp.Body).Decode(&out) != nil {
return 0, false
}
n, ok := new(big.Int).SetString(out.Stake, 10)
if !ok {
return 0, false
}
f, _ := new(big.Float).Quo(new(big.Float).SetInt(n), big.NewFloat(microLUX)).Float64()
return f, true
}
// publishNativeSupply records the figures on the wrapped native token — the
// entity a token page opens for LUX.
//
// Written each pass rather than once: the treasury balance falls as tokens are
// distributed and the staked figure moves with every delegation, so a value
// read at boot would be quietly stale within the hour.
//
// It writes to the pass's own copy of the row as well as to the store, and that
// is not belt-and-braces. The pass reads every token into memory at the top and
// writes those copies back near the end, so a store-only write sits inside a
// window where a copy read before it lands on top — the coin then reports
// whatever its wrapper holds wrapped, and Zoo's two trillion showed as the
// fifteen billion sitting in the contract. Correcting the copy the pass already
// holds makes the result the same whichever runs last.
func (idx *Indexer) publishNativeSupply(ctx context.Context, inFlight map[string]*storage.SeedTokenData) {
if idx.native == "" {
return
}
n, ok := idx.readNativeSupply(ctx)
if !ok {
return
}
tok := inFlight[idx.native]
if tok == nil {
tok = idx.store.TokensRaw()[idx.native]
}
if tok == nil {
return
}
tok.TotalSupply = fmtUnits(n.Minted)
// One field for everything that exists but cannot be sold, because that is
// the single quantity the market-cap subtraction needs.
tok.Staked = fmtUnits(n.Staked + n.Undistributed)
idx.store.SeedToken(idx.native, tok)
idx.logf("[supply] native %s: %s minted, %s undistributed, %s staked, %s circulating",
tok.Symbol, fmtUnits(n.Minted), fmtUnits(n.Undistributed),
fmtUnits(n.Staked), fmtUnits(n.Circulating()))
}