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, TransactionExt,
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/// Extract the resource fee from a transaction's SorobanTransactionData if present.
222///
223/// Returns Some(resource_fee) if the transaction has TransactionExt::V1 with resourceFee > 0.
224/// Returns None if TransactionExt::V0 or resourceFee is 0 or the transaction is V0 format.
225pub fn extract_soroban_resource_fee(envelope: &TransactionEnvelope) -> Option<i64> {
226    let ext = match envelope {
227        TransactionEnvelope::TxV0(_) => return None, // V0 doesn't support Soroban
228        TransactionEnvelope::Tx(e) => &e.tx.ext,
229        TransactionEnvelope::TxFeeBump(fb) => {
230            let FeeBumpTransactionInnerTx::Tx(inner) = &fb.tx.inner_tx;
231            &inner.tx.ext
232        }
233    };
234
235    match ext {
236        TransactionExt::V1(soroban_data) => {
237            let fee = soroban_data.resource_fee;
238            if fee > 0 {
239                Some(fee)
240            } else {
241                None
242            }
243        }
244        TransactionExt::V0 => None,
245    }
246}
247
248/// Attach signatures to a transaction envelope
249/// This function handles all envelope types (V0, V1, and FeeBump)
250pub fn attach_signatures_to_envelope(
251    envelope: &mut TransactionEnvelope,
252    signatures: Vec<DecoratedSignature>,
253) -> Result<()> {
254    let signatures_vec: VecM<DecoratedSignature, 20> = signatures
255        .try_into()
256        .map_err(|_| eyre!("Too many signatures (max 20)"))?;
257
258    match envelope {
259        TransactionEnvelope::TxV0(ref mut v0_env) => {
260            v0_env.signatures = signatures_vec;
261        }
262        TransactionEnvelope::Tx(ref mut v1_env) => {
263            v1_env.signatures = signatures_vec;
264        }
265        TransactionEnvelope::TxFeeBump(ref mut fb_env) => {
266            fb_env.signatures = signatures_vec;
267        }
268    }
269
270    Ok(())
271}
272
273/// Convert a V0 transaction envelope to V1 format
274/// This is required for fee-bump transactions as they only support V1 inner transactions
275fn convert_v0_to_v1_envelope(
276    v0_envelope: soroban_rs::xdr::TransactionV0Envelope,
277) -> TransactionV1Envelope {
278    let v0_tx = &v0_envelope.tx;
279    let source_bytes: [u8; 32] = v0_tx.source_account_ed25519.0;
280
281    // Create V1 transaction from V0 data
282    let tx = soroban_rs::xdr::Transaction {
283        source_account: MuxedAccount::Ed25519(Uint256(source_bytes)),
284        fee: v0_tx.fee,
285        seq_num: v0_tx.seq_num.clone(),
286        cond: match v0_tx.time_bounds.clone() {
287            Some(tb) => soroban_rs::xdr::Preconditions::Time(tb),
288            None => soroban_rs::xdr::Preconditions::None,
289        },
290        memo: v0_tx.memo.clone(),
291        operations: v0_tx.operations.clone(),
292        ext: soroban_rs::xdr::TransactionExt::V0,
293    };
294
295    // Create V1 envelope with V0 signatures
296    TransactionV1Envelope {
297        tx,
298        signatures: v0_envelope.signatures.clone(),
299    }
300}
301
302/// Update the sequence number in an XDR envelope
303pub fn update_xdr_sequence(envelope: &mut TransactionEnvelope, sequence: i64) -> Result<()> {
304    match envelope {
305        TransactionEnvelope::TxV0(ref mut e) => {
306            e.tx.seq_num = soroban_rs::xdr::SequenceNumber(sequence);
307        }
308        TransactionEnvelope::Tx(ref mut e) => {
309            e.tx.seq_num = soroban_rs::xdr::SequenceNumber(sequence);
310        }
311        TransactionEnvelope::TxFeeBump(_) => {
312            return Err(eyre!("Cannot set sequence number on fee-bump transaction"));
313        }
314    }
315    Ok(())
316}
317
318/// Update the fee in an XDR envelope
319pub fn update_xdr_fee(envelope: &mut TransactionEnvelope, fee: u32) -> Result<()> {
320    match envelope {
321        TransactionEnvelope::TxV0(ref mut e) => {
322            e.tx.fee = fee;
323        }
324        TransactionEnvelope::Tx(ref mut e) => {
325            e.tx.fee = fee;
326        }
327        TransactionEnvelope::TxFeeBump(_) => {
328            return Err(eyre!(
329                "Cannot set fee on fee-bump transaction - use max_fee instead"
330            ));
331        }
332    }
333    Ok(())
334}
335
336#[cfg(test)]
337mod tests {
338    use super::*;
339    use crate::domain::transaction::stellar::test_helpers::*;
340    use soroban_rs::xdr::{
341        Asset, FeeBumpTransactionInnerTx, HostFunction, InvokeContractArgs, InvokeHostFunctionOp,
342        Limits, Memo, MuxedAccount, Operation, OperationBody, PaymentOp, Preconditions,
343        SequenceNumber, Signature, SignatureHint, TransactionV0, TransactionV0Envelope, Uint256,
344        VecM,
345    };
346    use stellar_strkey::ed25519::PublicKey;
347
348    // Helper to get test XDR
349    fn get_unsigned_xdr() -> String {
350        create_unsigned_xdr(TEST_PK, TEST_PK_2)
351    }
352
353    fn get_signed_xdr() -> String {
354        create_signed_xdr(TEST_PK, TEST_PK_2)
355    }
356
357    #[test]
358    fn test_parse_unsigned_xdr() {
359        // This test should parse an unsigned transaction XDR successfully
360        let unsigned_xdr = get_unsigned_xdr();
361        let result = parse_transaction_xdr(&unsigned_xdr, false);
362        assert!(result.is_ok(), "Failed to parse unsigned XDR");
363
364        let envelope = result.unwrap();
365        assert!(
366            !is_signed(&envelope),
367            "Unsigned XDR should not have signatures"
368        );
369    }
370
371    #[test]
372    fn test_parse_signed_xdr() {
373        // This test should parse a signed transaction XDR successfully
374        let signed_xdr = get_signed_xdr();
375        let result = parse_transaction_xdr(&signed_xdr, true);
376        assert!(result.is_ok(), "Failed to parse signed XDR");
377
378        let envelope = result.unwrap();
379        assert!(is_signed(&envelope), "Signed XDR should have signatures");
380    }
381
382    #[test]
383    fn test_parse_invalid_xdr() {
384        // This test should fail when parsing invalid XDR
385        let result = parse_transaction_xdr(INVALID_XDR, false);
386        assert!(result.is_err(), "Should fail to parse invalid XDR");
387    }
388
389    #[test]
390    fn test_validate_unsigned_xdr_expecting_signed() {
391        // This test should fail when unsigned XDR is provided but signed is expected
392        let unsigned_xdr = get_unsigned_xdr();
393        let result = parse_transaction_xdr(&unsigned_xdr, true);
394        assert!(
395            result.is_err(),
396            "Should fail when expecting signed but got unsigned"
397        );
398    }
399
400    #[test]
401    fn test_extract_source_account_from_xdr() {
402        // This test should extract the source account from the transaction
403        let unsigned_xdr = get_unsigned_xdr();
404        let envelope = parse_transaction_xdr(&unsigned_xdr, false).unwrap();
405        let source_account = extract_source_account(&envelope).unwrap();
406        assert!(!source_account.is_empty(), "Should extract source account");
407        assert_eq!(source_account, TEST_PK);
408    }
409
410    #[test]
411    fn test_validate_source_account() {
412        // This test should validate that the source account matches expected
413        let unsigned_xdr = get_unsigned_xdr();
414        let envelope = parse_transaction_xdr(&unsigned_xdr, false).unwrap();
415        let source_account = extract_source_account(&envelope).unwrap();
416
417        // This should pass
418        let result = validate_source_account(&envelope, &source_account);
419        assert!(result.is_ok(), "Should validate matching source account");
420
421        // This should fail
422        let result = validate_source_account(&envelope, "DIFFERENT_ACCOUNT");
423        assert!(
424            result.is_err(),
425            "Should fail with non-matching source account"
426        );
427    }
428
429    #[test]
430    fn test_build_fee_bump_envelope() {
431        // This test should create a fee-bump transaction from a signed inner transaction
432        let signed_xdr = get_signed_xdr();
433        let inner_envelope = parse_transaction_xdr(&signed_xdr, true).unwrap();
434        let max_fee = 10_000_000; // 1 XLM
435
436        let result = build_fee_bump_envelope(inner_envelope, TEST_PK_2, max_fee);
437        assert!(result.is_ok(), "Should build fee-bump envelope");
438
439        let fee_bump_envelope = result.unwrap();
440        assert!(
441            is_fee_bump(&fee_bump_envelope),
442            "Should be a fee-bump transaction"
443        );
444    }
445
446    #[test]
447    fn test_fee_bump_requires_different_source() {
448        // This test should fail when trying to fee-bump with same source as inner tx
449        let signed_xdr = get_signed_xdr();
450        let inner_envelope = parse_transaction_xdr(&signed_xdr, true).unwrap();
451        let inner_source = extract_source_account(&inner_envelope).unwrap();
452        let max_fee = 10_000_000;
453
454        let result = build_fee_bump_envelope(inner_envelope, &inner_source, max_fee);
455        assert!(
456            result.is_err(),
457            "Should fail when fee-bump source equals inner source"
458        );
459    }
460
461    #[test]
462    fn test_extract_inner_transaction_hash() {
463        // This test should extract the hash of the inner transaction from a fee-bump
464        let signed_xdr = get_signed_xdr();
465        let inner_envelope = parse_transaction_xdr(&signed_xdr, true).unwrap();
466        let fee_bump_envelope =
467            build_fee_bump_envelope(inner_envelope.clone(), TEST_PK_2, 10_000_000).unwrap();
468
469        let inner_hash = extract_inner_transaction_hash(&fee_bump_envelope).unwrap();
470        assert!(
471            !inner_hash.is_empty(),
472            "Should extract inner transaction hash"
473        );
474    }
475
476    #[test]
477    fn test_extract_operations_from_v1_envelope() {
478        // Test extracting operations from a V1 envelope
479        let envelope_xdr = get_unsigned_xdr();
480        let parsed = TransactionEnvelope::from_xdr_base64(envelope_xdr, Limits::none()).unwrap();
481
482        let operations = extract_operations(&parsed).unwrap();
483        assert_eq!(operations.len(), 1, "Should extract 1 operation");
484
485        // Verify the operation details
486        if let OperationBody::Payment(payment) = &operations[0].body {
487            assert_eq!(payment.amount, 1000000, "Payment amount should be 0.1 XLM");
488        } else {
489            panic!("Expected payment operation");
490        }
491    }
492
493    #[test]
494    fn test_extract_operations_from_v0_envelope() {
495        // Test extracting operations from a V0 envelope
496        let payment_op = create_native_payment_operation(TEST_PK_2, 2000000);
497        let envelope = create_v0_envelope(TEST_PK, vec![payment_op], 100, 1);
498
499        let operations = extract_operations(&envelope).unwrap();
500        assert_eq!(operations.len(), 1, "Should extract 1 operation from V0");
501
502        if let OperationBody::Payment(payment) = &operations[0].body {
503            assert_eq!(payment.amount, 2000000, "Payment amount should be 0.2 XLM");
504        } else {
505            panic!("Expected payment operation");
506        }
507    }
508
509    #[test]
510    fn test_extract_operations_from_fee_bump() {
511        // Test extracting operations from a fee-bump envelope
512        let signed_xdr = get_signed_xdr();
513        let inner_envelope = parse_transaction_xdr(&signed_xdr, true).unwrap();
514        let fee_bump_envelope =
515            build_fee_bump_envelope(inner_envelope, TEST_PK_2, 10_000_000).unwrap();
516
517        let operations = extract_operations(&fee_bump_envelope).unwrap();
518        assert_eq!(
519            operations.len(),
520            1,
521            "Should extract operations from inner tx"
522        );
523
524        if let OperationBody::Payment(payment) = &operations[0].body {
525            assert_eq!(payment.amount, 1000000, "Payment amount should be 0.1 XLM");
526        } else {
527            panic!("Expected payment operation");
528        }
529    }
530
531    #[test]
532    fn test_xdr_needs_simulation_with_soroban_operation() {
533        // Test that Soroban operations require simulation
534        let invoke_op = InvokeHostFunctionOp {
535            host_function: HostFunction::InvokeContract(InvokeContractArgs {
536                contract_address: soroban_rs::xdr::ScAddress::Contract(
537                    soroban_rs::xdr::ContractId(soroban_rs::xdr::Hash([0u8; 32])),
538                ),
539                function_name: "test".try_into().unwrap(),
540                args: vec![].try_into().unwrap(),
541            }),
542            auth: vec![].try_into().unwrap(),
543        };
544
545        let operation = Operation {
546            source_account: None,
547            body: OperationBody::InvokeHostFunction(invoke_op),
548        };
549
550        let envelope = create_v1_envelope(TEST_PK, vec![operation], 100, 1);
551
552        let needs_sim = xdr_needs_simulation(&envelope).unwrap();
553        assert!(needs_sim, "Soroban operations should require simulation");
554    }
555
556    #[test]
557    fn test_xdr_needs_simulation_without_soroban() {
558        // Test that non-Soroban operations don't require simulation
559        let envelope_xdr = get_unsigned_xdr();
560        let parsed = TransactionEnvelope::from_xdr_base64(envelope_xdr, Limits::none()).unwrap();
561
562        let needs_sim = xdr_needs_simulation(&parsed).unwrap();
563        assert!(
564            !needs_sim,
565            "Payment operations should not require simulation"
566        );
567    }
568
569    #[test]
570    fn test_xdr_needs_simulation_with_multiple_operations() {
571        // Test with multiple operations where at least one is Soroban
572        let payment_op = create_native_payment_operation(TEST_PK_2, 1000000);
573
574        // Create a Soroban operation
575        let soroban_op = Operation {
576            source_account: None,
577            body: OperationBody::InvokeHostFunction(InvokeHostFunctionOp {
578                host_function: HostFunction::InvokeContract(InvokeContractArgs {
579                    contract_address: soroban_rs::xdr::ScAddress::Contract(
580                        soroban_rs::xdr::ContractId(soroban_rs::xdr::Hash([0u8; 32])),
581                    ),
582                    function_name: "test".try_into().unwrap(),
583                    args: vec![].try_into().unwrap(),
584                }),
585                auth: vec![].try_into().unwrap(),
586            }),
587        };
588
589        let envelope = create_v1_envelope(TEST_PK, vec![payment_op, soroban_op], 100, 1);
590
591        let needs_sim = xdr_needs_simulation(&envelope).unwrap();
592        assert!(
593            needs_sim,
594            "Should require simulation when any operation is Soroban"
595        );
596    }
597
598    #[test]
599    fn test_calculate_transaction_hash() {
600        // Test transaction hash calculation
601        let envelope_xdr = get_signed_xdr();
602        let envelope = parse_transaction_xdr(&envelope_xdr, true).unwrap();
603
604        let hash1 = calculate_transaction_hash(&envelope).unwrap();
605        let hash2 = calculate_transaction_hash(&envelope).unwrap();
606
607        // Hash should be deterministic
608        assert_eq!(hash1, hash2, "Hash should be deterministic");
609        assert_eq!(hash1.len(), 64, "SHA256 hash should be 64 hex characters");
610
611        // Verify it's valid hex
612        assert!(
613            hash1.chars().all(|c| c.is_ascii_hexdigit()),
614            "Hash should be valid hex"
615        );
616    }
617
618    #[test]
619    fn test_muxed_account_conversion() {
620        let address = "GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ";
621        let muxed = string_to_muxed_account(address).unwrap();
622        let back = muxed_account_to_string(&muxed).unwrap();
623        assert_eq!(address, back);
624    }
625
626    #[test]
627    fn test_muxed_account_ed25519_variant() {
628        // Test handling of regular Ed25519 accounts
629        let muxed = string_to_muxed_account(TEST_PK).unwrap();
630
631        match muxed {
632            MuxedAccount::Ed25519(_) => (),
633            _ => panic!("Expected Ed25519 variant"),
634        }
635
636        let back = muxed_account_to_string(&muxed).unwrap();
637        assert_eq!(TEST_PK, back);
638    }
639
640    #[test]
641    fn test_muxed_account_muxed_ed25519_variant() {
642        // Test handling of MuxedEd25519 accounts
643        let pk = parse_public_key(TEST_PK);
644
645        let muxed = MuxedAccount::MuxedEd25519(soroban_rs::xdr::MuxedAccountMed25519 {
646            id: 123456789,
647            ed25519: Uint256(pk.0),
648        });
649
650        let address = muxed_account_to_string(&muxed).unwrap();
651        assert_eq!(address, TEST_PK);
652    }
653
654    #[test]
655    fn test_v0_to_v1_conversion_in_fee_bump() {
656        // Test the V0 to V1 conversion logic in build_fee_bump_envelope
657        let source_pk =
658            PublicKey::from_string("GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF")
659                .unwrap();
660        let dest_pk =
661            PublicKey::from_string("GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ")
662                .unwrap();
663
664        // Create V0 transaction with time bounds
665        let time_bounds = soroban_rs::xdr::TimeBounds {
666            min_time: soroban_rs::xdr::TimePoint(1000),
667            max_time: soroban_rs::xdr::TimePoint(2000),
668        };
669
670        let payment_op = Operation {
671            source_account: None,
672            body: OperationBody::Payment(PaymentOp {
673                destination: MuxedAccount::Ed25519(Uint256(dest_pk.0)),
674                asset: Asset::Native,
675                amount: 3000000,
676            }),
677        };
678
679        let operations: VecM<Operation, 100> = vec![payment_op].try_into().unwrap();
680
681        let tx_v0 = TransactionV0 {
682            source_account_ed25519: Uint256(source_pk.0),
683            fee: 200,
684            seq_num: SequenceNumber(42),
685            time_bounds: Some(time_bounds.clone()),
686            memo: Memo::Text("Test memo".as_bytes().to_vec().try_into().unwrap()),
687            operations: operations.clone(),
688            ext: soroban_rs::xdr::TransactionV0Ext::V0,
689        };
690
691        // Add a signature to V0 envelope
692        let sig = DecoratedSignature {
693            hint: SignatureHint([1, 2, 3, 4]),
694            signature: Signature(vec![5u8; 64].try_into().unwrap()),
695        };
696
697        let v0_envelope = TransactionEnvelope::TxV0(TransactionV0Envelope {
698            tx: tx_v0,
699            signatures: vec![sig.clone()].try_into().unwrap(),
700        });
701
702        // Build fee-bump from V0 envelope
703        let fee_source = "GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ";
704        let fee_bump_envelope =
705            build_fee_bump_envelope(v0_envelope, fee_source, 50_000_000).unwrap();
706
707        // Verify it's a fee-bump envelope
708        assert!(matches!(
709            fee_bump_envelope,
710            TransactionEnvelope::TxFeeBump(_)
711        ));
712
713        if let TransactionEnvelope::TxFeeBump(fb_env) = fee_bump_envelope {
714            // Verify fee source
715            let fb_source = muxed_account_to_string(&fb_env.tx.fee_source).unwrap();
716            assert_eq!(fb_source, fee_source);
717            assert_eq!(fb_env.tx.fee, 50_000_000);
718
719            // Verify inner transaction was properly converted
720            let FeeBumpTransactionInnerTx::Tx(inner_v1) = &fb_env.tx.inner_tx;
721            // Check that V0 data was preserved in V1 format
722            assert_eq!(inner_v1.tx.fee, 200);
723            assert_eq!(inner_v1.tx.seq_num.0, 42);
724
725            // Check time bounds conversion
726            if let Preconditions::Time(tb) = &inner_v1.tx.cond {
727                assert_eq!(tb.min_time.0, 1000);
728                assert_eq!(tb.max_time.0, 2000);
729            } else {
730                panic!("Expected time bounds in preconditions");
731            }
732
733            // Check memo preservation
734            if let Memo::Text(text) = &inner_v1.tx.memo {
735                assert_eq!(text.as_slice(), "Test memo".as_bytes());
736            } else {
737                panic!("Expected text memo");
738            }
739
740            // Check operations preservation
741            assert_eq!(inner_v1.tx.operations.len(), 1);
742            // Check signatures were preserved
743            assert_eq!(inner_v1.signatures.len(), 1);
744            assert_eq!(inner_v1.signatures[0].hint, sig.hint);
745        }
746    }
747
748    #[test]
749    fn test_attach_signatures_to_envelope() {
750        use soroban_rs::xdr::{
751            DecoratedSignature, Memo, Operation, OperationBody, PaymentOp, SequenceNumber,
752            Signature, SignatureHint, TransactionV0, TransactionV0Envelope,
753        };
754        use stellar_strkey::ed25519::PublicKey;
755
756        let source_pk =
757            PublicKey::from_string("GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF")
758                .unwrap();
759        let dest_pk =
760            PublicKey::from_string("GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ")
761                .unwrap();
762
763        // Create a test transaction
764        let payment_op = Operation {
765            source_account: None,
766            body: OperationBody::Payment(PaymentOp {
767                destination: MuxedAccount::Ed25519(Uint256(dest_pk.0)),
768                asset: soroban_rs::xdr::Asset::Native,
769                amount: 1000000,
770            }),
771        };
772
773        let operations: VecM<Operation, 100> = vec![payment_op].try_into().unwrap();
774
775        let tx_v0 = TransactionV0 {
776            source_account_ed25519: Uint256(source_pk.0),
777            fee: 100,
778            seq_num: SequenceNumber(42),
779            time_bounds: None,
780            memo: Memo::None,
781            operations,
782            ext: soroban_rs::xdr::TransactionV0Ext::V0,
783        };
784
785        let mut envelope = TransactionEnvelope::TxV0(TransactionV0Envelope {
786            tx: tx_v0,
787            signatures: vec![].try_into().unwrap(),
788        });
789
790        // Create test signatures
791        let sig1 = DecoratedSignature {
792            hint: SignatureHint([1, 2, 3, 4]),
793            signature: Signature(vec![1u8; 64].try_into().unwrap()),
794        };
795        let sig2 = DecoratedSignature {
796            hint: SignatureHint([5, 6, 7, 8]),
797            signature: Signature(vec![2u8; 64].try_into().unwrap()),
798        };
799
800        // Attach signatures
801        let result = attach_signatures_to_envelope(&mut envelope, vec![sig1, sig2]);
802        assert!(result.is_ok());
803
804        // Verify signatures were attached
805        match &envelope {
806            TransactionEnvelope::TxV0(e) => {
807                assert_eq!(e.signatures.len(), 2);
808                assert_eq!(e.signatures[0].hint.0, [1, 2, 3, 4]);
809                assert_eq!(e.signatures[1].hint.0, [5, 6, 7, 8]);
810            }
811            _ => panic!("Expected V0 envelope"),
812        }
813    }
814
815    #[test]
816    fn test_extract_operations() {
817        use soroban_rs::xdr::{
818            Memo, Operation, OperationBody, PaymentOp, SequenceNumber, Transaction, TransactionV0,
819            TransactionV0Envelope, TransactionV1Envelope,
820        };
821        use stellar_strkey::ed25519::PublicKey;
822
823        let source_pk =
824            PublicKey::from_string("GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF")
825                .unwrap();
826        let dest_pk =
827            PublicKey::from_string("GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ")
828                .unwrap();
829
830        // Create test operation
831        let payment_op = Operation {
832            source_account: None,
833            body: OperationBody::Payment(PaymentOp {
834                destination: MuxedAccount::Ed25519(Uint256(dest_pk.0)),
835                asset: soroban_rs::xdr::Asset::Native,
836                amount: 1000000,
837            }),
838        };
839
840        let operations: VecM<Operation, 100> = vec![payment_op.clone()].try_into().unwrap();
841
842        // Test V0 envelope
843        let tx_v0 = TransactionV0 {
844            source_account_ed25519: Uint256(source_pk.0),
845            fee: 100,
846            seq_num: SequenceNumber(42),
847            time_bounds: None,
848            memo: Memo::None,
849            operations: operations.clone(),
850            ext: soroban_rs::xdr::TransactionV0Ext::V0,
851        };
852
853        let v0_envelope = TransactionEnvelope::TxV0(TransactionV0Envelope {
854            tx: tx_v0,
855            signatures: vec![].try_into().unwrap(),
856        });
857
858        let extracted_ops = extract_operations(&v0_envelope).unwrap();
859        assert_eq!(extracted_ops.len(), 1);
860
861        // Test V1 envelope
862        let tx_v1 = Transaction {
863            source_account: MuxedAccount::Ed25519(Uint256(source_pk.0)),
864            fee: 100,
865            seq_num: SequenceNumber(42),
866            cond: soroban_rs::xdr::Preconditions::None,
867            memo: Memo::None,
868            operations: operations.clone(),
869            ext: soroban_rs::xdr::TransactionExt::V0,
870        };
871
872        let v1_envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
873            tx: tx_v1,
874            signatures: vec![].try_into().unwrap(),
875        });
876
877        let extracted_ops = extract_operations(&v1_envelope).unwrap();
878        assert_eq!(extracted_ops.len(), 1);
879    }
880
881    #[test]
882    fn test_xdr_needs_simulation() {
883        use soroban_rs::xdr::{
884            HostFunction, InvokeHostFunctionOp, Memo, Operation, OperationBody, PaymentOp,
885            ScSymbol, ScVal, SequenceNumber, Transaction, TransactionV1Envelope,
886        };
887        use stellar_strkey::ed25519::PublicKey;
888
889        let source_pk =
890            PublicKey::from_string("GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF")
891                .unwrap();
892        let dest_pk =
893            PublicKey::from_string("GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ")
894                .unwrap();
895
896        // Test with payment operation (should not need simulation)
897        let payment_op = Operation {
898            source_account: None,
899            body: OperationBody::Payment(PaymentOp {
900                destination: MuxedAccount::Ed25519(Uint256(dest_pk.0)),
901                asset: soroban_rs::xdr::Asset::Native,
902                amount: 1000000,
903            }),
904        };
905
906        let operations: VecM<Operation, 100> = vec![payment_op].try_into().unwrap();
907
908        let tx = Transaction {
909            source_account: MuxedAccount::Ed25519(Uint256(source_pk.0)),
910            fee: 100,
911            seq_num: SequenceNumber(42),
912            cond: soroban_rs::xdr::Preconditions::None,
913            memo: Memo::None,
914            operations,
915            ext: soroban_rs::xdr::TransactionExt::V0,
916        };
917
918        let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
919            tx,
920            signatures: vec![].try_into().unwrap(),
921        });
922
923        assert!(!xdr_needs_simulation(&envelope).unwrap());
924
925        // Test with InvokeHostFunction operation (should need simulation)
926        let invoke_op = Operation {
927            source_account: None,
928            body: OperationBody::InvokeHostFunction(InvokeHostFunctionOp {
929                host_function: HostFunction::InvokeContract(soroban_rs::xdr::InvokeContractArgs {
930                    contract_address: soroban_rs::xdr::ScAddress::Contract(
931                        soroban_rs::xdr::ContractId(soroban_rs::xdr::Hash([0u8; 32])),
932                    ),
933                    function_name: ScSymbol("test".try_into().unwrap()),
934                    args: vec![ScVal::U32(42)].try_into().unwrap(),
935                }),
936                auth: vec![].try_into().unwrap(),
937            }),
938        };
939
940        let operations: VecM<Operation, 100> = vec![invoke_op].try_into().unwrap();
941
942        let tx = Transaction {
943            source_account: MuxedAccount::Ed25519(Uint256(source_pk.0)),
944            fee: 100,
945            seq_num: SequenceNumber(42),
946            cond: soroban_rs::xdr::Preconditions::None,
947            memo: Memo::None,
948            operations,
949            ext: soroban_rs::xdr::TransactionExt::V0,
950        };
951
952        let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
953            tx,
954            signatures: vec![].try_into().unwrap(),
955        });
956
957        assert!(xdr_needs_simulation(&envelope).unwrap());
958    }
959
960    #[test]
961    fn test_v0_to_v1_conversion() {
962        use soroban_rs::xdr::{
963            Memo, Operation, OperationBody, PaymentOp, SequenceNumber, TimeBounds, TimePoint,
964            TransactionV0, TransactionV0Envelope,
965        };
966        use stellar_strkey::ed25519::PublicKey;
967
968        let source_pk =
969            PublicKey::from_string("GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF")
970                .unwrap();
971        let dest_pk =
972            PublicKey::from_string("GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ")
973                .unwrap();
974
975        // Create test V0 transaction with various fields
976        let time_bounds = TimeBounds {
977            min_time: TimePoint(1000),
978            max_time: TimePoint(2000),
979        };
980
981        let payment_op = Operation {
982            source_account: None,
983            body: OperationBody::Payment(PaymentOp {
984                destination: MuxedAccount::Ed25519(Uint256(dest_pk.0)),
985                asset: soroban_rs::xdr::Asset::Native,
986                amount: 1000000,
987            }),
988        };
989
990        let operations: VecM<Operation, 100> = vec![payment_op].try_into().unwrap();
991
992        let tx_v0 = TransactionV0 {
993            source_account_ed25519: Uint256(source_pk.0),
994            fee: 100,
995            seq_num: SequenceNumber(42),
996            time_bounds: Some(time_bounds.clone()),
997            memo: Memo::Text("Test".as_bytes().to_vec().try_into().unwrap()),
998            operations: operations.clone(),
999            ext: soroban_rs::xdr::TransactionV0Ext::V0,
1000        };
1001
1002        let sig = soroban_rs::xdr::DecoratedSignature {
1003            hint: soroban_rs::xdr::SignatureHint([1, 2, 3, 4]),
1004            signature: soroban_rs::xdr::Signature(vec![0u8; 64].try_into().unwrap()),
1005        };
1006
1007        let v0_envelope = TransactionV0Envelope {
1008            tx: tx_v0,
1009            signatures: vec![sig.clone()].try_into().unwrap(),
1010        };
1011
1012        // Convert to V1
1013        let v1_envelope = convert_v0_to_v1_envelope(v0_envelope);
1014
1015        // Verify conversion preserved all data
1016        assert_eq!(v1_envelope.tx.fee, 100);
1017        assert_eq!(v1_envelope.tx.seq_num.0, 42);
1018        assert_eq!(v1_envelope.tx.operations.len(), 1);
1019        assert_eq!(v1_envelope.signatures.len(), 1);
1020
1021        // Check source account conversion
1022        if let MuxedAccount::Ed25519(key) = &v1_envelope.tx.source_account {
1023            assert_eq!(key.0, source_pk.0);
1024        } else {
1025            panic!("Expected Ed25519 source account");
1026        }
1027
1028        // Check time bounds conversion
1029        if let soroban_rs::xdr::Preconditions::Time(tb) = &v1_envelope.tx.cond {
1030            assert_eq!(tb.min_time.0, 1000);
1031            assert_eq!(tb.max_time.0, 2000);
1032        } else {
1033            panic!("Expected time bounds in preconditions");
1034        }
1035
1036        // Check memo preservation
1037        if let Memo::Text(text) = &v1_envelope.tx.memo {
1038            assert_eq!(text.as_slice(), "Test".as_bytes());
1039        } else {
1040            panic!("Expected text memo");
1041        }
1042    }
1043
1044    #[test]
1045    fn test_update_xdr_sequence_v0() {
1046        use soroban_rs::xdr::{
1047            Memo, Operation, OperationBody, PaymentOp, SequenceNumber, TransactionV0,
1048            TransactionV0Envelope,
1049        };
1050        use stellar_strkey::ed25519::PublicKey;
1051
1052        let source_pk =
1053            PublicKey::from_string("GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF")
1054                .unwrap();
1055        let dest_pk =
1056            PublicKey::from_string("GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ")
1057                .unwrap();
1058
1059        let payment_op = Operation {
1060            source_account: None,
1061            body: OperationBody::Payment(PaymentOp {
1062                destination: MuxedAccount::Ed25519(Uint256(dest_pk.0)),
1063                asset: soroban_rs::xdr::Asset::Native,
1064                amount: 1000000,
1065            }),
1066        };
1067
1068        let operations: VecM<Operation, 100> = vec![payment_op].try_into().unwrap();
1069
1070        let tx_v0 = TransactionV0 {
1071            source_account_ed25519: Uint256(source_pk.0),
1072            fee: 100,
1073            seq_num: SequenceNumber(42),
1074            time_bounds: None,
1075            memo: Memo::None,
1076            operations,
1077            ext: soroban_rs::xdr::TransactionV0Ext::V0,
1078        };
1079
1080        let mut envelope = TransactionEnvelope::TxV0(TransactionV0Envelope {
1081            tx: tx_v0,
1082            signatures: vec![].try_into().unwrap(),
1083        });
1084
1085        // Update sequence number
1086        let result = update_xdr_sequence(&mut envelope, 100);
1087        assert!(result.is_ok());
1088
1089        // Verify the sequence was updated
1090        if let TransactionEnvelope::TxV0(e) = envelope {
1091            assert_eq!(e.tx.seq_num.0, 100);
1092        } else {
1093            panic!("Expected V0 envelope");
1094        }
1095    }
1096
1097    #[test]
1098    fn test_update_xdr_sequence_v1() {
1099        use soroban_rs::xdr::{
1100            Memo, Operation, OperationBody, PaymentOp, SequenceNumber, Transaction,
1101            TransactionV1Envelope,
1102        };
1103        use stellar_strkey::ed25519::PublicKey;
1104
1105        let source_pk =
1106            PublicKey::from_string("GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF")
1107                .unwrap();
1108        let dest_pk =
1109            PublicKey::from_string("GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ")
1110                .unwrap();
1111
1112        let payment_op = Operation {
1113            source_account: None,
1114            body: OperationBody::Payment(PaymentOp {
1115                destination: MuxedAccount::Ed25519(Uint256(dest_pk.0)),
1116                asset: soroban_rs::xdr::Asset::Native,
1117                amount: 1000000,
1118            }),
1119        };
1120
1121        let operations: VecM<Operation, 100> = vec![payment_op].try_into().unwrap();
1122
1123        let tx = Transaction {
1124            source_account: MuxedAccount::Ed25519(Uint256(source_pk.0)),
1125            fee: 100,
1126            seq_num: SequenceNumber(42),
1127            cond: soroban_rs::xdr::Preconditions::None,
1128            memo: Memo::None,
1129            operations,
1130            ext: soroban_rs::xdr::TransactionExt::V0,
1131        };
1132
1133        let mut envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
1134            tx,
1135            signatures: vec![].try_into().unwrap(),
1136        });
1137
1138        // Update sequence number
1139        let result = update_xdr_sequence(&mut envelope, 200);
1140        assert!(result.is_ok());
1141
1142        // Verify the sequence was updated
1143        if let TransactionEnvelope::Tx(e) = envelope {
1144            assert_eq!(e.tx.seq_num.0, 200);
1145        } else {
1146            panic!("Expected V1 envelope");
1147        }
1148    }
1149
1150    #[test]
1151    fn test_update_xdr_sequence_fee_bump_fails() {
1152        // Create a fee-bump envelope
1153        let signed_xdr = get_signed_xdr();
1154        let inner_envelope = parse_transaction_xdr(&signed_xdr, true).unwrap();
1155        let fee_source = "GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ";
1156        let mut fee_bump_envelope =
1157            build_fee_bump_envelope(inner_envelope, fee_source, 10_000_000).unwrap();
1158
1159        // Attempt to update sequence number on fee-bump should fail
1160        let result = update_xdr_sequence(&mut fee_bump_envelope, 100);
1161        assert!(result.is_err());
1162        assert!(result
1163            .unwrap_err()
1164            .to_string()
1165            .contains("Cannot set sequence number on fee-bump transaction"));
1166    }
1167
1168    #[test]
1169    fn test_update_xdr_fee_v0() {
1170        use soroban_rs::xdr::{
1171            Memo, Operation, OperationBody, PaymentOp, SequenceNumber, TransactionV0,
1172            TransactionV0Envelope,
1173        };
1174        use stellar_strkey::ed25519::PublicKey;
1175
1176        let source_pk =
1177            PublicKey::from_string("GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF")
1178                .unwrap();
1179        let dest_pk =
1180            PublicKey::from_string("GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ")
1181                .unwrap();
1182
1183        let payment_op = Operation {
1184            source_account: None,
1185            body: OperationBody::Payment(PaymentOp {
1186                destination: MuxedAccount::Ed25519(Uint256(dest_pk.0)),
1187                asset: soroban_rs::xdr::Asset::Native,
1188                amount: 1000000,
1189            }),
1190        };
1191
1192        let operations: VecM<Operation, 100> = vec![payment_op].try_into().unwrap();
1193
1194        let tx_v0 = TransactionV0 {
1195            source_account_ed25519: Uint256(source_pk.0),
1196            fee: 100,
1197            seq_num: SequenceNumber(42),
1198            time_bounds: None,
1199            memo: Memo::None,
1200            operations,
1201            ext: soroban_rs::xdr::TransactionV0Ext::V0,
1202        };
1203
1204        let mut envelope = TransactionEnvelope::TxV0(TransactionV0Envelope {
1205            tx: tx_v0,
1206            signatures: vec![].try_into().unwrap(),
1207        });
1208
1209        // Update fee
1210        let result = update_xdr_fee(&mut envelope, 500);
1211        assert!(result.is_ok());
1212
1213        // Verify the fee was updated
1214        if let TransactionEnvelope::TxV0(e) = envelope {
1215            assert_eq!(e.tx.fee, 500);
1216        } else {
1217            panic!("Expected V0 envelope");
1218        }
1219    }
1220
1221    #[test]
1222    fn test_update_xdr_fee_v1() {
1223        use soroban_rs::xdr::{
1224            Memo, Operation, OperationBody, PaymentOp, SequenceNumber, Transaction,
1225            TransactionV1Envelope,
1226        };
1227        use stellar_strkey::ed25519::PublicKey;
1228
1229        let source_pk =
1230            PublicKey::from_string("GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF")
1231                .unwrap();
1232        let dest_pk =
1233            PublicKey::from_string("GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ")
1234                .unwrap();
1235
1236        let payment_op = Operation {
1237            source_account: None,
1238            body: OperationBody::Payment(PaymentOp {
1239                destination: MuxedAccount::Ed25519(Uint256(dest_pk.0)),
1240                asset: soroban_rs::xdr::Asset::Native,
1241                amount: 1000000,
1242            }),
1243        };
1244
1245        let operations: VecM<Operation, 100> = vec![payment_op].try_into().unwrap();
1246
1247        let tx = Transaction {
1248            source_account: MuxedAccount::Ed25519(Uint256(source_pk.0)),
1249            fee: 100,
1250            seq_num: SequenceNumber(42),
1251            cond: soroban_rs::xdr::Preconditions::None,
1252            memo: Memo::None,
1253            operations,
1254            ext: soroban_rs::xdr::TransactionExt::V0,
1255        };
1256
1257        let mut envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
1258            tx,
1259            signatures: vec![].try_into().unwrap(),
1260        });
1261
1262        // Update fee
1263        let result = update_xdr_fee(&mut envelope, 1000);
1264        assert!(result.is_ok());
1265
1266        // Verify the fee was updated
1267        if let TransactionEnvelope::Tx(e) = envelope {
1268            assert_eq!(e.tx.fee, 1000);
1269        } else {
1270            panic!("Expected V1 envelope");
1271        }
1272    }
1273
1274    #[test]
1275    fn test_update_xdr_fee_fee_bump_fails() {
1276        // Create a fee-bump envelope
1277        let signed_xdr = get_signed_xdr();
1278        let inner_envelope = parse_transaction_xdr(&signed_xdr, true).unwrap();
1279        let fee_source = "GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ";
1280        let mut fee_bump_envelope =
1281            build_fee_bump_envelope(inner_envelope, fee_source, 10_000_000).unwrap();
1282
1283        // Attempt to update fee on fee-bump should fail
1284        let result = update_xdr_fee(&mut fee_bump_envelope, 500);
1285        assert!(result.is_err());
1286        assert!(result
1287            .unwrap_err()
1288            .to_string()
1289            .contains("Cannot set fee on fee-bump transaction"));
1290    }
1291
1292    #[test]
1293    fn test_update_xdr_sequence_preserves_other_fields() {
1294        use soroban_rs::xdr::{
1295            Memo, Operation, OperationBody, PaymentOp, SequenceNumber, Transaction,
1296            TransactionV1Envelope,
1297        };
1298        use stellar_strkey::ed25519::PublicKey;
1299
1300        let source_pk =
1301            PublicKey::from_string("GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF")
1302                .unwrap();
1303        let dest_pk =
1304            PublicKey::from_string("GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ")
1305                .unwrap();
1306
1307        let payment_op = Operation {
1308            source_account: None,
1309            body: OperationBody::Payment(PaymentOp {
1310                destination: MuxedAccount::Ed25519(Uint256(dest_pk.0)),
1311                asset: soroban_rs::xdr::Asset::Native,
1312                amount: 5000000,
1313            }),
1314        };
1315
1316        let operations: VecM<Operation, 100> = vec![payment_op].try_into().unwrap();
1317
1318        let tx = Transaction {
1319            source_account: MuxedAccount::Ed25519(Uint256(source_pk.0)),
1320            fee: 300,
1321            seq_num: SequenceNumber(10),
1322            cond: soroban_rs::xdr::Preconditions::None,
1323            memo: Memo::Text("Test".as_bytes().to_vec().try_into().unwrap()),
1324            operations,
1325            ext: soroban_rs::xdr::TransactionExt::V0,
1326        };
1327
1328        let mut envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
1329            tx,
1330            signatures: vec![].try_into().unwrap(),
1331        });
1332
1333        // Update sequence number
1334        update_xdr_sequence(&mut envelope, 50).unwrap();
1335
1336        // Verify other fields are preserved
1337        if let TransactionEnvelope::Tx(e) = envelope {
1338            assert_eq!(e.tx.seq_num.0, 50); // Updated
1339            assert_eq!(e.tx.fee, 300); // Preserved
1340            assert_eq!(e.tx.operations.len(), 1); // Preserved
1341            if let Memo::Text(text) = &e.tx.memo {
1342                assert_eq!(text.as_slice(), "Test".as_bytes()); // Preserved
1343            } else {
1344                panic!("Expected text memo");
1345            }
1346        } else {
1347            panic!("Expected V1 envelope");
1348        }
1349    }
1350
1351    #[test]
1352    fn test_update_xdr_fee_preserves_other_fields() {
1353        use soroban_rs::xdr::{
1354            Memo, Operation, OperationBody, PaymentOp, SequenceNumber, Transaction,
1355            TransactionV1Envelope,
1356        };
1357        use stellar_strkey::ed25519::PublicKey;
1358
1359        let source_pk =
1360            PublicKey::from_string("GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF")
1361                .unwrap();
1362        let dest_pk =
1363            PublicKey::from_string("GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ")
1364                .unwrap();
1365
1366        let payment_op = Operation {
1367            source_account: None,
1368            body: OperationBody::Payment(PaymentOp {
1369                destination: MuxedAccount::Ed25519(Uint256(dest_pk.0)),
1370                asset: soroban_rs::xdr::Asset::Native,
1371                amount: 5000000,
1372            }),
1373        };
1374
1375        let operations: VecM<Operation, 100> = vec![payment_op].try_into().unwrap();
1376
1377        let tx = Transaction {
1378            source_account: MuxedAccount::Ed25519(Uint256(source_pk.0)),
1379            fee: 300,
1380            seq_num: SequenceNumber(10),
1381            cond: soroban_rs::xdr::Preconditions::None,
1382            memo: Memo::Text("Test".as_bytes().to_vec().try_into().unwrap()),
1383            operations,
1384            ext: soroban_rs::xdr::TransactionExt::V0,
1385        };
1386
1387        let mut envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
1388            tx,
1389            signatures: vec![].try_into().unwrap(),
1390        });
1391
1392        // Update fee
1393        update_xdr_fee(&mut envelope, 750).unwrap();
1394
1395        // Verify other fields are preserved
1396        if let TransactionEnvelope::Tx(e) = envelope {
1397            assert_eq!(e.tx.fee, 750); // Updated
1398            assert_eq!(e.tx.seq_num.0, 10); // Preserved
1399            assert_eq!(e.tx.operations.len(), 1); // Preserved
1400            if let Memo::Text(text) = &e.tx.memo {
1401                assert_eq!(text.as_slice(), "Test".as_bytes()); // Preserved
1402            } else {
1403                panic!("Expected text memo");
1404            }
1405        } else {
1406            panic!("Expected V1 envelope");
1407        }
1408    }
1409
1410    #[test]
1411    fn test_extract_soroban_resource_fee_with_v1_data() {
1412        use soroban_rs::xdr::{
1413            LedgerFootprint, Memo, SequenceNumber, SorobanResources, SorobanTransactionData,
1414            SorobanTransactionDataExt, Transaction, TransactionV1Envelope,
1415        };
1416        use stellar_strkey::ed25519::PublicKey;
1417
1418        let source_pk =
1419            PublicKey::from_string("GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF")
1420                .unwrap();
1421
1422        // Create Soroban transaction data with a resource fee
1423        let soroban_data = SorobanTransactionData {
1424            ext: SorobanTransactionDataExt::V0,
1425            resources: SorobanResources {
1426                footprint: LedgerFootprint {
1427                    read_only: vec![].try_into().unwrap(),
1428                    read_write: vec![].try_into().unwrap(),
1429                },
1430                instructions: 1000,
1431                disk_read_bytes: 100,
1432                write_bytes: 50,
1433            },
1434            resource_fee: 50000, // The resource fee we expect to extract
1435        };
1436
1437        let tx = Transaction {
1438            source_account: MuxedAccount::Ed25519(Uint256(source_pk.0)),
1439            fee: 100,
1440            seq_num: SequenceNumber(42),
1441            cond: soroban_rs::xdr::Preconditions::None,
1442            memo: Memo::None,
1443            operations: VecM::default(),
1444            ext: soroban_rs::xdr::TransactionExt::V1(soroban_data),
1445        };
1446
1447        let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
1448            tx,
1449            signatures: vec![].try_into().unwrap(),
1450        });
1451
1452        let result = extract_soroban_resource_fee(&envelope);
1453        assert_eq!(result, Some(50000));
1454    }
1455
1456    #[test]
1457    fn test_extract_soroban_resource_fee_with_v0_data() {
1458        use soroban_rs::xdr::{
1459            Memo, Operation, OperationBody, PaymentOp, SequenceNumber, Transaction,
1460            TransactionV1Envelope,
1461        };
1462        use stellar_strkey::ed25519::PublicKey;
1463
1464        let source_pk =
1465            PublicKey::from_string("GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF")
1466                .unwrap();
1467        let dest_pk =
1468            PublicKey::from_string("GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ")
1469                .unwrap();
1470
1471        let payment_op = Operation {
1472            source_account: None,
1473            body: OperationBody::Payment(PaymentOp {
1474                destination: MuxedAccount::Ed25519(Uint256(dest_pk.0)),
1475                asset: soroban_rs::xdr::Asset::Native,
1476                amount: 1000000,
1477            }),
1478        };
1479
1480        let operations: VecM<Operation, 100> = vec![payment_op].try_into().unwrap();
1481
1482        let tx = Transaction {
1483            source_account: MuxedAccount::Ed25519(Uint256(source_pk.0)),
1484            fee: 100,
1485            seq_num: SequenceNumber(42),
1486            cond: soroban_rs::xdr::Preconditions::None,
1487            memo: Memo::None,
1488            operations,
1489            ext: soroban_rs::xdr::TransactionExt::V0, // V0 extension
1490        };
1491
1492        let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
1493            tx,
1494            signatures: vec![].try_into().unwrap(),
1495        });
1496
1497        let result = extract_soroban_resource_fee(&envelope);
1498        assert_eq!(result, None);
1499    }
1500
1501    #[test]
1502    fn test_extract_soroban_resource_fee_with_zero_fee() {
1503        use soroban_rs::xdr::{
1504            LedgerFootprint, Memo, SequenceNumber, SorobanResources, SorobanTransactionData,
1505            SorobanTransactionDataExt, Transaction, TransactionV1Envelope,
1506        };
1507        use stellar_strkey::ed25519::PublicKey;
1508
1509        let source_pk =
1510            PublicKey::from_string("GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF")
1511                .unwrap();
1512
1513        // Create Soroban transaction data with zero resource fee
1514        let soroban_data = SorobanTransactionData {
1515            ext: SorobanTransactionDataExt::V0,
1516            resources: SorobanResources {
1517                footprint: LedgerFootprint {
1518                    read_only: vec![].try_into().unwrap(),
1519                    read_write: vec![].try_into().unwrap(),
1520                },
1521                instructions: 0,
1522                disk_read_bytes: 0,
1523                write_bytes: 0,
1524            },
1525            resource_fee: 0, // Zero resource fee
1526        };
1527
1528        let tx = Transaction {
1529            source_account: MuxedAccount::Ed25519(Uint256(source_pk.0)),
1530            fee: 100,
1531            seq_num: SequenceNumber(42),
1532            cond: soroban_rs::xdr::Preconditions::None,
1533            memo: Memo::None,
1534            operations: VecM::default(),
1535            ext: soroban_rs::xdr::TransactionExt::V1(soroban_data),
1536        };
1537
1538        let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
1539            tx,
1540            signatures: vec![].try_into().unwrap(),
1541        });
1542
1543        let result = extract_soroban_resource_fee(&envelope);
1544        assert_eq!(result, None); // Should return None for zero fee
1545    }
1546
1547    #[test]
1548    fn test_extract_soroban_resource_fee_from_fee_bump() {
1549        use soroban_rs::xdr::{
1550            DecoratedSignature, FeeBumpTransaction, FeeBumpTransactionEnvelope,
1551            FeeBumpTransactionInnerTx, LedgerFootprint, Memo, SequenceNumber, Signature,
1552            SignatureHint, SorobanResources, SorobanTransactionData, SorobanTransactionDataExt,
1553            Transaction, TransactionV1Envelope,
1554        };
1555        use stellar_strkey::ed25519::PublicKey;
1556
1557        let source_pk =
1558            PublicKey::from_string("GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF")
1559                .unwrap();
1560        let fee_source_pk =
1561            PublicKey::from_string("GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ")
1562                .unwrap();
1563
1564        // Create Soroban transaction data with a resource fee
1565        let soroban_data = SorobanTransactionData {
1566            ext: SorobanTransactionDataExt::V0,
1567            resources: SorobanResources {
1568                footprint: LedgerFootprint {
1569                    read_only: vec![].try_into().unwrap(),
1570                    read_write: vec![].try_into().unwrap(),
1571                },
1572                instructions: 2000,
1573                disk_read_bytes: 200,
1574                write_bytes: 100,
1575            },
1576            resource_fee: 75000, // The resource fee we expect to extract
1577        };
1578
1579        let inner_tx = Transaction {
1580            source_account: MuxedAccount::Ed25519(Uint256(source_pk.0)),
1581            fee: 100,
1582            seq_num: SequenceNumber(42),
1583            cond: soroban_rs::xdr::Preconditions::None,
1584            memo: Memo::None,
1585            operations: VecM::default(),
1586            ext: soroban_rs::xdr::TransactionExt::V1(soroban_data),
1587        };
1588
1589        // Add a signature to the inner transaction
1590        let sig = DecoratedSignature {
1591            hint: SignatureHint([1, 2, 3, 4]),
1592            signature: Signature(vec![5u8; 64].try_into().unwrap()),
1593        };
1594
1595        let inner_envelope = TransactionV1Envelope {
1596            tx: inner_tx,
1597            signatures: vec![sig].try_into().unwrap(),
1598        };
1599
1600        // Create fee-bump envelope
1601        let fee_bump_tx = FeeBumpTransaction {
1602            fee_source: MuxedAccount::Ed25519(Uint256(fee_source_pk.0)),
1603            fee: 10_000_000,
1604            inner_tx: FeeBumpTransactionInnerTx::Tx(inner_envelope),
1605            ext: soroban_rs::xdr::FeeBumpTransactionExt::V0,
1606        };
1607
1608        let fee_bump_envelope = TransactionEnvelope::TxFeeBump(FeeBumpTransactionEnvelope {
1609            tx: fee_bump_tx,
1610            signatures: vec![].try_into().unwrap(),
1611        });
1612
1613        let result = extract_soroban_resource_fee(&fee_bump_envelope);
1614        assert_eq!(result, Some(75000));
1615    }
1616
1617    #[test]
1618    fn test_extract_soroban_resource_fee_from_v0_envelope() {
1619        use soroban_rs::xdr::{
1620            Memo, Operation, OperationBody, PaymentOp, SequenceNumber, TransactionV0,
1621            TransactionV0Envelope,
1622        };
1623        use stellar_strkey::ed25519::PublicKey;
1624
1625        let source_pk =
1626            PublicKey::from_string("GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF")
1627                .unwrap();
1628        let dest_pk =
1629            PublicKey::from_string("GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ")
1630                .unwrap();
1631
1632        let payment_op = Operation {
1633            source_account: None,
1634            body: OperationBody::Payment(PaymentOp {
1635                destination: MuxedAccount::Ed25519(Uint256(dest_pk.0)),
1636                asset: soroban_rs::xdr::Asset::Native,
1637                amount: 1000000,
1638            }),
1639        };
1640
1641        let operations: VecM<Operation, 100> = vec![payment_op].try_into().unwrap();
1642
1643        let tx_v0 = TransactionV0 {
1644            source_account_ed25519: Uint256(source_pk.0),
1645            fee: 100,
1646            seq_num: SequenceNumber(42),
1647            time_bounds: None,
1648            memo: Memo::None,
1649            operations,
1650            ext: soroban_rs::xdr::TransactionV0Ext::V0,
1651        };
1652
1653        let envelope = TransactionEnvelope::TxV0(TransactionV0Envelope {
1654            tx: tx_v0,
1655            signatures: vec![].try_into().unwrap(),
1656        });
1657
1658        // V0 envelopes don't support Soroban, so should return None
1659        let result = extract_soroban_resource_fee(&envelope);
1660        assert_eq!(result, None);
1661    }
1662}