Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 49 additions & 2 deletions FULL_HELP_DOCS.md
Original file line number Diff line number Diff line change
Expand Up @@ -1957,6 +1957,7 @@ Interact with SEP-41 tokens and Stellar Asset Contracts
- `allowance` — Read the allowance a spender has on an owner's behalf
- `mint` — Mint new tokens to an account or contract (SAC admin)
- `clawback` — Claw back tokens from an account or contract (SAC admin)
- `set-admin` — Transfer administration of the token to a new admin (SAC admin)

## `stellar token transfer`

Expand Down Expand Up @@ -2323,7 +2324,7 @@ Calls the token's Stellar Asset Contract `mint` function. A non-SAC contract wit

###### **Options:**

- `--id <ID>` — The token to mint: a contract id or alias, `native`, or a classic asset as `CODE:ISSUER`
- `--id <ID>` — The token to mint: a contract id or alias, or a classic asset as `CODE:ISSUER`
- `--to <TO>` — Account or contract to mint the tokens to. Accepts a `G…`/`M…` account, a `C…` contract address, or an alias
- `--amount <AMOUNT>` — Amount to mint, in the token's smallest unit (stroops for a Stellar Asset Contract)
- `--output <OUTPUT>` — Format of the output
Expand Down Expand Up @@ -2370,7 +2371,7 @@ Calls the token's Stellar Asset Contract `clawback` function. A non-SAC contract

###### **Options:**

- `--id <ID>` — The token to claw back: a contract id or alias, `native`, or a classic asset as `CODE:ISSUER`
- `--id <ID>` — The token to claw back: a contract id or alias, or a classic asset as `CODE:ISSUER`
- `--from <FROM>` — Account or contract to claw the tokens back from. Accepts a `G…` account, a `C…` contract address, or an alias
- `--amount <AMOUNT>` — Amount to claw back, in the token's smallest unit (stroops for a Stellar Asset Contract)
- `--output <OUTPUT>` — Format of the output
Expand Down Expand Up @@ -2403,6 +2404,52 @@ Calls the token's Stellar Asset Contract `clawback` function. A non-SAC contract
- `--fee <FEE>` — ⚠️ Deprecated, use `--inclusion-fee`. Fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm
- `--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

## `stellar token set-admin`

Transfer administration of the token to a new admin (SAC admin)

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.

**Usage:** `stellar token set-admin [OPTIONS] --id <ID> --new-admin <NEW_ADMIN> --source-account <SOURCE_ACCOUNT>`

###### **Global Options:**

- `--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

###### **Options:**

- `--id <ID>` — The token to re-administer: a contract id or alias, or a classic asset as `CODE:ISSUER`
- `--new-admin <NEW_ADMIN>` — The new administrator to hand control to. Accepts a `G…` account, a `C…` contract address, or an alias
- `--output <OUTPUT>` — Format of the output

Default value: `text`

Possible values:
- `text`: Human-readable text
- `json`: Compact, single-line JSON output
- `json-formatted`: Formatted (multiline) JSON output

###### **RPC Options:**

- `--rpc-url <RPC_URL>` — RPC server endpoint
- `--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
- `--network-passphrase <NETWORK_PASSPHRASE>` — Network passphrase to sign the transaction sent to the rpc server
- `-n`, `--network <NETWORK>` — Name of network to use from config

###### **Signing Options:**

- `--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
- `--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`
- `--sign-with-lab` — Sign with https://lab.stellar.org
- `--sign-with-ledger` — Sign with a ledger wallet
- `--auto-sign` — Sign without prompting for approval. Only applies to signatures that require user approval, like non-root Soroban auth entries

###### **Transaction Options:**

- `-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
- `--fee <FEE>` — ⚠️ Deprecated, use `--inclusion-fee`. Fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm
- `--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

## `stellar tx`

Sign, Simulate, and Send transactions
Expand Down
1 change: 1 addition & 0 deletions cmd/crates/soroban-test/tests/it/integration/token/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ pub mod decimals;
pub mod mint;
pub mod name;
pub mod renamed;
pub mod set_admin;
pub mod symbol;
pub mod transfer;
pub mod transfer_from;
Expand Down
201 changes: 201 additions & 0 deletions cmd/crates/soroban-test/tests/it/integration/token/set_admin.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
use serde_json::Value;
use soroban_test::{AssertExt, TestEnv};

use crate::integration::{
token::{add_trustline, deploy_sac, sac_balance, sac_id},
util::{deploy_hello, new_account, test_address},
};

#[tokio::test]
async fn set_admin_transfers_control_and_returns_receipt() {
let sandbox = &TestEnv::new();
let test = test_address(sandbox);
let issuer = new_account(sandbox, "issuer");
let new_admin = new_account(sandbox, "newadmin");
let asset = format!("USDC:{issuer}");

add_trustline(sandbox, "test", &asset);
deploy_sac(sandbox, &asset, "issuer");

let stdout = sandbox
.new_assert_cmd("token")
.args([
"set-admin",
"--id",
&asset,
"--source",
"issuer",
"--new-admin",
&new_admin,
"--output",
"json",
])
.assert()
.success()
.stdout_as_str();
let receipt: Value = serde_json::from_str(&stdout).unwrap();
assert!(
receipt["tx_hash"].as_str().is_some(),
"expected a tx hash, got: {receipt}"
);

// Control has transferred: the new admin can now mint, proving the change
// took effect.
sandbox
.new_assert_cmd("token")
.args([
"mint", "--id", &asset, "--source", "newadmin", "--to", &test, "--amount", "9000000",
])
.assert()
.success();
let sac = sac_id(sandbox, &asset);
assert_eq!(
sac_balance(sandbox, &sac, &test),
9_000_000,
"the new admin should be able to mint"
);
}

#[tokio::test]
async fn set_admin_fails_when_sac_not_deployed() {
let sandbox = &TestEnv::new();
let issuer = new_account(sandbox, "issuer");
let new_admin = new_account(sandbox, "newadmin");
let asset = format!("USDC:{issuer}");

// No SAC deployed → structured deploy-pointer error with a typed discriminator.
let stdout = sandbox
.new_assert_cmd("token")
.args([
"set-admin",
"--id",
&asset,
"--source",
"issuer",
"--new-admin",
&new_admin,
"--output",
"json",
])
.assert()
.failure()
.stdout_as_str();
let value: Value = serde_json::from_str(&stdout).unwrap();
assert_eq!(
value["error"]["type"], "sac_not_deployed",
"expected a typed error, got: {stdout}"
);
}

#[tokio::test]
async fn set_admin_rejects_muxed_source_with_clear_error() {
let sandbox = &TestEnv::new();
let new_admin = new_account(sandbox, "newadmin");

// Muxed (M…) source accounts aren't supported by the invoke pipeline yet
// (see #2645). Until then the command must reject them up front with a clear
// message rather than a raw strkey decode error deep in the pipeline.
let muxed = "MA3D5KRYM6CB7OWQ6TWYRR3Z4T7GNZLKERYNZGGA5SOAOPIFY6YQGAAAAAAAAAPCICBKU";
sandbox
.new_assert_cmd("token")
.args([
"set-admin",
"--id",
"native",
"--source",
muxed,
"--new-admin",
&new_admin,
])
.assert()
.failure()
.stderr(predicates::str::contains(
"muxed (M…) source accounts are not yet supported",
));
}

#[tokio::test]
async fn set_admin_rejects_muxed_new_admin_with_clear_error() {
let sandbox = &TestEnv::new();

// A muxed (M…) successor would be stranded — it can't sign as a source and
// the SAC stores a plain `Address` — so the command rejects it up front
// rather than performing an irreversible transfer to an unusable admin.
let muxed = "MA3D5KRYM6CB7OWQ6TWYRR3Z4T7GNZLKERYNZGGA5SOAOPIFY6YQGAAAAAAAAAPCICBKU";
sandbox
.new_assert_cmd("token")
.args([
"set-admin",
"--id",
"native",
"--source",
"test",
"--new-admin",
muxed,
])
.assert()
.failure()
.stderr(predicates::str::contains(
"muxed (M…) new-admin accounts are not yet supported",
));
}

#[tokio::test]
async fn set_admin_warns_when_target_is_not_a_sac() {
let sandbox = &TestEnv::new();
let new_admin = new_account(sandbox, "newadmin");
let contract_id = deploy_hello(sandbox).await;

// Pointing a SAC-admin command at a plain wasm contract warns. The call then
// fails (hello_world has no `set_admin`), but the heads-up is the point.
let stderr = sandbox
.new_assert_cmd("token")
.args([
"set-admin",
"--id",
&contract_id,
"--source",
"test",
"--new-admin",
&new_admin,
])
.assert()
.failure()
.stderr_as_str();
assert!(
stderr.contains("is not a Stellar Asset Contract"),
"expected a non-SAC warning, got: {stderr}"
);
}

#[tokio::test]
async fn set_admin_does_not_warn_when_target_is_a_sac() {
let sandbox = &TestEnv::new();
let issuer = new_account(sandbox, "issuer");
let new_admin = new_account(sandbox, "newadmin");
let asset = format!("USDC:{issuer}");

deploy_sac(sandbox, &asset, "issuer");
// Reference the SAC by its contract id, not the asset, so the check can only
// clear it by inspecting the on-chain executable — not the id's text form.
let sac = sac_id(sandbox, &asset);

let stderr = sandbox
.new_assert_cmd("token")
.args([
"set-admin",
"--id",
&sac,
"--source",
"issuer",
"--new-admin",
&new_admin,
])
.assert()
.success()
.stderr_as_str();
assert!(
!stderr.contains("is not a Stellar Asset Contract"),
"a genuine SAC should not warn, got: {stderr}"
);
}
1 change: 1 addition & 0 deletions cmd/soroban-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ fn json_error_format(cmd: &commands::Cmd) -> Option<crate::output::Format> {
commands::Cmd::Token(token::Cmd::Allowance(cmd)) => cmd.output.into(),
commands::Cmd::Token(token::Cmd::Mint(cmd)) => cmd.output.into(),
commands::Cmd::Token(token::Cmd::Clawback(cmd)) => cmd.output.into(),
commands::Cmd::Token(token::Cmd::SetAdmin(cmd)) => cmd.output.into(),
_ => return None,
};

Expand Down
4 changes: 2 additions & 2 deletions cmd/soroban-cli/src/commands/token/clawback.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ use crate::{
#[derive(Debug, Parser, Clone)]
#[group(skip)]
pub struct Cmd {
/// The token to claw back: a contract id or alias, `native`, or a classic
/// asset as `CODE:ISSUER`.
/// The token to claw back: a contract id or alias, or a classic asset as
/// `CODE:ISSUER`.
#[arg(long = "id")]
pub id: UnresolvedToken,

Expand Down
4 changes: 2 additions & 2 deletions cmd/soroban-cli/src/commands/token/mint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ use crate::{
#[derive(Debug, Parser, Clone)]
#[group(skip)]
pub struct Cmd {
/// The token to mint: a contract id or alias, `native`, or a classic asset
/// as `CODE:ISSUER`.
/// The token to mint: a contract id or alias, or a classic asset as
/// `CODE:ISSUER`.
#[arg(long = "id")]
pub id: UnresolvedToken,

Expand Down
12 changes: 12 additions & 0 deletions cmd/soroban-cli/src/commands/token/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ pub mod clawback;
pub mod decimals;
pub mod mint;
pub mod name;
pub mod set_admin;
pub mod symbol;
pub mod transfer;
pub mod transfer_from;
Expand Down Expand Up @@ -59,6 +60,13 @@ pub enum Cmd {
/// contract with a same-named function that takes different arguments will
/// fail or misbehave — use `stellar contract invoke` for those.
Clawback(clawback::Cmd),

/// Transfer administration of the token to a new admin (SAC admin)
///
/// 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.
SetAdmin(set_admin::Cmd),
}

#[derive(thiserror::Error, Debug)]
Expand Down Expand Up @@ -87,6 +95,8 @@ pub enum Error {
Mint(#[from] mint::Error),
#[error(transparent)]
Clawback(#[from] clawback::Error),
#[error(transparent)]
SetAdmin(#[from] set_admin::Error),
}

impl Error {
Expand All @@ -106,6 +116,7 @@ impl Error {
Error::Allowance(e) => e.error_type(),
Error::Mint(e) => e.error_type(),
Error::Clawback(e) => e.error_type(),
Error::SetAdmin(e) => e.error_type(),
}
}
}
Expand All @@ -125,6 +136,7 @@ impl Cmd {
Cmd::Allowance(cmd) => cmd.run(global_args).await?,
Cmd::Mint(cmd) => cmd.run(global_args).await?,
Cmd::Clawback(cmd) => cmd.run(global_args).await?,
Cmd::SetAdmin(cmd) => cmd.run(global_args).await?,
}
Ok(())
}
Expand Down
Loading
Loading