riftenlabs-indexer/src/rpc/pool.rs
Dagur Valberg Johannsson e8371c3967
Document improvements
Misc improves to API documentation
2026-02-24 11:39:20 +01:00

239 lines
6.7 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::{
cashaddr::utiladdr::p2pkh_hex_to_addr,
db::{
cauldron::{
pool::{db_pool_get_details, db_pool_history, db_pool_id_from_utxo},
poolvisitor::{
db_visit_pool_entries, OptionalFields, OptionalPoolFields, PoolFilters, PoolVisitor,
},
},
DB,
},
def::PoolID,
rpc::err::{bad_request, db_error, not_found, ApiErrorCode, CachedApiResult},
rpc::response::{cached_ok, CACHE_AGGREGATE, CACHE_NONE},
timeutil::time_now,
};
use anyhow::Result;
use bitcoin_hashes::hex::{FromHex, ToHex};
use bitcoincash::Txid;
use riftenlabs_defi::chainutil::compute_outpoint_hash;
use rocket::{get, State};
use serde_json::{json, Value};
#[derive(serde::Serialize)]
struct ActivePool {
owner_pkh: String,
owner_p2pkh_addr: String,
token_id: String,
sats: u64,
tokens: u64,
txid: String,
tx_pos: u32,
pool_id: String,
}
#[derive(Default)]
struct ActivePoolList {
active: Vec<ActivePool>,
}
impl PoolVisitor for ActivePoolList {
fn optional_fields_wanted(&self) -> u64 {
OptionalPoolFields::Owner as u64
| OptionalPoolFields::Txid as u64
| OptionalPoolFields::TxPos as u64
| OptionalPoolFields::TokenId as u64
| OptionalPoolFields::PoolId as u64
}
fn visit(&mut self, sats: u64, tokens: u64, optional_fields: OptionalFields) -> Result<bool> {
let owner_pkh = optional_fields.owner.unwrap().to_lowercase();
let owner_p2pkh_addr = p2pkh_hex_to_addr(&owner_pkh)?;
self.active.push(ActivePool {
owner_pkh,
owner_p2pkh_addr,
token_id: optional_fields.token_id.unwrap().to_lowercase(),
sats,
tokens,
txid: optional_fields.txid.unwrap().to_lowercase(),
tx_pos: optional_fields.tx_pos.unwrap(),
pool_id: optional_fields.pool_id.unwrap().to_lowercase(),
});
Ok(true)
}
}
/// Get list of active pools for given token and/or user.
/// Either user or token ID must be provided.
///
/// - user: 20 byte hash of a users pkh
/// - token: byte token ID.
///
/// Status: Stable
///
/// - token: Token ID or symbol
/// - pkh: Public key hash
///
/// **Response Example:**
/// ```json
/// {
/// "active": [
/// {
/// "owner_p2pkh_addr": "bitcoincash:zqmvqqsd6w08e4nvy8er0hzn6wzxvxj40u7tlk8wl3",
/// "owner_pkh": "36c0020dd39e7cd66c21f237dc53d384661a557f",
/// "sats": 776661580,
/// "token_id": "b79bfc8246b5fc4707e7c7dedcb6619ef1ab91f494a790c20b0f4c422ed95b92",
/// "tokens": 16,
/// "tx_pos": 0,
/// "txid": "94a933a0fa55093a0965eb867f1b9cac2bb07488ced4825bc31f86c9371f76aa"
/// }
/// ]
/// }
/// ```
#[get("/pool/active?<token>&<pkh>")]
pub async fn list_active_pools(
token: Option<&str>,
pkh: Option<&str>,
conn: &State<DB>,
) -> CachedApiResult<Value> {
if token.is_none() && pkh.is_none() {
return Err(bad_request(
ApiErrorCode::MissingParameters,
"Provide token or pkh",
));
}
let mut active_pool_list = ActivePoolList::default();
db_visit_pool_entries(
&conn.cauldron_r,
&mut active_pool_list,
PoolFilters {
token_id: token.map(|s| s.to_string()),
owner: pkh.map(|s| s.to_string()),
timestamp_lt: None,
timestamp_lte: None,
timestamp_gt: None,
timestamp_gte: None,
},
)
.await
.map_err(db_error)?;
Ok(cached_ok(
json!({
"active": active_pool_list.active,
}),
CACHE_NONE,
))
}
/// Get historical state changes for a specific pool.
///
/// Returns the price history (token/BCH ratio) over time. Each entry represents
/// a state change event.
///
/// - pool_id: The pool ID (can be obtained via `/pool/id_from_utxo`)
/// - start: Unix timestamp for period start (default: 30 days ago)
///
/// **Response Example:**
/// ```json
/// {
/// "history": [
/// {
/// "sats": 1000000,
/// "tokens": 500,
/// "timestamp": 1709468902,
/// "txid": "..."
/// }
/// ],
/// "token_id": "b79bfc8246b5fc4707e7c7dedcb6619ef1ab91f494a790c20b0f4c422ed95b92",
/// "owner_pkh": "36c0020dd39e7cd66c21f237dc53d384661a557f"
/// }
/// ```
#[get("/pool/history/<pool_id>?<start>")]
pub async fn pool_history(
pool_id: &str,
start: Option<u64>,
conn: &State<DB>,
) -> CachedApiResult<Value> {
let start = start.unwrap_or(time_now() as u64 - (30 * 3600 * 24) /* 30 days ago */);
let pool_id = PoolID::from_hex(pool_id).map_err(|e| {
bad_request(
ApiErrorCode::InvalidPoolId,
&format!("Invalid pool ID: {e}"),
)
})?;
let (token_id, owner_pkh) = db_pool_get_details(&conn.cauldron_r, &pool_id)
.await
.map_err(|e| {
let err_msg = e.to_string();
if err_msg.contains("not found") || err_msg.contains("no rows") {
not_found(ApiErrorCode::PoolNotFound, &format!("Pool not found: {e}"))
} else {
db_error(e)
}
})?;
let history = db_pool_history(&conn.cauldron_r, &pool_id, start)
.await
.map_err(db_error)?;
Ok(cached_ok(
json!({
"history": history,
"token_id": token_id,
"owner_pkh": owner_pkh,
}),
CACHE_NONE,
))
}
/// Get pool ID from a UTXO specified by transaction ID and output position.
///
/// - txid: Transaction ID (hex string)
/// - n: Output position (vout) in the transaction
///
/// **Response Example:**
/// ```json
/// {
/// "pool_id": "a1b2c3d4e5f6..."
/// }
/// ```
#[get("/pool/id_from_utxo?<txid>&<n>")]
pub async fn pool_id_from_utxo(txid: &str, n: u32, conn: &State<DB>) -> CachedApiResult<Value> {
let txid = Txid::from_hex(txid)
.map_err(|e| bad_request(ApiErrorCode::InvalidTxid, &format!("Invalid txid: {e}")))?;
let utxo_hash = compute_outpoint_hash(&txid, n);
let pool_id = db_pool_id_from_utxo(&conn.cauldron_r, &utxo_hash)
.await
.map_err(db_error)?;
match pool_id {
Some(pool_id) => Ok(cached_ok(
json!({
"pool_id": pool_id
}),
CACHE_AGGREGATE,
)),
None => Err(not_found(
ApiErrorCode::PoolNotFound,
&format!(
"No pool found for UTXO: txid={}, input_pos={}",
txid.to_hex(),
n
),
)),
}
}