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