diff --git a/Cargo.lock b/Cargo.lock index 3922fa55..21652b6b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1522,9 +1522,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.86" +version = "1.0.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +checksum = "7c3a7fc5db1e57d5a779a352c8cdb57b29aa4c40cc69c3a68a7fedc815fbf2f9" dependencies = [ "unicode-ident", ] @@ -2039,6 +2039,7 @@ dependencies = [ "starknet-accounts", "starknet-contract", "starknet-core", + "starknet-core-derive", "starknet-crypto", "starknet-macros", "starknet-providers", @@ -2092,17 +2093,28 @@ dependencies = [ "flate2", "hex", "hex-literal", + "num-traits", "serde", "serde_json", "serde_json_pythonic", "serde_with", "sha3", "starknet-core", + "starknet-core-derive", "starknet-crypto", "starknet-types-core", "wasm-bindgen-test", ] +[[package]] +name = "starknet-core-derive" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + [[package]] name = "starknet-crypto" version = "0.7.2" diff --git a/Cargo.toml b/Cargo.toml index 831cc42a..3e2ad949 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ exclude = [".github/**", "images/**"] [workspace] members = [ "starknet-core", + "starknet-core-derive", "starknet-providers", "starknet-contract", "starknet-crypto", @@ -34,6 +35,7 @@ all-features = true [dependencies] starknet-crypto = { version = "0.7.2", path = "./starknet-crypto" } starknet-core = { version = "0.12.0", path = "./starknet-core", default-features = false } +starknet-core-derive = { version = "0.1.0", path = "./starknet-core-derive", features = ["import_from_starknet"] } starknet-providers = { version = "0.12.0", path = "./starknet-providers" } starknet-contract = { version = "0.11.0", path = "./starknet-contract" } starknet-signers = { version = "0.10.0", path = "./starknet-signers" } diff --git a/README.md b/README.md index 3371af44..c6d4e2a8 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ This workspace contains the following crates: - `starknet-accounts`: Types for handling Starknet account abstraction - `starknet-curve`: Starknet curve operations - `starknet-macros`: Useful macros for using the `starknet` crates +- `starknet-core-derive`: Derive macros for traits in `starknet-core` ## WebAssembly @@ -92,21 +93,23 @@ Examples can be found in the [examples folder](./examples): 6. [Query the latest block number with JSON-RPC](./examples/jsonrpc.rs) -7. [Batched JSON-RPC requests](./examples/batch.rs) +7. [Encoding and decoding Cairo types](./examples/serde.rs) -8. [Call a contract view function](./examples/erc20_balance.rs) +8. [Batched JSON-RPC requests](./examples/batch.rs) -9. [Deploy an Argent X account to a pre-funded address](./examples/deploy_argent_account.rs) +9. [Call a contract view function](./examples/erc20_balance.rs) -10. [Inspect public key with Ledger](./examples/ledger_public_key.rs) +10. [Deploy an Argent X account to a pre-funded address](./examples/deploy_argent_account.rs) -11. [Deploy an OpenZeppelin account with Ledger](./examples/deploy_account_with_ledger.rs) +11. [Inspect public key with Ledger](./examples/ledger_public_key.rs) -12. [Transfer ERC20 tokens with Ledger](./examples/transfer_with_ledger.rs) +12. [Deploy an OpenZeppelin account with Ledger](./examples/deploy_account_with_ledger.rs) -13. [Parsing a JSON-RPC request on the server side](./examples/parse_jsonrpc_request.rs) +13. [Transfer ERC20 tokens with Ledger](./examples/transfer_with_ledger.rs) -14. [Inspecting a erased provider-specific error type](./examples/downcast_provider_error.rs) +14. [Parsing a JSON-RPC request on the server side](./examples/parse_jsonrpc_request.rs) + +15. [Inspecting a erased provider-specific error type](./examples/downcast_provider_error.rs) ## License diff --git a/assets/CORE_DERIVE_README.md b/assets/CORE_DERIVE_README.md new file mode 120000 index 00000000..430edd5f --- /dev/null +++ b/assets/CORE_DERIVE_README.md @@ -0,0 +1 @@ +../starknet-core-derive/README.md \ No newline at end of file diff --git a/examples/serde.rs b/examples/serde.rs new file mode 100644 index 00000000..4c80d530 --- /dev/null +++ b/examples/serde.rs @@ -0,0 +1,34 @@ +use starknet::{ + core::{ + codec::{Decode, Encode}, + types::Felt, + }, + macros::felt, +}; + +#[derive(Debug, Eq, PartialEq, Encode, Decode)] +struct CairoType { + a: Felt, + b: Option, + c: bool, +} + +fn main() { + let instance = CairoType { + a: felt!("123456789"), + b: Some(100), + c: false, + }; + + let mut serialized = vec![]; + instance.encode(&mut serialized).unwrap(); + + assert_eq!( + serialized, + [felt!("123456789"), felt!("0"), felt!("100"), felt!("0")] + ); + + let restored = CairoType::decode(&serialized).unwrap(); + + assert_eq!(instance, restored); +} diff --git a/starknet-core-derive/Cargo.toml b/starknet-core-derive/Cargo.toml new file mode 100644 index 00000000..bcc687fa --- /dev/null +++ b/starknet-core-derive/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "starknet-core-derive" +version = "0.1.0" +authors = ["Jonathan LEI "] +license = "MIT OR Apache-2.0" +edition = "2021" +readme = "README.md" +repository = "https://github.com/xJonathanLEI/starknet-rs" +homepage = "https://starknet.rs/" +description = """ +Procedural macros for `starknet-core` +""" +keywords = ["ethereum", "starknet", "web3"] + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "1.0.88" +quote = "1.0.37" +syn = "2.0.15" + +[features] +default = [] +import_from_starknet = [] + +[lints] +workspace = true diff --git a/starknet-core-derive/README.md b/starknet-core-derive/README.md new file mode 100644 index 00000000..42911dd7 --- /dev/null +++ b/starknet-core-derive/README.md @@ -0,0 +1,14 @@ +# Procedural macros for `starknet-core` + +This crate provides procedural macros for deriving the `Encode` and `Decode` traits from `starknet-core`. This allows defining a type like: + +```rust +#[derive(Debug, PartialEq, Eq, Decode, Encode)] +struct CairoType { + a: Felt, + b: U256, + c: bool, +} +``` + +and using the `::encode()` and `::decode()` methods, without manually implementing the corresponding traits. diff --git a/starknet-core-derive/src/lib.rs b/starknet-core-derive/src/lib.rs new file mode 100644 index 00000000..b7aadf85 --- /dev/null +++ b/starknet-core-derive/src/lib.rs @@ -0,0 +1,373 @@ +//! Procedural derive macros for the `starknet-core` crate. + +#![deny(missing_docs)] + +use proc_macro::TokenStream; +use proc_macro2::Span; +use quote::quote; +use syn::{ + parse::{Error as ParseError, Parse, ParseStream}, + parse_macro_input, DeriveInput, Fields, LitInt, LitStr, Meta, Token, +}; + +#[derive(Default)] +struct Args { + core: Option, +} + +impl Args { + fn merge(&mut self, other: Self) { + if let Some(core) = other.core { + if self.core.is_some() { + panic!("starknet attribute `core` defined more than once"); + } else { + self.core = Some(core); + } + } + } +} + +impl Parse for Args { + fn parse(input: ParseStream<'_>) -> Result { + let mut core: Option = None; + + while !input.is_empty() { + let lookahead = input.lookahead1(); + if lookahead.peek(kw::core) { + let _ = input.parse::()?; + let _ = input.parse::()?; + let value = input.parse::()?; + + match core { + Some(_) => { + return Err(ParseError::new( + Span::call_site(), + "starknet attribute `core` defined more than once", + )) + } + None => { + core = Some(value); + } + } + } else { + return Err(lookahead.error()); + } + } + + Ok(Self { core }) + } +} + +mod kw { + syn::custom_keyword!(core); +} + +/// Derives the `Encode` trait. +#[proc_macro_derive(Encode, attributes(starknet))] +pub fn derive_encode(input: TokenStream) -> TokenStream { + let input: DeriveInput = parse_macro_input!(input); + let ident = &input.ident; + + let core = derive_core_path(&input); + + let impl_block = match input.data { + syn::Data::Struct(data) => { + let field_impls = data.fields.iter().enumerate().map(|(ind_field, field)| { + let field_ident = match &field.ident { + Some(field_ident) => quote! { self.#field_ident }, + None => { + let ind_field = syn::Index::from(ind_field); + quote! { self.#ind_field } + } + }; + let field_type = &field.ty; + + quote! { + <#field_type as #core::codec::Encode>::encode(&#field_ident, writer)?; + } + }); + + quote! { + #(#field_impls)* + } + } + syn::Data::Enum(data) => { + let variant_impls = + data.variants + .iter() + .enumerate() + .map(|(ind_variant, variant)| { + let variant_ident = &variant.ident; + let ind_variant = int_to_felt(ind_variant, &core); + + match &variant.fields { + Fields::Named(fields_named) => { + let names = fields_named + .named + .iter() + .map(|field| field.ident.as_ref().unwrap()); + + let field_impls = fields_named.named.iter().map(|field| { + let field_ident = field.ident.as_ref().unwrap(); + let field_type = &field.ty; + + quote! { + <#field_type as #core::codec::Encode> + ::encode(#field_ident, writer)?; + } + }); + + quote! { + Self::#variant_ident { #(#names),* } => { + writer.write(#ind_variant); + #(#field_impls)* + }, + } + } + Fields::Unnamed(fields_unnamed) => { + let names = fields_unnamed.unnamed.iter().enumerate().map( + |(ind_field, _)| { + syn::Ident::new( + &format!("field_{}", ind_field), + Span::call_site(), + ) + }, + ); + + let field_impls = fields_unnamed.unnamed.iter().enumerate().map( + |(ind_field, field)| { + let field_ident = syn::Ident::new( + &format!("field_{}", ind_field), + Span::call_site(), + ); + let field_type = &field.ty; + + quote! { + <#field_type as #core::codec::Encode> + ::encode(#field_ident, writer)?; + } + }, + ); + + quote! { + Self::#variant_ident( #(#names),* ) => { + writer.write(#ind_variant); + #(#field_impls)* + }, + } + } + Fields::Unit => { + quote! { + Self::#variant_ident => { + writer.write(#ind_variant); + }, + } + } + } + }); + + quote! { + match self { + #(#variant_impls)* + } + } + } + syn::Data::Union(_) => panic!("union type not supported"), + }; + + quote! { + #[automatically_derived] + impl #core::codec::Encode for #ident { + fn encode(&self, writer: &mut W) + -> ::core::result::Result<(), #core::codec::Error> { + #impl_block + + Ok(()) + } + } + } + .into() +} + +/// Derives the `Decode` trait. +#[proc_macro_derive(Decode, attributes(starknet))] +pub fn derive_decode(input: TokenStream) -> TokenStream { + let input: DeriveInput = parse_macro_input!(input); + let ident = &input.ident; + + let core = derive_core_path(&input); + + let impl_block = match input.data { + syn::Data::Struct(data) => match &data.fields { + Fields::Named(fields_named) => { + let field_impls = fields_named.named.iter().map(|field| { + let field_ident = &field.ident; + let field_type = &field.ty; + + quote! { + #field_ident: <#field_type as #core::codec::Decode> + ::decode_iter(iter)?, + } + }); + + quote! { + Ok(Self { + #(#field_impls)* + }) + } + } + Fields::Unnamed(fields_unnamed) => { + let field_impls = fields_unnamed.unnamed.iter().map(|field| { + let field_type = &field.ty; + quote! { + <#field_type as #core::codec::Decode>::decode_iter(iter)? + } + }); + + quote! { + Ok(Self ( + #(#field_impls),* + )) + } + } + Fields::Unit => { + quote! { + Ok(Self) + } + } + }, + syn::Data::Enum(data) => { + let variant_impls = data + .variants + .iter() + .enumerate() + .map(|(ind_variant, variant)| { + let variant_ident = &variant.ident; + let ind_variant = int_to_felt(ind_variant, &core); + + let decode_impl = match &variant.fields { + Fields::Named(fields_named) => { + let field_impls = fields_named.named.iter().map(|field| { + let field_ident = field.ident.as_ref().unwrap(); + let field_type = &field.ty; + + quote! { + #field_ident: <#field_type as #core::codec::Decode> + ::decode_iter(iter)?, + } + }); + + quote! { + return Ok(Self::#variant_ident { + #(#field_impls)* + }); + } + } + Fields::Unnamed(fields_unnamed) => { + let field_impls = fields_unnamed.unnamed.iter().map(|field| { + let field_type = &field.ty; + + quote! { + <#field_type as #core::codec::Decode>::decode_iter(iter)? + } + }); + + quote! { + return Ok(Self::#variant_ident( #(#field_impls),* )); + } + } + Fields::Unit => { + quote! { + return Ok(Self::#variant_ident); + } + } + }; + + quote! { + if tag == &#ind_variant { + #decode_impl + } + } + }); + + let ident = ident.to_string(); + + quote! { + let tag = iter.next().ok_or_else(#core::codec::Error::input_exhausted)?; + + #(#variant_impls)* + + Err(#core::codec::Error::unknown_enum_tag(tag, #ident)) + } + } + syn::Data::Union(_) => panic!("union type not supported"), + }; + + quote! { + #[automatically_derived] + impl<'a> #core::codec::Decode<'a> for #ident { + fn decode_iter(iter: &mut T) -> ::core::result::Result + where + T: core::iter::Iterator + { + #impl_block + } + } + } + .into() +} + +/// Determines the path to the `starknet-core` crate root. +fn derive_core_path(input: &DeriveInput) -> proc_macro2::TokenStream { + let mut attr_args = Args::default(); + + for attr in &input.attrs { + if !attr.meta.path().is_ident("starknet") { + continue; + } + + match &attr.meta { + Meta::Path(_) => {} + Meta::List(meta_list) => { + let args: Args = meta_list + .parse_args() + .expect("unable to parse starknet attribute args"); + + attr_args.merge(args); + } + Meta::NameValue(_) => panic!("starknet attribute must not be name-value"), + } + } + + attr_args.core.map_or_else( + || { + #[cfg(not(feature = "import_from_starknet"))] + quote! { + ::starknet_core + } + + // This feature is enabled by the `starknet` crate. When using `starknet` it's assumed + // that users would not have imported `starknet-core` directly. + #[cfg(feature = "import_from_starknet")] + quote! { + ::starknet::core + } + }, + |id| id.parse().expect("unable to parse core crate path"), + ) +} + +/// Turns an integer into an optimal `TokenStream` that constructs a `Felt` with the same value. +fn int_to_felt(int: usize, core: &proc_macro2::TokenStream) -> proc_macro2::TokenStream { + match int { + 0 => quote! { #core::types::Felt::ZERO }, + 1 => quote! { #core::types::Felt::ONE }, + 2 => quote! { #core::types::Felt::TWO }, + 3 => quote! { #core::types::Felt::THREE }, + // TODO: turn the number into Montgomery repr and use const ctor instead. + _ => { + let literal = LitInt::new(&int.to_string(), Span::call_site()); + quote! { #core::types::Felt::from(#literal) } + } + } +} diff --git a/starknet-core/Cargo.toml b/starknet-core/Cargo.toml index f3302ac2..5b0b1eac 100644 --- a/starknet-core/Cargo.toml +++ b/starknet-core/Cargo.toml @@ -18,10 +18,12 @@ all-features = true [dependencies] starknet-crypto = { version = "0.7.2", path = "../starknet-crypto", default-features = false, features = ["alloc"] } +starknet-core-derive = { version = "0.1.0", path = "../starknet-core-derive" } base64 = { version = "0.21.0", default-features = false, features = ["alloc"] } crypto-bigint = { version = "0.5.1", default-features = false } flate2 = { version = "1.0.25", optional = true } hex = { version = "0.4.3", default-features = false, features = ["alloc"] } +num-traits = { version = "0.2.19", default-features = false } serde = { version = "1.0.160", default-features = false, features = ["derive"] } serde_json = { version = "1.0.96", default-features = false, features = ["alloc", "raw_value"] } serde_json_pythonic = { version = "0.1.2", default-features = false, features = ["alloc", "raw_value"] } diff --git a/starknet-core/src/codec.rs b/starknet-core/src/codec.rs new file mode 100644 index 00000000..0cc60f6e --- /dev/null +++ b/starknet-core/src/codec.rs @@ -0,0 +1,747 @@ +use alloc::{boxed::Box, fmt::Formatter, format, string::*, vec::*}; +use core::fmt::Display; + +use num_traits::ToPrimitive; + +use crate::types::{Felt, U256}; + +pub use starknet_core_derive::{Decode, Encode}; + +/// Any type where [`Felt`]s can be written into. This would typically be [`Vec`], but can +/// also be something like a stateful hasher. +/// +/// The trait method is infallible, as the most common use case is to simply write into a `Vec`. +/// Making the method infallible avoids over-engineering. However, if deemed necessary, a future +/// breaking change can make this fallible instead. +pub trait FeltWriter { + /// Adds a single [Felt] element into the writer. + fn write(&mut self, felt: Felt); +} + +/// Any type that can be serialized into a series of [Felt]s. This trait corresponds to the +/// `serialize` function of the Cairo `Serde` trait. +pub trait Encode { + /// Converts the type into a list of [`Felt`] and append them into the writer. + fn encode(&self, writer: &mut W) -> Result<(), Error>; +} + +/// Any type that can be deserialized from a series of [Felt]s. This trait corresponds to the +/// `deserialize` function of the Cairo `Serde` trait. +pub trait Decode<'a>: Sized { + /// Converts into the type from a list of [`Felt`]. + fn decode(reader: T) -> Result + where + T: IntoIterator, + { + Self::decode_iter(&mut reader.into_iter()) + } + + /// Converts into the type from an iterator of references to [`Felt`]. + fn decode_iter(iter: &mut T) -> Result + where + T: Iterator; +} + +/// Error type for any encoding/decoding operations. +/// +/// A simple string representation is forced onto all implementations for simplicity. This is +/// because most of the time, a encoding/decoding error indicates a bug that requires human +/// attention to fix anyway; even when handling untrusted data, the program is likely to only be +/// interested in knowing that an error _did_ occur, instead of handling based on cause. +/// +/// There might be cases where allocations must be avoided. A feature could be added in the future +/// that turns the `repr` into `()` to address this. Such a feature would be a non-breaking change +/// so there's no need to add it now. +#[derive(Debug)] +pub struct Error { + repr: Box, +} + +impl FeltWriter for Vec { + fn write(&mut self, felt: Felt) { + self.push(felt); + } +} + +impl Encode for Felt { + fn encode(&self, writer: &mut W) -> Result<(), Error> { + writer.write(*self); + Ok(()) + } +} + +impl Encode for bool { + fn encode(&self, writer: &mut W) -> Result<(), Error> { + writer.write(if *self { Felt::ONE } else { Felt::ZERO }); + Ok(()) + } +} + +impl Encode for u8 { + fn encode(&self, writer: &mut W) -> Result<(), Error> { + writer.write((*self).into()); + Ok(()) + } +} + +impl Encode for u16 { + fn encode(&self, writer: &mut W) -> Result<(), Error> { + writer.write((*self).into()); + Ok(()) + } +} + +impl Encode for u32 { + fn encode(&self, writer: &mut W) -> Result<(), Error> { + writer.write((*self).into()); + Ok(()) + } +} + +impl Encode for u64 { + fn encode(&self, writer: &mut W) -> Result<(), Error> { + writer.write((*self).into()); + Ok(()) + } +} + +impl Encode for u128 { + fn encode(&self, writer: &mut W) -> Result<(), Error> { + writer.write((*self).into()); + Ok(()) + } +} + +impl Encode for U256 { + fn encode(&self, writer: &mut W) -> Result<(), Error> { + self.low().encode(writer)?; + self.high().encode(writer)?; + Ok(()) + } +} + +impl Encode for Option +where + T: Encode, +{ + fn encode(&self, writer: &mut W) -> Result<(), Error> { + match self { + Some(inner) => { + writer.write(Felt::ZERO); + inner.encode(writer)?; + } + None => { + writer.write(Felt::ONE); + } + } + + Ok(()) + } +} + +impl<'a> Decode<'a> for Felt { + fn decode_iter(iter: &mut T) -> Result + where + T: Iterator, + { + iter.next().ok_or_else(Error::input_exhausted).cloned() + } +} + +impl<'a> Decode<'a> for bool { + fn decode_iter(iter: &mut T) -> Result + where + T: Iterator, + { + let input = iter.next().ok_or_else(Error::input_exhausted)?; + if input == &Felt::ZERO { + Ok(false) + } else if input == &Felt::ONE { + Ok(true) + } else { + Err(Error::value_out_of_range(input, "bool")) + } + } +} + +impl<'a> Decode<'a> for u8 { + fn decode_iter(iter: &mut T) -> Result + where + T: Iterator, + { + let input = iter.next().ok_or_else(Error::input_exhausted)?; + input + .to_u8() + .ok_or_else(|| Error::value_out_of_range(input, "u8")) + } +} + +impl<'a> Decode<'a> for u16 { + fn decode_iter(iter: &mut T) -> Result + where + T: Iterator, + { + let input = iter.next().ok_or_else(Error::input_exhausted)?; + input + .to_u16() + .ok_or_else(|| Error::value_out_of_range(input, "u16")) + } +} + +impl<'a> Decode<'a> for u32 { + fn decode_iter(iter: &mut T) -> Result + where + T: Iterator, + { + let input = iter.next().ok_or_else(Error::input_exhausted)?; + input + .to_u32() + .ok_or_else(|| Error::value_out_of_range(input, "u32")) + } +} + +impl<'a> Decode<'a> for u64 { + fn decode_iter(iter: &mut T) -> Result + where + T: Iterator, + { + let input = iter.into_iter().next().ok_or_else(Error::input_exhausted)?; + input + .to_u64() + .ok_or_else(|| Error::value_out_of_range(input, "u64")) + } +} + +impl<'a> Decode<'a> for u128 { + fn decode_iter(iter: &mut T) -> Result + where + T: Iterator, + { + let input = iter.next().ok_or_else(Error::input_exhausted)?; + input + .to_u128() + .ok_or_else(|| Error::value_out_of_range(input, "u128")) + } +} + +impl<'a> Decode<'a> for U256 { + fn decode_iter(iter: &mut T) -> Result + where + T: Iterator, + { + let input_low = iter.next().ok_or_else(Error::input_exhausted)?; + let input_high = iter.next().ok_or_else(Error::input_exhausted)?; + + let input_low = input_low + .to_u128() + .ok_or_else(|| Error::value_out_of_range(input_low, "u128"))?; + let input_high = input_high + .to_u128() + .ok_or_else(|| Error::value_out_of_range(input_high, "u128"))?; + + Ok(Self::from_words(input_low, input_high)) + } +} + +impl<'a, T> Decode<'a> for Option +where + T: Decode<'a>, +{ + fn decode_iter(iter: &mut I) -> Result + where + I: Iterator, + { + let tag = iter.next().ok_or_else(Error::input_exhausted)?; + + if tag == &Felt::ZERO { + Ok(Some(T::decode_iter(iter)?)) + } else if tag == &Felt::ONE { + Ok(None) + } else { + Err(Error::unknown_enum_tag(tag, "Option")) + } + } +} + +impl Error { + /// Creates an [`Error`] which indicates that the input stream has ended prematurely. + pub fn input_exhausted() -> Self { + Self { + repr: "unexpected end of input stream" + .to_string() + .into_boxed_str(), + } + } + + /// Creates an [`Error`] which indicates that the input value is out of range. + pub fn value_out_of_range(value: V, type_name: &str) -> Self + where + V: Display, + { + Self { + repr: format!("value `{}` is out of range for type `{}`", value, type_name) + .into_boxed_str(), + } + } + + /// Creates an [`Error`] which indicates that the enum tag does not belong to a known variant. + pub fn unknown_enum_tag(tag: V, type_name: &str) -> Self + where + V: Display, + { + Self { + repr: format!("enum tag `{}` is unknown for type `{}`", tag, type_name) + .into_boxed_str(), + } + } + + /// Creates an [`Error`] using a custom error string. + pub fn custom(content: T) -> Self + where + T: Display, + { + Self { + repr: content.to_string().into_boxed_str(), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for Error {} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + write!(f, "{}", self.repr) + } +} + +#[cfg(test)] +mod tests { + use core::str::FromStr; + + use super::*; + + #[test] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] + fn test_encode_felt() { + let mut serialized = Vec::::new(); + Felt::from_str("99999999999999999999999999") + .unwrap() + .encode(&mut serialized) + .unwrap(); + assert_eq!( + serialized, + vec![Felt::from_str("99999999999999999999999999").unwrap()] + ); + } + + #[test] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] + fn test_encode_bool() { + let mut serialized = Vec::::new(); + true.encode(&mut serialized).unwrap(); + assert_eq!(serialized, vec![Felt::from_str("1").unwrap()]); + + let mut serialized = Vec::::new(); + false.encode(&mut serialized).unwrap(); + assert_eq!(serialized, vec![Felt::from_str("0").unwrap()]); + } + + #[test] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] + fn test_encode_u8() { + let mut serialized = Vec::::new(); + 123u8.encode(&mut serialized).unwrap(); + assert_eq!(serialized, vec![Felt::from_str("123").unwrap()]); + } + + #[test] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] + fn test_encode_u16() { + let mut serialized = Vec::::new(); + 12345u16.encode(&mut serialized).unwrap(); + assert_eq!(serialized, vec![Felt::from_str("12345").unwrap()]); + } + + #[test] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] + fn test_encode_u32() { + let mut serialized = Vec::::new(); + 1234567890u32.encode(&mut serialized).unwrap(); + assert_eq!(serialized, vec![Felt::from_str("1234567890").unwrap()]); + } + + #[test] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] + fn test_encode_u64() { + let mut serialized = Vec::::new(); + 12345678900000000000u64.encode(&mut serialized).unwrap(); + assert_eq!( + serialized, + vec![Felt::from_str("12345678900000000000").unwrap()] + ); + } + + #[test] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] + fn test_encode_u128() { + let mut serialized = Vec::::new(); + 123456789000000000000000000000u128 + .encode(&mut serialized) + .unwrap(); + assert_eq!( + serialized, + vec![Felt::from_str("123456789000000000000000000000").unwrap()] + ); + } + + #[test] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] + fn test_encode_u256() { + let mut serialized = Vec::::new(); + U256::from_words(12345, 67890) + .encode(&mut serialized) + .unwrap(); + assert_eq!( + serialized, + vec![ + Felt::from_str("12345").unwrap(), + Felt::from_str("67890").unwrap() + ] + ); + } + + #[test] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] + fn test_encode_option() { + let mut serialized = Vec::::new(); + Some(10u32).encode(&mut serialized).unwrap(); + assert_eq!( + serialized, + vec![Felt::from_str("0").unwrap(), Felt::from_str("10").unwrap()] + ); + + serialized.clear(); + Option::::None.encode(&mut serialized).unwrap(); + assert_eq!(serialized, vec![Felt::from_str("1").unwrap()]); + } + + #[test] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] + fn test_derive_encode_struct_named() { + #[derive(Encode)] + #[starknet(core = "crate")] + struct CairoType { + a: Felt, + b: U256, + c: bool, + } + + let mut serialized = Vec::::new(); + CairoType { + a: Felt::from_str("12345").unwrap(), + b: U256::from_words(12, 34), + c: true, + } + .encode(&mut serialized) + .unwrap(); + assert_eq!( + serialized, + vec![ + Felt::from_str("12345").unwrap(), + Felt::from_str("12").unwrap(), + Felt::from_str("34").unwrap(), + Felt::from_str("1").unwrap(), + ] + ); + } + + #[test] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] + fn test_derive_encode_struct_tuple() { + #[derive(Encode)] + #[starknet(core = "crate")] + struct CairoType(Felt, U256, bool); + + let mut serialized = Vec::::new(); + CairoType( + Felt::from_str("12345").unwrap(), + U256::from_words(12, 34), + true, + ) + .encode(&mut serialized) + .unwrap(); + assert_eq!( + serialized, + vec![ + Felt::from_str("12345").unwrap(), + Felt::from_str("12").unwrap(), + Felt::from_str("34").unwrap(), + Felt::from_str("1").unwrap(), + ] + ); + } + + #[test] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] + fn test_derive_encode_enum() { + #[derive(Encode)] + #[starknet(core = "crate")] + enum CairoType { + A, + B(bool), + C(Option, u8), + D { a: u64, b: bool }, + } + + let mut serialized = Vec::::new(); + CairoType::A.encode(&mut serialized).unwrap(); + assert_eq!(serialized, vec![Felt::from_str("0").unwrap()]); + + serialized.clear(); + CairoType::B(true).encode(&mut serialized).unwrap(); + assert_eq!( + serialized, + vec![Felt::from_str("1").unwrap(), Felt::from_str("1").unwrap()] + ); + + serialized.clear(); + CairoType::C(Some(U256::from_words(12, 23)), 4) + .encode(&mut serialized) + .unwrap(); + assert_eq!( + serialized, + vec![ + Felt::from_str("2").unwrap(), + Felt::from_str("0").unwrap(), + Felt::from_str("12").unwrap(), + Felt::from_str("23").unwrap(), + Felt::from_str("4").unwrap(), + ] + ); + + serialized.clear(); + CairoType::C(None, 8).encode(&mut serialized).unwrap(); + assert_eq!( + serialized, + vec![ + Felt::from_str("2").unwrap(), + Felt::from_str("1").unwrap(), + Felt::from_str("8").unwrap(), + ] + ); + + serialized.clear(); + CairoType::D { a: 100, b: false } + .encode(&mut serialized) + .unwrap(); + assert_eq!( + serialized, + vec![ + Felt::from_str("3").unwrap(), + Felt::from_str("100").unwrap(), + Felt::from_str("0").unwrap() + ] + ); + } + + #[test] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] + fn test_decode_felt() { + assert_eq!( + Felt::from_str("99999999999999999999999999").unwrap(), + Felt::decode(&[Felt::from_str("99999999999999999999999999").unwrap()]).unwrap() + ); + } + + #[allow(clippy::bool_assert_comparison)] + #[test] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] + fn test_decode_bool() { + assert_eq!(true, bool::decode(&[Felt::from_str("1").unwrap()]).unwrap()); + + assert_eq!( + false, + bool::decode(&[Felt::from_str("0").unwrap()]).unwrap() + ); + } + + #[test] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] + fn test_decode_u8() { + assert_eq!( + 123u8, + u8::decode(&[Felt::from_str("123").unwrap()]).unwrap() + ); + } + + #[test] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] + fn test_decode_u16() { + assert_eq!( + 12345u16, + u16::decode(&[Felt::from_str("12345").unwrap()]).unwrap() + ); + } + + #[test] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] + fn test_decode_u32() { + assert_eq!( + 1234567890u32, + u32::decode(&[Felt::from_str("1234567890").unwrap()]).unwrap() + ); + } + + #[test] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] + fn test_decode_u64() { + assert_eq!( + 12345678900000000000u64, + u64::decode(&[Felt::from_str("12345678900000000000").unwrap()]).unwrap() + ); + } + + #[test] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] + fn test_decode_u128() { + assert_eq!( + 123456789000000000000000000000u128, + u128::decode(&[Felt::from_str("123456789000000000000000000000").unwrap()]).unwrap() + ); + } + + #[test] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] + fn test_decode_u256() { + assert_eq!( + U256::from_words(12345, 67890), + U256::decode(&[ + Felt::from_str("12345").unwrap(), + Felt::from_str("67890").unwrap() + ]) + .unwrap() + ); + } + + #[test] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] + fn test_decode_option() { + assert_eq!( + Some(10u32), + Option::::decode(&[Felt::from_str("0").unwrap(), Felt::from_str("10").unwrap()]) + .unwrap() + ); + + assert_eq!( + Option::::None, + Option::::decode(&[Felt::from_str("1").unwrap()]).unwrap() + ); + } + + #[test] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] + fn test_derive_decode_struct_named() { + #[derive(Debug, PartialEq, Eq, Decode)] + #[starknet(core = "crate")] + struct CairoType { + a: Felt, + b: U256, + c: bool, + } + + assert_eq!( + CairoType { + a: Felt::from_str("12345").unwrap(), + b: U256::from_words(12, 34), + c: true, + }, + CairoType::decode(&[ + Felt::from_str("12345").unwrap(), + Felt::from_str("12").unwrap(), + Felt::from_str("34").unwrap(), + Felt::from_str("1").unwrap(), + ]) + .unwrap() + ); + } + + #[test] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] + fn test_derive_decode_struct_tuple() { + #[derive(Debug, PartialEq, Eq, Decode)] + #[starknet(core = "crate")] + struct CairoType(Felt, U256, bool); + + assert_eq!( + CairoType( + Felt::from_str("12345").unwrap(), + U256::from_words(12, 34), + true, + ), + CairoType::decode(&[ + Felt::from_str("12345").unwrap(), + Felt::from_str("12").unwrap(), + Felt::from_str("34").unwrap(), + Felt::from_str("1").unwrap(), + ]) + .unwrap() + ); + } + + #[test] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] + fn test_derive_decode_enum() { + #[derive(Debug, PartialEq, Eq, Decode)] + #[starknet(core = "crate")] + enum CairoType { + A, + B(bool), + C(Option, u8), + D { a: u64, b: bool }, + } + + assert_eq!( + CairoType::A, + CairoType::decode(&[Felt::from_str("0").unwrap()]).unwrap() + ); + + assert_eq!( + CairoType::B(true), + CairoType::decode(&[Felt::from_str("1").unwrap(), Felt::from_str("1").unwrap()]) + .unwrap() + ); + + assert_eq!( + CairoType::C(Some(U256::from_words(12, 23)), 4), + CairoType::decode(&[ + Felt::from_str("2").unwrap(), + Felt::from_str("0").unwrap(), + Felt::from_str("12").unwrap(), + Felt::from_str("23").unwrap(), + Felt::from_str("4").unwrap(), + ]) + .unwrap() + ); + + assert_eq!( + CairoType::C(None, 8), + CairoType::decode(&[ + Felt::from_str("2").unwrap(), + Felt::from_str("1").unwrap(), + Felt::from_str("8").unwrap(), + ]) + .unwrap() + ); + + assert_eq!( + CairoType::D { a: 100, b: false }, + CairoType::decode(&[ + Felt::from_str("3").unwrap(), + Felt::from_str("100").unwrap(), + Felt::from_str("0").unwrap() + ]) + .unwrap() + ); + } +} diff --git a/starknet-core/src/lib.rs b/starknet-core/src/lib.rs index 676c1bed..88eb2e90 100644 --- a/starknet-core/src/lib.rs +++ b/starknet-core/src/lib.rs @@ -19,4 +19,7 @@ pub mod utils; /// Chain IDs for commonly used public Starknet networks. pub mod chain_id; +/// Types for serializing high-level Cairo types into field elements and vice versa. +pub mod codec; + extern crate alloc;