Skip to content
Merged
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
10 changes: 1 addition & 9 deletions FULL_HELP_DOCS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4145,7 +4145,7 @@ Encode a transaction envelope from JSON to XDR

Decode and encode XDR

**Usage:** `stellar xdr [CHANNEL] <COMMAND>`
**Usage:** `stellar xdr <COMMAND>`

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

Expand All @@ -4158,14 +4158,6 @@ Decode and encode XDR
- `xfile` — Preprocess XDR .x files
- `version` — Print version information

###### **Arguments:**

- `<CHANNEL>` — Channel of XDR to operate on

Default value: `+curr`

Possible values: `+curr`, `+next`

## `stellar xdr types`

View information about types
Expand Down
207 changes: 207 additions & 0 deletions cmd/crates/soroban-test/tests/it/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -994,3 +994,210 @@ fn contract_alias_add_rejects_path_traversal() {
.stderr(predicate::str::contains("Invalid name"));
});
}

#[test]
fn native_alias_resolves_to_native_asset_contract() {
TestEnv::with_default(|sandbox| {
let native_sac = sandbox
.new_assert_cmd("contract")
.args(["id", "asset", "--asset", "native"])
.assert()
.success()
.stdout_as_str();

let resolved = sandbox
.new_assert_cmd("contract")
.args(["alias", "show", "native"])
.assert()
.success()
.stdout_as_str();

assert_eq!(native_sac, resolved);
});
}

#[test]
fn cannot_add_reserved_native_alias() {
TestEnv::with_default(|sandbox| {
sandbox
.new_assert_cmd("contract")
.arg("alias")
.arg("add")
.arg("native")
.arg("--id=CA3D5KRYM6CB7OWQ6TWYRR3Z4T7GNZLKERYNZGGA5SOAOPIFY6YQGAXE")
.assert()
.failure()
.stderr(predicate::str::contains("reserved"));
});
}

#[test]
fn can_remove_shadowed_native_alias() {
TestEnv::with_default(|sandbox| {
// A `native` alias created before it became reserved can still be
// removed to clean up the shadowed file.
let contract_ids = sandbox.config_dir().join("contract-ids");
fs::create_dir_all(&contract_ids).unwrap();
fs::write(
contract_ids.join("native.json"),
r#"{"ids":{"Standalone Network ; February 2017":"CA3D5KRYM6CB7OWQ6TWYRR3Z4T7GNZLKERYNZGGA5SOAOPIFY6YQGAXE"}}"#,
)
.unwrap();

sandbox
.new_assert_cmd("contract")
.args(["alias", "remove", "native"])
.assert()
.success();

// The shadowed entry is gone; only the built-in remains.
sandbox
.new_assert_cmd("contract")
.args(["alias", "ls"])
.assert()
.success()
.stdout(
predicate::str::contains("(built-in)")
.and(predicate::str::contains("(disabled)").not()),
);
});
}

#[test]
fn alias_ls_always_shows_builtin_native() {
TestEnv::with_default(|sandbox| {
// A user alias makes a network group appear in the listing.
sandbox
.new_assert_cmd("contract")
.args([
"alias",
"add",
"my-token",
"--id=CA3D5KRYM6CB7OWQ6TWYRR3Z4T7GNZLKERYNZGGA5SOAOPIFY6YQGAXE",
])
.assert()
.success();

sandbox
.new_assert_cmd("contract")
.args(["alias", "ls"])
.assert()
.success()
.stdout(
predicate::str::contains("native:").and(predicate::str::contains("(built-in)")),
);
});
}

#[test]
fn alias_ls_marks_preexisting_native_alias_disabled() {
TestEnv::with_default(|sandbox| {
// Simulate a `native` alias created before it became a reserved
// built-in. It is now shadowed and should be listed as disabled.
let contract_ids = sandbox.config_dir().join("contract-ids");
fs::create_dir_all(&contract_ids).unwrap();
fs::write(
contract_ids.join("native.json"),
r#"{"ids":{"Standalone Network ; February 2017":"CA3D5KRYM6CB7OWQ6TWYRR3Z4T7GNZLKERYNZGGA5SOAOPIFY6YQGAXE"}}"#,
)
.unwrap();

sandbox
.new_assert_cmd("contract")
.args(["alias", "ls"])
.assert()
.success()
.stdout(
predicate::str::contains("(disabled)").and(predicate::str::contains("(built-in)")),
);
});
}

#[test]
fn remove_native_without_stored_file_reports_reserved() {
TestEnv::with_default(|sandbox| {
// No stored `native.json` exists (the normal case, since `alias add
// native` is blocked). Removal must say the alias is reserved, matching
// what `ls` and `show` report, not "no contract found".
sandbox
.new_assert_cmd("contract")
.args(["alias", "remove", "native"])
.assert()
.failure()
.stderr(
predicate::str::contains("reserved")
.and(predicate::str::contains("no contract found").not()),
);
});
}

#[test]
fn cannot_generate_reserved_native_key() {
TestEnv::with_default(|sandbox| {
sandbox
.new_assert_cmd("keys")
.arg("generate")
.arg("--seed")
.arg("0000000000000000")
.arg("native")
.assert()
.failure()
.stderr(predicate::str::contains("reserved"));
});
}

#[test]
fn cannot_add_reserved_native_key() {
TestEnv::with_default(|sandbox| {
sandbox
.new_assert_cmd("keys")
.arg("add")
.arg("native")
.env(
"STELLAR_SECRET_KEY",
"SBEQMTXGCLDFQG3OXMRSMGLKJCPROAHB5GZCCGVZERDI645LCCCRLFGY",
)
.assert()
.failure()
.stderr(predicate::str::contains("reserved"));
});
}

#[test]
fn shadowed_native_alias_errors_on_show() {
TestEnv::with_default(|sandbox| {
// A `native` alias stored before the name became reserved points at a
// different contract; resolving it must error rather than silently
// returning the native asset contract.
let contract_ids = sandbox.config_dir().join("contract-ids");
fs::create_dir_all(&contract_ids).unwrap();
fs::write(
contract_ids.join("native.json"),
r#"{"ids":{"Standalone Network ; February 2017":"CA3D5KRYM6CB7OWQ6TWYRR3Z4T7GNZLKERYNZGGA5SOAOPIFY6YQGAXE"}}"#,
)
.unwrap();

sandbox
.new_assert_cmd("contract")
.args(["alias", "show", "native"])
.assert()
.failure()
.stderr(predicate::str::contains("reserved"));
});
}

#[test]
fn cannot_deploy_with_reserved_native_alias() {
// The reserved alias must be rejected before building, simulating, or
// deploying, so this fails fast without needing a network.
TestEnv::with_default(|sandbox| {
sandbox
.new_assert_cmd("contract")
.arg("deploy")
.arg("--alias=native")
.arg("--wasm=/does/not/matter.wasm")
.assert()
.failure()
.stderr(predicate::str::contains("reserved"));
});
}
3 changes: 3 additions & 0 deletions cmd/soroban-cli/src/commands/contract/alias/add.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ impl Cmd {
pub fn run(&self, global_args: &global::Args) -> Result<(), Error> {
let print = Print::new(global_args.quiet);
let alias = &self.alias;

crate::config::alias::validate_reserved_aliases(alias)?;

let network = self.network.get(&self.config_locator)?;
let network_passphrase = &network.network_passphrase;

Expand Down
42 changes: 30 additions & 12 deletions cmd/soroban-cli/src/commands/contract/alias/ls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ use clap::Parser;
use std::collections::HashMap;
use std::ffi::OsStr;
use std::fmt::Debug;
use std::fs;
use std::path::Path;
use std::{fs, process};

use crate::commands::config::network;
use crate::config::locator::{print_deprecation_warning, Location};
Expand Down Expand Up @@ -32,6 +32,7 @@ pub enum Error {
struct AliasEntry {
alias: String,
contract: String,
builtin: bool,
}

impl Cmd {
Expand All @@ -45,7 +46,7 @@ impl Cmd {
print_deprecation_warning(&config_dir);
}
}
Location::Global(config_dir) => Self::read_from_config_dir(&config_dir)?,
Location::Global(config_dir) => self.read_from_config_dir(&config_dir)?,
}
}

Expand Down Expand Up @@ -76,6 +77,7 @@ impl Cmd {
let entry = AliasEntry {
alias: alias.clone(),
contract: contract_id.clone(),
builtin: false,
};

map.entry(network_passphrase.clone())
Expand All @@ -88,29 +90,45 @@ impl Cmd {
Ok(map)
}

fn read_from_config_dir(config_dir: &Path) -> Result<(), Error> {
fn read_from_config_dir(&self, config_dir: &Path) -> Result<(), Error> {
let mut map = Self::collect_aliases(config_dir)?;
let mut found = false;

// The built-in `native` alias resolves to the native asset contract for
// a network, so make sure every known network is listed even when it has
// no stored aliases of its own.
for (_, network, _) in self.config_locator.list_networks_long()? {
map.entry(network.network_passphrase).or_default();
}

for (network_passphrase, list) in &mut map {
if let Some(contract) = alias::resolve_reserved(alias::NATIVE, network_passphrase) {
list.push(AliasEntry {
alias: alias::NATIVE.to_string(),
contract: format!("{contract}"),
builtin: true,
});
}

println!("ℹ️ Aliases available for network '{network_passphrase}'");

list.sort_by(|a, b| a.alias.cmp(&b.alias));
Comment thread
fnando marked this conversation as resolved.

for entry in list.iter() {
found = true;
println!("{}: {}", entry.alias, entry.contract);
let note = if entry.builtin {
" (built-in)"
} else if alias::is_reserved(&entry.alias) {
// A user alias whose name is now reserved is shadowed by the
// built-in alias of the same name, so it no longer resolves.
" (disabled)"
} else {
""
};
println!("{}: {}{note}", entry.alias, entry.contract);
}

println!();
}

if !found {
eprintln!("⚠️ No aliases defined for network");

process::exit(1);
}

Ok(())
}
}
Expand Down
14 changes: 12 additions & 2 deletions cmd/soroban-cli/src/commands/contract/alias/remove.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::fmt::Debug;
use clap::Parser;

use crate::commands::{config::network, global};
use crate::config::{address::AliasName, locator};
use crate::config::{address::AliasName, alias, locator};
use crate::print::Print;

#[derive(Parser, Debug, Clone)]
Expand Down Expand Up @@ -41,10 +41,20 @@ impl Cmd {
let network = self.network.get(&self.config_locator)?;
let network_passphrase = &network.network_passphrase;

// Use the stored value so a reserved alias reflects the shadowed file
// being removed, not its built-in resolution.
let Some(contract) = self
.config_locator
.get_contract_id(&self.alias, network_passphrase)?
.get_stored_contract_id(&self.alias, network_passphrase)?
else {
// Without a stored file there's nothing to remove. For a reserved
// alias, say so truthfully instead of "no contract found" — `ls`
// and `show` both report the built-in exists, so that error would
// contradict them.
if alias::is_reserved(&self.alias) {
return Err(locator::Error::ContractAliasReserved(self.alias.to_string()).into());
}

return Err(Error::NoContract {
alias: alias.to_string(),
network_passphrase: network_passphrase.into(),
Expand Down
24 changes: 24 additions & 0 deletions cmd/soroban-cli/src/commands/contract/arg_parsing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1158,6 +1158,30 @@ mod tests {
// A real account strkey that should pass through resolve_address unchanged.
const TEST_G_ADDRESS: &str = "GD5KD2KEZJIGTC63IGW6UMUSMVUVG5IHG64HUTFWCHVZH2N2IBOQN7PS";

#[test]
fn resolve_aliases_resolves_native_to_asset_contract_address() {
let ty = ScSpecTypeDef::Address;
let spec = Spec(Some(vec![]));
let config = crate::config::Args::default();

let mut value = serde_json::json!("native");
let mutated = resolve_aliases_in_json(&mut value, &ty, &spec, &config).unwrap();
assert!(
mutated,
"native should resolve to the native asset contract"
);

let network_passphrase = config.get_network().unwrap().network_passphrase;
let expected = format!(
"{}",
crate::utils::contract_id_hash_from_asset(
&crate::xdr::Asset::Native,
&network_passphrase,
)
);
assert_eq!(value, serde_json::Value::String(expected));
}

#[test]
fn resolve_aliases_in_json_walks_vec_of_address() {
use stellar_xdr::ScSpecTypeVec;
Expand Down
Loading
Loading