diff --git a/Cargo.lock b/Cargo.lock index 9d742cc..8c4c565 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4068,6 +4068,7 @@ dependencies = [ "harbor-client", "iced", "keyring-lib", + "lnurl-rs", "log", "lyon_algorithms", "opener", diff --git a/Cargo.toml b/Cargo.toml index 7fada15..e2550ed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ members = [ [workspace.dependencies] chrono = "0.4.38" +lnurl-rs = { version = "0.9.0", default-features = false } log = "0.4" serde = { version = "1.0.217", features = ["derive"] } serde_json = "1.0.138" diff --git a/harbor-client/Cargo.toml b/harbor-client/Cargo.toml index 059b805..23b864b 100644 --- a/harbor-client/Cargo.toml +++ b/harbor-client/Cargo.toml @@ -48,7 +48,7 @@ fedimint-lnv2-client = "0.7.1" # BEGIN BLOCK OF KEEP IN SYNC WITH FEDIMINT'S VERSION arti-client = { version = "0.20.0", default-features = false, features = ["tokio", "rustls"], package = "fedimint-arti-client" } -lnurl-rs = { version = "0.9.0", default-features = false } +lnurl-rs = { workspace = true } hyper = { version = "1.6.0", default-features = false, features = ["client", "http1"] } hyper-rustls = { version = "0.27.3", default-features = false } hyper-util = { version = "0.1.3", default-features = false, features = ["client", "client-legacy", "tokio"] } diff --git a/harbor-ui/Cargo.toml b/harbor-ui/Cargo.toml index 4f730f5..93f4dd1 100644 --- a/harbor-ui/Cargo.toml +++ b/harbor-ui/Cargo.toml @@ -23,4 +23,5 @@ uuid = { workspace = true } opener = { version = "0.7.2", features = ["reveal"] } serde = { workspace = true } serde_json = { workspace = true } -keyring-lib = "1.0.2" \ No newline at end of file +keyring-lib = "1.0.2" +lnurl-rs = { workspace = true } diff --git a/harbor-ui/src/main.rs b/harbor-ui/src/main.rs index 75f1746..96a0fe6 100644 --- a/harbor-ui/src/main.rs +++ b/harbor-ui/src/main.rs @@ -36,12 +36,12 @@ use crate::config::{Config, write_config}; use components::{MUTINY_GREEN, MUTINY_RED}; use harbor_client::Bolt11Invoice; use harbor_client::bip39::Mnemonic; +use harbor_client::bitcoin::address::NetworkUnchecked; use harbor_client::bitcoin::{Address, Network}; use harbor_client::db_models::MintItem; use harbor_client::db_models::transaction_item::TransactionItem; use harbor_client::fedimint_core::Amount; use harbor_client::fedimint_core::core::ModuleKind; -use harbor_client::lightning_address::parse_lnurl; use harbor_client::{ CoreUIMsg, CoreUIMsgPacket, MintConnectionInfo, MintIdentifier, ReceiveSuccessMsg, SendSuccessMsg, UICoreMsg, data_dir, @@ -53,6 +53,7 @@ use iced::widget::qr_code::Data; use iced::widget::row; use iced::{Color, clipboard}; use iced::{Element, window}; +use lnurl::lnurl::LnUrl; use log::{debug, error, info, trace}; use routes::Route; use std::collections::HashMap; @@ -159,6 +160,13 @@ pub enum AddFederationStatus { Adding, } +#[derive(Debug, Clone)] +pub enum SendDestination { + Invoice(Bolt11Invoice), + LnUrl(LnUrl), + Address(Address), +} + #[derive(Debug, Clone)] pub enum Message { // Setup @@ -201,7 +209,7 @@ pub enum Message { SetTorEnabled(bool), // Async commands we fire from the UI to core Noop, - Send(String), + Send(SendDestination), Transfer, GenerateInvoice, GenerateAddress, @@ -613,7 +621,7 @@ impl HarborWallet { } // Async commands we fire from the UI to core Message::Noop => Task::none(), - Message::Send(invoice_str) => match self.send_status { + Message::Send(destination) => match self.send_status { SendStatus::Sending => Task::none(), SendStatus::Idle => { self.send_failure_reason = None; @@ -629,70 +637,59 @@ impl HarborWallet { } }; - if let Ok(invoice) = Bolt11Invoice::from_str(&invoice_str) { - let (id, task) = - self.send_from_ui(UICoreMsg::SendLightning { mint, invoice }); - self.current_send_id = Some(id); - task - } else { - match parse_lnurl(&invoice_str) { - Ok(lnurl) => { - // TODO: can we handle is_max somehow? - let amount = if self.is_max { - return Task::done(Message::AddToast(Toast { - title: "Cannot send max with Lightning Address".to_string(), - body: Some("Please enter a specific amount".to_string()), - status: ToastStatus::Bad, - })); - } else { - match self.send_amount_input_str.parse::() { - Ok(amount) => amount, - Err(e) => { - error!("Error parsing amount: {e}"); - self.send_failure_reason = Some(e.to_string()); - return Task::none(); - } + match destination { + SendDestination::Invoice(invoice) => { + let (id, task) = + self.send_from_ui(UICoreMsg::SendLightning { mint, invoice }); + self.current_send_id = Some(id); + task + } + SendDestination::LnUrl(lnurl) => { + // TODO: can we handle is_max somehow? + let amount = if self.is_max { + return Task::done(Message::AddToast(Toast { + title: "Cannot send max with Lightning Address".to_string(), + body: Some("Please enter a specific amount".to_string()), + status: ToastStatus::Bad, + })); + } else { + match self.send_amount_input_str.parse::() { + Ok(amount) => amount, + Err(e) => { + error!("Error parsing amount: {e}"); + self.send_failure_reason = Some(e.to_string()); + return Task::none(); } - }; - let (id, task) = self.send_from_ui(UICoreMsg::SendLnurlPay { - mint, - lnurl, - amount_sats: amount, - }); - self.current_send_id = Some(id); - task - } - _ => { - if let Ok(address) = Address::from_str(&invoice_str) { - let amount = if self.is_max { - None - } else { - match self.send_amount_input_str.parse::() { - Ok(amount) => Some(amount), - Err(e) => { - error!("Error parsing amount: {e}"); - self.send_failure_reason = Some(e.to_string()); - return Task::none(); - } - } - }; - let (id, task) = self.send_from_ui(UICoreMsg::SendOnChain { - mint, - address, - amount_sats: amount, - }); - self.current_send_id = Some(id); - task - } else { - error!("Invalid invoice or address"); - self.current_send_id = None; - Task::done(Message::AddToast(Toast { - title: "Failed to send".to_string(), - body: Some("Invalid invoice or address".to_string()), - status: ToastStatus::Bad, - })) } - } + }; + let (id, task) = self.send_from_ui(UICoreMsg::SendLnurlPay { + mint, + lnurl, + amount_sats: amount, + }); + self.current_send_id = Some(id); + task + } + SendDestination::Address(address) => { + let amount = if self.is_max { + None + } else { + match self.send_amount_input_str.parse::() { + Ok(amount) => Some(amount), + Err(e) => { + error!("Error parsing amount: {e}"); + self.send_failure_reason = Some(e.to_string()); + return Task::none(); + } + } + }; + let (id, task) = self.send_from_ui(UICoreMsg::SendOnChain { + mint, + address, + amount_sats: amount, + }); + self.current_send_id = Some(id); + task } } } diff --git a/harbor-ui/src/routes/send.rs b/harbor-ui/src/routes/send.rs index c9f3b92..e2b322c 100644 --- a/harbor-ui/src/routes/send.rs +++ b/harbor-ui/src/routes/send.rs @@ -1,11 +1,18 @@ +use std::str::FromStr; + use iced::Element; use iced::widget::{column, row}; +use harbor_client::Bolt11Invoice; +use harbor_client::bitcoin::Address; +use harbor_client::bitcoin::address::NetworkUnchecked; +use harbor_client::lightning_address::parse_lnurl; + use crate::components::{ ConfirmModalState, InputArgs, SvgIcon, basic_layout, h_button, h_checkbox, h_header, h_input, h_screen_header, operation_status_for_id, }; -use crate::{HarborWallet, Message, SendStatus}; +use crate::{HarborWallet, Message, SendDestination, SendStatus}; pub fn send(harbor: &HarborWallet) -> Element { let header = h_header("Send", "Send to an on-chain address or lightning invoice."); @@ -34,7 +41,7 @@ pub fn send(harbor: &HarborWallet) -> Element { SvgIcon::UpRight, harbor.send_status == SendStatus::Sending, ) - .on_press(Message::Send(harbor.send_dest_input_str.clone())); + .on_press_maybe(parse_send_destination(&harbor.send_dest_input_str).map(Message::Send)); let checkbox = h_checkbox( "Send Max", @@ -78,3 +85,19 @@ pub fn send(harbor: &HarborWallet) -> Element { column![h_screen_header(harbor, true, false), basic_layout(content)].into() } + +fn parse_send_destination(input: &str) -> Option { + if let Ok(invoice) = Bolt11Invoice::from_str(input) { + return Some(SendDestination::Invoice(invoice)); + } + + if let Ok(lnurl) = parse_lnurl(input) { + return Some(SendDestination::LnUrl(lnurl)); + } + + if let Ok(address) = input.parse::>() { + return Some(SendDestination::Address(address)); + } + + None +}