1use std::collections::HashMap;
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5
6const LOG2_MAX_SATS: f64 = 50.897_f64;
10
11pub fn encode_movement_range_log(sats: i64) -> u8 {
17 if sats <= 0 {
18 return 0;
19 }
20 let log_val = (1.0 + sats as f64).log2();
21 ((255.0 * log_val / LOG2_MAX_SATS).floor() as i32).clamp(0, 255) as u8
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
25pub enum FeeRateTier {
26 #[default]
27 SubMinima = 0,
28 Minima = 1,
29 UltraPatient = 2,
30 VeryLow = 3,
31 Low = 4,
32 BelowStandard = 5,
33 NormalLow = 6,
34 Normal = 7,
35 NormalMedium = 8,
36 MarketStandard = 9,
37 AboveStandard = 10,
38 High = 11,
39 Priority = 12,
40 Urgent = 13,
41 UrgentHigh = 14,
42 PanicLow = 15,
43 Panic = 16,
44 PanicHigh = 17,
45 Despair = 18,
46 ExtremeDespair = 19,
47 Outlier = 20,
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
51pub enum FeePercentileTier {
52 #[default]
53 BelowP10 = 0,
54 P10ToP25 = 1,
55 P25ToP50 = 2,
56 P50ToP75 = 3,
57 P75ToP90 = 4,
58 P90ToP99 = 5,
59 AboveP99 = 6,
60 Reserved7 = 7,
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
64pub enum FeeEfficiencyRatio {
65 #[default]
66 UltraEfficient = 0,
67 Efficient = 1,
68 NormalLight = 2,
69 NormalHeavy = 3,
70 Expensive = 4,
71 Destructive = 5,
72 Reserved6 = 6,
73 Reserved7 = 7,
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
77pub enum AbsoluteFeeTier {
78 #[default]
79 Tiny = 0,
80 VeryLow = 1,
81 Low = 2,
82 LowModerate = 3,
83 Moderate = 4,
84 MidHigh = 5,
85 High = 6,
86 VeryHigh = 7,
87 ExtremeUrgency = 8,
88 Critical = 9,
89 Extreme = 10,
90 Outlier = 11,
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
94pub enum UtxoAgeVariance {
95 #[default]
96 Uniform = 0,
97 Mixed = 1,
98 Dispersed = 2,
99 HighlyDispersed = 3,
100}
101
102#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
103pub enum StructuralPattern {
104 #[default]
105 SimpleSend = 0,
106 Consolidation = 1,
107 Distribution = 2,
108 TotalSweep = 3,
109 SymmetricComplex = 4,
110 AsymmetricComplex = 5,
111 ExtremeFanOut = 6,
112 ExtremeFanIn = 7,
113 Reserved8 = 8,
114 Reserved9 = 9,
115 Reserved10 = 10,
116 Reserved11 = 11,
117 Reserved12 = 12,
118 Reserved13 = 13,
119 Reserved14 = 14,
120 Reserved15 = 15,
121}
122
123#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
124pub enum IoScaleTier {
125 #[default]
126 Minimal = 0,
127 Basic = 1,
128 Low = 2,
129 Medium = 3,
130 High = 4,
131 Institutional = 5,
132 Wholesale = 6,
133 Massive = 7,
134}
135
136#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
137pub enum ChangeDetection {
138 #[default]
139 None = 0,
140 ProbableRoundNumber = 1,
141 ProbableScriptContinuity = 2,
142 ProbableValueProximity = 3,
143 HighRoundNumber = 4,
144 HighScoreContinuity = 5,
145 HighMultiMethod = 6,
146 Reserved7 = 7,
147}
148
149#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
150pub enum CoinJoinScore {
151 #[default]
152 None = 0,
153 SuspectLow = 1,
154 SuspectMed = 2,
155 Probable = 3,
156 HighProb = 4,
157 Guaranteed = 5,
158}
159
160#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
161pub enum AddressHotness {
162 #[default]
163 Virgin = 0,
164 Recent = 1,
165 Recurring = 2,
166 Reserved3 = 3,
167}
168
169#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
170pub enum MultisigThreshold {
171 #[default]
172 None = 0,
173 OneOfTwo = 1,
174 TwoOfTwo = 2,
175 TwoOfThree = 3,
176 ThreeOfFive = 4,
177 MOfNComplex = 5,
178 Reserved6 = 6,
179 Reserved7 = 7,
180}
181
182#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
183pub enum TaprootSpendPath {
184 #[default]
185 None = 0,
186 KeyPath = 1,
187 ScriptPath = 2,
188 Mixed = 3,
189}
190
191#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
192pub enum EmbeddedProtocol {
193 #[default]
194 None = 0,
195 OpReturnHash = 1,
196 OpReturnData = 2,
197 OrdinalsBRC20 = 3,
198 Runes = 4,
199 Stamps = 5,
200 MultiProtocol = 6,
201 LargeOpReturn = 7,
202}
203
204#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
205pub enum TimelockFingerprint {
206 #[default]
207 None = 0,
208 AntiFeeSnipingCurrent = 1,
209 AntiFeeSnipingFuzzy = 2,
210 FutureBlockLocked = 3,
211 FutureTimeLocked = 4,
212 Reserved5 = 5,
213 Reserved6 = 6,
214 Reserved7 = 7,
215}
216
217#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
218pub enum LightningIndicator {
219 #[default]
220 NoSignal = 0,
221 ChannelOpen = 1,
222 CooperativeClose = 2,
223 ForceClose = 3,
224 HtlcResolution = 4,
225 AnchorOutput = 5,
226 Reserved6 = 6,
227 Reserved7 = 7,
228}
229
230#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
231pub enum VsizeTier {
232 #[default]
233 Light = 0,
234 Medium = 1,
235 Heavy = 2,
236 Massive = 3,
237}
238
239#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
240pub enum TxVersionTier {
241 #[default]
242 V1 = 0,
243 V2 = 1,
244 V3 = 2,
245 Unknown = 3,
246}
247
248#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
249pub enum EntityPresence {
250 #[default]
251 Anonymous = 0,
252 KnownOrigin = 1,
253 KnownDestination = 2,
254 InternalTransfer = 3,
255}
256
257#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
258pub enum EntityCategory {
259 #[default]
260 None = 0,
261 ExchangeTier1 = 1,
262 ExchangeTier2 = 2,
263 DexOnChain = 3,
264 MinerPool = 4,
265 CustodyService = 5,
266 MixerPrivacy = 6,
267 DarknetIllicit = 7,
268 OfacSdnHack = 8,
269 DefiBridge = 9,
270 EtfInstitutional = 10,
271 GovSeizure = 11,
272}
273
274impl EntityCategory {
275 #[allow(clippy::should_implement_trait)]
276 pub fn from_str(s: &str) -> Self {
277 match s.to_lowercase().as_str() {
278 "exchange" => Self::ExchangeTier1,
279 "mining_pool" => Self::MinerPool,
280 "custodian" => Self::CustodyService,
281 "darknet" => Self::DarknetIllicit,
282 "seized" => Self::GovSeizure,
283 "defi" => Self::DefiBridge,
284 "mixer" => Self::MixerPrivacy,
285 _ => Self::None,
286 }
287 }
288}
289
290impl FeeRateTier {
291 pub fn from_sat_vb(rate: f64) -> Self {
292 if rate < 1.0 {
293 Self::SubMinima
294 } else if rate < 2.0 {
295 Self::Minima
296 } else if rate < 3.0 {
297 Self::UltraPatient
298 } else if rate < 4.0 {
299 Self::VeryLow
300 } else if rate < 6.0 {
301 Self::Low
302 } else if rate < 9.0 {
303 Self::BelowStandard
304 } else if rate < 12.0 {
305 Self::NormalLow
306 } else if rate < 16.0 {
307 Self::Normal
308 } else if rate < 21.0 {
309 Self::NormalMedium
310 } else if rate < 26.0 {
311 Self::MarketStandard
312 } else if rate < 34.0 {
313 Self::AboveStandard
314 } else if rate < 45.0 {
315 Self::High
316 } else if rate < 61.0 {
317 Self::Priority
318 } else if rate < 81.0 {
319 Self::Urgent
320 } else if rate < 101.0 {
321 Self::UrgentHigh
322 } else if rate < 141.0 {
323 Self::PanicLow
324 } else if rate < 201.0 {
325 Self::Panic
326 } else if rate < 301.0 {
327 Self::PanicHigh
328 } else if rate < 501.0 {
329 Self::Despair
330 } else if rate < 1001.0 {
331 Self::ExtremeDespair
332 } else {
333 Self::Outlier
334 }
335 }
336}
337
338impl FeeEfficiencyRatio {
339 pub fn from_values(fee: i64, total_input: i64) -> Self {
340 if total_input == 0 {
341 return Self::NormalLight;
342 }
343 let ratio = (fee as f64 / total_input as f64) * 100.0;
344 if ratio < 0.001 {
345 Self::UltraEfficient
346 } else if ratio < 0.01 {
347 Self::Efficient
348 } else if ratio < 0.05 {
349 Self::NormalLight
350 } else if ratio < 0.1 {
351 Self::NormalHeavy
352 } else if ratio < 1.0 {
353 Self::Expensive
354 } else {
355 Self::Destructive
356 }
357 }
358}
359
360impl AbsoluteFeeTier {
361 pub fn from_sats(sats: i64) -> Self {
362 if sats < 100 {
363 Self::Tiny
364 } else if sats < 500 {
365 Self::VeryLow
366 } else if sats < 2_000 {
367 Self::Low
368 } else if sats < 5_000 {
369 Self::LowModerate
370 } else if sats < 15_000 {
371 Self::Moderate
372 } else if sats < 50_000 {
373 Self::MidHigh
374 } else if sats < 100_000 {
375 Self::High
376 } else if sats < 200_000 {
377 Self::VeryHigh
378 } else if sats < 500_000 {
379 Self::ExtremeUrgency
380 } else if sats < 1_000_000 {
381 Self::Critical
382 } else if sats < 5_000_000 {
383 Self::Extreme
384 } else {
385 Self::Outlier
386 }
387 }
388}
389
390impl VsizeTier {
391 pub fn from_vsize(vsize: i32) -> Self {
392 if vsize < 200 {
393 Self::Light
394 } else if vsize < 1000 {
395 Self::Medium
396 } else if vsize < 5000 {
397 Self::Heavy
398 } else {
399 Self::Massive
400 }
401 }
402}
403
404impl TxVersionTier {
405 pub fn from_version(version: i32) -> Self {
406 match version {
407 1 => Self::V1,
408 2 => Self::V2,
409 3 => Self::V3,
410 _ => Self::Unknown,
411 }
412 }
413}
414
415impl TimelockFingerprint {
416 pub fn from_locktime(locktime: u32) -> Self {
417 Self::from_locktime_with_height(locktime, 0)
418 }
419
420 pub fn from_locktime_with_height(locktime: u32, block_height: i64) -> Self {
422 if locktime == 0 {
423 return Self::None;
424 }
425 if locktime >= 500_000_000 {
426 return Self::FutureTimeLocked;
427 }
428 let lt = locktime as i64;
429 if block_height > 0 {
430 if lt > block_height {
431 return Self::FutureBlockLocked;
432 }
433 if lt == block_height {
434 return Self::AntiFeeSnipingCurrent;
435 }
436 if lt >= block_height - 100 {
437 return Self::AntiFeeSnipingFuzzy;
438 }
439 Self::None
440 } else if lt > 800_000 {
441 Self::FutureBlockLocked
442 } else {
443 Self::AntiFeeSnipingCurrent
444 }
445 }
446}
447
448impl CoinJoinScore {
449 pub fn from_f64(score: f64) -> Self {
450 if score < 0.20 {
451 Self::None
452 } else if score < 0.40 {
453 Self::SuspectLow
454 } else if score < 0.60 {
455 Self::SuspectMed
456 } else if score < 0.75 {
457 Self::Probable
458 } else if score < 0.90 {
459 Self::HighProb
460 } else {
461 Self::Guaranteed
462 }
463 }
464}
465
466#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
468pub enum ScriptType {
469 #[default]
471 P2PKH = 0,
472 P2SH = 1,
474 P2shP2wpkh = 2,
475 P2WPKH = 3,
477 P2WSH = 4,
479 P2TR = 5,
481 Multisig = 10,
483 OpReturn = 11,
485 Unknown = 12,
487 Mixed = 7,
489}
490
491impl ScriptType {
492 pub fn to_bits(self) -> u128 {
493 match self {
494 ScriptType::P2PKH => 0,
495 ScriptType::P2SH => 1,
496 ScriptType::P2shP2wpkh => 2,
497 ScriptType::P2WPKH => 3,
498 ScriptType::P2WSH => 4,
499 ScriptType::P2TR => 5,
500 ScriptType::Multisig | ScriptType::OpReturn | ScriptType::Unknown => 6,
501 ScriptType::Mixed => 7,
502 }
503 }
504}
505
506#[derive(Debug, Clone, Serialize, Deserialize, Default)]
532pub struct TxFeatures {
533 pub txid: Vec<u8>,
535 pub block_height: i64,
536 pub block_timestamp: DateTime<Utc>,
537
538 pub input_count: i32,
540 pub output_count: i32,
541 pub is_coinbase: bool,
542
543 pub total_input_value: i64,
545 pub total_output_value: i64,
546 pub fee: i64,
547 pub output_value_min: i64,
548 pub output_value_max: i64,
549 pub output_value_median: f64,
550 pub output_value_variance: f64,
551
552 pub fee_rate_sat_vb: Option<f64>,
555
556 pub input_p2pkh_count: i32,
558 pub input_p2sh_count: i32,
559 pub input_p2wpkh_count: i32,
560 pub input_p2wsh_count: i32,
561 pub input_p2tr_count: i32,
562 pub input_unknown_count: i32,
563
564 pub output_p2pkh_count: i32,
566 pub output_p2sh_count: i32,
567 pub output_p2wpkh_count: i32,
568 pub output_p2wsh_count: i32,
569 pub output_p2tr_count: i32,
570 pub output_op_return_count: i32,
571 pub output_unknown_count: i32,
572
573 pub is_consolidation: bool,
576 pub is_distribution: bool,
578 pub is_simple_send: bool,
580 pub is_sweep: bool,
582
583 pub has_equal_outputs: bool,
586 pub coinjoin_score: f64,
588
589 pub has_multisig: bool,
591 pub has_op_return: bool,
592 pub is_rbf_enabled: bool,
594 pub has_locktime: bool,
595
596 pub tx_vsize_vbytes: i32,
598 pub tx_locktime: i64,
599 pub tx_version: i32,
600
601 pub input_utxo_refs: Vec<(Vec<u8>, u32)>,
604 pub output_values: Vec<i64>,
606
607 pub unique_output_addresses: Option<u32>,
610 pub address_reuse_in_tx: bool,
612 pub output_addresses: Vec<Option<String>>,
614
615 #[serde(skip)]
620 pub utxo_ages_blocks: HashMap<(Vec<u8>, u32), i64>,
621
622 pub movement_range: u8,
625 pub fee_rate_tier: FeeRateTier,
626 pub fee_percentile_tier: FeePercentileTier,
627 pub fee_efficiency_ratio: FeeEfficiencyRatio,
628 pub absolute_fee_tier: AbsoluteFeeTier,
629 pub utxo_age_max: u8,
631 pub utxo_age_median: u8,
633 pub utxo_age_variance: UtxoAgeVariance,
634 pub structural_pattern: StructuralPattern,
635 pub io_scale_tier: IoScaleTier,
636 pub change_detection: ChangeDetection,
637 pub coinjoin_probability: CoinJoinScore,
638 pub has_dust_output: bool,
639 pub address_hotness: AddressHotness,
640 pub multisig_threshold: MultisigThreshold,
641 pub is_input_script_homogeneous: bool,
642 pub taproot_spend_path: TaprootSpendPath,
643 pub embedded_protocol: EmbeddedProtocol,
644 pub timelock_fingerprint: TimelockFingerprint,
645 pub lightning_indicator: LightningIndicator,
646 pub has_relative_timelock: bool,
647 pub vsize_tier: VsizeTier,
648 pub tx_version_tier: TxVersionTier,
649 pub is_cpfp_signal: bool,
650 pub entity_presence: EntityPresence,
651 pub entity_category: EntityCategory,
652 pub dominant_input_script: ScriptType,
653 pub dominant_output_script: ScriptType,
654
655 pub block_fee_median_sat_vb: f64,
658 pub script_type_evolution: bool,
660 pub block_price_usd: Option<f64>,
662}
663
664#[cfg(test)]
665mod tests {
666 use super::*;
667
668 fn make_features() -> TxFeatures {
669 TxFeatures {
670 txid: vec![1u8; 32],
671 block_height: 840_000,
672 block_timestamp: Utc::now(),
673 input_count: 1,
674 output_count: 2,
675 is_coinbase: false,
676 total_input_value: 100_000,
677 total_output_value: 99_000,
678 fee: 1_000,
679 output_value_min: 40_000,
680 output_value_max: 59_000,
681 output_value_median: 49_500.0,
682 output_value_variance: 9_025_000.0,
683 fee_rate_sat_vb: Some(5.0),
684 input_p2pkh_count: 1,
685 is_simple_send: true,
686 output_p2pkh_count: 2,
687 tx_vsize_vbytes: 200,
688 tx_version: 1,
689 input_utxo_refs: vec![(vec![0xffu8; 32], 0)],
690 output_values: vec![40_000, 59_000],
691 is_input_script_homogeneous: true,
692 ..Default::default()
693 }
694 }
695
696 #[test]
697 fn test_entity_category_from_str() {
698 assert_eq!(
699 EntityCategory::from_str("exchange"),
700 EntityCategory::ExchangeTier1
701 );
702 assert_eq!(
703 EntityCategory::from_str("EXCHANGE"),
704 EntityCategory::ExchangeTier1
705 );
706 assert_eq!(
707 EntityCategory::from_str("mining_pool"),
708 EntityCategory::MinerPool
709 );
710 assert_eq!(
711 EntityCategory::from_str("custodian"),
712 EntityCategory::CustodyService
713 );
714 assert_eq!(
715 EntityCategory::from_str("darknet"),
716 EntityCategory::DarknetIllicit
717 );
718 assert_eq!(
719 EntityCategory::from_str("seized"),
720 EntityCategory::GovSeizure
721 );
722 assert_eq!(EntityCategory::from_str("defi"), EntityCategory::DefiBridge);
723 assert_eq!(
724 EntityCategory::from_str("mixer"),
725 EntityCategory::MixerPrivacy
726 );
727 assert_eq!(EntityCategory::from_str("unknown"), EntityCategory::None);
728 }
729
730 #[test]
731 fn test_tx_features_clone() {
732 let f = make_features();
733 let cloned = f.clone();
734 assert_eq!(f.txid, cloned.txid);
735 assert_eq!(f.block_height, cloned.block_height);
736 assert_eq!(f.input_count, cloned.input_count);
737 assert_eq!(f.is_simple_send, cloned.is_simple_send);
738 }
739
740 #[test]
741 fn test_tx_features_serde_roundtrip() {
742 let f = make_features();
743 let json = serde_json::to_string(&f).unwrap();
744 let decoded: TxFeatures = serde_json::from_str(&json).unwrap();
745 assert_eq!(decoded.txid, f.txid);
746 assert_eq!(decoded.block_height, f.block_height);
747 assert_eq!(decoded.fee, f.fee);
748 assert_eq!(decoded.is_simple_send, f.is_simple_send);
749 assert!((decoded.coinjoin_score - f.coinjoin_score).abs() < f64::EPSILON);
750 }
751
752 #[test]
753 fn test_tx_features_default_values() {
754 let f = make_features();
755 assert!(!f.is_consolidation);
756 assert!(!f.is_distribution);
757 assert!(f.is_simple_send);
758 assert!(!f.is_sweep);
759 assert!(!f.has_equal_outputs);
760 assert!(!f.has_multisig);
761 assert!(!f.has_op_return);
762 assert!(!f.is_rbf_enabled);
763 assert!(!f.has_locktime);
764 assert!(!f.is_coinbase);
765 assert_eq!(f.coinjoin_score, 0.0);
766 }
767
768 #[test]
769 fn test_encode_movement_range_log_zero() {
770 assert_eq!(encode_movement_range_log(0), 0);
771 assert_eq!(encode_movement_range_log(-1), 0);
772 }
773
774 #[test]
775 fn test_encode_movement_range_log_monotone() {
776 let sats = [
777 1,
778 100,
779 1_000,
780 100_000,
781 1_000_000,
782 100_000_000,
783 21_000_000_000_000_i64,
784 ];
785 let tiers: Vec<u8> = sats.iter().map(|&s| encode_movement_range_log(s)).collect();
786 for i in 1..tiers.len() {
787 assert!(
788 tiers[i] >= tiers[i - 1],
789 "tier[{}]={} should be >= tier[{}]={} (sat: {} vs {})",
790 i,
791 tiers[i],
792 i - 1,
793 tiers[i - 1],
794 sats[i],
795 sats[i - 1]
796 );
797 }
798 }
799
800 #[test]
801 fn test_encode_movement_range_log_max_sats() {
802 assert_eq!(encode_movement_range_log(2_100_000_000_000_000), 255);
804 }
805
806 #[test]
807 fn test_fee_rate_tier_sat_vb() {
808 assert_eq!(FeeRateTier::from_sat_vb(0.5), FeeRateTier::SubMinima);
809 assert_eq!(FeeRateTier::from_sat_vb(5.0), FeeRateTier::Low);
810 assert_eq!(FeeRateTier::from_sat_vb(20.0), FeeRateTier::NormalMedium);
811 assert_eq!(FeeRateTier::from_sat_vb(1_500.0), FeeRateTier::Outlier);
812 }
813
814 #[test]
815 fn test_absolute_fee_tier() {
816 assert_eq!(AbsoluteFeeTier::from_sats(50), AbsoluteFeeTier::Tiny);
817 assert_eq!(AbsoluteFeeTier::from_sats(5_000), AbsoluteFeeTier::Moderate);
818 assert_eq!(
819 AbsoluteFeeTier::from_sats(10_000_000),
820 AbsoluteFeeTier::Outlier
821 );
822 }
823
824 #[test]
825 fn test_vsize_tier() {
826 assert_eq!(VsizeTier::from_vsize(100), VsizeTier::Light);
827 assert_eq!(VsizeTier::from_vsize(500), VsizeTier::Medium);
828 assert_eq!(VsizeTier::from_vsize(10_000), VsizeTier::Massive);
829 }
830
831 #[test]
832 fn test_tx_version_tier() {
833 assert_eq!(TxVersionTier::from_version(1), TxVersionTier::V1);
834 assert_eq!(TxVersionTier::from_version(2), TxVersionTier::V2);
835 assert_eq!(TxVersionTier::from_version(99), TxVersionTier::Unknown);
836 }
837
838 #[test]
839 fn test_timelock_fingerprint_no_locktime() {
840 assert_eq!(
841 TimelockFingerprint::from_locktime(0),
842 TimelockFingerprint::None
843 );
844 }
845
846 #[test]
847 fn test_timelock_fingerprint_future_time() {
848 assert_eq!(
849 TimelockFingerprint::from_locktime(500_000_001),
850 TimelockFingerprint::FutureTimeLocked
851 );
852 }
853
854 #[test]
855 fn test_coinjoin_score_enum() {
856 assert_eq!(CoinJoinScore::from_f64(0.0), CoinJoinScore::None);
857 assert_eq!(CoinJoinScore::from_f64(0.5), CoinJoinScore::SuspectMed);
858 assert_eq!(CoinJoinScore::from_f64(0.95), CoinJoinScore::Guaranteed);
859 }
860
861 #[test]
862 fn test_script_type_to_bits_unique() {
863 use std::collections::HashSet;
864 let types = [
865 ScriptType::P2PKH,
866 ScriptType::P2SH,
867 ScriptType::P2shP2wpkh,
868 ScriptType::P2WPKH,
869 ScriptType::P2WSH,
870 ScriptType::P2TR,
871 ScriptType::Mixed,
872 ];
873 let bits: HashSet<u128> = types.iter().map(|t| t.to_bits()).collect();
874 assert_eq!(
875 bits.len(),
876 types.len(),
877 "each ScriptType should map to a unique bit"
878 );
879 }
880
881 #[test]
882 fn test_fee_efficiency_ratio() {
883 assert_eq!(
884 FeeEfficiencyRatio::from_values(1, 1_000_000),
885 FeeEfficiencyRatio::UltraEfficient
886 );
887 assert_eq!(
888 FeeEfficiencyRatio::from_values(100_000, 1_000_000),
889 FeeEfficiencyRatio::Destructive
890 );
891 assert_eq!(
892 FeeEfficiencyRatio::from_values(0, 0),
893 FeeEfficiencyRatio::NormalLight
894 );
895 }
896}