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