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.
184 lines
6.6 KiB
Go
184 lines
6.6 KiB
Go
package indexer
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/luxfi/graph/storage"
|
|
)
|
|
|
|
// Circulating is what is left after everything that exists but cannot be sold:
|
|
// what the treasury has not distributed, and what is bonded to a validator. It
|
|
// is the difference between a market cap and a fully diluted value.
|
|
func TestCirculatingExcludesStake(t *testing.T) {
|
|
// Lux as it actually stands: 2T minted, 994.7B still in the treasury,
|
|
// 2.5B bonded. Reading the P-Chain's own counter instead reported 13.27B
|
|
// as the supply and priced the token 150x under.
|
|
n := NativeSupply{Minted: 2_000_000_000_000, Undistributed: 994_738_895_226, Staked: 2_500_000_002}
|
|
if got := n.Circulating(); got != 1_002_761_104_772 {
|
|
t.Errorf("circulating = %.0f, want 1002761104772", got)
|
|
}
|
|
}
|
|
|
|
// Undistributed supply is not circulating supply. A treasury holding half the
|
|
// mint would otherwise double the reported market cap.
|
|
func TestTreasuryIsNotCirculating(t *testing.T) {
|
|
n := NativeSupply{Minted: 1000, Undistributed: 500}
|
|
if got := n.Circulating(); got != 500 {
|
|
t.Errorf("circulating = %.0f, want 500", got)
|
|
}
|
|
}
|
|
|
|
// A treasury that somehow exceeds the mint must not produce a negative supply,
|
|
// which would render as a negative market cap.
|
|
func TestCirculatingNeverNegative(t *testing.T) {
|
|
n := NativeSupply{Minted: 100, Undistributed: 200}
|
|
if got := n.Circulating(); got != 0 {
|
|
t.Errorf("circulating = %.0f, want 0", got)
|
|
}
|
|
}
|
|
|
|
// A chain with no validators of its own has nothing bonded, so all of its
|
|
// supply circulates. That is a real answer, not a missing one.
|
|
func TestNoStakeMeansAllCirculates(t *testing.T) {
|
|
n := NativeSupply{Minted: 1000}
|
|
if got := n.Circulating(); got != 1000 {
|
|
t.Errorf("circulating = %.0f, want 1000", got)
|
|
}
|
|
}
|
|
|
|
// The P-Chain sits beside the EVM chain under one node, so its endpoint is
|
|
// derived rather than configured — nothing to set, nothing to drift.
|
|
func TestPlatformEndpointDerivesFromEVM(t *testing.T) {
|
|
for _, c := range []struct{ in, want string }{
|
|
{"https://api.lux.network/v1/chain/c/rpc", "https://api.lux.network/v1/chain/p"},
|
|
{"http://luxd:9630/v1/chain/c/rpc", "http://luxd:9630/v1/chain/p"},
|
|
} {
|
|
got, ok := platformEndpoint(c.in)
|
|
if !ok || got != c.want {
|
|
t.Errorf("platformEndpoint(%q) = %q,%v; want %q", c.in, got, ok, c.want)
|
|
}
|
|
}
|
|
// An endpoint that names no chain cannot be turned into one, and guessing
|
|
// would point supply reads at whatever answered.
|
|
if _, ok := platformEndpoint("https://example.com/rpc"); ok {
|
|
t.Error("a URL with no /bc/ must not yield a platform endpoint")
|
|
}
|
|
}
|
|
|
|
// Whole units, no exponent: two trillion in scientific notation parses to a
|
|
// different number in some clients and to NaN in others.
|
|
func TestUnitsAreWrittenPlainly(t *testing.T) {
|
|
if got := fmtUnits(2_000_000_000_000); got != "2000000000000" {
|
|
t.Errorf("fmtUnits = %q, want 2000000000000", got)
|
|
}
|
|
}
|
|
|
|
// The coin's minted supply survives a copy of its row written after the publish.
|
|
//
|
|
// The valuation pass reads every token into memory at the top and writes those
|
|
// copies back near the end. A supply written only to the store sits inside that
|
|
// window: the copy read before it lands on top, and the chain's own coin then
|
|
// reports whatever its wrapper holds wrapped — Zoo's two trillion showed as the
|
|
// fifteen billion sitting in the contract.
|
|
func TestNativeSupplySurvivesAStaleCopy(t *testing.T) {
|
|
const wrapper = "0x4888e4a2ee0f03051c72d2bd3acf755ed3498b3e"
|
|
s := newMemSQLiteStore(t)
|
|
// The wrapper as the chain reports it: what has actually been wrapped.
|
|
s.SeedToken(wrapper, &storage.SeedTokenData{
|
|
Symbol: "WLUX", Name: "Wrapped LUX", Decimals: 18, TotalSupply: "15026903324",
|
|
})
|
|
idx := NewWithConfig(Config{
|
|
RPC: chainAt(t, 1767225600, nil).URL,
|
|
Native: wrapper,
|
|
GenesisSupply: "2000000000000",
|
|
}, s)
|
|
|
|
// What the pass does: read the rows, publish the supply, write the rows back.
|
|
inFlight := s.TokensRaw()
|
|
idx.publishNativeSupply(context.Background(), inFlight)
|
|
for addr, t := range inFlight {
|
|
s.SeedToken(addr, t)
|
|
}
|
|
|
|
got := s.TokensRaw()[wrapper]
|
|
if got == nil {
|
|
t.Fatal("the wrapper's row vanished")
|
|
}
|
|
if got.TotalSupply != "2000000000000" {
|
|
t.Errorf("supply = %q, want the minted 2000000000000 — a copy read before the publish landed on top", got.TotalSupply)
|
|
}
|
|
}
|
|
|
|
// Volume for all time is the whole table, not the window a pass reads.
|
|
//
|
|
// A pass values a bounded slice of the swap table because that table grows
|
|
// forever. The bound is right for the pass and wrong for the field the wire
|
|
// calls total volume. Summing the day series does not escape it — those rows are
|
|
// folded from the same window, so their sum is that window again.
|
|
//
|
|
// A pool's own figure and the protocol's total are the same question sliced two
|
|
// ways, so one grouped answer serves both: asked separately they drifted fifty
|
|
// times apart, a landing page claiming $206,718 over a list of pools claiming
|
|
// $4,159.
|
|
func TestTradedByPoolCoversEveryTrade(t *testing.T) {
|
|
s := newMemSQLiteStore(t)
|
|
|
|
for i, tr := range []struct {
|
|
pool string
|
|
usd string
|
|
}{{"0xaaa", "1000.00"}, {"0xaaa", "2000.00"}, {"0xbbb", "500.00"}} {
|
|
s.SeedSwap(fmt.Sprintf("0xabc#%d", i), &storage.SeedSwapData{
|
|
Timestamp: int64(1767225600 + i*86400),
|
|
Block: uint64(100 + i),
|
|
Pool: tr.pool,
|
|
AmountUSD: tr.usd,
|
|
})
|
|
}
|
|
|
|
got, err := s.TradedByPool()
|
|
if err != nil {
|
|
t.Fatalf("TradedByPool: %v", err)
|
|
}
|
|
if got["0xaaa"].VolumeUSD != 3000 || got["0xaaa"].Trades != 2 {
|
|
t.Errorf("0xaaa = %+v, want 2 trades worth 3000", got["0xaaa"])
|
|
}
|
|
if got["0xbbb"].VolumeUSD != 500 || got["0xbbb"].Trades != 1 {
|
|
t.Errorf("0xbbb = %+v, want 1 trade worth 500", got["0xbbb"])
|
|
}
|
|
|
|
var total float64
|
|
for _, v := range got {
|
|
total += v.VolumeUSD
|
|
}
|
|
if total != 3500 {
|
|
t.Errorf("protocol total = %v, want 3500 — it is the fold of the pools, not a second sum", total)
|
|
}
|
|
}
|
|
|
|
// Stake is read from the P-Chain's REST op beside the C-Chain, in microLUX.
|
|
func TestTotalStakeReadsThePChainOp(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet || r.URL.Path != "/v1/chain/p/ops/stake/total" {
|
|
t.Errorf("unexpected %s %s", r.Method, r.URL.Path)
|
|
w.WriteHeader(http.StatusNotFound)
|
|
return
|
|
}
|
|
w.Write([]byte(`{"stake":"15000000000","weight":"15000000000"}`))
|
|
}))
|
|
t.Cleanup(srv.Close)
|
|
|
|
idx := NewWithConfig(Config{RPC: srv.URL + "/v1/chain/c/rpc"}, newMemSQLiteStore(t))
|
|
base, ok := platformEndpoint(idx.rpc)
|
|
if !ok {
|
|
t.Fatal("no P-Chain endpoint derived")
|
|
}
|
|
got, ok := idx.totalStake(context.Background(), base)
|
|
if !ok || got != 15000 {
|
|
t.Errorf("totalStake = %v,%v; want 15000 LUX", got, ok)
|
|
}
|
|
}
|