From 7ec2f66c6842d028dffbc3a7e128f4d4cf6e4d00 Mon Sep 17 00:00:00 2001 From: Tony Giorgio Date: Tue, 30 Apr 2024 00:22:09 -0500 Subject: [PATCH 1/8] onchain support for fedimint --- mutiny-core/src/federation.rs | 499 ++++++++++++++++++++++++--------- mutiny-core/src/lib.rs | 179 ++++++++++-- mutiny-core/src/nodemanager.rs | 64 +---- mutiny-core/src/onchain.rs | 10 +- mutiny-core/src/storage.rs | 62 +++- mutiny-wasm/src/lib.rs | 4 +- mutiny-wasm/src/models.rs | 2 +- 7 files changed, 606 insertions(+), 214 deletions(-) diff --git a/mutiny-core/src/federation.rs b/mutiny-core/src/federation.rs index a1d5a0a63..bac11790d 100644 --- a/mutiny-core/src/federation.rs +++ b/mutiny-core/src/federation.rs @@ -1,24 +1,29 @@ use crate::utils::{ convert_from_fedimint_invoice, convert_to_fedimint_invoice, fetch_with_timeout, now, spawn, }; +use crate::TransactionDetails; use crate::{ error::{MutinyError, MutinyStorageError}, event::PaymentInfo, key::{create_root_child_key, ChildKey}, logging::MutinyLogger, onchain::coin_type_from_network, - storage::{list_payment_info, persist_payment_info, MutinyStorage, VersionedValue}, + storage::{ + get_transaction_details, list_payment_info, persist_payment_info, + persist_transaction_details, MutinyStorage, VersionedValue, TRANSACTION_DETAILS_PREFIX_KEY, + }, utils::sleep, HTLCStatus, MutinyInvoice, DEFAULT_PAYMENT_TIMEOUT, }; use async_lock::RwLock; use async_trait::async_trait; +use bdk_chain::ConfirmationTime; use bip39::Mnemonic; -use bitcoin::secp256k1::{SecretKey, ThirtyTwoByteHash}; use bitcoin::{ bip32::{ChildNumber, DerivationPath, ExtendedPrivKey}, - secp256k1::Secp256k1, - Network, + hashes::Hash, + secp256k1::{Secp256k1, SecretKey, ThirtyTwoByteHash}, + Address, Network, Txid, }; use core::fmt; use fedimint_bip39::Bip39RootSecretStrategy; @@ -52,7 +57,9 @@ use fedimint_ln_client::{ use fedimint_ln_common::lightning_invoice::{Bolt11InvoiceDescription, Description, RoutingFees}; use fedimint_ln_common::{LightningCommonInit, LightningGateway}; use fedimint_mint_client::MintClientInit; -use fedimint_wallet_client::{WalletClientInit, WalletClientModule}; +use fedimint_wallet_client::{ + WalletClientInit, WalletClientModule, WalletCommonInit, WalletOperationMeta, WithdrawState, +}; use futures::{select, FutureExt}; use futures_util::{pin_mut, StreamExt}; use hex_conservative::{DisplayHex, FromHex}; @@ -60,6 +67,7 @@ use lightning::{log_debug, log_error, log_info, log_trace, log_warn, util::logge use lightning_invoice::Bolt11Invoice; use reqwest::Method; use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use std::time::Duration; #[cfg(not(target_arch = "wasm32"))] use std::time::Instant; use std::{ @@ -79,6 +87,9 @@ use web_time::Instant; // their internal list. const FEDIMINT_OPERATIONS_LIST_MAX: usize = 100; +// On chain peg in timeout +const PEG_IN_TIMEOUT_YEAR: Duration = Duration::from_secs(86400 * 365); + pub const FEDIMINTS_PREFIX_KEY: &str = "fedimints/"; // Default signet/mainnet federation gateway info @@ -411,6 +422,18 @@ impl FederationClient { .map(|(h, _i)| h.0), ); + // pending on chain operations + let pending_wallet_txids = self + .storage + .scan::(TRANSACTION_DETAILS_PREFIX_KEY, None)? + .into_iter() + .filter(|(_k, v)| match v.confirmation_time { + ConfirmationTime::Unconfirmed { .. } => true, // return all unconfirmed transactions + _ => false, // skip confirmed transactions + }) + .map(|(_h, i)| i.internal_id) + .collect::>(); + // go through last 100 operations let operations = self .fedimint_client @@ -420,51 +443,45 @@ impl FederationClient { // find all of the pending ones for (key, entry) in operations { - if entry.operation_module_kind() == LightningCommonInit::KIND.as_str() { + let module_type = entry.operation_module_kind(); + if module_type == LightningCommonInit::KIND.as_str() { let lightning_meta: LightningOperationMeta = entry.meta(); match lightning_meta.variant { LightningOperationMetaVariant::Pay(pay_meta) => { let hash = pay_meta.invoice.payment_hash().into_inner(); if pending_invoices.contains(&hash) { - self.subscribe_operation( - entry, - hash, - key.operation_id, - self.fedimint_client.clone(), - ); + self.subscribe_operation(entry, key.operation_id); } } LightningOperationMetaVariant::Receive { invoice, .. } => { let hash = invoice.payment_hash().into_inner(); if pending_invoices.contains(&hash) { - self.subscribe_operation( - entry, - hash, - key.operation_id, - self.fedimint_client.clone(), - ); + self.subscribe_operation(entry, key.operation_id); } } LightningOperationMetaVariant::Claim { .. } => {} } + } else if module_type == WalletCommonInit::KIND.as_str() { + let internal_id = Txid::from_slice(&key.operation_id.0) + .map_err(|_| MutinyError::ChainAccessFailed) + .expect("should convert"); + + if pending_wallet_txids.contains(&internal_id) { + self.subscribe_operation(entry, key.operation_id); + } + } else { + log_warn!(self.logger, "Unknown module type: {module_type}") } } Ok(()) } - fn subscribe_operation( - &self, - entry: OperationLogEntry, - hash: [u8; 32], - operation_id: OperationId, - fedimint_client: ClientHandleArc, - ) { + fn subscribe_operation(&self, entry: OperationLogEntry, operation_id: OperationId) { subscribe_operation_ext( entry, - hash, operation_id, - fedimint_client, + self.fedimint_client.clone(), self.logger.clone(), self.stop.clone(), self.storage.clone(), @@ -517,44 +534,69 @@ impl FederationClient { let storage_clone = self.storage.clone(); let stop = self.stop.clone(); spawn(async move { - let lightning_module = - Arc::new(fedimint_client_clone.get_first_module::()); - - let operations = fedimint_client_clone + let operation = fedimint_client_clone .operation_log() .get_operation(id) .await .expect("just created it"); - if let Some(updated_invoice) = process_operation_until_timeout( - logger_clone.clone(), - operations.meta(), - hash, + subscribe_operation_ext( + operation, id, - &lightning_module, - None, + fedimint_client_clone, + logger_clone, stop, - ) - .await - { - match maybe_update_after_checking_fedimint( - updated_invoice.clone(), - logger_clone.clone(), - storage_clone, - ) { - Ok(_) => { - log_info!(logger_clone, "updated invoice"); - } - Err(e) => { - log_error!(logger_clone, "could not check update invoice: {e}"); - } - } - } + storage_clone, + ); }); Ok(invoice.into()) } + pub(crate) async fn get_new_address( + &self, + labels: Vec, + ) -> Result { + let wallet_module = self + .fedimint_client + .get_first_module::(); + + let (op_id, address) = wallet_module + .get_deposit_address(fedimint_core::time::now() + PEG_IN_TIMEOUT_YEAR, ()) + .await?; + + let internal_id = Txid::from_slice(&op_id.0).map_err(|_| MutinyError::ChainAccessFailed)?; + + // persist the data we can while we wait for the transaction to come from fedimint + let pending_transaction_details = TransactionDetails { + transaction: None, + txid: None, + internal_id, + received: 0, + sent: 0, + fee: None, + confirmation_time: ConfirmationTime::Unconfirmed { + last_seen: now().as_secs(), + }, + labels, + }; + + persist_transaction_details(&self.storage, &pending_transaction_details)?; + + // subscribe + let operation = self + .fedimint_client + .operation_log() + .get_operation(op_id) + .await + .expect("just created it"); + self.subscribe_operation(operation, op_id); + + Ok(Address::from_str(&address.to_string()) + .expect("should convert") + .assume_checked()) + } + /// Get the balance of this federation client in sats pub(crate) async fn get_balance(&self) -> Result { Ok(self.fedimint_client.get_balance().await.msats / 1_000) @@ -603,7 +645,7 @@ impl FederationClient { fedimint_ln_client::PayType::Internal(pay_id) => { match lightning_module.subscribe_internal_pay(pay_id).await { Ok(o) => { - let o = process_outcome( + let o = process_ln_outcome( o, process_pay_state_internal, invoice.clone(), @@ -621,7 +663,7 @@ impl FederationClient { fedimint_ln_client::PayType::Lightning(pay_id) => { match lightning_module.subscribe_ln_pay(pay_id).await { Ok(o) => { - let o = process_outcome( + let o = process_ln_outcome( o, process_pay_state_ln, invoice.clone(), @@ -659,7 +701,6 @@ impl FederationClient { subscribe_operation_ext( operation, - hash, id, fedimint_client_clone, logger_clone, @@ -915,42 +956,23 @@ fn merge_values(a: Option, b: Option) -> Option { fn subscribe_operation_ext( entry: OperationLogEntry, - hash: [u8; 32], operation_id: OperationId, fedimint_client: ClientHandleArc, logger: Arc, stop: Arc, storage: S, ) { - let lightning_meta: LightningOperationMeta = entry.meta(); spawn(async move { - let lightning_module = - Arc::new(fedimint_client.get_first_module::()); - - if let Some(updated_invoice) = process_operation_until_timeout( + process_operation_until_timeout( logger.clone(), - lightning_meta, - hash, + entry, operation_id, - &lightning_module, + fedimint_client, + storage, None, stop, ) - .await - { - match maybe_update_after_checking_fedimint( - updated_invoice.clone(), - logger.clone(), - storage, - ) { - Ok(_) => { - log_debug!(logger, "subscribed and updated federation operation") - } - Err(e) => { - log_error!(logger, "could not update federation operation: {e}") - } - } - } + .await; }); } @@ -1071,71 +1093,167 @@ pub(crate) fn mnemonic_from_xpriv(xpriv: ExtendedPrivKey) -> Result( logger: Arc, - lightning_meta: LightningOperationMeta, - hash: [u8; 32], + entry: OperationLogEntry, operation_id: OperationId, - lightning_module: &Arc>, + fedimint_client: ClientHandleArc, + storage: S, timeout: Option, stop: Arc, -) -> Option { - match lightning_meta.variant { - LightningOperationMetaVariant::Pay(pay_meta) => { - let invoice = convert_from_fedimint_invoice(&pay_meta.invoice); - if invoice.payment_hash().into_32() == hash { - match lightning_module.subscribe_ln_pay(operation_id).await { - Ok(o) => Some( - process_outcome( - o, - process_pay_state_ln, - invoice, - false, - timeout, - stop, - logger, - ) - .await, - ), - Err(e) => { - log_error!(logger, "Error trying to process stream outcome: {e}"); +) { + let module_type = entry.operation_module_kind(); + if module_type == LightningCommonInit::KIND.as_str() { + let lightning_meta: LightningOperationMeta = entry.meta(); - // return the latest status of the invoice even if it fails - Some(invoice.into()) + let lightning_module = + Arc::new(fedimint_client.get_first_module::()); + + let updated_invoice = match lightning_meta.variant { + LightningOperationMetaVariant::Pay(pay_meta) => { + let hash = pay_meta.invoice.payment_hash().into_inner(); + let invoice = convert_from_fedimint_invoice(&pay_meta.invoice); + if invoice.payment_hash().into_32() == hash { + match lightning_module.subscribe_ln_pay(operation_id).await { + Ok(o) => Some( + process_ln_outcome( + o, + process_pay_state_ln, + invoice, + false, + timeout, + stop, + logger.clone(), + ) + .await, + ), + Err(e) => { + log_error!(logger, "Error trying to process stream outcome: {e}"); + + // return the latest status of the invoice even if it fails + Some(invoice.into()) + } } + } else { + None + } + } + LightningOperationMetaVariant::Receive { invoice, .. } => { + let hash = invoice.payment_hash().into_inner(); + let invoice = convert_from_fedimint_invoice(&invoice); + if invoice.payment_hash().into_32() == hash { + match lightning_module.subscribe_ln_receive(operation_id).await { + Ok(o) => Some( + process_ln_outcome( + o, + process_receive_state, + invoice, + true, + timeout, + stop, + logger.clone(), + ) + .await, + ), + Err(e) => { + log_error!(logger, "Error trying to process stream outcome: {e}"); + + // return the latest status of the invoice even if it fails + Some(invoice.into()) + } + } + } else { + None } - } else { - None } + LightningOperationMetaVariant::Claim { .. } => None, + }; + + if let Some(updated_invoice) = updated_invoice { + match maybe_update_after_checking_fedimint( + updated_invoice.clone(), + logger.clone(), + storage, + ) { + Ok(_) => { + log_debug!(logger, "subscribed and updated federation operation") + } + Err(e) => { + log_error!(logger, "could not update federation operation: {e}") + } + } + } + } else if module_type == WalletCommonInit::KIND.as_str() { + let wallet_meta: WalletOperationMeta = entry.meta(); + let wallet_module = Arc::new(fedimint_client.get_first_module::()); + let internal_id = Txid::from_slice(&operation_id.0) + .map_err(|_| MutinyError::ChainAccessFailed) + .expect("should convert"); + let stored_transaction_details = get_transaction_details(&storage, internal_id, &logger); + if stored_transaction_details.is_none() { + log_warn!(logger, "could not find transaction details: {internal_id}") } - LightningOperationMetaVariant::Receive { invoice, .. } => { - let invoice = convert_from_fedimint_invoice(&invoice); - if invoice.payment_hash().into_32() == hash { - match lightning_module.subscribe_ln_receive(operation_id).await { - Ok(o) => Some( - process_outcome( + + match wallet_meta.variant { + fedimint_wallet_client::WalletOperationMetaVariant::Deposit { + address: _, + expires_at: _, + } => { + match wallet_module.subscribe_deposit_updates(operation_id).await { + Ok(o) => { + process_onchain_deposit_outcome( o, - process_receive_state, - invoice, - true, + stored_transaction_details, + operation_id, + storage, timeout, stop, logger, ) - .await, - ), + .await + } Err(e) => { log_error!(logger, "Error trying to process stream outcome: {e}"); - - // return the latest status of the invoice even if it fails - Some(invoice.into()) + } + }; + } + fedimint_wallet_client::WalletOperationMetaVariant::Withdraw { + address: _, + amount: _, + fee: _, + change: _, + } => { + // TODO + let mut updates = wallet_module + .subscribe_withdraw_updates(operation_id) + .await + .expect("should stream") + .into_stream(); // TODO non-stream version + + while let Some(update) = updates.next().await { + match update { + WithdrawState::Succeeded(txid) => { + log_info!(logger, "Withdraw successful, txid: {txid}"); + // TODO update state + } + WithdrawState::Failed(e) => { + log_error!(logger, "Withdraw failed: {e}"); + // TODO update state + } + WithdrawState::Created => { + log_debug!(logger, "Withdraw created"); + // TODO update state + } } } - } else { - None + } + fedimint_wallet_client::WalletOperationMetaVariant::RbfWithdraw { .. } => { + // not supported yet + unimplemented!("User RBF withdrawals not supported yet") } } - LightningOperationMetaVariant::Claim { .. } => None, + } else { + log_warn!(logger, "Unknown module type: {module_type}") } } @@ -1163,7 +1281,7 @@ fn process_receive_state(receive_state: LnReceiveState, invoice: &mut MutinyInvo invoice.status = receive_state.into(); } -async fn process_outcome( +async fn process_ln_outcome( stream_or_outcome: UpdateStreamOrOutcome, process_fn: F, invoice: Bolt11Invoice, @@ -1244,6 +1362,135 @@ where invoice } +async fn process_onchain_deposit_outcome( + stream_or_outcome: UpdateStreamOrOutcome, + original_transaction_details: Option, + operation_id: OperationId, + storage: S, + timeout: Option, + stop: Arc, + logger: Arc, +) { + let labels = original_transaction_details + .as_ref() + .map(|o| o.labels.clone()) + .unwrap_or_default(); + + match stream_or_outcome { + UpdateStreamOrOutcome::Outcome(outcome) => { + // TODO + log_trace!(logger, "Outcome received: {:?}", outcome); + } + UpdateStreamOrOutcome::UpdateStream(mut s) => { + // break out after sleep time or check stop signal + log_trace!(logger, "start timeout stream futures"); + loop { + let timeout_future = if let Some(t) = timeout { + sleep(t as i32) + } else { + sleep(1_000_i32) + }; + + let mut stream_fut = Box::pin(s.next()).fuse(); + let delay_fut = Box::pin(timeout_future).fuse(); + pin_mut!(delay_fut); + + select! { + outcome_option = stream_fut => { + if let Some(outcome) = outcome_option { + // TODO refactor outcome parsing into seperate method + match outcome { + fedimint_wallet_client::DepositState::WaitingForTransaction => { + // Nothing to do + log_debug!(logger, "Waiting for transaction"); + } + fedimint_wallet_client::DepositState::WaitingForConfirmation(tx) => { + // Pending state, update with info we have + log_debug!(logger, "Waiting for confirmation"); + let txid = Txid::from_slice(&tx.btc_transaction.txid()).expect("should convert"); + let internal_id = Txid::from_slice(&operation_id.0).expect("should convert"); + let output = tx.btc_transaction.output[tx.out_idx as usize].clone(); + + let updated_transaction_details = TransactionDetails { + transaction: None, + txid: Some(txid), + internal_id, + received: output.value, + sent: 0, + fee: None, + confirmation_time: ConfirmationTime::Unconfirmed { last_seen: now().as_secs() }, + labels: labels.clone(), + }; + + match persist_transaction_details(&storage, &updated_transaction_details) { + Ok(_) => { + log_info!(logger, "Transaction updated"); + }, + Err(e) => { + log_error!(logger, "Error updating transaction: {e}"); + }, + } + } + fedimint_wallet_client::DepositState::Confirmed(tx) => { + // Pending state, update with info we have + log_debug!(logger, "Transaction confirmed"); + let txid = Txid::from_slice(&tx.btc_transaction.txid()).expect("should convert"); + let internal_id = Txid::from_slice(&operation_id.0).expect("should convert"); + let output = tx.btc_transaction.output[tx.out_idx as usize].clone(); + + let updated_transaction_details = TransactionDetails { + transaction: None, + txid: Some(txid), + internal_id, + received: output.value, + sent: 0, + fee: None, + confirmation_time: ConfirmationTime::Confirmed { height: 0, time: now().as_secs() }, // FIXME: can't figure this out + labels: labels.clone(), + }; + + match persist_transaction_details(&storage, &updated_transaction_details) { + Ok(_) => { + log_info!(logger, "Transaction updated"); + }, + Err(e) => { + log_error!(logger, "Error updating transaction: {e}"); + }, + } + } + fedimint_wallet_client::DepositState::Claimed(_) => { + // Nothing really to change from confirmed to claimed + log_debug!(logger, "Transaction claimed"); + break; + } + fedimint_wallet_client::DepositState::Failed(e) => { + // TODO delete + log_error!(logger, "Transaction failed: {e}"); + break; + } + } + } + } + _ = delay_fut => { + if timeout.is_none() { + if stop.load(Ordering::Relaxed) { + break; + } + } else { + log_debug!( + logger, + "Timeout reached, exiting loop for on chain tx", + ); + break; + } + } + } + } + log_trace!(logger, "Done with stream outcome",); + } + } +} + #[derive(Clone)] pub struct FedimintStorage { pub(crate) storage: S, diff --git a/mutiny-core/src/lib.rs b/mutiny-core/src/lib.rs index af461538a..9e934c5e1 100644 --- a/mutiny-core/src/lib.rs +++ b/mutiny-core/src/lib.rs @@ -43,15 +43,16 @@ pub mod vss; #[cfg(test)] mod test_utils; +use crate::federation::get_federation_identity; pub use crate::gossip::{GOSSIP_SYNC_TIME_KEY, NETWORK_GRAPH_KEY, PROB_SCORER_KEY}; pub use crate::keymanager::generate_seed; pub use crate::ldkstorage::{CHANNEL_CLOSURE_PREFIX, CHANNEL_MANAGER_KEY, MONITORS_PREFIX_KEY}; -pub use bitcoin; -pub use fedimint_core; -pub use lightning; -pub use lightning_invoice; -pub use nostr_sdk; - +use crate::storage::{ + get_payment_hash_from_key, get_transaction_details, list_payment_info, persist_payment_info, + update_nostr_contact_list, IndexItem, MutinyStorage, DEVICE_ID_KEY, EXPECTED_NETWORK_KEY, + NEED_FULL_SYNC_KEY, ONCHAIN_PREFIX, PAYMENT_INBOUND_PREFIX_KEY, PAYMENT_OUTBOUND_PREFIX_KEY, + SUBSCRIPTION_TIMESTAMP, TRANSACTION_DETAILS_PREFIX_KEY, +}; use crate::utils::spawn; use crate::{auth::MutinyAuthClient, hermes::HermesClient, logging::MutinyLogger}; use crate::{blindauth::BlindAuthClient, cashu::CashuHttpClient}; @@ -60,15 +61,6 @@ use crate::{ event::{HTLCStatus, MillisatAmount, PaymentInfo}, onchain::FULL_SYNC_STOP_GAP, }; -use crate::{ - federation::get_federation_identity, - storage::{ - get_payment_hash_from_key, list_payment_info, persist_payment_info, - update_nostr_contact_list, IndexItem, MutinyStorage, DEVICE_ID_KEY, EXPECTED_NETWORK_KEY, - NEED_FULL_SYNC_KEY, ONCHAIN_PREFIX, PAYMENT_INBOUND_PREFIX_KEY, - PAYMENT_OUTBOUND_PREFIX_KEY, SUBSCRIPTION_TIMESTAMP, - }, -}; use crate::{ federation::{ FederationClient, FederationIdentity, FederationIndex, FederationStorage, GatewayFees, @@ -78,7 +70,7 @@ use crate::{ }; use crate::{ lnurlauth::make_lnurl_auth_connection, - nodemanager::{ChannelClosure, MutinyBip21RawMaterials, TransactionDetails}, + nodemanager::{ChannelClosure, MutinyBip21RawMaterials}, }; use crate::{lnurlauth::AuthManager, nostr::MUTINY_PLUS_SUBSCRIPTION_LABEL}; use crate::{logging::LOGGING_KEY, nodemanager::NodeManagerBuilder}; @@ -103,20 +95,24 @@ use ::nostr::{EventBuilder, EventId, JsonUtil, Keys, Kind}; use async_lock::RwLock; use bdk_chain::ConfirmationTime; use bip39::Mnemonic; -use bitcoin::bip32::ExtendedPrivKey; +pub use bitcoin; use bitcoin::hashes::Hash; use bitcoin::secp256k1::{PublicKey, ThirtyTwoByteHash}; +use bitcoin::{bip32::ExtendedPrivKey, Transaction}; use bitcoin::{hashes::sha256, Network, Txid}; +pub use fedimint_core; use fedimint_core::{api::InviteCode, config::FederationId}; use futures::{pin_mut, select, FutureExt}; use futures_util::join; use futures_util::lock::Mutex; use hex_conservative::{DisplayHex, FromHex}; use itertools::Itertools; +pub use lightning; use lightning::chain::BestBlock; use lightning::ln::PaymentHash; use lightning::util::logger::Logger; use lightning::{log_debug, log_error, log_info, log_trace, log_warn}; +pub use lightning_invoice; use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription}; use lnurl::{lnurl::LnUrl, AsyncClient as LnUrlClient, LnUrlResponse, Response}; use moksha_core::primitives::{ @@ -124,6 +120,7 @@ use moksha_core::primitives::{ PostMeltQuoteBolt11Response, }; use moksha_core::token::TokenV3; +pub use nostr_sdk; use nostr_sdk::{Client, NostrSigner, RelayPoolNotification}; use reqwest::multipart::{Form, Part}; use serde::{Deserialize, Serialize}; @@ -226,6 +223,57 @@ pub enum ActivityItem { ChannelClosed(ChannelClosure), } +/// A wallet transaction +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +pub struct TransactionDetails { + /// Optional transaction + pub transaction: Option, + /// Transaction id + pub txid: Option, + /// Internal id before a transaction id is created + pub internal_id: Txid, + /// Received value (sats) + /// Sum of owned outputs of this transaction. + pub received: u64, + /// Sent value (sats) + /// Sum of owned inputs of this transaction. + pub sent: u64, + /// Fee value in sats if it was available. + pub fee: Option, + /// If the transaction is confirmed, contains height and Unix timestamp of the block containing the + /// transaction, unconfirmed transaction contains `None`. + pub confirmation_time: ConfirmationTime, + /// Labels associated with this transaction + pub labels: Vec, +} + +impl PartialOrd for TransactionDetails { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for TransactionDetails { + fn cmp(&self, other: &Self) -> core::cmp::Ordering { + match (self.confirmation_time, other.confirmation_time) { + (ConfirmationTime::Confirmed { .. }, ConfirmationTime::Confirmed { .. }) => self + .confirmation_time + .cmp(&self.confirmation_time) + .then_with(|| self.txid.cmp(&other.txid)), + (ConfirmationTime::Confirmed { .. }, ConfirmationTime::Unconfirmed { .. }) => { + core::cmp::Ordering::Less + } + (ConfirmationTime::Unconfirmed { .. }, ConfirmationTime::Confirmed { .. }) => { + core::cmp::Ordering::Greater + } + ( + ConfirmationTime::Unconfirmed { last_seen: a }, + ConfirmationTime::Unconfirmed { last_seen: b }, + ) => a.cmp(&b).then_with(|| self.txid.cmp(&other.txid)), + } + } +} + impl ActivityItem { pub fn last_updated(&self) -> Option { match self { @@ -1036,10 +1084,25 @@ impl MutinyWalletBuilder { ConfirmationTime::Confirmed { time, .. } => Some(time), ConfirmationTime::Unconfirmed { .. } => None, }, - key: format!("{ONCHAIN_PREFIX}{}", t.txid), + key: format!("{ONCHAIN_PREFIX}{}", t.internal_id), }) .collect::>(); + // add any transaction details stored from fedimint + let transaction_details = self + .storage + .scan::(TRANSACTION_DETAILS_PREFIX_KEY, None)? + .into_iter() + .map(|(k, v)| { + let timestamp = match v.confirmation_time { + ConfirmationTime::Confirmed { height: _, time } => Some(time), // confirmed timestamp + ConfirmationTime::Unconfirmed { .. } => None, // unconfirmed timestamp + }; + IndexItem { timestamp, key: k } + }) + .collect::>(); + activity_index.extend(transaction_details); + // add the channel closures to the activity index let closures = self .storage @@ -1581,7 +1644,7 @@ impl MutinyWallet { ) }; - let Ok(address) = self.node_manager.get_new_address(labels.clone()) else { + let Ok(address) = self.create_address(labels.clone()).await else { return Err(MutinyError::WalletOperationFailed); }; @@ -1790,6 +1853,29 @@ impl MutinyWallet { Ok(Some(lsp_fee + federation_fee)) } + async fn create_address(&self, labels: Vec) -> Result { + // Attempt to create federation invoice if available + let federation_ids = self.list_federation_ids().await?; + if !federation_ids.is_empty() { + let federation_id = &federation_ids[0]; + let fedimint_client = self.federations.read().await.get(federation_id).cloned(); + + if let Some(client) = fedimint_client { + if let Ok(addr) = client.get_new_address(labels.clone()).await { + self.storage.set_address_labels(addr.clone(), labels)?; + return Ok(addr); + } + } + } + + // Fallback to node_manager address creation + let Ok(addr) = self.node_manager.get_new_address(labels.clone()) else { + return Err(MutinyError::WalletOperationFailed); + }; + + Ok(addr) + } + async fn create_lightning_invoice( &self, amount: u64, @@ -1923,12 +2009,35 @@ impl MutinyWallet { activities.push(ActivityItem::OnChain(tx_details)); } } + } else if item.key.starts_with(TRANSACTION_DETAILS_PREFIX_KEY) { + // convert keys to internal transaction id + let internal_id_str = item.key.trim_start_matches(TRANSACTION_DETAILS_PREFIX_KEY); + let internal_id: Txid = Txid::from_str(internal_id_str)?; + if let Some(tx_details) = + get_transaction_details(&self.storage, internal_id, &self.logger) + { + // make sure it is a relevant transaction + if tx_details.sent != 0 || tx_details.received != 0 { + activities.push(ActivityItem::OnChain(tx_details)); + } + } } } Ok(activities) } + pub fn get_transaction(&self, txid: Txid) -> Result, MutinyError> { + // check our local cache/state for fedimint first + match get_transaction_details(&self.storage, txid, &self.logger) { + Some(t) => Ok(Some(t)), + None => { + // fall back to node manager + self.node_manager.get_transaction(txid) + } + } + } + /// Returns all the lightning activity for a given label pub async fn get_label_activity( &self, @@ -3456,9 +3565,6 @@ mod tests { #[cfg(test)] #[cfg(target_arch = "wasm32")] mod tests { - use crate::event::{HTLCStatus, MillisatAmount, PaymentInfo}; - use crate::ldkstorage::CHANNEL_CLOSURE_PREFIX; - use crate::nodemanager::ChannelClosure; use crate::storage::{ payment_key, persist_payment_info, IndexItem, MemoryStorage, MutinyStorage, ONCHAIN_PREFIX, PAYMENT_OUTBOUND_PREFIX_KEY, @@ -3467,12 +3573,18 @@ mod tests { encrypt::encryption_key_from_pass, generate_seed, max_routing_fee_amount, nodemanager::NodeManager, MutinyWallet, MutinyWalletBuilder, MutinyWalletConfigBuilder, }; + use crate::{ + event::{HTLCStatus, MillisatAmount, PaymentInfo}, + TransactionDetails, + }; + use crate::{ldkstorage::CHANNEL_CLOSURE_PREFIX, storage::persist_transaction_details}; + use crate::{nodemanager::ChannelClosure, storage::TRANSACTION_DETAILS_PREFIX_KEY}; use bdk_chain::{BlockId, ConfirmationTime}; - use bitcoin::absolute::LockTime; use bitcoin::bip32::ExtendedPrivKey; use bitcoin::hashes::hex::FromHex; use bitcoin::hashes::Hash; use bitcoin::secp256k1::PublicKey; + use bitcoin::{absolute::LockTime, Txid}; use bitcoin::{BlockHash, Network, Transaction, TxOut}; use hex_conservative::DisplayHex; use itertools::Itertools; @@ -3985,6 +4097,20 @@ mod tests { }; persist_payment_info(&storage, &payment_hash4, &invoice4, false).unwrap(); + let transaction_details1 = TransactionDetails { + transaction: None, + txid: Some(Txid::all_zeros()), + internal_id: Txid::all_zeros(), + received: 0, + sent: 10_000, + fee: Some(100), + confirmation_time: ConfirmationTime::Unconfirmed { + last_seen: now().as_secs(), + }, + labels: vec![], + }; + persist_transaction_details(&storage, &transaction_details1).unwrap(); + let vec = { let index = storage.activity_index(); let vec = index.read().unwrap().clone().into_iter().collect_vec(); @@ -4010,6 +4136,13 @@ mod tests { payment_hash3.to_lower_hex_string() ), }, + IndexItem { + timestamp: None, + key: format!( + "{TRANSACTION_DETAILS_PREFIX_KEY}{}", + transaction_details1.internal_id + ), + }, IndexItem { timestamp: Some(invoice2.last_update), key: format!( diff --git a/mutiny-core/src/nodemanager.rs b/mutiny-core/src/nodemanager.rs index dd891a2ae..c67163283 100644 --- a/mutiny-core/src/nodemanager.rs +++ b/mutiny-core/src/nodemanager.rs @@ -1,10 +1,10 @@ -use crate::auth::MutinyAuthClient; use crate::labels::LabelStorage; use crate::ldkstorage::CHANNEL_CLOSURE_PREFIX; use crate::logging::LOGGING_KEY; use crate::utils::{sleep, spawn}; use crate::MutinyInvoice; use crate::MutinyWalletConfig; +use crate::{auth::MutinyAuthClient, TransactionDetails}; use crate::{ chain::MutinyChain, error::MutinyError, @@ -167,55 +167,6 @@ impl From<&ChannelDetails> for MutinyChannel { } } -/// A wallet transaction -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] -pub struct TransactionDetails { - /// Optional transaction - pub transaction: Option, - /// Transaction id - pub txid: Txid, - /// Received value (sats) - /// Sum of owned outputs of this transaction. - pub received: u64, - /// Sent value (sats) - /// Sum of owned inputs of this transaction. - pub sent: u64, - /// Fee value in sats if it was available. - pub fee: Option, - /// If the transaction is confirmed, contains height and Unix timestamp of the block containing the - /// transaction, unconfirmed transaction contains `None`. - pub confirmation_time: ConfirmationTime, - /// Labels associated with this transaction - pub labels: Vec, -} - -impl PartialOrd for TransactionDetails { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for TransactionDetails { - fn cmp(&self, other: &Self) -> core::cmp::Ordering { - match (self.confirmation_time, other.confirmation_time) { - (ConfirmationTime::Confirmed { .. }, ConfirmationTime::Confirmed { .. }) => self - .confirmation_time - .cmp(&self.confirmation_time) - .then_with(|| self.txid.cmp(&other.txid)), - (ConfirmationTime::Confirmed { .. }, ConfirmationTime::Unconfirmed { .. }) => { - core::cmp::Ordering::Less - } - (ConfirmationTime::Unconfirmed { .. }, ConfirmationTime::Confirmed { .. }) => { - core::cmp::Ordering::Greater - } - ( - ConfirmationTime::Unconfirmed { last_seen: a }, - ConfirmationTime::Unconfirmed { last_seen: b }, - ) => a.cmp(&b).then_with(|| self.txid.cmp(&other.txid)), - } - } -} - /// Information about a channel that was closed. #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash)] pub struct ChannelClosure { @@ -885,7 +836,8 @@ impl NodeManager { let details = TransactionDetails { transaction: Some(tx.to_tx()), - txid: tx.txid, + txid: Some(tx.txid), + internal_id: tx.txid, received, sent: 0, fee: None, @@ -2047,12 +1999,12 @@ mod tests { assert_eq!(txs.len(), 1); let tx = &txs[0]; - assert_eq!(tx.txid, fake_tx.txid()); + assert_eq!(tx.txid, Some(fake_tx.txid())); assert_eq!(tx.labels, labels); assert!(tx_opt.is_some()); let tx = tx_opt.unwrap(); - assert_eq!(tx.txid, fake_tx.txid()); + assert_eq!(tx.txid, Some(fake_tx.txid())); assert_eq!(tx.labels, labels); } @@ -2219,7 +2171,8 @@ mod tests { let tx1: TransactionDetails = TransactionDetails { transaction: None, - txid: Txid::all_zeros(), + txid: Some(Txid::all_zeros()), + internal_id: Txid::all_zeros(), received: 0, sent: 0, fee: None, @@ -2229,7 +2182,8 @@ mod tests { let tx2: TransactionDetails = TransactionDetails { transaction: None, - txid: Txid::all_zeros(), + txid: Some(Txid::all_zeros()), + internal_id: Txid::all_zeros(), received: 0, sent: 0, fee: None, diff --git a/mutiny-core/src/onchain.rs b/mutiny-core/src/onchain.rs index 7cad3d3bc..d46574a07 100644 --- a/mutiny-core/src/onchain.rs +++ b/mutiny-core/src/onchain.rs @@ -25,12 +25,12 @@ use crate::error::MutinyError; use crate::fees::MutinyFeeEstimator; use crate::labels::*; use crate::logging::MutinyLogger; -use crate::nodemanager::TransactionDetails; use crate::storage::{ IndexItem, MutinyStorage, OnChainStorage, KEYCHAIN_STORE_KEY, NEED_FULL_SYNC_KEY, ONCHAIN_PREFIX, }; use crate::utils::{now, sleep}; +use crate::TransactionDetails; pub(crate) const FULL_SYNC_STOP_GAP: usize = 150; pub(crate) const RESTORE_SYNC_STOP_GAP: usize = 20; @@ -152,7 +152,7 @@ impl OnChainWallet { ConfirmationTime::Confirmed { time, .. } => Some(time), ConfirmationTime::Unconfirmed { .. } => None, }, - key: format!("{ONCHAIN_PREFIX}{}", t.txid), + key: format!("{ONCHAIN_PREFIX}{}", t.internal_id), }) .collect::>(); @@ -381,7 +381,8 @@ impl OnChainWallet { Some(TransactionDetails { transaction, - txid: tx.tx_node.txid, + txid: Some(tx.tx_node.txid), + internal_id: tx.tx_node.txid, received, sent, fee, @@ -413,7 +414,8 @@ impl OnChainWallet { let fee = wallet.calculate_fee(tx.tx_node.tx).ok(); let details = TransactionDetails { transaction: Some(tx.tx_node.tx.to_owned()), - txid, + txid: Some(txid), + internal_id: txid, received, sent, fee, diff --git a/mutiny-core/src/storage.rs b/mutiny-core/src/storage.rs index 1884ee4cf..a6be6c796 100644 --- a/mutiny-core/src/storage.rs +++ b/mutiny-core/src/storage.rs @@ -1,4 +1,3 @@ -use crate::labels::LabelStorage; use crate::nodemanager::{ChannelClosure, NodeStorage}; use crate::utils::{now, spawn}; use crate::vss::{MutinyVssClient, VssKeyValueItem}; @@ -13,11 +12,13 @@ use crate::{ event::PaymentInfo, }; use crate::{event::HTLCStatus, MutinyInvoice}; +use crate::{labels::LabelStorage, TransactionDetails}; use crate::{ldkstorage::CHANNEL_MANAGER_KEY, utils::sleep}; use async_trait::async_trait; use bdk::chain::{Append, PersistBackend}; use bip39::Mnemonic; -use bitcoin::secp256k1::ThirtyTwoByteHash; +use bitcoin::{secp256k1::ThirtyTwoByteHash, Txid}; +use fedimint_ln_common::bitcoin::hashes::hex::ToHex; use futures_util::lock::Mutex; use hex_conservative::*; use lightning::{ln::PaymentHash, util::logger::Logger}; @@ -45,6 +46,7 @@ pub const DEVICE_LOCK_KEY: &str = "device_lock"; pub(crate) const EXPECTED_NETWORK_KEY: &str = "network"; pub const PAYMENT_INBOUND_PREFIX_KEY: &str = "payment_inbound/"; pub const PAYMENT_OUTBOUND_PREFIX_KEY: &str = "payment_outbound/"; +pub const TRANSACTION_DETAILS_PREFIX_KEY: &str = "transaction_details/"; pub(crate) const ONCHAIN_PREFIX: &str = "onchain_tx/"; pub const LAST_DM_SYNC_TIME_KEY: &str = "last_dm_sync_time"; pub const LAST_HERMES_SYNC_TIME_KEY: &str = "last_hermes_sync_time"; @@ -881,6 +883,62 @@ impl MutinyStorage for () { } } +pub(crate) fn transaction_details_key(internal_id: Txid) -> String { + format!( + "{}{}", + TRANSACTION_DETAILS_PREFIX_KEY, + internal_id.to_raw_hash().to_hex(), + ) +} + +pub(crate) fn persist_transaction_details( + storage: &S, + transaction_details: &TransactionDetails, +) -> Result<(), MutinyError> { + let key = transaction_details_key(transaction_details.internal_id); + storage.set_data(key.clone(), transaction_details, None)?; + + // insert into activity index + match transaction_details.confirmation_time { + bdk_chain::ConfirmationTime::Confirmed { height: _, time } => { + let index = storage.activity_index(); + let mut index = index.try_write()?; + // remove old version + index.remove(&IndexItem { + timestamp: None, // timestamp would be None for Unconfirmed + key: key.clone(), + }); + index.insert(IndexItem { + timestamp: Some(time), + key, + }); + } + bdk_chain::ConfirmationTime::Unconfirmed { .. } => { + let index = storage.activity_index(); + let mut index = index.try_write()?; + index.insert(IndexItem { + timestamp: None, + key, + }); + } + } + + Ok(()) +} + +pub(crate) fn get_transaction_details( + storage: &S, + internal_id: Txid, + logger: &MutinyLogger, +) -> Option { + let key = transaction_details_key(internal_id); + log_trace!(logger, "Trace: checking payment key: {key}"); + match storage.get_data(&key).transpose() { + Some(Ok(v)) => Some(v), + _ => None, + } +} + pub(crate) fn payment_key(inbound: bool, payment_hash: &[u8; 32]) -> String { if inbound { format!("{}{}", PAYMENT_INBOUND_PREFIX_KEY, payment_hash.as_hex()) diff --git a/mutiny-wasm/src/lib.rs b/mutiny-wasm/src/lib.rs index b0033a227..8b4d45d90 100644 --- a/mutiny-wasm/src/lib.rs +++ b/mutiny-wasm/src/lib.rs @@ -700,9 +700,7 @@ impl MutinyWallet { txid: String, ) -> Result */, MutinyJsError> { let txid = Txid::from_str(&txid)?; - Ok(JsValue::from_serde( - &self.inner.node_manager.get_transaction(txid)?, - )?) + Ok(JsValue::from_serde(&self.inner.get_transaction(txid)?)?) } /// Gets the current balance of the wallet. diff --git a/mutiny-wasm/src/models.rs b/mutiny-wasm/src/models.rs index 279632cf2..5bc00396d 100644 --- a/mutiny-wasm/src/models.rs +++ b/mutiny-wasm/src/models.rs @@ -75,7 +75,7 @@ impl From for ActivityItem { }; let id = match a { - mutiny_core::ActivityItem::OnChain(ref t) => t.txid.to_string(), + mutiny_core::ActivityItem::OnChain(ref t) => t.internal_id.to_string(), mutiny_core::ActivityItem::Lightning(ref ln) => { ln.payment_hash.into_32().to_lower_hex_string() } From 6b106eda87fbb8ba9fecb39cd0e82c0ce8923480 Mon Sep 17 00:00:00 2001 From: Tony Giorgio Date: Thu, 2 May 2024 16:40:37 -0500 Subject: [PATCH 2/8] Add on chain withdraw --- mutiny-core/src/federation.rs | 229 ++++++++++++++++++++++++++++++---- mutiny-core/src/lib.rs | 57 ++++++++- mutiny-wasm/src/lib.rs | 1 - 3 files changed, 260 insertions(+), 27 deletions(-) diff --git a/mutiny-core/src/federation.rs b/mutiny-core/src/federation.rs index bac11790d..acc96c3d4 100644 --- a/mutiny-core/src/federation.rs +++ b/mutiny-core/src/federation.rs @@ -20,6 +20,7 @@ use async_trait::async_trait; use bdk_chain::ConfirmationTime; use bip39::Mnemonic; use bitcoin::{ + address::NetworkUnchecked, bip32::{ChildNumber, DerivationPath, ExtendedPrivKey}, hashes::Hash, secp256k1::{Secp256k1, SecretKey, ThirtyTwoByteHash}, @@ -33,6 +34,7 @@ use fedimint_client::{ secret::{get_default_client_secret, RootSecretStrategy}, ClientHandleArc, }; +use fedimint_core::bitcoin_migration::bitcoin30_to_bitcoin29_address; use fedimint_core::config::ClientConfig; use fedimint_core::{ api::InviteCode, @@ -257,6 +259,7 @@ pub(crate) struct FederationClient { #[allow(dead_code)] fedimint_storage: FedimintStorage, gateway: Arc>>, + network: Network, stop: Arc, pub(crate) logger: Arc, } @@ -397,6 +400,7 @@ impl FederationClient { storage, logger, invite_code: federation_code, + network, stop, gateway, }; @@ -714,6 +718,85 @@ impl FederationClient { } } + /// Send on chain transaction + pub(crate) async fn send_onchain( + &self, + send_to: bitcoin::Address, + amount: u64, + labels: Vec, + ) -> Result { + let address = bitcoin30_to_bitcoin29_address(send_to.require_network(self.network)?); + + let btc_amount = fedimint_ln_common::bitcoin::Amount::from_sat(amount); + + let wallet_module = self + .fedimint_client + .get_first_module::(); + + let peg_out_fees = wallet_module + .get_withdraw_fees(address.clone(), btc_amount) + .await?; + + let op_id = wallet_module + .withdraw(address, btc_amount, peg_out_fees, ()) + .await?; + + let internal_id = Txid::from_slice(&op_id.0).map_err(|_| MutinyError::ChainAccessFailed)?; + + let pending_transaction_details = TransactionDetails { + transaction: None, + txid: None, + internal_id, + received: 0, + sent: amount, + fee: Some(peg_out_fees.amount().to_sat()), + confirmation_time: ConfirmationTime::Unconfirmed { + last_seen: now().as_secs(), + }, + labels, + }; + + persist_transaction_details(&self.storage, &pending_transaction_details)?; + + // subscribe + let operation = self + .fedimint_client + .operation_log() + .get_operation(op_id) + .await + .expect("just created it"); + + // Subscribe for a little bit, just to hopefully get transaction id + process_operation_until_timeout( + self.logger.clone(), + operation, + op_id, + self.fedimint_client.clone(), + self.storage.clone(), + Some(DEFAULT_PAYMENT_TIMEOUT * 1_000), + self.stop.clone(), + ) + .await; + + // now check the status of the payment from storage + if let Some(t) = get_transaction_details(&self.storage, internal_id, &self.logger) { + if t.txid.is_some() { + return Ok(internal_id); + } + } + + // keep subscribing if txid wasn't retrieved, but then return timeout + let operation = self + .fedimint_client + .operation_log() + .get_operation(op_id) + .await + .expect("just created it"); + self.subscribe_operation(operation, op_id); + + Err(MutinyError::PaymentTimeout) + } + /// Someone received a payment on our behalf, we need to claim it pub async fn claim_external_receive( &self, @@ -1219,33 +1302,29 @@ async fn process_operation_until_timeout( } fedimint_wallet_client::WalletOperationMetaVariant::Withdraw { address: _, - amount: _, - fee: _, + amount, + fee, change: _, } => { - // TODO - let mut updates = wallet_module - .subscribe_withdraw_updates(operation_id) - .await - .expect("should stream") - .into_stream(); // TODO non-stream version - - while let Some(update) = updates.next().await { - match update { - WithdrawState::Succeeded(txid) => { - log_info!(logger, "Withdraw successful, txid: {txid}"); - // TODO update state - } - WithdrawState::Failed(e) => { - log_error!(logger, "Withdraw failed: {e}"); - // TODO update state - } - WithdrawState::Created => { - log_debug!(logger, "Withdraw created"); - // TODO update state - } + match wallet_module.subscribe_withdraw_updates(operation_id).await { + Ok(o) => { + process_onchain_withdraw_outcome( + o, + stored_transaction_details, + amount, + fee.amount(), + operation_id, + storage, + timeout, + stop, + logger, + ) + .await } - } + Err(e) => { + log_error!(logger, "Error trying to process stream outcome: {e}"); + } + }; } fedimint_wallet_client::WalletOperationMetaVariant::RbfWithdraw { .. } => { // not supported yet @@ -1362,6 +1441,108 @@ where invoice } +// FIXME: refactor +#[allow(clippy::too_many_arguments)] +async fn process_onchain_withdraw_outcome( + stream_or_outcome: UpdateStreamOrOutcome, + original_transaction_details: Option, + amount: fedimint_ln_common::bitcoin::Amount, + fee: fedimint_ln_common::bitcoin::Amount, + operation_id: OperationId, + storage: S, + timeout: Option, + stop: Arc, + logger: Arc, +) { + let labels = original_transaction_details + .as_ref() + .map(|o| o.labels.clone()) + .unwrap_or_default(); + + match stream_or_outcome { + UpdateStreamOrOutcome::Outcome(outcome) => { + // TODO + log_trace!(logger, "Outcome received: {:?}", outcome); + } + UpdateStreamOrOutcome::UpdateStream(mut s) => { + // break out after sleep time or check stop signal + log_trace!(logger, "start timeout stream futures"); + loop { + let timeout_future = if let Some(t) = timeout { + sleep(t as i32) + } else { + sleep(1_000_i32) + }; + + let mut stream_fut = Box::pin(s.next()).fuse(); + let delay_fut = Box::pin(timeout_future).fuse(); + pin_mut!(delay_fut); + + select! { + outcome_option = stream_fut => { + if let Some(outcome) = outcome_option { + // TODO refactor outcome parsing into seperate method + match outcome { + WithdrawState::Created => { + // Nothing to do + log_debug!(logger, "Waiting for withdraw"); + }, + WithdrawState::Succeeded(txid) => { + log_info!(logger, "Withdraw successful: {txid}"); + + let internal_id = Txid::from_slice(&operation_id.0).expect("should convert"); + let txid = Txid::from_slice(&txid).expect("should convert"); + let updated_transaction_details = TransactionDetails { + transaction: None, + txid: Some(txid), + internal_id, + received: 0, + sent: amount.to_sat(), + fee: Some(fee.to_sat()), + confirmation_time: ConfirmationTime::Unconfirmed { last_seen: now().as_secs() }, + labels: labels.clone(), + }; + + match persist_transaction_details(&storage, &updated_transaction_details) { + Ok(_) => { + log_info!(logger, "Transaction updated"); + }, + Err(e) => { + log_error!(logger, "Error updating transaction: {e}"); + }, + } + + // TODO we need to get confirmations for this txid and update + break; + }, + WithdrawState::Failed(e) => { + // TODO delete + log_error!(logger, "Transaction failed: {e}"); + break; + }, + } + } + } + _ = delay_fut => { + if timeout.is_none() { + if stop.load(Ordering::Relaxed) { + break; + } + } else { + log_debug!( + logger, + "Timeout reached, exiting loop for on chain tx", + ); + break; + } + } + } + } + log_trace!(logger, "Done with stream outcome",); + } + } +} + async fn process_onchain_deposit_outcome( stream_or_outcome: UpdateStreamOrOutcome, original_transaction_details: Option, diff --git a/mutiny-core/src/lib.rs b/mutiny-core/src/lib.rs index 9e934c5e1..18eae2ac4 100644 --- a/mutiny-core/src/lib.rs +++ b/mutiny-core/src/lib.rs @@ -96,10 +96,13 @@ use async_lock::RwLock; use bdk_chain::ConfirmationTime; use bip39::Mnemonic; pub use bitcoin; -use bitcoin::hashes::Hash; -use bitcoin::secp256k1::{PublicKey, ThirtyTwoByteHash}; +use bitcoin::{ + address::NetworkUnchecked, + secp256k1::{PublicKey, ThirtyTwoByteHash}, +}; use bitcoin::{bip32::ExtendedPrivKey, Transaction}; use bitcoin::{hashes::sha256, Network, Txid}; +use bitcoin::{hashes::Hash, Address}; pub use fedimint_core; use fedimint_core::{api::InviteCode, config::FederationId}; use futures::{pin_mut, select, FutureExt}; @@ -1853,6 +1856,56 @@ impl MutinyWallet { Ok(Some(lsp_fee + federation_fee)) } + pub async fn send_to_address( + &self, + send_to: Address, + amount: u64, + labels: Vec, + fee_rate: Option, + ) -> Result { + // Try each federation first + let federation_ids = self.list_federation_ids().await?; + let mut last_federation_error = None; + for federation_id in federation_ids { + if let Some(fedimint_client) = self.federations.read().await.get(&federation_id) { + // Check if the federation has enough balance + let balance = fedimint_client.get_balance().await?; + if balance >= amount / 1_000 { + match fedimint_client + .send_onchain(send_to.clone(), amount, labels.clone()) + .await + { + Ok(t) => { + return Ok(t); + } + Err(e) => match e { + MutinyError::PaymentTimeout => return Err(e), + _ => { + log_warn!(self.logger, "unhandled error: {e}"); + last_federation_error = Some(e); + } + }, + } + } + // If payment fails or balance is not sufficient, continue to next federation + } + // If federation client is not found, continue to next federation + } + + // If any balance at all, then fallback to node manager for payment. + // Take the error from the node manager as the priority. + let b = self.node_manager.get_balance().await?; + if b.confirmed + b.unconfirmed > 0 { + let res = self + .node_manager + .send_to_address(send_to, amount, labels, fee_rate) + .await?; + Ok(res) + } else { + Err(last_federation_error.unwrap_or(MutinyError::InsufficientBalance)) + } + } + async fn create_address(&self, labels: Vec) -> Result { // Attempt to create federation invoice if available let federation_ids = self.list_federation_ids().await?; diff --git a/mutiny-wasm/src/lib.rs b/mutiny-wasm/src/lib.rs index 8b4d45d90..898b17d27 100644 --- a/mutiny-wasm/src/lib.rs +++ b/mutiny-wasm/src/lib.rs @@ -548,7 +548,6 @@ impl MutinyWallet { let send_to = Address::from_str(&destination_address)?; Ok(self .inner - .node_manager .send_to_address(send_to, amount, labels, fee_rate) .await? .to_string()) From e52ada2d7a2c3e48a5f122257fc9291244c52d94 Mon Sep 17 00:00:00 2001 From: Tony Giorgio Date: Fri, 3 May 2024 01:12:33 -0500 Subject: [PATCH 3/8] Check esplora after fedimint withdraw --- mutiny-core/src/federation.rs | 66 ++++++++++++++++++++++++++++++++-- mutiny-core/src/lib.rs | 29 +++++++++++---- mutiny-core/src/nodemanager.rs | 18 +++++++--- 3 files changed, 101 insertions(+), 12 deletions(-) diff --git a/mutiny-core/src/federation.rs b/mutiny-core/src/federation.rs index acc96c3d4..b14701581 100644 --- a/mutiny-core/src/federation.rs +++ b/mutiny-core/src/federation.rs @@ -27,6 +27,7 @@ use bitcoin::{ Address, Network, Txid, }; use core::fmt; +use esplora_client::AsyncClient; use fedimint_bip39::Bip39RootSecretStrategy; use fedimint_client::{ derivable_secret::DerivableSecret, @@ -259,6 +260,7 @@ pub(crate) struct FederationClient { #[allow(dead_code)] fedimint_storage: FedimintStorage, gateway: Arc>>, + esplora: Arc, network: Network, stop: Arc, pub(crate) logger: Arc, @@ -271,6 +273,7 @@ impl FederationClient { federation_code: InviteCode, xprivkey: ExtendedPrivKey, storage: S, + esplora: Arc, network: Network, stop: Arc, logger: Arc, @@ -400,6 +403,7 @@ impl FederationClient { storage, logger, invite_code: federation_code, + esplora, network, stop, gateway, @@ -486,6 +490,7 @@ impl FederationClient { entry, operation_id, self.fedimint_client.clone(), + self.esplora.clone(), self.logger.clone(), self.stop.clone(), self.storage.clone(), @@ -536,6 +541,7 @@ impl FederationClient { let fedimint_client_clone = self.fedimint_client.clone(); let logger_clone = self.logger.clone(); let storage_clone = self.storage.clone(); + let esplora_clone = self.esplora.clone(); let stop = self.stop.clone(); spawn(async move { let operation = fedimint_client_clone @@ -548,6 +554,7 @@ impl FederationClient { operation, id, fedimint_client_clone, + esplora_clone, logger_clone, stop, storage_clone, @@ -695,6 +702,7 @@ impl FederationClient { let fedimint_client_clone = self.fedimint_client.clone(); let logger_clone = self.logger.clone(); let storage_clone = self.storage.clone(); + let esplora_clone = self.esplora.clone(); let stop = self.stop.clone(); spawn(async move { let operation = fedimint_client_clone @@ -707,6 +715,7 @@ impl FederationClient { operation, id, fedimint_client_clone, + esplora_clone, logger_clone, stop, storage_clone, @@ -773,6 +782,7 @@ impl FederationClient { op_id, self.fedimint_client.clone(), self.storage.clone(), + self.esplora.clone(), Some(DEFAULT_PAYMENT_TIMEOUT * 1_000), self.stop.clone(), ) @@ -1041,6 +1051,7 @@ fn subscribe_operation_ext( entry: OperationLogEntry, operation_id: OperationId, fedimint_client: ClientHandleArc, + esplora: Arc, logger: Arc, stop: Arc, storage: S, @@ -1052,6 +1063,7 @@ fn subscribe_operation_ext( operation_id, fedimint_client, storage, + esplora, None, stop, ) @@ -1176,12 +1188,15 @@ pub(crate) fn mnemonic_from_xpriv(xpriv: ExtendedPrivKey) -> Result( logger: Arc, entry: OperationLogEntry, operation_id: OperationId, fedimint_client: ClientHandleArc, storage: S, + esplora: Arc, timeout: Option, stop: Arc, ) { @@ -1315,6 +1330,7 @@ async fn process_operation_until_timeout( fee.amount(), operation_id, storage, + esplora, timeout, stop, logger, @@ -1450,6 +1466,7 @@ async fn process_onchain_withdraw_outcome( fee: fedimint_ln_common::bitcoin::Amount, operation_id: OperationId, storage: S, + esplora: Arc, timeout: Option, stop: Arc, logger: Arc, @@ -1512,8 +1529,10 @@ async fn process_onchain_withdraw_outcome( }, } - // TODO we need to get confirmations for this txid and update - break; + // we need to get confirmations for this txid and update + subscribe_onchain_confirmation_check(storage.clone(), esplora.clone(), txid, updated_transaction_details, stop, logger.clone()).await; + + break }, WithdrawState::Failed(e) => { // TODO delete @@ -1543,6 +1562,49 @@ async fn process_onchain_withdraw_outcome( } } +async fn subscribe_onchain_confirmation_check( + storage: S, + esplora: Arc, + txid: Txid, + mut transaction_details: TransactionDetails, + stop: Arc, + logger: Arc, +) { + spawn(async move { + loop { + if stop.load(Ordering::Relaxed) { + break; + }; + + match esplora.get_tx_status(&txid).await { + Ok(s) => { + if s.confirmed { + log_info!(logger, "Transaction confirmed"); + transaction_details.confirmation_time = ConfirmationTime::Confirmed { + height: s.block_height.expect("confirmed"), + time: now().as_secs(), + }; + match persist_transaction_details(&storage, &transaction_details) { + Ok(_) => { + log_info!(logger, "Transaction updated"); + break; + } + Err(e) => { + log_error!(logger, "Error updating transaction: {e}"); + } + } + } + } + Err(e) => { + log_error!(logger, "Error updating transaction: {e}"); + } + } + + sleep(5_000).await; + } + }); +} + async fn process_onchain_deposit_outcome( stream_or_outcome: UpdateStreamOrOutcome, original_transaction_details: Option, diff --git a/mutiny-core/src/lib.rs b/mutiny-core/src/lib.rs index 18eae2ac4..157836015 100644 --- a/mutiny-core/src/lib.rs +++ b/mutiny-core/src/lib.rs @@ -47,12 +47,6 @@ use crate::federation::get_federation_identity; pub use crate::gossip::{GOSSIP_SYNC_TIME_KEY, NETWORK_GRAPH_KEY, PROB_SCORER_KEY}; pub use crate::keymanager::generate_seed; pub use crate::ldkstorage::{CHANNEL_CLOSURE_PREFIX, CHANNEL_MANAGER_KEY, MONITORS_PREFIX_KEY}; -use crate::storage::{ - get_payment_hash_from_key, get_transaction_details, list_payment_info, persist_payment_info, - update_nostr_contact_list, IndexItem, MutinyStorage, DEVICE_ID_KEY, EXPECTED_NETWORK_KEY, - NEED_FULL_SYNC_KEY, ONCHAIN_PREFIX, PAYMENT_INBOUND_PREFIX_KEY, PAYMENT_OUTBOUND_PREFIX_KEY, - SUBSCRIPTION_TIMESTAMP, TRANSACTION_DETAILS_PREFIX_KEY, -}; use crate::utils::spawn; use crate::{auth::MutinyAuthClient, hermes::HermesClient, logging::MutinyLogger}; use crate::{blindauth::BlindAuthClient, cashu::CashuHttpClient}; @@ -84,6 +78,15 @@ use crate::{ storage::get_invoice_by_hash, }; use crate::{nostr::NostrManager, utils::sleep}; +use crate::{ + onchain::get_esplora_url, + storage::{ + get_payment_hash_from_key, get_transaction_details, list_payment_info, + persist_payment_info, update_nostr_contact_list, IndexItem, MutinyStorage, DEVICE_ID_KEY, + EXPECTED_NETWORK_KEY, NEED_FULL_SYNC_KEY, ONCHAIN_PREFIX, PAYMENT_INBOUND_PREFIX_KEY, + PAYMENT_OUTBOUND_PREFIX_KEY, SUBSCRIPTION_TIMESTAMP, TRANSACTION_DETAILS_PREFIX_KEY, + }, +}; use ::nostr::nips::nip47::Method; use ::nostr::nips::nip57; #[cfg(target_arch = "wasm32")] @@ -103,6 +106,7 @@ use bitcoin::{ use bitcoin::{bip32::ExtendedPrivKey, Transaction}; use bitcoin::{hashes::sha256, Network, Txid}; use bitcoin::{hashes::Hash, Address}; +use esplora_client::AsyncClient; pub use fedimint_core; use fedimint_core::{api::InviteCode, config::FederationId}; use futures::{pin_mut, select, FutureExt}; @@ -930,11 +934,16 @@ impl MutinyWalletBuilder { } }); + let esplora_server_url = get_esplora_url(network, config.user_esplora_url.clone()); + let esplora = esplora_client::Builder::new(&esplora_server_url).build_async()?; + let esplora = Arc::new(esplora); + let start = Instant::now(); let mut nm_builder = NodeManagerBuilder::new(self.xprivkey, self.storage.clone()) .with_config(config.clone()); nm_builder.with_logger(logger.clone()); + nm_builder.with_esplora(esplora.clone()); let node_manager = Arc::new(nm_builder.build().await?); log_trace!( @@ -982,6 +991,7 @@ impl MutinyWalletBuilder { federation_storage.clone(), &config, self.storage.clone(), + esplora.clone(), stop.clone(), &logger, ) @@ -1170,6 +1180,7 @@ impl MutinyWalletBuilder { subscription_client, blind_auth_client, hermes_client, + esplora, auth, stop, logger, @@ -1242,6 +1253,7 @@ pub struct MutinyWallet { subscription_client: Option>, blind_auth_client: Option>>, hermes_client: Option>>, + esplora: Arc, pub stop: Arc, pub logger: Arc, network: Network, @@ -2675,6 +2687,7 @@ impl MutinyWallet { self.federation_storage.clone(), self.federations.clone(), self.hermes_client.clone(), + self.esplora.clone(), federation_code, self.stop.clone(), ) @@ -3332,6 +3345,7 @@ async fn create_federations( federation_storage: FederationStorage, c: &MutinyWalletConfig, storage: S, + esplora: Arc, stop: Arc, logger: &Arc, ) -> Result>>>>, MutinyError> { @@ -3342,6 +3356,7 @@ async fn create_federations( federation_index.federation_code, c.xprivkey, storage.clone(), + esplora.clone(), c.network, stop.clone(), logger.clone(), @@ -3366,6 +3381,7 @@ pub(crate) async fn create_new_federation( federation_storage: Arc>, federations: Arc>>>>, hermes_client: Option>>, + esplora: Arc, federation_code: InviteCode, stop: Arc, ) -> Result { @@ -3395,6 +3411,7 @@ pub(crate) async fn create_new_federation( federation_code.clone(), xprivkey, storage.clone(), + esplora, network, stop.clone(), logger.clone(), diff --git a/mutiny-core/src/nodemanager.rs b/mutiny-core/src/nodemanager.rs index c67163283..42b961ab3 100644 --- a/mutiny-core/src/nodemanager.rs +++ b/mutiny-core/src/nodemanager.rs @@ -232,6 +232,7 @@ pub struct NodeBalance { pub struct NodeManagerBuilder { xprivkey: ExtendedPrivKey, storage: S, + esplora: Option>, config: Option, stop: Option>, logger: Option>, @@ -242,6 +243,7 @@ impl NodeManagerBuilder { NodeManagerBuilder:: { xprivkey, storage, + esplora: None, config: None, stop: None, logger: None, @@ -257,6 +259,10 @@ impl NodeManagerBuilder { self.stop = Some(stop); } + pub fn with_esplora(&mut self, esplora: Arc) { + self.esplora = Some(esplora); + } + pub fn with_logger(&mut self, logger: Arc) { self.logger = Some(logger); } @@ -271,6 +277,13 @@ impl NodeManagerBuilder { .map_or_else(|| Err(MutinyError::InvalidArgumentsError), Ok)?; let logger = self.logger.unwrap_or(Arc::new(MutinyLogger::default())); let stop = self.stop.unwrap_or(Arc::new(AtomicBool::new(false))); + let esplora = if let Some(e) = self.esplora { + e + } else { + let esplora_server_url = get_esplora_url(c.network, c.user_esplora_url); + let esplora = Builder::new(&esplora_server_url).build_async()?; + Arc::new(esplora) + }; #[cfg(target_arch = "wasm32")] let websocket_proxy_addr = c @@ -280,14 +293,11 @@ impl NodeManagerBuilder { let start = Instant::now(); log_info!(logger, "Building node manager components"); - let esplora_server_url = get_esplora_url(c.network, c.user_esplora_url); - let esplora = Builder::new(&esplora_server_url).build_async()?; let tx_sync = Arc::new(EsploraSyncClient::from_client( - esplora.clone(), + esplora.as_ref().clone(), logger.clone(), )); - let esplora = Arc::new(esplora); let fee_estimator = Arc::new(MutinyFeeEstimator::new( self.storage.clone(), esplora.clone(), From 2ebe551611c4762946c4b52d9d61c2f253913f24 Mon Sep 17 00:00:00 2001 From: Tony Giorgio Date: Mon, 6 May 2024 17:31:32 -0500 Subject: [PATCH 4/8] Send max with fedimint --- mutiny-core/src/federation.rs | 26 ++++-- mutiny-core/src/lib.rs | 156 +++++++++++++++++++++++++++++++-- mutiny-core/src/nodemanager.rs | 16 ++-- mutiny-wasm/src/lib.rs | 23 ++--- 4 files changed, 187 insertions(+), 34 deletions(-) diff --git a/mutiny-core/src/federation.rs b/mutiny-core/src/federation.rs index b14701581..c06b99f19 100644 --- a/mutiny-core/src/federation.rs +++ b/mutiny-core/src/federation.rs @@ -20,7 +20,6 @@ use async_trait::async_trait; use bdk_chain::ConfirmationTime; use bip39::Mnemonic; use bitcoin::{ - address::NetworkUnchecked, bip32::{ChildNumber, DerivationPath, ExtendedPrivKey}, hashes::Hash, secp256k1::{Secp256k1, SecretKey, ThirtyTwoByteHash}, @@ -261,7 +260,6 @@ pub(crate) struct FederationClient { fedimint_storage: FedimintStorage, gateway: Arc>>, esplora: Arc, - network: Network, stop: Arc, pub(crate) logger: Arc, } @@ -404,7 +402,6 @@ impl FederationClient { logger, invite_code: federation_code, esplora, - network, stop, gateway, }; @@ -730,11 +727,11 @@ impl FederationClient { /// Send on chain transaction pub(crate) async fn send_onchain( &self, - send_to: bitcoin::Address, + send_to: bitcoin::Address, amount: u64, labels: Vec, ) -> Result { - let address = bitcoin30_to_bitcoin29_address(send_to.require_network(self.network)?); + let address = bitcoin30_to_bitcoin29_address(send_to); let btc_amount = fedimint_ln_common::bitcoin::Amount::from_sat(amount); @@ -807,6 +804,25 @@ impl FederationClient { Err(MutinyError::PaymentTimeout) } + pub async fn estimate_tx_fee( + &self, + destination_address: bitcoin::Address, + amount: u64, + ) -> Result { + let address = bitcoin30_to_bitcoin29_address(destination_address); + let btc_amount = fedimint_ln_common::bitcoin::Amount::from_sat(amount); + + let wallet_module = self + .fedimint_client + .get_first_module::(); + + let peg_out_fees = wallet_module + .get_withdraw_fees(address.clone(), btc_amount) + .await?; + + Ok(peg_out_fees.amount().to_sat()) + } + /// Someone received a payment on our behalf, we need to claim it pub async fn claim_external_receive( &self, diff --git a/mutiny-core/src/lib.rs b/mutiny-core/src/lib.rs index 157836015..cc80047f4 100644 --- a/mutiny-core/src/lib.rs +++ b/mutiny-core/src/lib.rs @@ -99,10 +99,7 @@ use async_lock::RwLock; use bdk_chain::ConfirmationTime; use bip39::Mnemonic; pub use bitcoin; -use bitcoin::{ - address::NetworkUnchecked, - secp256k1::{PublicKey, ThirtyTwoByteHash}, -}; +use bitcoin::secp256k1::{PublicKey, ThirtyTwoByteHash}; use bitcoin::{bip32::ExtendedPrivKey, Transaction}; use bitcoin::{hashes::sha256, Network, Txid}; use bitcoin::{hashes::Hash, Address}; @@ -1870,7 +1867,7 @@ impl MutinyWallet { pub async fn send_to_address( &self, - send_to: Address, + send_to: Address, amount: u64, labels: Vec, fee_rate: Option, @@ -1918,6 +1915,150 @@ impl MutinyWallet { } } + /// Estimates the onchain fee for a transaction sending to the given address. + /// The amount is in satoshis and the fee rate is in sat/vbyte. + pub async fn estimate_tx_fee( + &self, + destination_address: Address, + amount: u64, + fee_rate: Option, + ) -> Result { + log_warn!(self.logger, "estimate_tx_fee"); + + // Try each federation first + let federation_ids = self.list_federation_ids().await?; + let mut last_federation_error = None; + for federation_id in federation_ids { + if let Some(fedimint_client) = self.federations.read().await.get(&federation_id) { + // Check if the federation has enough balance + let balance = fedimint_client.get_balance().await?; + if balance >= amount / 1_000 { + match fedimint_client + .estimate_tx_fee(destination_address.clone(), amount) + .await + { + Ok(t) => { + return Ok(t); + } + Err(e) => { + log_warn!(self.logger, "error estimating fedimint fee: {e}"); + last_federation_error = Some(e); + } + } + } + // If estimation fails or balance is not sufficient, continue to next federation + } + // If federation client is not found, continue to next federation + } + + let b = self.node_manager.get_balance().await?; + if b.confirmed + b.unconfirmed > 0 { + let res = self + .node_manager + .estimate_tx_fee(destination_address, amount, fee_rate)?; + + Ok(res) + } else { + Err(last_federation_error.unwrap_or(MutinyError::InsufficientBalance)) + } + } + + /// Estimates the onchain fee for a transaction sweep our on-chain balance + /// to the given address. If the fedimint has a balance, sweep that first. + /// Do not sweep the on chain wallet unless that is empty. + /// + /// The fee rate is in sat/vbyte. + pub async fn estimate_sweep_tx_fee( + &self, + destination_address: Address, + fee_rate: Option, + ) -> Result { + // Try each federation first + let federation_ids = self.list_federation_ids().await?; + for federation_id in federation_ids { + if let Some(fedimint_client) = self.federations.read().await.get(&federation_id) { + // Check if the federation has enough balance + let balance = fedimint_client.get_balance().await?; + match fedimint_client + .estimate_tx_fee(destination_address.clone(), balance) + .await + { + Ok(t) => { + return Ok(t); + } + Err(e) => return Err(e), + } + // If estimation fails or balance is not sufficient, continue to next federation + } + // If federation client is not found, continue to next federation + } + + let b = self.node_manager.get_balance().await?; + if b.confirmed + b.unconfirmed > 0 { + let res = self + .node_manager + .estimate_sweep_tx_fee(destination_address, fee_rate)?; + + Ok(res) + } else { + log_error!(self.logger, "node manager doesn't have a balance"); + Err(MutinyError::InsufficientBalance) + } + } + + /// Sweeps all the funds from the wallet to the given address. + /// The fee rate is in sat/vbyte. + /// + /// If a fee rate is not provided, one will be used from the fee estimator. + pub async fn sweep_wallet( + &self, + send_to: Address, + labels: Vec, + fee_rate: Option, + ) -> Result { + // Try each federation first + let federation_ids = self.list_federation_ids().await?; + for federation_id in federation_ids { + if let Some(fedimint_client) = self.federations.read().await.get(&federation_id) { + // Check if the federation has enough balance + let balance = fedimint_client.get_balance().await?; + match fedimint_client + .estimate_tx_fee(send_to.clone(), balance) + .await + { + Ok(f) => { + match fedimint_client + .send_onchain(send_to.clone(), balance - f, labels) + .await + { + Ok(t) => return Ok(t), + Err(e) => { + log_error!(self.logger, "error sending the fedimint balance"); + return Err(e); + } + } + } + Err(e) => return Err(e), + } + // If payment fails or balance is not sufficient, continue to next federation + } + // If federation client is not found, continue to next federation + } + + let b = self.node_manager.get_balance().await?; + if b.confirmed + b.unconfirmed > 0 { + let res = self + .node_manager + .sweep_wallet(send_to.clone(), labels, fee_rate) + .await?; + + Ok(res) + } else { + log_error!(self.logger, "node manager doesn't have a balance"); + Err(MutinyError::InsufficientBalance) + } + } + async fn create_address(&self, labels: Vec) -> Result { // Attempt to create federation invoice if available let federation_ids = self.list_federation_ids().await?; @@ -3297,6 +3438,11 @@ impl MutinyWallet { Ok(response.price) } + + /// Returns the network of the wallet. + pub fn get_network(&self) -> Network { + self.network + } } impl InvoiceHandler for MutinyWallet { diff --git a/mutiny-core/src/nodemanager.rs b/mutiny-core/src/nodemanager.rs index 42b961ab3..a048f692f 100644 --- a/mutiny-core/src/nodemanager.rs +++ b/mutiny-core/src/nodemanager.rs @@ -712,14 +712,12 @@ impl NodeManager { /// If a fee rate is not provided, one will be used from the fee estimator. pub async fn send_to_address( &self, - send_to: Address, + send_to: Address, amount: u64, labels: Vec, fee_rate: Option, ) -> Result { - let address = send_to.require_network(self.network)?; - - self.wallet.send(address, amount, labels, fee_rate).await + self.wallet.send(send_to, amount, labels, fee_rate).await } /// Sweeps all the funds from the wallet to the given address. @@ -728,18 +726,16 @@ impl NodeManager { /// If a fee rate is not provided, one will be used from the fee estimator. pub async fn sweep_wallet( &self, - send_to: Address, + send_to: Address, labels: Vec, fee_rate: Option, ) -> Result { - let address = send_to.require_network(self.network)?; - - self.wallet.sweep(address, labels, fee_rate).await + self.wallet.sweep(send_to, labels, fee_rate).await } /// Estimates the onchain fee for a transaction sending to the given address. /// The amount is in satoshis and the fee rate is in sat/vbyte. - pub fn estimate_tx_fee( + pub(crate) fn estimate_tx_fee( &self, destination_address: Address, amount: u64, @@ -753,7 +749,7 @@ impl NodeManager { /// to the given address. /// /// The fee rate is in sat/vbyte. - pub fn estimate_sweep_tx_fee( + pub(crate) fn estimate_sweep_tx_fee( &self, destination_address: Address, fee_rate: Option, diff --git a/mutiny-wasm/src/lib.rs b/mutiny-wasm/src/lib.rs index 898b17d27..53ae731b7 100644 --- a/mutiny-wasm/src/lib.rs +++ b/mutiny-wasm/src/lib.rs @@ -476,7 +476,7 @@ impl MutinyWallet { /// Returns the network of the wallet. #[wasm_bindgen] pub fn get_network(&self) -> String { - self.inner.node_manager.get_network().to_string() + self.inner.get_network().to_string() } /// Gets a new bitcoin address from the wallet. @@ -545,7 +545,8 @@ impl MutinyWallet { labels: Vec, fee_rate: Option, ) -> Result { - let send_to = Address::from_str(&destination_address)?; + let send_to = + Address::from_str(&destination_address)?.require_network(self.inner.get_network())?; Ok(self .inner .send_to_address(send_to, amount, labels, fee_rate) @@ -583,10 +584,10 @@ impl MutinyWallet { labels: Vec, fee_rate: Option, ) -> Result { - let send_to = Address::from_str(&destination_address)?; + let send_to = + Address::from_str(&destination_address)?.require_network(self.inner.get_network())?; Ok(self .inner - .node_manager .sweep_wallet(send_to, labels, fee_rate) .await? .to_string()) @@ -594,33 +595,27 @@ impl MutinyWallet { /// Estimates the onchain fee for a transaction sending to the given address. /// The amount is in satoshis and the fee rate is in sat/vbyte. - pub fn estimate_tx_fee( + pub async fn estimate_tx_fee( &self, destination_address: String, amount: u64, fee_rate: Option, ) -> Result { let addr = Address::from_str(&destination_address)?.assume_checked(); - Ok(self - .inner - .node_manager - .estimate_tx_fee(addr, amount, fee_rate)?) + Ok(self.inner.estimate_tx_fee(addr, amount, fee_rate).await?) } /// Estimates the onchain fee for a transaction sweep our on-chain balance /// to the given address. /// /// The fee rate is in sat/vbyte. - pub fn estimate_sweep_tx_fee( + pub async fn estimate_sweep_tx_fee( &self, destination_address: String, fee_rate: Option, ) -> Result { let addr = Address::from_str(&destination_address)?.assume_checked(); - Ok(self - .inner - .node_manager - .estimate_sweep_tx_fee(addr, fee_rate)?) + Ok(self.inner.estimate_sweep_tx_fee(addr, fee_rate).await?) } /// Estimates the onchain fee for a opening a lightning channel. From 7eaffc4b848bb98f35c3af7dd5000d569374d2ab Mon Sep 17 00:00:00 2001 From: Tony Giorgio Date: Mon, 6 May 2024 18:27:10 -0500 Subject: [PATCH 5/8] Don't process estimated tx fee with a dust amt --- mutiny-core/src/lib.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mutiny-core/src/lib.rs b/mutiny-core/src/lib.rs index cc80047f4..2ee3ba01c 100644 --- a/mutiny-core/src/lib.rs +++ b/mutiny-core/src/lib.rs @@ -150,6 +150,7 @@ const BITCOIN_PRICE_CACHE_SEC: u64 = 300; const DEFAULT_PAYMENT_TIMEOUT: u64 = 30; const SWAP_LABEL: &str = "SWAP"; const MELT_CASHU_TOKEN: &str = "Cashu Token Melt"; +const DUST_LIMIT: u64 = 546; #[cfg_attr(test, automock)] pub trait InvoiceHandler { @@ -1923,7 +1924,9 @@ impl MutinyWallet { amount: u64, fee_rate: Option, ) -> Result { - log_warn!(self.logger, "estimate_tx_fee"); + if amount < DUST_LIMIT { + return Err(MutinyError::WalletOperationFailed); + } // Try each federation first let federation_ids = self.list_federation_ids().await?; From 4d156021c6ce79bdb5a111094b9091b6ef2d4cc0 Mon Sep 17 00:00:00 2001 From: Tony Giorgio Date: Tue, 7 May 2024 23:19:40 -0500 Subject: [PATCH 6/8] Fedimint onchain cleanup --- mutiny-core/src/federation.rs | 369 ++++++++++++++++++---------------- mutiny-core/src/storage.rs | 18 ++ 2 files changed, 212 insertions(+), 175 deletions(-) diff --git a/mutiny-core/src/federation.rs b/mutiny-core/src/federation.rs index c06b99f19..5d51bf85a 100644 --- a/mutiny-core/src/federation.rs +++ b/mutiny-core/src/federation.rs @@ -9,8 +9,9 @@ use crate::{ logging::MutinyLogger, onchain::coin_type_from_network, storage::{ - get_transaction_details, list_payment_info, persist_payment_info, - persist_transaction_details, MutinyStorage, VersionedValue, TRANSACTION_DETAILS_PREFIX_KEY, + delete_transaction_details, get_transaction_details, list_payment_info, + persist_payment_info, persist_transaction_details, MutinyStorage, VersionedValue, + TRANSACTION_DETAILS_PREFIX_KEY, }, utils::sleep, HTLCStatus, MutinyInvoice, DEFAULT_PAYMENT_TIMEOUT, @@ -1320,6 +1321,7 @@ async fn process_operation_until_timeout( stored_transaction_details, operation_id, storage, + esplora, timeout, stop, logger, @@ -1492,90 +1494,95 @@ async fn process_onchain_withdraw_outcome( .map(|o| o.labels.clone()) .unwrap_or_default(); - match stream_or_outcome { - UpdateStreamOrOutcome::Outcome(outcome) => { - // TODO - log_trace!(logger, "Outcome received: {:?}", outcome); - } - UpdateStreamOrOutcome::UpdateStream(mut s) => { - // break out after sleep time or check stop signal - log_trace!(logger, "start timeout stream futures"); - loop { - let timeout_future = if let Some(t) = timeout { - sleep(t as i32) - } else { - sleep(1_000_i32) - }; + let mut s = stream_or_outcome.into_stream(); - let mut stream_fut = Box::pin(s.next()).fuse(); - let delay_fut = Box::pin(timeout_future).fuse(); - pin_mut!(delay_fut); + // break out after sleep time or check stop signal + log_trace!(logger, "start timeout stream futures"); + loop { + let timeout_future = if let Some(t) = timeout { + sleep(t as i32) + } else { + sleep(1_000_i32) + }; - select! { - outcome_option = stream_fut => { - if let Some(outcome) = outcome_option { - // TODO refactor outcome parsing into seperate method - match outcome { - WithdrawState::Created => { - // Nothing to do - log_debug!(logger, "Waiting for withdraw"); + let mut stream_fut = Box::pin(s.next()).fuse(); + let delay_fut = Box::pin(timeout_future).fuse(); + pin_mut!(delay_fut); + + select! { + outcome_option = stream_fut => { + if let Some(outcome) = outcome_option { + match outcome { + WithdrawState::Created => { + // Nothing to do + log_debug!(logger, "Waiting for withdraw"); + }, + WithdrawState::Succeeded(txid) => { + log_info!(logger, "Withdraw successful: {txid}"); + + let internal_id = Txid::from_slice(&operation_id.0).expect("should convert"); + let txid = Txid::from_slice(&txid).expect("should convert"); + let updated_transaction_details = TransactionDetails { + transaction: None, + txid: Some(txid), + internal_id, + received: 0, + sent: amount.to_sat(), + fee: Some(fee.to_sat()), + confirmation_time: ConfirmationTime::Unconfirmed { last_seen: now().as_secs() }, + labels: labels.clone(), + }; + + match persist_transaction_details(&storage, &updated_transaction_details) { + Ok(_) => { + log_info!(logger, "Transaction updated"); }, - WithdrawState::Succeeded(txid) => { - log_info!(logger, "Withdraw successful: {txid}"); - - let internal_id = Txid::from_slice(&operation_id.0).expect("should convert"); - let txid = Txid::from_slice(&txid).expect("should convert"); - let updated_transaction_details = TransactionDetails { - transaction: None, - txid: Some(txid), - internal_id, - received: 0, - sent: amount.to_sat(), - fee: Some(fee.to_sat()), - confirmation_time: ConfirmationTime::Unconfirmed { last_seen: now().as_secs() }, - labels: labels.clone(), - }; - - match persist_transaction_details(&storage, &updated_transaction_details) { - Ok(_) => { - log_info!(logger, "Transaction updated"); - }, - Err(e) => { - log_error!(logger, "Error updating transaction: {e}"); - }, - } - - // we need to get confirmations for this txid and update - subscribe_onchain_confirmation_check(storage.clone(), esplora.clone(), txid, updated_transaction_details, stop, logger.clone()).await; - - break - }, - WithdrawState::Failed(e) => { - // TODO delete - log_error!(logger, "Transaction failed: {e}"); - break; + Err(e) => { + log_error!(logger, "Error updating transaction: {e}"); }, } - } - } - _ = delay_fut => { - if timeout.is_none() { - if stop.load(Ordering::Relaxed) { - break; + + // we need to get confirmations for this txid and update + subscribe_onchain_confirmation_check(storage.clone(), esplora.clone(), txid, updated_transaction_details, stop, logger.clone()).await; + + break + }, + WithdrawState::Failed(e) => { + log_error!(logger, "Transaction failed: {e}"); + + // if we have the original transaction details, delete it + if let Some(t) = original_transaction_details { + match delete_transaction_details(&storage, &t) { + Ok(_) => { + log_info!(logger, "Transaction deleted"); + }, + Err(e) => { + log_error!(logger, "Error deleting transaction: {e}"); + }, + } } - } else { - log_debug!( - logger, - "Timeout reached, exiting loop for on chain tx", - ); + break; - } + }, } } } - log_trace!(logger, "Done with stream outcome",); + _ = delay_fut => { + if timeout.is_none() { + if stop.load(Ordering::Relaxed) { + break; + } + } else { + log_debug!( + logger, + "Timeout reached, exiting loop for on chain tx", + ); + break; + } + } } } + log_trace!(logger, "Done with stream outcome",); } async fn subscribe_onchain_confirmation_check( @@ -1621,11 +1628,14 @@ async fn subscribe_onchain_confirmation_check( }); } +// FIXME refactor +#[allow(clippy::too_many_arguments)] async fn process_onchain_deposit_outcome( stream_or_outcome: UpdateStreamOrOutcome, original_transaction_details: Option, operation_id: OperationId, storage: S, + esplora: Arc, timeout: Option, stop: Arc, logger: Arc, @@ -1635,119 +1645,128 @@ async fn process_onchain_deposit_outcome( .map(|o| o.labels.clone()) .unwrap_or_default(); - match stream_or_outcome { - UpdateStreamOrOutcome::Outcome(outcome) => { - // TODO - log_trace!(logger, "Outcome received: {:?}", outcome); - } - UpdateStreamOrOutcome::UpdateStream(mut s) => { - // break out after sleep time or check stop signal - log_trace!(logger, "start timeout stream futures"); - loop { - let timeout_future = if let Some(t) = timeout { - sleep(t as i32) - } else { - sleep(1_000_i32) - }; + let mut s = stream_or_outcome.into_stream(); - let mut stream_fut = Box::pin(s.next()).fuse(); - let delay_fut = Box::pin(timeout_future).fuse(); - pin_mut!(delay_fut); + // break out after sleep time or check stop signal + log_trace!(logger, "start timeout stream futures"); + loop { + let timeout_future = if let Some(t) = timeout { + sleep(t as i32) + } else { + sleep(1_000_i32) + }; - select! { - outcome_option = stream_fut => { - if let Some(outcome) = outcome_option { - // TODO refactor outcome parsing into seperate method - match outcome { - fedimint_wallet_client::DepositState::WaitingForTransaction => { - // Nothing to do - log_debug!(logger, "Waiting for transaction"); - } - fedimint_wallet_client::DepositState::WaitingForConfirmation(tx) => { - // Pending state, update with info we have - log_debug!(logger, "Waiting for confirmation"); - let txid = Txid::from_slice(&tx.btc_transaction.txid()).expect("should convert"); - let internal_id = Txid::from_slice(&operation_id.0).expect("should convert"); - let output = tx.btc_transaction.output[tx.out_idx as usize].clone(); - - let updated_transaction_details = TransactionDetails { - transaction: None, - txid: Some(txid), - internal_id, - received: output.value, - sent: 0, - fee: None, - confirmation_time: ConfirmationTime::Unconfirmed { last_seen: now().as_secs() }, - labels: labels.clone(), - }; - - match persist_transaction_details(&storage, &updated_transaction_details) { - Ok(_) => { - log_info!(logger, "Transaction updated"); - }, - Err(e) => { - log_error!(logger, "Error updating transaction: {e}"); - }, - } - } - fedimint_wallet_client::DepositState::Confirmed(tx) => { - // Pending state, update with info we have - log_debug!(logger, "Transaction confirmed"); - let txid = Txid::from_slice(&tx.btc_transaction.txid()).expect("should convert"); - let internal_id = Txid::from_slice(&operation_id.0).expect("should convert"); - let output = tx.btc_transaction.output[tx.out_idx as usize].clone(); - - let updated_transaction_details = TransactionDetails { - transaction: None, - txid: Some(txid), - internal_id, - received: output.value, - sent: 0, - fee: None, - confirmation_time: ConfirmationTime::Confirmed { height: 0, time: now().as_secs() }, // FIXME: can't figure this out - labels: labels.clone(), - }; - - match persist_transaction_details(&storage, &updated_transaction_details) { - Ok(_) => { - log_info!(logger, "Transaction updated"); - }, - Err(e) => { - log_error!(logger, "Error updating transaction: {e}"); - }, - } - } - fedimint_wallet_client::DepositState::Claimed(_) => { - // Nothing really to change from confirmed to claimed - log_debug!(logger, "Transaction claimed"); - break; - } - fedimint_wallet_client::DepositState::Failed(e) => { - // TODO delete - log_error!(logger, "Transaction failed: {e}"); - break; - } + let mut stream_fut = Box::pin(s.next()).fuse(); + let delay_fut = Box::pin(timeout_future).fuse(); + pin_mut!(delay_fut); + + select! { + outcome_option = stream_fut => { + if let Some(outcome) = outcome_option { + match outcome { + fedimint_wallet_client::DepositState::WaitingForTransaction => { + // Nothing to do + log_debug!(logger, "Waiting for transaction"); + } + fedimint_wallet_client::DepositState::WaitingForConfirmation(tx) => { + // Pending state, update with info we have + log_debug!(logger, "Waiting for confirmation"); + let txid = Txid::from_slice(&tx.btc_transaction.txid()).expect("should convert"); + let internal_id = Txid::from_slice(&operation_id.0).expect("should convert"); + let output = tx.btc_transaction.output[tx.out_idx as usize].clone(); + + let updated_transaction_details = TransactionDetails { + transaction: None, + txid: Some(txid), + internal_id, + received: output.value, + sent: 0, + fee: None, + confirmation_time: ConfirmationTime::Unconfirmed { last_seen: now().as_secs() }, + labels: labels.clone(), + }; + + match persist_transaction_details(&storage, &updated_transaction_details) { + Ok(_) => { + log_info!(logger, "Transaction updated"); + }, + Err(e) => { + log_error!(logger, "Error updating transaction: {e}"); + }, } } - } - _ = delay_fut => { - if timeout.is_none() { - if stop.load(Ordering::Relaxed) { - break; + fedimint_wallet_client::DepositState::Confirmed(tx) => { + // Pending state, update with info we have + log_debug!(logger, "Transaction confirmed"); + let txid = Txid::from_slice(&tx.btc_transaction.txid()).expect("should convert"); + let internal_id = Txid::from_slice(&operation_id.0).expect("should convert"); + let output = tx.btc_transaction.output[tx.out_idx as usize].clone(); + + // store as confirmed 0 block height until we can check esplora after + let updated_transaction_details = TransactionDetails { + transaction: None, + txid: Some(txid), + internal_id, + received: output.value, + sent: 0, + fee: None, + confirmation_time: ConfirmationTime::Confirmed { height: 0, time: now().as_secs() }, + labels: labels.clone(), + }; + + match persist_transaction_details(&storage, &updated_transaction_details) { + Ok(_) => { + log_info!(logger, "Transaction updated"); + }, + Err(e) => { + log_error!(logger, "Error updating transaction: {e}"); + }, } - } else { - log_debug!( - logger, - "Timeout reached, exiting loop for on chain tx", - ); + + // we need to get confirmations for this txid and update + subscribe_onchain_confirmation_check(storage.clone(), esplora.clone(), txid, updated_transaction_details, stop.clone(), logger.clone()).await; + } + fedimint_wallet_client::DepositState::Claimed(_) => { + // Nothing really to change from confirmed to claimed + log_debug!(logger, "Transaction claimed"); + break; + } + fedimint_wallet_client::DepositState::Failed(e) => { + log_error!(logger, "Transaction failed: {e}"); + + // if we have the original transaction details, delete it + if let Some(t) = original_transaction_details { + match delete_transaction_details(&storage, &t) { + Ok(_) => { + log_info!(logger, "Transaction deleted"); + }, + Err(e) => { + log_error!(logger, "Error deleting transaction: {e}"); + }, + } + } + break; } } } } - log_trace!(logger, "Done with stream outcome",); + _ = delay_fut => { + if timeout.is_none() { + if stop.load(Ordering::Relaxed) { + break; + } + } else { + log_debug!( + logger, + "Timeout reached, exiting loop for on chain tx", + ); + break; + } + } } } + log_trace!(logger, "Done with stream outcome",); } #[derive(Clone)] diff --git a/mutiny-core/src/storage.rs b/mutiny-core/src/storage.rs index a6be6c796..9f970e74a 100644 --- a/mutiny-core/src/storage.rs +++ b/mutiny-core/src/storage.rs @@ -926,6 +926,24 @@ pub(crate) fn persist_transaction_details( Ok(()) } +pub(crate) fn delete_transaction_details( + storage: &S, + transaction_details: &TransactionDetails, +) -> Result<(), MutinyError> { + let key = transaction_details_key(transaction_details.internal_id); + storage.delete(&[key.clone()])?; + + // delete from index + let index = storage.activity_index(); + let mut index = index.try_write()?; + index.insert(IndexItem { + timestamp: None, + key, + }); + + Ok(()) +} + pub(crate) fn get_transaction_details( storage: &S, internal_id: Txid, From 570cdc7ae67864f224a9108eeeac6c490757909e Mon Sep 17 00:00:00 2001 From: Tony Giorgio Date: Wed, 8 May 2024 12:12:50 -0500 Subject: [PATCH 7/8] Cleanup and handle tags without persisting first --- mutiny-core/src/federation.rs | 122 ++++++++++++++-------------------- mutiny-core/src/lib.rs | 5 +- mutiny-core/src/storage.rs | 13 ++-- 3 files changed, 61 insertions(+), 79 deletions(-) diff --git a/mutiny-core/src/federation.rs b/mutiny-core/src/federation.rs index 5d51bf85a..353476d7b 100644 --- a/mutiny-core/src/federation.rs +++ b/mutiny-core/src/federation.rs @@ -11,11 +11,11 @@ use crate::{ storage::{ delete_transaction_details, get_transaction_details, list_payment_info, persist_payment_info, persist_transaction_details, MutinyStorage, VersionedValue, - TRANSACTION_DETAILS_PREFIX_KEY, }, utils::sleep, HTLCStatus, MutinyInvoice, DEFAULT_PAYMENT_TIMEOUT, }; +use crate::{labels::LabelStorage, storage::TRANSACTION_DETAILS_PREFIX_KEY}; use async_lock::RwLock; use async_trait::async_trait; use bdk_chain::ConfirmationTime; @@ -428,14 +428,14 @@ impl FederationClient { .map(|(h, _i)| h.0), ); - // pending on chain operations - let pending_wallet_txids = self + // confirmed on chain operations + let confirmed_wallet_txids = self .storage .scan::(TRANSACTION_DETAILS_PREFIX_KEY, None)? .into_iter() .filter(|(_k, v)| match v.confirmation_time { - ConfirmationTime::Unconfirmed { .. } => true, // return all unconfirmed transactions - _ => false, // skip confirmed transactions + ConfirmationTime::Unconfirmed { .. } => false, // skip unconfirmed transactions + ConfirmationTime::Confirmed { .. } => true, // return all confirmed transactions }) .map(|(_h, i)| i.internal_id) .collect::>(); @@ -472,7 +472,8 @@ impl FederationClient { .map_err(|_| MutinyError::ChainAccessFailed) .expect("should convert"); - if pending_wallet_txids.contains(&internal_id) { + // if already confirmed, no reason to subscribe + if !confirmed_wallet_txids.contains(&internal_id) { self.subscribe_operation(entry, key.operation_id); } } else { @@ -574,23 +575,13 @@ impl FederationClient { .get_deposit_address(fedimint_core::time::now() + PEG_IN_TIMEOUT_YEAR, ()) .await?; - let internal_id = Txid::from_slice(&op_id.0).map_err(|_| MutinyError::ChainAccessFailed)?; - - // persist the data we can while we wait for the transaction to come from fedimint - let pending_transaction_details = TransactionDetails { - transaction: None, - txid: None, - internal_id, - received: 0, - sent: 0, - fee: None, - confirmation_time: ConfirmationTime::Unconfirmed { - last_seen: now().as_secs(), - }, - labels, - }; + let address = Address::from_str(&address.to_string()) + .expect("should convert") + .assume_checked(); - persist_transaction_details(&self.storage, &pending_transaction_details)?; + // persist the labels + self.storage + .set_address_labels(address.clone(), labels.clone())?; // subscribe let operation = self @@ -601,9 +592,7 @@ impl FederationClient { .expect("just created it"); self.subscribe_operation(operation, op_id); - Ok(Address::from_str(&address.to_string()) - .expect("should convert") - .assume_checked()) + Ok(address) } /// Get the balance of this federation client in sats @@ -732,7 +721,7 @@ impl FederationClient { amount: u64, labels: Vec, ) -> Result { - let address = bitcoin30_to_bitcoin29_address(send_to); + let address = bitcoin30_to_bitcoin29_address(send_to.clone()); let btc_amount = fedimint_ln_common::bitcoin::Amount::from_sat(amount); @@ -760,11 +749,14 @@ impl FederationClient { confirmation_time: ConfirmationTime::Unconfirmed { last_seen: now().as_secs(), }, - labels, + labels: labels.clone(), }; persist_transaction_details(&self.storage, &pending_transaction_details)?; + // persist the labels + self.storage.set_address_labels(send_to, labels)?; + // subscribe let operation = self .fedimint_client @@ -1301,24 +1293,25 @@ async fn process_operation_until_timeout( } else if module_type == WalletCommonInit::KIND.as_str() { let wallet_meta: WalletOperationMeta = entry.meta(); let wallet_module = Arc::new(fedimint_client.get_first_module::()); - let internal_id = Txid::from_slice(&operation_id.0) - .map_err(|_| MutinyError::ChainAccessFailed) - .expect("should convert"); - let stored_transaction_details = get_transaction_details(&storage, internal_id, &logger); - if stored_transaction_details.is_none() { - log_warn!(logger, "could not find transaction details: {internal_id}") - } match wallet_meta.variant { fedimint_wallet_client::WalletOperationMetaVariant::Deposit { - address: _, + address, expires_at: _, } => { match wallet_module.subscribe_deposit_updates(operation_id).await { Ok(o) => { + let labels = match storage.get_address_labels() { + Ok(l) => l.get(&address.to_string()).cloned(), + Err(e) => { + log_warn!(logger, "could not get labels: {e}"); + None + } + }; + process_onchain_deposit_outcome( o, - stored_transaction_details, + labels.unwrap_or_default(), operation_id, storage, esplora, @@ -1334,16 +1327,24 @@ async fn process_operation_until_timeout( }; } fedimint_wallet_client::WalletOperationMetaVariant::Withdraw { - address: _, + address, amount, fee, change: _, } => { match wallet_module.subscribe_withdraw_updates(operation_id).await { Ok(o) => { + let labels = match storage.get_address_labels() { + Ok(l) => l.get(&address.to_string()).cloned(), + Err(e) => { + log_warn!(logger, "could not get labels: {e}"); + None + } + }; + process_onchain_withdraw_outcome( o, - stored_transaction_details, + labels.unwrap_or_default(), amount, fee.amount(), operation_id, @@ -1479,7 +1480,7 @@ where #[allow(clippy::too_many_arguments)] async fn process_onchain_withdraw_outcome( stream_or_outcome: UpdateStreamOrOutcome, - original_transaction_details: Option, + labels: Vec, amount: fedimint_ln_common::bitcoin::Amount, fee: fedimint_ln_common::bitcoin::Amount, operation_id: OperationId, @@ -1489,10 +1490,7 @@ async fn process_onchain_withdraw_outcome( stop: Arc, logger: Arc, ) { - let labels = original_transaction_details - .as_ref() - .map(|o| o.labels.clone()) - .unwrap_or_default(); + let internal_id = Txid::from_slice(&operation_id.0).expect("should convert"); let mut s = stream_or_outcome.into_stream(); @@ -1520,7 +1518,6 @@ async fn process_onchain_withdraw_outcome( WithdrawState::Succeeded(txid) => { log_info!(logger, "Withdraw successful: {txid}"); - let internal_id = Txid::from_slice(&operation_id.0).expect("should convert"); let txid = Txid::from_slice(&txid).expect("should convert"); let updated_transaction_details = TransactionDetails { transaction: None, @@ -1550,16 +1547,14 @@ async fn process_onchain_withdraw_outcome( WithdrawState::Failed(e) => { log_error!(logger, "Transaction failed: {e}"); - // if we have the original transaction details, delete it - if let Some(t) = original_transaction_details { - match delete_transaction_details(&storage, &t) { - Ok(_) => { - log_info!(logger, "Transaction deleted"); - }, - Err(e) => { - log_error!(logger, "Error deleting transaction: {e}"); - }, - } + // Delete the pending tx if it failed + match delete_transaction_details(&storage, internal_id) { + Ok(_) => { + log_info!(logger, "Transaction deleted"); + }, + Err(e) => { + log_error!(logger, "Error deleting transaction: {e}"); + }, } break; @@ -1632,7 +1627,7 @@ async fn subscribe_onchain_confirmation_check( #[allow(clippy::too_many_arguments)] async fn process_onchain_deposit_outcome( stream_or_outcome: UpdateStreamOrOutcome, - original_transaction_details: Option, + labels: Vec, operation_id: OperationId, storage: S, esplora: Arc, @@ -1640,11 +1635,6 @@ async fn process_onchain_deposit_outcome( stop: Arc, logger: Arc, ) { - let labels = original_transaction_details - .as_ref() - .map(|o| o.labels.clone()) - .unwrap_or_default(); - let mut s = stream_or_outcome.into_stream(); // break out after sleep time or check stop signal @@ -1734,18 +1724,6 @@ async fn process_onchain_deposit_outcome( fedimint_wallet_client::DepositState::Failed(e) => { log_error!(logger, "Transaction failed: {e}"); - // if we have the original transaction details, delete it - if let Some(t) = original_transaction_details { - match delete_transaction_details(&storage, &t) { - Ok(_) => { - log_info!(logger, "Transaction deleted"); - }, - Err(e) => { - log_error!(logger, "Error deleting transaction: {e}"); - }, - } - } - break; } } diff --git a/mutiny-core/src/lib.rs b/mutiny-core/src/lib.rs index 2ee3ba01c..4415d9aaf 100644 --- a/mutiny-core/src/lib.rs +++ b/mutiny-core/src/lib.rs @@ -2062,7 +2062,10 @@ impl MutinyWallet { } } - async fn create_address(&self, labels: Vec) -> Result { + pub async fn create_address( + &self, + labels: Vec, + ) -> Result { // Attempt to create federation invoice if available let federation_ids = self.list_federation_ids().await?; if !federation_ids.is_empty() { diff --git a/mutiny-core/src/storage.rs b/mutiny-core/src/storage.rs index 9f970e74a..e5a5a41ae 100644 --- a/mutiny-core/src/storage.rs +++ b/mutiny-core/src/storage.rs @@ -926,19 +926,20 @@ pub(crate) fn persist_transaction_details( Ok(()) } +// Deletes the transaction detail and removes the pending index if it exists pub(crate) fn delete_transaction_details( storage: &S, - transaction_details: &TransactionDetails, + txid: Txid, ) -> Result<(), MutinyError> { - let key = transaction_details_key(transaction_details.internal_id); + let key = transaction_details_key(txid); storage.delete(&[key.clone()])?; - // delete from index + // delete the pending index item, if it exists let index = storage.activity_index(); let mut index = index.try_write()?; - index.insert(IndexItem { - timestamp: None, - key, + index.remove(&IndexItem { + timestamp: None, // timestamp would be None for Unconfirmed + key: key.clone(), }); Ok(()) From ce0bf87171bde93d192fe46ec9e7fa53ea97af9d Mon Sep 17 00:00:00 2001 From: Tony Giorgio Date: Wed, 8 May 2024 18:53:48 -0500 Subject: [PATCH 8/8] Review nits --- mutiny-core/src/federation.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/mutiny-core/src/federation.rs b/mutiny-core/src/federation.rs index 353476d7b..b52a2687a 100644 --- a/mutiny-core/src/federation.rs +++ b/mutiny-core/src/federation.rs @@ -1600,7 +1600,7 @@ async fn subscribe_onchain_confirmation_check( log_info!(logger, "Transaction confirmed"); transaction_details.confirmation_time = ConfirmationTime::Confirmed { height: s.block_height.expect("confirmed"), - time: now().as_secs(), + time: s.block_time.unwrap_or(now().as_secs()), }; match persist_transaction_details(&storage, &transaction_details) { Ok(_) => { @@ -1618,7 +1618,14 @@ async fn subscribe_onchain_confirmation_check( } } - sleep(5_000).await; + // wait for one minute before checking mempool again + // sleep every second to check if we need to stop + for _ in 0..60 { + if stop.load(Ordering::Relaxed) { + return; + } + sleep(1_000).await; + } } }); }