diff --git a/.gitignore b/.gitignore index 41d487b15..d3b3b4d8b 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ debug/ target/ .vim/ .direnv +.editorconfig # These are backup files generated by rustfmt **/*.rs.bk diff --git a/Cargo.lock b/Cargo.lock index 5e6e9ad2f..b41b2c69d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2548,6 +2548,7 @@ dependencies = [ "bitcoin-waila", "console_error_panic_hook", "fedimint-core", + "fedimint-mint-client", "futures", "getrandom", "gloo-utils", diff --git a/mutiny-core/src/error.rs b/mutiny-core/src/error.rs index 50b1eee6e..41e454116 100644 --- a/mutiny-core/src/error.rs +++ b/mutiny-core/src/error.rs @@ -171,6 +171,10 @@ pub enum MutinyError { /// Token already spent. #[error("Token has been already spent.")] TokenAlreadySpent, + #[error("Fedimint external note re-issuance failed.")] + FedimintReissueFailed, + #[error("Fedimint external note re-issuance resulted in stale outcome state.")] + FedimintReissueStaleState, #[error(transparent)] Other(#[from] anyhow::Error), } diff --git a/mutiny-core/src/federation.rs b/mutiny-core/src/federation.rs index 847e9108a..34c273d8a 100644 --- a/mutiny-core/src/federation.rs +++ b/mutiny-core/src/federation.rs @@ -52,7 +52,7 @@ use fedimint_ln_client::{ }; use fedimint_ln_common::lightning_invoice::RoutingFees; use fedimint_ln_common::LightningCommonInit; -use fedimint_mint_client::MintClientInit; +use fedimint_mint_client::{MintClientInit, MintClientModule, OOBNotes, ReissueExternalNotesState}; use fedimint_wallet_client::{WalletClientInit, WalletClientModule}; use futures::future::{self}; use futures_util::{pin_mut, StreamExt}; @@ -72,6 +72,12 @@ use std::{ sync::atomic::{AtomicU32, Ordering}, }; +// The amount of time in milliseconds to wait for +// checking the status of a fedimint OOB re-issuance. This +// is to work around their stream status checking +// when wanting just the current status. +const FEDIMINT_OOB_REISSUE_TIMEOUT_CHECK_MS: u64 = 30; + // The amount of time in milliseconds to wait for // checking the status of a fedimint payment. This // is to work around their stream status checking @@ -609,6 +615,42 @@ impl FederationClient { } } + pub(crate) async fn reissue(&self, oob_notes: OOBNotes) -> Result<(), MutinyError> { + // Get the `MintClientModule` + let mint_module = self.fedimint_client.get_first_module::(); + + // Reissue `OOBNotes` + let operation_id = mint_module.reissue_external_notes(oob_notes, ()).await?; + + match process_reissue_outcome( + &mint_module, + operation_id, + FEDIMINT_OOB_REISSUE_TIMEOUT_CHECK_MS, + self.logger.clone(), + ) + .await? + { + ReissueExternalNotesState::Done => { + log_trace!(self.logger, "re-issuance of OOBNotes was successful!"); + Ok(()) + } + ReissueExternalNotesState::Failed(reason) => { + log_trace!( + self.logger, + "re-issuance of OOBNotes failed explicitly, reason: {reason}" + ); + Err(MutinyError::FedimintReissueFailed) + } + _ => { + log_trace!( + self.logger, + "re-issuance of OOBNotes explicitly failed, due to outcome with a stale state!" + ); + Err(MutinyError::FedimintReissueStaleState) + } + } + } + pub async fn get_mutiny_federation_identity(&self) -> FederationIdentity { let gateway_fees = self.gateway_fee().await.ok(); @@ -866,6 +908,60 @@ where invoice } +async fn process_reissue_outcome( + mint_module: &MintClientModule, + operation_id: OperationId, + timeout: u64, + logger: Arc, +) -> Result { + // Subscribe to the outcome based on `ReissueExternalNotesState` + let stream_or_outcome = mint_module + .subscribe_reissue_external_notes(operation_id) + .await + .map_err(MutinyError::Other)?; + + // Process the outcome based on `ReissueExternalNotesState` + match stream_or_outcome { + UpdateStreamOrOutcome::Outcome(outcome) => { + log_trace!(logger, "outcome received {:?}", outcome); + Ok(outcome) + } + UpdateStreamOrOutcome::UpdateStream(mut stream) => { + let timeout_fut = sleep(timeout as i32); + pin_mut!(timeout_fut); + + log_trace!(logger, "started timeout future {:?}", timeout); + + while let future::Either::Left((outcome_opt, _)) = + future::select(stream.next(), &mut timeout_fut).await + { + if let Some(outcome) = outcome_opt { + log_trace!(logger, "streamed outcome received {:?}", outcome); + + match outcome { + ReissueExternalNotesState::Failed(_) | ReissueExternalNotesState::Done => { + log_trace!( + logger, + "streamed outcome received is final {:?}, returning", + outcome + ); + return Ok(outcome); + } + _ => { + log_trace!( + logger, + "streamed outcome received is not final {:?}, skipping", + outcome + ); + } + } + }; + } + Err(MutinyError::FedimintReissueFailed) + } + } +} + #[derive(Clone)] pub struct FedimintStorage { pub(crate) storage: S, diff --git a/mutiny-core/src/lib.rs b/mutiny-core/src/lib.rs index 5310292fd..2205502cb 100644 --- a/mutiny-core/src/lib.rs +++ b/mutiny-core/src/lib.rs @@ -7,7 +7,6 @@ clippy::arc_with_non_send_sync, type_alias_bounds )] -extern crate core; pub mod auth; mod cashu; @@ -85,6 +84,7 @@ use bitcoin::hashes::Hash; use bitcoin::secp256k1::{PublicKey, ThirtyTwoByteHash}; use bitcoin::{hashes::sha256, Network}; use fedimint_core::{api::InviteCode, config::FederationId}; +use fedimint_mint_client::OOBNotes; use futures::{pin_mut, select, FutureExt}; use futures_util::join; use hex_conservative::{DisplayHex, FromHex}; @@ -1362,6 +1362,37 @@ impl MutinyWallet { }) } + pub async fn reissue_oob_notes(&self, oob_notes: OOBNotes) -> Result<(), MutinyError> { + let federation_lock = self.federations.read().await; + let federation_ids = self.list_federation_ids().await?; + + let maybe_federation_id = federation_ids + .iter() + .find(|id| id.to_prefix() == oob_notes.federation_id_prefix()); + + if let Some(fed_id) = maybe_federation_id { + log_trace!(self.logger, "found federation_id {:?}", fed_id); + + let fedimint_client = federation_lock.get(fed_id).ok_or(MutinyError::NotFound)?; + log_trace!( + self.logger, + "got fedimint client for federation_id {:?}", + fed_id + ); + + fedimint_client.reissue(oob_notes).await?; + log_trace!( + self.logger, + "successfully reissued for federation_id {:?}", + fed_id + ); + + Ok(()) + } else { + Err(MutinyError::NotFound) + } + } + /// Estimate the fee before trying to sweep from federation pub async fn estimate_sweep_federation_fee( &self, diff --git a/mutiny-wasm/Cargo.toml b/mutiny-wasm/Cargo.toml index 03db847be..f18057a39 100644 --- a/mutiny-wasm/Cargo.toml +++ b/mutiny-wasm/Cargo.toml @@ -45,8 +45,8 @@ once_cell = "1.18.0" hex-conservative = "0.1.1" payjoin = { version = "0.13.0", features = ["send", "base64"] } fedimint-core = { git = "https://github.com/fedimint/fedimint", rev = "5ade2536015a12a7e003a42b159ccc4a431e1a32" } +fedimint-mint-client = { git = "https://github.com/fedimint/fedimint", rev = "5ade2536015a12a7e003a42b159ccc4a431e1a32" } moksha-core = { git = "https://github.com/ngutech21/moksha", rev = "18d99977965662d46ccec29fecdb0ce493745917" } - bitcoin-waila = { git = "https://github.com/mutinywallet/bitcoin-waila", rev = "b8b6a4d709e438fbadeb16bdf0c577c59be4a7f2" } # The `console_error_panic_hook` crate provides better debugging of panics by diff --git a/mutiny-wasm/src/error.rs b/mutiny-wasm/src/error.rs index 8f7955af6..7a1f2e12c 100644 --- a/mutiny-wasm/src/error.rs +++ b/mutiny-wasm/src/error.rs @@ -168,6 +168,10 @@ pub enum MutinyJsError { /// Token already spent. #[error("Token has been already spent.")] TokenAlreadySpent, + #[error("Fedimint external note re-issuance failed.")] + FedimintReissueFailed, + #[error("Fedimint external note re-issuance resulted in stale outcome state.")] + FedimintReissueStaleState, /// Unknown error. #[error("Unknown Error")] UnknownError, @@ -238,6 +242,8 @@ impl From for MutinyJsError { MutinyError::PayjoinConfigError => MutinyJsError::PayjoinConfigError, MutinyError::PayjoinCreateRequest => MutinyJsError::PayjoinCreateRequest, MutinyError::PayjoinResponse(e) => MutinyJsError::PayjoinResponse(e.to_string()), + MutinyError::FedimintReissueFailed => MutinyJsError::FedimintReissueFailed, + MutinyError::FedimintReissueStaleState => MutinyJsError::FedimintReissueStaleState, } } } diff --git a/mutiny-wasm/src/lib.rs b/mutiny-wasm/src/lib.rs index 4f4bb1de0..5bb9765a1 100644 --- a/mutiny-wasm/src/lib.rs +++ b/mutiny-wasm/src/lib.rs @@ -24,6 +24,7 @@ use bitcoin::hashes::sha256; use bitcoin::secp256k1::PublicKey; use bitcoin::{Address, Network, OutPoint, Transaction, Txid}; use fedimint_core::{api::InviteCode, config::FederationId}; +use fedimint_mint_client::OOBNotes; use futures::lock::Mutex; use gloo_utils::format::JsValueSerdeExt; use hex_conservative::DisplayHex; @@ -1022,6 +1023,17 @@ impl MutinyWallet { Ok(self.inner.sweep_federation_balance(amount).await?.into()) } + pub async fn reissue_oob_notes(&self, oob_notes: String) -> Result<(), MutinyJsError> { + let notes = OOBNotes::from_str(&oob_notes).map_err(|e| { + log_error!( + self.inner.logger, + "Error parsing federation `OOBNotes` ({oob_notes}): {e}" + ); + MutinyJsError::InvalidArgumentsError + })?; + Ok(self.inner.reissue_oob_notes(notes).await?) + } + /// Estimate the fee before trying to sweep from federation pub async fn estimate_sweep_federation_fee( &self,