From 56e044b2b9f5af71b128f9be89547c84796a68f3 Mon Sep 17 00:00:00 2001 From: Jonathan LEI Date: Tue, 20 Aug 2024 19:10:26 +0800 Subject: [PATCH] feat: hashing and declaring jsonrpc-formatted classes --- book/src/declaring-classes.md | 17 +- src/subcommands/class_hash.rs | 17 +- src/subcommands/declare.rs | 419 +++++++++++++++++++--------------- 3 files changed, 247 insertions(+), 206 deletions(-) diff --git a/book/src/declaring-classes.md b/book/src/declaring-classes.md index 79a5b54..b64aa21 100644 --- a/book/src/declaring-classes.md +++ b/book/src/declaring-classes.md @@ -43,24 +43,15 @@ To come up with the _CASM hash_, Starkli compiles the Sierra class provided unde While the normal process of declaring a class involves getting the compiled contract artifact from the compiler and following the steps documented above, it's sometimes helpful to _redeclare_ a class you found from another network. -Currently, Starkli does _not_ support directly declaring the class file fetched from a network. So **this would fail**: +To do so, simply run `starkli declare` on any class fetched from `class-at` or `class-by-hash` commands. For example: ```console starkli class-by-hash --network sepolia SOME_CLASS_HASH_HERE > class.json starkli declare --network mainnet ./class.json ``` -The above would fail as the JSON-RPC class format is different from compiler output. The fix is simple: just add `--parse` to the first command to instruct Starkli to recover the JSON-RPC-formatted class back into the original compiler output format: - -```console -starkli class-by-hash --network sepolia SOME_CLASS_HASH_HERE --parse > class.json -starkli declare --network mainnet ./class.json -``` - -Now the commands should execute successfully. - -> ℹ️ **Note** +> 💡 **Tips** > -> _Technically_, Starkli could support declaring JSON-RPC-formatted class files. It's just that the current Starkli implementation does not come with that capability. This might change in the future. For now you'll have to use the `--parse` flag when fetching the class. +> While Starkli is capable of handling its declaration, the class format retrieved from JSON-RPC is different from the original compiler output. To obtain the format identical to what comes out of the compiler, use the `--parse` flag when retrieving the class (e.g. in a `class-by-hash` command). > -> While `--parse` should work just fine most of the time, unfortunately, certain exotic classes might not be parsable. In these rare cases, Starkli cannot redeclare them until the aformentioned capability is implemented, which would remove the need of parsing. +> Note that while `--parse` should work just fine most of the time, unfortunately, certain exotic classes might not be parsable. Nevertheless, Starkli would still be able to redeclare the format without parsing. diff --git a/src/subcommands/class_hash.rs b/src/subcommands/class_hash.rs index 54afd33..5206193 100644 --- a/src/subcommands/class_hash.rs +++ b/src/subcommands/class_hash.rs @@ -2,9 +2,12 @@ use std::path::PathBuf; use anyhow::Result; use clap::Parser; -use starknet::core::types::contract::{legacy::LegacyContractClass, CompiledClass, SierraClass}; +use starknet::core::types::{ + contract::{legacy::LegacyContractClass, CompiledClass, SierraClass}, + CompressedLegacyContractClass, FlattenedSierraClass, +}; -use crate::path::ExpandedPathbufParser; +use crate::{path::ExpandedPathbufParser, utils::parse_compressed_legacy_class}; #[derive(Debug, Parser)] pub struct ClassHash { @@ -29,9 +32,17 @@ impl ClassHash { { class.class_hash()? } else if let Ok(class) = - serde_json::from_reader::<_, LegacyContractClass>(std::fs::File::open(self.file)?) + serde_json::from_reader::<_, LegacyContractClass>(std::fs::File::open(&self.file)?) { class.class_hash()? + } else if let Ok(class) = + serde_json::from_reader::<_, FlattenedSierraClass>(std::fs::File::open(&self.file)?) + { + class.class_hash() + } else if let Ok(class) = serde_json::from_reader::<_, CompressedLegacyContractClass>( + std::fs::File::open(self.file)?, + ) { + parse_compressed_legacy_class(class)?.class_hash()? } else { anyhow::bail!("failed to parse contract artifact"); }; diff --git a/src/subcommands/declare.rs b/src/subcommands/declare.rs index fed24ed..4b9bb90 100644 --- a/src/subcommands/declare.rs +++ b/src/subcommands/declare.rs @@ -6,8 +6,9 @@ use colored::Colorize; use starknet::{ accounts::Account, core::types::{ - contract::{legacy::LegacyContractClass, CompiledClass, SierraClass}, - BlockId, BlockTag, Felt, StarknetError, + contract::{legacy::LegacyContractClass, CompiledClass, SierraClass, SierraClassDebugInfo}, + BlockId, BlockTag, CompressedLegacyContractClass, Felt, FlattenedSierraClass, + StarknetError, }, providers::{Provider, ProviderError}, }; @@ -18,7 +19,7 @@ use crate::{ error::account_error_mapper, fee::{FeeArgs, FeeSetting, TokenFeeSetting}, path::ExpandedPathbufParser, - utils::{felt_to_bigdecimal, print_colored_json, watch_tx}, + utils::{felt_to_bigdecimal, parse_compressed_legacy_class, print_colored_json, watch_tx}, verbosity::VerbosityArgs, ProviderArgs, }; @@ -57,6 +58,11 @@ pub struct Declare { verbosity: VerbosityArgs, } +enum Declarable { + CairoOne(FlattenedSierraClass), + CairoZero(LegacyContractClass), +} + impl Declare { pub async fn run(self) -> Result<()> { self.verbosity.setup_logging(); @@ -73,226 +79,259 @@ impl Declare { // Working around a deserialization bug in `starknet-rs`: // https://github.com/xJonathanLEI/starknet-rs/issues/392 - #[allow(clippy::redundant_pattern_matching)] - let (class_hash, declaration_tx_hash) = if let Ok(mut class) = + let declarable = if let Ok(class) = serde_json::from_reader::<_, SierraClass>(std::fs::File::open(&self.file)?) { - if self.no_abi { - class.abi = vec![]; - } + Declarable::CairoOne(class.flatten()?) + } else if let Ok(class) = + serde_json::from_reader::<_, FlattenedSierraClass>(std::fs::File::open(&self.file)?) + { + Declarable::CairoOne(class) + } else if let Ok(class) = + serde_json::from_reader::<_, LegacyContractClass>(std::fs::File::open(&self.file)?) + { + Declarable::CairoZero(class) + } else if let Ok(class) = serde_json::from_reader::<_, CompressedLegacyContractClass>( + std::fs::File::open(&self.file)?, + ) { + Declarable::CairoZero(parse_compressed_legacy_class(class)?) + } else if serde_json::from_reader::<_, CompiledClass>(std::fs::File::open(&self.file)?) + .is_ok() + { + // TODO: add more helpful instructions to fix this + anyhow::bail!("unexpected CASM class"); + } else { + anyhow::bail!("failed to parse contract artifact"); + }; - // Declaring Cairo 1 class - let class_hash = class.class_hash()?; + let (class_hash, declaration_tx_hash) = match declarable { + Declarable::CairoOne(mut class) => { + if self.no_abi { + "[]".clone_into(&mut class.abi); + } - // TODO: add option to skip checking - if Self::check_already_declared(&provider, class_hash).await? { - return Ok(()); - } + // Declaring Cairo 1 class + let class_hash = class.class_hash(); - let casm_source = self.casm.into_casm_hash_source(&provider).await?; + // TODO: add option to skip checking + if Self::check_already_declared(&provider, class_hash).await? { + return Ok(()); + } - if !fee_setting.is_estimate_only() { - eprintln!( - "Declaring Cairo 1 class: {}", - format!("{:#064x}", class_hash).bright_yellow() - ); + let casm_source = self.casm.into_casm_hash_source(&provider).await?; - match &casm_source { - CasmHashSource::BuiltInCompiler(compiler) => { - eprintln!( - "Compiling Sierra class to CASM with compiler version {}...", - format!("{}", compiler.version()).bright_yellow() - ); - } - CasmHashSource::CompilerBinary(compiler) => { - eprintln!( - "Compiling Sierra class to CASM with compiler binary {}...", - format!("{}", compiler.path().display()).bright_yellow() - ); - } - CasmHashSource::CasmFile(path) => { - eprintln!( - "Using a compiled CASM file directly: {}...", - format!("{}", path.display()).bright_yellow() - ); - } - CasmHashSource::Hash(hash) => { - eprintln!( - "Using the provided CASM hash: {}...", - format!("{:#064x}", hash).bright_yellow() - ); + if !fee_setting.is_estimate_only() { + eprintln!( + "Declaring Cairo 1 class: {}", + format!("{:#064x}", class_hash).bright_yellow() + ); + + match &casm_source { + CasmHashSource::BuiltInCompiler(compiler) => { + eprintln!( + "Compiling Sierra class to CASM with compiler version {}...", + format!("{}", compiler.version()).bright_yellow() + ); + } + CasmHashSource::CompilerBinary(compiler) => { + eprintln!( + "Compiling Sierra class to CASM with compiler binary {}...", + format!("{}", compiler.path().display()).bright_yellow() + ); + } + CasmHashSource::CasmFile(path) => { + eprintln!( + "Using a compiled CASM file directly: {}...", + format!("{}", path.display()).bright_yellow() + ); + } + CasmHashSource::Hash(hash) => { + eprintln!( + "Using the provided CASM hash: {}...", + format!("{:#064x}", hash).bright_yellow() + ); + } } } - } - let casm_class_hash = casm_source.get_casm_hash(&class)?; + // Reconstructs an original Sierra class just for CASM compilation purposes. It's a + // bit inefficient but acceptable. + let sierra_class = SierraClass { + sierra_program: class.sierra_program.clone(), + sierra_program_debug_info: SierraClassDebugInfo { + type_names: vec![], + libfunc_names: vec![], + user_func_names: vec![], + }, + contract_class_version: class.contract_class_version.clone(), + entry_points_by_type: class.entry_points_by_type.clone(), + abi: vec![], + }; + + let casm_class_hash = casm_source.get_casm_hash(&sierra_class)?; + + if !fee_setting.is_estimate_only() { + eprintln!( + "CASM class hash: {}", + format!("{:#064x}", casm_class_hash).bright_yellow() + ); + } - if !fee_setting.is_estimate_only() { - eprintln!( - "CASM class hash: {}", - format!("{:#064x}", casm_class_hash).bright_yellow() - ); - } + let declare_tx = match fee_setting { + FeeSetting::Eth(fee_setting) => { + let declaration = account + .declare_v2(Arc::new(class), casm_class_hash) + .fee_estimate_multiplier(1.5f64); + let declaration = match self.nonce { + Some(nonce) => declaration.nonce(nonce), + None => declaration, + }; + + let declaration = match fee_setting { + TokenFeeSetting::EstimateOnly => { + let estimated_fee = declaration + .estimate_fee() + .await + .map_err(account_error_mapper)? + .overall_fee; + + println!( + "{} ETH", + format!("{}", felt_to_bigdecimal(estimated_fee, 18)) + .bright_yellow(), + ); + return Ok(()); + } + TokenFeeSetting::Manual(fee) => declaration.max_fee(fee.max_fee), + TokenFeeSetting::None => declaration, + }; - let declare_tx = match fee_setting { - FeeSetting::Eth(fee_setting) => { - let declaration = account - .declare_v2(Arc::new(class.flatten()?), casm_class_hash) - .fee_estimate_multiplier(1.5f64); - let declaration = match self.nonce { - Some(nonce) => declaration.nonce(nonce), - None => declaration, - }; - - let declaration = match fee_setting { - TokenFeeSetting::EstimateOnly => { - let estimated_fee = declaration - .estimate_fee() - .await - .map_err(account_error_mapper)? - .overall_fee; - - println!( - "{} ETH", - format!("{}", felt_to_bigdecimal(estimated_fee, 18)) - .bright_yellow(), - ); + if self.simulate { + print_colored_json(&declaration.simulate(false, false).await?)?; return Ok(()); } - TokenFeeSetting::Manual(fee) => declaration.max_fee(fee.max_fee), - TokenFeeSetting::None => declaration, - }; - if self.simulate { - print_colored_json(&declaration.simulate(false, false).await?)?; - return Ok(()); + declaration.send().await } + FeeSetting::Strk(fee_setting) => { + let declaration = account.declare_v3(Arc::new(class), casm_class_hash); + let declaration = match self.nonce { + Some(nonce) => declaration.nonce(nonce), + None => declaration, + }; + + let declaration = match fee_setting { + TokenFeeSetting::EstimateOnly => { + let estimated_fee = declaration + .estimate_fee() + .await + .map_err(account_error_mapper)? + .overall_fee; + + println!( + "{} STRK", + format!("{}", felt_to_bigdecimal(estimated_fee, 18)) + .bright_yellow(), + ); + return Ok(()); + } + TokenFeeSetting::Manual(fee) => { + let declaration = if let Some(gas) = fee.gas { + declaration.gas(gas) + } else { + declaration + }; + + if let Some(gas_price) = fee.gas_price { + declaration.gas_price(gas_price) + } else { + declaration + } + } + TokenFeeSetting::None => declaration, + }; - declaration.send().await - } - FeeSetting::Strk(fee_setting) => { - let declaration = - account.declare_v3(Arc::new(class.flatten()?), casm_class_hash); - let declaration = match self.nonce { - Some(nonce) => declaration.nonce(nonce), - None => declaration, - }; - - let declaration = match fee_setting { - TokenFeeSetting::EstimateOnly => { - let estimated_fee = declaration - .estimate_fee() - .await - .map_err(account_error_mapper)? - .overall_fee; - - println!( - "{} STRK", - format!("{}", felt_to_bigdecimal(estimated_fee, 18)) - .bright_yellow(), - ); + if self.simulate { + print_colored_json(&declaration.simulate(false, false).await?)?; return Ok(()); } - TokenFeeSetting::Manual(fee) => { - let declaration = if let Some(gas) = fee.gas { - declaration.gas(gas) - } else { - declaration - }; - - if let Some(gas_price) = fee.gas_price { - declaration.gas_price(gas_price) - } else { - declaration - } - } - TokenFeeSetting::None => declaration, - }; - if self.simulate { - print_colored_json(&declaration.simulate(false, false).await?)?; - return Ok(()); + declaration.send().await } - - declaration.send().await } - } - .map_err(account_error_mapper)? - .transaction_hash; + .map_err(account_error_mapper)? + .transaction_hash; - (class_hash, declare_tx) - } else if let Ok(_) = - serde_json::from_reader::<_, CompiledClass>(std::fs::File::open(&self.file)?) - { - // TODO: add more helpful instructions to fix this - anyhow::bail!("unexpected CASM class"); - } else if let Ok(mut class) = - serde_json::from_reader::<_, LegacyContractClass>(std::fs::File::open(self.file)?) - { - if self.no_abi { - class.abi = vec![]; + (class_hash, declare_tx) } + Declarable::CairoZero(mut class) => { + if self.no_abi { + class.abi = vec![]; + } - // Declaring Cairo 0 class - let class_hash = class.class_hash()?; + // Declaring Cairo 0 class + let class_hash = class.class_hash()?; - // TODO: add option to skip checking - if Self::check_already_declared(&provider, class_hash).await? { - return Ok(()); - } + // TODO: add option to skip checking + if Self::check_already_declared(&provider, class_hash).await? { + return Ok(()); + } - if !fee_setting.is_estimate_only() { - eprintln!( - "Declaring Cairo 0 (deprecated) class: {}", - format!("{:#064x}", class_hash).bright_yellow() - ); - } + if !fee_setting.is_estimate_only() { + eprintln!( + "Declaring Cairo 0 (deprecated) class: {}", + format!("{:#064x}", class_hash).bright_yellow() + ); + } - // TODO: make buffer configurable - let declaration = account - .declare_legacy(Arc::new(class)) - .fee_estimate_multiplier(1.5f64); - let declaration = match self.nonce { - Some(nonce) => declaration.nonce(nonce), - None => declaration, - }; - - let declaration = match fee_setting { - FeeSetting::Eth(TokenFeeSetting::EstimateOnly) => { - let estimated_fee = declaration - .estimate_fee() - .await - .map_err(account_error_mapper)? - .overall_fee; + // TODO: make buffer configurable + let declaration = account + .declare_legacy(Arc::new(class)) + .fee_estimate_multiplier(1.5f64); + let declaration = match self.nonce { + Some(nonce) => declaration.nonce(nonce), + None => declaration, + }; + + let declaration = match fee_setting { + FeeSetting::Eth(TokenFeeSetting::EstimateOnly) => { + let estimated_fee = declaration + .estimate_fee() + .await + .map_err(account_error_mapper)? + .overall_fee; + + println!( + "{} ETH", + format!("{}", felt_to_bigdecimal(estimated_fee, 18)).bright_yellow(), + ); + return Ok(()); + } + FeeSetting::Eth(TokenFeeSetting::Manual(fee)) => { + declaration.max_fee(fee.max_fee) + } + FeeSetting::Eth(TokenFeeSetting::None) => declaration, + FeeSetting::Strk(_) => { + anyhow::bail!( + "Cairo 0 declaration transactions must not pay fees with STRK" + ) + } + }; - println!( - "{} ETH", - format!("{}", felt_to_bigdecimal(estimated_fee, 18)).bright_yellow(), - ); + if self.simulate { + print_colored_json(&declaration.simulate(false, false).await?)?; return Ok(()); } - FeeSetting::Eth(TokenFeeSetting::Manual(fee)) => declaration.max_fee(fee.max_fee), - FeeSetting::Eth(TokenFeeSetting::None) => declaration, - FeeSetting::Strk(_) => { - anyhow::bail!("Cairo 0 declaration transactions must not pay fees with STRK") - } - }; - if self.simulate { - print_colored_json(&declaration.simulate(false, false).await?)?; - return Ok(()); + ( + class_hash, + declaration + .send() + .await + .map_err(account_error_mapper)? + .transaction_hash, + ) } - - ( - class_hash, - declaration - .send() - .await - .map_err(account_error_mapper)? - .transaction_hash, - ) - } else { - anyhow::bail!("failed to parse contract artifact"); }; eprintln!(