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