use std::{fmt, str::FromStr};
use anyhow::Context;
use lexe_byte_array::ByteArray;
use lexe_crypto::rng::{RngCore, RngExt};
use lexe_serde::hexstr_or_bytes;
use lexe_sha256::sha256;
use lexe_std::Apply;
use lightning::{
chain::transaction::OutPoint,
ln::{channel_state::ChannelDetails, types::ChannelId},
};
#[cfg(any(test, feature = "test-utils"))]
use proptest_derive::Arbitrary;
use ref_cast::RefCast;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use serde_with::{DeserializeFromStr, SerializeDisplay};
use crate::{
api::user::{NodePk, Scid},
dec,
ln::{amount::Amount, hashes::Txid},
};
#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
#[derive(RefCast, Serialize, Deserialize)]
#[repr(transparent)]
pub struct LxChannelId(#[serde(with = "hexstr_or_bytes")] pub [u8; 32]);
lexe_byte_array::impl_byte_array!(LxChannelId, 32);
lexe_byte_array::impl_fromstr_fromhex!(LxChannelId, 32);
lexe_byte_array::impl_debug_display_as_hex!(LxChannelId);
impl From<ChannelId> for LxChannelId {
fn from(cid: ChannelId) -> Self {
Self(cid.0)
}
}
impl From<LxChannelId> for ChannelId {
fn from(cid: LxChannelId) -> Self {
Self(cid.0)
}
}
#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
#[derive(Copy, Clone, Eq, PartialEq, Hash, RefCast, Serialize, Deserialize)]
#[repr(transparent)]
pub struct LxUserChannelId(#[serde(with = "hexstr_or_bytes")] pub [u8; 16]);
impl LxUserChannelId {
#[inline]
pub fn to_u128(self) -> u128 {
u128::from_le_bytes(self.0)
}
pub fn from_rng<R: RngCore>(rng: &mut R) -> Self {
Self(rng.gen_bytes())
}
pub fn derive_temporary_channel_id(&self) -> LxChannelId {
LxChannelId(sha256::digest(&self.0).to_array())
}
}
lexe_byte_array::impl_byte_array!(LxUserChannelId, 16);
lexe_byte_array::impl_fromstr_fromhex!(LxUserChannelId, 16);
lexe_byte_array::impl_debug_display_as_hex!(LxUserChannelId);
impl From<u128> for LxUserChannelId {
fn from(value: u128) -> Self {
Self(value.to_le_bytes())
}
}
impl From<LxUserChannelId> for u128 {
fn from(value: LxUserChannelId) -> Self {
value.to_u128()
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct LxChannelDetails {
pub channel_id: LxChannelId,
pub user_channel_id: LxUserChannelId,
pub scid: Option<Scid>,
pub inbound_payment_scid: Option<Scid>,
pub outbound_payment_scid: Option<Scid>,
pub funding_txo: Option<LxOutPoint>,
pub counterparty_alias: Option<String>,
pub counterparty_node_id: NodePk,
pub channel_value: Amount,
pub punishment_reserve: Amount,
pub force_close_spend_delay: Option<u16>,
pub is_announced: bool,
pub is_outbound: bool,
pub is_ready: bool,
pub is_usable: bool,
pub our_balance: Amount,
pub outbound_capacity: Amount,
pub next_outbound_htlc_limit: Amount,
pub their_balance: Amount,
pub inbound_capacity: Amount,
pub our_base_fee: Amount,
pub our_prop_fee: Decimal,
pub our_cltv_expiry_delta: u16,
pub their_base_fee: Option<Amount>,
pub their_prop_fee: Option<Decimal>,
pub their_cltv_expiry_delta: Option<u16>,
pub inbound_htlc_minimum: Amount,
pub inbound_htlc_maximum: Option<Amount>,
pub outbound_htlc_minimum: Option<Amount>,
pub outbound_htlc_maximum: Option<Amount>,
pub cpty_supports_basic_mpp: bool,
pub cpty_supports_onion_messages: bool,
pub cpty_supports_wumbo: bool,
pub cpty_supports_zero_conf: bool,
}
impl LxChannelDetails {
pub fn from_ldk(
details: ChannelDetails,
our_balance: Amount,
counterparty_alias: Option<String>,
) -> anyhow::Result<Self> {
let inbound_payment_scid = details.get_inbound_payment_scid().map(Scid);
let outbound_payment_scid =
details.get_outbound_payment_scid().map(Scid);
let ChannelDetails {
channel_id,
counterparty,
funding_txo,
channel_type: _,
short_channel_id,
outbound_scid_alias: _,
inbound_scid_alias: _,
channel_value_satoshis,
unspendable_punishment_reserve,
user_channel_id,
feerate_sat_per_1000_weight: _,
outbound_capacity_msat,
next_outbound_htlc_limit_msat,
next_outbound_htlc_minimum_msat: _,
inbound_capacity_msat,
confirmations_required: _,
confirmations: _,
force_close_spend_delay,
is_outbound,
is_channel_ready,
channel_shutdown_state: _,
is_usable,
is_announced,
inbound_htlc_minimum_msat,
inbound_htlc_maximum_msat,
config,
pending_inbound_htlcs: _,
pending_outbound_htlcs: _,
funding_redeem_script: _,
} = details;
let channel_id = LxChannelId::from(channel_id);
let user_channel_id = LxUserChannelId::from(user_channel_id);
let scid = short_channel_id.map(Scid);
let funding_txo = funding_txo.map(LxOutPoint::from);
let counterparty_node_id = NodePk(counterparty.node_id);
let channel_value = Amount::try_from_sats_u64(channel_value_satoshis)
.context("Channel value overflow")?;
let punishment_reserve = unspendable_punishment_reserve
.unwrap_or(0)
.apply(Amount::try_from_sats_u64)
.context("Punishment reserve overflow")?;
let is_ready = is_channel_ready;
let outbound_capacity = Amount::from_msat(outbound_capacity_msat);
let next_outbound_htlc_limit =
Amount::from_msat(next_outbound_htlc_limit_msat);
let their_balance = channel_value
.checked_sub(our_balance)
.context("Our balance was higher than the total channel value")?;
let inbound_capacity = Amount::from_msat(inbound_capacity_msat);
let config = config
.context("Missing config")?;
let one_million = dec!(1_000_000);
let our_base_fee = config
.forwarding_fee_base_msat
.apply(u64::from)
.apply(Amount::from_msat);
let our_prop_fee =
Decimal::from(config.forwarding_fee_proportional_millionths)
/ one_million;
let our_cltv_expiry_delta = config.cltv_expiry_delta;
let their_base_fee =
counterparty.forwarding_info.as_ref().map(|info| {
info.fee_base_msat.apply(u64::from).apply(Amount::from_msat)
});
let their_prop_fee =
counterparty.forwarding_info.as_ref().map(|info| {
Decimal::from(info.fee_proportional_millionths) / one_million
});
let their_cltv_expiry_delta = counterparty
.forwarding_info
.as_ref()
.map(|info| info.cltv_expiry_delta);
let inbound_htlc_minimum = inbound_htlc_minimum_msat
.context("Missing inbound_htlc_minimum_msat")?
.apply(Amount::from_msat);
let inbound_htlc_maximum =
inbound_htlc_maximum_msat.map(Amount::from_msat);
let outbound_htlc_minimum = counterparty
.outbound_htlc_minimum_msat
.map(Amount::from_msat);
let outbound_htlc_maximum = counterparty
.outbound_htlc_maximum_msat
.map(Amount::from_msat);
let cpty_supports_basic_mpp =
counterparty.features.supports_basic_mpp();
let cpty_supports_onion_messages =
counterparty.features.supports_onion_messages();
let cpty_supports_wumbo = counterparty.features.supports_wumbo();
let cpty_supports_zero_conf = true;
Ok(Self {
channel_id,
user_channel_id,
scid,
inbound_payment_scid,
outbound_payment_scid,
funding_txo,
counterparty_alias,
counterparty_node_id,
channel_value,
punishment_reserve,
force_close_spend_delay,
is_announced,
is_outbound,
is_ready,
is_usable,
our_balance,
outbound_capacity,
next_outbound_htlc_limit,
their_balance,
inbound_capacity,
our_base_fee,
our_prop_fee,
our_cltv_expiry_delta,
their_base_fee,
their_prop_fee,
their_cltv_expiry_delta,
inbound_htlc_minimum,
inbound_htlc_maximum,
outbound_htlc_minimum,
outbound_htlc_maximum,
cpty_supports_basic_mpp,
cpty_supports_onion_messages,
cpty_supports_wumbo,
cpty_supports_zero_conf,
})
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
#[derive(SerializeDisplay, DeserializeFromStr)]
#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
pub struct LxOutPoint {
pub txid: Txid,
pub index: u16,
}
impl From<OutPoint> for LxOutPoint {
fn from(op: OutPoint) -> Self {
Self {
txid: Txid(op.txid),
index: op.index,
}
}
}
impl From<LxOutPoint> for OutPoint {
fn from(op: LxOutPoint) -> Self {
Self {
txid: op.txid.0,
index: op.index,
}
}
}
impl From<LxOutPoint> for bitcoin::OutPoint {
fn from(op: LxOutPoint) -> Self {
bitcoin::OutPoint {
txid: op.txid.0,
vout: u32::from(op.index),
}
}
}
impl FromStr for LxOutPoint {
type Err = anyhow::Error;
fn from_str(outpoint_str: &str) -> anyhow::Result<Self> {
let mut txid_and_txindex = outpoint_str.split('_');
let txid_str = txid_and_txindex
.next()
.context("Missing <txid> in <txid>_<index>")?;
let index_str = txid_and_txindex
.next()
.context("Missing <index> in <txid>_<index>")?;
let txid = Txid::from_str(txid_str)
.context("Invalid txid returned from DB")?;
let index = u16::from_str(index_str)
.context("Could not parse index into u16")?;
Ok(Self { txid, index })
}
}
impl fmt::Display for LxOutPoint {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}_{}", self.txid, self.index)
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::test_utils::roundtrip;
#[test]
fn outpoint_fromstr_display_roundtrip() {
roundtrip::fromstr_display_roundtrip_proptest::<LxOutPoint>();
}
}