riftenlabs-indexer/src/main.rs

703 lines
25 KiB
Rust
Raw Normal View History

2026-01-21 12:34:59 +01:00
// Copyright (C) 2024-2026 Whiterun LLC
2024-02-05 16:32:42 +01:00
//
// This software is licensed under the GNU Affero General Public License (AGPL), version 3.0 or later.
// A copy of the license can be found in the LICENSE file or at https://www.gnu.org/licenses/agpl-3.0.html
use anyhow::{bail, Result};
use bcmr::wellknowndowloader::WellKnownDownloader;
2026-01-05 15:40:01 +01:00
use bitcoincash::{consensus::deserialize, Block, BlockHash, Network};
2024-10-21 12:48:47 +02:00
use crc20::crc20fetcher::CRC20Fetcher;
use db::cauldron::tokenlist::db_utils::create_cached_token_metrics_table;
2026-01-05 15:40:01 +01:00
use db::DB;
use electrum::electrum_get_tip;
use electrum_client_netagnostic::{Client, ElectrumApi, Param};
use log::{error, info, warn};
use rocket::{launch, routes};
2024-01-19 14:38:31 +01:00
use rocket_cors::{AllowedHeaders, AllowedOrigins};
2026-03-25 11:38:10 +00:00
use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU64, Ordering};
2023-11-29 12:25:04 +01:00
use std::{
backtrace::Backtrace,
2026-01-05 15:40:01 +01:00
panic, process,
sync::{Arc, Mutex},
2026-01-05 15:40:01 +01:00
time::Duration,
2023-11-29 12:25:04 +01:00
};
2024-02-14 11:11:16 +01:00
use stderrlog::LogLevelNum;
2023-11-29 12:25:04 +01:00
2026-03-25 11:38:10 +00:00
/// Tracks how far the OHLCV pre-aggregation table has been populated.
/// `materialized_end` is the exclusive upper bound: `ohlcv_1h` contains
/// data for all complete 1-hour buckets whose `bucket_ts + 3600 ≤ materialized_end`.
/// Value 0 means nothing has been materialised yet.
pub struct OhlcvState {
pub materialized_end: AtomicI64,
}
/// State tracking for Initial Block Download (IBD).
/// Used to return 503 errors while the indexer is catching up.
pub struct IbdState {
/// Set to true once the initial sync completes
pub initial_sync_complete: AtomicBool,
/// Current block height being indexed
pub current_height: AtomicU64,
/// Target chain tip height
pub target_height: AtomicU64,
/// If true, serve requests even during IBD (for debugging)
pub serve_during_ibd: bool,
}
use crate::bcmr::bcmrdownloader::BCMRDownloader;
use crate::db::cauldron::header::load_all_headers;
2025-09-05 13:53:10 +00:00
use crate::db::cauldron::tokenlist::metrics_cache::spawn_token_metrics_updater;
2026-02-10 14:48:45 +01:00
use crate::db::init::{initialize_databases, ReadSlots};
use crate::index::{index_blocks, update_mempool};
2023-11-29 12:25:04 +01:00
#[macro_use]
extern crate configure_me;
include_config!();
2024-02-14 11:11:16 +01:00
// The block where first cauldron contract was deployed. (Block 799870)
#[allow(dead_code)]
2024-02-14 11:11:16 +01:00
const RIFTEN_LABS_GENESIS_BLOCK: &str =
"000000000000000000ed24c811077f7268a21ecf25cb437655aaba33d8ff4997";
// Start parsing for BCMR data from this height
const CASHTOKEN_ACTIVATION_HEIGHT: &str =
"000000000000000002b678c471841c3e404ec7ae9ca9c32026fe27eb6e3a1ed1";
2026-01-05 15:40:01 +01:00
// Chipnet genesis
const CHIPNET_START_BLOCK: &str =
"000000001dd410c49a788668ce26751718cc797474d3152a5fc073dd44fd9f7b";
2023-11-29 12:25:04 +01:00
// Last indexed block height.
const KEY_LAST_INDEXED: &str = "last_indexed";
mod bcmr;
2024-04-03 21:40:33 +02:00
mod cashaddr;
2024-02-14 11:11:16 +01:00
mod chain;
2024-10-21 12:48:47 +02:00
mod crc20;
2023-11-29 12:25:04 +01:00
mod db;
2024-11-13 11:16:24 +01:00
mod def;
2024-02-14 11:11:16 +01:00
mod electrum;
mod index;
2024-03-04 16:40:50 +01:00
mod rpc;
mod signal;
2024-04-03 09:49:44 +02:00
mod timeutil;
2024-10-31 10:55:29 +00:00
mod utiltest;
2024-10-21 12:48:47 +02:00
mod utiltoken;
mod utiltx;
2023-11-29 12:25:04 +01:00
fn set_panic_hook() {
panic::set_hook(Box::new(|panic_info| {
2024-03-04 16:40:50 +01:00
error!("A thread panicked, terminating the program.");
if let Some(error) = panic_info.payload().downcast_ref::<anyhow::Error>() {
2025-07-16 09:31:14 +02:00
error!("Panic occurred: {error:?}");
2024-03-04 16:40:50 +01:00
error!("Anyhow backtrace:\n{}", error.backtrace());
let mut source = error.source();
while let Some(cause) = source {
2025-07-16 09:31:14 +02:00
error!("Caused by: {cause:?}");
2024-03-04 16:40:50 +01:00
source = cause.source();
}
} else if let Some(message) = panic_info.payload().downcast_ref::<&str>() {
2025-07-16 09:31:14 +02:00
error!("Panic occurred: {message}");
2024-10-21 12:48:47 +02:00
} else if let Some(message) = panic_info.payload().downcast_ref::<String>() {
2025-07-16 09:31:14 +02:00
error!("Panic occurred: {message}");
2024-02-14 11:11:16 +01:00
} else {
2025-07-16 09:31:14 +02:00
error!("Panic info: {panic_info:?}");
2023-11-29 12:25:04 +01:00
}
2024-03-04 16:40:50 +01:00
let backtrace = Backtrace::capture();
2025-07-16 09:31:14 +02:00
error!("Backtrace (if RUST_BACKTRACE=1):\n{backtrace}");
2023-11-29 12:25:04 +01:00
process::exit(1);
}));
}
async fn start_program(
config: Config,
) -> Result<(
DB,
BCMRDownloader,
WellKnownDownloader,
CRC20Fetcher,
Arc<IbdState>,
2026-03-25 11:38:10 +00:00
Arc<AtomicBool>, // indexing_in_progress
)> {
2026-01-05 15:40:01 +01:00
let network = match config.network.to_lowercase().as_str() {
"mainnet" => Network::Bitcoin,
"chipnet" => Network::Chipnet,
_ => bail!(
"Invalid network '{}'. Must be 'mainnet' or 'chipnet'",
config.network
),
};
2026-01-05 15:40:01 +01:00
info!("Using network: {:?}", network);
2023-11-29 12:25:04 +01:00
2026-01-05 15:40:01 +01:00
let rostrum_addr = if config.rostrum_addr.is_empty() {
match network {
Network::Chipnet => "127.0.0.1:64001".to_string(),
_ => "127.0.0.1:50001".to_string(),
}
} else {
config.rostrum_addr
};
2024-10-21 12:48:47 +02:00
2026-02-03 09:37:24 +01:00
// Normalize address: if no scheme, default to tcp:// for backwards compatibility.
// Supported schemes: tcp://, ssl://, ws://, wss://
let electrum_url = {
let s = rostrum_addr.trim();
if s.starts_with("tcp://")
|| s.starts_with("ssl://")
|| s.starts_with("ws://")
|| s.starts_with("wss://")
{
s.to_string()
} else {
format!("tcp://{}", s)
}
};
2026-01-05 15:40:01 +01:00
let network_str = match network {
Network::Bitcoin => "mainnet",
Network::Chipnet => "chipnet",
_ => "mainnet",
};
2026-02-10 14:48:45 +01:00
let db = initialize_databases(
network_str,
ReadSlots {
cauldron: config.cauldron_read_slots,
bcmr: config.bcmr_read_slots,
crc20: config.crc20_read_slots,
oracle: config.oracle_read_slots,
moria: config.moria_read_slots,
2026-04-25 20:15:15 +03:00
ido: config.ido_read_slots,
2026-02-10 14:48:45 +01:00
},
)
.await?;
let indexing_in_progress = Arc::new(AtomicBool::new(false));
let ibd_state = Arc::new(IbdState {
initial_sync_complete: AtomicBool::new(false),
current_height: AtomicU64::new(0),
target_height: AtomicU64::new(0),
serve_during_ibd: config.serve_during_ibd,
});
2026-02-03 09:37:24 +01:00
let client = Arc::new(Mutex::new(match Client::new(&electrum_url) {
Ok(server) => server,
Err(e) => {
error!(
"Failed to connect to {}: {}. See --help for setting a different server.",
electrum_url, e
);
bail!(e)
}
}));
2024-02-14 11:11:16 +01:00
let genesis = match client
2024-02-14 11:11:16 +01:00
.lock()
.unwrap()
.raw_call("blockchain.block.get", vec![Param::U32(0)])
{
Ok(g) => g,
Err(e) => {
error!(
"Failed to fetch genesis block from {}: {}. See --help for setting a different server.",
electrum_url, e
);
bail!(e)
}
};
2024-02-14 11:11:16 +01:00
let genesis: Block = deserialize(&hex::decode(genesis.as_str().unwrap()).unwrap()).unwrap();
let chain = Arc::new(Mutex::new(chain::Chain::new(genesis.header)));
db::cauldron::pool::initialize_seq(&db.cauldron_r).await;
db::cauldron::tokentoken::initialize_seq(&db.cauldron_r).await;
tokentoken/tokenbch: index delegation pools, replace the pre-delegation AMM Replace the old P2SH32/CONJURE TokenToken AMM indexer (never in production; its tables are dropped by an always-run migration) with the delegation-model indexers from riftenlabs-defi 0.4.5: - tokentoken: two co-created bare-P2S UTXOs (thin main + storage sibling). tokenbch (new): single-UTXO token<->BCH pool with the runtime feePaidInToken fee side. Both admitted purely by the constant locking derived from the per-network delegation-pool platform NFTH. - src/db/orbconstants.rs: ONE platform NFTH serves both contracts — chipnet pins the deployed ORB v0 value (libriften orb/v0/chipnet.ts), mainnet is the all-zero placeholder: while unconfigured NOTHING delegation-related is parsed, fetched or indexed (fail-closed). - Schema: immutable config (nftOwner, tokens, poolFeeRate, minFee, virtualX/Y, tokenbch feePaidInToken) on the pool row; per-state history carries raw reserves, owed, platformFeeRate (both mutable by platform sweeps) and deltas. History rows now FK tx(txid) ON DELETE CASCADE, so evicted mempool txs clean up after themselves (the old tokentoken indexer leaked those). - LP teardowns carry no pool output: ingestion probes every blob-presenting tx's spent outpoints against the recorded pool set and flags a known pool UTXO spent without successor as withdrawn (withdrawn_in_txid FK ON DELETE SET NULL reverts an evicted teardown; reorg undo reverts a confirmed one). - electrum mempool filters: the combined logic blob (scriptsig; every spend incl. teardowns) + the constant thin-main locking (scriptpubkey; creations), per contract, skipped while unconfigured. - RPC: /tokentoken responses gain owed_a, platform/pool fee rates, minFee and virtual offsets; new /tokenbch (pool/active?token, pools, tokens) mirrors it for BCH pairs. - deps: bitcoincash 0.32.4 (token.amount -> .to_int() in the ido indexer), riftenlabs-defi 0.4.5. NOTE: 0.4.5 (rust-riftenlabs-defi 7c65f66) is not on crates.io yet — until it is published, build with a local [patch.crates-io] pointing at the sibling checkout (the committed Cargo.lock carries the path-patched entry). Also fixes a latent test bug (ido_params_nft_commitment_roundtrip never wrote createExecutionFee after the 127-byte layout shift). Verified: cargo build, test (232 passing), clippy (no new warnings) and fmt all clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 15:30:55 +00:00
db::cauldron::tokenbch::initialize_seq(&db.cauldron_r).await;
2024-02-14 11:11:16 +01:00
info!("Loading block headers...");
let all_headers = load_all_headers(&db.cauldron_r).await.unwrap();
2024-02-14 11:11:16 +01:00
info!("Initializing {} headers...", all_headers.len());
chain.lock().unwrap().load(all_headers).unwrap();
info!("Headers loaded.");
let db_cpy = db.clone();
2024-10-21 12:48:47 +02:00
let mut crc20fetcher = CRC20Fetcher::new();
crc20fetcher.start(
db.crc20_w.clone(),
db.bcmr_r.clone(),
client.clone(),
indexing_in_progress.clone(),
)?;
2023-11-29 12:25:04 +01:00
db::oracle::clear_mempool(&db.oracle_w).await.unwrap();
db::moria::clear_mempool(&db.moria_w).await.unwrap();
db::bcmr::clear_mempool(&db.bcmr_w).await.unwrap();
let indexing_in_progress_clone = indexing_in_progress.clone();
let ibd_state_clone = ibd_state.clone();
let start_height = config.start_height;
tokio::spawn(async move {
let db = db_cpy;
2024-02-14 11:11:16 +01:00
{
let client_lock = client.lock().unwrap();
if let Ok(tip_info) = electrum_get_tip(&client_lock) {
let tip_height = chain.lock().unwrap().height();
ibd_state_clone
.target_height
.store(tip_height.max(tip_info.1), Ordering::Relaxed);
}
}
// Initial full index
let mut tip: BlockHash = loop {
if signal::shutdown_requested() {
info!("Shutdown requested, exiting indexing task during initial index");
return;
}
indexing_in_progress_clone.store(true, Ordering::Relaxed);
2026-01-05 15:40:01 +01:00
break match index_blocks(
chain.clone(),
db.clone(),
client.clone(),
true,
Some(network),
Some(ibd_state_clone.clone()),
start_height,
)
.await
{
Ok(tip) => tip,
Err(e) => {
if e.to_string().contains("database is locked") {
2025-07-16 09:31:14 +02:00
warn!("initial index error, trying again: {e}");
continue;
}
panic!("Initial index failed: {}\n {}", e, e.backtrace());
}
};
2024-02-14 11:11:16 +01:00
};
indexing_in_progress_clone.store(false, Ordering::Relaxed);
info!("Initial block download complete");
2026-03-25 11:38:10 +00:00
// Run ANALYZE before signalling initial_sync_complete so the ohlcv post-IBD
// backfill (which waits for that flag) doesn't race with this write.
info!("Running ANALYZE on cauldron database...");
if let Err(e) = sqlx::query("ANALYZE;").execute(&db.cauldron_w).await {
warn!("ANALYZE failed: {e}");
} else {
info!("ANALYZE complete");
}
2026-03-25 11:38:10 +00:00
ibd_state_clone
.initial_sync_complete
.store(true, Ordering::Relaxed);
// Follow chain
2023-11-29 12:25:04 +01:00
loop {
if signal::shutdown_requested() {
info!("Shutdown requested, exiting indexing task");
return;
}
let new_tip_result = {
let client_lock = client.lock().unwrap();
electrum_get_tip(&client_lock).map(|t| t.0.block_hash())
};
let new_tip = match new_tip_result {
Ok(t) => t,
Err(e) => {
warn!("Failed to get chain tip from electrum: {e}");
tokio::time::sleep(Duration::from_secs(5)).await;
continue;
}
};
2023-11-29 12:25:04 +01:00
if new_tip != tip {
indexing_in_progress_clone.store(true, Ordering::Relaxed);
2026-01-05 15:40:01 +01:00
tip = match index_blocks(
chain.clone(),
db.clone(),
client.clone(),
true,
Some(network),
None,
0, // start_height only matters for initial sync
)
.await
{
Ok(t) => t,
Err(e) => {
warn!("Indexing block failed: {} {}", e, e.backtrace());
tip
}
};
indexing_in_progress_clone.store(false, Ordering::Relaxed);
2023-11-29 12:25:04 +01:00
}
// Avoid overlapping writer while indexer is on
if !indexing_in_progress_clone.load(Ordering::Relaxed) {
if let Err(e) = update_mempool(&db, client.clone(), Some(network)).await {
error!("Failed to update mempool: {e}");
}
2024-02-16 09:11:40 +01:00
}
tokio::time::sleep(Duration::from_secs(5)).await;
2023-11-29 12:25:04 +01:00
}
});
let mut bcmrdownloader =
BCMRDownloader::new(db.bcmr_w.clone(), config.riften_ipfs_gateway.clone());
bcmrdownloader.start()?;
let mut wellknowndownloader = WellKnownDownloader::new(db.bcmr_w.clone());
wellknowndownloader.start()?;
spawn_token_metrics_updater(db.clone(), indexing_in_progress.clone());
Ok((
db,
bcmrdownloader,
wellknowndownloader,
crc20fetcher,
ibd_state,
2026-03-25 11:38:10 +00:00
indexing_in_progress,
))
2024-02-14 11:11:16 +01:00
}
#[launch]
async fn launch() -> _ {
2024-02-14 11:11:16 +01:00
stderrlog::new()
.verbosity(LogLevelNum::Info)
2024-02-14 11:11:16 +01:00
.init()
.unwrap();
set_panic_hook();
let config = {
let (config, _extra) =
Config::including_optional_config_files(std::iter::empty::<std::ffi::OsString>())
.unwrap_or_exit();
config
};
2026-03-25 11:38:10 +00:00
let (
dbpool,
bcmrdownloader,
wellknowndownloader,
crc20fetcher,
ibd_state,
indexing_in_progress,
) = match start_program(config).await {
Ok(db) => db,
Err(e) => {
let backtrace = Backtrace::capture();
error!("Backtrace (if RUST_BACKTRACE=1):\n{backtrace}");
error!("Error: {e}");
panic!("Failed at program startup")
}
};
2024-01-19 14:38:31 +01:00
let allowed_origins = AllowedOrigins::all();
let cors = rocket_cors::CorsOptions {
allowed_origins,
allowed_methods: vec![rocket::http::Method::Get]
.into_iter()
.map(From::from)
.collect(),
allowed_headers: AllowedHeaders::some(&["Authorization", "Accept"]),
allow_credentials: true,
..Default::default()
}
.to_cors()
.unwrap();
create_cached_token_metrics_table(&dbpool.cauldron_w)
.await
.expect("ensure cached_token_metrics exists");
2025-09-05 13:53:10 +00:00
2026-03-25 11:38:10 +00:00
// Ensure the OHLCV pre-aggregation table exists (safe on both new and existing DBs).
db::cauldron::ohlcv::create_table(&dbpool.cauldron_w).await;
// Discard buckets materialised under a superseded pricing rule.
let ohlcv_wiped =
match db::cauldron::ohlcv::migrate_if_stale(&dbpool.cauldron_r, &dbpool.cauldron_w).await {
Ok(wiped) => {
if wiped {
info!(
"ohlcv: cleared for rebuild at version {}; \
serving the raw path until the background task catches up",
db::cauldron::ohlcv::OHLCV_VERSION
);
}
wiped
}
Err(e) => {
warn!("ohlcv: version check failed, leaving table as-is: {e}");
false
}
};
2026-03-25 11:38:10 +00:00
// Bootstrap OhlcvState from whatever is already in the table (survives restarts).
let max_bucket_ts = db::cauldron::ohlcv::get_max_bucket_ts(&dbpool.cauldron_r)
.await
.unwrap_or(None);
let initial_ohlcv_end = max_bucket_ts.map(|ts| ts + 3600).unwrap_or(0);
let ohlcv_state = Arc::new(OhlcvState {
materialized_end: AtomicI64::new(initial_ohlcv_end),
});
// Synchronous post-IBD backfill: run the full ohlcv_1h materialisation before
// allowing metrics_cache and other background writers to start. We reuse the
// indexing_in_progress flag so metrics_cache backs off during this window.
//
// Skipped after a version wipe: the backfill runs before `rocket::build()` returns,
// so re-materialising all of history here would refuse connections for the whole
// rebuild rather than degrading to the (correct, slower) raw path.
if !ohlcv_wiped {
2026-03-25 11:38:10 +00:00
const BACKFILL_BATCH_SECS: i64 = 24 * 3600;
const BACKFILL_SAFETY_SECS: i64 = 3 * 3600;
// Wait for IBD to finish — ohlcv_1h data is only useful for confirmed blocks.
while !ibd_state.initial_sync_complete.load(Ordering::Relaxed) {
tokio::time::sleep(Duration::from_secs(1)).await;
}
// Gate metrics_cache so it doesn't compete for cauldron_w during backfill.
indexing_in_progress.store(true, Ordering::Relaxed);
info!("ohlcv: starting post-IBD full backfill");
let now = crate::timeutil::time_now();
let cutoff = (now - BACKFILL_SAFETY_SECS) / 3600 * 3600;
let since_opt = match max_bucket_ts {
Some(ts) => Some(ts + 3600),
None => match db::cauldron::ohlcv::get_min_trade_bucket_ts(&dbpool.cauldron_r).await {
Ok(v) => v,
Err(e) => {
warn!("ohlcv backfill: could not read min trade ts: {e}");
None
}
},
};
if since_opt.is_none() {
info!("ohlcv backfill: no confirmed trades found, skipping");
}
if let Some(mut batch_start) = since_opt {
while batch_start < cutoff {
let batch_end = (batch_start + BACKFILL_BATCH_SECS).min(cutoff);
match db::cauldron::ohlcv::rebuild_range(
&dbpool.cauldron_r,
&dbpool.cauldron_w,
batch_start,
batch_end,
)
.await
{
Ok(n) => {
info!("ohlcv backfill: {n} buckets [{batch_start}, {batch_end})");
ohlcv_state
.materialized_end
.store(batch_end, Ordering::Relaxed);
}
Err(e) => {
warn!("ohlcv backfill failed at [{batch_start}, {batch_end}): {e}");
break;
}
}
batch_start = batch_end;
// Brief yield so new block writes are not starved.
tokio::time::sleep(Duration::from_millis(200)).await;
}
}
info!("ohlcv: post-IBD backfill complete");
indexing_in_progress.store(false, Ordering::Relaxed);
}
// Background task: incrementally materialise new 1-hour OHLCV buckets as blocks arrive.
// After a version wipe this is also what repopulates history, since the synchronous
// backfill above is skipped in that case.
2026-03-25 11:38:10 +00:00
// Only processes buckets older than 3 hours (well beyond BCH reorg depth).
{
let ohlcv_write = dbpool.cauldron_w.clone();
let ohlcv_read = dbpool.cauldron_r.clone();
let ohlcv_state_bg = ohlcv_state.clone();
let ohlcv_ibd = ibd_state.clone();
2026-03-25 11:38:10 +00:00
tokio::spawn(async move {
// Batch size: 1 day per SQL call to keep each write short.
const BATCH_SECS: i64 = 24 * 3600;
// Safety margin: only materialise buckets older than this many seconds.
const SAFETY_SECS: i64 = 3 * 3600;
// Wait for IBD before materialising anything.
//
// The synchronous backfill above waits too, but it is skipped whenever
// `ohlcv_wiped` is set — which includes every fresh database, since a missing
// version key reads as stale. Without this the task would sweep from the
// first trade all the way to `now - 3h` while indexing is still years behind,
// writing nothing, contending with block writes for the cauldron write lock,
// and advancing `materialized_end` to roughly now against an empty table — at
// which point `candlesticks()` would take the fast path over nothing.
while !ohlcv_ibd.initial_sync_complete.load(Ordering::Relaxed) {
tokio::time::sleep(Duration::from_secs(1)).await;
}
2026-03-25 11:38:10 +00:00
loop {
let now = crate::timeutil::time_now();
// Floor to 1-hour boundary, 3 hours ago.
let cutoff = (now - SAFETY_SECS) / 3600 * 3600;
let since = match db::cauldron::ohlcv::get_max_bucket_ts(&ohlcv_read).await {
Ok(Some(max_ts)) => max_ts + 3600,
Ok(None) => {
// Table is empty: start from the first confirmed trade rather than
// scanning from Unix epoch 0 through thousands of empty batches.
match db::cauldron::ohlcv::get_min_trade_bucket_ts(&ohlcv_read).await {
Ok(Some(min_ts)) => min_ts,
Ok(None) => {
// No confirmed trades yet; wait before retrying.
tokio::time::sleep(Duration::from_secs(60)).await;
continue;
}
Err(e) => {
error!("ohlcv rebuild (min trade ts): {e}");
tokio::time::sleep(Duration::from_secs(60)).await;
continue;
}
}
}
Err(e) => {
error!("ohlcv rebuild: {e}");
tokio::time::sleep(Duration::from_secs(60)).await;
continue;
}
};
let mut batch_start = since;
while batch_start < cutoff {
let batch_end = (batch_start + BATCH_SECS).min(cutoff);
match db::cauldron::ohlcv::rebuild_range(
&ohlcv_read,
&ohlcv_write,
batch_start,
batch_end,
)
.await
{
Ok(n) => {
info!("ohlcv: materialised {n} buckets [{batch_start}, {batch_end})");
ohlcv_state_bg
.materialized_end
.store(batch_end, Ordering::Relaxed);
}
Err(e) => {
error!("ohlcv rebuild failed: {e}");
break;
}
}
batch_start = batch_end;
// Yield between batches so block indexing writes can proceed.
tokio::time::sleep(Duration::from_millis(200)).await;
}
tokio::time::sleep(Duration::from_secs(600)).await;
}
});
}
2024-01-05 14:21:50 +01:00
rocket::build()
.attach(signal::ShutdownFairing)
.attach(signal::IbdCheckFairing)
2024-03-14 12:14:13 +01:00
.manage(dbpool)
.manage(ibd_state)
2026-03-25 11:38:10 +00:00
.manage(ohlcv_state)
2026-04-24 05:19:43 +00:00
// give rocket ownership of downloaders/fetchers to ensure threads aren't dropped
.manage(bcmrdownloader)
.manage(wellknowndownloader)
2024-10-21 12:48:47 +02:00
.manage(crc20fetcher)
2024-03-04 16:40:50 +01:00
.mount(
"/cauldron/",
2024-03-18 12:34:46 +01:00
routes![
2024-04-03 09:49:44 +02:00
rpc::tvl::deprecated_tvl,
rpc::tvl::valuelocked_token,
rpc::tvl::valuelocked_all,
rpc::volume::volume_all,
rpc::volume::volume_token,
2024-10-31 10:55:29 +00:00
rpc::tokens::search_by_volume,
2025-09-05 13:53:10 +00:00
rpc::tokens::search_cached,
rpc::tokens::list_cached,
rpc::tokens::list_cached_by_ids,
2024-04-03 11:39:49 +02:00
rpc::price::price_history,
2025-06-17 13:22:46 +00:00
rpc::candlesticks::price_candlesticks,
2024-04-03 11:39:49 +02:00
rpc::price::price_current,
2024-10-14 11:18:50 +00:00
rpc::price::price_at,
2024-04-03 21:40:33 +02:00
rpc::pool::list_active_pools,
2024-11-13 11:16:24 +01:00
rpc::pool::pool_history,
rpc::pool::pool_id_from_utxo,
rpc::apy::aggregate_apy,
2024-04-03 11:39:49 +02:00
rpc::contract::contract_count_token,
rpc::contract::contract_count_all,
rpc::contract::contract_volume,
2024-09-25 11:56:55 +02:00
rpc::user::unique_addresses,
rpc::tx::tx_latest,
2025-09-17 11:11:21 +02:00
rpc::tokens::first_pool_creation,
2024-03-18 12:34:46 +01:00
],
2024-03-04 16:40:50 +01:00
)
.mount(
"/bcmr",
routes![
rpc::bcmr::token_bcmr,
rpc::bcmr::token_bcmr_all,
rpc::bcmr::token_authhead
],
)
.mount(
"/oracle",
routes![
rpc::oracle::oracle_get_closest,
2025-06-17 14:04:26 +00:00
rpc::oracle::oracle_get_range,
2026-04-24 05:19:43 +00:00
rpc::oracle::oracle_get_history,
rpc::oracle::oracle_cash_closest,
rpc::oracle::oracle_cash_history,
],
)
.mount(
"/moria",
routes![
rpc::moria::loan_history,
rpc::moria::global_history,
rpc::moria::active_loans,
rpc::moria::moria_stats,
],
)
.mount(
"/tokentoken",
routes![
rpc::tokentoken::list_active_pools,
rpc::tokentoken::list_all_pools,
rpc::tokentoken::list_tokens,
],
)
tokentoken/tokenbch: index delegation pools, replace the pre-delegation AMM Replace the old P2SH32/CONJURE TokenToken AMM indexer (never in production; its tables are dropped by an always-run migration) with the delegation-model indexers from riftenlabs-defi 0.4.5: - tokentoken: two co-created bare-P2S UTXOs (thin main + storage sibling). tokenbch (new): single-UTXO token<->BCH pool with the runtime feePaidInToken fee side. Both admitted purely by the constant locking derived from the per-network delegation-pool platform NFTH. - src/db/orbconstants.rs: ONE platform NFTH serves both contracts — chipnet pins the deployed ORB v0 value (libriften orb/v0/chipnet.ts), mainnet is the all-zero placeholder: while unconfigured NOTHING delegation-related is parsed, fetched or indexed (fail-closed). - Schema: immutable config (nftOwner, tokens, poolFeeRate, minFee, virtualX/Y, tokenbch feePaidInToken) on the pool row; per-state history carries raw reserves, owed, platformFeeRate (both mutable by platform sweeps) and deltas. History rows now FK tx(txid) ON DELETE CASCADE, so evicted mempool txs clean up after themselves (the old tokentoken indexer leaked those). - LP teardowns carry no pool output: ingestion probes every blob-presenting tx's spent outpoints against the recorded pool set and flags a known pool UTXO spent without successor as withdrawn (withdrawn_in_txid FK ON DELETE SET NULL reverts an evicted teardown; reorg undo reverts a confirmed one). - electrum mempool filters: the combined logic blob (scriptsig; every spend incl. teardowns) + the constant thin-main locking (scriptpubkey; creations), per contract, skipped while unconfigured. - RPC: /tokentoken responses gain owed_a, platform/pool fee rates, minFee and virtual offsets; new /tokenbch (pool/active?token, pools, tokens) mirrors it for BCH pairs. - deps: bitcoincash 0.32.4 (token.amount -> .to_int() in the ido indexer), riftenlabs-defi 0.4.5. NOTE: 0.4.5 (rust-riftenlabs-defi 7c65f66) is not on crates.io yet — until it is published, build with a local [patch.crates-io] pointing at the sibling checkout (the committed Cargo.lock carries the path-patched entry). Also fixes a latent test bug (ido_params_nft_commitment_roundtrip never wrote createExecutionFee after the 127-byte layout shift). Verified: cargo build, test (232 passing), clippy (no new warnings) and fmt all clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 15:30:55 +00:00
.mount(
"/tokenbch",
routes![
rpc::tokenbch::list_active_pools,
rpc::tokenbch::list_all_pools,
rpc::tokenbch::list_tokens,
],
)
2026-06-22 19:48:09 +00:00
.mount(
"/ido",
routes![
rpc::ido::list_idos,
rpc::ido::get_ido_by_id,
rpc::ido::get_ido_by_offering_token,
rpc::ido::list_ido_entries,
],
)
2026-02-10 15:36:04 +01:00
.mount("/", routes![rpc::health::health])
2024-01-19 14:38:31 +01:00
.attach(cors)
2023-11-29 12:25:04 +01:00
}