diff --git a/cli/src/commands.rs b/cli/src/commands.rs index 835b30f34..1f0f29da1 100644 --- a/cli/src/commands.rs +++ b/cli/src/commands.rs @@ -8,7 +8,7 @@ use rustyline::history::DefaultHistory; use rustyline::Editor; use rustyline::{hint::HistoryHinter, Completer, Helper, Hinter, Validator}; -use breez_sdk_liquid::{ReceivePaymentRequest, Wallet}; +use breez_sdk_liquid::{ReceivePaymentRequest, SendPaymentResponse, Wallet}; #[derive(Parser, Debug, Clone, PartialEq)] pub(crate) enum Command { @@ -61,13 +61,14 @@ pub(crate) fn handle_command( )) } Command::SendPayment { bolt11 } => { - let response = wallet.send_payment(&bolt11)?; + let prepare_response = dbg!(wallet.prepare_payment(&bolt11)?); + let SendPaymentResponse { txid } = wallet.send_payment(&prepare_response)?; Ok(format!( r#" Successfully paid the invoice! You can view the onchain transaction at https://blockstream.info/liquidtestnet/tx/{}"#, - response.txid + txid )) } Command::GetInfo => { diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 066ee17d1..1909d8e4e 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -49,7 +49,8 @@ mod tests { println!("Please paste the invoice to be paid: "); io::stdin().read_line(&mut invoice)?; - breez_wallet.send_payment(&invoice)?; + let prepare_response = breez_wallet.prepare_payment(&invoice)?; + breez_wallet.send_payment(&prepare_response)?; Ok(()) } diff --git a/lib/src/model.rs b/lib/src/model.rs index 7a6119d6a..70d1af3c7 100644 --- a/lib/src/model.rs +++ b/lib/src/model.rs @@ -1,6 +1,57 @@ use boltz_client::util::error::S5Error; use lwk_signer::SwSigner; use lwk_wollet::{ElectrumUrl, ElementsNetwork}; +use rusqlite::types::FromSql; + +#[derive(Debug)] +#[allow(dead_code)] +pub(crate) enum SwapTypeFilter { + Sent, + Received, +} + +#[derive(Debug, Eq, PartialEq, Hash)] +pub(crate) enum SwapType { + ReverseSubmarine, + Submarine, +} + +impl ToString for SwapType { + fn to_string(&self) -> String { + match self { + SwapType::Submarine => "Sent", + SwapType::ReverseSubmarine => "Received", + } + .to_string() + } +} + +impl FromSql for SwapType { + fn column_result(value: rusqlite::types::ValueRef<'_>) -> rusqlite::types::FromSqlResult { + String::column_result(value).and_then(|str| match str.as_str() { + "Sent" => Ok(SwapType::Submarine), + "Received" => Ok(SwapType::ReverseSubmarine), + _ => Err(rusqlite::types::FromSqlError::InvalidType), + }) + } +} + +#[derive(Debug)] +pub(crate) struct OngoingSwap { + pub id: String, + pub swap_type: SwapType, + + /// For [SwapType::Submarine] + pub preimage: Option, + pub redeem_script: Option, + pub blinding_key: Option, + pub invoice_amount_sat: Option, + pub onchain_amount_sat: Option, + + /// For [SwapType::ReverseSubmarine] + pub funding_address: Option, + pub funding_amount_sat: Option, +} pub enum Network { Liquid, @@ -28,29 +79,37 @@ pub struct WalletOptions { pub electrum_url: Option, } +pub struct ReceivePaymentRequest { + pub invoice_amount_sat: Option, + pub onchain_amount_sat: Option, +} + #[derive(Debug)] -pub struct SwapLbtcResponse { +pub struct ReceivePaymentResponse { pub id: String, pub invoice: String, } -pub enum SwapStatus { - Created, - Mempool, - Completed, +#[derive(Debug)] +pub struct PreparePaymentRequest { + pub invoice: String, } -pub struct ReceivePaymentRequest { - pub invoice_amount_sat: Option, - pub onchain_amount_sat: Option, +#[derive(Debug)] +pub struct PreparePaymentResponse { + pub id: String, + pub funding_address: String, + pub funding_amount_sat: u64, + pub fees_sat: u64, } +#[derive(Debug)] pub struct SendPaymentResponse { pub txid: String, } #[derive(thiserror::Error, Debug)] -pub enum SwapError { +pub enum PaymentError { #[error("Could not contact Boltz servers: {err}")] ServersUnreachable { err: String }, @@ -79,15 +138,15 @@ pub enum SwapError { BoltzGeneric { err: String }, } -impl From for SwapError { +impl From for PaymentError { fn from(err: S5Error) -> Self { match err.kind { boltz_client::util::error::ErrorKind::Network | boltz_client::util::error::ErrorKind::BoltzApi => { - SwapError::ServersUnreachable { err: err.message } + PaymentError::ServersUnreachable { err: err.message } } - boltz_client::util::error::ErrorKind::Input => SwapError::BadResponse, - _ => SwapError::BoltzGeneric { err: err.message }, + boltz_client::util::error::ErrorKind::Input => PaymentError::BadResponse, + _ => PaymentError::BoltzGeneric { err: err.message }, } } } @@ -99,30 +158,21 @@ pub struct WalletInfo { pub active_address: String, } -#[derive(Debug)] -pub struct OngoingReceiveSwap { - pub id: String, - pub preimage: String, - pub redeem_script: String, - pub blinding_key: String, - pub invoice_amount_sat: u64, - pub onchain_amount_sat: u64, -} - -pub struct OngoingSendSwap { - pub id: String, - // pub preimage: String, - // pub redeem_script: String, - // pub blinding_key: String, - // pub invoice_amount_sat: Option, - // pub onchain_amount_sat: Option, -} - #[derive(Debug)] pub enum PaymentType { Sent, Received, PendingReceive, + PendingSend, +} + +impl From for PaymentType { + fn from(swap_type: SwapType) -> Self { + match swap_type { + SwapType::ReverseSubmarine => Self::PendingReceive, + SwapType::Submarine => Self::PendingSend, + } + } } #[derive(Debug)] diff --git a/lib/src/persist/migrations.rs b/lib/src/persist/migrations.rs index 3e95fa076..ba9fb701d 100644 --- a/lib/src/persist/migrations.rs +++ b/lib/src/persist/migrations.rs @@ -2,11 +2,14 @@ pub(crate) fn current_migrations() -> Vec<&'static str> { vec![ "CREATE TABLE IF NOT EXISTS ongoing_swaps ( id TEXT NOT NULL PRIMARY KEY, - preimage TEXT NOT NULL, - redeem_script TEXT NOT NULL, - blinding_key TEXT NOT NULL, - invoice_amount_sat INTEGER NOT NULL, - onchain_amount_sat INTEGER NOT NULL + swap_type TEXT NOT NULL check(swap_type in('Sent', 'Received')), + preimage TEXT, + redeem_script TEXT, + blinding_key TEXT, + invoice_amount_sat INTEGER, + onchain_amount_sat INTEGER, + funding_address TEXT, + funding_amount_sat INTEGER ) STRICT;", ] } diff --git a/lib/src/persist/mod.rs b/lib/src/persist/mod.rs index 54d60d70d..de30d5674 100644 --- a/lib/src/persist/mod.rs +++ b/lib/src/persist/mod.rs @@ -1,13 +1,15 @@ mod migrations; +use std::collections::HashSet; + use anyhow::Result; use rusqlite::{params, Connection, Row}; use rusqlite_migration::{Migrations, M}; -use crate::OngoingReceiveSwap; - use migrations::current_migrations; +use crate::{OngoingSwap, SwapType, SwapTypeFilter}; + pub(crate) struct Persister { main_db_file: String, } @@ -35,31 +37,37 @@ impl Persister { Ok(()) } - pub fn insert_ongoing_swaps(&self, swaps: &[OngoingReceiveSwap]) -> Result<()> { + pub fn insert_ongoing_swaps(&self, swaps: &[OngoingSwap]) -> Result<()> { let con = self.get_connection()?; let mut stmt = con.prepare( " INSERT INTO ongoing_swaps ( id, + swap_type, preimage, redeem_script, blinding_key, invoice_amount_sat, - onchain_amount_sat + onchain_amount_sat, + funding_address, + funding_amount_sat ) - VALUES (?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) ", )?; for swap in swaps { _ = stmt.execute(( &swap.id, + &swap.swap_type.to_string(), &swap.preimage, &swap.redeem_script, &swap.blinding_key, &swap.invoice_amount_sat, &swap.onchain_amount_sat, + &swap.funding_address, + &swap.funding_amount_sat, ))? } @@ -75,27 +83,82 @@ impl Persister { Ok(()) } - pub fn list_ongoing_swaps(&self) -> Result> { + fn build_where_clause(&self, type_filters: &Option>) -> String { + let mut where_clause: Vec = vec![]; + + if let Some(filters) = type_filters { + if !filters.is_empty() { + let mut type_filter_clause: HashSet = HashSet::new(); + for type_filter in filters { + type_filter_clause.insert(match type_filter { + SwapTypeFilter::Sent => SwapType::Submarine, + SwapTypeFilter::Received => SwapType::ReverseSubmarine, + }); + } + + where_clause.push(format!( + "swap_type in ({})", + type_filter_clause + .iter() + .map(|t| format!("'{}'", t.to_string())) + .collect::>() + .join(", ") + )); + } + } + + let mut where_clause_str = String::new(); + if !where_clause.is_empty() { + where_clause_str = String::from("where "); + where_clause_str.push_str(where_clause.join(" and ").as_str()); + } + where_clause_str + } + + fn sql_row_to_swap(&self, row: &Row) -> Result { + Ok(OngoingSwap { + id: row.get(0)?, + swap_type: row.get(1)?, + preimage: row.get(2)?, + redeem_script: row.get(3)?, + blinding_key: row.get(4)?, + invoice_amount_sat: row.get(5)?, + onchain_amount_sat: row.get(6)?, + funding_address: row.get(7)?, + funding_amount_sat: row.get(8)?, + }) + } + + pub fn list_ongoing_swaps( + &self, + type_filters: Option>, + ) -> Result> { let con = self.get_connection()?; - let mut stmt = con.prepare("SELECT * FROM ongoing_swaps")?; + let where_clause = self.build_where_clause(&type_filters); - let swaps: Vec = stmt + let mut stmt = con.prepare(&format!( + " + SELECT + id, + swap_type, + preimage, + redeem_script, + blinding_key, + invoice_amount_sat, + onchain_amount_sat, + funding_address, + funding_amount_sat + FROM ongoing_swaps + {where_clause} + " + ))?; + + let swaps: Vec = stmt .query_map(params![], |row| self.sql_row_to_swap(row))? .map(|i| i.unwrap()) .collect(); Ok(swaps) } - - fn sql_row_to_swap(&self, row: &Row) -> Result { - Ok(OngoingReceiveSwap { - id: row.get(0)?, - preimage: row.get(1)?, - redeem_script: row.get(2)?, - blinding_key: row.get(3)?, - invoice_amount_sat: row.get(4)?, - onchain_amount_sat: row.get(5)?, - }) - } } diff --git a/lib/src/wallet.rs b/lib/src/wallet.rs index 4c7da27ba..a800ff84f 100644 --- a/lib/src/wallet.rs +++ b/lib/src/wallet.rs @@ -24,8 +24,9 @@ use lwk_wollet::{ }; use crate::{ - persist::Persister, Network, OngoingReceiveSwap, Payment, PaymentType, ReceivePaymentRequest, - SendPaymentResponse, SwapError, SwapLbtcResponse, WalletInfo, WalletOptions, + persist::Persister, Network, OngoingSwap, Payment, PaymentError, PaymentType, + PreparePaymentResponse, ReceivePaymentRequest, ReceivePaymentResponse, SendPaymentResponse, + SwapType, SwapTypeFilter, WalletInfo, WalletOptions, }; // To avoid sendrawtransaction error "min relay fee not met" @@ -105,18 +106,22 @@ impl Wallet { thread::spawn(move || loop { thread::sleep(Duration::from_secs(5)); - let ongoing_swaps = cloned.swap_persister.list_ongoing_swaps().unwrap(); + let ongoing_swaps = cloned + .swap_persister + .list_ongoing_swaps(Some(vec![SwapTypeFilter::Received])) + .unwrap(); thread::scope(|scope| { for swap in ongoing_swaps { scope.spawn(|| { - let OngoingReceiveSwap { + let OngoingSwap { preimage, redeem_script, blinding_key, .. } = swap; - match cloned.try_claim(&preimage, &redeem_script, &blinding_key, None) { + + match cloned.try_claim(preimage, redeem_script, blinding_key, None) { Ok(_) => cloned.swap_persister.resolve_ongoing_swap(swap.id).unwrap(), Err(e) => warn!("Could not claim yet. Err: {}", e), } @@ -209,24 +214,24 @@ impl Wallet { Ok(txid.to_string()) } - pub fn send_payment(&self, invoice: &str) -> Result { + pub fn prepare_payment(&self, invoice: &str) -> Result { let client = self.boltz_client(); let invoice = invoice .trim() .parse::() - .map_err(|_| SwapError::InvalidInvoice)?; + .map_err(|_| PaymentError::InvalidInvoice)?; let lbtc_pair = client.get_pairs()?.get_lbtc_pair()?; let amount_sat = invoice .amount_milli_satoshis() - .ok_or(SwapError::AmountOutOfRange)? + .ok_or(PaymentError::AmountOutOfRange)? / 1000; lbtc_pair .limits .within(amount_sat) - .map_err(|_| SwapError::AmountOutOfRange)?; + .map_err(|_| PaymentError::AmountOutOfRange)?; let swap_response = client.create_swap(CreateSwapRequest::new_lbtc_submarine( &lbtc_pair.hash, @@ -234,31 +239,49 @@ impl Wallet { "", ))?; - let funding_amount = swap_response.get_funding_amount()?; - - let funding_addr = swap_response.get_funding_address()?; + Ok(PreparePaymentResponse { + id: swap_response.get_id(), + funding_address: swap_response.get_funding_address()?, + funding_amount_sat: swap_response.get_funding_amount()?, + fees_sat: lbtc_pair.fees.submarine_boltz(amount_sat)? + + lbtc_pair.fees.submarine_lockup_estimate(), + }) + } + pub fn send_payment( + &self, + res: &PreparePaymentResponse, + ) -> Result { let signer = AnySigner::Software(self.get_signer()); let txid = self - .sign_and_send(&[signer], None, &funding_addr, funding_amount) - .map_err(|_| SwapError::SendError)?; + .sign_and_send( + &[signer], + None, + &res.funding_address, + res.funding_amount_sat, + ) + .map_err(|_| PaymentError::SendError)?; Ok(SendPaymentResponse { txid }) } fn try_claim( &self, - preimage: &str, - redeem_script: &str, - blinding_key: &str, + preimage: Option, + redeem_script: Option, + blinding_key: Option, absolute_fees: Option, - ) -> Result { + ) -> Result { + let preimage = preimage.ok_or(PaymentError::PersistError)?; + let redeem_script = redeem_script.ok_or(PaymentError::PersistError)?; + let blinding_key = blinding_key.ok_or(PaymentError::PersistError)?; + let network_config = &self.get_network_config(); let mut rev_swap_tx = LBtcSwapTx::new_claim( - LBtcSwapScript::reverse_from_str(redeem_script, blinding_key)?, + LBtcSwapScript::reverse_from_str(&redeem_script, &blinding_key)?, self.address() - .map_err(|_| SwapError::WalletError)? + .map_err(|_| PaymentError::WalletError)? .to_string(), network_config, )?; @@ -271,7 +294,7 @@ impl Wallet { let signed_tx = rev_swap_tx.sign_claim( &lsk.keypair, - &Preimage::from_str(preimage)?, + &Preimage::from_str(&preimage)?, absolute_fees.unwrap_or(CLAIM_ABSOLUTE_FEES), )?; let txid = rev_swap_tx.broadcast(signed_tx, network_config)?; @@ -282,11 +305,11 @@ impl Wallet { pub fn receive_payment( &self, req: ReceivePaymentRequest, - ) -> Result { + ) -> Result { let mut amount_sat = req .onchain_amount_sat .or(req.invoice_amount_sat) - .ok_or(SwapError::AmountOutOfRange)?; + .ok_or(PaymentError::AmountOutOfRange)?; let client = self.boltz_client(); @@ -295,7 +318,7 @@ impl Wallet { lbtc_pair .limits .within(amount_sat) - .map_err(|_| SwapError::AmountOutOfRange)?; + .map_err(|_| PaymentError::AmountOutOfRange)?; let mnemonic = self.signer.mnemonic(); let swap_key = @@ -303,7 +326,7 @@ impl Wallet { let lsk = LiquidSwapKey::from(swap_key); let preimage = Preimage::new(); - let preimage_str = preimage.to_string().ok_or(SwapError::InvalidPreimage)?; + let preimage_str = preimage.to_string().ok_or(PaymentError::InvalidPreimage)?; let preimage_hash = preimage.sha256.to_string(); let swap_response = if req.onchain_amount_sat.is_some() { @@ -331,31 +354,34 @@ impl Wallet { // Double check that the generated invoice includes our data // https://docs.boltz.exchange/v/api/dont-trust-verify#lightning-invoice-verification if invoice.payment_hash().to_string() != preimage_hash { - return Err(SwapError::InvalidInvoice); + return Err(PaymentError::InvalidInvoice); }; let invoice_amount_sat = invoice .amount_milli_satoshis() - .ok_or(SwapError::InvalidInvoice)? + .ok_or(PaymentError::InvalidInvoice)? / 1000; self.swap_persister - .insert_ongoing_swaps(dbg!(&[OngoingReceiveSwap { + .insert_ongoing_swaps(dbg!(&[OngoingSwap { id: swap_id.clone(), - preimage: preimage_str, - blinding_key: blinding_str, - redeem_script, - invoice_amount_sat, - onchain_amount_sat: req.onchain_amount_sat.unwrap_or( + swap_type: SwapType::ReverseSubmarine, + preimage: Some(preimage_str), + blinding_key: Some(blinding_str), + redeem_script: Some(redeem_script), + invoice_amount_sat: Some(invoice_amount_sat), + onchain_amount_sat: req.onchain_amount_sat.or(Some( invoice_amount_sat - lbtc_pair.fees.reverse_boltz(invoice_amount_sat)? - lbtc_pair.fees.reverse_lockup()? - CLAIM_ABSOLUTE_FEES - ), + )), + funding_address: None, + funding_amount_sat: None, }])) - .map_err(|_| SwapError::PersistError)?; + .map_err(|_| PaymentError::PersistError)?; - Ok(SwapLbtcResponse { + Ok(ReceivePaymentResponse { id: swap_id, invoice: invoice.to_string(), }) @@ -386,7 +412,7 @@ impl Wallet { .collect(); if include_pending { - let pending_swaps = self.swap_persister.list_ongoing_swaps()?; + let pending_swaps = self.swap_persister.list_ongoing_swaps(None)?; for swap in pending_swaps { payments.insert( @@ -394,8 +420,12 @@ impl Wallet { Payment { id: None, timestamp: None, - payment_type: PaymentType::PendingReceive, - amount_sat: swap.invoice_amount_sat, + payment_type: swap.swap_type.into(), + amount_sat: swap + .onchain_amount_sat + .or(swap.invoice_amount_sat) + .or(swap.funding_amount_sat) + .unwrap_or(0), }, ); }