rpc: List pools by token and pkh

This commit is contained in:
Dagur Valberg Johannsson 2024-04-03 21:40:33 +02:00
parent a53782d035
commit 76f0dcdb31
No known key found for this signature in database
GPG key ID: FD701804AEE88107
3 changed files with 255 additions and 134 deletions

View file

@ -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<DBPool>) -> Result<Json<Value>, Custom<String>> {
let pools: Vec<rpc::PoolYield> = 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?<end>")]
fn contract_volume(
end: Option<i64>,
@ -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

View file

@ -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<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
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<Vec<(String, i64)>> {
let sql = "
SELECT

251
src/rpc/pool.rs Normal file
View file

@ -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<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
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<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 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<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,
})))
}