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