From c31f6b3621216e2b52edeea231b310428a9e14fa Mon Sep 17 00:00:00 2001 From: Richard Holzeis Date: Fri, 30 Jun 2023 11:20:00 +0200 Subject: [PATCH] feat: Add channel fee payment subscriber --- CHANGELOG.md | 2 + crates/tests-e2e/tests/basic.rs | 3 +- mobile/native/Cargo.toml | 4 +- mobile/native/src/api.rs | 2 + mobile/native/src/channel_fee.rs | 157 ++++++++++++++++++ mobile/native/src/lib.rs | 1 + .../native/src/ln_dlc/lightning_subscriber.rs | 25 ++- mobile/native/src/ln_dlc/mod.rs | 24 +++ 8 files changed, 199 insertions(+), 19 deletions(-) create mode 100644 mobile/native/src/channel_fee.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index a10c595e1..b0102d555 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +- Charge funding transaction on-chain fees upon receiving and inbound JIT Channel + ## [1.0.21] - 2023-07-02 - Fix issue where `Next` button on the create invoice screen was hidden behind keyboard. The keyboard can now be closed by tapping outside the text-field. diff --git a/crates/tests-e2e/tests/basic.rs b/crates/tests-e2e/tests/basic.rs index 3b6329bf3..379b9e860 100644 --- a/crates/tests-e2e/tests/basic.rs +++ b/crates/tests-e2e/tests/basic.rs @@ -26,12 +26,13 @@ async fn app_can_be_funded_with_lnd_faucet() -> Result<()> { assert_eq!(app.rx.wallet_info().unwrap().balances.lightning, 0); let funding_amount = 50_000; + let funding_transaction_fees = 153; fund_app_with_faucet(&client, funding_amount).await?; assert_eq!(app.rx.wallet_info().unwrap().balances.on_chain, 0); assert_eq!( app.rx.wallet_info().unwrap().balances.lightning, - funding_amount + funding_amount - funding_transaction_fees ); Ok(()) } diff --git a/mobile/native/Cargo.toml b/mobile/native/Cargo.toml index 5975adcd8..924a83d70 100644 --- a/mobile/native/Cargo.toml +++ b/mobile/native/Cargo.toml @@ -10,6 +10,7 @@ crate-type = ["rlib", "cdylib", "staticlib"] anyhow = "1" base64 = "0.21.0" bdk = { version = "0.27.0", default-features = false, features = ["key-value-db", "use-esplora-blocking"] } +bitcoin = "0.29" coordinator-commons = { path = "../../crates/coordinator-commons" } diesel = { version = "2.0.0", features = ["sqlite", "r2d2", "extras"] } diesel_migrations = "2.0.0" @@ -39,6 +40,3 @@ tracing-subscriber = { version = "0.3", default-features = false, features = ["f trade = { path = "../../crates/trade" } url = "2.3.1" uuid = { version = "1.3.0", features = ["v4", "fast-rng", "macro-diagnostics"] } - -[dev-dependencies] -bitcoin = "0.29" diff --git a/mobile/native/src/api.rs b/mobile/native/src/api.rs index 795a832a1..be33ee3e1 100644 --- a/mobile/native/src/api.rs +++ b/mobile/native/src/api.rs @@ -1,4 +1,5 @@ use crate::calculations; +use crate::channel_fee::ChannelFeePaymentSubscriber; use crate::commons::api::Price; use crate::config; use crate::config::api::Config; @@ -196,6 +197,7 @@ pub fn run(config: Config, app_dir: String, seed_dir: String) -> Result<()> { db::init_db(&app_dir, get_network())?; let runtime = ln_dlc::get_or_create_tokio_runtime()?; ln_dlc::run(app_dir, seed_dir, runtime)?; + event::subscribe(ChannelFeePaymentSubscriber::new()); orderbook::subscribe(ln_dlc::get_node_key(), runtime) } diff --git a/mobile/native/src/channel_fee.rs b/mobile/native/src/channel_fee.rs new file mode 100644 index 000000000..3860f8b60 --- /dev/null +++ b/mobile/native/src/channel_fee.rs @@ -0,0 +1,157 @@ +use crate::commons::reqwest_client; +use crate::config; +use crate::event::subscriber::Subscriber; +use crate::event::EventInternal; +use crate::event::EventType; +use crate::ln_dlc; +use anyhow::anyhow; +use anyhow::Context; +use anyhow::Result; +use bitcoin::Txid; +use lightning_invoice::Invoice; +use ln_dlc_node::node::rust_dlc_manager::ChannelId; +use serde::Deserialize; +use serde::Serialize; +use std::str::FromStr; +use std::sync::Arc; +use std::sync::Mutex; +use tokio::runtime::Handle; + +#[derive(Clone)] +pub struct ChannelFeePaymentSubscriber { + pub open_channel_tx: Arc>>, +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +pub struct EsploraTransaction { + pub txid: String, + pub fee: u32, +} + +impl Subscriber for ChannelFeePaymentSubscriber { + fn notify(&self, event: &EventInternal) { + let result = match event { + EventInternal::ChannelReady(channel_id) => { + self.register_funding_transaction(channel_id) + } + EventInternal::PaymentClaimed(amount_msats) => { + self.pay_funding_transaction_fees(*amount_msats) + } + _ => Ok(()), + }; + + if let Err(e) = result { + tracing::error!("{e:#}"); + } + } + + fn events(&self) -> Vec { + vec![EventType::ChannelReady, EventType::PaymentClaimed] + } +} + +impl ChannelFeePaymentSubscriber { + pub fn new() -> Self { + Self { + open_channel_tx: Arc::new(Mutex::new(None)), + } + } + + /// Attempts to pay the transaction fees for opening an inbound channel. + fn pay_funding_transaction_fees(&self, amount_msats: u64) -> Result<()> { + let transaction = match self.get_funding_transaction() { + Some(transaction) => transaction, + None => { + tracing::debug!("No pending funding transaction found!"); + return Ok(()); + } + }; + + tracing::debug!("Trying to pay channel opening fees of {}", transaction.fee); + let funding_tx_fees_msats = (transaction.fee * 1000) as u64; + + if funding_tx_fees_msats > amount_msats { + tracing::warn!("Trying to pay fees with an amount smaller than the fees!") + } + + let invoice_str = tokio::task::block_in_place(|| { + Handle::current().block_on(fetch_funding_transaction_fee_invoice(transaction.fee)) + })?; + + let invoice = Invoice::from_str(&invoice_str).context("Could not parse Invoice string")?; + let _payment_hash = invoice.payment_hash(); + + match ln_dlc::send_payment(&invoice_str) { + Ok(_) => { + // unset the funding transaction marking it as being paid. + self.unset_funding_transaction(); + tracing::info!("Successfully triggered funding transaction fees payment of {funding_tx_fees_msats} msats to {}", config::get_coordinator_info().pubkey); + } + Err(e) => { + tracing::error!("Failed to pay funding transaction fees of {funding_tx_fees_msats} msats to {}. Error: {e:#}", config::get_coordinator_info().pubkey); + } + }; + + Ok(()) + } + + /// Register jit channel opening transaction for fee payment + fn register_funding_transaction(&self, channel_id: &ChannelId) -> Result<()> { + let channel_id_as_str = hex::encode(channel_id); + tracing::debug!("Received new inbound channel with id {channel_id_as_str}"); + + let txid = ln_dlc::get_funding_transaction(channel_id)?; + + let transaction: EsploraTransaction = tokio::task::block_in_place(|| { + Handle::current().block_on(fetch_funding_transaction(txid)) + })?; + tracing::debug!("Successfully fetched transaction fees of {} for new inbound channel with id {channel_id_as_str}", transaction.fee); + self.set_funding_transaction(transaction); + Ok(()) + } + + fn set_funding_transaction(&self, transaction: EsploraTransaction) { + *self + .open_channel_tx + .lock() + .expect("Mutex to not be poisoned") = Some(transaction); + } + + fn unset_funding_transaction(&self) { + *self + .open_channel_tx + .lock() + .expect("Mutex to not be poisoned") = None; + } + + fn get_funding_transaction(&self) -> Option { + self.open_channel_tx + .lock() + .expect("Mutex to not be poisoned") + .clone() + } +} + +async fn fetch_funding_transaction(txid: Txid) -> Result { + reqwest_client() + .get(format!("{}tx/{txid}", config::get_esplora_endpoint())) + .send() + .await? + .json() + .await + .map_err(|e| anyhow!("Failed to fetch transaction: {txid} from esplora. Error: {e:?}")) +} + +async fn fetch_funding_transaction_fee_invoice(funding_tx_fee: u32) -> Result { + reqwest_client() + .get(format!( + "http://{}/api/invoice?amount={}", + config::get_http_endpoint(), + funding_tx_fee + )) + .send() + .await? + .text() + .await + .map_err(|e| anyhow!("Failed to fetch invoice from coordinator. Error:{e:?}")) +} diff --git a/mobile/native/src/lib.rs b/mobile/native/src/lib.rs index 1ec93076f..d394adaaf 100644 --- a/mobile/native/src/lib.rs +++ b/mobile/native/src/lib.rs @@ -7,6 +7,7 @@ pub mod schema; pub mod api; pub mod calculations; +mod channel_fee; pub mod commons; pub mod config; pub mod event; diff --git a/mobile/native/src/ln_dlc/lightning_subscriber.rs b/mobile/native/src/ln_dlc/lightning_subscriber.rs index 7c4eee556..35fbee7aa 100644 --- a/mobile/native/src/ln_dlc/lightning_subscriber.rs +++ b/mobile/native/src/ln_dlc/lightning_subscriber.rs @@ -7,28 +7,23 @@ use tokio::sync::watch::Receiver; impl Node { pub async fn listen_for_lightning_events(&self, mut event_receiver: Receiver>) { loop { - let event = match event_receiver.changed().await { + match event_receiver.changed().await { Ok(()) => { if let Some(event) = event_receiver.borrow().clone() { - event - } else { - continue; + match event { + Event::ChannelReady { channel_id, .. } => { + event::publish(&EventInternal::ChannelReady(channel_id)) + } + Event::PaymentClaimed { amount_msat, .. } => { + event::publish(&EventInternal::PaymentClaimed(amount_msat)) + } + _ => tracing::trace!("Ignoring event on the mobile app"), + } } } Err(e) => { tracing::error!("Failed to receive event: {e:#}"); - continue; } - }; - - match event { - Event::ChannelReady { channel_id, .. } => { - event::publish(&EventInternal::ChannelReady(channel_id)) - } - Event::PaymentClaimed { amount_msat, .. } => { - event::publish(&EventInternal::PaymentClaimed(amount_msat)) - } - _ => tracing::debug!("Ignoring event on the mobile app"), } } } diff --git a/mobile/native/src/ln_dlc/mod.rs b/mobile/native/src/ln_dlc/mod.rs index 5674b648a..39b63c4c5 100644 --- a/mobile/native/src/ln_dlc/mod.rs +++ b/mobile/native/src/ln_dlc/mod.rs @@ -17,6 +17,7 @@ use anyhow::Result; use bdk::bitcoin::secp256k1::rand::thread_rng; use bdk::bitcoin::secp256k1::rand::RngCore; use bdk::bitcoin::secp256k1::SecretKey; +use bdk::bitcoin::Txid; use bdk::bitcoin::XOnlyPublicKey; use bdk::BlockTime; use coordinator_commons::TradeParams; @@ -24,6 +25,8 @@ use itertools::chain; use itertools::Itertools; use lightning::util::events::Event; use lightning_invoice::Invoice; +use ln_dlc_node::node::rust_dlc_manager::subchannel::LNChannelManager; +use ln_dlc_node::node::rust_dlc_manager::ChannelId; use ln_dlc_node::node::LnDlcNodeSettings; use ln_dlc_node::node::NodeInfo; use ln_dlc_node::seed::Bip39Seed; @@ -82,6 +85,27 @@ pub fn get_oracle_pubkey() -> XOnlyPublicKey { NODE.get().inner.oracle_pk() } +pub fn get_funding_transaction(channel_id: &ChannelId) -> Result { + let node = NODE.get(); + let channel_details = node.inner.channel_manager.get_channel_details(channel_id); + + let funding_transaction = match channel_details { + Some(channel_details) => match channel_details.funding_txo { + Some(funding_txo) => funding_txo.txid, + None => bail!( + "Could not find funding transaction for channel {}", + hex::encode(channel_id) + ), + }, + None => bail!( + "Could not find channel details for {}", + hex::encode(channel_id) + ), + }; + + Ok(funding_transaction) +} + /// Lazily creates a multi threaded runtime with the the number of worker threads corresponding to /// the number of available cores. pub fn get_or_create_tokio_runtime() -> Result<&'static Runtime> {