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::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	/// The bitcoin network the server operates on
32	#[cfg_attr(feature = "utoipa", schema(value_type = String))]
33	pub network: bitcoin::Network,
34	/// The Ark server pubkey
35	#[cfg_attr(feature = "utoipa", schema(value_type = String))]
36	pub server_pubkey: PublicKey,
37	/// The pubkey used for blinding unified mailbox IDs
38	#[cfg_attr(feature = "utoipa", schema(value_type = String))]
39	pub mailbox_pubkey: PublicKey,
40	/// The interval between each round
41	#[serde(with = "serde_utils::duration")]
42	#[cfg_attr(feature = "utoipa", schema(value_type = String))]
43	pub round_interval: Duration,
44	/// Number of nonces per round
45	pub nb_round_nonces: usize,
46	/// Delta between exit confirmation and coins becoming spendable
47	pub vtxo_exit_delta: BlockDelta,
48	/// Expiration delta of the VTXO
49	pub vtxo_expiry_delta: BlockDelta,
50	/// The number of blocks after which an HTLC-send VTXO expires once granted.
51	pub htlc_send_expiry_delta: BlockDelta,
52	/// The number of blocks to keep between Lightning and Ark HTLCs expiries
53	pub htlc_expiry_delta: BlockDelta,
54	/// Maximum amount of a VTXO
55	#[cfg_attr(feature = "utoipa", schema(value_type = u64))]
56	pub max_vtxo_amount: Option<Amount>,
57	/// The number of confirmations required to register a board vtxo
58	pub required_board_confirmations: usize,
59	/// Maximum CLTV delta server will allow clients to request an
60	/// invoice generation with.
61	pub max_user_invoice_cltv_delta: u16,
62	/// Minimum amount for a board the server will cosign
63	#[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	/// offboard feerate in sat per kvb
67	pub offboard_feerate_sat_per_kvb: u64,
68	/// Indicates whether the Ark server requires clients to either
69	/// provide a VTXO ownership proof, or a lightning receive token
70	/// when preparing a lightning claim.
71	pub ln_receive_anti_dos_required: bool,
72	/// The fee schedule outlining any fees that must be paid to interact with the Ark server.
73	pub fees: FeeSchedule,
74	/// Maximum exit depth (genesis chain length) allowed for a VTXO.
75	/// Once a VTXO's exit depth reaches this value the server will refuse to
76	/// cosign further OOR transactions spending it. Clients should refresh
77	/// their VTXOs into a round before this limit is reached.
78	pub max_vtxo_exit_depth: u16,
79	/// The maximum number of inputs for an offboard
80	pub max_offboard_inputs: usize,
81}
82
83#[derive(Debug, Clone, Deserialize, Serialize)]
84#[cfg_attr(feature = "utoipa", derive(ToSchema))]
85pub struct NextRoundStart {
86	/// The next round start time in RFC 3339 format
87	pub start_time: chrono::DateTime<chrono::Local>,
88}
89
90impl<T: Borrow<ark::ArkInfo>> From<T> for ArkInfo {
91	#[allow(deprecated)] // offboard_feerate kept for old clients
92	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/// The different balances of a Bark wallet, broken down by state.
118///
119/// All amounts are in sats.
120#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
121#[cfg_attr(feature = "utoipa", derive(ToSchema))]
122pub struct Balance {
123	/// Sats that are immediately spendable, either in-round or
124	/// out-of-round.
125	#[serde(rename = "spendable_sat", with = "bitcoin::amount::serde::as_sat")]
126	#[cfg_attr(feature = "utoipa", schema(value_type = u64))]
127	pub spendable: Amount,
128	/// Sats locked in an outgoing Lightning payment that has not yet
129	/// settled.
130	#[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	/// Sats from an incoming Lightning payment that can be claimed but
134	/// have not yet been swept into a spendable VTXO.
135	#[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	/// Sats locked in VTXOs forfeited for a round that has not yet
139	/// completed.
140	#[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	/// Sats in board transactions that are waiting for sufficient
144	/// on-chain confirmations before becoming spendable.
145	#[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	/// Sats held in VTXOs whose unilateral exit chain is confirmed on-chain but which
149	/// haven't yet been drained to the onchain wallet. Equivalent to the sum of
150	/// `Exited` VTXOs whose exit state hasn't reached `Claimed`.
151	/// `null` if the exit subsystem is unavailable.
152	#[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	/// Status of each pending exit transaction
179	pub exits: Vec<ExitProgressStatus>,
180	/// Whether all transactions have been confirmed
181	pub done: bool,
182	/// Block height at which all exit outputs will be spendable
183	pub claimable_height: Option<u32>,
184	/// Top-level error that prevented progress from running cleanly this round. Per-exit
185	/// problems live on each `ExitProgressStatus`; this slot is for failures that can't
186	/// be attributed to a specific VTXO (e.g. the chain source becoming unavailable, or
187	/// the exit manager failing to refresh its view of pending transactions).
188	#[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	/// The ID of the VTXO that is being unilaterally exited
196	#[cfg_attr(feature = "utoipa", schema(value_type = String))]
197	pub vtxo_id: VtxoId,
198	/// The current state of the exit transaction
199	pub state: ExitState,
200	/// Any error that occurred during the exit process
201	#[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	/// The ID of the VTXO that is being unilaterally exited
219	#[cfg_attr(feature = "utoipa", schema(value_type = String))]
220	pub vtxo_id: VtxoId,
221	/// The current state of the exit transaction
222	pub state: ExitState,
223	/// The history of each state the exit transaction has gone through
224	#[serde(default, skip_serializing_if = "Option::is_none")]
225	pub history: Option<Vec<ExitState>>,
226	/// Each exit transaction package required for the unilateral exit
227	#[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/// Describes a completed transition of funds from onchain to offchain.
243#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
244#[cfg_attr(feature = "utoipa", derive(ToSchema))]
245pub struct PendingBoardInfo {
246	/// The funding transaction.
247	/// This is the transaction that has to be confirmed
248	/// onchain for the board to succeed.
249	pub funding_tx: TransactionInfo,
250	/// The IDs of the VTXOs that were created
251	/// in this board.
252	///
253	/// Currently, this is always a vector of length 1
254	#[cfg_attr(feature = "utoipa", schema(value_type = Vec<String>))]
255	pub vtxos: Vec<VtxoId>,
256	/// The amount of the board.
257	#[serde(rename = "amount_sat", with = "bitcoin::amount::serde::as_sat")]
258	#[cfg_attr(feature = "utoipa", schema(value_type = u64))]
259	pub amount: Amount,
260	/// The ID of the movement associated with this board.
261	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	/// Failed to sync round
280	SyncError {
281		error: String,
282	},
283	/// The round was successful and is fully confirmed
284	Confirmed {
285		#[cfg_attr(feature = "utoipa", schema(value_type = String))]
286		funding_txid: Txid,
287	},
288	/// Round successful but not fully confirmed
289	Unconfirmed {
290		#[cfg_attr(feature = "utoipa", schema(value_type = String))]
291		funding_txid: Txid,
292	},
293	/// We have unsigned funding transactions that might confirm
294	Pending,
295	/// The round failed
296	Failed {
297		error: String,
298	},
299	/// The round canceled
300	Canceled,
301}
302
303impl RoundStatus {
304	/// Whether this is the final state and it won't change anymore
305	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	/// Whether it looks like the round succeeded
317	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	/// The invoice string
355	pub invoice: String,
356}
357
358#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
359#[cfg_attr(feature = "utoipa", derive(ToSchema))]
360pub struct OffboardResult {
361	/// The transaction id of the offboard transaction
362	#[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	/// The payment hash linked to the lightning receive
370	#[cfg_attr(feature = "utoipa", schema(value_type = String))]
371	pub payment_hash: PaymentHash,
372	/// Lifecycle phase of the receive: `awaiting-payment`, `htlcs-ready`,
373	/// `preimage-revealed`, `delivering`, or `settled`.
374	pub state: String,
375	/// The invoice string, if known.
376	pub invoice: String,
377	/// The payment preimage, if known.
378	#[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
379	pub payment_preimage: Option<Preimage>,
380	/// The amount of the lightning receive, if known.
381	#[serde(rename = "amount_sat", with = "bitcoin::amount::serde::as_sat")]
382	#[cfg_attr(feature = "utoipa", schema(value_type = u64))]
383	pub amount: Amount,
384	/// IDs of the HTLC-recv VTXOs granted by the server, if any.
385	///
386	/// Empty until the inbound HTLC has been received and prepared.
387	#[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	/// The timestamp at which the receive settled, if it has.
391	pub settled_at: Option<chrono::DateTime<chrono::Local>>,
392
393	/// The timestamp at which the preimage was revealed.
394	#[deprecated(note = "no longer tracked; use `state` and `settled_at`")]
395	#[serde(default)]
396	pub preimage_revealed_at: Option<chrono::DateTime<chrono::Local>>,
397	/// The timestamp at which the lightning receive was finished.
398	#[deprecated(note = "renamed to `settled_at`")]
399	#[serde(default)]
400	pub finished_at: Option<chrono::DateTime<chrono::Local>>,
401	/// The HTLC VTXOs granted by the server for the lightning receive.
402	#[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	/// Render a triaged receive state, mirroring the send-side status.
410	#[allow(deprecated)] // populates deprecated compat fields kept for old clients
411	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)] // populates deprecated compat fields kept for old clients
432	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			// The HTLCs are spent once the claim outputs await delivery.
438			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		//! the purpose of this test is to fail if we add a field to
496		//! ark::ArkInfo but we forgot to add it to the ArkInfo here
497
498		#[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