openzeppelin_relayer/models/transaction/stellar/
host_function.rs

1//! Host function types and conversions for Stellar transactions
2
3use crate::models::SignerError;
4use serde::{Deserialize, Serialize};
5use soroban_rs::xdr::{
6    AccountId, ContractExecutable, ContractId, ContractIdPreimage, ContractIdPreimageFromAddress,
7    CreateContractArgs, CreateContractArgsV2, Hash, HostFunction, InvokeContractArgs,
8    PublicKey as XdrPublicKey, ScAddress, ScSymbol, ScVal, Uint256, VecM,
9};
10use std::convert::TryFrom;
11use utoipa::ToSchema;
12
13/// HACK: Temporary fix for stellar-xdr bug where u64/i64 values are expected as numbers
14/// but are provided as strings. This recursively converts string values to numbers for:
15/// - {"u64":"1000"} to {"u64":1000}
16/// - {"i64":"-1000"} to {"i64":-1000}
17/// - {"timepoint":"1000"} to {"timepoint":1000}
18/// - {"duration":"1000"} to {"duration":1000}
19/// - UInt128Parts: {"hi":"1", "lo":"2"} to {"hi":1, "lo":2}
20/// - Int128Parts: {"hi":"-1", "lo":"2"} to {"hi":-1, "lo":2}
21/// - UInt256Parts: {"hi_hi":"1", "hi_lo":"2", "lo_hi":"3", "lo_lo":"4"} to numbers
22/// - Int256Parts: {"hi_hi":"-1", "hi_lo":"2", "lo_hi":"3", "lo_lo":"4"} to numbers
23///
24/// TODO: Remove this once stellar-xdr properly handles u64/i64 as strings.
25/// Track the issue at: https://github.com/stellar/rs-stellar-xdr
26fn fix_u64_format(value: &mut serde_json::Value) {
27    match value {
28        serde_json::Value::Object(map) => {
29            // Handle single-field u64/i64 objects
30            if map.len() == 1 {
31                if let Some(serde_json::Value::String(s)) = map.get("u64") {
32                    if let Ok(num) = s.parse::<u64>() {
33                        map.insert("u64".to_string(), serde_json::json!(num));
34                    }
35                } else if let Some(serde_json::Value::String(s)) = map.get("i64") {
36                    if let Ok(num) = s.parse::<i64>() {
37                        map.insert("i64".to_string(), serde_json::json!(num));
38                    }
39                } else if let Some(serde_json::Value::String(s)) = map.get("timepoint") {
40                    if let Ok(num) = s.parse::<u64>() {
41                        map.insert("timepoint".to_string(), serde_json::json!(num));
42                    }
43                } else if let Some(serde_json::Value::String(s)) = map.get("duration") {
44                    if let Ok(num) = s.parse::<u64>() {
45                        map.insert("duration".to_string(), serde_json::json!(num));
46                    }
47                }
48            }
49
50            // Handle UInt128Parts (hi: u64, lo: u64)
51            if map.contains_key("hi") && map.contains_key("lo") && map.len() == 2 {
52                if let Some(serde_json::Value::String(s)) = map.get("hi") {
53                    if let Ok(num) = s.parse::<u64>() {
54                        map.insert("hi".to_string(), serde_json::json!(num));
55                    }
56                }
57                if let Some(serde_json::Value::String(s)) = map.get("lo") {
58                    if let Ok(num) = s.parse::<u64>() {
59                        map.insert("lo".to_string(), serde_json::json!(num));
60                    }
61                }
62            }
63
64            // Handle u128 wrapper object
65            if map.contains_key("u128") {
66                if let Some(serde_json::Value::Object(inner)) = map.get_mut("u128") {
67                    // Convert UInt128Parts (hi: u64, lo: u64)
68                    if let Some(serde_json::Value::String(s)) = inner.get("hi") {
69                        if let Ok(num) = s.parse::<u64>() {
70                            inner.insert("hi".to_string(), serde_json::json!(num));
71                        }
72                    }
73                    if let Some(serde_json::Value::String(s)) = inner.get("lo") {
74                        if let Ok(num) = s.parse::<u64>() {
75                            inner.insert("lo".to_string(), serde_json::json!(num));
76                        }
77                    }
78                }
79            }
80
81            // Handle i128 wrapper object
82            if map.contains_key("i128") {
83                if let Some(serde_json::Value::Object(inner)) = map.get_mut("i128") {
84                    // Convert Int128Parts (hi: i64, lo: u64)
85                    if let Some(serde_json::Value::String(s)) = inner.get("hi") {
86                        if let Ok(num) = s.parse::<i64>() {
87                            inner.insert("hi".to_string(), serde_json::json!(num));
88                        }
89                    }
90                    if let Some(serde_json::Value::String(s)) = inner.get("lo") {
91                        if let Ok(num) = s.parse::<u64>() {
92                            inner.insert("lo".to_string(), serde_json::json!(num));
93                        }
94                    }
95                }
96            }
97
98            // Handle u256 wrapper object
99            if map.contains_key("u256") {
100                if let Some(serde_json::Value::Object(inner)) = map.get_mut("u256") {
101                    // Convert UInt256Parts (all u64)
102                    for key in ["hi_hi", "hi_lo", "lo_hi", "lo_lo"] {
103                        if let Some(serde_json::Value::String(s)) = inner.get(key) {
104                            if let Ok(num) = s.parse::<u64>() {
105                                inner.insert(key.to_string(), serde_json::json!(num));
106                            }
107                        }
108                    }
109                }
110            }
111
112            // Handle i256 wrapper object
113            if map.contains_key("i256") {
114                if let Some(serde_json::Value::Object(inner)) = map.get_mut("i256") {
115                    // Convert Int256Parts (hi_hi: i64, others: u64)
116                    if let Some(serde_json::Value::String(s)) = inner.get("hi_hi") {
117                        if let Ok(num) = s.parse::<i64>() {
118                            inner.insert("hi_hi".to_string(), serde_json::json!(num));
119                        }
120                    }
121                    for key in ["hi_lo", "lo_hi", "lo_lo"] {
122                        if let Some(serde_json::Value::String(s)) = inner.get(key) {
123                            if let Ok(num) = s.parse::<u64>() {
124                                inner.insert(key.to_string(), serde_json::json!(num));
125                            }
126                        }
127                    }
128                }
129            }
130
131            // Also handle direct UInt256Parts (all u64) without wrapper
132            if map.contains_key("hi_hi")
133                && map.contains_key("hi_lo")
134                && map.contains_key("lo_hi")
135                && map.contains_key("lo_lo")
136                && map.len() == 4
137            {
138                for key in ["hi_hi", "hi_lo", "lo_hi", "lo_lo"] {
139                    if let Some(serde_json::Value::String(s)) = map.get(key) {
140                        if let Ok(num) = s.parse::<u64>() {
141                            map.insert(key.to_string(), serde_json::json!(num));
142                        }
143                    }
144                }
145            }
146
147            // Recursively process nested structures
148            for (_, v) in map.iter_mut() {
149                fix_u64_format(v);
150            }
151        }
152        serde_json::Value::Array(arr) => {
153            // Recursively fix all array elements
154            for v in arr.iter_mut() {
155                fix_u64_format(v);
156            }
157        }
158        _ => {}
159    }
160}
161
162/// Represents different ways to provide WASM code
163#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)]
164#[serde(untagged)]
165pub enum WasmSource {
166    Hex { hex: String },
167    Base64 { base64: String },
168}
169
170/// Represents the source for contract creation
171#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)]
172#[serde(tag = "from", rename_all = "snake_case")]
173pub enum ContractSource {
174    Address { address: String }, // Account address that will own the contract
175    Contract { contract: String }, // Existing contract address
176}
177
178/// Represents different host function specifications
179#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)]
180#[serde(tag = "type", rename_all = "snake_case")]
181pub enum HostFunctionSpec {
182    // Contract invocation
183    InvokeContract {
184        contract_address: String,
185        function_name: String,
186        args: Vec<serde_json::Value>,
187    },
188
189    // WASM upload
190    UploadWasm {
191        wasm: WasmSource,
192    },
193
194    // Contract creation with explicit fields
195    CreateContract {
196        source: ContractSource,
197        wasm_hash: String, // hex-encoded
198        #[serde(skip_serializing_if = "Option::is_none")]
199        salt: Option<String>, // hex-encoded, defaults to zeros
200        #[serde(skip_serializing_if = "Option::is_none")]
201        constructor_args: Option<Vec<serde_json::Value>>,
202    },
203}
204
205// Helper functions for HostFunctionSpec conversion
206
207/// Converts a WasmSource to bytes
208fn wasm_source_to_bytes(wasm: WasmSource) -> Result<Vec<u8>, SignerError> {
209    match wasm {
210        WasmSource::Hex { hex } => hex::decode(&hex)
211            .map_err(|e| SignerError::ConversionError(format!("Invalid hex in wasm: {e}"))),
212        WasmSource::Base64 { base64 } => {
213            use base64::{engine::general_purpose, Engine as _};
214            general_purpose::STANDARD
215                .decode(&base64)
216                .map_err(|e| SignerError::ConversionError(format!("Invalid base64 in wasm: {e}")))
217        }
218    }
219}
220
221/// Parses and validates salt bytes from optional hex string
222fn parse_salt_bytes(salt: Option<String>) -> Result<[u8; 32], SignerError> {
223    if let Some(salt_hex) = salt {
224        let bytes = hex::decode(&salt_hex)
225            .map_err(|e| SignerError::ConversionError(format!("Invalid salt hex: {e}")))?;
226        if bytes.len() != 32 {
227            return Err(SignerError::ConversionError("Salt must be 32 bytes".into()));
228        }
229        let mut array = [0u8; 32];
230        array.copy_from_slice(&bytes);
231        Ok(array)
232    } else {
233        Ok([0u8; 32]) // Default to zeros
234    }
235}
236
237/// Converts hex string to 32-byte hash
238fn parse_wasm_hash(wasm_hash: &str) -> Result<Hash, SignerError> {
239    let hash_bytes = hex::decode(wasm_hash)
240        .map_err(|e| SignerError::ConversionError(format!("Invalid hex in wasm_hash: {e}")))?;
241    if hash_bytes.len() != 32 {
242        return Err(SignerError::ConversionError(format!(
243            "Hash must be 32 bytes, got {}",
244            hash_bytes.len()
245        )));
246    }
247    let mut hash_array = [0u8; 32];
248    hash_array.copy_from_slice(&hash_bytes);
249    Ok(Hash(hash_array))
250}
251
252/// Builds contract ID preimage from contract source
253fn build_contract_preimage(
254    source: ContractSource,
255    salt: Option<String>,
256) -> Result<ContractIdPreimage, SignerError> {
257    let salt_bytes = parse_salt_bytes(salt)?;
258
259    match source {
260        ContractSource::Address { address } => {
261            let public_key =
262                stellar_strkey::ed25519::PublicKey::from_string(&address).map_err(|e| {
263                    SignerError::ConversionError(format!("Invalid account address: {e}"))
264                })?;
265            let account_id = AccountId(XdrPublicKey::PublicKeyTypeEd25519(Uint256(public_key.0)));
266
267            Ok(ContractIdPreimage::Address(ContractIdPreimageFromAddress {
268                address: ScAddress::Account(account_id),
269                salt: Uint256(salt_bytes),
270            }))
271        }
272        ContractSource::Contract { contract } => {
273            let contract_id = stellar_strkey::Contract::from_string(&contract).map_err(|e| {
274                SignerError::ConversionError(format!("Invalid contract address: {e}"))
275            })?;
276
277            Ok(ContractIdPreimage::Address(ContractIdPreimageFromAddress {
278                address: ScAddress::Contract(ContractId(Hash(contract_id.0))),
279                salt: Uint256(salt_bytes),
280            }))
281        }
282    }
283}
284
285/// Converts InvokeContract spec to HostFunction
286fn convert_invoke_contract(
287    contract_address: String,
288    function_name: String,
289    args: Vec<serde_json::Value>,
290) -> Result<HostFunction, SignerError> {
291    // Parse contract address
292    let contract = stellar_strkey::Contract::from_string(&contract_address)
293        .map_err(|e| SignerError::ConversionError(format!("Invalid contract address: {e}")))?;
294    let contract_addr = ScAddress::Contract(ContractId(Hash(contract.0)));
295
296    // Convert function name to symbol
297    let function_symbol = ScSymbol::try_from(function_name.as_bytes().to_vec())
298        .map_err(|e| SignerError::ConversionError(format!("Invalid function name: {e}")))?;
299
300    // Convert JSON args to ScVals using serde
301    // HACK: stellar-xdr expects u64 as number but it should be string
302    // Convert {"u64":"1000"} to {"u64":1000} before deserialization
303    let scval_args: Vec<ScVal> = args
304        .iter()
305        .map(|json| {
306            let mut modified_json = json.clone();
307            fix_u64_format(&mut modified_json);
308            serde_json::from_value(modified_json)
309        })
310        .collect::<Result<Vec<_>, _>>()
311        .map_err(|e| SignerError::ConversionError(format!("Failed to deserialize ScVal: {e}")))?;
312    let args_vec = VecM::try_from(scval_args)
313        .map_err(|e| SignerError::ConversionError(format!("Failed to convert arguments: {e}")))?;
314
315    Ok(HostFunction::InvokeContract(InvokeContractArgs {
316        contract_address: contract_addr,
317        function_name: function_symbol,
318        args: args_vec,
319    }))
320}
321
322/// Converts UploadWasm spec to HostFunction
323fn convert_upload_wasm(wasm: WasmSource) -> Result<HostFunction, SignerError> {
324    let bytes = wasm_source_to_bytes(wasm)?;
325    Ok(HostFunction::UploadContractWasm(bytes.try_into().map_err(
326        |e| SignerError::ConversionError(format!("Failed to convert wasm bytes: {e:?}")),
327    )?))
328}
329
330/// Converts CreateContract spec to HostFunction
331fn convert_create_contract(
332    source: ContractSource,
333    wasm_hash: String,
334    salt: Option<String>,
335    constructor_args: Option<Vec<serde_json::Value>>,
336) -> Result<HostFunction, SignerError> {
337    let preimage = build_contract_preimage(source, salt)?;
338    let wasm_hash = parse_wasm_hash(&wasm_hash)?;
339
340    // Handle constructor args if provided
341    if let Some(args) = constructor_args {
342        if !args.is_empty() {
343            // Convert JSON args to ScVals using serde
344            // HACK: stellar-xdr expects u64 as number but it should be string
345            // Convert {"u64":"1000"} to {"u64":1000} before deserialization
346            let scval_args: Vec<ScVal> = args
347                .iter()
348                .map(|json| {
349                    let mut modified_json = json.clone();
350                    fix_u64_format(&mut modified_json);
351                    serde_json::from_value(modified_json)
352                })
353                .collect::<Result<Vec<_>, _>>()
354                .map_err(|e| {
355                    SignerError::ConversionError(format!("Failed to deserialize ScVal: {e}"))
356                })?;
357            let constructor_args_vec = VecM::try_from(scval_args).map_err(|e| {
358                SignerError::ConversionError(format!(
359                    "Failed to convert constructor arguments: {e}"
360                ))
361            })?;
362
363            let create_args_v2 = CreateContractArgsV2 {
364                contract_id_preimage: preimage,
365                executable: ContractExecutable::Wasm(wasm_hash),
366                constructor_args: constructor_args_vec,
367            };
368
369            return Ok(HostFunction::CreateContractV2(create_args_v2));
370        }
371    }
372
373    // No constructor args, use v1
374    let create_args = CreateContractArgs {
375        contract_id_preimage: preimage,
376        executable: ContractExecutable::Wasm(wasm_hash),
377    };
378
379    Ok(HostFunction::CreateContract(create_args))
380}
381
382impl TryFrom<HostFunctionSpec> for HostFunction {
383    type Error = SignerError;
384
385    fn try_from(spec: HostFunctionSpec) -> Result<Self, Self::Error> {
386        match spec {
387            HostFunctionSpec::InvokeContract {
388                contract_address,
389                function_name,
390                args,
391            } => convert_invoke_contract(contract_address, function_name, args),
392
393            HostFunctionSpec::UploadWasm { wasm } => convert_upload_wasm(wasm),
394
395            HostFunctionSpec::CreateContract {
396                source,
397                wasm_hash,
398                salt,
399                constructor_args,
400            } => convert_create_contract(source, wasm_hash, salt, constructor_args),
401        }
402    }
403}
404
405#[cfg(test)]
406mod tests {
407    use super::*;
408    use serde_json::json;
409
410    const TEST_PK: &str = "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF";
411    const TEST_CONTRACT: &str = "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA";
412
413    mod wasm_source_to_bytes_tests {
414        use super::*;
415
416        #[test]
417        fn test_hex_conversion() {
418            let wasm = WasmSource::Hex {
419                hex: "deadbeef".to_string(),
420            };
421            let result = wasm_source_to_bytes(wasm).unwrap();
422            assert_eq!(result, vec![0xde, 0xad, 0xbe, 0xef]);
423        }
424
425        #[test]
426        fn test_base64_conversion() {
427            let wasm = WasmSource::Base64 {
428                base64: "3q2+7w==".to_string(), // base64 for "deadbeef"
429            };
430            let result = wasm_source_to_bytes(wasm).unwrap();
431            assert_eq!(result, vec![0xde, 0xad, 0xbe, 0xef]);
432        }
433
434        #[test]
435        fn test_invalid_hex() {
436            let wasm = WasmSource::Hex {
437                hex: "invalid_hex".to_string(),
438            };
439            let result = wasm_source_to_bytes(wasm);
440            assert!(result.is_err());
441            assert!(result.unwrap_err().to_string().contains("Invalid hex"));
442        }
443
444        #[test]
445        fn test_invalid_base64() {
446            let wasm = WasmSource::Base64 {
447                base64: "!!!invalid!!!".to_string(),
448            };
449            let result = wasm_source_to_bytes(wasm);
450            assert!(result.is_err());
451            assert!(result.unwrap_err().to_string().contains("Invalid base64"));
452        }
453    }
454
455    mod parse_salt_bytes_tests {
456        use super::*;
457
458        #[test]
459        fn test_valid_32_byte_hex() {
460            let salt = Some(
461                "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string(),
462            );
463            let result = parse_salt_bytes(salt).unwrap();
464            assert_eq!(result.len(), 32);
465            assert_eq!(result[0], 0x01);
466            assert_eq!(result[1], 0x23);
467        }
468
469        #[test]
470        fn test_none_returns_zeros() {
471            let result = parse_salt_bytes(None).unwrap();
472            assert_eq!(result, [0u8; 32]);
473        }
474
475        #[test]
476        fn test_invalid_hex() {
477            let salt = Some("gg".to_string()); // Invalid hex
478            let result = parse_salt_bytes(salt);
479            assert!(result.is_err());
480            assert!(result.unwrap_err().to_string().contains("Invalid salt hex"));
481        }
482
483        #[test]
484        fn test_wrong_length() {
485            let salt = Some("abcd".to_string()); // Too short
486            let result = parse_salt_bytes(salt);
487            assert!(result.is_err());
488            assert!(result
489                .unwrap_err()
490                .to_string()
491                .contains("Salt must be 32 bytes"));
492        }
493    }
494
495    mod parse_wasm_hash_tests {
496        use super::*;
497
498        #[test]
499        fn test_valid_32_byte_hex() {
500            let hash_hex = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
501            let result = parse_wasm_hash(hash_hex).unwrap();
502            assert_eq!(result.0[0], 0x01);
503            assert_eq!(result.0[31], 0xef);
504        }
505
506        #[test]
507        fn test_invalid_hex() {
508            let result = parse_wasm_hash("invalid_hex");
509            assert!(result.is_err());
510            assert!(result.unwrap_err().to_string().contains("Invalid hex"));
511        }
512
513        #[test]
514        fn test_wrong_length() {
515            let result = parse_wasm_hash("abcd");
516            assert!(result.is_err());
517            assert!(result
518                .unwrap_err()
519                .to_string()
520                .contains("Hash must be 32 bytes"));
521        }
522    }
523
524    mod build_contract_preimage_tests {
525        use super::*;
526
527        #[test]
528        fn test_with_address_source() {
529            let source = ContractSource::Address {
530                address: TEST_PK.to_string(),
531            };
532            let result = build_contract_preimage(source, None).unwrap();
533
534            if let ContractIdPreimage::Address(preimage) = result {
535                assert!(matches!(preimage.address, ScAddress::Account(_)));
536                assert_eq!(preimage.salt.0, [0u8; 32]);
537            } else {
538                panic!("Expected Address preimage");
539            }
540        }
541
542        #[test]
543        fn test_with_contract_source() {
544            let source = ContractSource::Contract {
545                contract: TEST_CONTRACT.to_string(),
546            };
547            let result = build_contract_preimage(source, None).unwrap();
548
549            if let ContractIdPreimage::Address(preimage) = result {
550                assert!(matches!(preimage.address, ScAddress::Contract(_)));
551                assert_eq!(preimage.salt.0, [0u8; 32]);
552            } else {
553                panic!("Expected Address preimage");
554            }
555        }
556
557        #[test]
558        fn test_with_custom_salt() {
559            let source = ContractSource::Address {
560                address: TEST_PK.to_string(),
561            };
562            let salt = Some(
563                "0000000000000000000000000000000000000000000000000000000000000042".to_string(),
564            );
565            let result = build_contract_preimage(source, salt).unwrap();
566
567            if let ContractIdPreimage::Address(preimage) = result {
568                assert_eq!(preimage.salt.0[31], 0x42);
569            } else {
570                panic!("Expected Address preimage");
571            }
572        }
573
574        #[test]
575        fn test_invalid_address() {
576            let source = ContractSource::Address {
577                address: "INVALID".to_string(),
578            };
579            let result = build_contract_preimage(source, None);
580            assert!(result.is_err());
581        }
582    }
583
584    mod convert_invoke_contract_tests {
585        use super::*;
586
587        #[test]
588        fn test_valid_contract_address() {
589            let result =
590                convert_invoke_contract(TEST_CONTRACT.to_string(), "hello".to_string(), vec![]);
591            assert!(result.is_ok());
592
593            if let HostFunction::InvokeContract(args) = result.unwrap() {
594                assert!(matches!(args.contract_address, ScAddress::Contract(_)));
595                assert_eq!(args.function_name.to_utf8_string_lossy(), "hello");
596                assert_eq!(args.args.len(), 0);
597            } else {
598                panic!("Expected InvokeContract");
599            }
600        }
601
602        #[test]
603        fn test_function_name_conversion() {
604            let result = convert_invoke_contract(
605                TEST_CONTRACT.to_string(),
606                "transfer_tokens".to_string(),
607                vec![],
608            );
609            assert!(result.is_ok());
610
611            if let HostFunction::InvokeContract(args) = result.unwrap() {
612                assert_eq!(args.function_name.to_utf8_string_lossy(), "transfer_tokens");
613            } else {
614                panic!("Expected InvokeContract");
615            }
616        }
617
618        #[test]
619        fn test_various_arg_types() {
620            let args = vec![
621                json!({"u64": 1000}),
622                json!({"string": "hello"}),
623                json!({"address": TEST_PK}),
624            ];
625            let result =
626                convert_invoke_contract(TEST_CONTRACT.to_string(), "test".to_string(), args);
627            assert!(result.is_ok());
628
629            if let HostFunction::InvokeContract(invoke_args) = result.unwrap() {
630                assert_eq!(invoke_args.args.len(), 3);
631            } else {
632                panic!("Expected InvokeContract");
633            }
634        }
635
636        #[test]
637        fn test_invalid_contract_address() {
638            let result =
639                convert_invoke_contract("INVALID".to_string(), "hello".to_string(), vec![]);
640            assert!(result.is_err());
641        }
642    }
643
644    mod convert_upload_wasm_tests {
645        use super::*;
646
647        #[test]
648        fn test_hex_source() {
649            let wasm = WasmSource::Hex {
650                hex: "deadbeef".to_string(),
651            };
652            let result = convert_upload_wasm(wasm);
653            assert!(result.is_ok());
654
655            if let HostFunction::UploadContractWasm(bytes) = result.unwrap() {
656                assert_eq!(bytes.to_vec(), vec![0xde, 0xad, 0xbe, 0xef]);
657            } else {
658                panic!("Expected UploadContractWasm");
659            }
660        }
661
662        #[test]
663        fn test_base64_source() {
664            let wasm = WasmSource::Base64 {
665                base64: "3q2+7w==".to_string(),
666            };
667            let result = convert_upload_wasm(wasm);
668            assert!(result.is_ok());
669        }
670
671        #[test]
672        fn test_invalid_wasm() {
673            let wasm = WasmSource::Hex {
674                hex: "invalid".to_string(),
675            };
676            let result = convert_upload_wasm(wasm);
677            assert!(result.is_err());
678        }
679    }
680
681    mod convert_create_contract_tests {
682        use super::*;
683
684        #[test]
685        fn test_v1_no_constructor_args() {
686            let source = ContractSource::Address {
687                address: TEST_PK.to_string(),
688            };
689            let wasm_hash =
690                "0000000000000000000000000000000000000000000000000000000000000001".to_string();
691            let result = convert_create_contract(source, wasm_hash, None, None);
692
693            assert!(result.is_ok());
694            assert!(matches!(result.unwrap(), HostFunction::CreateContract(_)));
695        }
696
697        #[test]
698        fn test_v2_with_constructor_args() {
699            let source = ContractSource::Address {
700                address: TEST_PK.to_string(),
701            };
702            let wasm_hash =
703                "0000000000000000000000000000000000000000000000000000000000000001".to_string();
704            let args = Some(vec![json!({"string": "hello"}), json!({"u64": 42})]);
705            let result = convert_create_contract(source, wasm_hash, None, args);
706
707            assert!(result.is_ok());
708            if let HostFunction::CreateContractV2(args) = result.unwrap() {
709                assert_eq!(args.constructor_args.len(), 2);
710            } else {
711                panic!("Expected CreateContractV2");
712            }
713        }
714
715        #[test]
716        fn test_empty_constructor_args_uses_v1() {
717            let source = ContractSource::Address {
718                address: TEST_PK.to_string(),
719            };
720            let wasm_hash =
721                "0000000000000000000000000000000000000000000000000000000000000001".to_string();
722            let args = Some(vec![]);
723            let result = convert_create_contract(source, wasm_hash, None, args);
724
725            assert!(result.is_ok());
726            assert!(matches!(result.unwrap(), HostFunction::CreateContract(_)));
727        }
728
729        #[test]
730        fn test_salt_handling() {
731            let source = ContractSource::Address {
732                address: TEST_PK.to_string(),
733            };
734            let wasm_hash =
735                "0000000000000000000000000000000000000000000000000000000000000001".to_string();
736            let salt = Some(
737                "0000000000000000000000000000000000000000000000000000000000000042".to_string(),
738            );
739            let result = convert_create_contract(source, wasm_hash, salt, None);
740
741            assert!(result.is_ok());
742        }
743    }
744
745    // Integration tests
746    #[test]
747    fn test_invoke_contract() {
748        let spec = HostFunctionSpec::InvokeContract {
749            contract_address: TEST_CONTRACT.to_string(),
750            function_name: "hello".to_string(),
751            args: vec![json!({"string": "world"})],
752        };
753
754        let result = HostFunction::try_from(spec);
755        assert!(result.is_ok());
756        assert!(matches!(result.unwrap(), HostFunction::InvokeContract(_)));
757    }
758
759    #[test]
760    fn test_upload_wasm() {
761        let spec = HostFunctionSpec::UploadWasm {
762            wasm: WasmSource::Hex {
763                hex: "deadbeef".to_string(),
764            },
765        };
766
767        let result = HostFunction::try_from(spec);
768        assert!(result.is_ok());
769        assert!(matches!(
770            result.unwrap(),
771            HostFunction::UploadContractWasm(_)
772        ));
773    }
774
775    #[test]
776    fn test_create_contract_v1() {
777        let spec = HostFunctionSpec::CreateContract {
778            source: ContractSource::Address {
779                address: TEST_PK.to_string(),
780            },
781            wasm_hash: "0000000000000000000000000000000000000000000000000000000000000001"
782                .to_string(),
783            salt: None,
784            constructor_args: None,
785        };
786
787        let result = HostFunction::try_from(spec);
788        assert!(result.is_ok());
789        assert!(matches!(result.unwrap(), HostFunction::CreateContract(_)));
790    }
791
792    #[test]
793    fn test_create_contract_v2() {
794        let spec = HostFunctionSpec::CreateContract {
795            source: ContractSource::Address {
796                address: TEST_PK.to_string(),
797            },
798            wasm_hash: "0000000000000000000000000000000000000000000000000000000000000001"
799                .to_string(),
800            salt: None,
801            constructor_args: Some(vec![json!({"string": "init"})]),
802        };
803
804        let result = HostFunction::try_from(spec);
805        assert!(result.is_ok());
806        assert!(matches!(result.unwrap(), HostFunction::CreateContractV2(_)));
807    }
808
809    #[test]
810    fn test_host_function_spec_serde() {
811        let spec = HostFunctionSpec::InvokeContract {
812            contract_address: TEST_CONTRACT.to_string(),
813            function_name: "test".to_string(),
814            args: vec![json!({"u64": 42})],
815        };
816        let json = serde_json::to_string(&spec).unwrap();
817        assert!(json.contains("invoke_contract"));
818        assert!(json.contains(TEST_CONTRACT));
819
820        let deserialized: HostFunctionSpec = serde_json::from_str(&json).unwrap();
821        assert_eq!(spec, deserialized);
822    }
823
824    #[test]
825    fn test_u64_string_to_number_conversion() {
826        // Test direct u64 conversion
827        let args = vec![
828            json!({"u64": "1000"}),
829            json!({"i64": "-500"}),
830            json!({"timepoint": "123456"}),
831            json!({"duration": "7890"}),
832        ];
833
834        let result = convert_invoke_contract(TEST_CONTRACT.to_string(), "test".to_string(), args);
835        assert!(
836            result.is_ok(),
837            "Should successfully convert string u64/i64 to numbers"
838        );
839
840        // Test nested u128 parts
841        let u128_arg = vec![json!({"u128": {"hi": "100", "lo": "200"}})];
842        let result =
843            convert_invoke_contract(TEST_CONTRACT.to_string(), "test".to_string(), u128_arg);
844        assert!(result.is_ok(), "Should successfully convert u128 parts");
845
846        // Test nested i128 parts
847        let i128_arg = vec![json!({"i128": {"hi": "-100", "lo": "200"}})];
848        let result =
849            convert_invoke_contract(TEST_CONTRACT.to_string(), "test".to_string(), i128_arg);
850        assert!(result.is_ok(), "Should successfully convert i128 parts");
851
852        // Test nested u256 parts
853        let u256_arg =
854            vec![json!({"u256": {"hi_hi": "1", "hi_lo": "2", "lo_hi": "3", "lo_lo": "4"}})];
855        let result =
856            convert_invoke_contract(TEST_CONTRACT.to_string(), "test".to_string(), u256_arg);
857        assert!(result.is_ok(), "Should successfully convert u256 parts");
858
859        // Test nested i256 parts
860        let i256_arg =
861            vec![json!({"i256": {"hi_hi": "-1", "hi_lo": "2", "lo_hi": "3", "lo_lo": "4"}})];
862        let result =
863            convert_invoke_contract(TEST_CONTRACT.to_string(), "test".to_string(), i256_arg);
864        assert!(result.is_ok(), "Should successfully convert i256 parts");
865    }
866
867    #[test]
868    fn test_host_function_spec_json_format() {
869        // Test InvokeContract
870        let invoke = HostFunctionSpec::InvokeContract {
871            contract_address: TEST_CONTRACT.to_string(),
872            function_name: "test".to_string(),
873            args: vec![json!({"u64": 42})],
874        };
875        let invoke_json = serde_json::to_value(&invoke).unwrap();
876        assert_eq!(invoke_json["type"], "invoke_contract");
877        assert_eq!(invoke_json["contract_address"], TEST_CONTRACT);
878        assert_eq!(invoke_json["function_name"], "test");
879
880        // Test UploadWasm
881        let upload = HostFunctionSpec::UploadWasm {
882            wasm: WasmSource::Hex {
883                hex: "deadbeef".to_string(),
884            },
885        };
886        let upload_json = serde_json::to_value(&upload).unwrap();
887        assert_eq!(upload_json["type"], "upload_wasm");
888        assert!(upload_json["wasm"].is_object());
889
890        // Test CreateContract
891        let create = HostFunctionSpec::CreateContract {
892            source: ContractSource::Address {
893                address: TEST_PK.to_string(),
894            },
895            wasm_hash: "0000000000000000000000000000000000000000000000000000000000000001"
896                .to_string(),
897            salt: None,
898            constructor_args: None,
899        };
900        let create_json = serde_json::to_value(&create).unwrap();
901        assert_eq!(create_json["type"], "create_contract");
902        assert_eq!(create_json["source"]["from"], "address");
903        assert_eq!(create_json["source"]["address"], TEST_PK);
904    }
905}