wallet_fingerprint/
lib.rs

1//! This module contains functions for detecting a wallet given a Bitcoin transaction.
2//! This is a port of Python code from here: https://github.com/ishaanam/wallet-fingerprinting/blob/master/fingerprinting.py
3
4mod global;
5pub mod heuristics;
6mod input;
7mod output;
8mod util;
9
10use bitcoin::transaction::Version;
11use bitcoin::{AddressType, Transaction};
12use std::collections::HashSet;
13
14use crate::global::{address_reuse, signals_rbf, using_uncompressed_pubkeys};
15use crate::input::{
16    get_input_order, get_input_types, low_order_r_grinding, mixed_input_types, InputSortingType,
17};
18use crate::output::{
19    change_type_matched_inputs, get_change_index, get_output_structure, get_output_types,
20    ChangeIndex, ChangeTypeMatchedInputs, OutputStructureType,
21};
22use crate::util::OutputType;
23use crate::{global::is_anti_fee_sniping, util::TxOutWithOutpoint};
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
26enum WalletType {
27    BitcoinCore,
28    Electrum,
29    BlueWallet,
30    Coinbase,
31    Exodus,
32    Trust,
33    Trezor,
34    Ledger,
35    #[allow(unused)]
36    Unclear,
37    Other,
38}
39
40impl WalletType {
41    #[allow(unused)]
42    pub fn as_str(&self) -> &'static str {
43        match self {
44            WalletType::BitcoinCore => "Bitcoin Core",
45            WalletType::Electrum => "Electrum",
46            WalletType::BlueWallet => "Blue Wallet",
47            WalletType::Coinbase => "Coinbase Wallet",
48            WalletType::Exodus => "Exodus Wallet",
49            WalletType::Trust => "Trust Wallet",
50            WalletType::Trezor => "Trezor",
51            WalletType::Ledger => "Ledger",
52            WalletType::Unclear => "Unclear",
53            WalletType::Other => "Other",
54        }
55    }
56}
57
58#[derive(Debug)]
59pub struct Heuristics {
60    pub tx_version: i32,
61    pub anti_fee_snipe: bool,
62    pub low_r_grinding: f32,
63    pub prob_bip69: Option<f64>,
64    pub mixed_input_types: bool,
65    pub maybe_same_change_type: Option<bool>,
66    pub input_types: HashSet<AddressType>,
67}
68
69/// Attempt to detect the wallet type of a transaction
70/// Given the transaction and the previous transactions which are the inputs to the current transaction
71/// TODO: this method is was ported from the python impl and is most likely not up to date
72#[allow(unused)]
73fn detect_wallet(tx: &Transaction, prev_txs: &[Transaction]) -> (HashSet<WalletType>, Vec<String>) {
74    // TODO do some validation on the previous transactions
75    let prev_txouts = tx
76        .input
77        .iter()
78        .map(|txin| TxOutWithOutpoint {
79            txout: prev_txs
80                .iter()
81                .find(|prev_tx| prev_tx.compute_txid() == txin.previous_output.txid)
82                .unwrap()
83                .output[txin.previous_output.vout as usize]
84                .clone(),
85            outpoint: txin.previous_output,
86        })
87        .collect::<Vec<_>>();
88
89    // Sanity checks
90    assert!(prev_txouts.len() == tx.input.len());
91    // assert outpoints match
92    for (prev_txout, txin) in prev_txouts.iter().zip(tx.input.iter()) {
93        assert_eq!(prev_txout.outpoint, txin.previous_output);
94    }
95
96    let mut possible_wallets = HashSet::from([
97        WalletType::BitcoinCore,
98        WalletType::Electrum,
99        WalletType::BlueWallet,
100        WalletType::Coinbase,
101        WalletType::Exodus,
102        WalletType::Trust,
103        WalletType::Trezor,
104        WalletType::Ledger,
105    ]);
106    let mut reasoning = Vec::new();
107
108    // Anti-fee-sniping
109    if is_anti_fee_sniping(tx) {
110        reasoning.push("Anti-fee-sniping".to_string());
111        possible_wallets.retain(|w| *w == WalletType::BitcoinCore || *w == WalletType::Electrum);
112    } else {
113        reasoning.push("No Anti-fee-sniping".to_string());
114        possible_wallets.remove(&WalletType::BitcoinCore);
115        possible_wallets.remove(&WalletType::Electrum);
116    }
117
118    // Uncompressed public keys
119    if !using_uncompressed_pubkeys(tx, &prev_txouts) {
120        reasoning.push("Uncompressed public key(s)".to_string());
121        possible_wallets.clear();
122        return (possible_wallets, reasoning);
123    } else {
124        reasoning.push("All compressed public keys".to_string());
125    }
126
127    // Transaction version
128    match tx.version {
129        Version::ONE => {
130            reasoning.push("nVersion = 1".to_string());
131            possible_wallets.remove(&WalletType::BitcoinCore);
132            possible_wallets.remove(&WalletType::Electrum);
133            possible_wallets.remove(&WalletType::BlueWallet);
134            possible_wallets.remove(&WalletType::Exodus);
135            possible_wallets.remove(&WalletType::Coinbase);
136        }
137        Version::TWO => {
138            reasoning.push("nVersion = 2".to_string());
139            possible_wallets.remove(&WalletType::Ledger);
140            possible_wallets.remove(&WalletType::Trezor);
141            possible_wallets.remove(&WalletType::Trust);
142        }
143        _ => {
144            reasoning.push("non-standard nVersion number".to_string());
145            possible_wallets.clear();
146        }
147    }
148
149    // Low-r signatures
150    if !low_order_r_grinding(tx) {
151        reasoning.push("Not low-r-grinding".to_string());
152        possible_wallets.remove(&WalletType::BitcoinCore);
153        possible_wallets.remove(&WalletType::Electrum);
154    } else {
155        reasoning.push("Low r signatures only".to_string());
156    }
157
158    // RBF
159    if signals_rbf(tx) {
160        reasoning.push("signals RBF".to_string());
161        possible_wallets.remove(&WalletType::Coinbase);
162        possible_wallets.remove(&WalletType::Exodus);
163    } else {
164        reasoning.push("does not signal RBF".to_string());
165        possible_wallets.remove(&WalletType::BitcoinCore);
166        possible_wallets.remove(&WalletType::Electrum);
167        possible_wallets.remove(&WalletType::BlueWallet);
168        possible_wallets.remove(&WalletType::Ledger);
169        possible_wallets.remove(&WalletType::Trezor);
170        possible_wallets.remove(&WalletType::Trust);
171    }
172
173    let input_types = get_input_types(tx, &prev_txouts);
174    if input_types
175        .iter()
176        // TODO: Should differenciate between P2tr key and script spend
177        .any(|t| *t == OutputType::Address(AddressType::P2tr))
178    {
179        reasoning.push("Sends to taproot address".to_string());
180        possible_wallets.remove(&WalletType::Coinbase);
181    }
182    if input_types
183        .iter()
184        .any(|t| *t == OutputType::Opreturn || *t == OutputType::NonStandard)
185    {
186        reasoning.push("Creates OP_RETURN output".to_string());
187        possible_wallets.remove(&WalletType::Coinbase);
188        possible_wallets.remove(&WalletType::Exodus);
189        possible_wallets.remove(&WalletType::BlueWallet);
190        possible_wallets.remove(&WalletType::Ledger);
191        possible_wallets.remove(&WalletType::Trust);
192    }
193
194    // get output types
195    // TODO: these output types are super outdate now
196    let output_types = get_output_types(tx);
197    if output_types
198        .iter()
199        .any(|t| t == &OutputType::Address(AddressType::P2tr))
200    {
201        reasoning.push("Spends taproot output".to_string());
202        possible_wallets.remove(&WalletType::Coinbase);
203        possible_wallets.remove(&WalletType::Exodus);
204        possible_wallets.remove(&WalletType::Electrum);
205        possible_wallets.remove(&WalletType::BlueWallet);
206        possible_wallets.remove(&WalletType::Ledger);
207        possible_wallets.remove(&WalletType::Trust);
208    }
209    if output_types
210        .iter()
211        .any(|t| t == &OutputType::Address(AddressType::P2wsh))
212    {
213        possible_wallets.remove(&WalletType::Coinbase);
214        possible_wallets.remove(&WalletType::Exodus);
215        possible_wallets.remove(&WalletType::Trust);
216        possible_wallets.remove(&WalletType::Trezor);
217    }
218    if output_types
219        .iter()
220        .any(|t| t == &OutputType::Address(AddressType::P2pkh))
221    {
222        reasoning.push("Spends P2PKH output".to_string());
223        possible_wallets.remove(&WalletType::Exodus);
224        possible_wallets.remove(&WalletType::Trust);
225    }
226
227    // Multi-type vin
228    if mixed_input_types(tx, &prev_txouts) {
229        reasoning.push("Has multi-type vin".to_string());
230        possible_wallets.remove(&WalletType::Exodus);
231        possible_wallets.remove(&WalletType::Electrum);
232        possible_wallets.remove(&WalletType::BlueWallet);
233        possible_wallets.remove(&WalletType::Ledger);
234        possible_wallets.remove(&WalletType::Trezor);
235        possible_wallets.remove(&WalletType::Trust);
236    }
237
238    // Change type matched inputs/outputs
239    let change_matched_inputs = change_type_matched_inputs(tx, &prev_txouts);
240    if matches!(
241        change_matched_inputs,
242        ChangeTypeMatchedInputs::ChangeMatchesOutputsTypes
243    ) {
244        reasoning.push("Change type matched outputs".to_string());
245        if possible_wallets.contains(&WalletType::BitcoinCore) {
246            possible_wallets = HashSet::from([WalletType::BitcoinCore]);
247        } else {
248            possible_wallets.clear();
249        }
250    } else if matches!(
251        change_matched_inputs,
252        ChangeTypeMatchedInputs::ChangeMatchesInputsTypes
253    ) {
254        reasoning.push("Change type matched inputs".to_string());
255        possible_wallets.remove(&WalletType::BitcoinCore);
256    }
257
258    // Address reuse
259    if address_reuse(tx, &prev_txouts) {
260        reasoning.push("Address reuse between vin and vout".to_string());
261        possible_wallets.remove(&WalletType::Coinbase);
262        possible_wallets.remove(&WalletType::BitcoinCore);
263        possible_wallets.remove(&WalletType::Electrum);
264        possible_wallets.remove(&WalletType::BlueWallet);
265        possible_wallets.remove(&WalletType::Ledger);
266        possible_wallets.remove(&WalletType::Trezor);
267    } else {
268        reasoning.push("No address reuse between vin and vout".to_string());
269        possible_wallets.remove(&WalletType::Exodus);
270        possible_wallets.remove(&WalletType::Trust);
271    }
272
273    // Input/output structure
274    let input_order = get_input_order(tx, &prev_txouts);
275    println!("input_order: {:?}", input_order);
276    let output_structure = get_output_structure(tx, &prev_txouts);
277
278    if output_structure.contains(&OutputStructureType::Multi) {
279        reasoning.push("More than 2 outputs".to_string());
280        possible_wallets.remove(&WalletType::Coinbase);
281        possible_wallets.remove(&WalletType::Exodus);
282        possible_wallets.remove(&WalletType::Ledger);
283        possible_wallets.remove(&WalletType::Trust);
284    }
285
286    if !output_structure.contains(&OutputStructureType::Bip69) {
287        reasoning.push("BIP-69 not followed by outputs".to_string());
288        possible_wallets.remove(&WalletType::Electrum);
289        possible_wallets.remove(&WalletType::Trezor);
290    } else {
291        reasoning.push("BIP-69 followed by outputs".to_string());
292    }
293
294    if !input_order.contains(&InputSortingType::Single) {
295        if !input_order.contains(&InputSortingType::Bip69) {
296            reasoning.push("BIP-69 not followed by inputs".to_string());
297            possible_wallets.remove(&WalletType::Electrum);
298            possible_wallets.remove(&WalletType::Trezor);
299        } else {
300            reasoning.push("BIP-69 followed by inputs".to_string());
301        }
302        // TODO: historical input sorting not supported until we can have # of confirmations passed in
303        // if !input_order.contains(&InputSortingType::Historical) {
304        //     reasoning.push("Inputs not ordered historically".to_string());
305        //     possible_wallets.remove(&WalletType::Ledger);
306        // } else {
307        //     reasoning.push("Inputs ordered historically".to_string());
308        // }
309    }
310
311    // Change index
312    let change_index = get_change_index(tx, &prev_txouts);
313    if let ChangeIndex::Found(idx) = change_index {
314        if idx != tx.output.len() - 1 {
315            reasoning.push("Last index is not change".to_string());
316            possible_wallets.remove(&WalletType::Ledger);
317            possible_wallets.remove(&WalletType::BlueWallet);
318            possible_wallets.remove(&WalletType::Coinbase);
319        } else {
320            reasoning.push("Last index is change".to_string());
321        }
322    }
323
324    if possible_wallets.is_empty() {
325        return (HashSet::from([WalletType::Other]), reasoning);
326    }
327
328    (possible_wallets, reasoning)
329}
330
331#[cfg(test)]
332mod tests {
333    use bitcoin::consensus::Decodable;
334    use hex;
335
336    use super::*;
337
338    fn get_tx_from_hex(hex: &str) -> Transaction {
339        let reader = hex::decode(hex).unwrap();
340        Transaction::consensus_decode(&mut reader.as_slice()).unwrap()
341    }
342
343    // Test vectors
344    #[test]
345    fn test_detect_wallet() {
346        struct TestVector {
347            tx: Transaction,
348            prev_txs: Vec<Transaction>,
349            expected_wallets: HashSet<WalletType>,
350        }
351        let test_vectors = vec![
352        // Elecrum: 5d857401648a667303cde43295bce1326e6329353eac3dddf15b151e701405e7    
353        TestVector {
354            tx: get_tx_from_hex("02000000000102ac5718a0e7b3ee13ce2f273aa9c6a04becf8a1696edb75d3217c0d3790a620860000000000fdffffff74e1d8045cfe6b823943db609ceb3aa13216a936a9e18b92e26db770a8e4eae60000000000fdffffff02f6250000000000001600145333aa7bcef7bd632edaf5a326d4c6085417282d133f0000000000001976a914c8f57d6b8bc08fa211c71b8d255e7c4b25bd432288ac02473044022037059673792d5af9ab1cf5fc8ccf3c1c1ad300e9e6c25edda7a172e455d49e07022046d2c2638c129a8c9a54ca5adb5df01bde564066c36edade43c3845b3d25940101210202ca6c82b9cc52f7a8c34de6a6ccd807d8437a8368ddf7638a2b50002e745b360247304402207b3d3c39ee66bdaa509094072ae629794bd7ef0f14694f0e3695d89ed573c57202205cc9b6d059500ccf621621a657115e33c51064efad2dcf352ad32c69b0ae6ab301210202ca6c82b9cc52f7a8c34de6a6ccd807d8437a8368ddf7638a2b50002e745b3670360c00"),
355            prev_txs: vec![
356            get_tx_from_hex("01000000000101b6d971c9ca363c5f901780d578bd0449d74b80bb565f367d56278c3b1601f94301000000000000000001f41400000000000016001460ac2a83f14bdc2016edf615138aabdd52d6c331024730440220560c4bdf1acc416517bd9d50ef65f0a99ac1633a5b1a7a3cb69ee486ed688a3a022079db25e85e6b34690456ad49f952302a80e1c146a7bc7af5387e92c2d4277c7a01210281bfdda07273f79522c04bff9e43c03655ebf96e482c8f3e262ccb5551c969f200000000"),
357            get_tx_from_hex("02000000000101b6d971c9ca363c5f901780d578bd0449d74b80bb565f367d56278c3b1601f9430000000000fdffffff019e5700000000000016001460ac2a83f14bdc2016edf615138aabdd52d6c331014079a93a95b32520c99a08cfae6f1dfca31242359ca42ba56873cf2be60f472ea330ab7273753602fa362ce106287b365bae5542cb7358157641d8e2a7a052245400000000")
358            ],
359            expected_wallets: HashSet::from([WalletType::Electrum]),
360        },
361            // Ledger: C1094c70a9b23ca5d755234cffefca69f639d7a938f745dfd1190cc9c9d8b5ad
362            TestVector {
363                tx: get_tx_from_hex("010000000001039201ee164de0fe87bb1557be1b59270210ac793869d3e5149aa8c2d02b5d47d40000000000000000002becf7dd346f05756bba071eb894ccbf74f5ae9ca24b4a11159188f6b9b6f4850000000000000000008dc5773f385757f87bee0c4b64b5b85f4a12af0a6fa396cf18d50d8cb43b54af0000000000000000000298180100000000001976a9149c4075e0b1718eceb2322cfa1a8ab25b033a8aa988acf90c000000000000160014d0202edd81a21eab5a1637a616d5fcaccceea876024730440220630d494285d69bf6897f1b9326c034f899a6e1bc6485c925b5dcf1843a287daa022039cc491eff85a22d9e056017ca4e8873f8cec15985b5c8afcd7d2a867cc5d9210121033053287e92b72914ad0f95788112e028fb3c05de55e07ee19e66270568d871df0247304402200bd6e3104f853408de60dad1bcbbbade32f6e87a13736c5ef91652aa1ed5ac2302201dff726970330dd7608adaec9376cfe6b75a52d3c2c4ef56ccab0e8f1c138bdf0121029962e24537d5c9de63269f90fa6d89cd8b46a1580f7c7d30ab9e7990c668f92c0247304402207cde943346d08076876825b7b9763effce507a890f9c8c388d1c9b9d21f804bd02203f1f84b6264328e17d57d46ae006bc495a6417a1c8e66305e557c435e49771eb012103629299e79f95dec998663d5bd2cb9856726c81bde98791aa0622253510ed2ec500000000"),
364                prev_txs: vec![
365                    get_tx_from_hex("02000000000101160940344ab4e4c19877910c3584c57a1899a2903031056c9df0c68568d710080000000000fdffffff029442000000000000160014ba2ec40badac5c116a3aaa3e5ef52196e7d358af4c39000000000000160014b749341796e04d189fb7a9f3f4b56a71432b939202473044022078603bb9313bbe500e8599c305e7cc18f71a6abfa62890e4177aa3193094e34002200127ac6bfd56df9a29f1fcc2c655d153b3ee45c462e9e63844900fcdb2f27278012103b6e92d92aef77e32076052a4376bd2ce5fd78a18344b9df1db5c8c809991cee600000000"),
366                    get_tx_from_hex("02000000011d040c7807779db11afc738beba87aed8104bc6bd30f892d8528ebfc79177b04000000006b483045022100f39d0f64f73bd335e014d13ed46e4cbacae89b0b014d7eb08b1eacfd7148da0a0220286699c7f12d8e1ef6770971b2aa19f4864bdeb1ea9e5137ea4138c4c7e9294f0121024b48ce8bdd016ce2e1538d0d4c9570eab7ecfedab348e8d89c92b88cd35fa0ebffffffff01d7ad0000000000001600145452750cd65d903f76e4bdbb99850584ade8357400000000"),
367                    get_tx_from_hex("02000000000102c4ceb3f8be27f4af334cd6a1a1bf6cdf47a4937e54e3d549d08cb927edbfd5010000000000fdffffff9201ee164de0fe87bb1557be1b59270210ac793869d3e5149aa8c2d02b5d47d40100000000fdffffff01ae46000000000000160014b9de4f9f5c61e643fbc078c90beb6162b40abf4e02483045022100c3ab67bd13cbdfad7352ac514de1a02923834f40d0bbfc093d695c6205166cbb022010c13d427fc9d3ffcbb883fa849f6de22e513883782f2d57445335885bd013fe012103b6e92d92aef77e32076052a4376bd2ce5fd78a18344b9df1db5c8c809991cee602483045022100a1957c757c983306de87357d8a541ca659495b2b441db3a9fc9fd3622033ac1e02207394dc48c19d9c55348f076780ed475686d8a5f5365054dd94756929fb5e883d012102ed13f37ca6c7a478b120b5cc126828a145285a7273f1c75994517838e31064fe00000000"),
368                ],
369                expected_wallets: HashSet::from([WalletType::Ledger]),
370            },
371            // Trezor: 87670b12778d17c759db459479d66acfd1c4d444094270991d8e1de09a56cc7c
372            TestVector {
373                tx: get_tx_from_hex("01000000000103c54d8c88e4f5d43bd0afd365ab8af7688af9ca8d5c10dcb86519a924dd3a12e30000000000fdffffffc54d8c88e4f5d43bd0afd365ab8af7688af9ca8d5c10dcb86519a924dd3a12e30100000000fdffffffc54d8c88e4f5d43bd0afd365ab8af7688af9ca8d5c10dcb86519a924dd3a12e30200000000fdffffff03cf0a000000000000160014b47e4a3828865a23bb63da619b40bc3ec586480bb471000000000000160014eee06789bad1948746d16d69f6e698c99f62c341b4710000000000001976a9145b3263a7adcbd55ea653edfc4e4c04945a303a3788ac02483045022100a24d87256cdf7d63e526f7832282341d8d6c727c7c6aba536d7fa89a39522a4f022049a9e4d92c41fd99edd17c0f8614fd8421413b71e763f90dba6fb164a062a8b30121020c0bd6c738c36c415734e2d05614f861a083970c7cbe4a7b1db2bea740a9e54602473044022018234159f2a1085eab3f318a8596ecf9d3cbfeec3d3f46b3c47bc30bb3946c6d0220278c82c5bbdf1bef7ceb39bf904ffe72f88c43af598096b2569c1f1a51d67d6c0121020c0bd6c738c36c415734e2d05614f861a083970c7cbe4a7b1db2bea740a9e54602483045022100c1df2dbedcf0dc8c9b19098aeb9e6b2daead5b17bfd038922fa6480cc90c529202206fb4f7c0c81ed56eadc8e5771584fd36a877ffb750151a4b0ffbc5e16ab311b00121020c0bd6c738c36c415734e2d05614f861a083970c7cbe4a7b1db2bea740a9e54600000000"),
374                prev_txs: vec![
375                    get_tx_from_hex("0200000001adb5d8c9c90c19d1df45f738a9d739f669caefff4c2355d7a53cb2a9704c09c1000000006a47304402205825a5dcf15947113796f2da4f891ad39d5f1f761f4716770143cd470610e1ec0220261e1abe8ecf908ee718149d3587e9440ce96d9c8e680b34f306b8a405c2ae470121020b8a58237f6650d658730f5945c5fa9284c494040fefd8b6f33a2ac49862aa42ffffffff03895d00000000000016001444e650ca651d519813b57dc387a54b2c33016520cf4200000000000016001444e650ca651d519813b57dc387a54b2c33016520f46400000000000016001444e650ca651d519813b57dc387a54b2c3301652000000000"),
376                ],
377                expected_wallets: HashSet::from([WalletType::Trezor])
378            },
379            // Blue wallet: 1bf659e17568e48d6f47bb5470bc8df567cfe89d79c6e38cafbe798f43d5da22
380            TestVector {
381                tx: get_tx_from_hex("02000000000103570a26bfae0867b97212558189f80cdd44a2a24ff9d76b6dac599a73ed84d22f000000000000000080570a26bfae0867b97212558189f80cdd44a2a24ff9d76b6dac599a73ed84d22f010000000000000080570a26bfae0867b97212558189f80cdd44a2a24ff9d76b6dac599a73ed84d22f02000000000000008003a0860100000000001600147d2741001a5502d0db282c640d376ea7c5c5ba75a00f000000000000160014b4bf9b7ceab50cf7474b260aee1f5d880773b0e12ca100000000000016001442d0b6bc4040fd50aaadf3c6616d9ae0c42fa8db02483045022100fe6f638daa02a2de220ae87ffe212f55a0587147370284db8fa0c73f0a9f569402207b97624856cf66e1649c6459273ad3b0221906e1478154f0c7c9323e32f6fa65012103f307c3e3031babd35634a0f7989798ff6523d9267408828405f4873a10fde8ee02483045022100d4b3ac0e3a3eaa22667ada79693a322530b174e9e5821f0de29afdd1efbe1c4b022077f3eb4b7ea2a6eb49db2ba05c7ec8f461f14f2c4b8303ace17f12d17124aa2a012103bb5d910d5d7a336e8cc30effbca4a7bc7e01febd558523f709d62bb31c3215d40247304402203081506178cfe98d2b4ebeb17d89a3f9ad06966118fdffc01e173a1bd030c6950220667ac8c26f24e9e176aabe840e563e83b8112279b101373b5493e915fa5b29fb0121021ea4ff443e6b5db8c95ed934bf113fc6e5d740d166eb66aa6a5accbd990e064500000000"),
382                prev_txs: vec![
383                    get_tx_from_hex("020000000001013f17fa5fdc451e6fba6ac2fa02592af9ba8ee5f69b400f3559e23bc68ab8db2b0000000000000000800450c300000000000016001471e2c1575903a000d1486f9cbec0a245ecb9c19e50c3000000000000160014b536927be1e633e6674e1f36b8c8ee310adf2da150c3000000000000160014addbf648bada5bceca425289105731b09f434347cd9900000000000016001411385cc2c893fdef44ef6dd458241b19e5b3ffd202483045022100f42fe40dbacc20e40cd2e4c1ba86e3c38afec96528681af5335fa8c7c33aa6aa02202af9c25393097cd93a83a37d7a24702e302405a047b9e9f166209deb13ec821701210386ccea785809b6e69a1ed483c119e993a425a8bb100042f9f3d0dffda283a24700000000"),
384                ],
385                expected_wallets: HashSet::from([WalletType::BlueWallet])
386            },
387            // Exodus: 6f8c37db6ed88bfd0fd483963ebf06c5557326f8d2a3617af5ceba878442e1ad
388            TestVector {
389                tx: get_tx_from_hex("020000000001011309192e20a892daee269de43babb203a1ff68ae996406ca8b56ed9e8bca7d810000000000ffffffff02e91a000000000000160014fe3f8293b01b1d32db8dfc5ccd9a595e5af189b26f33000000000000160014ffed07852461fcef0ef3e2dd6ed598614037bb2902483045022100984153898e29ab101b666443ba1ca73f823ffd951347257f183afeeb5edac83a02204df7a36fb67d71089cf9d34be8c9c1ff7e8a30b33b96023e55e230c573f4f5bb01210315d9ffabd251ae57cd2a6843bf207e73ac95eeda9db75043bc0d18306f43be4d00000000"),
390                prev_txs: vec![
391                    get_tx_from_hex("020000000001017cca6cb0ed3a291dc8f385ba17100ea2749e56aea344dae6ded3bcd56a5af91600000000000000008001125b000000000000160014ffed07852461fcef0ef3e2dd6ed598614037bb2902483045022100ef12adecd8ada80560d64421707c653b19d11039e3a54e989433b8dc5d8aadb70220448d557e548767ee0652851e81dab1fe732c5d0af85634715956e397dcd25548012103a7a4f8c99a2ddf4fde317023fb73cee4d1b3191a20e722af88614857688f4f8400000000"),
392                ],
393                expected_wallets: HashSet::from([WalletType::Exodus])
394            },
395        ];
396        fn do_test(test_vector: TestVector) {
397            let (wallets, reasoning) = detect_wallet(&test_vector.tx, &test_vector.prev_txs);
398            let expected_wallets = test_vector.expected_wallets;
399            println!("wallets: {:?}", wallets);
400            println!("reasoning: {:?}", reasoning);
401            assert_eq!(wallets, expected_wallets);
402        }
403
404        for test_vector in test_vectors {
405            do_test(test_vector);
406        }
407    }
408}