ido: hide txchain_entrypoint, expose txchain_head as head txid
The IDO RPC record no longer leaks internal txchain row ids. Drop the txchain_entrypoint field entirely, and change txchain_head from the internal ido_txchain.id to the head record's txid (display hex). list_idos and get_ido_by_offering_token_id resolve this in a single query via LEFT JOIN ido_txchain ON head_tx.id = ido.txchain_head; a NULL head yields null. Adds tests for both the resolved and null cases. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6728753db0
commit
e86de7d5f5
1 changed files with 60 additions and 17 deletions
|
|
@ -2271,8 +2271,8 @@ pub struct IdoRpcRecord {
|
|||
pub status: String,
|
||||
pub parameters: serde_json::Value,
|
||||
pub state: serde_json::Value,
|
||||
pub txchain_entrypoint: Option<i64>,
|
||||
pub txchain_head: Option<i64>,
|
||||
/// Txid (display hex) of the txchain head record, or null if there is no head.
|
||||
pub txchain_head: Option<String>,
|
||||
pub is_valid: bool,
|
||||
}
|
||||
|
||||
|
|
@ -2307,7 +2307,10 @@ pub struct IdoTrackerMapRpcRecord {
|
|||
}
|
||||
|
||||
impl IdoDBRecord {
|
||||
fn into_rpc_record(self) -> Result<IdoRpcRecord> {
|
||||
/// Build the public RPC record. `txchain_head_txid` is the resolved txid blob of the
|
||||
/// txchain head record (joined in by the query); the internal `txchain_head` id and
|
||||
/// `txchain_entrypoint` are intentionally not exposed.
|
||||
fn into_rpc_record(self, txchain_head_txid: Option<Vec<u8>>) -> Result<IdoRpcRecord> {
|
||||
let preinit_txid_hex = blob_to_display_hex::<Txid>(&self.preinit_txid)?;
|
||||
Ok(IdoRpcRecord {
|
||||
id: preinit_txid_hex.clone(),
|
||||
|
|
@ -2319,8 +2322,7 @@ impl IdoDBRecord {
|
|||
status: self.status,
|
||||
parameters: serde_json::from_slice(&self.parameters)?,
|
||||
state: serde_json::from_slice(&self.state)?,
|
||||
txchain_entrypoint: self.txchain_entrypoint,
|
||||
txchain_head: self.txchain_head,
|
||||
txchain_head: txchain_head_txid.as_deref().map(blob_to_display_hex::<Txid>).transpose()?,
|
||||
is_valid: self.is_valid,
|
||||
})
|
||||
}
|
||||
|
|
@ -2420,23 +2422,24 @@ pub async fn list_idos(
|
|||
limit: i64,
|
||||
) -> Result<Vec<IdoRpcRecord>> {
|
||||
let mut qb: sqlx::QueryBuilder<sqlx::Sqlite> = sqlx::QueryBuilder::new(
|
||||
"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",
|
||||
"SELECT ido.internal_id, ido.preinit_txid, ido.init_txid, ido.launch_txid, ido.offering_token_id, ido.offered_token_id, \
|
||||
ido.status, ido.parameters, ido.state, ido.txchain_entrypoint, ido.txchain_head, ido.is_valid, ido.is_token_created_at_preinit, \
|
||||
head_tx.txid \
|
||||
FROM ido LEFT JOIN ido_txchain head_tx ON head_tx.id = ido.txchain_head WHERE 1=1",
|
||||
);
|
||||
if let Some(v) = is_valid {
|
||||
qb.push(" AND is_valid = ");
|
||||
qb.push(" AND ido.is_valid = ");
|
||||
qb.push_bind(v as i64);
|
||||
}
|
||||
if let Some(v) = status {
|
||||
qb.push(" AND status = ");
|
||||
qb.push(" AND ido.status = ");
|
||||
qb.push_bind(v);
|
||||
}
|
||||
if let Some(v) = offered_token_id_blob {
|
||||
qb.push(" AND offered_token_id = ");
|
||||
qb.push(" AND ido.offered_token_id = ");
|
||||
qb.push_bind(v);
|
||||
}
|
||||
qb.push(" ORDER BY internal_id ASC LIMIT ");
|
||||
qb.push(" ORDER BY ido.internal_id ASC LIMIT ");
|
||||
qb.push_bind(limit);
|
||||
qb.push(" OFFSET ");
|
||||
qb.push_bind(offset);
|
||||
|
|
@ -2445,7 +2448,10 @@ pub async fn list_idos(
|
|||
.fetch_all(pool)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|row| IdoDBRecord::from_row(&row)?.into_rpc_record())
|
||||
.map(|row| {
|
||||
let txchain_head_txid: Option<Vec<u8>> = row.get(13);
|
||||
IdoDBRecord::from_row(&row)?.into_rpc_record(txchain_head_txid)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
|
|
@ -2454,14 +2460,19 @@ pub async fn get_ido_by_offering_token_id(
|
|||
offering_token_id_blob: Vec<u8>,
|
||||
) -> Result<Option<IdoRpcRecord>> {
|
||||
let row = sqlx::query(
|
||||
"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 = ?",
|
||||
"SELECT ido.internal_id, ido.preinit_txid, ido.init_txid, ido.launch_txid, ido.offering_token_id, ido.offered_token_id, \
|
||||
ido.status, ido.parameters, ido.state, ido.txchain_entrypoint, ido.txchain_head, ido.is_valid, ido.is_token_created_at_preinit, \
|
||||
head_tx.txid \
|
||||
FROM ido LEFT JOIN ido_txchain head_tx ON head_tx.id = ido.txchain_head WHERE ido.offering_token_id = ?",
|
||||
)
|
||||
.bind(offering_token_id_blob)
|
||||
.fetch_optional(pool)
|
||||
.await?;
|
||||
row.map(|r| IdoDBRecord::from_row(&r)?.into_rpc_record()).transpose()
|
||||
row.map(|r| {
|
||||
let txchain_head_txid: Option<Vec<u8>> = r.get(13);
|
||||
IdoDBRecord::from_row(&r)?.into_rpc_record(txchain_head_txid)
|
||||
})
|
||||
.transpose()
|
||||
}
|
||||
|
||||
pub async fn list_ido_entries(
|
||||
|
|
@ -3040,4 +3051,36 @@ mod querytests {
|
|||
let chain = list_ido_txchain(&pool, 9999, "deadbeef", 0, 100).await.unwrap();
|
||||
assert_eq!(chain.len(), 0);
|
||||
}
|
||||
|
||||
// ─── DB: txchain_head is exposed as the head record's txid ────────────────
|
||||
|
||||
#[rocket::async_test]
|
||||
async fn ido_record_resolves_txchain_head_to_txid() {
|
||||
let pool = make_pool().await;
|
||||
let tok = vec![0xCC; 32];
|
||||
let ido_id = insert_test_ido(&pool, txid(1), "ACTIVE", true, true, None, Some(tok.clone())).await;
|
||||
let id_a = insert_txchain_item(&pool, ido_id, txid(10), None).await;
|
||||
let head_id = insert_txchain_item(&pool, ido_id, txid(11), Some(id_a)).await;
|
||||
set_txchain_head(&pool, ido_id, head_id).await;
|
||||
|
||||
// list_idos exposes txchain_head as the head record's txid (display hex), not its id.
|
||||
let listed = list_idos(&pool, None, None, None, 0, 100).await.unwrap();
|
||||
assert_eq!(listed.len(), 1);
|
||||
assert_eq!(listed[0].txchain_head, Some(hex::encode(txid(11))));
|
||||
|
||||
// get_ido_by_offering_token_id resolves it the same way.
|
||||
let one = get_ido_by_offering_token_id(&pool, tok).await.unwrap().unwrap();
|
||||
assert_eq!(one.txchain_head, Some(hex::encode(txid(11))));
|
||||
}
|
||||
|
||||
#[rocket::async_test]
|
||||
async fn ido_record_txchain_head_null_is_none() {
|
||||
let pool = make_pool().await;
|
||||
let ido_id = insert_test_ido(&pool, txid(1), "ACTIVE", true, true, None, None).await;
|
||||
// A txchain row exists, but txchain_head is NULL: the join must not match it.
|
||||
let _ = insert_txchain_item(&pool, ido_id, txid(10), None).await;
|
||||
let listed = list_idos(&pool, None, None, None, 0, 100).await.unwrap();
|
||||
assert_eq!(listed.len(), 1);
|
||||
assert_eq!(listed[0].txchain_head, None);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue