1use 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 InvalidTransport(String),
65
66 #[display(doc_comments)]
67 InvalidTransportHost(String),
69}
70
71#[derive(Debug, Display, Error, From)]
72#[display(doc_comments)]
73pub enum InvoiceParseError {
74 Invalid,
76
77 Authority,
80
81 ContractMissing,
83
84 SchemaMissing,
86
87 AssignmentStateMissing,
89
90 BeneficiaryMissing,
92
93 InvalidScheme(String),
95
96 NoTransport,
98
99 InvalidContractId(String),
101
102 InvalidSchemaId(String),
104
105 InvalidAssignmentState(String),
107
108 InvalidAssignmentName(String),
110
111 InvalidExpiration(String),
113
114 InvalidNetwork(Network),
116
117 InvalidQueryParam(String),
119
120 Beneficiary(String),
123
124 #[from]
125 #[display(inner)]
126 Num(ParseIntError),
127
128 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 != vec![RgbTransport::UnspecifiedMeans]
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 != vec![RgbTransport::UnspecifiedMeans] {
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 RgbTransport::UnspecifiedMeans => {}
179 };
180 Ok(())
181 }
182}
183
184impl FromStr for RgbTransport {
185 type Err = TransportParseError;
186
187 fn from_str(s: &str) -> Result<Self, Self::Err> {
188 let tokens = s.split_once(TRANSPORT_HOST_SEP);
189 if tokens.is_none() {
190 return Err(TransportParseError::InvalidTransport(s.to_string()));
191 }
192 let (trans_type, host) = tokens.unwrap();
193 if host.is_empty() {
194 return Err(TransportParseError::InvalidTransportHost(host.to_string()));
195 }
196 let host = host.to_string();
197 let transport = match trans_type {
198 "rpc" => RgbTransport::JsonRpc { tls: false, host },
199 "rpcs" => RgbTransport::JsonRpc { tls: true, host },
200 "http" => RgbTransport::RestHttp { tls: false, host },
201 "https" => RgbTransport::RestHttp { tls: true, host },
202 "ws" => RgbTransport::WebSockets { tls: false, host },
203 "wss" => RgbTransport::WebSockets { tls: true, host },
204 "storm" => RgbTransport::Storm {},
205 _ => return Err(TransportParseError::InvalidTransport(s.to_string())),
206 };
207 Ok(transport)
208 }
209}
210
211impl Display for XChainNet<Beneficiary> {
212 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
213 write!(f, "{}:", self.chain_network().prefix())?;
214 match self.into_inner() {
215 Beneficiary::BlindedSeal(seal) => Display::fmt(&seal, f),
216 Beneficiary::WitnessVout(pay2vout, internal_pk) => {
217 write!(
218 f,
219 "{}{}",
220 pay2vout.to_baid64_string(),
221 if let Some(ipk) = internal_pk { format!("+{ipk}") } else { s!("") }
222 )
223 }
224 }
225 }
226}
227
228#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug, From)]
230pub enum AddressPayload {
231 #[from]
233 Pkh(PubkeyHash),
234
235 #[from]
237 Sh(ScriptHash),
238
239 #[from]
241 Wpkh(WPubkeyHash),
242
243 #[from]
245 Wsh(WScriptHash),
246
247 #[from]
249 Tr(TweakedPublicKey),
250}
251
252#[derive(Clone, Eq, PartialEq, Debug, Display, Error)]
254#[display(doc_comments)]
255pub enum AddressError {
256 InvalidTaprootKey,
258 UnsupportedScriptPubkey,
260}
261
262impl AddressPayload {
263 pub fn into_address(self, network: Network) -> Address {
264 Address::from_script(&self.to_script(), Params::new(network)).unwrap()
267 }
268
269 pub fn from_script(script: &ScriptBuf) -> Result<Self, AddressError> {
272 Ok(if script.is_p2pkh() {
273 let mut bytes = [0u8; 20];
274 bytes.copy_from_slice(&script.as_bytes()[3..23]);
275 AddressPayload::Pkh(PubkeyHash::from_raw_hash(*hash160::Hash::from_bytes_ref(&bytes)))
276 } else if script.is_p2sh() {
277 let mut bytes = [0u8; 20];
278 bytes.copy_from_slice(&script.as_bytes()[2..22]);
279 AddressPayload::Sh(ScriptHash::from_raw_hash(*hash160::Hash::from_bytes_ref(&bytes)))
280 } else if script.is_p2wpkh() {
281 let mut bytes = [0u8; 20];
282 bytes.copy_from_slice(&script.as_bytes()[2..]);
283 AddressPayload::Wpkh(WPubkeyHash::from_raw_hash(*hash160::Hash::from_bytes_ref(&bytes)))
284 } else if script.is_p2wsh() {
285 let mut bytes = [0u8; 32];
286 bytes.copy_from_slice(&script.as_bytes()[2..]);
287 AddressPayload::Wsh(WScriptHash::from_raw_hash(*sha256::Hash::from_bytes_ref(&bytes)))
288 } else if script.is_p2tr() {
289 let mut bytes = [0u8; 32];
290 bytes.copy_from_slice(&script.as_bytes()[2..]);
291 AddressPayload::Tr(TweakedPublicKey::dangerous_assume_tweaked(
292 UntweakedPublicKey::from_slice(&bytes)
293 .map_err(|_| AddressError::InvalidTaprootKey)?,
294 ))
295 } else {
296 return Err(AddressError::UnsupportedScriptPubkey);
297 })
298 }
299
300 pub fn to_script(self) -> ScriptBuf {
302 match self {
303 AddressPayload::Pkh(hash) => ScriptBuf::new_p2pkh(&hash),
304 AddressPayload::Sh(hash) => ScriptBuf::new_p2sh(&hash),
305 AddressPayload::Wpkh(hash) => ScriptBuf::new_p2wpkh(&hash),
306 AddressPayload::Wsh(hash) => ScriptBuf::new_p2wsh(&hash),
307 AddressPayload::Tr(output_key) => ScriptBuf::new_p2tr_tweaked(output_key),
308 }
309 }
310}
311
312impl DisplayBaid64<33> for Pay2Vout {
313 const HRI: &'static str = "wvout";
314 const CHUNKING: bool = true;
315 const PREFIX: bool = true;
316 const EMBED_CHECKSUM: bool = true;
317 const MNEMONIC: bool = false;
318
319 fn to_baid64_payload(&self) -> [u8; 33] {
320 let mut payload = [0u8; 33];
321 let schnorr_pk: [u8; 32];
323 let (addr_type, spk) = match &**self {
324 AddressPayload::Pkh(pkh) => (Self::P2PKH, pkh.as_ref()),
325 AddressPayload::Sh(sh) => (Self::P2SH, sh.as_ref()),
326 AddressPayload::Wpkh(wpkh) => (Self::P2WPKH, wpkh.as_ref()),
327 AddressPayload::Wsh(wsh) => (Self::P2WSH, wsh.as_ref()),
328 AddressPayload::Tr(tr) => {
329 schnorr_pk = tr.serialize();
330 (Self::P2TR, &schnorr_pk[..])
331 }
332 };
333 payload[0] = addr_type;
334 Cursor::new(&mut payload[1..])
335 .write_all(spk)
336 .expect("address payload always less than 32 bytes");
337 payload
338 }
339}
340
341impl Display for Pay2Vout {
342 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { self.fmt_baid64(f) }
343}
344impl FromBaid64Str<33> for Pay2Vout {}
345impl FromStr for Pay2Vout {
346 type Err = Baid64ParseError;
347 fn from_str(s: &str) -> Result<Self, Self::Err> { Self::from_baid64_str(s) }
348}
349
350impl FromStr for XChainNet<Beneficiary> {
351 type Err = InvoiceParseError;
352
353 fn from_str(s: &str) -> Result<Self, Self::Err> {
354 let Some((cn, beneficiary)) = s.split_once(':') else {
355 return Err(InvoiceParseError::Beneficiary(s!("missing beneficiary HRI")));
356 };
357 let cn =
358 ChainNet::from_str(cn).map_err(|e| InvoiceParseError::Beneficiary(e.to_string()))?;
359 if let Ok(seal) = SecretSeal::from_str(beneficiary) {
360 return Ok(XChainNet::with(cn, Beneficiary::BlindedSeal(seal)));
361 }
362
363 let (pay2vout, internal_pk) = beneficiary
364 .split_once("+")
365 .map(|(p, i)| (p, Some(i)))
366 .unwrap_or((beneficiary, None));
367
368 let pay2vout = Pay2Vout::from_str(pay2vout)
369 .map_err(|e| InvoiceParseError::Beneficiary(e.to_string()))?;
370
371 let internal_pk = match internal_pk {
372 None => None,
373 Some(i) => {
374 if i.is_empty() {
375 return Err(InvoiceParseError::Beneficiary(s!("missing internal pk")));
376 }
377 Some(
378 UntweakedPublicKey::from_str(i)
379 .map_err(|_| InvoiceParseError::Beneficiary(s!("invalid internal pk")))?,
380 )
381 }
382 };
383
384 Ok(XChainNet::with(cn, Beneficiary::WitnessVout(pay2vout, internal_pk)))
385 }
386}
387
388impl Display for RgbInvoice {
389 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
390 if let Some(contract) = self.contract {
391 let id = if f.alternate() {
392 contract.to_string().replace('-', "")
393 } else {
394 contract.to_string()
395 };
396 write!(f, "{id}/")?;
397 } else {
398 write!(f, "rgb:{OMITTED}/")?;
399 }
400 if let Some(schema) = self.schema {
401 let schema_str = format!("{schema:-#}");
402 let id = if f.alternate() { schema_str.replace('-', "") } else { schema_str };
403 write!(f, "{id}/")?;
404 } else {
405 write!(f, "{OMITTED}/")?;
406 }
407 if let Some(ref assignment_state) = self.assignment_state {
408 write!(f, "{assignment_state}/")?;
409 } else {
410 write!(f, "{OMITTED}/")?;
411 }
412 let beneficiary = if f.alternate() {
413 self.beneficiary.to_string().replace('-', "")
414 } else {
415 self.beneficiary.to_string()
416 };
417 f.write_str(&beneficiary)?;
418 if self.has_params() {
419 f.write_str("?")?;
420 }
421 let query_params = self.query_params();
422 for (key, val) in query_params.iter().take(1) {
423 write!(
424 f,
425 "{}={}",
426 utf8_percent_encode(key, QUERY_ENCODE),
427 utf8_percent_encode(val, QUERY_ENCODE)
428 )?;
429 }
430 for (key, val) in query_params.iter().skip(1) {
431 write!(
432 f,
433 "&{}={}",
434 utf8_percent_encode(key, QUERY_ENCODE),
435 utf8_percent_encode(val, QUERY_ENCODE)
436 )?;
437 }
438 Ok(())
439 }
440}
441
442impl FromStr for RgbInvoice {
443 type Err = InvoiceParseError;
444
445 fn from_str(s: &str) -> Result<Self, Self::Err> {
446 let uri = Uri::parse(s).map_err(|_| InvoiceParseError::Invalid)?;
447
448 let scheme = uri.scheme();
449 if scheme.as_str() != "rgb" {
450 return Err(InvoiceParseError::InvalidScheme(scheme.to_string()));
451 }
452
453 let path = uri.path();
454 if path.is_absolute() || uri.authority().is_some() {
455 return Err(InvoiceParseError::Authority);
456 }
457
458 let mut path = path.split('/');
459
460 let Some(contract_id_str) = path.next() else {
461 return Err(InvoiceParseError::ContractMissing);
462 };
463 let contract = match ContractId::from_str(contract_id_str.as_str()) {
464 Ok(cid) => Some(cid),
465 Err(_) if contract_id_str.as_str() == OMITTED => None,
466 Err(_) => {
467 return Err(InvoiceParseError::InvalidContractId(contract_id_str.to_string()));
468 }
469 };
470
471 let Some(schema_str) = path.next() else {
472 return Err(InvoiceParseError::SchemaMissing);
473 };
474 let schema = match SchemaId::from_str(schema_str.as_ref()) {
475 Ok(i) => Some(i),
476 Err(_) if schema_str.as_str() == OMITTED => None,
477 Err(_) => return Err(InvoiceParseError::InvalidSchemaId(schema_str.to_string())),
478 };
479
480 let Some(assignment_str) = path.next() else {
481 return Err(InvoiceParseError::AssignmentStateMissing);
482 };
483 let assignment_state = match InvoiceState::from_str(assignment_str.as_ref()) {
484 Ok(i) => Some(i),
485 Err(_) if assignment_str.as_str() == OMITTED => None,
486 Err(_) => {
487 return Err(InvoiceParseError::InvalidAssignmentState(assignment_str.to_string()))
488 }
489 };
490
491 let Some(beneficiary_str) = path.next() else {
492 return Err(InvoiceParseError::BeneficiaryMissing);
493 };
494 let beneficiary = XChainNet::<Beneficiary>::from_str(beneficiary_str.as_ref())?;
495 let mut query_params = map_query_params(&uri)?;
496
497 let transports = if let Some(endpoints) = query_params.shift_remove(ENDPOINTS) {
498 let tokens = endpoints.split(TRANSPORT_SEP);
499 let mut transport_vec: Vec<RgbTransport> = vec![];
500 for token in tokens {
501 transport_vec.push(
502 RgbTransport::from_str(token)
503 .map_err(|e| InvoiceParseError::InvalidQueryParam(e.to_string()))?,
504 );
505 }
506 transport_vec
507 } else {
508 vec![RgbTransport::UnspecifiedMeans]
509 };
510
511 let mut assignment_name = None;
512 if let Some(assignment) = query_params.shift_remove(ASSIGNMENT) {
513 let name = FieldName::try_from(assignment.clone())
514 .map_err(|_| InvoiceParseError::InvalidAssignmentName(assignment))?;
515 assignment_name = Some(name);
516 }
517
518 let mut expiry = None;
519 if let Some(exp) = query_params.shift_remove(EXPIRY) {
520 let timestamp = exp
521 .parse::<i64>()
522 .map_err(|e| InvoiceParseError::InvalidExpiration(e.to_string()))?;
523 expiry = Some(timestamp);
524 }
525
526 Ok(RgbInvoice {
527 transports,
528 contract,
529 schema,
530 assignment_name,
531 beneficiary,
532 assignment_state,
533 expiry,
534 unknown_query: query_params,
535 })
536 }
537}
538
539fn percent_decode(estr: &EStr<Query>) -> Result<String, InvoiceParseError> {
540 Ok(estr
541 .decode()
542 .into_string()
543 .map_err(|e| InvoiceParseError::InvalidQueryParam(e.to_string()))?
544 .to_string())
545}
546
547fn map_query_params(uri: &Uri<&str>) -> Result<IndexMap<String, String>, InvoiceParseError> {
548 let mut map: IndexMap<String, String> = IndexMap::new();
549 if let Some(q) = uri.query() {
550 let params = q.split('&');
551 for p in params {
552 if let Some((k, v)) = p.split_once('=') {
553 map.insert(percent_decode(k)?, percent_decode(v)?);
554 } else {
555 return Err(InvoiceParseError::InvalidQueryParam(p.to_string()));
556 }
557 }
558 }
559 Ok(map)
560}
561
562#[cfg(test)]
563mod test {
564 use rgb::bitcoin::hashes::{hash160, sha256};
565
566 use super::*;
567 use crate::{Allocation, Amount, NonFungible};
568
569 #[test]
570 fn parse() {
571 let invoice_str = "rgb:eIbQx5Am-XRDjj01-RM~5eo7-rv2nluD-OnBJRAy-S9~Yfts/\
573 XvmU3d4_nQQ8S7oagbXi07x5vjMm7P~ERukQNX6SC4M/BF/bc:utxob:\
574 4vm1CX2Z-K8hMo59-e7dgGBS-Jka7mYn-Xe~yP85-yUiHHxr-aVlYa";
575 let invoice = RgbInvoice::from_str(invoice_str).unwrap();
576 assert_eq!(invoice.assignment_state, Some(InvoiceState::Amount(Amount::from(100u64))));
577 assert_eq!(invoice.to_string(), invoice_str);
578 assert_eq!(format!("{invoice:#}"), invoice_str.replace('-', ""));
579
580 let invoice_str = "rgb:tx8NOyGe-NkPZex~-U0J_1om-CfrOeoO-7di9xZb-vT3nxyo/\
582 XvmU3d4_nQQ8S7oagbXi07x5vjMm7P~ERukQNX6SC4M/1@0/bc:utxob:\
583 4vm1CX2Z-K8hMo59-e7dgGBS-Jka7mYn-Xe~yP85-yUiHHxr-aVlYa";
584 let invoice = RgbInvoice::from_str(invoice_str).unwrap();
585 assert_eq!(
586 invoice.assignment_state,
587 Some(InvoiceState::Data(NonFungible::FractionedToken(Allocation::with(0, 1))))
588 );
589 assert_eq!(invoice.to_string(), invoice_str);
590 assert_eq!(format!("{invoice:#}"), invoice_str.replace('-', ""));
591
592 let invoice_str = "rgb:eIbQx5Am-XRDjj01-RM~5eo7-rv2nluD-OnBJRAy-S9~Yfts/\
594 XvmU3d4_nQQ8S7oagbXi07x5vjMm7P~ERukQNX6SC4M/Sa/bc:wvout:\
595 A8cJ7Ww3-NIzADo3-Tzp_5aD-7CTBWmA-AAAAAAA-AAAAAAA-ALSQkcw";
596 let invoice = RgbInvoice::from_str(invoice_str).unwrap();
597 assert_eq!(invoice.to_string(), invoice_str);
598
599 let invoice_str = "rgb:eIbQx5Am-XRDjj01-RM~5eo7-rv2nluD-OnBJRAy-S9~Yfts/\
601 XvmU3d4_nQQ8S7oagbXi07x5vjMm7P~ERukQNX6SC4M/Sa/bc:wvout:\
602 A8cJ7Ww3-NIzADo3-Tzp_5aD-7CTBWmA-AAAAAAA-AAAAAAA-ALSQkcw\
603 +750f58bcca0fdb11891e7979d829b8c56e0963dba08c44f54a256cf7dbc09caf";
604 let invoice = RgbInvoice::from_str(invoice_str).unwrap();
605 assert_eq!(invoice.to_string(), invoice_str);
606
607 let invoice_str = "rgb:eIbQx5Am-XRDjj01-RM~5eo7-rv2nluD-OnBJRAy-S9~Yfts/\
609 XvmU3d4_nQQ8S7oagbXi07x5vjMm7P~ERukQNX6SC4M/~/bc:utxob:\
610 4vm1CX2Z-K8hMo59-e7dgGBS-Jka7mYn-Xe~yP85-yUiHHxr-aVlYa";
611 let invoice = RgbInvoice::from_str(invoice_str).unwrap();
612 assert_eq!(invoice.assignment_state, None);
613 assert_eq!(invoice.to_string(), invoice_str);
614
615 let invoice_str = "rgb:eIbQx5Am-XRDjj01-RM~5eo7-rv2nluD-OnBJRAy-S9~Yfts/\
617 V8ujLLtH2k2QSmaDpZI3o06ACIm2UNT0TZl11FiqRuY/~/bc:utxob:\
618 4vm1CX2Z-K8hMo59-e7dgGBS-Jka7mYn-Xe~yP85-yUiHHxr-aVlYa";
619 let invoice = RgbInvoice::from_str(invoice_str).unwrap();
620 assert_eq!(invoice.assignment_state, None);
621 assert_eq!(invoice.to_string(), invoice_str);
622
623 let invoice_str = "rgb:~/XvmU3d4_nQQ8S7oagbXi07x5vjMm7P~ERukQNX6SC4M/~/bc:utxob:\
625 4vm1CX2Z-K8hMo59-e7dgGBS-Jka7mYn-Xe~yP85-yUiHHxr-aVlYa";
626 let invoice = RgbInvoice::from_str(invoice_str).unwrap();
627 assert_eq!(invoice.to_string(), invoice_str);
628
629 let invoice_str =
631 "rgb:~/~/~/bc:utxob:4vm1CX2Z-K8hMo59-e7dgGBS-Jka7mYn-Xe~yP85-yUiHHxr-aVlYa";
632 let invoice = RgbInvoice::from_str(invoice_str).unwrap();
633 assert_eq!(invoice.to_string(), invoice_str);
634
635 let invoice_str = "rgb:eIbQx5Am-XRDjj01-RM~5eo7-rv2nluD-OnBJRAy-S9~Yfts/~/~/bc:utxob:\
637 4vm1CX2Z-K8hMo59-e7dgGBS-Jka7mYn-Xe~yP85-yUiHHxr-aVlYa";
638 let invoice = RgbInvoice::from_str(invoice_str).unwrap();
639 assert_eq!(invoice.to_string(), invoice_str);
640
641 let invalid_contract_id = "invalid";
643 let invoice_str = format!(
644 "rgb:{invalid_contract_id}/XvmU3d4_nQQ8S7oagbXi07x5vjMm7P~ERukQNX6SC4M/bc:utxob:\
645 4vm1CX2Z-K8hMo59-e7dgGBS-Jka7mYn-Xe~yP85-yUiHHxr-aVlYa"
646 );
647 let result = RgbInvoice::from_str(&invoice_str);
648 assert!(matches!(result,
649 Err(InvoiceParseError::InvalidContractId(c)) if c == invalid_contract_id));
650
651 let assignment_name = "assetOwner";
653 let invoice_str = format!(
654 "rgb:3NoxsLum-cRPebTV-gTZY8qY-KS20lx7-OqgtBls-t7muan4/\
655 XvmU3d4_nQQ8S7oagbXi07x5vjMm7P~ERukQNX6SC4M/BF/bc:utxob:\
656 4vm1CX2Z-K8hMo59-e7dgGBS-Jka7mYn-Xe~yP85-yUiHHxr-aVlYa?{ASSIGNMENT}={assignment_name}"
657 );
658 let invoice = RgbInvoice::from_str(&invoice_str).unwrap();
659 assert_eq!(invoice.assignment_name, Some(FieldName::from(assignment_name)));
660 assert_eq!(invoice.to_string(), invoice_str);
661
662 let assignment_name = "";
664 let invoice_str = format!(
665 "rgb:3NoxsLum-cRPebTV-gTZY8qY-KS20lx7-OqgtBls-t7muan4/\
666 XvmU3d4_nQQ8S7oagbXi07x5vjMm7P~ERukQNX6SC4M/BF/bc:utxob:\
667 4vm1CX2Z-K8hMo59-e7dgGBS-Jka7mYn-Xe~yP85-yUiHHxr-aVlYa?{ASSIGNMENT}={assignment_name}"
668 );
669 let result = RgbInvoice::from_str(&invoice_str);
670 assert!(matches!(result, Err(InvoiceParseError::InvalidAssignmentName(_))));
671
672 let invoice_str = "rgb:3NoxsLum-cRPebTV-gTZY8qY-KS20lx7-OqgtBls-t7muan4/\
674 XvmU3d4_nQQ8S7oagbXi07x5vjMm7P~ERukQNX6SC4M/BF/bc:utxob:\
675 4vm1CX2Z-K8hMo59-e7dgGBS-Jka7mYn-Xe~yP85-yUiHHxr-aVlYa?\
676 expiry=1682086371";
677 let invoice = RgbInvoice::from_str(invoice_str).unwrap();
678 assert_eq!(invoice.to_string(), invoice_str);
679
680 let invoice_str = "rgb:3NoxsLum-cRPebTV-gTZY8qY-KS20lx7-OqgtBls-t7muan4/\
682 XvmU3d4_nQQ8S7oagbXi07x5vjMm7P~ERukQNX6SC4M/BF/bc:utxob:\
683 4vm1CX2Z-K8hMo59-e7dgGBS-Jka7mYn-Xe~yP85-yUiHHxr-aVlYa?expiry=six";
684 let result = RgbInvoice::from_str(invoice_str);
685 assert!(matches!(result, Err(InvoiceParseError::InvalidExpiration(_))));
686
687 let invoice_str = "rgb:3NoxsLum-cRPebTV-gTZY8qY-KS20lx7-OqgtBls-t7muan4/\
689 XvmU3d4_nQQ8S7oagbXi07x5vjMm7P~ERukQNX6SC4M/BF/bc:utxob:\
690 4vm1CX2Z-K8hMo59-e7dgGBS-Jka7mYn-Xe~yP85-yUiHHxr-aVlYa?expiry";
691 let result = RgbInvoice::from_str(invoice_str);
692 assert!(matches!(result, Err(InvoiceParseError::InvalidQueryParam(_))));
693
694 let invoice_str = "rgb:3NoxsLum-cRPebTV-gTZY8qY-KS20lx7-OqgtBls-t7muan4/\
696 XvmU3d4_nQQ8S7oagbXi07x5vjMm7P~ERukQNX6SC4M/BF/bc:utxob:\
697 4vm1CX2Z-K8hMo59-e7dgGBS-Jka7mYn-Xe~yP85-yUiHHxr-aVlYa?unknown=new";
698 let invoice = RgbInvoice::from_str(invoice_str).unwrap();
699 assert_eq!(invoice.to_string(), invoice_str);
700
701 let invoice_str = "rgb:3NoxsLum-cRPebTV-gTZY8qY-KS20lx7-OqgtBls-t7muan4/\
703 XvmU3d4_nQQ8S7oagbXi07x5vjMm7P~ERukQNX6SC4M/BF/bc:utxob:\
704 4vm1CX2Z-K8hMo59-e7dgGBS-Jka7mYn-Xe~yP85-yUiHHxr-aVlYa?unknown=new&\
705 another=new";
706 let invoice = RgbInvoice::from_str(invoice_str).unwrap();
707 assert_eq!(invoice.to_string(), invoice_str);
708
709 let invoice_str = "rgb:3NoxsLum-cRPebTV-gTZY8qY-KS20lx7-OqgtBls-t7muan4/\
711 XvmU3d4_nQQ8S7oagbXi07x5vjMm7P~ERukQNX6SC4M/BF/bc:utxob:\
712 4vm1CX2Z-K8hMo59-e7dgGBS-Jka7mYn-Xe~yP85-yUiHHxr-aVlYa?\
713 expiry=1682086371&unknown=new";
714 let invoice = RgbInvoice::from_str(invoice_str).unwrap();
715 assert_eq!(invoice.to_string(), invoice_str);
716
717 let invoice_base = "rgb:3NoxsLum-cRPebTV-gTZY8qY-KS20lx7-OqgtBls-t7muan4/\
719 XvmU3d4_nQQ8S7oagbXi07x5vjMm7P~ERukQNX6SC4M/BF/bc:utxob:\
720 4vm1CX2Z-K8hMo59-e7dgGBS-Jka7mYn-Xe~yP85-yUiHHxr-aVlYa?";
721 let query_key_encoded = ":@-%20%23";
722 let query_key_decoded = ":@- #";
723 let query_val_encoded = "?/.%26%3D";
724 let query_val_decoded = "?/.&=";
725 let invoice =
726 RgbInvoice::from_str(&format!("{invoice_base}{query_key_encoded}={query_val_encoded}"))
727 .unwrap();
728 let query_params = invoice.query_params();
729 assert_eq!(query_params[query_key_decoded], query_val_decoded);
730 assert_eq!(
731 invoice.to_string(),
732 format!("{invoice_base}{query_key_encoded}={query_val_encoded}")
733 );
734
735 let invoice_str = "eIbQx5Am-XRDjj01-RM~5eo7-rv2nluD-OnBJRAy-S9~Yfts/~/bc:utxob:\
737 4vm1CX2Z-K8hMo59-e7dgGBS-Jka7mYn-Xe~yP85-yUiHHxr-aVlYa";
738 let result = RgbInvoice::from_str(invoice_str);
739 assert!(matches!(result, Err(InvoiceParseError::Invalid)));
740
741 let invoice_str = "bad:2WBcas9-yjzEvGufY-9GEgnyMj7-beMNMWA8r-sPHtV1nPU-TMsGMQX/~/bc:utxob:\
743 4vm1CX2Z-K8hMo59-e7dgGBS-Jka7mYn-Xe~yP85-yUiHHxr-aVlYa";
744 let result = RgbInvoice::from_str(invoice_str);
745 assert!(matches!(result, Err(InvoiceParseError::InvalidScheme(_))));
746
747 let invoice_str = "rgb:3NoxsLum-cRPebTV-gTZY8qY-KS20lx7-OqgtBls-t7muan4/\
749 XvmU3d4_nQQ8S7oagbXi07x5vjMm7P~ERukQNX6SC4M/BF/bc:utxob:\
750 4vm1CX2Z-K8hMo59-e7dgGBS-Jka7mYn-Xe~yP85-yUiHHxr-aVlYa?endpoints=";
751 let result = RgbInvoice::from_str(invoice_str);
752 assert!(matches!(result, Err(InvoiceParseError::InvalidQueryParam(_))));
753
754 let invoice_str = "rgb:3NoxsLum-cRPebTV-gTZY8qY-KS20lx7-OqgtBls-t7muan4/\
756 XvmU3d4_nQQ8S7oagbXi07x5vjMm7P~ERukQNX6SC4M/BF/bc:utxob:\
757 4vm1CX2Z-K8hMo59-e7dgGBS-Jka7mYn-Xe~yP85-yUiHHxr-aVlYa?endpoints=bad";
758 let result = RgbInvoice::from_str(invoice_str);
759 assert!(matches!(result, Err(InvoiceParseError::InvalidQueryParam(_))));
760
761 let invoice_str = "rgb:3NoxsLum-cRPebTV-gTZY8qY-KS20lx7-OqgtBls-t7muan4/\
763 XvmU3d4_nQQ8S7oagbXi07x5vjMm7P~ERukQNX6SC4M/BF/bc:utxob:\
764 4vm1CX2Z-K8hMo59-e7dgGBS-Jka7mYn-Xe~yP85-yUiHHxr-aVlYa?endpoints=rpca:/\
765 /host.example.com";
766 let result = RgbInvoice::from_str(invoice_str);
767 assert!(matches!(result, Err(InvoiceParseError::InvalidQueryParam(_))));
768
769 let invoice_str = "rgb:3NoxsLum-cRPebTV-gTZY8qY-KS20lx7-OqgtBls-t7muan4/\
771 XvmU3d4_nQQ8S7oagbXi07x5vjMm7P~ERukQNX6SC4M/BF/bc:utxob:\
772 4vm1CX2Z-K8hMo59-e7dgGBS-Jka7mYn-Xe~yP85-yUiHHxr-aVlYa?endpoints=rpc://\
773 host.example.com";
774 let invoice = RgbInvoice::from_str(invoice_str).unwrap();
775 assert_eq!(invoice.transports, vec![RgbTransport::JsonRpc {
776 tls: false,
777 host: "host.example.com".to_string()
778 }]);
779 assert_eq!(invoice.to_string(), invoice_str);
780
781 let invoice_str = "rgb:3NoxsLum-cRPebTV-gTZY8qY-KS20lx7-OqgtBls-t7muan4/\
783 XvmU3d4_nQQ8S7oagbXi07x5vjMm7P~ERukQNX6SC4M/BF/bc:utxob:\
784 4vm1CX2Z-K8hMo59-e7dgGBS-Jka7mYn-Xe~yP85-yUiHHxr-aVlYa?endpoints=rpcs:/\
785 /user:pass@host-1.ex-ample.com:1234";
786 let invoice = RgbInvoice::from_str(invoice_str).unwrap();
787 assert_eq!(invoice.transports, vec![RgbTransport::JsonRpc {
788 tls: true,
789 host: "user:pass@host-1.ex-ample.com:1234".to_string()
790 }]);
791 assert_eq!(invoice.to_string(), invoice_str);
792
793 let invoice_str = "rgb:3NoxsLum-cRPebTV-gTZY8qY-KS20lx7-OqgtBls-t7muan4/\
795 XvmU3d4_nQQ8S7oagbXi07x5vjMm7P~ERukQNX6SC4M/BF/bc:utxob:\
796 4vm1CX2Z-K8hMo59-e7dgGBS-Jka7mYn-Xe~yP85-yUiHHxr-aVlYa?endpoints=rpcs:/\
797 /%5B2001:db8::1%5D:1234";
798 let invoice = RgbInvoice::from_str(invoice_str).unwrap();
799 assert_eq!(invoice.transports, vec![RgbTransport::JsonRpc {
800 tls: true,
801 host: "[2001:db8::1]:1234".to_string()
802 }]);
803 assert_eq!(invoice.to_string(), invoice_str);
804
805 let invoice_str = "rgb:3NoxsLum-cRPebTV-gTZY8qY-KS20lx7-OqgtBls-t7muan4/\
807 XvmU3d4_nQQ8S7oagbXi07x5vjMm7P~ERukQNX6SC4M/BF/bc:utxob:\
808 4vm1CX2Z-K8hMo59-e7dgGBS-Jka7mYn-Xe~yP85-yUiHHxr-aVlYa?endpoints=rpc://";
809 let result = RgbInvoice::from_str(invoice_str);
810 assert!(matches!(result, Err(InvoiceParseError::InvalidQueryParam(_))));
811
812 let invoice_str = "rgb:3NoxsLum-cRPebTV-gTZY8qY-KS20lx7-OqgtBls-t7muan4/\
814 XvmU3d4_nQQ8S7oagbXi07x5vjMm7P~ERukQNX6SC4M/BF/bc:utxob:\
815 4vm1CX2Z-K8hMo59-e7dgGBS-Jka7mYn-Xe~yP85-yUiHHxr-aVlYa?endpoints=rpc/\
816 host.example.com";
817 let result = RgbInvoice::from_str(invoice_str);
818 assert!(matches!(result, Err(InvoiceParseError::InvalidQueryParam(_))));
819
820 let invoice_str = "rgb:3NoxsLum-cRPebTV-gTZY8qY-KS20lx7-OqgtBls-t7muan4/\
822 XvmU3d4_nQQ8S7oagbXi07x5vjMm7P~ERukQNX6SC4M/BF/bc:utxob:\
823 4vm1CX2Z-K8hMo59-e7dgGBS-Jka7mYn-Xe~yP85-yUiHHxr-aVlYa?endpoints=rpc://\
824 ho]t";
825 let result = RgbInvoice::from_str(invoice_str);
826 assert!(matches!(result, Err(InvoiceParseError::Invalid)));
827
828 let invoice_str = "rgb:\
830 3NoxsLum-cRPebTV-gTZY8qY-KS20lx7-OqgtBls-t7muan4/XvmU3d4_nQQ8S7oagbXi07x5vjMm7P~ERukQNX6SC4M/\
831 BF/bc:utxob:4vm1CX2Z-K8hMo59-e7dgGBS-Jka7mYn-Xe~yP85-yUiHHxr-aVlYa?endpoints=https://\
832 host.example.com";
833 let invoice = RgbInvoice::from_str(invoice_str).unwrap();
834 let transports = vec![RgbTransport::RestHttp {
835 tls: true,
836 host: "host.example.com".to_string(),
837 }];
838 assert_eq!(invoice.transports, transports);
839 assert_eq!(invoice.to_string(), invoice_str);
840
841 let invoice_str = "rgb:3NoxsLum-cRPebTV-gTZY8qY-KS20lx7-OqgtBls-t7muan4/\
843 XvmU3d4_nQQ8S7oagbXi07x5vjMm7P~ERukQNX6SC4M/BF/bc:utxob:\
844 4vm1CX2Z-K8hMo59-e7dgGBS-Jka7mYn-Xe~yP85-yUiHHxr-aVlYa?endpoints=wss://\
845 host.example.com";
846 let invoice = RgbInvoice::from_str(invoice_str).unwrap();
847 let transports = vec![RgbTransport::WebSockets {
848 tls: true,
849 host: "host.example.com".to_string(),
850 }];
851 assert_eq!(invoice.transports, transports);
852 assert_eq!(invoice.to_string(), invoice_str);
853
854 let invoice_str = "rgb:3NoxsLum-cRPebTV-gTZY8qY-KS20lx7-OqgtBls-t7muan4/\
856 XvmU3d4_nQQ8S7oagbXi07x5vjMm7P~ERukQNX6SC4M/BF/bc:utxob:\
857 4vm1CX2Z-K8hMo59-e7dgGBS-Jka7mYn-Xe~yP85-yUiHHxr-aVlYa?endpoints=storm:\
858 //_/";
859 let invoice = RgbInvoice::from_str(invoice_str).unwrap();
860 let transports = vec![RgbTransport::Storm {}];
861 assert_eq!(invoice.transports, transports);
862 assert_eq!(invoice.to_string(), invoice_str);
863
864 let invoice_str = "rgb:\
866 3NoxsLum-cRPebTV-gTZY8qY-KS20lx7-OqgtBls-t7muan4/XvmU3d4_nQQ8S7oagbXi07x5vjMm7P~ERukQNX6SC4M/\
867 BF/bc:utxob:4vm1CX2Z-K8hMo59-e7dgGBS-Jka7mYn-Xe~yP85-yUiHHxr-aVlYa?endpoints=rpcs://\
868 host1.example.com,http://host2.example.com,ws://host3.example.com";
869 let invoice = RgbInvoice::from_str(invoice_str).unwrap();
870 let transports = vec![
871 RgbTransport::JsonRpc {
872 tls: true,
873 host: "host1.example.com".to_string(),
874 },
875 RgbTransport::RestHttp {
876 tls: false,
877 host: "host2.example.com".to_string(),
878 },
879 RgbTransport::WebSockets {
880 tls: false,
881 host: "host3.example.com".to_string(),
882 },
883 ];
884 assert_eq!(invoice.transports, transports);
885 assert_eq!(invoice.to_string(), invoice_str);
886
887 let result = RgbTransport::from_str("");
889 assert!(matches!(result, Err(TransportParseError::InvalidTransport(_))));
890
891 let result = RgbTransport::from_str("bad");
893 assert!(matches!(result, Err(TransportParseError::InvalidTransport(_))));
894
895 let result = RgbTransport::from_str("rpca://host.example.com");
897 assert!(matches!(result, Err(TransportParseError::InvalidTransport(_))));
898
899 let result = RgbTransport::from_str("rpc://");
901 assert!(matches!(result, Err(TransportParseError::InvalidTransportHost(_))));
902
903 let result = RgbTransport::from_str("rpc/host.example.com");
905 assert!(matches!(result, Err(TransportParseError::InvalidTransport(_))));
906
907 let invoice_str = "rgb:3NoxsLum-cRPebTV-gTZY8qY-KS20lx7-OqgtBls-t7muan4/\
909 XvmU3d4_nQQ8S7oagbXi07x5vjMm7P~ERukQNX6SC4M/BF/bc:wvout:\
910 +750f58bcca0fdb11891e7979d829b8c56e0963dba08c44f54a256cf7dbc09caf";
911 let result = RgbInvoice::from_str(invoice_str);
912 assert!(matches!(result, Err(InvoiceParseError::Beneficiary(_))));
913
914 let invoice_str = "rgb:3NoxsLum-cRPebTV-gTZY8qY-KS20lx7-OqgtBls-t7muan4/\
916 XvmU3d4_nQQ8S7oagbXi07x5vjMm7P~ERukQNX6SC4M/BF/\
917 750f58bcca0fdb11891e7979d829b8c56e0963dba08c44f54a256cf7dbc09caf";
918 let result = RgbInvoice::from_str(invoice_str);
919 assert!(matches!(result, Err(InvoiceParseError::Beneficiary(_))));
920
921 let invoice_str = "rgb:3NoxsLum-cRPebTV-gTZY8qY-KS20lx7-OqgtBls-t7muan4/\
923 XvmU3d4_nQQ8S7oagbXi07x5vjMm7P~ERukQNX6SC4M/BF/:\
924 +750f58bcca0fdb11891e7979d829b8c56e0963dba08c44f54a256cf7dbc09caf";
925 let result = RgbInvoice::from_str(invoice_str);
926 assert!(matches!(result, Err(InvoiceParseError::Beneficiary(_))));
927
928 let invoice_str = "rgb:3NoxsLum-cRPebTV-gTZY8qY-KS20lx7-OqgtBls-t7muan4/\
930 XvmU3d4_nQQ8S7oagbXi07x5vjMm7P~ERukQNX6SC4M/BF/bc:wvout:\
931 BYWmQlmL-$i5Co3j-LtTvxSr-53!\
932 Brv7-fc7ZntC-ha988ci-jqKOj4Q+750f58bcca0fdb11891e7979";
933 let result = RgbInvoice::from_str(invoice_str);
934 assert!(matches!(result, Err(InvoiceParseError::Beneficiary(_))));
935
936 let invoice_str = "rgb:3NoxsLum-cRPebTV-gTZY8qY-KS20lx7-OqgtBls-t7muan4/\
938 XvmU3d4_nQQ8S7oagbXi07x5vjMm7P~ERukQNX6SC4M/BF/bc:wvout:\
939 BYWmQlmL-$i5Co3j-LtTvxSr-53!Brv7-fc7ZntC-ha988ci-jqKOj4Q+";
940 let result = RgbInvoice::from_str(invoice_str);
941 assert!(matches!(result, Err(InvoiceParseError::Beneficiary(_))));
942 }
943
944 #[test]
945 fn pay2vout_parse() {
946 let p = Pay2Vout::new(AddressPayload::Pkh(PubkeyHash::from_raw_hash(
947 *hash160::Hash::from_bytes_ref(&[0xff; 20]),
948 )));
949 assert_eq!(Pay2Vout::from_str(&p.to_string()).unwrap(), p);
950
951 let p = Pay2Vout::new(AddressPayload::Sh(ScriptHash::from_raw_hash(
952 *hash160::Hash::from_bytes_ref(&[0xff; 20]),
953 )));
954 assert_eq!(Pay2Vout::from_str(&p.to_string()).unwrap(), p);
955
956 let p = Pay2Vout::new(AddressPayload::Wpkh(WPubkeyHash::from_raw_hash(
957 *hash160::Hash::from_bytes_ref(&[0xff; 20]),
958 )));
959 assert_eq!(Pay2Vout::from_str(&p.to_string()).unwrap(), p);
960
961 let p = Pay2Vout::new(AddressPayload::Wsh(WScriptHash::from_raw_hash(
962 *sha256::Hash::from_bytes_ref(&[0xff; 32]),
963 )));
964 assert_eq!(Pay2Vout::from_str(&p.to_string()).unwrap(), p);
965
966 let p = Pay2Vout::new(AddressPayload::Tr(TweakedPublicKey::dangerous_assume_tweaked(
967 UntweakedPublicKey::from_slice(&[
968 0x85, 0xa6, 0x42, 0x59, 0x8b, 0xfe, 0x2e, 0x42, 0xa3, 0x78, 0xcb, 0xb5, 0x3b, 0xf1,
969 0x4a, 0xbe, 0x77, 0xf8, 0x1a, 0xef, 0xed, 0xf7, 0x3b, 0x66, 0x7b, 0x42, 0x85, 0xaf,
970 0x7c, 0xf1, 0xc8, 0xa3,
971 ])
972 .unwrap(),
973 )));
974 assert_eq!(Pay2Vout::from_str(&p.to_string()).unwrap(), p);
975 }
976}