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([0u8; 32].into()),
601                function_name: "test".try_into().unwrap(),
602                args: vec![].try_into().unwrap(),
603            }),
604            auth: vec![].try_into().unwrap(),
605        };
606
607        let operation = Operation {
608            source_account: None,
609            body: OperationBody::InvokeHostFunction(invoke_op),
610        };
611
612        let operations: VecM<Operation, 100> = vec![operation].try_into().unwrap();
613
614        let tx = Transaction {
615            source_account: MuxedAccount::Ed25519(Uint256(source_pk.0)),
616            fee: 100,
617            seq_num: SequenceNumber(1),
618            cond: Preconditions::None,
619            memo: Memo::None,
620            operations,
621            ext: TransactionExt::V0,
622        };
623
624        let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
625            tx,
626            signatures: vec![].try_into().unwrap(),
627        });
628
629        let needs_sim = xdr_needs_simulation(&envelope).unwrap();
630        assert!(needs_sim, "Soroban operations should require simulation");
631    }
632
633    #[test]
634    fn test_xdr_needs_simulation_without_soroban() {
635        // Test that non-Soroban operations don't require simulation
636        let envelope = create_test_transaction_xdr(false);
637        let parsed = TransactionEnvelope::from_xdr_base64(envelope, Limits::none()).unwrap();
638
639        let needs_sim = xdr_needs_simulation(&parsed).unwrap();
640        assert!(
641            !needs_sim,
642            "Payment operations should not require simulation"
643        );
644    }
645
646    #[test]
647    fn test_xdr_needs_simulation_with_multiple_operations() {
648        // Test with multiple operations where at least one is Soroban
649        let source_pk =
650            PublicKey::from_string("GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF")
651                .unwrap();
652        let dest_pk =
653            PublicKey::from_string("GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ")
654                .unwrap();
655
656        // Create a payment operation
657        let payment_op = Operation {
658            source_account: None,
659            body: OperationBody::Payment(PaymentOp {
660                destination: MuxedAccount::Ed25519(Uint256(dest_pk.0)),
661                asset: Asset::Native,
662                amount: 1000000,
663            }),
664        };
665
666        // Create a Soroban operation
667        let soroban_op = Operation {
668            source_account: None,
669            body: OperationBody::InvokeHostFunction(InvokeHostFunctionOp {
670                host_function: HostFunction::InvokeContract(InvokeContractArgs {
671                    contract_address: soroban_rs::xdr::ScAddress::Contract([0u8; 32].into()),
672                    function_name: "test".try_into().unwrap(),
673                    args: vec![].try_into().unwrap(),
674                }),
675                auth: vec![].try_into().unwrap(),
676            }),
677        };
678
679        let operations: VecM<Operation, 100> = vec![payment_op, soroban_op].try_into().unwrap();
680
681        let tx = Transaction {
682            source_account: MuxedAccount::Ed25519(Uint256(source_pk.0)),
683            fee: 100,
684            seq_num: SequenceNumber(1),
685            cond: Preconditions::None,
686            memo: Memo::None,
687            operations,
688            ext: TransactionExt::V0,
689        };
690
691        let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
692            tx,
693            signatures: vec![].try_into().unwrap(),
694        });
695
696        let needs_sim = xdr_needs_simulation(&envelope).unwrap();
697        assert!(
698            needs_sim,
699            "Should require simulation when any operation is Soroban"
700        );
701    }
702
703    #[test]
704    fn test_calculate_transaction_hash() {
705        // Test transaction hash calculation
706        let envelope_xdr = get_signed_xdr();
707        let envelope = parse_transaction_xdr(&envelope_xdr, true).unwrap();
708
709        let hash1 = calculate_transaction_hash(&envelope).unwrap();
710        let hash2 = calculate_transaction_hash(&envelope).unwrap();
711
712        // Hash should be deterministic
713        assert_eq!(hash1, hash2, "Hash should be deterministic");
714        assert_eq!(hash1.len(), 64, "SHA256 hash should be 64 hex characters");
715
716        // Verify it's valid hex
717        assert!(
718            hash1.chars().all(|c| c.is_ascii_hexdigit()),
719            "Hash should be valid hex"
720        );
721    }
722
723    #[test]
724    fn test_muxed_account_conversion() {
725        let address = "GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ";
726        let muxed = string_to_muxed_account(address).unwrap();
727        let back = muxed_account_to_string(&muxed).unwrap();
728        assert_eq!(address, back);
729    }
730
731    #[test]
732    fn test_muxed_account_ed25519_variant() {
733        // Test handling of regular Ed25519 accounts
734        let address = "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF";
735        let muxed = string_to_muxed_account(address).unwrap();
736
737        match muxed {
738            MuxedAccount::Ed25519(_) => (),
739            _ => panic!("Expected Ed25519 variant"),
740        }
741
742        let back = muxed_account_to_string(&muxed).unwrap();
743        assert_eq!(address, back);
744    }
745
746    #[test]
747    fn test_muxed_account_muxed_ed25519_variant() {
748        // Test handling of MuxedEd25519 accounts
749        let pk = PublicKey::from_string("GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF")
750            .unwrap();
751
752        let muxed = MuxedAccount::MuxedEd25519(soroban_rs::xdr::MuxedAccountMed25519 {
753            id: 123456789,
754            ed25519: Uint256(pk.0),
755        });
756
757        let address = muxed_account_to_string(&muxed).unwrap();
758        assert_eq!(
759            address,
760            "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF"
761        );
762    }
763
764    #[test]
765    fn test_v0_to_v1_conversion_in_fee_bump() {
766        // Test the V0 to V1 conversion logic in build_fee_bump_envelope
767        let source_pk =
768            PublicKey::from_string("GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF")
769                .unwrap();
770        let dest_pk =
771            PublicKey::from_string("GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ")
772                .unwrap();
773
774        // Create V0 transaction with time bounds
775        let time_bounds = soroban_rs::xdr::TimeBounds {
776            min_time: soroban_rs::xdr::TimePoint(1000),
777            max_time: soroban_rs::xdr::TimePoint(2000),
778        };
779
780        let payment_op = Operation {
781            source_account: None,
782            body: OperationBody::Payment(PaymentOp {
783                destination: MuxedAccount::Ed25519(Uint256(dest_pk.0)),
784                asset: Asset::Native,
785                amount: 3000000,
786            }),
787        };
788
789        let operations: VecM<Operation, 100> = vec![payment_op].try_into().unwrap();
790
791        let tx_v0 = TransactionV0 {
792            source_account_ed25519: Uint256(source_pk.0),
793            fee: 200,
794            seq_num: SequenceNumber(42),
795            time_bounds: Some(time_bounds.clone()),
796            memo: Memo::Text("Test memo".as_bytes().to_vec().try_into().unwrap()),
797            operations: operations.clone(),
798            ext: soroban_rs::xdr::TransactionV0Ext::V0,
799        };
800
801        // Add a signature to V0 envelope
802        let sig = DecoratedSignature {
803            hint: SignatureHint([1, 2, 3, 4]),
804            signature: Signature(vec![5u8; 64].try_into().unwrap()),
805        };
806
807        let v0_envelope = TransactionEnvelope::TxV0(TransactionV0Envelope {
808            tx: tx_v0,
809            signatures: vec![sig.clone()].try_into().unwrap(),
810        });
811
812        // Build fee-bump from V0 envelope
813        let fee_source = "GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ";
814        let fee_bump_envelope =
815            build_fee_bump_envelope(v0_envelope, fee_source, 50_000_000).unwrap();
816
817        // Verify it's a fee-bump envelope
818        assert!(matches!(
819            fee_bump_envelope,
820            TransactionEnvelope::TxFeeBump(_)
821        ));
822
823        if let TransactionEnvelope::TxFeeBump(fb_env) = fee_bump_envelope {
824            // Verify fee source
825            let fb_source = muxed_account_to_string(&fb_env.tx.fee_source).unwrap();
826            assert_eq!(fb_source, fee_source);
827            assert_eq!(fb_env.tx.fee, 50_000_000);
828
829            // Verify inner transaction was properly converted
830            let FeeBumpTransactionInnerTx::Tx(inner_v1) = &fb_env.tx.inner_tx;
831            // Check that V0 data was preserved in V1 format
832            assert_eq!(inner_v1.tx.fee, 200);
833            assert_eq!(inner_v1.tx.seq_num.0, 42);
834
835            // Check time bounds conversion
836            if let Preconditions::Time(tb) = &inner_v1.tx.cond {
837                assert_eq!(tb.min_time.0, 1000);
838                assert_eq!(tb.max_time.0, 2000);
839            } else {
840                panic!("Expected time bounds in preconditions");
841            }
842
843            // Check memo preservation
844            if let Memo::Text(text) = &inner_v1.tx.memo {
845                assert_eq!(text.as_slice(), "Test memo".as_bytes());
846            } else {
847                panic!("Expected text memo");
848            }
849
850            // Check operations preservation
851            assert_eq!(inner_v1.tx.operations.len(), 1);
852            // Check signatures were preserved
853            assert_eq!(inner_v1.signatures.len(), 1);
854            assert_eq!(inner_v1.signatures[0].hint, sig.hint);
855        }
856    }
857
858    #[test]
859    fn test_attach_signatures_to_envelope() {
860        use soroban_rs::xdr::{
861            DecoratedSignature, Memo, Operation, OperationBody, PaymentOp, SequenceNumber,
862            Signature, SignatureHint, TransactionV0, TransactionV0Envelope,
863        };
864        use stellar_strkey::ed25519::PublicKey;
865
866        let source_pk =
867            PublicKey::from_string("GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF")
868                .unwrap();
869        let dest_pk =
870            PublicKey::from_string("GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ")
871                .unwrap();
872
873        // Create a test transaction
874        let payment_op = Operation {
875            source_account: None,
876            body: OperationBody::Payment(PaymentOp {
877                destination: MuxedAccount::Ed25519(Uint256(dest_pk.0)),
878                asset: soroban_rs::xdr::Asset::Native,
879                amount: 1000000,
880            }),
881        };
882
883        let operations: VecM<Operation, 100> = vec![payment_op].try_into().unwrap();
884
885        let tx_v0 = TransactionV0 {
886            source_account_ed25519: Uint256(source_pk.0),
887            fee: 100,
888            seq_num: SequenceNumber(42),
889            time_bounds: None,
890            memo: Memo::None,
891            operations,
892            ext: soroban_rs::xdr::TransactionV0Ext::V0,
893        };
894
895        let mut envelope = TransactionEnvelope::TxV0(TransactionV0Envelope {
896            tx: tx_v0,
897            signatures: vec![].try_into().unwrap(),
898        });
899
900        // Create test signatures
901        let sig1 = DecoratedSignature {
902            hint: SignatureHint([1, 2, 3, 4]),
903            signature: Signature(vec![1u8; 64].try_into().unwrap()),
904        };
905        let sig2 = DecoratedSignature {
906            hint: SignatureHint([5, 6, 7, 8]),
907            signature: Signature(vec![2u8; 64].try_into().unwrap()),
908        };
909
910        // Attach signatures
911        let result = attach_signatures_to_envelope(&mut envelope, vec![sig1, sig2]);
912        assert!(result.is_ok());
913
914        // Verify signatures were attached
915        match &envelope {
916            TransactionEnvelope::TxV0(e) => {
917                assert_eq!(e.signatures.len(), 2);
918                assert_eq!(e.signatures[0].hint.0, [1, 2, 3, 4]);
919                assert_eq!(e.signatures[1].hint.0, [5, 6, 7, 8]);
920            }
921            _ => panic!("Expected V0 envelope"),
922        }
923    }
924
925    #[test]
926    fn test_extract_operations() {
927        use soroban_rs::xdr::{
928            Memo, Operation, OperationBody, PaymentOp, SequenceNumber, Transaction, TransactionV0,
929            TransactionV0Envelope, TransactionV1Envelope,
930        };
931        use stellar_strkey::ed25519::PublicKey;
932
933        let source_pk =
934            PublicKey::from_string("GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF")
935                .unwrap();
936        let dest_pk =
937            PublicKey::from_string("GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ")
938                .unwrap();
939
940        // Create test operation
941        let payment_op = Operation {
942            source_account: None,
943            body: OperationBody::Payment(PaymentOp {
944                destination: MuxedAccount::Ed25519(Uint256(dest_pk.0)),
945                asset: soroban_rs::xdr::Asset::Native,
946                amount: 1000000,
947            }),
948        };
949
950        let operations: VecM<Operation, 100> = vec![payment_op.clone()].try_into().unwrap();
951
952        // Test V0 envelope
953        let tx_v0 = TransactionV0 {
954            source_account_ed25519: Uint256(source_pk.0),
955            fee: 100,
956            seq_num: SequenceNumber(42),
957            time_bounds: None,
958            memo: Memo::None,
959            operations: operations.clone(),
960            ext: soroban_rs::xdr::TransactionV0Ext::V0,
961        };
962
963        let v0_envelope = TransactionEnvelope::TxV0(TransactionV0Envelope {
964            tx: tx_v0,
965            signatures: vec![].try_into().unwrap(),
966        });
967
968        let extracted_ops = extract_operations(&v0_envelope).unwrap();
969        assert_eq!(extracted_ops.len(), 1);
970
971        // Test V1 envelope
972        let tx_v1 = Transaction {
973            source_account: MuxedAccount::Ed25519(Uint256(source_pk.0)),
974            fee: 100,
975            seq_num: SequenceNumber(42),
976            cond: soroban_rs::xdr::Preconditions::None,
977            memo: Memo::None,
978            operations: operations.clone(),
979            ext: soroban_rs::xdr::TransactionExt::V0,
980        };
981
982        let v1_envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
983            tx: tx_v1,
984            signatures: vec![].try_into().unwrap(),
985        });
986
987        let extracted_ops = extract_operations(&v1_envelope).unwrap();
988        assert_eq!(extracted_ops.len(), 1);
989    }
990
991    #[test]
992    fn test_xdr_needs_simulation() {
993        use soroban_rs::xdr::{
994            HostFunction, InvokeHostFunctionOp, Memo, Operation, OperationBody, PaymentOp,
995            ScSymbol, ScVal, SequenceNumber, Transaction, TransactionV1Envelope,
996        };
997        use stellar_strkey::ed25519::PublicKey;
998
999        let source_pk =
1000            PublicKey::from_string("GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF")
1001                .unwrap();
1002        let dest_pk =
1003            PublicKey::from_string("GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ")
1004                .unwrap();
1005
1006        // Test with payment operation (should not need simulation)
1007        let payment_op = Operation {
1008            source_account: None,
1009            body: OperationBody::Payment(PaymentOp {
1010                destination: MuxedAccount::Ed25519(Uint256(dest_pk.0)),
1011                asset: soroban_rs::xdr::Asset::Native,
1012                amount: 1000000,
1013            }),
1014        };
1015
1016        let operations: VecM<Operation, 100> = vec![payment_op].try_into().unwrap();
1017
1018        let tx = Transaction {
1019            source_account: MuxedAccount::Ed25519(Uint256(source_pk.0)),
1020            fee: 100,
1021            seq_num: SequenceNumber(42),
1022            cond: soroban_rs::xdr::Preconditions::None,
1023            memo: Memo::None,
1024            operations,
1025            ext: soroban_rs::xdr::TransactionExt::V0,
1026        };
1027
1028        let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
1029            tx,
1030            signatures: vec![].try_into().unwrap(),
1031        });
1032
1033        assert!(!xdr_needs_simulation(&envelope).unwrap());
1034
1035        // Test with InvokeHostFunction operation (should need simulation)
1036        let invoke_op = Operation {
1037            source_account: None,
1038            body: OperationBody::InvokeHostFunction(InvokeHostFunctionOp {
1039                host_function: HostFunction::InvokeContract(soroban_rs::xdr::InvokeContractArgs {
1040                    contract_address: soroban_rs::xdr::ScAddress::Contract([0u8; 32].into()),
1041                    function_name: ScSymbol("test".try_into().unwrap()),
1042                    args: vec![ScVal::U32(42)].try_into().unwrap(),
1043                }),
1044                auth: vec![].try_into().unwrap(),
1045            }),
1046        };
1047
1048        let operations: VecM<Operation, 100> = vec![invoke_op].try_into().unwrap();
1049
1050        let tx = Transaction {
1051            source_account: MuxedAccount::Ed25519(Uint256(source_pk.0)),
1052            fee: 100,
1053            seq_num: SequenceNumber(42),
1054            cond: soroban_rs::xdr::Preconditions::None,
1055            memo: Memo::None,
1056            operations,
1057            ext: soroban_rs::xdr::TransactionExt::V0,
1058        };
1059
1060        let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
1061            tx,
1062            signatures: vec![].try_into().unwrap(),
1063        });
1064
1065        assert!(xdr_needs_simulation(&envelope).unwrap());
1066    }
1067
1068    #[test]
1069    fn test_v0_to_v1_conversion() {
1070        use soroban_rs::xdr::{
1071            Memo, Operation, OperationBody, PaymentOp, SequenceNumber, TimeBounds, TimePoint,
1072            TransactionV0, TransactionV0Envelope,
1073        };
1074        use stellar_strkey::ed25519::PublicKey;
1075
1076        let source_pk =
1077            PublicKey::from_string("GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF")
1078                .unwrap();
1079        let dest_pk =
1080            PublicKey::from_string("GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ")
1081                .unwrap();
1082
1083        // Create test V0 transaction with various fields
1084        let time_bounds = TimeBounds {
1085            min_time: TimePoint(1000),
1086            max_time: TimePoint(2000),
1087        };
1088
1089        let payment_op = Operation {
1090            source_account: None,
1091            body: OperationBody::Payment(PaymentOp {
1092                destination: MuxedAccount::Ed25519(Uint256(dest_pk.0)),
1093                asset: soroban_rs::xdr::Asset::Native,
1094                amount: 1000000,
1095            }),
1096        };
1097
1098        let operations: VecM<Operation, 100> = vec![payment_op].try_into().unwrap();
1099
1100        let tx_v0 = TransactionV0 {
1101            source_account_ed25519: Uint256(source_pk.0),
1102            fee: 100,
1103            seq_num: SequenceNumber(42),
1104            time_bounds: Some(time_bounds.clone()),
1105            memo: Memo::Text("Test".as_bytes().to_vec().try_into().unwrap()),
1106            operations: operations.clone(),
1107            ext: soroban_rs::xdr::TransactionV0Ext::V0,
1108        };
1109
1110        let sig = soroban_rs::xdr::DecoratedSignature {
1111            hint: soroban_rs::xdr::SignatureHint([1, 2, 3, 4]),
1112            signature: soroban_rs::xdr::Signature(vec![0u8; 64].try_into().unwrap()),
1113        };
1114
1115        let v0_envelope = TransactionV0Envelope {
1116            tx: tx_v0,
1117            signatures: vec![sig.clone()].try_into().unwrap(),
1118        };
1119
1120        // Convert to V1
1121        let v1_envelope = convert_v0_to_v1_envelope(v0_envelope);
1122
1123        // Verify conversion preserved all data
1124        assert_eq!(v1_envelope.tx.fee, 100);
1125        assert_eq!(v1_envelope.tx.seq_num.0, 42);
1126        assert_eq!(v1_envelope.tx.operations.len(), 1);
1127        assert_eq!(v1_envelope.signatures.len(), 1);
1128
1129        // Check source account conversion
1130        if let MuxedAccount::Ed25519(key) = &v1_envelope.tx.source_account {
1131            assert_eq!(key.0, source_pk.0);
1132        } else {
1133            panic!("Expected Ed25519 source account");
1134        }
1135
1136        // Check time bounds conversion
1137        if let soroban_rs::xdr::Preconditions::Time(tb) = &v1_envelope.tx.cond {
1138            assert_eq!(tb.min_time.0, 1000);
1139            assert_eq!(tb.max_time.0, 2000);
1140        } else {
1141            panic!("Expected time bounds in preconditions");
1142        }
1143
1144        // Check memo preservation
1145        if let Memo::Text(text) = &v1_envelope.tx.memo {
1146            assert_eq!(text.as_slice(), "Test".as_bytes());
1147        } else {
1148            panic!("Expected text memo");
1149        }
1150    }
1151}