Skip to content

Commit 0b373b0

Browse files
committed
Add stellar token set-admin subcommand.
1 parent 69c296d commit 0b373b0

6 files changed

Lines changed: 358 additions & 0 deletions

File tree

‎FULL_HELP_DOCS.md‎

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

19291930
## `stellar token transfer`
19301931

@@ -2237,6 +2238,45 @@ Claw back tokens from an account or contract (SAC admin)
22372238
- `--sign-with-ledger` — Sign with a ledger wallet
22382239
- `--auto-sign` — Sign without prompting for approval. Only applies to signatures that require user approval, like non-root Soroban auth entries
22392240

2241+
## `stellar token set-admin`
2242+
2243+
Transfer administration of the token to a new admin (SAC admin)
2244+
2245+
**Usage:** `stellar token set-admin [OPTIONS] --id <ID> --admin <ADMIN> --new-admin <NEW_ADMIN>`
2246+
2247+
###### **Global Options:**
2248+
2249+
- `--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
2250+
2251+
###### **Options:**
2252+
2253+
- `--id <ID>` — The token to re-administer: a contract id or alias, or a classic asset as `CODE:ISSUER`
2254+
- `--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)
2255+
- `--new-admin <NEW_ADMIN>` — The new administrator to hand control to. Accepts a `G…`/`M…` account, a `C…` contract address, or an alias
2256+
- `--output <OUTPUT>` — Format of the output
2257+
2258+
Default value: `text`
2259+
2260+
Possible values:
2261+
- `text`: Human-readable text
2262+
- `json`: Compact, single-line JSON output
2263+
- `json-formatted`: Formatted (multiline) JSON output
2264+
2265+
###### **RPC Options:**
2266+
2267+
- `--rpc-url <RPC_URL>` — RPC server endpoint
2268+
- `--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
2269+
- `--network-passphrase <NETWORK_PASSPHRASE>` — Network passphrase to sign the transaction sent to the rpc server
2270+
- `-n`, `--network <NETWORK>` — Name of network to use from config
2271+
2272+
###### **Signing Options:**
2273+
2274+
- `--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
2275+
- `--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`
2276+
- `--sign-with-lab` — Sign with https://lab.stellar.org
2277+
- `--sign-with-ledger` — Sign with a ledger wallet
2278+
- `--auto-sign` — Sign without prompting for approval. Only applies to signatures that require user approval, like non-root Soroban auth entries
2279+
22402280
## `stellar tx`
22412281

22422282
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
@@ -6,6 +6,7 @@ pub mod decimals;
66
pub mod mint;
77
pub mod name;
88
pub mod renamed;
9+
pub mod set_admin;
910
pub mod symbol;
1011
pub mod transfer;
1112

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
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::{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+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ fn json_error_format(cmd: &commands::Cmd) -> Option<crate::output::Format> {
151151
commands::Cmd::Token(token::Cmd::Allowance(cmd)) => cmd.output.into(),
152152
commands::Cmd::Token(token::Cmd::Mint(cmd)) => cmd.output.into(),
153153
commands::Cmd::Token(token::Cmd::Clawback(cmd)) => cmd.output.into(),
154+
commands::Cmd::Token(token::Cmd::SetAdmin(cmd)) => cmd.output.into(),
154155
_ => return None,
155156
};
156157

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ pub mod clawback;
66
pub mod decimals;
77
pub mod mint;
88
pub mod name;
9+
pub mod set_admin;
910
pub mod symbol;
1011
pub mod transfer;
1112

@@ -39,6 +40,9 @@ pub enum Cmd {
3940

4041
/// Claw back tokens from an account or contract (SAC admin)
4142
Clawback(clawback::Cmd),
43+
44+
/// Transfer administration of the token to a new admin (SAC admin)
45+
SetAdmin(set_admin::Cmd),
4246
}
4347

4448
#[derive(thiserror::Error, Debug)]
@@ -61,6 +65,8 @@ pub enum Error {
6165
Mint(#[from] mint::Error),
6266
#[error(transparent)]
6367
Clawback(#[from] clawback::Error),
68+
#[error(transparent)]
69+
SetAdmin(#[from] set_admin::Error),
6470
}
6571

6672
impl Error {
@@ -77,6 +83,7 @@ impl Error {
7783
Error::Allowance(e) => e.error_type(),
7884
Error::Mint(e) => e.error_type(),
7985
Error::Clawback(e) => e.error_type(),
86+
Error::SetAdmin(e) => e.error_type(),
8087
}
8188
}
8289
}
@@ -93,6 +100,7 @@ impl Cmd {
93100
Cmd::Allowance(cmd) => cmd.run(global_args).await?,
94101
Cmd::Mint(cmd) => cmd.run(global_args).await?,
95102
Cmd::Clawback(cmd) => cmd.run(global_args).await?,
103+
Cmd::SetAdmin(cmd) => cmd.run(global_args).await?,
96104
}
97105
Ok(())
98106
}

0 commit comments

Comments
 (0)