riftenlabs-indexer/src/rpc/price.rs

764 lines
25 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};
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()
2025-07-16 09:31:14 +02:00
.map_err(|e| Custom(Status::InternalServerError, format!("Error: {e}")))?;
2024-10-14 11:18:50 +00:00
let current_time = time_now();
if timestamp > current_time {
return Err(Custom(
Status::BadRequest,
"Timestamp is in the future".to_string(),
));
}
2025-07-16 09:31:14 +02:00
let token =
TokenID::from_hex(token).map_err(|e| Custom(Status::BadRequest, format!("Error: {e}")))?;
2024-10-14 11:18:50 +00:00
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,
2025-07-16 09:31:14 +02:00
format!("Error fetching price: {e}"),
2024-10-14 11:18:50 +00:00
)),
}
}
2024-04-03 11:39:49 +02:00
#[get("/price/<token>/current")]
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
.cauldron_r
2024-04-03 11:39:49 +02:00
.get()
2025-07-16 09:31:14 +02:00
.map_err(|e| Custom(Status::InternalServerError, format!("Error: {e}")))?;
2024-04-03 11:39:49 +02:00
let price = current_price(&db, token)
2025-07-16 09:31:14 +02:00
.map_err(|e| Custom(Status::InternalServerError, format!("Error: {e}")))?;
2024-04-03 11:39:49 +02:00
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<DB>,
2024-04-03 11:39:49 +02:00
) -> Result<Json<Value>, Custom<String>> {
let current_timestamp = time_now();
let db = conn
.cauldron_r
2024-04-03 11:39:49 +02:00
.get()
2025-07-16 09:31:14 +02:00
.map_err(|e| Custom(Status::InternalServerError, format!("Error: {e}")))?;
2024-04-03 11:39:49 +02:00
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,
)
2025-07-16 09:31:14 +02:00
.map_err(|e| Custom(Status::BadRequest, format!("Error: {e}")))?;
2024-04-03 11:39:49 +02:00
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 {
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;
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;
use rusqlite::Connection;
2024-10-14 11:18:50 +00: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,
is_withdrawn: false,
spent_utxo_hash: OutPointHash::all_zeros(),
new_utxo_hash: Some(*utxo),
new_utxo_txid: Some(*txid),
new_utxo_n: Some(0),
token_id: Some(*token),
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
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
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`
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
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
)
.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)
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
)
.unwrap();
insert_new_pool(
conn,
&dummy_cauldron(&Txid::all_zeros(), &pool2, &token2, 0, 0, &pkh2),
2024-10-14 11:18:50 +00:00
)
.unwrap();
insert_new_pool(
conn,
&dummy_cauldron(&Txid::all_zeros(), &pool3, &token3, 0, 0, &pkh3),
2024-10-14 11:18:50 +00:00
)
.unwrap();
2024-10-14 11:18:50 +00:00
// Insert an inactive pool
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).unwrap();
insert_pool_history_entry(
conn,
&inactive_pool,
&inactive_cauldron,
Some(1727963350),
Some(1727963350),
2024-10-14 11:18:50 +00: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
let timestamp = TIME_1;
2024-10-14 11:18:50 +00:00
let response = client
2025-07-16 09:31:14 +02:00
.get(format!("/cauldron/price/{token_id}/at/{timestamp}"))
2024-10-14 11:18:50 +00:00
.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
let timestamp = TIME_2;
2024-10-14 11:18:50 +00:00
let response = client
2025-07-16 09:31:14 +02:00
.get(format!("/cauldron/price/{token_id}/at/{timestamp}"))
2024-10-14 11:18:50 +00:00
.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;
assert!(
(actual_price - expected_price).abs() < 0.01,
2025-07-16 09:31:14 +02:00
"expected {expected_price} != actual {actual_price}"
);
2024-10-14 11:18:50 +00:00
// Test 3: Querying a timestamp that includes Pool 1, Pool 2, and Pool 3
let timestamp = TIME_3;
2024-10-14 11:18:50 +00:00
let response = client
2025-07-16 09:31:14 +02:00
.get(format!("/cauldron/price/{token_id}/at/{timestamp}"))
2024-10-14 11:18:50 +00:00
.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
2025-07-16 09:31:14 +02:00
.get(format!("/cauldron/price/{token_id}/at/{timestamp}"))
2024-10-14 11:18:50 +00:00
.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
assert!(
(actual_price - expected_price).abs() < 0.01,
2025-07-16 09:31:14 +02:00
"expected {expected_price} != actual {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)
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
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
let timestamp = TIME_1;
2024-10-14 11:18:50 +00:00
let response = client
2025-07-16 09:31:14 +02:00
.get(format!("/cauldron/price/{token_id}/at/{timestamp}"))
2024-10-14 11:18:50 +00:00
.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
2025-07-16 09:31:14 +02:00
.get(format!("/cauldron/price/{bad_token_id}/at/{timestamp}"))
2024-10-14 11:18:50 +00:00
.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
2025-07-16 09:31:14 +02:00
.get(format!("/cauldron/price/{token_id}/at/{future_timestamp}"))
2024-10-14 11:18:50 +00:00
.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
2025-07-16 09:31:14 +02:00
.get(format!("/cauldron/price/{token_id}/at/{invalid_timestamp}"))
2024-10-14 11:18:50 +00:00
.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");
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
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
.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);
}
}