Skip to main content

cast/cmd/
mod.rs

1//! `cast` subcommands.
2//!
3//! All subcommands should respect the `foundry_config::Config`.
4//! If a subcommand accepts values that are supported by the `Config`, then the subcommand should
5//! implement `figment::Provider` which allows the subcommand to override the config's defaults, see
6//! [`foundry_config::Config`].
7
8use 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
29/// Loads Cast's config and applies its normalized network to the EVM options.
30pub(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
37/// Loads the config for `rpc` and builds the default provider from it.
38pub(crate) fn rpc_provider(rpc: &RpcOpts) -> Result<RetryProvider> {
39    get_provider(&rpc.load_config()?)
40}
41
42/// Asks whether to continue after a warning, printing `Aborted.` when the user declines.
43pub(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
52/// Prints `json` pretty-printed (un-enveloped) in JSON mode, `plain` otherwise.
53pub(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
62/// Prints the primary result of a command: a JSON envelope in `--json` mode, otherwise a raw line
63/// that bypasses the shell verbosity layer so `--quiet` does not suppress it.
64pub(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
71/// Prints a raw line to stdout, bypassing the shell verbosity layer so `--quiet` does not
72/// suppress it.
73pub(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
81/// Fetches the non-empty code of `addresses` at `block` with bounded concurrency, warning about
82/// addresses whose code cannot be fetched.
83pub(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
149/// Validates a Tempo transaction requirement against an explicit execution family.
150pub(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
162/// Resolves the RPC request family before typed transaction construction.
163pub(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/// Rejects blob options before Base's transaction builder can discard them.
194#[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}