diff --git a/FULL_HELP_DOCS.md b/FULL_HELP_DOCS.md index a8f19fd7b6..06bbc2c114 100644 --- a/FULL_HELP_DOCS.md +++ b/FULL_HELP_DOCS.md @@ -1724,6 +1724,7 @@ Create a new transaction * `path-payment-strict-send` — Send a payment with a different asset using path finding, specifying the send amount * `path-payment-strict-receive` — Send a payment with a different asset using path finding, specifying the receive amount * `payment` — Send asset to destination account +* `revoke-sponsorship` — Revoke sponsorship of a ledger entry or signer * `set-options` — Set account options like flags, signers, and home domain * `set-trustline-flags` — Configure authorization and trustline flags for an asset @@ -1933,7 +1934,7 @@ Clawback a claimable balance by its balance ID * `--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 -* `--balance-id ` — Balance ID of the claimable balance to clawback. Accepts multiple formats: - API format with type prefix (72 chars): 000000006f2179b31311fa8064760b48942c8e166702ba0b8fbe7358c4fd570421840461 - Direct hash format (64 chars): 6f2179b31311fa8064760b48942c8e166702ba0b8fbe7358c4fd570421840461 - StrKey format (base32): BAAMLBZI42AD52HKGIZOU7WFVZM6BPEJCLPL44QU2AT6TY3P57I5QDNYIA +* `--balance-id ` — Balance ID of the claimable balance to clawback. Accepts multiple formats: - API format with type prefix (72 chars): 000000006f2179b31311fa8064760b48942c8e166702ba0b8fbe7358c4fd570421840461 - Direct hash format (64 chars): 6f2179b31311fa8064760b48942c8e166702ba0b8fbe7358c4fd570421840461 - Address format (base32): BAAMLBZI42AD52HKGIZOU7WFVZM6BPEJCLPL44QU2AT6TY3P57I5QDNYIA @@ -2336,6 +2337,41 @@ Send asset to destination account +## `stellar tx new revoke-sponsorship` + +Revoke sponsorship of a ledger entry or signer + +**Usage:** `stellar tx new revoke-sponsorship [OPTIONS] --source-account --account-id ` + +###### **Options:** + +* `--fee ` — fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm + + Default value: `100` +* `--cost` — Output the cost execution to stderr +* `--instructions ` — Number of instructions to simulate +* `--build-only` — Build the transaction and only write the base64 xdr to stdout +* `--rpc-url ` — RPC server endpoint +* `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider +* `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server +* `-n`, `--network ` — Name of network to use from config +* `-s`, `--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 ` — 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 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 ` — 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 +* `--account-id ` — Account ID (required for all sponsorship types) +* `--asset ` — Asset for trustline sponsorship (format: CODE:ISSUER) +* `--data-name ` — Data name for data entry sponsorship +* `--offer-id ` — Offer ID for offer sponsorship +* `--liquidity-pool-id ` — Pool ID for liquidity pool sponsorship. Accepts multiple formats: - API format with type prefix (72 chars): 000000006f2179b31311fa8064760b48942c8e166702ba0b8fbe7358c4fd570421840461 - Direct hash format (64 chars): 6f2179b31311fa8064760b48942c8e166702ba0b8fbe7358c4fd570421840461 - Address format (base32): LAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +* `--claimable-balance-id ` — Claimable balance ID for claimable balance sponsorship. Accepts multiple formats: - API format with type prefix (72 chars): 000000006f2179b31311fa8064760b48942c8e166702ba0b8fbe7358c4fd570421840461 - Direct hash format (64 chars): 6f2179b31311fa8064760b48942c8e166702ba0b8fbe7358c4fd570421840461 - Address format (base32): BAAMLBZI42AD52HKGIZOU7WFVZM6BPEJCLPL44QU2AT6TY3P57I5QDNYIA +* `--signer-key ` — Signer key for signer sponsorship + + + ## `stellar tx new set-options` Set account options like flags, signers, and home domain @@ -2457,6 +2493,7 @@ Add Operation to a transaction * `path-payment-strict-receive` — Send a payment with a different asset using path finding, specifying the receive amount * `path-payment-strict-send` — Send a payment with a different asset using path finding, specifying the send amount * `payment` — Send asset to destination account +* `revoke-sponsorship` — Revoke sponsorship of a ledger entry or signer * `set-options` — Set account options like flags, signers, and home domain * `set-trustline-flags` — Configure authorization and trustline flags for an asset @@ -2701,7 +2738,7 @@ Clawback a claimable balance by its balance ID * `--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 -* `--balance-id ` — Balance ID of the claimable balance to clawback. Accepts multiple formats: - API format with type prefix (72 chars): 000000006f2179b31311fa8064760b48942c8e166702ba0b8fbe7358c4fd570421840461 - Direct hash format (64 chars): 6f2179b31311fa8064760b48942c8e166702ba0b8fbe7358c4fd570421840461 - StrKey format (base32): BAAMLBZI42AD52HKGIZOU7WFVZM6BPEJCLPL44QU2AT6TY3P57I5QDNYIA +* `--balance-id ` — Balance ID of the claimable balance to clawback. Accepts multiple formats: - API format with type prefix (72 chars): 000000006f2179b31311fa8064760b48942c8e166702ba0b8fbe7358c4fd570421840461 - Direct hash format (64 chars): 6f2179b31311fa8064760b48942c8e166702ba0b8fbe7358c4fd570421840461 - Address format (base32): BAAMLBZI42AD52HKGIZOU7WFVZM6BPEJCLPL44QU2AT6TY3P57I5QDNYIA @@ -3164,6 +3201,46 @@ Send asset to destination account +## `stellar tx operation add revoke-sponsorship` + +Revoke sponsorship of a ledger entry or signer + +**Usage:** `stellar tx operation add revoke-sponsorship [OPTIONS] --source-account --account-id [TX_XDR]` + +###### **Arguments:** + +* `` — Base-64 transaction envelope XDR or file containing XDR to decode, or stdin if empty + +###### **Options:** + +* `--operation-source-account ` [alias: `op-source`] — Source account used for the operation +* `--fee ` — fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm + + Default value: `100` +* `--cost` — Output the cost execution to stderr +* `--instructions ` — Number of instructions to simulate +* `--build-only` — Build the transaction and only write the base64 xdr to stdout +* `--rpc-url ` — RPC server endpoint +* `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider +* `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server +* `-n`, `--network ` — Name of network to use from config +* `-s`, `--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 ` — 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 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 ` — 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 +* `--account-id ` — Account ID (required for all sponsorship types) +* `--asset ` — Asset for trustline sponsorship (format: CODE:ISSUER) +* `--data-name ` — Data name for data entry sponsorship +* `--offer-id ` — Offer ID for offer sponsorship +* `--liquidity-pool-id ` — Pool ID for liquidity pool sponsorship. Accepts multiple formats: - API format with type prefix (72 chars): 000000006f2179b31311fa8064760b48942c8e166702ba0b8fbe7358c4fd570421840461 - Direct hash format (64 chars): 6f2179b31311fa8064760b48942c8e166702ba0b8fbe7358c4fd570421840461 - Address format (base32): LAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +* `--claimable-balance-id ` — Claimable balance ID for claimable balance sponsorship. Accepts multiple formats: - API format with type prefix (72 chars): 000000006f2179b31311fa8064760b48942c8e166702ba0b8fbe7358c4fd570421840461 - Direct hash format (64 chars): 6f2179b31311fa8064760b48942c8e166702ba0b8fbe7358c4fd570421840461 - Address format (base32): BAAMLBZI42AD52HKGIZOU7WFVZM6BPEJCLPL44QU2AT6TY3P57I5QDNYIA +* `--signer-key ` — Signer key for signer sponsorship + + + ## `stellar tx operation add set-options` Set account options like flags, signers, and home domain diff --git a/cmd/crates/soroban-spec-tools/src/lib.rs b/cmd/crates/soroban-spec-tools/src/lib.rs index 6a6683c409..50b0e6ac2e 100644 --- a/cmd/crates/soroban-spec-tools/src/lib.rs +++ b/cmd/crates/soroban-spec-tools/src/lib.rs @@ -848,9 +848,7 @@ pub fn from_json_primitives(v: &Value, t: &ScType) -> Result { .map_err(|_| Error::InvalidValue(Some(t.clone())))?, )), - (ScType::Address, Value::String(s)) => sc_address_from_json(s)?, - - (ScType::MuxedAddress, Value::String(s)) => sc_address_from_json(s)?, + (ScType::Address | ScType::MuxedAddress, Value::String(s)) => sc_address_from_json(s)?, // Bytes parsing (bytes @ ScType::BytesN(_), Value::Number(n)) => { diff --git a/cmd/crates/soroban-test/tests/it/integration/tx/operations.rs b/cmd/crates/soroban-test/tests/it/integration/tx/operations.rs index a35a7654ea..9534d2b619 100644 --- a/cmd/crates/soroban-test/tests/it/integration/tx/operations.rs +++ b/cmd/crates/soroban-test/tests/it/integration/tx/operations.rs @@ -13,6 +13,16 @@ use crate::integration::{ util::{deploy_contract, test_address, DeployOptions, HELLO_WORLD}, }; +fn get_sponsoring_count(account: &soroban_cli::xdr::AccountEntry) -> u32 { + match &account.ext { + soroban_cli::xdr::AccountEntryExt::V1(v1) => match &v1.ext { + soroban_cli::xdr::AccountEntryExtensionV1Ext::V2(v2) => v2.num_sponsoring, + _ => panic!("Account extension V1 should have V2 extension for sponsoring"), + }, + _ => panic!("Account should have V1 extension for sponsoring"), + } +} + fn new_account(sandbox: &TestEnv, name: &str) -> String { sandbox.generate_account(name, None).assert().success(); sandbox @@ -1683,3 +1693,677 @@ async fn begin_sponsoring_future_reserves() { "Sponsor account should have paid for the sponsored account reserves" ); } + +#[tokio::test] +async fn revoke_sponsorship_account() { + let sandbox = &TestEnv::new(); + let client = sandbox.network.rpc_client().unwrap(); + + // Create sponsor account (use test account as sponsor) + let _sponsor = test_address(sandbox); + + // Create a new account to sponsor (but don't fund it) + let sponsored_account = gen_account_no_fund(sandbox, "sponsored"); + + // Set up sponsorship first + let sponsor_tx = sandbox + .new_assert_cmd("tx") + .args([ + "new", + "begin-sponsoring-future-reserves", + "--source-account", + "test", + "--sponsored-id", + &sponsored_account, + "--fee", + "1000000", + "--build-only", + ]) + .assert() + .success() + .stdout_as_str(); + + // Add create account operation with sponsor as operation source + let create_account_tx = sandbox + .new_assert_cmd("tx") + .args([ + "op", + "add", + "create-account", + "--destination", + &sponsored_account, + "--starting-balance", + "50000000", + "--operation-source-account", + "test", + ]) + .write_stdin(sponsor_tx.as_bytes()) + .assert() + .success() + .stdout_as_str(); + + // Add end sponsoring future reserves operation + let complete_tx = sandbox + .new_assert_cmd("tx") + .args([ + "op", + "add", + "end-sponsoring-future-reserves", + "--operation-source-account", + "sponsored", + ]) + .write_stdin(create_account_tx.as_bytes()) + .assert() + .success() + .stdout_as_str(); + + // Sign with sponsor first + let sponsor_signed_tx = sandbox + .new_assert_cmd("tx") + .args(["sign", "--sign-with-key=test"]) + .write_stdin(complete_tx.as_bytes()) + .assert() + .success() + .stdout_as_str(); + + // Sign with sponsored account second + let fully_signed_tx = sandbox + .new_assert_cmd("tx") + .args(["sign", "--sign-with-key=sponsored"]) + .write_stdin(sponsor_signed_tx.as_bytes()) + .assert() + .success() + .stdout_as_str(); + + // Submit the sponsorship transaction + sandbox + .new_assert_cmd("tx") + .arg("send") + .write_stdin(fully_signed_tx.as_bytes()) + .assert() + .success(); + + // Verify the sponsored account exists and is sponsored + let sponsored_account_info = client.get_account(&sponsored_account).await.unwrap(); + assert_eq!(sponsored_account_info.balance, 50000000); + + // Check sponsor's sponsoring count before revoking + let sponsor_account_before = client.get_account(&test_address(sandbox)).await.unwrap(); + let num_sponsoring_before = get_sponsoring_count(&sponsor_account_before); + + // Now test revoke sponsorship for the account ledger entry + // The sponsor should be able to revoke sponsorship of the account + sandbox + .new_assert_cmd("tx") + .args([ + "new", + "revoke-sponsorship", + "--source-account", + "test", // sponsor account + "--account-id", + &sponsored_account, + ]) + .assert() + .success(); + + // Verify that the sponsorship was revoked by checking the sponsor's sponsoring count + let sponsor_account_after = client.get_account(&test_address(sandbox)).await.unwrap(); + let num_sponsoring_after = get_sponsoring_count(&sponsor_account_after); + + // The sponsor should have fewer sponsored entries after revoking sponsorship + assert!( + num_sponsoring_after < num_sponsoring_before, + "Sponsor should have fewer sponsored entries after revoking sponsorship. Before: {}, After: {}", + num_sponsoring_before, + num_sponsoring_after + ); +} + +#[tokio::test] +async fn revoke_sponsorship_trustline() { + let sandbox = &TestEnv::new(); + + let sponsored_account = new_account(sandbox, "sponsored"); + let _issuer_account = new_account(sandbox, "issuer"); + let asset = "USD:issuer".to_string(); + + let tx = sandbox + .new_assert_cmd("tx") + .args([ + "new", + "begin-sponsoring-future-reserves", + "--source-account", + "test", + "--sponsored-id", + &sponsored_account, + "--fee", + "1000000", + "--build-only", + ]) + .assert() + .success() + .stdout_as_str(); + + let tx = sandbox + .new_assert_cmd("tx") + .args([ + "op", + "add", + "change-trust", + "--operation-source-account", + "sponsored", + "--line", + &asset, + ]) + .write_stdin(tx.as_bytes()) + .assert() + .success() + .stdout_as_str(); + + let tx = sandbox + .new_assert_cmd("tx") + .args([ + "op", + "add", + "end-sponsoring-future-reserves", + "--operation-source-account", + "sponsored", + ]) + .write_stdin(tx.as_bytes()) + .assert() + .success() + .stdout_as_str(); + + let tx_signed = sandbox + .new_assert_cmd("tx") + .args(["sign", "--sign-with-key=test"]) + .write_stdin(tx.as_bytes()) + .assert() + .success() + .stdout_as_str(); + + let tx_signed = sandbox + .new_assert_cmd("tx") + .args(["sign", "--sign-with-key=sponsored"]) + .write_stdin(tx_signed.as_bytes()) + .assert() + .success() + .stdout_as_str(); + + sandbox + .new_assert_cmd("tx") + .arg("send") + .write_stdin(tx_signed.as_bytes()) + .assert() + .success(); + + // Check if trustline was created and sponsorship is working + let client = sandbox.network.rpc_client().unwrap(); + let sponsored_account_details = client.get_account(&sponsored_account).await.unwrap(); + println!( + "Sponsored account sub-entries: {}", + sponsored_account_details.num_sub_entries + ); + + let test_address = test_address(sandbox); + let sponsor_account_details = client.get_account(&test_address).await.unwrap(); + let sponsoring_count = get_sponsoring_count(&sponsor_account_details); + println!("Sponsor account sponsoring count: {}", sponsoring_count); + println!("Sponsored account address: {}", sponsored_account); + println!("Test/sponsor account address: {}", test_address); + println!("Asset: {}", asset); + + // Get current sponsoring count for comparison before revoke + let sponsoring_count_before = sponsoring_count; + + // Test revoke trustline sponsorship + sandbox + .new_assert_cmd("tx") + .args([ + "new", + "revoke-sponsorship", + "--source-account", + "test", + "--account-id", + &sponsored_account, + "--asset", + &asset, + ]) + .assert() + .success(); + + // Verify sponsorship was revoked by checking sponsoring count decreased + let account_details_after = client.get_account(&test_address).await.unwrap(); + let sponsoring_count_after = get_sponsoring_count(&account_details_after); + assert!( + sponsoring_count_after < sponsoring_count_before, + "Sponsor should have fewer sponsored entries after revoking sponsorship. Before: {}, After: {}", + sponsoring_count_before, + sponsoring_count_after + ); +} + +#[tokio::test] +async fn revoke_sponsorship_data() { + let sandbox = &TestEnv::new(); + + let sponsored_account = new_account(sandbox, "sponsored"); + let _issuer_account = new_account(sandbox, "issuer"); + let asset = "USD:issuer".to_string(); + + let tx = sandbox + .new_assert_cmd("tx") + .args([ + "new", + "begin-sponsoring-future-reserves", + "--source-account", + "test", + "--sponsored-id", + &sponsored_account, + "--fee", + "1000000", + "--build-only", + ]) + .assert() + .success() + .stdout_as_str(); + + let tx = sandbox + .new_assert_cmd("tx") + .args([ + "op", + "add", + "manage-data", + "--data-name", + "msg", + "--data-value", + "beefface", + "--operation-source-account", + "sponsored", + ]) + .write_stdin(tx.as_bytes()) + .assert() + .success() + .stdout_as_str(); + + let tx = sandbox + .new_assert_cmd("tx") + .args([ + "op", + "add", + "end-sponsoring-future-reserves", + "--operation-source-account", + "sponsored", + ]) + .write_stdin(tx.as_bytes()) + .assert() + .success() + .stdout_as_str(); + + let tx_signed = sandbox + .new_assert_cmd("tx") + .args(["sign", "--sign-with-key=test"]) + .write_stdin(tx.as_bytes()) + .assert() + .success() + .stdout_as_str(); + + let tx_signed = sandbox + .new_assert_cmd("tx") + .args(["sign", "--sign-with-key=sponsored"]) + .write_stdin(tx_signed.as_bytes()) + .assert() + .success() + .stdout_as_str(); + + sandbox + .new_assert_cmd("tx") + .arg("send") + .write_stdin(tx_signed.as_bytes()) + .assert() + .success(); + + let client = sandbox.network.rpc_client().unwrap(); + let sponsored_account_details = client.get_account(&sponsored_account).await.unwrap(); + println!( + "Sponsored account sub-entries: {}", + sponsored_account_details.num_sub_entries + ); + + let test_address = test_address(sandbox); + let sponsor_account_details = client.get_account(&test_address).await.unwrap(); + let sponsoring_count = get_sponsoring_count(&sponsor_account_details); + println!("Sponsor account sponsoring count: {}", sponsoring_count); + println!("Sponsored account address: {}", sponsored_account); + println!("Test/sponsor account address: {}", test_address); + println!("Asset: {}", asset); + + // Get current sponsoring count for comparison before revoke + let sponsoring_count_before = sponsoring_count; + + // Test revoke manage data sponsorship + sandbox + .new_assert_cmd("tx") + .args([ + "new", + "revoke-sponsorship", + "--source-account", + "test", + "--account-id", + &sponsored_account, + "--data-name", + "msg", + ]) + .assert() + .success(); + + // Verify sponsorship was revoked by checking sponsoring count decreased + let account_details_after = client.get_account(&test_address).await.unwrap(); + let sponsoring_count_after = get_sponsoring_count(&account_details_after); + assert!( + sponsoring_count_after < sponsoring_count_before, + "Sponsor should have fewer sponsored entries after revoking sponsorship. Before: {}, After: {}", + sponsoring_count_before, + sponsoring_count_after + ); +} + +#[tokio::test] +async fn revoke_sponsorship_signer() { + let sandbox = &TestEnv::new(); + + let sponsored_account = new_account(sandbox, "sponsored"); + + // Generate a new signer account without funding + let signer_account = gen_account_no_fund(sandbox, "signer"); + + let tx = sandbox + .new_assert_cmd("tx") + .args([ + "new", + "begin-sponsoring-future-reserves", + "--source-account", + "test", + "--sponsored-id", + &sponsored_account, + "--fee", + "1000000", + "--build-only", + ]) + .assert() + .success() + .stdout_as_str(); + + let tx = sandbox + .new_assert_cmd("tx") + .args([ + "op", + "add", + "set-options", + "--signer", + &signer_account, + "--signer-weight", + "1", + "--operation-source-account", + "sponsored", + ]) + .write_stdin(tx.as_bytes()) + .assert() + .success() + .stdout_as_str(); + + let tx = sandbox + .new_assert_cmd("tx") + .args([ + "op", + "add", + "end-sponsoring-future-reserves", + "--operation-source-account", + "sponsored", + ]) + .write_stdin(tx.as_bytes()) + .assert() + .success() + .stdout_as_str(); + + let tx_signed = sandbox + .new_assert_cmd("tx") + .args(["sign", "--sign-with-key=test"]) + .write_stdin(tx.as_bytes()) + .assert() + .success() + .stdout_as_str(); + + let tx_signed = sandbox + .new_assert_cmd("tx") + .args(["sign", "--sign-with-key=sponsored"]) + .write_stdin(tx_signed.as_bytes()) + .assert() + .success() + .stdout_as_str(); + + sandbox + .new_assert_cmd("tx") + .arg("send") + .write_stdin(tx_signed.as_bytes()) + .assert() + .success(); + + let client = sandbox.network.rpc_client().unwrap(); + let sponsored_account_details = client.get_account(&sponsored_account).await.unwrap(); + println!( + "Sponsored account sub-entries: {}", + sponsored_account_details.num_sub_entries + ); + + let test_address = test_address(sandbox); + let sponsor_account_details = client.get_account(&test_address).await.unwrap(); + let sponsoring_count = get_sponsoring_count(&sponsor_account_details); + println!("Sponsor account sponsoring count: {}", sponsoring_count); + println!("Sponsored account address: {}", sponsored_account); + println!("Test/sponsor account address: {}", test_address); + println!("Signer account: {}", signer_account); + + // Get current sponsoring count for comparison before revoke + let sponsoring_count_before = sponsoring_count; + + // Test revoke signer sponsorship + sandbox + .new_assert_cmd("tx") + .args([ + "new", + "revoke-sponsorship", + "--source-account", + "test", + "--account-id", + &sponsored_account, + "--signer-key", + &signer_account, + ]) + .assert() + .success(); + + // Verify sponsorship was revoked by checking sponsoring count decreased + let account_details_after = client.get_account(&test_address).await.unwrap(); + let sponsoring_count_after = get_sponsoring_count(&account_details_after); + assert!( + sponsoring_count_after < sponsoring_count_before, + "Sponsor should have fewer sponsored entries after revoking sponsorship. Before: {}, After: {}", + sponsoring_count_before, + sponsoring_count_after + ); +} + +#[tokio::test] +async fn revoke_sponsorship_offer() { + let sandbox = &TestEnv::new(); + + let sponsored_account = new_account(sandbox, "sponsored"); + let _issuer_account = new_account(sandbox, "issuer"); + let selling_asset = "USD:issuer".to_string(); + + // First create a trustline for the selling asset + sandbox + .new_assert_cmd("tx") + .args([ + "new", + "change-trust", + "--source-account", + "sponsored", + "--line", + &selling_asset, + ]) + .assert() + .success(); + + // Fund the sponsored account with some USD tokens to sell + sandbox + .new_assert_cmd("tx") + .args([ + "new", + "payment", + "--source-account", + "issuer", + "--destination", + "sponsored", + "--asset", + &selling_asset, + "--amount", + "1000", + ]) + .assert() + .success(); + + let tx = sandbox + .new_assert_cmd("tx") + .args([ + "new", + "begin-sponsoring-future-reserves", + "--source-account", + "test", + "--sponsored-id", + &sponsored_account, + "--fee", + "1000000", + "--build-only", + ]) + .assert() + .success() + .stdout_as_str(); + + let tx = sandbox + .new_assert_cmd("tx") + .args([ + "op", + "add", + "manage-sell-offer", + "--selling", + &selling_asset, + "--buying", + "native", + "--amount", + "100", + "--price", + "1:1", + "--operation-source-account", + "sponsored", + ]) + .write_stdin(tx.as_bytes()) + .assert() + .success() + .stdout_as_str(); + + let tx = sandbox + .new_assert_cmd("tx") + .args([ + "op", + "add", + "end-sponsoring-future-reserves", + "--operation-source-account", + "sponsored", + ]) + .write_stdin(tx.as_bytes()) + .assert() + .success() + .stdout_as_str(); + + let tx_signed = sandbox + .new_assert_cmd("tx") + .args(["sign", "--sign-with-key=test"]) + .write_stdin(tx.as_bytes()) + .assert() + .success() + .stdout_as_str(); + + let tx_signed = sandbox + .new_assert_cmd("tx") + .args(["sign", "--sign-with-key=sponsored"]) + .write_stdin(tx_signed.as_bytes()) + .assert() + .success() + .stdout_as_str(); + + sandbox + .new_assert_cmd("tx") + .arg("send") + .write_stdin(tx_signed.as_bytes()) + .assert() + .success(); + + let client = sandbox.network.rpc_client().unwrap(); + let sponsored_account_details = client.get_account(&sponsored_account).await.unwrap(); + println!( + "Sponsored account sub-entries: {}", + sponsored_account_details.num_sub_entries + ); + + let test_address = test_address(sandbox); + let sponsor_account_details = client.get_account(&test_address).await.unwrap(); + let sponsoring_count = get_sponsoring_count(&sponsor_account_details); + println!("Sponsor account sponsoring count: {}", sponsoring_count); + println!("Sponsored account address: {}", sponsored_account); + println!("Test/sponsor account address: {}", test_address); + + // Get current sponsoring count for comparison before revoke + let sponsoring_count_before = sponsoring_count; + + // Test revoke offer sponsorship - we need the offer ID + // Fetch the actual offer ID from Horizon + let horizon_url = format!( + "http://localhost:8000/accounts/{}/offers", + sponsored_account + ); + let response = reqwest::get(&horizon_url).await.unwrap(); + let json: serde_json::Value = response.json().await.unwrap(); + let offers = &json["_embedded"]["records"]; + assert!( + !offers.as_array().unwrap().is_empty(), + "No offers found for sponsored account" + ); + let offer_id = offers[0]["id"].as_str().unwrap(); + + sandbox + .new_assert_cmd("tx") + .args([ + "new", + "revoke-sponsorship", + "--source-account", + "test", + "--account-id", + &sponsored_account, + "--offer-id", + offer_id, + ]) + .assert() + .success(); + + // Verify sponsorship was revoked by checking sponsoring count decreased + let account_details_after = client.get_account(&test_address).await.unwrap(); + let sponsoring_count_after = get_sponsoring_count(&account_details_after); + assert!( + sponsoring_count_after < sponsoring_count_before, + "Sponsor should have fewer sponsored entries after revoking sponsorship. Before: {}, After: {}", + sponsoring_count_before, + sponsoring_count_after + ); +} diff --git a/cmd/soroban-cli/src/commands/tx/help.rs b/cmd/soroban-cli/src/commands/tx/help.rs index 002dbc9e03..21f7609180 100644 --- a/cmd/soroban-cli/src/commands/tx/help.rs +++ b/cmd/soroban-cli/src/commands/tx/help.rs @@ -23,3 +23,4 @@ pub const SET_TRUSTLINE_FLAGS: &str = "Configure authorization and trustline fla pub const BEGIN_SPONSORING_FUTURE_RESERVES: &str = "Begin sponsoring future reserves for another account"; pub const END_SPONSORING_FUTURE_RESERVES: &str = "End sponsoring future reserves"; +pub const REVOKE_SPONSORSHIP: &str = "Revoke sponsorship of a ledger entry or signer"; diff --git a/cmd/soroban-cli/src/commands/tx/new/clawback_claimable_balance.rs b/cmd/soroban-cli/src/commands/tx/new/clawback_claimable_balance.rs index 6c06d2d725..d76215c2b2 100644 --- a/cmd/soroban-cli/src/commands/tx/new/clawback_claimable_balance.rs +++ b/cmd/soroban-cli/src/commands/tx/new/clawback_claimable_balance.rs @@ -17,7 +17,7 @@ pub struct Args { /// Balance ID of the claimable balance to clawback. Accepts multiple formats: /// - API format with type prefix (72 chars): 000000006f2179b31311fa8064760b48942c8e166702ba0b8fbe7358c4fd570421840461 /// - Direct hash format (64 chars): 6f2179b31311fa8064760b48942c8e166702ba0b8fbe7358c4fd570421840461 - /// - StrKey format (base32): BAAMLBZI42AD52HKGIZOU7WFVZM6BPEJCLPL44QU2AT6TY3P57I5QDNYIA + /// - Address format (base32): BAAMLBZI42AD52HKGIZOU7WFVZM6BPEJCLPL44QU2AT6TY3P57I5QDNYIA #[arg(long)] pub balance_id: String, } @@ -46,14 +46,14 @@ impl TryFrom<&Cmd> for xdr::OperationBody { } } -fn parse_balance_id(balance_id: &str) -> Result, tx::args::Error> { +pub fn parse_balance_id(balance_id: &str) -> Result, tx::args::Error> { // Handle multiple formats: - // 1. StrKey format (base32): BAAMLBZI42AD52HKGIZOU7WFVZM6BPEJCLPL44QU2AT6TY3P57I5QDNYIA + // 1. Address format (base32): BAAMLBZI42AD52HKGIZOU7WFVZM6BPEJCLPL44QU2AT6TY3P57I5QDNYIA // 2. API format with type prefix (72 hex chars): 000000006f2179b3... // 3. Direct hash format (64 hex chars): 6f2179b3... if balance_id.starts_with('B') && balance_id.len() > 50 { - // StrKey format - use stellar-strkey crate to decode claimable balance address + // Address format - use stellar-strkey crate to decode claimable balance address match stellar_strkey::Strkey::from_string(balance_id) { Ok(stellar_strkey::Strkey::ClaimableBalance(stellar_strkey::ClaimableBalance::V0( bytes, @@ -64,9 +64,7 @@ fn parse_balance_id(balance_id: &str) -> Result, tx::args::Error> { }), } } else { - // Hex format - handle both API format (72 chars) and direct hash (64 chars) let cleaned_balance_id = if balance_id.len() == 72 && balance_id.starts_with("00000000") { - // Remove the 8-character type prefix (00000000 for ClaimableBalanceIdTypeV0) &balance_id[8..] } else { balance_id diff --git a/cmd/soroban-cli/src/commands/tx/new/mod.rs b/cmd/soroban-cli/src/commands/tx/new/mod.rs index 2b01ec5e2b..7126bd2762 100644 --- a/cmd/soroban-cli/src/commands/tx/new/mod.rs +++ b/cmd/soroban-cli/src/commands/tx/new/mod.rs @@ -22,6 +22,7 @@ pub mod manage_sell_offer; pub mod path_payment_strict_receive; pub mod path_payment_strict_send; pub mod payment; +pub mod revoke_sponsorship; pub mod set_options; pub mod set_trustline_flags; @@ -66,6 +67,8 @@ pub enum Cmd { PathPaymentStrictReceive(path_payment_strict_receive::Cmd), #[command(about = super::help::PAYMENT)] Payment(payment::Cmd), + #[command(about = super::help::REVOKE_SPONSORSHIP)] + RevokeSponsorship(revoke_sponsorship::Cmd), #[command(about = super::help::SET_OPTIONS)] SetOptions(set_options::Cmd), #[command(about = super::help::SET_TRUSTLINE_FLAGS)] @@ -101,6 +104,7 @@ impl TryFrom<&Cmd> for OperationBody { Cmd::PathPaymentStrictSend(cmd) => cmd.try_into()?, Cmd::PathPaymentStrictReceive(cmd) => cmd.try_into()?, Cmd::Payment(cmd) => cmd.try_into()?, + Cmd::RevokeSponsorship(cmd) => cmd.try_into()?, Cmd::SetOptions(cmd) => cmd.try_into()?, Cmd::SetTrustlineFlags(cmd) => cmd.try_into()?, }) @@ -132,6 +136,7 @@ impl Cmd { Cmd::PathPaymentStrictSend(cmd) => cmd.tx.handle_and_print(op, global_args).await, Cmd::PathPaymentStrictReceive(cmd) => cmd.tx.handle_and_print(op, global_args).await, Cmd::Payment(cmd) => cmd.tx.handle_and_print(op, global_args).await, + Cmd::RevokeSponsorship(cmd) => cmd.tx.handle_and_print(op, global_args).await, Cmd::SetOptions(cmd) => cmd.tx.handle_and_print(op, global_args).await, Cmd::SetTrustlineFlags(cmd) => cmd.tx.handle_and_print(op, global_args).await, }?; diff --git a/cmd/soroban-cli/src/commands/tx/new/revoke_sponsorship.rs b/cmd/soroban-cli/src/commands/tx/new/revoke_sponsorship.rs new file mode 100644 index 0000000000..7abf36a99f --- /dev/null +++ b/cmd/soroban-cli/src/commands/tx/new/revoke_sponsorship.rs @@ -0,0 +1,179 @@ +use clap::Parser; +use soroban_sdk::xdr; + +use super::clawback_claimable_balance::parse_balance_id; +use crate::{commands::tx, config::address, tx::builder}; + +#[derive(Parser, Debug, Clone)] +#[group(skip)] +pub struct Cmd { + #[command(flatten)] + pub tx: tx::args::Args, + + #[command(flatten)] + pub op: Args, +} + +#[derive(Debug, clap::Args, Clone)] +pub struct Args { + /// Account ID (required for all sponsorship types) + #[arg(long)] + pub account_id: address::UnresolvedMuxedAccount, + + /// Asset for trustline sponsorship (format: CODE:ISSUER) + #[arg(long, group = "sponsorship_type")] + pub asset: Option, + + /// Data name for data entry sponsorship + #[arg(long, group = "sponsorship_type")] + pub data_name: Option, + + /// Offer ID for offer sponsorship + #[arg(long, group = "sponsorship_type")] + pub offer_id: Option, + + /// Pool ID for liquidity pool sponsorship. Accepts multiple formats: + /// - API format with type prefix (72 chars): 000000006f2179b31311fa8064760b48942c8e166702ba0b8fbe7358c4fd570421840461 + /// - Direct hash format (64 chars): 6f2179b31311fa8064760b48942c8e166702ba0b8fbe7358c4fd570421840461 + /// - Address format (base32): LAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + #[arg(long, group = "sponsorship_type")] + pub liquidity_pool_id: Option, + + /// Claimable balance ID for claimable balance sponsorship. Accepts multiple formats: + /// - API format with type prefix (72 chars): 000000006f2179b31311fa8064760b48942c8e166702ba0b8fbe7358c4fd570421840461 + /// - Direct hash format (64 chars): 6f2179b31311fa8064760b48942c8e166702ba0b8fbe7358c4fd570421840461 + /// - Address format (base32): BAAMLBZI42AD52HKGIZOU7WFVZM6BPEJCLPL44QU2AT6TY3P57I5QDNYIA + #[arg(long, group = "sponsorship_type")] + pub claimable_balance_id: Option, + + /// Signer key for signer sponsorship + #[arg(long, group = "sponsorship_type")] + pub signer_key: Option, +} + +fn parse_liquidity_pool_id(pool_id: &str) -> Result, tx::args::Error> { + // Handle multiple formats: + // 1. Address format (base32): LAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + // 2. API format with type prefix (72 hex chars): 000000006f2179b3... + // 3. Direct hash format (64 hex chars): 6f2179b3... + + if pool_id.starts_with('L') && pool_id.len() > 50 { + match stellar_strkey::Strkey::from_string(pool_id) { + Ok(stellar_strkey::Strkey::LiquidityPool(pool)) => Ok(pool.0.to_vec()), + _ => Err(tx::args::Error::InvalidHex { + name: "liquidity_pool_id".to_string(), + hex: pool_id.to_string(), + }), + } + } else { + let cleaned_pool_id = if pool_id.len() == 72 && pool_id.starts_with("00000000") { + &pool_id[8..] + } else { + pool_id + }; + + let pool_id_bytes = + hex::decode(cleaned_pool_id).map_err(|_| tx::args::Error::InvalidHex { + name: "liquidity_pool_id".to_string(), + hex: pool_id.to_string(), + })?; + + if pool_id_bytes.len() != 32 { + return Err(tx::args::Error::InvalidHex { + name: "liquidity_pool_id".to_string(), + hex: pool_id.to_string(), + }); + } + + Ok(pool_id_bytes) + } +} + +impl TryFrom<&Cmd> for xdr::OperationBody { + type Error = tx::args::Error; + fn try_from(cmd: &Cmd) -> Result { + let account_id_key = cmd.tx.resolve_account_id(&cmd.op.account_id)?; + + let revoke_op = if let Some(signer_key) = &cmd.op.signer_key { + // Signer sponsorship + let resolved_account = cmd.tx.resolve_account_id(signer_key)?; + let signer_key = match resolved_account.0 { + xdr::PublicKey::PublicKeyTypeEd25519(uint256) => xdr::SignerKey::Ed25519(uint256), + }; + xdr::RevokeSponsorshipOp::Signer(xdr::RevokeSponsorshipOpSigner { + account_id: account_id_key, + signer_key, + }) + } else if let Some(asset) = &cmd.op.asset { + // Trustline sponsorship + let resolved_asset = cmd.tx.resolve_asset(asset)?; + let trustline_asset = match resolved_asset { + xdr::Asset::CreditAlphanum4(asset) => xdr::TrustLineAsset::CreditAlphanum4(asset), + xdr::Asset::CreditAlphanum12(asset) => xdr::TrustLineAsset::CreditAlphanum12(asset), + xdr::Asset::Native => xdr::TrustLineAsset::Native, + }; + let ledger_key = xdr::LedgerKey::Trustline(xdr::LedgerKeyTrustLine { + account_id: account_id_key, + asset: trustline_asset, + }); + xdr::RevokeSponsorshipOp::LedgerEntry(ledger_key) + } else if let Some(data_name) = &cmd.op.data_name { + // Data entry sponsorship + let data_name_xdr: xdr::StringM<64> = + data_name.parse().map_err(|_| tx::args::Error::InvalidHex { + name: "data_name".to_string(), + hex: "invalid data name".to_string(), + })?; + let ledger_key = xdr::LedgerKey::Data(xdr::LedgerKeyData { + account_id: account_id_key, + data_name: data_name_xdr.into(), + }); + xdr::RevokeSponsorshipOp::LedgerEntry(ledger_key) + } else if let Some(offer_id) = cmd.op.offer_id { + // Offer sponsorship + let ledger_key = xdr::LedgerKey::Offer(xdr::LedgerKeyOffer { + seller_id: account_id_key, + offer_id: offer_id + .try_into() + .map_err(|_| tx::args::Error::InvalidHex { + name: "offer_id".to_string(), + hex: "offer ID too large".to_string(), + })?, + }); + xdr::RevokeSponsorshipOp::LedgerEntry(ledger_key) + } else if let Some(claimable_balance_id) = &cmd.op.claimable_balance_id { + // Claimable balance sponsorship + let balance_id_bytes = parse_balance_id(claimable_balance_id)?; + let mut balance_id_array = [0u8; 32]; + balance_id_array.copy_from_slice(&balance_id_bytes); + let claimable_balance_id_xdr = + xdr::ClaimableBalanceId::ClaimableBalanceIdTypeV0(xdr::Hash(balance_id_array)); + let ledger_key = xdr::LedgerKey::ClaimableBalance(xdr::LedgerKeyClaimableBalance { + balance_id: claimable_balance_id_xdr, + }); + xdr::RevokeSponsorshipOp::LedgerEntry(ledger_key) + } else if let Some(liquidity_pool_id) = &cmd.op.liquidity_pool_id { + // Liquidity pool sponsorship + let pool_id_bytes = parse_liquidity_pool_id(liquidity_pool_id)?; + let pool_id_array: [u8; 32] = + pool_id_bytes + .try_into() + .map_err(|_| tx::args::Error::InvalidHex { + name: "liquidity_pool_id".to_string(), + hex: "must be 32 bytes".to_string(), + })?; + let ledger_key = xdr::LedgerKey::LiquidityPool(xdr::LedgerKeyLiquidityPool { + liquidity_pool_id: xdr::PoolId(xdr::Hash(pool_id_array)), + }); + xdr::RevokeSponsorshipOp::LedgerEntry(ledger_key) + } else { + // Account sponsorship (default when no other specific args provided) + let ledger_key = xdr::LedgerKey::Account(xdr::LedgerKeyAccount { + account_id: account_id_key, + }); + xdr::RevokeSponsorshipOp::LedgerEntry(ledger_key) + }; + + Ok(xdr::OperationBody::RevokeSponsorship(revoke_op)) + } +} diff --git a/cmd/soroban-cli/src/commands/tx/op/add/mod.rs b/cmd/soroban-cli/src/commands/tx/op/add/mod.rs index bf01171245..9b9ab66fe2 100644 --- a/cmd/soroban-cli/src/commands/tx/op/add/mod.rs +++ b/cmd/soroban-cli/src/commands/tx/op/add/mod.rs @@ -23,6 +23,7 @@ mod manage_sell_offer; mod path_payment_strict_receive; mod path_payment_strict_send; mod payment; +mod revoke_sponsorship; mod set_options; mod set_trustline_flags; @@ -67,6 +68,8 @@ pub enum Cmd { PathPaymentStrictSend(path_payment_strict_send::Cmd), #[command(about = help::PAYMENT)] Payment(payment::Cmd), + #[command(about = help::REVOKE_SPONSORSHIP)] + RevokeSponsorship(revoke_sponsorship::Cmd), #[command(about = help::SET_OPTIONS)] SetOptions(set_options::Cmd), #[command(about = help::SET_TRUSTLINE_FLAGS)] @@ -123,6 +126,7 @@ impl TryFrom<&Cmd> for OperationBody { op.try_into()? } Cmd::Payment(payment::Cmd { op, .. }) => op.try_into()?, + Cmd::RevokeSponsorship(revoke_sponsorship::Cmd { op, .. }) => op.try_into()?, Cmd::SetOptions(set_options::Cmd { op, .. }) => op.try_into()?, Cmd::SetTrustlineFlags(set_trustline_flags::Cmd { op, .. }) => op.try_into()?, }) @@ -229,6 +233,11 @@ impl Cmd { tx_envelope_from_input(&cmd.args.tx_xdr)?, cmd.args.source(), ), + Cmd::RevokeSponsorship(cmd) => cmd.op.tx.add_op( + op, + tx_envelope_from_input(&cmd.args.tx_xdr)?, + cmd.args.source(), + ), Cmd::SetOptions(cmd) => cmd.op.tx.add_op( op, tx_envelope_from_input(&cmd.args.tx_xdr)?, diff --git a/cmd/soroban-cli/src/commands/tx/op/add/revoke_sponsorship.rs b/cmd/soroban-cli/src/commands/tx/op/add/revoke_sponsorship.rs new file mode 100644 index 0000000000..75228d9252 --- /dev/null +++ b/cmd/soroban-cli/src/commands/tx/op/add/revoke_sponsorship.rs @@ -0,0 +1,8 @@ +#[derive(clap::Parser, Debug, Clone)] +#[group(skip)] +pub struct Cmd { + #[command(flatten)] + pub args: super::args::Args, + #[command(flatten)] + pub op: super::new::revoke_sponsorship::Cmd, +}