diff --git a/src/bcmr/bcmrdownloader.rs b/src/bcmr/bcmrdownloader.rs index b552b0c..021223f 100644 --- a/src/bcmr/bcmrdownloader.rs +++ b/src/bcmr/bcmrdownloader.rs @@ -192,6 +192,7 @@ impl BCMRDownloader { &conn, &entry.utxo, &entry.txid, + &entry.token_id, "Invalid BCMR OP_RETURN in DB", true, ) { @@ -211,6 +212,7 @@ impl BCMRDownloader { &conn, &entry.utxo, &entry.txid, + &entry.token_id, &format!("Failed to fetch BCMR: {error}"), is_fatal, ) { @@ -227,6 +229,7 @@ impl BCMRDownloader { &conn, &entry.utxo, &entry.txid, + &entry.token_id, &format!("BCMR invalid JSON error: {e}"), true, ) { @@ -249,6 +252,7 @@ impl BCMRDownloader { &conn, &entry.utxo, &entry.txid, + &entry.token_id, &format!("BCMR contents error: {err}"), true, ) { @@ -258,7 +262,7 @@ impl BCMRDownloader { } }; - if let Err(err) = insert_bcmr_data(&conn, &entry.utxo, &bcmr_parsed) { + if let Err(err) = insert_bcmr_data(&conn, &entry.token_id, &entry.utxo, &bcmr_parsed) { warn!("bcmr: Failed to insert BCMR data {err}") } }); diff --git a/src/bcmr/mod.rs b/src/bcmr/mod.rs index 60535e2..4980f20 100644 --- a/src/bcmr/mod.rs +++ b/src/bcmr/mod.rs @@ -6,7 +6,7 @@ use rayon::prelude::*; use std::convert::TryInto; -use anyhow::{Context, Result}; +use anyhow::Result; use bitcoin_hashes::{hex::FromHex, hex::ToHex, Hash}; use bitcoincash::{BlockHash, Script, TokenID, Transaction, Txid}; use log::debug; @@ -51,65 +51,50 @@ pub struct BCMR { pub op_return: Vec, } -fn find_parent_auth_entry(conn: &Connection, tx: &Transaction) -> Result> { - let mut best: Option = None; +fn find_parent_auth_entries(conn: &Connection, tx: &Transaction) -> Result> { + let mut parents = Vec::new(); for vin in &tx.input { - // BCMR auth chain is always the previous tx’s vout 0 if vin.previous_output.vout != 0 { continue; } let prev = &vin.previous_output; let prev_utxo: OutPointHash = compute_outpoint_hash(&prev.txid, prev.vout); - let mut stmt = conn - .prepare( - "SELECT token_id, txid, height, bcmr_data, utxo + let mut stmt = conn.prepare( + "SELECT token_id, txid, height, bcmr_data, utxo FROM auth_chain_entry - WHERE utxo = ?1 - LIMIT 1", - ) - .context("prepare SELECT parent auth_chain_entry by utxo")?; + WHERE utxo = ?1", + )?; + let mut rows = stmt.query(params![prev_utxo.to_hex()])?; - let mut rows = stmt - .query(params![prev_utxo.to_hex()]) - .context("query parent auth_chain_entry by utxo")?; - - if let Some(row) = rows.next().context("iterate parent rows")? { + while let Some(row) = rows.next()? { let token_hex: String = row.get(0)?; let txid_hex: String = row.get(1)?; let height: usize = row.get(2)?; let bcmr_data_hex: Option = row.get(3)?; let utxo_hex: String = row.get(4)?; - let token_id = TokenID::from_hex(&token_hex).context("parse token_id from hex")?; - let txid = Txid::from_hex(&txid_hex).context("parse txid from hex")?; - let utxo = OutPointHash::from_hex(&utxo_hex).context("parse utxo from hex")?; + let token_id = TokenID::from_hex(&token_hex)?; + let txid = Txid::from_hex(&txid_hex)?; + let utxo = OutPointHash::from_hex(&utxo_hex)?; - let bcmr_data = if let Some(h) = bcmr_data_hex { - Some(hex::decode(&h).context("decode bcmr_data hex")?) - } else { - None + let bcmr_data = match bcmr_data_hex { + Some(h) => Some(hex::decode(&h)?), + None => None, }; - let candidate = AuthChainEntry { + parents.push(AuthChainEntry { utxo, token_id, txid, height, bcmr_data, - }; - - // Keep the *highest* height (current head) - match &best { - None => best = Some(candidate), - Some(cur) if candidate.height > cur.height => best = Some(candidate), - _ => {} - } + }); } } - Ok(best) + Ok(parents) } pub fn parse_bcmr_from_opreturn(bcmr_op_return: &Script) -> Option { @@ -213,13 +198,18 @@ pub fn index_bcmr( continue; } - // 2) Update path: if any input spends a known auth utxo, extend that chain - if let Some(parent) = find_parent_auth_entry(conn, tx)? { - let bcmr = parse_bcmr(tx); - let txid = tx.txid(); - let new_utxo = compute_outpoint_hash(&txid, 0); - let new_height = parent.height + 1; + let parents = find_parent_auth_entries(conn, tx)?; + if parents.is_empty() { + continue; + } + // 2) Update path: if any input spends a known auth utxo, extend that chain + let bcmr = parse_bcmr(tx); + let txid = tx.txid(); + let new_utxo = compute_outpoint_hash(&txid, 0); + + for parent in parents { + let new_height = parent.height + 1; debug!( "Found authheader update {} in {}; has bcmr: {}; new height: {}, utxo: {}", parent.token_id.to_hex(), @@ -236,16 +226,138 @@ pub fn index_bcmr( &txid, &parent.token_id, new_height, - bcmr.map(|b| b.op_return), + bcmr.as_ref().map(|b| b.op_return.clone()), )?; inserts += 1; } - // else: not a BCMR auth-chain tx → ignore } Ok(inserts) } +#[test] +fn multi_parent_auth_update_creates_entries_for_all_tokens() { + use crate::db::bcmr::prepare_tables; + use bitcoincash::{OutPoint, PackedLockTime, Sequence, TxIn, TxOut}; + // In-memory DB + let mut conn = Connection::open_in_memory().unwrap(); + prepare_tables(&conn); + let db_tx = conn.transaction().unwrap(); // <-- DB transaction + + let bh = BlockHash::all_zeros(); + + // Two fake token IDs + let token_a = + TokenID::from_hex("0101010101010101010101010101010101010101010101010101010101010101") + .unwrap(); + let token_b = + TokenID::from_hex("0202020202020202020202020202020202020202020202020202020202020202") + .unwrap(); + + // Two fake previous txids (current heads for A and B) + let prev_txid_a = + Txid::from_hex("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").unwrap(); + let prev_txid_b = + Txid::from_hex("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb").unwrap(); + + // Their auth-head UTXOs (vout 0) + let utxo_a = compute_outpoint_hash(&prev_txid_a, 0); + let utxo_b = compute_outpoint_hash(&prev_txid_b, 0); + + // Insert both auth heads at height 0 + insert_authheader(&db_tx, &utxo_a, &bh, &prev_txid_a, &token_a, 0, None).unwrap(); + + insert_authheader(&db_tx, &utxo_b, &bh, &prev_txid_b, &token_b, 0, None).unwrap(); + + // Build a single tx that spends BOTH auth heads + let input_a = TxIn { + previous_output: OutPoint { + txid: prev_txid_a, + vout: 0, + }, + script_sig: Script::new(), + sequence: Sequence(0xFFFF_FFFF), + ..Default::default() + }; + + let input_b = TxIn { + previous_output: OutPoint { + txid: prev_txid_b, + vout: 0, + }, + script_sig: Script::new(), + sequence: Sequence(0xFFFF_FFFF), + ..Default::default() + }; + + let output0 = TxOut { + value: 0, + script_pubkey: Script::new(), + ..Default::default() + }; + + let update_tx = Transaction { + version: 2, + lock_time: PackedLockTime(0), + input: vec![input_a, input_b], + output: vec![output0], + }; + + let update_txid = update_tx.txid(); + + // Index this one tx as if it's in a block + index_bcmr(&db_tx, &bh, vec![update_tx]).unwrap(); + + // ---- ASSERTIONS ---- + + // Expect 2 auth_chain_entry rows for this tx (one per token) + { + let mut stmt = db_tx + .prepare("SELECT COUNT(*) FROM auth_chain_entry WHERE txid = ?") + .unwrap(); + let count: i64 = stmt + .query_row([update_txid.to_hex()], |row| row.get(0)) + .unwrap(); + + assert_eq!( + count, 2, + "Expected 2 auth_chain_entry rows for multi-parent auth update tx" + ); + } + + // Expect each token to have height 1 in this tx + { + let mut stmt = db_tx + .prepare( + "SELECT token_id, height + FROM auth_chain_entry + WHERE txid = ? + ORDER BY token_id", + ) + .unwrap(); + + let mut rows = stmt.query([update_txid.to_hex()]).unwrap(); + let mut seen: Vec<(String, i64)> = Vec::new(); + while let Some(row) = rows.next().unwrap() { + let t: String = row.get(0).unwrap(); + let h: i64 = row.get(1).unwrap(); + seen.push((t, h)); + } + + assert_eq!(seen.len(), 2, "expected 2 rows for update tx"); + + seen.sort_by(|a, b| a.0.cmp(&b.0)); + + assert_eq!(seen[0].0, token_a.to_hex()); + assert_eq!(seen[0].1, 1); + assert_eq!(seen[1].0, token_b.to_hex()); + assert_eq!(seen[1].1, 1); + } + + // No need to commit in the test; we're only asserting state + // db_tx.commit().unwrap(); +} + #[cfg(test)] mod tests { use bitcoincash::{consensus::deserialize, Block}; @@ -427,6 +539,8 @@ mod tests { #[test] fn olando_issue() { + #[ignore] + // Integration test - requires network access. Run with `cargo test olando_issue --ignored` // Test that the indexer is able to follow the auth chain for OLANDO token (part of issue #15). // Map: block height -> expected txid of BCMR auth update transaction diff --git a/src/db/bcmr/mod.rs b/src/db/bcmr/mod.rs index f6b89e0..323daf0 100644 --- a/src/db/bcmr/mod.rs +++ b/src/db/bcmr/mod.rs @@ -27,12 +27,13 @@ pub struct AuthChainEntry { pub fn prepare_tables(conn: &Connection) { conn.execute( "CREATE TABLE auth_chain_entry ( - utxo TEXT PRIMARY KEY, + token_id TEXT NOT NULL, + utxo TEXT NOT NULL, blockhash TEXT NOT NULL, - txid TEXT NOT NULL, - token_id TEXT NOT NULL, - height INT NOT NULL, - bcmr_data TEXT + txid TEXT NOT NULL, + height INT NOT NULL, + bcmr_data TEXT, + PRIMARY KEY (token_id, utxo) )", [], ) @@ -40,17 +41,22 @@ pub fn prepare_tables(conn: &Connection) { conn.execute( "CREATE TABLE bcmr_data ( - utxo TEXT PRIMARY KEY, - symbol TEXT NOT NULL, - decimals INT NOT NULL, - name TEXT NOT NULL, - description TEXT NOT NULL, - icon TEXT NOT NULL, - web TEXT NOT NULL, + token_id TEXT NOT NULL, + utxo TEXT NOT NULL, + symbol TEXT NOT NULL, + decimals INT NOT NULL, + name TEXT NOT NULL, + description TEXT NOT NULL, + icon TEXT NOT NULL, + web TEXT NOT NULL, expected_hash TEXT NOT NULL, - actual_hash TEXT NOT NULL, - FOREIGN KEY (utxo) REFERENCES auth_chain_entry(utxo) ON DELETE CASCADE - )", + actual_hash TEXT NOT NULL, + PRIMARY KEY (token_id, utxo), + FOREIGN KEY (token_id, utxo) + REFERENCES auth_chain_entry(token_id, utxo) + ON DELETE CASCADE + ); + ", [], ) .expect("failed to create bcmr_data table"); @@ -58,13 +64,14 @@ pub fn prepare_tables(conn: &Connection) { // NOTE: PRIMARY KEY (utxo, txid) is required for ON CONFLICT(utxo, txid) conn.execute( "CREATE TABLE bcmr_failure ( - utxo TEXT NOT NULL, - txid TEXT NOT NULL, + token_id TEXT NOT NULL, + utxo TEXT NOT NULL, + txid TEXT NOT NULL, last_attempt INT NOT NULL, - attempts INT NOT NULL, + attempts INT NOT NULL, error_message TEXT, - give_up BOOLEAN, - PRIMARY KEY (utxo, txid) + give_up BOOLEAN, + PRIMARY KEY (token_id, utxo, txid) )", [], ) @@ -94,6 +101,10 @@ pub fn prepare_tables(conn: &Connection) { ) .unwrap(); + // Create index for efficient token BCMR lookups + conn.execute("CREATE INDEX idx_auth_utxo ON auth_chain_entry(utxo);", []) + .expect("failed to create index for token BCMR lookups"); + // Create index for efficient token BCMR lookups conn.execute( "CREATE INDEX IF NOT EXISTS idx_auth_chain_token_bcmr_height @@ -226,9 +237,11 @@ pub fn get_entries_missing_bcmr_download(conn: &Connection) -> Result Result Result<()> { - let sql = "INSERT OR REPLACE INTO bcmr_data (utxo, symbol, decimals, name, description, icon, web, expected_hash, actual_hash) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"; + let sql = "INSERT OR REPLACE INTO bcmr_data ( + token_id, + utxo, + symbol, + decimals, + name, + description, + icon, + web, + expected_hash, + actual_hash + ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; info!("Trying to insert {}", utxo.to_hex()); let empty_string: String = "".to_owned(); + conn.execute( sql, rusqlite::params![ + &token_id.to_hex(), &utxo.to_hex(), &bcmr.token.symbol, bcmr.token.decimals, @@ -297,10 +324,11 @@ pub fn insert_bcmr_data( &bcmr.description, &bcmr.uris.icon.as_ref().unwrap_or(&empty_string), &bcmr.uris.web.as_ref().unwrap_or(&empty_string), - &bcmr.filemeta.expected_hash, - &bcmr.filemeta.actual_hash + &bcmr.filemeta.expected_hash.as_deref().unwrap_or(""), + &bcmr.filemeta.actual_hash.as_deref().unwrap_or(""), ], )?; + Ok(()) } @@ -335,26 +363,34 @@ pub fn update_bcmr_failure( conn: &Connection, utxo: &OutPointHash, txid: &Txid, + token_id: &TokenID, error_message: &str, give_up: bool, ) -> Result<()> { let sql = format!( " - INSERT INTO bcmr_failure (utxo, txid, last_attempt, attempts, error_message, give_up) - VALUES (?1, ?2, strftime('%s','now'), 1, ?3, ?4) - ON CONFLICT(utxo, txid) DO UPDATE SET - last_attempt = excluded.last_attempt, - attempts = bcmr_failure.attempts + 1, - error_message= excluded.error_message, - give_up = CASE - WHEN bcmr_failure.attempts + 1 > {max} THEN 1 - ELSE excluded.give_up - END", - max = MAX_DOWNLOAD_ATTEMPTS + INSERT INTO bcmr_failure (token_id, utxo, txid, last_attempt, attempts, error_message, give_up) + VALUES (?1, ?2, ?3, strftime('%s','now'), 1, ?4, ?5) + ON CONFLICT(token_id, utxo, txid) DO UPDATE SET + last_attempt = excluded.last_attempt, + attempts = bcmr_failure.attempts + 1, + error_message = excluded.error_message, + give_up = CASE + WHEN bcmr_failure.attempts + 1 > {max} THEN 1 + ELSE excluded.give_up + END + ", + max = MAX_DOWNLOAD_ATTEMPTS, ); conn.execute( &sql, - params![&utxo.to_hex(), &txid.to_hex(), error_message, give_up], + params![ + &token_id.to_hex(), + &utxo.to_hex(), + &txid.to_hex(), + error_message, + give_up + ], )?; Ok(()) } @@ -362,18 +398,11 @@ pub fn update_bcmr_failure( pub fn get_token_bcmr(conn: &Connection, token_hex: &str) -> Result> { // Pick *exactly* the head utxo for this token. let sql = r#" - SELECT b.symbol, b.decimals, b.name, b.description, b.icon, b.web, - b.actual_hash, b.expected_hash - FROM bcmr_data b - WHERE b.utxo = ( - SELECT ace.utxo - FROM auth_chain_entry ace - WHERE ace.token_id = ? - AND ace.bcmr_data IS NOT NULL - ORDER BY ace.height DESC - LIMIT 1 - ) - LIMIT 1 + SELECT symbol, decimals, name, description, icon, web, actual_hash, expected_hash + FROM bcmr_data + WHERE token_id = ? + ORDER BY ROWID DESC + LIMIT 1; "#; let mut stmt = conn.prepare(sql)?; diff --git a/src/db/cauldron/tokenlist/test.rs b/src/db/cauldron/tokenlist/test.rs index 78dc1b4..df50576 100644 --- a/src/db/cauldron/tokenlist/test.rs +++ b/src/db/cauldron/tokenlist/test.rs @@ -197,7 +197,7 @@ mod tests { source: "src".to_string(), }, }; - insert_bcmr_data(&rw, &utxo_a, &row_a).unwrap(); + insert_bcmr_data(&rw, &token_a, &utxo_a, &row_a).unwrap(); insert_authheader( &rw, &utxo_a, @@ -1035,7 +1035,7 @@ mod tests { source: "test".into(), }, }; - insert_bcmr_data(&bcmr_w, &utxo, &row).unwrap(); + insert_bcmr_data(&bcmr_w, &token, &utxo, &row).unwrap(); insert_authheader( &bcmr_w, &utxo, @@ -1101,6 +1101,7 @@ mod tests { // decimals = 28 OK, but we’ll make price huge by tiny tokens insert_bcmr_data( &bcmr_w, + &token, &utxo, &BCMRRow { name: "HugePrice".into(), diff --git a/src/db/search/test.rs b/src/db/search/test.rs index 3797059..c31c355 100644 --- a/src/db/search/test.rs +++ b/src/db/search/test.rs @@ -431,8 +431,9 @@ mod tests { let utxo1 = OutPointHash::from_hex( "a3f1d42e2a5c9f2b5f1b9d7f7c2b19e7a3b1d2c3f4a5e6f2d1c3e4f5a1b2c3d4", )?; + let token_id1: TokenID = TokenID::from_inner([0xda; 32]); - // Step 1: Prepare and insert BCMR data for token1 with non-null fields + // BCMR data for Token 1 let token1 = Token { category: "asset_category".to_string(), symbol: "TONE".to_string(), @@ -454,101 +455,72 @@ mod tests { 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 + insert_bcmr_data(conn, &token_id1, &utxo1, &parsed_bcmr1)?; + 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:?}"); - } + insert_new_pool(conn, &cauldron1)?; - // Step 3: Insert auth_chain_entry for token1 - if let Err(e) = insert_authheader( + insert_authheader( conn, &utxo1, - &BlockHash::all_zeros(), + &block_zero, &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:?}"); - } + insert_utxo_funding(conn, &vec![cauldron1.clone()], &txid1, true)?; + insert_block_tx(conn, &txid1, &block_zero, thirty_days_ago)?; + insert_mempool_tx(conn, &txid1, thirty_days_ago as u64)?; - // 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 { + // Simulate volume on token1 + let txid1_spend = Txid::from_inner([0xf1; 32]); + let utxo1_spend = OutPointHash::from_inner([0xe1; 32]); + let cauldron1_spent = ParsedContract { pkh: owner_pkh, is_withdrawn: false, spent_utxo_hash: utxo1, - new_utxo_hash: Some(utxo2), - new_utxo_txid: Some(txid2), + new_utxo_hash: Some(utxo1_spend), + new_utxo_txid: Some(txid1_spend), 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:?}"); - } + insert_utxo_funding(conn, &vec![cauldron1_spent.clone()], &txid1_spend, true)?; + insert_block_tx(conn, &txid1_spend, &block_zero, current_timestamp)?; + insert_mempool_tx(conn, &txid1_spend, current_timestamp as u64)?; - // 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( + insert_pool_history_entry( conn, &utxo1, - &cauldron1.clone(), + &cauldron1, 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( + 0, + 0, + )?; + insert_pool_history_entry( conn, &utxo1, - &cauldron2, + &cauldron1_spent, 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:?}"); - } + 2000, + 1000, + )?; // ================== TOKEN 2 (BCMR Token) ================== let utxo2 = OutPointHash::from_hex( "c3d2e1f4b6a7c8d1e2f5d7c3b1a9e4f2b1d3f4e6c2b9f3a8d1e4f5b6c3d2e7a4", )?; + let token_id2 = TokenID::from_inner([0xdb; 32]); - // Step 1: Prepare and insert BCMR data for token2 with unique values + // BCMR for Token 2 let token2 = Token { category: "asset_category_2".to_string(), symbol: "TWO".to_string(), @@ -570,99 +542,71 @@ mod tests { 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_bcmr_data(conn, &token_id2, &utxo2, &parsed_bcmr2)?; + + let txid2_init = Txid::from_inner([0xf2; 32]); + let cauldron2 = dummy_cauldron(&txid2_init, &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( + insert_authheader( conn, &utxo2, - &BlockHash::all_zeros(), - &txid2, + &block_zero, + &txid2_init, &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)?; + insert_utxo_funding(conn, &vec![cauldron2.clone()], &txid2_init, true)?; + insert_block_tx(conn, &txid2_init, &block_zero, thirty_days_ago)?; + insert_mempool_tx(conn, &txid2_init, 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 { + // Simulate second funding for token2 + let txid2_spend = Txid::from_inner([0xf3; 32]); + let utxo2_spend = OutPointHash::from_inner([0xe2; 32]); + let cauldron2_spent = ParsedContract { pkh: owner_pkh, is_withdrawn: false, spent_utxo_hash: utxo2, - new_utxo_hash: Some(utxo3), - new_utxo_txid: Some(txid3), + new_utxo_hash: Some(utxo2_spend), + new_utxo_txid: Some(txid2_spend), 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)?; + insert_utxo_funding(conn, &vec![cauldron2_spent.clone()], &txid2_spend, true)?; + insert_block_tx(conn, &txid2_spend, &block_zero, current_timestamp)?; + insert_mempool_tx(conn, &txid2_spend, current_timestamp as u64)?; - // 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(), + &cauldron2, 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) + 0, + 0, )?; - - // Step 8: Insert pool history entry for the spending of utxo2 (creation of utxo3) insert_pool_history_entry( conn, &utxo2, - &cauldron3, + &cauldron2_spent, 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) + 3000, + 1500, )?; // ================== TOKEN 3 (BCMR Token with No Volume) ================== let utxo3 = OutPointHash::from_hex( "d5e1f2a3b4c3d2f5b6a8e7d3c2f4b9a6d2e3f1c4b5a9e3d1b2c5f7a3d4b8e2c3", )?; + let token_id3 = TokenID::from_inner([0xdd; 32]); - // Prepare and insert BCMR data for TOKEN 3 with unique values let token3 = Token { category: "asset_category_3".to_string(), symbol: "TN3".to_string(), @@ -685,77 +629,50 @@ mod tests { filemeta: filemeta3, }; - if let Err(e) = insert_bcmr_data(conn, &utxo3, &parsed_bcmr3) { - println!("Failed to insert BCMR data for TOKEN 3: {e:?}"); - } + insert_bcmr_data(conn, &token_id3, &utxo3, &parsed_bcmr3)?; - // 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); + let txid3_init = Txid::from_inner([0xf6; 32]); + let cauldron3_init = dummy_cauldron(&txid3_init, &utxo3, &token_id3, 1000, 500, &owner_pkh); + insert_new_pool(conn, &cauldron3_init)?; - 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( + insert_authheader( conn, &utxo3, - &BlockHash::all_zeros(), - &txid3, + &block_zero, + &txid3_init, &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_utxo_funding(conn, &vec![cauldron3_init.clone()], &txid3_init, true)?; + insert_block_tx(conn, &txid3_init, &block_zero, thirty_days_ago)?; + insert_mempool_tx(conn, &txid3_init, thirty_days_ago as u64)?; - // 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( + insert_pool_history_entry( conn, &utxo3, - &cauldron3_initial, + &cauldron3_init, 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:?}"); - } + 0, + 0, + )?; // ================== 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 + let token_id4 = TokenID::from_inner([0xdc; 32]); - // 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, + let txid4_init = Txid::from_inner([0xf4; 32]); + let cauldron4_init = dummy_cauldron( + &txid4_init, &utxo4_initial, &token_id4, 3600, @@ -763,26 +680,11 @@ mod tests { &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:?}"); - } + insert_new_pool(conn, &cauldron4_init)?; + insert_utxo_funding(conn, &vec![cauldron4_init.clone()], &txid4_init, true)?; + insert_block_tx(conn, &txid4_init, &block_zero, thirty_days_ago)?; + insert_mempool_tx(conn, &txid4_init, thirty_days_ago as u64)?; - 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 { @@ -797,43 +699,28 @@ mod tests { 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_utxo_funding(conn, &vec![cauldron4_spent.clone()], &txid4_spend, true)?; + insert_block_tx(conn, &txid4_spend, &block_zero, current_timestamp)?; + insert_mempool_tx(conn, &txid4_spend, current_timestamp as u64)?; - // 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( + insert_pool_history_entry( conn, &utxo4_initial, - &cauldron4_initial, + &cauldron4_init, 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( + 0, + 0, + )?; + 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:?}"); - } + 5000, + 1500, + )?; Ok(()) }