Skip to main content

forge/cmd/
create.rs

1use crate::cmd::install;
2use alloy_chains::Chain;
3use alloy_consensus::{SignableTransaction, Signed};
4use alloy_dyn_abi::{DynSolValue, JsonAbiExt, Specifier};
5use alloy_json_abi::{Constructor, JsonAbi};
6use alloy_network::{Ethereum, EthereumWallet, Network, ReceiptResponse, TransactionBuilder};
7use alloy_primitives::{Address, Bytes, U256, hex};
8use alloy_provider::{PendingTransactionError, Provider, ProviderBuilder as AlloyProviderBuilder};
9use alloy_signer::{Signature, Signer};
10use alloy_transport::TransportError;
11use clap::{Parser, ValueHint};
12use eyre::{Context, ContextCompat, Result};
13use forge_verify::{RetryArgs, VerifierArgs, VerifyArgs};
14use foundry_cli::{
15    opts::{BuildOpts, EthereumOpts, EtherscanOpts, TransactionOpts},
16    utils::{
17        LoadConfig, ResolvedLane, find_contract_artifacts, maybe_print_resolved_lane,
18        read_constructor_args_file, resolve_lane,
19    },
20};
21use foundry_common::{
22    FoundryTransactionBuilder,
23    compile::{self},
24    fmt::parse_tokens,
25    provider::ProviderBuilder,
26    shell,
27    tempo::{TEMPO_BROWSER_GAS_BUFFER, print_resolved_fee_token_selection},
28};
29use foundry_compilers::{
30    ArtifactId, artifacts::BytecodeObject, info::ContractInfo, utils::canonicalize,
31};
32use foundry_config::{
33    Config,
34    figment::{
35        self, Metadata, Profile,
36        value::{Dict, Map},
37    },
38    merge_impl_figment_convert,
39};
40use foundry_wallets::{
41    BrowserWalletOpts, TempoAccessKeyConfig, WalletSigner, wallet_browser::signer::BrowserSigner,
42};
43use serde_json::json;
44use std::{borrow::Borrow, marker::PhantomData, path::PathBuf, sync::Arc, time::Duration};
45use tempo_alloy::TempoNetwork;
46
47merge_impl_figment_convert!(CreateArgs, build, eth);
48
49/// CLI arguments for `forge create`.
50#[derive(Clone, Debug, Parser)]
51pub struct CreateArgs {
52    /// The contract identifier in the form `<path>:<contractname>`.
53    contract: ContractInfo,
54
55    /// The constructor arguments.
56    #[arg(
57        long,
58        num_args(1..),
59        conflicts_with = "constructor_args_path",
60        value_name = "ARGS",
61        allow_hyphen_values = true,
62    )]
63    constructor_args: Vec<String>,
64
65    /// The path to a file containing the constructor arguments.
66    #[arg(
67        long,
68        value_hint = ValueHint::FilePath,
69        value_name = "PATH",
70    )]
71    constructor_args_path: Option<PathBuf>,
72
73    /// Broadcast the transaction.
74    #[arg(long)]
75    pub broadcast: bool,
76
77    /// Verify contract after creation.
78    #[arg(long)]
79    verify: bool,
80
81    /// Send via `eth_sendTransaction` using the `--from` argument or `$ETH_FROM` as sender
82    #[arg(long, requires = "from")]
83    unlocked: bool,
84
85    /// Prints the standard json compiler input if `--verify` is provided.
86    ///
87    /// The standard json compiler input can be used to manually submit contract verification in
88    /// the browser.
89    #[arg(long, requires = "verify")]
90    show_standard_json_input: bool,
91
92    /// Timeout to use for broadcasting transactions.
93    #[arg(long, env = "ETH_TIMEOUT")]
94    pub timeout: Option<u64>,
95
96    #[command(flatten)]
97    build: BuildOpts,
98
99    #[command(flatten)]
100    tx: TransactionOpts,
101
102    #[command(flatten)]
103    eth: EthereumOpts,
104
105    #[command(flatten)]
106    pub verifier: VerifierArgs,
107
108    #[command(flatten)]
109    retry: RetryArgs,
110
111    /// Browser wallet options
112    #[command(flatten)]
113    browser: BrowserWalletOpts,
114}
115
116impl CreateArgs {
117    /// Executes the command to create a contract
118    pub async fn run(mut self) -> Result<()> {
119        let (signer, tempo_access_key) = self.eth.wallet.maybe_signer().await?;
120
121        // Resolve chain early so we can dispatch to the correct network type.
122        if self.chain_id().is_none() {
123            let config = self.load_config()?;
124            let provider = ProviderBuilder::<Ethereum>::from_config(&config)?.build()?;
125            let chain_id = provider.get_chain_id().await?;
126            self.eth.etherscan.chain = Some(chain_id.into());
127        }
128
129        if tempo_access_key.is_some()
130            || self.tx.tempo.is_tempo()
131            || self.chain_id().is_some_and(|c| c.is_tempo())
132        {
133            self.run_generic::<TempoNetwork>(signer, tempo_access_key).await
134        } else {
135            self.run_generic::<Ethereum>(signer, None).await
136        }
137    }
138
139    async fn run_generic<N: Network>(
140        mut self,
141        pre_resolved_signer: Option<WalletSigner>,
142        access_key: Option<TempoAccessKeyConfig>,
143    ) -> Result<()>
144    where
145        N::TxEnvelope: From<Signed<N::UnsignedTx>>,
146        N::UnsignedTx: SignableTransaction<Signature>,
147        N::TransactionRequest: FoundryTransactionBuilder<N> + serde::Serialize,
148        N::ReceiptResponse: serde::Serialize,
149    {
150        let mut config = self.load_config()?;
151
152        // Install missing dependencies.
153        if install::install_missing_dependencies(&mut config).await && config.auto_detect_remappings
154        {
155            // need to re-configure here to also catch additional remappings
156            config = self.load_config()?;
157        }
158
159        // Find Project & Compile
160        let project = config.project()?;
161
162        let target_path = if let Some(ref mut path) = self.contract.path {
163            canonicalize(project.root().join(path))?
164        } else {
165            project.find_contract_path(&self.contract.name)?
166        };
167
168        let output = compile::compile_target(&target_path, &project, shell::is_json())?;
169
170        let (abi, bin, id) = find_contract_artifacts(output, &target_path, &self.contract.name)?;
171
172        let bin = match bin.object {
173            BytecodeObject::Bytecode(_) => bin.object,
174            _ => {
175                let link_refs = bin
176                    .link_references
177                    .iter()
178                    .flat_map(|(path, names)| {
179                        names.keys().map(move |name| format!("\t{name}: {path}"))
180                    })
181                    .collect::<Vec<String>>()
182                    .join("\n");
183                eyre::bail!(
184                    "Dynamic linking not supported in `create` command - deploy the following library contracts first, then provide the address to link at compile time\n{}",
185                    link_refs
186                )
187            }
188        };
189
190        // Add arguments to constructor
191        let params = if let Some(constructor) = &abi.constructor {
192            let constructor_args =
193                self.constructor_args_path.clone().map(read_constructor_args_file).transpose()?;
194            self.parse_constructor_args(
195                constructor,
196                constructor_args.as_deref().unwrap_or(&self.constructor_args),
197            )?
198        } else {
199            if !self.constructor_args.is_empty() || self.constructor_args_path.is_some() {
200                sh_warn!(
201                    "`{}` has no constructor; ignoring provided constructor arguments",
202                    self.contract.name
203                )?;
204            }
205            vec![]
206        };
207
208        let provider = ProviderBuilder::<N>::from_config(&config)?.build()?;
209
210        // Inject access key ID into TempoOpts so it's set before gas estimation.
211        if let Some(ref ak) = access_key {
212            self.tx.tempo.key_id = Some(ak.key_address);
213        }
214
215        // Resolve `--tempo.lane <name>` against the lanes file (default
216        // `<root>/tempo.lanes.toml`) and populate `self.tx.tempo.nonce_key` from the lane.
217        // Must happen before `self.deploy(...)` so `TempoOpts::apply` picks up the nonce_key.
218        let resolved_lane = resolve_lane(&mut self.tx.tempo, &config.root)?;
219        let expires_at = self.tx.tempo.resolve_expires();
220
221        // Whether to broadcast the transaction or not
222        let dry_run = !self.broadcast;
223
224        // Launch browser signer if `--browser` flag is set
225        let browser = self.browser.run::<N>().await?;
226
227        if let Some(browser) = browser {
228            // Deploy with browser wallet
229            let deployer_address = browser.address();
230            self.deploy(
231                abi,
232                bin,
233                params,
234                provider,
235                deployer_address,
236                config.transaction_timeout,
237                id,
238                dry_run,
239                None,
240                Some(browser),
241                resolved_lane,
242                expires_at,
243            )
244            .await
245        } else if self.unlocked {
246            // Deploy with unlocked account
247            let sender = self.eth.wallet.from.expect("required");
248            self.deploy(
249                abi,
250                bin,
251                params,
252                provider,
253                sender,
254                config.transaction_timeout,
255                id,
256                dry_run,
257                None,
258                None,
259                resolved_lane,
260                expires_at,
261            )
262            .await
263        } else if let Some(ak) = access_key {
264            // Tempo keychain mode: sign with access key and send raw
265            let signer = match pre_resolved_signer {
266                Some(s) => s,
267                None => self.eth.wallet.signer().await?,
268            };
269            let deployer_address = ak.wallet_address;
270            self.deploy(
271                abi,
272                bin,
273                params,
274                provider,
275                deployer_address,
276                config.transaction_timeout,
277                id,
278                dry_run,
279                Some((signer, ak)),
280                None,
281                resolved_lane,
282                expires_at,
283            )
284            .await
285        } else {
286            // Deploy with signer
287            let signer = match pre_resolved_signer {
288                Some(s) => s,
289                None => self.eth.wallet.signer().await?,
290            };
291            let deployer = signer.address();
292            let provider = AlloyProviderBuilder::<_, _, N>::default()
293                .wallet(EthereumWallet::new(signer))
294                .connect_provider(provider);
295            self.deploy(
296                abi,
297                bin,
298                params,
299                provider,
300                deployer,
301                config.transaction_timeout,
302                id,
303                dry_run,
304                None,
305                None,
306                resolved_lane,
307                expires_at,
308            )
309            .await
310        }
311    }
312
313    /// Returns the resolved chain, if any.
314    const fn chain_id(&self) -> Option<Chain> {
315        self.eth.etherscan.chain
316    }
317
318    /// Ensures the verify command can be executed.
319    ///
320    /// This is supposed to check any things that might go wrong when preparing a verify request
321    /// before the contract is deployed. This should prevent situations where a contract is deployed
322    /// successfully, but we fail to prepare a verify request which would require manual
323    /// verification.
324    async fn verify_preflight_check(
325        &self,
326        constructor_args: Option<String>,
327        id: &ArtifactId,
328    ) -> Result<()> {
329        // NOTE: this does not represent the same `VerifyArgs` that would be sent after deployment,
330        // since we don't know the address yet.
331        let mut verify = VerifyArgs {
332            address: Default::default(),
333            contract: Some(self.contract.clone()),
334            compiler_version: Some(id.version.to_string()),
335            constructor_args,
336            constructor_args_path: None,
337            no_auto_detect: false,
338            use_solc: None,
339            num_of_optimizations: None,
340            etherscan: EtherscanOpts {
341                key: self.eth.etherscan.key.clone(),
342                chain: self.chain_id(),
343            },
344            rpc: Default::default(),
345            flatten: false,
346            force: false,
347            skip_is_verified_check: true,
348            watch: true,
349            retry: self.retry,
350            libraries: self.build.libraries.clone(),
351            root: None,
352            verifier: self.verifier.clone(),
353            via_ir: self.build.via_ir,
354            license_type: None,
355            evm_version: self.build.compiler.evm_version,
356            show_standard_json_input: self.show_standard_json_input,
357            guess_constructor_args: false,
358            compilation_profile: Some(id.profile.clone()),
359            language: None,
360            creation_transaction_hash: None,
361        };
362
363        // Check config for Etherscan API Keys to avoid preflight check failing if no
364        // ETHERSCAN_API_KEY value set.
365        let config = verify.load_config()?;
366        verify.etherscan.key = config
367            .get_etherscan_config_with_chain(self.chain_id())?
368            .map(|c| c.key)
369            .or_else(|| config.etherscan_api_key.clone());
370
371        let context = verify.resolve_context().await?;
372
373        verify.verification_provider()?.preflight_verify_check(verify.clone(), context).await?;
374
375        let api_key = verify.verifier.resolve_api_key(verify.etherscan.key.as_deref());
376        let chain = verify.etherscan.chain.context("chain ID not resolved")?;
377        verify
378            .verifier
379            .check_credentials(api_key, chain, &config)
380            .await
381            .wrap_err("Verification preflight check failed")?;
382
383        Ok(())
384    }
385
386    /// Deploys the contract
387    #[expect(clippy::too_many_arguments)]
388    async fn deploy<N: Network, P: Provider<N>>(
389        self,
390        abi: JsonAbi,
391        bin: BytecodeObject,
392        args: Vec<DynSolValue>,
393        provider: P,
394        deployer_address: Address,
395        timeout: u64,
396        id: ArtifactId,
397        dry_run: bool,
398        tempo_keychain: Option<(WalletSigner, TempoAccessKeyConfig)>,
399        browser_signer: Option<BrowserSigner<N>>,
400        resolved_lane: Option<ResolvedLane>,
401        expires_at: Option<u64>,
402    ) -> Result<()>
403    where
404        N::TransactionRequest: FoundryTransactionBuilder<N> + serde::Serialize,
405        N::ReceiptResponse: serde::Serialize,
406    {
407        let chain = self.chain_id().context("chain ID not resolved")?;
408
409        let bin = bin.into_bytes().unwrap_or_default();
410        if bin.is_empty() {
411            eyre::bail!("no bytecode found in bin object for {}", self.contract.name)
412        }
413
414        let provider = Arc::new(provider);
415        let factory =
416            ContractFactory::<N, _>::new(abi.clone(), bin.clone(), provider.clone(), timeout);
417
418        let is_args_empty = args.is_empty();
419        let mut deployer =
420            factory.deploy_tokens(args.clone()).context("failed to deploy contract").map_err(|e| {
421                if is_args_empty {
422                    e.wrap_err("no arguments provided for contract constructor; consider --constructor-args or --constructor-args-path")
423                } else {
424                    e
425                }
426            })?;
427        let is_legacy = self.tx.legacy || chain.is_legacy();
428
429        deployer.tx.set_from(deployer_address);
430        deployer.tx.set_chain_id(chain.id());
431        // `to` field must be set explicitly, cannot be None.
432        if deployer.tx.to().is_none() {
433            deployer.tx.set_create();
434        }
435
436        // Apply user-provided gas, fee, nonce, and Tempo options.
437        self.tx.apply::<N>(&mut deployer.tx, is_legacy);
438
439        // Convert the CREATE into an AA-compatible call entry since Tempo AA
440        // transactions use a `calls` list instead of `to`+`input`.
441        if chain.is_tempo() {
442            deployer.tx.convert_create_to_call();
443        }
444
445        // For keychain mode, set key_id and nonce_key before gas estimation.
446        if let Some((_, ref ak)) = tempo_keychain {
447            deployer.tx.set_key_id(ak.key_address);
448            if deployer.tx.nonce_key().is_none() {
449                deployer.tx.set_nonce_key(U256::ZERO);
450            }
451        }
452
453        // Fetch defaults from provider for values not specified by user.
454        if self.tx.nonce.is_none() && !self.tx.tempo.expiring_nonce {
455            deployer.tx.set_nonce(provider.get_transaction_count(deployer_address).await?);
456        }
457
458        maybe_print_resolved_lane(resolved_lane.as_ref(), deployer.tx.nonce().unwrap_or_default())?;
459
460        if let Some((_, ref ak)) = tempo_keychain {
461            deployer
462                .tx
463                .prepare_access_key_authorization(
464                    provider.as_ref(),
465                    ak.wallet_address,
466                    ak.key_address,
467                    ak.key_authorization.as_ref(),
468                )
469                .await?;
470        }
471
472        if is_legacy {
473            if self.tx.gas_price.is_none() {
474                deployer.tx.set_gas_price(provider.get_gas_price().await?);
475            }
476        } else {
477            if self.tx.gas_price.is_none() || self.tx.priority_gas_price.is_none() {
478                let mut estimate = provider.estimate_eip1559_fees().await.wrap_err("Failed to estimate EIP1559 fees. This chain might not support EIP1559, try adding --legacy to your command.")?;
479                if browser_signer.is_some()
480                    && self.tx.priority_gas_price.is_none()
481                    && let Ok(suggested_tip) = provider.get_max_priority_fee_per_gas().await
482                    && suggested_tip > estimate.max_priority_fee_per_gas
483                {
484                    estimate.max_fee_per_gas += suggested_tip - estimate.max_priority_fee_per_gas;
485                    estimate.max_priority_fee_per_gas = suggested_tip;
486                }
487                if self.tx.priority_gas_price.is_none() {
488                    deployer.tx.set_max_priority_fee_per_gas(estimate.max_priority_fee_per_gas);
489                }
490                if self.tx.gas_price.is_none() {
491                    deployer.tx.set_max_fee_per_gas(estimate.max_fee_per_gas);
492                }
493            }
494            if let (Some(max_fee), Some(priority)) =
495                (deployer.tx.max_fee_per_gas(), deployer.tx.max_priority_fee_per_gas())
496            {
497                eyre::ensure!(
498                    priority <= max_fee,
499                    "max priority fee per gas ({priority}) cannot exceed max fee per gas ({max_fee})"
500                );
501            }
502        }
503
504        // set access list if specified
505        if let Some(access_list) = match self.tx.access_list {
506            None => None,
507            Some(None) => Some(provider.create_access_list(&deployer.tx).await?.access_list),
508            Some(Some(ref access_list)) => Some(access_list.clone()),
509        } {
510            deployer.tx.set_access_list(access_list);
511        }
512
513        if self.tx.gas_limit.is_none() {
514            let mut estimated = provider.estimate_gas(deployer.tx.clone()).await?;
515
516            // Browser wallets may sign with P256/WebAuthn instead of secp256k1, which
517            // costs more gas for signature verification on Tempo chains. Add a
518            // conservative buffer since we can't determine the signature type beforehand.
519            if browser_signer.is_some() && chain.is_tempo() {
520                estimated += TEMPO_BROWSER_GAS_BUFFER;
521            }
522
523            deployer.tx.set_gas_limit(estimated);
524        }
525
526        // Before we actually deploy the contract we try check if the verify settings are valid
527        let mut constructor_args = None;
528        if self.verify {
529            if !args.is_empty() {
530                let encoded_args = abi
531                    .constructor()
532                    .ok_or_else(|| eyre::eyre!("could not find constructor"))?
533                    .abi_encode_input(&args)?;
534                constructor_args = Some(hex::encode(encoded_args));
535            }
536
537            self.verify_preflight_check(constructor_args.clone(), &id).await?;
538        }
539
540        if dry_run {
541            if shell::is_json() {
542                let output = json!({
543                    "contract": self.contract.name,
544                    "transaction": &deployer.tx,
545                    "abi":&abi
546                });
547                sh_println!("{}", serde_json::to_string_pretty(&output)?)?;
548            } else {
549                sh_warn!("Dry run enabled, not broadcasting transaction\n")?;
550
551                sh_println!("Contract: {}", self.contract.name)?;
552                sh_println!(
553                    "Transaction: {}",
554                    serde_json::to_string_pretty(&deployer.tx.clone())?
555                )?;
556                sh_println!("ABI: {}\n", serde_json::to_string_pretty(&abi)?)?;
557
558                sh_warn!(
559                    "To broadcast this transaction, add --broadcast to the previous command. See forge create --help for more."
560                )?;
561            }
562
563            return Ok(());
564        }
565
566        if let Some(ts) = expires_at {
567            sh_status!("Transaction expires at unix timestamp {ts}")?;
568        }
569
570        let tempo_sponsor = self.tx.tempo.sponsor_config().await?;
571        if let Some(sponsor) = &tempo_sponsor {
572            sponsor.attach_and_print::<N>(&mut deployer.tx, deployer_address).await?;
573        }
574        print_resolved_fee_token_selection(Some(chain), deployer.tx.fee_token())?;
575
576        // Deploy the actual contract
577        let (deployed_contract, receipt) = if let Some(browser) = browser_signer {
578            // Browser wallet signs and sends the transaction
579            let tx_hash = browser.send_transaction_via_browser(deployer.tx).await?;
580
581            // Wait for the transaction to be confirmed, then fetch the receipt.
582            provider
583                .watch_pending_transaction(alloy_provider::PendingTransactionConfig::new(tx_hash))
584                .await?
585                .await?;
586
587            let receipt = provider
588                .get_transaction_receipt(tx_hash)
589                .await?
590                .ok_or_else(|| eyre::eyre!("could not get transaction receipt for {tx_hash}"))?;
591
592            if !receipt.status() {
593                eyre::bail!("deployment transaction failed (receipt status 0): {tx_hash}");
594            }
595
596            let address = receipt
597                .contract_address()
598                .ok_or_else(|| eyre::eyre!("contract was not deployed"))?;
599
600            (address, receipt)
601        } else if let Some((signer, ak)) = tempo_keychain {
602            // Tempo keychain mode: sign with access key provisioning and send raw
603            let raw_tx = deployer
604                .tx
605                .sign_with_access_key(
606                    &provider,
607                    &signer,
608                    ak.wallet_address,
609                    ak.key_address,
610                    ak.key_authorization.as_ref(),
611                )
612                .await?;
613
614            let receipt = provider
615                .send_raw_transaction(&raw_tx)
616                .await?
617                .with_required_confirmations(1)
618                .with_timeout(Some(Duration::from_secs(timeout)))
619                .get_receipt()
620                .await?;
621
622            let address = receipt
623                .contract_address()
624                .ok_or_else(|| eyre::eyre!("contract was not deployed"))?;
625
626            (address, receipt)
627        } else {
628            deployer.send_with_receipt().await?
629        };
630
631        let address = deployed_contract;
632        let tx_hash = receipt.transaction_hash();
633        if shell::is_json() {
634            let output = json!({
635                "deployer": deployer_address.to_string(),
636                "deployedTo": address.to_string(),
637                "transactionHash": tx_hash
638            });
639            sh_println!("{}", serde_json::to_string_pretty(&output)?)?;
640        } else {
641            sh_println!("Deployer: {deployer_address}")?;
642            sh_println!("Deployed to: {address}")?;
643            sh_println!("Transaction hash: {tx_hash:?}")?;
644        };
645
646        if !self.verify {
647            return Ok(());
648        }
649
650        sh_status!("Starting contract verification...")?;
651
652        let num_of_optimizations = if let Some(optimizer) = self.build.compiler.optimize {
653            optimizer.then(|| self.build.compiler.optimizer_runs.unwrap_or(200))
654        } else {
655            self.build.compiler.optimizer_runs
656        };
657
658        let verify = VerifyArgs {
659            address,
660            contract: Some(self.contract),
661            compiler_version: Some(id.version.to_string()),
662            constructor_args,
663            constructor_args_path: None,
664            no_auto_detect: false,
665            use_solc: None,
666            num_of_optimizations,
667            etherscan: EtherscanOpts { key: self.eth.etherscan.key(), chain: Some(chain) },
668            rpc: Default::default(),
669            flatten: false,
670            force: false,
671            skip_is_verified_check: true,
672            watch: true,
673            retry: self.retry,
674            libraries: self.build.libraries.clone(),
675            root: None,
676            verifier: self.verifier,
677            via_ir: self.build.via_ir,
678            license_type: None,
679            evm_version: self.build.compiler.evm_version,
680            show_standard_json_input: self.show_standard_json_input,
681            guess_constructor_args: false,
682            compilation_profile: Some(id.profile.clone()),
683            language: None,
684            creation_transaction_hash: Some(tx_hash),
685        };
686        // Load the full config (including foundry.toml) so the key used for resolution matches
687        // what `verify.run()` will actually use, preventing a "Waiting for sourcify..." message
688        // when the run will actually use Etherscan (or vice versa for unknown chains).
689        let verify_config = verify.load_config()?;
690        let effective_key = verify_config
691            .get_etherscan_config_with_chain(Some(chain))?
692            .map(|c| c.key)
693            .or_else(|| verify_config.etherscan_api_key.clone());
694        let resolved_verifier = verify.verifier.resolve(effective_key.as_deref(), Some(chain));
695        sh_status!("Waiting for {resolved_verifier} to detect contract deployment...")?;
696        verify.run().await
697    }
698
699    /// Parses the given constructor arguments into a vector of `DynSolValue`s, by matching them
700    /// against the constructor's input params.
701    ///
702    /// Returns a list of parsed values that match the constructor's input params.
703    fn parse_constructor_args(
704        &self,
705        constructor: &Constructor,
706        constructor_args: &[String],
707    ) -> Result<Vec<DynSolValue>> {
708        if constructor.inputs.len() != constructor_args.len() {
709            eyre::bail!(
710                "Constructor argument count mismatch: expected {} but got {}",
711                constructor.inputs.len(),
712                constructor_args.len()
713            );
714        }
715
716        let mut params = Vec::with_capacity(constructor.inputs.len());
717        for (input, arg) in constructor.inputs.iter().zip(constructor_args) {
718            // resolve the input type directly
719            let ty = input
720                .resolve()
721                .wrap_err_with(|| format!("Could not resolve constructor arg: input={input}"))?;
722            params.push((ty, arg));
723        }
724        let params = params.iter().map(|(ty, arg)| (ty, arg.as_str()));
725        parse_tokens(params).map_err(Into::into)
726    }
727}
728
729impl figment::Provider for CreateArgs {
730    fn metadata(&self) -> Metadata {
731        Metadata::named("Create Args Provider")
732    }
733
734    fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
735        let mut dict = Dict::default();
736        if let Some(timeout) = self.timeout {
737            dict.insert("transaction_timeout".to_string(), timeout.into());
738        }
739        Ok(Map::from([(Config::selected_profile(), dict)]))
740    }
741}
742
743/// `ContractFactory` is a [`DeploymentTxFactory`] object with an
744/// [`Arc`] middleware. This type alias exists to preserve backwards
745/// compatibility with less-abstract Contracts.
746///
747/// For full usage docs, see [`DeploymentTxFactory`].
748pub type ContractFactory<N, P> = DeploymentTxFactory<N, P>;
749
750/// Helper which manages the deployment transaction of a smart contract. It
751/// wraps a deployment transaction, and retrieves the contract address output
752/// by it.
753#[derive(Debug)]
754#[must_use = "ContractDeploymentTx does nothing unless you `send` it"]
755pub struct ContractDeploymentTx<N: Network, P, C> {
756    /// the actual deployer, exposed for overriding the defaults
757    pub deployer: Deployer<N, P>,
758    /// marker for the `Contract` type to create afterwards
759    ///
760    /// this type will be used to construct it via `From::from(Contract)`
761    _contract: PhantomData<C>,
762}
763
764impl<N: Network, P: Clone, C> Clone for ContractDeploymentTx<N, P, C> {
765    fn clone(&self) -> Self {
766        Self { deployer: self.deployer.clone(), _contract: self._contract }
767    }
768}
769
770impl<N: Network, P, C> From<Deployer<N, P>> for ContractDeploymentTx<N, P, C> {
771    fn from(deployer: Deployer<N, P>) -> Self {
772        Self { deployer, _contract: PhantomData }
773    }
774}
775
776/// Helper which manages the deployment transaction of a smart contract
777#[derive(Clone, Debug)]
778#[must_use = "Deployer does nothing unless you `send` it"]
779pub struct Deployer<N: Network, P> {
780    /// The deployer's transaction, exposed for overriding the defaults
781    pub tx: N::TransactionRequest,
782    client: P,
783    confs: usize,
784    timeout: u64,
785}
786
787impl<N: Network, P: Provider<N>> Deployer<N, P> {
788    /// Broadcasts the contract deployment transaction and after waiting for it to
789    /// be sufficiently confirmed (default: 1), it returns a tuple with the [`Address`] at the
790    /// deployed contract's address and the corresponding receipt.
791    pub async fn send_with_receipt(
792        self,
793    ) -> Result<(Address, N::ReceiptResponse), ContractDeploymentError> {
794        let receipt = self
795            .client
796            .borrow()
797            .send_transaction(self.tx)
798            .await?
799            .with_required_confirmations(self.confs as u64)
800            .with_timeout(Some(Duration::from_secs(self.timeout)))
801            .get_receipt()
802            .await?;
803
804        if !receipt.status() {
805            return Err(ContractDeploymentError::DeploymentFailed(receipt.transaction_hash()));
806        }
807
808        let address =
809            receipt.contract_address().ok_or(ContractDeploymentError::ContractNotDeployed)?;
810
811        Ok((address, receipt))
812    }
813}
814
815/// To deploy a contract to the Ethereum network, a [`ContractFactory`] can be
816/// created which manages the Contract bytecode and Application Binary Interface
817/// (ABI), usually generated from the Solidity compiler.
818#[derive(Clone, Debug)]
819pub struct DeploymentTxFactory<N: Network, P> {
820    client: P,
821    abi: JsonAbi,
822    bytecode: Bytes,
823    timeout: u64,
824    _network: PhantomData<N>,
825}
826
827impl<N: Network, P: Provider<N> + Clone> DeploymentTxFactory<N, P> {
828    /// Creates a factory for deployment of the Contract with bytecode, and the
829    /// constructor defined in the abi. The client will be used to send any deployment
830    /// transaction.
831    pub const fn new(abi: JsonAbi, bytecode: Bytes, client: P, timeout: u64) -> Self {
832        Self { client, abi, bytecode, timeout, _network: PhantomData }
833    }
834
835    /// Create a deployment tx using the provided tokens as constructor
836    /// arguments
837    pub fn deploy_tokens(
838        self,
839        params: Vec<DynSolValue>,
840    ) -> Result<Deployer<N, P>, ContractDeploymentError>
841    where
842        N::TransactionRequest: FoundryTransactionBuilder<N>,
843    {
844        // Encode the constructor args & concatenate with the bytecode if necessary
845        let data: Bytes = match (self.abi.constructor(), params.is_empty()) {
846            (None, false) => return Err(ContractDeploymentError::ConstructorError),
847            (None, true) => self.bytecode.clone(),
848            (Some(constructor), _) => {
849                let input: Bytes = constructor
850                    .abi_encode_input(&params)
851                    .map_err(ContractDeploymentError::DetokenizationError)?
852                    .into();
853                // Concatenate the bytecode and abi-encoded constructor call.
854                self.bytecode.iter().copied().chain(input).collect()
855            }
856        };
857
858        // create the tx object. Since we're deploying a contract, `to` is `None`
859        let mut tx = N::TransactionRequest::default();
860        tx.set_input(data);
861        Ok(Deployer { client: self.client.clone(), tx, confs: 1, timeout: self.timeout })
862    }
863}
864
865#[derive(thiserror::Error, Debug)]
866/// An Error which is thrown when interacting with a smart contract
867pub enum ContractDeploymentError {
868    #[error("constructor is not defined in the ABI")]
869    ConstructorError,
870    #[error(transparent)]
871    DetokenizationError(#[from] alloy_dyn_abi::Error),
872    #[error("contract was not deployed")]
873    ContractNotDeployed,
874    #[error("deployment transaction failed (receipt status 0): {0}")]
875    DeploymentFailed(alloy_primitives::TxHash),
876    #[error(transparent)]
877    RpcError(#[from] TransportError),
878}
879
880impl From<PendingTransactionError> for ContractDeploymentError {
881    fn from(_err: PendingTransactionError) -> Self {
882        Self::ContractNotDeployed
883    }
884}
885
886#[cfg(test)]
887mod tests {
888    use super::*;
889    use alloy_primitives::I256;
890
891    #[test]
892    fn can_parse_create() {
893        let args: CreateArgs = CreateArgs::parse_from([
894            "foundry-cli",
895            "src/Domains.sol:Domains",
896            "--verify",
897            "--retries",
898            "10",
899            "--delay",
900            "30",
901        ]);
902        assert_eq!(args.retry.retries, 10);
903        assert_eq!(args.retry.delay, 30);
904    }
905    #[test]
906    fn can_parse_chain_id() {
907        let args: CreateArgs = CreateArgs::parse_from([
908            "foundry-cli",
909            "src/Domains.sol:Domains",
910            "--verify",
911            "--retries",
912            "10",
913            "--delay",
914            "30",
915            "--chain-id",
916            "9999",
917        ]);
918        assert_eq!(args.chain_id().map(|c| c.id()), Some(9999));
919    }
920
921    #[test]
922    fn test_parse_constructor_args() {
923        let args: CreateArgs = CreateArgs::parse_from([
924            "foundry-cli",
925            "src/Domains.sol:Domains",
926            "--constructor-args",
927            "Hello",
928        ]);
929        let constructor: Constructor = serde_json::from_str(r#"{"type":"constructor","inputs":[{"name":"_name","type":"string","internalType":"string"}],"stateMutability":"nonpayable"}"#).unwrap();
930        let params = args.parse_constructor_args(&constructor, &args.constructor_args).unwrap();
931        assert_eq!(params, vec![DynSolValue::String("Hello".to_string())]);
932    }
933
934    #[test]
935    fn test_parse_tuple_constructor_args() {
936        let args: CreateArgs = CreateArgs::parse_from([
937            "foundry-cli",
938            "src/Domains.sol:Domains",
939            "--constructor-args",
940            "[(1,2), (2,3), (3,4)]",
941        ]);
942        let constructor: Constructor = serde_json::from_str(r#"{"type":"constructor","inputs":[{"name":"_points","type":"tuple[]","internalType":"struct Point[]","components":[{"name":"x","type":"uint256","internalType":"uint256"},{"name":"y","type":"uint256","internalType":"uint256"}]}],"stateMutability":"nonpayable"}"#).unwrap();
943        let _params = args.parse_constructor_args(&constructor, &args.constructor_args).unwrap();
944    }
945
946    #[test]
947    fn test_parse_int_constructor_args() {
948        let args: CreateArgs = CreateArgs::parse_from([
949            "foundry-cli",
950            "src/Domains.sol:Domains",
951            "--constructor-args",
952            "-5",
953        ]);
954        let constructor: Constructor = serde_json::from_str(r#"{"type":"constructor","inputs":[{"name":"_name","type":"int256","internalType":"int256"}],"stateMutability":"nonpayable"}"#).unwrap();
955        let params = args.parse_constructor_args(&constructor, &args.constructor_args).unwrap();
956        assert_eq!(params, vec![DynSolValue::Int(I256::unchecked_from(-5), 256)]);
957    }
958}