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::ln::channel_state::ChannelDetails;
10#[cfg(any(test, feature = "test-utils"))]
11use proptest_derive::Arbitrary;
12use ref_cast::RefCast;
13use rust_decimal::Decimal;
14use serde::{Deserialize, Serialize};
15use serde_with::{DeserializeFromStr, SerializeDisplay};
16
17use crate::{
18 api::user::{NodePk, Scid},
19 dec,
20 ln::{amount::Amount, hashes::Txid},
21};
22
23#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
27#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
28#[derive(RefCast, Serialize, Deserialize)]
29#[repr(transparent)]
30pub struct ChannelId(#[serde(with = "hexstr_or_bytes")] pub [u8; 32]);
31
32lexe_byte_array::impl_byte_array!(ChannelId, 32);
33lexe_byte_array::impl_fromstr_fromhex!(ChannelId, 32);
34lexe_byte_array::impl_debug_display_as_hex!(ChannelId);
35
36impl From<lightning::ln::types::ChannelId> for ChannelId {
37 fn from(cid: lightning::ln::types::ChannelId) -> Self {
38 Self(cid.0)
39 }
40}
41impl From<ChannelId> for lightning::ln::types::ChannelId {
42 fn from(cid: ChannelId) -> Self {
43 Self(cid.0)
44 }
45}
46
47#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
61#[derive(Copy, Clone, Eq, PartialEq, Hash, RefCast, Serialize, Deserialize)]
62#[repr(transparent)]
63pub struct UserChannelId(#[serde(with = "hexstr_or_bytes")] pub [u8; 16]);
64
65impl UserChannelId {
66 #[inline]
67 pub fn to_u128(self) -> u128 {
68 u128::from_le_bytes(self.0)
69 }
70
71 pub fn from_rng<R: RngCore>(rng: &mut R) -> Self {
72 Self(rng.gen_bytes())
73 }
74
75 pub fn derive_temporary_channel_id(&self) -> ChannelId {
76 ChannelId(sha256::digest(&self.0).to_array())
77 }
78}
79
80lexe_byte_array::impl_byte_array!(UserChannelId, 16);
81lexe_byte_array::impl_fromstr_fromhex!(UserChannelId, 16);
82lexe_byte_array::impl_debug_display_as_hex!(UserChannelId);
83
84impl From<u128> for UserChannelId {
85 fn from(value: u128) -> Self {
86 Self(value.to_le_bytes())
87 }
88}
89
90impl From<UserChannelId> for u128 {
91 fn from(value: UserChannelId) -> Self {
92 value.to_u128()
93 }
94}
95
96#[derive(Debug, Serialize, Deserialize)]
99pub struct LxChannelDetails {
100 pub channel_id: ChannelId,
102 pub user_channel_id: UserChannelId,
103 pub scid: Option<Scid>,
110 pub inbound_payment_scid: Option<Scid>,
115 pub outbound_payment_scid: Option<Scid>,
121 pub funding_txo: Option<OutPoint>,
122 pub counterparty_alias: Option<String>,
124 pub counterparty_node_id: NodePk,
125 pub channel_value: Amount,
126 pub punishment_reserve: Amount,
129 pub force_close_spend_delay: Option<u16>,
133 pub is_announced: bool,
135 pub is_outbound: bool,
136 pub is_ready: bool,
140 pub is_usable: bool,
143
144 pub our_balance: Amount,
150 pub outbound_capacity: Amount,
154 pub next_outbound_htlc_limit: Amount,
163
164 pub their_balance: Amount,
166 pub inbound_capacity: Amount,
171
172 pub our_base_fee: Amount,
176 pub our_prop_fee: Decimal,
179 pub our_cltv_expiry_delta: u16,
182 pub their_base_fee: Option<Amount>,
184 pub their_prop_fee: Option<Decimal>,
187 pub their_cltv_expiry_delta: Option<u16>,
190
191 pub inbound_htlc_minimum: Amount,
195 pub inbound_htlc_maximum: Option<Amount>,
198 pub outbound_htlc_minimum: Option<Amount>,
202 pub outbound_htlc_maximum: Option<Amount>,
206 pub current_dust_exposure: Option<Amount>,
213
214 pub cpty_supports_basic_mpp: bool,
217 pub cpty_supports_onion_messages: bool,
218 pub cpty_supports_wumbo: bool,
219 pub cpty_supports_zero_conf: bool,
220}
221
222impl LxChannelDetails {
223 pub fn from_ldk(
232 details: ChannelDetails,
233 our_balance: Amount,
234 counterparty_alias: Option<String>,
235 ) -> anyhow::Result<Self> {
236 let inbound_payment_scid = details.get_inbound_payment_scid().map(Scid);
237 let outbound_payment_scid =
238 details.get_outbound_payment_scid().map(Scid);
239
240 let ChannelDetails {
243 channel_id,
244 counterparty,
245 funding_txo,
246 channel_type: _,
247 short_channel_id,
248 outbound_scid_alias: _,
249 inbound_scid_alias: _,
250 channel_value_satoshis,
251 unspendable_punishment_reserve,
252 user_channel_id,
253 feerate_sat_per_1000_weight: _,
254 outbound_capacity_msat,
255 next_outbound_htlc_limit_msat,
256 next_outbound_htlc_minimum_msat: _,
257 next_splice_out_maximum_sat: _,
258 inbound_capacity_msat,
259 confirmations_required: _,
260 confirmations: _,
261 force_close_spend_delay,
262 is_outbound,
263 is_channel_ready,
264 channel_shutdown_state: _,
265 is_usable,
266 is_announced,
267 inbound_htlc_minimum_msat,
268 inbound_htlc_maximum_msat,
269 config,
270 pending_inbound_htlcs: _,
271 pending_outbound_htlcs: _,
272 funding_redeem_script: _,
273 current_dust_exposure_msat,
274 splice_details: _,
275 } = details;
276
277 let channel_id = ChannelId::from(channel_id);
278 let user_channel_id = UserChannelId::from(user_channel_id);
279 let scid = short_channel_id.map(Scid);
280 let funding_txo = funding_txo.map(OutPoint::from);
281 let counterparty_node_id = NodePk(counterparty.node_id);
282 let channel_value = Amount::try_from_sats_u64(channel_value_satoshis)
283 .context("Channel value overflow")?;
284 let punishment_reserve = unspendable_punishment_reserve
285 .unwrap_or(0)
286 .apply(Amount::try_from_sats_u64)
287 .context("Punishment reserve overflow")?;
288 let is_ready = is_channel_ready;
289
290 let outbound_capacity = Amount::from_msat(outbound_capacity_msat);
291 let next_outbound_htlc_limit =
292 Amount::from_msat(next_outbound_htlc_limit_msat);
293
294 let their_balance = channel_value
295 .checked_sub(our_balance)
296 .context("Our balance was higher than the total channel value")?;
297 let inbound_capacity = Amount::from_msat(inbound_capacity_msat);
298
299 let config = config
300 .context("Missing config")?;
302 let one_million = dec!(1_000_000);
303 let our_base_fee = config
304 .forwarding_fee_base_msat
305 .apply(u64::from)
306 .apply(Amount::from_msat);
307 let our_prop_fee =
308 Decimal::from(config.forwarding_fee_proportional_millionths)
309 / one_million;
310 let our_cltv_expiry_delta = config.cltv_expiry_delta;
311 let their_base_fee =
312 counterparty.forwarding_info.as_ref().map(|info| {
313 info.fee_base_msat.apply(u64::from).apply(Amount::from_msat)
314 });
315 let their_prop_fee =
316 counterparty.forwarding_info.as_ref().map(|info| {
317 Decimal::from(info.fee_proportional_millionths) / one_million
318 });
319 let their_cltv_expiry_delta = counterparty
320 .forwarding_info
321 .as_ref()
322 .map(|info| info.cltv_expiry_delta);
323
324 let inbound_htlc_minimum = inbound_htlc_minimum_msat
325 .context("Missing inbound_htlc_minimum_msat")?
327 .apply(Amount::from_msat);
328 let inbound_htlc_maximum =
329 inbound_htlc_maximum_msat.map(Amount::from_msat);
330 let outbound_htlc_minimum = counterparty
331 .outbound_htlc_minimum_msat
332 .map(Amount::from_msat);
333 let outbound_htlc_maximum = counterparty
334 .outbound_htlc_maximum_msat
335 .map(Amount::from_msat);
336 let current_dust_exposure =
337 current_dust_exposure_msat.map(Amount::from_msat);
338
339 let cpty_supports_basic_mpp =
340 counterparty.features.supports_basic_mpp();
341 let cpty_supports_onion_messages =
342 counterparty.features.supports_onion_messages();
343 let cpty_supports_wumbo = counterparty.features.supports_wumbo();
344 let cpty_supports_zero_conf =
345 counterparty.features.supports_zero_conf();
346
347 Ok(Self {
348 channel_id,
349 user_channel_id,
350 scid,
351 inbound_payment_scid,
352 outbound_payment_scid,
353 funding_txo,
354 counterparty_alias,
355 counterparty_node_id,
356 channel_value,
357 punishment_reserve,
358 force_close_spend_delay,
359 is_announced,
360 is_outbound,
361 is_ready,
362 is_usable,
363 our_balance,
364 outbound_capacity,
365 next_outbound_htlc_limit,
366
367 their_balance,
368 inbound_capacity,
369
370 our_base_fee,
371 our_prop_fee,
372 our_cltv_expiry_delta,
373 their_base_fee,
374 their_prop_fee,
375 their_cltv_expiry_delta,
376
377 inbound_htlc_minimum,
378 inbound_htlc_maximum,
379 outbound_htlc_minimum,
380 outbound_htlc_maximum,
381 current_dust_exposure,
382
383 cpty_supports_basic_mpp,
384 cpty_supports_onion_messages,
385 cpty_supports_wumbo,
386 cpty_supports_zero_conf,
387 })
388 }
389}
390
391#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
402#[derive(SerializeDisplay, DeserializeFromStr)]
403#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
404pub struct OutPoint {
405 pub txid: Txid,
406 pub index: u16,
407}
408
409impl From<lightning::chain::transaction::OutPoint> for OutPoint {
410 fn from(op: lightning::chain::transaction::OutPoint) -> Self {
411 Self {
412 txid: Txid(op.txid),
413 index: op.index,
414 }
415 }
416}
417
418impl From<OutPoint> for lightning::chain::transaction::OutPoint {
419 fn from(op: OutPoint) -> Self {
420 Self {
421 txid: op.txid.0,
422 index: op.index,
423 }
424 }
425}
426
427impl From<OutPoint> for bitcoin::OutPoint {
428 fn from(op: OutPoint) -> Self {
429 bitcoin::OutPoint {
430 txid: op.txid.0,
431 vout: u32::from(op.index),
432 }
433 }
434}
435
436impl FromStr for OutPoint {
438 type Err = anyhow::Error;
439 fn from_str(outpoint_str: &str) -> anyhow::Result<Self> {
440 let mut txid_and_txindex = outpoint_str.split('_');
441 let txid_str = txid_and_txindex
442 .next()
443 .context("Missing <txid> in <txid>_<index>")?;
444 let index_str = txid_and_txindex
445 .next()
446 .context("Missing <index> in <txid>_<index>")?;
447
448 let txid = Txid::from_str(txid_str)
449 .context("Invalid txid returned from DB")?;
450 let index = u16::from_str(index_str)
451 .context("Could not parse index into u16")?;
452
453 Ok(Self { txid, index })
454 }
455}
456
457impl fmt::Display for OutPoint {
463 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
464 write!(f, "{}_{}", self.txid, self.index)
465 }
466}
467
468#[cfg(test)]
469mod test {
470 use super::*;
471 use crate::test_utils::roundtrip;
472
473 #[test]
474 fn outpoint_fromstr_display_roundtrip() {
475 roundtrip::fromstr_display_roundtrip_proptest::<OutPoint>();
476 }
477}