Skip to content

Commit ffdd315

Browse files
committed
Add stellar token set-authorized subcommand.
1 parent d0b26d9 commit ffdd315

6 files changed

Lines changed: 460 additions & 0 deletions

File tree

‎FULL_HELP_DOCS.md‎

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

19621963
## `stellar token transfer`
19631964

@@ -2450,6 +2451,56 @@ Calls the token's Stellar Asset Contract `set_admin` function. A non-SAC contrac
24502451
- `--fee <FEE>` — ⚠️ Deprecated, use `--inclusion-fee`. Fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm
24512452
- `--inclusion-fee <INCLUSION_FEE>` — Maximum fee amount for transaction inclusion, in stroops. 1 stroop = 0.0000001 xlm. Defaults to 100 if no arg, env, or config value is provided
24522453

2454+
## `stellar token set-authorized`
2455+
2456+
Authorize or deauthorize an account to hold the token (SAC admin)
2457+
2458+
Calls the token's Stellar Asset Contract `set_authorized` function. A non-SAC contract with a same-named function that takes different arguments will fail or misbehave — use `stellar contract invoke` for those.
2459+
2460+
**Usage:** `stellar token set-authorized [OPTIONS] --id <ID> --account <ACCOUNT> --authorize <AUTHORIZE> --source-account <SOURCE_ACCOUNT>`
2461+
2462+
###### **Global Options:**
2463+
2464+
- `--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
2465+
2466+
###### **Options:**
2467+
2468+
- `--id <ID>` — The token whose authorization to set: a contract id or alias, or a classic asset as `CODE:ISSUER`
2469+
- `--account <ACCOUNT>` — Account or contract whose authorization to set. Accepts a `G…`/`M…` account, a `C…` contract address, or an alias
2470+
- `--authorize <AUTHORIZE>` — Whether the account is authorized (`true`) to hold and transact the token, or deauthorized/frozen (`false`)
2471+
2472+
Possible values: `true`, `false`
2473+
2474+
- `--output <OUTPUT>` — Format of the output
2475+
2476+
Default value: `text`
2477+
2478+
Possible values:
2479+
- `text`: Human-readable text
2480+
- `json`: Compact, single-line JSON output
2481+
- `json-formatted`: Formatted (multiline) JSON output
2482+
2483+
###### **RPC Options:**
2484+
2485+
- `--rpc-url <RPC_URL>` — RPC server endpoint
2486+
- `--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
2487+
- `--network-passphrase <NETWORK_PASSPHRASE>` — Network passphrase to sign the transaction sent to the rpc server
2488+
- `-n`, `--network <NETWORK>` — Name of network to use from config
2489+
2490+
###### **Signing Options:**
2491+
2492+
- `--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
2493+
- `--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`
2494+
- `--sign-with-lab` — Sign with https://lab.stellar.org
2495+
- `--sign-with-ledger` — Sign with a ledger wallet
2496+
- `--auto-sign` — Sign without prompting for approval. Only applies to signatures that require user approval, like non-root Soroban auth entries
2497+
2498+
###### **Transaction Options:**
2499+
2500+
- `-s`, `--source-account <SOURCE_ACCOUNT>` [alias: `source`] — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` was NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail
2501+
- `--fee <FEE>` — ⚠️ Deprecated, use `--inclusion-fee`. Fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm
2502+
- `--inclusion-fee <INCLUSION_FEE>` — Maximum fee amount for transaction inclusion, in stroops. 1 stroop = 0.0000001 xlm. Defaults to 100 if no arg, env, or config value is provided
2503+
24532504
## `stellar tx`
24542505

24552506
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
@@ -9,6 +9,7 @@ pub mod mint;
99
pub mod name;
1010
pub mod renamed;
1111
pub mod set_admin;
12+
pub mod set_authorized;
1213
pub mod symbol;
1314
pub mod transfer;
1415
pub mod transfer_from;
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
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::{deploy_hello, 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+
"--source",
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+
"--source",
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+
"--source",
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+
}
151+
152+
#[tokio::test]
153+
async fn set_authorized_warns_when_target_is_not_a_sac() {
154+
let sandbox = &TestEnv::new();
155+
let test = test_address(sandbox);
156+
let contract_id = deploy_hello(sandbox).await;
157+
158+
// Pointing a SAC-admin command at a plain wasm contract warns. The call then
159+
// fails (hello_world has no `set_authorized`), but the heads-up is the point.
160+
let stderr = sandbox
161+
.new_assert_cmd("token")
162+
.args([
163+
"set-authorized",
164+
"--id",
165+
&contract_id,
166+
"--source",
167+
"test",
168+
"--account",
169+
&test,
170+
"--authorize",
171+
"true",
172+
])
173+
.assert()
174+
.failure()
175+
.stderr_as_str();
176+
assert!(
177+
stderr.contains("is not a Stellar Asset Contract"),
178+
"expected a non-SAC warning, got: {stderr}"
179+
);
180+
}
181+
182+
#[tokio::test]
183+
async fn set_authorized_does_not_warn_when_target_is_a_sac() {
184+
let sandbox = &TestEnv::new();
185+
let test = test_address(sandbox);
186+
let issuer = new_account(sandbox, "issuer");
187+
let asset = format!("USDC:{issuer}");
188+
189+
add_trustline(sandbox, "test", &asset);
190+
deploy_sac(sandbox, &asset, "issuer");
191+
// Reference the SAC by its contract id, not the asset, so the check can only
192+
// clear it by inspecting the on-chain executable — not the id's text form.
193+
let sac = sac_id(sandbox, &asset);
194+
195+
// Re-authorizing an already-authorized trustline is a no-op success and needs
196+
// no revocable flag — enough to exercise the SAC path without warning.
197+
let stderr = sandbox
198+
.new_assert_cmd("token")
199+
.args([
200+
"set-authorized",
201+
"--id",
202+
&sac,
203+
"--source",
204+
"issuer",
205+
"--account",
206+
&test,
207+
"--authorize",
208+
"true",
209+
])
210+
.assert()
211+
.success()
212+
.stderr_as_str();
213+
assert!(
214+
!stderr.contains("is not a Stellar Asset Contract"),
215+
"a genuine SAC should not warn, got: {stderr}"
216+
);
217+
}

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

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

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ pub mod decimals;
99
pub mod mint;
1010
pub mod name;
1111
pub mod set_admin;
12+
pub mod set_authorized;
1213
pub mod symbol;
1314
pub mod transfer;
1415
pub mod transfer_from;
@@ -67,6 +68,13 @@ pub enum Cmd {
6768
/// contract with a same-named function that takes different arguments will
6869
/// fail or misbehave — use `stellar contract invoke` for those.
6970
SetAdmin(set_admin::Cmd),
71+
72+
/// Authorize or deauthorize an account to hold the token (SAC admin)
73+
///
74+
/// Calls the token's Stellar Asset Contract `set_authorized` function. A
75+
/// non-SAC contract with a same-named function that takes different arguments
76+
/// will fail or misbehave — use `stellar contract invoke` for those.
77+
SetAuthorized(set_authorized::Cmd),
7078
}
7179

7280
#[derive(thiserror::Error, Debug)]
@@ -97,6 +105,8 @@ pub enum Error {
97105
Clawback(#[from] clawback::Error),
98106
#[error(transparent)]
99107
SetAdmin(#[from] set_admin::Error),
108+
#[error(transparent)]
109+
SetAuthorized(#[from] set_authorized::Error),
100110
}
101111

102112
impl Error {
@@ -117,6 +127,7 @@ impl Error {
117127
Error::Mint(e) => e.error_type(),
118128
Error::Clawback(e) => e.error_type(),
119129
Error::SetAdmin(e) => e.error_type(),
130+
Error::SetAuthorized(e) => e.error_type(),
120131
}
121132
}
122133
}
@@ -137,6 +148,7 @@ impl Cmd {
137148
Cmd::Mint(cmd) => cmd.run(global_args).await?,
138149
Cmd::Clawback(cmd) => cmd.run(global_args).await?,
139150
Cmd::SetAdmin(cmd) => cmd.run(global_args).await?,
151+
Cmd::SetAuthorized(cmd) => cmd.run(global_args).await?,
140152
}
141153
Ok(())
142154
}

0 commit comments

Comments
 (0)