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