diff --git a/rust/rbac-registration/Cargo.toml b/rust/rbac-registration/Cargo.toml index 9b1936d685..153c0a5392 100644 --- a/rust/rbac-registration/Cargo.toml +++ b/rust/rbac-registration/Cargo.toml @@ -21,13 +21,11 @@ workspace = true hex = "0.4.3" anyhow = "1.0.89" strum_macros = "0.26.4" -regex = "1.11.0" minicbor = { version = "0.25.1", features = ["alloc", "derive", "half"] } brotli = "7.0.0" zstd = "0.13.2" x509-cert = "0.2.5" der-parser = "9.0.0" -bech32 = "0.11.0" dashmap = "6.1.0" blake2b_simd = "1.0.2" tracing = "0.1.40" diff --git a/rust/rbac-registration/src/cardano/cip509/utils/cip134.rs b/rust/rbac-registration/src/cardano/cip509/utils/cip134.rs index 714c62b142..9b346b8d60 100644 --- a/rust/rbac-registration/src/cardano/cip509/utils/cip134.rs +++ b/rust/rbac-registration/src/cardano/cip509/utils/cip134.rs @@ -1,30 +1,91 @@ //! Utility functions for CIP-0134 address. +// Ignore URIs that are used in tests and doc-examples. +// cSpell:ignoreRegExp web\+cardano:.+ + +use std::fmt::{Display, Formatter}; + use anyhow::{anyhow, Context, Result}; use pallas::ledger::addresses::Address; -/// Parses CIP-0134 URI and returns an address. -/// -/// # Errors -/// - Invalid URI. +/// An URI in the CIP-0134 format. /// -/// # Examples +/// See the [proposal] for more details. /// -/// ``` -/// use pallas::ledger::addresses::{Address, Network}; -/// use rbac_registration::cardano::cip509::utils::parse_cip0134_uri; -/// -/// let uri = "web+cardano://addr/stake1uyehkck0lajq8gr28t9uxnuvgcqrc6070x3k9r8048z8y5gh6ffgw"; -/// let Address::Stake(address) = parse_cip0134_uri(uri).unwrap() else { -/// panic!("Unexpected address type"); -/// }; -/// assert_eq!(address.network(), Network::Mainnet); -/// ``` -pub fn parse_cip0134_uri(uri: &str) -> Result
{ - let bech32 = uri - .strip_prefix("web+cardano://addr/") - .ok_or_else(|| anyhow!("Missing schema part of URI"))?; - Address::from_bech32(bech32).context("Unable to parse bech32 part of URI") +/// [proposal]: https://github.com/cardano-foundation/CIPs/pull/888 +#[derive(Debug)] +pub struct Cip0134Uri { + /// A URI string. + uri: String, + /// An address parsed from the URI. + address: Address, +} + +impl Cip0134Uri { + /// Creates a new `Cip0134Uri` instance by parsing the given URI. + /// + /// # Errors + /// - Invalid URI. + /// + /// # Examples + /// + /// ``` + /// use rbac_registration::cardano::cip509::utils::Cip0134Uri; + /// + /// let uri = "web+cardano://addr/stake1uyehkck0lajq8gr28t9uxnuvgcqrc6070x3k9r8048z8y5gh6ffgw"; + /// let cip0134_uri = Cip0134Uri::parse(uri).unwrap(); + /// ``` + pub fn parse(uri: &str) -> Result { + let bech32 = uri + .strip_prefix("web+cardano://addr/") + .ok_or_else(|| anyhow!("Missing schema part of URI"))?; + let address = Address::from_bech32(bech32).context("Unable to parse bech32 part of URI")?; + + Ok(Self { + uri: uri.to_owned(), + address, + }) + } + + /// Returns a URI string. + /// + /// # Examples + /// + /// ``` + /// use rbac_registration::cardano::cip509::utils::Cip0134Uri; + /// + /// let uri = "web+cardano://addr/stake1uyehkck0lajq8gr28t9uxnuvgcqrc6070x3k9r8048z8y5gh6ffgw"; + /// let cip0134_uri = Cip0134Uri::parse(uri).unwrap(); + /// assert_eq!(cip0134_uri.uri(), uri); + #[must_use] + pub fn uri(&self) -> &str { + &self.uri + } + + /// Returns a URI string. + /// + /// # Examples + /// + /// ``` + /// use pallas::ledger::addresses::{Address, Network}; + /// use rbac_registration::cardano::cip509::utils::Cip0134Uri; + /// + /// let uri = "web+cardano://addr/stake1uyehkck0lajq8gr28t9uxnuvgcqrc6070x3k9r8048z8y5gh6ffgw"; + /// let cip0134_uri = Cip0134Uri::parse(uri).unwrap(); + /// let Address::Stake(address) = cip0134_uri.address() else { + /// panic!("Unexpected address type"); + /// }; + /// assert_eq!(address.network(), Network::Mainnet); + #[must_use] + pub fn address(&self) -> &Address { + &self.address + } +} + +impl Display for Cip0134Uri { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.uri()) + } } #[cfg(test)] @@ -35,15 +96,17 @@ mod tests { #[test] fn invalid_prefix() { + // cSpell:disable let test_uris = [ "addr1qx2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3n0d3vllmyqwsx5wktcd8cc3sq835lu7drv2xwl2wywfgse35a3x", "//addr/addr1qx2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3n0d3vllmyqwsx5wktcd8cc3sq835lu7drv2xwl2wywfgse35a3x", "web+cardano:/addr1qx2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3n0d3vllmyqwsx5wktcd8cc3sq835lu7drv2xwl2wywfgse35a3x", "somthing+unexpected://addr/addr1qx2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3n0d3vllmyqwsx5wktcd8cc3sq835lu7drv2xwl2wywfgse35a3x", ]; + // cSpell:enable for uri in test_uris { - let err = format!("{:?}", parse_cip0134_uri(uri).expect_err(&format!("{uri}"))); + let err = format!("{:?}", Cip0134Uri::parse(uri).expect_err(&format!("{uri}"))); assert_eq!("Missing schema part of URI", err); } } @@ -51,7 +114,7 @@ mod tests { #[test] fn invalid_bech32() { let uri = "web+cardano://addr/adr1qx2fxv2umyh"; - let err = format!("{:?}", parse_cip0134_uri(uri).unwrap_err()); + let err = format!("{:?}", Cip0134Uri::parse(uri).unwrap_err()); assert!(err.starts_with("Unable to parse bech32 part of URI")); } @@ -76,7 +139,8 @@ mod tests { ]; for (uri, network, payload) in test_data { - let Address::Stake(address) = parse_cip0134_uri(uri).unwrap() else { + let cip0134_uri = Cip0134Uri::parse(uri).expect(&format!("{uri}")); + let Address::Stake(address) = cip0134_uri.address else { panic!("Unexpected address type ({uri})"); }; assert_eq!(network, address.network()); @@ -102,10 +166,19 @@ mod tests { ]; for (uri, network) in test_data { - let Address::Shelley(address) = parse_cip0134_uri(uri).unwrap() else { + let cip0134_uri = Cip0134Uri::parse(uri).expect(&format!("{uri}")); + let Address::Shelley(address) = cip0134_uri.address else { panic!("Unexpected address type ({uri})"); }; assert_eq!(network, address.network()); } } + + // The Display should return the original URI. + #[test] + fn display() { + let uri = "web+cardano://addr/stake1uyehkck0lajq8gr28t9uxnuvgcqrc6070x3k9r8048z8y5gh6ffgw"; + let cip0134_uri = Cip0134Uri::parse(uri).expect(&format!("{uri}")); + assert_eq!(uri, cip0134_uri.to_string()); + } } diff --git a/rust/rbac-registration/src/cardano/cip509/utils/mod.rs b/rust/rbac-registration/src/cardano/cip509/utils/mod.rs index d9e71ab2e4..ab7c3954d6 100644 --- a/rust/rbac-registration/src/cardano/cip509/utils/mod.rs +++ b/rust/rbac-registration/src/cardano/cip509/utils/mod.rs @@ -1,6 +1,6 @@ //! Utility functions for CIP-509 pub mod cip19; -pub use cip134::parse_cip0134_uri; +pub use cip134::Cip0134Uri; mod cip134; diff --git a/rust/rbac-registration/src/cardano/cip509/validation.rs b/rust/rbac-registration/src/cardano/cip509/validation.rs index a49f635b61..205957f546 100644 --- a/rust/rbac-registration/src/cardano/cip509/validation.rs +++ b/rust/rbac-registration/src/cardano/cip509/validation.rs @@ -40,7 +40,7 @@ use super::{ }, utils::{ cip19::{compare_key_hash, extract_key_hash}, - parse_cip0134_uri, + Cip0134Uri, }, Cip509, TxInputHash, TxWitness, }; @@ -169,11 +169,12 @@ pub(crate) fn validate_stake_public_key( // Extract the CIP19 hash and push into // array - if let Ok(Address::Stake(a)) = - parse_cip0134_uri(&addr) - { - pk_addrs - .push(a.payload().as_hash().to_vec()); + if let Ok(uri) = Cip0134Uri::parse(&addr) { + if let Address::Stake(a) = uri.address() { + pk_addrs.push( + a.payload().as_hash().to_vec(), + ); + } } }, Err(e) => { @@ -222,8 +223,10 @@ pub(crate) fn validate_stake_public_key( if name.gn_type() == &c509_certificate::general_names::general_name::GeneralNameTypeRegistry::UniformResourceIdentifier { match name.gn_value() { GeneralNameValue::Text(s) => { - if let Ok(Address::Stake(a)) = parse_cip0134_uri(s) { - pk_addrs.push(a.payload().as_hash().to_vec()); + if let Ok(uri) = Cip0134Uri::parse(s) { + if let Address::Stake(a) = uri.address() { + pk_addrs.push(a.payload().as_hash().to_vec()); + } } }, _ => {