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