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