Skip to content

Commit

Permalink
feat: serialize integers higher than u32 in hex format (#65)
Browse files Browse the repository at this point in the history
* feat: serialize integers higher than u32 in hex format

* fix: add support for tuple of size 2 and 3

Due to current naive approach for this serde as hex,
only tuple of integers/felts are supported.

* fix: scarb fmt

* fix: add support for arrays

* fix: add missing fmt
  • Loading branch information
glihm authored Oct 18, 2024
1 parent aef5b49 commit 8b8ca8e
Show file tree
Hide file tree
Showing 7 changed files with 302 additions and 18 deletions.
35 changes: 35 additions & 0 deletions contracts/src/gen.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,41 @@ mod gen {
v2: felt252,
}

#[derive(Serde, Drop)]
struct PlainStruct {
f1: u8,
f2: u16,
f3: u32,
f4: u64,
f5: u128,
f6: felt252,
f7: (felt252, u64),
f8: Array<u8>,
f9: Array<u128>,
}

#[derive(Serde, Drop)]
struct MyStruct<T> {
f1: felt252,
f2: T,
f3: felt252,
}

#[derive(Serde, Drop)]
enum MyEnum {
One: u8,
Two: u16,
Three: u32,
Four: u64,
Five: u128,
Six: felt252,
Seven: i32,
Eight: i64,
Nine: i128,
Ten: (u8, u128),
Eleven: (felt252, u8, u128),
}

#[external(v0)]
fn func1(ref self: ContractState, a: MyStruct<felt252>) {
self.v1.write(a.f1);
Expand All @@ -31,4 +59,11 @@ mod gen {
fn read(self: @ContractState) -> (felt252, felt252) {
(self.v1.read(), self.v2.read())
}

#[external(v0)]
fn func3(self: @ContractState, _a: PlainStruct) {}

#[external(v0)]
fn func4(self: @ContractState, _a: MyEnum) {}
}

60 changes: 60 additions & 0 deletions crates/cairo-serde/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ pub use error::{Error, Result};

pub mod call;
pub mod types;
use serde::ser::SerializeSeq;
pub use types::array_legacy::*;
pub use types::byte_array::*;
pub use types::non_zero::*;
Expand Down Expand Up @@ -51,3 +52,62 @@ pub trait CairoSerde {
/// Deserializes an array of felts into the given type.
fn cairo_deserialize(felts: &[Felt], offset: usize) -> Result<Self::RustType>;
}

/// Serialize a value as a hex string.
pub fn serialize_as_hex<S, T>(value: &T, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
T: serde::Serialize + std::fmt::LowerHex,
{
serializer.serialize_str(&format!("{:#x}", value))
}

/// Serialize a vector of values as a hex string.
pub fn serialize_as_hex_vec<S, T>(
value: &Vec<T>,
serializer: S,
) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
T: serde::Serialize + std::fmt::LowerHex,
{
let mut seq = serializer.serialize_seq(Some(value.len()))?;
for v in value {
seq.serialize_element(&format!("{:#x}", v))?;
}
seq.end()
}

/// Serialize a tuple of two values as a hex string.
pub fn serialize_as_hex_t2<S, T1, T2>(
value: &(T1, T2),
serializer: S,
) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
T1: serde::Serialize + std::fmt::LowerHex,
T2: serde::Serialize + std::fmt::LowerHex,
{
let mut seq = serializer.serialize_seq(Some(2))?;
seq.serialize_element(&format!("{:#x}", value.0))?;
seq.serialize_element(&format!("{:#x}", value.1))?;
seq.end()
}

/// Serialize a tuple of three values as a hex string.
pub fn serialize_as_hex_t3<S, T1, T2, T3>(
value: &(T1, T2, T3),
serializer: S,
) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
T1: serde::Serialize + std::fmt::LowerHex,
T2: serde::Serialize + std::fmt::LowerHex,
T3: serde::Serialize + std::fmt::LowerHex,
{
let mut seq = serializer.serialize_seq(Some(2))?;
seq.serialize_element(&format!("{:#x}", value.0))?;
seq.serialize_element(&format!("{:#x}", value.1))?;
seq.serialize_element(&format!("{:#x}", value.2))?;
seq.end()
}
6 changes: 4 additions & 2 deletions crates/rs/src/expand/enum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,12 @@ impl CairoEnum {
let name = utils::str_to_ident(&inner.name);
let ty = utils::str_to_type(&inner.token.to_rust_type());

let serde = utils::serde_hex_derive(&inner.token.to_rust_type());

if inner.token.type_name() == "()" {
variants.push(quote!(#name));
variants.push(quote!(#serde #name));
} else {
variants.push(quote!(#name(#ty)));
variants.push(quote!(#serde #name(#ty)));
}
}

Expand Down
14 changes: 8 additions & 6 deletions crates/rs/src/expand/struct.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,19 @@ impl CairoStruct {
let name = utils::str_to_ident(&inner.name);
let ty = utils::str_to_type(&inner.token.to_rust_type());

let serde = utils::serde_hex_derive(&inner.token.to_rust_type());

// r#{name} is not a valid identifier, thus we can't create an ident.
// And with proc macro 2, we cannot do `quote!(r##name)`.
// TODO: this needs to be done more elegantly...
if &inner.name == "type" {
members.push(quote!(r#type: #ty));
members.push(quote!(#serde pub r#type: #ty));
} else if &inner.name == "move" {
members.push(quote!(r#move: #ty));
members.push(quote!(#serde pub r#move: #ty));
} else if &inner.name == "final" {
members.push(quote!(r#final: #ty));
members.push(quote!(#serde pub r#final: #ty));
} else {
members.push(quote!(#name: #ty));
members.push(quote!(#serde pub #name: #ty));
}
}

Expand All @@ -57,14 +59,14 @@ impl CairoStruct {
quote! {
#[derive(#(#internal_derives,)*)]
pub struct #struct_name<#(#gen_args),*> {
#(pub #members),*
#(#members),*
}
}
} else {
quote! {
#[derive(#(#internal_derives,)*)]
pub struct #struct_name {
#(pub #members),*
#(#members),*
}
}
}
Expand Down
139 changes: 139 additions & 0 deletions crates/rs/src/expand/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,142 @@ pub fn rust_associated_type_gen_args(entity_name: &Ident, gen_args: &[Ident]) ->

quote!(type RustType = #entity_name<#(#gen_args_rust),*>;)
}

#[derive(Debug, PartialEq)]
enum SerdeHexType {
None,
Single,
Tuple(usize),
Vec,
}

impl SerdeHexType {
pub fn is_none(&self) -> bool {
matches!(self, SerdeHexType::None)
}
}

/// Serde derive for hex serialization of struct member or enum variant.
/// In the case of tuples, all the elements will be serialized as hex.
pub fn serde_hex_derive(ty: &str) -> TokenStream2 {
let serde_single = format!("{}::serialize_as_hex", cainome_cairo_serde_path());
let serde_vec = format!("{}::serialize_as_hex_vec", cainome_cairo_serde_path());
let serde_tuple_2 = format!("{}::serialize_as_hex_t2", cainome_cairo_serde_path());
let serde_tuple_3 = format!("{}::serialize_as_hex_t3", cainome_cairo_serde_path());

let serde_hex = is_serde_hex_int(ty);

match serde_hex {
SerdeHexType::None => quote!(),
SerdeHexType::Single => quote! {
#[serde(serialize_with = #serde_single)]
},
SerdeHexType::Tuple(2) => quote! {
#[serde(serialize_with = #serde_tuple_2)]
},
SerdeHexType::Tuple(3) => quote! {
#[serde(serialize_with = #serde_tuple_3)]
},
SerdeHexType::Vec => quote! {
#[serde(serialize_with = #serde_vec)]
},
_ => panic!("Unsupported type {} for serde_hex", ty),
}
}

/// To simplify the serde interop with client in javascript,
/// we use the hex format for all the types greater than u32.
/// IEEE 754 standard for floating-point arithmetic,
/// which can safely represent integers up to 2^53 - 1, which is what Javascript uses.
///
/// This function returns the number of type that require serde_hex serialization.
/// The type might be a single integer type, or a tuple of integer types.
#[inline]
fn is_serde_hex_int(ty: &str) -> SerdeHexType {
let tuple = is_serde_hex_tuple(ty);
if !tuple.is_none() {
return tuple;
}

let vec = is_serde_hex_vec(ty);
if !vec.is_none() {
return vec;
}

if ty == "u128" || ty == "u64" || ty == "i128" || ty == "i64" {
return SerdeHexType::Single;
}

SerdeHexType::None
}

/// Checks if the type is a tuple of integers that should be serialized as hex.
fn is_serde_hex_tuple(ty: &str) -> SerdeHexType {
if ty.starts_with('(') && ty.ends_with(')') {
let elements: Vec<&str> = ty[1..ty.len() - 1].split(',').collect();

let has_hex_int = elements
.iter()
.any(|t| !is_serde_hex_int(t.trim()).is_none());

if has_hex_int {
return SerdeHexType::Tuple(elements.len());
} else {
return SerdeHexType::None;
}
}

SerdeHexType::None
}

/// Checks if the type is a vector of integers that should be serialized as hex.
fn is_serde_hex_vec(ty: &str) -> SerdeHexType {
if ty.starts_with("Vec<") && ty.ends_with('>') {
let inner_type = &ty[4..ty.len() - 1];

if !is_serde_hex_int(inner_type).is_none() {
return SerdeHexType::Vec;
} else {
return SerdeHexType::None;
}
}

SerdeHexType::None
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_is_serde_hex_int() {
assert_eq!(is_serde_hex_int("u128"), SerdeHexType::Single);
assert_eq!(is_serde_hex_int("u64"), SerdeHexType::Single);
assert_eq!(is_serde_hex_int("i128"), SerdeHexType::Single);
assert_eq!(is_serde_hex_int("i64"), SerdeHexType::Single);
assert_eq!(is_serde_hex_int("u32"), SerdeHexType::None);
assert_eq!(is_serde_hex_int("u16"), SerdeHexType::None);
assert_eq!(is_serde_hex_int("u8"), SerdeHexType::None);
assert_eq!(is_serde_hex_int("i32"), SerdeHexType::None);
assert_eq!(is_serde_hex_int("i16"), SerdeHexType::None);
assert_eq!(is_serde_hex_int("i8"), SerdeHexType::None);
}

#[test]
fn test_is_serde_hex_tuple() {
assert_eq!(is_serde_hex_tuple("(u32, u64)"), SerdeHexType::Tuple(2));
assert_eq!(
is_serde_hex_tuple("(u128, u64, i128)"),
SerdeHexType::Tuple(3)
);
assert_eq!(is_serde_hex_tuple("(felt252, u32)"), SerdeHexType::None);
}

#[test]
fn test_is_serde_hex_vec() {
assert_eq!(is_serde_hex_vec("Vec<u128>"), SerdeHexType::Vec);
assert_eq!(is_serde_hex_vec("Vec<i64>"), SerdeHexType::Vec);
assert_eq!(is_serde_hex_vec("Vec<u32>"), SerdeHexType::None);
assert_eq!(is_serde_hex_vec("Vec<MyStruct>"), SerdeHexType::None);
}
}
14 changes: 11 additions & 3 deletions examples/cairo_serde_derive.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
use cainome_cairo_serde::CairoSerde;
use cainome_cairo_serde_derive::CairoSerde;
use serde::Serialize;
use starknet_types_core::felt::Felt;

#[derive(Debug, CairoSerde, PartialEq)]
#[derive(Debug, CairoSerde, PartialEq, Serialize)]
struct ExampleSimple {
x: Vec<Felt>,
y: Felt,
#[serde(serialize_with = "cainome_cairo_serde::serialize_as_hex")]
z: u128,
}

#[derive(Debug, CairoSerde, PartialEq)]
#[derive(Debug, CairoSerde, PartialEq, Serialize)]
struct ExampleNested {
x: Felt,
y: ExampleSimple,
}

#[derive(Debug, CairoSerde, PartialEq)]
#[derive(Debug, CairoSerde, PartialEq, Serialize)]
struct ExampleTuple(ExampleNested, Vec<Felt>);

#[derive(Debug, CairoSerde, PartialEq)]
Expand All @@ -32,16 +35,21 @@ fn main() {
y: ExampleSimple {
x: vec![Felt::from(2), Felt::from(3)],
y: Felt::from(4),
z: 1729281360,
},
},
vec![Felt::from(1)],
);

let s = serde_json::to_string(&tuple).unwrap();
println!("s = {}", s);

let example = ExampleEnum::Struct {
x: tuple,
y: ExampleSimple {
x: vec![Felt::from(5), Felt::from(6)],
y: Felt::from(7),
z: 1729281360,
},
};

Expand Down
Loading

0 comments on commit 8b8ca8e

Please sign in to comment.