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}