From 76f0dcdb31510fbf259498411d13f666fc720441 Mon Sep 17 00:00:00 2001 From: Dagur Valberg Johannsson Date: Wed, 3 Apr 2024 21:40:33 +0200 Subject: [PATCH] rpc: List pools by token and pkh --- src/main.rs | 14 +-- src/rpc/mod.rs | 124 +----------------------- src/rpc/pool.rs | 251 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 255 insertions(+), 134 deletions(-) create mode 100644 src/rpc/pool.rs diff --git a/src/main.rs b/src/main.rs index ecf5ce2..86f65a5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -59,6 +59,7 @@ const RIFTEN_LABS_GENESIS_BLOCK: &str = // Last indexed block height. const KEY_LAST_INDEXED: &str = "last_indexed"; +mod cashaddr; mod chain; mod db; mod electrum; @@ -320,16 +321,6 @@ fn list_by_volume( Ok(Json(result)) } -#[get("/pool/list_by_apy")] -fn list_pools_by_apy(conn: &State) -> Result, Custom> { - let pools: Vec = rpc::pools_by_apy(&conn.get().unwrap()) - .map_err(|e| Custom(Status::BadRequest, format!("Error: {}", e)))?; - - Ok(Json(json!({ - "pools": json!(pools) - }))) -} - #[get("/contract/volume?")] fn contract_volume( end: Option, @@ -493,7 +484,8 @@ fn launch() -> _ { list_by_volume, rpc::price::price_history, rpc::price::price_current, - list_pools_by_apy, + rpc::pool::list_pools_by_apy, + rpc::pool::list_active_pools, rpc::contract::contract_count_token, rpc::contract::contract_count_all, contract_volume diff --git a/src/rpc/mod.rs b/src/rpc/mod.rs index 6e4f5bb..4e79c55 100644 --- a/src/rpc/mod.rs +++ b/src/rpc/mod.rs @@ -8,9 +8,8 @@ use std::collections::HashMap; use anyhow::{Context, Result}; use rusqlite::{params, Connection}; -use crate::timeutil::time_now; - pub mod contract; +pub mod pool; pub mod price; pub mod tvl; @@ -106,127 +105,6 @@ pub fn list_tokens_by_volume( Ok(tokens) } -#[derive(serde::Serialize)] -pub struct PoolYield { - token_id: String, - txid: String, - tx_pos: i64, - sats: i64, - tokens: i64, - - pool_yield: f64, - apy: f64, -} - -pub 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 - JOIN tx ON uf.txid = tx.txid - WHERE - phe.pool IN (SELECT creation_utxo FROM OriginalData) - 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) -} - fn all_time_volume(db: &Connection, end_timestamp: u64) -> Result> { let sql = " SELECT diff --git a/src/rpc/pool.rs b/src/rpc/pool.rs new file mode 100644 index 0000000..3135be3 --- /dev/null +++ b/src/rpc/pool.rs @@ -0,0 +1,251 @@ +// 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 + JOIN tx ON uf.txid = tx.txid + WHERE + phe.pool IN (SELECT creation_utxo FROM OriginalData) + 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 utxo_funding uf ON p.creation_utxo = uf.new_utxo_hash + WHERE + p.withdrawn_in_utxo 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, + }))) +}