Skip to main content

forge_script/
wallet_session.rs

1//! `forge script --session` CLI wrapper support.
2//!
3//! This module translates forge-specific `--session-*` flags into the outer
4//! `cast wallet session --for` invocation and keeps those wrapper details out of the main script
5//! execution flow.
6
7use crate::ScriptArgs;
8use alloy_primitives::Address;
9use clap::Args;
10use eyre::{Result, WrapErr};
11use foundry_cli::{opts::TEMPO_SESSION_ID_ENV, utils::LoadConfig};
12use std::{
13    ffi::{OsStr, OsString},
14    path::{Path, PathBuf},
15    process::Command,
16};
17
18const SESSION_WRAPPER_ENV_REMOVE: &[&str] = &[
19    TEMPO_SESSION_ID_ENV,
20    "ETH_KEYSTORE",
21    "ETH_KEYSTORE_ACCOUNT",
22    "ETH_PASSWORD",
23    "TEMPO_ACCESS_KEY",
24    "TEMPO_ROOT_ACCOUNT",
25];
26
27/// Arguments that make `forge script` a thin wrapper around `cast wallet session --for`.
28#[derive(Clone, Debug, Default, Args)]
29pub struct ScriptWalletSessionArgs {
30    /// Create a temporary Tempo wallet session for this script run.
31    #[arg(
32        long = "session",
33        id = "wallet_session",
34        conflicts_with_all = ["tempo_session", "unlocked"]
35    )]
36    pub enabled: bool,
37
38    /// Root account that authorizes the temporary session.
39    #[arg(
40        long = "session-root",
41        id = "wallet_session_root",
42        value_name = "ADDRESS",
43        requires = "wallet_session"
44    )]
45    pub root: Option<Address>,
46
47    /// Session lifetime, expressed as a duration like `10m`, `2h`, or `7d`.
48    #[arg(
49        long = "session-expires",
50        id = "wallet_session_expires",
51        value_name = "DURATION",
52        requires = "wallet_session"
53    )]
54    pub expires: Option<String>,
55
56    /// Allowed call scope, in `TARGET[:SELECTORS[@RECIPIENTS]]` format.
57    #[arg(
58        long = "session-scope",
59        id = "wallet_session_scope",
60        value_name = "SCOPE",
61        requires = "wallet_session"
62    )]
63    pub scopes: Vec<String>,
64
65    /// Allowed call target for issue-style `--target ... --selector ...` input.
66    #[arg(
67        long = "session-target",
68        id = "wallet_session_target",
69        value_name = "ADDRESS",
70        requires = "wallet_session"
71    )]
72    pub target: Option<Address>,
73
74    /// Function selector allowed for `--session-target`, such as `register(address)`.
75    #[arg(
76        long = "session-selector",
77        id = "wallet_session_selector",
78        value_name = "SELECTOR",
79        requires = "wallet_session"
80    )]
81    pub selectors: Vec<String>,
82
83    /// Token spend limit, in `TOKEN:AMOUNT` or `TOKEN=AMOUNT` format.
84    #[arg(
85        long = "session-spend-limit",
86        id = "wallet_session_spend_limit",
87        value_name = "LIMIT",
88        requires = "wallet_session"
89    )]
90    pub spend_limits: Vec<String>,
91
92    /// Open an interactive prompt for the root private key.
93    #[arg(
94        long = "session-interactive",
95        id = "wallet_session_interactive",
96        requires = "wallet_session"
97    )]
98    pub interactive: bool,
99
100    /// Root private key that signs the session authorization.
101    #[arg(
102        long = "session-private-key",
103        id = "wallet_session_private_key",
104        value_name = "RAW_PRIVATE_KEY",
105        requires = "wallet_session"
106    )]
107    pub private_key: Option<String>,
108
109    /// Root mnemonic phrase or mnemonic file path.
110    #[arg(
111        long = "session-mnemonic",
112        id = "wallet_session_mnemonic",
113        value_name = "MNEMONIC",
114        requires = "wallet_session"
115    )]
116    pub mnemonic: Option<String>,
117
118    /// Passphrase for `--session-mnemonic`.
119    #[arg(
120        long = "session-mnemonic-passphrase",
121        id = "wallet_session_mnemonic_passphrase",
122        value_name = "PASSPHRASE",
123        requires = "wallet_session_mnemonic"
124    )]
125    pub mnemonic_passphrase: Option<String>,
126
127    /// Wallet derivation path for the root signer.
128    #[arg(
129        long = "session-hd-path",
130        id = "wallet_session_hd_path",
131        value_name = "PATH",
132        requires = "wallet_session"
133    )]
134    pub hd_path: Option<String>,
135
136    /// Mnemonic or hardware-wallet index for the root signer.
137    #[arg(
138        long = "session-mnemonic-index",
139        id = "wallet_session_mnemonic_index",
140        value_name = "INDEX",
141        requires = "wallet_session"
142    )]
143    pub mnemonic_index: Option<u32>,
144
145    /// Root keystore path.
146    #[arg(
147        long = "session-keystore",
148        id = "wallet_session_keystore",
149        value_name = "PATH",
150        requires = "wallet_session"
151    )]
152    pub keystore: Option<String>,
153
154    /// Root account name from the default keystore directory.
155    #[arg(
156        long = "session-account",
157        id = "wallet_session_account",
158        value_name = "ACCOUNT_NAME",
159        requires = "wallet_session"
160    )]
161    pub account: Option<String>,
162
163    /// Root keystore password.
164    #[arg(
165        long = "session-password",
166        id = "wallet_session_password",
167        value_name = "PASSWORD",
168        requires = "wallet_session"
169    )]
170    pub password: Option<String>,
171
172    /// Root keystore password file.
173    #[arg(
174        long = "session-password-file",
175        id = "wallet_session_password_file",
176        value_name = "PATH",
177        requires = "wallet_session"
178    )]
179    pub password_file: Option<String>,
180
181    /// Use a Ledger as the root signer.
182    #[arg(long = "session-ledger", id = "wallet_session_ledger", requires = "wallet_session")]
183    pub ledger: bool,
184
185    /// Use a Trezor as the root signer.
186    #[arg(long = "session-trezor", id = "wallet_session_trezor", requires = "wallet_session")]
187    pub trezor: bool,
188}
189
190impl ScriptArgs {
191    /// Runs `forge script --session ...` by delegating to `cast wallet session --for`.
192    ///
193    /// The outer `cast` process owns the temporary session lifecycle: create the scoped access key,
194    /// run the reconstructed inner `forge script` command, then revoke the key on exit.
195    pub(super) fn run_wallet_session_wrapper(&self) -> Result<()> {
196        let command = self.wallet_session_command_from_env()?;
197        let mut child = Command::new(&command.program);
198        child.args(&command.args);
199        // The outer `cast wallet session` must resolve the root signer from explicit
200        // `--session-*` inputs, not from stale session/access-key env inherited from the shell.
201        for key in SESSION_WRAPPER_ENV_REMOVE {
202            child.env_remove(key);
203        }
204
205        let status = child.status().wrap_err_with(|| {
206            format!(
207                "failed to run `{}` for forge script wallet session",
208                command.program.to_string_lossy()
209            )
210        })?;
211
212        if status.success() {
213            Ok(())
214        } else {
215            match status.code() {
216                Some(code) => {
217                    eyre::bail!("forge script wallet session exited with code {code}");
218                }
219                None => {
220                    eyre::bail!("forge script wallet session terminated by a signal");
221                }
222            }
223        }
224    }
225
226    fn wallet_session_command_from_env(&self) -> Result<WalletSessionCommand> {
227        let forge = std::env::current_exe().wrap_err("failed to resolve current forge binary")?;
228        let cast = sibling_binary(&forge, "cast");
229        self.wallet_session_command_from_raw_args(std::env::args_os(), forge.into(), cast.into())
230    }
231
232    /// Builds the `cast wallet session` command that implements `forge script --session`.
233    ///
234    /// `raw_args` is the original `forge script` argv. Wrapper-only `--session-*` flags are removed
235    /// from the inner command, while the corresponding policy, RPC, and root signer options are
236    /// translated onto the outer `cast wallet session` invocation.
237    fn wallet_session_command_from_raw_args<I>(
238        &self,
239        raw_args: I,
240        forge_program: OsString,
241        cast_program: OsString,
242    ) -> Result<WalletSessionCommand>
243    where
244        I: IntoIterator<Item = OsString>,
245    {
246        self.wallet_session.validate(self)?;
247
248        // Reconstruct the command that `cast wallet session --for` will run. The inner `forge`
249        // must not see wrapper-only flags such as `--session-private-key`.
250        let mut inner = strip_wallet_session_args(raw_args)?;
251        let Some(program) = inner.first_mut() else {
252            eyre::bail!("failed to reconstruct forge script command");
253        };
254        *program = forge_program;
255        let inner = quote_command(&inner)?;
256
257        let (config, evm_opts) = self.load_config_and_evm_opts()?;
258        let session = &self.wallet_session;
259        let mut args = vec![OsString::from("wallet"), OsString::from("session")];
260
261        // The outer `cast` process creates and later revokes the temporary access key, so it needs
262        // the session policy, RPC transport settings, and root signer configuration itself.
263        if let Some(root) = session.root {
264            push_arg(&mut args, "--root", root.to_string());
265            push_arg(&mut args, "--from", root.to_string());
266        }
267        push_opt_arg(&mut args, "--expires", session.expires.as_deref());
268
269        push_repeated_args(&mut args, "--scope", &session.scopes);
270        push_opt_arg(&mut args, "--target", session.target);
271        push_repeated_args(&mut args, "--selector", &session.selectors);
272        push_repeated_args(&mut args, "--spend-limit", &session.spend_limits);
273
274        if let Some(rpc_url) = evm_opts.fork_url.as_ref() {
275            push_arg(&mut args, "--rpc-url", rpc_url);
276        }
277        push_opt_arg(&mut args, "--chain", evm_opts.env.chain_id);
278        if config.eth_rpc_accept_invalid_certs {
279            args.push("--insecure".into());
280        }
281        if config.eth_rpc_no_proxy {
282            args.push("--no-proxy".into());
283        }
284        push_opt_arg(&mut args, "--rpc-timeout", config.eth_rpc_timeout);
285
286        session.push_root_signer_args(&mut args);
287
288        push_arg(&mut args, "--for", inner);
289
290        Ok(WalletSessionCommand { program: cast_program, args })
291    }
292}
293
294impl ScriptWalletSessionArgs {
295    const STRIP_BOOL_ARGS: &[&str] =
296        &["--session", "--session-interactive", "--session-ledger", "--session-trezor"];
297
298    const STRIP_VALUE_ARGS: &[&str] = &[
299        "--session-root",
300        "--session-expires",
301        "--session-scope",
302        "--session-target",
303        "--session-selector",
304        "--session-spend-limit",
305        "--session-private-key",
306        "--session-mnemonic",
307        "--session-mnemonic-passphrase",
308        "--session-hd-path",
309        "--session-mnemonic-index",
310        "--session-keystore",
311        "--session-account",
312        "--session-password",
313        "--session-password-file",
314    ];
315
316    /// Rejects `--session` combinations that cannot be represented by the wallet-session wrapper.
317    ///
318    /// Temporary sessions only make sense for a script run that will submit or resume transactions,
319    /// and debugger runs should stay in the normal in-process execution path.
320    fn validate(&self, args: &ScriptArgs) -> Result<()> {
321        if !self.enabled {
322            return Ok(());
323        }
324        // `cast wallet session --for` creates credentials for the wrapped command; dry-run and
325        // debugger-only flows do not need a temporary signing session.
326        if !args.should_broadcast() {
327            eyre::bail!("forge script --session requires --broadcast or --resume");
328        }
329        if args.debug {
330            eyre::bail!("forge script --session cannot be used with --debug");
331        }
332        Ok(())
333    }
334
335    /// Appends root-signer wallet flags for the outer `cast wallet session` command.
336    ///
337    /// `forge script` exposes these as `--session-*` flags so they do not leak into the inner
338    /// script command. Here they are translated back to the wallet flags that `cast` already
339    /// understands for signing the session authorization.
340    fn push_root_signer_args(&self, args: &mut Vec<OsString>) {
341        if self.interactive {
342            args.push("--interactive".into());
343        }
344        for (name, value) in [
345            ("--private-key", self.private_key.as_deref()),
346            ("--mnemonic", self.mnemonic.as_deref()),
347            ("--mnemonic-passphrase", self.mnemonic_passphrase.as_deref()),
348            ("--hd-path", self.hd_path.as_deref()),
349            ("--keystore", self.keystore.as_deref()),
350            ("--account", self.account.as_deref()),
351            ("--password", self.password.as_deref()),
352            ("--password-file", self.password_file.as_deref()),
353        ] {
354            push_opt_arg(args, name, value);
355        }
356        push_opt_arg(args, "--mnemonic-index", self.mnemonic_index);
357        if self.ledger {
358            args.push("--ledger".into());
359        }
360        if self.trezor {
361            args.push("--trezor".into());
362        }
363    }
364}
365
366#[derive(Debug)]
367struct WalletSessionCommand {
368    program: OsString,
369    args: Vec<OsString>,
370}
371
372fn push_arg(args: &mut Vec<OsString>, name: &'static str, value: impl Into<OsString>) {
373    args.push(name.into());
374    args.push(value.into());
375}
376
377fn push_opt_arg(
378    args: &mut Vec<OsString>,
379    name: &'static str,
380    value: Option<impl std::fmt::Display>,
381) {
382    if let Some(value) = value {
383        push_arg(args, name, value.to_string());
384    }
385}
386
387fn push_repeated_args(args: &mut Vec<OsString>, name: &'static str, values: &[String]) {
388    for value in values {
389        push_arg(args, name, value.as_str());
390    }
391}
392
393/// Resolves a sibling Foundry binary first, falling back to `PATH` for source-tree test binaries.
394fn sibling_binary(current: &Path, name: &str) -> PathBuf {
395    let mut binary = current.with_file_name(name);
396    if cfg!(windows) {
397        binary.set_extension("exe");
398    }
399    if binary.exists() { binary } else { PathBuf::from(name) }
400}
401
402/// Removes `forge script --session` wrapper flags from the command passed to `--for`.
403///
404/// Arguments after `--` and non-UTF-8 values are preserved verbatim because they belong to the
405/// script invocation, not to the wrapper's option parser.
406fn strip_wallet_session_args<I>(raw_args: I) -> Result<Vec<OsString>>
407where
408    I: IntoIterator<Item = OsString>,
409{
410    let mut out = Vec::new();
411    let mut args = raw_args.into_iter();
412    let mut after_double_dash = false;
413
414    while let Some(arg) = args.next() {
415        if after_double_dash {
416            out.push(arg);
417            continue;
418        }
419        if arg == OsStr::new("--") {
420            after_double_dash = true;
421            out.push(arg);
422            continue;
423        }
424
425        let Some(arg_str) = arg.to_str() else {
426            out.push(arg);
427            continue;
428        };
429
430        if ScriptWalletSessionArgs::STRIP_BOOL_ARGS.contains(&arg_str) {
431            continue;
432        }
433        let (flag, has_value) =
434            arg_str.split_once('=').map_or((arg_str, false), |(flag, _)| (flag, true));
435        if ScriptWalletSessionArgs::STRIP_VALUE_ARGS.contains(&flag) {
436            // Support both `--session-root=value` and `--session-root value` while consuming only
437            // the wrapper option and its value.
438            if has_value {
439                continue;
440            }
441            args.next().ok_or_else(|| eyre::eyre!("{arg_str} requires a value"))?;
442            continue;
443        }
444
445        out.push(arg);
446    }
447
448    Ok(out)
449}
450
451/// Converts the reconstructed inner argv into the single command string accepted by
452/// `cast wallet session --for`.
453fn quote_command(args: &[OsString]) -> Result<String> {
454    args.iter().map(quote_arg).collect::<Result<Vec<_>>>().map(|args| args.join(" "))
455}
456
457/// Quotes one argv item for `--for` while preserving the exact value after shell-style splitting.
458fn quote_arg(arg: &OsString) -> Result<String> {
459    let arg = arg
460        .to_str()
461        .ok_or_else(|| eyre::eyre!("forge script wallet session commands must be valid UTF-8"))?;
462    if arg.is_empty() {
463        return Ok("\"\"".to_string());
464    }
465    if arg.chars().all(|ch| !ch.is_whitespace() && !matches!(ch, '"' | '\'' | '\\')) {
466        return Ok(arg.to_string());
467    }
468
469    let mut quoted = String::with_capacity(arg.len() + 2);
470    quoted.push('"');
471    for ch in arg.chars() {
472        if matches!(ch, '"' | '\\') {
473            quoted.push('\\');
474        }
475        quoted.push(ch);
476    }
477    quoted.push('"');
478    Ok(quoted)
479}
480
481#[cfg(test)]
482mod tests {
483    use super::*;
484    use clap::Parser;
485    use foundry_config::Config;
486    use std::{borrow::Cow, fs};
487    use tempfile::tempdir;
488
489    const SESSION_PRIVATE_KEY: &str =
490        "0x59c6995e998f97a5a004497e5da3b5d2b2b66a87f064d39c44da0b6d6e4f8ff0";
491    const SESSION_ROOT_ADDRESS: &str = "0x1111111111111111111111111111111111111111";
492    const SESSION_SCOPE_ADDRESS: &str = "0x2222222222222222222222222222222222222222";
493
494    fn option_value<'a>(args: &'a [Cow<'_, str>], option: &str) -> Option<&'a str> {
495        args.windows(2)
496            .find_map(|window| (window[0].as_ref() == option).then_some(window[1].as_ref()))
497    }
498
499    fn parse_script_args(args: &[&str]) -> ScriptArgs {
500        ScriptArgs::parse_from(["foundry-cli"].into_iter().chain(args.iter().copied()))
501    }
502
503    fn raw_forge_script_args<'a>(args: &'a [&'a str]) -> impl Iterator<Item = OsString> + 'a {
504        ["forge", "script"].into_iter().chain(args.iter().copied()).map(OsString::from)
505    }
506
507    fn wallet_session_command(
508        args: &ScriptArgs,
509        raw_args: &[&str],
510    ) -> Result<WalletSessionCommand> {
511        args.wallet_session_command_from_raw_args(
512            raw_forge_script_args(raw_args),
513            OsString::from("/tmp/forge"),
514            OsString::from("/tmp/cast"),
515        )
516    }
517
518    fn command_args(command: &WalletSessionCommand) -> Vec<Cow<'_, str>> {
519        command.args.iter().map(|arg| arg.to_string_lossy()).collect()
520    }
521
522    fn inner_for_command<'a>(args: &'a [Cow<'_, str>]) -> &'a str {
523        let for_pos = args.iter().position(|arg| arg.as_ref() == "--for").unwrap();
524        args[for_pos + 1].as_ref()
525    }
526
527    fn session_root() -> Address {
528        SESSION_ROOT_ADDRESS.parse().unwrap()
529    }
530
531    fn session_target() -> Address {
532        SESSION_SCOPE_ADDRESS.parse().unwrap()
533    }
534
535    #[test]
536    fn can_parse_session_wrapper() {
537        let root = session_root();
538        let target = session_target();
539        let args = ScriptArgs::parse_from([
540            "foundry-cli",
541            "Deploy.s.sol",
542            "--broadcast",
543            "--session",
544            "--session-root",
545            &root.to_string(),
546            "--session-expires",
547            "10m",
548            "--session-target",
549            &target.to_string(),
550            "--session-selector",
551            "register(address)",
552            "--session-spend-limit",
553            "PathUSD=0",
554            "--session-private-key",
555            SESSION_PRIVATE_KEY,
556        ]);
557
558        assert!(args.wallet_session.enabled);
559        assert_eq!(args.wallet_session.root, Some(root));
560        assert_eq!(args.wallet_session.expires.as_deref(), Some("10m"));
561        assert_eq!(args.wallet_session.target, Some(target));
562        assert_eq!(args.wallet_session.selectors, ["register(address)"]);
563        assert_eq!(args.wallet_session.spend_limits, ["PathUSD=0"]);
564        assert_eq!(args.wallet_session.private_key.as_deref(), Some(SESSION_PRIVATE_KEY));
565    }
566
567    #[test]
568    fn session_wrapper_conflicts_with_existing_session_id() {
569        let err = ScriptArgs::try_parse_from([
570            "foundry-cli",
571            "Deploy.s.sol",
572            "--session",
573            "--tempo.session",
574            "0x1111111111111111111111111111111111111111111111111111111111111111",
575        ])
576        .unwrap_err();
577
578        assert!(err.to_string().contains("cannot be used with"), "{err}");
579    }
580
581    #[test]
582    fn session_wrapper_rejects_dry_run() {
583        let raw_args = [
584            "Deploy.s.sol",
585            "--session",
586            "--session-root",
587            SESSION_ROOT_ADDRESS,
588            "--session-expires",
589            "10m",
590            "--session-scope",
591            SESSION_SCOPE_ADDRESS,
592        ];
593        let args = parse_script_args(&raw_args);
594
595        let err = wallet_session_command(&args, &raw_args).unwrap_err();
596
597        assert!(err.to_string().contains("requires --broadcast or --resume"), "{err}");
598    }
599
600    #[test]
601    fn session_wrapper_rejects_debug() {
602        let raw_args = [
603            "Deploy.s.sol",
604            "--broadcast",
605            "--debug",
606            "--session",
607            "--session-root",
608            SESSION_ROOT_ADDRESS,
609            "--session-expires",
610            "10m",
611            "--session-scope",
612            SESSION_SCOPE_ADDRESS,
613        ];
614        let args = parse_script_args(&raw_args);
615
616        let err = wallet_session_command(&args, &raw_args).unwrap_err();
617
618        assert!(err.to_string().contains("cannot be used with --debug"), "{err}");
619    }
620
621    #[test]
622    fn session_wrapper_rewrites_to_cast_session_command() {
623        let root = session_root();
624        let target = session_target();
625        let root_arg = root.to_string();
626        let target_arg = target.to_string();
627        let raw_args = [
628            "Deploy.s.sol",
629            "--broadcast",
630            "--rpc-url",
631            "http://127.0.0.1:8545",
632            "--chain",
633            "4217",
634            "--session",
635            "--session-root",
636            &root_arg,
637            "--session-expires",
638            "10m",
639            "--session-target",
640            &target_arg,
641            "--session-selector",
642            "register(address)",
643            "--session-private-key",
644            SESSION_PRIVATE_KEY,
645        ];
646        let args = parse_script_args(&raw_args);
647
648        let command = wallet_session_command(&args, &raw_args).unwrap();
649
650        assert_eq!(command.program, OsString::from("/tmp/cast"));
651        let command_args = command_args(&command);
652        assert_eq!(command_args[0], "wallet");
653        assert_eq!(command_args[1], "session");
654        assert!(command_args.contains(&"--root".into()));
655        assert!(command_args.contains(&root.to_string().into()));
656        assert!(command_args.contains(&"--from".into()));
657        assert!(command_args.contains(&"--target".into()));
658        assert!(command_args.contains(&target.to_string().into()));
659        assert!(command_args.contains(&"--selector".into()));
660        assert!(command_args.contains(&"register(address)".into()));
661        assert!(command_args.contains(&"--private-key".into()));
662        assert!(command_args.contains(&SESSION_PRIVATE_KEY.into()));
663
664        let inner = inner_for_command(&command_args);
665        assert!(inner.starts_with("/tmp/forge script Deploy.s.sol --broadcast"), "{inner}");
666        assert!(inner.contains("--rpc-url http://127.0.0.1:8545"), "{inner}");
667        assert!(inner.contains("--chain 4217"), "{inner}");
668        assert!(!inner.contains("--session "), "{inner}");
669        assert!(!inner.contains("--session-private-key"), "{inner}");
670    }
671
672    #[test]
673    fn session_wrapper_cleans_inherited_tempo_signer_env_for_outer_cast() {
674        assert_eq!(
675            SESSION_WRAPPER_ENV_REMOVE,
676            [
677                TEMPO_SESSION_ID_ENV,
678                "ETH_KEYSTORE",
679                "ETH_KEYSTORE_ACCOUNT",
680                "ETH_PASSWORD",
681                "TEMPO_ACCESS_KEY",
682                "TEMPO_ROOT_ACCOUNT",
683            ]
684        );
685    }
686
687    #[test]
688    fn session_wrapper_uses_project_config_for_cast_session() {
689        let temp = tempdir().unwrap();
690        let project_root = temp.path();
691        fs::write(
692            project_root.join(Config::FILE_NAME),
693            r#"
694                [profile.default]
695                eth_rpc_url = "http://127.0.0.1:8545"
696                chain_id = 4217
697            "#,
698        )
699        .unwrap();
700
701        let root = session_root();
702        let root_arg = root.to_string();
703        let project_root_arg = project_root.to_string_lossy();
704        let raw_args = [
705            "Deploy.s.sol",
706            "--root",
707            &project_root_arg,
708            "--broadcast",
709            "--session",
710            "--session-root",
711            &root_arg,
712            "--session-expires",
713            "10m",
714            "--session-scope",
715            SESSION_SCOPE_ADDRESS,
716        ];
717        let args = parse_script_args(&raw_args);
718
719        let command = wallet_session_command(&args, &raw_args).unwrap();
720
721        let command_args = command_args(&command);
722        assert_eq!(option_value(&command_args, "--rpc-url"), Some("http://127.0.0.1:8545"));
723        assert_eq!(option_value(&command_args, "--chain"), Some("4217"));
724
725        let inner = inner_for_command(&command_args);
726        assert!(inner.contains("--root "), "{inner}");
727        if !cfg!(windows) {
728            assert!(inner.contains(project_root.to_string_lossy().as_ref()), "{inner}");
729        }
730        assert!(inner.ends_with("--broadcast"), "{inner}");
731    }
732
733    #[test]
734    fn session_wrapper_forwards_rpc_transport_flags_to_outer_cast() {
735        let root = session_root();
736        let root_arg = root.to_string();
737        let raw_args = [
738            "Deploy.s.sol",
739            "--broadcast",
740            "--rpc-url",
741            "https://127.0.0.1:8545",
742            "--insecure",
743            "--no-proxy",
744            "--rpc-timeout",
745            "7",
746            "--session",
747            "--session-root",
748            &root_arg,
749            "--session-expires",
750            "10m",
751            "--session-scope",
752            SESSION_SCOPE_ADDRESS,
753            "--session-private-key",
754            SESSION_PRIVATE_KEY,
755        ];
756        let args = parse_script_args(&raw_args);
757
758        let command = wallet_session_command(&args, &raw_args).unwrap();
759
760        let command_args = command_args(&command);
761        assert!(command_args.contains(&"--insecure".into()));
762        assert!(command_args.contains(&"--no-proxy".into()));
763        assert_eq!(option_value(&command_args, "--rpc-timeout"), Some("7"));
764
765        let inner = inner_for_command(&command_args);
766        assert!(inner.contains("--insecure"), "{inner}");
767        assert!(inner.contains("--no-proxy"), "{inner}");
768        assert!(inner.contains("--rpc-timeout 7"), "{inner}");
769    }
770
771    #[test]
772    fn session_wrapper_leaves_browser_for_inner_forge_validation() {
773        let raw_args = [
774            "Deploy.s.sol",
775            "--broadcast",
776            "--session",
777            "--session-root",
778            SESSION_ROOT_ADDRESS,
779            "--session-expires",
780            "10m",
781            "--session-scope",
782            SESSION_SCOPE_ADDRESS,
783            "--browser",
784        ];
785        let args = parse_script_args(&raw_args);
786
787        let command = wallet_session_command(&args, &raw_args).unwrap();
788        let command_args = command_args(&command);
789        let inner = inner_for_command(&command_args);
790
791        assert!(inner.contains("--browser"), "{inner}");
792    }
793
794    #[test]
795    fn session_wrapper_leaves_script_wallet_signers_for_inner_forge_validation() {
796        let raw_args = [
797            "Deploy.s.sol",
798            "--broadcast",
799            "--session",
800            "--session-root",
801            SESSION_ROOT_ADDRESS,
802            "--session-expires",
803            "10m",
804            "--session-scope",
805            SESSION_SCOPE_ADDRESS,
806            "--private-key",
807            SESSION_PRIVATE_KEY,
808        ];
809        let args = parse_script_args(&raw_args);
810
811        let command = wallet_session_command(&args, &raw_args).unwrap();
812        let command_args = command_args(&command);
813        let inner = inner_for_command(&command_args);
814
815        assert!(inner.contains("--private-key"), "{inner}");
816        assert!(!inner.contains("--session-private-key"), "{inner}");
817    }
818
819    #[test]
820    fn session_wrapper_does_not_infer_root_from_sender() {
821        let raw_args = [
822            "Deploy.s.sol",
823            "--broadcast",
824            "--session",
825            "--sender",
826            SESSION_ROOT_ADDRESS,
827            "--session-expires",
828            "10m",
829            "--session-scope",
830            SESSION_SCOPE_ADDRESS,
831        ];
832        let args = parse_script_args(&raw_args);
833
834        let command = wallet_session_command(&args, &raw_args).unwrap();
835        let command_args = command_args(&command);
836        assert_eq!(option_value(&command_args, "--root"), None);
837        assert_eq!(option_value(&command_args, "--from"), None);
838
839        let inner = inner_for_command(&command_args);
840        assert!(inner.contains(&format!("--sender {SESSION_ROOT_ADDRESS}")), "{inner}");
841    }
842
843    #[test]
844    fn session_wrapper_leaves_session_policy_requirements_to_cast() {
845        let raw_args = ["Deploy.s.sol", "--broadcast", "--session"];
846        let args = parse_script_args(&raw_args);
847
848        let command = wallet_session_command(&args, &raw_args).unwrap();
849        let command_args = command_args(&command);
850
851        assert_eq!(option_value(&command_args, "--root"), None);
852        assert_eq!(option_value(&command_args, "--expires"), None);
853        assert_eq!(option_value(&command_args, "--scope"), None);
854        assert_eq!(option_value(&command_args, "--target"), None);
855    }
856}