1pub mod file_store;
12pub mod persistent;
13
14pub use file_store::FileBasedWalletStore;
15pub use persistent::PersistentWallet;
16
17use abtc_domain::primitives::{Amount, OutPoint, Transaction, TxOut};
18use abtc_domain::wallet::address::{Address, AddressType};
19use abtc_domain::wallet::coin_selection::{Coin, CoinSelector, SelectionStrategy};
20use abtc_domain::wallet::keys::PrivateKey;
21use abtc_domain::wallet::tx_builder::{
22 InputInfo, TransactionBuilder, P2PKH_INPUT_VSIZE, P2WPKH_INPUT_VSIZE, P2WPKH_OUTPUT_VSIZE,
23 TX_OVERHEAD_VSIZE,
24};
25use abtc_ports::wallet::store::{WalletKeyEntry, WalletSnapshot, WalletUtxoEntry};
26use abtc_ports::wallet::UnspentOutput;
27use abtc_ports::{Balance, WalletPort};
28use async_trait::async_trait;
29use std::collections::HashMap;
30use std::sync::Arc;
31use tokio::sync::RwLock;
32
33const DEFAULT_FEE_RATE: f64 = 10.0;
35
36#[derive(Clone)]
38struct WalletKey {
39 private_key: PrivateKey,
41 address: Address,
43 label: Option<String>,
45}
46
47pub struct InMemoryWallet {
52 keys: Arc<RwLock<HashMap<String, WalletKey>>>,
54 utxos: Arc<RwLock<Vec<UnspentOutput>>>,
56 mainnet: bool,
58 address_type: AddressType,
60 key_counter: Arc<RwLock<u64>>,
62}
63
64impl InMemoryWallet {
65 pub fn new(mainnet: bool, address_type: AddressType) -> Self {
67 InMemoryWallet {
68 keys: Arc::new(RwLock::new(HashMap::new())),
69 utxos: Arc::new(RwLock::new(Vec::new())),
70 mainnet,
71 address_type,
72 key_counter: Arc::new(RwLock::new(0)),
73 }
74 }
75
76 pub fn default_mainnet() -> Self {
78 Self::new(true, AddressType::P2WPKH)
79 }
80
81 pub fn default_testnet() -> Self {
83 Self::new(false, AddressType::P2WPKH)
84 }
85
86 pub async fn add_utxo(&self, utxo: UnspentOutput) {
88 let mut utxos = self.utxos.write().await;
89 utxos.push(utxo);
90 }
91
92 pub async fn remove_utxos(&self, spent_outpoints: &[OutPoint]) {
94 let mut utxos = self.utxos.write().await;
95 utxos.retain(|u| !spent_outpoints.contains(&u.outpoint));
96 }
97
98 pub async fn key_count(&self) -> usize {
100 let keys = self.keys.read().await;
101 keys.len()
102 }
103
104 async fn find_key_for_script(&self, script_pubkey: &abtc_domain::Script) -> Option<WalletKey> {
106 let keys = self.keys.read().await;
107 keys.values()
108 .find(|wk| wk.address.script_pubkey.as_bytes() == script_pubkey.as_bytes())
109 .cloned()
110 }
111
112 fn estimate_input_vsize(script_pubkey: &abtc_domain::Script) -> u32 {
114 if script_pubkey.is_p2wpkh() {
115 P2WPKH_INPUT_VSIZE
116 } else if script_pubkey.is_p2pkh() {
117 P2PKH_INPUT_VSIZE
118 } else {
119 P2WPKH_INPUT_VSIZE
121 }
122 }
123
124 pub fn is_mainnet(&self) -> bool {
126 self.mainnet
127 }
128
129 pub fn address_type_str(&self) -> &str {
131 match self.address_type {
132 AddressType::P2PKH => "p2pkh",
133 AddressType::P2WPKH => "p2wpkh",
134 AddressType::P2shP2wpkh => "p2sh-p2wpkh",
135 AddressType::P2TR => "p2tr",
136 }
137 }
138
139 pub fn parse_address_type(s: &str) -> Option<AddressType> {
141 match s {
142 "p2pkh" => Some(AddressType::P2PKH),
143 "p2wpkh" => Some(AddressType::P2WPKH),
144 "p2sh-p2wpkh" => Some(AddressType::P2shP2wpkh),
145 "p2tr" => Some(AddressType::P2TR),
146 _ => None,
147 }
148 }
149
150 pub async fn snapshot(&self) -> WalletSnapshot {
155 let keys = self.keys.read().await;
156 let utxos = self.utxos.read().await;
157 let counter = self.key_counter.read().await;
158
159 let key_entries: Vec<WalletKeyEntry> = keys
160 .iter()
161 .map(|(addr, wk)| WalletKeyEntry {
162 address: addr.clone(),
163 wif: wk.private_key.to_wif(),
164 label: wk.label.clone(),
165 })
166 .collect();
167
168 let utxo_entries: Vec<WalletUtxoEntry> = utxos
169 .iter()
170 .map(|u| {
171 let script_bytes = u.output.script_pubkey.as_bytes();
172 let script_hex: String =
173 script_bytes.iter().map(|b| format!("{:02x}", b)).collect();
174
175 WalletUtxoEntry {
176 txid_hex: u.outpoint.txid.to_hex_reversed(),
177 vout: u.outpoint.vout,
178 amount_sat: u.output.value.as_sat(),
179 script_pubkey_hex: script_hex,
180 confirmations: u.confirmations,
181 is_coinbase: u.is_coinbase,
182 }
183 })
184 .collect();
185
186 WalletSnapshot {
187 version: 1,
188 mainnet: self.mainnet,
189 address_type: self.address_type_str().to_string(),
190 key_counter: *counter,
191 keys: key_entries,
192 utxos: utxo_entries,
193 }
194 }
195
196 pub async fn restore_from_snapshot(
201 &self,
202 snapshot: &WalletSnapshot,
203 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
204 let mut keys = self.keys.write().await;
206 keys.clear();
207
208 for entry in &snapshot.keys {
209 let private_key = PrivateKey::from_wif(&entry.wif)
210 .map_err(|e| format!("failed to parse WIF for {}: {}", entry.address, e))?;
211
212 let address = Address::decode(&entry.address)
213 .map_err(|e| format!("failed to decode address {}: {}", entry.address, e))?;
214
215 keys.insert(
216 entry.address.clone(),
217 WalletKey {
218 private_key,
219 address,
220 label: entry.label.clone(),
221 },
222 );
223 }
224
225 let mut utxos = self.utxos.write().await;
227 utxos.clear();
228
229 for entry in &snapshot.utxos {
230 let txid_hex_raw = Self::reverse_hex(&entry.txid_hex)
232 .ok_or_else(|| format!("invalid txid hex: {}", entry.txid_hex))?;
233 let txid = abtc_domain::Txid::from_hex(&txid_hex_raw)
234 .ok_or_else(|| format!("failed to parse txid: {}", entry.txid_hex))?;
235
236 let script_bytes = Self::hex_to_bytes(&entry.script_pubkey_hex)
238 .ok_or_else(|| format!("invalid script hex: {}", entry.script_pubkey_hex))?;
239
240 utxos.push(UnspentOutput {
241 outpoint: OutPoint::new(txid, entry.vout),
242 output: TxOut::new(
243 Amount::from_sat(entry.amount_sat),
244 abtc_domain::Script::from_bytes(script_bytes),
245 ),
246 confirmations: entry.confirmations,
247 is_coinbase: entry.is_coinbase,
248 });
249 }
250
251 let mut counter = self.key_counter.write().await;
253 *counter = snapshot.key_counter;
254
255 tracing::info!(
256 "Restored wallet: {} keys, {} UTXOs, counter={}",
257 keys.len(),
258 utxos.len(),
259 snapshot.key_counter
260 );
261
262 Ok(())
263 }
264
265 fn reverse_hex(hex: &str) -> Option<String> {
267 if hex.len() % 2 != 0 {
268 return None;
269 }
270 let mut result = String::with_capacity(hex.len());
271 for i in (0..hex.len()).rev().step_by(2) {
272 if i == 0 {
273 result.push_str(&hex[0..2]);
274 } else {
275 result.push_str(&hex[i - 1..i + 1]);
276 }
277 }
278 Some(result)
279 }
280
281 fn hex_to_bytes(hex: &str) -> Option<Vec<u8>> {
283 if hex.len() % 2 != 0 {
284 return None;
285 }
286 let mut bytes = Vec::with_capacity(hex.len() / 2);
287 for i in (0..hex.len()).step_by(2) {
288 let byte = u8::from_str_radix(&hex[i..i + 2], 16).ok()?;
289 bytes.push(byte);
290 }
291 Some(bytes)
292 }
293}
294
295impl Default for InMemoryWallet {
296 fn default() -> Self {
297 Self::default_mainnet()
298 }
299}
300
301#[async_trait]
302impl WalletPort for InMemoryWallet {
303 async fn get_balance(&self) -> Result<Balance, Box<dyn std::error::Error + Send + Sync>> {
304 let utxos = self.utxos.read().await;
305
306 let confirmed: i64 = utxos
307 .iter()
308 .filter(|u| u.confirmations >= 1)
309 .map(|u| u.output.value.as_sat())
310 .sum();
311
312 let unconfirmed: i64 = utxos
313 .iter()
314 .filter(|u| u.confirmations == 0)
315 .map(|u| u.output.value.as_sat())
316 .sum();
317
318 let immature: i64 = utxos
319 .iter()
320 .filter(|u| u.is_coinbase && u.confirmations < 100)
321 .map(|u| u.output.value.as_sat())
322 .sum();
323
324 Ok(Balance {
325 confirmed: Amount::from_sat(confirmed),
326 unconfirmed: Amount::from_sat(unconfirmed),
327 immature: Amount::from_sat(immature),
328 })
329 }
330
331 async fn list_unspent(
332 &self,
333 min_confirmations: u32,
334 max_amount: Option<Amount>,
335 ) -> Result<Vec<UnspentOutput>, Box<dyn std::error::Error + Send + Sync>> {
336 let utxos = self.utxos.read().await;
337 let filtered: Vec<UnspentOutput> = utxos
338 .iter()
339 .filter(|u| u.confirmations >= min_confirmations)
340 .filter(|u| match max_amount {
341 Some(max) => u.output.value.as_sat() <= max.as_sat(),
342 None => true,
343 })
344 .cloned()
345 .collect();
346 Ok(filtered)
347 }
348
349 async fn create_transaction(
350 &self,
351 to: Vec<(String, Amount)>,
352 fee_rate: Option<f64>,
353 ) -> Result<Transaction, Box<dyn std::error::Error + Send + Sync>> {
354 let fee_rate = fee_rate.unwrap_or(DEFAULT_FEE_RATE);
355
356 let mut outputs = Vec::new();
358 let mut total_send: i64 = 0;
359
360 for (addr_str, amount) in &to {
361 let addr = Address::decode(addr_str)
362 .map_err(|e| format!("invalid address '{}': {}", addr_str, e))?;
363 outputs.push(TxOut::new(*amount, addr.script_pubkey));
364 total_send += amount.as_sat();
365 }
366
367 let target = Amount::from_sat(total_send);
368
369 let utxos = self.utxos.read().await;
371 let coins: Vec<Coin> = utxos
372 .iter()
373 .enumerate()
374 .filter(|(_, u)| u.confirmations >= 1) .map(|(i, u)| Coin {
376 index: i,
377 amount: u.output.value,
378 input_size: Self::estimate_input_vsize(&u.output.script_pubkey),
379 })
380 .collect();
381
382 let output_vsize: u32 = outputs.len() as u32 * P2WPKH_OUTPUT_VSIZE + P2WPKH_OUTPUT_VSIZE; let selection = CoinSelector::select(
387 &coins,
388 target,
389 fee_rate,
390 output_vsize,
391 TX_OVERHEAD_VSIZE,
392 SelectionStrategy::LargestFirst,
393 )
394 .map_err(|e| format!("coin selection failed: {}", e))?;
395
396 if selection.change.as_sat() > 546 {
398 let keys = self.keys.read().await;
400 if let Some(change_key) = keys.values().next() {
401 outputs.push(TxOut::new(
402 selection.change,
403 change_key.address.script_pubkey.clone(),
404 ));
405 }
406 }
407
408 let mut builder = TransactionBuilder::new().version(2);
410
411 for &coin_idx in &selection.selected_indices {
412 let utxo = &utxos[coin_idx];
413 let wallet_key = self.find_key_for_script(&utxo.output.script_pubkey).await;
414
415 builder = builder.add_input(InputInfo {
416 outpoint: utxo.outpoint,
417 script_pubkey: utxo.output.script_pubkey.clone(),
418 amount: utxo.output.value,
419 signing_key: wallet_key.map(|wk| wk.private_key),
420 sequence: 0xFFFFFFFE, tap_script_path: None,
422 });
423 }
424
425 for output in outputs {
426 builder = builder.add_output(output);
427 }
428
429 let tx = builder
431 .build_unsigned()
432 .map_err(|e| format!("build failed: {}", e))?;
433
434 Ok(tx)
435 }
436
437 async fn sign_transaction(
438 &self,
439 tx: &Transaction,
440 ) -> Result<Transaction, Box<dyn std::error::Error + Send + Sync>> {
441 let utxos = self.utxos.read().await;
443 let mut builder = TransactionBuilder::new().version(tx.version);
444
445 for input in &tx.inputs {
446 let utxo = utxos
448 .iter()
449 .find(|u| u.outpoint == input.previous_output)
450 .ok_or_else(|| format!("unknown UTXO for input {}", input.previous_output))?;
451
452 let wallet_key = self.find_key_for_script(&utxo.output.script_pubkey).await;
453
454 builder = builder.add_input(InputInfo {
455 outpoint: input.previous_output,
456 script_pubkey: utxo.output.script_pubkey.clone(),
457 amount: utxo.output.value,
458 signing_key: wallet_key.map(|wk| wk.private_key),
459 sequence: input.sequence,
460 tap_script_path: None,
461 });
462 }
463
464 for output in &tx.outputs {
465 builder = builder.add_output(output.clone());
466 }
467
468 builder = builder.lock_time(tx.lock_time);
469
470 let signed = builder
471 .sign()
472 .map_err(|e| format!("signing failed: {}", e))?;
473
474 Ok(signed)
475 }
476
477 async fn send_transaction(
478 &self,
479 tx: &Transaction,
480 ) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
481 let txid = tx.txid();
482 tracing::info!("Wallet: broadcasting transaction {}", txid);
483
484 let spent: Vec<OutPoint> = tx
486 .inputs
487 .iter()
488 .map(|input| input.previous_output)
489 .collect();
490 self.remove_utxos(&spent).await;
491
492 Ok(txid.to_hex_reversed())
493 }
494
495 async fn get_new_address(
496 &self,
497 label: Option<&str>,
498 ) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
499 let key = PrivateKey::generate(true, self.mainnet);
501 let pubkey = key.public_key();
502
503 let address = match self.address_type {
505 AddressType::P2PKH => Address::p2pkh(&pubkey, self.mainnet),
506 AddressType::P2WPKH => Address::p2wpkh(&pubkey, self.mainnet)
507 .map_err(|e| format!("address derivation failed: {}", e))?,
508 AddressType::P2shP2wpkh => Address::p2sh_p2wpkh(&pubkey, self.mainnet)
509 .map_err(|e| format!("address derivation failed: {}", e))?,
510 AddressType::P2TR => {
511 let serialized = pubkey.serialize();
513 let mut x_only = [0u8; 32];
514 x_only.copy_from_slice(&serialized[1..33]);
515 Address::p2tr_from_internal_key(&x_only, self.mainnet)
516 .map_err(|e| format!("address derivation failed: {}", e))?
517 }
518 };
519
520 let addr_string = address.encoded.clone();
521
522 let wallet_key = WalletKey {
524 private_key: key,
525 address,
526 label: label.map(|s| s.to_string()),
527 };
528
529 let mut keys = self.keys.write().await;
530 keys.insert(addr_string.clone(), wallet_key);
531
532 let mut counter = self.key_counter.write().await;
534 *counter += 1;
535
536 tracing::debug!(
537 "Generated new {} address: {}",
538 match self.address_type {
539 AddressType::P2PKH => "P2PKH",
540 AddressType::P2WPKH => "P2WPKH",
541 AddressType::P2shP2wpkh => "P2SH-P2WPKH",
542 AddressType::P2TR => "P2TR",
543 },
544 addr_string
545 );
546
547 Ok(addr_string)
548 }
549
550 async fn import_key(
551 &self,
552 privkey_wif: &str,
553 label: Option<&str>,
554 _rescan: bool,
555 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
556 let key =
557 PrivateKey::from_wif(privkey_wif).map_err(|e| format!("invalid WIF key: {}", e))?;
558
559 let pubkey = key.public_key();
560
561 let address = match self.address_type {
563 AddressType::P2PKH => Address::p2pkh(&pubkey, self.mainnet),
564 AddressType::P2WPKH => Address::p2wpkh(&pubkey, self.mainnet)
565 .map_err(|e| format!("address derivation failed: {}", e))?,
566 AddressType::P2shP2wpkh => Address::p2sh_p2wpkh(&pubkey, self.mainnet)
567 .map_err(|e| format!("address derivation failed: {}", e))?,
568 AddressType::P2TR => {
569 let serialized = pubkey.serialize();
570 let mut x_only = [0u8; 32];
571 x_only.copy_from_slice(&serialized[1..33]);
572 Address::p2tr_from_internal_key(&x_only, self.mainnet)
573 .map_err(|e| format!("address derivation failed: {}", e))?
574 }
575 };
576
577 let addr_string = address.encoded.clone();
578
579 let wallet_key = WalletKey {
580 private_key: key,
581 address,
582 label: label.map(|s| s.to_string()),
583 };
584
585 let mut keys = self.keys.write().await;
586 keys.insert(addr_string.clone(), wallet_key);
587
588 tracing::info!("Imported key for address: {}", addr_string);
589 Ok(())
590 }
591
592 async fn get_transaction_history(
593 &self,
594 _count: u32,
595 _skip: u32,
596 ) -> Result<Vec<Transaction>, Box<dyn std::error::Error + Send + Sync>> {
597 Ok(Vec::new())
599 }
600}
601
602#[cfg(test)]
603mod tests {
604 use super::*;
605
606 #[tokio::test]
607 async fn test_wallet_creation() {
608 let wallet = InMemoryWallet::default_mainnet();
609 let balance = wallet.get_balance().await.unwrap();
610 assert_eq!(balance.confirmed.as_sat(), 0);
611 assert_eq!(balance.unconfirmed.as_sat(), 0);
612 }
613
614 #[tokio::test]
615 async fn test_generate_p2pkh_address() {
616 let wallet = InMemoryWallet::new(true, AddressType::P2PKH);
617 let addr = wallet.get_new_address(Some("test")).await.unwrap();
618 assert!(addr.starts_with('1'));
619 assert_eq!(wallet.key_count().await, 1);
620 }
621
622 #[tokio::test]
623 async fn test_generate_p2wpkh_address() {
624 let wallet = InMemoryWallet::default_mainnet();
625 let addr = wallet.get_new_address(None).await.unwrap();
626 assert!(addr.starts_with("bc1q"));
627 assert_eq!(wallet.key_count().await, 1);
628 }
629
630 #[tokio::test]
631 async fn test_generate_p2sh_p2wpkh_address() {
632 let wallet = InMemoryWallet::new(true, AddressType::P2shP2wpkh);
633 let addr = wallet.get_new_address(None).await.unwrap();
634 assert!(addr.starts_with('3'));
635 }
636
637 #[tokio::test]
638 async fn test_generate_testnet_address() {
639 let wallet = InMemoryWallet::default_testnet();
640 let addr = wallet.get_new_address(None).await.unwrap();
641 assert!(addr.starts_with("tb1q"));
642 }
643
644 #[tokio::test]
645 async fn test_import_and_export_key() {
646 let wallet = InMemoryWallet::default_mainnet();
647
648 let key = PrivateKey::generate(true, true);
650 let wif = key.to_wif();
651
652 wallet
654 .import_key(&wif, Some("imported"), false)
655 .await
656 .unwrap();
657 assert_eq!(wallet.key_count().await, 1);
658 }
659
660 #[tokio::test]
661 async fn test_add_and_list_utxos() {
662 let wallet = InMemoryWallet::default_mainnet();
663
664 let utxo = UnspentOutput {
665 outpoint: OutPoint::new(abtc_domain::Txid::zero(), 0),
666 output: TxOut::new(Amount::from_sat(100_000), abtc_domain::Script::new()),
667 confirmations: 6,
668 is_coinbase: false,
669 };
670
671 wallet.add_utxo(utxo).await;
672
673 let unspent = wallet.list_unspent(1, None).await.unwrap();
674 assert_eq!(unspent.len(), 1);
675 assert_eq!(unspent[0].output.value.as_sat(), 100_000);
676
677 let balance = wallet.get_balance().await.unwrap();
678 assert_eq!(balance.confirmed.as_sat(), 100_000);
679 }
680
681 #[tokio::test]
682 async fn test_multiple_addresses() {
683 let wallet = InMemoryWallet::default_mainnet();
684
685 let addr1 = wallet.get_new_address(Some("first")).await.unwrap();
686 let addr2 = wallet.get_new_address(Some("second")).await.unwrap();
687
688 assert_ne!(addr1, addr2);
689 assert_eq!(wallet.key_count().await, 2);
690 }
691
692 #[tokio::test]
693 async fn test_balance_confirmed_vs_unconfirmed() {
694 let wallet = InMemoryWallet::default_mainnet();
695
696 wallet
698 .add_utxo(UnspentOutput {
699 outpoint: OutPoint::new(abtc_domain::Txid::zero(), 0),
700 output: TxOut::new(Amount::from_sat(50_000), abtc_domain::Script::new()),
701 confirmations: 3,
702 is_coinbase: false,
703 })
704 .await;
705
706 wallet
708 .add_utxo(UnspentOutput {
709 outpoint: OutPoint::new(abtc_domain::Txid::zero(), 1),
710 output: TxOut::new(Amount::from_sat(25_000), abtc_domain::Script::new()),
711 confirmations: 0,
712 is_coinbase: false,
713 })
714 .await;
715
716 let balance = wallet.get_balance().await.unwrap();
717 assert_eq!(balance.confirmed.as_sat(), 50_000);
718 assert_eq!(balance.unconfirmed.as_sat(), 25_000);
719 }
720
721 #[tokio::test]
722 async fn test_balance_immature_coinbase() {
723 let wallet = InMemoryWallet::default_mainnet();
724
725 wallet
727 .add_utxo(UnspentOutput {
728 outpoint: OutPoint::new(abtc_domain::Txid::zero(), 0),
729 output: TxOut::new(Amount::from_sat(5_000_000_000), abtc_domain::Script::new()),
730 confirmations: 50,
731 is_coinbase: true,
732 })
733 .await;
734
735 wallet
737 .add_utxo(UnspentOutput {
738 outpoint: OutPoint::new(abtc_domain::Txid::zero(), 1),
739 output: TxOut::new(Amount::from_sat(5_000_000_000), abtc_domain::Script::new()),
740 confirmations: 100,
741 is_coinbase: true,
742 })
743 .await;
744
745 let balance = wallet.get_balance().await.unwrap();
746 assert_eq!(balance.immature.as_sat(), 5_000_000_000); }
748
749 #[tokio::test]
750 async fn test_list_unspent_min_confirmations() {
751 let wallet = InMemoryWallet::default_mainnet();
752
753 wallet
754 .add_utxo(UnspentOutput {
755 outpoint: OutPoint::new(abtc_domain::Txid::zero(), 0),
756 output: TxOut::new(Amount::from_sat(10_000), abtc_domain::Script::new()),
757 confirmations: 0,
758 is_coinbase: false,
759 })
760 .await;
761 wallet
762 .add_utxo(UnspentOutput {
763 outpoint: OutPoint::new(abtc_domain::Txid::zero(), 1),
764 output: TxOut::new(Amount::from_sat(20_000), abtc_domain::Script::new()),
765 confirmations: 3,
766 is_coinbase: false,
767 })
768 .await;
769 wallet
770 .add_utxo(UnspentOutput {
771 outpoint: OutPoint::new(abtc_domain::Txid::zero(), 2),
772 output: TxOut::new(Amount::from_sat(30_000), abtc_domain::Script::new()),
773 confirmations: 10,
774 is_coinbase: false,
775 })
776 .await;
777
778 let all = wallet.list_unspent(0, None).await.unwrap();
780 assert_eq!(all.len(), 3);
781
782 let confirmed = wallet.list_unspent(1, None).await.unwrap();
784 assert_eq!(confirmed.len(), 2);
785
786 let deep = wallet.list_unspent(6, None).await.unwrap();
788 assert_eq!(deep.len(), 1);
789 assert_eq!(deep[0].output.value.as_sat(), 30_000);
790 }
791
792 #[tokio::test]
793 async fn test_list_unspent_max_amount() {
794 let wallet = InMemoryWallet::default_mainnet();
795
796 wallet
797 .add_utxo(UnspentOutput {
798 outpoint: OutPoint::new(abtc_domain::Txid::zero(), 0),
799 output: TxOut::new(Amount::from_sat(1_000), abtc_domain::Script::new()),
800 confirmations: 1,
801 is_coinbase: false,
802 })
803 .await;
804 wallet
805 .add_utxo(UnspentOutput {
806 outpoint: OutPoint::new(abtc_domain::Txid::zero(), 1),
807 output: TxOut::new(Amount::from_sat(100_000), abtc_domain::Script::new()),
808 confirmations: 1,
809 is_coinbase: false,
810 })
811 .await;
812
813 let filtered = wallet
814 .list_unspent(0, Some(Amount::from_sat(50_000)))
815 .await
816 .unwrap();
817 assert_eq!(filtered.len(), 1);
818 assert_eq!(filtered[0].output.value.as_sat(), 1_000);
819 }
820
821 #[tokio::test]
822 async fn test_remove_utxos() {
823 let wallet = InMemoryWallet::default_mainnet();
824
825 let op1 = OutPoint::new(
826 abtc_domain::Txid::from_hash(abtc_domain::primitives::Hash256::from_bytes([0x01; 32])),
827 0,
828 );
829 let op2 = OutPoint::new(
830 abtc_domain::Txid::from_hash(abtc_domain::primitives::Hash256::from_bytes([0x02; 32])),
831 0,
832 );
833
834 wallet
835 .add_utxo(UnspentOutput {
836 outpoint: op1,
837 output: TxOut::new(Amount::from_sat(10_000), abtc_domain::Script::new()),
838 confirmations: 1,
839 is_coinbase: false,
840 })
841 .await;
842 wallet
843 .add_utxo(UnspentOutput {
844 outpoint: op2,
845 output: TxOut::new(Amount::from_sat(20_000), abtc_domain::Script::new()),
846 confirmations: 1,
847 is_coinbase: false,
848 })
849 .await;
850
851 assert_eq!(wallet.list_unspent(0, None).await.unwrap().len(), 2);
852
853 wallet.remove_utxos(&[op1]).await;
854
855 let remaining = wallet.list_unspent(0, None).await.unwrap();
856 assert_eq!(remaining.len(), 1);
857 assert_eq!(remaining[0].outpoint, op2);
858 }
859
860 #[tokio::test]
861 async fn test_send_transaction_removes_spent_utxos() {
862 let wallet = InMemoryWallet::default_mainnet();
863
864 let op = OutPoint::new(
865 abtc_domain::Txid::from_hash(abtc_domain::primitives::Hash256::from_bytes([0x01; 32])),
866 0,
867 );
868
869 wallet
870 .add_utxo(UnspentOutput {
871 outpoint: op,
872 output: TxOut::new(Amount::from_sat(100_000), abtc_domain::Script::new()),
873 confirmations: 6,
874 is_coinbase: false,
875 })
876 .await;
877
878 use abtc_domain::primitives::TxIn;
880 let tx = Transaction::v1(
881 vec![TxIn::final_input(op, abtc_domain::Script::new())],
882 vec![TxOut::new(
883 Amount::from_sat(90_000),
884 abtc_domain::Script::new(),
885 )],
886 0,
887 );
888
889 let txid_hex = wallet.send_transaction(&tx).await.unwrap();
890 assert!(!txid_hex.is_empty());
891
892 let remaining = wallet.list_unspent(0, None).await.unwrap();
894 assert_eq!(remaining.len(), 0);
895 }
896
897 #[tokio::test]
898 async fn test_import_invalid_wif_fails() {
899 let wallet = InMemoryWallet::default_mainnet();
900 let result = wallet.import_key("not-a-valid-wif", None, false).await;
901 assert!(result.is_err());
902 }
903
904 #[tokio::test]
905 async fn test_default_wallet() {
906 let wallet = InMemoryWallet::default();
907 let addr = wallet.get_new_address(None).await.unwrap();
908 assert!(addr.starts_with("bc1q"));
910 }
911
912 #[tokio::test]
913 async fn test_empty_balance() {
914 let wallet = InMemoryWallet::default_mainnet();
915 let balance = wallet.get_balance().await.unwrap();
916 assert_eq!(balance.confirmed.as_sat(), 0);
917 assert_eq!(balance.unconfirmed.as_sat(), 0);
918 assert_eq!(balance.immature.as_sat(), 0);
919 }
920}