From 85e8a6d1a36d1541540290a9841e299d72eabe24 Mon Sep 17 00:00:00 2001 From: yse <70684173+hydra-yse@users.noreply.github.com> Date: Fri, 12 Apr 2024 15:51:16 +0200 Subject: [PATCH] feat: persist invoice rather than amount (#66) Co-authored-by: ok300 <106775972+ok300@users.noreply.github.com> --- cli/src/commands.rs | 28 +++++++++-- lib/src/lib.rs | 1 + lib/src/model.rs | 36 ++++++++++++--- lib/src/persist/migrations.rs | 3 +- lib/src/persist/mod.rs | 38 +++++++++------ lib/src/wallet.rs | 87 ++++++++++++++++++++++------------- 6 files changed, 135 insertions(+), 58 deletions(-) diff --git a/cli/src/commands.rs b/cli/src/commands.rs index b91777c..ff80fa5 100644 --- a/cli/src/commands.rs +++ b/cli/src/commands.rs @@ -1,5 +1,7 @@ use std::borrow::Cow::{self, Owned}; use std::sync::Arc; +use std::thread; +use std::time::Duration; use anyhow::Result; use clap::{arg, Parser}; @@ -15,7 +17,13 @@ use serde_json::to_string_pretty; #[derive(Parser, Debug, Clone, PartialEq)] pub(crate) enum Command { /// Send lbtc and receive btc through a swap - SendPayment { bolt11: String }, + SendPayment { + bolt11: String, + + /// Delay for the send, in seconds + #[arg(short, long)] + delay: Option, + }, /// Receive lbtc and send btc through a swap ReceivePayment { #[arg(short, long)] @@ -76,10 +84,22 @@ pub(crate) fn handle_command( qr2term::print_qr(response.invoice.clone())?; command_result!(response) } - Command::SendPayment { bolt11 } => { + Command::SendPayment { bolt11, delay } => { let prepare_response = wallet.prepare_payment(&bolt11)?; - let response = wallet.send_payment(&prepare_response)?; - command_result!(response) + + if let Some(delay) = delay { + let wallet_cloned = wallet.clone(); + let prepare_cloned = prepare_response.clone(); + + thread::spawn(move || { + thread::sleep(Duration::from_secs(delay)); + wallet_cloned.send_payment(&prepare_cloned).unwrap(); + }); + command_result!(prepare_response) + } else { + let response = wallet.send_payment(&prepare_response)?; + command_result!(response) + } } Command::GetInfo => { command_result!(wallet.get_info(true)?) diff --git a/lib/src/lib.rs b/lib/src/lib.rs index b021597..cf74b6f 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -8,6 +8,7 @@ pub use wallet::*; // To avoid sendrawtransaction error "min relay fee not met" const CLAIM_ABSOLUTE_FEES: u64 = 134; const DEFAULT_DATA_DIR: &str = ".data"; +const MAIN_DB_FILE: &str = "storage.sql"; #[macro_export] macro_rules! ensure_sdk { diff --git a/lib/src/model.rs b/lib/src/model.rs index b3975a1..90ce620 100644 --- a/lib/src/model.rs +++ b/lib/src/model.rs @@ -69,7 +69,7 @@ pub struct ReceivePaymentResponse { pub invoice: String, } -#[derive(Debug)] +#[derive(Debug, Clone, Serialize)] pub struct PreparePaymentResponse { pub id: String, pub funding_amount: u64, @@ -101,14 +101,26 @@ pub enum PaymentError { #[error("The generated preimage is not valid")] InvalidPreimage, + #[error("The specified funds have already been claimed")] + AlreadyClaimed, + #[error("Generic boltz error: {err}")] BoltzGeneric { err: String }, } impl From for PaymentError { fn from(err: Error) -> Self { - PaymentError::BoltzGeneric { - err: format!("{err:?}"), + match err { + Error::Protocol(msg) => { + if msg == "Could not find utxos for script" { + return PaymentError::AlreadyClaimed; + } + + PaymentError::BoltzGeneric { err: msg } + } + _ => PaymentError::BoltzGeneric { + err: format!("{err:?}"), + }, } } } @@ -126,13 +138,14 @@ pub(crate) enum OngoingSwap { id: String, amount_sat: u64, funding_address: String, + invoice: String, }, Receive { id: String, preimage: String, redeem_script: String, blinding_key: String, - invoice_amount_sat: u64, + invoice: String, onchain_amount_sat: u64, }, } @@ -152,24 +165,35 @@ pub struct Payment { pub amount_sat: u64, #[serde(rename(serialize = "type"))] pub payment_type: PaymentType, + + /// Only for [PaymentType::PendingReceive] + pub invoice: Option, } impl From for Payment { fn from(swap: OngoingSwap) -> Self { match swap { - OngoingSwap::Send { amount_sat, .. } => Payment { + OngoingSwap::Send { + amount_sat, + invoice, + .. + } => Payment { id: None, timestamp: None, payment_type: PaymentType::PendingSend, amount_sat, + invoice: Some(invoice), }, OngoingSwap::Receive { - onchain_amount_sat, .. + onchain_amount_sat, + invoice, + .. } => Payment { id: None, timestamp: None, payment_type: PaymentType::PendingReceive, amount_sat: onchain_amount_sat, + invoice: Some(invoice), }, } } diff --git a/lib/src/persist/migrations.rs b/lib/src/persist/migrations.rs index a64d3f6..a9a6724 100644 --- a/lib/src/persist/migrations.rs +++ b/lib/src/persist/migrations.rs @@ -5,7 +5,7 @@ pub(crate) fn current_migrations() -> Vec<&'static str> { preimage TEXT NOT NULL, redeem_script TEXT NOT NULL, blinding_key TEXT NOT NULL, - invoice_amount_sat INTEGER NOT NULL, + invoice TEXT NOT NULL, onchain_amount_sat INTEGER NOT NULL, created_at TEXT DEFAULT CURRENT_TIMESTAMP ) STRICT;", @@ -13,6 +13,7 @@ pub(crate) fn current_migrations() -> Vec<&'static str> { id TEXT NOT NULL PRIMARY KEY, amount_sat INTEGER NOT NULL, funding_address TEXT NOT NULL, + invoice TEXT NOT NULL, created_at TEXT DEFAULT CURRENT_TIMESTAMP ) STRICT;", ] diff --git a/lib/src/persist/mod.rs b/lib/src/persist/mod.rs index 019b242..fafa2a6 100644 --- a/lib/src/persist/mod.rs +++ b/lib/src/persist/mod.rs @@ -1,26 +1,30 @@ mod migrations; +use std::{fs::create_dir_all, path::PathBuf, str::FromStr}; + use anyhow::Result; use rusqlite::{params, Connection}; use rusqlite_migration::{Migrations, M}; -use crate::OngoingSwap; +use crate::{OngoingSwap, MAIN_DB_FILE}; use migrations::current_migrations; pub(crate) struct Persister { - main_db_file: String, + pub(crate) main_db_dir: PathBuf, } impl Persister { - pub fn new(working_dir: &str) -> Self { - let main_db_file = format!("{}/storage.sql", working_dir); - Persister { main_db_file } + pub fn new(working_dir: &str) -> Result { + let main_db_dir = PathBuf::from_str(working_dir)?; + if !main_db_dir.exists() { + create_dir_all(&main_db_dir)?; + } + Ok(Persister { main_db_dir }) } pub(crate) fn get_connection(&self) -> Result { - let con = Connection::open(self.main_db_file.clone())?; - Ok(con) + Ok(Connection::open(self.main_db_dir.join(MAIN_DB_FILE))?) } pub fn init(&self) -> Result<()> { @@ -44,26 +48,28 @@ impl Persister { id, funding_address, amount_sat, + invoice, } => { let mut stmt = con.prepare( " INSERT INTO ongoing_send_swaps ( id, amount_sat, - funding_address + funding_address, + invoice ) - VALUES (?, ?, ?) + VALUES (?, ?, ?, ?) ", )?; - _ = stmt.execute((&id, &amount_sat, &funding_address))? + _ = stmt.execute((&id, &amount_sat, &funding_address, invoice))? } OngoingSwap::Receive { id, preimage, redeem_script, blinding_key, - invoice_amount_sat, + invoice, onchain_amount_sat, } => { let mut stmt = con.prepare( @@ -73,7 +79,7 @@ impl Persister { preimage, redeem_script, blinding_key, - invoice_amount_sat, + invoice, onchain_amount_sat ) VALUES (?, ?, ?, ?, ?, ?) @@ -85,7 +91,7 @@ impl Persister { &preimage, &redeem_script, &blinding_key, - &invoice_amount_sat, + &invoice, &onchain_amount_sat, ))? } @@ -123,6 +129,7 @@ impl Persister { id, amount_sat, funding_address, + invoice, created_at FROM ongoing_send_swaps ORDER BY created_at @@ -135,6 +142,7 @@ impl Persister { id: row.get(0)?, amount_sat: row.get(1)?, funding_address: row.get(2)?, + invoice: row.get(3)?, }) })? .map(|i| i.unwrap()) @@ -151,7 +159,7 @@ impl Persister { preimage, redeem_script, blinding_key, - invoice_amount_sat, + invoice, onchain_amount_sat, created_at FROM ongoing_receive_swaps @@ -166,7 +174,7 @@ impl Persister { preimage: row.get(1)?, redeem_script: row.get(2)?, blinding_key: row.get(3)?, - invoice_amount_sat: row.get(4)?, + invoice: row.get(4)?, onchain_amount_sat: row.get(5)?, }) })? diff --git a/lib/src/wallet.rs b/lib/src/wallet.rs index f824ea5..bc770f9 100644 --- a/lib/src/wallet.rs +++ b/lib/src/wallet.rs @@ -10,7 +10,10 @@ use anyhow::{anyhow, Result}; use boltz_client::{ network::electrum::ElectrumConfig, swaps::{ - boltz::{BoltzApiClient, CreateSwapRequest, BOLTZ_MAINNET_URL, BOLTZ_TESTNET_URL}, + boltz::{ + BoltzApiClient, CreateSwapRequest, SubSwapStates, SwapStatusRequest, BOLTZ_MAINNET_URL, + BOLTZ_TESTNET_URL, + }, liquid::{LBtcSwapScript, LBtcSwapTx}, }, util::secrets::{LBtcReverseRecovery, LiquidSwapKey, Preimage, SwapKey}, @@ -46,7 +49,7 @@ pub struct Wallet { network: Network, wallet: Arc>, active_address: Option, - swap_persister: Persister, + persister: Persister, data_dir_path: String, } @@ -80,8 +83,8 @@ impl Wallet { fs::create_dir_all(&data_dir_path)?; - let swap_persister = Persister::new(&data_dir_path); - swap_persister.init()?; + let persister = Persister::new(&data_dir_path)?; + persister.init()?; let wallet = Arc::new(Wallet { wallet, @@ -89,11 +92,11 @@ impl Wallet { electrum_url, signer: opts.signer, active_address: None, - swap_persister, + persister, data_dir_path, }); - Wallet::track_claims(&wallet)?; + Wallet::track_pending_swaps(&wallet)?; Ok(wallet) } @@ -110,25 +113,52 @@ impl Wallet { Ok(descriptor_str.parse()?) } - fn track_claims(self: &Arc) -> Result<()> { + 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.swap_persister.list_ongoing_swaps().unwrap(); + let ongoing_swaps = cloned.persister.list_ongoing_swaps().unwrap(); for swap in ongoing_swaps { - if let OngoingSwap::Receive { - id, - preimage, - redeem_script, - blinding_key, - .. - } = swap - { - match cloned.try_claim(&preimage, &redeem_script, &blinding_key, None) { - Ok(_) => cloned.swap_persister.resolve_ongoing_swap(&id).unwrap(), - Err(e) => warn!("Could not claim yet. Err: {e}"), + 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:?}") + }); + } } } } @@ -242,11 +272,12 @@ impl Wallet { let funding_amount = swap_response.get_funding_amount()?; let funding_address = swap_response.get_funding_address()?; - self.swap_persister + self.persister .insert_ongoing_swap(&[OngoingSwap::Send { id: id.clone(), amount_sat, funding_address: funding_address.clone(), + invoice: invoice.to_string(), }]) .map_err(|_| PaymentError::PersistError)?; @@ -269,10 +300,6 @@ impl Wallet { err: err.to_string(), })?; - self.swap_persister - .resolve_ongoing_swap(&res.id) - .map_err(|_| PaymentError::PersistError)?; - Ok(SendPaymentResponse { txid }) } @@ -396,18 +423,13 @@ impl Wallet { return Err(PaymentError::InvalidInvoice); }; - let invoice_amount_sat = invoice - .amount_milli_satoshis() - .ok_or(PaymentError::InvalidInvoice)? - / 1000; - - self.swap_persister + self.persister .insert_ongoing_swap(dbg!(&[OngoingSwap::Receive { id: swap_id.clone(), preimage: preimage_str, blinding_key: blinding_str, redeem_script, - invoice_amount_sat, + invoice: invoice.to_string(), onchain_amount_sat, }])) .map_err(|_| PaymentError::PersistError)?; @@ -438,12 +460,13 @@ impl Wallet { true => PaymentType::Received, false => PaymentType::Sent, }, + invoice: None, } }) .collect(); if include_pending { - for swap in self.swap_persister.list_ongoing_swaps()? { + for swap in self.persister.list_ongoing_swaps()? { payments.insert(0, swap.into()); } }