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) => eyre::bail!("forge script wallet session exited with code {code}"),
217                None => eyre::bail!("forge script wallet session terminated by a signal"),
218            }
219        }
220    }
221
222    fn wallet_session_command_from_env(&self) -> Result<WalletSessionCommand> {
223        let forge = std::env::current_exe().wrap_err("failed to resolve current forge binary")?;
224        let cast = sibling_binary(&forge, "cast");
225        self.wallet_session_command_from_raw_args(std::env::args_os(), forge.into(), cast.into())
226    }
227
228    /// Builds the `cast wallet session` command that implements `forge script --session`.
229    ///
230    /// `raw_args` is the original `forge script` argv. Wrapper-only `--session-*` flags are removed
231    /// from the inner command, while the corresponding policy, RPC, and root signer options are
232    /// translated onto the outer `cast wallet session` invocation.
233    fn wallet_session_command_from_raw_args<I>(
234        &self,
235        raw_args: I,
236        forge_program: OsString,
237        cast_program: OsString,
238    ) -> Result<WalletSessionCommand>
239    where
240        I: IntoIterator<Item = OsString>,
241    {
242        self.wallet_session.validate(self)?;
243
244        // Reconstruct the command that `cast wallet session --for` will run. The inner `forge`
245        // must not see wrapper-only flags such as `--session-private-key`.
246        let mut inner = strip_wallet_session_args(raw_args)?;
247        let Some(program) = inner.first_mut() else {
248            eyre::bail!("failed to reconstruct forge script command");
249        };
250        *program = forge_program;
251        let inner = quote_command(&inner)?;
252
253        let (config, evm_opts) = self.load_config_and_evm_opts()?;
254        let session = &self.wallet_session;
255        let mut args = vec![OsString::from("wallet"), OsString::from("session")];
256
257        // The outer `cast` process creates and later revokes the temporary access key, so it needs
258        // the session policy, RPC transport settings, and root signer configuration itself.
259        if let Some(root) = session.root {
260            push_arg(&mut args, "--root", root.to_string());
261            push_arg(&mut args, "--from", root.to_string());
262        }
263        push_opt_arg(&mut args, "--expires", session.expires.as_deref());
264
265        push_repeated_args(&mut args, "--scope", &session.scopes);
266        push_opt_arg(&mut args, "--target", session.target);
267        push_repeated_args(&mut args, "--selector", &session.selectors);
268        push_repeated_args(&mut args, "--spend-limit", &session.spend_limits);
269
270        if let Some(rpc_url) = evm_opts.fork_url.as_ref() {
271            push_arg(&mut args, "--rpc-url", rpc_url);
272        }
273        push_opt_arg(&mut args, "--chain", evm_opts.env.chain_id);
274        if config.eth_rpc_accept_invalid_certs {
275            args.push("--insecure".into());
276        }
277        if config.eth_rpc_no_proxy {
278            args.push("--no-proxy".into());
279        }
280        push_opt_arg(&mut args, "--rpc-timeout", config.eth_rpc_timeout);
281
282        session.push_root_signer_args(&mut args);
283
284        push_arg(&mut args, "--for", inner);
285
286        Ok(WalletSessionCommand { program: cast_program, args })
287    }
288}
289
290impl ScriptWalletSessionArgs {
291    const STRIP_BOOL_ARGS: &[&str] =
292        &["--session", "--session-interactive", "--session-ledger", "--session-trezor"];
293
294    const STRIP_VALUE_ARGS: &[&str] = &[
295        "--session-root",
296        "--session-expires",
297        "--session-scope",
298        "--session-target",
299        "--session-selector",
300        "--session-spend-limit",
301        "--session-private-key",
302        "--session-mnemonic",
303        "--session-mnemonic-passphrase",
304        "--session-hd-path",
305        "--session-mnemonic-index",
306        "--session-keystore",
307        "--session-account",
308        "--session-password",
309        "--session-password-file",
310    ];
311
312    /// Rejects `--session` combinations that cannot be represented by the wallet-session wrapper.
313    ///
314    /// Temporary sessions only make sense for a script run that will submit or resume transactions,
315    /// and debugger runs should stay in the normal in-process execution path.
316    fn validate(&self, args: &ScriptArgs) -> Result<()> {
317        if !self.enabled {
318            return Ok(());
319        }
320        // `cast wallet session --for` creates credentials for the wrapped command; dry-run and
321        // debugger-only flows do not need a temporary signing session.
322        if !args.should_broadcast() {
323            eyre::bail!("forge script --session requires --broadcast or --resume");
324        }
325        if args.debug {
326            eyre::bail!("forge script --session cannot be used with --debug");
327        }
328        Ok(())
329    }
330
331    /// Appends root-signer wallet flags for the outer `cast wallet session` command.
332    ///
333    /// `forge script` exposes these as `--session-*` flags so they do not leak into the inner
334    /// script command. Here they are translated back to the wallet flags that `cast` already
335    /// understands for signing the session authorization.
336    fn push_root_signer_args(&self, args: &mut Vec<OsString>) {
337        if self.interactive {
338            args.push("--interactive".into());
339        }
340        for (name, value) in [
341            ("--private-key", self.private_key.as_deref()),
342            ("--mnemonic", self.mnemonic.as_deref()),
343            ("--mnemonic-passphrase", self.mnemonic_passphrase.as_deref()),
344            ("--hd-path", self.hd_path.as_deref()),
345            ("--keystore", self.keystore.as_deref()),
346            ("--account", self.account.as_deref()),
347            ("--password", self.password.as_deref()),
348            ("--password-file", self.password_file.as_deref()),
349        ] {
350            push_opt_arg(args, name, value);
351        }
352        push_opt_arg(args, "--mnemonic-index", self.mnemonic_index);
353        if self.ledger {
354            args.push("--ledger".into());
355        }
356        if self.trezor {
357            args.push("--trezor".into());
358        }
359    }
360}
361
362#[derive(Debug)]
363struct WalletSessionCommand {
364    program: OsString,
365    args: Vec<OsString>,
366}
367
368fn push_arg(args: &mut Vec<OsString>, name: &'static str, value: impl Into<OsString>) {
369    args.push(name.into());
370    args.push(value.into());
371}
372
373fn push_opt_arg(
374    args: &mut Vec<OsString>,
375    name: &'static str,
376    value: Option<impl std::fmt::Display>,
377) {
378    if let Some(value) = value {
379        push_arg(args, name, value.to_string());
380    }
381}
382
383fn push_repeated_args(args: &mut Vec<OsString>, name: &'static str, values: &[String]) {
384    for value in values {
385        push_arg(args, name, value.as_str());
386    }
387}
388
389/// Resolves a sibling Foundry binary first, falling back to `PATH` for source-tree test binaries.
390fn sibling_binary(current: &Path, name: &str) -> PathBuf {
391    let mut binary = current.with_file_name(name);
392    if cfg!(windows) {
393        binary.set_extension("exe");
394    }
395    if binary.exists() { binary } else { PathBuf::from(name) }
396}
397
398/// Removes `forge script --session` wrapper flags from the command passed to `--for`.
399///
400/// Arguments after `--` and non-UTF-8 values are preserved verbatim because they belong to the
401/// script invocation, not to the wrapper's option parser.
402fn strip_wallet_session_args<I>(raw_args: I) -> Result<Vec<OsString>>
403where
404    I: IntoIterator<Item = OsString>,
405{
406    let mut out = Vec::new();
407    let mut args = raw_args.into_iter();
408    let mut after_double_dash = false;
409
410    while let Some(arg) = args.next() {
411        if after_double_dash {
412            out.push(arg);
413            continue;
414        }
415        if arg == OsStr::new("--") {
416            after_double_dash = true;
417            out.push(arg);
418            continue;
419        }
420
421        let Some(arg_str) = arg.to_str() else {
422            out.push(arg);
423            continue;
424        };
425
426        if ScriptWalletSessionArgs::STRIP_BOOL_ARGS.contains(&arg_str) {
427            continue;
428        }
429        let (flag, has_value) =
430            arg_str.split_once('=').map_or((arg_str, false), |(flag, _)| (flag, true));
431        if ScriptWalletSessionArgs::STRIP_VALUE_ARGS.contains(&flag) {
432            // Support both `--session-root=value` and `--session-root value` while consuming only
433            // the wrapper option and its value.
434            if has_value {
435                continue;
436            }
437            args.next().ok_or_else(|| eyre::eyre!("{arg_str} requires a value"))?;
438            continue;
439        }
440
441        out.push(arg);
442    }
443
444    Ok(out)
445}
446
447/// Converts the reconstructed inner argv into the single command string accepted by
448/// `cast wallet session --for`.
449fn quote_command(args: &[OsString]) -> Result<String> {
450    args.iter().map(quote_arg).collect::<Result<Vec<_>>>().map(|args| args.join(" "))
451}
452
453/// Quotes one argv item for `--for` while preserving the exact value after shell-style splitting.
454fn quote_arg(arg: &OsString) -> Result<String> {
455    let arg = arg
456        .to_str()
457        .ok_or_else(|| eyre::eyre!("forge script wallet session commands must be valid UTF-8"))?;
458    if arg.is_empty() {
459        return Ok("\"\"".to_string());
460    }
461    if arg.chars().all(|ch| !ch.is_whitespace() && !matches!(ch, '"' | '\'' | '\\')) {
462        return Ok(arg.to_string());
463    }
464
465    let mut quoted = String::with_capacity(arg.len() + 2);
466    quoted.push('"');
467    for ch in arg.chars() {
468        if matches!(ch, '"' | '\\') {
469            quoted.push('\\');
470        }
471        quoted.push(ch);
472    }
473    quoted.push('"');
474    Ok(quoted)
475}
476
477#[cfg(test)]
478mod tests {
479    use super::*;
480    use clap::Parser;
481    use foundry_config::Config;
482    use std::{borrow::Cow, fs};
483    use tempfile::tempdir;
484
485    const SESSION_PRIVATE_KEY: &str =
486        "0x59c6995e998f97a5a004497e5da3b5d2b2b66a87f064d39c44da0b6d6e4f8ff0";
487    const SESSION_ROOT_ADDRESS: &str = "0x1111111111111111111111111111111111111111";
488    const SESSION_SCOPE_ADDRESS: &str = "0x2222222222222222222222222222222222222222";
489
490    fn option_value<'a>(args: &'a [Cow<'_, str>], option: &str) -> Option<&'a str> {
491        args.windows(2)
492            .find_map(|window| (window[0].as_ref() == option).then_some(window[1].as_ref()))
493    }
494
495    fn parse_script_args(args: &[&str]) -> ScriptArgs {
496        ScriptArgs::parse_from(["foundry-cli"].into_iter().chain(args.iter().copied()))
497    }
498
499    fn raw_forge_script_args<'a>(args: &'a [&'a str]) -> impl Iterator<Item = OsString> + 'a {
500        ["forge", "script"].into_iter().chain(args.iter().copied()).map(OsString::from)
501    }
502
503    fn wallet_session_command(
504        args: &ScriptArgs,
505        raw_args: &[&str],
506    ) -> Result<WalletSessionCommand> {
507        args.wallet_session_command_from_raw_args(
508            raw_forge_script_args(raw_args),
509            OsString::from("/tmp/forge"),
510            OsString::from("/tmp/cast"),
511        )
512    }
513
514    fn command_args(command: &WalletSessionCommand) -> Vec<Cow<'_, str>> {
515        command.args.iter().map(|arg| arg.to_string_lossy()).collect()
516    }
517
518    fn inner_for_command<'a>(args: &'a [Cow<'_, str>]) -> &'a str {
519        let for_pos = args.iter().position(|arg| arg.as_ref() == "--for").unwrap();
520        args[for_pos + 1].as_ref()
521    }
522
523    fn session_root() -> Address {
524        SESSION_ROOT_ADDRESS.parse().unwrap()
525    }
526
527    fn session_target() -> Address {
528        SESSION_SCOPE_ADDRESS.parse().unwrap()
529    }
530
531    #[test]
532    fn can_parse_session_wrapper() {
533        let root = session_root();
534        let target = session_target();
535        let args = ScriptArgs::parse_from([
536            "foundry-cli",
537            "Deploy.s.sol",
538            "--broadcast",
539            "--session",
540            "--session-root",
541            &root.to_string(),
542            "--session-expires",
543            "10m",
544            "--session-target",
545            &target.to_string(),
546            "--session-selector",
547            "register(address)",
548            "--session-spend-limit",
549            "PathUSD=0",
550            "--session-private-key",
551            SESSION_PRIVATE_KEY,
552        ]);
553
554        assert!(args.wallet_session.enabled);
555        assert_eq!(args.wallet_session.root, Some(root));
556        assert_eq!(args.wallet_session.expires.as_deref(), Some("10m"));
557        assert_eq!(args.wallet_session.target, Some(target));
558        assert_eq!(args.wallet_session.selectors, ["register(address)"]);
559        assert_eq!(args.wallet_session.spend_limits, ["PathUSD=0"]);
560        assert_eq!(args.wallet_session.private_key.as_deref(), Some(SESSION_PRIVATE_KEY));
561    }
562
563    #[test]
564    fn session_wrapper_conflicts_with_existing_session_id() {
565        let err = ScriptArgs::try_parse_from([
566            "foundry-cli",
567            "Deploy.s.sol",
568            "--session",
569            "--tempo.session",
570            "0x1111111111111111111111111111111111111111111111111111111111111111",
571        ])
572        .unwrap_err();
573
574        assert!(err.to_string().contains("cannot be used with"), "{err}");
575    }
576
577    #[test]
578    fn session_wrapper_rejects_dry_run() {
579        let raw_args = [
580            "Deploy.s.sol",
581            "--session",
582            "--session-root",
583            SESSION_ROOT_ADDRESS,
584            "--session-expires",
585            "10m",
586            "--session-scope",
587            SESSION_SCOPE_ADDRESS,
588        ];
589        let args = parse_script_args(&raw_args);
590
591        let err = wallet_session_command(&args, &raw_args).unwrap_err();
592
593        assert!(err.to_string().contains("requires --broadcast or --resume"), "{err}");
594    }
595
596    #[test]
597    fn session_wrapper_rejects_debug() {
598        let raw_args = [
599            "Deploy.s.sol",
600            "--broadcast",
601            "--debug",
602            "--session",
603            "--session-root",
604            SESSION_ROOT_ADDRESS,
605            "--session-expires",
606            "10m",
607            "--session-scope",
608            SESSION_SCOPE_ADDRESS,
609        ];
610        let args = parse_script_args(&raw_args);
611
612        let err = wallet_session_command(&args, &raw_args).unwrap_err();
613
614        assert!(err.to_string().contains("cannot be used with --debug"), "{err}");
615    }
616
617    #[test]
618    fn session_wrapper_rewrites_to_cast_session_command() {
619        let root = session_root();
620        let target = session_target();
621        let root_arg = root.to_string();
622        let target_arg = target.to_string();
623        let raw_args = [
624            "Deploy.s.sol",
625            "--broadcast",
626            "--rpc-url",
627            "http://127.0.0.1:8545",
628            "--chain",
629            "4217",
630            "--session",
631            "--session-root",
632            &root_arg,
633            "--session-expires",
634            "10m",
635            "--session-target",
636            &target_arg,
637            "--session-selector",
638            "register(address)",
639            "--session-private-key",
640            SESSION_PRIVATE_KEY,
641        ];
642        let args = parse_script_args(&raw_args);
643
644        let command = wallet_session_command(&args, &raw_args).unwrap();
645
646        assert_eq!(command.program, OsString::from("/tmp/cast"));
647        let command_args = command_args(&command);
648        assert_eq!(command_args[0], "wallet");
649        assert_eq!(command_args[1], "session");
650        assert!(command_args.contains(&"--root".into()));
651        assert!(command_args.contains(&root.to_string().into()));
652        assert!(command_args.contains(&"--from".into()));
653        assert!(command_args.contains(&"--target".into()));
654        assert!(command_args.contains(&target.to_string().into()));
655        assert!(command_args.contains(&"--selector".into()));
656        assert!(command_args.contains(&"register(address)".into()));
657        assert!(command_args.contains(&"--private-key".into()));
658        assert!(command_args.contains(&SESSION_PRIVATE_KEY.into()));
659
660        let inner = inner_for_command(&command_args);
661        assert!(inner.starts_with("/tmp/forge script Deploy.s.sol --broadcast"), "{inner}");
662        assert!(inner.contains("--rpc-url http://127.0.0.1:8545"), "{inner}");
663        assert!(inner.contains("--chain 4217"), "{inner}");
664        assert!(!inner.contains("--session "), "{inner}");
665        assert!(!inner.contains("--session-private-key"), "{inner}");
666    }
667
668    #[test]
669    fn session_wrapper_cleans_inherited_tempo_signer_env_for_outer_cast() {
670        assert_eq!(
671            SESSION_WRAPPER_ENV_REMOVE,
672            [
673                TEMPO_SESSION_ID_ENV,
674                "ETH_KEYSTORE",
675                "ETH_KEYSTORE_ACCOUNT",
676                "ETH_PASSWORD",
677                "TEMPO_ACCESS_KEY",
678                "TEMPO_ROOT_ACCOUNT",
679            ]
680        );
681    }
682
683    #[test]
684    fn session_wrapper_uses_project_config_for_cast_session() {
685        let temp = tempdir().unwrap();
686        let project_root = temp.path();
687        fs::write(
688            project_root.join(Config::FILE_NAME),
689            r#"
690                [profile.default]
691                eth_rpc_url = "http://127.0.0.1:8545"
692                chain_id = 4217
693            "#,
694        )
695        .unwrap();
696
697        let root = session_root();
698        let root_arg = root.to_string();
699        let project_root_arg = project_root.to_string_lossy();
700        let raw_args = [
701            "Deploy.s.sol",
702            "--root",
703            &project_root_arg,
704            "--broadcast",
705            "--session",
706            "--session-root",
707            &root_arg,
708            "--session-expires",
709            "10m",
710            "--session-scope",
711            SESSION_SCOPE_ADDRESS,
712        ];
713        let args = parse_script_args(&raw_args);
714
715        let command = wallet_session_command(&args, &raw_args).unwrap();
716
717        let command_args = command_args(&command);
718        assert_eq!(option_value(&command_args, "--rpc-url"), Some("http://127.0.0.1:8545"));
719        assert_eq!(option_value(&command_args, "--chain"), Some("4217"));
720
721        let inner = inner_for_command(&command_args);
722        assert!(inner.contains("--root "), "{inner}");
723        if !cfg!(windows) {
724            assert!(inner.contains(project_root.to_string_lossy().as_ref()), "{inner}");
725        }
726        assert!(inner.ends_with("--broadcast"), "{inner}");
727    }
728
729    #[test]
730    fn session_wrapper_forwards_rpc_transport_flags_to_outer_cast() {
731        let root = session_root();
732        let root_arg = root.to_string();
733        let raw_args = [
734            "Deploy.s.sol",
735            "--broadcast",
736            "--rpc-url",
737            "https://127.0.0.1:8545",
738            "--insecure",
739            "--no-proxy",
740            "--rpc-timeout",
741            "7",
742            "--session",
743            "--session-root",
744            &root_arg,
745            "--session-expires",
746            "10m",
747            "--session-scope",
748            SESSION_SCOPE_ADDRESS,
749            "--session-private-key",
750            SESSION_PRIVATE_KEY,
751        ];
752        let args = parse_script_args(&raw_args);
753
754        let command = wallet_session_command(&args, &raw_args).unwrap();
755
756        let command_args = command_args(&command);
757        assert!(command_args.contains(&"--insecure".into()));
758        assert!(command_args.contains(&"--no-proxy".into()));
759        assert_eq!(option_value(&command_args, "--rpc-timeout"), Some("7"));
760
761        let inner = inner_for_command(&command_args);
762        assert!(inner.contains("--insecure"), "{inner}");
763        assert!(inner.contains("--no-proxy"), "{inner}");
764        assert!(inner.contains("--rpc-timeout 7"), "{inner}");
765    }
766
767    #[test]
768    fn session_wrapper_leaves_browser_for_inner_forge_validation() {
769        let raw_args = [
770            "Deploy.s.sol",
771            "--broadcast",
772            "--session",
773            "--session-root",
774            SESSION_ROOT_ADDRESS,
775            "--session-expires",
776            "10m",
777            "--session-scope",
778            SESSION_SCOPE_ADDRESS,
779            "--browser",
780        ];
781        let args = parse_script_args(&raw_args);
782
783        let command = wallet_session_command(&args, &raw_args).unwrap();
784        let command_args = command_args(&command);
785        let inner = inner_for_command(&command_args);
786
787        assert!(inner.contains("--browser"), "{inner}");
788    }
789
790    #[test]
791    fn session_wrapper_leaves_script_wallet_signers_for_inner_forge_validation() {
792        let raw_args = [
793            "Deploy.s.sol",
794            "--broadcast",
795            "--session",
796            "--session-root",
797            SESSION_ROOT_ADDRESS,
798            "--session-expires",
799            "10m",
800            "--session-scope",
801            SESSION_SCOPE_ADDRESS,
802            "--private-key",
803            SESSION_PRIVATE_KEY,
804        ];
805        let args = parse_script_args(&raw_args);
806
807        let command = wallet_session_command(&args, &raw_args).unwrap();
808        let command_args = command_args(&command);
809        let inner = inner_for_command(&command_args);
810
811        assert!(inner.contains("--private-key"), "{inner}");
812        assert!(!inner.contains("--session-private-key"), "{inner}");
813    }
814
815    #[test]
816    fn session_wrapper_does_not_infer_root_from_sender() {
817        let raw_args = [
818            "Deploy.s.sol",
819            "--broadcast",
820            "--session",
821            "--sender",
822            SESSION_ROOT_ADDRESS,
823            "--session-expires",
824            "10m",
825            "--session-scope",
826            SESSION_SCOPE_ADDRESS,
827        ];
828        let args = parse_script_args(&raw_args);
829
830        let command = wallet_session_command(&args, &raw_args).unwrap();
831        let command_args = command_args(&command);
832        assert_eq!(option_value(&command_args, "--root"), None);
833        assert_eq!(option_value(&command_args, "--from"), None);
834
835        let inner = inner_for_command(&command_args);
836        assert!(inner.contains(&format!("--sender {SESSION_ROOT_ADDRESS}")), "{inner}");
837    }
838
839    #[test]
840    fn session_wrapper_leaves_session_policy_requirements_to_cast() {
841        let raw_args = ["Deploy.s.sol", "--broadcast", "--session"];
842        let args = parse_script_args(&raw_args);
843
844        let command = wallet_session_command(&args, &raw_args).unwrap();
845        let command_args = command_args(&command);
846
847        assert_eq!(option_value(&command_args, "--root"), None);
848        assert_eq!(option_value(&command_args, "--expires"), None);
849        assert_eq!(option_value(&command_args, "--scope"), None);
850        assert_eq!(option_value(&command_args, "--target"), None);
851    }
852}