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
8#[cfg(feature = "uniffi")]
18uniffi::setup_scaffolding!();
19
20pub mod client;
21pub mod config;
22pub mod contracts;
23pub mod federation_endpoint_constants;
24pub mod gateway_endpoint_constants;
25mod preimage_auth;
26
27pub const LNV1_INCOMING_HTLC_EXPIRY_SAFETY_MARGIN: u16 = 30;
35
36pub const LNV1_INCOMING_HTLC_ADVERTISED_EXPIRY_DELTA: u16 = 144;
44
45use std::collections::BTreeMap;
46use std::io::{Error, Read, Write};
47use std::time::{Duration, SystemTime};
48
49use bitcoin::hashes::{Hash, sha256};
50use config::LightningClientConfig;
51use fedimint_core::core::{Decoder, ModuleInstanceId, ModuleKind};
52use fedimint_core::encoding::{Decodable, DecodeContext, DecodeError, Encodable};
53use fedimint_core::module::registry::ModuleDecoderRegistry;
54use fedimint_core::module::{CommonModuleInit, ModuleCommon, ModuleConsensusVersion};
55use fedimint_core::secp256k1::Message;
56use fedimint_core::util::SafeUrl;
57use fedimint_core::{
58 Amount, PeerId, encode_bolt11_invoice_features_without_length,
59 extensible_associated_module_type, plugin_types_trait_impl_common, secp256k1,
60};
61use lightning_invoice::{Bolt11Invoice, RoutingFees};
62pub use preimage_auth::PreimageAuth;
63pub use reqwest::Method;
64use secp256k1::schnorr::Signature;
65use serde::{Deserialize, Serialize};
66use thiserror::Error;
67use threshold_crypto::PublicKey;
68pub use threshold_crypto::PublicKey as FederationPublicKey;
72pub use {bitcoin, lightning_invoice};
73
74use crate::contracts::incoming::OfferId;
75use crate::contracts::{Contract, ContractId, ContractOutcome, Preimage, PreimageDecryptionShare};
76use crate::route_hints::RouteHint;
77
78pub const KIND: ModuleKind = ModuleKind::from_static_str("ln");
79pub const MODULE_CONSENSUS_VERSION: ModuleConsensusVersion = ModuleConsensusVersion::new(2, 1);
80
81pub const CONTRACT_FUNDED_ONCE_MODULE_CONSENSUS_VERSION: ModuleConsensusVersion =
92 ModuleConsensusVersion::new(2, 1);
93
94extensible_associated_module_type!(
95 LightningInput,
96 LightningInputV0,
97 UnknownLightningInputVariantError
98);
99
100impl LightningInput {
101 pub fn new_v0(
102 contract_id: ContractId,
103 amount: Amount,
104 witness: Option<Preimage>,
105 ) -> LightningInput {
106 LightningInput::V0(LightningInputV0 {
107 contract_id,
108 amount,
109 witness,
110 })
111 }
112}
113
114#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Encodable, Decodable)]
115pub struct LightningInputV0 {
116 pub contract_id: contracts::ContractId,
117 pub amount: Amount,
120 pub witness: Option<Preimage>,
124}
125
126impl std::fmt::Display for LightningInputV0 {
127 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
128 write!(
129 f,
130 "Lightning Contract {} with amount {}",
131 self.contract_id, self.amount
132 )
133 }
134}
135
136extensible_associated_module_type!(
137 LightningOutput,
138 LightningOutputV0,
139 UnknownLightningOutputVariantError
140);
141
142impl LightningOutput {
143 pub fn new_v0_contract(contract: ContractOutput) -> LightningOutput {
144 LightningOutput::V0(LightningOutputV0::Contract(contract))
145 }
146
147 pub fn new_v0_offer(offer: contracts::incoming::IncomingContractOffer) -> LightningOutput {
148 LightningOutput::V0(LightningOutputV0::Offer(offer))
149 }
150
151 pub fn new_v0_cancel_outgoing(
152 contract: ContractId,
153 gateway_signature: secp256k1::schnorr::Signature,
154 ) -> LightningOutput {
155 LightningOutput::V0(LightningOutputV0::CancelOutgoing {
156 contract,
157 gateway_signature,
158 })
159 }
160}
161
162#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Encodable, Decodable)]
175pub enum LightningOutputV0 {
176 Contract(ContractOutput),
178 Offer(contracts::incoming::IncomingContractOffer),
180 CancelOutgoing {
182 contract: ContractId,
184 gateway_signature: fedimint_core::secp256k1::schnorr::Signature,
186 },
187}
188
189impl std::fmt::Display for LightningOutputV0 {
190 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
191 match self {
192 LightningOutputV0::Contract(ContractOutput { amount, contract }) => match contract {
193 Contract::Incoming(incoming) => {
194 write!(
195 f,
196 "LN Incoming Contract for {} hash {}",
197 amount, incoming.hash
198 )
199 }
200 Contract::Outgoing(outgoing) => {
201 write!(
202 f,
203 "LN Outgoing Contract for {} hash {}",
204 amount, outgoing.hash
205 )
206 }
207 },
208 LightningOutputV0::Offer(offer) => {
209 write!(f, "LN offer for {} with hash {}", offer.amount, offer.hash)
210 }
211 LightningOutputV0::CancelOutgoing { contract, .. } => {
212 write!(f, "LN outgoing contract cancellation {contract}")
213 }
214 }
215 }
216}
217
218#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Encodable, Decodable)]
219pub struct ContractOutput {
220 pub amount: fedimint_core::Amount,
221 pub contract: contracts::Contract,
222}
223
224#[derive(Debug, Eq, PartialEq, Hash, Encodable, Decodable, Serialize, Deserialize, Clone)]
225pub struct ContractAccount {
226 pub amount: fedimint_core::Amount,
227 pub contract: contracts::FundedContract,
228}
229
230extensible_associated_module_type!(
231 LightningOutputOutcome,
232 LightningOutputOutcomeV0,
233 UnknownLightningOutputOutcomeVariantError
234);
235
236impl LightningOutputOutcome {
237 pub fn new_v0_contract(id: ContractId, outcome: ContractOutcome) -> LightningOutputOutcome {
238 LightningOutputOutcome::V0(LightningOutputOutcomeV0::Contract { id, outcome })
239 }
240
241 pub fn new_v0_offer(id: OfferId) -> LightningOutputOutcome {
242 LightningOutputOutcome::V0(LightningOutputOutcomeV0::Offer { id })
243 }
244
245 pub fn new_v0_cancel_outgoing(id: ContractId) -> LightningOutputOutcome {
246 LightningOutputOutcome::V0(LightningOutputOutcomeV0::CancelOutgoingContract { id })
247 }
248}
249
250#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Encodable, Decodable)]
251pub enum LightningOutputOutcomeV0 {
252 Contract {
253 id: ContractId,
254 outcome: ContractOutcome,
255 },
256 Offer {
257 id: OfferId,
258 },
259 CancelOutgoingContract {
260 id: ContractId,
261 },
262}
263
264impl LightningOutputOutcomeV0 {
265 pub fn is_permanent(&self) -> bool {
266 match self {
267 LightningOutputOutcomeV0::Contract { id: _, outcome } => outcome.is_permanent(),
268 LightningOutputOutcomeV0::Offer { .. }
269 | LightningOutputOutcomeV0::CancelOutgoingContract { .. } => true,
270 }
271 }
272}
273
274impl std::fmt::Display for LightningOutputOutcomeV0 {
275 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
276 match self {
277 LightningOutputOutcomeV0::Contract { id, .. } => {
278 write!(f, "LN Contract {id}")
279 }
280 LightningOutputOutcomeV0::Offer { id } => {
281 write!(f, "LN Offer {id}")
282 }
283 LightningOutputOutcomeV0::CancelOutgoingContract { id: contract_id } => {
284 write!(f, "LN Outgoing Contract Cancellation {contract_id}")
285 }
286 }
287 }
288}
289
290#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
299#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
300pub struct GatewayRegistrationAuth {
301 pub nonce: u64,
306 pub signature: Signature,
308}
309
310#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
316pub struct LightningGatewayRegistration {
317 pub info: LightningGateway,
318 pub vetted: bool,
320 pub valid_until: SystemTime,
323 #[serde(default, skip_serializing_if = "Option::is_none")]
326 pub auth: Option<GatewayRegistrationAuth>,
327}
328
329impl Encodable for LightningGatewayRegistration {
330 fn consensus_encode<W: Write>(&self, writer: &mut W) -> Result<(), Error> {
331 let json_repr = serde_json::to_string(self).map_err(|e| {
332 Error::other(format!(
333 "Failed to serialize LightningGatewayRegistration: {e}"
334 ))
335 })?;
336
337 json_repr.consensus_encode(writer)
338 }
339}
340
341impl Decodable for LightningGatewayRegistration {
342 fn consensus_decode_partial<R: Read>(
343 r: &mut R,
344 modules: &ModuleDecoderRegistry,
345 ) -> Result<Self, DecodeError> {
346 let json_repr = String::consensus_decode_partial(r, modules)?;
347 DecodeContext::context(
348 serde_json::from_str(&json_repr).map_err(DecodeError::from_err),
349 "Failed to deserialize LightningGatewayRegistration",
350 )
351 }
352}
353
354impl LightningGatewayRegistration {
355 pub fn unanchor(self) -> LightningGatewayAnnouncement {
360 LightningGatewayAnnouncement {
361 info: self.info,
362 ttl: self
363 .valid_until
364 .duration_since(fedimint_core::time::now())
365 .unwrap_or_default(),
366 vetted: self.vetted,
367 auth: self.auth,
368 }
369 }
370
371 pub fn is_expired(&self) -> bool {
372 self.valid_until < fedimint_core::time::now()
373 }
374}
375
376#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
384#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
385pub struct LightningGatewayAnnouncement {
386 pub info: LightningGateway,
387 pub vetted: bool,
389 pub ttl: Duration,
393 #[serde(default, skip_serializing_if = "Option::is_none")]
396 pub auth: Option<GatewayRegistrationAuth>,
397}
398
399#[cfg(feature = "uniffi")]
400use std::str::FromStr;
401
402#[cfg(feature = "uniffi")]
403uniffi::custom_type!(Signature, String, {
404 remote,
405 lower: |sig| sig.to_string(),
406 try_lift: |s| Signature::from_str(&s).map_err(Into::into),
407});
408
409pub const MAX_GATEWAY_REGISTRATION_TTL: Duration = Duration::from_secs(24 * 60 * 60);
413
414impl LightningGatewayAnnouncement {
415 pub fn registration_proof_is_valid(&self, federation_public_key: PublicKey) -> bool {
425 let Some(auth) = &self.auth else {
426 return true;
427 };
428
429 let msg =
430 create_gateway_registration_message(federation_public_key, auth.nonce, &self.info);
431
432 auth.signature
433 .verify(&msg, &self.info.gateway_id.x_only_public_key().0)
434 .is_ok()
435 }
436
437 pub fn anchor(self) -> LightningGatewayRegistration {
444 let ttl = self.ttl.min(MAX_GATEWAY_REGISTRATION_TTL);
445
446 LightningGatewayRegistration {
447 info: self.info,
448 vetted: self.vetted,
449 valid_until: fedimint_core::time::now() + ttl,
450 auth: self.auth,
451 }
452 }
453}
454
455#[derive(Debug, Clone, Serialize, Deserialize, Encodable, Decodable, PartialEq, Eq, Hash)]
457#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
458pub struct LightningGateway {
459 #[serde(rename = "mint_channel_id")]
464 pub federation_index: u64,
465 pub gateway_redeem_key: fedimint_core::secp256k1::PublicKey,
467 pub node_pub_key: fedimint_core::secp256k1::PublicKey,
468 pub lightning_alias: String,
469 pub api: SafeUrl,
472 pub route_hints: Vec<route_hints::RouteHint>,
477 #[serde(with = "serde_routing_fees")]
479 pub fees: RoutingFees,
480 pub gateway_id: secp256k1::PublicKey,
481 pub supports_private_payments: bool,
483}
484
485#[cfg(feature = "uniffi")]
486#[derive(uniffi::Record)]
487pub struct RoutingFeesFfi {
488 pub base_msat: u32,
490 pub proportional_millionths: u32,
493}
494
495#[cfg(feature = "uniffi")]
496uniffi::custom_type!(RoutingFees, RoutingFeesFfi, {
497 remote,
498 lower: |fees| RoutingFeesFfi {
499 base_msat: fees.base_msat,
500 proportional_millionths: fees.proportional_millionths,
501 },
502 try_lift: |fees_ffi| Ok(RoutingFees {
503 base_msat: fees_ffi.base_msat,
504 proportional_millionths: fees_ffi.proportional_millionths,
505 }),
506});
507
508#[derive(Debug, Clone, PartialEq, Eq, Hash, Encodable, Decodable, Serialize, Deserialize)]
509pub enum LightningConsensusItem {
510 DecryptPreimage(ContractId, PreimageDecryptionShare),
511 BlockCount(u64),
512 ModuleConsensusVersion(ModuleConsensusVersion),
513 #[encodable_default]
514 Default {
515 variant: u64,
516 bytes: Vec<u8>,
517 },
518}
519
520impl std::fmt::Display for LightningConsensusItem {
521 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
522 match self {
523 LightningConsensusItem::DecryptPreimage(contract_id, _) => {
524 write!(f, "LN Decryption Share - contract_id: {contract_id}")
525 }
526 LightningConsensusItem::BlockCount(count) => write!(f, "LN Block Count {count}"),
527 LightningConsensusItem::ModuleConsensusVersion(version) => {
528 write!(
529 f,
530 "LN Consensus Version {}.{}",
531 version.major, version.minor
532 )
533 }
534 LightningConsensusItem::Default { variant, .. } => {
535 write!(f, "LN Unknown - variant={variant}")
536 }
537 }
538 }
539}
540
541#[derive(Debug)]
542pub struct LightningCommonInit;
543
544impl CommonModuleInit for LightningCommonInit {
545 const CONSENSUS_VERSION: ModuleConsensusVersion = MODULE_CONSENSUS_VERSION;
546 const KIND: ModuleKind = KIND;
547
548 type ClientConfig = LightningClientConfig;
549
550 fn decoder() -> Decoder {
551 LightningModuleTypes::decoder()
552 }
553}
554
555pub struct LightningModuleTypes;
556
557plugin_types_trait_impl_common!(
558 KIND,
559 LightningModuleTypes,
560 LightningClientConfig,
561 LightningInput,
562 LightningOutput,
563 LightningOutputOutcome,
564 LightningConsensusItem,
565 LightningInputError,
566 LightningOutputError
567);
568
569pub mod route_hints {
572 #[cfg(feature = "uniffi")]
573 use std::str::FromStr;
574
575 use fedimint_core::encoding::{Decodable, Encodable};
576 use fedimint_core::secp256k1::PublicKey;
577 use lightning_invoice::RoutingFees;
578 use serde::{Deserialize, Serialize};
579
580 #[derive(Clone, Debug, Hash, Eq, PartialEq, Serialize, Deserialize, Encodable, Decodable)]
581 #[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
582 pub struct RouteHintHop {
583 pub src_node_id: PublicKey,
585 pub short_channel_id: u64,
587 pub base_msat: u32,
589 pub proportional_millionths: u32,
592 pub cltv_expiry_delta: u16,
594 pub htlc_minimum_msat: Option<u64>,
596 pub htlc_maximum_msat: Option<u64>,
598 }
599
600 #[cfg(feature = "uniffi")]
601 uniffi::custom_type!(PublicKey, String, {
602 remote,
603 lower: |pk| pk.to_string(),
604 try_lift: |s| PublicKey::from_str(&s).map_err(Into::into),
605 });
606
607 #[derive(Clone, Debug, Hash, Eq, PartialEq, Serialize, Deserialize, Encodable, Decodable)]
610 pub struct RouteHint(pub Vec<RouteHintHop>);
611
612 #[cfg(feature = "uniffi")]
613 uniffi::custom_newtype!(RouteHint, Vec<RouteHintHop>);
614
615 impl RouteHint {
616 pub fn to_ldk_route_hint(&self) -> lightning_invoice::RouteHint {
617 lightning_invoice::RouteHint(
618 self.0
619 .iter()
620 .map(|hop| lightning_invoice::RouteHintHop {
621 src_node_id: hop.src_node_id,
622 short_channel_id: hop.short_channel_id,
623 fees: RoutingFees {
624 base_msat: hop.base_msat,
625 proportional_millionths: hop.proportional_millionths,
626 },
627 cltv_expiry_delta: hop.cltv_expiry_delta,
628 htlc_minimum_msat: hop.htlc_minimum_msat,
629 htlc_maximum_msat: hop.htlc_maximum_msat,
630 })
631 .collect(),
632 )
633 }
634 }
635
636 impl From<lightning_invoice::RouteHint> for RouteHint {
637 fn from(rh: lightning_invoice::RouteHint) -> Self {
638 RouteHint(rh.0.into_iter().map(Into::into).collect())
639 }
640 }
641
642 impl From<lightning_invoice::RouteHintHop> for RouteHintHop {
643 fn from(rhh: lightning_invoice::RouteHintHop) -> Self {
644 RouteHintHop {
645 src_node_id: rhh.src_node_id,
646 short_channel_id: rhh.short_channel_id,
647 base_msat: rhh.fees.base_msat,
648 proportional_millionths: rhh.fees.proportional_millionths,
649 cltv_expiry_delta: rhh.cltv_expiry_delta,
650 htlc_minimum_msat: rhh.htlc_minimum_msat,
651 htlc_maximum_msat: rhh.htlc_maximum_msat,
652 }
653 }
654 }
655}
656
657pub mod serde_routing_fees {
661 use lightning_invoice::RoutingFees;
662 use serde::ser::SerializeStruct;
663 use serde::{Deserialize, Deserializer, Serializer};
664
665 #[allow(missing_docs)]
666 pub fn serialize<S>(fees: &RoutingFees, serializer: S) -> Result<S::Ok, S::Error>
667 where
668 S: Serializer,
669 {
670 let mut state = serializer.serialize_struct("RoutingFees", 2)?;
671 state.serialize_field("base_msat", &fees.base_msat)?;
672 state.serialize_field("proportional_millionths", &fees.proportional_millionths)?;
673 state.end()
674 }
675
676 #[allow(missing_docs)]
677 pub fn deserialize<'de, D>(deserializer: D) -> Result<RoutingFees, D::Error>
678 where
679 D: Deserializer<'de>,
680 {
681 let fees = serde_json::Value::deserialize(deserializer)?;
682 let base_msat = fees["base_msat"]
684 .as_u64()
685 .ok_or_else(|| serde::de::Error::custom("base_msat is not a u64"))?;
686 let proportional_millionths = fees["proportional_millionths"]
687 .as_u64()
688 .ok_or_else(|| serde::de::Error::custom("proportional_millionths is not a u64"))?;
689
690 Ok(RoutingFees {
691 base_msat: base_msat
692 .try_into()
693 .map_err(|_| serde::de::Error::custom("base_msat is greater than u32::MAX"))?,
694 proportional_millionths: proportional_millionths.try_into().map_err(|_| {
695 serde::de::Error::custom("proportional_millionths is greater than u32::MAX")
696 })?,
697 })
698 }
699}
700
701pub mod serde_option_routing_fees {
702 use lightning_invoice::RoutingFees;
703 use serde::ser::SerializeStruct;
704 use serde::{Deserialize, Deserializer, Serializer};
705
706 #[allow(missing_docs)]
707 pub fn serialize<S>(fees: &Option<RoutingFees>, serializer: S) -> Result<S::Ok, S::Error>
708 where
709 S: Serializer,
710 {
711 if let Some(fees) = fees {
712 let mut state = serializer.serialize_struct("RoutingFees", 2)?;
713 state.serialize_field("base_msat", &fees.base_msat)?;
714 state.serialize_field("proportional_millionths", &fees.proportional_millionths)?;
715 state.end()
716 } else {
717 let state = serializer.serialize_struct("RoutingFees", 0)?;
718 state.end()
719 }
720 }
721
722 #[allow(missing_docs)]
723 pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<RoutingFees>, D::Error>
724 where
725 D: Deserializer<'de>,
726 {
727 let fees = serde_json::Value::deserialize(deserializer)?;
728 let base_msat = fees["base_msat"].as_u64();
730
731 if let Some(base_msat) = base_msat
732 && let Some(proportional_millionths) = fees["proportional_millionths"].as_u64()
733 {
734 let base_msat: u32 = base_msat
735 .try_into()
736 .map_err(|_| serde::de::Error::custom("base_msat is greater than u32::MAX"))?;
737 let proportional_millionths: u32 =
738 proportional_millionths.try_into().map_err(|_| {
739 serde::de::Error::custom("proportional_millionths is greater than u32::MAX")
740 })?;
741 return Ok(Some(RoutingFees {
742 base_msat,
743 proportional_millionths,
744 }));
745 }
746
747 Ok(None)
748 }
749}
750
751#[derive(Debug, Error, Eq, PartialEq, Encodable, Decodable, Hash, Clone)]
752pub enum LightningInputError {
753 #[error("The input contract {0} does not exist")]
754 UnknownContract(ContractId),
755 #[error("The input contract has too little funds, got {0}, input spends {1}")]
756 InsufficientFunds(Amount, Amount),
757 #[error("An outgoing LN contract spend did not provide a preimage")]
758 MissingPreimage,
759 #[error("An outgoing LN contract spend provided a wrong preimage")]
760 InvalidPreimage,
761 #[error("Incoming contract not ready to be spent yet, decryption in progress")]
762 ContractNotReady,
763 #[error("The lightning input version is not supported by this federation")]
764 UnknownInputVariant(#[from] UnknownLightningInputVariantError),
765}
766
767#[derive(Debug, Error, Eq, PartialEq, Encodable, Decodable, Hash, Clone)]
768pub enum LightningOutputError {
769 #[error("The input contract {0} does not exist")]
770 UnknownContract(ContractId),
771 #[error("Output contract value may not be zero unless it's an offer output")]
772 ZeroOutput,
773 #[error("Offer contains invalid threshold-encrypted data")]
774 InvalidEncryptedPreimage,
775 #[error("Offer contains a ciphertext that has already been used")]
776 DuplicateEncryptedPreimage,
777 #[error("The incoming LN account requires more funding (need {0} got {1})")]
778 InsufficientIncomingFunding(Amount, Amount),
779 #[error("No offer found for payment hash {0}")]
780 NoOffer(bitcoin::secp256k1::hashes::sha256::Hash),
781 #[error("Only outgoing contracts support cancellation")]
782 NotOutgoingContract,
783 #[error("Cancellation request wasn't properly signed")]
784 InvalidCancellationSignature,
785 #[error("The lightning output version is not supported by this federation")]
786 UnknownOutputVariant(#[from] UnknownLightningOutputVariantError),
787 #[error("The incoming contract {0} has already been funded")]
790 ContractAlreadyFunded(ContractId),
791 #[error("An incoming contract account for the offer's payment hash already exists: {0}")]
792 OfferForFundedContract(ContractId),
793 #[error("The contract's encrypted preimage does not match the offer's")]
794 EncryptedPreimageMismatch,
795 #[error("An incoming contract must be funded with a pending decryption")]
796 PreDecryptedIncomingContract,
797}
798
799#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize, Decodable, Encodable)]
804pub struct PrunedInvoice {
805 pub amount: Amount,
806 pub destination: secp256k1::PublicKey,
807 #[serde(with = "fedimint_core::hex::serde", default)]
809 pub destination_features: Vec<u8>,
810 pub payment_hash: sha256::Hash,
811 pub payment_secret: [u8; 32],
812 pub route_hints: Vec<RouteHint>,
813 pub min_final_cltv_delta: u64,
814 pub expiry_timestamp: u64,
816}
817
818impl PrunedInvoice {
819 pub fn new(invoice: &Bolt11Invoice, amount: Amount) -> Self {
820 let expiry_timestamp = invoice.expires_at().map_or(u64::MAX, |t| t.as_secs());
823
824 let destination_features = if let Some(features) = invoice.features() {
825 encode_bolt11_invoice_features_without_length(features)
826 } else {
827 vec![]
828 };
829
830 PrunedInvoice {
831 amount,
832 destination: invoice
833 .payee_pub_key()
834 .copied()
835 .unwrap_or_else(|| invoice.recover_payee_pub_key()),
836 destination_features,
837 payment_hash: *invoice.payment_hash(),
838 payment_secret: invoice.payment_secret().0,
839 route_hints: invoice.route_hints().into_iter().map(Into::into).collect(),
840 min_final_cltv_delta: invoice.min_final_cltv_expiry_delta(),
841 expiry_timestamp,
842 }
843 }
844}
845
846#[derive(Debug, Clone, Copy, Eq, PartialEq, thiserror::Error)]
849#[error("The invoice does not specify an amount")]
850pub struct MissingInvoiceAmountError;
851
852impl TryFrom<Bolt11Invoice> for PrunedInvoice {
853 type Error = MissingInvoiceAmountError;
854
855 fn try_from(invoice: Bolt11Invoice) -> Result<Self, Self::Error> {
856 Ok(PrunedInvoice::new(
857 &invoice,
858 Amount::from_msats(
859 invoice
860 .amount_milli_satoshis()
861 .ok_or(MissingInvoiceAmountError)?,
862 ),
863 ))
864 }
865}
866
867#[derive(Debug, Clone, Serialize, Deserialize)]
872pub struct RemoveGatewayRequest {
873 pub gateway_id: secp256k1::PublicKey,
874 pub signatures: BTreeMap<PeerId, Signature>,
875}
876
877pub fn create_gateway_registration_message(
894 federation_public_key: PublicKey,
895 nonce: u64,
896 info: &LightningGateway,
897) -> Message {
898 let mut message_preimage = "register-gateway".as_bytes().to_vec();
899 message_preimage.append(&mut federation_public_key.consensus_encode_to_vec());
900 message_preimage.append(&mut nonce.consensus_encode_to_vec());
901 message_preimage.append(&mut info.consensus_encode_to_vec());
902 Message::from_digest(*sha256::Hash::hash(message_preimage.as_slice()).as_ref())
903}
904
905pub fn create_gateway_remove_message(
913 federation_public_key: PublicKey,
914 peer_id: PeerId,
915 challenge: sha256::Hash,
916) -> Message {
917 let mut message_preimage = "remove-gateway".as_bytes().to_vec();
918 message_preimage.append(&mut federation_public_key.consensus_encode_to_vec());
919 let guardian_id: u16 = peer_id.into();
920 message_preimage.append(&mut guardian_id.consensus_encode_to_vec());
921 message_preimage.append(&mut challenge.consensus_encode_to_vec());
922 Message::from_digest(*sha256::Hash::hash(message_preimage.as_slice()).as_ref())
923}
924
925#[cfg(test)]
926mod tests {
927 use bitcoin::hashes::{Hash as _, sha256};
928 use bitcoin::secp256k1::{Secp256k1, SecretKey};
929 use lightning_invoice::{Bolt11Invoice, Currency, InvoiceBuilder, PaymentSecret};
930
931 use super::{MissingInvoiceAmountError, PrunedInvoice};
932
933 #[test]
936 fn pruning_an_amountless_invoice_reports_the_missing_amount() {
937 let ctx = Secp256k1::new();
938 let sk = SecretKey::from_slice(&[1; 32]).expect("Valid secret key");
939 let invoice: Bolt11Invoice = InvoiceBuilder::new(Currency::Regtest)
940 .description(String::new())
941 .payment_hash(sha256::Hash::hash(&[0; 32]))
942 .current_timestamp()
943 .min_final_cltv_expiry_delta(0)
944 .payment_secret(PaymentSecret([0; 32]))
945 .build_signed(|m| ctx.sign_ecdsa_recoverable(m, &sk))
946 .expect("Failed to build invoice");
947
948 assert_eq!(
949 PrunedInvoice::try_from(invoice),
950 Err(MissingInvoiceAmountError)
951 );
952 }
953}