From d574a3ad418903a901ef44dd4fcc51676a688947 Mon Sep 17 00:00:00 2001 From: Richard Holzeis Date: Thu, 26 Oct 2023 10:53:49 +0200 Subject: [PATCH] feat: Charge channel opening fee through the lsp flow --- CHANGELOG.md | 1 + .../down.sql | 8 + .../up.sql | 6 + coordinator/src/db/channels.rs | 22 +- coordinator/src/db/payments.rs | 1 + coordinator/src/node.rs | 1 - coordinator/src/node/channel_opening_fee.rs | 66 ----- coordinator/src/node/storage.rs | 8 + coordinator/src/routes.rs | 23 +- coordinator/src/schema.rs | 2 +- crates/ln-dlc-node/src/channel.rs | 11 +- crates/ln-dlc-node/src/ldk_node_wallet.rs | 5 + crates/ln-dlc-node/src/lib.rs | 5 + .../ln-dlc-node/src/ln/app_event_handler.rs | 75 +++++- crates/ln-dlc-node/src/ln/common_handlers.rs | 9 +- .../src/ln/coordinator_event_handler.rs | 2 + crates/ln-dlc-node/src/node/invoice.rs | 2 + crates/ln-dlc-node/src/node/mod.rs | 1 + crates/ln-dlc-node/src/node/storage.rs | 21 ++ crates/ln-dlc-node/src/node/wallet.rs | 7 +- .../just_in_time_channel/channel_close.rs | 15 +- .../src/tests/just_in_time_channel/create.rs | 19 +- .../fail_intercepted_htlc.rs | 3 + .../tests/just_in_time_channel/payments.rs | 2 +- crates/tests-e2e/src/fund.rs | 39 +-- .../receive_payment_when_open_position.rs | 2 +- maker/src/ln/event_handler.rs | 2 + .../wallet/application/wallet_service.dart | 7 +- .../wallet/domain/wallet_history.dart | 65 +---- .../wallet/onboarding/liquidity_card.dart | 2 +- .../features/wallet/wallet_history_item.dart | 66 +---- .../down.sql | 5 + .../up.sql | 5 + mobile/native/src/api.rs | 17 +- mobile/native/src/channel_fee.rs | 232 ------------------ mobile/native/src/db/mod.rs | 18 ++ mobile/native/src/db/models.rs | 46 +++- mobile/native/src/lib.rs | 1 - mobile/native/src/ln_dlc/mod.rs | 45 ++-- mobile/native/src/ln_dlc/node.rs | 8 + mobile/native/src/schema.rs | 3 + 41 files changed, 329 insertions(+), 549 deletions(-) create mode 100644 coordinator/migrations/2023-10-25-120015_add_fee_to_channel/down.sql create mode 100644 coordinator/migrations/2023-10-25-120015_add_fee_to_channel/up.sql delete mode 100644 coordinator/src/node/channel_opening_fee.rs create mode 100644 mobile/native/migrations/2023-10-25-120015_add_fee_to_channel/down.sql create mode 100644 mobile/native/migrations/2023-10-25-120015_add_fee_to_channel/up.sql delete mode 100644 mobile/native/src/channel_fee.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 21cb488ed..a734a0b9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Improved settings screen. - BREAKING CHANGE: Upgrade `rust-lightning` to version 0.0.116. Positions opened prior to this version will not be loaded from storage. +- Charge channel opening fee through the lsp flow ## [1.4.3] - 2023-10-23 diff --git a/coordinator/migrations/2023-10-25-120015_add_fee_to_channel/down.sql b/coordinator/migrations/2023-10-25-120015_add_fee_to_channel/down.sql new file mode 100644 index 000000000..7a2761f89 --- /dev/null +++ b/coordinator/migrations/2023-10-25-120015_add_fee_to_channel/down.sql @@ -0,0 +1,8 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE channels DROP COLUMN "fee_sats"; + +ALTER TABLE + channels + ADD + COLUMN open_channel_fee_payment_hash TEXT REFERENCES payments(payment_hash); +CREATE INDEX IF NOT EXISTS channels_funding_txid ON channels(funding_txid); \ No newline at end of file diff --git a/coordinator/migrations/2023-10-25-120015_add_fee_to_channel/up.sql b/coordinator/migrations/2023-10-25-120015_add_fee_to_channel/up.sql new file mode 100644 index 000000000..23c0e70c9 --- /dev/null +++ b/coordinator/migrations/2023-10-25-120015_add_fee_to_channel/up.sql @@ -0,0 +1,6 @@ +-- Your SQL goes here +ALTER TABLE channels ADD COLUMN "fee_sats" BIGINT DEFAULT null; + +ALTER TABLE + channels DROP COLUMN open_channel_fee_payment_hash; +DROP INDEX IF EXISTS channels_funding_txid; \ No newline at end of file diff --git a/coordinator/src/db/channels.rs b/coordinator/src/db/channels.rs index 32435f899..51fd82d15 100644 --- a/coordinator/src/db/channels.rs +++ b/coordinator/src/db/channels.rs @@ -2,7 +2,6 @@ use crate::schema; use crate::schema::channels; use crate::schema::sql_types::ChannelStateType; use anyhow::ensure; -use anyhow::Context; use anyhow::Result; use bitcoin::hashes::hex::ToHex; use bitcoin::secp256k1::PublicKey; @@ -23,7 +22,6 @@ use diesel::QueryableByName; use diesel::RunQueryDsl; use dlc_manager::ChannelId; use hex::FromHex; -use lightning::ln::PaymentHash; use ln_dlc_node::channel::UserChannelId; use std::any::TypeId; use std::str::FromStr; @@ -61,8 +59,8 @@ pub(crate) struct Channel { pub counterparty_pubkey: String, pub created_at: OffsetDateTime, pub updated_at: OffsetDateTime, - pub open_channel_fee_payment_hash: Option, pub liquidity_option_id: Option, + pub fee_sats: Option, } pub(crate) fn get(user_channel_id: &str, conn: &mut PgConnection) -> QueryResult> { @@ -93,20 +91,6 @@ pub(crate) fn get_all_non_pending_channels(conn: &mut PgConnection) -> QueryResu .load(conn) } -pub(crate) fn update_payment_hash( - payment_hash: PaymentHash, - funding_txid: String, - conn: &mut PgConnection, -) -> Result<()> { - let mut channel: Channel = channels::table - .filter(channels::funding_txid.eq(funding_txid.clone())) - .first(conn) - .with_context(|| format!("No channel found for funding txid {funding_txid}"))?; - - channel.open_channel_fee_payment_hash = Some(payment_hash.0.to_hex()); - upsert(channel, conn) -} - pub(crate) fn upsert(channel: Channel, conn: &mut PgConnection) -> Result<()> { let affected_rows = diesel::insert_into(channels::table) .values(channel.clone()) @@ -133,7 +117,7 @@ impl From for Channel { counterparty_pubkey: value.counterparty.to_string(), created_at: value.created_at, updated_at: value.updated_at, - open_channel_fee_payment_hash: None, + fee_sats: value.fee_sats.map(|fee| fee as i64), } } } @@ -175,6 +159,8 @@ impl From for ln_dlc_node::channel::Channel { .expect("valid public key"), created_at: value.created_at, updated_at: value.updated_at, + fee_sats: value.fee_sats.map(|fee| fee as u64), + open_channel_payment_hash: None, } } } diff --git a/coordinator/src/db/payments.rs b/coordinator/src/db/payments.rs index 6a0094bd1..c6e2d6181 100644 --- a/coordinator/src/db/payments.rs +++ b/coordinator/src/db/payments.rs @@ -129,6 +129,7 @@ impl TryFrom for (lightning::ln::PaymentHash, ln_dlc_node::PaymentInfo) timestamp: value.payment_timestamp, description: value.description, invoice: value.invoice, + funding_txid: None, }, )) } diff --git a/coordinator/src/node.rs b/coordinator/src/node.rs index 044fac5f9..3da9c4ba1 100644 --- a/coordinator/src/node.rs +++ b/coordinator/src/node.rs @@ -58,7 +58,6 @@ use trade::cfd::BTCUSD_MAX_PRICE; use trade::Direction; use uuid::Uuid; -pub mod channel_opening_fee; pub mod closed_positions; pub mod connection; pub mod expired_positions; diff --git a/coordinator/src/node/channel_opening_fee.rs b/coordinator/src/node/channel_opening_fee.rs deleted file mode 100644 index 5417b6ae0..000000000 --- a/coordinator/src/node/channel_opening_fee.rs +++ /dev/null @@ -1,66 +0,0 @@ -use crate::db; -use crate::node::Node; -use anyhow::Context; -use anyhow::Result; -use bitcoin::hashes::hex::ToHex; -use bitcoin::secp256k1::ThirtyTwoByteHash; -use lightning::ln::PaymentHash; -use lightning_invoice::Bolt11Invoice; -use ln_dlc_node::channel::JIT_FEE_INVOICE_DESCRIPTION_PREFIX; -use ln_dlc_node::PaymentInfo; - -impl Node { - pub async fn channel_opening_fee_invoice( - &self, - amount: u64, - funding_txid: String, - expiry: Option, - ) -> Result { - let description = format!("{JIT_FEE_INVOICE_DESCRIPTION_PREFIX}{funding_txid}"); - let invoice = self - .inner - .create_invoice(amount, description, expiry.unwrap_or(180))?; - let payment_hash = invoice.payment_hash().into_32(); - let payment_hash_hex = payment_hash.to_hex(); - - // In case we run into an error here we still return the invoice to the user to collect the - // payment and log an error on the coordinator This means that it can happen that we receive - // a payment that we cannot associate if we run into an error here. - tokio::task::spawn_blocking({ - let node = self.clone(); - let invoice = invoice.clone(); - move || { - if let Err(e) = associate_channel_open_fee_payment_with_channel( - node, - payment_hash, - invoice.into(), - funding_txid.clone(), - ) { - tracing::error!(%funding_txid, payment_hash=%payment_hash_hex, "Failed to associate open channel fee payment with channel: {e:#}"); - } - } - }); - - Ok(invoice) - } -} - -fn associate_channel_open_fee_payment_with_channel( - node: Node, - payment_hash: [u8; 32], - payment_info: PaymentInfo, - funding_txid: String, -) -> Result<()> { - let mut conn = node.pool.get().context("Failed to get connection")?; - - // Insert the payment into the database - db::payments::insert((PaymentHash(payment_hash), payment_info), &mut conn) - .context("Failed to insert channel opening payment into database")?; - - // Update the payment hash in the channels table. The channel is identified by the - // funding_tx - db::channels::update_payment_hash(PaymentHash(payment_hash), funding_txid, &mut conn) - .context("Failed to update payment hash in channels table")?; - - Ok(()) -} diff --git a/coordinator/src/node/storage.rs b/coordinator/src/node/storage.rs index de51bdc64..19d6e487b 100644 --- a/coordinator/src/node/storage.rs +++ b/coordinator/src/node/storage.rs @@ -3,6 +3,7 @@ use crate::db::payments; use anyhow::anyhow; use anyhow::Result; use bitcoin::secp256k1::PublicKey; +use bitcoin::Txid; use diesel::r2d2::ConnectionManager; use diesel::r2d2::Pool; use diesel::PgConnection; @@ -48,6 +49,7 @@ impl node::Storage for NodeStorage { htlc_status: HTLCStatus, preimage: Option, secret: Option, + _: Option, ) -> Result<()> { let mut conn = self.pool.get()?; @@ -77,6 +79,7 @@ impl node::Storage for NodeStorage { timestamp: OffsetDateTime::now_utc(), description: "".to_string(), invoice: None, + funding_txid: None, }, ), &mut conn, @@ -161,6 +164,11 @@ impl node::Storage for NodeStorage { Ok(channel) } + fn get_channel_by_payment_hash(&self, _payment_hash: String) -> Result> { + // the payment hash is not stored on the coordinator side. + Ok(None) + } + // Transaction fn upsert_transaction(&self, transaction: Transaction) -> Result<()> { diff --git a/coordinator/src/routes.rs b/coordinator/src/routes.rs index 60fee75b1..34b202e98 100644 --- a/coordinator/src/routes.rs +++ b/coordinator/src/routes.rs @@ -63,6 +63,8 @@ use orderbook_commons::Message; use orderbook_commons::RouteHintHop; use prometheus::Encoder; use prometheus::TextEncoder; +use rust_decimal::prelude::ToPrimitive; +use rust_decimal::Decimal; use serde::Deserialize; use serde::Serialize; use std::str::FromStr; @@ -123,10 +125,6 @@ pub fn router( .route("/api/newaddress", get(get_unused_address)) .route("/api/node", get(get_node_info)) .route("/api/invoice", get(get_invoice)) - .route( - "/api/invoice/open_channel_fee", - get(get_open_channel_fee_invoice), - ) .route("/api/orderbook/orders", get(get_orders).post(post_order)) .route( "/api/orderbook/orders/:order_id", @@ -247,6 +245,10 @@ pub async fn prepare_onboarding_payment( trade_up_to_sats: liquidity_option.trade_up_to_sats, max_deposit_sats: liquidity_option.max_deposit_sats, coordinator_leverage: liquidity_option.coordinator_leverage, + fee_sats: liquidity_option + .get_fee(Decimal::from(amount_sats)) + .to_u64() + .expect("to fit into u64"), }) } }) @@ -299,19 +301,6 @@ pub async fn get_invoice( Ok(invoice.to_string()) } -pub async fn get_open_channel_fee_invoice( - Query(params): Query, - State(state): State>, -) -> Result { - let invoice = state - .node - .channel_opening_fee_invoice(params.amount, params.channel_funding_txid, params.expiry) - .await - .map_err(|e| AppError::InternalServerError(format!("Failed to create invoice: {e:#}")))?; - - Ok(invoice.to_string()) -} - // TODO: We might want to have our own ContractInput type here so we can potentially map fields if // the library changes? #[instrument(skip_all, err(Debug))] diff --git a/coordinator/src/schema.rs b/coordinator/src/schema.rs index 9ee26edf8..7c58c1938 100644 --- a/coordinator/src/schema.rs +++ b/coordinator/src/schema.rs @@ -56,8 +56,8 @@ diesel::table! { counterparty_pubkey -> Text, created_at -> Timestamptz, updated_at -> Timestamptz, - open_channel_fee_payment_hash -> Nullable, liquidity_option_id -> Nullable, + fee_sats -> Nullable, } } diff --git a/crates/ln-dlc-node/src/channel.rs b/crates/ln-dlc-node/src/channel.rs index 0bde44890..1ae2dd5c3 100644 --- a/crates/ln-dlc-node/src/channel.rs +++ b/crates/ln-dlc-node/src/channel.rs @@ -13,10 +13,6 @@ use std::str::FromStr; use time::OffsetDateTime; use uuid::Uuid; -/// The prefix used in the description field of an JIT channel opening invoice to be paid by the -/// client. -pub const JIT_FEE_INVOICE_DESCRIPTION_PREFIX: &str = "jit-channel-fee-"; - /// We introduce a shadow copy of the Lightning channel as LDK deletes channels from its /// [`ChannelManager`] as soon as they are closed. /// @@ -46,6 +42,8 @@ pub struct Channel { pub counterparty: PublicKey, pub created_at: OffsetDateTime, pub updated_at: OffsetDateTime, + pub fee_sats: Option, + pub open_channel_payment_hash: Option, } impl Channel { @@ -66,6 +64,8 @@ impl Channel { channel_id: None, funding_txid: None, liquidity_option_id: None, + fee_sats: None, + open_channel_payment_hash: None, } } @@ -73,6 +73,7 @@ impl Channel { user_channel_id: UserChannelId, counterparty: PublicKey, liquidity_option_id: i32, + fee: u64, ) -> Self { Channel { user_channel_id, @@ -85,6 +86,8 @@ impl Channel { channel_id: None, funding_txid: None, liquidity_option_id: Some(liquidity_option_id), + fee_sats: Some(fee), + open_channel_payment_hash: None, } } diff --git a/crates/ln-dlc-node/src/ldk_node_wallet.rs b/crates/ln-dlc-node/src/ldk_node_wallet.rs index 79163f875..df071dff8 100644 --- a/crates/ln-dlc-node/src/ldk_node_wallet.rs +++ b/crates/ln-dlc-node/src/ldk_node_wallet.rs @@ -492,6 +492,7 @@ pub mod tests { _htlc_status: crate::HTLCStatus, _preimage: Option, _secret: Option, + _funding_txid: Option, ) -> Result<()> { unimplemented!(); } @@ -551,6 +552,10 @@ pub mod tests { unimplemented!(); } + fn get_channel_by_payment_hash(&self, _payment_hash: String) -> Result> { + unimplemented!(); + } + fn upsert_transaction(&self, _transaction: crate::transaction::Transaction) -> Result<()> { unimplemented!(); } diff --git a/crates/ln-dlc-node/src/lib.rs b/crates/ln-dlc-node/src/lib.rs index a94fe49ff..67e5d4626 100644 --- a/crates/ln-dlc-node/src/lib.rs +++ b/crates/ln-dlc-node/src/lib.rs @@ -1,6 +1,7 @@ use crate::ln::TracingLogger; use crate::node::SubChannelManager; use bitcoin::hashes::hex::ToHex; +use bitcoin::Txid; use dlc_custom_signer::CustomKeysManager; use dlc_custom_signer::CustomSigner; use dlc_messages::message_handler::MessageHandler as DlcMessageHandler; @@ -106,6 +107,9 @@ pub struct PaymentInfo { pub timestamp: OffsetDateTime, pub description: String, pub invoice: Option, + /// If the payment was used to open an inbound channel, this tx id refers the funding + /// transaction for opening the channel. + pub funding_txid: Option, } #[derive(Debug, Clone, Copy)] @@ -151,6 +155,7 @@ impl From for PaymentInfo { Bolt11InvoiceDescription::Hash(hash) => hash.0.to_hex(), }, invoice: Some(value.to_string()), + funding_txid: None, } } } diff --git a/crates/ln-dlc-node/src/ln/app_event_handler.rs b/crates/ln-dlc-node/src/ln/app_event_handler.rs index 4ccf10077..21492322f 100644 --- a/crates/ln-dlc-node/src/ln/app_event_handler.rs +++ b/crates/ln-dlc-node/src/ln/app_event_handler.rs @@ -6,6 +6,7 @@ use crate::channel::UserChannelId; use crate::node::ChannelManager; use crate::node::Node; use crate::node::Storage; +use crate::util; use crate::EventHandlerTrait; use anyhow::anyhow; use anyhow::Context; @@ -15,6 +16,7 @@ use bitcoin::hashes::hex::ToHex; use bitcoin::secp256k1::PublicKey; use dlc_manager::subchannel::LNChannelManager; use lightning::events::Event; +use lightning::ln::channelmanager::FailureCode; use parking_lot::Mutex; use std::collections::HashMap; use std::sync::Arc; @@ -69,9 +71,24 @@ where amount_msat, receiver_node_id: _, } => { + let payment_hash_str = util::hex_str(&payment_hash.0); + let channel = self + .node + .storage + .get_channel_by_payment_hash(payment_hash_str)?; + let fee_msat = channel + .clone() + .map(|c| c.fee_sats.map(|fee| fee * 1000).unwrap_or(0)); + let funding_txid = match channel { + Some(channel) => channel.funding_txid, + None => None, + }; + common_handlers::handle_payment_claimed( &self.node, amount_msat, + fee_msat, + funding_txid, payment_hash, purpose, ); @@ -233,12 +250,66 @@ where payment_hash, onion_fields: _, amount_msat, - counterparty_skimmed_fee_msat: _, + counterparty_skimmed_fee_msat, purpose, via_channel_id: _, - via_user_channel_id: _, + via_user_channel_id, claim_deadline: _, } => { + if counterparty_skimmed_fee_msat > 0 { + tracing::info!("Checking if counterparty skimmed fee msat is justified"); + let user_channel_id = via_user_channel_id.context("Missing user channel id")?; + let user_channel_id = UserChannelId::from(user_channel_id); + let channel = self + .node + .storage + .get_channel(&user_channel_id.to_string())? + .with_context(|| { + format!("Couldn't find channel for user_channel_id {user_channel_id}",) + })?; + + if channel.channel_state == ChannelState::Open { + tracing::error!( + "Channel opening fees have already been paid. Rejecting payment." + ); + self.node.channel_manager.fail_htlc_backwards_with_reason( + &payment_hash, + FailureCode::IncorrectOrUnknownPaymentDetails, + ); + return Ok(()); + } + + let channel_fee_msats = channel.fee_sats.unwrap_or(0) * 1000; + if channel_fee_msats < counterparty_skimmed_fee_msat { + tracing::error!("Counterparty skimmed too much fee. Expected fee {channel_fee_msats} msats, Got fee {counterparty_skimmed_fee_msat} msats Rejecting payment!"); + self.node.channel_manager.fail_htlc_backwards_with_reason( + &payment_hash, + FailureCode::IncorrectOrUnknownPaymentDetails, + ); + return Ok(()); + } + + tracing::info!( + funding_txid=?channel.funding_txid, + "Paying channel opening fee of {}", + counterparty_skimmed_fee_msat / 1000 + ); + + common_handlers::handle_payment_claimable( + &self.node.channel_manager, + payment_hash, + purpose, + amount_msat, + )?; + + let mut channel = channel; + channel.channel_state = ChannelState::Open; + channel.open_channel_payment_hash = Some(util::hex_str(&payment_hash.0)); + self.node.storage.upsert_channel(channel)?; + + return Ok(()); + } + common_handlers::handle_payment_claimable( &self.node.channel_manager, payment_hash, diff --git a/crates/ln-dlc-node/src/ln/common_handlers.rs b/crates/ln-dlc-node/src/ln/common_handlers.rs index 237357db4..bed21d375 100644 --- a/crates/ln-dlc-node/src/ln/common_handlers.rs +++ b/crates/ln-dlc-node/src/ln/common_handlers.rs @@ -15,6 +15,7 @@ use anyhow::Result; use bitcoin::consensus::encode::serialize_hex; use bitcoin::hashes::hex::ToHex; use bitcoin::secp256k1::PublicKey; +use bitcoin::Txid; use lightning::chain::chaininterface::BroadcasterInterface; use lightning::chain::chaininterface::ConfirmationTarget; use lightning::chain::chaininterface::FeeEstimator; @@ -163,6 +164,7 @@ where HTLCStatus::Succeeded, Some(payment_preimage), None, + None, ) { anyhow::bail!( "Failed to update sent payment: {e:#}, hash: {payment_hash}", @@ -187,6 +189,7 @@ where timestamp: OffsetDateTime::now_utc(), description: "".to_string(), invoice: None, + funding_txid: None, }, ) { tracing::error!( @@ -305,6 +308,8 @@ where pub fn handle_payment_claimed( node: &Arc>, amount_msat: u64, + fee_sats: Option, + funding_txid: Option, payment_hash: PaymentHash, purpose: PaymentPurpose, ) where @@ -330,10 +335,11 @@ pub fn handle_payment_claimed( &payment_hash, PaymentFlow::Inbound, amount_msat, - MillisatAmount(None), + MillisatAmount(fee_sats), HTLCStatus::Succeeded, payment_preimage, payment_secret, + funding_txid, ) { tracing::error!( payment_hash = %payment_hash.0.to_hex(), @@ -360,6 +366,7 @@ where HTLCStatus::Failed, None, None, + None, ) { tracing::error!( payment_hash = %payment_hash.0.to_hex(), diff --git a/crates/ln-dlc-node/src/ln/coordinator_event_handler.rs b/crates/ln-dlc-node/src/ln/coordinator_event_handler.rs index af4e4613e..ff876774f 100644 --- a/crates/ln-dlc-node/src/ln/coordinator_event_handler.rs +++ b/crates/ln-dlc-node/src/ln/coordinator_event_handler.rs @@ -91,6 +91,8 @@ where common_handlers::handle_payment_claimed( &self.node, amount_msat, + None, + None, payment_hash, purpose, ); diff --git a/crates/ln-dlc-node/src/node/invoice.rs b/crates/ln-dlc-node/src/node/invoice.rs index 85ba09e3e..202abd038 100644 --- a/crates/ln-dlc-node/src/node/invoice.rs +++ b/crates/ln-dlc-node/src/node/invoice.rs @@ -164,6 +164,7 @@ where user_channel_id, trader_id, liquidity_request.liquidity_option_id, + liquidity_request.fee_sats, ); self.storage.upsert_channel(channel).with_context(|| { format!( @@ -286,6 +287,7 @@ where timestamp: OffsetDateTime::now_utc(), description, invoice: Some(format!("{invoice}")), + funding_txid: None, }, )?; diff --git a/crates/ln-dlc-node/src/node/mod.rs b/crates/ln-dlc-node/src/node/mod.rs index 83f02cee7..39c7cdff6 100644 --- a/crates/ln-dlc-node/src/node/mod.rs +++ b/crates/ln-dlc-node/src/node/mod.rs @@ -116,6 +116,7 @@ pub struct LiquidityRequest { pub trade_up_to_sats: u64, pub max_deposit_sats: u64, pub coordinator_leverage: f32, + pub fee_sats: u64, } /// An LN-DLC node. diff --git a/crates/ln-dlc-node/src/node/storage.rs b/crates/ln-dlc-node/src/node/storage.rs index 3dd11e76c..c26cd0233 100644 --- a/crates/ln-dlc-node/src/node/storage.rs +++ b/crates/ln-dlc-node/src/node/storage.rs @@ -7,6 +7,7 @@ use crate::PaymentFlow; use crate::PaymentInfo; use anyhow::Result; use bitcoin::secp256k1::PublicKey; +use bitcoin::Txid; use lightning::chain::transaction::OutPoint; use lightning::ln::PaymentHash; use lightning::ln::PaymentPreimage; @@ -38,6 +39,7 @@ pub trait Storage { htlc_status: HTLCStatus, preimage: Option, secret: Option, + funding_txid: Option, ) -> Result<()>; /// Get a payment based on its payment hash. /// @@ -82,6 +84,10 @@ pub trait Storage { fn all_non_pending_channels(&self) -> Result>; /// Get announced channel with counterparty fn get_announced_channel(&self, counterparty_pubkey: PublicKey) -> Result>; + /// Get channel by payment hash. + /// + /// The payment from which the open channel fee was deducted. + fn get_channel_by_payment_hash(&self, payment_hash: String) -> Result>; // Transaction @@ -119,6 +125,7 @@ impl Storage for InMemoryStore { htlc_status: HTLCStatus, preimage: Option, secret: Option, + funding_txid: Option, ) -> Result<()> { let mut payments = self.payments.lock(); match payments.get_mut(payment_hash) { @@ -140,6 +147,10 @@ impl Storage for InMemoryStore { if let Some(secret) = secret { payment.secret = Some(secret); } + + if let Some(funding_txid) = funding_txid { + payment.funding_txid = Some(funding_txid); + } } None => { payments.insert( @@ -154,6 +165,7 @@ impl Storage for InMemoryStore { timestamp: OffsetDateTime::now_utc(), description: "".to_string(), invoice: None, + funding_txid, }, ); } @@ -249,6 +261,15 @@ impl Storage for InMemoryStore { .cloned()) } + fn get_channel_by_payment_hash(&self, payment_hash: String) -> Result> { + Ok(self + .channels + .lock() + .values() + .find(|c| c.open_channel_payment_hash == Some(payment_hash.clone())) + .cloned()) + } + // Transaction fn upsert_transaction(&self, transaction: Transaction) -> Result<()> { diff --git a/crates/ln-dlc-node/src/node/wallet.rs b/crates/ln-dlc-node/src/node/wallet.rs index bccedba55..6bea7aef3 100644 --- a/crates/ln-dlc-node/src/node/wallet.rs +++ b/crates/ln-dlc-node/src/node/wallet.rs @@ -150,6 +150,7 @@ where description: info.description.clone(), preimage: info.preimage.map(|preimage| preimage.0.to_hex()), invoice: info.invoice.clone(), + funding_txid: info.funding_txid.map(|txid| txid.to_string()), }) .collect::>(); @@ -170,6 +171,7 @@ pub struct PaymentDetails { pub description: String, pub preimage: Option, pub invoice: Option, + pub funding_txid: Option, } impl fmt::Display for PaymentDetails { @@ -182,11 +184,12 @@ impl fmt::Display for PaymentDetails { let timestamp = self.timestamp.to_string(); let description = self.description.clone(); let invoice = self.invoice.clone(); + let funding_txid = self.funding_txid.clone(); write!( f, - "payment_hash {}, status {}, flow {}, amount_msat {}, fee_msat {}, timestamp {}, description {}, invoice {:?}", - payment_hash, status, flow, amount_msat, fee_msat, timestamp, description, invoice + "payment_hash {}, status {}, flow {}, amount_msat {}, fee_msat {}, timestamp {}, description {}, invoice {:?}, funding_txid {:?}", + payment_hash, status, flow, amount_msat, fee_msat, timestamp, description, invoice, funding_txid ) } } diff --git a/crates/ln-dlc-node/src/tests/just_in_time_channel/channel_close.rs b/crates/ln-dlc-node/src/tests/just_in_time_channel/channel_close.rs index 832f8bbcf..0c4063742 100644 --- a/crates/ln-dlc-node/src/tests/just_in_time_channel/channel_close.rs +++ b/crates/ln-dlc-node/src/tests/just_in_time_channel/channel_close.rs @@ -19,7 +19,7 @@ async fn ln_collab_close() { payer.connect(coordinator.info).await.unwrap(); payee.connect(coordinator.info).await.unwrap(); - let payer_to_payee_invoice_amount = 10_000; + let payer_to_payee_invoice_amount = 60_000; let expected_coordinator_payee_channel_value = setup_coordinator_payer_channel(payer_to_payee_invoice_amount, &coordinator, &payer).await; @@ -33,10 +33,12 @@ async fn ln_collab_close() { .await .unwrap(); + let fee_sats = 10_000; + assert_eq!(payee.get_on_chain_balance().unwrap().confirmed, 0); assert_eq!( payee.get_ldk_balance().available(), - payer_to_payee_invoice_amount + payer_to_payee_invoice_amount - fee_sats ); assert_eq!(payee.get_ldk_balance().pending_close(), 0); @@ -76,7 +78,7 @@ async fn ln_collab_close() { assert_eq!( payee.get_on_chain_balance().unwrap().confirmed, - payer_to_payee_invoice_amount + payer_to_payee_invoice_amount - fee_sats ); } @@ -94,7 +96,7 @@ async fn ln_force_close() { payer.connect(coordinator.info).await.unwrap(); payee.connect(coordinator.info).await.unwrap(); - let payer_to_payee_invoice_amount = 5_000; + let payer_to_payee_invoice_amount = 70_000; let expected_coordinator_payee_channel_value = setup_coordinator_payer_channel(payer_to_payee_invoice_amount, &coordinator, &payer).await; @@ -108,10 +110,11 @@ async fn ln_force_close() { .await .unwrap(); + let fee_sats = 10_000; assert_eq!(payee.get_on_chain_balance().unwrap().confirmed, 0); assert_eq!( payee.get_ldk_balance().available(), - payer_to_payee_invoice_amount + payer_to_payee_invoice_amount - fee_sats ); assert_eq!(payee.get_ldk_balance().pending_close(), 0); @@ -134,7 +137,7 @@ async fn ln_force_close() { assert_eq!(payee.get_ldk_balance().available(), 0); assert_eq!( payee.get_ldk_balance().pending_close(), - payer_to_payee_invoice_amount + payer_to_payee_invoice_amount - fee_sats ); // Mine enough blocks so that the payee's revocable output in the commitment transaction diff --git a/crates/ln-dlc-node/src/tests/just_in_time_channel/create.rs b/crates/ln-dlc-node/src/tests/just_in_time_channel/create.rs index 82c20b620..371d37b22 100644 --- a/crates/ln-dlc-node/src/tests/just_in_time_channel/create.rs +++ b/crates/ln-dlc-node/src/tests/just_in_time_channel/create.rs @@ -1,3 +1,4 @@ +use crate::channel::Channel; use crate::channel::ChannelState; use crate::channel::UserChannelId; use crate::fee_rate_estimator::EstimateFeeRate; @@ -136,6 +137,7 @@ async fn fail_to_open_jit_channel_with_fee_rate_over_max() { trade_up_to_sats: 200_000, max_deposit_sats: 200_000, coordinator_leverage: 1.0, + fee_sats: 10_000, }; let final_route_hint_hop = coordinator .prepare_onboarding_payment(liquidity_request) @@ -189,6 +191,7 @@ async fn open_jit_channel_with_disconnected_payee() { trade_up_to_sats: payer_to_payee_invoice_amount, max_deposit_sats: payer_to_payee_invoice_amount, coordinator_leverage: 1.0, + fee_sats: 0, }; let final_route_hint_hop = coordinator .prepare_onboarding_payment(liquidity_request) @@ -251,16 +254,27 @@ pub(crate) async fn send_interceptable_payment( let coordinator_balance_before = coordinator.get_ldk_balance(); let payee_balance_before = payee.get_ldk_balance(); + let fee_sats = 10_000; + let user_channel_id = UserChannelId::new(); let liquidity_request = LiquidityRequest { - user_channel_id: UserChannelId::new(), + user_channel_id, liquidity_option_id: 1, trader_id: payee.info.pubkey, trade_up_to_sats: invoice_amount_sat, max_deposit_sats: invoice_amount_sat, coordinator_leverage: 1.0, + fee_sats, }; let interceptable_route_hint_hop = coordinator.prepare_onboarding_payment(liquidity_request)?; + // Announce the jit channel on the app side. This is done by the app when preparing the + // onboarding invoice. But since we do not have the app available here, we need to do this + // manually. + let channel = Channel::new_jit_channel(user_channel_id, coordinator.info.pubkey, 1, fee_sats); + payee.storage.upsert_channel(channel).with_context(|| { + format!("Failed to insert shadow JIT channel with user channel id {user_channel_id}") + })?; + let invoice = payee.create_invoice_with_route_hint( Some(invoice_amount_sat), None, @@ -315,11 +329,12 @@ pub(crate) async fn send_interceptable_payment( coordinator_balance_after.available_msat() - coordinator_balance_before.available_msat(), coordinator_just_in_time_channel_creation_outbound_liquidity.unwrap_or_default() * 1000 + routing_fee_msat + + fee_sats * 1000 ); assert_eq!( payee_balance_after.available() - payee_balance_before.available(), - invoice_amount_sat + invoice_amount_sat - fee_sats ); Ok(()) diff --git a/crates/ln-dlc-node/src/tests/just_in_time_channel/fail_intercepted_htlc.rs b/crates/ln-dlc-node/src/tests/just_in_time_channel/fail_intercepted_htlc.rs index 9c2e5d000..b9bb2cc1f 100644 --- a/crates/ln-dlc-node/src/tests/just_in_time_channel/fail_intercepted_htlc.rs +++ b/crates/ln-dlc-node/src/tests/just_in_time_channel/fail_intercepted_htlc.rs @@ -37,6 +37,7 @@ async fn fail_intercepted_htlc_if_coordinator_cannot_reconnect_to_payee() { trade_up_to_sats: 100_000, max_deposit_sats: 100_000, coordinator_leverage: 1.0, + fee_sats: 10_000, }; let interceptable_route_hint_hop = coordinator .prepare_onboarding_payment(liquidity_request) @@ -110,6 +111,7 @@ async fn fail_intercepted_htlc_if_connection_lost_after_funding_tx_generated() { trade_up_to_sats: 100_000, max_deposit_sats: 100_000, coordinator_leverage: 1.0, + fee_sats: 10_000, }; let interceptable_route_hint_hop = coordinator .prepare_onboarding_payment(liquidity_request) @@ -188,6 +190,7 @@ async fn fail_intercepted_htlc_if_coordinator_cannot_pay_to_open_jit_channel() { trade_up_to_sats: 100_000, max_deposit_sats: 100_000, coordinator_leverage: 1.0, + fee_sats: 10_000, }; let interceptable_route_hint_hop = coordinator .prepare_onboarding_payment(liquidity_request) diff --git a/crates/ln-dlc-node/src/tests/just_in_time_channel/payments.rs b/crates/ln-dlc-node/src/tests/just_in_time_channel/payments.rs index b4c5e8c9c..bec768b52 100644 --- a/crates/ln-dlc-node/src/tests/just_in_time_channel/payments.rs +++ b/crates/ln-dlc-node/src/tests/just_in_time_channel/payments.rs @@ -74,7 +74,7 @@ async fn new_config_affects_routing_fees() { payer.connect(coordinator.info).await.unwrap(); payee.connect(coordinator.info).await.unwrap(); - let opening_invoice_amount = 10_000; + let opening_invoice_amount = 60_000; let expected_coordinator_payee_channel_value = setup_coordinator_payer_channel(opening_invoice_amount, &coordinator, &payer).await; diff --git a/crates/tests-e2e/src/fund.rs b/crates/tests-e2e/src/fund.rs index 1a11f482d..dfe18e33f 100644 --- a/crates/tests-e2e/src/fund.rs +++ b/crates/tests-e2e/src/fund.rs @@ -1,3 +1,4 @@ +use std::cmp::max; use crate::app::AppHandle; use crate::wait_until; use anyhow::bail; @@ -9,7 +10,6 @@ use native::api::WalletHistoryItem; use native::api::WalletHistoryItemType; use reqwest::Client; use serde::Deserialize; -use std::cmp; use tokio::task::spawn_blocking; /// Instruct the LND faucet to pay an invoice generated with the purpose of opening a JIT channel @@ -23,9 +23,11 @@ pub async fn fund_app_with_faucet( api::register_beta("satoshi@vistomail.com".to_string()).expect("to succeed") }) .await?; - let invoice = - spawn_blocking(move || api::create_onboarding_invoice(fund_amount, 1).expect("to succeed")) - .await?; + let fee_sats = max(fund_amount / 100, 10_000); + let invoice = spawn_blocking(move || { + api::create_onboarding_invoice(1, fund_amount, fee_sats).expect("to succeed") + }) + .await?; api::decode_destination(invoice.clone()).expect("to decode invoice we created"); pay_with_faucet(client, invoice).await?; @@ -43,44 +45,23 @@ pub async fn fund_app_with_faucet( .any(|item| matches!( item, WalletHistoryItem { - flow: PaymentFlow::Outbound, + wallet_type: WalletHistoryItemType::Lightning { .. }, + flow: PaymentFlow::Inbound, status: Status::Confirmed, .. } ))); - let channel_opening_fee = app - .rx - .wallet_info() - .expect("to have wallet info") - .history - .iter() - .find_map(|item| match item { - WalletHistoryItem { - flow: PaymentFlow::Outbound, - wallet_type: WalletHistoryItemType::JitChannelFee { .. }, - amount_sats, - .. - } => Some(amount_sats), - _ => None, - }) - .copied() - .expect("to have an jit channel opening fee"); - - tracing::info!(%fund_amount, %channel_opening_fee, "Successfully funded app with faucet"); + tracing::info!(%fund_amount, %fee_sats, "Successfully funded app with faucet"); assert_eq!( app.rx .wallet_info() .expect("to have wallet info") .balances .lightning, - fund_amount - channel_opening_fee + fund_amount - fee_sats ); - // the fees is the max of 10_000 sats or 1% of the fund amount. - let fee = cmp::max(10_000, fund_amount / 100); - assert_eq!(channel_opening_fee, fee); - Ok(()) } diff --git a/crates/tests-e2e/tests/receive_payment_when_open_position.rs b/crates/tests-e2e/tests/receive_payment_when_open_position.rs index 2b0e5ffdc..b32f33ad2 100644 --- a/crates/tests-e2e/tests/receive_payment_when_open_position.rs +++ b/crates/tests-e2e/tests/receive_payment_when_open_position.rs @@ -13,7 +13,7 @@ async fn can_receive_payment_with_open_position() { let invoice_amount = 10_000; tracing::info!("Creating an invoice"); - let invoice = spawn_blocking(move || api::create_onboarding_invoice(invoice_amount, 1)) + let invoice = spawn_blocking(move || api::create_onboarding_invoice(1, invoice_amount, 10_000)) .await .unwrap() .unwrap(); diff --git a/maker/src/ln/event_handler.rs b/maker/src/ln/event_handler.rs index 57097139d..df94cecf1 100644 --- a/maker/src/ln/event_handler.rs +++ b/maker/src/ln/event_handler.rs @@ -69,6 +69,8 @@ where common_handlers::handle_payment_claimed( &self.node, amount_msat, + None, + None, payment_hash, purpose, ); diff --git a/mobile/lib/features/wallet/application/wallet_service.dart b/mobile/lib/features/wallet/application/wallet_service.dart index d091952cd..00917c2a5 100644 --- a/mobile/lib/features/wallet/application/wallet_service.dart +++ b/mobile/lib/features/wallet/application/wallet_service.dart @@ -18,10 +18,11 @@ class WalletService { } /// Throws an exception if coordinator cannot provide required liquidity. - Future createOnboardingInvoice(Amount amount, int liquidityOptionId) async { + Future createOnboardingInvoice( + Amount amount, int liquidityOptionId, Amount feeSats) async { try { - String invoice = await rust.api - .createOnboardingInvoice(amountSats: amount.sats, liquidityOptionId: liquidityOptionId); + String invoice = await rust.api.createOnboardingInvoice( + amountSats: amount.sats, liquidityOptionId: liquidityOptionId, feeSats: feeSats.sats); logger.i("Successfully created invoice."); return invoice; } catch (error) { diff --git a/mobile/lib/features/wallet/domain/wallet_history.dart b/mobile/lib/features/wallet/domain/wallet_history.dart index 91a66f401..08f3d1f4e 100644 --- a/mobile/lib/features/wallet/domain/wallet_history.dart +++ b/mobile/lib/features/wallet/domain/wallet_history.dart @@ -70,32 +70,6 @@ abstract class WalletHistoryItemData { pnl: type.pnl != null ? Amount(type.pnl!) : null); } - if (item.walletType is rust.WalletHistoryItemType_OrderMatchingFee) { - rust.WalletHistoryItemType_OrderMatchingFee type = - item.walletType as rust.WalletHistoryItemType_OrderMatchingFee; - - return OrderMatchingFeeData( - flow: flow, - amount: amount, - status: status, - timestamp: timestamp, - orderId: type.orderId, - paymentHash: type.paymentHash); - } - - if (item.walletType is rust.WalletHistoryItemType_JitChannelFee) { - rust.WalletHistoryItemType_JitChannelFee type = - item.walletType as rust.WalletHistoryItemType_JitChannelFee; - - return JitChannelOpenFeeData( - flow: flow, - amount: amount, - status: status, - timestamp: timestamp, - txid: type.fundingTxid, - ); - } - rust.WalletHistoryItemType_Lightning type = item.walletType as rust.WalletHistoryItemType_Lightning; @@ -113,7 +87,8 @@ abstract class WalletHistoryItemData { paymentHash: type.paymentHash, feeMsats: type.feeMsat, expiry: expiry, - invoice: type.invoice); + invoice: type.invoice, + fundingTxid: type.fundingTxid); } } @@ -124,6 +99,7 @@ class LightningPaymentData extends WalletHistoryItemData { final String? invoice; final DateTime? expiry; final int? feeMsats; + final String? fundingTxid; LightningPaymentData( {required super.flow, @@ -135,6 +111,7 @@ class LightningPaymentData extends WalletHistoryItemData { required this.invoice, required this.expiry, required this.feeMsats, + required this.fundingTxid, required this.paymentHash}); @override @@ -163,40 +140,6 @@ class OnChainPaymentData extends WalletHistoryItemData { } } -class OrderMatchingFeeData extends WalletHistoryItemData { - final String orderId; - final String paymentHash; - - OrderMatchingFeeData( - {required super.flow, - required super.amount, - required super.status, - required super.timestamp, - required this.orderId, - required this.paymentHash}); - - @override - WalletHistoryItem toWidget() { - return OrderMatchingFeeHistoryItem(data: this); - } -} - -class JitChannelOpenFeeData extends WalletHistoryItemData { - final String txid; - - JitChannelOpenFeeData( - {required super.flow, - required super.amount, - required super.status, - required super.timestamp, - required this.txid}); - - @override - WalletHistoryItem toWidget() { - return JitChannelOpenFeeHistoryItem(data: this); - } -} - class TradeData extends WalletHistoryItemData { final String orderId; final Amount fee; diff --git a/mobile/lib/features/wallet/onboarding/liquidity_card.dart b/mobile/lib/features/wallet/onboarding/liquidity_card.dart index bcddc7ea6..18c02b08d 100644 --- a/mobile/lib/features/wallet/onboarding/liquidity_card.dart +++ b/mobile/lib/features/wallet/onboarding/liquidity_card.dart @@ -61,7 +61,7 @@ class _LiquidityCardState extends State { widget.onTap(minDeposit, maxDeposit); if (amount.sats >= minDeposit.sats && amount.sats <= maxDeposit.sats) { walletService - .createOnboardingInvoice(amount, widget.liquidityOptionId) + .createOnboardingInvoice(amount, widget.liquidityOptionId, fee) .then( (invoice) => showFundWalletModal(context, widget.amount!, fee, invoice!)) .catchError((error) { diff --git a/mobile/lib/features/wallet/wallet_history_item.dart b/mobile/lib/features/wallet/wallet_history_item.dart index dd1ab4eb4..bcb8b234a 100644 --- a/mobile/lib/features/wallet/wallet_history_item.dart +++ b/mobile/lib/features/wallet/wallet_history_item.dart @@ -311,6 +311,12 @@ class LightningPaymentHistoryItem extends WalletHistoryItem { truncate: false, ), ), + Visibility( + visible: data.fundingTxid != null, + child: HistoryDetail( + label: "Funding txid", + displayWidget: TransactionIdText(data.fundingTxid ?? ""), + value: data.fundingTxid ?? ""), ), Visibility( visible: data.expiry != null, @@ -389,66 +395,6 @@ class TradeHistoryItem extends WalletHistoryItem { } } -class OrderMatchingFeeHistoryItem extends WalletHistoryItem { - @override - final OrderMatchingFeeData data; - - const OrderMatchingFeeHistoryItem({super.key, required this.data}); - - @override - List getDetails() { - return [ - HistoryDetail(label: "Order", value: data.orderId), - HistoryDetail(label: "Payment hash", value: data.paymentHash) - ]; - } - - @override - IconData getFlowIcon() { - return Icons.toll; - } - - @override - String getTitle() { - return "Matching fee"; - } - - @override - bool isOnChain() { - return false; - } -} - -class JitChannelOpenFeeHistoryItem extends WalletHistoryItem { - @override - final JitChannelOpenFeeData data; - - const JitChannelOpenFeeHistoryItem({super.key, required this.data}); - - @override - List getDetails() { - return [ - HistoryDetail( - label: "Funding tx ID", displayWidget: TransactionIdText(data.txid), value: data.txid) - ]; - } - - @override - IconData getFlowIcon() { - return Icons.toll; - } - - @override - String getTitle() { - return "Channel opening fee"; - } - - @override - bool isOnChain() { - return false; - } -} - class OnChainPaymentHistoryItem extends WalletHistoryItem { @override final OnChainPaymentData data; diff --git a/mobile/native/migrations/2023-10-25-120015_add_fee_to_channel/down.sql b/mobile/native/migrations/2023-10-25-120015_add_fee_to_channel/down.sql new file mode 100644 index 000000000..899fb4aae --- /dev/null +++ b/mobile/native/migrations/2023-10-25-120015_add_fee_to_channel/down.sql @@ -0,0 +1,5 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE "channels" DROP COLUMN "fee_sats"; +ALTER TABLE "channels" DROP COLUMN "open_channel_payment_hash"; + +ALTER TABLE "payments" DROP COLUMN "funding_txid"; \ No newline at end of file diff --git a/mobile/native/migrations/2023-10-25-120015_add_fee_to_channel/up.sql b/mobile/native/migrations/2023-10-25-120015_add_fee_to_channel/up.sql new file mode 100644 index 000000000..b3edaae0c --- /dev/null +++ b/mobile/native/migrations/2023-10-25-120015_add_fee_to_channel/up.sql @@ -0,0 +1,5 @@ +-- Your SQL goes here +ALTER TABLE "channels" ADD COLUMN "fee_sats" BIGINT; +ALTER TABLE "channels" ADD COLUMN "open_channel_payment_hash" TEXT; + +ALTER TABLE "payments" ADD COLUMN "funding_txid" TEXT; diff --git a/mobile/native/src/api.rs b/mobile/native/src/api.rs index 2b8db653e..d8a138e10 100644 --- a/mobile/native/src/api.rs +++ b/mobile/native/src/api.rs @@ -86,20 +86,13 @@ pub enum WalletHistoryItemType { invoice: Option, fee_msat: Option, expiry_timestamp: Option, + funding_txid: Option, }, Trade { order_id: String, fee_sat: u64, pnl: Option, }, - OrderMatchingFee { - order_id: String, - payment_hash: String, - }, - JitChannelFee { - funding_txid: String, - payment_hash: String, - }, } #[derive(Clone, Debug, Default)] @@ -370,8 +363,12 @@ pub fn liquidity_options() -> Result> { Ok(options) } -pub fn create_onboarding_invoice(amount_sats: u64, liquidity_option_id: i32) -> Result { - Ok(ln_dlc::create_onboarding_invoice(amount_sats, liquidity_option_id)?.to_string()) +pub fn create_onboarding_invoice( + liquidity_option_id: i32, + amount_sats: u64, + fee_sats: u64, +) -> Result { + Ok(ln_dlc::create_onboarding_invoice(liquidity_option_id, amount_sats, fee_sats)?.to_string()) } pub struct PaymentRequest { diff --git a/mobile/native/src/channel_fee.rs b/mobile/native/src/channel_fee.rs deleted file mode 100644 index 2b6d72aa3..000000000 --- a/mobile/native/src/channel_fee.rs +++ /dev/null @@ -1,232 +0,0 @@ -use crate::api::SendPayment; -use crate::commons::reqwest_client; -use crate::config; -use crate::db; -use crate::event::subscriber::Subscriber; -use crate::event::EventInternal; -use crate::event::EventType; -use crate::ln_dlc; -use anyhow::anyhow; -use anyhow::bail; -use anyhow::Context; -use anyhow::Result; -use ln_dlc_node::channel::ChannelState; -use ln_dlc_node::channel::UserChannelId; -use ln_dlc_node::node::rust_dlc_manager::subchannel::LNChannelManager; -use ln_dlc_node::node::rust_dlc_manager::ChannelId; -use ln_dlc_node::node::ChannelManager; -use parking_lot::Mutex; -use rust_decimal::prelude::ToPrimitive; -use rust_decimal::Decimal; -use std::sync::Arc; -use std::time::Duration; -use tokio::runtime::Handle; - -const WAIT_FOR_OUTBOUND_CAPACITY_TIMEOUT: Duration = Duration::from_secs(60); - -#[derive(Clone)] -pub struct ChannelFeePaymentSubscriber { - open_fee_amount: Arc>>, - channel_manager: Arc, -} - -impl Subscriber for ChannelFeePaymentSubscriber { - fn notify(&self, event: &EventInternal) { - if let EventInternal::PaymentClaimed(amount_msats) = event { - if let Err(e) = self.pay_jit_channel_open_fee(*amount_msats) { - tracing::error!("{e:#}"); - } - } - } - - fn events(&self) -> Vec { - vec![EventType::PaymentClaimed] - } -} - -/// This subscriber tries to pay the channel opening fees through a regular lightning payment. -/// -/// TODO(holzeis): This shouldn't be required once we implement a proper LSP flow for opening an -/// inbound channel to the user. -impl ChannelFeePaymentSubscriber { - pub fn new(channel_manager: Arc) -> Self { - Self { - open_fee_amount: Arc::new(Mutex::new(None)), - channel_manager, - } - } - - /// Attempts to pay the fees for opening an inbound channel. - fn pay_jit_channel_open_fee(&self, amount_msats: u64) -> Result<()> { - let channels = self.channel_manager.list_channels(); - // Assuming the user ever only has one channel. Needs to be changed when we are supporting - // multiple open channels at the same time. - let channel_details = channels.first(); - if channels.len() > 1 { - let channel_id = channel_details - .expect("expect channel detail to be some") - .channel_id; - tracing::warn!( - channel_id = hex::encode(channel_id), - "Found more than one channel! Using the first one" - ); - } - - match channel_details { - Some(channel_details) => { - let user_channel_id = UserChannelId::from(channel_details.user_channel_id); - let mut channel = - db::get_channel(&user_channel_id.to_string())?.with_context(|| { - format!("Couldn't find channel by user_channel_id {user_channel_id}") - })?; - - if channel.channel_state != ChannelState::OpenUnpaid { - tracing::debug!("Channel inbound fees have already been paid. Skipping."); - return Ok(()); - } - - let liquidity_option_id = match channel.liquidity_option_id { - Some(liquidity_option_id) => liquidity_option_id, - None => { - tracing::warn!("Couldn't find liquidity option. Not charging for the inbound channel creation."); - return Ok(()); - } - }; - - let liquidity_options = tokio::task::block_in_place(ln_dlc::liquidity_options)?; - let liquidity_option = liquidity_options - .iter() - .find(|l| l.id == liquidity_option_id) - .with_context(|| { - format!("Couldn't find liquidity option for id {liquidity_option_id}") - })?; - - let amount = Decimal::from(amount_msats) / Decimal::from(1000); - let fee = match self.get_open_fee_amount() { - Some(fee) => fee, - None => { - let fee = liquidity_option.get_fee(amount); - self.set_open_fee_amount(fee); - fee - } - }; - - let fee_msats = fee.to_u64().expect("to fit into u64") * 1000; - let channel_id = channel_details.channel_id; - tokio::task::block_in_place(|| { - tracing::debug!( - %user_channel_id, - channel_id = hex::encode(channel_id), - fee_msats, - "Waiting for outbound capacity on channel to pay jit channel opening fee.", - ); - Handle::current().block_on(async { - self.wait_for_outbound_capacity(channel_id, fee_msats) - .await?; - // We add another sleep to ensure that the channel has actually been updated - // after receiving the payment. Note, this is by no - // means ideal and should be revisited some other - // time. - tokio::time::sleep(Duration::from_millis(500)).await; - anyhow::Ok(()) - }) - }) - .context("Failed during wait for outbound capacity")?; - - tracing::debug!("Trying to pay channel opening fees of {fee} sats"); - let funding_txid = channel.funding_txid.with_context(|| format!("Funding transaction id for user_channel_id {user_channel_id} should be set after the ChannelReady event."))?; - - if fee > amount { - 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_fee_invoice( - fee.to_u32().expect("to fit into u32"), - funding_txid.to_string(), - )) - })?; - - match ln_dlc::send_payment(SendPayment::Lightning { - invoice: invoice_str, - amount: None, - }) { - Ok(_) => { - // unset the open fee amount as the payment has been initiated. - self.unset_open_fee_amount(); - channel.channel_state = ChannelState::Open; - db::upsert_channel(channel)?; - tracing::info!("Successfully triggered inbound channel fees payment of {fee} sats to {}", config::get_coordinator_info().pubkey); - } - Err(e) => { - tracing::error!("Failed to pay funding transaction fees of {fee} sats to {}. Error: {e:#}", config::get_coordinator_info().pubkey); - } - }; - } - None => tracing::warn!("Received a payment, but did not have any channel details"), - } - - Ok(()) - } - - fn set_open_fee_amount(&self, fee: Decimal) { - *self.open_fee_amount.lock() = Some(fee); - } - - fn unset_open_fee_amount(&self) { - *self.open_fee_amount.lock() = None; - } - - fn get_open_fee_amount(&self) -> Option { - *self.open_fee_amount.lock() - } - - async fn wait_for_outbound_capacity( - &self, - channel_id: ChannelId, - funding_tx_fees_msats: u64, - ) -> Result<()> { - tokio::time::timeout(WAIT_FOR_OUTBOUND_CAPACITY_TIMEOUT, async { - loop { - let channel_details = match self - .channel_manager - .get_channel_details(&channel_id) { - Some(channel_details) => channel_details, - None => { - bail!("Could not find channel details for {}", hex::encode(channel_id)); - }, - }; - - if channel_details.outbound_capacity_msat >= funding_tx_fees_msats { - tracing::debug!(channel_details.outbound_capacity_msat, channel_id=hex::encode(channel_id), - "Channel has enough outbound capacity"); - return Ok(()) - } else { - tracing::debug!(channel_id = hex::encode(channel_id), outbound_capacity_msats = channel_details.outbound_capacity_msat, funding_tx_fees_msats, - "Channel does not have enough outbound capacity to pay jit channel opening fees yet. Waiting."); - tokio::time::sleep(Duration::from_millis(200)).await - } - } - }) - .await?.map_err(|e| anyhow!("{e:#}")) - .with_context(||format!( - "Timed-out waiting for channel {} to become usable", - hex::encode(channel_id) - )) - } -} - -async fn fetch_fee_invoice(funding_tx_fee: u32, funding_txid: String) -> Result { - reqwest_client() - .get(format!( - "http://{}/api/invoice/open_channel_fee?amount={}&channel_funding_txid={}", - config::get_http_endpoint(), - funding_tx_fee, - funding_txid.as_str() - )) - .send() - .await? - .text() - .await - .map_err(|e| anyhow!("Failed to fetch invoice from coordinator. Error:{e:?}")) -} diff --git a/mobile/native/src/db/mod.rs b/mobile/native/src/db/mod.rs index 03c87615f..15cb30f85 100644 --- a/mobile/native/src/db/mod.rs +++ b/mobile/native/src/db/mod.rs @@ -17,6 +17,7 @@ use anyhow::Result; use base64::Engine; use bdk::bitcoin; use bitcoin::secp256k1::PublicKey; +use bitcoin::Txid; use diesel::connection::SimpleConnection; use diesel::r2d2; use diesel::r2d2::ConnectionManager; @@ -290,6 +291,7 @@ pub fn update_payment( fee_msat: ln_dlc_node::MillisatAmount, preimage: Option, secret: Option, + funding_txid: Option, ) -> Result<()> { tracing::info!(?payment_hash, "Updating payment"); @@ -299,6 +301,7 @@ pub fn update_payment( let preimage = preimage.map(|preimage| base64.encode(preimage.0)); let secret = secret.map(|secret| base64.encode(secret.0)); + let funding_txid = funding_txid.map(|txid| txid.to_string()); PaymentInsertable::update( base64.encode(payment_hash.0), @@ -307,6 +310,7 @@ pub fn update_payment( fee_msat.to_inner().map(|amt| amt as i64), preimage, secret, + funding_txid, &mut db, )?; @@ -443,6 +447,20 @@ pub fn get_announced_channel( Ok(channel) } +pub fn get_channel_by_payment_hash( + payment_hash: String, +) -> Result> { + tracing::debug!(payment_hash, "Getting channel by payment hash"); + + let mut db = connection()?; + + let channel = Channel::get_channel_by_payment_hash(&mut db, &payment_hash) + .map_err(|e| anyhow!("{e:#}"))? + .map(|c| c.into()); + + Ok(channel) +} + // Transaction pub fn upsert_transaction(transaction: ln_dlc_node::transaction::Transaction) -> Result<()> { diff --git a/mobile/native/src/db/models.rs b/mobile/native/src/db/models.rs index 30e2ab13b..70dcda42c 100644 --- a/mobile/native/src/db/models.rs +++ b/mobile/native/src/db/models.rs @@ -723,6 +723,8 @@ pub(crate) struct PaymentInsertable { pub description: String, #[diesel(sql_type = Nullable)] pub invoice: Option, + #[diesel(sql_type = Nullable)] + pub funding_txid: Option, } #[derive(Debug, Clone, Copy, PartialEq, FromSqlRow, AsExpression)] @@ -751,6 +753,7 @@ impl PaymentInsertable { Ok(()) } + #[allow(clippy::too_many_arguments)] pub fn update( payment_hash: String, htlc_status: HtlcStatus, @@ -758,6 +761,7 @@ impl PaymentInsertable { fee_msat: Option, preimage: Option, secret: Option, + funding_txid: Option, conn: &mut SqliteConnection, ) -> Result { let updated_at = OffsetDateTime::now_utc().unix_timestamp(); @@ -816,13 +820,24 @@ impl PaymentInsertable { } } + if let Some(funding_txid) = funding_txid { + let affected_rows = diesel::update(payments::table) + .filter(schema::payments::payment_hash.eq(&payment_hash)) + .set(schema::payments::funding_txid.eq(funding_txid)) + .execute(conn)?; + + if affected_rows == 0 { + bail!("Could not update payment funding_txid") + } + } + let affected_rows = diesel::update(payments::table) .filter(schema::payments::payment_hash.eq(&payment_hash)) .set(schema::payments::updated_at.eq(updated_at)) .execute(conn)?; if affected_rows == 0 { - bail!("Could not update payment updated_at xtimestamp") + bail!("Could not update payment updated_at timestamp") } Ok(()) @@ -847,6 +862,7 @@ pub(crate) struct PaymentQueryable { pub description: String, pub invoice: Option, pub fee_msat: Option, + pub funding_txid: Option, } impl PaymentQueryable { @@ -879,6 +895,7 @@ impl From<(lightning::ln::PaymentHash, ln_dlc_node::PaymentInfo)> for PaymentIns updated_at: timestamp, description: info.description, invoice: info.invoice, + funding_txid: info.funding_txid.map(|txid| txid.to_string()), } } } @@ -919,6 +936,10 @@ impl TryFrom for (lightning::ln::PaymentHash, ln_dlc_node::Pay }) .transpose()?; + let funding_txid = value + .funding_txid + .map(|txid| Txid::from_str(&txid).expect("valid txid")); + let status = value.htlc_status.into(); let amt_msat = @@ -944,6 +965,7 @@ impl TryFrom for (lightning::ln::PaymentHash, ln_dlc_node::Pay timestamp, description, invoice, + funding_txid, }, )) } @@ -1119,6 +1141,8 @@ pub struct Channel { pub created_at: i64, pub updated_at: i64, pub liquidity_option_id: Option, + pub fee_sats: Option, + pub open_channel_payment_hash: Option, } impl Channel { @@ -1140,6 +1164,16 @@ impl Channel { .optional() } + pub fn get_channel_by_payment_hash( + conn: &mut SqliteConnection, + payment_hash: &str, + ) -> QueryResult> { + channels::table + .filter(schema::channels::open_channel_payment_hash.eq(payment_hash)) + .first(conn) + .optional() + } + pub fn get_all(conn: &mut SqliteConnection) -> QueryResult> { channels::table.load(conn) } @@ -1243,6 +1277,8 @@ impl From for Channel { created_at: value.created_at.unix_timestamp(), updated_at: value.updated_at.unix_timestamp(), liquidity_option_id: value.liquidity_option_id, + fee_sats: value.fee_sats.map(|fee| fee as i64), + open_channel_payment_hash: value.open_channel_payment_hash, } } } @@ -1284,6 +1320,8 @@ impl From for ln_dlc_node::channel::Channel { .expect("valid timestamp"), updated_at: OffsetDateTime::from_unix_timestamp(value.updated_at) .expect("valid timestamp"), + fee_sats: value.fee_sats.map(|fee| fee as u64), + open_channel_payment_hash: value.open_channel_payment_hash, } } } @@ -1581,6 +1619,7 @@ pub mod test { updated_at, description: description.clone(), invoice: invoice.clone(), + funding_txid: None, }; PaymentInsertable::insert(payment, &mut connection).unwrap(); @@ -1599,6 +1638,7 @@ pub mod test { updated_at: 200, description: "payment2".to_string(), invoice: Some("invoice2".to_string()), + funding_txid: None }, &mut connection, ) @@ -1621,6 +1661,7 @@ pub mod test { updated_at, description, invoice, + funding_txid: None, }; assert_eq!(expected_payment, loaded_payment); @@ -1640,6 +1681,7 @@ pub mod test { fee_msat, preimage.clone(), secret.clone(), + None, &mut connection, ) .unwrap(); @@ -1753,6 +1795,8 @@ pub mod test { // we need to set the time manually as the nano seconds are not stored in sql. created_at: OffsetDateTime::now_utc().replace_time(Time::from_hms(0, 0, 0).unwrap()), updated_at: OffsetDateTime::now_utc().replace_time(Time::from_hms(0, 0, 0).unwrap()), + fee_sats: Some(10_000), + open_channel_payment_hash: None, }; Channel::upsert(channel.clone().into(), &mut connection).unwrap(); diff --git a/mobile/native/src/lib.rs b/mobile/native/src/lib.rs index b0f0b006c..7bbb214bf 100644 --- a/mobile/native/src/lib.rs +++ b/mobile/native/src/lib.rs @@ -12,7 +12,6 @@ pub mod health; pub mod logger; pub mod schema; -mod channel_fee; mod orderbook; #[allow( diff --git a/mobile/native/src/ln_dlc/mod.rs b/mobile/native/src/ln_dlc/mod.rs index c11b7606e..c20718c8d 100644 --- a/mobile/native/src/ln_dlc/mod.rs +++ b/mobile/native/src/ln_dlc/mod.rs @@ -5,7 +5,6 @@ use crate::api::Status; use crate::api::WalletHistoryItem; use crate::api::WalletHistoryItemType; use crate::calculations; -use crate::channel_fee::ChannelFeePaymentSubscriber; use crate::commons::reqwest_client; use crate::config; use crate::db; @@ -51,7 +50,6 @@ use lightning::events::Event; use lightning::ln::channelmanager::ChannelDetails; use ln_dlc_node::channel::Channel; use ln_dlc_node::channel::UserChannelId; -use ln_dlc_node::channel::JIT_FEE_INVOICE_DESCRIPTION_PREFIX; use ln_dlc_node::config::app_config; use ln_dlc_node::lightning_invoice::Bolt11Invoice; use ln_dlc_node::node::rust_dlc_manager; @@ -349,10 +347,6 @@ pub fn run(data_dir: String, seed_dir: String, runtime: &Runtime) -> Result<()> } }); - event::subscribe(ChannelFeePaymentSubscriber::new( - node.inner.channel_manager.clone(), - )); - runtime.spawn(track_channel_status(node.clone())); if let Err(e) = node.sync_position_with_dlc_channel_state().await { @@ -478,27 +472,18 @@ fn keep_wallet_balance_and_history_up_to_date(node: &Node) -> Result<()> { let payment_hash = hex::encode(details.payment_hash.0); - let description = &details.description; - let wallet_type = if let Some(funding_txid) = - description.strip_prefix(JIT_FEE_INVOICE_DESCRIPTION_PREFIX) - { - WalletHistoryItemType::JitChannelFee { - funding_txid: funding_txid.to_string(), - payment_hash, - } - } else { - let expiry_timestamp = decoded_invoice - .and_then(|inv| inv.timestamp().checked_add(inv.expiry_time())) - .map(|time| OffsetDateTime::from(time).unix_timestamp() as u64); - - WalletHistoryItemType::Lightning { - payment_hash, - description: details.description.clone(), - payment_preimage: details.preimage.clone(), - invoice: details.invoice.clone(), - fee_msat: details.fee_msat, - expiry_timestamp, - } + let expiry_timestamp = decoded_invoice + .and_then(|inv| inv.timestamp().checked_add(inv.expiry_time())) + .map(|time| OffsetDateTime::from(time).unix_timestamp() as u64); + + let wallet_type = WalletHistoryItemType::Lightning { + payment_hash, + description: details.description.clone(), + payment_preimage: details.preimage.clone(), + invoice: details.invoice.clone(), + fee_msat: details.fee_msat, + expiry_timestamp, + funding_txid: details.funding_txid.clone(), }; Some(WalletHistoryItem { @@ -894,8 +879,9 @@ pub fn liquidity_options() -> Result> { } pub fn create_onboarding_invoice( - amount_sats: u64, liquidity_option_id: i32, + amount_sats: u64, + fee_sats: u64, ) -> Result { let runtime = get_or_create_tokio_runtime()?; @@ -914,7 +900,8 @@ pub fn create_onboarding_invoice( let channel = Channel::new_jit_channel( user_channel_id, config::get_coordinator_info().pubkey, - liquidity_option_id + liquidity_option_id, + fee_sats, ); node.inner .storage diff --git a/mobile/native/src/ln_dlc/node.rs b/mobile/native/src/ln_dlc/node.rs index 769037a29..27681d8e2 100644 --- a/mobile/native/src/ln_dlc/node.rs +++ b/mobile/native/src/ln_dlc/node.rs @@ -10,6 +10,7 @@ use anyhow::Context; use anyhow::Result; use bdk::bitcoin::secp256k1::PublicKey; use bdk::TransactionDetails; +use bitcoin::Txid; use dlc_messages::sub_channel::SubChannelCloseFinalize; use dlc_messages::sub_channel::SubChannelRevoke; use dlc_messages::ChannelMessage; @@ -363,6 +364,7 @@ impl node::Storage for NodeStorage { htlc_status: HTLCStatus, preimage: Option, secret: Option, + funding_txid: Option, ) -> Result<()> { match db::get_payment(*payment_hash)? { Some(_) => { @@ -373,6 +375,7 @@ impl node::Storage for NodeStorage { fee_msat, preimage, secret, + funding_txid, )?; } None => { @@ -388,6 +391,7 @@ impl node::Storage for NodeStorage { timestamp: OffsetDateTime::now_utc(), description: "".to_string(), invoice: None, + funding_txid, }, )?; } @@ -453,6 +457,10 @@ impl node::Storage for NodeStorage { db::get_announced_channel(counterparty_pubkey) } + fn get_channel_by_payment_hash(&self, payment_hash: String) -> Result> { + db::get_channel_by_payment_hash(payment_hash) + } + // Transactions fn upsert_transaction(&self, transaction: Transaction) -> Result<()> { diff --git a/mobile/native/src/schema.rs b/mobile/native/src/schema.rs index 808537006..833f35a82 100644 --- a/mobile/native/src/schema.rs +++ b/mobile/native/src/schema.rs @@ -12,6 +12,8 @@ diesel::table! { created_at -> BigInt, updated_at -> BigInt, liquidity_option_id -> Nullable, + fee_sats -> Nullable, + open_channel_payment_hash -> Nullable, } } @@ -55,6 +57,7 @@ diesel::table! { description -> Text, invoice -> Nullable, fee_msat -> Nullable, + funding_txid -> Nullable, } }