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
46 changes: 46 additions & 0 deletions FULL_HELP_DOCS.md
Original file line number Diff line number Diff line change
Expand Up @@ -1349,6 +1349,7 @@ Configure connection to networks
- `info` — Checks the health of the configured RPC
- `settings` — Fetch the network's config settings
- `unset` — Unset the default network defined previously with `network use <network>`
- `root-account` — Compute the root account keypair for a network

## `stellar network add`

Expand Down Expand Up @@ -1515,6 +1516,51 @@ Unset the default network defined previously with `network use <network>`
- `--global` — ⚠️ Deprecated: global config is always on
- `--config-dir <CONFIG_DIR>` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings

## `stellar network root-account`

Compute the root account keypair for a network

**Usage:** `stellar network root-account <COMMAND>`

###### **Subcommands:**

- `public-key` — Output a network's root account address (public key)
- `secret` — Output a network's root account secret key

## `stellar network root-account public-key`

Output a network's root account address (public key)

**Usage:** `stellar network root-account public-key [OPTIONS]`

**Command Alias:** `address`

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

- `--network-passphrase <NETWORK_PASSPHRASE>` — Network passphrase to derive the root account from
- `-n`, `--network <NETWORK>` — Name of network to use from config

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

- `--global` — ⚠️ Deprecated: global config is always on
- `--config-dir <CONFIG_DIR>` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings

## `stellar network root-account secret`

Output a network's root account secret key

**Usage:** `stellar network root-account secret [OPTIONS]`

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

- `--network-passphrase <NETWORK_PASSPHRASE>` — Network passphrase to derive the root account from
- `-n`, `--network <NETWORK>` — Name of network to use from config

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

- `--global` — ⚠️ Deprecated: global config is always on
- `--config-dir <CONFIG_DIR>` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings

## `stellar container`

Start local networks in containers
Expand Down
94 changes: 93 additions & 1 deletion cmd/crates/soroban-test/tests/it/config.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
use crate::util::{add_key, add_test_id, SecretKind, GENERATED_SEED_PHRASE};
use predicates::prelude::predicate;
use soroban_cli::commands::network;
use soroban_cli::config::network::passphrase::LOCAL as LOCAL_NETWORK_PASSPHRASE;
use soroban_cli::config::network::passphrase::{
FUTURENET, LOCAL as LOCAL_NETWORK_PASSPHRASE, MAINNET,
};
use soroban_test::{AssertExt, TestEnv};
use std::fs;

Expand Down Expand Up @@ -476,6 +478,96 @@ fn cannot_create_contract_with_test_name() {
.failure();
}

#[test]
fn root_account_default_network() {
// test env default network is standalone
let sandbox = TestEnv::default();
sandbox
.new_assert_cmd("network")
.arg("root-account")
.arg("secret")
.assert()
.success()
.stdout("SC5O7VZUXDJ6JBDSZ74DSERXL7W3Y5LTOAMRF7RQRL3TAGAPS7LUVG3L\n");
}

#[test]
fn root_account_public_key() {
let sandbox = TestEnv::default();
sandbox
.bin()
.arg("network")
.arg("root-account")
.arg("public-key")
.arg("--network")
.arg("testnet")
.assert()
.success()
.stdout("GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H\n");
}

#[test]
fn root_account_address_alias() {
let sandbox = TestEnv::default();
sandbox
.bin()
.arg("network")
.arg("root-account")
.arg("address")
.arg("--network")
.arg("testnet")
.assert()
.success()
.stdout("GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H\n");
}

#[test]
fn root_account_secret_key() {
let sandbox = TestEnv::default();
sandbox
.bin()
.arg("network")
.arg("root-account")
.arg("secret")
.arg("--network")
.arg("testnet")
.assert()
.success()
.stdout("SDHOAMBNLGCE2MV5ZKIVZAQD3VCLGP53P3OBSBI6UN5L5XZI5TKHFQL4\n");
}

#[test]
fn root_account_with_explicit_passphrase() {
let sandbox = TestEnv::default();
sandbox
.bin()
.arg("network")
.arg("root-account")
.arg("secret")
.arg("--network-passphrase")
.arg(FUTURENET)
.assert()
.success()
.stdout("SCR2DRVHQKDHCPRJXYHJPBLHB6UDRUJZC7GY5LVUUNLZ74O6XR75KK5K\n");
}

#[test]
fn root_account_with_explicit_passphrase_overrides_network() {
let sandbox = TestEnv::default();
sandbox
.bin()
.arg("network")
.arg("root-account")
.arg("public-key")
.arg("--network")
.arg("testnet")
.arg("--network-passphrase")
.arg(MAINNET)
.assert()
.success()
.stdout("GAAZI4TCR3TY5OJHCTJC2A4QSY6CJWJH5IAJTGKIN2ER7LBNVKOCCWN7\n");
}

#[test]
fn cannot_create_key_with_alias() {
let sandbox = TestEnv::default();
Expand Down
9 changes: 9 additions & 0 deletions cmd/soroban-cli/src/commands/network/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ pub mod health;
pub mod info;
pub mod ls;
pub mod rm;
pub mod root_account;
pub mod settings;
pub mod unset;

Expand Down Expand Up @@ -38,6 +39,10 @@ pub enum Cmd {

/// Unset the default network defined previously with `network use <network>`
Unset(unset::Cmd),

/// Compute the root account keypair for a network.
#[command(subcommand)]
RootAccount(root_account::Cmd),
}

#[derive(thiserror::Error, Debug)]
Expand Down Expand Up @@ -65,6 +70,9 @@ pub enum Error {

#[error(transparent)]
Unset(#[from] unset::Error),

#[error(transparent)]
RootAccount(#[from] root_account::Error),
}

impl Cmd {
Expand All @@ -78,6 +86,7 @@ impl Cmd {
Cmd::Info(cmd) => cmd.run(global_args).await?,
Cmd::Settings(cmd) => cmd.run(global_args).await?,
Cmd::Unset(cmd) => cmd.run(global_args)?,
Cmd::RootAccount(cmd) => cmd.run(global_args)?,
}
Ok(())
}
Expand Down
71 changes: 71 additions & 0 deletions cmd/soroban-cli/src/commands/network/root_account/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
use clap::Parser;
use sha2::{Digest, Sha256};

use crate::commands::global;
use crate::config::network::passphrase;
use crate::config::{locator, network};

pub mod public_key;
pub mod secret;

#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error(transparent)]
Config(#[from] locator::Error),
#[error(transparent)]
Network(#[from] network::Error),
#[error(transparent)]
Strkey(#[from] stellar_strkey::DecodeError),
}

/// Shared arguments for resolving the network passphrase and deriving the root account seed.
#[derive(Debug, clap::Parser, Clone)]
#[group(skip)]
pub struct Args {
// @dev: `network` and `network-passphrase` args are provided explicitly as the `rpc-url` is not needed
/// Network passphrase to derive the root account from
#[arg(long = "network-passphrase", env = "STELLAR_NETWORK_PASSPHRASE")]
pub network_passphrase: Option<String>,

/// Name of network to use from config
#[arg(long, short = 'n', env = "STELLAR_NETWORK")]
pub network: Option<String>,

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

impl Args {
/// Resolve the network passphrase and derive the 32-byte key.
pub fn root_key(&self) -> Result<[u8; 32], Error> {
// If a user explicitly provides a network passphrase, use that.
// Otherwise, look up the network with the typical resolution process and use its passphrase.
let network_passphrase = match (self.network.as_deref(), self.network_passphrase.clone()) {
// Fall back to testnet as the default network if no config default is set
(None, None) => passphrase::TESTNET.to_string(),
(Some(network), None) => self.locator.read_network(network)?.network_passphrase,
(_, Some(network_passphrase)) => network_passphrase,
};
Ok(Sha256::digest(network_passphrase.as_bytes()).into())
}
}

#[derive(Debug, Parser)]
pub enum Cmd {
/// Output a network's root account address (public key)
#[command(visible_alias = "address")]
PublicKey(public_key::Cmd),

/// Output a network's root account secret key
Secret(secret::Cmd),
}

impl Cmd {
pub fn run(&self, _global_args: &global::Args) -> Result<(), Error> {
match self {
Cmd::PublicKey(cmd) => cmd.run()?,
Cmd::Secret(cmd) => cmd.run()?,
}
Ok(())
}
}
20 changes: 20 additions & 0 deletions cmd/soroban-cli/src/commands/network/root_account/public_key.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
use stellar_strkey::ed25519::PublicKey;

use super::{Args, Error};

#[derive(Debug, clap::Parser, Clone)]
#[group(skip)]
pub struct Cmd {
#[command(flatten)]
pub args: Args,
}

impl Cmd {
pub fn run(&self) -> Result<(), Error> {
let root_key = self.args.root_key()?;
let signing_key = ed25519_dalek::SigningKey::from_bytes(&root_key);
let public_key = PublicKey::from_payload(signing_key.verifying_key().as_bytes())?;
println!("{public_key}");
Ok(())
}
}
19 changes: 19 additions & 0 deletions cmd/soroban-cli/src/commands/network/root_account/secret.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
use stellar_strkey::ed25519::PrivateKey;

use super::{Args, Error};

#[derive(Debug, clap::Parser, Clone)]
#[group(skip)]
pub struct Cmd {
#[command(flatten)]
pub args: Args,
}

impl Cmd {
pub fn run(&self) -> Result<(), Error> {
let root_key = self.args.root_key()?;
let private_key = PrivateKey::from_payload(&root_key)?;
println!("{private_key}");
Ok(())
}
}