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 events;
19use events::SendPaymentEvent;
20mod pegin_monitor;
22mod withdraw;
23
24use std::collections::{BTreeMap, BTreeSet};
25use std::future;
26use std::sync::Arc;
27use std::time::{Duration, SystemTime};
28
29use anyhow::{Context as AnyhowContext, anyhow, bail, ensure};
30use async_stream::{stream, try_stream};
31use backup::WalletModuleBackup;
32use bitcoin::address::NetworkUnchecked;
33use bitcoin::secp256k1::{All, SECP256K1, Secp256k1};
34use bitcoin::{Address, Network, ScriptBuf};
35use client_db::{DbKeyPrefix, PegInTweakIndexKey, SupportsSafeDepositKey, TweakIdx};
36use fedimint_api_client::api::{DynModuleApi, FederationResult};
37use fedimint_bitcoind::{BitcoindTracked, DynBitcoindRpc, IBitcoindRpc, create_esplora_rpc};
38use fedimint_client_module::module::init::{
39 ClientModuleInit, ClientModuleInitArgs, ClientModuleRecoverArgs,
40};
41use fedimint_client_module::module::recovery::RecoveryProgress;
42use fedimint_client_module::module::{ClientContext, ClientModule, IClientModule, OutPointRange};
43use fedimint_client_module::oplog::UpdateStreamOrOutcome;
44use fedimint_client_module::sm::{Context, DynState, ModuleNotifier, State, StateTransition};
45use fedimint_client_module::transaction::{
46 ClientOutput, ClientOutputBundle, ClientOutputSM, FeeQuote, FeeQuoteRequest, TransactionBuilder,
47};
48use fedimint_client_module::{DynGlobalClientContext, sm_enum_variant_translation};
49use fedimint_core::core::{Decoder, IntoDynInstance, ModuleInstanceId, ModuleKind, OperationId};
50use fedimint_core::db::{
51 AutocommitError, Database, DatabaseTransaction, IDatabaseTransactionOpsCoreTyped,
52};
53use fedimint_core::encoding::{Decodable, Encodable};
54use fedimint_core::envs::{BitcoinRpcConfig, is_running_in_test_env};
55use fedimint_core::module::{
56 Amounts, ApiAuth, ApiVersion, CommonModuleInit, ModuleCommon, ModuleConsensusVersion,
57 ModuleInit, MultiApiVersion,
58};
59use fedimint_core::task::{MaybeSend, MaybeSync, TaskGroup, sleep};
60use fedimint_core::util::backoff_util::background_backoff;
61use fedimint_core::util::{BoxStream, backoff_util, retry};
62use fedimint_core::{
63 BitcoinHash, OutPoint, TransactionId, apply, async_trait_maybe_send, push_db_pair_items,
64 runtime, secp256k1,
65};
66use fedimint_derive_secret::{ChildId, DerivableSecret};
67use fedimint_logging::LOG_CLIENT_MODULE_WALLET;
68pub use fedimint_wallet_common as common;
69use fedimint_wallet_common::config::{FeeConsensus, WalletClientConfig};
70use fedimint_wallet_common::tweakable::Tweakable;
71pub use fedimint_wallet_common::*;
72use futures::{Stream, StreamExt};
73use rand::{Rng, thread_rng};
74use secp256k1::Keypair;
75use serde::{Deserialize, Serialize};
76use strum::IntoEnumIterator;
77use tokio::sync::watch;
78use tracing::{debug, instrument};
79
80use crate::api::WalletFederationApi;
81use crate::backup::{FEDERATION_RECOVER_MAX_GAP, RecoveryStateV2, WalletRecovery};
82use crate::client_db::{
83 ClaimedPegInData, ClaimedPegInKey, ClaimedPegInPrefix, NextPegInTweakIndexKey,
84 PegInPoolCursorKey, PegInTweakIndexData, PegInTweakIndexPrefix, RecoveryFinalizedKey,
85 RecoveryStateKey, SupportsSafeDepositPrefix,
86};
87use crate::deposit::DepositStateMachine;
88use crate::withdraw::{CreatedWithdrawState, WithdrawStateMachine, WithdrawStates};
89
90const WALLET_TWEAK_CHILD_ID: ChildId = ChildId(0);
91
92#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
93pub struct BitcoinTransactionData {
94 pub btc_transaction: bitcoin::Transaction,
97 pub out_idx: u32,
99}
100
101#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
102pub enum DepositStateV1 {
103 WaitingForTransaction,
104 WaitingForConfirmation(BitcoinTransactionData),
105 Confirmed(BitcoinTransactionData),
106 Claimed(BitcoinTransactionData),
107 Failed(String),
108}
109
110#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
111pub enum DepositStateV2 {
112 WaitingForTransaction,
113 WaitingForConfirmation {
114 #[serde(with = "bitcoin::amount::serde::as_sat")]
115 btc_deposited: bitcoin::Amount,
116 btc_out_point: bitcoin::OutPoint,
117 },
118 Confirmed {
119 #[serde(with = "bitcoin::amount::serde::as_sat")]
120 btc_deposited: bitcoin::Amount,
121 btc_out_point: bitcoin::OutPoint,
122 },
123 Claimed {
124 #[serde(with = "bitcoin::amount::serde::as_sat")]
125 btc_deposited: bitcoin::Amount,
126 btc_out_point: bitcoin::OutPoint,
127 },
128 Failed(String),
129}
130
131#[derive(Debug, Clone, PartialEq, Eq)]
133pub struct DepositAddressInfo {
134 pub operation_id: OperationId,
135 pub address: Address,
136 pub tweak_idx: TweakIdx,
137}
138
139#[allow(clippy::enum_variant_names)]
145#[derive(Debug, Clone, PartialEq, Eq)]
146pub enum MaybeNewAddress {
147 NewAddress(DepositAddressInfo),
149 TooManyUnusedAddresses(Vec<DepositAddressInfo>),
153}
154
155#[derive(Debug, Clone, Copy, PartialEq, Eq)]
161pub enum AllocateDepositOutcome {
162 Fresh,
164 Reused { original_tweak_idx: TweakIdx },
168}
169
170#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
171pub enum WithdrawState {
172 Created,
173 Succeeded(bitcoin::Txid),
174 Failed(String),
175 }
179
180async fn next_withdraw_state<S>(stream: &mut S) -> Option<WithdrawStates>
181where
182 S: Stream<Item = WalletClientStates> + Unpin,
183{
184 loop {
185 if let WalletClientStates::Withdraw(ds) = stream.next().await? {
186 return Some(ds.state);
187 }
188 tokio::task::yield_now().await;
189 }
190}
191
192#[derive(Debug, Clone, Default)]
193pub struct WalletClientInit(pub Option<DynBitcoindRpc>);
195
196const SLICE_SIZE: u64 = 1000;
197
198impl WalletClientInit {
199 pub fn new(rpc: DynBitcoindRpc) -> Self {
200 Self(Some(rpc))
201 }
202
203 async fn recover_from_slices(
204 &self,
205 args: &ClientModuleRecoverArgs<Self>,
206 ) -> anyhow::Result<()> {
207 let data = WalletClientModuleData {
208 cfg: args.cfg().clone(),
209 module_root_secret: args.module_root_secret().clone(),
210 };
211
212 let total_items = args.module_api().fetch_recovery_count().await?;
213
214 let mut state = RecoveryStateV2::new();
215
216 state.refill_pending_pool_up_to(&data, TweakIdx(FEDERATION_RECOVER_MAX_GAP));
217
218 for start in (0..total_items).step_by(SLICE_SIZE as usize) {
219 let end = std::cmp::min(start + SLICE_SIZE, total_items);
220
221 let items = args.module_api().fetch_recovery_slice(start, end).await?;
222
223 for item in &items {
224 match item {
225 RecoveryItem::Input { outpoint, script } => {
226 state.handle_item(*outpoint, script, &data);
227 }
228 }
229 }
230
231 args.update_recovery_progress(RecoveryProgress {
232 complete: end.try_into().unwrap_or(u32::MAX),
233 total: total_items.try_into().unwrap_or(u32::MAX),
234 });
235 }
236
237 let mut dbtx = args.db().begin_transaction().await;
238
239 for tweak_idx in 0..state.new_start_idx().0 {
240 let operation_id = data.derive_peg_in_script(TweakIdx(tweak_idx)).3;
241
242 let claimed = state
243 .claimed_outpoints
244 .get(&TweakIdx(tweak_idx))
245 .cloned()
246 .unwrap_or_default();
247
248 dbtx.insert_new_entry(
249 &PegInTweakIndexKey(TweakIdx(tweak_idx)),
250 &PegInTweakIndexData {
251 operation_id,
252 creation_time: fedimint_core::time::now(),
253 last_check_time: None,
254 next_check_time: Some(fedimint_core::time::now()),
255 claimed,
256 },
257 )
258 .await;
259 }
260
261 dbtx.insert_new_entry(&NextPegInTweakIndexKey, &state.new_start_idx())
262 .await;
263
264 dbtx.commit_tx().await;
265
266 Ok(())
267 }
268}
269
270impl ModuleInit for WalletClientInit {
271 type Common = WalletCommonInit;
272
273 async fn dump_database(
274 &self,
275 dbtx: &mut DatabaseTransaction<'_>,
276 prefix_names: Vec<String>,
277 ) -> Box<dyn Iterator<Item = (String, Box<dyn erased_serde::Serialize + Send>)> + '_> {
278 let mut wallet_client_items: BTreeMap<String, Box<dyn erased_serde::Serialize + Send>> =
279 BTreeMap::new();
280 let filtered_prefixes = DbKeyPrefix::iter().filter(|f| {
281 prefix_names.is_empty() || prefix_names.contains(&f.to_string().to_lowercase())
282 });
283
284 for table in filtered_prefixes {
285 match table {
286 DbKeyPrefix::NextPegInTweakIndex => {
287 if let Some(index) = dbtx.get_value(&NextPegInTweakIndexKey).await {
288 wallet_client_items
289 .insert("NextPegInTweakIndex".to_string(), Box::new(index));
290 }
291 }
292 DbKeyPrefix::PegInTweakIndex => {
293 push_db_pair_items!(
294 dbtx,
295 PegInTweakIndexPrefix,
296 PegInTweakIndexKey,
297 PegInTweakIndexData,
298 wallet_client_items,
299 "Peg-In Tweak Index"
300 );
301 }
302 DbKeyPrefix::ClaimedPegIn => {
303 push_db_pair_items!(
304 dbtx,
305 ClaimedPegInPrefix,
306 ClaimedPegInKey,
307 ClaimedPegInData,
308 wallet_client_items,
309 "Claimed Peg-In"
310 );
311 }
312 DbKeyPrefix::RecoveryFinalized => {
313 if let Some(val) = dbtx.get_value(&RecoveryFinalizedKey).await {
314 wallet_client_items.insert("RecoveryFinalized".to_string(), Box::new(val));
315 }
316 }
317 DbKeyPrefix::SupportsSafeDeposit => {
318 push_db_pair_items!(
319 dbtx,
320 SupportsSafeDepositPrefix,
321 SupportsSafeDepositKey,
322 (),
323 wallet_client_items,
324 "Supports Safe Deposit"
325 );
326 }
327 DbKeyPrefix::PegInPoolCursor => {
328 if let Some(cursor) = dbtx.get_value(&PegInPoolCursorKey).await {
329 wallet_client_items.insert("PegInPoolCursor".to_string(), Box::new(cursor));
330 }
331 }
332 DbKeyPrefix::RecoveryState
333 | DbKeyPrefix::ExternalReservedStart
334 | DbKeyPrefix::CoreInternalReservedStart
335 | DbKeyPrefix::CoreInternalReservedEnd => {}
336 }
337 }
338
339 Box::new(wallet_client_items.into_iter())
340 }
341}
342
343#[apply(async_trait_maybe_send!)]
344impl ClientModuleInit for WalletClientInit {
345 type Module = WalletClientModule;
346
347 fn supported_api_versions(&self) -> MultiApiVersion {
348 MultiApiVersion::try_from_iter([ApiVersion { major: 0, minor: 0 }])
349 .expect("no version conflicts")
350 }
351
352 async fn init(&self, args: &ClientModuleInitArgs<Self>) -> anyhow::Result<Self::Module> {
353 let data = WalletClientModuleData {
354 cfg: args.cfg().clone(),
355 module_root_secret: args.module_root_secret().clone(),
356 };
357
358 let db = args.db().clone();
359
360 let rpc_config = WalletClientModule::get_rpc_config(args.cfg());
361
362 let btc_rpc = if let Some(user_rpc) = args.user_bitcoind_rpc() {
369 user_rpc.clone()
370 } else if let Some(factory) = args.user_bitcoind_rpc_no_chain_id() {
371 if let Some(rpc) = factory(rpc_config.url.clone()).await {
372 rpc
373 } else {
374 self.0
375 .clone()
376 .unwrap_or(create_esplora_rpc(&rpc_config.url)?)
377 }
378 } else {
379 self.0
380 .clone()
381 .unwrap_or(create_esplora_rpc(&rpc_config.url)?)
382 };
383 let btc_rpc = BitcoindTracked::new(btc_rpc, "wallet-client").into_dyn();
384
385 let module_api = args.module_api().clone();
386
387 let (pegin_claimed_sender, pegin_claimed_receiver) = watch::channel(());
388 let (pegin_monitor_wakeup_sender, pegin_monitor_wakeup_receiver) = watch::channel(());
389
390 Ok(WalletClientModule {
391 db,
392 data,
393 module_api,
394 notifier: args.notifier().clone(),
395 rpc: btc_rpc,
396 client_ctx: args.context(),
397 pegin_monitor_wakeup_sender,
398 pegin_monitor_wakeup_receiver,
399 pegin_claimed_receiver,
400 pegin_claimed_sender,
401 task_group: args.task_group().clone(),
402 client_span: args.client_span().clone(),
403 admin_auth: args.admin_auth().cloned(),
404 })
405 }
406
407 async fn recover(
412 &self,
413 args: &ClientModuleRecoverArgs<Self>,
414 snapshot: Option<&<Self::Module as ClientModule>::Backup>,
415 ) -> anyhow::Result<()> {
416 if args
419 .db()
420 .begin_transaction_nc()
421 .await
422 .get_value(&RecoveryStateKey)
423 .await
424 .is_some()
425 {
426 return args
427 .recover_from_history::<WalletRecovery>(self, snapshot)
428 .await;
429 }
430
431 if args.module_api().fetch_recovery_count().await.is_ok() {
433 self.recover_from_slices(args).await
434 } else {
435 args.recover_from_history::<WalletRecovery>(self, snapshot)
436 .await
437 }
438 }
439
440 fn used_db_prefixes(&self) -> Option<BTreeSet<u8>> {
441 Some(
442 DbKeyPrefix::iter()
443 .map(|p| p as u8)
444 .chain(
445 DbKeyPrefix::ExternalReservedStart as u8
446 ..=DbKeyPrefix::CoreInternalReservedEnd as u8,
447 )
448 .collect(),
449 )
450 }
451}
452
453#[derive(Debug, Clone, Serialize, Deserialize)]
454pub struct WalletOperationMeta {
455 pub variant: WalletOperationMetaVariant,
456 pub extra_meta: serde_json::Value,
457}
458
459#[derive(Debug, Clone, Serialize, Deserialize)]
460#[serde(rename_all = "snake_case")]
461pub enum WalletOperationMetaVariant {
462 Deposit {
463 address: Address<NetworkUnchecked>,
464 #[serde(default)]
469 tweak_idx: Option<TweakIdx>,
470 #[serde(default, skip_serializing_if = "Option::is_none")]
471 expires_at: Option<SystemTime>,
472 },
473 Withdraw {
474 address: Address<NetworkUnchecked>,
475 #[serde(with = "bitcoin::amount::serde::as_sat")]
476 amount: bitcoin::Amount,
477 fee: PegOutFees,
478 change: Vec<OutPoint>,
479 },
480
481 RbfWithdraw {
482 rbf: Rbf,
483 change: Vec<OutPoint>,
484 },
485}
486
487#[derive(Debug, Clone)]
489pub struct WalletClientModuleData {
490 cfg: WalletClientConfig,
491 module_root_secret: DerivableSecret,
492}
493
494impl WalletClientModuleData {
495 fn derive_deposit_address(
496 &self,
497 idx: TweakIdx,
498 ) -> (Keypair, secp256k1::PublicKey, Address, OperationId) {
499 let idx = ChildId(idx.0);
500
501 let secret_tweak_key = self
502 .module_root_secret
503 .child_key(WALLET_TWEAK_CHILD_ID)
504 .child_key(idx)
505 .to_secp_key(fedimint_core::secp256k1::SECP256K1);
506
507 let public_tweak_key = secret_tweak_key.public_key();
508
509 let address = self
510 .cfg
511 .peg_in_descriptor
512 .tweak(&public_tweak_key, bitcoin::secp256k1::SECP256K1)
513 .address(self.cfg.network.0)
514 .unwrap();
515
516 let operation_id = OperationId(public_tweak_key.x_only_public_key().0.serialize());
518
519 (secret_tweak_key, public_tweak_key, address, operation_id)
520 }
521
522 fn derive_peg_in_script(
523 &self,
524 idx: TweakIdx,
525 ) -> (ScriptBuf, bitcoin::Address, Keypair, OperationId) {
526 let (secret_tweak_key, _, address, operation_id) = self.derive_deposit_address(idx);
527
528 (
529 self.cfg
530 .peg_in_descriptor
531 .tweak(&secret_tweak_key.public_key(), SECP256K1)
532 .script_pubkey(),
533 address,
534 secret_tweak_key,
535 operation_id,
536 )
537 }
538}
539
540#[derive(Debug)]
541pub struct WalletClientModule {
542 data: WalletClientModuleData,
543 db: Database,
544 module_api: DynModuleApi,
545 notifier: ModuleNotifier<WalletClientStates>,
546 rpc: DynBitcoindRpc,
547 client_ctx: ClientContext<Self>,
548 pegin_monitor_wakeup_sender: watch::Sender<()>,
550 pegin_monitor_wakeup_receiver: watch::Receiver<()>,
551 pegin_claimed_sender: watch::Sender<()>,
553 pegin_claimed_receiver: watch::Receiver<()>,
554 task_group: TaskGroup,
555 client_span: tracing::Span,
556 admin_auth: Option<ApiAuth>,
557}
558
559#[apply(async_trait_maybe_send!)]
560impl ClientModule for WalletClientModule {
561 type Init = WalletClientInit;
562 type Common = WalletModuleTypes;
563 type Backup = WalletModuleBackup;
564 type ModuleStateMachineContext = WalletClientContext;
565 type States = WalletClientStates;
566
567 fn context(&self) -> Self::ModuleStateMachineContext {
568 WalletClientContext {
569 rpc: self.rpc.clone(),
570 wallet_descriptor: self.cfg().peg_in_descriptor.clone(),
571 wallet_decoder: self.decoder(),
572 secp: Secp256k1::default(),
573 client_ctx: self.client_ctx.clone(),
574 }
575 }
576
577 async fn start(&self) {
578 self.task_group
579 .spawn_cancellable_with_span(self.client_span.clone(), "peg-in monitor", {
580 let client_ctx = self.client_ctx.clone();
581 let db = self.db.clone();
582 let btc_rpc = self.rpc.clone();
583 let module_api = self.module_api.clone();
584 let data = self.data.clone();
585 let pegin_claimed_sender = self.pegin_claimed_sender.clone();
586 let pegin_monitor_wakeup_receiver = self.pegin_monitor_wakeup_receiver.clone();
587 pegin_monitor::run_peg_in_monitor(
588 client_ctx,
589 db,
590 btc_rpc,
591 module_api,
592 data,
593 pegin_claimed_sender,
594 pegin_monitor_wakeup_receiver,
595 )
596 });
597
598 self.task_group.spawn_cancellable_with_span(
599 self.client_span.clone(),
600 "supports-safe-deposit-version",
601 {
602 let db = self.db.clone();
603 let module_api = self.module_api.clone();
604
605 poll_supports_safe_deposit_version(db, module_api)
606 },
607 );
608 }
609
610 fn supports_backup(&self) -> bool {
611 true
612 }
613
614 async fn backup(&self) -> anyhow::Result<backup::WalletModuleBackup> {
615 let session_count = self.client_ctx.global_api().session_count().await?;
617
618 let mut dbtx = self.db.begin_transaction_nc().await;
619 let next_pegin_tweak_idx = dbtx
620 .get_value(&NextPegInTweakIndexKey)
621 .await
622 .unwrap_or_default();
623 let claimed = dbtx
624 .find_by_prefix(&PegInTweakIndexPrefix)
625 .await
626 .filter_map(|(k, v)| async move {
627 if v.claimed.is_empty() {
628 None
629 } else {
630 Some(k.0)
631 }
632 })
633 .collect()
634 .await;
635 Ok(backup::WalletModuleBackup::new_v1(
636 session_count,
637 next_pegin_tweak_idx,
638 claimed,
639 ))
640 }
641
642 fn input_fee(
643 &self,
644 _amount: &Amounts,
645 _input: &<Self::Common as ModuleCommon>::Input,
646 ) -> Option<Amounts> {
647 Some(Amounts::new_bitcoin(self.cfg().fee_consensus.peg_in_abs))
648 }
649
650 fn output_fee(
651 &self,
652 _amount: &Amounts,
653 _output: &<Self::Common as ModuleCommon>::Output,
654 ) -> Option<Amounts> {
655 Some(Amounts::new_bitcoin(self.cfg().fee_consensus.peg_out_abs))
656 }
657
658 async fn handle_rpc(
659 &self,
660 method: String,
661 request: serde_json::Value,
662 ) -> BoxStream<'_, anyhow::Result<serde_json::Value>> {
663 Box::pin(try_stream! {
664 match method.as_str() {
665 "get_wallet_summary" => {
666 let _req: WalletSummaryRequest = serde_json::from_value(request)?;
667 let wallet_summary = self.get_wallet_summary()
668 .await
669 .expect("Failed to fetch wallet summary");
670 let result = serde_json::to_value(&wallet_summary)
671 .expect("Serialization error");
672 yield result;
673 }
674 "get_block_count_local" => {
675 let block_count = self.get_block_count_local().await
676 .expect("Failed to fetch block count");
677 yield serde_json::to_value(block_count)?;
678 }
679 "peg_in" => {
680 let req: PegInRequest = serde_json::from_value(request)?;
681 let response = self.peg_in(req)
682 .await
683 .map_err(|e| anyhow::anyhow!("peg_in failed: {e}"))?;
684 let result = serde_json::to_value(&response)?;
685 yield result;
686 },
687 "peg_out" => {
688 let req: PegOutRequest = serde_json::from_value(request)?;
689 let response = self.peg_out(req)
690 .await
691 .map_err(|e| anyhow::anyhow!("peg_out failed: {e}"))?;
692 let result = serde_json::to_value(&response)?;
693 yield result;
694 },
695 "subscribe_deposit" => {
696 let req: SubscribeDepositRequest = serde_json::from_value(request)?;
697 for await state in self.subscribe_deposit(req.operation_id).await?.into_stream() {
698 yield serde_json::to_value(state)?;
699 }
700 },
701 "subscribe_withdraw" => {
702 let req: SubscribeWithdrawRequest = serde_json::from_value(request)?;
703 for await state in self.subscribe_withdraw_updates(req.operation_id).await?.into_stream(){
704 yield serde_json::to_value(state)?;
705 }
706 }
707 _ => {
708 Err(anyhow::format_err!("Unknown method: {method}"))?;
709 }
710 }
711 })
712 }
713
714 #[cfg(feature = "cli")]
715 async fn handle_cli_command(
716 &self,
717 args: &[std::ffi::OsString],
718 ) -> anyhow::Result<serde_json::Value> {
719 cli::handle_cli_command(self, args).await
720 }
721}
722
723#[derive(Deserialize)]
724struct WalletSummaryRequest {}
725
726#[derive(Debug, Clone)]
727pub struct WalletClientContext {
728 rpc: DynBitcoindRpc,
729 wallet_descriptor: PegInDescriptor,
730 wallet_decoder: Decoder,
731 secp: Secp256k1<All>,
732 pub client_ctx: ClientContext<WalletClientModule>,
733}
734
735#[derive(Debug, Clone, Serialize, Deserialize)]
736pub struct PegInRequest {
737 pub extra_meta: serde_json::Value,
738}
739
740#[derive(Deserialize)]
741struct SubscribeDepositRequest {
742 operation_id: OperationId,
743}
744
745#[derive(Deserialize)]
746struct SubscribeWithdrawRequest {
747 operation_id: OperationId,
748}
749
750#[derive(Debug, Clone, Serialize, Deserialize)]
751pub struct PegInResponse {
752 pub deposit_address: Address<NetworkUnchecked>,
753 pub operation_id: OperationId,
754}
755
756#[derive(Debug, Clone, Serialize, Deserialize)]
757pub struct PegOutRequest {
758 pub amount_sat: u64,
759 pub destination_address: Address<NetworkUnchecked>,
760 pub extra_meta: serde_json::Value,
761}
762
763#[derive(Debug, Clone, Serialize, Deserialize)]
764pub struct PegOutResponse {
765 pub operation_id: OperationId,
766}
767
768impl Context for WalletClientContext {
769 const KIND: Option<ModuleKind> = Some(KIND);
770}
771
772impl WalletClientModule {
773 fn cfg(&self) -> &WalletClientConfig {
774 &self.data.cfg
775 }
776
777 fn get_rpc_config(cfg: &WalletClientConfig) -> BitcoinRpcConfig {
778 match BitcoinRpcConfig::get_defaults_from_env_vars() {
779 Ok(rpc_config) => {
780 if rpc_config.kind == "bitcoind" {
783 cfg.default_bitcoin_rpc.clone()
784 } else {
785 rpc_config
786 }
787 }
788 _ => cfg.default_bitcoin_rpc.clone(),
789 }
790 }
791
792 pub fn get_network(&self) -> Network {
793 self.cfg().network.0
794 }
795
796 pub fn get_finality_delay(&self) -> u32 {
797 self.cfg().finality_delay
798 }
799
800 pub fn get_fee_consensus(&self) -> FeeConsensus {
801 self.cfg().fee_consensus
802 }
803
804 async fn allocate_deposit_address_inner(
805 &self,
806 dbtx: &mut DatabaseTransaction<'_>,
807 ) -> DepositAddressInfo {
808 dbtx.ensure_isolated().expect("Must be isolated db");
809
810 let tweak_idx = get_next_peg_in_tweak_child_id(dbtx).await;
811 let (_secret_tweak_key, _, address, operation_id) =
812 self.data.derive_deposit_address(tweak_idx);
813
814 let now = fedimint_core::time::now();
815
816 dbtx.insert_new_entry(
817 &PegInTweakIndexKey(tweak_idx),
818 &PegInTweakIndexData {
819 creation_time: now,
820 next_check_time: Some(now),
821 last_check_time: None,
822 operation_id,
823 claimed: vec![],
824 },
825 )
826 .await;
827
828 DepositAddressInfo {
829 operation_id,
830 address,
831 tweak_idx,
832 }
833 }
834
835 pub async fn get_withdraw_fees(
842 &self,
843 address: &bitcoin::Address,
844 amount: bitcoin::Amount,
845 ) -> anyhow::Result<PegOutFees> {
846 self.module_api
847 .fetch_peg_out_fees(address, amount)
848 .await?
849 .context("Federation didn't return peg-out fees")
850 }
851
852 pub async fn send_fee_quote(&self, amount: bitcoin::Amount) -> anyhow::Result<FeeQuote> {
867 let amount = fedimint_core::Amount::from_sats(amount.to_sat());
868 self.client_ctx
869 .fee_quote(
870 OperationId::new_random(),
871 FeeQuoteRequest {
872 input_amount: Amounts::ZERO,
873 output_amount: Amounts::new_bitcoin(amount),
874 input_fee: Amounts::ZERO,
875 output_fee: Amounts::new_bitcoin(self.cfg().fee_consensus.peg_out_abs),
876 },
877 )
878 .await
879 }
880
881 pub async fn get_wallet_summary(&self) -> anyhow::Result<WalletSummary> {
883 Ok(self.module_api.fetch_wallet_summary().await?)
884 }
885
886 pub async fn get_block_count_local(&self) -> anyhow::Result<u32> {
887 Ok(self.module_api.fetch_block_count_local().await?)
888 }
889
890 pub fn create_withdraw_output(
891 &self,
892 operation_id: OperationId,
893 address: bitcoin::Address,
894 amount: bitcoin::Amount,
895 fees: PegOutFees,
896 ) -> anyhow::Result<ClientOutputBundle<WalletOutput, WalletClientStates>> {
897 let output = WalletOutput::new_v0_peg_out(address, amount, fees);
898
899 let amount = output.maybe_v0_ref().expect("v0 output").amount().into();
900
901 let sm_gen = move |out_point_range: OutPointRange| {
902 assert_eq!(out_point_range.count(), 1);
903 let out_idx = out_point_range.start_idx();
904 vec![WalletClientStates::Withdraw(WithdrawStateMachine {
905 operation_id,
906 state: WithdrawStates::Created(CreatedWithdrawState {
907 fm_outpoint: OutPoint {
908 txid: out_point_range.txid(),
909 out_idx,
910 },
911 }),
912 })]
913 };
914
915 Ok(ClientOutputBundle::new(
916 vec![ClientOutput::<WalletOutput> {
917 output,
918 amounts: Amounts::new_bitcoin(amount),
919 }],
920 vec![ClientOutputSM::<WalletClientStates> {
921 state_machines: Arc::new(sm_gen),
922 }],
923 ))
924 }
925
926 pub async fn peg_in(&self, req: PegInRequest) -> anyhow::Result<PegInResponse> {
927 let deposit_address = self.safe_allocate_deposit_address(req.extra_meta).await?;
928
929 Ok(PegInResponse {
930 deposit_address: Address::from_script(
931 &deposit_address.address.script_pubkey(),
932 self.get_network(),
933 )?
934 .as_unchecked()
935 .clone(),
936 operation_id: deposit_address.operation_id,
937 })
938 }
939
940 pub async fn peg_out(&self, req: PegOutRequest) -> anyhow::Result<PegOutResponse> {
941 let amount = bitcoin::Amount::from_sat(req.amount_sat);
942 let destination = req
943 .destination_address
944 .require_network(self.get_network())?;
945
946 let fees = self.get_withdraw_fees(&destination, amount).await?;
947 let operation_id = self
948 .withdraw(&destination, amount, fees, req.extra_meta)
949 .await
950 .context("Failed to initiate withdraw")?;
951
952 Ok(PegOutResponse { operation_id })
953 }
954
955 pub fn create_rbf_withdraw_output(
956 &self,
957 operation_id: OperationId,
958 rbf: &Rbf,
959 ) -> anyhow::Result<ClientOutputBundle<WalletOutput, WalletClientStates>> {
960 let output = WalletOutput::new_v0_rbf(rbf.fees, rbf.txid);
961
962 let amount = output.maybe_v0_ref().expect("v0 output").amount().into();
963
964 let sm_gen = move |out_point_range: OutPointRange| {
965 assert_eq!(out_point_range.count(), 1);
966 let out_idx = out_point_range.start_idx();
967 vec![WalletClientStates::Withdraw(WithdrawStateMachine {
968 operation_id,
969 state: WithdrawStates::Created(CreatedWithdrawState {
970 fm_outpoint: OutPoint {
971 txid: out_point_range.txid(),
972 out_idx,
973 },
974 }),
975 })]
976 };
977
978 Ok(ClientOutputBundle::new(
979 vec![ClientOutput::<WalletOutput> {
980 output,
981 amounts: Amounts::new_bitcoin(amount),
982 }],
983 vec![ClientOutputSM::<WalletClientStates> {
984 state_machines: Arc::new(sm_gen),
985 }],
986 ))
987 }
988
989 pub async fn btc_tx_has_no_size_limit(&self) -> FederationResult<bool> {
990 Ok(self.module_api.module_consensus_version().await? >= ModuleConsensusVersion::new(2, 2))
991 }
992
993 pub async fn supports_safe_deposit(&self) -> bool {
1002 let mut dbtx = self.db.begin_transaction().await;
1003
1004 let already_verified_supports_safe_deposit =
1005 dbtx.get_value(&SupportsSafeDepositKey).await.is_some();
1006
1007 already_verified_supports_safe_deposit || {
1008 match self.module_api.module_consensus_version().await {
1009 Ok(module_consensus_version) => {
1010 let supported_version =
1011 SAFE_DEPOSIT_MODULE_CONSENSUS_VERSION <= module_consensus_version;
1012
1013 if supported_version {
1014 dbtx.insert_new_entry(&SupportsSafeDepositKey, &()).await;
1015 dbtx.commit_tx().await;
1016 }
1017
1018 supported_version
1019 }
1020 Err(_) => false,
1021 }
1022 }
1023 }
1024
1025 pub async fn safe_allocate_deposit_address<M>(
1033 &self,
1034 extra_meta: M,
1035 ) -> anyhow::Result<DepositAddressInfo>
1036 where
1037 M: Serialize + MaybeSend + MaybeSync,
1038 {
1039 ensure!(
1040 self.supports_safe_deposit().await,
1041 "Wallet module consensus version doesn't support safe deposits",
1042 );
1043
1044 self.allocate_deposit_address_expert_only(extra_meta).await
1045 }
1046
1047 pub async fn allocate_deposit_address_expert_only<M>(
1065 &self,
1066 extra_meta: M,
1067 ) -> anyhow::Result<DepositAddressInfo>
1068 where
1069 M: Serialize + MaybeSend + MaybeSync,
1070 {
1071 let extra_meta_value =
1072 serde_json::to_value(extra_meta).expect("Failed to serialize extra meta");
1073 let deposit_address = self
1074 .db
1075 .autocommit(
1076 move |dbtx, _| {
1077 let extra_meta_value_inner = extra_meta_value.clone();
1078 Box::pin(async move {
1079 let deposit_address = self.allocate_deposit_address_inner(dbtx).await;
1080
1081 self.client_ctx
1082 .manual_operation_start_dbtx(
1083 dbtx,
1084 deposit_address.operation_id,
1085 WalletCommonInit::KIND.as_str(),
1086 WalletOperationMeta {
1087 variant: WalletOperationMetaVariant::Deposit {
1088 address: deposit_address.address.clone().into_unchecked(),
1089 tweak_idx: Some(deposit_address.tweak_idx),
1090 expires_at: None,
1091 },
1092 extra_meta: extra_meta_value_inner,
1093 },
1094 vec![],
1095 )
1096 .await?;
1097
1098 debug!(
1099 target: LOG_CLIENT_MODULE_WALLET,
1100 tweak_idx = %deposit_address.tweak_idx,
1101 address = %deposit_address.address,
1102 "Derived a new deposit address"
1103 );
1104
1105 self.rpc
1107 .watch_script_history(&deposit_address.address.script_pubkey())
1108 .await?;
1109
1110 let sender = self.pegin_monitor_wakeup_sender.clone();
1111 dbtx.on_commit(move || {
1112 sender.send_replace(());
1113 });
1114
1115 Ok(deposit_address)
1116 })
1117 },
1118 Some(100),
1119 )
1120 .await
1121 .map_err(|e| match e {
1122 AutocommitError::CommitFailed {
1123 last_error,
1124 attempts,
1125 } => anyhow!("Failed to commit after {attempts} attempts: {last_error}"),
1126 AutocommitError::ClosureError { error, .. } => error,
1127 })?;
1128
1129 Ok(deposit_address)
1130 }
1131
1132 pub async fn allocate_deposit_address_pooled_stateless(
1170 &self,
1171 max_gap_size: usize,
1172 ) -> anyhow::Result<MaybeNewAddress> {
1173 let max_gap_size_u64 = u64::try_from(max_gap_size).unwrap_or(u64::MAX);
1174 let extra_meta_value = serde_json::Value::Null;
1175 let result = self
1176 .db
1177 .autocommit(
1178 move |dbtx, _| {
1179 let extra_meta_value_inner = extra_meta_value.clone();
1180 Box::pin(async move {
1181 let unused = self.unused_pooled_deposit_addresses(dbtx).await;
1182
1183 if max_gap_size_u64 <= unused.len() as u64 && !unused.is_empty() {
1184 let addresses = unused
1185 .into_iter()
1186 .map(|(tweak_idx, data)| {
1187 let (_script, address, _key, operation_id) =
1188 self.data.derive_peg_in_script(tweak_idx);
1189
1190 debug_assert_eq!(operation_id, data.operation_id);
1191
1192 DepositAddressInfo {
1193 operation_id,
1194 address,
1195 tweak_idx,
1196 }
1197 })
1198 .collect();
1199
1200 return Ok::<_, anyhow::Error>(
1201 MaybeNewAddress::TooManyUnusedAddresses(addresses),
1202 );
1203 }
1204
1205 let deposit_address = self.allocate_deposit_address_inner(dbtx).await;
1206
1207 self.client_ctx
1208 .manual_operation_start_dbtx(
1209 dbtx,
1210 deposit_address.operation_id,
1211 WalletCommonInit::KIND.as_str(),
1212 WalletOperationMeta {
1213 variant: WalletOperationMetaVariant::Deposit {
1214 address: deposit_address.address.clone().into_unchecked(),
1215 tweak_idx: Some(deposit_address.tweak_idx),
1216 expires_at: None,
1217 },
1218 extra_meta: extra_meta_value_inner,
1219 },
1220 vec![],
1221 )
1222 .await?;
1223
1224 debug!(
1225 target: LOG_CLIENT_MODULE_WALLET,
1226 tweak_idx = %deposit_address.tweak_idx,
1227 address = %deposit_address.address,
1228 "Derived a new pooled deposit address"
1229 );
1230
1231 self.rpc
1232 .watch_script_history(&deposit_address.address.script_pubkey())
1233 .await?;
1234
1235 let sender = self.pegin_monitor_wakeup_sender.clone();
1236 dbtx.on_commit(move || {
1237 sender.send_replace(());
1238 });
1239
1240 Ok(MaybeNewAddress::NewAddress(deposit_address))
1241 })
1242 },
1243 Some(100),
1244 )
1245 .await
1246 .map_err(|e| match e {
1247 AutocommitError::CommitFailed {
1248 last_error,
1249 attempts,
1250 } => anyhow!("Failed to commit after {attempts} attempts: {last_error}"),
1251 AutocommitError::ClosureError { error, .. } => error,
1252 })?;
1253
1254 Ok(result)
1255 }
1256
1257 async fn unused_pooled_deposit_addresses(
1258 &self,
1259 dbtx: &mut DatabaseTransaction<'_>,
1260 ) -> Vec<(TweakIdx, PegInTweakIndexData)> {
1261 let mut unused: Vec<(TweakIdx, PegInTweakIndexData)> = dbtx
1266 .find_by_prefix_sorted_descending(&PegInTweakIndexPrefix)
1267 .await
1268 .take_while(|(_, d)| std::future::ready(d.claimed.is_empty()))
1269 .map(|(k, v)| (k.0, v))
1270 .collect()
1271 .await;
1272
1273 unused.sort_by_key(|(t, d)| (d.creation_time, *t));
1276 unused
1277 }
1278
1279 #[allow(clippy::too_many_lines)]
1302 pub async fn allocate_deposit_address_pooled(
1303 &self,
1304 max_gap_size: usize,
1305 ) -> anyhow::Result<(DepositAddressInfo, AllocateDepositOutcome)> {
1306 let stateless = self
1307 .allocate_deposit_address_pooled_stateless(max_gap_size)
1308 .await?;
1309
1310 let reused_addresses = match stateless {
1311 MaybeNewAddress::NewAddress(deposit_address) => {
1312 return Ok((deposit_address, AllocateDepositOutcome::Fresh));
1313 }
1314 MaybeNewAddress::TooManyUnusedAddresses(addresses) => addresses,
1315 };
1316
1317 let result = self
1318 .db
1319 .autocommit(
1320 move |dbtx, _| {
1321 let reused_addresses = reused_addresses.clone();
1322 Box::pin(async move {
1323 let cursor = dbtx
1324 .get_value(&PegInPoolCursorKey)
1325 .await
1326 .unwrap_or(TweakIdx(0));
1327
1328 let pick_pos = reused_addresses
1329 .iter()
1330 .position(|a| cursor <= a.tweak_idx)
1331 .unwrap_or(0);
1332 let reused_address = reused_addresses[pick_pos].clone();
1333
1334 let existing_tweak_idx = reused_address.tweak_idx;
1335 let existing = dbtx
1336 .get_value(&PegInTweakIndexKey(reused_address.tweak_idx))
1337 .await
1338 .with_context(|| {
1339 format!(
1340 "Pooled address disappeared while reusing {}",
1341 reused_address.tweak_idx
1342 )
1343 })?;
1344
1345 ensure!(
1346 existing.claimed.is_empty(),
1347 "Pooled address was used while reusing {}",
1348 reused_address.tweak_idx
1349 );
1350
1351 dbtx.insert_entry(&PegInPoolCursorKey, &reused_address.tweak_idx.next())
1352 .await;
1353
1354 let now = fedimint_core::time::now();
1362 dbtx.insert_entry(
1363 &PegInTweakIndexKey(reused_address.tweak_idx),
1364 &PegInTweakIndexData {
1365 creation_time: now,
1366 last_check_time: None,
1367 next_check_time: Some(now),
1368 operation_id: existing.operation_id,
1369 claimed: existing.claimed,
1370 },
1371 )
1372 .await;
1373
1374 let sender = self.pegin_monitor_wakeup_sender.clone();
1375 dbtx.on_commit(move || {
1376 sender.send_replace(());
1377 });
1378
1379 Ok::<_, anyhow::Error>((
1380 reused_address,
1381 AllocateDepositOutcome::Reused {
1382 original_tweak_idx: existing_tweak_idx,
1383 },
1384 ))
1385 })
1386 },
1387 Some(100),
1388 )
1389 .await
1390 .map_err(|e| match e {
1391 AutocommitError::CommitFailed {
1392 last_error,
1393 attempts,
1394 } => anyhow!("Failed to commit after {attempts} attempts: {last_error}"),
1395 AutocommitError::ClosureError { error, .. } => error,
1396 })?;
1397
1398 Ok(result)
1399 }
1400
1401 pub async fn subscribe_deposit(
1407 &self,
1408 operation_id: OperationId,
1409 ) -> anyhow::Result<UpdateStreamOrOutcome<DepositStateV2>> {
1410 let operation = self
1411 .client_ctx
1412 .get_operation(operation_id)
1413 .await
1414 .with_context(|| anyhow!("Operation not found: {}", operation_id.fmt_short()))?;
1415
1416 if operation.operation_module_kind() != WalletCommonInit::KIND.as_str() {
1417 bail!("Operation is not a wallet operation");
1418 }
1419
1420 let operation_meta = operation.meta::<WalletOperationMeta>();
1421
1422 let WalletOperationMetaVariant::Deposit {
1423 address, tweak_idx, ..
1424 } = operation_meta.variant
1425 else {
1426 bail!("Operation is not a deposit operation");
1427 };
1428
1429 let address = address.require_network(self.cfg().network.0)?;
1430
1431 let Some(tweak_idx) = tweak_idx else {
1433 let outcome_v1 = operation
1437 .outcome::<DepositStateV1>()
1438 .context("Old pending deposit, can't subscribe to updates")?;
1439
1440 let outcome_v2 = match outcome_v1 {
1441 DepositStateV1::Claimed(tx_info) => DepositStateV2::Claimed {
1442 btc_deposited: tx_info.btc_transaction.output[tx_info.out_idx as usize].value,
1443 btc_out_point: bitcoin::OutPoint {
1444 txid: tx_info.btc_transaction.compute_txid(),
1445 vout: tx_info.out_idx,
1446 },
1447 },
1448 DepositStateV1::Failed(error) => DepositStateV2::Failed(error),
1449 _ => bail!("Non-final outcome in operation log"),
1450 };
1451
1452 return Ok(UpdateStreamOrOutcome::Outcome(outcome_v2));
1453 };
1454
1455 Ok(self.client_ctx.outcome_or_updates(operation, operation_id, {
1456 let stream_rpc = self.rpc.clone();
1457 let stream_client_ctx = self.client_ctx.clone();
1458 let stream_script_pub_key = address.script_pubkey();
1459 move || {
1460
1461 stream! {
1462 yield DepositStateV2::WaitingForTransaction;
1463
1464 retry(
1465 "subscribe script history",
1466 background_backoff(),
1467 || stream_rpc.watch_script_history(&stream_script_pub_key)
1468 ).await.expect("Will never give up");
1469 let (btc_out_point, btc_deposited) = retry(
1470 "fetch history",
1471 background_backoff(),
1472 || async {
1473 let history = stream_rpc.get_script_history(&stream_script_pub_key).await?;
1474 history.first().and_then(|tx| {
1475 let (out_idx, amount) = tx.output
1476 .iter()
1477 .enumerate()
1478 .find_map(|(idx, output)| (output.script_pubkey == stream_script_pub_key).then_some((idx, output.value)))?;
1479 let txid = tx.compute_txid();
1480
1481 Some((
1482 bitcoin::OutPoint {
1483 txid,
1484 vout: out_idx as u32,
1485 },
1486 amount
1487 ))
1488 }).context("No deposit transaction found")
1489 }
1490 ).await.expect("Will never give up");
1491
1492 yield DepositStateV2::WaitingForConfirmation {
1493 btc_deposited,
1494 btc_out_point
1495 };
1496
1497 let claim_data = stream_client_ctx.module_db().wait_key_exists(&ClaimedPegInKey {
1498 peg_in_index: tweak_idx,
1499 btc_out_point,
1500 }).await;
1501
1502 yield DepositStateV2::Confirmed {
1503 btc_deposited,
1504 btc_out_point
1505 };
1506
1507 match stream_client_ctx.await_primary_module_outputs(operation_id, claim_data.change).await {
1508 Ok(()) => yield DepositStateV2::Claimed {
1509 btc_deposited,
1510 btc_out_point
1511 },
1512 Err(e) => yield DepositStateV2::Failed(e.to_string())
1513 }
1514 }
1515 }}))
1516 }
1517
1518 pub async fn list_peg_in_tweak_idxes(&self) -> BTreeMap<TweakIdx, PegInTweakIndexData> {
1519 self.client_ctx
1520 .module_db()
1521 .clone()
1522 .begin_transaction_nc()
1523 .await
1524 .find_by_prefix(&PegInTweakIndexPrefix)
1525 .await
1526 .map(|(key, data)| (key.0, data))
1527 .collect()
1528 .await
1529 }
1530
1531 pub async fn find_tweak_idx_by_address(
1532 &self,
1533 address: bitcoin::Address<NetworkUnchecked>,
1534 ) -> anyhow::Result<TweakIdx> {
1535 let data = self.data.clone();
1536 let Some((tweak_idx, _)) = self
1537 .db
1538 .begin_transaction_nc()
1539 .await
1540 .find_by_prefix(&PegInTweakIndexPrefix)
1541 .await
1542 .filter(|(k, _)| {
1543 let (_, derived_address, _tweak_key, _) = data.derive_peg_in_script(k.0);
1544 future::ready(derived_address.into_unchecked() == address)
1545 })
1546 .next()
1547 .await
1548 else {
1549 bail!("Address not found in the list of derived keys");
1550 };
1551
1552 Ok(tweak_idx.0)
1553 }
1554 pub async fn find_tweak_idx_by_operation_id(
1555 &self,
1556 operation_id: OperationId,
1557 ) -> anyhow::Result<TweakIdx> {
1558 Ok(self
1559 .client_ctx
1560 .module_db()
1561 .clone()
1562 .begin_transaction_nc()
1563 .await
1564 .find_by_prefix(&PegInTweakIndexPrefix)
1565 .await
1566 .filter(|(_k, v)| future::ready(v.operation_id == operation_id))
1567 .next()
1568 .await
1569 .ok_or_else(|| anyhow::format_err!("OperationId not found"))?
1570 .0
1571 .0)
1572 }
1573
1574 pub async fn get_pegin_tweak_idx(
1575 &self,
1576 tweak_idx: TweakIdx,
1577 ) -> anyhow::Result<PegInTweakIndexData> {
1578 self.client_ctx
1579 .module_db()
1580 .clone()
1581 .begin_transaction_nc()
1582 .await
1583 .get_value(&PegInTweakIndexKey(tweak_idx))
1584 .await
1585 .ok_or_else(|| anyhow::format_err!("TweakIdx not found"))
1586 }
1587
1588 pub async fn get_claimed_pegins(
1589 &self,
1590 dbtx: &mut DatabaseTransaction<'_>,
1591 tweak_idx: TweakIdx,
1592 ) -> Vec<(
1593 bitcoin::OutPoint,
1594 TransactionId,
1595 Vec<fedimint_core::OutPoint>,
1596 )> {
1597 let outpoints = dbtx
1598 .get_value(&PegInTweakIndexKey(tweak_idx))
1599 .await
1600 .map(|v| v.claimed)
1601 .unwrap_or_default();
1602
1603 let mut res = vec![];
1604
1605 for outpoint in outpoints {
1606 let claimed_peg_in_data = dbtx
1607 .get_value(&ClaimedPegInKey {
1608 peg_in_index: tweak_idx,
1609 btc_out_point: outpoint,
1610 })
1611 .await
1612 .expect("Must have a corresponding claim record");
1613 res.push((
1614 outpoint,
1615 claimed_peg_in_data.claim_txid,
1616 claimed_peg_in_data.change,
1617 ));
1618 }
1619
1620 res
1621 }
1622
1623 pub async fn recheck_pegin_address_by_op_id(
1625 &self,
1626 operation_id: OperationId,
1627 ) -> anyhow::Result<()> {
1628 let tweak_idx = self.find_tweak_idx_by_operation_id(operation_id).await?;
1629
1630 self.recheck_pegin_address(tweak_idx).await
1631 }
1632
1633 pub async fn recheck_pegin_address_by_address(
1635 &self,
1636 address: bitcoin::Address<NetworkUnchecked>,
1637 ) -> anyhow::Result<()> {
1638 self.recheck_pegin_address(self.find_tweak_idx_by_address(address).await?)
1639 .await
1640 }
1641
1642 pub async fn recheck_pegin_address(&self, tweak_idx: TweakIdx) -> anyhow::Result<()> {
1644 self.db
1645 .autocommit(
1646 |dbtx, _| {
1647 Box::pin(async {
1648 let db_key = PegInTweakIndexKey(tweak_idx);
1649 let db_val = dbtx
1650 .get_value(&db_key)
1651 .await
1652 .ok_or_else(|| anyhow::format_err!("DBKey not found"))?;
1653
1654 dbtx.insert_entry(
1655 &db_key,
1656 &PegInTweakIndexData {
1657 next_check_time: Some(fedimint_core::time::now()),
1658 ..db_val
1659 },
1660 )
1661 .await;
1662
1663 let sender = self.pegin_monitor_wakeup_sender.clone();
1664 dbtx.on_commit(move || {
1665 sender.send_replace(());
1666 });
1667
1668 Ok::<_, anyhow::Error>(())
1669 })
1670 },
1671 Some(100),
1672 )
1673 .await?;
1674
1675 Ok(())
1676 }
1677
1678 pub async fn await_num_deposits_by_operation_id(
1680 &self,
1681 operation_id: OperationId,
1682 num_deposits: usize,
1683 ) -> anyhow::Result<()> {
1684 let tweak_idx = self.find_tweak_idx_by_operation_id(operation_id).await?;
1685 self.await_num_deposits(tweak_idx, num_deposits).await
1686 }
1687
1688 pub async fn await_num_deposits_by_address(
1689 &self,
1690 address: bitcoin::Address<NetworkUnchecked>,
1691 num_deposits: usize,
1692 ) -> anyhow::Result<()> {
1693 self.await_num_deposits(self.find_tweak_idx_by_address(address).await?, num_deposits)
1694 .await
1695 }
1696
1697 #[instrument(target = LOG_CLIENT_MODULE_WALLET, skip_all, fields(tweak_idx=?tweak_idx, num_deposists=num_deposits))]
1698 pub async fn await_num_deposits(
1699 &self,
1700 tweak_idx: TweakIdx,
1701 num_deposits: usize,
1702 ) -> anyhow::Result<()> {
1703 let operation_id = self.get_pegin_tweak_idx(tweak_idx).await?.operation_id;
1704
1705 let mut receiver = self.pegin_claimed_receiver.clone();
1706 let mut backoff = backoff_util::aggressive_backoff();
1707
1708 loop {
1709 let pegins = self
1710 .get_claimed_pegins(
1711 &mut self.client_ctx.module_db().begin_transaction_nc().await,
1712 tweak_idx,
1713 )
1714 .await;
1715
1716 if pegins.len() < num_deposits {
1717 debug!(target: LOG_CLIENT_MODULE_WALLET, has=pegins.len(), "Not enough deposits");
1718 self.recheck_pegin_address(tweak_idx).await?;
1719 runtime::sleep(backoff.next().unwrap_or_default()).await;
1720 receiver.changed().await?;
1721 continue;
1722 }
1723
1724 debug!(target: LOG_CLIENT_MODULE_WALLET, has=pegins.len(), "Enough deposits detected");
1725
1726 for (_outpoint, transaction_id, change) in pegins {
1727 if transaction_id == TransactionId::from_byte_array([0; 32]) && change.is_empty() {
1728 debug!(target: LOG_CLIENT_MODULE_WALLET, "Deposited amount was too low, skipping");
1729 continue;
1730 }
1731
1732 debug!(target: LOG_CLIENT_MODULE_WALLET, out_points=?change, "Ensuring deposists claimed");
1733 let tx_subscriber = self.client_ctx.transaction_updates(operation_id).await;
1734
1735 if let Err(e) = tx_subscriber.await_tx_accepted(transaction_id).await {
1736 bail!("{e}");
1737 }
1738
1739 debug!(target: LOG_CLIENT_MODULE_WALLET, out_points=?change, "Ensuring outputs claimed");
1740 self.client_ctx
1741 .await_primary_module_outputs(operation_id, change)
1742 .await
1743 .expect("Cannot fail if tx was accepted and federation is honest");
1744 }
1745
1746 return Ok(());
1747 }
1748 }
1749
1750 pub async fn withdraw<M: Serialize + MaybeSend + MaybeSync>(
1755 &self,
1756 address: &bitcoin::Address,
1757 amount: bitcoin::Amount,
1758 fee: PegOutFees,
1759 extra_meta: M,
1760 ) -> anyhow::Result<OperationId> {
1761 {
1762 let operation_id = OperationId(thread_rng().r#gen());
1763
1764 let withdraw_output =
1765 self.create_withdraw_output(operation_id, address.clone(), amount, fee)?;
1766 let tx_builder = TransactionBuilder::new()
1767 .with_outputs(self.client_ctx.make_client_outputs(withdraw_output));
1768
1769 let extra_meta =
1770 serde_json::to_value(extra_meta).expect("Failed to serialize extra meta");
1771 self.client_ctx
1772 .finalize_and_submit_transaction(
1773 operation_id,
1774 WalletCommonInit::KIND.as_str(),
1775 {
1776 let address = address.clone();
1777 move |change_range: OutPointRange| WalletOperationMeta {
1778 variant: WalletOperationMetaVariant::Withdraw {
1779 address: address.clone().into_unchecked(),
1780 amount,
1781 fee,
1782 change: change_range.into_iter().collect(),
1783 },
1784 extra_meta: extra_meta.clone(),
1785 }
1786 },
1787 tx_builder,
1788 )
1789 .await?;
1790
1791 let mut dbtx = self.client_ctx.module_db().begin_transaction().await;
1792
1793 self.client_ctx
1794 .log_event(
1795 &mut dbtx,
1796 SendPaymentEvent {
1797 operation_id,
1798 amount: amount + fee.amount(),
1799 fee: fee.amount(),
1800 },
1801 )
1802 .await;
1803
1804 dbtx.commit_tx().await;
1805
1806 Ok(operation_id)
1807 }
1808 }
1809
1810 #[deprecated(
1815 since = "0.4.0",
1816 note = "RBF withdrawals are rejected by the federation"
1817 )]
1818 pub async fn rbf_withdraw<M: Serialize + MaybeSync + MaybeSend>(
1819 &self,
1820 rbf: Rbf,
1821 extra_meta: M,
1822 ) -> anyhow::Result<OperationId> {
1823 let operation_id = OperationId(thread_rng().r#gen());
1824
1825 let withdraw_output = self.create_rbf_withdraw_output(operation_id, &rbf)?;
1826 let tx_builder = TransactionBuilder::new()
1827 .with_outputs(self.client_ctx.make_client_outputs(withdraw_output));
1828
1829 let extra_meta = serde_json::to_value(extra_meta).expect("Failed to serialize extra meta");
1830 self.client_ctx
1831 .finalize_and_submit_transaction(
1832 operation_id,
1833 WalletCommonInit::KIND.as_str(),
1834 move |change_range: OutPointRange| WalletOperationMeta {
1835 variant: WalletOperationMetaVariant::RbfWithdraw {
1836 rbf: rbf.clone(),
1837 change: change_range.into_iter().collect(),
1838 },
1839 extra_meta: extra_meta.clone(),
1840 },
1841 tx_builder,
1842 )
1843 .await?;
1844
1845 Ok(operation_id)
1846 }
1847
1848 pub async fn subscribe_withdraw_updates(
1849 &self,
1850 operation_id: OperationId,
1851 ) -> anyhow::Result<UpdateStreamOrOutcome<WithdrawState>> {
1852 let operation = self
1853 .client_ctx
1854 .get_operation(operation_id)
1855 .await
1856 .with_context(|| anyhow!("Operation not found: {}", operation_id.fmt_short()))?;
1857
1858 if operation.operation_module_kind() != WalletCommonInit::KIND.as_str() {
1859 bail!("Operation is not a wallet operation");
1860 }
1861
1862 let operation_meta = operation.meta::<WalletOperationMeta>();
1863
1864 let (WalletOperationMetaVariant::Withdraw { change, .. }
1865 | WalletOperationMetaVariant::RbfWithdraw { change, .. }) = operation_meta.variant
1866 else {
1867 bail!("Operation is not a withdraw operation");
1868 };
1869
1870 let mut operation_stream = self.notifier.subscribe(operation_id).await;
1871 let client_ctx = self.client_ctx.clone();
1872
1873 Ok(self
1874 .client_ctx
1875 .outcome_or_updates(operation, operation_id, move || {
1876 stream! {
1877 match next_withdraw_state(&mut operation_stream).await {
1878 Some(WithdrawStates::Created(_)) => {
1879 yield WithdrawState::Created;
1880 },
1881 Some(s) => {
1882 panic!("Unexpected state {s:?}")
1883 },
1884 None => return,
1885 }
1886
1887 let _ = client_ctx
1892 .await_primary_module_outputs(operation_id, change)
1893 .await;
1894
1895
1896 match next_withdraw_state(&mut operation_stream).await {
1897 Some(WithdrawStates::Aborted(inner)) => {
1898 yield WithdrawState::Failed(inner.error);
1899 },
1900 Some(WithdrawStates::Success(inner)) => {
1901 yield WithdrawState::Succeeded(inner.txid);
1902 },
1903 Some(s) => {
1904 panic!("Unexpected state {s:?}")
1905 },
1906 None => {},
1907 }
1908 }
1909 }))
1910 }
1911
1912 fn admin_auth(&self) -> anyhow::Result<ApiAuth> {
1913 self.admin_auth
1914 .clone()
1915 .ok_or_else(|| anyhow::format_err!("Admin auth not set"))
1916 }
1917
1918 pub async fn activate_consensus_version_voting(&self) -> anyhow::Result<()> {
1919 self.module_api
1920 .activate_consensus_version_voting(self.admin_auth()?)
1921 .await?;
1922
1923 Ok(())
1924 }
1925}
1926
1927async fn poll_supports_safe_deposit_version(db: Database, module_api: DynModuleApi) {
1930 loop {
1931 let mut dbtx = db.begin_transaction().await;
1932
1933 if dbtx.get_value(&SupportsSafeDepositKey).await.is_some() {
1934 break;
1935 }
1936
1937 module_api.wait_for_initialized_connections().await;
1938
1939 if let Ok(module_consensus_version) = module_api.module_consensus_version().await
1940 && SAFE_DEPOSIT_MODULE_CONSENSUS_VERSION <= module_consensus_version
1941 {
1942 dbtx.insert_new_entry(&SupportsSafeDepositKey, &()).await;
1943 dbtx.commit_tx().await;
1944 break;
1945 }
1946
1947 drop(dbtx);
1948
1949 if is_running_in_test_env() {
1950 sleep(Duration::from_secs(10)).await;
1952 } else {
1953 sleep(Duration::from_hours(1)).await;
1954 }
1955 }
1956}
1957
1958async fn get_next_peg_in_tweak_child_id(dbtx: &mut DatabaseTransaction<'_>) -> TweakIdx {
1960 let index = dbtx
1961 .get_value(&NextPegInTweakIndexKey)
1962 .await
1963 .unwrap_or_default();
1964 dbtx.insert_entry(&NextPegInTweakIndexKey, &(index.next()))
1965 .await;
1966 index
1967}
1968
1969#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
1970pub enum WalletClientStates {
1971 Deposit(DepositStateMachine),
1972 Withdraw(WithdrawStateMachine),
1973}
1974
1975impl IntoDynInstance for WalletClientStates {
1976 type DynType = DynState;
1977
1978 fn into_dyn(self, instance_id: ModuleInstanceId) -> Self::DynType {
1979 DynState::from_typed(instance_id, self)
1980 }
1981}
1982
1983impl State for WalletClientStates {
1984 type ModuleContext = WalletClientContext;
1985
1986 fn transitions(
1987 &self,
1988 context: &Self::ModuleContext,
1989 global_context: &DynGlobalClientContext,
1990 ) -> Vec<StateTransition<Self>> {
1991 match self {
1992 WalletClientStates::Deposit(sm) => {
1993 sm_enum_variant_translation!(
1994 sm.transitions(context, global_context),
1995 WalletClientStates::Deposit
1996 )
1997 }
1998 WalletClientStates::Withdraw(sm) => {
1999 sm_enum_variant_translation!(
2000 sm.transitions(context, global_context),
2001 WalletClientStates::Withdraw
2002 )
2003 }
2004 }
2005 }
2006
2007 fn operation_id(&self) -> OperationId {
2008 match self {
2009 WalletClientStates::Deposit(sm) => sm.operation_id(),
2010 WalletClientStates::Withdraw(sm) => sm.operation_id(),
2011 }
2012 }
2013}
2014
2015#[cfg(all(test, not(target_family = "wasm")))]
2016mod tests {
2017 use std::collections::BTreeSet;
2018 use std::sync::atomic::{AtomicBool, Ordering};
2019
2020 use super::*;
2021 use crate::backup::{
2022 RECOVER_NUM_IDX_ADD_TO_LAST_USED, RecoverScanOutcome, recover_scan_idxes_for_activity,
2023 };
2024
2025 #[allow(clippy::too_many_lines)] #[tokio::test(flavor = "multi_thread")]
2027 async fn sanity_test_recover_inner() {
2028 {
2029 let last_checked = AtomicBool::new(false);
2030 let last_checked = &last_checked;
2031 assert_eq!(
2032 recover_scan_idxes_for_activity(
2033 TweakIdx(0),
2034 &BTreeSet::new(),
2035 |cur_idx| async move {
2036 Ok(match cur_idx {
2037 TweakIdx(9) => {
2038 last_checked.store(true, Ordering::SeqCst);
2039 vec![]
2040 }
2041 TweakIdx(10) => panic!("Shouldn't happen"),
2042 TweakIdx(11) => {
2043 vec![0usize] }
2045 _ => vec![],
2046 })
2047 }
2048 )
2049 .await
2050 .unwrap(),
2051 RecoverScanOutcome {
2052 last_used_idx: None,
2053 new_start_idx: TweakIdx(RECOVER_NUM_IDX_ADD_TO_LAST_USED),
2054 tweak_idxes_with_pegins: BTreeSet::from([])
2055 }
2056 );
2057 assert!(last_checked.load(Ordering::SeqCst));
2058 }
2059
2060 {
2061 let last_checked = AtomicBool::new(false);
2062 let last_checked = &last_checked;
2063 assert_eq!(
2064 recover_scan_idxes_for_activity(
2065 TweakIdx(0),
2066 &BTreeSet::from([TweakIdx(1), TweakIdx(2)]),
2067 |cur_idx| async move {
2068 Ok(match cur_idx {
2069 TweakIdx(1) => panic!("Shouldn't happen: already used (1)"),
2070 TweakIdx(2) => panic!("Shouldn't happen: already used (2)"),
2071 TweakIdx(11) => {
2072 last_checked.store(true, Ordering::SeqCst);
2073 vec![]
2074 }
2075 TweakIdx(12) => panic!("Shouldn't happen"),
2076 TweakIdx(13) => {
2077 vec![0usize] }
2079 _ => vec![],
2080 })
2081 }
2082 )
2083 .await
2084 .unwrap(),
2085 RecoverScanOutcome {
2086 last_used_idx: Some(TweakIdx(2)),
2087 new_start_idx: TweakIdx(2 + RECOVER_NUM_IDX_ADD_TO_LAST_USED),
2088 tweak_idxes_with_pegins: BTreeSet::from([])
2089 }
2090 );
2091 assert!(last_checked.load(Ordering::SeqCst));
2092 }
2093
2094 {
2095 let last_checked = AtomicBool::new(false);
2096 let last_checked = &last_checked;
2097 assert_eq!(
2098 recover_scan_idxes_for_activity(
2099 TweakIdx(10),
2100 &BTreeSet::new(),
2101 |cur_idx| async move {
2102 Ok(match cur_idx {
2103 TweakIdx(10) => vec![()],
2104 TweakIdx(19) => {
2105 last_checked.store(true, Ordering::SeqCst);
2106 vec![]
2107 }
2108 TweakIdx(20) => panic!("Shouldn't happen"),
2109 _ => vec![],
2110 })
2111 }
2112 )
2113 .await
2114 .unwrap(),
2115 RecoverScanOutcome {
2116 last_used_idx: Some(TweakIdx(10)),
2117 new_start_idx: TweakIdx(10 + RECOVER_NUM_IDX_ADD_TO_LAST_USED),
2118 tweak_idxes_with_pegins: BTreeSet::from([TweakIdx(10)])
2119 }
2120 );
2121 assert!(last_checked.load(Ordering::SeqCst));
2122 }
2123
2124 assert_eq!(
2125 recover_scan_idxes_for_activity(TweakIdx(0), &BTreeSet::new(), |cur_idx| async move {
2126 Ok(match cur_idx {
2127 TweakIdx(6 | 15) => vec![()],
2128 _ => vec![],
2129 })
2130 })
2131 .await
2132 .unwrap(),
2133 RecoverScanOutcome {
2134 last_used_idx: Some(TweakIdx(15)),
2135 new_start_idx: TweakIdx(15 + RECOVER_NUM_IDX_ADD_TO_LAST_USED),
2136 tweak_idxes_with_pegins: BTreeSet::from([TweakIdx(6), TweakIdx(15)])
2137 }
2138 );
2139 assert_eq!(
2140 recover_scan_idxes_for_activity(TweakIdx(10), &BTreeSet::new(), |cur_idx| async move {
2141 Ok(match cur_idx {
2142 TweakIdx(8) => {
2143 vec![()] }
2145 TweakIdx(9) => {
2146 panic!("Shouldn't happen")
2147 }
2148 _ => vec![],
2149 })
2150 })
2151 .await
2152 .unwrap(),
2153 RecoverScanOutcome {
2154 last_used_idx: None,
2155 new_start_idx: TweakIdx(9 + RECOVER_NUM_IDX_ADD_TO_LAST_USED),
2156 tweak_idxes_with_pegins: BTreeSet::from([])
2157 }
2158 );
2159 assert_eq!(
2160 recover_scan_idxes_for_activity(TweakIdx(10), &BTreeSet::new(), |cur_idx| async move {
2161 Ok(match cur_idx {
2162 TweakIdx(9) => panic!("Shouldn't happen"),
2163 TweakIdx(15) => vec![()],
2164 _ => vec![],
2165 })
2166 })
2167 .await
2168 .unwrap(),
2169 RecoverScanOutcome {
2170 last_used_idx: Some(TweakIdx(15)),
2171 new_start_idx: TweakIdx(15 + RECOVER_NUM_IDX_ADD_TO_LAST_USED),
2172 tweak_idxes_with_pegins: BTreeSet::from([TweakIdx(15)])
2173 }
2174 );
2175 }
2176}