Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions FULL_HELP_DOCS.md
Original file line number Diff line number Diff line change
Expand Up @@ -1283,6 +1283,10 @@ Remove an identity

- `<NAME>` — Identity to remove

###### **Options:**

- `--force` — Skip confirmation prompt

###### **Options (Global):**

- `--global` — ⚠️ Deprecated: global config is always on
Expand Down
112 changes: 112 additions & 0 deletions cmd/crates/soroban-test/tests/it/integration/keys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -210,3 +210,115 @@ async fn unset_default_identity() {
.stdout(predicate::str::contains("STELLAR_ACCOUNT=").not())
.success();
}

#[tokio::test]
async fn rm_requires_confirmation() {
let sandbox = &TestEnv::new();
sandbox
.new_assert_cmd("keys")
.arg("generate")
.arg("rmtest1")
.assert()
.success();

// Piping "n" should cancel removal
sandbox
.new_assert_cmd("keys")
.arg("rm")
.arg("rmtest1")
.write_stdin("n\n")
.assert()
.stderr(predicate::str::contains("removal cancelled by user"))
.failure();

sandbox
.new_assert_cmd("keys")
.arg("address")
.arg("rmtest1")
.assert()
.success();

// Piping empty input (just Enter) should default to cancel
sandbox
.new_assert_cmd("keys")
.arg("rm")
.arg("rmtest1")
.write_stdin("\n")
.assert()
.stderr(predicate::str::contains("removal cancelled by user"))
.failure();

sandbox
.new_assert_cmd("keys")
.arg("address")
.arg("rmtest1")
.assert()
.success();

// Piping "y" should confirm removal
sandbox
.new_assert_cmd("keys")
.arg("rm")
.arg("rmtest1")
.write_stdin("y\n")
.assert()
.stderr(predicate::str::contains(
"Removing the key's cli config file",
))
.success();

sandbox
.new_assert_cmd("keys")
.arg("address")
.arg("rmtest1")
.assert()
.failure();
}

#[tokio::test]
async fn rm_with_force_skips_confirmation() {
let sandbox = &TestEnv::new();
sandbox
.new_assert_cmd("keys")
.arg("generate")
.arg("rmtest2")
.assert()
.success();

sandbox
.new_assert_cmd("keys")
.arg("rm")
.arg("rmtest2")
.arg("--force")
.assert()
.success();

sandbox
.new_assert_cmd("keys")
.arg("address")
.arg("rmtest2")
.assert()
.failure();
}

#[tokio::test]
async fn rm_nonexistent_key() {
let sandbox = &TestEnv::new();

// Without --force: should fail before prompting
sandbox
.new_assert_cmd("keys")
.arg("rm")
.arg("doesnotexist")
.assert()
.failure();

// With --force: should still fail
sandbox
.new_assert_cmd("keys")
.arg("rm")
.arg("doesnotexist")
.arg("--force")
.assert()
.failure();
}
31 changes: 31 additions & 0 deletions cmd/soroban-cli/src/commands/keys/rm.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
use std::io::{self, BufRead, IsTerminal};

use crate::commands::global;
use crate::config::address::KeyName;
use crate::print::Print;

use super::super::config::locator;

#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error(transparent)]
Locator(#[from] locator::Error),
#[error("removal cancelled by user")]
Cancelled,
#[error(transparent)]
Io(#[from] io::Error),
}

#[derive(Debug, clap::Parser, Clone)]
Expand All @@ -15,12 +22,36 @@ pub struct Cmd {
/// Identity to remove
pub name: KeyName,

/// Skip confirmation prompt
#[arg(long)]
pub force: bool,

#[command(flatten)]
pub config: locator::Args,
}

impl Cmd {
pub fn run(&self, global_args: &global::Args) -> Result<(), Error> {
if !self.force {
let print = Print::new(global_args.quiet);
let stdin = io::stdin();

// Check that the key exists before asking for confirmation
self.config.read_identity(&self.name)?;

// Show the prompt only when the user can see it
if stdin.is_terminal() {
print.warnln(format!(
"Are you sure you want to remove the key '{}'? This action cannot be undone. (y/N)",
self.name
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to show the key path?

We show the key path during key creation and I think it would be helpful to follow suit here too. For example:

✅ Key saved with alias asdfasdf in "/Users/leighmcculloch/.config/stellar/identity/asdfasdf.toml"

));
Comment on lines +43 to +47
}
let mut response = String::new();
stdin.lock().read_line(&mut response)?;
if !response.trim().eq_ignore_ascii_case("y") {
return Err(Error::Cancelled);
}
}
Ok(self.config.remove_identity(&self.name, global_args)?)
}
}
2 changes: 1 addition & 1 deletion cookbook/stellar-keys.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ You can also fund the account while creating the key by using `stellar keys gene
When you no longer need this identity, remove it using:

```bash
stellar keys rm carol
stellar keys rm carol --force
```

Output:
Expand Down
Loading