Skip to main content

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