wallet-fingerprint 0.0.1

A library for detecting wallet fingerprints in Bitcoin transactions
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
//! This module contains functions for detecting a wallet given a Bitcoin transaction.
//! This is a port of Python code from here: https://github.com/ishaanam/wallet-fingerprinting/blob/master/fingerprinting.py

mod global;
pub mod heuristics;
mod input;
mod output;
mod util;

use bitcoin::transaction::Version;
use bitcoin::{AddressType, Transaction};
use std::collections::HashSet;

use crate::global::{address_reuse, signals_rbf, using_uncompressed_pubkeys};
use crate::input::{
    get_input_order, get_input_types, low_order_r_grinding, mixed_input_types, InputSortingType,
};
use crate::output::{
    change_type_matched_inputs, get_change_index, get_output_structure, get_output_types,
    ChangeIndex, ChangeTypeMatchedInputs, OutputStructureType,
};
use crate::util::OutputType;
use crate::{global::is_anti_fee_sniping, util::TxOutWithOutpoint};

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum WalletType {
    BitcoinCore,
    Electrum,
    BlueWallet,
    Coinbase,
    Exodus,
    Trust,
    Trezor,
    Ledger,
    #[allow(unused)]
    Unclear,
    Other,
}

impl WalletType {
    #[allow(unused)]
    pub fn as_str(&self) -> &'static str {
        match self {
            WalletType::BitcoinCore => "Bitcoin Core",
            WalletType::Electrum => "Electrum",
            WalletType::BlueWallet => "Blue Wallet",
            WalletType::Coinbase => "Coinbase Wallet",
            WalletType::Exodus => "Exodus Wallet",
            WalletType::Trust => "Trust Wallet",
            WalletType::Trezor => "Trezor",
            WalletType::Ledger => "Ledger",
            WalletType::Unclear => "Unclear",
            WalletType::Other => "Other",
        }
    }
}

#[derive(Debug)]
pub struct Heuristics {
    pub tx_version: i32,
    pub anti_fee_snipe: bool,
    pub low_r_grinding: f32,
    pub prob_bip69: Option<f64>,
    pub mixed_input_types: bool,
    pub maybe_same_change_type: Option<bool>,
    pub input_types: HashSet<AddressType>,
}

/// Attempt to detect the wallet type of a transaction
/// Given the transaction and the previous transactions which are the inputs to the current transaction
/// TODO: this method is was ported from the python impl and is most likely not up to date
#[allow(unused)]
fn detect_wallet(tx: &Transaction, prev_txs: &[Transaction]) -> (HashSet<WalletType>, Vec<String>) {
    // TODO do some validation on the previous transactions
    let prev_txouts = tx
        .input
        .iter()
        .map(|txin| TxOutWithOutpoint {
            txout: prev_txs
                .iter()
                .find(|prev_tx| prev_tx.compute_txid() == txin.previous_output.txid)
                .unwrap()
                .output[txin.previous_output.vout as usize]
                .clone(),
            outpoint: txin.previous_output,
        })
        .collect::<Vec<_>>();

    // Sanity checks
    assert!(prev_txouts.len() == tx.input.len());
    // assert outpoints match
    for (prev_txout, txin) in prev_txouts.iter().zip(tx.input.iter()) {
        assert_eq!(prev_txout.outpoint, txin.previous_output);
    }

    let mut possible_wallets = HashSet::from([
        WalletType::BitcoinCore,
        WalletType::Electrum,
        WalletType::BlueWallet,
        WalletType::Coinbase,
        WalletType::Exodus,
        WalletType::Trust,
        WalletType::Trezor,
        WalletType::Ledger,
    ]);
    let mut reasoning = Vec::new();

    // Anti-fee-sniping
    if is_anti_fee_sniping(tx) {
        reasoning.push("Anti-fee-sniping".to_string());
        possible_wallets.retain(|w| *w == WalletType::BitcoinCore || *w == WalletType::Electrum);
    } else {
        reasoning.push("No Anti-fee-sniping".to_string());
        possible_wallets.remove(&WalletType::BitcoinCore);
        possible_wallets.remove(&WalletType::Electrum);
    }

    // Uncompressed public keys
    if !using_uncompressed_pubkeys(tx, &prev_txouts) {
        reasoning.push("Uncompressed public key(s)".to_string());
        possible_wallets.clear();
        return (possible_wallets, reasoning);
    } else {
        reasoning.push("All compressed public keys".to_string());
    }

    // Transaction version
    match tx.version {
        Version::ONE => {
            reasoning.push("nVersion = 1".to_string());
            possible_wallets.remove(&WalletType::BitcoinCore);
            possible_wallets.remove(&WalletType::Electrum);
            possible_wallets.remove(&WalletType::BlueWallet);
            possible_wallets.remove(&WalletType::Exodus);
            possible_wallets.remove(&WalletType::Coinbase);
        }
        Version::TWO => {
            reasoning.push("nVersion = 2".to_string());
            possible_wallets.remove(&WalletType::Ledger);
            possible_wallets.remove(&WalletType::Trezor);
            possible_wallets.remove(&WalletType::Trust);
        }
        _ => {
            reasoning.push("non-standard nVersion number".to_string());
            possible_wallets.clear();
        }
    }

    // Low-r signatures
    if !low_order_r_grinding(tx) {
        reasoning.push("Not low-r-grinding".to_string());
        possible_wallets.remove(&WalletType::BitcoinCore);
        possible_wallets.remove(&WalletType::Electrum);
    } else {
        reasoning.push("Low r signatures only".to_string());
    }

    // RBF
    if signals_rbf(tx) {
        reasoning.push("signals RBF".to_string());
        possible_wallets.remove(&WalletType::Coinbase);
        possible_wallets.remove(&WalletType::Exodus);
    } else {
        reasoning.push("does not signal RBF".to_string());
        possible_wallets.remove(&WalletType::BitcoinCore);
        possible_wallets.remove(&WalletType::Electrum);
        possible_wallets.remove(&WalletType::BlueWallet);
        possible_wallets.remove(&WalletType::Ledger);
        possible_wallets.remove(&WalletType::Trezor);
        possible_wallets.remove(&WalletType::Trust);
    }

    let input_types = get_input_types(tx, &prev_txouts);
    if input_types
        .iter()
        // TODO: Should differenciate between P2tr key and script spend
        .any(|t| *t == OutputType::Address(AddressType::P2tr))
    {
        reasoning.push("Sends to taproot address".to_string());
        possible_wallets.remove(&WalletType::Coinbase);
    }
    if input_types
        .iter()
        .any(|t| *t == OutputType::Opreturn || *t == OutputType::NonStandard)
    {
        reasoning.push("Creates OP_RETURN output".to_string());
        possible_wallets.remove(&WalletType::Coinbase);
        possible_wallets.remove(&WalletType::Exodus);
        possible_wallets.remove(&WalletType::BlueWallet);
        possible_wallets.remove(&WalletType::Ledger);
        possible_wallets.remove(&WalletType::Trust);
    }

    // get output types
    // TODO: these output types are super outdate now
    let output_types = get_output_types(tx);
    if output_types
        .iter()
        .any(|t| t == &OutputType::Address(AddressType::P2tr))
    {
        reasoning.push("Spends taproot output".to_string());
        possible_wallets.remove(&WalletType::Coinbase);
        possible_wallets.remove(&WalletType::Exodus);
        possible_wallets.remove(&WalletType::Electrum);
        possible_wallets.remove(&WalletType::BlueWallet);
        possible_wallets.remove(&WalletType::Ledger);
        possible_wallets.remove(&WalletType::Trust);
    }
    if output_types
        .iter()
        .any(|t| t == &OutputType::Address(AddressType::P2wsh))
    {
        possible_wallets.remove(&WalletType::Coinbase);
        possible_wallets.remove(&WalletType::Exodus);
        possible_wallets.remove(&WalletType::Trust);
        possible_wallets.remove(&WalletType::Trezor);
    }
    if output_types
        .iter()
        .any(|t| t == &OutputType::Address(AddressType::P2pkh))
    {
        reasoning.push("Spends P2PKH output".to_string());
        possible_wallets.remove(&WalletType::Exodus);
        possible_wallets.remove(&WalletType::Trust);
    }

    // Multi-type vin
    if mixed_input_types(tx, &prev_txouts) {
        reasoning.push("Has multi-type vin".to_string());
        possible_wallets.remove(&WalletType::Exodus);
        possible_wallets.remove(&WalletType::Electrum);
        possible_wallets.remove(&WalletType::BlueWallet);
        possible_wallets.remove(&WalletType::Ledger);
        possible_wallets.remove(&WalletType::Trezor);
        possible_wallets.remove(&WalletType::Trust);
    }

    // Change type matched inputs/outputs
    let change_matched_inputs = change_type_matched_inputs(tx, &prev_txouts);
    if matches!(
        change_matched_inputs,
        ChangeTypeMatchedInputs::ChangeMatchesOutputsTypes
    ) {
        reasoning.push("Change type matched outputs".to_string());
        if possible_wallets.contains(&WalletType::BitcoinCore) {
            possible_wallets = HashSet::from([WalletType::BitcoinCore]);
        } else {
            possible_wallets.clear();
        }
    } else if matches!(
        change_matched_inputs,
        ChangeTypeMatchedInputs::ChangeMatchesInputsTypes
    ) {
        reasoning.push("Change type matched inputs".to_string());
        possible_wallets.remove(&WalletType::BitcoinCore);
    }

    // Address reuse
    if address_reuse(tx, &prev_txouts) {
        reasoning.push("Address reuse between vin and vout".to_string());
        possible_wallets.remove(&WalletType::Coinbase);
        possible_wallets.remove(&WalletType::BitcoinCore);
        possible_wallets.remove(&WalletType::Electrum);
        possible_wallets.remove(&WalletType::BlueWallet);
        possible_wallets.remove(&WalletType::Ledger);
        possible_wallets.remove(&WalletType::Trezor);
    } else {
        reasoning.push("No address reuse between vin and vout".to_string());
        possible_wallets.remove(&WalletType::Exodus);
        possible_wallets.remove(&WalletType::Trust);
    }

    // Input/output structure
    let input_order = get_input_order(tx, &prev_txouts);
    println!("input_order: {:?}", input_order);
    let output_structure = get_output_structure(tx, &prev_txouts);

    if output_structure.contains(&OutputStructureType::Multi) {
        reasoning.push("More than 2 outputs".to_string());
        possible_wallets.remove(&WalletType::Coinbase);
        possible_wallets.remove(&WalletType::Exodus);
        possible_wallets.remove(&WalletType::Ledger);
        possible_wallets.remove(&WalletType::Trust);
    }

    if !output_structure.contains(&OutputStructureType::Bip69) {
        reasoning.push("BIP-69 not followed by outputs".to_string());
        possible_wallets.remove(&WalletType::Electrum);
        possible_wallets.remove(&WalletType::Trezor);
    } else {
        reasoning.push("BIP-69 followed by outputs".to_string());
    }

    if !input_order.contains(&InputSortingType::Single) {
        if !input_order.contains(&InputSortingType::Bip69) {
            reasoning.push("BIP-69 not followed by inputs".to_string());
            possible_wallets.remove(&WalletType::Electrum);
            possible_wallets.remove(&WalletType::Trezor);
        } else {
            reasoning.push("BIP-69 followed by inputs".to_string());
        }
        // TODO: historical input sorting not supported until we can have # of confirmations passed in
        // if !input_order.contains(&InputSortingType::Historical) {
        //     reasoning.push("Inputs not ordered historically".to_string());
        //     possible_wallets.remove(&WalletType::Ledger);
        // } else {
        //     reasoning.push("Inputs ordered historically".to_string());
        // }
    }

    // Change index
    let change_index = get_change_index(tx, &prev_txouts);
    if let ChangeIndex::Found(idx) = change_index {
        if idx != tx.output.len() - 1 {
            reasoning.push("Last index is not change".to_string());
            possible_wallets.remove(&WalletType::Ledger);
            possible_wallets.remove(&WalletType::BlueWallet);
            possible_wallets.remove(&WalletType::Coinbase);
        } else {
            reasoning.push("Last index is change".to_string());
        }
    }

    if possible_wallets.is_empty() {
        return (HashSet::from([WalletType::Other]), reasoning);
    }

    (possible_wallets, reasoning)
}

#[cfg(test)]
mod tests {
    use bitcoin::consensus::Decodable;
    use hex;

    use super::*;

    fn get_tx_from_hex(hex: &str) -> Transaction {
        let reader = hex::decode(hex).unwrap();
        Transaction::consensus_decode(&mut reader.as_slice()).unwrap()
    }

    // Test vectors
    #[test]
    fn test_detect_wallet() {
        struct TestVector {
            tx: Transaction,
            prev_txs: Vec<Transaction>,
            expected_wallets: HashSet<WalletType>,
        }
        let test_vectors = vec![
        // Elecrum: 5d857401648a667303cde43295bce1326e6329353eac3dddf15b151e701405e7    
        TestVector {
            tx: get_tx_from_hex("02000000000102ac5718a0e7b3ee13ce2f273aa9c6a04becf8a1696edb75d3217c0d3790a620860000000000fdffffff74e1d8045cfe6b823943db609ceb3aa13216a936a9e18b92e26db770a8e4eae60000000000fdffffff02f6250000000000001600145333aa7bcef7bd632edaf5a326d4c6085417282d133f0000000000001976a914c8f57d6b8bc08fa211c71b8d255e7c4b25bd432288ac02473044022037059673792d5af9ab1cf5fc8ccf3c1c1ad300e9e6c25edda7a172e455d49e07022046d2c2638c129a8c9a54ca5adb5df01bde564066c36edade43c3845b3d25940101210202ca6c82b9cc52f7a8c34de6a6ccd807d8437a8368ddf7638a2b50002e745b360247304402207b3d3c39ee66bdaa509094072ae629794bd7ef0f14694f0e3695d89ed573c57202205cc9b6d059500ccf621621a657115e33c51064efad2dcf352ad32c69b0ae6ab301210202ca6c82b9cc52f7a8c34de6a6ccd807d8437a8368ddf7638a2b50002e745b3670360c00"),
            prev_txs: vec![
            get_tx_from_hex("01000000000101b6d971c9ca363c5f901780d578bd0449d74b80bb565f367d56278c3b1601f94301000000000000000001f41400000000000016001460ac2a83f14bdc2016edf615138aabdd52d6c331024730440220560c4bdf1acc416517bd9d50ef65f0a99ac1633a5b1a7a3cb69ee486ed688a3a022079db25e85e6b34690456ad49f952302a80e1c146a7bc7af5387e92c2d4277c7a01210281bfdda07273f79522c04bff9e43c03655ebf96e482c8f3e262ccb5551c969f200000000"),
            get_tx_from_hex("02000000000101b6d971c9ca363c5f901780d578bd0449d74b80bb565f367d56278c3b1601f9430000000000fdffffff019e5700000000000016001460ac2a83f14bdc2016edf615138aabdd52d6c331014079a93a95b32520c99a08cfae6f1dfca31242359ca42ba56873cf2be60f472ea330ab7273753602fa362ce106287b365bae5542cb7358157641d8e2a7a052245400000000")
            ],
            expected_wallets: HashSet::from([WalletType::Electrum]),
        },
            // Ledger: C1094c70a9b23ca5d755234cffefca69f639d7a938f745dfd1190cc9c9d8b5ad
            TestVector {
                tx: get_tx_from_hex("010000000001039201ee164de0fe87bb1557be1b59270210ac793869d3e5149aa8c2d02b5d47d40000000000000000002becf7dd346f05756bba071eb894ccbf74f5ae9ca24b4a11159188f6b9b6f4850000000000000000008dc5773f385757f87bee0c4b64b5b85f4a12af0a6fa396cf18d50d8cb43b54af0000000000000000000298180100000000001976a9149c4075e0b1718eceb2322cfa1a8ab25b033a8aa988acf90c000000000000160014d0202edd81a21eab5a1637a616d5fcaccceea876024730440220630d494285d69bf6897f1b9326c034f899a6e1bc6485c925b5dcf1843a287daa022039cc491eff85a22d9e056017ca4e8873f8cec15985b5c8afcd7d2a867cc5d9210121033053287e92b72914ad0f95788112e028fb3c05de55e07ee19e66270568d871df0247304402200bd6e3104f853408de60dad1bcbbbade32f6e87a13736c5ef91652aa1ed5ac2302201dff726970330dd7608adaec9376cfe6b75a52d3c2c4ef56ccab0e8f1c138bdf0121029962e24537d5c9de63269f90fa6d89cd8b46a1580f7c7d30ab9e7990c668f92c0247304402207cde943346d08076876825b7b9763effce507a890f9c8c388d1c9b9d21f804bd02203f1f84b6264328e17d57d46ae006bc495a6417a1c8e66305e557c435e49771eb012103629299e79f95dec998663d5bd2cb9856726c81bde98791aa0622253510ed2ec500000000"),
                prev_txs: vec![
                    get_tx_from_hex("02000000000101160940344ab4e4c19877910c3584c57a1899a2903031056c9df0c68568d710080000000000fdffffff029442000000000000160014ba2ec40badac5c116a3aaa3e5ef52196e7d358af4c39000000000000160014b749341796e04d189fb7a9f3f4b56a71432b939202473044022078603bb9313bbe500e8599c305e7cc18f71a6abfa62890e4177aa3193094e34002200127ac6bfd56df9a29f1fcc2c655d153b3ee45c462e9e63844900fcdb2f27278012103b6e92d92aef77e32076052a4376bd2ce5fd78a18344b9df1db5c8c809991cee600000000"),
                    get_tx_from_hex("02000000011d040c7807779db11afc738beba87aed8104bc6bd30f892d8528ebfc79177b04000000006b483045022100f39d0f64f73bd335e014d13ed46e4cbacae89b0b014d7eb08b1eacfd7148da0a0220286699c7f12d8e1ef6770971b2aa19f4864bdeb1ea9e5137ea4138c4c7e9294f0121024b48ce8bdd016ce2e1538d0d4c9570eab7ecfedab348e8d89c92b88cd35fa0ebffffffff01d7ad0000000000001600145452750cd65d903f76e4bdbb99850584ade8357400000000"),
                    get_tx_from_hex("02000000000102c4ceb3f8be27f4af334cd6a1a1bf6cdf47a4937e54e3d549d08cb927edbfd5010000000000fdffffff9201ee164de0fe87bb1557be1b59270210ac793869d3e5149aa8c2d02b5d47d40100000000fdffffff01ae46000000000000160014b9de4f9f5c61e643fbc078c90beb6162b40abf4e02483045022100c3ab67bd13cbdfad7352ac514de1a02923834f40d0bbfc093d695c6205166cbb022010c13d427fc9d3ffcbb883fa849f6de22e513883782f2d57445335885bd013fe012103b6e92d92aef77e32076052a4376bd2ce5fd78a18344b9df1db5c8c809991cee602483045022100a1957c757c983306de87357d8a541ca659495b2b441db3a9fc9fd3622033ac1e02207394dc48c19d9c55348f076780ed475686d8a5f5365054dd94756929fb5e883d012102ed13f37ca6c7a478b120b5cc126828a145285a7273f1c75994517838e31064fe00000000"),
                ],
                expected_wallets: HashSet::from([WalletType::Ledger]),
            },
            // Trezor: 87670b12778d17c759db459479d66acfd1c4d444094270991d8e1de09a56cc7c
            TestVector {
                tx: get_tx_from_hex("01000000000103c54d8c88e4f5d43bd0afd365ab8af7688af9ca8d5c10dcb86519a924dd3a12e30000000000fdffffffc54d8c88e4f5d43bd0afd365ab8af7688af9ca8d5c10dcb86519a924dd3a12e30100000000fdffffffc54d8c88e4f5d43bd0afd365ab8af7688af9ca8d5c10dcb86519a924dd3a12e30200000000fdffffff03cf0a000000000000160014b47e4a3828865a23bb63da619b40bc3ec586480bb471000000000000160014eee06789bad1948746d16d69f6e698c99f62c341b4710000000000001976a9145b3263a7adcbd55ea653edfc4e4c04945a303a3788ac02483045022100a24d87256cdf7d63e526f7832282341d8d6c727c7c6aba536d7fa89a39522a4f022049a9e4d92c41fd99edd17c0f8614fd8421413b71e763f90dba6fb164a062a8b30121020c0bd6c738c36c415734e2d05614f861a083970c7cbe4a7b1db2bea740a9e54602473044022018234159f2a1085eab3f318a8596ecf9d3cbfeec3d3f46b3c47bc30bb3946c6d0220278c82c5bbdf1bef7ceb39bf904ffe72f88c43af598096b2569c1f1a51d67d6c0121020c0bd6c738c36c415734e2d05614f861a083970c7cbe4a7b1db2bea740a9e54602483045022100c1df2dbedcf0dc8c9b19098aeb9e6b2daead5b17bfd038922fa6480cc90c529202206fb4f7c0c81ed56eadc8e5771584fd36a877ffb750151a4b0ffbc5e16ab311b00121020c0bd6c738c36c415734e2d05614f861a083970c7cbe4a7b1db2bea740a9e54600000000"),
                prev_txs: vec![
                    get_tx_from_hex("0200000001adb5d8c9c90c19d1df45f738a9d739f669caefff4c2355d7a53cb2a9704c09c1000000006a47304402205825a5dcf15947113796f2da4f891ad39d5f1f761f4716770143cd470610e1ec0220261e1abe8ecf908ee718149d3587e9440ce96d9c8e680b34f306b8a405c2ae470121020b8a58237f6650d658730f5945c5fa9284c494040fefd8b6f33a2ac49862aa42ffffffff03895d00000000000016001444e650ca651d519813b57dc387a54b2c33016520cf4200000000000016001444e650ca651d519813b57dc387a54b2c33016520f46400000000000016001444e650ca651d519813b57dc387a54b2c3301652000000000"),
                ],
                expected_wallets: HashSet::from([WalletType::Trezor])
            },
            // Blue wallet: 1bf659e17568e48d6f47bb5470bc8df567cfe89d79c6e38cafbe798f43d5da22
            TestVector {
                tx: get_tx_from_hex("02000000000103570a26bfae0867b97212558189f80cdd44a2a24ff9d76b6dac599a73ed84d22f000000000000000080570a26bfae0867b97212558189f80cdd44a2a24ff9d76b6dac599a73ed84d22f010000000000000080570a26bfae0867b97212558189f80cdd44a2a24ff9d76b6dac599a73ed84d22f02000000000000008003a0860100000000001600147d2741001a5502d0db282c640d376ea7c5c5ba75a00f000000000000160014b4bf9b7ceab50cf7474b260aee1f5d880773b0e12ca100000000000016001442d0b6bc4040fd50aaadf3c6616d9ae0c42fa8db02483045022100fe6f638daa02a2de220ae87ffe212f55a0587147370284db8fa0c73f0a9f569402207b97624856cf66e1649c6459273ad3b0221906e1478154f0c7c9323e32f6fa65012103f307c3e3031babd35634a0f7989798ff6523d9267408828405f4873a10fde8ee02483045022100d4b3ac0e3a3eaa22667ada79693a322530b174e9e5821f0de29afdd1efbe1c4b022077f3eb4b7ea2a6eb49db2ba05c7ec8f461f14f2c4b8303ace17f12d17124aa2a012103bb5d910d5d7a336e8cc30effbca4a7bc7e01febd558523f709d62bb31c3215d40247304402203081506178cfe98d2b4ebeb17d89a3f9ad06966118fdffc01e173a1bd030c6950220667ac8c26f24e9e176aabe840e563e83b8112279b101373b5493e915fa5b29fb0121021ea4ff443e6b5db8c95ed934bf113fc6e5d740d166eb66aa6a5accbd990e064500000000"),
                prev_txs: vec![
                    get_tx_from_hex("020000000001013f17fa5fdc451e6fba6ac2fa02592af9ba8ee5f69b400f3559e23bc68ab8db2b0000000000000000800450c300000000000016001471e2c1575903a000d1486f9cbec0a245ecb9c19e50c3000000000000160014b536927be1e633e6674e1f36b8c8ee310adf2da150c3000000000000160014addbf648bada5bceca425289105731b09f434347cd9900000000000016001411385cc2c893fdef44ef6dd458241b19e5b3ffd202483045022100f42fe40dbacc20e40cd2e4c1ba86e3c38afec96528681af5335fa8c7c33aa6aa02202af9c25393097cd93a83a37d7a24702e302405a047b9e9f166209deb13ec821701210386ccea785809b6e69a1ed483c119e993a425a8bb100042f9f3d0dffda283a24700000000"),
                ],
                expected_wallets: HashSet::from([WalletType::BlueWallet])
            },
            // Exodus: 6f8c37db6ed88bfd0fd483963ebf06c5557326f8d2a3617af5ceba878442e1ad
            TestVector {
                tx: get_tx_from_hex("020000000001011309192e20a892daee269de43babb203a1ff68ae996406ca8b56ed9e8bca7d810000000000ffffffff02e91a000000000000160014fe3f8293b01b1d32db8dfc5ccd9a595e5af189b26f33000000000000160014ffed07852461fcef0ef3e2dd6ed598614037bb2902483045022100984153898e29ab101b666443ba1ca73f823ffd951347257f183afeeb5edac83a02204df7a36fb67d71089cf9d34be8c9c1ff7e8a30b33b96023e55e230c573f4f5bb01210315d9ffabd251ae57cd2a6843bf207e73ac95eeda9db75043bc0d18306f43be4d00000000"),
                prev_txs: vec![
                    get_tx_from_hex("020000000001017cca6cb0ed3a291dc8f385ba17100ea2749e56aea344dae6ded3bcd56a5af91600000000000000008001125b000000000000160014ffed07852461fcef0ef3e2dd6ed598614037bb2902483045022100ef12adecd8ada80560d64421707c653b19d11039e3a54e989433b8dc5d8aadb70220448d557e548767ee0652851e81dab1fe732c5d0af85634715956e397dcd25548012103a7a4f8c99a2ddf4fde317023fb73cee4d1b3191a20e722af88614857688f4f8400000000"),
                ],
                expected_wallets: HashSet::from([WalletType::Exodus])
            },
        ];
        fn do_test(test_vector: TestVector) {
            let (wallets, reasoning) = detect_wallet(&test_vector.tx, &test_vector.prev_txs);
            let expected_wallets = test_vector.expected_wallets;
            println!("wallets: {:?}", wallets);
            println!("reasoning: {:?}", reasoning);
            assert_eq!(wallets, expected_wallets);
        }

        for test_vector in test_vectors {
            do_test(test_vector);
        }
    }
}