bitcoin_uri/
lib.rs

1//! Rust-idiomatic, compliant, flexible and performant bitcoin URI crate.
2//!
3//! **Important:** while lot of work went into polishing the crate it's still considered
4//! early-development!
5//!
6//! * Rust-idiomatic: uses strong types, standard traits and other things
7//! * Compliant: implements all requirements of BIP21, including protections to not forget about
8//!              `req-`. (But see features.)
9//! * Flexible: enables parsing/serializing additional arguments not defined by BIP21
10//! * Performant: uses zero-copy deserialization and lazy evaluation wherever possible.
11//!
12//! Serialization and deserialization is inspired by `serde` with these important differences:
13//!
14//! * Deserialization signals if the field is known so that `req-` fields can be rejected.
15//! * Much simpler API - we don't need all the features.
16//! * Use of [`Param<'a>`] to enable lazy evaluation.
17//!
18//! The crate is `no_std` but does require `alloc`.
19//!
20//! ## Features
21//!
22//! * `std` enables integration with `std` - mainly `std::error::Error`.
23//! * `non-compliant-bytes` - enables use of non-compliant API that can parse non-UTF-8 URI values.
24//!
25//! ## Stabilization roadmap
26//!
27//! The crate can not (and will not) be stabilized until either [`bitcoin`] is stabilized or
28//! [`bitcoin::Address`] and [`bitcoin::Amount`] are moved to (a) separate stable crate(s).
29//!
30//! ## MSRV
31//!
32//! 1.56.1
33
34#![cfg_attr(docsrs, feature(doc_cfg))]
35#![no_std]
36#![deny(unused_must_use)]
37#![deny(missing_docs)]
38
39#[cfg(feature = "std")]
40extern crate std;
41
42extern crate alloc;
43
44pub mod de;
45pub mod ser;
46
47use alloc::borrow::ToOwned;
48use alloc::borrow::Cow;
49#[cfg(feature = "non-compliant-bytes")]
50use alloc::vec::Vec;
51use alloc::string::String;
52use percent_encoding_rfc3986::{PercentDecode, PercentDecodeError};
53#[cfg(feature = "non-compliant-bytes")]
54use either::Either;
55use core::convert::{TryFrom, TryInto};
56use bitcoin::address::NetworkValidation;
57
58pub use de::{DeserializeParams, DeserializationState, DeserializationError};
59pub use ser::SerializeParams;
60
61/// Parsed BIP21 URI.
62///
63/// This struct represents all fields of BIP21 URI with the ability to add more extra fields using
64/// the `extras` field. By default there are no extra fields so an empty implementation is used.
65///
66/// ## Parsing
67///
68/// `Uri` implements `FromStr` so you can simply use `s.parse::<Uri<'static>>()`. However that is
69/// not zero-copy. If you wish to use zero-copy parsing call `try_into()` instead.
70///
71/// ## Displaying
72///
73/// `Display` is implemented for `Uri` so you can format it naturally. However it currently does
74/// **not** support alignment.
75///
76/// The code does _not_ assume strict BIP-21, so it displays the schema lower case
77/// instead as upper case.
78/// This makes it compatible with some (buggy) wallets but does not create the most optimal QR codes.
79///
80/// [See compatibility table.](https://github.com/btcpayserver/btcpayserver/issues/2110)
81#[non_exhaustive]
82#[derive(Debug, Clone)]
83pub struct Uri<'a, NetVal = bitcoin::address::NetworkChecked, Extras = NoExtras>
84where
85    NetVal: NetworkValidation,
86{
87    /// The address provided in the URI.
88    ///
89    /// This field is mandatory because the address is mandatory in BIP21.
90    pub address: bitcoin::Address<NetVal>,
91
92    /// Number of satoshis requested as payment.
93    pub amount: Option<bitcoin::Amount>,
94
95    /// The label of the address - e.g. name of the receiver.
96    pub label: Option<Param<'a>>,
97
98    /// Message that describes the transaction to the user.
99    pub message: Option<Param<'a>>,
100
101    /// Extra fields that can occur in a BIP21 URI.
102    pub extras: Extras,
103}
104
105impl<NetVal: NetworkValidation, T: Default> Uri<'_, NetVal, T> {
106    /// Creates an URI with defaults.
107    ///
108    /// This sets all fields except `address` to default values.
109    /// They can be overwritten in subsequent assignments before displaying the URI.
110    pub fn new(address: bitcoin::Address<NetVal>) -> Self {
111        Uri {
112            address,
113            amount: None,
114            label: None,
115            message: None,
116            extras: Default::default(),
117        }
118    }
119}
120
121impl<NetVal: NetworkValidation, T> Uri<'_, NetVal, T> {
122    /// Creates an URI with defaults.
123    ///
124    /// This sets all fields except `address` and `extras` to default values.
125    /// They can be overwritten in subsequent assignments before displaying the URI.
126    pub fn with_extras(address: bitcoin::Address<NetVal>, extras: T) -> Self {
127        Uri {
128            address,
129            amount: None,
130            label: None,
131            message: None,
132            extras,
133        }
134    }
135}
136
137/// Abstracted stringly parameter in the URI.
138///
139/// This type abstracts the parameter that may be encoded allowing lazy decoding, possibly even
140/// without allocation.
141/// When constructing [`Uri`] to be displayed you may use `From<S>` where `S` is one of various
142/// stringly types. The conversion is always cheap.
143#[derive(Debug, Clone)]
144pub struct Param<'a>(ParamInner<'a>);
145
146impl<'a> Param<'a> {
147    /// Convenience constructor.
148    fn decode(s: &'a str) -> Result<Self, PercentDecodeError> {
149        Ok(Param(ParamInner::EncodedBorrowed(percent_encoding_rfc3986::percent_decode_str(s)?)))
150    }
151
152    /// Creates a byte iterator yielding decoded bytes.
153    #[cfg(feature = "non-compliant-bytes")]
154    #[cfg_attr(docsrs, doc(cfg(feature = "non-compliant-bytes")))]
155    pub fn bytes(&self) -> ParamBytes<'_> {
156        ParamBytes(match &self.0 {
157            ParamInner::EncodedBorrowed(decoder) => Either::Left(decoder.clone()),
158            ParamInner::UnencodedBytes(bytes) => Either::Right(bytes.iter().cloned()),
159            ParamInner::UnencodedString(string) => Either::Right(string.as_bytes().iter().cloned()),
160        })
161    }
162
163    /// Converts the parameter into iterator yielding decoded bytes.
164    #[cfg(feature = "non-compliant-bytes")]
165    #[cfg_attr(docsrs, doc(cfg(feature = "non-compliant-bytes")))]
166    pub fn into_bytes(self) -> ParamBytesOwned<'a> {
167        ParamBytesOwned(match self.0 {
168            ParamInner::EncodedBorrowed(decoder) => Either::Left(decoder),
169            ParamInner::UnencodedBytes(Cow::Borrowed(bytes)) => Either::Right(Either::Left(bytes.iter().cloned())),
170            ParamInner::UnencodedBytes(Cow::Owned(bytes)) => Either::Right(Either::Right(bytes.into_iter())),
171            ParamInner::UnencodedString(Cow::Borrowed(string)) => Either::Right(Either::Left(string.as_bytes().iter().cloned())),
172            ParamInner::UnencodedString(Cow::Owned(string)) => Either::Right(Either::Right(Vec::from(string).into_iter())),
173        })
174    }
175
176    /// Decodes the param if encoded making the lifetime static.
177    fn decode_into_owned<'b>(self) -> Param<'b> {
178        let owned = match self.0 {
179            ParamInner::EncodedBorrowed(decoder) => ParamInner::UnencodedBytes(decoder.collect()),
180            ParamInner::UnencodedString(Cow::Borrowed(value)) => ParamInner::UnencodedString(Cow::Owned(value.to_owned())),
181            ParamInner::UnencodedString(Cow::Owned(value)) => ParamInner::UnencodedString(Cow::Owned(value)),
182            ParamInner::UnencodedBytes(Cow::Borrowed(value)) => ParamInner::UnencodedBytes(Cow::Owned(value.to_owned())),
183            ParamInner::UnencodedBytes(Cow::Owned(value)) => ParamInner::UnencodedBytes(Cow::Owned(value)),
184        };
185        Param(owned)
186    }
187}
188
189/// Cheap conversion
190impl<'a> From<&'a str> for Param<'a> {
191    fn from(value: &'a str) -> Self {
192        Param(ParamInner::UnencodedString(Cow::Borrowed(value)))
193    }
194}
195
196/// Cheap conversion
197impl From<String> for Param<'_> {
198    fn from(value: String) -> Self {
199        Param(ParamInner::UnencodedString(Cow::Owned(value)))
200    }
201}
202
203/// Cheap conversion
204#[cfg(feature = "non-compliant-bytes")]
205#[cfg_attr(docsrs, doc(cfg(feature = "non-compliant-bytes")))]
206impl<'a> From<&'a [u8]> for Param<'a> {
207    fn from(value: &'a [u8]) -> Self {
208        Param(ParamInner::UnencodedBytes(Cow::Borrowed(value)))
209    }
210}
211
212/// Cheap conversion
213#[cfg(feature = "non-compliant-bytes")]
214#[cfg_attr(docsrs, doc(cfg(feature = "non-compliant-bytes")))]
215impl From<Vec<u8>> for Param<'_> {
216    fn from(value: Vec<u8>) -> Self {
217        Param(ParamInner::UnencodedBytes(Cow::Owned(value)))
218    }
219}
220
221/// Cheap conversion
222#[cfg(feature = "non-compliant-bytes")]
223#[cfg_attr(docsrs, doc(cfg(feature = "non-compliant-bytes")))]
224impl<'a> From<Param<'a>> for Vec<u8> {
225    fn from(value: Param<'a>) -> Self {
226        match value.0 {
227            ParamInner::EncodedBorrowed(decoder) => decoder.collect(),
228            ParamInner::UnencodedString(Cow::Borrowed(value)) => value.as_bytes().to_owned(),
229            ParamInner::UnencodedString(Cow::Owned(value)) => value.into(),
230            ParamInner::UnencodedBytes(value) => value.into(),
231        }
232    }
233}
234
235/// Cheap conversion
236#[cfg(feature = "non-compliant-bytes")]
237#[cfg_attr(docsrs, doc(cfg(feature = "non-compliant-bytes")))]
238impl<'a> From<Param<'a>> for Cow<'a, [u8]> {
239    fn from(value: Param<'a>) -> Self {
240        match value.0 {
241            ParamInner::EncodedBorrowed(decoder) => decoder.into(),
242            ParamInner::UnencodedString(Cow::Borrowed(value)) => Cow::Borrowed(value.as_bytes()),
243            ParamInner::UnencodedString(Cow::Owned(value)) => Cow::Owned(value.into()),
244            ParamInner::UnencodedBytes(value) => value,
245        }
246    }
247}
248
249impl<'a> TryFrom<Param<'a>> for String {
250    type Error = core::str::Utf8Error;
251
252    fn try_from(value: Param<'a>) -> Result<Self, Self::Error> {
253        match value.0 {
254            ParamInner::EncodedBorrowed(decoder) => <Cow<'_, str>>::try_from(decoder).map(Into::into),
255            ParamInner::UnencodedString(value) => Ok(value.into()),
256            ParamInner::UnencodedBytes(Cow::Borrowed(value)) => Ok(core::str::from_utf8(value)?.to_owned()),
257            ParamInner::UnencodedBytes(Cow::Owned(value)) => String::from_utf8(value).map_err(|error| error.utf8_error()),
258        }
259    }
260}
261
262impl<'a> TryFrom<Param<'a>> for Cow<'a, str> {
263    type Error = core::str::Utf8Error;
264
265    fn try_from(value: Param<'a>) -> Result<Self, Self::Error> {
266        match value.0 {
267            ParamInner::EncodedBorrowed(decoder) => decoder.try_into(),
268            ParamInner::UnencodedString(value) => Ok(value),
269            ParamInner::UnencodedBytes(Cow::Borrowed(value)) => Ok(Cow::Borrowed(core::str::from_utf8(value)?)),
270            ParamInner::UnencodedBytes(Cow::Owned(value)) => Ok(Cow::Owned(String::from_utf8(value).map_err(|error| error.utf8_error())?)),
271        }
272    }
273}
274
275#[derive(Debug, Clone)]
276enum ParamInner<'a> {
277    EncodedBorrowed(PercentDecode<'a>),
278    UnencodedBytes(Cow<'a, [u8]>),
279    UnencodedString(Cow<'a, str>),
280}
281
282/// Iterator over decoded bytes inside paramter.
283///
284/// The lifetime of this may be shorter than that of [`Param<'a>`].
285#[cfg(feature = "non-compliant-bytes")]
286#[cfg_attr(docsrs, doc(cfg(feature = "non-compliant-bytes")))]
287#[cfg_attr(feature = "non-compliant-bytes", allow(dead_code))]
288pub struct ParamBytes<'a>(ParamIterInner<'a, core::iter::Cloned<core::slice::Iter<'a, u8>>>);
289
290/// Iterator over decoded bytes inside paramter.
291///
292/// The lifetime of this is same as that of [`Param<'a>`].
293#[cfg(feature = "non-compliant-bytes")]
294#[cfg_attr(docsrs, doc(cfg(feature = "non-compliant-bytes")))]
295#[cfg_attr(feature = "non-compliant-bytes", allow(dead_code))]
296pub struct ParamBytesOwned<'a>(ParamIterInner<'a, Either<core::iter::Cloned<core::slice::Iter<'a, u8>>, alloc::vec::IntoIter<u8>>>);
297
298#[cfg(feature = "non-compliant-bytes")]
299type ParamIterInner<'a, T> = either::Either<PercentDecode<'a>, T>;
300
301/// Empty extras.
302///
303/// This type can be used if extras are not required.
304/// It is also the default type parameter of [`Uri`].
305#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
306pub struct NoExtras;
307
308/// This is a state used to deserialize `NoExtras` - it doesn't expect any parameters.
309#[derive(Debug, Default, Copy, Clone)]
310pub struct EmptyState;
311
312impl DeserializeParams<'_> for NoExtras {
313    type DeserializationState = EmptyState;
314}
315
316impl DeserializationError for NoExtras {
317    type Error = core::convert::Infallible;
318}
319
320impl DeserializationState<'_> for EmptyState {
321    type Value = NoExtras;
322
323    fn is_param_known(&self, _key: &str) -> bool {
324        false
325    }
326
327    fn deserialize_temp(&mut self, _key: &str, _value: Param<'_>) -> Result<de::ParamKind, <Self::Value as DeserializationError>::Error> {
328        Ok(de::ParamKind::Unknown)
329    }
330
331    fn finalize(self) -> Result<Self::Value, <Self::Value as DeserializationError>::Error> {
332        Ok(Default::default())
333    }
334}
335
336impl SerializeParams for &NoExtras {
337    type Key = core::convert::Infallible;
338    type Value = core::convert::Infallible;
339    type Iterator = core::iter::Empty<(Self::Key, Self::Value)>;
340
341    fn serialize_params(self) -> Self::Iterator {
342        core::iter::empty()
343    }
344}
345
346#[cfg(test)]
347mod tests {
348    use crate::Uri;
349    use alloc::string::ToString;
350    use alloc::borrow::Cow;
351    use core::convert::TryInto;
352
353    fn check_send_sync<T: Send + Sync>() {}
354
355    #[test]
356    fn send_sync() {
357        check_send_sync::<crate::de::UriError>();
358    }
359
360    // Note: the official test vectors contained an invalid address so it was replaced with the address of Andreas Antonopoulos.
361
362    #[test]
363    fn just_address() {
364        let input = "bitcoin:1andreas3batLhQa2FawWjeyjCqyBzypd";
365        let uri = input.parse::<Uri<'_, _>>().unwrap().require_network(bitcoin::Network::Bitcoin).unwrap();
366        assert_eq!(uri.address.to_string(), "1andreas3batLhQa2FawWjeyjCqyBzypd");
367        assert!(uri.amount.is_none());
368        assert!(uri.label.is_none());
369        assert!(uri.message.is_none());
370
371        assert_eq!(uri.to_string(), input);
372    }
373
374    #[test]
375    fn address_with_name() {
376        let input = "bitcoin:1andreas3batLhQa2FawWjeyjCqyBzypd?label=Luke-Jr";
377        let uri = input.parse::<Uri<'_, _>>().unwrap().require_network(bitcoin::Network::Bitcoin).unwrap();
378        let label: Cow<'_, str> = uri.label.clone().unwrap().try_into().unwrap();
379        assert_eq!(uri.address.to_string(), "1andreas3batLhQa2FawWjeyjCqyBzypd");
380        assert_eq!(label, "Luke-Jr");
381        assert!(uri.amount.is_none());
382        assert!(uri.message.is_none());
383
384        assert_eq!(uri.to_string(), input);
385    }
386
387    #[allow(clippy::inconsistent_digit_grouping)] // Use sats/bitcoin when grouping.
388    #[test]
389    fn request_20_point_30_btc_to_luke_dash_jr() {
390        let input = "bitcoin:1andreas3batLhQa2FawWjeyjCqyBzypd?amount=20.3&label=Luke-Jr";
391        let uri = input.parse::<Uri<'_, _>>().unwrap().require_network(bitcoin::Network::Bitcoin).unwrap();
392        let label: Cow<'_, str> = uri.label.clone().unwrap().try_into().unwrap();
393        assert_eq!(uri.address.to_string(), "1andreas3batLhQa2FawWjeyjCqyBzypd");
394        assert_eq!(label, "Luke-Jr");
395        assert_eq!(uri.amount, Some(bitcoin::Amount::from_sat(20_30_000_000)));
396        assert!(uri.message.is_none());
397
398        assert_eq!(uri.to_string(), input);
399    }
400
401    #[allow(clippy::inconsistent_digit_grouping)] // Use sats/bitcoin when grouping.
402    #[test]
403    fn request_50_btc_with_message() {
404        let input = "bitcoin:1andreas3batLhQa2FawWjeyjCqyBzypd?amount=50&label=Luke-Jr&message=Donation%20for%20project%20xyz";
405        let uri = input.parse::<Uri<'_, _>>().unwrap().require_network(bitcoin::Network::Bitcoin).unwrap();
406        let label: Cow<'_, str> = uri.label.clone().unwrap().try_into().unwrap();
407        let message: Cow<'_, str> = uri.message.clone().unwrap().try_into().unwrap();
408        assert_eq!(uri.address.to_string(), "1andreas3batLhQa2FawWjeyjCqyBzypd");
409        assert_eq!(uri.amount, Some(bitcoin::Amount::from_sat(50_00_000_000)));
410        assert_eq!(label, "Luke-Jr");
411        assert_eq!(message, "Donation for project xyz");
412
413        assert_eq!(uri.to_string(), input);
414    }
415
416    #[test]
417    fn required_not_understood() {
418        let input = "bitcoin:1andreas3batLhQa2FawWjeyjCqyBzypd?req-somethingyoudontunderstand=50&req-somethingelseyoudontget=999";
419        let uri = input.parse::<Uri<'_, _>>();
420        assert!(uri.is_err());
421    }
422
423    #[test]
424    fn required_understood() {
425        let input = "bitcoin:1andreas3batLhQa2FawWjeyjCqyBzypd?somethingyoudontunderstand=50&somethingelseyoudontget=999";
426        let uri = input.parse::<Uri<'_, _>>().unwrap().require_network(bitcoin::Network::Bitcoin).unwrap();
427        assert_eq!(uri.address.to_string(), "1andreas3batLhQa2FawWjeyjCqyBzypd");
428        assert!(uri.amount.is_none());
429        assert!(uri.label.is_none());
430        assert!(uri.message.is_none());
431
432        assert_eq!(uri.to_string(), "bitcoin:1andreas3batLhQa2FawWjeyjCqyBzypd");
433    }
434
435    #[test]
436    fn label_with_rfc3986_param_separator() {
437        let input = "bitcoin:1andreas3batLhQa2FawWjeyjCqyBzypd?label=foo%26bar%20%3D%20baz/blah?;:@";
438        let uri = input.parse::<Uri<'_, _>>().unwrap().require_network(bitcoin::Network::Bitcoin).unwrap();
439        let label: Cow<'_, str> = uri.label.clone().unwrap().try_into().unwrap();
440        assert_eq!(uri.address.to_string(), "1andreas3batLhQa2FawWjeyjCqyBzypd");
441        assert_eq!(label, "foo&bar = baz/blah?;:@");
442        assert!(uri.amount.is_none());
443        assert!(uri.message.is_none());
444
445        assert_eq!(uri.to_string(), input);
446    }
447
448    #[test]
449    fn label_with_rfc3986_fragment_separator() {
450        let input = "bitcoin:1andreas3batLhQa2FawWjeyjCqyBzypd?label=foo%23bar";
451        let uri = input.parse::<Uri<'_, _>>().unwrap().require_network(bitcoin::Network::Bitcoin).unwrap();
452        let label: Cow<'_, str> = uri.label.clone().unwrap().try_into().unwrap();
453        assert_eq!(uri.address.to_string(), "1andreas3batLhQa2FawWjeyjCqyBzypd");
454        assert_eq!(label, "foo#bar");
455        assert!(uri.amount.is_none());
456        assert!(uri.message.is_none());
457
458        assert_eq!(uri.to_string(), input);
459    }
460
461    #[test]
462    fn rfc3986_empty_fragment_not_defined_in_bip21() {
463        let input = "bitcoin:1andreas3batLhQa2FawWjeyjCqyBzypd?label=foo#";
464        let uri = input.parse::<Uri<'_, _>>().unwrap().require_network(bitcoin::Network::Bitcoin).unwrap();
465        let label: Cow<'_, str> = uri.label.clone().unwrap().try_into().unwrap();
466        assert_eq!(uri.address.to_string(), "1andreas3batLhQa2FawWjeyjCqyBzypd");
467        assert_eq!(label, "foo");
468        assert!(uri.amount.is_none());
469        assert!(uri.message.is_none());
470        assert_eq!(uri.to_string(), "bitcoin:1andreas3batLhQa2FawWjeyjCqyBzypd?label=foo");
471    }
472
473    #[test]
474    fn rfc3986_non_empty_fragment_not_defined_in_bip21() {
475        let input = "bitcoin:1andreas3batLhQa2FawWjeyjCqyBzypd?label=foo#&message=not%20part%20of%20a%20message";
476        let uri = input.parse::<Uri<'_, _>>().unwrap().require_network(bitcoin::Network::Bitcoin).unwrap();
477        let label: Cow<'_, str> = uri.label.clone().unwrap().try_into().unwrap();
478        assert_eq!(uri.address.to_string(), "1andreas3batLhQa2FawWjeyjCqyBzypd");
479        assert_eq!(label, "foo");
480        assert!(uri.amount.is_none());
481        assert!(uri.message.is_none());
482        assert_eq!(uri.to_string(), "bitcoin:1andreas3batLhQa2FawWjeyjCqyBzypd?label=foo");
483    }
484
485    #[test]
486    fn bad_unicode_scheme() {
487        let input = "bitcoinö:1andreas3batLhQa2FawWjeyjCqyBzypd";
488        let uri = input.parse::<Uri<'_, _>>();
489        assert!(uri.is_err());
490    }
491}