diff --git a/contracts/src/gen.cairo b/contracts/src/gen.cairo index fe47df7..b449be4 100644 --- a/contracts/src/gen.cairo +++ b/contracts/src/gen.cairo @@ -8,6 +8,19 @@ 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, + f9: Array, + } + #[derive(Serde, Drop)] struct MyStruct { f1: felt252, @@ -15,6 +28,21 @@ mod gen { 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) { self.v1.write(a.f1); @@ -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) {} } + diff --git a/crates/cairo-serde/src/lib.rs b/crates/cairo-serde/src/lib.rs index 034d753..333d245 100644 --- a/crates/cairo-serde/src/lib.rs +++ b/crates/cairo-serde/src/lib.rs @@ -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::*; @@ -51,3 +52,62 @@ pub trait CairoSerde { /// Deserializes an array of felts into the given type. fn cairo_deserialize(felts: &[Felt], offset: usize) -> Result; } + +/// Serialize a value as a hex string. +pub fn serialize_as_hex(value: &T, serializer: S) -> std::result::Result +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( + value: &Vec, + serializer: S, +) -> std::result::Result +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( + value: &(T1, T2), + serializer: S, +) -> std::result::Result +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( + value: &(T1, T2, T3), + serializer: S, +) -> std::result::Result +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() +} diff --git a/crates/rs/src/expand/enum.rs b/crates/rs/src/expand/enum.rs index a61542a..405b169 100644 --- a/crates/rs/src/expand/enum.rs +++ b/crates/rs/src/expand/enum.rs @@ -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))); } } diff --git a/crates/rs/src/expand/struct.rs b/crates/rs/src/expand/struct.rs index 606d410..7b952ba 100644 --- a/crates/rs/src/expand/struct.rs +++ b/crates/rs/src/expand/struct.rs @@ -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)); } } @@ -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),* } } } diff --git a/crates/rs/src/expand/utils.rs b/crates/rs/src/expand/utils.rs index a38951a..5400aca 100644 --- a/crates/rs/src/expand/utils.rs +++ b/crates/rs/src/expand/utils.rs @@ -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"), SerdeHexType::Vec); + assert_eq!(is_serde_hex_vec("Vec"), SerdeHexType::Vec); + assert_eq!(is_serde_hex_vec("Vec"), SerdeHexType::None); + assert_eq!(is_serde_hex_vec("Vec"), SerdeHexType::None); + } +} diff --git a/examples/cairo_serde_derive.rs b/examples/cairo_serde_derive.rs index 9fd4cb2..7c0b094 100644 --- a/examples/cairo_serde_derive.rs +++ b/examples/cairo_serde_derive.rs @@ -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, 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); #[derive(Debug, CairoSerde, PartialEq)] @@ -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, }, }; diff --git a/examples/structs.rs b/examples/structs.rs index deeac0b..feb5fb6 100644 --- a/examples/structs.rs +++ b/examples/structs.rs @@ -1,22 +1,60 @@ use cainome::rs::abigen; - use starknet::core::types::Felt; abigen!( MyContract, "./contracts/abi/gen.abi.json", - derives(Debug, Clone) + derives(Debug, Clone, serde::Serialize) ); #[tokio::main] async fn main() { - let s = MyStruct:: { - f1: Felt::ONE, - f2: Felt::TWO, - f3: Felt::THREE, + let s = PlainStruct { + f1: 1, + f2: 2, + f3: 3, + f4: 4, + f5: 5, + f6: Felt::from(6), + f7: (Felt::from(7), 8), + f8: vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + f9: vec![1_u128, 2_u128], }; - println!("{:?}", s); + println!("{}", serde_json::to_string(&s).unwrap()); let _s2 = s.clone(); + + let e = MyEnum::One(1_u8); + println!("{}", serde_json::to_string(&e).unwrap()); + + let e = MyEnum::Two(1_u16); + println!("{}", serde_json::to_string(&e).unwrap()); + + let e = MyEnum::Three(1_u32); + println!("{}", serde_json::to_string(&e).unwrap()); + + let e = MyEnum::Four(1_u64); + println!("{}", serde_json::to_string(&e).unwrap()); + + let e = MyEnum::Five(1_u128); + println!("{}", serde_json::to_string(&e).unwrap()); + + let e = MyEnum::Six(Felt::from(6)); + println!("{}", serde_json::to_string(&e).unwrap()); + + let e = MyEnum::Seven(-1_i32); + println!("{}", serde_json::to_string(&e).unwrap()); + + let e = MyEnum::Eight(-1_i64); + println!("{}", serde_json::to_string(&e).unwrap()); + + let e = MyEnum::Nine(-1_i128); + println!("{}", serde_json::to_string(&e).unwrap()); + + let e = MyEnum::Ten((1_u8, 1_u128)); + println!("{}", serde_json::to_string(&e).unwrap()); + + let e = MyEnum::Eleven((Felt::from(1), 1_u8, 1_u128)); + println!("{}", serde_json::to_string(&e).unwrap()); }