1use alloy_chains::Chain;
2use alloy_consensus::{SignableTransaction, Signed};
3use alloy_dyn_abi::{DynSolValue, JsonAbiExt};
4use alloy_json_abi::JsonAbi;
5use alloy_network::{Ethereum, EthereumWallet, Network, ReceiptResponse, TransactionBuilder};
6use alloy_primitives::{Address, Bytes, U256, hex};
7use alloy_provider::{PendingTransactionError, Provider, ProviderBuilder as AlloyProviderBuilder};
8use alloy_signer::{Signature, Signer};
9use alloy_transport::TransportError;
10use clap::{Parser, ValueHint};
11use eyre::{Context, ContextCompat, Result};
12use forge_verify::{RetryArgs, VerifierArgs, VerifyArgs, parse_etherscan_license_type};
13use foundry_cli::{
14 opts::{BuildOpts, EthereumOpts, EtherscanOpts, TransactionOpts},
15 utils::{
16 LoadConfig, ResolvedLane, apply_gas_estimate_multiplier, find_contract_artifacts,
17 maybe_print_resolved_lane, parse_constructor_args, read_constructor_args_file,
18 resolve_lane,
19 },
20};
21use foundry_common::{
22 FoundryTransactionBuilder, compile,
23 provider::{
24 ProviderBuilder,
25 fee::{estimate_eip1559_fees, resolve_broadcast_eip1559_fees},
26 },
27 shell,
28 tempo::{maybe_print_fee_token, resolve_and_set_fee_token},
29};
30use foundry_compilers::{
31 ArtifactId, artifacts::BytecodeObject, info::ContractInfo, utils::canonicalize,
32};
33use foundry_config::{
34 Config, Eip1559FeeEstimatePreset,
35 figment::{
36 self, Metadata, Profile,
37 value::{Dict, Map},
38 },
39 merge_impl_figment_convert,
40};
41use foundry_wallets::{
42 BrowserWalletOpts, TempoAccountsWallet, WalletSigner, wallet_browser::signer::BrowserSigner,
43};
44use serde_json::json;
45use std::{borrow::Borrow, marker::PhantomData, path::PathBuf, sync::Arc, time::Duration};
46use tempo_alloy::TempoNetwork;
47
48merge_impl_figment_convert!(CreateArgs, build, eth);
49
50#[derive(Clone, Debug, Parser)]
52#[command(mut_arg("auth", |arg| arg.hide(true)))]
53pub struct CreateArgs {
54 contract: ContractInfo,
56
57 #[arg(
59 long,
60 num_args(1..),
61 conflicts_with = "constructor_args_path",
62 value_name = "ARGS",
63 allow_hyphen_values = true,
64 )]
65 constructor_args: Vec<String>,
66
67 #[arg(
69 long,
70 value_hint = ValueHint::FilePath,
71 value_name = "PATH",
72 )]
73 constructor_args_path: Option<PathBuf>,
74
75 #[arg(long)]
77 pub broadcast: bool,
78
79 #[arg(long)]
81 verify: bool,
82
83 #[arg(long, requires = "from")]
85 unlocked: bool,
86
87 #[arg(long, requires = "verify")]
92 show_standard_json_input: bool,
93
94 #[arg(
100 long,
101 requires = "verify",
102 value_name = "LICENSE",
103 help_heading = "Verifier options",
104 value_parser = parse_etherscan_license_type,
105 )]
106 license_type: Option<String>,
107
108 #[arg(long, env = "ETH_TIMEOUT")]
110 pub timeout: Option<u64>,
111
112 #[arg(long, value_name = "PERCENT", help_heading = "Transaction options")]
114 gas_estimate_multiplier: Option<u64>,
115
116 #[command(flatten)]
117 build: BuildOpts,
118
119 #[command(flatten)]
120 tx: TransactionOpts,
121
122 #[command(flatten)]
123 eth: EthereumOpts,
124
125 #[command(flatten)]
126 pub verifier: VerifierArgs,
127
128 #[command(flatten)]
129 retry: RetryArgs,
130
131 #[command(flatten)]
133 browser: BrowserWalletOpts,
134}
135
136impl CreateArgs {
137 pub async fn run(mut self) -> Result<()> {
139 if self.tx.tempo.sponsor_url.is_some() {
140 eyre::bail!(
141 "--sponsor-url is not supported by forge create; use --tempo.sponsor with \
142 --tempo.sponsor-signer or --tempo.sponsor-sig"
143 );
144 }
145
146 let chain = if let Some(chain) = self.chain_id() {
148 chain
149 } else {
150 let config = self.load_config()?;
151 let provider = ProviderBuilder::<Ethereum>::from_config(&config)?.build()?;
152 let chain_id = provider.get_chain_id().await?;
153 let chain = Chain::from(chain_id);
154 self.eth.etherscan.chain = Some(chain);
155 chain
156 };
157 let mut wallet = self.eth.wallet.clone();
158 if !chain.is_tempo() && !self.tx.tempo.is_tempo() {
159 wallet.from = None;
162 }
163 let (signer, tempo_access_key) = wallet.maybe_signer_for_chain(chain.id()).await?;
164
165 if tempo_access_key.is_some() || self.tx.tempo.is_tempo() || chain.is_tempo() {
166 self.run_generic::<TempoNetwork>(signer, tempo_access_key).await
167 } else {
168 self.run_generic::<Ethereum>(signer, None).await
169 }
170 }
171
172 async fn run_generic<N: Network>(
173 mut self,
174 pre_resolved_signer: Option<WalletSigner>,
175 access_key: Option<TempoAccountsWallet>,
176 ) -> Result<()>
177 where
178 N::TxEnvelope: From<Signed<N::UnsignedTx>>,
179 N::UnsignedTx: SignableTransaction<Signature>,
180 N::TransactionRequest: FoundryTransactionBuilder<N> + serde::Serialize,
181 N::ReceiptResponse: serde::Serialize,
182 {
183 let mut config = self.load_config()?;
184 let resolve_unknown_fee_token_symbol = !config.eth_rpc_curl;
185
186 self.install_missing_dependencies(&mut config)?;
188
189 let project = config.project()?;
191
192 let target_path = if let Some(ref mut path) = self.contract.path {
193 canonicalize(project.root().join(path))?
194 } else {
195 project.find_contract_path(&self.contract.name)?
196 };
197
198 let output = compile::compile_target(&target_path, &project, shell::is_json())?;
199
200 let (abi, bin, id) = find_contract_artifacts(output, &target_path, &self.contract.name)?;
201
202 let bin = match bin.object {
203 BytecodeObject::Bytecode(_) => bin.object,
204 _ => {
205 let link_refs = bin
206 .link_references
207 .iter()
208 .flat_map(|(path, names)| {
209 names.keys().map(move |name| format!("\t{name}: {path}"))
210 })
211 .collect::<Vec<String>>()
212 .join("\n");
213 eyre::bail!(
214 "Dynamic linking not supported in `create` command - deploy the following library contracts first, then provide the address to link at compile time\n{}",
215 link_refs
216 );
217 }
218 };
219
220 let params = if let Some(constructor) = &abi.constructor {
222 let constructor_args =
223 self.constructor_args_path.clone().map(read_constructor_args_file).transpose()?;
224 parse_constructor_args(
225 constructor,
226 constructor_args.as_deref().unwrap_or(&self.constructor_args),
227 )?
228 } else {
229 if !self.constructor_args.is_empty() || self.constructor_args_path.is_some() {
230 sh_warn!(
231 "`{}` has no constructor; ignoring provided constructor arguments",
232 self.contract.name
233 )?;
234 }
235 vec![]
236 };
237
238 let provider = ProviderBuilder::<N>::from_config(&config)?.build()?;
239
240 if let Some(ref ak) = access_key {
242 self.tx.tempo.key_id = Some(ak.key_id()?);
243 }
244
245 let resolved_lane = resolve_lane(&mut self.tx.tempo, &config.root)?;
249 let expires_at = self.tx.tempo.resolve_expires();
250
251 let dry_run = !self.broadcast;
253
254 let browser = self.browser.run::<N>().await?;
256
257 if let Some(browser) = browser {
258 let deployer_address = browser.address();
260 self.deploy(
261 abi,
262 bin,
263 params,
264 provider,
265 deployer_address,
266 config.transaction_timeout,
267 id,
268 dry_run,
269 None,
270 Some(browser),
271 resolved_lane,
272 expires_at,
273 resolve_unknown_fee_token_symbol,
274 config.eip1559_fee_estimate,
275 )
276 .await
277 } else if self.unlocked {
278 let sender = self.eth.wallet.from.expect("required");
280 self.deploy(
281 abi,
282 bin,
283 params,
284 provider,
285 sender,
286 config.transaction_timeout,
287 id,
288 dry_run,
289 None,
290 None,
291 resolved_lane,
292 expires_at,
293 resolve_unknown_fee_token_symbol,
294 config.eip1559_fee_estimate,
295 )
296 .await
297 } else if let Some(ak) = access_key {
298 let deployer_address = ak.account();
299 self.deploy(
300 abi,
301 bin,
302 params,
303 provider,
304 deployer_address,
305 config.transaction_timeout,
306 id,
307 dry_run,
308 Some(ak),
309 None,
310 resolved_lane,
311 expires_at,
312 resolve_unknown_fee_token_symbol,
313 config.eip1559_fee_estimate,
314 )
315 .await
316 } else {
317 let signer = match pre_resolved_signer {
319 Some(s) => s,
320 None => self.eth.wallet.signer().await?,
321 };
322 let deployer = signer.address();
323 let provider = AlloyProviderBuilder::<_, _, N>::default()
324 .wallet(EthereumWallet::new(signer))
325 .connect_provider(provider);
326 self.deploy(
327 abi,
328 bin,
329 params,
330 provider,
331 deployer,
332 config.transaction_timeout,
333 id,
334 dry_run,
335 None,
336 None,
337 resolved_lane,
338 expires_at,
339 resolve_unknown_fee_token_symbol,
340 config.eip1559_fee_estimate,
341 )
342 .await
343 }
344 }
345
346 const fn chain_id(&self) -> Option<Chain> {
348 self.eth.etherscan.chain
349 }
350
351 async fn verify_preflight_check(
358 &self,
359 constructor_args: Option<String>,
360 id: &ArtifactId,
361 ) -> Result<()> {
362 let mut verify = VerifyArgs {
365 address: Default::default(),
366 contract: Some(self.contract.clone()),
367 compiler_version: Some(id.version.to_string()),
368 constructor_args,
369 constructor_args_path: None,
370 no_auto_detect: false,
371 use_solc: None,
372 num_of_optimizations: None,
373 etherscan: EtherscanOpts {
374 key: self.eth.etherscan.key.clone(),
375 chain: self.chain_id(),
376 },
377 rpc: Default::default(),
378 flatten: false,
379 force: false,
380 skip_is_verified_check: true,
381 watch: true,
382 print_submission_result_to_stdout: false,
383 retry: self.retry,
384 libraries: self.build.libraries.clone(),
385 root: None,
386 verifier: self.verifier.clone(),
387 via_ir: self.build.compiler.via_ir,
388 license_type: self.license_type.clone(),
389 evm_version: self.build.compiler.evm_version,
390 show_standard_json_input: self.show_standard_json_input,
391 guess_constructor_args: false,
392 compilation_profile: Some(id.profile.clone()),
393 language: None,
394 creation_transaction_hash: None,
395 };
396
397 let config = verify.load_config()?;
400 verify.etherscan.key = config
401 .get_etherscan_config_with_chain(self.chain_id())?
402 .map(|c| c.key)
403 .or_else(|| config.etherscan_api_key.clone());
404
405 let context = verify.resolve_context().await?;
406
407 verify.verification_provider()?.preflight_verify_check(verify.clone(), context).await?;
408
409 let api_key = verify.verifier.resolve_api_key(verify.etherscan.key.as_deref());
410 let chain = verify.etherscan.chain.context("chain ID not resolved")?;
411 verify
412 .verifier
413 .check_credentials(api_key, chain, &config)
414 .await
415 .wrap_err("Verification preflight check failed")?;
416
417 Ok(())
418 }
419
420 #[expect(clippy::too_many_arguments)]
422 async fn deploy<N: Network, P: Provider<N>>(
423 self,
424 abi: JsonAbi,
425 bin: BytecodeObject,
426 args: Vec<DynSolValue>,
427 provider: P,
428 deployer_address: Address,
429 timeout: u64,
430 id: ArtifactId,
431 dry_run: bool,
432 mut tempo_keychain: Option<TempoAccountsWallet>,
433 browser_signer: Option<BrowserSigner<N>>,
434 resolved_lane: Option<ResolvedLane>,
435 expires_at: Option<u64>,
436 resolve_unknown_fee_token_symbol: bool,
437 eip1559_fee_estimate: Eip1559FeeEstimatePreset,
438 ) -> Result<()>
439 where
440 N::TransactionRequest: FoundryTransactionBuilder<N> + serde::Serialize,
441 N::ReceiptResponse: serde::Serialize,
442 {
443 let chain = self.chain_id().context("chain ID not resolved")?;
444
445 let bin = bin.into_bytes().unwrap_or_default();
446 if bin.is_empty() {
447 eyre::bail!("no bytecode found in bin object for {}", self.contract.name);
448 }
449
450 let provider = Arc::new(provider);
451 let factory =
452 ContractFactory::<N, _>::new(abi.clone(), bin.clone(), provider.clone(), timeout);
453
454 let is_args_empty = args.is_empty();
455 let mut deployer =
456 factory.deploy_tokens(args.clone()).context("failed to deploy contract").map_err(|e| {
457 if is_args_empty {
458 e.wrap_err("no arguments provided for contract constructor; consider --constructor-args or --constructor-args-path")
459 } else {
460 e
461 }
462 })?;
463 let is_legacy = self.tx.legacy || chain.is_legacy();
464
465 deployer.tx.set_from(deployer_address);
466 deployer.tx.set_chain_id(chain.id());
467 if deployer.tx.to().is_none() {
469 deployer.tx.set_create();
470 }
471
472 self.tx.apply::<N>(&mut deployer.tx, is_legacy);
474
475 if deployer.tx.is_tempo_aa() {
479 deployer.tx.convert_create_to_call();
480 }
481
482 if tempo_keychain.is_some() && deployer.tx.nonce_key().is_none() {
483 deployer.tx.set_nonce_key(U256::ZERO);
484 }
485
486 if self.tx.nonce.is_none() && !self.tx.tempo.expiring_nonce {
488 deployer.tx.set_nonce(provider.get_transaction_count(deployer_address).await?);
489 }
490
491 maybe_print_resolved_lane(resolved_lane.as_ref(), deployer.tx.nonce().unwrap_or_default())?;
492
493 if let Some(wallet) = tempo_keychain.as_ref() {
494 tempo_keychain =
495 Some(deployer.tx.prepare_with_tempo_wallet(provider.as_ref(), wallet).await?);
496 }
497
498 if is_legacy {
499 if self.tx.gas_price.is_none() {
500 deployer.tx.set_gas_price(provider.get_gas_price().await?);
501 }
502 } else {
503 if self.tx.gas_price.is_none() || self.tx.priority_gas_price.is_none() {
504 let estimate = estimate_eip1559_fees(&provider, eip1559_fee_estimate).await.wrap_err("Failed to estimate EIP1559 fees. This chain might not support EIP1559, try adding --legacy to your command.")?;
505
506 let browser_suggested_tip =
509 if browser_signer.is_some() && self.tx.priority_gas_price.is_none() {
510 provider.get_max_priority_fee_per_gas().await.ok()
511 } else {
512 None
513 };
514
515 let estimate =
518 resolve_broadcast_eip1559_fees(estimate, None, None, browser_suggested_tip)?;
519
520 if self.tx.priority_gas_price.is_none() {
521 deployer.tx.set_max_priority_fee_per_gas(estimate.max_priority_fee_per_gas);
522 }
523 if self.tx.gas_price.is_none() {
524 deployer.tx.set_max_fee_per_gas(estimate.max_fee_per_gas);
525 }
526 }
527 if let (Some(max_fee), Some(priority)) =
528 (deployer.tx.max_fee_per_gas(), deployer.tx.max_priority_fee_per_gas())
529 {
530 eyre::ensure!(
531 priority <= max_fee,
532 "max priority fee per gas ({priority}) cannot exceed max fee per gas ({max_fee})"
533 );
534 }
535 }
536
537 if let Some(access_list) = match self.tx.access_list {
539 None => None,
540 Some(None) => Some(provider.create_access_list(&deployer.tx).await?.access_list),
541 Some(Some(ref access_list)) => Some(access_list.clone()),
542 } {
543 deployer.tx.set_access_list(access_list);
544 }
545
546 if self.tx.gas_limit.is_none() {
547 let request = if browser_signer.is_some() && chain.is_tempo() {
548 deployer.tx.browser_wallet_gas_estimation_request()
549 } else {
550 deployer.tx.clone()
551 };
552 let estimated = provider.estimate_gas(request).await?;
553 deployer.tx.set_gas_limit(apply_gas_estimate_multiplier(
554 estimated,
555 self.gas_estimate_multiplier,
556 )?);
557 }
558
559 let mut constructor_args = None;
561 if self.verify {
562 if !args.is_empty() {
563 let encoded_args = abi
564 .constructor()
565 .ok_or_else(|| eyre::eyre!("could not find constructor"))?
566 .abi_encode_input(&args)?;
567 constructor_args = Some(hex::encode(encoded_args));
568 }
569
570 self.verify_preflight_check(constructor_args.clone(), &id).await?;
571 }
572
573 if dry_run {
574 if shell::is_json() {
575 let output = json!({
576 "contract": self.contract.name,
577 "transaction": &deployer.tx,
578 "abi":&abi
579 });
580 sh_println!("{}", serde_json::to_string_pretty(&output)?)?;
581 } else {
582 sh_warn!("Dry run enabled, not broadcasting transaction\n")?;
583
584 sh_println!("Contract: {}", self.contract.name)?;
585 sh_println!(
586 "Transaction: {}",
587 serde_json::to_string_pretty(&deployer.tx.clone())?
588 )?;
589 sh_println!("ABI: {}\n", serde_json::to_string_pretty(&abi)?)?;
590
591 sh_warn!(
592 "To broadcast this transaction, add --broadcast to the previous command. See forge create --help for more."
593 )?;
594 }
595
596 return Ok(());
597 }
598
599 if let Some(ts) = expires_at {
600 sh_status!("Transaction expires at unix timestamp {ts}")?;
601 }
602
603 let tempo_sponsor = self.tx.tempo.sponsor_config().await?;
604 if let Some(sponsor) = &tempo_sponsor {
605 sponsor
606 .resolve_and_set_fee_token(
607 resolve_unknown_fee_token_symbol.then_some(&provider),
608 Some(chain),
609 &mut deployer.tx,
610 )
611 .await?;
612 sponsor.attach_and_print::<N>(&mut deployer.tx, deployer_address).await?;
613 } else {
614 let fee_token = resolve_and_set_fee_token(
615 resolve_unknown_fee_token_symbol.then_some(&provider),
616 Some(chain),
617 &mut deployer.tx,
618 Some(deployer_address),
619 )
620 .await?;
621 maybe_print_fee_token(resolve_unknown_fee_token_symbol.then_some(&provider), fee_token)
622 .await?;
623 }
624
625 let (deployed_contract, receipt) = if let Some(browser) = browser_signer {
627 let tx_hash = browser.send_transaction_via_browser(deployer.tx).await?;
629
630 provider
632 .watch_pending_transaction(alloy_provider::PendingTransactionConfig::new(tx_hash))
633 .await?
634 .await?;
635
636 let receipt = provider
637 .get_transaction_receipt(tx_hash)
638 .await?
639 .ok_or_else(|| eyre::eyre!("could not get transaction receipt for {tx_hash}"))?;
640
641 if !receipt.status() {
642 eyre::bail!("deployment transaction failed (receipt status 0): {tx_hash}");
643 }
644
645 let address = receipt
646 .contract_address()
647 .ok_or_else(|| eyre::eyre!("contract was not deployed"))?;
648
649 (address, receipt)
650 } else if let Some(wallet) = tempo_keychain {
651 let raw_tx = deployer.tx.sign_with_tempo_wallet(&wallet).await?;
652
653 let receipt = provider
654 .send_raw_transaction(&raw_tx)
655 .await?
656 .with_required_confirmations(1)
657 .with_timeout(Some(Duration::from_secs(timeout)))
658 .get_receipt()
659 .await?;
660
661 let address = receipt
662 .contract_address()
663 .ok_or_else(|| eyre::eyre!("contract was not deployed"))?;
664
665 (address, receipt)
666 } else {
667 deployer.send_with_receipt().await?
668 };
669
670 let address = deployed_contract;
671 let tx_hash = receipt.transaction_hash();
672 if shell::is_json() {
673 let output = json!({
674 "deployer": deployer_address.to_string(),
675 "deployedTo": address.to_string(),
676 "transactionHash": tx_hash
677 });
678 sh_println!("{}", serde_json::to_string_pretty(&output)?)?;
679 } else {
680 sh_println!("Deployer: {deployer_address}")?;
681 sh_println!("Deployed to: {address}")?;
682 sh_println!("Transaction hash: {tx_hash:?}")?;
683 };
684
685 if !self.verify {
686 return Ok(());
687 }
688
689 sh_status!("Starting contract verification...")?;
690
691 let num_of_optimizations = if let Some(optimizer) = self.build.compiler.optimize {
692 optimizer.then(|| self.build.compiler.optimizer_runs.unwrap_or(200))
693 } else {
694 self.build.compiler.optimizer_runs
695 };
696
697 let verify = VerifyArgs {
698 address,
699 contract: Some(self.contract),
700 compiler_version: Some(id.version.to_string()),
701 constructor_args,
702 constructor_args_path: None,
703 no_auto_detect: false,
704 use_solc: None,
705 num_of_optimizations,
706 etherscan: EtherscanOpts { key: self.eth.etherscan.key(), chain: Some(chain) },
707 rpc: Default::default(),
708 flatten: false,
709 force: false,
710 skip_is_verified_check: true,
711 watch: true,
712 print_submission_result_to_stdout: false,
713 retry: self.retry,
714 libraries: self.build.libraries.clone(),
715 root: None,
716 verifier: self.verifier,
717 via_ir: self.build.compiler.via_ir,
718 license_type: self.license_type,
719 evm_version: self.build.compiler.evm_version,
720 show_standard_json_input: self.show_standard_json_input,
721 guess_constructor_args: false,
722 compilation_profile: Some(id.profile.clone()),
723 language: None,
724 creation_transaction_hash: Some(tx_hash),
725 };
726 let verify_config = verify.load_config()?;
730 let effective_key = verify_config
731 .get_etherscan_config_with_chain(Some(chain))?
732 .map(|c| c.key)
733 .or_else(|| verify_config.etherscan_api_key.clone());
734 let resolved_verifier = verify.verifier.resolve(effective_key.as_deref(), Some(chain));
735 sh_status!("Waiting for {resolved_verifier} to detect contract deployment...")?;
736 verify.run().await
737 }
738}
739
740impl figment::Provider for CreateArgs {
741 fn metadata(&self) -> Metadata {
742 Metadata::named("Create Args Provider")
743 }
744
745 fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
746 let mut dict = Dict::default();
747 if let Some(timeout) = self.timeout {
748 dict.insert("transaction_timeout".to_string(), timeout.into());
749 }
750 Ok(Map::from([(Config::selected_profile(), dict)]))
751 }
752}
753
754pub type ContractFactory<N, P> = DeploymentTxFactory<N, P>;
760
761#[derive(Debug)]
765#[must_use = "ContractDeploymentTx does nothing unless you `send` it"]
766pub struct ContractDeploymentTx<N: Network, P, C> {
767 pub deployer: Deployer<N, P>,
769 _contract: PhantomData<C>,
773}
774
775impl<N: Network, P: Clone, C> Clone for ContractDeploymentTx<N, P, C> {
776 fn clone(&self) -> Self {
777 Self { deployer: self.deployer.clone(), _contract: self._contract }
778 }
779}
780
781impl<N: Network, P, C> From<Deployer<N, P>> for ContractDeploymentTx<N, P, C> {
782 fn from(deployer: Deployer<N, P>) -> Self {
783 Self { deployer, _contract: PhantomData }
784 }
785}
786
787#[derive(Clone, Debug)]
789#[must_use = "Deployer does nothing unless you `send` it"]
790pub struct Deployer<N: Network, P> {
791 pub tx: N::TransactionRequest,
793 client: P,
794 confs: usize,
795 timeout: u64,
796}
797
798impl<N: Network, P: Provider<N>> Deployer<N, P> {
799 pub async fn send_with_receipt(
803 self,
804 ) -> Result<(Address, N::ReceiptResponse), ContractDeploymentError> {
805 let receipt = self
806 .client
807 .borrow()
808 .send_transaction(self.tx)
809 .await?
810 .with_required_confirmations(self.confs as u64)
811 .with_timeout(Some(Duration::from_secs(self.timeout)))
812 .get_receipt()
813 .await?;
814
815 if !receipt.status() {
816 return Err(ContractDeploymentError::DeploymentFailed(receipt.transaction_hash()));
817 }
818
819 let address =
820 receipt.contract_address().ok_or(ContractDeploymentError::ContractNotDeployed)?;
821
822 Ok((address, receipt))
823 }
824}
825
826#[derive(Clone, Debug)]
830pub struct DeploymentTxFactory<N: Network, P> {
831 client: P,
832 abi: JsonAbi,
833 bytecode: Bytes,
834 timeout: u64,
835 _network: PhantomData<N>,
836}
837
838impl<N: Network, P: Provider<N> + Clone> DeploymentTxFactory<N, P> {
839 pub const fn new(abi: JsonAbi, bytecode: Bytes, client: P, timeout: u64) -> Self {
843 Self { client, abi, bytecode, timeout, _network: PhantomData }
844 }
845
846 pub fn deploy_tokens(
849 self,
850 params: Vec<DynSolValue>,
851 ) -> Result<Deployer<N, P>, ContractDeploymentError>
852 where
853 N::TransactionRequest: FoundryTransactionBuilder<N>,
854 {
855 let data: Bytes = match (self.abi.constructor(), params.is_empty()) {
857 (None, false) => return Err(ContractDeploymentError::ConstructorError),
858 (None, true) => self.bytecode.clone(),
859 (Some(constructor), _) => {
860 let input: Bytes = constructor
861 .abi_encode_input(¶ms)
862 .map_err(ContractDeploymentError::DetokenizationError)?
863 .into();
864 self.bytecode.iter().copied().chain(input).collect()
866 }
867 };
868
869 let mut tx = N::TransactionRequest::default();
871 tx.set_input(data);
872 Ok(Deployer { client: self.client.clone(), tx, confs: 1, timeout: self.timeout })
873 }
874}
875
876#[derive(thiserror::Error, Debug)]
877pub enum ContractDeploymentError {
879 #[error("constructor is not defined in the ABI")]
880 ConstructorError,
881 #[error(transparent)]
882 DetokenizationError(#[from] alloy_dyn_abi::Error),
883 #[error("contract was not deployed")]
884 ContractNotDeployed,
885 #[error("deployment transaction failed (receipt status 0): {0}")]
886 DeploymentFailed(alloy_primitives::TxHash),
887 #[error(transparent)]
888 RpcError(#[from] TransportError),
889}
890
891impl From<PendingTransactionError> for ContractDeploymentError {
892 fn from(_err: PendingTransactionError) -> Self {
893 Self::ContractNotDeployed
894 }
895}
896
897#[cfg(test)]
898mod tests {
899 use super::*;
900 use alloy_json_abi::Constructor;
901 use alloy_primitives::I256;
902
903 #[test]
904 fn can_parse_create() {
905 let args: CreateArgs = CreateArgs::parse_from([
906 "foundry-cli",
907 "src/Domains.sol:Domains",
908 "--verify",
909 "--retries",
910 "10",
911 "--delay",
912 "30",
913 "--license-type",
914 "13",
915 "--gas-estimate-multiplier",
916 "125",
917 ]);
918 assert_eq!(args.retry.retries, 10);
919 assert_eq!(args.retry.delay, 30);
920 assert_eq!(args.license_type.as_deref(), Some("13"));
921 assert_eq!(args.gas_estimate_multiplier, Some(125));
922 }
923
924 #[test]
925 fn create_help_hides_auth() {
926 let help = <CreateArgs as clap::CommandFactory>::command().render_long_help().to_string();
927 assert!(!help.contains("--auth"));
928 }
929
930 #[test]
931 fn can_parse_create_license_type_spdx() {
932 let args: CreateArgs = CreateArgs::parse_from([
933 "foundry-cli",
934 "src/Domains.sol:Domains",
935 "--verify",
936 "--license-type",
937 "MIT",
938 ]);
939 assert_eq!(args.license_type.as_deref(), Some("3"));
940 }
941
942 #[test]
943 fn errors_on_invalid_create_license_type() {
944 let err = CreateArgs::try_parse_from([
945 "foundry-cli",
946 "src/Domains.sol:Domains",
947 "--verify",
948 "--license-type",
949 "definitely-not-a-license",
950 ])
951 .unwrap_err();
952 assert!(err.to_string().contains("unsupported Etherscan license type"));
953 }
954
955 #[test]
956 fn can_parse_chain_id() {
957 let args: CreateArgs = CreateArgs::parse_from([
958 "foundry-cli",
959 "src/Domains.sol:Domains",
960 "--verify",
961 "--retries",
962 "10",
963 "--delay",
964 "30",
965 "--chain-id",
966 "9999",
967 ]);
968 assert_eq!(args.chain_id().map(|c| c.id()), Some(9999));
969 }
970
971 #[test]
972 fn test_parse_constructor_args() {
973 let args: CreateArgs = CreateArgs::parse_from([
974 "foundry-cli",
975 "src/Domains.sol:Domains",
976 "--constructor-args",
977 "Hello",
978 ]);
979 let constructor: Constructor = serde_json::from_str(r#"{"type":"constructor","inputs":[{"name":"_name","type":"string","internalType":"string"}],"stateMutability":"nonpayable"}"#).unwrap();
980 let params = parse_constructor_args(&constructor, &args.constructor_args).unwrap();
981 assert_eq!(params, vec![DynSolValue::String("Hello".to_string())]);
982 }
983
984 #[test]
985 fn test_parse_tuple_constructor_args() {
986 let args: CreateArgs = CreateArgs::parse_from([
987 "foundry-cli",
988 "src/Domains.sol:Domains",
989 "--constructor-args",
990 "[(1,2), (2,3), (3,4)]",
991 ]);
992 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();
993 let _params = parse_constructor_args(&constructor, &args.constructor_args).unwrap();
994 }
995
996 #[test]
997 fn test_parse_int_constructor_args() {
998 let args: CreateArgs = CreateArgs::parse_from([
999 "foundry-cli",
1000 "src/Domains.sol:Domains",
1001 "--constructor-args",
1002 "-5",
1003 ]);
1004 let constructor: Constructor = serde_json::from_str(r#"{"type":"constructor","inputs":[{"name":"_name","type":"int256","internalType":"int256"}],"stateMutability":"nonpayable"}"#).unwrap();
1005 let params = parse_constructor_args(&constructor, &args.constructor_args).unwrap();
1006 assert_eq!(params, vec![DynSolValue::Int(I256::unchecked_from(-5), 256)]);
1007 }
1008}