// Copyright (C) 2024 Riften Labs AS // // This software is licensed under the GNU Affero General Public License (AGPL), version 3.0 or later. // A copy of the license can be found in the LICENSE file or at https://www.gnu.org/licenses/agpl-3.0.html use anyhow::{Context, Result}; use bitcoin_hashes::hex::FromHex; use bitcoincash::TokenID; use r2d2::Pool; use r2d2_sqlite::SqliteConnectionManager; use rayon::prelude::*; use rusqlite::params; use rusqlite::Connection; type TokenBasicInfo = (String, Option, Option); type TokenVolumeInfo = (String, Option, Option, u64); use crate::db::bcmr::{get_token_bcmr, get_well_known_bcmr}; use crate::db::cauldron::tokenlist::db_utils::order_clause; use crate::db::cauldron::tokenlist::db_utils::CachedSort; use crate::db::cauldron::tokenlist::list_cached::TokenListItemCached; fn search_bcmr(bcmr_conn: &Connection, search_query: &str) -> Result> { let is_full_hex_token_id = TokenID::from_hex(search_query).is_ok(); // Fetch the latest (highest) record that has BCMR data. let sql = if is_full_hex_token_id { "SELECT token_id, name, symbol FROM ( SELECT a.token_id, b.name, b.symbol, a.height, ROW_NUMBER() OVER (PARTITION BY a.token_id ORDER BY a.height DESC) AS rn FROM auth_chain_entry a LEFT JOIN bcmr_data b ON a.utxo = b.utxo ) subquery WHERE rn = 1 AND (token_id = ?1);" } else { "SELECT token_id, name, symbol FROM ( SELECT a.token_id, b.name, b.symbol, a.height, ROW_NUMBER() OVER (PARTITION BY a.token_id ORDER BY a.height DESC) AS rn FROM auth_chain_entry a LEFT JOIN bcmr_data b ON a.utxo = b.utxo WHERE a.bcmr_data IS NOT NULL ) subquery WHERE rn = 1 AND (name LIKE ?1 OR symbol LIKE ?1);" }; let mut statement = bcmr_conn.prepare(sql)?; let search_pattern = if is_full_hex_token_id { search_query.to_string() } else { format!("%{search_query}%") }; let mut bcmr_data: Vec = Vec::new(); let mut query = statement.query([search_pattern.as_str()])?; while let Some(result) = query.next()? { let token_id: String = result.get(0)?; let name: Option = result.get(1)?; let ticker: Option = result.get(2)?; bcmr_data.push((token_id, name, ticker)); } Ok(bcmr_data) } fn search_crc20(crc20_conn: &Connection, search_query: &str) -> Result> { let is_full_hex_token_id = TokenID::from_hex(search_query).is_ok(); let sql = if is_full_hex_token_id { // Search by token_id if it's a full hex token ID " SELECT token_id, name, symbol FROM crc20 WHERE token_id = ?1; " } else { // Only search by name or symbol otherwise " SELECT token_id, name, symbol FROM crc20 WHERE name LIKE ?1 OR symbol LIKE ?1; " }; let mut statement = crc20_conn.prepare(sql)?; let search_pattern = if is_full_hex_token_id { search_query.to_string() } else { format!("%{search_query}%") }; let mut crc20_data: Vec = Vec::new(); let mut query = statement.query([&search_pattern])?; while let Some(result) = query.next()? { let token_id: String = result.get(0)?; let name: Option = result.get(1)?; let ticker: Option = result.get(2)?; crc20_data.push((token_id, name, ticker)); } Ok(crc20_data) } fn token_volume( cauldron_pool: Pool, tokens: Vec, ) -> Result> { let result: Vec = tokens .into_par_iter() .map(|(token_id, name, ticker)| { let conn = cauldron_pool .get() .context("Failed to get connection from pool")?; let mut statement = conn .prepare( " WITH TradeData AS ( SELECT p.token_id, ABS(phe.sats_delta) as trade_volume FROM pool_history_entry phe JOIN pool p ON phe.pool = p.creation_utxo JOIN tx ON phe.txid = tx.txid WHERE tx.effective_timestamp >= (strftime('%s', 'now') - 2592000) AND p.token_id = ? ) SELECT token_id, COALESCE(SUM(trade_volume), 0) as total_trade_volume FROM TradeData; ", ) .context("Failed to prepare statement")?; let trade_volume: u64 = statement .query_row(params![&token_id], |row| row.get(1)) .unwrap_or(0); // Return result as Ok tuple for successful case Ok((token_id, name, ticker, trade_volume)) }) .collect::>>()?; // Collect results into a Vec, propagating errors Ok(result) } pub fn search_tokens_by_volume( cauldron_pool: &Pool, bcmr_conn: &Connection, crc20_conn: &Connection, search_query: &str, ) -> Result> { let bcmr_data = search_bcmr(bcmr_conn, search_query)?; let crc20_data = search_crc20(crc20_conn, search_query)?; let combined_tokens = [bcmr_data, crc20_data].concat(); let mut result = token_volume(cauldron_pool.clone(), combined_tokens)?; result.sort_by(|a, b| b.3.cmp(&a.3)); Ok(result) } /// Search the cached list by token_id (exact), or display_name/symbol (LIKE), /// then sort by *whatever* using CachedSort. /// /// - `query`: /// - empty/whitespace => returns everything /// - full 32-byte hex => exact token_id match /// - otherwise => display_name LIKE or display_symbol LIKE /// - `filter_zero_tvl`: skip rows with tvl_sats=0 (usually you want `true`) pub fn db_search_tokens_cached( cauldron_conn: &Connection, bcmr_conn: &Connection, q: &str, sort: CachedSort, limit: usize, offset: usize, ) -> Result> { use rusqlite::types::ToSql; let order_sql = order_clause(sort); enum Filter { None, Id(String), Pat(String), } let mut where_sql = String::new(); let filter = { let q = q.trim(); if q.is_empty() { Filter::None } else if TokenID::from_hex(q).is_ok() { where_sql.push_str("WHERE token_id = :id"); Filter::Id(q.to_lowercase()) } else { where_sql.push_str( "WHERE (display_name LIKE :pat COLLATE NOCASE OR display_symbol LIKE :pat COLLATE NOCASE)" ); Filter::Pat(format!("%{}%", q)) } }; let sql = format!( "SELECT token_id, trade_volume, tvl_sats, tvl_tokens, score, price_now, price_24h, price_7d, change_24h_bp, change_7d_bp, display_name, display_symbol, price_now_usd, price_24h_usd, price_7d_usd, change_24h_usd_bp, change_7d_usd_bp, apy_30d_bp, score_rank FROM cached_token_metrics {where_sql} {order_sql} LIMIT :limit OFFSET :offset" ); let mut stmt = cauldron_conn.prepare(&sql)?; let limit_i64 = limit as i64; let offset_i64 = offset as i64; let mut params_vec: Vec<(&str, &dyn ToSql)> = Vec::with_capacity(3); match &filter { Filter::Id(s) => params_vec.push((":id", s as &dyn ToSql)), Filter::Pat(s) => params_vec.push((":pat", s as &dyn ToSql)), Filter::None => {} } params_vec.push((":limit", &limit_i64)); params_vec.push((":offset", &offset_i64)); let mut rows = stmt.query(¶ms_vec[..])?; let mut out = Vec::with_capacity(limit); while let Some(row) = rows.next()? { let token_id: String = row.get(0)?; let trade_volume: u64 = row.get(1)?; let tvl_sats: u64 = row.get::<_, i64>(2)? as u64; let tvl_tokens: u64 = row.get::<_, i64>(3)? as u64; let score: i64 = row.get(4)?; let price_now: f64 = row.get(5)?; let price_24h: f64 = row.get(6)?; let price_7d: f64 = row.get(7)?; let change_24h_bp: i64 = row.get(8)?; let change_7d_bp: i64 = row.get(9)?; let display_name: String = row.get(10)?; let display_symbol: String = row.get(11)?; let price_now_usd: f64 = row.get(12)?; let price_24h_usd: f64 = row.get(13)?; let price_7d_usd: f64 = row.get(14)?; let change_24h_usd_bp: i64 = row.get(15)?; let change_7d_usd_bp: i64 = row.get(16)?; let apy_30d_bp: i64 = row.get(17)?; let score_rank: i64 = row.get(18)?; let bcmr = get_token_bcmr(bcmr_conn, &token_id)?; let bcmr_well_known = get_well_known_bcmr(bcmr_conn, &token_id)?; out.push(TokenListItemCached { token_id, trade_volume, tvl_sats, tvl_tokens, bcmr, bcmr_well_known, trade_count: 0, score, // <-- NEW score_rank, price_now, price_24h, price_7d, change_24h_bp, change_7d_bp, display_name, display_symbol, price_now_usd, price_24h_usd, price_7d_usd, change_24h_usd_bp, change_7d_usd_bp, apy_30d_bp, }); } Ok(out) } #[cfg(test)] mod tests { use super::*; use crate::bcmr::parsedbcmr::{FileMeta, ParsedBCMR, Token, Uris}; use crate::db::bcmr::{ insert_authheader, insert_bcmr_data, prepare_tables as bcmr_prepare_tables, }; use crate::db::cauldron::pool::{dummy_init_seq, insert_new_pool, insert_pool_history_entry}; use crate::db::cauldron::prepare_tables as cauldron_prepare_tables; use crate::db::cauldron::tokenlist::db_utils::create_cached_token_metrics_table; use crate::db::cauldron::tx::{insert_block_tx, insert_mempool_tx}; use crate::db::cauldron::utxo_funding::insert_utxo_funding; use crate::db::crc20::prepare_tables as crc20_prepare_tables; use crate::timeutil::time_now; use crate::utiltest::mock_db_pool; use bitcoin_hashes::hex::{FromHex, ToHex}; use bitcoin_hashes::Hash; use bitcoincash::{BlockHash, PubkeyHash, TokenID, Txid}; use riftenlabs_defi::cauldron::ParsedContract; use riftenlabs_defi::chainutil::OutPointHash; use rusqlite::{params, Connection}; fn setup_mock_db(conn: &Connection) { cauldron_prepare_tables(conn); bcmr_prepare_tables(conn); crc20_prepare_tables(conn); } fn setup_cached_tables(conn: &Connection) { // you already call the other prepare_* in setup_mock_db; add this: create_cached_token_metrics_table(conn).expect("create cached_token_metrics"); } fn seed_cached_row_simple( conn: &Connection, token_hex: &str, display_name: &str, display_symbol: &str, tvl_sats: i64, tvl_tokens: i64, trade_volume: i64, score: i64, price_now: f64, price_24h: f64, price_7d: f64, ch24_bp: i64, ch7d_bp: i64, price_now_usd: f64, price_24h_usd: f64, price_7d_usd: f64, ch24_usd_bp: i64, ch7d_usd_bp: i64, apy_bp: i64, ) { conn.execute( r#" INSERT INTO cached_token_metrics (token_id, trade_volume, tvl_sats, tvl_tokens, score, display_name, display_symbol, price_now, price_24h, price_7d, change_24h_bp, change_7d_bp, price_now_usd, price_24h_usd, price_7d_usd, change_24h_usd_bp, change_7d_usd_bp, apy_30d_bp, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, strftime('%s','now')) "#, params![ token_hex, trade_volume, tvl_sats, tvl_tokens, score, display_name, display_symbol, price_now, price_24h, price_7d, ch24_bp, ch7d_bp, price_now_usd, price_24h_usd, price_7d_usd, ch24_usd_bp, ch7d_usd_bp, apy_bp ], ) .unwrap(); } #[test] fn test_db_search_tokens_cached_fuzzy_and_exact() { let mock = mock_db_pool(|conn| { cauldron_prepare_tables(conn); bcmr_prepare_tables(conn); crc20_prepare_tables(conn); setup_cached_tables(conn); }); let r = mock.cauldron_r.get().unwrap(); let w = mock.cauldron_w.get().unwrap(); let bcmr = mock.bcmr_r.get().unwrap(); // three tokens let t_alpha = TokenID::from_inner([0xA1; 32]).to_hex(); let t_beta = TokenID::from_inner([0xB2; 32]).to_hex(); let t_gamma = TokenID::from_inner([0xC3; 32]).to_hex(); // tvl > 0 so they’re visible; make volumes/prices different so sorts are deterministic seed_cached_row_simple( &w, &t_alpha, "Alpha", "ALP", 1_000_000, 500_000, 5_000, 10, 10.0, 9.0, 8.0, 111, 222, 1.0, 0.9, 0.8, 100, 200, 1000, ); seed_cached_row_simple( &w, &t_beta, "Beta", "BET", 2_000_000, 700_000, 50_000, 99, 20.0, 19.0, 18.0, 222, 111, 2.0, 1.9, 1.8, 200, 100, 500, ); seed_cached_row_simple( &w, &t_gamma, "Gamma", "GAM", 1_500_000, 600_000, 10_000, 50, 30.0, 29.0, 28.0, 50, -10, 3.0, 2.9, 2.8, 40, -20, 3000, ); // fuzzy name (case-insensitive) let v = db_search_tokens_cached(&r, &bcmr, "alp", CachedSort::NameAsc, 10, 0).unwrap(); assert_eq!(v.len(), 1); assert_eq!(v[0].display_name, "Alpha"); // fuzzy symbol (case-insensitive) let v = db_search_tokens_cached(&r, &bcmr, "BET", CachedSort::NameAsc, 10, 0).unwrap(); assert_eq!(v.len(), 1); assert_eq!(v[0].display_name, "Beta"); // exact token id (full 64-hex) let v = db_search_tokens_cached(&r, &bcmr, &t_gamma, CachedSort::NameAsc, 10, 0).unwrap(); assert_eq!(v.len(), 1); assert_eq!(v[0].display_name, "Gamma"); } #[test] fn test_db_search_tokens_cached_sort_and_paging() { let mock = mock_db_pool(|conn| { cauldron_prepare_tables(conn); bcmr_prepare_tables(conn); crc20_prepare_tables(conn); setup_cached_tables(conn); }); let r = mock.cauldron_r.get().unwrap(); let w = mock.cauldron_w.get().unwrap(); let bcmr = mock.bcmr_r.get().unwrap(); let t1 = TokenID::from_inner([0x11; 32]).to_hex(); // smaller volume let t2 = TokenID::from_inner([0x22; 32]).to_hex(); // larger volume seed_cached_row_simple( &w, &t1, "Alpha", "ALP", 1_000_000, 500_000, 5_000, 10, 10.0, 9.0, 8.0, 111, 222, 1.0, 0.9, 0.8, 100, 200, 1000, ); seed_cached_row_simple( &w, &t2, "Beta", "BET", 2_000_000, 700_000, 50_000, 999, 20.0, 19.0, 18.0, 222, 111, 2.0, 1.9, 1.8, 200, 100, 500, ); // sort by volume desc → Beta then Alpha let v = db_search_tokens_cached(&r, &bcmr, "", CachedSort::VolumeDesc, 10, 0).unwrap(); assert_eq!(v.len(), 2); assert_eq!(v[0].display_name, "Beta"); assert_eq!(v[1].display_name, "Alpha"); // paging let v = db_search_tokens_cached(&r, &bcmr, "", CachedSort::VolumeDesc, 1, 0).unwrap(); assert_eq!(v.len(), 1); assert_eq!(v[0].display_name, "Beta"); let v = db_search_tokens_cached(&r, &bcmr, "", CachedSort::VolumeDesc, 1, 1).unwrap(); assert_eq!(v.len(), 1); assert_eq!(v[0].display_name, "Alpha"); } #[test] fn test_db_search_tokens_cached_other_sorts() { let mock = mock_db_pool(|conn| { cauldron_prepare_tables(conn); bcmr_prepare_tables(conn); crc20_prepare_tables(conn); setup_cached_tables(conn); }); let r = mock.cauldron_r.get().unwrap(); let w = mock.cauldron_w.get().unwrap(); let bcmr = mock.bcmr_r.get().unwrap(); let t_low = TokenID::from_inner([0x33; 32]).to_hex(); let t_mid = TokenID::from_inner([0x44; 32]).to_hex(); let t_high = TokenID::from_inner([0x55; 32]).to_hex(); // make USD price ascending: low < mid < high seed_cached_row_simple( &w, &t_low, "LowUSD", "LUS", 1_000, 100, 1, 1, 1.0, 1.0, 1.0, 0, 0, 0.5, 0.4, 0.3, -100, -200, 100, ); seed_cached_row_simple( &w, &t_mid, "MidUSD", "MUS", 2_000, 200, 1, 1, 2.0, 2.0, 2.0, 0, 0, 1.5, 1.4, 1.3, 100, -50, 1000, ); seed_cached_row_simple( &w, &t_high, "HighUSD", "HUS", 3_000, 300, 1, 1, 3.0, 3.0, 3.0, 0, 0, 2.5, 2.4, 2.3, 200, 150, 3000, ); // price_usd desc → HighUSD, MidUSD, LowUSD let v = db_search_tokens_cached(&r, &bcmr, "", CachedSort::PriceUsdDesc, 10, 0).unwrap(); assert_eq!( v.iter().map(|x| x.display_name.clone()).collect::>(), vec!["HighUSD", "MidUSD", "LowUSD"] ); // change_24h_usd_bp asc → LowUSD (-100), MidUSD (+100), HighUSD (+200) let v = db_search_tokens_cached(&r, &bcmr, "", CachedSort::Change24hUsdAsc, 10, 0).unwrap(); assert_eq!( v.iter().map(|x| x.display_name.clone()).collect::>(), vec!["LowUSD", "MidUSD", "HighUSD"] ); // apy desc → HighUSD (3000) > MidUSD (1000) > LowUSD (100) let v = db_search_tokens_cached(&r, &bcmr, "", CachedSort::Apy30dDesc, 10, 0).unwrap(); assert_eq!( v.iter().map(|x| x.display_name.clone()).collect::>(), vec!["HighUSD", "MidUSD", "LowUSD"] ); } #[test] // Ensure that we are NOT looking for token_id when using incomplete hex. fn test_search_token_by_inexact_hex_id() { let mock_db = mock_db_pool(setup_mock_db); let write_conn = mock_db.cauldron_w.get().unwrap(); match insert_mock_data(&write_conn) { Ok(_) => println!("Mock data inserted successfully."), Err(e) => println!("Failed to insert mock data: {e:?}"), } let token_id_hex = "dadadadadadadadadadadadadadadadada"; let result = search_tokens_by_volume( &mock_db.cauldron_r, &mock_db.bcmr_r.get().unwrap(), &mock_db.crc20_r.get().unwrap(), token_id_hex, ) .expect("Failed to search tokens by volume"); assert_eq!(result.len(), 0); } #[test] fn test_search_token_by_exact_hex_id() { let mock_db = mock_db_pool(setup_mock_db); let write_conn = mock_db.cauldron_w.get().unwrap(); match insert_mock_data(&write_conn) { Ok(_) => println!("Mock data inserted successfully."), Err(e) => println!("Failed to insert mock data: {e:?}"), } let token_id_hex = "dadadadadadadadadadadadadadadadadadadadadadadadadadadadadadadada"; let result = search_tokens_by_volume( &mock_db.cauldron_r, &mock_db.bcmr_r.get().unwrap(), &mock_db.crc20_r.get().unwrap(), token_id_hex, ) .expect("Failed to search tokens by volume"); assert_eq!(result.len(), 1); assert_eq!(result[0].1, Some("TokenOne".to_string())); // Ensure it matches "TokenOne" } #[test] fn test_search_tokens_by_volume_with_bcmr_and_crc20() { let mock_db = mock_db_pool(setup_mock_db); let write_conn = mock_db.cauldron_w.get().unwrap(); match insert_mock_data(&write_conn) { Ok(_) => println!("Mock data inserted successfully."), Err(e) => println!("Failed to insert mock data: {e:?}"), } let result: Vec<(String, Option, Option, u64)> = search_tokens_by_volume( &mock_db.cauldron_r, &mock_db.bcmr_r.get().unwrap(), &mock_db.crc20_r.get().unwrap(), "", ) .expect("Failed to search tokens by volume"); println!("Test Results: {result:?}"); assert_eq!(result.len(), 4); // We expect 4 tokens in total (3 BCMR + 1 CRC20) // Assert ordering by volume (highest to lowest). assert_eq!(result[0].1, Some("TokenFour".to_string())); // 5000 volume assert_eq!(result[1].1, Some("TokenTwo".to_string())); // 3000 volume assert_eq!(result[2].1, Some("TokenOne".to_string())); // 2000 volume assert_eq!(result[3].1, Some("TokenThree".to_string())); // 0 volume } #[test] fn test_search_tokens_by_volume_no_results() { let mock_db = mock_db_pool(setup_mock_db); let write_conn = mock_db.cauldron_w.get().unwrap(); match insert_mock_data(&write_conn) { Ok(_) => println!("Mock data inserted successfully."), Err(e) => println!("Failed to insert mock data: {e:?}"), } let result = search_tokens_by_volume( &mock_db.cauldron_r, &mock_db.bcmr_r.get().unwrap(), &mock_db.crc20_r.get().unwrap(), "doesNotExist", ) .expect("Failed to search tokens by volume"); assert_eq!(result.len(), 0); } fn dummy_cauldron( txid: &Txid, utxo: &OutPointHash, token: &TokenID, sats: u64, tokens: i64, pkh: &PubkeyHash, ) -> ParsedContract { ParsedContract { pkh: *pkh, is_withdrawn: false, spent_utxo_hash: OutPointHash::all_zeros(), new_utxo_hash: Some(*utxo), new_utxo_txid: Some(*txid), new_utxo_n: Some(0), token_id: Some(*token), sats: Some(sats), token_amount: Some(tokens), } } #[test] fn test_search_token_by_name_symbol_and_id() { let mock_db = mock_db_pool(setup_mock_db); let write_conn = mock_db.cauldron_w.get().unwrap(); match insert_mock_data(&write_conn) { Ok(_) => println!("Mock data inserted successfully."), Err(e) => println!("Failed to insert mock data: {e:?}"), } let token_tests = vec![ ( "TokenOne", "TONE", "dadadadadadadadadadadadadadadadadadadadadadadadadadadadadadadada", "TokenOne", ), ( "TokenTwo", "TWO", "dbdbdbdbdbdbdbdbdbdbdbdbdbdbdbdbdbdbdbdbdbdbdbdbdbdbdbdbdbdbdbdb", "TokenTwo", ), ( "TokenThree", "TN3", "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", "TokenThree", ), ( "TokenFour", "TFOUR", "dcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdc", "TokenFour", ), ]; for (name, symbol, token_id, expected_name) in token_tests { let result = search_tokens_by_volume( &mock_db.cauldron_r, &mock_db.bcmr_r.get().unwrap(), &mock_db.crc20_r.get().unwrap(), name, ) .expect("Failed to search tokens by volume"); assert_eq!(result.len(), 1); assert_eq!(result[0].1, Some(expected_name.to_string())); let result = search_tokens_by_volume( &mock_db.cauldron_r, &mock_db.bcmr_r.get().unwrap(), &mock_db.crc20_r.get().unwrap(), symbol, ) .expect("Failed to search tokens by volume"); assert_eq!(result.len(), 1); assert_eq!(result[0].1, Some(expected_name.to_string())); let result = search_tokens_by_volume( &mock_db.cauldron_r, &mock_db.bcmr_r.get().unwrap(), &mock_db.crc20_r.get().unwrap(), token_id, ) .expect("Failed to search tokens by volume"); assert_eq!(result.len(), 1); assert_eq!(result[0].1, Some(expected_name.to_string())); } } fn insert_mock_data(conn: &Connection) -> Result<(), Box> { dummy_init_seq(); let current_timestamp = time_now(); let thirty_days_ago = current_timestamp - 30 * 24 * 60 * 60; let owner_pkh = PubkeyHash::from_inner([0xca; 20]); let block_zero = BlockHash::all_zeros(); // ================== TOKEN 1 (BCMR Token with Volume) ================== let utxo1 = OutPointHash::from_hex( "a3f1d42e2a5c9f2b5f1b9d7f7c2b19e7a3b1d2c3f4a5e6f2d1c3e4f5a1b2c3d4", )?; // Step 1: Prepare and insert BCMR data for token1 with non-null fields let token1 = Token { category: "asset_category".to_string(), symbol: "TONE".to_string(), decimals: 8, }; let uris1 = Uris { icon: Some("https://example.com/icon.png".to_string()), web: Some("https://example.com".to_string()), }; let filemeta1 = FileMeta { expected_hash: Some("expected_hash1".to_string()), actual_hash: Some("actual_hash1".to_string()), source: "source_url".to_string(), }; let parsed_bcmr1 = ParsedBCMR { name: "TokenOne".to_string(), description: "Test Token 1".to_string(), token: token1, uris: uris1, filemeta: filemeta1, }; if let Err(e) = insert_bcmr_data(conn, &utxo1, &parsed_bcmr1) { println!("Failed to insert BCMR data for token1: {e:?}"); } // Step 2: Insert Pool for token1 let txid1 = Txid::from_inner([0xf0; 32]); let token_id1: TokenID = TokenID::from_inner([0xda; 32]); let cauldron1 = dummy_cauldron(&txid1, &utxo1, &token_id1, 1000, 500, &owner_pkh); if let Err(e) = insert_new_pool(conn, &cauldron1) { println!("Failed to insert pool for token1: {e:?}"); } // Step 3: Insert auth_chain_entry for token1 if let Err(e) = insert_authheader( conn, &utxo1, &BlockHash::all_zeros(), &txid1, &token_id1, 10, Some(Vec::from("bcmr_data1".as_bytes())), ) { println!("Failed to insert auth_chain_entry for token1: {e:?}"); } // Step 4: Insert the initial UTXO funding for token1 if let Err(e) = insert_utxo_funding(conn, &vec![cauldron1.clone()], &txid1, true) { println!("Failed to insert initial UTXO funding for token1: {e:?}"); } // Step 5: Insert the transaction for utxo1 if let Err(e) = insert_block_tx(conn, &txid1, &block_zero, thirty_days_ago) { println!("Failed to insert block transaction for utxo1: {e:?}"); } if let Err(e) = insert_mempool_tx(conn, &txid1, thirty_days_ago as u64) { println!("Failed to insert mempool transaction for utxo1: {e:?}"); } // Step 6: Simulate Volume - Insert second funding for token1 let txid2 = Txid::from_inner([0xf1; 32]); let utxo2 = OutPointHash::from_inner([0xe1; 32]); let cauldron2 = ParsedContract { pkh: owner_pkh, is_withdrawn: false, spent_utxo_hash: utxo1, new_utxo_hash: Some(utxo2), new_utxo_txid: Some(txid2), new_utxo_n: Some(0), token_id: Some(token_id1), sats: Some(2000), token_amount: Some(1000), }; if let Err(e) = insert_utxo_funding(conn, &vec![cauldron2.clone()], &txid2, true) { println!("Failed to insert second UTXO funding for token1: {e:?}"); } // Step 7: Insert the transaction for utxo2 (spending utxo1) if let Err(e) = insert_block_tx(conn, &txid2, &block_zero, current_timestamp) { println!("Failed to insert block transaction for utxo2: {e:?}"); } if let Err(e) = insert_mempool_tx(conn, &txid2, current_timestamp as u64) { println!("Failed to insert mempool transaction for utxo2: {e:?}"); } // Step 8: Insert pool history entries for initial funding and spending if let Err(e) = insert_pool_history_entry( conn, &utxo1, &cauldron1.clone(), Some(thirty_days_ago as u64), Some(thirty_days_ago as u64), 0, // sats_delta for initial funding (no trading activity) 0, // token_delta for initial funding (no trading activity) ) { println!("Failed to insert pool history entry for initial funding of token1: {e:?}"); } if let Err(e) = insert_pool_history_entry( conn, &utxo1, &cauldron2, Some(current_timestamp as u64), Some(current_timestamp as u64), 2000, // sats_delta for spending (actual trading activity) 1000, // token_delta for spending (actual trading activity) ) { println!("Failed to insert pool history entry for spending of token1: {e:?}"); } // ================== TOKEN 2 (BCMR Token) ================== let utxo2 = OutPointHash::from_hex( "c3d2e1f4b6a7c8d1e2f5d7c3b1a9e4f2b1d3f4e6c2b9f3a8d1e4f5b6c3d2e7a4", )?; // Step 1: Prepare and insert BCMR data for token2 with unique values let token2 = Token { category: "asset_category_2".to_string(), symbol: "TWO".to_string(), decimals: 8, }; let uris2 = Uris { icon: Some("https://example.com/icon2.png".to_string()), web: Some("https://example.com/two".to_string()), }; let filemeta2 = FileMeta { expected_hash: Some("expected_hash2".to_string()), actual_hash: Some("actual_hash2".to_string()), source: "source_url_2".to_string(), }; let parsed_bcmr2 = ParsedBCMR { name: "TokenTwo".to_string(), description: "Test Token 2".to_string(), token: token2, uris: uris2, filemeta: filemeta2, }; if let Err(e) = insert_bcmr_data(conn, &utxo2, &parsed_bcmr2) { println!("Failed to insert BCMR data for token2: {e:?}"); } // Step 2: Insert Pool for token2 let token_id2 = TokenID::from_inner([0xdb; 32]); let cauldron2 = dummy_cauldron( &Txid::from_inner([0xf2; 32]), &utxo2, &token_id2, 1500, 700, &owner_pkh, ); insert_new_pool(conn, &cauldron2)?; // Step 9: Insert auth_chain_entry for token2 if let Err(e) = insert_authheader( conn, &utxo2, &BlockHash::all_zeros(), &txid2, &token_id2, 15, Some(Vec::from("bcmr_data2".as_bytes())), ) { println!("Failed to insert auth_chain_entry for TOKEN 3: {e:?}"); }; // Step 3: Insert the initial UTXO funding for token2 insert_utxo_funding( conn, &vec![cauldron2.clone()], &Txid::from_inner([0xf2; 32]), true, )?; // Step 4: Insert the transaction for utxo2 let txid2 = Txid::from_inner([0xf2; 32]); insert_block_tx(conn, &txid2, &block_zero, thirty_days_ago)?; insert_mempool_tx(conn, &txid2, thirty_days_ago as u64)?; // Step 5: Define cauldron3 for token2 with spent_utxo_hash referring to utxo2, then insert it let txid3 = Txid::from_inner([0xf3; 32]); let utxo3 = OutPointHash::from_inner([0xe2; 32]); let cauldron3 = ParsedContract { pkh: owner_pkh, is_withdrawn: false, spent_utxo_hash: utxo2, new_utxo_hash: Some(utxo3), new_utxo_txid: Some(txid3), new_utxo_n: Some(0), token_id: Some(token_id2), sats: Some(3000), token_amount: Some(1500), }; // Insert the second funding with `spent_utxo_hash` correctly set insert_utxo_funding(conn, &vec![cauldron3.clone()], &txid3, true)?; // Step 6: Insert the transaction for utxo3 (the transaction that spent utxo2) insert_block_tx(conn, &txid3, &block_zero, current_timestamp)?; insert_mempool_tx(conn, &txid3, current_timestamp as u64)?; // Step 7: Insert pool history entry for the initial UTXO funding (utxo2) insert_pool_history_entry( conn, &utxo2, &cauldron2.clone(), Some(thirty_days_ago as u64), Some(thirty_days_ago as u64), 0, // sats_delta for initial funding (no trading activity) 0, // token_delta for initial funding (no trading activity) )?; // Step 8: Insert pool history entry for the spending of utxo2 (creation of utxo3) insert_pool_history_entry( conn, &utxo2, &cauldron3, Some(current_timestamp as u64), Some(current_timestamp as u64), 3000, // sats_delta for spending (actual trading activity) 1500, // token_delta for spending (actual trading activity) )?; // ================== TOKEN 3 (BCMR Token with No Volume) ================== let utxo3 = OutPointHash::from_hex( "d5e1f2a3b4c3d2f5b6a8e7d3c2f4b9a6d2e3f1c4b5a9e3d1b2c5f7a3d4b8e2c3", )?; // Prepare and insert BCMR data for TOKEN 3 with unique values let token3 = Token { category: "asset_category_3".to_string(), symbol: "TN3".to_string(), decimals: 8, }; let uris3 = Uris { icon: Some("https://example.com/icon3.png".to_string()), web: Some("https://example.com/three".to_string()), }; let filemeta3 = FileMeta { expected_hash: Some("expected_hash3".to_string()), actual_hash: Some("actual_hash3".to_string()), source: "source_url_3".to_string(), }; let parsed_bcmr3 = ParsedBCMR { name: "TokenThree".to_string(), description: "Test Token 3 without Volume".to_string(), token: token3, uris: uris3, filemeta: filemeta3, }; if let Err(e) = insert_bcmr_data(conn, &utxo3, &parsed_bcmr3) { println!("Failed to insert BCMR data for TOKEN 3: {e:?}"); } // Insert Pool for TOKEN 3 let txid3_initial = Txid::from_inner([0xf6; 32]); let token_id3 = TokenID::from_inner([0xdd; 32]); let cauldron3_initial = dummy_cauldron(&txid3_initial, &utxo3, &token_id3, 1000, 500, &owner_pkh); if let Err(e) = insert_new_pool(conn, &cauldron3_initial) { println!("Failed to insert pool for TOKEN 3: {e:?}"); } // Insert auth_chain_entry for TOKEN 3 if let Err(e) = insert_authheader( conn, &utxo3, &BlockHash::all_zeros(), &txid3, &token_id3, 20, Some(Vec::from("bcmr_data3".as_bytes())), ) { println!("Failed to insert auth_chain_entry for TOKEN 3: {e:?}"); } // Insert the initial UTXO funding for TOKEN 3 (No additional funding to keep volume at 0) if let Err(e) = insert_utxo_funding(conn, &vec![cauldron3_initial.clone()], &txid3_initial, true) { println!("Failed to insert initial UTXO funding for TOKEN 3: {e:?}"); } // Insert transaction for the initial funding without spending if let Err(e) = insert_block_tx(conn, &txid3_initial, &block_zero, thirty_days_ago) { println!("Failed to insert block transaction for TOKEN 3 initial funding: {e:?}"); } if let Err(e) = insert_mempool_tx(conn, &txid3_initial, thirty_days_ago as u64) { println!("Failed to insert mempool transaction for TOKEN 3 initial funding: {e:?}"); } // Insert pool history entry for the initial funding of TOKEN 3 (no spending history) if let Err(e) = insert_pool_history_entry( conn, &utxo3, &cauldron3_initial, Some(thirty_days_ago as u64), Some(thirty_days_ago as u64), 0, // sats_delta for initial funding (no trading activity) 0, // token_delta for initial funding (no trading activity) ) { println!("Failed to insert pool history entry for TOKEN 3 initial funding: {e:?}"); } // ================== TOKEN 4 (CRC20 Token with Volume) ================== let utxo4_initial = OutPointHash::from_hex( "f1d4e2a3b5c4d2f7b6a8e7d3c2f4b9a6d3e1f1c4b7a8e2d3b6c7f9a5d4b1e6c3", )?; let token_id4 = TokenID::from_inner([0xdc; 32]); // Unique ID for TOKEN 4 // Insert CRC20 data for TOKEN 4 conn.execute( "INSERT INTO crc20 (token_id, name, symbol, decimals) VALUES (?, ?, ?, ?)", params![&token_id4.to_hex(), "TokenFour", "TFOUR", 18], )?; // Step 1: Insert initial funding for TOKEN 4 let txid4_initial = Txid::from_inner([0xf4; 32]); let cauldron4_initial = dummy_cauldron( &txid4_initial, &utxo4_initial, &token_id4, 3600, 1200, &owner_pkh, ); // Insert pool for TOKEN 4 if let Err(e) = insert_new_pool(conn, &cauldron4_initial) { println!("Failed to insert pool for TOKEN 4: {e:?}"); } if let Err(e) = insert_utxo_funding(conn, &vec![cauldron4_initial.clone()], &txid4_initial, true) { println!("Failed to insert initial UTXO funding for TOKEN 4: {e:?}"); } // Insert transaction for the initial funding if let Err(e) = insert_block_tx(conn, &txid4_initial, &block_zero, thirty_days_ago) { println!("Failed to insert block transaction for TOKEN 4 initial funding: {e:?}"); } if let Err(e) = insert_mempool_tx(conn, &txid4_initial, thirty_days_ago as u64) { println!("Failed to insert mempool transaction for TOKEN 4 initial funding: {e:?}"); } // Step 2: Insert second funding entry to simulate volume let txid4_spend = Txid::from_inner([0xf5; 32]); let utxo4_spent = OutPointHash::from_inner([0xe4; 32]); let cauldron4_spent = ParsedContract { pkh: owner_pkh, is_withdrawn: false, spent_utxo_hash: utxo4_initial, new_utxo_hash: Some(utxo4_spent), new_utxo_txid: Some(txid4_spend), new_utxo_n: Some(0), token_id: Some(token_id4), sats: Some(5000), token_amount: Some(1500), }; if let Err(e) = insert_utxo_funding(conn, &vec![cauldron4_spent.clone()], &txid4_spend, true) { println!("Failed to insert spent UTXO funding for TOKEN 4: {e:?}"); } // Insert transaction for the spending UTXO if let Err(e) = insert_block_tx(conn, &txid4_spend, &block_zero, current_timestamp) { println!("Failed to insert block transaction for TOKEN 4 spending: {e:?}"); } if let Err(e) = insert_mempool_tx(conn, &txid4_spend, current_timestamp as u64) { println!("Failed to insert mempool transaction for TOKEN 4 spending: {e:?}"); } // Step 3: Insert pool history entries for funding and spending if let Err(e) = insert_pool_history_entry( conn, &utxo4_initial, &cauldron4_initial, Some(thirty_days_ago as u64), Some(thirty_days_ago as u64), 0, // sats_delta for initial funding (no trading activity) 0, // token_delta for initial funding (no trading activity) ) { println!("Failed to insert pool history entry for TOKEN 4 initial funding: {e:?}"); } if let Err(e) = insert_pool_history_entry( conn, &utxo4_initial, &cauldron4_spent, Some(current_timestamp as u64), Some(current_timestamp as u64), 5000, // sats_delta for spending (actual trading activity) 1500, // token_delta for spending (actual trading activity) ) { println!("Failed to insert pool history entry for TOKEN 4 spending: {e:?}"); } Ok(()) } }