2026-01-21 12:34:59 +01:00
|
|
|
// Copyright (C) 2024-2026 Whiterun LLC
|
2024-03-04 16:40:50 +01:00
|
|
|
//
|
|
|
|
|
// 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
|
|
|
|
|
|
2024-10-21 15:13:16 +02:00
|
|
|
use std::{
|
|
|
|
|
collections::{HashMap, VecDeque},
|
|
|
|
|
sync::atomic::AtomicI64,
|
|
|
|
|
};
|
2024-03-04 16:40:50 +01:00
|
|
|
|
2026-02-01 13:23:57 +01:00
|
|
|
use crate::db::blob::{blob_to_display_hex, display_hex_to_blob, FromBlob, ToBlob};
|
2025-07-18 14:11:42 +02:00
|
|
|
use crate::def::PoolID;
|
2025-08-15 17:48:25 +02:00
|
|
|
use anyhow::{Context, Result};
|
Convert hash columns from TEXT to BLOB storage
Migrate all 32-byte hash fields from TEXT (64-char hex) to BLOB
(32 bytes) for ~50% storage savings on hash columns.
Changes:
- Add src/db/blob.rs with ToBlob/FromBlob traits for hash types
- Update all table schemas to use BLOB for hash columns
- Replace .to_hex() with .to_blob() for DB inserts
- Replace ::from_hex() with ::from_blob() for DB reads
- Use SQL hex() function where API responses need hex strings
- Bump DB_VERSION from 4 to 5
Affected tables: tx, pool, pool_history_entry, utxo_spending,
utxo_funding, user_action, auth_chain_entry, bcmr_data,
bcmr_failure, bcmr_well_known, crc20, crc20_candidates,
delphi_entry, cached_token_metrics
Fixes #1
2026-01-21 13:25:35 +01:00
|
|
|
use bitcoin_hashes::hex::ToHex;
|
2026-02-01 13:23:57 +01:00
|
|
|
use bitcoincash::TokenID;
|
2024-10-21 15:13:16 +02:00
|
|
|
use log::{debug, info, warn};
|
2025-07-16 09:30:17 +02:00
|
|
|
use malachite::Integer;
|
2024-03-04 16:40:50 +01:00
|
|
|
use riftenlabs_defi::{cauldron::ParsedContract, chainutil::OutPointHash};
|
2024-10-21 15:13:16 +02:00
|
|
|
use rust_decimal::prelude::Zero;
|
2025-07-16 09:30:17 +02:00
|
|
|
use serde::{Serialize, Serializer};
|
2026-02-17 17:47:43 +01:00
|
|
|
use sqlx::{Row, SqliteConnection, SqlitePool};
|
2024-10-21 15:13:16 +02:00
|
|
|
|
|
|
|
|
use crate::rpc::apy::PoolSnapshot;
|
2024-03-04 16:40:50 +01:00
|
|
|
|
2025-07-16 09:30:17 +02:00
|
|
|
fn serialize_integer_as_string<S>(integer: &Integer, serializer: S) -> Result<S::Ok, S::Error>
|
|
|
|
|
where
|
|
|
|
|
S: Serializer,
|
|
|
|
|
{
|
|
|
|
|
serializer.serialize_str(&integer.to_string())
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-17 17:47:43 +01:00
|
|
|
pub async fn create_table(pool: &SqlitePool) {
|
|
|
|
|
sqlx::query(
|
|
|
|
|
"CREATE TABLE pool (
|
Convert hash columns from TEXT to BLOB storage
Migrate all 32-byte hash fields from TEXT (64-char hex) to BLOB
(32 bytes) for ~50% storage savings on hash columns.
Changes:
- Add src/db/blob.rs with ToBlob/FromBlob traits for hash types
- Update all table schemas to use BLOB for hash columns
- Replace .to_hex() with .to_blob() for DB inserts
- Replace ::from_hex() with ::from_blob() for DB reads
- Use SQL hex() function where API responses need hex strings
- Bump DB_VERSION from 4 to 5
Affected tables: tx, pool, pool_history_entry, utxo_spending,
utxo_funding, user_action, auth_chain_entry, bcmr_data,
bcmr_failure, bcmr_well_known, crc20, crc20_candidates,
delphi_entry, cached_token_metrics
Fixes #1
2026-01-21 13:25:35 +01:00
|
|
|
creation_utxo BLOB PRIMARY KEY REFERENCES utxo_funding(new_utxo_hash) ON DELETE CASCADE,
|
|
|
|
|
owner_pkh BLOB NOT NULL,
|
|
|
|
|
token_id BLOB NOT NULL,
|
|
|
|
|
withdrawn_in_utxo BLOB REFERENCES utxo_spending(spent_utxo_hash) ON DELETE SET NULL
|
2024-03-04 16:40:50 +01:00
|
|
|
)",
|
|
|
|
|
)
|
2026-02-17 17:47:43 +01:00
|
|
|
.execute(pool)
|
|
|
|
|
.await
|
2024-03-04 16:40:50 +01:00
|
|
|
.expect("failed to create table pool");
|
|
|
|
|
|
2026-02-17 17:47:43 +01:00
|
|
|
sqlx::query(
|
2024-03-04 16:40:50 +01:00
|
|
|
"CREATE TABLE pool_history_entry (
|
Convert hash columns from TEXT to BLOB storage
Migrate all 32-byte hash fields from TEXT (64-char hex) to BLOB
(32 bytes) for ~50% storage savings on hash columns.
Changes:
- Add src/db/blob.rs with ToBlob/FromBlob traits for hash types
- Update all table schemas to use BLOB for hash columns
- Replace .to_hex() with .to_blob() for DB inserts
- Replace ::from_hex() with ::from_blob() for DB reads
- Use SQL hex() function where API responses need hex strings
- Bump DB_VERSION from 4 to 5
Affected tables: tx, pool, pool_history_entry, utxo_spending,
utxo_funding, user_action, auth_chain_entry, bcmr_data,
bcmr_failure, bcmr_well_known, crc20, crc20_candidates,
delphi_entry, cached_token_metrics
Fixes #1
2026-01-21 13:25:35 +01:00
|
|
|
utxo BLOB PRIMARY KEY REFERENCES utxo_funding(new_utxo_hash) ON DELETE CASCADE,
|
|
|
|
|
pool BLOB REFERENCES pool(creation_utxo) ON DELETE CASCADE,
|
|
|
|
|
token_id BLOB NOT NULL,
|
|
|
|
|
txid BLOB REFERENCES tx(txid) ON DELETE CASCADE,
|
2024-10-25 14:45:37 +02:00
|
|
|
tx_pos INT NOT NULL,
|
2024-10-08 17:21:22 +02:00
|
|
|
mtp_timestamp BIGINT,
|
2024-10-21 15:13:16 +02:00
|
|
|
first_seen_timestamp BIGINT,
|
2025-08-18 22:30:10 +02:00
|
|
|
effective_timestamp BIGINT GENERATED ALWAYS AS (COALESCE(first_seen_timestamp, mtp_timestamp)),
|
2024-10-21 15:13:16 +02:00
|
|
|
sequence BIGINT NOT NULL,
|
2025-08-15 22:22:07 +02:00
|
|
|
sats BIGINT NOT NULL,
|
|
|
|
|
token_amount BIGINT NOT NULL,
|
|
|
|
|
sats_delta BIGINT NOT NULL,
|
|
|
|
|
token_delta BIGINT NOT NULL
|
2024-03-04 16:40:50 +01:00
|
|
|
)",
|
|
|
|
|
)
|
2026-02-17 17:47:43 +01:00
|
|
|
.execute(pool)
|
|
|
|
|
.await
|
2024-03-04 16:40:50 +01:00
|
|
|
.expect("failed to create table pool_history_entry");
|
2024-10-25 14:45:37 +02:00
|
|
|
|
2026-02-17 17:47:43 +01:00
|
|
|
sqlx::query(
|
2024-10-25 14:45:37 +02:00
|
|
|
"CREATE INDEX idx_pool_history_entry_pool_sequence ON pool_history_entry(pool, sequence)",
|
2025-08-18 22:30:10 +02:00
|
|
|
)
|
2026-02-17 17:47:43 +01:00
|
|
|
.execute(pool)
|
|
|
|
|
.await
|
2025-08-18 22:30:10 +02:00
|
|
|
.unwrap();
|
2026-02-17 17:47:43 +01:00
|
|
|
sqlx::query("CREATE INDEX idx_pool_owner_pkh ON pool(owner_pkh)")
|
|
|
|
|
.execute(pool)
|
|
|
|
|
.await
|
|
|
|
|
.unwrap();
|
|
|
|
|
sqlx::query("CREATE INDEX idx_pool_withdrawn_in_utxo ON pool(withdrawn_in_utxo)")
|
|
|
|
|
.execute(pool)
|
|
|
|
|
.await
|
|
|
|
|
.unwrap();
|
|
|
|
|
sqlx::query("CREATE INDEX idx_pool_history_entry_pool_timestamp_sequence ON pool_history_entry (pool, effective_timestamp, sequence DESC)")
|
|
|
|
|
.execute(pool).await.unwrap();
|
|
|
|
|
sqlx::query("CREATE INDEX idx_pool_history_entry_token_id ON pool_history_entry(token_id)")
|
|
|
|
|
.execute(pool)
|
|
|
|
|
.await
|
|
|
|
|
.unwrap();
|
2024-03-04 16:40:50 +01:00
|
|
|
}
|
|
|
|
|
|
2026-02-17 17:47:43 +01:00
|
|
|
async fn get_pool_by_utxo(
|
|
|
|
|
conn: &mut SqliteConnection,
|
|
|
|
|
utxo_hash: &OutPointHash,
|
|
|
|
|
) -> Result<Option<OutPointHash>> {
|
|
|
|
|
let row: Option<(Vec<u8>,)> =
|
|
|
|
|
sqlx::query_as("SELECT pool FROM pool_history_entry WHERE utxo = ?")
|
|
|
|
|
.bind(utxo_hash.to_blob())
|
|
|
|
|
.fetch_optional(&mut *conn)
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
match row {
|
|
|
|
|
Some((blob,)) => Ok(Some(
|
Convert hash columns from TEXT to BLOB storage
Migrate all 32-byte hash fields from TEXT (64-char hex) to BLOB
(32 bytes) for ~50% storage savings on hash columns.
Changes:
- Add src/db/blob.rs with ToBlob/FromBlob traits for hash types
- Update all table schemas to use BLOB for hash columns
- Replace .to_hex() with .to_blob() for DB inserts
- Replace ::from_hex() with ::from_blob() for DB reads
- Use SQL hex() function where API responses need hex strings
- Bump DB_VERSION from 4 to 5
Affected tables: tx, pool, pool_history_entry, utxo_spending,
utxo_funding, user_action, auth_chain_entry, bcmr_data,
bcmr_failure, bcmr_well_known, crc20, crc20_candidates,
delphi_entry, cached_token_metrics
Fixes #1
2026-01-21 13:25:35 +01:00
|
|
|
OutPointHash::from_blob(&blob).expect("invalid original_utxo utxo in db"),
|
2024-03-04 16:40:50 +01:00
|
|
|
)),
|
|
|
|
|
None => Ok(None),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-17 17:47:43 +01:00
|
|
|
pub async fn flag_as_withdrawn(
|
|
|
|
|
conn: &mut SqliteConnection,
|
2024-03-04 16:40:50 +01:00
|
|
|
pool_utxo: &OutPointHash,
|
|
|
|
|
cauldron: &ParsedContract,
|
|
|
|
|
) -> Result<()> {
|
2026-02-17 17:47:43 +01:00
|
|
|
sqlx::query("UPDATE pool SET withdrawn_in_utxo = ? WHERE creation_utxo = ?")
|
|
|
|
|
.bind(cauldron.spent_utxo_hash.to_blob())
|
|
|
|
|
.bind(pool_utxo.to_blob())
|
|
|
|
|
.execute(&mut *conn)
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| anyhow::anyhow!("failed flag pool as withdrawn. Original error: {:?}", e))?;
|
2024-03-04 16:40:50 +01:00
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-17 17:47:43 +01:00
|
|
|
pub async fn insert_new_pool(conn: &mut SqliteConnection, cauldron: &ParsedContract) -> Result<()> {
|
|
|
|
|
sqlx::query(
|
2024-03-04 16:40:50 +01:00
|
|
|
"INSERT OR REPLACE INTO pool (creation_utxo, owner_pkh, token_id, withdrawn_in_utxo) VALUES (?, ?, ?, ?)",
|
2026-02-17 17:47:43 +01:00
|
|
|
)
|
|
|
|
|
.bind(cauldron.new_utxo_hash.expect("outpoint hash for new pool missing").to_blob())
|
|
|
|
|
.bind(cauldron.pkh.to_blob())
|
|
|
|
|
.bind(cauldron.token_id.expect("token id for new pool missing").to_blob())
|
|
|
|
|
.bind(None::<Vec<u8>>)
|
|
|
|
|
.execute(&mut *conn)
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| anyhow::anyhow!("failed to insert new pool. Original error: {:?}", e))?;
|
2024-03-04 16:40:50 +01:00
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-21 15:13:16 +02:00
|
|
|
/// Next sequence number in the `pool_history_entry` table
|
|
|
|
|
static NEXT_SEQUENCE: AtomicI64 = AtomicI64::new(-10);
|
|
|
|
|
|
2026-02-17 17:47:43 +01:00
|
|
|
pub async fn initialize_seq(pool: &SqlitePool) {
|
|
|
|
|
let row: (i64,) = sqlx::query_as("SELECT IFNULL(MAX(sequence), 0) + 1 FROM pool_history_entry")
|
|
|
|
|
.fetch_one(pool)
|
|
|
|
|
.await
|
2024-10-21 15:13:16 +02:00
|
|
|
.unwrap();
|
2026-02-17 17:47:43 +01:00
|
|
|
NEXT_SEQUENCE.store(row.0, std::sync::atomic::Ordering::SeqCst);
|
2024-10-21 15:13:16 +02:00
|
|
|
}
|
|
|
|
|
|
2026-02-17 17:47:43 +01:00
|
|
|
#[allow(dead_code)]
|
2024-10-24 09:12:12 +02:00
|
|
|
pub fn dummy_init_seq() {
|
|
|
|
|
if NEXT_SEQUENCE.load(std::sync::atomic::Ordering::SeqCst) < 0 {
|
|
|
|
|
NEXT_SEQUENCE.store(42, std::sync::atomic::Ordering::SeqCst);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-17 17:47:43 +01:00
|
|
|
pub async fn insert_pool_history_entry(
|
|
|
|
|
conn: &mut SqliteConnection,
|
2024-03-04 16:40:50 +01:00
|
|
|
pool: &OutPointHash,
|
|
|
|
|
cauldron: &ParsedContract,
|
2024-10-08 17:21:22 +02:00
|
|
|
mtp_timestamp: Option<u64>,
|
|
|
|
|
first_seen_timestamp: Option<u64>,
|
2025-08-15 17:48:25 +02:00
|
|
|
sats_delta: i64,
|
|
|
|
|
token_delta: i64,
|
2024-03-04 16:40:50 +01:00
|
|
|
) -> Result<()> {
|
2024-10-21 15:13:16 +02:00
|
|
|
let next_seq = NEXT_SEQUENCE.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
|
|
|
|
|
assert!(next_seq >= 0);
|
|
|
|
|
|
2026-02-17 17:47:43 +01:00
|
|
|
sqlx::query(
|
2025-08-18 22:30:10 +02:00
|
|
|
"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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
2024-10-08 17:21:22 +02:00
|
|
|
ON CONFLICT(utxo) DO UPDATE SET
|
|
|
|
|
pool = excluded.pool,
|
2025-08-18 22:30:10 +02:00
|
|
|
token_id = excluded.token_id,
|
2024-10-08 17:21:22 +02:00
|
|
|
txid = excluded.txid,
|
|
|
|
|
tx_pos = excluded.tx_pos,
|
|
|
|
|
mtp_timestamp = COALESCE(excluded.mtp_timestamp, pool_history_entry.mtp_timestamp),
|
2024-10-21 15:13:16 +02:00
|
|
|
first_seen_timestamp = COALESCE(excluded.first_seen_timestamp, pool_history_entry.first_seen_timestamp),
|
|
|
|
|
sequence = excluded.sequence",
|
2024-03-04 16:40:50 +01:00
|
|
|
)
|
2026-02-17 17:47:43 +01:00
|
|
|
.bind(cauldron.new_utxo_hash.expect("utxo hash on new pool history entry").to_blob())
|
|
|
|
|
.bind(pool.to_blob())
|
|
|
|
|
.bind(cauldron.token_id.expect("token id on new pool history entry").to_blob())
|
|
|
|
|
.bind(cauldron.new_utxo_txid.expect("txid of new pool history entry").to_blob())
|
|
|
|
|
.bind(cauldron.new_utxo_n.expect("utxo index of new pool history entry") as i64)
|
|
|
|
|
.bind(mtp_timestamp.map(|t| t as i64))
|
|
|
|
|
.bind(first_seen_timestamp.map(|t| t as i64))
|
|
|
|
|
.bind(next_seq)
|
|
|
|
|
.bind(cauldron.sats.map(|s| s as i64))
|
|
|
|
|
.bind(cauldron.token_amount)
|
|
|
|
|
.bind(sats_delta)
|
|
|
|
|
.bind(token_delta)
|
|
|
|
|
.execute(&mut *conn)
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| anyhow::anyhow!("failed to insert pool history entry. Original error: {:?}", e))?;
|
2024-03-04 16:40:50 +01:00
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-17 17:47:43 +01:00
|
|
|
pub async fn update_pool_history(
|
|
|
|
|
conn: &mut SqliteConnection,
|
2024-10-08 17:21:22 +02:00
|
|
|
cauldrons: Vec<ParsedContract>,
|
|
|
|
|
mtp_timestamp: Option<u64>,
|
|
|
|
|
first_seen_timestamp: Option<u64>,
|
|
|
|
|
) -> Result<()> {
|
2024-03-04 16:40:50 +01:00
|
|
|
if cauldrons.is_empty() {
|
|
|
|
|
return Ok(());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let mut queue = VecDeque::from(cauldrons);
|
|
|
|
|
|
|
|
|
|
while let Some(current) = queue.pop_front() {
|
2026-02-17 17:47:43 +01:00
|
|
|
let (pool_utxo, is_new) =
|
|
|
|
|
match get_pool_by_utxo(&mut *conn, ¤t.spent_utxo_hash).await? {
|
|
|
|
|
Some(c) => (c, false),
|
|
|
|
|
None => {
|
|
|
|
|
let has_parent = queue
|
|
|
|
|
.iter()
|
|
|
|
|
.any(|parent| Some(current.spent_utxo_hash) == parent.new_utxo_hash);
|
|
|
|
|
|
|
|
|
|
if has_parent {
|
|
|
|
|
queue.push_back(current);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let Some(utxo) = current.new_utxo_hash {
|
|
|
|
|
assert!(!current.is_withdrawn);
|
|
|
|
|
(utxo, true)
|
|
|
|
|
} else {
|
|
|
|
|
assert!(current.is_withdrawn);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2024-03-04 16:40:50 +01:00
|
|
|
}
|
2026-02-17 17:47:43 +01:00
|
|
|
};
|
2024-03-04 16:40:50 +01:00
|
|
|
|
|
|
|
|
if current.is_withdrawn {
|
|
|
|
|
info!(
|
|
|
|
|
"LP {} withdrawn in {}",
|
|
|
|
|
pool_utxo.to_hex(),
|
|
|
|
|
current.spent_utxo_hash.to_hex()
|
|
|
|
|
);
|
2026-02-17 17:47:43 +01:00
|
|
|
flag_as_withdrawn(&mut *conn, &pool_utxo, ¤t).await?;
|
2024-03-04 16:40:50 +01:00
|
|
|
} else if is_new {
|
|
|
|
|
info!(
|
|
|
|
|
"Cauldron LP created in tx {}",
|
|
|
|
|
current
|
|
|
|
|
.new_utxo_txid
|
|
|
|
|
.expect("expected txid in new LP")
|
|
|
|
|
.to_hex()
|
|
|
|
|
);
|
2026-02-17 17:47:43 +01:00
|
|
|
insert_new_pool(&mut *conn, ¤t).await?;
|
2024-10-08 17:21:22 +02:00
|
|
|
insert_pool_history_entry(
|
2026-02-17 17:47:43 +01:00
|
|
|
&mut *conn,
|
2024-10-08 17:21:22 +02:00
|
|
|
&pool_utxo,
|
|
|
|
|
¤t,
|
|
|
|
|
mtp_timestamp,
|
|
|
|
|
first_seen_timestamp,
|
2026-02-17 17:47:43 +01:00
|
|
|
0,
|
|
|
|
|
0,
|
|
|
|
|
)
|
|
|
|
|
.await?;
|
2024-03-04 16:40:50 +01:00
|
|
|
} else {
|
|
|
|
|
debug!("New entry for pool {}", pool_utxo.to_hex());
|
2026-02-17 17:47:43 +01:00
|
|
|
let prev_entry = get_pool_history_entry(&mut *conn, current.spent_utxo_hash).await?;
|
2025-08-15 17:48:25 +02:00
|
|
|
let sats_delta = if let Some(current_sats) = current.sats {
|
|
|
|
|
current_sats as i64 - prev_entry.sats as i64
|
|
|
|
|
} else {
|
|
|
|
|
0
|
|
|
|
|
};
|
|
|
|
|
let token_delta = if let Some(current_token_amount) = current.token_amount {
|
|
|
|
|
current_token_amount - prev_entry.token_amount as i64
|
|
|
|
|
} else {
|
|
|
|
|
0
|
|
|
|
|
};
|
2024-10-08 17:21:22 +02:00
|
|
|
insert_pool_history_entry(
|
2026-02-17 17:47:43 +01:00
|
|
|
&mut *conn,
|
2024-10-08 17:21:22 +02:00
|
|
|
&pool_utxo,
|
|
|
|
|
¤t,
|
|
|
|
|
mtp_timestamp,
|
|
|
|
|
first_seen_timestamp,
|
2025-08-15 17:48:25 +02:00
|
|
|
sats_delta,
|
|
|
|
|
token_delta,
|
2026-02-17 17:47:43 +01:00
|
|
|
)
|
|
|
|
|
.await?;
|
2024-03-04 16:40:50 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
2024-10-21 15:13:16 +02:00
|
|
|
|
|
|
|
|
/// Strategy for which pool state to select when two pool states are available for a timestamp
|
|
|
|
|
#[derive(PartialEq, Eq)]
|
|
|
|
|
enum SnapshotSelection {
|
|
|
|
|
UseBefore,
|
|
|
|
|
UseAfter,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Get pools and their state nearest to given timestamp.
|
2026-02-17 17:47:43 +01:00
|
|
|
async fn get_nearest_entries(
|
|
|
|
|
pool: &SqlitePool,
|
2024-10-21 15:13:16 +02:00
|
|
|
timestamp: i64,
|
|
|
|
|
token_id: Option<&str>,
|
|
|
|
|
owner_pkh: Option<&str>,
|
|
|
|
|
resolution: SnapshotSelection,
|
|
|
|
|
) -> Result<HashMap<String, PoolSnapshot>> {
|
2026-02-05 10:18:06 +01:00
|
|
|
let token_blob = match token_id {
|
|
|
|
|
Some(id) => Some(display_hex_to_blob::<TokenID>(id)?),
|
|
|
|
|
None => None,
|
|
|
|
|
};
|
|
|
|
|
let owner_pkh_blob = match owner_pkh {
|
2026-02-17 17:47:43 +01:00
|
|
|
Some(pkh) => Some(hex::decode(pkh)?),
|
2026-02-05 10:18:06 +01:00
|
|
|
None => None,
|
2024-10-21 15:13:16 +02:00
|
|
|
};
|
|
|
|
|
|
2026-02-05 10:18:06 +01:00
|
|
|
let extra_filters = match (&token_blob, &owner_pkh_blob) {
|
|
|
|
|
(Some(_), Some(_)) => "token_id = ?2 AND owner_pkh = ?3",
|
|
|
|
|
(Some(_), None) => "token_id = ?2",
|
|
|
|
|
(None, Some(_)) => "owner_pkh = ?2",
|
|
|
|
|
(None, None) => "1=1",
|
2024-10-21 15:13:16 +02:00
|
|
|
};
|
|
|
|
|
|
2025-08-18 22:30:10 +02:00
|
|
|
let ts = "effective_timestamp";
|
2024-10-21 15:13:16 +02:00
|
|
|
|
|
|
|
|
let preferred = match resolution {
|
2026-02-17 17:47:43 +01:00
|
|
|
SnapshotSelection::UseBefore => "ts ASC",
|
|
|
|
|
SnapshotSelection::UseAfter => "ts DESC",
|
2024-10-21 15:13:16 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let query = format!(
|
|
|
|
|
"WITH NearestEntries AS (
|
|
|
|
|
SELECT phe.pool, phe.sats, phe.token_amount, {ts} AS ts, phe.sequence
|
|
|
|
|
FROM pool_history_entry phe
|
|
|
|
|
WHERE {ts} <= ?1
|
|
|
|
|
AND phe.pool IN (
|
|
|
|
|
SELECT creation_utxo
|
|
|
|
|
FROM pool
|
|
|
|
|
WHERE {extra_filters}
|
2026-03-19 08:55:51 +00:00
|
|
|
AND withdrawn_in_utxo IS NULL
|
2024-10-21 15:13:16 +02:00
|
|
|
)
|
|
|
|
|
GROUP BY phe.pool
|
|
|
|
|
HAVING MAX({ts})
|
|
|
|
|
|
|
|
|
|
UNION ALL
|
|
|
|
|
|
|
|
|
|
SELECT phe.pool, phe.sats, phe.token_amount, {ts} AS ts, phe.sequence
|
|
|
|
|
FROM pool_history_entry phe
|
|
|
|
|
WHERE {ts} > ?1
|
|
|
|
|
AND phe.pool IN (
|
|
|
|
|
SELECT creation_utxo
|
|
|
|
|
FROM pool
|
|
|
|
|
WHERE {extra_filters}
|
2026-03-19 08:55:51 +00:00
|
|
|
AND withdrawn_in_utxo IS NULL
|
2024-10-21 15:13:16 +02:00
|
|
|
)
|
|
|
|
|
GROUP BY phe.pool
|
|
|
|
|
HAVING MIN({ts})
|
|
|
|
|
)
|
|
|
|
|
SELECT * FROM NearestEntries
|
|
|
|
|
ORDER BY {preferred}",
|
|
|
|
|
);
|
|
|
|
|
|
2026-02-17 17:47:43 +01:00
|
|
|
let rows = match (&token_blob, &owner_pkh_blob) {
|
|
|
|
|
(Some(t), Some(p)) => {
|
|
|
|
|
sqlx::query(&query)
|
|
|
|
|
.bind(timestamp)
|
|
|
|
|
.bind(t)
|
|
|
|
|
.bind(p)
|
|
|
|
|
.fetch_all(pool)
|
|
|
|
|
.await?
|
|
|
|
|
}
|
|
|
|
|
(Some(t), None) => {
|
|
|
|
|
sqlx::query(&query)
|
|
|
|
|
.bind(timestamp)
|
|
|
|
|
.bind(t)
|
|
|
|
|
.fetch_all(pool)
|
|
|
|
|
.await?
|
|
|
|
|
}
|
|
|
|
|
(None, Some(p)) => {
|
|
|
|
|
sqlx::query(&query)
|
|
|
|
|
.bind(timestamp)
|
|
|
|
|
.bind(p)
|
|
|
|
|
.fetch_all(pool)
|
|
|
|
|
.await?
|
|
|
|
|
}
|
|
|
|
|
(None, None) => sqlx::query(&query).bind(timestamp).fetch_all(pool).await?,
|
2024-10-21 15:13:16 +02:00
|
|
|
};
|
|
|
|
|
let mut pools: HashMap<String, PoolSnapshot> = HashMap::default();
|
|
|
|
|
|
2026-02-17 17:47:43 +01:00
|
|
|
for row in rows {
|
|
|
|
|
let pool_blob: Vec<u8> = row.get(0);
|
|
|
|
|
let pool_id = blob_to_display_hex::<PoolID>(&pool_blob)?;
|
|
|
|
|
let sats: i64 = row.get(1);
|
|
|
|
|
let token_amount: i64 = row.get(2);
|
|
|
|
|
let ts_val: i64 = row.get(3);
|
|
|
|
|
|
|
|
|
|
let pool_snapshot = PoolSnapshot {
|
|
|
|
|
pool_id: pool_id.clone(),
|
|
|
|
|
sats: sats as u64,
|
|
|
|
|
token_amount: token_amount as u64,
|
|
|
|
|
timestamp: ts_val as u64,
|
|
|
|
|
};
|
|
|
|
|
|
2024-10-21 15:13:16 +02:00
|
|
|
if !pools.contains_key(&pool_snapshot.pool_id) {
|
2026-02-17 17:47:43 +01:00
|
|
|
pools.insert(pool_snapshot.pool_id.clone(), pool_snapshot);
|
2024-10-21 15:13:16 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(pools)
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-11 09:31:21 +00:00
|
|
|
/// A capital injection event: both-side-positive delta on a pool_history_entry row.
|
|
|
|
|
/// Carries the actual pool state immediately before and after the injection so callers
|
|
|
|
|
/// can split a pool period into clean sub-periods without synthetic values.
|
|
|
|
|
pub struct InjectionRecord {
|
|
|
|
|
pub sats_before: u64,
|
|
|
|
|
pub tokens_before: u64,
|
|
|
|
|
pub sats_after: u64,
|
|
|
|
|
pub tokens_after: u64,
|
|
|
|
|
pub timestamp: u64,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Returns injection events (both-side-positive deltas) for a set of pools within a time window.
|
|
|
|
|
/// An injection is a pool state where both sats_delta >= 0 and token_delta >= 0, meaning
|
|
|
|
|
/// capital was added rather than a normal trade occurring.
|
|
|
|
|
/// Callers use the k_before/k_after ratio to neutralize the K jump in APY calculations.
|
|
|
|
|
pub async fn get_injections_between(
|
|
|
|
|
pool: &SqlitePool,
|
|
|
|
|
pool_id_blobs: &[Vec<u8>],
|
|
|
|
|
min_start_ts: i64,
|
|
|
|
|
end_ts: i64,
|
|
|
|
|
) -> Result<HashMap<String, Vec<InjectionRecord>>> {
|
|
|
|
|
if pool_id_blobs.is_empty() {
|
|
|
|
|
return Ok(HashMap::new());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let placeholders = (1..=pool_id_blobs.len())
|
|
|
|
|
.map(|i| format!("?{i}"))
|
|
|
|
|
.collect::<Vec<_>>()
|
|
|
|
|
.join(",");
|
|
|
|
|
let n = pool_id_blobs.len();
|
|
|
|
|
let query = format!(
|
|
|
|
|
"SELECT pool, sats, token_amount, sats_delta, token_delta, effective_timestamp
|
|
|
|
|
FROM pool_history_entry
|
|
|
|
|
WHERE pool IN ({placeholders})
|
|
|
|
|
AND effective_timestamp > ?{start_bind}
|
|
|
|
|
AND effective_timestamp <= ?{end_bind}
|
|
|
|
|
AND sats_delta >= 0 AND token_delta >= 0
|
|
|
|
|
AND (sats_delta > 0 OR token_delta > 0)",
|
|
|
|
|
start_bind = n + 1,
|
|
|
|
|
end_bind = n + 2,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let mut query_builder = sqlx::query(&query);
|
|
|
|
|
for blob in pool_id_blobs {
|
|
|
|
|
query_builder = query_builder.bind(blob.clone());
|
|
|
|
|
}
|
|
|
|
|
query_builder = query_builder.bind(min_start_ts).bind(end_ts);
|
|
|
|
|
|
|
|
|
|
let rows = query_builder.fetch_all(pool).await?;
|
|
|
|
|
let mut result: HashMap<String, Vec<InjectionRecord>> = HashMap::new();
|
|
|
|
|
|
|
|
|
|
for row in rows {
|
|
|
|
|
let pool_blob: Vec<u8> = row.get(0);
|
|
|
|
|
let pool_id = blob_to_display_hex::<PoolID>(&pool_blob)?;
|
|
|
|
|
let sats: i64 = row.get(1);
|
|
|
|
|
let token_amount: i64 = row.get(2);
|
|
|
|
|
let sats_delta: i64 = row.get(3);
|
|
|
|
|
let token_delta: i64 = row.get(4);
|
|
|
|
|
let ts: i64 = row.get(5);
|
|
|
|
|
|
|
|
|
|
result.entry(pool_id).or_default().push(InjectionRecord {
|
|
|
|
|
sats_before: (sats - sats_delta).max(0) as u64,
|
|
|
|
|
tokens_before: (token_amount - token_delta).max(0) as u64,
|
|
|
|
|
sats_after: sats.max(0) as u64,
|
|
|
|
|
tokens_after: token_amount.max(0) as u64,
|
|
|
|
|
timestamp: ts as u64,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(result)
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-06 12:08:15 +00:00
|
|
|
/// Returns pool period snapshots filtered by token and/or owner PKH.
|
2026-02-17 17:47:43 +01:00
|
|
|
pub async fn get_pool_period_snapshot(
|
|
|
|
|
pool: &SqlitePool,
|
2024-10-21 15:13:16 +02:00
|
|
|
token_id: Option<&str>,
|
|
|
|
|
owner_pkh: Option<&str>,
|
|
|
|
|
start: i64,
|
|
|
|
|
end: i64,
|
|
|
|
|
) -> Result<Vec<(PoolSnapshot, PoolSnapshot)>> {
|
|
|
|
|
let pools_start = get_nearest_entries(
|
2026-02-17 17:47:43 +01:00
|
|
|
pool,
|
2024-10-21 15:13:16 +02:00
|
|
|
start,
|
|
|
|
|
token_id,
|
|
|
|
|
owner_pkh,
|
|
|
|
|
SnapshotSelection::UseBefore,
|
2026-02-17 17:47:43 +01:00
|
|
|
)
|
|
|
|
|
.await?;
|
2024-10-21 15:13:16 +02:00
|
|
|
let mut pools_end =
|
2026-02-17 17:47:43 +01:00
|
|
|
get_nearest_entries(pool, end, token_id, owner_pkh, SnapshotSelection::UseAfter).await?;
|
2024-10-21 15:13:16 +02:00
|
|
|
|
2026-03-19 08:55:51 +00:00
|
|
|
let window_end = end as u64;
|
2024-10-21 15:13:16 +02:00
|
|
|
let mut pools: Vec<(PoolSnapshot, PoolSnapshot)> = Vec::default();
|
|
|
|
|
for (start_pool_id, start_pool) in pools_start {
|
2026-03-19 08:55:51 +00:00
|
|
|
let mut end_pool = match pools_end.remove(&start_pool_id) {
|
2024-10-21 15:13:16 +02:00
|
|
|
Some(end) => end,
|
|
|
|
|
None => {
|
2025-07-16 09:31:14 +02:00
|
|
|
warn!("Found no 'end pool' for {start_pool_id}");
|
2024-10-21 15:13:16 +02:00
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-19 08:55:51 +00:00
|
|
|
// Clamp the end snapshot timestamp to the window boundary.
|
|
|
|
|
// For dormant pools, the nearest entry before `end` may have a timestamp far
|
|
|
|
|
// in the past (e.g. the pool's only trade was hours after creation). Without
|
|
|
|
|
// clamping, yield would be annualised over that short active window rather than
|
|
|
|
|
// the full dormancy period, grossly inflating the reported APY.
|
|
|
|
|
//
|
|
|
|
|
// Guard: only clamp when the end snapshot is actually a newer state than the start.
|
|
|
|
|
// If both snapshots point to the same pre-window trade (pool had no activity
|
|
|
|
|
// during the window), their timestamps are equal and duration stays zero →
|
|
|
|
|
// the pool is correctly excluded. Without this guard, clamping would give those
|
|
|
|
|
// pools a non-zero duration and a zero yield, diluting the AAPY of active tokens.
|
|
|
|
|
if end_pool.timestamp < window_end && end_pool.timestamp > start_pool.timestamp {
|
|
|
|
|
end_pool.timestamp = window_end;
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-21 15:13:16 +02:00
|
|
|
let duration = end_pool.timestamp.saturating_sub(start_pool.timestamp);
|
|
|
|
|
if !duration.is_zero() {
|
|
|
|
|
pools.push((start_pool, end_pool))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !pools_end.is_empty() {
|
|
|
|
|
warn!("Found {} end pools not in start pools", pools_end.len());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(pools)
|
|
|
|
|
}
|
2024-11-13 11:16:24 +01:00
|
|
|
|
2026-03-06 12:08:15 +00:00
|
|
|
/// Get the nearest pool history entries for a specific set of pool IDs.
|
|
|
|
|
async fn get_nearest_entries_by_pool_ids(
|
|
|
|
|
pool: &SqlitePool,
|
|
|
|
|
timestamp: i64,
|
|
|
|
|
pool_id_blobs: &[Vec<u8>],
|
|
|
|
|
resolution: SnapshotSelection,
|
|
|
|
|
) -> Result<HashMap<String, PoolSnapshot>> {
|
|
|
|
|
let placeholders: String = (2..=(pool_id_blobs.len() + 1))
|
|
|
|
|
.map(|i| format!("?{i}"))
|
|
|
|
|
.collect::<Vec<_>>()
|
|
|
|
|
.join(",");
|
|
|
|
|
let pool_filter = format!("phe.pool IN ({placeholders})");
|
|
|
|
|
|
|
|
|
|
let ts = "effective_timestamp";
|
|
|
|
|
let preferred = match resolution {
|
|
|
|
|
SnapshotSelection::UseBefore => "ts ASC",
|
|
|
|
|
SnapshotSelection::UseAfter => "ts DESC",
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let query = format!(
|
|
|
|
|
"WITH NearestEntries AS (
|
|
|
|
|
SELECT phe.pool, phe.sats, phe.token_amount, {ts} AS ts, phe.sequence
|
|
|
|
|
FROM pool_history_entry phe
|
|
|
|
|
WHERE {ts} <= ?1 AND {pool_filter}
|
|
|
|
|
GROUP BY phe.pool
|
|
|
|
|
HAVING MAX({ts})
|
|
|
|
|
|
|
|
|
|
UNION ALL
|
|
|
|
|
|
|
|
|
|
SELECT phe.pool, phe.sats, phe.token_amount, {ts} AS ts, phe.sequence
|
|
|
|
|
FROM pool_history_entry phe
|
|
|
|
|
WHERE {ts} > ?1 AND {pool_filter}
|
|
|
|
|
GROUP BY phe.pool
|
|
|
|
|
HAVING MIN({ts})
|
|
|
|
|
)
|
|
|
|
|
SELECT * FROM NearestEntries
|
|
|
|
|
ORDER BY {preferred}",
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let mut query_builder = sqlx::query(&query).bind(timestamp);
|
|
|
|
|
for blob in pool_id_blobs {
|
|
|
|
|
query_builder = query_builder.bind(blob.clone());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let rows = query_builder.fetch_all(pool).await?;
|
|
|
|
|
let mut pools: HashMap<String, PoolSnapshot> = HashMap::default();
|
|
|
|
|
|
|
|
|
|
for row in rows {
|
|
|
|
|
let pool_blob: Vec<u8> = row.get(0);
|
|
|
|
|
let pool_id = blob_to_display_hex::<PoolID>(&pool_blob)?;
|
|
|
|
|
let sats: i64 = row.get(1);
|
|
|
|
|
let token_amount: i64 = row.get(2);
|
|
|
|
|
let ts_val: i64 = row.get(3);
|
|
|
|
|
|
|
|
|
|
let pool_snapshot = PoolSnapshot {
|
|
|
|
|
pool_id: pool_id.clone(),
|
|
|
|
|
sats: sats as u64,
|
|
|
|
|
token_amount: token_amount as u64,
|
|
|
|
|
timestamp: ts_val as u64,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if !pools.contains_key(&pool_snapshot.pool_id) {
|
|
|
|
|
pools.insert(pool_snapshot.pool_id.clone(), pool_snapshot);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(pools)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Returns pool period snapshots for a specific list of pool IDs.
|
|
|
|
|
/// Use this instead of `get_pool_period_snapshot` when filtering by pool ID —
|
|
|
|
|
/// it avoids a join to the `pool` table and works across future contract types.
|
|
|
|
|
pub async fn get_pool_period_snapshot_by_pool_ids(
|
|
|
|
|
pool: &SqlitePool,
|
|
|
|
|
pool_ids: &[String],
|
|
|
|
|
start: i64,
|
|
|
|
|
end: i64,
|
|
|
|
|
) -> Result<Vec<(PoolSnapshot, PoolSnapshot)>> {
|
|
|
|
|
let pool_id_blobs: Result<Vec<Vec<u8>>> = pool_ids
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|id| display_hex_to_blob::<PoolID>(id))
|
|
|
|
|
.collect();
|
|
|
|
|
let pool_id_blobs = pool_id_blobs?;
|
|
|
|
|
|
|
|
|
|
let pools_start =
|
|
|
|
|
get_nearest_entries_by_pool_ids(pool, start, &pool_id_blobs, SnapshotSelection::UseBefore)
|
|
|
|
|
.await?;
|
|
|
|
|
let mut pools_end =
|
|
|
|
|
get_nearest_entries_by_pool_ids(pool, end, &pool_id_blobs, SnapshotSelection::UseAfter)
|
|
|
|
|
.await?;
|
|
|
|
|
|
2026-03-19 08:55:51 +00:00
|
|
|
let window_end = end as u64;
|
2026-03-06 12:08:15 +00:00
|
|
|
let mut pools: Vec<(PoolSnapshot, PoolSnapshot)> = Vec::default();
|
|
|
|
|
for (start_pool_id, start_pool) in pools_start {
|
2026-03-19 08:55:51 +00:00
|
|
|
let mut end_pool = match pools_end.remove(&start_pool_id) {
|
2026-03-06 12:08:15 +00:00
|
|
|
Some(end) => end,
|
|
|
|
|
None => {
|
|
|
|
|
warn!("Found no 'end pool' for {start_pool_id}");
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-19 08:55:51 +00:00
|
|
|
// Clamp end snapshot to window boundary — see comment in get_pool_period_snapshot.
|
|
|
|
|
if end_pool.timestamp < window_end && end_pool.timestamp > start_pool.timestamp {
|
|
|
|
|
end_pool.timestamp = window_end;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-06 12:08:15 +00:00
|
|
|
let duration = end_pool.timestamp.saturating_sub(start_pool.timestamp);
|
|
|
|
|
if !duration.is_zero() {
|
|
|
|
|
pools.push((start_pool, end_pool))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !pools_end.is_empty() {
|
|
|
|
|
warn!("Found {} end pools not in start pools", pools_end.len());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(pools)
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-13 11:16:24 +01:00
|
|
|
#[derive(Serialize)]
|
|
|
|
|
pub struct PoolHistoryEntry {
|
|
|
|
|
txid: String,
|
|
|
|
|
sats: u64,
|
|
|
|
|
token_amount: u64,
|
|
|
|
|
timestamp: u64,
|
2025-07-16 09:30:17 +02:00
|
|
|
#[serde(serialize_with = "serialize_integer_as_string")]
|
|
|
|
|
k: Integer,
|
2024-11-13 11:16:24 +01:00
|
|
|
}
|
|
|
|
|
|
2026-02-17 17:47:43 +01:00
|
|
|
async fn get_pool_history_entry(
|
|
|
|
|
conn: &mut SqliteConnection,
|
|
|
|
|
utxo_hash: OutPointHash,
|
|
|
|
|
) -> Result<PoolHistoryEntry> {
|
|
|
|
|
let row = sqlx::query(
|
|
|
|
|
"SELECT txid, sats, token_amount, effective_timestamp as timestamp FROM pool_history_entry WHERE utxo = ?",
|
|
|
|
|
)
|
|
|
|
|
.bind(utxo_hash.to_blob())
|
|
|
|
|
.fetch_optional(&mut *conn)
|
|
|
|
|
.await?
|
|
|
|
|
.context("no pool history entry found for UTXO hash")?;
|
|
|
|
|
|
|
|
|
|
let txid_blob: Vec<u8> = row.get(0);
|
2026-02-01 13:23:57 +01:00
|
|
|
let txid = blob_to_display_hex::<bitcoincash::Txid>(&txid_blob)?;
|
2026-02-17 17:47:43 +01:00
|
|
|
let sats: i64 = row.get(1);
|
|
|
|
|
let token_amount: i64 = row.get(2);
|
|
|
|
|
let timestamp: i64 = row.get(3);
|
|
|
|
|
let sats = sats as u64;
|
|
|
|
|
let token_amount = token_amount as u64;
|
2025-08-15 17:48:25 +02:00
|
|
|
Ok(PoolHistoryEntry {
|
2026-02-01 13:23:57 +01:00
|
|
|
txid,
|
2025-08-15 17:48:25 +02:00
|
|
|
sats,
|
|
|
|
|
token_amount,
|
2026-02-17 17:47:43 +01:00
|
|
|
timestamp: timestamp as u64,
|
2025-08-15 17:48:25 +02:00
|
|
|
k: Integer::from(sats) * Integer::from(token_amount),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-17 17:47:43 +01:00
|
|
|
pub async fn db_pool_history(
|
|
|
|
|
pool: &SqlitePool,
|
|
|
|
|
pool_id: &PoolID,
|
2024-11-13 11:16:24 +01:00
|
|
|
start_time: u64,
|
|
|
|
|
) -> Result<Vec<PoolHistoryEntry>> {
|
|
|
|
|
let query = "SELECT
|
2026-02-01 13:23:57 +01:00
|
|
|
phe.txid,
|
2024-11-13 11:16:24 +01:00
|
|
|
phe.sats,
|
|
|
|
|
phe.token_amount,
|
2025-08-18 22:30:10 +02:00
|
|
|
phe.effective_timestamp as timestamp
|
2024-11-13 11:16:24 +01:00
|
|
|
FROM
|
|
|
|
|
pool_history_entry phe
|
|
|
|
|
WHERE
|
|
|
|
|
phe.pool = ?1
|
|
|
|
|
AND timestamp >= ?2
|
|
|
|
|
ORDER BY
|
|
|
|
|
phe.sequence ASC;
|
|
|
|
|
";
|
|
|
|
|
|
2026-02-17 17:47:43 +01:00
|
|
|
let rows = sqlx::query(query)
|
|
|
|
|
.bind(pool_id.to_blob())
|
|
|
|
|
.bind(start_time as i64)
|
|
|
|
|
.fetch_all(pool)
|
|
|
|
|
.await?;
|
2024-11-13 11:16:24 +01:00
|
|
|
|
2026-02-01 13:23:57 +01:00
|
|
|
let mut history: Vec<PoolHistoryEntry> = Vec::default();
|
|
|
|
|
|
2026-02-17 17:47:43 +01:00
|
|
|
for row in rows {
|
|
|
|
|
let txid_blob: Vec<u8> = row.get(0);
|
2026-02-01 13:23:57 +01:00
|
|
|
let txid = blob_to_display_hex::<bitcoincash::Txid>(&txid_blob)?;
|
2026-02-17 17:47:43 +01:00
|
|
|
let sats: i64 = row.get(1);
|
|
|
|
|
let token_amount: i64 = row.get(2);
|
|
|
|
|
let timestamp: i64 = row.get(3);
|
|
|
|
|
let sats = sats as u64;
|
|
|
|
|
let token_amount = token_amount as u64;
|
2024-11-13 11:16:24 +01:00
|
|
|
|
2025-07-16 09:30:17 +02:00
|
|
|
let k = Integer::from(sats) * Integer::from(token_amount);
|
2024-11-13 11:16:24 +01:00
|
|
|
|
2026-02-01 13:23:57 +01:00
|
|
|
history.push(PoolHistoryEntry {
|
|
|
|
|
txid,
|
2024-11-13 11:16:24 +01:00
|
|
|
sats,
|
|
|
|
|
token_amount,
|
2026-02-17 17:47:43 +01:00
|
|
|
timestamp: timestamp as u64,
|
2024-11-13 11:16:24 +01:00
|
|
|
k,
|
2026-02-01 13:23:57 +01:00
|
|
|
});
|
2024-11-13 11:16:24 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(history)
|
|
|
|
|
}
|
2024-11-13 13:24:06 +01:00
|
|
|
|
2026-02-17 17:47:43 +01:00
|
|
|
pub async fn db_pool_get_details(pool: &SqlitePool, pool_id: &PoolID) -> Result<(String, String)> {
|
|
|
|
|
let row: (Vec<u8>, Vec<u8>) =
|
|
|
|
|
sqlx::query_as("SELECT token_id, owner_pkh FROM pool WHERE creation_utxo = ?1")
|
|
|
|
|
.bind(pool_id.to_blob())
|
|
|
|
|
.fetch_one(pool)
|
|
|
|
|
.await?;
|
|
|
|
|
let token_hex = blob_to_display_hex::<TokenID>(&row.0)?;
|
|
|
|
|
let owner_pkh_hex = hex::encode(&row.1);
|
2026-02-01 13:23:57 +01:00
|
|
|
Ok((token_hex, owner_pkh_hex))
|
2024-11-13 13:24:06 +01:00
|
|
|
}
|
2025-08-15 22:22:07 +02:00
|
|
|
|
2026-02-17 17:47:43 +01:00
|
|
|
pub async fn db_pool_id_from_utxo(
|
|
|
|
|
pool: &SqlitePool,
|
|
|
|
|
utxo_hash: &OutPointHash,
|
|
|
|
|
) -> Result<Option<String>> {
|
|
|
|
|
let row: Option<(Vec<u8>,)> =
|
|
|
|
|
sqlx::query_as("SELECT pool FROM pool_history_entry WHERE utxo = ?")
|
|
|
|
|
.bind(utxo_hash.to_blob())
|
|
|
|
|
.fetch_optional(pool)
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
match row {
|
|
|
|
|
Some((pool_blob,)) => {
|
|
|
|
|
let pool_id = blob_to_display_hex::<PoolID>(&pool_blob)?;
|
|
|
|
|
Ok(Some(pool_id))
|
|
|
|
|
}
|
|
|
|
|
None => Ok(None),
|
2026-01-09 15:28:36 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-15 22:22:07 +02:00
|
|
|
/// Get total volume in satoshis across all tokens for a given time period
|
2026-02-17 17:47:43 +01:00
|
|
|
pub async fn get_total_volume_sats(
|
|
|
|
|
pool: &SqlitePool,
|
2025-08-15 22:22:07 +02:00
|
|
|
start_timestamp: u64,
|
|
|
|
|
end_timestamp: u64,
|
|
|
|
|
) -> anyhow::Result<i64> {
|
|
|
|
|
let sql = "
|
|
|
|
|
SELECT
|
2025-08-19 11:38:05 +02:00
|
|
|
COALESCE(SUM(ABS(phe.sats_delta)), 0) AS total_volume_sats
|
2025-08-15 22:22:07 +02:00
|
|
|
FROM pool_history_entry phe
|
|
|
|
|
JOIN pool p ON phe.pool = p.creation_utxo
|
|
|
|
|
JOIN tx ON phe.txid = tx.txid
|
2025-08-18 22:30:10 +02:00
|
|
|
WHERE tx.effective_timestamp BETWEEN ? AND ?";
|
2025-08-15 22:22:07 +02:00
|
|
|
|
2026-02-17 17:47:43 +01:00
|
|
|
let row: (i64,) = sqlx::query_as(sql)
|
|
|
|
|
.bind(start_timestamp as i64)
|
|
|
|
|
.bind(end_timestamp as i64)
|
|
|
|
|
.fetch_one(pool)
|
|
|
|
|
.await?;
|
2025-08-15 22:22:07 +02:00
|
|
|
|
2026-02-17 17:47:43 +01:00
|
|
|
Ok(row.0)
|
2025-08-15 22:22:07 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Get volume in both satoshis and tokens for a specific token for a given time period
|
2026-02-17 17:47:43 +01:00
|
|
|
pub async fn get_token_volume_sats(
|
|
|
|
|
pool: &SqlitePool,
|
2025-08-15 22:22:07 +02:00
|
|
|
start_timestamp: u64,
|
|
|
|
|
end_timestamp: u64,
|
|
|
|
|
token_id: &str,
|
|
|
|
|
) -> anyhow::Result<(i64, i64)> {
|
2026-02-17 17:47:43 +01:00
|
|
|
// CROSS JOIN forces tx-first join order: filter by timestamp (few rows),
|
|
|
|
|
// then look up phe by txid, then filter pool by token_id.
|
|
|
|
|
// Without CROSS JOIN, the planner starts from pool→phe (924K rows) → tx.
|
2025-08-15 22:22:07 +02:00
|
|
|
let sql = "
|
|
|
|
|
SELECT
|
2025-08-19 11:38:05 +02:00
|
|
|
COALESCE(SUM(ABS(phe.sats_delta)), 0) AS token_volume_sats,
|
|
|
|
|
COALESCE(SUM(ABS(phe.token_delta)), 0) AS token_volume_tokens
|
2025-09-29 20:32:46 +02:00
|
|
|
FROM tx
|
2026-02-17 17:47:43 +01:00
|
|
|
CROSS JOIN pool_history_entry phe ON tx.txid = phe.txid
|
2025-08-15 22:22:07 +02:00
|
|
|
JOIN pool p ON phe.pool = p.creation_utxo
|
2025-08-18 22:30:10 +02:00
|
|
|
WHERE tx.effective_timestamp BETWEEN ? AND ?
|
2025-08-15 22:22:07 +02:00
|
|
|
AND p.token_id = ?";
|
2026-02-01 13:23:57 +01:00
|
|
|
let token_blob = display_hex_to_blob::<TokenID>(token_id)?;
|
2026-02-17 17:47:43 +01:00
|
|
|
let row: (i64, i64) = sqlx::query_as(sql)
|
|
|
|
|
.bind(start_timestamp as i64)
|
|
|
|
|
.bind(end_timestamp as i64)
|
|
|
|
|
.bind(token_blob)
|
|
|
|
|
.fetch_one(pool)
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
Ok(row)
|
2025-08-15 22:22:07 +02:00
|
|
|
}
|
2025-08-19 11:38:05 +02:00
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
use crate::db::cauldron::prepare_tables;
|
2026-02-05 10:18:06 +01:00
|
|
|
use crate::db::cauldron::tx::{insert_block_tx, insert_mempool_tx};
|
|
|
|
|
use crate::db::cauldron::utxo_funding::insert_utxo_funding;
|
|
|
|
|
use bitcoin_hashes::hex::ToHex;
|
|
|
|
|
use bitcoin_hashes::Hash;
|
|
|
|
|
use bitcoincash::{BlockHash, PubkeyHash, Txid};
|
|
|
|
|
use riftenlabs_defi::cauldron::ParsedContract;
|
2026-02-17 17:47:43 +01:00
|
|
|
use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
|
|
|
|
|
use std::sync::atomic::{AtomicU64, Ordering};
|
|
|
|
|
|
|
|
|
|
static POOL_TEST_COUNTER: AtomicU64 = AtomicU64::new(0);
|
|
|
|
|
|
|
|
|
|
async fn test_pool() -> SqlitePool {
|
|
|
|
|
let id = POOL_TEST_COUNTER.fetch_add(1, Ordering::SeqCst);
|
|
|
|
|
let uri = format!("file:pool_test_{}?mode=memory&cache=shared", id);
|
|
|
|
|
let opts = SqliteConnectOptions::new()
|
|
|
|
|
.filename(&uri)
|
|
|
|
|
.foreign_keys(false);
|
|
|
|
|
SqlitePoolOptions::new().connect_with(opts).await.unwrap()
|
|
|
|
|
}
|
2025-08-19 11:38:05 +02:00
|
|
|
|
2026-02-17 17:47:43 +01:00
|
|
|
async fn setup_test_db(pool: &SqlitePool) {
|
|
|
|
|
prepare_tables(pool).await;
|
2026-02-05 10:18:06 +01:00
|
|
|
dummy_init_seq();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Helper to insert a simple pool with two history entries for testing get_pool_period_snapshot
|
|
|
|
|
/// Inserts entries at t0 and t1 so the period has non-zero duration
|
2026-02-17 17:47:43 +01:00
|
|
|
async fn seed_test_pool(
|
|
|
|
|
pool: &SqlitePool,
|
2026-02-05 10:18:06 +01:00
|
|
|
token: TokenID,
|
|
|
|
|
owner_pkh: PubkeyHash,
|
|
|
|
|
t0: i64,
|
|
|
|
|
t1: i64,
|
|
|
|
|
sats: u64,
|
|
|
|
|
token_amount: i64,
|
|
|
|
|
) -> OutPointHash {
|
2026-03-06 12:08:15 +00:00
|
|
|
// Generate unique hashes based on owner_pkh to avoid PK conflicts in DB
|
|
|
|
|
let mut seed = [0u8; 32];
|
|
|
|
|
|
|
|
|
|
// Use the hex representation to seed the buffer if into_inner() is missing
|
|
|
|
|
// This ensures a unique, deterministic seed for each owner in the test
|
|
|
|
|
let pkh_hex = owner_pkh.to_hex();
|
|
|
|
|
let pkh_bytes = hex::decode(pkh_hex).unwrap();
|
|
|
|
|
seed[0..20].copy_from_slice(&pkh_bytes);
|
|
|
|
|
|
|
|
|
|
let pool_utxo_0 = OutPointHash::from_inner(seed);
|
|
|
|
|
let mut seed_tx = seed;
|
|
|
|
|
seed_tx[31] = 0xFF; // slightly different for txid
|
|
|
|
|
let txid0 = Txid::from_inner(seed_tx);
|
|
|
|
|
|
|
|
|
|
let txid1 = Txid::from_inner(seed); // different enough for test
|
2026-02-05 10:18:06 +01:00
|
|
|
|
|
|
|
|
let contract0 = ParsedContract {
|
|
|
|
|
pkh: owner_pkh,
|
|
|
|
|
is_withdrawn: false,
|
|
|
|
|
spent_utxo_hash: OutPointHash::all_zeros(),
|
|
|
|
|
new_utxo_hash: Some(pool_utxo_0),
|
|
|
|
|
new_utxo_txid: Some(txid0),
|
|
|
|
|
new_utxo_n: Some(0),
|
|
|
|
|
token_id: Some(token),
|
|
|
|
|
sats: Some(sats),
|
|
|
|
|
token_amount: Some(token_amount),
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-17 17:47:43 +01:00
|
|
|
let mut conn = pool.acquire().await.unwrap();
|
|
|
|
|
insert_new_pool(&mut conn, &contract0).await.unwrap();
|
|
|
|
|
insert_utxo_funding(&mut conn, &vec![contract0.clone()], &txid0, true)
|
|
|
|
|
.await
|
|
|
|
|
.unwrap();
|
|
|
|
|
insert_block_tx(&mut conn, &txid0, &BlockHash::all_zeros(), t0)
|
|
|
|
|
.await
|
|
|
|
|
.unwrap();
|
|
|
|
|
insert_mempool_tx(&mut conn, &txid0, t0 as u64)
|
|
|
|
|
.await
|
|
|
|
|
.unwrap();
|
2026-02-05 10:18:06 +01:00
|
|
|
insert_pool_history_entry(
|
2026-02-17 17:47:43 +01:00
|
|
|
&mut conn,
|
2026-02-05 10:18:06 +01:00
|
|
|
&pool_utxo_0,
|
|
|
|
|
&contract0,
|
|
|
|
|
Some(t0 as u64),
|
|
|
|
|
Some(t0 as u64),
|
|
|
|
|
0,
|
|
|
|
|
0,
|
|
|
|
|
)
|
2026-02-17 17:47:43 +01:00
|
|
|
.await
|
2026-02-05 10:18:06 +01:00
|
|
|
.unwrap();
|
|
|
|
|
|
2026-03-06 12:08:15 +00:00
|
|
|
// Use a different UTXO for the second entry in the history to satisfy DB constraints
|
|
|
|
|
let pool_utxo_1 = OutPointHash::from_inner(seed_tx);
|
2026-02-05 10:18:06 +01:00
|
|
|
let contract1 = ParsedContract {
|
|
|
|
|
pkh: owner_pkh,
|
|
|
|
|
is_withdrawn: false,
|
|
|
|
|
spent_utxo_hash: pool_utxo_0,
|
|
|
|
|
new_utxo_hash: Some(pool_utxo_1),
|
|
|
|
|
new_utxo_txid: Some(txid1),
|
|
|
|
|
new_utxo_n: Some(0),
|
|
|
|
|
token_id: Some(token),
|
2026-02-17 17:47:43 +01:00
|
|
|
sats: Some(sats + 100),
|
2026-02-05 10:18:06 +01:00
|
|
|
token_amount: Some(token_amount - 10),
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-17 17:47:43 +01:00
|
|
|
insert_utxo_funding(&mut conn, &vec![contract1.clone()], &txid1, true)
|
|
|
|
|
.await
|
|
|
|
|
.unwrap();
|
|
|
|
|
insert_block_tx(&mut conn, &txid1, &BlockHash::all_zeros(), t1)
|
|
|
|
|
.await
|
|
|
|
|
.unwrap();
|
|
|
|
|
insert_mempool_tx(&mut conn, &txid1, t1 as u64)
|
|
|
|
|
.await
|
|
|
|
|
.unwrap();
|
2026-02-05 10:18:06 +01:00
|
|
|
insert_pool_history_entry(
|
2026-02-17 17:47:43 +01:00
|
|
|
&mut conn,
|
2026-03-06 12:08:15 +00:00
|
|
|
&pool_utxo_0, // Recording history for the original pool
|
2026-02-05 10:18:06 +01:00
|
|
|
&contract1,
|
|
|
|
|
Some(t1 as u64),
|
|
|
|
|
Some(t1 as u64),
|
|
|
|
|
100,
|
|
|
|
|
-10,
|
|
|
|
|
)
|
2026-02-17 17:47:43 +01:00
|
|
|
.await
|
2026-02-05 10:18:06 +01:00
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
pool_utxo_0
|
2025-08-19 11:38:05 +02:00
|
|
|
}
|
|
|
|
|
|
2026-02-17 17:47:43 +01:00
|
|
|
#[tokio::test]
|
|
|
|
|
async fn test_volume_no_trades_in_period() {
|
|
|
|
|
let pool = test_pool().await;
|
|
|
|
|
setup_test_db(&pool).await;
|
2025-08-19 11:38:05 +02:00
|
|
|
|
|
|
|
|
let start_timestamp = 1755508856u64;
|
|
|
|
|
let end_timestamp = 1755595256u64;
|
|
|
|
|
let token_id = "f6677f3d3805d70949b375d36e094ff0ec9ece2a2cb1fde6d8b0e90b368f1f63";
|
|
|
|
|
|
2026-02-17 17:47:43 +01:00
|
|
|
let result = get_token_volume_sats(&pool, start_timestamp, end_timestamp, token_id).await;
|
2025-08-19 11:38:05 +02:00
|
|
|
let (sats_volume, token_volume) = result.unwrap();
|
|
|
|
|
assert_eq!(sats_volume, 0);
|
|
|
|
|
assert_eq!(token_volume, 0);
|
|
|
|
|
|
2026-02-17 17:47:43 +01:00
|
|
|
let result = get_total_volume_sats(&pool, start_timestamp, end_timestamp).await;
|
2025-08-19 11:38:05 +02:00
|
|
|
let total_volume = result.unwrap();
|
|
|
|
|
assert_eq!(total_volume, 0);
|
|
|
|
|
}
|
2026-02-05 10:18:06 +01:00
|
|
|
|
2026-02-17 17:47:43 +01:00
|
|
|
#[tokio::test]
|
|
|
|
|
async fn test_get_pool_period_snapshot_with_token_filter() {
|
|
|
|
|
let db_pool = test_pool().await;
|
|
|
|
|
setup_test_db(&db_pool).await;
|
2026-02-05 10:18:06 +01:00
|
|
|
|
|
|
|
|
let token = TokenID::from_inner([0x01; 32]);
|
|
|
|
|
let owner = PubkeyHash::all_zeros();
|
|
|
|
|
let t0 = 1700000000i64;
|
2026-02-17 17:47:43 +01:00
|
|
|
let t1 = 1700001000i64;
|
2026-02-05 10:18:06 +01:00
|
|
|
|
2026-02-17 17:47:43 +01:00
|
|
|
seed_test_pool(&db_pool, token, owner, t0, t1, 1000, 500).await;
|
2026-02-05 10:18:06 +01:00
|
|
|
|
|
|
|
|
let token_hex = token.to_hex();
|
2026-02-17 17:47:43 +01:00
|
|
|
let result =
|
|
|
|
|
get_pool_period_snapshot(&db_pool, Some(&token_hex), None, t0 - 100, t1 + 100).await;
|
2026-02-05 10:18:06 +01:00
|
|
|
|
|
|
|
|
let pools = result.expect("query should not fail");
|
|
|
|
|
assert!(
|
|
|
|
|
!pools.is_empty(),
|
|
|
|
|
"token filter should return matching pools (got 0)"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-17 17:47:43 +01:00
|
|
|
#[tokio::test]
|
|
|
|
|
async fn test_get_pool_period_snapshot_with_pkh_filter() {
|
|
|
|
|
let db_pool = test_pool().await;
|
|
|
|
|
setup_test_db(&db_pool).await;
|
2026-02-05 10:18:06 +01:00
|
|
|
|
|
|
|
|
let token = TokenID::from_inner([0x02; 32]);
|
|
|
|
|
let owner = PubkeyHash::from_inner([0x11; 20]);
|
|
|
|
|
let t0 = 1700000000i64;
|
|
|
|
|
let t1 = 1700001000i64;
|
|
|
|
|
|
2026-02-17 17:47:43 +01:00
|
|
|
seed_test_pool(&db_pool, token, owner, t0, t1, 2000, 1000).await;
|
2026-02-05 10:18:06 +01:00
|
|
|
|
|
|
|
|
let pkh_hex = owner.to_hex();
|
2026-02-17 17:47:43 +01:00
|
|
|
let result =
|
|
|
|
|
get_pool_period_snapshot(&db_pool, None, Some(&pkh_hex), t0 - 100, t1 + 100).await;
|
2026-02-05 10:18:06 +01:00
|
|
|
|
|
|
|
|
let pools = result.expect("query should not fail");
|
|
|
|
|
assert!(
|
|
|
|
|
!pools.is_empty(),
|
|
|
|
|
"pkh filter should return matching pools (got 0)"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-06 12:08:15 +00:00
|
|
|
#[tokio::test]
|
|
|
|
|
async fn test_get_pool_period_snapshot_by_pool_ids() {
|
|
|
|
|
let db_pool = test_pool().await;
|
|
|
|
|
setup_test_db(&db_pool).await;
|
|
|
|
|
|
|
|
|
|
let token = TokenID::from_inner([0x02; 32]);
|
|
|
|
|
let t0 = 1700000000i64;
|
|
|
|
|
let t1 = 1700001000i64;
|
|
|
|
|
|
|
|
|
|
let owner_1 = PubkeyHash::from_inner([0x11; 20]);
|
|
|
|
|
let owner_2 = PubkeyHash::from_inner([0x22; 20]);
|
|
|
|
|
|
|
|
|
|
let pool_id_1 = seed_test_pool(&db_pool, token, owner_1, t0, t1, 2000, 1000).await;
|
|
|
|
|
let pool_id_2 = seed_test_pool(&db_pool, token, owner_2, t0, t1, 3000, 1500).await;
|
|
|
|
|
|
|
|
|
|
// Filter by a single pool ID
|
|
|
|
|
let result = get_pool_period_snapshot_by_pool_ids(
|
|
|
|
|
&db_pool,
|
|
|
|
|
&[pool_id_1.to_hex()],
|
|
|
|
|
t0 - 100,
|
|
|
|
|
t1 + 100,
|
|
|
|
|
)
|
|
|
|
|
.await;
|
|
|
|
|
let pools = result.expect("single pool_id query should not fail");
|
|
|
|
|
assert_eq!(pools.len(), 1, "Should return exactly 1 pool");
|
|
|
|
|
|
|
|
|
|
// Filter by both pool IDs
|
|
|
|
|
let result = get_pool_period_snapshot_by_pool_ids(
|
|
|
|
|
&db_pool,
|
|
|
|
|
&[pool_id_1.to_hex(), pool_id_2.to_hex()],
|
|
|
|
|
t0 - 100,
|
|
|
|
|
t1 + 100,
|
|
|
|
|
)
|
|
|
|
|
.await;
|
|
|
|
|
let pools = result.expect("multi pool_id query should not fail");
|
|
|
|
|
assert_eq!(pools.len(), 2, "Should return 2 pools for both IDs");
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-17 17:47:43 +01:00
|
|
|
#[tokio::test]
|
|
|
|
|
async fn test_get_pool_period_snapshot_pool_id_format() {
|
|
|
|
|
let db_pool = test_pool().await;
|
|
|
|
|
setup_test_db(&db_pool).await;
|
2026-02-05 10:18:06 +01:00
|
|
|
|
|
|
|
|
let token = TokenID::from_inner([0x03; 32]);
|
|
|
|
|
let owner = PubkeyHash::all_zeros();
|
|
|
|
|
let t0 = 1700000000i64;
|
|
|
|
|
let t1 = 1700001000i64;
|
|
|
|
|
|
2026-02-17 17:47:43 +01:00
|
|
|
let expected_pool_id = seed_test_pool(&db_pool, token, owner, t0, t1, 3000, 1500).await;
|
2026-02-05 10:18:06 +01:00
|
|
|
|
2026-02-17 17:47:43 +01:00
|
|
|
let result = get_pool_period_snapshot(&db_pool, None, None, t0 - 100, t1 + 100).await;
|
2026-02-05 10:18:06 +01:00
|
|
|
|
|
|
|
|
let pools = result.expect("query should not fail");
|
|
|
|
|
assert!(!pools.is_empty(), "should return at least one pool");
|
|
|
|
|
|
|
|
|
|
let (start_pool, _end_pool) = &pools[0];
|
|
|
|
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
start_pool.pool_id.len(),
|
|
|
|
|
64,
|
|
|
|
|
"pool_id should be 64 hex chars, got: {}",
|
|
|
|
|
start_pool.pool_id
|
|
|
|
|
);
|
|
|
|
|
assert!(
|
|
|
|
|
start_pool.pool_id.chars().all(|c| c.is_ascii_hexdigit()),
|
|
|
|
|
"pool_id should be valid hex, got: {}",
|
|
|
|
|
start_pool.pool_id
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
start_pool.pool_id,
|
|
|
|
|
expected_pool_id.to_hex(),
|
|
|
|
|
"pool_id should match expected"
|
|
|
|
|
);
|
|
|
|
|
}
|
2025-08-19 11:38:05 +02:00
|
|
|
}
|