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};
|
2024-10-14 11:18:50 +00:00
|
|
|
use bitcoin_hashes::hex::{FromHex, ToHex};
|
|
|
|
|
use bitcoincash::TokenID;
|
2024-04-03 11:39:49 +02:00
|
|
|
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};
|
|
|
|
|
|
2024-11-28 08:45:21 +01:00
|
|
|
use crate::{
|
|
|
|
|
db::{cauldron::pool::db_list_active_pools, DB},
|
|
|
|
|
timeutil::time_now,
|
|
|
|
|
};
|
2024-04-03 11:39:49 +02:00
|
|
|
|
|
|
|
|
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> {
|
2024-11-28 08:45:21 +01:00
|
|
|
let active = db_list_active_pools(Some(token_id), None, db, false)?;
|
2024-04-03 11:39:49 +02:00
|
|
|
|
2024-11-28 08:45:21 +01:00
|
|
|
let mut sats: u128 = 0;
|
|
|
|
|
let mut tokens: u128 = 0;
|
2024-04-03 11:39:49 +02:00
|
|
|
|
2024-11-28 08:45:21 +01:00
|
|
|
for pool in active {
|
|
|
|
|
sats += pool.sats as u128;
|
|
|
|
|
tokens += pool.tokens as u128;
|
2024-04-03 11:39:49 +02:00
|
|
|
}
|
|
|
|
|
|
2024-11-28 08:45:21 +01:00
|
|
|
let sum_sats = Decimal::from_u128(sats).context("overflow")?;
|
|
|
|
|
let sum_tokens = Decimal::from_u128(tokens).context("overflow")?;
|
2024-06-06 08:03:12 +02:00
|
|
|
|
|
|
|
|
let price = sum_sats.checked_div(sum_tokens).unwrap_or_default();
|
2024-04-03 11:39:49 +02:00
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-14 11:18:50 +00:00
|
|
|
fn price_at_or_before(
|
|
|
|
|
connection: &Connection,
|
|
|
|
|
timestamp: i64,
|
|
|
|
|
token_id: &TokenID,
|
|
|
|
|
) -> Result<(i64, f64)> {
|
|
|
|
|
let sql = "
|
|
|
|
|
WITH max_timestamps AS (
|
2024-10-21 12:48:47 +02:00
|
|
|
SELECT
|
2024-10-14 11:18:50 +00:00
|
|
|
phe.pool,
|
|
|
|
|
MAX(COALESCE(phe.first_seen_timestamp, phe.mtp_timestamp)) AS max_effective_timestamp
|
2024-10-21 12:48:47 +02:00
|
|
|
FROM
|
2024-10-14 11:18:50 +00:00
|
|
|
pool_history_entry phe
|
2024-10-21 12:48:47 +02:00
|
|
|
JOIN
|
2024-10-14 11:18:50 +00:00
|
|
|
utxo_funding uf ON phe.utxo = uf.new_utxo_hash
|
2024-10-21 12:48:47 +02:00
|
|
|
WHERE
|
2024-10-14 11:18:50 +00:00
|
|
|
COALESCE(phe.first_seen_timestamp, phe.mtp_timestamp) <= ?
|
|
|
|
|
AND uf.token_id = ?
|
2024-10-21 12:48:47 +02:00
|
|
|
GROUP BY
|
2024-10-14 11:18:50 +00:00
|
|
|
phe.pool
|
|
|
|
|
)
|
|
|
|
|
SELECT
|
|
|
|
|
uf.token_amount,
|
|
|
|
|
uf.sats,
|
|
|
|
|
COALESCE(phe.first_seen_timestamp, phe.mtp_timestamp) AS effective_timestamp
|
2024-10-21 12:48:47 +02:00
|
|
|
FROM
|
2024-10-14 11:18:50 +00:00
|
|
|
pool_history_entry phe
|
2024-10-21 12:48:47 +02:00
|
|
|
JOIN
|
2024-10-14 11:18:50 +00:00
|
|
|
utxo_funding uf ON phe.utxo = uf.new_utxo_hash
|
2024-10-21 12:48:47 +02:00
|
|
|
JOIN
|
2024-10-14 11:18:50 +00:00
|
|
|
pool p ON p.creation_utxo = phe.pool
|
2024-10-21 12:48:47 +02:00
|
|
|
JOIN
|
2024-10-14 11:18:50 +00:00
|
|
|
max_timestamps mt ON phe.pool = mt.pool
|
|
|
|
|
AND COALESCE(phe.first_seen_timestamp, phe.mtp_timestamp) = mt.max_effective_timestamp
|
2024-10-21 12:48:47 +02:00
|
|
|
WHERE
|
2024-10-14 11:18:50 +00:00
|
|
|
p.withdrawn_in_utxo IS NULL
|
|
|
|
|
";
|
|
|
|
|
|
|
|
|
|
let mut statement = connection.prepare(sql)?;
|
|
|
|
|
let mut rows = statement.query(params![timestamp, token_id.to_hex()])?;
|
|
|
|
|
|
|
|
|
|
let mut sum_sats: u64 = 0;
|
|
|
|
|
let mut sum_tokens: u64 = 0;
|
|
|
|
|
let mut latest_timestamp = 0;
|
|
|
|
|
|
|
|
|
|
while let Some(row) = rows.next()? {
|
|
|
|
|
let token_amount: i64 = row.get(0)?;
|
|
|
|
|
let sats: i64 = row.get(1)?;
|
|
|
|
|
let row_timestamp: i64 = row.get(2)?;
|
|
|
|
|
|
|
|
|
|
if sats >= 0 && token_amount >= 0 {
|
|
|
|
|
sum_sats += sats as u64;
|
|
|
|
|
sum_tokens += token_amount as u64;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if row_timestamp > latest_timestamp {
|
|
|
|
|
latest_timestamp = row_timestamp;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let sum_sats_decimal = Decimal::from_u64(sum_sats).unwrap_or_default();
|
|
|
|
|
let sum_tokens_decimal = Decimal::from_u64(sum_tokens).unwrap_or_default();
|
|
|
|
|
|
|
|
|
|
let overall_price = sum_sats_decimal
|
|
|
|
|
.checked_div(sum_tokens_decimal)
|
|
|
|
|
.context("Division failed: sum_tokens is zero or invalid")?;
|
|
|
|
|
|
|
|
|
|
let overall_price_f64 = overall_price.to_f64().context("Conversion to f64 failed")?;
|
|
|
|
|
|
|
|
|
|
Ok((latest_timestamp, overall_price_f64))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[get("/price/<token>/at/<timestamp>")]
|
|
|
|
|
pub fn price_at(
|
|
|
|
|
token: &str,
|
|
|
|
|
timestamp: &str,
|
|
|
|
|
conn: &State<DB>,
|
|
|
|
|
) -> Result<Json<Value>, Custom<String>> {
|
|
|
|
|
let timestamp: i64 = timestamp.parse().map_err(|_| {
|
|
|
|
|
Custom(
|
|
|
|
|
Status::BadRequest,
|
|
|
|
|
"Invalid timestamp format. Must be a valid number.".to_string(),
|
|
|
|
|
)
|
|
|
|
|
})?;
|
|
|
|
|
|
|
|
|
|
let db = conn
|
|
|
|
|
.cauldron_r
|
|
|
|
|
.get()
|
|
|
|
|
.map_err(|e| Custom(Status::InternalServerError, format!("Error: {}", e)))?;
|
|
|
|
|
|
|
|
|
|
let current_time = time_now();
|
|
|
|
|
if timestamp > current_time {
|
|
|
|
|
return Err(Custom(
|
|
|
|
|
Status::BadRequest,
|
|
|
|
|
"Timestamp is in the future".to_string(),
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
let token = TokenID::from_hex(token)
|
|
|
|
|
.map_err(|e| Custom(Status::BadRequest, format!("Error: {}", e)))?;
|
|
|
|
|
|
|
|
|
|
match price_at_or_before(&db, timestamp, &token) {
|
|
|
|
|
Ok((latest_timestamp, price)) => Ok(Json(json!({
|
|
|
|
|
"timestamp": latest_timestamp,
|
|
|
|
|
"price": price
|
|
|
|
|
}))),
|
|
|
|
|
Err(e) => Err(Custom(
|
|
|
|
|
Status::InternalServerError,
|
|
|
|
|
format!("Error fetching price: {}", e),
|
|
|
|
|
)),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-04-03 11:39:49 +02:00
|
|
|
#[get("/price/<token>/current")]
|
2024-05-09 11:25:27 +02:00
|
|
|
pub fn price_current(token: &str, conn: &State<DB>) -> Result<Json<Value>, Custom<String>> {
|
2024-04-03 11:39:49 +02:00
|
|
|
let db = conn
|
2024-05-09 11:25:27 +02:00
|
|
|
.cauldron_r
|
2024-04-03 11:39:49 +02:00
|
|
|
.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>,
|
2024-05-09 11:25:27 +02:00
|
|
|
conn: &State<DB>,
|
2024-04-03 11:39:49 +02:00
|
|
|
) -> Result<Json<Value>, Custom<String>> {
|
|
|
|
|
let current_timestamp = time_now();
|
|
|
|
|
|
|
|
|
|
let db = conn
|
2024-05-09 11:25:27 +02:00
|
|
|
.cauldron_r
|
2024-04-03 11:39:49 +02:00
|
|
|
.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)
|
|
|
|
|
})))
|
|
|
|
|
}
|
2024-10-14 11:18:50 +00:00
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
2024-10-24 09:12:12 +02:00
|
|
|
use crate::db::cauldron::{
|
|
|
|
|
pool::{
|
|
|
|
|
self, dummy_init_seq, flag_as_withdrawn, insert_new_pool, insert_pool_history_entry,
|
|
|
|
|
},
|
|
|
|
|
tx::{self, insert_block_tx, insert_mempool_tx},
|
|
|
|
|
utxo_funding::{self, insert_utxo_funding},
|
|
|
|
|
};
|
2024-10-31 10:55:29 +00:00
|
|
|
use crate::utiltest::mock_db_pool;
|
2024-10-14 11:18:50 +00:00
|
|
|
|
|
|
|
|
use super::*;
|
|
|
|
|
use bitcoin_hashes::Hash;
|
2024-10-24 09:12:12 +02:00
|
|
|
use bitcoincash::{BlockHash, PubkeyHash, Txid};
|
|
|
|
|
use riftenlabs_defi::{cauldron::ParsedContract, chainutil::OutPointHash};
|
2024-10-14 11:18:50 +00:00
|
|
|
use rocket::http::Status;
|
|
|
|
|
use rocket::local::blocking::Client;
|
|
|
|
|
use rocket::routes;
|
2024-10-24 09:12:12 +02:00
|
|
|
use rusqlite::Connection;
|
2024-10-14 11:18:50 +00:00
|
|
|
|
2024-10-24 09:12:12 +02:00
|
|
|
const TIME_1: u64 = 1727963300;
|
|
|
|
|
const TIME_2: u64 = 1727963350;
|
|
|
|
|
const TIME_3: u64 = 1727963400;
|
|
|
|
|
|
|
|
|
|
fn dummy_cauldron(
|
|
|
|
|
txid: &Txid,
|
|
|
|
|
utxo: &OutPointHash,
|
|
|
|
|
token: &TokenID,
|
|
|
|
|
sats: u64,
|
|
|
|
|
tokens: i64,
|
|
|
|
|
pkh: &PubkeyHash,
|
|
|
|
|
) -> ParsedContract {
|
|
|
|
|
ParsedContract {
|
|
|
|
|
pkh: pkh.clone(),
|
|
|
|
|
is_withdrawn: false,
|
|
|
|
|
spent_utxo_hash: OutPointHash::all_zeros(),
|
|
|
|
|
new_utxo_hash: Some(utxo.clone()),
|
|
|
|
|
new_utxo_txid: Some(txid.clone()),
|
|
|
|
|
new_utxo_n: Some(0),
|
|
|
|
|
token_id: Some(token.clone()),
|
|
|
|
|
sats: Some(sats),
|
|
|
|
|
token_amount: Some(tokens),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn setup_mock_db(conn: &Connection) {
|
|
|
|
|
utxo_funding::create_table(conn);
|
|
|
|
|
tx::create_table(conn);
|
|
|
|
|
pool::create_table(conn);
|
|
|
|
|
dummy_init_seq();
|
|
|
|
|
|
|
|
|
|
let token_zero = TokenID::all_zeros();
|
|
|
|
|
let pkh_zero = PubkeyHash::all_zeros();
|
|
|
|
|
|
|
|
|
|
let txid1 = Txid::from_inner([0xf0; 32]);
|
|
|
|
|
let txid2 = Txid::from_inner([0xf1; 32]);
|
|
|
|
|
let txid3 = Txid::from_inner([0xf2; 32]);
|
|
|
|
|
let utxo1 = OutPointHash::from_inner([0xe0; 32]);
|
|
|
|
|
let utxo2 = OutPointHash::from_inner([0xe1; 32]);
|
|
|
|
|
let utxo3 = OutPointHash::from_inner([0xe2; 32]);
|
2024-10-14 11:18:50 +00:00
|
|
|
|
|
|
|
|
// Insert mock data into utxo_funding
|
2024-10-24 09:12:12 +02:00
|
|
|
let cauldron1 = dummy_cauldron(&txid1, &utxo1, &token_zero, 50000, 1000, &pkh_zero);
|
|
|
|
|
let cauldron2 = dummy_cauldron(&txid2, &utxo2, &token_zero, 60000, 2000, &pkh_zero);
|
|
|
|
|
let cauldron3 = dummy_cauldron(&txid2, &utxo3, &token_zero, 90000, 3000, &pkh_zero);
|
|
|
|
|
insert_utxo_funding(&conn, &vec![cauldron1.clone()], &txid1, true).unwrap();
|
|
|
|
|
insert_utxo_funding(&conn, &vec![cauldron2.clone()], &txid2, true).unwrap();
|
|
|
|
|
insert_utxo_funding(&conn, &vec![cauldron3.clone()], &txid3, true).unwrap();
|
2024-10-14 11:18:50 +00:00
|
|
|
|
|
|
|
|
// Insert mock data into tx table
|
2024-10-24 09:12:12 +02:00
|
|
|
let block_zero = BlockHash::all_zeros();
|
|
|
|
|
insert_block_tx(&conn, &txid1, &block_zero, TIME_1 as i64).unwrap();
|
|
|
|
|
insert_mempool_tx(&conn, &txid1, TIME_1).unwrap();
|
|
|
|
|
insert_block_tx(&conn, &txid2, &block_zero, TIME_2 as i64).unwrap();
|
|
|
|
|
insert_mempool_tx(&conn, &txid2, TIME_2).unwrap();
|
|
|
|
|
insert_block_tx(&conn, &txid3, &block_zero, TIME_3 as i64).unwrap();
|
|
|
|
|
insert_mempool_tx(&conn, &txid3, TIME_3).unwrap();
|
2024-10-14 11:18:50 +00:00
|
|
|
|
|
|
|
|
// Insert mock data into `pool_history_entry`
|
2024-10-24 09:12:12 +02:00
|
|
|
let pool1 = OutPointHash::from_inner([0x0a; 32]);
|
|
|
|
|
let pool2 = OutPointHash::from_inner([0x0b; 32]);
|
|
|
|
|
let pool3 = OutPointHash::from_inner([0x0c; 32]);
|
|
|
|
|
let txid1_newer = Txid::from_inner([0xf3; 32]);
|
|
|
|
|
let utxo1_newer = OutPointHash::from_inner([0xe3; 32]);
|
|
|
|
|
insert_pool_history_entry(&conn, &pool1, &cauldron1, Some(TIME_1), Some(TIME_1)).unwrap();
|
|
|
|
|
insert_pool_history_entry(&conn, &pool2, &cauldron2, Some(TIME_2), Some(TIME_2)).unwrap();
|
|
|
|
|
insert_pool_history_entry(&conn, &pool3, &cauldron3, Some(TIME_3), Some(TIME_3)).unwrap();
|
2024-10-14 11:18:50 +00:00
|
|
|
|
|
|
|
|
// Insert newer entry for pool1
|
2024-10-24 09:12:12 +02:00
|
|
|
let cauldron1_newer = dummy_cauldron(
|
|
|
|
|
&txid1_newer,
|
|
|
|
|
&utxo1_newer,
|
|
|
|
|
&token_zero,
|
|
|
|
|
70000,
|
|
|
|
|
1500,
|
|
|
|
|
&pkh_zero,
|
|
|
|
|
);
|
|
|
|
|
insert_pool_history_entry(
|
|
|
|
|
&conn,
|
|
|
|
|
&pool1,
|
|
|
|
|
&cauldron1_newer,
|
|
|
|
|
Some(1727963500),
|
|
|
|
|
Some(1727963500),
|
2024-10-14 11:18:50 +00:00
|
|
|
)
|
2024-10-24 09:12:12 +02:00
|
|
|
.unwrap();
|
|
|
|
|
insert_utxo_funding(&conn, &vec![cauldron1_newer], &txid1_newer, true).unwrap();
|
2024-10-14 11:18:50 +00:00
|
|
|
|
|
|
|
|
// Insert active pools with required columns (owner_pkh and token_id)
|
2024-10-24 09:12:12 +02:00
|
|
|
let token1 = TokenID::from_inner([0xda; 32]);
|
|
|
|
|
let token2 = TokenID::from_inner([0xdb; 32]);
|
|
|
|
|
let token3 = TokenID::from_inner([0xdc; 32]);
|
|
|
|
|
let pkh1 = PubkeyHash::from_inner([0xca; 20]);
|
|
|
|
|
let pkh2 = PubkeyHash::from_inner([0xcb; 20]);
|
|
|
|
|
let pkh3 = PubkeyHash::from_inner([0xcc; 20]);
|
|
|
|
|
insert_new_pool(
|
|
|
|
|
&conn,
|
|
|
|
|
&dummy_cauldron(&Txid::all_zeros(), &pool1, &token1, 0, 0, &pkh1),
|
2024-10-14 11:18:50 +00:00
|
|
|
)
|
2024-10-24 09:12:12 +02:00
|
|
|
.unwrap();
|
|
|
|
|
insert_new_pool(
|
|
|
|
|
&conn,
|
|
|
|
|
&dummy_cauldron(&Txid::all_zeros(), &pool2, &token2, 0, 0, &pkh2),
|
2024-10-14 11:18:50 +00:00
|
|
|
)
|
2024-10-24 09:12:12 +02:00
|
|
|
.unwrap();
|
|
|
|
|
insert_new_pool(
|
|
|
|
|
&conn,
|
|
|
|
|
&dummy_cauldron(&Txid::all_zeros(), &pool3, &token3, 0, 0, &pkh3),
|
2024-10-14 11:18:50 +00:00
|
|
|
)
|
2024-10-24 09:12:12 +02:00
|
|
|
.unwrap();
|
2024-10-14 11:18:50 +00:00
|
|
|
|
|
|
|
|
// Insert an inactive pool
|
2024-10-24 09:12:12 +02:00
|
|
|
let inactive_pool = OutPointHash::from_inner([0xaa; 32]);
|
|
|
|
|
//let inactive_withdrawn_utxo = OutPointHash::from_inner([0xab; 32]);
|
|
|
|
|
let inactive_token = TokenID::from_inner([0xac; 32]);
|
|
|
|
|
let inactive_pkh = PubkeyHash::from_inner([0xac; 20]);
|
|
|
|
|
let inactive_txid = Txid::from_inner([0xad; 32]);
|
|
|
|
|
let inactive_cauldron = dummy_cauldron(
|
|
|
|
|
&inactive_txid,
|
|
|
|
|
&inactive_pool,
|
|
|
|
|
&inactive_token,
|
|
|
|
|
80000,
|
|
|
|
|
2500,
|
|
|
|
|
&inactive_pkh,
|
|
|
|
|
);
|
|
|
|
|
let inactive_cauldron_withdraw = ParsedContract {
|
|
|
|
|
pkh: inactive_pkh,
|
|
|
|
|
is_withdrawn: true,
|
|
|
|
|
spent_utxo_hash: inactive_pool,
|
|
|
|
|
new_utxo_hash: None,
|
|
|
|
|
new_utxo_txid: None,
|
|
|
|
|
new_utxo_n: None,
|
|
|
|
|
token_id: None,
|
|
|
|
|
sats: None,
|
|
|
|
|
token_amount: None,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
insert_new_pool(&conn, &inactive_cauldron).unwrap();
|
|
|
|
|
insert_utxo_funding(
|
|
|
|
|
&conn,
|
|
|
|
|
&vec![inactive_cauldron.clone()],
|
|
|
|
|
&inactive_txid,
|
|
|
|
|
true,
|
2024-10-14 11:18:50 +00:00
|
|
|
)
|
2024-10-24 09:12:12 +02:00
|
|
|
.unwrap();
|
|
|
|
|
insert_pool_history_entry(
|
|
|
|
|
&conn,
|
|
|
|
|
&inactive_pool,
|
|
|
|
|
&inactive_cauldron,
|
|
|
|
|
Some(1727963350),
|
|
|
|
|
Some(1727963350),
|
2024-10-14 11:18:50 +00:00
|
|
|
)
|
2024-10-24 09:12:12 +02:00
|
|
|
.unwrap();
|
|
|
|
|
flag_as_withdrawn(&conn, &inactive_pool, &inactive_cauldron_withdraw).unwrap();
|
2024-10-14 11:18:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_price_at_specific_time() {
|
2024-10-31 10:55:29 +00:00
|
|
|
let mock_db = mock_db_pool(setup_mock_db);
|
2024-10-14 11:18:50 +00:00
|
|
|
|
|
|
|
|
let rocket = rocket::build()
|
|
|
|
|
.manage(mock_db)
|
|
|
|
|
.mount("/cauldron", routes![price_at]);
|
|
|
|
|
|
|
|
|
|
let client = Client::tracked(rocket).expect("valid rocket instance");
|
|
|
|
|
|
|
|
|
|
let token_id = TokenID::all_zeros().to_hex();
|
|
|
|
|
|
|
|
|
|
// Test 1: Querying at a timestamp that exactly matches test_txid1
|
2024-10-24 09:12:12 +02:00
|
|
|
let timestamp = TIME_1;
|
2024-10-14 11:18:50 +00:00
|
|
|
let response = client
|
|
|
|
|
.get(format!("/cauldron/price/{}/at/{}", token_id, timestamp))
|
|
|
|
|
.dispatch();
|
|
|
|
|
assert_eq!(response.status(), Status::Ok);
|
|
|
|
|
|
|
|
|
|
let json_value: serde_json::Value =
|
|
|
|
|
serde_json::from_str(response.into_string().unwrap().as_str()).unwrap();
|
|
|
|
|
let actual_price = json_value["price"]
|
|
|
|
|
.as_f64()
|
|
|
|
|
.expect("Price field is not a valid f64");
|
|
|
|
|
|
|
|
|
|
// Expected price based on 50,000 sats and 1,000 tokens
|
|
|
|
|
let expected_price = 50.0;
|
|
|
|
|
assert!((actual_price - expected_price).abs() < 0.01);
|
|
|
|
|
|
|
|
|
|
// Test 2: Querying at a timestamp that includes Pool 1 and Pool 2
|
2024-10-24 09:12:12 +02:00
|
|
|
let timestamp = TIME_2;
|
2024-10-14 11:18:50 +00:00
|
|
|
let response = client
|
|
|
|
|
.get(format!("/cauldron/price/{}/at/{}", token_id, timestamp))
|
|
|
|
|
.dispatch();
|
|
|
|
|
assert_eq!(response.status(), Status::Ok);
|
|
|
|
|
let json_value: serde_json::Value =
|
|
|
|
|
serde_json::from_str(response.into_string().unwrap().as_str()).unwrap();
|
|
|
|
|
let actual_price = json_value["price"]
|
|
|
|
|
.as_f64()
|
|
|
|
|
.expect("Price field is not a valid f64");
|
|
|
|
|
|
|
|
|
|
// The combined price should be 36.67 (rounded)
|
|
|
|
|
let expected_price = 36.67;
|
2024-10-24 09:12:12 +02:00
|
|
|
assert!(
|
|
|
|
|
(actual_price - expected_price).abs() < 0.01,
|
|
|
|
|
"expected {} != actual {}",
|
|
|
|
|
expected_price,
|
|
|
|
|
actual_price
|
|
|
|
|
);
|
2024-10-14 11:18:50 +00:00
|
|
|
|
|
|
|
|
// Test 3: Querying a timestamp that includes Pool 1, Pool 2, and Pool 3
|
2024-10-24 09:12:12 +02:00
|
|
|
let timestamp = TIME_3;
|
2024-10-14 11:18:50 +00:00
|
|
|
let response = client
|
|
|
|
|
.get(format!("/cauldron/price/{}/at/{}", token_id, timestamp))
|
|
|
|
|
.dispatch();
|
|
|
|
|
assert_eq!(response.status(), Status::Ok);
|
|
|
|
|
let json_value: serde_json::Value =
|
|
|
|
|
serde_json::from_str(response.into_string().unwrap().as_str()).unwrap();
|
|
|
|
|
let actual_price = json_value["price"]
|
|
|
|
|
.as_f64()
|
|
|
|
|
.expect("Price field is not a valid f64");
|
|
|
|
|
|
|
|
|
|
// The combined price should be 33.33 (rounded)
|
|
|
|
|
let expected_price = 33.33;
|
|
|
|
|
assert!((actual_price - expected_price).abs() < 0.01);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_price_at_newer_timestamp_for_pool1() {
|
2024-10-31 10:55:29 +00:00
|
|
|
let mock_db = mock_db_pool(setup_mock_db);
|
2024-10-14 11:18:50 +00:00
|
|
|
|
|
|
|
|
let rocket = rocket::build()
|
|
|
|
|
.manage(mock_db.clone())
|
|
|
|
|
.mount("/cauldron", routes![price_at]);
|
|
|
|
|
|
|
|
|
|
let client = Client::tracked(rocket).expect("valid rocket instance");
|
|
|
|
|
|
|
|
|
|
let token_id = TokenID::all_zeros().to_hex();
|
|
|
|
|
|
|
|
|
|
// Test: Query at a newer timestamp (1727963500) for pool1 and ensure it only accounts for the newer entry
|
|
|
|
|
let timestamp = 1727963500; // Newer timestamp
|
|
|
|
|
let response = client
|
|
|
|
|
.get(format!("/cauldron/price/{}/at/{}", token_id, timestamp))
|
|
|
|
|
.dispatch();
|
|
|
|
|
assert_eq!(response.status(), Status::Ok);
|
|
|
|
|
|
|
|
|
|
let json_value: serde_json::Value =
|
|
|
|
|
serde_json::from_str(response.into_string().unwrap().as_str()).unwrap();
|
|
|
|
|
let actual_price = json_value["price"]
|
|
|
|
|
.as_f64()
|
|
|
|
|
.expect("Price field is not a valid f64");
|
|
|
|
|
|
|
|
|
|
// Expected price based on the total from pool1, pool2, and pool3
|
|
|
|
|
let expected_price = 33.85; // Rounded to 2 decimal places
|
2024-10-24 09:12:12 +02:00
|
|
|
assert!(
|
|
|
|
|
(actual_price - expected_price).abs() < 0.01,
|
|
|
|
|
"expected {} != actual {}",
|
|
|
|
|
expected_price,
|
|
|
|
|
actual_price
|
|
|
|
|
);
|
2024-10-14 11:18:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_price_with_multiple_entries_at_same_timestamp() {
|
2024-10-31 10:55:29 +00:00
|
|
|
let mock_db = mock_db_pool(setup_mock_db);
|
2024-10-14 11:18:50 +00:00
|
|
|
|
|
|
|
|
let rocket = rocket::build()
|
|
|
|
|
.manage(mock_db.clone())
|
|
|
|
|
.mount("/cauldron", routes![price_at]);
|
|
|
|
|
|
|
|
|
|
let client = Client::tracked(rocket).expect("valid rocket instance");
|
|
|
|
|
|
|
|
|
|
let token_id = TokenID::all_zeros().to_hex();
|
|
|
|
|
|
|
|
|
|
// Insert additional entries with the same timestamp for an existing pool (e.g., pool1)
|
2024-10-24 09:12:12 +02:00
|
|
|
let conn = mock_db.cauldron_w.get().expect("Failed to get connection.");
|
2024-10-14 11:18:50 +00:00
|
|
|
|
|
|
|
|
// These entries should have the same timestamp as previous mockdata and be counted in the price calculation
|
2024-10-24 09:12:12 +02:00
|
|
|
let pool1 = OutPointHash::from_inner([0x0a; 32]);
|
|
|
|
|
let txid = Txid::hash("txid_extra".as_bytes());
|
|
|
|
|
|
|
|
|
|
let cauldron = dummy_cauldron(
|
|
|
|
|
&txid,
|
|
|
|
|
&OutPointHash::hash("utxo_extra".as_bytes()),
|
|
|
|
|
&TokenID::all_zeros(),
|
|
|
|
|
40000,
|
|
|
|
|
500,
|
|
|
|
|
&PubkeyHash::all_zeros(),
|
|
|
|
|
);
|
|
|
|
|
insert_pool_history_entry(&conn, &pool1, &cauldron, Some(TIME_1), Some(TIME_1)).unwrap();
|
|
|
|
|
insert_utxo_funding(&conn, &vec![cauldron], &txid, true).unwrap();
|
2024-10-14 11:18:50 +00:00
|
|
|
|
|
|
|
|
// Test: Query at the timestamp matching test_txid1 (1727963300) and check the price
|
2024-10-24 09:12:12 +02:00
|
|
|
let timestamp = TIME_1;
|
2024-10-14 11:18:50 +00:00
|
|
|
let response = client
|
|
|
|
|
.get(format!("/cauldron/price/{}/at/{}", token_id, timestamp))
|
|
|
|
|
.dispatch();
|
|
|
|
|
assert_eq!(response.status(), Status::Ok);
|
|
|
|
|
|
|
|
|
|
let json_value: serde_json::Value =
|
|
|
|
|
serde_json::from_str(response.into_string().unwrap().as_str()).unwrap();
|
|
|
|
|
let actual_price = json_value["price"]
|
|
|
|
|
.as_f64()
|
|
|
|
|
.expect("Price field is not a valid f64");
|
|
|
|
|
|
|
|
|
|
// Expected price based on (50,000 + 40,000) sats and (1,000 + 500) tokens = 90,000 / 1,500 = 60.0
|
|
|
|
|
let expected_price = 60.0;
|
|
|
|
|
assert!((actual_price - expected_price).abs() < 0.01);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_bad_token_hash() {
|
2024-10-31 10:55:29 +00:00
|
|
|
let mock_db = mock_db_pool(setup_mock_db);
|
2024-10-14 11:18:50 +00:00
|
|
|
|
|
|
|
|
let rocket = rocket::build()
|
|
|
|
|
.manage(mock_db)
|
|
|
|
|
.mount("/cauldron", routes![price_current, price_history, price_at]);
|
|
|
|
|
|
|
|
|
|
let client = Client::tracked(rocket).expect("valid rocket instance");
|
|
|
|
|
|
|
|
|
|
// Test the `/price/<token>/at/<timestamp>` endpoint with a bad token hash
|
|
|
|
|
let bad_token_id = "bad_token_id";
|
|
|
|
|
let timestamp = 1727963432; // Arbitrary timestamp
|
|
|
|
|
|
|
|
|
|
let response = client
|
|
|
|
|
.get(format!("/cauldron/price/{}/at/{}", bad_token_id, timestamp))
|
|
|
|
|
.dispatch();
|
|
|
|
|
assert_eq!(response.status(), Status::BadRequest);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_timestamp_in_future() {
|
2024-10-31 10:55:29 +00:00
|
|
|
let mock_db = mock_db_pool(setup_mock_db);
|
2024-10-14 11:18:50 +00:00
|
|
|
|
|
|
|
|
let rocket = rocket::build()
|
|
|
|
|
.manage(mock_db)
|
|
|
|
|
.mount("/cauldron", routes![price_at]);
|
|
|
|
|
|
|
|
|
|
let client = Client::tracked(rocket).expect("valid rocket instance");
|
|
|
|
|
|
|
|
|
|
let token_id = TokenID::all_zeros().to_hex();
|
|
|
|
|
let future_timestamp = (time_now() + 100000).to_string();
|
|
|
|
|
|
|
|
|
|
let response = client
|
|
|
|
|
.get(format!(
|
|
|
|
|
"/cauldron/price/{}/at/{}",
|
|
|
|
|
token_id, future_timestamp
|
|
|
|
|
))
|
|
|
|
|
.dispatch();
|
|
|
|
|
|
|
|
|
|
assert_eq!(response.status(), Status::BadRequest);
|
|
|
|
|
let body = response.into_string().unwrap();
|
|
|
|
|
assert!(body.contains("Timestamp is in the future"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_invalid_timestamp_format() {
|
2024-10-31 10:55:29 +00:00
|
|
|
let mock_db = mock_db_pool(setup_mock_db);
|
2024-10-14 11:18:50 +00:00
|
|
|
|
|
|
|
|
let rocket = rocket::build()
|
|
|
|
|
.manage(mock_db)
|
|
|
|
|
.mount("/cauldron", routes![price_at]);
|
|
|
|
|
|
|
|
|
|
let client = Client::tracked(rocket).expect("valid rocket instance");
|
|
|
|
|
|
|
|
|
|
// Test the `/price/<token>/at/<timestamp>` endpoint with an invalid timestamp
|
|
|
|
|
let token_id = TokenID::all_zeros().to_hex();
|
|
|
|
|
let invalid_timestamp = "ASDASD:";
|
|
|
|
|
|
|
|
|
|
let response = client
|
|
|
|
|
.get(format!(
|
|
|
|
|
"/cauldron/price/{}/at/{}",
|
|
|
|
|
token_id, invalid_timestamp
|
|
|
|
|
))
|
|
|
|
|
.dispatch();
|
|
|
|
|
|
|
|
|
|
assert_eq!(response.status(), Status::BadRequest);
|
|
|
|
|
let body = response.into_string().unwrap();
|
|
|
|
|
assert!(body.contains("Invalid timestamp format"));
|
|
|
|
|
}
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_price_with_high_tokens_and_low_sats() {
|
2024-10-31 10:55:29 +00:00
|
|
|
let mock_db = mock_db_pool(setup_mock_db);
|
2024-10-14 11:18:50 +00:00
|
|
|
|
|
|
|
|
let rocket = rocket::build()
|
|
|
|
|
.manage(mock_db.clone())
|
|
|
|
|
.mount("/cauldron", routes![price_at]);
|
|
|
|
|
|
|
|
|
|
let client = Client::tracked(rocket).expect("valid rocket instance");
|
|
|
|
|
|
2024-10-24 09:12:12 +02:00
|
|
|
let conn = mock_db.cauldron_w.get().expect("Failed to get connection.");
|
2024-10-14 11:18:50 +00:00
|
|
|
|
|
|
|
|
// Insert high tokens and low sats
|
2024-10-24 09:12:12 +02:00
|
|
|
let pool = OutPointHash::hash("many tokens pool".as_bytes());
|
|
|
|
|
let txid = Txid::hash("many tokens txid".as_bytes());
|
|
|
|
|
let utxo = OutPointHash::hash("many tokens utxo".as_bytes());
|
|
|
|
|
let token = TokenID::hash("many tokens tokenid".as_bytes());
|
|
|
|
|
|
|
|
|
|
let cauldron = dummy_cauldron(
|
|
|
|
|
&txid,
|
|
|
|
|
&utxo,
|
|
|
|
|
&token,
|
|
|
|
|
1, /* low sats */
|
|
|
|
|
9999999999999999, /* many tokens */
|
|
|
|
|
&PubkeyHash::all_zeros(),
|
|
|
|
|
);
|
|
|
|
|
insert_utxo_funding(&conn, &vec![cauldron.clone()], &txid, true).unwrap();
|
|
|
|
|
insert_pool_history_entry(&conn, &pool, &cauldron, Some(TIME_1), Some(TIME_1)).unwrap();
|
2024-10-14 11:18:50 +00:00
|
|
|
|
|
|
|
|
// Test: Querying the price with high tokens and low sats
|
|
|
|
|
let response = client
|
2024-10-24 09:12:12 +02:00
|
|
|
.get(format!("/cauldron/price/{}/at/{}", token.to_hex(), TIME_1))
|
2024-10-14 11:18:50 +00:00
|
|
|
.dispatch();
|
|
|
|
|
|
|
|
|
|
// Expect that we could not handle the calculation/conversion.
|
|
|
|
|
assert_eq!(response.status(), Status::InternalServerError);
|
|
|
|
|
}
|
|
|
|
|
}
|