1#![deny(clippy::pedantic)]
2#![allow(clippy::cast_possible_truncation)]
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#![allow(clippy::too_many_lines)]
8
9pub mod api;
10#[cfg(feature = "cli")]
11pub mod cli;
12pub mod db;
13pub mod incoming;
14pub mod pay;
15pub mod receive;
16pub mod recurring;
18
19use std::collections::{BTreeMap, BTreeSet};
20use std::iter::once;
21use std::str::FromStr;
22use std::sync::Arc;
23use std::time::Duration;
24
25use anyhow::{Context, anyhow, bail, ensure, format_err};
26use api::LnFederationApi;
27use async_stream::{stream, try_stream};
28use bitcoin::Network;
29use bitcoin::hashes::{Hash, HashEngine, Hmac, HmacEngine, sha256};
30use db::{
31 DbKeyPrefix, LightningGatewayKey, LightningGatewayKeyPrefix, PaymentResult, PaymentResultKey,
32 RecurringPaymentCodeKeyPrefix,
33};
34use fedimint_api_client::api::DynModuleApi;
35use fedimint_client_module::db::{ClientModuleMigrationFn, migrate_state};
36use fedimint_client_module::module::init::{ClientModuleInit, ClientModuleInitArgs};
37use fedimint_client_module::module::recovery::NoModuleBackup;
38use fedimint_client_module::module::{ClientContext, ClientModule, IClientModule, OutPointRange};
39use fedimint_client_module::oplog::UpdateStreamOrOutcome;
40use fedimint_client_module::sm::util::MapStateTransitions;
41use fedimint_client_module::sm::{DynState, ModuleNotifier, State, StateTransition};
42use fedimint_client_module::transaction::{
43 ClientInput, ClientInputBundle, ClientOutput, ClientOutputBundle, ClientOutputSM,
44 TransactionBuilder,
45};
46use fedimint_client_module::{DynGlobalClientContext, sm_enum_variant_translation};
47use fedimint_core::config::FederationId;
48use fedimint_core::core::{Decoder, IntoDynInstance, ModuleInstanceId, ModuleKind, OperationId};
49use fedimint_core::db::{DatabaseTransaction, DatabaseVersion, IDatabaseTransactionOpsCoreTyped};
50use fedimint_core::encoding::{Decodable, Encodable};
51use fedimint_core::module::{
52 ApiVersion, CommonModuleInit, ModuleCommon, ModuleInit, MultiApiVersion,
53};
54use fedimint_core::secp256k1::{
55 All, Keypair, PublicKey, Scalar, Secp256k1, SecretKey, Signing, Verification,
56};
57use fedimint_core::task::{MaybeSend, MaybeSync, timeout};
58use fedimint_core::util::update_merge::UpdateMerge;
59use fedimint_core::util::{BoxStream, backoff_util, retry};
60use fedimint_core::{
61 Amount, OutPoint, apply, async_trait_maybe_send, push_db_pair_items, runtime, secp256k1,
62};
63use fedimint_derive_secret::{ChildId, DerivableSecret};
64use fedimint_ln_common::config::{FeeToAmount, LightningClientConfig};
65use fedimint_ln_common::contracts::incoming::{IncomingContract, IncomingContractOffer};
66use fedimint_ln_common::contracts::outgoing::{
67 OutgoingContract, OutgoingContractAccount, OutgoingContractData,
68};
69use fedimint_ln_common::contracts::{
70 Contract, ContractId, DecryptedPreimage, EncryptedPreimage, IdentifiableContract, Preimage,
71 PreimageKey,
72};
73use fedimint_ln_common::gateway_endpoint_constants::{
74 GET_GATEWAY_ID_ENDPOINT, PAY_INVOICE_ENDPOINT,
75};
76use fedimint_ln_common::{
77 ContractOutput, KIND, LightningCommonInit, LightningGateway, LightningGatewayAnnouncement,
78 LightningGatewayRegistration, LightningInput, LightningModuleTypes, LightningOutput,
79 LightningOutputV0,
80};
81use fedimint_logging::LOG_CLIENT_MODULE_LN;
82use futures::{Future, StreamExt};
83use incoming::IncomingSmError;
84use lightning_invoice::{
85 Bolt11Invoice, Currency, InvoiceBuilder, PaymentSecret, RouteHint, RouteHintHop, RoutingFees,
86};
87use pay::PayInvoicePayload;
88use rand::rngs::OsRng;
89use rand::seq::IteratorRandom as _;
90use rand::{CryptoRng, Rng, RngCore};
91use serde::{Deserialize, Serialize};
92use serde_json::json;
93use strum::IntoEnumIterator;
94use tokio::sync::Notify;
95use tracing::{debug, error, info};
96
97use crate::db::PaymentResultPrefix;
98use crate::incoming::{
99 FundingOfferState, IncomingSmCommon, IncomingSmStates, IncomingStateMachine,
100};
101use crate::pay::lightningpay::LightningPayStates;
102use crate::pay::{
103 GatewayPayError, LightningPayCommon, LightningPayCreatedOutgoingLnContract,
104 LightningPayStateMachine,
105};
106use crate::receive::{
107 LightningReceiveError, LightningReceiveStateMachine, LightningReceiveStates,
108 LightningReceiveSubmittedOffer, get_incoming_contract,
109};
110use crate::recurring::RecurringPaymentCodeEntry;
111
112const OUTGOING_LN_CONTRACT_TIMELOCK: u64 = 500;
115
116const DEFAULT_INVOICE_EXPIRY_TIME: Duration = Duration::from_secs(60 * 60 * 24);
119
120#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize, Encodable, Decodable)]
121#[serde(rename_all = "snake_case")]
122pub enum PayType {
123 Internal(OperationId),
125 Lightning(OperationId),
127}
128
129impl PayType {
130 pub fn operation_id(&self) -> OperationId {
131 match self {
132 PayType::Internal(operation_id) | PayType::Lightning(operation_id) => *operation_id,
133 }
134 }
135
136 pub fn payment_type(&self) -> String {
137 match self {
138 PayType::Internal(_) => "internal",
139 PayType::Lightning(_) => "lightning",
140 }
141 .into()
142 }
143}
144
145#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Serialize, Deserialize, Encodable, Decodable)]
147pub enum ReceivingKey {
148 Personal(Keypair),
151 External(PublicKey),
154}
155
156impl ReceivingKey {
157 pub fn public_key(&self) -> PublicKey {
159 match self {
160 ReceivingKey::Personal(keypair) => keypair.public_key(),
161 ReceivingKey::External(public_key) => *public_key,
162 }
163 }
164}
165
166#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
169#[serde(rename_all = "snake_case")]
170pub enum InternalPayState {
171 Funding,
172 Preimage(Preimage),
173 RefundSuccess {
174 out_points: Vec<OutPoint>,
175 error: IncomingSmError,
176 },
177 RefundError {
178 error_message: String,
179 error: IncomingSmError,
180 },
181 FundingFailed {
182 error: IncomingSmError,
183 },
184 UnexpectedError(String),
185}
186
187#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
190#[serde(rename_all = "snake_case")]
191pub enum LnPayState {
192 Created,
193 Canceled,
194 Funded { block_height: u32 },
195 WaitingForRefund { error_reason: String },
196 AwaitingChange,
197 Success { preimage: String },
198 Refunded { gateway_error: GatewayPayError },
199 UnexpectedError { error_message: String },
200}
201
202#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
205#[serde(rename_all = "snake_case")]
206pub enum LnReceiveState {
207 Created,
208 WaitingForPayment { invoice: String, timeout: Duration },
209 Canceled { reason: LightningReceiveError },
210 Funded,
211 AwaitingFunds,
212 Claimed,
213}
214
215fn invoice_has_internal_payment_markers(
216 invoice: &Bolt11Invoice,
217 markers: (fedimint_core::secp256k1::PublicKey, u64),
218) -> bool {
219 invoice
222 .route_hints()
223 .first()
224 .and_then(|rh| rh.0.last())
225 .map(|hop| (hop.src_node_id, hop.short_channel_id))
226 == Some(markers)
227}
228
229fn invoice_routes_back_to_federation(
230 invoice: &Bolt11Invoice,
231 gateways: Vec<LightningGateway>,
232) -> bool {
233 gateways.into_iter().any(|gateway| {
234 invoice
235 .route_hints()
236 .first()
237 .and_then(|rh| rh.0.last())
238 .map(|hop| (hop.src_node_id, hop.short_channel_id))
239 == Some((gateway.node_pub_key, gateway.federation_index))
240 })
241}
242
243#[derive(Debug, Clone, Serialize, Deserialize)]
244#[serde(rename_all = "snake_case")]
245pub struct LightningOperationMetaPay {
246 pub out_point: OutPoint,
247 pub invoice: Bolt11Invoice,
248 pub fee: Amount,
249 pub change: Vec<OutPoint>,
250 pub is_internal_payment: bool,
251 pub contract_id: ContractId,
252 pub gateway_id: Option<secp256k1::PublicKey>,
253}
254
255#[derive(Debug, Clone, Serialize, Deserialize)]
256pub struct LightningOperationMeta {
257 pub variant: LightningOperationMetaVariant,
258 pub extra_meta: serde_json::Value,
259}
260
261pub use depreacated_variant_hack::LightningOperationMetaVariant;
262
263#[allow(deprecated)]
268mod depreacated_variant_hack {
269 use super::{
270 Bolt11Invoice, Deserialize, LightningOperationMetaPay, OutPoint, Serialize, secp256k1,
271 };
272 use crate::recurring::ReurringPaymentReceiveMeta;
273
274 #[derive(Debug, Clone, Serialize, Deserialize)]
275 #[serde(rename_all = "snake_case")]
276 pub enum LightningOperationMetaVariant {
277 Pay(LightningOperationMetaPay),
278 Receive {
279 out_point: OutPoint,
280 invoice: Bolt11Invoice,
281 gateway_id: Option<secp256k1::PublicKey>,
282 },
283 #[deprecated(
284 since = "0.7.0",
285 note = "Use recurring payment functionality instead instead"
286 )]
287 Claim {
288 out_points: Vec<OutPoint>,
289 },
290 RecurringPaymentReceive(ReurringPaymentReceiveMeta),
291 }
292}
293
294#[derive(Debug, Clone)]
295pub struct LightningClientInit {
296 pub gateway_conn: Arc<dyn GatewayConnection + Send + Sync>,
297}
298
299impl Default for LightningClientInit {
300 fn default() -> Self {
301 LightningClientInit {
302 gateway_conn: Arc::new(RealGatewayConnection::default()),
303 }
304 }
305}
306
307impl ModuleInit for LightningClientInit {
308 type Common = LightningCommonInit;
309
310 async fn dump_database(
311 &self,
312 dbtx: &mut DatabaseTransaction<'_>,
313 prefix_names: Vec<String>,
314 ) -> Box<dyn Iterator<Item = (String, Box<dyn erased_serde::Serialize + Send>)> + '_> {
315 let mut ln_client_items: BTreeMap<String, Box<dyn erased_serde::Serialize + Send>> =
316 BTreeMap::new();
317 let filtered_prefixes = DbKeyPrefix::iter().filter(|f| {
318 prefix_names.is_empty() || prefix_names.contains(&f.to_string().to_lowercase())
319 });
320
321 for table in filtered_prefixes {
322 #[allow(clippy::match_same_arms)]
323 match table {
324 DbKeyPrefix::ActiveGateway | DbKeyPrefix::MetaOverridesDeprecated => {
325 }
327 DbKeyPrefix::PaymentResult => {
328 push_db_pair_items!(
329 dbtx,
330 PaymentResultPrefix,
331 PaymentResultKey,
332 PaymentResult,
333 ln_client_items,
334 "Payment Result"
335 );
336 }
337 DbKeyPrefix::LightningGateway => {
338 push_db_pair_items!(
339 dbtx,
340 LightningGatewayKeyPrefix,
341 LightningGatewayKey,
342 LightningGatewayRegistration,
343 ln_client_items,
344 "Lightning Gateways"
345 );
346 }
347 DbKeyPrefix::RecurringPaymentKey => {
348 push_db_pair_items!(
349 dbtx,
350 RecurringPaymentCodeKeyPrefix,
351 RecurringPaymentCodeKey,
352 RecurringPaymentCodeEntry,
353 ln_client_items,
354 "Recurring Payment Code"
355 );
356 }
357 DbKeyPrefix::ExternalReservedStart
358 | DbKeyPrefix::CoreInternalReservedStart
359 | DbKeyPrefix::CoreInternalReservedEnd => {}
360 }
361 }
362
363 Box::new(ln_client_items.into_iter())
364 }
365}
366
367#[derive(Debug)]
368#[repr(u64)]
369pub enum LightningChildKeys {
370 RedeemKey = 0,
371 PreimageAuthentication = 1,
372 RecurringPaymentCodeSecret = 2,
373}
374
375#[apply(async_trait_maybe_send!)]
376impl ClientModuleInit for LightningClientInit {
377 type Module = LightningClientModule;
378
379 fn supported_api_versions(&self) -> MultiApiVersion {
380 MultiApiVersion::try_from_iter([ApiVersion { major: 0, minor: 0 }])
381 .expect("no version conflicts")
382 }
383
384 async fn init(&self, args: &ClientModuleInitArgs<Self>) -> anyhow::Result<Self::Module> {
385 Ok(LightningClientModule::new(args, self.gateway_conn.clone()))
386 }
387
388 fn get_database_migrations(&self) -> BTreeMap<DatabaseVersion, ClientModuleMigrationFn> {
389 let mut migrations: BTreeMap<DatabaseVersion, ClientModuleMigrationFn> = BTreeMap::new();
390 migrations.insert(DatabaseVersion(0), |dbtx, _, _| {
391 Box::pin(async {
392 dbtx.remove_entry(&crate::db::ActiveGatewayKey).await;
393 Ok(None)
394 })
395 });
396
397 migrations.insert(DatabaseVersion(1), |_, active_states, inactive_states| {
398 Box::pin(async {
399 migrate_state(active_states, inactive_states, db::get_v1_migrated_state)
400 })
401 });
402
403 migrations.insert(DatabaseVersion(2), |_, active_states, inactive_states| {
404 Box::pin(async {
405 migrate_state(active_states, inactive_states, db::get_v2_migrated_state)
406 })
407 });
408
409 migrations.insert(DatabaseVersion(3), |_, active_states, inactive_states| {
410 Box::pin(async {
411 migrate_state(active_states, inactive_states, db::get_v3_migrated_state)
412 })
413 });
414
415 migrations
416 }
417
418 fn used_db_prefixes(&self) -> Option<BTreeSet<u8>> {
419 Some(
420 DbKeyPrefix::iter()
421 .map(|p| p as u8)
422 .chain(
423 DbKeyPrefix::ExternalReservedStart as u8
424 ..=DbKeyPrefix::CoreInternalReservedEnd as u8,
425 )
426 .collect(),
427 )
428 }
429}
430
431#[derive(Debug)]
436pub struct LightningClientModule {
437 pub cfg: LightningClientConfig,
438 notifier: ModuleNotifier<LightningClientStateMachines>,
439 redeem_key: Keypair,
440 recurring_payment_code_secret: DerivableSecret,
441 secp: Secp256k1<All>,
442 module_api: DynModuleApi,
443 preimage_auth: Keypair,
444 client_ctx: ClientContext<Self>,
445 update_gateway_cache_merge: UpdateMerge,
446 gateway_conn: Arc<dyn GatewayConnection + Send + Sync>,
447 new_recurring_payment_code: Arc<Notify>,
448}
449
450#[apply(async_trait_maybe_send!)]
451impl ClientModule for LightningClientModule {
452 type Init = LightningClientInit;
453 type Common = LightningModuleTypes;
454 type Backup = NoModuleBackup;
455 type ModuleStateMachineContext = LightningClientContext;
456 type States = LightningClientStateMachines;
457
458 fn context(&self) -> Self::ModuleStateMachineContext {
459 LightningClientContext {
460 ln_decoder: self.decoder(),
461 redeem_key: self.redeem_key,
462 gateway_conn: self.gateway_conn.clone(),
463 }
464 }
465
466 fn input_fee(
467 &self,
468 _amount: Amount,
469 _input: &<Self::Common as ModuleCommon>::Input,
470 ) -> Option<Amount> {
471 Some(self.cfg.fee_consensus.contract_input)
472 }
473
474 fn output_fee(
475 &self,
476 _amount: Amount,
477 output: &<Self::Common as ModuleCommon>::Output,
478 ) -> Option<Amount> {
479 match output.maybe_v0_ref()? {
480 LightningOutputV0::Contract(_) => Some(self.cfg.fee_consensus.contract_output),
481 LightningOutputV0::Offer(_) | LightningOutputV0::CancelOutgoing { .. } => {
482 Some(Amount::ZERO)
483 }
484 }
485 }
486
487 #[cfg(feature = "cli")]
488 async fn handle_cli_command(
489 &self,
490 args: &[std::ffi::OsString],
491 ) -> anyhow::Result<serde_json::Value> {
492 cli::handle_cli_command(self, args).await
493 }
494
495 async fn handle_rpc(
496 &self,
497 method: String,
498 payload: serde_json::Value,
499 ) -> BoxStream<'_, anyhow::Result<serde_json::Value>> {
500 Box::pin(try_stream! {
501 match method.as_str() {
502 "create_bolt11_invoice" => {
503 let req: CreateBolt11InvoiceRequest = serde_json::from_value(payload)?;
504 let (op, invoice, _) = self
505 .create_bolt11_invoice(
506 req.amount,
507 lightning_invoice::Bolt11InvoiceDescription::Direct(
508 &lightning_invoice::Description::new(req.description)?,
509 ),
510 req.expiry_time,
511 req.extra_meta,
512 req.gateway,
513 )
514 .await?;
515 yield serde_json::json!({
516 "operation_id": op,
517 "invoice": invoice,
518 });
519 }
520 "pay_bolt11_invoice" => {
521 let req: PayBolt11InvoiceRequest = serde_json::from_value(payload)?;
522 let outgoing_payment = self
523 .pay_bolt11_invoice(req.maybe_gateway, req.invoice, req.extra_meta)
524 .await?;
525 yield serde_json::to_value(outgoing_payment)?;
526 }
527 "subscribe_ln_pay" => {
528 let req: SubscribeLnPayRequest = serde_json::from_value(payload)?;
529 for await state in self.subscribe_ln_pay(req.operation_id).await?.into_stream() {
530 yield serde_json::to_value(state)?;
531 }
532 }
533 "subscribe_ln_receive" => {
534 let req: SubscribeLnReceiveRequest = serde_json::from_value(payload)?;
535 for await state in self.subscribe_ln_receive(req.operation_id).await?.into_stream()
536 {
537 yield serde_json::to_value(state)?;
538 }
539 }
540 "create_bolt11_invoice_for_user_tweaked" => {
541 let req: CreateBolt11InvoiceForUserTweakedRequest = serde_json::from_value(payload)?;
542 let (op, invoice, _) = self
543 .create_bolt11_invoice_for_user_tweaked(
544 req.amount,
545 lightning_invoice::Bolt11InvoiceDescription::Direct(
546 &lightning_invoice::Description::new(req.description)?,
547 ),
548 req.expiry_time,
549 req.user_key,
550 req.index,
551 req.extra_meta,
552 req.gateway,
553 )
554 .await?;
555 yield serde_json::json!({
556 "operation_id": op,
557 "invoice": invoice,
558 });
559 }
560 #[allow(deprecated)]
561 "scan_receive_for_user_tweaked" => {
562 let req: ScanReceiveForUserTweakedRequest = serde_json::from_value(payload)?;
563 let keypair = Keypair::from_secret_key(&self.secp, &req.user_key);
564 let operation_ids = self.scan_receive_for_user_tweaked(keypair, req.indices, req.extra_meta).await;
565 yield serde_json::to_value(operation_ids)?;
566 }
567 #[allow(deprecated)]
568 "subscribe_ln_claim" => {
569 let req: SubscribeLnClaimRequest = serde_json::from_value(payload)?;
570 for await state in self.subscribe_ln_claim(req.operation_id).await?.into_stream() {
571 yield serde_json::to_value(state)?;
572 }
573 }
574 "get_gateway" => {
575 let req: GetGatewayRequest = serde_json::from_value(payload)?;
576 let gateway = self.get_gateway(req.gateway_id, req.force_internal).await?;
577 yield serde_json::to_value(gateway)?;
578 }
579 "list_gateways" => {
580 let gateways = self.list_gateways().await;
581 yield serde_json::to_value(gateways)?;
582 }
583 "update_gateway_cache" => {
584 self.update_gateway_cache().await?;
585 yield serde_json::Value::Null;
586 }
587 _ => {
588 Err(anyhow::format_err!("Unknown method: {}", method))?;
589 unreachable!()
590 },
591 }
592 })
593 }
594}
595
596#[derive(Deserialize)]
597struct CreateBolt11InvoiceRequest {
598 amount: Amount,
599 description: String,
600 expiry_time: Option<u64>,
601 extra_meta: serde_json::Value,
602 gateway: Option<LightningGateway>,
603}
604
605#[derive(Deserialize)]
606struct PayBolt11InvoiceRequest {
607 maybe_gateway: Option<LightningGateway>,
608 invoice: Bolt11Invoice,
609 extra_meta: Option<serde_json::Value>,
610}
611
612#[derive(Deserialize)]
613struct SubscribeLnPayRequest {
614 operation_id: OperationId,
615}
616
617#[derive(Deserialize)]
618struct SubscribeLnReceiveRequest {
619 operation_id: OperationId,
620}
621
622#[derive(Deserialize)]
623struct CreateBolt11InvoiceForUserTweakedRequest {
624 amount: Amount,
625 description: String,
626 expiry_time: Option<u64>,
627 user_key: PublicKey,
628 index: u64,
629 extra_meta: serde_json::Value,
630 gateway: Option<LightningGateway>,
631}
632
633#[derive(Deserialize)]
634struct ScanReceiveForUserTweakedRequest {
635 user_key: SecretKey,
636 indices: Vec<u64>,
637 extra_meta: serde_json::Value,
638}
639
640#[derive(Deserialize)]
641struct SubscribeLnClaimRequest {
642 operation_id: OperationId,
643}
644
645#[derive(Deserialize)]
646struct GetGatewayRequest {
647 gateway_id: Option<secp256k1::PublicKey>,
648 force_internal: bool,
649}
650
651#[derive(thiserror::Error, Debug, Clone)]
652pub enum PayBolt11InvoiceError {
653 #[error("Previous payment attempt({}) still in progress", .operation_id.fmt_full())]
654 PreviousPaymentAttemptStillInProgress { operation_id: OperationId },
655 #[error("No LN gateway available")]
656 NoLnGatewayAvailable,
657 #[error("Funded contract already exists: {}", .contract_id)]
658 FundedContractAlreadyExists { contract_id: ContractId },
659}
660
661impl LightningClientModule {
662 fn new(
663 args: &ClientModuleInitArgs<LightningClientInit>,
664 gateway_conn: Arc<dyn GatewayConnection + Send + Sync>,
665 ) -> Self {
666 let secp = Secp256k1::new();
667
668 let new_recurring_payment_code = Arc::new(Notify::new());
669 args.task_group().spawn_cancellable(
670 "Recurring payment sync",
671 Self::scan_recurring_payment_code_invoices(
672 args.context(),
673 new_recurring_payment_code.clone(),
674 ),
675 );
676
677 Self {
678 cfg: args.cfg().clone(),
679 notifier: args.notifier().clone(),
680 redeem_key: args
681 .module_root_secret()
682 .child_key(ChildId(LightningChildKeys::RedeemKey as u64))
683 .to_secp_key(&secp),
684 recurring_payment_code_secret: args.module_root_secret().child_key(ChildId(
685 LightningChildKeys::RecurringPaymentCodeSecret as u64,
686 )),
687 module_api: args.module_api().clone(),
688 preimage_auth: args
689 .module_root_secret()
690 .child_key(ChildId(LightningChildKeys::PreimageAuthentication as u64))
691 .to_secp_key(&secp),
692 secp,
693 client_ctx: args.context(),
694 update_gateway_cache_merge: UpdateMerge::default(),
695 gateway_conn,
696 new_recurring_payment_code,
697 }
698 }
699
700 pub async fn get_prev_payment_result(
701 &self,
702 payment_hash: &sha256::Hash,
703 dbtx: &mut DatabaseTransaction<'_>,
704 ) -> PaymentResult {
705 let prev_result = dbtx
706 .get_value(&PaymentResultKey {
707 payment_hash: *payment_hash,
708 })
709 .await;
710 prev_result.unwrap_or(PaymentResult {
711 index: 0,
712 completed_payment: None,
713 })
714 }
715
716 fn get_payment_operation_id(payment_hash: &sha256::Hash, index: u16) -> OperationId {
717 let mut bytes = [0; 34];
720 bytes[0..32].copy_from_slice(&payment_hash.to_byte_array());
721 bytes[32..34].copy_from_slice(&index.to_le_bytes());
722 let hash: sha256::Hash = Hash::hash(&bytes);
723 OperationId(hash.to_byte_array())
724 }
725
726 fn get_preimage_authentication(&self, payment_hash: &sha256::Hash) -> sha256::Hash {
731 let mut bytes = [0; 64];
732 bytes[0..32].copy_from_slice(&payment_hash.to_byte_array());
733 bytes[32..64].copy_from_slice(&self.preimage_auth.secret_bytes());
734 Hash::hash(&bytes)
735 }
736
737 async fn create_outgoing_output<'a, 'b>(
741 &'a self,
742 operation_id: OperationId,
743 invoice: Bolt11Invoice,
744 gateway: LightningGateway,
745 fed_id: FederationId,
746 mut rng: impl RngCore + CryptoRng + 'a,
747 ) -> anyhow::Result<(
748 ClientOutput<LightningOutputV0>,
749 ClientOutputSM<LightningClientStateMachines>,
750 ContractId,
751 )> {
752 let federation_currency: Currency = self.cfg.network.0.into();
753 let invoice_currency = invoice.currency();
754 ensure!(
755 federation_currency == invoice_currency,
756 "Invalid invoice currency: expected={:?}, got={:?}",
757 federation_currency,
758 invoice_currency
759 );
760
761 self.gateway_conn
764 .verify_gateway_availability(&gateway)
765 .await?;
766
767 let consensus_count = self
768 .module_api
769 .fetch_consensus_block_count()
770 .await?
771 .ok_or(format_err!("Cannot get consensus block count"))?;
772
773 let min_final_cltv = invoice.min_final_cltv_expiry_delta();
776 let absolute_timelock =
777 consensus_count + min_final_cltv + OUTGOING_LN_CONTRACT_TIMELOCK - 1;
778
779 let invoice_amount = Amount::from_msats(
781 invoice
782 .amount_milli_satoshis()
783 .context("MissingInvoiceAmount")?,
784 );
785
786 let gateway_fee = gateway.fees.to_amount(&invoice_amount);
787 let contract_amount = invoice_amount + gateway_fee;
788
789 let user_sk = Keypair::new(&self.secp, &mut rng);
790
791 let payment_hash = *invoice.payment_hash();
792 let preimage_auth = self.get_preimage_authentication(&payment_hash);
793 let contract = OutgoingContract {
794 hash: payment_hash,
795 gateway_key: gateway.gateway_redeem_key,
796 timelock: absolute_timelock as u32,
797 user_key: user_sk.public_key(),
798 cancelled: false,
799 };
800
801 let outgoing_payment = OutgoingContractData {
802 recovery_key: user_sk,
803 contract_account: OutgoingContractAccount {
804 amount: contract_amount,
805 contract: contract.clone(),
806 },
807 };
808
809 let contract_id = contract.contract_id();
810 let sm_gen = Arc::new(move |out_point_range: OutPointRange| {
811 vec![LightningClientStateMachines::LightningPay(
812 LightningPayStateMachine {
813 common: LightningPayCommon {
814 operation_id,
815 federation_id: fed_id,
816 contract: outgoing_payment.clone(),
817 gateway_fee,
818 preimage_auth,
819 invoice: invoice.clone(),
820 },
821 state: LightningPayStates::CreatedOutgoingLnContract(
822 LightningPayCreatedOutgoingLnContract {
823 funding_txid: out_point_range.txid(),
824 contract_id,
825 gateway: gateway.clone(),
826 },
827 ),
828 },
829 )]
830 });
831
832 let ln_output = LightningOutputV0::Contract(ContractOutput {
833 amount: contract_amount,
834 contract: Contract::Outgoing(contract),
835 });
836
837 Ok((
838 ClientOutput {
839 output: ln_output,
840 amount: contract_amount,
841 },
842 ClientOutputSM {
843 state_machines: sm_gen,
844 },
845 contract_id,
846 ))
847 }
848
849 async fn create_incoming_output(
853 &self,
854 operation_id: OperationId,
855 invoice: Bolt11Invoice,
856 ) -> anyhow::Result<(
857 ClientOutput<LightningOutputV0>,
858 ClientOutputSM<LightningClientStateMachines>,
859 ContractId,
860 )> {
861 let payment_hash = *invoice.payment_hash();
862 let invoice_amount = Amount {
863 msats: invoice
864 .amount_milli_satoshis()
865 .ok_or(IncomingSmError::AmountError {
866 invoice: invoice.clone(),
867 })?,
868 };
869
870 let (incoming_output, amount, contract_id) = create_incoming_contract_output(
871 &self.module_api,
872 payment_hash,
873 invoice_amount,
874 &self.redeem_key,
875 )
876 .await?;
877
878 let client_output = ClientOutput::<LightningOutputV0> {
879 output: incoming_output,
880 amount,
881 };
882
883 let client_output_sm = ClientOutputSM::<LightningClientStateMachines> {
884 state_machines: Arc::new(move |out_point_range| {
885 vec![LightningClientStateMachines::InternalPay(
886 IncomingStateMachine {
887 common: IncomingSmCommon {
888 operation_id,
889 contract_id,
890 payment_hash,
891 },
892 state: IncomingSmStates::FundingOffer(FundingOfferState {
893 txid: out_point_range.txid(),
894 }),
895 },
896 )]
897 }),
898 };
899
900 Ok((client_output, client_output_sm, contract_id))
901 }
902
903 async fn await_receive_success(
905 &self,
906 operation_id: OperationId,
907 ) -> Result<bool, LightningReceiveError> {
908 let mut stream = self.notifier.subscribe(operation_id).await;
909 loop {
910 if let Some(LightningClientStateMachines::Receive(state)) = stream.next().await {
911 match state.state {
912 LightningReceiveStates::Funded(_) => return Ok(false),
913 LightningReceiveStates::Success(outpoints) => return Ok(outpoints.is_empty()), LightningReceiveStates::Canceled(e) => {
915 return Err(e);
916 }
917 _ => {}
918 }
919 }
920 }
921 }
922
923 async fn await_claim_acceptance(
924 &self,
925 operation_id: OperationId,
926 ) -> Result<Vec<OutPoint>, LightningReceiveError> {
927 let mut stream = self.notifier.subscribe(operation_id).await;
928 loop {
929 if let Some(LightningClientStateMachines::Receive(state)) = stream.next().await {
930 match state.state {
931 LightningReceiveStates::Success(out_points) => return Ok(out_points),
932 LightningReceiveStates::Canceled(e) => {
933 return Err(e);
934 }
935 _ => {}
936 }
937 }
938 }
939 }
940
941 #[allow(clippy::too_many_arguments)]
942 #[allow(clippy::type_complexity)]
943 fn create_lightning_receive_output<'a>(
944 &'a self,
945 amount: Amount,
946 description: lightning_invoice::Bolt11InvoiceDescription<'a>,
947 receiving_key: ReceivingKey,
948 mut rng: impl RngCore + CryptoRng + 'a,
949 expiry_time: Option<u64>,
950 src_node_id: secp256k1::PublicKey,
951 short_channel_id: u64,
952 route_hints: &[fedimint_ln_common::route_hints::RouteHint],
953 network: Network,
954 ) -> anyhow::Result<(
955 OperationId,
956 Bolt11Invoice,
957 ClientOutputBundle<LightningOutput, LightningClientStateMachines>,
958 [u8; 32],
959 )> {
960 let preimage_key: [u8; 33] = receiving_key.public_key().serialize();
961 let preimage = sha256::Hash::hash(&preimage_key);
962 let payment_hash = sha256::Hash::hash(&preimage.to_byte_array());
963
964 let (node_secret_key, node_public_key) = self.secp.generate_keypair(&mut rng);
966
967 let route_hint_last_hop = RouteHintHop {
969 src_node_id,
970 short_channel_id,
971 fees: RoutingFees {
972 base_msat: 0,
973 proportional_millionths: 0,
974 },
975 cltv_expiry_delta: 30,
976 htlc_minimum_msat: None,
977 htlc_maximum_msat: None,
978 };
979 let mut final_route_hints = vec![RouteHint(vec![route_hint_last_hop.clone()])];
980 if !route_hints.is_empty() {
981 let mut two_hop_route_hints: Vec<RouteHint> = route_hints
982 .iter()
983 .map(|rh| {
984 RouteHint(
985 rh.to_ldk_route_hint()
986 .0
987 .iter()
988 .cloned()
989 .chain(once(route_hint_last_hop.clone()))
990 .collect(),
991 )
992 })
993 .collect();
994 final_route_hints.append(&mut two_hop_route_hints);
995 }
996
997 let duration_since_epoch = fedimint_core::time::duration_since_epoch();
998
999 let mut invoice_builder = InvoiceBuilder::new(network.into())
1000 .amount_milli_satoshis(amount.msats)
1001 .invoice_description(description)
1002 .payment_hash(payment_hash)
1003 .payment_secret(PaymentSecret(rng.r#gen()))
1004 .duration_since_epoch(duration_since_epoch)
1005 .min_final_cltv_expiry_delta(18)
1006 .payee_pub_key(node_public_key)
1007 .expiry_time(Duration::from_secs(
1008 expiry_time.unwrap_or(DEFAULT_INVOICE_EXPIRY_TIME.as_secs()),
1009 ));
1010
1011 for rh in final_route_hints {
1012 invoice_builder = invoice_builder.private_route(rh);
1013 }
1014
1015 let invoice = invoice_builder
1016 .build_signed(|msg| self.secp.sign_ecdsa_recoverable(msg, &node_secret_key))?;
1017
1018 let operation_id = OperationId(*invoice.payment_hash().as_ref());
1019
1020 let sm_invoice = invoice.clone();
1021 let sm_gen = Arc::new(move |out_point_range: OutPointRange| {
1022 vec![LightningClientStateMachines::Receive(
1023 LightningReceiveStateMachine {
1024 operation_id,
1025 state: LightningReceiveStates::SubmittedOffer(LightningReceiveSubmittedOffer {
1026 offer_txid: out_point_range.txid(),
1027 invoice: sm_invoice.clone(),
1028 receiving_key,
1029 }),
1030 },
1031 )]
1032 });
1033
1034 let ln_output = LightningOutput::new_v0_offer(IncomingContractOffer {
1035 amount,
1036 hash: payment_hash,
1037 encrypted_preimage: EncryptedPreimage::new(
1038 &PreimageKey(preimage_key),
1039 &self.cfg.threshold_pub_key,
1040 ),
1041 expiry_time,
1042 });
1043
1044 Ok((
1045 operation_id,
1046 invoice,
1047 ClientOutputBundle::new(
1048 vec![ClientOutput {
1049 output: ln_output,
1050 amount: Amount::ZERO,
1051 }],
1052 vec![ClientOutputSM {
1053 state_machines: sm_gen,
1054 }],
1055 ),
1056 *preimage.as_ref(),
1057 ))
1058 }
1059
1060 pub async fn select_gateway(
1063 &self,
1064 gateway_id: &secp256k1::PublicKey,
1065 ) -> Option<LightningGateway> {
1066 let mut dbtx = self.client_ctx.module_db().begin_transaction_nc().await;
1067 let gateways = dbtx
1068 .find_by_prefix(&LightningGatewayKeyPrefix)
1069 .await
1070 .map(|(_, gw)| gw.info)
1071 .collect::<Vec<_>>()
1072 .await;
1073 gateways.into_iter().find(|g| &g.gateway_id == gateway_id)
1074 }
1075
1076 pub async fn update_gateway_cache(&self) -> anyhow::Result<()> {
1081 self.update_gateway_cache_merge
1082 .merge(async {
1083 let gateways = self.module_api.fetch_gateways().await?;
1084 let mut dbtx = self.client_ctx.module_db().begin_transaction().await;
1085
1086 dbtx.remove_by_prefix(&LightningGatewayKeyPrefix).await;
1088
1089 for gw in &gateways {
1090 dbtx.insert_entry(
1091 &LightningGatewayKey(gw.info.gateway_id),
1092 &gw.clone().anchor(),
1093 )
1094 .await;
1095 }
1096
1097 dbtx.commit_tx().await;
1098
1099 Ok(())
1100 })
1101 .await
1102 }
1103
1104 pub async fn update_gateway_cache_continuously<Fut>(
1109 &self,
1110 gateways_filter: impl Fn(Vec<LightningGatewayAnnouncement>) -> Fut,
1111 ) -> !
1112 where
1113 Fut: Future<Output = Vec<LightningGatewayAnnouncement>>,
1114 {
1115 const ABOUT_TO_EXPIRE: Duration = Duration::from_secs(30);
1116 const EMPTY_GATEWAY_SLEEP: Duration = Duration::from_secs(10 * 60);
1117
1118 let mut first_time = true;
1119
1120 loop {
1121 let gateways = self.list_gateways().await;
1122 let sleep_time = gateways_filter(gateways)
1123 .await
1124 .into_iter()
1125 .map(|x| x.ttl.saturating_sub(ABOUT_TO_EXPIRE))
1126 .min()
1127 .unwrap_or(if first_time {
1128 Duration::ZERO
1130 } else {
1131 EMPTY_GATEWAY_SLEEP
1132 });
1133 runtime::sleep(sleep_time).await;
1134
1135 let _ = retry(
1137 "update_gateway_cache",
1138 backoff_util::background_backoff(),
1139 || self.update_gateway_cache(),
1140 )
1141 .await;
1142 first_time = false;
1143 }
1144 }
1145
1146 pub async fn list_gateways(&self) -> Vec<LightningGatewayAnnouncement> {
1148 let mut dbtx = self.client_ctx.module_db().begin_transaction_nc().await;
1149 dbtx.find_by_prefix(&LightningGatewayKeyPrefix)
1150 .await
1151 .map(|(_, gw)| gw.unanchor())
1152 .collect::<Vec<_>>()
1153 .await
1154 }
1155
1156 pub async fn pay_bolt11_invoice<M: Serialize + MaybeSend + MaybeSync>(
1165 &self,
1166 maybe_gateway: Option<LightningGateway>,
1167 invoice: Bolt11Invoice,
1168 extra_meta: M,
1169 ) -> anyhow::Result<OutgoingLightningPayment> {
1170 let mut dbtx = self.client_ctx.module_db().begin_transaction().await;
1171 let maybe_gateway_id = maybe_gateway.as_ref().map(|g| g.gateway_id);
1172 let prev_payment_result = self
1173 .get_prev_payment_result(invoice.payment_hash(), &mut dbtx.to_ref_nc())
1174 .await;
1175
1176 if let Some(completed_payment) = prev_payment_result.completed_payment {
1177 return Ok(completed_payment);
1178 }
1179
1180 let prev_operation_id = LightningClientModule::get_payment_operation_id(
1182 invoice.payment_hash(),
1183 prev_payment_result.index,
1184 );
1185 if self.client_ctx.has_active_states(prev_operation_id).await {
1186 bail!(
1187 PayBolt11InvoiceError::PreviousPaymentAttemptStillInProgress {
1188 operation_id: prev_operation_id
1189 }
1190 )
1191 }
1192
1193 let next_index = prev_payment_result.index + 1;
1194 let operation_id =
1195 LightningClientModule::get_payment_operation_id(invoice.payment_hash(), next_index);
1196
1197 let new_payment_result = PaymentResult {
1198 index: next_index,
1199 completed_payment: None,
1200 };
1201
1202 dbtx.insert_entry(
1203 &PaymentResultKey {
1204 payment_hash: *invoice.payment_hash(),
1205 },
1206 &new_payment_result,
1207 )
1208 .await;
1209
1210 let markers = self.client_ctx.get_internal_payment_markers()?;
1211
1212 let mut is_internal_payment = invoice_has_internal_payment_markers(&invoice, markers);
1213 if !is_internal_payment {
1214 let gateways = dbtx
1215 .find_by_prefix(&LightningGatewayKeyPrefix)
1216 .await
1217 .map(|(_, gw)| gw.info)
1218 .collect::<Vec<_>>()
1219 .await;
1220 is_internal_payment = invoice_routes_back_to_federation(&invoice, gateways);
1221 }
1222
1223 let (pay_type, client_output, client_output_sm, contract_id) = if is_internal_payment {
1224 let (output, output_sm, contract_id) = self
1225 .create_incoming_output(operation_id, invoice.clone())
1226 .await?;
1227 (
1228 PayType::Internal(operation_id),
1229 output,
1230 output_sm,
1231 contract_id,
1232 )
1233 } else {
1234 let gateway = maybe_gateway.context(PayBolt11InvoiceError::NoLnGatewayAvailable)?;
1235 let (output, output_sm, contract_id) = self
1236 .create_outgoing_output(
1237 operation_id,
1238 invoice.clone(),
1239 gateway,
1240 self.client_ctx
1241 .get_config()
1242 .await
1243 .global
1244 .calculate_federation_id(),
1245 rand::rngs::OsRng,
1246 )
1247 .await?;
1248 (
1249 PayType::Lightning(operation_id),
1250 output,
1251 output_sm,
1252 contract_id,
1253 )
1254 };
1255
1256 if let Ok(Some(contract)) = self.module_api.fetch_contract(contract_id).await {
1258 if contract.amount.msats != 0 {
1259 bail!(PayBolt11InvoiceError::FundedContractAlreadyExists { contract_id });
1260 }
1261 }
1262
1263 let fee = match &client_output.output {
1266 LightningOutputV0::Contract(contract) => {
1267 let fee_msat = contract
1268 .amount
1269 .msats
1270 .checked_sub(
1271 invoice
1272 .amount_milli_satoshis()
1273 .ok_or(anyhow!("MissingInvoiceAmount"))?,
1274 )
1275 .expect("Contract amount should be greater or equal than invoice amount");
1276 Amount::from_msats(fee_msat)
1277 }
1278 _ => unreachable!("User client will only create contract outputs on spend"),
1279 };
1280
1281 let output = self.client_ctx.make_client_outputs(ClientOutputBundle::new(
1282 vec![ClientOutput {
1283 output: LightningOutput::V0(client_output.output),
1284 amount: client_output.amount,
1285 }],
1286 vec![client_output_sm],
1287 ));
1288
1289 let tx = TransactionBuilder::new().with_outputs(output);
1290 let extra_meta =
1291 serde_json::to_value(extra_meta).context("Failed to serialize extra meta")?;
1292 let operation_meta_gen = move |change_range: OutPointRange| LightningOperationMeta {
1293 variant: LightningOperationMetaVariant::Pay(LightningOperationMetaPay {
1294 out_point: OutPoint {
1295 txid: change_range.txid(),
1296 out_idx: 0,
1297 },
1298 invoice: invoice.clone(),
1299 fee,
1300 change: change_range.into_iter().collect(),
1301 is_internal_payment,
1302 contract_id,
1303 gateway_id: maybe_gateway_id,
1304 }),
1305 extra_meta: extra_meta.clone(),
1306 };
1307
1308 dbtx.commit_tx_result().await?;
1311
1312 self.client_ctx
1313 .finalize_and_submit_transaction(
1314 operation_id,
1315 LightningCommonInit::KIND.as_str(),
1316 operation_meta_gen,
1317 tx,
1318 )
1319 .await?;
1320
1321 Ok(OutgoingLightningPayment {
1322 payment_type: pay_type,
1323 contract_id,
1324 fee,
1325 })
1326 }
1327
1328 pub async fn get_ln_pay_details_for(
1329 &self,
1330 operation_id: OperationId,
1331 ) -> anyhow::Result<LightningOperationMetaPay> {
1332 let operation = self.client_ctx.get_operation(operation_id).await?;
1333 let LightningOperationMetaVariant::Pay(pay) =
1334 operation.meta::<LightningOperationMeta>().variant
1335 else {
1336 anyhow::bail!("Operation is not a lightning payment")
1337 };
1338 Ok(pay)
1339 }
1340
1341 pub async fn subscribe_internal_pay(
1342 &self,
1343 operation_id: OperationId,
1344 ) -> anyhow::Result<UpdateStreamOrOutcome<InternalPayState>> {
1345 let operation = self.client_ctx.get_operation(operation_id).await?;
1346
1347 let LightningOperationMetaVariant::Pay(LightningOperationMetaPay {
1348 out_point: _,
1349 invoice: _,
1350 change: _, is_internal_payment,
1352 ..
1353 }) = operation.meta::<LightningOperationMeta>().variant
1354 else {
1355 bail!("Operation is not a lightning payment")
1356 };
1357
1358 ensure!(
1359 is_internal_payment,
1360 "Subscribing to an external LN payment, expected internal LN payment"
1361 );
1362
1363 let mut stream = self.notifier.subscribe(operation_id).await;
1364 let client_ctx = self.client_ctx.clone();
1365
1366 Ok(self.client_ctx.outcome_or_updates(operation, operation_id, move || {
1367 stream! {
1368 yield InternalPayState::Funding;
1369
1370 let state = loop {
1371 match stream.next().await { Some(LightningClientStateMachines::InternalPay(state)) => {
1372 match state.state {
1373 IncomingSmStates::Preimage(preimage) => break InternalPayState::Preimage(preimage),
1374 IncomingSmStates::RefundSubmitted{ out_points, error } => {
1375 match client_ctx.await_primary_module_outputs(operation_id, out_points.clone()).await {
1376 Ok(()) => break InternalPayState::RefundSuccess { out_points, error },
1377 Err(e) => break InternalPayState::RefundError{ error_message: e.to_string(), error },
1378 }
1379 },
1380 IncomingSmStates::FundingFailed { error } => break InternalPayState::FundingFailed{ error },
1381 _ => {}
1382 }
1383 } _ => {
1384 break InternalPayState::UnexpectedError("Unexpected State! Expected an InternalPay state".to_string())
1385 }}
1386 };
1387 yield state;
1388 }
1389 }))
1390 }
1391
1392 pub async fn subscribe_ln_pay(
1395 &self,
1396 operation_id: OperationId,
1397 ) -> anyhow::Result<UpdateStreamOrOutcome<LnPayState>> {
1398 async fn get_next_pay_state(
1399 stream: &mut BoxStream<'_, LightningClientStateMachines>,
1400 ) -> Option<LightningPayStates> {
1401 match stream.next().await {
1402 Some(LightningClientStateMachines::LightningPay(state)) => Some(state.state),
1403 Some(event) => {
1404 error!(?event, "Operation is not a lightning payment");
1405 debug_assert!(false, "Operation is not a lightning payment: {event:?}");
1406 None
1407 }
1408 None => None,
1409 }
1410 }
1411
1412 let operation = self.client_ctx.get_operation(operation_id).await?;
1413 let LightningOperationMetaVariant::Pay(LightningOperationMetaPay {
1414 out_point: _,
1415 invoice: _,
1416 change,
1417 is_internal_payment,
1418 ..
1419 }) = operation.meta::<LightningOperationMeta>().variant
1420 else {
1421 bail!("Operation is not a lightning payment")
1422 };
1423
1424 ensure!(
1425 !is_internal_payment,
1426 "Subscribing to an internal LN payment, expected external LN payment"
1427 );
1428
1429 let client_ctx = self.client_ctx.clone();
1430
1431 Ok(self.client_ctx.outcome_or_updates(operation, operation_id, move || {
1432 stream! {
1433 let self_ref = client_ctx.self_ref();
1434
1435 let mut stream = self_ref.notifier.subscribe(operation_id).await;
1436 let state = get_next_pay_state(&mut stream).await;
1437 match state {
1438 Some(LightningPayStates::CreatedOutgoingLnContract(_)) => {
1439 yield LnPayState::Created;
1440 }
1441 Some(LightningPayStates::FundingRejected) => {
1442 yield LnPayState::Canceled;
1443 return;
1444 }
1445 Some(state) => {
1446 yield LnPayState::UnexpectedError { error_message: format!("Found unexpected state during lightning payment: {state:?}") };
1447 return;
1448 }
1449 None => {
1450 error!("Unexpected end of lightning pay state machine");
1451 return;
1452 }
1453 }
1454
1455 let state = get_next_pay_state(&mut stream).await;
1456 match state {
1457 Some(LightningPayStates::Funded(funded)) => {
1458 yield LnPayState::Funded { block_height: funded.timelock }
1459 }
1460 Some(state) => {
1461 yield LnPayState::UnexpectedError { error_message: format!("Found unexpected state during lightning payment: {state:?}") };
1462 return;
1463 }
1464 _ => {
1465 error!("Unexpected end of lightning pay state machine");
1466 return;
1467 }
1468 }
1469
1470 let state = get_next_pay_state(&mut stream).await;
1471 match state {
1472 Some(LightningPayStates::Success(preimage)) => {
1473 if change.is_empty() {
1474 yield LnPayState::Success { preimage };
1475 } else {
1476 yield LnPayState::AwaitingChange;
1477 match client_ctx.await_primary_module_outputs(operation_id, change.clone()).await {
1478 Ok(()) => {
1479 yield LnPayState::Success { preimage };
1480 }
1481 Err(e) => {
1482 yield LnPayState::UnexpectedError { error_message: format!("Error occurred while waiting for the change: {e:?}") };
1483 }
1484 }
1485 }
1486 }
1487 Some(LightningPayStates::Refund(refund)) => {
1488 yield LnPayState::WaitingForRefund {
1489 error_reason: refund.error_reason.clone(),
1490 };
1491
1492 match client_ctx.await_primary_module_outputs(operation_id, refund.out_points).await {
1493 Ok(()) => {
1494 let gateway_error = GatewayPayError::GatewayInternalError { error_code: Some(500), error_message: refund.error_reason };
1495 yield LnPayState::Refunded { gateway_error };
1496 }
1497 Err(e) => {
1498 yield LnPayState::UnexpectedError {
1499 error_message: format!("Error occurred trying to get refund. Refund was not successful: {e:?}"),
1500 };
1501 }
1502 }
1503 }
1504 Some(state) => {
1505 yield LnPayState::UnexpectedError { error_message: format!("Found unexpected state during lightning payment: {state:?}") };
1506 }
1507 None => {
1508 error!("Unexpected end of lightning pay state machine");
1509 yield LnPayState::UnexpectedError { error_message: "Unexpected end of lightning pay state machine".to_string() };
1510 }
1511 }
1512 }
1513 }))
1514 }
1515
1516 #[deprecated(since = "0.7.0", note = "Use recurring payment functionality instead")]
1519 #[allow(deprecated)]
1520 pub async fn scan_receive_for_user_tweaked<M: Serialize + Send + Sync + Clone>(
1521 &self,
1522 key_pair: Keypair,
1523 indices: Vec<u64>,
1524 extra_meta: M,
1525 ) -> Vec<OperationId> {
1526 let mut claims = Vec::new();
1527 for i in indices {
1528 let key_pair_tweaked = tweak_user_secret_key(&self.secp, key_pair, i);
1529 match self
1530 .scan_receive_for_user(key_pair_tweaked, extra_meta.clone())
1531 .await
1532 {
1533 Ok(operation_id) => claims.push(operation_id),
1534 Err(e) => {
1535 error!(?e, ?i, "Failed to scan tweaked key at index i");
1536 }
1537 }
1538 }
1539
1540 claims
1541 }
1542
1543 #[deprecated(since = "0.7.0", note = "Use recurring payment functionality instead")]
1546 #[allow(deprecated)]
1547 pub async fn scan_receive_for_user<M: Serialize + Send + Sync>(
1548 &self,
1549 key_pair: Keypair,
1550 extra_meta: M,
1551 ) -> anyhow::Result<OperationId> {
1552 let preimage_key: [u8; 33] = key_pair.public_key().serialize();
1553 let preimage = sha256::Hash::hash(&preimage_key);
1554 let contract_id = ContractId::from_raw_hash(sha256::Hash::hash(&preimage.to_byte_array()));
1555 self.claim_funded_incoming_contract(key_pair, contract_id, extra_meta)
1556 .await
1557 }
1558
1559 #[deprecated(since = "0.7.0", note = "Use recurring payment functionality instead")]
1562 #[allow(deprecated)]
1563 pub async fn claim_funded_incoming_contract<M: Serialize + Send + Sync>(
1564 &self,
1565 key_pair: Keypair,
1566 contract_id: ContractId,
1567 extra_meta: M,
1568 ) -> anyhow::Result<OperationId> {
1569 let incoming_contract_account = get_incoming_contract(self.module_api.clone(), contract_id)
1570 .await?
1571 .ok_or(anyhow!("No contract account found"))
1572 .with_context(|| format!("No contract found for {contract_id:?}"))?;
1573
1574 let input = incoming_contract_account.claim();
1575 let client_input = ClientInput::<LightningInput> {
1576 input,
1577 amount: incoming_contract_account.amount,
1578 keys: vec![key_pair],
1579 };
1580
1581 let tx = TransactionBuilder::new().with_inputs(
1582 self.client_ctx
1583 .make_client_inputs(ClientInputBundle::new_no_sm(vec![client_input])),
1584 );
1585 let extra_meta = serde_json::to_value(extra_meta).expect("extra_meta is serializable");
1586 let operation_meta_gen = move |change_range: OutPointRange| LightningOperationMeta {
1587 variant: LightningOperationMetaVariant::Claim {
1588 out_points: change_range.into_iter().collect(),
1589 },
1590 extra_meta: extra_meta.clone(),
1591 };
1592 let operation_id = OperationId::new_random();
1593 self.client_ctx
1594 .finalize_and_submit_transaction(
1595 operation_id,
1596 LightningCommonInit::KIND.as_str(),
1597 operation_meta_gen,
1598 tx,
1599 )
1600 .await?;
1601 Ok(operation_id)
1602 }
1603
1604 pub async fn create_bolt11_invoice<M: Serialize + Send + Sync>(
1606 &self,
1607 amount: Amount,
1608 description: lightning_invoice::Bolt11InvoiceDescription<'_>,
1609 expiry_time: Option<u64>,
1610 extra_meta: M,
1611 gateway: Option<LightningGateway>,
1612 ) -> anyhow::Result<(OperationId, Bolt11Invoice, [u8; 32])> {
1613 let receiving_key =
1614 ReceivingKey::Personal(Keypair::new(&self.secp, &mut rand::rngs::OsRng));
1615 self.create_bolt11_invoice_internal(
1616 amount,
1617 description,
1618 expiry_time,
1619 receiving_key,
1620 extra_meta,
1621 gateway,
1622 )
1623 .await
1624 }
1625
1626 #[allow(clippy::too_many_arguments)]
1629 pub async fn create_bolt11_invoice_for_user_tweaked<M: Serialize + Send + Sync>(
1630 &self,
1631 amount: Amount,
1632 description: lightning_invoice::Bolt11InvoiceDescription<'_>,
1633 expiry_time: Option<u64>,
1634 user_key: PublicKey,
1635 index: u64,
1636 extra_meta: M,
1637 gateway: Option<LightningGateway>,
1638 ) -> anyhow::Result<(OperationId, Bolt11Invoice, [u8; 32])> {
1639 let tweaked_key = tweak_user_key(&self.secp, user_key, index);
1640 self.create_bolt11_invoice_for_user(
1641 amount,
1642 description,
1643 expiry_time,
1644 tweaked_key,
1645 extra_meta,
1646 gateway,
1647 )
1648 .await
1649 }
1650
1651 pub async fn create_bolt11_invoice_for_user<M: Serialize + Send + Sync>(
1653 &self,
1654 amount: Amount,
1655 description: lightning_invoice::Bolt11InvoiceDescription<'_>,
1656 expiry_time: Option<u64>,
1657 user_key: PublicKey,
1658 extra_meta: M,
1659 gateway: Option<LightningGateway>,
1660 ) -> anyhow::Result<(OperationId, Bolt11Invoice, [u8; 32])> {
1661 let receiving_key = ReceivingKey::External(user_key);
1662 self.create_bolt11_invoice_internal(
1663 amount,
1664 description,
1665 expiry_time,
1666 receiving_key,
1667 extra_meta,
1668 gateway,
1669 )
1670 .await
1671 }
1672
1673 async fn create_bolt11_invoice_internal<M: Serialize + Send + Sync>(
1675 &self,
1676 amount: Amount,
1677 description: lightning_invoice::Bolt11InvoiceDescription<'_>,
1678 expiry_time: Option<u64>,
1679 receiving_key: ReceivingKey,
1680 extra_meta: M,
1681 gateway: Option<LightningGateway>,
1682 ) -> anyhow::Result<(OperationId, Bolt11Invoice, [u8; 32])> {
1683 let gateway_id = gateway.as_ref().map(|g| g.gateway_id);
1684 let (src_node_id, short_channel_id, route_hints) = if let Some(current_gateway) = gateway {
1685 (
1686 current_gateway.node_pub_key,
1687 current_gateway.federation_index,
1688 current_gateway.route_hints,
1689 )
1690 } else {
1691 let markers = self.client_ctx.get_internal_payment_markers()?;
1693 (markers.0, markers.1, vec![])
1694 };
1695
1696 debug!(target: LOG_CLIENT_MODULE_LN, ?gateway_id, %amount, "Selected LN gateway for invoice generation");
1697
1698 let (operation_id, invoice, output, preimage) = self.create_lightning_receive_output(
1699 amount,
1700 description,
1701 receiving_key,
1702 rand::rngs::OsRng,
1703 expiry_time,
1704 src_node_id,
1705 short_channel_id,
1706 &route_hints,
1707 self.cfg.network.0,
1708 )?;
1709
1710 let tx =
1711 TransactionBuilder::new().with_outputs(self.client_ctx.make_client_outputs(output));
1712 let extra_meta = serde_json::to_value(extra_meta).expect("extra_meta is serializable");
1713 let operation_meta_gen = {
1714 let invoice = invoice.clone();
1715 move |change_range: OutPointRange| LightningOperationMeta {
1716 variant: LightningOperationMetaVariant::Receive {
1717 out_point: OutPoint {
1718 txid: change_range.txid(),
1719 out_idx: 0,
1720 },
1721 invoice: invoice.clone(),
1722 gateway_id,
1723 },
1724 extra_meta: extra_meta.clone(),
1725 }
1726 };
1727 let change_range = self
1728 .client_ctx
1729 .finalize_and_submit_transaction(
1730 operation_id,
1731 LightningCommonInit::KIND.as_str(),
1732 operation_meta_gen,
1733 tx,
1734 )
1735 .await?;
1736
1737 debug!(target: LOG_CLIENT_MODULE_LN, txid = ?change_range.txid(), ?operation_id, "Waiting for LN invoice to be confirmed");
1738
1739 self.client_ctx
1742 .transaction_updates(operation_id)
1743 .await
1744 .await_tx_accepted(change_range.txid())
1745 .await
1746 .map_err(|e| anyhow!("Offer transaction was not accepted: {e:?}"))?;
1747
1748 debug!(target: LOG_CLIENT_MODULE_LN, %invoice, "Invoice confirmed");
1749
1750 Ok((operation_id, invoice, preimage))
1751 }
1752
1753 #[deprecated(since = "0.7.0", note = "Use recurring payment functionality instead")]
1754 #[allow(deprecated)]
1755 pub async fn subscribe_ln_claim(
1756 &self,
1757 operation_id: OperationId,
1758 ) -> anyhow::Result<UpdateStreamOrOutcome<LnReceiveState>> {
1759 let operation = self.client_ctx.get_operation(operation_id).await?;
1760 let LightningOperationMetaVariant::Claim { out_points } =
1761 operation.meta::<LightningOperationMeta>().variant
1762 else {
1763 bail!("Operation is not a lightning claim")
1764 };
1765
1766 let client_ctx = self.client_ctx.clone();
1767
1768 Ok(self.client_ctx.outcome_or_updates(operation, operation_id, move || {
1769 stream! {
1770 yield LnReceiveState::AwaitingFunds;
1771
1772 if client_ctx.await_primary_module_outputs(operation_id, out_points).await.is_ok() {
1773 yield LnReceiveState::Claimed;
1774 } else {
1775 yield LnReceiveState::Canceled { reason: LightningReceiveError::ClaimRejected }
1776 }
1777 }
1778 }))
1779 }
1780
1781 pub async fn subscribe_ln_receive(
1782 &self,
1783 operation_id: OperationId,
1784 ) -> anyhow::Result<UpdateStreamOrOutcome<LnReceiveState>> {
1785 let operation = self.client_ctx.get_operation(operation_id).await?;
1786 let LightningOperationMetaVariant::Receive {
1787 out_point, invoice, ..
1788 } = operation.meta::<LightningOperationMeta>().variant
1789 else {
1790 bail!("Operation is not a lightning payment")
1791 };
1792
1793 let tx_accepted_future = self
1794 .client_ctx
1795 .transaction_updates(operation_id)
1796 .await
1797 .await_tx_accepted(out_point.txid);
1798
1799 let client_ctx = self.client_ctx.clone();
1800
1801 Ok(self.client_ctx.outcome_or_updates(operation, operation_id, move || {
1802 stream! {
1803
1804 let self_ref = client_ctx.self_ref();
1805
1806 yield LnReceiveState::Created;
1807
1808 if tx_accepted_future.await.is_err() {
1809 yield LnReceiveState::Canceled { reason: LightningReceiveError::Rejected };
1810 return;
1811 }
1812 yield LnReceiveState::WaitingForPayment { invoice: invoice.to_string(), timeout: invoice.expiry_time() };
1813
1814 match self_ref.await_receive_success(operation_id).await {
1815 Ok(is_external) if is_external => {
1816 yield LnReceiveState::Claimed;
1818 return;
1819 }
1820 Ok(_) => {
1821
1822 yield LnReceiveState::Funded;
1823
1824 if let Ok(out_points) = self_ref.await_claim_acceptance(operation_id).await {
1825 yield LnReceiveState::AwaitingFunds;
1826
1827 if client_ctx.await_primary_module_outputs(operation_id, out_points).await.is_ok() {
1828 yield LnReceiveState::Claimed;
1829 return;
1830 }
1831 }
1832
1833 yield LnReceiveState::Canceled { reason: LightningReceiveError::Rejected };
1834 }
1835 Err(e) => {
1836 yield LnReceiveState::Canceled { reason: e };
1837 }
1838 }
1839 }
1840 }))
1841 }
1842
1843 pub async fn get_gateway(
1847 &self,
1848 gateway_id: Option<secp256k1::PublicKey>,
1849 force_internal: bool,
1850 ) -> anyhow::Result<Option<LightningGateway>> {
1851 match gateway_id {
1852 Some(gateway_id) => {
1853 if let Some(gw) = self.select_gateway(&gateway_id).await {
1854 Ok(Some(gw))
1855 } else {
1856 self.update_gateway_cache().await?;
1859 Ok(self.select_gateway(&gateway_id).await)
1860 }
1861 }
1862 None if !force_internal => {
1863 self.update_gateway_cache().await?;
1865 let gateways = self.list_gateways().await;
1866 let gw = gateways.into_iter().choose(&mut OsRng).map(|gw| gw.info);
1867 if let Some(gw) = gw {
1868 let gw_id = gw.gateway_id;
1869 info!(%gw_id, "Using random gateway");
1870 Ok(Some(gw))
1871 } else {
1872 Err(anyhow!(
1873 "No gateways exist in gateway cache and `force_internal` is false"
1874 ))
1875 }
1876 }
1877 None => Ok(None),
1878 }
1879 }
1880
1881 pub async fn wait_for_ln_payment(
1882 &self,
1883 payment_type: PayType,
1884 contract_id: ContractId,
1885 return_on_funding: bool,
1886 ) -> anyhow::Result<Option<serde_json::Value>> {
1887 match payment_type {
1888 PayType::Internal(operation_id) => {
1889 let mut updates = self
1890 .subscribe_internal_pay(operation_id)
1891 .await?
1892 .into_stream();
1893
1894 while let Some(update) = updates.next().await {
1895 match update {
1896 InternalPayState::Preimage(preimage) => {
1897 return Ok(Some(
1898 serde_json::to_value(PayInvoiceResponse {
1899 operation_id,
1900 contract_id,
1901 preimage: preimage.consensus_encode_to_hex(),
1902 })
1903 .unwrap(),
1904 ));
1905 }
1906 InternalPayState::RefundSuccess { out_points, error } => {
1907 let e = format!(
1908 "Internal payment failed. A refund was issued to {out_points:?} Error: {error}"
1909 );
1910 bail!("{e}");
1911 }
1912 InternalPayState::UnexpectedError(e) => {
1913 bail!("{e}");
1914 }
1915 InternalPayState::Funding if return_on_funding => return Ok(None),
1916 InternalPayState::Funding => {}
1917 InternalPayState::RefundError {
1918 error_message,
1919 error,
1920 } => bail!("RefundError: {error_message} {error}"),
1921 InternalPayState::FundingFailed { error } => {
1922 bail!("FundingFailed: {error}")
1923 }
1924 }
1925 debug!(target: LOG_CLIENT_MODULE_LN, ?update, "Wait for ln payment state update");
1926 }
1927 }
1928 PayType::Lightning(operation_id) => {
1929 let mut updates = self.subscribe_ln_pay(operation_id).await?.into_stream();
1930
1931 while let Some(update) = updates.next().await {
1932 match update {
1933 LnPayState::Success { preimage } => {
1934 return Ok(Some(
1935 serde_json::to_value(PayInvoiceResponse {
1936 operation_id,
1937 contract_id,
1938 preimage,
1939 })
1940 .unwrap(),
1941 ));
1942 }
1943 LnPayState::Refunded { gateway_error } => {
1944 return Ok(Some(json! {
1946 {
1947 "status": "refunded",
1948 "gateway_error": gateway_error.to_string(),
1949 }
1950 }));
1951 }
1952 LnPayState::Funded { block_height: _ } if return_on_funding => {
1953 return Ok(None);
1954 }
1955 LnPayState::Created
1956 | LnPayState::AwaitingChange
1957 | LnPayState::WaitingForRefund { .. }
1958 | LnPayState::Funded { block_height: _ } => {}
1959 LnPayState::UnexpectedError { error_message } => {
1960 bail!("UnexpectedError: {error_message}")
1961 }
1962 LnPayState::Canceled => bail!("Funding transaction was rejected"),
1963 }
1964 debug!(target: LOG_CLIENT_MODULE_LN, ?update, "Wait for ln payment state update");
1965 }
1966 }
1967 };
1968 bail!("Lightning Payment failed")
1969 }
1970}
1971
1972#[derive(Debug, Clone, Serialize, Deserialize)]
1975#[serde(rename_all = "snake_case")]
1976pub struct PayInvoiceResponse {
1977 operation_id: OperationId,
1978 contract_id: ContractId,
1979 preimage: String,
1980}
1981
1982#[allow(clippy::large_enum_variant)]
1983#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
1984pub enum LightningClientStateMachines {
1985 InternalPay(IncomingStateMachine),
1986 LightningPay(LightningPayStateMachine),
1987 Receive(LightningReceiveStateMachine),
1988}
1989
1990impl IntoDynInstance for LightningClientStateMachines {
1991 type DynType = DynState;
1992
1993 fn into_dyn(self, instance_id: ModuleInstanceId) -> Self::DynType {
1994 DynState::from_typed(instance_id, self)
1995 }
1996}
1997
1998impl State for LightningClientStateMachines {
1999 type ModuleContext = LightningClientContext;
2000
2001 fn transitions(
2002 &self,
2003 context: &Self::ModuleContext,
2004 global_context: &DynGlobalClientContext,
2005 ) -> Vec<StateTransition<Self>> {
2006 match self {
2007 LightningClientStateMachines::InternalPay(internal_pay_state) => {
2008 sm_enum_variant_translation!(
2009 internal_pay_state.transitions(context, global_context),
2010 LightningClientStateMachines::InternalPay
2011 )
2012 }
2013 LightningClientStateMachines::LightningPay(lightning_pay_state) => {
2014 sm_enum_variant_translation!(
2015 lightning_pay_state.transitions(context, global_context),
2016 LightningClientStateMachines::LightningPay
2017 )
2018 }
2019 LightningClientStateMachines::Receive(receive_state) => {
2020 sm_enum_variant_translation!(
2021 receive_state.transitions(context, global_context),
2022 LightningClientStateMachines::Receive
2023 )
2024 }
2025 }
2026 }
2027
2028 fn operation_id(&self) -> OperationId {
2029 match self {
2030 LightningClientStateMachines::InternalPay(internal_pay_state) => {
2031 internal_pay_state.operation_id()
2032 }
2033 LightningClientStateMachines::LightningPay(lightning_pay_state) => {
2034 lightning_pay_state.operation_id()
2035 }
2036 LightningClientStateMachines::Receive(receive_state) => receive_state.operation_id(),
2037 }
2038 }
2039}
2040
2041async fn fetch_and_validate_offer(
2042 module_api: &DynModuleApi,
2043 payment_hash: sha256::Hash,
2044 amount_msat: Amount,
2045) -> anyhow::Result<IncomingContractOffer, IncomingSmError> {
2046 let offer = timeout(Duration::from_secs(5), module_api.fetch_offer(payment_hash))
2047 .await
2048 .map_err(|_| IncomingSmError::TimeoutFetchingOffer { payment_hash })?
2049 .map_err(|e| IncomingSmError::FetchContractError {
2050 payment_hash,
2051 error_message: e.to_string(),
2052 })?;
2053
2054 if offer.amount > amount_msat {
2055 return Err(IncomingSmError::ViolatedFeePolicy {
2056 offer_amount: offer.amount,
2057 payment_amount: amount_msat,
2058 });
2059 }
2060 if offer.hash != payment_hash {
2061 return Err(IncomingSmError::InvalidOffer {
2062 offer_hash: offer.hash,
2063 payment_hash,
2064 });
2065 }
2066 Ok(offer)
2067}
2068
2069pub async fn create_incoming_contract_output(
2070 module_api: &DynModuleApi,
2071 payment_hash: sha256::Hash,
2072 amount_msat: Amount,
2073 redeem_key: &Keypair,
2074) -> Result<(LightningOutputV0, Amount, ContractId), IncomingSmError> {
2075 let offer = fetch_and_validate_offer(module_api, payment_hash, amount_msat).await?;
2076 let our_pub_key = secp256k1::PublicKey::from_keypair(redeem_key);
2077 let contract = IncomingContract {
2078 hash: offer.hash,
2079 encrypted_preimage: offer.encrypted_preimage.clone(),
2080 decrypted_preimage: DecryptedPreimage::Pending,
2081 gateway_key: our_pub_key,
2082 };
2083 let contract_id = contract.contract_id();
2084 let incoming_output = LightningOutputV0::Contract(ContractOutput {
2085 amount: offer.amount,
2086 contract: Contract::Incoming(contract),
2087 });
2088
2089 Ok((incoming_output, offer.amount, contract_id))
2090}
2091
2092#[derive(Debug, Encodable, Decodable, Serialize)]
2093pub struct OutgoingLightningPayment {
2094 pub payment_type: PayType,
2095 pub contract_id: ContractId,
2096 pub fee: Amount,
2097}
2098
2099async fn set_payment_result(
2100 dbtx: &mut DatabaseTransaction<'_>,
2101 payment_hash: sha256::Hash,
2102 payment_type: PayType,
2103 contract_id: ContractId,
2104 fee: Amount,
2105) {
2106 if let Some(mut payment_result) = dbtx.get_value(&PaymentResultKey { payment_hash }).await {
2107 payment_result.completed_payment = Some(OutgoingLightningPayment {
2108 payment_type,
2109 contract_id,
2110 fee,
2111 });
2112 dbtx.insert_entry(&PaymentResultKey { payment_hash }, &payment_result)
2113 .await;
2114 }
2115}
2116
2117pub fn tweak_user_key<Ctx: Verification + Signing>(
2120 secp: &Secp256k1<Ctx>,
2121 user_key: PublicKey,
2122 index: u64,
2123) -> PublicKey {
2124 let mut hasher = HmacEngine::<sha256::Hash>::new(&user_key.serialize()[..]);
2125 hasher.input(&index.to_be_bytes());
2126 let tweak = Hmac::from_engine(hasher).to_byte_array();
2127
2128 user_key
2129 .add_exp_tweak(secp, &Scalar::from_be_bytes(tweak).expect("can't fail"))
2130 .expect("tweak is always 32 bytes, other failure modes are negligible")
2131}
2132
2133fn tweak_user_secret_key<Ctx: Verification + Signing>(
2136 secp: &Secp256k1<Ctx>,
2137 key_pair: Keypair,
2138 index: u64,
2139) -> Keypair {
2140 let public_key = key_pair.public_key();
2141 let mut hasher = HmacEngine::<sha256::Hash>::new(&public_key.serialize()[..]);
2142 hasher.input(&index.to_be_bytes());
2143 let tweak = Hmac::from_engine(hasher).to_byte_array();
2144
2145 let secret_key = key_pair.secret_key();
2146 let sk_tweaked = secret_key
2147 .add_tweak(&Scalar::from_be_bytes(tweak).expect("Cant fail"))
2148 .expect("Cant fail");
2149 Keypair::from_secret_key(secp, &sk_tweaked)
2150}
2151
2152pub async fn get_invoice(
2154 info: &str,
2155 amount: Option<Amount>,
2156 lnurl_comment: Option<String>,
2157) -> anyhow::Result<Bolt11Invoice> {
2158 let info = info.trim();
2159 match lightning_invoice::Bolt11Invoice::from_str(info) {
2160 Ok(invoice) => {
2161 debug!("Parsed parameter as bolt11 invoice: {invoice}");
2162 match (invoice.amount_milli_satoshis(), amount) {
2163 (Some(_), Some(_)) => {
2164 bail!("Amount specified in both invoice and command line")
2165 }
2166 (None, _) => {
2167 bail!("We don't support invoices without an amount")
2168 }
2169 _ => {}
2170 };
2171 Ok(invoice)
2172 }
2173 Err(e) => {
2174 let lnurl = if info.to_lowercase().starts_with("lnurl") {
2175 lnurl::lnurl::LnUrl::from_str(info)?
2176 } else if info.contains('@') {
2177 lnurl::lightning_address::LightningAddress::from_str(info)?.lnurl()
2178 } else {
2179 bail!("Invalid invoice or lnurl: {e:?}");
2180 };
2181 debug!("Parsed parameter as lnurl: {lnurl:?}");
2182 let amount = amount.context("When using a lnurl, an amount must be specified")?;
2183 let async_client = lnurl::AsyncClient::from_client(reqwest::Client::new());
2184 let response = async_client.make_request(&lnurl.url).await?;
2185 match response {
2186 lnurl::LnUrlResponse::LnUrlPayResponse(response) => {
2187 let invoice = async_client
2188 .get_invoice(&response, amount.msats, None, lnurl_comment.as_deref())
2189 .await?;
2190 let invoice = Bolt11Invoice::from_str(invoice.invoice())?;
2191 let invoice_amount = invoice.amount_milli_satoshis();
2192 ensure!(
2193 invoice_amount == Some(amount.msats),
2194 "the amount generated by the lnurl ({invoice_amount:?}) is different from the requested amount ({amount}), try again using a different amount"
2195 );
2196 Ok(invoice)
2197 }
2198 other => {
2199 bail!("Unexpected response from lnurl: {other:?}");
2200 }
2201 }
2202 }
2203 }
2204}
2205
2206#[derive(Debug, Clone)]
2207pub struct LightningClientContext {
2208 pub ln_decoder: Decoder,
2209 pub redeem_key: Keypair,
2210 pub gateway_conn: Arc<dyn GatewayConnection + Send + Sync>,
2211}
2212
2213impl fedimint_client_module::sm::Context for LightningClientContext {
2214 const KIND: Option<ModuleKind> = Some(KIND);
2215}
2216
2217#[apply(async_trait_maybe_send!)]
2218pub trait GatewayConnection: std::fmt::Debug {
2219 async fn verify_gateway_availability(&self, gateway: &LightningGateway) -> anyhow::Result<()>;
2222
2223 async fn pay_invoice(
2225 &self,
2226 gateway: LightningGateway,
2227 payload: PayInvoicePayload,
2228 ) -> Result<String, GatewayPayError>;
2229}
2230
2231#[derive(Debug, Default)]
2232pub struct RealGatewayConnection {
2233 client: reqwest::Client,
2234}
2235
2236#[apply(async_trait_maybe_send!)]
2237impl GatewayConnection for RealGatewayConnection {
2238 async fn verify_gateway_availability(&self, gateway: &LightningGateway) -> anyhow::Result<()> {
2239 let response = self
2240 .client
2241 .get(
2242 gateway
2243 .api
2244 .join(GET_GATEWAY_ID_ENDPOINT)
2245 .expect("id contains no invalid characters for a URL")
2246 .as_str(),
2247 )
2248 .send()
2249 .await
2250 .context("Gateway is not available")?;
2251 if !response.status().is_success() {
2252 return Err(anyhow!(
2253 "Gateway is not available. Returned error code: {}",
2254 response.status()
2255 ));
2256 }
2257
2258 let text_gateway_id = response.text().await?;
2259 let gateway_id = PublicKey::from_str(&text_gateway_id[1..text_gateway_id.len() - 1])?;
2260 if gateway_id != gateway.gateway_id {
2261 return Err(anyhow!("Unexpected gateway id returned: {gateway_id}"));
2262 }
2263
2264 Ok(())
2265 }
2266
2267 async fn pay_invoice(
2268 &self,
2269 gateway: LightningGateway,
2270 payload: PayInvoicePayload,
2271 ) -> Result<String, GatewayPayError> {
2272 let response = self
2273 .client
2274 .post(
2275 gateway
2276 .api
2277 .join(PAY_INVOICE_ENDPOINT)
2278 .expect("'pay_invoice' contains no invalid characters for a URL")
2279 .as_str(),
2280 )
2281 .json(&payload)
2282 .send()
2283 .await
2284 .map_err(|e| GatewayPayError::GatewayInternalError {
2285 error_code: None,
2286 error_message: e.to_string(),
2287 })?;
2288
2289 if !response.status().is_success() {
2290 return Err(GatewayPayError::GatewayInternalError {
2291 error_code: Some(response.status().as_u16()),
2292 error_message: response
2293 .text()
2294 .await
2295 .expect("Could not retrieve text from response"),
2296 });
2297 }
2298
2299 let preimage =
2300 response
2301 .text()
2302 .await
2303 .map_err(|_| GatewayPayError::GatewayInternalError {
2304 error_code: None,
2305 error_message: "Error retrieving preimage from response".to_string(),
2306 })?;
2307 let length = preimage.len();
2308 Ok(preimage[1..length - 1].to_string())
2309 }
2310}
2311
2312#[derive(Debug)]
2313pub struct MockGatewayConnection;
2314
2315#[apply(async_trait_maybe_send!)]
2316impl GatewayConnection for MockGatewayConnection {
2317 async fn verify_gateway_availability(&self, _gateway: &LightningGateway) -> anyhow::Result<()> {
2318 Ok(())
2319 }
2320
2321 async fn pay_invoice(
2322 &self,
2323 _gateway: LightningGateway,
2324 _payload: PayInvoicePayload,
2325 ) -> Result<String, GatewayPayError> {
2326 Ok("00000000".to_string())
2328 }
2329}