Skip to main content

bark/movement/
payment_method.rs

1use std::fmt;
2use std::str::FromStr;
3
4use anyhow::Context;
5use bitcoin::address::{NetworkChecked, NetworkUnchecked};
6use bitcoin::hex::DisplayHex;
7use lightning::offers::invoice::Bolt12Invoice;
8use lightning::offers::offer::Offer;
9use lightning_invoice::Bolt11Invoice;
10use lnurllib::lightning_address::LightningAddress;
11use serde::{Deserialize, Serialize};
12
13use ark::lightning::Invoice;
14
15const PAYMENT_METHOD_TAG: &str = "type";
16const PAYMENT_METHOD_VALUE: &str = "value";
17const PAYMENT_METHOD_ARK: &str = "ark";
18const PAYMENT_METHOD_BITCOIN: &str = "bitcoin";
19const PAYMENT_METHOD_OUTPUT_SCRIPT: &str = "output-script";
20const PAYMENT_METHOD_INVOICE: &str = "invoice";
21const PAYMENT_METHOD_OFFER: &str = "offer";
22const PAYMENT_METHOD_LIGHTNING_ADDRESS: &str = "lightning-address";
23const PAYMENT_METHOD_CUSTOM: &str = "custom";
24
25/// Provides a typed mechanism for describing the recipient in a
26/// [MovementDestination](crate::movement::MovementDestination).
27#[derive(Debug, Clone, PartialEq, Eq, Hash)]
28pub enum PaymentMethod {
29	/// An [ark::Address] for bark.
30	Ark(ark::Address),
31	/// An onchain [bitcoin::Address].
32	Bitcoin(bitcoin::Address<NetworkUnchecked>),
33	/// An onchain [bitcoin::ScriptBuf] output, typically used for non-address formats like
34	/// OP_RETURN.
35	OutputScript(bitcoin::ScriptBuf),
36	/// Any supported form of lightning [Invoice], e.g., [Bolt11Invoice] and [Bolt12Invoice].
37	Invoice(Invoice),
38	/// A reusable BOLT12 [Offer] for lightning payments.
39	Offer(Offer),
40	/// An email-like format used to retrieve a [Bolt11Invoice].
41	LightningAddress(LightningAddress),
42	/// An alternative payment method that isn't native to bark.
43	Custom(String),
44}
45
46impl PaymentMethod {
47	pub fn is_ark(&self) -> bool {
48		match self {
49			PaymentMethod::Ark(_) => true,
50			PaymentMethod::Bitcoin(_) => false,
51			PaymentMethod::OutputScript(_) => false,
52			PaymentMethod::Invoice(_) => false,
53			PaymentMethod::Offer(_) => false,
54			PaymentMethod::LightningAddress(_) => false,
55			PaymentMethod::Custom(_) => false,
56		}
57	}
58
59	pub fn is_bitcoin(&self) -> bool {
60		match self {
61			PaymentMethod::Ark(_) => false,
62			PaymentMethod::Bitcoin(_) => true,
63			PaymentMethod::OutputScript(_) => true,
64			PaymentMethod::Invoice(_) => false,
65			PaymentMethod::Offer(_) => false,
66			PaymentMethod::LightningAddress(_) => false,
67			PaymentMethod::Custom(_) => false,
68		}
69	}
70
71	pub fn is_custom(&self) -> bool {
72		match self {
73			PaymentMethod::Ark(_) => false,
74			PaymentMethod::Bitcoin(_) => false,
75			PaymentMethod::OutputScript(_) => false,
76			PaymentMethod::Invoice(_) => false,
77			PaymentMethod::Offer(_) => false,
78			PaymentMethod::LightningAddress(_) => false,
79			PaymentMethod::Custom(_) => true,
80		}
81	}
82
83	/// Returns whether the payment method is a lightning payment method, e.g., BOLT11.
84	pub fn is_lightning(&self) -> bool {
85		match self {
86			PaymentMethod::Ark(_) => false,
87			PaymentMethod::Bitcoin(_) => false,
88			PaymentMethod::OutputScript(_) => false,
89			PaymentMethod::Invoice(_) => true,
90			PaymentMethod::Offer(_) => true,
91			PaymentMethod::LightningAddress(_) => true,
92			PaymentMethod::Custom(_) => false,
93		}
94	}
95
96	/// Returns the type tag string for this payment method.
97	pub fn type_str(&self) -> &'static str {
98		match self {
99			PaymentMethod::Ark(_) => PAYMENT_METHOD_ARK,
100			PaymentMethod::Bitcoin(_) => PAYMENT_METHOD_BITCOIN,
101			PaymentMethod::OutputScript(_) => PAYMENT_METHOD_OUTPUT_SCRIPT,
102			PaymentMethod::Invoice(_) => PAYMENT_METHOD_INVOICE,
103			PaymentMethod::Offer(_) => PAYMENT_METHOD_OFFER,
104			PaymentMethod::LightningAddress(_) => PAYMENT_METHOD_LIGHTNING_ADDRESS,
105			PaymentMethod::Custom(_) => PAYMENT_METHOD_CUSTOM,
106		}
107	}
108
109	/// Returns the value as a plain string for this payment method.
110	pub fn value_string(&self) -> String {
111		match self {
112			PaymentMethod::Ark(addr) => addr.to_string(),
113			PaymentMethod::Bitcoin(addr) => addr.assume_checked_ref().to_string(),
114			PaymentMethod::OutputScript(script) => script.as_bytes().to_lower_hex_string(),
115			PaymentMethod::Invoice(invoice) => invoice.to_string(),
116			PaymentMethod::Offer(offer) => offer.to_string(),
117			PaymentMethod::LightningAddress(addr) => addr.to_string(),
118			PaymentMethod::Custom(custom) => custom.clone(),
119		}
120	}
121
122	/// Construct a PaymentMethod from a type tag and value string.
123	pub fn from_type_value(type_str: &str, value: &str) -> anyhow::Result<Self> {
124		match type_str {
125			PAYMENT_METHOD_ARK => {
126				let addr = ark::Address::from_str(value)
127					.context("invalid ark address")?;
128				Ok(PaymentMethod::Ark(addr))
129			},
130			PAYMENT_METHOD_BITCOIN => {
131				let addr = bitcoin::Address::from_str(value)
132					.context("invalid bitcoin address")?;
133				Ok(PaymentMethod::Bitcoin(addr))
134			},
135			PAYMENT_METHOD_OUTPUT_SCRIPT => {
136				let script = bitcoin::ScriptBuf::from_hex(value)
137					.context("invalid output script hex")?;
138				Ok(PaymentMethod::OutputScript(script))
139			},
140			PAYMENT_METHOD_INVOICE => {
141				let invoice = Invoice::from_str(value)
142					.context("invalid invoice")?;
143				Ok(PaymentMethod::Invoice(invoice))
144			},
145			PAYMENT_METHOD_OFFER => {
146				let offer = value.parse()
147					.map_err(|e| anyhow!("{:?}", e))
148					.context("invalid offer")?;
149				Ok(PaymentMethod::Offer(offer))
150			},
151			PAYMENT_METHOD_LIGHTNING_ADDRESS => {
152				let addr = LightningAddress::from_str(value)
153					.context("invalid lightning address")?;
154				Ok(PaymentMethod::LightningAddress(addr))
155			},
156			PAYMENT_METHOD_CUSTOM => {
157				Ok(PaymentMethod::Custom(value.to_string()))
158			},
159			_ => bail!("unknown payment method type: {}", type_str),
160		}
161	}
162}
163
164impl fmt::Display for PaymentMethod {
165	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
166	    match self {
167			PaymentMethod::Ark(a) => fmt::Display::fmt(a, f),
168			PaymentMethod::Bitcoin(b) => fmt::Display::fmt(b.assume_checked_ref(), f),
169			PaymentMethod::OutputScript(o) => fmt::Display::fmt(&o.as_bytes().as_hex(), f),
170			PaymentMethod::Invoice(i) => fmt::Display::fmt(i, f),
171			PaymentMethod::Offer(o) => fmt::Display::fmt(o, f),
172			PaymentMethod::LightningAddress(a) => fmt::Display::fmt(a, f),
173			PaymentMethod::Custom(v) => fmt::Display::fmt(v, f),
174		}
175	}
176}
177
178impl From<ark::Address> for PaymentMethod {
179	fn from(addr: ark::Address) -> Self {
180		PaymentMethod::Ark(addr)
181	}
182}
183
184impl From<bitcoin::Address<NetworkUnchecked>> for PaymentMethod {
185	fn from(addr: bitcoin::Address<NetworkUnchecked>) -> Self {
186		PaymentMethod::Bitcoin(addr)
187	}
188}
189
190impl From<bitcoin::Address<NetworkChecked>> for PaymentMethod {
191	fn from(addr: bitcoin::Address<NetworkChecked>) -> Self {
192		PaymentMethod::Bitcoin(addr.into_unchecked())
193	}
194}
195
196impl From<Bolt11Invoice> for PaymentMethod {
197	fn from(invoice: Bolt11Invoice) -> Self {
198		PaymentMethod::Invoice(invoice.into())
199	}
200}
201
202impl From<Bolt12Invoice> for PaymentMethod {
203	fn from(invoice: Bolt12Invoice) -> Self {
204		PaymentMethod::Invoice(invoice.into())
205	}
206}
207
208impl From<Invoice> for PaymentMethod {
209	fn from(invoice: Invoice) -> Self {
210		PaymentMethod::Invoice(invoice)
211	}
212}
213
214impl From<Offer> for PaymentMethod {
215	fn from(offer: Offer) -> Self {
216		PaymentMethod::Offer(offer)
217	}
218}
219
220impl From<LightningAddress> for PaymentMethod {
221	fn from(addr: LightningAddress) -> Self {
222		PaymentMethod::LightningAddress(addr)
223	}
224}
225
226impl Serialize for PaymentMethod {
227	fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
228	where
229		S: serde::Serializer,
230	{
231		use serde::ser::SerializeStruct;
232		let mut state = serializer.serialize_struct("PaymentMethod", 2)?;
233		state.serialize_field(PAYMENT_METHOD_TAG, self.type_str())?;
234		state.serialize_field(PAYMENT_METHOD_VALUE, &self.value_string())?;
235		state.end()
236	}
237}
238
239impl<'de> Deserialize<'de> for PaymentMethod {
240	fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
241	where
242		D: serde::Deserializer<'de>,
243	{
244		use serde::de::{self, MapAccess, Visitor};
245		use std::fmt;
246
247		struct PaymentMethodVisitor;
248
249		impl<'de> Visitor<'de> for PaymentMethodVisitor {
250			type Value = PaymentMethod;
251
252			fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
253				formatter.write_str(&format!(
254					"a PaymentMethod with {} and {} fields", PAYMENT_METHOD_TAG, PAYMENT_METHOD_VALUE,
255				))
256			}
257
258			fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
259			where
260				A: MapAccess<'de>,
261			{
262				let mut type_value: Option<String> = None;
263				let mut value_string: Option<String> = None;
264
265				while let Some(key) = map.next_key::<String>()? {
266					match key.as_str() {
267						PAYMENT_METHOD_TAG => {
268							if type_value.is_some() {
269								return Err(de::Error::duplicate_field(PAYMENT_METHOD_TAG));
270							}
271							type_value = Some(map.next_value()?);
272						}
273						PAYMENT_METHOD_VALUE => {
274							if value_string.is_some() {
275								return Err(de::Error::duplicate_field(PAYMENT_METHOD_VALUE));
276							}
277							value_string = Some(map.next_value()?);
278						}
279						_ => {
280							let _: de::IgnoredAny = map.next_value()?;
281						}
282					}
283				}
284
285				let type_str = type_value.ok_or_else(|| de::Error::missing_field(PAYMENT_METHOD_TAG))?;
286				let value = value_string.ok_or_else(|| de::Error::missing_field(PAYMENT_METHOD_VALUE))?;
287
288				PaymentMethod::from_type_value(&type_str, &value).map_err(de::Error::custom)
289			}
290		}
291
292		deserializer.deserialize_struct(
293			"PaymentMethod", &[PAYMENT_METHOD_TAG, PAYMENT_METHOD_VALUE], PaymentMethodVisitor,
294		)
295	}
296}
297
298#[cfg(test)]
299mod test {
300	use std::str::FromStr;
301
302	use super::*;
303
304	#[test]
305	fn test_serialization() {
306		let ark_str = "tark1pwh9vsmezqqpjy9akejayl2vvcse6he97rn40g84xrlvrlnhayuuyefrp9nse2y3zqqpjy9akejayl2vvcse6he97rn40g84xrlvrlnhayuuyefrp9nse2yscufs5u";
307		let serialised = r#"{"type":"ark","value":"tark1pwh9vsmezqqpjy9akejayl2vvcse6he97rn40g84xrlvrlnhayuuyefrp9nse2y3zqqpjy9akejayl2vvcse6he97rn40g84xrlvrlnhayuuyefrp9nse2yscufs5u"}"#;
308		let ark_method = PaymentMethod::Ark(ark::Address::from_str(ark_str).unwrap());
309		assert_eq!(serde_json::to_string(&ark_method).unwrap(), serialised);
310		assert_eq!(serde_json::from_str::<PaymentMethod>(serialised).unwrap(), ark_method);
311
312		let bitcoin_str = "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa";
313		let serialised = r#"{"type":"bitcoin","value":"1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa"}"#;
314		let bitcoin_method = PaymentMethod::Bitcoin(bitcoin::Address::from_str(bitcoin_str).unwrap());
315		assert_eq!(serde_json::to_string(&bitcoin_method).unwrap(), serialised);
316		assert_eq!(serde_json::from_str::<PaymentMethod>(serialised).unwrap(), bitcoin_method);
317
318		let script_str = "6a0474657374"; // OP_RETURN, push 4 bytes with the string "test"
319		let serialised = r#"{"type":"output-script","value":"6a0474657374"}"#;
320		let output_method = PaymentMethod::OutputScript(bitcoin::ScriptBuf::from_hex(script_str).unwrap());
321		assert_eq!(serde_json::to_string(&output_method).unwrap(), serialised);
322		assert_eq!(serde_json::from_str::<PaymentMethod>(serialised).unwrap(), output_method);
323
324		let invoice_str = "lntbs100u1p5j0x82sp5d0rwfh7tgrrlwsegy9rx3tzpt36cqwjqza5x4wvcjxjzscfaf6jspp5d8q7354dg3p8h0kywhqq5dq984r8f5en98hf9ln85ug0w8fx6hhsdqqcqzpc9qyysgqyk54v7tpzprxll7e0jyvtxcpgwttzk84wqsfjsqvcdtq47zt2wssxsmtjhz8dka62mdnf9jafhu3l4cpyfnsx449v4wstrwzzql2w5qqs8uh7p";
325		let serialised = r#"{"type":"invoice","value":"lntbs100u1p5j0x82sp5d0rwfh7tgrrlwsegy9rx3tzpt36cqwjqza5x4wvcjxjzscfaf6jspp5d8q7354dg3p8h0kywhqq5dq984r8f5en98hf9ln85ug0w8fx6hhsdqqcqzpc9qyysgqyk54v7tpzprxll7e0jyvtxcpgwttzk84wqsfjsqvcdtq47zt2wssxsmtjhz8dka62mdnf9jafhu3l4cpyfnsx449v4wstrwzzql2w5qqs8uh7p"}"#;
326		let invoice_method = PaymentMethod::Invoice(Bolt11Invoice::from_str(invoice_str).unwrap().into());
327		assert_eq!(serde_json::to_string(&invoice_method).unwrap(), serialised);
328		assert_eq!(serde_json::from_str::<PaymentMethod>(serialised).unwrap(), invoice_method);
329
330		let offer_str = "lno1qgsyxjtl6luzd9t3pr62xr7eemp6awnejusgf6gw45q75vcfqqqqqqq2p32x2um5ypmx2cm5dae8x93pqthvwfzadd7jejes8q9lhc4rvjxd022zv5l44g6qah82ru5rdpnpj";
331		let serialised = r#"{"type":"offer","value":"lno1qgsyxjtl6luzd9t3pr62xr7eemp6awnejusgf6gw45q75vcfqqqqqqq2p32x2um5ypmx2cm5dae8x93pqthvwfzadd7jejes8q9lhc4rvjxd022zv5l44g6qah82ru5rdpnpj"}"#;
332		let offer_method = PaymentMethod::Offer(Offer::from_str(offer_str).unwrap());
333		assert_eq!(serde_json::to_string(&offer_method).unwrap(), serialised);
334		assert_eq!(serde_json::from_str::<PaymentMethod>(serialised).unwrap(), offer_method);
335
336		let lnaddr_str = "byte@second.tech";
337		let serialised = r#"{"type":"lightning-address","value":"byte@second.tech"}"#;
338		let lnaddr_method = PaymentMethod::LightningAddress(LightningAddress::from_str(lnaddr_str).unwrap());
339		assert_eq!(serde_json::to_string(&lnaddr_method).unwrap(), serialised);
340		assert_eq!(serde_json::from_str::<PaymentMethod>(serialised).unwrap(), lnaddr_method);
341
342		let custom_str = "THIS IS AN EXAMPLE OF A CUSTOM STRING";
343		let serialised = r#"{"type":"custom","value":"THIS IS AN EXAMPLE OF A CUSTOM STRING"}"#;
344		let custom_method = PaymentMethod::Custom(String::from(custom_str));
345		assert_eq!(serde_json::to_string(&custom_method).unwrap(), serialised);
346		assert_eq!(serde_json::from_str::<PaymentMethod>(serialised).unwrap(), custom_method);
347	}
348}