ido: filter list_ido_entries by owner_nfthash

Add an optional owner_nfthash query param to GET /<id>/entries. Accepts
up to 20 comma-delimited 32-byte hex hashes; an entry matches any listed
value via an owner_nfthash IN (...) clause. Rejects malformed hex,
wrong-length values, and >20 filters with a 400.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hossein Zoda 2026-06-11 11:28:32 +00:00
parent aec0a6f0f8
commit d5bdf5a740
2 changed files with 64 additions and 2 deletions

View file

@ -2668,6 +2668,7 @@ pub async fn list_ido_entries(
internal_id: i64, internal_id: i64,
preinit_txid_hex: &str, preinit_txid_hex: &str,
distributed: Option<bool>, distributed: Option<bool>,
owner_nfthashes: &[Vec<u8>],
offset: i64, offset: i64,
limit: i64, limit: i64,
) -> Result<Vec<IdoEntryRpcRecord>> { ) -> Result<Vec<IdoEntryRpcRecord>> {
@ -2680,6 +2681,14 @@ pub async fn list_ido_entries(
qb.push(" AND distributed = "); qb.push(" AND distributed = ");
qb.push_bind(v as i64); qb.push_bind(v as i64);
} }
if !owner_nfthashes.is_empty() {
qb.push(" AND owner_nfthash IN (");
let mut separated = qb.separated(", ");
for hash in owner_nfthashes {
separated.push_bind(hash.clone());
}
separated.push_unseparated(")");
}
qb.push(" ORDER BY rowid ASC LIMIT "); qb.push(" ORDER BY rowid ASC LIMIT ");
qb.push_bind(limit); qb.push_bind(limit);
qb.push(" OFFSET "); qb.push(" OFFSET ");

View file

@ -14,6 +14,12 @@ use serde_json::Value;
const LIST_DEFAULT_LIMIT: i64 = 20; const LIST_DEFAULT_LIMIT: i64 = 20;
const LIST_MAX_LIMIT: i64 = 100; const LIST_MAX_LIMIT: i64 = 100;
/// Maximum number of comma-delimited owner_nfthash filters accepted per request.
const MAX_OWNER_NFTHASH_FILTERS: usize = 20;
/// Length in bytes of an owner_nfthash.
const OWNER_NFTHASH_LEN: usize = 32;
const DEBUG_DEFAULT_LIMIT: i64 = 100; const DEBUG_DEFAULT_LIMIT: i64 = 100;
const DEBUG_MAX_LIMIT: i64 = 10_000; const DEBUG_MAX_LIMIT: i64 = 10_000;
@ -96,16 +102,61 @@ pub async fn get_ido_by_offering_token(
Ok(cached_ok(serde_json::to_value(item).unwrap(), CACHE_NONE)) Ok(cached_ok(serde_json::to_value(item).unwrap(), CACHE_NONE))
} }
/// Parse the comma-delimited `owner_nfthash` query parameter into raw byte
/// filters. Returns an empty vec when the parameter is absent. Rejects more
/// than [`MAX_OWNER_NFTHASH_FILTERS`] values, blank entries, and invalid hex.
fn parse_owner_nfthash_filters(
owner_nfthash: Option<&str>,
) -> Result<Vec<Vec<u8>>, rocket::response::status::Custom<rocket::serde::json::Json<Value>>> {
let Some(raw) = owner_nfthash else {
return Ok(Vec::new());
};
let parts: Vec<&str> = raw.split(',').map(|s| s.trim()).filter(|s| !s.is_empty()).collect();
if parts.len() > MAX_OWNER_NFTHASH_FILTERS {
return Err(bad_request(
ApiErrorCode::InvalidParameters,
&format!("Too many owner_nfthash filters (max {MAX_OWNER_NFTHASH_FILTERS})"),
));
}
parts
.into_iter()
.map(|s| {
let bytes = hex::decode(s).map_err(|e| {
bad_request(
ApiErrorCode::InvalidParameters,
&format!("Invalid owner_nfthash '{s}': {e}"),
)
})?;
if bytes.len() != OWNER_NFTHASH_LEN {
return Err(bad_request(
ApiErrorCode::InvalidParameters,
&format!(
"Invalid owner_nfthash '{s}': expected {OWNER_NFTHASH_LEN} bytes, got {}",
bytes.len()
),
));
}
Ok(bytes)
})
.collect()
}
/// List entries for an IDO. /// List entries for an IDO.
/// ///
/// - `id`: the IDO's public id (preinit txid, 64-char hex) /// - `id`: the IDO's public id (preinit txid, 64-char hex)
/// - `distributed`: optional boolean filter /// - `distributed`: optional boolean filter
/// - `owner_nfthash`: optional filter by owner nfthash (hex). Multiple hashes
/// may be supplied comma-delimited; up to 20 per request. An entry matches
/// if its owner_nfthash equals any of the supplied values.
/// ///
/// Pagination: `offset` (default 0), `limit` (default 20, max 100) /// Pagination: `offset` (default 0), `limit` (default 20, max 100)
#[get("/<id>/entries?<distributed>&<offset>&<limit>")] #[get("/<id>/entries?<distributed>&<owner_nfthash>&<offset>&<limit>")]
pub async fn list_ido_entries( pub async fn list_ido_entries(
id: &str, id: &str,
distributed: Option<bool>, distributed: Option<bool>,
owner_nfthash: Option<&str>,
offset: Option<i64>, offset: Option<i64>,
limit: Option<i64>, limit: Option<i64>,
db: &State<DB>, db: &State<DB>,
@ -113,9 +164,11 @@ 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 owner_nfthashes = parse_owner_nfthash_filters(owner_nfthash)?;
let (internal_id, preinit_txid_hex) = resolve_ido(id, &db.ido_r).await?; 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) let items = crate::db::ido::list_ido_entries(&db.ido_r, internal_id, &preinit_txid_hex, distributed, &owner_nfthashes, offset, limit)
.await .await
.map_err(db_error)?; .map_err(db_error)?;