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