devimint/
recurringd.rs

1use std::collections::HashMap;
2use std::ops::ControlFlow;
3use std::path::PathBuf;
4
5use anyhow::Result;
6use fedimint_core::task::sleep;
7use fedimint_core::util::SafeUrl;
8use reqwest::get;
9use tracing::info;
10use {reqwest, serde_json};
11
12use crate::cmd;
13use crate::envs::{
14    FM_RECURRING_API_ADDRESS_ENV, FM_RECURRING_API_BEARER_TOKEN_ENV, FM_RECURRING_BIND_ADDRESS_ENV,
15    FM_RECURRING_DATA_DIR_ENV,
16};
17use crate::util::{ProcessHandle, ProcessManager, poll};
18
19#[derive(Clone)]
20pub struct Recurringd {
21    pub(crate) process: ProcessHandle,
22    pub addr: String,
23    pub api_url: SafeUrl,
24    pub log_path: PathBuf,
25}
26
27impl Recurringd {
28    pub async fn new(process_mgr: &ProcessManager) -> Result<Self> {
29        let test_dir = &process_mgr.globals.FM_TEST_DIR;
30        let port = process_mgr.globals.FM_PORT_RECURRINGD;
31        let bind_address = format!("127.0.0.1:{port}");
32        let api_url = SafeUrl::parse(&format!("http://{bind_address}/")).expect("Valid URL");
33
34        // Default bearer token for development
35        let bearer_token = "devimint-recurring-token";
36
37        let recurring_env: HashMap<String, String> = HashMap::from_iter([
38            (
39                FM_RECURRING_DATA_DIR_ENV.to_owned(),
40                format!("{}/recurringd", test_dir.display()),
41            ),
42            (
43                FM_RECURRING_BIND_ADDRESS_ENV.to_owned(),
44                bind_address.clone(),
45            ),
46            (FM_RECURRING_API_ADDRESS_ENV.to_owned(), api_url.to_string()),
47            (
48                FM_RECURRING_API_BEARER_TOKEN_ENV.to_owned(),
49                bearer_token.to_string(),
50            ),
51        ]);
52
53        let process = process_mgr
54            .spawn_daemon(
55                "recurringd",
56                cmd!("fedimint-recurringd").envs(recurring_env),
57            )
58            .await?;
59
60        let log_path = process_mgr.globals.FM_LOGS_DIR.join("recurringd.log");
61
62        let recurringd = Self {
63            process,
64            addr: bind_address,
65            api_url,
66            log_path,
67        };
68
69        // Poll to ensure the service is ready by checking the /federations endpoint
70        poll("waiting for recurringd to be ready", || async {
71            match get(format!("http://{}/lnv1/federations", recurringd.addr)).await {
72                Ok(response) if response.status().is_success() => Ok(()),
73                _ => {
74                    sleep(tokio::time::Duration::from_millis(100)).await;
75                    Err(ControlFlow::Continue(anyhow::anyhow!(
76                        "recurringd not ready yet"
77                    )))
78                }
79            }
80        })
81        .await?;
82
83        info!("Recurringd started at {}", recurringd.addr);
84        Ok(recurringd)
85    }
86
87    pub async fn terminate(self) -> Result<()> {
88        self.process.terminate().await
89    }
90
91    // Add a federation to recurringd
92    pub async fn add_federation(&self, invite_code: &str) -> Result<String> {
93        let url = format!("http://{}/lnv1/federations", self.addr);
94        let client = reqwest::Client::new();
95        let response = client
96            .put(&url)
97            .header("Authorization", "Bearer devimint-recurring-token")
98            .header("Content-Type", "application/json")
99            .json(&serde_json::json!({ "invite": invite_code }))
100            .send()
101            .await?;
102
103        Ok(response.text().await?)
104    }
105
106    // List federations registered with recurringd
107    pub async fn list_federations(&self) -> Result<String> {
108        let url = format!("http://{}/lnv1/federations", self.addr);
109        let client = reqwest::Client::new();
110        let response = client
111            .get(&url)
112            .header("Authorization", "Bearer devimint-recurring-token")
113            .send()
114            .await?;
115
116        Ok(response.text().await?)
117    }
118
119    pub fn api_url(&self) -> SafeUrl {
120        self.api_url.clone()
121    }
122}