Skip to content

Commit 8c5d73e

Browse files
committed
Add stellar token set-authorized subcommand.
1 parent 903d339 commit 8c5d73e

6 files changed

Lines changed: 404 additions & 0 deletions

File tree

FULL_HELP_DOCS.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1926,6 +1926,7 @@ Interact with SEP-41 tokens and Stellar Asset Contracts
19261926
- `mint` — Mint new tokens to an account or contract (SAC admin)
19271927
- `clawback` — Claw back tokens from an account or contract (SAC admin)
19281928
- `set-admin` — Transfer administration of the token to a new admin (SAC admin)
1929+
- `set-authorized` — Authorize or deauthorize an account to hold the token (SAC admin)
19291930

19301931
## `stellar token transfer`
19311932

@@ -2277,6 +2278,49 @@ Transfer administration of the token to a new admin (SAC admin)
22772278
- `--sign-with-ledger` — Sign with a ledger wallet
22782279
- `--auto-sign` — Sign without prompting for approval. Only applies to signatures that require user approval, like non-root Soroban auth entries
22792280

2281+
## `stellar token set-authorized`
2282+
2283+
Authorize or deauthorize an account to hold the token (SAC admin)
2284+
2285+
**Usage:** `stellar token set-authorized [OPTIONS] --id <ID> --admin <ADMIN> --account <ACCOUNT> --authorize <AUTHORIZE>`
2286+
2287+
###### **Global Options:**
2288+
2289+
- `--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
2290+
2291+
###### **Options:**
2292+
2293+
- `--id <ID>` — The token whose authorization to set: a contract id or alias, or a classic asset as `CODE:ISSUER`
2294+
- `--admin <ADMIN>` — The token's 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)
2295+
- `--account <ACCOUNT>` — Account or contract whose authorization to set. Accepts a `G…`/`M…` account, a `C…` contract address, or an alias
2296+
- `--authorize <AUTHORIZE>` — Whether the account is authorized (`true`) to hold and transact the token, or deauthorized/frozen (`false`)
2297+
2298+
Possible values: `true`, `false`
2299+
2300+
- `--output <OUTPUT>` — Format of the output
2301+
2302+
Default value: `text`
2303+
2304+
Possible values:
2305+
- `text`: Human-readable text
2306+
- `json`: Compact, single-line JSON output
2307+
- `json-formatted`: Formatted (multiline) JSON output
2308+
2309+
###### **RPC Options:**
2310+
2311+
- `--rpc-url <RPC_URL>` — RPC server endpoint
2312+
- `--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
2313+
- `--network-passphrase <NETWORK_PASSPHRASE>` — Network passphrase to sign the transaction sent to the rpc server
2314+
- `-n`, `--network <NETWORK>` — Name of network to use from config
2315+
2316+
###### **Signing Options:**
2317+
2318+
- `--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
2319+
- `--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`
2320+
- `--sign-with-lab` — Sign with https://lab.stellar.org
2321+
- `--sign-with-ledger` — Sign with a ledger wallet
2322+
- `--auto-sign` — Sign without prompting for approval. Only applies to signatures that require user approval, like non-root Soroban auth entries
2323+
22802324
## `stellar tx`
22812325

22822326
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
@@ -7,6 +7,7 @@ pub mod mint;
77
pub mod name;
88
pub mod renamed;
99
pub mod set_admin;
10+
pub mod set_authorized;
1011
pub mod symbol;
1112
pub mod transfer;
1213

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
use serde_json::Value;
2+
use soroban_test::{AssertExt, TestEnv};
3+
4+
use crate::integration::{
5+
token::{add_trustline, deploy_sac, sac_id},
6+
util::{new_account, test_address},
7+
};
8+
9+
/// Enable the revocable flag on `issuer`, required to deauthorize an existing
10+
/// trustline.
11+
fn enable_revocable(sandbox: &TestEnv, issuer: &str) {
12+
sandbox
13+
.new_assert_cmd("tx")
14+
.args(["new", "set-options", "--set-revocable", "--source", issuer])
15+
.assert()
16+
.success();
17+
}
18+
19+
/// Read whether `account` is authorized on the token through its SAC.
20+
fn sac_authorized(sandbox: &TestEnv, contract_id: &str, account: &str) -> bool {
21+
let stdout = sandbox
22+
.new_assert_cmd("contract")
23+
.args([
24+
"invoke",
25+
"--id",
26+
contract_id,
27+
"--source-account",
28+
"test",
29+
"--",
30+
"authorized",
31+
"--id",
32+
account,
33+
])
34+
.assert()
35+
.success()
36+
.stdout_as_str();
37+
stdout.trim().parse().unwrap()
38+
}
39+
40+
#[tokio::test]
41+
async fn set_authorized_toggles_authorization_and_returns_receipt() {
42+
let sandbox = &TestEnv::new();
43+
let test = test_address(sandbox);
44+
let issuer = new_account(sandbox, "issuer");
45+
let asset = format!("USDC:{issuer}");
46+
47+
// Deauthorizing an existing trustline requires the issuer to be revocable.
48+
enable_revocable(sandbox, "issuer");
49+
add_trustline(sandbox, "test", &asset);
50+
deploy_sac(sandbox, &asset, "issuer");
51+
let sac = sac_id(sandbox, &asset);
52+
53+
// A fresh trustline starts authorized.
54+
assert!(
55+
sac_authorized(sandbox, &sac, &test),
56+
"trustline should start authorized"
57+
);
58+
59+
let stdout = sandbox
60+
.new_assert_cmd("token")
61+
.args([
62+
"set-authorized",
63+
"--id",
64+
&asset,
65+
"--admin",
66+
"issuer",
67+
"--account",
68+
&test,
69+
"--authorize",
70+
"false",
71+
"--output",
72+
"json",
73+
])
74+
.assert()
75+
.success()
76+
.stdout_as_str();
77+
let receipt: Value = serde_json::from_str(&stdout).unwrap();
78+
assert!(
79+
receipt["tx_hash"].as_str().is_some(),
80+
"expected a tx hash, got: {receipt}"
81+
);
82+
83+
// The account is now deauthorized on-chain.
84+
assert!(
85+
!sac_authorized(sandbox, &sac, &test),
86+
"account should be deauthorized after set-authorized false"
87+
);
88+
}
89+
90+
#[tokio::test]
91+
async fn set_authorized_fails_when_sac_not_deployed() {
92+
let sandbox = &TestEnv::new();
93+
let test = test_address(sandbox);
94+
let issuer = new_account(sandbox, "issuer");
95+
let asset = format!("USDC:{issuer}");
96+
97+
// No SAC deployed → structured deploy-pointer error with a typed discriminator.
98+
let stdout = sandbox
99+
.new_assert_cmd("token")
100+
.args([
101+
"set-authorized",
102+
"--id",
103+
&asset,
104+
"--admin",
105+
"issuer",
106+
"--account",
107+
&test,
108+
"--authorize",
109+
"true",
110+
"--output",
111+
"json",
112+
])
113+
.assert()
114+
.failure()
115+
.stdout_as_str();
116+
let value: Value = serde_json::from_str(&stdout).unwrap();
117+
assert_eq!(
118+
value["error"]["type"], "sac_not_deployed",
119+
"expected a typed error, got: {stdout}"
120+
);
121+
}
122+
123+
#[tokio::test]
124+
async fn set_authorized_rejects_muxed_source_with_clear_error() {
125+
let sandbox = &TestEnv::new();
126+
let test = test_address(sandbox);
127+
128+
// Muxed (M…) source accounts aren't supported by the invoke pipeline yet
129+
// (see #2645). Until then the command must reject them up front with a clear
130+
// message rather than a raw strkey decode error deep in the pipeline.
131+
let muxed = "MA3D5KRYM6CB7OWQ6TWYRR3Z4T7GNZLKERYNZGGA5SOAOPIFY6YQGAAAAAAAAAPCICBKU";
132+
sandbox
133+
.new_assert_cmd("token")
134+
.args([
135+
"set-authorized",
136+
"--id",
137+
"native",
138+
"--admin",
139+
muxed,
140+
"--account",
141+
&test,
142+
"--authorize",
143+
"true",
144+
])
145+
.assert()
146+
.failure()
147+
.stderr(predicates::str::contains(
148+
"muxed (M…) source accounts are not yet supported",
149+
));
150+
}

cmd/soroban-cli/src/cli.rs

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

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

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

@@ -43,6 +44,9 @@ pub enum Cmd {
4344

4445
/// Transfer administration of the token to a new admin (SAC admin)
4546
SetAdmin(set_admin::Cmd),
47+
48+
/// Authorize or deauthorize an account to hold the token (SAC admin)
49+
SetAuthorized(set_authorized::Cmd),
4650
}
4751

4852
#[derive(thiserror::Error, Debug)]
@@ -67,6 +71,8 @@ pub enum Error {
6771
Clawback(#[from] clawback::Error),
6872
#[error(transparent)]
6973
SetAdmin(#[from] set_admin::Error),
74+
#[error(transparent)]
75+
SetAuthorized(#[from] set_authorized::Error),
7076
}
7177

7278
impl Error {
@@ -84,6 +90,7 @@ impl Error {
8490
Error::Mint(e) => e.error_type(),
8591
Error::Clawback(e) => e.error_type(),
8692
Error::SetAdmin(e) => e.error_type(),
93+
Error::SetAuthorized(e) => e.error_type(),
8794
}
8895
}
8996
}
@@ -101,6 +108,7 @@ impl Cmd {
101108
Cmd::Mint(cmd) => cmd.run(global_args).await?,
102109
Cmd::Clawback(cmd) => cmd.run(global_args).await?,
103110
Cmd::SetAdmin(cmd) => cmd.run(global_args).await?,
111+
Cmd::SetAuthorized(cmd) => cmd.run(global_args).await?,
104112
}
105113
Ok(())
106114
}

0 commit comments

Comments
 (0)