feat: add fee persistence to payments (#83)

This commit is contained in:
yse
2024-04-18 11:24:04 +02:00
committed by GitHub
parent 7443a8717a
commit 2929d668cf
8 changed files with 452 additions and 192 deletions

View File

@@ -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<CliHelper, DefaultHistory>,
wallet: &Arc<Wallet>,
@@ -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()?;

View File

@@ -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<PrepareSendResponse, PaymentError> {
self.ln_sdk.prepare_send_payment(&invoice)
}
pub fn send_payment(
&self,
req: PreparePaymentResponse,
req: PrepareSendResponse,
) -> Result<SendPaymentResponse, PaymentError> {
self.ln_sdk.send_payment(&req)
}
pub fn prepare_receive_payment(
&self,
req: PrepareReceiveRequest,
) -> Result<PrepareReceiveResponse, PaymentError> {
self.ln_sdk.prepare_receive_payment(&req)
}
pub fn receive_payment(
&self,
req: ReceivePaymentRequest,
req: PrepareReceiveResponse,
) -> Result<ReceivePaymentResponse, PaymentError> {
self.ln_sdk.receive_payment(req)
self.ln_sdk.receive_payment(&req)
}
}

View File

@@ -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);
};
SendPaymentResponse send_payment(PrepareSendResponse req);
[Throws=PaymentError]
PrepareReceiveResponse prepare_receive_payment(PrepareReceiveRequest req);
[Throws=PaymentError]
ReceivePaymentResponse receive_payment(PrepareReceiveResponse req);
};

View File

@@ -19,12 +19,24 @@ macro_rules! ensure_sdk {
};
}
#[macro_export]
macro_rules! get_invoice_amount {
($invoice:expr) => {
$invoice
.parse::<Bolt11Invoice>()
.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(())

View File

@@ -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<u64>,
pub receiver_amount_sat: Option<u64>,
}
#[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<Error> for PaymentError {
fn from(err: Error) -> Self {
impl From<boltz_client::error::Error> 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<Error> for PaymentError {
}
}
#[allow(clippy::match_single_binding)]
impl From<lwk_wollet::Error> for PaymentError {
fn from(err: lwk_wollet::Error) -> Self {
match err {
_ => PaymentError::LwkError {
err: format!("{err:?}"),
},
}
}
}
#[allow(clippy::match_single_binding)]
impl From<lwk_signer::SignerError> 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<String>,
},
Receive {
id: String,
@@ -163,6 +204,7 @@ pub struct Payment {
pub id: Option<String>,
pub timestamp: Option<u32>,
pub amount_sat: u64,
pub fees_sat: Option<u64>,
#[serde(rename(serialize = "type"))]
pub payment_type: PaymentType,
@@ -174,27 +216,39 @@ impl From<OngoingSwap> 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,
}

View File

@@ -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;",
]
}

View File

@@ -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<HashMap<String, PaymentData>> {
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)
}
}

View File

@@ -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<Wallet>,
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::<SubSwapStates>()
.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<Wallet>) -> 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<Address> {
fn address(&self) -> Result<Address, lwk_wollet::Error> {
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<f32>,
recipient: &str,
recipient_address: &str,
amount_sat: u64,
) -> Result<String> {
) -> Result<Transaction, PaymentError> {
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<PreparePaymentResponse, PaymentError> {
pub fn prepare_send_payment(&self, invoice: &str) -> Result<PrepareSendResponse, PaymentError> {
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<SendPaymentResponse, PaymentError> {
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<ReceivePaymentResponse, PaymentError> {
req: &PrepareReceiveRequest,
) -> Result<PrepareReceiveResponse, PaymentError> {
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<ReceivePaymentResponse, PaymentError> {
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<Payment> = transactions
.iter()
.map(|tx| {
let id = tx.txid.to_string();
let data = payment_data.get(&id);
let amount_sat = tx.balance.values().sum::<i64>();
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();