bark_json/cli/
mod.rs

1#[cfg(feature = "onchain_bdk")]
2pub mod onchain;
3
4use std::borrow::Borrow;
5use std::time::Duration;
6
7use bitcoin::secp256k1::PublicKey;
8use bitcoin::{Amount, FeeRate, Txid, SignedAmount};
9use chrono::DateTime;
10#[cfg(feature = "utoipa")]
11use utoipa::ToSchema;
12
13use ark::lightning::{PaymentHash, Preimage};
14use ark::VtxoId;
15use bark::movement::{MovementId, MovementStatus};
16use bitcoin_ext::{BlockDelta, BlockHeight};
17
18use crate::exit::error::ExitError;
19use crate::exit::package::ExitTransactionPackage;
20use crate::exit::ExitState;
21use crate::primitives::{VtxoInfo, 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 interval between each round
34	#[serde(with = "serde_utils::duration")]
35	#[cfg_attr(feature = "utoipa", schema(value_type = String))]
36	pub round_interval: Duration,
37	/// Number of nonces per round
38	pub nb_round_nonces: usize,
39	/// Delta between exit confirmation and coins becoming spendable
40	pub vtxo_exit_delta: BlockDelta,
41	/// Expiration delta of the VTXO
42	pub vtxo_expiry_delta: BlockDelta,
43	/// The number of blocks after which an HTLC-send VTXO expires once granted.
44	pub htlc_send_expiry_delta: BlockDelta,
45	/// The number of blocks to keep between Lightning and Ark HTLCs expiries
46	pub htlc_expiry_delta: BlockDelta,
47	/// Maximum amount of a VTXO
48	#[cfg_attr(feature = "utoipa", schema(value_type = u64))]
49	pub max_vtxo_amount: Option<Amount>,
50	/// Maximum number of OOR transition after VTXO tree leaf
51	pub max_arkoor_depth: u16,
52	/// The number of confirmations required to register a board vtxo
53	pub required_board_confirmations: usize,
54	/// Maximum CLTV delta server will allow clients to request an
55	/// invoice generation with.
56	pub max_user_invoice_cltv_delta: u16,
57	/// Minimum amount for a board the server will cosign
58	#[serde(rename = "min_board_amount_sat", with = "bitcoin::amount::serde::as_sat")]
59	#[cfg_attr(feature = "utoipa", schema(value_type = u64))]
60	pub min_board_amount: Amount,
61	/// offboard feerate in sat per kvb
62	pub offboard_feerate_sat_per_kvb: u64,
63	/// Indicates whether the Ark server requires clients to either
64	/// provide a VTXO ownership proof, or a lightning receive token
65	/// when preparing a lightning claim.
66	pub ln_receive_anti_dos_required: bool,
67}
68
69impl<T: Borrow<ark::ArkInfo>> From<T> for ArkInfo {
70	fn from(v: T) -> Self {
71		let v = v.borrow();
72	    ArkInfo {
73			network: v.network,
74			server_pubkey: v.server_pubkey,
75			round_interval: v.round_interval,
76			nb_round_nonces: v.nb_round_nonces,
77			vtxo_exit_delta: v.vtxo_exit_delta,
78			vtxo_expiry_delta: v.vtxo_expiry_delta,
79			htlc_send_expiry_delta: v.htlc_send_expiry_delta,
80			htlc_expiry_delta: v.htlc_expiry_delta,
81			max_vtxo_amount: v.max_vtxo_amount,
82			max_arkoor_depth: v.max_arkoor_depth,
83			required_board_confirmations: v.required_board_confirmations,
84			max_user_invoice_cltv_delta: v.max_user_invoice_cltv_delta,
85			min_board_amount: v.min_board_amount,
86			offboard_feerate_sat_per_kvb: v.offboard_feerate.to_sat_per_kwu() * 4,
87			ln_receive_anti_dos_required: v.ln_receive_anti_dos_required,
88		}
89	}
90}
91
92#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
93#[cfg_attr(feature = "utoipa", derive(ToSchema))]
94pub struct LightningReceiveBalance {
95	#[serde(rename = "total_sat", with = "bitcoin::amount::serde::as_sat")]
96	#[cfg_attr(feature = "utoipa", schema(value_type = u64))]
97	pub total: Amount,
98	#[serde(rename = "claimable_sat", with = "bitcoin::amount::serde::as_sat")]
99	#[cfg_attr(feature = "utoipa", schema(value_type = u64))]
100	pub claimable: Amount,
101}
102
103impl From<bark::LightningReceiveBalance> for LightningReceiveBalance {
104	fn from(v: bark::LightningReceiveBalance) -> Self {
105		LightningReceiveBalance {
106			total: v.total,
107			claimable: v.claimable,
108		}
109	}
110}
111
112#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
113#[cfg_attr(feature = "utoipa", derive(ToSchema))]
114pub struct Balance {
115	#[serde(rename = "spendable_sat", with = "bitcoin::amount::serde::as_sat")]
116	#[cfg_attr(feature = "utoipa", schema(value_type = u64))]
117	pub spendable: Amount,
118	#[serde(rename = "pending_lightning_send_sat", with = "bitcoin::amount::serde::as_sat")]
119	#[cfg_attr(feature = "utoipa", schema(value_type = u64))]
120	pub pending_lightning_send: Amount,
121	pub pending_lightning_receive: LightningReceiveBalance,
122	#[serde(rename = "pending_in_round_sat", with = "bitcoin::amount::serde::as_sat")]
123	#[cfg_attr(feature = "utoipa", schema(value_type = u64))]
124	pub pending_in_round: Amount,
125	#[serde(rename = "pending_board_sat", with = "bitcoin::amount::serde::as_sat")]
126	#[cfg_attr(feature = "utoipa", schema(value_type = u64))]
127	pub pending_board: Amount,
128	#[serde(
129		default,
130		rename = "pending_exit_sat",
131		with = "bitcoin::amount::serde::as_sat::opt",
132		skip_serializing_if = "Option::is_none",
133	)]
134	#[cfg_attr(feature = "utoipa", schema(value_type = u64, nullable=true))]
135	pub pending_exit: Option<Amount>,
136}
137
138impl From<bark::Balance> for Balance {
139	fn from(v: bark::Balance) -> Self {
140		Balance {
141			spendable: v.spendable,
142			pending_in_round: v.pending_in_round,
143			pending_lightning_send: v.pending_lightning_send,
144			pending_lightning_receive: v.pending_lightning_receive.into(),
145			pending_exit: v.pending_exit,
146			pending_board: v.pending_board,
147		}
148	}
149}
150
151#[derive(Debug, Clone, Deserialize, Serialize)]
152#[cfg_attr(feature = "utoipa", derive(ToSchema))]
153pub struct Config {
154	/// Ark server address
155	pub ark: String,
156	/// Bitcoin Core RPC address to use for syncing
157	pub bitcoind: Option<String>,
158	/// Cookie to use for RPC authentication
159	pub bitcoind_cookie: Option<String>,
160	/// Username to use for RPC authentication
161	pub bitcoind_user: Option<String>,
162	/// password to use for RPC authentication
163	pub bitcoind_pass: Option<String>,
164	/// The Esplora REST API address to use for syncing
165	pub esplora: Option<String>,
166	/// How many blocks before VTXO expiration before preemptively refreshing them
167	pub vtxo_refresh_expiry_threshold: BlockHeight,
168	#[serde(rename = "fallback_fee_rate_kvb", with = "serde_utils::fee_rate_sats_per_kvb")]
169	#[cfg_attr(feature = "utoipa", schema(value_type = u64, nullable = true))]
170	pub fallback_fee_rate: Option<FeeRate>,
171}
172
173#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
174#[cfg_attr(feature = "utoipa", derive(ToSchema))]
175pub struct ExitProgressResponse {
176	/// Status of each pending exit transaction
177	pub exits: Vec<ExitProgressStatus>,
178	/// Whether all transactions have been confirmed
179	pub done: bool,
180	/// Block height at which all exit outputs will be spendable
181	pub claimable_height: Option<u32>,
182}
183
184#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
185#[cfg_attr(feature = "utoipa", derive(ToSchema))]
186pub struct ExitProgressStatus {
187	/// The ID of the VTXO that is being unilaterally exited
188	#[cfg_attr(feature = "utoipa", schema(value_type = String))]
189	pub vtxo_id: VtxoId,
190	/// The current state of the exit transaction
191	pub state: ExitState,
192	/// Any error that occurred during the exit process
193	#[serde(default, skip_serializing_if = "Option::is_none")]
194	pub error: Option<ExitError>,
195}
196
197impl From<bark::exit::models::ExitProgressStatus> for ExitProgressStatus {
198	fn from(v: bark::exit::models::ExitProgressStatus) -> Self {
199		ExitProgressStatus {
200			vtxo_id: v.vtxo_id,
201			state: v.state.into(),
202			error: v.error.map(ExitError::from),
203		}
204	}
205}
206
207#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
208#[cfg_attr(feature = "utoipa", derive(ToSchema))]
209pub struct ExitTransactionStatus {
210	/// The ID of the VTXO that is being unilaterally exited
211	#[cfg_attr(feature = "utoipa", schema(value_type = String))]
212	pub vtxo_id: VtxoId,
213	/// The current state of the exit transaction
214	pub state: ExitState,
215	/// The history of each state the exit transaction has gone through
216	#[serde(default, skip_serializing_if = "Option::is_none")]
217	pub history: Option<Vec<ExitState>>,
218	/// Each exit transaction package required for the unilateral exit
219	#[serde(default, skip_serializing_if = "Vec::is_empty")]
220	pub transactions: Vec<ExitTransactionPackage>,
221}
222
223impl From<bark::exit::models::ExitTransactionStatus> for ExitTransactionStatus {
224	fn from(v: bark::exit::models::ExitTransactionStatus) -> Self {
225		ExitTransactionStatus {
226			vtxo_id: v.vtxo_id,
227			state: v.state.into(),
228			history: v.history.map(|h| h.into_iter().map(ExitState::from).collect()),
229			transactions: v.transactions.into_iter().map(ExitTransactionPackage::from).collect(),
230		}
231	}
232}
233
234/// Describes a completed transition of funds from onchain to offchain.
235#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
236#[cfg_attr(feature = "utoipa", derive(ToSchema))]
237pub struct Board {
238	/// The [Txid] of the funding-transaction.
239	/// This is the transaction that has to be confirmed
240	/// onchain for the board to succeed.
241	#[cfg_attr(feature = "utoipa", schema(value_type = String))]
242	pub funding_txid: Txid,
243	/// The info for each [ark::Vtxo] that was created
244	/// in this board.
245	///
246	/// Currently, this is always a vector of length 1
247	pub vtxos: Vec<VtxoInfo>,
248}
249
250impl From<bark::Board> for Board {
251	fn from(v: bark::Board) -> Self {
252		Board {
253			funding_txid: v.funding_txid,
254			vtxos: v.vtxos.into_iter().map(VtxoInfo::from).collect(),
255		}
256	}
257}
258
259/// Describes an attempted movement of offchain funds within the Bark [Wallet].
260#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
261#[cfg_attr(feature = "utoipa", derive(ToSchema))]
262pub struct Movement {
263	/// The internal ID of the movement.
264	#[cfg_attr(feature = "utoipa", schema(value_type = u32))]
265	pub id: MovementId,
266	/// The status of the movement.
267	#[cfg_attr(feature = "utoipa", schema(value_type = String))]
268	pub status: MovementStatus,
269	/// Contains information about the subsystem that created the movement as well as the purpose
270	/// of the movement.
271	pub subsystem: MovementSubsystem,
272	/// Miscellaneous metadata for the movement. This is JSON containing arbitrary information as
273	/// defined by the subsystem that created the movement.
274	#[serde(default, skip_serializing_if = "Option::is_none")]
275	pub metadata: Option<String>,
276	/// How much the movement was expected to increase or decrease the balance by. This is always an
277	/// estimate and often discounts any applicable fees.
278	#[serde(rename="intended_balance_sat", with="bitcoin::amount::serde::as_sat")]
279	#[cfg_attr(feature = "utoipa", schema(value_type = i64))]
280	pub intended_balance: SignedAmount,
281	/// How much the wallet balance actually changed by. Positive numbers indicate an increase and
282	/// negative numbers indicate a decrease. This is often inclusive of applicable fees, and it
283	/// should be the most accurate number.
284	#[serde(rename="effective_balance_sat", with="bitcoin::amount::serde::as_sat")]
285	#[cfg_attr(feature = "utoipa", schema(value_type = i64))]
286	pub effective_balance: SignedAmount,
287	/// How much the movement cost the user in offchain fees. If there are applicable onchain fees
288	/// they will not be included in this value but, depending on the subsystem, could be found in
289	/// the metadata.
290	#[serde(rename="offchain_fee_sat", with="bitcoin::amount::serde::as_sat")]
291	#[cfg_attr(feature = "utoipa", schema(value_type = u64))]
292	pub offchain_fee: Amount,
293	/// A list of external recipients that received funds from this movement.
294	pub sent_to: Vec<MovementDestination>,
295	/// Describes the means by which the wallet received funds in this movement. This could include
296	/// BOLT11 invoices or other useful data.
297	pub received_on: Vec<MovementDestination>,
298	/// A list of [Vtxo] IDs that were consumed by this movement and are either locked or
299	/// unavailable.
300	#[cfg_attr(feature = "utoipa", schema(value_type = Vec<String>))]
301	pub input_vtxos: Vec<VtxoId>,
302	/// A list of IDs for new VTXOs that were produced as a result of this movement. Often change
303	/// VTXOs will be found here for outbound actions unless this was an inbound action.
304	#[cfg_attr(feature = "utoipa", schema(value_type = Vec<String>))]
305	pub output_vtxos: Vec<VtxoId>,
306	/// A list of IDs for VTXOs that were marked for unilateral exit as a result of this movement.
307	/// This could happen for many reasons, e.g. an unsuccessful lightning payment which can't be
308	/// revoked but is about to expire. VTXOs listed here will result in a reduction of spendable
309	/// balance due to the VTXOs being managed by the [bark::Exit] system.
310	#[cfg_attr(feature = "utoipa", schema(value_type = Vec<String>))]
311	pub exited_vtxos: Vec<VtxoId>,
312	/// Contains the times at which the movement was created, updated and completed.
313	pub time: MovementTimestamp,
314}
315
316impl TryFrom<bark::movement::Movement> for Movement {
317	type Error = serde_json::Error;
318
319	fn try_from(m: bark::movement::Movement) -> Result<Self, Self::Error> {
320		Ok(Movement {
321			id: m.id,
322			status: m.status,
323			subsystem: MovementSubsystem::from(m.subsystem),
324			metadata: if m.metadata.is_empty() { None } else {
325				Some(serde_json::to_string(&m.metadata)?)
326			},
327			intended_balance: m.intended_balance,
328			effective_balance: m.effective_balance,
329			offchain_fee: m.offchain_fee,
330			sent_to: m.sent_to.into_iter().map(MovementDestination::from).collect(),
331			received_on: m.received_on.into_iter().map(MovementDestination::from).collect(),
332			input_vtxos: m.input_vtxos,
333			output_vtxos: m.output_vtxos,
334			exited_vtxos: m.exited_vtxos,
335			time: MovementTimestamp::from(m.time),
336		})
337	}
338}
339
340/// Describes a recipient of a movement. This could either be an external recipient in send actions
341/// or it could be the bark wallet itself.
342#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
343#[cfg_attr(feature = "utoipa", derive(ToSchema))]
344pub struct MovementDestination {
345	/// An address, invoice or any other identifier to distinguish the recipient.
346	pub destination: String,
347	/// How many sats the recipient received.
348	#[serde(rename="amount_sat", with="bitcoin::amount::serde::as_sat")]
349	#[cfg_attr(feature = "utoipa", schema(value_type = u64))]
350	pub amount: Amount,
351}
352
353impl From<bark::movement::MovementDestination> for MovementDestination {
354	fn from(d: bark::movement::MovementDestination) -> Self {
355		MovementDestination {
356			destination: d.destination,
357			amount: d.amount,
358		}
359	}
360}
361
362/// Contains information about the subsystem that created the movement as well as the purpose
363/// of the movement.
364#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
365#[cfg_attr(feature = "utoipa", derive(ToSchema))]
366pub struct MovementSubsystem {
367	/// The name of the subsystem that created and manages the movement.
368	pub name: String,
369	/// The action responsible for registering the movement.
370	pub kind: String,
371}
372
373impl From<bark::movement::MovementSubsystem> for MovementSubsystem {
374	fn from(s: bark::movement::MovementSubsystem) -> Self {
375		MovementSubsystem {
376			name: s.name,
377			kind: s.kind,
378		}
379	}
380}
381
382/// Contains the times at which the movement was created, updated and completed.
383#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
384#[cfg_attr(feature = "utoipa", derive(ToSchema))]
385pub struct MovementTimestamp {
386	/// When the movement was first created.
387	pub created_at: DateTime<chrono::Utc>,
388	/// When the movement was last updated.
389	pub updated_at: DateTime<chrono::Utc>,
390	/// The action responsible for registering the movement.
391	#[serde(default, skip_serializing_if = "Option::is_none")]
392	pub completed_at: Option<DateTime<chrono::Utc>>,
393}
394
395impl From<bark::movement::MovementTimestamp> for MovementTimestamp {
396	fn from(t: bark::movement::MovementTimestamp) -> Self {
397		MovementTimestamp {
398			created_at: t.created_at,
399			updated_at: t.updated_at,
400			completed_at: t.completed_at,
401		}
402	}
403}
404
405#[derive(Debug, Clone, Serialize, Deserialize)]
406#[serde(tag = "result", rename_all = "lowercase")]
407#[cfg_attr(feature = "utoipa", derive(ToSchema))]
408pub enum RoundStatus {
409	/// The round was successful and is fully confirmed
410	Confirmed {
411		#[cfg_attr(feature = "utoipa", schema(value_type = String))]
412		funding_txid: Txid,
413	},
414	/// Round successful but not fully confirmed
415	Unconfirmed {
416		#[cfg_attr(feature = "utoipa", schema(value_type = String))]
417		funding_txid: Txid,
418	},
419	/// We have unsigned funding transactions that might confirm
420	Pending {
421		#[cfg_attr(feature = "utoipa", schema(value_type = Vec<String>))]
422		unsigned_funding_txids: Vec<Txid>,
423	},
424	/// The round failed
425	Failed {
426		error: String,
427	},
428}
429
430impl RoundStatus {
431	/// Whether this is the final state and it won't change anymore
432	pub fn is_final(&self) -> bool {
433		match self {
434			Self::Confirmed { .. } => true,
435			Self::Unconfirmed { .. } => false,
436			Self::Pending { .. } => false,
437			Self::Failed { .. } => true,
438		}
439	}
440
441	/// Whether it looks like the round succeeded
442	pub fn is_success(&self) -> bool {
443		match self {
444			Self::Confirmed { .. } => true,
445			Self::Unconfirmed { .. } => true,
446			Self::Pending { .. } => false,
447			Self::Failed { .. } => false,
448		}
449	}
450}
451
452impl From<bark::round::RoundStatus> for RoundStatus {
453	fn from(s: bark::round::RoundStatus) -> Self {
454		match s {
455			bark::round::RoundStatus::Confirmed { funding_txid } => Self::Confirmed { funding_txid },
456			bark::round::RoundStatus::Unconfirmed { funding_txid } => Self::Unconfirmed { funding_txid },
457			bark::round::RoundStatus::Pending { unsigned_funding_txids } => Self::Pending { unsigned_funding_txids },
458			bark::round::RoundStatus::Failed { error } => Self::Failed { error },
459		}
460	}
461}
462
463#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
464#[cfg_attr(feature = "utoipa", derive(ToSchema))]
465pub struct InvoiceInfo {
466	/// The invoice string
467	pub invoice: String,
468}
469
470#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
471#[cfg_attr(feature = "utoipa", derive(ToSchema))]
472pub struct LightningReceiveInfo {
473	/// The payment hash linked to the lightning receive info
474	#[cfg_attr(feature = "utoipa", schema(value_type = String))]
475	pub payment_hash: PaymentHash,
476	/// The payment preimage linked to the lightning receive info
477	#[cfg_attr(feature = "utoipa", schema(value_type = String))]
478	pub payment_preimage: Preimage,
479	/// The timestamp at which the preimage was revealed
480	pub preimage_revealed_at: Option<chrono::DateTime<chrono::Utc>>,
481	/// The invoice string
482	pub invoice: String,
483	/// The HTLC VTXOs granted by the server for the lightning receive
484	///
485	/// Only present if the lightning HTLC has been received by the server.
486	#[cfg_attr(feature = "utoipa", schema(value_type = Vec<WalletVtxoInfo>, nullable = true))]
487	pub htlc_vtxos: Option<Vec<WalletVtxoInfo>>,
488}
489
490impl From<bark::persist::models::LightningReceive> for LightningReceiveInfo {
491	fn from(v: bark::persist::models::LightningReceive) -> Self {
492		LightningReceiveInfo {
493			payment_hash: v.payment_hash,
494			payment_preimage: v.payment_preimage,
495			preimage_revealed_at: v.preimage_revealed_at.map(|ts| {
496				chrono::DateTime::from_timestamp_secs(ts as i64)
497					.expect("timestamp is valid")
498			}),
499			invoice: v.invoice.to_string(),
500			htlc_vtxos: v.htlc_vtxos.map(|vtxos| vtxos.into_iter()
501				.map(crate::primitives::WalletVtxoInfo::from).collect()),
502		}
503	}
504}
505
506#[cfg(test)]
507mod test {
508	use super::*;
509
510	#[test]
511	fn ark_info_fields() {
512		//! the purpose of this test is to fail if we add a field to
513		//! ark::ArkInfo but we forgot to add it to the ArkInfo here
514
515		#[allow(unused)]
516		fn convert(j: ArkInfo) -> ark::ArkInfo {
517			ark::ArkInfo {
518				network: j.network,
519				server_pubkey: j.server_pubkey,
520				round_interval: j.round_interval,
521				nb_round_nonces: j.nb_round_nonces,
522				vtxo_exit_delta: j.vtxo_exit_delta,
523				vtxo_expiry_delta: j.vtxo_expiry_delta,
524				htlc_send_expiry_delta: j.htlc_send_expiry_delta,
525				htlc_expiry_delta: j.htlc_expiry_delta,
526				max_vtxo_amount: j.max_vtxo_amount,
527				max_arkoor_depth: j.max_arkoor_depth,
528				required_board_confirmations: j.required_board_confirmations,
529				max_user_invoice_cltv_delta: j.max_user_invoice_cltv_delta,
530				min_board_amount: j.min_board_amount,
531				offboard_feerate: FeeRate::from_sat_per_kwu(j.offboard_feerate_sat_per_kvb / 4),
532				ln_receive_anti_dos_required: j.ln_receive_anti_dos_required,
533			}
534		}
535	}
536}
537