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_connectors::ConnectorRegistry;
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 let client_secret = Client::load_or_generate_client_secret(&db).await?;
163 let root_secret =
164 RootSecret::StandardDoubleDerive(PlainRootSecretStrategy::to_root_secret(&client_secret));
165 let connectors = ConnectorRegistry::build_from_client_env()?.bind().await?;
166
167 let client = if Client::is_initialized(&db).await {
168 client_builder.open(connectors, db, root_secret).await
169 } else if let Some(invite_code) = &invite_code {
170 client_builder
171 .preview(connectors, 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 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 retry(
365 format!("remint_{denomination}_{quantity}"),
366 aggressive_backoff(),
367 || {
368 let client = client.clone();
369 async move { try_remint_denomination(&client, denomination, quantity).await }
370 },
371 )
372 .await
373}
374
375pub async fn try_remint_denomination(
376 client: &ClientHandleArc,
377 denomination: Amount,
378 quantity: u16,
379) -> anyhow::Result<()> {
380 let mint_client = client.get_first_module::<MintClientModule>()?;
381 let mut dbtx = client.db().begin_transaction().await;
382 let mut module_transaction = dbtx.to_ref_with_prefix_module_id(mint_client.id).0;
383 let mut tx = TransactionBuilder::new();
384 let operation_id = OperationId::new_random();
385 for _ in 0..quantity {
386 let outputs = mint_client
387 .create_output(
388 &mut module_transaction.to_ref_nc(),
389 operation_id,
390 1,
391 denomination,
392 )
393 .await
394 .into_dyn(mint_client.id);
395
396 tx = tx.with_outputs(outputs);
397 }
398 drop(module_transaction);
399 let operation_meta_gen = |_| ();
400 let txid = client
401 .finalize_and_submit_transaction(
402 operation_id,
403 MintCommonInit::KIND.as_str(),
404 operation_meta_gen,
405 tx,
406 )
407 .await?
408 .txid();
409 let tx_subscription = client.transaction_updates(operation_id).await;
410 tx_subscription
411 .await_tx_accepted(txid)
412 .await
413 .map_err(|e| anyhow!("{e}"))?;
414 dbtx.commit_tx().await;
415 for i in 0..quantity {
416 let out_point = OutPoint {
417 txid,
418 out_idx: u64::from(i),
419 };
420 mint_client
421 .await_output_finalized(operation_id, out_point)
422 .await?;
423 }
424 Ok(())
425}