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.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#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug, From)]
229pub enum AddressPayload {
230 #[from]
232 Pkh(PubkeyHash),
233
234 #[from]
236 Sh(ScriptHash),
237
238 #[from]
240 Wpkh(WPubkeyHash),
241
242 #[from]
244 Wsh(WScriptHash),
245
246 #[from]
248 Tr(TweakedPublicKey),
249}
250
251#[derive(Clone, Eq, PartialEq, Debug, Display, Error)]
253#[display(doc_comments)]
254pub enum AddressError {
255 InvalidTaprootKey,
257 UnsupportedScriptPubkey,
259}
260
261impl AddressPayload {
262 pub fn into_address(self, network: Network) -> Address {
263 Address::from_script(&self.to_script(), Params::new(network)).unwrap()
266 }
267
268 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 let result = RgbTransport::from_str("");
888 assert!(matches!(result, Err(TransportParseError::InvalidTransport(_))));
889
890 let result = RgbTransport::from_str("bad");
892 assert!(matches!(result, Err(TransportParseError::InvalidTransport(_))));
893
894 let result = RgbTransport::from_str("rpca://host.example.com");
896 assert!(matches!(result, Err(TransportParseError::InvalidTransport(_))));
897
898 let result = RgbTransport::from_str("rpc://");
900 assert!(matches!(result, Err(TransportParseError::InvalidTransportHost(_))));
901
902 let result = RgbTransport::from_str("rpc/host.example.com");
904 assert!(matches!(result, Err(TransportParseError::InvalidTransport(_))));
905
906 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 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 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 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 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}