Volume for all time is the days added up, not one window summed

A pass values a bounded slice of the swap table, newest first, because the table
grows forever and rescanning it would cost more every day the chain lives. That
bound is right for the pass and wrong for the field the wire calls total volume:
totalling a window answers for the newest trades only. On the Lux C-Chain, with
a million swaps behind it, the protocol reported the last twelve percent of its
trading and called it everything.

The day series already holds each day's volume, written once by the pass that
saw that day and kept since. Their sum is the whole history at the cost of
reading a row per day, and it grows correctly as days accrue rather than sliding
forward and losing the past. The window is untouched; only what is claimed of it
changes.

The test gives the store three days and a window that would cover one.

Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
2026-08-14 06:34:29 -07:00
co-authored by Hanzo Dev
parent c8725690c3
commit 84d71e5a98
2 changed files with 78 additions and 1 deletions
+28
View File
@@ -2,6 +2,7 @@ package indexer
import (
"context"
"fmt"
"testing"
"github.com/luxfi/graph/storage"
@@ -109,3 +110,30 @@ func TestNativeSupplySurvivesAStaleCopy(t *testing.T) {
t.Errorf("supply = %q, want the minted 2000000000000 — a copy read before the publish landed on top", got.TotalSupply)
}
}
// Volume for all time is every day added up, not the newest window summed.
//
// 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: on a chain with a million swaps it reported the last
// twelve percent and called it everything. The day rows are written once and
// kept, so their sum is the whole history at the cost of a row per day.
func TestFactoryVolumeCountsEveryDay(t *testing.T) {
s := newMemSQLiteStore(t)
idx := NewWithConfig(Config{RPC: chainAt(t, 1767225600, nil).URL}, s)
// Three days the indexer has seen, only one of which a recent window covers.
for i, v := range []string{"1000.00", "2000.00", "500.00"} {
s.SetEntity(storage.FactoryDay, fmt.Sprintf("1-%d", 20000+i), map[string]interface{}{
"id": fmt.Sprintf("1-%d", 20000+i), "date": (20000 + i) * 86400, "volumeUSD": v,
})
}
got, err := idx.factoryVolume()
if err != nil {
t.Fatalf("factoryVolume: %v", err)
}
if got != 3500 {
t.Errorf("volume = %v, want 3500 — a day outside the window still traded", got)
}
}
+50 -1
View File
@@ -275,11 +275,28 @@ func (idx *Indexer) revalue(parent context.Context) {
if m, ok := f.(map[string]interface{}); ok {
txCount = asInt64(m["txCount"])
}
// Volume for all time is the days added up, not one window summed.
//
// A pass values a bounded slice of the swap table, newest first, because the
// table grows forever and rescanning it would cost more every day the chain
// lives. That bound is right for the pass and wrong for this field: the wire
// calls it total volume, and totalling a window answers for the newest
// trades only — on a chain with a million swaps it reported the last
// twelve percent and called it everything.
//
// The day series already holds each day's volume, written once and kept, so
// the sum of those rows is the whole history at the cost of reading a row
// per day. The window stays exactly as it is; only what is claimed of it
// changes.
volume := totalVol
if allDays, err := idx.factoryVolume(); err == nil && allDays > volume {
volume = allDays
}
idx.store.SeedFactory("1", &storage.SeedFactoryData{
PoolCount: int64(len(pools)),
TxCount: txCount,
TotalValueLockedUSD: fmtUSD(totalTVL),
TotalVolumeUSD: fmtUSD(totalVol),
TotalVolumeUSD: fmtUSD(volume),
})
// ── The day series: the same trades, grouped by the day they happened ──
@@ -665,3 +682,35 @@ func fmtRatio(num, den float64) string {
}
return strconv.FormatFloat(num/den, 'g', 12, 64)
}
// factoryVolume adds up every day the protocol has traded.
//
// Each row was written by the pass that saw that day and has been kept since,
// so this is the whole history without rereading a swap. A day the indexer
// never saw contributes nothing, which is honest: it can only report what it
// has been shown.
func (idx *Indexer) factoryVolume() (float64, error) {
rows, err := idx.store.Entities(storage.FactoryDay, maxValuedPools, "date", "asc", nil)
if err != nil {
return 0, err
}
list, ok := rows.([]interface{})
if !ok {
return 0, nil
}
var sum float64
for _, r := range list {
m, ok := r.(map[string]interface{})
if !ok {
continue
}
// The same overflow guard the rest of this pass uses (1e12): a tiny pool
// can produce a decimal artefact that would otherwise become the headline.
v, err := strconv.ParseFloat(fmt.Sprint(m["volumeUSD"]), 64)
if err != nil || !(v > 0) || v > 1e12 {
continue
}
sum += v
}
return sum, nil
}