Conversation
The map is regenerated per session and is not shared state, so it does not belong in history.
Reserves are read from the pool-plane ContractData entry rather than reconstructed from event arithmetic; the arithmetic route failed its oracle test (6 of 49 cases exact) and is kept only as evidence. Refs #405
…type Two of three adversarial challenges were refuted by re-measuring: the router registries have zero orphan pools, and the concentrated share is an adoption curve rather than a sampling artefact. The router counts were low and are corrected. Also corrects the deposit_liquidity data vector, recorded here backwards: shares is element 0, not the last element. A parser reading it from the wrong end would fail the share-token derivation everywhere. Refs #405
…_update List returns positions, header counts holders; constant pools are the degenerate one-position case. Open positions only; raw L until the token-amount conversion passes an on-chain check. Refs #405
One decoder, no stitch at 57573730. update_reserves demoted to a monitored cross-check; volume stays event-sourced. Grounded in the gap pilot: 80/80 router-A pools had PoolData in their first trade ledger, and router B writes to its own plane, so discovery keys on the entry shape, never a hard-coded address. Refs #405
…overy Discovery keys on the event shape, never on a contract address: ten contracts emit add_pool and the vendor documents one, so an address list silently drops about 6% of live pools. Measured on all history, 497 of 497 events match the decoder's structural checks. Anchored on the vendor's own emitting code, with the limits of that anchor recorded: their repository is now unreachable, so the snippet comes from an archived capture, and the deployed contract's spec — which carries no event definitions at all — corroborates only the parts it covers. The third data field is the router's pool lookup key, not a WASM hash; naming it after a hash would have invited a join that matches the wrong contract. Pool type keeps both a normalised and a raw spelling because three vocabularies for it exist at once, and an uncatalogued shape yields None rather than a plausible wrong value. Refs #405
… real data An adversarial review and four production measurements found the decoder asserting things the chain contradicts. init_args was parsed into a numeric list, which flattened three different vocabularies and, worse, dropped any element it could not parse and shifted the rest left - turning a three-argument stable pool into the exact shape of a two-argument concentrated one. Kept as raw text now: position 1 is a tick spacing in one pool type and an amplification factor in another, and u128 does not fit i64. The salt was documented as identifying a pool. It does not: 497 registrations carry 81 distinct salts, and 47 slots were registered more than once - one of them seven times, each naming a different pool contract, because a slot is re-pointed when a pool is redeployed. Legs were documented as two or three. Four-leg pools exist. Option collapsed 'this is another event' with 'this is a registration I could not read', so the counting the docs asked for was unwritable. Now a typed rejection reason. Two corpus tests replace assertion with measurement. Every one of the 497 registrations decodes through the compiled decoder, not through a query imitating it; and a cross-section of 307 real events over 30 signatures - including the family's own trade, update_reserves and pool_state - yields not one false positive. Refs #405
Nine columns with DEFAULTs turn liquidity_pools into the registry for every pool; classic rows read unchanged as pool_kind=0. The pair-shaped asset_a_*/asset_b_* columns are marked legacy in place - 3- and 4-leg stable pools exist and do not fit a pair - and stay until the ~612 pair-shaped call sites migrate to legs. Refs #405
…n prod Depth-first cut: the earlier wide mirror never reached the database; the ALTER that did carries exactly pool_kind, legs, deployment_id, pool_type_raw, share_token_id. Registration provenance (salt, raw init_args) is not materialised - the add_pool event sits complete in soroban_events; extract on demand, never copy. Refs #405
Discovery follows the house detector idiom: the semantic decode lives in xdr_parser::pool_router::detect_pool_registrations (shaped like detect_nft_events), and the liquidity_pools staging section only maps registrations to rows - the event serialization loop stays free of interpretation. An unreadable registration is warned about with its reject reason, never dropped silently. A registration records only what the event itself says: share_token_id stays 0 (the share token is unknowable until the first deposit, T6) and no venue label is stored anywhere - labels resolve from deployment_id at read time. pool_id carries the raw 32-byte contract payload (reversible to the C... address), while legs and deployment_id carry the Int64 surrogates that join the rest of the schema. Adds stellar-strkey to db-clickhouse for the payload decode. Refs #405
An unparseable init_args[0] used to become a plausible fee of 0 - the misleading-fallback class. Every mainnet shape carries a u32 there (497/497, pinned by the corpus test), so a failure is a vocabulary nobody has seen: the registration is now refused with tracing::error and a named reason, the raw event still lands, and the ledger does not fail. Karol's call: error, not warn-and-default. Also ships the one-off backfill generator (same ids/decoder as the live writer, so backfilled and live rows are identical by construction) and records in the task file why filling share_token_id later must go through a side table rather than a partial RMT row. Refs #405
A single-use tool is not a maintained surface. The final-phase catch-up resurrects it verbatim from history (pointer recorded in the task file), so nothing is lost and nothing rots in src/bin meanwhile. Refs #405
…e-leg-and-soroban-amm-completeness
One id-IN resolver in the 0344/0345 canon shape: sorted+deduped id list formatted into the query, every dimension subquery bounded by it, and the soroban_contracts hop picked with LIMIT 1 BY id - the strkey is immutable across RMT versions. Two arms by construction: a leg is always a token contract, so a classic asset appears only through its deployed SAC (asset_sac facet, 96% of legs) and a bespoke token through its own type-3 row. 1005/1005 leg occurrences resolve on production. Decimals are the protocol constant 7 for the classic family and on-chain metadata for bespoke tokens - an unpublished scale stays None and renders as an explicit marker, never a guessed default (an 18-decimal leg with late metadata is live in a stable pool). Bespoke legs also carry their SEP-41 symbol/name, resolved exactly as the assets page does. Also corrects a lying comment: legs hold token-contract surrogates, which equal assets.id only for bespoke tokens - the doc claimed assets.id in general while 96% of legs are SACs whose classic asset has a different id. Refs #405
… 15) The relation lives in a SIDE table (pool_share_tokens), the asset_sac pattern: the deposit path knows only (pool, token), and a partial row in the whole-row-replace registry would wipe legs, type and deployment on merge. Versioned by sighting ledger, so the 13 measured share-token migrations converge on the newest - matching share_id() on chain. The detector implements the T6 rule and is proven the way add_pool was: 85,586 real deposit transactions through the COMPILED code, 16/16 exact against on-chain share_id(), and 394 resolved pools reconciled independently via chq as precisely the fungible-pools-with-deposits count (328 constant + 63 stable + 3 elastic; 37 concentrated yield nothing by construction; 66 deposit-less pools are unreachable by events at all). Anchored at source where a source exists: the vendor's own capture spells the deposit body [stake_amount, amounts...] - shares first. Task notes record the fundamental correction this commit anticipates: the chain stores TokenShare as instance STATE in the registration tx, so step 7 swaps the primary source to state and this detector becomes the monitored cross-check (update_reserves' fate in T4). Refs #405
The ContractInstance arm serialized only the executable and silently dropped the instance's storage map. Nothing read that section before; pool state does — TokenShare (the share-token relation's primary source), Reserve0/1 (the ONLY reserve source for concentrated pools) and the Plane/Router shape gate all live in instance storage. Same defect class that hid contract metadata (chain-refuted "off-ledger" verdict in 0156/0283/0297).
Two on-chain layouts feed one reserve source (T4): fungible pools write PoolData[pool] on the deployment's shared plane (vector kept VERBATIM — per-tick tails exist, readers slice by leg count); concentrated pools write Reserve0/Reserve1 on their own instance, where TokenShare (share-token relation) and the Plane/Router shape gate also live. Extraction mirrors the token-balance extractors: state images from created/updated/restored changes only. Fixtures are verbatim house-dialect JSON from mainnet ledgers 63,893,403 and 64,134,576.
Env-gated like the other prod-harvest corpora (skip cleanly in CI). pool_state_real_ledger runs one raw registration ledger end to end; pool_state_real_corpus sweeps 90 raw ledgers (registration eras, hot event eras, pre-family dead era) through the COMPILED extractors with the invariant: PoolData-shaped-but-unparseable count is zero — a vendor payload change surfaces here as a failure, not as pools quietly losing state.
New fact table at the chain's grain (pool, ledger, tx, change_index) with reserves Array(Int128) VERBATIM. The full key matters: (pool, ledger) alone collapses 23.5% of rows (12 writes/ledger measured). Named without a family prefix on purpose — it is the target state-fact shape; classic history joins INTO it if the snapshot models ever unify (ADR 0058). Staging arms: plane PoolData rows for fungible pools; instance rows for concentrated (their reserves ride Reserve0/1 on the instance — plane only at registration; anti-test discovery). The instance's TokenShare key becomes the PRIMARY pool_share_tokens writer; the event-rule detector arm is removed from staging and survives only as a cross-check. Unparseable reserves refuse the row loudly, never a plausible default. persist_ledger_clickhouse grows two inputs — the indexer call sites and both e2e drivers ride along (workspace-atomic signature change).
Env-gated e2e: the raw registration ledger (63,893,403) decompressed, extracted and staged through prepare_with_sac_overrides, asserting the exact pool_state_changes and pool_share_tokens rows — values, change_index and plane_id to the digit. Covers all three transaction meta eras (V0/V1/V2 — the batch era is CAP-67 V2, which the first cut missed and read zero rows).
A soroban pool's 32 id bytes are a CONTRACT address payload — an L-strkey render of them would be well-formed and WRONG, so the API never mints one and must accept the C-form on pool paths and in the list's free-text box (pasting a pool's own id has to find it, task 0470's lesson). Adds contract_hex_to_strkey for the response side (consumed by the next commit — transient dead-code warning until then).
List and detail publish pool_kind / protocol / pool_type / legs[]; the pair fields go null on soroban rows (their storage defaults would render every pool as native/native). Legs resolve through the asset_sac facet or the bespoke-token assets row — an unresolvable leg surfaces as family "unresolved", never a plausible empty asset. Protocol labels are attribution resolved at read time from the registering deployment against a verified-operator list; the second live router (same WASM, disjoint admin roles) stays unlabelled. Values with the wrong population's truth go ABSENT, not zero: participant_count is null on soroban rows. Participants branch to share-token holders in balances (asset-direction full scan measured 121.5M rows / 71 ms; skip index on asset_id is the upgrade path); shares display scaled by the token's on-chain decimals or null when unpublished — the cursor rides the raw value. Classic-only feeds (USD chart, lp_operation_amounts activity) refuse soroban pools with an explanatory 400 instead of a confidently empty series — the activity surrogates would otherwise hash to the NATIVE asset and answer with unrelated XLM traffic. pool_exists retired: every gate needs the kind now. OpenAPI + generated types ride along (CI gate: API types freshness).
One poolLegViews expands a classic pair or a soroban legs[] into the same render shape, so labelling / linking / scaling rules live in one place. Soroban raw reserves scale by the leg's on-chain decimals with exact string surgery (18-decimal tokens are precisely what a double would corrupt); an unknown scale renders as absent, never a raw integer posing as scaled. An unresolved leg keeps an explicit '?'.
Every pool surface consumes poolLegViews, so soroban rows render their 2-4 legs (avatars, reserve dots, KPI cells, summary rows chunk two per row) next to classic pairs. Protocol chip appears only for a verified operator. Detail accepts C-strkey ids and does not mount the classic-only sections (USD chart, activity) for soroban pools — the API refuses them explicitly, so no doomed queries. Nullable fields render as em-dash, never as zero (participant_count, unknown-scale shares, missing first_deposit_ledger).
ADR 0058 records the durable decisions: shape-driven discovery with labels as read-time attribution; one pool dimension, two id worlds; reserves as ledger state at the chain's grain (pool_state_changes as the target state-fact shape); the share-token relation as a side table; explicit API refusals over misleading zeros. The three architecture overviews (database-schema, indexing-pipeline, xdr-parsing) gain the matching sections per ADR 0032.
…raming, steps 16-20 Deep-testing record (90-ledger corpus, staging e2e, bidirectional anti-test with the concentrated-instance discovery), the snapshot one-vs-two-tables investigation corrected by a devil's-advocate pass (two fact tables greenfield: state + trades; no in-band sentinels; parallel model not 'legacy'), the pool_state_changes rename rationale, and the read-path/frontend/docs completion state.
filter[pool_kind] = classic | soroban narrows the union list to one world; omitted keeps both. Validated in the handler so a bad value gets the envelope with the allowed list (the filter[event] shape); the SQL inlines the validated 0/1 discriminant. Regenerated OpenAPI types ride along (CI gate: API types freshness).
URL key 'kind' through the existing cursor-pagination filter mechanism (cursor resets on change); empty value = both worlds.
The step-7 StageInputs growth missed backfill-runner (outside that commit's test sweep): sink.rs and the pilot example no longer built. Wired to the ParsedLedger fields rather than stubbed empty, so the historical S3 re-parse emits pool_state_changes / pool_share_tokens for free — the same shared-path property token balances already ride.
The pass the 0463 balances seed deliberately deferred. K4-6 measured and chain-validated the gap: holders whose pool-share trustline predates the ingest floor have no lp_positions row (2,681 pools know <50% of their shares' owners, 1,164 live), and pre-floor pools with no post-floor entry change have no liquidity_pools row at all. Seeds missing positions (versioned on each entry's OWN lastModifiedLedgerSeq — the 0492 rule; first_deposit_ledger = 0 is the documented predates-our-history sentinel), self-heals newer differing pairs while restating their real first deposit (whole-row RMT), stubs missing pools + one snapshot row from the entry itself, and stubs missing holder accounts. Ghosts are REPORTED to ghosts.tsv, never corrected — the balances seed earned zeroing rights through a 100/100 RPC probe; this pass has no such evidence yet. The summary carries a protocol-identity decode check: sum(pool_shares_trust_line_count) over live pools must equal the decoded live pool-share count; mismatch = records dropped, do not --execute. Decoder extended with LiquidityPoolEntry (NetPool, first-wins), unit-tested on a constructed XDR entry.
Both per-pool feeds now branch per kind behind one URL instead of refusing soroban pools with a 400. Activity reads the pool's own trade/deposit_liquidity/withdraw_liquidity events out of soroban_events (per-leg signed raw amounts in leg_amounts; the actor is the tx source — the trader topic names the router on routed trades). The chart folds pool_state_changes reserves + trade events against the same prices series the classic chart joins (bespoke tokens via asset_kind = 'contract'); inputs are pre-aggregated in ClickHouse and the output is sparse calendar buckets on the classic grain, so both arms share one wire contract. An unpriceable trade nulls its bucket, never a partial sum. Shared mechanisms extracted rather than copied: one tx-enrichment seek (fetch_activity_txs), one interval-to-prices-view mapping (the soroban path had named a daily view that only existed in a local stub — prod's is prices.price_usd_series), the tuple keyset idiom, PriceLeg reuse via ChartPriceId, and a PoolFeed enum in place of the Option-pair dispatch. application_order is nullable on the wire now: soroban rows carry no per-op anchor, and the 0 sentinel built dangling #op-0 links and collided row keys. Unit tests for the module files move to sibling *_tests.rs files (the mod declarations tie them to this commit). Refs #405
Drop the detail page's soroban unmount gates: charts and activity mount for both kinds now that the API serves them. poolAmountLegs collapses to one arm — rows normalize to (leg index, signed raw amount) and the unified PoolLegView (now carrying decimals; classic legs are stroops, 7) supplies label, link, icon and scale for both worlds, so the pair fields and leg_amounts render through the same tail. tradeRate divides scaled values (raw units of legs with different decimals are not comparable), the local scaleRawAmount duplicate gives way to the library's validated scaleByDecimals, and rows without an application_order link plain /transactions/<hash> and key by page index. Refs #405
The real-data spec's soroban detail test flips from asserting the sections are absent to asserting the chart metric tabs render and the activity table shows a Trade row with the full swap phrase (via the amount stack's aria-label — the visible text interleaves linked codes). Refs #405
ADR 0058 §5 rewritten — the chart/activity refusals are replaced by the per-kind branches; the one refusal left is participants without a known share token. Task 0374 gains the build record and the simplify-pass record, including the prod-breaking stub-only prices view name the cross-check caught and the local-rig notes (vite base URL, stub daily view name).
…n crate The deposit-mint correlation rule (detect_share_tokens + its shape tests) is a verification oracle, not the live share-token source — TokenShare in pool instance storage is. It now lives with its 85k-transaction corpus in tests/share_token_real_corpus.rs, so nothing verification-only ships in the production module (pool_router.rs 802 -> 543 lines); a doc pointer remains at the old site.
…nit_args PlanePoolData decoded pool_type_raw + init_args, but staging consumes only pool/plane/reserves — the two fields were decoded and dropped on every plane write. The LIVE source of the pool type and fee is the add_pool registration event; the plane's copy fed nothing. Tests updated to the slimmer shape (the 5-init-arg stable case still pins that extra map keys cannot confuse the decode).
…idence rule The verified-operator mapping moves out of queries.rs into protocol_labels.rs — one place for the dictionary, the rule that earns an entry (operator verified at the vendor's own publications; code identity is not enough), the one-sided failure direction (incomplete is allowed, wrong is not — unlisted deployments render no chip), and the per-entry evidence links (aqua.network docs publish the router address; re-checked live 2026-08-31, archived captures in lore research 0003/0008).
PoolSummary chunked reserve cells into pairs itself because SummaryRow's contract is 'one row = up to two cells'. The pairing now lives beside that contract as a SummaryRows sibling (flat cell list laid out two-per-row), so any caller with a variable number of cells — a 3/4-leg soroban pool's reserves — reuses it instead of re-implementing the chunking. Rendering unchanged (verified on the 3-leg demo pool).
plane_id stays as provenance insurance (a removal was built and reverted same day on the owner's correction — recorded so the exercise is not repeated); the reserve-cell pairing moved into SummaryRows; duplicate leg codes stay label-ambiguous with the disambiguation options on file.
The 0374 sequence lived only in an init.sql comment, while CLAUDE.md names deployment.md and backfills.md as the mandatory pre-deploy and pre-backfill reading. Deploying the indexer against an un-migrated schema is an ingest outage, not a degradation: the clickhouse-rs client refuses an insert when a table is missing or still carries a no-DEFAULT column the row struct dropped, and the soroban arm runs unconditionally. That is the 0310 outage class, and production is mid-migration today, so the window is open. deployment.md gains the DDL-before-indexer gotcha with the exact statements; backfills.md gains the three catch-up passes and the mandatory window-closure check that proves no registration slipped between the backfill and the live writer.
ADR 0032 requires the architecture docs to move with any change to the shape of the system, and this branch changed it: the share-token side table now also carries the plane a pool declares for itself, and reserve provenance became a read-time predicate rather than a stored column nobody consulted. Three overviews updated (database-schema, indexing-pipeline, xdr-parsing), and ADR 0058 decision 4 amended with why one table carries both facts instead of a second side table beside it: they come from one authenticated source, read in one pass, on one version clock. Two claims that had already gone stale are corrected while here: the reserve grain was described as (pool, ledger, tx, change_index) after the 2026-08-30 collapse to (pool, ledger), and the deposit-mint share-token rule was described as production code after it moved to tests. The parser overview also now states the asymmetry the storage contract rests on — an instance keys on its owner, so a pool can only ever describe itself, while a plane entry names its pool in a payload.
…ainer Soroban emits a diagnostic copy of a transaction's events even when the transaction FAILED, so the diagnostic container is the failed-tx exclusion for the event path — there is no separate success flag on events. Every sibling detector filters it, and the module right beside this one documents why, but `detect_pool_registrations` did not: a transaction that merely ATTEMPTED to register a pool was indexed as a real registration. One guard, matching the four siblings. The regression test supplies a fully corroborated registration so the event source is the only thing keeping it out, and it fails without the guard.
…tance An add_pool event names its pool in the DATA payload, which the emitter chooses freely, and discovery is shape-driven by design — emitting the event IS what makes a contract a router. Nothing checked that the named pool agreed. Since liquidity_pools is a ReplacingMergeTree keyed on pool_id and versioned by ledger, a contract could name a REAL pool at a later ledger and replace its registry row wholesale: protocol label stripped, legs, fee and type replaced. Inventing unlimited pools was the milder half. The corroborating fact was already in hand and thrown away. A pool's instance storage records its own Router, and the pool contract is the ledger-authenticated owner of that entry, so a third party cannot write it. The instance is written in the SAME transaction as add_pool, so it is always in the same parse output. A registration now becomes a row only when the named pool declares that emitter. The refusal is a warn, not an error: unlike a malformed payload it is not evidence of a pool going missing. Also fixes a test that would have started passing for the wrong reason — prepare_refuses_a_registration_with_an_unparseable_fee no longer reached the fee check at all, since the new guard refuses it earlier. It now supplies a corroborating instance, as mainnet does.
Two arms feed pool_state_change_rows — the plane arm for fungible pools and the instance arm for concentrated ones — and they collide on a pool's registration ledger. The parser already folds each SOURCE independently, but neither fold can see the other, so both rows survived into one insert. pool_state_changes is a version-less ReplacingMergeTree, and per backfills.md rule 4 that is safe for re-parsing (a later parse lands last and wins) while the real hazard is exactly this: two rows for one key inside a single insert, where the survivor is arbitrary. That is the defect class task 0463 measured on balances, at 1.24M keys. Folding at stage time is what the classic twin already does (dedup_final_pool_snapshots, lore 0356): it makes the stored row a deterministic function of the ledger. Deliberately NOT a version column — any version keyed on something batch-local would let a narrow re-parse stamp a lower value than the original wide parse and lose to the stale row, inverting the guarantee every backfill depends on. The regression test emits both arms for one (pool, ledger) and fails without the fold, with two rows where one is expected.
…ying about soroban pools Closes the review of PR #438. Four defects share one root and are fixed together, because the fix spans schema, writer and reader. IDENTITY AND PROVENANCE. A plane entry names its pool in a KEY payload the writing contract chooses freely, so any contract can publish reserves under a victim pool's id — and argMax by ledger then serves them, deterministically, since a later ledger always wins. The authority already existed and was thrown away: a pool's own instance storage declares its Plane, and the pool contract is the ledger-authenticated owner of that entry. pool_share_tokens is therefore reshaped into pool_instance_state, carrying plane_id beside share_token_id — one table, because both facts come from one authenticated source, read in one pass, on one version clock. All three reserve reads (KPI, chart, list activity) now keep only rows whose plane_id matches what the pool declares. Free to do: nothing has deployed. XLM/XLM POISONING. Soroban registry rows carry the legacy pair columns at defaults, and asset_a_type = 0 reads as native there, so every soroban pool matched an XLM filter — 497 false positives against 754 real ones, measured on production. The shared predicate is gated to pool_kind = 0, which is its actual domain: it reads only the legacy pair columns, which only classic rows fill. Global search becomes kind-aware too, and stops minting a well-formed WRONG L-strkey for a contract address; a soroban pool is now named from its resolved legs, the same identities the list and detail render. FALSE UI STATE ON EVERY SOROBAN POOL. latest_snapshot_at is null for the whole kind (soroban never writes the classic snapshot table), so staleness derived from it captioned live reserves "no recent snapshot" — and that caption replaced the asset link. isPoolStale is kind-aware and the caption keeps its link. Participants for a pool with no share token rendered a generic "try again" for a DELIBERATE 400 that can never succeed; it now explains itself. LIST AND CHART. The list ordered soroban pools by registration while classic pools order by last trade, burying live Aquarius pools under ~26.5k classic ones; it now orders by activity, itself provenance-gated. The chart opened on "no activity" beside a full trades table because the guard caught all-null TVL but not zero rows; it widens its own window until the user picks a range. API types regenerated for the four endpoint descriptions that claimed L-only ids while accepting and returning contract ids.
Records what the seven-agent review found and how each finding was fixed, the sweep that bounded the root class to this branch's own code (the one pre-existing relative lives in 0410), and the decisions a future session would otherwise re-litigate: why pool_state_changes stays version-less, why the pool_kind guard belongs in the predicate, and why a write_order column was built and then removed. Also records the facts verified outside our own code — Stellar's docs plus a production measurement showing failed soroban transactions carry only fee events — and the measured cardinality of routers, pools and share tokens, including why 39 pools structurally have no share token.
karolko9
added a commit
that referenced
this pull request
Sep 1, 2026
The end state — `legs` as the only leg source in both worlds, the six legacy pair columns dropped — was decided inside 0374 while the soroban work was in flight, with four ordered steps and their verifiers. It never got a task, so the schema's "do not add new readers" rule had no owner. The review of PR #438 showed the cost of the columns surviving: a soroban registry row writes asset_a_type = 0, which every classic reader takes to mean native XLM, so the shared asset-code predicate rendered all 497 soroban pools as XLM/XLM on production. That was fixed with a pool_kind = 0 guard, which is correct as an interim and is meant to be deleted by step 3 of this task rather than preserved.
Read from chain 2026-09-01, one pool per deployment: five of the ten live deployments run an OLDER contract whose instance storage carries Plane, TokenShare and reserves but no Router key at all — 23 real pools, exactly the five dead deployments this task already counted. Two things had generalised a measurement taken on live creations into a rule about the whole population, and both were wrong: parse_pool_instance required BOTH Router and Plane, so those pools decoded to nothing — no pool_instance_state row, no plane_id, and therefore no reserves at all once reads filter on provenance. That is a regression the provenance work introduced. Plane alone now decides, which is safe because this decode is keyed on the entry's OWNER: a foreign contract writing a Plane key describes only itself, and nothing reads an instance row for a pool that never reached the registry. The corroboration guard then refused their registrations. It now ACCEPTS them with a distinct warn, because a missing key is an older contract version, not a forgery, and dropping a real pool is the failure the reject taxonomy exists to prevent. The residual is stated rather than hidden, in the code and in the ADR: the registry row of a pool that declares no router is forgeable. Every such pool measured is dead, with no flow event ever. No source settles the question, which is itself the finding: the protocol docs say nothing about trusting entry or event contents, the vendor's docs describe the roles without promising the key, and the vendor's source is unreachable (404, re-checked). The chain is the only authority available, so the code says what was measured and when. Regression tests pin both shapes — an older pool decodes and registers, an instance with no Plane is still not a pool.
The function maps every leg and joins them, so a 3- or 4-leg pool comes back as "XLM / AQUA / USDx" — which the test already pinned. Only the name still said pair, and that is precisely the assumption this model exists to remove: 491 pools on mainnet carry two legs, 7 carry three and 2 carry four. Renamed to poolLegsLabel, matching poolLegViews, which it is built on. The local binding in the detail header was also called pair and is now legs. Mechanical across three consumers; the doc comment carries the measurement so the next reader does not have to re-derive why the old name was wrong.
isPoolStale measured pool IDLENESS and presented it as data unreliability. A classic pool writes a snapshot on every mutating change, so an old snapshot means nothing has happened — the reserves beside it are exactly current. The caption said the opposite, and it fired on the majority: 60% of classic pools (31,420 of 52,677) last changed over a week ago, the median 35 days. The function had grown a kind branch to stop it lying about soroban pools, which only moved the lie: soroban returned "not stale", which reads as "fresh", where the truth is that snapshot freshness does not apply to a kind whose reserves come from live ledger state. A boolean cannot carry fresh/stale/not-applicable, and the two "unknown" cases were answered oppositely depending on kind. Removed rather than replaced. Nothing takes its place: the age would be a fact worth showing, but it is not one this endpoint carries, and inventing a field to justify keeping the cell is how the original judgement got here. Net −85 lines. Also corrects the doc comment it rested on, which claimed stale pools come back with null reserves — true of the Postgres path, false on ClickHouse, where the module notes the freshness window is NOT applied to the detail pick. Separately: the pool list ordered its page by activity_ledger and its cursor by activity_ledger, then re-sorted the result by last_updated_ledger — the right pools in the wrong order, and a cursor keyed on a column the display did not follow. Introduced with the activity ordering; fixed here.
…urning null first_deposit_ledger was null for every soroban participant, on the grounds that `balances` records current state rather than a first sighting. That is true of `balances` — it carries no such column — but it was never the only source. The share token emits an event whenever a position begins, and we store them: 76,508 mints and 2,031 transfers across the LP tokens on production. The first mint or incoming transfer naming a holder IS the ledger their position began. Measured on the busiest share token: 655 of 655 current holders resolve, the four contract holders included. Cheap by construction — `soroban_events` is ordered on `contract_id` first, so this is a primary-key prefix seek over one token's events, and it runs alongside the three lookups the path already issues in parallel. One trap, verified before relying on it: this family's `mint` carries [sym, pool, to] — three topics, not the two a bare SEP-41 mint has. Reading the recipient at the SEP-41 index returns the POOL and dates almost nobody (3 of 655 on the first attempt). Checked across every LP share token: 78,539 acquisition events, all three topics, the third always an address. `shares` stays optional, now for a measured reason rather than an assumed one: it is absent only when the token published no decimals, and all 483 LP share tokens publish 7. Alongside, three reductions from a fresh-eyes pass over the PR: - `reserveDotColor` deleted — zero callers. - `family_label` defers to `AssetFamily::as_str` instead of repeating the discriminant mapping, which is the drift task 0496 recorded. Only the "unresolved" fallback stays local, because it is not a family. - The house typed-JSON READERS get a canonical home beside the encoder in `scval.rs`. `pool_state` and `pool_router` had each grown their own set in one PR — the same duplication `map_get` was consolidated to fix in 0393, recurring because only the encode side had a home. The dedup folds keep their (pool, ledger) key. It never varies today, since `ParseOutput` is built per ledger, but dropping it would turn dead weight into a silent trap for a future batching caller; the invariant is now stated in the docs instead.
Making it nullable was the PR conceding a gap it did not have. The coverage is STRUCTURAL, not lucky: every LP share token was deployed after our event floor — the family's first mint is L50,639,009 against a floor of L50,457,424 — so the mint or transfer that gave a current holder their tokens is always an event we hold. Measured 655/655 on the busiest token, the four contract holders included. So the field goes back to a plain i64 for every consumer, including the classic ones that never lost the ability to provide it. A soroban holder we cannot date is now treated as a defect rather than an absence: dropped with an error log, the same contract this path already applies to a holder that resolves to no account or contract. The frontend loses its em-dash branch with it. `shares` deliberately stays nullable, and the reason is now recorded next to it. Its coverage rests on a different kind of fact: not on our index, but on the vendor's contract publishing `decimals`. All 483 LP share tokens on production do, and all say 7 — but this same PR found five deployments running an older pool contract that publishes no `Router` key at all. A measurement over the instances we have met is not a guarantee about a version we have not.
Every soroban pool plotted a TVL curve on its chart and showed an empty TVL figure above it. Both numbers came from the same reserves and the same price views, computed a few hundred lines apart. The cause was not a missing capability. `fetch_pool_usd_analytics` values a pool from `leg_a`/`leg_b` — the CLASSIC pair — and `priceable_legs` reads exactly those two, ignoring the `legs` the context already carries. A soroban row holds storage defaults in the pair columns, so both legs filtered out as unpriceable and TVL, volume and fee revenue came back null for all 500 of them. The detail handler made it explicit, passing `legs: Vec::new()` under the note that only the chart branch reads them. TVL now sums over a soroban pool's 2-4 legs the way its chart sums each point: reserve × last hourly close, each leg scaled by its own decimals, SAC legs priced by classic identity and bespoke tokens under `asset_kind = 'contract'` — the second identity shape needed the sibling of `fetch_last_closes`, which did not exist because nothing had asked for it. A leg that cannot be identified, scaled or priced nulls the whole figure, never a partial sum over the legs that happened to resolve, which is the rule the classic path and the chart already apply. Two price queries for a whole page, not per row, matching how the classic list prices its page; the detail is a page of one. Errors degrade the figure to null rather than failing the page, as the classic analytics do. Still absent for soroban, and deliberately: 24h volume and fee revenue read `liquidity_pool_snapshots.gross_volume_a`, which soroban never writes. Their source is the pool's own trade events, a different aggregation from this one.
The last gap of the same shape as the TVL one: a soroban pool's chart plotted volume and fee revenue per bucket while its detail showed neither. The classic path sums `liquidity_pool_snapshots.gross_volume_a`, a column soroban never writes — so the figure was absent, not because the data is missing, but because the only source wired up was the one that does not apply. Soroban volume now comes from the pool's own `trade` events, the source its chart already aggregates: sum the in-token amounts over 24h, price each hop by its in-token's leg at that leg's last hourly close, scaled by that leg's own decimals. Fee revenue follows from it through the same `fee_revenue_usd` the classic branch uses. The chart's rules come with it rather than being re-invented: a hop we cannot price or parse nulls the whole figure instead of contributing a partial sum, and the token→leg match goes by SURROGATE against the registry's own legs, never through the contracts dimension. A pool with no trades in the window is a genuine zero, not an unknown. Detail only, matching the classic contract exactly — both read a per-pool source that has no place in a list page. One query, on a primary-key prefix (`soroban_events` is ordered on `contract_id`), reusing the legs and prices already resolved for TVL.
karolko9
added a commit
that referenced
this pull request
Sep 2, 2026
The task file's session record lived on the PR #438 branch while the code moved to develop — the record follows the code. Brings over the full 0374 history: research, verification passes, review records, the split decision, the created-gate + total_shares hardenings, the e2e re-run with the production cross-check, and the read-half-last roadmap.
karolko9
deleted the
feat/0374_lp-native-leg-and-soroban-amm-completeness
branch
September 15, 2026 15:17
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
add_pooldiscovery into theliquidity_poolsunion (pool_kind = 1), verified on the full mainnet population (497/497 registrations decode, 0 false positives on an all-signatures negative corpus). Refs improvement suggestion: add other protocols to the explorer #405pool_state_changestable fed by two on-chain layouts (planePoolDatafor fungible, instanceReserve0/1for concentrated) plus thepool_share_tokensside table from the instanceTokenSharekey; verified by raw-ledger corpora, a staging e2e and a bidirectional anti-test againstupdate_reserves(0 missing, values 17/17)legs[]/pool_kind/protocol(verified-operator label only) /pool_typeon list+detail,filter[pool_kind], C-strkey pool ids, soroban participants from share-token balances, explicit 400s for the classic-only chart/activity feeds instead of confidently empty series; frontend renders 2–4 legs through one leg-view modelshare_percentageis confirmed correct (denominator chain-exact via rawgetLedgerEntries; the holder-coverage gap is filed separately as task 0523)