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