Improve pool visitor queries; make price a visitor
This adds a big performance boost to pool visitor queries, also moving token price queries to use the visitor model.
This commit is contained in:
parent
fa97ec1be5
commit
ec49bf765b
15 changed files with 435 additions and 232 deletions
72
linters/timestamp_coalesce_check.py
Executable file
72
linters/timestamp_coalesce_check.py
Executable file
|
|
@ -0,0 +1,72 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# 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
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import re
|
||||||
|
|
||||||
|
"""
|
||||||
|
Disallow COALESCE with first_seen_timestamp and mtp_timestamp on the same line in SQL queries.
|
||||||
|
These combinations should use effective_timestamp instead.
|
||||||
|
Note: Database schema definitions using GENERATED ALWAYS AS are allowed.
|
||||||
|
Note: COALESCE aliased as effective_timestamp is allowed (this is the correct pattern).
|
||||||
|
"""
|
||||||
|
|
||||||
|
OUR_PATH = os.path.dirname(os.path.realpath(__file__))
|
||||||
|
|
||||||
|
def check_file_for_timestamp_coalesce(file_path):
|
||||||
|
"""Check if the file contains disallowed timestamp combinations."""
|
||||||
|
with open(file_path, 'r', encoding='utf-8') as file:
|
||||||
|
for line_num, line in enumerate(file, 1):
|
||||||
|
# Check if line contains COALESCE and both timestamp fields
|
||||||
|
if (re.search(r'\bCOALESCE\b', line, re.IGNORECASE) and
|
||||||
|
re.search(r'\bfirst_seen_timestamp\b', line) and
|
||||||
|
re.search(r'\bmtp_timestamp\b', line)):
|
||||||
|
|
||||||
|
# Skip database schema definitions (GENERATED ALWAYS AS)
|
||||||
|
if re.search(r'GENERATED\s+ALWAYS\s+AS', line, re.IGNORECASE):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Skip comments that are just documenting the pattern
|
||||||
|
if line.strip().startswith('//') and 'COALESCE' in line:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Allow COALESCE aliased as effective_timestamp (this is the correct pattern)
|
||||||
|
if re.search(r'AS\s+effective_timestamp', line, re.IGNORECASE):
|
||||||
|
continue
|
||||||
|
|
||||||
|
print(f"{file_path}:{line_num}: Disallowed COALESCE with first_seen_timestamp and mtp_timestamp")
|
||||||
|
print(f" Line: {line.strip()}")
|
||||||
|
print(f" Suggestion: Use effective_timestamp instead")
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def check_files(directories):
|
||||||
|
"""
|
||||||
|
Traverse the specified directories and check each .rs file.
|
||||||
|
"""
|
||||||
|
found_forbidden_combinations = False
|
||||||
|
|
||||||
|
for directory in directories:
|
||||||
|
for root, dirs, files in os.walk(os.path.join(OUR_PATH, "..", directory)):
|
||||||
|
for file in files:
|
||||||
|
if file.endswith('.rs'):
|
||||||
|
file_path = os.path.join(root, file)
|
||||||
|
if not check_file_for_timestamp_coalesce(file_path):
|
||||||
|
found_forbidden_combinations = True
|
||||||
|
|
||||||
|
if found_forbidden_combinations:
|
||||||
|
print("\nForbidden timestamp combinations found.")
|
||||||
|
print("Please use effective_timestamp instead of COALESCE with first_seen_timestamp and mtp_timestamp.")
|
||||||
|
print("Note: Database schema definitions using GENERATED ALWAYS AS are allowed.")
|
||||||
|
print("Note: COALESCE aliased as effective_timestamp is allowed (this is the correct pattern).")
|
||||||
|
sys.exit(1)
|
||||||
|
else:
|
||||||
|
print("OK")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
check_files(['src', 'contrib'])
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
use anyhow::{bail, Result};
|
use anyhow::{bail, Result};
|
||||||
use rusqlite::{params, Connection};
|
use rusqlite::{params, Connection};
|
||||||
|
|
||||||
pub const DB_VERSION: u32 = 3;
|
pub const DB_VERSION: u32 = 4;
|
||||||
const DB_VERSION_KEY: &str = "db_version";
|
const DB_VERSION_KEY: &str = "db_version";
|
||||||
|
|
||||||
/// Create the config table
|
/// Create the config table
|
||||||
|
|
|
||||||
|
|
@ -46,10 +46,7 @@ pub fn prepare_tables(conn: &Connection) {
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
conn.execute("CREATE INDEX idx_utxo_funding_tvl_highest ON utxo_funding(token_id, new_utxo_hash, sats, token_amount);", []).unwrap();
|
conn.execute("CREATE INDEX idx_utxo_funding_tvl_highest ON utxo_funding(token_id, new_utxo_hash, sats, token_amount);", []).unwrap();
|
||||||
// Create the new index for (pool, COALESCE(first_seen_timestamp, mtp_timestamp))
|
conn.execute("CREATE INDEX idx_pool_history_entry_pool_timestamp ON pool_history_entry (pool, effective_timestamp);", []).unwrap();
|
||||||
conn.execute("CREATE INDEX idx_pool_history_entry_pool_timestamp ON pool_history_entry (pool, COALESCE(first_seen_timestamp, mtp_timestamp));",
|
|
||||||
[],
|
|
||||||
).unwrap();
|
|
||||||
|
|
||||||
// Set the database version
|
// Set the database version
|
||||||
config::set_db_version(conn);
|
config::set_db_version(conn);
|
||||||
|
|
|
||||||
|
|
@ -45,10 +45,12 @@ pub fn create_table(conn: &Connection) {
|
||||||
"CREATE TABLE pool_history_entry (
|
"CREATE TABLE pool_history_entry (
|
||||||
utxo TEXT PRIMARY KEY REFERENCES utxo_funding(new_utxo_hash) ON DELETE CASCADE,
|
utxo TEXT PRIMARY KEY REFERENCES utxo_funding(new_utxo_hash) ON DELETE CASCADE,
|
||||||
pool TEXT REFERENCES pool(creation_utxo) ON DELETE CASCADE,
|
pool TEXT REFERENCES pool(creation_utxo) ON DELETE CASCADE,
|
||||||
|
token_id TEXT NOT NULL,
|
||||||
txid TEXT REFERENCES tx(txid) ON DELETE CASCADE,
|
txid TEXT REFERENCES tx(txid) ON DELETE CASCADE,
|
||||||
tx_pos INT NOT NULL,
|
tx_pos INT NOT NULL,
|
||||||
mtp_timestamp BIGINT,
|
mtp_timestamp BIGINT,
|
||||||
first_seen_timestamp BIGINT,
|
first_seen_timestamp BIGINT,
|
||||||
|
effective_timestamp BIGINT GENERATED ALWAYS AS (COALESCE(first_seen_timestamp, mtp_timestamp)),
|
||||||
sequence BIGINT NOT NULL,
|
sequence BIGINT NOT NULL,
|
||||||
sats BIGINT NOT NULL,
|
sats BIGINT NOT NULL,
|
||||||
token_amount BIGINT NOT NULL,
|
token_amount BIGINT NOT NULL,
|
||||||
|
|
@ -77,7 +79,23 @@ pub fn create_table(conn: &Connection) {
|
||||||
params![],
|
params![],
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
conn.execute("CREATE INDEX idx_pool_history_entry_timestamp_sequence ON pool_history_entry(mtp_timestamp, sequence)", params![]).unwrap();
|
|
||||||
|
// speeds up pool visitor query
|
||||||
|
conn.execute(
|
||||||
|
"CREATE INDEX idx_pool_history_entry_pool_timestamp_sequence ON pool_history_entry (
|
||||||
|
pool,
|
||||||
|
effective_timestamp,
|
||||||
|
sequence DESC)",
|
||||||
|
params![],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// for pool visitor; filtering on token
|
||||||
|
conn.execute(
|
||||||
|
"CREATE INDEX idx_pool_history_entry_token_id ON pool_history_entry(token_id)",
|
||||||
|
params![],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_pool_by_utxo(conn: &Connection, utxo_hash: &OutPointHash) -> Result<Option<OutPointHash>> {
|
fn get_pool_by_utxo(conn: &Connection, utxo_hash: &OutPointHash) -> Result<Option<OutPointHash>> {
|
||||||
|
|
@ -160,10 +178,11 @@ pub fn insert_pool_history_entry(
|
||||||
assert!(next_seq >= 0);
|
assert!(next_seq >= 0);
|
||||||
|
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"INSERT INTO pool_history_entry (utxo, pool, txid, tx_pos, mtp_timestamp, first_seen_timestamp, sequence, sats, token_amount, sats_delta, token_delta)
|
"INSERT INTO pool_history_entry (utxo, pool, token_id, txid, tx_pos, mtp_timestamp, first_seen_timestamp, sequence, sats, token_amount, sats_delta, token_delta)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
ON CONFLICT(utxo) DO UPDATE SET
|
ON CONFLICT(utxo) DO UPDATE SET
|
||||||
pool = excluded.pool,
|
pool = excluded.pool,
|
||||||
|
token_id = excluded.token_id,
|
||||||
txid = excluded.txid,
|
txid = excluded.txid,
|
||||||
tx_pos = excluded.tx_pos,
|
tx_pos = excluded.tx_pos,
|
||||||
mtp_timestamp = COALESCE(excluded.mtp_timestamp, pool_history_entry.mtp_timestamp),
|
mtp_timestamp = COALESCE(excluded.mtp_timestamp, pool_history_entry.mtp_timestamp),
|
||||||
|
|
@ -175,6 +194,7 @@ pub fn insert_pool_history_entry(
|
||||||
.expect("utxo hash on new pool history entry")
|
.expect("utxo hash on new pool history entry")
|
||||||
.to_hex(),
|
.to_hex(),
|
||||||
pool.to_hex(),
|
pool.to_hex(),
|
||||||
|
cauldron.token_id.expect("token id on new pool history entry").to_hex(),
|
||||||
cauldron
|
cauldron
|
||||||
.new_utxo_txid
|
.new_utxo_txid
|
||||||
.expect("txid of new pool history entry")
|
.expect("txid of new pool history entry")
|
||||||
|
|
@ -323,7 +343,7 @@ fn get_nearest_entries(
|
||||||
(false, false) => params![timestamp],
|
(false, false) => params![timestamp],
|
||||||
};
|
};
|
||||||
|
|
||||||
let ts = "COALESCE(first_seen_timestamp, mtp_timestamp)";
|
let ts = "effective_timestamp";
|
||||||
|
|
||||||
let preferred = match resolution {
|
let preferred = match resolution {
|
||||||
SnapshotSelection::UseBefore => "ts ASC", // Prefer the lower timestamp first
|
SnapshotSelection::UseBefore => "ts ASC", // Prefer the lower timestamp first
|
||||||
|
|
@ -439,7 +459,7 @@ pub struct PoolHistoryEntry {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_pool_history_entry(conn: &Connection, utxo_hash: OutPointHash) -> Result<PoolHistoryEntry> {
|
fn get_pool_history_entry(conn: &Connection, utxo_hash: OutPointHash) -> Result<PoolHistoryEntry> {
|
||||||
let mut stmt = conn.prepare("SELECT txid, sats, token_amount, COALESCE(first_seen_timestamp, mtp_timestamp) as timestamp FROM pool_history_entry WHERE utxo = ?")?;
|
let mut stmt = conn.prepare("SELECT txid, sats, token_amount, effective_timestamp as timestamp FROM pool_history_entry WHERE utxo = ?")?;
|
||||||
let mut rows = stmt.query(params![utxo_hash.to_hex()])?;
|
let mut rows = stmt.query(params![utxo_hash.to_hex()])?;
|
||||||
let row = rows
|
let row = rows
|
||||||
.next()?
|
.next()?
|
||||||
|
|
@ -464,7 +484,7 @@ pub fn db_pool_history(
|
||||||
phe.txid,
|
phe.txid,
|
||||||
phe.sats,
|
phe.sats,
|
||||||
phe.token_amount,
|
phe.token_amount,
|
||||||
COALESCE(phe.first_seen_timestamp, phe.mtp_timestamp) as timestamp
|
phe.effective_timestamp as timestamp
|
||||||
FROM
|
FROM
|
||||||
pool_history_entry phe
|
pool_history_entry phe
|
||||||
WHERE
|
WHERE
|
||||||
|
|
@ -522,7 +542,7 @@ pub fn get_total_volume_sats(
|
||||||
FROM pool_history_entry phe
|
FROM pool_history_entry phe
|
||||||
JOIN pool p ON phe.pool = p.creation_utxo
|
JOIN pool p ON phe.pool = p.creation_utxo
|
||||||
JOIN tx ON phe.txid = tx.txid
|
JOIN tx ON phe.txid = tx.txid
|
||||||
WHERE COALESCE(tx.first_seen_timestamp, tx.mtp_timestamp) BETWEEN ? AND ?";
|
WHERE tx.effective_timestamp BETWEEN ? AND ?";
|
||||||
|
|
||||||
let total_volume: i64 = db.query_row(sql, params![start_timestamp, end_timestamp], |row| {
|
let total_volume: i64 = db.query_row(sql, params![start_timestamp, end_timestamp], |row| {
|
||||||
row.get(0)
|
row.get(0)
|
||||||
|
|
@ -545,7 +565,7 @@ pub fn get_token_volume_sats(
|
||||||
FROM pool_history_entry phe
|
FROM pool_history_entry phe
|
||||||
JOIN pool p ON phe.pool = p.creation_utxo
|
JOIN pool p ON phe.pool = p.creation_utxo
|
||||||
JOIN tx ON phe.txid = tx.txid
|
JOIN tx ON phe.txid = tx.txid
|
||||||
WHERE COALESCE(tx.first_seen_timestamp, tx.mtp_timestamp) BETWEEN ? AND ?
|
WHERE tx.effective_timestamp BETWEEN ? AND ?
|
||||||
AND p.token_id = ?";
|
AND p.token_id = ?";
|
||||||
|
|
||||||
let (sats_volume, token_volume): (i64, i64) = db.query_row(
|
let (sats_volume, token_volume): (i64, i64) = db.query_row(
|
||||||
|
|
|
||||||
|
|
@ -17,14 +17,59 @@ pub(crate) enum OptionalPoolFields {
|
||||||
TxPos = 1 << 3,
|
TxPos = 1 << 3,
|
||||||
TokenId = 1 << 4,
|
TokenId = 1 << 4,
|
||||||
PoolId = 1 << 5,
|
PoolId = 1 << 5,
|
||||||
|
Timestamp = 1 << 6,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
pub(crate) struct PoolFilters {
|
pub(crate) struct PoolFilters {
|
||||||
pub timestamp_less_than: Option<u64>,
|
pub timestamp_lt: Option<u64>,
|
||||||
|
pub timestamp_lte: Option<u64>,
|
||||||
|
pub timestamp_gt: Option<u64>,
|
||||||
|
pub timestamp_gte: Option<u64>,
|
||||||
pub token_id: Option<String>,
|
pub token_id: Option<String>,
|
||||||
pub owner: Option<String>,
|
pub owner: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl PoolFilters {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn lt(mut self, timestamp: u64) -> Self {
|
||||||
|
self.timestamp_lt = Some(timestamp);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn lte(mut self, timestamp: u64) -> Self {
|
||||||
|
self.timestamp_lte = Some(timestamp);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn gt(mut self, timestamp: u64) -> Self {
|
||||||
|
self.timestamp_gt = Some(timestamp);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn gte(mut self, timestamp: u64) -> Self {
|
||||||
|
self.timestamp_gte = Some(timestamp);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn token_id(mut self, token_id: &str) -> Self {
|
||||||
|
self.token_id = Some(token_id.to_string());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn owner(mut self, owner: &str) -> Self {
|
||||||
|
self.owner = Some(owner.to_string());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub(crate) struct OptionalFields {
|
pub(crate) struct OptionalFields {
|
||||||
pub owner: Option<String>,
|
pub owner: Option<String>,
|
||||||
|
|
@ -32,6 +77,7 @@ pub(crate) struct OptionalFields {
|
||||||
pub tx_pos: Option<u32>,
|
pub tx_pos: Option<u32>,
|
||||||
pub token_id: Option<String>,
|
pub token_id: Option<String>,
|
||||||
pub pool_id: Option<String>,
|
pub pool_id: Option<String>,
|
||||||
|
pub timestamp: Option<i64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) trait PoolVisitor {
|
pub(crate) trait PoolVisitor {
|
||||||
|
|
@ -40,46 +86,77 @@ pub(crate) trait PoolVisitor {
|
||||||
fn visit(&mut self, sats: u64, tokens: u64, optional_fields: OptionalFields) -> Result<bool>;
|
fn visit(&mut self, sats: u64, tokens: u64, optional_fields: OptionalFields) -> Result<bool>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn time_filters(
|
||||||
|
filters: &PoolFilters,
|
||||||
|
param_index: usize,
|
||||||
|
) -> (String, Vec<rusqlite::types::ToSqlOutput>) {
|
||||||
|
let now = time_now() as u64;
|
||||||
|
|
||||||
|
let mut clauses: Vec<String> = vec!["1=1".to_string()];
|
||||||
|
let mut params: Vec<rusqlite::types::ToSqlOutput> = Vec::new();
|
||||||
|
let mut next_idx: usize = param_index;
|
||||||
|
|
||||||
|
if filters.timestamp_lt.map(|t| t <= now).unwrap_or(false) {
|
||||||
|
clauses.push(format!("effective_timestamp < ?{}", next_idx));
|
||||||
|
params.push(rusqlite::types::ToSqlOutput::Owned(
|
||||||
|
(filters.timestamp_lt.unwrap() as i64).into(),
|
||||||
|
));
|
||||||
|
next_idx += 1;
|
||||||
|
}
|
||||||
|
if filters.timestamp_lte.map(|t| t <= now).unwrap_or(false) {
|
||||||
|
clauses.push(format!("effective_timestamp <= ?{}", next_idx));
|
||||||
|
params.push(rusqlite::types::ToSqlOutput::Owned(
|
||||||
|
(filters.timestamp_lte.unwrap() as i64).into(),
|
||||||
|
));
|
||||||
|
next_idx += 1;
|
||||||
|
}
|
||||||
|
if let Some(gt) = filters.timestamp_gt {
|
||||||
|
clauses.push(format!("effective_timestamp > ?{}", next_idx));
|
||||||
|
params.push(rusqlite::types::ToSqlOutput::Owned((gt as i64).into()));
|
||||||
|
next_idx += 1;
|
||||||
|
}
|
||||||
|
if let Some(gte) = filters.timestamp_gte {
|
||||||
|
clauses.push(format!("effective_timestamp >= ?{}", next_idx));
|
||||||
|
params.push(rusqlite::types::ToSqlOutput::Owned((gte as i64).into()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let phe_sql = format!(" WHERE {} ", clauses.join(" AND "));
|
||||||
|
|
||||||
|
(phe_sql, params)
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn db_visit_pool_entries<T: PoolVisitor>(
|
pub(crate) fn db_visit_pool_entries<T: PoolVisitor>(
|
||||||
conn: &Connection,
|
conn: &Connection,
|
||||||
visitor: &mut T,
|
visitor: &mut T,
|
||||||
filters: PoolFilters,
|
filters: PoolFilters,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let mut sql_filters = Vec::new();
|
let (phe_timestamp_filter, time_params) = time_filters(&filters, 2); // Start at ?2, ?1 for withdrawn
|
||||||
let mut params: Vec<rusqlite::types::ToSqlOutput> = Vec::new();
|
|
||||||
let mut param_index = 2; // Start from 2 since ?1 is used by time filter
|
|
||||||
|
|
||||||
if let Some(token_id) = &filters.token_id {
|
let mut sql_filters = Vec::new();
|
||||||
sql_filters.push(format!("p.token_id = ?{}", param_index));
|
let mut params: Vec<rusqlite::types::ToSqlOutput> = time_params;
|
||||||
|
let mut param_index = 2 + params.len();
|
||||||
|
|
||||||
|
let phe_tokenid_filter = if let Some(token_id) = &filters.token_id {
|
||||||
params.push(rusqlite::types::ToSqlOutput::Owned(
|
params.push(rusqlite::types::ToSqlOutput::Owned(
|
||||||
token_id.to_owned().into(),
|
token_id.to_owned().into(),
|
||||||
));
|
));
|
||||||
|
let this_param_index = param_index;
|
||||||
param_index += 1;
|
param_index += 1;
|
||||||
}
|
format!("AND token_id = ?{}", this_param_index)
|
||||||
|
} else {
|
||||||
|
"".to_string()
|
||||||
|
};
|
||||||
|
|
||||||
if let Some(owner) = &filters.owner {
|
if let Some(owner) = &filters.owner {
|
||||||
sql_filters.push(format!("p.owner_pkh = ?{}", param_index));
|
sql_filters.push(format!("p.owner_pkh = ?{}", param_index));
|
||||||
params.push(rusqlite::types::ToSqlOutput::Owned(owner.to_owned().into()));
|
params.push(rusqlite::types::ToSqlOutput::Owned(owner.to_owned().into()));
|
||||||
}
|
}
|
||||||
|
|
||||||
let sql_filters_str = if sql_filters.is_empty() {
|
let sql_filters_str = if sql_filters.is_empty() {
|
||||||
"".to_string()
|
"".to_string()
|
||||||
} else {
|
} else {
|
||||||
format!("AND {}", sql_filters.join(" AND "))
|
format!("AND {}", sql_filters.join(" AND "))
|
||||||
};
|
};
|
||||||
|
|
||||||
// don't need this filter if its in the future.
|
|
||||||
// faster without filter.
|
|
||||||
let time_filter = filters
|
|
||||||
.timestamp_less_than
|
|
||||||
.filter(|&t| t <= time_now() as u64);
|
|
||||||
|
|
||||||
let phe_timestamp_filter = if time_filter.is_some() {
|
|
||||||
" WHERE mtp_timestamp < ?1 ".to_string()
|
|
||||||
} else {
|
|
||||||
"".to_string()
|
|
||||||
};
|
|
||||||
|
|
||||||
let query: String = "
|
let query: String = "
|
||||||
SELECT
|
SELECT
|
||||||
p.owner_pkh,
|
p.owner_pkh,
|
||||||
|
|
@ -88,7 +165,8 @@ pub(crate) fn db_visit_pool_entries<T: PoolVisitor>(
|
||||||
phe.txid,
|
phe.txid,
|
||||||
phe.tx_pos,
|
phe.tx_pos,
|
||||||
p.token_id,
|
p.token_id,
|
||||||
p.creation_utxo
|
p.creation_utxo,
|
||||||
|
phe.effective_timestamp
|
||||||
FROM
|
FROM
|
||||||
pool p
|
pool p
|
||||||
JOIN pool_history_entry phe ON p.creation_utxo = phe.pool
|
JOIN pool_history_entry phe ON p.creation_utxo = phe.pool
|
||||||
|
|
@ -96,23 +174,31 @@ pub(crate) fn db_visit_pool_entries<T: PoolVisitor>(
|
||||||
SELECT pool, MAX(sequence) AS max_sequence
|
SELECT pool, MAX(sequence) AS max_sequence
|
||||||
FROM pool_history_entry
|
FROM pool_history_entry
|
||||||
{phe_timestamp_filter}
|
{phe_timestamp_filter}
|
||||||
|
{phe_tokenid_filter}
|
||||||
|
|
||||||
GROUP BY pool
|
GROUP BY pool
|
||||||
) max_phe ON phe.pool = max_phe.pool AND phe.sequence = max_phe.max_sequence
|
) max_phe ON phe.pool = max_phe.pool AND phe.sequence = max_phe.max_sequence
|
||||||
WHERE
|
WHERE
|
||||||
(p.withdrawn_in_utxo IS NULL OR (
|
(p.withdrawn_in_utxo IS NULL OR (
|
||||||
SELECT t.mtp_timestamp
|
SELECT effective_timestamp
|
||||||
FROM utxo_spending us
|
FROM utxo_spending us
|
||||||
JOIN tx t ON us.txid = t.txid
|
JOIN tx t ON us.txid = t.txid
|
||||||
WHERE us.spent_utxo_hash = p.withdrawn_in_utxo
|
WHERE us.spent_utxo_hash = p.withdrawn_in_utxo
|
||||||
) >= ?1)
|
) >= ?1)
|
||||||
{filters}
|
{filters}
|
||||||
GROUP BY
|
"
|
||||||
p.creation_utxo"
|
|
||||||
.replace("{filters}", &sql_filters_str)
|
.replace("{filters}", &sql_filters_str)
|
||||||
.replace("{phe_timestamp_filter}", &phe_timestamp_filter);
|
.replace("{phe_timestamp_filter}", &phe_timestamp_filter)
|
||||||
|
.replace("{phe_tokenid_filter}", &phe_tokenid_filter);
|
||||||
|
|
||||||
|
// allow to include pools that were withdrawn AFTER a time filter.
|
||||||
|
let withdraw_time_filter = filters
|
||||||
|
.timestamp_lt
|
||||||
|
.min(filters.timestamp_lte)
|
||||||
|
.unwrap_or(i64::MAX as u64);
|
||||||
|
|
||||||
let mut final_params = vec![rusqlite::types::ToSqlOutput::Owned(
|
let mut final_params = vec![rusqlite::types::ToSqlOutput::Owned(
|
||||||
(time_filter.unwrap_or(i64::MAX as u64) as i64).into(),
|
(withdraw_time_filter as i64).into(),
|
||||||
)];
|
)];
|
||||||
final_params.extend(params);
|
final_params.extend(params);
|
||||||
|
|
||||||
|
|
@ -142,6 +228,9 @@ pub(crate) fn db_visit_pool_entries<T: PoolVisitor>(
|
||||||
if (wanted & OptionalPoolFields::PoolId as u64) != 0 {
|
if (wanted & OptionalPoolFields::PoolId as u64) != 0 {
|
||||||
optional.pool_id = Some(row.get(6)?);
|
optional.pool_id = Some(row.get(6)?);
|
||||||
}
|
}
|
||||||
|
if (wanted & OptionalPoolFields::Timestamp as u64) != 0 {
|
||||||
|
optional.timestamp = Some(row.get(7)?);
|
||||||
|
}
|
||||||
|
|
||||||
if !visitor.visit(sats, tokens, optional)? {
|
if !visitor.visit(sats, tokens, optional)? {
|
||||||
break;
|
break;
|
||||||
|
|
|
||||||
|
|
@ -31,8 +31,7 @@ pub fn db_list_tokens_by_volume(
|
||||||
seconds: usize,
|
seconds: usize,
|
||||||
limit: usize,
|
limit: usize,
|
||||||
) -> Result<Vec<TokenListItem>> {
|
) -> Result<Vec<TokenListItem>> {
|
||||||
let mut statement = cauldron_conn
|
let mut statement = cauldron_conn.prepare(
|
||||||
.prepare(
|
|
||||||
"
|
"
|
||||||
WITH TradeData AS (
|
WITH TradeData AS (
|
||||||
SELECT
|
SELECT
|
||||||
|
|
@ -41,7 +40,7 @@ pub fn db_list_tokens_by_volume(
|
||||||
FROM pool_history_entry phe
|
FROM pool_history_entry phe
|
||||||
JOIN pool p ON phe.pool = p.creation_utxo
|
JOIN pool p ON phe.pool = p.creation_utxo
|
||||||
JOIN tx ON phe.txid = tx.txid
|
JOIN tx ON phe.txid = tx.txid
|
||||||
WHERE COALESCE(tx.first_seen_timestamp, tx.mtp_timestamp) >= (strftime('%s', 'now') - ?1)
|
WHERE tx.effective_timestamp >= (strftime('%s', 'now') - ?1)
|
||||||
),
|
),
|
||||||
AllTokenIDs AS (
|
AllTokenIDs AS (
|
||||||
SELECT DISTINCT token_id
|
SELECT DISTINCT token_id
|
||||||
|
|
|
||||||
|
|
@ -13,13 +13,18 @@ pub fn create_table(conn: &Connection) {
|
||||||
txid TEXT PRIMARY KEY,
|
txid TEXT PRIMARY KEY,
|
||||||
blockhash TEXT,
|
blockhash TEXT,
|
||||||
mtp_timestamp BIGINT,
|
mtp_timestamp BIGINT,
|
||||||
first_seen_timestamp BIGINT
|
first_seen_timestamp BIGINT,
|
||||||
|
effective_timestamp BIGINT GENERATED ALWAYS AS (COALESCE(first_seen_timestamp, mtp_timestamp))
|
||||||
)";
|
)";
|
||||||
|
|
||||||
conn.execute(tbl, []).expect("failed to create tx table");
|
conn.execute(tbl, []).expect("failed to create tx table");
|
||||||
|
|
||||||
// Create index for volume queries that filter by COALESCE(timestamp)
|
// Create index for volume queries that filter by COALESCE(timestamp)
|
||||||
conn.execute("CREATE INDEX idx_tx_coalesce_timestamp ON tx(COALESCE(first_seen_timestamp, mtp_timestamp))", []).expect("failed to create tx timestamp index");
|
conn.execute(
|
||||||
|
"CREATE INDEX idx_tx_effective_timestamp ON tx(effective_timestamp)",
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
.expect("failed to create tx timestamp index");
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn insert_block_tx(
|
pub fn insert_block_tx(
|
||||||
|
|
@ -63,14 +68,14 @@ pub fn latest(
|
||||||
FROM tx
|
FROM tx
|
||||||
JOIN utxo_funding ON tx.txid = utxo_funding.txid
|
JOIN utxo_funding ON tx.txid = utxo_funding.txid
|
||||||
WHERE utxo_funding.token_id = ?
|
WHERE utxo_funding.token_id = ?
|
||||||
ORDER BY COALESCE(tx.first_seen_timestamp, tx.mtp_timestamp) DESC
|
ORDER BY tx.effective_timestamp DESC
|
||||||
LIMIT ? OFFSET ?",
|
LIMIT ? OFFSET ?",
|
||||||
params![tid.to_hex(), limit, offset],
|
params![tid.to_hex(), limit, offset],
|
||||||
),
|
),
|
||||||
None => (
|
None => (
|
||||||
"SELECT tx.txid, tx.blockhash, tx.mtp_timestamp, tx.first_seen_timestamp
|
"SELECT tx.txid, tx.blockhash, tx.mtp_timestamp, tx.first_seen_timestamp
|
||||||
FROM tx
|
FROM tx
|
||||||
ORDER BY COALESCE(tx.first_seen_timestamp, tx.mtp_timestamp) DESC
|
ORDER BY tx.effective_timestamp DESC
|
||||||
LIMIT ? OFFSET ?",
|
LIMIT ? OFFSET ?",
|
||||||
params![limit, offset],
|
params![limit, offset],
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ use anyhow::Result;
|
||||||
use bitcoin_hashes::hex::ToHex;
|
use bitcoin_hashes::hex::ToHex;
|
||||||
use bitcoincash::Txid;
|
use bitcoincash::Txid;
|
||||||
use riftenlabs_defi::cauldron::ParsedContract;
|
use riftenlabs_defi::cauldron::ParsedContract;
|
||||||
use rusqlite::{params, Connection, Transaction};
|
use rusqlite::{params, Connection};
|
||||||
|
|
||||||
pub fn create_table(conn: &Connection) {
|
pub fn create_table(conn: &Connection) {
|
||||||
conn.execute(
|
conn.execute(
|
||||||
|
|
@ -22,7 +22,7 @@ pub fn create_table(conn: &Connection) {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn insert_utxo_spending(
|
pub fn insert_utxo_spending(
|
||||||
tx: &Transaction,
|
tx: &Connection,
|
||||||
cauldrons: &Vec<ParsedContract>,
|
cauldrons: &Vec<ParsedContract>,
|
||||||
txid: &Txid,
|
txid: &Txid,
|
||||||
replace: bool,
|
replace: bool,
|
||||||
|
|
|
||||||
|
|
@ -106,12 +106,12 @@ fn token_volume(
|
||||||
let result: Vec<TokenVolumeInfo> = tokens
|
let result: Vec<TokenVolumeInfo> = tokens
|
||||||
.into_par_iter()
|
.into_par_iter()
|
||||||
.map(|(token_id, name, ticker)| {
|
.map(|(token_id, name, ticker)| {
|
||||||
|
|
||||||
let conn = cauldron_pool
|
let conn = cauldron_pool
|
||||||
.get()
|
.get()
|
||||||
.context("Failed to get connection from pool")?;
|
.context("Failed to get connection from pool")?;
|
||||||
|
|
||||||
let mut statement = conn.prepare(
|
let mut statement = conn
|
||||||
|
.prepare(
|
||||||
"
|
"
|
||||||
WITH TradeData AS (
|
WITH TradeData AS (
|
||||||
SELECT
|
SELECT
|
||||||
|
|
@ -120,7 +120,7 @@ fn token_volume(
|
||||||
FROM pool_history_entry phe
|
FROM pool_history_entry phe
|
||||||
JOIN pool p ON phe.pool = p.creation_utxo
|
JOIN pool p ON phe.pool = p.creation_utxo
|
||||||
JOIN tx ON phe.txid = tx.txid
|
JOIN tx ON phe.txid = tx.txid
|
||||||
WHERE COALESCE(tx.first_seen_timestamp, tx.mtp_timestamp) >= (strftime('%s', 'now') - 2592000)
|
WHERE tx.effective_timestamp >= (strftime('%s', 'now') - 2592000)
|
||||||
AND p.token_id = ?
|
AND p.token_id = ?
|
||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
|
|
@ -128,7 +128,8 @@ fn token_volume(
|
||||||
COALESCE(SUM(trade_volume), 0) as total_trade_volume
|
COALESCE(SUM(trade_volume), 0) as total_trade_volume
|
||||||
FROM TradeData;
|
FROM TradeData;
|
||||||
",
|
",
|
||||||
).context("Failed to prepare statement")?;
|
)
|
||||||
|
.context("Failed to prepare statement")?;
|
||||||
|
|
||||||
let trade_volume: u64 = statement
|
let trade_volume: u64 = statement
|
||||||
.query_row(params![&token_id], |row| row.get(1))
|
.query_row(params![&token_id], |row| row.get(1))
|
||||||
|
|
|
||||||
|
|
@ -135,8 +135,8 @@ pub fn candlesticks(
|
||||||
JOIN tx ON tx.txid = phe.txid
|
JOIN tx ON tx.txid = phe.txid
|
||||||
WHERE
|
WHERE
|
||||||
uf.token_id = ?
|
uf.token_id = ?
|
||||||
AND COALESCE(tx.first_seen_timestamp, tx.mtp_timestamp) >= ?
|
AND tx.effective_timestamp >= ?
|
||||||
AND COALESCE(tx.first_seen_timestamp, tx.mtp_timestamp) < ?
|
AND tx.effective_timestamp < ?
|
||||||
GROUP BY tx.txid
|
GROUP BY tx.txid
|
||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ fn all_time_volume(db: &Connection, end_timestamp: u64) -> Result<Vec<(String, i
|
||||||
FROM pool_history_entry phe
|
FROM pool_history_entry phe
|
||||||
JOIN pool p ON phe.pool = p.creation_utxo
|
JOIN pool p ON phe.pool = p.creation_utxo
|
||||||
JOIN tx ON phe.txid = tx.txid
|
JOIN tx ON phe.txid = tx.txid
|
||||||
WHERE COALESCE(tx.first_seen_timestamp, tx.mtp_timestamp) <= ?
|
WHERE tx.effective_timestamp <= ?
|
||||||
GROUP BY p.token_id";
|
GROUP BY p.token_id";
|
||||||
|
|
||||||
let mut stmt = db.prepare(sql)?;
|
let mut stmt = db.prepare(sql)?;
|
||||||
|
|
@ -70,7 +70,7 @@ fn period_volume(
|
||||||
FROM pool_history_entry phe
|
FROM pool_history_entry phe
|
||||||
JOIN pool p ON phe.pool = p.creation_utxo
|
JOIN pool p ON phe.pool = p.creation_utxo
|
||||||
JOIN tx ON phe.txid = tx.txid
|
JOIN tx ON phe.txid = tx.txid
|
||||||
WHERE COALESCE(tx.first_seen_timestamp, tx.mtp_timestamp) BETWEEN ? AND ?
|
WHERE tx.effective_timestamp BETWEEN ? AND ?
|
||||||
GROUP BY p.token_id
|
GROUP BY p.token_id
|
||||||
";
|
";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ fn pools_by_apy(connection: &Connection) -> Result<Vec<PoolYield>> {
|
||||||
uf.sats AS original_sats,
|
uf.sats AS original_sats,
|
||||||
uf.token_amount AS original_token_amount,
|
uf.token_amount AS original_token_amount,
|
||||||
uf.token_id as token_id,
|
uf.token_id as token_id,
|
||||||
COALESCE(tx.first_seen_timestamp, tx.mtp_timestamp) AS original_timestamp
|
tx.effective_timestamp AS original_timestamp
|
||||||
FROM
|
FROM
|
||||||
pool p
|
pool p
|
||||||
JOIN utxo_funding uf ON p.creation_utxo = uf.new_utxo_hash
|
JOIN utxo_funding uf ON p.creation_utxo = uf.new_utxo_hash
|
||||||
|
|
@ -57,7 +57,7 @@ fn pools_by_apy(connection: &Connection) -> Result<Vec<PoolYield>> {
|
||||||
phe.pool,
|
phe.pool,
|
||||||
uf.sats AS latest_sats,
|
uf.sats AS latest_sats,
|
||||||
uf.token_amount AS latest_token_amount,
|
uf.token_amount AS latest_token_amount,
|
||||||
COALESCE(tx.first_seen_timestamp, tx.mtp_timestamp) AS latest_timestamp,
|
tx.effective_timestamp AS latest_timestamp,
|
||||||
uf.new_utxo_txid,
|
uf.new_utxo_txid,
|
||||||
uf.new_utxo_n
|
uf.new_utxo_n
|
||||||
FROM
|
FROM
|
||||||
|
|
@ -70,7 +70,7 @@ fn pools_by_apy(connection: &Connection) -> Result<Vec<PoolYield>> {
|
||||||
AND us.spent_utxo_hash IS NULL
|
AND us.spent_utxo_hash IS NULL
|
||||||
|
|
||||||
ORDER BY
|
ORDER BY
|
||||||
COALESCE(tx.first_seen_timestamp, tx.mtp_timestamp) DESC
|
tx.effective_timestamp DESC
|
||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
od.original_sats,
|
od.original_sats,
|
||||||
|
|
@ -257,7 +257,10 @@ pub fn list_active_pools(
|
||||||
PoolFilters {
|
PoolFilters {
|
||||||
token_id: token.map(|s| s.to_string()),
|
token_id: token.map(|s| s.to_string()),
|
||||||
owner: pkh.map(|s| s.to_string()),
|
owner: pkh.map(|s| s.to_string()),
|
||||||
timestamp_less_than: None,
|
timestamp_lt: None,
|
||||||
|
timestamp_lte: None,
|
||||||
|
timestamp_gt: None,
|
||||||
|
timestamp_gte: None,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.map_err(|e| Custom(Status::InternalServerError, format!("Error: {e}")))?;
|
.map_err(|e| Custom(Status::InternalServerError, format!("Error: {e}")))?;
|
||||||
|
|
|
||||||
281
src/rpc/price.rs
281
src/rpc/price.rs
|
|
@ -11,7 +11,16 @@ use rusqlite::{params, Connection};
|
||||||
use rust_decimal::prelude::*;
|
use rust_decimal::prelude::*;
|
||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
|
|
||||||
use crate::{db::DB, rpc::tvl::get_token_tvl, timeutil::time_now};
|
use crate::{
|
||||||
|
db::{
|
||||||
|
cauldron::poolvisitor::{
|
||||||
|
db_visit_pool_entries, OptionalFields, OptionalPoolFields, PoolFilters, PoolVisitor,
|
||||||
|
},
|
||||||
|
DB,
|
||||||
|
},
|
||||||
|
rpc::tvl::get_token_tvl,
|
||||||
|
timeutil::time_now,
|
||||||
|
};
|
||||||
|
|
||||||
struct PriceInterval {
|
struct PriceInterval {
|
||||||
start: i64,
|
start: i64,
|
||||||
|
|
@ -166,75 +175,84 @@ fn historic_price(
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn price_at_or_before(
|
/// Works by getting the total sats/tokens for each unique pool close to the requested timestamp.
|
||||||
|
/// In case of multiple price points for the same pool, the one with the greater K (sats * tokens) is used, since K grows with usage.
|
||||||
|
#[derive(Default)]
|
||||||
|
struct PriceVisitor {
|
||||||
|
// key is pool id
|
||||||
|
max_timestamp: i64,
|
||||||
|
sum_sats: u64,
|
||||||
|
sum_tokens: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PoolVisitor for PriceVisitor {
|
||||||
|
fn optional_fields_wanted(&self) -> u64 {
|
||||||
|
OptionalPoolFields::Timestamp as u64
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit(&mut self, sats: u64, tokens: u64, optional_fields: OptionalFields) -> Result<bool> {
|
||||||
|
let timestamp = optional_fields.timestamp.unwrap();
|
||||||
|
|
||||||
|
self.max_timestamp = self.max_timestamp.max(timestamp);
|
||||||
|
self.sum_sats += sats;
|
||||||
|
self.sum_tokens += tokens;
|
||||||
|
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PriceVisitor {
|
||||||
|
fn price(&self) -> Result<Decimal> {
|
||||||
|
let sum_sats = Decimal::from_u64(self.sum_sats).unwrap_or_default();
|
||||||
|
let sum_tokens = Decimal::from_u64(self.sum_tokens).unwrap_or_default();
|
||||||
|
|
||||||
|
sum_sats
|
||||||
|
.checked_div(sum_tokens)
|
||||||
|
.context("Division by zero or overflow")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn has_data(&self) -> bool {
|
||||||
|
self.sum_sats != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn max_timestamp(&self) -> i64 {
|
||||||
|
self.max_timestamp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn price_at_or_before_2(
|
||||||
connection: &Connection,
|
connection: &Connection,
|
||||||
timestamp: i64,
|
timestamp: i64,
|
||||||
token_id: &TokenID,
|
token_id: &TokenID,
|
||||||
) -> Result<(i64, f64)> {
|
) -> Result<(i64, f64)> {
|
||||||
let sql = "
|
// Create a visitor to collect price data
|
||||||
WITH max_timestamps AS (
|
let mut visitor = PriceVisitor::default();
|
||||||
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)?;
|
// note that we may return old timestamp; but it's still the last known trade at that time
|
||||||
let mut rows = statement.query(params![timestamp, token_id.to_hex()])?;
|
|
||||||
|
|
||||||
let mut sum_sats: u64 = 0;
|
// Use db_visit_pool_entries with filters for the specific token and timestamp
|
||||||
let mut sum_tokens: u64 = 0;
|
db_visit_pool_entries(
|
||||||
let mut latest_timestamp = 0;
|
connection,
|
||||||
|
&mut visitor,
|
||||||
|
PoolFilters::new()
|
||||||
|
.lte(timestamp as u64)
|
||||||
|
.token_id(&token_id.to_hex()),
|
||||||
|
)?;
|
||||||
|
|
||||||
while let Some(row) = rows.next()? {
|
// Check if we found any data
|
||||||
let token_amount: i64 = row.get(0)?;
|
if !visitor.has_data() {
|
||||||
let sats: i64 = row.get(1)?;
|
return Err(anyhow::anyhow!(
|
||||||
let row_timestamp: i64 = row.get(2)?;
|
"No price data found for token {} at timestamp {}",
|
||||||
|
token_id.to_hex(),
|
||||||
if sats >= 0 && token_amount >= 0 {
|
timestamp
|
||||||
sum_sats += sats as u64;
|
));
|
||||||
sum_tokens += token_amount as u64;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if row_timestamp > latest_timestamp {
|
// Calculate the price
|
||||||
latest_timestamp = row_timestamp;
|
let price = visitor.price()?;
|
||||||
}
|
let price_f64 = price.to_f64().context("Failed to convert price to f64")?;
|
||||||
}
|
|
||||||
|
|
||||||
let sum_sats_decimal = Decimal::from_u64(sum_sats).unwrap_or_default();
|
Ok((visitor.max_timestamp(), price_f64))
|
||||||
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 the price of a given token at a specific timestamp.
|
/// Get the price of a given token at a specific timestamp.
|
||||||
|
|
@ -277,7 +295,7 @@ pub fn price_at(
|
||||||
let token =
|
let token =
|
||||||
TokenID::from_hex(token).map_err(|e| Custom(Status::BadRequest, format!("Error: {e}")))?;
|
TokenID::from_hex(token).map_err(|e| Custom(Status::BadRequest, format!("Error: {e}")))?;
|
||||||
|
|
||||||
match price_at_or_before(&db, timestamp, &token) {
|
match price_at_or_before_2(&db, timestamp, &token) {
|
||||||
Ok((latest_timestamp, price)) => Ok(Json(json!({
|
Ok((latest_timestamp, price)) => Ok(Json(json!({
|
||||||
"timestamp": latest_timestamp,
|
"timestamp": latest_timestamp,
|
||||||
"price": price
|
"price": price
|
||||||
|
|
@ -394,17 +412,16 @@ pub fn price_history(
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::utiltest::mock_db_pool;
|
use crate::db::cauldron::pool::update_pool_history;
|
||||||
use crate::{
|
use crate::db::cauldron::utxo_spending::{self, insert_utxo_spending};
|
||||||
db::cauldron::{
|
use crate::db::cauldron::{
|
||||||
pool::{
|
pool::{
|
||||||
self, dummy_init_seq, flag_as_withdrawn, insert_new_pool, insert_pool_history_entry,
|
self, dummy_init_seq, flag_as_withdrawn, insert_new_pool, insert_pool_history_entry,
|
||||||
},
|
},
|
||||||
tx::{self, insert_block_tx, insert_mempool_tx},
|
tx::{self, insert_block_tx, insert_mempool_tx},
|
||||||
utxo_funding::{self, insert_utxo_funding},
|
utxo_funding::{self, insert_utxo_funding},
|
||||||
},
|
|
||||||
utiltest::{calc_sats_delta, calc_token_delta},
|
|
||||||
};
|
};
|
||||||
|
use crate::utiltest::mock_db_pool;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use bitcoin_hashes::Hash;
|
use bitcoin_hashes::Hash;
|
||||||
|
|
@ -426,11 +443,12 @@ mod tests {
|
||||||
sats: u64,
|
sats: u64,
|
||||||
tokens: i64,
|
tokens: i64,
|
||||||
pkh: &PubkeyHash,
|
pkh: &PubkeyHash,
|
||||||
|
spent_utxo_hash: &OutPointHash,
|
||||||
) -> ParsedContract {
|
) -> ParsedContract {
|
||||||
ParsedContract {
|
ParsedContract {
|
||||||
pkh: *pkh,
|
pkh: *pkh,
|
||||||
is_withdrawn: false,
|
is_withdrawn: false,
|
||||||
spent_utxo_hash: OutPointHash::all_zeros(),
|
spent_utxo_hash: *spent_utxo_hash,
|
||||||
new_utxo_hash: Some(*utxo),
|
new_utxo_hash: Some(*utxo),
|
||||||
new_utxo_txid: Some(*txid),
|
new_utxo_txid: Some(*txid),
|
||||||
new_utxo_n: Some(0),
|
new_utxo_n: Some(0),
|
||||||
|
|
@ -442,6 +460,7 @@ mod tests {
|
||||||
|
|
||||||
fn setup_mock_db(conn: &Connection) {
|
fn setup_mock_db(conn: &Connection) {
|
||||||
utxo_funding::create_table(conn);
|
utxo_funding::create_table(conn);
|
||||||
|
utxo_spending::create_table(&conn);
|
||||||
tx::create_table(conn);
|
tx::create_table(conn);
|
||||||
pool::create_table(conn);
|
pool::create_table(conn);
|
||||||
dummy_init_seq();
|
dummy_init_seq();
|
||||||
|
|
@ -452,14 +471,38 @@ mod tests {
|
||||||
let txid1 = Txid::from_inner([0xf0; 32]);
|
let txid1 = Txid::from_inner([0xf0; 32]);
|
||||||
let txid2 = Txid::from_inner([0xf1; 32]);
|
let txid2 = Txid::from_inner([0xf1; 32]);
|
||||||
let txid3 = Txid::from_inner([0xf2; 32]);
|
let txid3 = Txid::from_inner([0xf2; 32]);
|
||||||
let utxo1 = OutPointHash::from_inner([0xe0; 32]);
|
let utxo1 = OutPointHash::from_inner([0xe0; 32]); // pool 1
|
||||||
let utxo2 = OutPointHash::from_inner([0xe1; 32]);
|
let utxo2 = OutPointHash::from_inner([0xe1; 32]); // pool 2
|
||||||
let utxo3 = OutPointHash::from_inner([0xe2; 32]);
|
let utxo3 = OutPointHash::from_inner([0xe2; 32]); // pool 3
|
||||||
|
|
||||||
// Insert mock data into utxo_funding
|
// Insert mock data into utxo_funding
|
||||||
let cauldron1 = dummy_cauldron(&txid1, &utxo1, &token_zero, 50000, 1000, &pkh_zero);
|
let cauldron1 = dummy_cauldron(
|
||||||
let cauldron2 = dummy_cauldron(&txid2, &utxo2, &token_zero, 60000, 2000, &pkh_zero);
|
&txid1,
|
||||||
let cauldron3 = dummy_cauldron(&txid2, &utxo3, &token_zero, 90000, 3000, &pkh_zero);
|
&utxo1,
|
||||||
|
&token_zero,
|
||||||
|
50000,
|
||||||
|
1000,
|
||||||
|
&pkh_zero,
|
||||||
|
&OutPointHash::all_zeros(),
|
||||||
|
);
|
||||||
|
let cauldron2 = dummy_cauldron(
|
||||||
|
&txid2,
|
||||||
|
&utxo2,
|
||||||
|
&token_zero,
|
||||||
|
60000,
|
||||||
|
2000,
|
||||||
|
&pkh_zero,
|
||||||
|
&OutPointHash::all_zeros(),
|
||||||
|
);
|
||||||
|
let cauldron3 = dummy_cauldron(
|
||||||
|
&txid2,
|
||||||
|
&utxo3,
|
||||||
|
&token_zero,
|
||||||
|
90000,
|
||||||
|
3000,
|
||||||
|
&pkh_zero,
|
||||||
|
&OutPointHash::all_zeros(),
|
||||||
|
);
|
||||||
insert_utxo_funding(conn, &vec![cauldron1.clone()], &txid1, true).unwrap();
|
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![cauldron2.clone()], &txid2, true).unwrap();
|
||||||
insert_utxo_funding(conn, &vec![cauldron3.clone()], &txid3, true).unwrap();
|
insert_utxo_funding(conn, &vec![cauldron3.clone()], &txid3, true).unwrap();
|
||||||
|
|
@ -474,17 +517,12 @@ mod tests {
|
||||||
insert_mempool_tx(conn, &txid3, TIME_3).unwrap();
|
insert_mempool_tx(conn, &txid3, TIME_3).unwrap();
|
||||||
|
|
||||||
// Insert mock data into `pool_history_entry`
|
// Insert mock data into `pool_history_entry`
|
||||||
let pool1 = OutPointHash::from_inner([0x0a; 32]);
|
update_pool_history(conn, vec![cauldron1.clone()], Some(TIME_1), Some(TIME_1)).unwrap();
|
||||||
let pool2 = OutPointHash::from_inner([0x0b; 32]);
|
update_pool_history(conn, vec![cauldron2], Some(TIME_2), Some(TIME_2)).unwrap();
|
||||||
let pool3 = OutPointHash::from_inner([0x0c; 32]);
|
update_pool_history(conn, vec![cauldron3], Some(TIME_3), Some(TIME_3)).unwrap();
|
||||||
|
|
||||||
let txid1_newer = Txid::from_inner([0xf3; 32]);
|
let txid1_newer = Txid::from_inner([0xf3; 32]);
|
||||||
let utxo1_newer = OutPointHash::from_inner([0xe3; 32]);
|
let utxo1_newer = OutPointHash::from_inner([0xe3; 32]);
|
||||||
insert_pool_history_entry(conn, &pool1, &cauldron1, Some(TIME_1), Some(TIME_1), 0, 0)
|
|
||||||
.unwrap();
|
|
||||||
insert_pool_history_entry(conn, &pool2, &cauldron2, Some(TIME_2), Some(TIME_2), 0, 0)
|
|
||||||
.unwrap();
|
|
||||||
insert_pool_history_entry(conn, &pool3, &cauldron3, Some(TIME_3), Some(TIME_3), 0, 0)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// Insert newer entry for pool1
|
// Insert newer entry for pool1
|
||||||
let cauldron1_newer = dummy_cauldron(
|
let cauldron1_newer = dummy_cauldron(
|
||||||
|
|
@ -494,39 +532,14 @@ mod tests {
|
||||||
70000,
|
70000,
|
||||||
1500,
|
1500,
|
||||||
&pkh_zero,
|
&pkh_zero,
|
||||||
|
&utxo1,
|
||||||
);
|
);
|
||||||
insert_pool_history_entry(
|
insert_utxo_spending(conn, &vec![cauldron1], &txid1_newer, true).unwrap();
|
||||||
|
update_pool_history(
|
||||||
conn,
|
conn,
|
||||||
&pool1,
|
vec![cauldron1_newer],
|
||||||
&cauldron1_newer,
|
|
||||||
Some(1727963500),
|
Some(1727963500),
|
||||||
Some(1727963500),
|
Some(1727963500),
|
||||||
calc_sats_delta(&cauldron1, &cauldron1_newer),
|
|
||||||
calc_token_delta(&cauldron1, &cauldron1_newer),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
insert_utxo_funding(conn, &vec![cauldron1_newer], &txid1_newer, true).unwrap();
|
|
||||||
|
|
||||||
// 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),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
insert_new_pool(
|
|
||||||
conn,
|
|
||||||
&dummy_cauldron(&Txid::all_zeros(), &pool2, &token2, 0, 0, &pkh2),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
insert_new_pool(
|
|
||||||
conn,
|
|
||||||
&dummy_cauldron(&Txid::all_zeros(), &pool3, &token3, 0, 0, &pkh3),
|
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
|
@ -543,6 +556,7 @@ mod tests {
|
||||||
80000,
|
80000,
|
||||||
2500,
|
2500,
|
||||||
&inactive_pkh,
|
&inactive_pkh,
|
||||||
|
&OutPointHash::all_zeros(),
|
||||||
);
|
);
|
||||||
let inactive_cauldron_withdraw = ParsedContract {
|
let inactive_cauldron_withdraw = ParsedContract {
|
||||||
pkh: inactive_pkh,
|
pkh: inactive_pkh,
|
||||||
|
|
@ -588,7 +602,12 @@ mod tests {
|
||||||
let response = client
|
let response = client
|
||||||
.get(format!("/cauldron/price/{token_id}/at/{timestamp}"))
|
.get(format!("/cauldron/price/{token_id}/at/{timestamp}"))
|
||||||
.dispatch();
|
.dispatch();
|
||||||
assert_eq!(response.status(), Status::Ok);
|
assert_eq!(
|
||||||
|
response.status(),
|
||||||
|
Status::Ok,
|
||||||
|
"response: {}",
|
||||||
|
response.into_string().unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
let json_value: serde_json::Value =
|
let json_value: serde_json::Value =
|
||||||
serde_json::from_str(response.into_string().unwrap().as_str()).unwrap();
|
serde_json::from_str(response.into_string().unwrap().as_str()).unwrap();
|
||||||
|
|
@ -653,7 +672,12 @@ mod tests {
|
||||||
let response = client
|
let response = client
|
||||||
.get(format!("/cauldron/price/{token_id}/at/{timestamp}"))
|
.get(format!("/cauldron/price/{token_id}/at/{timestamp}"))
|
||||||
.dispatch();
|
.dispatch();
|
||||||
assert_eq!(response.status(), Status::Ok);
|
assert_eq!(
|
||||||
|
response.status(),
|
||||||
|
Status::Ok,
|
||||||
|
"response: {}",
|
||||||
|
response.into_string().unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
let json_value: serde_json::Value =
|
let json_value: serde_json::Value =
|
||||||
serde_json::from_str(response.into_string().unwrap().as_str()).unwrap();
|
serde_json::from_str(response.into_string().unwrap().as_str()).unwrap();
|
||||||
|
|
@ -671,6 +695,9 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_price_with_multiple_entries_at_same_timestamp() {
|
fn test_price_with_multiple_entries_at_same_timestamp() {
|
||||||
|
// when encountering dupicate timestamp for same pool; it should
|
||||||
|
// use the latest entry
|
||||||
|
|
||||||
let mock_db = mock_db_pool(setup_mock_db);
|
let mock_db = mock_db_pool(setup_mock_db);
|
||||||
|
|
||||||
let rocket = rocket::build()
|
let rocket = rocket::build()
|
||||||
|
|
@ -685,27 +712,32 @@ mod tests {
|
||||||
let conn = mock_db.cauldron_w.get().expect("Failed to get connection.");
|
let conn = mock_db.cauldron_w.get().expect("Failed to get connection.");
|
||||||
|
|
||||||
// These entries should have the same timestamp as previous mockdata and be counted in the price calculation
|
// 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 pool1 = OutPointHash::from_inner([0xe0; 32]);
|
||||||
let txid = Txid::hash("txid_extra".as_bytes());
|
let txid = Txid::hash("txid_extra".as_bytes());
|
||||||
|
|
||||||
let cauldron = dummy_cauldron(
|
let cauldron = dummy_cauldron(
|
||||||
&txid,
|
&txid,
|
||||||
&OutPointHash::hash("utxo_extra".as_bytes()),
|
&OutPointHash::hash("utxo_extra".as_bytes()),
|
||||||
&TokenID::all_zeros(),
|
&TokenID::all_zeros(),
|
||||||
40000,
|
1,
|
||||||
500,
|
1,
|
||||||
&PubkeyHash::all_zeros(),
|
&PubkeyHash::all_zeros(),
|
||||||
|
&pool1,
|
||||||
);
|
);
|
||||||
insert_pool_history_entry(&conn, &pool1, &cauldron, Some(TIME_1), Some(TIME_1), 0, 0)
|
update_pool_history(&conn, vec![cauldron], Some(TIME_1), Some(TIME_1)).unwrap();
|
||||||
.unwrap();
|
// insert_utxo_funding(&conn, &vec![cauldron], &txid, true).unwrap();
|
||||||
insert_utxo_funding(&conn, &vec![cauldron], &txid, true).unwrap();
|
|
||||||
|
|
||||||
// Test: Query at the timestamp matching test_txid1 (1727963300) and check the price
|
// Test: Query at the timestamp matching test_txid1 (TIME_1) and check the price
|
||||||
let timestamp = TIME_1;
|
let timestamp = TIME_1;
|
||||||
let response = client
|
let response = client
|
||||||
.get(format!("/cauldron/price/{token_id}/at/{timestamp}"))
|
.get(format!("/cauldron/price/{token_id}/at/{timestamp}"))
|
||||||
.dispatch();
|
.dispatch();
|
||||||
assert_eq!(response.status(), Status::Ok);
|
assert_eq!(
|
||||||
|
response.status(),
|
||||||
|
Status::Ok,
|
||||||
|
"response: {}",
|
||||||
|
response.into_string().unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
let json_value: serde_json::Value =
|
let json_value: serde_json::Value =
|
||||||
serde_json::from_str(response.into_string().unwrap().as_str()).unwrap();
|
serde_json::from_str(response.into_string().unwrap().as_str()).unwrap();
|
||||||
|
|
@ -713,9 +745,9 @@ mod tests {
|
||||||
.as_f64()
|
.as_f64()
|
||||||
.expect("Price field is not a valid 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
|
// Expected price based on 1 sats and 1 tokens
|
||||||
let expected_price = 60.0;
|
let expected_price = 1.0;
|
||||||
assert!((actual_price - expected_price).abs() < 0.01);
|
assert_eq!(actual_price, expected_price);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -807,6 +839,7 @@ mod tests {
|
||||||
1, /* low sats */
|
1, /* low sats */
|
||||||
9999999999999999, /* many tokens */
|
9999999999999999, /* many tokens */
|
||||||
&PubkeyHash::all_zeros(),
|
&PubkeyHash::all_zeros(),
|
||||||
|
&OutPointHash::all_zeros(),
|
||||||
);
|
);
|
||||||
insert_utxo_funding(&conn, &vec![cauldron.clone()], &txid, true).unwrap();
|
insert_utxo_funding(&conn, &vec![cauldron.clone()], &txid, true).unwrap();
|
||||||
insert_pool_history_entry(&conn, &pool, &cauldron, Some(TIME_1), Some(TIME_1), 0, 0)
|
insert_pool_history_entry(&conn, &pool, &cauldron, Some(TIME_1), Some(TIME_1), 0, 0)
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,10 @@ pub fn deprecated_get_all_token_tvl(
|
||||||
connection,
|
connection,
|
||||||
&mut visitor,
|
&mut visitor,
|
||||||
PoolFilters {
|
PoolFilters {
|
||||||
timestamp_less_than: Some(max_timestamp as u64),
|
timestamp_lt: Some(max_timestamp as u64),
|
||||||
|
timestamp_lte: None,
|
||||||
|
timestamp_gt: None,
|
||||||
|
timestamp_gte: None,
|
||||||
token_id: None,
|
token_id: None,
|
||||||
owner: None,
|
owner: None,
|
||||||
},
|
},
|
||||||
|
|
@ -101,7 +104,10 @@ pub fn get_total_sats_tvl(connection: &Connection, max_timestamp: Option<usize>)
|
||||||
connection,
|
connection,
|
||||||
&mut visitor,
|
&mut visitor,
|
||||||
PoolFilters {
|
PoolFilters {
|
||||||
timestamp_less_than: max_timestamp.map(|t| t as u64),
|
timestamp_lt: max_timestamp.map(|t| t as u64),
|
||||||
|
timestamp_lte: None,
|
||||||
|
timestamp_gt: None,
|
||||||
|
timestamp_gte: None,
|
||||||
token_id: None,
|
token_id: None,
|
||||||
owner: None,
|
owner: None,
|
||||||
},
|
},
|
||||||
|
|
@ -121,7 +127,10 @@ pub fn get_token_tvl(
|
||||||
connection,
|
connection,
|
||||||
&mut visitor,
|
&mut visitor,
|
||||||
PoolFilters {
|
PoolFilters {
|
||||||
timestamp_less_than: max_timestamp.map(|t| t as u64),
|
timestamp_lt: max_timestamp.map(|t| t as u64),
|
||||||
|
timestamp_lte: None,
|
||||||
|
timestamp_gt: None,
|
||||||
|
timestamp_gte: None,
|
||||||
token_id: Some(token_id.to_string()),
|
token_id: Some(token_id.to_string()),
|
||||||
owner: None,
|
owner: None,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -9,30 +9,9 @@ mod test_utils {
|
||||||
use crate::db::DB;
|
use crate::db::DB;
|
||||||
use r2d2::Pool;
|
use r2d2::Pool;
|
||||||
use r2d2_sqlite::SqliteConnectionManager;
|
use r2d2_sqlite::SqliteConnectionManager;
|
||||||
use riftenlabs_defi::cauldron::ParsedContract;
|
|
||||||
use rusqlite::Connection;
|
use rusqlite::Connection;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
pub fn calc_sats_delta(a: &ParsedContract, b: &ParsedContract) -> i64 {
|
|
||||||
assert!(
|
|
||||||
a.token_id == b.token_id,
|
|
||||||
"Different token ID; so definetly not the same pool"
|
|
||||||
);
|
|
||||||
let a_sats = a.sats.unwrap_or(0);
|
|
||||||
let b_sats = b.sats.unwrap_or(0);
|
|
||||||
b_sats as i64 - a_sats as i64
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn calc_token_delta(a: &ParsedContract, b: &ParsedContract) -> i64 {
|
|
||||||
assert!(
|
|
||||||
a.token_id == b.token_id,
|
|
||||||
"Different token ID; so definetly not the same pool"
|
|
||||||
);
|
|
||||||
let a_tokens = a.token_amount.unwrap_or(0);
|
|
||||||
let b_tokens = b.token_amount.unwrap_or(0);
|
|
||||||
b_tokens as i64 - a_tokens as i64
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn mock_db_pool<F>(setup_fn: F) -> DB
|
pub fn mock_db_pool<F>(setup_fn: F) -> DB
|
||||||
where
|
where
|
||||||
F: Fn(&Connection),
|
F: Fn(&Connection),
|
||||||
|
|
@ -56,9 +35,5 @@ mod test_utils {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
pub use test_utils::calc_sats_delta;
|
|
||||||
#[cfg(test)]
|
|
||||||
pub use test_utils::calc_token_delta;
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub use test_utils::mock_db_pool;
|
pub use test_utils::mock_db_pool;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue