Skip to main content

rgbinvoice/
parse.rs

1// RGB wallet library for smart contracts on Bitcoin & Lightning network
2//
3// SPDX-License-Identifier: Apache-2.0
4//
5// Written in 2019-2024 by
6//     Dr Maxim Orlovsky <orlovsky@lnp-bp.org>
7//
8// Copyright (C) 2019-2024 LNP/BP Standards Association. All rights reserved.
9//
10// Licensed under the Apache License, Version 2.0 (the "License");
11// you may not use this file except in compliance with the License.
12// You may obtain a copy of the License at
13//
14//     http://www.apache.org/licenses/LICENSE-2.0
15//
16// Unless required by applicable law or agreed to in writing, software
17// distributed under the License is distributed on an "AS IS" BASIS,
18// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
19// See the License for the specific language governing permissions and
20// limitations under the License.
21
22use std::fmt::{self, Debug, Display, Formatter};
23use std::io::{Cursor, Write};
24use std::num::ParseIntError;
25use std::str::FromStr;
26
27use baid64::{Baid64ParseError, DisplayBaid64, FromBaid64Str};
28use fluent_uri::encoding::encoder::Query;
29use fluent_uri::encoding::EStr;
30use fluent_uri::Uri;
31use indexmap::IndexMap;
32use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS};
33use rgb::bitcoin::hashes::{hash160, sha256};
34use rgb::bitcoin::key::{TweakedPublicKey, UntweakedPublicKey};
35use rgb::bitcoin::params::Params;
36use rgb::bitcoin::{Address, Network, PubkeyHash, ScriptBuf, ScriptHash, WPubkeyHash, WScriptHash};
37use rgb::{ChainNet, ContractId, SchemaId, SecretSeal};
38use strict_types::FieldName;
39
40use crate::invoice::{Beneficiary, InvoiceState, Pay2Vout, RgbInvoice, RgbTransport, XChainNet};
41
42const OMITTED: &str = "~";
43const ASSIGNMENT: &str = "assignment_name";
44const EXPIRY: &str = "expiry";
45const ENDPOINTS: &str = "endpoints";
46const TRANSPORT_SEP: char = ',';
47const TRANSPORT_HOST_SEP: &str = "://";
48const QUERY_ENCODE: &AsciiSet = &CONTROLS
49    .add(b' ')
50    .add(b'"')
51    .add(b'#')
52    .add(b'<')
53    .add(b'>')
54    .add(b'[')
55    .add(b']')
56    .add(b'&')
57    .add(b'=');
58
59#[derive(Clone, PartialEq, Eq, Debug, Display, Error, From)]
60#[display(inner)]
61pub enum TransportParseError {
62    #[display(doc_comments)]
63    /// invalid transport {0}.
64    InvalidTransport(String),
65
66    #[display(doc_comments)]
67    /// invalid transport host {0}.
68    InvalidTransportHost(String),
69}
70
71#[derive(Debug, Display, Error, From)]
72#[display(doc_comments)]
73pub enum InvoiceParseError {
74    /// invalid invoice.
75    Invalid,
76
77    /// RGB invoice must not contain any URI authority data, including empty
78    /// one.
79    Authority,
80
81    /// contract id is missing from the invoice.
82    ContractMissing,
83
84    /// schema information is missing from the invoice.
85    SchemaMissing,
86
87    /// assignment state is missing from the invoice.
88    AssignmentStateMissing,
89
90    /// beneficiary is missing from the invoice.
91    BeneficiaryMissing,
92
93    /// invalid invoice scheme {0}.
94    InvalidScheme(String),
95
96    /// no invoice transport has been provided.
97    NoTransport,
98
99    /// invalid contract ID.
100    InvalidContractId(String),
101
102    /// invalid schema {0}.
103    InvalidSchemaId(String),
104
105    /// invalid assignment state {0}.
106    InvalidAssignmentState(String),
107
108    /// invalid assignment name {0}.
109    InvalidAssignmentName(String),
110
111    /// invalid expiration timestamp {0}.
112    InvalidExpiration(String),
113
114    /// invalid network {0}
115    InvalidNetwork(Network),
116
117    /// invalid query parameter {0}.
118    InvalidQueryParam(String),
119
120    /// can't recognize beneficiary "{0}": it should be either a bitcoin address
121    /// or a blinded UTXO seal.
122    Beneficiary(String),
123
124    #[from]
125    #[display(inner)]
126    Num(ParseIntError),
127
128    /// can't recognize amount "{0}": it should be valid allocation data.
129    Data(String),
130}
131
132impl RgbInvoice {
133    fn has_params(&self) -> bool {
134        self.expiry.is_some()
135            || self.assignment_name.is_some()
136            || !self.transports.is_empty()
137            || !self.unknown_query.is_empty()
138    }
139
140    fn query_params(&self) -> IndexMap<String, String> {
141        let mut query_params: IndexMap<String, String> = IndexMap::new();
142        if let Some(ref assignment) = self.assignment_name {
143            query_params.insert(ASSIGNMENT.to_string(), assignment.to_string());
144        }
145        if let Some(expiry) = self.expiry {
146            query_params.insert(EXPIRY.to_string(), expiry.to_string());
147        }
148        if !self.transports.is_empty() {
149            let mut transports: Vec<String> = vec![];
150            for transport in self.transports.clone() {
151                transports.push(transport.to_string());
152            }
153            query_params.insert(ENDPOINTS.to_string(), transports.join(&TRANSPORT_SEP.to_string()));
154        }
155        query_params.extend(self.unknown_query.clone());
156        query_params
157    }
158}
159
160impl Display for RgbTransport {
161    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
162        match self {
163            RgbTransport::JsonRpc { tls, host } => {
164                let s = if *tls { "s" } else { "" };
165                write!(f, "rpc{s}{TRANSPORT_HOST_SEP}{host}")?;
166            }
167            RgbTransport::RestHttp { tls, host } => {
168                let s = if *tls { "s" } else { "" };
169                write!(f, "http{s}{TRANSPORT_HOST_SEP}{host}")?;
170            }
171            RgbTransport::WebSockets { tls, host } => {
172                let s = if *tls { "s" } else { "" };
173                write!(f, "ws{s}{TRANSPORT_HOST_SEP}{host}")?;
174            }
175            RgbTransport::Storm {} => {
176                write!(f, "storm{TRANSPORT_HOST_SEP}_/")?;
177            }
178        };
179        Ok(())
180    }
181}
182
183impl FromStr for RgbTransport {
184    type Err = TransportParseError;
185
186    fn from_str(s: &str) -> Result<Self, Self::Err> {
187        let tokens = s.split_once(TRANSPORT_HOST_SEP);
188        if tokens.is_none() {
189            return Err(TransportParseError::InvalidTransport(s.to_string()));
190        }
191        let (trans_type, host) = tokens.unwrap();
192        if host.is_empty() {
193            return Err(TransportParseError::InvalidTransportHost(host.to_string()));
194        }
195        let host = host.to_string();
196        let transport = match trans_type {
197            "rpc" => RgbTransport::JsonRpc { tls: false, host },
198            "rpcs" => RgbTransport::JsonRpc { tls: true, host },
199            "http" => RgbTransport::RestHttp { tls: false, host },
200            "https" => RgbTransport::RestHttp { tls: true, host },
201            "ws" => RgbTransport::WebSockets { tls: false, host },
202            "wss" => RgbTransport::WebSockets { tls: true, host },
203            "storm" => RgbTransport::Storm {},
204            _ => return Err(TransportParseError::InvalidTransport(s.to_string())),
205        };
206        Ok(transport)
207    }
208}
209
210impl Display for XChainNet<Beneficiary> {
211    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
212        write!(f, "{}:", self.chain_network().prefix())?;
213        match self.into_inner() {
214            Beneficiary::BlindedSeal(seal) => Display::fmt(&seal, f),
215            Beneficiary::WitnessVout(pay2vout, internal_pk) => {
216                write!(
217                    f,
218                    "{}{}",
219                    pay2vout.to_baid64_string(),
220                    if let Some(ipk) = internal_pk { format!("+{ipk}") } else { s!("") }
221                )
222            }
223        }
224    }
225}
226
227/// Internal address content. Consists of serialized hashes or x-only key value.
228#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug, From)]
229pub enum AddressPayload {
230    /// P2PKH payload.
231    #[from]
232    Pkh(PubkeyHash),
233
234    /// P2SH and SegWit nested (proprietary) P2WPKH/WSH-in-P2SH payloads.
235    #[from]
236    Sh(ScriptHash),
237
238    /// P2WPKH payload.
239    #[from]
240    Wpkh(WPubkeyHash),
241
242    /// P2WSH payload.
243    #[from]
244    Wsh(WScriptHash),
245
246    /// P2TR payload.
247    #[from]
248    Tr(TweakedPublicKey),
249}
250
251/// Errors creating address from scriptPubkey.
252#[derive(Clone, Eq, PartialEq, Debug, Display, Error)]
253#[display(doc_comments)]
254pub enum AddressError {
255    /// scriptPubkey contains invalid BIP340 output pubkey.
256    InvalidTaprootKey,
257    /// scriptPubkey can't be represented with any known address standard.
258    UnsupportedScriptPubkey,
259}
260
261impl AddressPayload {
262    pub fn into_address(self, network: Network) -> Address {
263        // since we don't allow to construct AddressPayload without checking its script, the unwrap
264        // is safe here
265        Address::from_script(&self.to_script(), Params::new(network)).unwrap()
266    }
267
268    /// Constructs payload from a given `ScriptBuf`. Fails on future
269    /// (post-taproot) witness types with `None`.
270    pub fn from_script(script: &ScriptBuf) -> Result<Self, AddressError> {
271        Ok(if script.is_p2pkh() {
272            let mut bytes = [0u8; 20];
273            bytes.copy_from_slice(&script.as_bytes()[3..23]);
274            AddressPayload::Pkh(PubkeyHash::from_raw_hash(*hash160::Hash::from_bytes_ref(&bytes)))
275        } else if script.is_p2sh() {
276            let mut bytes = [0u8; 20];
277            bytes.copy_from_slice(&script.as_bytes()[2..22]);
278            AddressPayload::Sh(ScriptHash::from_raw_hash(*hash160::Hash::from_bytes_ref(&bytes)))
279        } else if script.is_p2wpkh() {
280            let mut bytes = [0u8; 20];
281            bytes.copy_from_slice(&script.as_bytes()[2..]);
282            AddressPayload::Wpkh(WPubkeyHash::from_raw_hash(*hash160::Hash::from_bytes_ref(&bytes)))
283        } else if script.is_p2wsh() {
284            let mut bytes = [0u8; 32];
285            bytes.copy_from_slice(&script.as_bytes()[2..]);
286            AddressPayload::Wsh(WScriptHash::from_raw_hash(*sha256::Hash::from_bytes_ref(&bytes)))
287        } else if script.is_p2tr() {
288            let mut bytes = [0u8; 32];
289            bytes.copy_from_slice(&script.as_bytes()[2..]);
290            AddressPayload::Tr(TweakedPublicKey::dangerous_assume_tweaked(
291                UntweakedPublicKey::from_slice(&bytes)
292                    .map_err(|_| AddressError::InvalidTaprootKey)?,
293            ))
294        } else {
295            return Err(AddressError::UnsupportedScriptPubkey);
296        })
297    }
298
299    /// Returns script corresponding to the given address.
300    pub fn to_script(self) -> ScriptBuf {
301        match self {
302            AddressPayload::Pkh(hash) => ScriptBuf::new_p2pkh(&hash),
303            AddressPayload::Sh(hash) => ScriptBuf::new_p2sh(&hash),
304            AddressPayload::Wpkh(hash) => ScriptBuf::new_p2wpkh(&hash),
305            AddressPayload::Wsh(hash) => ScriptBuf::new_p2wsh(&hash),
306            AddressPayload::Tr(output_key) => ScriptBuf::new_p2tr_tweaked(output_key),
307        }
308    }
309}
310
311impl DisplayBaid64<33> for Pay2Vout {
312    const HRI: &'static str = "wvout";
313    const CHUNKING: bool = true;
314    const PREFIX: bool = true;
315    const EMBED_CHECKSUM: bool = true;
316    const MNEMONIC: bool = false;
317
318    fn to_baid64_payload(&self) -> [u8; 33] {
319        let mut payload = [0u8; 33];
320        // tmp stack array to store the tr payload to resolve lifetime issue
321        let schnorr_pk: [u8; 32];
322        let (addr_type, spk) = match &**self {
323            AddressPayload::Pkh(pkh) => (Self::P2PKH, pkh.as_ref()),
324            AddressPayload::Sh(sh) => (Self::P2SH, sh.as_ref()),
325            AddressPayload::Wpkh(wpkh) => (Self::P2WPKH, wpkh.as_ref()),
326            AddressPayload::Wsh(wsh) => (Self::P2WSH, wsh.as_ref()),
327            AddressPayload::Tr(tr) => {
328                schnorr_pk = tr.serialize();
329                (Self::P2TR, &schnorr_pk[..])
330            }
331        };
332        payload[0] = addr_type;
333        Cursor::new(&mut payload[1..])
334            .write_all(spk)
335            .expect("address payload always less than 32 bytes");
336        payload
337    }
338}
339
340impl Display for Pay2Vout {
341    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { self.fmt_baid64(f) }
342}
343impl FromBaid64Str<33> for Pay2Vout {}
344impl FromStr for Pay2Vout {
345    type Err = Baid64ParseError;
346    fn from_str(s: &str) -> Result<Self, Self::Err> { Self::from_baid64_str(s) }
347}
348
349impl FromStr for XChainNet<Beneficiary> {
350    type Err = InvoiceParseError;
351
352    fn from_str(s: &str) -> Result<Self, Self::Err> {
353        let Some((cn, beneficiary)) = s.split_once(':') else {
354            return Err(InvoiceParseError::Beneficiary(s!("missing beneficiary HRI")));
355        };
356        let cn =
357            ChainNet::from_str(cn).map_err(|e| InvoiceParseError::Beneficiary(e.to_string()))?;
358        if let Ok(seal) = SecretSeal::from_str(beneficiary) {
359            return Ok(XChainNet::with(cn, Beneficiary::BlindedSeal(seal)));
360        }
361
362        let (pay2vout, internal_pk) = beneficiary
363            .split_once("+")
364            .map(|(p, i)| (p, Some(i)))
365            .unwrap_or((beneficiary, None));
366
367        let pay2vout = Pay2Vout::from_str(pay2vout)
368            .map_err(|e| InvoiceParseError::Beneficiary(e.to_string()))?;
369
370        let internal_pk = match internal_pk {
371            None => None,
372            Some(i) => {
373                if i.is_empty() {
374                    return Err(InvoiceParseError::Beneficiary(s!("missing internal pk")));
375                }
376                Some(
377                    UntweakedPublicKey::from_str(i)
378                        .map_err(|_| InvoiceParseError::Beneficiary(s!("invalid internal pk")))?,
379                )
380            }
381        };
382
383        Ok(XChainNet::with(cn, Beneficiary::WitnessVout(pay2vout, internal_pk)))
384    }
385}
386
387impl Display for RgbInvoice {
388    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
389        if let Some(contract) = self.contract {
390            let id = if f.alternate() {
391                contract.to_string().replace('-', "")
392            } else {
393                contract.to_string()
394            };
395            write!(f, "{id}/")?;
396        } else {
397            write!(f, "rgb:{OMITTED}/")?;
398        }
399        if let Some(schema) = self.schema {
400            let schema_str = format!("{schema:-#}");
401            let id = if f.alternate() { schema_str.replace('-', "") } else { schema_str };
402            write!(f, "{id}/")?;
403        } else {
404            write!(f, "{OMITTED}/")?;
405        }
406        if let Some(ref assignment_state) = self.assignment_state {
407            write!(f, "{assignment_state}/")?;
408        } else {
409            write!(f, "{OMITTED}/")?;
410        }
411        let beneficiary = if f.alternate() {
412            self.beneficiary.to_string().replace('-', "")
413        } else {
414            self.beneficiary.to_string()
415        };
416        f.write_str(&beneficiary)?;
417        if self.has_params() {
418            f.write_str("?")?;
419        }
420        let query_params = self.query_params();
421        for (key, val) in query_params.iter().take(1) {
422            write!(
423                f,
424                "{}={}",
425                utf8_percent_encode(key, QUERY_ENCODE),
426                utf8_percent_encode(val, QUERY_ENCODE)
427            )?;
428        }
429        for (key, val) in query_params.iter().skip(1) {
430            write!(
431                f,
432                "&{}={}",
433                utf8_percent_encode(key, QUERY_ENCODE),
434                utf8_percent_encode(val, QUERY_ENCODE)
435            )?;
436        }
437        Ok(())
438    }
439}
440
441impl FromStr for RgbInvoice {
442    type Err = InvoiceParseError;
443
444    fn from_str(s: &str) -> Result<Self, Self::Err> {
445        let uri = Uri::parse(s).map_err(|_| InvoiceParseError::Invalid)?;
446
447        let scheme = uri.scheme();
448        if scheme.as_str() != "rgb" {
449            return Err(InvoiceParseError::InvalidScheme(scheme.to_string()));
450        }
451
452        let path = uri.path();
453        if path.is_absolute() || uri.authority().is_some() {
454            return Err(InvoiceParseError::Authority);
455        }
456
457        let mut path = path.split('/');
458
459        let Some(contract_id_str) = path.next() else {
460            return Err(InvoiceParseError::ContractMissing);
461        };
462        let contract = match ContractId::from_str(contract_id_str.as_str()) {
463            Ok(cid) => Some(cid),
464            Err(_) if contract_id_str.as_str() == OMITTED => None,
465            Err(_) => {
466                return Err(InvoiceParseError::InvalidContractId(contract_id_str.to_string()));
467            }
468        };
469
470        let Some(schema_str) = path.next() else {
471            return Err(InvoiceParseError::SchemaMissing);
472        };
473        let schema = match SchemaId::from_str(schema_str.as_ref()) {
474            Ok(i) => Some(i),
475            Err(_) if schema_str.as_str() == OMITTED => None,
476            Err(_) => return Err(InvoiceParseError::InvalidSchemaId(schema_str.to_string())),
477        };
478
479        let Some(assignment_str) = path.next() else {
480            return Err(InvoiceParseError::AssignmentStateMissing);
481        };
482        let assignment_state = match InvoiceState::from_str(assignment_str.as_ref()) {
483            Ok(i) => Some(i),
484            Err(_) if assignment_str.as_str() == OMITTED => None,
485            Err(_) => {
486                return Err(InvoiceParseError::InvalidAssignmentState(assignment_str.to_string()))
487            }
488        };
489
490        let Some(beneficiary_str) = path.next() else {
491            return Err(InvoiceParseError::BeneficiaryMissing);
492        };
493        let beneficiary = XChainNet::<Beneficiary>::from_str(beneficiary_str.as_ref())?;
494        let mut query_params = map_query_params(&uri)?;
495
496        let transports = if let Some(endpoints) = query_params.shift_remove(ENDPOINTS) {
497            let tokens = endpoints.split(TRANSPORT_SEP);
498            let mut transport_vec: Vec<RgbTransport> = vec![];
499            for token in tokens {
500                transport_vec.push(
501                    RgbTransport::from_str(token)
502                        .map_err(|e| InvoiceParseError::InvalidQueryParam(e.to_string()))?,
503                );
504            }
505            transport_vec
506        } else {
507            vec![]
508        };
509
510        let mut assignment_name = None;
511        if let Some(assignment) = query_params.shift_remove(ASSIGNMENT) {
512            let name = FieldName::try_from(assignment.clone())
513                .map_err(|_| InvoiceParseError::InvalidAssignmentName(assignment))?;
514            assignment_name = Some(name);
515        }
516
517        let mut expiry = None;
518        if let Some(exp) = query_params.shift_remove(EXPIRY) {
519            let timestamp = exp
520                .parse::<i64>()
521                .map_err(|e| InvoiceParseError::InvalidExpiration(e.to_string()))?;
522            expiry = Some(timestamp);
523        }
524
525        Ok(RgbInvoice {
526            transports,
527            contract,
528            schema,
529            assignment_name,
530            beneficiary,
531            assignment_state,
532            expiry,
533            unknown_query: query_params,
534        })
535    }
536}
537
538fn percent_decode(estr: &EStr<Query>) -> Result<String, InvoiceParseError> {
539    Ok(estr
540        .decode()
541        .into_string()
542        .map_err(|e| InvoiceParseError::InvalidQueryParam(e.to_string()))?
543        .to_string())
544}
545
546fn map_query_params(uri: &Uri<&str>) -> Result<IndexMap<String, String>, InvoiceParseError> {
547    let mut map: IndexMap<String, String> = IndexMap::new();
548    if let Some(q) = uri.query() {
549        let params = q.split('&');
550        for p in params {
551            if let Some((k, v)) = p.split_once('=') {
552                map.insert(percent_decode(k)?, percent_decode(v)?);
553            } else {
554                return Err(InvoiceParseError::InvalidQueryParam(p.to_string()));
555            }
556        }
557    }
558    Ok(map)
559}
560
561#[cfg(test)]
562mod test {
563    use rgb::bitcoin::hashes::{hash160, sha256};
564
565    use super::*;
566    use crate::{Allocation, Amount, NonFungible};
567
568    #[test]
569    fn parse() {
570        // nia parameters
571        let invoice_str = "rgb:eIbQx5Am-XRDjj01-RM~5eo7-rv2nluD-OnBJRAy-S9~Yfts/\
572                           XvmU3d4_nQQ8S7oagbXi07x5vjMm7P~ERukQNX6SC4M/BF/bc:utxob:\
573                           4vm1CX2Z-K8hMo59-e7dgGBS-Jka7mYn-Xe~yP85-yUiHHxr-aVlYa";
574        let invoice = RgbInvoice::from_str(invoice_str).unwrap();
575        assert_eq!(invoice.assignment_state, Some(InvoiceState::Amount(Amount::from(100u64))));
576        assert_eq!(invoice.to_string(), invoice_str);
577        assert_eq!(format!("{invoice:#}"), invoice_str.replace('-', ""));
578
579        // uda parameters
580        let invoice_str = "rgb:tx8NOyGe-NkPZex~-U0J_1om-CfrOeoO-7di9xZb-vT3nxyo/\
581                           XvmU3d4_nQQ8S7oagbXi07x5vjMm7P~ERukQNX6SC4M/1@0/bc:utxob:\
582                           4vm1CX2Z-K8hMo59-e7dgGBS-Jka7mYn-Xe~yP85-yUiHHxr-aVlYa";
583        let invoice = RgbInvoice::from_str(invoice_str).unwrap();
584        assert_eq!(
585            invoice.assignment_state,
586            Some(InvoiceState::Data(NonFungible::FractionedToken(Allocation::with(0, 1))))
587        );
588        assert_eq!(invoice.to_string(), invoice_str);
589        assert_eq!(format!("{invoice:#}"), invoice_str.replace('-', ""));
590
591        // witness vout without internal pk
592        let invoice_str = "rgb:eIbQx5Am-XRDjj01-RM~5eo7-rv2nluD-OnBJRAy-S9~Yfts/\
593                           XvmU3d4_nQQ8S7oagbXi07x5vjMm7P~ERukQNX6SC4M/Sa/bc:wvout:\
594                           A8cJ7Ww3-NIzADo3-Tzp_5aD-7CTBWmA-AAAAAAA-AAAAAAA-ALSQkcw";
595        let invoice = RgbInvoice::from_str(invoice_str).unwrap();
596        assert_eq!(invoice.to_string(), invoice_str);
597
598        // witness vout with internal pk
599        let invoice_str = "rgb:eIbQx5Am-XRDjj01-RM~5eo7-rv2nluD-OnBJRAy-S9~Yfts/\
600                           XvmU3d4_nQQ8S7oagbXi07x5vjMm7P~ERukQNX6SC4M/Sa/bc:wvout:\
601                           A8cJ7Ww3-NIzADo3-Tzp_5aD-7CTBWmA-AAAAAAA-AAAAAAA-ALSQkcw\
602                           +750f58bcca0fdb11891e7979d829b8c56e0963dba08c44f54a256cf7dbc09caf";
603        let invoice = RgbInvoice::from_str(invoice_str).unwrap();
604        assert_eq!(invoice.to_string(), invoice_str);
605
606        // no amount
607        let invoice_str = "rgb:eIbQx5Am-XRDjj01-RM~5eo7-rv2nluD-OnBJRAy-S9~Yfts/\
608                           XvmU3d4_nQQ8S7oagbXi07x5vjMm7P~ERukQNX6SC4M/~/bc:utxob:\
609                           4vm1CX2Z-K8hMo59-e7dgGBS-Jka7mYn-Xe~yP85-yUiHHxr-aVlYa";
610        let invoice = RgbInvoice::from_str(invoice_str).unwrap();
611        assert_eq!(invoice.assignment_state, None);
612        assert_eq!(invoice.to_string(), invoice_str);
613
614        // no allocation
615        let invoice_str = "rgb:eIbQx5Am-XRDjj01-RM~5eo7-rv2nluD-OnBJRAy-S9~Yfts/\
616                           V8ujLLtH2k2QSmaDpZI3o06ACIm2UNT0TZl11FiqRuY/~/bc:utxob:\
617                           4vm1CX2Z-K8hMo59-e7dgGBS-Jka7mYn-Xe~yP85-yUiHHxr-aVlYa";
618        let invoice = RgbInvoice::from_str(invoice_str).unwrap();
619        assert_eq!(invoice.assignment_state, None);
620        assert_eq!(invoice.to_string(), invoice_str);
621
622        // no contract ID
623        let invoice_str = "rgb:~/XvmU3d4_nQQ8S7oagbXi07x5vjMm7P~ERukQNX6SC4M/~/bc:utxob:\
624                           4vm1CX2Z-K8hMo59-e7dgGBS-Jka7mYn-Xe~yP85-yUiHHxr-aVlYa";
625        let invoice = RgbInvoice::from_str(invoice_str).unwrap();
626        assert_eq!(invoice.to_string(), invoice_str);
627
628        // no contract ID nor schema
629        let invoice_str =
630            "rgb:~/~/~/bc:utxob:4vm1CX2Z-K8hMo59-e7dgGBS-Jka7mYn-Xe~yP85-yUiHHxr-aVlYa";
631        let invoice = RgbInvoice::from_str(invoice_str).unwrap();
632        assert_eq!(invoice.to_string(), invoice_str);
633
634        // contract ID provided but no schema
635        let invoice_str = "rgb:eIbQx5Am-XRDjj01-RM~5eo7-rv2nluD-OnBJRAy-S9~Yfts/~/~/bc:utxob:\
636                           4vm1CX2Z-K8hMo59-e7dgGBS-Jka7mYn-Xe~yP85-yUiHHxr-aVlYa";
637        let invoice = RgbInvoice::from_str(invoice_str).unwrap();
638        assert_eq!(invoice.to_string(), invoice_str);
639
640        // invalid contract ID
641        let invalid_contract_id = "invalid";
642        let invoice_str = format!(
643            "rgb:{invalid_contract_id}/XvmU3d4_nQQ8S7oagbXi07x5vjMm7P~ERukQNX6SC4M/bc:utxob:\
644             4vm1CX2Z-K8hMo59-e7dgGBS-Jka7mYn-Xe~yP85-yUiHHxr-aVlYa"
645        );
646        let result = RgbInvoice::from_str(&invoice_str);
647        assert!(matches!(result,
648                Err(InvoiceParseError::InvalidContractId(c)) if c == invalid_contract_id));
649
650        // with assignment name
651        let assignment_name = "assetOwner";
652        let invoice_str = format!(
653            "rgb:3NoxsLum-cRPebTV-gTZY8qY-KS20lx7-OqgtBls-t7muan4/\
654             XvmU3d4_nQQ8S7oagbXi07x5vjMm7P~ERukQNX6SC4M/BF/bc:utxob:\
655             4vm1CX2Z-K8hMo59-e7dgGBS-Jka7mYn-Xe~yP85-yUiHHxr-aVlYa?{ASSIGNMENT}={assignment_name}"
656        );
657        let invoice = RgbInvoice::from_str(&invoice_str).unwrap();
658        assert_eq!(invoice.assignment_name, Some(FieldName::from(assignment_name)));
659        assert_eq!(invoice.to_string(), invoice_str);
660
661        // bad assignment_name
662        let assignment_name = "";
663        let invoice_str = format!(
664            "rgb:3NoxsLum-cRPebTV-gTZY8qY-KS20lx7-OqgtBls-t7muan4/\
665             XvmU3d4_nQQ8S7oagbXi07x5vjMm7P~ERukQNX6SC4M/BF/bc:utxob:\
666             4vm1CX2Z-K8hMo59-e7dgGBS-Jka7mYn-Xe~yP85-yUiHHxr-aVlYa?{ASSIGNMENT}={assignment_name}"
667        );
668        let result = RgbInvoice::from_str(&invoice_str);
669        assert!(matches!(result, Err(InvoiceParseError::InvalidAssignmentName(_))));
670
671        // with expiration
672        let invoice_str = "rgb:3NoxsLum-cRPebTV-gTZY8qY-KS20lx7-OqgtBls-t7muan4/\
673                           XvmU3d4_nQQ8S7oagbXi07x5vjMm7P~ERukQNX6SC4M/BF/bc:utxob:\
674                           4vm1CX2Z-K8hMo59-e7dgGBS-Jka7mYn-Xe~yP85-yUiHHxr-aVlYa?\
675                           expiry=1682086371";
676        let invoice = RgbInvoice::from_str(invoice_str).unwrap();
677        assert_eq!(invoice.to_string(), invoice_str);
678
679        // bad expiration
680        let invoice_str = "rgb:3NoxsLum-cRPebTV-gTZY8qY-KS20lx7-OqgtBls-t7muan4/\
681                           XvmU3d4_nQQ8S7oagbXi07x5vjMm7P~ERukQNX6SC4M/BF/bc:utxob:\
682                           4vm1CX2Z-K8hMo59-e7dgGBS-Jka7mYn-Xe~yP85-yUiHHxr-aVlYa?expiry=six";
683        let result = RgbInvoice::from_str(invoice_str);
684        assert!(matches!(result, Err(InvoiceParseError::InvalidExpiration(_))));
685
686        // with bad query parameter
687        let invoice_str = "rgb:3NoxsLum-cRPebTV-gTZY8qY-KS20lx7-OqgtBls-t7muan4/\
688                           XvmU3d4_nQQ8S7oagbXi07x5vjMm7P~ERukQNX6SC4M/BF/bc:utxob:\
689                           4vm1CX2Z-K8hMo59-e7dgGBS-Jka7mYn-Xe~yP85-yUiHHxr-aVlYa?expiry";
690        let result = RgbInvoice::from_str(invoice_str);
691        assert!(matches!(result, Err(InvoiceParseError::InvalidQueryParam(_))));
692
693        // with an unknown query parameter
694        let invoice_str = "rgb:3NoxsLum-cRPebTV-gTZY8qY-KS20lx7-OqgtBls-t7muan4/\
695                           XvmU3d4_nQQ8S7oagbXi07x5vjMm7P~ERukQNX6SC4M/BF/bc:utxob:\
696                           4vm1CX2Z-K8hMo59-e7dgGBS-Jka7mYn-Xe~yP85-yUiHHxr-aVlYa?unknown=new";
697        let invoice = RgbInvoice::from_str(invoice_str).unwrap();
698        assert_eq!(invoice.to_string(), invoice_str);
699
700        // with two unknown query parameters
701        let invoice_str = "rgb:3NoxsLum-cRPebTV-gTZY8qY-KS20lx7-OqgtBls-t7muan4/\
702                           XvmU3d4_nQQ8S7oagbXi07x5vjMm7P~ERukQNX6SC4M/BF/bc:utxob:\
703                           4vm1CX2Z-K8hMo59-e7dgGBS-Jka7mYn-Xe~yP85-yUiHHxr-aVlYa?unknown=new&\
704                           another=new";
705        let invoice = RgbInvoice::from_str(invoice_str).unwrap();
706        assert_eq!(invoice.to_string(), invoice_str);
707
708        // with expiration and an unknown query parameter
709        let invoice_str = "rgb:3NoxsLum-cRPebTV-gTZY8qY-KS20lx7-OqgtBls-t7muan4/\
710                           XvmU3d4_nQQ8S7oagbXi07x5vjMm7P~ERukQNX6SC4M/BF/bc:utxob:\
711                           4vm1CX2Z-K8hMo59-e7dgGBS-Jka7mYn-Xe~yP85-yUiHHxr-aVlYa?\
712                           expiry=1682086371&unknown=new";
713        let invoice = RgbInvoice::from_str(invoice_str).unwrap();
714        assert_eq!(invoice.to_string(), invoice_str);
715
716        // with an unknown query parameter containing percent-encoded text
717        let invoice_base = "rgb:3NoxsLum-cRPebTV-gTZY8qY-KS20lx7-OqgtBls-t7muan4/\
718                            XvmU3d4_nQQ8S7oagbXi07x5vjMm7P~ERukQNX6SC4M/BF/bc:utxob:\
719                            4vm1CX2Z-K8hMo59-e7dgGBS-Jka7mYn-Xe~yP85-yUiHHxr-aVlYa?";
720        let query_key_encoded = ":@-%20%23";
721        let query_key_decoded = ":@- #";
722        let query_val_encoded = "?/.%26%3D";
723        let query_val_decoded = "?/.&=";
724        let invoice =
725            RgbInvoice::from_str(&format!("{invoice_base}{query_key_encoded}={query_val_encoded}"))
726                .unwrap();
727        let query_params = invoice.query_params();
728        assert_eq!(query_params[query_key_decoded], query_val_decoded);
729        assert_eq!(
730            invoice.to_string(),
731            format!("{invoice_base}{query_key_encoded}={query_val_encoded}")
732        );
733
734        // no scheme
735        let invoice_str = "eIbQx5Am-XRDjj01-RM~5eo7-rv2nluD-OnBJRAy-S9~Yfts/~/bc:utxob:\
736                           4vm1CX2Z-K8hMo59-e7dgGBS-Jka7mYn-Xe~yP85-yUiHHxr-aVlYa";
737        let result = RgbInvoice::from_str(invoice_str);
738        assert!(matches!(result, Err(InvoiceParseError::Invalid)));
739
740        // invalid scheme
741        let invoice_str = "bad:2WBcas9-yjzEvGufY-9GEgnyMj7-beMNMWA8r-sPHtV1nPU-TMsGMQX/~/bc:utxob:\
742                           4vm1CX2Z-K8hMo59-e7dgGBS-Jka7mYn-Xe~yP85-yUiHHxr-aVlYa";
743        let result = RgbInvoice::from_str(invoice_str);
744        assert!(matches!(result, Err(InvoiceParseError::InvalidScheme(_))));
745
746        // empty transport endpoint specification
747        let invoice_str = "rgb:3NoxsLum-cRPebTV-gTZY8qY-KS20lx7-OqgtBls-t7muan4/\
748                           XvmU3d4_nQQ8S7oagbXi07x5vjMm7P~ERukQNX6SC4M/BF/bc:utxob:\
749                           4vm1CX2Z-K8hMo59-e7dgGBS-Jka7mYn-Xe~yP85-yUiHHxr-aVlYa?endpoints=";
750        let result = RgbInvoice::from_str(invoice_str);
751        assert!(matches!(result, Err(InvoiceParseError::InvalidQueryParam(_))));
752
753        // invalid transport endpoint specification
754        let invoice_str = "rgb:3NoxsLum-cRPebTV-gTZY8qY-KS20lx7-OqgtBls-t7muan4/\
755                           XvmU3d4_nQQ8S7oagbXi07x5vjMm7P~ERukQNX6SC4M/BF/bc:utxob:\
756                           4vm1CX2Z-K8hMo59-e7dgGBS-Jka7mYn-Xe~yP85-yUiHHxr-aVlYa?endpoints=bad";
757        let result = RgbInvoice::from_str(invoice_str);
758        assert!(matches!(result, Err(InvoiceParseError::InvalidQueryParam(_))));
759
760        // invalid transport variant
761        let invoice_str = "rgb:3NoxsLum-cRPebTV-gTZY8qY-KS20lx7-OqgtBls-t7muan4/\
762                           XvmU3d4_nQQ8S7oagbXi07x5vjMm7P~ERukQNX6SC4M/BF/bc:utxob:\
763                           4vm1CX2Z-K8hMo59-e7dgGBS-Jka7mYn-Xe~yP85-yUiHHxr-aVlYa?endpoints=rpca:/\
764                           /host.example.com";
765        let result = RgbInvoice::from_str(invoice_str);
766        assert!(matches!(result, Err(InvoiceParseError::InvalidQueryParam(_))));
767
768        // rgb-rpc variant
769        let invoice_str = "rgb:3NoxsLum-cRPebTV-gTZY8qY-KS20lx7-OqgtBls-t7muan4/\
770                           XvmU3d4_nQQ8S7oagbXi07x5vjMm7P~ERukQNX6SC4M/BF/bc:utxob:\
771                           4vm1CX2Z-K8hMo59-e7dgGBS-Jka7mYn-Xe~yP85-yUiHHxr-aVlYa?endpoints=rpc://\
772                           host.example.com";
773        let invoice = RgbInvoice::from_str(invoice_str).unwrap();
774        assert_eq!(invoice.transports, vec![RgbTransport::JsonRpc {
775            tls: false,
776            host: "host.example.com".to_string()
777        }]);
778        assert_eq!(invoice.to_string(), invoice_str);
779
780        // rgb-rpc variant, host containing authentication, "-" characters and port
781        let invoice_str = "rgb:3NoxsLum-cRPebTV-gTZY8qY-KS20lx7-OqgtBls-t7muan4/\
782                           XvmU3d4_nQQ8S7oagbXi07x5vjMm7P~ERukQNX6SC4M/BF/bc:utxob:\
783                           4vm1CX2Z-K8hMo59-e7dgGBS-Jka7mYn-Xe~yP85-yUiHHxr-aVlYa?endpoints=rpcs:/\
784                           /user:pass@host-1.ex-ample.com:1234";
785        let invoice = RgbInvoice::from_str(invoice_str).unwrap();
786        assert_eq!(invoice.transports, vec![RgbTransport::JsonRpc {
787            tls: true,
788            host: "user:pass@host-1.ex-ample.com:1234".to_string()
789        }]);
790        assert_eq!(invoice.to_string(), invoice_str);
791
792        // rgb-rpc variant, IPv6 host
793        let invoice_str = "rgb:3NoxsLum-cRPebTV-gTZY8qY-KS20lx7-OqgtBls-t7muan4/\
794                           XvmU3d4_nQQ8S7oagbXi07x5vjMm7P~ERukQNX6SC4M/BF/bc:utxob:\
795                           4vm1CX2Z-K8hMo59-e7dgGBS-Jka7mYn-Xe~yP85-yUiHHxr-aVlYa?endpoints=rpcs:/\
796                           /%5B2001:db8::1%5D:1234";
797        let invoice = RgbInvoice::from_str(invoice_str).unwrap();
798        assert_eq!(invoice.transports, vec![RgbTransport::JsonRpc {
799            tls: true,
800            host: "[2001:db8::1]:1234".to_string()
801        }]);
802        assert_eq!(invoice.to_string(), invoice_str);
803
804        // rgb-rpc variant with missing host
805        let invoice_str = "rgb:3NoxsLum-cRPebTV-gTZY8qY-KS20lx7-OqgtBls-t7muan4/\
806                           XvmU3d4_nQQ8S7oagbXi07x5vjMm7P~ERukQNX6SC4M/BF/bc:utxob:\
807                           4vm1CX2Z-K8hMo59-e7dgGBS-Jka7mYn-Xe~yP85-yUiHHxr-aVlYa?endpoints=rpc://";
808        let result = RgbInvoice::from_str(invoice_str);
809        assert!(matches!(result, Err(InvoiceParseError::InvalidQueryParam(_))));
810
811        // rgb-rpc variant with invalid separator
812        let invoice_str = "rgb:3NoxsLum-cRPebTV-gTZY8qY-KS20lx7-OqgtBls-t7muan4/\
813                           XvmU3d4_nQQ8S7oagbXi07x5vjMm7P~ERukQNX6SC4M/BF/bc:utxob:\
814                           4vm1CX2Z-K8hMo59-e7dgGBS-Jka7mYn-Xe~yP85-yUiHHxr-aVlYa?endpoints=rpc/\
815                           host.example.com";
816        let result = RgbInvoice::from_str(invoice_str);
817        assert!(matches!(result, Err(InvoiceParseError::InvalidQueryParam(_))));
818
819        // rgb-rpc variant with invalid transport host specification
820        let invoice_str = "rgb:3NoxsLum-cRPebTV-gTZY8qY-KS20lx7-OqgtBls-t7muan4/\
821                           XvmU3d4_nQQ8S7oagbXi07x5vjMm7P~ERukQNX6SC4M/BF/bc:utxob:\
822                           4vm1CX2Z-K8hMo59-e7dgGBS-Jka7mYn-Xe~yP85-yUiHHxr-aVlYa?endpoints=rpc://\
823                           ho]t";
824        let result = RgbInvoice::from_str(invoice_str);
825        assert!(matches!(result, Err(InvoiceParseError::Invalid)));
826
827        // rgb+http variant
828        let invoice_str = "rgb:\
829                           3NoxsLum-cRPebTV-gTZY8qY-KS20lx7-OqgtBls-t7muan4/XvmU3d4_nQQ8S7oagbXi07x5vjMm7P~ERukQNX6SC4M/\
830                           BF/bc:utxob:4vm1CX2Z-K8hMo59-e7dgGBS-Jka7mYn-Xe~yP85-yUiHHxr-aVlYa?endpoints=https://\
831                           host.example.com";
832        let invoice = RgbInvoice::from_str(invoice_str).unwrap();
833        let transports = vec![RgbTransport::RestHttp {
834            tls: true,
835            host: "host.example.com".to_string(),
836        }];
837        assert_eq!(invoice.transports, transports);
838        assert_eq!(invoice.to_string(), invoice_str);
839
840        // rgb+ws variant
841        let invoice_str = "rgb:3NoxsLum-cRPebTV-gTZY8qY-KS20lx7-OqgtBls-t7muan4/\
842                           XvmU3d4_nQQ8S7oagbXi07x5vjMm7P~ERukQNX6SC4M/BF/bc:utxob:\
843                           4vm1CX2Z-K8hMo59-e7dgGBS-Jka7mYn-Xe~yP85-yUiHHxr-aVlYa?endpoints=wss://\
844                           host.example.com";
845        let invoice = RgbInvoice::from_str(invoice_str).unwrap();
846        let transports = vec![RgbTransport::WebSockets {
847            tls: true,
848            host: "host.example.com".to_string(),
849        }];
850        assert_eq!(invoice.transports, transports);
851        assert_eq!(invoice.to_string(), invoice_str);
852
853        // rgb+storm variant
854        let invoice_str = "rgb:3NoxsLum-cRPebTV-gTZY8qY-KS20lx7-OqgtBls-t7muan4/\
855                           XvmU3d4_nQQ8S7oagbXi07x5vjMm7P~ERukQNX6SC4M/BF/bc:utxob:\
856                           4vm1CX2Z-K8hMo59-e7dgGBS-Jka7mYn-Xe~yP85-yUiHHxr-aVlYa?endpoints=storm:\
857                           //_/";
858        let invoice = RgbInvoice::from_str(invoice_str).unwrap();
859        let transports = vec![RgbTransport::Storm {}];
860        assert_eq!(invoice.transports, transports);
861        assert_eq!(invoice.to_string(), invoice_str);
862
863        // multiple transports
864        let invoice_str = "rgb:\
865                           3NoxsLum-cRPebTV-gTZY8qY-KS20lx7-OqgtBls-t7muan4/XvmU3d4_nQQ8S7oagbXi07x5vjMm7P~ERukQNX6SC4M/\
866                           BF/bc:utxob:4vm1CX2Z-K8hMo59-e7dgGBS-Jka7mYn-Xe~yP85-yUiHHxr-aVlYa?endpoints=rpcs://\
867                           host1.example.com,http://host2.example.com,ws://host3.example.com";
868        let invoice = RgbInvoice::from_str(invoice_str).unwrap();
869        let transports = vec![
870            RgbTransport::JsonRpc {
871                tls: true,
872                host: "host1.example.com".to_string(),
873            },
874            RgbTransport::RestHttp {
875                tls: false,
876                host: "host2.example.com".to_string(),
877            },
878            RgbTransport::WebSockets {
879                tls: false,
880                host: "host3.example.com".to_string(),
881            },
882        ];
883        assert_eq!(invoice.transports, transports);
884        assert_eq!(invoice.to_string(), invoice_str);
885
886        // empty transport parse error
887        let result = RgbTransport::from_str("");
888        assert!(matches!(result, Err(TransportParseError::InvalidTransport(_))));
889
890        // invalid transport parse error
891        let result = RgbTransport::from_str("bad");
892        assert!(matches!(result, Err(TransportParseError::InvalidTransport(_))));
893
894        // invalid transport variant parse error
895        let result = RgbTransport::from_str("rpca://host.example.com");
896        assert!(matches!(result, Err(TransportParseError::InvalidTransport(_))));
897
898        // rgb-rpc variant with missing host parse error
899        let result = RgbTransport::from_str("rpc://");
900        assert!(matches!(result, Err(TransportParseError::InvalidTransportHost(_))));
901
902        // rgb-rpc variant with invalid separator parse error
903        let result = RgbTransport::from_str("rpc/host.example.com");
904        assert!(matches!(result, Err(TransportParseError::InvalidTransport(_))));
905
906        // invalid witness vout: invalid length of identifier wvout
907        let invoice_str = "rgb:3NoxsLum-cRPebTV-gTZY8qY-KS20lx7-OqgtBls-t7muan4/\
908                           XvmU3d4_nQQ8S7oagbXi07x5vjMm7P~ERukQNX6SC4M/BF/bc:wvout:\
909                           +750f58bcca0fdb11891e7979d829b8c56e0963dba08c44f54a256cf7dbc09caf";
910        let result = RgbInvoice::from_str(invoice_str);
911        assert!(matches!(result, Err(InvoiceParseError::Beneficiary(_))));
912
913        // invalid witness vout: missing beneficiary HRI
914        let invoice_str = "rgb:3NoxsLum-cRPebTV-gTZY8qY-KS20lx7-OqgtBls-t7muan4/\
915                           XvmU3d4_nQQ8S7oagbXi07x5vjMm7P~ERukQNX6SC4M/BF/\
916                           750f58bcca0fdb11891e7979d829b8c56e0963dba08c44f54a256cf7dbc09caf";
917        let result = RgbInvoice::from_str(invoice_str);
918        assert!(matches!(result, Err(InvoiceParseError::Beneficiary(_))));
919
920        // invalid witness vout: invalid chain-network pair
921        let invoice_str = "rgb:3NoxsLum-cRPebTV-gTZY8qY-KS20lx7-OqgtBls-t7muan4/\
922                           XvmU3d4_nQQ8S7oagbXi07x5vjMm7P~ERukQNX6SC4M/BF/:\
923                           +750f58bcca0fdb11891e7979d829b8c56e0963dba08c44f54a256cf7dbc09caf";
924        let result = RgbInvoice::from_str(invoice_str);
925        assert!(matches!(result, Err(InvoiceParseError::Beneficiary(_))));
926
927        // invalid witness vout: invalid internal pk
928        let invoice_str = "rgb:3NoxsLum-cRPebTV-gTZY8qY-KS20lx7-OqgtBls-t7muan4/\
929                           XvmU3d4_nQQ8S7oagbXi07x5vjMm7P~ERukQNX6SC4M/BF/bc:wvout:\
930                           BYWmQlmL-$i5Co3j-LtTvxSr-53!\
931                           Brv7-fc7ZntC-ha988ci-jqKOj4Q+750f58bcca0fdb11891e7979";
932        let result = RgbInvoice::from_str(invoice_str);
933        assert!(matches!(result, Err(InvoiceParseError::Beneficiary(_))));
934
935        // invalid witness vout: missing internal pk
936        let invoice_str = "rgb:3NoxsLum-cRPebTV-gTZY8qY-KS20lx7-OqgtBls-t7muan4/\
937                           XvmU3d4_nQQ8S7oagbXi07x5vjMm7P~ERukQNX6SC4M/BF/bc:wvout:\
938                           BYWmQlmL-$i5Co3j-LtTvxSr-53!Brv7-fc7ZntC-ha988ci-jqKOj4Q+";
939        let result = RgbInvoice::from_str(invoice_str);
940        assert!(matches!(result, Err(InvoiceParseError::Beneficiary(_))));
941    }
942
943    #[test]
944    fn pay2vout_parse() {
945        let p = Pay2Vout::new(AddressPayload::Pkh(PubkeyHash::from_raw_hash(
946            *hash160::Hash::from_bytes_ref(&[0xff; 20]),
947        )));
948        assert_eq!(Pay2Vout::from_str(&p.to_string()).unwrap(), p);
949
950        let p = Pay2Vout::new(AddressPayload::Sh(ScriptHash::from_raw_hash(
951            *hash160::Hash::from_bytes_ref(&[0xff; 20]),
952        )));
953        assert_eq!(Pay2Vout::from_str(&p.to_string()).unwrap(), p);
954
955        let p = Pay2Vout::new(AddressPayload::Wpkh(WPubkeyHash::from_raw_hash(
956            *hash160::Hash::from_bytes_ref(&[0xff; 20]),
957        )));
958        assert_eq!(Pay2Vout::from_str(&p.to_string()).unwrap(), p);
959
960        let p = Pay2Vout::new(AddressPayload::Wsh(WScriptHash::from_raw_hash(
961            *sha256::Hash::from_bytes_ref(&[0xff; 32]),
962        )));
963        assert_eq!(Pay2Vout::from_str(&p.to_string()).unwrap(), p);
964
965        let p = Pay2Vout::new(AddressPayload::Tr(TweakedPublicKey::dangerous_assume_tweaked(
966            UntweakedPublicKey::from_slice(&[
967                0x85, 0xa6, 0x42, 0x59, 0x8b, 0xfe, 0x2e, 0x42, 0xa3, 0x78, 0xcb, 0xb5, 0x3b, 0xf1,
968                0x4a, 0xbe, 0x77, 0xf8, 0x1a, 0xef, 0xed, 0xf7, 0x3b, 0x66, 0x7b, 0x42, 0x85, 0xaf,
969                0x7c, 0xf1, 0xc8, 0xa3,
970            ])
971            .unwrap(),
972        )));
973        assert_eq!(Pay2Vout::from_str(&p.to_string()).unwrap(), p);
974    }
975}