1use std::collections::{HashMap, HashSet};
2use std::sync::Arc;
3use std::time::Duration;
4
5use anyhow::anyhow;
6use fedimint_client::meta::MetaService;
7use fedimint_client::{Client, ClientHandleArc, ClientModule, ClientModuleInstance};
8use fedimint_client_module::meta::LegacyMetaSource;
9use fedimint_connectors::ConnectorRegistry;
10use fedimint_core::config::FederationId;
11use fedimint_core::core::OperationId;
12use fedimint_core::db::{
13 AutocommitResultExt, Database, DatabaseTransaction, IDatabaseTransactionOpsCoreTyped,
14 IRawDatabase,
15};
16use fedimint_core::encoding::{Decodable, Encodable};
17use fedimint_core::invite_code::InviteCode;
18use fedimint_core::secp256k1::hashes::sha256;
19use fedimint_core::secp256k1::{PublicKey, SECP256K1};
20use fedimint_core::task::timeout;
21use fedimint_core::util::{FmtCompact, FmtCompactAnyhow, SafeUrl};
22use fedimint_core::{Amount, BitcoinHash, runtime};
23use fedimint_derive_secret::DerivableSecret;
24use fedimint_ln_client::common::{LightningGateway, LightningGatewayAnnouncement};
25use fedimint_ln_client::recurring::{
26 PaymentCodeId, PaymentCodeRootKey, RecurringPaymentError, RecurringPaymentProtocol,
27};
28use fedimint_ln_client::{
29 LightningClientInit, LightningClientModule, LightningOperationMeta,
30 LightningOperationMetaVariant, LnReceiveState, tweak_user_key,
31};
32use fedimint_lnurl::{PayResponse, encode_lnurl, pay_request_tag};
33use fedimint_meta_client::MetaModuleMetaSourceWithFallback;
34use fedimint_mint_client::MintClientInit;
35use futures::StreamExt;
36use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription, Sha256};
37use serde::{Deserialize, Serialize};
38use tokio::sync::{Notify, RwLock, watch};
39use tracing::{debug, info, warn};
40
41use crate::db::{
42 FederationDbPrefix, PaymentCodeEntry, PaymentCodeInvoiceEntry, PaymentCodeInvoiceKey,
43 PaymentCodeKey, PaymentCodeNextInvoiceIndexKey, PaymentCodeVariant, SchemaVersionKey,
44 load_federation_client_databases, open_client_db, try_add_federation_database,
45};
46
47mod db;
48
49#[derive(Clone)]
50pub struct RecurringInvoiceServer {
51 db: Database,
52 connectors: ConnectorRegistry,
53 clients: Arc<RwLock<HashMap<FederationId, ClientHandleArc>>>,
54 gateway_cache: Arc<RwLock<HashMap<FederationId, watch::Receiver<Vec<CachedGateway>>>>>,
55 invoice_generated: Arc<Notify>,
56 base_url: SafeUrl,
57}
58
59#[derive(Clone)]
60struct CachedGateway {
61 gateway: LightningGateway,
62 vetted: bool,
63}
64
65impl RecurringInvoiceServer {
66 pub async fn new(
67 connectors: ConnectorRegistry,
68 db: impl IRawDatabase + 'static,
69 base_url: SafeUrl,
70 ) -> anyhow::Result<Self> {
71 let db = Database::new(db, Default::default());
72
73 let mut clients = HashMap::<_, ClientHandleArc>::new();
74 let mut gateway_cache = HashMap::<FederationId, watch::Receiver<Vec<CachedGateway>>>::new();
75
76 for (federation_id, db) in load_federation_client_databases(&db).await {
77 let mut client_builder = Client::builder().await;
78 client_builder.with_meta_service(recurringd_meta_service());
79 client_builder.with_module(LightningClientInit::default());
80 client_builder.with_module(MintClientInit);
81 let client = client_builder
82 .open(
83 connectors.clone(),
84 db,
85 fedimint_client::RootSecret::StandardDoubleDerive(Self::default_secret()),
86 )
87 .await?;
88 let client = Arc::new(client);
89 gateway_cache.insert(
90 federation_id,
91 spawn_gateway_cache_refresh(federation_id, &client),
92 );
93 clients.insert(federation_id, client);
94 }
95
96 let slf = Self {
97 db: db.clone(),
98 clients: Arc::new(RwLock::new(clients)),
99 gateway_cache: Arc::new(RwLock::new(gateway_cache)),
100 invoice_generated: Arc::new(Default::default()),
101 base_url,
102 connectors,
103 };
104
105 slf.run_db_migrations().await;
106
107 Ok(slf)
108 }
109
110 fn default_secret() -> DerivableSecret {
114 DerivableSecret::new_root(&[], &[])
115 }
116
117 pub async fn register_federation(
118 &self,
119 invite_code: &InviteCode,
120 ) -> Result<FederationId, RecurringPaymentError> {
121 let federation_id = invite_code.federation_id();
122 info!("Registering federation {}", federation_id);
123
124 let mut clients = self.clients.write().await;
127 if clients.contains_key(&federation_id) {
128 return Err(RecurringPaymentError::FederationAlreadyRegistered(
129 federation_id,
130 ));
131 }
132
133 let client_db_prefix = FederationDbPrefix::random();
138 let client_db = open_client_db(&self.db, client_db_prefix);
139
140 match Self::join_federation_static(self.connectors.clone(), client_db, invite_code).await {
141 Ok(client) => {
142 try_add_federation_database(&self.db, federation_id, client_db_prefix)
143 .await
144 .expect("We hold a global lock, no parallel joining can happen");
145 self.gateway_cache.write().await.insert(
146 federation_id,
147 spawn_gateway_cache_refresh(federation_id, &client),
148 );
149 clients.insert(federation_id, client);
150 Ok(federation_id)
151 }
152 Err(e) => {
153 Err(e)
155 }
156 }
157 }
158
159 async fn join_federation_static(
160 connectors: ConnectorRegistry,
161 client_db: Database,
162 invite_code: &InviteCode,
163 ) -> Result<ClientHandleArc, RecurringPaymentError> {
164 let mut client_builder = Client::builder().await;
165
166 client_builder.with_meta_service(recurringd_meta_service());
167 client_builder.with_module(LightningClientInit::default());
168 client_builder.with_module(MintClientInit);
169
170 let client = client_builder
171 .preview(connectors, invite_code)
172 .await
173 .map_err(|err| {
174 RecurringPaymentError::JoiningFederationFailed(anyhow!("{}", err.fmt_compact()))
175 })?
176 .join(
177 client_db,
178 fedimint_client::RootSecret::StandardDoubleDerive(Self::default_secret()),
179 )
180 .await
181 .map_err(|err| {
182 RecurringPaymentError::JoiningFederationFailed(anyhow!("{}", err.fmt_compact()))
183 })?;
184 Ok(Arc::new(client))
185 }
186
187 pub async fn register_recurring_payment_code(
188 &self,
189 federation_id: FederationId,
190 payment_code_root_key: PaymentCodeRootKey,
191 protocol: RecurringPaymentProtocol,
192 meta: &str,
193 ) -> Result<String, RecurringPaymentError> {
194 if protocol != RecurringPaymentProtocol::LNURL {
196 return Err(RecurringPaymentError::UnsupportedProtocol(protocol));
197 }
198
199 self.get_federation_client(federation_id).await?;
201
202 let payment_code = self.create_lnurl(payment_code_root_key.to_payment_code_id());
203 let payment_code_entry = PaymentCodeEntry {
204 root_key: payment_code_root_key,
205 federation_id,
206 protocol,
207 payment_code: payment_code.clone(),
208 variant: PaymentCodeVariant::Lnurl {
209 meta: meta.to_owned(),
210 },
211 };
212
213 let mut dbtx = self.db.begin_transaction().await;
214 if let Some(existing_code) = dbtx
215 .insert_entry(
216 &PaymentCodeKey {
217 payment_code_id: payment_code_root_key.to_payment_code_id(),
218 },
219 &payment_code_entry,
220 )
221 .await
222 {
223 if existing_code != payment_code_entry {
224 return Err(RecurringPaymentError::PaymentCodeAlreadyExists(
225 payment_code_root_key,
226 ));
227 }
228
229 dbtx.ignore_uncommitted();
230 return Ok(payment_code);
231 }
232
233 dbtx.insert_new_entry(
234 &PaymentCodeNextInvoiceIndexKey {
235 payment_code_id: payment_code_root_key.to_payment_code_id(),
236 },
237 &0,
238 )
239 .await;
240 dbtx.commit_tx_result().await.map_err(anyhow::Error::from)?;
241
242 Ok(payment_code)
243 }
244
245 fn create_lnurl(&self, payment_code_id: PaymentCodeId) -> String {
246 encode_lnurl(
247 &self
248 .base_url
249 .join_path(&format!("lnv1/paycodes/{payment_code_id}"))
250 .to_string(),
251 )
252 }
253
254 pub async fn lnurl_pay(
255 &self,
256 payment_code_id: PaymentCodeId,
257 ) -> Result<PayResponse, RecurringPaymentError> {
258 let payment_code = self.get_payment_code(payment_code_id).await?;
259 let PaymentCodeVariant::Lnurl { meta } = payment_code.variant;
260
261 Ok(PayResponse {
262 callback: self
263 .base_url
264 .join_path(&format!("lnv1/paycodes/{payment_code_id}/invoice"))
265 .to_string(),
266 max_sendable: 100000000000,
267 min_sendable: 1,
268 tag: pay_request_tag(),
269 metadata: meta,
270 })
271 }
272
273 pub async fn lnurl_invoice(
274 &self,
275 payment_code_id: PaymentCodeId,
276 amount: Amount,
277 ) -> Result<LNURLPayInvoice, RecurringPaymentError> {
278 let (operation_id, federation_id, invoice) =
279 self.create_bolt11_invoice(payment_code_id, amount).await?;
280 Ok(LNURLPayInvoice {
281 pr: invoice.to_string(),
282 verify: self
283 .base_url
284 .join_path(&format!(
285 "lnv1/verify/{federation_id}/{}",
286 operation_id.fmt_full()
287 ))
288 .to_string(),
289 })
290 }
291
292 async fn create_bolt11_invoice(
293 &self,
294 payment_code_id: PaymentCodeId,
295 amount: Amount,
296 ) -> Result<(OperationId, FederationId, Bolt11Invoice), RecurringPaymentError> {
297 const DEFAULT_EXPIRY_TIME: u64 = 60 * 60 * 24;
300
301 let payment_code = self.get_payment_code(payment_code_id).await?;
302
303 let federation_client = self
304 .get_federation_client(payment_code.federation_id)
305 .await?;
306
307 let gateway = self
308 .get_cached_gateway(payment_code.federation_id, amount)
309 .await?;
310
311 let (operation_id, invoice) = self
312 .db
313 .autocommit(
314 |dbtx, _| {
315 let federation_client = federation_client.clone();
316 let payment_code = payment_code.clone();
317 let gateway = gateway.clone();
318 Box::pin(async move {
319 let mut invoice_index = self
320 .get_next_invoice_index(&mut dbtx.to_ref_nc(), payment_code_id)
321 .await;
322
323 let invoice_index = loop {
335 let operation_id =
336 operation_id_from_user_key(payment_code.root_key, invoice_index);
337
338 let Some(invoice) =
339 Self::check_if_invoice_exists(&federation_client, operation_id)
340 .await
341 else {
342 break invoice_index;
343 };
344
345 self.save_bolt11_invoice(
346 dbtx,
347 operation_id,
348 payment_code_id,
349 invoice_index,
350 invoice,
351 )
352 .await;
353
354 invoice_index = self
355 .get_next_invoice_index(&mut dbtx.to_ref_nc(), payment_code_id)
356 .await;
357 };
358
359 let federation_client_ln_module = federation_client.get_ln_module()?;
362
363 let lnurl_meta = match payment_code.variant {
364 PaymentCodeVariant::Lnurl { meta } => meta,
365 };
366 let meta_hash = Sha256(sha256::Hash::hash(lnurl_meta.as_bytes()));
367 let description = Bolt11InvoiceDescription::Hash(meta_hash);
368
369 let (operation_id, invoice, _preimage) = federation_client_ln_module
372 .create_bolt11_invoice_for_user_tweaked(
373 amount,
374 description,
375 Some(DEFAULT_EXPIRY_TIME),
376 payment_code.root_key.0,
377 invoice_index,
378 serde_json::Value::Null,
379 Some(gateway),
380 )
381 .await?;
382
383 self.save_bolt11_invoice(
384 dbtx,
385 operation_id,
386 payment_code_id,
387 invoice_index,
388 invoice.clone(),
389 )
390 .await;
391
392 Result::<_, anyhow::Error>::Ok((operation_id, invoice))
393 })
394 },
395 None,
396 )
397 .await
398 .unwrap_autocommit()?;
399
400 await_invoice_confirmed(&federation_client.get_ln_module()?, operation_id).await?;
401
402 Ok((operation_id, federation_client.federation_id(), invoice))
403 }
404
405 async fn save_bolt11_invoice(
406 &self,
407 dbtx: &mut DatabaseTransaction<'_>,
408 operation_id: OperationId,
409 payment_code_id: PaymentCodeId,
410 invoice_index: u64,
411 invoice: Bolt11Invoice,
412 ) {
413 dbtx.insert_new_entry(
414 &PaymentCodeInvoiceKey {
415 payment_code_id,
416 index: invoice_index,
417 },
418 &PaymentCodeInvoiceEntry {
419 operation_id,
420 invoice: PaymentCodeInvoice::Bolt11(invoice.clone()),
421 },
422 )
423 .await;
424
425 let invoice_generated_notifier = self.invoice_generated.clone();
426 dbtx.on_commit(move || {
427 invoice_generated_notifier.notify_waiters();
428 });
429 }
430
431 async fn check_if_invoice_exists(
432 federation_client: &ClientHandleArc,
433 operation_id: OperationId,
434 ) -> Option<Bolt11Invoice> {
435 let operation = federation_client
436 .operation_log()
437 .get_operation(operation_id)
438 .await?;
439
440 assert_eq!(
441 operation.operation_module_kind(),
442 LightningClientModule::kind().as_str()
443 );
444
445 let LightningOperationMetaVariant::Receive { invoice, .. } =
446 operation.meta::<LightningOperationMeta>().variant
447 else {
448 panic!(
449 "Unexpected operation meta variant: {:?}",
450 operation.meta::<LightningOperationMeta>().variant
451 );
452 };
453
454 Some(invoice)
455 }
456
457 async fn get_federation_client(
458 &self,
459 federation_id: FederationId,
460 ) -> Result<ClientHandleArc, RecurringPaymentError> {
461 self.clients
462 .read()
463 .await
464 .get(&federation_id)
465 .cloned()
466 .ok_or(RecurringPaymentError::UnknownFederationId(federation_id))
467 }
468
469 async fn get_cached_gateway(
470 &self,
471 federation_id: FederationId,
472 amount: Amount,
473 ) -> Result<LightningGateway, RecurringPaymentError> {
474 const EMPTY_GATEWAY_CACHE_WAIT: Duration = Duration::from_secs(60);
475
476 let mut gateway_cache = self
477 .gateway_cache
478 .read()
479 .await
480 .get(&federation_id)
481 .cloned()
482 .ok_or(RecurringPaymentError::NoGatewayFound)?;
483
484 if let Some(gateway) = select_preferred_gateway(&gateway_cache.borrow(), amount) {
485 return Ok(gateway);
486 }
487
488 timeout(EMPTY_GATEWAY_CACHE_WAIT, async {
489 loop {
490 gateway_cache
491 .changed()
492 .await
493 .map_err(|_| RecurringPaymentError::NoGatewayFound)?;
494
495 if let Some(gateway) =
496 select_preferred_gateway(&gateway_cache.borrow_and_update(), amount)
497 {
498 break Ok(gateway);
499 }
500 }
501 })
502 .await
503 .map_err(|_| RecurringPaymentError::NoGatewayFound)?
504 }
505
506 pub async fn await_invoice_index_generated(
507 &self,
508 payment_code_id: PaymentCodeId,
509 invoice_index: u64,
510 ) -> Result<PaymentCodeInvoiceEntry, RecurringPaymentError> {
511 self.get_payment_code(payment_code_id).await?;
512
513 let mut notified = self.invoice_generated.notified();
514 loop {
515 let mut dbtx = self.db.begin_transaction_nc().await;
516 if let Some(invoice_entry) = dbtx
517 .get_value(&PaymentCodeInvoiceKey {
518 payment_code_id,
519 index: invoice_index,
520 })
521 .await
522 {
523 break Ok(invoice_entry);
524 };
525
526 notified.await;
527 notified = self.invoice_generated.notified();
528 }
529 }
530
531 async fn get_next_invoice_index(
532 &self,
533 dbtx: &mut DatabaseTransaction<'_>,
534 payment_code_id: PaymentCodeId,
535 ) -> u64 {
536 let next_index = dbtx
537 .get_value(&PaymentCodeNextInvoiceIndexKey { payment_code_id })
538 .await
539 .map(|index| index + 1)
540 .unwrap_or(0);
541 dbtx.insert_entry(
542 &PaymentCodeNextInvoiceIndexKey { payment_code_id },
543 &next_index,
544 )
545 .await;
546
547 next_index
548 }
549
550 pub async fn list_federations(&self) -> Vec<FederationId> {
551 self.clients.read().await.keys().cloned().collect()
552 }
553
554 async fn get_payment_code(
555 &self,
556 payment_code_id: PaymentCodeId,
557 ) -> Result<PaymentCodeEntry, RecurringPaymentError> {
558 self.db
559 .begin_transaction_nc()
560 .await
561 .get_value(&PaymentCodeKey { payment_code_id })
562 .await
563 .ok_or(RecurringPaymentError::UnknownPaymentCode(payment_code_id))
564 }
565
566 pub async fn verify_invoice_paid(
575 &self,
576 federation_id: FederationId,
577 operation_id: OperationId,
578 ) -> Result<InvoiceStatus, RecurringPaymentError> {
579 let federation_client = self.get_federation_client(federation_id).await?;
580
581 let invoice = {
584 let operation = federation_client
585 .operation_log()
586 .get_operation(operation_id)
587 .await
588 .ok_or(RecurringPaymentError::UnknownInvoice(operation_id))?;
589
590 if operation.operation_module_kind() != LightningClientModule::kind().as_str() {
591 return Err(RecurringPaymentError::UnknownInvoice(operation_id));
592 }
593
594 let LightningOperationMetaVariant::Receive { invoice, .. } =
595 operation.meta::<LightningOperationMeta>().variant
596 else {
597 return Err(RecurringPaymentError::UnknownInvoice(operation_id));
598 };
599
600 invoice
601 };
602
603 let ln_module = federation_client
604 .get_first_module::<LightningClientModule>()
605 .map_err(|e| {
606 warn!("No compatible lightning module found {e}");
607 RecurringPaymentError::NoLightningModuleFound
608 })?;
609
610 let mut stream = ln_module
611 .subscribe_ln_receive(operation_id)
612 .await
613 .map_err(|_| RecurringPaymentError::UnknownInvoice(operation_id))?
614 .into_stream();
615 let status = loop {
616 let update = timeout(Duration::from_millis(100), stream.next()).await;
624 match update {
625 Ok(Some(LnReceiveState::Funded | LnReceiveState::Claimed)) => {
629 break PaymentStatus::Paid;
630 }
631 Ok(Some(_)) => {
633 continue;
634 }
635 Ok(None) | Err(_) => {
639 break PaymentStatus::Pending;
640 }
641 }
642 };
643
644 Ok(InvoiceStatus { invoice, status })
645 }
646
647 async fn run_db_migrations(&self) {
648 let migrations = Self::migrations();
649 let schema_version: u64 = self
650 .db
651 .begin_transaction_nc()
652 .await
653 .get_value(&SchemaVersionKey)
654 .await
655 .unwrap_or_default();
656
657 for (target_schema, migration_fn) in migrations
658 .into_iter()
659 .skip_while(|(target_schema, _)| *target_schema <= schema_version)
660 {
661 let mut dbtx = self.db.begin_transaction().await;
662 dbtx.insert_entry(&SchemaVersionKey, &target_schema).await;
663
664 migration_fn(self, dbtx.to_ref_nc()).await;
665
666 dbtx.commit_tx().await;
667 }
668 }
669}
670
671async fn await_invoice_confirmed(
672 ln_module: &ClientModuleInstance<'_, LightningClientModule>,
673 operation_id: OperationId,
674) -> Result<(), RecurringPaymentError> {
675 let mut operation_updated = ln_module
676 .subscribe_ln_receive(operation_id)
677 .await?
678 .into_stream();
679
680 while let Some(update) = operation_updated.next().await {
681 if matches!(update, LnReceiveState::WaitingForPayment { .. }) {
682 return Ok(());
683 }
684 }
685
686 Err(RecurringPaymentError::Other(anyhow!(
687 "BOLT11 invoice not confirmed"
688 )))
689}
690
691#[derive(Debug, Clone, Eq, PartialEq, Hash, Encodable, Decodable)]
692pub enum PaymentCodeInvoice {
693 Bolt11(Bolt11Invoice),
694}
695
696pub struct InvoiceStatus {
699 pub invoice: Bolt11Invoice,
700 pub status: PaymentStatus,
701}
702
703pub enum PaymentStatus {
704 Paid,
705 Pending,
706}
707
708impl PaymentStatus {
709 pub fn is_paid(&self) -> bool {
710 matches!(self, PaymentStatus::Paid)
711 }
712}
713
714#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
718pub struct LNURLPayInvoice {
719 pub pr: String,
720 pub verify: String,
721}
722
723fn operation_id_from_user_key(user_key: PaymentCodeRootKey, index: u64) -> OperationId {
724 let invoice_key = tweak_user_key(SECP256K1, user_key.0, index);
725 let preimage = sha256::Hash::hash(&invoice_key.serialize()[..]);
726 let payment_hash = sha256::Hash::hash(&preimage[..]);
727
728 OperationId(payment_hash.to_byte_array())
729}
730
731trait LnClientContextExt {
732 fn get_ln_module(
733 &'_ self,
734 ) -> Result<ClientModuleInstance<'_, LightningClientModule>, RecurringPaymentError>;
735}
736
737impl LnClientContextExt for ClientHandleArc {
738 fn get_ln_module(
739 &'_ self,
740 ) -> Result<ClientModuleInstance<'_, LightningClientModule>, RecurringPaymentError> {
741 self.get_first_module::<LightningClientModule>()
742 .map_err(|e| {
743 warn!("No compatible lightning module found {e}");
744 RecurringPaymentError::NoLightningModuleFound
745 })
746 }
747}
748
749fn recurringd_meta_service() -> Arc<MetaService> {
750 MetaService::new(MetaModuleMetaSourceWithFallback::<LegacyMetaSource>::default())
751}
752
753fn spawn_gateway_cache_refresh(
754 federation_id: FederationId,
755 client: &ClientHandleArc,
756) -> watch::Receiver<Vec<CachedGateway>> {
757 const REFRESH_INTERVAL: Duration = Duration::from_secs(10);
758
759 let (gateway_cache_sender, gateway_cache_receiver) = watch::channel(Vec::new());
760 let task_group = client.task_group().clone();
761 let client = client.clone();
762 task_group.spawn_cancellable("recurringd-gateway-cache-refresh", async move {
763 loop {
764 match select_available_gateways(&client).await {
765 Ok(gateways) => {
766 gateway_cache_sender.send_replace(gateways);
767 }
768 Err(err) => {
769 warn!(
770 federation_id = %federation_id,
771 err = %err.fmt_compact(),
772 "Failed to refresh recurringd gateway cache"
773 );
774 }
775 }
776
777 runtime::sleep(REFRESH_INTERVAL).await;
778 }
779 });
780
781 gateway_cache_receiver
782}
783
784async fn select_available_gateways(
785 client: &ClientHandleArc,
786) -> Result<Vec<CachedGateway>, RecurringPaymentError> {
787 let ln_module = client.get_ln_module()?;
788 ln_module.update_gateway_cache().await.map_err(|err| {
789 warn!(
790 err = %err.fmt_compact_anyhow(),
791 "Failed to refresh gateway announcements"
792 );
793 RecurringPaymentError::NoGatewayFound
794 })?;
795
796 let mut gateways = ln_module.list_gateways().await;
797 if gateways.is_empty() {
798 return Err(RecurringPaymentError::NoGatewayFound);
799 }
800
801 let vetted_gateway_ids = fetch_vetted_gateway_ids(client).await;
802 sort_gateways_by_preference(&mut gateways, &vetted_gateway_ids);
803
804 let mut available_gateways = Vec::new();
805 for gateway in gateways {
806 let gateway_id = gateway.info.gateway_id;
807 let vetted = gateway.vetted || vetted_gateway_ids.contains(&gateway_id);
808 match ln_module
809 .select_available_gateway(Some(gateway.info), None)
810 .await
811 {
812 Ok(gateway) => available_gateways.push(CachedGateway { gateway, vetted }),
813 Err(err) => {
814 debug!(
815 gateway_id = %gateway_id,
816 err = %err.fmt_compact_anyhow(),
817 "Gateway failed availability check"
818 );
819 }
820 }
821 }
822
823 if available_gateways.is_empty() {
824 return Err(RecurringPaymentError::NoGatewayFound);
825 }
826
827 Ok(available_gateways)
828}
829
830fn select_preferred_gateway(
831 gateways: &[CachedGateway],
832 amount: Amount,
833) -> Option<LightningGateway> {
834 gateways
835 .iter()
836 .min_by_key(|gateway| {
837 (
838 !gateway.vetted,
839 gateway_fee_msat(&gateway.gateway, amount),
840 gateway.gateway.gateway_id.serialize(),
841 )
842 })
843 .map(|gateway| gateway.gateway.clone())
844}
845
846fn gateway_fee_msat(gateway: &LightningGateway, amount: Amount) -> u64 {
847 let proportional_fee =
848 (u128::from(amount.msats) * u128::from(gateway.fees.proportional_millionths)) / 1_000_000;
849
850 u64::from(gateway.fees.base_msat)
851 .saturating_add(u64::try_from(proportional_fee).unwrap_or(u64::MAX))
852}
853
854fn sort_gateways_by_preference(
855 gateways: &mut [LightningGatewayAnnouncement],
856 vetted_gateway_ids: &HashSet<PublicKey>,
857) {
858 gateways.sort_by_cached_key(|gateway| {
859 let vetted = gateway.vetted || vetted_gateway_ids.contains(&gateway.info.gateway_id);
860 (
861 !vetted,
862 u64::from(gateway.info.fees.base_msat),
863 gateway.info.gateway_id.serialize(),
864 )
865 });
866}
867
868async fn fetch_vetted_gateway_ids(client: &ClientHandleArc) -> HashSet<PublicKey> {
869 let Some(vetted_gateways) = client
870 .meta_service()
871 .entries(client.db())
872 .await
873 .and_then(|entries| entries.get("vetted_gateways").cloned())
874 .and_then(|value| parse_vetted_gateway_ids(&value))
875 else {
876 debug!("No vetted gateways configured in federation metadata");
877 return HashSet::new();
878 };
879
880 vetted_gateways
881 .into_iter()
882 .filter_map(|gateway_id| match gateway_id.parse::<PublicKey>() {
883 Ok(gateway_id) => Some(gateway_id),
884 Err(err) => {
885 warn!(
886 %gateway_id,
887 err = %err.fmt_compact(),
888 "Failed to parse vetted gateway ID"
889 );
890 None
891 }
892 })
893 .collect()
894}
895
896fn parse_vetted_gateway_ids(value: &serde_json::Value) -> Option<Vec<String>> {
897 if let Ok(gateway_ids) = serde_json::from_value::<Vec<String>>(value.clone()) {
898 return Some(gateway_ids);
899 }
900
901 let value = value.as_str()?;
902
903 match serde_json::from_str::<Vec<String>>(value) {
906 Ok(gateway_ids) => {
907 warn!("vetted_gateways metadata should be configured as a JSON array, not a string");
908 Some(gateway_ids)
909 }
910 Err(_) => None,
911 }
912}