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