openzeppelin_relayer/domain/relayer/stellar/
xdr_utils.rs

1//! XDR utility functions for Stellar transaction processing.
2//!
3//! This module provides utilities for parsing, validating, and manipulating
4//! Stellar transaction XDR (External Data Representation) structures. It includes
5//! support for regular transactions, fee-bump transactions, and various transaction
6//! formats (V0, V1).
7
8use crate::models::StellarValidationError;
9use eyre::{eyre, Result};
10use soroban_rs::xdr::{
11    DecoratedSignature, FeeBumpTransaction, FeeBumpTransactionEnvelope, FeeBumpTransactionInnerTx,
12    Limits, MuxedAccount, Operation, OperationBody, ReadXdr, TransactionEnvelope,
13    TransactionV1Envelope, Uint256, VecM, WriteXdr,
14};
15use stellar_strkey::ed25519::{MuxedAccount as StrkeyMuxedAccount, PublicKey};
16
17/// Parse a transaction XDR string into a TransactionEnvelope
18pub fn parse_transaction_xdr(xdr: &str, expect_signed: bool) -> Result<TransactionEnvelope> {
19    let envelope = TransactionEnvelope::from_xdr_base64(xdr, Limits::none())
20        .map_err(|e| StellarValidationError::InvalidXdr(e.to_string()))?;
21
22    if expect_signed && !is_signed(&envelope) {
23        return Err(StellarValidationError::UnexpectedUnsignedXdr.into());
24    }
25
26    Ok(envelope)
27}
28
29/// Check if a transaction envelope is signed
30pub fn is_signed(envelope: &TransactionEnvelope) -> bool {
31    match envelope {
32        TransactionEnvelope::TxV0(e) => !e.signatures.is_empty(),
33        TransactionEnvelope::Tx(TransactionV1Envelope { signatures, .. }) => !signatures.is_empty(),
34        TransactionEnvelope::TxFeeBump(FeeBumpTransactionEnvelope { signatures, .. }) => {
35            !signatures.is_empty()
36        }
37    }
38}
39
40/// Check if a transaction envelope is a fee-bump transaction
41pub fn is_fee_bump(envelope: &TransactionEnvelope) -> bool {
42    matches!(envelope, TransactionEnvelope::TxFeeBump(_))
43}
44
45/// Extract the source account from a transaction envelope
46pub fn extract_source_account(envelope: &TransactionEnvelope) -> Result<String> {
47    let muxed_account = match envelope {
48        TransactionEnvelope::TxV0(e) => {
49            // For V0 transactions, the source account is Ed25519 only
50            let bytes: [u8; 32] = e.tx.source_account_ed25519.0;
51            let pk = PublicKey(bytes);
52            return Ok(pk.to_string());
53        }
54        TransactionEnvelope::Tx(TransactionV1Envelope { tx, .. }) => &tx.source_account,
55        TransactionEnvelope::TxFeeBump(FeeBumpTransactionEnvelope { tx, .. }) => &tx.fee_source,
56    };
57
58    muxed_account_to_string(muxed_account)
59}
60
61/// Validate that the source account of a transaction matches the expected account
62pub fn validate_source_account(envelope: &TransactionEnvelope, expected: &str) -> Result<()> {
63    let source = extract_source_account(envelope)?;
64    if source != expected {
65        return Err(eyre!(
66            "Source account mismatch: expected {}, got {}",
67            expected,
68            source
69        ));
70    }
71    Ok(())
72}
73
74/// Build a fee-bump transaction envelope
75pub fn build_fee_bump_envelope(
76    inner_envelope: TransactionEnvelope,
77    fee_source: &str,
78    max_fee: i64,
79) -> Result<TransactionEnvelope> {
80    // Validate that the inner transaction is signed
81    if !is_signed(&inner_envelope) {
82        return Err(eyre!("Inner transaction must be signed before fee-bumping"));
83    }
84
85    // Extract inner transaction source to ensure it's different from fee source
86    let inner_source = extract_source_account(&inner_envelope)?;
87    if inner_source == fee_source {
88        return Err(eyre!(
89            "Fee-bump source cannot be the same as inner transaction source"
90        ));
91    }
92
93    // Convert fee source to MuxedAccount
94    let fee_source_muxed = string_to_muxed_account(fee_source)?;
95
96    // Create the inner transaction wrapper
97    let inner_tx = match inner_envelope {
98        TransactionEnvelope::TxV0(v0_envelope) => {
99            // Convert V0 to V1 envelope for fee-bump
100            FeeBumpTransactionInnerTx::Tx(convert_v0_to_v1_envelope(v0_envelope))
101        }
102        TransactionEnvelope::Tx(e) => FeeBumpTransactionInnerTx::Tx(e),
103        TransactionEnvelope::TxFeeBump(_) => {
104            return Err(eyre!("Cannot fee-bump a fee-bump transaction"));
105        }
106    };
107
108    // Create the fee-bump transaction
109    let fee_bump_tx = FeeBumpTransaction {
110        fee_source: fee_source_muxed,
111        fee: max_fee,
112        inner_tx,
113        ext: soroban_rs::xdr::FeeBumpTransactionExt::V0,
114    };
115
116    // Create the fee-bump envelope (unsigned initially)
117    let fee_bump_envelope = FeeBumpTransactionEnvelope {
118        tx: fee_bump_tx,
119        signatures: vec![].try_into()?,
120    };
121
122    Ok(TransactionEnvelope::TxFeeBump(fee_bump_envelope))
123}
124
125/// Extract the inner transaction hash from a fee-bump envelope
126pub fn extract_inner_transaction_hash(envelope: &TransactionEnvelope) -> Result<String> {
127    match envelope {
128        TransactionEnvelope::TxFeeBump(fb_envelope) => {
129            let FeeBumpTransactionInnerTx::Tx(inner_tx) = &fb_envelope.tx.inner_tx;
130
131            // Calculate the hash of the inner transaction
132            let inner_envelope = TransactionEnvelope::Tx(inner_tx.clone());
133            let hash = calculate_transaction_hash(&inner_envelope)?;
134            Ok(hash)
135        }
136        _ => Err(eyre!("Not a fee-bump transaction")),
137    }
138}
139
140/// Calculate the hash of a transaction envelope
141pub fn calculate_transaction_hash(envelope: &TransactionEnvelope) -> Result<String> {
142    use sha2::{Digest, Sha256};
143
144    let xdr_bytes = envelope
145        .to_xdr(Limits::none())
146        .map_err(|e| eyre!("Failed to serialize transaction: {}", e))?;
147
148    let mut hasher = Sha256::new();
149    hasher.update(&xdr_bytes);
150    let hash = hasher.finalize();
151
152    Ok(hex::encode(hash))
153}
154
155/// Convert a MuxedAccount to a string representation
156pub fn muxed_account_to_string(muxed: &MuxedAccount) -> Result<String> {
157    match muxed {
158        MuxedAccount::Ed25519(key) => {
159            let bytes: [u8; 32] = key.0;
160            let pk = PublicKey(bytes);
161            Ok(pk.to_string())
162        }
163        MuxedAccount::MuxedEd25519(m) => {
164            // For muxed accounts, we need to extract the underlying ed25519 key
165            let bytes: [u8; 32] = m.ed25519.0;
166            let pk = PublicKey(bytes);
167            Ok(pk.to_string())
168        }
169    }
170}
171
172/// Convert a string address to a MuxedAccount
173/// Supports both Ed25519 (G...) and MuxedEd25519 (M...) account formats
174pub fn string_to_muxed_account(address: &str) -> Result<MuxedAccount> {
175    // Try to parse as muxed account first (M... format)
176    if let Ok(muxed) = StrkeyMuxedAccount::from_string(address) {
177        return Ok(MuxedAccount::MuxedEd25519(
178            soroban_rs::xdr::MuxedAccountMed25519 {
179                id: muxed.id,
180                ed25519: Uint256(muxed.ed25519),
181            },
182        ));
183    }
184
185    // Fall back to Ed25519 (G... format)
186    let pk =
187        PublicKey::from_string(address).map_err(|e| eyre!("Failed to decode account ID: {}", e))?;
188
189    let key = Uint256(pk.0);
190    Ok(MuxedAccount::Ed25519(key))
191}
192
193/// Extract operations from a transaction envelope
194pub fn extract_operations(envelope: &TransactionEnvelope) -> Result<&VecM<Operation, 100>> {
195    match envelope {
196        TransactionEnvelope::TxV0(e) => Ok(&e.tx.operations),
197        TransactionEnvelope::Tx(e) => Ok(&e.tx.operations),
198        TransactionEnvelope::TxFeeBump(e) => {
199            // For fee-bump transactions, extract operations from inner transaction
200            match &e.tx.inner_tx {
201                FeeBumpTransactionInnerTx::Tx(inner) => Ok(&inner.tx.operations),
202            }
203        }
204    }
205}
206
207/// Check if a transaction envelope contains operations that require simulation
208pub fn xdr_needs_simulation(envelope: &TransactionEnvelope) -> Result<bool> {
209    let operations = extract_operations(envelope)?;
210
211    // Check if any operation is a Soroban operation
212    for op in operations.iter() {
213        if matches!(op.body, OperationBody::InvokeHostFunction(_)) {
214            return Ok(true);
215        }
216    }
217
218    Ok(false)
219}
220
221/// Attach signatures to a transaction envelope
222/// This function handles all envelope types (V0, V1, and FeeBump)
223pub fn attach_signatures_to_envelope(
224    envelope: &mut TransactionEnvelope,
225    signatures: Vec<DecoratedSignature>,
226) -> Result<()> {
227    let signatures_vec: VecM<DecoratedSignature, 20> = signatures
228        .try_into()
229        .map_err(|_| eyre!("Too many signatures (max 20)"))?;
230
231    match envelope {
232        TransactionEnvelope::TxV0(ref mut v0_env) => {
233            v0_env.signatures = signatures_vec;
234        }
235        TransactionEnvelope::Tx(ref mut v1_env) => {
236            v1_env.signatures = signatures_vec;
237        }
238        TransactionEnvelope::TxFeeBump(ref mut fb_env) => {
239            fb_env.signatures = signatures_vec;
240        }
241    }
242
243    Ok(())
244}
245
246/// Convert a V0 transaction envelope to V1 format
247/// This is required for fee-bump transactions as they only support V1 inner transactions
248fn convert_v0_to_v1_envelope(
249    v0_envelope: soroban_rs::xdr::TransactionV0Envelope,
250) -> TransactionV1Envelope {
251    let v0_tx = &v0_envelope.tx;
252    let source_bytes: [u8; 32] = v0_tx.source_account_ed25519.0;
253
254    // Create V1 transaction from V0 data
255    let tx = soroban_rs::xdr::Transaction {
256        source_account: MuxedAccount::Ed25519(Uint256(source_bytes)),
257        fee: v0_tx.fee,
258        seq_num: v0_tx.seq_num.clone(),
259        cond: match v0_tx.time_bounds.clone() {
260            Some(tb) => soroban_rs::xdr::Preconditions::Time(tb),
261            None => soroban_rs::xdr::Preconditions::None,
262        },
263        memo: v0_tx.memo.clone(),
264        operations: v0_tx.operations.clone(),
265        ext: soroban_rs::xdr::TransactionExt::V0,
266    };
267
268    // Create V1 envelope with V0 signatures
269    TransactionV1Envelope {
270        tx,
271        signatures: v0_envelope.signatures.clone(),
272    }
273}
274
275/// Update the sequence number in an XDR envelope
276pub fn update_xdr_sequence(envelope: &mut TransactionEnvelope, sequence: i64) -> Result<()> {
277    match envelope {
278        TransactionEnvelope::TxV0(ref mut e) => {
279            e.tx.seq_num = soroban_rs::xdr::SequenceNumber(sequence);
280        }
281        TransactionEnvelope::Tx(ref mut e) => {
282            e.tx.seq_num = soroban_rs::xdr::SequenceNumber(sequence);
283        }
284        TransactionEnvelope::TxFeeBump(_) => {
285            return Err(eyre!("Cannot set sequence number on fee-bump transaction"));
286        }
287    }
288    Ok(())
289}
290
291/// Update the fee in an XDR envelope
292pub fn update_xdr_fee(envelope: &mut TransactionEnvelope, fee: u32) -> Result<()> {
293    match envelope {
294        TransactionEnvelope::TxV0(ref mut e) => {
295            e.tx.fee = fee;
296        }
297        TransactionEnvelope::Tx(ref mut e) => {
298            e.tx.fee = fee;
299        }
300        TransactionEnvelope::TxFeeBump(_) => {
301            return Err(eyre!(
302                "Cannot set fee on fee-bump transaction - use max_fee instead"
303            ));
304        }
305    }
306    Ok(())
307}
308
309#[cfg(test)]
310mod tests {
311    use super::*;
312    use crate::domain::transaction::stellar::test_helpers::*;
313    use soroban_rs::xdr::{
314        Asset, FeeBumpTransactionInnerTx, HostFunction, InvokeContractArgs, InvokeHostFunctionOp,
315        Limits, Memo, MuxedAccount, Operation, OperationBody, PaymentOp, Preconditions,
316        SequenceNumber, Signature, SignatureHint, TransactionV0, TransactionV0Envelope, Uint256,
317        VecM,
318    };
319    use stellar_strkey::ed25519::PublicKey;
320
321    // Helper to get test XDR
322    fn get_unsigned_xdr() -> String {
323        create_unsigned_xdr(TEST_PK, TEST_PK_2)
324    }
325
326    fn get_signed_xdr() -> String {
327        create_signed_xdr(TEST_PK, TEST_PK_2)
328    }
329
330    #[test]
331    fn test_parse_unsigned_xdr() {
332        // This test should parse an unsigned transaction XDR successfully
333        let unsigned_xdr = get_unsigned_xdr();
334        let result = parse_transaction_xdr(&unsigned_xdr, false);
335        assert!(result.is_ok(), "Failed to parse unsigned XDR");
336
337        let envelope = result.unwrap();
338        assert!(
339            !is_signed(&envelope),
340            "Unsigned XDR should not have signatures"
341        );
342    }
343
344    #[test]
345    fn test_parse_signed_xdr() {
346        // This test should parse a signed transaction XDR successfully
347        let signed_xdr = get_signed_xdr();
348        let result = parse_transaction_xdr(&signed_xdr, true);
349        assert!(result.is_ok(), "Failed to parse signed XDR");
350
351        let envelope = result.unwrap();
352        assert!(is_signed(&envelope), "Signed XDR should have signatures");
353    }
354
355    #[test]
356    fn test_parse_invalid_xdr() {
357        // This test should fail when parsing invalid XDR
358        let result = parse_transaction_xdr(INVALID_XDR, false);
359        assert!(result.is_err(), "Should fail to parse invalid XDR");
360    }
361
362    #[test]
363    fn test_validate_unsigned_xdr_expecting_signed() {
364        // This test should fail when unsigned XDR is provided but signed is expected
365        let unsigned_xdr = get_unsigned_xdr();
366        let result = parse_transaction_xdr(&unsigned_xdr, true);
367        assert!(
368            result.is_err(),
369            "Should fail when expecting signed but got unsigned"
370        );
371    }
372
373    #[test]
374    fn test_extract_source_account_from_xdr() {
375        // This test should extract the source account from the transaction
376        let unsigned_xdr = get_unsigned_xdr();
377        let envelope = parse_transaction_xdr(&unsigned_xdr, false).unwrap();
378        let source_account = extract_source_account(&envelope).unwrap();
379        assert!(!source_account.is_empty(), "Should extract source account");
380        assert_eq!(source_account, TEST_PK);
381    }
382
383    #[test]
384    fn test_validate_source_account() {
385        // This test should validate that the source account matches expected
386        let unsigned_xdr = get_unsigned_xdr();
387        let envelope = parse_transaction_xdr(&unsigned_xdr, false).unwrap();
388        let source_account = extract_source_account(&envelope).unwrap();
389
390        // This should pass
391        let result = validate_source_account(&envelope, &source_account);
392        assert!(result.is_ok(), "Should validate matching source account");
393
394        // This should fail
395        let result = validate_source_account(&envelope, "DIFFERENT_ACCOUNT");
396        assert!(
397            result.is_err(),
398            "Should fail with non-matching source account"
399        );
400    }
401
402    #[test]
403    fn test_build_fee_bump_envelope() {
404        // This test should create a fee-bump transaction from a signed inner transaction
405        let signed_xdr = get_signed_xdr();
406        let inner_envelope = parse_transaction_xdr(&signed_xdr, true).unwrap();
407        let max_fee = 10_000_000; // 1 XLM
408
409        let result = build_fee_bump_envelope(inner_envelope, TEST_PK_2, max_fee);
410        assert!(result.is_ok(), "Should build fee-bump envelope");
411
412        let fee_bump_envelope = result.unwrap();
413        assert!(
414            is_fee_bump(&fee_bump_envelope),
415            "Should be a fee-bump transaction"
416        );
417    }
418
419    #[test]
420    fn test_fee_bump_requires_different_source() {
421        // This test should fail when trying to fee-bump with same source as inner tx
422        let signed_xdr = get_signed_xdr();
423        let inner_envelope = parse_transaction_xdr(&signed_xdr, true).unwrap();
424        let inner_source = extract_source_account(&inner_envelope).unwrap();
425        let max_fee = 10_000_000;
426
427        let result = build_fee_bump_envelope(inner_envelope, &inner_source, max_fee);
428        assert!(
429            result.is_err(),
430            "Should fail when fee-bump source equals inner source"
431        );
432    }
433
434    #[test]
435    fn test_extract_inner_transaction_hash() {
436        // This test should extract the hash of the inner transaction from a fee-bump
437        let signed_xdr = get_signed_xdr();
438        let inner_envelope = parse_transaction_xdr(&signed_xdr, true).unwrap();
439        let fee_bump_envelope =
440            build_fee_bump_envelope(inner_envelope.clone(), TEST_PK_2, 10_000_000).unwrap();
441
442        let inner_hash = extract_inner_transaction_hash(&fee_bump_envelope).unwrap();
443        assert!(
444            !inner_hash.is_empty(),
445            "Should extract inner transaction hash"
446        );
447    }
448
449    #[test]
450    fn test_extract_operations_from_v1_envelope() {
451        // Test extracting operations from a V1 envelope
452        let envelope_xdr = get_unsigned_xdr();
453        let parsed = TransactionEnvelope::from_xdr_base64(envelope_xdr, Limits::none()).unwrap();
454
455        let operations = extract_operations(&parsed).unwrap();
456        assert_eq!(operations.len(), 1, "Should extract 1 operation");
457
458        // Verify the operation details
459        if let OperationBody::Payment(payment) = &operations[0].body {
460            assert_eq!(payment.amount, 1000000, "Payment amount should be 0.1 XLM");
461        } else {
462            panic!("Expected payment operation");
463        }
464    }
465
466    #[test]
467    fn test_extract_operations_from_v0_envelope() {
468        // Test extracting operations from a V0 envelope
469        let payment_op = create_native_payment_operation(TEST_PK_2, 2000000);
470        let envelope = create_v0_envelope(TEST_PK, vec![payment_op], 100, 1);
471
472        let operations = extract_operations(&envelope).unwrap();
473        assert_eq!(operations.len(), 1, "Should extract 1 operation from V0");
474
475        if let OperationBody::Payment(payment) = &operations[0].body {
476            assert_eq!(payment.amount, 2000000, "Payment amount should be 0.2 XLM");
477        } else {
478            panic!("Expected payment operation");
479        }
480    }
481
482    #[test]
483    fn test_extract_operations_from_fee_bump() {
484        // Test extracting operations from a fee-bump envelope
485        let signed_xdr = get_signed_xdr();
486        let inner_envelope = parse_transaction_xdr(&signed_xdr, true).unwrap();
487        let fee_bump_envelope =
488            build_fee_bump_envelope(inner_envelope, TEST_PK_2, 10_000_000).unwrap();
489
490        let operations = extract_operations(&fee_bump_envelope).unwrap();
491        assert_eq!(
492            operations.len(),
493            1,
494            "Should extract operations from inner tx"
495        );
496
497        if let OperationBody::Payment(payment) = &operations[0].body {
498            assert_eq!(payment.amount, 1000000, "Payment amount should be 0.1 XLM");
499        } else {
500            panic!("Expected payment operation");
501        }
502    }
503
504    #[test]
505    fn test_xdr_needs_simulation_with_soroban_operation() {
506        // Test that Soroban operations require simulation
507        let invoke_op = InvokeHostFunctionOp {
508            host_function: HostFunction::InvokeContract(InvokeContractArgs {
509                contract_address: soroban_rs::xdr::ScAddress::Contract(
510                    soroban_rs::xdr::ContractId(soroban_rs::xdr::Hash([0u8; 32])),
511                ),
512                function_name: "test".try_into().unwrap(),
513                args: vec![].try_into().unwrap(),
514            }),
515            auth: vec![].try_into().unwrap(),
516        };
517
518        let operation = Operation {
519            source_account: None,
520            body: OperationBody::InvokeHostFunction(invoke_op),
521        };
522
523        let envelope = create_v1_envelope(TEST_PK, vec![operation], 100, 1);
524
525        let needs_sim = xdr_needs_simulation(&envelope).unwrap();
526        assert!(needs_sim, "Soroban operations should require simulation");
527    }
528
529    #[test]
530    fn test_xdr_needs_simulation_without_soroban() {
531        // Test that non-Soroban operations don't require simulation
532        let envelope_xdr = get_unsigned_xdr();
533        let parsed = TransactionEnvelope::from_xdr_base64(envelope_xdr, Limits::none()).unwrap();
534
535        let needs_sim = xdr_needs_simulation(&parsed).unwrap();
536        assert!(
537            !needs_sim,
538            "Payment operations should not require simulation"
539        );
540    }
541
542    #[test]
543    fn test_xdr_needs_simulation_with_multiple_operations() {
544        // Test with multiple operations where at least one is Soroban
545        let payment_op = create_native_payment_operation(TEST_PK_2, 1000000);
546
547        // Create a Soroban operation
548        let soroban_op = Operation {
549            source_account: None,
550            body: OperationBody::InvokeHostFunction(InvokeHostFunctionOp {
551                host_function: HostFunction::InvokeContract(InvokeContractArgs {
552                    contract_address: soroban_rs::xdr::ScAddress::Contract(
553                        soroban_rs::xdr::ContractId(soroban_rs::xdr::Hash([0u8; 32])),
554                    ),
555                    function_name: "test".try_into().unwrap(),
556                    args: vec![].try_into().unwrap(),
557                }),
558                auth: vec![].try_into().unwrap(),
559            }),
560        };
561
562        let envelope = create_v1_envelope(TEST_PK, vec![payment_op, soroban_op], 100, 1);
563
564        let needs_sim = xdr_needs_simulation(&envelope).unwrap();
565        assert!(
566            needs_sim,
567            "Should require simulation when any operation is Soroban"
568        );
569    }
570
571    #[test]
572    fn test_calculate_transaction_hash() {
573        // Test transaction hash calculation
574        let envelope_xdr = get_signed_xdr();
575        let envelope = parse_transaction_xdr(&envelope_xdr, true).unwrap();
576
577        let hash1 = calculate_transaction_hash(&envelope).unwrap();
578        let hash2 = calculate_transaction_hash(&envelope).unwrap();
579
580        // Hash should be deterministic
581        assert_eq!(hash1, hash2, "Hash should be deterministic");
582        assert_eq!(hash1.len(), 64, "SHA256 hash should be 64 hex characters");
583
584        // Verify it's valid hex
585        assert!(
586            hash1.chars().all(|c| c.is_ascii_hexdigit()),
587            "Hash should be valid hex"
588        );
589    }
590
591    #[test]
592    fn test_muxed_account_conversion() {
593        let address = "GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ";
594        let muxed = string_to_muxed_account(address).unwrap();
595        let back = muxed_account_to_string(&muxed).unwrap();
596        assert_eq!(address, back);
597    }
598
599    #[test]
600    fn test_muxed_account_ed25519_variant() {
601        // Test handling of regular Ed25519 accounts
602        let muxed = string_to_muxed_account(TEST_PK).unwrap();
603
604        match muxed {
605            MuxedAccount::Ed25519(_) => (),
606            _ => panic!("Expected Ed25519 variant"),
607        }
608
609        let back = muxed_account_to_string(&muxed).unwrap();
610        assert_eq!(TEST_PK, back);
611    }
612
613    #[test]
614    fn test_muxed_account_muxed_ed25519_variant() {
615        // Test handling of MuxedEd25519 accounts
616        let pk = parse_public_key(TEST_PK);
617
618        let muxed = MuxedAccount::MuxedEd25519(soroban_rs::xdr::MuxedAccountMed25519 {
619            id: 123456789,
620            ed25519: Uint256(pk.0),
621        });
622
623        let address = muxed_account_to_string(&muxed).unwrap();
624        assert_eq!(address, TEST_PK);
625    }
626
627    #[test]
628    fn test_v0_to_v1_conversion_in_fee_bump() {
629        // Test the V0 to V1 conversion logic in build_fee_bump_envelope
630        let source_pk =
631            PublicKey::from_string("GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF")
632                .unwrap();
633        let dest_pk =
634            PublicKey::from_string("GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ")
635                .unwrap();
636
637        // Create V0 transaction with time bounds
638        let time_bounds = soroban_rs::xdr::TimeBounds {
639            min_time: soroban_rs::xdr::TimePoint(1000),
640            max_time: soroban_rs::xdr::TimePoint(2000),
641        };
642
643        let payment_op = Operation {
644            source_account: None,
645            body: OperationBody::Payment(PaymentOp {
646                destination: MuxedAccount::Ed25519(Uint256(dest_pk.0)),
647                asset: Asset::Native,
648                amount: 3000000,
649            }),
650        };
651
652        let operations: VecM<Operation, 100> = vec![payment_op].try_into().unwrap();
653
654        let tx_v0 = TransactionV0 {
655            source_account_ed25519: Uint256(source_pk.0),
656            fee: 200,
657            seq_num: SequenceNumber(42),
658            time_bounds: Some(time_bounds.clone()),
659            memo: Memo::Text("Test memo".as_bytes().to_vec().try_into().unwrap()),
660            operations: operations.clone(),
661            ext: soroban_rs::xdr::TransactionV0Ext::V0,
662        };
663
664        // Add a signature to V0 envelope
665        let sig = DecoratedSignature {
666            hint: SignatureHint([1, 2, 3, 4]),
667            signature: Signature(vec![5u8; 64].try_into().unwrap()),
668        };
669
670        let v0_envelope = TransactionEnvelope::TxV0(TransactionV0Envelope {
671            tx: tx_v0,
672            signatures: vec![sig.clone()].try_into().unwrap(),
673        });
674
675        // Build fee-bump from V0 envelope
676        let fee_source = "GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ";
677        let fee_bump_envelope =
678            build_fee_bump_envelope(v0_envelope, fee_source, 50_000_000).unwrap();
679
680        // Verify it's a fee-bump envelope
681        assert!(matches!(
682            fee_bump_envelope,
683            TransactionEnvelope::TxFeeBump(_)
684        ));
685
686        if let TransactionEnvelope::TxFeeBump(fb_env) = fee_bump_envelope {
687            // Verify fee source
688            let fb_source = muxed_account_to_string(&fb_env.tx.fee_source).unwrap();
689            assert_eq!(fb_source, fee_source);
690            assert_eq!(fb_env.tx.fee, 50_000_000);
691
692            // Verify inner transaction was properly converted
693            let FeeBumpTransactionInnerTx::Tx(inner_v1) = &fb_env.tx.inner_tx;
694            // Check that V0 data was preserved in V1 format
695            assert_eq!(inner_v1.tx.fee, 200);
696            assert_eq!(inner_v1.tx.seq_num.0, 42);
697
698            // Check time bounds conversion
699            if let Preconditions::Time(tb) = &inner_v1.tx.cond {
700                assert_eq!(tb.min_time.0, 1000);
701                assert_eq!(tb.max_time.0, 2000);
702            } else {
703                panic!("Expected time bounds in preconditions");
704            }
705
706            // Check memo preservation
707            if let Memo::Text(text) = &inner_v1.tx.memo {
708                assert_eq!(text.as_slice(), "Test memo".as_bytes());
709            } else {
710                panic!("Expected text memo");
711            }
712
713            // Check operations preservation
714            assert_eq!(inner_v1.tx.operations.len(), 1);
715            // Check signatures were preserved
716            assert_eq!(inner_v1.signatures.len(), 1);
717            assert_eq!(inner_v1.signatures[0].hint, sig.hint);
718        }
719    }
720
721    #[test]
722    fn test_attach_signatures_to_envelope() {
723        use soroban_rs::xdr::{
724            DecoratedSignature, Memo, Operation, OperationBody, PaymentOp, SequenceNumber,
725            Signature, SignatureHint, TransactionV0, TransactionV0Envelope,
726        };
727        use stellar_strkey::ed25519::PublicKey;
728
729        let source_pk =
730            PublicKey::from_string("GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF")
731                .unwrap();
732        let dest_pk =
733            PublicKey::from_string("GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ")
734                .unwrap();
735
736        // Create a test transaction
737        let payment_op = Operation {
738            source_account: None,
739            body: OperationBody::Payment(PaymentOp {
740                destination: MuxedAccount::Ed25519(Uint256(dest_pk.0)),
741                asset: soroban_rs::xdr::Asset::Native,
742                amount: 1000000,
743            }),
744        };
745
746        let operations: VecM<Operation, 100> = vec![payment_op].try_into().unwrap();
747
748        let tx_v0 = TransactionV0 {
749            source_account_ed25519: Uint256(source_pk.0),
750            fee: 100,
751            seq_num: SequenceNumber(42),
752            time_bounds: None,
753            memo: Memo::None,
754            operations,
755            ext: soroban_rs::xdr::TransactionV0Ext::V0,
756        };
757
758        let mut envelope = TransactionEnvelope::TxV0(TransactionV0Envelope {
759            tx: tx_v0,
760            signatures: vec![].try_into().unwrap(),
761        });
762
763        // Create test signatures
764        let sig1 = DecoratedSignature {
765            hint: SignatureHint([1, 2, 3, 4]),
766            signature: Signature(vec![1u8; 64].try_into().unwrap()),
767        };
768        let sig2 = DecoratedSignature {
769            hint: SignatureHint([5, 6, 7, 8]),
770            signature: Signature(vec![2u8; 64].try_into().unwrap()),
771        };
772
773        // Attach signatures
774        let result = attach_signatures_to_envelope(&mut envelope, vec![sig1, sig2]);
775        assert!(result.is_ok());
776
777        // Verify signatures were attached
778        match &envelope {
779            TransactionEnvelope::TxV0(e) => {
780                assert_eq!(e.signatures.len(), 2);
781                assert_eq!(e.signatures[0].hint.0, [1, 2, 3, 4]);
782                assert_eq!(e.signatures[1].hint.0, [5, 6, 7, 8]);
783            }
784            _ => panic!("Expected V0 envelope"),
785        }
786    }
787
788    #[test]
789    fn test_extract_operations() {
790        use soroban_rs::xdr::{
791            Memo, Operation, OperationBody, PaymentOp, SequenceNumber, Transaction, TransactionV0,
792            TransactionV0Envelope, TransactionV1Envelope,
793        };
794        use stellar_strkey::ed25519::PublicKey;
795
796        let source_pk =
797            PublicKey::from_string("GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF")
798                .unwrap();
799        let dest_pk =
800            PublicKey::from_string("GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ")
801                .unwrap();
802
803        // Create test operation
804        let payment_op = Operation {
805            source_account: None,
806            body: OperationBody::Payment(PaymentOp {
807                destination: MuxedAccount::Ed25519(Uint256(dest_pk.0)),
808                asset: soroban_rs::xdr::Asset::Native,
809                amount: 1000000,
810            }),
811        };
812
813        let operations: VecM<Operation, 100> = vec![payment_op.clone()].try_into().unwrap();
814
815        // Test V0 envelope
816        let tx_v0 = TransactionV0 {
817            source_account_ed25519: Uint256(source_pk.0),
818            fee: 100,
819            seq_num: SequenceNumber(42),
820            time_bounds: None,
821            memo: Memo::None,
822            operations: operations.clone(),
823            ext: soroban_rs::xdr::TransactionV0Ext::V0,
824        };
825
826        let v0_envelope = TransactionEnvelope::TxV0(TransactionV0Envelope {
827            tx: tx_v0,
828            signatures: vec![].try_into().unwrap(),
829        });
830
831        let extracted_ops = extract_operations(&v0_envelope).unwrap();
832        assert_eq!(extracted_ops.len(), 1);
833
834        // Test V1 envelope
835        let tx_v1 = Transaction {
836            source_account: MuxedAccount::Ed25519(Uint256(source_pk.0)),
837            fee: 100,
838            seq_num: SequenceNumber(42),
839            cond: soroban_rs::xdr::Preconditions::None,
840            memo: Memo::None,
841            operations: operations.clone(),
842            ext: soroban_rs::xdr::TransactionExt::V0,
843        };
844
845        let v1_envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
846            tx: tx_v1,
847            signatures: vec![].try_into().unwrap(),
848        });
849
850        let extracted_ops = extract_operations(&v1_envelope).unwrap();
851        assert_eq!(extracted_ops.len(), 1);
852    }
853
854    #[test]
855    fn test_xdr_needs_simulation() {
856        use soroban_rs::xdr::{
857            HostFunction, InvokeHostFunctionOp, Memo, Operation, OperationBody, PaymentOp,
858            ScSymbol, ScVal, SequenceNumber, Transaction, TransactionV1Envelope,
859        };
860        use stellar_strkey::ed25519::PublicKey;
861
862        let source_pk =
863            PublicKey::from_string("GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF")
864                .unwrap();
865        let dest_pk =
866            PublicKey::from_string("GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ")
867                .unwrap();
868
869        // Test with payment operation (should not need simulation)
870        let payment_op = Operation {
871            source_account: None,
872            body: OperationBody::Payment(PaymentOp {
873                destination: MuxedAccount::Ed25519(Uint256(dest_pk.0)),
874                asset: soroban_rs::xdr::Asset::Native,
875                amount: 1000000,
876            }),
877        };
878
879        let operations: VecM<Operation, 100> = vec![payment_op].try_into().unwrap();
880
881        let tx = Transaction {
882            source_account: MuxedAccount::Ed25519(Uint256(source_pk.0)),
883            fee: 100,
884            seq_num: SequenceNumber(42),
885            cond: soroban_rs::xdr::Preconditions::None,
886            memo: Memo::None,
887            operations,
888            ext: soroban_rs::xdr::TransactionExt::V0,
889        };
890
891        let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
892            tx,
893            signatures: vec![].try_into().unwrap(),
894        });
895
896        assert!(!xdr_needs_simulation(&envelope).unwrap());
897
898        // Test with InvokeHostFunction operation (should need simulation)
899        let invoke_op = Operation {
900            source_account: None,
901            body: OperationBody::InvokeHostFunction(InvokeHostFunctionOp {
902                host_function: HostFunction::InvokeContract(soroban_rs::xdr::InvokeContractArgs {
903                    contract_address: soroban_rs::xdr::ScAddress::Contract(
904                        soroban_rs::xdr::ContractId(soroban_rs::xdr::Hash([0u8; 32])),
905                    ),
906                    function_name: ScSymbol("test".try_into().unwrap()),
907                    args: vec![ScVal::U32(42)].try_into().unwrap(),
908                }),
909                auth: vec![].try_into().unwrap(),
910            }),
911        };
912
913        let operations: VecM<Operation, 100> = vec![invoke_op].try_into().unwrap();
914
915        let tx = Transaction {
916            source_account: MuxedAccount::Ed25519(Uint256(source_pk.0)),
917            fee: 100,
918            seq_num: SequenceNumber(42),
919            cond: soroban_rs::xdr::Preconditions::None,
920            memo: Memo::None,
921            operations,
922            ext: soroban_rs::xdr::TransactionExt::V0,
923        };
924
925        let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
926            tx,
927            signatures: vec![].try_into().unwrap(),
928        });
929
930        assert!(xdr_needs_simulation(&envelope).unwrap());
931    }
932
933    #[test]
934    fn test_v0_to_v1_conversion() {
935        use soroban_rs::xdr::{
936            Memo, Operation, OperationBody, PaymentOp, SequenceNumber, TimeBounds, TimePoint,
937            TransactionV0, TransactionV0Envelope,
938        };
939        use stellar_strkey::ed25519::PublicKey;
940
941        let source_pk =
942            PublicKey::from_string("GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF")
943                .unwrap();
944        let dest_pk =
945            PublicKey::from_string("GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ")
946                .unwrap();
947
948        // Create test V0 transaction with various fields
949        let time_bounds = TimeBounds {
950            min_time: TimePoint(1000),
951            max_time: TimePoint(2000),
952        };
953
954        let payment_op = Operation {
955            source_account: None,
956            body: OperationBody::Payment(PaymentOp {
957                destination: MuxedAccount::Ed25519(Uint256(dest_pk.0)),
958                asset: soroban_rs::xdr::Asset::Native,
959                amount: 1000000,
960            }),
961        };
962
963        let operations: VecM<Operation, 100> = vec![payment_op].try_into().unwrap();
964
965        let tx_v0 = TransactionV0 {
966            source_account_ed25519: Uint256(source_pk.0),
967            fee: 100,
968            seq_num: SequenceNumber(42),
969            time_bounds: Some(time_bounds.clone()),
970            memo: Memo::Text("Test".as_bytes().to_vec().try_into().unwrap()),
971            operations: operations.clone(),
972            ext: soroban_rs::xdr::TransactionV0Ext::V0,
973        };
974
975        let sig = soroban_rs::xdr::DecoratedSignature {
976            hint: soroban_rs::xdr::SignatureHint([1, 2, 3, 4]),
977            signature: soroban_rs::xdr::Signature(vec![0u8; 64].try_into().unwrap()),
978        };
979
980        let v0_envelope = TransactionV0Envelope {
981            tx: tx_v0,
982            signatures: vec![sig.clone()].try_into().unwrap(),
983        };
984
985        // Convert to V1
986        let v1_envelope = convert_v0_to_v1_envelope(v0_envelope);
987
988        // Verify conversion preserved all data
989        assert_eq!(v1_envelope.tx.fee, 100);
990        assert_eq!(v1_envelope.tx.seq_num.0, 42);
991        assert_eq!(v1_envelope.tx.operations.len(), 1);
992        assert_eq!(v1_envelope.signatures.len(), 1);
993
994        // Check source account conversion
995        if let MuxedAccount::Ed25519(key) = &v1_envelope.tx.source_account {
996            assert_eq!(key.0, source_pk.0);
997        } else {
998            panic!("Expected Ed25519 source account");
999        }
1000
1001        // Check time bounds conversion
1002        if let soroban_rs::xdr::Preconditions::Time(tb) = &v1_envelope.tx.cond {
1003            assert_eq!(tb.min_time.0, 1000);
1004            assert_eq!(tb.max_time.0, 2000);
1005        } else {
1006            panic!("Expected time bounds in preconditions");
1007        }
1008
1009        // Check memo preservation
1010        if let Memo::Text(text) = &v1_envelope.tx.memo {
1011            assert_eq!(text.as_slice(), "Test".as_bytes());
1012        } else {
1013            panic!("Expected text memo");
1014        }
1015    }
1016
1017    #[test]
1018    fn test_update_xdr_sequence_v0() {
1019        use soroban_rs::xdr::{
1020            Memo, Operation, OperationBody, PaymentOp, SequenceNumber, TransactionV0,
1021            TransactionV0Envelope,
1022        };
1023        use stellar_strkey::ed25519::PublicKey;
1024
1025        let source_pk =
1026            PublicKey::from_string("GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF")
1027                .unwrap();
1028        let dest_pk =
1029            PublicKey::from_string("GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ")
1030                .unwrap();
1031
1032        let payment_op = Operation {
1033            source_account: None,
1034            body: OperationBody::Payment(PaymentOp {
1035                destination: MuxedAccount::Ed25519(Uint256(dest_pk.0)),
1036                asset: soroban_rs::xdr::Asset::Native,
1037                amount: 1000000,
1038            }),
1039        };
1040
1041        let operations: VecM<Operation, 100> = vec![payment_op].try_into().unwrap();
1042
1043        let tx_v0 = TransactionV0 {
1044            source_account_ed25519: Uint256(source_pk.0),
1045            fee: 100,
1046            seq_num: SequenceNumber(42),
1047            time_bounds: None,
1048            memo: Memo::None,
1049            operations,
1050            ext: soroban_rs::xdr::TransactionV0Ext::V0,
1051        };
1052
1053        let mut envelope = TransactionEnvelope::TxV0(TransactionV0Envelope {
1054            tx: tx_v0,
1055            signatures: vec![].try_into().unwrap(),
1056        });
1057
1058        // Update sequence number
1059        let result = update_xdr_sequence(&mut envelope, 100);
1060        assert!(result.is_ok());
1061
1062        // Verify the sequence was updated
1063        if let TransactionEnvelope::TxV0(e) = envelope {
1064            assert_eq!(e.tx.seq_num.0, 100);
1065        } else {
1066            panic!("Expected V0 envelope");
1067        }
1068    }
1069
1070    #[test]
1071    fn test_update_xdr_sequence_v1() {
1072        use soroban_rs::xdr::{
1073            Memo, Operation, OperationBody, PaymentOp, SequenceNumber, Transaction,
1074            TransactionV1Envelope,
1075        };
1076        use stellar_strkey::ed25519::PublicKey;
1077
1078        let source_pk =
1079            PublicKey::from_string("GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF")
1080                .unwrap();
1081        let dest_pk =
1082            PublicKey::from_string("GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ")
1083                .unwrap();
1084
1085        let payment_op = Operation {
1086            source_account: None,
1087            body: OperationBody::Payment(PaymentOp {
1088                destination: MuxedAccount::Ed25519(Uint256(dest_pk.0)),
1089                asset: soroban_rs::xdr::Asset::Native,
1090                amount: 1000000,
1091            }),
1092        };
1093
1094        let operations: VecM<Operation, 100> = vec![payment_op].try_into().unwrap();
1095
1096        let tx = Transaction {
1097            source_account: MuxedAccount::Ed25519(Uint256(source_pk.0)),
1098            fee: 100,
1099            seq_num: SequenceNumber(42),
1100            cond: soroban_rs::xdr::Preconditions::None,
1101            memo: Memo::None,
1102            operations,
1103            ext: soroban_rs::xdr::TransactionExt::V0,
1104        };
1105
1106        let mut envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
1107            tx,
1108            signatures: vec![].try_into().unwrap(),
1109        });
1110
1111        // Update sequence number
1112        let result = update_xdr_sequence(&mut envelope, 200);
1113        assert!(result.is_ok());
1114
1115        // Verify the sequence was updated
1116        if let TransactionEnvelope::Tx(e) = envelope {
1117            assert_eq!(e.tx.seq_num.0, 200);
1118        } else {
1119            panic!("Expected V1 envelope");
1120        }
1121    }
1122
1123    #[test]
1124    fn test_update_xdr_sequence_fee_bump_fails() {
1125        // Create a fee-bump envelope
1126        let signed_xdr = get_signed_xdr();
1127        let inner_envelope = parse_transaction_xdr(&signed_xdr, true).unwrap();
1128        let fee_source = "GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ";
1129        let mut fee_bump_envelope =
1130            build_fee_bump_envelope(inner_envelope, fee_source, 10_000_000).unwrap();
1131
1132        // Attempt to update sequence number on fee-bump should fail
1133        let result = update_xdr_sequence(&mut fee_bump_envelope, 100);
1134        assert!(result.is_err());
1135        assert!(result
1136            .unwrap_err()
1137            .to_string()
1138            .contains("Cannot set sequence number on fee-bump transaction"));
1139    }
1140
1141    #[test]
1142    fn test_update_xdr_fee_v0() {
1143        use soroban_rs::xdr::{
1144            Memo, Operation, OperationBody, PaymentOp, SequenceNumber, TransactionV0,
1145            TransactionV0Envelope,
1146        };
1147        use stellar_strkey::ed25519::PublicKey;
1148
1149        let source_pk =
1150            PublicKey::from_string("GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF")
1151                .unwrap();
1152        let dest_pk =
1153            PublicKey::from_string("GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ")
1154                .unwrap();
1155
1156        let payment_op = Operation {
1157            source_account: None,
1158            body: OperationBody::Payment(PaymentOp {
1159                destination: MuxedAccount::Ed25519(Uint256(dest_pk.0)),
1160                asset: soroban_rs::xdr::Asset::Native,
1161                amount: 1000000,
1162            }),
1163        };
1164
1165        let operations: VecM<Operation, 100> = vec![payment_op].try_into().unwrap();
1166
1167        let tx_v0 = TransactionV0 {
1168            source_account_ed25519: Uint256(source_pk.0),
1169            fee: 100,
1170            seq_num: SequenceNumber(42),
1171            time_bounds: None,
1172            memo: Memo::None,
1173            operations,
1174            ext: soroban_rs::xdr::TransactionV0Ext::V0,
1175        };
1176
1177        let mut envelope = TransactionEnvelope::TxV0(TransactionV0Envelope {
1178            tx: tx_v0,
1179            signatures: vec![].try_into().unwrap(),
1180        });
1181
1182        // Update fee
1183        let result = update_xdr_fee(&mut envelope, 500);
1184        assert!(result.is_ok());
1185
1186        // Verify the fee was updated
1187        if let TransactionEnvelope::TxV0(e) = envelope {
1188            assert_eq!(e.tx.fee, 500);
1189        } else {
1190            panic!("Expected V0 envelope");
1191        }
1192    }
1193
1194    #[test]
1195    fn test_update_xdr_fee_v1() {
1196        use soroban_rs::xdr::{
1197            Memo, Operation, OperationBody, PaymentOp, SequenceNumber, Transaction,
1198            TransactionV1Envelope,
1199        };
1200        use stellar_strkey::ed25519::PublicKey;
1201
1202        let source_pk =
1203            PublicKey::from_string("GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF")
1204                .unwrap();
1205        let dest_pk =
1206            PublicKey::from_string("GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ")
1207                .unwrap();
1208
1209        let payment_op = Operation {
1210            source_account: None,
1211            body: OperationBody::Payment(PaymentOp {
1212                destination: MuxedAccount::Ed25519(Uint256(dest_pk.0)),
1213                asset: soroban_rs::xdr::Asset::Native,
1214                amount: 1000000,
1215            }),
1216        };
1217
1218        let operations: VecM<Operation, 100> = vec![payment_op].try_into().unwrap();
1219
1220        let tx = Transaction {
1221            source_account: MuxedAccount::Ed25519(Uint256(source_pk.0)),
1222            fee: 100,
1223            seq_num: SequenceNumber(42),
1224            cond: soroban_rs::xdr::Preconditions::None,
1225            memo: Memo::None,
1226            operations,
1227            ext: soroban_rs::xdr::TransactionExt::V0,
1228        };
1229
1230        let mut envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
1231            tx,
1232            signatures: vec![].try_into().unwrap(),
1233        });
1234
1235        // Update fee
1236        let result = update_xdr_fee(&mut envelope, 1000);
1237        assert!(result.is_ok());
1238
1239        // Verify the fee was updated
1240        if let TransactionEnvelope::Tx(e) = envelope {
1241            assert_eq!(e.tx.fee, 1000);
1242        } else {
1243            panic!("Expected V1 envelope");
1244        }
1245    }
1246
1247    #[test]
1248    fn test_update_xdr_fee_fee_bump_fails() {
1249        // Create a fee-bump envelope
1250        let signed_xdr = get_signed_xdr();
1251        let inner_envelope = parse_transaction_xdr(&signed_xdr, true).unwrap();
1252        let fee_source = "GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ";
1253        let mut fee_bump_envelope =
1254            build_fee_bump_envelope(inner_envelope, fee_source, 10_000_000).unwrap();
1255
1256        // Attempt to update fee on fee-bump should fail
1257        let result = update_xdr_fee(&mut fee_bump_envelope, 500);
1258        assert!(result.is_err());
1259        assert!(result
1260            .unwrap_err()
1261            .to_string()
1262            .contains("Cannot set fee on fee-bump transaction"));
1263    }
1264
1265    #[test]
1266    fn test_update_xdr_sequence_preserves_other_fields() {
1267        use soroban_rs::xdr::{
1268            Memo, Operation, OperationBody, PaymentOp, SequenceNumber, Transaction,
1269            TransactionV1Envelope,
1270        };
1271        use stellar_strkey::ed25519::PublicKey;
1272
1273        let source_pk =
1274            PublicKey::from_string("GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF")
1275                .unwrap();
1276        let dest_pk =
1277            PublicKey::from_string("GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ")
1278                .unwrap();
1279
1280        let payment_op = Operation {
1281            source_account: None,
1282            body: OperationBody::Payment(PaymentOp {
1283                destination: MuxedAccount::Ed25519(Uint256(dest_pk.0)),
1284                asset: soroban_rs::xdr::Asset::Native,
1285                amount: 5000000,
1286            }),
1287        };
1288
1289        let operations: VecM<Operation, 100> = vec![payment_op].try_into().unwrap();
1290
1291        let tx = Transaction {
1292            source_account: MuxedAccount::Ed25519(Uint256(source_pk.0)),
1293            fee: 300,
1294            seq_num: SequenceNumber(10),
1295            cond: soroban_rs::xdr::Preconditions::None,
1296            memo: Memo::Text("Test".as_bytes().to_vec().try_into().unwrap()),
1297            operations,
1298            ext: soroban_rs::xdr::TransactionExt::V0,
1299        };
1300
1301        let mut envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
1302            tx,
1303            signatures: vec![].try_into().unwrap(),
1304        });
1305
1306        // Update sequence number
1307        update_xdr_sequence(&mut envelope, 50).unwrap();
1308
1309        // Verify other fields are preserved
1310        if let TransactionEnvelope::Tx(e) = envelope {
1311            assert_eq!(e.tx.seq_num.0, 50); // Updated
1312            assert_eq!(e.tx.fee, 300); // Preserved
1313            assert_eq!(e.tx.operations.len(), 1); // Preserved
1314            if let Memo::Text(text) = &e.tx.memo {
1315                assert_eq!(text.as_slice(), "Test".as_bytes()); // Preserved
1316            } else {
1317                panic!("Expected text memo");
1318            }
1319        } else {
1320            panic!("Expected V1 envelope");
1321        }
1322    }
1323
1324    #[test]
1325    fn test_update_xdr_fee_preserves_other_fields() {
1326        use soroban_rs::xdr::{
1327            Memo, Operation, OperationBody, PaymentOp, SequenceNumber, Transaction,
1328            TransactionV1Envelope,
1329        };
1330        use stellar_strkey::ed25519::PublicKey;
1331
1332        let source_pk =
1333            PublicKey::from_string("GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF")
1334                .unwrap();
1335        let dest_pk =
1336            PublicKey::from_string("GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ")
1337                .unwrap();
1338
1339        let payment_op = Operation {
1340            source_account: None,
1341            body: OperationBody::Payment(PaymentOp {
1342                destination: MuxedAccount::Ed25519(Uint256(dest_pk.0)),
1343                asset: soroban_rs::xdr::Asset::Native,
1344                amount: 5000000,
1345            }),
1346        };
1347
1348        let operations: VecM<Operation, 100> = vec![payment_op].try_into().unwrap();
1349
1350        let tx = Transaction {
1351            source_account: MuxedAccount::Ed25519(Uint256(source_pk.0)),
1352            fee: 300,
1353            seq_num: SequenceNumber(10),
1354            cond: soroban_rs::xdr::Preconditions::None,
1355            memo: Memo::Text("Test".as_bytes().to_vec().try_into().unwrap()),
1356            operations,
1357            ext: soroban_rs::xdr::TransactionExt::V0,
1358        };
1359
1360        let mut envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
1361            tx,
1362            signatures: vec![].try_into().unwrap(),
1363        });
1364
1365        // Update fee
1366        update_xdr_fee(&mut envelope, 750).unwrap();
1367
1368        // Verify other fields are preserved
1369        if let TransactionEnvelope::Tx(e) = envelope {
1370            assert_eq!(e.tx.fee, 750); // Updated
1371            assert_eq!(e.tx.seq_num.0, 10); // Preserved
1372            assert_eq!(e.tx.operations.len(), 1); // Preserved
1373            if let Memo::Text(text) = &e.tx.memo {
1374                assert_eq!(text.as_slice(), "Test".as_bytes()); // Preserved
1375            } else {
1376                panic!("Expected text memo");
1377            }
1378        } else {
1379            panic!("Expected V1 envelope");
1380        }
1381    }
1382}