Skip to content

Commit d53b6b3

Browse files
Support inbuilt devnet accounts (#3761)
<!-- Reference any GitHub issues resolved by this PR --> Towards #3603 **Stack**: - #3762 - #3761 ⬅ - #3760 ## 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` --------- Co-authored-by: Artur Michalek <[email protected]> Co-authored-by: Artur Michałek <[email protected]>
1 parent b995d55 commit d53b6b3

File tree

12 files changed

+426
-93
lines changed

12 files changed

+426
-93
lines changed

crates/sncast/src/helpers/account.rs

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
1-
use crate::NestedMap;
2-
use anyhow::Result;
1+
use crate::{
2+
NestedMap, build_account, check_account_file_exists, helpers::devnet_provider::DevnetProvider,
3+
};
4+
use anyhow::{Result, ensure};
35
use camino::Utf8PathBuf;
4-
use std::collections::HashSet;
6+
use starknet::{
7+
accounts::SingleOwnerAccount,
8+
providers::{JsonRpcClient, Provider, jsonrpc::HttpTransport},
9+
signers::LocalWallet,
10+
};
11+
use std::collections::{HashMap, HashSet};
512
use std::fs;
613

714
use crate::{AccountData, read_and_parse_json_file};
@@ -48,3 +55,64 @@ pub fn load_accounts(accounts_file: &Utf8PathBuf) -> Result<Value> {
4855

4956
Ok(accounts)
5057
}
58+
59+
pub fn check_account_exists(
60+
account_name: &str,
61+
network_name: &str,
62+
accounts_file: &Utf8PathBuf,
63+
) -> Result<bool> {
64+
check_account_file_exists(accounts_file)?;
65+
66+
let accounts: HashMap<String, HashMap<String, AccountData>> =
67+
read_and_parse_json_file(accounts_file)?;
68+
69+
accounts
70+
.get(network_name)
71+
.map(|network_accounts| network_accounts.contains_key(account_name))
72+
.ok_or_else(|| {
73+
anyhow::anyhow!("Network with name {network_name} does not exist in accounts file")
74+
})
75+
}
76+
77+
#[must_use]
78+
pub fn is_devnet_account(account: &str) -> bool {
79+
account.starts_with("devnet-")
80+
}
81+
82+
pub async fn get_account_from_devnet<'a>(
83+
account: &str,
84+
provider: &'a JsonRpcClient<HttpTransport>,
85+
url: &str,
86+
) -> Result<SingleOwnerAccount<&'a JsonRpcClient<HttpTransport>, LocalWallet>> {
87+
let account_number: u8 = account
88+
.strip_prefix("devnet-")
89+
.map(|s| s.parse::<u8>().expect("Invalid devnet account number"))
90+
.context("Failed to parse devnet account number")?;
91+
92+
let devnet_provider = DevnetProvider::new(url);
93+
devnet_provider.ensure_alive().await?;
94+
95+
let devnet_config = devnet_provider.get_config().await;
96+
let devnet_config = match devnet_config {
97+
Ok(config) => config,
98+
Err(err) => {
99+
return Err(err);
100+
}
101+
};
102+
103+
ensure!(
104+
account_number <= devnet_config.total_accounts && account_number != 0,
105+
"Devnet account number must be between 1 and {}",
106+
devnet_config.total_accounts
107+
);
108+
109+
let devnet_accounts = devnet_provider.get_predeployed_accounts().await?;
110+
let predeployed_account = devnet_accounts
111+
.get((account_number - 1) as usize)
112+
.expect("Failed to get devnet account")
113+
.to_owned();
114+
115+
let account_data = AccountData::from(predeployed_account);
116+
let chain_id = provider.chain_id().await?;
117+
build_account(account_data, chain_id, provider).await
118+
}

crates/sncast/src/lib.rs

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1+
use crate::helpers::account::{check_account_exists, get_account_from_devnet, is_devnet_account};
2+
use crate::helpers::configuration::CastConfig;
13
use crate::helpers::constants::{DEFAULT_STATE_FILE_SUFFIX, WAIT_RETRY_INTERVAL, WAIT_TIMEOUT};
4+
use crate::helpers::rpc::RpcArgs;
25
use crate::response::errors::SNCastProviderError;
36
use anyhow::{Context, Error, Result, anyhow, bail};
47
use camino::Utf8PathBuf;
58
use clap::ValueEnum;
69
use conversions::serde::serialize::CairoSerialize;
710
use foundry_ui::UI;
11+
use foundry_ui::components::warning::WarningMessage;
812
use helpers::constants::{KEYSTORE_PASSWORD_ENV_VAR, UDC_ADDRESS};
913
use rand::RngCore;
1014
use rand::rngs::OsRng;
@@ -85,7 +89,7 @@ pub const MAINNET: Felt =
8589
pub const SEPOLIA: Felt =
8690
Felt::from_hex_unchecked(const_hex::const_encode::<10, true>(b"SN_SEPOLIA").as_str());
8791

88-
#[derive(ValueEnum, Clone, Copy, Debug)]
92+
#[derive(ValueEnum, Clone, Copy, Debug, PartialEq)]
8993
pub enum Network {
9094
Mainnet,
9195
Sepolia,
@@ -245,6 +249,51 @@ pub async fn get_nonce(
245249
}
246250

247251
pub async fn get_account<'a>(
252+
config: &CastConfig,
253+
provider: &'a JsonRpcClient<HttpTransport>,
254+
rpc_args: &RpcArgs,
255+
keystore: Option<&Utf8PathBuf>,
256+
ui: &UI,
257+
) -> Result<SingleOwnerAccount<&'a JsonRpcClient<HttpTransport>, LocalWallet>> {
258+
let chain_id = get_chain_id(provider).await?;
259+
let network_name = chain_id_to_network_name(chain_id);
260+
let account = &config.account;
261+
let is_devnet_account = is_devnet_account(account);
262+
263+
if is_devnet_account
264+
&& let Some(network) = rpc_args.network
265+
&& (network == Network::Mainnet || network == Network::Sepolia)
266+
{
267+
bail!(format!(
268+
"Devnet accounts cannot be used with `--network {network}`"
269+
));
270+
}
271+
272+
let accounts_file = &config.accounts_file;
273+
let exists_in_accounts_file = check_account_exists(account, &network_name, accounts_file)?;
274+
275+
match (is_devnet_account, exists_in_accounts_file) {
276+
(true, true) => {
277+
ui.println(&WarningMessage::new(format!(
278+
"Using account {account} from accounts file {accounts_file}. \
279+
To use an inbuilt devnet account, please rename your existing account or use an account with a different number."
280+
)));
281+
ui.print_blank_line();
282+
return get_account_from_accounts_file(account, accounts_file, provider, keystore)
283+
.await;
284+
}
285+
(true, false) => {
286+
let url = rpc_args.get_url(&config.url).context("Failed to get url")?;
287+
return get_account_from_devnet(account, provider, &url).await;
288+
}
289+
_ => {
290+
return get_account_from_accounts_file(account, accounts_file, provider, keystore)
291+
.await;
292+
}
293+
}
294+
}
295+
296+
pub async fn get_account_from_accounts_file<'a>(
248297
account: &str,
249298
accounts_file: &Utf8PathBuf,
250299
provider: &'a JsonRpcClient<HttpTransport>,

crates/sncast/src/main.rs

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -269,10 +269,11 @@ async fn run_async_command(cli: Cli, config: CastConfig, ui: &UI) -> Result<()>
269269
let rpc = declare.rpc.clone();
270270

271271
let account = get_account(
272-
&config.account,
273-
&config.accounts_file,
272+
&config,
274273
&provider,
274+
&declare.rpc,
275275
config.keystore.as_ref(),
276+
ui,
276277
)
277278
.await?;
278279
let manifest_path = assert_manifest_path_exists()?;
@@ -347,11 +348,13 @@ async fn run_async_command(cli: Cli, config: CastConfig, ui: &UI) -> Result<()>
347348
let provider = declare_from.rpc.get_provider(&config, ui).await?;
348349
let rpc_args = declare_from.rpc.clone();
349350
let source_provider = declare_from.source_rpc.get_provider(ui).await?;
351+
350352
let account = get_account(
351-
&config.account,
352-
&config.accounts_file,
353+
&config,
353354
&provider,
355+
&declare_from.rpc,
354356
config.keystore.as_ref(),
357+
ui,
355358
)
356359
.await?;
357360

@@ -395,13 +398,8 @@ async fn run_async_command(cli: Cli, config: CastConfig, ui: &UI) -> Result<()>
395398

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

398-
let account = get_account(
399-
&config.account,
400-
&config.accounts_file,
401-
&provider,
402-
config.keystore.as_ref(),
403-
)
404-
.await?;
401+
let account =
402+
get_account(&config, &provider, &rpc, config.keystore.as_ref(), ui).await?;
405403

406404
// safe to unwrap because "constructor" is a standardized name
407405
let selector = get_selector_from_name("constructor").unwrap();
@@ -484,13 +482,8 @@ async fn run_async_command(cli: Cli, config: CastConfig, ui: &UI) -> Result<()>
484482

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

487-
let account = get_account(
488-
&config.account,
489-
&config.accounts_file,
490-
&provider,
491-
config.keystore.as_ref(),
492-
)
493-
.await?;
485+
let account =
486+
get_account(&config, &provider, &rpc, config.keystore.as_ref(), ui).await?;
494487

495488
let selector = get_selector_from_name(&function)
496489
.context("Failed to convert entry point selector to FieldElement")?;

crates/sncast/src/starknet_commands/multicall/mod.rs

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -51,13 +51,8 @@ pub async fn multicall(
5151
starknet_commands::multicall::Commands::Run(run) => {
5252
let provider = run.rpc.get_provider(&config, ui).await?;
5353

54-
let account = get_account(
55-
&config.account,
56-
&config.accounts_file,
57-
&provider,
58-
config.keystore.as_ref(),
59-
)
60-
.await?;
54+
let account =
55+
get_account(&config, &provider, &run.rpc, config.keystore.as_ref(), ui).await?;
6156
let result =
6257
starknet_commands::multicall::run::run(run.clone(), &account, wait_config, ui)
6358
.await;

crates/sncast/src/starknet_commands/script/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use crate::starknet_commands::script::run::Run;
22
use crate::{Cli, starknet_commands::script::init::Init};
33
use crate::{get_cast_config, process_command_result, starknet_commands};
4+
use anyhow::Context;
45
use clap::{Args, Subcommand};
56
use foundry_ui::UI;
67
use sncast::helpers::scarb_utils::{
@@ -78,12 +79,14 @@ pub fn run_script_command(
7879
)))
7980
};
8081

82+
let url = run.rpc.get_url(&config.url).context("Failed to get url")?;
8183
let result = starknet_commands::script::run::run(
8284
&run.script_name,
8385
&metadata_with_deps,
8486
&package_metadata,
8587
&mut artifacts,
8688
&provider,
89+
&url,
8790
runtime,
8891
&config,
8992
state_file_path,

crates/sncast/src/starknet_commands/script/run.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,7 @@ pub fn run(
284284
package_metadata: &PackageMetadata,
285285
artifacts: &mut HashMap<String, StarknetContractArtifacts>,
286286
provider: &JsonRpcClient<HttpTransport>,
287+
url: &str,
287288
tokio_runtime: Runtime,
288289
config: &CastConfig,
289290
state_file_path: Option<Utf8PathBuf>,
@@ -364,11 +365,16 @@ pub fn run(
364365
let account = if config.account.is_empty() {
365366
None
366367
} else {
368+
let rpc_args = RpcArgs {
369+
url: Some(url.to_string()),
370+
network: None,
371+
};
367372
Some(tokio_runtime.block_on(get_account(
368-
&config.account,
369-
&config.accounts_file,
373+
config,
370374
provider,
375+
&rpc_args,
371376
config.keystore.as_ref(),
377+
ui,
372378
))?)
373379
};
374380
let state = StateManager::from(state_file_path)?;

crates/sncast/tests/data/accounts/accounts.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,11 @@
140140
"public_key": "0x36a64f9d432cce317c3e50bc467d48f2797463babe8f29c83c2e6125ddd1947",
141141
"salt": "0x25e7144fce03d200",
142142
"type": "open_zeppelin"
143+
},
144+
"devnet-1": {
145+
"private_key": "0x0000000000000000000000000000000056c12e097e49ea382ca8eadec0839401",
146+
"public_key": "0x048234b9bc6c1e749f4b908d310d8c53dae6564110b05ccf79016dca8ce7dfac",
147+
"address": "0x06f4621e7ad43707b3f69f9df49425c3d94fdc5ab2e444bfa0e7e4edeff7992d"
143148
}
144149
}
145150
}

0 commit comments

Comments
 (0)