riftenlabs-indexer/src/rpc/price.rs

236 lines
6.3 KiB
Rust
Raw Normal View History

2024-04-03 11:39:49 +02:00
// 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 anyhow::{bail, Context, Result};
use rocket::{get, http::Status, response::status::Custom, serde::json::Json, State};
use rusqlite::{params, Connection};
use rust_decimal::prelude::*;
use serde_json::{json, Value};
use crate::{db::DBPool, timeutil::time_now};
struct PriceInterval {
start: i64,
step: i64,
sats: i64,
tokens: i64,
min: f64,
max: f64,
}
impl PriceInterval {
pub fn new(start: i64, step: i64) -> Self {
Self {
start,
step,
sats: 0,
tokens: 0,
min: f64::MAX,
max: f64::MIN,
}
}
pub fn next(&self) -> Self {
let new_start = self.start + self.step;
Self::new(new_start, self.step)
}
pub fn end(&self) -> i64 {
self.start + self.step
}
pub fn avg_price(&self) -> Option<f64> {
if self.tokens == 0 {
return None;
}
Some(self.sats as f64 / self.tokens as f64)
}
pub fn add_pool(&mut self, sats: i64, tokens: i64) {
if tokens != 0 {
let price = sats as f64 / tokens as f64;
if price > self.max {
self.max = price
}
if price < self.min {
self.min = price
}
}
self.sats += sats;
self.tokens += tokens;
}
pub fn to_result(&self) -> Option<(i64, f64, f64, f64)> {
if self.tokens == 0 {
None
} else {
Some((
self.start,
self.avg_price().expect("avg price not calculated"),
self.max,
self.min,
))
}
}
}
// Get the current price of a given token
fn current_price(db: &Connection, token_id: &str) -> Result<f64> {
let sql = "SELECT uf.sats, uf.token_amount
FROM utxo_funding uf
LEFT JOIN utxo_spending us ON uf.new_utxo_hash = us.spent_utxo_hash
WHERE us.spent_utxo_hash IS NULL AND uf.token_id = ?";
let mut statement = db.prepare(sql)?;
let mut rows = statement.query(params![token_id])?;
let mut sum_sats: u64 = 0;
let mut sum_tokens: u64 = 0;
while let Some(row) = rows.next()? {
let sats: i64 = row.get(0)?;
let tokens: i64 = row.get(1)?;
sum_sats += sats as u64;
sum_tokens += tokens as u64;
}
let price = Decimal::from_u64(sum_sats).context("overflow")?
/ Decimal::from_u64(sum_tokens).context("overflow")?;
price.to_f64().context("overflow")
}
#[allow(clippy::type_complexity)]
fn historic_price(
connection: &Connection,
timestamp_start: i64, // Start timestamp in posix
timestamp_end: i64, // End timestamp in posix
step_size: i64, // Interval in seconds (e.g., 600 for 10 minutes)
token_id: &str,
) -> Result<Vec<(i64, f64, f64, f64)>> {
if timestamp_start > timestamp_end {
bail!("Start cannot be higher than end");
}
let total_intervals = (timestamp_end - timestamp_start) / step_size;
const MAX_INTERVALS: i64 = 10000;
if total_intervals > MAX_INTERVALS {
bail!(
"Too many intervals ({} > {})",
total_intervals,
MAX_INTERVALS
);
}
// Prepare and execute the SQL query for the current interval
let sql = "
SELECT
COALESCE(tx.first_seen_timestamp, tx.mtp_timestamp) AS effective_timestamp,
utxo_funding.sats,
utxo_funding.token_amount
FROM
utxo_funding
LEFT JOIN
tx ON utxo_funding.txid = tx.txid
WHERE
utxo_funding.token_id = ? AND
effective_timestamp >= ? AND
effective_timestamp < ?
ORDER BY
effective_timestamp ASC
";
let mut statement = connection.prepare(sql)?;
let mut rows = statement.query(params![token_id, timestamp_start, timestamp_end])?;
let mut result: Vec<(i64, f64, f64, f64)> = Vec::with_capacity(total_intervals as usize);
let mut current_interval = PriceInterval::new(timestamp_start, step_size);
while let Some(row) = rows.next()? {
let timestamp: i64 = row.get(0)?;
let sats: i64 = row.get(1)?;
let tokens: i64 = row.get(2)?;
if timestamp >= current_interval.end() {
if let Some(r) = current_interval.to_result() {
result.push(r);
}
loop {
current_interval = current_interval.next();
if timestamp < current_interval.end() {
break;
}
}
}
current_interval.add_pool(sats, tokens);
}
// final trade window
if let Some(r) = current_interval.to_result() {
result.push(r);
}
Ok(result)
}
#[get("/price/<token>/current")]
pub fn price_current(token: &str, conn: &State<DBPool>) -> Result<Json<Value>, Custom<String>> {
let db = conn
.get()
.map_err(|e| Custom(Status::InternalServerError, format!("Error: {}", e)))?;
let price = current_price(&db, token)
.map_err(|e| Custom(Status::InternalServerError, format!("Error: {}", e)))?;
Ok(Json(json!({
"price": price,
})))
}
#[get("/price/<token>/history?<start>&<end>&<stepsize>")]
pub fn price_history(
token: &str,
start: Option<i64>,
end: Option<i64>,
stepsize: Option<i64>,
conn: &State<DBPool>,
) -> Result<Json<Value>, Custom<String>> {
let current_timestamp = time_now();
let db = conn
.get()
.map_err(|e| Custom(Status::InternalServerError, format!("Error: {}", e)))?;
let history = historic_price(
&db,
start.unwrap_or(current_timestamp - 30 * 24 * 3600 /* 30 days */),
end.unwrap_or(current_timestamp),
stepsize.unwrap_or(3600 /* 1 hour */),
token,
)
.map_err(|e| Custom(Status::BadRequest, format!("Error: {}", e)))?;
let history_json: Vec<Value> = history
.iter()
.map(|(time, avg, max, min)| {
json!({
"time": time,
"avg": avg,
"max": max,
"min": min,
})
})
.collect();
Ok(Json(json!({
"history": json!(history_json)
})))
}