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