use std::collections::HashSet;
use bsv_rs::transaction::Beef;
pub fn subject_txid_of(beef: &mut Beef) -> Option<String> {
if let Some(atomic) = beef.atomic_txid.clone() {
if beef
.txs
.iter()
.any(|b| b.txid().eq_ignore_ascii_case(&atomic))
{
return Some(atomic.to_ascii_lowercase());
}
}
let mut spent: HashSet<String> = HashSet::new();
for b in &beef.txs {
if let Some(tx) = b.tx() {
for input in &tx.inputs {
let src = input
.source_txid
.clone()
.or_else(|| input.source_transaction.as_ref().map(|t| t.id()));
if let Some(src) = src {
spent.insert(src.to_ascii_lowercase());
}
}
}
}
let tips: Vec<String> = beef
.txs
.iter()
.filter(|b| b.tx().is_some())
.map(|b| b.txid().to_ascii_lowercase())
.filter(|t| !spent.contains(t))
.collect();
if tips.len() == 1 {
return tips.into_iter().next();
}
beef.sort_txs();
beef.txs
.last()
.filter(|b| b.tx().is_some())
.map(|b| b.txid().to_ascii_lowercase())
}
pub fn sorted_last_txid_of(beef: &Beef) -> Option<String> {
let mut sorted = beef.clone();
sorted.sort_txs();
sorted.txs.last().map(|b| b.txid().to_ascii_lowercase())
}
#[cfg(test)]
mod tests {
use super::*;
use bsv_rs::script::LockingScript;
use bsv_rs::transaction::{Transaction, TransactionInput, TransactionOutput};
fn stray(seed: u8) -> Transaction {
let mut tx = Transaction::new();
tx.inputs.push(TransactionInput {
source_txid: Some(format!("{seed:02x}").repeat(32)),
source_output_index: 0,
..Default::default()
});
tx.outputs.push(TransactionOutput::new(
1_000,
LockingScript::from_hex("51").unwrap(),
));
tx
}
fn child_of(parent: &Transaction) -> Transaction {
let mut tx = Transaction::new();
tx.inputs.push(TransactionInput {
source_txid: Some(parent.id()),
source_output_index: 0,
..Default::default()
});
tx.outputs.push(TransactionOutput::new(
900,
LockingScript::from_hex("52").unwrap(),
));
tx
}
#[test]
fn the_unique_tip_is_the_subject_whatever_the_wire_order() {
let parent = stray(0xaa);
let child = child_of(&parent);
for order in [
vec![parent.clone(), child.clone()],
vec![child.clone(), parent.clone()],
] {
let mut beef = Beef::new();
for tx in order {
beef.merge_transaction(tx);
}
let mut parsed = Beef::from_binary(&beef.to_binary()).unwrap();
assert_eq!(
subject_txid_of(&mut parsed).as_deref(),
Some(child.id().as_str())
);
}
}
#[test]
fn an_atomic_name_wins_and_a_stray_name_falls_to_the_tip() {
let parent = stray(0xbb);
let child = child_of(&parent);
let mut beef = Beef::new();
beef.merge_transaction(parent.clone());
beef.merge_transaction(child.clone());
let atomic = beef.to_binary_atomic(&parent.id()).unwrap();
let mut named = Beef::from_binary(&atomic).unwrap();
assert!(named.is_atomic());
assert_eq!(
subject_txid_of(&mut named).as_deref(),
Some(parent.id().as_str()),
"the name wins"
);
let mut stray_named = Beef::from_binary(&atomic).unwrap();
stray_named.atomic_txid = Some("00".repeat(32));
assert_eq!(
subject_txid_of(&mut stray_named).as_deref(),
Some(child.id().as_str()),
"a name not in the body is ignored"
);
}
#[test]
fn two_tips_fall_back_to_the_reference_sorted_last() {
let (a, b) = (stray(0xcc), stray(0xdd));
let mut two = Beef::new();
two.merge_transaction(a);
two.merge_transaction(b);
let expect = sorted_last_txid_of(&two);
assert!(expect.is_some());
assert_eq!(subject_txid_of(&mut two), expect);
assert!(subject_txid_of(&mut Beef::new()).is_none());
}
}