Skip to main content

fedimint_recurringd_tests/
fedimint-recurringd-tests.rs

1use std::ops::ControlFlow;
2
3use devimint::tests::log_binary_versions;
4use devimint::util::{FedimintCli, almost_equal, poll};
5use devimint::version_constants::VERSION_0_12_0_ALPHA;
6use devimint::{DevFed, cmd};
7use tracing::info;
8
9#[tokio::main]
10async fn main() -> anyhow::Result<()> {
11    devimint::run_devfed_test()
12        .call(|dev_fed, process_mgr| async move {
13            log_binary_versions().await?;
14
15            let DevFed {
16                fed,
17                gw_lnd,
18                gw_ldk_second,
19                recurringd,
20                ..
21            } = dev_fed.to_dev_fed(&process_mgr).await?;
22
23            // Test admin auth is checked
24            {
25                let dummy_invite = "fed114znk7uk7ppugdjuytr8venqf2tkywd65cqvg3u93um64tu5cw4yr0n3fvn7qmwvm4g48cpndgnm4gqq4waen5te0xyerwt3s9cczuvf6xyurzde597s7crdvsk2vmyarjw9gwyqjdzj";
26                let url = format!("{}lnv1/federations", recurringd.api_url);
27                let client = reqwest::Client::new();
28                let response_no_auth = client
29                    .put(&url)
30                    .header("Content-Type", "application/json")
31                    .json(&serde_json::json!({ "invite": dummy_invite }))
32                    .send()
33                    .await?;
34                assert!(response_no_auth.status().is_client_error());
35
36                let response_with_wrong_auth = client
37                    .put(&url)
38                    .header("Authorization", "Bearer wrong-token")
39                    .header("Content-Type", "application/json")
40                    .json(&serde_json::json!({ "invite": dummy_invite }))
41                    .send()
42                    .await?;
43                assert!(response_with_wrong_auth.status().is_client_error());
44            }
45
46            // Give the LND Gateway a balance, it's the only GW serving LNv1 and recurringd
47            // is currently LNv1-only
48            fed.pegin_gateways(10_000_000, vec![&gw_lnd]).await?;
49
50            let client = fed.new_joined_client("recurringd-test-client").await?;
51
52            let lnurl = cmd!(
53                client,
54                "module",
55                "ln",
56                "lnurl",
57                "register",
58                recurringd.api_url()
59            )
60            .out_json()
61            .await?["lnurl"]
62                .as_str()
63                .unwrap()
64                .to_owned();
65
66            let lnurl_list = cmd!(client, "module", "ln", "lnurl", "list")
67                .out_json()
68                .await?["codes"]
69                .as_object()
70                .unwrap()
71                .clone();
72
73            assert_eq!(lnurl_list.len(), 1);
74
75            let listed_lnurl = lnurl_list["0"].clone();
76            assert_eq!(listed_lnurl["lnurl"].as_str().unwrap(), &lnurl);
77            assert_eq!(listed_lnurl["last_derivation_index"].as_i64().unwrap(), 0);
78
79            let url = fedimint_lnurl::parse_lnurl(&lnurl).expect("valid lnurl");
80            let pay_response = fedimint_lnurl::request(&url).await.expect("pay request");
81            let invoice_response = fedimint_lnurl::get_invoice(&pay_response, 1_000_000)
82                .await
83                .expect("invoice request");
84            gw_ldk_second.client().pay_invoice(invoice_response.pr.clone()).await?;
85
86            let invoice_op_id = poll("lnurl_receive", || async {
87                cmd!(client, "dev", "wait", "2")
88                    .run()
89                    .await
90                    .map_err(ControlFlow::Break)?;
91
92                let invoice_list = cmd!(client, "module", "ln", "lnurl", "invoices", "0")
93                    .out_json()
94                    .await
95                    .map_err(ControlFlow::Break)?["invoices"]
96                    .as_object()
97                    .unwrap()
98                    .clone();
99
100                if invoice_list.is_empty() {
101                    return Err(ControlFlow::Continue(anyhow::anyhow!(
102                        "No invoice recognized yet"
103                    )));
104                }
105
106                Ok(invoice_list["1"]["operation_id"]
107                    .as_str()
108                    .unwrap()
109                    .to_owned())
110            })
111            .await?;
112
113            let await_invoice_result = cmd!(
114                client,
115                "module",
116                "ln",
117                "lnurl",
118                "await-invoice-paid",
119                invoice_op_id
120            )
121            .out_json()
122            .await?;
123
124            assert_eq!(
125                await_invoice_result["amount_msat"].as_i64().unwrap(),
126                1_000_000
127            );
128            assert_eq!(
129                await_invoice_result["invoice"].as_str().unwrap(),
130                &invoice_response.pr.to_string()
131            );
132
133            let client_balance = client.balance().await?;
134            almost_equal(client_balance, 1_000_000, 5_000).unwrap();
135            info!("Client balance: {client_balance}");
136
137            // Exercise LNv1 `module ln pay <lnurl> --all`: a single payment that
138            // drains (nearly) the client's whole balance to a second client's
139            // LNURL. gw_lnd both issues the invoice and settles the payment, so
140            // this is a direct ecash swap within the federation.
141            //
142            // Version-gated: `--all` was added to `ln pay` in 0.12, older CLIs
143            // reject the flag.
144            if FedimintCli::version_or_default().await >= *VERSION_0_12_0_ALPHA {
145                let drain_receiver = fed
146                    .new_joined_client("recurringd-test-drain-receiver")
147                    .await?;
148                let drain_lnurl = cmd!(
149                    drain_receiver,
150                    "module",
151                    "ln",
152                    "lnurl",
153                    "register",
154                    recurringd.api_url()
155                )
156                .out_json()
157                .await?["lnurl"]
158                    .as_str()
159                    .unwrap()
160                    .to_owned();
161
162                let balance_before_drain = client.balance().await?;
163                let drain_outcome = cmd!(
164                    client,
165                    "module",
166                    "ln",
167                    "pay",
168                    &drain_lnurl,
169                    "--all",
170                    "--gateway-id",
171                    &gw_lnd.gateway_id,
172                )
173                .out_json()
174                .await?;
175                // `LightningPaymentOutcome` is externally tagged; success
176                // serializes as `{"Success": {..}}`.
177                assert!(
178                    drain_outcome.get("Success").is_some(),
179                    "draining the balance with `--all` should succeed, got: {drain_outcome}"
180                );
181
182                let balance_after_drain = client.balance().await?;
183                info!(
184                    "Client balance after `--all` drain: {balance_after_drain} (was {balance_before_drain})"
185                );
186                // A spend-all leaves only sub-denomination dust behind — in
187                // practice a few hundred msats out of ~1_000_000.
188                assert!(
189                    balance_after_drain < 20_000,
190                    "`pay --all` should have drained (nearly) the whole balance, but {balance_after_drain} of {balance_before_drain} msats remain"
191                );
192            } else {
193                info!("Skipping `pay --all` drain test, fedimint-cli is too old");
194            }
195
196            Ok(())
197        })
198        .await
199}