Context
ADR-0011 Algorithm 3 derives SEP-41 token supply from a running event sum, total = Σ mint − Σ burn − Σ clawback and circulating = total − admin_balance. It accumulates events rather than reading state, and the contract's total is not stored anywhere; discovery records contract ids but does not aggregate amounts. Amounts are i128 (ADR-0003).
Decision
An event-stream observer, internal/sources/sep41_supply, writes mint, burn and clawback events to sep41_supply_events (migration 0015), and supply.StorageSEP41SupplyReader feeds the real SEP41Computer (internal/supply/sep41.go).
- Why events, not ledger entries. Summing holder balances needs a generic SEP-41
ContractDataobserver that does not exist, is expensive even for SAC-backed tokens, and the SEP-41 mint/burn/clawback events are the audit trail. The event sum works for classic-SAC and pure SEP-41 tokens alike. - Match.
contract_id ∈ watched set AND topic[0] ∈ {mint, burn, clawback}, using its own copy of the sniffer'sclassifySymbol(the sniffer's helper is unexported). Bodies are either a barei128or the CAP-67 map{amount, to_muxed_id}; the decoder type-tests before reading the amount:
| Event | Topic shape |
|---|---|
mint | ["mint", admin, to] |
burn | ["burn", from] |
clawback | ["clawback", admin, from] |
- Table. Key
(contract_id, ledger, tx_hash, op_index, observed_at);event_kindconstrained to the three kinds;amount NUMERIC CHECK (amount >= 0), direction carried by the kind (mint adds, burn and clawback subtract); nullablecounterparty(mint: the recipient; burn and clawback: the holder reduced). Hypertable onobserved_at, 7-day chunks. - Storage methods.
InsertSEP41SupplyEventis idempotent on the key;SEP41NetMintAtOrBefore(contract, ledger)returnsΣ mint − Σ(burn + clawback). - Reader.
total = net mint;circulating = total − admin balance − Σ locked, the admin and locked holders read fromsac_balance_observations. - Watched set.
[supply] watched_sep41_contracts(C-strkeys), keyed by the bare contract id insupply.AssetKey; onesupply.Refresherper contract in the aggregator (buildSEP41Refreshers), with the sharedstellarindex_aggregator_supply_refresh_total{outcome}counter. - Serving fallback.
/v1/assets/{id}and/v1/assets/{id}/supplyfall back to the lake-derived supply,Σ mint − Σ burn − Σ clawbackover the certified ClickHouse lake'ssupply_flows(supply_basis: "sep41_lake_flows"), so every SEP-41 token serves a complete-history total without curation. The watched-set path is the precise source where admin and locked-set exclusion matter. - Transfers are ignored. They move ownership, not supply.
Invariant
- SEP-41 supply is
Σ mint − Σ burn − Σ clawback;transfernever enters it. Enforced by theinternal/supplySEP-41 reader and computer tests. - Event amounts are NUMERIC, non-negative, never truncated (ADR-0003); the kind discriminates direction.
- The decoder matches only
mint,burnandclawbackon a watched contract, so it never claims a topic another decoder owns (first-match-wins). sep41_supply_eventshas one writer, the projector (ADR-0031/0032), so catch-up isprojector-replay, not a second path.
Consequences
- One hypertable with low row volume (a handful of events per contract per day) while the watched set stays bounded.
- Three-domain supply coverage closes ADR-0011.
- Admin and locked-set exclusion is operator policy and needs the watched-set path; the lake-derived total does not apply it.
Evidence
- Migration 0015;
internal/sources/sep41_supply;internal/supply/storage_sep41_reader.go,internal/supply/sep41.go;internal/projector/registry.go(sep41_supplycase). - ADR-0003, ADR-0011, ADR-0021, ADR-0022.