Skip to content

Commit

Permalink
feat: cast mktx and mktx --create support (#911)
Browse files Browse the repository at this point in the history
* Add mktx branching for zksync, added tests and nonce fix to have it available for the TxEIP712

* Cargo fmt

* Move estimate and mktx common logic to cast zksync module

* Put common logic in zksync cast module and the rest on mktx and estimate submodules for zksync

* clippy and missing import
  • Loading branch information
Jrigada authored Feb 11, 2025
1 parent 2f0d85f commit 4f89e2f
Show file tree
Hide file tree
Showing 7 changed files with 222 additions and 85 deletions.
5 changes: 3 additions & 2 deletions crates/cast/bin/cmd/estimate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use crate::tx::{CastTxBuilder, SenderKind};
use alloy_primitives::U256;
use alloy_provider::Provider;
use alloy_rpc_types::BlockId;
use cast::zksync::ZkTransactionOpts;
use clap::Parser;
use eyre::Result;
use foundry_cli::{
Expand Down Expand Up @@ -43,7 +44,7 @@ pub struct EstimateArgs {

/// Zksync Transaction
#[command(flatten)]
zk_tx: zksync::ZkTransactionOpts,
zk_tx: ZkTransactionOpts,

/// Force a zksync eip-712 transaction and apply CREATE overrides
#[arg(long = "zksync")]
Expand Down Expand Up @@ -112,7 +113,7 @@ impl EstimateArgs {
.await?;

let gas = if zk_tx.has_zksync_args() || zk_force {
zksync::estimate_gas(zk_tx, &config, tx, code).await?
zksync::estimate_gas(zk_tx, tx, code, &config).await?
} else {
provider.estimate_gas(&tx).block(block.unwrap_or_default()).await?
};
Expand Down
83 changes: 4 additions & 79 deletions crates/cast/bin/cmd/estimate/zksync.rs
Original file line number Diff line number Diff line change
@@ -1,93 +1,18 @@
use alloy_network::TransactionBuilder;
use alloy_primitives::{hex, Address, Bytes, TxKind, U256};
use alloy_provider::Provider;
use alloy_rpc_types::TransactionRequest;
use alloy_serde::WithOtherFields;
use alloy_zksync::network::{
transaction_request::TransactionRequest as ZkTransactionRequest,
unsigned_tx::eip712::PaymasterParams,
};
use clap::{command, Parser};
use cast::ZkTransactionOpts;
use eyre::Result;
use foundry_cli::utils;
use foundry_config::Config;

#[derive(Clone, Debug, Parser)]
#[command(next_help_heading = "Transaction options")]
pub struct ZkTransactionOpts {
/// Paymaster address for the ZKSync transaction
#[arg(long = "zk-paymaster-address", requires = "paymaster_input")]
pub paymaster_address: Option<Address>,

/// Paymaster input for the ZKSync transaction
#[arg(long = "zk-paymaster-input", requires = "paymaster_address", value_parser = parse_hex_bytes)]
pub paymaster_input: Option<Bytes>,

/// Factory dependencies for the ZKSync transaction
#[arg(long = "zk-factory-deps", value_parser = parse_hex_bytes, value_delimiter = ',')]
pub factory_deps: Vec<Bytes>,

// TODO: fix custom signature serialization and then add this field
// /// Custom signature for the ZKSync transaction
// #[arg(long = "zk-custom-signature", value_parser = parse_hex_bytes)]
// pub custom_signature: Option<Bytes>,
/// Gas per pubdata for the ZKSync transaction
#[arg(long = "zk-gas-per-pubdata")]
pub gas_per_pubdata: Option<U256>,
}

fn parse_hex_bytes(s: &str) -> Result<Bytes, String> {
hex::decode(s).map(Bytes::from).map_err(|e| format!("Invalid hex string: {e}"))
}

impl ZkTransactionOpts {
pub fn has_zksync_args(&self) -> bool {
self.paymaster_address.is_some()
|| !self.factory_deps.is_empty()
// TODO: add this when fixing serialization || self.custom_signature.is_some()
|| self.gas_per_pubdata.is_some()
}
}

/// Estimates gas for a ZkSync transaction
pub async fn estimate_gas(
zk_tx: ZkTransactionOpts,
config: &Config,
evm_tx: WithOtherFields<TransactionRequest>,
zk_code: Option<String>,
config: &Config,
) -> Result<u64> {
let zk_provider = utils::get_provider_zksync(config)?;
let is_create = evm_tx.to == Some(TxKind::Create);
let mut tx: ZkTransactionRequest = evm_tx.inner.clone().into();
if let Some(gas_per_pubdata) = zk_tx.gas_per_pubdata {
tx.set_gas_per_pubdata(gas_per_pubdata)
}

// TODO: Fix custom_signature serialization and then add this field
// if let Some(custom_signature) = &zk_tx.custom_signature {
// tx.set_custom_signature(custom_signature.clone());
// }

if let (Some(paymaster), Some(paymaster_input)) =
(zk_tx.paymaster_address, zk_tx.paymaster_input.clone())
{
tx.set_paymaster_params(PaymasterParams { paymaster, paymaster_input });
}

if is_create {
let evm_input: Vec<u8> = tx.input().cloned().map(|bytes| bytes.into()).unwrap_or_default();
let zk_code_decoded = hex::decode(zk_code.unwrap_or_default())?;
// constructor input gets appended to the bytecode
let zk_input = &evm_input[zk_code_decoded.len()..];
tx = tx.with_create_params(
zk_code_decoded,
zk_input.to_vec(),
zk_tx.factory_deps.into_iter().map(|v| v.into()).collect(),
)?;
} else {
tx.set_factory_deps(zk_tx.factory_deps.clone());
}

// TODO: Check if alloy calls this for estimate_gas. If so, we do not need this.
tx.prep_for_submission();
let tx = zk_tx.build_base_tx(evm_tx, zk_code)?;
Ok(zk_provider.estimate_gas(&tx).await?)
}
26 changes: 24 additions & 2 deletions crates/cast/bin/cmd/mktx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ use crate::tx::{self, CastTxBuilder};
use alloy_network::{eip2718::Encodable2718, EthereumWallet, TransactionBuilder};
use alloy_primitives::hex;
use alloy_signer::Signer;
use alloy_zksync::wallet::ZksyncWallet;
use cast::ZkTransactionOpts;
use clap::Parser;
use eyre::Result;
use foundry_cli::{
Expand All @@ -11,6 +13,8 @@ use foundry_cli::{
use foundry_common::ens::NameOrAddress;
use std::{path::PathBuf, str::FromStr};

mod zksync;

/// CLI arguments for `cast mktx`.
#[derive(Debug, Parser)]
pub struct MakeTxArgs {
Expand Down Expand Up @@ -44,6 +48,13 @@ pub struct MakeTxArgs {

#[command(flatten)]
eth: EthereumOpts,
/// Zksync Transaction
#[command(flatten)]
zk_tx: ZkTransactionOpts,

/// Force a zksync eip-712 transaction and apply CREATE overrides
#[arg(long = "zksync")]
zk_force: bool,
}

#[derive(Debug, Parser)]
Expand All @@ -64,16 +75,18 @@ pub enum MakeTxSubcommands {

impl MakeTxArgs {
pub async fn run(self) -> Result<()> {
let Self { to, mut sig, mut args, command, tx, path, eth } = self;
let Self { to, mut sig, mut args, command, tx, path, eth, zk_tx, zk_force } = self;

let blob_data = if let Some(path) = path { Some(std::fs::read(path)?) } else { None };

let mut zkcode = Default::default();
let code = if let Some(MakeTxSubcommands::Create {
code,
sig: constructor_sig,
args: constructor_args,
}) = command
{
zkcode = code.clone();
sig = constructor_sig;
args = constructor_args;
Some(code)
Expand All @@ -98,12 +111,21 @@ impl MakeTxArgs {
.with_code_sig_and_args(code, sig, args)
.await?
.with_blob_data(blob_data)?
.build(&signer)
.build_raw(&signer)
.await?;

if zk_tx.has_zksync_args() || zk_force {
let zk_wallet = ZksyncWallet::new(signer);
let zktx = zksync::build_tx(zk_tx, tx, zkcode, &config).await?;
let signed = zktx.build(&zk_wallet).await?.encoded_2718();
sh_println!("0x{}", hex::encode(signed))?;
return Ok(());
}

let tx = tx.build(&EthereumWallet::new(signer)).await?;

let signed_tx = hex::encode(tx.encoded_2718());

sh_println!("0x{signed_tx}")?;

Ok(())
Expand Down
29 changes: 29 additions & 0 deletions crates/cast/bin/cmd/mktx/zksync.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
use alloy_network::TransactionBuilder;
use alloy_rpc_types::TransactionRequest;
use alloy_serde::WithOtherFields;
use alloy_zksync::{
network::transaction_request::TransactionRequest as ZkTransactionRequest,
provider::ZksyncProvider,
};
use cast::ZkTransactionOpts;
use eyre::Result;
use foundry_cli::utils;
use foundry_config::Config;

/// Builds a complete ZkSync transaction request with fee estimation
pub async fn build_tx(
zk_tx: ZkTransactionOpts,
evm_tx: WithOtherFields<TransactionRequest>,
zk_code: String,
config: &Config,
) -> Result<ZkTransactionRequest> {
let zk_provider = utils::get_provider_zksync(config)?;
let mut tx = zk_tx.build_base_tx(evm_tx, Some(zk_code))?;

let fee = ZksyncProvider::estimate_fee(&zk_provider, tx.clone()).await?;
tx.set_max_fee_per_gas(fee.max_fee_per_gas);
tx.set_max_priority_fee_per_gas(fee.max_priority_fee_per_gas);
tx.set_gas_limit(fee.gas_limit);

Ok(tx)
}
2 changes: 2 additions & 0 deletions crates/cast/bin/tx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,7 @@ where
}

if !fill {
self.tx.nonce = Some(tx_nonce);
return Ok((self.tx, self.state.func));
}

Expand Down Expand Up @@ -370,6 +371,7 @@ where
self.tx.gas = Some(self.provider.estimate_gas(&self.tx).await?);
}

self.tx.nonce = Some(tx_nonce);
Ok((self.tx, self.state.func))
}

Expand Down
80 changes: 78 additions & 2 deletions crates/cast/src/zksync.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,91 @@
//! Contains zksync specific logic for foundry's `cast` functionality
use alloy_network::AnyNetwork;
use alloy_network::{AnyNetwork, TransactionBuilder};
use alloy_primitives::{hex, Address, Bytes, TxKind, U256};
use alloy_provider::{PendingTransactionBuilder, Provider};
use alloy_rpc_types::TransactionRequest;
use alloy_serde::WithOtherFields;
use alloy_transport::Transport;
use alloy_zksync::network::{
transaction_request::TransactionRequest as ZkTransactionRequest, Zksync,
transaction_request::TransactionRequest as ZkTransactionRequest,
unsigned_tx::eip712::PaymasterParams, Zksync,
};
use clap::{command, Parser};
use eyre::Result;

use crate::Cast;

#[derive(Clone, Debug, Parser)]
#[command(next_help_heading = "Transaction options")]
pub struct ZkTransactionOpts {
/// Paymaster address for the ZKSync transaction
#[arg(long = "zk-paymaster-address", requires = "paymaster_input")]
pub paymaster_address: Option<Address>,

/// Paymaster input for the ZKSync transaction
#[arg(long = "zk-paymaster-input", requires = "paymaster_address", value_parser = parse_hex_bytes)]
pub paymaster_input: Option<Bytes>,

/// Factory dependencies for the ZKSync transaction
#[arg(long = "zk-factory-deps", value_parser = parse_hex_bytes, value_delimiter = ',')]
pub factory_deps: Vec<Bytes>,

/// Gas per pubdata for the ZKSync transaction
#[arg(long = "zk-gas-per-pubdata")]
pub gas_per_pubdata: Option<U256>,
}

fn parse_hex_bytes(s: &str) -> Result<Bytes, String> {
hex::decode(s).map(Bytes::from).map_err(|e| format!("Invalid hex string: {e}"))
}

impl ZkTransactionOpts {
pub fn has_zksync_args(&self) -> bool {
self.paymaster_address.is_some() ||
!self.factory_deps.is_empty() ||
self.gas_per_pubdata.is_some()
}

/// Builds a base ZkSync transaction request from the common parameters
pub fn build_base_tx(
&self,
evm_tx: WithOtherFields<TransactionRequest>,
zk_code: Option<String>,
) -> Result<ZkTransactionRequest> {
let is_create = evm_tx.to == Some(TxKind::Create);
let mut tx: ZkTransactionRequest = evm_tx.inner.into();

if let Some(gas_per_pubdata) = self.gas_per_pubdata {
tx.set_gas_per_pubdata(gas_per_pubdata);
}

if let (Some(paymaster), Some(paymaster_input)) =
(self.paymaster_address, self.paymaster_input.clone())
{
tx.set_paymaster_params(PaymasterParams { paymaster, paymaster_input });
}

if is_create {
let input_data = tx.input().cloned().unwrap_or_default().to_vec();
let zk_code = zk_code
.ok_or_else(|| eyre::eyre!("ZkSync code is required for contract creation"))?;
let zk_code_bytes = hex::decode(zk_code)?;
let constructor_args = &input_data[zk_code_bytes.len()..];

tx = tx.with_create_params(
zk_code_bytes,
constructor_args.to_vec(),
self.factory_deps.iter().map(|b| b.to_vec()).collect(),
)?;
} else {
tx.set_factory_deps(self.factory_deps.clone());
}

tx.prep_for_submission();
Ok(tx)
}
}

pub struct ZkCast<P, T, Z> {
provider: Z,
inner: Cast<P, T>,
Expand Down
Loading

0 comments on commit 4f89e2f

Please sign in to comment.