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