stylus_tools/core/deployment/
mod.rs

1// Copyright 2025, Offchain Labs, Inc.
2// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
3
4use crate::core::activation;
5use crate::core::activation::ActivationError;
6use crate::core::cache::format_gas;
7use crate::core::deployment::deployer::{
8    get_address_from_receipt, parse_tx_calldata, DeployerError,
9};
10use crate::core::deployment::DeploymentError::{
11    InvalidConstructor, NoContractAddress, ReadConstructorFailure,
12};
13use crate::ops::activate::print_gas_estimate;
14use crate::ops::get_constructor_signature;
15use crate::{
16    core::{
17        check::{check_contract, CheckConfig},
18        project::contract::{Contract, ContractStatus},
19    },
20    utils::color::{Color, DebugColor},
21};
22use alloy::json_abi::StateMutability::Payable;
23use alloy::{
24    network::TransactionBuilder,
25    primitives::{Address, TxHash, U256},
26    providers::{Provider, WalletProvider},
27    rpc::types::{TransactionReceipt, TransactionRequest},
28};
29
30use alloy::primitives::B256;
31use prelude::DeploymentCalldata;
32
33pub mod deployer;
34pub mod prelude;
35
36#[derive(Debug, Default)]
37pub struct DeploymentConfig {
38    pub check: CheckConfig,
39    pub max_fee_per_gas_gwei: Option<u128>,
40    pub estimate_gas: bool,
41    pub no_activate: bool,
42    pub constructor_value: U256,
43    pub deployer_address: Address,
44    pub constructor_args: Vec<String>,
45    pub deployer_salt: B256,
46}
47
48#[derive(Debug)]
49pub struct DeploymentRequest {
50    tx: TransactionRequest,
51    max_fee_per_gas_wei: Option<u128>,
52}
53
54impl DeploymentRequest {
55    pub fn new_with_args(
56        sender: Address,
57        deployer: Address,
58        tx_value: U256,
59        tx_calldata: Vec<u8>,
60        max_fee_per_gas_wei: Option<u128>,
61    ) -> Self {
62        Self {
63            tx: TransactionRequest::default()
64                .with_to(deployer)
65                .with_from(sender)
66                .with_value(tx_value)
67                .with_input(tx_calldata),
68            max_fee_per_gas_wei,
69        }
70    }
71    pub fn new(sender: Address, code: &[u8], max_fee_per_gas_wei: Option<u128>) -> Self {
72        Self {
73            tx: TransactionRequest::default()
74                .with_from(sender)
75                .with_deploy_code(DeploymentCalldata::new(code)),
76            max_fee_per_gas_wei,
77        }
78    }
79
80    pub async fn estimate_gas(&self, provider: &impl Provider) -> Result<u64, DeploymentError> {
81        Ok(provider.estimate_gas(self.tx.clone()).await?)
82    }
83
84    pub async fn exec(
85        self,
86        provider: &impl Provider,
87    ) -> Result<TransactionReceipt, DeploymentError> {
88        let gas = self.estimate_gas(provider).await?;
89        let max_fee_per_gas = self.fee_per_gas(provider).await?;
90
91        let mut tx = self.tx;
92        tx.gas = Some(gas);
93        tx.max_fee_per_gas = Some(max_fee_per_gas);
94        tx.max_priority_fee_per_gas = Some(0);
95
96        let tx = provider.send_transaction(tx).await?;
97        let tx_hash = *tx.tx_hash();
98        debug!(@grey, "sent deploy tx: {}", tx_hash.debug_lavender());
99
100        let receipt = tx
101            .get_receipt()
102            .await
103            .or(Err(DeploymentError::FailedToComplete))?;
104        if !receipt.status() {
105            return Err(DeploymentError::Reverted { tx_hash });
106        }
107
108        Ok(receipt)
109    }
110
111    async fn fee_per_gas(&self, provider: &impl Provider) -> Result<u128, DeploymentError> {
112        match self.max_fee_per_gas_wei {
113            Some(wei) => Ok(wei),
114            None => Ok(provider.get_gas_price().await?),
115        }
116    }
117}
118
119#[derive(Debug, thiserror::Error)]
120pub enum DeploymentError {
121    #[error("rpc error: {0}")]
122    Rpc(#[from] alloy::transports::RpcError<alloy::transports::TransportErrorKind>),
123
124    #[error("{0}")]
125    Check(#[from] crate::core::check::CheckError),
126
127    #[error("tx failed to complete")]
128    FailedToComplete,
129    #[error("failed to get balance")]
130    FailedToGetBalance,
131    #[error(
132        "not enough funds in account {} to pay for data fee\n\
133         balance {} < {}\n\
134         please see the Quickstart guide for funding new accounts:\n{}",
135        .from_address.red(),
136        .balance.red(),
137        format!("{} wei", .data_fee).red(),
138        "https://docs.arbitrum.io/stylus/stylus-quickstart".yellow(),
139    )]
140    NotEnoughFunds {
141        from_address: Address,
142        balance: U256,
143        data_fee: U256,
144    },
145    #[error("deploy tx reverted {}", .tx_hash.debug_red())]
146    Reverted { tx_hash: TxHash },
147    #[error("{0}")]
148    DeployerFailure(#[from] DeployerError),
149    #[error("{0}")]
150    ActivationFailure(#[from] ActivationError),
151    #[error("missing address: {0}")]
152    NoContractAddress(String),
153    #[error("failed to get constructor signature")]
154    ReadConstructorFailure,
155    #[error("invalid constructor: {0}")]
156    InvalidConstructor(String),
157}
158
159/// Deploys a stylus contract, activating if needed.
160pub async fn deploy(
161    contract: &Contract,
162    config: &DeploymentConfig,
163    provider: &(impl Provider + WalletProvider),
164) -> Result<(), DeploymentError> {
165    let status = check_contract(contract, None, &config.check, provider).await?;
166    let from_address = provider.default_signer_address();
167    debug!(@grey, "sender address: {}", from_address.debug_lavender());
168    let data_fee = status.suggest_fee() + config.constructor_value;
169
170    if let ContractStatus::Ready { .. } = status {
171        // check balance early
172        let balance = provider
173            .get_balance(from_address)
174            .await
175            .map_err(|_| DeploymentError::FailedToGetBalance)?;
176        if balance < data_fee {
177            return Err(DeploymentError::NotEnoughFunds {
178                from_address,
179                balance,
180                data_fee,
181            });
182        }
183    }
184
185    let constructor = get_constructor_signature(contract.package.name.as_str())
186        .map_err(|_| ReadConstructorFailure)?;
187
188    let req = match &constructor {
189        None => DeploymentRequest::new(from_address, status.code(), config.max_fee_per_gas_gwei),
190        Some(constructor) => {
191            if constructor.state_mutability != Payable && !config.constructor_value.is_zero() {
192                return Err(InvalidConstructor(
193                    "attempting to send Ether to non-payable constructor".to_string(),
194                ));
195            }
196            if config.constructor_args.len() != constructor.inputs.len() {
197                return Err(InvalidConstructor(format!(
198                    "mismatch number of constructor arguments (want {:?} ({}); got {})",
199                    constructor.inputs,
200                    constructor.inputs.len(),
201                    config.constructor_args.len(),
202                )));
203            }
204
205            let tx_calldata = parse_tx_calldata(
206                status.code(),
207                constructor,
208                config.constructor_value,
209                config.constructor_args.clone(),
210                config.deployer_salt,
211                &provider,
212            )
213            .await
214            .map_err(|err| InvalidConstructor(err.to_string()))?;
215
216            DeploymentRequest::new_with_args(
217                from_address,
218                config.deployer_address,
219                data_fee,
220                tx_calldata,
221                config.max_fee_per_gas_gwei,
222            )
223        }
224    };
225
226    if config.estimate_gas {
227        let gas = req
228            .estimate_gas(&provider)
229            .await
230            .or(Err(DeployerError::GasEstimationFailure))?;
231        let gas_price = req
232            .fee_per_gas(&provider)
233            .await
234            .or(Err(DeployerError::GasEstimationFailure))?;
235        print_gas_estimate("deployment", gas, gas_price)
236            .or(Err(DeployerError::GasEstimationFailure))?;
237        // TODO: Is this part needed?
238        let nonce = provider.get_transaction_count(from_address).await?;
239        let _ = from_address.create(nonce);
240        return Ok(());
241    }
242    let receipt = req.exec(&provider).await?;
243
244    let contract_addr = match &constructor {
245        None => receipt
246            .contract_address
247            .ok_or(NoContractAddress("in receipt".to_string())),
248        Some(_) => get_address_from_receipt(&receipt),
249    }?;
250
251    info!(@grey, "deployed code at address: {}", contract_addr.debug_lavender());
252    debug!(@grey, "gas used: {}", format_gas(receipt.gas_used.into()));
253    info!(@grey, "deployment tx hash: {}", receipt.transaction_hash.debug_lavender());
254
255    if constructor.is_none() {
256        if matches!(status, ContractStatus::Active { .. }) {
257            greyln!("wasm already activated!")
258        } else if config.no_activate {
259            mintln!(
260                r#"NOTE:
261            You must activate the stylus contract before calling it. To do so, we recommend running:
262            cargo stylus activate --address {}"#,
263                hex::encode(contract_addr)
264            )
265        } else {
266            activation::activate_contract(contract_addr, &config.check.activation, provider)
267                .await?;
268        }
269    }
270
271    mintln!(
272        r#"NOTE:
273        We recommend running cargo stylus cache bid {} 0 to cache your activated contract in ArbOS.
274        Cached contracts benefit from cheaper calls.
275        To read more about the Stylus contract cache, see:
276        https://docs.arbitrum.io/stylus/how-tos/caching-contracts"#,
277        hex::encode(contract_addr)
278    );
279
280    Ok(())
281}