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.try_into().map_err(|e| {
157        SignerError::ConversionError(format!("Failed to convert auth entries: {:?}", e))
158    })
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, ContractIdPreimage, ContractIdPreimageFromAddress,
305        CreateContractArgs, CreateContractArgsV2, Hash, PublicKey as XdrPublicKey, ScAddress,
306    };
307
308    const TEST_PK: &str = "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF";
309    const TEST_CONTRACT: &str = "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA";
310    const TEST_MUXED: &str =
311        "MAAAAAAAAAAAAAB7BQ2L7E5NBWMXDUCMZSIPOBKRDSBYVLMXGSSKF6YNPIB7Y77ITLVL6";
312
313    mod parse_destination_address_tests {
314        use super::*;
315
316        #[test]
317        fn test_regular_public_key() {
318            let result = parse_destination_address(TEST_PK).unwrap();
319            assert!(matches!(result, XdrMuxedAccount::Ed25519(_)));
320        }
321
322        #[test]
323        fn test_muxed_account() {
324            let result = parse_destination_address(TEST_MUXED).unwrap();
325            assert!(matches!(result, XdrMuxedAccount::MuxedEd25519(_)));
326        }
327
328        #[test]
329        fn test_invalid_address() {
330            let result = parse_destination_address("INVALID");
331            assert!(result.is_err());
332        }
333    }
334
335    mod create_source_account_auth_entry_tests {
336        use super::*;
337        use soroban_rs::xdr::Uint256;
338
339        #[test]
340        fn test_creates_correct_structure() {
341            let function = SorobanAuthorizedFunction::CreateContractHostFn(CreateContractArgs {
342                contract_id_preimage: ContractIdPreimage::Address(ContractIdPreimageFromAddress {
343                    address: ScAddress::Account(AccountId(XdrPublicKey::PublicKeyTypeEd25519(
344                        Uint256([0u8; 32]),
345                    ))),
346                    salt: Uint256([0u8; 32]),
347                }),
348                executable: ContractExecutable::Wasm(Hash([0u8; 32])),
349            });
350
351            let entry = create_source_account_auth_entry(function.clone());
352            assert!(matches!(
353                entry.credentials,
354                SorobanCredentials::SourceAccount
355            ));
356            // Can't directly compare functions due to Clone requirement, but structure is validated
357        }
358    }
359
360    mod decode_xdr_auth_entries_tests {
361        use super::*;
362
363        #[test]
364        fn test_invalid_base64() {
365            let xdr_entries = vec!["!!!invalid!!!".to_string()];
366            let result = decode_xdr_auth_entries(xdr_entries);
367            assert!(result.is_err());
368        }
369
370        #[test]
371        fn test_malformed_xdr() {
372            let xdr_entries = vec!["dGVzdA==".to_string()]; // Valid base64 but not valid XDR
373            let result = decode_xdr_auth_entries(xdr_entries);
374            assert!(result.is_err());
375        }
376
377        #[test]
378        fn test_empty_list() {
379            let xdr_entries = vec![];
380            let result = decode_xdr_auth_entries(xdr_entries);
381            assert!(result.is_ok());
382            assert_eq!(result.unwrap().len(), 0);
383        }
384    }
385
386    mod generate_default_auth_entries_tests {
387        use super::*;
388        use soroban_rs::xdr::Uint256;
389
390        #[test]
391        fn test_create_contract() {
392            let host_function = HostFunction::CreateContract(CreateContractArgs {
393                contract_id_preimage: ContractIdPreimage::Address(ContractIdPreimageFromAddress {
394                    address: ScAddress::Account(AccountId(XdrPublicKey::PublicKeyTypeEd25519(
395                        Uint256([0u8; 32]),
396                    ))),
397                    salt: Uint256([0u8; 32]),
398                }),
399                executable: ContractExecutable::Wasm(Hash([0u8; 32])),
400            });
401
402            let result = generate_default_auth_entries(&host_function);
403            assert!(result.is_ok());
404            assert_eq!(result.unwrap().len(), 1);
405        }
406
407        #[test]
408        fn test_create_contract_v2() {
409            let host_function = HostFunction::CreateContractV2(CreateContractArgsV2 {
410                contract_id_preimage: ContractIdPreimage::Address(ContractIdPreimageFromAddress {
411                    address: ScAddress::Account(AccountId(XdrPublicKey::PublicKeyTypeEd25519(
412                        Uint256([0u8; 32]),
413                    ))),
414                    salt: Uint256([0u8; 32]),
415                }),
416                executable: ContractExecutable::Wasm(Hash([0u8; 32])),
417                constructor_args: VecM::default(),
418            });
419
420            let result = generate_default_auth_entries(&host_function);
421            assert!(result.is_ok());
422            assert_eq!(result.unwrap().len(), 1);
423        }
424
425        #[test]
426        fn test_invoke_contract() {
427            let host_function = HostFunction::InvokeContract(soroban_rs::xdr::InvokeContractArgs {
428                contract_address: ScAddress::Contract(Hash([0u8; 32])),
429                function_name: soroban_rs::xdr::ScSymbol::try_from(b"test".to_vec()).unwrap(),
430                args: VecM::default(),
431            });
432
433            let result = generate_default_auth_entries(&host_function);
434            assert!(result.is_ok());
435            assert_eq!(result.unwrap().len(), 1);
436        }
437
438        #[test]
439        fn test_other_operations() {
440            let host_function = HostFunction::UploadContractWasm(vec![0u8; 10].try_into().unwrap());
441
442            let result = generate_default_auth_entries(&host_function);
443            assert!(result.is_ok());
444            assert_eq!(result.unwrap().len(), 0);
445        }
446    }
447
448    mod build_auth_vector_tests {
449        use super::*;
450        use soroban_rs::xdr::Uint256;
451
452        #[test]
453        fn test_simple_auth() {
454            let host_function = HostFunction::CreateContract(CreateContractArgs {
455                contract_id_preimage: ContractIdPreimage::Address(ContractIdPreimageFromAddress {
456                    address: ScAddress::Account(AccountId(XdrPublicKey::PublicKeyTypeEd25519(
457                        Uint256([0u8; 32]),
458                    ))),
459                    salt: Uint256([0u8; 32]),
460                }),
461                executable: ContractExecutable::Wasm(Hash([0u8; 32])),
462            });
463
464            let auth = Some(AuthSpec::SourceAccount);
465            let result = build_auth_vector(auth, &host_function);
466
467            assert!(result.is_ok());
468            assert_eq!(result.unwrap().len(), 1);
469        }
470
471        #[test]
472        fn test_xdr_auth_invalid() {
473            let host_function = HostFunction::CreateContract(CreateContractArgs {
474                contract_id_preimage: ContractIdPreimage::Address(ContractIdPreimageFromAddress {
475                    address: ScAddress::Account(AccountId(XdrPublicKey::PublicKeyTypeEd25519(
476                        Uint256([0u8; 32]),
477                    ))),
478                    salt: Uint256([0u8; 32]),
479                }),
480                executable: ContractExecutable::Wasm(Hash([0u8; 32])),
481            });
482
483            let auth = Some(AuthSpec::Xdr {
484                entries: vec!["invalid".to_string()],
485            });
486            let result = build_auth_vector(auth, &host_function);
487
488            assert!(result.is_err());
489        }
490
491        #[test]
492        fn test_none_default_create_contract() {
493            let host_function = HostFunction::CreateContract(CreateContractArgs {
494                contract_id_preimage: ContractIdPreimage::Address(ContractIdPreimageFromAddress {
495                    address: ScAddress::Account(AccountId(XdrPublicKey::PublicKeyTypeEd25519(
496                        Uint256([0u8; 32]),
497                    ))),
498                    salt: Uint256([0u8; 32]),
499                }),
500                executable: ContractExecutable::Wasm(Hash([0u8; 32])),
501            });
502
503            let result = build_auth_vector(None, &host_function);
504
505            assert!(result.is_ok());
506            assert_eq!(result.unwrap().len(), 1);
507        }
508
509        #[test]
510        fn test_none_default_invoke_contract() {
511            let host_function = HostFunction::InvokeContract(soroban_rs::xdr::InvokeContractArgs {
512                contract_address: ScAddress::Contract(Hash([0u8; 32])),
513                function_name: soroban_rs::xdr::ScSymbol::try_from(b"test".to_vec()).unwrap(),
514                args: VecM::default(),
515            });
516
517            let result = build_auth_vector(None, &host_function);
518
519            assert!(result.is_ok());
520            assert_eq!(result.unwrap().len(), 1);
521        }
522    }
523
524    mod convert_payment_operation_tests {
525        use super::*;
526
527        #[test]
528        fn test_with_native_asset() {
529            let result = convert_payment_operation(TEST_PK.to_string(), 1000, AssetSpec::Native);
530
531            assert!(result.is_ok());
532            if let Operation {
533                body: OperationBody::Payment(op),
534                ..
535            } = result.unwrap()
536            {
537                assert_eq!(op.amount, 1000);
538                assert!(matches!(op.asset, soroban_rs::xdr::Asset::Native));
539            } else {
540                panic!("Expected Payment operation");
541            }
542        }
543
544        #[test]
545        fn test_with_credit_asset() {
546            let result = convert_payment_operation(
547                TEST_PK.to_string(),
548                500,
549                AssetSpec::Credit4 {
550                    code: "USDC".to_string(),
551                    issuer: TEST_PK.to_string(),
552                },
553            );
554
555            assert!(result.is_ok());
556        }
557
558        #[test]
559        fn test_invalid_destination() {
560            let result = convert_payment_operation("INVALID".to_string(), 1000, AssetSpec::Native);
561
562            assert!(result.is_err());
563        }
564
565        #[test]
566        fn test_invalid_asset() {
567            let result = convert_payment_operation(
568                TEST_PK.to_string(),
569                1000,
570                AssetSpec::Credit4 {
571                    code: "TOOLONG".to_string(),
572                    issuer: TEST_PK.to_string(),
573                },
574            );
575
576            assert!(result.is_err());
577        }
578    }
579
580    mod convert_invoke_contract_operation_tests {
581        use super::*;
582
583        #[test]
584        fn test_basic_contract_invocation() {
585            let result = convert_invoke_contract_operation(
586                TEST_CONTRACT.to_string(),
587                "test".to_string(),
588                vec![],
589                None,
590            );
591            assert!(result.is_ok());
592        }
593
594        #[test]
595        fn test_with_auth() {
596            let auth = Some(AuthSpec::SourceAccount);
597            let result = convert_invoke_contract_operation(
598                TEST_CONTRACT.to_string(),
599                "transfer".to_string(),
600                vec![],
601                auth,
602            );
603
604            assert!(result.is_ok());
605            if let Operation {
606                body: OperationBody::InvokeHostFunction(op),
607                ..
608            } = result.unwrap()
609            {
610                assert_eq!(op.auth.len(), 1);
611            } else {
612                panic!("Expected InvokeHostFunction operation");
613            }
614        }
615    }
616
617    mod convert_create_contract_operation_tests {
618        use super::*;
619
620        #[test]
621        fn test_create_contract() {
622            let source = ContractSource::Address {
623                address: TEST_PK.to_string(),
624            };
625            let wasm_hash =
626                "0000000000000000000000000000000000000000000000000000000000000001".to_string();
627
628            let result = convert_create_contract_operation(source, wasm_hash, None, None, None);
629            assert!(result.is_ok());
630        }
631    }
632
633    // Integration tests
634    #[test]
635    fn test_payment_operation() {
636        let spec = OperationSpec::Payment {
637            destination: TEST_PK.to_string(),
638            amount: 1000,
639            asset: AssetSpec::Native,
640        };
641
642        let result = Operation::try_from(spec);
643        assert!(result.is_ok());
644        assert!(matches!(result.unwrap().body, OperationBody::Payment(_)));
645    }
646
647    #[test]
648    fn test_invoke_contract_operation() {
649        let spec = OperationSpec::InvokeContract {
650            contract_address: TEST_CONTRACT.to_string(),
651            function_name: "test".to_string(),
652            args: vec![],
653            auth: None,
654        };
655
656        let result = Operation::try_from(spec);
657        assert!(result.is_ok());
658        assert!(matches!(
659            result.unwrap().body,
660            OperationBody::InvokeHostFunction(_)
661        ));
662    }
663
664    #[test]
665    fn test_operation_spec_serde() {
666        let spec = OperationSpec::Payment {
667            destination: TEST_PK.to_string(),
668            amount: 1000,
669            asset: AssetSpec::Native,
670        };
671        let json = serde_json::to_string(&spec).unwrap();
672        assert!(json.contains("payment"));
673        assert!(json.contains("native"));
674
675        let deserialized: OperationSpec = serde_json::from_str(&json).unwrap();
676        assert_eq!(spec, deserialized);
677    }
678
679    #[test]
680    fn test_auth_spec_serde() {
681        let spec = AuthSpec::SourceAccount;
682        let json = serde_json::to_string(&spec).unwrap();
683        assert!(json.contains("source_account"));
684
685        let deserialized: AuthSpec = serde_json::from_str(&json).unwrap();
686        assert_eq!(spec, deserialized);
687    }
688
689    #[test]
690    fn test_auth_spec_json_format() {
691        // Test None
692        let none = AuthSpec::None;
693        let none_json = serde_json::to_value(&none).unwrap();
694        assert_eq!(none_json["type"], "none");
695
696        // Test SourceAccount
697        let source = AuthSpec::SourceAccount;
698        let source_json = serde_json::to_value(&source).unwrap();
699        assert_eq!(source_json["type"], "source_account");
700
701        // Test Addresses
702        let addresses = AuthSpec::Addresses {
703            signers: vec![TEST_PK.to_string()],
704        };
705        let addresses_json = serde_json::to_value(&addresses).unwrap();
706        assert_eq!(addresses_json["type"], "addresses");
707        assert!(addresses_json["signers"].is_array());
708
709        // Test Xdr
710        let xdr = AuthSpec::Xdr {
711            entries: vec!["base64data".to_string()],
712        };
713        let xdr_json = serde_json::to_value(&xdr).unwrap();
714        assert_eq!(xdr_json["type"], "xdr");
715        assert!(xdr_json["entries"].is_array());
716    }
717
718    #[test]
719    fn test_operation_spec_json_format() {
720        // Test Payment
721        let payment = OperationSpec::Payment {
722            destination: TEST_PK.to_string(),
723            amount: 1000,
724            asset: AssetSpec::Native,
725        };
726        let payment_json = serde_json::to_value(&payment).unwrap();
727        assert_eq!(payment_json["type"], "payment");
728        assert_eq!(payment_json["asset"]["type"], "native");
729
730        // Test InvokeContract
731        let invoke = OperationSpec::InvokeContract {
732            contract_address: TEST_CONTRACT.to_string(),
733            function_name: "test".to_string(),
734            args: vec![],
735            auth: None,
736        };
737        let invoke_json = serde_json::to_value(&invoke).unwrap();
738        assert_eq!(invoke_json["type"], "invoke_contract");
739        assert_eq!(invoke_json["contract_address"], TEST_CONTRACT);
740        assert_eq!(invoke_json["function_name"], "test");
741        assert!(invoke_json["args"].is_array());
742    }
743
744    #[test]
745    fn test_invoke_contract_with_source_account_auth_integration() {
746        // Create a realistic InvokeContract operation with source account auth
747        let spec = OperationSpec::InvokeContract {
748            contract_address: TEST_CONTRACT.to_string(),
749            function_name: "transfer".to_string(),
750            args: vec![], // In real scenario, this would contain transfer args
751            auth: Some(AuthSpec::SourceAccount),
752        };
753
754        // Convert to XDR Operation
755        let result = Operation::try_from(spec);
756        assert!(result.is_ok());
757
758        let operation = result.unwrap();
759        match operation.body {
760            OperationBody::InvokeHostFunction(ref invoke_op) => {
761                // Verify auth entries were created
762                assert_eq!(invoke_op.auth.len(), 1);
763
764                // Verify it's a source account credential
765                let auth_entry = &invoke_op.auth[0];
766                assert!(matches!(
767                    auth_entry.credentials,
768                    SorobanCredentials::SourceAccount
769                ));
770
771                // Verify the authorized function matches our contract invocation
772                match &auth_entry.root_invocation.function {
773                    SorobanAuthorizedFunction::ContractFn(invoke_args) => {
774                        // The contract address and function name should match
775                        assert!(matches!(
776                            invoke_args.contract_address,
777                            ScAddress::Contract(_)
778                        ));
779                        assert_eq!(invoke_args.function_name.0.as_slice(), b"transfer");
780                    }
781                    _ => panic!("Expected ContractFn authorization"),
782                }
783            }
784            _ => panic!("Expected InvokeHostFunction operation"),
785        }
786    }
787
788    #[test]
789    fn test_invoke_contract_with_none_auth_gets_default() {
790        // Create InvokeContract operation with NO auth specified
791        let spec = OperationSpec::InvokeContract {
792            contract_address: TEST_CONTRACT.to_string(),
793            function_name: "mint".to_string(),
794            args: vec![],
795            auth: None, // No auth specified - should get default source account
796        };
797
798        // Convert to XDR Operation
799        let result = Operation::try_from(spec);
800        assert!(result.is_ok());
801
802        let operation = result.unwrap();
803        match operation.body {
804            OperationBody::InvokeHostFunction(ref invoke_op) => {
805                // Verify default auth entry was created
806                assert_eq!(invoke_op.auth.len(), 1);
807
808                // Verify it's a source account credential
809                let auth_entry = &invoke_op.auth[0];
810                assert!(matches!(
811                    auth_entry.credentials,
812                    SorobanCredentials::SourceAccount
813                ));
814
815                // Verify the authorized function matches our contract invocation
816                match &auth_entry.root_invocation.function {
817                    SorobanAuthorizedFunction::ContractFn(invoke_args) => {
818                        assert_eq!(invoke_args.function_name.0.as_slice(), b"mint");
819                    }
820                    _ => panic!("Expected ContractFn authorization"),
821                }
822            }
823            _ => panic!("Expected InvokeHostFunction operation"),
824        }
825    }
826}