diff --git a/.github/workflows/typo.yml b/.github/workflows/typo.yml new file mode 100644 index 00000000..efa57519 --- /dev/null +++ b/.github/workflows/typo.yml @@ -0,0 +1,14 @@ +name: Typo Check + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + typos: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: crate-ci/typos@master diff --git a/crates/starknet-types-core/Cargo.toml b/crates/starknet-types-core/Cargo.toml index 1fb1294d..be91bc8b 100644 --- a/crates/starknet-types-core/Cargo.toml +++ b/crates/starknet-types-core/Cargo.toml @@ -24,7 +24,7 @@ serde = { version = "1", optional = true, default-features = false, features = [ "alloc", "derive" ] } lambdaworks-crypto = { version = "0.13.0", default-features = false, optional = true } -parity-scale-codec = { version = "3.6", default-features = false, optional = true } +parity-scale-codec = { version = "3.6", default-features = false, features = ["derive"], optional = true } lazy_static = { version = "1.5", default-features = false, optional = true } zeroize = { version = "1.8.1", default-features = false, optional = true } subtle = { version = "2.6.1", default-features = false, optional = true } diff --git a/crates/starknet-types-core/src/contract_address.rs b/crates/starknet-types-core/src/contract_address.rs new file mode 100644 index 00000000..8d4ae915 --- /dev/null +++ b/crates/starknet-types-core/src/contract_address.rs @@ -0,0 +1,218 @@ +//! A starknet contract address +//! +//! In Starknet, a valid contract addresses exist as a subset of the type `Felt`. +//! Therefore some checks must be done in order to produce protocol valid addresses. +//! This module provides this logic as a Rust type `ContractAddress`, that can guarantee the validity of the address. +//! It also comes with some quality of life methods. + +use core::str::FromStr; + +use crate::{ + felt::Felt, + patricia_key::{ + PatriciaKey, PatriciaKeyFromFeltError, PatriciaKeyFromStrError, + STORAGE_LEAF_ADDRESS_UPPER_BOUND, + }, +}; + +#[repr(transparent)] +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(transparent))] +#[cfg_attr( + feature = "parity-scale-codec", + derive(parity_scale_codec::Encode, parity_scale_codec::Decode) +)] +pub struct ContractAddress(PatriciaKey); + +impl ContractAddress { + pub const ZERO: Self = Self::from_hex_unchecked("0x0"); + pub const ONE: Self = Self::from_hex_unchecked("0x1"); + pub const TWO: Self = Self::from_hex_unchecked("0x2"); + pub const THREE: Self = Self::from_hex_unchecked("0x3"); + + /// Lower inclusive bound + pub const LOWER_BOUND: Self = Self::ZERO; + /// Upper non-inclusive bound + /// + /// For consistency with other merkle leaf bounds, [ContractAddress] is also bounded by [STORAGE_LEAF_ADDRESS_UPPER_BOUND] + pub const UPPER_BOUND: Self = Self(STORAGE_LEAF_ADDRESS_UPPER_BOUND); +} + +impl core::fmt::Display for ContractAddress { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl AsRef for ContractAddress { + fn as_ref(&self) -> &Felt { + self.0.as_ref() + } +} + +impl From for Felt { + fn from(value: ContractAddress) -> Self { + value.0.into() + } +} + +impl AsRef for ContractAddress { + fn as_ref(&self) -> &PatriciaKey { + &self.0 + } +} + +impl From for PatriciaKey { + fn from(value: ContractAddress) -> Self { + value.0 + } +} + +#[derive(Debug)] +pub enum ContractAddressFromPatriciaKeyError { + OutOfBounds, +} + +impl core::fmt::Display for ContractAddressFromPatriciaKeyError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + ContractAddressFromPatriciaKeyError::OutOfBounds => write!( + f, + "value out of bounds, upper non-inclusive bound is {}", + ContractAddress::UPPER_BOUND + ), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for ContractAddressFromPatriciaKeyError {} + +impl TryFrom for ContractAddress { + type Error = ContractAddressFromPatriciaKeyError; + + fn try_from(value: PatriciaKey) -> Result { + if value >= STORAGE_LEAF_ADDRESS_UPPER_BOUND { + Err(ContractAddressFromPatriciaKeyError::OutOfBounds) + } else { + Ok(ContractAddress(value)) + } + } +} + +#[derive(Debug)] +pub enum ContractAddressFromFeltError { + PatriciaKey(PatriciaKeyFromFeltError), + OutOfBounds(ContractAddressFromPatriciaKeyError), +} + +impl core::fmt::Display for ContractAddressFromFeltError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + ContractAddressFromFeltError::OutOfBounds(e) => { + write!(f, "invalid value for contract address: {e}") + } + ContractAddressFromFeltError::PatriciaKey(e) => { + write!(f, "invalid patricia key value: {e}") + } + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for ContractAddressFromFeltError {} +impl TryFrom for ContractAddress { + type Error = ContractAddressFromFeltError; + + fn try_from(value: Felt) -> Result { + let pk = PatriciaKey::try_from(value).map_err(ContractAddressFromFeltError::PatriciaKey)?; + let ca = + ContractAddress::try_from(pk).map_err(ContractAddressFromFeltError::OutOfBounds)?; + + Ok(ca) + } +} + +impl Felt { + /// Validates that a Felt value represents a valid Starknet contract address. + pub fn is_valid_contract_address(&self) -> bool { + self < &Felt::from(ContractAddress::UPPER_BOUND) + } +} + +#[derive(Debug)] +pub enum ContractAddressFromStrError { + PatriciaKey(PatriciaKeyFromStrError), + OutOfBounds(ContractAddressFromPatriciaKeyError), +} + +impl core::fmt::Display for ContractAddressFromStrError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + ContractAddressFromStrError::PatriciaKey(e) => { + write!(f, "invalid patricia key: {e}") + } + ContractAddressFromStrError::OutOfBounds(e) => { + write!(f, "invalid value for contract address: {e}") + } + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for ContractAddressFromStrError {} + +impl FromStr for ContractAddress { + type Err = ContractAddressFromStrError; + + fn from_str(s: &str) -> Result { + let pk = PatriciaKey::from_str(s).map_err(ContractAddressFromStrError::PatriciaKey)?; + let ca = ContractAddress::try_from(pk).map_err(ContractAddressFromStrError::OutOfBounds)?; + + Ok(ca) + } +} + +impl ContractAddress { + /// Creates a new [ContractAddress] from an hex encoded string without checking it is a valid value. + /// + /// Should NEVER be used on user inputs, as it can cause erroneous execution if dynamically initialized with bad values. + /// Should mostly be used at compilation time on hardcoded static string. + pub const fn from_hex_unchecked(s: &'static str) -> ContractAddress { + let patricia_key = PatriciaKey::from_hex_unchecked(s); + + ContractAddress(patricia_key) + } +} + +#[cfg(test)] +mod test { + #[cfg(feature = "alloc")] + pub extern crate alloc; + use proptest::prelude::*; + + use crate::{ + contract_address::ContractAddress, felt::Felt, patricia_key::PATRICIA_KEY_UPPER_BOUND, + }; + + #[test] + fn basic_values() { + assert!(ContractAddress::try_from(PATRICIA_KEY_UPPER_BOUND).is_err()); + + let felt = Felt::TWO; + let contract_address = ContractAddress::try_from(felt).unwrap(); + assert_eq!(Felt::from(contract_address), felt); + } + + proptest! { + #[test] + fn is_valid_match_try_into(ref x in any::()) { + if x.is_valid_contract_address() { + prop_assert!(ContractAddress::try_from(*x).is_ok()); + } else { + prop_assert!(ContractAddress::try_from(*x).is_err()); + } + } + } +} diff --git a/crates/starknet-types-core/src/felt/alloc_impls.rs b/crates/starknet-types-core/src/felt/alloc_impls.rs index 8394de30..03ee300d 100644 --- a/crates/starknet-types-core/src/felt/alloc_impls.rs +++ b/crates/starknet-types-core/src/felt/alloc_impls.rs @@ -18,7 +18,7 @@ impl Felt { /// 2. an amount of padding zeros so that the resulting string length is fixed (This amount may be 0), /// 3. the felt value represented in hexadecimal /// - /// The resulting string is guaranted to be 66 chars long, which is enough to represent `Felt::MAX`: + /// The resulting string is guaranteed to be 66 chars long, which is enough to represent `Felt::MAX`: /// 2 chars for the `0x` prefix and 64 chars for the padded hexadecimal felt value. pub fn to_fixed_hex_string(&self) -> alloc::string::String { alloc::format!("{self:#066x}") diff --git a/crates/starknet-types-core/src/felt/mod.rs b/crates/starknet-types-core/src/felt/mod.rs index bb3e689c..021cf90f 100644 --- a/crates/starknet-types-core/src/felt/mod.rs +++ b/crates/starknet-types-core/src/felt/mod.rs @@ -473,22 +473,22 @@ impl Add<&Felt> for u64 { [0, 0, 0, low] => self.checked_add(low), // Now we need to compare the 3 most significant digits. // There are two relevant cases from now on, either `rhs` behaves like a - // substraction of a `u64` or the result of the sum falls out of range. + // subtraction of a `u64` or the result of the sum falls out of range. // The 3 MSB only match the prime for Felt::max_value(), which is -1 - // in the signed field, so this is equivalent to substracting 1 to `self`. + // in the signed field, so this is equivalent to subtracting 1 to `self`. [hi @ .., _] if hi == PRIME_DIGITS_BE_HI => self.checked_sub(1), // For the remaining values between `[-u64::MAX..0]` (where `{0, -1}` have // already been covered) the MSB matches that of `PRIME - u64::MAX`. // Because we're in the negative number case, we count down. Because `0` // and `-1` correspond to different MSBs, `0` and `1` in the LSB are less - // than `-u64::MAX`, the smallest value we can add to (read, substract its + // than `-u64::MAX`, the smallest value we can add to (read, subtract its // magnitude from) a `u64` number, meaning we exclude them from the valid // case. // For the remaining range, we take the absolute value module-2 while - // correcting by substracting `1` (note we actually substract `2` because - // the absolute value itself requires substracting `1`. + // correcting by subtracting `1` (note we actually subtract `2` because + // the absolute value itself requires subtracting `1`. [hi @ .., low] if hi == PRIME_MINUS_U64_MAX_DIGITS_BE_HI && low >= 2 => { (self).checked_sub(u64::MAX - (low - 2)) } diff --git a/crates/starknet-types-core/src/felt/parity_scale_codec.rs b/crates/starknet-types-core/src/felt/parity_scale_codec.rs index 2cbfba98..55ddfe25 100644 --- a/crates/starknet-types-core/src/felt/parity_scale_codec.rs +++ b/crates/starknet-types-core/src/felt/parity_scale_codec.rs @@ -26,7 +26,7 @@ mod tests { fn parity_scale_codec_serialization() { use parity_scale_codec::{Decode, Encode}; - // use an endianness-asymetric number to test that byte order is correct in serialization + // use an endianness-asymmetric number to test that byte order is correct in serialization let initial_felt = Felt::from_hex("0xabcdef123").unwrap(); // serialize the felt diff --git a/crates/starknet-types-core/src/lib.rs b/crates/starknet-types-core/src/lib.rs index 8190b5d5..31b596a1 100644 --- a/crates/starknet-types-core/src/lib.rs +++ b/crates/starknet-types-core/src/lib.rs @@ -8,6 +8,9 @@ pub mod hash; pub mod felt; pub mod qm31; +pub mod contract_address; +pub mod patricia_key; +pub mod regular_contract_address; #[cfg(feature = "alloc")] pub mod short_string; pub mod u256; diff --git a/crates/starknet-types-core/src/patricia_key.rs b/crates/starknet-types-core/src/patricia_key.rs new file mode 100644 index 00000000..2dbdc0ce --- /dev/null +++ b/crates/starknet-types-core/src/patricia_key.rs @@ -0,0 +1,163 @@ +//! A key used for nodes of a Patricia tree. +//! +//! The state of the Starknet blockchain (contracts declared, contracts deployed, storage of each contract), +//! is represented as multiple binary Merkle-Patricia trees. +//! Those trees have an height of 251, which means that they contain at most 2^251 values. +//! The keys of those values are represented as `Felt`, with range [0, PATRICIA_KEY_UPPER_BOUND). +//! Therefore not every `Felt` is a valid `PatriciaKey`, +//! and we can use the `PatriciaKey` type to enforce type safety in our code. +//! +//! See https://docs.starknet.io/learn/protocol/state for further details. + +use core::str::FromStr; + +use crate::felt::Felt; + +pub const PATRICIA_KEY_UPPER_BOUND: PatriciaKey = PatriciaKey(Felt::from_hex_unwrap( + "0x800000000000000000000000000000000000000000000000000000000000000", +)); + +/// The index upper bound for a Starknet tree +/// +/// Equal to `0x800000000000000000000000000000000000000000000000000000000000000 - 256`. +/// +/// In Starknet, users are allowed to store up to 256 felts in a tree leaf. +/// Therefore, storage addresses can be used as "pointers" to some specific felt stored in a leaf: +/// ValueAddress = LeafAddress + IndexInsideTheLeaf +/// So, all leaf addresses are modulo this value. +pub const STORAGE_LEAF_ADDRESS_UPPER_BOUND: PatriciaKey = PatriciaKey(Felt::from_raw([ + 576459263475590224, + 18446744073709255680, + 160989183, + 18446743986131443745, +])); + +#[repr(transparent)] +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(transparent))] +#[cfg_attr( + feature = "parity-scale-codec", + derive(parity_scale_codec::Encode, parity_scale_codec::Decode) +)] +pub struct PatriciaKey(Felt); + +impl PatriciaKey { + /// Lower inclusive bound + pub const LOWER_BOUND: Self = Self(Felt::ZERO); + /// Upper non-inclusive bound + pub const UPPER_BOUND: Self = PATRICIA_KEY_UPPER_BOUND; +} + +impl core::fmt::Display for PatriciaKey { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl AsRef for PatriciaKey { + fn as_ref(&self) -> &Felt { + &self.0 + } +} + +impl From for Felt { + fn from(value: PatriciaKey) -> Self { + value.0 + } +} + +#[derive(Debug, Clone, Copy)] +pub struct PatriciaKeyFromFeltError(Felt); + +impl core::fmt::Display for PatriciaKeyFromFeltError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + #[cfg(feature = "alloc")] + return write!( + f, + "invalid felt value for patricia key. Upper non-inclusive bound is 2^251 got {:#x}", + self.0 + ); + + #[cfg(not(feature = "alloc"))] + return write!( + f, + "invalid felt value for patricia key. Upper non-inclusive bound is 2^251 got {}", + self.0 + ); + } +} + +#[cfg(feature = "std")] +impl std::error::Error for PatriciaKeyFromFeltError {} + +impl TryFrom for PatriciaKey { + type Error = PatriciaKeyFromFeltError; + + fn try_from(value: Felt) -> Result { + if value >= PATRICIA_KEY_UPPER_BOUND.0 { + return Err(PatriciaKeyFromFeltError(value)); + } + + Ok(PatriciaKey(value)) + } +} + +#[derive(Debug)] +pub enum PatriciaKeyFromStrError { + BadFelt(::Err), + BadKey(PatriciaKeyFromFeltError), +} + +impl core::fmt::Display for PatriciaKeyFromStrError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + PatriciaKeyFromStrError::BadFelt(e) => write!(f, "invalid felt string: {e}"), + PatriciaKeyFromStrError::BadKey(e) => write!(f, "invalid address value: {e}"), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for PatriciaKeyFromStrError {} + +impl FromStr for PatriciaKey { + type Err = PatriciaKeyFromStrError; + + fn from_str(s: &str) -> Result { + let felt = Felt::from_str(s).map_err(PatriciaKeyFromStrError::BadFelt)?; + let contract_address = + PatriciaKey::try_from(felt).map_err(PatriciaKeyFromStrError::BadKey)?; + + Ok(contract_address) + } +} + +impl PatriciaKey { + /// Create a new [PatriciaKey] from an hex encoded string without checking if it is a valid value. + /// + /// Should NEVER be used on user inputs, + /// as it can cause erroneous execution if dynamically initialized with bad values. + /// Should mostly be used at compilation time on hardcoded static string. + pub const fn from_hex_unchecked(s: &'static str) -> PatriciaKey { + let felt = Felt::from_hex_unwrap(s); + + PatriciaKey(felt) + } +} + +#[cfg(test)] +mod tests { + use crate::{ + felt::Felt, + patricia_key::{PATRICIA_KEY_UPPER_BOUND, STORAGE_LEAF_ADDRESS_UPPER_BOUND}, + }; + + #[test] + fn enforce_max_storage_leaf_address() { + assert_eq!( + PATRICIA_KEY_UPPER_BOUND.0 - Felt::from(256), + STORAGE_LEAF_ADDRESS_UPPER_BOUND.into(), + ); + } +} diff --git a/crates/starknet-types-core/src/regular_contract_address.rs b/crates/starknet-types-core/src/regular_contract_address.rs new file mode 100644 index 00000000..3528b259 --- /dev/null +++ b/crates/starknet-types-core/src/regular_contract_address.rs @@ -0,0 +1,275 @@ +//! A regular Starknet contract address +//! +//! Similar to [ContractAddress] but it excludes the following values as they are reserved by the protocol: +//! - 0x0 is the default caller address used for external calls. Nothing is ever stored there. +//! - 0x1 is used for block hash mapping. +//! - 0x2 is used for alias. +//! - 0x3 is reserved without used for now. +//! +//! See: https://docs.starknet.io/learn/protocol/state#special-addresses +//! +//! Most user applications should not interact with those special addresses. +//! Doing so would result in a bug or invalid input. +//! `RegularContractAddress` enforces this at the type level. + +use core::str::FromStr; + +use crate::{ + contract_address::{ + ContractAddress, ContractAddressFromFeltError, ContractAddressFromStrError, + }, + felt::Felt, + patricia_key::PatriciaKey, +}; + +#[repr(transparent)] +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(transparent))] +#[cfg_attr( + feature = "parity-scale-codec", + derive(parity_scale_codec::Encode, parity_scale_codec::Decode) +)] +pub struct RegularContractAddress(ContractAddress); + +impl RegularContractAddress { + /// Lower inclusive bound + pub const LOWER_BOUND: Self = Self::from_hex_unchecked("0x4"); + /// Upper non-inclusive bound + pub const UPPER_BOUND: Self = Self(ContractAddress::UPPER_BOUND); +} + +impl core::fmt::Display for RegularContractAddress { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl AsRef for RegularContractAddress { + fn as_ref(&self) -> &Felt { + self.0.as_ref() + } +} + +impl From for Felt { + fn from(value: RegularContractAddress) -> Self { + value.0.into() + } +} + +impl AsRef for RegularContractAddress { + fn as_ref(&self) -> &PatriciaKey { + self.0.as_ref() + } +} + +impl From for PatriciaKey { + fn from(value: RegularContractAddress) -> Self { + value.0.into() + } +} + +impl AsRef for RegularContractAddress { + fn as_ref(&self) -> &ContractAddress { + &self.0 + } +} + +impl From for ContractAddress { + fn from(value: RegularContractAddress) -> Self { + value.0 + } +} + +/// In Starknet, contract addresses must follow specific constraints to be less than 2^251 (0x800000000000000000000000000000000000000000000000000000000000000) to be valid. +/// But the following addresses are reserved for the protocol use: +/// * 0x0 acts as the default caller address for external calls and has no storage +/// * 0x1 functions as a storage space for block mapping +/// * 0x2 is an alias +/// * 0x3 is an reserved but not used +/// +/// Making the regular contract address range be [4, 2^251) +#[derive(Debug, Clone, Copy)] +pub enum RegularContractAddressFromContractAddressError { + Zero, + One, + Two, + Three, +} + +impl core::fmt::Display for RegularContractAddressFromContractAddressError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + RegularContractAddressFromContractAddressError::Zero => { + write!( + f, + "address 0x0 is reserved as the default caller address and has no storage" + ) + } + RegularContractAddressFromContractAddressError::One => { + write!( + f, + "address 0x1 is reserved as storage space for block mapping" + ) + } + RegularContractAddressFromContractAddressError::Two => { + write!(f, "address 0x2 is reserved as alias") + } + RegularContractAddressFromContractAddressError::Three => { + write!(f, "address 0x3 is reserved for future uses") + } + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for RegularContractAddressFromContractAddressError {} + +impl TryFrom for RegularContractAddress { + type Error = RegularContractAddressFromContractAddressError; + + fn try_from(value: ContractAddress) -> Result { + if AsRef::::as_ref(&value) == &Felt::ZERO { + return Err(RegularContractAddressFromContractAddressError::Zero); + } + if AsRef::::as_ref(&value) == &Felt::ONE { + return Err(RegularContractAddressFromContractAddressError::One); + } + if AsRef::::as_ref(&value) == &Felt::TWO { + return Err(RegularContractAddressFromContractAddressError::Two); + } + if AsRef::::as_ref(&value) == &Felt::THREE { + return Err(RegularContractAddressFromContractAddressError::Three); + } + + Ok(RegularContractAddress(value)) + } +} + +#[derive(Debug)] +pub enum RegularContractAddressFromFeltError { + ContractAddress(ContractAddressFromFeltError), + SpecialAddress(RegularContractAddressFromContractAddressError), +} + +impl core::fmt::Display for RegularContractAddressFromFeltError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + RegularContractAddressFromFeltError::ContractAddress(e) => { + write!(f, "invalid contract address: {}", e) + } + RegularContractAddressFromFeltError::SpecialAddress(e) => { + write!(f, "value is a special contract address: {e}") + } + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for RegularContractAddressFromFeltError {} + +impl Felt { + /// Validates that a Felt value represents a valid Starknet contract address, + /// excluding the starknet reserved contract address `0x0`, `0x1`, `0x2` and `0x3`. + /// + /// https://docs.starknet.io/learn/protocol/state#special-addresses + pub fn is_regular_contract_address(&self) -> bool { + self >= &Felt::from(RegularContractAddress::LOWER_BOUND) + && self < &Felt::from(RegularContractAddress::UPPER_BOUND) + } +} + +impl TryFrom for RegularContractAddress { + type Error = RegularContractAddressFromFeltError; + + fn try_from(value: Felt) -> Result { + let contract_address = ContractAddress::try_from(value) + .map_err(RegularContractAddressFromFeltError::ContractAddress)?; + + RegularContractAddress::try_from(contract_address) + .map_err(RegularContractAddressFromFeltError::SpecialAddress) + } +} + +#[derive(Debug)] +pub enum RegularContractAddressFromStrError { + ContractAddress(ContractAddressFromStrError), + SpecialContractAddress(RegularContractAddressFromContractAddressError), +} + +impl core::fmt::Display for RegularContractAddressFromStrError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + RegularContractAddressFromStrError::ContractAddress(e) => { + write!(f, "invalid felt string: {e}") + } + RegularContractAddressFromStrError::SpecialContractAddress(e) => { + write!(f, "got special contract address: {e}") + } + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for RegularContractAddressFromStrError {} + +impl FromStr for RegularContractAddress { + type Err = RegularContractAddressFromStrError; + + fn from_str(s: &str) -> Result { + let contract_address = ContractAddress::from_str(s) + .map_err(RegularContractAddressFromStrError::ContractAddress)?; + + RegularContractAddress::try_from(contract_address) + .map_err(RegularContractAddressFromStrError::SpecialContractAddress) + } +} + +impl RegularContractAddress { + /// Create a new [RegularContractAddress] from an hex encoded string without checking it is a valid value. + /// + /// Should NEVER be used on user inputs, + /// as it can cause erroneous execution if dynamically initialized with bad values. + /// Should mostly be used at compilation time on hardcoded static string. + pub const fn from_hex_unchecked(s: &'static str) -> RegularContractAddress { + let contract_address = ContractAddress::from_hex_unchecked(s); + + RegularContractAddress(contract_address) + } +} + +#[cfg(test)] +mod test { + #[cfg(feature = "alloc")] + pub extern crate alloc; + use proptest::prelude::*; + + use crate::{ + felt::Felt, patricia_key::PATRICIA_KEY_UPPER_BOUND, + regular_contract_address::RegularContractAddress, + }; + + #[test] + fn basic_values() { + assert!(RegularContractAddress::try_from(Felt::ZERO).is_err()); + assert!(RegularContractAddress::try_from(Felt::ONE).is_err()); + assert!(RegularContractAddress::try_from(Felt::TWO).is_err()); + assert!(RegularContractAddress::try_from(Felt::THREE).is_err()); + assert!(RegularContractAddress::try_from(Felt::from(PATRICIA_KEY_UPPER_BOUND)).is_err()); + + let felt = Felt::from_hex_unwrap("0xcaffe"); + let contract_address = RegularContractAddress::try_from(felt).unwrap(); + assert_eq!(Felt::from(contract_address), felt); + } + + proptest! { + #[test] + fn is_valid_match_try_into(ref x in any::()) { + if x.is_regular_contract_address() { + prop_assert!(RegularContractAddress::try_from(*x).is_ok()); + } else { + prop_assert!(RegularContractAddress::try_from(*x).is_err()); + } + } + } +} diff --git a/crates/starknet-types-core/src/short_string/mod.rs b/crates/starknet-types-core/src/short_string/mod.rs index 5e2e4470..e899835f 100644 --- a/crates/starknet-types-core/src/short_string/mod.rs +++ b/crates/starknet-types-core/src/short_string/mod.rs @@ -7,7 +7,7 @@ //! A `ShortString` is string that have been checked and is guaranteed to be convertible into a valid `Felt`. //! It checks that the `String` only contains ascii characters and is no longer than 31 characters. //! -//! The convesion to `Felt` is done by using the internal ascii short string as bytes and parse those as a big endian number. +//! The conversion to `Felt` is done by using the internal ascii short string as bytes and parse those as a big endian number. use crate::felt::Felt; use core::str::FromStr;