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