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 [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 [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                if self.logging {
331                    println!("Starting automatic derivation amount inference");
332                }
333
334                for addr in self.addrs.iter() {
335                    let standard = match DerivationStandard::from_address(addr) {
336                        Some(s) => s,
337                        None => {
338                            unmatched_addresses.push(addr);
339                            continue;
340                        }
341                    };
342
343                    standards
344                        .iter_mut()
345                        .find(|s| s.0 == *standard)
346                        .map(|s| s.1 += 1);
347                }
348
349                let total = standards.iter().fold(0, |acc, s| acc + s.1);
350
351                for standard in standards.iter_mut() {
352                    standard.1 = (standard.1 as f64 / total as f64 * addr_to_gen_per_mnem as f64)
353                        .ceil() as usize;
354                }
355
356                if self.logging {
357                    println!(
358                        "Found {} addresses whose standard is not supported",
359                        unmatched_addresses.len()
360                    );
361
362                    for standard in standards.iter() {
363                        println!("Standard: {}, Amount: {}", standard.0.base_path, standard.1);
364                    }
365                }
366
367                standards
368            }
369        };
370
371        let mut found = HashMap::new();
372
373        for (index, mnemonic) in self.mnems.iter().enumerate() {
374            let addresses = Self::generate_addresses(&amount, mnemonic)?;
375
376            for addr in addresses {
377                if self.addrs.contains(&addr) {
378                    if self.logging {
379                        println!("Found address {} for mnemonic {:?}", addr, mnemonic);
380                    }
381                    found.entry(mnemonic).or_insert(vec![]).push(addr);
382                }
383            }
384
385            if self.logging && (self.mnems.len() / 10 > 0) && index % (self.mnems.len() / 10) == 0 {
386                println!(
387                    "Processed {}% of the mnemonics ({}/{})",
388                    index * 100 / self.mnems.len(),
389                    index,
390                    self.mnems.len()
391                );
392            }
393        }
394
395        Ok(found)
396    }
397
398    /// Generates Bitcoin addresses from a mnemonic seed using the specified derivation standards.
399    ///
400    /// For each tuple in `amount` (which consists of a `DerivationStandard` and a count), this method
401    /// modifies the derivation path by appending the index and derives the corresponding Bitcoin address
402    /// using BIP32/BIP39. The generated addresses are returned as a vector of Strings.
403    ///
404    /// # Arguments
405    ///
406    /// * `amount` - A slice of tuples specifying for each derivation standard (and its base path)
407    ///   how many addresses to generate.
408    /// * `mnemonic` - The mnemonic seed used to derive keys and generate addresses.
409    ///
410    /// # Returns
411    ///
412    /// A Vec of generated Bitcoin addresses (as Strings).
413    ///
414    /// # Errors
415    ///
416    /// Returns a `MatcherError` if key derivation or address generation fails.
417    ///
418    /// /// # Examples
419    ///
420    /// ```
421    /// use bip39::Mnemonic;
422    /// use bit_digger::matcher::{Matcher, DerivationStandard};
423    /// use std::str::FromStr;
424    ///
425    /// let mnemonic = Mnemonic::from_str("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about").unwrap();
426    /// let amount = vec![
427    ///     (DerivationStandard::new("m/44'/0'/0'/0/", "1"), 10),
428    ///     (DerivationStandard::new("m/49'/0'/0'/0/", "3"), 10),
429    ///     (DerivationStandard::new("m/84'/0'/0'/0/", "bc1q"), 10),
430    /// ];
431    ///
432    /// let addresses = Matcher::generate_addresses(&amount, &mnemonic).unwrap();
433    /// // addresses now holds the generated Bitcoin addresses.
434    /// ```
435    pub fn generate_addresses(
436        amount: &[(DerivationStandard, usize)],
437        mnemonic: &Mnemonic,
438    ) -> Result<Vec<String>, MatcherError> {
439        let mut addresses = vec![];
440        let seed = mnemonic.to_seed("");
441        let xpriv = Xpriv::new_master(Network::Bitcoin, &seed)
442            .map_err(|_| MatcherError::PrivKey("Failed to create master key".to_string()))?;
443
444        for (ds, n) in amount {
445            let mut base_path: String = ds.base_path.to_string();
446            base_path.reserve(n.to_string().len() + 10);
447
448            for i in 0..*n {
449                modify_path_index(&mut base_path, i)?;
450
451                let path = DerivationPath::from_str(&base_path)
452                    .map_err(|_| MatcherError::from_derivation_path(base_path.as_str()))?;
453
454                let address = ds.into_address(&path, &xpriv)?;
455
456                addresses.push(address);
457            }
458        }
459
460        Ok(addresses)
461    }
462}
463
464#[cfg(test)]
465mod tests {
466    use super::*;
467    use bip39::Mnemonic;
468    use std::collections::HashSet;
469
470    #[test]
471    fn test_modify_path_index() {
472        let mut path = "m/44'/0'/0'/0/".to_string();
473        modify_path_index(&mut path, 1).unwrap();
474        assert_eq!(path, "m/44'/0'/0'/0/1");
475        modify_path_index(&mut path, 32).unwrap();
476        assert_eq!(path, "m/44'/0'/0'/0/32");
477
478        let mut path = "m/44'/0'/0'/0/".to_string();
479        modify_path_index(&mut path, 10).unwrap();
480        assert_eq!(path, "m/44'/0'/0'/0/10");
481        modify_path_index(&mut path, 100).unwrap();
482        assert_eq!(path, "m/44'/0'/0'/0/100");
483        modify_path_index(&mut path, 0).unwrap();
484        assert_eq!(path, "m/44'/0'/0'/0/0");
485
486        let mut path = "m/44'/0'/0'/0/".to_string();
487        modify_path_index(&mut path, 100).unwrap();
488        assert_eq!(path, "m/44'/0'/0'/0/100");
489        modify_path_index(&mut path, 1000).unwrap();
490        assert_eq!(path, "m/44'/0'/0'/0/1000");
491        modify_path_index(&mut path, 1).unwrap();
492        assert_eq!(path, "m/44'/0'/0'/0/1");
493    }
494
495    #[test]
496    fn test_derivation_standard_from_address() {
497        let standard = DerivationStandard::from_address("1BvB...");
498        assert_eq!(standard.unwrap().base_path, "m/44'/0'/0'/0/");
499
500        let standard = DerivationStandard::from_address("3J98...");
501        assert_eq!(standard.unwrap().base_path, "m/49'/0'/0'/0/");
502
503        let standard = DerivationStandard::from_address("bc1qar0s...");
504        assert_eq!(standard.unwrap().base_path, "m/84'/0'/0'/0/");
505
506        let standard = DerivationStandard::from_address("bc1par0s...");
507        assert_eq!(standard.unwrap().base_path, "m/86'/0'/0'/0/");
508    }
509
510    #[test]
511    fn test_derivation_standard_from_prefix() {
512        let standard = DerivationStandard::from_prefix("1");
513        assert_eq!(standard.unwrap().base_path, "m/44'/0'/0'/0/");
514
515        let standard = DerivationStandard::from_prefix("3");
516        assert_eq!(standard.unwrap().base_path, "m/49'/0'/0'/0/");
517
518        let standard = DerivationStandard::from_prefix("bc1q");
519        assert_eq!(standard.unwrap().base_path, "m/84'/0'/0'/0/");
520
521        let standard = DerivationStandard::from_prefix("bc1p");
522        assert_eq!(standard.unwrap().base_path, "m/86'/0'/0'/0/");
523    }
524
525    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";
526    const LEGACY_ADDRESSES: [&str; 10] = [
527        "1BGLgRL7EiFxS9H616bfoJPjSugKudECCn",
528        "1Kc48oyfPrTv9UD1Fk61dZkfxgRM83bU46",
529        "1MqmuiTTY8tTgBMFiDZSB9UygSnM5X9sd",
530        "15VRc3icVAJmrHC2CXQgecnRyUWc1oWVQW",
531        "19DosUKX9zQEdAHWaqCvq67pDAVs5AEpck",
532        "1CP296asrQ8hZnFP3GUjrpDcfyc71f6u2r",
533        "14kNFbSL4Tt67LG5H5p4Cje9CXqJvNKqfT",
534        "1E8CV9uRR8GZSuZAZsaFLHwbjKiQGmifqM",
535        "12k1o5tyV1HqimC2FWbN7gzktStByJMYma",
536        "1DsLE3UoAo98Vwmbi6a7pDKRcmkBB4Txzh",
537    ];
538
539    const SEG_WIT_ADDRESSES: [&str; 10] = [
540        "38cArkkfxxL7LtVWAwYNcvZTgf3KtDBcD7",
541        "37xqCGNb1rTokVVtjBkCoWBLxmAZjXjUyQ",
542        "3EXGQZu3GQnFzJLEyx27dxpjuGTenzfDka",
543        "3AyVvwN2SvgwFfQPD423jrg7Yw4umwZcWv",
544        "38VgHXNcp2wJZg5KVvAFuKSyD7fhGWVoz8",
545        "39vHAo5qSVvQj4XWNPJSzbvfhgKZ8PJF7N",
546        "36EHh1dKfXTURcR4N69KAHwAKoZuZp28Qi",
547        "37cEAoExVS2roXeu3QDVERAACMym8wL29d",
548        "32yaS5LmjcavajMcwU7Ebyq3FmXi7o85HT",
549        "32R9GYSTMqWrjW5oyX2oM4xzM52eL8J5jt",
550    ];
551
552    const NATIVE_SEG_WIT_ADDRESSES: [&str; 10] = [
553        "bc1q39ytrq296c6skxvrkd3m64j2fz5keep26q239t",
554        "bc1qe3kvt65klvzmnwehshzfv7w08cng6dqd3vfs2a",
555        "bc1q0c7qe879lh7x6cyufen7q8kmrf4rmr032u2ykx",
556        "bc1qvwn3a4jflzlkrxckjp76jhu5qhl7tjrw09wgc2",
557        "bc1qlmprztxqq4a8l8syfsn48u8v4zejmv4d5l3hle",
558        "bc1qyg60vy2wgp2sfmt825yu60kg7pg8t6mej8cjkh",
559        "bc1qlakvxvjj3raplc4xu678cr8g9kxdj9249gw7tg",
560        "bc1qg8e6kkwzmjek4mt4t6lzu5jm6fq8a2f9y9s9pa",
561        "bc1qncyyemztujnavnpqv0jpe53yayu0a8nrquztcl",
562        "bc1qszrsm6623dz5kf7dc7fyma0zq4enqtm4e8aqlx",
563    ];
564
565    const NOT_MATCHING_LEGACY_ADDRESSES: [&str; 10] = [
566        "15JcFwxJEEektpqjyQpRWPEHF9DQBX6NLy",
567        "1BZkYY2RdM4iLCD7mGuZu5DpGCssst1NuH",
568        "1Ga2CrW3unYeU4esEoNxtLanLXbsu4eR9Z",
569        "1HSXNFBkrHAH87NPXDMd8fDS9iTJaKUtih",
570        "1FqrZNdEs8yk5eV4kTZJzdpHdWbobFk5y4",
571        "17aUocSwJBDdQv8u7S7CN7vMMu7BEAJj1D",
572        "12GPo6Wih1Ps1MsvZ2Yo8gsRuJMfn4dpXu",
573        "14datpqKqjDMoAwsJQWxF7ZeZNgSDMwghC",
574        "1G8bC5cDtE3V6niunvd3B8zG9pdQNM8ePw",
575        "1KnjQiJjsdFhaPcKaxNPY9xubTHXibLKn6",
576    ];
577
578    const NOT_MATCHING_SEGWIT_ADDRESSES: [&str; 10] = [
579        "3BnH3FPZ9CpN4RfxxCJXFLt6tzibvYCi9k",
580        "3NYZQsjn8vYR4oE9xFdueLVY7ofMpSncEK",
581        "39hwATVfL8Nfe9P6ogpFU5xiDoaVPdWZsh",
582        "384QcePn5Z9ZixRXJwaaN5ot2ThPedJiMP",
583        "35MEXexaZyK6BTtikKxVadVeeVw9HhdMm7",
584        "3EScCZRYJjHuPaYvCevV2WCMJaeCoG9H1R",
585        "3JR8Y511MxV2cfP7i7wDXMizbnPZVC8zbo",
586        "3Ft9CkUDQx5ybvahFuSR7hggruGNeZRhuP",
587        "32VWQ66jafP47tE1q7i3aHZo7eYfHyaXHc",
588        "35MVo6Q48NUdtxc2dBxzm1SnEgZqXojPfA",
589    ];
590
591    const NOT_MATCHING_NATIVE_SEGWIT_ADDRESSES: [&str; 10] = [
592        "bc1q6wznd8c7v4pgwaugt5u3u9mfmus7fglpp9ef60",
593        "bc1qz9nx30gtl353gmr5ckmd7wg7jlxkmwl5q9dqr8",
594        "bc1qrwafwp7mq36zsryg98c45yj96c28qx3puahafp",
595        "bc1q560f5kg4me8tp28mpah7gpphhnwe5aa9wxck6e",
596        "bc1qumg52ymsm74y065x6n30uxns8dv5ul73trckgt",
597        "bc1q3f6swqv7r3prj9l5z9nphld9z5tzgmh3snf4zg",
598        "bc1q4z98wwqdjl5drg5esz98lzyf99fzsvyulyr6fy",
599        "bc1qp447e30gatvlww4sz2an9ymaugyeemtksw6fa8",
600        "bc1q0jvyt5hm42ukh7c3t98st0xmeyeu6w4thhadkp",
601        "bc1qlywz7zrwxna4pln0q5xdjpkwxvtnwk8qchz7xd",
602    ];
603
604    // TODO: Generate Taproot
605
606    #[test]
607    fn test_match_in() {
608        let mut total_addresses = 0;
609
610        let mut addresses = HashSet::new();
611        let mut mnems = vec![];
612
613        for addr in LEGACY_ADDRESSES.iter() {
614            addresses.insert(addr.to_string());
615            total_addresses += 1;
616        }
617
618        for addr in SEG_WIT_ADDRESSES.iter() {
619            addresses.insert(addr.to_string());
620            total_addresses += 1;
621        }
622
623        for addr in NATIVE_SEG_WIT_ADDRESSES.iter() {
624            addresses.insert(addr.to_string());
625            total_addresses += 1;
626        }
627
628        let mnemonic = Mnemonic::from_str(MNEMONIC).unwrap();
629        mnems.push(mnemonic);
630
631        let matcher = Matcher::new(&addresses, &mnems, false);
632
633        let amount = vec![
634            (DerivationStandard::new("m/44'/0'/0'/0/", "1"), 10),
635            (DerivationStandard::new("m/49'/0'/0'/0/", "3"), 10),
636            (DerivationStandard::new("m/84'/0'/0'/0/", "bc1q"), 10),
637        ];
638
639        let found = matcher.match_in(total_addresses, Some(amount)).unwrap();
640
641        let mut mnemonic_with_addresses = 0;
642
643        for (_mn, addresses) in found {
644            assert_eq!(addresses.len(), total_addresses);
645            for addr in addresses {
646                assert!(matcher.addrs.contains(&addr));
647            }
648            mnemonic_with_addresses += 1;
649        }
650
651        assert_eq!(mnemonic_with_addresses, 1);
652    }
653
654    #[test]
655    fn test_match_in_auto() {
656        let mut total_addresses = 0;
657
658        let mut addresses = HashSet::new();
659        let mut mnems = vec![];
660
661        for addr in LEGACY_ADDRESSES.iter() {
662            addresses.insert(addr.to_string());
663            total_addresses += 1;
664        }
665
666        for addr in SEG_WIT_ADDRESSES.iter() {
667            addresses.insert(addr.to_string());
668            total_addresses += 1;
669        }
670
671        for addr in NATIVE_SEG_WIT_ADDRESSES.iter() {
672            addresses.insert(addr.to_string());
673            total_addresses += 1;
674        }
675
676        let mnemonic = Mnemonic::from_str(MNEMONIC).unwrap();
677        mnems.push(mnemonic);
678        let matcher = Matcher::new(&addresses, &mnems, false);
679        let found = matcher.match_in(total_addresses, None).unwrap();
680        let mut mnemonic_with_addresses = 0;
681
682        for (_mn, addresses) in found {
683            assert_eq!(addresses.len(), total_addresses);
684            for addr in addresses {
685                assert!(matcher.addrs.contains(&addr));
686            }
687            mnemonic_with_addresses += 1;
688        }
689
690        assert_eq!(mnemonic_with_addresses, 1);
691    }
692
693    #[test]
694    fn test_match_in_no_more_than_real_matches() {
695        let mut total_addresses = 0;
696
697        let mut addresses = HashSet::new();
698        let mut mnems = vec![];
699
700        for addr in LEGACY_ADDRESSES.iter() {
701            addresses.insert(addr.to_string());
702            total_addresses += 1;
703        }
704
705        for addr in SEG_WIT_ADDRESSES.iter() {
706            addresses.insert(addr.to_string());
707            total_addresses += 1;
708        }
709
710        for addr in NATIVE_SEG_WIT_ADDRESSES.iter() {
711            addresses.insert(addr.to_string());
712            total_addresses += 1;
713        }
714
715        let mut not_matching_addresses = 0;
716
717        for addr in NOT_MATCHING_LEGACY_ADDRESSES.iter() {
718            addresses.insert(addr.to_string());
719            not_matching_addresses += 1;
720        }
721
722        for addr in NOT_MATCHING_SEGWIT_ADDRESSES.iter() {
723            addresses.insert(addr.to_string());
724            not_matching_addresses += 1;
725        }
726
727        for addr in NOT_MATCHING_NATIVE_SEGWIT_ADDRESSES.iter() {
728            addresses.insert(addr.to_string());
729            not_matching_addresses += 1;
730        }
731
732        let mnemonic = Mnemonic::from_str(MNEMONIC).unwrap();
733        mnems.push(mnemonic);
734
735        let matcher = Matcher::new(&addresses, &mnems, false);
736
737        let found = matcher
738            .match_in(total_addresses + not_matching_addresses, None)
739            .unwrap();
740
741        let mut mnemonic_with_addresses = 0;
742
743        for (_mn, addresses) in found {
744            assert_eq!(addresses.len(), total_addresses);
745            for addr in addresses {
746                assert!(matcher.addrs.contains(&addr));
747            }
748            mnemonic_with_addresses += 1;
749        }
750
751        assert_eq!(mnemonic_with_addresses, 1);
752    }
753
754    #[test]
755    fn test_generate_addresses() {
756        let mnemonic = Mnemonic::from_str(MNEMONIC).unwrap();
757
758        let amount = vec![
759            (DerivationStandard::new("m/44'/0'/0'/0/", "1"), 10),
760            (DerivationStandard::new("m/49'/0'/0'/0/", "3"), 10),
761            (DerivationStandard::new("m/84'/0'/0'/0/", "bc1q"), 10),
762        ];
763
764        let addresses = Matcher::generate_addresses(&amount, &mnemonic).unwrap();
765
766        for (addr, legacy) in addresses.iter().zip(LEGACY_ADDRESSES.iter()) {
767            assert_eq!(addr, legacy);
768        }
769
770        for (addr, segwit) in addresses.iter().skip(10).zip(SEG_WIT_ADDRESSES.iter()) {
771            assert_eq!(addr, segwit);
772        }
773
774        for (addr, native_segwit) in addresses
775            .iter()
776            .skip(20)
777            .zip(NATIVE_SEG_WIT_ADDRESSES.iter())
778        {
779            assert_eq!(addr, native_segwit);
780        }
781    }
782}