diff --git a/libs/sdk-bindings/src/breez_sdk.udl b/libs/sdk-bindings/src/breez_sdk.udl index 4c9f5f61d..cd028c526 100644 --- a/libs/sdk-bindings/src/breez_sdk.udl +++ b/libs/sdk-bindings/src/breez_sdk.udl @@ -282,6 +282,7 @@ dictionary LnPaymentDetails { string? ln_address; string? lnurl_withdraw_endpoint; SwapInfo? swap_info; + u32? pending_expiration_block; }; dictionary ClosedChannelPaymentDetails { diff --git a/libs/sdk-core/src/breez_services.rs b/libs/sdk-core/src/breez_services.rs index a85e03147..8f071ec82 100644 --- a/libs/sdk-core/src/breez_services.rs +++ b/libs/sdk-core/src/breez_services.rs @@ -919,7 +919,6 @@ impl BreezServices { let mut payments = closed_channel_payments; payments.extend(new_data.payments.clone()); self.persister.insert_or_update_payments(&payments, true)?; - let duration = start.elapsed(); info!("Sync duration: {:?}", duration); @@ -989,6 +988,7 @@ impl BreezServices { lnurl_metadata: None, lnurl_withdraw_endpoint: None, swap_info: None, + pending_expiration_block: None, }, }, }], @@ -2278,6 +2278,7 @@ pub(crate) mod tests { ln_address: None, lnurl_withdraw_endpoint: None, swap_info: None, + pending_expiration_block: None, }, }, }, @@ -2303,6 +2304,7 @@ pub(crate) mod tests { ln_address: None, lnurl_withdraw_endpoint: Some(test_lnurl_withdraw_endpoint.to_string()), swap_info: None, + pending_expiration_block: None, }, }, }, @@ -2328,6 +2330,7 @@ pub(crate) mod tests { ln_address: Some(test_ln_address.to_string()), lnurl_withdraw_endpoint: None, swap_info: None, + pending_expiration_block: None, }, }, }, @@ -2353,6 +2356,7 @@ pub(crate) mod tests { ln_address: None, lnurl_withdraw_endpoint: None, swap_info: Some(swap_info.clone()), + pending_expiration_block: None, }, }, }, diff --git a/libs/sdk-core/src/bridge_generated.rs b/libs/sdk-core/src/bridge_generated.rs index f48746f75..66d549ce6 100644 --- a/libs/sdk-core/src/bridge_generated.rs +++ b/libs/sdk-core/src/bridge_generated.rs @@ -1251,6 +1251,7 @@ impl support::IntoDart for LnPaymentDetails { self.lnurl_metadata.into_dart(), self.lnurl_withdraw_endpoint.into_dart(), self.swap_info.into_dart(), + self.pending_expiration_block.into_dart(), ] .into_dart() } diff --git a/libs/sdk-core/src/chain.rs b/libs/sdk-core/src/chain.rs index 757bccd29..85d1ab20d 100644 --- a/libs/sdk-core/src/chain.rs +++ b/libs/sdk-core/src/chain.rs @@ -78,8 +78,8 @@ pub(crate) fn get_utxos(address: String, transactions: Vec) -> Result // Calculate confirmed amount associated with this address let mut spent_outputs: Vec = Vec::new(); let mut utxos: Vec = Vec::new(); - for (_, tx) in transactions.iter().enumerate() { - for (_, vin) in tx.vin.iter().enumerate() { + for tx in transactions.iter() { + for vin in tx.vin.iter() { if vin.prevout.scriptpubkey_address == address.clone() { spent_outputs.push(OutPoint { txid: Txid::from_hex(vin.txid.as_str())?, @@ -89,7 +89,7 @@ pub(crate) fn get_utxos(address: String, transactions: Vec) -> Result } } - for (_i, tx) in transactions.iter().enumerate() { + for tx in transactions.iter() { for (index, vout) in tx.vout.iter().enumerate() { if vout.scriptpubkey_address == address { let outpoint = OutPoint { diff --git a/libs/sdk-core/src/greenlight/node_api.rs b/libs/sdk-core/src/greenlight/node_api.rs index d2e170a7d..09dd659f9 100644 --- a/libs/sdk-core/src/greenlight/node_api.rs +++ b/libs/sdk-core/src/greenlight/node_api.rs @@ -14,6 +14,7 @@ use bitcoin::secp256k1::Secp256k1; use bitcoin::util::bip32::{ChildNumber, ExtendedPrivKey}; use bitcoin::{Address, OutPoint, Script, Sequence, Transaction, TxIn, TxOut, Txid, Witness}; use ecies::symmetric::{sym_decrypt, sym_encrypt}; +use futures::Stream; use gl_client::node::ClnClient; use gl_client::pb::cln::listinvoices_invoices::ListinvoicesInvoicesStatus; use gl_client::pb::cln::listpays_pays::ListpaysPaysStatus; @@ -36,12 +37,13 @@ use serde::{Deserialize, Serialize}; use strum_macros::{Display, EnumString}; use tokio::sync::{mpsc, Mutex}; use tokio::time::sleep; -use tokio_stream::{Stream, StreamExt}; +use tokio_stream::StreamExt; use tonic::Streaming; use crate::invoice::{parse_invoice, validate_network, InvoiceError, RouteHintHop}; use crate::models::*; use crate::node_api::{NodeAPI, NodeError, NodeResult}; + use crate::persist::db::SqliteStorage; use crate::{ Channel, ChannelState, NodeConfig, PrepareRedeemOnchainFundsRequest, @@ -733,7 +735,6 @@ impl NodeAPI for Greenlight { let max_allowed_to_receive_msats = MAX_INBOUND_LIQUIDITY_MSAT.saturating_sub(channels_balance); let node_pubkey = hex::encode(node_info.id); - // construct the node state let node_state = NodeState { id: node_pubkey.clone(), @@ -748,10 +749,14 @@ impl NodeAPI for Greenlight { connected_peers, inbound_liquidity_msats: max_receivable_single_channel, }; + let mut htlc_list: Vec = Vec::new(); + for channel in all_channel_models.clone() { + htlc_list.extend(channel.htlcs); + } Ok(SyncResponse { node_state, - payments: pull_transactions(since_timestamp, node_client.clone()).await?, + payments: pull_transactions(since_timestamp, node_client.clone(), htlc_list).await?, channels: all_channel_models, }) } @@ -1419,6 +1424,7 @@ enum NodeCommand { async fn pull_transactions( since_timestamp: u64, client: node::ClnClient, + htlc_list: Vec, ) -> NodeResult> { let mut c = client.clone(); @@ -1452,6 +1458,9 @@ async fn pull_transactions( .map(TryInto::try_into) .collect(); + let outbound_transactions: NodeResult> = + update_payment_expirations(outbound_transactions?, htlc_list); + let mut transactions: Vec = Vec::new(); transactions.extend(received_transactions?); transactions.extend(outbound_transactions?); @@ -1459,6 +1468,33 @@ async fn pull_transactions( Ok(transactions) } +fn update_payment_expirations( + payments: Vec, + htlc_list: Vec, +) -> NodeResult> { + let mut payments_res: Vec = Vec::new(); + if htlc_list.is_empty() { + return Ok(payments_res); + } + + for mut payment in payments.clone() { + let new_data = payment.clone().details; + if let PaymentDetails::Ln { data } = new_data { + for htlc in &htlc_list { + let payment_hash = hex::encode(htlc.clone().payment_hash); + if payment_hash == data.payment_hash + && data.pending_expiration_block < Some(htlc.expiry) + { + payment.details.add_pending_expiration_block(htlc.clone()) + } + } + } + payments_res.push(payment); + } + info!("pending htlc payments {:?}", payments_res); + Ok(payments_res) +} + //pub(crate) fn offchain_payment_to_transaction impl TryFrom for Payment { type Error = NodeError; @@ -1487,6 +1523,7 @@ impl TryFrom for Payment { ln_address: None, lnurl_withdraw_endpoint: None, swap_info: None, + pending_expiration_block: None, }, }, }) @@ -1526,6 +1563,7 @@ impl TryFrom for Payment { ln_address: None, lnurl_withdraw_endpoint: None, swap_info: None, + pending_expiration_block: None, }, }, }) @@ -1580,6 +1618,7 @@ impl TryFrom for Payment { ln_address: None, lnurl_withdraw_endpoint: None, swap_info: None, + pending_expiration_block: None, }, }, }) @@ -1621,6 +1660,7 @@ impl TryFrom for Payment { ln_address: None, lnurl_withdraw_endpoint: None, swap_info: None, + pending_expiration_block: None, }, }, }) @@ -1685,6 +1725,7 @@ impl TryFrom for Payment { ln_address: None, lnurl_withdraw_endpoint: None, swap_info: None, + pending_expiration_block: None, }, }, }) @@ -1772,6 +1813,11 @@ impl From for Channel { alias_remote, alias_local, closing_txid: None, + htlcs: c + .htlcs + .into_iter() + .map(|c| Htlc::from(c.expiry, c.payment_hash)) + .collect(), } } } @@ -1846,6 +1892,7 @@ impl TryFrom for Channel { alias_remote, alias_local, closing_txid: None, + htlcs: Vec::new(), }) } } diff --git a/libs/sdk-core/src/input_parser.rs b/libs/sdk-core/src/input_parser.rs index 978c41170..eab28ff83 100644 --- a/libs/sdk-core/src/input_parser.rs +++ b/libs/sdk-core/src/input_parser.rs @@ -1165,11 +1165,11 @@ pub(crate) mod tests { assert_eq!(pd.metadata_vec()?.len(), 3); assert_eq!( - pd.metadata_vec()?.get(0).ok_or("Key not found")?.key, + pd.metadata_vec()?.first().ok_or("Key not found")?.key, "text/plain" ); assert_eq!( - pd.metadata_vec()?.get(0).ok_or("Key not found")?.value, + pd.metadata_vec()?.first().ok_or("Key not found")?.value, "WRhtV" ); assert_eq!( @@ -1207,11 +1207,11 @@ pub(crate) mod tests { assert_eq!(pd.metadata_vec()?.len(), 3); assert_eq!( - pd.metadata_vec()?.get(0).ok_or("Key not found")?.key, + pd.metadata_vec()?.first().ok_or("Key not found")?.key, "text/plain" ); assert_eq!( - pd.metadata_vec()?.get(0).ok_or("Key not found")?.value, + pd.metadata_vec()?.first().ok_or("Key not found")?.value, "WRhtV" ); assert_eq!( @@ -1404,11 +1404,11 @@ pub(crate) mod tests { assert_eq!(pd.metadata_vec()?.len(), 3); assert_eq!( - pd.metadata_vec()?.get(0).ok_or("Key not found")?.key, + pd.metadata_vec()?.first().ok_or("Key not found")?.key, "text/plain" ); assert_eq!( - pd.metadata_vec()?.get(0).ok_or("Key not found")?.value, + pd.metadata_vec()?.first().ok_or("Key not found")?.value, "WRhtV" ); assert_eq!( diff --git a/libs/sdk-core/src/models.rs b/libs/sdk-core/src/models.rs index 882431ac9..8826c86bc 100644 --- a/libs/sdk-core/src/models.rs +++ b/libs/sdk-core/src/models.rs @@ -675,6 +675,14 @@ pub enum PaymentDetails { }, } +impl PaymentDetails { + pub fn add_pending_expiration_block(&mut self, htlc: Htlc) { + if let PaymentDetails::Ln { data } = self { + data.pending_expiration_block = Some(htlc.expiry) + } + } +} + /// Details of a LN payment, as included in a [Payment] #[derive(PartialEq, Eq, Debug, Clone, Deserialize, Serialize)] pub struct LnPaymentDetails { @@ -700,6 +708,9 @@ pub struct LnPaymentDetails { /// Only set for [PaymentType::Received] payments that were received in the context of a swap pub swap_info: Option, + + /// Only set for [PaymentType::Pending] payments that are inflight. + pub pending_expiration_block: Option, } /// Represents the funds that were on the user side of the channel at the time it was closed. @@ -1098,6 +1109,23 @@ pub struct Channel { /// /// This may be empty for older closed channels, if it was not possible to retrieve the closing txid. pub closing_txid: Option, + + pub htlcs: Vec, +} + +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct Htlc { + pub expiry: u32, + pub payment_hash: Vec, +} + +impl Htlc { + pub fn from(expiry: u32, payment_hash: Vec) -> Self { + Htlc { + expiry, + payment_hash, + } + } } /// State of a Lightning channel diff --git a/libs/sdk-core/src/persist/channels.rs b/libs/sdk-core/src/persist/channels.rs index db28696a1..1528bbb10 100644 --- a/libs/sdk-core/src/persist/channels.rs +++ b/libs/sdk-core/src/persist/channels.rs @@ -95,6 +95,7 @@ impl SqliteStorage { alias_local: row.get(7)?, alias_remote: row.get(8)?, closing_txid: row.get(9)?, + htlcs: Vec::new(), }) })? .map(|i| i.unwrap()) @@ -158,6 +159,7 @@ fn test_simple_sync_channels() { alias_local: None, alias_remote: None, closing_txid: None, + htlcs: Vec::new(), }, Channel { funding_txid: "456".to_string(), @@ -170,6 +172,7 @@ fn test_simple_sync_channels() { alias_local: None, alias_remote: None, closing_txid: None, + htlcs: Vec::new(), }, ]; @@ -201,6 +204,7 @@ fn test_sync_closed_channels() { alias_local: None, alias_remote: None, closing_txid: None, + htlcs: Vec::new(), }, // Simulate closed channel that was persisted with closed_at and closing_txid Channel { @@ -214,6 +218,7 @@ fn test_sync_closed_channels() { alias_local: None, alias_remote: None, closing_txid: Some("a".into()), + htlcs: Vec::new(), }, ]; @@ -243,6 +248,7 @@ fn test_sync_closed_channels() { alias_local: None, alias_remote: None, closing_txid: None, + htlcs: Vec::new(), }, Channel { funding_txid: "456".to_string(), @@ -255,6 +261,7 @@ fn test_sync_closed_channels() { alias_local: None, alias_remote: None, closing_txid: None, + htlcs: Vec::new(), }, ]; assert_eq!(expected.len(), queried_channels.len()); diff --git a/libs/sdk-core/src/persist/migrations.rs b/libs/sdk-core/src/persist/migrations.rs index 422fad536..83133c411 100644 --- a/libs/sdk-core/src/persist/migrations.rs +++ b/libs/sdk-core/src/persist/migrations.rs @@ -120,7 +120,7 @@ pub(crate) fn current_migrations() -> Vec<&'static str> { confirmed_tx_ids TEXT NOT NULL, min_allowed_deposit INTEGER NOT NULL, max_allowed_deposit INTEGER NOT NULL, - last_redeem_error TEXT + last_redeem_error TEXT ) STRICT; INSERT INTO swaps diff --git a/libs/sdk-core/src/persist/transactions.rs b/libs/sdk-core/src/persist/transactions.rs index b1cbb9176..96280609b 100644 --- a/libs/sdk-core/src/persist/transactions.rs +++ b/libs/sdk-core/src/persist/transactions.rs @@ -285,7 +285,7 @@ impl SqliteStorage { data.lnurl_withdraw_endpoint = row.get(11)?; data.swap_info = self .get_swap_info_by_hash(&hex::decode(&payment.id).unwrap_or_default()) - .unwrap_or(None) + .unwrap_or(None); } // In case we have a record of the open channel fee, let's use it. @@ -473,6 +473,7 @@ fn test_ln_transactions() -> PersistResult<(), Box> { ln_address: Some(test_ln_address.to_string()), lnurl_withdraw_endpoint: None, swap_info: None, + pending_expiration_block: None, }, }, }, @@ -498,6 +499,7 @@ fn test_ln_transactions() -> PersistResult<(), Box> { ln_address: None, lnurl_withdraw_endpoint: Some(lnurl_withdraw_url.to_string()), swap_info: None, + pending_expiration_block: None, }, }, }, @@ -523,6 +525,7 @@ fn test_ln_transactions() -> PersistResult<(), Box> { ln_address: None, lnurl_withdraw_endpoint: None, swap_info: Some(swap_info.clone()), + pending_expiration_block: None, }, }, }, @@ -549,6 +552,7 @@ fn test_ln_transactions() -> PersistResult<(), Box> { ln_address: None, lnurl_withdraw_endpoint: None, swap_info: None, + pending_expiration_block: None, }, }, }]; diff --git a/libs/sdk-core/src/swap_in/swap.rs b/libs/sdk-core/src/swap_in/swap.rs index 5b49409f1..641d3c3d5 100644 --- a/libs/sdk-core/src/swap_in/swap.rs +++ b/libs/sdk-core/src/swap_in/swap.rs @@ -872,6 +872,7 @@ mod tests { ln_address: None, lnurl_withdraw_endpoint: None, swap_info: None, + pending_expiration_block: None, }, }, }; diff --git a/libs/sdk-flutter/lib/bridge_generated.dart b/libs/sdk-flutter/lib/bridge_generated.dart index 10f4c98b2..f12e08487 100644 --- a/libs/sdk-flutter/lib/bridge_generated.dart +++ b/libs/sdk-flutter/lib/bridge_generated.dart @@ -705,6 +705,9 @@ class LnPaymentDetails { /// Only set for [PaymentType::Received] payments that were received in the context of a swap final SwapInfo? swapInfo; + /// Only set for [PaymentType::Pending] payments that are inflight. + final int? pendingExpirationBlock; + const LnPaymentDetails({ required this.paymentHash, required this.label, @@ -717,6 +720,7 @@ class LnPaymentDetails { this.lnurlMetadata, this.lnurlWithdrawEndpoint, this.swapInfo, + this.pendingExpirationBlock, }); } @@ -3102,7 +3106,7 @@ class BreezSdkCoreImpl implements BreezSdkCore { LnPaymentDetails _wire2api_ln_payment_details(dynamic raw) { final arr = raw as List; - if (arr.length != 11) throw Exception('unexpected arr length: expect 11 but see ${arr.length}'); + if (arr.length != 12) throw Exception('unexpected arr length: expect 12 but see ${arr.length}'); return LnPaymentDetails( paymentHash: _wire2api_String(arr[0]), label: _wire2api_String(arr[1]), @@ -3115,6 +3119,7 @@ class BreezSdkCoreImpl implements BreezSdkCore { lnurlMetadata: _wire2api_opt_String(arr[8]), lnurlWithdrawEndpoint: _wire2api_opt_String(arr[9]), swapInfo: _wire2api_opt_box_autoadd_swap_info(arr[10]), + pendingExpirationBlock: _wire2api_opt_box_autoadd_u32(arr[11]), ); } diff --git a/libs/sdk-react-native/android/src/main/java/com/breezsdk/BreezSDKMapper.kt b/libs/sdk-react-native/android/src/main/java/com/breezsdk/BreezSDKMapper.kt index 7732563e4..112b524e2 100644 --- a/libs/sdk-react-native/android/src/main/java/com/breezsdk/BreezSDKMapper.kt +++ b/libs/sdk-react-native/android/src/main/java/com/breezsdk/BreezSDKMapper.kt @@ -847,6 +847,16 @@ fun asLnPaymentDetails(lnPaymentDetails: ReadableMap): LnPaymentDetails? { null } val swapInfo = if (hasNonNullKey(lnPaymentDetails, "swapInfo")) lnPaymentDetails.getMap("swapInfo")?.let { asSwapInfo(it) } else null + val pendingExpirationBlock = + if (hasNonNullKey( + lnPaymentDetails, + "pendingExpirationBlock", + ) + ) { + lnPaymentDetails.getInt("pendingExpirationBlock").toUInt() + } else { + null + } return LnPaymentDetails( paymentHash, label, @@ -859,6 +869,7 @@ fun asLnPaymentDetails(lnPaymentDetails: ReadableMap): LnPaymentDetails? { lnAddress, lnurlWithdrawEndpoint, swapInfo, + pendingExpirationBlock, ) } @@ -875,6 +886,7 @@ fun readableMapOf(lnPaymentDetails: LnPaymentDetails): ReadableMap { "lnAddress" to lnPaymentDetails.lnAddress, "lnurlWithdrawEndpoint" to lnPaymentDetails.lnurlWithdrawEndpoint, "swapInfo" to lnPaymentDetails.swapInfo?.let { readableMapOf(it) }, + "pendingExpirationBlock" to lnPaymentDetails.pendingExpirationBlock, ) } diff --git a/libs/sdk-react-native/ios/BreezSDKMapper.swift b/libs/sdk-react-native/ios/BreezSDKMapper.swift index 2f468c5cd..24b95dc70 100644 --- a/libs/sdk-react-native/ios/BreezSDKMapper.swift +++ b/libs/sdk-react-native/ios/BreezSDKMapper.swift @@ -952,6 +952,14 @@ enum BreezSDKMapper { swapInfo = try asSwapInfo(swapInfo: swapInfoTmp) } + var pendingExpirationBlock: UInt32? + if hasNonNilKey(data: lnPaymentDetails, key: "pendingExpirationBlock") { + guard let pendingExpirationBlockTmp = lnPaymentDetails["pendingExpirationBlock"] as? UInt32 else { + throw SdkError.Generic(message: errUnexpectedValue(fieldName: "pendingExpirationBlock")) + } + pendingExpirationBlock = pendingExpirationBlockTmp + } + return LnPaymentDetails( paymentHash: paymentHash, label: label, @@ -963,7 +971,8 @@ enum BreezSDKMapper { lnurlMetadata: lnurlMetadata, lnAddress: lnAddress, lnurlWithdrawEndpoint: lnurlWithdrawEndpoint, - swapInfo: swapInfo + swapInfo: swapInfo, + pendingExpirationBlock: pendingExpirationBlock ) } @@ -980,6 +989,7 @@ enum BreezSDKMapper { "lnAddress": lnPaymentDetails.lnAddress == nil ? nil : lnPaymentDetails.lnAddress, "lnurlWithdrawEndpoint": lnPaymentDetails.lnurlWithdrawEndpoint == nil ? nil : lnPaymentDetails.lnurlWithdrawEndpoint, "swapInfo": lnPaymentDetails.swapInfo == nil ? nil : dictionaryOf(swapInfo: lnPaymentDetails.swapInfo!), + "pendingExpirationBlock": lnPaymentDetails.pendingExpirationBlock == nil ? nil : lnPaymentDetails.pendingExpirationBlock, ] } diff --git a/libs/sdk-react-native/src/index.ts b/libs/sdk-react-native/src/index.ts index 253b77424..856f6d220 100644 --- a/libs/sdk-react-native/src/index.ts +++ b/libs/sdk-react-native/src/index.ts @@ -148,6 +148,7 @@ export type LnPaymentDetails = { lnAddress?: string lnurlWithdrawEndpoint?: string swapInfo?: SwapInfo + pendingExpirationBlock?: number } export type LnUrlAuthRequestData = {