soroban-cli 26.0.0

Soroban CLI
Documentation
use std::io::{IsTerminal, Write};

use sep5::SeedPhrase;

use crate::{
    commands::global,
    config::{
        address::KeyName,
        key, locator,
        secret::{self, Secret},
    },
    print::Print,
    signer::secure_store,
};

#[derive(thiserror::Error, Debug)]
pub enum Error {
    #[error(transparent)]
    Secret(#[from] secret::Error),
    #[error(transparent)]
    Key(#[from] key::Error),
    #[error(transparent)]
    Config(#[from] locator::Error),

    #[error(transparent)]
    SecureStore(#[from] secure_store::Error),

    #[error(transparent)]
    SeedPhrase(#[from] sep5::error::Error),

    #[error("secret input error")]
    PasswordRead,

    #[error("An identity with the name '{0}' already exists")]
    IdentityAlreadyExists(String),
}

#[derive(Debug, clap::Parser, Clone)]
#[group(skip)]
pub struct Cmd {
    /// Name of identity
    pub name: KeyName,

    #[command(flatten)]
    pub secrets: secret::Args,

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

    /// Add a public key, ed25519, or muxed account, e.g. G1.., M2..
    #[arg(long, conflicts_with = "seed_phrase", conflicts_with = "secret_key")]
    pub public_key: Option<String>,

    /// Overwrite existing identity if it already exists.
    #[arg(long)]
    pub overwrite: bool,
}

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

        if self.config_locator.read_identity(&self.name).is_ok() {
            if !self.overwrite {
                return Err(Error::IdentityAlreadyExists(self.name.to_string()));
            }

            print.exclaimln(format!("Overwriting identity '{}'", &self.name.to_string()));
        }

        let key = if let Some(key) = self.public_key.as_ref() {
            key.parse()?
        } else {
            self.read_secret(&print)?.into()
        };

        let path = self.config_locator.write_key(&self.name, &key)?;

        print.checkln(format!("Key saved with alias {} in {path:?}", self.name));

        Ok(())
    }

    fn read_secret(&self, print: &Print) -> Result<Secret, Error> {
        if let Ok(secret_key) = std::env::var("STELLAR_SECRET_KEY") {
            Ok(Secret::SecretKey { secret_key })
        } else if self.secrets.secure_store {
            let prompt = "Type a 12/24 word seed phrase:";
            let secret_key = read_password(print, prompt)?;
            if secret_key.split_whitespace().count() < 24 {
                print.warnln("The provided seed phrase lacks sufficient entropy and should be avoided. Using a 24-word seed phrase is a safer option.".to_string());
                print.warnln(
                    "To generate a new key, use the `stellar keys generate` command.".to_string(),
                );
            }

            let seed_phrase: SeedPhrase = secret_key.parse()?;

            let secret = secure_store::save_secret(print, &self.name, &seed_phrase)?;
            Ok(secret.parse()?)
        } else {
            let prompt = "Type a secret key or 12/24 word seed phrase:";
            let secret_key = read_password(print, prompt)?;
            let secret = secret_key.parse()?;
            if let Secret::SeedPhrase { seed_phrase } = &secret {
                if seed_phrase.split_whitespace().count() < 24 {
                    print.warnln("The provided seed phrase lacks sufficient entropy and should be avoided. Using a 24-word seed phrase is a safer option.".to_string());
                    print.warnln(
                        "To generate a new key, use the `stellar keys generate` command."
                            .to_string(),
                    );
                }
            }
            Ok(secret)
        }
    }
}

fn read_password(print: &Print, prompt: &str) -> Result<String, Error> {
    if std::io::stdin().is_terminal() {
        // Interactive: prompt and read from TTY
        print.arrowln(prompt);
        std::io::stdout().flush().map_err(|_| Error::PasswordRead)?;
        rpassword::read_password().map_err(|_| Error::PasswordRead)
    } else {
        // Non-interactive: read from stdin
        let mut input = String::new();
        std::io::stdin()
            .read_line(&mut input)
            .map_err(|_| Error::PasswordRead)?;
        let input = input.trim().to_string();
        if input.is_empty() {
            return Err(Error::PasswordRead);
        }
        Ok(input)
    }
}