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