riftenlabs-indexer/src/rpc/ido.rs
Hossein Zoda b56ce5bc63 ido: key txchain tx lookup by txid; gate debug endpoints behind config
Change get_txchain_tx to take the public txid (a unique field) instead of
the non-public internal txchain item id, and promote it to a production
endpoint. Mount the two remaining debug endpoints (list_ido_txchain,
list_txchain_tracker_map) only when `debug = true` in the config.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 01:44:39 +03:00

192 lines
6.4 KiB
Rust

// Copyright (C) 2024-2026 Whiterun LLC
//
// 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::{blob_to_display_hex, display_hex_to_blob};
use crate::db::DB;
use crate::rpc::err::{bad_request, db_error, not_found, ApiErrorCode, CachedApiResult};
use crate::rpc::response::{cached_ok, CACHE_NONE};
use bitcoincash::{TokenID, Txid};
use rocket::{get, State};
use serde_json::Value;
const LIST_DEFAULT_LIMIT: i64 = 20;
const LIST_MAX_LIMIT: i64 = 100;
const DEBUG_DEFAULT_LIMIT: i64 = 100;
const DEBUG_MAX_LIMIT: i64 = 10_000;
/// List IDOs with optional filters and pagination.
///
/// Filters:
/// - `is_valid`: boolean filter on the is_valid flag
/// - `status`: one of PREINIT, ACTIVE, DISTRIBUTING, DISTRIBUTED
/// - `offered_token_id`: 64-char hex token ID (reversed display format)
///
/// Pagination: `offset` (default 0), `limit` (default 20, max 100)
#[get("/list?<is_valid>&<status>&<offered_token_id>&<offset>&<limit>")]
pub async fn list_idos(
is_valid: Option<bool>,
status: Option<String>,
offered_token_id: Option<&str>,
offset: Option<i64>,
limit: Option<i64>,
db: &State<DB>,
) -> CachedApiResult<Value> {
let offset = offset.unwrap_or(0).max(0);
let limit = limit.unwrap_or(LIST_DEFAULT_LIMIT).clamp(1, LIST_MAX_LIMIT);
let offered_token_id_blob = offered_token_id
.map(|s| {
display_hex_to_blob::<TokenID>(s).map_err(|e| {
bad_request(
ApiErrorCode::InvalidParameters,
&format!("Invalid offered_token_id: {e}"),
)
})
})
.transpose()?;
let items =
crate::db::ido::list_idos(&db.ido_r, is_valid, status, offered_token_id_blob, offset, limit)
.await
.map_err(db_error)?;
Ok(cached_ok(serde_json::to_value(items).unwrap(), CACHE_NONE))
}
/// Get a single IDO by its offering token ID. Returns null if not found.
#[get("/by-offering-token/<offering_token_id>")]
pub async fn get_ido_by_offering_token(
offering_token_id: &str,
db: &State<DB>,
) -> CachedApiResult<Value> {
let blob = display_hex_to_blob::<TokenID>(offering_token_id).map_err(|e| {
bad_request(
ApiErrorCode::InvalidParameters,
&format!("Invalid offering_token_id: {e}"),
)
})?;
let item = crate::db::ido::get_ido_by_offering_token_id(&db.ido_r, blob)
.await
.map_err(db_error)?;
Ok(cached_ok(serde_json::to_value(item).unwrap(), CACHE_NONE))
}
/// List entries for an IDO.
///
/// - `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("/<id>/entries?<distributed>&<offset>&<limit>")]
pub async fn list_ido_entries(
id: &str,
distributed: Option<bool>,
offset: Option<i64>,
limit: Option<i64>,
db: &State<DB>,
) -> CachedApiResult<Value> {
let offset = offset.unwrap_or(0).max(0);
let limit = limit.unwrap_or(LIST_DEFAULT_LIMIT).clamp(1, LIST_MAX_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)?;
Ok(cached_ok(serde_json::to_value(items).unwrap(), CACHE_NONE))
}
/// Get the raw transaction hex for a txchain item by its txid.
/// Returns `{"tx": "<hex>"}` or `{"tx": null}` if not found.
#[get("/txchain/<txid>/tx")]
pub async fn get_txchain_tx(
txid: &str,
db: &State<DB>,
) -> CachedApiResult<Value> {
let txid_blob = display_hex_to_blob::<Txid>(txid).map_err(|e| {
bad_request(
ApiErrorCode::InvalidParameters,
&format!("Invalid txid hex: {e}"),
)
})?;
let tx_hex = crate::db::ido::get_txchain_tx_hex(&db.ido_r, &txid_blob)
.await
.map_err(db_error)?;
Ok(cached_ok(
serde_json::json!({ "tx": tx_hex }),
CACHE_NONE,
))
}
/// [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("/<id>/txchain?<offset>&<limit>")]
pub async fn list_ido_txchain(
id: &str,
offset: Option<i64>,
limit: Option<i64>,
db: &State<DB>,
) -> CachedApiResult<Value> {
let offset = offset.unwrap_or(0).max(0);
let limit = limit.unwrap_or(DEBUG_DEFAULT_LIMIT).clamp(1, DEBUG_MAX_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] List all entries in the txchain tracker map.
///
/// Pagination: `offset` (default 0), `limit` (default 100, max 10000)
#[get("/txchain-tracker?<offset>&<limit>")]
pub async fn list_txchain_tracker_map(
offset: Option<i64>,
limit: Option<i64>,
db: &State<DB>,
) -> CachedApiResult<Value> {
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_txchain_tracker_map(&db.ido_r, offset, limit)
.await
.map_err(db_error)?;
Ok(cached_ok(serde_json::to_value(items).unwrap(), CACHE_NONE))
}