Skip to content

Commit 3246d87

Browse files
committed
Add stellar token set-admin subcommand.
1 parent 40effee commit 3246d87

6 files changed

Lines changed: 429 additions & 0 deletions

File tree

‎FULL_HELP_DOCS.md‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1957,6 +1957,7 @@ Interact with SEP-41 tokens and Stellar Asset Contracts
19571957
- `allowance` — Read the allowance a spender has on an owner's behalf
19581958
- `mint` — Mint new tokens to an account or contract (SAC admin)
19591959
- `clawback` — Claw back tokens from an account or contract (SAC admin)
1960+
- `set-admin` — Transfer administration of the token to a new admin (SAC admin)
19601961

19611962
## `stellar token transfer`
19621963

@@ -2393,6 +2394,47 @@ Calls the token's Stellar Asset Contract `clawback` function. A non-SAC contract
23932394
- `--sign-with-ledger` — Sign with a ledger wallet
23942395
- `--auto-sign` — Sign without prompting for approval. Only applies to signatures that require user approval, like non-root Soroban auth entries
23952396

2397+
## `stellar token set-admin`
2398+
2399+
Transfer administration of the token to a new admin (SAC admin)
2400+
2401+
Calls the token's Stellar Asset Contract `set_admin` function. A non-SAC contract with a same-named function that takes different arguments will fail or misbehave — use `stellar contract invoke` for those.
2402+
2403+
**Usage:** `stellar token set-admin [OPTIONS] --id <ID> --admin <ADMIN> --new-admin <NEW_ADMIN>`
2404+
2405+
###### **Global Options:**
2406+
2407+
- `--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
2408+
2409+
###### **Options:**
2410+
2411+
- `--id <ID>` — The token to re-administer: a contract id or alias, or a classic asset as `CODE:ISSUER`
2412+
- `--admin <ADMIN>` — The token's current administrator. Signs and authorizes the change, so it must be an identity or secret key you control (the asset issuer for a Stellar Asset Contract)
2413+
- `--new-admin <NEW_ADMIN>` — The new administrator to hand control to. Accepts a `G…`/`M…` account, a `C…` contract address, or an alias
2414+
- `--output <OUTPUT>` — Format of the output
2415+
2416+
Default value: `text`
2417+
2418+
Possible values:
2419+
- `text`: Human-readable text
2420+
- `json`: Compact, single-line JSON output
2421+
- `json-formatted`: Formatted (multiline) JSON output
2422+
2423+
###### **RPC Options:**
2424+
2425+
- `--rpc-url <RPC_URL>` — RPC server endpoint
2426+
- `--rpc-header <RPC_HEADERS>` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times
2427+
- `--network-passphrase <NETWORK_PASSPHRASE>` — Network passphrase to sign the transaction sent to the rpc server
2428+
- `-n`, `--network <NETWORK>` — Name of network to use from config
2429+
2430+
###### **Signing Options:**
2431+
2432+
- `--sign-with-key <SIGN_WITH_KEY>` — Sign with a local key or key saved in OS secure storage. Can be an identity (--sign-with-key alice), a secret key (--sign-with-key SC36…), or a seed phrase (--sign-with-key "kite urban…"). If using seed phrase, `--hd-path` defaults to the `0` path
2433+
- `--hd-path <HD_PATH>` — If using a seed phrase to sign, sets which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0`
2434+
- `--sign-with-lab` — Sign with https://lab.stellar.org
2435+
- `--sign-with-ledger` — Sign with a ledger wallet
2436+
- `--auto-sign` — Sign without prompting for approval. Only applies to signatures that require user approval, like non-root Soroban auth entries
2437+
23962438
## `stellar tx`
23972439

23982440
Sign, Simulate, and Send transactions

‎cmd/crates/soroban-test/tests/it/integration/token/mod.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ pub mod decimals;
88
pub mod mint;
99
pub mod name;
1010
pub mod renamed;
11+
pub mod set_admin;
1112
pub mod symbol;
1213
pub mod transfer;
1314
pub mod transfer_from;
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
use serde_json::Value;
2+
use soroban_test::{AssertExt, TestEnv};
3+
4+
use crate::integration::{
5+
token::{add_trustline, deploy_sac, sac_balance, sac_id},
6+
util::{deploy_hello, new_account, test_address},
7+
};
8+
9+
#[tokio::test]
10+
async fn set_admin_transfers_control_and_returns_receipt() {
11+
let sandbox = &TestEnv::new();
12+
let test = test_address(sandbox);
13+
let issuer = new_account(sandbox, "issuer");
14+
let new_admin = new_account(sandbox, "newadmin");
15+
let asset = format!("USDC:{issuer}");
16+
17+
add_trustline(sandbox, "test", &asset);
18+
deploy_sac(sandbox, &asset, "issuer");
19+
20+
let stdout = sandbox
21+
.new_assert_cmd("token")
22+
.args([
23+
"set-admin",
24+
"--id",
25+
&asset,
26+
"--admin",
27+
"issuer",
28+
"--new-admin",
29+
&new_admin,
30+
"--output",
31+
"json",
32+
])
33+
.assert()
34+
.success()
35+
.stdout_as_str();
36+
let receipt: Value = serde_json::from_str(&stdout).unwrap();
37+
assert!(
38+
receipt["tx_hash"].as_str().is_some(),
39+
"expected a tx hash, got: {receipt}"
40+
);
41+
42+
// Control has transferred: the new admin can now mint, proving the change
43+
// took effect.
44+
sandbox
45+
.new_assert_cmd("token")
46+
.args([
47+
"mint", "--id", &asset, "--admin", "newadmin", "--to", &test, "--amount", "9000000",
48+
])
49+
.assert()
50+
.success();
51+
let sac = sac_id(sandbox, &asset);
52+
assert_eq!(
53+
sac_balance(sandbox, &sac, &test),
54+
9_000_000,
55+
"the new admin should be able to mint"
56+
);
57+
}
58+
59+
#[tokio::test]
60+
async fn set_admin_fails_when_sac_not_deployed() {
61+
let sandbox = &TestEnv::new();
62+
let issuer = new_account(sandbox, "issuer");
63+
let new_admin = new_account(sandbox, "newadmin");
64+
let asset = format!("USDC:{issuer}");
65+
66+
// No SAC deployed → structured deploy-pointer error with a typed discriminator.
67+
let stdout = sandbox
68+
.new_assert_cmd("token")
69+
.args([
70+
"set-admin",
71+
"--id",
72+
&asset,
73+
"--admin",
74+
"issuer",
75+
"--new-admin",
76+
&new_admin,
77+
"--output",
78+
"json",
79+
])
80+
.assert()
81+
.failure()
82+
.stdout_as_str();
83+
let value: Value = serde_json::from_str(&stdout).unwrap();
84+
assert_eq!(
85+
value["error"]["type"], "sac_not_deployed",
86+
"expected a typed error, got: {stdout}"
87+
);
88+
}
89+
90+
#[tokio::test]
91+
async fn set_admin_rejects_muxed_source_with_clear_error() {
92+
let sandbox = &TestEnv::new();
93+
let new_admin = new_account(sandbox, "newadmin");
94+
95+
// Muxed (M…) source accounts aren't supported by the invoke pipeline yet
96+
// (see #2645). Until then the command must reject them up front with a clear
97+
// message rather than a raw strkey decode error deep in the pipeline.
98+
let muxed = "MA3D5KRYM6CB7OWQ6TWYRR3Z4T7GNZLKERYNZGGA5SOAOPIFY6YQGAAAAAAAAAPCICBKU";
99+
sandbox
100+
.new_assert_cmd("token")
101+
.args([
102+
"set-admin",
103+
"--id",
104+
"native",
105+
"--admin",
106+
muxed,
107+
"--new-admin",
108+
&new_admin,
109+
])
110+
.assert()
111+
.failure()
112+
.stderr(predicates::str::contains(
113+
"muxed (M…) source accounts are not yet supported",
114+
));
115+
}
116+
117+
#[tokio::test]
118+
async fn set_admin_warns_when_target_is_not_a_sac() {
119+
let sandbox = &TestEnv::new();
120+
let new_admin = new_account(sandbox, "newadmin");
121+
let contract_id = deploy_hello(sandbox).await;
122+
123+
// Pointing a SAC-admin command at a plain wasm contract warns. The call then
124+
// fails (hello_world has no `set_admin`), but the heads-up is the point.
125+
let stderr = sandbox
126+
.new_assert_cmd("token")
127+
.args([
128+
"set-admin",
129+
"--id",
130+
&contract_id,
131+
"--admin",
132+
"test",
133+
"--new-admin",
134+
&new_admin,
135+
])
136+
.assert()
137+
.failure()
138+
.stderr_as_str();
139+
assert!(
140+
stderr.contains("is not a Stellar Asset Contract"),
141+
"expected a non-SAC warning, got: {stderr}"
142+
);
143+
}
144+
145+
#[tokio::test]
146+
async fn set_admin_does_not_warn_when_target_is_a_sac() {
147+
let sandbox = &TestEnv::new();
148+
let issuer = new_account(sandbox, "issuer");
149+
let new_admin = new_account(sandbox, "newadmin");
150+
let asset = format!("USDC:{issuer}");
151+
152+
deploy_sac(sandbox, &asset, "issuer");
153+
// Reference the SAC by its contract id, not the asset, so the check can only
154+
// clear it by inspecting the on-chain executable — not the id's text form.
155+
let sac = sac_id(sandbox, &asset);
156+
157+
let stderr = sandbox
158+
.new_assert_cmd("token")
159+
.args([
160+
"set-admin",
161+
"--id",
162+
&sac,
163+
"--admin",
164+
"issuer",
165+
"--new-admin",
166+
&new_admin,
167+
])
168+
.assert()
169+
.success()
170+
.stderr_as_str();
171+
assert!(
172+
!stderr.contains("is not a Stellar Asset Contract"),
173+
"a genuine SAC should not warn, got: {stderr}"
174+
);
175+
}

‎cmd/soroban-cli/src/cli.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ fn json_error_format(cmd: &commands::Cmd) -> Option<crate::output::Format> {
154154
commands::Cmd::Token(token::Cmd::Allowance(cmd)) => cmd.output.into(),
155155
commands::Cmd::Token(token::Cmd::Mint(cmd)) => cmd.output.into(),
156156
commands::Cmd::Token(token::Cmd::Clawback(cmd)) => cmd.output.into(),
157+
commands::Cmd::Token(token::Cmd::SetAdmin(cmd)) => cmd.output.into(),
157158
_ => return None,
158159
};
159160

‎cmd/soroban-cli/src/commands/token/mod.rs‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ pub mod clawback;
88
pub mod decimals;
99
pub mod mint;
1010
pub mod name;
11+
pub mod set_admin;
1112
pub mod symbol;
1213
pub mod transfer;
1314
pub mod transfer_from;
@@ -59,6 +60,13 @@ pub enum Cmd {
5960
/// contract with a same-named function that takes different arguments will
6061
/// fail or misbehave — use `stellar contract invoke` for those.
6162
Clawback(clawback::Cmd),
63+
64+
/// Transfer administration of the token to a new admin (SAC admin)
65+
///
66+
/// Calls the token's Stellar Asset Contract `set_admin` function. A non-SAC
67+
/// contract with a same-named function that takes different arguments will
68+
/// fail or misbehave — use `stellar contract invoke` for those.
69+
SetAdmin(set_admin::Cmd),
6270
}
6371

6472
#[derive(thiserror::Error, Debug)]
@@ -87,6 +95,8 @@ pub enum Error {
8795
Mint(#[from] mint::Error),
8896
#[error(transparent)]
8997
Clawback(#[from] clawback::Error),
98+
#[error(transparent)]
99+
SetAdmin(#[from] set_admin::Error),
90100
}
91101

92102
impl Error {
@@ -106,6 +116,7 @@ impl Error {
106116
Error::Allowance(e) => e.error_type(),
107117
Error::Mint(e) => e.error_type(),
108118
Error::Clawback(e) => e.error_type(),
119+
Error::SetAdmin(e) => e.error_type(),
109120
}
110121
}
111122
}
@@ -125,6 +136,7 @@ impl Cmd {
125136
Cmd::Allowance(cmd) => cmd.run(global_args).await?,
126137
Cmd::Mint(cmd) => cmd.run(global_args).await?,
127138
Cmd::Clawback(cmd) => cmd.run(global_args).await?,
139+
Cmd::SetAdmin(cmd) => cmd.run(global_args).await?,
128140
}
129141
Ok(())
130142
}

0 commit comments

Comments
 (0)