diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..e69de29bb diff --git a/Cargo.lock b/Cargo.lock index 3396193ce..045aa81e4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2350,6 +2350,7 @@ dependencies = [ "bitcoin 0.30.2", "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 84c4c368d..9ee37a965 100644 --- a/mutiny-core/src/error.rs +++ b/mutiny-core/src/error.rs @@ -162,6 +162,8 @@ pub enum MutinyError { /// Payjoin configuration error #[error("Payjoin configuration failed.")] PayjoinConfigError, + #[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 f6e886f10..c4ed36ad6 100644 --- a/mutiny-core/src/federation.rs +++ b/mutiny-core/src/federation.rs @@ -1,4 +1,5 @@ use crate::utils::{convert_from_fedimint_invoice, convert_to_fedimint_invoice, spawn}; +use crate::DEFAULT_REISSUE_TIMEOUT; use crate::{ error::{MutinyError, MutinyStorageError}, event::PaymentInfo, @@ -12,6 +13,7 @@ use crate::{ HTLCStatus, MutinyInvoice, DEFAULT_PAYMENT_TIMEOUT, }; use async_trait::async_trait; +use bip39::rand_core::le; use bip39::Mnemonic; use bitcoin::secp256k1::ThirtyTwoByteHash; use bitcoin::{ @@ -52,7 +54,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}; @@ -603,6 +605,59 @@ impl FederationClient { } } + pub(crate) async fn reissue(&self, oob_notes: OOBNotes) -> Result { + let logger = Arc::clone(&self.logger); + + // Get the `MintClientModule` + let mint_module = self.fedimint_client.get_first_module::(); + + // TODO: (@leonardo) Do we need any `extra_meta` ? + // Reissue `OOBNotes` + let operation_id = mint_module.reissue_external_notes(oob_notes, ()).await?; + + // Subscribe/Process the outcome based on `ReissueExternalNotesState` + let mut stream_or_outcome = mint_module + .subscribe_reissue_external_notes(operation_id) + .await + .map_err(|e| MutinyError::Other(e))?; + + let mut reissue_state = ReissueExternalNotesState::Created; + + let _ = match stream_or_outcome { + UpdateStreamOrOutcome::Outcome(outcome) => { + reissue_state = outcome; + log_trace!(logger, "Outcome received: {:?}", reissue_state); + } + UpdateStreamOrOutcome::UpdateStream(mut stream) => { + let timeout = DEFAULT_REISSUE_TIMEOUT * 1_000; + let timeout_future = sleep(timeout as i32); + pin_mut!(timeout_future); + + log_trace!(logger, "Start timeout stream features"); + while let future::Either::Left((outcome_opt, _)) = + future::select(stream.next(), &mut timeout_future).await + { + if let Some(outcome) = outcome_opt { + reissue_state = outcome; + log_trace!(logger, "Streamed Outcome received: {:?}", reissue_state); + if let ReissueExternalNotesState::Failed(_) = reissue_state { + log_trace!(logger, "Streamed Outcome final, returning"); + break; + } + } + } + } + }; + + // TODO: (@leonardo) re-think about the results and errors that we need/want + match reissue_state { + ReissueExternalNotesState::Created | ReissueExternalNotesState::Failed(_) => { + Err(MutinyError::FedimintReissueFailed) + } + _ => Ok(true), + } + } + pub async fn get_mutiny_federation_identity(&self) -> FederationIdentity { let gateway_fees = self.gateway_fee().await.ok(); diff --git a/mutiny-core/src/lib.rs b/mutiny-core/src/lib.rs index d74a6c22d..1c9b0eda0 100644 --- a/mutiny-core/src/lib.rs +++ b/mutiny-core/src/lib.rs @@ -84,6 +84,7 @@ use bitcoin::hashes::Hash; use bitcoin::secp256k1::PublicKey; 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}; @@ -113,6 +114,7 @@ use crate::utils::parse_profile_metadata; use mockall::{automock, predicate::*}; const DEFAULT_PAYMENT_TIMEOUT: u64 = 30; +const DEFAULT_REISSUE_TIMEOUT: u64 = 30; const MAX_FEDERATION_INVOICE_AMT: u64 = 200_000; const SWAP_LABEL: &str = "SWAP"; @@ -1308,6 +1310,22 @@ impl MutinyWallet { }) } + pub async fn reissue_oob_notes(&self, oob_notes: OOBNotes) -> Result { + 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 == oob_notes.federation_id_prefix()); + + if let Some(fed_id) = maybe_federation_id { + let fedimint_client = federation_lock.get(&fed_id).ok_or(MutinyError::NotFound)?; + Ok(fedimint_client.reissue(oob_notes).await?) + } else { + return 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 92f4492bd..e682038b1 100644 --- a/mutiny-wasm/Cargo.toml +++ b/mutiny-wasm/Cargo.toml @@ -45,6 +45,7 @@ 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 = "6a923ee10c3a578cd835044e3fdd94aa5123735a" } +fedimint-mint-client = { git = "https://github.com/fedimint/fedimint", rev = "6a923ee10c3a578cd835044e3fdd94aa5123735a" } # The `console_error_panic_hook` crate provides better debugging of panics by # logging them with `console.error`. This is great for development, but requires diff --git a/mutiny-wasm/src/error.rs b/mutiny-wasm/src/error.rs index bc02daf28..e7efb313d 100644 --- a/mutiny-wasm/src/error.rs +++ b/mutiny-wasm/src/error.rs @@ -159,6 +159,8 @@ pub enum MutinyJsError { /// Payjoin configuration error #[error("Payjoin configuration failed.")] PayjoinConfigError, + #[error("Fedimint external note reissuance failed.")] + FedimintReissueFailed, /// Unknown error. #[error("Unknown Error")] UnknownError, @@ -226,6 +228,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 4b827cc53..b4787105b 100644 --- a/mutiny-wasm/src/lib.rs +++ b/mutiny-wasm/src/lib.rs @@ -23,6 +23,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; @@ -1023,6 +1024,17 @@ impl MutinyWallet { Ok(self.inner.sweep_federation_balance(amount).await?.into()) } + pub async fn reissue_oob_notes(&self, oob_notes: String) -> Result { + 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,