1use std::{fmt, str::FromStr};
2
3use anyhow::Context;
4use lexe_byte_array::ByteArray;
5use lexe_crypto::rng::{RngCore, RngExt};
6use lexe_serde::hexstr_or_bytes;
7use lexe_sha256::sha256;
8use lexe_std::Apply;
9use lightning::{
10 chain::transaction::OutPoint,
11 ln::{channel_state::ChannelDetails, types::ChannelId},
12};
13#[cfg(any(test, feature = "test-utils"))]
14use proptest_derive::Arbitrary;
15use ref_cast::RefCast;
16use rust_decimal::Decimal;
17use serde::{Deserialize, Serialize};
18use serde_with::{DeserializeFromStr, SerializeDisplay};
19
20use crate::{
21 api::user::{NodePk, Scid},
22 dec,
23 ln::{amount::Amount, hashes::Txid},
24};
25
26#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
28#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
29#[derive(RefCast, Serialize, Deserialize)]
30#[repr(transparent)]
31pub struct LxChannelId(#[serde(with = "hexstr_or_bytes")] pub [u8; 32]);
32
33lexe_byte_array::impl_byte_array!(LxChannelId, 32);
34lexe_byte_array::impl_fromstr_fromhex!(LxChannelId, 32);
35lexe_byte_array::impl_debug_display_as_hex!(LxChannelId);
36
37impl From<ChannelId> for LxChannelId {
38 fn from(cid: ChannelId) -> Self {
39 Self(cid.0)
40 }
41}
42impl From<LxChannelId> for ChannelId {
43 fn from(cid: LxChannelId) -> Self {
44 Self(cid.0)
45 }
46}
47
48#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
58#[derive(Copy, Clone, Eq, PartialEq, Hash, RefCast, Serialize, Deserialize)]
59#[repr(transparent)]
60pub struct LxUserChannelId(#[serde(with = "hexstr_or_bytes")] pub [u8; 16]);
61
62impl LxUserChannelId {
63 #[inline]
64 pub fn to_u128(self) -> u128 {
65 u128::from_le_bytes(self.0)
66 }
67
68 pub fn from_rng<R: RngCore>(rng: &mut R) -> Self {
69 Self(rng.gen_bytes())
70 }
71
72 pub fn derive_temporary_channel_id(&self) -> LxChannelId {
73 LxChannelId(sha256::digest(&self.0).to_array())
74 }
75}
76
77lexe_byte_array::impl_byte_array!(LxUserChannelId, 16);
78lexe_byte_array::impl_fromstr_fromhex!(LxUserChannelId, 16);
79lexe_byte_array::impl_debug_display_as_hex!(LxUserChannelId);
80
81impl From<u128> for LxUserChannelId {
82 fn from(value: u128) -> Self {
83 Self(value.to_le_bytes())
84 }
85}
86
87impl From<LxUserChannelId> for u128 {
88 fn from(value: LxUserChannelId) -> Self {
89 value.to_u128()
90 }
91}
92
93#[derive(Debug, Serialize, Deserialize)]
96pub struct LxChannelDetails {
97 pub channel_id: LxChannelId,
99 pub user_channel_id: LxUserChannelId,
100 pub scid: Option<Scid>,
107 pub inbound_payment_scid: Option<Scid>,
112 pub outbound_payment_scid: Option<Scid>,
118 pub funding_txo: Option<LxOutPoint>,
119 pub counterparty_alias: Option<String>,
121 pub counterparty_node_id: NodePk,
122 pub channel_value: Amount,
123 pub punishment_reserve: Amount,
126 pub force_close_spend_delay: Option<u16>,
130 pub is_announced: bool,
132 pub is_outbound: bool,
133 pub is_ready: bool,
137 pub is_usable: bool,
140
141 pub our_balance: Amount,
147 pub outbound_capacity: Amount,
151 pub next_outbound_htlc_limit: Amount,
160
161 pub their_balance: Amount,
163 pub inbound_capacity: Amount,
168
169 pub our_base_fee: Amount,
173 pub our_prop_fee: Decimal,
176 pub our_cltv_expiry_delta: u16,
179 pub their_base_fee: Option<Amount>,
181 pub their_prop_fee: Option<Decimal>,
184 pub their_cltv_expiry_delta: Option<u16>,
187
188 pub inbound_htlc_minimum: Amount,
192 pub inbound_htlc_maximum: Option<Amount>,
195 pub outbound_htlc_minimum: Option<Amount>,
199 pub outbound_htlc_maximum: Option<Amount>,
203
204 pub cpty_supports_basic_mpp: bool,
207 pub cpty_supports_onion_messages: bool,
208 pub cpty_supports_wumbo: bool,
209 pub cpty_supports_zero_conf: bool,
210}
211
212impl LxChannelDetails {
213 pub fn from_ldk(
222 details: ChannelDetails,
223 our_balance: Amount,
224 counterparty_alias: Option<String>,
225 ) -> anyhow::Result<Self> {
226 let inbound_payment_scid = details.get_inbound_payment_scid().map(Scid);
227 let outbound_payment_scid =
228 details.get_outbound_payment_scid().map(Scid);
229
230 let ChannelDetails {
233 channel_id,
234 counterparty,
235 funding_txo,
236 channel_type: _,
237 short_channel_id,
238 outbound_scid_alias: _,
239 inbound_scid_alias: _,
240 channel_value_satoshis,
241 unspendable_punishment_reserve,
242 user_channel_id,
243 feerate_sat_per_1000_weight: _,
244 outbound_capacity_msat,
245 next_outbound_htlc_limit_msat,
246 next_outbound_htlc_minimum_msat: _,
247 inbound_capacity_msat,
248 confirmations_required: _,
249 confirmations: _,
250 force_close_spend_delay,
251 is_outbound,
252 is_channel_ready,
253 channel_shutdown_state: _,
254 is_usable,
255 is_announced,
256 inbound_htlc_minimum_msat,
257 inbound_htlc_maximum_msat,
258 config,
259 pending_inbound_htlcs: _,
260 pending_outbound_htlcs: _,
261 funding_redeem_script: _,
262 } = details;
263
264 let channel_id = LxChannelId::from(channel_id);
265 let user_channel_id = LxUserChannelId::from(user_channel_id);
266 let scid = short_channel_id.map(Scid);
267 let funding_txo = funding_txo.map(LxOutPoint::from);
268 let counterparty_node_id = NodePk(counterparty.node_id);
269 let channel_value = Amount::try_from_sats_u64(channel_value_satoshis)
270 .context("Channel value overflow")?;
271 let punishment_reserve = unspendable_punishment_reserve
272 .unwrap_or(0)
273 .apply(Amount::try_from_sats_u64)
274 .context("Punishment reserve overflow")?;
275 let is_ready = is_channel_ready;
276
277 let outbound_capacity = Amount::from_msat(outbound_capacity_msat);
278 let next_outbound_htlc_limit =
279 Amount::from_msat(next_outbound_htlc_limit_msat);
280
281 let their_balance = channel_value
282 .checked_sub(our_balance)
283 .context("Our balance was higher than the total channel value")?;
284 let inbound_capacity = Amount::from_msat(inbound_capacity_msat);
285
286 let config = config
287 .context("Missing config")?;
289 let one_million = dec!(1_000_000);
290 let our_base_fee = config
291 .forwarding_fee_base_msat
292 .apply(u64::from)
293 .apply(Amount::from_msat);
294 let our_prop_fee =
295 Decimal::from(config.forwarding_fee_proportional_millionths)
296 / one_million;
297 let our_cltv_expiry_delta = config.cltv_expiry_delta;
298 let their_base_fee =
299 counterparty.forwarding_info.as_ref().map(|info| {
300 info.fee_base_msat.apply(u64::from).apply(Amount::from_msat)
301 });
302 let their_prop_fee =
303 counterparty.forwarding_info.as_ref().map(|info| {
304 Decimal::from(info.fee_proportional_millionths) / one_million
305 });
306 let their_cltv_expiry_delta = counterparty
307 .forwarding_info
308 .as_ref()
309 .map(|info| info.cltv_expiry_delta);
310
311 let inbound_htlc_minimum = inbound_htlc_minimum_msat
312 .context("Missing inbound_htlc_minimum_msat")?
314 .apply(Amount::from_msat);
315 let inbound_htlc_maximum =
316 inbound_htlc_maximum_msat.map(Amount::from_msat);
317 let outbound_htlc_minimum = counterparty
318 .outbound_htlc_minimum_msat
319 .map(Amount::from_msat);
320 let outbound_htlc_maximum = counterparty
321 .outbound_htlc_maximum_msat
322 .map(Amount::from_msat);
323
324 let cpty_supports_basic_mpp =
325 counterparty.features.supports_basic_mpp();
326 let cpty_supports_onion_messages =
327 counterparty.features.supports_onion_messages();
328 let cpty_supports_wumbo = counterparty.features.supports_wumbo();
329 let cpty_supports_zero_conf = true;
337
338 Ok(Self {
339 channel_id,
340 user_channel_id,
341 scid,
342 inbound_payment_scid,
343 outbound_payment_scid,
344 funding_txo,
345 counterparty_alias,
346 counterparty_node_id,
347 channel_value,
348 punishment_reserve,
349 force_close_spend_delay,
350 is_announced,
351 is_outbound,
352 is_ready,
353 is_usable,
354 our_balance,
355 outbound_capacity,
356 next_outbound_htlc_limit,
357
358 their_balance,
359 inbound_capacity,
360
361 our_base_fee,
362 our_prop_fee,
363 our_cltv_expiry_delta,
364 their_base_fee,
365 their_prop_fee,
366 their_cltv_expiry_delta,
367
368 inbound_htlc_minimum,
369 inbound_htlc_maximum,
370 outbound_htlc_minimum,
371 outbound_htlc_maximum,
372
373 cpty_supports_basic_mpp,
374 cpty_supports_onion_messages,
375 cpty_supports_wumbo,
376 cpty_supports_zero_conf,
377 })
378 }
379}
380
381#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
388#[derive(SerializeDisplay, DeserializeFromStr)]
389#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
390pub struct LxOutPoint {
391 pub txid: Txid,
392 pub index: u16,
393}
394
395impl From<OutPoint> for LxOutPoint {
396 fn from(op: OutPoint) -> Self {
397 Self {
398 txid: Txid(op.txid),
399 index: op.index,
400 }
401 }
402}
403
404impl From<LxOutPoint> for OutPoint {
405 fn from(op: LxOutPoint) -> Self {
406 Self {
407 txid: op.txid.0,
408 index: op.index,
409 }
410 }
411}
412
413impl From<LxOutPoint> for bitcoin::OutPoint {
414 fn from(op: LxOutPoint) -> Self {
415 bitcoin::OutPoint {
416 txid: op.txid.0,
417 vout: u32::from(op.index),
418 }
419 }
420}
421
422impl FromStr for LxOutPoint {
424 type Err = anyhow::Error;
425 fn from_str(outpoint_str: &str) -> anyhow::Result<Self> {
426 let mut txid_and_txindex = outpoint_str.split('_');
427 let txid_str = txid_and_txindex
428 .next()
429 .context("Missing <txid> in <txid>_<index>")?;
430 let index_str = txid_and_txindex
431 .next()
432 .context("Missing <index> in <txid>_<index>")?;
433
434 let txid = Txid::from_str(txid_str)
435 .context("Invalid txid returned from DB")?;
436 let index = u16::from_str(index_str)
437 .context("Could not parse index into u16")?;
438
439 Ok(Self { txid, index })
440 }
441}
442
443impl fmt::Display for LxOutPoint {
445 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
446 write!(f, "{}_{}", self.txid, self.index)
447 }
448}
449
450#[cfg(test)]
451mod test {
452 use super::*;
453 use crate::test_utils::roundtrip;
454
455 #[test]
456 fn outpoint_fromstr_display_roundtrip() {
457 roundtrip::fromstr_display_roundtrip_proptest::<LxOutPoint>();
458 }
459}