fedimint_load_test_tool/
common.rs

1use std::path::PathBuf;
2use std::str::FromStr;
3use std::sync::Arc;
4use std::time::Duration;
5
6use anyhow::{Context, Result, anyhow, bail};
7use devimint::cmd;
8use devimint::util::{FedimintCli, GatewayLdkCli, LnCli};
9use fedimint_client::secret::{PlainRootSecretStrategy, RootSecretStrategy};
10use fedimint_client::transaction::TransactionBuilder;
11use fedimint_client::{Client, ClientHandleArc, RootSecret};
12use fedimint_core::core::{IntoDynInstance, OperationId};
13use fedimint_core::db::Database;
14use fedimint_core::invite_code::InviteCode;
15use fedimint_core::module::CommonModuleInit;
16use fedimint_core::module::registry::ModuleRegistry;
17use fedimint_core::util::backoff_util::aggressive_backoff;
18use fedimint_core::util::retry;
19use fedimint_core::{Amount, OutPoint, PeerId, TieredCounts, secp256k1};
20use fedimint_ln_client::{
21    LightningClientInit, LightningClientModule, LnPayState, OutgoingLightningPayment,
22};
23use fedimint_ln_common::LightningGateway;
24use fedimint_mint_client::{
25    MintClientInit, MintClientModule, MintCommonInit, OOBNotes, SelectNotesWithAtleastAmount,
26};
27use fedimint_wallet_client::WalletClientInit;
28use futures::StreamExt;
29use lightning_invoice::Bolt11Invoice;
30use tokio::sync::mpsc;
31use tracing::{info, warn};
32
33use crate::MetricEvent;
34
35pub async fn get_invite_code_cli(peer: PeerId) -> anyhow::Result<InviteCode> {
36    cmd!(FedimintCli, "invite-code", peer).out_json().await?["invite_code"]
37        .as_str()
38        .map(InviteCode::from_str)
39        .transpose()?
40        .context("missing invite code")
41}
42
43pub async fn get_notes_cli(amount: &Amount) -> anyhow::Result<OOBNotes> {
44    cmd!(FedimintCli, "spend", amount.msats.to_string())
45        .out_json()
46        .await?["notes"]
47        .as_str()
48        .map(OOBNotes::from_str)
49        .transpose()?
50        .context("missing notes output")
51}
52
53pub async fn try_get_notes_cli(amount: &Amount, tries: usize) -> anyhow::Result<OOBNotes> {
54    for _ in 0..tries {
55        match get_notes_cli(amount).await {
56            Ok(oob_notes) => return Ok(oob_notes),
57            Err(e) => {
58                info!("Failed to get notes from cli: {e}, trying again after a second...");
59                fedimint_core::task::sleep(Duration::from_secs(1)).await;
60            }
61        }
62    }
63    get_notes_cli(amount).await
64}
65
66pub async fn reissue_notes(
67    client: &ClientHandleArc,
68    oob_notes: OOBNotes,
69    event_sender: &mpsc::UnboundedSender<MetricEvent>,
70) -> anyhow::Result<()> {
71    let m = fedimint_core::time::now();
72    let mint = &client.get_first_module::<MintClientModule>()?;
73    let operation_id = mint.reissue_external_notes(oob_notes, ()).await?;
74    let mut updates = mint
75        .subscribe_reissue_external_notes(operation_id)
76        .await?
77        .into_stream();
78    while let Some(update) = updates.next().await {
79        if let fedimint_mint_client::ReissueExternalNotesState::Failed(e) = update {
80            bail!("Reissue failed: {e}")
81        }
82    }
83    event_sender.send(MetricEvent {
84        name: "reissue_notes".into(),
85        duration: m.elapsed()?,
86    })?;
87    Ok(())
88}
89
90pub async fn do_spend_notes(
91    mint: &ClientHandleArc,
92    amount: Amount,
93) -> anyhow::Result<(OperationId, OOBNotes)> {
94    let mint = &mint.get_first_module::<MintClientModule>()?;
95    let (operation_id, oob_notes) = mint
96        .spend_notes_with_selector(
97            &SelectNotesWithAtleastAmount,
98            amount,
99            Duration::from_secs(600),
100            false,
101            (),
102        )
103        .await?;
104    let mut updates = mint
105        .subscribe_spend_notes(operation_id)
106        .await?
107        .into_stream();
108    if let Some(update) = updates.next().await {
109        match update {
110            fedimint_mint_client::SpendOOBState::Created
111            | fedimint_mint_client::SpendOOBState::Success => {}
112            other => {
113                bail!("Spend failed: {other:?}");
114            }
115        }
116    }
117    Ok((operation_id, oob_notes))
118}
119
120pub async fn await_spend_notes_finish(
121    client: &ClientHandleArc,
122    operation_id: OperationId,
123) -> anyhow::Result<()> {
124    let mut updates = client
125        .get_first_module::<MintClientModule>()?
126        .subscribe_spend_notes(operation_id)
127        .await?
128        .into_stream();
129    while let Some(update) = updates.next().await {
130        info!("SpendOOBState update: {:?}", update);
131        match update {
132            fedimint_mint_client::SpendOOBState::Created
133            | fedimint_mint_client::SpendOOBState::Success => {}
134            other => {
135                bail!("Spend failed: {other:?}");
136            }
137        }
138    }
139    Ok(())
140}
141
142pub async fn build_client(
143    invite_code: Option<InviteCode>,
144    rocksdb: Option<&PathBuf>,
145) -> anyhow::Result<(ClientHandleArc, Option<InviteCode>)> {
146    let db = if let Some(rocksdb) = rocksdb {
147        Database::new(
148            fedimint_rocksdb::RocksDb::open(rocksdb).await?,
149            ModuleRegistry::default(),
150        )
151    } else {
152        fedimint_core::db::mem_impl::MemDatabase::new().into()
153    };
154    let mut client_builder = Client::builder()
155        .await?
156        .with_iroh_enable_dht(false)
157        .with_iroh_enable_dht(false);
158    client_builder.with_module(MintClientInit);
159    client_builder.with_module(LightningClientInit::default());
160    client_builder.with_module(WalletClientInit::default());
161    let client_secret = Client::load_or_generate_client_secret(&db).await?;
162    let root_secret =
163        RootSecret::StandardDoubleDerive(PlainRootSecretStrategy::to_root_secret(&client_secret));
164
165    let client = if Client::is_initialized(&db).await {
166        client_builder.open(db, root_secret).await
167    } else if let Some(invite_code) = &invite_code {
168        client_builder
169            .preview(invite_code)
170            .await?
171            .join(db, root_secret)
172            .await
173    } else {
174        bail!("Database not initialize and invite code not provided");
175    }?;
176    Ok((Arc::new(client), invite_code))
177}
178
179pub async fn lnd_create_invoice(amount: Amount) -> anyhow::Result<(Bolt11Invoice, String)> {
180    let result = cmd!(LnCli, "addinvoice", "--amt_msat", amount.msats)
181        .out_json()
182        .await?;
183    let invoice = result["payment_request"]
184        .as_str()
185        .map(Bolt11Invoice::from_str)
186        .transpose()?
187        .context("Missing payment_request field")?;
188    let r_hash = result["r_hash"]
189        .as_str()
190        .context("Missing r_hash field")?
191        .to_owned();
192    Ok((invoice, r_hash))
193}
194
195pub async fn lnd_pay_invoice(invoice: Bolt11Invoice) -> anyhow::Result<()> {
196    let status = cmd!(
197        LnCli,
198        "payinvoice",
199        "--force",
200        "--allow_self_payment",
201        "--json",
202        invoice.to_string()
203    )
204    .out_json()
205    .await?["status"]
206        .as_str()
207        .context("Missing status field")?
208        .to_owned();
209    anyhow::ensure!(status == "SUCCEEDED");
210    Ok(())
211}
212
213pub async fn lnd_wait_invoice_payment(r_hash: String) -> anyhow::Result<()> {
214    for _ in 0..60 {
215        let result = cmd!(LnCli, "lookupinvoice", &r_hash).out_json().await?;
216        let state = result["state"].as_str().context("Missing state field")?;
217        if state == "SETTLED" {
218            return Ok(());
219        }
220
221        fedimint_core::task::sleep(Duration::from_millis(500)).await;
222    }
223    anyhow::bail!("Timeout waiting for invoice to settle: {r_hash}")
224}
225
226pub async fn gateway_pay_invoice(
227    prefix: &str,
228    gateway_name: &str,
229    client: &ClientHandleArc,
230    invoice: Bolt11Invoice,
231    event_sender: &mpsc::UnboundedSender<MetricEvent>,
232    ln_gateway: Option<LightningGateway>,
233) -> anyhow::Result<()> {
234    let m = fedimint_core::time::now();
235    let lightning_module = &client.get_first_module::<LightningClientModule>()?;
236    let OutgoingLightningPayment {
237        payment_type,
238        contract_id: _,
239        fee: _,
240    } = lightning_module
241        .pay_bolt11_invoice(ln_gateway, invoice, ())
242        .await?;
243    let operation_id = match payment_type {
244        fedimint_ln_client::PayType::Internal(_) => bail!("Internal payment not expected"),
245        fedimint_ln_client::PayType::Lightning(operation_id) => operation_id,
246    };
247    let mut updates = lightning_module
248        .subscribe_ln_pay(operation_id)
249        .await?
250        .into_stream();
251    while let Some(update) = updates.next().await {
252        info!("{prefix} LnPayState update: {update:?}");
253        match update {
254            LnPayState::Success { preimage: _ } => {
255                let elapsed: Duration = m.elapsed()?;
256                info!("{prefix} Invoice paid in {elapsed:?}");
257                event_sender.send(MetricEvent {
258                    name: "gateway_pay_invoice_success".into(),
259                    duration: elapsed,
260                })?;
261                event_sender.send(MetricEvent {
262                    name: format!("gateway_{gateway_name}_pay_invoice_success"),
263                    duration: elapsed,
264                })?;
265                break;
266            }
267            LnPayState::Created
268            | LnPayState::Funded { block_height: _ }
269            | LnPayState::AwaitingChange => {}
270            LnPayState::Canceled => {
271                let elapsed: Duration = m.elapsed()?;
272                warn!("{prefix} Invoice canceled in {elapsed:?}");
273                event_sender.send(MetricEvent {
274                    name: "gateway_pay_invoice_canceled".into(),
275                    duration: elapsed,
276                })?;
277                break;
278            }
279            LnPayState::Refunded { gateway_error } => {
280                let elapsed: Duration = m.elapsed()?;
281                warn!("{prefix} Invoice refunded due to {gateway_error} in {elapsed:?}");
282                event_sender.send(MetricEvent {
283                    name: "gateway_pay_invoice_refunded".into(),
284                    duration: elapsed,
285                })?;
286                break;
287            }
288            LnPayState::WaitingForRefund { error_reason } => {
289                warn!("{prefix} Waiting for refund: {error_reason:?}");
290            }
291            LnPayState::UnexpectedError { error_message } => {
292                bail!("Failed to pay invoice: {error_message:?}")
293            }
294        }
295    }
296    Ok(())
297}
298
299pub async fn ldk_create_invoice(amount: Amount) -> anyhow::Result<Bolt11Invoice> {
300    let invoice_string = cmd!(GatewayLdkCli, "lightning", "create-invoice", amount.msats)
301        .out_string()
302        .await?;
303    Ok(Bolt11Invoice::from_str(&invoice_string)?)
304}
305
306pub async fn ldk_pay_invoice(invoice: Bolt11Invoice) -> anyhow::Result<()> {
307    cmd!(
308        GatewayLdkCli,
309        "lightning",
310        "pay-invoice",
311        invoice.to_string()
312    )
313    .run()
314    .await?;
315    Ok(())
316}
317
318pub async fn ldk_wait_invoice_payment(invoice: &Bolt11Invoice) -> anyhow::Result<()> {
319    let status = cmd!(
320        GatewayLdkCli,
321        "lightning",
322        "get-invoice",
323        "--payment-hash",
324        invoice.payment_hash()
325    )
326    .out_json()
327    .await?["status"]
328        .as_str()
329        .context("Missing status field")?
330        .to_owned();
331    if status == "Succeeded" {
332        Ok(())
333    } else {
334        bail!("Got status {status} for invoice {invoice}")
335    }
336}
337
338pub fn parse_gateway_id(s: &str) -> Result<secp256k1::PublicKey, secp256k1::Error> {
339    secp256k1::PublicKey::from_str(s)
340}
341
342pub async fn get_note_summary(client: &ClientHandleArc) -> anyhow::Result<TieredCounts> {
343    let mint_client = client.get_first_module::<MintClientModule>()?;
344    let summary = mint_client
345        .get_note_counts_by_denomination(
346            &mut client
347                .db()
348                .begin_transaction_nc()
349                .await
350                .to_ref_with_prefix_module_id(1)
351                .0,
352        )
353        .await;
354    Ok(summary)
355}
356
357pub async fn remint_denomination(
358    client: &ClientHandleArc,
359    denomination: Amount,
360    quantity: u16,
361) -> anyhow::Result<()> {
362    retry(
363        format!("remint_{denomination}_{quantity}"),
364        aggressive_backoff(),
365        || {
366            let client = client.clone();
367            async move { try_remint_denomination(&client, denomination, quantity).await }
368        },
369    )
370    .await
371}
372
373pub async fn try_remint_denomination(
374    client: &ClientHandleArc,
375    denomination: Amount,
376    quantity: u16,
377) -> anyhow::Result<()> {
378    let mint_client = client.get_first_module::<MintClientModule>()?;
379    let mut dbtx = client.db().begin_transaction().await;
380    let mut module_transaction = dbtx.to_ref_with_prefix_module_id(mint_client.id).0;
381    let mut tx = TransactionBuilder::new();
382    let operation_id = OperationId::new_random();
383    for _ in 0..quantity {
384        let outputs = mint_client
385            .create_output(
386                &mut module_transaction.to_ref_nc(),
387                operation_id,
388                1,
389                denomination,
390            )
391            .await
392            .into_dyn(mint_client.id);
393
394        tx = tx.with_outputs(outputs);
395    }
396    drop(module_transaction);
397    let operation_meta_gen = |_| ();
398    let txid = client
399        .finalize_and_submit_transaction(
400            operation_id,
401            MintCommonInit::KIND.as_str(),
402            operation_meta_gen,
403            tx,
404        )
405        .await?
406        .txid();
407    let tx_subscription = client.transaction_updates(operation_id).await;
408    tx_subscription
409        .await_tx_accepted(txid)
410        .await
411        .map_err(|e| anyhow!("{e}"))?;
412    dbtx.commit_tx().await;
413    for i in 0..quantity {
414        let out_point = OutPoint {
415            txid,
416            out_idx: u64::from(i),
417        };
418        mint_client
419            .await_output_finalized(operation_id, out_point)
420            .await?;
421    }
422    Ok(())
423}