1use crate::models::transaction::stellar::asset::AssetSpec;
4use crate::models::transaction::stellar::host_function::{ContractSource, WasmSource};
5use crate::models::SignerError;
6use crate::utils::{deserialize_i64, serialize_i64};
7use serde::{Deserialize, Serialize};
8use soroban_rs::xdr::{
9 HostFunction, InvokeHostFunctionOp, MuxedAccount as XdrMuxedAccount, MuxedAccountMed25519,
10 Operation, OperationBody, PaymentOp, SorobanAuthorizationEntry, SorobanAuthorizedFunction,
11 SorobanAuthorizedInvocation, SorobanCredentials, Uint256, VecM,
12};
13use std::convert::TryFrom;
14use stellar_strkey::{ed25519::MuxedAccount, ed25519::PublicKey};
15use utoipa::ToSchema;
16
17#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)]
19#[serde(tag = "type", rename_all = "snake_case")]
20pub enum AuthSpec {
21 None,
23
24 SourceAccount,
26
27 Addresses { signers: Vec<String> },
29
30 Xdr { entries: Vec<String> },
32}
33
34#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)]
35#[serde(tag = "type", rename_all = "snake_case")]
36pub enum OperationSpec {
37 Payment {
38 destination: String,
39 #[serde(serialize_with = "serialize_i64", deserialize_with = "deserialize_i64")]
44 #[schema(value_type = String, pattern = "^-?[0-9]+$")]
45 amount: i64,
46 asset: AssetSpec,
47 },
48 InvokeContract {
49 contract_address: String,
50 function_name: String,
51 args: Vec<serde_json::Value>,
52 #[serde(skip_serializing_if = "Option::is_none")]
53 auth: Option<AuthSpec>,
54 },
55 CreateContract {
56 source: ContractSource,
57 wasm_hash: String,
58 #[serde(skip_serializing_if = "Option::is_none")]
59 salt: Option<String>,
60 #[serde(skip_serializing_if = "Option::is_none")]
61 constructor_args: Option<Vec<serde_json::Value>>,
62 #[serde(skip_serializing_if = "Option::is_none")]
63 auth: Option<AuthSpec>,
64 },
65 UploadWasm {
66 wasm: WasmSource,
67 #[serde(skip_serializing_if = "Option::is_none")]
68 auth: Option<AuthSpec>,
69 },
70}
71
72fn parse_destination_address(destination: &str) -> Result<XdrMuxedAccount, SignerError> {
76 if let Ok(m) = MuxedAccount::from_string(destination) {
77 Ok(XdrMuxedAccount::MuxedEd25519(MuxedAccountMed25519 {
79 id: m.id,
80 ed25519: Uint256(m.ed25519),
81 }))
82 } else {
83 let pk = PublicKey::from_string(destination)
85 .map_err(|e| SignerError::ConversionError(format!("Invalid destination: {e}")))?;
86 Ok(XdrMuxedAccount::Ed25519(Uint256(pk.0)))
87 }
88}
89
90fn create_source_account_auth_entry(
92 function: SorobanAuthorizedFunction,
93) -> SorobanAuthorizationEntry {
94 SorobanAuthorizationEntry {
95 credentials: SorobanCredentials::SourceAccount,
96 root_invocation: SorobanAuthorizedInvocation {
97 function,
98 sub_invocations: VecM::default(),
99 },
100 }
101}
102
103fn decode_xdr_auth_entries(
105 xdr_entries: Vec<String>,
106) -> Result<Vec<SorobanAuthorizationEntry>, SignerError> {
107 use soroban_rs::xdr::{Limits, ReadXdr};
108
109 xdr_entries
110 .iter()
111 .map(|xdr_str| {
112 SorobanAuthorizationEntry::from_xdr_base64(xdr_str, Limits::none())
113 .map_err(|e| SignerError::ConversionError(format!("Invalid auth XDR: {e}")))
114 })
115 .collect()
116}
117
118fn generate_default_auth_entries(
120 host_function: &HostFunction,
121) -> Result<Vec<SorobanAuthorizationEntry>, SignerError> {
122 match host_function {
123 HostFunction::CreateContract(ref create_args) => {
124 let auth_entry = create_source_account_auth_entry(
125 SorobanAuthorizedFunction::CreateContractHostFn(create_args.clone()),
126 );
127 Ok(vec![auth_entry])
128 }
129 HostFunction::CreateContractV2(ref create_args_v2) => {
130 let auth_entry = create_source_account_auth_entry(
131 SorobanAuthorizedFunction::CreateContractV2HostFn(create_args_v2.clone()),
132 );
133 Ok(vec![auth_entry])
134 }
135 HostFunction::InvokeContract(ref invoke_args) => {
136 let auth_entry = create_source_account_auth_entry(
137 SorobanAuthorizedFunction::ContractFn(invoke_args.clone()),
138 );
139 Ok(vec![auth_entry])
140 }
141 _ => Ok(vec![]),
142 }
143}
144
145fn build_auth_vector(
147 auth: Option<AuthSpec>,
148 host_function: &HostFunction,
149) -> Result<VecM<SorobanAuthorizationEntry, { u32::MAX }>, SignerError> {
150 let auth_entries = match auth {
151 Some(AuthSpec::None) => vec![],
152 Some(AuthSpec::SourceAccount) => generate_default_auth_entries(host_function)?,
153 Some(AuthSpec::Addresses { signers: _ }) => {
154 return Err(SignerError::ConversionError(
156 "Address-based auth not yet implemented".into(),
157 ));
158 }
159 Some(AuthSpec::Xdr { entries }) => decode_xdr_auth_entries(entries)?,
160 None => generate_default_auth_entries(host_function)?,
161 };
162
163 auth_entries
164 .try_into()
165 .map_err(|e| SignerError::ConversionError(format!("Failed to convert auth entries: {e:?}")))
166}
167
168fn convert_payment_operation(
170 destination: String,
171 amount: i64,
172 asset: AssetSpec,
173) -> Result<Operation, SignerError> {
174 let dest = parse_destination_address(&destination)?;
175
176 Ok(Operation {
177 source_account: None,
178 body: OperationBody::Payment(PaymentOp {
179 destination: dest,
180 asset: asset.try_into()?,
181 amount,
182 }),
183 })
184}
185
186fn convert_invoke_contract_operation(
188 contract_address: String,
189 function_name: String,
190 args: Vec<serde_json::Value>,
191 auth: Option<AuthSpec>,
192) -> Result<Operation, SignerError> {
193 use crate::models::transaction::stellar::host_function::HostFunctionSpec;
194
195 let spec = HostFunctionSpec::InvokeContract {
197 contract_address,
198 function_name,
199 args,
200 };
201
202 let host_function = HostFunction::try_from(spec)?;
204
205 let auth_vec = build_auth_vector(auth, &host_function)?;
207
208 Ok(Operation {
209 source_account: None,
210 body: OperationBody::InvokeHostFunction(InvokeHostFunctionOp {
211 auth: auth_vec,
212 host_function,
213 }),
214 })
215}
216
217fn convert_create_contract_operation(
219 source: ContractSource,
220 wasm_hash: String,
221 salt: Option<String>,
222 constructor_args: Option<Vec<serde_json::Value>>,
223 auth: Option<AuthSpec>,
224) -> Result<Operation, SignerError> {
225 use crate::models::transaction::stellar::host_function::HostFunctionSpec;
226
227 let spec = HostFunctionSpec::CreateContract {
229 source,
230 wasm_hash,
231 salt,
232 constructor_args,
233 };
234
235 let host_function = HostFunction::try_from(spec)?;
237
238 let auth_vec = build_auth_vector(auth, &host_function)?;
240
241 Ok(Operation {
242 source_account: None,
243 body: OperationBody::InvokeHostFunction(InvokeHostFunctionOp {
244 auth: auth_vec,
245 host_function,
246 }),
247 })
248}
249
250fn convert_upload_wasm_operation(
252 wasm: WasmSource,
253 auth: Option<AuthSpec>,
254) -> Result<Operation, SignerError> {
255 use crate::models::transaction::stellar::host_function::HostFunctionSpec;
256
257 let spec = HostFunctionSpec::UploadWasm { wasm };
259
260 let host_function = HostFunction::try_from(spec)?;
262
263 let auth_vec = build_auth_vector(auth, &host_function)?;
265
266 Ok(Operation {
267 source_account: None,
268 body: OperationBody::InvokeHostFunction(InvokeHostFunctionOp {
269 auth: auth_vec,
270 host_function,
271 }),
272 })
273}
274
275impl TryFrom<OperationSpec> for Operation {
276 type Error = SignerError;
277
278 fn try_from(op: OperationSpec) -> Result<Self, Self::Error> {
279 match op {
280 OperationSpec::Payment {
281 destination,
282 amount,
283 asset,
284 } => convert_payment_operation(destination, amount, asset),
285
286 OperationSpec::InvokeContract {
287 contract_address,
288 function_name,
289 args,
290 auth,
291 } => convert_invoke_contract_operation(contract_address, function_name, args, auth),
292
293 OperationSpec::CreateContract {
294 source,
295 wasm_hash,
296 salt,
297 constructor_args,
298 auth,
299 } => convert_create_contract_operation(source, wasm_hash, salt, constructor_args, auth),
300
301 OperationSpec::UploadWasm { wasm, auth } => convert_upload_wasm_operation(wasm, auth),
302 }
303 }
304}
305
306#[cfg(test)]
307mod tests {
308 use super::*;
309 use crate::models::transaction::stellar::host_function::ContractSource;
310 use soroban_rs::xdr::{
311 AccountId, ContractExecutable, ContractId, ContractIdPreimage,
312 ContractIdPreimageFromAddress, CreateContractArgs, CreateContractArgsV2, Hash,
313 PublicKey as XdrPublicKey, ScAddress,
314 };
315
316 const TEST_PK: &str = "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF";
317 const TEST_CONTRACT: &str = "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA";
318 const TEST_MUXED: &str =
319 "MAAAAAAAAAAAAAB7BQ2L7E5NBWMXDUCMZSIPOBKRDSBYVLMXGSSKF6YNPIB7Y77ITLVL6";
320
321 #[test]
325 fn test_payment_amount_serializes_as_string() {
326 let op = OperationSpec::Payment {
327 destination: TEST_PK.to_string(),
328 amount: 643918676885760,
329 asset: AssetSpec::Native,
330 };
331 let json = serde_json::to_string(&op).unwrap();
332 assert!(
333 json.contains(r#""amount":"643918676885760""#),
334 "amount should serialize as a string, got: {json}"
335 );
336 }
337
338 #[test]
339 fn test_payment_amount_deserializes_from_corrupted_float() {
340 let op = OperationSpec::Payment {
341 destination: TEST_PK.to_string(),
342 amount: 643918676885760,
343 asset: AssetSpec::Native,
344 };
345 let mut value = serde_json::to_value(&op).unwrap();
346 value["amount"] = serde_json::json!(643918676885760.0);
347 let json = serde_json::to_string(&value).unwrap();
348
349 let parsed: OperationSpec = serde_json::from_str(&json).unwrap();
350 match parsed {
351 OperationSpec::Payment { amount, .. } => assert_eq!(amount, 643918676885760),
352 other => panic!("expected Payment, got {other:?}"),
353 }
354 }
355
356 mod parse_destination_address_tests {
357 use super::*;
358
359 #[test]
360 fn test_regular_public_key() {
361 let result = parse_destination_address(TEST_PK).unwrap();
362 assert!(matches!(result, XdrMuxedAccount::Ed25519(_)));
363 }
364
365 #[test]
366 fn test_muxed_account() {
367 let result = parse_destination_address(TEST_MUXED).unwrap();
368 assert!(matches!(result, XdrMuxedAccount::MuxedEd25519(_)));
369 }
370
371 #[test]
372 fn test_invalid_address() {
373 let result = parse_destination_address("INVALID");
374 assert!(result.is_err());
375 }
376 }
377
378 mod create_source_account_auth_entry_tests {
379 use super::*;
380 use soroban_rs::xdr::Uint256;
381
382 #[test]
383 fn test_creates_correct_structure() {
384 let function = SorobanAuthorizedFunction::CreateContractHostFn(CreateContractArgs {
385 contract_id_preimage: ContractIdPreimage::Address(ContractIdPreimageFromAddress {
386 address: ScAddress::Account(AccountId(XdrPublicKey::PublicKeyTypeEd25519(
387 Uint256([0u8; 32]),
388 ))),
389 salt: Uint256([0u8; 32]),
390 }),
391 executable: ContractExecutable::Wasm(Hash([0u8; 32])),
392 });
393
394 let entry = create_source_account_auth_entry(function.clone());
395 assert!(matches!(
396 entry.credentials,
397 SorobanCredentials::SourceAccount
398 ));
399 }
401 }
402
403 mod decode_xdr_auth_entries_tests {
404 use super::*;
405
406 #[test]
407 fn test_invalid_base64() {
408 let xdr_entries = vec!["!!!invalid!!!".to_string()];
409 let result = decode_xdr_auth_entries(xdr_entries);
410 assert!(result.is_err());
411 }
412
413 #[test]
414 fn test_malformed_xdr() {
415 let xdr_entries = vec!["dGVzdA==".to_string()]; let result = decode_xdr_auth_entries(xdr_entries);
417 assert!(result.is_err());
418 }
419
420 #[test]
421 fn test_empty_list() {
422 let xdr_entries = vec![];
423 let result = decode_xdr_auth_entries(xdr_entries);
424 assert!(result.is_ok());
425 assert_eq!(result.unwrap().len(), 0);
426 }
427 }
428
429 mod generate_default_auth_entries_tests {
430 use super::*;
431 use soroban_rs::xdr::Uint256;
432
433 #[test]
434 fn test_create_contract() {
435 let host_function = HostFunction::CreateContract(CreateContractArgs {
436 contract_id_preimage: ContractIdPreimage::Address(ContractIdPreimageFromAddress {
437 address: ScAddress::Account(AccountId(XdrPublicKey::PublicKeyTypeEd25519(
438 Uint256([0u8; 32]),
439 ))),
440 salt: Uint256([0u8; 32]),
441 }),
442 executable: ContractExecutable::Wasm(Hash([0u8; 32])),
443 });
444
445 let result = generate_default_auth_entries(&host_function);
446 assert!(result.is_ok());
447 assert_eq!(result.unwrap().len(), 1);
448 }
449
450 #[test]
451 fn test_create_contract_v2() {
452 let host_function = HostFunction::CreateContractV2(CreateContractArgsV2 {
453 contract_id_preimage: ContractIdPreimage::Address(ContractIdPreimageFromAddress {
454 address: ScAddress::Account(AccountId(XdrPublicKey::PublicKeyTypeEd25519(
455 Uint256([0u8; 32]),
456 ))),
457 salt: Uint256([0u8; 32]),
458 }),
459 executable: ContractExecutable::Wasm(Hash([0u8; 32])),
460 constructor_args: VecM::default(),
461 });
462
463 let result = generate_default_auth_entries(&host_function);
464 assert!(result.is_ok());
465 assert_eq!(result.unwrap().len(), 1);
466 }
467
468 #[test]
469 fn test_invoke_contract() {
470 let host_function = HostFunction::InvokeContract(soroban_rs::xdr::InvokeContractArgs {
471 contract_address: ScAddress::Contract(ContractId(Hash([0u8; 32]))),
472 function_name: soroban_rs::xdr::ScSymbol::try_from(b"test".to_vec()).unwrap(),
473 args: VecM::default(),
474 });
475
476 let result = generate_default_auth_entries(&host_function);
477 assert!(result.is_ok());
478 assert_eq!(result.unwrap().len(), 1);
479 }
480
481 #[test]
482 fn test_other_operations() {
483 let host_function = HostFunction::UploadContractWasm(vec![0u8; 10].try_into().unwrap());
484
485 let result = generate_default_auth_entries(&host_function);
486 assert!(result.is_ok());
487 assert_eq!(result.unwrap().len(), 0);
488 }
489 }
490
491 mod build_auth_vector_tests {
492 use super::*;
493 use soroban_rs::xdr::Uint256;
494
495 #[test]
496 fn test_simple_auth() {
497 let host_function = HostFunction::CreateContract(CreateContractArgs {
498 contract_id_preimage: ContractIdPreimage::Address(ContractIdPreimageFromAddress {
499 address: ScAddress::Account(AccountId(XdrPublicKey::PublicKeyTypeEd25519(
500 Uint256([0u8; 32]),
501 ))),
502 salt: Uint256([0u8; 32]),
503 }),
504 executable: ContractExecutable::Wasm(Hash([0u8; 32])),
505 });
506
507 let auth = Some(AuthSpec::SourceAccount);
508 let result = build_auth_vector(auth, &host_function);
509
510 assert!(result.is_ok());
511 assert_eq!(result.unwrap().len(), 1);
512 }
513
514 #[test]
515 fn test_xdr_auth_invalid() {
516 let host_function = HostFunction::CreateContract(CreateContractArgs {
517 contract_id_preimage: ContractIdPreimage::Address(ContractIdPreimageFromAddress {
518 address: ScAddress::Account(AccountId(XdrPublicKey::PublicKeyTypeEd25519(
519 Uint256([0u8; 32]),
520 ))),
521 salt: Uint256([0u8; 32]),
522 }),
523 executable: ContractExecutable::Wasm(Hash([0u8; 32])),
524 });
525
526 let auth = Some(AuthSpec::Xdr {
527 entries: vec!["invalid".to_string()],
528 });
529 let result = build_auth_vector(auth, &host_function);
530
531 assert!(result.is_err());
532 }
533
534 #[test]
535 fn test_none_default_create_contract() {
536 let host_function = HostFunction::CreateContract(CreateContractArgs {
537 contract_id_preimage: ContractIdPreimage::Address(ContractIdPreimageFromAddress {
538 address: ScAddress::Account(AccountId(XdrPublicKey::PublicKeyTypeEd25519(
539 Uint256([0u8; 32]),
540 ))),
541 salt: Uint256([0u8; 32]),
542 }),
543 executable: ContractExecutable::Wasm(Hash([0u8; 32])),
544 });
545
546 let result = build_auth_vector(None, &host_function);
547
548 assert!(result.is_ok());
549 assert_eq!(result.unwrap().len(), 1);
550 }
551
552 #[test]
553 fn test_none_default_invoke_contract() {
554 let host_function = HostFunction::InvokeContract(soroban_rs::xdr::InvokeContractArgs {
555 contract_address: ScAddress::Contract(ContractId(Hash([0u8; 32]))),
556 function_name: soroban_rs::xdr::ScSymbol::try_from(b"test".to_vec()).unwrap(),
557 args: VecM::default(),
558 });
559
560 let result = build_auth_vector(None, &host_function);
561
562 assert!(result.is_ok());
563 assert_eq!(result.unwrap().len(), 1);
564 }
565 }
566
567 mod convert_payment_operation_tests {
568 use super::*;
569
570 #[test]
571 fn test_with_native_asset() {
572 let result = convert_payment_operation(TEST_PK.to_string(), 1000, AssetSpec::Native);
573
574 assert!(result.is_ok());
575 if let Operation {
576 body: OperationBody::Payment(op),
577 ..
578 } = result.unwrap()
579 {
580 assert_eq!(op.amount, 1000);
581 assert!(matches!(op.asset, soroban_rs::xdr::Asset::Native));
582 } else {
583 panic!("Expected Payment operation");
584 }
585 }
586
587 #[test]
588 fn test_with_credit_asset() {
589 let result = convert_payment_operation(
590 TEST_PK.to_string(),
591 500,
592 AssetSpec::Credit4 {
593 code: "USDC".to_string(),
594 issuer: TEST_PK.to_string(),
595 },
596 );
597
598 assert!(result.is_ok());
599 }
600
601 #[test]
602 fn test_invalid_destination() {
603 let result = convert_payment_operation("INVALID".to_string(), 1000, AssetSpec::Native);
604
605 assert!(result.is_err());
606 }
607
608 #[test]
609 fn test_invalid_asset() {
610 let result = convert_payment_operation(
611 TEST_PK.to_string(),
612 1000,
613 AssetSpec::Credit4 {
614 code: "TOOLONG".to_string(),
615 issuer: TEST_PK.to_string(),
616 },
617 );
618
619 assert!(result.is_err());
620 }
621 }
622
623 mod convert_invoke_contract_operation_tests {
624 use super::*;
625
626 #[test]
627 fn test_basic_contract_invocation() {
628 let result = convert_invoke_contract_operation(
629 TEST_CONTRACT.to_string(),
630 "test".to_string(),
631 vec![],
632 None,
633 );
634 assert!(result.is_ok());
635 }
636
637 #[test]
638 fn test_with_auth() {
639 let auth = Some(AuthSpec::SourceAccount);
640 let result = convert_invoke_contract_operation(
641 TEST_CONTRACT.to_string(),
642 "transfer".to_string(),
643 vec![],
644 auth,
645 );
646
647 assert!(result.is_ok());
648 if let Operation {
649 body: OperationBody::InvokeHostFunction(op),
650 ..
651 } = result.unwrap()
652 {
653 assert_eq!(op.auth.len(), 1);
654 } else {
655 panic!("Expected InvokeHostFunction operation");
656 }
657 }
658 }
659
660 mod convert_create_contract_operation_tests {
661 use super::*;
662
663 #[test]
664 fn test_create_contract() {
665 let source = ContractSource::Address {
666 address: TEST_PK.to_string(),
667 };
668 let wasm_hash =
669 "0000000000000000000000000000000000000000000000000000000000000001".to_string();
670
671 let result = convert_create_contract_operation(source, wasm_hash, None, None, None);
672 assert!(result.is_ok());
673 }
674 }
675
676 #[test]
678 fn test_payment_operation() {
679 let spec = OperationSpec::Payment {
680 destination: TEST_PK.to_string(),
681 amount: 1000,
682 asset: AssetSpec::Native,
683 };
684
685 let result = Operation::try_from(spec);
686 assert!(result.is_ok());
687 assert!(matches!(result.unwrap().body, OperationBody::Payment(_)));
688 }
689
690 #[test]
691 fn test_invoke_contract_operation() {
692 let spec = OperationSpec::InvokeContract {
693 contract_address: TEST_CONTRACT.to_string(),
694 function_name: "test".to_string(),
695 args: vec![],
696 auth: None,
697 };
698
699 let result = Operation::try_from(spec);
700 assert!(result.is_ok());
701 assert!(matches!(
702 result.unwrap().body,
703 OperationBody::InvokeHostFunction(_)
704 ));
705 }
706
707 #[test]
708 fn test_operation_spec_serde() {
709 let spec = OperationSpec::Payment {
710 destination: TEST_PK.to_string(),
711 amount: 1000,
712 asset: AssetSpec::Native,
713 };
714 let json = serde_json::to_string(&spec).unwrap();
715 assert!(json.contains("payment"));
716 assert!(json.contains("native"));
717
718 let deserialized: OperationSpec = serde_json::from_str(&json).unwrap();
719 assert_eq!(spec, deserialized);
720 }
721
722 #[test]
723 fn test_auth_spec_serde() {
724 let spec = AuthSpec::SourceAccount;
725 let json = serde_json::to_string(&spec).unwrap();
726 assert!(json.contains("source_account"));
727
728 let deserialized: AuthSpec = serde_json::from_str(&json).unwrap();
729 assert_eq!(spec, deserialized);
730 }
731
732 #[test]
733 fn test_auth_spec_json_format() {
734 let none = AuthSpec::None;
736 let none_json = serde_json::to_value(&none).unwrap();
737 assert_eq!(none_json["type"], "none");
738
739 let source = AuthSpec::SourceAccount;
741 let source_json = serde_json::to_value(&source).unwrap();
742 assert_eq!(source_json["type"], "source_account");
743
744 let addresses = AuthSpec::Addresses {
746 signers: vec![TEST_PK.to_string()],
747 };
748 let addresses_json = serde_json::to_value(&addresses).unwrap();
749 assert_eq!(addresses_json["type"], "addresses");
750 assert!(addresses_json["signers"].is_array());
751
752 let xdr = AuthSpec::Xdr {
754 entries: vec!["base64data".to_string()],
755 };
756 let xdr_json = serde_json::to_value(&xdr).unwrap();
757 assert_eq!(xdr_json["type"], "xdr");
758 assert!(xdr_json["entries"].is_array());
759 }
760
761 #[test]
762 fn test_operation_spec_json_format() {
763 let payment = OperationSpec::Payment {
765 destination: TEST_PK.to_string(),
766 amount: 1000,
767 asset: AssetSpec::Native,
768 };
769 let payment_json = serde_json::to_value(&payment).unwrap();
770 assert_eq!(payment_json["type"], "payment");
771 assert_eq!(payment_json["asset"]["type"], "native");
772
773 let invoke = OperationSpec::InvokeContract {
775 contract_address: TEST_CONTRACT.to_string(),
776 function_name: "test".to_string(),
777 args: vec![],
778 auth: None,
779 };
780 let invoke_json = serde_json::to_value(&invoke).unwrap();
781 assert_eq!(invoke_json["type"], "invoke_contract");
782 assert_eq!(invoke_json["contract_address"], TEST_CONTRACT);
783 assert_eq!(invoke_json["function_name"], "test");
784 assert!(invoke_json["args"].is_array());
785 }
786
787 #[test]
788 fn test_invoke_contract_with_source_account_auth_integration() {
789 let spec = OperationSpec::InvokeContract {
791 contract_address: TEST_CONTRACT.to_string(),
792 function_name: "transfer".to_string(),
793 args: vec![], auth: Some(AuthSpec::SourceAccount),
795 };
796
797 let result = Operation::try_from(spec);
799 assert!(result.is_ok());
800
801 let operation = result.unwrap();
802 match operation.body {
803 OperationBody::InvokeHostFunction(ref invoke_op) => {
804 assert_eq!(invoke_op.auth.len(), 1);
806
807 let auth_entry = &invoke_op.auth[0];
809 assert!(matches!(
810 auth_entry.credentials,
811 SorobanCredentials::SourceAccount
812 ));
813
814 match &auth_entry.root_invocation.function {
816 SorobanAuthorizedFunction::ContractFn(invoke_args) => {
817 assert!(matches!(
819 invoke_args.contract_address,
820 ScAddress::Contract(_)
821 ));
822 assert_eq!(invoke_args.function_name.0.as_slice(), b"transfer");
823 }
824 _ => panic!("Expected ContractFn authorization"),
825 }
826 }
827 _ => panic!("Expected InvokeHostFunction operation"),
828 }
829 }
830
831 #[test]
832 fn test_invoke_contract_with_none_auth_gets_default() {
833 let spec = OperationSpec::InvokeContract {
835 contract_address: TEST_CONTRACT.to_string(),
836 function_name: "mint".to_string(),
837 args: vec![],
838 auth: None, };
840
841 let result = Operation::try_from(spec);
843 assert!(result.is_ok());
844
845 let operation = result.unwrap();
846 match operation.body {
847 OperationBody::InvokeHostFunction(ref invoke_op) => {
848 assert_eq!(invoke_op.auth.len(), 1);
850
851 let auth_entry = &invoke_op.auth[0];
853 assert!(matches!(
854 auth_entry.credentials,
855 SorobanCredentials::SourceAccount
856 ));
857
858 match &auth_entry.root_invocation.function {
860 SorobanAuthorizedFunction::ContractFn(invoke_args) => {
861 assert_eq!(invoke_args.function_name.0.as_slice(), b"mint");
862 }
863 _ => panic!("Expected ContractFn authorization"),
864 }
865 }
866 _ => panic!("Expected InvokeHostFunction operation"),
867 }
868 }
869}