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