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