Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `sncast get nonce` command to get the nonce of a contract.
- `sncast get tx` command to get transaction details by hash.
- `sncast utils selector` command to calculate entrypoint selector (sn_keccak) from function name.
- `sncast get class-hash-at` command to get the class hash of a contract at a given address.

### Forge

Expand Down
30 changes: 30 additions & 0 deletions crates/sncast/src/response/class_hash_at.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
use crate::helpers::block_explorer::LinkProvider;
use crate::response::cast_message::SncastCommandMessage;
use crate::response::explorer_link::OutputLink;
use conversions::padded_felt::PaddedFelt;
use conversions::string::IntoPaddedHexStr;
use foundry_ui::styling;
use serde::Serialize;

#[derive(Serialize, Clone)]
pub struct ClassHashAtResponse {
pub class_hash: PaddedFelt,
}

impl SncastCommandMessage for ClassHashAtResponse {
fn text(&self) -> String {
styling::OutputBuilder::new()
.success_message("Class hash retrieved")
.blank_line()
.field("Class Hash", &self.class_hash.into_padded_hex_str())
.build()
}
}

impl OutputLink for ClassHashAtResponse {
const TITLE: &'static str = "class";

fn format_links(&self, provider: Box<dyn LinkProvider>) -> String {
format!("class: {}", provider.class(self.class_hash))
}
}
1 change: 1 addition & 0 deletions crates/sncast/src/response/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ pub mod account;
pub mod balance;
pub mod call;
pub mod cast_message;
pub mod class_hash_at;
pub mod completions;
pub mod declare;
pub mod deploy;
Expand Down
59 changes: 59 additions & 0 deletions crates/sncast/src/starknet_commands/get/class_hash_at.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
use anyhow::Result;
use clap::Args;
use conversions::IntoConv;
use sncast::get_block_id;
use sncast::helpers::command::process_command_result;
use sncast::helpers::configuration::CastConfig;
use sncast::helpers::rpc::RpcArgs;
use sncast::response::class_hash_at::ClassHashAtResponse;
use sncast::response::errors::{SNCastProviderError, StarknetCommandError};
use sncast::response::explorer_link::block_explorer_link_if_allowed;
use sncast::response::ui::UI;
use starknet_rust::providers::jsonrpc::HttpTransport;
use starknet_rust::providers::{JsonRpcClient, Provider};
use starknet_types_core::felt::Felt;

#[derive(Debug, Args)]
#[command(about = "Get the class hash of a contract deployed at a given address")]
pub struct ClassHashAt {
/// Address of the contract
pub contract_address: Felt,

/// Block identifier on which class hash should be fetched.
/// Possible values: `pre_confirmed`, `latest`, block hash (0x prefixed string)
/// and block number (u64)
#[arg(short, long, default_value = "pre_confirmed")]
pub block_id: String,

#[command(flatten)]
pub rpc: RpcArgs,
}

pub async fn class_hash_at(args: ClassHashAt, config: CastConfig, ui: &UI) -> anyhow::Result<()> {
let provider = args.rpc.get_provider(&config, ui).await?;

let result = get_class_hash_at(&provider, args.contract_address, &args.block_id).await;

let chain_id = provider.chain_id().await?;
let block_explorer_link = block_explorer_link_if_allowed(&result, chain_id, &config).await;

process_command_result("get class-hash-at", result, ui, block_explorer_link);
Ok(())
}

async fn get_class_hash_at(
provider: &JsonRpcClient<HttpTransport>,
contract_address: Felt,
block_id: &str,
) -> Result<ClassHashAtResponse> {
let block_id = get_block_id(block_id)?;

let class_hash = provider
.get_class_hash_at(block_id, contract_address)
.await
.map_err(|err| StarknetCommandError::ProviderError(SNCastProviderError::from(err)))?;

Ok(ClassHashAtResponse {
class_hash: class_hash.into_(),
})
}
8 changes: 8 additions & 0 deletions crates/sncast/src/starknet_commands/get/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use sncast::helpers::configuration::CastConfig;
use sncast::response::ui::UI;

pub mod balance;
pub mod class_hash_at;
pub mod nonce;
pub mod transaction;
pub mod tx_status;
Expand All @@ -27,6 +28,9 @@ pub enum GetCommands {

/// Get nonce of a contract
Nonce(nonce::Nonce),

/// Get class hash of a contract at a given address
ClassHashAt(class_hash_at::ClassHashAt),
}

pub async fn get(get: Get, config: CastConfig, ui: &UI) -> anyhow::Result<()> {
Expand All @@ -38,6 +42,10 @@ pub async fn get(get: Get, config: CastConfig, ui: &UI) -> anyhow::Result<()> {
GetCommands::Balance(balance) => balance::balance(balance, config, ui).await?,

GetCommands::Nonce(nonce) => nonce::nonce(nonce, config, ui).await?,

GetCommands::ClassHashAt(args) => {
class_hash_at::class_hash_at(args, config, ui).await?;
}
}

Ok(())
Expand Down
108 changes: 108 additions & 0 deletions crates/sncast/tests/e2e/class_hash_at.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
use crate::helpers::constants::{
DEVNET_PREDEPLOYED_ACCOUNT_ADDRESS, MAP_CONTRACT_ADDRESS_SEPOLIA, URL,
};
use crate::helpers::runner::runner;
use indoc::indoc;
use shared::test_utils::output_assert::assert_stderr_contains;

#[tokio::test]
async fn test_happy_case() {
let args = vec![
"get",
"class-hash-at",
MAP_CONTRACT_ADDRESS_SEPOLIA,
"--url",
URL,
];
let snapbox = runner(&args).env("SNCAST_FORCE_SHOW_EXPLORER_LINKS", "1");

snapbox.assert().success().stdout_eq(indoc! {r"
Success: Class hash retrieved

Class Hash: 0x02a09379665a749e609b4a8459c86fe954566a6beeaddd0950e43f6c700ed321

To see class details, visit:
class: https://sepolia.voyager.online/class/0x02a09379665a749e609b4a8459c86fe954566a6beeaddd0950e43f6c700ed321
"});
}

#[tokio::test]
async fn test_with_block_id() {
let args = vec![
"get",
"class-hash-at",
MAP_CONTRACT_ADDRESS_SEPOLIA,
"--block-id",
"latest",
"--url",
URL,
];
let snapbox = runner(&args);

snapbox.assert().success().stdout_eq(indoc! {r"
Success: Class hash retrieved

Class Hash: 0x02a09379665a749e609b4a8459c86fe954566a6beeaddd0950e43f6c700ed321
"});
}

#[tokio::test]
async fn test_json_output() {
let args = vec![
"--json",
"get",
"class-hash-at",
MAP_CONTRACT_ADDRESS_SEPOLIA,
"--url",
URL,
];
let snapbox = runner(&args);
let output = snapbox.assert().success();
let stdout = output.get_output().stdout.clone();

let json: serde_json::Value = serde_json::from_slice(&stdout).unwrap();
assert_eq!(json["command"], "get class-hash-at");
assert_eq!(json["type"], "response");
assert_eq!(
json["class_hash"],
"0x02a09379665a749e609b4a8459c86fe954566a6beeaddd0950e43f6c700ed321"
);
}

#[tokio::test]
async fn test_nonexistent_contract_address() {
let args = vec!["get", "class-hash-at", "0x0", "--url", URL];
let snapbox = runner(&args);
let output = snapbox.assert().success();

assert_stderr_contains(
output,
indoc! {r"
Command: get class-hash-at
Error: There is no contract at the specified address
"},
);
}

#[tokio::test]
async fn test_invalid_block_id() {
let args = vec![
"get",
"class-hash-at",
DEVNET_PREDEPLOYED_ACCOUNT_ADDRESS,
"--block-id",
"invalid_block",
"--url",
URL,
];
let snapbox = runner(&args);
let output = snapbox.assert().success();

assert_stderr_contains(
output,
indoc! {r"
Command: get class-hash-at
Error: Incorrect value passed for block_id = invalid_block. Possible values are `pre_confirmed`, `latest`, block hash (hex) and block number (u64)
"},
);
}
1 change: 1 addition & 0 deletions crates/sncast/tests/e2e/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ mod account;
pub mod balance;
mod call;
mod class_hash;
mod class_hash_at;
mod completions;
mod declare;
mod declare_from;
Expand Down
1 change: 1 addition & 0 deletions docs/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@
* [completions](appendix/sncast/completions.md)
* [get](appendix/sncast/get/get.md)
* [balance](appendix/sncast/get/balance.md)
* [class-hash-at](appendix/sncast/get/class_hash_at.md)
* [nonce](appendix/sncast/get/nonce.md)
* [tx](appendix/sncast/get/tx.md)
* [tx-status](appendix/sncast/get/tx-status.md)
Expand Down
1 change: 1 addition & 0 deletions docs/src/appendix/sncast.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
* [show-config](./sncast/show_config.md)
* [get](./sncast/get/get.md)
* [balance](./sncast/get/balance.md)
* [class-hash-at](./sncast/get/class_hash_at.md)
* [nonce](./sncast/get/nonce.md)
* [tx](./sncast/get/tx.md)
* [tx-status](./sncast/get/tx-status.md)
Expand Down
35 changes: 35 additions & 0 deletions docs/src/appendix/sncast/get/class_hash_at.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# `get class-hash-at`

Get the class hash of a contract deployed at a given address.

## `<CONTRACT_ADDRESS>`

Required.

Address of the contract in hex (prefixed with '0x') or decimal representation.

## `--block-id, -b <BLOCK_ID>`

Optional.

Block identifier on which class hash should be fetched.
Possible values: `pre_confirmed`, `latest`, block hash (0x prefixed string), and block number (u64).
`pre_confirmed` is used as a default value.

## `--url, -u <RPC_URL>`

Optional.

Starknet RPC node url address.

Overrides url from `snfoundry.toml`.

## `--network <NETWORK>`

Optional.

Use predefined network with public provider

Possible values: `mainnet`, `sepolia`, `devnet`.

Overrides network from `snfoundry.toml`.
1 change: 1 addition & 0 deletions docs/src/appendix/sncast/get/get.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ Commands for querying Starknet state.

It has the following subcommands:
* [`balance`](./balance.md)
* [`class-hash-at`](./class_hash_at.md)
* [`nonce`](./nonce.md)
* [`tx`](./tx.md)
* [`tx-status`](./tx-status.md)
Loading