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