Skip to content
This repository was archived by the owner on Feb 3, 2025. It is now read-only.
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ debug/
target/
.vim/
.direnv
.editorconfig

# These are backup files generated by rustfmt
**/*.rs.bk
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions mutiny-core/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}
Expand Down
71 changes: 69 additions & 2 deletions mutiny-core/src/federation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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};
Expand Down Expand Up @@ -609,6 +609,26 @@ impl<S: MutinyStorage> FederationClient<S> {
}
}

pub(crate) async fn reissue(&self, oob_notes: OOBNotes) -> Result<(), MutinyError> {
// Get the `MintClientModule`
let mint_module = self.fedimint_client.get_first_module::<MintClientModule>();

// 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(_) => {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why return error on created

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure how to proceed with this, but if the stream is already finished and the final result is still Created, it didn't get processed by the federation without a real guaranteeing it will.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll need to check in the discord about this.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAIK there is no known error it would still be stuck at created even after all the stream is consumed, but still needs to handle the variant 🤔

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@benthecarman What do you think we should do here with Created variant?
It's not supposed to have that after the whole stream is consumed, so still handle as failure/error ? 🤔

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in process_reissue_outcome you only return the outcome if it is Failed or Done so you should just handle those two here, and leave the rest in the _ => case where you can throw an error.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wdyt about eb59a8b

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();

Expand Down Expand Up @@ -866,6 +886,53 @@ where
invoice
}

async fn process_reissue_outcome(
mint_module: &MintClientModule,
operation_id: OperationId,
logger: Arc<MutinyLogger>,
) -> Result<ReissueExternalNotesState, MutinyError> {
// 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;
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 */ }
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would be helpful to log each state

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice, I'll add it :)

}
};
}
Err(MutinyError::FedimintReissueFailed)
}
}
}

#[derive(Clone)]
pub struct FedimintStorage<S: MutinyStorage> {
pub(crate) storage: S,
Expand Down
34 changes: 33 additions & 1 deletion mutiny-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
clippy::arc_with_non_send_sync,
type_alias_bounds
)]
extern crate core;

pub mod auth;
mod cashu;
Expand Down Expand Up @@ -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};
Expand Down Expand Up @@ -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 = 50;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is only used in federation.rs, would be better to define in there. This should probably go back to 30 seconds too. you're also always casting to i32, can just define it as such

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice, done!
but I kept it as u64 to follow the standard, or should I also change the other ones? 🤔

const MAX_FEDERATION_INVOICE_AMT: u64 = 200_000;
const SWAP_LABEL: &str = "SWAP";
const MELT_CASHU_TOKEN: &str = "Cashu Token Melt";
Expand Down Expand Up @@ -1362,6 +1363,37 @@ impl<S: MutinyStorage> MutinyWallet<S> {
})
}

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,
Expand Down
2 changes: 1 addition & 1 deletion mutiny-wasm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions mutiny-wasm/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -238,6 +240,7 @@ impl From<MutinyError> for MutinyJsError {
MutinyError::PayjoinConfigError => MutinyJsError::PayjoinConfigError,
MutinyError::PayjoinCreateRequest => MutinyJsError::PayjoinCreateRequest,
MutinyError::PayjoinResponse(e) => MutinyJsError::PayjoinResponse(e.to_string()),
MutinyError::FedimintReissueFailed => MutinyJsError::FedimintReissueFailed,
}
}
}
Expand Down
12 changes: 12 additions & 0 deletions mutiny-wasm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down