From 3bd662a601b06a113fea8abb950fc34a5f22409f Mon Sep 17 00:00:00 2001 From: guoyunfeng Date: Fri, 23 Oct 2026 02:24:21 +0800 Subject: [PATCH] fix(cli): support fee-bump transactions in tx hash (#2769) --- cmd/crates/soroban-test/tests/it/tx.rs | 25 ++++++++++++ cmd/soroban-cli/src/commands/tx/hash.rs | 52 +++++++++++++++++++++++-- 2 files changed, 74 insertions(+), 3 deletions(-) diff --git a/cmd/crates/soroban-test/tests/it/tx.rs b/cmd/crates/soroban-test/tests/it/tx.rs index c51ba464b1..99b29c3bfb 100644 --- a/cmd/crates/soroban-test/tests/it/tx.rs +++ b/cmd/crates/soroban-test/tests/it/tx.rs @@ -4,6 +4,9 @@ use soroban_test::{AssertExt, TestEnv}; const TX_ENVELOPE: &str = "AAAAAgAAAAAnEWb4nxMsQhdnS16MqBxwItF3X/JNRkfxu8eyEQyfegAAAGQAAAAAAAAAAQAAAAAAAAAAAAAAAQAAAAAAAAABAAAAABk++lrW4HlZwaWwYdYvJPCil1ibyI/VTH6WKda2sOC+AAAAAAAAAAAAAABkAAAAAAAAAAA="; const SECRET_KEY: &str = "SAKICEVQLYWGSOJS4WW7HZJWAHZVEEBS527LHK5V4MLJALYKICQCJXMW"; +// Fee-bump transaction envelope (regression test for #2769). +const FEE_BUMP_ENVELOPE: &str = "AAAABQAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAACcQAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGQAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="; + // `tx sign` only mixes the network passphrase into the transaction hash, so it // must not require an RPC URL (regression test for #2455). #[tokio::test] @@ -49,3 +52,25 @@ async fn tx_hash_requires_only_network_passphrase() { assert_eq!(hash.len(), 64, "expected a 32-byte hex hash, got: {hash}"); assert!(hash.chars().all(|c| c.is_ascii_hexdigit())); } + +// `tx hash` must also support fee-bump transaction envelopes (regression test for #2769). +#[tokio::test] +async fn tx_hash_supports_fee_bump_transaction() { + let sandbox = &TestEnv::new(); + + let output = sandbox + .new_assert_cmd("tx") + .args([ + "hash", + FEE_BUMP_ENVELOPE, + "--network-passphrase", + "specified manually", + ]) + .assert() + .success() + .stdout_as_str(); + + let hash = output.trim(); + assert_eq!(hash.len(), 64, "expected a 32-byte hex hash, got: {hash}"); + assert!(hash.chars().all(|c| c.is_ascii_hexdigit())); +} diff --git a/cmd/soroban-cli/src/commands/tx/hash.rs b/cmd/soroban-cli/src/commands/tx/hash.rs index a6282b5747..8044f8a52d 100644 --- a/cmd/soroban-cli/src/commands/tx/hash.rs +++ b/cmd/soroban-cli/src/commands/tx/hash.rs @@ -1,7 +1,7 @@ use hex; use std::ffi::OsString; -use crate::{commands::global, config::network, utils::transaction_hash}; +use crate::{commands::global, config::network, utils::transaction_env_hash}; #[derive(thiserror::Error, Debug)] pub enum Error { @@ -28,12 +28,58 @@ pub struct Cmd { impl Cmd { pub fn run(&self, global_args: &global::Args) -> Result<(), Error> { - let tx = super::xdr::unwrap_envelope_v1(super::xdr::tx_envelope_from_input(&self.tx_xdr)?)?; + let tx_env = super::xdr::tx_envelope_from_input(&self.tx_xdr)?; let network = &self.network.resolve(&global_args.locator, false)?; println!( "{}", - hex::encode(transaction_hash(&tx, &network.network_passphrase)?) + hex::encode(transaction_env_hash(&tx_env, &network.network_passphrase)?) ); Ok(()) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::xdr::{ + FeeBumpTransaction, FeeBumpTransactionEnvelope, FeeBumpTransactionExt, + FeeBumpTransactionInnerTx, MuxedAccount, SequenceNumber, Transaction, TransactionEnvelope, + TransactionExt, TransactionV1Envelope, Uint256, + }; + + #[test] + fn test_tx_hash_fee_bump() { + let v1_env = TransactionV1Envelope { + tx: Transaction { + source_account: MuxedAccount::Ed25519(Uint256([0u8; 32])), + fee: 100, + seq_num: SequenceNumber(1), + cond: crate::xdr::Preconditions::None, + memo: crate::xdr::Memo::None, + operations: [].try_into().unwrap(), + ext: TransactionExt::V0, + }, + signatures: [].try_into().unwrap(), + }; + + let fee_bump_env = FeeBumpTransactionEnvelope { + tx: FeeBumpTransaction { + fee_source: MuxedAccount::Ed25519(Uint256([1u8; 32])), + fee: 10000, + inner_tx: FeeBumpTransactionInnerTx::Tx(v1_env), + ext: FeeBumpTransactionExt::V0, + }, + signatures: [].try_into().unwrap(), + }; + + let tx_env = TransactionEnvelope::TxFeeBump(fee_bump_env); + let network_passphrase = "Test SDF Network ; September 2015"; + let hash_result = transaction_env_hash(&tx_env, network_passphrase); + assert!(hash_result.is_ok()); + let hash = hash_result.unwrap(); + assert_eq!(hash.len(), 32); + let hex_hash = hex::encode(hash); + assert_eq!(hex_hash.len(), 64); + assert!(hex_hash.chars().all(|c| c.is_ascii_hexdigit())); + } +}