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,
&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}")
}
});

View file

@ -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<u8>,
}
fn find_parent_auth_entry(conn: &Connection, tx: &Transaction) -> Result<Option<AuthChainEntry>> {
let mut best: Option<AuthChainEntry> = None;
fn find_parent_auth_entries(conn: &Connection, tx: &Transaction) -> Result<Vec<AuthChainEntry>> {
let mut parents = Vec::new();
for vin in &tx.input {
// BCMR auth chain is always the previous txs 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<String> = 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<BCMR> {
@ -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

View file

@ -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<Vec<AuthCh
FROM ranked r
LEFT JOIN bcmr_data bd
ON bd.utxo = r.utxo
AND bd.token_id = r.token_id
LEFT JOIN bcmr_failure bf
ON bf.utxo = r.utxo
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)
WHERE r.rn = 1
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(
conn: &rusqlite::Connection,
token_id: &TokenID,
utxo: &OutPointHash,
bcmr: &ParsedBCMR,
) -> 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<Option<ParsedBCMR>> {
// 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)?;

View file

@ -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 well make price huge by tiny tokens
insert_bcmr_data(
&bcmr_w,
&token,
&utxo,
&BCMRRow {
name: "HugePrice".into(),

View file

@ -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(())
}