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