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 foundry_config::Chain;
214
215 #[tokio::test]
216 async fn transaction_network_respects_explicit_selection() {
217 for network in [NetworkVariant::Ethereum, NetworkVariant::Tempo] {
218 let config =
219 Config { networks: network.into(), eth_rpc_curl: true, ..Default::default() };
220 assert_eq!(resolve_transaction_network(&config, false).await.unwrap(), network);
221 assert_eq!(
222 resolve_transaction_network(&config, true).await.is_ok(),
223 network.is_tempo()
224 );
225 }
226 let config = Config { eth_rpc_curl: true, ..Default::default() };
227 assert_eq!(
228 resolve_transaction_network(&config, true).await.unwrap(),
229 NetworkVariant::Tempo
230 );
231 assert_eq!(
232 resolve_transaction_network(&config, false).await.unwrap(),
233 NetworkVariant::Ethereum
234 );
235
236 let config = Config {
237 networks: foundry_evm_networks::NetworkConfigs::with_celo(),
238 eth_rpc_curl: true,
239 ..Default::default()
240 };
241 assert_eq!(
242 resolve_transaction_network(&config, false).await.unwrap(),
243 NetworkVariant::Ethereum
244 );
245 assert_eq!(
246 resolve_transaction_network(&config, true).await.unwrap_err().to_string(),
247 "Tempo transaction options conflict with configured network `celo`"
248 );
249 }
250
251 #[cfg(feature = "monad")]
252 #[test]
253 fn normalized_hardfork_network_is_applied_to_evm_opts() {
254 let figment = Config::figment().merge(("hardfork", "monad:MonadNine"));
255 let (config, evm_opts) = load_cast_config_and_evm_opts(figment).unwrap();
256
257 assert!(config.networks.is_monad());
258 assert!(evm_opts.networks.is_monad());
259 }
260
261 #[cfg(feature = "base")]
262 #[tokio::test]
263 async fn resolve_network_preserves_explicit_base() {
264 let config = Config { networks: NetworkVariant::Base.into(), ..Default::default() };
265 assert_eq!(
266 resolve_transaction_network(&config, false).await.unwrap(),
267 NetworkVariant::Base
268 );
269 assert_eq!(
270 resolve_transaction_network(&config, true).await.unwrap_err().to_string(),
271 "Tempo transaction options conflict with configured network `base`"
272 );
273 }
274
275 #[cfg(feature = "base")]
276 #[tokio::test]
277 async fn resolve_network_infers_base_from_chain_id() {
278 let config = Config { chain: Some(Chain::base_mainnet()), ..Default::default() };
279 assert_eq!(
280 resolve_transaction_network(&config, false).await.unwrap(),
281 NetworkVariant::Base
282 );
283 }
284
285 #[cfg(all(any(feature = "base", feature = "optimism"), not(feature = "monad")))]
286 #[tokio::test]
287 async fn resolve_network_allows_rpc_without_local_evm() {
288 let config = Config {
289 chain: Some(foundry_config::Chain::from_named(alloy_chains::NamedChain::Monad)),
290 ..Default::default()
291 };
292 assert_eq!(
293 resolve_transaction_network(&config, false).await.unwrap(),
294 NetworkVariant::Ethereum
295 );
296 }
297
298 #[cfg(feature = "base")]
299 #[tokio::test]
300 async fn resolve_network_preserves_config_over_chain_in_curl_mode() {
301 let config = Config {
302 networks: NetworkVariant::Base.into(),
303 chain: Some(foundry_config::Chain::from_id(31337)),
304 eth_rpc_curl: true,
305 ..Default::default()
306 };
307 assert_eq!(
308 resolve_transaction_network(&config, false).await.unwrap(),
309 NetworkVariant::Base
310 );
311 let config = Config {
312 networks: NetworkVariant::Ethereum.into(),
313 chain: Some(foundry_config::Chain::from_id(8453)),
314 ..config
315 };
316 assert_eq!(
317 resolve_transaction_network(&config, false).await.unwrap(),
318 NetworkVariant::Ethereum
319 );
320 }
321
322 #[cfg(all(feature = "base", not(feature = "optimism")))]
323 #[tokio::test]
324 async fn resolve_network_allows_rpc_without_optimism() {
325 let config =
326 Config { chain: Some(foundry_config::Chain::from_id(10)), ..Default::default() };
327 assert_eq!(
328 resolve_transaction_network(&config, false).await.unwrap(),
329 NetworkVariant::Ethereum
330 );
331 }
332
333 #[cfg(any(feature = "base", feature = "optimism"))]
334 #[tokio::test]
335 async fn resolve_network_still_defaults_unknown_chain_ids_to_ethereum() {
336 let config =
337 Config { chain: Some(foundry_config::Chain::from_id(u64::MAX)), ..Default::default() };
338 assert_eq!(
339 resolve_transaction_network(&config, false).await.unwrap(),
340 NetworkVariant::Ethereum
341 );
342 }
343}