Skip to content

Commit 71f2729

Browse files
authored
Add stellar tx clawback and stellar tx op add clawback. (#2200)
1 parent 381b923 commit 71f2729

7 files changed

Lines changed: 291 additions & 0 deletions

File tree

FULL_HELP_DOCS.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1709,6 +1709,7 @@ Create a new transaction
17091709
* `bump-sequence` — Bump sequence number to invalidate older transactions
17101710
* `change-trust` — Create, update, or delete a trustline
17111711
* `claim-claimable-balance` — Claim a claimable balance by its balance ID
1712+
* `clawback` — Clawback an asset from an account
17121713
* `clawback-claimable-balance` — Clawback a claimable balance by its balance ID
17131714
* `create-account` — Create and fund a new account
17141715
* `create-claimable-balance` — Create a claimable balance that can be claimed by specified accounts
@@ -1843,6 +1844,37 @@ Claim a claimable balance by its balance ID
18431844

18441845

18451846

1847+
## `stellar tx new clawback`
1848+
1849+
Clawback an asset from an account
1850+
1851+
**Usage:** `stellar tx new clawback [OPTIONS] --source-account <SOURCE_ACCOUNT> --from <FROM> --asset <ASSET> --amount <AMOUNT>`
1852+
1853+
###### **Options:**
1854+
1855+
* `--fee <FEE>` — fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm
1856+
1857+
Default value: `100`
1858+
* `--cost` — Output the cost execution to stderr
1859+
* `--instructions <INSTRUCTIONS>` — Number of instructions to simulate
1860+
* `--build-only` — Build the transaction and only write the base64 xdr to stdout
1861+
* `--rpc-url <RPC_URL>` — RPC server endpoint
1862+
* `--rpc-header <RPC_HEADERS>` — RPC Header(s) to include in requests to the RPC provider
1863+
* `--network-passphrase <NETWORK_PASSPHRASE>` — Network passphrase to sign the transaction sent to the rpc server
1864+
* `-n`, `--network <NETWORK>` — Name of network to use from config
1865+
* `-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
1866+
* `--global` — ⚠️ Deprecated: global config is always on
1867+
* `--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
1868+
* `--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
1869+
* `--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`
1870+
* `--sign-with-lab` — Sign with https://lab.stellar.org
1871+
* `--sign-with-ledger` — Sign with a ledger wallet
1872+
* `--from <FROM>` — Account to clawback assets from, e.g. `GBX...`
1873+
* `--asset <ASSET>` — Asset to clawback
1874+
* `--amount <AMOUNT>` — Amount of the asset to clawback, in stroops. 1 stroop = 0.0000001 of the asset
1875+
1876+
1877+
18461878
## `stellar tx new clawback-claimable-balance`
18471879

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

24352468

24362469

2470+
## `stellar tx operation add clawback`
2471+
2472+
Clawback an asset from an account
2473+
2474+
**Usage:** `stellar tx operation add clawback [OPTIONS] --source-account <SOURCE_ACCOUNT> --from <FROM> --asset <ASSET> --amount <AMOUNT> [TX_XDR]`
2475+
2476+
###### **Arguments:**
2477+
2478+
* `<TX_XDR>` — Base-64 transaction envelope XDR or file containing XDR to decode, or stdin if empty
2479+
2480+
###### **Options:**
2481+
2482+
* `--operation-source-account <OPERATION_SOURCE_ACCOUNT>` [alias: `op-source`] — Source account used for the operation
2483+
* `--fee <FEE>` — fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm
2484+
2485+
Default value: `100`
2486+
* `--cost` — Output the cost execution to stderr
2487+
* `--instructions <INSTRUCTIONS>` — Number of instructions to simulate
2488+
* `--build-only` — Build the transaction and only write the base64 xdr to stdout
2489+
* `--rpc-url <RPC_URL>` — RPC server endpoint
2490+
* `--rpc-header <RPC_HEADERS>` — RPC Header(s) to include in requests to the RPC provider
2491+
* `--network-passphrase <NETWORK_PASSPHRASE>` — Network passphrase to sign the transaction sent to the rpc server
2492+
* `-n`, `--network <NETWORK>` — Name of network to use from config
2493+
* `-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
2494+
* `--global` — ⚠️ Deprecated: global config is always on
2495+
* `--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
2496+
* `--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
2497+
* `--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`
2498+
* `--sign-with-lab` — Sign with https://lab.stellar.org
2499+
* `--sign-with-ledger` — Sign with a ledger wallet
2500+
* `--from <FROM>` — Account to clawback assets from, e.g. `GBX...`
2501+
* `--asset <ASSET>` — Asset to clawback
2502+
* `--amount <AMOUNT>` — Amount of the asset to clawback, in stroops. 1 stroop = 0.0000001 of the asset
2503+
2504+
2505+
24372506
## `stellar tx operation add clawback-claimable-balance`
24382507

24392508
Clawback a claimable balance by its balance ID

cmd/crates/soroban-test/tests/it/integration/tx/operations.rs

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1429,3 +1429,154 @@ async fn clawback_claimable_balance() {
14291429
.assert()
14301430
.failure(); // This should fail because the balance was clawed back
14311431
}
1432+
1433+
#[tokio::test]
1434+
async fn clawback() {
1435+
let sandbox = &TestEnv::new();
1436+
let (test, issuer) = setup_accounts(sandbox);
1437+
1438+
// Enable revocable flag first, then clawback on the issuer account
1439+
sandbox
1440+
.new_assert_cmd("tx")
1441+
.args(["new", "set-options", "--set-revocable", "--source", "test1"])
1442+
.assert()
1443+
.success();
1444+
1445+
sandbox
1446+
.new_assert_cmd("tx")
1447+
.args([
1448+
"new",
1449+
"set-options",
1450+
"--set-clawback-enabled",
1451+
"--source",
1452+
"test1",
1453+
])
1454+
.assert()
1455+
.success();
1456+
1457+
// Create asset for clawback test
1458+
let asset = format!("USDC:{issuer}");
1459+
let limit = 100_000_000_000;
1460+
let initial_balance = 50_000_000_000;
1461+
issue_asset(sandbox, &test, &asset, limit, initial_balance).await;
1462+
1463+
// Create holder account for clawback
1464+
let holder = new_account(sandbox, "holder");
1465+
1466+
// Setup trustline for holder
1467+
sandbox
1468+
.new_assert_cmd("tx")
1469+
.args([
1470+
"new",
1471+
"change-trust",
1472+
"--source",
1473+
"holder",
1474+
"--line",
1475+
&asset,
1476+
])
1477+
.assert()
1478+
.success();
1479+
1480+
// Authorize holder's trustline and enable clawback
1481+
sandbox
1482+
.new_assert_cmd("tx")
1483+
.args([
1484+
"new",
1485+
"set-trustline-flags",
1486+
"--asset",
1487+
&asset,
1488+
"--trustor",
1489+
&holder,
1490+
"--set-authorize",
1491+
"--source",
1492+
"test1",
1493+
])
1494+
.assert()
1495+
.success();
1496+
1497+
// Send some assets to the holder account
1498+
let payment_amount = "10000000000"; // 1000 USDC
1499+
sandbox
1500+
.new_assert_cmd("tx")
1501+
.args([
1502+
"new",
1503+
"payment",
1504+
"--destination",
1505+
&holder,
1506+
"--asset",
1507+
&asset,
1508+
"--amount",
1509+
payment_amount,
1510+
"--source",
1511+
"test1",
1512+
])
1513+
.assert()
1514+
.success();
1515+
1516+
// Test clawback command
1517+
// this should succeed for the issuer
1518+
let clawback_amount = "5000000000"; // 500 USDC
1519+
sandbox
1520+
.new_assert_cmd("tx")
1521+
.args([
1522+
"new",
1523+
"clawback",
1524+
"--from",
1525+
&holder,
1526+
"--asset",
1527+
&asset,
1528+
"--amount",
1529+
clawback_amount,
1530+
"--source",
1531+
"test1", // issuer should be able to clawback
1532+
])
1533+
.assert()
1534+
.success();
1535+
1536+
// Verify holder's balance after clawback (should be 500 USDC: 1000 sent - 500 clawed back)
1537+
let horizon_url = format!("http://localhost:8000/accounts/{}", holder);
1538+
let response = reqwest::get(&horizon_url)
1539+
.await
1540+
.expect("Failed to fetch account from Horizon");
1541+
let json: serde_json::Value = response
1542+
.json()
1543+
.await
1544+
.expect("Failed to parse Horizon response");
1545+
1546+
let final_balance = json["balances"]
1547+
.as_array()
1548+
.unwrap()
1549+
.iter()
1550+
.find(|balance| {
1551+
balance["asset_code"].as_str() == Some("USDC")
1552+
&& balance["asset_issuer"].as_str() == Some(&issuer)
1553+
})
1554+
.expect("USDC balance not found after clawback")["balance"]
1555+
.as_str()
1556+
.unwrap()
1557+
.parse::<f64>()
1558+
.unwrap();
1559+
1560+
assert_eq!(
1561+
final_balance, 500.0,
1562+
"Holder should have 500 USDC remaining after clawback (1000 sent - 500 clawed back)"
1563+
);
1564+
1565+
// Verify that a non-issuer cannot perform clawback
1566+
sandbox
1567+
.new_assert_cmd("tx")
1568+
.args([
1569+
"new",
1570+
"clawback",
1571+
"--from",
1572+
&holder,
1573+
"--asset",
1574+
&asset,
1575+
"--amount",
1576+
"1000000000", // 100 USDC
1577+
"--source",
1578+
"holder", // non-issuer should not be able to clawback
1579+
])
1580+
.assert()
1581+
.failure();
1582+
}

cmd/soroban-cli/src/commands/tx/help.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ pub const ACCOUNT_MERGE: &str = "Transfer XLM balance to another account and rem
22
pub const BUMP_SEQUENCE: &str = "Bump sequence number to invalidate older transactions";
33
pub const CHANGE_TRUST: &str = "Create, update, or delete a trustline";
44
pub const CLAIM_CLAIMABLE_BALANCE: &str = "Claim a claimable balance by its balance ID";
5+
pub const CLAWBACK: &str = "Clawback an asset from an account";
56
pub const CLAWBACK_CLAIMABLE_BALANCE: &str = "Clawback a claimable balance by its balance ID";
67
pub const CREATE_ACCOUNT: &str = "Create and fund a new account";
78
pub const CREATE_CLAIMABLE_BALANCE: &str =
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
use clap::Parser;
2+
3+
use crate::{commands::tx, config::address, tx::builder, xdr};
4+
5+
#[derive(Parser, Debug, Clone)]
6+
#[group(skip)]
7+
pub struct Cmd {
8+
#[command(flatten)]
9+
pub tx: tx::Args,
10+
#[clap(flatten)]
11+
pub op: Args,
12+
}
13+
14+
#[derive(Debug, clap::Args, Clone)]
15+
pub struct Args {
16+
/// Account to clawback assets from, e.g. `GBX...`
17+
#[arg(long)]
18+
pub from: address::UnresolvedMuxedAccount,
19+
/// Asset to clawback
20+
#[arg(long)]
21+
pub asset: builder::Asset,
22+
/// Amount of the asset to clawback, in stroops. 1 stroop = 0.0000001 of the asset
23+
#[arg(long)]
24+
pub amount: builder::Amount,
25+
}
26+
27+
impl TryFrom<&Cmd> for xdr::OperationBody {
28+
type Error = tx::args::Error;
29+
fn try_from(
30+
Cmd {
31+
tx,
32+
op:
33+
Args {
34+
from,
35+
asset,
36+
amount,
37+
},
38+
}: &Cmd,
39+
) -> Result<Self, Self::Error> {
40+
Ok(xdr::OperationBody::Clawback(xdr::ClawbackOp {
41+
from: tx.resolve_muxed_address(from)?,
42+
asset: tx.resolve_asset(asset)?,
43+
amount: amount.into(),
44+
}))
45+
}
46+
}

cmd/soroban-cli/src/commands/tx/new/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ pub mod account_merge;
77
pub mod bump_sequence;
88
pub mod change_trust;
99
pub mod claim_claimable_balance;
10+
pub mod clawback;
1011
pub mod clawback_claimable_balance;
1112
pub mod create_account;
1213
pub mod create_claimable_balance;
@@ -31,6 +32,8 @@ pub enum Cmd {
3132
ChangeTrust(change_trust::Cmd),
3233
#[command(about = super::help::CLAIM_CLAIMABLE_BALANCE)]
3334
ClaimClaimableBalance(claim_claimable_balance::Cmd),
35+
#[command(about = super::help::CLAWBACK)]
36+
Clawback(clawback::Cmd),
3437
#[command(about = super::help::CLAWBACK_CLAIMABLE_BALANCE)]
3538
ClawbackClaimableBalance(clawback_claimable_balance::Cmd),
3639
#[command(about = super::help::CREATE_ACCOUNT)]
@@ -71,6 +74,7 @@ impl TryFrom<&Cmd> for OperationBody {
7174
Cmd::BumpSequence(cmd) => cmd.into(),
7275
Cmd::ChangeTrust(cmd) => cmd.try_into()?,
7376
Cmd::ClaimClaimableBalance(cmd) => cmd.try_into()?,
77+
Cmd::Clawback(cmd) => cmd.try_into()?,
7478
Cmd::ClawbackClaimableBalance(cmd) => cmd.try_into()?,
7579
Cmd::CreateAccount(cmd) => cmd.try_into()?,
7680
Cmd::CreateClaimableBalance(cmd) => cmd.try_into()?,
@@ -95,6 +99,7 @@ impl Cmd {
9599
Cmd::BumpSequence(cmd) => cmd.tx.handle_and_print(op, global_args).await,
96100
Cmd::ChangeTrust(cmd) => cmd.tx.handle_and_print(op, global_args).await,
97101
Cmd::ClaimClaimableBalance(cmd) => cmd.tx.handle_and_print(op, global_args).await,
102+
Cmd::Clawback(cmd) => cmd.tx.handle_and_print(op, global_args).await,
98103
Cmd::ClawbackClaimableBalance(cmd) => cmd.tx.handle_and_print(op, global_args).await,
99104
Cmd::CreateAccount(cmd) => cmd.tx.handle_and_print(op, global_args).await,
100105
Cmd::CreateClaimableBalance(cmd) => cmd.tx.handle_and_print(op, global_args).await,
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
use crate::commands::tx::new::clawback;
2+
3+
#[derive(clap::Parser, Debug, Clone)]
4+
#[group(skip)]
5+
pub struct Cmd {
6+
#[command(flatten)]
7+
pub args: super::args::Args,
8+
#[command(flatten)]
9+
pub op: clawback::Cmd,
10+
}

cmd/soroban-cli/src/commands/tx/op/add/mod.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ mod args;
88
mod bump_sequence;
99
mod change_trust;
1010
mod claim_claimable_balance;
11+
mod clawback;
1112
mod clawback_claimable_balance;
1213
mod create_account;
1314
mod create_claimable_balance;
@@ -32,6 +33,8 @@ pub enum Cmd {
3233
ChangeTrust(change_trust::Cmd),
3334
#[command(about = help::CLAIM_CLAIMABLE_BALANCE)]
3435
ClaimClaimableBalance(claim_claimable_balance::Cmd),
36+
#[command(about = help::CLAWBACK)]
37+
Clawback(clawback::Cmd),
3538
#[command(about = help::CLAWBACK_CLAIMABLE_BALANCE)]
3639
ClawbackClaimableBalance(clawback_claimable_balance::Cmd),
3740
#[command(about = help::CREATE_ACCOUNT)]
@@ -78,6 +81,7 @@ impl TryFrom<&Cmd> for OperationBody {
7881
Cmd::BumpSequence(bump_sequence::Cmd { op, .. }) => op.into(),
7982
Cmd::ChangeTrust(change_trust::Cmd { op, .. }) => op.try_into()?,
8083
Cmd::ClaimClaimableBalance(claim_claimable_balance::Cmd { op, .. }) => op.try_into()?,
84+
Cmd::Clawback(clawback::Cmd { op, .. }) => op.try_into()?,
8185
Cmd::ClawbackClaimableBalance(clawback_claimable_balance::Cmd { op, .. }) => {
8286
op.try_into()?
8387
}
@@ -128,6 +132,11 @@ impl Cmd {
128132
tx_envelope_from_input(&cmd.args.tx_xdr)?,
129133
cmd.args.source(),
130134
),
135+
Cmd::Clawback(cmd) => cmd.op.tx.add_op(
136+
op,
137+
tx_envelope_from_input(&cmd.args.tx_xdr)?,
138+
cmd.args.source(),
139+
),
131140
Cmd::ClawbackClaimableBalance(cmd) => cmd.op.tx.add_op(
132141
op,
133142
tx_envelope_from_input(&cmd.args.tx_xdr)?,

0 commit comments

Comments
 (0)