New module: candlestick/policy.rs implements the core state machine that judges
and applies legs for acceptance based on deviation from a reference price and
qualification credit.
Key components:
- Leg: per-pool per-transaction change (sats_delta, token_delta, post-state reserves)
- JudgeResult: verdict on whether a leg is accepted into OHLC
- Policy: stateful fold that maintains pool reserves, qualification credit, and
a min-depth-weighted median reference price
Qualification rules (two-tier):
- Tier 1: dev <= F (F=5) always accepted
- Tier 2: dev > F but summed_credit >= q*largest_credit (q=5%) also accepted
- Everything else is muted (volume still counted)
Reference computation:
- R = min-depth-weighted median of pool spot ratios
- Updated only on reserve events (swaps, creations, withdrawals), never on prints
- Depth = min(sats, tokens * R_prev) to zero-weight lopsided pools
- Avoids ratchet-walking and qualifies token-heavy reseeds (OLA-like)
Tests: 5 passing
- unpriceable legs
- first leg (no reference yet)
- tier 1 acceptance (dev within F)
- tier 1 rejection (dev > F, no credit)
- credit seeding on accepted prints
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
A transaction that trades against several pools of the same token had its
price computed as |SUM(sats_delta) / SUM(token_delta)|. Arbitrage routers buy
from one pool and sell into the others, so the token deltas very nearly
cancel, and the division turned real satoshis into a price no leg ever traded
at. Mainnet token NWB printed 30,792,599.5 sats/unit from a 27-pool sweep
whose legs all executed between 0.288 and 0.335 — a hundred-million-fold
error, and the visible spike on its chart. 820 such transactions exist across
91 tokens.
Price is now SUM(ABS(sats_delta)) / SUM(ABS(token_delta)): the volume-weighted
average of the prices the transaction's legs actually executed at, which is
always bounded by its cheapest and dearest leg. For single-direction
transactions — 99.76% of all prints, including the OLA supply-shock crash —
this is arithmetically identical to the old formula, so honest history is
untouched.
Transactions whose legs cancel exactly used to print nothing and let the
candle carry the previous close; they now price from their legs like any
other trade.
ohlcv_1h is materialised with INSERT OR IGNORE and the materialiser only ever
moves forward, so contaminated buckets could never be corrected in place. An
ohlcv_version config key clears the table once when the pricing rule changes.
The synchronous post-IBD backfill is skipped on that pass: it runs before
rocket::build() returns, so rebuilding all of history there would refuse
connections for the duration instead of falling back to the raw query path.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The EntryDistributed update was pushed in the POSTLAUNCH->DISTRIBUTED
branch using the collection tx's own txid. That branch fires once at
final collection rather than per distribution tx, and tx.compute_txid()
never equals an entry's creation txid, so the dist_expr join
(d.entry_txid = e.txid) never matched and entries were never marked
distributed.
Push it in the DISTRIBUTING handler instead, once per distribution tx,
using input#1.previous_output.txid (the entry NFT being spent), which is
the entry's creation txid recorded by IdoUpdate::Entry.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace index_block/index_mempool with a single index_txs that takes an
Option<blockhash> (Some for confirmed, None for mempool). Add
delete_entries(blockhash) for reorg undo and to drop stale mempool state
before applying confirmed blocks. Replace has_txchain_tx with
has_indexed_tx; chain-follow now keys on ido_state.next_output_index
instead of the removed tracker map.
Remove the debug-only txchain RPC endpoints and the debug config gating.
Keep txchain_head as a public RPC field.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Migrate the ido module's arbitrary-precision math from num-bigint to
malachite (already a workspace dependency). BigInt becomes
malachite::Integer throughout; the VM-number byte codec uses
PowerOf2Digits, sign handling uses Integer::sign(), and primitive
conversions use try_from.
Add an IntegerAsStr serde adapter for string-serialized fields, since
malachite's FromStr::Err is () and does not satisfy DisplayFromStr's
Display bound. JSON output is unchanged.
Drop the now-unused num-bigint and num-traits dependencies.
Also fix all clippy warnings in the module surfaced by the migration
(unwrap-after-is_none control flow, a const->static LazyLock bug,
an oversized enum variant boxed, and assorted mechanical lints).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
on_add_ido_tx rebuilt the whole chain whenever the input tx was not the
txchain head. A mempool tx confirming after the chain advanced past it
would needlessly trigger a full rebuild.
Reconstruct the current chain (txchain_head .. txchain_entrypoint) and,
if the tx is already part of it, only update its block height. Extract
the shared chain-walking logic into reconstruct_txchain, reused by both
the membership check and the rebuild branch.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the per-request get_ido_entry_aggregates SQL scan with running
totals carried in IdoActiveState: totalDemandAmount, totalSupplyAmount,
and totalDiscount. Initialized to 0 at the preinit->active transition and
incremented on each entry added; the rebuild path recomputes them by
replaying the txchain, so they self-heal.
Drop the now-unused get_ido_entry_aggregates DB fn, the IdoEntryAggregatesRpc
type, the /<id>/aggregates RPC handler, and its route registration.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add two timestamp columns to the ido table, both sourced from Delphi NFT
commitments (48-bit LE unix seconds):
- created_at (NOT NULL, default 0): from the Delphi NFT in the preinit's
first output, set at IDO creation. 0 if the commitment is too short.
- launched_at (NULL): from the Delphi NFT in output#3 of the launch
transaction, set in lockstep with launch_txid. Null until launched.
Both are threaded through the parse/context/update paths and exposed on
IdoRpcRecord.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add an optional owner_nfthash query param to GET /<id>/entries. Accepts
up to 20 comma-delimited 32-byte hex hashes; an entry matches any listed
value via an owner_nfthash IN (...) clause. Rejects malformed hex,
wrong-length values, and >20 filters with a 400.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sum demand/supply amounts and count entries for an IDO, used to show
"raised so far" for active offerings where on-chain state keeps no
running total.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Update ido/mod.rs for the bitcoincash 0.32 API: Script -> ScriptBuf,
push_slice via &PushBytes (new pb() helper), as_byte_array/as_bytes.
Fix update_mempool to diff against cauldron_txs instead of defi_txs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Stamp mempool tracker entries with the negative of the highest known
block height instead of a fixed -1 sentinel, and trim on abs(height) so
entries whose tx is invalidated before confirming (e.g. by a double
spend) age out after TRACKER_MAX_BLOCK_DEPTH like confirmed ones. The
height is still replaced with the real one once the tx confirms.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- electrum mempool fetch gains an ido filter: state machine spends
(IDO_SIGNATURE in scriptsig) union preinit announcements
(IDO_PREINIT_ANNOUNCEMENT_SIGNATURE in scriptpubkey)
- split tx scanning out of index_block into index_txs and add
index_mempool, which indexes with a -1 sentinel height;
txchain_trim_tracker leaves negative heights alone and the real
height replaces the sentinel once the tx confirms in a block
- block indexing now skips re-creating an ido already seen in the
mempool, only updating its tracker height
- mempool txs are kahn-sorted so txchain parents index before children
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The preinit tx layout changed: the first output is now the Delphi NFT and
the announcement OP_RETURN is the last output. Read the announcement from the
last output in both is_preinit_broadcast and parse_ido_preinit_tx_params.
Add validity checks on the first output's Delphi NFT: its category must match
the announcement's delphiCategory, and the 48-bit commitment timestamp (current
time) must place launchConditions.expiresAt within a 1-30 day window.
The two PREINIT_TEST_TX vectors use the old layout, so the three preinit parse
tests are marked #[ignore] pending new-format sample transactions.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Change get_txchain_tx to take the public txid (a unique field) instead of
the non-public internal txchain item id, and promote it to a production
endpoint. Mount the two remaining debug endpoints (list_ido_txchain,
list_txchain_tracker_map) only when `debug = true` in the config.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The IDO RPC record no longer leaks internal txchain row ids. Drop the
txchain_entrypoint field entirely, and change txchain_head from the
internal ido_txchain.id to the head record's txid (display hex).
list_idos and get_ido_by_offering_token_id resolve this in a single
query via LEFT JOIN ido_txchain ON head_tx.id = ido.txchain_head; a
NULL head yields null. Adds tests for both the resolved and null cases.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Rename ido.id -> ido.internal_id in the schema and IdoDBRecord, and
expose the preinit_txid hex as the public "id" in all RPC responses.
Public API shape:
- IdoRpcRecord.id: i64 -> String (hex of preinit_txid). preinit_txid
field is preserved unchanged.
- IdoEntryRpcRecord.ido_id: i64 -> String (hex of parent preinit_txid).
- IdoTxChainRpcRecord.ido_id: i64 -> String (hex of parent preinit_txid).
- IdoTxChainRpcRecord gains ido_internal_id: i64 (debug endpoint only).
- Routes /<id>/entries and /<id>/txchain accept the preinit_txid hex
as the path id; returns 404 IDO_NOT_FOUND on miss.
Internals:
- New lookup_internal_id_by_preinit_txid helper.
- list_ido_entries / list_ido_txchain take preinit_txid_hex as input
so the caller (which already parsed it from the path) avoids the
extra "preinit_txid by internal_id" lookup.
- Child table FK columns (ido_entry.ido_id, ido_txchain.ido_id) keep
their names; only the parent PK and field accesses were renamed.
Notes:
- Breaking API change for /ido/* endpoints. Clients reading "id" or
"ido_id" as integers must switch to strings.
- ido.db has no migration framework: drop the file and re-index on
deploy.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
List every active TokenToken pool (no pair filter) so the frontend can
present the set of pooled token pairs without probing each candidate pair.
Mirrors db_active_pools_for_pair minus the pair WHERE clause; served at a
distinct /pools path (the param'd /pool/active already matches param-less
requests via its Option guards, so reusing it would collide).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fixes the indexer crash on chipnet: bitcoincash 0.32.1's
Opcode::classify panicked on BCH re-enabled opcodes (e.g. OP_SPLIT),
which riftenlabs-defi hit while parsing input scriptSigs in
parse_cauldrons_from_tx. bitcoincash 0.32.2 makes classify total and
panic-free (and adds the May 2026 upgrade opcodes); riftenlabs-defi
0.4.1 additionally hardens read_push_from_script to not classify
non-push opcodes at all.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Migrate off the deprecated bitcoincash 0.29 API (Amount/Version types,
FromHex -> FromStr/parse, non-exhaustive Network) across the indexer and
tests.
Add indexing of native token-A <-> token-B (TokenToken) AMM pools:
- tokentoken_pool / tokentoken_pool_history_entry tables in cauldron.db,
created via an always-run idempotent migration (no DB_VERSION bump, so
existing databases upgrade in place)
- block-path indexing sharing the cauldron write transaction and reorg
undo, with creation/swap/withdrawal state tracking and reserve deltas
- mempool indexing: electrum mempool.get filters on the tokentoken
contract code (spends) and the CONJURE op_return hint (creations);
first_seen_timestamp reconciles with mtp on confirmation
- RPC endpoints /tokentoken/pool/active (pair lookup, order-insensitive)
and /tokentoken/tokens
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>