Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .github/workflows/typo.yml
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
[workspace]
members = [
"crates/felt-macro",
"crates/starknet-types-core",
]

Expand Down
12 changes: 12 additions & 0 deletions crates/felt-macro/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[package]
name = "felt-macro"
version = "0.1.0"
edition = "2024"

[lib]
proc-macro = true

[dependencies]
syn = { version = "2.0", features = ["full"] }
quote = "1.0"
proc-macro2 = "1.0"
101 changes: 101 additions & 0 deletions crates/felt-macro/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
use proc_macro::TokenStream;
use quote::quote;
use syn::{Expr, Lit, parse_macro_input};

#[proc_macro]
pub fn felt(input: TokenStream) -> TokenStream {
let expr = parse_macro_input!(input as Expr);

match &expr {
Expr::Lit(expr_lit) => match &expr_lit.lit {
Lit::Str(lit_str) => {
let value = lit_str.value();

// Check if it's a hex string (starts with 0x or 0X)
if value.starts_with("0x") || value.starts_with("0X") {
// Hex string: use const fn for compile-time validation
quote! {
{
const __FELT_VALUE: Felt = Felt::from_hex_unwrap(#lit_str);
__FELT_VALUE
}
}
.into()
} else {
// Check for valid decimal format (optional leading minus, then digits)
let is_valid = if let Some(stripped) = value.strip_prefix('-') {
!stripped.is_empty() && stripped.chars().all(|c| c.is_ascii_digit())
} else {
!value.is_empty() && value.chars().all(|c| c.is_ascii_digit())
};

if !is_valid {
return syn::Error::new_spanned(
lit_str,
format!("Invalid Felt decimal string literal: '{}'. Expected decimal digits (0-9), optionally prefixed with '-'.", value)
)
.to_compile_error()
.into();
}

// Valid format, generate runtime parsing code
quote! {
match <Felt as ::core::str::FromStr>::from_str(#lit_str) {
Ok(f) => f,
Err(_) => panic!(concat!("Invalid Felt decimal string literal: ", #lit_str)),
}
}
.into()
}
}

Lit::Bool(lit_bool) => quote! {
match #lit_bool {
true => Felt::ONE,
false => Felt::ZERO,
}
}
.into(),

Lit::Int(lit_int) => quote! {
Felt::from(#lit_int)
}
.into(),

_ => panic!("Unsupported literal type for felt! macro"),
},

// Handle negative integer literals: -42, -123, etc.
Expr::Unary(expr_unary) if matches!(expr_unary.op, syn::UnOp::Neg(_)) => {
if let Expr::Lit(syn::ExprLit {
lit: Lit::Int(lit_int),
..
}) = &*expr_unary.expr
{
// Negative integer literal
quote! {
Felt::from(-#lit_int)
}
.into()
} else {
// Some other unary negation, treat as expression
quote! {
match <Felt as ::core::str::FromStr>::from_str(&#expr) {
Ok(f) => f,
Err(_) => panic!("Invalid Felt value"),
}
}
.into()
}
}

// Anything else is handled as a string and will fail if it is not one
_ => quote! {
match Felt::try_from(#expr) {
Ok(f) => f,
Err(_) => panic!("Invalid Felt value"),
}
}
.into(),
}
}
3 changes: 3 additions & 0 deletions crates/starknet-types-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ rand = { version = "0.9.2", default-features = false, optional = true }
# TODO: in the future remove this dependency to allow upstream crates to use version 1.X.X of generic-array
generic-array = { version = ">=0.14.0, <=0.14.7", default-features = false }

# macro
felt-macro = { path = "../felt-macro" }

[features]
default = ["std", "serde", "curve", "num-traits"]
std = [
Expand Down
2 changes: 1 addition & 1 deletion crates/starknet-types-core/src/felt/alloc_impls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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}")
Expand Down
76 changes: 76 additions & 0 deletions crates/starknet-types-core/src/felt/macro_impl.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/// Handy macro to initialize `Felt`
//
/// Accepts:
/// - booleans
/// - positive and negative number literals (eg. `5`, `12u8`, `-77`, `-2007i32`)
/// - positive and negative decimal string literals (eg. `"5"`, `"-12"`)
/// - positive hexadecimal string literal (eg. `"0x0"`, `"0x42"`)
/// - variables of any type that implements `TryFrom<T> for Felt` (eg. `u32`, `i128`, `&str`, `String`)
/// - functions and closure which return type implements `TryFrom<T> for Felt` (eg. `|x| x * 42`, `fn ret42() -> u32 { 42 }` )
/// - code block (eg. `{40 + 2}`) and more generally any expression that returns as types that implements `TryFrom<T> for Felt`
///
/// Use in `const` expression is only possible using literal `bool` and literal hex string
/// because the other types rely on non-`const` function for conversion (eg. `From::from` for numbers).
#[macro_export]
macro_rules! felt {
($($tt:tt)*) => {{
let felt: $crate::felt::Felt = felt_macro::felt!($($tt)*);
felt
}};
}

#[cfg(test)]
mod tests {
#[cfg(feature = "alloc")]
pub extern crate alloc;

use crate::felt::Felt;

#[test]
fn felt_macro() {
// Bools
assert_eq!(felt!(false), Felt::ZERO);
assert_eq!(felt!(true), Felt::ONE);

// Primitive numbers
assert_eq!(felt!(42), Felt::from(42));
assert_eq!(felt!(42u8), Felt::from(42));
assert_eq!(felt!(42i8), Felt::from(42));
assert_eq!(felt!(42u128), Felt::from(42));
assert_eq!(felt!(-42), Felt::ZERO - Felt::from(42));
assert_eq!(felt!(-42i8), Felt::ZERO - Felt::from(42));

// Static &str
assert_eq!(felt!("42"), Felt::from(42));
assert_eq!(felt!("-42"), Felt::ZERO - Felt::from(42));
assert_eq!(felt!("0x42"), Felt::from_hex_unwrap("0x42"));

// Variables
let x = true;
assert_eq!(felt!(x), Felt::ONE);
let x = "42";
assert_eq!(felt!(x), Felt::from(42));
let x = alloc::string::String::from("42");
assert_eq!(felt!(x), Felt::from(42));
let x = 42u32;
assert_eq!(felt!(x), Felt::from(42));
let x = -42;
assert_eq!(felt!(x), Felt::ZERO - Felt::from(42));

// Expressions
let double_closure = |x| x * 2;
assert_eq!(felt!(double_closure(5)), Felt::from(10));
assert_eq!(felt!({ 40 + 2 }), Felt::from(42));

// Constants
const X: &str = "42";
assert_eq!(felt!(X), Felt::from(42));
const Y: u32 = 42;
assert_eq!(felt!(Y), Felt::from(42));

// Use in const expressions
const _: Felt = felt!("0x42");
const _: Felt = felt!(true);
const _: Felt = felt!(false);
}
}
28 changes: 23 additions & 5 deletions crates/starknet-types-core/src/felt/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ mod apollo_serialization;
mod arbitrary;
#[cfg(test)]
mod felt_arbitrary;
mod macro_impl;
mod non_zero;
#[cfg(feature = "num-traits")]
mod num_traits_impl;
Expand Down Expand Up @@ -455,6 +456,23 @@ impl FromStr for Felt {
}
}

#[cfg(feature = "alloc")]
impl TryFrom<alloc::string::String> for Felt {
type Error = FromStrError;

fn try_from(value: alloc::string::String) -> Result<Self, Self::Error> {
Felt::from_str(&value)
}
}

impl TryFrom<&str> for Felt {
type Error = FromStrError;

fn try_from(value: &str) -> Result<Self, Self::Error> {
Felt::from_str(value)
}
}

impl Add<&Felt> for u64 {
type Output = Option<u64>;

Expand All @@ -473,22 +491,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))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion crates/starknet-types-core/src/short_string/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion rust-toolchain.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[toolchain]
channel = "1.87.0"
channel = "1.89.0"
components = ["rustfmt", "clippy", "rust-analyzer"]
profile = "minimal"