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..0f985a96c 100644 --- a/mutiny-core/src/error.rs +++ b/mutiny-core/src/error.rs @@ -171,6 +171,8 @@ pub enum MutinyError { /// Token already spent. #[error("Token has been already spent.")] TokenAlreadySpent, + #[error("Fedimint external note reissuance failed.")] + FedimintReissueFailed, #[error(transparent)] Other(#[from] anyhow::Error), } diff --git a/mutiny-core/src/federation.rs b/mutiny-core/src/federation.rs index 847e9108a..26c2d97a7 100644 --- a/mutiny-core/src/federation.rs +++ b/mutiny-core/src/federation.rs @@ -9,7 +9,7 @@ use crate::{ get_payment_info, list_payment_info, persist_payment_info, MutinyStorage, VersionedValue, }, utils::sleep, - HTLCStatus, MutinyInvoice, DEFAULT_PAYMENT_TIMEOUT, + HTLCStatus, MutinyInvoice, DEFAULT_PAYMENT_TIMEOUT, DEFAULT_REISSUE_TIMEOUT, }; use async_trait::async_trait; use bip39::Mnemonic; @@ -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}; @@ -609,6 +609,26 @@ 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?; + + // TODO: (@leonardo) re-think about the results and errors that we need/want + match process_reissue_outcome(&mint_module, operation_id, self.logger.clone()).await? { + ReissueExternalNotesState::Created | ReissueExternalNotesState::Failed(_) => { + log_trace!(self.logger, "re-issuance of OOBNotes failed!"); + Err(MutinyError::FedimintReissueFailed) + } + _ => { + log_trace!(self.logger, "re-issuance of OOBNotes was successful!"); + Ok(()) + } + } + } + pub async fn get_mutiny_federation_identity(&self) -> FederationIdentity { let gateway_fees = self.gateway_fee().await.ok(); @@ -866,6 +886,53 @@ where invoice } +async fn process_reissue_outcome( + mint_module: &MintClientModule, + operation_id: OperationId, + logger: Arc, +) -> Result { + // Subscribe/Process the outcome based on `ReissueExternalNotesState` + let stream_or_outcome = mint_module + .subscribe_reissue_external_notes(operation_id) + .await + .map_err(MutinyError::Other)?; + + match stream_or_outcome { + UpdateStreamOrOutcome::Outcome(outcome) => { + log_trace!(logger, "outcome received {:?}", outcome); + Ok(outcome) + } + UpdateStreamOrOutcome::UpdateStream(mut stream) => { + let timeout = DEFAULT_REISSUE_TIMEOUT * 1_000; + 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); + } + _ => { /* ignore and continue */ } + } + }; + } + 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..2a33e7917 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}; @@ -119,6 +119,7 @@ use crate::utils::parse_profile_metadata; use mockall::{automock, predicate::*}; const DEFAULT_PAYMENT_TIMEOUT: u64 = 30; +const DEFAULT_REISSUE_TIMEOUT: u64 = 5; const MAX_FEDERATION_INVOICE_AMT: u64 = 200_000; const SWAP_LABEL: &str = "SWAP"; const MELT_CASHU_TOKEN: &str = "Cashu Token Melt"; @@ -1362,6 +1363,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..58004e23e 100644 --- a/mutiny-wasm/src/error.rs +++ b/mutiny-wasm/src/error.rs @@ -168,6 +168,8 @@ pub enum MutinyJsError { /// Token already spent. #[error("Token has been already spent.")] TokenAlreadySpent, + #[error("Fedimint external note reissuance failed.")] + FedimintReissueFailed, /// Unknown error. #[error("Unknown Error")] UnknownError, @@ -238,6 +240,7 @@ impl From for MutinyJsError { MutinyError::PayjoinConfigError => MutinyJsError::PayjoinConfigError, MutinyError::PayjoinCreateRequest => MutinyJsError::PayjoinCreateRequest, MutinyError::PayjoinResponse(e) => MutinyJsError::PayjoinResponse(e.to_string()), + MutinyError::FedimintReissueFailed => MutinyJsError::FedimintReissueFailed, } } } 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,