openzeppelin_relayer/models/transaction/stellar/
operation.rs

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