diff --git a/src/db/ido/mod.rs b/src/db/ido/mod.rs index 72e60eb..aa22cbe 100644 --- a/src/db/ido/mod.rs +++ b/src/db/ido/mod.rs @@ -677,7 +677,7 @@ pub fn pad_minimally_encoded_vm_number(bin: &[u8], length: usize) -> Vec { pub async fn prepare_tables(pool: &SqlitePool) { sqlx::query( "CREATE TABLE ido ( - id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + internal_id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, preinit_txid BLOB NOT NULL UNIQUE, init_txid BLOB NULL, launch_txid BLOB NULL, @@ -728,7 +728,7 @@ pub async fn prepare_tables(pool: &SqlitePool) { "CREATE TABLE ido_txchain ( id INTEGER NOT NULL PRIMARY KEY, prev_id INTEGER NULL, - ido_id INTEGER NOT NULL REFERENCES ido(id), + ido_id INTEGER NOT NULL REFERENCES ido(internal_id), txid BLOB NOT NULL UNIQUE, tx BLOB NOT NULL )", @@ -771,7 +771,7 @@ pub struct IdoEntryDBRecord { #[derive(Debug, Clone, serde::Serialize)] pub struct IdoDBRecord { - id: i64, + internal_id: i64, preinit_txid: Vec, init_txid: Option>, launch_txid: Option>, @@ -788,7 +788,7 @@ pub struct IdoDBRecord { impl IdoDBRecord { fn from_row(row: &sqlx::sqlite::SqliteRow) -> Result { Ok(Self { - id: row.get(0), + internal_id: row.get(0), preinit_txid: row.get(1), init_txid: row.get(2), launch_txid: row.get(3), @@ -827,13 +827,13 @@ impl IdoTxChainDBRecord { async fn get_ido_txchain_list( pool: &SqlitePool, - ido_id: i64, + internal_id: i64, ) -> Result> { sqlx::query( "SELECT id, prev_id, ido_id, txid, tx FROM ido_txchain WHERE ido_id = ?" ) - .bind(ido_id) + .bind(internal_id) .fetch_all(pool) .await? .iter() @@ -881,14 +881,14 @@ async fn get_txchain_item_by_txid( async fn ido_lookup( pool: &SqlitePool, - ido_id: i64, + internal_id: i64, ) -> Result> { let row = sqlx::query( - "SELECT id, preinit_txid, init_txid, launch_txid, offering_token_id, offered_token_id, + "SELECT internal_id, preinit_txid, init_txid, launch_txid, offering_token_id, offered_token_id, status, parameters, state, txchain_entrypoint, txchain_head, is_valid, is_token_created_at_preinit - FROM ido WHERE id = ?", + FROM ido WHERE internal_id = ?", ) - .bind(ido_id) + .bind(internal_id) .fetch_optional(pool) .await?; if let Some(row) = row { @@ -1533,16 +1533,16 @@ async fn on_create_ido( match result { Ok(r) => { // insert txchain entrypoint - let ido_id = r.last_insert_rowid(); - let txchain_item_id = upsert_ido_txchain(&mut dbtx, tx, ido_id, None::, block_height, 1) + let internal_id = r.last_insert_rowid(); + let txchain_item_id = upsert_ido_txchain(&mut dbtx, tx, internal_id, None::, block_height, 1) .await?; sqlx::query( "UPDATE ido SET txchain_entrypoint = ?, txchain_head = ? - WHERE id = ?", + WHERE internal_id = ?", ) .bind(txchain_item_id) .bind(txchain_item_id) - .bind(ido_id) + .bind(internal_id) .execute(&mut *dbtx) .await?; dbtx.commit().await?; @@ -1934,13 +1934,13 @@ fn ido_add_tx( async fn update_ido( dbtx: &mut sqlx::Transaction<'_, sqlx::Sqlite>, - ido_id: i64, + internal_id: i64, context: &IdoContext, txchain_head: i64, ) -> Result<()> { sqlx::query( "UPDATE ido set init_txid = ?, launch_txid = ?, status = ?, parameters = ?, state = ?, - offering_token_id = ?, offered_token_id = ?, txchain_head = ?, is_valid = ? WHERE id = ?", + offering_token_id = ?, offered_token_id = ?, txchain_head = ?, is_valid = ? WHERE internal_id = ?", ) .bind(&context.init_txid) .bind(&context.launch_txid) @@ -1951,7 +1951,7 @@ async fn update_ido( .bind(&context.offered_token_id) .bind(txchain_head) .bind(context.is_valid_ido) - .bind(ido_id) + .bind(internal_id) .execute(&mut **dbtx) .await?; Ok(()) @@ -1991,7 +1991,7 @@ fn apply_updates_to_context( async fn upsert_updates_to_ido_entries( dbtx: &mut sqlx::Transaction<'_, sqlx::Sqlite>, - ido_id: i64, + internal_id: i64, updates: &[IdoUpdate], ) -> Result<()> { let mut insert_list: Vec = Vec::new(); @@ -2018,7 +2018,7 @@ async fn upsert_updates_to_ido_entries( "INSERT INTO ido_entry (ido_id, txid, owner_nfthash, commitment, supply_amount, demand_amount, lockup_timeval, discount, distributed) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", ) - .bind(ido_id) + .bind(internal_id) .bind(entry.txid) .bind(entry.owner_nfthash) .bind(entry.commitment) @@ -2045,7 +2045,7 @@ async fn upsert_updates_to_ido_entries( async fn upsert_ido_txchain( dbtx: &mut sqlx::Transaction<'_, sqlx::Sqlite>, tx: &Transaction, - ido_id: i64, + internal_id: i64, prev_txchain_id: Option, block_height: i64, next_output_index: i32, @@ -2056,7 +2056,7 @@ async fn upsert_ido_txchain( (?, ?, ?, ?)" ) .bind(prev_txchain_id) - .bind(ido_id) + .bind(internal_id) .bind(tx.txid().to_blob()) .bind(serialized_tx) .execute(&mut **dbtx) @@ -2110,7 +2110,7 @@ async fn on_add_ido_tx( .await?; } else if prev_txchain_item.id != ido.txchain_head.unwrap_or(0) { // rebuild the state, txchain is broken, recreate the chain - let items = get_ido_txchain_list(pool, ido.id) + let items = get_ido_txchain_list(pool, ido.internal_id) .await?; let mut items_tx_map: HashMap, Transaction> = HashMap::new(); for item in &items { @@ -2148,19 +2148,19 @@ async fn on_add_ido_tx( let result = ido_add_tx(&context, tx)?; apply_updates_to_context(&mut context, &result.updates); updates.extend_from_slice(&result.updates); - let txchain_item_id = upsert_ido_txchain(&mut dbtx, tx, ido.id, Some(prev_txchain_item.id), block_height, result.next_output_index) + let txchain_item_id = upsert_ido_txchain(&mut dbtx, tx, ido.internal_id, Some(prev_txchain_item.id), block_height, result.next_output_index) .await?; - update_ido(&mut dbtx, ido.id, &context, txchain_item_id) + update_ido(&mut dbtx, ido.internal_id, &context, txchain_item_id) .await?; // delete all entries sqlx::query( "DELETE FROM ido_entry WHERE ido_id = ?", ) - .bind(ido.id) + .bind(ido.internal_id) .execute(&mut *dbtx) .await?; // insert the entries of the new state - upsert_updates_to_ido_entries(&mut dbtx, ido.id, &updates) + upsert_updates_to_ido_entries(&mut dbtx, ido.internal_id, &updates) .await?; } else { // create context from ido @@ -2183,11 +2183,11 @@ async fn on_add_ido_tx( // add to the chain let result = ido_add_tx(&context, tx)?; apply_updates_to_context(&mut context, &result.updates); - let txchain_item_id = upsert_ido_txchain(&mut dbtx, tx, ido.id, Some(prev_txchain_item.id), block_height, result.next_output_index) + let txchain_item_id = upsert_ido_txchain(&mut dbtx, tx, ido.internal_id, Some(prev_txchain_item.id), block_height, result.next_output_index) .await?; - update_ido(&mut dbtx, ido.id, &context, txchain_item_id) + update_ido(&mut dbtx, ido.internal_id, &context, txchain_item_id) .await?; - upsert_updates_to_ido_entries(&mut dbtx, ido.id, &result.updates) + upsert_updates_to_ido_entries(&mut dbtx, ido.internal_id, &result.updates) .await?; } dbtx.commit().await?; @@ -2262,7 +2262,7 @@ pub async fn index_block( #[derive(Serialize, Clone)] pub struct IdoRpcRecord { - pub id: i64, + pub id: String, pub preinit_txid: String, pub init_txid: Option, pub launch_txid: Option, @@ -2278,7 +2278,7 @@ pub struct IdoRpcRecord { #[derive(Serialize, Clone)] pub struct IdoEntryRpcRecord { - pub ido_id: i64, + pub ido_id: String, pub txid: String, pub owner_nfthash: String, pub commitment: Option, @@ -2293,7 +2293,8 @@ pub struct IdoEntryRpcRecord { pub struct IdoTxChainRpcRecord { pub id: i64, pub prev_id: Option, - pub ido_id: i64, + pub ido_id: String, + pub ido_internal_id: i64, pub txid: String, } @@ -2307,9 +2308,10 @@ pub struct IdoTrackerMapRpcRecord { impl IdoDBRecord { fn into_rpc_record(self) -> Result { + let preinit_txid_hex = blob_to_display_hex::(&self.preinit_txid)?; Ok(IdoRpcRecord { - id: self.id, - preinit_txid: blob_to_display_hex::(&self.preinit_txid)?, + id: preinit_txid_hex.clone(), + preinit_txid: preinit_txid_hex, init_txid: self.init_txid.as_deref().map(blob_to_display_hex::).transpose()?, launch_txid: self.launch_txid.as_deref().map(blob_to_display_hex::).transpose()?, offering_token_id: self.offering_token_id.as_deref().map(blob_to_display_hex::).transpose()?, @@ -2398,6 +2400,17 @@ mod tests { // ─── Public RPC query functions ────────────────────────────────────────────── +pub async fn lookup_internal_id_by_preinit_txid( + pool: &SqlitePool, + preinit_txid: &[u8], +) -> Result> { + let row = sqlx::query("SELECT internal_id FROM ido WHERE preinit_txid = ?") + .bind(preinit_txid) + .fetch_optional(pool) + .await?; + Ok(row.map(|r| r.get(0))) +} + pub async fn list_idos( pool: &SqlitePool, is_valid: Option, @@ -2407,7 +2420,7 @@ pub async fn list_idos( limit: i64, ) -> Result> { let mut qb: sqlx::QueryBuilder = sqlx::QueryBuilder::new( - "SELECT id, preinit_txid, init_txid, launch_txid, offering_token_id, offered_token_id, \ + "SELECT internal_id, preinit_txid, init_txid, launch_txid, offering_token_id, offered_token_id, \ status, parameters, state, txchain_entrypoint, txchain_head, is_valid, is_token_created_at_preinit FROM ido WHERE 1=1", ); @@ -2423,7 +2436,7 @@ pub async fn list_idos( qb.push(" AND offered_token_id = "); qb.push_bind(v); } - qb.push(" ORDER BY id ASC LIMIT "); + qb.push(" ORDER BY internal_id ASC LIMIT "); qb.push_bind(limit); qb.push(" OFFSET "); qb.push_bind(offset); @@ -2441,7 +2454,7 @@ pub async fn get_ido_by_offering_token_id( offering_token_id_blob: Vec, ) -> Result> { let row = sqlx::query( - "SELECT id, preinit_txid, init_txid, launch_txid, offering_token_id, offered_token_id, \ + "SELECT internal_id, preinit_txid, init_txid, launch_txid, offering_token_id, offered_token_id, \ status, parameters, state, txchain_entrypoint, txchain_head, is_valid, is_token_created_at_preinit \ FROM ido WHERE offering_token_id = ?", ) @@ -2453,16 +2466,17 @@ pub async fn get_ido_by_offering_token_id( pub async fn list_ido_entries( pool: &SqlitePool, - ido_id: i64, + internal_id: i64, + preinit_txid_hex: &str, distributed: Option, offset: i64, limit: i64, ) -> Result> { let mut qb: sqlx::QueryBuilder = sqlx::QueryBuilder::new( - "SELECT ido_id, txid, owner_nfthash, commitment, supply_amount, demand_amount, \ + "SELECT txid, owner_nfthash, commitment, supply_amount, demand_amount, \ lockup_timeval, discount, distributed FROM ido_entry WHERE ido_id = ", ); - qb.push_bind(ido_id); + qb.push_bind(internal_id); if let Some(v) = distributed { qb.push(" AND distributed = "); qb.push_bind(v as i64); @@ -2477,19 +2491,19 @@ pub async fn list_ido_entries( .await? .into_iter() .map(|row| { - let txid_blob: Vec = row.get(1); - let owner_nfthash: Vec = row.get(2); - let commitment: Option> = row.get(3); - let distributed_int: i64 = row.get(8); + let txid_blob: Vec = row.get(0); + let owner_nfthash: Vec = row.get(1); + let commitment: Option> = row.get(2); + let distributed_int: i64 = row.get(7); Ok(IdoEntryRpcRecord { - ido_id: row.get(0), + ido_id: preinit_txid_hex.to_string(), txid: blob_to_display_hex::(&txid_blob)?, owner_nfthash: hex::encode(&owner_nfthash), commitment: commitment.map(|c| hex::encode(&c)), - supply_amount: row.get(4), - demand_amount: row.get(5), - lockup_timeval: row.get(6), - discount: row.get(7), + supply_amount: row.get(3), + demand_amount: row.get(4), + lockup_timeval: row.get(5), + discount: row.get(6), distributed: distributed_int != 0, }) }) @@ -2498,12 +2512,13 @@ pub async fn list_ido_entries( pub async fn list_ido_txchain( pool: &SqlitePool, - ido_id: i64, + internal_id: i64, + preinit_txid_hex: &str, offset: i64, limit: i64, ) -> Result> { - let head_id: Option = sqlx::query("SELECT txchain_head FROM ido WHERE id = ?") - .bind(ido_id) + let head_id: Option = sqlx::query("SELECT txchain_head FROM ido WHERE internal_id = ?") + .bind(internal_id) .fetch_optional(pool) .await? .and_then(|row| row.get(0)); @@ -2514,9 +2529,9 @@ pub async fn list_ido_txchain( }; let rows = sqlx::query( - "SELECT id, prev_id, ido_id, txid FROM ido_txchain WHERE ido_id = ?", + "SELECT id, prev_id, txid FROM ido_txchain WHERE ido_id = ?", ) - .bind(ido_id) + .bind(internal_id) .fetch_all(pool) .await?; @@ -2524,11 +2539,12 @@ pub async fn list_ido_txchain( let records: Vec = rows .iter() .map(|row| { - let txid_blob: Vec = row.get(3); + let txid_blob: Vec = row.get(2); Ok(IdoTxChainRpcRecord { id: row.get(0), prev_id: row.get(1), - ido_id: row.get(2), + ido_id: preinit_txid_hex.to_string(), + ido_internal_id: internal_id, txid: blob_to_display_hex::(&txid_blob)?, }) }) @@ -2851,9 +2867,13 @@ mod querytests { assert_eq!(page1.len(), 2); assert_eq!(page2.len(), 2); assert_eq!(page3.len(), 1); - // IDs are ascending - assert!(page1[0].id < page1[1].id); - assert!(page1[1].id < page2[0].id); + // Pages reflect internal_id ascending insertion order: preinit_txid hex of txid(i) bytes + // For txid(i) = [i; 32], the display hex is "ii".repeat(32). + assert_eq!(page1[0].id, hex::encode(txid(1))); + assert_eq!(page1[1].id, hex::encode(txid(2))); + assert_eq!(page2[0].id, hex::encode(txid(3))); + assert_eq!(page2[1].id, hex::encode(txid(4))); + assert_eq!(page3[0].id, hex::encode(txid(5))); } // ─── DB: get_ido_by_offering_token_id ──────────────────────────────────── @@ -2897,9 +2917,10 @@ mod querytests { async fn list_ido_entries_all() { let pool = make_pool().await; let ido_id = insert_test_ido(&pool, txid(1), "ACTIVE", true, true, None, None).await; + let preinit_hex = hex::encode(txid(1)); insert_test_entry(&pool, ido_id, txid(10), false).await; insert_test_entry(&pool, ido_id, txid(11), true).await; - let all = list_ido_entries(&pool, ido_id, None, 0, 100).await.unwrap(); + let all = list_ido_entries(&pool, ido_id, &preinit_hex, None, 0, 100).await.unwrap(); assert_eq!(all.len(), 2); } @@ -2907,13 +2928,14 @@ mod querytests { async fn list_ido_entries_filter_distributed() { let pool = make_pool().await; let ido_id = insert_test_ido(&pool, txid(1), "ACTIVE", true, true, None, None).await; + let preinit_hex = hex::encode(txid(1)); insert_test_entry(&pool, ido_id, txid(10), false).await; insert_test_entry(&pool, ido_id, txid(11), true).await; insert_test_entry(&pool, ido_id, txid(12), true).await; - let dist = list_ido_entries(&pool, ido_id, Some(true), 0, 100).await.unwrap(); + let dist = list_ido_entries(&pool, ido_id, &preinit_hex, Some(true), 0, 100).await.unwrap(); assert_eq!(dist.len(), 2); assert!(dist.iter().all(|e| e.distributed)); - let undist = list_ido_entries(&pool, ido_id, Some(false), 0, 100).await.unwrap(); + let undist = list_ido_entries(&pool, ido_id, &preinit_hex, Some(false), 0, 100).await.unwrap(); assert_eq!(undist.len(), 1); assert!(!undist[0].distributed); } @@ -2923,11 +2945,13 @@ mod querytests { let pool = make_pool().await; let ido1 = insert_test_ido(&pool, txid(1), "ACTIVE", true, true, None, None).await; let ido2 = insert_test_ido(&pool, txid(2), "ACTIVE", true, true, None, None).await; + let preinit_hex_1 = hex::encode(txid(1)); + let preinit_hex_2 = hex::encode(txid(2)); insert_test_entry(&pool, ido1, txid(10), false).await; insert_test_entry(&pool, ido1, txid(11), false).await; insert_test_entry(&pool, ido2, txid(12), false).await; - let e1 = list_ido_entries(&pool, ido1, None, 0, 100).await.unwrap(); - let e2 = list_ido_entries(&pool, ido2, None, 0, 100).await.unwrap(); + let e1 = list_ido_entries(&pool, ido1, &preinit_hex_1, None, 0, 100).await.unwrap(); + let e2 = list_ido_entries(&pool, ido2, &preinit_hex_2, None, 0, 100).await.unwrap(); assert_eq!(e1.len(), 2); assert_eq!(e2.len(), 1); } @@ -2954,7 +2978,7 @@ mod querytests { } async fn set_txchain_head(pool: &SqlitePool, ido_id: i64, head_id: i64) { - sqlx::query("UPDATE ido SET txchain_head = ? WHERE id = ?") + sqlx::query("UPDATE ido SET txchain_head = ? WHERE internal_id = ?") .bind(head_id) .bind(ido_id) .execute(pool) @@ -2966,12 +2990,13 @@ mod querytests { async fn list_ido_txchain_ordered_oldest_first() { let pool = make_pool().await; let ido_id = insert_test_ido(&pool, txid(1), "ACTIVE", true, true, None, None).await; + let preinit_hex = hex::encode(txid(1)); let id_a = insert_txchain_item(&pool, ido_id, txid(10), None).await; let id_b = insert_txchain_item(&pool, ido_id, txid(11), Some(id_a)).await; let id_c = insert_txchain_item(&pool, ido_id, txid(12), Some(id_b)).await; set_txchain_head(&pool, ido_id, id_c).await; - let chain = list_ido_txchain(&pool, ido_id, 0, 100).await.unwrap(); + let chain = list_ido_txchain(&pool, ido_id, &preinit_hex, 0, 100).await.unwrap(); assert_eq!(chain.len(), 3); assert_eq!(chain[0].id, id_a); assert_eq!(chain[1].id, id_b); @@ -2982,14 +3007,15 @@ mod querytests { async fn list_ido_txchain_pagination() { let pool = make_pool().await; let ido_id = insert_test_ido(&pool, txid(1), "ACTIVE", true, true, None, None).await; + let preinit_hex = hex::encode(txid(1)); let id_a = insert_txchain_item(&pool, ido_id, txid(10), None).await; let id_b = insert_txchain_item(&pool, ido_id, txid(11), Some(id_a)).await; let id_c = insert_txchain_item(&pool, ido_id, txid(12), Some(id_b)).await; let id_d = insert_txchain_item(&pool, ido_id, txid(13), Some(id_c)).await; set_txchain_head(&pool, ido_id, id_d).await; - let page1 = list_ido_txchain(&pool, ido_id, 0, 2).await.unwrap(); - let page2 = list_ido_txchain(&pool, ido_id, 2, 2).await.unwrap(); + let page1 = list_ido_txchain(&pool, ido_id, &preinit_hex, 0, 2).await.unwrap(); + let page2 = list_ido_txchain(&pool, ido_id, &preinit_hex, 2, 2).await.unwrap(); assert_eq!(page1.len(), 2); assert_eq!(page2.len(), 2); assert_eq!(page1[0].id, id_a); @@ -3002,15 +3028,16 @@ mod querytests { async fn list_ido_txchain_no_head_returns_empty() { let pool = make_pool().await; let ido_id = insert_test_ido(&pool, txid(1), "ACTIVE", true, true, None, None).await; + let preinit_hex = hex::encode(txid(1)); // txchain_head is NULL (default from insert_test_ido) - let chain = list_ido_txchain(&pool, ido_id, 0, 100).await.unwrap(); + let chain = list_ido_txchain(&pool, ido_id, &preinit_hex, 0, 100).await.unwrap(); assert_eq!(chain.len(), 0); } #[rocket::async_test] async fn list_ido_txchain_unknown_ido_returns_empty() { let pool = make_pool().await; - let chain = list_ido_txchain(&pool, 9999, 0, 100).await.unwrap(); + let chain = list_ido_txchain(&pool, 9999, "deadbeef", 0, 100).await.unwrap(); assert_eq!(chain.len(), 0); } } diff --git a/src/rpc/err.rs b/src/rpc/err.rs index f24f97c..31b3ff8 100644 --- a/src/rpc/err.rs +++ b/src/rpc/err.rs @@ -35,6 +35,7 @@ pub enum ApiErrorCode { // 404 Not Found codes PriceNotFound, PoolNotFound, + IdoNotFound, // 500 Internal Server Error codes InternalError, @@ -61,6 +62,7 @@ impl fmt::Display for ApiErrorCode { // 404 codes Self::PriceNotFound => "PRICE_NOT_FOUND", Self::PoolNotFound => "POOL_NOT_FOUND", + Self::IdoNotFound => "IDO_NOT_FOUND", // 500 codes Self::InternalError => "INTERNAL_ERROR", // 503 codes diff --git a/src/rpc/ido.rs b/src/rpc/ido.rs index 170e878..18d56ff 100644 --- a/src/rpc/ido.rs +++ b/src/rpc/ido.rs @@ -3,11 +3,11 @@ // 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::blob::display_hex_to_blob; +use crate::db::blob::{blob_to_display_hex, display_hex_to_blob}; use crate::db::DB; -use crate::rpc::err::{bad_request, db_error, ApiErrorCode, CachedApiResult}; +use crate::rpc::err::{bad_request, db_error, not_found, ApiErrorCode, CachedApiResult}; use crate::rpc::response::{cached_ok, CACHE_NONE}; -use bitcoincash::TokenID; +use bitcoincash::{TokenID, Txid}; use rocket::{get, State}; use serde_json::Value; @@ -78,13 +78,13 @@ pub async fn get_ido_by_offering_token( /// List entries for an IDO. /// -/// - `ido_id`: the integer IDO ID from the ido table +/// - `id`: the IDO's public id (preinit txid, 64-char hex) /// - `distributed`: optional boolean filter /// /// Pagination: `offset` (default 0), `limit` (default 20, max 100) -#[get("//entries?&&")] +#[get("//entries?&&")] pub async fn list_ido_entries( - ido_id: i64, + id: &str, distributed: Option, offset: Option, limit: Option, @@ -93,7 +93,9 @@ pub async fn list_ido_entries( let offset = offset.unwrap_or(0).max(0); let limit = limit.unwrap_or(LIST_DEFAULT_LIMIT).clamp(1, LIST_MAX_LIMIT); - let items = crate::db::ido::list_ido_entries(&db.ido_r, ido_id, distributed, offset, limit) + let (internal_id, preinit_txid_hex) = resolve_ido(id, &db.ido_r).await?; + + let items = crate::db::ido::list_ido_entries(&db.ido_r, internal_id, &preinit_txid_hex, distributed, offset, limit) .await .map_err(db_error)?; @@ -102,10 +104,12 @@ pub async fn list_ido_entries( /// [Debug] List the txchain for an IDO, sorted oldest-first (head is last). /// +/// - `id`: the IDO's public id (preinit txid, 64-char hex) +/// /// Pagination: `offset` (default 0), `limit` (default 100, max 10000) -#[get("//txchain?&")] +#[get("//txchain?&")] pub async fn list_ido_txchain( - ido_id: i64, + id: &str, offset: Option, limit: Option, db: &State, @@ -113,13 +117,37 @@ pub async fn list_ido_txchain( let offset = offset.unwrap_or(0).max(0); let limit = limit.unwrap_or(DEBUG_DEFAULT_LIMIT).clamp(1, DEBUG_MAX_LIMIT); - let items = crate::db::ido::list_ido_txchain(&db.ido_r, ido_id, offset, limit) + let (internal_id, preinit_txid_hex) = resolve_ido(id, &db.ido_r).await?; + + let items = crate::db::ido::list_ido_txchain(&db.ido_r, internal_id, &preinit_txid_hex, offset, limit) .await .map_err(db_error)?; Ok(cached_ok(serde_json::to_value(items).unwrap(), CACHE_NONE)) } +async fn resolve_ido( + id: &str, + pool: &sqlx::SqlitePool, +) -> Result<(i64, String), rocket::response::status::Custom>> { + let preinit_blob = display_hex_to_blob::(id).map_err(|e| { + bad_request( + ApiErrorCode::InvalidParameters, + &format!("Invalid IDO id (expected preinit_txid hex): {e}"), + ) + })?; + + let internal_id = crate::db::ido::lookup_internal_id_by_preinit_txid(pool, &preinit_blob) + .await + .map_err(db_error)? + .ok_or_else(|| not_found(ApiErrorCode::IdoNotFound, "IDO not found"))?; + + let preinit_txid_hex = blob_to_display_hex::(&preinit_blob) + .map_err(|e| bad_request(ApiErrorCode::InvalidParameters, &format!("Invalid IDO id: {e}")))?; + + Ok((internal_id, preinit_txid_hex)) +} + /// [Debug] Get the raw transaction hex for a txchain item by its ID. /// Returns `{"tx": ""}` or `{"tx": null}` if not found. #[get("/txchain//tx")]