Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable deploying argent accounts #2107

Merged
Merged
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
21 changes: 18 additions & 3 deletions crates/sncast/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,12 @@ impl AccountData {
Self::get_as_felt(&self.class_hash, "class_hash")
}

pub fn get_account_type(&self) -> Result<String> {
self.account_type
.clone()
.ok_or_else(|| anyhow!("Failed to get account type"))
}

fn get_as_felt(value: &Option<String>, key_name: &str) -> Result<FieldElement> {
let value_to_parse = value
.as_ref()
Expand Down Expand Up @@ -325,9 +331,6 @@ pub fn get_account_data_from_keystore(
.map(str::to_string)
};

let public_key = parse_to_string(&account_info, "variant", "public_key")
.ok_or_else(|| anyhow::anyhow!("Failed to get public key from account JSON file"))?;

let address = parse_to_string(&account_info, "deployment", "address");
let class_hash = parse_to_string(&account_info, "deployment", "class_hash");
let salt = parse_to_string(&account_info, "deployment", "salt");
Expand All @@ -336,6 +339,18 @@ pub fn get_account_data_from_keystore(
let legacy = get_from_json(&account_info, "variant", "legacy").and_then(Value::as_bool);
let account_type = parse_to_string(&account_info, "variant", "type");

let public_key_field = match account_type
.clone()
.ok_or_else(|| anyhow!("Failed to get type key"))?
.as_str()
{
"argent" => "owner",
_ => "public_key",
};

let public_key = parse_to_string(&account_info, "variant", public_key_field)
.ok_or_else(|| anyhow::anyhow!("Failed to get public key from account JSON file"))?;

Ok(AccountData {
private_key,
public_key,
Expand Down
115 changes: 97 additions & 18 deletions crates/sncast/src/starknet_commands/account/deploy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ use clap::Args;
use serde_json::Map;
use sncast::helpers::constants::KEYSTORE_PASSWORD_ENV_VAR;
use sncast::response::structs::{Felt, InvokeResponse};
use starknet::accounts::AccountFactoryError;
use starknet::accounts::{AccountFactory, OpenZeppelinAccountFactory};
use starknet::accounts::{AccountFactoryError, ArgentAccountFactory};
use starknet::core::types::BlockTag::Pending;
use starknet::core::types::{BlockId, FieldElement, StarknetError::ClassHashNotFound};
use starknet::core::utils::get_contract_address;
Expand Down Expand Up @@ -100,12 +100,15 @@ async fn deploy_from_keystore(
let salt = account_data.get_salt_as_felt()?;
let class_hash = account_data.get_class_hash_as_felt()?;

let address = get_contract_address(
salt,
class_hash,
&[private_key.verifying_key().scalar()],
FieldElement::ZERO,
);
// TODO: Improve code for checking if address exists or remove this logic
let account_type = &account_data.get_account_type()?;
let calldata = match account_type.as_str() {
"open_zeppelin" => vec![private_key.verifying_key().scalar()],
"argent" => vec![private_key.verifying_key().scalar(), FieldElement::ZERO],
_ => panic!("Invalid account type"),
};

let address = get_contract_address(salt, class_hash, &calldata, FieldElement::ZERO);

let result = if provider
.get_class_hash_at(BlockId::Tag(Pending), address)
Expand All @@ -116,8 +119,9 @@ async fn deploy_from_keystore(
transaction_hash: Felt(FieldElement::ZERO),
}
} else {
deploy_oz_account(
get_deployment_result(
provider,
account_type.as_str(),
class_hash,
private_key,
salt,
Expand Down Expand Up @@ -147,8 +151,9 @@ async fn deploy_from_accounts_file(
parse_number(&account_data.private_key).context("Failed to parse private key")?,
);

let result = deploy_oz_account(
let result = get_deployment_result(
provider,
&account_data.get_account_type()?,
account_data.get_class_hash_as_felt()?,
private_key,
account_data.get_salt_as_felt()?,
Expand All @@ -163,34 +168,108 @@ async fn deploy_from_accounts_file(
Ok(result)
}

#[allow(clippy::too_many_arguments)]
async fn get_deployment_result(
provider: &JsonRpcClient<HttpTransport>,
account_type: &str,
class_hash: FieldElement,
private_key: SigningKey,
salt: FieldElement,
chain_id: FieldElement,
max_fee: Option<FieldElement>,
wait_config: WaitForTx,
) -> Result<InvokeResponse> {
match account_type {
"argent" => {
deploy_argent_account(
provider,
class_hash,
private_key,
salt,
chain_id,
max_fee,
wait_config,
)
.await
}
"open_zeppelin" => {
deploy_oz_account(
provider,
class_hash,
private_key,
salt,
chain_id,
max_fee,
wait_config,
)
.await
}
_ => Err(anyhow!(
"Incorrect account type, possible values are ['open_zeppelin', 'argent']"
)),
}
}

async fn deploy_oz_account(
provider: &JsonRpcClient<HttpTransport>,
oz_class_hash: FieldElement,
class_hash: FieldElement,
private_key: SigningKey,
salt: FieldElement,
chain_id: FieldElement,
max_fee: Option<FieldElement>,
wait_config: WaitForTx,
) -> Result<InvokeResponse> {
let factory = OpenZeppelinAccountFactory::new(
oz_class_hash,
class_hash,
chain_id,
LocalWallet::from_signing_key(private_key),
provider,
)
.await?;

deploy_account(factory, provider, salt, max_fee, wait_config, class_hash).await
}

async fn deploy_argent_account(
provider: &JsonRpcClient<HttpTransport>,
class_hash: FieldElement,
private_key: SigningKey,
salt: FieldElement,
chain_id: FieldElement,
max_fee: Option<FieldElement>,
wait_config: WaitForTx,
) -> Result<InvokeResponse> {
let factory = ArgentAccountFactory::new(
class_hash,
chain_id,
FieldElement::ZERO,
LocalWallet::from_signing_key(private_key),
provider,
)
.await?;

let deployment = factory.deploy(salt);
deploy_account(factory, provider, salt, max_fee, wait_config, class_hash).await
}

async fn deploy_account<T>(
account_factory: T,
provider: &JsonRpcClient<HttpTransport>,
salt: FieldElement,
max_fee: Option<FieldElement>,
wait_config: WaitForTx,
class_hash: FieldElement,
) -> Result<InvokeResponse>
where
T: AccountFactory + Sync,
{
let deployment = account_factory.deploy(salt);

let deploy_max_fee = if let Some(max_fee) = max_fee {
max_fee
} else {
match deployment.estimate_fee().await {
Ok(max_fee) => max_fee.overall_fee,
Err(error) => {
return Err(handle_account_factory_error::<
OpenZeppelinAccountFactory<LocalWallet, &JsonRpcClient<HttpTransport>>,
>(error))
}
Err(error) => return Err(handle_account_factory_error::<T>(error)),
}
};
let result = deployment.max_fee(deploy_max_fee).send().await;
Expand All @@ -199,7 +278,7 @@ async fn deploy_oz_account(
Err(AccountFactoryError::Provider(error)) => match error {
StarknetError(ClassHashNotFound) => Err(anyhow!(
"Provided class hash {:#x} does not exist",
oz_class_hash,
class_hash,
)),
_ => Err(handle_rpc_error(error)),
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"version": 1,
"variant": {
"type": "argent",
"version": 1,
"owner": "0xe2d3d7080bfc665e0060a06e8e95c3db3ff78a1fec4cc81ddc87e49a12e0a",
"guardian": "0x0"
},
"deployment": {
"status": "undeployed",
"class_hash": "0x29927c8af6bccf3f6fda035981e765a7bdbf18a2dc0d630494f8758aa908e2b",
"salt": "0x296a50ef3b2272b97072186f2905ae6fb7a7bde2d4f2c1f1375c324fc529b9b"
}
}
22 changes: 14 additions & 8 deletions crates/sncast/tests/e2e/account/deploy.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use crate::helpers::constants::{DEVNET_OZ_CLASS_HASH_CAIRO_0, DEVNET_OZ_CLASS_HASH_CAIRO_1, URL};
use crate::helpers::constants::{
ARGENT_ACCOUNT_CLASS_HASH, DEVNET_OZ_CLASS_HASH_CAIRO_0, DEVNET_OZ_CLASS_HASH_CAIRO_1, URL,
};
use crate::helpers::fixtures::copy_file;
use crate::helpers::fixtures::{
get_address_from_keystore, get_transaction_hash, get_transaction_receipt, mint_token,
Expand All @@ -17,6 +19,7 @@ use test_case::test_case;

#[test_case(DEVNET_OZ_CLASS_HASH_CAIRO_0, "oz"; "cairo_0_class_hash")]
#[test_case(DEVNET_OZ_CLASS_HASH_CAIRO_1, "oz"; "cairo_1_class_hash")]
#[test_case(ARGENT_ACCOUNT_CLASS_HASH, "argent"; "argent_class_hash")]
#[tokio::test]
pub async fn test_happy_case(class_hash: &str, account_type: &str) {
let tempdir = create_account(false, class_hash, account_type).await;
Expand Down Expand Up @@ -88,7 +91,6 @@ pub async fn test_happy_case_add_profile() {

#[test_case("{\"alpha-sepolia\": {}}", "error: Account = my_account not found under network = alpha-sepolia" ; "when account name not present")]
#[test_case("{\"alpha-sepolia\": {\"my_account\" : {}}}", "error: Failed to parse file = accounts.json to JSON: missing field `private_key`[..]" ; "when private key not present")]
#[test_case("{\"alpha-sepolia\": {\"my_account\" : {\"private_key\": \"0x1\", \"public_key\": \"0x2\", \"class_hash\": \"0x3\"}}}", "error: Failed to get salt" ; "when salt not present")]
fn test_account_deploy_error(accounts_content: &str, error: &str) {
let temp_dir = tempdir().expect("Unable to create a temporary directory");

Expand Down Expand Up @@ -236,27 +238,30 @@ pub async fn create_account(add_profile: bool, class_hash: &str, account_type: &
tempdir
}

#[test_case("oz"; "open_zeppelin_account")]
#[test_case("argent"; "argent_account")]
#[tokio::test]
pub async fn test_happy_case_keystore() {
pub async fn test_happy_case_keystore(account_type: &str) {
let tempdir = tempdir().expect("Unable to create a temporary directory");

let keystore_file = "my_key.json";
let account_file = "my_account_undeployed_happy_case.json";
let account_file = format!("my_account_{account_type}_undeployed_happy_case.json");

copy_file(
"tests/data/keystore/my_key.json",
tempdir.path().join(keystore_file),
);
copy_file(
"tests/data/keystore/my_account_undeployed_happy_case.json",
tempdir.path().join(account_file),
format!("tests/data/keystore/{account_file}"),
tempdir.path().join(&account_file),
);
env::set_var(KEYSTORE_PASSWORD_ENV_VAR, "123");

let address = get_address_from_keystore(
tempdir.path().join(keystore_file).to_str().unwrap(),
tempdir.path().join(account_file).to_str().unwrap(),
tempdir.path().join(&account_file).to_str().unwrap(),
KEYSTORE_PASSWORD_ENV_VAR,
account_type,
);

mint_token(&address.into_hex_string(), 9_999_999_999_999_999_999).await;
Expand All @@ -267,7 +272,7 @@ pub async fn test_happy_case_keystore() {
"--keystore",
keystore_file,
"--account",
account_file,
&account_file,
"account",
"deploy",
"--max-fee",
Expand Down Expand Up @@ -513,6 +518,7 @@ pub async fn test_deploy_keystore_other_args() {
tempdir.path().join(keystore_file),
tempdir.path().join(account_file),
KEYSTORE_PASSWORD_ENV_VAR,
"oz",
);

mint_token(&address.into_hex_string(), 9_999_999_999_999_999_999).await;
Expand Down
14 changes: 8 additions & 6 deletions crates/sncast/tests/helpers/fixtures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,7 @@ pub fn get_address_from_keystore(
keystore_path: impl AsRef<std::path::Path>,
account_path: impl AsRef<std::path::Path>,
password: &str,
account_type: &str,
) -> FieldElement {
let contents = std::fs::read_to_string(account_path).unwrap();
let items: Map<String, serde_json::Value> = serde_json::from_str(&contents).unwrap();
Expand All @@ -457,12 +458,13 @@ pub fn get_address_from_keystore(
)
.unwrap();

get_contract_address(
salt,
oz_class_hash,
&[private_key.verifying_key().scalar()],
FieldElement::ZERO,
)
let calldata = match account_type {
"oz" => vec![private_key.verifying_key().scalar()],
"argent" => vec![private_key.verifying_key().scalar(), FieldElement::ZERO],
_ => panic!("Invalid account type"),
};

get_contract_address(salt, oz_class_hash, &calldata, FieldElement::ZERO)
}
#[must_use]
pub fn get_accounts_path(relative_path_from_cargo_toml: &str) -> String {
Expand Down
Loading