mirror of
https://github.com/luxfi/graph.git
synced 2026-09-24 00:26:07 +00:00
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.
177 lines
6.0 KiB
Go
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()))
|
|
}
|