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 crate::cli::fees::FeeSchedule;
18use crate::exit::error::ExitError;
19use crate::exit::package::ExitTransactionPackage;
20use crate::exit::ExitState;
21use crate::primitives::{TransactionInfo, WalletVtxoInfo};
22use crate::serde_utils;
23
24#[derive(Debug, Clone, Deserialize, Serialize)]
25#[cfg_attr(feature = "utoipa", derive(ToSchema))]
26pub struct ArkInfo {
27	/// The bitcoin network the server operates on
28	#[cfg_attr(feature = "utoipa", schema(value_type = String))]
29	pub network: bitcoin::Network,
30	/// The Ark server pubkey
31	#[cfg_attr(feature = "utoipa", schema(value_type = String))]
32	pub server_pubkey: PublicKey,
33	/// The pubkey used for blinding unified mailbox IDs
34	#[cfg_attr(feature = "utoipa", schema(value_type = String))]
35	pub mailbox_pubkey: PublicKey,
36	/// The interval between each round
37	#[serde(with = "serde_utils::duration")]
38	#[cfg_attr(feature = "utoipa", schema(value_type = String))]
39	pub round_interval: Duration,
40	/// Number of nonces per round
41	pub nb_round_nonces: usize,
42	/// Delta between exit confirmation and coins becoming spendable
43	pub vtxo_exit_delta: BlockDelta,
44	/// Expiration delta of the VTXO
45	pub vtxo_expiry_delta: BlockDelta,
46	/// The number of blocks after which an HTLC-send VTXO expires once granted.
47	pub htlc_send_expiry_delta: BlockDelta,
48	/// The number of blocks to keep between Lightning and Ark HTLCs expiries
49	pub htlc_expiry_delta: BlockDelta,
50	/// Maximum amount of a VTXO
51	#[cfg_attr(feature = "utoipa", schema(value_type = u64))]
52	pub max_vtxo_amount: Option<Amount>,
53	/// The number of confirmations required to register a board vtxo
54	pub required_board_confirmations: usize,
55	/// Maximum CLTV delta server will allow clients to request an
56	/// invoice generation with.
57	pub max_user_invoice_cltv_delta: u16,
58	/// Minimum amount for a board the server will cosign
59	#[serde(rename = "min_board_amount_sat", with = "bitcoin::amount::serde::as_sat")]
60	#[cfg_attr(feature = "utoipa", schema(value_type = u64))]
61	pub min_board_amount: Amount,
62	/// offboard feerate in sat per kvb
63	pub offboard_feerate_sat_per_kvb: u64,
64	/// Indicates whether the Ark server requires clients to either
65	/// provide a VTXO ownership proof, or a lightning receive token
66	/// when preparing a lightning claim.
67	pub ln_receive_anti_dos_required: bool,
68	/// The fee schedule outlining any fees that must be paid to interact with the Ark server.
69	pub fees: FeeSchedule,
70	/// Maximum exit depth (genesis chain length) allowed for a VTXO.
71	/// Once a VTXO's exit depth reaches this value the server will refuse to
72	/// cosign further OOR transactions spending it. Clients should refresh
73	/// their VTXOs into a round before this limit is reached.
74	pub max_vtxo_exit_depth: u16,
75}
76
77#[derive(Debug, Clone, Deserialize, Serialize)]
78#[cfg_attr(feature = "utoipa", derive(ToSchema))]
79pub struct NextRoundStart {
80	/// The next round start time in RFC 3339 format
81	pub start_time: chrono::DateTime<chrono::Local>,
82}
83
84impl<T: Borrow<ark::ArkInfo>> From<T> for ArkInfo {
85	#[allow(deprecated)] // offboard_feerate kept for old clients
86	fn from(v: T) -> Self {
87		let v = v.borrow();
88	    ArkInfo {
89			network: v.network,
90			server_pubkey: v.server_pubkey,
91			mailbox_pubkey: v.mailbox_pubkey,
92			round_interval: v.round_interval,
93			nb_round_nonces: v.nb_round_nonces,
94			vtxo_exit_delta: v.vtxo_exit_delta,
95			vtxo_expiry_delta: v.vtxo_expiry_delta,
96			htlc_send_expiry_delta: v.htlc_send_expiry_delta,
97			htlc_expiry_delta: v.htlc_expiry_delta,
98			max_vtxo_amount: v.max_vtxo_amount,
99			required_board_confirmations: v.required_board_confirmations,
100			max_user_invoice_cltv_delta: v.max_user_invoice_cltv_delta,
101			min_board_amount: v.min_board_amount,
102			offboard_feerate_sat_per_kvb: v.offboard_feerate.to_sat_per_kwu() * 4,
103			ln_receive_anti_dos_required: v.ln_receive_anti_dos_required,
104			fees: v.fees.clone().into(),
105			max_vtxo_exit_depth: v.max_vtxo_exit_depth,
106		}
107	}
108}
109
110/// The different balances of a Bark wallet, broken down by state.
111///
112/// All amounts are in sats.
113#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
114#[cfg_attr(feature = "utoipa", derive(ToSchema))]
115pub struct Balance {
116	/// Sats that are immediately spendable, either in-round or
117	/// out-of-round.
118	#[serde(rename = "spendable_sat", with = "bitcoin::amount::serde::as_sat")]
119	#[cfg_attr(feature = "utoipa", schema(value_type = u64))]
120	pub spendable: Amount,
121	/// Sats locked in an outgoing Lightning payment that has not yet
122	/// settled.
123	#[serde(rename = "pending_lightning_send_sat", with = "bitcoin::amount::serde::as_sat")]
124	#[cfg_attr(feature = "utoipa", schema(value_type = u64))]
125	pub pending_lightning_send: Amount,
126	/// Sats from an incoming Lightning payment that can be claimed but
127	/// have not yet been swept into a spendable VTXO.
128	#[serde(rename = "claimable_lightning_receive_sat", with = "bitcoin::amount::serde::as_sat")]
129	#[cfg_attr(feature = "utoipa", schema(value_type = u64))]
130	pub claimable_lightning_receive: Amount,
131	/// Sats locked in VTXOs forfeited for a round that has not yet
132	/// completed.
133	#[serde(rename = "pending_in_round_sat", with = "bitcoin::amount::serde::as_sat")]
134	#[cfg_attr(feature = "utoipa", schema(value_type = u64))]
135	pub pending_in_round: Amount,
136	/// Sats in board transactions that are waiting for sufficient
137	/// on-chain confirmations before becoming spendable.
138	#[serde(rename = "pending_board_sat", with = "bitcoin::amount::serde::as_sat")]
139	#[cfg_attr(feature = "utoipa", schema(value_type = u64))]
140	pub pending_board: Amount,
141	/// Sats in VTXOs undergoing an emergency exit back on-chain.
142	/// `null` if the exit subsystem is unavailable.
143	#[serde(
144		default,
145		rename = "pending_exit_sat",
146		with = "bitcoin::amount::serde::as_sat::opt",
147		skip_serializing_if = "Option::is_none",
148	)]
149	#[cfg_attr(feature = "utoipa", schema(value_type = u64, nullable=true))]
150	pub pending_exit: Option<Amount>,
151}
152
153impl From<bark::Balance> for Balance {
154	fn from(v: bark::Balance) -> Self {
155		Balance {
156			spendable: v.spendable,
157			pending_in_round: v.pending_in_round,
158			pending_lightning_send: v.pending_lightning_send,
159			claimable_lightning_receive: v.claimable_lightning_receive,
160			pending_exit: v.pending_exit,
161			pending_board: v.pending_board,
162		}
163	}
164}
165
166#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
167#[cfg_attr(feature = "utoipa", derive(ToSchema))]
168pub struct ExitProgressResponse {
169	/// Status of each pending exit transaction
170	pub exits: Vec<ExitProgressStatus>,
171	/// Whether all transactions have been confirmed
172	pub done: bool,
173	/// Block height at which all exit outputs will be spendable
174	pub claimable_height: Option<u32>,
175	/// Top-level error that prevented progress from running cleanly this round. Per-exit
176	/// problems live on each `ExitProgressStatus`; this slot is for failures that can't
177	/// be attributed to a specific VTXO (e.g. the chain source becoming unavailable, or
178	/// the exit manager failing to refresh its view of pending transactions).
179	#[serde(default, skip_serializing_if = "Option::is_none")]
180	pub error: Option<ExitError>,
181}
182
183#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
184#[cfg_attr(feature = "utoipa", derive(ToSchema))]
185pub struct ExitProgressStatus {
186	/// The ID of the VTXO that is being unilaterally exited
187	#[cfg_attr(feature = "utoipa", schema(value_type = String))]
188	pub vtxo_id: VtxoId,
189	/// The current state of the exit transaction
190	pub state: ExitState,
191	/// Any error that occurred during the exit process
192	#[serde(default, skip_serializing_if = "Option::is_none")]
193	pub error: Option<ExitError>,
194}
195
196impl From<bark::exit::ExitProgressStatus> for ExitProgressStatus {
197	fn from(v: bark::exit::ExitProgressStatus) -> Self {
198		ExitProgressStatus {
199			vtxo_id: v.vtxo_id,
200			state: v.state.into(),
201			error: v.error.map(ExitError::from),
202		}
203	}
204}
205
206#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
207#[cfg_attr(feature = "utoipa", derive(ToSchema))]
208pub struct ExitTransactionStatus {
209	/// The ID of the VTXO that is being unilaterally exited
210	#[cfg_attr(feature = "utoipa", schema(value_type = String))]
211	pub vtxo_id: VtxoId,
212	/// The current state of the exit transaction
213	pub state: ExitState,
214	/// The history of each state the exit transaction has gone through
215	#[serde(default, skip_serializing_if = "Option::is_none")]
216	pub history: Option<Vec<ExitState>>,
217	/// Each exit transaction package required for the unilateral exit
218	#[serde(default, skip_serializing_if = "Vec::is_empty")]
219	pub transactions: Vec<ExitTransactionPackage>,
220}
221
222impl From<bark::exit::ExitTransactionStatus> for ExitTransactionStatus {
223	fn from(v: bark::exit::ExitTransactionStatus) -> Self {
224		ExitTransactionStatus {
225			vtxo_id: v.vtxo_id,
226			state: v.state.into(),
227			history: v.history.map(|h| h.into_iter().map(ExitState::from).collect()),
228			transactions: v.transactions.into_iter().map(ExitTransactionPackage::from).collect(),
229		}
230	}
231}
232
233/// Describes a completed transition of funds from onchain to offchain.
234#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
235#[cfg_attr(feature = "utoipa", derive(ToSchema))]
236pub struct PendingBoardInfo {
237	/// The funding transaction.
238	/// This is the transaction that has to be confirmed
239	/// onchain for the board to succeed.
240	pub funding_tx: TransactionInfo,
241	/// The IDs of the VTXOs that were created
242	/// in this board.
243	///
244	/// Currently, this is always a vector of length 1
245	#[cfg_attr(feature = "utoipa", schema(value_type = Vec<String>))]
246	pub vtxos: Vec<VtxoId>,
247	/// The amount of the board.
248	#[serde(rename = "amount_sat", with = "bitcoin::amount::serde::as_sat")]
249	#[cfg_attr(feature = "utoipa", schema(value_type = u64))]
250	pub amount: Amount,
251	/// The ID of the movement associated with this board.
252	pub movement_id: u32,
253}
254
255impl From<bark::persist::models::PendingBoard> for PendingBoardInfo {
256	fn from(v: bark::persist::models::PendingBoard) -> Self {
257		PendingBoardInfo {
258			funding_tx: v.funding_tx.into(),
259			vtxos: v.vtxos,
260			amount: v.amount,
261			movement_id: v.movement_id.0,
262		}
263	}
264}
265
266#[derive(Debug, Clone, Serialize, Deserialize)]
267#[serde(tag = "status", rename_all = "kebab-case")]
268#[cfg_attr(feature = "utoipa", derive(ToSchema))]
269pub enum RoundStatus {
270	/// Failed to sync round
271	SyncError {
272		error: String,
273	},
274	/// The round was successful and is fully confirmed
275	Confirmed {
276		#[cfg_attr(feature = "utoipa", schema(value_type = String))]
277		funding_txid: Txid,
278	},
279	/// Round successful but not fully confirmed
280	Unconfirmed {
281		#[cfg_attr(feature = "utoipa", schema(value_type = String))]
282		funding_txid: Txid,
283	},
284	/// We have unsigned funding transactions that might confirm
285	Pending,
286	/// The round failed
287	Failed {
288		error: String,
289	},
290	/// The round canceled
291	Canceled,
292}
293
294impl RoundStatus {
295	/// Whether this is the final state and it won't change anymore
296	pub fn is_final(&self) -> bool {
297		match self {
298			Self::SyncError { .. } => false,
299			Self::Confirmed { .. } => true,
300			Self::Unconfirmed { .. } => false,
301			Self::Pending { .. } => false,
302			Self::Failed { .. } => true,
303			Self::Canceled => true,
304		}
305	}
306
307	/// Whether it looks like the round succeeded
308	pub fn is_success(&self) -> bool {
309		match self {
310			Self::SyncError { .. } => false,
311			Self::Confirmed { .. } => true,
312			Self::Unconfirmed { .. } => true,
313			Self::Pending { .. } => false,
314			Self::Failed { .. } => false,
315			Self::Canceled => false,
316		}
317	}
318}
319
320impl From<bark::round::RoundStatus> for RoundStatus {
321	fn from(s: bark::round::RoundStatus) -> Self {
322		match s {
323			bark::round::RoundStatus::Confirmed { funding_txid } => {
324				Self::Confirmed { funding_txid }
325			},
326			bark::round::RoundStatus::Unconfirmed { funding_txid } => {
327				Self::Unconfirmed { funding_txid }
328			},
329			bark::round::RoundStatus::Pending => Self::Pending,
330			bark::round::RoundStatus::Failed { error } => Self::Failed { error },
331			bark::round::RoundStatus::Canceled => Self::Canceled,
332		}
333	}
334}
335
336#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
337#[cfg_attr(feature = "utoipa", derive(ToSchema))]
338pub struct RoundStateInfo {
339	pub round_state_id: u32,
340}
341
342#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
343#[cfg_attr(feature = "utoipa", derive(ToSchema))]
344pub struct InvoiceInfo {
345	/// The invoice string
346	pub invoice: String,
347}
348
349#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
350#[cfg_attr(feature = "utoipa", derive(ToSchema))]
351pub struct OffboardResult {
352	/// The transaction id of the offboard transaction
353	#[cfg_attr(feature = "utoipa", schema(value_type = String))]
354	pub offboard_txid: Txid,
355}
356
357#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
358#[cfg_attr(feature = "utoipa", derive(ToSchema))]
359pub struct LightningReceiveInfo {
360	/// The amount of the lightning receive
361	#[serde(rename = "amount_sat", with = "bitcoin::amount::serde::as_sat")]
362	#[cfg_attr(feature = "utoipa", schema(value_type = u64))]
363	pub amount: Amount,
364	/// The payment hash linked to the lightning receive info
365	#[cfg_attr(feature = "utoipa", schema(value_type = String))]
366	pub payment_hash: PaymentHash,
367	/// The payment preimage linked to the lightning receive info
368	#[cfg_attr(feature = "utoipa", schema(value_type = String))]
369	pub payment_preimage: Preimage,
370	/// The timestamp at which the preimage was revealed
371	pub preimage_revealed_at: Option<chrono::DateTime<chrono::Local>>,
372	/// The timestamp at which the lightning receive was finished
373	pub finished_at: Option<chrono::DateTime<chrono::Local>>,
374	/// The invoice string
375	pub invoice: String,
376	/// The HTLC VTXOs granted by the server for the lightning receive
377	///
378	/// Empty if the lightning HTLC has not yet been received by the server.
379	#[serde(default, deserialize_with = "serde_utils::null_as_default")]
380	#[cfg_attr(feature = "utoipa", schema(required = true))]
381	pub htlc_vtxos: Vec<WalletVtxoInfo>,
382}
383
384impl From<bark::persist::models::LightningReceive> for LightningReceiveInfo {
385	fn from(v: bark::persist::models::LightningReceive) -> Self {
386		LightningReceiveInfo {
387			payment_hash: v.payment_hash,
388			payment_preimage: v.payment_preimage,
389			preimage_revealed_at: v.preimage_revealed_at,
390			invoice: v.invoice.to_string(),
391			htlc_vtxos: v.htlc_vtxos.into_iter()
392				.map(crate::primitives::WalletVtxoInfo::from).collect(),
393			amount: v.invoice.amount_milli_satoshis().map(Amount::from_msat_floor)
394				.unwrap_or(Amount::ZERO),
395			finished_at: v.finished_at,
396		}
397	}
398}
399
400#[cfg(test)]
401mod test {
402	use bitcoin::FeeRate;
403	use super::*;
404
405	fn lightning_receive_base_json() -> serde_json::Value {
406		serde_json::json!({
407			"amount_sat": 1000,
408			"payment_hash": "0000000000000000000000000000000000000000000000000000000000000000",
409			"payment_preimage": "0000000000000000000000000000000000000000000000000000000000000000",
410			"preimage_revealed_at": null,
411			"finished_at": null,
412			"invoice": "lnbc1",
413		})
414	}
415
416	#[test]
417	fn deserialize_lightning_receive_htlc_vtxos_missing() {
418		let json = lightning_receive_base_json();
419		serde_json::from_value::<LightningReceiveInfo>(json).unwrap();
420	}
421
422	#[test]
423	fn deserialize_lightning_receive_htlc_vtxos_null() {
424		let mut json = lightning_receive_base_json();
425		json["htlc_vtxos"] = serde_json::json!(null);
426		serde_json::from_value::<LightningReceiveInfo>(json).unwrap();
427	}
428
429	#[test]
430	fn deserialize_lightning_receive_htlc_vtxos_empty() {
431		let mut json = lightning_receive_base_json();
432		json["htlc_vtxos"] = serde_json::json!([]);
433		serde_json::from_value::<LightningReceiveInfo>(json).unwrap();
434	}
435
436	#[test]
437	fn ark_info_fields() {
438		//! the purpose of this test is to fail if we add a field to
439		//! ark::ArkInfo but we forgot to add it to the ArkInfo here
440
441		#[allow(unused, deprecated)]
442		fn convert(j: ArkInfo) -> ark::ArkInfo {
443			ark::ArkInfo {
444				network: j.network,
445				server_pubkey: j.server_pubkey,
446				mailbox_pubkey: j.mailbox_pubkey,
447				round_interval: j.round_interval,
448				nb_round_nonces: j.nb_round_nonces,
449				vtxo_exit_delta: j.vtxo_exit_delta,
450				vtxo_expiry_delta: j.vtxo_expiry_delta,
451				htlc_send_expiry_delta: j.htlc_send_expiry_delta,
452				htlc_expiry_delta: j.htlc_expiry_delta,
453				max_vtxo_amount: j.max_vtxo_amount,
454				required_board_confirmations: j.required_board_confirmations,
455				max_user_invoice_cltv_delta: j.max_user_invoice_cltv_delta,
456				min_board_amount: j.min_board_amount,
457				offboard_feerate: FeeRate::from_sat_per_kwu(j.offboard_feerate_sat_per_kvb / 4),
458				ln_receive_anti_dos_required: j.ln_receive_anti_dos_required,
459				fees: j.fees.into(),
460				max_vtxo_exit_depth: j.max_vtxo_exit_depth,
461			}
462		}
463	}
464}
465