diff --git a/FULL_HELP_DOCS.md b/FULL_HELP_DOCS.md index ee140f8e3b..28d9f5dde7 100644 --- a/FULL_HELP_DOCS.md +++ b/FULL_HELP_DOCS.md @@ -1575,6 +1575,7 @@ Sign, Simulate, and Send transactions ###### **Subcommands:** +* `update` — Update the transaction * `edit` — Edit a transaction envelope from stdin. This command respects the environment variables `STELLAR_EDITOR`, `EDITOR` and `VISUAL`, in that order * `hash` — Calculate the hash of a transaction envelope * `new` — Create a new transaction @@ -1585,6 +1586,47 @@ Sign, Simulate, and Send transactions +## `stellar tx update` + +Update the transaction + +**Usage:** `stellar tx update ` + +###### **Subcommands:** + +* `sequence-number` — Edit the sequence number on a transaction + + + +## `stellar tx update sequence-number` + +Edit the sequence number on a transaction + +**Usage:** `stellar tx update sequence-number ` + +###### **Subcommands:** + +* `next` — Fetch the source account's seq-num and increment for the given tx + + + +## `stellar tx update sequence-number next` + +Fetch the source account's seq-num and increment for the given tx + +**Usage:** `stellar tx update sequence-number next [OPTIONS]` + +###### **Options:** + +* `--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 +* `--global` — Use global config +* `--config-dir ` — Location of config directory, default is "." + + + ## `stellar tx edit` Edit a transaction envelope from stdin. This command respects the environment variables `STELLAR_EDITOR`, `EDITOR` and `VISUAL`, in that order. diff --git a/cmd/crates/soroban-test/tests/it/integration/custom_types.rs b/cmd/crates/soroban-test/tests/it/integration/custom_types.rs index f4c2be61b5..6ccec4fbb2 100644 --- a/cmd/crates/soroban-test/tests/it/integration/custom_types.rs +++ b/cmd/crates/soroban-test/tests/it/integration/custom_types.rs @@ -3,9 +3,9 @@ use serde_json::json; use soroban_cli::commands; use soroban_test::TestEnv; -use crate::integration::util::{deploy_custom, extend_contract}; - -use super::util::{invoke, invoke_with_roundtrip}; +use crate::integration::util::{ + deploy_custom, extend_contract, invoke, invoke_with_roundtrip, test_address, +}; fn invoke_custom(e: &TestEnv, id: &str, func: &str) -> assert_cmd::Command { let mut s = e.new_assert_cmd("contract"); @@ -241,7 +241,7 @@ async fn account_address(sandbox: &TestEnv, id: &str) { async fn account_address_with_alias(sandbox: &TestEnv, id: &str) { let res = invoke(sandbox, id, "addresse", &json!("test").to_string()).await; - let test = format!("\"{}\"", super::tx::operations::test_address(sandbox)); + let test = format!("\"{}\"", test_address(sandbox)); assert_eq!(test, res); } diff --git a/cmd/crates/soroban-test/tests/it/integration/tx.rs b/cmd/crates/soroban-test/tests/it/integration/tx.rs index 1863e7b2e7..6fc618e58f 100644 --- a/cmd/crates/soroban-test/tests/it/integration/tx.rs +++ b/cmd/crates/soroban-test/tests/it/integration/tx.rs @@ -2,7 +2,9 @@ use soroban_cli::assembled::simulate_and_assemble_transaction; use soroban_cli::xdr::{Limits, ReadXdr, TransactionEnvelope, WriteXdr}; use soroban_test::{AssertExt, TestEnv}; -use crate::integration::util::{deploy_contract, DeployKind, DeployOptions, HELLO_WORLD}; +use crate::integration::util::{ + deploy_contract, test_address, DeployKind, DeployOptions, HELLO_WORLD, +}; pub mod operations; @@ -60,6 +62,47 @@ async fn simulate() { ); } +fn test_tx_string(sandbox: &TestEnv) -> String { + sandbox + .new_assert_cmd("contract") + .arg("install") + .args([ + "--wasm", + HELLO_WORLD.path().as_os_str().to_str().unwrap(), + "--build-only", + ]) + .assert() + .success() + .stdout_as_str() +} + +#[tokio::test] +async fn sequence_number_next() { + let sandbox = &TestEnv::new(); + let tx_base64 = test_tx_string(sandbox); + let test = test_address(sandbox); + let client = sandbox.network.rpc_client().unwrap(); + let test_account = client.get_account(&test).await.unwrap(); + let test_account_seq_num = test_account.seq_num.as_ref(); + + let updated_tx = sandbox + .new_assert_cmd("tx") + .arg("update") + .arg("seq-num") + .arg("next") + .write_stdin(tx_base64.as_bytes()) + .assert() + .success() + .stdout_as_str(); + + let updated_tx_env = TransactionEnvelope::from_xdr_base64(&updated_tx, Limits::none()).unwrap(); + let tx = soroban_cli::commands::tx::xdr::unwrap_envelope_v1(updated_tx_env).unwrap(); + assert_eq!( + tx.seq_num, + soroban_cli::xdr::SequenceNumber(test_account_seq_num + 1) + ); +} + #[tokio::test] async fn txn_hash() { let sandbox = &TestEnv::new(); 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 880c8b1cf3..ff3f7aa60c 100644 --- a/cmd/crates/soroban-test/tests/it/integration/tx/operations.rs +++ b/cmd/crates/soroban-test/tests/it/integration/tx/operations.rs @@ -4,24 +4,15 @@ use soroban_cli::{ utils::contract_id_hash_from_asset, xdr::{self, ReadXdr, SequenceNumber}, }; + use soroban_rpc::LedgerEntryResult; use soroban_test::{AssertExt, TestEnv}; use crate::integration::{ hello_world::invoke_hello_world, - util::{deploy_contract, DeployOptions, HELLO_WORLD}, + util::{deploy_contract, test_address, DeployOptions, HELLO_WORLD}, }; -pub fn test_address(sandbox: &TestEnv) -> String { - sandbox - .new_assert_cmd("keys") - .arg("address") - .arg("test") - .assert() - .success() - .stdout_as_str() -} - fn new_account(sandbox: &TestEnv, name: &str) -> String { sandbox.generate_account(name, None).assert().success(); sandbox diff --git a/cmd/crates/soroban-test/tests/it/integration/util.rs b/cmd/crates/soroban-test/tests/it/integration/util.rs index 16f7b91987..cdb468898b 100644 --- a/cmd/crates/soroban-test/tests/it/integration/util.rs +++ b/cmd/crates/soroban-test/tests/it/integration/util.rs @@ -2,7 +2,7 @@ use soroban_cli::{ commands, xdr::{Limits, WriteXdr}, }; -use soroban_test::{TestEnv, Wasm}; +use soroban_test::{AssertExt, TestEnv, Wasm}; use std::fmt::Display; pub const HELLO_WORLD: &Wasm = &Wasm::Custom("test-wasms", "test_hello_world"); @@ -125,3 +125,13 @@ pub async fn extend(sandbox: &TestEnv, id: &str, value: Option<&str>) { .assert() .success(); } + +pub fn test_address(sandbox: &TestEnv) -> String { + sandbox + .new_assert_cmd("keys") + .arg("address") + .arg("test") + .assert() + .success() + .stdout_as_str() +} diff --git a/cmd/soroban-cli/src/commands/tx/mod.rs b/cmd/soroban-cli/src/commands/tx/mod.rs index 6862cdcd85..114ad5e77a 100644 --- a/cmd/soroban-cli/src/commands/tx/mod.rs +++ b/cmd/soroban-cli/src/commands/tx/mod.rs @@ -9,12 +9,16 @@ pub mod op; pub mod send; pub mod sign; pub mod simulate; +pub mod update; pub mod xdr; pub use args::Args; #[derive(Debug, clap::Subcommand)] pub enum Cmd { + /// Update the transaction + #[command(subcommand)] + Update(update::Cmd), /// Edit a transaction envelope from stdin. This command respects the environment variables /// `STELLAR_EDITOR`, `EDITOR` and `VISUAL`, in that order. /// @@ -61,6 +65,8 @@ pub enum Error { Args(#[from] args::Error), #[error(transparent)] Simulate(#[from] simulate::Error), + #[error(transparent)] + Update(#[from] update::Error), } impl Cmd { @@ -73,6 +79,7 @@ impl Cmd { Cmd::Send(cmd) => cmd.run(global_args).await?, Cmd::Sign(cmd) => cmd.run(global_args).await?, Cmd::Simulate(cmd) => cmd.run(global_args).await?, + Cmd::Update(cmd) => cmd.run(global_args).await?, }; Ok(()) } diff --git a/cmd/soroban-cli/src/commands/tx/update/mod.rs b/cmd/soroban-cli/src/commands/tx/update/mod.rs new file mode 100644 index 0000000000..65948969da --- /dev/null +++ b/cmd/soroban-cli/src/commands/tx/update/mod.rs @@ -0,0 +1,25 @@ +use super::global; + +pub mod sequence_number; + +#[derive(Debug, clap::Subcommand)] +pub enum Cmd { + /// Edit the sequence number on a transaction + #[command(subcommand, visible_alias = "seq-num")] + SequenceNumber(sequence_number::Cmd), +} + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error(transparent)] + SequenceNumber(#[from] sequence_number::Error), +} + +impl Cmd { + pub async fn run(&self, global_args: &global::Args) -> Result<(), Error> { + match self { + Cmd::SequenceNumber(cmd) => cmd.run(global_args).await?, + }; + Ok(()) + } +} diff --git a/cmd/soroban-cli/src/commands/tx/update/sequence_number/mod.rs b/cmd/soroban-cli/src/commands/tx/update/sequence_number/mod.rs new file mode 100644 index 0000000000..fee0395e76 --- /dev/null +++ b/cmd/soroban-cli/src/commands/tx/update/sequence_number/mod.rs @@ -0,0 +1,25 @@ +use super::global; + +mod next; + +#[derive(Debug, clap::Subcommand)] +pub enum Cmd { + /// Fetch the source account's seq-num and increment for the given tx + #[command()] + Next(next::Cmd), +} + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error(transparent)] + Next(#[from] next::Error), +} + +impl Cmd { + pub async fn run(&self, global_args: &global::Args) -> Result<(), Error> { + match self { + Cmd::Next(cmd) => cmd.run(global_args).await?, + }; + Ok(()) + } +} diff --git a/cmd/soroban-cli/src/commands/tx/update/sequence_number/next.rs b/cmd/soroban-cli/src/commands/tx/update/sequence_number/next.rs new file mode 100644 index 0000000000..b552037994 --- /dev/null +++ b/cmd/soroban-cli/src/commands/tx/update/sequence_number/next.rs @@ -0,0 +1,75 @@ +use stellar_xdr::curr::MuxedAccount; + +use crate::{ + commands::{ + global, + tx::xdr::{tx_envelope_from_input, Error as XdrParsingError}, + }, + config::{self, locator, network}, + xdr::{self, SequenceNumber, TransactionEnvelope, WriteXdr}, +}; + +#[derive(clap::Parser, Debug, Clone)] +pub struct Cmd { + #[command(flatten)] + pub network: network::Args, + #[command(flatten)] + pub locator: locator::Args, +} + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error(transparent)] + XdrStdin(#[from] XdrParsingError), + #[error(transparent)] + Xdr(#[from] xdr::Error), + #[error("V0 and fee bump transactions are not supported")] + Unsupported, + #[error(transparent)] + RpcClient(#[from] crate::rpc::Error), + #[error(transparent)] + Config(#[from] config::Error), + #[error(transparent)] + Network(#[from] config::network::Error), +} + +impl Cmd { + pub async fn run(&self, global_args: &global::Args) -> Result<(), Error> { + let mut tx = tx_envelope_from_input(&None)?; + self.update_tx_env(&mut tx, global_args).await?; + println!("{}", tx.to_xdr_base64(xdr::Limits::none())?); + Ok(()) + } + + pub async fn update_tx_env( + &self, + tx_env: &mut TransactionEnvelope, + _global: &global::Args, + ) -> Result<(), Error> { + match tx_env { + TransactionEnvelope::Tx(transaction_v1_envelope) => { + let tx_source_acct = &transaction_v1_envelope.tx.source_account; + let current_seq_num = self.current_seq_num(tx_source_acct).await?; + let next_seq_num = current_seq_num + 1; + transaction_v1_envelope.tx.seq_num = SequenceNumber(next_seq_num); + } + TransactionEnvelope::TxV0(_) | TransactionEnvelope::TxFeeBump(_) => { + return Err(Error::Unsupported); + } + }; + Ok(()) + } + + async fn current_seq_num(&self, tx_source_acct: &MuxedAccount) -> Result { + let network = &self.network.get(&self.locator)?; + let client = network.rpc_client()?; + client + .verify_network_passphrase(Some(&network.network_passphrase)) + .await?; + + let address = tx_source_acct.to_string(); + + let account = client.get_account(&address).await?; + Ok(*account.seq_num.as_ref()) + } +}