riftenlabs-indexer/src/rpc/price.rs

797 lines
28 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};
use crate::{db::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> {
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;
}
2024-06-06 08:03:12 +02:00
let sum_sats = Decimal::from_u64(sum_sats).context("overflow")?;
let sum_tokens = Decimal::from_u64(sum_tokens).context("overflow")?;
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 (
SELECT
phe.pool,
MAX(COALESCE(phe.first_seen_timestamp, phe.mtp_timestamp)) AS max_effective_timestamp
FROM
pool_history_entry phe
JOIN
utxo_funding uf ON phe.utxo = uf.new_utxo_hash
WHERE
COALESCE(phe.first_seen_timestamp, phe.mtp_timestamp) <= ?
AND uf.token_id = ?
GROUP BY
phe.pool
)
SELECT
uf.token_amount,
uf.sats,
COALESCE(phe.first_seen_timestamp, phe.mtp_timestamp) AS effective_timestamp
FROM
pool_history_entry phe
JOIN
utxo_funding uf ON phe.utxo = uf.new_utxo_hash
JOIN
pool p ON p.creation_utxo = phe.pool
JOIN
max_timestamps mt ON phe.pool = mt.pool
AND COALESCE(phe.first_seen_timestamp, phe.mtp_timestamp) = mt.max_effective_timestamp
WHERE
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")]
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()
.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<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()
.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 {
use crate::db::cauldron::{pool, tx, utxo_funding};
use super::*;
use bitcoin_hashes::Hash;
use r2d2::Pool;
use r2d2_sqlite::SqliteConnectionManager;
use rocket::http::Status;
use rocket::local::blocking::Client;
use rocket::routes;
use rusqlite::{params, Connection};
use std::sync::Arc;
fn setup_mock_db(connection: &Connection) {
utxo_funding::create_table(connection);
tx::create_table(connection);
pool::create_table(connection);
// Insert mock data into utxo_funding
connection
.execute(
"INSERT INTO utxo_funding (new_utxo_hash, txid, sats, token_amount, token_id) VALUES (?, ?, ?, ?, ?)",
params!["mock_utxo_hash1", "test_txid1", 50000, 1000, "0000000000000000000000000000000000000000000000000000000000000000"],
)
.expect("Failed to insert test data 1 into utxo_funding");
connection
.execute(
"INSERT INTO utxo_funding (new_utxo_hash, txid, sats, token_amount, token_id) VALUES (?, ?, ?, ?, ?)",
params!["mock_utxo_hash2", "test_txid2", 60000, 2000, "0000000000000000000000000000000000000000000000000000000000000000"],
)
.expect("Failed to insert test data 2 into utxo_funding");
connection
.execute(
"INSERT INTO utxo_funding (new_utxo_hash, txid, sats, token_amount, token_id) VALUES (?, ?, ?, ?, ?)",
params!["mock_utxo_hash3", "test_txid3", 90000, 3000, "0000000000000000000000000000000000000000000000000000000000000000"],
)
.expect("Failed to insert test data 3 into utxo_funding");
// Insert mock data into tx table
connection
.execute(
"INSERT INTO tx (txid, first_seen_timestamp, mtp_timestamp) VALUES (?, ?, ?)",
params!["test_txid1", 1727963300, 1727963300],
)
.expect("Failed to insert test data 1 into tx");
connection
.execute(
"INSERT INTO tx (txid, first_seen_timestamp, mtp_timestamp) VALUES (?, ?, ?)",
params!["test_txid2", 1727963350, 1727963350],
)
.expect("Failed to insert test data 2 into tx");
connection
.execute(
"INSERT INTO tx (txid, first_seen_timestamp, mtp_timestamp) VALUES (?, ?, ?)",
params!["test_txid3", 1727963400, 1727963400],
)
.expect("Failed to insert test data 3 into tx");
// Insert mock data into `pool_history_entry`
connection
.execute(
"INSERT INTO pool_history_entry (pool, utxo, txid, tx_pos, mtp_timestamp, first_seen_timestamp) VALUES (?, ?, ?, ?, ?, ?)",
params!["pool1", "mock_utxo_hash1", "test_txid1", "tx_pos1", 1727963300, 1727963300],
)
.expect("Failed to insert test data 1 into pool_history_entry");
connection
.execute(
"INSERT INTO pool_history_entry (pool, utxo, txid, tx_pos, mtp_timestamp, first_seen_timestamp) VALUES (?, ?, ?, ?, ?, ?)",
params!["pool2", "mock_utxo_hash2", "test_txid2", "tx_pos2", 1727963350, 1727963350],
)
.expect("Failed to insert test data 2 into pool_history_entry");
connection
.execute(
"INSERT INTO pool_history_entry (pool, utxo, txid, tx_pos, mtp_timestamp, first_seen_timestamp) VALUES (?, ?, ?, ?, ?, ?)",
params!["pool3", "mock_utxo_hash3", "test_txid3", "tx_pos3", 1727963400, 1727963400],
)
.expect("Failed to insert test data 3 into pool_history_entry");
// Insert newer entry for pool1
connection
.execute(
"INSERT INTO pool_history_entry (pool, utxo, txid, tx_pos, mtp_timestamp, first_seen_timestamp) VALUES (?, ?, ?, ?, ?, ?)",
params!["pool1", "mock_utxo_hash1_newer", "test_txid1_newer", "tx_pos1_newer", 1727963500, 1727963500],
)
.expect("Failed to insert newer test data into pool_history_entry");
connection
.execute(
"INSERT INTO utxo_funding (new_utxo_hash, txid, sats, token_amount, token_id) VALUES (?, ?, ?, ?, ?)",
params!["mock_utxo_hash1_newer", "test_txid1_newer", 70000, 1500, "0000000000000000000000000000000000000000000000000000000000000000"],
)
.expect("Failed to insert newer test data into utxo_funding");
// Insert active pools with required columns (owner_pkh and token_id)
connection
.execute(
"INSERT INTO pool (creation_utxo, owner_pkh, token_id, withdrawn_in_utxo) VALUES (?, ?, ?, ?)",
params!["pool1", "dummy_owner_pkh1", "dummy_token_id1", Option::<String>::None], // Active pool
)
.expect("Failed to insert test data 1 into pool");
connection
.execute(
"INSERT INTO pool (creation_utxo, owner_pkh, token_id, withdrawn_in_utxo) VALUES (?, ?, ?, ?)",
params!["pool2", "dummy_owner_pkh2", "dummy_token_id2", Option::<String>::None], // Active pool
)
.expect("Failed to insert test data 2 into pool");
connection
.execute(
"INSERT INTO pool (creation_utxo, owner_pkh, token_id, withdrawn_in_utxo) VALUES (?, ?, ?, ?)",
params!["pool3", "dummy_owner_pkh3", "dummy_token_id3", Option::<String>::None], // Active pool
)
.expect("Failed to insert test data 3 into pool");
// Insert an inactive pool
connection
.execute(
"INSERT INTO pool (creation_utxo, owner_pkh, token_id, withdrawn_in_utxo) VALUES (?, ?, ?, ?)",
params!["inactive_pool", "dummy_owner_pkh_inactive", "dummy_token_id_inactive", "inactive_utxo"], // Inactive pool
)
.expect("Failed to insert inactive pool");
// Insert corresponding utxo_funding for the inactive pool
connection
.execute(
"INSERT INTO utxo_funding (new_utxo_hash, txid, sats, token_amount, token_id) VALUES (?, ?, ?, ?, ?)",
params!["inactive_utxo", "inactive_txid", 80000, 2500, "0000000000000000000000000000000000000000000000000000000000000000"],
)
.expect("Failed to insert inactive utxo_funding");
// Insert pool_history_entry for the inactive pool
connection
.execute(
"INSERT INTO pool_history_entry (pool, utxo, txid, tx_pos, mtp_timestamp, first_seen_timestamp) VALUES (?, ?, ?, ?, ?, ?)",
params!["inactive_pool", "inactive_utxo", "inactive_txid", "tx_pos_inactive", 1727963350, 1727963350],
)
.expect("Failed to insert inactive pool_history_entry");
}
// Mock function to create a DB pool
fn mock_db_pool() -> DB {
let manager = SqliteConnectionManager::memory();
let pool = Pool::new(manager).expect("Failed to create pool.");
let write_conn = pool.get().expect("Failed to get connection.");
setup_mock_db(&write_conn);
DB {
cauldron_w: Arc::new(pool.clone()),
cauldron_r: Arc::new(pool.clone()),
bcmr_w: Arc::new(pool.clone()),
bcmr_r: Arc::new(pool.clone()),
}
}
#[test]
fn test_price_at_specific_time() {
let mock_db = mock_db_pool();
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 = 1727963300;
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
let timestamp = 1727963350;
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;
assert!((actual_price - expected_price).abs() < 0.01);
// Test 3: Querying a timestamp that includes Pool 1, Pool 2, and Pool 3
let timestamp = 1727963400;
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() {
let mock_db = mock_db_pool();
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
assert!((actual_price - expected_price).abs() < 0.01);
}
#[test]
fn test_price_with_multiple_entries_at_same_timestamp() {
let mock_db = mock_db_pool();
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 connection = mock_db.cauldron_r.get().expect("Failed to get connection.");
// These entries should have the same timestamp as previous mockdata and be counted in the price calculation
connection
.execute(
"INSERT INTO pool_history_entry (pool, utxo, txid, tx_pos, mtp_timestamp, first_seen_timestamp) VALUES (?, ?, ?, ?, ?, ?)",
params!["pool1", "mock_utxo_hash1_extra", "test_txid1_extra", "0", 1727963300, 1727963300],
)
.expect("Failed to insert extra test data into pool_history_entry");
connection
.execute(
"INSERT INTO utxo_funding (new_utxo_hash, txid, sats, token_amount, token_id) VALUES (?, ?, ?, ?, ?)",
params!["mock_utxo_hash1_extra", "test_txid1_extra", 40000, 500, "0000000000000000000000000000000000000000000000000000000000000000"],
)
.expect("Failed to insert extra test data into utxo_funding");
// Test: Query at the timestamp matching test_txid1 (1727963300) and check the price
let timestamp = 1727963300;
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() {
let mock_db = mock_db_pool();
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() {
let mock_db = mock_db_pool();
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() {
let mock_db = mock_db_pool();
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() {
let mock_db = mock_db_pool();
let rocket = rocket::build()
.manage(mock_db.clone())
.mount("/cauldron", routes![price_at]);
let client = Client::tracked(rocket).expect("valid rocket instance");
let connection = mock_db.cauldron_r.get().expect("Failed to get connection.");
// Setup mock data for this specific test
connection
.execute(
"CREATE TABLE IF NOT EXISTS utxo_funding (
new_utxo_hash TEXT PRIMARY KEY,
txid TEXT,
sats BIGINT,
token_amount BIGINT,
token_id TEXT
)",
[],
)
.expect("Failed to create utxo_funding table");
connection
.execute(
"CREATE TABLE IF NOT EXISTS pool_history_entry (
pool TEXT,
utxo TEXT PRIMARY KEY,
txid TEXT,
tx_pos TEXT,
mtp_timestamp BIGINT,
first_seen_timestamp BIGINT
)",
[],
)
.expect("Failed to create pool_history_entry table");
// Insert high tokens and low sats
connection
.execute(
"INSERT INTO utxo_funding (new_utxo_hash, txid, sats, token_amount, token_id) VALUES (?, ?, ?, ?, ?)",
params![
"mock_utxo_hash_high_tokens",
"test_txid_high_tokens",
1_i64, // Low sats
9999999999999999_i64, // Very high token amount
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
],
)
.expect("Failed to insert high-token data into utxo_funding");
connection
.execute(
"INSERT INTO pool_history_entry (pool, utxo, txid, tx_pos, mtp_timestamp, first_seen_timestamp) VALUES (?, ?, ?, ?, ?, ?)",
params![
"pool_high_tokens",
"mock_utxo_hash_high_tokens",
"test_txid_high_tokens",
"tx_pos_high_tokens",
1727963300,
1727963300
],
)
.expect("Failed to insert high-token data into pool_history_entry");
// Test: Querying the price with high tokens and low sats
let token_id = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
let timestamp = 1727963300;
let response = client
.get(format!("/cauldron/price/{}/at/{}", token_id, timestamp))
.dispatch();
// Expect that we could not handle the calculation/conversion.
assert_eq!(response.status(), Status::InternalServerError);
}
}