Merge branch 'idoIndexer' into 'master'

Draft: What we need to keep track of idos.

See merge request riftenlabs/riftenlabs-indexer!79
This commit is contained in:
jakobsn 2026-03-31 10:41:42 +00:00
commit 00588e2021
7 changed files with 1262 additions and 0 deletions

1132
src/db/cauldron/ido.rs Normal file

File diff suppressed because it is too large Load diff

View file

@ -15,6 +15,7 @@ use crate::db::cauldron::tokenlist::db_utils::{
pub mod config;
pub mod header;
pub mod ido;
pub mod mempool;
pub mod ohlcv;
pub mod pool;
@ -45,6 +46,7 @@ pub async fn prepare_tables(pool: &SqlitePool) {
.unwrap();
pool::create_table(pool).await;
ido::create_tables(pool).await;
sqlx::query(
"CREATE INDEX idx_utxo_funding_join ON utxo_funding(new_utxo_hash, sats, token_id);",

View file

@ -13,6 +13,7 @@ use std::time::Duration;
use super::DB;
use crate::db::bcmr::prepare_tables as bcmr_prepare_tables;
use crate::db::cauldron::config::check_db_version;
use crate::db::cauldron::ido;
use crate::db::cauldron::prepare_tables as cauldron_prepare_tables;
use crate::db::crc20::prepare_tables as crc20_prepare_tables;
use crate::db::oracle::prepare_tables as oracle_prepare_tables;
@ -100,6 +101,9 @@ pub async fn initialize_databases(network: &str, read_slots: ReadSlots) -> Resul
cauldron_prepare_tables(&cauldron_db_write).await;
} else {
check_db_version(&cauldron_db_read).await?;
// Run IDO table migration/creation separately so that schema additions
// take effect on existing databases without requiring a full DB wipe.
ido::create_tables(&cauldron_db_write).await;
}
// Initialize BCMR database

View file

@ -356,6 +356,9 @@ pub async fn index_blocks(
// last successful block indexed was. It must commit atomically with block data.
config_set(&mut *db_tx, KEY_LAST_INDEXED, &blockhash.to_hex()).await;
// ido
db::cauldron::ido::index_ido(&mut *db_tx, &sorted_txs, mtp as i64).await?;
// crc20
index_crc20(&db.crc20_w, &sorted_txs).await?;

View file

@ -588,6 +588,9 @@ async fn launch() -> _ {
rpc::pool::list_active_pools,
rpc::pool::pool_history,
rpc::pool::pool_id_from_utxo,
rpc::ido::list_idos,
rpc::ido::get_ido,
rpc::ido::get_ido_entries,
rpc::apy::aggregate_apy,
rpc::contract::contract_count_token,
rpc::contract::contract_count_all,

117
src/rpc/ido.rs Normal file
View file

@ -0,0 +1,117 @@
// Copyright (C) 2024-2026 Whiterun LLC
//
// 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 crate::{
db::{cauldron::ido::{self, deserialize_ido_params}, DB},
rpc::{
err::{bad_request, db_error, not_found, ApiErrorCode, CachedApiResult},
response::{cached_ok, CACHE_NONE},
},
};
use bitcoin_hashes::hex::FromHex;
use bitcoincash::{TokenID, Txid};
use rocket::{get, State};
use serde_json::{json, Value};
fn format_ido(
creation_txid: String,
offered_token_category: String,
offering_category: Option<String>,
params_raw: Vec<u8>,
mtp: i64,
phase: String,
) -> Value {
let params = deserialize_ido_params(&params_raw);
json!({
"creation_txid": creation_txid,
"offered_token_category": offered_token_category,
"offering_category": offering_category,
"params_raw": hex::encode(&params_raw),
"params": params,
"discovered_at_mtp": mtp,
"phase": phase,
})
}
/// List all discovered IDOs with parsed parameters.
#[get("/ido")]
pub async fn list_idos(conn: &State<DB>) -> CachedApiResult<Value> {
let idos = ido::list_idos(&conn.cauldron_r).await.map_err(db_error)?;
let idos_json: Vec<Value> = idos
.into_iter()
.map(|(txid, offered_cat, offering_cat, params_raw, mtp, phase)| {
format_ido(txid, offered_cat, offering_cat, params_raw, mtp, phase)
})
.collect();
Ok(cached_ok(json!({ "idos": idos_json }), CACHE_NONE))
}
/// Get all IDOs for a token category (offered_token_category) with parsed parameters.
///
/// Returns a list — multiple IDOs per token are allowed.
#[get("/ido/<category>")]
pub async fn get_ido(category: &str, conn: &State<DB>) -> CachedApiResult<Value> {
let category = TokenID::from_hex(category).map_err(|e| {
bad_request(
ApiErrorCode::InvalidTokenId,
&format!("Invalid category: {e}"),
)
})?;
let results = ido::get_idos_by_category(&conn.cauldron_r, &category)
.await
.map_err(db_error)?;
if results.is_empty() {
return Err(not_found(
ApiErrorCode::PoolNotFound,
&format!("No IDO found for category: {category}"),
));
}
let idos_json: Vec<Value> = results
.into_iter()
.map(|(txid, offered_cat, offering_cat, params_raw, mtp, phase)| {
format_ido(txid, offered_cat, offering_cat, params_raw, mtp, phase)
})
.collect();
Ok(cached_ok(json!({ "idos": idos_json }), CACHE_NONE))
}
/// Get active (unspent) entry NFT UTXOs for a specific IDO.
///
/// `creation_txid` is the txid of the IDO announcement transaction.
/// Used by the distribution robot to find which entries still need to be processed.
#[get("/ido/<creation_txid>/entries")]
pub async fn get_ido_entries(creation_txid: &str, conn: &State<DB>) -> CachedApiResult<Value> {
let creation_txid = Txid::from_hex(creation_txid).map_err(|e| {
bad_request(
ApiErrorCode::InvalidTokenId,
&format!("Invalid creation_txid: {e}"),
)
})?;
let entries = ido::get_ido_entries(&conn.cauldron_r, &creation_txid)
.await
.map_err(db_error)?;
let entries_json: Vec<Value> = entries
.into_iter()
.map(|(txid, n, sats, token_amount, commitment)| {
json!({
"txid": txid,
"n": n,
"sats": sats,
"token_amount": token_amount,
"commitment": commitment,
})
})
.collect();
Ok(cached_ok(json!({ "entries": entries_json }), CACHE_NONE))
}

View file

@ -17,6 +17,7 @@ pub mod candlesticks;
pub mod contract;
pub mod err;
pub mod health;
pub mod ido;
pub mod oracle;
pub mod pool;
pub mod price;