Skip to main content

fedimint_recurringd/
lib.rs

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    /// We don't want to hold any money or sign anything ourselves, we only use
110    /// the client with externally supplied key material and to track
111    /// ongoing progress of other users' receives.
112    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        // We lock to prevent parallel join attempts
124        // TODO: lock per federation
125        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        // We don't know if joining will succeed or be interrupted. We use a random DB
133        // prefix to initialize the client and only write the prefix to the DB if that
134        // succeeds. If it fails we end up with some orphaned data in the DB, if it ever
135        // becomes a problem we can clean it up later.
136        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                // TODO: clean up DB?
153                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        // TODO: support BOLT12
190        if protocol != RecurringPaymentProtocol::LNURL {
191            return Err(RecurringPaymentError::UnsupportedProtocol(protocol));
192        }
193
194        // Ensure the federation is supported
195        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        // Invoices are valid for one day by default, might become dynamic with BOLT12
293        // support
294        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                        // Check if any invoice indices were already used in aborted calls to this
319                        // fn. If so:
320                        //   1. Save each previously generated invoice. We don't want to reuse it
321                        //      since it may be expired and in the future may contain call-specific
322                        //      data, but also want to allow the client to sync past it.
323                        //   2. Increment the invoice index until we find an unused one, since
324                        //      re-using an index would re-use an operation id, which is forbidden.
325                        //
326                        // A single request can only create one orphaned operation, but multiple
327                        // cancelled/restarted requests in a row can leave multiple consecutive
328                        // orphaned operations before recurringd commits its own DB state.
329                        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                        // This is where the main part starts: generate the invoice and save it to
355                        // the DB
356                        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                        // TODO: ideally creating the invoice would take a dbtx as argument so we
365                        // don't have to do the "check if invoice already exists" dance
366                        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    /// Returns if an invoice has been paid yet. To avoid DB indirection and
563    /// since the URLs would be similarly long either way we identify
564    /// invoices by federation id and operation id instead of the payment
565    /// code. This function is the basis of `recurringd`'s [LUD-21]
566    /// implementation that allows clients to verify if a given invoice they
567    /// generated using the LNURL has been paid yet.
568    ///
569    /// [LUD-21]: https://github.com/lnurl/luds/blob/luds/21.md
570    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        // Unfortunately LUD-21 wants us to return the invoice again, so we have to
578        // fetch it from the operation meta.
579        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            // Unfortunately the fedimint client doesn't track payment status internally
613            // yet, but relies on integrators to consume the update streams belonging to
614            // operations to figure out their state. Since the verify endpoint is meant to
615            // be non-blocking, we need to find a way to consume the stream until we think
616            // no immediate progress will be made anymore. That's why we limit each update
617            // step to 100ms, far more than a DB read should ever take, and abort if we'd
618            // block to wait for further progress to be made.
619            let update = timeout(Duration::from_millis(100), stream.next()).await;
620            match update {
621                // For some reason recurringd jumps right to claimed without going over funded … but
622                // either is fine to conclude the user will receive their money once they come
623                // online.
624                Ok(Some(LnReceiveState::Funded | LnReceiveState::Claimed)) => {
625                    break PaymentStatus::Paid;
626                }
627                // Keep looking for a state update indicating the invoice having been paid
628                Ok(Some(_)) => {
629                    continue;
630                }
631                // If we reach the end of the update stream without observing a state indicating the
632                // invoice having been paid there was likely some error or the invoice timed out.
633                // Either way we just show the invoice as unpaid.
634                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
691/// Helper struct indicating if an invoice was paid. In the future it may also
692/// contain the preimage to be fully LUD-21 compliant.
693pub 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/// The lnurl-rs crate doesn't have the `verify` field in this type and we don't
710/// use any of the other fields right now. Once we upstream the verify field
711/// this struct can be removed.
712#[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    // The canonical metadata format is a JSON array of gateway ID strings. Older
899    // configs may have stored that JSON array as a string.
900    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}