riftenlabs-indexer/src/rpc/pool.rs
2024-05-04 19:45:13 +02:00

256 lines
7.2 KiB
Rust

// 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<Vec<PoolYield>> {
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<DBPool>) -> Result<Json<Value>, Custom<String>> {
let db = conn
.get()
.map_err(|e| Custom(Status::InternalServerError, format!("Error: {}", e)))?;
let pools: Vec<PoolYield> =
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<Vec<ActivePool>> {
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<ActivePool> = 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?<token>&<pkh>")]
pub fn list_active_pools(
token: Option<&str>,
pkh: Option<&str>,
conn: &State<DBPool>,
) -> Result<Json<Value>, Custom<String>> {
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,
})))
}