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};
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)?,
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(root_secret).await
164    } else if let Some(invite_code) = &invite_code {
165        let client_config = fedimint_api_client::api::net::Connector::default()
166            .download_from_invite_code(invite_code)
167            .await?;
168        client_builder
169            .join(root_secret, client_config.clone(), invite_code.api_secret())
170            .await
171    } else {
172        bail!("Database not initialize and invite code not provided");
173    }?;
174    Ok((Arc::new(client), invite_code))
175}
176
177pub async fn lnd_create_invoice(amount: Amount) -> anyhow::Result<(Bolt11Invoice, String)> {
178    let result = cmd!(LnCli, "addinvoice", "--amt_msat", amount.msats)
179        .out_json()
180        .await?;
181    let invoice = result["payment_request"]
182        .as_str()
183        .map(Bolt11Invoice::from_str)
184        .transpose()?
185        .context("Missing payment_request field")?;
186    let r_hash = result["r_hash"]
187        .as_str()
188        .context("Missing r_hash field")?
189        .to_owned();
190    Ok((invoice, r_hash))
191}
192
193pub async fn lnd_pay_invoice(invoice: Bolt11Invoice) -> anyhow::Result<()> {
194    let status = cmd!(
195        LnCli,
196        "payinvoice",
197        "--force",
198        "--allow_self_payment",
199        "--json",
200        invoice.to_string()
201    )
202    .out_json()
203    .await?["status"]
204        .as_str()
205        .context("Missing status field")?
206        .to_owned();
207    anyhow::ensure!(status == "SUCCEEDED");
208    Ok(())
209}
210
211pub async fn lnd_wait_invoice_payment(r_hash: String) -> anyhow::Result<()> {
212    for _ in 0..60 {
213        let result = cmd!(LnCli, "lookupinvoice", &r_hash).out_json().await?;
214        let state = result["state"].as_str().context("Missing state field")?;
215        if state == "SETTLED" {
216            return Ok(());
217        }
218
219        fedimint_core::task::sleep(Duration::from_millis(500)).await;
220    }
221    anyhow::bail!("Timeout waiting for invoice to settle: {r_hash}")
222}
223
224pub async fn gateway_pay_invoice(
225    prefix: &str,
226    gateway_name: &str,
227    client: &ClientHandleArc,
228    invoice: Bolt11Invoice,
229    event_sender: &mpsc::UnboundedSender<MetricEvent>,
230    ln_gateway: Option<LightningGateway>,
231) -> anyhow::Result<()> {
232    let m = fedimint_core::time::now();
233    let lightning_module = &client.get_first_module::<LightningClientModule>()?;
234    let OutgoingLightningPayment {
235        payment_type,
236        contract_id: _,
237        fee: _,
238    } = lightning_module
239        .pay_bolt11_invoice(ln_gateway, invoice, ())
240        .await?;
241    let operation_id = match payment_type {
242        fedimint_ln_client::PayType::Internal(_) => bail!("Internal payment not expected"),
243        fedimint_ln_client::PayType::Lightning(operation_id) => operation_id,
244    };
245    let mut updates = lightning_module
246        .subscribe_ln_pay(operation_id)
247        .await?
248        .into_stream();
249    while let Some(update) = updates.next().await {
250        info!("{prefix} LnPayState update: {update:?}");
251        match update {
252            LnPayState::Success { preimage: _ } => {
253                let elapsed: Duration = m.elapsed()?;
254                info!("{prefix} Invoice paid in {elapsed:?}");
255                event_sender.send(MetricEvent {
256                    name: "gateway_pay_invoice_success".into(),
257                    duration: elapsed,
258                })?;
259                event_sender.send(MetricEvent {
260                    name: format!("gateway_{gateway_name}_pay_invoice_success"),
261                    duration: elapsed,
262                })?;
263                break;
264            }
265            LnPayState::Created
266            | LnPayState::Funded { block_height: _ }
267            | LnPayState::AwaitingChange => {}
268            LnPayState::Canceled => {
269                let elapsed: Duration = m.elapsed()?;
270                warn!("{prefix} Invoice canceled in {elapsed:?}");
271                event_sender.send(MetricEvent {
272                    name: "gateway_pay_invoice_canceled".into(),
273                    duration: elapsed,
274                })?;
275                break;
276            }
277            LnPayState::Refunded { gateway_error } => {
278                let elapsed: Duration = m.elapsed()?;
279                warn!("{prefix} Invoice refunded due to {gateway_error} in {elapsed:?}");
280                event_sender.send(MetricEvent {
281                    name: "gateway_pay_invoice_refunded".into(),
282                    duration: elapsed,
283                })?;
284                break;
285            }
286            LnPayState::WaitingForRefund { error_reason } => {
287                warn!("{prefix} Waiting for refund: {error_reason:?}");
288            }
289            LnPayState::UnexpectedError { error_message } => {
290                bail!("Failed to pay invoice: {error_message:?}")
291            }
292        }
293    }
294    Ok(())
295}
296
297pub async fn ldk_create_invoice(amount: Amount) -> anyhow::Result<Bolt11Invoice> {
298    let invoice_string = cmd!(GatewayLdkCli, "lightning", "create-invoice", amount.msats)
299        .out_string()
300        .await?;
301    Ok(Bolt11Invoice::from_str(&invoice_string)?)
302}
303
304pub async fn ldk_pay_invoice(invoice: Bolt11Invoice) -> anyhow::Result<()> {
305    cmd!(
306        GatewayLdkCli,
307        "lightning",
308        "pay-invoice",
309        invoice.to_string()
310    )
311    .run()
312    .await?;
313    Ok(())
314}
315
316pub async fn ldk_wait_invoice_payment(invoice: &Bolt11Invoice) -> anyhow::Result<()> {
317    let gatewayd_version = devimint::util::Gatewayd::version_or_default().await;
318    if gatewayd_version < *VERSION_0_7_0_ALPHA {
319        return Ok(());
320    }
321
322    let status = cmd!(
323        GatewayLdkCli,
324        "lightning",
325        "get-invoice",
326        "--payment-hash",
327        invoice.payment_hash()
328    )
329    .out_json()
330    .await?["status"]
331        .as_str()
332        .context("Missing status field")?
333        .to_owned();
334    if status == "Succeeded" {
335        Ok(())
336    } else {
337        bail!("Got status {status} for invoice {invoice}")
338    }
339}
340
341pub fn parse_gateway_id(s: &str) -> Result<secp256k1::PublicKey, secp256k1::Error> {
342    secp256k1::PublicKey::from_str(s)
343}
344
345pub async fn get_note_summary(client: &ClientHandleArc) -> anyhow::Result<TieredCounts> {
346    let mint_client = client.get_first_module::<MintClientModule>()?;
347    let summary = mint_client
348        .get_note_counts_by_denomination(
349            &mut client
350                .db()
351                .begin_transaction_nc()
352                .await
353                .to_ref_with_prefix_module_id(1)
354                .0,
355        )
356        .await;
357    Ok(summary)
358}
359
360pub async fn remint_denomination(
361    client: &ClientHandleArc,
362    denomination: Amount,
363    quantity: u16,
364) -> anyhow::Result<()> {
365    let mint_client = client.get_first_module::<MintClientModule>()?;
366    let mut dbtx = client.db().begin_transaction().await;
367    let mut module_transaction = dbtx.to_ref_with_prefix_module_id(mint_client.id).0;
368    let mut tx = TransactionBuilder::new();
369    let operation_id = OperationId::new_random();
370    for _ in 0..quantity {
371        let outputs = mint_client
372            .create_output(
373                &mut module_transaction.to_ref_nc(),
374                operation_id,
375                1,
376                denomination,
377            )
378            .await
379            .into_dyn(mint_client.id);
380
381        tx = tx.with_outputs(outputs);
382    }
383    drop(module_transaction);
384    let operation_meta_gen = |_| ();
385    let txid = client
386        .finalize_and_submit_transaction(
387            operation_id,
388            MintCommonInit::KIND.as_str(),
389            operation_meta_gen,
390            tx,
391        )
392        .await?
393        .txid();
394    let tx_subscription = client.transaction_updates(operation_id).await;
395    tx_subscription
396        .await_tx_accepted(txid)
397        .await
398        .map_err(|e| anyhow!("{e}"))?;
399    dbtx.commit_tx().await;
400    for i in 0..quantity {
401        let out_point = OutPoint {
402            txid,
403            out_idx: u64::from(i),
404        };
405        mint_client
406            .await_output_finalized(operation_id, out_point)
407            .await?;
408    }
409    Ok(())
410}