1#![deny(clippy::pedantic)]
2#![allow(clippy::cast_possible_truncation)]
3#![allow(clippy::missing_errors_doc)]
4#![allow(clippy::missing_panics_doc)]
5#![allow(clippy::module_name_repetitions)]
6#![allow(clippy::must_use_candidate)]
7
8pub mod api;
9#[cfg(feature = "cli")]
10mod cli;
11
12mod backup;
13
14pub mod client_db;
15mod deposit;
18pub mod error;
20pub mod events;
21use events::SendPaymentEvent;
22mod pegin_monitor;
24mod withdraw;
25
26use std::collections::{BTreeMap, BTreeSet};
27use std::future;
28use std::sync::Arc;
29use std::time::{Duration, SystemTime};
30
31use async_stream::{stream, try_stream};
32use backup::WalletModuleBackup;
33use bitcoin::address::NetworkUnchecked;
34use bitcoin::secp256k1::{All, SECP256K1, Secp256k1};
35use bitcoin::{Address, Network, ScriptBuf};
36use client_db::{DbKeyPrefix, PegInTweakIndexKey, SupportsSafeDepositKey, TweakIdx};
37use fedimint_api_client::api::{DynModuleApi, FederationResult};
38use fedimint_bitcoind::{
39 BitcoinRpcError, BitcoindTracked, DynBitcoindRpc, IBitcoindRpc, create_esplora_rpc,
40};
41use fedimint_client_module::error::{ClientModuleError, TransactionSubmitError};
42use fedimint_client_module::module::init::{
43 ClientModuleInit, ClientModuleInitArgs, ClientModuleRecoverArgs, RecoveryMode,
44};
45use fedimint_client_module::module::recovery::RecoveryProgress;
46use fedimint_client_module::module::{ClientContext, ClientModule, IClientModule, OutPointRange};
47use fedimint_client_module::oplog::UpdateStreamOrOutcome;
48use fedimint_client_module::sm::{Context, DynState, ModuleNotifier, State, StateTransition};
49use fedimint_client_module::transaction::{
50 ClientOutput, ClientOutputBundle, ClientOutputSM, FeeQuote, FeeQuoteRequest,
51 TransactionBuilder, max_affordable_send_amount,
52};
53use fedimint_client_module::{DynGlobalClientContext, sm_enum_variant_translation};
54use fedimint_core::core::{Decoder, IntoDynInstance, ModuleInstanceId, ModuleKind, OperationId};
55use fedimint_core::db::{
56 Committable, Database, DatabaseError, DatabaseTransaction, IDatabaseTransactionOpsCoreTyped,
57};
58use fedimint_core::encoding::{Decodable, Encodable};
59use fedimint_core::envs::{BitcoinRpcConfig, is_running_in_test_env};
60use fedimint_core::module::{
61 Amounts, ApiAuth, ApiVersion, CommonModuleInit, ModuleCommon, ModuleConsensusVersion,
62 ModuleInit, MultiApiVersion,
63};
64use fedimint_core::task::{MaybeSend, MaybeSync, TaskGroup, sleep};
65use fedimint_core::util::backoff_util::background_backoff;
66use fedimint_core::util::{BoxStream, FmtCompact as _, backoff_util, retry};
67use fedimint_core::{
68 BitcoinHash, OutPoint, TransactionId, apply, async_trait_maybe_send, push_db_pair_items,
69 runtime, secp256k1,
70};
71use fedimint_derive_secret::{ChildId, DerivableSecret};
72use fedimint_logging::LOG_CLIENT_MODULE_WALLET;
73pub use fedimint_wallet_common as common;
74use fedimint_wallet_common::config::{FeeConsensus, WalletClientConfig};
75use fedimint_wallet_common::tweakable::Tweakable;
76pub use fedimint_wallet_common::*;
77use futures::{Stream, StreamExt, TryStreamExt as _};
78use rand::{Rng, thread_rng};
79use secp256k1::Keypair;
80use serde::{Deserialize, Serialize};
81use strum::IntoEnumIterator;
82use tokio::sync::watch;
83use tracing::{debug, instrument, warn};
84
85use crate::api::WalletFederationApi;
86use crate::backup::{FEDERATION_RECOVER_MAX_GAP, RecoveryStateV2, WalletRecovery};
87use crate::client_db::{
88 ClaimedPegInData, ClaimedPegInKey, ClaimedPegInPrefix, NextPegInTweakIndexKey,
89 PegInPoolCursorKey, PegInTweakIndexData, PegInTweakIndexPrefix, RecoveryFinalizedKey,
90 RecoveryStateKey, SupportsSafeDepositPrefix,
91};
92use crate::deposit::DepositStateMachine;
93pub use crate::error::{
94 ConsensusVersionVotingError, DepositAddressError, MaxWithdrawableAmountError, PegInError,
95 PegOutError, SubscribeDepositError, SubscribeWithdrawError, WithdrawFeesError,
96};
97use crate::withdraw::{CreatedWithdrawState, WithdrawStateMachine, WithdrawStates};
98
99const WALLET_TWEAK_CHILD_ID: ChildId = ChildId(0);
100
101#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
102pub struct BitcoinTransactionData {
103 pub btc_transaction: bitcoin::Transaction,
106 pub out_idx: u32,
108}
109
110#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
111pub enum DepositStateV1 {
112 WaitingForTransaction,
113 WaitingForConfirmation(BitcoinTransactionData),
114 Confirmed(BitcoinTransactionData),
115 Claimed(BitcoinTransactionData),
116 Failed(String),
117}
118
119#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
120pub enum DepositStateV2 {
121 WaitingForTransaction,
122 WaitingForConfirmation {
123 #[serde(with = "bitcoin::amount::serde::as_sat")]
124 btc_deposited: bitcoin::Amount,
125 btc_out_point: bitcoin::OutPoint,
126 },
127 Confirmed {
128 #[serde(with = "bitcoin::amount::serde::as_sat")]
129 btc_deposited: bitcoin::Amount,
130 btc_out_point: bitcoin::OutPoint,
131 },
132 Claimed {
133 #[serde(with = "bitcoin::amount::serde::as_sat")]
134 btc_deposited: bitcoin::Amount,
135 btc_out_point: bitcoin::OutPoint,
136 },
137 Failed(String),
138}
139
140#[derive(Debug, Clone, PartialEq, Eq)]
142pub struct DepositAddressInfo {
143 pub operation_id: OperationId,
144 pub address: Address,
145 pub tweak_idx: TweakIdx,
146}
147
148#[allow(clippy::enum_variant_names)]
154#[derive(Debug, Clone, PartialEq, Eq)]
155pub enum MaybeNewAddress {
156 NewAddress(DepositAddressInfo),
158 TooManyUnusedAddresses(Vec<DepositAddressInfo>),
162}
163
164#[derive(Debug, Clone, Copy, PartialEq, Eq)]
170pub enum AllocateDepositOutcome {
171 Fresh,
173 Reused { original_tweak_idx: TweakIdx },
177}
178
179#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
180pub enum WithdrawState {
181 Created,
182 Succeeded(bitcoin::Txid),
183 Failed(String),
184 }
188
189async fn next_withdraw_state<S>(stream: &mut S) -> Option<WithdrawStates>
190where
191 S: Stream<Item = WalletClientStates> + Unpin,
192{
193 loop {
194 if let WalletClientStates::Withdraw(ds) = stream.next().await? {
195 return Some(ds.state);
196 }
197 tokio::task::yield_now().await;
198 }
199}
200
201#[derive(Debug, Clone, Default)]
202pub struct WalletClientInit(pub Option<DynBitcoindRpc>);
204
205const SLICE_SIZE: u64 = 1000;
206
207impl WalletClientInit {
208 pub fn new(rpc: DynBitcoindRpc) -> Self {
209 Self(Some(rpc))
210 }
211
212 async fn recover_from_slices(
213 &self,
214 args: &ClientModuleRecoverArgs<Self>,
215 total_items: u64,
216 ) -> Option<fedimint_core::Amount> {
217 let data = WalletClientModuleData {
218 cfg: args.cfg().clone(),
219 module_root_secret: args.module_root_secret().clone(),
220 };
221
222 let mut state = RecoveryStateV2::new();
223
224 state.refill_pending_pool_up_to(&data, TweakIdx(FEDERATION_RECOVER_MAX_GAP));
225
226 for start in (0..total_items).step_by(SLICE_SIZE as usize) {
227 let end = std::cmp::min(start + SLICE_SIZE, total_items);
228
229 let items = args.module_api().fetch_recovery_slice(start, end).await;
230
231 for item in &items {
232 match item {
233 RecoveryItem::Input { outpoint, script } => {
234 state.handle_item(*outpoint, script, &data);
235 }
236 }
237 }
238
239 args.update_recovery_progress(RecoveryProgress {
240 complete: end.try_into().unwrap_or(u32::MAX),
241 total: total_items.try_into().unwrap_or(u32::MAX),
242 });
243 }
244
245 let entries = (0..state.new_start_idx().0)
246 .map(|tweak_idx| {
247 let operation_id = data.derive_peg_in_script(TweakIdx(tweak_idx)).3;
248
249 let claimed = state
250 .claimed_outpoints
251 .get(&TweakIdx(tweak_idx))
252 .cloned()
253 .unwrap_or_default();
254
255 (
256 TweakIdx(tweak_idx),
257 PegInTweakIndexData {
258 operation_id,
259 creation_time: fedimint_core::time::now(),
260 last_check_time: None,
261 next_check_time: Some(fedimint_core::time::now()),
262 claimed,
263 },
264 )
265 })
266 .collect();
267
268 store_recovered_peg_in_indexes(args.db(), entries, state.new_start_idx()).await;
269
270 None
274 }
275}
276
277async fn recovery_already_finalized(db: &Database) -> bool {
288 let mut dbtx = db.begin_transaction_nc().await;
289 dbtx.get_value(&RecoveryFinalizedKey).await == Some(true)
290 || dbtx.get_value(&NextPegInTweakIndexKey).await.is_some()
291}
292
293async fn store_recovered_peg_in_indexes(
302 db: &Database,
303 entries: Vec<(TweakIdx, PegInTweakIndexData)>,
304 new_start_idx: TweakIdx,
305) -> bool {
306 let mut dbtx = db.begin_transaction().await;
307
308 if dbtx.get_value(&RecoveryFinalizedKey).await == Some(true)
309 || dbtx.get_value(&NextPegInTweakIndexKey).await.is_some()
310 {
311 return false;
312 }
313
314 for (tweak_idx, entry) in entries {
315 dbtx.insert_new_entry(&PegInTweakIndexKey(tweak_idx), &entry)
316 .await;
317 }
318
319 dbtx.insert_new_entry(&NextPegInTweakIndexKey, &new_start_idx)
320 .await;
321 dbtx.insert_entry(&RecoveryFinalizedKey, &true).await;
322
323 dbtx.commit_tx().await;
324
325 true
326}
327
328impl ModuleInit for WalletClientInit {
329 type Common = WalletCommonInit;
330
331 async fn dump_database(
332 &self,
333 dbtx: &mut DatabaseTransaction<'_>,
334 prefix_names: Vec<String>,
335 ) -> Box<dyn Iterator<Item = (String, Box<dyn erased_serde::Serialize + Send>)> + '_> {
336 let mut wallet_client_items: BTreeMap<String, Box<dyn erased_serde::Serialize + Send>> =
337 BTreeMap::new();
338 let filtered_prefixes = DbKeyPrefix::iter().filter(|f| {
339 prefix_names.is_empty() || prefix_names.contains(&f.to_string().to_lowercase())
340 });
341
342 for table in filtered_prefixes {
343 match table {
344 DbKeyPrefix::NextPegInTweakIndex => {
345 if let Some(index) = dbtx.get_value(&NextPegInTweakIndexKey).await {
346 wallet_client_items
347 .insert("NextPegInTweakIndex".to_string(), Box::new(index));
348 }
349 }
350 DbKeyPrefix::PegInTweakIndex => {
351 push_db_pair_items!(
352 dbtx,
353 PegInTweakIndexPrefix,
354 PegInTweakIndexKey,
355 PegInTweakIndexData,
356 wallet_client_items,
357 "Peg-In Tweak Index"
358 );
359 }
360 DbKeyPrefix::ClaimedPegIn => {
361 push_db_pair_items!(
362 dbtx,
363 ClaimedPegInPrefix,
364 ClaimedPegInKey,
365 ClaimedPegInData,
366 wallet_client_items,
367 "Claimed Peg-In"
368 );
369 }
370 DbKeyPrefix::RecoveryFinalized => {
371 if let Some(val) = dbtx.get_value(&RecoveryFinalizedKey).await {
372 wallet_client_items.insert("RecoveryFinalized".to_string(), Box::new(val));
373 }
374 }
375 DbKeyPrefix::SupportsSafeDeposit => {
376 push_db_pair_items!(
377 dbtx,
378 SupportsSafeDepositPrefix,
379 SupportsSafeDepositKey,
380 (),
381 wallet_client_items,
382 "Supports Safe Deposit"
383 );
384 }
385 DbKeyPrefix::PegInPoolCursor => {
386 if let Some(cursor) = dbtx.get_value(&PegInPoolCursorKey).await {
387 wallet_client_items.insert("PegInPoolCursor".to_string(), Box::new(cursor));
388 }
389 }
390 DbKeyPrefix::RecoveryState
391 | DbKeyPrefix::ExternalReservedStart
392 | DbKeyPrefix::CoreInternalReservedStart
393 | DbKeyPrefix::CoreInternalReservedEnd => {}
394 }
395 }
396
397 Box::new(wallet_client_items.into_iter())
398 }
399}
400
401#[apply(async_trait_maybe_send!)]
402impl ClientModuleInit for WalletClientInit {
403 type Module = WalletClientModule;
404
405 fn supported_api_versions(&self) -> MultiApiVersion {
406 MultiApiVersion::try_from_iter([ApiVersion { major: 0, minor: 0 }])
407 .expect("no version conflicts")
408 }
409
410 async fn init(
411 &self,
412 args: &ClientModuleInitArgs<Self>,
413 ) -> Result<Self::Module, ClientModuleError> {
414 let data = WalletClientModuleData {
415 cfg: args.cfg().clone(),
416 module_root_secret: args.module_root_secret().clone(),
417 };
418
419 let db = args.db().clone();
420
421 let rpc_config = WalletClientModule::get_rpc_config(args.cfg());
422
423 let btc_rpc = if let Some(user_rpc) = args.user_bitcoind_rpc() {
430 user_rpc.clone()
431 } else if let Some(factory) = args.user_bitcoind_rpc_no_chain_id() {
432 if let Some(rpc) = factory(rpc_config.url.clone()).await {
433 rpc
434 } else {
435 self.0.clone().unwrap_or(
436 create_esplora_rpc(&rpc_config.url).map_err(ClientModuleError::other)?,
437 )
438 }
439 } else {
440 self.0
441 .clone()
442 .unwrap_or(create_esplora_rpc(&rpc_config.url).map_err(ClientModuleError::other)?)
443 };
444 let btc_rpc = BitcoindTracked::new(btc_rpc, "wallet-client").into_dyn();
445
446 let module_api = args.module_api().clone();
447
448 let (pegin_claimed_sender, pegin_claimed_receiver) = watch::channel(());
449 let (pegin_monitor_wakeup_sender, pegin_monitor_wakeup_receiver) = watch::channel(());
450
451 Ok(WalletClientModule {
452 db,
453 data,
454 module_api,
455 notifier: args.notifier().clone(),
456 rpc: btc_rpc,
457 client_ctx: args.context(),
458 pegin_monitor_wakeup_sender,
459 pegin_monitor_wakeup_receiver,
460 pegin_claimed_receiver,
461 pegin_claimed_sender,
462 task_group: args.task_group().clone(),
463 client_span: args.client_span().clone(),
464 admin_auth: args.admin_auth().cloned(),
465 })
466 }
467
468 fn recovery_mode(&self) -> RecoveryMode {
469 RecoveryMode::Unusable
470 }
471
472 async fn recover(
477 &self,
478 args: &ClientModuleRecoverArgs<Self>,
479 snapshot: Option<&<Self::Module as ClientModule>::Backup>,
480 ) -> Result<Option<fedimint_core::Amount>, ClientModuleError> {
481 if recovery_already_finalized(args.db()).await {
486 debug!(
487 target: LOG_CLIENT_MODULE_WALLET,
488 "Wallet recovery already finalized by an earlier run, nothing to redo"
489 );
490 return Ok(None);
491 }
492
493 if args
496 .db()
497 .begin_transaction_nc()
498 .await
499 .get_value(&RecoveryStateKey)
500 .await
501 .is_some()
502 {
503 return args
504 .recover_from_history::<WalletRecovery>(self, snapshot)
505 .await;
506 }
507
508 match args.module_api().fetch_recovery_count().await {
512 Ok(total_items) => Ok(self.recover_from_slices(args, total_items).await),
513 Err(_) => {
514 args.recover_from_history::<WalletRecovery>(self, snapshot)
515 .await
516 }
517 }
518 }
519
520 fn used_db_prefixes(&self) -> Option<BTreeSet<u8>> {
521 Some(
522 DbKeyPrefix::iter()
523 .map(|p| p as u8)
524 .chain(
525 DbKeyPrefix::ExternalReservedStart as u8
526 ..=DbKeyPrefix::CoreInternalReservedEnd as u8,
527 )
528 .collect(),
529 )
530 }
531}
532
533#[derive(Debug, Clone, Serialize, Deserialize)]
534pub struct WalletOperationMeta {
535 pub variant: WalletOperationMetaVariant,
536 pub extra_meta: serde_json::Value,
537}
538
539#[derive(Debug, Clone, Serialize, Deserialize)]
540#[serde(rename_all = "snake_case")]
541pub enum WalletOperationMetaVariant {
542 Deposit {
543 address: Address<NetworkUnchecked>,
544 #[serde(default)]
549 tweak_idx: Option<TweakIdx>,
550 #[serde(default, skip_serializing_if = "Option::is_none")]
551 expires_at: Option<SystemTime>,
552 },
553 Withdraw {
554 address: Address<NetworkUnchecked>,
555 #[serde(with = "bitcoin::amount::serde::as_sat")]
556 amount: bitcoin::Amount,
557 fee: PegOutFees,
558 change: Vec<OutPoint>,
559 },
560
561 RbfWithdraw {
562 rbf: Rbf,
563 change: Vec<OutPoint>,
564 },
565}
566
567#[derive(Debug, Clone)]
569pub struct WalletClientModuleData {
570 cfg: WalletClientConfig,
571 module_root_secret: DerivableSecret,
572}
573
574impl WalletClientModuleData {
575 fn derive_deposit_address(
576 &self,
577 idx: TweakIdx,
578 ) -> (Keypair, secp256k1::PublicKey, Address, OperationId) {
579 let idx = ChildId(idx.0);
580
581 let secret_tweak_key = self
582 .module_root_secret
583 .child_key(WALLET_TWEAK_CHILD_ID)
584 .child_key(idx)
585 .to_secp_key(fedimint_core::secp256k1::SECP256K1);
586
587 let public_tweak_key = secret_tweak_key.public_key();
588
589 let address = self
590 .cfg
591 .peg_in_descriptor
592 .tweak(&public_tweak_key, bitcoin::secp256k1::SECP256K1)
593 .address(self.cfg.network.0)
594 .unwrap();
595
596 let operation_id = OperationId(public_tweak_key.x_only_public_key().0.serialize());
598
599 (secret_tweak_key, public_tweak_key, address, operation_id)
600 }
601
602 fn derive_peg_in_script(
603 &self,
604 idx: TweakIdx,
605 ) -> (ScriptBuf, bitcoin::Address, Keypair, OperationId) {
606 let (secret_tweak_key, _, address, operation_id) = self.derive_deposit_address(idx);
607
608 (
609 self.cfg
610 .peg_in_descriptor
611 .tweak(&secret_tweak_key.public_key(), SECP256K1)
612 .script_pubkey(),
613 address,
614 secret_tweak_key,
615 operation_id,
616 )
617 }
618}
619
620#[derive(Debug)]
621pub struct WalletClientModule {
622 data: WalletClientModuleData,
623 db: Database,
624 module_api: DynModuleApi,
625 notifier: ModuleNotifier<WalletClientStates>,
626 rpc: DynBitcoindRpc,
627 client_ctx: ClientContext<Self>,
628 pegin_monitor_wakeup_sender: watch::Sender<()>,
630 pegin_monitor_wakeup_receiver: watch::Receiver<()>,
631 pegin_claimed_sender: watch::Sender<()>,
633 pegin_claimed_receiver: watch::Receiver<()>,
634 task_group: TaskGroup,
635 client_span: tracing::Span,
636 admin_auth: Option<ApiAuth>,
637}
638
639#[apply(async_trait_maybe_send!)]
640impl ClientModule for WalletClientModule {
641 type Init = WalletClientInit;
642 type Common = WalletModuleTypes;
643 type Backup = WalletModuleBackup;
644 type ModuleStateMachineContext = WalletClientContext;
645 type States = WalletClientStates;
646
647 fn context(&self) -> Self::ModuleStateMachineContext {
648 WalletClientContext {
649 rpc: self.rpc.clone(),
650 wallet_descriptor: self.cfg().peg_in_descriptor.clone(),
651 wallet_decoder: self.decoder(),
652 secp: Secp256k1::default(),
653 client_ctx: self.client_ctx.clone(),
654 }
655 }
656
657 async fn start(&self) {
658 self.task_group
659 .spawn_cancellable_with_span(self.client_span.clone(), "peg-in monitor", {
660 let client_ctx = self.client_ctx.clone();
661 let db = self.db.clone();
662 let btc_rpc = self.rpc.clone();
663 let module_api = self.module_api.clone();
664 let data = self.data.clone();
665 let pegin_claimed_sender = self.pegin_claimed_sender.clone();
666 let pegin_monitor_wakeup_receiver = self.pegin_monitor_wakeup_receiver.clone();
667 pegin_monitor::run_peg_in_monitor(
668 client_ctx,
669 db,
670 btc_rpc,
671 module_api,
672 data,
673 pegin_claimed_sender,
674 pegin_monitor_wakeup_receiver,
675 )
676 });
677
678 self.task_group.spawn_cancellable_with_span(
679 self.client_span.clone(),
680 "supports-safe-deposit-version",
681 {
682 let db = self.db.clone();
683 let module_api = self.module_api.clone();
684
685 poll_supports_safe_deposit_version(db, module_api)
686 },
687 );
688 }
689
690 fn supports_backup(&self) -> bool {
691 true
692 }
693
694 async fn backup(&self) -> Result<backup::WalletModuleBackup, ClientModuleError> {
695 let session_count = self
697 .client_ctx
698 .global_api()
699 .session_count()
700 .await
701 .map_err(ClientModuleError::other)?;
702
703 let mut dbtx = self.db.begin_transaction_nc().await;
704 let next_pegin_tweak_idx = dbtx
705 .get_value(&NextPegInTweakIndexKey)
706 .await
707 .unwrap_or_default();
708 let claimed = dbtx
709 .find_by_prefix(&PegInTweakIndexPrefix)
710 .await
711 .filter_map(|(k, v)| async move {
712 if v.claimed.is_empty() {
713 None
714 } else {
715 Some(k.0)
716 }
717 })
718 .collect()
719 .await;
720 Ok(backup::WalletModuleBackup::new_v1(
721 session_count,
722 next_pegin_tweak_idx,
723 claimed,
724 ))
725 }
726
727 fn input_fee(
728 &self,
729 _amount: &Amounts,
730 _input: &<Self::Common as ModuleCommon>::Input,
731 ) -> Option<Amounts> {
732 Some(Amounts::new_bitcoin(self.cfg().fee_consensus.peg_in_abs))
733 }
734
735 fn output_fee(
736 &self,
737 _amount: &Amounts,
738 _output: &<Self::Common as ModuleCommon>::Output,
739 ) -> Option<Amounts> {
740 Some(Amounts::new_bitcoin(self.cfg().fee_consensus.peg_out_abs))
741 }
742
743 async fn handle_rpc(
744 &self,
745 method: String,
746 request: serde_json::Value,
747 ) -> BoxStream<'_, Result<serde_json::Value, ClientModuleError>> {
748 let stream: BoxStream<'_, Result<serde_json::Value, RpcError>> = Box::pin(try_stream! {
749 match method.as_str() {
750 "get_wallet_summary" => {
751 let _req: WalletSummaryRequest = serde_json::from_value(request)?;
752 let wallet_summary = self.get_wallet_summary()
753 .await
754 .expect("Failed to fetch wallet summary");
755 let result = serde_json::to_value(&wallet_summary)
756 .expect("Serialization error");
757 yield result;
758 }
759 "get_block_count_local" => {
760 let block_count = self.get_block_count_local().await
761 .expect("Failed to fetch block count");
762 yield serde_json::to_value(block_count)?;
763 }
764 "peg_in" => {
765 let req: PegInRequest = serde_json::from_value(request)?;
766 let response = self.peg_in(req)
767 .await
768 .map_err(RpcError::PegIn)?;
769 let result = serde_json::to_value(&response)?;
770 yield result;
771 },
772 "peg_out" => {
773 let req: PegOutRequest = serde_json::from_value(request)?;
774 let response = self.peg_out(req)
775 .await
776 .map_err(RpcError::PegOut)?;
777 let result = serde_json::to_value(&response)?;
778 yield result;
779 },
780 "subscribe_deposit" => {
781 let req: SubscribeDepositRequest = serde_json::from_value(request)?;
782 for await state in self.subscribe_deposit(req.operation_id).await?.into_stream() {
783 yield serde_json::to_value(state)?;
784 }
785 },
786 "subscribe_withdraw" => {
787 let req: SubscribeWithdrawRequest = serde_json::from_value(request)?;
788 for await state in self.subscribe_withdraw_updates(req.operation_id).await?.into_stream(){
789 yield serde_json::to_value(state)?;
790 }
791 }
792 _ => {
793 Err(RpcError::UnknownMethod { method: method.clone() })?;
794 }
795 }
796 });
797 Box::pin(stream.map_err(ClientModuleError::other))
798 }
799
800 #[cfg(feature = "cli")]
801 async fn handle_cli_command(
802 &self,
803 args: &[std::ffi::OsString],
804 ) -> Result<serde_json::Value, ClientModuleError> {
805 cli::handle_cli_command(self, args)
806 .await
807 .map_err(ClientModuleError::other)
808 }
809}
810
811#[derive(Debug, thiserror::Error)]
813enum RpcError {
814 #[error(transparent)]
817 Json(#[from] serde_json::Error),
818
819 #[error("peg_in failed")]
821 PegIn(#[source] DepositAddressError),
822
823 #[error("peg_out failed")]
825 PegOut(#[source] PegOutError),
826
827 #[error(transparent)]
829 SubscribeDeposit(#[from] SubscribeDepositError),
830
831 #[error(transparent)]
833 SubscribeWithdraw(#[from] SubscribeWithdrawError),
834
835 #[error("Unknown method: {method}")]
837 UnknownMethod { method: String },
838}
839
840#[derive(Deserialize)]
841struct WalletSummaryRequest {}
842
843#[derive(Debug, Clone)]
844pub struct WalletClientContext {
845 rpc: DynBitcoindRpc,
846 wallet_descriptor: PegInDescriptor,
847 wallet_decoder: Decoder,
848 secp: Secp256k1<All>,
849 pub client_ctx: ClientContext<WalletClientModule>,
850}
851
852#[derive(Debug, Clone, Serialize, Deserialize)]
853pub struct PegInRequest {
854 pub extra_meta: serde_json::Value,
855}
856
857#[derive(Deserialize)]
858struct SubscribeDepositRequest {
859 operation_id: OperationId,
860}
861
862#[derive(Deserialize)]
863struct SubscribeWithdrawRequest {
864 operation_id: OperationId,
865}
866
867#[derive(Debug, Clone, Serialize, Deserialize)]
868pub struct PegInResponse {
869 pub deposit_address: Address<NetworkUnchecked>,
870 pub operation_id: OperationId,
871}
872
873#[derive(Debug, Clone, Serialize, Deserialize)]
874pub struct PegOutRequest {
875 pub amount_sat: u64,
876 pub destination_address: Address<NetworkUnchecked>,
877 pub extra_meta: serde_json::Value,
878}
879
880#[derive(Debug, Clone, Serialize, Deserialize)]
881pub struct PegOutResponse {
882 pub operation_id: OperationId,
883}
884
885impl Context for WalletClientContext {
886 const KIND: Option<ModuleKind> = Some(KIND);
887}
888
889impl WalletClientModule {
890 fn cfg(&self) -> &WalletClientConfig {
891 &self.data.cfg
892 }
893
894 fn get_rpc_config(cfg: &WalletClientConfig) -> BitcoinRpcConfig {
895 match BitcoinRpcConfig::get_defaults_from_env_vars() {
896 Ok(rpc_config) => {
897 if rpc_config.kind == "bitcoind" {
900 cfg.default_bitcoin_rpc.clone()
901 } else {
902 rpc_config
903 }
904 }
905 _ => cfg.default_bitcoin_rpc.clone(),
906 }
907 }
908
909 pub fn get_network(&self) -> Network {
910 self.cfg().network.0
911 }
912
913 pub fn get_finality_delay(&self) -> u32 {
914 self.cfg().finality_delay
915 }
916
917 pub fn get_fee_consensus(&self) -> FeeConsensus {
918 self.cfg().fee_consensus
919 }
920
921 async fn allocate_deposit_address_inner(
922 &self,
923 dbtx: &mut DatabaseTransaction<'_>,
924 ) -> DepositAddressInfo {
925 dbtx.ensure_isolated().expect("Must be isolated db");
926
927 let tweak_idx = get_next_peg_in_tweak_child_id(dbtx).await;
928 let (_secret_tweak_key, _, address, operation_id) =
929 self.data.derive_deposit_address(tweak_idx);
930
931 let now = fedimint_core::time::now();
932
933 dbtx.insert_new_entry(
934 &PegInTweakIndexKey(tweak_idx),
935 &PegInTweakIndexData {
936 creation_time: now,
937 next_check_time: Some(now),
938 last_check_time: None,
939 operation_id,
940 claimed: vec![],
941 },
942 )
943 .await;
944
945 DepositAddressInfo {
946 operation_id,
947 address,
948 tweak_idx,
949 }
950 }
951
952 pub async fn get_withdraw_fees(
959 &self,
960 address: &bitcoin::Address,
961 amount: bitcoin::Amount,
962 ) -> Result<PegOutFees, WithdrawFeesError> {
963 self.module_api
964 .fetch_peg_out_fees(address, amount)
965 .await?
966 .ok_or(WithdrawFeesError::NoQuote)
967 }
968
969 pub async fn send_fee_quote(
984 &self,
985 amount: bitcoin::Amount,
986 ) -> Result<FeeQuote, TransactionSubmitError> {
987 let amount = fedimint_core::Amount::from_sats(amount.to_sat());
988 self.client_ctx
989 .fee_quote(
990 OperationId::new_random(),
991 FeeQuoteRequest {
992 input_amount: Amounts::ZERO,
993 output_amount: Amounts::new_bitcoin(amount),
994 input_fee: Amounts::ZERO,
995 output_fee: Amounts::new_bitcoin(self.cfg().fee_consensus.peg_out_abs),
996 },
997 )
998 .await
999 }
1000
1001 pub async fn max_withdrawable_amount(
1036 &self,
1037 address: &bitcoin::Address,
1038 balance: fedimint_core::Amount,
1039 ) -> Result<(bitcoin::Amount, PegOutFees), MaxWithdrawableAmountError> {
1040 let max_fees = self
1043 .get_withdraw_fees(
1044 address,
1045 bitcoin::Amount::from_sat(balance.sats_round_down()),
1046 )
1047 .await?;
1048 let max_fee_msats = fedimint_core::Amount::from_sats(max_fees.amount().to_sat());
1049 let dust_limit = address.script_pubkey().minimal_non_dust();
1050
1051 let max = max_affordable_send_amount(
1052 balance,
1053 fedimint_core::Amount::from_sats(dust_limit.to_sat()),
1054 balance,
1055 |amount: fedimint_core::Amount| {
1059 fedimint_core::Amount::from_sats(amount.msats.div_ceil(1000)) + max_fee_msats
1060 },
1061 |funded: fedimint_core::Amount| {
1062 self.send_fee_quote(bitcoin::Amount::from_sat(funded.msats / 1000))
1063 },
1064 )
1065 .await
1066 .map_err(MaxWithdrawableAmountError::Quote)?
1067 .ok_or(MaxWithdrawableAmountError::BalanceTooLow {
1068 balance,
1069 dust_limit,
1070 })?;
1071
1072 let amount = bitcoin::Amount::from_sat(max.msats.div_ceil(1000));
1075
1076 let fees = self.get_withdraw_fees(address, amount).await?;
1081
1082 Ok((amount, fees))
1083 }
1084
1085 pub async fn get_wallet_summary(&self) -> FederationResult<WalletSummary> {
1087 self.module_api.fetch_wallet_summary().await
1088 }
1089
1090 pub async fn get_block_count_local(&self) -> FederationResult<u32> {
1091 self.module_api.fetch_block_count_local().await
1092 }
1093
1094 pub fn create_withdraw_output(
1095 &self,
1096 operation_id: OperationId,
1097 address: bitcoin::Address,
1098 amount: bitcoin::Amount,
1099 fees: PegOutFees,
1100 ) -> ClientOutputBundle<WalletOutput, WalletClientStates> {
1101 let output = WalletOutput::new_v0_peg_out(address, amount, fees);
1102
1103 let amount = output.maybe_v0_ref().expect("v0 output").amount().into();
1104
1105 let sm_gen = move |out_point_range: OutPointRange| {
1106 assert_eq!(out_point_range.count(), 1);
1107 let out_idx = out_point_range.start_idx();
1108 vec![WalletClientStates::Withdraw(WithdrawStateMachine {
1109 operation_id,
1110 state: WithdrawStates::Created(CreatedWithdrawState {
1111 fm_outpoint: OutPoint {
1112 txid: out_point_range.txid(),
1113 out_idx,
1114 },
1115 }),
1116 })]
1117 };
1118
1119 ClientOutputBundle::new(
1120 vec![ClientOutput::<WalletOutput> {
1121 output,
1122 amounts: Amounts::new_bitcoin(amount),
1123 }],
1124 vec![ClientOutputSM::<WalletClientStates> {
1125 state_machines: Arc::new(sm_gen),
1126 }],
1127 )
1128 }
1129
1130 pub async fn peg_in(&self, req: PegInRequest) -> Result<PegInResponse, DepositAddressError> {
1131 let deposit_address = self.safe_allocate_deposit_address(req.extra_meta).await?;
1132
1133 Ok(PegInResponse {
1134 deposit_address: deposit_address.address.into_unchecked(),
1135 operation_id: deposit_address.operation_id,
1136 })
1137 }
1138
1139 pub async fn peg_out(&self, req: PegOutRequest) -> Result<PegOutResponse, PegOutError> {
1140 let amount = bitcoin::Amount::from_sat(req.amount_sat);
1141 let network = self.get_network();
1142 let destination = req
1143 .destination_address
1144 .require_network(network)
1145 .map_err(|_| PegOutError::WrongNetwork { expected: network })?;
1146
1147 let fees = self.get_withdraw_fees(&destination, amount).await?;
1148 let operation_id = self
1149 .withdraw(&destination, amount, fees, req.extra_meta)
1150 .await?;
1151
1152 Ok(PegOutResponse { operation_id })
1153 }
1154
1155 pub fn create_rbf_withdraw_output(
1156 &self,
1157 operation_id: OperationId,
1158 rbf: &Rbf,
1159 ) -> ClientOutputBundle<WalletOutput, WalletClientStates> {
1160 let output = WalletOutput::new_v0_rbf(rbf.fees, rbf.txid);
1161
1162 let amount = output.maybe_v0_ref().expect("v0 output").amount().into();
1163
1164 let sm_gen = move |out_point_range: OutPointRange| {
1165 assert_eq!(out_point_range.count(), 1);
1166 let out_idx = out_point_range.start_idx();
1167 vec![WalletClientStates::Withdraw(WithdrawStateMachine {
1168 operation_id,
1169 state: WithdrawStates::Created(CreatedWithdrawState {
1170 fm_outpoint: OutPoint {
1171 txid: out_point_range.txid(),
1172 out_idx,
1173 },
1174 }),
1175 })]
1176 };
1177
1178 ClientOutputBundle::new(
1179 vec![ClientOutput::<WalletOutput> {
1180 output,
1181 amounts: Amounts::new_bitcoin(amount),
1182 }],
1183 vec![ClientOutputSM::<WalletClientStates> {
1184 state_machines: Arc::new(sm_gen),
1185 }],
1186 )
1187 }
1188
1189 pub async fn btc_tx_has_no_size_limit(&self) -> FederationResult<bool> {
1190 Ok(self.module_api.module_consensus_version().await? >= ModuleConsensusVersion::new(2, 2))
1191 }
1192
1193 pub async fn supports_safe_deposit(&self) -> bool {
1202 if supports_safe_deposit_verified(&self.db).await {
1203 return true;
1204 }
1205
1206 verify_supports_safe_deposit(&self.db, &self.module_api)
1207 .await
1208 .unwrap_or(false)
1209 }
1210
1211 pub async fn safe_allocate_deposit_address<M>(
1219 &self,
1220 extra_meta: M,
1221 ) -> Result<DepositAddressInfo, DepositAddressError>
1222 where
1223 M: Serialize + MaybeSend + MaybeSync,
1224 {
1225 if !self.supports_safe_deposit().await {
1226 return Err(DepositAddressError::SafeDepositUnverified);
1227 }
1228
1229 self.allocate_deposit_address_expert_only(extra_meta).await
1230 }
1231
1232 pub async fn allocate_deposit_address_expert_only<M>(
1250 &self,
1251 extra_meta: M,
1252 ) -> Result<DepositAddressInfo, DepositAddressError>
1253 where
1254 M: Serialize + MaybeSend + MaybeSync,
1255 {
1256 let extra_meta_value =
1257 serde_json::to_value(extra_meta).expect("Failed to serialize extra meta");
1258 let deposit_address = self
1259 .db
1260 .autocommit(
1261 move |dbtx, _| {
1262 let extra_meta_value_inner = extra_meta_value.clone();
1263 Box::pin(async move {
1264 let deposit_address = self.allocate_deposit_address_inner(dbtx).await;
1265
1266 self.client_ctx
1267 .manual_operation_start_dbtx(
1268 dbtx,
1269 deposit_address.operation_id,
1270 WalletCommonInit::KIND.as_str(),
1271 WalletOperationMeta {
1272 variant: WalletOperationMetaVariant::Deposit {
1273 address: deposit_address.address.clone().into_unchecked(),
1274 tweak_idx: Some(deposit_address.tweak_idx),
1275 expires_at: None,
1276 },
1277 extra_meta: extra_meta_value_inner,
1278 },
1279 vec![],
1280 )
1281 .await?;
1282
1283 debug!(
1284 target: LOG_CLIENT_MODULE_WALLET,
1285 tweak_idx = %deposit_address.tweak_idx,
1286 address = %deposit_address.address,
1287 "Derived a new deposit address"
1288 );
1289
1290 self.rpc
1292 .watch_script_history(&deposit_address.address.script_pubkey())
1293 .await?;
1294
1295 let sender = self.pegin_monitor_wakeup_sender.clone();
1296 dbtx.on_commit(move || {
1297 sender.send_replace(());
1298 });
1299
1300 Ok(deposit_address)
1301 })
1302 },
1303 Some(100),
1304 )
1305 .await?;
1306
1307 Ok(deposit_address)
1308 }
1309
1310 pub async fn allocate_deposit_address_pooled_stateless(
1348 &self,
1349 max_gap_size: usize,
1350 ) -> Result<MaybeNewAddress, DepositAddressError> {
1351 let max_gap_size_u64 = u64::try_from(max_gap_size).unwrap_or(u64::MAX);
1352 let extra_meta_value = serde_json::Value::Null;
1353 let result = self
1354 .db
1355 .autocommit(
1356 move |dbtx, _| {
1357 let extra_meta_value_inner = extra_meta_value.clone();
1358 Box::pin(async move {
1359 let unused = self.unused_pooled_deposit_addresses(dbtx).await;
1360
1361 if max_gap_size_u64 <= unused.len() as u64 && !unused.is_empty() {
1362 let addresses = unused
1363 .into_iter()
1364 .map(|(tweak_idx, data)| {
1365 let (_script, address, _key, operation_id) =
1366 self.data.derive_peg_in_script(tweak_idx);
1367
1368 debug_assert_eq!(operation_id, data.operation_id);
1369
1370 DepositAddressInfo {
1371 operation_id,
1372 address,
1373 tweak_idx,
1374 }
1375 })
1376 .collect();
1377
1378 return Ok::<_, DepositAddressError>(
1379 MaybeNewAddress::TooManyUnusedAddresses(addresses),
1380 );
1381 }
1382
1383 let deposit_address = self.allocate_deposit_address_inner(dbtx).await;
1384
1385 self.client_ctx
1386 .manual_operation_start_dbtx(
1387 dbtx,
1388 deposit_address.operation_id,
1389 WalletCommonInit::KIND.as_str(),
1390 WalletOperationMeta {
1391 variant: WalletOperationMetaVariant::Deposit {
1392 address: deposit_address.address.clone().into_unchecked(),
1393 tweak_idx: Some(deposit_address.tweak_idx),
1394 expires_at: None,
1395 },
1396 extra_meta: extra_meta_value_inner,
1397 },
1398 vec![],
1399 )
1400 .await?;
1401
1402 debug!(
1403 target: LOG_CLIENT_MODULE_WALLET,
1404 tweak_idx = %deposit_address.tweak_idx,
1405 address = %deposit_address.address,
1406 "Derived a new pooled deposit address"
1407 );
1408
1409 self.rpc
1410 .watch_script_history(&deposit_address.address.script_pubkey())
1411 .await?;
1412
1413 let sender = self.pegin_monitor_wakeup_sender.clone();
1414 dbtx.on_commit(move || {
1415 sender.send_replace(());
1416 });
1417
1418 Ok(MaybeNewAddress::NewAddress(deposit_address))
1419 })
1420 },
1421 Some(100),
1422 )
1423 .await?;
1424
1425 Ok(result)
1426 }
1427
1428 async fn unused_pooled_deposit_addresses(
1429 &self,
1430 dbtx: &mut DatabaseTransaction<'_>,
1431 ) -> Vec<(TweakIdx, PegInTweakIndexData)> {
1432 let mut unused: Vec<(TweakIdx, PegInTweakIndexData)> = dbtx
1437 .find_by_prefix_sorted_descending(&PegInTweakIndexPrefix)
1438 .await
1439 .take_while(|(_, d)| std::future::ready(d.claimed.is_empty()))
1440 .map(|(k, v)| (k.0, v))
1441 .collect()
1442 .await;
1443
1444 unused.sort_by_key(|(t, d)| (d.creation_time, *t));
1447 unused
1448 }
1449
1450 #[allow(clippy::too_many_lines)]
1473 pub async fn allocate_deposit_address_pooled(
1474 &self,
1475 max_gap_size: usize,
1476 ) -> Result<(DepositAddressInfo, AllocateDepositOutcome), DepositAddressError> {
1477 let stateless = self
1478 .allocate_deposit_address_pooled_stateless(max_gap_size)
1479 .await?;
1480
1481 let reused_addresses = match stateless {
1482 MaybeNewAddress::NewAddress(deposit_address) => {
1483 return Ok((deposit_address, AllocateDepositOutcome::Fresh));
1484 }
1485 MaybeNewAddress::TooManyUnusedAddresses(addresses) => addresses,
1486 };
1487
1488 let result = self
1489 .db
1490 .autocommit(
1491 move |dbtx, _| {
1492 let reused_addresses = reused_addresses.clone();
1493 Box::pin(async move {
1494 let cursor = dbtx
1495 .get_value(&PegInPoolCursorKey)
1496 .await
1497 .unwrap_or(TweakIdx(0));
1498
1499 let pick_pos = reused_addresses
1500 .iter()
1501 .position(|a| cursor <= a.tweak_idx)
1502 .unwrap_or(0);
1503 let reused_address = reused_addresses[pick_pos].clone();
1504
1505 let existing_tweak_idx = reused_address.tweak_idx;
1506 let existing = dbtx
1507 .get_value(&PegInTweakIndexKey(reused_address.tweak_idx))
1508 .await
1509 .ok_or(DepositAddressError::PooledAddressDisappeared {
1510 tweak_idx: reused_address.tweak_idx,
1511 })?;
1512
1513 if !existing.claimed.is_empty() {
1514 return Err(DepositAddressError::PooledAddressUsed {
1515 tweak_idx: reused_address.tweak_idx,
1516 });
1517 }
1518
1519 dbtx.insert_entry(&PegInPoolCursorKey, &reused_address.tweak_idx.next())
1520 .await;
1521
1522 let now = fedimint_core::time::now();
1530 dbtx.insert_entry(
1531 &PegInTweakIndexKey(reused_address.tweak_idx),
1532 &PegInTweakIndexData {
1533 creation_time: now,
1534 last_check_time: None,
1535 next_check_time: Some(now),
1536 operation_id: existing.operation_id,
1537 claimed: existing.claimed,
1538 },
1539 )
1540 .await;
1541
1542 let sender = self.pegin_monitor_wakeup_sender.clone();
1543 dbtx.on_commit(move || {
1544 sender.send_replace(());
1545 });
1546
1547 Ok::<_, DepositAddressError>((
1548 reused_address,
1549 AllocateDepositOutcome::Reused {
1550 original_tweak_idx: existing_tweak_idx,
1551 },
1552 ))
1553 })
1554 },
1555 Some(100),
1556 )
1557 .await?;
1558
1559 Ok(result)
1560 }
1561
1562 pub async fn subscribe_deposit(
1568 &self,
1569 operation_id: OperationId,
1570 ) -> Result<UpdateStreamOrOutcome<DepositStateV2>, SubscribeDepositError> {
1571 let operation = self.client_ctx.get_operation(operation_id).await?;
1572
1573 let operation_meta = operation.meta::<WalletOperationMeta>();
1574
1575 let WalletOperationMetaVariant::Deposit {
1576 address, tweak_idx, ..
1577 } = operation_meta.variant
1578 else {
1579 return Err(SubscribeDepositError::NotADeposit);
1580 };
1581
1582 let network = self.cfg().network.0;
1583 let address = address
1584 .require_network(network)
1585 .map_err(|_| SubscribeDepositError::WrongNetwork { expected: network })?;
1586
1587 let Some(tweak_idx) = tweak_idx else {
1589 let outcome_v1 = operation
1593 .outcome::<DepositStateV1>()
1594 .ok_or(SubscribeDepositError::OldPendingDeposit)?;
1595
1596 let outcome_v2 = match outcome_v1 {
1597 DepositStateV1::Claimed(tx_info) => DepositStateV2::Claimed {
1598 btc_deposited: tx_info.btc_transaction.output[tx_info.out_idx as usize].value,
1599 btc_out_point: bitcoin::OutPoint {
1600 txid: tx_info.btc_transaction.compute_txid(),
1601 vout: tx_info.out_idx,
1602 },
1603 },
1604 DepositStateV1::Failed(error) => DepositStateV2::Failed(error),
1605 _ => return Err(SubscribeDepositError::NonFinalOutcome),
1606 };
1607
1608 return Ok(UpdateStreamOrOutcome::Outcome(outcome_v2));
1609 };
1610
1611 Ok(self.client_ctx.outcome_or_updates(
1612 &operation,
1613 operation_id,
1614 |state| match state {
1615 DepositStateV2::WaitingForTransaction
1616 | DepositStateV2::WaitingForConfirmation { .. }
1617 | DepositStateV2::Confirmed { .. } => false,
1618 DepositStateV2::Claimed { .. } | DepositStateV2::Failed(_) => true,
1619 },
1620 {
1621 let stream_rpc = self.rpc.clone();
1622 let stream_client_ctx = self.client_ctx.clone();
1623 let stream_script_pub_key = address.script_pubkey();
1624 move || {
1625
1626 stream! {
1627 yield DepositStateV2::WaitingForTransaction;
1628
1629 retry(
1630 "subscribe script history",
1631 background_backoff(),
1632 || stream_rpc.watch_script_history(&stream_script_pub_key)
1633 ).await.expect("Will never give up");
1634 let (btc_out_point, btc_deposited) = retry(
1635 "fetch history",
1636 background_backoff(),
1637 || async {
1638 let history = stream_rpc.get_script_history(&stream_script_pub_key).await?;
1639 history.first().and_then(|tx| {
1640 let (out_idx, amount) = tx.output
1641 .iter()
1642 .enumerate()
1643 .find_map(|(idx, output)| (output.script_pubkey == stream_script_pub_key).then_some((idx, output.value)))?;
1644 let txid = tx.compute_txid();
1645
1646 Some((
1647 bitcoin::OutPoint {
1648 txid,
1649 vout: out_idx as u32,
1650 },
1651 amount
1652 ))
1653 }).ok_or(FetchDepositTransactionError::NotFound)
1654 }
1655 ).await.expect("Will never give up");
1656
1657 yield DepositStateV2::WaitingForConfirmation {
1658 btc_deposited,
1659 btc_out_point
1660 };
1661
1662 let claim_data = stream_client_ctx.module_db().wait_key_exists(&ClaimedPegInKey {
1663 peg_in_index: tweak_idx,
1664 btc_out_point,
1665 }).await;
1666
1667 yield DepositStateV2::Confirmed {
1668 btc_deposited,
1669 btc_out_point
1670 };
1671
1672 match stream_client_ctx.await_primary_module_outputs(operation_id, claim_data.change).await {
1673 Ok(()) => yield DepositStateV2::Claimed {
1674 btc_deposited,
1675 btc_out_point
1676 },
1677 Err(e) => yield DepositStateV2::Failed(e.fmt_compact().to_string())
1678 }
1679 }
1680 }}))
1681 }
1682
1683 pub async fn list_peg_in_tweak_idxes(&self) -> BTreeMap<TweakIdx, PegInTweakIndexData> {
1684 self.client_ctx
1685 .module_db()
1686 .clone()
1687 .begin_transaction_nc()
1688 .await
1689 .find_by_prefix(&PegInTweakIndexPrefix)
1690 .await
1691 .map(|(key, data)| (key.0, data))
1692 .collect()
1693 .await
1694 }
1695
1696 pub async fn find_tweak_idx_by_address(
1697 &self,
1698 address: bitcoin::Address<NetworkUnchecked>,
1699 ) -> Result<TweakIdx, PegInError> {
1700 let data = self.data.clone();
1701 let Some((tweak_idx, _)) = self
1702 .db
1703 .begin_transaction_nc()
1704 .await
1705 .find_by_prefix(&PegInTweakIndexPrefix)
1706 .await
1707 .filter(|(k, _)| {
1708 let (_, derived_address, _tweak_key, _) = data.derive_peg_in_script(k.0);
1709 future::ready(derived_address.into_unchecked() == address)
1710 })
1711 .next()
1712 .await
1713 else {
1714 return Err(PegInError::AddressNotDerived);
1715 };
1716
1717 Ok(tweak_idx.0)
1718 }
1719 pub async fn find_tweak_idx_by_operation_id(
1720 &self,
1721 operation_id: OperationId,
1722 ) -> Result<TweakIdx, PegInError> {
1723 Ok(self
1724 .client_ctx
1725 .module_db()
1726 .clone()
1727 .begin_transaction_nc()
1728 .await
1729 .find_by_prefix(&PegInTweakIndexPrefix)
1730 .await
1731 .filter(|(_k, v)| future::ready(v.operation_id == operation_id))
1732 .next()
1733 .await
1734 .ok_or(PegInError::NoAddressForOperation { operation_id })?
1735 .0
1736 .0)
1737 }
1738
1739 pub async fn get_pegin_tweak_idx(
1740 &self,
1741 tweak_idx: TweakIdx,
1742 ) -> Result<PegInTweakIndexData, PegInError> {
1743 self.client_ctx
1744 .module_db()
1745 .clone()
1746 .begin_transaction_nc()
1747 .await
1748 .get_value(&PegInTweakIndexKey(tweak_idx))
1749 .await
1750 .ok_or(PegInError::TweakIdxNotFound { tweak_idx })
1751 }
1752
1753 pub async fn get_claimed_pegins(
1754 &self,
1755 dbtx: &mut DatabaseTransaction<'_>,
1756 tweak_idx: TweakIdx,
1757 ) -> Vec<(
1758 bitcoin::OutPoint,
1759 TransactionId,
1760 Vec<fedimint_core::OutPoint>,
1761 )> {
1762 let outpoints = dbtx
1763 .get_value(&PegInTweakIndexKey(tweak_idx))
1764 .await
1765 .map(|v| v.claimed)
1766 .unwrap_or_default();
1767
1768 let mut res = vec![];
1769
1770 for outpoint in outpoints {
1771 let claimed_peg_in_data = dbtx
1772 .get_value(&ClaimedPegInKey {
1773 peg_in_index: tweak_idx,
1774 btc_out_point: outpoint,
1775 })
1776 .await
1777 .expect("Must have a corresponding claim record");
1778 res.push((
1779 outpoint,
1780 claimed_peg_in_data.claim_txid,
1781 claimed_peg_in_data.change,
1782 ));
1783 }
1784
1785 res
1786 }
1787
1788 pub async fn recheck_pegin_address_by_op_id(
1790 &self,
1791 operation_id: OperationId,
1792 ) -> Result<(), PegInError> {
1793 let tweak_idx = self.find_tweak_idx_by_operation_id(operation_id).await?;
1794
1795 self.recheck_pegin_address(tweak_idx).await
1796 }
1797
1798 pub async fn recheck_pegin_address_by_address(
1800 &self,
1801 address: bitcoin::Address<NetworkUnchecked>,
1802 ) -> Result<(), PegInError> {
1803 self.recheck_pegin_address(self.find_tweak_idx_by_address(address).await?)
1804 .await
1805 }
1806
1807 pub async fn recheck_pegin_address(&self, tweak_idx: TweakIdx) -> Result<(), PegInError> {
1809 self.db
1810 .autocommit(
1811 |dbtx, _| {
1812 Box::pin(async {
1813 let db_key = PegInTweakIndexKey(tweak_idx);
1814 let db_val = dbtx
1815 .get_value(&db_key)
1816 .await
1817 .ok_or(PegInError::TweakIdxNotFound { tweak_idx })?;
1818
1819 dbtx.insert_entry(
1820 &db_key,
1821 &PegInTweakIndexData {
1822 next_check_time: Some(fedimint_core::time::now()),
1823 ..db_val
1824 },
1825 )
1826 .await;
1827
1828 let sender = self.pegin_monitor_wakeup_sender.clone();
1829 dbtx.on_commit(move || {
1830 sender.send_replace(());
1831 });
1832
1833 Ok::<_, PegInError>(())
1834 })
1835 },
1836 Some(100),
1837 )
1838 .await?;
1839
1840 Ok(())
1841 }
1842
1843 pub async fn await_num_deposits_by_operation_id(
1845 &self,
1846 operation_id: OperationId,
1847 num_deposits: usize,
1848 ) -> Result<(), PegInError> {
1849 let tweak_idx = self.find_tweak_idx_by_operation_id(operation_id).await?;
1850 self.await_num_deposits(tweak_idx, num_deposits).await
1851 }
1852
1853 pub async fn await_num_deposits_by_address(
1854 &self,
1855 address: bitcoin::Address<NetworkUnchecked>,
1856 num_deposits: usize,
1857 ) -> Result<(), PegInError> {
1858 self.await_num_deposits(self.find_tweak_idx_by_address(address).await?, num_deposits)
1859 .await
1860 }
1861
1862 #[instrument(target = LOG_CLIENT_MODULE_WALLET, skip_all, fields(tweak_idx=?tweak_idx, num_deposists=num_deposits))]
1863 pub async fn await_num_deposits(
1864 &self,
1865 tweak_idx: TweakIdx,
1866 num_deposits: usize,
1867 ) -> Result<(), PegInError> {
1868 let operation_id = self.get_pegin_tweak_idx(tweak_idx).await?.operation_id;
1869
1870 let mut receiver = self.pegin_claimed_receiver.clone();
1871 let mut backoff = backoff_util::aggressive_backoff();
1872
1873 loop {
1874 let pegins = self
1875 .get_claimed_pegins(
1876 &mut self.client_ctx.module_db().begin_transaction_nc().await,
1877 tweak_idx,
1878 )
1879 .await;
1880
1881 if pegins.len() < num_deposits {
1882 debug!(target: LOG_CLIENT_MODULE_WALLET, has=pegins.len(), "Not enough deposits");
1883 self.recheck_pegin_address(tweak_idx).await?;
1884 runtime::sleep(backoff.next().unwrap_or_default()).await;
1885 receiver
1886 .changed()
1887 .await
1888 .map_err(|_| PegInError::MonitorStopped)?;
1889 continue;
1890 }
1891
1892 debug!(target: LOG_CLIENT_MODULE_WALLET, has=pegins.len(), "Enough deposits detected");
1893
1894 for (_outpoint, transaction_id, change) in pegins {
1895 if transaction_id == TransactionId::from_byte_array([0; 32]) && change.is_empty() {
1896 debug!(target: LOG_CLIENT_MODULE_WALLET, "Deposited amount was too low, skipping");
1897 continue;
1898 }
1899
1900 debug!(target: LOG_CLIENT_MODULE_WALLET, out_points=?change, "Ensuring deposists claimed");
1901 let tx_subscriber = self.client_ctx.transaction_updates(operation_id).await;
1902
1903 if let Err(reason) = tx_subscriber.await_tx_accepted(transaction_id).await {
1904 return Err(PegInError::TransactionRejected { reason });
1905 }
1906
1907 debug!(target: LOG_CLIENT_MODULE_WALLET, out_points=?change, "Ensuring outputs claimed");
1908 self.client_ctx
1909 .await_primary_module_outputs(operation_id, change)
1910 .await
1911 .expect("Cannot fail if tx was accepted and federation is honest");
1912 }
1913
1914 return Ok(());
1915 }
1916 }
1917
1918 pub async fn withdraw<M: Serialize + MaybeSend + MaybeSync>(
1923 &self,
1924 address: &bitcoin::Address,
1925 amount: bitcoin::Amount,
1926 fee: PegOutFees,
1927 extra_meta: M,
1928 ) -> Result<OperationId, TransactionSubmitError> {
1929 {
1930 let operation_id = OperationId(thread_rng().r#gen());
1931
1932 let withdraw_output =
1933 self.create_withdraw_output(operation_id, address.clone(), amount, fee);
1934 let tx_builder = TransactionBuilder::new()
1935 .with_outputs(self.client_ctx.make_client_outputs(withdraw_output));
1936
1937 let extra_meta =
1938 serde_json::to_value(extra_meta).expect("Failed to serialize extra meta");
1939 self.client_ctx
1940 .finalize_and_submit_transaction(
1941 operation_id,
1942 WalletCommonInit::KIND.as_str(),
1943 {
1944 let address = address.clone();
1945 move |change_range: OutPointRange| WalletOperationMeta {
1946 variant: WalletOperationMetaVariant::Withdraw {
1947 address: address.clone().into_unchecked(),
1948 amount,
1949 fee,
1950 change: change_range.into_iter().collect(),
1951 },
1952 extra_meta: extra_meta.clone(),
1953 }
1954 },
1955 tx_builder,
1956 )
1957 .await?;
1958
1959 let mut dbtx = self.client_ctx.module_db().begin_transaction().await;
1960
1961 self.client_ctx
1962 .log_event(
1963 &mut dbtx,
1964 SendPaymentEvent {
1965 operation_id,
1966 amount: amount + fee.amount(),
1967 fee: fee.amount(),
1968 },
1969 )
1970 .await;
1971
1972 dbtx.commit_tx().await;
1973
1974 Ok(operation_id)
1975 }
1976 }
1977
1978 #[deprecated(
1983 since = "0.4.0",
1984 note = "RBF withdrawals are rejected by the federation"
1985 )]
1986 pub async fn rbf_withdraw<M: Serialize + MaybeSync + MaybeSend>(
1987 &self,
1988 rbf: Rbf,
1989 extra_meta: M,
1990 ) -> Result<OperationId, TransactionSubmitError> {
1991 let operation_id = OperationId(thread_rng().r#gen());
1992
1993 let withdraw_output = self.create_rbf_withdraw_output(operation_id, &rbf);
1994 let tx_builder = TransactionBuilder::new()
1995 .with_outputs(self.client_ctx.make_client_outputs(withdraw_output));
1996
1997 let extra_meta = serde_json::to_value(extra_meta).expect("Failed to serialize extra meta");
1998 self.client_ctx
1999 .finalize_and_submit_transaction(
2000 operation_id,
2001 WalletCommonInit::KIND.as_str(),
2002 move |change_range: OutPointRange| WalletOperationMeta {
2003 variant: WalletOperationMetaVariant::RbfWithdraw {
2004 rbf: rbf.clone(),
2005 change: change_range.into_iter().collect(),
2006 },
2007 extra_meta: extra_meta.clone(),
2008 },
2009 tx_builder,
2010 )
2011 .await?;
2012
2013 Ok(operation_id)
2014 }
2015
2016 pub async fn subscribe_withdraw_updates(
2017 &self,
2018 operation_id: OperationId,
2019 ) -> Result<UpdateStreamOrOutcome<WithdrawState>, SubscribeWithdrawError> {
2020 let operation = self.client_ctx.get_operation(operation_id).await?;
2021
2022 let operation_meta = operation.meta::<WalletOperationMeta>();
2023
2024 let (WalletOperationMetaVariant::Withdraw { change, .. }
2025 | WalletOperationMetaVariant::RbfWithdraw { change, .. }) = operation_meta.variant
2026 else {
2027 return Err(SubscribeWithdrawError::NotAWithdrawal);
2028 };
2029
2030 let mut operation_stream = self.notifier.subscribe(operation_id).await;
2031 let client_ctx = self.client_ctx.clone();
2032
2033 Ok(self.client_ctx.outcome_or_updates(
2034 &operation,
2035 operation_id,
2036 |state| match state {
2037 WithdrawState::Created => false,
2038 WithdrawState::Succeeded(_) | WithdrawState::Failed(_) => true,
2039 },
2040 move || {
2041 stream! {
2042 match next_withdraw_state(&mut operation_stream).await {
2043 Some(WithdrawStates::Created(_)) => {
2044 yield WithdrawState::Created;
2045 },
2046 Some(s) => {
2047 panic!("Unexpected state {s:?}")
2048 },
2049 None => return,
2050 }
2051
2052 let _ = client_ctx
2057 .await_primary_module_outputs(operation_id, change)
2058 .await;
2059
2060
2061 match next_withdraw_state(&mut operation_stream).await {
2062 Some(WithdrawStates::Aborted(inner)) => {
2063 yield WithdrawState::Failed(inner.error);
2064 },
2065 Some(WithdrawStates::Success(inner)) => {
2066 yield WithdrawState::Succeeded(inner.txid);
2067 },
2068 Some(s) => {
2069 panic!("Unexpected state {s:?}")
2070 },
2071 None => {},
2072 }
2073 }
2074 },
2075 ))
2076 }
2077
2078 fn admin_auth(&self) -> Result<ApiAuth, ConsensusVersionVotingError> {
2079 self.admin_auth
2080 .clone()
2081 .ok_or(ConsensusVersionVotingError::AdminAuthMissing)
2082 }
2083
2084 pub async fn activate_consensus_version_voting(
2085 &self,
2086 ) -> Result<(), ConsensusVersionVotingError> {
2087 self.module_api
2088 .activate_consensus_version_voting(self.admin_auth()?)
2089 .await?;
2090
2091 Ok(())
2092 }
2093}
2094
2095#[derive(Debug, thiserror::Error)]
2098enum FetchDepositTransactionError {
2099 #[error(transparent)]
2101 BitcoinRpc(#[from] BitcoinRpcError),
2102
2103 #[error("No deposit transaction found")]
2105 NotFound,
2106}
2107
2108async fn poll_supports_safe_deposit_version(db: Database, module_api: DynModuleApi) {
2111 loop {
2112 if supports_safe_deposit_verified(&db).await {
2113 break;
2114 }
2115
2116 module_api.wait_for_initialized_connections().await;
2117
2118 if verify_supports_safe_deposit(&db, &module_api).await == Some(true) {
2119 break;
2120 }
2121
2122 if is_running_in_test_env() {
2123 sleep(Duration::from_secs(10)).await;
2125 } else {
2126 sleep(Duration::from_hours(1)).await;
2127 }
2128 }
2129}
2130
2131async fn supports_safe_deposit_verified(db: &Database) -> bool {
2134 db.begin_transaction_nc()
2135 .await
2136 .get_value(&SupportsSafeDepositKey)
2137 .await
2138 .is_some()
2139}
2140
2141async fn verify_supports_safe_deposit(db: &Database, module_api: &DynModuleApi) -> Option<bool> {
2146 let module_consensus_version = match module_api.module_consensus_version().await {
2147 Ok(module_consensus_version) => module_consensus_version,
2148 Err(err) => {
2149 debug!(
2150 target: LOG_CLIENT_MODULE_WALLET,
2151 err = %err.fmt_compact(),
2152 "Could not fetch the wallet module consensus version"
2153 );
2154 return None;
2155 }
2156 };
2157
2158 let supported_version = SAFE_DEPOSIT_MODULE_CONSENSUS_VERSION <= module_consensus_version;
2159
2160 if supported_version {
2161 store_supports_safe_deposit(db.begin_transaction().await).await;
2162 }
2163
2164 Some(supported_version)
2165}
2166
2167async fn store_supports_safe_deposit(mut dbtx: DatabaseTransaction<'_, Committable>) {
2182 if dbtx.get_value(&SupportsSafeDepositKey).await.is_some() {
2183 return;
2185 }
2186
2187 dbtx.insert_entry(&SupportsSafeDepositKey, &()).await;
2188
2189 match dbtx.commit_tx_result().await {
2190 Ok(()) | Err(DatabaseError::WriteConflict) => {}
2191 Err(err) => {
2192 warn!(
2193 target: LOG_CLIENT_MODULE_WALLET,
2194 err = %err.fmt_compact(),
2195 "Failed to store the safe-deposit marker"
2196 );
2197 }
2198 }
2199}
2200
2201async fn get_next_peg_in_tweak_child_id(dbtx: &mut DatabaseTransaction<'_>) -> TweakIdx {
2203 let index = dbtx
2204 .get_value(&NextPegInTweakIndexKey)
2205 .await
2206 .unwrap_or_default();
2207 dbtx.insert_entry(&NextPegInTweakIndexKey, &(index.next()))
2208 .await;
2209 index
2210}
2211
2212#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
2213pub enum WalletClientStates {
2214 Deposit(DepositStateMachine),
2215 Withdraw(WithdrawStateMachine),
2216}
2217
2218impl IntoDynInstance for WalletClientStates {
2219 type DynType = DynState;
2220
2221 fn into_dyn(self, instance_id: ModuleInstanceId) -> Self::DynType {
2222 DynState::from_typed(instance_id, self)
2223 }
2224}
2225
2226impl State for WalletClientStates {
2227 type ModuleContext = WalletClientContext;
2228
2229 fn transitions(
2230 &self,
2231 context: &Self::ModuleContext,
2232 global_context: &DynGlobalClientContext,
2233 ) -> Vec<StateTransition<Self>> {
2234 match self {
2235 WalletClientStates::Deposit(sm) => {
2236 sm_enum_variant_translation!(
2237 sm.transitions(context, global_context),
2238 WalletClientStates::Deposit
2239 )
2240 }
2241 WalletClientStates::Withdraw(sm) => {
2242 sm_enum_variant_translation!(
2243 sm.transitions(context, global_context),
2244 WalletClientStates::Withdraw
2245 )
2246 }
2247 }
2248 }
2249
2250 fn operation_id(&self) -> OperationId {
2251 match self {
2252 WalletClientStates::Deposit(sm) => sm.operation_id(),
2253 WalletClientStates::Withdraw(sm) => sm.operation_id(),
2254 }
2255 }
2256}
2257
2258#[cfg(all(test, not(target_family = "wasm")))]
2259mod tests;