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