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