Skip to content

Releases: Stellar-Index/StellarIndex

Stellar Index v0.24.0

Choose a tag to compare

@github-actions github-actions released this 02 Aug 23:03

[v0.24.0] — 2026-08-02

Fixed

  • Router serving is anchored to the highest-confidence route (audit H1) —
    a confidence-blind outlier omission ran before confidence-weighted serving,
    so once a target resolved to ≥3 routes a divergent low-confidence majority
    could evict the trusted route as a price-outlier and set the served composite.
    CombineRoutes now serves the highest-confidence route(s) among all gated
    routes, using a member-median (never an averaged midpoint), so a lower-
    confidence route corroborates and can flag divergence but can never move the
    served price. Outlier omission still computes the diverged/corroboration
    signals. Single-route serving (the whole shipped config) is byte-identical.
  • Corroboration requires a per-route confidence floor (audit M2) — a route
    counts toward the anomaly-freeze corroboration only if its weakest-link
    confidence clears 0.5, so a thin route can't fake independence when
    min_route_confidence ships at 0.
  • Router staleness is immune to wall-clock steps (audit M1) — the composite
    corroboration timestamp retains its monotonic reading (time.Now(), not
    .UTC()), so a backward NTP/VM clock step can't latch a stale freeze-
    suppressing corroboration count.
  • Undirected market dedup (audit M3) — a market quoted in both orientations
    (XLM/USD + USD/XLM) no longer produces duplicate edges / double-counted
    routes.
  • Frozen-leg reroute is gated + flagged like a dry-leg reroute (audit H2) —
    a reroute around a frozen chain leg is now subject to rerouteMinConfidence
    and marked Rerouted; a target frozen on its own direct market withholds
    (frozen_leg) rather than overwriting its frozen last-known-good.
  • Dust guard no longer suppresses on a placeholder "0" volume (audit M5) —
    a Soroban SEP-41 volume-reader artifact ("0") is treated as unmeasured
    (like nil); only a positive sub-floor volume suppresses a market cap.
  • Catalogue rows surface market_cap_low_liquidity (audit M4) — the flag is
    now propagated through mergeTwinStats, so a dust-suppressed verified-currency
    twin no longer shows a null cap without the explaining flag.
  • Native XLM is never dust-suppressed on the detail path (audit L2) —
    mirrors the listing path's existing carve-out.
  • Negative market cap guardusdMarketValue omits a negative market cap
    (a negative-supply data error) rather than serving it, matching the listing
    path's computeMarketCapUSD.
  • Hardening: non-finite route confidence is sanitised and medianRat guards
    empty input (audit L5); cached non-FX chain legs no longer enter the graph at
    max confidence (audit L1); the single-Tick invariant on the orchestrator's
    per-tick maps is documented (audit L4).

Added

  • /v1/price router quality flags (audit L3) — flags.diverged (the
    composite came from disagreeing routes) and flags.rerouted (priced via a
    substitute path around a dry configured leg) now surface the router quality
    signal the aggregator already computed and persisted but that had no reader.

Stellar Index v0.23.0

Choose a tag to compare

@github-actions github-actions released this 02 Aug 20:05

[v0.23.0] — 2026-08-02

Added

  • Multi-path rate router with anti-manipulation corroboration — thin FX
    crosses (e.g. XLM/GBP served 100% from a single venue) can now be priced
    and cross-checked through multiple independent routes over the XLM/BTC/USD
    hub topology (internal/aggregate/router.go), modelled on the predecessor
    Rates Engine. Shortest routes preferred; each route's price is an exact
    big.Rat composite and its confidence the weakest link. The freeze's
    source_count is widened only by routes that are INDEPENDENT
    (edge-disjoint), TIGHTLY agreeing (within 3%, far tighter than the 40%
    outlier band) and non-diverged — a manipulator cannot manufacture false
    corroboration by wash-trading one shared leg (fail-closed: a divergent or
    shared-bottleneck set drops below the single-path baseline). The SERVED
    price is the median of only the highest-confidence surviving route(s), so a
    lower-confidence route (e.g. through a thin, USD-volume-floor-exempt bridge)
    corroborates and can flag divergence but can never move the served value.
    New config aggregate.max_hops (default 3) + aggregate.min_route_confidence
    (default 0.0). Single-route targets — the entire shipped config today — are
    byte-identical to the pre-router behaviour; the machinery activates only when
    a target has a second independent route configured.
  • Valuation-integrity guard on market cap / FDV — a market cap is no
    longer presented as authoritative when its backing price came from
    negligible liquidity (an obscure asset with one $10 SDEX trade must not
    read as worth billions). market_cap_usd and fdv_usd are now SUPPRESSED
    (served null) with a new market_cap_low_liquidity: true flag on
    Asset and GlobalAssetView when the backing price came from a single
    venue AND the asset's trailing-24h USD volume is below the new
    aggregate.min_market_cap_volume_usd floor (default 1000 USD). The AND is
    load-bearing: a single-venue asset with real volume, or any multi-source
    asset, keeps its cap; the price_usd itself is untouched — the guard is on
    the valuation, not the price. Applies to both the /v1/assets/{id} detail
    path (populateMarketCap) and the /v1/assets listing path
    (computeMarketCapUSD); the fiat market cap (M2 × deep FX rate) is
    deliberately not gated. New config knob aggregate.min_market_cap_volume_usd
    (0 disables).

Stellar Index v0.22.0

Choose a tag to compare

@github-actions github-actions released this 01 Aug 22:59

[v0.22.0] — 2026-08-01

Added

  • Cycle-level regression test for the v0.21.12 sink-side adaptive
    shrink
    (internal/projector/cycle_wedge_test.go): drives
    cycleOneSource under an already-expired cycle budget with a sink
    fast-failing on context.DeadlineExceeded and pins that the window
    pointer halves per cycle, floors at MinBatchLimit, the cursor holds
    (no advance-past-loss, no quarantine), and a healthy cycle then
    commits + doubles the window back. The fix previously had only the
    shrinkWindow unit test.
  • tx_hash_index_parity config assertion
    (scripts/ops/config-assertions.sh): hourly sampled parity probe —
    500 random tx hashes from the trailing 10k ledgers must all resolve
    in stellar.tx_hash_index via primary-key point lookups. The
    explorer treats an index miss as an authoritative 404 and the index
    was parity-verified genesis→tip only once (2026-07-30); this catches
    future divergence (MV drop/recreate windows, ATTACH-style loads that
    bypass the MV). Runbook row added to
    docs/operations/runbooks/config-assertion-failed.md.

Fixed

  • Served TWAP now folds the two stored market directions by minute
    COVERAGE, not trade count
    (audit finding M-B;
    internal/storage/timescale/aggregates.go, migration
    0126_twap_sample_count). SDEX stores each market in both
    orientations, and TWAPPointsInRange merged them with a
    trade-count-weighted mean of {twap, 1.0/twap_flipped}. But the
    twap_1h/twap_1d CAGGs (migration 0081) define twap as
    avg(prices_1m.twap) — equal per elapsed MINUTE, deliberately NOT per
    trade — so the only correct merge weight is each direction's minute
    coverage (how many prices_1m minute-buckets it contributed), which
    the CAGGs did not store; count-weighting was exact only when trade
    count happened to track coverage and wrong by an unbounded factor
    otherwise. Migration 0126 adds sample_count (count(*)) to both
    hierarchical TWAP CAGGs, and the read now folds in Go with the new
    combineDirTWAP: Σ(oriented_twap·sample_count)/Σ(sample_count) in
    exact big.Rat, flipped rows inverted as an exact 1/twap (no
    1.0/twap SQL rounding — ADR-0003). Pinned by
    internal/storage/timescale/twap_direction_combine_test.go
    (cannedConn) and TestTWAPSampleCount_CoverageWeighted (real
    TimescaleDB). ⚠ DEPLOY: migration is WITH NO DATA — after applying
    it the operator must re-materialize the two views:
    CALL refresh_continuous_aggregate('twap_1h', NULL, now()); then
    ('twap_1d', NULL, now()); (run after prices_1m is whole).
  • Empty tx_hash_index no longer grants authoritative 404s
    (internal/storage/clickhouse/explorer_reader.go): the tx-hash
    availability probe now requires the index to be NON-EMPTY, not merely
    to exist. An existing-but-empty index (the MV-drop / TRUNCATE
    pathology: transactions keep flowing, the index silently stops) is
    treated as index-unavailable — hash lookups take the bloom-scan path
    and the verdict is not cached, so a repopulated index is picked back
    up on re-probe ("empty table is not a definitive answer", the
    DailyActivityAvailable convention). A per-hash miss against a
    non-empty index remains an authoritative not-found (the DoS
    protection), pinned by new unit tests
    (TestTransactionByHashEmptyIndexFallsBackToScan,
    TestTransactionByHashRepopulatedIndexRegainsAuthority). Complements
    the hourly tx_hash_index_parity assertion, which catches PARTIAL
    divergence. Un-reds TestClickHouseTxHashIndexProbeFallback in CI.
  • Explorer scan-settings integration test seeds genuine LedgerKey
    XDR
    (test/integration/explorer_scan_settings_test.go): the
    trustline seed carried a synthetic "e5-trustline-key" placeholder
    that could never match the trustline/offer readers' real-XDR
    PK-prefix LIKE ranges introduced in 85f706e
    (accountEntryKeyPrefix), so the test failed while production (real
    keys) worked. The seed now marshals real trustline AND offer
    xdr.LedgerKeys + entries for the seeded account, exercising both
    prefix-range paths end-to-end (balance, limit, offer round-trip
    asserted).
    now mirrors the registry/catalogue — the stale 62,403,000 floor left
    each source's true head range invisible to the supporting gap signal
    (the authoritative ADR-0033 verdict was already corrected on 07-31).

Security

  • Unauthenticated metric-cardinality DoS closed (audit W4-obs-1;
    internal/obs/http_middleware.go): unrecognized HTTP methods passed
    through to the Prometheus method label verbatim; net/http accepts any
    token, so curl -X <random> in a loop minted unbounded label children.
    Unknown verbs now collapse to a bounded "other".
  • SSE price-bridge payloads validated + re-marshalled (audit F2;
    internal/api/streaming/redispub): the subscriber fanned the raw Redis
    payload to all stream clients with only an asset/quote-non-empty check;
    with no-AUTH Redis a host-adjacent PUBLISH could inject a forged price.
    Now the decoded event is bounds-validated (exact big.Rat VWAP, sane
    window, fresh timestamp) and the validated struct re-marshalled, dropping
    injected fields.
  • SAC/asset label cross-checked on /movements (audit W2-explorer-1):
    a hostile token could set a sep0011 label impersonating a trusted asset
    (e.g. Circle USDC) on the public feed. The fallback now derives the SAC
    address and trusts the label only on a contract-id match (the check its
    sibling wasm_view already had); also fixes /v1/accounts/{g}/positions
    and the ?asset= filter.
  • WASM export-parser prealloc capped (audit W6-go-1): a section's
    attacker-influenced LEB128 count sized make() before any read, risking a
    multi-GB allocation on adversarial WASM; the prealloc hint is now bounded
    by the reader's remaining bytes (defensive — reachable only if an
    un-validated WASM path is ever added).

Changed

  • Cache-gate saturation now returns HTTP 503, not 500 (audit recon R3):
    a saturated detached-refresh gate on a cold /v1/accounts/{g} read was
    indistinguishable from a real error; a distinct ErrRefreshSaturated
    sentinel now maps to the retryable 503 (already declared on the route),
    while genuine scan failures stay 500.
  • Anomaly-freeze now releases reliably (audit W3-freeze-1 + W3-freeze-3):
    (1) the shared freeze marker's Clear is deferred until the last frozen
    window for a pair releases, so a short window auto-releasing no longer
    unfreezes still-frozen sibling windows; (2) once a freeze is active Phase 1
    stands down so the Phase 2 lifecycle (the sole auto-unfreeze authority) can
    release it — a freeze whose price settled at a residual level past
    FreezePct but statistically healthy could previously stay frozen forever.
  • Divergence warning debounced (audit W3-guards-2): comparing our
    shortest-window VWAP against an instantaneous reference spot produced false
    warnings on fast moves; the warning now fires only after the over-threshold
    condition persists a full VWAP-window horizon (default 5m), so a transient
    VWAP-vs-spot gap self-clears while a sustained genuine divergence still
    fires.

Fixed

  • decimalsguard retries the durable write (audit W3-guards-1): it latched
    fired before the DB upsert and never retried, so a transient write failure
    left a non-7-decimal token serving a 10^(7-decimals)-skewed price until
    restart. Split into an in-memory alarm latch (fire-once) and a persisted
    latch set only on a successful upsert (retries otherwise).
  • /v1/price first-ever bucket served as low-confidence (audit W6-fresh-1):
    the served-VWAP guard failed open (unbounded) on an empty baseline, so a
    thin pair's first minute — a lone manipulated print — was served
    stale=false. It is now flagged low-confidence/stale (value still served,
    never a blackout).
  • SAC-balance watched-asset keys canonicalized (audit W1-supply-1): the
    SAC-balance observer copied the asset_key verbatim, skipping the
    CanonicalizeWatchedClassic its sibling observers apply, so a dash-form SAC
    wrapper never matched the colon-form supply and its balances were
    under-reported (same class as the 2026-07-02 trustline miss). (Historical
    dash-form rows need a re-derive.)
  • One-side-zero SDEX fills no longer sink the trades batch (audit W1-defi-1):
    a fill with one leg rounded to 0 (decoder-kept, census-counted) violated the
    base>0 AND quote>0 CHECK, rolling back the whole all-or-nothing batch and
    firing a spurious insert_errors alert. They are now filtered before the
    batch (consistent with the completeness reconcile, which already excludes
    them); genuine Validate failures stay loud.
  • Duplicate contract events no longer served during the RMT merge window
    (audit W4-storage-1): ContractEventsRecent (the /v1/contracts/{id} feed)
    and EventsByTx read contract_events (a ReplacingMergeTree) without
    dedup, returning re-ingested events twice until a background merge. Added
    the package's per-shape idiom: LIMIT 1 BY <sort-tuple> and FINAL on the
    bounded read respectively.
  • Aggregator background workers panic-isolated (audit W4-cmd-1): the
    aggregator had zero panic recovery while the API isolates every worker, so
    one worker panic crash-looped the price pipeline. Added a shared
    internal/worker.Recover and wrapped all 14 workers + 3 inner fan-out
    goroutines.
  • Three dead alerts made fireable (audit W5-mon, W5-mon-3):
    supply_refresh_stalled + config_assertions_stale used timestamp()
    (scrape freshness, always ~0) and never fired — rewritten to real age /
    changes(); the ledgerstream both_missing page's counter was
    nil-in-prod (registry-gated) and is now registered unconditionally in
    obs. Each gained a promtool positive control.
  • Unbounded asset-detail response cache bounded (audit W6-perf-1): capped
    at 4096 with opportunistic expired-purge / oldest-eviction (a crawler...
Read more

Stellar Index v0.21.12

Choose a tag to compare

@github-actions github-actions released this 01 Aug 06:45

[v0.21.12] — 2026-08-01

Fixed

  • Projector: the adaptive window now also shrinks when the cycle budget
    dies in SINK writes, not only in the CH scan.
    The 2026-07-10 shrink
    fired only on stream-level timeouts; a window dense enough that the scan
    finishes but the per-event writes exhaust PerSourceTimeout retried the
    identical range forever — the aquarius reserves replay wedged 3.5 h at
    ledger 63,488,687 this way (every remaining write fast-failing on the
    dead cycle context, cursor held, no shrink). Budget-exhausted cycles
    with held transient rows now halve the window to the same
    MinBatchLimit floor so the retry converges.

Stellar Index v0.21.11

Choose a tag to compare

@github-actions github-actions released this 01 Aug 04:18

[v0.21.11] — 2026-08-01

Fixed

  • Aquarius zero-amount trades are recognized no-ops, not decode errors
    (internal/sources/aquarius/decode.go + dispatcher_adapter.go). The
    lake disproves the "every trade event has positive amounts" assumption:
    genuine dust swaps whose output rounds to zero — first exemplar ledger
    53,626,410, body (sold=2, bought=0, fee=0) from a registered pool —
    still emit the trade event, and refusing them as malformed made the
    ADR-0033 projection re-derive blind (40 of the 41
    undecodable-but-matched events on aquarius's first full-range
    completeness reconcile, 2026-08-01; all 40 are bought=0 dust swaps,
    ledgers 53.63M → 57.32M). canonical.Trade.Validate forbids
    non-positive amounts, so these can never be served trade rows: decode
    now succeeds with zero rows (new ErrZeroAmountTrade sentinel consumed
    by the adapter), the reconcile sees expected == served == 0, and
    negative amounts remain ErrMalformedPayload. Real-lake-bytes golden
    tests pin both the no-op and the negative refusal. Same classification
    pattern as redstone's empty write_prices batch.
  • Aquarius set_privileged_addrs decodes BOTH wire generations
    (internal/sources/aquarius/decode_admin.go). Contract-schema
    evolution: every lake event ≤ ledger 57,604,772 carries the 4-element
    body Vec[Address×3, Vec[Address]]; every event from 57,697,794
    (2025-06-25 WASM) onward carries 5 elements — the same four plus ONE
    trailing role Address. The decoder pinned arity==4 (the 2026-07-10
    audit sampled a v1 exemplar), leaving the canonical router's single
    v2 event (ledger 57,711,797) as the 41st blind event. The v2 arm
    decodes the trailing address into Attributes["addr_3"]; any other
    arity still fails closed. Real-lake-bytes golden test pins the v2
    shape. Post-deploy for both fixes: re-run
    compute-completeness -source aquarius (no replay needed — neither
    class changes served rows; expect blind 41 → 0).

Stellar Index v0.21.10

Choose a tag to compare

@github-actions github-actions released this 01 Aug 03:08

[v0.21.10] — 2026-08-01

Fixed

  • v0.21.9's deploy was blocked by the migration rollback-safety gate
    on 0124's CHECK rebuild (rule 9). The change is a pure enum widening
    — every value an old binary writes remains valid — now annotated
    migration-compat:ok inline. No code changes vs v0.21.9.

Stellar Index v0.21.9

Choose a tag to compare

@github-actions github-actions released this 01 Aug 03:00

[v0.21.9] — 2026-08-01

Fixed

  • Carried-claim invalidation gap: a projector-replay below the watermark
    now forces re-reconciliation
    (migration 0125 +
    internal/ops/ingest/projector.go + internal/ops/chops/compute_completeness.go).
    The daily completeness driver reconciles each source only from its prior
    watermark to tip and CARRIES the prior clean projection claim for the older
    prefix (projectionClaim rule 3 / INV-5) — sound only while the served tier
    below the watermark is immutable. A projector-replay rewind rewrites
    exactly that region, and nothing forced a re-check: the carried claim kept
    certifying rows the replay had changed, which is how the 07-30 cctp
    replay's 19,366 over-projected event_index-0 twin rows (62.27M–63.55M)
    escaped the verifier. projector-replay now records a per-source dirty
    window (projection_dirty_windows, widened LEAST/GREATEST on overlap)
    BEFORE rewinding the cursor — refusing to rewind if the record fails —
    projected-rebuild -write records the same window before its first write
    (same invalidation, bulk path), and
    compute-completeness fails closed on reading the table, extends its
    projection reconcile floor over any pending window regardless of -from,
    and clears the window only after a CLEAN projection verdict whose scope
    covered it (bounded delete, race-safe against a concurrent widening replay).
  • cctp + rozo reconciliation-catalogue genesis floors corrected to the
    lake-derived exact first events
    (internal/ops/chops/reconciliation_catalogue.go),
    mirroring the 07-30 protocols_registry.go fix: cctp 62_403_000 →
    62_146_641 (410 real served rows sat permanently BELOW the verify floor,
    structurally out of every verdict), rozo 62_403_000 → 60_829_397. All other
    catalogue entries audited against the registry — no further drift.
    (DefaultGapDetectorTargets in per_source_gaps.go still carries the old
    floors — supporting signal only, owned by another change.)
  • cctp bespoke reader post-twin-deletion cleanup
    (internal/storage/timescale/protocol_bespoke_cctp.go). The flow CTEs
    collapsed per (tx_hash, op_index) with max(amount) — a workaround for the
    legacy event_index-0 twin rows deleted from r1 on 2026-07-31 — which would
    silently HALVE a future genuine batched double-transfer in one op (the 0112
    class: same-op same-type groups are proven on the wire by admin events).
    Value-carrying CTEs (deposit_for_burn / mint_and_withdraw) now read raw
    per-event rows; the message_received CTE keeps its one-body-per-op group
    (join semantics — a second body row would fan out every joined mint row),
    and the mint_and_forward restatement exclusion (a real semantic rule, not
    twin dedup) is untouched. Also: the all-time "Unique recipients" KPI now
    counts DISTINCT recipient addresses as labelled — it previously counted
    recipient-bearing ops.
  • Explorer honesty wave — cold-audit findings across web/explorer/src.
    Eight fabrication/robustness classes fixed in the explorer only (no
    API/server changes): (1) /lending pools table: an API failure now
    renders an explicit unavailable state instead of the fabricated
    "No Blend pools have emitted auction events yet" empty-state, and
    PoolRealStats (hoisted to module scope — it was re-created every
    parent render) computes TVL/utilization/weighted APRs over PRICED
    reserves only with the basis labelled, instead of coercing absent
    supplied_usd/borrowed_usd/APRs to 0 inside sums and weighted
    averages; (2) partial trailing daily bucket: a new shared
    @/lib/series helper (dropPartialTrailingDay) drops today's
    accumulating UTC bucket from every daily-grain chart mapper
    (BespokeSection standalone + grouped panels, BridgeShowcase lines,
    ProtocolView activity, SourceVolumeHistory, SdexVolumeSection;
    hourly 24h series keep the live day) and TimeSeriesChart headlines
    "Latest" from the last COMPLETE point; (3) /network
    protocol-upgrade markers now require BOTH sides of a step to be a
    real (>0) version — lake sentinel rows minted phantom "protocol v0/vN"
    markers (the field is a uint32 without omitempty, so null-guards never
    fired); (4) /anomalies freeze durations render "—" instead of
    "NaNd" for missing/garbage timestamps; (5) holder balances render via
    a BigInt-divide-first formatBaseUnits/scaleBaseUnits
    (@/lib/format, ADR-0003) instead of Number() on >2^53
    smallest-unit strings (absent → "—", not NaN); SupplyFlowsBar shares
    the same scaler; (6) toChartNumber returns null — the point is
    dropped — for non-numeric series values (previously plotted as
    fabricated 0s) and for compact-suffixed figures ("1.2M" previously
    mis-scaled 10^6× to 1.2); (7) non-parsable point dates are dropped
    before setData instead of the epoch-0 fallback that plotted 1970
    points and could crash lightweight-charts on duplicate time:0;
    (8) small honesty items: SdexVolumeSection renders a served 1-point
    series as the point ("insufficient history"), not a false
    "unavailable"; the anomalies ReasonHeatmap derives its day columns
    from the served data's date range, not the client clock; a bespoke
    block's notes no longer caption other windows' data after a failed
    window refetch.
  • Daily-grain protocol/bespoke series exclude the current (partial)
    UTC day server-side.
    Every bespoke daily series (DEX activity/
    traders/top-pairs, lending sides/backstop/auctions/per-pool, credit,
    oracle, defindex, CCTP flows + cumulative net inflow, rozo) and the
    protocol activity series/breakdown included today's still-accumulating
    bucket with no marker — the UXP-16 "phantom cliff" class, previously
    fixed only on /network. Daily-grain SQL now bounds at the day start
    (shared completeDaysOnly fragment; ClickHouse readers bound
    close_time/day likewise — the event breakdown shares the bound so
    sum(event_breakdown) == events_total still reconciles). The 24h
    window's hourly grain keeps its live edge.
  • Three small honesty guards (audit 2026-07-31). (a) The SDEX
    order-book reader no longer silently skips a non-removed offer change
    whose XDR fails to decode — each skip freezes that key's prior served
    state, so it now logs a warn line and increments the new
    stellarindex_sdex_orderbook_undecodable_offers_total counter.
    (b) mapFreezeReason no longer defaults unrecognized automated
    freeze decisions to manual (the operator-initiated reason) — they
    record as the new other reason (migration 0124 extends the CHECK;
    spec 1.18.0 adds the enum value).
    (c) A soroswap swap with all FOUR amounts non-zero satisfied both
    direction arms and silently decoded as 0→1, dropping the 1→0 leg —
    now refused whole as ErrAmbiguousSwapDirection (wraps
    ErrNonDirectionalSwap, so it stays a recognized no-op for ADR-0033
    completeness).
  • Blend bespoke block no longer sums token amounts across assets.
    The headline "Net supplied/borrowed" KPIs and the per-pool table's
    cross-asset sums + "Util %" ratio added token_amount across tokens
    with different decimals — a meaningless number under an authoritative
    label (the sibling count-first suite in bespoke_lending.go documents
    exactly why). KPIs are now count-first (active users, flash loans,
    alongside the existing side-event/pool counts), the per-pool table
    serves supply/borrow-side event + user counts, and amount sums survive
    only where scoped to a single asset (the per-asset table).
  • Stale "soroswap has no taker" claims removed from the wire. The
    soroswap decoder captures the SwapEvent to recipient since
    2026-07-30 (100% taker coverage on new rows), but the
    /v1/accounts/{g}/trades scope note still excluded soroswap and the
    DEX bespoke block carried a hard-coded "0% taker coverage" note. The
    scope note now includes soroswap (with the pre-2026-07-30 caveat) and
    the bespoke omission note is data-driven — emitted only when the
    served window observably has zero taker-stamped trades.
  • usd_volume no longer hard-codes the off-chain 1e8 amount scale.
    The FX pollers stamp trades at 1e6 (registry AmountDecimals: 6),
    but both the insert-path stamper (tradeUSDVolume/tradeUSDVolumeViaFX)
    and ClassifyUSDVolumeTier (the verify-usd-volume checker, which
    would have certified the error) divided by a hard-coded 1e8 — a latent
    100× under-valuation the moment FX trades re-enable. Both now consult
    the source's registered AmountScaleDecimals (CS-040).
  • Explorer detached SWR refreshes are now globally bounded. The
    stale-while-revalidate caches (account state, asset holders, contracts
    directory, contract detail) single-flighted per key but were unbounded
    ACROSS keys — and the key space is attacker-chosen on unauthenticated
    routes, so churning fabricated G-/C-addresses queued one detached
    multi-minute lake scan per key onto the 8-connection serving pool. A
    shared semaphore (clickhouse.RefreshGate, limit 4 — half the pool)
    now bounds all detached refreshes; on saturation the refresh is
    skipped, never queued: stale entries keep serving, cold keys 503 with
    the retryable timeout contract.
  • /v1/assets/{asset_id}/holders normalizes the asset spelling before
    the cache and the lake query.
    ParseAsset admits the Horizon
    CODE:ISSUER spelling, but the raw request string was used as both
    the SWR cache key and the query asset — the colon spelling scanned
    for a value the lake never stores and cached an authoritative-looking
    empty board under a duplicate key. Both spellings now share one
    canonical (CODE-ISSUER) cache entry and query.
  • Protocol-detail cache: a degraded fast-failing rebuild can no longer
    displace a good cached entry.
    The keep-old-entry guard only checked
    the rebuild context's timeout, so a build that failed FAST (ClickHouse
    down → every enrich errors in ms) replaced the previously-good view
    with an analytics-empty one stamped fresh — one ...
Read more

Stellar Index v0.21.8

Choose a tag to compare

@github-actions github-actions released this 31 Jul 20:50

[v0.21.8] — 2026-07-31

Added

  • Visuals wave 3 — /network chain economics, /divergences Δ% history,
    /anomalies visual pack (spec 1.17.0).
    Three M-effort items off the
    visuals survey, each a small backend addition + charts over it:
    (1) /v1/network/throughput buckets now carry end-of-day chain state
    off each day's last ledger — fee_pool + total_coins (stroop
    strings, ADR-0003) and protocol_version — added as argMax
    aggregates inside the SAME bounded partition-pruned
    stellar.ledgers scan; /network charts daily fee burn (delta of
    consecutive complete days), a total-XLM line, and protocol-upgrade
    step markers on the throughput chart (via a new optional
    markers/priceLines extension to the shared LineChart).
    (2) New GET /v1/divergence/series?pair=&reference=&days= — the Δ%
    history for one (pair, reference) from divergence_observations,
    the same (asset, quote, reference, observed_at) index scan as the
    board minus the DISTINCT ON, last-observation-per-bucket
    downsampled (5m/30m/2h for the whitelisted 1/7/30-day windows, ≤
    ~360 points; firing is bucket-wide any-breach) with the
    operator's real divergence.threshold_pct on the wire;
    /divergences plots the selected board row's history with dashed
    ±threshold lines plus a signed |Δ%| bar list of the current board.
    (3) /v1/anomalies?include=daily adds a per-(UTC day, reason)
    freeze tally (daily: null = not requested, [] = zero freezes
    — degraded ≠ zero) via a new chunk-pruned FreezeDailyReasonCounts
    reader; /anomalies renders a reason-composition donut, a
    freeze-duration histogram over the recovered events served, the
    previously-fetched-never-rendered detail.deviation_pct as a
    table column, and a day×reason calendar heatmap (new token-driven
    ReasonHeatmap, sequential single-hue). API minor → 1.17.0; all
    three spec artifacts regenerated.
  • Explorer visuals — quick-win wave over data already on the wire.
    Five under-visualized surfaces now chart what their pages already
    fetch, reusing the shared chart system plus three new bar
    primitives (web/explorer/src/components/charts/Bars.tsx:
    HBarList, PairedBars, DivergingColumns): (1) /dexes/[source]
    renders the full /v1/protocols/{name} bespoke DEX suite (KPI
    cards, trades + unique-traders series, top-pairs multi-line,
    volume-by-pair donut, window pills) via a shared BespokeSection
    with the standalone USD-volume series de-duplicated against the
    page's own 90d volume panel; (2) /protocols (and every locked
    category page over ProtocolsIndex) charts per-protocol TVL as a
    bar list with the priced/unpriced honesty split — "≥" prefix +
    hatched tail + pools_priced/pools_total on lower-bound rows, the
    server's basis as the tooltip, and NO bar for protocols without a
    TVL derivation; (3) /liquidity-pools expanded rows replace the
    nested depth tables with per-direction slippage/depth bars (one
    chart per direction — different units never share an axis) and a
    reserve-composition donut valued at the pool's own mid price;
    (4) /accounts/[g] gains an account-character pack: ops-by-type
    bars, cumulative-USD trade area + volume-by-venue donut over the
    loaded priced trades, daily in/out diverging movement counts
    (counts, not cross-asset amount sums), and inbound-vs-outbound
    bridge bars; (5) /lending/[pool] reserves get real
    supplied-vs-borrowed, fixed-scale utilization (near-cap flagged),
    and supply-vs-borrow APR bars replacing the 16px CSS strips, the
    /lending index gets an all-time liquidation-auctions-by-pool bar,
    and /yield swaps its static CategoryHub for the live
    ProtocolsIndex lockedCategory="yield" directory (the /bridges
    pattern).
  • Explorer flagship visuals: SDEX cumulative depth chart + asset
    visual pack.
    /markets/[pair] and /sdex gain a mirrored
    step-area cumulative depth chart with a best-bid/ask + mid/spread
    stat strip over the served /v1/sdex/orderbook levels (new
    DepthChart component; a crossed snapshot renders an explicit
    warning instead of a negative spread, and /sdex — previously a
    static brochure — now shows the live book for four headline classic
    pairs plus the daily USD-volume series + KPIs from
    /v1/protocols/sdex). /assets/[slug] gains the asset visual
    pack: the volume-by-source donut (/v1/markets/sources?asset=,
    handler + component both existed unwired), per-market 24h volume
    sparklines in the Markets tab (?include=sparkline), a live
    1h/24h/7d/30d change strip + streak + honestly-labelled
    "low since indexed" chip from the previously consumer-less
    /v1/changes/coin/{id} endpoint (absent windows render "—"), a
    holders concentration donut explicitly scoped to the served top-100
    rows, and mint/burn/clawback comparison bars in the Supply tab.
    Also fixes the /markets/[pair] build-time /v1/chart fetch
    discarding ~95% of its series (now rendered as a 24h VWAP sparkline
    • summed 24h volume stat) and the OrderBookPanel test's cross-test
      mock leak.
  • Protocol analytics are warm by construction, and degradation is
    now explicit on the wire (spec 1.16.0).
    Under replay load every
    on-demand /v1/protocols/{name} bespoke build died at the request
    deadline ("protocol bespoke build failed", 2026-07-31), pages lost
    their visual suites, and the omitted block was indistinguishable
    from a protocol with no data. Three changes: (1) a dedicated API
    prewarm worker sweeps ALL registry protocols × ?days= windows
    (one build at a time, 2 s pause, re-swept 10 min after each sweep
    ends — sized from r1 timings under replay: soroswap 90d bespoke
    ~1.9 s, cctp ~0.4 s) so every protocol page + window is built
    before anyone asks, with the detail cache now stale-serving past
    its (raised, 20 min) TTL — a previously-built view is served with
    flags.stale while ONE detached rebuild (own 90 s budget, never a
    request deadline) refreshes it, and a failed/timed-out rebuild
    keeps the old entry instead of blanking the block; (2) a new
    analytics: {status: ok|stale|unavailable, as_of} object on the
    detail view so clients can tell fresh / stale-but-served / degraded
    apart from real zeros — the explorer renders an explicit
    "temporarily unavailable" hint from the status instead of inferring
    from field absence; (3) the /v1/operations op-type panel joins
    the API's 5-minute prewarm loop (it had SWR + detached refresh but
    nothing warmed it at boot, so the first directory hit after every
    deploy rendered without it; the aggregate measured ~70 ms warm on
    r1). New paired metrics
    stellarindex_protocol_detail_refresh_{total,duration_seconds}
    (ok|degraded|timeout) cover the prewarm sweep and request-kicked
    revalidations; /v1/assets/{id}/holders was audited as already
    stale-serving (never hard-misses a previously-seen asset).

Fixed

  • stellarindex_dex_trade_unit_ratio_detected alerts on the FRACTION
    of a source's flow (>50% AND >25 count), not absolute count
    — a busy
    wrapper-redemption morning (yUSDC/USDC, yETH/ETH parity pairs) tripped
    the old threshold at ~2% of sdex flow; the decode-bug fingerprint the
    alert exists for makes EVERY trade 1:1. Both rule trees + runbook.
  • soroswap: non-directional swaps are recognized no-ops, not decode
    errors — closes the last soroswap undecodable-but-matched blind
    ledger (57,403,300).
    The lake disproves the "a well-formed swap
    has exactly one in/out pair non-zero" assumption: pair.swap() is
    directly invokable (Uniswap-v2-style) and accepts any argument
    combination keeping K non-decreasing, so a swap can settle with all
    value movement confined to ONE token side (the real case: pair
    CAM7DY…, tx be7028b9…, amount_1_in=265, amount_1_out=70, both
    token0 amounts zero). That is a real, recognized on-chain event but
    NOT a trade — no (base, quote, price) exists — so decodeSwap now
    surfaces ErrNonDirectionalSwap and the Decoder projects zero rows
    with nil error (same recognized-no-op contract as redstone's empty
    write_prices batches), flipping the blind ledger to verifiable
    expected-zero under the ADR-0033 honest-blind accounting. Golden
    test pins the real lake bytes. Alongside, classify() now
    enumerates the pair WASM's LP-share SEP-41 token events
    (transfer/mint/burn/approve, lake-verified: 1,622/907/333/0
    across all 230 registered pairs) as EventPairToken — classified
    per the EVERY-event principle but deliberately NOT claimed by
    Matches(): they are the sep41 domain, and the dispatcher is
    first-match-wins, so claiming them would silently swallow any
    LP-share token later added to watched_sep41_contracts.
  • /v1/sdex/orderbook served CROSSED books — zombie offers dead
    since 2021 quarantined out and lake-verified.
    The live XLM/USDC
    book quoted best bid 0.4327 vs best ask 0.1722 and carried ~46k
    bids / ~40k asks where the real pair book is a few hundred offers.
    Root cause: historical entry-change backfill wrote
    intra_ledger_seq = 0 on every row, so all same-ledger changes to
    one offer key TIE on ledger_entries_current's ReplacingMergeTree
    version and an arbitrary row survives the merge — an offer updated
    then fully consumed within one ledger can survive as updated
    forever (founding zombies: offers 845025288 / 845025425 / 845025699
    / 845028065, consumed at ledger 38,224,736± on 2021-11-10, still
    "live" on 2026-07-31; their losing removed rows are physically
    gone from ledger_entries_current but intact in
    ledger_entry_changes). Class fix in the book maintainer: loaded
    offers whose winning version carries intra 0 (the tie-ambiguous
    class) are quarantined out of the served book and drained at
    2,500/tick by a new batched, partition-pruned
    OfferRemovedAt(ledger, key) probe against the change stream —
    proven-dead offers are dropped for good (offer LedgerKeys are never
    reused, so a same...
Read more

Stellar Index v0.21.7

Choose a tag to compare

@github-actions github-actions released this 30 Jul 22:32

[v0.21.7] — 2026-07-30

Fixed

  • Soroswap trades now carry their trader (taker) — and a replay
    backfills all history.
    SwapEvent.to (the swap recipient) is
    on-chain in every soroswap swap since genesis, but the decoder
    dropped it — soroswap was the one venue with 0% trades.taker
    coverage (every sibling: 100%), which excluded soroswap from the
    per-address trades view and its page's trader metrics. Captured
    best-effort (an unreadable recipient never drops the trade); the
    trades upsert already carries taker = EXCLUDED.taker, so a
    projector-replay -source soroswap retro-fills every historical
    trade.

Added

  • Every protocol category now ships a full visual suite (the
    "primary point for protocols to show their stats" directive) —
    window-reactive (24h/7d/30d/90d pills now on EVERY protocol page,
    hourly buckets at 24h): Lending/Blend — activity-by-pool +
    event-kind donuts, supply-vs-borrow / backstop / auction series,
    per-pool top-5 lines, flash-loan table (counts over mixed-asset
    sums by policy); SoroCredit — positions + settlement series;
    Yield/DeFindex — vault deposits-vs-withdrawals lines, flows-by-
    vault donut, per-vault table (single-asset amounts only — the 2
    multi-asset vaults honestly render "—"); Oracles (reflector ×3,
    redstone, band) — update-cadence series, per-feed top-5 lines,
    updates-by-feed donut, feeds table, freshness + median-interval
    KPIs (counts and timestamps only — no price restatement). All new
    SQL validated read-only on r1 with timings pinned in comments.

Fixed

  • CCTP flow volumes no longer double-count: transfers are deduplicated
    and mint_and_forward is excluded from sums.
    Ground-truthed on r1
    (2026-07-30, all 52,205 cctp_events rows): (1) rows decoded before
    event_index landed (migration 0112) coexist with their re-derived
    copies — 12,139 (tx, op, type) groups hold a legacy event_index-0 row
    plus the true-index row with identical amounts — so every flow query
    now collapses to one row per (tx_hash, op_index) transfer; (2) every
    mint_and_forward op also emits mint_and_withdraw for the SAME
    funds (0 forward-only ops), with the forward amount exactly 10× on
    all 13,651 pairs — it restates the canonical 6-decimal value at the
    local 7-decimal scale, so summing both inflated a forwarded transfer
    11×. Inbound sums now use deduplicated mint_and_withdraw only.
    Shape tests pin both rules. (The stale legacy rows themselves remain
    in the table; queries are dedupe-safe regardless — an operator
    cleanup can drop them independently.)
  • Redstone subset attribution is order-preserving — the residual 170
    blind ledgers resolve.
    The v0.21.5 full verify cut the blind class
    1,626 → 170; the residue was prices matching TWO candidates' medians
    simultaneously, which the unordered rule refused. The adapter builds
    updated_feeds in a single pass over feed_ids (the same fact that
    makes the equal-arity case a positional zip), so the surviving
    entries are an ordered SUBSEQUENCE — attribution is now a
    DP-counted subsequence alignment that attributes only a UNIQUE
    complete alignment. Order can only disambiguate, never misattribute
    (any true assignment is order-preserving by construction); genuinely
    ambiguous events still refuse. Golden-pinned on the real
    ledger-60104689 event (7 feeds → 5 survivors with a shared-median
    collision). Completeness flips at the post-deploy replay + verify.

Added

  • Explorer: the 24h/7d/30d/90d analytics window pills now live at
    BespokeSection level, so EVERY protocol category's bespoke block —
    KPIs included — is window-reactive.
    Previously only the bridge
    showcase owned pills (and its KPIs stayed pinned to 90d while its
    charts switched). Switching a pill refetches
    /v1/protocols/{name}?days=N and swaps the whole block;
    BridgeShowcase now consumes the section-level window as props (its
    all-time cumulative headline stays pinned to the initial fetch), and
    non-bridge categories gain generic rendering for " · "
    series — folded into one palette-colored multi-line panel per group
    (the DEX "Top pairs · XLM/USDC" lines) instead of one panel per
    series — plus window-labelled donuts/tables and honest
    loading/empty/error states. TimeSeriesChart accepts the hourly
    "YYYY-MM-DDTHH:MM" 24h-grain point dates with a time-of-day axis.
    Vitest covers the lift for both paths: all categories refetch, the
    bridge suite is unaffected.
  • DEX/AMM protocol pages get the full visual analytics suite
    (soroswap / aquarius / phoenix / comet / sdex; no spec change — every
    shape exists in 1.15.0). The bespoke block now serves: a "Volume by
    pair" composition (top 8 pairs by USD volume + a SQL-side "Others"
    fold, so SDEX's ~99k window pairs never cross the wire); USD-volume,
    trade-count and unique-trader time-series plus top-5-pairs-by-volume
    multi-series ("Top pairs · XLM/USDC", …), hourly-grained at the 24h
    window via the real-time source_volume_1h rollup; avg-trade-size
    (exact NUMERIC division by the PRICED trade count) and unique-trader
    KPIs; honest "since 2026-03-18" lifetime totals labelled by the daily
    rollup's materialization floor (never "all-time" — raw SDEX history
    reaches 2018); and a window-scoped "Largest trades" top-10 table with
    tx links. Pair labels resolve token contracts through the verified-
    currency catalogue's derived SAC addresses (XLM/USDC, …); unverified
    tokens show truncated contract ids, never guessed symbols. Every
    query was validated read-only on r1 (2026-07-30): two deliberate
    SQL shapes keep SDEX's 90d window sub-second (hash-agg pair count
    0.67s vs 6.0s naive; direct CAGG join for top-5 series 0.87s vs
    19.8s), and raw-trades-derived surfaces (traders / avg size / largest
    trades) are gated to ≤7d windows on SDEX with the omission Noted on
    the block — soroswap's trader metrics are omitted honestly too (its
    decoder captures no taker). Shape tests pin the grain, the window
    bounds, the ADR-0003 NUMERIC rules and both perf-critical shapes.
  • CCTP SDF-showcase analytics suite: where USDC comes from and where
    it goes
    (spec 1.15.0, additive). /v1/protocols/cctp's bespoke
    block gains a generic breakdowns field (named composition datasets
    for donut rendering) carrying "Inflows by source chain" / "Outflows by
    destination chain"; per-chain top-5 inbound + outbound time-series
    ("Inbound · Base", …); an all-time daily "Cumulative net inflow"
    series; all-time inbound/outbound/net KPIs plus unique depositor and
    recipient counts; and a window-scoped "Largest transfers" table.
    Source chains are attributed from the burn-side USDC token embedded in
    each transfer's CCTP BurnMessage; destinations via Circle's public
    domain registry — both maps verified against Circle's primary docs
    (USDC contract addresses + domain list, 2026-07-30), including the
    32-byte-address tails for Solana / Aptos / Starknet. Unrecognised
    tokens/domains are labelled "Unverified (0x…)" / "Domain N" — never
    guessed. The explorer's CCTP page renders the suite: side-by-side
    source/destination donuts, per-chain multi-line charts, the headline
    cumulative net-inflow chart, and largest transfers linking to
    /transactions/{hash}, all reactive to the existing window pills.
  • /v1/protocols/{name} accepts ?days= (1/7/30/90, default 90) to
    window the bespoke analytics block
    (spec 1.14.0, additive). Anything
    outside the whitelist is a 400 problem+json (never a silent clamp);
    the detail TTL cache now keys on (protocol, window) so window switches
    can't serve another window's cached numbers. At days=1 the bridge
    flow series bucket HOURLY (a daily bucket collapses 24h to one point);
    series names dropped their grain prefix and are now window-stable —
    cctp "Inbound (USDC)" / "Outbound (USDC)", rozo "Settled volume
    (USDC)". Lake-analytics fields keep their fixed 90-day lookback. The
    explorer's bridge pages (cctp/rozo) render the flows as one combined
    inbound/outbound line chart with 24h/7d/30d/90d window pills.
  • Bridge pages show USDC-denominated in/out FLOWS, not just counts.
    /v1/protocols/cctp gains directional volumes (inbound = mints from
    remote burns, outbound = deposit_for_burn), daily in/out series, and
    per-domain volumes; /v1/protocols/rozo gains its first bespoke
    block (settled payment volume + daily series; admin flush sweeps
    excluded to avoid double-counting). Amount scales ground-truthed
    on-chain against the USDC SAC leg of real transactions — and they
    DIFFER: CCTP events are canonical 6-decimal, Rozo events are local
    7-decimal SAC stroops (the documented external-scaling trap class).
    Exact NUMERIC division end-to-end. Registry genesis labels corrected
    to lake-derived exact first events (cctp 62,403,000→62,146,641 —
    the re-projected head; rozo 62,403,000→60,829,397).

Fixed

  • Unknown tx hashes no longer trigger a 10.5B-row scan.
    TransactionByHash fell through to the full-table bloom probe on
    every index miss — written when tx_hash_index was partially
    backfilled. The index now covers genesis→tip (20.78B rows from
    ledger 3, verified on r1), so an index miss is an authoritative
    not-found; the scan remains only for index-path errors and
    index/base inconsistencies (both pinned by tests). Closes the last
    serving-path instance of the non-sort-key filter class — and an
    unauthenticated multi-second-per-request lever.

Fixed

  • Account history (tx + ops) sourced arms are primary-index reads —
    the 6-second bloom probe is gone.
    /v1/accounts/{g}/transactions
    and /operations resolved "transactions this account SOURCED" via
    the source_account bloom skip-index over the full 23B/34B-row
    tables — granule-pruned but scan-shaped (measured on r1: 6.17 s for
    the sourced arm vs 0.056 s for the participant arm on the SAME
    328-op account), which is what pushed heavy...
Read more

Stellar Index v0.21.6

Choose a tag to compare

@github-actions github-actions released this 30 Jul 11:57

[v0.21.6] — 2026-07-30

Fixed

  • Account trustline + offer reads are primary-index range reads — 75×
    faster.
    /v1/accounts/{g}'s trustline and offer queries rode the
    account_id bloom skip-index (scan-shaped over the 43.6M-row
    current-state table; the whale account's trustline read alone measured
    5.18 s). An account's trustline/offer LedgerKeys share a fixed
    key_xdr byte prefix and sit contiguous in the table's
    (entry_type, key_xdr) sort order, so both reads now prune by a
    52-char base64 PK prefix (accountEntryKeyPrefix; exact
    account_id equality closes the prefix's one-byte residual) —
    0.069 s measured on the same whale. Account detail is now
    interactive for every account, including the top wealth-ranked
    ones; the stale-serving layer below remains as the belt-and-braces
    for pathological cases.
  • GET /v1/accounts/{g} whale accounts no longer 503 outside the
    30-second post-fill window.
    The v0.21.5 detached account-state fill
    fixed the cache-never-fills problem, but the 30s AccountStateCacheTTL
    still treated an expired entry as a hard miss — so an account whose
    scan outruns the request budget was warm for only ~30s after each
    fill and cold the rest of the time (the final route-sweep holdout).
    An expired entry is now SERVED while one detached refresh runs —
    the same stale-serving contract the wealth cache adopted 2026-07-29 —
    and the stale serve is surfaced honestly on the envelope's
    flags.stale (AccountStateCached now returns the staleness so the
    handler can pair the serve with the flag).