// Copyright (C) 2024 Riften Labs AS // // 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::{self, version_byte_flags}, db::DBPool, timeutil::time_now, }; use anyhow::{Context, Result}; use rocket::{get, http::Status, response::status::Custom, serde::json::Json, State}; use rusqlite::{params, Connection}; use serde::Serialize; use serde_json::{json, Value}; #[derive(serde::Serialize)] struct PoolYield { token_id: String, txid: String, tx_pos: i64, sats: i64, tokens: i64, pool_yield: f64, apy: f64, } fn pools_by_apy(connection: &Connection) -> Result> { let sql = " WITH OriginalData AS ( SELECT p.creation_utxo, uf.sats AS original_sats, uf.token_amount AS original_token_amount, uf.token_id as token_id, COALESCE(tx.first_seen_timestamp, tx.mtp_timestamp) AS original_timestamp FROM pool p JOIN utxo_funding uf ON p.creation_utxo = uf.new_utxo_hash JOIN tx ON uf.txid = tx.txid WHERE p.withdrawn_in_utxo IS NULL AND uf.sats >= 1000000 ), LatestData AS ( SELECT phe.pool, uf.sats AS latest_sats, uf.token_amount AS latest_token_amount, COALESCE(tx.first_seen_timestamp, tx.mtp_timestamp) AS latest_timestamp, uf.new_utxo_txid, uf.new_utxo_n FROM pool_history_entry phe JOIN utxo_funding uf ON phe.utxo = uf.new_utxo_hash LEFT JOIN utxo_spending us ON uf.new_utxo_hash = us.spent_utxo_hash JOIN tx ON uf.txid = tx.txid WHERE phe.pool IN (SELECT creation_utxo FROM OriginalData) AND us.spent_utxo_hash IS NULL ORDER BY COALESCE(tx.first_seen_timestamp, tx.mtp_timestamp) DESC ) SELECT od.original_sats, od.original_token_amount, od.original_timestamp, ld.latest_sats, ld.latest_token_amount, od.token_id, ld.new_utxo_txid, ld.new_utxo_n FROM OriginalData od JOIN LatestData ld ON od.creation_utxo = ld.pool; "; let mut statement = connection.prepare(sql)?; let current_timestamp = time_now(); let pool_rows = statement.query_map(params![], |row| { let original_sats: i64 = row.get(0)?; let original_tokens: i64 = row.get(1)?; let original_timestamp: i64 = row.get(2)?; let latest_sats: i64 = row.get(3)?; let latest_token_amount: i64 = row.get(4)?; let token_id = row.get(5)?; let txid = row.get(6)?; let tx_pos = row.get(7)?; assert!(current_timestamp >= original_timestamp); let original_k_sr = f64::sqrt((original_sats * original_tokens) as f64); let latest_k_sr = f64::sqrt((latest_sats * latest_token_amount) as f64); let pool_yield = ((latest_k_sr - original_k_sr) / original_k_sr) * 100.; let days_elapsed = (current_timestamp - original_timestamp) as f64 / 86400.0; let apy: f64 = if days_elapsed > 0.0 { let years_elapsed = 365.25 / days_elapsed; (((pool_yield / 100.0) + 1.0).powf(years_elapsed) - 1.0) * 100.0 } else { 0.0 }; Ok(PoolYield { token_id, txid, tx_pos, sats: latest_sats, tokens: latest_token_amount, pool_yield, apy, }) })?; let mut pools = Vec::new(); for pool_row in pool_rows { let pool_data = pool_row?; pools.push(pool_data); } // Sort pools by highest APY first pools.sort_by(|a, b| { b.apy .partial_cmp(&a.apy) .unwrap_or(std::cmp::Ordering::Equal) }); pools.truncate(1000); Ok(pools) } #[get("/pool/list_by_apy")] pub fn list_pools_by_apy(conn: &State) -> Result, Custom> { let db = conn .get() .map_err(|e| Custom(Status::InternalServerError, format!("Error: {}", e)))?; let pools: Vec = pools_by_apy(&db).map_err(|e| Custom(Status::BadRequest, format!("Error: {}", e)))?; Ok(Json(json!({ "pools": json!(pools) }))) } #[derive(Serialize)] struct ActivePool { owner_pkh: String, owner_p2pkh_addr: String, token_id: String, sats: u64, tokens: u64, txid: String, tx_pos: u32, } fn db_list_active_pools( token_id: Option<&str>, pkh: Option<&str>, conn: &Connection, ) -> Result> { let mut query: String = " SELECT p.owner_pkh, uf.sats, uf.token_amount, uf.new_utxo_txid, uf.new_utxo_n, p.token_id FROM pool p JOIN pool_history_entry phe ON p.creation_utxo = phe.pool JOIN utxo_funding uf ON phe.utxo = uf.new_utxo_hash LEFT JOIN utxo_spending us ON uf.new_utxo_hash = us.spent_utxo_hash WHERE p.withdrawn_in_utxo IS NULL AND us.spent_utxo_hash IS NULL " .to_string(); if token_id.is_some() { query.push_str(" AND p.token_id = ?"); } if pkh.is_some() { query.push_str(" AND p.owner_pkh = ?"); } let mut stmt = conn.prepare(&query)?; let mut rows = match (token_id, pkh) { (Some(t), Some(k)) => stmt.query([t, k])?, (None, Some(k)) => stmt.query([k])?, (Some(t), None) => stmt.query([t])?, (None, None) => stmt.query([])?, }; let mut pools: Vec = vec![]; while let Some(row) = rows.next()? { let owner_pkh = row.get(0)?; let sats = row.get(1)?; let tokens = row.get(2)?; let txid = row.get(3)?; let tx_pos = row.get(4)?; let token_id = row.get(5)?; let pkh = hex::decode(&owner_pkh).context("failed to decode ownerpkh")?; let owner_p2pkh_addr = cashaddr::encode( &pkh, version_byte_flags::SIZE_160 | version_byte_flags::TYPE_P2PKH_TOKEN, bitcoincash::Network::Bitcoin, ) .context("failed to encode p2pkh")?; pools.push(ActivePool { owner_pkh, owner_p2pkh_addr, token_id, sats, tokens, txid, tx_pos, }) } Ok(pools) } #[get("/pool/active?&")] pub fn list_active_pools( token: Option<&str>, pkh: Option<&str>, conn: &State, ) -> Result, Custom> { let db = conn .get() .map_err(|e| Custom(Status::InternalServerError, format!("Error: {}", e)))?; if token.is_none() && pkh.is_none() { return Err(Custom(Status::BadRequest, "Provide token or pkh".into())); } let active = db_list_active_pools(token, pkh, &db) .map_err(|e| Custom(Status::InternalServerError, format!("Error: {}", e)))?; Ok(Json(json!({ "active": active, }))) }