258 lines
7.2 KiB
Rust
258 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::DB,
|
|
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 >= 10000
|
|
),
|
|
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<DB>) -> Result<Json<Value>, Custom<String>> {
|
|
let db = conn
|
|
.cauldron_r
|
|
.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<DB>,
|
|
) -> Result<Json<Value>, Custom<String>> {
|
|
let db = conn
|
|
.cauldron_r
|
|
.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,
|
|
})))
|
|
}
|