bit_digger/
matcher.rs

1use std::{
2    collections::{HashMap, HashSet},
3    str::FromStr,
4};
5
6use bip39::Mnemonic;
7use bitcoin::{
8    Address, Network, PublicKey,
9    bip32::{DerivationPath, Xpriv},
10    key::Secp256k1,
11};
12
13use thiserror::Error;
14
15type PathStr = String;
16
17#[derive(Debug, Error)]
18pub enum MatcherError {
19    #[error("Derivation path is unexpectedly invalid, derivation path: {0}")]
20    Derivation(PathStr),
21
22    #[error("Failed to create extended private key, error: {0}")]
23    PrivKey(String),
24
25    #[error("Failed to inplace-modify path string index: {0}")]
26    PathStrModify(PathStr),
27}
28
29impl MatcherError {
30    pub fn from_derivation_path(path: &str) -> MatcherError {
31        MatcherError::Derivation(path.to_string())
32    }
33}
34
35/// A derivation standard encapsulates the base derivation path used for key derivation
36/// and the expected address prefix that results from using that standard.
37///
38/// For instance, legacy addresses use the base path `"m/44'/0'/0'/0/"` and expect addresses starting with `"1"`,
39/// whereas native SegWit addresses use `"m/84'/0'/0'/0/"` with addresses beginning with `"bc1q"`.
40///
41/// # Examples
42///
43/// ```
44/// use bit_digger::matcher::DerivationStandard;
45///
46/// let standard = DerivationStandard::new("m/44'/0'/0'/0/", "1");
47/// assert_eq!(standard.base_path, "m/44'/0'/0'/0/");
48/// assert_eq!(standard.starts_with, "1");
49/// ```
50#[derive(Debug, PartialEq, Clone)]
51pub struct DerivationStandard<'a, 'b> {
52    /// The base derivation path used to generate keys.
53    /// Must have an ending back slash, i.e.
54    /// ```
55    /// let valid_base_path = "m/44'/0'/0'/0/";
56    /// ```
57    pub base_path: &'a str,
58    /// The expected starting string/prefix for addresses generated with this standard.
59    pub starts_with: &'b str,
60}
61
62pub const SUPPORTED_STANDARDS: [DerivationStandard; 4] = [
63    DerivationStandard {
64        base_path: "m/44'/0'/0'/0/", // Legacy
65        starts_with: "1",
66    },
67    DerivationStandard {
68        base_path: "m/49'/0'/0'/0/", // SegWit
69        starts_with: "3",
70    },
71    DerivationStandard {
72        base_path: "m/84'/0'/0'/0/", // Native SegWit
73        starts_with: "bc1q",
74    },
75    DerivationStandard {
76        base_path: "m/86'/0'/0'/0/", // Taproot
77        starts_with: "bc1p",
78    },
79];
80
81impl DerivationStandard<'_, '_> {
82    /// Returns a derivation standard with the given base path and expected address prefix.
83    ///
84    /// # Examples
85    ///
86    /// ```
87    /// use bit_digger::matcher::DerivationStandard;
88    /// let standard = DerivationStandard::new("m/44'/0'/0'/0/", "1");
89    /// ```
90    pub fn new<'a, 'b>(base_path: &'a str, starts_with: &'b str) -> DerivationStandard<'a, 'b> {
91        DerivationStandard {
92            base_path,
93            starts_with,
94        }
95    }
96
97    /// Returns a reference to derivation standard if the address starts with a supported prefix.
98    ///
99    /// # Examples
100    ///
101    /// ```
102    /// use bit_digger::matcher::DerivationStandard;
103    /// let standard = DerivationStandard::from_address("1BvB...");
104    /// assert_eq!(standard.unwrap().base_path, "m/44'/0'/0'/0/");
105    /// ```
106    pub fn from_address(address: &str) -> Option<&DerivationStandard> {
107        for standard in SUPPORTED_STANDARDS.iter() {
108            if address.starts_with(standard.starts_with) {
109                return Some(standard);
110            }
111        }
112
113        None
114    }
115
116    /// Converts the provided derivation path and extended private key into a Bitcoin address
117    ///
118    /// # Errors
119    ///
120    /// Returns a `MatcherError` if key derivation fails or if a public key cannot be properly compressed.
121    ///
122    /// # Examples
123    ///
124    /// See examples in Matcher::generate_addresses
125    pub fn into_address(
126        &self,
127        path: &DerivationPath,
128        xpriv: &Xpriv,
129    ) -> Result<String, MatcherError> {
130        let secp = Secp256k1::new();
131
132        let child_xpriv = xpriv
133            .derive_priv(&secp, path)
134            .map_err(|_| MatcherError::PrivKey("Failed to derive child key".to_string()))?;
135
136        // Get the secp256k1 public key and convert it to bitcoin::PublicKey.
137        let child_secp_pubkey = child_xpriv.private_key.public_key(&secp);
138        let child_pubkey = PublicKey::new(child_secp_pubkey);
139
140        match self.starts_with {
141            "1" => {
142                // Legacy P2PKH address.
143                Ok(Address::p2pkh(child_pubkey, Network::Bitcoin).to_string())
144            }
145            "3" => {
146                // For P2SH-wrapped segwit addresses.
147                use bitcoin::CompressedPublicKey;
148                let cp =
149                    CompressedPublicKey::from_slice(&child_pubkey.to_bytes()).map_err(|_| {
150                        MatcherError::PrivKey("Unable to compress public key".to_string())
151                    })?;
152                Ok(Address::p2shwpkh(&cp, Network::Bitcoin).to_string())
153            }
154            "bc1q" => {
155                // For native SegWit (bech32) addresses.
156                use bitcoin::CompressedPublicKey;
157
158                let cp: bitcoin::CompressedPublicKey =
159                    CompressedPublicKey::from_slice(&child_pubkey.to_bytes()).map_err(|_| {
160                        MatcherError::PrivKey("Unable to compress public key".to_string())
161                    })?;
162                Ok(Address::p2wpkh(&cp, Network::Bitcoin).to_string())
163            }
164            "bc1p" => {
165                // For Taproot addresses. p2tr takes the secp context, an untweaked internal key,
166                // an optional script tree (here None), and the network.
167                Ok(
168                    Address::p2tr(&secp, child_pubkey.inner.into(), None, Network::Bitcoin)
169                        .to_string(),
170                )
171            }
172            _ => {
173                // Fallback to legacy P2PKH address.
174                Ok(Address::p2pkh(child_pubkey, Network::Bitcoin).to_string())
175            }
176        }
177    }
178
179    /// Returns a derivation standard if the address starts with a supported prefix.
180    pub fn from_prefix(prefix: &str) -> Option<&DerivationStandard> {
181        for standard in SUPPORTED_STANDARDS.iter() {
182            if prefix.starts_with(standard.starts_with) {
183                return Some(standard);
184            }
185        }
186
187        None
188    }
189    /// Returns a slice of supported derivation standards.
190    pub fn get_supported_standards() -> &'static [DerivationStandard<'static, 'static>] {
191        &SUPPORTED_STANDARDS
192    }
193}
194
195/// Helper function to inplace modify path index
196fn modify_path_index(path: &mut String, index: usize) -> Result<(), MatcherError> {
197    let index_str = index.to_string();
198    let path_len = path.len();
199
200    let last_slash = match path.rfind('/') {
201        Some(pos) => pos,
202        None => return Err(MatcherError::PathStrModify(path.to_string())),
203    };
204
205    path.replace_range(last_slash + 1..path_len, &index_str);
206
207    Ok(())
208}
209
210/// The Matcher struct holds references to a set of Bitcoin addresses and a collection of mnemonic
211/// phrases. It generates addresses from mnemonics using derivation standards and checks whether
212/// they match addresses in its stored set. This can be useful, for example, to verify wallet addresses
213/// or scan for address matches.
214///
215/// # Fields
216///
217/// - `addrs`: A reference to a HashSet of Bitcoin addresses (each represented as a String).
218/// - `mnems`: A reference to a Vec of mnemonic phrases (from bip39) used for key derivation.
219/// - `logging`: A boolean flag to enable or disable logging during operations.
220pub struct Matcher<'a, 'b> {
221    pub addrs: &'a HashSet<String>,
222    pub mnems: &'b Vec<Mnemonic>,
223    pub logging: bool,
224}
225
226impl<'a, 'b> Matcher<'a, 'b> {
227    /// Creates a new Matcher instance.
228    ///
229    /// # Arguments
230    ///
231    /// * `addrs` - A reference to the set of Bitcoin addresses to match against.
232    /// * `mnems` - A reference to the vector of mnemonic phrases used for deriving keys.
233    /// * `logging` - A flag specifying whether logging should be enabled.
234    ///
235    /// # Examples
236    ///
237    /// ```
238    /// use std::{
239    ///     collections::HashSet,
240    ///     str::FromStr,
241    /// };
242    /// use bip39::Mnemonic;
243    /// use bit_digger::matcher::Matcher;
244    ///
245    /// let addresses: HashSet<String> = ["1BGLgRL7EiFxS9H616bfoJPjSugKudECCn"]
246    ///     .iter()
247    ///     .map(|s| s.to_string())
248    ///     .collect();
249    /// let mnem = Mnemonic::from_str("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about").unwrap();
250    /// let mnems = vec![mnem];
251    ///
252    /// let matcher = Matcher::new(&addresses, &mnems, false);
253    /// // matcher now holds references to the addresses set and mnemonic vector.
254    /// ```
255    pub fn new(
256        addrs: &'a HashSet<String>,
257        mnems: &'b Vec<Mnemonic>,
258        logging: bool,
259    ) -> Matcher<'a, 'b> {
260        Matcher {
261            addrs,
262            mnems,
263            logging,
264        }
265    }
266
267    /// Matches generated addresses against the stored addresses.
268    ///
269    /// This method generates addresses for each mnemonic by using the provided derivation amounts.
270    /// If `amount` is None, it automatically infers how many addresses to generate based on the
271    /// stored addresses and supported derivation standards. Then it returns a map of each mnemonic
272    /// reference to the vector of addresses (as Strings) that were found in the matcher's address set.
273    ///
274    /// # Arguments
275    ///
276    /// * `addr_to_gen_per_mnem` - The total number of addresses to generate per mnemonic.
277    /// * `amount` - An optional vector of tuples of the form `(DerivationStandard, usize)` specifying
278    ///   the number of addresses to generate for each derivation standard.
279    ///
280    /// # Returns
281    ///
282    /// A `HashMap` mapping each mnemonic (by reference) to a Vec of matching Bitcoin addresses.
283    ///
284    /// # Errors
285    ///
286    /// Returns a `MatcherError` if key derivation or address generation fails.
287    ///
288    /// # Examples
289    ///
290    /// ```
291    /// use std::{
292    ///     collections::HashSet,
293    ///     str::FromStr,
294    /// };
295    /// use bip39::Mnemonic;
296    /// use bit_digger::matcher::{Matcher, DerivationStandard};
297    ///
298    /// let addresses: HashSet<String> = ["1BGLgRL7EiFxS9H616bfoJPjSugKudECCn"].iter()
299    ///     .map(|s| s.to_string())
300    ///     .collect();
301    ///
302    /// let mnem = Mnemonic::from_str("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about").unwrap();
303    /// let mnems = vec![mnem];
304    /// let matcher = Matcher::new(&addresses, &mnems, false);
305    ///
306    /// let amount = vec![
307    ///     (DerivationStandard::new("m/44'/0'/0'/0/", "1"), 10),
308    ///     (DerivationStandard::new("m/49'/0'/0'/0/", "3"), 10),
309    ///     (DerivationStandard::new("m/84'/0'/0'/0/", "bc1q"), 10),
310    /// ];
311    ///
312    /// let result = matcher.match_in(30, Some(amount)).unwrap();
313    /// // The result maps the mnemonic to its matched addresses.
314    /// ```
315    pub fn match_in(
316        &self,
317        addr_to_gen_per_mnem: usize,
318        amount: Option<Vec<(DerivationStandard, usize)>>,
319    ) -> Result<HashMap<&Mnemonic, Vec<String>>, MatcherError> {
320        let amount = match amount {
321            Some(amount) => amount,
322            None => {
323                let mut unmatched_addresses = vec![];
324
325                let mut standards = DerivationStandard::get_supported_standards()
326                    .iter()
327                    .map(|s| (s.clone(), 0))
328                    .collect::<Vec<(DerivationStandard, usize)>>();
329
330                for addr in self.addrs.iter() {
331                    let standard = match DerivationStandard::from_address(addr) {
332                        Some(s) => s,
333                        None => {
334                            unmatched_addresses.push(addr);
335                            continue;
336                        }
337                    };
338
339                    standards
340                        .iter_mut()
341                        .find(|s| s.0 == *standard)
342                        .map(|s| s.1 += 1);
343                }
344
345                if self.logging {
346                    println!(
347                        "Found {} addresses whose standard is not supported",
348                        unmatched_addresses.len()
349                    );
350                }
351
352                let total = standards.iter().fold(0, |acc, s| acc + s.1);
353
354                for standard in standards.iter_mut() {
355                    standard.1 = (standard.1 as f64 / total as f64 * addr_to_gen_per_mnem as f64)
356                        .ceil() as usize;
357                }
358
359                standards
360            }
361        };
362
363        let mut found = HashMap::new();
364
365        for mnemonic in self.mnems.iter() {
366            let addresses = Self::generate_addresses(&amount, mnemonic)?;
367
368            for addr in addresses {
369                if self.addrs.contains(&addr) {
370                    found.entry(mnemonic).or_insert(vec![]).push(addr);
371                }
372            }
373        }
374
375        Ok(found)
376    }
377
378    /// Generates Bitcoin addresses from a mnemonic seed using the specified derivation standards.
379    ///
380    /// For each tuple in `amount` (which consists of a `DerivationStandard` and a count), this method
381    /// modifies the derivation path by appending the index and derives the corresponding Bitcoin address
382    /// using BIP32/BIP39. The generated addresses are returned as a vector of Strings.
383    ///
384    /// # Arguments
385    ///
386    /// * `amount` - A slice of tuples specifying for each derivation standard (and its base path)
387    ///   how many addresses to generate.
388    /// * `mnemonic` - The mnemonic seed used to derive keys and generate addresses.
389    ///
390    /// # Returns
391    ///
392    /// A Vec of generated Bitcoin addresses (as Strings).
393    ///
394    /// # Errors
395    ///
396    /// Returns a `MatcherError` if key derivation or address generation fails.
397    ///
398    /// /// # Examples
399    ///
400    /// ```
401    /// use bip39::Mnemonic;
402    /// use bit_digger::matcher::{Matcher, DerivationStandard};
403    /// use std::str::FromStr;
404    ///
405    /// let mnemonic = Mnemonic::from_str("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about").unwrap();
406    /// let amount = vec![
407    ///     (DerivationStandard::new("m/44'/0'/0'/0/", "1"), 10),
408    ///     (DerivationStandard::new("m/49'/0'/0'/0/", "3"), 10),
409    ///     (DerivationStandard::new("m/84'/0'/0'/0/", "bc1q"), 10),
410    /// ];
411    ///
412    /// let addresses = Matcher::generate_addresses(&amount, &mnemonic).unwrap();
413    /// // addresses now holds the generated Bitcoin addresses.
414    /// ```
415    pub fn generate_addresses(
416        amount: &[(DerivationStandard, usize)],
417        mnemonic: &Mnemonic,
418    ) -> Result<Vec<String>, MatcherError> {
419        let mut addresses = vec![];
420        let seed = mnemonic.to_seed("");
421        let xpriv = Xpriv::new_master(Network::Bitcoin, &seed)
422            .map_err(|_| MatcherError::PrivKey("Failed to create master key".to_string()))?;
423
424        for (ds, n) in amount {
425            let mut base_path: String = ds.base_path.to_string();
426            base_path.reserve(n.to_string().len() + 10);
427
428            for i in 0..*n {
429                modify_path_index(&mut base_path, i)?;
430
431                let path = DerivationPath::from_str(&base_path)
432                    .map_err(|_| MatcherError::from_derivation_path(base_path.as_str()))?;
433
434                let address = ds.into_address(&path, &xpriv)?;
435
436                addresses.push(address);
437            }
438        }
439
440        Ok(addresses)
441    }
442}
443
444#[cfg(test)]
445mod tests {
446    use super::*;
447    use bip39::Mnemonic;
448    use std::collections::HashSet;
449
450    #[test]
451    fn test_modify_path_index() {
452        let mut path = "m/44'/0'/0'/0/".to_string();
453        modify_path_index(&mut path, 1).unwrap();
454        assert_eq!(path, "m/44'/0'/0'/0/1");
455        modify_path_index(&mut path, 32).unwrap();
456        assert_eq!(path, "m/44'/0'/0'/0/32");
457
458        let mut path = "m/44'/0'/0'/0/".to_string();
459        modify_path_index(&mut path, 10).unwrap();
460        assert_eq!(path, "m/44'/0'/0'/0/10");
461        modify_path_index(&mut path, 100).unwrap();
462        assert_eq!(path, "m/44'/0'/0'/0/100");
463        modify_path_index(&mut path, 0).unwrap();
464        assert_eq!(path, "m/44'/0'/0'/0/0");
465
466        let mut path = "m/44'/0'/0'/0/".to_string();
467        modify_path_index(&mut path, 100).unwrap();
468        assert_eq!(path, "m/44'/0'/0'/0/100");
469        modify_path_index(&mut path, 1000).unwrap();
470        assert_eq!(path, "m/44'/0'/0'/0/1000");
471        modify_path_index(&mut path, 1).unwrap();
472        assert_eq!(path, "m/44'/0'/0'/0/1");
473    }
474
475    #[test]
476    fn test_derivation_standard_from_address() {
477        let standard = DerivationStandard::from_address("1BvB...");
478        assert_eq!(standard.unwrap().base_path, "m/44'/0'/0'/0/");
479
480        let standard = DerivationStandard::from_address("3J98...");
481        assert_eq!(standard.unwrap().base_path, "m/49'/0'/0'/0/");
482
483        let standard = DerivationStandard::from_address("bc1qar0s...");
484        assert_eq!(standard.unwrap().base_path, "m/84'/0'/0'/0/");
485
486        let standard = DerivationStandard::from_address("bc1par0s...");
487        assert_eq!(standard.unwrap().base_path, "m/86'/0'/0'/0/");
488    }
489
490    #[test]
491    fn test_derivation_standard_from_prefix() {
492        let standard = DerivationStandard::from_prefix("1");
493        assert_eq!(standard.unwrap().base_path, "m/44'/0'/0'/0/");
494
495        let standard = DerivationStandard::from_prefix("3");
496        assert_eq!(standard.unwrap().base_path, "m/49'/0'/0'/0/");
497
498        let standard = DerivationStandard::from_prefix("bc1q");
499        assert_eq!(standard.unwrap().base_path, "m/84'/0'/0'/0/");
500
501        let standard = DerivationStandard::from_prefix("bc1p");
502        assert_eq!(standard.unwrap().base_path, "m/86'/0'/0'/0/");
503    }
504
505    const MNEMONIC: &str = "method tribe morning flock suit upon salt puppy jar harbor west wealth device tooth bundle expose mansion scrap erupt helmet hurt promote fit hire";
506    const LEGACY_ADDRESSES: [&str; 10] = [
507        "1BGLgRL7EiFxS9H616bfoJPjSugKudECCn",
508        "1Kc48oyfPrTv9UD1Fk61dZkfxgRM83bU46",
509        "1MqmuiTTY8tTgBMFiDZSB9UygSnM5X9sd",
510        "15VRc3icVAJmrHC2CXQgecnRyUWc1oWVQW",
511        "19DosUKX9zQEdAHWaqCvq67pDAVs5AEpck",
512        "1CP296asrQ8hZnFP3GUjrpDcfyc71f6u2r",
513        "14kNFbSL4Tt67LG5H5p4Cje9CXqJvNKqfT",
514        "1E8CV9uRR8GZSuZAZsaFLHwbjKiQGmifqM",
515        "12k1o5tyV1HqimC2FWbN7gzktStByJMYma",
516        "1DsLE3UoAo98Vwmbi6a7pDKRcmkBB4Txzh",
517    ];
518
519    const SEG_WIT_ADDRESSES: [&str; 10] = [
520        "38cArkkfxxL7LtVWAwYNcvZTgf3KtDBcD7",
521        "37xqCGNb1rTokVVtjBkCoWBLxmAZjXjUyQ",
522        "3EXGQZu3GQnFzJLEyx27dxpjuGTenzfDka",
523        "3AyVvwN2SvgwFfQPD423jrg7Yw4umwZcWv",
524        "38VgHXNcp2wJZg5KVvAFuKSyD7fhGWVoz8",
525        "39vHAo5qSVvQj4XWNPJSzbvfhgKZ8PJF7N",
526        "36EHh1dKfXTURcR4N69KAHwAKoZuZp28Qi",
527        "37cEAoExVS2roXeu3QDVERAACMym8wL29d",
528        "32yaS5LmjcavajMcwU7Ebyq3FmXi7o85HT",
529        "32R9GYSTMqWrjW5oyX2oM4xzM52eL8J5jt",
530    ];
531
532    const NATIVE_SEG_WIT_ADDRESSES: [&str; 10] = [
533        "bc1q39ytrq296c6skxvrkd3m64j2fz5keep26q239t",
534        "bc1qe3kvt65klvzmnwehshzfv7w08cng6dqd3vfs2a",
535        "bc1q0c7qe879lh7x6cyufen7q8kmrf4rmr032u2ykx",
536        "bc1qvwn3a4jflzlkrxckjp76jhu5qhl7tjrw09wgc2",
537        "bc1qlmprztxqq4a8l8syfsn48u8v4zejmv4d5l3hle",
538        "bc1qyg60vy2wgp2sfmt825yu60kg7pg8t6mej8cjkh",
539        "bc1qlakvxvjj3raplc4xu678cr8g9kxdj9249gw7tg",
540        "bc1qg8e6kkwzmjek4mt4t6lzu5jm6fq8a2f9y9s9pa",
541        "bc1qncyyemztujnavnpqv0jpe53yayu0a8nrquztcl",
542        "bc1qszrsm6623dz5kf7dc7fyma0zq4enqtm4e8aqlx",
543    ];
544
545    const NOT_MATCHING_LEGACY_ADDRESSES: [&str; 10] = [
546        "15JcFwxJEEektpqjyQpRWPEHF9DQBX6NLy",
547        "1BZkYY2RdM4iLCD7mGuZu5DpGCssst1NuH",
548        "1Ga2CrW3unYeU4esEoNxtLanLXbsu4eR9Z",
549        "1HSXNFBkrHAH87NPXDMd8fDS9iTJaKUtih",
550        "1FqrZNdEs8yk5eV4kTZJzdpHdWbobFk5y4",
551        "17aUocSwJBDdQv8u7S7CN7vMMu7BEAJj1D",
552        "12GPo6Wih1Ps1MsvZ2Yo8gsRuJMfn4dpXu",
553        "14datpqKqjDMoAwsJQWxF7ZeZNgSDMwghC",
554        "1G8bC5cDtE3V6niunvd3B8zG9pdQNM8ePw",
555        "1KnjQiJjsdFhaPcKaxNPY9xubTHXibLKn6",
556    ];
557
558    const NOT_MATCHING_SEGWIT_ADDRESSES: [&str; 10] = [
559        "3BnH3FPZ9CpN4RfxxCJXFLt6tzibvYCi9k",
560        "3NYZQsjn8vYR4oE9xFdueLVY7ofMpSncEK",
561        "39hwATVfL8Nfe9P6ogpFU5xiDoaVPdWZsh",
562        "384QcePn5Z9ZixRXJwaaN5ot2ThPedJiMP",
563        "35MEXexaZyK6BTtikKxVadVeeVw9HhdMm7",
564        "3EScCZRYJjHuPaYvCevV2WCMJaeCoG9H1R",
565        "3JR8Y511MxV2cfP7i7wDXMizbnPZVC8zbo",
566        "3Ft9CkUDQx5ybvahFuSR7hggruGNeZRhuP",
567        "32VWQ66jafP47tE1q7i3aHZo7eYfHyaXHc",
568        "35MVo6Q48NUdtxc2dBxzm1SnEgZqXojPfA",
569    ];
570
571    const NOT_MATCHING_NATIVE_SEGWIT_ADDRESSES: [&str; 10] = [
572        "bc1q6wznd8c7v4pgwaugt5u3u9mfmus7fglpp9ef60",
573        "bc1qz9nx30gtl353gmr5ckmd7wg7jlxkmwl5q9dqr8",
574        "bc1qrwafwp7mq36zsryg98c45yj96c28qx3puahafp",
575        "bc1q560f5kg4me8tp28mpah7gpphhnwe5aa9wxck6e",
576        "bc1qumg52ymsm74y065x6n30uxns8dv5ul73trckgt",
577        "bc1q3f6swqv7r3prj9l5z9nphld9z5tzgmh3snf4zg",
578        "bc1q4z98wwqdjl5drg5esz98lzyf99fzsvyulyr6fy",
579        "bc1qp447e30gatvlww4sz2an9ymaugyeemtksw6fa8",
580        "bc1q0jvyt5hm42ukh7c3t98st0xmeyeu6w4thhadkp",
581        "bc1qlywz7zrwxna4pln0q5xdjpkwxvtnwk8qchz7xd",
582    ];
583
584    // TODO: Generate Taproot
585
586    #[test]
587    fn test_match_in() {
588        let mut total_addresses = 0;
589
590        let mut addresses = HashSet::new();
591        let mut mnems = vec![];
592
593        for addr in LEGACY_ADDRESSES.iter() {
594            addresses.insert(addr.to_string());
595            total_addresses += 1;
596        }
597
598        for addr in SEG_WIT_ADDRESSES.iter() {
599            addresses.insert(addr.to_string());
600            total_addresses += 1;
601        }
602
603        for addr in NATIVE_SEG_WIT_ADDRESSES.iter() {
604            addresses.insert(addr.to_string());
605            total_addresses += 1;
606        }
607
608        let mnemonic = Mnemonic::from_str(MNEMONIC).unwrap();
609        mnems.push(mnemonic);
610
611        let matcher = Matcher::new(&addresses, &mnems, false);
612
613        let amount = vec![
614            (DerivationStandard::new("m/44'/0'/0'/0/", "1"), 10),
615            (DerivationStandard::new("m/49'/0'/0'/0/", "3"), 10),
616            (DerivationStandard::new("m/84'/0'/0'/0/", "bc1q"), 10),
617        ];
618
619        let found = matcher.match_in(total_addresses, Some(amount)).unwrap();
620
621        let mut mnemonic_with_addresses = 0;
622
623        for (_mn, addresses) in found {
624            assert_eq!(addresses.len(), total_addresses);
625            for addr in addresses {
626                assert!(matcher.addrs.contains(&addr));
627            }
628            mnemonic_with_addresses += 1;
629        }
630
631        assert_eq!(mnemonic_with_addresses, 1);
632    }
633
634    #[test]
635    fn test_match_in_auto() {
636        let mut total_addresses = 0;
637
638        let mut addresses = HashSet::new();
639        let mut mnems = vec![];
640
641        for addr in LEGACY_ADDRESSES.iter() {
642            addresses.insert(addr.to_string());
643            total_addresses += 1;
644        }
645
646        for addr in SEG_WIT_ADDRESSES.iter() {
647            addresses.insert(addr.to_string());
648            total_addresses += 1;
649        }
650
651        for addr in NATIVE_SEG_WIT_ADDRESSES.iter() {
652            addresses.insert(addr.to_string());
653            total_addresses += 1;
654        }
655
656        let mnemonic = Mnemonic::from_str(MNEMONIC).unwrap();
657        mnems.push(mnemonic);
658        let matcher = Matcher::new(&addresses, &mnems, false);
659        let found = matcher.match_in(total_addresses, None).unwrap();
660        let mut mnemonic_with_addresses = 0;
661
662        for (_mn, addresses) in found {
663            assert_eq!(addresses.len(), total_addresses);
664            for addr in addresses {
665                assert!(matcher.addrs.contains(&addr));
666            }
667            mnemonic_with_addresses += 1;
668        }
669
670        assert_eq!(mnemonic_with_addresses, 1);
671    }
672
673    #[test]
674    fn test_match_in_no_more_than_real_matches() {
675        let mut total_addresses = 0;
676
677        let mut addresses = HashSet::new();
678        let mut mnems = vec![];
679
680        for addr in LEGACY_ADDRESSES.iter() {
681            addresses.insert(addr.to_string());
682            total_addresses += 1;
683        }
684
685        for addr in SEG_WIT_ADDRESSES.iter() {
686            addresses.insert(addr.to_string());
687            total_addresses += 1;
688        }
689
690        for addr in NATIVE_SEG_WIT_ADDRESSES.iter() {
691            addresses.insert(addr.to_string());
692            total_addresses += 1;
693        }
694
695        let mut not_matching_addresses = 0;
696
697        for addr in NOT_MATCHING_LEGACY_ADDRESSES.iter() {
698            addresses.insert(addr.to_string());
699            not_matching_addresses += 1;
700        }
701
702        for addr in NOT_MATCHING_SEGWIT_ADDRESSES.iter() {
703            addresses.insert(addr.to_string());
704            not_matching_addresses += 1;
705        }
706
707        for addr in NOT_MATCHING_NATIVE_SEGWIT_ADDRESSES.iter() {
708            addresses.insert(addr.to_string());
709            not_matching_addresses += 1;
710        }
711
712        let mnemonic = Mnemonic::from_str(MNEMONIC).unwrap();
713        mnems.push(mnemonic);
714
715        let matcher = Matcher::new(&addresses, &mnems, false);
716
717        let found = matcher
718            .match_in(total_addresses + not_matching_addresses, None)
719            .unwrap();
720
721        let mut mnemonic_with_addresses = 0;
722
723        for (_mn, addresses) in found {
724            assert_eq!(addresses.len(), total_addresses);
725            for addr in addresses {
726                assert!(matcher.addrs.contains(&addr));
727            }
728            mnemonic_with_addresses += 1;
729        }
730
731        assert_eq!(mnemonic_with_addresses, 1);
732    }
733
734    #[test]
735    fn test_generate_addresses() {
736        let mnemonic = Mnemonic::from_str(MNEMONIC).unwrap();
737
738        let amount = vec![
739            (DerivationStandard::new("m/44'/0'/0'/0/", "1"), 10),
740            (DerivationStandard::new("m/49'/0'/0'/0/", "3"), 10),
741            (DerivationStandard::new("m/84'/0'/0'/0/", "bc1q"), 10),
742        ];
743
744        let addresses = Matcher::generate_addresses(&amount, &mnemonic).unwrap();
745
746        for (addr, legacy) in addresses.iter().zip(LEGACY_ADDRESSES.iter()) {
747            assert_eq!(addr, legacy);
748        }
749
750        for (addr, segwit) in addresses.iter().skip(10).zip(SEG_WIT_ADDRESSES.iter()) {
751            assert_eq!(addr, segwit);
752        }
753
754        for (addr, native_segwit) in addresses
755            .iter()
756            .skip(20)
757            .zip(NATIVE_SEG_WIT_ADDRESSES.iter())
758        {
759            assert_eq!(addr, native_segwit);
760        }
761    }
762}