// 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, } 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 { 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?&")] pub async fn list_active_pools( token: Option<&str>, pkh: Option<&str>, conn: &State, ) -> CachedApiResult { 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/?")] pub async fn pool_history( pool_id: &str, start: Option, conn: &State, ) -> CachedApiResult { 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?&")] pub async fn pool_id_from_utxo(txid: &str, n: u32, conn: &State) -> CachedApiResult { 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 ), )), } }