fedimint_ln_client/recurring/
api.rs

1use fedimint_core::config::FederationId;
2use fedimint_core::util::{FmtCompactErrorAnyhow, SafeUrl};
3use lightning_invoice::Bolt11Invoice;
4use serde::{Deserialize, Serialize};
5use thiserror::Error;
6
7use crate::recurring::{PaymentCodeRootKey, RecurringPaymentProtocol};
8
9pub struct RecurringdClient {
10    client: reqwest::Client,
11    base_url: SafeUrl,
12}
13
14impl RecurringdClient {
15    pub fn new(base_url: &SafeUrl) -> Self {
16        Self {
17            client: reqwest::Client::new(),
18            base_url: SafeUrl::parse(&format!("{base_url}lnv1/"))
19                .expect("failed to parse extended base url"),
20        }
21    }
22
23    pub async fn register_recurring_payment_code(
24        &self,
25        federation_id: FederationId,
26        protocol: RecurringPaymentProtocol,
27        payment_code_root_key: PaymentCodeRootKey,
28        meta: &str,
29    ) -> Result<RecurringPaymentRegistrationResponse, RecurringdApiError> {
30        // TODO: validate decoding works like this and maybe figure out a cleaner way to
31        // communicate errors
32        let request = RecurringPaymentRegistrationRequest {
33            federation_id,
34            protocol,
35            payment_code_root_key,
36            meta: meta.to_owned(),
37        };
38
39        let response = self
40            .client
41            .put(format!("{}paycodes", self.base_url))
42            .json(&request)
43            .send()
44            .await
45            .map_err(RecurringdApiError::NetworkError)?;
46
47        response
48            .json::<ApiResult<RecurringPaymentRegistrationResponse>>()
49            .await
50            .map_err(|e| RecurringdApiError::DecodingError(e.into()))?
51            .into_result()
52    }
53
54    pub async fn await_new_invoice(
55        &self,
56        payment_code_root_key: PaymentCodeRootKey,
57        invoice_index: u64,
58    ) -> Result<Bolt11Invoice, RecurringdApiError> {
59        let response = self
60            .client
61            .get(format!(
62                "{}paycodes/recipient/{}/generated/{}",
63                self.base_url, payment_code_root_key, invoice_index
64            ))
65            .send()
66            .await
67            .map_err(RecurringdApiError::NetworkError)?;
68        response
69            .json::<ApiResult<Bolt11Invoice>>()
70            .await
71            .map_err(|e| RecurringdApiError::DecodingError(e.into()))?
72            .into_result()
73    }
74}
75
76#[derive(Debug, Error)]
77pub enum RecurringdApiError {
78    #[error("Recurring payment server error: {0}")]
79    ApiError(String),
80    #[error("Invalid response: {}", FmtCompactErrorAnyhow(.0))]
81    DecodingError(anyhow::Error),
82    #[error("Network error: {0}")]
83    NetworkError(#[from] reqwest::Error),
84}
85
86#[derive(Debug, Clone, PartialOrd, PartialEq, Hash, Serialize, Deserialize)]
87pub struct RecurringPaymentRegistrationRequest {
88    /// Federation ID in which the invoices should be generated
89    pub federation_id: FederationId,
90    /// Recurring payment protocol to use
91    pub protocol: RecurringPaymentProtocol,
92    /// Public key from which other keys will be derived for each generated
93    /// invoice
94    pub payment_code_root_key: PaymentCodeRootKey,
95    /// LNURL meta data, see LUD-06 for more details on the format
96    pub meta: String,
97}
98
99#[derive(Debug, Clone, PartialOrd, PartialEq, Hash, Serialize, Deserialize)]
100pub struct RecurringPaymentRegistrationResponse {
101    /// Either a BOLT12 offer or LNURL
102    pub recurring_payment_code: String,
103}
104
105#[derive(Debug, Deserialize)]
106#[serde(untagged)]
107enum ApiResult<T> {
108    Ok(T),
109    Err { error: String },
110}
111
112impl<T> ApiResult<T> {
113    pub fn into_result(self) -> Result<T, RecurringdApiError> {
114        match self {
115            ApiResult::Ok(result) => Ok(result),
116            ApiResult::Err { error } => Err(RecurringdApiError::ApiError(error)),
117        }
118    }
119}