Skip to content

Commit b342ff2

Browse files
Tudmotumattsse
andauthored
feat(cheatcodes) vm.prompt: Prompt user for interactive input (foundry-rs#7012)
* Implement vm.prompt cheatcode * chore: speedup prompt test locally * move prompt.sol --------- Co-authored-by: Matthias Seitz <[email protected]>
1 parent 3e565e8 commit b342ff2

File tree

13 files changed

+138
-0
lines changed

13 files changed

+138
-0
lines changed

Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/cheatcodes/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,4 @@ walkdir = "2"
4343
p256 = "0.13.2"
4444
thiserror = "1"
4545
rustc-hash.workspace = true
46+
dialoguer = "0.11.0"

crates/cheatcodes/assets/cheatcodes.json

Lines changed: 40 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/cheatcodes/spec/src/vm.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1399,6 +1399,16 @@ interface Vm {
13991399
#[cheatcode(group = Filesystem)]
14001400
function tryFfi(string[] calldata commandInput) external returns (FfiResult memory result);
14011401

1402+
// -------- User Interaction --------
1403+
1404+
/// Prompts the user for a string value in the terminal.
1405+
#[cheatcode(group = Filesystem)]
1406+
function prompt(string calldata promptText) external returns (string memory input);
1407+
1408+
/// Prompts the user for a hidden string value in the terminal.
1409+
#[cheatcode(group = Filesystem)]
1410+
function promptSecret(string calldata promptText) external returns (string memory input);
1411+
14021412
// ======== Environment Variables ========
14031413

14041414
/// Sets environment variables.

crates/cheatcodes/src/config.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use foundry_evm_core::opts::EvmOpts;
1111
use std::{
1212
collections::HashMap,
1313
path::{Path, PathBuf},
14+
time::Duration,
1415
};
1516

1617
/// Additional, configurable context the `Cheatcodes` inspector has access to
@@ -22,6 +23,8 @@ pub struct CheatsConfig {
2223
pub ffi: bool,
2324
/// Use the create 2 factory in all cases including tests and non-broadcasting scripts.
2425
pub always_use_create_2_factory: bool,
26+
/// Sets a timeout for vm.prompt cheatcodes
27+
pub prompt_timeout: Duration,
2528
/// RPC storage caching settings determines what chains and endpoints to cache
2629
pub rpc_storage_caching: StorageCachingConfig,
2730
/// All known endpoints and their aliases
@@ -55,6 +58,7 @@ impl CheatsConfig {
5558
Self {
5659
ffi: evm_opts.ffi,
5760
always_use_create_2_factory: evm_opts.always_use_create_2_factory,
61+
prompt_timeout: Duration::from_secs(config.prompt_timeout),
5862
rpc_storage_caching: config.rpc_storage_caching.clone(),
5963
rpc_endpoints,
6064
paths: config.project_paths(),
@@ -171,6 +175,7 @@ impl Default for CheatsConfig {
171175
Self {
172176
ffi: false,
173177
always_use_create_2_factory: false,
178+
prompt_timeout: Duration::from_secs(120),
174179
rpc_storage_caching: Default::default(),
175180
rpc_endpoints: Default::default(),
176181
paths: ProjectPathsConfig::builder().build_with_root("./"),

crates/cheatcodes/src/fs.rs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,16 @@ use crate::{Cheatcode, Cheatcodes, Result, Vm::*};
44
use alloy_json_abi::ContractObject;
55
use alloy_primitives::U256;
66
use alloy_sol_types::SolValue;
7+
use dialoguer::{Input, Password};
78
use foundry_common::{fs, get_artifact_path};
89
use foundry_config::fs_permissions::FsAccessKind;
910
use std::{
1011
collections::hash_map::Entry,
1112
io::{BufRead, BufReader, Write},
1213
path::Path,
1314
process::Command,
15+
sync::mpsc,
16+
thread,
1417
time::{SystemTime, UNIX_EPOCH},
1518
};
1619
use walkdir::WalkDir;
@@ -296,6 +299,20 @@ impl Cheatcode for tryFfiCall {
296299
}
297300
}
298301

302+
impl Cheatcode for promptCall {
303+
fn apply(&self, state: &mut Cheatcodes) -> Result {
304+
let Self { promptText: text } = self;
305+
prompt(state, text, prompt_input).map(|res| res.abi_encode())
306+
}
307+
}
308+
309+
impl Cheatcode for promptSecretCall {
310+
fn apply(&self, state: &mut Cheatcodes) -> Result {
311+
let Self { promptText: text } = self;
312+
prompt(state, text, prompt_password).map(|res| res.abi_encode())
313+
}
314+
}
315+
299316
pub(super) fn write_file(state: &Cheatcodes, path: &Path, contents: &[u8]) -> Result {
300317
let path = state.config.ensure_path_allowed(path, FsAccessKind::Write)?;
301318
// write access to foundry.toml is not allowed
@@ -370,6 +387,39 @@ fn ffi(state: &Cheatcodes, input: &[String]) -> Result<FfiResult> {
370387
})
371388
}
372389

390+
fn prompt_input(prompt_text: &str) -> Result<String, dialoguer::Error> {
391+
Input::new().allow_empty(true).with_prompt(prompt_text).interact_text()
392+
}
393+
394+
fn prompt_password(prompt_text: &str) -> Result<String, dialoguer::Error> {
395+
Password::new().with_prompt(prompt_text).interact()
396+
}
397+
398+
fn prompt(
399+
state: &Cheatcodes,
400+
prompt_text: &str,
401+
input: fn(&str) -> Result<String, dialoguer::Error>,
402+
) -> Result<String> {
403+
let text_clone = prompt_text.to_string();
404+
let timeout = state.config.prompt_timeout;
405+
let (send, recv) = mpsc::channel();
406+
407+
thread::spawn(move || {
408+
send.send(input(&text_clone)).unwrap();
409+
});
410+
411+
match recv.recv_timeout(timeout) {
412+
Ok(res) => res.map_err(|err| {
413+
println!();
414+
err.to_string().into()
415+
}),
416+
Err(_) => {
417+
println!();
418+
Err("Prompt timed out".into())
419+
}
420+
}
421+
}
422+
373423
#[cfg(test)]
374424
mod tests {
375425
use super::*;

crates/config/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ match_path = "*/Foo*"
116116
no_match_path = "*/Bar*"
117117
ffi = false
118118
always_use_create_2_factory = false
119+
prompt_timeout = 120
119120
# These are the default callers, generated using `address(uint160(uint256(keccak256("foundry default caller"))))`
120121
sender = '0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38'
121122
tx_origin = '0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38'

crates/config/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,8 @@ pub struct Config {
244244
pub ffi: bool,
245245
/// Use the create 2 factory in all cases including tests and non-broadcasting scripts.
246246
pub always_use_create_2_factory: bool,
247+
/// Sets a timeout in seconds for vm.prompt cheatcodes
248+
pub prompt_timeout: u64,
247249
/// The address which will be executing all tests
248250
pub sender: Address,
249251
/// The tx.origin value during EVM execution
@@ -1873,6 +1875,7 @@ impl Default for Config {
18731875
invariant: Default::default(),
18741876
always_use_create_2_factory: false,
18751877
ffi: false,
1878+
prompt_timeout: 120,
18761879
sender: Config::DEFAULT_SENDER,
18771880
tx_origin: Config::DEFAULT_SENDER,
18781881
initial_balance: U256::from(0xffffffffffffffffffffffffu128),

crates/evm/traces/src/decoder/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -520,6 +520,7 @@ impl CallTraceDecoder {
520520
match func.name.as_str() {
521521
s if s.starts_with("env") => Some("<env var value>"),
522522
"createWallet" | "deriveKey" => Some("<pk>"),
523+
"promptSecret" => Some("<secret>"),
523524
"parseJson" if self.verbosity < 5 => Some("<encoded JSON value>"),
524525
"readFile" if self.verbosity < 5 => Some("<file>"),
525526
_ => None,

crates/forge/tests/cli/config.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ forgetest!(can_extract_config_values, |prj, cmd| {
7272
invariant: InvariantConfig { runs: 256, ..Default::default() },
7373
ffi: true,
7474
always_use_create_2_factory: false,
75+
prompt_timeout: 0,
7576
sender: "00a329c0648769A73afAc7F9381D08FB43dBEA72".parse().unwrap(),
7677
tx_origin: "00a329c0648769A73afAc7F9F81E08FB43dBEA72".parse().unwrap(),
7778
initial_balance: U256::from(0xffffffffffffffffffffffffu128),

crates/forge/tests/it/test_helpers.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,9 @@ impl ForgeTestData {
197197
config.rpc_endpoints = rpc_endpoints();
198198
config.allow_paths.push(manifest_root().to_path_buf());
199199

200+
// no prompt testing
201+
config.prompt_timeout = 0;
202+
200203
let root = self.project.root();
201204
let opts = self.evm_opts.clone();
202205
let env = opts.local_evm_env();

testdata/cheats/Vm.sol

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

testdata/default/cheats/Prompt.t.sol

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// SPDX-License-Identifier: MIT OR Apache-2.0
2+
pragma solidity 0.8.18;
3+
4+
import "ds-test/test.sol";
5+
import "cheats/Vm.sol";
6+
7+
contract PromptTest is DSTest {
8+
Vm constant vm = Vm(HEVM_ADDRESS);
9+
10+
function testPrompt_revertNotATerminal() public {
11+
// should revert in CI and testing environments either with timout or because no terminal is available
12+
vm._expectCheatcodeRevert();
13+
vm.prompt("test");
14+
15+
vm._expectCheatcodeRevert();
16+
vm.promptSecret("test");
17+
}
18+
}

0 commit comments

Comments
 (0)