ido: maintain entry aggregates in active state

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>
This commit is contained in:
Hossein Zoda 2026-06-11 15:52:38 +00:00
parent b8468a1256
commit 9b0a54c926
3 changed files with 23 additions and 52 deletions

View file

@ -241,6 +241,16 @@ pub struct IdoActiveState {
idoCategory: Vec<u8>, idoCategory: Vec<u8>,
#[serde_as(as = "DisplayFromStr")] #[serde_as(as = "DisplayFromStr")]
counter: BigInt, counter: BigInt,
// Running totals over the IDO's entries, maintained while the IDO is ACTIVE
// so "raised so far" can be served from state instead of aggregating the
// ido_entry table on every request. Initialized to 0 at the preinit→active
// transition and incremented on each entry added.
#[serde_as(as = "DisplayFromStr")]
totalDemandAmount: BigInt,
#[serde_as(as = "DisplayFromStr")]
totalSupplyAmount: BigInt,
#[serde_as(as = "DisplayFromStr")]
totalDiscount: BigInt,
} }
#[serde_as] #[serde_as]
@ -1821,7 +1831,10 @@ fn ido_add_tx(
updates.push(IdoUpdate::State(IdoState::Active(IdoActiveState { updates.push(IdoUpdate::State(IdoState::Active(IdoActiveState {
authguardCategory: preinit_state.authguardCategory.clone(), authguardCategory: preinit_state.authguardCategory.clone(),
idoCategory: preinit_state.idoCategory.clone(), idoCategory: preinit_state.idoCategory.clone(),
counter: BigInt::from(0) counter: BigInt::from(0),
totalDemandAmount: BigInt::from(0),
totalSupplyAmount: BigInt::from(0),
totalDiscount: BigInt::from(0),
}))); })));
let tokenStorageOut = tx.output.get(2).ok_or_else(|| anyhow::anyhow!("token storage does not exist!"))?; let tokenStorageOut = tx.output.get(2).ok_or_else(|| anyhow::anyhow!("token storage does not exist!"))?;
let offeringOut = tx.output.get(1).ok_or_else(|| anyhow::anyhow!("offering does not exist!"))?; let offeringOut = tx.output.get(1).ok_or_else(|| anyhow::anyhow!("offering does not exist!"))?;
@ -1913,13 +1926,16 @@ fn ido_add_tx(
if second_output.token.as_ref().unwrap().commitment.len() < 15 { if second_output.token.as_ref().unwrap().commitment.len() < 15 {
return Err(anyhow::anyhow!("Incorrect commitment size at output#1")); return Err(anyhow::anyhow!("Incorrect commitment size at output#1"));
} }
let supply_amount = second_output.value.to_sat();
let demand_amount = second_output.token.as_ref().unwrap().amount as u64;
let discount = decode_padded_vm_number(&second_output.token.as_ref().unwrap().commitment[7..15]).to_u64().unwrap_or(0);
updates.push(IdoUpdate::Entry(IdoUpdateEntry { updates.push(IdoUpdate::Entry(IdoUpdateEntry {
txid: tx.compute_txid().to_blob(), txid: tx.compute_txid().to_blob(),
owner_nfthash: owner_nfthash, owner_nfthash: owner_nfthash,
supply_amount: second_output.value.to_sat(), supply_amount,
demand_amount: second_output.token.as_ref().unwrap().amount as u64, demand_amount,
lockup_timeval: decode_padded_vm_number(&second_output.token.as_ref().unwrap().commitment[1..7]).to_u64().unwrap_or(0), lockup_timeval: decode_padded_vm_number(&second_output.token.as_ref().unwrap().commitment[1..7]).to_u64().unwrap_or(0),
discount: decode_padded_vm_number(&second_output.token.as_ref().unwrap().commitment[7..15]).to_u64().unwrap_or(0), discount,
commitment: second_output.token.as_ref().unwrap().commitment.clone(), commitment: second_output.token.as_ref().unwrap().commitment.clone(),
distributed: false, distributed: false,
})); }));
@ -1927,6 +1943,9 @@ fn ido_add_tx(
authguardCategory: active_state.authguardCategory.clone(), authguardCategory: active_state.authguardCategory.clone(),
idoCategory: active_state.idoCategory.clone(), idoCategory: active_state.idoCategory.clone(),
counter: decode_padded_vm_number(&first_output.token.as_ref().unwrap().commitment[5..9]), counter: decode_padded_vm_number(&first_output.token.as_ref().unwrap().commitment[5..9]),
totalDemandAmount: &active_state.totalDemandAmount + BigInt::from(demand_amount),
totalSupplyAmount: &active_state.totalSupplyAmount + BigInt::from(supply_amount),
totalDiscount: &active_state.totalDiscount + BigInt::from(discount),
}))); })));
} else if (first_output.token.as_ref().unwrap().commitment[0] & ITEM_TYPE_BITS) == ITEM_TYPE_DISTRIBUTOR { } else if (first_output.token.as_ref().unwrap().commitment[0] & ITEM_TYPE_BITS) == ITEM_TYPE_DISTRIBUTOR {
// input#0 is the launcher // input#0 is the launcher
@ -2790,34 +2809,6 @@ pub async fn list_ido_entries(
.collect() .collect()
} }
#[derive(Serialize, Clone)]
pub struct IdoEntryAggregatesRpc {
pub total_demand_amount: i64,
pub total_supply_amount: i64,
pub entry_count: i64,
}
/// Aggregate totals over an IDO's entries. Used to show "raised so far" for
/// ACTIVE offerings, where on-chain state keeps no running total.
pub async fn get_ido_entry_aggregates(
pool: &SqlitePool,
internal_id: i64,
) -> Result<IdoEntryAggregatesRpc> {
let row = sqlx::query(
"SELECT COALESCE(SUM(demand_amount), 0) AS total_demand_amount, \
COALESCE(SUM(supply_amount), 0) AS total_supply_amount, \
COUNT(*) AS entry_count FROM ido_entry WHERE ido_id = ?",
)
.bind(internal_id)
.fetch_one(pool)
.await?;
Ok(IdoEntryAggregatesRpc {
total_demand_amount: row.get(0),
total_supply_amount: row.get(1),
entry_count: row.get(2),
})
}
pub async fn list_ido_txchain( pub async fn list_ido_txchain(
pool: &SqlitePool, pool: &SqlitePool,
internal_id: i64, internal_id: i64,

View file

@ -575,7 +575,6 @@ async fn launch() -> _ {
rpc::ido::get_ido_by_id, rpc::ido::get_ido_by_id,
rpc::ido::get_ido_by_offering_token, rpc::ido::get_ido_by_offering_token,
rpc::ido::list_ido_entries, rpc::ido::list_ido_entries,
rpc::ido::get_ido_entries_aggregates,
rpc::ido::get_txchain_tx, rpc::ido::get_txchain_tx,
]; ];
// Debug-only IDO endpoints, mounted only when `debug = true` in the config. // Debug-only IDO endpoints, mounted only when `debug = true` in the config.

View file

@ -175,25 +175,6 @@ pub async fn list_ido_entries(
Ok(cached_ok(serde_json::to_value(items).unwrap(), CACHE_NONE)) Ok(cached_ok(serde_json::to_value(items).unwrap(), CACHE_NONE))
} }
/// Aggregate totals over an IDO's entries (summed demand/supply and count).
///
/// - `id`: the IDO's public id (preinit txid, 64-char hex)
///
/// Returns `{"total_demand_amount", "total_supply_amount", "entry_count"}`.
#[get("/<id>/aggregates")]
pub async fn get_ido_entries_aggregates(
id: &str,
db: &State<DB>,
) -> CachedApiResult<Value> {
let (internal_id, _preinit_txid_hex) = resolve_ido(id, &db.ido_r).await?;
let agg = crate::db::ido::get_ido_entry_aggregates(&db.ido_r, internal_id)
.await
.map_err(db_error)?;
Ok(cached_ok(serde_json::to_value(agg).unwrap(), CACHE_NONE))
}
/// Get the raw transaction hex for a txchain item by its txid. /// Get the raw transaction hex for a txchain item by its txid.
/// Returns `{"tx": "<hex>"}` or `{"tx": null}` if not found. /// Returns `{"tx": "<hex>"}` or `{"tx": null}` if not found.
#[get("/txchain/<txid>/tx")] #[get("/txchain/<txid>/tx")]