use std::time::{Duration, SystemTime, UNIX_EPOCH};
use bitcoin::{BlockHash, Txid};
use lightning::ln::channelmanager::PaymentId;
use lightning::ln::msgs::DecodeError;
use lightning::offers::offer::OfferId;
use lightning::util::ser::{Readable, Writeable};
use lightning::{
_init_and_read_len_prefixed_tlv_fields, impl_writeable_tlv_based,
impl_writeable_tlv_based_enum, write_tlv_fields,
};
use lightning_types::payment::{PaymentHash, PaymentPreimage, PaymentSecret};
use lightning_types::string::UntrustedString;
use crate::data_store::{StorableObject, StorableObjectId, StorableObjectUpdate};
use crate::hex_utils;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PaymentDetails {
pub id: PaymentId,
pub kind: PaymentKind,
pub amount_msat: Option<u64>,
pub fee_paid_msat: Option<u64>,
pub direction: PaymentDirection,
pub status: PaymentStatus,
pub latest_update_timestamp: u64,
}
impl PaymentDetails {
pub(crate) fn new(
id: PaymentId, kind: PaymentKind, amount_msat: Option<u64>, fee_paid_msat: Option<u64>,
direction: PaymentDirection, status: PaymentStatus,
) -> Self {
let latest_update_timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or(Duration::from_secs(0))
.as_secs();
Self { id, kind, amount_msat, fee_paid_msat, direction, status, latest_update_timestamp }
}
}
impl Writeable for PaymentDetails {
fn write<W: lightning::util::ser::Writer>(
&self, writer: &mut W,
) -> Result<(), lightning::io::Error> {
write_tlv_fields!(writer, {
(0, self.id, required), (2, None::<Option<PaymentPreimage>>, required),
(3, self.kind, required),
(4, None::<Option<PaymentSecret>>, required),
(5, self.latest_update_timestamp, required),
(6, self.amount_msat, required),
(7, self.fee_paid_msat, option),
(8, self.direction, required),
(10, self.status, required)
});
Ok(())
}
}
impl Readable for PaymentDetails {
fn read<R: lightning::io::Read>(reader: &mut R) -> Result<PaymentDetails, DecodeError> {
let unix_time_secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or(Duration::from_secs(0))
.as_secs();
_init_and_read_len_prefixed_tlv_fields!(reader, {
(0, id, required), (1, lsp_fee_limits, option),
(2, preimage, required),
(3, kind_opt, option),
(4, secret, required),
(5, latest_update_timestamp, (default_value, unix_time_secs)),
(6, amount_msat, required),
(7, fee_paid_msat, option),
(8, direction, required),
(10, status, required)
});
let id: PaymentId = id.0.ok_or(DecodeError::InvalidValue)?;
let preimage: Option<PaymentPreimage> = preimage.0.ok_or(DecodeError::InvalidValue)?;
let secret: Option<PaymentSecret> = secret.0.ok_or(DecodeError::InvalidValue)?;
let latest_update_timestamp: u64 =
latest_update_timestamp.0.ok_or(DecodeError::InvalidValue)?;
let amount_msat: Option<u64> = amount_msat.0.ok_or(DecodeError::InvalidValue)?;
let direction: PaymentDirection = direction.0.ok_or(DecodeError::InvalidValue)?;
let status: PaymentStatus = status.0.ok_or(DecodeError::InvalidValue)?;
let kind = if let Some(kind) = kind_opt {
kind
} else {
let hash = PaymentHash(id.0);
if secret.is_some() {
if let Some(lsp_fee_limits) = lsp_fee_limits {
let counterparty_skimmed_fee_msat = None;
PaymentKind::Bolt11Jit {
hash,
preimage,
secret,
counterparty_skimmed_fee_msat,
lsp_fee_limits,
}
} else {
PaymentKind::Bolt11 { hash, preimage, secret }
}
} else {
PaymentKind::Spontaneous { hash, preimage }
}
};
Ok(PaymentDetails {
id,
kind,
amount_msat,
fee_paid_msat,
direction,
status,
latest_update_timestamp,
})
}
}
impl StorableObjectId for PaymentId {
fn encode_to_hex_str(&self) -> String {
hex_utils::to_string(&self.0)
}
}
impl StorableObject for PaymentDetails {
type Id = PaymentId;
type Update = PaymentDetailsUpdate;
fn id(&self) -> Self::Id {
self.id
}
fn update(&mut self, update: &Self::Update) -> bool {
debug_assert_eq!(
self.id, update.id,
"We should only ever override payment data for the same payment id"
);
let mut updated = false;
macro_rules! update_if_necessary {
($val:expr, $update:expr) => {
if $val != $update {
$val = $update;
updated = true;
}
};
}
if let Some(hash_opt) = update.hash {
match self.kind {
PaymentKind::Bolt12Offer { ref mut hash, .. } => {
debug_assert_eq!(
self.direction,
PaymentDirection::Outbound,
"We should only ever override payment hash for outbound BOLT 12 payments"
);
debug_assert!(
hash.is_none() || *hash == hash_opt,
"We should never change a payment hash after being initially set"
);
update_if_necessary!(*hash, hash_opt);
},
PaymentKind::Bolt12Refund { ref mut hash, .. } => {
debug_assert_eq!(
self.direction,
PaymentDirection::Outbound,
"We should only ever override payment hash for outbound BOLT 12 payments"
);
debug_assert!(
hash.is_none() || *hash == hash_opt,
"We should never change a payment hash after being initially set"
);
update_if_necessary!(*hash, hash_opt);
},
_ => {
},
}
}
if let Some(preimage_opt) = update.preimage {
match self.kind {
PaymentKind::Bolt11 { ref mut preimage, .. } => {
update_if_necessary!(*preimage, preimage_opt)
},
PaymentKind::Bolt11Jit { ref mut preimage, .. } => {
update_if_necessary!(*preimage, preimage_opt)
},
PaymentKind::Bolt12Offer { ref mut preimage, .. } => {
update_if_necessary!(*preimage, preimage_opt)
},
PaymentKind::Bolt12Refund { ref mut preimage, .. } => {
update_if_necessary!(*preimage, preimage_opt)
},
PaymentKind::Spontaneous { ref mut preimage, .. } => {
update_if_necessary!(*preimage, preimage_opt)
},
_ => {},
}
}
if let Some(secret_opt) = update.secret {
match self.kind {
PaymentKind::Bolt11 { ref mut secret, .. } => {
update_if_necessary!(*secret, secret_opt)
},
PaymentKind::Bolt11Jit { ref mut secret, .. } => {
update_if_necessary!(*secret, secret_opt)
},
PaymentKind::Bolt12Offer { ref mut secret, .. } => {
update_if_necessary!(*secret, secret_opt)
},
PaymentKind::Bolt12Refund { ref mut secret, .. } => {
update_if_necessary!(*secret, secret_opt)
},
_ => {},
}
}
if let Some(amount_opt) = update.amount_msat {
update_if_necessary!(self.amount_msat, amount_opt);
}
if let Some(fee_paid_msat_opt) = update.fee_paid_msat {
update_if_necessary!(self.fee_paid_msat, fee_paid_msat_opt);
}
if let Some(skimmed_fee_msat) = update.counterparty_skimmed_fee_msat {
match self.kind {
PaymentKind::Bolt11Jit { ref mut counterparty_skimmed_fee_msat, .. } => {
update_if_necessary!(*counterparty_skimmed_fee_msat, skimmed_fee_msat);
},
_ => debug_assert!(
false,
"We should only ever override counterparty_skimmed_fee_msat for JIT payments"
),
}
}
if let Some(status) = update.status {
update_if_necessary!(self.status, status);
}
if let Some(confirmation_status) = update.confirmation_status {
match self.kind {
PaymentKind::Onchain { ref mut status, .. } => {
update_if_necessary!(*status, confirmation_status);
},
_ => {},
}
}
if updated {
self.latest_update_timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or(Duration::from_secs(0))
.as_secs();
}
updated
}
fn to_update(&self) -> Self::Update {
self.into()
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum PaymentDirection {
Inbound,
Outbound,
}
impl_writeable_tlv_based_enum!(PaymentDirection,
(0, Inbound) => {},
(1, Outbound) => {}
);
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum PaymentStatus {
Pending,
Succeeded,
Failed,
}
impl_writeable_tlv_based_enum!(PaymentStatus,
(0, Pending) => {},
(2, Succeeded) => {},
(4, Failed) => {}
);
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum PaymentKind {
Onchain {
txid: Txid,
status: ConfirmationStatus,
},
Bolt11 {
hash: PaymentHash,
preimage: Option<PaymentPreimage>,
secret: Option<PaymentSecret>,
},
Bolt11Jit {
hash: PaymentHash,
preimage: Option<PaymentPreimage>,
secret: Option<PaymentSecret>,
counterparty_skimmed_fee_msat: Option<u64>,
lsp_fee_limits: LSPFeeLimits,
},
Bolt12Offer {
hash: Option<PaymentHash>,
preimage: Option<PaymentPreimage>,
secret: Option<PaymentSecret>,
offer_id: OfferId,
payer_note: Option<UntrustedString>,
quantity: Option<u64>,
},
Bolt12Refund {
hash: Option<PaymentHash>,
preimage: Option<PaymentPreimage>,
secret: Option<PaymentSecret>,
payer_note: Option<UntrustedString>,
quantity: Option<u64>,
},
Spontaneous {
hash: PaymentHash,
preimage: Option<PaymentPreimage>,
},
}
impl_writeable_tlv_based_enum!(PaymentKind,
(0, Onchain) => {
(0, txid, required),
(2, status, required),
},
(2, Bolt11) => {
(0, hash, required),
(2, preimage, option),
(4, secret, option),
},
(4, Bolt11Jit) => {
(0, hash, required),
(1, counterparty_skimmed_fee_msat, option),
(2, preimage, option),
(4, secret, option),
(6, lsp_fee_limits, required),
},
(6, Bolt12Offer) => {
(0, hash, option),
(1, payer_note, option),
(2, preimage, option),
(3, quantity, option),
(4, secret, option),
(6, offer_id, required),
},
(8, Spontaneous) => {
(0, hash, required),
(2, preimage, option),
},
(10, Bolt12Refund) => {
(0, hash, option),
(1, payer_note, option),
(2, preimage, option),
(3, quantity, option),
(4, secret, option),
}
);
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum ConfirmationStatus {
Confirmed {
block_hash: BlockHash,
height: u32,
timestamp: u64,
},
Unconfirmed,
}
impl_writeable_tlv_based_enum!(ConfirmationStatus,
(0, Confirmed) => {
(0, block_hash, required),
(2, height, required),
(4, timestamp, required),
},
(2, Unconfirmed) => {},
);
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct LSPFeeLimits {
pub max_total_opening_fee_msat: Option<u64>,
pub max_proportional_opening_fee_ppm_msat: Option<u64>,
}
impl_writeable_tlv_based!(LSPFeeLimits, {
(0, max_total_opening_fee_msat, option),
(2, max_proportional_opening_fee_ppm_msat, option),
});
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct PaymentDetailsUpdate {
pub id: PaymentId,
pub hash: Option<Option<PaymentHash>>,
pub preimage: Option<Option<PaymentPreimage>>,
pub secret: Option<Option<PaymentSecret>>,
pub amount_msat: Option<Option<u64>>,
pub fee_paid_msat: Option<Option<u64>>,
pub counterparty_skimmed_fee_msat: Option<Option<u64>>,
pub direction: Option<PaymentDirection>,
pub status: Option<PaymentStatus>,
pub confirmation_status: Option<ConfirmationStatus>,
}
impl PaymentDetailsUpdate {
pub fn new(id: PaymentId) -> Self {
Self {
id,
hash: None,
preimage: None,
secret: None,
amount_msat: None,
fee_paid_msat: None,
counterparty_skimmed_fee_msat: None,
direction: None,
status: None,
confirmation_status: None,
}
}
}
impl From<&PaymentDetails> for PaymentDetailsUpdate {
fn from(value: &PaymentDetails) -> Self {
let (hash, preimage, secret) = match value.kind {
PaymentKind::Bolt11 { hash, preimage, secret, .. } => (Some(hash), preimage, secret),
PaymentKind::Bolt11Jit { hash, preimage, secret, .. } => (Some(hash), preimage, secret),
PaymentKind::Bolt12Offer { hash, preimage, secret, .. } => (hash, preimage, secret),
PaymentKind::Bolt12Refund { hash, preimage, secret, .. } => (hash, preimage, secret),
PaymentKind::Spontaneous { hash, preimage, .. } => (Some(hash), preimage, None),
_ => (None, None, None),
};
let confirmation_status = match value.kind {
PaymentKind::Onchain { status, .. } => Some(status),
_ => None,
};
let counterparty_skimmed_fee_msat = match value.kind {
PaymentKind::Bolt11Jit { counterparty_skimmed_fee_msat, .. } => {
Some(counterparty_skimmed_fee_msat)
},
_ => None,
};
Self {
id: value.id,
hash: Some(hash),
preimage: Some(preimage),
secret: Some(secret),
amount_msat: Some(value.amount_msat),
fee_paid_msat: Some(value.fee_paid_msat),
counterparty_skimmed_fee_msat,
direction: Some(value.direction),
status: Some(value.status),
confirmation_status,
}
}
}
impl StorableObjectUpdate<PaymentDetails> for PaymentDetailsUpdate {
fn id(&self) -> <PaymentDetails as StorableObject>::Id {
self.id
}
}
#[cfg(test)]
mod tests {
use bitcoin::io::Cursor;
use lightning::util::ser::Readable;
use super::*;
#[derive(Clone, Debug, PartialEq, Eq)]
struct OldPaymentDetails {
pub hash: PaymentHash,
pub preimage: Option<PaymentPreimage>,
pub secret: Option<PaymentSecret>,
pub amount_msat: Option<u64>,
pub direction: PaymentDirection,
pub status: PaymentStatus,
pub lsp_fee_limits: Option<LSPFeeLimits>,
}
impl_writeable_tlv_based!(OldPaymentDetails, {
(0, hash, required),
(1, lsp_fee_limits, option),
(2, preimage, required),
(4, secret, required),
(6, amount_msat, required),
(8, direction, required),
(10, status, required)
});
#[test]
fn old_payment_details_deser_compat() {
let hash = PaymentHash([42u8; 32]);
let preimage = Some(PaymentPreimage([43u8; 32]));
let secret = Some(PaymentSecret([44u8; 32]));
let amount_msat = Some(45_000_000);
{
let old_bolt11_payment = OldPaymentDetails {
hash,
preimage,
secret,
amount_msat,
direction: PaymentDirection::Inbound,
status: PaymentStatus::Pending,
lsp_fee_limits: None,
};
let old_bolt11_encoded = old_bolt11_payment.encode();
assert_eq!(
old_bolt11_payment,
OldPaymentDetails::read(&mut Cursor::new(old_bolt11_encoded.clone())).unwrap()
);
let bolt11_decoded =
PaymentDetails::read(&mut Cursor::new(old_bolt11_encoded)).unwrap();
let bolt11_reencoded = bolt11_decoded.encode();
assert_eq!(
bolt11_decoded,
PaymentDetails::read(&mut Cursor::new(bolt11_reencoded)).unwrap()
);
match bolt11_decoded.kind {
PaymentKind::Bolt11 { hash: h, preimage: p, secret: s } => {
assert_eq!(hash, h);
assert_eq!(preimage, p);
assert_eq!(secret, s);
},
_ => {
panic!("Unexpected kind!");
},
}
}
{
let lsp_fee_limits = Some(LSPFeeLimits {
max_total_opening_fee_msat: Some(46_000),
max_proportional_opening_fee_ppm_msat: Some(47_000),
});
let old_bolt11_jit_payment = OldPaymentDetails {
hash,
preimage,
secret,
amount_msat,
direction: PaymentDirection::Inbound,
status: PaymentStatus::Pending,
lsp_fee_limits,
};
let old_bolt11_jit_encoded = old_bolt11_jit_payment.encode();
assert_eq!(
old_bolt11_jit_payment,
OldPaymentDetails::read(&mut Cursor::new(old_bolt11_jit_encoded.clone())).unwrap()
);
let bolt11_jit_decoded =
PaymentDetails::read(&mut Cursor::new(old_bolt11_jit_encoded)).unwrap();
let bolt11_jit_reencoded = bolt11_jit_decoded.encode();
assert_eq!(
bolt11_jit_decoded,
PaymentDetails::read(&mut Cursor::new(bolt11_jit_reencoded)).unwrap()
);
match bolt11_jit_decoded.kind {
PaymentKind::Bolt11Jit {
hash: h,
preimage: p,
secret: s,
counterparty_skimmed_fee_msat: c,
lsp_fee_limits: l,
} => {
assert_eq!(hash, h);
assert_eq!(preimage, p);
assert_eq!(secret, s);
assert_eq!(None, c);
assert_eq!(lsp_fee_limits, Some(l));
},
_ => {
panic!("Unexpected kind!");
},
}
}
{
let old_spontaneous_payment = OldPaymentDetails {
hash,
preimage,
secret: None,
amount_msat,
direction: PaymentDirection::Inbound,
status: PaymentStatus::Pending,
lsp_fee_limits: None,
};
let old_spontaneous_encoded = old_spontaneous_payment.encode();
assert_eq!(
old_spontaneous_payment,
OldPaymentDetails::read(&mut Cursor::new(old_spontaneous_encoded.clone())).unwrap()
);
let spontaneous_decoded =
PaymentDetails::read(&mut Cursor::new(old_spontaneous_encoded)).unwrap();
let spontaneous_reencoded = spontaneous_decoded.encode();
assert_eq!(
spontaneous_decoded,
PaymentDetails::read(&mut Cursor::new(spontaneous_reencoded)).unwrap()
);
match spontaneous_decoded.kind {
PaymentKind::Spontaneous { hash: h, preimage: p } => {
assert_eq!(hash, h);
assert_eq!(preimage, p);
},
_ => {
panic!("Unexpected kind!");
},
}
}
}
}