Allow multiple idos per category, parse data, and add tests

This commit is contained in:
Jakob Notland 2026-03-31 11:41:58 +02:00
parent b97ec39fdb
commit 8e76a62d78
2 changed files with 980 additions and 141 deletions

File diff suppressed because it is too large Load diff

View file

@ -4,67 +4,55 @@
// 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, DB},
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;
use bitcoincash::{TokenID, Txid};
use rocket::{get, State};
use serde_json::{json, Value};
/// List all discovered IDOs.
///
/// **Response Example:**
/// ```json
/// {
/// "idos": [
/// {
/// "creation_txid": "abc123...",
/// "category": "def456...",
/// "params_raw": "...",
/// "discovered_at_mtp": 1700000000,
/// "phase": "fundraising"
/// }
/// ]
/// }
/// ```
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, category, params_raw, mtp, phase)| {
json!({
"creation_txid": txid,
"category": category,
"params_raw": hex::encode(params_raw),
"discovered_at_mtp": mtp,
"phase": phase,
})
.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 a specific IDO by token category.
/// Get all IDOs for a token category (offered_token_category) with parsed parameters.
///
/// - category: Token category hex (32 bytes)
///
/// **Response Example:**
/// ```json
/// {
/// "creation_txid": "abc123...",
/// "category": "def456...",
/// "params_raw": "...",
/// "discovered_at_mtp": 1700000000,
/// "phase": "fundraising"
/// }
/// ```
/// 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| {
@ -74,67 +62,52 @@ pub async fn get_ido(category: &str, conn: &State<DB>) -> CachedApiResult<Value>
)
})?;
let result = ido::get_ido(&conn.cauldron_r, &category)
let results = ido::get_idos_by_category(&conn.cauldron_r, &category)
.await
.map_err(db_error)?;
match result {
Some((txid, category_hex, params_raw, mtp, phase)) => Ok(cached_ok(
json!({
"creation_txid": txid,
"category": category_hex,
"params_raw": hex::encode(params_raw),
"discovered_at_mtp": mtp,
"phase": phase,
}),
CACHE_NONE,
)),
None => Err(not_found(
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 an IDO.
/// 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.
///
/// - category: Token category hex (32 bytes)
///
/// **Response Example:**
/// ```json
/// {
/// "entries": [
/// {
/// "txid": "abc123...",
/// "n": 0,
/// "sats": 10000,
/// "token_amount": 100
/// }
/// ]
/// }
/// ```
#[get("/ido/<category>/entries")]
pub async fn get_ido_entries(category: &str, conn: &State<DB>) -> CachedApiResult<Value> {
let category = TokenID::from_hex(category).map_err(|e| {
#[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 category: {e}"),
&format!("Invalid creation_txid: {e}"),
)
})?;
let entries = ido::get_ido_entries(&conn.cauldron_r, &category)
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)| {
.map(|(txid, n, sats, commitment)| {
json!({
"txid": txid,
"n": n,
"sats": sats,
"token_amount": token_amount,
"commitment": commitment,
})
})
.collect();