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::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    client_builder.with_primary_module_kind(fedimint_mint_client::KIND);
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
167    let client = if Client::is_initialized(&db).await {
168        client_builder.open(db, root_secret).await
169    } else if let Some(invite_code) = &invite_code {
170        client_builder
171            .preview(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 gatewayd_version = devimint::util::Gatewayd::version_or_default().await;
322    if gatewayd_version < *VERSION_0_7_0_ALPHA {
323        return Ok(());
324    }
325
326    let status = cmd!(
327        GatewayLdkCli,
328        "lightning",
329        "get-invoice",
330        "--payment-hash",
331        invoice.payment_hash()
332    )
333    .out_json()
334    .await?["status"]
335        .as_str()
336        .context("Missing status field")?
337        .to_owned();
338    if status == "Succeeded" {
339        Ok(())
340    } else {
341        bail!("Got status {status} for invoice {invoice}")
342    }
343}
344
345pub fn parse_gateway_id(s: &str) -> Result<secp256k1::PublicKey, secp256k1::Error> {
346    secp256k1::PublicKey::from_str(s)
347}
348
349pub async fn get_note_summary(client: &ClientHandleArc) -> anyhow::Result<TieredCounts> {
350    let mint_client = client.get_first_module::<MintClientModule>()?;
351    let summary = mint_client
352        .get_note_counts_by_denomination(
353            &mut client
354                .db()
355                .begin_transaction_nc()
356                .await
357                .to_ref_with_prefix_module_id(1)
358                .0,
359        )
360        .await;
361    Ok(summary)
362}
363
364pub async fn remint_denomination(
365    client: &ClientHandleArc,
366    denomination: Amount,
367    quantity: u16,
368) -> anyhow::Result<()> {
369    retry(
370        format!("remint_{denomination}_{quantity}"),
371        aggressive_backoff(),
372        || {
373            let client = client.clone();
374            async move { try_remint_denomination(&client, denomination, quantity).await }
375        },
376    )
377    .await
378}
379
380pub async fn try_remint_denomination(
381    client: &ClientHandleArc,
382    denomination: Amount,
383    quantity: u16,
384) -> anyhow::Result<()> {
385    let mint_client = client.get_first_module::<MintClientModule>()?;
386    let mut dbtx = client.db().begin_transaction().await;
387    let mut module_transaction = dbtx.to_ref_with_prefix_module_id(mint_client.id).0;
388    let mut tx = TransactionBuilder::new();
389    let operation_id = OperationId::new_random();
390    for _ in 0..quantity {
391        let outputs = mint_client
392            .create_output(
393                &mut module_transaction.to_ref_nc(),
394                operation_id,
395                1,
396                denomination,
397            )
398            .await
399            .into_dyn(mint_client.id);
400
401        tx = tx.with_outputs(outputs);
402    }
403    drop(module_transaction);
404    let operation_meta_gen = |_| ();
405    let txid = client
406        .finalize_and_submit_transaction(
407            operation_id,
408            MintCommonInit::KIND.as_str(),
409            operation_meta_gen,
410            tx,
411        )
412        .await?
413        .txid();
414    let tx_subscription = client.transaction_updates(operation_id).await;
415    tx_subscription
416        .await_tx_accepted(txid)
417        .await
418        .map_err(|e| anyhow!("{e}"))?;
419    dbtx.commit_tx().await;
420    for i in 0..quantity {
421        let out_point = OutPoint {
422            txid,
423            out_idx: u64::from(i),
424        };
425        mint_client
426            .await_output_finalized(operation_id, out_point)
427            .await?;
428    }
429    Ok(())
430}