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
207 pub cpty_supports_basic_mpp: bool,
210 pub cpty_supports_onion_messages: bool,
211 pub cpty_supports_wumbo: bool,
212 pub cpty_supports_zero_conf: bool,
213}
214
215impl LxChannelDetails {
216 pub fn from_ldk(
225 details: ChannelDetails,
226 our_balance: Amount,
227 counterparty_alias: Option<String>,
228 ) -> anyhow::Result<Self> {
229 let inbound_payment_scid = details.get_inbound_payment_scid().map(Scid);
230 let outbound_payment_scid =
231 details.get_outbound_payment_scid().map(Scid);
232
233 let ChannelDetails {
236 channel_id,
237 counterparty,
238 funding_txo,
239 channel_type: _,
240 short_channel_id,
241 outbound_scid_alias: _,
242 inbound_scid_alias: _,
243 channel_value_satoshis,
244 unspendable_punishment_reserve,
245 user_channel_id,
246 feerate_sat_per_1000_weight: _,
247 outbound_capacity_msat,
248 next_outbound_htlc_limit_msat,
249 next_outbound_htlc_minimum_msat: _,
250 inbound_capacity_msat,
251 confirmations_required: _,
252 confirmations: _,
253 force_close_spend_delay,
254 is_outbound,
255 is_channel_ready,
256 channel_shutdown_state: _,
257 is_usable,
258 is_announced,
259 inbound_htlc_minimum_msat,
260 inbound_htlc_maximum_msat,
261 config,
262 pending_inbound_htlcs: _,
263 pending_outbound_htlcs: _,
264 funding_redeem_script: _,
265 } = details;
266
267 let channel_id = ChannelId::from(channel_id);
268 let user_channel_id = UserChannelId::from(user_channel_id);
269 let scid = short_channel_id.map(Scid);
270 let funding_txo = funding_txo.map(OutPoint::from);
271 let counterparty_node_id = NodePk(counterparty.node_id);
272 let channel_value = Amount::try_from_sats_u64(channel_value_satoshis)
273 .context("Channel value overflow")?;
274 let punishment_reserve = unspendable_punishment_reserve
275 .unwrap_or(0)
276 .apply(Amount::try_from_sats_u64)
277 .context("Punishment reserve overflow")?;
278 let is_ready = is_channel_ready;
279
280 let outbound_capacity = Amount::from_msat(outbound_capacity_msat);
281 let next_outbound_htlc_limit =
282 Amount::from_msat(next_outbound_htlc_limit_msat);
283
284 let their_balance = channel_value
285 .checked_sub(our_balance)
286 .context("Our balance was higher than the total channel value")?;
287 let inbound_capacity = Amount::from_msat(inbound_capacity_msat);
288
289 let config = config
290 .context("Missing config")?;
292 let one_million = dec!(1_000_000);
293 let our_base_fee = config
294 .forwarding_fee_base_msat
295 .apply(u64::from)
296 .apply(Amount::from_msat);
297 let our_prop_fee =
298 Decimal::from(config.forwarding_fee_proportional_millionths)
299 / one_million;
300 let our_cltv_expiry_delta = config.cltv_expiry_delta;
301 let their_base_fee =
302 counterparty.forwarding_info.as_ref().map(|info| {
303 info.fee_base_msat.apply(u64::from).apply(Amount::from_msat)
304 });
305 let their_prop_fee =
306 counterparty.forwarding_info.as_ref().map(|info| {
307 Decimal::from(info.fee_proportional_millionths) / one_million
308 });
309 let their_cltv_expiry_delta = counterparty
310 .forwarding_info
311 .as_ref()
312 .map(|info| info.cltv_expiry_delta);
313
314 let inbound_htlc_minimum = inbound_htlc_minimum_msat
315 .context("Missing inbound_htlc_minimum_msat")?
317 .apply(Amount::from_msat);
318 let inbound_htlc_maximum =
319 inbound_htlc_maximum_msat.map(Amount::from_msat);
320 let outbound_htlc_minimum = counterparty
321 .outbound_htlc_minimum_msat
322 .map(Amount::from_msat);
323 let outbound_htlc_maximum = counterparty
324 .outbound_htlc_maximum_msat
325 .map(Amount::from_msat);
326
327 let cpty_supports_basic_mpp =
328 counterparty.features.supports_basic_mpp();
329 let cpty_supports_onion_messages =
330 counterparty.features.supports_onion_messages();
331 let cpty_supports_wumbo = counterparty.features.supports_wumbo();
332 let cpty_supports_zero_conf = true;
340
341 Ok(Self {
342 channel_id,
343 user_channel_id,
344 scid,
345 inbound_payment_scid,
346 outbound_payment_scid,
347 funding_txo,
348 counterparty_alias,
349 counterparty_node_id,
350 channel_value,
351 punishment_reserve,
352 force_close_spend_delay,
353 is_announced,
354 is_outbound,
355 is_ready,
356 is_usable,
357 our_balance,
358 outbound_capacity,
359 next_outbound_htlc_limit,
360
361 their_balance,
362 inbound_capacity,
363
364 our_base_fee,
365 our_prop_fee,
366 our_cltv_expiry_delta,
367 their_base_fee,
368 their_prop_fee,
369 their_cltv_expiry_delta,
370
371 inbound_htlc_minimum,
372 inbound_htlc_maximum,
373 outbound_htlc_minimum,
374 outbound_htlc_maximum,
375
376 cpty_supports_basic_mpp,
377 cpty_supports_onion_messages,
378 cpty_supports_wumbo,
379 cpty_supports_zero_conf,
380 })
381 }
382}
383
384#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
395#[derive(SerializeDisplay, DeserializeFromStr)]
396#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
397pub struct OutPoint {
398 pub txid: Txid,
399 pub index: u16,
400}
401
402impl From<lightning::chain::transaction::OutPoint> for OutPoint {
403 fn from(op: lightning::chain::transaction::OutPoint) -> Self {
404 Self {
405 txid: Txid(op.txid),
406 index: op.index,
407 }
408 }
409}
410
411impl From<OutPoint> for lightning::chain::transaction::OutPoint {
412 fn from(op: OutPoint) -> Self {
413 Self {
414 txid: op.txid.0,
415 index: op.index,
416 }
417 }
418}
419
420impl From<OutPoint> for bitcoin::OutPoint {
421 fn from(op: OutPoint) -> Self {
422 bitcoin::OutPoint {
423 txid: op.txid.0,
424 vout: u32::from(op.index),
425 }
426 }
427}
428
429impl FromStr for OutPoint {
431 type Err = anyhow::Error;
432 fn from_str(outpoint_str: &str) -> anyhow::Result<Self> {
433 let mut txid_and_txindex = outpoint_str.split('_');
434 let txid_str = txid_and_txindex
435 .next()
436 .context("Missing <txid> in <txid>_<index>")?;
437 let index_str = txid_and_txindex
438 .next()
439 .context("Missing <index> in <txid>_<index>")?;
440
441 let txid = Txid::from_str(txid_str)
442 .context("Invalid txid returned from DB")?;
443 let index = u16::from_str(index_str)
444 .context("Could not parse index into u16")?;
445
446 Ok(Self { txid, index })
447 }
448}
449
450impl fmt::Display for OutPoint {
452 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
453 write!(f, "{}_{}", self.txid, self.index)
454 }
455}
456
457#[cfg(test)]
458mod test {
459 use super::*;
460 use crate::test_utils::roundtrip;
461
462 #[test]
463 fn outpoint_fromstr_display_roundtrip() {
464 roundtrip::fromstr_display_roundtrip_proptest::<OutPoint>();
465 }
466}