Skip to main content

bark/movement/
mod.rs

1
2
3pub mod error;
4pub mod manager;
5pub mod update;
6mod payment_method;
7
8use crate::subsystem::{LightningMovement, Subsystem};
9
10pub use self::payment_method::PaymentMethod;
11
12use std::fmt;
13use std::str::FromStr;
14
15use bitcoin::{Amount, SignedAmount};
16use chrono::DateTime;
17use lightning::offers::offer::Offer;
18use lnurllib::lightning_address::LightningAddress;
19use serde::{Deserialize, Serialize};
20
21use ark::VtxoId;
22use ark::lightning::{Invoice, PaymentHash};
23
24const MOVEMENT_PENDING: &'static str = "pending";
25const MOVEMENT_SUCCESSFUL: &'static str = "successful";
26const MOVEMENT_FAILED: &'static str = "failed";
27const MOVEMENT_CANCELED: &'static str = "canceled";
28
29/// Describes an attempted movement of offchain funds within the [Wallet](crate::Wallet).
30#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
31pub struct Movement {
32	/// The internal ID of the movement.
33	pub id: MovementId,
34	/// The status of the movement.
35	pub status: MovementStatus,
36	/// Contains information about the subsystem that created the movement as well as the purpose
37	/// of the movement.
38	pub subsystem: MovementSubsystem,
39	/// Miscellaneous metadata for the movement. This is JSON containing arbitrary information as
40	/// defined by the subsystem that created the movement.
41	pub metadata: serde_json::Map<String, serde_json::Value>,
42	/// How much the movement was expected to increase or decrease the balance by. This is always an
43	/// estimate and often discounts any applicable fees.
44	#[serde(with = "bitcoin::amount::serde::as_sat")]
45	pub intended_balance: SignedAmount,
46	/// How much the wallet balance actually changed by. Positive numbers indicate an increase and
47	/// negative numbers indicate a decrease. This is often inclusive of applicable fees, and it
48	/// should be the most accurate number.
49	#[serde(with = "bitcoin::amount::serde::as_sat")]
50	pub effective_balance: SignedAmount,
51	/// How much the movement cost the user in offchain fees. If there are applicable onchain fees
52	/// they will not be included in this value but, depending on the subsystem, could be found in
53	/// the metadata.
54	#[serde(with = "bitcoin::amount::serde::as_sat")]
55	pub offchain_fee: Amount,
56	/// A list of external recipients that received funds from this movement.
57	pub sent_to: Vec<MovementDestination>,
58	/// Describes the means by which the wallet received funds in this movement. This could include
59	/// BOLT11 invoices or other useful data.
60	pub received_on: Vec<MovementDestination>,
61	/// A list of [Vtxo](ark::Vtxo) IDs that were consumed by this movement and are either locked or
62	/// unavailable.
63	pub input_vtxos: Vec<VtxoId>,
64	/// A list of IDs for new VTXOs that were produced as a result of this movement. Often change
65	/// VTXOs will be found here for outbound actions unless this was an inbound action.
66	pub output_vtxos: Vec<VtxoId>,
67	/// A list of IDs for VTXOs that were marked for unilateral exit as a result of this movement.
68	/// This could happen for many reasons, e.g. an unsuccessful lightning payment which can't be
69	/// revoked but is about to expire. VTXOs listed here will result in a reduction of spendable
70	/// balance due to the VTXOs being managed by the [crate::Exit] system.
71	pub exited_vtxos: Vec<VtxoId>,
72	/// Contains the times at which the movement was created, updated and completed.
73	pub time: MovementTimestamp,
74}
75
76impl Movement {
77	pub fn new(
78		id: MovementId,
79		status: MovementStatus,
80		subsystem: &MovementSubsystem,
81		time: DateTime<chrono::Local>,
82	) -> Self {
83		Self {
84			id,
85			status,
86			subsystem: subsystem.clone(),
87			time: MovementTimestamp {
88				created_at: time,
89				updated_at: time,
90				completed_at: None,
91			},
92			metadata: serde_json::Map::new(),
93			intended_balance: SignedAmount::ZERO,
94			effective_balance: SignedAmount::ZERO,
95			offchain_fee: Amount::ZERO,
96			sent_to: vec![],
97			received_on: vec![],
98			input_vtxos: vec![],
99			output_vtxos: vec![],
100			exited_vtxos: vec![],
101		}
102	}
103
104	/// Checks whether this movement received the given payment method
105	pub fn received_on(&self, payment_method: &PaymentMethod) -> bool {
106		self.received_on.iter().any(|d| d.destination == *payment_method)
107	}
108
109	/// Checks whether this movement sent to the given payment method
110	pub fn sent_to(&self, payment_method: &PaymentMethod) -> bool {
111		self.sent_to.iter().any(|d| d.destination == *payment_method)
112	}
113
114	/// Get the Lightning invoice associated with this movement
115	///
116	/// Returns `None` for movements that don't have an invoice.
117	pub fn lightning_invoice(&self) -> Option<&Invoice> {
118		for dest in &self.received_on {
119			if let PaymentMethod::Invoice(ref i) = dest.destination {
120				return Some(i);
121			}
122		}
123
124		for dest in &self.sent_to {
125			if let PaymentMethod::Invoice(ref i) = dest.destination {
126				return Some(i);
127			}
128		}
129
130		None
131	}
132
133	/// Get the Lightning offer associated with this movement
134	///
135	/// Returns `None` for movements that don't have an offer.
136	pub fn lightning_offer(&self) -> Option<&Offer> {
137		for dest in &self.received_on {
138			if let PaymentMethod::Offer(ref o) = dest.destination {
139				return Some(o);
140			}
141		}
142
143		for dest in &self.sent_to {
144			if let PaymentMethod::Offer(ref o) = dest.destination {
145				return Some(o);
146			}
147		}
148
149		None
150	}
151
152	/// Get the Lightning payment hash associated with this movement
153	///
154	/// Returns `None` for movements that are not Lightning payments.
155	pub fn lightning_payment_hash(&self) -> Option<PaymentHash> {
156		LightningMovement::get_payment_hash(&self.metadata)
157			.or_else(|| self.lightning_invoice().map(|i| i.payment_hash()))
158	}
159}
160
161/// A unique identifier for a movement.
162#[derive(Clone, Copy, Eq, Hash, PartialEq, Deserialize, Serialize, Ord, PartialOrd)]
163pub struct MovementId(pub u32);
164
165impl MovementId {
166	pub fn new(id: u32) -> Self {
167		Self(id)
168	}
169
170	pub fn to_bytes(&self) -> [u8; 4] {
171		self.0.to_be_bytes()
172	}
173}
174
175impl fmt::Display for MovementId {
176	fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
177		fmt::Display::fmt(&self.0, f)
178	}
179}
180
181impl fmt::Debug for MovementId {
182	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
183		fmt::Display::fmt(&self, f)
184	}
185}
186
187/// Represents the current status of a [Movement]. It's important to note that each status can
188/// result in fund changes. As an example, a lightning payment could fail but this will still result
189/// in a change of VTXOs. You can't assume that [MovementStatus::Failed] means that user funds
190/// didn't change.
191#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
192pub enum MovementStatus {
193	/// The default status of a new [Movement]. Should be treated as in-progress.
194	Pending,
195	/// The [Movement] has completed with changes. Note; this does not necessarily mean the [Movement]
196	/// completed successfully, e.g., VTXOs may be consumed and new ones produced.
197	Successful,
198	/// The [Movement] failed to complete due to an error. This should result in changes in user
199	/// funds.
200	Failed,
201	/// A [Movement] was canceled, either by the protocol (e.g., lightning payments) or by the
202	/// user.
203	Canceled,
204}
205
206impl MovementStatus {
207	/// Returns the canonical stable string for this status.
208	///
209	/// The returned value is intended for persistence and interoperability.
210	/// Use [`MovementStatus::from_str`] to parse it back.
211	pub fn as_str(&self) -> &'static str {
212		match self {
213			Self::Pending => MOVEMENT_PENDING,
214			Self::Successful => MOVEMENT_SUCCESSFUL,
215			Self::Failed => MOVEMENT_FAILED,
216			Self::Canceled => MOVEMENT_CANCELED,
217		}
218	}
219}
220
221impl fmt::Display for MovementStatus {
222	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
223		f.write_str(self.as_str())
224	}
225}
226
227impl fmt::Debug for MovementStatus {
228	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
229		fmt::Display::fmt(&self, f)
230	}
231}
232
233impl FromStr for MovementStatus {
234	type Err = anyhow::Error;
235
236	/// Formats the kind as its canonical string (same as [`MovementStatus::as_str`]).
237	fn from_str(s: &str) -> Result<Self, Self::Err> {
238		match s {
239			MOVEMENT_PENDING => Ok(MovementStatus::Pending),
240			MOVEMENT_SUCCESSFUL => Ok(MovementStatus::Successful),
241			MOVEMENT_FAILED => Ok(MovementStatus::Failed),
242			MOVEMENT_CANCELED => Ok(MovementStatus::Canceled),
243			_ => bail!("Invalid MovementStatus: {}", s),
244		}
245	}
246}
247
248impl Serialize for MovementStatus {
249	fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
250	where
251		S: serde::Serializer,
252	{
253		serializer.serialize_str(self.as_str())
254	}
255}
256
257impl<'de> Deserialize<'de> for MovementStatus {
258	fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
259	where
260		D: serde::Deserializer<'de>,
261	{
262		let s = String::deserialize(deserializer)?;
263		MovementStatus::from_str(&s).map_err(serde::de::Error::custom)
264	}
265}
266
267/// Describes a recipient of a movement. This could either be an external recipient in send actions
268/// or it could be the bark wallet itself.
269#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
270pub struct MovementDestination {
271	/// An address, invoice or any other identifier to distinguish the recipient.
272	pub destination: PaymentMethod,
273	/// How many sats the recipient received.
274	#[serde(with = "bitcoin::amount::serde::as_sat")]
275	pub amount: Amount,
276}
277
278impl MovementDestination {
279	pub fn new(payment_method: PaymentMethod, amount: Amount) -> Self {
280		Self { destination: payment_method, amount }
281	}
282
283	pub fn ark(address: ark::Address, amount: Amount) -> Self {
284		Self::new(address.into(), amount)
285	}
286
287	pub fn bitcoin(address: bitcoin::Address, amount: Amount) -> Self {
288		Self::new(address.into(), amount)
289	}
290
291	pub fn invoice(invoice: Invoice, amount: Amount) -> Self {
292		Self::new(invoice.into(), amount)
293	}
294
295	pub fn offer(offer: Offer, amount: Amount) -> Self {
296		Self::new(offer.into(), amount)
297	}
298
299	pub fn lightning_address(address: LightningAddress, amount: Amount) -> Self {
300		Self::new(address.into(), amount)
301	}
302
303	pub fn custom(destination: String, amount: Amount) -> Self {
304		Self::new(PaymentMethod::Custom(destination), amount)
305	}
306}
307
308/// Contains information about the subsystem that created the movement as well as the purpose
309/// of the movement.
310#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
311pub struct MovementSubsystem {
312	/// The name of the subsystem that created and manages the movement.
313	pub name: String,
314	/// The action responsible for registering the movement.
315	pub kind: String,
316}
317
318impl MovementSubsystem {
319	/// Check whether this subsytem matches the given subsystem
320	pub fn is_subsystem(&self, subsystem: Subsystem) -> bool {
321		self.name.as_str() == subsystem.as_name()
322	}
323}
324
325/// Contains the times at which the movement was created, updated and completed.
326#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
327pub struct MovementTimestamp {
328	/// When the movement was first created.
329	pub created_at: DateTime<chrono::Local>,
330	/// When the movement was last updated.
331	pub updated_at: DateTime<chrono::Local>,
332	/// The action responsible for registering the movement.
333	pub completed_at: Option<DateTime<chrono::Local>>,
334}