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;
8pub use fedimint_walletv2_common::MempoolVisibility;
9
10mod api;
11#[cfg(feature = "cli")]
12mod cli;
13pub mod db;
14pub mod events;
15mod receive_sm;
16mod send_sm;
17
18use std::collections::{BTreeMap, BTreeSet};
19use std::sync::Arc;
20use std::time::Duration;
21
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, FederationError, 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::error::{
35 ClientModuleError, OperationLookupError, TransactionSubmitError,
36};
37use fedimint_client_module::module::init::{ClientModuleInit, ClientModuleInitArgs};
38use fedimint_client_module::module::recovery::NoModuleBackup;
39use fedimint_client_module::module::{ClientContext, ClientModule, OutPointRange};
40use fedimint_client_module::sm::{Context, DynState, ModuleNotifier, State, StateTransition};
41use fedimint_client_module::sm_enum_variant_translation;
42use fedimint_core::core::{IntoDynInstance, ModuleInstanceId, ModuleKind, OperationId};
43use fedimint_core::db::{
44 AutocommitError, Database, DatabaseError, DatabaseTransaction, DatabaseVersion,
45 IDatabaseTransactionOpsCoreTyped,
46};
47use fedimint_core::encoding::{Decodable, Encodable};
48use fedimint_core::module::{
49 AmountUnit, Amounts, ApiVersion, CommonModuleInit, ModuleCommon, ModuleInit, MultiApiVersion,
50};
51use fedimint_core::task::{MaybeSend, MaybeSync, TaskGroup, TaskHandle, sleep};
52use fedimint_core::{Amount, OutPoint, TransactionId, apply, async_trait_maybe_send};
53use fedimint_derive_secret::{ChildId, DerivableSecret};
54use fedimint_eventlog::{Event, EventLogId};
55use fedimint_logging::LOG_CLIENT_MODULE_WALLETV2;
56use fedimint_walletv2_common::config::WalletClientConfig;
57use fedimint_walletv2_common::{
58 BLOCK_COUNT_VOTE_DELAY, KIND, OutputInfo, PendingOutput, StandardScript, TxInfo,
59 WalletCommonInit, WalletInput, WalletInputV0, WalletModuleTypes, WalletOutput, WalletOutputV0,
60 descriptor, is_potential_receive,
61};
62use futures::StreamExt;
63use receive_sm::{ReceiveSMCommon, ReceiveSMState, ReceiveStateMachine};
64use secp256k1::Keypair;
65use send_sm::{SendSMCommon, SendSMState, SendStateMachine};
66use serde::{Deserialize, Serialize};
67use strum::IntoEnumIterator as _;
68use thiserror::Error;
69use tracing::{debug, warn};
70
71const SLICE_SIZE: u64 = 1000;
73
74const EVENT_LOG_PAGE_SIZE: u64 = 1000;
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
78pub enum WalletOperationMeta {
79 Send(SendMeta),
80 Receive(ReceiveMeta),
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct SendMeta {
85 pub change_outpoint_range: OutPointRange,
86 pub address: Address<NetworkUnchecked>,
87 pub value: bitcoin::Amount,
88 pub fee: bitcoin::Amount,
89 #[serde(default)]
90 pub custom_meta: serde_json::Value,
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct ReceiveMeta {
95 pub change_outpoint_range: OutPointRange,
96 pub value: bitcoin::Amount,
97 pub fee: bitcoin::Amount,
98 pub address: Option<Address<NetworkUnchecked>>,
99 pub outpoint: Option<bitcoin::OutPoint>,
100}
101
102#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
104pub enum FinalSendOperationState {
105 Success(bitcoin::Txid),
107 Aborted,
109 Failure,
111}
112
113#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
115pub enum FinalReceiveOperationState {
116 Success,
118 Aborted,
120}
121
122#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
140pub enum ReceiveProgress {
141 Mempool { value: bitcoin::Amount },
151 Confirming {
154 value: bitcoin::Amount,
155 height: u64,
160 confirmations: u64,
163 required: u64,
165 },
166}
167
168#[derive(Debug, Clone, PartialEq, Eq)]
174pub struct AddressReceiveProgress {
175 pub deposits: Vec<(bitcoin::OutPoint, ReceiveProgress)>,
177 pub mempool_visibility: MempoolVisibility,
197}
198
199#[derive(Debug, Clone)]
200pub struct WalletClientModule {
201 root_secret: DerivableSecret,
202 cfg: WalletClientConfig,
203 notifier: ModuleNotifier<WalletClientStateMachines>,
204 client_ctx: ClientContext<Self>,
205 db: Database,
206 module_api: DynModuleApi,
207}
208
209#[derive(Debug, Clone)]
210pub struct WalletClientContext {
211 pub client_ctx: ClientContext<WalletClientModule>,
212}
213
214impl Context for WalletClientContext {
215 const KIND: Option<ModuleKind> = Some(KIND);
216}
217
218#[apply(async_trait_maybe_send!)]
219impl ClientModule for WalletClientModule {
220 type Init = WalletClientInit;
221 type Common = WalletModuleTypes;
222 type Backup = NoModuleBackup;
223 type ModuleStateMachineContext = WalletClientContext;
224 type States = WalletClientStateMachines;
225
226 fn context(&self) -> Self::ModuleStateMachineContext {
227 WalletClientContext {
228 client_ctx: self.client_ctx.clone(),
229 }
230 }
231
232 fn input_fee(
233 &self,
234 amount: &Amounts,
235 _input: &<Self::Common as ModuleCommon>::Input,
236 ) -> Option<Amounts> {
237 amount
238 .get(&AmountUnit::BITCOIN)
239 .map(|a| Amounts::new_bitcoin(self.cfg.fee_consensus.fee(*a)))
240 }
241
242 fn output_fee(
243 &self,
244 amount: &Amounts,
245 _output: &<Self::Common as ModuleCommon>::Output,
246 ) -> Option<Amounts> {
247 amount
248 .get(&AmountUnit::BITCOIN)
249 .map(|a| Amounts::new_bitcoin(self.cfg.fee_consensus.fee(*a)))
250 }
251
252 #[cfg(feature = "cli")]
253 async fn handle_cli_command(
254 &self,
255 args: &[std::ffi::OsString],
256 ) -> Result<serde_json::Value, ClientModuleError> {
257 cli::handle_cli_command(self, args)
258 .await
259 .map_err(ClientModuleError::other)
260 }
261}
262
263#[derive(Debug, Clone, Default)]
264pub struct WalletClientInit;
265
266impl ModuleInit for WalletClientInit {
267 type Common = WalletCommonInit;
268
269 async fn dump_database(
270 &self,
271 _dbtx: &mut DatabaseTransaction<'_>,
272 _prefix_names: Vec<String>,
273 ) -> Box<dyn Iterator<Item = (String, Box<dyn erased_serde::Serialize + Send>)> + '_> {
274 Box::new(BTreeMap::new().into_iter())
275 }
276}
277
278#[apply(async_trait_maybe_send!)]
279impl ClientModuleInit for WalletClientInit {
280 type Module = WalletClientModule;
281
282 fn supported_api_versions(&self) -> MultiApiVersion {
283 MultiApiVersion::try_from_iter([ApiVersion { major: 0, minor: 0 }])
284 .expect("no version conflicts")
285 }
286
287 async fn init(
288 &self,
289 args: &ClientModuleInitArgs<Self>,
290 ) -> Result<Self::Module, ClientModuleError> {
291 let module = WalletClientModule {
292 root_secret: args.module_root_secret().clone(),
293 cfg: args.cfg().clone(),
294 notifier: args.notifier().clone(),
295 client_ctx: args.context(),
296 db: args.db().clone(),
297 module_api: args.module_api().clone(),
298 };
299
300 module.spawn_output_scanner(args.task_group(), args.client_span());
301
302 Ok(module)
303 }
304
305 fn get_database_migrations(&self) -> BTreeMap<DatabaseVersion, ClientModuleMigrationFn> {
306 BTreeMap::new()
307 }
308
309 fn used_db_prefixes(&self) -> Option<BTreeSet<u8>> {
310 Some(db::DbKeyPrefix::iter().map(|p| p as u8).collect())
311 }
312}
313
314impl WalletClientModule {
315 pub fn get_network(&self) -> bitcoin::Network {
317 self.cfg.network
318 }
319
320 pub async fn total_value(&self) -> FederationResult<bitcoin::Amount> {
322 self.module_api
323 .federation_wallet()
324 .await
325 .map(|tx_out| tx_out.map_or(bitcoin::Amount::ZERO, |tx_out| tx_out.value))
326 }
327
328 pub async fn block_count(&self) -> FederationResult<u64> {
330 self.module_api.consensus_block_count().await
331 }
332
333 pub async fn feerate(&self) -> FederationResult<Option<u64>> {
335 self.module_api.consensus_feerate().await
336 }
337
338 pub async fn pending_tx_chain(&self) -> FederationResult<Vec<TxInfo>> {
340 self.module_api.pending_tx_chain().await
341 }
342
343 pub async fn tx_chain(&self) -> FederationResult<Vec<TxInfo>> {
345 self.module_api.tx_chain().await
346 }
347
348 pub async fn send_fee(&self) -> Result<bitcoin::Amount, SendError> {
350 self.module_api
351 .send_fee()
352 .await
353 .map_err(|e| SendError::Federation(Box::new(e)))?
354 .ok_or(SendError::NoConsensusFeerateAvailable)
355 }
356
357 pub async fn send_fee_quote(
372 &self,
373 amount: bitcoin::Amount,
374 ) -> Result<FeeQuote, TransactionSubmitError> {
375 let amount = Amount::from_sats(amount.to_sat());
376 self.client_ctx
377 .fee_quote(
378 OperationId::new_random(),
379 FeeQuoteRequest {
380 input_amount: Amounts::ZERO,
381 output_amount: Amounts::new_bitcoin(amount),
382 input_fee: Amounts::ZERO,
383 output_fee: Amounts::new_bitcoin(self.cfg.fee_consensus.fee(amount)),
384 },
385 )
386 .await
387 }
388
389 pub async fn max_sendable_amount(
425 &self,
426 balance: Amount,
427 fee: bitcoin::Amount,
428 ) -> Result<bitcoin::Amount, SendError> {
429 let fee_msats = Amount::from_sats(fee.to_sat());
430
431 let max = max_affordable_send_amount(
432 balance,
433 Amount::from_sats(self.cfg.dust_limit.to_sat()),
434 balance,
435 |value: Amount| Amount::from_sats(value.msats.div_ceil(1000)) + fee_msats,
440 |funded: Amount| self.send_fee_quote(bitcoin::Amount::from_sat(funded.msats / 1000)),
441 )
442 .await
443 .map_err(SendError::Failed)?
444 .ok_or(SendError::InsufficientFunds)?;
445
446 Ok(bitcoin::Amount::from_sat(max.msats.div_ceil(1000)))
449 }
450
451 pub async fn receive_fee(&self) -> Result<bitcoin::Amount, ReceiveError> {
453 self.module_api
454 .receive_fee()
455 .await
456 .map_err(|e| ReceiveError::Federation(Box::new(e)))?
457 .ok_or(ReceiveError::NoConsensusFeerateAvailable)
458 }
459
460 pub async fn send(
462 &self,
463 address: Address<NetworkUnchecked>,
464 value: bitcoin::Amount,
465 fee: Option<bitcoin::Amount>,
466 custom_meta: serde_json::Value,
467 ) -> Result<OperationId, SendError> {
468 if !address.is_valid_for_network(self.cfg.network) {
469 return Err(SendError::WrongNetwork);
470 }
471
472 if value < self.cfg.dust_limit {
473 return Err(SendError::DustValue);
474 }
475
476 let fee = match fee {
477 Some(value) => value,
478 None => self
479 .module_api
480 .send_fee()
481 .await
482 .map_err(|e| SendError::Federation(Box::new(e)))?
483 .ok_or(SendError::NoConsensusFeerateAvailable)?,
484 };
485
486 let operation_id = OperationId::new_random();
487
488 let destination = StandardScript::from_address(&address.clone().assume_checked())
489 .ok_or(SendError::UnsupportedAddress)?;
490
491 let client_output = ClientOutput::<WalletOutput> {
492 output: WalletOutput::V0(WalletOutputV0 {
493 destination,
494 value,
495 fee,
496 }),
497 amounts: Amounts::new_bitcoin(Amount::from_sats((value + fee).to_sat())),
498 };
499
500 let client_output_sm = ClientOutputSM::<WalletClientStateMachines> {
501 state_machines: Arc::new(move |range: OutPointRange| {
502 vec![WalletClientStateMachines::Send(SendStateMachine {
503 common: SendSMCommon {
504 operation_id,
505 outpoint: OutPoint {
506 txid: range.txid(),
507 out_idx: 0,
508 },
509 value,
510 fee,
511 },
512 state: SendSMState::Funding,
513 })]
514 }),
515 };
516
517 let client_output_bundle = self.client_ctx.make_client_outputs(ClientOutputBundle::new(
518 vec![client_output],
519 vec![client_output_sm],
520 ));
521
522 let address_clone = address.clone();
523
524 self.submit_with_event(
525 operation_id,
526 move |change_outpoint_range| {
527 WalletOperationMeta::Send(SendMeta {
528 change_outpoint_range,
529 address: address_clone.clone(),
530 value,
531 fee,
532 custom_meta: custom_meta.clone(),
533 })
534 },
535 TransactionBuilder::new().with_outputs(client_output_bundle),
536 SendPaymentEvent {
537 operation_id,
538 address,
539 value,
540 fee,
541 },
542 )
543 .await
544 .map_err(|error| match error {
545 TransactionSubmitError::InsufficientFunds(_) => SendError::InsufficientFunds,
546 error => SendError::Failed(error),
547 })?;
548
549 Ok(operation_id)
550 }
551
552 pub async fn await_final_send_operation_state(
554 &self,
555 operation_id: OperationId,
556 ) -> Result<FinalSendOperationState, OperationLookupError> {
557 let operation = self.client_ctx.get_operation(operation_id).await?;
558 let mut stream = self.notifier.subscribe(operation_id).await;
559
560 let mut stream = self
561 .client_ctx
562 .outcome_or_updates(&operation, operation_id, |_| true, move || {
563 async_stream::stream! {
564 loop {
565 if let Some(WalletClientStateMachines::Send(state)) = stream.next().await {
566 match state.state {
567 SendSMState::Funding => {}
568 SendSMState::Success(txid) => {
569 yield FinalSendOperationState::Success(txid);
570 return;
571 }
572 SendSMState::Aborted(..) => {
573 yield FinalSendOperationState::Aborted;
574 return;
575 }
576 SendSMState::Failure => {
577 yield FinalSendOperationState::Failure;
578 return;
579 }
580 }
581 }
582 }
583 }
584 })
585 .into_stream();
586
587 let mut final_state = None;
588
589 while let Some(state) = stream.next().await {
590 final_state = Some(state);
591 }
592
593 Ok(final_state.expect("Stream contains one final state"))
594 }
595
596 pub async fn await_final_receive_operation_state(
598 &self,
599 operation_id: OperationId,
600 ) -> Result<FinalReceiveOperationState, OperationLookupError> {
601 let operation = self.client_ctx.get_operation(operation_id).await?;
602 let mut stream = self.notifier.subscribe(operation_id).await;
603
604 let mut stream = self
605 .client_ctx
606 .outcome_or_updates(&operation, operation_id, |_| true, move || {
607 async_stream::stream! {
608 loop {
609 if let Some(WalletClientStateMachines::Receive(state)) = stream.next().await {
610 match state.state {
611 ReceiveSMState::Funding => {}
612 ReceiveSMState::Success => {
613 yield FinalReceiveOperationState::Success;
614 return;
615 }
616 ReceiveSMState::Aborted(..) => {
617 yield FinalReceiveOperationState::Aborted;
618 return;
619 }
620 }
621 }
622 }
623 }
624 })
625 .into_stream();
626
627 let mut final_state = None;
628
629 while let Some(state) = stream.next().await {
630 final_state = Some(state);
631 }
632
633 Ok(final_state.expect("Stream contains one final state"))
634 }
635
636 async fn address_index(&self, address: &Address) -> Option<u64> {
643 let script = address.script_pubkey();
644
645 let indices: Vec<u64> = self
646 .db
647 .begin_transaction_nc()
648 .await
649 .find_by_prefix(&ValidAddressIndexPrefix)
650 .await
651 .map(|entry| entry.0.0)
652 .collect()
653 .await;
654
655 indices
656 .into_iter()
657 .find(|index| self.derive_address(*index).script_pubkey() == script)
658 }
659
660 pub async fn receive_progress(
673 &self,
674 outpoint: bitcoin::OutPoint,
675 ) -> Result<ReceiveProgress, ReceiveProgressError> {
676 let pending = self.module_api.pending_outputs().await;
677
678 let ours = self.address_map().await;
682
683 pending
684 .outputs
685 .iter()
686 .find(|output| output.outpoint == outpoint && ours.contains_key(&output.script))
687 .map(|output| Self::pending_progress(output, pending.block_count))
688 .ok_or(ReceiveProgressError::UnknownDeposit)
689 }
690
691 pub async fn address_receive_progress(
704 &self,
705 address: &Address,
706 ) -> Result<AddressReceiveProgress, ReceiveProgressError> {
707 self.address_index(address)
708 .await
709 .ok_or(ReceiveProgressError::UnknownAddress)?;
710
711 Ok(self.deposits_paying(&address.script_pubkey()).await)
712 }
713
714 async fn deposits_paying(&self, script: &ScriptBuf) -> AddressReceiveProgress {
716 let pending = self.module_api.pending_outputs().await;
717
718 let mut deposits: Vec<(bitcoin::OutPoint, ReceiveProgress)> = pending
719 .outputs
720 .iter()
721 .filter(|output| output.script == *script)
722 .map(|output| {
723 (
724 output.outpoint,
725 Self::pending_progress(output, pending.block_count),
726 )
727 })
728 .collect();
729
730 deposits.sort_by_key(|(outpoint, _)| *outpoint);
731
732 AddressReceiveProgress {
733 deposits,
734 mempool_visibility: pending.mempool_visibility,
735 }
736 }
737
738 async fn address_map(&self) -> BTreeMap<ScriptBuf, u64> {
740 self.db
741 .begin_transaction_nc()
742 .await
743 .find_by_prefix(&ValidAddressIndexPrefix)
744 .await
745 .map(|entry| entry.0.0)
746 .map(|index| (self.derive_address(index).script_pubkey(), index))
747 .collect()
748 .await
749 }
750
751 fn pending_progress(output: &PendingOutput, block_count: u64) -> ReceiveProgress {
752 let required = BLOCK_COUNT_VOTE_DELAY + 1;
757
758 match output.height {
759 Some(height) => ReceiveProgress::Confirming {
760 value: output.value,
761 height,
762 confirmations: block_count.saturating_sub(height).min(required),
770 required,
771 },
772 None => ReceiveProgress::Mempool {
773 value: output.value,
774 },
775 }
776 }
777
778 async fn valid_index(&self) -> Option<u64> {
781 self.db
782 .begin_transaction_nc()
783 .await
784 .find_by_prefix_sorted_descending(&ValidAddressIndexPrefix)
785 .await
786 .next()
787 .await
788 .map(|entry| entry.0.0)
789 }
790
791 pub async fn receive(&self) -> Address {
804 loop {
805 if let Some(index) = self.valid_index().await {
806 return self.derive_address(index);
807 }
808
809 sleep(Duration::from_secs(1)).await;
810 }
811 }
812
813 pub async fn await_receive(
824 &self,
825 position: EventLogId,
826 ) -> Result<(FinalReceiveOperationState, EventLogId), AwaitReceiveError> {
827 let mut position = position;
828
829 loop {
830 let (operation_id, next_position) = self.next_receive_operation(position).await;
831
832 position = next_position;
833
834 let state = self
835 .await_final_receive_operation_state(operation_id)
836 .await?;
837
838 if state == FinalReceiveOperationState::Success {
841 let operation = self.client_ctx.get_operation(operation_id).await?;
847
848 if let WalletOperationMeta::Receive(ReceiveMeta {
849 change_outpoint_range,
850 ..
851 }) = operation.meta::<WalletOperationMeta>()
852 {
853 self.client_ctx
854 .await_primary_module_outputs(
855 operation_id,
856 change_outpoint_range.into_iter().collect(),
857 )
858 .await?;
859 }
860
861 return Ok((state, position));
862 }
863 }
864 }
865
866 async fn next_receive_operation(&self, position: EventLogId) -> (OperationId, EventLogId) {
870 let mut position = position;
871
872 loop {
873 let events = self
874 .client_ctx
875 .get_event_log(Some(position), EVENT_LOG_PAGE_SIZE)
876 .await;
877
878 for entry in &events {
879 position = entry.id().saturating_add(1);
880
881 if entry.module_kind() == Some(&KIND)
882 && entry.kind == ReceivePaymentEvent::KIND
883 && let Some(event) = entry.to_event::<ReceivePaymentEvent>()
884 {
885 return (event.operation_id, position);
886 }
887 }
888
889 if events.is_empty() {
890 sleep(Duration::from_secs(1)).await;
892 }
893 }
894 }
895
896 fn derive_address(&self, index: u64) -> Address {
897 descriptor(
898 &self.cfg.bitcoin_pks,
899 &self.derive_tweak(index).public_key().consensus_hash(),
900 )
901 .address(self.cfg.network)
902 }
903
904 fn derive_tweak(&self, index: u64) -> Keypair {
905 self.root_secret
906 .child_key(ChildId(index))
907 .to_secp_key(secp256k1::SECP256K1)
908 }
909
910 async fn next_valid_index(&self, start_index: u64, handle: &TaskHandle) -> Option<u64> {
918 const SCAN_BATCH: u64 = 256;
920
921 let pks_hash = self.cfg.bitcoin_pks.consensus_hash();
922
923 let mut index = start_index;
924
925 while !handle.is_shutting_down() {
926 for _ in 0..SCAN_BATCH {
927 if is_potential_receive(&self.derive_address(index).script_pubkey(), &pks_hash) {
928 return Some(index);
929 }
930
931 index += 1;
932 }
933
934 sleep(Duration::ZERO).await;
936 }
937
938 None
939 }
940
941 async fn receive_output(
946 &self,
947 output_index: u64,
948 value: bitcoin::Amount,
949 address_index: u64,
950 fee: bitcoin::Amount,
951 outpoint: Option<bitcoin::OutPoint>,
952 ) -> Option<(OperationId, TransactionId)> {
953 let operation_id = OperationId::new_random();
954
955 let client_input = ClientInput::<WalletInput> {
956 input: WalletInput::V0(WalletInputV0 {
957 output_index,
958 fee,
959 tweak: self.derive_tweak(address_index).public_key(),
960 }),
961 keys: vec![self.derive_tweak(address_index)],
962 amounts: Amounts::new_bitcoin(Amount::from_sats(value.checked_sub(fee)?.to_sat())),
963 };
964
965 let client_input_sm = ClientInputSM::<WalletClientStateMachines> {
966 state_machines: Arc::new(move |range: OutPointRange| {
967 vec![WalletClientStateMachines::Receive(ReceiveStateMachine {
968 common: ReceiveSMCommon {
969 operation_id,
970 txid: range.txid(),
971 value,
972 fee,
973 },
974 state: ReceiveSMState::Funding,
975 })]
976 }),
977 };
978
979 let client_input_bundle = self.client_ctx.make_client_inputs(ClientInputBundle::new(
980 vec![client_input],
981 vec![client_input_sm],
982 ));
983
984 let address = self.derive_address(address_index).as_unchecked().clone();
985
986 let meta_address = address.clone();
987 let range = self
988 .submit_with_event(
989 operation_id,
990 move |change_outpoint_range| {
991 WalletOperationMeta::Receive(ReceiveMeta {
992 change_outpoint_range,
993 value,
994 fee,
995 address: Some(meta_address.clone()),
996 outpoint,
997 })
998 },
999 TransactionBuilder::new().with_inputs(client_input_bundle),
1000 ReceivePaymentEvent {
1001 operation_id,
1002 value,
1003 fee,
1004 address,
1005 outpoint,
1006 },
1007 )
1008 .await
1009 .ok()?;
1010
1011 Some((operation_id, range.txid()))
1012 }
1013
1014 async fn submit_with_event<F, E>(
1021 &self,
1022 operation_id: OperationId,
1023 operation_meta_gen: F,
1024 tx_builder: TransactionBuilder,
1025 event: E,
1026 ) -> Result<OutPointRange, TransactionSubmitError>
1027 where
1028 F: Fn(OutPointRange) -> WalletOperationMeta + Clone + MaybeSend + MaybeSync + 'static,
1029 E: Event + Clone + Send + MaybeSync,
1030 {
1031 self.client_ctx
1032 .module_db()
1033 .autocommit(
1034 |dbtx, _| {
1035 let operation_meta_gen = operation_meta_gen.clone();
1036 let tx_builder = tx_builder.clone();
1037 let event = event.clone();
1038 Box::pin(async move {
1039 let range = self
1040 .client_ctx
1041 .finalize_and_submit_transaction_dbtx(
1042 dbtx,
1043 operation_id,
1044 WalletCommonInit::KIND.as_str(),
1045 operation_meta_gen,
1046 tx_builder,
1047 )
1048 .await?;
1049
1050 self.client_ctx.log_event(dbtx, event).await;
1051
1052 Ok::<_, TransactionSubmitError>(range)
1053 })
1054 },
1055 Some(100),
1056 )
1057 .await
1058 .map_err(|error| match error {
1059 AutocommitError::ClosureError { error, .. } => error,
1060 AutocommitError::CommitFailed { last_error, .. } => {
1061 TransactionSubmitError::Database(last_error)
1062 }
1063 })
1064 }
1065
1066 fn spawn_output_scanner(&self, task_group: &TaskGroup, client_span: &tracing::Span) {
1067 let module = self.clone();
1068 let handle = task_group.make_handle();
1069
1070 task_group.spawn_cancellable_with_span(client_span.clone(), "output-scanner", async move {
1071 let address_index_exists = {
1078 let mut dbtx = module.db.begin_transaction().await;
1079 dbtx.find_by_prefix(&ValidAddressIndexPrefix)
1080 .await
1081 .next()
1082 .await
1083 .is_some()
1084 };
1085
1086 if !address_index_exists {
1087 let Some(index) = module.next_valid_index(0, &handle).await else {
1088 return;
1089 };
1090
1091 let mut dbtx = module.db.begin_transaction().await;
1092 dbtx.insert_new_entry(&ValidAddressIndexKey(index), &())
1093 .await;
1094 dbtx.commit_tx().await;
1095 }
1096
1097 loop {
1098 match module.check_outputs(&handle).await {
1099 Ok(skip_wait) => {
1100 if skip_wait {
1101 continue;
1102 }
1103 }
1104 Err(e) => {
1105 warn!(target: LOG_CLIENT_MODULE_WALLETV2, "Failed to fetch outputs: {e}");
1106 }
1107 }
1108
1109 sleep(fedimint_walletv2_common::sleep_duration()).await;
1110 }
1111 });
1112 }
1113
1114 async fn check_outputs(&self, handle: &TaskHandle) -> Result<bool, CheckOutputsError> {
1115 let mut dbtx = self.db.begin_transaction_nc().await;
1116
1117 let next_output_index = dbtx.get_value(&NextOutputIndexKey).await.unwrap_or(0);
1118
1119 let mut valid_indices: Vec<u64> = dbtx
1120 .find_by_prefix(&ValidAddressIndexPrefix)
1121 .await
1122 .map(|entry| entry.0.0)
1123 .collect()
1124 .await;
1125
1126 let mut address_map: BTreeMap<ScriptBuf, u64> = valid_indices
1127 .iter()
1128 .map(|&i| (self.derive_address(i).script_pubkey(), i))
1129 .collect();
1130
1131 let outputs = self
1132 .module_api
1133 .output_info_slice(next_output_index, next_output_index + SLICE_SIZE)
1134 .await?;
1135
1136 let returned_num = outputs.len();
1137 let mut matched_num: usize = 0;
1138
1139 for output in &outputs {
1140 if let Some(&address_index) = address_map.get(&output.script) {
1141 matched_num += 1;
1142
1143 if !output.spent && !self.process_unspent_output(output, address_index).await? {
1148 return Ok(false);
1149 }
1150
1151 let next_address_index = valid_indices
1152 .last()
1153 .copied()
1154 .expect("we have at least one address index");
1155
1156 if address_index == next_address_index {
1158 let Some(index) = self.next_valid_index(next_address_index + 1, handle).await
1159 else {
1160 return Ok(false);
1161 };
1162
1163 let mut dbtx = self.db.begin_transaction().await;
1164
1165 dbtx.insert_entry(&ValidAddressIndexKey(index), &()).await;
1166
1167 dbtx.commit_tx_result().await?;
1168
1169 valid_indices.push(index);
1170
1171 address_map.insert(self.derive_address(index).script_pubkey(), index);
1172 }
1173 }
1174
1175 let mut dbtx = self.db.begin_transaction().await;
1176
1177 dbtx.insert_entry(&NextOutputIndexKey, &(output.index + 1))
1178 .await;
1179
1180 dbtx.commit_tx_result().await?;
1181 }
1182
1183 debug!(
1184 target: LOG_CLIENT_MODULE_WALLETV2,
1185 next_output_index,
1186 returned_num,
1187 matched_num,
1188 valid_indices_num = valid_indices.len(),
1189 "Scanning for outputs"
1190 );
1191
1192 Ok(!outputs.is_empty())
1193 }
1194
1195 async fn process_unspent_output(
1196 &self,
1197 output: &OutputInfo,
1198 address_index: u64,
1199 ) -> Result<bool, ProcessOutputError> {
1200 debug!(
1201 target: LOG_CLIENT_MODULE_WALLETV2,
1202 output_index = output.index,
1203 value_sat = output.value.to_sat(),
1204 address_index,
1205 outpoint = ?output.outpoint,
1206 "Discovered unspent walletv2 receive output"
1207 );
1208
1209 let pending_tx_chain_len = self.module_api.pending_tx_chain().await?.len();
1212 if 3 <= pending_tx_chain_len {
1213 debug!(
1214 target: LOG_CLIENT_MODULE_WALLETV2,
1215 output_index = output.index,
1216 pending_tx_chain_len,
1217 "Delaying walletv2 receive claim because pending transaction chain is full"
1218 );
1219 return Ok(false);
1220 }
1221
1222 let receive_fee = self
1223 .module_api
1224 .receive_fee()
1225 .await?
1226 .ok_or(ProcessOutputError::NoFeerate)?;
1227
1228 if let Some((operation_id, txid)) = self
1229 .receive_output(
1230 output.index,
1231 output.value,
1232 address_index,
1233 receive_fee,
1234 output.outpoint,
1235 )
1236 .await
1237 {
1238 debug!(
1239 target: LOG_CLIENT_MODULE_WALLETV2,
1240 output_index = output.index,
1241 ?operation_id,
1242 %txid,
1243 "Waiting for walletv2 receive claim acceptance"
1244 );
1245 self.client_ctx
1246 .transaction_updates(operation_id)
1247 .await
1248 .await_tx_accepted(txid)
1249 .await
1250 .map_err(ProcessOutputError::ClaimRejected)?;
1251 debug!(
1252 target: LOG_CLIENT_MODULE_WALLETV2,
1253 output_index = output.index,
1254 ?operation_id,
1255 %txid,
1256 "Walletv2 receive claim accepted"
1257 );
1258 } else {
1259 debug!(
1260 target: LOG_CLIENT_MODULE_WALLETV2,
1261 output_index = output.index,
1262 value_sat = output.value.to_sat(),
1263 fee_sat = receive_fee.to_sat(),
1264 "Skipping walletv2 receive claim; value cannot cover the claim fees"
1265 );
1266 }
1267
1268 Ok(true)
1269 }
1270}
1271
1272#[derive(Error, Debug)]
1274#[non_exhaustive]
1275pub enum SendError {
1276 #[error("Address is from a different network than the federation")]
1278 WrongNetwork,
1279
1280 #[error("The value is too small")]
1282 DustValue,
1283
1284 #[error("The federation returned an error")]
1286 Federation(#[source] Box<FederationError>),
1287
1288 #[error("No consensus feerate is available at this time")]
1291 NoConsensusFeerateAvailable,
1292
1293 #[error("The client does not have sufficient funds to send the payment")]
1295 InsufficientFunds,
1296
1297 #[error("Unsupported address type")]
1299 UnsupportedAddress,
1300
1301 #[error("The send transaction could not be submitted")]
1304 Failed(#[source] TransactionSubmitError),
1305}
1306
1307#[derive(Error, Debug)]
1309#[non_exhaustive]
1310pub enum ReceiveError {
1311 #[error("The federation returned an error")]
1313 Federation(#[source] Box<FederationError>),
1314
1315 #[error("No consensus feerate is available at this time")]
1318 NoConsensusFeerateAvailable,
1319}
1320
1321#[derive(Debug, Error)]
1323#[non_exhaustive]
1324pub enum AwaitReceiveError {
1325 #[error("The receive operation could not be looked up")]
1327 Operation(#[from] OperationLookupError),
1328
1329 #[error("The ecash for the claimed deposit could not be issued")]
1331 Issuance(#[from] TransactionSubmitError),
1332}
1333
1334#[derive(Debug, Error)]
1336#[non_exhaustive]
1337pub enum ReceiveProgressError {
1338 #[error("No deposit of ours at this outpoint")]
1342 UnknownDeposit,
1343
1344 #[error("Address was not derived by this client")]
1346 UnknownAddress,
1347}
1348
1349#[derive(Debug, Error)]
1352enum CheckOutputsError {
1353 #[error(transparent)]
1355 Federation(#[from] FederationError),
1356
1357 #[error(transparent)]
1359 Database(#[from] DatabaseError),
1360
1361 #[error(transparent)]
1363 ProcessOutput(#[from] ProcessOutputError),
1364}
1365
1366#[derive(Debug, Error)]
1368enum ProcessOutputError {
1369 #[error(transparent)]
1372 Federation(#[from] FederationError),
1373
1374 #[error("No consensus feerate is available")]
1376 NoFeerate,
1377
1378 #[error("Claim transaction was rejected: {0}")]
1380 ClaimRejected(String),
1381}
1382
1383#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
1384pub enum WalletClientStateMachines {
1385 Send(send_sm::SendStateMachine),
1386 Receive(receive_sm::ReceiveStateMachine),
1387}
1388
1389impl State for WalletClientStateMachines {
1390 type ModuleContext = WalletClientContext;
1391
1392 fn transitions(
1393 &self,
1394 context: &Self::ModuleContext,
1395 global_context: &DynGlobalClientContext,
1396 ) -> Vec<StateTransition<Self>> {
1397 match self {
1398 WalletClientStateMachines::Send(sm) => sm_enum_variant_translation!(
1399 sm.transitions(context, global_context),
1400 WalletClientStateMachines::Send
1401 ),
1402 WalletClientStateMachines::Receive(sm) => sm_enum_variant_translation!(
1403 sm.transitions(context, global_context),
1404 WalletClientStateMachines::Receive
1405 ),
1406 }
1407 }
1408
1409 fn operation_id(&self) -> OperationId {
1410 match self {
1411 WalletClientStateMachines::Send(sm) => sm.operation_id(),
1412 WalletClientStateMachines::Receive(sm) => sm.operation_id(),
1413 }
1414 }
1415}
1416
1417impl IntoDynInstance for WalletClientStateMachines {
1418 type DynType = DynState;
1419
1420 fn into_dyn(self, instance_id: ModuleInstanceId) -> Self::DynType {
1421 DynState::from_typed(instance_id, self)
1422 }
1423}