diff --git a/rust/cardano-blockchain-types/src/cip134_uri.rs b/rust/cardano-blockchain-types/src/cip134_uri.rs new file mode 100644 index 00000000000..1f97d6dfe0a --- /dev/null +++ b/rust/cardano-blockchain-types/src/cip134_uri.rs @@ -0,0 +1,197 @@ +//! An URI in the CIP-0134 format. + +// Ignore URIs that are used in tests and doc-examples. +// cSpell:ignoreRegExp web\+cardano:.+ + +use std::fmt::{Display, Formatter}; + +use anyhow::{anyhow, Context, Error, Result}; +use pallas::ledger::addresses::Address; + +/// A URI in the CIP-0134 format. +/// +/// See the [proposal] for more details. +/// +/// [proposal]: https://github.com/cardano-foundation/CIPs/pull/888 +#[derive(Debug, Clone, Eq, PartialEq)] +#[allow(clippy::module_name_repetitions)] +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 cardano_blockchain_types::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 cardano_blockchain_types::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 cardano_blockchain_types::Cip0134Uri; + /// use pallas::ledger::addresses::{Address, Network}; + /// + /// 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()) + } +} + +impl TryFrom<&[u8]> for Cip0134Uri { + type Error = Error; + + fn try_from(value: &[u8]) -> Result { + let address = std::str::from_utf8(value) + .with_context(|| format!("Invalid utf8 string: '{value:?}'"))?; + Self::parse(address) + } +} + +#[cfg(test)] +mod tests { + use pallas::ledger::addresses::{Address, Network}; + + use super::*; + + #[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!("{:?}", Cip0134Uri::parse(uri).expect_err(uri)); + assert!(err.starts_with("Missing schema part of URI")); + } + } + + #[test] + fn invalid_bech32() { + let uri = "web+cardano://addr/adr1qx2fxv2umyh"; + let err = format!("{:?}", Cip0134Uri::parse(uri).unwrap_err()); + assert!(err.starts_with("Unable to parse bech32 part of URI")); + } + + #[test] + fn stake_address() { + let test_data = [ + ( + "web+cardano://addr/stake_test1uqehkck0lajq8gr28t9uxnuvgcqrc6070x3k9r8048z8y5gssrtvn", + Network::Testnet, + "337b62cfff6403a06a3acbc34f8c46003c69fe79a3628cefa9c47251", + ), + ( + "web+cardano://addr/stake1uyehkck0lajq8gr28t9uxnuvgcqrc6070x3k9r8048z8y5gh6ffgw", + Network::Mainnet, + "337b62cfff6403a06a3acbc34f8c46003c69fe79a3628cefa9c47251", + ), + ( + "web+cardano://addr/drep_vk17axh4sc9zwkpsft3tlgpjemfwc0u5mnld80r85zw7zdqcst6w54sdv4a4e", + Network::Other(7), + "4d7ac30513ac1825715fd0196769761fca6e7f69de33d04ef09a0c41", + ) + ]; + + for (uri, network, payload) in test_data { + let cip0134_uri = Cip0134Uri::parse(uri).expect(uri); + let Address::Stake(address) = cip0134_uri.address() else { + panic!("Unexpected address type ({uri})"); + }; + assert_eq!(network, address.network()); + assert_eq!(payload, address.payload().as_hash().to_string()); + } + } + + #[test] + fn shelley_address() { + let test_data = [ + ( + "web+cardano://addr/addr1qx2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3n0d3vllmyqwsx5wktcd8cc3sq835lu7drv2xwl2wywfgse35a3x", + Network::Mainnet, + ), + ( + "web+cardano://addr/addr_test1gz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer5pnz75xxcrdw5vky", + Network::Testnet, + ), + ( + "web+cardano://addr/cc_hot_vk10y48lq72hypxraew74lwjjn9e2dscuwphckglh2nrrpkgweqk5hschnzv5", + Network::Other(9), + ) + ]; + + for (uri, network) in test_data { + let cip0134_uri = Cip0134Uri::parse(uri).expect(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(uri); + assert_eq!(uri, cip0134_uri.to_string()); + } +} diff --git a/rust/cardano-blockchain-types/src/lib.rs b/rust/cardano-blockchain-types/src/lib.rs index e4cbb5f3940..84afb9ec368 100644 --- a/rust/cardano-blockchain-types/src/lib.rs +++ b/rust/cardano-blockchain-types/src/lib.rs @@ -1,6 +1,7 @@ //! Catalyst Enhanced `MultiEraBlock` Structures mod auxdata; +mod cip134_uri; mod fork; mod metadata; mod multi_era_block_data; @@ -18,6 +19,7 @@ pub use auxdata::{ metadatum_value::MetadatumValue, scripts::{Script, ScriptArray, ScriptType, TransactionScripts}, }; +pub use cip134_uri::Cip0134Uri; pub use fork::Fork; pub use metadata::cip36::{voting_pk::VotingPubKey, Cip36}; pub use multi_era_block_data::MultiEraBlock; diff --git a/rust/cardano-blockchain-types/src/multi_era_block_data.rs b/rust/cardano-blockchain-types/src/multi_era_block_data.rs index 612fa32f38a..a1508950b37 100644 --- a/rust/cardano-blockchain-types/src/multi_era_block_data.rs +++ b/rust/cardano-blockchain-types/src/multi_era_block_data.rs @@ -12,6 +12,7 @@ use std::{cmp::Ordering, fmt::Display, sync::Arc}; use anyhow::bail; use ed25519_dalek::VerifyingKey; use ouroboros::self_referencing; +use pallas::ledger::traverse::MultiEraTx; use tracing::debug; use crate::{ @@ -91,7 +92,7 @@ impl MultiEraBlock { /// # Errors /// /// If the given bytes cannot be decoded as a multi-era block, an error is returned. - fn new_block( + pub fn new( network: Network, raw_data: Vec, previous: &Point, fork: Fork, ) -> anyhow::Result { let builder = SelfReferencedMultiEraBlockTryBuilder { @@ -149,17 +150,6 @@ impl MultiEraBlock { }) } - /// Creates a new `MultiEraBlockData` from the given bytes. - /// - /// # Errors - /// - /// If the given bytes cannot be decoded as a multi-era block, an error is returned. - pub fn new( - network: Network, raw_data: Vec, previous: &Point, fork: Fork, - ) -> anyhow::Result { - MultiEraBlock::new_block(network, raw_data, previous, fork) - } - /// Remake the block on a new fork. pub fn set_fork(&mut self, fork: Fork) { self.fork = fork; @@ -282,6 +272,12 @@ impl MultiEraBlock { None } + /// Returns a list of transactions withing this block. + #[must_use] + pub fn txs(&self) -> Vec { + self.decode().txs() + } + /// Get the auxiliary data of the block. #[must_use] pub fn aux_data(&self) -> &BlockAuxData { diff --git a/rust/cardano-blockchain-types/src/slot.rs b/rust/cardano-blockchain-types/src/slot.rs index 8cb565d8368..ee0c69e2312 100644 --- a/rust/cardano-blockchain-types/src/slot.rs +++ b/rust/cardano-blockchain-types/src/slot.rs @@ -11,18 +11,14 @@ use serde::Serialize; #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Default, Serialize)] -/// Slot on the blockchain, typically one slot equals one second. However chain +/// Slot on the blockchain, typically one slot equals one second. However chain /// parameters can alter how long a slot is. pub struct Slot(u64); impl Slot { /// Convert an `` to Slot. (saturate if out of range.) pub fn from_saturating< - T: Copy - + TryInto - + std::ops::Sub - + std::cmp::PartialOrd - + num_traits::identities::Zero, + T: Copy + TryInto + Sub + PartialOrd + num_traits::identities::Zero, >( value: T, ) -> Self { diff --git a/rust/cardano-blockchain-types/src/txn_index.rs b/rust/cardano-blockchain-types/src/txn_index.rs index 1d238af4541..7b1480de410 100644 --- a/rust/cardano-blockchain-types/src/txn_index.rs +++ b/rust/cardano-blockchain-types/src/txn_index.rs @@ -10,7 +10,7 @@ impl< T: Copy + TryInto + std::ops::Sub - + std::cmp::PartialOrd + + PartialOrd + num_traits::identities::Zero, > From for TxnIndex { @@ -25,6 +25,12 @@ impl From for i16 { } } +impl From for usize { + fn from(value: TxnIndex) -> Self { + value.0.into() + } +} + #[cfg(test)] mod tests { use super::*;