diff --git a/crates/rs-macro/src/lib.rs b/crates/rs-macro/src/lib.rs index 1bc7561..d3bcbf4 100644 --- a/crates/rs-macro/src/lib.rs +++ b/crates/rs-macro/src/lib.rs @@ -29,7 +29,11 @@ fn abigen_internal(input: TokenStream) -> TokenStream { let abi_tokens = AbiParser::collect_tokens(&abi_entries, &contract_abi.type_aliases) .expect("failed tokens parsing"); - let expanded = cainome_rs::abi_to_tokenstream(&contract_name.to_string(), &abi_tokens); + let expanded = cainome_rs::abi_to_tokenstream( + &contract_name.to_string(), + &abi_tokens, + contract_abi.execution_version, + ); if let Some(out_path) = contract_abi.output_path { let content: String = expanded.to_string(); @@ -53,7 +57,11 @@ fn abigen_internal_legacy(input: TokenStream) -> TokenStream { let abi_tokens = AbiParserLegacy::collect_tokens(&abi_entries, &contract_abi.type_aliases) .expect("failed tokens parsing"); - let expanded = cainome_rs::abi_to_tokenstream(&contract_name.to_string(), &abi_tokens); + let expanded = cainome_rs::abi_to_tokenstream( + &contract_name.to_string(), + &abi_tokens, + cainome_rs::ExecutionVersion::V1, + ); if let Some(out_path) = contract_abi.output_path { let content: String = expanded.to_string(); diff --git a/crates/rs-macro/src/macro_inputs.rs b/crates/rs-macro/src/macro_inputs.rs index db28c55..df6a08a 100644 --- a/crates/rs-macro/src/macro_inputs.rs +++ b/crates/rs-macro/src/macro_inputs.rs @@ -16,6 +16,7 @@ use starknet::core::types::contract::{AbiEntry, SierraClass}; use std::collections::{HashMap, HashSet}; use std::fs::File; use std::path::Path; +use std::str::FromStr; use syn::{ braced, ext::IdentExt, @@ -25,6 +26,7 @@ use syn::{ }; use crate::spanned::Spanned; +use cainome_rs::ExecutionVersion; const CARGO_MANIFEST_DIR: &str = "$CARGO_MANIFEST_DIR/"; @@ -34,6 +36,7 @@ pub(crate) struct ContractAbi { pub abi: Vec, pub output_path: Option, pub type_aliases: HashMap, + pub execution_version: ExecutionVersion, } impl Parse for ContractAbi { @@ -84,6 +87,7 @@ impl Parse for ContractAbi { }; let mut output_path: Option = None; + let mut execution_version = ExecutionVersion::V1; let mut type_aliases = HashMap::new(); loop { @@ -123,6 +127,14 @@ impl Parse for ContractAbi { parenthesized!(content in input); output_path = Some(content.parse::()?.value()); } + "execution_version" => { + let content; + parenthesized!(content in input); + let ev = content.parse::()?.value(); + execution_version = ExecutionVersion::from_str(&ev).map_err(|e| { + syn::Error::new(content.span(), format!("Invalid execution version: {}", e)) + })?; + } _ => panic!("unexpected named parameter `{}`", name), } } @@ -132,6 +144,7 @@ impl Parse for ContractAbi { abi, output_path, type_aliases, + execution_version, }) } } diff --git a/crates/rs/src/execution_version.rs b/crates/rs/src/execution_version.rs new file mode 100644 index 0000000..d849134 --- /dev/null +++ b/crates/rs/src/execution_version.rs @@ -0,0 +1,42 @@ +/// Execution version of Starknet transactions. + +/// The version of transaction to be executed. +#[derive(Debug, Clone, Copy, Default)] +pub enum ExecutionVersion { + /// Execute the transaction using the `execute_v1` method, where fees are only payable in WEI. + #[default] + V1, + /// Execute the transaction using the `execute_v3` method, where fees are payable in WEI or FRI. + V3, +} + +#[derive(Debug, PartialEq, Eq)] +pub struct ParseExecutionVersionError { + invalid_value: String, +} + +impl std::fmt::Display for ParseExecutionVersionError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "Invalid execution version '{}'. Supported values are 'v1', 'V1', 'v3', or 'V3'.", + self.invalid_value + ) + } +} + +impl std::error::Error for ParseExecutionVersionError {} + +impl std::str::FromStr for ExecutionVersion { + type Err = ParseExecutionVersionError; + + fn from_str(input: &str) -> Result { + match input { + "v1" | "V1" => Ok(ExecutionVersion::V1), + "v3" | "V3" => Ok(ExecutionVersion::V3), + _ => Err(ParseExecutionVersionError { + invalid_value: input.to_string(), + }), + } + } +} diff --git a/crates/rs/src/expand/function.rs b/crates/rs/src/expand/function.rs index a360d8a..196a86f 100644 --- a/crates/rs/src/expand/function.rs +++ b/crates/rs/src/expand/function.rs @@ -19,6 +19,23 @@ use quote::quote; use crate::expand::types::CairoToRust; use crate::expand::utils; +use crate::ExecutionVersion; + +impl ExecutionVersion { + pub fn get_type_str(&self) -> String { + match self { + ExecutionVersion::V1 => "starknet::accounts::ExecutionV1".to_string(), + ExecutionVersion::V3 => "starknet::accounts::ExecutionV3".to_string(), + } + } + + pub fn get_call_str(&self) -> TokenStream2 { + match self { + ExecutionVersion::V1 => quote!(self.account.execute_v1(vec![__call])), + ExecutionVersion::V3 => quote!(self.account.execute_v3(vec![__call])), + } + } +} fn get_func_inputs(inputs: &[(String, Token)]) -> Vec { let mut out: Vec = vec![]; @@ -35,7 +52,11 @@ fn get_func_inputs(inputs: &[(String, Token)]) -> Vec { pub struct CairoFunction; impl CairoFunction { - pub fn expand(func: &Function, is_for_reader: bool) -> TokenStream2 { + pub fn expand( + func: &Function, + is_for_reader: bool, + execution_version: ExecutionVersion, + ) -> TokenStream2 { let func_name = &func.name; let func_name_ident = utils::str_to_ident(func_name); @@ -110,6 +131,9 @@ impl CairoFunction { // // TODO: if it's possible to do it with lifetime, // this can be tried in an issue. + let exec_type = utils::str_to_type(&execution_version.get_type_str()); + let exec_call = execution_version.get_call_str(); + quote! { #[allow(clippy::ptr_arg)] #[allow(clippy::too_many_arguments)] @@ -133,7 +157,7 @@ impl CairoFunction { pub fn #func_name_ident( &self, #(#inputs),* - ) -> starknet::accounts::ExecutionV1 { + ) -> #exec_type { use #ccs::CairoSerde; let mut __calldata = vec![]; @@ -145,7 +169,7 @@ impl CairoFunction { calldata: __calldata, }; - self.account.execute_v1(vec![__call]) + #exec_call } } } diff --git a/crates/rs/src/lib.rs b/crates/rs/src/lib.rs index dd5fcf4..73e478e 100644 --- a/crates/rs/src/lib.rs +++ b/crates/rs/src/lib.rs @@ -9,7 +9,9 @@ use std::fmt; use std::fs; use std::io; +mod execution_version; mod expand; +pub use execution_version::{ExecutionVersion, ParseExecutionVersionError}; use crate::expand::utils; use crate::expand::{CairoContract, CairoEnum, CairoEnumEvent, CairoFunction, CairoStruct}; @@ -63,6 +65,8 @@ pub struct Abigen { /// Types aliases to avoid name conflicts, as for now the types are limited to the /// latest segment of the fully qualified path. pub types_aliases: HashMap, + /// The version of transaction to be executed. + pub execution_version: ExecutionVersion, } impl Abigen { @@ -78,6 +82,7 @@ impl Abigen { contract_name: contract_name.to_string(), abi_source: Utf8PathBuf::from(abi_source), types_aliases: HashMap::new(), + execution_version: ExecutionVersion::V1, } } @@ -91,13 +96,24 @@ impl Abigen { self } + /// Sets the execution version to be used. + /// + /// # Arguments + /// + /// * `execution_version` - The version of transaction to be executed. + pub fn with_execution_version(mut self, execution_version: ExecutionVersion) -> Self { + self.execution_version = execution_version; + self + } + /// Generates the contract bindings. pub fn generate(&self) -> Result { let file_content = std::fs::read_to_string(&self.abi_source)?; match AbiParser::tokens_from_abi_string(&file_content, &self.types_aliases) { Ok(tokens) => { - let expanded = abi_to_tokenstream(&self.contract_name, &tokens); + let expanded = + abi_to_tokenstream(&self.contract_name, &tokens, self.execution_version); Ok(ContractBindings { name: self.contract_name.clone(), @@ -120,7 +136,11 @@ impl Abigen { /// /// * `contract_name` - Name of the contract. /// * `abi_tokens` - Tokenized ABI. -pub fn abi_to_tokenstream(contract_name: &str, abi_tokens: &TokenizedAbi) -> TokenStream2 { +pub fn abi_to_tokenstream( + contract_name: &str, + abi_tokens: &TokenizedAbi, + execution_version: ExecutionVersion, +) -> TokenStream2 { let contract_name = utils::str_to_ident(contract_name); let mut tokens: Vec = vec![]; @@ -160,10 +180,12 @@ pub fn abi_to_tokenstream(contract_name: &str, abi_tokens: &TokenizedAbi) -> Tok let f = f.to_function().expect("function expected"); match f.state_mutability { StateMutability::View => { - reader_views.push(CairoFunction::expand(f, true)); - views.push(CairoFunction::expand(f, false)); + reader_views.push(CairoFunction::expand(f, true, execution_version)); + views.push(CairoFunction::expand(f, false, execution_version)); + } + StateMutability::External => { + externals.push(CairoFunction::expand(f, false, execution_version)) } - StateMutability::External => externals.push(CairoFunction::expand(f, false)), } } diff --git a/examples/exec_v3.rs b/examples/exec_v3.rs new file mode 100644 index 0000000..8e3deea --- /dev/null +++ b/examples/exec_v3.rs @@ -0,0 +1,85 @@ +use cainome::rs::abigen; +use starknet::{ + accounts::{ExecutionEncoding, SingleOwnerAccount}, + core::types::Felt, + providers::{jsonrpc::HttpTransport, AnyProvider, JsonRpcClient}, + signers::{LocalWallet, SigningKey}, +}; +use std::sync::Arc; +use url::Url; + +// To run this example, please first run `make setup_simple_get_set` in the contracts directory with a Katana running. This will declare and deploy the testing contract. + +const CONTRACT_ADDRESS: &str = "0x007997dd654f2c079597a6c461489ee89981d0df733b8bcd3525153b0e700f98"; +const KATANA_ACCOUNT_0: &str = "0x6162896d1d7ab204c7ccac6dd5f8e9e7c25ecd5ae4fcb4ad32e57786bb46e03"; +const KATANA_PRIVKEY_0: &str = "0x1800000000300000180000000000030000000000003006001800006600"; +const KATANA_CHAIN_ID: &str = "0x4b4154414e41"; + +// You can load of the sierra class entirely from the artifact. +// Or you can use the extracted abi entries with jq in contracts/abi/. +abigen!( + MyContract, + "./contracts/target/dev/contracts_simple_get_set.contract_class.json", + execution_version("V3"), +); +//abigen!(MyContract, "./contracts/abi/simple_get_set.abi.json"); + +#[tokio::main] +async fn main() { + let rpc_url = Url::parse("http://0.0.0.0:5050").expect("Expecting Starknet RPC URL"); + let provider = + AnyProvider::JsonRpcHttp(JsonRpcClient::new(HttpTransport::new(rpc_url.clone()))); + + let contract_address = Felt::from_hex(CONTRACT_ADDRESS).unwrap(); + + // If you only plan to call views functions, you can use the `Reader`, which + // only requires a provider along with your contract address. + let contract = MyContractReader::new(contract_address, &provider); + + // To call a view, there is no need to initialize an account. You can directly + // use the name of the method in the ABI and then use the `call()` method. + let a = contract + .get_a() + .call() + .await + .expect("Call to `get_a` failed"); + println!("a initial value: {:?}", a); + + // If you want to do some invoke for external functions, you must use an account. + let signer = LocalWallet::from(SigningKey::from_secret_scalar( + Felt::from_hex(KATANA_PRIVKEY_0).unwrap(), + )); + let address = Felt::from_hex(KATANA_ACCOUNT_0).unwrap(); + + let account = Arc::new(SingleOwnerAccount::new( + provider, + signer, + address, + Felt::from_hex(KATANA_CHAIN_ID).unwrap(), + ExecutionEncoding::New, + )); + + // A `Contract` exposes all the methods of the ABI, which includes the views (as the `ContractReader`) and + // the externals (sending transaction). + let contract = MyContract::new(contract_address, account); + + // The transaction is actually sent when `send()` is called. + // You can before that configure the fees, or even only run an estimation of the + // fees without actually sending the transaction. + let _tx_res = contract + .set_a(&(a + Felt::ONE)) + .gas_estimate_multiplier(1.2) + .send() + .await + .expect("Call to `set_a` failed"); + + // In production code, you want to poll the transaction status. + tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; + + let a = contract + .get_a() + .call() + .await + .expect("Call to `get_a` failed"); + println!("a after invoke: {:?}", a); +} diff --git a/src/bin/cli/args.rs b/src/bin/cli/args.rs index 1cbf95d..5adc1ce 100644 --- a/src/bin/cli/args.rs +++ b/src/bin/cli/args.rs @@ -1,5 +1,6 @@ //! Cainome CLI arguments. //! +use cainome_rs::ExecutionVersion; use camino::Utf8PathBuf; use clap::{Args, Parser}; use starknet::core::types::Felt; @@ -56,6 +57,11 @@ pub struct CainomeArgs { #[command(flatten)] #[command(next_help_heading = "Plugins options")] pub plugins: PluginOptions, + + #[arg(long)] + #[arg(value_name = "EXECUTION_VERSION")] + #[arg(help = "The execution version to use. Supported values are 'v1', 'V1', 'v3', or 'V3'.")] + pub execution_version: ExecutionVersion, } #[derive(Debug, Args, Clone)] diff --git a/src/bin/cli/main.rs b/src/bin/cli/main.rs index 408c9a4..43ca2da 100644 --- a/src/bin/cli/main.rs +++ b/src/bin/cli/main.rs @@ -51,6 +51,7 @@ async fn main() -> CainomeCliResult<()> { pm.generate(PluginInput { output_dir: args.output_dir, contracts, + execution_version: args.execution_version, }) .await?; diff --git a/src/bin/cli/plugins/builtins/rust.rs b/src/bin/cli/plugins/builtins/rust.rs index de465e4..458b83e 100644 --- a/src/bin/cli/plugins/builtins/rust.rs +++ b/src/bin/cli/plugins/builtins/rust.rs @@ -32,7 +32,11 @@ impl BuiltinPlugin for RustPlugin { .from_case(Case::Snake) .to_case(Case::Pascal); - let expanded = cainome_rs::abi_to_tokenstream(&contract_name, &contract.tokens); + let expanded = cainome_rs::abi_to_tokenstream( + &contract_name, + &contract.tokens, + input.execution_version, + ); let filename = format!( "{}.rs", contract_name.from_case(Case::Pascal).to_case(Case::Snake) diff --git a/src/bin/cli/plugins/mod.rs b/src/bin/cli/plugins/mod.rs index c9e9012..0c7cb6a 100644 --- a/src/bin/cli/plugins/mod.rs +++ b/src/bin/cli/plugins/mod.rs @@ -1,3 +1,4 @@ +use cainome_rs::ExecutionVersion; use camino::Utf8PathBuf; pub mod builtins; @@ -11,6 +12,7 @@ use crate::plugins::builtins::{BuiltinPlugin, RustPlugin}; pub struct PluginInput { pub output_dir: Utf8PathBuf, pub contracts: Vec, + pub execution_version: ExecutionVersion, } #[derive(Debug)]