1pub mod fees;
2#[cfg(feature = "onchain-bdk")]
3pub mod onchain;
4
5use std::borrow::Borrow;
6use std::time::Duration;
7
8use bitcoin::secp256k1::PublicKey;
9use bitcoin::{Amount, Txid};
10#[cfg(feature = "utoipa")]
11use utoipa::ToSchema;
12
13use ark::VtxoId;
14use ark::lightning::{PaymentHash, Preimage};
15use bitcoin_ext::{AmountExt, BlockDelta};
16
17use bark::actions::lightning::receive::{
18 LightningReceive, LightningReceiveState, Progress as ReceiveProgress,
19};
20
21use crate::cli::fees::FeeSchedule;
22use crate::exit::error::ExitError;
23use crate::exit::package::ExitTransactionPackage;
24use crate::exit::ExitState;
25use crate::primitives::{TransactionInfo, WalletVtxoInfo};
26use crate::serde_utils;
27
28#[derive(Debug, Clone, Deserialize, Serialize)]
29#[cfg_attr(feature = "utoipa", derive(ToSchema))]
30pub struct ArkInfo {
31 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
33 pub network: bitcoin::Network,
34 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
36 pub server_pubkey: PublicKey,
37 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
39 pub mailbox_pubkey: PublicKey,
40 #[serde(with = "serde_utils::duration")]
42 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
43 pub round_interval: Duration,
44 pub nb_round_nonces: usize,
46 pub vtxo_exit_delta: BlockDelta,
48 pub vtxo_expiry_delta: BlockDelta,
50 pub htlc_send_expiry_delta: BlockDelta,
52 pub htlc_expiry_delta: BlockDelta,
54 #[cfg_attr(feature = "utoipa", schema(value_type = u64))]
56 pub max_vtxo_amount: Option<Amount>,
57 pub required_board_confirmations: usize,
59 pub max_user_invoice_cltv_delta: u16,
62 #[serde(rename = "min_board_amount_sat", with = "bitcoin::amount::serde::as_sat")]
64 #[cfg_attr(feature = "utoipa", schema(value_type = u64))]
65 pub min_board_amount: Amount,
66 pub offboard_feerate_sat_per_kvb: u64,
68 pub ln_receive_anti_dos_required: bool,
72 pub fees: FeeSchedule,
74 pub max_vtxo_exit_depth: u16,
79 pub max_offboard_inputs: usize,
81}
82
83#[derive(Debug, Clone, Deserialize, Serialize)]
84#[cfg_attr(feature = "utoipa", derive(ToSchema))]
85pub struct NextRoundStart {
86 pub start_time: chrono::DateTime<chrono::Local>,
88}
89
90impl<T: Borrow<ark::ArkInfo>> From<T> for ArkInfo {
91 #[allow(deprecated)] fn from(v: T) -> Self {
93 let v = v.borrow();
94 ArkInfo {
95 network: v.network,
96 server_pubkey: v.server_pubkey,
97 mailbox_pubkey: v.mailbox_pubkey,
98 round_interval: v.round_interval,
99 nb_round_nonces: v.nb_round_nonces,
100 vtxo_exit_delta: v.vtxo_exit_delta,
101 vtxo_expiry_delta: v.vtxo_expiry_delta,
102 htlc_send_expiry_delta: v.htlc_send_expiry_delta,
103 htlc_expiry_delta: v.htlc_expiry_delta,
104 max_vtxo_amount: v.max_vtxo_amount,
105 required_board_confirmations: v.required_board_confirmations,
106 max_user_invoice_cltv_delta: v.max_user_invoice_cltv_delta,
107 min_board_amount: v.min_board_amount,
108 offboard_feerate_sat_per_kvb: v.offboard_feerate.to_sat_per_kwu() * 4,
109 ln_receive_anti_dos_required: v.ln_receive_anti_dos_required,
110 fees: v.fees.clone().into(),
111 max_vtxo_exit_depth: v.max_vtxo_exit_depth,
112 max_offboard_inputs: v.max_offboard_inputs,
113 }
114 }
115}
116
117#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
121#[cfg_attr(feature = "utoipa", derive(ToSchema))]
122pub struct Balance {
123 #[serde(rename = "spendable_sat", with = "bitcoin::amount::serde::as_sat")]
126 #[cfg_attr(feature = "utoipa", schema(value_type = u64))]
127 pub spendable: Amount,
128 #[serde(rename = "pending_lightning_send_sat", with = "bitcoin::amount::serde::as_sat")]
131 #[cfg_attr(feature = "utoipa", schema(value_type = u64))]
132 pub pending_lightning_send: Amount,
133 #[serde(rename = "claimable_lightning_receive_sat", with = "bitcoin::amount::serde::as_sat")]
136 #[cfg_attr(feature = "utoipa", schema(value_type = u64))]
137 pub claimable_lightning_receive: Amount,
138 #[serde(rename = "pending_in_round_sat", with = "bitcoin::amount::serde::as_sat")]
141 #[cfg_attr(feature = "utoipa", schema(value_type = u64))]
142 pub pending_in_round: Amount,
143 #[serde(rename = "pending_board_sat", with = "bitcoin::amount::serde::as_sat")]
146 #[cfg_attr(feature = "utoipa", schema(value_type = u64))]
147 pub pending_board: Amount,
148 #[serde(
153 default,
154 rename = "pending_exit_sat",
155 with = "bitcoin::amount::serde::as_sat::opt",
156 skip_serializing_if = "Option::is_none",
157 )]
158 #[cfg_attr(feature = "utoipa", schema(value_type = u64, nullable=true))]
159 pub pending_exit: Option<Amount>,
160}
161
162impl From<bark::Balance> for Balance {
163 fn from(v: bark::Balance) -> Self {
164 Balance {
165 spendable: v.spendable,
166 pending_in_round: v.pending_in_round,
167 pending_lightning_send: v.pending_lightning_send,
168 claimable_lightning_receive: v.claimable_lightning_receive,
169 pending_exit: v.pending_exit,
170 pending_board: v.pending_board,
171 }
172 }
173}
174
175#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
176#[cfg_attr(feature = "utoipa", derive(ToSchema))]
177pub struct ExitProgressResponse {
178 pub exits: Vec<ExitProgressStatus>,
180 pub done: bool,
182 pub claimable_height: Option<u32>,
184 #[serde(default, skip_serializing_if = "Option::is_none")]
189 pub error: Option<ExitError>,
190}
191
192#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
193#[cfg_attr(feature = "utoipa", derive(ToSchema))]
194pub struct ExitProgressStatus {
195 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
197 pub vtxo_id: VtxoId,
198 pub state: ExitState,
200 #[serde(default, skip_serializing_if = "Option::is_none")]
202 pub error: Option<ExitError>,
203}
204
205impl From<bark::exit::ExitProgressStatus> for ExitProgressStatus {
206 fn from(v: bark::exit::ExitProgressStatus) -> Self {
207 ExitProgressStatus {
208 vtxo_id: v.vtxo_id,
209 state: v.state.into(),
210 error: v.error.map(ExitError::from),
211 }
212 }
213}
214
215#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
216#[cfg_attr(feature = "utoipa", derive(ToSchema))]
217pub struct ExitTransactionStatus {
218 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
220 pub vtxo_id: VtxoId,
221 pub state: ExitState,
223 #[serde(default, skip_serializing_if = "Option::is_none")]
225 pub history: Option<Vec<ExitState>>,
226 #[serde(default, skip_serializing_if = "Vec::is_empty")]
228 pub transactions: Vec<ExitTransactionPackage>,
229}
230
231impl From<bark::exit::ExitTransactionStatus> for ExitTransactionStatus {
232 fn from(v: bark::exit::ExitTransactionStatus) -> Self {
233 ExitTransactionStatus {
234 vtxo_id: v.vtxo_id,
235 state: v.state.into(),
236 history: v.history.map(|h| h.into_iter().map(ExitState::from).collect()),
237 transactions: v.transactions.into_iter().map(ExitTransactionPackage::from).collect(),
238 }
239 }
240}
241
242#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
244#[cfg_attr(feature = "utoipa", derive(ToSchema))]
245pub struct PendingBoardInfo {
246 pub funding_tx: TransactionInfo,
250 #[cfg_attr(feature = "utoipa", schema(value_type = Vec<String>))]
255 pub vtxos: Vec<VtxoId>,
256 #[serde(rename = "amount_sat", with = "bitcoin::amount::serde::as_sat")]
258 #[cfg_attr(feature = "utoipa", schema(value_type = u64))]
259 pub amount: Amount,
260 pub movement_id: u32,
262}
263
264impl From<bark::persist::models::PendingBoard> for PendingBoardInfo {
265 fn from(v: bark::persist::models::PendingBoard) -> Self {
266 PendingBoardInfo {
267 funding_tx: v.funding_tx.into(),
268 vtxos: v.vtxos,
269 amount: v.amount,
270 movement_id: v.movement_id.0,
271 }
272 }
273}
274
275#[derive(Debug, Clone, Serialize, Deserialize)]
276#[serde(tag = "status", rename_all = "kebab-case")]
277#[cfg_attr(feature = "utoipa", derive(ToSchema))]
278pub enum RoundStatus {
279 SyncError {
281 error: String,
282 },
283 Confirmed {
285 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
286 funding_txid: Txid,
287 },
288 Unconfirmed {
290 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
291 funding_txid: Txid,
292 },
293 Pending,
295 Failed {
297 error: String,
298 },
299 Canceled,
301}
302
303impl RoundStatus {
304 pub fn is_final(&self) -> bool {
306 match self {
307 Self::SyncError { .. } => false,
308 Self::Confirmed { .. } => true,
309 Self::Unconfirmed { .. } => false,
310 Self::Pending { .. } => false,
311 Self::Failed { .. } => true,
312 Self::Canceled => true,
313 }
314 }
315
316 pub fn is_success(&self) -> bool {
318 match self {
319 Self::SyncError { .. } => false,
320 Self::Confirmed { .. } => true,
321 Self::Unconfirmed { .. } => true,
322 Self::Pending { .. } => false,
323 Self::Failed { .. } => false,
324 Self::Canceled => false,
325 }
326 }
327}
328
329impl From<bark::round::RoundStatus> for RoundStatus {
330 fn from(s: bark::round::RoundStatus) -> Self {
331 match s {
332 bark::round::RoundStatus::Confirmed { funding_txid } => {
333 Self::Confirmed { funding_txid }
334 },
335 bark::round::RoundStatus::Unconfirmed { funding_txid } => {
336 Self::Unconfirmed { funding_txid }
337 },
338 bark::round::RoundStatus::Pending => Self::Pending,
339 bark::round::RoundStatus::Failed { error } => Self::Failed { error },
340 bark::round::RoundStatus::Canceled => Self::Canceled,
341 }
342 }
343}
344
345#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
346#[cfg_attr(feature = "utoipa", derive(ToSchema))]
347pub struct RoundStateInfo {
348 pub round_state_id: u32,
349}
350
351#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
352#[cfg_attr(feature = "utoipa", derive(ToSchema))]
353pub struct InvoiceInfo {
354 pub invoice: String,
356}
357
358#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
359#[cfg_attr(feature = "utoipa", derive(ToSchema))]
360pub struct OffboardResult {
361 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
363 pub offboard_txid: Txid,
364}
365
366#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
367#[cfg_attr(feature = "utoipa", derive(ToSchema))]
368pub struct LightningReceiveInfo {
369 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
371 pub payment_hash: PaymentHash,
372 pub state: String,
375 pub invoice: String,
377 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
379 pub payment_preimage: Option<Preimage>,
380 #[serde(rename = "amount_sat", with = "bitcoin::amount::serde::as_sat")]
382 #[cfg_attr(feature = "utoipa", schema(value_type = u64))]
383 pub amount: Amount,
384 #[serde(default, deserialize_with = "serde_utils::null_as_default")]
388 #[cfg_attr(feature = "utoipa", schema(value_type = Vec<String>, required = true))]
389 pub htlc_vtxo_ids: Vec<VtxoId>,
390 pub settled_at: Option<chrono::DateTime<chrono::Local>>,
392
393 #[deprecated(note = "no longer tracked; use `state` and `settled_at`")]
395 #[serde(default)]
396 pub preimage_revealed_at: Option<chrono::DateTime<chrono::Local>>,
397 #[deprecated(note = "renamed to `settled_at`")]
399 #[serde(default)]
400 pub finished_at: Option<chrono::DateTime<chrono::Local>>,
401 #[deprecated(note = "replaced by `htlc_vtxo_ids`")]
403 #[serde(default, deserialize_with = "serde_utils::null_as_default")]
404 #[cfg_attr(feature = "utoipa", schema(required = true))]
405 pub htlc_vtxos: Vec<WalletVtxoInfo>,
406}
407
408impl LightningReceiveInfo {
409 #[allow(deprecated)] pub fn from_state(state: &LightningReceiveState) -> Self {
412 match state {
413 LightningReceiveState::InProgress(recv) => LightningReceiveInfo::from(recv),
414 LightningReceiveState::Settled(s) => LightningReceiveInfo {
415 payment_hash: s.payment_hash,
416 state: "settled".to_string(),
417 invoice: s.invoice.to_string(),
418 payment_preimage: Some(s.preimage),
419 amount: s.amount,
420 htlc_vtxo_ids: vec![],
421 settled_at: Some(s.settled_at),
422 preimage_revealed_at: None,
423 finished_at: Some(s.settled_at),
424 htlc_vtxos: vec![],
425 },
426 }
427 }
428}
429
430impl From<&LightningReceive> for LightningReceiveInfo {
431 #[allow(deprecated)] fn from(recv: &LightningReceive) -> Self {
433 let (state, htlc_vtxo_ids) = match &recv.progress {
434 ReceiveProgress::AwaitingPayment => ("awaiting-payment", vec![]),
435 ReceiveProgress::HtlcsReady(htlcs) => ("htlcs-ready", htlcs.vtxo_ids.clone()),
436 ReceiveProgress::PreimageRevealed(htlcs) => ("preimage-revealed", htlcs.vtxo_ids.clone()),
437 ReceiveProgress::Delivering(_) => ("delivering", vec![]),
439 };
440 LightningReceiveInfo {
441 payment_hash: recv.payment_hash,
442 state: state.to_string(),
443 invoice: recv.invoice.to_string(),
444 payment_preimage: Some(recv.payment_preimage),
445 amount: recv.invoice.amount_milli_satoshis()
446 .map(Amount::from_msat_floor)
447 .expect("generated invoice with no amount"),
448 htlc_vtxo_ids,
449 settled_at: None,
450 preimage_revealed_at: None,
451 finished_at: None,
452 htlc_vtxos: vec![],
453 }
454 }
455}
456
457#[cfg(test)]
458mod test {
459 use bitcoin::FeeRate;
460 use super::*;
461
462 fn lightning_receive_base_json() -> serde_json::Value {
463 serde_json::json!({
464 "amount_sat": 1000,
465 "payment_hash": "0000000000000000000000000000000000000000000000000000000000000000",
466 "payment_preimage": "0000000000000000000000000000000000000000000000000000000000000000",
467 "state": "awaiting-payment",
468 "settled_at": null,
469 "invoice": "lnbc1",
470 })
471 }
472
473 #[test]
474 fn deserialize_lightning_receive_htlc_vtxo_ids_missing() {
475 let json = lightning_receive_base_json();
476 serde_json::from_value::<LightningReceiveInfo>(json).unwrap();
477 }
478
479 #[test]
480 fn deserialize_lightning_receive_htlc_vtxo_ids_null() {
481 let mut json = lightning_receive_base_json();
482 json["htlc_vtxo_ids"] = serde_json::json!(null);
483 serde_json::from_value::<LightningReceiveInfo>(json).unwrap();
484 }
485
486 #[test]
487 fn deserialize_lightning_receive_htlc_vtxo_ids_empty() {
488 let mut json = lightning_receive_base_json();
489 json["htlc_vtxo_ids"] = serde_json::json!([]);
490 serde_json::from_value::<LightningReceiveInfo>(json).unwrap();
491 }
492
493 #[test]
494 fn ark_info_fields() {
495 #[allow(unused, deprecated)]
499 fn convert(j: ArkInfo) -> ark::ArkInfo {
500 ark::ArkInfo {
501 network: j.network,
502 server_pubkey: j.server_pubkey,
503 mailbox_pubkey: j.mailbox_pubkey,
504 round_interval: j.round_interval,
505 nb_round_nonces: j.nb_round_nonces,
506 vtxo_exit_delta: j.vtxo_exit_delta,
507 vtxo_expiry_delta: j.vtxo_expiry_delta,
508 htlc_send_expiry_delta: j.htlc_send_expiry_delta,
509 htlc_expiry_delta: j.htlc_expiry_delta,
510 max_vtxo_amount: j.max_vtxo_amount,
511 required_board_confirmations: j.required_board_confirmations,
512 max_user_invoice_cltv_delta: j.max_user_invoice_cltv_delta,
513 min_board_amount: j.min_board_amount,
514 offboard_feerate: FeeRate::from_sat_per_kwu(j.offboard_feerate_sat_per_kvb / 4),
515 ln_receive_anti_dos_required: j.ln_receive_anti_dos_required,
516 fees: j.fees.into(),
517 max_vtxo_exit_depth: j.max_vtxo_exit_depth,
518 max_offboard_inputs: j.max_offboard_inputs,
519 }
520 }
521 }
522}
523