Skip to content

Commit 7778248

Browse files
Implement declare-from (#3727)
<!-- Reference any GitHub issues resolved by this PR --> Towards #3587 **Stack**: - #3729 - #3727 ⬅ - #3726 ## Introduced changes Add `sncast declare-from` command ## Checklist <!-- Make sure all of these are complete --> - [x] Linked relevant issue - [ ] Updated relevant documentation - [x] Added relevant tests - [x] Performed self-review of the code - [ ] Added changes to `CHANGELOG.md`
1 parent 1fe6776 commit 7778248

File tree

7 files changed

+423
-0
lines changed

7 files changed

+423
-0
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/sncast/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ dialoguer.workspace = true
5757
toml_edit.workspace = true
5858
num-traits.workspace = true
5959
foundry-ui = { path = "../foundry-ui" }
60+
universal-sierra-compiler-api = { path = "../universal-sierra-compiler-api" }
6061

6162
[dev-dependencies]
6263
ctor.workspace = true

crates/sncast/src/main.rs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use crate::starknet_commands::declare_from::DeclareFrom;
12
use crate::starknet_commands::deploy::DeployArguments;
23
use crate::starknet_commands::multicall;
34
use crate::starknet_commands::script::run_script_command;
@@ -118,6 +119,9 @@ enum Commands {
118119
/// Declare a contract
119120
Declare(Declare),
120121

122+
/// Declare a contract by fetching it from a different Starknet instance
123+
DeclareFrom(DeclareFrom),
124+
121125
/// Deploy a contract
122126
Deploy(Deploy),
123127

@@ -284,6 +288,48 @@ async fn run_async_command(cli: Cli, config: CastConfig, ui: &UI) -> Result<()>
284288
Ok(())
285289
}
286290

291+
Commands::DeclareFrom(declare_from) => {
292+
let provider = declare_from.rpc.get_provider(&config, ui).await?;
293+
let rpc_args = declare_from.rpc.clone();
294+
let source_provider = declare_from.source_rpc.get_provider(ui).await?;
295+
let account = get_account(
296+
&config.account,
297+
&config.accounts_file,
298+
&provider,
299+
config.keystore.as_ref(),
300+
)
301+
.await?;
302+
303+
let result = starknet_commands::declare_from::declare_from(
304+
declare_from,
305+
&account,
306+
wait_config,
307+
false,
308+
&source_provider,
309+
ui,
310+
)
311+
.await
312+
.map_err(handle_starknet_command_error)
313+
.map(|result| match result {
314+
DeclareResponse::Success(declare_transaction_response) => {
315+
declare_transaction_response
316+
}
317+
DeclareResponse::AlreadyDeclared(_) => {
318+
unreachable!("Argument `skip_on_already_declared` is false")
319+
}
320+
});
321+
322+
let block_explorer_link = block_explorer_link_if_allowed(
323+
&result,
324+
provider.chain_id().await?,
325+
&rpc_args,
326+
&config,
327+
);
328+
process_command_result("declare-from", result, ui, block_explorer_link);
329+
330+
Ok(())
331+
}
332+
287333
Commands::Deploy(deploy) => {
288334
let Deploy {
289335
arguments,
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
use crate::starknet_commands::declare::declare_with_artifacts;
2+
use anyhow::{Context, Result};
3+
use clap::Args;
4+
use foundry_ui::UI;
5+
use shared::verify_and_warn_if_incompatible_rpc_version;
6+
use sncast::helpers::fee::FeeArgs;
7+
use sncast::helpers::rpc::{FreeProvider, RpcArgs};
8+
use sncast::response::declare::DeclareResponse;
9+
use sncast::response::errors::{SNCastProviderError, StarknetCommandError};
10+
use sncast::{Network, WaitForTx, get_block_id, get_provider};
11+
use starknet::accounts::SingleOwnerAccount;
12+
use starknet::core::types::contract::{AbiEntry, CompiledClass, SierraClass, SierraClassDebugInfo};
13+
use starknet::core::types::{ContractClass, FlattenedSierraClass};
14+
use starknet::providers::Provider;
15+
use starknet::providers::jsonrpc::{HttpTransport, JsonRpcClient};
16+
use starknet::signers::LocalWallet;
17+
use starknet_types_core::felt::Felt;
18+
use universal_sierra_compiler_api::{SierraType, compile_sierra};
19+
20+
#[derive(Args)]
21+
#[command(about = "Declare a contract by fetching it from a different Starknet instance", long_about = None)]
22+
pub struct DeclareFrom {
23+
/// Class hash of contract declared on a different Starknet instance
24+
#[arg(short = 'g', long)]
25+
pub class_hash: Felt,
26+
27+
#[command(flatten)]
28+
pub fee_args: FeeArgs,
29+
30+
/// Nonce of the transaction. If not provided, nonce will be set automatically
31+
#[arg(short, long)]
32+
pub nonce: Option<Felt>,
33+
34+
#[command(flatten)]
35+
pub source_rpc: SourceRpcArgs,
36+
37+
#[command(flatten)]
38+
pub rpc: RpcArgs,
39+
40+
/// Block identifier from which the contract will be fetched.
41+
/// Possible values: `pre_confirmed`, `latest`, block hash (0x prefixed string)
42+
/// and block number (u64)
43+
#[arg(short, long, default_value = "latest")]
44+
pub block_id: String,
45+
}
46+
47+
#[derive(Args, Clone, Debug, Default)]
48+
#[group(required = false, multiple = false)]
49+
pub struct SourceRpcArgs {
50+
/// RPC provider url address
51+
#[arg(short, long)]
52+
pub source_url: Option<String>,
53+
54+
/// Use predefined network with a public provider. Note that this option may result in rate limits or other unexpected behavior
55+
#[arg(long)]
56+
pub source_network: Option<Network>,
57+
}
58+
59+
impl SourceRpcArgs {
60+
pub async fn get_provider(&self, ui: &UI) -> Result<JsonRpcClient<HttpTransport>> {
61+
let url = self
62+
.get_url()
63+
.context("Either `--source-network` or `--source-url` must be provided")?;
64+
65+
assert!(!url.is_empty(), "url cannot be empty");
66+
67+
let provider = get_provider(&url)?;
68+
verify_and_warn_if_incompatible_rpc_version(&provider, url, ui).await?;
69+
70+
Ok(provider)
71+
}
72+
73+
#[must_use]
74+
fn get_url(&self) -> Option<String> {
75+
if let Some(network) = self.source_network {
76+
let free_provider = FreeProvider::semi_random();
77+
Some(network.url(&free_provider))
78+
} else {
79+
self.source_url
80+
.as_ref()
81+
.map(std::string::ToString::to_string)
82+
}
83+
}
84+
}
85+
86+
pub async fn declare_from(
87+
declare_from: DeclareFrom,
88+
account: &SingleOwnerAccount<&JsonRpcClient<HttpTransport>, LocalWallet>,
89+
wait_config: WaitForTx,
90+
skip_on_already_declared: bool,
91+
source_provider: &JsonRpcClient<HttpTransport>,
92+
ui: &UI,
93+
) -> Result<DeclareResponse, StarknetCommandError> {
94+
let block_id = get_block_id(&declare_from.block_id)?;
95+
let class = source_provider
96+
.get_class(block_id, declare_from.class_hash)
97+
.await
98+
.map_err(SNCastProviderError::from)
99+
.map_err(StarknetCommandError::from)?;
100+
101+
let flattened_sierra = match class {
102+
ContractClass::Sierra(class) => class,
103+
ContractClass::Legacy(_) => {
104+
return Err(StarknetCommandError::UnknownError(anyhow::anyhow!(
105+
"Declaring from Cairo 0 (legacy) contracts is not supported"
106+
)));
107+
}
108+
};
109+
let sierra: SierraClass = flattened_sierra_to_sierra(flattened_sierra)
110+
.expect("Failed to parse flattened sierra class");
111+
112+
let casm_json: String = compile_sierra(
113+
&serde_json::to_value(&sierra).expect("Failed to convert sierra to json value"),
114+
&SierraType::Contract,
115+
)
116+
.expect("Failed to compile sierra to casm");
117+
let casm: CompiledClass = serde_json::from_str(&casm_json)
118+
.expect("Failed to deserialize casm JSON into CompiledClass");
119+
let sierra_class_hash = sierra.class_hash().map_err(anyhow::Error::from)?;
120+
121+
if declare_from.class_hash != sierra_class_hash {
122+
return Err(StarknetCommandError::UnknownError(anyhow::anyhow!(
123+
"The provided sierra class hash {:#x} does not match the computed class hash {:#x} from the fetched contract.",
124+
declare_from.class_hash,
125+
sierra_class_hash
126+
)));
127+
}
128+
129+
declare_with_artifacts(
130+
sierra,
131+
casm,
132+
declare_from.fee_args,
133+
declare_from.nonce,
134+
account,
135+
wait_config,
136+
skip_on_already_declared,
137+
ui,
138+
)
139+
.await
140+
}
141+
142+
fn flattened_sierra_to_sierra(class: FlattenedSierraClass) -> Result<SierraClass> {
143+
Ok(SierraClass {
144+
sierra_program: class.sierra_program,
145+
sierra_program_debug_info: SierraClassDebugInfo {
146+
type_names: vec![],
147+
libfunc_names: vec![],
148+
user_func_names: vec![],
149+
},
150+
contract_class_version: class.contract_class_version,
151+
entry_points_by_type: class.entry_points_by_type,
152+
abi: serde_json::from_str::<Vec<AbiEntry>>(&class.abi)?,
153+
})
154+
}

crates/sncast/src/starknet_commands/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
pub mod account;
22
pub mod call;
33
pub mod declare;
4+
pub mod declare_from;
45
pub mod deploy;
56
pub mod invoke;
67
pub mod multicall;

0 commit comments

Comments
 (0)