bitcoin_payment_instructions/
receive.rs

1//! These days, there are several possible ways to communicate Bitcoin receive instructions.
2//!
3//! This module attempts to unify them into a simple parser which can read text provided directly
4//! by a receiver or via a QR code scan/URI open and convert it into receive instructions.
5//!
6//! See the [`ReceiveInstructions`] type for the supported instruction formats.
7//!
8//! This module doesn't actually help you *receive* these funds, but provides a unified way to
9//! parse them.
10
11use crate::split_once;
12
13use bitcoin::secp256k1::SecretKey;
14use bitcoin::Network;
15
16use lightning::offers::parse::Bolt12ParseError;
17use lightning::offers::refund::Refund;
18
19use alloc::str::FromStr;
20use alloc::string::{String, ToString};
21use alloc::vec;
22use alloc::vec::Vec;
23
24use core::time::Duration;
25
26/// A method which can be used to receive a payment
27#[derive(PartialEq, Eq, Debug, Clone)]
28pub enum ReceiveMethod {
29	/// A raw bitcoin private key, this will be parsed WIF encoded
30	PrivateKey(SecretKey),
31	/// A Bolt 12 Refund
32	Bolt12Refund(Refund),
33	// TODO: lnurl withdrawal? (sadly, cannot be identified as such statically...)
34}
35
36/// An error when parsing payment instructions into [`ReceiveInstructions`].
37#[derive(Debug)]
38pub enum ParseError {
39	/// An invalid lightning BOLT 12 refund was encountered
40	InvalidBolt12(Bolt12ParseError),
41	/// The receive instructions encoded instructions for a network other than the one specified.
42	WrongNetwork,
43	/// The instructions were invalid due to a semantic error.
44	///
45	/// A developer-readable error string is provided, though you may or may not wish to provide
46	/// this directly to users.
47	InvalidInstructions(&'static str),
48	/// The receive instructions did not appear to match any known form of receive instructions.
49	UnknownReceiveInstructions,
50	/// The BIP 321 bitcoin: URI included unknown required parameter(s)
51	UnknownRequiredParameter,
52	/// The payment instructions have expired and are no longer payable.
53	InstructionsExpired,
54}
55
56pub(crate) fn check_expiry(_expiry: Duration) -> Result<(), ParseError> {
57	#[cfg(feature = "std")]
58	{
59		use std::time::SystemTime;
60		if let Ok(now) = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) {
61			if now > _expiry {
62				return Err(ParseError::InstructionsExpired);
63			}
64		}
65	}
66	Ok(())
67}
68
69/// Parsed receive instructions representing a set of possible ways to receive, as well as an
70/// associated description.
71///
72/// It supports:
73///  * Raw WIF encoded private keys (which should be swept)
74///  * Lightning Bolt 12 Refunds
75#[derive(PartialEq, Eq, Debug, Clone)]
76pub struct ReceiveInstructions {
77	description: Option<String>,
78	methods: Vec<ReceiveMethod>,
79}
80
81impl ReceiveInstructions {
82	/// The list of [`ReceiveMethod`]s.
83	pub fn methods(&self) -> &[ReceiveMethod] {
84		&self.methods
85	}
86
87	/// A sender-provided description of the payment instructions.
88	///
89	/// This may be:
90	///  * the `description` field in a lightning BOLT 12 offer
91	pub fn sender_description(&self) -> Option<&str> {
92		self.description.as_deref()
93	}
94
95	/// Resolves a string into [`ReceiveInstructions`]. Verifying it is valid for the given network
96	pub fn parse_receive_instructions(
97		instructions: &str, network: Network,
98	) -> Result<ReceiveInstructions, ParseError> {
99		if instructions.is_empty() {
100			return Err(ParseError::InvalidInstructions("Empty string"));
101		}
102		const BTC_URI_PFX_LEN: usize = "bitcoin:".len();
103
104		// TODO this is copied from `parse_resolved_instructions` could refactor into unified function potentially
105		// we don't check for WIF encoded private keys in bitcoin: qr codes because that is not really something
106		// anyone does, nor does it make sense in its context
107		if instructions.len() >= BTC_URI_PFX_LEN
108			&& instructions[..BTC_URI_PFX_LEN].eq_ignore_ascii_case("bitcoin:")
109		{
110			let (_, params) = split_once(&instructions[BTC_URI_PFX_LEN..], '?');
111			let mut methods = Vec::new();
112			let mut description = None;
113			if let Some(params) = params {
114				for param in params.split('&') {
115					let (k, v) = split_once(param, '=');
116					if k.eq_ignore_ascii_case("lnr") || k.eq_ignore_ascii_case("req-lnr") {
117						if let Some(v) = v {
118							match Refund::from_str(v) {
119								Ok(refund) => {
120									if refund.chain() != network.chain_hash() {
121										return Err(ParseError::WrongNetwork);
122									}
123
124									description = Some(refund.description().0.to_string());
125									methods.push(ReceiveMethod::Bolt12Refund(refund));
126								},
127								Err(err) => return Err(ParseError::InvalidBolt12(err)),
128							}
129						} else {
130							let err = "Missing value for a BOLT 12 refund parameter in a BIP 321 bitcoin: URI";
131							return Err(ParseError::InvalidInstructions(err));
132						}
133					} else if k.len() >= 4 && k[..4].eq_ignore_ascii_case("req-") {
134						return Err(ParseError::UnknownRequiredParameter);
135					}
136				}
137			}
138
139			if methods.is_empty() {
140				return Err(ParseError::UnknownReceiveInstructions);
141			}
142
143			return Ok(ReceiveInstructions { description, methods });
144		}
145
146		if let Ok(pk) = bitcoin::key::PrivateKey::from_wif(instructions) {
147			if pk.network != network.into() {
148				return Err(ParseError::WrongNetwork);
149			}
150
151			return Ok(ReceiveInstructions {
152				description: None,
153				methods: vec![ReceiveMethod::PrivateKey(pk.inner)],
154			});
155		}
156
157		if let Ok(refund) = Refund::from_str(instructions) {
158			if refund.chain() != network.chain_hash() {
159				return Err(ParseError::WrongNetwork);
160			}
161			if let Some(expiry) = refund.absolute_expiry() {
162				check_expiry(expiry)?;
163			}
164
165			return Ok(ReceiveInstructions {
166				description: Some(refund.description().to_string()),
167				methods: vec![ReceiveMethod::Bolt12Refund(refund)],
168			});
169		}
170
171		Err(ParseError::UnknownReceiveInstructions)
172	}
173}