Skip to content

Commit

Permalink
Add on chain withdraw
Browse files Browse the repository at this point in the history
  • Loading branch information
TonyGiorgio committed May 2, 2024
1 parent e32a464 commit 983cdd9
Show file tree
Hide file tree
Showing 3 changed files with 236 additions and 27 deletions.
197 changes: 173 additions & 24 deletions mutiny-core/src/federation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand All @@ -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,
Expand Down Expand Up @@ -192,6 +194,7 @@ pub(crate) struct FederationClient<S: MutinyStorage> {
#[allow(dead_code)]
fedimint_storage: FedimintStorage<S>,
gateway: Arc<RwLock<Option<LightningGateway>>>,
network: Network,
stop: Arc<AtomicBool>,
pub(crate) logger: Arc<MutinyLogger>,
}
Expand Down Expand Up @@ -332,6 +335,7 @@ impl<S: MutinyStorage> FederationClient<S> {
storage,
logger,
invite_code: federation_code,
network,
stop,
gateway,
};
Expand Down Expand Up @@ -649,6 +653,58 @@ impl<S: MutinyStorage> FederationClient<S> {
}
}

/// Send on chain transaction
pub(crate) async fn send_onchain(
&self,
send_to: bitcoin::Address<NetworkUnchecked>,
amount: u64,
labels: Vec<String>,
) -> Result<Txid, MutinyError> {
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::<WalletClientModule>();

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");
self.subscribe_operation(operation, op_id);

todo!()
}

/// Someone received a payment on our behalf, we need to claim it
pub async fn claim_external_receive(
&self,
Expand Down Expand Up @@ -977,33 +1033,29 @@ async fn process_operation_until_timeout<S: MutinyStorage>(
}
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
Expand Down Expand Up @@ -1120,6 +1172,103 @@ where
invoice
}

async fn process_onchain_withdraw_outcome<S: MutinyStorage>(
stream_or_outcome: UpdateStreamOrOutcome<fedimint_wallet_client::WithdrawState>,
original_transaction_details: Option<TransactionDetails>,
amount: fedimint_ln_common::bitcoin::Amount,
fee: fedimint_ln_common::bitcoin::Amount,
operation_id: OperationId,
storage: S,
timeout: Option<u64>,
stop: Arc<AtomicBool>,
logger: Arc<MutinyLogger>,
) {
let labels = original_transaction_details
.as_ref()
.map(|o| o.labels.clone())
.unwrap_or(Vec::new());

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) => {
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: amount.to_sat(),
sent: 0,
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
},
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<S: MutinyStorage>(
stream_or_outcome: UpdateStreamOrOutcome<fedimint_wallet_client::DepositState>,
original_transaction_details: Option<TransactionDetails>,
Expand Down
65 changes: 63 additions & 2 deletions mutiny-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,10 +94,13 @@ use ::nostr::{EventBuilder, EventId, JsonUtil, Keys, Kind};
use async_lock::RwLock;
use bdk_chain::ConfirmationTime;
use bip39::Mnemonic;
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};
use fedimint_core::{api::InviteCode, config::FederationId};
use futures::{pin_mut, select, FutureExt};
use futures_util::join;
Expand Down Expand Up @@ -1801,6 +1804,64 @@ impl<S: MutinyStorage> MutinyWallet<S> {
Ok(Some(lsp_fee + federation_fee))
}

pub async fn send_to_address(
&self,
send_to: Address<NetworkUnchecked>,
amount: u64,
labels: Vec<String>,
fee_rate: Option<f32>,
) -> Result<Txid, MutinyError> {
// 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.clone(), labels.clone())
.await
{
Ok(t) => {
return Ok(t);
}
Err(e) => match e {
MutinyError::PaymentTimeout => return Err(e),
MutinyError::RoutingFailed => {
log_debug!(
self.logger,
"could not make payment through federation: {e}"
);
last_federation_error = Some(e);
continue;
}
_ => {
log_warn!(self.logger, "unhandled error: {e}");
last_federation_error = Some(e);
}
},
}
}
// If payment fails or invoice amount is None 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<String>) -> Result<bitcoin::Address, MutinyError> {
// Attempt to create federation invoice if available
let federation_ids = self.list_federation_ids().await?;
Expand Down
1 change: 0 additions & 1 deletion mutiny-wasm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down

0 comments on commit 983cdd9

Please sign in to comment.