Add contract count for specific token
This commit is contained in:
parent
1b38f65224
commit
17f0d6017f
4 changed files with 321 additions and 268 deletions
69
src/main.rs
69
src/main.rs
|
|
@ -320,58 +320,6 @@ fn list_by_volume(
|
||||||
Ok(Json(result))
|
Ok(Json(result))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/price/<token>/current")]
|
|
||||||
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 (tvl_sats, tvl_tokens, price) = rpc::current_price(&db, token)
|
|
||||||
.map_err(|e| Custom(Status::InternalServerError, format!("Error: {}", e)))?;
|
|
||||||
|
|
||||||
Ok(Json(json!({
|
|
||||||
"price": price,
|
|
||||||
"tvl_sats": tvl_sats,
|
|
||||||
"tvl_tokens": tvl_tokens
|
|
||||||
})))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/price/<token>/history?<start>&<end>&<stepsize>")]
|
|
||||||
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 history = rpc::historic_price(
|
|
||||||
&conn.get().unwrap(),
|
|
||||||
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)
|
|
||||||
})))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/pool/list_by_apy")]
|
#[get("/pool/list_by_apy")]
|
||||||
fn list_pools_by_apy(conn: &State<DBPool>) -> Result<Json<Value>, Custom<String>> {
|
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())
|
let pools: Vec<rpc::PoolYield> = rpc::pools_by_apy(&conn.get().unwrap())
|
||||||
|
|
@ -382,16 +330,6 @@ fn list_pools_by_apy(conn: &State<DBPool>) -> Result<Json<Value>, Custom<String>
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/contract/count")]
|
|
||||||
fn contract_count(conn: &State<DBPool>) -> Result<Json<Value>, Custom<String>> {
|
|
||||||
let db = conn
|
|
||||||
.get()
|
|
||||||
.map_err(|e| Custom(Status::InternalServerError, format!("Error: {}", e)))?;
|
|
||||||
let count = rpc::contract_count(&db)
|
|
||||||
.map_err(|e| Custom(Status::BadRequest, format!("Error: {}", e)))?;
|
|
||||||
Ok(Json(json!(count)))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/contract/volume?<end>")]
|
#[get("/contract/volume?<end>")]
|
||||||
fn contract_volume(
|
fn contract_volume(
|
||||||
end: Option<i64>,
|
end: Option<i64>,
|
||||||
|
|
@ -553,10 +491,11 @@ fn launch() -> _ {
|
||||||
rpc::tvl::valuelocked_token,
|
rpc::tvl::valuelocked_token,
|
||||||
rpc::tvl::valuelocked_all,
|
rpc::tvl::valuelocked_all,
|
||||||
list_by_volume,
|
list_by_volume,
|
||||||
price_history,
|
rpc::price::price_history,
|
||||||
price_current,
|
rpc::price::price_current,
|
||||||
list_pools_by_apy,
|
list_pools_by_apy,
|
||||||
contract_count,
|
rpc::contract::contract_count_token,
|
||||||
|
rpc::contract::contract_count_all,
|
||||||
contract_volume
|
contract_volume
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
|
||||||
73
src/rpc/contract.rs
Normal file
73
src/rpc/contract.rs
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
// 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::Result;
|
||||||
|
use rocket::{get, http::Status, response::status::Custom, serde::json::Json, State};
|
||||||
|
use rusqlite::Connection;
|
||||||
|
use serde::Serialize;
|
||||||
|
use serde_json::{json, Value};
|
||||||
|
|
||||||
|
use crate::db::DBPool;
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct ContractCount {
|
||||||
|
active: u64,
|
||||||
|
ended: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn db_contract_count_all(db: &Connection) -> Result<ContractCount> {
|
||||||
|
let active: u64 = db.query_row(
|
||||||
|
"SELECT COUNT(*) FROM pool WHERE withdrawn_in_utxo IS NULL",
|
||||||
|
[],
|
||||||
|
|row| row.get(0),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let ended: u64 = db.query_row(
|
||||||
|
"SELECT COUNT(*) FROM pool WHERE withdrawn_in_utxo IS NOT NULL",
|
||||||
|
[],
|
||||||
|
|row| row.get(0),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(ContractCount { active, ended })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn db_contract_count_by_token(db: &Connection, token_id: &str) -> Result<ContractCount> {
|
||||||
|
let active: u64 = db.query_row(
|
||||||
|
"SELECT COUNT(*) FROM pool WHERE withdrawn_in_utxo IS NULL AND token_id = ?",
|
||||||
|
[token_id],
|
||||||
|
|row| row.get(0),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let ended: u64 = db.query_row(
|
||||||
|
"SELECT COUNT(*) FROM pool WHERE withdrawn_in_utxo IS NOT NULL AND token_id = ?",
|
||||||
|
[token_id],
|
||||||
|
|row| row.get(0),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(ContractCount { active, ended })
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/contract/count")]
|
||||||
|
pub fn contract_count_all(conn: &State<DBPool>) -> Result<Json<Value>, Custom<String>> {
|
||||||
|
let db = conn
|
||||||
|
.get()
|
||||||
|
.map_err(|e| Custom(Status::InternalServerError, format!("Error: {}", e)))?;
|
||||||
|
let count = db_contract_count_all(&db)
|
||||||
|
.map_err(|e| Custom(Status::BadRequest, format!("Error: {}", e)))?;
|
||||||
|
Ok(Json(json!(count)))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/contract/count/<token>")]
|
||||||
|
pub fn contract_count_token(
|
||||||
|
token: &str,
|
||||||
|
conn: &State<DBPool>,
|
||||||
|
) -> Result<Json<Value>, Custom<String>> {
|
||||||
|
let db = conn
|
||||||
|
.get()
|
||||||
|
.map_err(|e| Custom(Status::InternalServerError, format!("Error: {}", e)))?;
|
||||||
|
let count = db_contract_count_by_token(&db, token)
|
||||||
|
.map_err(|e| Custom(Status::BadRequest, format!("Error: {}", e)))?;
|
||||||
|
Ok(Json(json!(count)))
|
||||||
|
}
|
||||||
212
src/rpc/mod.rs
212
src/rpc/mod.rs
|
|
@ -3,15 +3,15 @@
|
||||||
// This software is licensed under the GNU Affero General Public License (AGPL), version 3.0 or later.
|
// 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
|
// 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 std::collections::HashMap;
|
||||||
use rusqlite::{params, Connection};
|
|
||||||
use rust_decimal::prelude::*;
|
|
||||||
use serde::Serialize;
|
|
||||||
use std::{
|
|
||||||
collections::HashMap,
|
|
||||||
time::{SystemTime, UNIX_EPOCH},
|
|
||||||
};
|
|
||||||
|
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use rusqlite::{params, Connection};
|
||||||
|
|
||||||
|
use crate::timeutil::time_now;
|
||||||
|
|
||||||
|
pub mod contract;
|
||||||
|
pub mod price;
|
||||||
pub mod tvl;
|
pub mod tvl;
|
||||||
|
|
||||||
#[allow(clippy::type_complexity)]
|
#[allow(clippy::type_complexity)]
|
||||||
|
|
@ -106,175 +106,6 @@ pub fn list_tokens_by_volume(
|
||||||
Ok(tokens)
|
Ok(tokens)
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
|
||||||
pub fn current_price(db: &Connection, token_id: &str) -> Result<(u64, u64, 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")?;
|
|
||||||
|
|
||||||
Ok((sum_sats, sum_tokens, price.to_f64().context("overflow")?))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::type_complexity)]
|
|
||||||
pub 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(serde::Serialize)]
|
#[derive(serde::Serialize)]
|
||||||
pub struct PoolYield {
|
pub struct PoolYield {
|
||||||
token_id: String,
|
token_id: String,
|
||||||
|
|
@ -338,10 +169,7 @@ pub fn pools_by_apy(connection: &Connection) -> Result<Vec<PoolYield>> {
|
||||||
|
|
||||||
let mut statement = connection.prepare(sql)?;
|
let mut statement = connection.prepare(sql)?;
|
||||||
|
|
||||||
let current_timestamp = SystemTime::now()
|
let current_timestamp = time_now();
|
||||||
.duration_since(UNIX_EPOCH)
|
|
||||||
.unwrap()
|
|
||||||
.as_secs() as i64;
|
|
||||||
|
|
||||||
let pool_rows = statement.query_map(params![], |row| {
|
let pool_rows = statement.query_map(params![], |row| {
|
||||||
let original_sats: i64 = row.get(0)?;
|
let original_sats: i64 = row.get(0)?;
|
||||||
|
|
@ -399,28 +227,6 @@ pub fn pools_by_apy(connection: &Connection) -> Result<Vec<PoolYield>> {
|
||||||
Ok(pools)
|
Ok(pools)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
pub struct ContractCount {
|
|
||||||
active: u64,
|
|
||||||
ended: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn contract_count(db: &Connection) -> Result<ContractCount> {
|
|
||||||
let active: u64 = db.query_row(
|
|
||||||
"SELECT COUNT(*) FROM pool WHERE withdrawn_in_utxo IS NULL",
|
|
||||||
[],
|
|
||||||
|row| row.get(0),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let ended: u64 = db.query_row(
|
|
||||||
"SELECT COUNT(*) FROM pool WHERE withdrawn_in_utxo IS NOT NULL",
|
|
||||||
[],
|
|
||||||
|row| row.get(0),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(ContractCount { active, ended })
|
|
||||||
}
|
|
||||||
|
|
||||||
fn all_time_volume(db: &Connection, end_timestamp: u64) -> Result<Vec<(String, i64)>> {
|
fn all_time_volume(db: &Connection, end_timestamp: u64) -> Result<Vec<(String, i64)>> {
|
||||||
let sql = "
|
let sql = "
|
||||||
SELECT
|
SELECT
|
||||||
|
|
|
||||||
235
src/rpc/price.rs
Normal file
235
src/rpc/price.rs
Normal file
|
|
@ -0,0 +1,235 @@
|
||||||
|
// 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)
|
||||||
|
})))
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue