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::{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
32/// Loads Cast's config and applies its normalized network to the EVM options.
33pub(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
40/// Loads the config for `rpc` and builds the default provider from it.
41pub(crate) fn rpc_provider(rpc: &RpcOpts) -> Result<RetryProvider> {
42    get_provider(&rpc.load_config()?)
43}
44
45/// Asks whether to continue after a warning, printing `Aborted.` when the user declines.
46pub(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
55/// Prints `json` pretty-printed (un-enveloped) in JSON mode, `plain` otherwise.
56pub(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
65/// Prints the primary result of a command: a JSON envelope in `--json` mode, otherwise a raw line
66/// that bypasses the shell verbosity layer so `--quiet` does not suppress it.
67pub(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
74/// Prints a raw line to stdout, bypassing the shell verbosity layer so `--quiet` does not
75/// suppress it.
76pub(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
84/// Fetches the non-empty code of `addresses` at `block` with bounded concurrency, warning about
85/// addresses whose code cannot be fetched.
86pub(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
152/// Validates a Tempo transaction requirement against an explicit execution family.
153pub(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
165/// Resolves the RPC request family before typed transaction construction.
166pub(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/// Rejects blob options before Base's transaction builder can discard them.
197#[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}