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