1#![deny(clippy::pedantic)]
2#![allow(clippy::doc_markdown)]
3#![allow(clippy::missing_errors_doc)]
4#![allow(clippy::missing_panics_doc)]
5#![allow(clippy::module_name_repetitions)]
6#![allow(clippy::must_use_candidate)]
7
8pub mod client;
18pub mod config;
19pub mod contracts;
20pub mod federation_endpoint_constants;
21pub mod gateway_endpoint_constants;
22
23use std::collections::BTreeMap;
24use std::io::{Error, Read, Write};
25use std::time::{Duration, SystemTime};
26
27use anyhow::Context as AnyhowContext;
28use bitcoin::hashes::{Hash, sha256};
29use config::LightningClientConfig;
30use fedimint_core::core::{Decoder, ModuleInstanceId, ModuleKind};
31use fedimint_core::encoding::{Decodable, DecodeError, Encodable};
32use fedimint_core::module::registry::ModuleDecoderRegistry;
33use fedimint_core::module::{CommonModuleInit, ModuleCommon, ModuleConsensusVersion};
34use fedimint_core::secp256k1::Message;
35use fedimint_core::util::SafeUrl;
36use fedimint_core::{
37 Amount, PeerId, encode_bolt11_invoice_features_without_length,
38 extensible_associated_module_type, plugin_types_trait_impl_common, secp256k1,
39};
40use lightning_invoice::{Bolt11Invoice, RoutingFees};
41pub use reqwest::Method;
42use secp256k1::schnorr::Signature;
43use serde::{Deserialize, Serialize};
44use thiserror::Error;
45use threshold_crypto::PublicKey;
46use tracing::error;
47pub use {bitcoin, lightning_invoice};
48
49use crate::contracts::incoming::OfferId;
50use crate::contracts::{Contract, ContractId, ContractOutcome, Preimage, PreimageDecryptionShare};
51use crate::route_hints::RouteHint;
52
53pub const KIND: ModuleKind = ModuleKind::from_static_str("ln");
54pub const MODULE_CONSENSUS_VERSION: ModuleConsensusVersion = ModuleConsensusVersion::new(2, 0);
55
56extensible_associated_module_type!(
57 LightningInput,
58 LightningInputV0,
59 UnknownLightningInputVariantError
60);
61
62impl LightningInput {
63 pub fn new_v0(
64 contract_id: ContractId,
65 amount: Amount,
66 witness: Option<Preimage>,
67 ) -> LightningInput {
68 LightningInput::V0(LightningInputV0 {
69 contract_id,
70 amount,
71 witness,
72 })
73 }
74}
75
76#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Encodable, Decodable)]
77pub struct LightningInputV0 {
78 pub contract_id: contracts::ContractId,
79 pub amount: Amount,
82 pub witness: Option<Preimage>,
86}
87
88impl std::fmt::Display for LightningInputV0 {
89 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90 write!(
91 f,
92 "Lightning Contract {} with amount {}",
93 self.contract_id, self.amount
94 )
95 }
96}
97
98extensible_associated_module_type!(
99 LightningOutput,
100 LightningOutputV0,
101 UnknownLightningOutputVariantError
102);
103
104impl LightningOutput {
105 pub fn new_v0_contract(contract: ContractOutput) -> LightningOutput {
106 LightningOutput::V0(LightningOutputV0::Contract(contract))
107 }
108
109 pub fn new_v0_offer(offer: contracts::incoming::IncomingContractOffer) -> LightningOutput {
110 LightningOutput::V0(LightningOutputV0::Offer(offer))
111 }
112
113 pub fn new_v0_cancel_outgoing(
114 contract: ContractId,
115 gateway_signature: secp256k1::schnorr::Signature,
116 ) -> LightningOutput {
117 LightningOutput::V0(LightningOutputV0::CancelOutgoing {
118 contract,
119 gateway_signature,
120 })
121 }
122}
123
124#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Encodable, Decodable)]
137pub enum LightningOutputV0 {
138 Contract(ContractOutput),
140 Offer(contracts::incoming::IncomingContractOffer),
142 CancelOutgoing {
144 contract: ContractId,
146 gateway_signature: fedimint_core::secp256k1::schnorr::Signature,
148 },
149}
150
151impl std::fmt::Display for LightningOutputV0 {
152 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
153 match self {
154 LightningOutputV0::Contract(ContractOutput { amount, contract }) => match contract {
155 Contract::Incoming(incoming) => {
156 write!(
157 f,
158 "LN Incoming Contract for {} hash {}",
159 amount, incoming.hash
160 )
161 }
162 Contract::Outgoing(outgoing) => {
163 write!(
164 f,
165 "LN Outgoing Contract for {} hash {}",
166 amount, outgoing.hash
167 )
168 }
169 },
170 LightningOutputV0::Offer(offer) => {
171 write!(f, "LN offer for {} with hash {}", offer.amount, offer.hash)
172 }
173 LightningOutputV0::CancelOutgoing { contract, .. } => {
174 write!(f, "LN outgoing contract cancellation {contract}")
175 }
176 }
177 }
178}
179
180#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Encodable, Decodable)]
181pub struct ContractOutput {
182 pub amount: fedimint_core::Amount,
183 pub contract: contracts::Contract,
184}
185
186#[derive(Debug, Eq, PartialEq, Hash, Encodable, Decodable, Serialize, Deserialize, Clone)]
187pub struct ContractAccount {
188 pub amount: fedimint_core::Amount,
189 pub contract: contracts::FundedContract,
190}
191
192extensible_associated_module_type!(
193 LightningOutputOutcome,
194 LightningOutputOutcomeV0,
195 UnknownLightningOutputOutcomeVariantError
196);
197
198impl LightningOutputOutcome {
199 pub fn new_v0_contract(id: ContractId, outcome: ContractOutcome) -> LightningOutputOutcome {
200 LightningOutputOutcome::V0(LightningOutputOutcomeV0::Contract { id, outcome })
201 }
202
203 pub fn new_v0_offer(id: OfferId) -> LightningOutputOutcome {
204 LightningOutputOutcome::V0(LightningOutputOutcomeV0::Offer { id })
205 }
206
207 pub fn new_v0_cancel_outgoing(id: ContractId) -> LightningOutputOutcome {
208 LightningOutputOutcome::V0(LightningOutputOutcomeV0::CancelOutgoingContract { id })
209 }
210}
211
212#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Encodable, Decodable)]
213pub enum LightningOutputOutcomeV0 {
214 Contract {
215 id: ContractId,
216 outcome: ContractOutcome,
217 },
218 Offer {
219 id: OfferId,
220 },
221 CancelOutgoingContract {
222 id: ContractId,
223 },
224}
225
226impl LightningOutputOutcomeV0 {
227 pub fn is_permanent(&self) -> bool {
228 match self {
229 LightningOutputOutcomeV0::Contract { id: _, outcome } => outcome.is_permanent(),
230 LightningOutputOutcomeV0::Offer { .. }
231 | LightningOutputOutcomeV0::CancelOutgoingContract { .. } => true,
232 }
233 }
234}
235
236impl std::fmt::Display for LightningOutputOutcomeV0 {
237 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
238 match self {
239 LightningOutputOutcomeV0::Contract { id, .. } => {
240 write!(f, "LN Contract {id}")
241 }
242 LightningOutputOutcomeV0::Offer { id } => {
243 write!(f, "LN Offer {id}")
244 }
245 LightningOutputOutcomeV0::CancelOutgoingContract { id: contract_id } => {
246 write!(f, "LN Outgoing Contract Cancellation {contract_id}")
247 }
248 }
249 }
250}
251
252#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
258pub struct LightningGatewayRegistration {
259 pub info: LightningGateway,
260 pub vetted: bool,
262 pub valid_until: SystemTime,
265}
266
267impl Encodable for LightningGatewayRegistration {
268 fn consensus_encode<W: Write>(&self, writer: &mut W) -> Result<(), Error> {
269 let json_repr = serde_json::to_string(self).map_err(|e| {
270 Error::other(format!(
271 "Failed to serialize LightningGatewayRegistration: {e}"
272 ))
273 })?;
274
275 json_repr.consensus_encode(writer)
276 }
277}
278
279impl Decodable for LightningGatewayRegistration {
280 fn consensus_decode_partial<R: Read>(
281 r: &mut R,
282 modules: &ModuleDecoderRegistry,
283 ) -> Result<Self, DecodeError> {
284 let json_repr = String::consensus_decode_partial(r, modules)?;
285 serde_json::from_str(&json_repr).map_err(|e| {
286 DecodeError::new_custom(
287 anyhow::Error::new(e).context("Failed to deserialize LightningGatewayRegistration"),
288 )
289 })
290 }
291}
292
293impl LightningGatewayRegistration {
294 pub fn unanchor(self) -> LightningGatewayAnnouncement {
299 LightningGatewayAnnouncement {
300 info: self.info,
301 ttl: self
302 .valid_until
303 .duration_since(fedimint_core::time::now())
304 .unwrap_or_default(),
305 vetted: self.vetted,
306 }
307 }
308
309 pub fn is_expired(&self) -> bool {
310 self.valid_until < fedimint_core::time::now()
311 }
312}
313
314#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
322pub struct LightningGatewayAnnouncement {
323 pub info: LightningGateway,
324 pub vetted: bool,
326 pub ttl: Duration,
330}
331
332impl LightningGatewayAnnouncement {
333 pub fn anchor(self) -> LightningGatewayRegistration {
336 LightningGatewayRegistration {
337 info: self.info,
338 vetted: self.vetted,
339 valid_until: fedimint_core::time::now() + self.ttl,
340 }
341 }
342}
343
344#[derive(Debug, Clone, Serialize, Deserialize, Encodable, Decodable, PartialEq, Eq, Hash)]
346pub struct LightningGateway {
347 #[serde(rename = "mint_channel_id")]
352 pub federation_index: u64,
353 pub gateway_redeem_key: fedimint_core::secp256k1::PublicKey,
355 pub node_pub_key: fedimint_core::secp256k1::PublicKey,
356 pub lightning_alias: String,
357 pub api: SafeUrl,
360 pub route_hints: Vec<route_hints::RouteHint>,
365 #[serde(with = "serde_routing_fees")]
367 pub fees: RoutingFees,
368 pub gateway_id: secp256k1::PublicKey,
369 pub supports_private_payments: bool,
371}
372
373#[derive(Debug, Clone, PartialEq, Eq, Hash, Encodable, Decodable, Serialize, Deserialize)]
374pub enum LightningConsensusItem {
375 DecryptPreimage(ContractId, PreimageDecryptionShare),
376 BlockCount(u64),
377 #[encodable_default]
378 Default {
379 variant: u64,
380 bytes: Vec<u8>,
381 },
382}
383
384impl std::fmt::Display for LightningConsensusItem {
385 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
386 match self {
387 LightningConsensusItem::DecryptPreimage(contract_id, _) => {
388 write!(f, "LN Decryption Share - contract_id: {contract_id}")
389 }
390 LightningConsensusItem::BlockCount(count) => write!(f, "LN Block Count {count}"),
391 LightningConsensusItem::Default { variant, .. } => {
392 write!(f, "LN Unknown - variant={variant}")
393 }
394 }
395 }
396}
397
398#[derive(Debug)]
399pub struct LightningCommonInit;
400
401impl CommonModuleInit for LightningCommonInit {
402 const CONSENSUS_VERSION: ModuleConsensusVersion = MODULE_CONSENSUS_VERSION;
403 const KIND: ModuleKind = KIND;
404
405 type ClientConfig = LightningClientConfig;
406
407 fn decoder() -> Decoder {
408 LightningModuleTypes::decoder()
409 }
410}
411
412pub struct LightningModuleTypes;
413
414plugin_types_trait_impl_common!(
415 KIND,
416 LightningModuleTypes,
417 LightningClientConfig,
418 LightningInput,
419 LightningOutput,
420 LightningOutputOutcome,
421 LightningConsensusItem,
422 LightningInputError,
423 LightningOutputError
424);
425
426pub mod route_hints {
429 use fedimint_core::encoding::{Decodable, Encodable};
430 use fedimint_core::secp256k1::PublicKey;
431 use lightning_invoice::RoutingFees;
432 use serde::{Deserialize, Serialize};
433
434 #[derive(Clone, Debug, Hash, Eq, PartialEq, Serialize, Deserialize, Encodable, Decodable)]
435 pub struct RouteHintHop {
436 pub src_node_id: PublicKey,
438 pub short_channel_id: u64,
440 pub base_msat: u32,
442 pub proportional_millionths: u32,
445 pub cltv_expiry_delta: u16,
447 pub htlc_minimum_msat: Option<u64>,
449 pub htlc_maximum_msat: Option<u64>,
451 }
452
453 #[derive(Clone, Debug, Hash, Eq, PartialEq, Serialize, Deserialize, Encodable, Decodable)]
456 pub struct RouteHint(pub Vec<RouteHintHop>);
457
458 impl RouteHint {
459 pub fn to_ldk_route_hint(&self) -> lightning_invoice::RouteHint {
460 lightning_invoice::RouteHint(
461 self.0
462 .iter()
463 .map(|hop| lightning_invoice::RouteHintHop {
464 src_node_id: hop.src_node_id,
465 short_channel_id: hop.short_channel_id,
466 fees: RoutingFees {
467 base_msat: hop.base_msat,
468 proportional_millionths: hop.proportional_millionths,
469 },
470 cltv_expiry_delta: hop.cltv_expiry_delta,
471 htlc_minimum_msat: hop.htlc_minimum_msat,
472 htlc_maximum_msat: hop.htlc_maximum_msat,
473 })
474 .collect(),
475 )
476 }
477 }
478
479 impl From<lightning_invoice::RouteHint> for RouteHint {
480 fn from(rh: lightning_invoice::RouteHint) -> Self {
481 RouteHint(rh.0.into_iter().map(Into::into).collect())
482 }
483 }
484
485 impl From<lightning_invoice::RouteHintHop> for RouteHintHop {
486 fn from(rhh: lightning_invoice::RouteHintHop) -> Self {
487 RouteHintHop {
488 src_node_id: rhh.src_node_id,
489 short_channel_id: rhh.short_channel_id,
490 base_msat: rhh.fees.base_msat,
491 proportional_millionths: rhh.fees.proportional_millionths,
492 cltv_expiry_delta: rhh.cltv_expiry_delta,
493 htlc_minimum_msat: rhh.htlc_minimum_msat,
494 htlc_maximum_msat: rhh.htlc_maximum_msat,
495 }
496 }
497 }
498}
499
500pub mod serde_routing_fees {
504 use lightning_invoice::RoutingFees;
505 use serde::ser::SerializeStruct;
506 use serde::{Deserialize, Deserializer, Serializer};
507
508 #[allow(missing_docs)]
509 pub fn serialize<S>(fees: &RoutingFees, serializer: S) -> Result<S::Ok, S::Error>
510 where
511 S: Serializer,
512 {
513 let mut state = serializer.serialize_struct("RoutingFees", 2)?;
514 state.serialize_field("base_msat", &fees.base_msat)?;
515 state.serialize_field("proportional_millionths", &fees.proportional_millionths)?;
516 state.end()
517 }
518
519 #[allow(missing_docs)]
520 pub fn deserialize<'de, D>(deserializer: D) -> Result<RoutingFees, D::Error>
521 where
522 D: Deserializer<'de>,
523 {
524 let fees = serde_json::Value::deserialize(deserializer)?;
525 let base_msat = fees["base_msat"]
527 .as_u64()
528 .ok_or_else(|| serde::de::Error::custom("base_msat is not a u64"))?;
529 let proportional_millionths = fees["proportional_millionths"]
530 .as_u64()
531 .ok_or_else(|| serde::de::Error::custom("proportional_millionths is not a u64"))?;
532
533 Ok(RoutingFees {
534 base_msat: base_msat
535 .try_into()
536 .map_err(|_| serde::de::Error::custom("base_msat is greater than u32::MAX"))?,
537 proportional_millionths: proportional_millionths.try_into().map_err(|_| {
538 serde::de::Error::custom("proportional_millionths is greater than u32::MAX")
539 })?,
540 })
541 }
542}
543
544pub mod serde_option_routing_fees {
545 use lightning_invoice::RoutingFees;
546 use serde::ser::SerializeStruct;
547 use serde::{Deserialize, Deserializer, Serializer};
548
549 #[allow(missing_docs)]
550 pub fn serialize<S>(fees: &Option<RoutingFees>, serializer: S) -> Result<S::Ok, S::Error>
551 where
552 S: Serializer,
553 {
554 if let Some(fees) = fees {
555 let mut state = serializer.serialize_struct("RoutingFees", 2)?;
556 state.serialize_field("base_msat", &fees.base_msat)?;
557 state.serialize_field("proportional_millionths", &fees.proportional_millionths)?;
558 state.end()
559 } else {
560 let state = serializer.serialize_struct("RoutingFees", 0)?;
561 state.end()
562 }
563 }
564
565 #[allow(missing_docs)]
566 pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<RoutingFees>, D::Error>
567 where
568 D: Deserializer<'de>,
569 {
570 let fees = serde_json::Value::deserialize(deserializer)?;
571 let base_msat = fees["base_msat"].as_u64();
573
574 if let Some(base_msat) = base_msat
575 && let Some(proportional_millionths) = fees["proportional_millionths"].as_u64()
576 {
577 let base_msat: u32 = base_msat
578 .try_into()
579 .map_err(|_| serde::de::Error::custom("base_msat is greater than u32::MAX"))?;
580 let proportional_millionths: u32 =
581 proportional_millionths.try_into().map_err(|_| {
582 serde::de::Error::custom("proportional_millionths is greater than u32::MAX")
583 })?;
584 return Ok(Some(RoutingFees {
585 base_msat,
586 proportional_millionths,
587 }));
588 }
589
590 Ok(None)
591 }
592}
593
594#[derive(Debug, Error, Eq, PartialEq, Encodable, Decodable, Hash, Clone)]
595pub enum LightningInputError {
596 #[error("The input contract {0} does not exist")]
597 UnknownContract(ContractId),
598 #[error("The input contract has too little funds, got {0}, input spends {1}")]
599 InsufficientFunds(Amount, Amount),
600 #[error("An outgoing LN contract spend did not provide a preimage")]
601 MissingPreimage,
602 #[error("An outgoing LN contract spend provided a wrong preimage")]
603 InvalidPreimage,
604 #[error("Incoming contract not ready to be spent yet, decryption in progress")]
605 ContractNotReady,
606 #[error("The lightning input version is not supported by this federation")]
607 UnknownInputVariant(#[from] UnknownLightningInputVariantError),
608}
609
610#[derive(Debug, Error, Eq, PartialEq, Encodable, Decodable, Hash, Clone)]
611pub enum LightningOutputError {
612 #[error("The input contract {0} does not exist")]
613 UnknownContract(ContractId),
614 #[error("Output contract value may not be zero unless it's an offer output")]
615 ZeroOutput,
616 #[error("Offer contains invalid threshold-encrypted data")]
617 InvalidEncryptedPreimage,
618 #[error("Offer contains a ciphertext that has already been used")]
619 DuplicateEncryptedPreimage,
620 #[error("The incoming LN account requires more funding (need {0} got {1})")]
621 InsufficientIncomingFunding(Amount, Amount),
622 #[error("No offer found for payment hash {0}")]
623 NoOffer(bitcoin::secp256k1::hashes::sha256::Hash),
624 #[error("Only outgoing contracts support cancellation")]
625 NotOutgoingContract,
626 #[error("Cancellation request wasn't properly signed")]
627 InvalidCancellationSignature,
628 #[error("The lightning output version is not supported by this federation")]
629 UnknownOutputVariant(#[from] UnknownLightningOutputVariantError),
630}
631
632#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize, Decodable, Encodable)]
637pub struct PrunedInvoice {
638 pub amount: Amount,
639 pub destination: secp256k1::PublicKey,
640 #[serde(with = "fedimint_core::hex::serde", default)]
642 pub destination_features: Vec<u8>,
643 pub payment_hash: sha256::Hash,
644 pub payment_secret: [u8; 32],
645 pub route_hints: Vec<RouteHint>,
646 pub min_final_cltv_delta: u64,
647 pub expiry_timestamp: u64,
649}
650
651impl PrunedInvoice {
652 pub fn new(invoice: &Bolt11Invoice, amount: Amount) -> Self {
653 let expiry_timestamp = invoice.expires_at().map_or(u64::MAX, |t| t.as_secs());
656
657 let destination_features = if let Some(features) = invoice.features() {
658 encode_bolt11_invoice_features_without_length(features)
659 } else {
660 vec![]
661 };
662
663 PrunedInvoice {
664 amount,
665 destination: invoice
666 .payee_pub_key()
667 .copied()
668 .unwrap_or_else(|| invoice.recover_payee_pub_key()),
669 destination_features,
670 payment_hash: *invoice.payment_hash(),
671 payment_secret: invoice.payment_secret().0,
672 route_hints: invoice.route_hints().into_iter().map(Into::into).collect(),
673 min_final_cltv_delta: invoice.min_final_cltv_expiry_delta(),
674 expiry_timestamp,
675 }
676 }
677}
678
679impl TryFrom<Bolt11Invoice> for PrunedInvoice {
680 type Error = anyhow::Error;
681
682 fn try_from(invoice: Bolt11Invoice) -> Result<Self, Self::Error> {
683 Ok(PrunedInvoice::new(
684 &invoice,
685 Amount::from_msats(
686 invoice
687 .amount_milli_satoshis()
688 .context("Invoice amount is missing")?,
689 ),
690 ))
691 }
692}
693
694#[derive(Debug, Clone, Serialize, Deserialize)]
699pub struct RemoveGatewayRequest {
700 pub gateway_id: secp256k1::PublicKey,
701 pub signatures: BTreeMap<PeerId, Signature>,
702}
703
704pub fn create_gateway_remove_message(
712 federation_public_key: PublicKey,
713 peer_id: PeerId,
714 challenge: sha256::Hash,
715) -> Message {
716 let mut message_preimage = "remove-gateway".as_bytes().to_vec();
717 message_preimage.append(&mut federation_public_key.consensus_encode_to_vec());
718 let guardian_id: u16 = peer_id.into();
719 message_preimage.append(&mut guardian_id.consensus_encode_to_vec());
720 message_preimage.append(&mut challenge.consensus_encode_to_vec());
721 Message::from_digest(*sha256::Hash::hash(message_preimage.as_slice()).as_ref())
722}