rpc: List pools by token and pkh
This commit is contained in:
parent
a53782d035
commit
76f0dcdb31
3 changed files with 255 additions and 134 deletions
14
src/main.rs
14
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<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
|
||||
|
|
|
|||
124
src/rpc/mod.rs
124
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<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
251
src/rpc/pool.rs
Normal 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,
|
||||
})))
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue