diff --git a/graduated-rebalancer/src/lib.rs b/graduated-rebalancer/src/lib.rs index 827d0ba..72d2ee9 100644 --- a/graduated-rebalancer/src/lib.rs +++ b/graduated-rebalancer/src/lib.rs @@ -12,6 +12,7 @@ use bitcoin_payment_instructions::PaymentMethod; use lightning::bitcoin::hashes::Hash; use lightning::bitcoin::hex::DisplayHex; use lightning::bitcoin::OutPoint; +use lightning::types::payment::PaymentHash; use lightning::util::logger::Logger; use lightning::{log_debug, log_error, log_info}; use lightning_invoice::Bolt11Invoice; @@ -159,6 +160,12 @@ pub enum RebalancerEvent { trigger_id: [u8; 32], /// Trusted wallet payment ID for the rebalance trusted_rebalance_payment_id: [u8; 32], + /// The [`PaymentHash`] of this rebalance payment. + /// + /// Note that if you're using LDK only we have the information required to make a + /// payment for this hash, meaning that any payments claimable by our lightning + /// wallet are related to this rebalance. + payment_hash: PaymentHash, /// Amount being rebalanced in millisatoshis amount_msat: u64, }, @@ -170,6 +177,12 @@ pub enum RebalancerEvent { trusted_rebalance_payment_id: [u8; 32], /// Lightning payment ID for the rebalance ln_rebalance_payment_id: [u8; 32], + /// The [`PaymentHash`] of this rebalance payment. + /// + /// Note that if you're using LDK only we have the information required to make a + /// payment for this hash, meaning that any payments claimable by our lightning + /// wallet are related to this rebalance. + payment_hash: PaymentHash, /// Amount rebalanced in millisatoshis amount_msat: u64, /// Total fee paid in millisatoshis @@ -281,6 +294,7 @@ where .handle_event(RebalancerEvent::RebalanceInitiated { trigger_id: params.id, trusted_rebalance_payment_id: rebalance_id, + payment_hash: PaymentHash(expected_hash.to_byte_array()), amount_msat: transfer_amt.milli_sats(), }) .await; @@ -322,6 +336,7 @@ where trusted_rebalance_payment_id: rebalance_id, ln_rebalance_payment_id: ln_payment.id, amount_msat: transfer_amt.milli_sats(), + payment_hash: PaymentHash(expected_hash.to_byte_array()), fee_msat: ln_payment.fee_paid_msat.unwrap_or_default() + trusted_payment.fee_paid_msat.unwrap_or_default(), }) diff --git a/orange-sdk/src/lib.rs b/orange-sdk/src/lib.rs index 9984055..ef4100c 100644 --- a/orange-sdk/src/lib.rs +++ b/orange-sdk/src/lib.rs @@ -669,7 +669,6 @@ impl Wallet { let mut res = Vec::with_capacity( trusted_payments.len() + lightning_payments.len() + splice_outs.len(), ); - let tx_metadata = self.inner.tx_metadata.read(); let mut internal_transfers = HashMap::new(); #[derive(Debug, Default)] @@ -679,164 +678,212 @@ impl Wallet { transaction: Option, } - for payment in trusted_payments { - if let Some(tx_metadata) = tx_metadata.get(&PaymentId::Trusted(payment.id)) { - match &tx_metadata.ty { - TxType::TrustedToLightning { - trusted_payment, - lightning_payment: _, - payment_triggering_transfer, - } => { - let entry = internal_transfers - .entry(*payment_triggering_transfer) - .or_insert(InternalTransfer::default()); - if payment.id == *trusted_payment { - debug_assert!(entry.send_fee.is_none()); - entry.send_fee = Some(payment.fee); - } else { - debug_assert!(false); - } - }, - TxType::OnchainToLightning { .. } => { + let mut completed_internal_transfers = HashMap::new(); + #[derive(Debug)] + struct CompletedInternalTransfer { + trusted_transfer_id: [u8; 32], + payment_triggering_transfer: PaymentId, + time: Duration, + ln_transfer_id: Option<[u8; 32]>, + } + { + let tx_metadata = self.inner.tx_metadata.read(); + for payment in trusted_payments { + if let Some(tx_metadata) = tx_metadata.get(&PaymentId::Trusted(payment.id)) { + match &tx_metadata.ty { + TxType::TrustedToLightning { + trusted_payment, + lightning_payment: _, + payment_triggering_transfer, + } => { + let entry = internal_transfers + .entry(*payment_triggering_transfer) + .or_insert(InternalTransfer::default()); + if payment.id == *trusted_payment { + debug_assert!(entry.send_fee.is_none()); + entry.send_fee = Some(payment.fee); + } else { + debug_assert!(false); + } + }, + TxType::OnchainToLightning { .. } => { + debug_assert!( + false, + "Onchain to lightning transfer should not be in trusted payments list" + ); + }, + TxType::PaymentTriggeringTransferLightning { ty } => { + let entry = internal_transfers + .entry(PaymentId::Trusted(payment.id)) + .or_insert(InternalTransfer::default()); + debug_assert!(entry.transaction.is_none()); + entry.transaction = Some(Transaction { + id: PaymentId::Trusted(payment.id), + status: payment.status, + outbound: payment.outbound, + amount: Some(payment.amount), + fee: Some(payment.fee), + payment_type: *ty, + time_since_epoch: tx_metadata.time, + }); + }, + TxType::Payment { ty } => { + debug_assert!(!matches!(ty, PaymentType::OutgoingOnChain { .. })); + debug_assert!(!matches!(ty, PaymentType::IncomingOnChain { .. })); + res.push(Transaction { + id: PaymentId::Trusted(payment.id), + status: payment.status, + outbound: payment.outbound, + amount: Some(payment.amount), + fee: Some(payment.fee), + payment_type: *ty, + time_since_epoch: tx_metadata.time, + }); + }, + TxType::PendingRebalance { + trusted_payment, + payment_triggering_transfer, + payment_hash, + } => { + if let Some(trusted_id) = trusted_payment { + debug_assert_eq!(*trusted_id, payment.id); + } + if payment.status == TxStatus::Completed { + if trusted_payment.is_some() + && payment_triggering_transfer.is_some() + && payment_hash.is_some() + { + let old_val = completed_internal_transfers.insert( + payment_hash.unwrap(), + CompletedInternalTransfer { + trusted_transfer_id: trusted_payment.unwrap(), + payment_triggering_transfer: + payment_triggering_transfer.unwrap(), + time: payment.time_since_epoch, + ln_transfer_id: None, + }, + ); + debug_assert!(old_val.is_none()); + } + } + // Pending rebalances are not shown in the transaction list. + continue; + }, + } + } else { + if payment.outbound { + log_warn!( + self.inner.logger, + "Missing outbound trusted payment metadata entry on {:?}", + payment.id + ); + #[cfg(feature = "_test-utils")] debug_assert!( false, - "Onchain to lightning transfer should not be in trusted payments list" + "Missing outbound trusted payment metadata entry on {:?}", + payment.id ); - }, - TxType::PaymentTriggeringTransferLightning { ty } => { - let entry = internal_transfers - .entry(PaymentId::Trusted(payment.id)) - .or_insert(InternalTransfer::default()); - debug_assert!(entry.transaction.is_none()); - entry.transaction = Some(Transaction { - id: PaymentId::Trusted(payment.id), - status: payment.status, - outbound: payment.outbound, - amount: Some(payment.amount), - fee: Some(payment.fee), - payment_type: *ty, - time_since_epoch: tx_metadata.time, - }); - }, - TxType::Payment { ty } => { - debug_assert!(!matches!(ty, PaymentType::OutgoingOnChain { .. })); - debug_assert!(!matches!(ty, PaymentType::IncomingOnChain { .. })); - res.push(Transaction { - id: PaymentId::Trusted(payment.id), - status: payment.status, - outbound: payment.outbound, - amount: Some(payment.amount), - fee: Some(payment.fee), - payment_type: *ty, - time_since_epoch: tx_metadata.time, - }); - }, - TxType::PendingRebalance { .. } => { - // Pending rebalances are not shown in the transaction list. - continue; - }, - } - } else { - if payment.outbound { - log_warn!( - self.inner.logger, - "Missing outbound trusted payment metadata entry on {:?}", - payment.id - ); - #[cfg(feature = "_test-utils")] - debug_assert!( - false, - "Missing outbound trusted payment metadata entry on {:?}", - payment.id - ); - } + } - if payment.status != TxStatus::Completed { - // We don't bother to surface pending inbound transactions (i.e. issued but - // unpaid invoices) in our transaction list. - continue; - } + if payment.status != TxStatus::Completed { + // We don't bother to surface pending inbound transactions (i.e. issued but + // unpaid invoices) in our transaction list. + continue; + } - let payment_type = if payment.outbound { - PaymentType::OutgoingLightningBolt11 { payment_preimage: None } - } else { - PaymentType::IncomingLightning {} - }; + let payment_type = if payment.outbound { + PaymentType::OutgoingLightningBolt11 { payment_preimage: None } + } else { + PaymentType::IncomingLightning {} + }; - res.push(Transaction { - id: PaymentId::Trusted(payment.id), - status: payment.status, - outbound: payment.outbound, - amount: Some(payment.amount), - fee: Some(payment.fee), - payment_type, - time_since_epoch: payment.time_since_epoch, - }); - } - } - for payment in lightning_payments { - use ldk_node::payment::PaymentDirection; - let lightning_receive_fee = match payment.kind { - PaymentKind::Bolt11Jit { counterparty_skimmed_fee_msat, .. } => { - let msats = counterparty_skimmed_fee_msat.unwrap_or(0); - debug_assert_eq!(payment.direction, PaymentDirection::Inbound); - Some(Amount::from_milli_sats(msats).expect("Must be valid")) - }, - _ => None, - }; - let fee = if payment.direction == PaymentDirection::Outbound { - match payment.fee_paid_msat { - None => Some(lightning_receive_fee.unwrap_or(Amount::ZERO)), - Some(fee) => Some( - Amount::from_milli_sats(fee) - .unwrap() - .saturating_add(lightning_receive_fee.unwrap_or(Amount::ZERO)), - ), + res.push(Transaction { + id: PaymentId::Trusted(payment.id), + status: payment.status, + outbound: payment.outbound, + amount: Some(payment.amount), + fee: Some(payment.fee), + payment_type, + time_since_epoch: payment.time_since_epoch, + }); } - } else { - Some(lightning_receive_fee.unwrap_or(Amount::ZERO)) - }; - if let Some(tx_metadata) = tx_metadata.get(&PaymentId::SelfCustodial(payment.id.0)) { - match &tx_metadata.ty { - TxType::TrustedToLightning { - trusted_payment: _, - lightning_payment, - payment_triggering_transfer, - } => { - let entry = internal_transfers - .entry(*payment_triggering_transfer) - .or_insert(InternalTransfer::default()); - if payment.id.0 == *lightning_payment { - debug_assert!(entry.receive_fee.is_none()); - entry.receive_fee = lightning_receive_fee.or(Some(Amount::ZERO)); - } else { - debug_assert!(false); - } - }, - TxType::OnchainToLightning { channel_txid, triggering_txid } => { - let entry = internal_transfers - .entry(PaymentId::SelfCustodial(triggering_txid.to_byte_array())) - .or_insert(InternalTransfer::default()); - if &payment.id.0 == channel_txid.as_byte_array() { - debug_assert!(entry.send_fee.is_none()); - entry.send_fee = payment - .fee_paid_msat - .map(|fee| Amount::from_milli_sats(fee).expect("Must be valid")); - } else { - debug_assert!(false); - } + } + for payment in lightning_payments { + use ldk_node::payment::PaymentDirection; + let lightning_receive_fee = match payment.kind { + PaymentKind::Bolt11Jit { counterparty_skimmed_fee_msat, .. } => { + let msats = counterparty_skimmed_fee_msat.unwrap_or(0); + debug_assert_eq!(payment.direction, PaymentDirection::Inbound); + Some(Amount::from_milli_sats(msats).expect("Must be valid")) }, - TxType::PaymentTriggeringTransferLightning { ty: _ } => { - let entry = internal_transfers - .entry(PaymentId::SelfCustodial(payment.id.0)) - .or_insert(InternalTransfer { - receive_fee: lightning_receive_fee, - send_fee: None, - transaction: None, + _ => None, + }; + let fee = if payment.direction == PaymentDirection::Outbound { + match payment.fee_paid_msat { + None => Some(lightning_receive_fee.unwrap_or(Amount::ZERO)), + Some(fee) => Some( + Amount::from_milli_sats(fee) + .unwrap() + .saturating_add(lightning_receive_fee.unwrap_or(Amount::ZERO)), + ), + } + } else { + Some(lightning_receive_fee.unwrap_or(Amount::ZERO)) + }; + if let Some(tx_metadata) = tx_metadata.get(&PaymentId::SelfCustodial(payment.id.0)) + { + match &tx_metadata.ty { + TxType::TrustedToLightning { + trusted_payment: _, + lightning_payment, + payment_triggering_transfer, + } => { + let entry = internal_transfers + .entry(*payment_triggering_transfer) + .or_insert(InternalTransfer::default()); + if payment.id.0 == *lightning_payment { + debug_assert!(entry.receive_fee.is_none()); + entry.receive_fee = lightning_receive_fee.or(Some(Amount::ZERO)); + } else { + debug_assert!(false); + } + }, + TxType::OnchainToLightning { channel_txid, triggering_txid } => { + let entry = internal_transfers + .entry(PaymentId::SelfCustodial(triggering_txid.to_byte_array())) + .or_insert(InternalTransfer::default()); + if &payment.id.0 == channel_txid.as_byte_array() { + debug_assert!(entry.send_fee.is_none()); + entry.send_fee = payment.fee_paid_msat.map(|fee| { + Amount::from_milli_sats(fee).expect("Must be valid") + }); + } else { + debug_assert!(false); + } + }, + TxType::PaymentTriggeringTransferLightning { ty: _ } => { + let entry = internal_transfers + .entry(PaymentId::SelfCustodial(payment.id.0)) + .or_insert(InternalTransfer { + receive_fee: lightning_receive_fee, + send_fee: None, + transaction: None, + }); + debug_assert!(entry.transaction.is_none()); + + entry.transaction = Some(Transaction { + id: PaymentId::SelfCustodial(payment.id.0), + status: payment.status.into(), + outbound: payment.direction == PaymentDirection::Outbound, + amount: payment + .amount_msat + .map(|a| Amount::from_milli_sats(a).expect("Must be valid")), + fee, + payment_type: (&payment).into(), + time_since_epoch: tx_metadata.time, }); - debug_assert!(entry.transaction.is_none()); - - entry.transaction = Some(Transaction { + }, + TxType::Payment { ty: _ } => res.push(Transaction { id: PaymentId::SelfCustodial(payment.id.0), status: payment.status.into(), outbound: payment.direction == PaymentDirection::Outbound, @@ -846,48 +893,90 @@ impl Wallet { fee, payment_type: (&payment).into(), time_since_epoch: tx_metadata.time, - }); - }, - TxType::Payment { ty: _ } => res.push(Transaction { - id: PaymentId::SelfCustodial(payment.id.0), - status: payment.status.into(), - outbound: payment.direction == PaymentDirection::Outbound, - amount: payment - .amount_msat - .map(|a| Amount::from_milli_sats(a).expect("Must be valid")), - fee, - payment_type: (&payment).into(), - time_since_epoch: tx_metadata.time, - }), - TxType::PendingRebalance { .. } => { - // Pending rebalances are not shown in the transaction list. + }), + TxType::PendingRebalance { .. } => { + // Pending rebalances are not shown in the transaction list. + continue; + }, + } + } else { + debug_assert_ne!( + payment.direction, + PaymentDirection::Outbound, + "Missing outbound lightning payment metadata entry on {}", + payment.id + ); + + let status = payment.status.into(); + if status != TxStatus::Completed { + // We don't bother to surface pending inbound transactions (i.e. issued but + // unpaid invoices) in our transaction list, in part because these may be + // failed rebalances. continue; - }, - } - } else { - debug_assert_ne!( - payment.direction, - PaymentDirection::Outbound, - "Missing outbound lightning payment metadata entry on {}", - payment.id - ); + } - let status = payment.status.into(); - if status != TxStatus::Completed { - // We don't bother to surface pending inbound transactions (i.e. issued but - // unpaid invoices) in our transaction list, in part because these may be - // failed rebalances. - continue; + let payment_hash = match payment.kind { + PaymentKind::Onchain { .. } => None, + PaymentKind::Bolt11 { hash, .. } => Some(hash), + PaymentKind::Bolt11Jit { hash, .. } => Some(hash), + PaymentKind::Bolt12Offer { hash, .. } => hash, + PaymentKind::Bolt12Refund { hash, .. } => hash, + PaymentKind::Spontaneous { hash, .. } => Some(hash), + }; + + if let Some(info) = payment_hash + .map(|hash| completed_internal_transfers.get_mut(&hash)) + .flatten() + { + info.ln_transfer_id = Some(payment.id.0); + } else { + res.push(Transaction { + id: PaymentId::SelfCustodial(payment.id.0), + status, + outbound: payment.direction == PaymentDirection::Outbound, + amount: payment + .amount_msat + .map(|a| Amount::from_milli_sats(a).unwrap()), + fee, + payment_type: (&payment).into(), + time_since_epoch: Duration::from_secs(payment.latest_update_timestamp), + }) + } } - res.push(Transaction { - id: PaymentId::SelfCustodial(payment.id.0), - status, - outbound: payment.direction == PaymentDirection::Outbound, - amount: payment.amount_msat.map(|a| Amount::from_milli_sats(a).unwrap()), - fee, - payment_type: (&payment).into(), - time_since_epoch: Duration::from_secs(payment.latest_update_timestamp), - }) + } + } + + for (_, info) in completed_internal_transfers { + debug_assert!(info.ln_transfer_id.is_some()); + if let Some(lightning_payment) = info.ln_transfer_id { + log_info!( + self.inner.logger, + "Setting metadata for background-completed internal transfer with from trusted transaction {:?} to LN transaction {:?} triggered by transaction {}", + info.trusted_transfer_id, + lightning_payment, + info.payment_triggering_transfer + ); + let metadata = TxMetadata { + ty: TxType::TrustedToLightning { + trusted_payment: info.trusted_transfer_id, + lightning_payment, + payment_triggering_transfer: info.payment_triggering_transfer, + }, + time: info.time, + }; + self.inner + .tx_metadata + .set_tx_caused_rebalance(&info.payment_triggering_transfer) + .await + .expect("Failed to write metadata for rebalance transaction"); + self.inner + .tx_metadata + .upsert(PaymentId::Trusted(info.trusted_transfer_id), metadata) + .await; + self.inner + .tx_metadata + .insert(PaymentId::SelfCustodial(lightning_payment), metadata) + .await; } } diff --git a/orange-sdk/src/rebalancer.rs b/orange-sdk/src/rebalancer.rs index 335717c..79abdb4 100644 --- a/orange-sdk/src/rebalancer.rs +++ b/orange-sdk/src/rebalancer.rs @@ -293,9 +293,14 @@ impl graduated_rebalancer::EventHandler for OrangeRebalanceEventHandler { trigger_id, trusted_rebalance_payment_id, amount_msat, + payment_hash, } => { let metadata = TxMetadata { - ty: TxType::PendingRebalance {}, + ty: TxType::PendingRebalance { + payment_triggering_transfer: Some(PaymentId::Trusted(trigger_id)), + trusted_payment: Some(trusted_rebalance_payment_id), + payment_hash: Some(payment_hash), + }, time: SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap(), }; self.tx_metadata @@ -317,6 +322,7 @@ impl graduated_rebalancer::EventHandler for OrangeRebalanceEventHandler { trigger_id, trusted_rebalance_payment_id: rebalance_id, ln_rebalance_payment_id: lightning_id, + payment_hash: _, amount_msat, fee_msat, } => { diff --git a/orange-sdk/src/store.rs b/orange-sdk/src/store.rs index f6eb15a..373b55a 100644 --- a/orange-sdk/src/store.rs +++ b/orange-sdk/src/store.rs @@ -18,7 +18,7 @@ use ldk_node::bitcoin::Txid; use ldk_node::bitcoin::hex::{DisplayHex, FromHex}; use ldk_node::lightning::io; use ldk_node::lightning::ln::msgs::DecodeError; -use ldk_node::lightning::types::payment::PaymentPreimage; +use ldk_node::lightning::types::payment::{PaymentHash, PaymentPreimage}; use ldk_node::lightning::util::persist::KVStore; use ldk_node::lightning::util::ser::{Readable, Writeable, Writer}; use ldk_node::lightning::{impl_writeable_tlv_based, impl_writeable_tlv_based_enum}; @@ -254,14 +254,21 @@ pub(crate) enum TxType { Payment { ty: PaymentType, }, - PendingRebalance {}, + PendingRebalance { + // Note that while all of these fields are `Option`al, they are always + // filled in by any released version of Orange. They were added after + // some initial beta testing, however. + trusted_payment: Option<[u8; 32]>, + payment_triggering_transfer: Option, + payment_hash: Option, + }, } impl TxType { pub(crate) fn is_rebalance(&self) -> bool { matches!( self, - TxType::PendingRebalance {} + TxType::PendingRebalance { .. } | TxType::TrustedToLightning { .. } | TxType::OnchainToLightning { .. } ) @@ -280,7 +287,11 @@ impl_writeable_tlv_based_enum!(TxType, }, (2, PaymentTriggeringTransferLightning) => { (0, ty, required), }, (3, Payment) => { (0, ty, required), }, - (4, PendingRebalance) => {}, + (4, PendingRebalance) => { + (1, trusted_payment, option), + (3, payment_triggering_transfer, option), + (5, payment_hash, option), + }, ); #[derive(Debug, Copy, Clone)]