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