Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

wip(feat): reissue ecash notes from OOBNotes #1037

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
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.

4 changes: 4 additions & 0 deletions mutiny-core/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}
Expand Down
98 changes: 97 additions & 1 deletion mutiny-core/src/federation.rs
Original file line number Diff line number Diff line change
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 All @@ -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
Expand Down Expand Up @@ -609,6 +615,42 @@ 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?;

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

Expand Down Expand Up @@ -866,6 +908,60 @@ where
invoice
}

async fn process_reissue_outcome(
mint_module: &MintClientModule,
operation_id: OperationId,
timeout: u64,
logger: Arc<MutinyLogger>,
) -> Result<ReissueExternalNotesState, MutinyError> {
// 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<S: MutinyStorage> {
pub(crate) storage: S,
Expand Down
33 changes: 32 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 @@ -1362,6 +1362,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
6 changes: 6 additions & 0 deletions mutiny-wasm/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -238,6 +242,8 @@ 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,
MutinyError::FedimintReissueStaleState => MutinyJsError::FedimintReissueStaleState,
}
}
}
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