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