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 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}