Skip to content
Open
10 changes: 8 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Cast

#### Added

- `--no-abi` flag for `declare`, `declare-from`, and `deploy`, erasing the ABI before class declaration.

## [0.60.0] - 2026-04-27

### Forge
Expand Down Expand Up @@ -280,7 +286,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Cast

#### Added
#### Added

- Possibility to configure urls of predefined networks used by `--network` flag via `sncast` profile in `snfoundry.toml`

Expand Down Expand Up @@ -324,7 +330,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `sncast declare` command now outputs a ready-to-use deployment command after successful declaration.
- Possibility to use [`starknet-devnet`](https://github.com/0xSpaceShard/starknet-devnet) predeployed accounts directly in `sncast` without needing to import them. They are available under specific names - `devnet-1`, `devnet-2`, ..., `devnet-<N>`. Read more [here](https://foundry-rs.github.io/starknet-foundry/starknet/integration_with_devnet.html#predeployed-accounts)
- Support for `--network devnet` flag that attempts to auto-detect running `starknet-devnet` instance and connect to it.
- Support for automatically declaring the contract when running `sncast deploy`, by providing `--contract-name` flag instead of `--class-hash`.
- Support for automatically declaring the contract when running `sncast deploy`, by providing `--contract-name` flag instead of `--class-hash`.
- `sncast balance` command to fetch the balance of an account for a specified token.

#### Fixed
Expand Down
96 changes: 71 additions & 25 deletions crates/sncast/src/main.rs
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When --no-abi is used, the declared class hash points to a class with an empty ABI. However, the generated deploy suggestion can still include --arguments, which requires sncast to fetch the ABI from that class hash in order to transform Cairo-like arguments into calldata. That suggested command will therefore fail or be misleading for classes with constructor args.

e.g. (constructor params x: felt252, y: felt252):

➜  no_abi_testing git:(main) ✗ ../target/debug/sncast declare  --contract-name=HelloStarknet --no-abi --network sepolia
   Compiling no_abi_testing v0.1.0 (/Users/franciszekjob/Projects/SWM/starknet-foundry/.worktrees/declare-no-abi/no_abi_testing/Scarb.toml)
    Finished `release` profile target(s) in 0 seconds
Success: Declaration completed

Class Hash:       0x1ef0773957778f0060d0c276745679e144227d0343f6c91cd0f8a3f7713fda8
Transaction Hash: 0x1e7217a6799be480aff1384062c1bf67d39c11721c14871b8cc52643c90a053

To see declaration details, visit:
class: https://sepolia.voyager.online/class/0x01ef0773957778f0060d0c276745679e144227d0343f6c91cd0f8a3f7713fda8
transaction: https://sepolia.voyager.online/tx/0x01e7217a6799be480aff1384062c1bf67d39c11721c14871b8cc52643c90a053

To deploy a contract of this class, replace the placeholders in `--arguments` with your actual values, then run:
sncast deploy --class-hash 0x1ef0773957778f0060d0c276745679e144227d0343f6c91cd0f8a3f7713fda8 --arguments '<x: felt252>, <y: felt252>' --network sepolia

➜  no_abi_testing git:(main) ✗ ../target/debug/sncast deploy --class-hash 0x1ef0773957778f0060d0c276745679e144227d0343f6c91cd0f8a3f7713fda8 --arguments '10, 20' --url http://188.34.188.184:7070/rpc/v0_10
Error: Error while processing Cairo-like calldata

Caused by:
    Invalid number of arguments: passed 2, expected 0

I think we should either omit the --arguments hint when --no-abi is passed, or preferably switch the suggestion to raw --constructor-calldata placeholders, since raw calldata does not depend on the ABI.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar problem occurs when --no-abi is used with deploy --contract-name ... --arguments ...

e.g.

➜  no_abi_testing git:(main) ✗ ../target/debug/sncast --no-abi  --contract-name HelloStarknet --arguments '10, 20, 30' --network sepolia
   Compiling no_abi_testing v0.1.0 (/Users/franciszekjob/Projects/SWM/starknet-foundry/.worktrees/declare-no-abi/no_abi_testing/Scarb.toml)
    Finished `release` profile target(s) in 0 seconds
Error: Error while processing Cairo-like calldata

Caused by:
    Invalid number of arguments: passed 3, expected 0

Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use std::borrow::Borrow;
use std::num::{NonZeroU8, NonZeroU16};
use std::str::FromStr;

use crate::starknet_commands::declare::declare;
use crate::starknet_commands::declare::declare_with_artifacts;
use crate::starknet_commands::declare_from::{ContractSource, DeclareFrom};
use crate::starknet_commands::deploy::{DeployArguments, DeployCommonArgs};
use crate::starknet_commands::get::Get;
Expand Down Expand Up @@ -49,7 +50,7 @@ use sncast::{
use starknet_commands::ledger::{self, Ledger};
use starknet_commands::verify::Verify;
use starknet_rust::core::types::ContractClass;
use starknet_rust::core::types::contract::{AbiEntry, SierraClass};
use starknet_rust::core::types::contract::{AbiEntry, CompiledClass, SierraClass};
use starknet_rust::core::utils::get_selector_from_name;
use starknet_rust::providers::Provider;
use starknet_types_core::felt::Felt;
Expand Down Expand Up @@ -240,24 +241,34 @@ pub struct Arguments {
pub arguments: Option<String>,
}

enum AbiOrContractClass<C> {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove this enum and add a function similar to:

fn abi_from_contract_class(contract_class: &ContractClass) -> Result<Vec<AbiEntry>> {
    let ContractClass::Sierra(sierra_class) = contract_class else {
        bail!("Transformation of arguments is not available for Cairo Zero contracts")
    };

    serde_json::from_str(sierra_class.abi.as_str())
        .context("Couldn't deserialize ABI received from network")
}

then replace all occurrences of AbiOrContractClass::ContractClass with a call to this function

Abi(Vec<AbiEntry>),
ContractClass(C),
}

impl Arguments {
fn try_into_calldata(
self,
contract_class: &ContractClass,
abi_or_contract_class: AbiOrContractClass<impl Borrow<ContractClass>>,
selector: &Felt,
) -> Result<Vec<Felt>> {
if let Some(calldata) = self.calldata {
calldata_to_felts(&calldata)
} else {
let ContractClass::Sierra(sierra_class) = contract_class else {
bail!("Transformation of arguments is not available for Cairo Zero contracts")
};
return calldata_to_felts(&calldata);
}

let abi: Vec<AbiEntry> = serde_json::from_str(sierra_class.abi.as_str())
.context("Couldn't deserialize ABI received from network")?;
let abi = match abi_or_contract_class {
AbiOrContractClass::Abi(abi) => abi,
AbiOrContractClass::ContractClass(contract_class) => {
let ContractClass::Sierra(sierra_class) = contract_class.borrow() else {
bail!("Transformation of arguments is not available for Cairo Zero contracts")
};

transform(&self.arguments.unwrap_or_default(), &abi, selector)
}
serde_json::from_str::<Vec<_>>(sierra_class.abi.as_str())
.context("Couldn't deserialize ABI received from network")?
}
};

transform(&self.arguments.unwrap_or_default(), &abi, selector)
}
}

Expand Down Expand Up @@ -363,6 +374,7 @@ async fn run_async_command(cli: Cli, config: CastConfig, ui: &UI) -> Result<Exit
declare.common.fee_args,
declare.common.dry_run_args,
declare.common.nonce,
declare.no_abi,
account,
&artifacts,
wait_config,
Expand Down Expand Up @@ -399,6 +411,7 @@ async fn run_async_command(cli: Cli, config: CastConfig, ui: &UI) -> Result<Exit
let network_flag = generate_network_flag(&rpc, &config);
Some(DeployCommandMessage::new(
&contract_definition.abi,
declare.no_abi,
response,
&config.account,
&config.accounts_file,
Expand Down Expand Up @@ -441,6 +454,7 @@ async fn run_async_command(cli: Cli, config: CastConfig, ui: &UI) -> Result<Exit
let result = with_account!(&account, |account| {
starknet_commands::declare_from::declare_from(
contract_source,
declare_from.no_abi,
&declare_from.common,
account,
wait_config,
Expand Down Expand Up @@ -474,6 +488,10 @@ async fn run_async_command(cli: Cli, config: CastConfig, ui: &UI) -> Result<Exit
}

Commands::Deploy(deploy) => {
if deploy.common.contract_identifier.contract_name.is_none() && deploy.no_abi {
bail!("`--no-abi` can only be used with `--contract-name`");
}

let Deploy {
common:
DeployCommonArgs {
Expand All @@ -486,15 +504,18 @@ async fn run_async_command(cli: Cli, config: CastConfig, ui: &UI) -> Result<Exit
dry_run_args,
rpc,
mut nonce,
no_abi,
..
} = deploy;

let provider = rpc.get_provider(&config, ui).await?;

let account = get_account(&config, &provider, &rpc, ui).await?;

let (class_hash, declare_response) = if let Some(class_hash) = identifier.class_hash {
(class_hash, None)
let (class_hash, declare_response, local_abi) = if let Some(class_hash) =
identifier.class_hash
{
(class_hash, None, None)
} else if let Some(contract_name) = identifier.contract_name {
let manifest_path = assert_manifest_path_exists()?;
let package_metadata = get_package_metadata(&manifest_path, &package)?;
Expand All @@ -511,14 +532,26 @@ async fn run_async_command(cli: Cli, config: CastConfig, ui: &UI) -> Result<Exit
)
.expect("Failed to build contract");

let contract_artifacts = artifacts
.get(&contract_name)
.expect("Failed to get contract artifacts");
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 local_abi = contract_definition.abi.clone();

let declare_result = with_account!(&account, |account| {
declare(
contract_name,
fee_args,
dry_run_args,
declare_with_artifacts(
contract_definition,
casm_contract_definition,
&fee_args,
&dry_run_args,
nonce,
no_abi,
account,
&artifacts,
WaitForTx {
wait: true,
wait_params: wait_config.wait_params,
Expand All @@ -537,10 +570,11 @@ async fn run_async_command(cli: Cli, config: CastConfig, ui: &UI) -> Result<Exit
match declare_result {
Ok(DeclareResponse::AlreadyDeclared(AlreadyDeclaredResponse {
class_hash,
})) => (class_hash.into_(), None),
})) => (class_hash.into_(), None, Some(local_abi)),
Ok(DeclareResponse::Success(declare_transaction_response)) => (
declare_transaction_response.class_hash.into_(),
Some(declare_transaction_response),
Some(local_abi),
),
Ok(DeclareResponse::DryRun(_)) => {
unreachable!(
Expand All @@ -565,10 +599,16 @@ async fn run_async_command(cli: Cli, config: CastConfig, ui: &UI) -> Result<Exit
// safe to unwrap because "constructor" is a standardized name
let selector = get_selector_from_name("constructor").unwrap();

let contract_class = get_contract_class(class_hash, &provider).await?;

let arguments: Arguments = arguments.into();
let calldata = arguments.try_into_calldata(&contract_class, &selector)?;
let calldata = arguments.try_into_calldata(
match local_abi {
Some(abi) => AbiOrContractClass::Abi(abi),
None => AbiOrContractClass::ContractClass(
get_contract_class(class_hash, &provider).await?,
),
},
&selector,
)?;

let result = with_account!(&account, |account| {
starknet_commands::deploy::deploy(
Expand Down Expand Up @@ -624,7 +664,10 @@ async fn run_async_command(cli: Cli, config: CastConfig, ui: &UI) -> Result<Exit
let selector = get_selector_from_name(&function)
.context("Failed to convert entry point selector to FieldElement")?;

let calldata = arguments.try_into_calldata(&contract_class, &selector)?;
let calldata = arguments.try_into_calldata(
AbiOrContractClass::ContractClass(&contract_class),
&selector,
)?;

let result = starknet_commands::call::call(
contract_address,
Expand Down Expand Up @@ -677,7 +720,10 @@ async fn run_async_command(cli: Cli, config: CastConfig, ui: &UI) -> Result<Exit
let class_hash = get_class_hash_by_address(&provider, contract_address).await?;
let contract_class = get_contract_class(class_hash, &provider).await?;

let calldata = arguments.try_into_calldata(&contract_class, &selector)?;
let calldata = arguments.try_into_calldata(
AbiOrContractClass::ContractClass(&contract_class),
&selector,
)?;

let result = with_account!(&account, |account| {
starknet_commands::invoke::invoke(
Expand Down
Loading
Loading