1use alloy_network::{AnyNetwork, Network};
9use alloy_primitives::{Address, Bytes, map::AddressHashMap};
10use alloy_provider::Provider;
11use alloy_rpc_types::BlockId;
12use eyre::Result;
13use foundry_cli::{
14 json::print_scalar,
15 opts::RpcOpts,
16 utils::{LoadConfig, get_provider, load_config_from_provider},
17};
18use foundry_common::{
19 provider::{ProviderBuilder, RetryProvider},
20 shell,
21};
22use foundry_config::{Config, figment::Figment};
23use foundry_evm::{core::bytecode::InstIter, opts::EvmOpts};
24use foundry_evm_networks::NetworkVariant;
25use futures::StreamExt;
26use serde::Serialize;
27use serde_json::Value;
28use std::fmt::{Display, Write};
29
30const MAX_CONCURRENT_RPC_REQUESTS: usize = 5;
31
32pub(crate) fn load_cast_config_and_evm_opts(figment: Figment) -> Result<(Box<Config>, EvmOpts)> {
34 let config = Box::new(load_config_from_provider(figment.clone())?);
35 let mut evm_opts = figment.extract::<EvmOpts>()?;
36 evm_opts.networks = config.networks;
37 Ok((config, evm_opts))
38}
39
40pub(crate) fn rpc_provider(rpc: &RpcOpts) -> Result<RetryProvider> {
42 get_provider(&rpc.load_config()?)
43}
44
45pub(crate) fn confirm_continue() -> Result<bool> {
47 let response: String = foundry_common::prompt!("\nContinue anyway? [y/N] ")?;
48 if matches!(response.trim(), "y" | "Y") {
49 return Ok(true);
50 }
51 sh_status!("Aborted.")?;
52 Ok(false)
53}
54
55pub(crate) fn print_json_or(json: Value, plain: impl Display) -> Result<()> {
57 if shell::is_json() {
58 sh_println!("{}", serde_json::to_string_pretty(&json)?)?;
59 } else {
60 sh_println!("{plain}")?;
61 }
62 Ok(())
63}
64
65pub(crate) fn print_result_line(value: impl Serialize + Display) -> Result<()> {
68 if shell::is_json() {
69 return print_scalar(value);
70 }
71 print_raw_line(value)
72}
73
74pub(crate) fn print_raw_line(value: impl Display) -> Result<()> {
77 let mut shell = shell::Shell::get();
78 let out = shell.out();
79 writeln!(out, "{value}")?;
80 out.flush()?;
81 Ok(())
82}
83
84pub(crate) async fn fetch_code_via_rpc<N: Network, P: Provider<N>>(
87 provider: &P,
88 addresses: impl IntoIterator<Item = Address>,
89 block: BlockId,
90) -> AddressHashMap<Bytes> {
91 let mut code_by_address = AddressHashMap::default();
92 let mut requests = futures::stream::iter(addresses)
93 .map(
94 |address| async move { (address, provider.get_code_at(address).block_id(block).await) },
95 )
96 .buffer_unordered(MAX_CONCURRENT_RPC_REQUESTS);
97 while let Some((address, code)) = requests.next().await {
98 match code {
99 Ok(code) if !code.is_empty() => {
100 code_by_address.insert(address, code);
101 }
102 Ok(_) => {}
103 Err(err) => {
104 let _ = sh_warn!("Failed to fetch code for {address}: {err}");
105 }
106 }
107 }
108 code_by_address
109}
110
111pub mod access_list;
112pub mod artifact;
113mod auth;
114pub mod b2e_payload;
115pub mod bal;
116pub mod batch_mktx;
117pub mod batch_send;
118pub mod bind;
119pub mod call;
120pub mod call_overrides;
121pub mod constructor_args;
122pub mod create2;
123pub mod creation_code;
124#[cfg(any(feature = "base", feature = "optimism"))]
125pub mod da_estimate;
126pub mod erc20;
127pub mod erc4626;
128pub mod estimate;
129pub mod events;
130pub mod find_block;
131pub mod interface;
132pub mod keychain;
133pub mod logs;
134pub(crate) mod miner;
135pub mod mktx;
136pub mod receive_policy;
137pub mod rpc;
138pub mod run;
139pub mod safe;
140pub mod send;
141pub mod storage;
142pub mod storage_credits;
143pub mod tempo;
144pub(crate) mod tempo_policy_args;
145pub mod tip20;
146pub mod tip403;
147pub mod trace;
148pub mod txpool;
149pub mod vaddr;
150pub mod wallet;
151
152pub(crate) fn validate_tempo_network(config: &Config, requires_tempo: bool) -> Result<()> {
154 if requires_tempo && config.networks.has_network_selection() {
155 let network = config.networks.execution_network();
156 eyre::ensure!(
157 network.is_tempo(),
158 "Tempo transaction options conflict with configured network `{}`",
159 config.networks.execution_profile_name()
160 );
161 }
162 Ok(())
163}
164
165pub(crate) async fn resolve_transaction_network(
167 config: &Config,
168 requires_tempo: bool,
169) -> Result<NetworkVariant> {
170 validate_tempo_network(config, requires_tempo)?;
171 if requires_tempo {
172 return Ok(NetworkVariant::Tempo);
173 }
174 if config.networks.has_network_selection() {
175 return Ok(config.networks.execution_network());
176 }
177 if let Some(chain) = config.chain {
178 return Ok(chain.id().into());
179 }
180 if config.eth_rpc_curl {
181 return Ok(NetworkVariant::Ethereum);
182 }
183
184 let provider = ProviderBuilder::<AnyNetwork>::from_config(config)?.build()?;
185 Ok(provider.get_chain_id().await?.into())
186}
187
188pub(crate) fn disassemble(code: &[u8]) -> Result<String> {
189 let mut output = String::new();
190 for (pc, inst) in InstIter::new(code).with_pc() {
191 writeln!(output, "{pc:08x}: {inst}")?;
192 }
193 Ok(output)
194}
195
196#[cfg(feature = "base")]
198pub(crate) fn validate_base_transaction_options(
199 tx: &foundry_cli::opts::TransactionOpts,
200) -> eyre::Result<()> {
201 eyre::ensure!(
202 !tx.blob && !tx.eip4844 && tx.blob_gas_price.is_none(),
203 "Base does not support blob transactions; remove --blob, --eip4844, and --blob-gas-price"
204 );
205 Ok(())
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211
212 #[cfg(feature = "base")]
213 use alloy_chains::NamedChain;
214 #[cfg(feature = "base")]
215 use foundry_config::Chain;
216
217 #[tokio::test]
218 async fn transaction_network_respects_explicit_selection() {
219 for network in [NetworkVariant::Ethereum, NetworkVariant::Tempo] {
220 let config =
221 Config { networks: network.into(), eth_rpc_curl: true, ..Default::default() };
222 assert_eq!(resolve_transaction_network(&config, false).await.unwrap(), network);
223 assert_eq!(
224 resolve_transaction_network(&config, true).await.is_ok(),
225 network.is_tempo()
226 );
227 }
228 let config = Config { eth_rpc_curl: true, ..Default::default() };
229 assert_eq!(
230 resolve_transaction_network(&config, true).await.unwrap(),
231 NetworkVariant::Tempo
232 );
233 assert_eq!(
234 resolve_transaction_network(&config, false).await.unwrap(),
235 NetworkVariant::Ethereum
236 );
237
238 let config = Config {
239 networks: foundry_evm_networks::NetworkConfigs::with_celo(),
240 eth_rpc_curl: true,
241 ..Default::default()
242 };
243 assert_eq!(
244 resolve_transaction_network(&config, false).await.unwrap(),
245 NetworkVariant::Ethereum
246 );
247 assert_eq!(
248 resolve_transaction_network(&config, true).await.unwrap_err().to_string(),
249 "Tempo transaction options conflict with configured network `celo`"
250 );
251 }
252
253 #[cfg(feature = "monad")]
254 #[test]
255 fn normalized_hardfork_network_is_applied_to_evm_opts() {
256 let figment = Config::figment().merge(("hardfork", "monad:MonadNine"));
257 let (config, evm_opts) = load_cast_config_and_evm_opts(figment).unwrap();
258
259 assert!(config.networks.is_monad());
260 assert!(evm_opts.networks.is_monad());
261 }
262
263 #[cfg(feature = "base")]
264 #[tokio::test]
265 async fn resolve_network_preserves_explicit_base() {
266 let config = Config { networks: NetworkVariant::Base.into(), ..Default::default() };
267 assert_eq!(
268 resolve_transaction_network(&config, false).await.unwrap(),
269 NetworkVariant::Base
270 );
271 assert_eq!(
272 resolve_transaction_network(&config, true).await.unwrap_err().to_string(),
273 "Tempo transaction options conflict with configured network `base`"
274 );
275 }
276
277 #[cfg(feature = "base")]
278 #[tokio::test]
279 async fn resolve_network_infers_base_from_chain_id() {
280 let config =
281 Config { chain: Some(Chain::from_named(NamedChain::Base)), ..Default::default() };
282 assert_eq!(
283 resolve_transaction_network(&config, false).await.unwrap(),
284 NetworkVariant::Base
285 );
286 }
287
288 #[cfg(all(any(feature = "base", feature = "optimism"), not(feature = "monad")))]
289 #[tokio::test]
290 async fn resolve_network_allows_rpc_without_local_evm() {
291 let config = Config {
292 chain: Some(foundry_config::Chain::from_named(alloy_chains::NamedChain::Monad)),
293 ..Default::default()
294 };
295 assert_eq!(
296 resolve_transaction_network(&config, false).await.unwrap(),
297 NetworkVariant::Ethereum
298 );
299 }
300
301 #[cfg(feature = "base")]
302 #[tokio::test]
303 async fn resolve_network_preserves_config_over_chain_in_curl_mode() {
304 let config = Config {
305 networks: NetworkVariant::Base.into(),
306 chain: Some(foundry_config::Chain::from_id(31337)),
307 eth_rpc_curl: true,
308 ..Default::default()
309 };
310 assert_eq!(
311 resolve_transaction_network(&config, false).await.unwrap(),
312 NetworkVariant::Base
313 );
314 let config = Config {
315 networks: NetworkVariant::Ethereum.into(),
316 chain: Some(foundry_config::Chain::from_id(8453)),
317 ..config
318 };
319 assert_eq!(
320 resolve_transaction_network(&config, false).await.unwrap(),
321 NetworkVariant::Ethereum
322 );
323 }
324
325 #[cfg(all(feature = "base", not(feature = "optimism")))]
326 #[tokio::test]
327 async fn resolve_network_allows_rpc_without_optimism() {
328 let config =
329 Config { chain: Some(foundry_config::Chain::from_id(10)), ..Default::default() };
330 assert_eq!(
331 resolve_transaction_network(&config, false).await.unwrap(),
332 NetworkVariant::Ethereum
333 );
334 }
335
336 #[cfg(any(feature = "base", feature = "optimism"))]
337 #[tokio::test]
338 async fn resolve_network_still_defaults_unknown_chain_ids_to_ethereum() {
339 let config =
340 Config { chain: Some(foundry_config::Chain::from_id(u64::MAX)), ..Default::default() };
341 assert_eq!(
342 resolve_transaction_network(&config, false).await.unwrap(),
343 NetworkVariant::Ethereum
344 );
345 }
346}