1mod 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#[allow(unused)]
73fn detect_wallet(tx: &Transaction, prev_txs: &[Transaction]) -> (HashSet<WalletType>, Vec<String>) {
74 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 assert!(prev_txouts.len() == tx.input.len());
91 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 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 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 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 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 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 .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 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 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 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 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 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 }
310
311 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]
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 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 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 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 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 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}