Skip to content

Commit bea90b3

Browse files
committed
feat: add ContractAddress type
1 parent cb28f70 commit bea90b3

File tree

3 files changed

+177
-1
lines changed

3 files changed

+177
-1
lines changed

crates/starknet-types-core/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ serde = { version = "1", optional = true, default-features = false, features = [
2424
"alloc", "derive"
2525
] }
2626
lambdaworks-crypto = { version = "0.12.0", default-features = false, optional = true }
27-
parity-scale-codec = { version = "3.6", default-features = false, optional = true }
27+
parity-scale-codec = { version = "3.6", default-features = false, features = ["derive"], optional = true }
2828
lazy_static = { version = "1.5", default-features = false, optional = true }
2929
zeroize = { version = "1.8.1", default-features = false, optional = true }
3030
subtle = { version = "2.6.1", default-features = false, optional = true }
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
//! A starknet contract address
2+
//!
3+
//! In starknet valid contract addresses exists as a subset of the type `Felt`.
4+
//! Therefore some checks must be done in order to produce protocol valid addresses.
5+
//! This module provides this logic as a type `ContractAddress`, that can garantee the validity of the address.
6+
//! It also comes with some quality of life methods.
7+
8+
use core::str::FromStr;
9+
10+
use crate::felt::Felt;
11+
12+
#[repr(transparent)]
13+
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
14+
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
15+
#[cfg_attr(feature = "serde", serde(transparent))]
16+
#[cfg_attr(
17+
feature = "parity-scale-codec",
18+
derive(parity_scale_codec::Encode, parity_scale_codec::Decode)
19+
)]
20+
pub struct ContractAddress(Felt);
21+
22+
impl core::fmt::Display for ContractAddress {
23+
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
24+
write!(f, "{}", self.0)
25+
}
26+
}
27+
28+
impl AsRef<Felt> for ContractAddress {
29+
fn as_ref(&self) -> &Felt {
30+
&self.0
31+
}
32+
}
33+
34+
impl From<ContractAddress> for Felt {
35+
fn from(value: ContractAddress) -> Self {
36+
value.0
37+
}
38+
}
39+
40+
#[derive(Debug, Copy, Clone)]
41+
/// In Starknet, contract addresses must follow specific constraints to be considered valid:
42+
/// - They must be greater than or equal to 2, as addresses 0 and 1 are reserved for system use:
43+
/// * 0x0 acts as the default caller address for external calls and has no storage
44+
/// * 0x1 functions as a storage space for block mapping [link](https://docs.starknet.io/architecture-and-concepts/network-architecture/starknet-state/#special_addresses)
45+
/// - They must be less than 2^251 (0x800000000000000000000000000000000000000000000000000000000000000)
46+
///
47+
/// Making the valid addressabe range be [2, 2^251)
48+
pub enum ContactAddressFromFeltError {
49+
Zero,
50+
One,
51+
TooBig,
52+
}
53+
54+
impl core::fmt::Display for ContactAddressFromFeltError {
55+
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
56+
match self {
57+
ContactAddressFromFeltError::Zero => {
58+
write!(
59+
f,
60+
"address 0x0 is reserved as the default caller address and has no storage"
61+
)
62+
}
63+
ContactAddressFromFeltError::One => {
64+
write!(
65+
f,
66+
"address 0x1 is reserved as storage space for block mapping"
67+
)
68+
}
69+
ContactAddressFromFeltError::TooBig => {
70+
write!(f, "the highest possible address is 2^251 - 1")
71+
}
72+
}
73+
}
74+
}
75+
76+
#[cfg(feature = "std")]
77+
impl std::error::Error for ContactAddressFromFeltError {}
78+
79+
const ADDRESS_UPPER_BOUND: Felt =
80+
Felt::from_hex_unchecked("0x800000000000000000000000000000000000000000000000000000000000000");
81+
82+
/// Validates that a Felt value represents a valid Starknet contract address.
83+
///
84+
/// This validation is critical for preventing funds from being sent to invalid addresses,
85+
/// which would result in permanent loss.
86+
impl Felt {
87+
pub fn is_valid_contract_address(&self) -> bool {
88+
self >= &Felt::from(2u64) && self < &ADDRESS_UPPER_BOUND
89+
}
90+
}
91+
92+
impl TryFrom<Felt> for ContractAddress {
93+
type Error = ContactAddressFromFeltError;
94+
95+
fn try_from(value: Felt) -> Result<Self, Self::Error> {
96+
if value == Felt::ZERO {
97+
return Err(ContactAddressFromFeltError::Zero);
98+
}
99+
if value == Felt::ONE {
100+
return Err(ContactAddressFromFeltError::One);
101+
}
102+
if value >= ADDRESS_UPPER_BOUND {
103+
return Err(ContactAddressFromFeltError::TooBig);
104+
}
105+
106+
Ok(ContractAddress(value))
107+
}
108+
}
109+
110+
#[derive(Debug)]
111+
pub enum ContractAddressFromStrError {
112+
BadFelt(<Felt as FromStr>::Err),
113+
BadAddress(ContactAddressFromFeltError),
114+
}
115+
116+
impl core::fmt::Display for ContractAddressFromStrError {
117+
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
118+
match self {
119+
ContractAddressFromStrError::BadFelt(e) => write!(f, "invalid felt string: {e}"),
120+
ContractAddressFromStrError::BadAddress(e) => write!(f, "invalid address value: {e}"),
121+
}
122+
}
123+
}
124+
125+
impl FromStr for ContractAddress {
126+
type Err = ContractAddressFromStrError;
127+
128+
fn from_str(s: &str) -> Result<Self, Self::Err> {
129+
let felt = Felt::from_str(s).map_err(ContractAddressFromStrError::BadFelt)?;
130+
let contract_address =
131+
ContractAddress::try_from(felt).map_err(ContractAddressFromStrError::BadAddress)?;
132+
133+
Ok(contract_address)
134+
}
135+
}
136+
137+
impl ContractAddress {
138+
pub const fn from_hex_unchecked(s: &str) -> ContractAddress {
139+
let felt = Felt::from_hex_unchecked(s);
140+
141+
ContractAddress(felt)
142+
}
143+
}
144+
145+
#[cfg(test)]
146+
mod test {
147+
use proptest::prelude::*;
148+
149+
use crate::{
150+
contract_address::{ContractAddress, ADDRESS_UPPER_BOUND},
151+
felt::Felt,
152+
};
153+
154+
#[test]
155+
fn basic_values() {
156+
assert!(ContractAddress::try_from(Felt::ZERO).is_err());
157+
assert!(ContractAddress::try_from(Felt::ONE).is_err());
158+
assert!(ContractAddress::try_from(ADDRESS_UPPER_BOUND).is_err());
159+
160+
let felt = Felt::TWO;
161+
let contract_address = ContractAddress::try_from(felt).unwrap();
162+
assert_eq!(Felt::from(contract_address), felt);
163+
}
164+
165+
proptest! {
166+
#[test]
167+
fn is_valid_match_try_into(ref x in any::<Felt>()) {
168+
if x.is_valid_contract_address() {
169+
prop_assert!(ContractAddress::try_from(*x).is_ok());
170+
} else {
171+
prop_assert!(ContractAddress::try_from(*x).is_err());
172+
}
173+
}
174+
}
175+
}

crates/starknet-types-core/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ pub mod hash;
88
pub mod felt;
99
pub mod qm31;
1010

11+
pub mod contract_address;
1112
#[cfg(any(feature = "std", feature = "alloc"))]
1213
pub mod short_string;
1314
pub mod u256;

0 commit comments

Comments
 (0)