dd_sds/secondary_validation/
btc_checksum.rs

1use crate::secondary_validation::Validator;
2use bitcoin::{Address, Network};
3use std::str::FromStr;
4
5pub struct BtcChecksum;
6
7impl Validator for BtcChecksum {
8    fn is_valid_match(&self, regex_match: &str) -> bool {
9        // Strip any whitespace or separators
10        let clean_input = regex_match
11            .chars()
12            .filter(|c| c.is_alphanumeric())
13            .collect::<String>();
14
15        let address = Address::from_str(&clean_input);
16        address.is_ok_and(|addr| addr.is_valid_for_network(Network::Bitcoin))
17    }
18}
19
20#[cfg(test)]
21mod test {
22    use crate::secondary_validation::*;
23
24    #[test]
25    fn test_valid_bitcoin_addresses() {
26        let valid_addresses = vec![
27            // P2PKH addresses (start with '1')
28            "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa",
29            "1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2",
30            "12cbQLTFMXRnSzktFkuoG3eHoMeFtpTu3S",
31            "1AGNa15ZQXAZUgFiqJ2i7Z2DPU2J6hW62i",
32            "17NdbrSGoUotzeGCcMMCqnFkEvLymoou9j",
33            "1Q1pE5vPGEEMqRcVRMbtBK842Y6Pzo6nK9",
34            // P2SH addresses (start with '3')
35            "3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLy",
36            "3QJmV3qfvL9SuYo34YihAf3sRCW3qSinyC",
37            // Bech32 addresses (P2WPKH and P2WSH)
38            "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4",
39            "bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3",
40            // Bech32m addresses (P2TR)
41            "bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqzk5jj0",
42        ];
43        for address in valid_addresses {
44            assert!(
45                BtcChecksum.is_valid_match(address),
46                "Failed for address: {address}"
47            );
48        }
49    }
50
51    #[test]
52    fn test_invalid_bitcoin_addresses() {
53        let invalid_addresses = vec![
54            // Invalid Base58Check checksum
55            "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNb",
56            "1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN3",
57            "1AGNa15ZQXAZUgFiqJ3i7Z2DPU2J6hW62i",
58            "1AGNa15ZQXAZUgFiqJ2i7Z2DPU2J6hW62j",
59            "1AGNa15ZQXAZUgFiqJ2i7Z2DPU2J6hW62X",
60            "1ANNa15ZQXAZUgFiqJ2i7Z2DPU2J6hW62i",
61            "1A Na15ZQXAZUgFiqJ2i7Z2DPU2J6hW62i",
62            "1AGNa15ZQXAZUgFiqJ2i7Z2DPU2J6hW62iz",
63            "1AGNa15ZQXAZUgFiqJ2i7Z2DPU2J6hW62izz",
64            "1Q1pE5vPGEEMqRcVRMbtBK842Y6Pzo6nJ9",
65            "1AGNa15ZQXAZUgFiqJ2i7Z2DPU2J6hW62I",
66            // Invalid Base58Check characters
67            "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfN0", // Contains '0'
68            "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNO", // Contains 'O'
69            "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNI", // Contains 'I'
70            "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNl", // Contains 'l'
71            "17NdbrSGoUotzeGCcMMC?nFkEvLymoou9j", // Contains '?'
72            // Invalid Bech32 checksum
73            "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t5", // Last character changed
74            "bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv4", // Last character changed
75            // Invalid Bech32 characters
76            "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3tb", // Contains 'b' (not in Bech32 charset)
77            "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3tO", // Contains 'O' (not in Bech32 charset)
78            // Mixed case (invalid for Bech32)
79            "BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7Kv8f3t4", // Mixed case
80            // Too short
81            "1",
82            "12",
83            "bc1",
84        ];
85        for address in invalid_addresses {
86            assert!(
87                !BtcChecksum.is_valid_match(address),
88                "Should be invalid: {address}"
89            );
90        }
91    }
92
93    #[test]
94    fn test_addresses_with_whitespace() {
95        // Should handle addresses with whitespace
96        assert!(BtcChecksum.is_valid_match(" 1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa "));
97        assert!(BtcChecksum.is_valid_match("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa\n"));
98        assert!(BtcChecksum.is_valid_match("\t1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa\t"));
99
100        // Bech32 with whitespace
101        assert!(BtcChecksum.is_valid_match(" bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 "));
102        assert!(BtcChecksum.is_valid_match("bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4\n"));
103    }
104
105    #[test]
106    fn test_bech32_specific_validation() {
107        // Test specific Bech32 features
108        assert!(BtcChecksum.is_valid_match("bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4"));
109        assert!(BtcChecksum.is_valid_match("BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4")); // uppercase should work
110        assert!(BtcChecksum.is_valid_match("bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4")); // lowercase should work
111
112        // Bech32m (taproot)
113        assert!(
114            BtcChecksum
115                .is_valid_match("bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqzk5jj0")
116        );
117
118        // Invalid: Testnet addresses
119        assert!(!BtcChecksum.is_valid_match("tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx"));
120
121        // Invalid: mixed case
122        assert!(!BtcChecksum.is_valid_match("bc1QW508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4"));
123
124        // Invalid: wrong checksum
125        assert!(!BtcChecksum.is_valid_match("bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t5"));
126
127        // Invalid: contains invalid Bech32 character
128        assert!(!BtcChecksum.is_valid_match("bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3tb"));
129    }
130}