bark/movement/
payment_method.rs

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