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