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:
parent
210a51f274
commit
6728753db0
3 changed files with 137 additions and 80 deletions
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")]
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue