ido: use preinit_txid hex as public id (rename internal id)

Rename ido.id -> ido.internal_id in the schema and IdoDBRecord, and
expose the preinit_txid hex as the public "id" in all RPC responses.

Public API shape:
  - IdoRpcRecord.id: i64 -> String (hex of preinit_txid). preinit_txid
    field is preserved unchanged.
  - IdoEntryRpcRecord.ido_id: i64 -> String (hex of parent preinit_txid).
  - IdoTxChainRpcRecord.ido_id: i64 -> String (hex of parent preinit_txid).
  - IdoTxChainRpcRecord gains ido_internal_id: i64 (debug endpoint only).
  - Routes /<id>/entries and /<id>/txchain accept the preinit_txid hex
    as the path id; returns 404 IDO_NOT_FOUND on miss.

Internals:
  - New lookup_internal_id_by_preinit_txid helper.
  - list_ido_entries / list_ido_txchain take preinit_txid_hex as input
    so the caller (which already parsed it from the path) avoids the
    extra "preinit_txid by internal_id" lookup.
  - Child table FK columns (ido_entry.ido_id, ido_txchain.ido_id) keep
    their names; only the parent PK and field accesses were renamed.

Notes:
  - Breaking API change for /ido/* endpoints. Clients reading "id" or
    "ido_id" as integers must switch to strings.
  - ido.db has no migration framework: drop the file and re-index on
    deploy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hossein Zoda 2026-05-24 20:08:39 +00:00
parent 210a51f274
commit 6728753db0
3 changed files with 137 additions and 80 deletions

View file

@ -677,7 +677,7 @@ pub fn pad_minimally_encoded_vm_number(bin: &[u8], length: usize) -> Vec<u8> {
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<u8>,
init_txid: Option<Vec<u8>>,
launch_txid: Option<Vec<u8>>,
@ -788,7 +788,7 @@ pub struct IdoDBRecord {
impl IdoDBRecord {
fn from_row(row: &sqlx::sqlite::SqliteRow) -> Result<Self> {
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<Vec<IdoTxChainDBRecord>> {
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<Option<IdoDBRecord>> {
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::<i64>, block_height, 1)
let internal_id = r.last_insert_rowid();
let txchain_item_id = upsert_ido_txchain(&mut dbtx, tx, internal_id, None::<i64>, 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<IdoUpdateEntry> = 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<i64>,
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<Vec<u8>, 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<String>,
pub launch_txid: Option<String>,
@ -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<String>,
@ -2293,7 +2293,8 @@ pub struct IdoEntryRpcRecord {
pub struct IdoTxChainRpcRecord {
pub id: i64,
pub prev_id: Option<i64>,
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<IdoRpcRecord> {
let preinit_txid_hex = blob_to_display_hex::<Txid>(&self.preinit_txid)?;
Ok(IdoRpcRecord {
id: self.id,
preinit_txid: blob_to_display_hex::<Txid>(&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::<Txid>).transpose()?,
launch_txid: self.launch_txid.as_deref().map(blob_to_display_hex::<Txid>).transpose()?,
offering_token_id: self.offering_token_id.as_deref().map(blob_to_display_hex::<TokenID>).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<Option<i64>> {
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<bool>,
@ -2407,7 +2420,7 @@ pub async fn list_idos(
limit: i64,
) -> Result<Vec<IdoRpcRecord>> {
let mut qb: sqlx::QueryBuilder<sqlx::Sqlite> = 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<u8>,
) -> Result<Option<IdoRpcRecord>> {
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<bool>,
offset: i64,
limit: i64,
) -> Result<Vec<IdoEntryRpcRecord>> {
let mut qb: sqlx::QueryBuilder<sqlx::Sqlite> = 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<u8> = row.get(1);
let owner_nfthash: Vec<u8> = row.get(2);
let commitment: Option<Vec<u8>> = row.get(3);
let distributed_int: i64 = row.get(8);
let txid_blob: Vec<u8> = row.get(0);
let owner_nfthash: Vec<u8> = row.get(1);
let commitment: Option<Vec<u8>> = 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>(&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<Vec<IdoTxChainRpcRecord>> {
let head_id: Option<i64> = sqlx::query("SELECT txchain_head FROM ido WHERE id = ?")
.bind(ido_id)
let head_id: Option<i64> = 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<IdoTxChainRpcRecord> = rows
.iter()
.map(|row| {
let txid_blob: Vec<u8> = row.get(3);
let txid_blob: Vec<u8> = 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>(&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);
}
}

View file

@ -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

View file

@ -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("/<ido_id>/entries?<distributed>&<offset>&<limit>")]
#[get("/<id>/entries?<distributed>&<offset>&<limit>")]
pub async fn list_ido_entries(
ido_id: i64,
id: &str,
distributed: Option<bool>,
offset: Option<i64>,
limit: Option<i64>,
@ -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("/<ido_id>/txchain?<offset>&<limit>")]
#[get("/<id>/txchain?<offset>&<limit>")]
pub async fn list_ido_txchain(
ido_id: i64,
id: &str,
offset: Option<i64>,
limit: Option<i64>,
db: &State<DB>,
@ -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<rocket::serde::json::Json<Value>>> {
let preinit_blob = display_hex_to_blob::<Txid>(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::<Txid>(&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": "<hex>"}` or `{"tx": null}` if not found.
#[get("/txchain/<txchain_item_id>/tx")]