Merge branch 'manage-several-entries-one-txid' into 'master'

Manage several entries one txid

See merge request riftenlabs/riftenlabs-indexer!44
This commit is contained in:
jakobsn 2025-11-17 09:21:51 +00:00
commit 1b7801dff8
5 changed files with 334 additions and 299 deletions

View file

@ -192,6 +192,7 @@ impl BCMRDownloader {
&conn, &conn,
&entry.utxo, &entry.utxo,
&entry.txid, &entry.txid,
&entry.token_id,
"Invalid BCMR OP_RETURN in DB", "Invalid BCMR OP_RETURN in DB",
true, true,
) { ) {
@ -211,6 +212,7 @@ impl BCMRDownloader {
&conn, &conn,
&entry.utxo, &entry.utxo,
&entry.txid, &entry.txid,
&entry.token_id,
&format!("Failed to fetch BCMR: {error}"), &format!("Failed to fetch BCMR: {error}"),
is_fatal, is_fatal,
) { ) {
@ -227,6 +229,7 @@ impl BCMRDownloader {
&conn, &conn,
&entry.utxo, &entry.utxo,
&entry.txid, &entry.txid,
&entry.token_id,
&format!("BCMR invalid JSON error: {e}"), &format!("BCMR invalid JSON error: {e}"),
true, true,
) { ) {
@ -249,6 +252,7 @@ impl BCMRDownloader {
&conn, &conn,
&entry.utxo, &entry.utxo,
&entry.txid, &entry.txid,
&entry.token_id,
&format!("BCMR contents error: {err}"), &format!("BCMR contents error: {err}"),
true, 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}") warn!("bcmr: Failed to insert BCMR data {err}")
} }
}); });

View file

@ -6,7 +6,7 @@
use rayon::prelude::*; use rayon::prelude::*;
use std::convert::TryInto; use std::convert::TryInto;
use anyhow::{Context, Result}; use anyhow::Result;
use bitcoin_hashes::{hex::FromHex, hex::ToHex, Hash}; use bitcoin_hashes::{hex::FromHex, hex::ToHex, Hash};
use bitcoincash::{BlockHash, Script, TokenID, Transaction, Txid}; use bitcoincash::{BlockHash, Script, TokenID, Transaction, Txid};
use log::debug; use log::debug;
@ -51,65 +51,50 @@ pub struct BCMR {
pub op_return: Vec<u8>, pub op_return: Vec<u8>,
} }
fn find_parent_auth_entry(conn: &Connection, tx: &Transaction) -> Result<Option<AuthChainEntry>> { fn find_parent_auth_entries(conn: &Connection, tx: &Transaction) -> Result<Vec<AuthChainEntry>> {
let mut best: Option<AuthChainEntry> = None; let mut parents = Vec::new();
for vin in &tx.input { for vin in &tx.input {
// BCMR auth chain is always the previous txs vout 0
if vin.previous_output.vout != 0 { if vin.previous_output.vout != 0 {
continue; continue;
} }
let prev = &vin.previous_output; let prev = &vin.previous_output;
let prev_utxo: OutPointHash = compute_outpoint_hash(&prev.txid, prev.vout); let prev_utxo: OutPointHash = compute_outpoint_hash(&prev.txid, prev.vout);
let mut stmt = conn let mut stmt = conn.prepare(
.prepare(
"SELECT token_id, txid, height, bcmr_data, utxo "SELECT token_id, txid, height, bcmr_data, utxo
FROM auth_chain_entry FROM auth_chain_entry
WHERE utxo = ?1 WHERE utxo = ?1",
LIMIT 1", )?;
) let mut rows = stmt.query(params![prev_utxo.to_hex()])?;
.context("prepare SELECT parent auth_chain_entry by utxo")?;
let mut rows = stmt while let Some(row) = rows.next()? {
.query(params![prev_utxo.to_hex()])
.context("query parent auth_chain_entry by utxo")?;
if let Some(row) = rows.next().context("iterate parent rows")? {
let token_hex: String = row.get(0)?; let token_hex: String = row.get(0)?;
let txid_hex: String = row.get(1)?; let txid_hex: String = row.get(1)?;
let height: usize = row.get(2)?; let height: usize = row.get(2)?;
let bcmr_data_hex: Option<String> = row.get(3)?; let bcmr_data_hex: Option<String> = row.get(3)?;
let utxo_hex: String = row.get(4)?; let utxo_hex: String = row.get(4)?;
let token_id = TokenID::from_hex(&token_hex).context("parse token_id from hex")?; let token_id = TokenID::from_hex(&token_hex)?;
let txid = Txid::from_hex(&txid_hex).context("parse txid from hex")?; let txid = Txid::from_hex(&txid_hex)?;
let utxo = OutPointHash::from_hex(&utxo_hex).context("parse utxo from hex")?; let utxo = OutPointHash::from_hex(&utxo_hex)?;
let bcmr_data = if let Some(h) = bcmr_data_hex { let bcmr_data = match bcmr_data_hex {
Some(hex::decode(&h).context("decode bcmr_data hex")?) Some(h) => Some(hex::decode(&h)?),
} else { None => None,
None
}; };
let candidate = AuthChainEntry { parents.push(AuthChainEntry {
utxo, utxo,
token_id, token_id,
txid, txid,
height, height,
bcmr_data, 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<BCMR> { pub fn parse_bcmr_from_opreturn(bcmr_op_return: &Script) -> Option<BCMR> {
@ -213,13 +198,18 @@ pub fn index_bcmr(
continue; continue;
} }
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 // 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 bcmr = parse_bcmr(tx);
let txid = tx.txid(); let txid = tx.txid();
let new_utxo = compute_outpoint_hash(&txid, 0); let new_utxo = compute_outpoint_hash(&txid, 0);
let new_height = parent.height + 1;
for parent in parents {
let new_height = parent.height + 1;
debug!( debug!(
"Found authheader update {} in {}; has bcmr: {}; new height: {}, utxo: {}", "Found authheader update {} in {}; has bcmr: {}; new height: {}, utxo: {}",
parent.token_id.to_hex(), parent.token_id.to_hex(),
@ -236,16 +226,138 @@ pub fn index_bcmr(
&txid, &txid,
&parent.token_id, &parent.token_id,
new_height, new_height,
bcmr.map(|b| b.op_return), bcmr.as_ref().map(|b| b.op_return.clone()),
)?; )?;
inserts += 1; inserts += 1;
} }
// else: not a BCMR auth-chain tx → ignore
} }
Ok(inserts) 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)] #[cfg(test)]
mod tests { mod tests {
use bitcoincash::{consensus::deserialize, Block}; use bitcoincash::{consensus::deserialize, Block};
@ -427,6 +539,8 @@ mod tests {
#[test] #[test]
fn olando_issue() { 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). // 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 // Map: block height -> expected txid of BCMR auth update transaction

View file

@ -27,12 +27,13 @@ pub struct AuthChainEntry {
pub fn prepare_tables(conn: &Connection) { pub fn prepare_tables(conn: &Connection) {
conn.execute( conn.execute(
"CREATE TABLE auth_chain_entry ( "CREATE TABLE auth_chain_entry (
utxo TEXT PRIMARY KEY, token_id TEXT NOT NULL,
utxo TEXT NOT NULL,
blockhash TEXT NOT NULL, blockhash TEXT NOT NULL,
txid TEXT NOT NULL, txid TEXT NOT NULL,
token_id TEXT NOT NULL,
height INT NOT NULL, height INT NOT NULL,
bcmr_data TEXT bcmr_data TEXT,
PRIMARY KEY (token_id, utxo)
)", )",
[], [],
) )
@ -40,7 +41,8 @@ pub fn prepare_tables(conn: &Connection) {
conn.execute( conn.execute(
"CREATE TABLE bcmr_data ( "CREATE TABLE bcmr_data (
utxo TEXT PRIMARY KEY, token_id TEXT NOT NULL,
utxo TEXT NOT NULL,
symbol TEXT NOT NULL, symbol TEXT NOT NULL,
decimals INT NOT NULL, decimals INT NOT NULL,
name TEXT NOT NULL, name TEXT NOT NULL,
@ -49,8 +51,12 @@ pub fn prepare_tables(conn: &Connection) {
web TEXT NOT NULL, web TEXT NOT NULL,
expected_hash TEXT NOT NULL, expected_hash TEXT NOT NULL,
actual_hash TEXT NOT NULL, actual_hash TEXT NOT NULL,
FOREIGN KEY (utxo) REFERENCES auth_chain_entry(utxo) ON DELETE CASCADE 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"); .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) // NOTE: PRIMARY KEY (utxo, txid) is required for ON CONFLICT(utxo, txid)
conn.execute( conn.execute(
"CREATE TABLE bcmr_failure ( "CREATE TABLE bcmr_failure (
token_id TEXT NOT NULL,
utxo TEXT NOT NULL, utxo TEXT NOT NULL,
txid TEXT NOT NULL, txid TEXT NOT NULL,
last_attempt INT NOT NULL, last_attempt INT NOT NULL,
attempts INT NOT NULL, attempts INT NOT NULL,
error_message TEXT, error_message TEXT,
give_up BOOLEAN, give_up BOOLEAN,
PRIMARY KEY (utxo, txid) PRIMARY KEY (token_id, utxo, txid)
)", )",
[], [],
) )
@ -94,6 +101,10 @@ pub fn prepare_tables(conn: &Connection) {
) )
.unwrap(); .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 // Create index for efficient token BCMR lookups
conn.execute( conn.execute(
"CREATE INDEX IF NOT EXISTS idx_auth_chain_token_bcmr_height "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<Vec<AuthCh
FROM ranked r FROM ranked r
LEFT JOIN bcmr_data bd LEFT JOIN bcmr_data bd
ON bd.utxo = r.utxo ON bd.utxo = r.utxo
AND bd.token_id = r.token_id
LEFT JOIN bcmr_failure bf LEFT JOIN bcmr_failure bf
ON bf.utxo = r.utxo ON bf.utxo = r.utxo
AND bf.txid = r.txid AND bf.txid = r.txid
AND bf.token_id = r.token_id
AND (bf.give_up = 1 OR (strftime('%s','now') - bf.last_attempt) < 1800) AND (bf.give_up = 1 OR (strftime('%s','now') - bf.last_attempt) < 1800)
WHERE r.rn = 1 WHERE r.rn = 1
AND bd.utxo IS NULL AND bd.utxo IS NULL
@ -279,17 +292,31 @@ pub fn get_entries_missing_bcmr_download(conn: &Connection) -> Result<Vec<AuthCh
pub fn insert_bcmr_data( pub fn insert_bcmr_data(
conn: &rusqlite::Connection, conn: &rusqlite::Connection,
token_id: &TokenID,
utxo: &OutPointHash, utxo: &OutPointHash,
bcmr: &ParsedBCMR, bcmr: &ParsedBCMR,
) -> Result<()> { ) -> Result<()> {
let sql = "INSERT OR REPLACE INTO bcmr_data (utxo, symbol, decimals, name, description, icon, web, expected_hash, actual_hash) let sql = "INSERT OR REPLACE INTO bcmr_data (
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"; token_id,
utxo,
symbol,
decimals,
name,
description,
icon,
web,
expected_hash,
actual_hash
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
info!("Trying to insert {}", utxo.to_hex()); info!("Trying to insert {}", utxo.to_hex());
let empty_string: String = "".to_owned(); let empty_string: String = "".to_owned();
conn.execute( conn.execute(
sql, sql,
rusqlite::params![ rusqlite::params![
&token_id.to_hex(),
&utxo.to_hex(), &utxo.to_hex(),
&bcmr.token.symbol, &bcmr.token.symbol,
bcmr.token.decimals, bcmr.token.decimals,
@ -297,10 +324,11 @@ pub fn insert_bcmr_data(
&bcmr.description, &bcmr.description,
&bcmr.uris.icon.as_ref().unwrap_or(&empty_string), &bcmr.uris.icon.as_ref().unwrap_or(&empty_string),
&bcmr.uris.web.as_ref().unwrap_or(&empty_string), &bcmr.uris.web.as_ref().unwrap_or(&empty_string),
&bcmr.filemeta.expected_hash, &bcmr.filemeta.expected_hash.as_deref().unwrap_or(""),
&bcmr.filemeta.actual_hash &bcmr.filemeta.actual_hash.as_deref().unwrap_or(""),
], ],
)?; )?;
Ok(()) Ok(())
} }
@ -335,26 +363,34 @@ pub fn update_bcmr_failure(
conn: &Connection, conn: &Connection,
utxo: &OutPointHash, utxo: &OutPointHash,
txid: &Txid, txid: &Txid,
token_id: &TokenID,
error_message: &str, error_message: &str,
give_up: bool, give_up: bool,
) -> Result<()> { ) -> Result<()> {
let sql = format!( let sql = format!(
" "
INSERT INTO bcmr_failure (utxo, txid, last_attempt, attempts, error_message, give_up) INSERT INTO bcmr_failure (token_id, utxo, txid, last_attempt, attempts, error_message, give_up)
VALUES (?1, ?2, strftime('%s','now'), 1, ?3, ?4) VALUES (?1, ?2, ?3, strftime('%s','now'), 1, ?4, ?5)
ON CONFLICT(utxo, txid) DO UPDATE SET ON CONFLICT(token_id, utxo, txid) DO UPDATE SET
last_attempt = excluded.last_attempt, last_attempt = excluded.last_attempt,
attempts = bcmr_failure.attempts + 1, attempts = bcmr_failure.attempts + 1,
error_message = excluded.error_message, error_message = excluded.error_message,
give_up = CASE give_up = CASE
WHEN bcmr_failure.attempts + 1 > {max} THEN 1 WHEN bcmr_failure.attempts + 1 > {max} THEN 1
ELSE excluded.give_up ELSE excluded.give_up
END", END
max = MAX_DOWNLOAD_ATTEMPTS ",
max = MAX_DOWNLOAD_ATTEMPTS,
); );
conn.execute( conn.execute(
&sql, &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(()) Ok(())
} }
@ -362,18 +398,11 @@ pub fn update_bcmr_failure(
pub fn get_token_bcmr(conn: &Connection, token_hex: &str) -> Result<Option<ParsedBCMR>> { pub fn get_token_bcmr(conn: &Connection, token_hex: &str) -> Result<Option<ParsedBCMR>> {
// Pick *exactly* the head utxo for this token. // Pick *exactly* the head utxo for this token.
let sql = r#" let sql = r#"
SELECT b.symbol, b.decimals, b.name, b.description, b.icon, b.web, SELECT symbol, decimals, name, description, icon, web, actual_hash, expected_hash
b.actual_hash, b.expected_hash FROM bcmr_data
FROM bcmr_data b WHERE token_id = ?
WHERE b.utxo = ( ORDER BY ROWID DESC
SELECT ace.utxo LIMIT 1;
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
"#; "#;
let mut stmt = conn.prepare(sql)?; let mut stmt = conn.prepare(sql)?;

View file

@ -197,7 +197,7 @@ mod tests {
source: "src".to_string(), 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( insert_authheader(
&rw, &rw,
&utxo_a, &utxo_a,
@ -1035,7 +1035,7 @@ mod tests {
source: "test".into(), source: "test".into(),
}, },
}; };
insert_bcmr_data(&bcmr_w, &utxo, &row).unwrap(); insert_bcmr_data(&bcmr_w, &token, &utxo, &row).unwrap();
insert_authheader( insert_authheader(
&bcmr_w, &bcmr_w,
&utxo, &utxo,
@ -1101,6 +1101,7 @@ mod tests {
// decimals = 28 OK, but well make price huge by tiny tokens // decimals = 28 OK, but well make price huge by tiny tokens
insert_bcmr_data( insert_bcmr_data(
&bcmr_w, &bcmr_w,
&token,
&utxo, &utxo,
&BCMRRow { &BCMRRow {
name: "HugePrice".into(), name: "HugePrice".into(),

View file

@ -431,8 +431,9 @@ mod tests {
let utxo1 = OutPointHash::from_hex( let utxo1 = OutPointHash::from_hex(
"a3f1d42e2a5c9f2b5f1b9d7f7c2b19e7a3b1d2c3f4a5e6f2d1c3e4f5a1b2c3d4", "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 { let token1 = Token {
category: "asset_category".to_string(), category: "asset_category".to_string(),
symbol: "TONE".to_string(), symbol: "TONE".to_string(),
@ -454,101 +455,72 @@ mod tests {
uris: uris1, uris: uris1,
filemeta: filemeta1, 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 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); let cauldron1 = dummy_cauldron(&txid1, &utxo1, &token_id1, 1000, 500, &owner_pkh);
if let Err(e) = insert_new_pool(conn, &cauldron1) { insert_new_pool(conn, &cauldron1)?;
println!("Failed to insert pool for token1: {e:?}");
}
// Step 3: Insert auth_chain_entry for token1 insert_authheader(
if let Err(e) = insert_authheader(
conn, conn,
&utxo1, &utxo1,
&BlockHash::all_zeros(), &block_zero,
&txid1, &txid1,
&token_id1, &token_id1,
10, 10,
Some(Vec::from("bcmr_data1".as_bytes())), 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 insert_utxo_funding(conn, &vec![cauldron1.clone()], &txid1, true)?;
if let Err(e) = insert_utxo_funding(conn, &vec![cauldron1.clone()], &txid1, true) { insert_block_tx(conn, &txid1, &block_zero, thirty_days_ago)?;
println!("Failed to insert initial UTXO funding for token1: {e:?}"); insert_mempool_tx(conn, &txid1, thirty_days_ago as u64)?;
}
// Step 5: Insert the transaction for utxo1 // Simulate volume on token1
if let Err(e) = insert_block_tx(conn, &txid1, &block_zero, thirty_days_ago) { let txid1_spend = Txid::from_inner([0xf1; 32]);
println!("Failed to insert block transaction for utxo1: {e:?}"); let utxo1_spend = OutPointHash::from_inner([0xe1; 32]);
} let cauldron1_spent = ParsedContract {
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, pkh: owner_pkh,
is_withdrawn: false, is_withdrawn: false,
spent_utxo_hash: utxo1, spent_utxo_hash: utxo1,
new_utxo_hash: Some(utxo2), new_utxo_hash: Some(utxo1_spend),
new_utxo_txid: Some(txid2), new_utxo_txid: Some(txid1_spend),
new_utxo_n: Some(0), new_utxo_n: Some(0),
token_id: Some(token_id1), token_id: Some(token_id1),
sats: Some(2000), sats: Some(2000),
token_amount: Some(1000), token_amount: Some(1000),
}; };
if let Err(e) = insert_utxo_funding(conn, &vec![cauldron2.clone()], &txid2, true) { insert_utxo_funding(conn, &vec![cauldron1_spent.clone()], &txid1_spend, true)?;
println!("Failed to insert second UTXO funding for token1: {e:?}"); 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) insert_pool_history_entry(
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, conn,
&utxo1, &utxo1,
&cauldron1.clone(), &cauldron1,
Some(thirty_days_ago as u64), Some(thirty_days_ago as u64),
Some(thirty_days_ago as u64), Some(thirty_days_ago as u64),
0, // sats_delta for initial funding (no trading activity) 0,
0, // token_delta for initial funding (no trading activity) 0,
) { )?;
println!("Failed to insert pool history entry for initial funding of token1: {e:?}"); insert_pool_history_entry(
}
if let Err(e) = insert_pool_history_entry(
conn, conn,
&utxo1, &utxo1,
&cauldron2, &cauldron1_spent,
Some(current_timestamp as u64), Some(current_timestamp as u64),
Some(current_timestamp as u64), Some(current_timestamp as u64),
2000, // sats_delta for spending (actual trading activity) 2000,
1000, // token_delta for spending (actual trading activity) 1000,
) { )?;
println!("Failed to insert pool history entry for spending of token1: {e:?}");
}
// ================== TOKEN 2 (BCMR Token) ================== // ================== TOKEN 2 (BCMR Token) ==================
let utxo2 = OutPointHash::from_hex( let utxo2 = OutPointHash::from_hex(
"c3d2e1f4b6a7c8d1e2f5d7c3b1a9e4f2b1d3f4e6c2b9f3a8d1e4f5b6c3d2e7a4", "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 { let token2 = Token {
category: "asset_category_2".to_string(), category: "asset_category_2".to_string(),
symbol: "TWO".to_string(), symbol: "TWO".to_string(),
@ -570,99 +542,71 @@ mod tests {
uris: uris2, uris: uris2,
filemeta: filemeta2, 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 insert_bcmr_data(conn, &token_id2, &utxo2, &parsed_bcmr2)?;
let token_id2 = TokenID::from_inner([0xdb; 32]);
let cauldron2 = dummy_cauldron( let txid2_init = Txid::from_inner([0xf2; 32]);
&Txid::from_inner([0xf2; 32]), let cauldron2 = dummy_cauldron(&txid2_init, &utxo2, &token_id2, 1500, 700, &owner_pkh);
&utxo2,
&token_id2,
1500,
700,
&owner_pkh,
);
insert_new_pool(conn, &cauldron2)?; insert_new_pool(conn, &cauldron2)?;
// Step 9: Insert auth_chain_entry for token2 insert_authheader(
if let Err(e) = insert_authheader(
conn, conn,
&utxo2, &utxo2,
&BlockHash::all_zeros(), &block_zero,
&txid2, &txid2_init,
&token_id2, &token_id2,
15, 15,
Some(Vec::from("bcmr_data2".as_bytes())), 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 insert_utxo_funding(conn, &vec![cauldron2.clone()], &txid2_init, true)?;
let txid2 = Txid::from_inner([0xf2; 32]); insert_block_tx(conn, &txid2_init, &block_zero, thirty_days_ago)?;
insert_block_tx(conn, &txid2, &block_zero, thirty_days_ago)?; insert_mempool_tx(conn, &txid2_init, thirty_days_ago as u64)?;
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 // Simulate second funding for token2
let txid3 = Txid::from_inner([0xf3; 32]); let txid2_spend = Txid::from_inner([0xf3; 32]);
let utxo3 = OutPointHash::from_inner([0xe2; 32]); let utxo2_spend = OutPointHash::from_inner([0xe2; 32]);
let cauldron2_spent = ParsedContract {
let cauldron3 = ParsedContract {
pkh: owner_pkh, pkh: owner_pkh,
is_withdrawn: false, is_withdrawn: false,
spent_utxo_hash: utxo2, spent_utxo_hash: utxo2,
new_utxo_hash: Some(utxo3), new_utxo_hash: Some(utxo2_spend),
new_utxo_txid: Some(txid3), new_utxo_txid: Some(txid2_spend),
new_utxo_n: Some(0), new_utxo_n: Some(0),
token_id: Some(token_id2), token_id: Some(token_id2),
sats: Some(3000), sats: Some(3000),
token_amount: Some(1500), token_amount: Some(1500),
}; };
// Insert the second funding with `spent_utxo_hash` correctly set insert_utxo_funding(conn, &vec![cauldron2_spent.clone()], &txid2_spend, true)?;
insert_utxo_funding(conn, &vec![cauldron3.clone()], &txid3, 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( insert_pool_history_entry(
conn, conn,
&utxo2, &utxo2,
&cauldron2.clone(), &cauldron2,
Some(thirty_days_ago as u64), Some(thirty_days_ago as u64),
Some(thirty_days_ago as u64), Some(thirty_days_ago as u64),
0, // sats_delta for initial funding (no trading activity) 0,
0, // token_delta for initial funding (no trading activity) 0,
)?; )?;
// Step 8: Insert pool history entry for the spending of utxo2 (creation of utxo3)
insert_pool_history_entry( insert_pool_history_entry(
conn, conn,
&utxo2, &utxo2,
&cauldron3, &cauldron2_spent,
Some(current_timestamp as u64), Some(current_timestamp as u64),
Some(current_timestamp as u64), Some(current_timestamp as u64),
3000, // sats_delta for spending (actual trading activity) 3000,
1500, // token_delta for spending (actual trading activity) 1500,
)?; )?;
// ================== TOKEN 3 (BCMR Token with No Volume) ================== // ================== TOKEN 3 (BCMR Token with No Volume) ==================
let utxo3 = OutPointHash::from_hex( let utxo3 = OutPointHash::from_hex(
"d5e1f2a3b4c3d2f5b6a8e7d3c2f4b9a6d2e3f1c4b5a9e3d1b2c5f7a3d4b8e2c3", "d5e1f2a3b4c3d2f5b6a8e7d3c2f4b9a6d2e3f1c4b5a9e3d1b2c5f7a3d4b8e2c3",
)?; )?;
let token_id3 = TokenID::from_inner([0xdd; 32]);
// Prepare and insert BCMR data for TOKEN 3 with unique values
let token3 = Token { let token3 = Token {
category: "asset_category_3".to_string(), category: "asset_category_3".to_string(),
symbol: "TN3".to_string(), symbol: "TN3".to_string(),
@ -685,77 +629,50 @@ mod tests {
filemeta: filemeta3, filemeta: filemeta3,
}; };
if let Err(e) = insert_bcmr_data(conn, &utxo3, &parsed_bcmr3) { insert_bcmr_data(conn, &token_id3, &utxo3, &parsed_bcmr3)?;
println!("Failed to insert BCMR data for TOKEN 3: {e:?}");
}
// Insert Pool for TOKEN 3 let txid3_init = Txid::from_inner([0xf6; 32]);
let txid3_initial = Txid::from_inner([0xf6; 32]); let cauldron3_init = dummy_cauldron(&txid3_init, &utxo3, &token_id3, 1000, 500, &owner_pkh);
let token_id3 = TokenID::from_inner([0xdd; 32]); insert_new_pool(conn, &cauldron3_init)?;
let cauldron3_initial =
dummy_cauldron(&txid3_initial, &utxo3, &token_id3, 1000, 500, &owner_pkh);
if let Err(e) = insert_new_pool(conn, &cauldron3_initial) { insert_authheader(
println!("Failed to insert pool for TOKEN 3: {e:?}");
}
// Insert auth_chain_entry for TOKEN 3
if let Err(e) = insert_authheader(
conn, conn,
&utxo3, &utxo3,
&BlockHash::all_zeros(), &block_zero,
&txid3, &txid3_init,
&token_id3, &token_id3,
20, 20,
Some(Vec::from("bcmr_data3".as_bytes())), 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) insert_utxo_funding(conn, &vec![cauldron3_init.clone()], &txid3_init, true)?;
if let Err(e) = insert_block_tx(conn, &txid3_init, &block_zero, thirty_days_ago)?;
insert_utxo_funding(conn, &vec![cauldron3_initial.clone()], &txid3_initial, true) insert_mempool_tx(conn, &txid3_init, thirty_days_ago as u64)?;
{
println!("Failed to insert initial UTXO funding for TOKEN 3: {e:?}");
}
// Insert transaction for the initial funding without spending insert_pool_history_entry(
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, conn,
&utxo3, &utxo3,
&cauldron3_initial, &cauldron3_init,
Some(thirty_days_ago as u64), Some(thirty_days_ago as u64),
Some(thirty_days_ago as u64), Some(thirty_days_ago as u64),
0, // sats_delta for initial funding (no trading activity) 0,
0, // token_delta for initial funding (no trading activity) 0,
) { )?;
println!("Failed to insert pool history entry for TOKEN 3 initial funding: {e:?}");
}
// ================== TOKEN 4 (CRC20 Token with Volume) ================== // ================== TOKEN 4 (CRC20 Token with Volume) ==================
let utxo4_initial = OutPointHash::from_hex( let utxo4_initial = OutPointHash::from_hex(
"f1d4e2a3b5c4d2f7b6a8e7d3c2f4b9a6d3e1f1c4b7a8e2d3b6c7f9a5d4b1e6c3", "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( conn.execute(
"INSERT INTO crc20 (token_id, name, symbol, decimals) VALUES (?, ?, ?, ?)", "INSERT INTO crc20 (token_id, name, symbol, decimals) VALUES (?, ?, ?, ?)",
params![&token_id4.to_hex(), "TokenFour", "TFOUR", 18], params![&token_id4.to_hex(), "TokenFour", "TFOUR", 18],
)?; )?;
// Step 1: Insert initial funding for TOKEN 4 let txid4_init = Txid::from_inner([0xf4; 32]);
let txid4_initial = Txid::from_inner([0xf4; 32]); let cauldron4_init = dummy_cauldron(
let cauldron4_initial = dummy_cauldron( &txid4_init,
&txid4_initial,
&utxo4_initial, &utxo4_initial,
&token_id4, &token_id4,
3600, 3600,
@ -763,26 +680,11 @@ mod tests {
&owner_pkh, &owner_pkh,
); );
// Insert pool for TOKEN 4 insert_new_pool(conn, &cauldron4_init)?;
if let Err(e) = insert_new_pool(conn, &cauldron4_initial) { insert_utxo_funding(conn, &vec![cauldron4_init.clone()], &txid4_init, true)?;
println!("Failed to insert pool for TOKEN 4: {e:?}"); 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 txid4_spend = Txid::from_inner([0xf5; 32]);
let utxo4_spent = OutPointHash::from_inner([0xe4; 32]); let utxo4_spent = OutPointHash::from_inner([0xe4; 32]);
let cauldron4_spent = ParsedContract { let cauldron4_spent = ParsedContract {
@ -797,43 +699,28 @@ mod tests {
token_amount: Some(1500), token_amount: Some(1500),
}; };
if let Err(e) = insert_utxo_funding(conn, &vec![cauldron4_spent.clone()], &txid4_spend, true)?;
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)?;
println!("Failed to insert spent UTXO funding for TOKEN 4: {e:?}");
}
// Insert transaction for the spending UTXO insert_pool_history_entry(
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, conn,
&utxo4_initial, &utxo4_initial,
&cauldron4_initial, &cauldron4_init,
Some(thirty_days_ago as u64), Some(thirty_days_ago as u64),
Some(thirty_days_ago as u64), Some(thirty_days_ago as u64),
0, // sats_delta for initial funding (no trading activity) 0,
0, // token_delta for initial funding (no trading activity) 0,
) { )?;
println!("Failed to insert pool history entry for TOKEN 4 initial funding: {e:?}"); insert_pool_history_entry(
}
if let Err(e) = insert_pool_history_entry(
conn, conn,
&utxo4_initial, &utxo4_initial,
&cauldron4_spent, &cauldron4_spent,
Some(current_timestamp as u64), Some(current_timestamp as u64),
Some(current_timestamp as u64), Some(current_timestamp as u64),
5000, // sats_delta for spending (actual trading activity) 5000,
1500, // token_delta for spending (actual trading activity) 1500,
) { )?;
println!("Failed to insert pool history entry for TOKEN 4 spending: {e:?}");
}
Ok(()) Ok(())
} }