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 let deployer = signer
168 .as_ref()
169 .map(|signer| signer.address())
170 .or_else(|| tempo_access_key.as_ref().map(|ak| ak.account()));
171 if !self.unlocked
172 && !self.browser.browser
173 && let Some(from) = self.eth.wallet.from
174 && let Some(deployer) = deployer
175 && from != deployer
176 {
177 eyre::bail!(
178 "the sender specified via `--from`/`ETH_FROM` ({from}) does not match the \
179 signer address ({deployer})"
180 );
181 }
182
183 if tempo_access_key.is_some() || self.tx.tempo.is_tempo() || chain.is_tempo() {
184 self.run_generic::<TempoNetwork>(signer, tempo_access_key).await
185 } else {
186 self.run_generic::<Ethereum>(signer, None).await
187 }
188 }
189
190 async fn run_generic<N: Network>(
191 mut self,
192 pre_resolved_signer: Option<WalletSigner>,
193 access_key: Option<TempoAccountsWallet>,
194 ) -> Result<()>
195 where
196 N::TxEnvelope: From<Signed<N::UnsignedTx>>,
197 N::UnsignedTx: SignableTransaction<Signature>,
198 N::TransactionRequest: FoundryTransactionBuilder<N> + serde::Serialize,
199 N::ReceiptResponse: serde::Serialize,
200 {
201 let mut config = self.load_config()?;
202 let resolve_unknown_fee_token_symbol = !config.eth_rpc_curl;
203
204 self.install_missing_dependencies(&mut config)?;
206
207 let project = config.project()?;
209
210 let target_path = if let Some(ref mut path) = self.contract.path {
211 canonicalize(project.root().join(path))?
212 } else {
213 project.find_contract_path(&self.contract.name)?
214 };
215
216 let compiler = compile::ProjectCompiler::new()
217 .external_compilers(&config)
218 .quiet(shell::is_json())
219 .target_files([target_path.clone()]);
220 let output = compiler.compile(&project)?;
221
222 let (abi, bin, id) = find_contract_artifacts(output, &target_path, &self.contract.name)?;
223
224 let bin = match bin.object {
225 BytecodeObject::Bytecode(_) => bin.object,
226 _ => {
227 let link_refs = bin
228 .link_references
229 .iter()
230 .flat_map(|(path, names)| {
231 names.keys().map(move |name| format!("\t{name}: {path}"))
232 })
233 .collect::<Vec<String>>()
234 .join("\n");
235 eyre::bail!(
236 "Dynamic linking not supported in `create` command - deploy the following library contracts first, then provide the address to link at compile time\n{}",
237 link_refs
238 );
239 }
240 };
241
242 let params = if let Some(constructor) = &abi.constructor {
244 let constructor_args =
245 self.constructor_args_path.clone().map(read_constructor_args_file).transpose()?;
246 parse_constructor_args(
247 constructor,
248 constructor_args.as_deref().unwrap_or(&self.constructor_args),
249 )?
250 } else {
251 if !self.constructor_args.is_empty() || self.constructor_args_path.is_some() {
252 sh_warn!(
253 "`{}` has no constructor; ignoring provided constructor arguments",
254 self.contract.name
255 )?;
256 }
257 vec![]
258 };
259
260 let provider = ProviderBuilder::<N>::from_config(&config)?.build()?;
261
262 if let Some(ref ak) = access_key {
264 self.tx.tempo.key_id = Some(ak.key_id()?);
265 }
266
267 let resolved_lane = resolve_lane(&mut self.tx.tempo, &config.root)?;
271 let expires_at = self.tx.tempo.resolve_expires();
272
273 let dry_run = !self.broadcast;
275
276 let browser = self.browser.run::<N>().await?;
278
279 if let Some(browser) = browser {
280 let deployer_address = browser.address();
282 self.deploy(
283 abi,
284 bin,
285 params,
286 provider,
287 deployer_address,
288 config.transaction_timeout,
289 id,
290 dry_run,
291 None,
292 Some(browser),
293 resolved_lane,
294 expires_at,
295 resolve_unknown_fee_token_symbol,
296 config.eip1559_fee_estimate,
297 )
298 .await
299 } else if self.unlocked {
300 let sender = self.eth.wallet.from.expect("required");
302 self.deploy(
303 abi,
304 bin,
305 params,
306 provider,
307 sender,
308 config.transaction_timeout,
309 id,
310 dry_run,
311 None,
312 None,
313 resolved_lane,
314 expires_at,
315 resolve_unknown_fee_token_symbol,
316 config.eip1559_fee_estimate,
317 )
318 .await
319 } else if let Some(ak) = access_key {
320 let deployer_address = ak.account();
321 self.deploy(
322 abi,
323 bin,
324 params,
325 provider,
326 deployer_address,
327 config.transaction_timeout,
328 id,
329 dry_run,
330 Some(ak),
331 None,
332 resolved_lane,
333 expires_at,
334 resolve_unknown_fee_token_symbol,
335 config.eip1559_fee_estimate,
336 )
337 .await
338 } else {
339 let signer = match pre_resolved_signer {
341 Some(s) => s,
342 None => self.eth.wallet.signer().await?,
343 };
344 let deployer = signer.address();
345 let provider = AlloyProviderBuilder::<_, _, N>::default()
346 .wallet(EthereumWallet::new(signer))
347 .connect_provider(provider);
348 self.deploy(
349 abi,
350 bin,
351 params,
352 provider,
353 deployer,
354 config.transaction_timeout,
355 id,
356 dry_run,
357 None,
358 None,
359 resolved_lane,
360 expires_at,
361 resolve_unknown_fee_token_symbol,
362 config.eip1559_fee_estimate,
363 )
364 .await
365 }
366 }
367
368 const fn chain_id(&self) -> Option<Chain> {
370 self.eth.etherscan.chain
371 }
372
373 async fn verify_preflight_check(
380 &self,
381 constructor_args: Option<String>,
382 id: &ArtifactId,
383 ) -> Result<()> {
384 let mut verify = VerifyArgs {
387 address: Default::default(),
388 contract: Some(self.contract.clone()),
389 compiler_version: Some(id.version.to_string()),
390 constructor_args,
391 constructor_args_path: None,
392 no_auto_detect: false,
393 use_solc: None,
394 num_of_optimizations: None,
395 etherscan: EtherscanOpts {
396 key: self.eth.etherscan.key.clone(),
397 chain: self.chain_id(),
398 },
399 rpc: Default::default(),
400 flatten: false,
401 force: false,
402 skip_is_verified_check: true,
403 watch: true,
404 print_submission_result_to_stdout: false,
405 retry: self.retry,
406 libraries: self.build.libraries.clone(),
407 root: None,
408 verifier: self.verifier.clone(),
409 via_ir: self.build.compiler.via_ir,
410 license_type: self.license_type.clone(),
411 evm_version: self.build.compiler.evm_version,
412 show_standard_json_input: self.show_standard_json_input,
413 guess_constructor_args: false,
414 compilation_profile: Some(id.profile.clone()),
415 language: None,
416 creation_transaction_hash: None,
417 };
418
419 let config = verify.load_config()?;
422 verify.etherscan.key = config
423 .get_etherscan_config_with_chain(self.chain_id())?
424 .map(|c| c.key)
425 .or_else(|| config.etherscan_api_key.clone());
426
427 let context = verify.resolve_context().await?;
428
429 verify.verification_provider()?.preflight_verify_check(verify.clone(), context).await?;
430
431 let api_key = verify.verifier.resolve_api_key(verify.etherscan.key.as_deref());
432 let chain = verify.etherscan.chain.context("chain ID not resolved")?;
433 verify
434 .verifier
435 .check_credentials(api_key, chain, &config)
436 .await
437 .wrap_err("Verification preflight check failed")?;
438
439 Ok(())
440 }
441
442 #[expect(clippy::too_many_arguments)]
444 async fn deploy<N: Network, P: Provider<N>>(
445 self,
446 abi: JsonAbi,
447 bin: BytecodeObject,
448 args: Vec<DynSolValue>,
449 provider: P,
450 deployer_address: Address,
451 timeout: u64,
452 id: ArtifactId,
453 dry_run: bool,
454 mut tempo_keychain: Option<TempoAccountsWallet>,
455 browser_signer: Option<BrowserSigner<N>>,
456 resolved_lane: Option<ResolvedLane>,
457 expires_at: Option<u64>,
458 resolve_unknown_fee_token_symbol: bool,
459 eip1559_fee_estimate: Eip1559FeeEstimatePreset,
460 ) -> Result<()>
461 where
462 N::TransactionRequest: FoundryTransactionBuilder<N> + serde::Serialize,
463 N::ReceiptResponse: serde::Serialize,
464 {
465 let chain = self.chain_id().context("chain ID not resolved")?;
466
467 let bin = bin.into_bytes().unwrap_or_default();
468 if bin.is_empty() {
469 eyre::bail!("no bytecode found in bin object for {}", self.contract.name);
470 }
471
472 let provider = Arc::new(provider);
473 let factory =
474 ContractFactory::<N, _>::new(abi.clone(), bin.clone(), provider.clone(), timeout);
475
476 let is_args_empty = args.is_empty();
477 let mut deployer =
478 factory.deploy_tokens(args.clone()).context("failed to deploy contract").map_err(|e| {
479 if is_args_empty {
480 e.wrap_err("no arguments provided for contract constructor; consider --constructor-args or --constructor-args-path")
481 } else {
482 e
483 }
484 })?;
485 let is_legacy = self.tx.legacy || chain.is_legacy();
486
487 deployer.tx.set_from(deployer_address);
488 deployer.tx.set_chain_id(chain.id());
489 if deployer.tx.to().is_none() {
491 deployer.tx.set_create();
492 }
493
494 self.tx.apply::<N>(&mut deployer.tx, is_legacy);
496
497 if deployer.tx.is_tempo_aa() {
501 deployer.tx.convert_create_to_call();
502 }
503
504 if tempo_keychain.is_some() && deployer.tx.nonce_key().is_none() {
505 deployer.tx.set_nonce_key(U256::ZERO);
506 }
507
508 if self.tx.nonce.is_none() && !self.tx.tempo.expiring_nonce {
510 deployer.tx.set_nonce(provider.get_transaction_count(deployer_address).await?);
511 }
512
513 maybe_print_resolved_lane(resolved_lane.as_ref(), deployer.tx.nonce().unwrap_or_default())?;
514
515 if let Some(wallet) = tempo_keychain.as_ref() {
516 tempo_keychain =
517 Some(deployer.tx.prepare_with_tempo_wallet(provider.as_ref(), wallet).await?);
518 }
519
520 if is_legacy {
521 if self.tx.gas_price.is_none() {
522 deployer.tx.set_gas_price(provider.get_gas_price().await?);
523 }
524 } else {
525 if self.tx.gas_price.is_none() || self.tx.priority_gas_price.is_none() {
526 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.")?;
527
528 let browser_suggested_tip =
531 if browser_signer.is_some() && self.tx.priority_gas_price.is_none() {
532 provider.get_max_priority_fee_per_gas().await.ok()
533 } else {
534 None
535 };
536
537 let estimate =
540 resolve_broadcast_eip1559_fees(estimate, None, None, browser_suggested_tip)?;
541
542 if self.tx.priority_gas_price.is_none() {
543 deployer.tx.set_max_priority_fee_per_gas(estimate.max_priority_fee_per_gas);
544 }
545 if self.tx.gas_price.is_none() {
546 deployer.tx.set_max_fee_per_gas(estimate.max_fee_per_gas);
547 }
548 }
549 if let (Some(max_fee), Some(priority)) =
550 (deployer.tx.max_fee_per_gas(), deployer.tx.max_priority_fee_per_gas())
551 {
552 eyre::ensure!(
553 priority <= max_fee,
554 "max priority fee per gas ({priority}) cannot exceed max fee per gas ({max_fee})"
555 );
556 }
557 }
558
559 if let Some(access_list) = match self.tx.access_list {
561 None => None,
562 Some(None) => Some(provider.create_access_list(&deployer.tx).await?.access_list),
563 Some(Some(ref access_list)) => Some(access_list.clone()),
564 } {
565 deployer.tx.set_access_list(access_list);
566 }
567
568 if self.tx.gas_limit.is_none() {
569 let request = if browser_signer.is_some() {
570 deployer.tx.browser_wallet_gas_estimation_request()
571 } else {
572 deployer.tx.clone()
573 };
574 let estimated = provider.estimate_gas(request).await?;
575 deployer.tx.set_gas_limit(apply_gas_estimate_multiplier(
576 estimated,
577 self.gas_estimate_multiplier,
578 )?);
579 }
580
581 let mut constructor_args = None;
583 if self.verify {
584 if !args.is_empty() {
585 let encoded_args = abi
586 .constructor()
587 .ok_or_else(|| eyre::eyre!("could not find constructor"))?
588 .abi_encode_input(&args)?;
589 constructor_args = Some(hex::encode(encoded_args));
590 }
591
592 self.verify_preflight_check(constructor_args.clone(), &id).await?;
593 }
594
595 if dry_run {
596 if shell::is_json() {
597 let output = json!({
598 "contract": self.contract.name,
599 "transaction": &deployer.tx,
600 "abi":&abi
601 });
602 sh_println!("{}", serde_json::to_string_pretty(&output)?)?;
603 } else {
604 sh_warn!("Dry run enabled, not broadcasting transaction\n")?;
605
606 sh_println!("Contract: {}", self.contract.name)?;
607 sh_println!(
608 "Transaction: {}",
609 serde_json::to_string_pretty(&deployer.tx.clone())?
610 )?;
611 sh_println!("ABI: {}\n", serde_json::to_string_pretty(&abi)?)?;
612
613 sh_warn!(
614 "To broadcast this transaction, add --broadcast to the previous command. See forge create --help for more."
615 )?;
616 }
617
618 return Ok(());
619 }
620
621 if let Some(ts) = expires_at {
622 sh_status!("Transaction expires at unix timestamp {ts}")?;
623 }
624
625 let tempo_sponsor = self.tx.tempo.sponsor_config().await?;
626 if let Some(sponsor) = &tempo_sponsor {
627 sponsor
628 .resolve_and_set_fee_token(
629 resolve_unknown_fee_token_symbol.then_some(&provider),
630 &mut deployer.tx,
631 )
632 .await?;
633 sponsor.attach_and_print::<N>(&mut deployer.tx, deployer_address).await?;
634 } else {
635 let fee_token = resolve_and_set_fee_token(
636 resolve_unknown_fee_token_symbol.then_some(&provider),
637 &mut deployer.tx,
638 Some(deployer_address),
639 )
640 .await?;
641 maybe_print_fee_token(resolve_unknown_fee_token_symbol.then_some(&provider), fee_token)
642 .await?;
643 }
644
645 let (deployed_contract, receipt) = if let Some(browser) = browser_signer {
647 let tx_hash = browser.send_transaction_via_browser(deployer.tx).await?;
649
650 provider
652 .watch_pending_transaction(alloy_provider::PendingTransactionConfig::new(tx_hash))
653 .await?
654 .await?;
655
656 let receipt = provider
657 .get_transaction_receipt(tx_hash)
658 .await?
659 .ok_or_else(|| eyre::eyre!("could not get transaction receipt for {tx_hash}"))?;
660
661 if !receipt.status() {
662 eyre::bail!("deployment transaction failed (receipt status 0): {tx_hash}");
663 }
664
665 let address = receipt
666 .contract_address()
667 .ok_or_else(|| eyre::eyre!("contract was not deployed"))?;
668
669 (address, receipt)
670 } else if let Some(wallet) = tempo_keychain {
671 let raw_tx = deployer.tx.sign_with_tempo_wallet(&wallet).await?;
672
673 let receipt = provider
674 .send_raw_transaction(&raw_tx)
675 .await?
676 .with_required_confirmations(1)
677 .with_timeout(Some(Duration::from_secs(timeout)))
678 .get_receipt()
679 .await?;
680
681 let address = receipt
682 .contract_address()
683 .ok_or_else(|| eyre::eyre!("contract was not deployed"))?;
684
685 (address, receipt)
686 } else {
687 deployer.send_with_receipt().await?
688 };
689
690 let address = deployed_contract;
691 let tx_hash = receipt.transaction_hash();
692 if shell::is_json() {
693 let output = json!({
694 "deployer": deployer_address.to_string(),
695 "deployedTo": address.to_string(),
696 "transactionHash": tx_hash
697 });
698 sh_println!("{}", serde_json::to_string_pretty(&output)?)?;
699 } else {
700 sh_println!("Deployer: {deployer_address}")?;
701 sh_println!("Deployed to: {address}")?;
702 sh_println!("Transaction hash: {tx_hash:?}")?;
703 };
704
705 if !self.verify {
706 return Ok(());
707 }
708
709 sh_status!("Starting contract verification...")?;
710
711 let num_of_optimizations = if let Some(optimizer) = self.build.compiler.optimize {
712 optimizer.then(|| self.build.compiler.optimizer_runs.unwrap_or(200))
713 } else {
714 self.build.compiler.optimizer_runs
715 };
716
717 let verify = VerifyArgs {
718 address,
719 contract: Some(self.contract),
720 compiler_version: Some(id.version.to_string()),
721 constructor_args,
722 constructor_args_path: None,
723 no_auto_detect: false,
724 use_solc: None,
725 num_of_optimizations,
726 etherscan: EtherscanOpts { key: self.eth.etherscan.key(), chain: Some(chain) },
727 rpc: Default::default(),
728 flatten: false,
729 force: false,
730 skip_is_verified_check: true,
731 watch: true,
732 print_submission_result_to_stdout: false,
733 retry: self.retry,
734 libraries: self.build.libraries.clone(),
735 root: None,
736 verifier: self.verifier,
737 via_ir: self.build.compiler.via_ir,
738 license_type: self.license_type,
739 evm_version: self.build.compiler.evm_version,
740 show_standard_json_input: self.show_standard_json_input,
741 guess_constructor_args: false,
742 compilation_profile: Some(id.profile.clone()),
743 language: None,
744 creation_transaction_hash: Some(tx_hash),
745 };
746 let verify_config = verify.load_config()?;
750 let effective_key = verify_config
751 .get_etherscan_config_with_chain(Some(chain))?
752 .map(|c| c.key)
753 .or_else(|| verify_config.etherscan_api_key.clone());
754 let resolved_verifier = verify.verifier.resolve(effective_key.as_deref(), Some(chain));
755 sh_status!("Waiting for {resolved_verifier} to detect contract deployment...")?;
756 verify.run().await
757 }
758}
759
760impl figment::Provider for CreateArgs {
761 fn metadata(&self) -> Metadata {
762 Metadata::named("Create Args Provider")
763 }
764
765 fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
766 let mut dict = Dict::default();
767 if let Some(timeout) = self.timeout {
768 dict.insert("transaction_timeout".to_string(), timeout.into());
769 }
770 Ok(Map::from([(Config::selected_profile(), dict)]))
771 }
772}
773
774pub type ContractFactory<N, P> = DeploymentTxFactory<N, P>;
780
781#[derive(Debug)]
785#[must_use = "ContractDeploymentTx does nothing unless you `send` it"]
786pub struct ContractDeploymentTx<N: Network, P, C> {
787 pub deployer: Deployer<N, P>,
789 _contract: PhantomData<C>,
793}
794
795impl<N: Network, P: Clone, C> Clone for ContractDeploymentTx<N, P, C> {
796 fn clone(&self) -> Self {
797 Self { deployer: self.deployer.clone(), _contract: self._contract }
798 }
799}
800
801impl<N: Network, P, C> From<Deployer<N, P>> for ContractDeploymentTx<N, P, C> {
802 fn from(deployer: Deployer<N, P>) -> Self {
803 Self { deployer, _contract: PhantomData }
804 }
805}
806
807#[derive(Clone, Debug)]
809#[must_use = "Deployer does nothing unless you `send` it"]
810pub struct Deployer<N: Network, P> {
811 pub tx: N::TransactionRequest,
813 client: P,
814 confs: usize,
815 timeout: u64,
816}
817
818impl<N: Network, P: Provider<N>> Deployer<N, P> {
819 pub async fn send_with_receipt(
823 self,
824 ) -> Result<(Address, N::ReceiptResponse), ContractDeploymentError> {
825 let receipt = self
826 .client
827 .borrow()
828 .send_transaction(self.tx)
829 .await?
830 .with_required_confirmations(self.confs as u64)
831 .with_timeout(Some(Duration::from_secs(self.timeout)))
832 .get_receipt()
833 .await?;
834
835 if !receipt.status() {
836 return Err(ContractDeploymentError::DeploymentFailed(receipt.transaction_hash()));
837 }
838
839 let address =
840 receipt.contract_address().ok_or(ContractDeploymentError::ContractNotDeployed)?;
841
842 Ok((address, receipt))
843 }
844}
845
846#[derive(Clone, Debug)]
850pub struct DeploymentTxFactory<N: Network, P> {
851 client: P,
852 abi: JsonAbi,
853 bytecode: Bytes,
854 timeout: u64,
855 _network: PhantomData<N>,
856}
857
858impl<N: Network, P: Provider<N> + Clone> DeploymentTxFactory<N, P> {
859 pub const fn new(abi: JsonAbi, bytecode: Bytes, client: P, timeout: u64) -> Self {
863 Self { client, abi, bytecode, timeout, _network: PhantomData }
864 }
865
866 pub fn deploy_tokens(
869 self,
870 params: Vec<DynSolValue>,
871 ) -> Result<Deployer<N, P>, ContractDeploymentError>
872 where
873 N::TransactionRequest: FoundryTransactionBuilder<N>,
874 {
875 let data: Bytes = match (self.abi.constructor(), params.is_empty()) {
877 (None, false) => return Err(ContractDeploymentError::ConstructorError),
878 (None, true) => self.bytecode.clone(),
879 (Some(constructor), _) => {
880 let input: Bytes = constructor
881 .abi_encode_input(¶ms)
882 .map_err(ContractDeploymentError::DetokenizationError)?
883 .into();
884 self.bytecode.iter().copied().chain(input).collect()
886 }
887 };
888
889 let mut tx = N::TransactionRequest::default();
891 tx.set_input(data);
892 Ok(Deployer { client: self.client.clone(), tx, confs: 1, timeout: self.timeout })
893 }
894}
895
896#[derive(thiserror::Error, Debug)]
897pub enum ContractDeploymentError {
899 #[error("constructor is not defined in the ABI")]
900 ConstructorError,
901 #[error(transparent)]
902 DetokenizationError(#[from] alloy_dyn_abi::Error),
903 #[error("contract was not deployed")]
904 ContractNotDeployed,
905 #[error("deployment transaction failed (receipt status 0): {0}")]
906 DeploymentFailed(alloy_primitives::TxHash),
907 #[error(transparent)]
908 RpcError(#[from] TransportError),
909}
910
911impl From<PendingTransactionError> for ContractDeploymentError {
912 fn from(_err: PendingTransactionError) -> Self {
913 Self::ContractNotDeployed
914 }
915}
916
917#[cfg(test)]
918mod tests {
919 use super::*;
920 use alloy_json_abi::Constructor;
921 use alloy_primitives::I256;
922 use foundry_cli::utils::parse_json;
923
924 #[test]
925 fn can_parse_create() {
926 let args: CreateArgs = CreateArgs::parse_from([
927 "foundry-cli",
928 "src/Domains.sol:Domains",
929 "--verify",
930 "--retries",
931 "10",
932 "--delay",
933 "30",
934 "--license-type",
935 "13",
936 "--gas-estimate-multiplier",
937 "125",
938 ]);
939 assert_eq!(args.retry.retries, 10);
940 assert_eq!(args.retry.delay, 30);
941 assert_eq!(args.license_type.as_deref(), Some("13"));
942 assert_eq!(args.gas_estimate_multiplier, Some(125));
943 }
944
945 #[test]
946 fn create_help_hides_auth() {
947 let help = <CreateArgs as clap::CommandFactory>::command().render_long_help().to_string();
948 assert!(!help.contains("--auth"));
949 }
950
951 #[test]
952 fn can_parse_create_license_type_spdx() {
953 let args: CreateArgs = CreateArgs::parse_from([
954 "foundry-cli",
955 "src/Domains.sol:Domains",
956 "--verify",
957 "--license-type",
958 "MIT",
959 ]);
960 assert_eq!(args.license_type.as_deref(), Some("3"));
961 }
962
963 #[test]
964 fn errors_on_invalid_create_license_type() {
965 let err = CreateArgs::try_parse_from([
966 "foundry-cli",
967 "src/Domains.sol:Domains",
968 "--verify",
969 "--license-type",
970 "definitely-not-a-license",
971 ])
972 .unwrap_err();
973 assert!(err.to_string().contains("unsupported Etherscan license type"));
974 }
975
976 #[test]
977 fn can_parse_chain_id() {
978 let args: CreateArgs = CreateArgs::parse_from([
979 "foundry-cli",
980 "src/Domains.sol:Domains",
981 "--verify",
982 "--retries",
983 "10",
984 "--delay",
985 "30",
986 "--chain-id",
987 "9999",
988 ]);
989 assert_eq!(args.chain_id().map(|c| c.id()), Some(9999));
990 }
991
992 #[test]
993 fn test_parse_constructor_args() {
994 let args: CreateArgs = CreateArgs::parse_from([
995 "foundry-cli",
996 "src/Domains.sol:Domains",
997 "--constructor-args",
998 "Hello",
999 ]);
1000 let constructor: Constructor = parse_json(r#"{"type":"constructor","inputs":[{"name":"_name","type":"string","internalType":"string"}],"stateMutability":"nonpayable"}"#).unwrap();
1001 let params = parse_constructor_args(&constructor, &args.constructor_args).unwrap();
1002 assert_eq!(params, vec![DynSolValue::String("Hello".to_string())]);
1003 }
1004
1005 #[test]
1006 fn test_parse_tuple_constructor_args() {
1007 let args: CreateArgs = CreateArgs::parse_from([
1008 "foundry-cli",
1009 "src/Domains.sol:Domains",
1010 "--constructor-args",
1011 "[(1,2), (2,3), (3,4)]",
1012 ]);
1013 let constructor: Constructor = parse_json(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();
1014 let _params = parse_constructor_args(&constructor, &args.constructor_args).unwrap();
1015 }
1016
1017 #[test]
1018 fn test_parse_int_constructor_args() {
1019 let args: CreateArgs = CreateArgs::parse_from([
1020 "foundry-cli",
1021 "src/Domains.sol:Domains",
1022 "--constructor-args",
1023 "-5",
1024 ]);
1025 let constructor: Constructor = parse_json(r#"{"type":"constructor","inputs":[{"name":"_name","type":"int256","internalType":"int256"}],"stateMutability":"nonpayable"}"#).unwrap();
1026 let params = parse_constructor_args(&constructor, &args.constructor_args).unwrap();
1027 assert_eq!(params, vec![DynSolValue::Int(I256::unchecked_from(-5), 256)]);
1028 }
1029}