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));
+}