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
8#[cfg(feature = "uniffi")]
9::uniffi::setup_scaffolding!();
10
11pub mod api;
12#[cfg(feature = "cli")]
13mod cli;
14
15mod backup;
16
17pub mod client_db;
18mod deposit;
21pub mod error;
23pub mod events;
24use events::SendPaymentEvent;
25#[cfg(feature = "uniffi")]
26pub mod ffi;
27mod pegin_monitor;
29mod withdraw;
30
31use std::collections::{BTreeMap, BTreeSet};
32use std::future;
33use std::sync::Arc;
34use std::time::{Duration, SystemTime};
35
36use async_stream::{stream, try_stream};
37use backup::WalletModuleBackup;
38use bitcoin::address::NetworkUnchecked;
39use bitcoin::secp256k1::{All, SECP256K1, Secp256k1};
40use bitcoin::{Address, Network, ScriptBuf};
41use client_db::{DbKeyPrefix, PegInTweakIndexKey, SupportsSafeDepositKey, TweakIdx};
42use fedimint_api_client::api::{DynModuleApi, FederationResult};
43use fedimint_bitcoind::{
44 BitcoinRpcError, BitcoindTracked, DynBitcoindRpc, IBitcoindRpc, create_esplora_rpc,
45};
46use fedimint_client_module::error::{ClientModuleError, TransactionSubmitError};
47use fedimint_client_module::module::init::{
48 ClientModuleInit, ClientModuleInitArgs, ClientModuleRecoverArgs, RecoveryMode,
49};
50use fedimint_client_module::module::recovery::RecoveryProgress;
51use fedimint_client_module::module::{ClientContext, ClientModule, IClientModule, OutPointRange};
52use fedimint_client_module::oplog::UpdateStreamOrOutcome;
53use fedimint_client_module::sm::{Context, DynState, ModuleNotifier, State, StateTransition};
54use fedimint_client_module::transaction::{
55 ClientOutput, ClientOutputBundle, ClientOutputSM, FeeQuote, FeeQuoteRequest,
56 TransactionBuilder, max_affordable_send_amount,
57};
58use fedimint_client_module::{DynGlobalClientContext, sm_enum_variant_translation};
59use fedimint_core::core::{Decoder, IntoDynInstance, ModuleInstanceId, ModuleKind, OperationId};
60use fedimint_core::db::{
61 Committable, Database, DatabaseError, DatabaseTransaction, IDatabaseTransactionOpsCoreTyped,
62};
63use fedimint_core::encoding::{Decodable, Encodable};
64use fedimint_core::envs::{BitcoinRpcConfig, is_running_in_test_env};
65use fedimint_core::module::{
66 Amounts, ApiAuth, ApiVersion, CommonModuleInit, ModuleCommon, ModuleConsensusVersion,
67 ModuleInit, MultiApiVersion,
68};
69use fedimint_core::task::{MaybeSend, MaybeSync, TaskGroup, sleep};
70use fedimint_core::util::backoff_util::background_backoff;
71use fedimint_core::util::{BoxStream, FmtCompact as _, backoff_util, retry};
72use fedimint_core::{
73 BitcoinHash, OutPoint, TransactionId, apply, async_trait_maybe_send, push_db_pair_items,
74 runtime, secp256k1,
75};
76use fedimint_derive_secret::{ChildId, DerivableSecret};
77use fedimint_logging::LOG_CLIENT_MODULE_WALLET;
78pub use fedimint_wallet_common as common;
79use fedimint_wallet_common::config::{FeeConsensus, WalletClientConfig};
80use fedimint_wallet_common::tweakable::Tweakable;
81pub use fedimint_wallet_common::*;
82use futures::{Stream, StreamExt, TryStreamExt as _};
83use rand::{Rng, thread_rng};
84use secp256k1::Keypair;
85use serde::{Deserialize, Serialize};
86use strum::IntoEnumIterator;
87use tokio::sync::watch;
88use tracing::{debug, instrument, warn};
89
90use crate::api::WalletFederationApi;
91use crate::backup::{FEDERATION_RECOVER_MAX_GAP, RecoveryStateV2, WalletRecovery};
92use crate::client_db::{
93 ClaimedPegInData, ClaimedPegInKey, ClaimedPegInPrefix, NextPegInTweakIndexKey,
94 PegInPoolCursorKey, PegInTweakIndexData, PegInTweakIndexPrefix, RecoveryFinalizedKey,
95 RecoveryStateKey, SupportsSafeDepositPrefix,
96};
97use crate::deposit::DepositStateMachine;
98pub use crate::error::{
99 ConsensusVersionVotingError, DepositAddressError, MaxWithdrawableAmountError, PegInError,
100 PegOutError, SubscribeDepositError, SubscribeWithdrawError, WithdrawFeesError,
101};
102use crate::withdraw::{CreatedWithdrawState, WithdrawStateMachine, WithdrawStates};
103
104const WALLET_TWEAK_CHILD_ID: ChildId = ChildId(0);
105
106#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
107pub struct BitcoinTransactionData {
108 pub btc_transaction: bitcoin::Transaction,
111 pub out_idx: u32,
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
116pub enum DepositStateV1 {
117 WaitingForTransaction,
118 WaitingForConfirmation(BitcoinTransactionData),
119 Confirmed(BitcoinTransactionData),
120 Claimed(BitcoinTransactionData),
121 Failed(String),
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
125#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
126pub enum DepositStateV2 {
127 WaitingForTransaction,
128 WaitingForConfirmation {
129 #[serde(with = "bitcoin::amount::serde::as_sat")]
130 btc_deposited: bitcoin::Amount,
131 btc_out_point: bitcoin::OutPoint,
132 },
133 Confirmed {
134 #[serde(with = "bitcoin::amount::serde::as_sat")]
135 btc_deposited: bitcoin::Amount,
136 btc_out_point: bitcoin::OutPoint,
137 },
138 Claimed {
139 #[serde(with = "bitcoin::amount::serde::as_sat")]
140 btc_deposited: bitcoin::Amount,
141 btc_out_point: bitcoin::OutPoint,
142 },
143 Failed(String),
144}
145
146#[derive(Debug, Clone, PartialEq, Eq)]
148pub struct DepositAddressInfo {
149 pub operation_id: OperationId,
150 pub address: Address,
151 pub tweak_idx: TweakIdx,
152}
153
154#[allow(clippy::enum_variant_names)]
160#[derive(Debug, Clone, PartialEq, Eq)]
161pub enum MaybeNewAddress {
162 NewAddress(DepositAddressInfo),
164 TooManyUnusedAddresses(Vec<DepositAddressInfo>),
168}
169
170#[derive(Debug, Clone, Copy, PartialEq, Eq)]
176pub enum AllocateDepositOutcome {
177 Fresh,
179 Reused { original_tweak_idx: TweakIdx },
183}
184
185#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
186#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
187pub enum WithdrawState {
188 Created,
189 Succeeded(bitcoin::Txid),
190 Failed(String),
191 }
195
196async fn next_withdraw_state<S>(stream: &mut S) -> Option<WithdrawStates>
197where
198 S: Stream<Item = WalletClientStates> + Unpin,
199{
200 loop {
201 if let WalletClientStates::Withdraw(ds) = stream.next().await? {
202 return Some(ds.state);
203 }
204 tokio::task::yield_now().await;
205 }
206}
207
208#[derive(Debug, Clone, Default)]
209pub struct WalletClientInit(pub Option<DynBitcoindRpc>);
211
212const SLICE_SIZE: u64 = 1000;
213
214impl WalletClientInit {
215 pub fn new(rpc: DynBitcoindRpc) -> Self {
216 Self(Some(rpc))
217 }
218
219 async fn recover_from_slices(
220 &self,
221 args: &ClientModuleRecoverArgs<Self>,
222 total_items: u64,
223 ) -> Option<fedimint_core::Amount> {
224 let data = WalletClientModuleData {
225 cfg: args.cfg().clone(),
226 module_root_secret: args.module_root_secret().clone(),
227 };
228
229 let mut state = RecoveryStateV2::new();
230
231 state.refill_pending_pool_up_to(&data, TweakIdx(FEDERATION_RECOVER_MAX_GAP));
232
233 for start in (0..total_items).step_by(SLICE_SIZE as usize) {
234 let end = std::cmp::min(start + SLICE_SIZE, total_items);
235
236 let items = args.module_api().fetch_recovery_slice(start, end).await;
237
238 for item in &items {
239 match item {
240 RecoveryItem::Input { outpoint, script } => {
241 state.handle_item(*outpoint, script, &data);
242 }
243 }
244 }
245
246 args.update_recovery_progress(RecoveryProgress {
247 complete: end.try_into().unwrap_or(u32::MAX),
248 total: total_items.try_into().unwrap_or(u32::MAX),
249 });
250 }
251
252 let mut dbtx = args.db().begin_transaction().await;
253
254 for tweak_idx in 0..state.new_start_idx().0 {
255 let operation_id = data.derive_peg_in_script(TweakIdx(tweak_idx)).3;
256
257 let claimed = state
258 .claimed_outpoints
259 .get(&TweakIdx(tweak_idx))
260 .cloned()
261 .unwrap_or_default();
262
263 dbtx.insert_new_entry(
264 &PegInTweakIndexKey(TweakIdx(tweak_idx)),
265 &PegInTweakIndexData {
266 operation_id,
267 creation_time: fedimint_core::time::now(),
268 last_check_time: None,
269 next_check_time: Some(fedimint_core::time::now()),
270 claimed,
271 },
272 )
273 .await;
274 }
275
276 dbtx.insert_new_entry(&NextPegInTweakIndexKey, &state.new_start_idx())
277 .await;
278
279 dbtx.commit_tx().await;
280
281 None
285 }
286}
287
288impl ModuleInit for WalletClientInit {
289 type Common = WalletCommonInit;
290
291 async fn dump_database(
292 &self,
293 dbtx: &mut DatabaseTransaction<'_>,
294 prefix_names: Vec<String>,
295 ) -> Box<dyn Iterator<Item = (String, Box<dyn erased_serde::Serialize + Send>)> + '_> {
296 let mut wallet_client_items: BTreeMap<String, Box<dyn erased_serde::Serialize + Send>> =
297 BTreeMap::new();
298 let filtered_prefixes = DbKeyPrefix::iter().filter(|f| {
299 prefix_names.is_empty() || prefix_names.contains(&f.to_string().to_lowercase())
300 });
301
302 for table in filtered_prefixes {
303 match table {
304 DbKeyPrefix::NextPegInTweakIndex => {
305 if let Some(index) = dbtx.get_value(&NextPegInTweakIndexKey).await {
306 wallet_client_items
307 .insert("NextPegInTweakIndex".to_string(), Box::new(index));
308 }
309 }
310 DbKeyPrefix::PegInTweakIndex => {
311 push_db_pair_items!(
312 dbtx,
313 PegInTweakIndexPrefix,
314 PegInTweakIndexKey,
315 PegInTweakIndexData,
316 wallet_client_items,
317 "Peg-In Tweak Index"
318 );
319 }
320 DbKeyPrefix::ClaimedPegIn => {
321 push_db_pair_items!(
322 dbtx,
323 ClaimedPegInPrefix,
324 ClaimedPegInKey,
325 ClaimedPegInData,
326 wallet_client_items,
327 "Claimed Peg-In"
328 );
329 }
330 DbKeyPrefix::RecoveryFinalized => {
331 if let Some(val) = dbtx.get_value(&RecoveryFinalizedKey).await {
332 wallet_client_items.insert("RecoveryFinalized".to_string(), Box::new(val));
333 }
334 }
335 DbKeyPrefix::SupportsSafeDeposit => {
336 push_db_pair_items!(
337 dbtx,
338 SupportsSafeDepositPrefix,
339 SupportsSafeDepositKey,
340 (),
341 wallet_client_items,
342 "Supports Safe Deposit"
343 );
344 }
345 DbKeyPrefix::PegInPoolCursor => {
346 if let Some(cursor) = dbtx.get_value(&PegInPoolCursorKey).await {
347 wallet_client_items.insert("PegInPoolCursor".to_string(), Box::new(cursor));
348 }
349 }
350 DbKeyPrefix::RecoveryState
351 | DbKeyPrefix::ExternalReservedStart
352 | DbKeyPrefix::CoreInternalReservedStart
353 | DbKeyPrefix::CoreInternalReservedEnd => {}
354 }
355 }
356
357 Box::new(wallet_client_items.into_iter())
358 }
359}
360
361#[apply(async_trait_maybe_send!)]
362impl ClientModuleInit for WalletClientInit {
363 type Module = WalletClientModule;
364
365 fn supported_api_versions(&self) -> MultiApiVersion {
366 MultiApiVersion::try_from_iter([ApiVersion { major: 0, minor: 0 }])
367 .expect("no version conflicts")
368 }
369
370 async fn init(
371 &self,
372 args: &ClientModuleInitArgs<Self>,
373 ) -> Result<Self::Module, ClientModuleError> {
374 let data = WalletClientModuleData {
375 cfg: args.cfg().clone(),
376 module_root_secret: args.module_root_secret().clone(),
377 };
378
379 let db = args.db().clone();
380
381 let rpc_config = WalletClientModule::get_rpc_config(args.cfg());
382
383 let btc_rpc = if let Some(user_rpc) = args.user_bitcoind_rpc() {
390 user_rpc.clone()
391 } else if let Some(factory) = args.user_bitcoind_rpc_no_chain_id() {
392 if let Some(rpc) = factory(rpc_config.url.clone()).await {
393 rpc
394 } else {
395 self.0.clone().unwrap_or(
396 create_esplora_rpc(&rpc_config.url).map_err(ClientModuleError::other)?,
397 )
398 }
399 } else {
400 self.0
401 .clone()
402 .unwrap_or(create_esplora_rpc(&rpc_config.url).map_err(ClientModuleError::other)?)
403 };
404 let btc_rpc = BitcoindTracked::new(btc_rpc, "wallet-client").into_dyn();
405
406 let module_api = args.module_api().clone();
407
408 let (pegin_claimed_sender, pegin_claimed_receiver) = watch::channel(());
409 let (pegin_monitor_wakeup_sender, pegin_monitor_wakeup_receiver) = watch::channel(());
410
411 Ok(WalletClientModule {
412 db,
413 data,
414 module_api,
415 notifier: args.notifier().clone(),
416 rpc: btc_rpc,
417 client_ctx: args.context(),
418 pegin_monitor_wakeup_sender,
419 pegin_monitor_wakeup_receiver,
420 pegin_claimed_receiver,
421 pegin_claimed_sender,
422 task_group: args.task_group().clone(),
423 client_span: args.client_span().clone(),
424 admin_auth: args.admin_auth().cloned(),
425 })
426 }
427
428 fn recovery_mode(&self) -> RecoveryMode {
429 RecoveryMode::Unusable
430 }
431
432 async fn recover(
437 &self,
438 args: &ClientModuleRecoverArgs<Self>,
439 snapshot: Option<&<Self::Module as ClientModule>::Backup>,
440 ) -> Result<Option<fedimint_core::Amount>, ClientModuleError> {
441 if args
444 .db()
445 .begin_transaction_nc()
446 .await
447 .get_value(&RecoveryStateKey)
448 .await
449 .is_some()
450 {
451 return args
452 .recover_from_history::<WalletRecovery>(self, snapshot)
453 .await;
454 }
455
456 match args.module_api().fetch_recovery_count().await {
460 Ok(total_items) => Ok(self.recover_from_slices(args, total_items).await),
461 Err(_) => {
462 args.recover_from_history::<WalletRecovery>(self, snapshot)
463 .await
464 }
465 }
466 }
467
468 fn used_db_prefixes(&self) -> Option<BTreeSet<u8>> {
469 Some(
470 DbKeyPrefix::iter()
471 .map(|p| p as u8)
472 .chain(
473 DbKeyPrefix::ExternalReservedStart as u8
474 ..=DbKeyPrefix::CoreInternalReservedEnd as u8,
475 )
476 .collect(),
477 )
478 }
479}
480
481#[derive(Debug, Clone, Serialize, Deserialize)]
482pub struct WalletOperationMeta {
483 pub variant: WalletOperationMetaVariant,
484 pub extra_meta: serde_json::Value,
485}
486
487#[derive(Debug, Clone, Serialize, Deserialize)]
488#[serde(rename_all = "snake_case")]
489pub enum WalletOperationMetaVariant {
490 Deposit {
491 address: Address<NetworkUnchecked>,
492 #[serde(default)]
497 tweak_idx: Option<TweakIdx>,
498 #[serde(default, skip_serializing_if = "Option::is_none")]
499 expires_at: Option<SystemTime>,
500 },
501 Withdraw {
502 address: Address<NetworkUnchecked>,
503 #[serde(with = "bitcoin::amount::serde::as_sat")]
504 amount: bitcoin::Amount,
505 fee: PegOutFees,
506 change: Vec<OutPoint>,
507 },
508
509 RbfWithdraw {
510 rbf: Rbf,
511 change: Vec<OutPoint>,
512 },
513}
514
515#[derive(Debug, Clone)]
517pub struct WalletClientModuleData {
518 cfg: WalletClientConfig,
519 module_root_secret: DerivableSecret,
520}
521
522impl WalletClientModuleData {
523 fn derive_deposit_address(
524 &self,
525 idx: TweakIdx,
526 ) -> (Keypair, secp256k1::PublicKey, Address, OperationId) {
527 let idx = ChildId(idx.0);
528
529 let secret_tweak_key = self
530 .module_root_secret
531 .child_key(WALLET_TWEAK_CHILD_ID)
532 .child_key(idx)
533 .to_secp_key(fedimint_core::secp256k1::SECP256K1);
534
535 let public_tweak_key = secret_tweak_key.public_key();
536
537 let address = self
538 .cfg
539 .peg_in_descriptor
540 .tweak(&public_tweak_key, bitcoin::secp256k1::SECP256K1)
541 .address(self.cfg.network.0)
542 .unwrap();
543
544 let operation_id = OperationId(public_tweak_key.x_only_public_key().0.serialize());
546
547 (secret_tweak_key, public_tweak_key, address, operation_id)
548 }
549
550 fn derive_peg_in_script(
551 &self,
552 idx: TweakIdx,
553 ) -> (ScriptBuf, bitcoin::Address, Keypair, OperationId) {
554 let (secret_tweak_key, _, address, operation_id) = self.derive_deposit_address(idx);
555
556 (
557 self.cfg
558 .peg_in_descriptor
559 .tweak(&secret_tweak_key.public_key(), SECP256K1)
560 .script_pubkey(),
561 address,
562 secret_tweak_key,
563 operation_id,
564 )
565 }
566}
567
568#[derive(Debug)]
569#[cfg_attr(feature = "uniffi", derive(uniffi::Object))]
570pub struct WalletClientModule {
571 data: WalletClientModuleData,
572 db: Database,
573 module_api: DynModuleApi,
574 notifier: ModuleNotifier<WalletClientStates>,
575 rpc: DynBitcoindRpc,
576 client_ctx: ClientContext<Self>,
577 pegin_monitor_wakeup_sender: watch::Sender<()>,
579 pegin_monitor_wakeup_receiver: watch::Receiver<()>,
580 pegin_claimed_sender: watch::Sender<()>,
582 pegin_claimed_receiver: watch::Receiver<()>,
583 task_group: TaskGroup,
584 client_span: tracing::Span,
585 admin_auth: Option<ApiAuth>,
586}
587
588#[apply(async_trait_maybe_send!)]
589impl ClientModule for WalletClientModule {
590 type Init = WalletClientInit;
591 type Common = WalletModuleTypes;
592 type Backup = WalletModuleBackup;
593 type ModuleStateMachineContext = WalletClientContext;
594 type States = WalletClientStates;
595
596 fn context(&self) -> Self::ModuleStateMachineContext {
597 WalletClientContext {
598 rpc: self.rpc.clone(),
599 wallet_descriptor: self.cfg().peg_in_descriptor.clone(),
600 wallet_decoder: self.decoder(),
601 secp: Secp256k1::default(),
602 client_ctx: self.client_ctx.clone(),
603 }
604 }
605
606 async fn start(&self) {
607 self.task_group
608 .spawn_cancellable_with_span(self.client_span.clone(), "peg-in monitor", {
609 let client_ctx = self.client_ctx.clone();
610 let db = self.db.clone();
611 let btc_rpc = self.rpc.clone();
612 let module_api = self.module_api.clone();
613 let data = self.data.clone();
614 let pegin_claimed_sender = self.pegin_claimed_sender.clone();
615 let pegin_monitor_wakeup_receiver = self.pegin_monitor_wakeup_receiver.clone();
616 pegin_monitor::run_peg_in_monitor(
617 client_ctx,
618 db,
619 btc_rpc,
620 module_api,
621 data,
622 pegin_claimed_sender,
623 pegin_monitor_wakeup_receiver,
624 )
625 });
626
627 self.task_group.spawn_cancellable_with_span(
628 self.client_span.clone(),
629 "supports-safe-deposit-version",
630 {
631 let db = self.db.clone();
632 let module_api = self.module_api.clone();
633
634 poll_supports_safe_deposit_version(db, module_api)
635 },
636 );
637 }
638
639 fn supports_backup(&self) -> bool {
640 true
641 }
642
643 async fn backup(&self) -> Result<backup::WalletModuleBackup, ClientModuleError> {
644 let session_count = self
646 .client_ctx
647 .global_api()
648 .session_count()
649 .await
650 .map_err(ClientModuleError::other)?;
651
652 let mut dbtx = self.db.begin_transaction_nc().await;
653 let next_pegin_tweak_idx = dbtx
654 .get_value(&NextPegInTweakIndexKey)
655 .await
656 .unwrap_or_default();
657 let claimed = dbtx
658 .find_by_prefix(&PegInTweakIndexPrefix)
659 .await
660 .filter_map(|(k, v)| async move {
661 if v.claimed.is_empty() {
662 None
663 } else {
664 Some(k.0)
665 }
666 })
667 .collect()
668 .await;
669 Ok(backup::WalletModuleBackup::new_v1(
670 session_count,
671 next_pegin_tweak_idx,
672 claimed,
673 ))
674 }
675
676 fn input_fee(
677 &self,
678 _amount: &Amounts,
679 _input: &<Self::Common as ModuleCommon>::Input,
680 ) -> Option<Amounts> {
681 Some(Amounts::new_bitcoin(self.cfg().fee_consensus.peg_in_abs))
682 }
683
684 fn output_fee(
685 &self,
686 _amount: &Amounts,
687 _output: &<Self::Common as ModuleCommon>::Output,
688 ) -> Option<Amounts> {
689 Some(Amounts::new_bitcoin(self.cfg().fee_consensus.peg_out_abs))
690 }
691
692 async fn handle_rpc(
693 &self,
694 method: String,
695 request: serde_json::Value,
696 ) -> BoxStream<'_, Result<serde_json::Value, ClientModuleError>> {
697 let stream: BoxStream<'_, Result<serde_json::Value, RpcError>> = Box::pin(try_stream! {
698 match method.as_str() {
699 "get_wallet_summary" => {
700 let _req: WalletSummaryRequest = serde_json::from_value(request)?;
701 let wallet_summary = self.get_wallet_summary()
702 .await
703 .expect("Failed to fetch wallet summary");
704 let result = serde_json::to_value(&wallet_summary)
705 .expect("Serialization error");
706 yield result;
707 }
708 "get_block_count_local" => {
709 let block_count = self.get_block_count_local().await
710 .expect("Failed to fetch block count");
711 yield serde_json::to_value(block_count)?;
712 }
713 "peg_in" => {
714 let req: PegInRequest = serde_json::from_value(request)?;
715 let response = self.peg_in(req)
716 .await
717 .map_err(RpcError::PegIn)?;
718 let result = serde_json::to_value(&response)?;
719 yield result;
720 },
721 "peg_out" => {
722 let req: PegOutRequest = serde_json::from_value(request)?;
723 let response = self.peg_out(req)
724 .await
725 .map_err(RpcError::PegOut)?;
726 let result = serde_json::to_value(&response)?;
727 yield result;
728 },
729 "subscribe_deposit" => {
730 let req: SubscribeDepositRequest = serde_json::from_value(request)?;
731 for await state in self.subscribe_deposit(req.operation_id).await?.into_stream() {
732 yield serde_json::to_value(state)?;
733 }
734 },
735 "subscribe_withdraw" => {
736 let req: SubscribeWithdrawRequest = serde_json::from_value(request)?;
737 for await state in self.subscribe_withdraw_updates(req.operation_id).await?.into_stream(){
738 yield serde_json::to_value(state)?;
739 }
740 }
741 _ => {
742 Err(RpcError::UnknownMethod { method: method.clone() })?;
743 }
744 }
745 });
746 Box::pin(stream.map_err(ClientModuleError::other))
747 }
748
749 #[cfg(feature = "cli")]
750 async fn handle_cli_command(
751 &self,
752 args: &[std::ffi::OsString],
753 ) -> Result<serde_json::Value, ClientModuleError> {
754 cli::handle_cli_command(self, args)
755 .await
756 .map_err(ClientModuleError::other)
757 }
758}
759
760#[derive(Debug, thiserror::Error)]
762enum RpcError {
763 #[error(transparent)]
766 Json(#[from] serde_json::Error),
767
768 #[error("peg_in failed")]
770 PegIn(#[source] DepositAddressError),
771
772 #[error("peg_out failed")]
774 PegOut(#[source] PegOutError),
775
776 #[error(transparent)]
778 SubscribeDeposit(#[from] SubscribeDepositError),
779
780 #[error(transparent)]
782 SubscribeWithdraw(#[from] SubscribeWithdrawError),
783
784 #[error("Unknown method: {method}")]
786 UnknownMethod { method: String },
787}
788
789#[derive(Deserialize)]
790struct WalletSummaryRequest {}
791
792#[derive(Debug, Clone)]
793pub struct WalletClientContext {
794 rpc: DynBitcoindRpc,
795 wallet_descriptor: PegInDescriptor,
796 wallet_decoder: Decoder,
797 secp: Secp256k1<All>,
798 pub client_ctx: ClientContext<WalletClientModule>,
799}
800
801#[derive(Debug, Clone, Serialize, Deserialize)]
802pub struct PegInRequest {
803 pub extra_meta: serde_json::Value,
804}
805
806#[cfg(feature = "uniffi")]
807uniffi::custom_type!(PegInRequest, String, {
808 lower: |v| serde_json::to_string(&v).expect("PegInRequest serialization cannot fail"),
809 try_lift: |s| serde_json::from_str::<PegInRequest>(&s)
810 .map_err(|e| uniffi::deps::anyhow::anyhow!("Failed to parse PegInRequest: {e}")),
811});
812
813#[derive(Deserialize)]
814struct SubscribeDepositRequest {
815 operation_id: OperationId,
816}
817
818#[derive(Deserialize)]
819struct SubscribeWithdrawRequest {
820 operation_id: OperationId,
821}
822
823#[derive(Debug, Clone, Serialize, Deserialize)]
824#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
825pub struct PegInResponse {
826 pub deposit_address: Address<NetworkUnchecked>,
827 pub operation_id: OperationId,
828}
829
830#[derive(Debug, Clone, Serialize, Deserialize)]
831#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
832pub struct PegOutRequest {
833 pub amount_sat: u64,
834 pub destination_address: Address<NetworkUnchecked>,
835 pub extra_meta: serde_json::Value,
836}
837
838#[derive(Debug, Clone, Serialize, Deserialize)]
839#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
840pub struct PegOutResponse {
841 pub operation_id: OperationId,
842}
843
844impl Context for WalletClientContext {
845 const KIND: Option<ModuleKind> = Some(KIND);
846}
847
848impl WalletClientModule {
849 fn cfg(&self) -> &WalletClientConfig {
850 &self.data.cfg
851 }
852
853 fn get_rpc_config(cfg: &WalletClientConfig) -> BitcoinRpcConfig {
854 match BitcoinRpcConfig::get_defaults_from_env_vars() {
855 Ok(rpc_config) => {
856 if rpc_config.kind == "bitcoind" {
859 cfg.default_bitcoin_rpc.clone()
860 } else {
861 rpc_config
862 }
863 }
864 _ => cfg.default_bitcoin_rpc.clone(),
865 }
866 }
867
868 pub fn get_network(&self) -> Network {
869 self.cfg().network.0
870 }
871
872 pub fn get_finality_delay(&self) -> u32 {
873 self.cfg().finality_delay
874 }
875
876 pub fn get_fee_consensus(&self) -> FeeConsensus {
877 self.cfg().fee_consensus
878 }
879
880 async fn allocate_deposit_address_inner(
881 &self,
882 dbtx: &mut DatabaseTransaction<'_>,
883 ) -> DepositAddressInfo {
884 dbtx.ensure_isolated().expect("Must be isolated db");
885
886 let tweak_idx = get_next_peg_in_tweak_child_id(dbtx).await;
887 let (_secret_tweak_key, _, address, operation_id) =
888 self.data.derive_deposit_address(tweak_idx);
889
890 let now = fedimint_core::time::now();
891
892 dbtx.insert_new_entry(
893 &PegInTweakIndexKey(tweak_idx),
894 &PegInTweakIndexData {
895 creation_time: now,
896 next_check_time: Some(now),
897 last_check_time: None,
898 operation_id,
899 claimed: vec![],
900 },
901 )
902 .await;
903
904 DepositAddressInfo {
905 operation_id,
906 address,
907 tweak_idx,
908 }
909 }
910
911 pub async fn get_withdraw_fees(
918 &self,
919 address: &bitcoin::Address,
920 amount: bitcoin::Amount,
921 ) -> Result<PegOutFees, WithdrawFeesError> {
922 self.module_api
923 .fetch_peg_out_fees(address, amount)
924 .await?
925 .ok_or(WithdrawFeesError::NoQuote)
926 }
927
928 pub async fn send_fee_quote(
943 &self,
944 amount: bitcoin::Amount,
945 ) -> Result<FeeQuote, TransactionSubmitError> {
946 let amount = fedimint_core::Amount::from_sats(amount.to_sat());
947 self.client_ctx
948 .fee_quote(
949 OperationId::new_random(),
950 FeeQuoteRequest {
951 input_amount: Amounts::ZERO,
952 output_amount: Amounts::new_bitcoin(amount),
953 input_fee: Amounts::ZERO,
954 output_fee: Amounts::new_bitcoin(self.cfg().fee_consensus.peg_out_abs),
955 },
956 )
957 .await
958 }
959
960 pub async fn max_withdrawable_amount(
995 &self,
996 address: &bitcoin::Address,
997 balance: fedimint_core::Amount,
998 ) -> Result<(bitcoin::Amount, PegOutFees), MaxWithdrawableAmountError> {
999 let max_fees = self
1002 .get_withdraw_fees(
1003 address,
1004 bitcoin::Amount::from_sat(balance.sats_round_down()),
1005 )
1006 .await?;
1007 let max_fee_msats = fedimint_core::Amount::from_sats(max_fees.amount().to_sat());
1008 let dust_limit = address.script_pubkey().minimal_non_dust();
1009
1010 let max = max_affordable_send_amount(
1011 balance,
1012 fedimint_core::Amount::from_sats(dust_limit.to_sat()),
1013 balance,
1014 |amount: fedimint_core::Amount| {
1018 fedimint_core::Amount::from_sats(amount.msats.div_ceil(1000)) + max_fee_msats
1019 },
1020 |funded: fedimint_core::Amount| {
1021 self.send_fee_quote(bitcoin::Amount::from_sat(funded.msats / 1000))
1022 },
1023 )
1024 .await
1025 .map_err(MaxWithdrawableAmountError::Quote)?
1026 .ok_or(MaxWithdrawableAmountError::BalanceTooLow {
1027 balance,
1028 dust_limit,
1029 })?;
1030
1031 let amount = bitcoin::Amount::from_sat(max.msats.div_ceil(1000));
1034
1035 let fees = self.get_withdraw_fees(address, amount).await?;
1040
1041 Ok((amount, fees))
1042 }
1043
1044 pub async fn get_wallet_summary(&self) -> FederationResult<WalletSummary> {
1046 self.module_api.fetch_wallet_summary().await
1047 }
1048
1049 pub async fn get_block_count_local(&self) -> FederationResult<u32> {
1050 self.module_api.fetch_block_count_local().await
1051 }
1052
1053 pub fn create_withdraw_output(
1054 &self,
1055 operation_id: OperationId,
1056 address: bitcoin::Address,
1057 amount: bitcoin::Amount,
1058 fees: PegOutFees,
1059 ) -> ClientOutputBundle<WalletOutput, WalletClientStates> {
1060 let output = WalletOutput::new_v0_peg_out(address, amount, fees);
1061
1062 let amount = output.maybe_v0_ref().expect("v0 output").amount().into();
1063
1064 let sm_gen = move |out_point_range: OutPointRange| {
1065 assert_eq!(out_point_range.count(), 1);
1066 let out_idx = out_point_range.start_idx();
1067 vec![WalletClientStates::Withdraw(WithdrawStateMachine {
1068 operation_id,
1069 state: WithdrawStates::Created(CreatedWithdrawState {
1070 fm_outpoint: OutPoint {
1071 txid: out_point_range.txid(),
1072 out_idx,
1073 },
1074 }),
1075 })]
1076 };
1077
1078 ClientOutputBundle::new(
1079 vec![ClientOutput::<WalletOutput> {
1080 output,
1081 amounts: Amounts::new_bitcoin(amount),
1082 }],
1083 vec![ClientOutputSM::<WalletClientStates> {
1084 state_machines: Arc::new(sm_gen),
1085 }],
1086 )
1087 }
1088
1089 pub async fn peg_in(&self, req: PegInRequest) -> Result<PegInResponse, DepositAddressError> {
1090 let deposit_address = self.safe_allocate_deposit_address(req.extra_meta).await?;
1091
1092 Ok(PegInResponse {
1093 deposit_address: deposit_address.address.into_unchecked(),
1094 operation_id: deposit_address.operation_id,
1095 })
1096 }
1097
1098 pub async fn peg_out(&self, req: PegOutRequest) -> Result<PegOutResponse, PegOutError> {
1099 let amount = bitcoin::Amount::from_sat(req.amount_sat);
1100 let network = self.get_network();
1101 let destination = req
1102 .destination_address
1103 .require_network(network)
1104 .map_err(|_| PegOutError::WrongNetwork { expected: network })?;
1105
1106 let fees = self.get_withdraw_fees(&destination, amount).await?;
1107 let operation_id = self
1108 .withdraw(&destination, amount, fees, req.extra_meta)
1109 .await?;
1110
1111 Ok(PegOutResponse { operation_id })
1112 }
1113
1114 pub fn create_rbf_withdraw_output(
1115 &self,
1116 operation_id: OperationId,
1117 rbf: &Rbf,
1118 ) -> ClientOutputBundle<WalletOutput, WalletClientStates> {
1119 let output = WalletOutput::new_v0_rbf(rbf.fees, rbf.txid);
1120
1121 let amount = output.maybe_v0_ref().expect("v0 output").amount().into();
1122
1123 let sm_gen = move |out_point_range: OutPointRange| {
1124 assert_eq!(out_point_range.count(), 1);
1125 let out_idx = out_point_range.start_idx();
1126 vec![WalletClientStates::Withdraw(WithdrawStateMachine {
1127 operation_id,
1128 state: WithdrawStates::Created(CreatedWithdrawState {
1129 fm_outpoint: OutPoint {
1130 txid: out_point_range.txid(),
1131 out_idx,
1132 },
1133 }),
1134 })]
1135 };
1136
1137 ClientOutputBundle::new(
1138 vec![ClientOutput::<WalletOutput> {
1139 output,
1140 amounts: Amounts::new_bitcoin(amount),
1141 }],
1142 vec![ClientOutputSM::<WalletClientStates> {
1143 state_machines: Arc::new(sm_gen),
1144 }],
1145 )
1146 }
1147
1148 pub async fn btc_tx_has_no_size_limit(&self) -> FederationResult<bool> {
1149 Ok(self.module_api.module_consensus_version().await? >= ModuleConsensusVersion::new(2, 2))
1150 }
1151
1152 pub async fn supports_safe_deposit(&self) -> bool {
1161 if supports_safe_deposit_verified(&self.db).await {
1162 return true;
1163 }
1164
1165 verify_supports_safe_deposit(&self.db, &self.module_api)
1166 .await
1167 .unwrap_or(false)
1168 }
1169
1170 pub async fn safe_allocate_deposit_address<M>(
1178 &self,
1179 extra_meta: M,
1180 ) -> Result<DepositAddressInfo, DepositAddressError>
1181 where
1182 M: Serialize + MaybeSend + MaybeSync,
1183 {
1184 if !self.supports_safe_deposit().await {
1185 return Err(DepositAddressError::SafeDepositUnverified);
1186 }
1187
1188 self.allocate_deposit_address_expert_only(extra_meta).await
1189 }
1190
1191 pub async fn allocate_deposit_address_expert_only<M>(
1209 &self,
1210 extra_meta: M,
1211 ) -> Result<DepositAddressInfo, DepositAddressError>
1212 where
1213 M: Serialize + MaybeSend + MaybeSync,
1214 {
1215 let extra_meta_value =
1216 serde_json::to_value(extra_meta).expect("Failed to serialize extra meta");
1217 let deposit_address = self
1218 .db
1219 .autocommit(
1220 move |dbtx, _| {
1221 let extra_meta_value_inner = extra_meta_value.clone();
1222 Box::pin(async move {
1223 let deposit_address = self.allocate_deposit_address_inner(dbtx).await;
1224
1225 self.client_ctx
1226 .manual_operation_start_dbtx(
1227 dbtx,
1228 deposit_address.operation_id,
1229 WalletCommonInit::KIND.as_str(),
1230 WalletOperationMeta {
1231 variant: WalletOperationMetaVariant::Deposit {
1232 address: deposit_address.address.clone().into_unchecked(),
1233 tweak_idx: Some(deposit_address.tweak_idx),
1234 expires_at: None,
1235 },
1236 extra_meta: extra_meta_value_inner,
1237 },
1238 vec![],
1239 )
1240 .await?;
1241
1242 debug!(
1243 target: LOG_CLIENT_MODULE_WALLET,
1244 tweak_idx = %deposit_address.tweak_idx,
1245 address = %deposit_address.address,
1246 "Derived a new deposit address"
1247 );
1248
1249 self.rpc
1251 .watch_script_history(&deposit_address.address.script_pubkey())
1252 .await?;
1253
1254 let sender = self.pegin_monitor_wakeup_sender.clone();
1255 dbtx.on_commit(move || {
1256 sender.send_replace(());
1257 });
1258
1259 Ok(deposit_address)
1260 })
1261 },
1262 Some(100),
1263 )
1264 .await?;
1265
1266 Ok(deposit_address)
1267 }
1268
1269 pub async fn allocate_deposit_address_pooled_stateless(
1307 &self,
1308 max_gap_size: usize,
1309 ) -> Result<MaybeNewAddress, DepositAddressError> {
1310 let max_gap_size_u64 = u64::try_from(max_gap_size).unwrap_or(u64::MAX);
1311 let extra_meta_value = serde_json::Value::Null;
1312 let result = self
1313 .db
1314 .autocommit(
1315 move |dbtx, _| {
1316 let extra_meta_value_inner = extra_meta_value.clone();
1317 Box::pin(async move {
1318 let unused = self.unused_pooled_deposit_addresses(dbtx).await;
1319
1320 if max_gap_size_u64 <= unused.len() as u64 && !unused.is_empty() {
1321 let addresses = unused
1322 .into_iter()
1323 .map(|(tweak_idx, data)| {
1324 let (_script, address, _key, operation_id) =
1325 self.data.derive_peg_in_script(tweak_idx);
1326
1327 debug_assert_eq!(operation_id, data.operation_id);
1328
1329 DepositAddressInfo {
1330 operation_id,
1331 address,
1332 tweak_idx,
1333 }
1334 })
1335 .collect();
1336
1337 return Ok::<_, DepositAddressError>(
1338 MaybeNewAddress::TooManyUnusedAddresses(addresses),
1339 );
1340 }
1341
1342 let deposit_address = self.allocate_deposit_address_inner(dbtx).await;
1343
1344 self.client_ctx
1345 .manual_operation_start_dbtx(
1346 dbtx,
1347 deposit_address.operation_id,
1348 WalletCommonInit::KIND.as_str(),
1349 WalletOperationMeta {
1350 variant: WalletOperationMetaVariant::Deposit {
1351 address: deposit_address.address.clone().into_unchecked(),
1352 tweak_idx: Some(deposit_address.tweak_idx),
1353 expires_at: None,
1354 },
1355 extra_meta: extra_meta_value_inner,
1356 },
1357 vec![],
1358 )
1359 .await?;
1360
1361 debug!(
1362 target: LOG_CLIENT_MODULE_WALLET,
1363 tweak_idx = %deposit_address.tweak_idx,
1364 address = %deposit_address.address,
1365 "Derived a new pooled deposit address"
1366 );
1367
1368 self.rpc
1369 .watch_script_history(&deposit_address.address.script_pubkey())
1370 .await?;
1371
1372 let sender = self.pegin_monitor_wakeup_sender.clone();
1373 dbtx.on_commit(move || {
1374 sender.send_replace(());
1375 });
1376
1377 Ok(MaybeNewAddress::NewAddress(deposit_address))
1378 })
1379 },
1380 Some(100),
1381 )
1382 .await?;
1383
1384 Ok(result)
1385 }
1386
1387 async fn unused_pooled_deposit_addresses(
1388 &self,
1389 dbtx: &mut DatabaseTransaction<'_>,
1390 ) -> Vec<(TweakIdx, PegInTweakIndexData)> {
1391 let mut unused: Vec<(TweakIdx, PegInTweakIndexData)> = dbtx
1396 .find_by_prefix_sorted_descending(&PegInTweakIndexPrefix)
1397 .await
1398 .take_while(|(_, d)| std::future::ready(d.claimed.is_empty()))
1399 .map(|(k, v)| (k.0, v))
1400 .collect()
1401 .await;
1402
1403 unused.sort_by_key(|(t, d)| (d.creation_time, *t));
1406 unused
1407 }
1408
1409 #[allow(clippy::too_many_lines)]
1432 pub async fn allocate_deposit_address_pooled(
1433 &self,
1434 max_gap_size: usize,
1435 ) -> Result<(DepositAddressInfo, AllocateDepositOutcome), DepositAddressError> {
1436 let stateless = self
1437 .allocate_deposit_address_pooled_stateless(max_gap_size)
1438 .await?;
1439
1440 let reused_addresses = match stateless {
1441 MaybeNewAddress::NewAddress(deposit_address) => {
1442 return Ok((deposit_address, AllocateDepositOutcome::Fresh));
1443 }
1444 MaybeNewAddress::TooManyUnusedAddresses(addresses) => addresses,
1445 };
1446
1447 let result = self
1448 .db
1449 .autocommit(
1450 move |dbtx, _| {
1451 let reused_addresses = reused_addresses.clone();
1452 Box::pin(async move {
1453 let cursor = dbtx
1454 .get_value(&PegInPoolCursorKey)
1455 .await
1456 .unwrap_or(TweakIdx(0));
1457
1458 let pick_pos = reused_addresses
1459 .iter()
1460 .position(|a| cursor <= a.tweak_idx)
1461 .unwrap_or(0);
1462 let reused_address = reused_addresses[pick_pos].clone();
1463
1464 let existing_tweak_idx = reused_address.tweak_idx;
1465 let existing = dbtx
1466 .get_value(&PegInTweakIndexKey(reused_address.tweak_idx))
1467 .await
1468 .ok_or(DepositAddressError::PooledAddressDisappeared {
1469 tweak_idx: reused_address.tweak_idx,
1470 })?;
1471
1472 if !existing.claimed.is_empty() {
1473 return Err(DepositAddressError::PooledAddressUsed {
1474 tweak_idx: reused_address.tweak_idx,
1475 });
1476 }
1477
1478 dbtx.insert_entry(&PegInPoolCursorKey, &reused_address.tweak_idx.next())
1479 .await;
1480
1481 let now = fedimint_core::time::now();
1489 dbtx.insert_entry(
1490 &PegInTweakIndexKey(reused_address.tweak_idx),
1491 &PegInTweakIndexData {
1492 creation_time: now,
1493 last_check_time: None,
1494 next_check_time: Some(now),
1495 operation_id: existing.operation_id,
1496 claimed: existing.claimed,
1497 },
1498 )
1499 .await;
1500
1501 let sender = self.pegin_monitor_wakeup_sender.clone();
1502 dbtx.on_commit(move || {
1503 sender.send_replace(());
1504 });
1505
1506 Ok::<_, DepositAddressError>((
1507 reused_address,
1508 AllocateDepositOutcome::Reused {
1509 original_tweak_idx: existing_tweak_idx,
1510 },
1511 ))
1512 })
1513 },
1514 Some(100),
1515 )
1516 .await?;
1517
1518 Ok(result)
1519 }
1520
1521 pub async fn subscribe_deposit(
1527 &self,
1528 operation_id: OperationId,
1529 ) -> Result<UpdateStreamOrOutcome<DepositStateV2>, SubscribeDepositError> {
1530 let operation = self.client_ctx.get_operation(operation_id).await?;
1531
1532 let operation_meta = operation.meta::<WalletOperationMeta>();
1533
1534 let WalletOperationMetaVariant::Deposit {
1535 address, tweak_idx, ..
1536 } = operation_meta.variant
1537 else {
1538 return Err(SubscribeDepositError::NotADeposit);
1539 };
1540
1541 let network = self.cfg().network.0;
1542 let address = address
1543 .require_network(network)
1544 .map_err(|_| SubscribeDepositError::WrongNetwork { expected: network })?;
1545
1546 let Some(tweak_idx) = tweak_idx else {
1548 let outcome_v1 = operation
1552 .outcome::<DepositStateV1>()
1553 .ok_or(SubscribeDepositError::OldPendingDeposit)?;
1554
1555 let outcome_v2 = match outcome_v1 {
1556 DepositStateV1::Claimed(tx_info) => DepositStateV2::Claimed {
1557 btc_deposited: tx_info.btc_transaction.output[tx_info.out_idx as usize].value,
1558 btc_out_point: bitcoin::OutPoint {
1559 txid: tx_info.btc_transaction.compute_txid(),
1560 vout: tx_info.out_idx,
1561 },
1562 },
1563 DepositStateV1::Failed(error) => DepositStateV2::Failed(error),
1564 _ => return Err(SubscribeDepositError::NonFinalOutcome),
1565 };
1566
1567 return Ok(UpdateStreamOrOutcome::Outcome(outcome_v2));
1568 };
1569
1570 Ok(self.client_ctx.outcome_or_updates(
1571 &operation,
1572 operation_id,
1573 |state| match state {
1574 DepositStateV2::WaitingForTransaction
1575 | DepositStateV2::WaitingForConfirmation { .. }
1576 | DepositStateV2::Confirmed { .. } => false,
1577 DepositStateV2::Claimed { .. } | DepositStateV2::Failed(_) => true,
1578 },
1579 {
1580 let stream_rpc = self.rpc.clone();
1581 let stream_client_ctx = self.client_ctx.clone();
1582 let stream_script_pub_key = address.script_pubkey();
1583 move || {
1584
1585 stream! {
1586 yield DepositStateV2::WaitingForTransaction;
1587
1588 retry(
1589 "subscribe script history",
1590 background_backoff(),
1591 || stream_rpc.watch_script_history(&stream_script_pub_key)
1592 ).await.expect("Will never give up");
1593 let (btc_out_point, btc_deposited) = retry(
1594 "fetch history",
1595 background_backoff(),
1596 || async {
1597 let history = stream_rpc.get_script_history(&stream_script_pub_key).await?;
1598 history.first().and_then(|tx| {
1599 let (out_idx, amount) = tx.output
1600 .iter()
1601 .enumerate()
1602 .find_map(|(idx, output)| (output.script_pubkey == stream_script_pub_key).then_some((idx, output.value)))?;
1603 let txid = tx.compute_txid();
1604
1605 Some((
1606 bitcoin::OutPoint {
1607 txid,
1608 vout: out_idx as u32,
1609 },
1610 amount
1611 ))
1612 }).ok_or(FetchDepositTransactionError::NotFound)
1613 }
1614 ).await.expect("Will never give up");
1615
1616 yield DepositStateV2::WaitingForConfirmation {
1617 btc_deposited,
1618 btc_out_point
1619 };
1620
1621 let claim_data = stream_client_ctx.module_db().wait_key_exists(&ClaimedPegInKey {
1622 peg_in_index: tweak_idx,
1623 btc_out_point,
1624 }).await;
1625
1626 yield DepositStateV2::Confirmed {
1627 btc_deposited,
1628 btc_out_point
1629 };
1630
1631 match stream_client_ctx.await_primary_module_outputs(operation_id, claim_data.change).await {
1632 Ok(()) => yield DepositStateV2::Claimed {
1633 btc_deposited,
1634 btc_out_point
1635 },
1636 Err(e) => yield DepositStateV2::Failed(e.fmt_compact().to_string())
1637 }
1638 }
1639 }}))
1640 }
1641
1642 pub async fn list_peg_in_tweak_idxes(&self) -> BTreeMap<TweakIdx, PegInTweakIndexData> {
1643 self.client_ctx
1644 .module_db()
1645 .clone()
1646 .begin_transaction_nc()
1647 .await
1648 .find_by_prefix(&PegInTweakIndexPrefix)
1649 .await
1650 .map(|(key, data)| (key.0, data))
1651 .collect()
1652 .await
1653 }
1654
1655 pub async fn find_tweak_idx_by_address(
1656 &self,
1657 address: bitcoin::Address<NetworkUnchecked>,
1658 ) -> Result<TweakIdx, PegInError> {
1659 let data = self.data.clone();
1660 let Some((tweak_idx, _)) = self
1661 .db
1662 .begin_transaction_nc()
1663 .await
1664 .find_by_prefix(&PegInTweakIndexPrefix)
1665 .await
1666 .filter(|(k, _)| {
1667 let (_, derived_address, _tweak_key, _) = data.derive_peg_in_script(k.0);
1668 future::ready(derived_address.into_unchecked() == address)
1669 })
1670 .next()
1671 .await
1672 else {
1673 return Err(PegInError::AddressNotDerived);
1674 };
1675
1676 Ok(tweak_idx.0)
1677 }
1678 pub async fn find_tweak_idx_by_operation_id(
1679 &self,
1680 operation_id: OperationId,
1681 ) -> Result<TweakIdx, PegInError> {
1682 Ok(self
1683 .client_ctx
1684 .module_db()
1685 .clone()
1686 .begin_transaction_nc()
1687 .await
1688 .find_by_prefix(&PegInTweakIndexPrefix)
1689 .await
1690 .filter(|(_k, v)| future::ready(v.operation_id == operation_id))
1691 .next()
1692 .await
1693 .ok_or(PegInError::NoAddressForOperation { operation_id })?
1694 .0
1695 .0)
1696 }
1697
1698 pub async fn get_pegin_tweak_idx(
1699 &self,
1700 tweak_idx: TweakIdx,
1701 ) -> Result<PegInTweakIndexData, PegInError> {
1702 self.client_ctx
1703 .module_db()
1704 .clone()
1705 .begin_transaction_nc()
1706 .await
1707 .get_value(&PegInTweakIndexKey(tweak_idx))
1708 .await
1709 .ok_or(PegInError::TweakIdxNotFound { tweak_idx })
1710 }
1711
1712 pub async fn get_claimed_pegins(
1713 &self,
1714 dbtx: &mut DatabaseTransaction<'_>,
1715 tweak_idx: TweakIdx,
1716 ) -> Vec<(
1717 bitcoin::OutPoint,
1718 TransactionId,
1719 Vec<fedimint_core::OutPoint>,
1720 )> {
1721 let outpoints = dbtx
1722 .get_value(&PegInTweakIndexKey(tweak_idx))
1723 .await
1724 .map(|v| v.claimed)
1725 .unwrap_or_default();
1726
1727 let mut res = vec![];
1728
1729 for outpoint in outpoints {
1730 let claimed_peg_in_data = dbtx
1731 .get_value(&ClaimedPegInKey {
1732 peg_in_index: tweak_idx,
1733 btc_out_point: outpoint,
1734 })
1735 .await
1736 .expect("Must have a corresponding claim record");
1737 res.push((
1738 outpoint,
1739 claimed_peg_in_data.claim_txid,
1740 claimed_peg_in_data.change,
1741 ));
1742 }
1743
1744 res
1745 }
1746
1747 pub async fn recheck_pegin_address_by_op_id(
1749 &self,
1750 operation_id: OperationId,
1751 ) -> Result<(), PegInError> {
1752 let tweak_idx = self.find_tweak_idx_by_operation_id(operation_id).await?;
1753
1754 self.recheck_pegin_address(tweak_idx).await
1755 }
1756
1757 pub async fn recheck_pegin_address_by_address(
1759 &self,
1760 address: bitcoin::Address<NetworkUnchecked>,
1761 ) -> Result<(), PegInError> {
1762 self.recheck_pegin_address(self.find_tweak_idx_by_address(address).await?)
1763 .await
1764 }
1765
1766 pub async fn recheck_pegin_address(&self, tweak_idx: TweakIdx) -> Result<(), PegInError> {
1768 self.db
1769 .autocommit(
1770 |dbtx, _| {
1771 Box::pin(async {
1772 let db_key = PegInTweakIndexKey(tweak_idx);
1773 let db_val = dbtx
1774 .get_value(&db_key)
1775 .await
1776 .ok_or(PegInError::TweakIdxNotFound { tweak_idx })?;
1777
1778 dbtx.insert_entry(
1779 &db_key,
1780 &PegInTweakIndexData {
1781 next_check_time: Some(fedimint_core::time::now()),
1782 ..db_val
1783 },
1784 )
1785 .await;
1786
1787 let sender = self.pegin_monitor_wakeup_sender.clone();
1788 dbtx.on_commit(move || {
1789 sender.send_replace(());
1790 });
1791
1792 Ok::<_, PegInError>(())
1793 })
1794 },
1795 Some(100),
1796 )
1797 .await?;
1798
1799 Ok(())
1800 }
1801
1802 pub async fn await_num_deposits_by_operation_id(
1804 &self,
1805 operation_id: OperationId,
1806 num_deposits: usize,
1807 ) -> Result<(), PegInError> {
1808 let tweak_idx = self.find_tweak_idx_by_operation_id(operation_id).await?;
1809 self.await_num_deposits(tweak_idx, num_deposits).await
1810 }
1811
1812 pub async fn await_num_deposits_by_address(
1813 &self,
1814 address: bitcoin::Address<NetworkUnchecked>,
1815 num_deposits: usize,
1816 ) -> Result<(), PegInError> {
1817 self.await_num_deposits(self.find_tweak_idx_by_address(address).await?, num_deposits)
1818 .await
1819 }
1820
1821 #[instrument(target = LOG_CLIENT_MODULE_WALLET, skip_all, fields(tweak_idx=?tweak_idx, num_deposists=num_deposits))]
1822 pub async fn await_num_deposits(
1823 &self,
1824 tweak_idx: TweakIdx,
1825 num_deposits: usize,
1826 ) -> Result<(), PegInError> {
1827 let operation_id = self.get_pegin_tweak_idx(tweak_idx).await?.operation_id;
1828
1829 let mut receiver = self.pegin_claimed_receiver.clone();
1830 let mut backoff = backoff_util::aggressive_backoff();
1831
1832 loop {
1833 let pegins = self
1834 .get_claimed_pegins(
1835 &mut self.client_ctx.module_db().begin_transaction_nc().await,
1836 tweak_idx,
1837 )
1838 .await;
1839
1840 if pegins.len() < num_deposits {
1841 debug!(target: LOG_CLIENT_MODULE_WALLET, has=pegins.len(), "Not enough deposits");
1842 self.recheck_pegin_address(tweak_idx).await?;
1843 runtime::sleep(backoff.next().unwrap_or_default()).await;
1844 receiver
1845 .changed()
1846 .await
1847 .map_err(|_| PegInError::MonitorStopped)?;
1848 continue;
1849 }
1850
1851 debug!(target: LOG_CLIENT_MODULE_WALLET, has=pegins.len(), "Enough deposits detected");
1852
1853 for (_outpoint, transaction_id, change) in pegins {
1854 if transaction_id == TransactionId::from_byte_array([0; 32]) && change.is_empty() {
1855 debug!(target: LOG_CLIENT_MODULE_WALLET, "Deposited amount was too low, skipping");
1856 continue;
1857 }
1858
1859 debug!(target: LOG_CLIENT_MODULE_WALLET, out_points=?change, "Ensuring deposists claimed");
1860 let tx_subscriber = self.client_ctx.transaction_updates(operation_id).await;
1861
1862 if let Err(reason) = tx_subscriber.await_tx_accepted(transaction_id).await {
1863 return Err(PegInError::TransactionRejected { reason });
1864 }
1865
1866 debug!(target: LOG_CLIENT_MODULE_WALLET, out_points=?change, "Ensuring outputs claimed");
1867 self.client_ctx
1868 .await_primary_module_outputs(operation_id, change)
1869 .await
1870 .expect("Cannot fail if tx was accepted and federation is honest");
1871 }
1872
1873 return Ok(());
1874 }
1875 }
1876
1877 pub async fn withdraw<M: Serialize + MaybeSend + MaybeSync>(
1882 &self,
1883 address: &bitcoin::Address,
1884 amount: bitcoin::Amount,
1885 fee: PegOutFees,
1886 extra_meta: M,
1887 ) -> Result<OperationId, TransactionSubmitError> {
1888 {
1889 let operation_id = OperationId(thread_rng().r#gen());
1890
1891 let withdraw_output =
1892 self.create_withdraw_output(operation_id, address.clone(), amount, fee);
1893 let tx_builder = TransactionBuilder::new()
1894 .with_outputs(self.client_ctx.make_client_outputs(withdraw_output));
1895
1896 let extra_meta =
1897 serde_json::to_value(extra_meta).expect("Failed to serialize extra meta");
1898 self.client_ctx
1899 .finalize_and_submit_transaction(
1900 operation_id,
1901 WalletCommonInit::KIND.as_str(),
1902 {
1903 let address = address.clone();
1904 move |change_range: OutPointRange| WalletOperationMeta {
1905 variant: WalletOperationMetaVariant::Withdraw {
1906 address: address.clone().into_unchecked(),
1907 amount,
1908 fee,
1909 change: change_range.into_iter().collect(),
1910 },
1911 extra_meta: extra_meta.clone(),
1912 }
1913 },
1914 tx_builder,
1915 )
1916 .await?;
1917
1918 let mut dbtx = self.client_ctx.module_db().begin_transaction().await;
1919
1920 self.client_ctx
1921 .log_event(
1922 &mut dbtx,
1923 SendPaymentEvent {
1924 operation_id,
1925 amount: amount + fee.amount(),
1926 fee: fee.amount(),
1927 },
1928 )
1929 .await;
1930
1931 dbtx.commit_tx().await;
1932
1933 Ok(operation_id)
1934 }
1935 }
1936
1937 #[deprecated(
1942 since = "0.4.0",
1943 note = "RBF withdrawals are rejected by the federation"
1944 )]
1945 pub async fn rbf_withdraw<M: Serialize + MaybeSync + MaybeSend>(
1946 &self,
1947 rbf: Rbf,
1948 extra_meta: M,
1949 ) -> Result<OperationId, TransactionSubmitError> {
1950 let operation_id = OperationId(thread_rng().r#gen());
1951
1952 let withdraw_output = self.create_rbf_withdraw_output(operation_id, &rbf);
1953 let tx_builder = TransactionBuilder::new()
1954 .with_outputs(self.client_ctx.make_client_outputs(withdraw_output));
1955
1956 let extra_meta = serde_json::to_value(extra_meta).expect("Failed to serialize extra meta");
1957 self.client_ctx
1958 .finalize_and_submit_transaction(
1959 operation_id,
1960 WalletCommonInit::KIND.as_str(),
1961 move |change_range: OutPointRange| WalletOperationMeta {
1962 variant: WalletOperationMetaVariant::RbfWithdraw {
1963 rbf: rbf.clone(),
1964 change: change_range.into_iter().collect(),
1965 },
1966 extra_meta: extra_meta.clone(),
1967 },
1968 tx_builder,
1969 )
1970 .await?;
1971
1972 Ok(operation_id)
1973 }
1974
1975 pub async fn subscribe_withdraw_updates(
1976 &self,
1977 operation_id: OperationId,
1978 ) -> Result<UpdateStreamOrOutcome<WithdrawState>, SubscribeWithdrawError> {
1979 let operation = self.client_ctx.get_operation(operation_id).await?;
1980
1981 let operation_meta = operation.meta::<WalletOperationMeta>();
1982
1983 let (WalletOperationMetaVariant::Withdraw { change, .. }
1984 | WalletOperationMetaVariant::RbfWithdraw { change, .. }) = operation_meta.variant
1985 else {
1986 return Err(SubscribeWithdrawError::NotAWithdrawal);
1987 };
1988
1989 let mut operation_stream = self.notifier.subscribe(operation_id).await;
1990 let client_ctx = self.client_ctx.clone();
1991
1992 Ok(self.client_ctx.outcome_or_updates(
1993 &operation,
1994 operation_id,
1995 |state| match state {
1996 WithdrawState::Created => false,
1997 WithdrawState::Succeeded(_) | WithdrawState::Failed(_) => true,
1998 },
1999 move || {
2000 stream! {
2001 match next_withdraw_state(&mut operation_stream).await {
2002 Some(WithdrawStates::Created(_)) => {
2003 yield WithdrawState::Created;
2004 },
2005 Some(s) => {
2006 panic!("Unexpected state {s:?}")
2007 },
2008 None => return,
2009 }
2010
2011 let _ = client_ctx
2016 .await_primary_module_outputs(operation_id, change)
2017 .await;
2018
2019
2020 match next_withdraw_state(&mut operation_stream).await {
2021 Some(WithdrawStates::Aborted(inner)) => {
2022 yield WithdrawState::Failed(inner.error);
2023 },
2024 Some(WithdrawStates::Success(inner)) => {
2025 yield WithdrawState::Succeeded(inner.txid);
2026 },
2027 Some(s) => {
2028 panic!("Unexpected state {s:?}")
2029 },
2030 None => {},
2031 }
2032 }
2033 },
2034 ))
2035 }
2036
2037 fn admin_auth(&self) -> Result<ApiAuth, ConsensusVersionVotingError> {
2038 self.admin_auth
2039 .clone()
2040 .ok_or(ConsensusVersionVotingError::AdminAuthMissing)
2041 }
2042
2043 pub async fn activate_consensus_version_voting(
2044 &self,
2045 ) -> Result<(), ConsensusVersionVotingError> {
2046 self.module_api
2047 .activate_consensus_version_voting(self.admin_auth()?)
2048 .await?;
2049
2050 Ok(())
2051 }
2052}
2053
2054#[derive(Debug, thiserror::Error)]
2057enum FetchDepositTransactionError {
2058 #[error(transparent)]
2060 BitcoinRpc(#[from] BitcoinRpcError),
2061
2062 #[error("No deposit transaction found")]
2064 NotFound,
2065}
2066
2067async fn poll_supports_safe_deposit_version(db: Database, module_api: DynModuleApi) {
2070 loop {
2071 if supports_safe_deposit_verified(&db).await {
2072 break;
2073 }
2074
2075 module_api.wait_for_initialized_connections().await;
2076
2077 if verify_supports_safe_deposit(&db, &module_api).await == Some(true) {
2078 break;
2079 }
2080
2081 if is_running_in_test_env() {
2082 sleep(Duration::from_secs(10)).await;
2084 } else {
2085 sleep(Duration::from_hours(1)).await;
2086 }
2087 }
2088}
2089
2090async fn supports_safe_deposit_verified(db: &Database) -> bool {
2093 db.begin_transaction_nc()
2094 .await
2095 .get_value(&SupportsSafeDepositKey)
2096 .await
2097 .is_some()
2098}
2099
2100async fn verify_supports_safe_deposit(db: &Database, module_api: &DynModuleApi) -> Option<bool> {
2105 let module_consensus_version = match module_api.module_consensus_version().await {
2106 Ok(module_consensus_version) => module_consensus_version,
2107 Err(err) => {
2108 debug!(
2109 target: LOG_CLIENT_MODULE_WALLET,
2110 err = %err.fmt_compact(),
2111 "Could not fetch the wallet module consensus version"
2112 );
2113 return None;
2114 }
2115 };
2116
2117 let supported_version = SAFE_DEPOSIT_MODULE_CONSENSUS_VERSION <= module_consensus_version;
2118
2119 if supported_version {
2120 store_supports_safe_deposit(db.begin_transaction().await).await;
2121 }
2122
2123 Some(supported_version)
2124}
2125
2126async fn store_supports_safe_deposit(mut dbtx: DatabaseTransaction<'_, Committable>) {
2141 if dbtx.get_value(&SupportsSafeDepositKey).await.is_some() {
2142 return;
2144 }
2145
2146 dbtx.insert_entry(&SupportsSafeDepositKey, &()).await;
2147
2148 match dbtx.commit_tx_result().await {
2149 Ok(()) | Err(DatabaseError::WriteConflict) => {}
2150 Err(err) => {
2151 warn!(
2152 target: LOG_CLIENT_MODULE_WALLET,
2153 err = %err.fmt_compact(),
2154 "Failed to store the safe-deposit marker"
2155 );
2156 }
2157 }
2158}
2159
2160async fn get_next_peg_in_tweak_child_id(dbtx: &mut DatabaseTransaction<'_>) -> TweakIdx {
2162 let index = dbtx
2163 .get_value(&NextPegInTweakIndexKey)
2164 .await
2165 .unwrap_or_default();
2166 dbtx.insert_entry(&NextPegInTweakIndexKey, &(index.next()))
2167 .await;
2168 index
2169}
2170
2171#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
2172pub enum WalletClientStates {
2173 Deposit(DepositStateMachine),
2174 Withdraw(WithdrawStateMachine),
2175}
2176
2177impl IntoDynInstance for WalletClientStates {
2178 type DynType = DynState;
2179
2180 fn into_dyn(self, instance_id: ModuleInstanceId) -> Self::DynType {
2181 DynState::from_typed(instance_id, self)
2182 }
2183}
2184
2185impl State for WalletClientStates {
2186 type ModuleContext = WalletClientContext;
2187
2188 fn transitions(
2189 &self,
2190 context: &Self::ModuleContext,
2191 global_context: &DynGlobalClientContext,
2192 ) -> Vec<StateTransition<Self>> {
2193 match self {
2194 WalletClientStates::Deposit(sm) => {
2195 sm_enum_variant_translation!(
2196 sm.transitions(context, global_context),
2197 WalletClientStates::Deposit
2198 )
2199 }
2200 WalletClientStates::Withdraw(sm) => {
2201 sm_enum_variant_translation!(
2202 sm.transitions(context, global_context),
2203 WalletClientStates::Withdraw
2204 )
2205 }
2206 }
2207 }
2208
2209 fn operation_id(&self) -> OperationId {
2210 match self {
2211 WalletClientStates::Deposit(sm) => sm.operation_id(),
2212 WalletClientStates::Withdraw(sm) => sm.operation_id(),
2213 }
2214 }
2215}
2216
2217#[cfg(all(test, not(target_family = "wasm")))]
2218mod tests {
2219 use std::collections::BTreeSet;
2220 use std::sync::atomic::{AtomicBool, Ordering};
2221
2222 use fedimint_core::db::mem_impl::MemDatabase;
2223 use fedimint_core::module::registry::ModuleDecoderRegistry;
2224
2225 use super::*;
2226 use crate::backup::{
2227 RECOVER_NUM_IDX_ADD_TO_LAST_USED, RecoverScanOutcome, recover_scan_idxes_for_activity,
2228 };
2229
2230 #[allow(clippy::too_many_lines)] #[tokio::test(flavor = "multi_thread")]
2232 async fn sanity_test_recover_inner() {
2233 {
2234 let last_checked = AtomicBool::new(false);
2235 let last_checked = &last_checked;
2236 assert_eq!(
2237 recover_scan_idxes_for_activity(
2238 TweakIdx(0),
2239 &BTreeSet::new(),
2240 |cur_idx| async move {
2241 Ok(match cur_idx {
2242 TweakIdx(9) => {
2243 last_checked.store(true, Ordering::SeqCst);
2244 vec![]
2245 }
2246 TweakIdx(10) => panic!("Shouldn't happen"),
2247 TweakIdx(11) => {
2248 vec![0usize] }
2250 _ => vec![],
2251 })
2252 }
2253 )
2254 .await
2255 .unwrap(),
2256 RecoverScanOutcome {
2257 last_used_idx: None,
2258 new_start_idx: TweakIdx(RECOVER_NUM_IDX_ADD_TO_LAST_USED),
2259 tweak_idxes_with_pegins: BTreeSet::from([])
2260 }
2261 );
2262 assert!(last_checked.load(Ordering::SeqCst));
2263 }
2264
2265 {
2266 let last_checked = AtomicBool::new(false);
2267 let last_checked = &last_checked;
2268 assert_eq!(
2269 recover_scan_idxes_for_activity(
2270 TweakIdx(0),
2271 &BTreeSet::from([TweakIdx(1), TweakIdx(2)]),
2272 |cur_idx| async move {
2273 Ok(match cur_idx {
2274 TweakIdx(1) => panic!("Shouldn't happen: already used (1)"),
2275 TweakIdx(2) => panic!("Shouldn't happen: already used (2)"),
2276 TweakIdx(11) => {
2277 last_checked.store(true, Ordering::SeqCst);
2278 vec![]
2279 }
2280 TweakIdx(12) => panic!("Shouldn't happen"),
2281 TweakIdx(13) => {
2282 vec![0usize] }
2284 _ => vec![],
2285 })
2286 }
2287 )
2288 .await
2289 .unwrap(),
2290 RecoverScanOutcome {
2291 last_used_idx: Some(TweakIdx(2)),
2292 new_start_idx: TweakIdx(2 + RECOVER_NUM_IDX_ADD_TO_LAST_USED),
2293 tweak_idxes_with_pegins: BTreeSet::from([])
2294 }
2295 );
2296 assert!(last_checked.load(Ordering::SeqCst));
2297 }
2298
2299 {
2300 let last_checked = AtomicBool::new(false);
2301 let last_checked = &last_checked;
2302 assert_eq!(
2303 recover_scan_idxes_for_activity(
2304 TweakIdx(10),
2305 &BTreeSet::new(),
2306 |cur_idx| async move {
2307 Ok(match cur_idx {
2308 TweakIdx(10) => vec![()],
2309 TweakIdx(19) => {
2310 last_checked.store(true, Ordering::SeqCst);
2311 vec![]
2312 }
2313 TweakIdx(20) => panic!("Shouldn't happen"),
2314 _ => vec![],
2315 })
2316 }
2317 )
2318 .await
2319 .unwrap(),
2320 RecoverScanOutcome {
2321 last_used_idx: Some(TweakIdx(10)),
2322 new_start_idx: TweakIdx(10 + RECOVER_NUM_IDX_ADD_TO_LAST_USED),
2323 tweak_idxes_with_pegins: BTreeSet::from([TweakIdx(10)])
2324 }
2325 );
2326 assert!(last_checked.load(Ordering::SeqCst));
2327 }
2328
2329 assert_eq!(
2330 recover_scan_idxes_for_activity(TweakIdx(0), &BTreeSet::new(), |cur_idx| async move {
2331 Ok(match cur_idx {
2332 TweakIdx(6 | 15) => vec![()],
2333 _ => vec![],
2334 })
2335 })
2336 .await
2337 .unwrap(),
2338 RecoverScanOutcome {
2339 last_used_idx: Some(TweakIdx(15)),
2340 new_start_idx: TweakIdx(15 + RECOVER_NUM_IDX_ADD_TO_LAST_USED),
2341 tweak_idxes_with_pegins: BTreeSet::from([TweakIdx(6), TweakIdx(15)])
2342 }
2343 );
2344 assert_eq!(
2345 recover_scan_idxes_for_activity(TweakIdx(10), &BTreeSet::new(), |cur_idx| async move {
2346 Ok(match cur_idx {
2347 TweakIdx(8) => {
2348 vec![()] }
2350 TweakIdx(9) => {
2351 panic!("Shouldn't happen")
2352 }
2353 _ => vec![],
2354 })
2355 })
2356 .await
2357 .unwrap(),
2358 RecoverScanOutcome {
2359 last_used_idx: None,
2360 new_start_idx: TweakIdx(9 + RECOVER_NUM_IDX_ADD_TO_LAST_USED),
2361 tweak_idxes_with_pegins: BTreeSet::from([])
2362 }
2363 );
2364 assert_eq!(
2365 recover_scan_idxes_for_activity(TweakIdx(10), &BTreeSet::new(), |cur_idx| async move {
2366 Ok(match cur_idx {
2367 TweakIdx(9) => panic!("Shouldn't happen"),
2368 TweakIdx(15) => vec![()],
2369 _ => vec![],
2370 })
2371 })
2372 .await
2373 .unwrap(),
2374 RecoverScanOutcome {
2375 last_used_idx: Some(TweakIdx(15)),
2376 new_start_idx: TweakIdx(15 + RECOVER_NUM_IDX_ADD_TO_LAST_USED),
2377 tweak_idxes_with_pegins: BTreeSet::from([TweakIdx(15)])
2378 }
2379 );
2380 }
2381
2382 #[tokio::test]
2383 async fn store_supports_safe_deposit_tolerates_a_lost_race() {
2384 let db = Database::new(MemDatabase::new(), ModuleDecoderRegistry::default());
2385
2386 let racing_dbtx = db.begin_transaction().await;
2389 let mut sibling_dbtx = db.begin_transaction().await;
2390
2391 let mut competing_dbtx = db.begin_transaction().await;
2392 competing_dbtx
2393 .insert_entry(&SupportsSafeDepositKey, &())
2394 .await;
2395 competing_dbtx.commit_tx().await;
2396
2397 sibling_dbtx
2399 .insert_entry(&SupportsSafeDepositKey, &())
2400 .await;
2401 assert!(matches!(
2402 sibling_dbtx.commit_tx_result().await,
2403 Err(DatabaseError::WriteConflict)
2404 ));
2405
2406 store_supports_safe_deposit(racing_dbtx).await;
2407
2408 assert!(supports_safe_deposit_verified(&db).await);
2409 }
2410}