From abc8bf6577f474ab6518fedd721ce5512f12dc64 Mon Sep 17 00:00:00 2001 From: Jonathan LEI Date: Sun, 13 Aug 2023 16:15:06 +0000 Subject: [PATCH] feat: argent x account support Adds the ability to deploy and fetch Argent X accounts. --- src/account.rs | 46 +++++++++++-- src/account_factory.rs | 63 ++++++++++++++++++ src/main.rs | 1 + src/subcommands/account/argent/init.rs | 92 ++++++++++++++++++++++++++ src/subcommands/account/argent/mod.rs | 25 +++++++ src/subcommands/account/deploy.rs | 69 +++++++++++++------ src/subcommands/account/fetch.rs | 67 +++++++++++++++---- src/subcommands/account/mod.rs | 6 ++ 8 files changed, 328 insertions(+), 41 deletions(-) create mode 100644 src/account_factory.rs create mode 100644 src/subcommands/account/argent/init.rs create mode 100644 src/subcommands/account/argent/mod.rs diff --git a/src/account.rs b/src/account.rs index 4153010..3e65eaa 100644 --- a/src/account.rs +++ b/src/account.rs @@ -7,14 +7,21 @@ use starknet::{ core::{ serde::unsigned_field_element::UfeHex, types::FieldElement, utils::get_contract_address, }, - macros::felt, + macros::{felt, selector}, }; -pub const KNOWN_ACCOUNT_CLASSES: [KnownAccountClass; 1] = [KnownAccountClass { - class_hash: felt!("0x048dd59fabc729a5db3afdf649ecaf388e931647ab2f53ca3c6183fa480aa292"), - variant: AccountVariantType::OpenZeppelin, - description: "OpenZeppelin account contract v0.6.1 compiled with cairo-lang v0.11.0.2", -}]; +pub const KNOWN_ACCOUNT_CLASSES: [KnownAccountClass; 2] = [ + KnownAccountClass { + class_hash: felt!("0x048dd59fabc729a5db3afdf649ecaf388e931647ab2f53ca3c6183fa480aa292"), + variant: AccountVariantType::OpenZeppelin, + description: "OpenZeppelin account contract v0.6.1 compiled with cairo-lang v0.11.0.2", + }, + KnownAccountClass { + class_hash: felt!("0x025ec026985a3bf9d0cc1fe17326b245dfdc3ff89b8fde106542a3ea56c5a918"), + variant: AccountVariantType::Argent, + description: "Argent X official proxy account", + }, +]; #[derive(Serialize, Deserialize)] pub struct AccountConfig { @@ -27,6 +34,7 @@ pub struct AccountConfig { #[serde(tag = "type", rename_all = "snake_case")] pub enum AccountVariant { OpenZeppelin(OzAccountConfig), + Argent(ArgentAccountConfig), } #[derive(Serialize, Deserialize)] @@ -44,6 +52,7 @@ pub struct KnownAccountClass { pub enum AccountVariantType { OpenZeppelin, + Argent, } #[serde_as] @@ -54,6 +63,18 @@ pub struct OzAccountConfig { pub public_key: FieldElement, } +#[serde_as] +#[derive(Serialize, Deserialize)] +pub struct ArgentAccountConfig { + pub version: u64, + #[serde_as(as = "UfeHex")] + pub implementation: FieldElement, + #[serde_as(as = "UfeHex")] + pub signer: FieldElement, + #[serde_as(as = "UfeHex")] + pub guardian: FieldElement, +} + #[serde_as] #[derive(Serialize, Deserialize)] pub struct UndeployedStatus { @@ -88,6 +109,18 @@ impl AccountConfig { &[oz.public_key], FieldElement::ZERO, )), + AccountVariant::Argent(argent) => Ok(get_contract_address( + undeployed_status.salt, + undeployed_status.class_hash, + &[ + argent.implementation, // implementation + selector!("initialize"), // selector + FieldElement::TWO, // calldata_len + argent.signer, // calldata[0]: signer + argent.guardian, // calldata[1]: guardian + ], + FieldElement::ZERO, + )), } } } @@ -96,6 +129,7 @@ impl Display for AccountVariantType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { AccountVariantType::OpenZeppelin => write!(f, "OpenZeppelin"), + AccountVariantType::Argent => write!(f, "Argent X"), } } } diff --git a/src/account_factory.rs b/src/account_factory.rs new file mode 100644 index 0000000..e1aa513 --- /dev/null +++ b/src/account_factory.rs @@ -0,0 +1,63 @@ +use async_trait::async_trait; +use starknet::{ + accounts::{ + AccountFactory, ArgentAccountFactory, OpenZeppelinAccountFactory, RawAccountDeployment, + }, + core::types::FieldElement, + providers::Provider, + signers::Signer, +}; + +pub enum AnyAccountFactory { + OpenZeppelin(OpenZeppelinAccountFactory), + Argent(ArgentAccountFactory), +} + +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +impl AccountFactory for AnyAccountFactory +where + S: Signer + Sync + Send, + P: Provider + Sync + Send, +{ + type Provider = P; + type SignError = S::SignError; + + fn class_hash(&self) -> FieldElement { + match self { + AnyAccountFactory::OpenZeppelin(inner) => inner.class_hash(), + AnyAccountFactory::Argent(inner) => inner.class_hash(), + } + } + + fn calldata(&self) -> Vec { + match self { + AnyAccountFactory::OpenZeppelin(inner) => inner.calldata(), + AnyAccountFactory::Argent(inner) => inner.calldata(), + } + } + + fn chain_id(&self) -> FieldElement { + match self { + AnyAccountFactory::OpenZeppelin(inner) => inner.chain_id(), + AnyAccountFactory::Argent(inner) => inner.chain_id(), + } + } + + fn provider(&self) -> &Self::Provider { + match self { + AnyAccountFactory::OpenZeppelin(inner) => inner.provider(), + AnyAccountFactory::Argent(inner) => inner.provider(), + } + } + + async fn sign_deployment( + &self, + deployment: &RawAccountDeployment, + ) -> Result, Self::SignError> { + match self { + AnyAccountFactory::OpenZeppelin(inner) => inner.sign_deployment(deployment).await, + AnyAccountFactory::Argent(inner) => inner.sign_deployment(deployment).await, + } + } +} diff --git a/src/main.rs b/src/main.rs index 7eda65c..69cdfb4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,7 @@ use colored::Colorize; use crate::{provider::ProviderArgs, subcommands::*}; mod account; +mod account_factory; mod address_book; mod casm; mod chain_id; diff --git a/src/subcommands/account/argent/init.rs b/src/subcommands/account/argent/init.rs new file mode 100644 index 0000000..b10fbab --- /dev/null +++ b/src/subcommands/account/argent/init.rs @@ -0,0 +1,92 @@ +use std::{io::Write, path::PathBuf}; + +use anyhow::Result; +use clap::Parser; +use colored::Colorize; +use starknet::{ + core::types::FieldElement, + macros::felt, + signers::{Signer, SigningKey}, +}; + +use crate::{ + account::{ + AccountConfig, AccountVariant, ArgentAccountConfig, DeploymentStatus, UndeployedStatus, + }, + path::ExpandedPathbufParser, + signer::SignerArgs, +}; + +/// Official hashes used as of extension version 5.7.0 +const ARGENT_PROXY_CLASS_HASH: FieldElement = + felt!("0x025ec026985a3bf9d0cc1fe17326b245dfdc3ff89b8fde106542a3ea56c5a918"); +const ARGENT_IMPL_CLASS_HASH: FieldElement = + felt!("0x033434ad846cdd5f23eb73ff09fe6fddd568284a0fb7d1be20ee482f044dabe2"); + +#[derive(Debug, Parser)] +pub struct Init { + // TODO: allow manually specifying public key without using a signer + #[clap(flatten)] + signer: SignerArgs, + #[clap( + long, + short, + help = "Overwrite the account config file if it already exists" + )] + force: bool, + #[clap( + value_parser = ExpandedPathbufParser, + help = "Path to save the account config file" + )] + output: PathBuf, +} + +impl Init { + pub async fn run(self) -> Result<()> { + if self.output.exists() && !self.force { + anyhow::bail!("account config file already exists"); + } + + let signer = self.signer.into_signer()?; + + // Too lazy to write random salt generation + let salt = SigningKey::from_random().secret_scalar(); + + let account_config = AccountConfig { + version: 1, + variant: AccountVariant::Argent(ArgentAccountConfig { + version: 1, + implementation: ARGENT_IMPL_CLASS_HASH, + signer: signer.get_public_key().await?.scalar(), + guardian: FieldElement::ZERO, + }), + deployment: DeploymentStatus::Undeployed(UndeployedStatus { + class_hash: ARGENT_PROXY_CLASS_HASH, + salt, + }), + }; + + let deployed_address = account_config.deploy_account_address()?; + + let mut file = std::fs::File::create(&self.output)?; + serde_json::to_writer_pretty(&mut file, &account_config)?; + file.write_all(b"\n")?; + + eprintln!( + "Created new account config file: {}", + std::fs::canonicalize(&self.output)?.display() + ); + eprintln!(); + eprintln!( + "Once deployed, this account will be available at:\n {}", + format!("{:#064x}", deployed_address).bright_yellow() + ); + eprintln!(); + eprintln!( + "Deploy this account by running:\n {}", + format!("starkli account deploy {}", self.output.display()).bright_yellow() + ); + + Ok(()) + } +} diff --git a/src/subcommands/account/argent/mod.rs b/src/subcommands/account/argent/mod.rs new file mode 100644 index 0000000..464511a --- /dev/null +++ b/src/subcommands/account/argent/mod.rs @@ -0,0 +1,25 @@ +use anyhow::Result; +use clap::{Parser, Subcommand}; + +mod init; +use init::Init; + +#[derive(Debug, Parser)] +pub struct Argent { + #[clap(subcommand)] + command: Subcommands, +} + +#[derive(Debug, Subcommand)] +enum Subcommands { + #[clap(about = "Create a new account configuration without actually deploying")] + Init(Init), +} + +impl Argent { + pub async fn run(self) -> Result<()> { + match self.command { + Subcommands::Init(cmd) => cmd.run().await, + } + } +} diff --git a/src/subcommands/account/deploy.rs b/src/subcommands/account/deploy.rs index 5779932..c76031c 100644 --- a/src/subcommands/account/deploy.rs +++ b/src/subcommands/account/deploy.rs @@ -4,7 +4,7 @@ use anyhow::Result; use clap::Parser; use colored::Colorize; use starknet::{ - accounts::{AccountFactory, OpenZeppelinAccountFactory}, + accounts::{AccountFactory, ArgentAccountFactory, OpenZeppelinAccountFactory}, core::types::FieldElement, providers::Provider, signers::Signer, @@ -12,6 +12,7 @@ use starknet::{ use crate::{ account::{AccountConfig, AccountVariant, DeployedStatus, DeploymentStatus}, + account_factory::AnyAccountFactory, fee::{FeeArgs, FeeSetting}, path::ExpandedPathbufParser, signer::SignerArgs, @@ -63,10 +64,7 @@ impl Deploy { let mut account: AccountConfig = serde_json::from_reader(&mut std::fs::File::open(&self.file)?)?; - #[allow(clippy::infallible_destructuring_match)] - let oz_config = match &account.variant { - AccountVariant::OpenZeppelin(inner) => inner, - }; + let signer_public_key = signer.get_public_key().await?.scalar(); let undeployed_status = match &account.deployment { DeploymentStatus::Undeployed(inner) => inner, @@ -75,25 +73,52 @@ impl Deploy { } }; - // Makes sure we're using the right key - let signer_public_key = signer.get_public_key().await?.scalar(); - if signer_public_key != oz_config.public_key { - anyhow::bail!( - "public key mismatch. Expected: {:#064x}; actual: {:#064x}.", - oz_config.public_key, - signer_public_key - ); - } - let chain_id = provider.chain_id().await?; - let factory = OpenZeppelinAccountFactory::new( - undeployed_status.class_hash, - chain_id, - signer.clone(), - provider.clone(), - ) - .await?; + let factory = match &account.variant { + AccountVariant::OpenZeppelin(oz_config) => { + // Makes sure we're using the right key + if signer_public_key != oz_config.public_key { + anyhow::bail!( + "public key mismatch. Expected: {:#064x}; actual: {:#064x}.", + oz_config.public_key, + signer_public_key + ); + } + + AnyAccountFactory::OpenZeppelin( + OpenZeppelinAccountFactory::new( + undeployed_status.class_hash, + chain_id, + signer.clone(), + provider.clone(), + ) + .await?, + ) + } + AccountVariant::Argent(argent_config) => { + // Makes sure we're using the right key + if signer_public_key != argent_config.signer { + anyhow::bail!( + "public key mismatch. Expected: {:#064x}; actual: {:#064x}.", + argent_config.signer, + signer_public_key + ); + } + + AnyAccountFactory::Argent( + ArgentAccountFactory::new( + undeployed_status.class_hash, + argent_config.implementation, + chain_id, + FieldElement::ZERO, + signer.clone(), + provider.clone(), + ) + .await?, + ) + } + }; let account_deployment = factory.deploy(undeployed_status.salt); diff --git a/src/subcommands/account/fetch.rs b/src/subcommands/account/fetch.rs index 9bf9ce4..264b9da 100644 --- a/src/subcommands/account/fetch.rs +++ b/src/subcommands/account/fetch.rs @@ -11,8 +11,8 @@ use starknet::{ use crate::{ account::{ - AccountConfig, AccountVariant, AccountVariantType, DeployedStatus, DeploymentStatus, - OzAccountConfig, KNOWN_ACCOUNT_CLASSES, + AccountConfig, AccountVariant, AccountVariantType, ArgentAccountConfig, DeployedStatus, + DeploymentStatus, OzAccountConfig, KNOWN_ACCOUNT_CLASSES, }, verbosity::VerbosityArgs, ProviderArgs, @@ -84,7 +84,7 @@ impl Fetch { None => return Ok(()), }; - let account = match known_class.variant { + let variant = match known_class.variant { AccountVariantType::OpenZeppelin => { let public_key = provider .call( @@ -97,18 +97,59 @@ impl Fetch { ) .await?[0]; - AccountConfig { + AccountVariant::OpenZeppelin(OzAccountConfig { version: 1, - variant: AccountVariant::OpenZeppelin(OzAccountConfig { - version: 1, - public_key, - }), - deployment: DeploymentStatus::Deployed(DeployedStatus { - class_hash, - address, - }), - } + public_key, + }) } + AccountVariantType::Argent => { + let implementation = provider + .call( + FunctionCall { + contract_address: address, + entry_point_selector: selector!("get_implementation"), + calldata: vec![], + }, + BlockId::Tag(BlockTag::Pending), + ) + .await?[0]; + let signer = provider + .call( + FunctionCall { + contract_address: address, + entry_point_selector: selector!("getSigner"), + calldata: vec![], + }, + BlockId::Tag(BlockTag::Pending), + ) + .await?[0]; + let guardian = provider + .call( + FunctionCall { + contract_address: address, + entry_point_selector: selector!("getGuardian"), + calldata: vec![], + }, + BlockId::Tag(BlockTag::Pending), + ) + .await?[0]; + + AccountVariant::Argent(ArgentAccountConfig { + version: 1, + implementation, + signer, + guardian, + }) + } + }; + + let account = AccountConfig { + version: 1, + variant, + deployment: DeploymentStatus::Deployed(DeployedStatus { + class_hash, + address, + }), }; let mut file = std::fs::File::create(&output)?; diff --git a/src/subcommands/account/mod.rs b/src/subcommands/account/mod.rs index c5e8747..3584a43 100644 --- a/src/subcommands/account/mod.rs +++ b/src/subcommands/account/mod.rs @@ -10,6 +10,9 @@ use deploy::Deploy; mod oz; use oz::Oz; +mod argent; +use argent::Argent; + #[derive(Debug, Parser)] pub struct Account { #[clap(subcommand)] @@ -24,6 +27,8 @@ enum Subcommands { Deploy(Deploy), #[clap(about = "Create and manage OpenZeppelin account contracts")] Oz(Oz), + #[clap(about = "Create and manage Argent X account contracts")] + Argent(Argent), } impl Account { @@ -32,6 +37,7 @@ impl Account { Subcommands::Fetch(cmd) => cmd.run().await, Subcommands::Deploy(cmd) => cmd.run().await, Subcommands::Oz(cmd) => cmd.run().await, + Subcommands::Argent(cmd) => cmd.run().await, } } }