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) { pub async fn prepare_tables(pool: &SqlitePool) {
sqlx::query( sqlx::query(
"CREATE TABLE ido ( "CREATE TABLE ido (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, internal_id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
preinit_txid BLOB NOT NULL UNIQUE, preinit_txid BLOB NOT NULL UNIQUE,
init_txid BLOB NULL, init_txid BLOB NULL,
launch_txid BLOB NULL, launch_txid BLOB NULL,
@ -728,7 +728,7 @@ pub async fn prepare_tables(pool: &SqlitePool) {
"CREATE TABLE ido_txchain ( "CREATE TABLE ido_txchain (
id INTEGER NOT NULL PRIMARY KEY, id INTEGER NOT NULL PRIMARY KEY,
prev_id INTEGER NULL, 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, txid BLOB NOT NULL UNIQUE,
tx BLOB NOT NULL tx BLOB NOT NULL
)", )",
@ -771,7 +771,7 @@ pub struct IdoEntryDBRecord {
#[derive(Debug, Clone, serde::Serialize)] #[derive(Debug, Clone, serde::Serialize)]
pub struct IdoDBRecord { pub struct IdoDBRecord {
id: i64, internal_id: i64,
preinit_txid: Vec<u8>, preinit_txid: Vec<u8>,
init_txid: Option<Vec<u8>>, init_txid: Option<Vec<u8>>,
launch_txid: Option<Vec<u8>>, launch_txid: Option<Vec<u8>>,
@ -788,7 +788,7 @@ pub struct IdoDBRecord {
impl IdoDBRecord { impl IdoDBRecord {
fn from_row(row: &sqlx::sqlite::SqliteRow) -> Result<Self> { fn from_row(row: &sqlx::sqlite::SqliteRow) -> Result<Self> {
Ok(Self { Ok(Self {
id: row.get(0), internal_id: row.get(0),
preinit_txid: row.get(1), preinit_txid: row.get(1),
init_txid: row.get(2), init_txid: row.get(2),
launch_txid: row.get(3), launch_txid: row.get(3),
@ -827,13 +827,13 @@ impl IdoTxChainDBRecord {
async fn get_ido_txchain_list( async fn get_ido_txchain_list(
pool: &SqlitePool, pool: &SqlitePool,
ido_id: i64, internal_id: i64,
) -> Result<Vec<IdoTxChainDBRecord>> { ) -> Result<Vec<IdoTxChainDBRecord>> {
sqlx::query( sqlx::query(
"SELECT id, prev_id, ido_id, txid, tx "SELECT id, prev_id, ido_id, txid, tx
FROM ido_txchain WHERE ido_id = ?" FROM ido_txchain WHERE ido_id = ?"
) )
.bind(ido_id) .bind(internal_id)
.fetch_all(pool) .fetch_all(pool)
.await? .await?
.iter() .iter()
@ -881,14 +881,14 @@ async fn get_txchain_item_by_txid(
async fn ido_lookup( async fn ido_lookup(
pool: &SqlitePool, pool: &SqlitePool,
ido_id: i64, internal_id: i64,
) -> Result<Option<IdoDBRecord>> { ) -> Result<Option<IdoDBRecord>> {
let row = sqlx::query( 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 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) .fetch_optional(pool)
.await?; .await?;
if let Some(row) = row { if let Some(row) = row {
@ -1533,16 +1533,16 @@ async fn on_create_ido(
match result { match result {
Ok(r) => { Ok(r) => {
// insert txchain entrypoint // insert txchain entrypoint
let ido_id = r.last_insert_rowid(); let internal_id = r.last_insert_rowid();
let txchain_item_id = upsert_ido_txchain(&mut dbtx, tx, ido_id, None::<i64>, block_height, 1) let txchain_item_id = upsert_ido_txchain(&mut dbtx, tx, internal_id, None::<i64>, block_height, 1)
.await?; .await?;
sqlx::query( sqlx::query(
"UPDATE ido SET txchain_entrypoint = ?, txchain_head = ? "UPDATE ido SET txchain_entrypoint = ?, txchain_head = ?
WHERE id = ?", WHERE internal_id = ?",
) )
.bind(txchain_item_id) .bind(txchain_item_id)
.bind(txchain_item_id) .bind(txchain_item_id)
.bind(ido_id) .bind(internal_id)
.execute(&mut *dbtx) .execute(&mut *dbtx)
.await?; .await?;
dbtx.commit().await?; dbtx.commit().await?;
@ -1934,13 +1934,13 @@ fn ido_add_tx(
async fn update_ido( async fn update_ido(
dbtx: &mut sqlx::Transaction<'_, sqlx::Sqlite>, dbtx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
ido_id: i64, internal_id: i64,
context: &IdoContext, context: &IdoContext,
txchain_head: i64, txchain_head: i64,
) -> Result<()> { ) -> Result<()> {
sqlx::query( sqlx::query(
"UPDATE ido set init_txid = ?, launch_txid = ?, status = ?, parameters = ?, state = ?, "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.init_txid)
.bind(&context.launch_txid) .bind(&context.launch_txid)
@ -1951,7 +1951,7 @@ async fn update_ido(
.bind(&context.offered_token_id) .bind(&context.offered_token_id)
.bind(txchain_head) .bind(txchain_head)
.bind(context.is_valid_ido) .bind(context.is_valid_ido)
.bind(ido_id) .bind(internal_id)
.execute(&mut **dbtx) .execute(&mut **dbtx)
.await?; .await?;
Ok(()) Ok(())
@ -1991,7 +1991,7 @@ fn apply_updates_to_context(
async fn upsert_updates_to_ido_entries( async fn upsert_updates_to_ido_entries(
dbtx: &mut sqlx::Transaction<'_, sqlx::Sqlite>, dbtx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
ido_id: i64, internal_id: i64,
updates: &[IdoUpdate], updates: &[IdoUpdate],
) -> Result<()> { ) -> Result<()> {
let mut insert_list: Vec<IdoUpdateEntry> = Vec::new(); 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) "INSERT INTO ido_entry (ido_id, txid, owner_nfthash, commitment, supply_amount, demand_amount, lockup_timeval, discount, distributed)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
) )
.bind(ido_id) .bind(internal_id)
.bind(entry.txid) .bind(entry.txid)
.bind(entry.owner_nfthash) .bind(entry.owner_nfthash)
.bind(entry.commitment) .bind(entry.commitment)
@ -2045,7 +2045,7 @@ async fn upsert_updates_to_ido_entries(
async fn upsert_ido_txchain( async fn upsert_ido_txchain(
dbtx: &mut sqlx::Transaction<'_, sqlx::Sqlite>, dbtx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
tx: &Transaction, tx: &Transaction,
ido_id: i64, internal_id: i64,
prev_txchain_id: Option<i64>, prev_txchain_id: Option<i64>,
block_height: i64, block_height: i64,
next_output_index: i32, next_output_index: i32,
@ -2056,7 +2056,7 @@ async fn upsert_ido_txchain(
(?, ?, ?, ?)" (?, ?, ?, ?)"
) )
.bind(prev_txchain_id) .bind(prev_txchain_id)
.bind(ido_id) .bind(internal_id)
.bind(tx.txid().to_blob()) .bind(tx.txid().to_blob())
.bind(serialized_tx) .bind(serialized_tx)
.execute(&mut **dbtx) .execute(&mut **dbtx)
@ -2110,7 +2110,7 @@ async fn on_add_ido_tx(
.await?; .await?;
} else if prev_txchain_item.id != ido.txchain_head.unwrap_or(0) { } else if prev_txchain_item.id != ido.txchain_head.unwrap_or(0) {
// rebuild the state, txchain is broken, recreate the chain // 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?; .await?;
let mut items_tx_map: HashMap<Vec<u8>, Transaction> = HashMap::new(); let mut items_tx_map: HashMap<Vec<u8>, Transaction> = HashMap::new();
for item in &items { for item in &items {
@ -2148,19 +2148,19 @@ async fn on_add_ido_tx(
let result = ido_add_tx(&context, tx)?; let result = ido_add_tx(&context, tx)?;
apply_updates_to_context(&mut context, &result.updates); apply_updates_to_context(&mut context, &result.updates);
updates.extend_from_slice(&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?; .await?;
update_ido(&mut dbtx, ido.id, &context, txchain_item_id) update_ido(&mut dbtx, ido.internal_id, &context, txchain_item_id)
.await?; .await?;
// delete all entries // delete all entries
sqlx::query( sqlx::query(
"DELETE FROM ido_entry WHERE ido_id = ?", "DELETE FROM ido_entry WHERE ido_id = ?",
) )
.bind(ido.id) .bind(ido.internal_id)
.execute(&mut *dbtx) .execute(&mut *dbtx)
.await?; .await?;
// insert the entries of the new state // 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?; .await?;
} else { } else {
// create context from ido // create context from ido
@ -2183,11 +2183,11 @@ async fn on_add_ido_tx(
// add to the chain // add to the chain
let result = ido_add_tx(&context, tx)?; let result = ido_add_tx(&context, tx)?;
apply_updates_to_context(&mut context, &result.updates); 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?; .await?;
update_ido(&mut dbtx, ido.id, &context, txchain_item_id) update_ido(&mut dbtx, ido.internal_id, &context, txchain_item_id)
.await?; .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?; .await?;
} }
dbtx.commit().await?; dbtx.commit().await?;
@ -2262,7 +2262,7 @@ pub async fn index_block(
#[derive(Serialize, Clone)] #[derive(Serialize, Clone)]
pub struct IdoRpcRecord { pub struct IdoRpcRecord {
pub id: i64, pub id: String,
pub preinit_txid: String, pub preinit_txid: String,
pub init_txid: Option<String>, pub init_txid: Option<String>,
pub launch_txid: Option<String>, pub launch_txid: Option<String>,
@ -2278,7 +2278,7 @@ pub struct IdoRpcRecord {
#[derive(Serialize, Clone)] #[derive(Serialize, Clone)]
pub struct IdoEntryRpcRecord { pub struct IdoEntryRpcRecord {
pub ido_id: i64, pub ido_id: String,
pub txid: String, pub txid: String,
pub owner_nfthash: String, pub owner_nfthash: String,
pub commitment: Option<String>, pub commitment: Option<String>,
@ -2293,7 +2293,8 @@ pub struct IdoEntryRpcRecord {
pub struct IdoTxChainRpcRecord { pub struct IdoTxChainRpcRecord {
pub id: i64, pub id: i64,
pub prev_id: Option<i64>, pub prev_id: Option<i64>,
pub ido_id: i64, pub ido_id: String,
pub ido_internal_id: i64,
pub txid: String, pub txid: String,
} }
@ -2307,9 +2308,10 @@ pub struct IdoTrackerMapRpcRecord {
impl IdoDBRecord { impl IdoDBRecord {
fn into_rpc_record(self) -> Result<IdoRpcRecord> { fn into_rpc_record(self) -> Result<IdoRpcRecord> {
let preinit_txid_hex = blob_to_display_hex::<Txid>(&self.preinit_txid)?;
Ok(IdoRpcRecord { Ok(IdoRpcRecord {
id: self.id, id: preinit_txid_hex.clone(),
preinit_txid: blob_to_display_hex::<Txid>(&self.preinit_txid)?, preinit_txid: preinit_txid_hex,
init_txid: self.init_txid.as_deref().map(blob_to_display_hex::<Txid>).transpose()?, 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()?, 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()?, 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 ────────────────────────────────────────────── // ─── 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( pub async fn list_idos(
pool: &SqlitePool, pool: &SqlitePool,
is_valid: Option<bool>, is_valid: Option<bool>,
@ -2407,7 +2420,7 @@ pub async fn list_idos(
limit: i64, limit: i64,
) -> Result<Vec<IdoRpcRecord>> { ) -> Result<Vec<IdoRpcRecord>> {
let mut qb: sqlx::QueryBuilder<sqlx::Sqlite> = sqlx::QueryBuilder::new( 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 status, parameters, state, txchain_entrypoint, txchain_head, is_valid, is_token_created_at_preinit
FROM ido WHERE 1=1", FROM ido WHERE 1=1",
); );
@ -2423,7 +2436,7 @@ pub async fn list_idos(
qb.push(" AND offered_token_id = "); qb.push(" AND offered_token_id = ");
qb.push_bind(v); 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_bind(limit);
qb.push(" OFFSET "); qb.push(" OFFSET ");
qb.push_bind(offset); qb.push_bind(offset);
@ -2441,7 +2454,7 @@ pub async fn get_ido_by_offering_token_id(
offering_token_id_blob: Vec<u8>, offering_token_id_blob: Vec<u8>,
) -> Result<Option<IdoRpcRecord>> { ) -> Result<Option<IdoRpcRecord>> {
let row = sqlx::query( 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 \ status, parameters, state, txchain_entrypoint, txchain_head, is_valid, is_token_created_at_preinit \
FROM ido WHERE offering_token_id = ?", 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( pub async fn list_ido_entries(
pool: &SqlitePool, pool: &SqlitePool,
ido_id: i64, internal_id: i64,
preinit_txid_hex: &str,
distributed: Option<bool>, distributed: Option<bool>,
offset: i64, offset: i64,
limit: i64, limit: i64,
) -> Result<Vec<IdoEntryRpcRecord>> { ) -> Result<Vec<IdoEntryRpcRecord>> {
let mut qb: sqlx::QueryBuilder<sqlx::Sqlite> = sqlx::QueryBuilder::new( 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 = ", 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 { if let Some(v) = distributed {
qb.push(" AND distributed = "); qb.push(" AND distributed = ");
qb.push_bind(v as i64); qb.push_bind(v as i64);
@ -2477,19 +2491,19 @@ pub async fn list_ido_entries(
.await? .await?
.into_iter() .into_iter()
.map(|row| { .map(|row| {
let txid_blob: Vec<u8> = row.get(1); let txid_blob: Vec<u8> = row.get(0);
let owner_nfthash: Vec<u8> = row.get(2); let owner_nfthash: Vec<u8> = row.get(1);
let commitment: Option<Vec<u8>> = row.get(3); let commitment: Option<Vec<u8>> = row.get(2);
let distributed_int: i64 = row.get(8); let distributed_int: i64 = row.get(7);
Ok(IdoEntryRpcRecord { Ok(IdoEntryRpcRecord {
ido_id: row.get(0), ido_id: preinit_txid_hex.to_string(),
txid: blob_to_display_hex::<Txid>(&txid_blob)?, txid: blob_to_display_hex::<Txid>(&txid_blob)?,
owner_nfthash: hex::encode(&owner_nfthash), owner_nfthash: hex::encode(&owner_nfthash),
commitment: commitment.map(|c| hex::encode(&c)), commitment: commitment.map(|c| hex::encode(&c)),
supply_amount: row.get(4), supply_amount: row.get(3),
demand_amount: row.get(5), demand_amount: row.get(4),
lockup_timeval: row.get(6), lockup_timeval: row.get(5),
discount: row.get(7), discount: row.get(6),
distributed: distributed_int != 0, distributed: distributed_int != 0,
}) })
}) })
@ -2498,12 +2512,13 @@ pub async fn list_ido_entries(
pub async fn list_ido_txchain( pub async fn list_ido_txchain(
pool: &SqlitePool, pool: &SqlitePool,
ido_id: i64, internal_id: i64,
preinit_txid_hex: &str,
offset: i64, offset: i64,
limit: i64, limit: i64,
) -> Result<Vec<IdoTxChainRpcRecord>> { ) -> Result<Vec<IdoTxChainRpcRecord>> {
let head_id: Option<i64> = sqlx::query("SELECT txchain_head FROM ido WHERE id = ?") let head_id: Option<i64> = sqlx::query("SELECT txchain_head FROM ido WHERE internal_id = ?")
.bind(ido_id) .bind(internal_id)
.fetch_optional(pool) .fetch_optional(pool)
.await? .await?
.and_then(|row| row.get(0)); .and_then(|row| row.get(0));
@ -2514,9 +2529,9 @@ pub async fn list_ido_txchain(
}; };
let rows = sqlx::query( 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) .fetch_all(pool)
.await?; .await?;
@ -2524,11 +2539,12 @@ pub async fn list_ido_txchain(
let records: Vec<IdoTxChainRpcRecord> = rows let records: Vec<IdoTxChainRpcRecord> = rows
.iter() .iter()
.map(|row| { .map(|row| {
let txid_blob: Vec<u8> = row.get(3); let txid_blob: Vec<u8> = row.get(2);
Ok(IdoTxChainRpcRecord { Ok(IdoTxChainRpcRecord {
id: row.get(0), id: row.get(0),
prev_id: row.get(1), 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)?, txid: blob_to_display_hex::<Txid>(&txid_blob)?,
}) })
}) })
@ -2851,9 +2867,13 @@ mod querytests {
assert_eq!(page1.len(), 2); assert_eq!(page1.len(), 2);
assert_eq!(page2.len(), 2); assert_eq!(page2.len(), 2);
assert_eq!(page3.len(), 1); assert_eq!(page3.len(), 1);
// IDs are ascending // Pages reflect internal_id ascending insertion order: preinit_txid hex of txid(i) bytes
assert!(page1[0].id < page1[1].id); // For txid(i) = [i; 32], the display hex is "ii".repeat(32).
assert!(page1[1].id < page2[0].id); 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 ──────────────────────────────────── // ─── DB: get_ido_by_offering_token_id ────────────────────────────────────
@ -2897,9 +2917,10 @@ mod querytests {
async fn list_ido_entries_all() { async fn list_ido_entries_all() {
let pool = make_pool().await; let pool = make_pool().await;
let ido_id = insert_test_ido(&pool, txid(1), "ACTIVE", true, true, None, None).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(10), false).await;
insert_test_entry(&pool, ido_id, txid(11), true).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); assert_eq!(all.len(), 2);
} }
@ -2907,13 +2928,14 @@ mod querytests {
async fn list_ido_entries_filter_distributed() { async fn list_ido_entries_filter_distributed() {
let pool = make_pool().await; let pool = make_pool().await;
let ido_id = insert_test_ido(&pool, txid(1), "ACTIVE", true, true, None, None).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(10), false).await;
insert_test_entry(&pool, ido_id, txid(11), true).await; insert_test_entry(&pool, ido_id, txid(11), true).await;
insert_test_entry(&pool, ido_id, txid(12), 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_eq!(dist.len(), 2);
assert!(dist.iter().all(|e| e.distributed)); 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_eq!(undist.len(), 1);
assert!(!undist[0].distributed); assert!(!undist[0].distributed);
} }
@ -2923,11 +2945,13 @@ mod querytests {
let pool = make_pool().await; let pool = make_pool().await;
let ido1 = insert_test_ido(&pool, txid(1), "ACTIVE", true, true, None, None).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 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(10), false).await;
insert_test_entry(&pool, ido1, txid(11), false).await; insert_test_entry(&pool, ido1, txid(11), false).await;
insert_test_entry(&pool, ido2, txid(12), false).await; insert_test_entry(&pool, ido2, txid(12), false).await;
let e1 = list_ido_entries(&pool, ido1, 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, 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!(e1.len(), 2);
assert_eq!(e2.len(), 1); assert_eq!(e2.len(), 1);
} }
@ -2954,7 +2978,7 @@ mod querytests {
} }
async fn set_txchain_head(pool: &SqlitePool, ido_id: i64, head_id: i64) { 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(head_id)
.bind(ido_id) .bind(ido_id)
.execute(pool) .execute(pool)
@ -2966,12 +2990,13 @@ mod querytests {
async fn list_ido_txchain_ordered_oldest_first() { async fn list_ido_txchain_ordered_oldest_first() {
let pool = make_pool().await; let pool = make_pool().await;
let ido_id = insert_test_ido(&pool, txid(1), "ACTIVE", true, true, None, None).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_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_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_c = insert_txchain_item(&pool, ido_id, txid(12), Some(id_b)).await;
set_txchain_head(&pool, ido_id, id_c).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.len(), 3);
assert_eq!(chain[0].id, id_a); assert_eq!(chain[0].id, id_a);
assert_eq!(chain[1].id, id_b); assert_eq!(chain[1].id, id_b);
@ -2982,14 +3007,15 @@ mod querytests {
async fn list_ido_txchain_pagination() { async fn list_ido_txchain_pagination() {
let pool = make_pool().await; let pool = make_pool().await;
let ido_id = insert_test_ido(&pool, txid(1), "ACTIVE", true, true, None, None).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_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_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_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; let id_d = insert_txchain_item(&pool, ido_id, txid(13), Some(id_c)).await;
set_txchain_head(&pool, ido_id, id_d).await; set_txchain_head(&pool, ido_id, id_d).await;
let page1 = list_ido_txchain(&pool, ido_id, 0, 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, 2, 2).await.unwrap(); let page2 = list_ido_txchain(&pool, ido_id, &preinit_hex, 2, 2).await.unwrap();
assert_eq!(page1.len(), 2); assert_eq!(page1.len(), 2);
assert_eq!(page2.len(), 2); assert_eq!(page2.len(), 2);
assert_eq!(page1[0].id, id_a); assert_eq!(page1[0].id, id_a);
@ -3002,15 +3028,16 @@ mod querytests {
async fn list_ido_txchain_no_head_returns_empty() { async fn list_ido_txchain_no_head_returns_empty() {
let pool = make_pool().await; let pool = make_pool().await;
let ido_id = insert_test_ido(&pool, txid(1), "ACTIVE", true, true, None, None).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) // 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); assert_eq!(chain.len(), 0);
} }
#[rocket::async_test] #[rocket::async_test]
async fn list_ido_txchain_unknown_ido_returns_empty() { async fn list_ido_txchain_unknown_ido_returns_empty() {
let pool = make_pool().await; 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); assert_eq!(chain.len(), 0);
} }
} }

View file

@ -35,6 +35,7 @@ pub enum ApiErrorCode {
// 404 Not Found codes // 404 Not Found codes
PriceNotFound, PriceNotFound,
PoolNotFound, PoolNotFound,
IdoNotFound,
// 500 Internal Server Error codes // 500 Internal Server Error codes
InternalError, InternalError,
@ -61,6 +62,7 @@ impl fmt::Display for ApiErrorCode {
// 404 codes // 404 codes
Self::PriceNotFound => "PRICE_NOT_FOUND", Self::PriceNotFound => "PRICE_NOT_FOUND",
Self::PoolNotFound => "POOL_NOT_FOUND", Self::PoolNotFound => "POOL_NOT_FOUND",
Self::IdoNotFound => "IDO_NOT_FOUND",
// 500 codes // 500 codes
Self::InternalError => "INTERNAL_ERROR", Self::InternalError => "INTERNAL_ERROR",
// 503 codes // 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. // 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 // 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::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 crate::rpc::response::{cached_ok, CACHE_NONE};
use bitcoincash::TokenID; use bitcoincash::{TokenID, Txid};
use rocket::{get, State}; use rocket::{get, State};
use serde_json::Value; use serde_json::Value;
@ -78,13 +78,13 @@ pub async fn get_ido_by_offering_token(
/// List entries for an IDO. /// 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 /// - `distributed`: optional boolean filter
/// ///
/// Pagination: `offset` (default 0), `limit` (default 20, max 100) /// 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( pub async fn list_ido_entries(
ido_id: i64, id: &str,
distributed: Option<bool>, distributed: Option<bool>,
offset: Option<i64>, offset: Option<i64>,
limit: Option<i64>, limit: Option<i64>,
@ -93,7 +93,9 @@ pub async fn list_ido_entries(
let offset = offset.unwrap_or(0).max(0); let offset = offset.unwrap_or(0).max(0);
let limit = limit.unwrap_or(LIST_DEFAULT_LIMIT).clamp(1, LIST_MAX_LIMIT); 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 .await
.map_err(db_error)?; .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). /// [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) /// 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( pub async fn list_ido_txchain(
ido_id: i64, id: &str,
offset: Option<i64>, offset: Option<i64>,
limit: Option<i64>, limit: Option<i64>,
db: &State<DB>, db: &State<DB>,
@ -113,13 +117,37 @@ pub async fn list_ido_txchain(
let offset = offset.unwrap_or(0).max(0); let offset = offset.unwrap_or(0).max(0);
let limit = limit.unwrap_or(DEBUG_DEFAULT_LIMIT).clamp(1, DEBUG_MAX_LIMIT); 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 .await
.map_err(db_error)?; .map_err(db_error)?;
Ok(cached_ok(serde_json::to_value(items).unwrap(), CACHE_NONE)) 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. /// [Debug] Get the raw transaction hex for a txchain item by its ID.
/// Returns `{"tx": "<hex>"}` or `{"tx": null}` if not found. /// Returns `{"tx": "<hex>"}` or `{"tx": null}` if not found.
#[get("/txchain/<txchain_item_id>/tx")] #[get("/txchain/<txchain_item_id>/tx")]