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
69 changes: 69 additions & 0 deletions FULL_HELP_DOCS.md
Original file line number Diff line number Diff line change
Expand Up @@ -1709,6 +1709,7 @@ Create a new transaction
* `bump-sequence` — Bump sequence number to invalidate older transactions
* `change-trust` — Create, update, or delete a trustline
* `claim-claimable-balance` — Claim a claimable balance by its balance ID
* `clawback` — Clawback an asset from an account
* `clawback-claimable-balance` — Clawback a claimable balance by its balance ID
* `create-account` — Create and fund a new account
* `create-claimable-balance` — Create a claimable balance that can be claimed by specified accounts
Expand Down Expand Up @@ -1843,6 +1844,37 @@ Claim a claimable balance by its balance ID



## `stellar tx new clawback`

Clawback an asset from an account

**Usage:** `stellar tx new clawback [OPTIONS] --source-account <SOURCE_ACCOUNT> --from <FROM> --asset <ASSET> --amount <AMOUNT>`

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

* `--fee <FEE>` — fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm

Default value: `100`
* `--cost` — Output the cost execution to stderr
* `--instructions <INSTRUCTIONS>` — Number of instructions to simulate
* `--build-only` — Build the transaction and only write the base64 xdr to stdout
* `--rpc-url <RPC_URL>` — RPC server endpoint
* `--rpc-header <RPC_HEADERS>` — RPC Header(s) to include in requests to the RPC provider
* `--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
* `-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` or `--sim-only` flags were NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail
* `--global` — ⚠️ Deprecated: global config is always on
* `--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
* `--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
* `--from <FROM>` — Account to clawback assets from, e.g. `GBX...`
* `--asset <ASSET>` — Asset to clawback
* `--amount <AMOUNT>` — Amount of the asset to clawback, in stroops. 1 stroop = 0.0000001 of the asset



## `stellar tx new clawback-claimable-balance`

Clawback a claimable balance by its balance ID
Expand Down Expand Up @@ -2280,6 +2312,7 @@ Add Operation to a transaction
* `bump-sequence` — Bump sequence number to invalidate older transactions
* `change-trust` — Create, update, or delete a trustline
* `claim-claimable-balance` — Claim a claimable balance by its balance ID
* `clawback` — Clawback an asset from an account
* `clawback-claimable-balance` — Clawback a claimable balance by its balance ID
* `create-account` — Create and fund a new account
* `create-claimable-balance` — Create a claimable balance that can be claimed by specified accounts
Expand Down Expand Up @@ -2434,6 +2467,42 @@ Claim a claimable balance by its balance ID



## `stellar tx operation add clawback`

Clawback an asset from an account

**Usage:** `stellar tx operation add clawback [OPTIONS] --source-account <SOURCE_ACCOUNT> --from <FROM> --asset <ASSET> --amount <AMOUNT> [TX_XDR]`

###### **Arguments:**

* `<TX_XDR>` — Base-64 transaction envelope XDR or file containing XDR to decode, or stdin if empty

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

* `--operation-source-account <OPERATION_SOURCE_ACCOUNT>` [alias: `op-source`] — Source account used for the operation
* `--fee <FEE>` — fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm

Default value: `100`
* `--cost` — Output the cost execution to stderr
* `--instructions <INSTRUCTIONS>` — Number of instructions to simulate
* `--build-only` — Build the transaction and only write the base64 xdr to stdout
* `--rpc-url <RPC_URL>` — RPC server endpoint
* `--rpc-header <RPC_HEADERS>` — RPC Header(s) to include in requests to the RPC provider
* `--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
* `-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` or `--sim-only` flags were NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail
* `--global` — ⚠️ Deprecated: global config is always on
* `--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
* `--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
* `--from <FROM>` — Account to clawback assets from, e.g. `GBX...`
* `--asset <ASSET>` — Asset to clawback
* `--amount <AMOUNT>` — Amount of the asset to clawback, in stroops. 1 stroop = 0.0000001 of the asset



## `stellar tx operation add clawback-claimable-balance`

Clawback a claimable balance by its balance ID
Expand Down
151 changes: 151 additions & 0 deletions cmd/crates/soroban-test/tests/it/integration/tx/operations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1429,3 +1429,154 @@ async fn clawback_claimable_balance() {
.assert()
.failure(); // This should fail because the balance was clawed back
}

#[tokio::test]
async fn clawback() {
let sandbox = &TestEnv::new();
let (test, issuer) = setup_accounts(sandbox);

// Enable revocable flag first, then clawback on the issuer account
sandbox
.new_assert_cmd("tx")
.args(["new", "set-options", "--set-revocable", "--source", "test1"])
.assert()
.success();

sandbox
.new_assert_cmd("tx")
.args([
"new",
"set-options",
"--set-clawback-enabled",
"--source",
"test1",
])
.assert()
.success();

// Create asset for clawback test
let asset = format!("USDC:{issuer}");
let limit = 100_000_000_000;
let initial_balance = 50_000_000_000;
issue_asset(sandbox, &test, &asset, limit, initial_balance).await;

// Create holder account for clawback
let holder = new_account(sandbox, "holder");

// Setup trustline for holder
sandbox
.new_assert_cmd("tx")
.args([
"new",
"change-trust",
"--source",
"holder",
"--line",
&asset,
])
.assert()
.success();

// Authorize holder's trustline and enable clawback
sandbox
.new_assert_cmd("tx")
.args([
"new",
"set-trustline-flags",
"--asset",
&asset,
"--trustor",
&holder,
"--set-authorize",
"--source",
"test1",
])
.assert()
.success();

// Send some assets to the holder account
let payment_amount = "10000000000"; // 1000 USDC
sandbox
.new_assert_cmd("tx")
.args([
"new",
"payment",
"--destination",
&holder,
"--asset",
&asset,
"--amount",
payment_amount,
"--source",
"test1",
])
.assert()
.success();

// Test clawback command
// this should succeed for the issuer
let clawback_amount = "5000000000"; // 500 USDC
sandbox
.new_assert_cmd("tx")
.args([
"new",
"clawback",
"--from",
&holder,
"--asset",
&asset,
"--amount",
clawback_amount,
"--source",
"test1", // issuer should be able to clawback
])
.assert()
.success();

// Verify holder's balance after clawback (should be 500 USDC: 1000 sent - 500 clawed back)
let horizon_url = format!("http://localhost:8000/accounts/{}", holder);
let response = reqwest::get(&horizon_url)
.await
.expect("Failed to fetch account from Horizon");
let json: serde_json::Value = response
.json()
.await
.expect("Failed to parse Horizon response");

let final_balance = json["balances"]
.as_array()
.unwrap()
.iter()
.find(|balance| {
balance["asset_code"].as_str() == Some("USDC")
&& balance["asset_issuer"].as_str() == Some(&issuer)
})
.expect("USDC balance not found after clawback")["balance"]
.as_str()
.unwrap()
.parse::<f64>()
.unwrap();

assert_eq!(
final_balance, 500.0,
"Holder should have 500 USDC remaining after clawback (1000 sent - 500 clawed back)"
);

// Verify that a non-issuer cannot perform clawback
sandbox
.new_assert_cmd("tx")
.args([
"new",
"clawback",
"--from",
&holder,
"--asset",
&asset,
"--amount",
"1000000000", // 100 USDC
"--source",
"holder", // non-issuer should not be able to clawback
])
.assert()
.failure();
}
1 change: 1 addition & 0 deletions cmd/soroban-cli/src/commands/tx/help.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ pub const ACCOUNT_MERGE: &str = "Transfer XLM balance to another account and rem
pub const BUMP_SEQUENCE: &str = "Bump sequence number to invalidate older transactions";
pub const CHANGE_TRUST: &str = "Create, update, or delete a trustline";
pub const CLAIM_CLAIMABLE_BALANCE: &str = "Claim a claimable balance by its balance ID";
pub const CLAWBACK: &str = "Clawback an asset from an account";
pub const CLAWBACK_CLAIMABLE_BALANCE: &str = "Clawback a claimable balance by its balance ID";
pub const CREATE_ACCOUNT: &str = "Create and fund a new account";
pub const CREATE_CLAIMABLE_BALANCE: &str =
Expand Down
46 changes: 46 additions & 0 deletions cmd/soroban-cli/src/commands/tx/new/clawback.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
use clap::Parser;

use crate::{commands::tx, config::address, tx::builder, xdr};

#[derive(Parser, Debug, Clone)]
#[group(skip)]
pub struct Cmd {
#[command(flatten)]
pub tx: tx::Args,
#[clap(flatten)]
pub op: Args,
}

#[derive(Debug, clap::Args, Clone)]
pub struct Args {
/// Account to clawback assets from, e.g. `GBX...`
#[arg(long)]
pub from: address::UnresolvedMuxedAccount,
/// Asset to clawback
#[arg(long)]
pub asset: builder::Asset,
/// Amount of the asset to clawback, in stroops. 1 stroop = 0.0000001 of the asset
#[arg(long)]
pub amount: builder::Amount,
}

impl TryFrom<&Cmd> for xdr::OperationBody {
type Error = tx::args::Error;
fn try_from(
Cmd {
tx,
op:
Args {
from,
asset,
amount,
},
}: &Cmd,
) -> Result<Self, Self::Error> {
Ok(xdr::OperationBody::Clawback(xdr::ClawbackOp {
from: tx.resolve_muxed_address(from)?,
asset: tx.resolve_asset(asset)?,
amount: amount.into(),
}))
}
}
5 changes: 5 additions & 0 deletions cmd/soroban-cli/src/commands/tx/new/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ pub mod account_merge;
pub mod bump_sequence;
pub mod change_trust;
pub mod claim_claimable_balance;
pub mod clawback;
pub mod clawback_claimable_balance;
pub mod create_account;
pub mod create_claimable_balance;
Expand All @@ -31,6 +32,8 @@ pub enum Cmd {
ChangeTrust(change_trust::Cmd),
#[command(about = super::help::CLAIM_CLAIMABLE_BALANCE)]
ClaimClaimableBalance(claim_claimable_balance::Cmd),
#[command(about = super::help::CLAWBACK)]
Clawback(clawback::Cmd),
#[command(about = super::help::CLAWBACK_CLAIMABLE_BALANCE)]
ClawbackClaimableBalance(clawback_claimable_balance::Cmd),
#[command(about = super::help::CREATE_ACCOUNT)]
Expand Down Expand Up @@ -71,6 +74,7 @@ impl TryFrom<&Cmd> for OperationBody {
Cmd::BumpSequence(cmd) => cmd.into(),
Cmd::ChangeTrust(cmd) => cmd.try_into()?,
Cmd::ClaimClaimableBalance(cmd) => cmd.try_into()?,
Cmd::Clawback(cmd) => cmd.try_into()?,
Cmd::ClawbackClaimableBalance(cmd) => cmd.try_into()?,
Cmd::CreateAccount(cmd) => cmd.try_into()?,
Cmd::CreateClaimableBalance(cmd) => cmd.try_into()?,
Expand All @@ -95,6 +99,7 @@ impl Cmd {
Cmd::BumpSequence(cmd) => cmd.tx.handle_and_print(op, global_args).await,
Cmd::ChangeTrust(cmd) => cmd.tx.handle_and_print(op, global_args).await,
Cmd::ClaimClaimableBalance(cmd) => cmd.tx.handle_and_print(op, global_args).await,
Cmd::Clawback(cmd) => cmd.tx.handle_and_print(op, global_args).await,
Cmd::ClawbackClaimableBalance(cmd) => cmd.tx.handle_and_print(op, global_args).await,
Cmd::CreateAccount(cmd) => cmd.tx.handle_and_print(op, global_args).await,
Cmd::CreateClaimableBalance(cmd) => cmd.tx.handle_and_print(op, global_args).await,
Expand Down
10 changes: 10 additions & 0 deletions cmd/soroban-cli/src/commands/tx/op/add/clawback.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
use crate::commands::tx::new::clawback;

#[derive(clap::Parser, Debug, Clone)]
#[group(skip)]
pub struct Cmd {
#[command(flatten)]
pub args: super::args::Args,
#[command(flatten)]
pub op: clawback::Cmd,
}
9 changes: 9 additions & 0 deletions cmd/soroban-cli/src/commands/tx/op/add/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ mod args;
mod bump_sequence;
mod change_trust;
mod claim_claimable_balance;
mod clawback;
mod clawback_claimable_balance;
mod create_account;
mod create_claimable_balance;
Expand All @@ -32,6 +33,8 @@ pub enum Cmd {
ChangeTrust(change_trust::Cmd),
#[command(about = help::CLAIM_CLAIMABLE_BALANCE)]
ClaimClaimableBalance(claim_claimable_balance::Cmd),
#[command(about = help::CLAWBACK)]
Clawback(clawback::Cmd),
#[command(about = help::CLAWBACK_CLAIMABLE_BALANCE)]
ClawbackClaimableBalance(clawback_claimable_balance::Cmd),
#[command(about = help::CREATE_ACCOUNT)]
Expand Down Expand Up @@ -78,6 +81,7 @@ impl TryFrom<&Cmd> for OperationBody {
Cmd::BumpSequence(bump_sequence::Cmd { op, .. }) => op.into(),
Cmd::ChangeTrust(change_trust::Cmd { op, .. }) => op.try_into()?,
Cmd::ClaimClaimableBalance(claim_claimable_balance::Cmd { op, .. }) => op.try_into()?,
Cmd::Clawback(clawback::Cmd { op, .. }) => op.try_into()?,
Cmd::ClawbackClaimableBalance(clawback_claimable_balance::Cmd { op, .. }) => {
op.try_into()?
}
Expand Down Expand Up @@ -128,6 +132,11 @@ impl Cmd {
tx_envelope_from_input(&cmd.args.tx_xdr)?,
cmd.args.source(),
),
Cmd::Clawback(cmd) => cmd.op.tx.add_op(
op,
tx_envelope_from_input(&cmd.args.tx_xdr)?,
cmd.args.source(),
),
Cmd::ClawbackClaimableBalance(cmd) => cmd.op.tx.add_op(
op,
tx_envelope_from_input(&cmd.args.tx_xdr)?,
Expand Down