Skip to content

Commit 078abaa

Browse files
authored
Add reserved native contract alias resolving to the native asset (#2646)
1 parent 4c2750e commit 078abaa

11 files changed

Lines changed: 631 additions & 24 deletions

File tree

FULL_HELP_DOCS.md

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4145,7 +4145,7 @@ Encode a transaction envelope from JSON to XDR
41454145

41464146
Decode and encode XDR
41474147

4148-
**Usage:** `stellar xdr [CHANNEL] <COMMAND>`
4148+
**Usage:** `stellar xdr <COMMAND>`
41494149

41504150
###### **Subcommands:**
41514151

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

4161-
###### **Arguments:**
4162-
4163-
- `<CHANNEL>` — Channel of XDR to operate on
4164-
4165-
Default value: `+curr`
4166-
4167-
Possible values: `+curr`, `+next`
4168-
41694161
## `stellar xdr types`
41704162

41714163
View information about types

cmd/crates/soroban-test/tests/it/config.rs

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -994,3 +994,210 @@ fn contract_alias_add_rejects_path_traversal() {
994994
.stderr(predicate::str::contains("Invalid name"));
995995
});
996996
}
997+
998+
#[test]
999+
fn native_alias_resolves_to_native_asset_contract() {
1000+
TestEnv::with_default(|sandbox| {
1001+
let native_sac = sandbox
1002+
.new_assert_cmd("contract")
1003+
.args(["id", "asset", "--asset", "native"])
1004+
.assert()
1005+
.success()
1006+
.stdout_as_str();
1007+
1008+
let resolved = sandbox
1009+
.new_assert_cmd("contract")
1010+
.args(["alias", "show", "native"])
1011+
.assert()
1012+
.success()
1013+
.stdout_as_str();
1014+
1015+
assert_eq!(native_sac, resolved);
1016+
});
1017+
}
1018+
1019+
#[test]
1020+
fn cannot_add_reserved_native_alias() {
1021+
TestEnv::with_default(|sandbox| {
1022+
sandbox
1023+
.new_assert_cmd("contract")
1024+
.arg("alias")
1025+
.arg("add")
1026+
.arg("native")
1027+
.arg("--id=CA3D5KRYM6CB7OWQ6TWYRR3Z4T7GNZLKERYNZGGA5SOAOPIFY6YQGAXE")
1028+
.assert()
1029+
.failure()
1030+
.stderr(predicate::str::contains("reserved"));
1031+
});
1032+
}
1033+
1034+
#[test]
1035+
fn can_remove_shadowed_native_alias() {
1036+
TestEnv::with_default(|sandbox| {
1037+
// A `native` alias created before it became reserved can still be
1038+
// removed to clean up the shadowed file.
1039+
let contract_ids = sandbox.config_dir().join("contract-ids");
1040+
fs::create_dir_all(&contract_ids).unwrap();
1041+
fs::write(
1042+
contract_ids.join("native.json"),
1043+
r#"{"ids":{"Standalone Network ; February 2017":"CA3D5KRYM6CB7OWQ6TWYRR3Z4T7GNZLKERYNZGGA5SOAOPIFY6YQGAXE"}}"#,
1044+
)
1045+
.unwrap();
1046+
1047+
sandbox
1048+
.new_assert_cmd("contract")
1049+
.args(["alias", "remove", "native"])
1050+
.assert()
1051+
.success();
1052+
1053+
// The shadowed entry is gone; only the built-in remains.
1054+
sandbox
1055+
.new_assert_cmd("contract")
1056+
.args(["alias", "ls"])
1057+
.assert()
1058+
.success()
1059+
.stdout(
1060+
predicate::str::contains("(built-in)")
1061+
.and(predicate::str::contains("(disabled)").not()),
1062+
);
1063+
});
1064+
}
1065+
1066+
#[test]
1067+
fn alias_ls_always_shows_builtin_native() {
1068+
TestEnv::with_default(|sandbox| {
1069+
// A user alias makes a network group appear in the listing.
1070+
sandbox
1071+
.new_assert_cmd("contract")
1072+
.args([
1073+
"alias",
1074+
"add",
1075+
"my-token",
1076+
"--id=CA3D5KRYM6CB7OWQ6TWYRR3Z4T7GNZLKERYNZGGA5SOAOPIFY6YQGAXE",
1077+
])
1078+
.assert()
1079+
.success();
1080+
1081+
sandbox
1082+
.new_assert_cmd("contract")
1083+
.args(["alias", "ls"])
1084+
.assert()
1085+
.success()
1086+
.stdout(
1087+
predicate::str::contains("native:").and(predicate::str::contains("(built-in)")),
1088+
);
1089+
});
1090+
}
1091+
1092+
#[test]
1093+
fn alias_ls_marks_preexisting_native_alias_disabled() {
1094+
TestEnv::with_default(|sandbox| {
1095+
// Simulate a `native` alias created before it became a reserved
1096+
// built-in. It is now shadowed and should be listed as disabled.
1097+
let contract_ids = sandbox.config_dir().join("contract-ids");
1098+
fs::create_dir_all(&contract_ids).unwrap();
1099+
fs::write(
1100+
contract_ids.join("native.json"),
1101+
r#"{"ids":{"Standalone Network ; February 2017":"CA3D5KRYM6CB7OWQ6TWYRR3Z4T7GNZLKERYNZGGA5SOAOPIFY6YQGAXE"}}"#,
1102+
)
1103+
.unwrap();
1104+
1105+
sandbox
1106+
.new_assert_cmd("contract")
1107+
.args(["alias", "ls"])
1108+
.assert()
1109+
.success()
1110+
.stdout(
1111+
predicate::str::contains("(disabled)").and(predicate::str::contains("(built-in)")),
1112+
);
1113+
});
1114+
}
1115+
1116+
#[test]
1117+
fn remove_native_without_stored_file_reports_reserved() {
1118+
TestEnv::with_default(|sandbox| {
1119+
// No stored `native.json` exists (the normal case, since `alias add
1120+
// native` is blocked). Removal must say the alias is reserved, matching
1121+
// what `ls` and `show` report, not "no contract found".
1122+
sandbox
1123+
.new_assert_cmd("contract")
1124+
.args(["alias", "remove", "native"])
1125+
.assert()
1126+
.failure()
1127+
.stderr(
1128+
predicate::str::contains("reserved")
1129+
.and(predicate::str::contains("no contract found").not()),
1130+
);
1131+
});
1132+
}
1133+
1134+
#[test]
1135+
fn cannot_generate_reserved_native_key() {
1136+
TestEnv::with_default(|sandbox| {
1137+
sandbox
1138+
.new_assert_cmd("keys")
1139+
.arg("generate")
1140+
.arg("--seed")
1141+
.arg("0000000000000000")
1142+
.arg("native")
1143+
.assert()
1144+
.failure()
1145+
.stderr(predicate::str::contains("reserved"));
1146+
});
1147+
}
1148+
1149+
#[test]
1150+
fn cannot_add_reserved_native_key() {
1151+
TestEnv::with_default(|sandbox| {
1152+
sandbox
1153+
.new_assert_cmd("keys")
1154+
.arg("add")
1155+
.arg("native")
1156+
.env(
1157+
"STELLAR_SECRET_KEY",
1158+
"SBEQMTXGCLDFQG3OXMRSMGLKJCPROAHB5GZCCGVZERDI645LCCCRLFGY",
1159+
)
1160+
.assert()
1161+
.failure()
1162+
.stderr(predicate::str::contains("reserved"));
1163+
});
1164+
}
1165+
1166+
#[test]
1167+
fn shadowed_native_alias_errors_on_show() {
1168+
TestEnv::with_default(|sandbox| {
1169+
// A `native` alias stored before the name became reserved points at a
1170+
// different contract; resolving it must error rather than silently
1171+
// returning the native asset contract.
1172+
let contract_ids = sandbox.config_dir().join("contract-ids");
1173+
fs::create_dir_all(&contract_ids).unwrap();
1174+
fs::write(
1175+
contract_ids.join("native.json"),
1176+
r#"{"ids":{"Standalone Network ; February 2017":"CA3D5KRYM6CB7OWQ6TWYRR3Z4T7GNZLKERYNZGGA5SOAOPIFY6YQGAXE"}}"#,
1177+
)
1178+
.unwrap();
1179+
1180+
sandbox
1181+
.new_assert_cmd("contract")
1182+
.args(["alias", "show", "native"])
1183+
.assert()
1184+
.failure()
1185+
.stderr(predicate::str::contains("reserved"));
1186+
});
1187+
}
1188+
1189+
#[test]
1190+
fn cannot_deploy_with_reserved_native_alias() {
1191+
// The reserved alias must be rejected before building, simulating, or
1192+
// deploying, so this fails fast without needing a network.
1193+
TestEnv::with_default(|sandbox| {
1194+
sandbox
1195+
.new_assert_cmd("contract")
1196+
.arg("deploy")
1197+
.arg("--alias=native")
1198+
.arg("--wasm=/does/not/matter.wasm")
1199+
.assert()
1200+
.failure()
1201+
.stderr(predicate::str::contains("reserved"));
1202+
});
1203+
}

cmd/soroban-cli/src/commands/contract/alias/add.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ impl Cmd {
4949
pub fn run(&self, global_args: &global::Args) -> Result<(), Error> {
5050
let print = Print::new(global_args.quiet);
5151
let alias = &self.alias;
52+
53+
crate::config::alias::validate_reserved_aliases(alias)?;
54+
5255
let network = self.network.get(&self.config_locator)?;
5356
let network_passphrase = &network.network_passphrase;
5457

cmd/soroban-cli/src/commands/contract/alias/ls.rs

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ use clap::Parser;
22
use std::collections::HashMap;
33
use std::ffi::OsStr;
44
use std::fmt::Debug;
5+
use std::fs;
56
use std::path::Path;
6-
use std::{fs, process};
77

88
use crate::commands::config::network;
99
use crate::config::locator::{print_deprecation_warning, Location};
@@ -32,6 +32,7 @@ pub enum Error {
3232
struct AliasEntry {
3333
alias: String,
3434
contract: String,
35+
builtin: bool,
3536
}
3637

3738
impl Cmd {
@@ -45,7 +46,7 @@ impl Cmd {
4546
print_deprecation_warning(&config_dir);
4647
}
4748
}
48-
Location::Global(config_dir) => Self::read_from_config_dir(&config_dir)?,
49+
Location::Global(config_dir) => self.read_from_config_dir(&config_dir)?,
4950
}
5051
}
5152

@@ -76,6 +77,7 @@ impl Cmd {
7677
let entry = AliasEntry {
7778
alias: alias.clone(),
7879
contract: contract_id.clone(),
80+
builtin: false,
7981
};
8082

8183
map.entry(network_passphrase.clone())
@@ -88,29 +90,45 @@ impl Cmd {
8890
Ok(map)
8991
}
9092

91-
fn read_from_config_dir(config_dir: &Path) -> Result<(), Error> {
93+
fn read_from_config_dir(&self, config_dir: &Path) -> Result<(), Error> {
9294
let mut map = Self::collect_aliases(config_dir)?;
93-
let mut found = false;
95+
96+
// The built-in `native` alias resolves to the native asset contract for
97+
// a network, so make sure every known network is listed even when it has
98+
// no stored aliases of its own.
99+
for (_, network, _) in self.config_locator.list_networks_long()? {
100+
map.entry(network.network_passphrase).or_default();
101+
}
94102

95103
for (network_passphrase, list) in &mut map {
104+
if let Some(contract) = alias::resolve_reserved(alias::NATIVE, network_passphrase) {
105+
list.push(AliasEntry {
106+
alias: alias::NATIVE.to_string(),
107+
contract: format!("{contract}"),
108+
builtin: true,
109+
});
110+
}
111+
96112
println!("ℹ️ Aliases available for network '{network_passphrase}'");
97113

98114
list.sort_by(|a, b| a.alias.cmp(&b.alias));
99115

100116
for entry in list.iter() {
101-
found = true;
102-
println!("{}: {}", entry.alias, entry.contract);
117+
let note = if entry.builtin {
118+
" (built-in)"
119+
} else if alias::is_reserved(&entry.alias) {
120+
// A user alias whose name is now reserved is shadowed by the
121+
// built-in alias of the same name, so it no longer resolves.
122+
" (disabled)"
123+
} else {
124+
""
125+
};
126+
println!("{}: {}{note}", entry.alias, entry.contract);
103127
}
104128

105129
println!();
106130
}
107131

108-
if !found {
109-
eprintln!("⚠️ No aliases defined for network");
110-
111-
process::exit(1);
112-
}
113-
114132
Ok(())
115133
}
116134
}

cmd/soroban-cli/src/commands/contract/alias/remove.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use std::fmt::Debug;
33
use clap::Parser;
44

55
use crate::commands::{config::network, global};
6-
use crate::config::{address::AliasName, locator};
6+
use crate::config::{address::AliasName, alias, locator};
77
use crate::print::Print;
88

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

44+
// Use the stored value so a reserved alias reflects the shadowed file
45+
// being removed, not its built-in resolution.
4446
let Some(contract) = self
4547
.config_locator
46-
.get_contract_id(&self.alias, network_passphrase)?
48+
.get_stored_contract_id(&self.alias, network_passphrase)?
4749
else {
50+
// Without a stored file there's nothing to remove. For a reserved
51+
// alias, say so truthfully instead of "no contract found" — `ls`
52+
// and `show` both report the built-in exists, so that error would
53+
// contradict them.
54+
if alias::is_reserved(&self.alias) {
55+
return Err(locator::Error::ContractAliasReserved(self.alias.to_string()).into());
56+
}
57+
4858
return Err(Error::NoContract {
4959
alias: alias.to_string(),
5060
network_passphrase: network_passphrase.into(),

cmd/soroban-cli/src/commands/contract/arg_parsing.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1158,6 +1158,30 @@ mod tests {
11581158
// A real account strkey that should pass through resolve_address unchanged.
11591159
const TEST_G_ADDRESS: &str = "GD5KD2KEZJIGTC63IGW6UMUSMVUVG5IHG64HUTFWCHVZH2N2IBOQN7PS";
11601160

1161+
#[test]
1162+
fn resolve_aliases_resolves_native_to_asset_contract_address() {
1163+
let ty = ScSpecTypeDef::Address;
1164+
let spec = Spec(Some(vec![]));
1165+
let config = crate::config::Args::default();
1166+
1167+
let mut value = serde_json::json!("native");
1168+
let mutated = resolve_aliases_in_json(&mut value, &ty, &spec, &config).unwrap();
1169+
assert!(
1170+
mutated,
1171+
"native should resolve to the native asset contract"
1172+
);
1173+
1174+
let network_passphrase = config.get_network().unwrap().network_passphrase;
1175+
let expected = format!(
1176+
"{}",
1177+
crate::utils::contract_id_hash_from_asset(
1178+
&crate::xdr::Asset::Native,
1179+
&network_passphrase,
1180+
)
1181+
);
1182+
assert_eq!(value, serde_json::Value::String(expected));
1183+
}
1184+
11611185
#[test]
11621186
fn resolve_aliases_in_json_walks_vec_of_address() {
11631187
use stellar_xdr::ScSpecTypeVec;

0 commit comments

Comments
 (0)