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