From 2929d668cfa577244ae20ce5984d883c33591e2e Mon Sep 17 00:00:00 2001 From: yse <70684173+hydra-yse@users.noreply.github.com> Date: Thu, 18 Apr 2024 11:24:04 +0200 Subject: [PATCH] feat: add fee persistence to payments (#83) --- cli/src/commands.rs | 44 ++- lib/ls-sdk-bindings/src/lib.rs | 24 +- lib/ls-sdk-bindings/src/ls_sdk.udl | 47 ++-- lib/ls-sdk-core/src/lib.rs | 19 +- lib/ls-sdk-core/src/model.rs | 110 ++++++-- lib/ls-sdk-core/src/persist/migrations.rs | 7 +- lib/ls-sdk-core/src/persist/mod.rs | 82 ++++-- lib/ls-sdk-core/src/wallet.rs | 311 ++++++++++++++-------- 8 files changed, 452 insertions(+), 192 deletions(-) diff --git a/cli/src/commands.rs b/cli/src/commands.rs index 1d0dc6c..51c4578 100644 --- a/cli/src/commands.rs +++ b/cli/src/commands.rs @@ -1,17 +1,19 @@ use std::borrow::Cow::{self, Owned}; +use std::io::Write; use std::sync::Arc; use std::thread; use std::time::Duration; use anyhow::Result; use clap::{arg, Parser}; -use ls_sdk::{ReceivePaymentRequest, Wallet}; +use ls_sdk::{PrepareReceiveRequest, Wallet}; use qrcode_rs::render::unicode; use qrcode_rs::{EcLevel, QrCode}; use rustyline::highlight::Highlighter; use rustyline::history::DefaultHistory; use rustyline::Editor; use rustyline::{hint::HistoryHinter, Completer, Helper, Hinter, Validator}; + use serde::Serialize; use serde_json::to_string_pretty; @@ -68,6 +70,19 @@ macro_rules! command_result { }}; } +macro_rules! wait_confirmation { + ($prompt:expr,$result:expr) => { + print!("{}", $prompt); + std::io::stdout().flush()?; + + let mut buf = String::new(); + std::io::stdin().read_line(&mut buf)?; + if !['y', 'Y'].contains(&(buf.as_bytes()[0] as char)) { + return Ok(command_result!($result)); + } + }; +} + pub(crate) fn handle_command( _rl: &mut Editor, wallet: &Arc, @@ -78,20 +93,37 @@ pub(crate) fn handle_command( receiver_amount_sat, payer_amount_sat, } => { - let response = wallet.receive_payment(ReceivePaymentRequest { + let prepare_response = wallet.prepare_receive_payment(&PrepareReceiveRequest { payer_amount_sat, receiver_amount_sat, })?; + wait_confirmation!( + format!( + "Fees: {} sat. Are the fees acceptable? (y/N) ", + prepare_response.fees_sat + ), + "Payment receive halted" + ); + + let response = wallet.receive_payment(&prepare_response)?; let invoice = response.invoice.clone(); + let mut result = command_result!(response); result.push('\n'); result.push_str(&build_qr_text(&invoice)); - result } Command::SendPayment { bolt11, delay } => { - let prepare_response = wallet.prepare_payment(&bolt11)?; + let prepare_response = wallet.prepare_send_payment(&bolt11)?; + + wait_confirmation!( + format!( + "Fees: {} sat. Are the fees acceptable? (y/N) ", + prepare_response.total_fees + ), + "Payment send halted" + ); if let Some(delay) = delay { let wallet_cloned = wallet.clone(); @@ -111,7 +143,9 @@ pub(crate) fn handle_command( command_result!(wallet.get_info(true)?) } Command::ListPayments => { - command_result!(wallet.list_payments(true, true)?) + let mut payments = wallet.list_payments(true, true)?; + payments.reverse(); + command_result!(payments) } Command::EmptyCache => { wallet.empty_wallet_cache()?; diff --git a/lib/ls-sdk-bindings/src/lib.rs b/lib/ls-sdk-bindings/src/lib.rs index c3bc714..a132125 100644 --- a/lib/ls-sdk-bindings/src/lib.rs +++ b/lib/ls-sdk-bindings/src/lib.rs @@ -2,8 +2,8 @@ use std::sync::Arc; use anyhow::{Error, Result}; use ls_sdk::{ - model::PaymentError, Network, PreparePaymentResponse, ReceivePaymentRequest, - ReceivePaymentResponse, SendPaymentResponse, Wallet, WalletInfo, + model::PaymentError, Network, PrepareReceiveRequest, PrepareReceiveResponse, + PrepareSendResponse, ReceivePaymentResponse, SendPaymentResponse, Wallet, WalletInfo, }; // TODO Unify error enum @@ -37,18 +37,32 @@ impl BindingWallet { self.ln_sdk.get_info(with_scan).map_err(Into::into) } + pub fn prepare_send_payment( + &self, + invoice: String, + ) -> Result { + self.ln_sdk.prepare_send_payment(&invoice) + } + pub fn send_payment( &self, - req: PreparePaymentResponse, + req: PrepareSendResponse, ) -> Result { self.ln_sdk.send_payment(&req) } + pub fn prepare_receive_payment( + &self, + req: PrepareReceiveRequest, + ) -> Result { + self.ln_sdk.prepare_receive_payment(&req) + } + pub fn receive_payment( &self, - req: ReceivePaymentRequest, + req: PrepareReceiveResponse, ) -> Result { - self.ln_sdk.receive_payment(req) + self.ln_sdk.receive_payment(&req) } } diff --git a/lib/ls-sdk-bindings/src/ls_sdk.udl b/lib/ls-sdk-bindings/src/ls_sdk.udl index 2480e60..c62d9ff 100644 --- a/lib/ls-sdk-bindings/src/ls_sdk.udl +++ b/lib/ls-sdk-bindings/src/ls_sdk.udl @@ -8,11 +8,13 @@ enum PaymentError { "AmountOutOfRange", "InvalidInvoice", "SendError", - "WalletError", "PersistError", "InvalidPreimage", "AlreadyClaimed", + "PairsNotFound", + "SignerError", "BoltzError", + "LwkError", }; enum Network { @@ -26,19 +28,12 @@ dictionary WalletInfo { string active_address; }; -dictionary PreparePaymentResponse { +dictionary PrepareSendResponse { string id; - u64 funding_amount; + u64 payer_amount_sat; + u64 receiver_amount_sat; + u64 total_fees; string funding_address; -}; - -dictionary ReceivePaymentRequest { - u64? payer_amount_sat; - u64? receiver_amount_sat; -}; - -dictionary ReceivePaymentResponse { - string id; string invoice; }; @@ -46,6 +41,22 @@ dictionary SendPaymentResponse { string txid; }; +dictionary PrepareReceiveRequest { + u64? payer_amount_sat; + u64? receiver_amount_sat; +}; + +dictionary PrepareReceiveResponse { + string pair_hash; + u64 payer_amount_sat; + u64 fees_sat; +}; + +dictionary ReceivePaymentResponse { + string id; + string invoice; +}; + namespace ls_sdk { [Throws=LsSdkError] BindingWallet init(string mnemonic, string? data_dir, Network network); @@ -56,8 +67,14 @@ interface BindingWallet { WalletInfo get_info(boolean with_scan); [Throws=PaymentError] - SendPaymentResponse send_payment(PreparePaymentResponse req); + PrepareSendResponse prepare_send_payment(string invoice); [Throws=PaymentError] - ReceivePaymentResponse receive_payment(ReceivePaymentRequest req); -}; \ No newline at end of file + SendPaymentResponse send_payment(PrepareSendResponse req); + + [Throws=PaymentError] + PrepareReceiveResponse prepare_receive_payment(PrepareReceiveRequest req); + + [Throws=PaymentError] + ReceivePaymentResponse receive_payment(PrepareReceiveResponse req); +}; diff --git a/lib/ls-sdk-core/src/lib.rs b/lib/ls-sdk-core/src/lib.rs index 887f71b..fd78308 100644 --- a/lib/ls-sdk-core/src/lib.rs +++ b/lib/ls-sdk-core/src/lib.rs @@ -19,12 +19,24 @@ macro_rules! ensure_sdk { }; } +#[macro_export] +macro_rules! get_invoice_amount { + ($invoice:expr) => { + $invoice + .parse::() + .expect("Expecting valid invoice") + .amount_milli_satoshis() + .expect("Expecting valid amount") + / 1000 + }; +} + #[cfg(test)] mod tests { use anyhow::Result; use tempdir::TempDir; - use crate::{Network, Payment, PaymentType, ReceivePaymentRequest, Wallet}; + use crate::{Network, Payment, PaymentType, PrepareReceiveRequest, Wallet}; const TEST_MNEMONIC: &str = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; @@ -57,7 +69,7 @@ mod tests { let breez_wallet = Wallet::init(TEST_MNEMONIC, Some(data_dir_str), Network::LiquidTestnet)?; let invoice = "lntb10u1pnqwkjrpp5j8ucv9mgww0ajk95yfpvuq0gg5825s207clrzl5thvtuzfn68h0sdqqcqzzsxqr23srzjqv8clnrfs9keq3zlg589jvzpw87cqh6rjks0f9g2t9tvuvcqgcl45f6pqqqqqfcqqyqqqqlgqqqqqqgq2qsp5jnuprlxrargr6hgnnahl28nvutj3gkmxmmssu8ztfhmmey3gq2ss9qyyssq9ejvcp6frwklf73xvskzdcuhnnw8dmxag6v44pffwqrxznsly4nqedem3p3zhn6u4ln7k79vk6zv55jjljhnac4gnvr677fyhfgn07qp4x6wrq"; - breez_wallet.prepare_payment(&invoice)?; + breez_wallet.prepare_send_payment(&invoice)?; assert!(!list_pending(&breez_wallet)?.is_empty()); Ok(()) @@ -68,10 +80,11 @@ mod tests { let (_data_dir, data_dir_str) = create_temp_dir()?; let breez_wallet = Wallet::init(TEST_MNEMONIC, Some(data_dir_str), Network::LiquidTestnet)?; - breez_wallet.receive_payment(ReceivePaymentRequest { + let prepare_response = breez_wallet.prepare_receive_payment(&PrepareReceiveRequest { receiver_amount_sat: Some(1000), payer_amount_sat: None, })?; + breez_wallet.receive_payment(&prepare_response)?; assert!(!list_pending(&breez_wallet)?.is_empty()); Ok(()) diff --git a/lib/ls-sdk-core/src/model.rs b/lib/ls-sdk-core/src/model.rs index a9d6bbe..4023ea4 100644 --- a/lib/ls-sdk-core/src/model.rs +++ b/lib/ls-sdk-core/src/model.rs @@ -1,9 +1,11 @@ -use boltz_client::error::Error; use boltz_client::network::Chain; +use boltz_client::Bolt11Invoice; use lwk_signer::SwSigner; use lwk_wollet::{ElectrumUrl, ElementsNetwork, WolletDescriptor}; use serde::Serialize; +use crate::get_invoice_amount; + #[derive(Debug, Copy, Clone, PartialEq)] pub enum Network { Liquid, @@ -57,23 +59,33 @@ impl WalletOptions { } } -#[derive(Debug)] -pub struct ReceivePaymentRequest { +#[derive(Debug, Serialize)] +pub struct PrepareReceiveRequest { pub payer_amount_sat: Option, pub receiver_amount_sat: Option, } +#[derive(Debug, Serialize)] +pub struct PrepareReceiveResponse { + pub pair_hash: String, + pub payer_amount_sat: u64, + pub fees_sat: u64, +} + #[derive(Debug, Serialize)] pub struct ReceivePaymentResponse { pub id: String, pub invoice: String, } -#[derive(Debug, Clone, Serialize)] -pub struct PreparePaymentResponse { +#[derive(Debug, Serialize, Clone)] +pub struct PrepareSendResponse { pub id: String, - pub funding_amount: u64, + pub payer_amount_sat: u64, + pub receiver_amount_sat: u64, + pub total_fees: u64, pub funding_address: String, + pub invoice: String, } #[derive(Debug, Serialize)] @@ -92,9 +104,6 @@ pub enum PaymentError { #[error("Could not sign/send the transaction: {err}")] SendError { err: String }, - #[error("Could not fetch the required wallet information")] - WalletError, - #[error("Could not store the swap details locally")] PersistError, @@ -104,14 +113,23 @@ pub enum PaymentError { #[error("The specified funds have already been claimed")] AlreadyClaimed, + #[error("Boltz did not return any pairs from the request")] + PairsNotFound, + + #[error("Could not sign the transaction: {err}")] + SignerError { err: String }, + #[error("Boltz error: {err}")] BoltzError { err: String }, + + #[error("Lwk error: {err}")] + LwkError { err: String }, } -impl From for PaymentError { - fn from(err: Error) -> Self { +impl From for PaymentError { + fn from(err: boltz_client::error::Error) -> Self { match err { - Error::Protocol(msg) => { + boltz_client::error::Error::Protocol(msg) => { if msg == "Could not find utxos for script" { return PaymentError::AlreadyClaimed; } @@ -125,6 +143,28 @@ impl From for PaymentError { } } +#[allow(clippy::match_single_binding)] +impl From for PaymentError { + fn from(err: lwk_wollet::Error) -> Self { + match err { + _ => PaymentError::LwkError { + err: format!("{err:?}"), + }, + } + } +} + +#[allow(clippy::match_single_binding)] +impl From for PaymentError { + fn from(err: lwk_signer::SignerError) -> Self { + match err { + _ => PaymentError::SignerError { + err: format!("{err:?}"), + }, + } + } +} + #[derive(Debug, Serialize)] pub struct WalletInfo { pub balance_sat: u64, @@ -136,9 +176,10 @@ pub struct WalletInfo { pub(crate) enum OngoingSwap { Send { id: String, - amount_sat: u64, funding_address: String, invoice: String, + receiver_amount_sat: u64, + txid: Option, }, Receive { id: String, @@ -163,6 +204,7 @@ pub struct Payment { pub id: Option, pub timestamp: Option, pub amount_sat: u64, + pub fees_sat: Option, #[serde(rename(serialize = "type"))] pub payment_type: PaymentType, @@ -174,27 +216,39 @@ impl From for Payment { fn from(swap: OngoingSwap) -> Self { match swap { OngoingSwap::Send { - amount_sat, invoice, + receiver_amount_sat, .. - } => Payment { - id: None, - timestamp: None, - payment_type: PaymentType::PendingSend, - amount_sat, - invoice: Some(invoice), - }, + } => { + let payer_amount_sat = get_invoice_amount!(invoice); + Payment { + id: None, + timestamp: None, + payment_type: PaymentType::PendingSend, + amount_sat: payer_amount_sat, + invoice: Some(invoice), + fees_sat: Some(receiver_amount_sat - payer_amount_sat), + } + } OngoingSwap::Receive { receiver_amount_sat, invoice, .. - } => Payment { - id: None, - timestamp: None, - payment_type: PaymentType::PendingReceive, - amount_sat: receiver_amount_sat, - invoice: Some(invoice), - }, + } => { + let payer_amount_sat = get_invoice_amount!(invoice); + Payment { + id: None, + timestamp: None, + payment_type: PaymentType::PendingReceive, + amount_sat: receiver_amount_sat, + invoice: Some(invoice), + fees_sat: Some(payer_amount_sat - receiver_amount_sat), + } + } } } } + +pub(crate) struct PaymentData { + pub payer_amount_sat: u64, +} diff --git a/lib/ls-sdk-core/src/persist/migrations.rs b/lib/ls-sdk-core/src/persist/migrations.rs index 4b35051..2bad5b4 100644 --- a/lib/ls-sdk-core/src/persist/migrations.rs +++ b/lib/ls-sdk-core/src/persist/migrations.rs @@ -11,10 +11,15 @@ pub(crate) fn current_migrations() -> Vec<&'static str> { ) STRICT;", "CREATE TABLE IF NOT EXISTS ongoing_send_swaps ( id TEXT NOT NULL PRIMARY KEY, - amount_sat INTEGER NOT NULL, funding_address TEXT NOT NULL, invoice TEXT NOT NULL, + receiver_amount_sat INTEGER NOT NULL, + txid TEXT, created_at TEXT DEFAULT CURRENT_TIMESTAMP ) STRICT;", + "CREATE TABLE IF NOT EXISTS payment_data( + id TEXT NOT NULL PRIMARY KEY, + payer_amount_sat INTEGER NOT NULL + ) STRICT;", ] } diff --git a/lib/ls-sdk-core/src/persist/mod.rs b/lib/ls-sdk-core/src/persist/mod.rs index 5278899..72345db 100644 --- a/lib/ls-sdk-core/src/persist/mod.rs +++ b/lib/ls-sdk-core/src/persist/mod.rs @@ -1,13 +1,13 @@ mod migrations; -use std::{fs::create_dir_all, path::PathBuf, str::FromStr}; +use std::{collections::HashMap, fs::create_dir_all, path::PathBuf, str::FromStr}; use anyhow::Result; use migrations::current_migrations; use rusqlite::{params, Connection}; use rusqlite_migration::{Migrations, M}; -use crate::{Network, Network::*, OngoingSwap}; +use crate::{Network, Network::*, OngoingSwap, PaymentData}; pub(crate) struct Persister { main_db_dir: PathBuf, @@ -46,7 +46,7 @@ impl Persister { Ok(()) } - pub fn insert_ongoing_swap(&self, swaps: &[OngoingSwap]) -> Result<()> { + pub fn insert_or_update_ongoing_swap(&self, swaps: &[OngoingSwap]) -> Result<()> { let con = self.get_connection()?; for swap in swaps { @@ -54,22 +54,23 @@ impl Persister { OngoingSwap::Send { id, funding_address, - amount_sat, invoice, + receiver_amount_sat, + txid, } => { let mut stmt = con.prepare( " - INSERT INTO ongoing_send_swaps ( + INSERT OR REPLACE INTO ongoing_send_swaps ( id, - amount_sat, funding_address, - invoice + invoice, + receiver_amount_sat, + txid ) - VALUES (?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?) ", )?; - - _ = stmt.execute((&id, &amount_sat, &funding_address, invoice))? + _ = stmt.execute((id, funding_address, invoice, receiver_amount_sat, txid))? } OngoingSwap::Receive { id, @@ -81,7 +82,7 @@ impl Persister { } => { let mut stmt = con.prepare( " - INSERT INTO ongoing_receive_swaps ( + INSERT OR REPLACE INTO ongoing_receive_swaps ( id, preimage, redeem_script, @@ -94,12 +95,12 @@ impl Persister { )?; _ = stmt.execute(( - &id, - &preimage, - &redeem_script, - &blinding_key, - &invoice, - &receiver_amount_sat, + id, + preimage, + redeem_script, + blinding_key, + invoice, + receiver_amount_sat, ))? } } @@ -108,7 +109,11 @@ impl Persister { Ok(()) } - pub fn resolve_ongoing_swap(&self, id: &str) -> Result<()> { + pub fn resolve_ongoing_swap( + &self, + id: &str, + payment_data: Option<(String, PaymentData)>, + ) -> Result<()> { let mut con = self.get_connection()?; let tx = con.transaction()?; @@ -117,6 +122,13 @@ impl Persister { "DELETE FROM ongoing_receive_swaps WHERE id = ?", params![id], )?; + if let Some((txid, payment_data)) = payment_data { + tx.execute( + "INSERT INTO payment_data(id, payer_amount_sat) + VALUES(?, ?)", + (txid, payment_data.payer_amount_sat), + )?; + } tx.commit()?; Ok(()) @@ -134,9 +146,10 @@ impl Persister { " SELECT id, - amount_sat, funding_address, invoice, + receiver_amount_sat, + txid, created_at FROM ongoing_send_swaps ORDER BY created_at @@ -147,9 +160,10 @@ impl Persister { .query_map(params![], |row| { Ok(OngoingSwap::Send { id: row.get(0)?, - amount_sat: row.get(1)?, - funding_address: row.get(2)?, - invoice: row.get(3)?, + funding_address: row.get(1)?, + invoice: row.get(2)?, + receiver_amount_sat: row.get(3)?, + txid: row.get(4)?, }) })? .map(|i| i.unwrap()) @@ -190,4 +204,28 @@ impl Persister { Ok(ongoing_receive) } + + pub fn get_payment_data(&self) -> Result> { + let con = self.get_connection()?; + + let mut stmt = con.prepare( + " + SELECT id, payer_amount_sat + FROM payment_data + ", + )?; + + let data = stmt + .query_map(params![], |row| { + Ok(( + row.get(0)?, + PaymentData { + payer_amount_sat: row.get(1)?, + }, + )) + })? + .map(|i| i.unwrap()) + .collect(); + Ok(data) + } } diff --git a/lib/ls-sdk-core/src/wallet.rs b/lib/ls-sdk-core/src/wallet.rs index 9027a14..1480b55 100644 --- a/lib/ls-sdk-core/src/wallet.rs +++ b/lib/ls-sdk-core/src/wallet.rs @@ -19,11 +19,11 @@ use boltz_client::{ util::secrets::{LBtcReverseRecovery, LiquidSwapKey, Preimage, SwapKey}, Bolt11Invoice, Keypair, }; -use log::{debug, warn}; +use log::{debug, error, warn}; use lwk_common::{singlesig_desc, Signer, Singlesig}; use lwk_signer::{AnySigner, SwSigner}; use lwk_wollet::{ - elements::Address, + elements::{Address, Transaction}, full_scan_with_electrum_client, hashes::{sha256t_hash_newtype, Hash}, BlockchainBackend, ElectrumClient, ElectrumUrl, ElementsNetwork, FsPersister, @@ -31,9 +31,10 @@ use lwk_wollet::{ }; use crate::{ - ensure_sdk, persist::Persister, Network, OngoingSwap, Payment, PaymentError, PaymentType, - PreparePaymentResponse, ReceivePaymentRequest, ReceivePaymentResponse, SendPaymentResponse, - WalletInfo, WalletOptions, CLAIM_ABSOLUTE_FEES, DEFAULT_DATA_DIR, + ensure_sdk, get_invoice_amount, persist::Persister, Network, OngoingSwap, Payment, PaymentData, + PaymentError, PaymentType, PrepareReceiveRequest, PrepareReceiveResponse, PrepareSendResponse, + ReceivePaymentResponse, SendPaymentResponse, WalletInfo, WalletOptions, CLAIM_ABSOLUTE_FEES, + DEFAULT_DATA_DIR, }; sha256t_hash_newtype! { @@ -113,54 +114,116 @@ impl Wallet { Ok(descriptor_str.parse()?) } + fn try_resolve_pending_swap( + wallet: &Arc, + client: &BoltzApiClient, + swap: &OngoingSwap, + ) -> Result<()> { + match swap { + OngoingSwap::Receive { + id, + preimage, + redeem_script, + blinding_key, + invoice, + .. + } => { + let status_response = client + .swap_status(SwapStatusRequest { id: id.clone() }) + .map_err(|_| anyhow!("Could not contact Boltz servers for claim status"))?; + + let swap_state = status_response + .status + .parse::() + .map_err(|_| anyhow!("Invalid swap state received"))?; + + match swap_state { + SubSwapStates::SwapExpired => { + warn!("Cannot claim: swap expired"); + wallet + .persister + .resolve_ongoing_swap(id, None) + .map_err(|_| anyhow!("Could not resolve swap in database"))?; + } + SubSwapStates::TransactionMempool | SubSwapStates::TransactionConfirmed => {} + _ => { + return Err(anyhow!( + "Cannot claim: invoice not paid yet. Swap state: {}", + swap_state.to_string() + )); + } + } + + match wallet.try_claim(preimage, redeem_script, blinding_key, None) { + Ok(txid) => { + let payer_amount_sat = get_invoice_amount!(invoice); + wallet + .persister + .resolve_ongoing_swap( + id, + Some((txid, PaymentData { payer_amount_sat })), + ) + .map_err(|_| anyhow!("Could not resolve swap in database"))?; + } + Err(err) => { + if let PaymentError::AlreadyClaimed = err { + warn!("Funds already claimed"); + wallet + .persister + .resolve_ongoing_swap(id, None) + .map_err(|_| anyhow!("Could not resolve swap in database"))?; + } + warn!("Could not claim yet. Err: {err}"); + } + } + } + OngoingSwap::Send { + id, invoice, txid, .. + } => { + let Some(txid) = txid.clone() else { + return Err(anyhow!("Transaction not broadcast yet")); + }; + + let status_response = client + .swap_status(SwapStatusRequest { id: id.clone() }) + .map_err(|_| anyhow!("Could not contact Boltz servers for claim status"))?; + + if [ + SubSwapStates::TransactionClaimed.to_string(), + SubSwapStates::SwapExpired.to_string(), + ] + .contains(&status_response.status) + { + let payer_amount_sat = get_invoice_amount!(invoice); + wallet + .persister + .resolve_ongoing_swap(id, Some((txid, PaymentData { payer_amount_sat }))) + .map_err(|_| anyhow!("Could not resolve swap in database"))?; + } + } + }; + + Ok(()) + } + fn track_pending_swaps(self: &Arc) -> Result<()> { let cloned = self.clone(); let client = self.boltz_client(); thread::spawn(move || loop { thread::sleep(Duration::from_secs(5)); - let ongoing_swaps = cloned.persister.list_ongoing_swaps().unwrap(); + let Ok(ongoing_swaps) = cloned.persister.list_ongoing_swaps() else { + error!("Could not read ongoing swaps from database"); + continue; + }; for swap in ongoing_swaps { - match swap { - OngoingSwap::Receive { - id, - preimage, - redeem_script, - blinding_key, - .. - } => match cloned.try_claim(&preimage, &redeem_script, &blinding_key, None) { - Ok(_) => cloned - .persister - .resolve_ongoing_swap(&id) - .unwrap_or_else(|err| { - warn!("Could not write to database. Err: {err:?}") - }), - Err(err) => { - if let PaymentError::AlreadyClaimed = err { - warn!("Funds already claimed"); - cloned.persister.resolve_ongoing_swap(&id).unwrap() - } - warn!("Could not claim yet. Err: {err}"); - } - }, - OngoingSwap::Send { id, .. } => { - let Ok(status_response) = - client.swap_status(SwapStatusRequest { id: id.clone() }) - else { - continue; - }; - - if status_response.status == SubSwapStates::TransactionClaimed.to_string() { - cloned - .persister - .resolve_ongoing_swap(&id) - .unwrap_or_else(|err| { - warn!("Could not write to database. Err: {err:?}") - }); - } + Wallet::try_resolve_pending_swap(&cloned, &client, &swap).unwrap_or_else(|err| { + match swap { + OngoingSwap::Send { .. } => error!("[Ongoing Send] {err}"), + OngoingSwap::Receive { .. } => error!("[Ongoing Receive] {err}"), } - } + }) } }); @@ -173,7 +236,7 @@ impl Wallet { full_scan_with_electrum_client(&mut wallet, &mut electrum_client) } - fn address(&self) -> Result
{ + fn address(&self) -> Result { let wallet = self.wallet.lock().unwrap(); Ok(wallet.address(self.active_address)?.address().clone()) } @@ -217,29 +280,20 @@ impl Wallet { ) } - fn sign_and_send( + fn build_tx( &self, - signers: &[AnySigner], fee_rate: Option, - recipient: &str, + recipient_address: &str, amount_sat: u64, - ) -> Result { + ) -> Result { let wallet = self.wallet.lock().unwrap(); - let electrum_client = ElectrumClient::new(&self.electrum_url)?; - - let mut pset = wallet.send_lbtc(amount_sat, recipient, fee_rate)?; - - for signer in signers { - signer.sign(&mut pset)?; - } - - let tx = wallet.finalize(&mut pset)?; - let txid = electrum_client.broadcast(&tx)?; - - Ok(txid.to_string()) + let mut pset = wallet.send_lbtc(amount_sat, recipient_address, fee_rate)?; + let signer = AnySigner::Software(self.get_signer()); + signer.sign(&mut pset)?; + Ok(wallet.finalize(&mut pset)?) } - pub fn prepare_payment(&self, invoice: &str) -> Result { + pub fn prepare_send_payment(&self, invoice: &str) -> Result { let client = self.boltz_client(); let invoice = invoice .trim() @@ -250,16 +304,16 @@ impl Wallet { let lbtc_pair = client .get_pairs()? .get_lbtc_pair() - .ok_or(PaymentError::WalletError)?; + .ok_or(PaymentError::PairsNotFound)?; - let amount_sat = invoice + let payer_amount_sat = invoice .amount_milli_satoshis() .ok_or(PaymentError::AmountOutOfRange)? / 1000; lbtc_pair .limits - .within(amount_sat) + .within(payer_amount_sat) .map_err(|_| PaymentError::AmountOutOfRange)?; let swap_response = client.create_swap(CreateSwapRequest::new_lbtc_submarine( @@ -269,36 +323,52 @@ impl Wallet { ))?; let id = swap_response.get_id(); - let funding_amount = swap_response.get_funding_amount()?; let funding_address = swap_response.get_funding_address()?; + let receiver_amount_sat = swap_response.get_funding_amount()?; + let network_fees: u64 = self + .build_tx(None, &funding_address.to_string(), receiver_amount_sat)? + .all_fees() + .values() + .sum(); self.persister - .insert_ongoing_swap(&[OngoingSwap::Send { + .insert_or_update_ongoing_swap(&[OngoingSwap::Send { id: id.clone(), - amount_sat, funding_address: funding_address.clone(), invoice: invoice.to_string(), + receiver_amount_sat: receiver_amount_sat + network_fees, + txid: None, }]) .map_err(|_| PaymentError::PersistError)?; - Ok(PreparePaymentResponse { + Ok(PrepareSendResponse { id, funding_address, - funding_amount, + invoice: invoice.to_string(), + payer_amount_sat, + receiver_amount_sat, + total_fees: receiver_amount_sat + network_fees - payer_amount_sat, }) } pub fn send_payment( &self, - res: &PreparePaymentResponse, + res: &PrepareSendResponse, ) -> Result { - let signer = AnySigner::Software(self.get_signer()); + let tx = self.build_tx(None, &res.funding_address, res.receiver_amount_sat)?; - let txid = self - .sign_and_send(&[signer], None, &res.funding_address, res.funding_amount) - .map_err(|err| PaymentError::SendError { - err: err.to_string(), - })?; + let electrum_client = ElectrumClient::new(&self.electrum_url)?; + let txid = electrum_client.broadcast(&tx)?.to_string(); + + self.persister + .insert_or_update_ongoing_swap(&[OngoingSwap::Send { + id: res.id.clone(), + funding_address: res.funding_address.clone(), + invoice: res.invoice.clone(), + receiver_amount_sat: res.receiver_amount_sat + res.total_fees, + txid: Some(txid.clone()), + }]) + .map_err(|_| PaymentError::PersistError)?; Ok(SendPaymentResponse { txid }) } @@ -313,13 +383,13 @@ impl Wallet { let network_config = &self.get_network_config(); let rev_swap_tx = LBtcSwapTx::new_claim( LBtcSwapScript::reverse_from_str(redeem_script, blinding_key)?, - self.address() - .map_err(|_| PaymentError::WalletError)? - .to_string(), + self.address()?.to_string(), network_config, )?; - let mnemonic = self.signer.mnemonic().ok_or(PaymentError::WalletError)?; + let mnemonic = self.signer.mnemonic().ok_or(PaymentError::SignerError { + err: "Could not claim: Mnemonic not found".to_string(), + })?; let swap_key = SwapKey::from_reverse_account(&mnemonic.to_string(), "", self.network.into(), 0)?; @@ -335,42 +405,42 @@ impl Wallet { Ok(txid) } - pub fn receive_payment( + pub fn prepare_receive_payment( &self, - req: ReceivePaymentRequest, - ) -> Result { + req: &PrepareReceiveRequest, + ) -> Result { let client = self.boltz_client(); let lbtc_pair = client .get_pairs()? .get_lbtc_pair() - .ok_or(PaymentError::WalletError)?; + .ok_or(PaymentError::PairsNotFound)?; let (receiver_amount_sat, payer_amount_sat) = match (req.receiver_amount_sat, req.payer_amount_sat) { - (Some(onchain_amount_sat), None) => { + (Some(receiver_amount_sat), None) => { let fees_lockup = lbtc_pair.fees.reverse_lockup(); let fees_claim = CLAIM_ABSOLUTE_FEES; // lbtc_pair.fees.reverse_claim_estimate(); let p = lbtc_pair.fees.percentage; - let temp_recv_amt = onchain_amount_sat; + let temp_recv_amt = receiver_amount_sat; let invoice_amt_minus_service_fee = temp_recv_amt + fees_lockup + fees_claim; - let invoice_amount_sat = + let payer_amount_sat = (invoice_amt_minus_service_fee as f64 * 100.0 / (100.0 - p)).ceil() as u64; - Ok((onchain_amount_sat, invoice_amount_sat)) + Ok((receiver_amount_sat, payer_amount_sat)) } - (None, Some(invoice_amount_sat)) => { - let fees_boltz = lbtc_pair.fees.reverse_boltz(invoice_amount_sat); + (None, Some(payer_amount_sat)) => { + let fees_boltz = lbtc_pair.fees.reverse_boltz(payer_amount_sat); let fees_lockup = lbtc_pair.fees.reverse_lockup(); let fees_claim = CLAIM_ABSOLUTE_FEES; // lbtc_pair.fees.reverse_claim_estimate(); let fees_total = fees_boltz + fees_lockup + fees_claim; ensure_sdk!( - invoice_amount_sat > fees_total, + payer_amount_sat > fees_total, PaymentError::AmountOutOfRange ); - Ok((invoice_amount_sat - fees_total, invoice_amount_sat)) + Ok((payer_amount_sat - fees_total, payer_amount_sat)) } (None, None) => Err(PaymentError::AmountOutOfRange), @@ -380,14 +450,29 @@ impl Wallet { err: "Both invoice and onchain amounts were specified".into(), }), }?; - debug!("Creating reverse swap with: receiver_amount_sat {receiver_amount_sat} sat, payer_amount_sat {payer_amount_sat} sat"); lbtc_pair .limits .within(payer_amount_sat) .map_err(|_| PaymentError::AmountOutOfRange)?; - let mnemonic = self.signer.mnemonic().ok_or(PaymentError::WalletError)?; + debug!("Creating reverse swap with: receiver_amount_sat {receiver_amount_sat} sat, payer_amount_sat {payer_amount_sat} sat"); + + Ok(PrepareReceiveResponse { + pair_hash: lbtc_pair.hash, + payer_amount_sat, + fees_sat: payer_amount_sat - receiver_amount_sat, + }) + } + + pub fn receive_payment( + &self, + res: &PrepareReceiveResponse, + ) -> Result { + let client = self.boltz_client(); + let mnemonic = self.signer.mnemonic().ok_or(PaymentError::SignerError { + err: "Could not claim: Mnemonic not found".to_string(), + })?; let swap_key = SwapKey::from_reverse_account(&mnemonic.to_string(), "", self.network.into(), 0)?; let lsk = LiquidSwapKey::try_from(swap_key)?; @@ -396,26 +481,21 @@ impl Wallet { let preimage_str = preimage.to_string().ok_or(PaymentError::InvalidPreimage)?; let preimage_hash = preimage.sha256.to_string(); - let swap_response = if req.receiver_amount_sat.is_some() { - client.create_swap(CreateSwapRequest::new_lbtc_reverse_onchain_amt( - lbtc_pair.hash, - preimage_hash.clone(), - lsk.keypair.public_key().to_string(), - receiver_amount_sat, - ))? - } else { - client.create_swap(CreateSwapRequest::new_lbtc_reverse_invoice_amt( - lbtc_pair.hash, - preimage_hash.clone(), - lsk.keypair.public_key().to_string(), - payer_amount_sat, - ))? - }; + let swap_response = client.create_swap(CreateSwapRequest::new_lbtc_reverse_invoice_amt( + res.pair_hash.clone(), + preimage_hash.clone(), + lsk.keypair.public_key().to_string(), + res.payer_amount_sat, + ))?; let swap_id = swap_response.get_id(); let invoice = swap_response.get_invoice()?; let blinding_str = swap_response.get_blinding_key()?; let redeem_script = swap_response.get_redeem_script()?; + let payer_amount_sat = invoice + .amount_milli_satoshis() + .ok_or(PaymentError::InvalidInvoice)? + / 1000; // Double check that the generated invoice includes our data // https://docs.boltz.exchange/v/api/dont-trust-verify#lightning-invoice-verification @@ -424,13 +504,13 @@ impl Wallet { }; self.persister - .insert_ongoing_swap(dbg!(&[OngoingSwap::Receive { + .insert_or_update_ongoing_swap(dbg!(&[OngoingSwap::Receive { id: swap_id.clone(), preimage: preimage_str, blinding_key: blinding_str, redeem_script, invoice: invoice.to_string(), - receiver_amount_sat, + receiver_amount_sat: payer_amount_sat - res.fees_sat, }])) .map_err(|_| PaymentError::PersistError)?; @@ -447,13 +527,16 @@ impl Wallet { let transactions = self.wallet.lock().unwrap().transactions()?; + let payment_data = self.persister.get_payment_data()?; let mut payments: Vec = transactions .iter() .map(|tx| { + let id = tx.txid.to_string(); + let data = payment_data.get(&id); let amount_sat = tx.balance.values().sum::(); Payment { - id: Some(tx.tx.txid().to_string()), + id: Some(id.clone()), timestamp: tx.timestamp, amount_sat: amount_sat.unsigned_abs(), payment_type: match amount_sat >= 0 { @@ -461,6 +544,8 @@ impl Wallet { false => PaymentType::Sent, }, invoice: None, + fees_sat: data + .map(|d| (amount_sat.abs() - d.payer_amount_sat as i64).unsigned_abs()), } }) .collect();