From 2384b57ab77696f1c0bbc3ab40f1ca616a2448a1 Mon Sep 17 00:00:00 2001 From: Jonathan LEI Date: Thu, 26 Oct 2023 07:56:37 +0000 Subject: [PATCH] feat: transaction simulation Adds a `--simulate` flag to commands that send out transactions. There's no support for skipping validation or fees yet. --- Cargo.lock | 22 ++++----- Cargo.toml | 2 +- src/provider.rs | 40 ++++++++++++++++ src/subcommands/account/deploy.rs | 79 ++++++++++++++++++------------- src/subcommands/declare.rs | 42 ++++++++++++---- src/subcommands/deploy.rs | 23 +++++++-- src/subcommands/invoke.rs | 19 +++++++- 7 files changed, 169 insertions(+), 58 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 937435b..3014982 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2883,7 +2883,7 @@ dependencies = [ [[package]] name = "starknet" version = "0.6.0" -source = "git+https://github.com/xJonathanLEI/starknet-rs?rev=3505fae8f97492416253cbda4f170e6b9a9cdb58#3505fae8f97492416253cbda4f170e6b9a9cdb58" +source = "git+https://github.com/xJonathanLEI/starknet-rs?rev=33e200d3d53a685586ee178b4bd0be4b48250c08#33e200d3d53a685586ee178b4bd0be4b48250c08" dependencies = [ "starknet-accounts", "starknet-contract", @@ -2898,7 +2898,7 @@ dependencies = [ [[package]] name = "starknet-accounts" version = "0.5.0" -source = "git+https://github.com/xJonathanLEI/starknet-rs?rev=3505fae8f97492416253cbda4f170e6b9a9cdb58#3505fae8f97492416253cbda4f170e6b9a9cdb58" +source = "git+https://github.com/xJonathanLEI/starknet-rs?rev=33e200d3d53a685586ee178b4bd0be4b48250c08#33e200d3d53a685586ee178b4bd0be4b48250c08" dependencies = [ "async-trait", "auto_impl", @@ -2911,7 +2911,7 @@ dependencies = [ [[package]] name = "starknet-contract" version = "0.5.0" -source = "git+https://github.com/xJonathanLEI/starknet-rs?rev=3505fae8f97492416253cbda4f170e6b9a9cdb58#3505fae8f97492416253cbda4f170e6b9a9cdb58" +source = "git+https://github.com/xJonathanLEI/starknet-rs?rev=33e200d3d53a685586ee178b4bd0be4b48250c08#33e200d3d53a685586ee178b4bd0be4b48250c08" dependencies = [ "serde", "serde_json", @@ -2925,7 +2925,7 @@ dependencies = [ [[package]] name = "starknet-core" version = "0.6.1" -source = "git+https://github.com/xJonathanLEI/starknet-rs?rev=3505fae8f97492416253cbda4f170e6b9a9cdb58#3505fae8f97492416253cbda4f170e6b9a9cdb58" +source = "git+https://github.com/xJonathanLEI/starknet-rs?rev=33e200d3d53a685586ee178b4bd0be4b48250c08#33e200d3d53a685586ee178b4bd0be4b48250c08" dependencies = [ "base64 0.21.2", "flate2", @@ -2942,7 +2942,7 @@ dependencies = [ [[package]] name = "starknet-crypto" version = "0.6.0" -source = "git+https://github.com/xJonathanLEI/starknet-rs?rev=3505fae8f97492416253cbda4f170e6b9a9cdb58#3505fae8f97492416253cbda4f170e6b9a9cdb58" +source = "git+https://github.com/xJonathanLEI/starknet-rs?rev=33e200d3d53a685586ee178b4bd0be4b48250c08#33e200d3d53a685586ee178b4bd0be4b48250c08" dependencies = [ "crypto-bigint", "hex", @@ -2961,7 +2961,7 @@ dependencies = [ [[package]] name = "starknet-crypto-codegen" version = "0.3.2" -source = "git+https://github.com/xJonathanLEI/starknet-rs?rev=3505fae8f97492416253cbda4f170e6b9a9cdb58#3505fae8f97492416253cbda4f170e6b9a9cdb58" +source = "git+https://github.com/xJonathanLEI/starknet-rs?rev=33e200d3d53a685586ee178b4bd0be4b48250c08#33e200d3d53a685586ee178b4bd0be4b48250c08" dependencies = [ "starknet-curve", "starknet-ff", @@ -2971,7 +2971,7 @@ dependencies = [ [[package]] name = "starknet-curve" version = "0.4.0" -source = "git+https://github.com/xJonathanLEI/starknet-rs?rev=3505fae8f97492416253cbda4f170e6b9a9cdb58#3505fae8f97492416253cbda4f170e6b9a9cdb58" +source = "git+https://github.com/xJonathanLEI/starknet-rs?rev=33e200d3d53a685586ee178b4bd0be4b48250c08#33e200d3d53a685586ee178b4bd0be4b48250c08" dependencies = [ "starknet-ff", ] @@ -2979,7 +2979,7 @@ dependencies = [ [[package]] name = "starknet-ff" version = "0.3.4" -source = "git+https://github.com/xJonathanLEI/starknet-rs?rev=3505fae8f97492416253cbda4f170e6b9a9cdb58#3505fae8f97492416253cbda4f170e6b9a9cdb58" +source = "git+https://github.com/xJonathanLEI/starknet-rs?rev=33e200d3d53a685586ee178b4bd0be4b48250c08#33e200d3d53a685586ee178b4bd0be4b48250c08" dependencies = [ "ark-ff", "bigdecimal 0.3.1", @@ -2993,7 +2993,7 @@ dependencies = [ [[package]] name = "starknet-macros" version = "0.1.3" -source = "git+https://github.com/xJonathanLEI/starknet-rs?rev=3505fae8f97492416253cbda4f170e6b9a9cdb58#3505fae8f97492416253cbda4f170e6b9a9cdb58" +source = "git+https://github.com/xJonathanLEI/starknet-rs?rev=33e200d3d53a685586ee178b4bd0be4b48250c08#33e200d3d53a685586ee178b4bd0be4b48250c08" dependencies = [ "starknet-core", "syn 2.0.28", @@ -3002,7 +3002,7 @@ dependencies = [ [[package]] name = "starknet-providers" version = "0.6.0" -source = "git+https://github.com/xJonathanLEI/starknet-rs?rev=3505fae8f97492416253cbda4f170e6b9a9cdb58#3505fae8f97492416253cbda4f170e6b9a9cdb58" +source = "git+https://github.com/xJonathanLEI/starknet-rs?rev=33e200d3d53a685586ee178b4bd0be4b48250c08#33e200d3d53a685586ee178b4bd0be4b48250c08" dependencies = [ "async-trait", "auto_impl", @@ -3021,7 +3021,7 @@ dependencies = [ [[package]] name = "starknet-signers" version = "0.4.0" -source = "git+https://github.com/xJonathanLEI/starknet-rs?rev=3505fae8f97492416253cbda4f170e6b9a9cdb58#3505fae8f97492416253cbda4f170e6b9a9cdb58" +source = "git+https://github.com/xJonathanLEI/starknet-rs?rev=33e200d3d53a685586ee178b4bd0be4b48250c08#33e200d3d53a685586ee178b4bd0be4b48250c08" dependencies = [ "async-trait", "auto_impl", diff --git a/Cargo.toml b/Cargo.toml index 7b3c043..c5d091a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,7 +31,7 @@ serde = { version = "1.0.164", features = ["derive"] } serde_json = { version = "1.0.99", features = ["preserve_order"] } serde_with = "2.3.3" shellexpand = "3.1.0" -starknet = { git = "https://github.com/xJonathanLEI/starknet-rs", rev = "3505fae8f97492416253cbda4f170e6b9a9cdb58" } +starknet = { git = "https://github.com/xJonathanLEI/starknet-rs", rev = "33e200d3d53a685586ee178b4bd0be4b48250c08" } tempfile = "3.8.0" thiserror = "1.0.40" tokio = { version = "1.28.2", default-features = false, features = ["macros", "rt-multi-thread"] } diff --git a/src/provider.rs b/src/provider.rs index 933dab9..6a20872 100644 --- a/src/provider.rs +++ b/src/provider.rs @@ -370,4 +370,44 @@ impl Provider for ExtendedProvider { ) .await } + + async fn trace_transaction( + &self, + transaction_hash: H, + ) -> Result> + where + H: AsRef + Send + Sync, + { + ::trace_transaction(&self.provider, transaction_hash).await + } + + async fn simulate_transactions( + &self, + block_id: B, + transactions: T, + simulation_flags: S, + ) -> Result, ProviderError> + where + B: AsRef + Send + Sync, + T: AsRef<[BroadcastedTransaction]> + Send + Sync, + S: AsRef<[SimulationFlag]> + Send + Sync, + { + ::simulate_transactions( + &self.provider, + block_id, + transactions, + simulation_flags, + ) + .await + } + + async fn trace_block_transactions( + &self, + block_hash: H, + ) -> Result, ProviderError> + where + H: AsRef + Send + Sync, + { + ::trace_block_transactions(&self.provider, block_hash).await + } } diff --git a/src/subcommands/account/deploy.rs b/src/subcommands/account/deploy.rs index c7f5e88..f59e004 100644 --- a/src/subcommands/account/deploy.rs +++ b/src/subcommands/account/deploy.rs @@ -3,6 +3,7 @@ use std::{io::Write, path::PathBuf, sync::Arc, time::Duration}; use anyhow::Result; use clap::Parser; use colored::Colorize; +use colored_json::{ColorMode, Output}; use starknet::{ accounts::{AccountFactory, ArgentAccountFactory, OpenZeppelinAccountFactory}, core::types::{BlockId, BlockTag, FieldElement}, @@ -32,6 +33,8 @@ pub struct Deploy { signer: SignerArgs, #[clap(flatten)] fee: FeeArgs, + #[clap(long, help = "Simulate the transaction only")] + simulate: bool, #[clap(long, help = "Provide transaction nonce manually")] nonce: Option, #[clap( @@ -65,6 +68,9 @@ impl Deploy { self.verbosity.setup_logging(); let fee_setting = self.fee.into_setting()?; + if self.simulate && fee_setting.is_estimate_only() { + anyhow::bail!("--simulate cannot be used with --estimate-only"); + } let provider = Arc::new(self.provider.into_provider()); let signer = Arc::new(self.signer.into_signer()?); @@ -216,48 +222,57 @@ impl Deploy { } }; - match max_fee { - MaxFeeType::Manual { max_fee } => { - eprintln!( - "You've manually specified the account deployment fee to be {}. \ - Therefore, fund at least:\n {}", - format!("{} ETH", max_fee.to_big_decimal(18)).bright_yellow(), - format!("{} ETH", max_fee.to_big_decimal(18)).bright_yellow(), - ); - } - MaxFeeType::Estimated { - estimate, - estimate_with_buffer, - } => { - eprintln!( - "The estimated account deployment fee is {}. \ - However, to avoid failure, fund at least:\n {}", - format!("{} ETH", estimate.to_big_decimal(18)).bright_yellow(), - format!("{} ETH", estimate_with_buffer.to_big_decimal(18)).bright_yellow() - ); + if !self.simulate { + match max_fee { + MaxFeeType::Manual { max_fee } => { + eprintln!( + "You've manually specified the account deployment fee to be {}. \ + Therefore, fund at least:\n {}", + format!("{} ETH", max_fee.to_big_decimal(18)).bright_yellow(), + format!("{} ETH", max_fee.to_big_decimal(18)).bright_yellow(), + ); + } + MaxFeeType::Estimated { + estimate, + estimate_with_buffer, + } => { + eprintln!( + "The estimated account deployment fee is {}. \ + However, to avoid failure, fund at least:\n {}", + format!("{} ETH", estimate.to_big_decimal(18)).bright_yellow(), + format!("{} ETH", estimate_with_buffer.to_big_decimal(18)).bright_yellow() + ); + } } - } - eprintln!( - "to the following address:\n {}", - format!("{:#064x}", target_deployment_address).bright_yellow() - ); + eprintln!( + "to the following address:\n {}", + format!("{:#064x}", target_deployment_address).bright_yellow() + ); - // TODO: add flag for skipping this manual confirmation step - eprint!("Press [ENTER] once you've funded the address."); - std::io::stdin().read_line(&mut String::new())?; + // TODO: add flag for skipping this manual confirmation step + eprint!("Press [ENTER] once you've funded the address."); + std::io::stdin().read_line(&mut String::new())?; + } let account_deployment = match self.nonce { Some(nonce) => account_deployment.nonce(nonce), None => account_deployment, }; + let account_deployment = account_deployment.max_fee(max_fee.max_fee()); + + if self.simulate { + let simulation = account_deployment.simulate(false, false).await?; + let simulation_json = serde_json::to_value(simulation)?; + + let simulation_json = + colored_json::to_colored_json(&simulation_json, ColorMode::Auto(Output::StdOut))?; + println!("{simulation_json}"); + return Ok(()); + } // TODO: add option to check ETH balance before sending out tx - let account_deployment_tx = account_deployment - .max_fee(max_fee.max_fee()) - .send() - .await? - .transaction_hash; + let account_deployment_tx = account_deployment.send().await?.transaction_hash; eprintln!( "Account deployment transaction: {}", format!("{:#064x}", account_deployment_tx).bright_yellow() diff --git a/src/subcommands/declare.rs b/src/subcommands/declare.rs index 5a55104..773b9ed 100644 --- a/src/subcommands/declare.rs +++ b/src/subcommands/declare.rs @@ -3,6 +3,7 @@ use std::{path::PathBuf, sync::Arc, time::Duration}; use anyhow::Result; use clap::Parser; use colored::Colorize; +use colored_json::{ColorMode, Output}; use starknet::{ accounts::Account, core::types::{ @@ -32,6 +33,8 @@ pub struct Declare { casm: CasmArgs, #[clap(flatten)] fee: FeeArgs, + #[clap(long, help = "Simulate the transaction only")] + simulate: bool, #[clap(long, help = "Provide transaction nonce manually")] nonce: Option, #[clap(long, help = "Wait for the transaction to confirm")] @@ -57,6 +60,9 @@ impl Declare { self.verbosity.setup_logging(); let fee_setting = self.fee.into_setting()?; + if self.simulate && fee_setting.is_estimate_only() { + anyhow::bail!("--simulate cannot be used with --estimate-only"); + } let provider = Arc::new(self.provider.into_provider()); @@ -159,11 +165,21 @@ impl Declare { Some(nonce) => declaration.nonce(nonce), None => declaration, }; + let declaration = declaration.max_fee(max_fee); - ( - class_hash, - declaration.max_fee(max_fee).send().await?.transaction_hash, - ) + if self.simulate { + let simulation = declaration.simulate(false, false).await?; + let simulation_json = serde_json::to_value(simulation)?; + + let simulation_json = colored_json::to_colored_json( + &simulation_json, + ColorMode::Auto(Output::StdOut), + )?; + println!("{simulation_json}"); + return Ok(()); + } + + (class_hash, declaration.send().await?.transaction_hash) } else if let Ok(_) = serde_json::from_reader::<_, CompiledClass>(std::fs::File::open(&self.file)?) { @@ -219,11 +235,21 @@ impl Declare { Some(nonce) => declaration.nonce(nonce), None => declaration, }; + let declaration = declaration.max_fee(max_fee); - ( - class_hash, - declaration.max_fee(max_fee).send().await?.transaction_hash, - ) + if self.simulate { + let simulation = declaration.simulate(false, false).await?; + let simulation_json = serde_json::to_value(simulation)?; + + let simulation_json = colored_json::to_colored_json( + &simulation_json, + ColorMode::Auto(Output::StdOut), + )?; + println!("{simulation_json}"); + return Ok(()); + } + + (class_hash, declaration.send().await?.transaction_hash) } else { anyhow::bail!("failed to parse contract artifact"); }; diff --git a/src/subcommands/deploy.rs b/src/subcommands/deploy.rs index 9fc2101..909655d 100644 --- a/src/subcommands/deploy.rs +++ b/src/subcommands/deploy.rs @@ -3,6 +3,7 @@ use std::{sync::Arc, time::Duration}; use anyhow::Result; use clap::Parser; use colored::Colorize; +use colored_json::{ColorMode, Output}; use starknet::{contract::ContractFactory, core::types::FieldElement, signers::SigningKey}; use crate::{ @@ -33,6 +34,8 @@ pub struct Deploy { not_unique: bool, #[clap(flatten)] fee: FeeArgs, + #[clap(long, help = "Simulate the transaction only")] + simulate: bool, #[clap(long, help = "Use the given salt to compute contract deploy address")] salt: Option, #[clap(long, help = "Provide transaction nonce manually")] @@ -59,6 +62,9 @@ impl Deploy { self.verbosity.setup_logging(); let fee_setting = self.fee.into_setting()?; + if self.simulate && fee_setting.is_estimate_only() { + anyhow::bail!("--simulate cannot be used with --estimate-only"); + } let provider = Arc::new(self.provider.into_provider()); let felt_decoder = FeltDecoder::new(AddressBookResolver::new(provider.clone())); @@ -121,12 +127,19 @@ impl Deploy { Some(nonce) => contract_deployment.nonce(nonce), None => contract_deployment, }; + let contract_deployment = contract_deployment.max_fee(max_fee); + + if self.simulate { + let simulation = contract_deployment.simulate(false, false).await?; + let simulation_json = serde_json::to_value(simulation)?; + + let simulation_json = + colored_json::to_colored_json(&simulation_json, ColorMode::Auto(Output::StdOut))?; + println!("{simulation_json}"); + return Ok(()); + } - let deployment_tx = contract_deployment - .max_fee(max_fee) - .send() - .await? - .transaction_hash; + let deployment_tx = contract_deployment.send().await?.transaction_hash; eprintln!( "Contract deployment transaction: {}", format!("{:#064x}", deployment_tx).bright_yellow() diff --git a/src/subcommands/invoke.rs b/src/subcommands/invoke.rs index d5f9cf1..62882a8 100644 --- a/src/subcommands/invoke.rs +++ b/src/subcommands/invoke.rs @@ -3,6 +3,7 @@ use std::{sync::Arc, time::Duration}; use anyhow::Result; use clap::Parser; use colored::Colorize; +use colored_json::{ColorMode, Output}; use starknet::{ accounts::{Account, Call}, core::types::FieldElement, @@ -26,6 +27,8 @@ pub struct Invoke { account: AccountArgs, #[clap(flatten)] fee: FeeArgs, + #[clap(long, help = "Simulate the transaction only")] + simulate: bool, #[clap(long, help = "Provide transaction nonce manually")] nonce: Option, #[clap(long, help = "Wait for the transaction to confirm")] @@ -48,6 +51,9 @@ impl Invoke { self.verbosity.setup_logging(); let fee_setting = self.fee.into_setting()?; + if self.simulate && fee_setting.is_estimate_only() { + anyhow::bail!("--simulate cannot be used with --estimate-only"); + } let provider = Arc::new(self.provider.into_provider()); let felt_decoder = FeltDecoder::new(AddressBookResolver::new(provider.clone())); @@ -126,8 +132,19 @@ impl Invoke { Some(nonce) => execution.nonce(nonce), None => execution, }; + let execution = execution.max_fee(max_fee); + + if self.simulate { + let simulation = execution.simulate(false, false).await?; + let simulation_json = serde_json::to_value(simulation)?; + + let simulation_json = + colored_json::to_colored_json(&simulation_json, ColorMode::Auto(Output::StdOut))?; + println!("{simulation_json}"); + return Ok(()); + } - let invoke_tx = execution.max_fee(max_fee).send().await?.transaction_hash; + let invoke_tx = execution.send().await?.transaction_hash; eprintln!( "Invoke transaction: {}", format!("{:#064x}", invoke_tx).bright_yellow()