Skip to main content

bark_json/cli/
mod.rs

1pub mod fees;
2#[cfg(feature = "onchain-bdk")]
3pub mod onchain;
4
5use std::borrow::Borrow;
6use std::time::Duration;
7
8use bitcoin::secp256k1::{schnorr, 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::pay::{LightningSendState, Progress as SendProgress};
18use bark::actions::lightning::receive::{
19	LightningReceive, LightningReceiveState, Progress as ReceiveProgress,
20};
21
22use crate::cli::fees::FeeSchedule;
23use crate::exit::error::ExitError;
24use crate::exit::package::ExitTransactionPackage;
25use crate::exit::ExitState;
26use crate::primitives::{TransactionInfo, WalletVtxoInfo};
27use crate::serde_utils;
28
29#[derive(Debug, Clone, Serialize)]
30#[cfg_attr(feature = "utoipa", derive(ToSchema))]
31pub struct ArkInfo {
32	/// The bitcoin network the server operates on
33	#[cfg_attr(feature = "utoipa", schema(value_type = String))]
34	pub network: bitcoin::Network,
35	/// The Ark server pubkey
36	#[cfg_attr(feature = "utoipa", schema(value_type = String))]
37	pub server_pubkey: PublicKey,
38	/// The pubkey used for blinding unified mailbox IDs
39	#[cfg_attr(feature = "utoipa", schema(value_type = String))]
40	pub mailbox_pubkey: PublicKey,
41	/// The interval between each round
42	#[serde(with = "serde_utils::duration")]
43	#[cfg_attr(feature = "utoipa", schema(value_type = String))]
44	pub round_interval: Duration,
45	/// Number of nonces per round
46	pub nb_round_nonces: usize,
47	/// Delta between exit confirmation and coins becoming spendable
48	pub vtxo_exit_delta: BlockDelta,
49	/// The number of blocks a VTXO lives before it expires
50	#[serde(default)]
51	pub vtxo_lifetime: BlockDelta,
52	/// The number of blocks after which an HTLC-send VTXO expires once granted.
53	pub htlc_send_expiry_delta: BlockDelta,
54	/// The number of blocks to keep between Lightning and Ark HTLCs expiries
55	pub htlc_expiry_delta: BlockDelta,
56	/// Maximum amount of a VTXO
57	#[cfg_attr(feature = "utoipa", schema(value_type = u64))]
58	pub max_vtxo_amount: Option<Amount>,
59	/// The number of confirmations required to register a board vtxo
60	pub required_board_confirmations: usize,
61	/// Maximum CLTV delta server will allow clients to request an
62	/// invoice generation with.
63	pub max_user_invoice_cltv_delta: u16,
64	/// Minimum amount for a board the server will cosign
65	#[serde(rename = "min_board_amount_sat", with = "bitcoin::amount::serde::as_sat")]
66	#[cfg_attr(feature = "utoipa", schema(value_type = u64))]
67	pub min_board_amount: Amount,
68	/// offboard feerate in sat per kvb
69	pub offboard_feerate_sat_per_kvb: u64,
70	/// Indicates whether the Ark server requires clients to either
71	/// provide a VTXO ownership proof, or a lightning receive token
72	/// when preparing a lightning claim.
73	pub ln_receive_anti_dos_required: bool,
74	/// The fee schedule outlining any fees that must be paid to interact with the Ark server.
75	pub fees: FeeSchedule,
76	/// Maximum exit depth (genesis chain length) allowed for a VTXO.
77	/// Once a VTXO's exit depth reaches this value the server will refuse to
78	/// cosign further OOR transactions spending it. Clients should refresh
79	/// their VTXOs into a round before this limit is reached.
80	pub max_vtxo_exit_depth: u16,
81	/// Link to the server's terms of service, if any.
82	pub tos_link: Option<String>,
83	/// The maximum number of inputs for an offboard
84	pub max_offboard_inputs: usize,
85
86	/// The number of blocks a VTXO lives before it expires.
87	///
88	/// **Deprecated**: renamed to `vtxo_lifetime`. This field is still
89	/// populated with the same value for backwards compatibility and will
90	/// be removed in a future release.
91	#[deprecated(note = "renamed to `vtxo_lifetime`")]
92	#[serde(default)]
93	#[cfg_attr(feature = "utoipa", schema(required = true))]
94	pub vtxo_expiry_delta: BlockDelta,
95}
96
97impl<'de> serde::Deserialize<'de> for ArkInfo {
98	#[allow(deprecated)]
99	fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
100		#[derive(Deserialize)]
101		struct ArkInfoStub {
102			network: bitcoin::Network,
103			server_pubkey: PublicKey,
104			mailbox_pubkey: PublicKey,
105			#[serde(with = "serde_utils::duration")]
106			round_interval: Duration,
107			nb_round_nonces: usize,
108			vtxo_exit_delta: BlockDelta,
109			#[serde(default)]
110			vtxo_lifetime: BlockDelta,
111			htlc_send_expiry_delta: BlockDelta,
112			htlc_expiry_delta: BlockDelta,
113			max_vtxo_amount: Option<Amount>,
114			required_board_confirmations: usize,
115			max_user_invoice_cltv_delta: u16,
116			#[serde(rename = "min_board_amount_sat", with = "bitcoin::amount::serde::as_sat")]
117			min_board_amount: Amount,
118			offboard_feerate_sat_per_kvb: u64,
119			ln_receive_anti_dos_required: bool,
120			fees: FeeSchedule,
121			max_vtxo_exit_depth: u16,
122			tos_link: Option<String>,
123			max_offboard_inputs: usize,
124			#[serde(default)]
125			vtxo_expiry_delta: BlockDelta,
126		}
127
128		let v = ArkInfoStub::deserialize(d)?;
129
130		let vtxo_lifetime = match (v.vtxo_lifetime, v.vtxo_expiry_delta) {
131			(0, expiry) => expiry,
132			(lifetime, 0) => lifetime,
133			(lifetime, expiry) if lifetime == expiry => lifetime,
134			(lifetime, expiry) => return Err(serde::de::Error::custom(format!(
135				"vtxo_lifetime ({}) and vtxo_expiry_delta ({}) don't match", lifetime, expiry,
136			))),
137		};
138
139		Ok(ArkInfo {
140			network: v.network,
141			server_pubkey: v.server_pubkey,
142			mailbox_pubkey: v.mailbox_pubkey,
143			round_interval: v.round_interval,
144			nb_round_nonces: v.nb_round_nonces,
145			vtxo_exit_delta: v.vtxo_exit_delta,
146			vtxo_lifetime: vtxo_lifetime,
147			vtxo_expiry_delta: vtxo_lifetime,
148			htlc_send_expiry_delta: v.htlc_send_expiry_delta,
149			htlc_expiry_delta: v.htlc_expiry_delta,
150			max_vtxo_amount: v.max_vtxo_amount,
151			required_board_confirmations: v.required_board_confirmations,
152			max_user_invoice_cltv_delta: v.max_user_invoice_cltv_delta,
153			min_board_amount: v.min_board_amount,
154			offboard_feerate_sat_per_kvb: v.offboard_feerate_sat_per_kvb,
155			ln_receive_anti_dos_required: v.ln_receive_anti_dos_required,
156			fees: v.fees,
157			max_vtxo_exit_depth: v.max_vtxo_exit_depth,
158			tos_link: v.tos_link,
159			max_offboard_inputs: v.max_offboard_inputs,
160		})
161	}
162}
163
164#[derive(Debug, Clone, Deserialize, Serialize)]
165#[cfg_attr(feature = "utoipa", derive(ToSchema))]
166pub struct NextRoundStart {
167	/// The next round start time in RFC 3339 format
168	pub start_time: chrono::DateTime<chrono::Local>,
169}
170
171impl<T: Borrow<ark::ArkInfo>> From<T> for ArkInfo {
172	#[allow(deprecated)] // vtxo_expiry_delta and offboard_feerate kept for old clients
173	fn from(v: T) -> Self {
174		let v = v.borrow();
175	    ArkInfo {
176			network: v.network,
177			server_pubkey: v.server_pubkey,
178			mailbox_pubkey: v.mailbox_pubkey,
179			round_interval: v.round_interval,
180			nb_round_nonces: v.nb_round_nonces,
181			vtxo_exit_delta: v.vtxo_exit_delta,
182			vtxo_lifetime: v.vtxo_lifetime,
183			// we serve the deprecated field from the new one so that it
184			// can never go stale for old clients
185			vtxo_expiry_delta: v.vtxo_lifetime,
186			htlc_send_expiry_delta: v.htlc_send_expiry_delta,
187			htlc_expiry_delta: v.htlc_expiry_delta,
188			max_vtxo_amount: v.max_vtxo_amount,
189			required_board_confirmations: v.required_board_confirmations,
190			max_user_invoice_cltv_delta: v.max_user_invoice_cltv_delta,
191			min_board_amount: v.min_board_amount,
192			offboard_feerate_sat_per_kvb: v.offboard_feerate.to_sat_per_kwu() * 4,
193			ln_receive_anti_dos_required: v.ln_receive_anti_dos_required,
194			fees: v.fees.clone().into(),
195			max_vtxo_exit_depth: v.max_vtxo_exit_depth,
196			max_offboard_inputs: v.max_offboard_inputs,
197			tos_link: v.tos_link.clone(),
198		}
199	}
200}
201
202/// A signature over a message
203#[derive(Debug, Clone, Deserialize, Serialize)]
204#[cfg_attr(feature = "utoipa", derive(ToSchema))]
205pub struct SignedMessage {
206	/// The BIP-340 Schnorr signature over the message digest
207	/// `SHA256("bark/message" || message)`
208	#[cfg_attr(feature = "utoipa", schema(value_type = String))]
209	pub signature: schnorr::Signature,
210}
211
212/// The result of verifying a signed message
213#[derive(Debug, Clone, Deserialize, Serialize)]
214#[cfg_attr(feature = "utoipa", derive(ToSchema))]
215pub struct MessageVerification {
216	/// Whether the signature is valid for the given message and key
217	pub valid: bool,
218}
219
220/// The different balances of a Bark wallet, broken down by state.
221///
222/// All amounts are in sats.
223#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
224#[cfg_attr(feature = "utoipa", derive(ToSchema))]
225pub struct Balance {
226	/// Sats that are immediately spendable, either in-round or
227	/// out-of-round.
228	#[serde(rename = "spendable_sat", with = "bitcoin::amount::serde::as_sat")]
229	#[cfg_attr(feature = "utoipa", schema(value_type = u64))]
230	pub spendable: Amount,
231	/// Sats locked in an outgoing Lightning payment that has not yet
232	/// settled.
233	#[serde(rename = "pending_lightning_send_sat", with = "bitcoin::amount::serde::as_sat")]
234	#[cfg_attr(feature = "utoipa", schema(value_type = u64))]
235	pub pending_lightning_send: Amount,
236	/// Sats from an incoming Lightning payment that can be claimed but
237	/// have not yet been swept into a spendable VTXO.
238	#[serde(rename = "claimable_lightning_receive_sat", with = "bitcoin::amount::serde::as_sat")]
239	#[cfg_attr(feature = "utoipa", schema(value_type = u64))]
240	pub claimable_lightning_receive: Amount,
241	/// Sats locked in VTXOs forfeited for a round that has not yet
242	/// completed.
243	#[serde(rename = "pending_in_round_sat", with = "bitcoin::amount::serde::as_sat")]
244	#[cfg_attr(feature = "utoipa", schema(value_type = u64))]
245	pub pending_in_round: Amount,
246	/// Sats in board transactions that are waiting for sufficient
247	/// on-chain confirmations before becoming spendable.
248	#[serde(rename = "pending_board_sat", with = "bitcoin::amount::serde::as_sat")]
249	#[cfg_attr(feature = "utoipa", schema(value_type = u64))]
250	pub pending_board: Amount,
251	/// Sats held in VTXOs whose unilateral exit chain is confirmed on-chain but which
252	/// haven't yet been drained to the onchain wallet. Equivalent to the sum of
253	/// `Exited` VTXOs whose exit state hasn't reached `Claimed`.
254	/// `null` if the exit subsystem is unavailable.
255	#[serde(
256		default,
257		rename = "pending_exit_sat",
258		with = "bitcoin::amount::serde::as_sat::opt",
259		skip_serializing_if = "Option::is_none",
260	)]
261	#[cfg_attr(feature = "utoipa", schema(value_type = u64, nullable=true))]
262	pub pending_exit: Option<Amount>,
263}
264
265impl From<bark::Balance> for Balance {
266	fn from(v: bark::Balance) -> Self {
267		Balance {
268			spendable: v.spendable,
269			pending_in_round: v.pending_in_round,
270			pending_lightning_send: v.pending_lightning_send,
271			claimable_lightning_receive: v.claimable_lightning_receive,
272			pending_exit: v.pending_exit,
273			pending_board: v.pending_board,
274		}
275	}
276}
277
278#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
279#[cfg_attr(feature = "utoipa", derive(ToSchema))]
280pub struct ExitProgressResponse {
281	/// Status of each pending exit transaction
282	pub exits: Vec<ExitProgressStatus>,
283	/// Whether all transactions have been confirmed
284	pub done: bool,
285	/// Block height at which all exit outputs will be spendable
286	pub claimable_height: Option<u32>,
287	/// Top-level error that prevented progress from running cleanly this round. Per-exit
288	/// problems live on each `ExitProgressStatus`; this slot is for failures that can't
289	/// be attributed to a specific VTXO (e.g. the chain source becoming unavailable, or
290	/// the exit manager failing to refresh its view of pending transactions).
291	#[serde(default, skip_serializing_if = "Option::is_none")]
292	pub error: Option<ExitError>,
293}
294
295#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
296#[cfg_attr(feature = "utoipa", derive(ToSchema))]
297pub struct ExitProgressStatus {
298	/// The ID of the VTXO that is being unilaterally exited
299	#[cfg_attr(feature = "utoipa", schema(value_type = String))]
300	pub vtxo_id: VtxoId,
301	/// The current state of the exit transaction
302	pub state: ExitState,
303	/// Any error that occurred during the exit process
304	#[serde(default, skip_serializing_if = "Option::is_none")]
305	pub error: Option<ExitError>,
306}
307
308impl From<bark::exit::ExitProgressStatus> for ExitProgressStatus {
309	fn from(v: bark::exit::ExitProgressStatus) -> Self {
310		ExitProgressStatus {
311			vtxo_id: v.vtxo_id,
312			state: v.state.into(),
313			error: v.error.map(ExitError::from),
314		}
315	}
316}
317
318#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
319#[cfg_attr(feature = "utoipa", derive(ToSchema))]
320pub struct ExitTransactionStatus {
321	/// The ID of the VTXO that is being unilaterally exited
322	#[cfg_attr(feature = "utoipa", schema(value_type = String))]
323	pub vtxo_id: VtxoId,
324	/// The current state of the exit transaction
325	pub state: ExitState,
326	/// The history of each state the exit transaction has gone through
327	#[serde(default, skip_serializing_if = "Option::is_none")]
328	pub history: Option<Vec<ExitState>>,
329	/// Each exit transaction package required for the unilateral exit
330	#[serde(default, skip_serializing_if = "Vec::is_empty")]
331	pub transactions: Vec<ExitTransactionPackage>,
332}
333
334impl From<bark::exit::ExitTransactionStatus> for ExitTransactionStatus {
335	fn from(v: bark::exit::ExitTransactionStatus) -> Self {
336		ExitTransactionStatus {
337			vtxo_id: v.vtxo_id,
338			state: v.state.into(),
339			history: v.history.map(|h| h.into_iter().map(ExitState::from).collect()),
340			transactions: v.transactions.into_iter().map(ExitTransactionPackage::from).collect(),
341		}
342	}
343}
344
345/// Describes a completed transition of funds from onchain to offchain.
346#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
347#[cfg_attr(feature = "utoipa", derive(ToSchema))]
348pub struct PendingBoardInfo {
349	/// The funding transaction.
350	/// This is the transaction that has to be confirmed
351	/// onchain for the board to succeed.
352	pub funding_tx: TransactionInfo,
353	/// The IDs of the VTXOs that were created
354	/// in this board.
355	///
356	/// Currently, this is always a vector of length 1
357	#[cfg_attr(feature = "utoipa", schema(value_type = Vec<String>))]
358	pub vtxos: Vec<VtxoId>,
359	/// The amount of the board.
360	#[serde(rename = "amount_sat", with = "bitcoin::amount::serde::as_sat")]
361	#[cfg_attr(feature = "utoipa", schema(value_type = u64))]
362	pub amount: Amount,
363	/// The ID of the movement associated with this board.
364	pub movement_id: u32,
365}
366
367impl From<bark::persist::models::PendingBoard> for PendingBoardInfo {
368	fn from(v: bark::persist::models::PendingBoard) -> Self {
369		PendingBoardInfo {
370			funding_tx: v.funding_tx.into(),
371			vtxos: v.vtxos,
372			amount: v.amount,
373			movement_id: v.movement_id.0,
374		}
375	}
376}
377
378#[derive(Debug, Clone, Serialize, Deserialize)]
379#[serde(tag = "status", rename_all = "kebab-case")]
380#[cfg_attr(feature = "utoipa", derive(ToSchema))]
381pub enum RoundStatus {
382	/// Failed to sync round
383	SyncError {
384		error: String,
385	},
386	/// The round was successful and is fully confirmed
387	Confirmed {
388		#[cfg_attr(feature = "utoipa", schema(value_type = String))]
389		funding_txid: Txid,
390	},
391	/// Round successful but not fully confirmed
392	Unconfirmed {
393		#[cfg_attr(feature = "utoipa", schema(value_type = String))]
394		funding_txid: Txid,
395	},
396	/// We have unsigned funding transactions that might confirm
397	Pending,
398	/// The round failed
399	Failed {
400		error: String,
401	},
402	/// The round canceled
403	Canceled,
404}
405
406impl RoundStatus {
407	/// Whether this is the final state and it won't change anymore
408	pub fn is_final(&self) -> bool {
409		match self {
410			Self::SyncError { .. } => false,
411			Self::Confirmed { .. } => true,
412			Self::Unconfirmed { .. } => false,
413			Self::Pending { .. } => false,
414			Self::Failed { .. } => true,
415			Self::Canceled => true,
416		}
417	}
418
419	/// Whether it looks like the round succeeded
420	pub fn is_success(&self) -> bool {
421		match self {
422			Self::SyncError { .. } => false,
423			Self::Confirmed { .. } => true,
424			Self::Unconfirmed { .. } => true,
425			Self::Pending { .. } => false,
426			Self::Failed { .. } => false,
427			Self::Canceled => false,
428		}
429	}
430}
431
432impl From<bark::round::RoundStatus> for RoundStatus {
433	fn from(s: bark::round::RoundStatus) -> Self {
434		match s {
435			bark::round::RoundStatus::Confirmed { funding_txid } => {
436				Self::Confirmed { funding_txid }
437			},
438			bark::round::RoundStatus::Unconfirmed { funding_txid } => {
439				Self::Unconfirmed { funding_txid }
440			},
441			bark::round::RoundStatus::Pending => Self::Pending,
442			bark::round::RoundStatus::Failed { error } => Self::Failed { error },
443			bark::round::RoundStatus::Canceled => Self::Canceled,
444		}
445	}
446}
447
448#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
449#[cfg_attr(feature = "utoipa", derive(ToSchema))]
450pub struct RoundStateInfo {
451	pub round_state_id: u32,
452}
453
454#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
455#[cfg_attr(feature = "utoipa", derive(ToSchema))]
456pub struct InvoiceInfo {
457	/// The invoice string
458	pub invoice: String,
459}
460
461#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
462#[cfg_attr(feature = "utoipa", derive(ToSchema))]
463pub struct OffboardResult {
464	/// The transaction id of the offboard transaction
465	#[cfg_attr(feature = "utoipa", schema(value_type = String))]
466	pub offboard_txid: Txid,
467}
468
469#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
470#[cfg_attr(feature = "utoipa", derive(ToSchema))]
471pub struct LightningReceiveInfo {
472	/// The payment hash linked to the lightning receive
473	#[cfg_attr(feature = "utoipa", schema(value_type = String))]
474	pub payment_hash: PaymentHash,
475	/// Lifecycle phase of the receive: `awaiting-payment`, `htlcs-ready`,
476	/// `preimage-revealed`, `delivering`, or `settled`.
477	pub state: String,
478	/// The invoice string, if known.
479	pub invoice: String,
480	/// The payment preimage, if known.
481	#[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
482	pub payment_preimage: Option<Preimage>,
483	/// The amount of the lightning receive, if known.
484	#[serde(rename = "amount_sat", with = "bitcoin::amount::serde::as_sat")]
485	#[cfg_attr(feature = "utoipa", schema(value_type = u64))]
486	pub amount: Amount,
487	/// IDs of the HTLC-recv VTXOs granted by the server, if any.
488	///
489	/// Empty until the inbound HTLC has been received and prepared.
490	#[serde(default, deserialize_with = "serde_utils::null_as_default")]
491	#[cfg_attr(feature = "utoipa", schema(value_type = Vec<String>, required = true))]
492	pub htlc_vtxo_ids: Vec<VtxoId>,
493	/// The timestamp at which the receive settled, if it has.
494	pub settled_at: Option<chrono::DateTime<chrono::Local>>,
495
496	/// The timestamp at which the preimage was revealed.
497	#[deprecated(note = "no longer tracked; use `state` and `settled_at`")]
498	#[serde(default)]
499	pub preimage_revealed_at: Option<chrono::DateTime<chrono::Local>>,
500	/// The timestamp at which the lightning receive was finished.
501	#[deprecated(note = "renamed to `settled_at`")]
502	#[serde(default)]
503	pub finished_at: Option<chrono::DateTime<chrono::Local>>,
504	/// The HTLC VTXOs granted by the server for the lightning receive.
505	#[deprecated(note = "replaced by `htlc_vtxo_ids`")]
506	#[serde(default, deserialize_with = "serde_utils::null_as_default")]
507	#[cfg_attr(feature = "utoipa", schema(required = true))]
508	pub htlc_vtxos: Vec<WalletVtxoInfo>,
509}
510
511impl LightningReceiveInfo {
512	/// Render a triaged receive state, mirroring the send-side status.
513	#[allow(deprecated)] // populates deprecated compat fields kept for old clients
514	pub fn from_state(state: &LightningReceiveState) -> Self {
515		match state {
516			LightningReceiveState::InProgress(recv) => LightningReceiveInfo::from(recv),
517			LightningReceiveState::Settled(s) => LightningReceiveInfo {
518				payment_hash: s.payment_hash,
519				state: "settled".to_string(),
520				invoice: s.invoice.to_string(),
521				payment_preimage: Some(s.preimage),
522				amount: s.amount,
523				htlc_vtxo_ids: vec![],
524				settled_at: Some(s.settled_at),
525				preimage_revealed_at: None,
526				finished_at: Some(s.settled_at),
527				htlc_vtxos: vec![],
528			},
529		}
530	}
531}
532
533#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
534#[cfg_attr(feature = "utoipa", derive(ToSchema))]
535pub struct LightningSendInfo {
536	/// The payment hash of the outgoing lightning payment
537	#[cfg_attr(feature = "utoipa", schema(value_type = String))]
538	pub payment_hash: PaymentHash,
539	/// Lifecycle phase of the send: `unknown`, `start`, `htlc-received`,
540	/// `payment-initiated`, `revocable-htlcs`, `revocation-stuck`, or `paid`.
541	pub state: String,
542	/// The invoice string, if known.
543	pub invoice: Option<String>,
544	/// The payment preimage, revealed once the payment succeeded.
545	#[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
546	pub preimage: Option<Preimage>,
547}
548
549impl LightningSendInfo {
550	/// Render a triaged send state, mirroring the receive-side status.
551	pub fn from_state(hash: PaymentHash, state: &LightningSendState) -> Self {
552		match state {
553			LightningSendState::Unknown => LightningSendInfo {
554				payment_hash: hash,
555				state: "unknown".to_string(),
556				invoice: None,
557				preimage: None,
558			},
559			LightningSendState::Paid(paid) => LightningSendInfo {
560				payment_hash: paid.payment_hash,
561				state: "paid".to_string(),
562				invoice: None,
563				preimage: Some(paid.preimage),
564			},
565			LightningSendState::InProgress(send) => {
566				let phase = match send.progress {
567					SendProgress::Start => "start",
568					SendProgress::HtlcReceived(_) => "htlc-received",
569					SendProgress::PaymentInitiated(_) => "payment-initiated",
570					SendProgress::RevocableHtlcs { .. } => "revocable-htlcs",
571					SendProgress::RevocationStuck { .. } => "revocation-stuck",
572				};
573				LightningSendInfo {
574					payment_hash: send.invoice.payment_hash(),
575					state: phase.to_string(),
576					invoice: Some(send.invoice.to_string()),
577					preimage: None,
578				}
579			},
580		}
581	}
582}
583
584impl From<&LightningReceive> for LightningReceiveInfo {
585	#[allow(deprecated)] // populates deprecated compat fields kept for old clients
586	fn from(recv: &LightningReceive) -> Self {
587		let (state, htlc_vtxo_ids) = match &recv.progress {
588			ReceiveProgress::AwaitingPayment => ("awaiting-payment", vec![]),
589			ReceiveProgress::HtlcsReady(htlcs) => ("htlcs-ready", htlcs.vtxo_ids.clone()),
590			ReceiveProgress::PreimageRevealed(htlcs) => ("preimage-revealed", htlcs.vtxo_ids.clone()),
591			// The HTLCs are spent once the claim outputs await delivery.
592			ReceiveProgress::Delivering(_) => ("delivering", vec![]),
593		};
594		LightningReceiveInfo {
595			payment_hash: recv.payment_hash,
596			state: state.to_string(),
597			invoice: recv.invoice.to_string(),
598			payment_preimage: Some(recv.payment_preimage),
599			amount: recv.invoice.amount_milli_satoshis()
600				.map(Amount::from_msat_floor)
601				.expect("generated invoice with no amount"),
602			htlc_vtxo_ids,
603			settled_at: None,
604			preimage_revealed_at: None,
605			finished_at: None,
606			htlc_vtxos: vec![],
607		}
608	}
609}
610
611#[cfg(test)]
612mod test {
613	use bitcoin::FeeRate;
614	use super::*;
615
616	fn lightning_receive_base_json() -> serde_json::Value {
617		serde_json::json!({
618			"amount_sat": 1000,
619			"payment_hash": "0000000000000000000000000000000000000000000000000000000000000000",
620			"payment_preimage": "0000000000000000000000000000000000000000000000000000000000000000",
621			"state": "awaiting-payment",
622			"settled_at": null,
623			"invoice": "lnbc1",
624		})
625	}
626
627	#[test]
628	fn deserialize_lightning_receive_htlc_vtxo_ids_missing() {
629		let json = lightning_receive_base_json();
630		serde_json::from_value::<LightningReceiveInfo>(json).unwrap();
631	}
632
633	#[test]
634	fn deserialize_lightning_receive_htlc_vtxo_ids_null() {
635		let mut json = lightning_receive_base_json();
636		json["htlc_vtxo_ids"] = serde_json::json!(null);
637		serde_json::from_value::<LightningReceiveInfo>(json).unwrap();
638	}
639
640	#[test]
641	fn deserialize_lightning_receive_htlc_vtxo_ids_empty() {
642		let mut json = lightning_receive_base_json();
643		json["htlc_vtxo_ids"] = serde_json::json!([]);
644		serde_json::from_value::<LightningReceiveInfo>(json).unwrap();
645	}
646
647	#[allow(deprecated)]
648	fn ark_info_base() -> ArkInfo {
649		let pubkey = "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"
650			.parse::<PublicKey>().unwrap();
651		ArkInfo {
652			network: bitcoin::Network::Regtest,
653			server_pubkey: pubkey,
654			mailbox_pubkey: pubkey,
655			round_interval: Duration::from_secs(60),
656			nb_round_nonces: 1,
657			vtxo_exit_delta: 12,
658			vtxo_lifetime: 100,
659			vtxo_expiry_delta: 100,
660			htlc_send_expiry_delta: 100,
661			htlc_expiry_delta: 6,
662			max_vtxo_amount: None,
663			required_board_confirmations: 3,
664			max_user_invoice_cltv_delta: 100,
665			min_board_amount: Amount::from_sat(1000),
666			offboard_feerate_sat_per_kvb: 1000,
667			ln_receive_anti_dos_required: false,
668			fees: ark::fees::FeeSchedule::default().into(),
669			max_vtxo_exit_depth: 10,
670			tos_link: None,
671			max_offboard_inputs: 4,
672		}
673	}
674
675	#[test]
676	#[allow(deprecated)]
677	fn ark_info_vtxo_lifetime_falls_back_to_deprecated_field() {
678		// Servers from before the rename only set vtxo_expiry_delta.
679		let mut json = serde_json::to_value(ark_info_base()).unwrap();
680		json.as_object_mut().unwrap().remove("vtxo_lifetime");
681		json["vtxo_expiry_delta"] = serde_json::json!(42);
682
683		let info = serde_json::from_value::<ArkInfo>(json).unwrap();
684		assert_eq!(info.vtxo_lifetime, 42);
685		assert_eq!(info.vtxo_expiry_delta, 42);
686	}
687
688	#[test]
689	#[allow(deprecated)]
690	fn ark_info_vtxo_lifetime_kept_in_sync() {
691		let mut json = serde_json::to_value(ark_info_base()).unwrap();
692		json["vtxo_lifetime"] = serde_json::json!(42);
693		json["vtxo_expiry_delta"] = serde_json::json!(42);
694
695		let info = serde_json::from_value::<ArkInfo>(json).unwrap();
696		assert_eq!(info.vtxo_lifetime, 42);
697		assert_eq!(info.vtxo_expiry_delta, 42);
698
699		// and both fields are populated again on the way out
700		let json = serde_json::to_value(&info).unwrap();
701		assert_eq!(json["vtxo_lifetime"], 42);
702		assert_eq!(json["vtxo_expiry_delta"], 42);
703	}
704
705	#[test]
706	fn ark_info_vtxo_lifetime_rejects_diverging_fields() {
707		let mut json = serde_json::to_value(ark_info_base()).unwrap();
708		json["vtxo_lifetime"] = serde_json::json!(42);
709		json["vtxo_expiry_delta"] = serde_json::json!(100);
710
711		assert!(serde_json::from_value::<ArkInfo>(json).is_err());
712	}
713
714	#[test]
715	fn ark_info_fields() {
716		//! the purpose of this test is to fail if we add a field to
717		//! ark::ArkInfo but we forgot to add it to the ArkInfo here
718
719		#[allow(unused, deprecated)]
720		fn convert(j: ArkInfo) -> ark::ArkInfo {
721			ark::ArkInfo {
722				network: j.network,
723				server_pubkey: j.server_pubkey,
724				mailbox_pubkey: j.mailbox_pubkey,
725				round_interval: j.round_interval,
726				nb_round_nonces: j.nb_round_nonces,
727				vtxo_exit_delta: j.vtxo_exit_delta,
728				vtxo_lifetime: j.vtxo_lifetime,
729				vtxo_expiry_delta: j.vtxo_expiry_delta,
730				htlc_send_expiry_delta: j.htlc_send_expiry_delta,
731				htlc_expiry_delta: j.htlc_expiry_delta,
732				max_vtxo_amount: j.max_vtxo_amount,
733				required_board_confirmations: j.required_board_confirmations,
734				max_user_invoice_cltv_delta: j.max_user_invoice_cltv_delta,
735				min_board_amount: j.min_board_amount,
736				offboard_feerate: FeeRate::from_sat_per_kwu(j.offboard_feerate_sat_per_kvb / 4),
737				ln_receive_anti_dos_required: j.ln_receive_anti_dos_required,
738				fees: j.fees.into(),
739				max_vtxo_exit_depth: j.max_vtxo_exit_depth,
740				max_offboard_inputs: j.max_offboard_inputs,
741				tos_link: j.tos_link,
742			}
743		}
744	}
745}
746