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