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 held in VTXOs whose unilateral exit chain is confirmed on-chain but which
142	/// haven't yet been drained to the onchain wallet. Equivalent to the sum of
143	/// `Exited` VTXOs whose exit state hasn't reached `Claimed`.
144	/// `null` if the exit subsystem is unavailable.
145	#[serde(
146		default,
147		rename = "pending_exit_sat",
148		with = "bitcoin::amount::serde::as_sat::opt",
149		skip_serializing_if = "Option::is_none",
150	)]
151	#[cfg_attr(feature = "utoipa", schema(value_type = u64, nullable=true))]
152	pub pending_exit: Option<Amount>,
153}
154
155impl From<bark::Balance> for Balance {
156	fn from(v: bark::Balance) -> Self {
157		Balance {
158			spendable: v.spendable,
159			pending_in_round: v.pending_in_round,
160			pending_lightning_send: v.pending_lightning_send,
161			claimable_lightning_receive: v.claimable_lightning_receive,
162			pending_exit: v.pending_exit,
163			pending_board: v.pending_board,
164		}
165	}
166}
167
168#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
169#[cfg_attr(feature = "utoipa", derive(ToSchema))]
170pub struct ExitProgressResponse {
171	/// Status of each pending exit transaction
172	pub exits: Vec<ExitProgressStatus>,
173	/// Whether all transactions have been confirmed
174	pub done: bool,
175	/// Block height at which all exit outputs will be spendable
176	pub claimable_height: Option<u32>,
177	/// Top-level error that prevented progress from running cleanly this round. Per-exit
178	/// problems live on each `ExitProgressStatus`; this slot is for failures that can't
179	/// be attributed to a specific VTXO (e.g. the chain source becoming unavailable, or
180	/// the exit manager failing to refresh its view of pending transactions).
181	#[serde(default, skip_serializing_if = "Option::is_none")]
182	pub error: Option<ExitError>,
183}
184
185#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
186#[cfg_attr(feature = "utoipa", derive(ToSchema))]
187pub struct ExitProgressStatus {
188	/// The ID of the VTXO that is being unilaterally exited
189	#[cfg_attr(feature = "utoipa", schema(value_type = String))]
190	pub vtxo_id: VtxoId,
191	/// The current state of the exit transaction
192	pub state: ExitState,
193	/// Any error that occurred during the exit process
194	#[serde(default, skip_serializing_if = "Option::is_none")]
195	pub error: Option<ExitError>,
196}
197
198impl From<bark::exit::ExitProgressStatus> for ExitProgressStatus {
199	fn from(v: bark::exit::ExitProgressStatus) -> Self {
200		ExitProgressStatus {
201			vtxo_id: v.vtxo_id,
202			state: v.state.into(),
203			error: v.error.map(ExitError::from),
204		}
205	}
206}
207
208#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
209#[cfg_attr(feature = "utoipa", derive(ToSchema))]
210pub struct ExitTransactionStatus {
211	/// The ID of the VTXO that is being unilaterally exited
212	#[cfg_attr(feature = "utoipa", schema(value_type = String))]
213	pub vtxo_id: VtxoId,
214	/// The current state of the exit transaction
215	pub state: ExitState,
216	/// The history of each state the exit transaction has gone through
217	#[serde(default, skip_serializing_if = "Option::is_none")]
218	pub history: Option<Vec<ExitState>>,
219	/// Each exit transaction package required for the unilateral exit
220	#[serde(default, skip_serializing_if = "Vec::is_empty")]
221	pub transactions: Vec<ExitTransactionPackage>,
222}
223
224impl From<bark::exit::ExitTransactionStatus> for ExitTransactionStatus {
225	fn from(v: bark::exit::ExitTransactionStatus) -> Self {
226		ExitTransactionStatus {
227			vtxo_id: v.vtxo_id,
228			state: v.state.into(),
229			history: v.history.map(|h| h.into_iter().map(ExitState::from).collect()),
230			transactions: v.transactions.into_iter().map(ExitTransactionPackage::from).collect(),
231		}
232	}
233}
234
235/// Describes a completed transition of funds from onchain to offchain.
236#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
237#[cfg_attr(feature = "utoipa", derive(ToSchema))]
238pub struct PendingBoardInfo {
239	/// The funding transaction.
240	/// This is the transaction that has to be confirmed
241	/// onchain for the board to succeed.
242	pub funding_tx: TransactionInfo,
243	/// The IDs of the VTXOs that were created
244	/// in this board.
245	///
246	/// Currently, this is always a vector of length 1
247	#[cfg_attr(feature = "utoipa", schema(value_type = Vec<String>))]
248	pub vtxos: Vec<VtxoId>,
249	/// The amount of the board.
250	#[serde(rename = "amount_sat", with = "bitcoin::amount::serde::as_sat")]
251	#[cfg_attr(feature = "utoipa", schema(value_type = u64))]
252	pub amount: Amount,
253	/// The ID of the movement associated with this board.
254	pub movement_id: u32,
255}
256
257impl From<bark::persist::models::PendingBoard> for PendingBoardInfo {
258	fn from(v: bark::persist::models::PendingBoard) -> Self {
259		PendingBoardInfo {
260			funding_tx: v.funding_tx.into(),
261			vtxos: v.vtxos,
262			amount: v.amount,
263			movement_id: v.movement_id.0,
264		}
265	}
266}
267
268#[derive(Debug, Clone, Serialize, Deserialize)]
269#[serde(tag = "status", rename_all = "kebab-case")]
270#[cfg_attr(feature = "utoipa", derive(ToSchema))]
271pub enum RoundStatus {
272	/// Failed to sync round
273	SyncError {
274		error: String,
275	},
276	/// The round was successful and is fully confirmed
277	Confirmed {
278		#[cfg_attr(feature = "utoipa", schema(value_type = String))]
279		funding_txid: Txid,
280	},
281	/// Round successful but not fully confirmed
282	Unconfirmed {
283		#[cfg_attr(feature = "utoipa", schema(value_type = String))]
284		funding_txid: Txid,
285	},
286	/// We have unsigned funding transactions that might confirm
287	Pending,
288	/// The round failed
289	Failed {
290		error: String,
291	},
292	/// The round canceled
293	Canceled,
294}
295
296impl RoundStatus {
297	/// Whether this is the final state and it won't change anymore
298	pub fn is_final(&self) -> bool {
299		match self {
300			Self::SyncError { .. } => false,
301			Self::Confirmed { .. } => true,
302			Self::Unconfirmed { .. } => false,
303			Self::Pending { .. } => false,
304			Self::Failed { .. } => true,
305			Self::Canceled => true,
306		}
307	}
308
309	/// Whether it looks like the round succeeded
310	pub fn is_success(&self) -> bool {
311		match self {
312			Self::SyncError { .. } => false,
313			Self::Confirmed { .. } => true,
314			Self::Unconfirmed { .. } => true,
315			Self::Pending { .. } => false,
316			Self::Failed { .. } => false,
317			Self::Canceled => false,
318		}
319	}
320}
321
322impl From<bark::round::RoundStatus> for RoundStatus {
323	fn from(s: bark::round::RoundStatus) -> Self {
324		match s {
325			bark::round::RoundStatus::Confirmed { funding_txid } => {
326				Self::Confirmed { funding_txid }
327			},
328			bark::round::RoundStatus::Unconfirmed { funding_txid } => {
329				Self::Unconfirmed { funding_txid }
330			},
331			bark::round::RoundStatus::Pending => Self::Pending,
332			bark::round::RoundStatus::Failed { error } => Self::Failed { error },
333			bark::round::RoundStatus::Canceled => Self::Canceled,
334		}
335	}
336}
337
338#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
339#[cfg_attr(feature = "utoipa", derive(ToSchema))]
340pub struct RoundStateInfo {
341	pub round_state_id: u32,
342}
343
344#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
345#[cfg_attr(feature = "utoipa", derive(ToSchema))]
346pub struct InvoiceInfo {
347	/// The invoice string
348	pub invoice: String,
349}
350
351#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
352#[cfg_attr(feature = "utoipa", derive(ToSchema))]
353pub struct OffboardResult {
354	/// The transaction id of the offboard transaction
355	#[cfg_attr(feature = "utoipa", schema(value_type = String))]
356	pub offboard_txid: Txid,
357}
358
359#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
360#[cfg_attr(feature = "utoipa", derive(ToSchema))]
361pub struct LightningReceiveInfo {
362	/// The amount of the lightning receive
363	#[serde(rename = "amount_sat", with = "bitcoin::amount::serde::as_sat")]
364	#[cfg_attr(feature = "utoipa", schema(value_type = u64))]
365	pub amount: Amount,
366	/// The payment hash linked to the lightning receive info
367	#[cfg_attr(feature = "utoipa", schema(value_type = String))]
368	pub payment_hash: PaymentHash,
369	/// The payment preimage linked to the lightning receive info
370	#[cfg_attr(feature = "utoipa", schema(value_type = String))]
371	pub payment_preimage: Preimage,
372	/// The timestamp at which the preimage was revealed
373	pub preimage_revealed_at: Option<chrono::DateTime<chrono::Local>>,
374	/// The timestamp at which the lightning receive was finished
375	pub finished_at: Option<chrono::DateTime<chrono::Local>>,
376	/// The invoice string
377	pub invoice: String,
378	/// The HTLC VTXOs granted by the server for the lightning receive
379	///
380	/// Empty if the lightning HTLC has not yet been received by the server.
381	#[serde(default, deserialize_with = "serde_utils::null_as_default")]
382	#[cfg_attr(feature = "utoipa", schema(required = true))]
383	pub htlc_vtxos: Vec<WalletVtxoInfo>,
384}
385
386impl From<bark::persist::models::LightningReceive> for LightningReceiveInfo {
387	fn from(v: bark::persist::models::LightningReceive) -> Self {
388		LightningReceiveInfo {
389			payment_hash: v.payment_hash,
390			payment_preimage: v.payment_preimage,
391			preimage_revealed_at: v.preimage_revealed_at,
392			invoice: v.invoice.to_string(),
393			htlc_vtxos: v.htlc_vtxos.iter()
394				.map(crate::primitives::WalletVtxoInfo::from)
395				.collect(),
396			amount: v.invoice.amount_milli_satoshis().map(Amount::from_msat_floor)
397				.unwrap_or(Amount::ZERO),
398			finished_at: v.finished_at,
399		}
400	}
401}
402
403#[cfg(test)]
404mod test {
405	use bitcoin::FeeRate;
406	use super::*;
407
408	fn lightning_receive_base_json() -> serde_json::Value {
409		serde_json::json!({
410			"amount_sat": 1000,
411			"payment_hash": "0000000000000000000000000000000000000000000000000000000000000000",
412			"payment_preimage": "0000000000000000000000000000000000000000000000000000000000000000",
413			"preimage_revealed_at": null,
414			"finished_at": null,
415			"invoice": "lnbc1",
416		})
417	}
418
419	#[test]
420	fn deserialize_lightning_receive_htlc_vtxos_missing() {
421		let json = lightning_receive_base_json();
422		serde_json::from_value::<LightningReceiveInfo>(json).unwrap();
423	}
424
425	#[test]
426	fn deserialize_lightning_receive_htlc_vtxos_null() {
427		let mut json = lightning_receive_base_json();
428		json["htlc_vtxos"] = serde_json::json!(null);
429		serde_json::from_value::<LightningReceiveInfo>(json).unwrap();
430	}
431
432	#[test]
433	fn deserialize_lightning_receive_htlc_vtxos_empty() {
434		let mut json = lightning_receive_base_json();
435		json["htlc_vtxos"] = serde_json::json!([]);
436		serde_json::from_value::<LightningReceiveInfo>(json).unwrap();
437	}
438
439	#[test]
440	fn ark_info_fields() {
441		//! the purpose of this test is to fail if we add a field to
442		//! ark::ArkInfo but we forgot to add it to the ArkInfo here
443
444		#[allow(unused, deprecated)]
445		fn convert(j: ArkInfo) -> ark::ArkInfo {
446			ark::ArkInfo {
447				network: j.network,
448				server_pubkey: j.server_pubkey,
449				mailbox_pubkey: j.mailbox_pubkey,
450				round_interval: j.round_interval,
451				nb_round_nonces: j.nb_round_nonces,
452				vtxo_exit_delta: j.vtxo_exit_delta,
453				vtxo_expiry_delta: j.vtxo_expiry_delta,
454				htlc_send_expiry_delta: j.htlc_send_expiry_delta,
455				htlc_expiry_delta: j.htlc_expiry_delta,
456				max_vtxo_amount: j.max_vtxo_amount,
457				required_board_confirmations: j.required_board_confirmations,
458				max_user_invoice_cltv_delta: j.max_user_invoice_cltv_delta,
459				min_board_amount: j.min_board_amount,
460				offboard_feerate: FeeRate::from_sat_per_kwu(j.offboard_feerate_sat_per_kvb / 4),
461				ln_receive_anti_dos_required: j.ln_receive_anti_dos_required,
462				fees: j.fees.into(),
463				max_vtxo_exit_depth: j.max_vtxo_exit_depth,
464			}
465		}
466	}
467}
468