diff --git a/CHANGELOG.md b/CHANGELOG.md index 17b432daf..ce6fe2505 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Allow to drain on-chain wallet by sending amount `0`. - Load persisted `rust-dlc` `ChainMonitor` on restart. +- Upgrade `rust-lightning` to version 0.0.116. +- Charge channel opening fee through the lsp flow ## [1.4.4] - 2023-10-28 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..b2670bf7a --- /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); 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 7b2fc9d33..f2a11dbc5 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 fn get_by_channel_id( channel_id: String, conn: &mut PgConnection, @@ -145,7 +129,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), } } } @@ -187,6 +171,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 8d37d9df7..01cb6ce22 100644 --- a/coordinator/src/node.rs +++ b/coordinator/src/node.rs @@ -49,7 +49,6 @@ use trade::cfd::calculate_short_liquidation_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..d4ea1d112 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. + unimplemented!() + } + // 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 d06d619f0..5367e4ace 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/config.rs b/crates/ln-dlc-node/src/config.rs index be5fd3c73..18ae1863b 100644 --- a/crates/ln-dlc-node/src/config.rs +++ b/crates/ln-dlc-node/src/config.rs @@ -53,6 +53,9 @@ pub fn app_config() -> UserConfig { }, channel_config: ChannelConfig { cltv_expiry_delta: MIN_CLTV_EXPIRY_DELTA, + // Allows the coordinator to charge us a channel-opening fee after intercepting the + // app's funding HTLC. + accept_underpaying_htlcs: true, ..Default::default() }, // we want to accept 0-conf channels from the coordinator diff --git a/crates/ln-dlc-node/src/ldk_node_wallet.rs b/crates/ln-dlc-node/src/ldk_node_wallet.rs index b67fb84cb..785933ab4 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 43a4a9ddb..ff2cdbce5 100644 --- a/crates/ln-dlc-node/src/ln/app_event_handler.rs +++ b/crates/ln-dlc-node/src/ln/app_event_handler.rs @@ -6,8 +6,10 @@ 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::bail; use anyhow::Context; use anyhow::Result; use async_trait::async_trait; @@ -15,6 +17,9 @@ use bitcoin::hashes::hex::ToHex; use bitcoin::secp256k1::PublicKey; use dlc_manager::subchannel::LNChannelManager; use lightning::events::Event; +use lightning::events::PaymentPurpose; +use lightning::ln::channelmanager::FailureCode; +use lightning::ln::PaymentHash; use parking_lot::Mutex; use std::collections::HashMap; use std::sync::Arc; @@ -69,9 +74,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, ); @@ -229,6 +249,34 @@ where failed_next_destination, ); } + Event::PaymentClaimable { + receiver_node_id: _, + payment_hash, + onion_fields: _, + amount_msat, + counterparty_skimmed_fee_msat, + purpose, + via_channel_id: _, + via_user_channel_id, + claim_deadline: _, + } if counterparty_skimmed_fee_msat > 0 => { + tracing::info!("Checking if counterparty skimmed fee msat is justified"); + if let Err(e) = handle_fees_on_claimable_payment( + &self.node.storage, + &self.node.channel_manager, + via_user_channel_id, + payment_hash, + counterparty_skimmed_fee_msat, + purpose, + amount_msat, + ) { + tracing::error!("Failed to handle fees on claimable payment. {e:#}"); + self.node.channel_manager.fail_htlc_backwards_with_reason( + &payment_hash, + FailureCode::IncorrectOrUnknownPaymentDetails, + ); + } + } Event::PaymentClaimable { receiver_node_id: _, payment_hash, @@ -277,6 +325,53 @@ where } } +pub(crate) fn handle_fees_on_claimable_payment( + storage: &Arc, + channel_manager: &Arc, + user_channel_id: Option, + payment_hash: PaymentHash, + counterparty_skimmed_fee_msat: u64, + purpose: PaymentPurpose, + amount_msat: u64, +) -> Result<()> { + let user_channel_id = user_channel_id.context("Missing user channel id")?; + let user_channel_id = UserChannelId::from(user_channel_id); + let channel = 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 { + bail!("Channel opening fees have already been paid. Rejecting payment."); + } + + let channel_fee_msats = channel.fee_sats.unwrap_or(0) * 1000; + tracing::info!( + %user_channel_id, + channel_fee_msats, + counterparty_skimmed_fee_msat, + "Handling fees on claimable payment" + ); + if channel_fee_msats < counterparty_skimmed_fee_msat { + bail!("Counterparty skimmed too much fee. Rejecting payment!"); + } + + tracing::info!( + %user_channel_id, + funding_txid=?channel.funding_txid, + "Skimmed channel opening fee from payment of {}", + counterparty_skimmed_fee_msat / 1000 + ); + + common_handlers::handle_payment_claimable(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)); + storage.upsert_channel(channel)?; + + Ok(()) +} + pub(crate) fn handle_open_channel_request_0_conf( channel_manager: &Arc, counterparty_node_id: PublicKey, 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 e7b4de9d8..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, ); @@ -318,7 +320,7 @@ where let channel = node.storage.get_channel(&user_channel_id)?; let channel = Channel::open_channel(channel, channel_details)?; - node.storage.upsert_channel(channel)?; + node.storage.upsert_channel(channel.clone())?; if let Some(interception) = pending_intercepted_htlcs.lock().get(&counterparty_node_id) { tracing::info!( @@ -328,12 +330,13 @@ where "Pending intercepted HTLC found, forwarding payment" ); + let fee_msat = channel.fee_sats.map(|fee| fee * 1000).unwrap_or(0); node.channel_manager .forward_intercepted_htlc( interception.id, &channel_id, counterparty_node_id, - interception.expected_outbound_amount_msat, + interception.expected_outbound_amount_msat - fee_msat, ) .map_err(|e| anyhow!("{e:?}")) .context("Failed to forward intercepted HTLC")?; @@ -529,6 +532,7 @@ where shadow_channel.outbound_sats = channel_value_sats; shadow_channel.channel_state = ChannelState::Pending; + shadow_channel.fee_sats = Some(liquidity_request.fee_sats); node.storage .upsert_channel(shadow_channel.clone()) @@ -607,6 +611,7 @@ mod tests { trade_up_to_sats: capacity * i, max_deposit_sats: capacity * i, coordinator_leverage: i as f32, + fee_sats: 5_000, }; let channel_value_sat = calculate_channel_value(10_000_000, &request); 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 dec1f1955..b821a1057 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..a4a24235f 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,15 +19,17 @@ 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; + let fee_sats = 10_000; send_interceptable_payment( &payer, &payee, &coordinator, payer_to_payee_invoice_amount, + fee_sats, Some(expected_coordinator_payee_channel_value), ) .await @@ -36,7 +38,7 @@ async fn ln_collab_close() { 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,15 +96,17 @@ 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; + let fee_sats = 10_000; send_interceptable_payment( &payer, &payee, &coordinator, payer_to_payee_invoice_amount, + fee_sats, Some(expected_coordinator_payee_channel_value), ) .await @@ -111,7 +115,7 @@ async fn ln_force_close() { 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 +138,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..3ea7eab07 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; @@ -63,6 +64,7 @@ async fn open_jit_channel() { &payee, &coordinator, payer_to_payee_invoice_amount, + 10_000, Some(expected_coordinator_payee_channel_value), ) .await @@ -136,6 +138,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 +192,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) @@ -241,6 +245,7 @@ pub(crate) async fn send_interceptable_payment( payee: &Node, coordinator: &Node, invoice_amount_sat: u64, + fee_sats: u64, coordinator_just_in_time_channel_creation_outbound_liquidity: Option, ) -> Result<()> { payer.wallet().sync()?; @@ -251,16 +256,26 @@ pub(crate) async fn send_interceptable_payment( let coordinator_balance_before = coordinator.get_ldk_balance(); let payee_balance_before = payee.get_ldk_balance(); + 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 +330,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 bd39a01c5..6ad44f06d 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 @@ -32,6 +32,7 @@ async fn just_in_time_channel_with_multiple_payments() { &payee, &coordinator, payer_to_payee_invoice_amount, + 10_000, Some(expected_coordinator_payee_channel_value), ) .await @@ -76,7 +77,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; @@ -85,6 +86,7 @@ async fn new_config_affects_routing_fees() { &payee, &coordinator, opening_invoice_amount, + 10_000, Some(expected_coordinator_payee_channel_value), ) .await diff --git a/crates/tests-e2e/src/fund.rs b/crates/tests-e2e/src/fund.rs index 1a11f482d..9e3b826af 100644 --- a/crates/tests-e2e/src/fund.rs +++ b/crates/tests-e2e/src/fund.rs @@ -9,7 +9,7 @@ use native::api::WalletHistoryItem; use native::api::WalletHistoryItemType; use reqwest::Client; use serde::Deserialize; -use std::cmp; +use std::cmp::max; 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..53c9341b9 --- /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"; 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 b10a5e5bc..93515ba6e 100644 --- a/mobile/native/src/api.rs +++ b/mobile/native/src/api.rs @@ -87,20 +87,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)] @@ -371,8 +364,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 eda12b774..0d466dfae 100644 --- a/mobile/native/src/db/mod.rs +++ b/mobile/native/src/db/mod.rs @@ -16,6 +16,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; @@ -282,6 +283,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"); @@ -291,6 +293,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), @@ -299,6 +302,7 @@ pub fn update_payment( fee_msat.to_inner().map(|amt| amt as i64), preimage, secret, + funding_txid, &mut db, )?; @@ -435,6 +439,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 da7e3aca6..b92a98099 100644 --- a/mobile/native/src/db/models.rs +++ b/mobile/native/src/db/models.rs @@ -654,6 +654,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)] @@ -682,6 +684,7 @@ impl PaymentInsertable { Ok(()) } + #[allow(clippy::too_many_arguments)] pub fn update( payment_hash: String, htlc_status: HtlcStatus, @@ -689,6 +692,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(); @@ -747,13 +751,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(()) @@ -778,6 +793,7 @@ pub(crate) struct PaymentQueryable { pub description: String, pub invoice: Option, pub fee_msat: Option, + pub funding_txid: Option, } impl PaymentQueryable { @@ -810,6 +826,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()), } } } @@ -850,6 +867,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 = @@ -875,6 +896,7 @@ impl TryFrom for (lightning::ln::PaymentHash, ln_dlc_node::Pay timestamp, description, invoice, + funding_txid, }, )) } @@ -1050,6 +1072,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 { @@ -1080,6 +1104,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) } @@ -1183,6 +1217,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, } } } @@ -1224,6 +1260,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, } } } @@ -1488,6 +1526,7 @@ pub mod test { updated_at, description: description.clone(), invoice: invoice.clone(), + funding_txid: None, }; PaymentInsertable::insert(payment, &mut connection).unwrap(); @@ -1506,6 +1545,7 @@ pub mod test { updated_at: 200, description: "payment2".to_string(), invoice: Some("invoice2".to_string()), + funding_txid: None }, &mut connection, ) @@ -1528,6 +1568,7 @@ pub mod test { updated_at, description, invoice, + funding_txid: None, }; assert_eq!(expected_payment, loaded_payment); @@ -1547,6 +1588,7 @@ pub mod test { fee_msat, preimage.clone(), secret.clone(), + None, &mut connection, ) .unwrap(); @@ -1660,6 +1702,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 e522e18b2..cbde0b0c1 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; @@ -54,7 +53,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; @@ -354,10 +352,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 { @@ -493,27 +487,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 { @@ -1001,8 +986,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()?; @@ -1021,7 +1007,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 d538803f7..7639084ad 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, } } @@ -48,6 +50,7 @@ diesel::table! { description -> Text, invoice -> Nullable, fee_msat -> Nullable, + funding_txid -> Nullable, } }