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