From f6a48f83fb3bddccdf57c7bf4c030998a581da8f Mon Sep 17 00:00:00 2001 From: benthecarman Date: Tue, 14 May 2024 14:59:17 -0500 Subject: [PATCH] On-chain send and receive --- src/bridge.rs | 19 ++++++++- src/core.rs | 78 +++++++++++++++++++++++++++++++++--- src/fedimint_client.rs | 91 +++++++++++++++++++++++++++++++++++++++++- src/main.rs | 74 +++++++++++++++++++++++++++++----- src/routes/receive.rs | 17 ++++++-- 5 files changed, 256 insertions(+), 23 deletions(-) diff --git a/src/bridge.rs b/src/bridge.rs index 5036c2b..0ff7b9d 100644 --- a/src/bridge.rs +++ b/src/bridge.rs @@ -1,4 +1,4 @@ -use bitcoin::Txid; +use bitcoin::{Address, Txid}; use fedimint_core::api::InviteCode; use fedimint_core::Amount; use fedimint_ln_common::lightning_invoice::Bolt11Invoice; @@ -8,6 +8,8 @@ use tokio::sync::mpsc; pub enum UICoreMsg { SendLightning(Bolt11Invoice), ReceiveLightning(Amount), + SendOnChain { address: Address, amount_sats: u64 }, + ReceiveOnChain, AddFederation(InviteCode), Unlock(String), } @@ -29,8 +31,9 @@ pub enum CoreUIMsg { Sending, SendSuccess(SendSuccessMsg), SendFailure(String), - ReceiveInvoiceGenerating, + ReceiveGenerating, ReceiveInvoiceGenerated(Bolt11Invoice), + ReceiveAddressGenerated(Address), ReceiveSuccess(ReceiveSuccessMsg), ReceiveFailed(String), BalanceUpdated(Amount), @@ -61,11 +64,23 @@ impl UIHandle { self.msg_send(UICoreMsg::SendLightning(invoice)).await; } + pub async fn send_onchain(&self, address: Address, amount_sats: u64) { + self.msg_send(UICoreMsg::SendOnChain { + address, + amount_sats, + }) + .await; + } + pub async fn receive(&self, amount: u64) { self.msg_send(UICoreMsg::ReceiveLightning(Amount::from_sats(amount))) .await; } + pub async fn receive_onchain(&self) { + self.msg_send(UICoreMsg::ReceiveOnChain).await; + } + pub async fn unlock(&self, password: String) { self.msg_send(UICoreMsg::Unlock(password)).await; } diff --git a/src/core.rs b/src/core.rs index 7baa9f1..519886a 100644 --- a/src/core.rs +++ b/src/core.rs @@ -1,16 +1,18 @@ use anyhow::anyhow; use bip39::Mnemonic; -use bitcoin::Network; +use bitcoin::{Address, Network}; use fedimint_core::api::InviteCode; use fedimint_core::config::FederationId; use fedimint_core::Amount; use fedimint_ln_client::{LightningClientModule, PayType}; use fedimint_ln_common::lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription, Description}; +use fedimint_wallet_client::WalletClientModule; use std::collections::HashMap; use std::path::PathBuf; use std::str::FromStr; use std::sync::atomic::AtomicBool; use std::sync::Arc; +use std::time::{Duration, SystemTime}; use iced::{ futures::{channel::mpsc::Sender, SinkExt}, @@ -19,6 +21,9 @@ use iced::{ use log::error; use tokio::sync::RwLock; +use crate::fedimint_client::{ + spawn_onchain_payment_subscription, spawn_onchain_receive_subscription, +}; use crate::{ bridge::{self, CoreUIMsg, UICoreMsg}, conf::{self, get_mnemonic}, @@ -33,6 +38,8 @@ use crate::{ }, }; +const PEG_IN_TIMEOUT_YEAR: Duration = Duration::from_secs(86400 * 365); + struct HarborCore { balance: Amount, network: Network, @@ -130,6 +137,44 @@ impl HarborCore { Ok(invoice) } + async fn send_onchain(&self, address: Address, sats: u64) -> anyhow::Result<()> { + // todo go through all clients and select the first one that has enough balance + let client = self.get_client().await.fedimint_client; + let onchain = client.get_first_module::(); + + let amount = bitcoin::Amount::from_sat(sats); + + // todo add manual fee selection + let fees = onchain.get_withdraw_fees(address.clone(), amount).await?; + + let op_id = onchain + .withdraw(address, bitcoin::Amount::from_sat(sats), fees, ()) + .await?; + + let sub = onchain.subscribe_withdraw_updates(op_id).await?; + + spawn_onchain_payment_subscription(self.tx.clone(), client.clone(), sub).await; + + Ok(()) + } + + async fn receive_onchain(&self) -> anyhow::Result
{ + // todo add federation id selection + let client = self.get_client().await.fedimint_client; + let onchain = client.get_first_module::(); + + // expire the address in 1 year + let valid_until = SystemTime::now() + PEG_IN_TIMEOUT_YEAR; + + let (op_id, address) = onchain.get_deposit_address(valid_until, ()).await?; + + let sub = onchain.subscribe_deposit_updates(op_id).await?; + + spawn_onchain_receive_subscription(self.tx.clone(), client.clone(), sub).await; + + Ok(address) + } + async fn add_federation(&self, invite_code: InviteCode) -> anyhow::Result<()> { let id = invite_code.federation_id(); @@ -245,16 +290,37 @@ pub fn run_core() -> Subscription { } } UICoreMsg::ReceiveLightning(amount) => { - core.msg(CoreUIMsg::ReceiveInvoiceGenerating).await; + core.msg(CoreUIMsg::ReceiveGenerating).await; match core.receive_lightning(amount).await { Err(e) => { core.msg(CoreUIMsg::ReceiveFailed(e.to_string())).await; } Ok(invoice) => { - core.msg(CoreUIMsg::ReceiveInvoiceGenerated( - invoice.clone(), - )) - .await; + core.msg(CoreUIMsg::ReceiveInvoiceGenerated(invoice)) + .await; + } + } + } + UICoreMsg::SendOnChain { + address, + amount_sats, + } => { + log::info!("Got UICoreMsg::SendOnChain"); + core.msg(CoreUIMsg::Sending).await; + if let Err(e) = core.send_onchain(address, amount_sats).await { + error!("Error sending: {e}"); + core.msg(CoreUIMsg::SendFailure(e.to_string())).await; + } + } + UICoreMsg::ReceiveOnChain => { + core.msg(CoreUIMsg::ReceiveGenerating).await; + match core.receive_onchain().await { + Err(e) => { + core.msg(CoreUIMsg::ReceiveFailed(e.to_string())).await; + } + Ok(address) => { + core.msg(CoreUIMsg::ReceiveAddressGenerated(address)) + .await; } } } diff --git a/src/fedimint_client.rs b/src/fedimint_client.rs index 4f47f01..b97158f 100644 --- a/src/fedimint_client.rs +++ b/src/fedimint_client.rs @@ -22,7 +22,7 @@ use fedimint_ln_client::{ }; use fedimint_ln_common::LightningGateway; use fedimint_mint_client::MintClientInit; -use fedimint_wallet_client::{WalletClientInit, WalletClientModule}; +use fedimint_wallet_client::{DepositState, WalletClientInit, WalletClientModule, WithdrawState}; use iced::futures::channel::mpsc::Sender; use iced::futures::{SinkExt, StreamExt}; use log::{debug, error, info, trace}; @@ -334,6 +334,95 @@ pub(crate) async fn spawn_internal_payment_subscription( }); } +pub(crate) async fn spawn_onchain_payment_subscription( + mut sender: Sender, + client: ClientHandleArc, + subscription: UpdateStreamOrOutcome, +) { + spawn(async move { + let mut stream = subscription.into_stream(); + while let Some(op_state) = stream.next().await { + match op_state { + WithdrawState::Created => {} + WithdrawState::Failed(error) => { + error!("Onchain payment failed: {error:?}"); + sender + .send(Message::CoreMessage(CoreUIMsg::SendFailure( + error.to_string(), + ))) + .await + .unwrap(); + break; + } + WithdrawState::Succeeded(txid) => { + info!("Onchain payment success: {txid}"); + let params = SendSuccessMsg::Onchain { txid }; + sender + .send(Message::CoreMessage(CoreUIMsg::SendSuccess(params))) + .await + .unwrap(); + + let new_balance = client.get_balance().await; + sender + .send(Message::CoreMessage(CoreUIMsg::BalanceUpdated(new_balance))) + .await + .unwrap(); + + break; + } + } + } + }); +} + +pub(crate) async fn spawn_onchain_receive_subscription( + mut sender: Sender, + client: ClientHandleArc, + subscription: UpdateStreamOrOutcome, +) { + spawn(async move { + let mut stream = subscription.into_stream(); + while let Some(op_state) = stream.next().await { + match op_state { + DepositState::WaitingForTransaction => {} + DepositState::Failed(error) => { + error!("Onchain receive failed: {error:?}"); + sender + .send(Message::CoreMessage(CoreUIMsg::ReceiveFailed( + error.to_string(), + ))) + .await + .unwrap(); + break; + } + DepositState::WaitingForConfirmation(data) => { + info!("Onchain receive waiting for confirmation: {data:?}"); + let params = ReceiveSuccessMsg::Onchain { + txid: data.btc_transaction.txid(), + }; + sender + .send(Message::CoreMessage(CoreUIMsg::ReceiveSuccess(params))) + .await + .unwrap(); + } + DepositState::Confirmed(data) => { + info!("Onchain receive confirmed: {data:?}"); + } + DepositState::Claimed(data) => { + info!("Onchain receive claimed: {data:?}"); + let new_balance = client.get_balance().await; + sender + .send(Message::CoreMessage(CoreUIMsg::BalanceUpdated(new_balance))) + .await + .unwrap(); + + break; + } + } + } + }); +} + #[derive(Clone)] pub struct FedimintStorage { storage: Arc, diff --git a/src/main.rs b/src/main.rs index 4e61ff8..4f21f09 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,4 @@ +use bitcoin::Address; use core::run_core; use fedimint_core::api::InviteCode; use fedimint_core::Amount; @@ -13,7 +14,7 @@ use iced::widget::row; use iced::Element; use iced::{clipboard, program, Color}; use iced::{Command, Font}; -use log::info; +use log::{error, info}; use crate::components::focus_input_id; @@ -63,6 +64,7 @@ pub struct HarborWallet { receive_status: ReceiveStatus, receive_amount_str: String, receive_invoice: Option, + receive_address: Option
, receive_qr_data: Option, mint_invite_code_str: String, add_federation_failure_reason: Option, @@ -115,6 +117,7 @@ pub enum Message { Send(String), Receive(u64), GenerateInvoice, + GenerateAddress, Unlock(String), AddFederation(String), // Core messages we get from core @@ -139,6 +142,7 @@ impl HarborWallet { receive_failure_reason: None, receive_status: ReceiveStatus::Idle, receive_invoice: None, + receive_address: None, receive_qr_data: None, mint_invite_code_str: String::new(), add_federation_failure_reason: None, @@ -157,6 +161,20 @@ impl HarborWallet { } } + async fn async_send_onchain( + ui_handle: Option>, + address: Address, + amount_sats: u64, + ) { + println!("Got to async_send"); + if let Some(ui_handle) = ui_handle { + println!("Have a ui_handle, sending the invoice over"); + ui_handle.clone().send_onchain(address, amount_sats).await; + } else { + panic!("UI handle is None"); + } + } + async fn async_receive(ui_handle: Option>, amount: u64) { if let Some(ui_handle) = ui_handle { ui_handle.clone().receive(amount).await; @@ -165,6 +183,14 @@ impl HarborWallet { } } + async fn async_receive_onchain(ui_handle: Option>) { + if let Some(ui_handle) = ui_handle { + ui_handle.clone().receive_onchain().await; + } else { + panic!("UI handle is None"); + } + } + async fn async_unlock(ui_handle: Option>, password: String) { if let Some(ui_handle) = ui_handle { ui_handle.clone().unlock(password).await; @@ -228,14 +254,20 @@ impl HarborWallet { SendStatus::Sending => Command::none(), _ => { self.send_failure_reason = None; - // todo get invoice from user - let invoice = Bolt11Invoice::from_str(&invoice_str).unwrap(); - println!("Sending to invoice: {invoice}"); - // let invoice = Bolt11Invoice::from_str(&invoice_str).unwrap(); - Command::perform(Self::async_send(self.ui_handle.clone(), invoice), |_| { - // I don't know if this is the best way to do this but we don't really know anyting after we've fired the message - Message::Noop - }) + if let Ok(invoice) = Bolt11Invoice::from_str(&invoice_str) { + Command::perform(Self::async_send(self.ui_handle.clone(), invoice), |_| { + Message::Noop + }) + } else if let Ok(address) = Address::from_str(&invoice_str) { + let amount = self.send_amount_input_str.parse::().unwrap(); // TODO: error handling + Command::perform( + Self::async_send_onchain(self.ui_handle.clone(), address, amount), + |_| Message::Noop, + ) + } else { + error!("Invalid invoice or address"); + Command::none() + } } }, Message::Receive(amount) => match self.send_status { @@ -265,6 +297,15 @@ impl HarborWallet { } } }, + Message::GenerateAddress => match self.receive_status { + ReceiveStatus::Generating => Command::none(), + _ => { + self.receive_failure_reason = None; + Command::perform(Self::async_receive_onchain(self.ui_handle.clone()), |_| { + Message::Noop + }) + } + }, Message::Unlock(password) => match self.unlock_status { UnlockStatus::Unlocking => Command::none(), _ => { @@ -320,7 +361,7 @@ impl HarborWallet { self.balance = balance; Command::none() } - CoreUIMsg::ReceiveInvoiceGenerating => { + CoreUIMsg::ReceiveGenerating => { self.receive_status = ReceiveStatus::Generating; Command::none() } @@ -345,6 +386,19 @@ impl HarborWallet { self.mint_invite_code_str = String::new(); Command::none() } + CoreUIMsg::ReceiveAddressGenerated(address) => { + self.receive_status = ReceiveStatus::WaitingToReceive; + println!("Received address: {address}"); + self.receive_qr_data = Some( + Data::with_error_correction( + format!("bitcoin:{address}"), + iced::widget::qr_code::ErrorCorrection::Low, + ) + .unwrap(), + ); + self.receive_address = Some(address); + Command::none() + } CoreUIMsg::Unlocking => { self.unlock_status = UnlockStatus::Unlocking; Command::none() diff --git a/src/routes/receive.rs b/src/routes/receive.rs index 7c26134..6c19eb2 100644 --- a/src/routes/receive.rs +++ b/src/routes/receive.rs @@ -7,7 +7,13 @@ use crate::components::{h_button, h_header, h_input, SvgIcon}; use crate::{HarborWallet, Message}; pub fn receive(harbor: &HarborWallet) -> Element { - let column = if let Some(invoice) = harbor.receive_invoice.as_ref() { + let receive_string = harbor + .receive_invoice + .as_ref() + .map(|i| i.to_string()) + .or_else(|| harbor.receive_address.as_ref().map(|a| a.to_string())); + + let column = if let Some(string) = receive_string { let header = h_header("Receive", "Scan this QR or copy the string."); let data = harbor.receive_qr_data.as_ref().unwrap(); @@ -24,14 +30,14 @@ pub fn receive(harbor: &HarborWallet) -> Element { ..Style::default() }); - let first_ten_chars = invoice.to_string().chars().take(10).collect::(); + let first_ten_chars = string.chars().take(10).collect::(); column![ header, qr_container, text(format!("{first_ten_chars}...")).size(16), h_button("Copy to clipboard", SvgIcon::Copy) - .on_press(Message::CopyToClipboard(invoice.to_string())), + .on_press(Message::CopyToClipboard(string)), ] } else { let header = h_header("Receive", "Receive on-chain or via lightning."); @@ -50,7 +56,10 @@ pub fn receive(harbor: &HarborWallet) -> Element { let generate_button = h_button("Generate Invoice", SvgIcon::DownLeft).on_press(Message::GenerateInvoice); - column![header, amount_input, generate_button] + let generate_address_button = + h_button("Generate Address", SvgIcon::Squirrel).on_press(Message::GenerateAddress); + + column![header, amount_input, generate_button, generate_address_button] }; container(scrollable(