mirror of
https://github.com/aljazceru/breez-sdk-liquid.git
synced 2026-01-04 14:54:21 +01:00
feat: add fee persistence to payments (#83)
This commit is contained in:
@@ -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()?;
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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(())
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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;",
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user