diff --git a/crates/chain/tests/test_indexed_tx_graph.rs b/crates/chain/tests/test_indexed_tx_graph.rs index 4014829e2..dbd7fd302 100644 --- a/crates/chain/tests/test_indexed_tx_graph.rs +++ b/crates/chain/tests/test_indexed_tx_graph.rs @@ -520,3 +520,147 @@ fn test_list_owned_txouts() { ); } } + +/// Given a `LocalChain`, `IndexedTxGraph`, and a `Transaction`, when we insert some anchor +/// (possibly non-canonical) and/or a last-seen timestamp into the graph, we expect the +/// result of `get_chain_position` in these cases: +/// +/// - tx with no anchors or last_seen has no `ChainPosition` +/// - tx with any last_seen will be `Unconfirmed` +/// - tx with an anchor in best chain will be `Confirmed` +/// - tx with an anchor not in best chain (no last_seen) has no `ChainPosition` +#[test] +fn test_get_chain_position() { + use bdk_chain::local_chain::CheckPoint; + use bdk_chain::BlockId; + use bdk_chain::SpkTxOutIndex; + + struct TestCase { + name: &'static str, + tx: Transaction, + anchor: Option, + last_seen: Option, + exp_pos: Option>, + } + + // addr: bcrt1qc6fweuf4xjvz4x3gx3t9e0fh4hvqyu2qw4wvxm + let spk = ScriptBuf::from_hex("0014c692ecf13534982a9a2834565cbd37add8027140").unwrap(); + let mut graph = IndexedTxGraph::new({ + let mut index = SpkTxOutIndex::default(); + let _ = index.insert_spk(0u32, spk.clone()); + index + }); + + // Anchors to test + let blocks = vec![block_id!(0, "g"), block_id!(1, "A"), block_id!(2, "B")]; + + let cp = CheckPoint::from_block_ids(blocks.clone()).unwrap(); + let chain = LocalChain::from_tip(cp).unwrap(); + + // The test will insert a transaction into the indexed tx graph + // along with any anchors and timestamps, then check the value + // returned by `get_chain_position`. + fn run( + chain: &LocalChain, + graph: &mut IndexedTxGraph>, + test: TestCase, + ) { + let TestCase { + name, + tx, + anchor, + last_seen, + exp_pos, + } = test; + + // add data to graph + let txid = tx.compute_txid(); + let _ = graph.insert_tx(tx); + if let Some(anchor) = anchor { + let _ = graph.insert_anchor(txid, anchor); + } + if let Some(seen_at) = last_seen { + let _ = graph.insert_seen_at(txid, seen_at); + } + + // check chain position + let res = graph + .graph() + .get_chain_position(chain, chain.tip().block_id(), txid); + assert_eq!( + res.map(ChainPosition::cloned), + exp_pos, + "failed test case: {name}" + ); + } + + [ + TestCase { + name: "tx no anchors or last_seen - no chain pos", + tx: Transaction { + output: vec![TxOut { + value: Amount::ONE_BTC, + script_pubkey: spk.clone(), + }], + ..common::new_tx(0) + }, + anchor: None, + last_seen: None, + exp_pos: None, + }, + TestCase { + name: "tx last_seen - unconfirmed", + tx: Transaction { + output: vec![TxOut { + value: Amount::ONE_BTC, + script_pubkey: spk.clone(), + }], + ..common::new_tx(1) + }, + anchor: None, + last_seen: Some(2), + exp_pos: Some(ChainPosition::Unconfirmed(2)), + }, + TestCase { + name: "tx anchor in best chain - confirmed", + tx: Transaction { + output: vec![TxOut { + value: Amount::ONE_BTC, + script_pubkey: spk.clone(), + }], + ..common::new_tx(2) + }, + anchor: Some(blocks[1]), + last_seen: None, + exp_pos: Some(ChainPosition::Confirmed(blocks[1])), + }, + TestCase { + name: "tx unknown anchor with last_seen - unconfirmed", + tx: Transaction { + output: vec![TxOut { + value: Amount::ONE_BTC, + script_pubkey: spk.clone(), + }], + ..common::new_tx(3) + }, + anchor: Some(block_id!(2, "B'")), + last_seen: Some(2), + exp_pos: Some(ChainPosition::Unconfirmed(2)), + }, + TestCase { + name: "tx unknown anchor - no chain pos", + tx: Transaction { + output: vec![TxOut { + value: Amount::ONE_BTC, + script_pubkey: spk.clone(), + }], + ..common::new_tx(4) + }, + anchor: Some(block_id!(2, "B'")), + last_seen: None, + exp_pos: None, + }, + ] + .into_iter() + .for_each(|t| run(&chain, &mut graph, t)); +}