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::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
173pub fn string_to_muxed_account(address: &str) -> Result<MuxedAccount> {
174    let pk =
175        PublicKey::from_string(address).map_err(|e| eyre!("Failed to decode account ID: {}", e))?;
176
177    let key = Uint256(pk.0);
178    Ok(MuxedAccount::Ed25519(key))
179}
180
181/// Extract operations from a transaction envelope
182pub fn extract_operations(envelope: &TransactionEnvelope) -> Result<&VecM<Operation, 100>> {
183    match envelope {
184        TransactionEnvelope::TxV0(e) => Ok(&e.tx.operations),
185        TransactionEnvelope::Tx(e) => Ok(&e.tx.operations),
186        TransactionEnvelope::TxFeeBump(e) => {
187            // For fee-bump transactions, extract operations from inner transaction
188            match &e.tx.inner_tx {
189                FeeBumpTransactionInnerTx::Tx(inner) => Ok(&inner.tx.operations),
190            }
191        }
192    }
193}
194
195/// Check if a transaction envelope contains operations that require simulation
196pub fn xdr_needs_simulation(envelope: &TransactionEnvelope) -> Result<bool> {
197    let operations = extract_operations(envelope)?;
198
199    // Check if any operation is a Soroban operation
200    for op in operations.iter() {
201        if matches!(op.body, OperationBody::InvokeHostFunction(_)) {
202            return Ok(true);
203        }
204    }
205
206    Ok(false)
207}
208
209/// Attach signatures to a transaction envelope
210/// This function handles all envelope types (V0, V1, and FeeBump)
211pub fn attach_signatures_to_envelope(
212    envelope: &mut TransactionEnvelope,
213    signatures: Vec<DecoratedSignature>,
214) -> Result<()> {
215    let signatures_vec: VecM<DecoratedSignature, 20> = signatures
216        .try_into()
217        .map_err(|_| eyre!("Too many signatures (max 20)"))?;
218
219    match envelope {
220        TransactionEnvelope::TxV0(ref mut v0_env) => {
221            v0_env.signatures = signatures_vec;
222        }
223        TransactionEnvelope::Tx(ref mut v1_env) => {
224            v1_env.signatures = signatures_vec;
225        }
226        TransactionEnvelope::TxFeeBump(ref mut fb_env) => {
227            fb_env.signatures = signatures_vec;
228        }
229    }
230
231    Ok(())
232}
233
234/// Convert a V0 transaction envelope to V1 format
235/// This is required for fee-bump transactions as they only support V1 inner transactions
236fn convert_v0_to_v1_envelope(
237    v0_envelope: soroban_rs::xdr::TransactionV0Envelope,
238) -> TransactionV1Envelope {
239    let v0_tx = &v0_envelope.tx;
240    let source_bytes: [u8; 32] = v0_tx.source_account_ed25519.0;
241
242    // Create V1 transaction from V0 data
243    let tx = soroban_rs::xdr::Transaction {
244        source_account: MuxedAccount::Ed25519(Uint256(source_bytes)),
245        fee: v0_tx.fee,
246        seq_num: v0_tx.seq_num.clone(),
247        cond: match v0_tx.time_bounds.clone() {
248            Some(tb) => soroban_rs::xdr::Preconditions::Time(tb),
249            None => soroban_rs::xdr::Preconditions::None,
250        },
251        memo: v0_tx.memo.clone(),
252        operations: v0_tx.operations.clone(),
253        ext: soroban_rs::xdr::TransactionExt::V0,
254    };
255
256    // Create V1 envelope with V0 signatures
257    TransactionV1Envelope {
258        tx,
259        signatures: v0_envelope.signatures.clone(),
260    }
261}
262
263/// Update the sequence number in an XDR envelope
264pub fn update_xdr_sequence(envelope: &mut TransactionEnvelope, sequence: i64) -> Result<()> {
265    match envelope {
266        TransactionEnvelope::TxV0(ref mut e) => {
267            e.tx.seq_num = soroban_rs::xdr::SequenceNumber(sequence);
268        }
269        TransactionEnvelope::Tx(ref mut e) => {
270            e.tx.seq_num = soroban_rs::xdr::SequenceNumber(sequence);
271        }
272        TransactionEnvelope::TxFeeBump(_) => {
273            return Err(eyre!("Cannot set sequence number on fee-bump transaction"));
274        }
275    }
276    Ok(())
277}
278
279/// Update the fee in an XDR envelope
280pub fn update_xdr_fee(envelope: &mut TransactionEnvelope, fee: u32) -> Result<()> {
281    match envelope {
282        TransactionEnvelope::TxV0(ref mut e) => {
283            e.tx.fee = fee;
284        }
285        TransactionEnvelope::Tx(ref mut e) => {
286            e.tx.fee = fee;
287        }
288        TransactionEnvelope::TxFeeBump(_) => {
289            return Err(eyre!(
290                "Cannot set fee on fee-bump transaction - use max_fee instead"
291            ));
292        }
293    }
294    Ok(())
295}
296
297#[cfg(test)]
298mod tests {
299    use super::*;
300    use soroban_rs::xdr::{
301        Asset, BytesM, DecoratedSignature, FeeBumpTransactionInnerTx, HostFunction,
302        InvokeContractArgs, InvokeHostFunctionOp, Limits, Memo, MuxedAccount, Operation,
303        OperationBody, PaymentOp, Preconditions, SequenceNumber, Signature, SignatureHint,
304        Transaction, TransactionEnvelope, TransactionExt, TransactionV0, TransactionV0Envelope,
305        TransactionV1Envelope, Uint256, VecM, WriteXdr,
306    };
307    use stellar_strkey::ed25519::PublicKey;
308
309    // Helper function to create test XDR
310    fn create_test_transaction_xdr(include_signature: bool) -> String {
311        // Create a test account public key
312        let source_pk =
313            PublicKey::from_string("GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF")
314                .unwrap();
315        let dest_pk =
316            PublicKey::from_string("GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ")
317                .unwrap();
318
319        // Create a payment operation
320        let payment_op = PaymentOp {
321            destination: MuxedAccount::Ed25519(Uint256(dest_pk.0)),
322            asset: Asset::Native,
323            amount: 1000000, // 0.1 XLM
324        };
325
326        let operation = Operation {
327            source_account: None,
328            body: OperationBody::Payment(payment_op),
329        };
330
331        let operations: VecM<Operation, 100> = vec![operation].try_into().unwrap();
332
333        // Create the transaction
334        let tx = Transaction {
335            source_account: MuxedAccount::Ed25519(Uint256(source_pk.0)),
336            fee: 100,
337            seq_num: SequenceNumber(1),
338            cond: Preconditions::None,
339            memo: Memo::None,
340            operations,
341            ext: TransactionExt::V0,
342        };
343
344        // Create the envelope
345        let mut envelope = TransactionV1Envelope {
346            tx,
347            signatures: vec![].try_into().unwrap(),
348        };
349
350        if include_signature {
351            // Add a dummy signature
352            let hint = SignatureHint([0; 4]);
353            let sig_bytes: Vec<u8> = vec![0u8; 64];
354            let sig_bytes_m: BytesM<64> = sig_bytes.try_into().unwrap();
355            let sig = DecoratedSignature {
356                hint,
357                signature: Signature(sig_bytes_m),
358            };
359            envelope.signatures = vec![sig].try_into().unwrap();
360        }
361
362        let tx_envelope = TransactionEnvelope::Tx(envelope);
363        tx_envelope.to_xdr_base64(Limits::none()).unwrap()
364    }
365
366    // Helper to get test XDR
367    fn get_unsigned_xdr() -> String {
368        create_test_transaction_xdr(false)
369    }
370
371    fn get_signed_xdr() -> String {
372        create_test_transaction_xdr(true)
373    }
374
375    const INVALID_XDR: &str = "INVALID_BASE64_XDR_DATA";
376
377    #[test]
378    fn test_parse_unsigned_xdr() {
379        // This test should parse an unsigned transaction XDR successfully
380        let unsigned_xdr = get_unsigned_xdr();
381        let result = parse_transaction_xdr(&unsigned_xdr, false);
382        assert!(result.is_ok(), "Failed to parse unsigned XDR");
383
384        let envelope = result.unwrap();
385        assert!(
386            !is_signed(&envelope),
387            "Unsigned XDR should not have signatures"
388        );
389    }
390
391    #[test]
392    fn test_parse_signed_xdr() {
393        // This test should parse a signed transaction XDR successfully
394        let signed_xdr = get_signed_xdr();
395        let result = parse_transaction_xdr(&signed_xdr, true);
396        assert!(result.is_ok(), "Failed to parse signed XDR");
397
398        let envelope = result.unwrap();
399        assert!(is_signed(&envelope), "Signed XDR should have signatures");
400    }
401
402    #[test]
403    fn test_parse_invalid_xdr() {
404        // This test should fail when parsing invalid XDR
405        let result = parse_transaction_xdr(INVALID_XDR, false);
406        assert!(result.is_err(), "Should fail to parse invalid XDR");
407    }
408
409    #[test]
410    fn test_validate_unsigned_xdr_expecting_signed() {
411        // This test should fail when unsigned XDR is provided but signed is expected
412        let unsigned_xdr = get_unsigned_xdr();
413        let result = parse_transaction_xdr(&unsigned_xdr, true);
414        assert!(
415            result.is_err(),
416            "Should fail when expecting signed but got unsigned"
417        );
418    }
419
420    #[test]
421    fn test_extract_source_account_from_xdr() {
422        // This test should extract the source account from the transaction
423        let unsigned_xdr = get_unsigned_xdr();
424        let envelope = parse_transaction_xdr(&unsigned_xdr, false).unwrap();
425        let source_account = extract_source_account(&envelope).unwrap();
426        assert!(!source_account.is_empty(), "Should extract source account");
427        assert_eq!(
428            source_account,
429            "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF"
430        );
431    }
432
433    #[test]
434    fn test_validate_source_account() {
435        // This test should validate that the source account matches expected
436        let unsigned_xdr = get_unsigned_xdr();
437        let envelope = parse_transaction_xdr(&unsigned_xdr, false).unwrap();
438        let source_account = extract_source_account(&envelope).unwrap();
439
440        // This should pass
441        let result = validate_source_account(&envelope, &source_account);
442        assert!(result.is_ok(), "Should validate matching source account");
443
444        // This should fail
445        let result = validate_source_account(&envelope, "DIFFERENT_ACCOUNT");
446        assert!(
447            result.is_err(),
448            "Should fail with non-matching source account"
449        );
450    }
451
452    #[test]
453    fn test_build_fee_bump_envelope() {
454        // This test should create a fee-bump transaction from a signed inner transaction
455        let signed_xdr = get_signed_xdr();
456        let inner_envelope = parse_transaction_xdr(&signed_xdr, true).unwrap();
457        let fee_source = "GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ";
458        let max_fee = 10_000_000; // 1 XLM
459
460        let result = build_fee_bump_envelope(inner_envelope, fee_source, max_fee);
461        assert!(result.is_ok(), "Should build fee-bump envelope");
462
463        let fee_bump_envelope = result.unwrap();
464        assert!(
465            is_fee_bump(&fee_bump_envelope),
466            "Should be a fee-bump transaction"
467        );
468    }
469
470    #[test]
471    fn test_fee_bump_requires_different_source() {
472        // This test should fail when trying to fee-bump with same source as inner tx
473        let signed_xdr = get_signed_xdr();
474        let inner_envelope = parse_transaction_xdr(&signed_xdr, true).unwrap();
475        let inner_source = extract_source_account(&inner_envelope).unwrap();
476        let max_fee = 10_000_000;
477
478        let result = build_fee_bump_envelope(inner_envelope, &inner_source, max_fee);
479        assert!(
480            result.is_err(),
481            "Should fail when fee-bump source equals inner source"
482        );
483    }
484
485    #[test]
486    fn test_extract_inner_transaction_hash() {
487        // This test should extract the hash of the inner transaction from a fee-bump
488        let signed_xdr = get_signed_xdr();
489        let inner_envelope = parse_transaction_xdr(&signed_xdr, true).unwrap();
490        let fee_source = "GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ";
491        let fee_bump_envelope =
492            build_fee_bump_envelope(inner_envelope.clone(), fee_source, 10_000_000).unwrap();
493
494        let inner_hash = extract_inner_transaction_hash(&fee_bump_envelope).unwrap();
495        assert!(
496            !inner_hash.is_empty(),
497            "Should extract inner transaction hash"
498        );
499    }
500
501    #[test]
502    fn test_extract_operations_from_v1_envelope() {
503        // Test extracting operations from a V1 envelope
504        let envelope = create_test_transaction_xdr(false);
505        let parsed = TransactionEnvelope::from_xdr_base64(envelope, Limits::none()).unwrap();
506
507        let operations = extract_operations(&parsed).unwrap();
508        assert_eq!(operations.len(), 1, "Should extract 1 operation");
509
510        // Verify the operation details
511        if let OperationBody::Payment(payment) = &operations[0].body {
512            assert_eq!(payment.amount, 1000000, "Payment amount should be 0.1 XLM");
513        } else {
514            panic!("Expected payment operation");
515        }
516    }
517
518    #[test]
519    fn test_extract_operations_from_v0_envelope() {
520        // Test extracting operations from a V0 envelope
521        let source_pk =
522            PublicKey::from_string("GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF")
523                .unwrap();
524        let dest_pk =
525            PublicKey::from_string("GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ")
526                .unwrap();
527
528        let payment_op = PaymentOp {
529            destination: MuxedAccount::Ed25519(Uint256(dest_pk.0)),
530            asset: Asset::Native,
531            amount: 2000000, // 0.2 XLM
532        };
533
534        let operation = Operation {
535            source_account: None,
536            body: OperationBody::Payment(payment_op),
537        };
538
539        let operations: VecM<Operation, 100> = vec![operation].try_into().unwrap();
540
541        // Create V0 transaction
542        let tx_v0 = TransactionV0 {
543            source_account_ed25519: Uint256(source_pk.0),
544            fee: 100,
545            seq_num: SequenceNumber(1),
546            time_bounds: None,
547            memo: Memo::None,
548            operations,
549            ext: soroban_rs::xdr::TransactionV0Ext::V0,
550        };
551
552        let envelope = TransactionEnvelope::TxV0(TransactionV0Envelope {
553            tx: tx_v0,
554            signatures: vec![].try_into().unwrap(),
555        });
556
557        let operations = extract_operations(&envelope).unwrap();
558        assert_eq!(operations.len(), 1, "Should extract 1 operation from V0");
559
560        if let OperationBody::Payment(payment) = &operations[0].body {
561            assert_eq!(payment.amount, 2000000, "Payment amount should be 0.2 XLM");
562        } else {
563            panic!("Expected payment operation");
564        }
565    }
566
567    #[test]
568    fn test_extract_operations_from_fee_bump() {
569        // Test extracting operations from a fee-bump envelope
570        let signed_xdr = get_signed_xdr();
571        let inner_envelope = parse_transaction_xdr(&signed_xdr, true).unwrap();
572        let fee_source = "GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ";
573        let fee_bump_envelope =
574            build_fee_bump_envelope(inner_envelope, fee_source, 10_000_000).unwrap();
575
576        let operations = extract_operations(&fee_bump_envelope).unwrap();
577        assert_eq!(
578            operations.len(),
579            1,
580            "Should extract operations from inner tx"
581        );
582
583        if let OperationBody::Payment(payment) = &operations[0].body {
584            assert_eq!(payment.amount, 1000000, "Payment amount should be 0.1 XLM");
585        } else {
586            panic!("Expected payment operation");
587        }
588    }
589
590    #[test]
591    fn test_xdr_needs_simulation_with_soroban_operation() {
592        // Test that Soroban operations require simulation
593        let source_pk =
594            PublicKey::from_string("GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF")
595                .unwrap();
596
597        // Create a Soroban InvokeHostFunction operation
598        let invoke_op = InvokeHostFunctionOp {
599            host_function: HostFunction::InvokeContract(InvokeContractArgs {
600                contract_address: soroban_rs::xdr::ScAddress::Contract(
601                    soroban_rs::xdr::ContractId(soroban_rs::xdr::Hash([0u8; 32])),
602                ),
603                function_name: "test".try_into().unwrap(),
604                args: vec![].try_into().unwrap(),
605            }),
606            auth: vec![].try_into().unwrap(),
607        };
608
609        let operation = Operation {
610            source_account: None,
611            body: OperationBody::InvokeHostFunction(invoke_op),
612        };
613
614        let operations: VecM<Operation, 100> = vec![operation].try_into().unwrap();
615
616        let tx = Transaction {
617            source_account: MuxedAccount::Ed25519(Uint256(source_pk.0)),
618            fee: 100,
619            seq_num: SequenceNumber(1),
620            cond: Preconditions::None,
621            memo: Memo::None,
622            operations,
623            ext: TransactionExt::V0,
624        };
625
626        let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
627            tx,
628            signatures: vec![].try_into().unwrap(),
629        });
630
631        let needs_sim = xdr_needs_simulation(&envelope).unwrap();
632        assert!(needs_sim, "Soroban operations should require simulation");
633    }
634
635    #[test]
636    fn test_xdr_needs_simulation_without_soroban() {
637        // Test that non-Soroban operations don't require simulation
638        let envelope = create_test_transaction_xdr(false);
639        let parsed = TransactionEnvelope::from_xdr_base64(envelope, Limits::none()).unwrap();
640
641        let needs_sim = xdr_needs_simulation(&parsed).unwrap();
642        assert!(
643            !needs_sim,
644            "Payment operations should not require simulation"
645        );
646    }
647
648    #[test]
649    fn test_xdr_needs_simulation_with_multiple_operations() {
650        // Test with multiple operations where at least one is Soroban
651        let source_pk =
652            PublicKey::from_string("GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF")
653                .unwrap();
654        let dest_pk =
655            PublicKey::from_string("GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ")
656                .unwrap();
657
658        // Create a payment operation
659        let payment_op = Operation {
660            source_account: None,
661            body: OperationBody::Payment(PaymentOp {
662                destination: MuxedAccount::Ed25519(Uint256(dest_pk.0)),
663                asset: Asset::Native,
664                amount: 1000000,
665            }),
666        };
667
668        // Create a Soroban operation
669        let soroban_op = Operation {
670            source_account: None,
671            body: OperationBody::InvokeHostFunction(InvokeHostFunctionOp {
672                host_function: HostFunction::InvokeContract(InvokeContractArgs {
673                    contract_address: soroban_rs::xdr::ScAddress::Contract(
674                        soroban_rs::xdr::ContractId(soroban_rs::xdr::Hash([0u8; 32])),
675                    ),
676                    function_name: "test".try_into().unwrap(),
677                    args: vec![].try_into().unwrap(),
678                }),
679                auth: vec![].try_into().unwrap(),
680            }),
681        };
682
683        let operations: VecM<Operation, 100> = vec![payment_op, soroban_op].try_into().unwrap();
684
685        let tx = Transaction {
686            source_account: MuxedAccount::Ed25519(Uint256(source_pk.0)),
687            fee: 100,
688            seq_num: SequenceNumber(1),
689            cond: Preconditions::None,
690            memo: Memo::None,
691            operations,
692            ext: TransactionExt::V0,
693        };
694
695        let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
696            tx,
697            signatures: vec![].try_into().unwrap(),
698        });
699
700        let needs_sim = xdr_needs_simulation(&envelope).unwrap();
701        assert!(
702            needs_sim,
703            "Should require simulation when any operation is Soroban"
704        );
705    }
706
707    #[test]
708    fn test_calculate_transaction_hash() {
709        // Test transaction hash calculation
710        let envelope_xdr = get_signed_xdr();
711        let envelope = parse_transaction_xdr(&envelope_xdr, true).unwrap();
712
713        let hash1 = calculate_transaction_hash(&envelope).unwrap();
714        let hash2 = calculate_transaction_hash(&envelope).unwrap();
715
716        // Hash should be deterministic
717        assert_eq!(hash1, hash2, "Hash should be deterministic");
718        assert_eq!(hash1.len(), 64, "SHA256 hash should be 64 hex characters");
719
720        // Verify it's valid hex
721        assert!(
722            hash1.chars().all(|c| c.is_ascii_hexdigit()),
723            "Hash should be valid hex"
724        );
725    }
726
727    #[test]
728    fn test_muxed_account_conversion() {
729        let address = "GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ";
730        let muxed = string_to_muxed_account(address).unwrap();
731        let back = muxed_account_to_string(&muxed).unwrap();
732        assert_eq!(address, back);
733    }
734
735    #[test]
736    fn test_muxed_account_ed25519_variant() {
737        // Test handling of regular Ed25519 accounts
738        let address = "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF";
739        let muxed = string_to_muxed_account(address).unwrap();
740
741        match muxed {
742            MuxedAccount::Ed25519(_) => (),
743            _ => panic!("Expected Ed25519 variant"),
744        }
745
746        let back = muxed_account_to_string(&muxed).unwrap();
747        assert_eq!(address, back);
748    }
749
750    #[test]
751    fn test_muxed_account_muxed_ed25519_variant() {
752        // Test handling of MuxedEd25519 accounts
753        let pk = PublicKey::from_string("GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF")
754            .unwrap();
755
756        let muxed = MuxedAccount::MuxedEd25519(soroban_rs::xdr::MuxedAccountMed25519 {
757            id: 123456789,
758            ed25519: Uint256(pk.0),
759        });
760
761        let address = muxed_account_to_string(&muxed).unwrap();
762        assert_eq!(
763            address,
764            "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF"
765        );
766    }
767
768    #[test]
769    fn test_v0_to_v1_conversion_in_fee_bump() {
770        // Test the V0 to V1 conversion logic in build_fee_bump_envelope
771        let source_pk =
772            PublicKey::from_string("GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF")
773                .unwrap();
774        let dest_pk =
775            PublicKey::from_string("GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ")
776                .unwrap();
777
778        // Create V0 transaction with time bounds
779        let time_bounds = soroban_rs::xdr::TimeBounds {
780            min_time: soroban_rs::xdr::TimePoint(1000),
781            max_time: soroban_rs::xdr::TimePoint(2000),
782        };
783
784        let payment_op = Operation {
785            source_account: None,
786            body: OperationBody::Payment(PaymentOp {
787                destination: MuxedAccount::Ed25519(Uint256(dest_pk.0)),
788                asset: Asset::Native,
789                amount: 3000000,
790            }),
791        };
792
793        let operations: VecM<Operation, 100> = vec![payment_op].try_into().unwrap();
794
795        let tx_v0 = TransactionV0 {
796            source_account_ed25519: Uint256(source_pk.0),
797            fee: 200,
798            seq_num: SequenceNumber(42),
799            time_bounds: Some(time_bounds.clone()),
800            memo: Memo::Text("Test memo".as_bytes().to_vec().try_into().unwrap()),
801            operations: operations.clone(),
802            ext: soroban_rs::xdr::TransactionV0Ext::V0,
803        };
804
805        // Add a signature to V0 envelope
806        let sig = DecoratedSignature {
807            hint: SignatureHint([1, 2, 3, 4]),
808            signature: Signature(vec![5u8; 64].try_into().unwrap()),
809        };
810
811        let v0_envelope = TransactionEnvelope::TxV0(TransactionV0Envelope {
812            tx: tx_v0,
813            signatures: vec![sig.clone()].try_into().unwrap(),
814        });
815
816        // Build fee-bump from V0 envelope
817        let fee_source = "GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ";
818        let fee_bump_envelope =
819            build_fee_bump_envelope(v0_envelope, fee_source, 50_000_000).unwrap();
820
821        // Verify it's a fee-bump envelope
822        assert!(matches!(
823            fee_bump_envelope,
824            TransactionEnvelope::TxFeeBump(_)
825        ));
826
827        if let TransactionEnvelope::TxFeeBump(fb_env) = fee_bump_envelope {
828            // Verify fee source
829            let fb_source = muxed_account_to_string(&fb_env.tx.fee_source).unwrap();
830            assert_eq!(fb_source, fee_source);
831            assert_eq!(fb_env.tx.fee, 50_000_000);
832
833            // Verify inner transaction was properly converted
834            let FeeBumpTransactionInnerTx::Tx(inner_v1) = &fb_env.tx.inner_tx;
835            // Check that V0 data was preserved in V1 format
836            assert_eq!(inner_v1.tx.fee, 200);
837            assert_eq!(inner_v1.tx.seq_num.0, 42);
838
839            // Check time bounds conversion
840            if let Preconditions::Time(tb) = &inner_v1.tx.cond {
841                assert_eq!(tb.min_time.0, 1000);
842                assert_eq!(tb.max_time.0, 2000);
843            } else {
844                panic!("Expected time bounds in preconditions");
845            }
846
847            // Check memo preservation
848            if let Memo::Text(text) = &inner_v1.tx.memo {
849                assert_eq!(text.as_slice(), "Test memo".as_bytes());
850            } else {
851                panic!("Expected text memo");
852            }
853
854            // Check operations preservation
855            assert_eq!(inner_v1.tx.operations.len(), 1);
856            // Check signatures were preserved
857            assert_eq!(inner_v1.signatures.len(), 1);
858            assert_eq!(inner_v1.signatures[0].hint, sig.hint);
859        }
860    }
861
862    #[test]
863    fn test_attach_signatures_to_envelope() {
864        use soroban_rs::xdr::{
865            DecoratedSignature, Memo, Operation, OperationBody, PaymentOp, SequenceNumber,
866            Signature, SignatureHint, TransactionV0, TransactionV0Envelope,
867        };
868        use stellar_strkey::ed25519::PublicKey;
869
870        let source_pk =
871            PublicKey::from_string("GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF")
872                .unwrap();
873        let dest_pk =
874            PublicKey::from_string("GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ")
875                .unwrap();
876
877        // Create a test transaction
878        let payment_op = Operation {
879            source_account: None,
880            body: OperationBody::Payment(PaymentOp {
881                destination: MuxedAccount::Ed25519(Uint256(dest_pk.0)),
882                asset: soroban_rs::xdr::Asset::Native,
883                amount: 1000000,
884            }),
885        };
886
887        let operations: VecM<Operation, 100> = vec![payment_op].try_into().unwrap();
888
889        let tx_v0 = TransactionV0 {
890            source_account_ed25519: Uint256(source_pk.0),
891            fee: 100,
892            seq_num: SequenceNumber(42),
893            time_bounds: None,
894            memo: Memo::None,
895            operations,
896            ext: soroban_rs::xdr::TransactionV0Ext::V0,
897        };
898
899        let mut envelope = TransactionEnvelope::TxV0(TransactionV0Envelope {
900            tx: tx_v0,
901            signatures: vec![].try_into().unwrap(),
902        });
903
904        // Create test signatures
905        let sig1 = DecoratedSignature {
906            hint: SignatureHint([1, 2, 3, 4]),
907            signature: Signature(vec![1u8; 64].try_into().unwrap()),
908        };
909        let sig2 = DecoratedSignature {
910            hint: SignatureHint([5, 6, 7, 8]),
911            signature: Signature(vec![2u8; 64].try_into().unwrap()),
912        };
913
914        // Attach signatures
915        let result = attach_signatures_to_envelope(&mut envelope, vec![sig1, sig2]);
916        assert!(result.is_ok());
917
918        // Verify signatures were attached
919        match &envelope {
920            TransactionEnvelope::TxV0(e) => {
921                assert_eq!(e.signatures.len(), 2);
922                assert_eq!(e.signatures[0].hint.0, [1, 2, 3, 4]);
923                assert_eq!(e.signatures[1].hint.0, [5, 6, 7, 8]);
924            }
925            _ => panic!("Expected V0 envelope"),
926        }
927    }
928
929    #[test]
930    fn test_extract_operations() {
931        use soroban_rs::xdr::{
932            Memo, Operation, OperationBody, PaymentOp, SequenceNumber, Transaction, TransactionV0,
933            TransactionV0Envelope, TransactionV1Envelope,
934        };
935        use stellar_strkey::ed25519::PublicKey;
936
937        let source_pk =
938            PublicKey::from_string("GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF")
939                .unwrap();
940        let dest_pk =
941            PublicKey::from_string("GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ")
942                .unwrap();
943
944        // Create test operation
945        let payment_op = Operation {
946            source_account: None,
947            body: OperationBody::Payment(PaymentOp {
948                destination: MuxedAccount::Ed25519(Uint256(dest_pk.0)),
949                asset: soroban_rs::xdr::Asset::Native,
950                amount: 1000000,
951            }),
952        };
953
954        let operations: VecM<Operation, 100> = vec![payment_op.clone()].try_into().unwrap();
955
956        // Test V0 envelope
957        let tx_v0 = TransactionV0 {
958            source_account_ed25519: Uint256(source_pk.0),
959            fee: 100,
960            seq_num: SequenceNumber(42),
961            time_bounds: None,
962            memo: Memo::None,
963            operations: operations.clone(),
964            ext: soroban_rs::xdr::TransactionV0Ext::V0,
965        };
966
967        let v0_envelope = TransactionEnvelope::TxV0(TransactionV0Envelope {
968            tx: tx_v0,
969            signatures: vec![].try_into().unwrap(),
970        });
971
972        let extracted_ops = extract_operations(&v0_envelope).unwrap();
973        assert_eq!(extracted_ops.len(), 1);
974
975        // Test V1 envelope
976        let tx_v1 = Transaction {
977            source_account: MuxedAccount::Ed25519(Uint256(source_pk.0)),
978            fee: 100,
979            seq_num: SequenceNumber(42),
980            cond: soroban_rs::xdr::Preconditions::None,
981            memo: Memo::None,
982            operations: operations.clone(),
983            ext: soroban_rs::xdr::TransactionExt::V0,
984        };
985
986        let v1_envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
987            tx: tx_v1,
988            signatures: vec![].try_into().unwrap(),
989        });
990
991        let extracted_ops = extract_operations(&v1_envelope).unwrap();
992        assert_eq!(extracted_ops.len(), 1);
993    }
994
995    #[test]
996    fn test_xdr_needs_simulation() {
997        use soroban_rs::xdr::{
998            HostFunction, InvokeHostFunctionOp, Memo, Operation, OperationBody, PaymentOp,
999            ScSymbol, ScVal, SequenceNumber, Transaction, TransactionV1Envelope,
1000        };
1001        use stellar_strkey::ed25519::PublicKey;
1002
1003        let source_pk =
1004            PublicKey::from_string("GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF")
1005                .unwrap();
1006        let dest_pk =
1007            PublicKey::from_string("GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ")
1008                .unwrap();
1009
1010        // Test with payment operation (should not need simulation)
1011        let payment_op = Operation {
1012            source_account: None,
1013            body: OperationBody::Payment(PaymentOp {
1014                destination: MuxedAccount::Ed25519(Uint256(dest_pk.0)),
1015                asset: soroban_rs::xdr::Asset::Native,
1016                amount: 1000000,
1017            }),
1018        };
1019
1020        let operations: VecM<Operation, 100> = vec![payment_op].try_into().unwrap();
1021
1022        let tx = Transaction {
1023            source_account: MuxedAccount::Ed25519(Uint256(source_pk.0)),
1024            fee: 100,
1025            seq_num: SequenceNumber(42),
1026            cond: soroban_rs::xdr::Preconditions::None,
1027            memo: Memo::None,
1028            operations,
1029            ext: soroban_rs::xdr::TransactionExt::V0,
1030        };
1031
1032        let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
1033            tx,
1034            signatures: vec![].try_into().unwrap(),
1035        });
1036
1037        assert!(!xdr_needs_simulation(&envelope).unwrap());
1038
1039        // Test with InvokeHostFunction operation (should need simulation)
1040        let invoke_op = Operation {
1041            source_account: None,
1042            body: OperationBody::InvokeHostFunction(InvokeHostFunctionOp {
1043                host_function: HostFunction::InvokeContract(soroban_rs::xdr::InvokeContractArgs {
1044                    contract_address: soroban_rs::xdr::ScAddress::Contract(
1045                        soroban_rs::xdr::ContractId(soroban_rs::xdr::Hash([0u8; 32])),
1046                    ),
1047                    function_name: ScSymbol("test".try_into().unwrap()),
1048                    args: vec![ScVal::U32(42)].try_into().unwrap(),
1049                }),
1050                auth: vec![].try_into().unwrap(),
1051            }),
1052        };
1053
1054        let operations: VecM<Operation, 100> = vec![invoke_op].try_into().unwrap();
1055
1056        let tx = Transaction {
1057            source_account: MuxedAccount::Ed25519(Uint256(source_pk.0)),
1058            fee: 100,
1059            seq_num: SequenceNumber(42),
1060            cond: soroban_rs::xdr::Preconditions::None,
1061            memo: Memo::None,
1062            operations,
1063            ext: soroban_rs::xdr::TransactionExt::V0,
1064        };
1065
1066        let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
1067            tx,
1068            signatures: vec![].try_into().unwrap(),
1069        });
1070
1071        assert!(xdr_needs_simulation(&envelope).unwrap());
1072    }
1073
1074    #[test]
1075    fn test_v0_to_v1_conversion() {
1076        use soroban_rs::xdr::{
1077            Memo, Operation, OperationBody, PaymentOp, SequenceNumber, TimeBounds, TimePoint,
1078            TransactionV0, TransactionV0Envelope,
1079        };
1080        use stellar_strkey::ed25519::PublicKey;
1081
1082        let source_pk =
1083            PublicKey::from_string("GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF")
1084                .unwrap();
1085        let dest_pk =
1086            PublicKey::from_string("GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ")
1087                .unwrap();
1088
1089        // Create test V0 transaction with various fields
1090        let time_bounds = TimeBounds {
1091            min_time: TimePoint(1000),
1092            max_time: TimePoint(2000),
1093        };
1094
1095        let payment_op = Operation {
1096            source_account: None,
1097            body: OperationBody::Payment(PaymentOp {
1098                destination: MuxedAccount::Ed25519(Uint256(dest_pk.0)),
1099                asset: soroban_rs::xdr::Asset::Native,
1100                amount: 1000000,
1101            }),
1102        };
1103
1104        let operations: VecM<Operation, 100> = vec![payment_op].try_into().unwrap();
1105
1106        let tx_v0 = TransactionV0 {
1107            source_account_ed25519: Uint256(source_pk.0),
1108            fee: 100,
1109            seq_num: SequenceNumber(42),
1110            time_bounds: Some(time_bounds.clone()),
1111            memo: Memo::Text("Test".as_bytes().to_vec().try_into().unwrap()),
1112            operations: operations.clone(),
1113            ext: soroban_rs::xdr::TransactionV0Ext::V0,
1114        };
1115
1116        let sig = soroban_rs::xdr::DecoratedSignature {
1117            hint: soroban_rs::xdr::SignatureHint([1, 2, 3, 4]),
1118            signature: soroban_rs::xdr::Signature(vec![0u8; 64].try_into().unwrap()),
1119        };
1120
1121        let v0_envelope = TransactionV0Envelope {
1122            tx: tx_v0,
1123            signatures: vec![sig.clone()].try_into().unwrap(),
1124        };
1125
1126        // Convert to V1
1127        let v1_envelope = convert_v0_to_v1_envelope(v0_envelope);
1128
1129        // Verify conversion preserved all data
1130        assert_eq!(v1_envelope.tx.fee, 100);
1131        assert_eq!(v1_envelope.tx.seq_num.0, 42);
1132        assert_eq!(v1_envelope.tx.operations.len(), 1);
1133        assert_eq!(v1_envelope.signatures.len(), 1);
1134
1135        // Check source account conversion
1136        if let MuxedAccount::Ed25519(key) = &v1_envelope.tx.source_account {
1137            assert_eq!(key.0, source_pk.0);
1138        } else {
1139            panic!("Expected Ed25519 source account");
1140        }
1141
1142        // Check time bounds conversion
1143        if let soroban_rs::xdr::Preconditions::Time(tb) = &v1_envelope.tx.cond {
1144            assert_eq!(tb.min_time.0, 1000);
1145            assert_eq!(tb.max_time.0, 2000);
1146        } else {
1147            panic!("Expected time bounds in preconditions");
1148        }
1149
1150        // Check memo preservation
1151        if let Memo::Text(text) = &v1_envelope.tx.memo {
1152            assert_eq!(text.as_slice(), "Test".as_bytes());
1153        } else {
1154            panic!("Expected text memo");
1155        }
1156    }
1157}