1#![deny(clippy::pedantic)]
2#![allow(clippy::missing_errors_doc)]
3#![allow(clippy::missing_panics_doc)]
4#![allow(clippy::must_use_candidate)]
5#![allow(clippy::module_name_repetitions)]
6
7pub use fedimint_walletv2_common as common;
8
9mod api;
10#[cfg(feature = "cli")]
11mod cli;
12pub mod db;
13pub mod events;
14mod receive_sm;
15mod send_sm;
16
17use std::collections::{BTreeMap, BTreeSet};
18use std::sync::Arc;
19use std::time::Duration;
20
21use anyhow::anyhow;
22use api::WalletFederationApi;
23use bitcoin::address::NetworkUnchecked;
24use bitcoin::{Address, ScriptBuf};
25use db::{NextOutputIndexKey, ValidAddressIndexKey, ValidAddressIndexPrefix};
26use events::{ReceivePaymentEvent, SendPaymentEvent};
27use fedimint_api_client::api::{DynModuleApi, FederationResult};
28use fedimint_client::DynGlobalClientContext;
29use fedimint_client::transaction::{
30 ClientInput, ClientInputBundle, ClientInputSM, ClientOutput, ClientOutputBundle,
31 ClientOutputSM, FeeQuote, FeeQuoteRequest, TransactionBuilder, max_affordable_send_amount,
32};
33use fedimint_client_module::db::ClientModuleMigrationFn;
34use fedimint_client_module::module::init::{ClientModuleInit, ClientModuleInitArgs};
35use fedimint_client_module::module::recovery::NoModuleBackup;
36use fedimint_client_module::module::{ClientContext, ClientModule, OutPointRange};
37use fedimint_client_module::sm::{Context, DynState, ModuleNotifier, State, StateTransition};
38use fedimint_client_module::sm_enum_variant_translation;
39use fedimint_core::core::{IntoDynInstance, ModuleInstanceId, ModuleKind, OperationId};
40use fedimint_core::db::{
41 Database, DatabaseTransaction, DatabaseVersion, IDatabaseTransactionOpsCoreTyped,
42};
43use fedimint_core::encoding::{Decodable, Encodable};
44use fedimint_core::module::{
45 AmountUnit, Amounts, ApiVersion, CommonModuleInit, ModuleCommon, ModuleInit, MultiApiVersion,
46};
47use fedimint_core::task::{TaskGroup, TaskHandle, sleep};
48use fedimint_core::{Amount, OutPoint, TransactionId, apply, async_trait_maybe_send};
49use fedimint_derive_secret::{ChildId, DerivableSecret};
50use fedimint_eventlog::{Event, EventLogId};
51use fedimint_logging::LOG_CLIENT_MODULE_WALLETV2;
52use fedimint_walletv2_common::config::WalletClientConfig;
53use fedimint_walletv2_common::{
54 KIND, OutputInfo, StandardScript, TxInfo, WalletCommonInit, WalletInput, WalletInputV0,
55 WalletModuleTypes, WalletOutput, WalletOutputV0, descriptor, is_potential_receive,
56};
57use futures::StreamExt;
58use receive_sm::{ReceiveSMCommon, ReceiveSMState, ReceiveStateMachine};
59use secp256k1::Keypair;
60use send_sm::{SendSMCommon, SendSMState, SendStateMachine};
61use serde::{Deserialize, Serialize};
62use strum::IntoEnumIterator as _;
63use thiserror::Error;
64use tracing::{debug, warn};
65
66const SLICE_SIZE: u64 = 1000;
68
69const EVENT_LOG_PAGE_SIZE: u64 = 1000;
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
73pub enum WalletOperationMeta {
74 Send(SendMeta),
75 Receive(ReceiveMeta),
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct SendMeta {
80 pub change_outpoint_range: OutPointRange,
81 pub address: Address<NetworkUnchecked>,
82 pub value: bitcoin::Amount,
83 pub fee: bitcoin::Amount,
84 #[serde(default)]
85 pub custom_meta: serde_json::Value,
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct ReceiveMeta {
90 pub change_outpoint_range: OutPointRange,
91 pub value: bitcoin::Amount,
92 pub fee: bitcoin::Amount,
93 pub address: Option<Address<NetworkUnchecked>>,
94 pub outpoint: Option<bitcoin::OutPoint>,
95}
96
97#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
99pub enum FinalSendOperationState {
100 Success(bitcoin::Txid),
102 Aborted,
104 Failure,
106}
107
108#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
110pub enum FinalReceiveOperationState {
111 Success,
113 Aborted,
115}
116
117#[derive(Debug, Clone)]
118pub struct WalletClientModule {
119 root_secret: DerivableSecret,
120 cfg: WalletClientConfig,
121 notifier: ModuleNotifier<WalletClientStateMachines>,
122 client_ctx: ClientContext<Self>,
123 db: Database,
124 module_api: DynModuleApi,
125}
126
127#[derive(Debug, Clone)]
128pub struct WalletClientContext {
129 pub client_ctx: ClientContext<WalletClientModule>,
130}
131
132impl Context for WalletClientContext {
133 const KIND: Option<ModuleKind> = Some(KIND);
134}
135
136#[apply(async_trait_maybe_send!)]
137impl ClientModule for WalletClientModule {
138 type Init = WalletClientInit;
139 type Common = WalletModuleTypes;
140 type Backup = NoModuleBackup;
141 type ModuleStateMachineContext = WalletClientContext;
142 type States = WalletClientStateMachines;
143
144 fn context(&self) -> Self::ModuleStateMachineContext {
145 WalletClientContext {
146 client_ctx: self.client_ctx.clone(),
147 }
148 }
149
150 fn input_fee(
151 &self,
152 amount: &Amounts,
153 _input: &<Self::Common as ModuleCommon>::Input,
154 ) -> Option<Amounts> {
155 amount
156 .get(&AmountUnit::BITCOIN)
157 .map(|a| Amounts::new_bitcoin(self.cfg.fee_consensus.fee(*a)))
158 }
159
160 fn output_fee(
161 &self,
162 amount: &Amounts,
163 _output: &<Self::Common as ModuleCommon>::Output,
164 ) -> Option<Amounts> {
165 amount
166 .get(&AmountUnit::BITCOIN)
167 .map(|a| Amounts::new_bitcoin(self.cfg.fee_consensus.fee(*a)))
168 }
169
170 #[cfg(feature = "cli")]
171 async fn handle_cli_command(
172 &self,
173 args: &[std::ffi::OsString],
174 ) -> anyhow::Result<serde_json::Value> {
175 cli::handle_cli_command(self, args).await
176 }
177}
178
179#[derive(Debug, Clone, Default)]
180pub struct WalletClientInit;
181
182impl ModuleInit for WalletClientInit {
183 type Common = WalletCommonInit;
184
185 async fn dump_database(
186 &self,
187 _dbtx: &mut DatabaseTransaction<'_>,
188 _prefix_names: Vec<String>,
189 ) -> Box<dyn Iterator<Item = (String, Box<dyn erased_serde::Serialize + Send>)> + '_> {
190 Box::new(BTreeMap::new().into_iter())
191 }
192}
193
194#[apply(async_trait_maybe_send!)]
195impl ClientModuleInit for WalletClientInit {
196 type Module = WalletClientModule;
197
198 fn supported_api_versions(&self) -> MultiApiVersion {
199 MultiApiVersion::try_from_iter([ApiVersion { major: 0, minor: 0 }])
200 .expect("no version conflicts")
201 }
202
203 async fn init(&self, args: &ClientModuleInitArgs<Self>) -> anyhow::Result<Self::Module> {
204 let module = WalletClientModule {
205 root_secret: args.module_root_secret().clone(),
206 cfg: args.cfg().clone(),
207 notifier: args.notifier().clone(),
208 client_ctx: args.context(),
209 db: args.db().clone(),
210 module_api: args.module_api().clone(),
211 };
212
213 module.spawn_output_scanner(args.task_group(), args.client_span());
214
215 Ok(module)
216 }
217
218 fn get_database_migrations(&self) -> BTreeMap<DatabaseVersion, ClientModuleMigrationFn> {
219 BTreeMap::new()
220 }
221
222 fn used_db_prefixes(&self) -> Option<BTreeSet<u8>> {
223 Some(db::DbKeyPrefix::iter().map(|p| p as u8).collect())
224 }
225}
226
227impl WalletClientModule {
228 pub fn get_network(&self) -> bitcoin::Network {
230 self.cfg.network
231 }
232
233 pub async fn total_value(&self) -> FederationResult<bitcoin::Amount> {
235 self.module_api
236 .federation_wallet()
237 .await
238 .map(|tx_out| tx_out.map_or(bitcoin::Amount::ZERO, |tx_out| tx_out.value))
239 }
240
241 pub async fn block_count(&self) -> FederationResult<u64> {
243 self.module_api.consensus_block_count().await
244 }
245
246 pub async fn feerate(&self) -> FederationResult<Option<u64>> {
248 self.module_api.consensus_feerate().await
249 }
250
251 pub async fn pending_tx_chain(&self) -> FederationResult<Vec<TxInfo>> {
253 self.module_api.pending_tx_chain().await
254 }
255
256 pub async fn tx_chain(&self) -> FederationResult<Vec<TxInfo>> {
258 self.module_api.tx_chain().await
259 }
260
261 pub async fn send_fee(&self) -> Result<bitcoin::Amount, SendError> {
263 self.module_api
264 .send_fee()
265 .await
266 .map_err(|e| SendError::FederationError(e.to_string()))?
267 .ok_or(SendError::NoConsensusFeerateAvailable)
268 }
269
270 pub async fn send_fee_quote(&self, amount: bitcoin::Amount) -> anyhow::Result<FeeQuote> {
285 let amount = Amount::from_sats(amount.to_sat());
286 self.client_ctx
287 .fee_quote(
288 OperationId::new_random(),
289 FeeQuoteRequest {
290 input_amount: Amounts::ZERO,
291 output_amount: Amounts::new_bitcoin(amount),
292 input_fee: Amounts::ZERO,
293 output_fee: Amounts::new_bitcoin(self.cfg.fee_consensus.fee(amount)),
294 },
295 )
296 .await
297 }
298
299 pub async fn max_sendable_amount(
334 &self,
335 balance: Amount,
336 fee: bitcoin::Amount,
337 ) -> anyhow::Result<bitcoin::Amount> {
338 let fee_msats = Amount::from_sats(fee.to_sat());
339
340 let max = max_affordable_send_amount(
341 balance,
342 Amount::from_sats(self.cfg.dust_limit.to_sat()),
343 balance,
344 |value: Amount| Amount::from_sats(value.msats.div_ceil(1000)) + fee_msats,
349 |funded: Amount| self.send_fee_quote(bitcoin::Amount::from_sat(funded.msats / 1000)),
350 )
351 .await
352 .ok_or_else(|| anyhow!("Balance is too low to send any amount on chain after fees"))?;
353
354 Ok(bitcoin::Amount::from_sat(max.msats.div_ceil(1000)))
357 }
358
359 pub async fn receive_fee(&self) -> Result<bitcoin::Amount, ReceiveError> {
361 self.module_api
362 .receive_fee()
363 .await
364 .map_err(|e| ReceiveError::FederationError(e.to_string()))?
365 .ok_or(ReceiveError::NoConsensusFeerateAvailable)
366 }
367
368 pub async fn send(
370 &self,
371 address: Address<NetworkUnchecked>,
372 value: bitcoin::Amount,
373 fee: Option<bitcoin::Amount>,
374 custom_meta: serde_json::Value,
375 ) -> Result<OperationId, SendError> {
376 if !address.is_valid_for_network(self.cfg.network) {
377 return Err(SendError::WrongNetwork);
378 }
379
380 if value < self.cfg.dust_limit {
381 return Err(SendError::DustValue);
382 }
383
384 let fee = match fee {
385 Some(value) => value,
386 None => self
387 .module_api
388 .send_fee()
389 .await
390 .map_err(|e| SendError::FederationError(e.to_string()))?
391 .ok_or(SendError::NoConsensusFeerateAvailable)?,
392 };
393
394 let operation_id = OperationId::new_random();
395
396 let destination = StandardScript::from_address(&address.clone().assume_checked())
397 .ok_or(SendError::UnsupportedAddress)?;
398
399 let client_output = ClientOutput::<WalletOutput> {
400 output: WalletOutput::V0(WalletOutputV0 {
401 destination,
402 value,
403 fee,
404 }),
405 amounts: Amounts::new_bitcoin(Amount::from_sats((value + fee).to_sat())),
406 };
407
408 let client_output_sm = ClientOutputSM::<WalletClientStateMachines> {
409 state_machines: Arc::new(move |range: OutPointRange| {
410 vec![WalletClientStateMachines::Send(SendStateMachine {
411 common: SendSMCommon {
412 operation_id,
413 outpoint: OutPoint {
414 txid: range.txid(),
415 out_idx: 0,
416 },
417 value,
418 fee,
419 },
420 state: SendSMState::Funding,
421 })]
422 }),
423 };
424
425 let client_output_bundle = self.client_ctx.make_client_outputs(ClientOutputBundle::new(
426 vec![client_output],
427 vec![client_output_sm],
428 ));
429
430 let address_clone = address.clone();
431
432 self.client_ctx
433 .finalize_and_submit_transaction(
434 operation_id,
435 WalletCommonInit::KIND.as_str(),
436 move |change_outpoint_range| {
437 WalletOperationMeta::Send(SendMeta {
438 change_outpoint_range,
439 address: address_clone.clone(),
440 value,
441 fee,
442 custom_meta: custom_meta.clone(),
443 })
444 },
445 TransactionBuilder::new().with_outputs(client_output_bundle),
446 )
447 .await
448 .map_err(|_| SendError::InsufficientFunds)?;
449
450 let mut dbtx = self.client_ctx.module_db().begin_transaction().await;
451
452 self.client_ctx
453 .log_event(
454 &mut dbtx,
455 SendPaymentEvent {
456 operation_id,
457 address,
458 value,
459 fee,
460 },
461 )
462 .await;
463
464 dbtx.commit_tx().await;
465
466 Ok(operation_id)
467 }
468
469 pub async fn await_final_send_operation_state(
471 &self,
472 operation_id: OperationId,
473 ) -> anyhow::Result<FinalSendOperationState> {
474 let operation = self.client_ctx.get_operation(operation_id).await?;
475 let mut stream = self.notifier.subscribe(operation_id).await;
476
477 let mut stream = self
478 .client_ctx
479 .outcome_or_updates(&operation, operation_id, |_| true, move || {
480 async_stream::stream! {
481 loop {
482 if let Some(WalletClientStateMachines::Send(state)) = stream.next().await {
483 match state.state {
484 SendSMState::Funding => {}
485 SendSMState::Success(txid) => {
486 yield FinalSendOperationState::Success(txid);
487 return;
488 }
489 SendSMState::Aborted(..) => {
490 yield FinalSendOperationState::Aborted;
491 return;
492 }
493 SendSMState::Failure => {
494 yield FinalSendOperationState::Failure;
495 return;
496 }
497 }
498 }
499 }
500 }
501 })
502 .into_stream();
503
504 let mut final_state = None;
505
506 while let Some(state) = stream.next().await {
507 final_state = Some(state);
508 }
509
510 Ok(final_state.expect("Stream contains one final state"))
511 }
512
513 pub async fn await_final_receive_operation_state(
515 &self,
516 operation_id: OperationId,
517 ) -> anyhow::Result<FinalReceiveOperationState> {
518 let operation = self.client_ctx.get_operation(operation_id).await?;
519 let mut stream = self.notifier.subscribe(operation_id).await;
520
521 let mut stream = self
522 .client_ctx
523 .outcome_or_updates(&operation, operation_id, |_| true, move || {
524 async_stream::stream! {
525 loop {
526 if let Some(WalletClientStateMachines::Receive(state)) = stream.next().await {
527 match state.state {
528 ReceiveSMState::Funding => {}
529 ReceiveSMState::Success => {
530 yield FinalReceiveOperationState::Success;
531 return;
532 }
533 ReceiveSMState::Aborted(..) => {
534 yield FinalReceiveOperationState::Aborted;
535 return;
536 }
537 }
538 }
539 }
540 }
541 })
542 .into_stream();
543
544 let mut final_state = None;
545
546 while let Some(state) = stream.next().await {
547 final_state = Some(state);
548 }
549
550 Ok(final_state.expect("Stream contains one final state"))
551 }
552
553 async fn valid_index(&self) -> Option<u64> {
556 self.db
557 .begin_transaction_nc()
558 .await
559 .find_by_prefix_sorted_descending(&ValidAddressIndexPrefix)
560 .await
561 .next()
562 .await
563 .map(|entry| entry.0.0)
564 }
565
566 pub async fn receive(&self) -> Address {
579 loop {
580 if let Some(index) = self.valid_index().await {
581 return self.derive_address(index);
582 }
583
584 sleep(Duration::from_secs(1)).await;
585 }
586 }
587
588 pub async fn await_receive(
599 &self,
600 position: EventLogId,
601 ) -> anyhow::Result<(FinalReceiveOperationState, EventLogId)> {
602 let mut position = position;
603
604 loop {
605 let (operation_id, next_position) = self.next_receive_operation(position).await;
606
607 position = next_position;
608
609 let state = self
610 .await_final_receive_operation_state(operation_id)
611 .await?;
612
613 if state == FinalReceiveOperationState::Success {
616 let operation = self.client_ctx.get_operation(operation_id).await?;
622
623 if let WalletOperationMeta::Receive(ReceiveMeta {
624 change_outpoint_range,
625 ..
626 }) = operation.meta::<WalletOperationMeta>()
627 {
628 self.client_ctx
629 .await_primary_module_outputs(
630 operation_id,
631 change_outpoint_range.into_iter().collect(),
632 )
633 .await?;
634 }
635
636 return Ok((state, position));
637 }
638 }
639 }
640
641 async fn next_receive_operation(&self, position: EventLogId) -> (OperationId, EventLogId) {
645 let mut position = position;
646
647 loop {
648 let events = self
649 .client_ctx
650 .get_event_log(Some(position), EVENT_LOG_PAGE_SIZE)
651 .await;
652
653 for entry in &events {
654 position = entry.id().saturating_add(1);
655
656 if entry.module_kind() == Some(&KIND)
657 && entry.kind == ReceivePaymentEvent::KIND
658 && let Some(event) = entry.to_event::<ReceivePaymentEvent>()
659 {
660 return (event.operation_id, position);
661 }
662 }
663
664 if events.is_empty() {
665 sleep(Duration::from_secs(1)).await;
667 }
668 }
669 }
670
671 fn derive_address(&self, index: u64) -> Address {
672 descriptor(
673 &self.cfg.bitcoin_pks,
674 &self.derive_tweak(index).public_key().consensus_hash(),
675 )
676 .address(self.cfg.network)
677 }
678
679 fn derive_tweak(&self, index: u64) -> Keypair {
680 self.root_secret
681 .child_key(ChildId(index))
682 .to_secp_key(secp256k1::SECP256K1)
683 }
684
685 async fn next_valid_index(&self, start_index: u64, handle: &TaskHandle) -> Option<u64> {
693 const SCAN_BATCH: u64 = 256;
695
696 let pks_hash = self.cfg.bitcoin_pks.consensus_hash();
697
698 let mut index = start_index;
699
700 while !handle.is_shutting_down() {
701 for _ in 0..SCAN_BATCH {
702 if is_potential_receive(&self.derive_address(index).script_pubkey(), &pks_hash) {
703 return Some(index);
704 }
705
706 index += 1;
707 }
708
709 sleep(Duration::ZERO).await;
711 }
712
713 None
714 }
715
716 async fn receive_output(
721 &self,
722 output_index: u64,
723 value: bitcoin::Amount,
724 address_index: u64,
725 fee: bitcoin::Amount,
726 outpoint: Option<bitcoin::OutPoint>,
727 ) -> Option<(OperationId, TransactionId)> {
728 let operation_id = OperationId::new_random();
729
730 let client_input = ClientInput::<WalletInput> {
731 input: WalletInput::V0(WalletInputV0 {
732 output_index,
733 fee,
734 tweak: self.derive_tweak(address_index).public_key(),
735 }),
736 keys: vec![self.derive_tweak(address_index)],
737 amounts: Amounts::new_bitcoin(Amount::from_sats(value.checked_sub(fee)?.to_sat())),
738 };
739
740 let client_input_sm = ClientInputSM::<WalletClientStateMachines> {
741 state_machines: Arc::new(move |range: OutPointRange| {
742 vec![WalletClientStateMachines::Receive(ReceiveStateMachine {
743 common: ReceiveSMCommon {
744 operation_id,
745 txid: range.txid(),
746 value,
747 fee,
748 },
749 state: ReceiveSMState::Funding,
750 })]
751 }),
752 };
753
754 let client_input_bundle = self.client_ctx.make_client_inputs(ClientInputBundle::new(
755 vec![client_input],
756 vec![client_input_sm],
757 ));
758
759 let address = self.derive_address(address_index).as_unchecked().clone();
760
761 let meta_address = address.clone();
762 let range = self
763 .client_ctx
764 .finalize_and_submit_transaction(
765 operation_id,
766 WalletCommonInit::KIND.as_str(),
767 move |change_outpoint_range| {
768 WalletOperationMeta::Receive(ReceiveMeta {
769 change_outpoint_range,
770 value,
771 fee,
772 address: Some(meta_address.clone()),
773 outpoint,
774 })
775 },
776 TransactionBuilder::new().with_inputs(client_input_bundle),
777 )
778 .await
779 .ok()?;
780
781 let mut dbtx = self.client_ctx.module_db().begin_transaction().await;
782
783 self.client_ctx
784 .log_event(
785 &mut dbtx,
786 ReceivePaymentEvent {
787 operation_id,
788 value,
789 fee,
790 address,
791 outpoint,
792 },
793 )
794 .await;
795
796 dbtx.commit_tx().await;
797
798 Some((operation_id, range.txid()))
799 }
800
801 fn spawn_output_scanner(&self, task_group: &TaskGroup, client_span: &tracing::Span) {
802 let module = self.clone();
803 let handle = task_group.make_handle();
804
805 task_group.spawn_cancellable_with_span(client_span.clone(), "output-scanner", async move {
806 let mut dbtx = module.db.begin_transaction().await;
807
808 if dbtx
809 .find_by_prefix(&ValidAddressIndexPrefix)
810 .await
811 .next()
812 .await
813 .is_none()
814 {
815 let Some(index) = module.next_valid_index(0, &handle).await else {
816 return;
817 };
818
819 dbtx.insert_new_entry(&ValidAddressIndexKey(index), &())
820 .await;
821 }
822
823 dbtx.commit_tx().await;
824
825 loop {
826 match module.check_outputs(&handle).await {
827 Ok(skip_wait) => {
828 if skip_wait {
829 continue;
830 }
831 }
832 Err(e) => {
833 warn!(target: LOG_CLIENT_MODULE_WALLETV2, "Failed to fetch outputs: {e}");
834 }
835 }
836
837 sleep(fedimint_walletv2_common::sleep_duration()).await;
838 }
839 });
840 }
841
842 async fn check_outputs(&self, handle: &TaskHandle) -> anyhow::Result<bool> {
843 let mut dbtx = self.db.begin_transaction_nc().await;
844
845 let next_output_index = dbtx.get_value(&NextOutputIndexKey).await.unwrap_or(0);
846
847 let mut valid_indices: Vec<u64> = dbtx
848 .find_by_prefix(&ValidAddressIndexPrefix)
849 .await
850 .map(|entry| entry.0.0)
851 .collect()
852 .await;
853
854 let mut address_map: BTreeMap<ScriptBuf, u64> = valid_indices
855 .iter()
856 .map(|&i| (self.derive_address(i).script_pubkey(), i))
857 .collect();
858
859 let outputs = self
860 .module_api
861 .output_info_slice(next_output_index, next_output_index + SLICE_SIZE)
862 .await?;
863
864 let returned_num = outputs.len();
865 let mut matched_num: usize = 0;
866
867 for output in &outputs {
868 if let Some(&address_index) = address_map.get(&output.script) {
869 matched_num += 1;
870
871 if !output.spent && !self.process_unspent_output(output, address_index).await? {
876 return Ok(false);
877 }
878
879 let next_address_index = valid_indices
880 .last()
881 .copied()
882 .expect("we have at least one address index");
883
884 if address_index == next_address_index {
886 let Some(index) = self.next_valid_index(next_address_index + 1, handle).await
887 else {
888 return Ok(false);
889 };
890
891 let mut dbtx = self.db.begin_transaction().await;
892
893 dbtx.insert_entry(&ValidAddressIndexKey(index), &()).await;
894
895 dbtx.commit_tx_result().await?;
896
897 valid_indices.push(index);
898
899 address_map.insert(self.derive_address(index).script_pubkey(), index);
900 }
901 }
902
903 let mut dbtx = self.db.begin_transaction().await;
904
905 dbtx.insert_entry(&NextOutputIndexKey, &(output.index + 1))
906 .await;
907
908 dbtx.commit_tx_result().await?;
909 }
910
911 debug!(
912 target: LOG_CLIENT_MODULE_WALLETV2,
913 next_output_index,
914 returned_num,
915 matched_num,
916 valid_indices_num = valid_indices.len(),
917 "Scanning for outputs"
918 );
919
920 Ok(!outputs.is_empty())
921 }
922
923 async fn process_unspent_output(
924 &self,
925 output: &OutputInfo,
926 address_index: u64,
927 ) -> anyhow::Result<bool> {
928 debug!(
929 target: LOG_CLIENT_MODULE_WALLETV2,
930 output_index = output.index,
931 value_sat = output.value.to_sat(),
932 address_index,
933 outpoint = ?output.outpoint,
934 "Discovered unspent walletv2 receive output"
935 );
936
937 let pending_tx_chain_len = self.module_api.pending_tx_chain().await?.len();
940 if 3 <= pending_tx_chain_len {
941 debug!(
942 target: LOG_CLIENT_MODULE_WALLETV2,
943 output_index = output.index,
944 pending_tx_chain_len,
945 "Delaying walletv2 receive claim because pending transaction chain is full"
946 );
947 return Ok(false);
948 }
949
950 let receive_fee = self
951 .module_api
952 .receive_fee()
953 .await?
954 .ok_or(anyhow!("No consensus feerate is available"))?;
955
956 if let Some((operation_id, txid)) = self
957 .receive_output(
958 output.index,
959 output.value,
960 address_index,
961 receive_fee,
962 output.outpoint,
963 )
964 .await
965 {
966 debug!(
967 target: LOG_CLIENT_MODULE_WALLETV2,
968 output_index = output.index,
969 ?operation_id,
970 %txid,
971 "Waiting for walletv2 receive claim acceptance"
972 );
973 self.client_ctx
974 .transaction_updates(operation_id)
975 .await
976 .await_tx_accepted(txid)
977 .await
978 .map_err(|e| anyhow!("Claim transaction was rejected: {e}"))?;
979 debug!(
980 target: LOG_CLIENT_MODULE_WALLETV2,
981 output_index = output.index,
982 ?operation_id,
983 %txid,
984 "Walletv2 receive claim accepted"
985 );
986 } else {
987 debug!(
988 target: LOG_CLIENT_MODULE_WALLETV2,
989 output_index = output.index,
990 value_sat = output.value.to_sat(),
991 fee_sat = receive_fee.to_sat(),
992 "Skipping walletv2 receive claim; value cannot cover the claim fees"
993 );
994 }
995
996 Ok(true)
997 }
998}
999
1000#[derive(Error, Debug, Clone, Eq, PartialEq)]
1001pub enum SendError {
1002 #[error("Address is from a different network than the federation.")]
1003 WrongNetwork,
1004 #[error("The value is too small")]
1005 DustValue,
1006 #[error("Federation returned an error: {0}")]
1007 FederationError(String),
1008 #[error("No consensus feerate is available at this time")]
1009 NoConsensusFeerateAvailable,
1010 #[error("The client does not have sufficient funds to send the payment")]
1011 InsufficientFunds,
1012 #[error("Unsupported address type")]
1013 UnsupportedAddress,
1014}
1015
1016#[derive(Error, Debug, Clone, Eq, PartialEq)]
1017pub enum ReceiveError {
1018 #[error("Federation returned an error: {0}")]
1019 FederationError(String),
1020 #[error("No consensus feerate is available at this time")]
1021 NoConsensusFeerateAvailable,
1022}
1023
1024#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
1025pub enum WalletClientStateMachines {
1026 Send(send_sm::SendStateMachine),
1027 Receive(receive_sm::ReceiveStateMachine),
1028}
1029
1030impl State for WalletClientStateMachines {
1031 type ModuleContext = WalletClientContext;
1032
1033 fn transitions(
1034 &self,
1035 context: &Self::ModuleContext,
1036 global_context: &DynGlobalClientContext,
1037 ) -> Vec<StateTransition<Self>> {
1038 match self {
1039 WalletClientStateMachines::Send(sm) => sm_enum_variant_translation!(
1040 sm.transitions(context, global_context),
1041 WalletClientStateMachines::Send
1042 ),
1043 WalletClientStateMachines::Receive(sm) => sm_enum_variant_translation!(
1044 sm.transitions(context, global_context),
1045 WalletClientStateMachines::Receive
1046 ),
1047 }
1048 }
1049
1050 fn operation_id(&self) -> OperationId {
1051 match self {
1052 WalletClientStateMachines::Send(sm) => sm.operation_id(),
1053 WalletClientStateMachines::Receive(sm) => sm.operation_id(),
1054 }
1055 }
1056}
1057
1058impl IntoDynInstance for WalletClientStateMachines {
1059 type DynType = DynState;
1060
1061 fn into_dyn(self, instance_id: ModuleInstanceId) -> Self::DynType {
1062 DynState::from_typed(instance_id, self)
1063 }
1064}