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