diff --git a/CHANGELOG.md b/CHANGELOG.md index b613a343c7..2a594e2ddc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,9 +9,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Cast -#### Added +### Added -- interactive interface that allows setting created or imported account as the default +- `sncast deploy` can now deploy contracts by name instead of class hash. [Read more here](https://foundry-rs.github.io/starknet-foundry/starknet/deploy.html) +- `sncast declare-deploy` allows to declare and deploy contract at once. [Read more here](https://foundry-rs.github.io/starknet-foundry/starknet/declare-deploy.html) +- Interactive interface that allows setting created or imported account as the default ## [0.35.1] - 2024-12-16 diff --git a/crates/sncast/src/helpers/fee.rs b/crates/sncast/src/helpers/fee.rs index 7492961687..423cde6ca2 100644 --- a/crates/sncast/src/helpers/fee.rs +++ b/crates/sncast/src/helpers/fee.rs @@ -8,7 +8,7 @@ use starknet::providers::Provider; use starknet_types_core::felt::{Felt, NonZeroFelt}; use std::str::FromStr; -#[derive(Args, Debug, Clone)] +#[derive(Args, Debug, Clone, Default)] pub struct FeeArgs { /// Token that transaction fee will be paid in #[clap(long, value_parser = parse_fee_token)] diff --git a/crates/sncast/src/helpers/scarb_utils.rs b/crates/sncast/src/helpers/scarb_utils.rs index d179b31244..6edbdb1d83 100644 --- a/crates/sncast/src/helpers/scarb_utils.rs +++ b/crates/sncast/src/helpers/scarb_utils.rs @@ -1,4 +1,4 @@ -use anyhow::{anyhow, Context, Result}; +use anyhow::{anyhow, bail, Context, Result}; use camino::{Utf8Path, Utf8PathBuf}; use scarb_api::{ get_contracts_artifacts_and_source_sierra_paths, @@ -7,6 +7,14 @@ use scarb_api::{ }; use scarb_ui::args::PackagesFilter; use shared::{command::CommandExt, print::print_as_warning}; +use starknet::{ + core::types::{ + contract::{CompiledClass, SierraClass}, + BlockId, FlattenedSierraClass, + }, + providers::{jsonrpc::HttpTransport, JsonRpcClient, Provider, ProviderError}, +}; +use starknet_types_core::felt::Felt; use std::collections::HashMap; use std::env; use std::str::FromStr; @@ -188,6 +196,72 @@ pub fn build_and_load_artifacts( } } +pub fn read_manifest_and_build_artifacts( + package: &Option, + json: bool, + profile: Option<&String>, +) -> Result> { + let manifest_path = assert_manifest_path_exists()?; + let package_metadata = get_package_metadata(&manifest_path, package)?; + + let default_profile = "dev".to_string(); + let profile = profile.unwrap_or(&default_profile); + + let build_config = BuildConfig { + scarb_toml_path: manifest_path, + json, + profile: profile.to_string(), + }; + + build_and_load_artifacts(&package_metadata, &build_config, false) + .context("Failed to build contract") +} + +pub struct CompiledContract { + pub class: FlattenedSierraClass, + pub sierra_class_hash: Felt, + pub casm_class_hash: Felt, +} + +impl TryFrom<&StarknetContractArtifacts> for CompiledContract { + type Error = anyhow::Error; + + fn try_from(artifacts: &StarknetContractArtifacts) -> Result { + let sierra_class = serde_json::from_str::(&artifacts.sierra) + .context("Failed to parse Sierra artifact")? + .flatten()?; + + let compiled_class = serde_json::from_str::(&artifacts.casm) + .context("Failed to parse CASM artifact")?; + + let sierra_class_hash = sierra_class.class_hash(); + let casm_class_hash = compiled_class.class_hash()?; + + Ok(Self { + class: sierra_class, + sierra_class_hash, + casm_class_hash, + }) + } +} + +impl CompiledContract { + pub async fn is_declared(&self, provider: &JsonRpcClient) -> Result { + let block_id = BlockId::Tag(starknet::core::types::BlockTag::Pending); + let class_hash = self.sierra_class_hash; + + let response = provider.get_class(block_id, class_hash).await; + + match response { + Ok(_) => Ok(true), + Err(ProviderError::StarknetError( + starknet::core::types::StarknetError::ClassHashNotFound, + )) => Ok(false), + Err(other) => bail!(other), + } + } +} + #[cfg(test)] mod tests { use crate::helpers::scarb_utils::{get_package_metadata, get_scarb_metadata}; diff --git a/crates/sncast/src/main.rs b/crates/sncast/src/main.rs index 2db1f8e788..301417be9b 100644 --- a/crates/sncast/src/main.rs +++ b/crates/sncast/src/main.rs @@ -2,10 +2,12 @@ use crate::starknet_commands::{ account, account::Account, call::Call, declare::Declare, deploy::Deploy, invoke::Invoke, multicall::Multicall, script::Script, show_config::ShowConfig, tx_status::TxStatus, }; -use anyhow::{Context, Result}; +use anyhow::{bail, Context, Result}; use data_transformer::Calldata; use sncast::response::explorer_link::print_block_explorer_link_if_allowed; use sncast::response::print::{print_command_result, OutputFormat}; +use starknet_commands::declare_deploy::DeclareDeploy; +use starknet_commands::deploy::DeployResolved; use std::io; use std::io::IsTerminal; @@ -20,10 +22,10 @@ use sncast::helpers::fee::PayableTransaction; use sncast::helpers::interactive::prompt_to_add_account_as_default; use sncast::helpers::scarb_utils::{ assert_manifest_path_exists, build, build_and_load_artifacts, get_package_metadata, - get_scarb_metadata_with_deps, BuildConfig, + get_scarb_metadata_with_deps, read_manifest_and_build_artifacts, BuildConfig, }; use sncast::response::errors::handle_starknet_command_error; -use sncast::response::structs::DeclareResponse; +use sncast::response::structs::{DeclareDeployResponse, DeclareResponse}; use sncast::{ chain_id_to_network_name, get_account, get_block_id, get_chain_id, get_class_hash_by_address, get_contract_class, get_default_state_file_name, NumbersFormat, ValidatedWaitParams, WaitForTx, @@ -127,6 +129,9 @@ enum Commands { /// Deploy a contract Deploy(Deploy), + // Declare a contract if it has not been yet and deploy it + DeclareDeploy(DeclareDeploy), + /// Call a contract Call(Call), @@ -192,6 +197,7 @@ impl From for Arguments { let DeployArguments { constructor_calldata, arguments, + .. } = value; Self { calldata: constructor_calldata, @@ -238,7 +244,7 @@ async fn run_async_command( Commands::Declare(declare) => { let provider = declare.rpc.get_provider(&config).await?; - let fee_token = declare.validate_and_get_token()?; + declare.validate_and_get_token()?; let account = get_account( &config.account, @@ -247,38 +253,136 @@ async fn run_async_command( config.keystore, ) .await?; - let manifest_path = assert_manifest_path_exists()?; - let package_metadata = get_package_metadata(&manifest_path, &declare.package)?; - let artifacts = build_and_load_artifacts( - &package_metadata, - &BuildConfig { - scarb_toml_path: manifest_path, - json: cli.json, - profile: cli.profile.unwrap_or("release".to_string()), - }, - false, + + let artifacts = read_manifest_and_build_artifacts( + &declare.package, + cli.json, + cli.profile.as_ref(), + )?; + + let result = + starknet_commands::declare::declare(declare, &account, &artifacts, wait_config) + .await + .map_err(handle_starknet_command_error) + .map(|result| match result { + DeclareResponse::Success(declare_transaction_response) => { + declare_transaction_response + } + DeclareResponse::AlreadyDeclared(_) => { + unreachable!("Argument `skip_on_already_declared` is false") + } + }); + + print_command_result("declare", &result, numbers_format, output_format)?; + print_block_explorer_link_if_allowed( + &result, + output_format, + provider.chain_id().await?, + config.show_explorer_links, + config.block_explorer, + ); + Ok(()) + } + + Commands::DeclareDeploy(declare_deploy) => { + let provider = declare_deploy.rpc.get_provider(&config).await?; + + let account = get_account( + &config.account, + &config.accounts_file, + &provider, + config.keystore, ) - .expect("Failed to build contract"); - let result = starknet_commands::declare::declare( - declare, + .await?; + + let fee_token = declare_deploy.fee_token.clone(); + let declare = Declare::from(&declare_deploy); + let deploy = Deploy::from(declare_deploy); + + let contract = + deploy.build_artifacts_and_get_compiled_contract(cli.json, cli.profile.as_ref())?; + let class_hash = contract.sierra_class_hash; + + let needs_declaration = !contract.is_declared(&provider).await?; + + let declare_result = if needs_declaration { + let result = crate::starknet_commands::declare::declare_compiled( + declare, + &account, + contract, + wait_config, + false, + fee_token, + ) + .await + .map_err(handle_starknet_command_error); + + if result.is_err() { + return print_command_result( + "declare-deploy", + &result, + numbers_format, + output_format, + ); + } + + Some(result.unwrap()) + } else { + None + }; + + let deploy_resolved = deploy.resolved_with_class_hash(class_hash); + + let DeployResolved { + class_hash, + constructor_calldata, + salt, + unique, + fee_args, + nonce, + .. + } = deploy_resolved; + + let block_id = account.block_id(); + let fee_settings = fee_args.try_into_fee_settings(&provider, block_id).await?; + + let calldata = constructor_calldata + .iter() + .map(|data| { + Felt::from_dec_str(data) + .or_else(|_| Felt::from_hex(data)) + .context("Failed to parse to felt") + }) + .collect::>>()?; + + let deploy_result = starknet_commands::deploy::deploy( + class_hash, + &calldata, + salt, + unique, + fee_settings, + nonce, &account, - &artifacts, wait_config, - false, - fee_token, ) .await - .map_err(handle_starknet_command_error) - .map(|result| match result { - DeclareResponse::Success(declare_transaction_response) => { - declare_transaction_response - } - DeclareResponse::AlreadyDeclared(_) => { - unreachable!("Argument `skip_on_already_declared` is false") - } - }); + .map_err(handle_starknet_command_error); - print_command_result("declare", &result, numbers_format, output_format)?; + if deploy_result.is_err() { + return print_command_result( + "declare-deploy", + &deploy_result, + numbers_format, + output_format, + ); + } + + let result = Ok(DeclareDeployResponse::new( + &declare_result, + &deploy_result.unwrap(), + )); + + print_command_result("declare-deploy", &result, numbers_format, output_format)?; print_block_explorer_link_if_allowed( &result, output_format, @@ -290,14 +394,7 @@ async fn run_async_command( } Commands::Deploy(deploy) => { - let fee_token = deploy.validate_and_get_token()?; - - let Deploy { - arguments, - fee_args, - rpc, - .. - } = deploy; + let rpc = deploy.rpc.clone(); let provider = rpc.get_provider(&config).await?; @@ -309,27 +406,49 @@ async fn run_async_command( ) .await?; - let fee_settings = fee_args - .clone() - .fee_token(fee_token) - .try_into_fee_settings(&provider, account.block_id()) - .await?; + let deploy_resolved: DeployResolved = if deploy.class_hash.clone().is_some() { + deploy.clone().try_into().unwrap() + } else { + let contract = deploy + .build_artifacts_and_get_compiled_contract(cli.json, cli.profile.as_ref())?; + let class_hash = contract.sierra_class_hash; + + if !contract.is_declared(&provider).await? { + bail!("Contract with class hash {:x} is not declared", class_hash); + } + + deploy.clone().resolved_with_class_hash(class_hash) + }; + + let fee_token = deploy_resolved.validate_and_get_token()?; + + let Deploy { + arguments, + fee_args, + .. + } = deploy; // safe to unwrap because "constructor" is a standardized name let selector = get_selector_from_name("constructor").unwrap(); - let contract_class = get_contract_class(deploy.class_hash, &provider).await?; + let contract_class = get_contract_class(deploy_resolved.class_hash, &provider).await?; let arguments: Arguments = arguments.into(); let calldata = arguments.try_into_calldata(contract_class, &selector)?; + let fee_settings = fee_args + .clone() + .fee_token(fee_token) + .try_into_fee_settings(&provider, account.block_id()) + .await?; + let result = starknet_commands::deploy::deploy( - deploy.class_hash, + deploy_resolved.class_hash, &calldata, - deploy.salt, - deploy.unique, + deploy_resolved.salt, + deploy_resolved.unique, fee_settings, - deploy.nonce, + deploy_resolved.nonce, &account, wait_config, ) diff --git a/crates/sncast/src/response/structs.rs b/crates/sncast/src/response/structs.rs index 33c4e15d52..70d7b7a55c 100644 --- a/crates/sncast/src/response/structs.rs +++ b/crates/sncast/src/response/structs.rs @@ -2,9 +2,10 @@ use super::explorer_link::OutputLink; use crate::helpers::block_explorer; use crate::helpers::block_explorer::LinkProvider; use camino::Utf8PathBuf; -use conversions::padded_felt::PaddedFelt; use conversions::serde::serialize::CairoSerialize; +use conversions::{padded_felt::PaddedFelt, IntoConv}; use indoc::formatdoc; +use itertools::Itertools; use serde::{Deserialize, Serialize, Serializer}; use starknet_types_core::felt::Felt; @@ -135,6 +136,52 @@ pub struct ScriptInitResponse { impl CommandResponse for ScriptInitResponse {} +#[derive(Serialize)] +pub struct DeclareDeployResponse { + class_hash: Option, + declare_transaction_hash: Option, + contract_address: Felt, + deploy_transaction_hash: Felt, +} + +impl DeclareDeployResponse { + #[must_use] + pub fn new(declare: &Option, deploy: &DeployResponse) -> Self { + let (class_hash, declare_transaction_hash) = match declare { + Some(DeclareResponse::Success(DeclareTransactionResponse { + class_hash, + transaction_hash, + })) => { + let class_hash: Felt = (*class_hash).into_(); + let transaction_hash: Felt = (*transaction_hash).into_(); + (Some(class_hash), Some(transaction_hash)) + } + Some(DeclareResponse::AlreadyDeclared(AlreadyDeclaredResponse { class_hash })) => { + let class_hash: Felt = (*class_hash).into_(); + (Some(class_hash), None) + } + None => (None, None), + }; + + let DeployResponse { + contract_address, + transaction_hash: deploy_transaction_hash, + } = deploy; + + let contract_address: Felt = (*contract_address).into_(); + let deploy_transaction_hash: Felt = (*deploy_transaction_hash).into_(); + + Self { + class_hash, + declare_transaction_hash, + contract_address, + deploy_transaction_hash, + } + } +} + +impl CommandResponse for DeclareDeployResponse {} + #[derive(Serialize, CairoSerialize)] pub enum FinalityStatus { Received, @@ -205,6 +252,39 @@ impl OutputLink for DeclareTransactionResponse { } } +impl OutputLink for DeclareDeployResponse { + const TITLE: &'static str = "declaration and deployment"; + + fn format_links(&self, provider: Box) -> String { + let mut links = vec![]; + + if let Some(ref class_hash) = self.class_hash { + let class_hash: PaddedFelt = (*class_hash).into_(); + links.push(format!("class: {}", provider.class(class_hash))); + } + + let contract_address: PaddedFelt = self.contract_address.into_(); + links.push(format!("contract: {}", provider.contract(contract_address))); + + if let Some(ref declare_transaction_hash) = self.declare_transaction_hash { + let declare_transaction_hash: PaddedFelt = (*declare_transaction_hash).into_(); + + links.push(format!( + "declaration transaction: {}", + provider.class(declare_transaction_hash) + )); + } + + let deploy_transaction_hash: PaddedFelt = self.deploy_transaction_hash.into_(); + links.push(format!( + "deployment transaction: {}", + provider.transaction(deploy_transaction_hash) + )); + + links.iter().join("\n") + } +} + impl OutputLink for AccountCreateResponse { const TITLE: &'static str = "account creation"; diff --git a/crates/sncast/src/starknet_commands/declare.rs b/crates/sncast/src/starknet_commands/declare.rs index d25a779d37..0b61fda70f 100644 --- a/crates/sncast/src/starknet_commands/declare.rs +++ b/crates/sncast/src/starknet_commands/declare.rs @@ -1,4 +1,4 @@ -use anyhow::{anyhow, Context, Result}; +use anyhow::{anyhow, Result}; use clap::{Args, ValueEnum}; use conversions::byte_array::ByteArray; use conversions::IntoConv; @@ -6,6 +6,7 @@ use scarb_api::StarknetContractArtifacts; use sncast::helpers::error::token_not_supported_for_declaration; use sncast::helpers::fee::{FeeArgs, FeeSettings, FeeToken, PayableTransaction}; use sncast::helpers::rpc::RpcArgs; +use sncast::helpers::scarb_utils::CompiledContract; use sncast::response::errors::StarknetCommandError; use sncast::response::structs::{ AlreadyDeclaredResponse, DeclareResponse, DeclareTransactionResponse, @@ -17,7 +18,6 @@ use starknet::core::types::{DeclareTransactionResult, StarknetError}; use starknet::providers::ProviderError; use starknet::{ accounts::{Account, SingleOwnerAccount}, - core::types::contract::{CompiledClass, SierraClass}, providers::jsonrpc::{HttpTransport, JsonRpcClient}, signers::LocalWallet, }; @@ -25,6 +25,9 @@ use starknet_types_core::felt::Felt; use std::collections::HashMap; use std::sync::Arc; +use super::declare_deploy::DeclareDeploy; +use super::deploy::DeployArguments; + #[derive(Args)] #[command(about = "Declare a contract to starknet", long_about = None)] pub struct Declare { @@ -62,11 +65,36 @@ impl_payable_transaction!(Declare, token_not_supported_for_declaration, DeclareVersion::V3 => FeeToken::Strk ); +impl From<&DeclareDeploy> for Declare { + fn from(declare_deploy: &DeclareDeploy) -> Self { + let DeclareDeploy { + contract_name, + deploy_args: DeployArguments { package, .. }, + fee_token, + rpc, + } = &declare_deploy; + + let fee_args = FeeArgs { + fee_token: Some(fee_token.to_owned()), + ..Default::default() + }; + + Declare { + contract: contract_name.to_owned(), + fee_args, + nonce: None, + package: package.to_owned(), + version: None, + rpc: rpc.to_owned(), + } + } +} + #[allow(clippy::too_many_lines)] -pub async fn declare( +pub async fn declare_compiled( declare: Declare, account: &SingleOwnerAccount<&JsonRpcClient, LocalWallet>, - artifacts: &HashMap, + contract: CompiledContract, wait_config: WaitForTx, skip_on_already_declared: bool, fee_token: FeeToken, @@ -78,46 +106,27 @@ pub async fn declare( .try_into_fee_settings(account.provider(), account.block_id()) .await?; - let contract_artifacts = - artifacts - .get(&declare.contract) - .ok_or(StarknetCommandError::ContractArtifactsNotFound(ErrorData { - data: ByteArray::from(declare.contract.as_str()), - }))?; - - let contract_definition: SierraClass = serde_json::from_str(&contract_artifacts.sierra) - .context("Failed to parse sierra artifact")?; - let casm_contract_definition: CompiledClass = - serde_json::from_str(&contract_artifacts.casm).context("Failed to parse casm artifact")?; - - let casm_class_hash = casm_contract_definition - .class_hash() - .map_err(anyhow::Error::from)?; + let CompiledContract { + class, + casm_class_hash: class_hash, + sierra_class_hash, + } = contract; - let class_hash = contract_definition - .class_hash() - .map_err(anyhow::Error::from)?; - - let declared = match fee_settings { + let result = match fee_settings { FeeSettings::Eth { max_fee } => { - let declaration = account.declare_v2( - Arc::new(contract_definition.flatten().map_err(anyhow::Error::from)?), - casm_class_hash, - ); + let declaration = account.declare_v2(Arc::new(class), class_hash); let declaration = apply_optional(declaration, max_fee, DeclarationV2::max_fee); let declaration = apply_optional(declaration, declare.nonce, DeclarationV2::nonce); declaration.send().await } + FeeSettings::Strk { max_gas, max_gas_unit_price, } => { - let declaration = account.declare_v3( - Arc::new(contract_definition.flatten().map_err(anyhow::Error::from)?), - casm_class_hash, - ); + let declaration = account.declare_v3(Arc::new(class), class_hash); let declaration = apply_optional(declaration, max_gas, DeclarationV3::gas); let declaration = @@ -128,29 +137,49 @@ pub async fn declare( } }; - match declared { + match result { Ok(DeclareTransactionResult { transaction_hash, class_hash, - }) => handle_wait_for_tx( - account.provider(), - transaction_hash, - DeclareResponse::Success(DeclareTransactionResponse { - class_hash: class_hash.into_(), - transaction_hash: transaction_hash.into_(), - }), - wait_config, - ) + }) => { + handle_wait_for_tx( + account.provider(), + transaction_hash, + DeclareResponse::Success(DeclareTransactionResponse { + class_hash: class_hash.into_(), + transaction_hash: transaction_hash.into_(), + }), + wait_config, + ) + } .await .map_err(StarknetCommandError::from), Err(Provider(ProviderError::StarknetError(StarknetError::ClassAlreadyDeclared))) if skip_on_already_declared => { Ok(DeclareResponse::AlreadyDeclared(AlreadyDeclaredResponse { - class_hash: class_hash.into_(), + class_hash: sierra_class_hash.into_(), })) } Err(Provider(error)) => Err(StarknetCommandError::ProviderError(error.into())), Err(error) => Err(anyhow!(format!("Unexpected error occurred: {error}")).into()), } } + +pub async fn declare( + declare: Declare, + account: &SingleOwnerAccount<&JsonRpcClient, LocalWallet>, + artifacts: &HashMap, + wait_config: WaitForTx, +) -> Result { + let contract_artifacts = artifacts.get(&declare.contract).ok_or_else(|| { + StarknetCommandError::ContractArtifactsNotFound(ErrorData { + data: ByteArray::from(declare.contract.clone().as_str()), + }) + })?; + + let contract = contract_artifacts.try_into()?; + let fee_token = declare.fee_args.clone().fee_token.unwrap_or_default(); + + declare_compiled(declare, account, contract, wait_config, true, fee_token).await +} diff --git a/crates/sncast/src/starknet_commands/declare_deploy.rs b/crates/sncast/src/starknet_commands/declare_deploy.rs new file mode 100644 index 0000000000..b50e6e9dbc --- /dev/null +++ b/crates/sncast/src/starknet_commands/declare_deploy.rs @@ -0,0 +1,21 @@ +use super::deploy::DeployArguments; +use clap::Args; +use sncast::helpers::{fee::FeeToken, rpc::RpcArgs}; + +#[derive(Args)] +#[command(about = "Declare and deploy a contract on Starknet")] +pub struct DeclareDeploy { + // Name of the contract to deploy + #[clap(long)] + pub contract_name: String, + + #[clap(flatten)] + pub deploy_args: DeployArguments, + + /// Token that transaction fee will be paid in + #[clap(long)] + pub fee_token: FeeToken, + + #[clap(flatten)] + pub rpc: RpcArgs, +} diff --git a/crates/sncast/src/starknet_commands/deploy.rs b/crates/sncast/src/starknet_commands/deploy.rs index 8b3bcb4e29..3254aa858d 100644 --- a/crates/sncast/src/starknet_commands/deploy.rs +++ b/crates/sncast/src/starknet_commands/deploy.rs @@ -4,6 +4,7 @@ use conversions::IntoConv; use sncast::helpers::error::token_not_supported_for_deployment; use sncast::helpers::fee::{FeeArgs, FeeSettings, FeeToken, PayableTransaction}; use sncast::helpers::rpc::RpcArgs; +use sncast::helpers::scarb_utils::{read_manifest_and_build_artifacts, CompiledContract}; use sncast::response::errors::StarknetCommandError; use sncast::response::structs::DeployResponse; use sncast::{extract_or_generate_salt, impl_payable_transaction, udc_uniqueness}; @@ -17,16 +18,39 @@ use starknet::providers::JsonRpcClient; use starknet::signers::LocalWallet; use starknet_types_core::felt::Felt; -#[derive(Args)] +use super::declare_deploy::DeclareDeploy; + +#[derive(Args, Clone)] #[command(about = "Deploy a contract on Starknet")] pub struct Deploy { /// Class hash of contract to deploy - #[clap(short = 'g', long)] - pub class_hash: Felt, + #[clap(short = 'g', long, conflicts_with = "contract_name")] + pub class_hash: Option, + + // Name of the contract to deploy + #[clap(long, conflicts_with = "class_hash")] + pub contract_name: Option, #[clap(flatten)] pub arguments: DeployArguments, + #[clap(flatten)] + pub fee_args: FeeArgs, + + #[clap(flatten)] + pub rpc: RpcArgs, +} + +#[derive(Debug, Clone, clap::Args)] +pub struct DeployArguments { + // A package to deploy a contract from + #[clap(long)] + pub package: Option, + + /// Arguments of the called function serialized as a series of felts + #[clap(short, long, value_delimiter = ' ', num_args = 1..)] + pub constructor_calldata: Option>, + /// Salt for the address #[clap(short, long)] pub salt: Option, @@ -35,9 +59,6 @@ pub struct Deploy { #[clap(long)] pub unique: bool, - #[clap(flatten)] - pub fee_args: FeeArgs, - /// Nonce of the transaction. If not provided, nonce will be set automatically #[clap(short, long)] pub nonce: Option, @@ -46,17 +67,6 @@ pub struct Deploy { #[clap(short, long)] pub version: Option, - #[clap(flatten)] - pub rpc: RpcArgs, -} - -#[derive(Debug, Clone, clap::Args)] -#[group(multiple = false)] -pub struct DeployArguments { - /// Arguments of the called function serialized as a series of felts - #[clap(short, long, value_delimiter = ' ', num_args = 1..)] - pub constructor_calldata: Option>, - // Arguments of the called function as a comma-separated string of Cairo expressions #[clap(long)] pub arguments: Option, @@ -68,7 +78,100 @@ pub enum DeployVersion { V3, } -impl_payable_transaction!(Deploy, token_not_supported_for_deployment, +impl From for Deploy { + fn from(declare_deploy: DeclareDeploy) -> Self { + let DeclareDeploy { + contract_name, + deploy_args, + fee_token, + rpc, + } = declare_deploy; + + let fee_args = FeeArgs { + fee_token: Some(fee_token), + ..Default::default() + }; + + Deploy { + class_hash: None, + contract_name: Some(contract_name), + arguments: deploy_args, + fee_args, + rpc, + } + } +} + +impl Deploy { + pub fn build_artifacts_and_get_compiled_contract( + &self, + json: bool, + profile: Option<&String>, + ) -> Result { + let contract_name = self + .contract_name + .clone() + .ok_or_else(|| anyhow!("Contract name and class hash unspecified"))?; + + let artifacts = read_manifest_and_build_artifacts(&self.arguments.package, json, profile)?; + + let contract_artifacts = artifacts + .get(&contract_name) + .ok_or_else(|| anyhow!("No artifacts found for contract: {}", contract_name))?; + + contract_artifacts.try_into() + } + + pub fn resolved_with_class_hash(mut self, value: Felt) -> DeployResolved { + self.class_hash = Some(value); + self.try_into().unwrap() + } +} + +pub struct DeployResolved { + pub class_hash: Felt, + pub constructor_calldata: Vec, + pub salt: Option, + pub unique: bool, + pub fee_args: FeeArgs, + pub nonce: Option, + pub version: Option, +} + +impl TryFrom for DeployResolved { + type Error = anyhow::Error; + + fn try_from(deploy: Deploy) -> Result { + let Deploy { + class_hash, + arguments: + DeployArguments { + constructor_calldata, + salt, + unique, + nonce, + version, + .. + }, + fee_args, + .. + } = deploy; + + let class_hash = class_hash.ok_or_else(|| anyhow!("Class hash unspecified"))?; + + Ok(DeployResolved { + class_hash, + constructor_calldata: constructor_calldata.unwrap_or_default(), + salt, + unique, + fee_args, + nonce, + version, + }) + } +} + +impl_payable_transaction!(DeployResolved, token_not_supported_for_deployment, DeployVersion::V1 => FeeToken::Eth, DeployVersion::V3 => FeeToken::Strk ); diff --git a/crates/sncast/src/starknet_commands/mod.rs b/crates/sncast/src/starknet_commands/mod.rs index 4a96b853bc..d7b669e8dc 100644 --- a/crates/sncast/src/starknet_commands/mod.rs +++ b/crates/sncast/src/starknet_commands/mod.rs @@ -1,6 +1,7 @@ pub mod account; pub mod call; pub mod declare; +pub mod declare_deploy; pub mod deploy; pub mod invoke; pub mod multicall; diff --git a/crates/sncast/src/starknet_commands/script/init.rs b/crates/sncast/src/starknet_commands/script/init.rs index 251bb9c8bd..ba468e51a3 100644 --- a/crates/sncast/src/starknet_commands/script/init.rs +++ b/crates/sncast/src/starknet_commands/script/init.rs @@ -65,8 +65,6 @@ fn init_scarb_project(script_name: &str, script_root_dir: &Utf8PathBuf) -> Resul "--no-vcs", "--quiet", script_root_dir.as_str(), - "--test-runner", - "cairo-test", ]) .env("SCARB_INIT_TEST_RUNNER", "cairo-test") .run() diff --git a/crates/sncast/src/starknet_commands/script/run.rs b/crates/sncast/src/starknet_commands/script/run.rs index d6bb62d27c..0bccb4adeb 100644 --- a/crates/sncast/src/starknet_commands/script/run.rs +++ b/crates/sncast/src/starknet_commands/script/run.rs @@ -117,7 +117,6 @@ impl<'a> ExtensionLogic for CastScriptExtension<'a> { "declare" => { let contract: String = input_reader.read::()?.to_string(); let fee_args: FeeArgs = input_reader.read::()?.into(); - let fee_token = fee_args.fee_token.clone().unwrap_or_default(); let nonce = input_reader.read()?; let declare = Declare { @@ -145,8 +144,6 @@ impl<'a> ExtensionLogic for CastScriptExtension<'a> { wait: true, wait_params: self.config.wait_params, }, - true, - fee_token, )); self.state.maybe_insert_tx_entry( diff --git a/crates/sncast/tests/e2e/declare.rs b/crates/sncast/tests/e2e/declare.rs index f423075c82..0bc6998c43 100644 --- a/crates/sncast/tests/e2e/declare.rs +++ b/crates/sncast/tests/e2e/declare.rs @@ -434,13 +434,17 @@ fn test_scarb_build_fails_when_wrong_cairo_path() { let snapbox = runner(&args).current_dir(tempdir.path()); let output = snapbox.assert().failure(); - assert_stderr_contains( - output, - "Failed to build contract: Failed to build using scarb; `scarb` exited with error", - ); + let expected = indoc! { + " + Error: Failed to build contract + Caused by: + Failed to build using scarb; `scarb` exited with error + " + }; + + assert_stderr_contains(output, expected); } -#[should_panic(expected = "Path to Scarb.toml manifest does not exist")] #[test] fn test_scarb_build_fails_scarb_toml_does_not_exist() { let tempdir = copy_directory_to_tempdir(CONTRACTS_DIR); @@ -460,7 +464,13 @@ fn test_scarb_build_fails_scarb_toml_does_not_exist() { "eth", ]; - runner(&args).current_dir(tempdir.path()).assert().success(); + let snapbox = runner(&args).current_dir(tempdir.path()); + let output = snapbox.assert().failure(); + + assert_stderr_contains( + output, + "Error: Path to Scarb.toml manifest does not exist =[..]", + ); } #[test] @@ -531,7 +541,6 @@ fn test_too_low_max_fee() { ); } -#[should_panic(expected = "Make sure you have enabled sierra code generation in Scarb.toml")] #[test] fn test_scarb_no_sierra_artifact() { let tempdir = copy_directory_to_tempdir(CONTRACTS_DIR.to_string() + "/no_sierra"); @@ -551,7 +560,18 @@ fn test_scarb_no_sierra_artifact() { "eth", ]; - runner(&args).current_dir(tempdir.path()).assert().success(); + let snapbox = runner(&args).current_dir(tempdir.path()); + let output = snapbox.assert().failure(); + + let expected = indoc! { + " + Error: Failed to build contract + Caused by: + [..]Make sure you have enabled sierra code generation in Scarb.toml[..] + " + }; + + assert_stderr_contains(output, expected); } #[test] diff --git a/crates/sncast/tests/e2e/declare_deploy.rs b/crates/sncast/tests/e2e/declare_deploy.rs new file mode 100644 index 0000000000..6e7d2b6b9a --- /dev/null +++ b/crates/sncast/tests/e2e/declare_deploy.rs @@ -0,0 +1,403 @@ +use crate::helpers::{ + constants::{ACCOUNT_FILE_PATH, CONTRACTS_DIR, MAP_CONTRACT_NAME, URL}, + fixtures::{ + copy_directory_to_tempdir, duplicate_contract_directory_with_salt, get_accounts_path, + last_line_as_json, + }, + runner::runner, +}; +use configuration::CONFIG_FILENAME; +use indoc::indoc; +use shared::test_utils::output_assert::{assert_stderr_contains, assert_stdout_contains, AsOutput}; +use snapbox::assert_matches; +use test_case::test_case; + +#[test_case("oz_cairo_0"; "cairo_0_account")] +#[test_case("oz_cairo_1"; "cairo_1_account")] +#[test_case("braavos"; "braavos_account")] +fn test_happy_case_eth(account: &str) { + let salt = account.to_owned() + "_eth"; + let tempdir = + duplicate_contract_directory_with_salt(CONTRACTS_DIR.to_string() + "/map", "put", &salt); + + let accounts_file = &get_accounts_path(ACCOUNT_FILE_PATH)[..]; + + let args = vec![ + "--accounts-file", + accounts_file, + "--account", + account, + "--int-format", + "--json", + "declare-deploy", + "--url", + URL, + "--contract-name", + MAP_CONTRACT_NAME, + "--fee-token", + "eth", + ]; + + let snapbox = runner(&args).current_dir(tempdir.path()); + let output = snapbox.assert().success(); + let json = last_line_as_json(output.as_stdout()); + + assert_eq!(output.as_stderr(), ""); + assert_eq!(json["command"], "declare-deploy"); + assert!(json["class_hash"].as_str().is_some()); + assert!(json["contract_address"].as_str().is_some()); + assert!(json["declare_transaction_hash"].as_str().is_some()); + assert!(json["deploy_transaction_hash"].as_str().is_some()); +} + +#[test_case("oz"; "oz_account")] +#[test_case("argent"; "argent_account")] +fn test_happy_case_strk(account: &str) { + let salt = account.to_owned() + "_strk"; + let tempdir = + duplicate_contract_directory_with_salt(CONTRACTS_DIR.to_string() + "/map", "put", &salt); + + let accounts_file = &get_accounts_path(ACCOUNT_FILE_PATH)[..]; + + let args = vec![ + "--accounts-file", + accounts_file, + "--account", + account, + "--int-format", + "--json", + "declare-deploy", + "--url", + URL, + "--contract-name", + MAP_CONTRACT_NAME, + "--fee-token", + "strk", + ]; + + let snapbox = runner(&args).current_dir(tempdir.path()); + let output = snapbox.assert().success(); + let json = last_line_as_json(output.as_stdout()); + + assert_eq!(output.as_stderr(), ""); + assert_eq!(json["command"], "declare-deploy"); + assert!(json["class_hash"].as_str().is_some()); + assert!(json["contract_address"].as_str().is_some()); + assert!(json["declare_transaction_hash"].as_str().is_some()); + assert!(json["deploy_transaction_hash"].as_str().is_some()); +} + +#[test] +fn test_happy_case_human_readable() { + let tempdir = duplicate_contract_directory_with_salt( + CONTRACTS_DIR.to_string() + "/map", + "put", + "_human_readable", + ); + + let accounts_file = &get_accounts_path(ACCOUNT_FILE_PATH)[..]; + + let args = vec![ + "--accounts-file", + accounts_file, + "--account", + "user17", + "--int-format", + "declare-deploy", + "--url", + URL, + "--contract-name", + MAP_CONTRACT_NAME, + "--fee-token", + "strk", + ]; + + let snapbox = runner(&args).current_dir(tempdir.path()); + let output = snapbox.assert().success(); + + let expected = indoc!( + " + [..] + command: [..] + class_hash: [..] + contract_address: [..] + declare_transaction_hash: [..] + deploy_transaction_hash: [..] + + To see declaration and deployment details, visit: + class: [..] + contract: [..] + declaration transaction: [..] + deployment transaction: [..] + " + ); + + assert_stdout_contains(output, expected); +} + +#[test] +#[ignore = "Expand the contract's code to more complex or wait for fix: https://github.com/xJonathanLEI/starknet-rs/issues/649#issue-2469861847"] +fn test_happy_case_specify_package() { + let tempdir = duplicate_contract_directory_with_salt( + CONTRACTS_DIR.to_string() + "/multiple_packages", + "whatever", + "salty", + ); + + let accounts_file = &get_accounts_path(ACCOUNT_FILE_PATH)[..]; + + let args = vec![ + "--accounts-file", + accounts_file, + "--account", + "user18", + "--int-format", + "--json", + "declare-deploy", + "--url", + URL, + "--contract-name", + "supercomplexcode", + "--salt", + "0x2", + "--unique", + "--fee-token", + "strk", + "--package", + "main_workspace", + ]; + + let snapbox = runner(&args).current_dir(tempdir.path()); + let output = snapbox.assert().success(); + let json = last_line_as_json(output.as_stdout()); + + assert_eq!(output.as_stderr(), ""); + assert_eq!(json["command"], "declare-deploy"); + assert!(json["contract_address"].as_str().is_some()); + assert!(json["deploy_transaction_hash"].as_str().is_some()); +} + +#[test] +fn test_happy_case_contract_already_declared() { + let tempdir = duplicate_contract_directory_with_salt( + CONTRACTS_DIR.to_string() + "/map", + "put", + "happy_case_contract_already_declared_put", + ); + + let accounts_file = &get_accounts_path("tests/data/accounts/accounts.json")[..]; + + let mut args = vec![ + "--accounts-file", + accounts_file, + "--account", + "user2", + "--json", + "declare", + "--url", + URL, + "--contract-name", + MAP_CONTRACT_NAME, + "--fee-token", + "strk", + ]; + + runner(&args).current_dir(tempdir.path()).assert().success(); + + args[5] = "declare-deploy"; + + let snapbox = runner(&args).current_dir(tempdir.path()); + let output = snapbox.assert().success(); + let json = last_line_as_json(output.as_stdout()); + + assert_eq!(output.as_stderr(), ""); + assert_eq!(json["command"], "declare-deploy"); + assert!(json["contract_address"].as_str().is_some()); + assert!(json["deploy_transaction_hash"].as_str().is_some()); +} + +#[test] +fn test_nonexistent_contract() { + let tempdir = copy_directory_to_tempdir(CONTRACTS_DIR.to_string() + "/map"); + let accounts_file = &get_accounts_path("tests/data/accounts/accounts.json")[..]; + + let args = vec![ + "--accounts-file", + accounts_file, + "--account", + "user3", + "declare-deploy", + "--url", + URL, + "--contract-name", + "some_non_existent_name", + "--fee-token", + "strk", + ]; + + let snapbox = runner(&args).current_dir(tempdir.path()); + let output = snapbox.assert().failure(); + + assert_stderr_contains( + output, + "Error: No artifacts found for contract: some_non_existent_name", + ); +} + +#[test] +fn test_multiple_packages() { + let tempdir = duplicate_contract_directory_with_salt( + CONTRACTS_DIR.to_string() + "/multiple_packages", + "whatever", + "multiple_packages", + ); + + let accounts_file = &get_accounts_path(ACCOUNT_FILE_PATH)[..]; + + let args = vec![ + "--accounts-file", + accounts_file, + "--account", + "user4", + "--int-format", + "declare-deploy", + "--url", + URL, + "--contract-name", + "supercomplexcode", + "--salt", + "0x2", + "--fee-token", + "strk", + ]; + + let snapbox = runner(&args).current_dir(tempdir.path()); + let output = snapbox.assert().failure(); + + let expected = "Error: More than one package found in scarb metadata - specify package using --package flag"; + assert_stderr_contains(output, expected); +} + +#[test] +fn test_invalid_nonce() { + let tempdir = duplicate_contract_directory_with_salt( + CONTRACTS_DIR.to_string() + "/map", + "put", + "salty_put", + ); + + let accounts_file = &get_accounts_path(ACCOUNT_FILE_PATH)[..]; + + let args = vec![ + "--accounts-file", + accounts_file, + "--account", + "user5", + "--json", + "declare-deploy", + "--url", + URL, + "--contract-name", + MAP_CONTRACT_NAME, + "--fee-token", + "strk", + "--nonce", + "2137", + ]; + + let snapbox = runner(&args).current_dir(tempdir.path()); + + let output = snapbox.assert().success(); + let output = last_line_as_json(output.as_stderr()); + + assert_eq!(output["command"], "declare-deploy"); + assert_matches( + "[..]Account transaction nonce is invalid[..]", + output["error"].as_str().unwrap(), + ); +} + +#[test] +fn test_no_scarb_toml() { + let tempdir = copy_directory_to_tempdir(CONTRACTS_DIR); + let accounts_file = &get_accounts_path("tests/data/accounts/accounts.json")[..]; + + let args = vec![ + "--accounts-file", + accounts_file, + "--account", + "user6", + "declare-deploy", + "--url", + URL, + "--contract-name", + "some_non_existent_name", + "--fee-token", + "strk", + ]; + + let snapbox = runner(&args).current_dir(tempdir.path()); + let output = snapbox.assert().failure(); + + assert_stderr_contains( + output, + "Error: Path to Scarb.toml manifest does not exist =[..]", + ); +} + +#[test] +fn test_no_scarb_profile() { + let tempdir = duplicate_contract_directory_with_salt( + CONTRACTS_DIR.to_string() + "/map", + "put", + "no_scarb_profile", + ); + + let accounts_file = &get_accounts_path(ACCOUNT_FILE_PATH)[..]; + + std::fs::copy( + "tests/data/files/correct_snfoundry.toml", + tempdir.path().join(CONFIG_FILENAME), + ) + .expect("Failed to copy config file to temp dir"); + let args = vec![ + "--accounts-file", + accounts_file, + "--account", + "user1", + "--profile", + "profile5", + "declare-deploy", + "--url", + URL, + "--contract-name", + MAP_CONTRACT_NAME, + "--fee-token", + "strk", + ]; + + let snapbox = runner(&args).current_dir(tempdir.path()); + let output = snapbox.assert().success(); + + assert_eq!(output.as_stderr(), ""); + + let expected = indoc!( + " + [..] + [WARNING] Profile profile5 does not exist in scarb, using 'release' profile. + command: [..] + class_hash: [..] + contract_address: [..] + declare_transaction_hash: [..] + deploy_transaction_hash: [..] + + To see declaration and deployment details, visit: + class: [..] + contract: [..] + declaration transaction: [..] + deployment transaction: [..] + " + ); + + assert_stdout_contains(output, expected); +} diff --git a/crates/sncast/tests/e2e/mod.rs b/crates/sncast/tests/e2e/mod.rs index 573cc5d827..c9d2e5132f 100644 --- a/crates/sncast/tests/e2e/mod.rs +++ b/crates/sncast/tests/e2e/mod.rs @@ -1,6 +1,7 @@ mod account; mod call; mod declare; +mod declare_deploy; mod deploy; mod invoke; mod main_tests; diff --git a/crates/sncast/tests/helpers/constants.rs b/crates/sncast/tests/helpers/constants.rs index 981c52cec6..c2205aa206 100644 --- a/crates/sncast/tests/helpers/constants.rs +++ b/crates/sncast/tests/helpers/constants.rs @@ -39,3 +39,5 @@ pub const CONSTRUCTOR_WITH_PARAMS_CONTRACT_CLASS_HASH_SEPOLIA: &str = pub const DATA_TRANSFORMER_CONTRACT_ADDRESS_SEPOLIA: &str = "0x016ad425af4585102e139d4fb2c76ce786d1aaa1cfcd88a51f3ed66601b23cdd"; + +pub const MAP_CONTRACT_NAME: &str = "Map"; diff --git a/crates/sncast/tests/helpers/fixtures.rs b/crates/sncast/tests/helpers/fixtures.rs index 986b0272e0..4eee360b37 100644 --- a/crates/sncast/tests/helpers/fixtures.rs +++ b/crates/sncast/tests/helpers/fixtures.rs @@ -635,3 +635,11 @@ pub fn join_tempdirs(from: &TempDir, to: &TempDir) { ) .expect("Failed to copy the directory"); } + +#[must_use] +pub fn last_line_as_json(output: &str) -> Value { + let last_line = output.trim_end_matches('\n').split('\n').last().unwrap(); + + serde_json::from_str::(last_line) + .unwrap_or_else(|_| panic!("Failed to deserialize output as JSON:\n{last_line}")) +} diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index f91ced61be..2ea2108ae4 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -47,6 +47,7 @@ * [Importing Accounts](starknet/account-import.md) * [Declaring New Contracts](starknet/declare.md) * [Deploying New Contracts](starknet/deploy.md) +* [Declaring and Deploying at Once](starknet/declare-deploy.md) * [Invoking Contracts](starknet/invoke.md) * [Calling Contracts](starknet/call.md) * [Performing Multicall](starknet/multicall.md) @@ -120,6 +121,7 @@ * [list](appendix/sncast/account/list.md) * [declare](appendix/sncast/declare.md) * [deploy](appendix/sncast/deploy.md) + * [declare-deploy](appendix/sncast/declare-deploy.md) * [invoke](appendix/sncast/invoke.md) * [call](appendix/sncast/call.md) * [multicall](appendix/sncast/multicall/multicall.md) diff --git a/docs/src/appendix/sncast.md b/docs/src/appendix/sncast.md index fdd658795f..69dbad73be 100644 --- a/docs/src/appendix/sncast.md +++ b/docs/src/appendix/sncast.md @@ -8,6 +8,7 @@ * [delete](./sncast/account/delete.md) * [declare](./sncast/declare.md) * [deploy](./sncast/deploy.md) +* [declare-deploy](./sncast/declare-deploy.md) * [invoke](./sncast/invoke.md) * [call](./sncast/call.md) * [multicall](./sncast/multicall/multicall.md) diff --git a/docs/src/appendix/sncast/declare-deploy.md b/docs/src/appendix/sncast/declare-deploy.md new file mode 100644 index 0000000000..83ae9b9949 --- /dev/null +++ b/docs/src/appendix/sncast/declare-deploy.md @@ -0,0 +1,48 @@ +# `declare-deploy` +Declare a contract and deploy it to Starknet immediately. If the contract has been already declared, behaves exactly as `deploy`. + +> ⚠️ **Warning** +> This command relies on auto-estimation and does not allow specifying max fees explicitly. +> ⚠️ **Warning** +> Only a `fee-token` can be specified. Transaction versions for both declaration and deployment are inferred from token type. + +## Required Common Arguments — Passed By CLI or Specified in `snfoundry.toml` + +* [`account`](./common.md#--account--a-account_name) + +## `--contract-name, -c ` +Required. + +Name of the contract. Contract name is a part after the `mod` keyword in your contract file. + +## `--fee-token ` +Optional. Required if `--version` is not provided. + +Token used for fee payment. Possible values: ETH, STRK. + +## `--package ` +Optional. + +Name of the package that should be used. + +If supplied, a contract from this package will be used. Required if more than one package exists in a workspace. + +## `--constructor-calldata, -c ` +Optional. + +Calldata for the contract constructor. + +## `--salt, -s ` +Optional. + +Salt for the contract address. + +## `--unique, -u` +Optional. + +If passed, the salt will be additionally modified with an account address. + +## `--nonce, -n ` +Optional. + +Nonce for transaction. If not provided, nonce will be set automatically. \ No newline at end of file diff --git a/docs/src/appendix/sncast/declare.md b/docs/src/appendix/sncast/declare.md index 28247ee49b..badd823104 100644 --- a/docs/src/appendix/sncast/declare.md +++ b/docs/src/appendix/sncast/declare.md @@ -8,7 +8,7 @@ Send a declare transaction of Cairo contract to Starknet. ## `--contract-name, -c ` Required. -Name of the contract. Contract name is a part after the mod keyword in your contract file. +Name of the contract. Contract name is a part after the `mod` keyword in your contract file. ## `--url, -u ` Optional. diff --git a/docs/src/appendix/sncast/deploy.md b/docs/src/appendix/sncast/deploy.md index f5a611cdb7..6db87b5847 100644 --- a/docs/src/appendix/sncast/deploy.md +++ b/docs/src/appendix/sncast/deploy.md @@ -6,9 +6,26 @@ Deploy a contract to Starknet. * [`account`](./common.md#--account--a-account_name) ## `--class-hash, -g ` -Required. +Optional (either this or the `--contract-name` must be specified). Class hash of contract to deploy. +Cannot be used together with `--contract-name`. + +# `--contract-name ` +Optional (either this or the `--class-hash` must be specified). + +Name of contract to deploy. +Cannot be used together with `--class-hash`. + +## `--package ` +Optional. + +Name of the package that should be used. + +If supplied, a contract from this package will be used. +Required if more than one package exists in a workspace. +Can only be used along with a `--contract-name`. +Cannot be used together with `--class-hash`. ## `--url, -u ` Optional. diff --git a/docs/src/starknet/declare-deploy.md b/docs/src/starknet/declare-deploy.md new file mode 100644 index 0000000000..5ff9e7742d --- /dev/null +++ b/docs/src/starknet/declare-deploy.md @@ -0,0 +1,33 @@ +# Deploying an Undeclared Contract + +`sncast` allows declaring and deploying contracts through dedicated commands. +The `declare-deploy` command simplifies this pipeline by performing these two actions at the same time. It is used to declare a contract and deploy it immediately. If the contract has been already declared, the command will behave exactly as `deploy`. + +For detailed description, see [declare-deploy command reference](../appendix/sncast/declare-deploy.md). + +## Examples + +Command operates under all the circumstances [`declare`](./declare.md) and [`deploy`](./deploy.md) would do when invoked separately. + +### General Example + +Make sure you have a `Scarb.toml` in your project directory. Suppose we would like to declare and instantly deploy an example contract named `HelloSncast`, defined in the default project. + +Running: + + + +```shell +$ sncast --account myuser \ + --url http://127.0.0.1:5050/rpc \ + declare-deploy \ + --fee-token strk \ + --contract-name HelloSncast +``` + +results in declaration and deployment. + +> ⚠️ **Warning** +> This command relies on auto-estimation and does not allow specifying max fees explicitly. +> ⚠️ **Warning** +> Only a `fee-token` can be specified. Transaction versions for both declaration and deployment are inferred from token type. \ No newline at end of file diff --git a/docs/src/starknet/deploy.md b/docs/src/starknet/deploy.md index 8d49f34b2e..9e25cffe78 100644 --- a/docs/src/starknet/deploy.md +++ b/docs/src/starknet/deploy.md @@ -40,6 +40,28 @@ transaction: https://sepolia.starkscan.co/tx/[..] > 💡 **Info** > Max fee will be automatically computed if `--max-fee ` is not passed. +### Deploying contract by name + +A contract can also be deployed by specifying its name as a `--contract-name` argument, instead of a class hash. + + + +```shell +$ sncast deploy \ + --contract-name SimpleBalance \ + --package SimplePackage +``` + +
+Output: + +```shell +command: deploy +contract_address: 0x301316d47a81b39c5e27cca4a7b8ca4773edbf1103218588d6da4d3ed5303aa +transaction_hash: 0x64a62a000240e034d1862c2bbfa154aac6a8195b4b2e570f38bf4fd47a5ab1e +``` +
+
### Deploying Contract With Constructor