1use bitcoin::{constants::COINBASE_MATURITY, OutPoint, TxOut, Txid};
2
3use crate::Anchor;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, core::hash::Hash)]
9#[cfg_attr(
10 feature = "serde",
11 derive(serde::Deserialize, serde::Serialize),
12 serde(bound(
13 deserialize = "A: Ord + serde::Deserialize<'de>",
14 serialize = "A: Ord + serde::Serialize",
15 ))
16)]
17pub enum ChainPosition<A> {
18 Confirmed {
20 anchor: A,
22 transitively: Option<Txid>,
27 },
28 Unconfirmed {
30 first_seen: Option<u64>,
34 last_seen: Option<u64>,
39 },
40}
41
42impl<A> ChainPosition<A> {
43 pub fn is_confirmed(&self) -> bool {
45 matches!(self, Self::Confirmed { .. })
46 }
47
48 pub fn is_unconfirmed(&self) -> bool {
50 matches!(self, Self::Unconfirmed { .. })
51 }
52}
53
54impl<A: Clone> ChainPosition<&A> {
55 pub fn cloned(self) -> ChainPosition<A> {
57 match self {
58 ChainPosition::Confirmed {
59 anchor,
60 transitively,
61 } => ChainPosition::Confirmed {
62 anchor: anchor.clone(),
63 transitively,
64 },
65 ChainPosition::Unconfirmed {
66 last_seen,
67 first_seen,
68 } => ChainPosition::Unconfirmed {
69 last_seen,
70 first_seen,
71 },
72 }
73 }
74}
75
76impl<A: Anchor> ChainPosition<A> {
77 pub fn confirmation_height_upper_bound(&self) -> Option<u32> {
79 match self {
80 ChainPosition::Confirmed { anchor, .. } => {
81 Some(anchor.confirmation_height_upper_bound())
82 }
83 ChainPosition::Unconfirmed { .. } => None,
84 }
85 }
86}
87
88impl<A: Ord> Ord for ChainPosition<A> {
96 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
97 use core::cmp::Ordering;
98
99 fn cmp_none_last<T: Ord>(t1: &Option<T>, t2: &Option<T>) -> Ordering {
101 match (t1, t2) {
102 (None, None) => Ordering::Equal,
103 (None, Some(_)) => Ordering::Greater,
104 (Some(_), None) => Ordering::Less,
105 (Some(t1), Some(t2)) => t1.cmp(t2),
106 }
107 }
108
109 match (self, other) {
110 (
112 ChainPosition::Confirmed {
113 anchor: a1,
114 transitively: t1,
115 },
116 ChainPosition::Confirmed {
117 anchor: a2,
118 transitively: t2,
119 },
120 ) => {
121 match a1.cmp(a2) {
123 Ordering::Equal => cmp_none_last(t1, t2),
125 other => other,
126 }
127 }
128
129 (
131 ChainPosition::Unconfirmed {
132 first_seen: f1,
133 last_seen: l1,
134 },
135 ChainPosition::Unconfirmed {
136 first_seen: f2,
137 last_seen: l2,
138 },
139 ) => {
140 match cmp_none_last(f1, f2) {
143 Ordering::Equal => cmp_none_last(l1, l2),
144 other => other,
145 }
146 }
147
148 (ChainPosition::Confirmed { .. }, ChainPosition::Unconfirmed { .. }) => Ordering::Less,
150 (ChainPosition::Unconfirmed { .. }, ChainPosition::Confirmed { .. }) => {
151 Ordering::Greater
152 }
153 }
154 }
155}
156
157impl<A: Ord> PartialOrd for ChainPosition<A> {
159 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
160 Some(self.cmp(other))
161 }
162}
163
164#[derive(Debug, Clone, PartialEq, Eq)]
166pub struct FullTxOut<A> {
167 pub chain_position: ChainPosition<A>,
169 pub outpoint: OutPoint,
171 pub txout: TxOut,
173 pub spent_by: Option<(ChainPosition<A>, Txid)>,
175 pub is_on_coinbase: bool,
177}
178
179impl<A: Ord> Ord for FullTxOut<A> {
180 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
181 self.chain_position
182 .cmp(&other.chain_position)
183 .then_with(|| self.outpoint.cmp(&other.outpoint))
185 .then_with(|| self.spent_by.cmp(&other.spent_by))
186 }
187}
188
189impl<A: Ord> PartialOrd for FullTxOut<A> {
190 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
191 Some(self.cmp(other))
192 }
193}
194
195impl<A: Anchor> FullTxOut<A> {
196 pub fn is_mature(&self, tip: u32) -> bool {
204 if self.is_on_coinbase {
205 let conf_height = match self.chain_position.confirmation_height_upper_bound() {
206 Some(height) => height,
207 None => {
208 debug_assert!(false, "coinbase tx can never be unconfirmed");
209 return false;
210 }
211 };
212 let age = tip.saturating_sub(conf_height);
213 if age + 1 < COINBASE_MATURITY {
214 return false;
215 }
216 }
217
218 true
219 }
220
221 pub fn is_confirmed_and_spendable(&self, tip: u32) -> bool {
231 if !self.is_mature(tip) {
232 return false;
233 }
234
235 let conf_height = match self.chain_position.confirmation_height_upper_bound() {
236 Some(height) => height,
237 None => return false,
238 };
239 if conf_height > tip {
240 return false;
241 }
242
243 if let Some(spend_height) = self
245 .spent_by
246 .as_ref()
247 .and_then(|(pos, _)| pos.confirmation_height_upper_bound())
248 {
249 if spend_height <= tip {
250 return false;
251 }
252 }
253
254 true
255 }
256}
257
258#[cfg(test)]
259#[cfg_attr(coverage_nightly, coverage(off))]
260mod test {
261 use bdk_core::ConfirmationBlockTime;
262 use bitcoin::hashes::Hash;
263
264 use crate::BlockId;
265
266 use super::*;
267
268 #[test]
269 fn chain_position_ord() {
270 let conf_deep = ChainPosition::Confirmed {
272 anchor: ConfirmationBlockTime {
273 confirmation_time: 20,
274 block_id: BlockId {
275 height: 9,
276 ..Default::default()
277 },
278 },
279 transitively: None,
280 };
281 let conf_deep_transitive = ChainPosition::Confirmed {
282 anchor: ConfirmationBlockTime {
283 confirmation_time: 20,
284 block_id: BlockId {
285 height: 9,
286 ..Default::default()
287 },
288 },
289 transitively: Some(Txid::all_zeros()),
290 };
291 let conf_shallow = ChainPosition::Confirmed {
292 anchor: ConfirmationBlockTime {
293 confirmation_time: 15,
294 block_id: BlockId {
295 height: 12,
296 ..Default::default()
297 },
298 },
299 transitively: None,
300 };
301 let unconf_seen_early = ChainPosition::<ConfirmationBlockTime>::Unconfirmed {
302 first_seen: Some(10),
303 last_seen: Some(10),
304 };
305 let unconf_seen_late = ChainPosition::<ConfirmationBlockTime>::Unconfirmed {
306 first_seen: Some(20),
307 last_seen: Some(20),
308 };
309 let unconf_seen_early_and_late = ChainPosition::<ConfirmationBlockTime>::Unconfirmed {
310 first_seen: Some(10),
311 last_seen: Some(20),
312 };
313 let unconf_never_seen = ChainPosition::<ConfirmationBlockTime>::Unconfirmed {
314 first_seen: None,
315 last_seen: None,
316 };
317
318 assert!(
320 conf_deep < unconf_seen_early,
321 "confirmed comes before unconfirmed"
322 );
323 assert!(
324 conf_shallow < unconf_seen_early,
325 "confirmed comes before unconfirmed"
326 );
327
328 assert!(
330 conf_deep < conf_shallow,
331 "deeper blocks (lower height) come first"
332 );
333
334 assert!(
336 conf_deep_transitive < conf_deep,
337 "transitive confirmation comes before direct at same height"
338 );
339
340 assert!(
343 unconf_seen_early < unconf_seen_late,
344 "earlier first_seen comes first"
345 );
346 assert!(
347 unconf_seen_early < unconf_seen_early_and_late,
348 "if first_seen is equal, tiebreak with last_seen"
349 );
350
351 assert!(
353 unconf_seen_early < unconf_never_seen,
354 "seen in mempool comes before never seen"
355 );
356 assert!(
357 unconf_seen_late < unconf_never_seen,
358 "seen in mempool comes before never seen"
359 );
360
361 let mut positions = vec![
364 unconf_never_seen,
365 unconf_seen_late,
366 conf_shallow,
367 unconf_seen_early,
368 conf_deep,
369 conf_deep_transitive,
370 unconf_seen_early_and_late,
371 ];
372 positions.sort();
373 assert_eq!(
374 positions,
375 vec![
376 conf_deep_transitive, conf_deep, conf_shallow, unconf_seen_early, unconf_seen_early_and_late, unconf_seen_late, unconf_never_seen, ]
384 );
385 }
386
387 #[test]
388 fn test_sort_unconfirmed_chain_position() {
389 let mut v = vec![
390 ChainPosition::<ConfirmationBlockTime>::Unconfirmed {
391 first_seen: Some(5),
392 last_seen: Some(20),
393 },
394 ChainPosition::<ConfirmationBlockTime>::Unconfirmed {
395 first_seen: Some(15),
396 last_seen: Some(30),
397 },
398 ChainPosition::<ConfirmationBlockTime>::Unconfirmed {
399 first_seen: Some(1),
400 last_seen: Some(10),
401 },
402 ChainPosition::<ConfirmationBlockTime>::Unconfirmed {
403 first_seen: Some(3),
404 last_seen: Some(6),
405 },
406 ];
407
408 v.sort();
409
410 assert_eq!(
411 v,
412 vec![
413 ChainPosition::<ConfirmationBlockTime>::Unconfirmed {
414 first_seen: Some(1),
415 last_seen: Some(10)
416 },
417 ChainPosition::<ConfirmationBlockTime>::Unconfirmed {
418 first_seen: Some(3),
419 last_seen: Some(6)
420 },
421 ChainPosition::<ConfirmationBlockTime>::Unconfirmed {
422 first_seen: Some(5),
423 last_seen: Some(20)
424 },
425 ChainPosition::<ConfirmationBlockTime>::Unconfirmed {
426 first_seen: Some(15),
427 last_seen: Some(30)
428 },
429 ]
430 );
431 }
432}