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, 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(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(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: {}",
360                    e
361                ))
362            })?;
363
364            let create_args_v2 = CreateContractArgsV2 {
365                contract_id_preimage: preimage,
366                executable: ContractExecutable::Wasm(wasm_hash),
367                constructor_args: constructor_args_vec,
368            };
369
370            return Ok(HostFunction::CreateContractV2(create_args_v2));
371        }
372    }
373
374    // No constructor args, use v1
375    let create_args = CreateContractArgs {
376        contract_id_preimage: preimage,
377        executable: ContractExecutable::Wasm(wasm_hash),
378    };
379
380    Ok(HostFunction::CreateContract(create_args))
381}
382
383impl TryFrom<HostFunctionSpec> for HostFunction {
384    type Error = SignerError;
385
386    fn try_from(spec: HostFunctionSpec) -> Result<Self, Self::Error> {
387        match spec {
388            HostFunctionSpec::InvokeContract {
389                contract_address,
390                function_name,
391                args,
392            } => convert_invoke_contract(contract_address, function_name, args),
393
394            HostFunctionSpec::UploadWasm { wasm } => convert_upload_wasm(wasm),
395
396            HostFunctionSpec::CreateContract {
397                source,
398                wasm_hash,
399                salt,
400                constructor_args,
401            } => convert_create_contract(source, wasm_hash, salt, constructor_args),
402        }
403    }
404}
405
406#[cfg(test)]
407mod tests {
408    use super::*;
409    use serde_json::json;
410
411    const TEST_PK: &str = "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF";
412    const TEST_CONTRACT: &str = "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA";
413
414    mod wasm_source_to_bytes_tests {
415        use super::*;
416
417        #[test]
418        fn test_hex_conversion() {
419            let wasm = WasmSource::Hex {
420                hex: "deadbeef".to_string(),
421            };
422            let result = wasm_source_to_bytes(wasm).unwrap();
423            assert_eq!(result, vec![0xde, 0xad, 0xbe, 0xef]);
424        }
425
426        #[test]
427        fn test_base64_conversion() {
428            let wasm = WasmSource::Base64 {
429                base64: "3q2+7w==".to_string(), // base64 for "deadbeef"
430            };
431            let result = wasm_source_to_bytes(wasm).unwrap();
432            assert_eq!(result, vec![0xde, 0xad, 0xbe, 0xef]);
433        }
434
435        #[test]
436        fn test_invalid_hex() {
437            let wasm = WasmSource::Hex {
438                hex: "invalid_hex".to_string(),
439            };
440            let result = wasm_source_to_bytes(wasm);
441            assert!(result.is_err());
442            assert!(result.unwrap_err().to_string().contains("Invalid hex"));
443        }
444
445        #[test]
446        fn test_invalid_base64() {
447            let wasm = WasmSource::Base64 {
448                base64: "!!!invalid!!!".to_string(),
449            };
450            let result = wasm_source_to_bytes(wasm);
451            assert!(result.is_err());
452            assert!(result.unwrap_err().to_string().contains("Invalid base64"));
453        }
454    }
455
456    mod parse_salt_bytes_tests {
457        use super::*;
458
459        #[test]
460        fn test_valid_32_byte_hex() {
461            let salt = Some(
462                "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string(),
463            );
464            let result = parse_salt_bytes(salt).unwrap();
465            assert_eq!(result.len(), 32);
466            assert_eq!(result[0], 0x01);
467            assert_eq!(result[1], 0x23);
468        }
469
470        #[test]
471        fn test_none_returns_zeros() {
472            let result = parse_salt_bytes(None).unwrap();
473            assert_eq!(result, [0u8; 32]);
474        }
475
476        #[test]
477        fn test_invalid_hex() {
478            let salt = Some("gg".to_string()); // Invalid hex
479            let result = parse_salt_bytes(salt);
480            assert!(result.is_err());
481            assert!(result.unwrap_err().to_string().contains("Invalid salt hex"));
482        }
483
484        #[test]
485        fn test_wrong_length() {
486            let salt = Some("abcd".to_string()); // Too short
487            let result = parse_salt_bytes(salt);
488            assert!(result.is_err());
489            assert!(result
490                .unwrap_err()
491                .to_string()
492                .contains("Salt must be 32 bytes"));
493        }
494    }
495
496    mod parse_wasm_hash_tests {
497        use super::*;
498
499        #[test]
500        fn test_valid_32_byte_hex() {
501            let hash_hex = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
502            let result = parse_wasm_hash(hash_hex).unwrap();
503            assert_eq!(result.0[0], 0x01);
504            assert_eq!(result.0[31], 0xef);
505        }
506
507        #[test]
508        fn test_invalid_hex() {
509            let result = parse_wasm_hash("invalid_hex");
510            assert!(result.is_err());
511            assert!(result.unwrap_err().to_string().contains("Invalid hex"));
512        }
513
514        #[test]
515        fn test_wrong_length() {
516            let result = parse_wasm_hash("abcd");
517            assert!(result.is_err());
518            assert!(result
519                .unwrap_err()
520                .to_string()
521                .contains("Hash must be 32 bytes"));
522        }
523    }
524
525    mod build_contract_preimage_tests {
526        use super::*;
527
528        #[test]
529        fn test_with_address_source() {
530            let source = ContractSource::Address {
531                address: TEST_PK.to_string(),
532            };
533            let result = build_contract_preimage(source, None).unwrap();
534
535            if let ContractIdPreimage::Address(preimage) = result {
536                assert!(matches!(preimage.address, ScAddress::Account(_)));
537                assert_eq!(preimage.salt.0, [0u8; 32]);
538            } else {
539                panic!("Expected Address preimage");
540            }
541        }
542
543        #[test]
544        fn test_with_contract_source() {
545            let source = ContractSource::Contract {
546                contract: TEST_CONTRACT.to_string(),
547            };
548            let result = build_contract_preimage(source, None).unwrap();
549
550            if let ContractIdPreimage::Address(preimage) = result {
551                assert!(matches!(preimage.address, ScAddress::Contract(_)));
552                assert_eq!(preimage.salt.0, [0u8; 32]);
553            } else {
554                panic!("Expected Address preimage");
555            }
556        }
557
558        #[test]
559        fn test_with_custom_salt() {
560            let source = ContractSource::Address {
561                address: TEST_PK.to_string(),
562            };
563            let salt = Some(
564                "0000000000000000000000000000000000000000000000000000000000000042".to_string(),
565            );
566            let result = build_contract_preimage(source, salt).unwrap();
567
568            if let ContractIdPreimage::Address(preimage) = result {
569                assert_eq!(preimage.salt.0[31], 0x42);
570            } else {
571                panic!("Expected Address preimage");
572            }
573        }
574
575        #[test]
576        fn test_invalid_address() {
577            let source = ContractSource::Address {
578                address: "INVALID".to_string(),
579            };
580            let result = build_contract_preimage(source, None);
581            assert!(result.is_err());
582        }
583    }
584
585    mod convert_invoke_contract_tests {
586        use super::*;
587
588        #[test]
589        fn test_valid_contract_address() {
590            let result =
591                convert_invoke_contract(TEST_CONTRACT.to_string(), "hello".to_string(), vec![]);
592            assert!(result.is_ok());
593
594            if let HostFunction::InvokeContract(args) = result.unwrap() {
595                assert!(matches!(args.contract_address, ScAddress::Contract(_)));
596                assert_eq!(args.function_name.to_utf8_string_lossy(), "hello");
597                assert_eq!(args.args.len(), 0);
598            } else {
599                panic!("Expected InvokeContract");
600            }
601        }
602
603        #[test]
604        fn test_function_name_conversion() {
605            let result = convert_invoke_contract(
606                TEST_CONTRACT.to_string(),
607                "transfer_tokens".to_string(),
608                vec![],
609            );
610            assert!(result.is_ok());
611
612            if let HostFunction::InvokeContract(args) = result.unwrap() {
613                assert_eq!(args.function_name.to_utf8_string_lossy(), "transfer_tokens");
614            } else {
615                panic!("Expected InvokeContract");
616            }
617        }
618
619        #[test]
620        fn test_various_arg_types() {
621            let args = vec![
622                json!({"u64": 1000}),
623                json!({"string": "hello"}),
624                json!({"address": TEST_PK}),
625            ];
626            let result =
627                convert_invoke_contract(TEST_CONTRACT.to_string(), "test".to_string(), args);
628            assert!(result.is_ok());
629
630            if let HostFunction::InvokeContract(invoke_args) = result.unwrap() {
631                assert_eq!(invoke_args.args.len(), 3);
632            } else {
633                panic!("Expected InvokeContract");
634            }
635        }
636
637        #[test]
638        fn test_invalid_contract_address() {
639            let result =
640                convert_invoke_contract("INVALID".to_string(), "hello".to_string(), vec![]);
641            assert!(result.is_err());
642        }
643    }
644
645    mod convert_upload_wasm_tests {
646        use super::*;
647
648        #[test]
649        fn test_hex_source() {
650            let wasm = WasmSource::Hex {
651                hex: "deadbeef".to_string(),
652            };
653            let result = convert_upload_wasm(wasm);
654            assert!(result.is_ok());
655
656            if let HostFunction::UploadContractWasm(bytes) = result.unwrap() {
657                assert_eq!(bytes.to_vec(), vec![0xde, 0xad, 0xbe, 0xef]);
658            } else {
659                panic!("Expected UploadContractWasm");
660            }
661        }
662
663        #[test]
664        fn test_base64_source() {
665            let wasm = WasmSource::Base64 {
666                base64: "3q2+7w==".to_string(),
667            };
668            let result = convert_upload_wasm(wasm);
669            assert!(result.is_ok());
670        }
671
672        #[test]
673        fn test_invalid_wasm() {
674            let wasm = WasmSource::Hex {
675                hex: "invalid".to_string(),
676            };
677            let result = convert_upload_wasm(wasm);
678            assert!(result.is_err());
679        }
680    }
681
682    mod convert_create_contract_tests {
683        use super::*;
684
685        #[test]
686        fn test_v1_no_constructor_args() {
687            let source = ContractSource::Address {
688                address: TEST_PK.to_string(),
689            };
690            let wasm_hash =
691                "0000000000000000000000000000000000000000000000000000000000000001".to_string();
692            let result = convert_create_contract(source, wasm_hash, None, None);
693
694            assert!(result.is_ok());
695            assert!(matches!(result.unwrap(), HostFunction::CreateContract(_)));
696        }
697
698        #[test]
699        fn test_v2_with_constructor_args() {
700            let source = ContractSource::Address {
701                address: TEST_PK.to_string(),
702            };
703            let wasm_hash =
704                "0000000000000000000000000000000000000000000000000000000000000001".to_string();
705            let args = Some(vec![json!({"string": "hello"}), json!({"u64": 42})]);
706            let result = convert_create_contract(source, wasm_hash, None, args);
707
708            assert!(result.is_ok());
709            if let HostFunction::CreateContractV2(args) = result.unwrap() {
710                assert_eq!(args.constructor_args.len(), 2);
711            } else {
712                panic!("Expected CreateContractV2");
713            }
714        }
715
716        #[test]
717        fn test_empty_constructor_args_uses_v1() {
718            let source = ContractSource::Address {
719                address: TEST_PK.to_string(),
720            };
721            let wasm_hash =
722                "0000000000000000000000000000000000000000000000000000000000000001".to_string();
723            let args = Some(vec![]);
724            let result = convert_create_contract(source, wasm_hash, None, args);
725
726            assert!(result.is_ok());
727            assert!(matches!(result.unwrap(), HostFunction::CreateContract(_)));
728        }
729
730        #[test]
731        fn test_salt_handling() {
732            let source = ContractSource::Address {
733                address: TEST_PK.to_string(),
734            };
735            let wasm_hash =
736                "0000000000000000000000000000000000000000000000000000000000000001".to_string();
737            let salt = Some(
738                "0000000000000000000000000000000000000000000000000000000000000042".to_string(),
739            );
740            let result = convert_create_contract(source, wasm_hash, salt, None);
741
742            assert!(result.is_ok());
743        }
744    }
745
746    // Integration tests
747    #[test]
748    fn test_invoke_contract() {
749        let spec = HostFunctionSpec::InvokeContract {
750            contract_address: TEST_CONTRACT.to_string(),
751            function_name: "hello".to_string(),
752            args: vec![json!({"string": "world"})],
753        };
754
755        let result = HostFunction::try_from(spec);
756        assert!(result.is_ok());
757        assert!(matches!(result.unwrap(), HostFunction::InvokeContract(_)));
758    }
759
760    #[test]
761    fn test_upload_wasm() {
762        let spec = HostFunctionSpec::UploadWasm {
763            wasm: WasmSource::Hex {
764                hex: "deadbeef".to_string(),
765            },
766        };
767
768        let result = HostFunction::try_from(spec);
769        assert!(result.is_ok());
770        assert!(matches!(
771            result.unwrap(),
772            HostFunction::UploadContractWasm(_)
773        ));
774    }
775
776    #[test]
777    fn test_create_contract_v1() {
778        let spec = HostFunctionSpec::CreateContract {
779            source: ContractSource::Address {
780                address: TEST_PK.to_string(),
781            },
782            wasm_hash: "0000000000000000000000000000000000000000000000000000000000000001"
783                .to_string(),
784            salt: None,
785            constructor_args: None,
786        };
787
788        let result = HostFunction::try_from(spec);
789        assert!(result.is_ok());
790        assert!(matches!(result.unwrap(), HostFunction::CreateContract(_)));
791    }
792
793    #[test]
794    fn test_create_contract_v2() {
795        let spec = HostFunctionSpec::CreateContract {
796            source: ContractSource::Address {
797                address: TEST_PK.to_string(),
798            },
799            wasm_hash: "0000000000000000000000000000000000000000000000000000000000000001"
800                .to_string(),
801            salt: None,
802            constructor_args: Some(vec![json!({"string": "init"})]),
803        };
804
805        let result = HostFunction::try_from(spec);
806        assert!(result.is_ok());
807        assert!(matches!(result.unwrap(), HostFunction::CreateContractV2(_)));
808    }
809
810    #[test]
811    fn test_host_function_spec_serde() {
812        let spec = HostFunctionSpec::InvokeContract {
813            contract_address: TEST_CONTRACT.to_string(),
814            function_name: "test".to_string(),
815            args: vec![json!({"u64": 42})],
816        };
817        let json = serde_json::to_string(&spec).unwrap();
818        assert!(json.contains("invoke_contract"));
819        assert!(json.contains(TEST_CONTRACT));
820
821        let deserialized: HostFunctionSpec = serde_json::from_str(&json).unwrap();
822        assert_eq!(spec, deserialized);
823    }
824
825    #[test]
826    fn test_u64_string_to_number_conversion() {
827        // Test direct u64 conversion
828        let args = vec![
829            json!({"u64": "1000"}),
830            json!({"i64": "-500"}),
831            json!({"timepoint": "123456"}),
832            json!({"duration": "7890"}),
833        ];
834
835        let result = convert_invoke_contract(TEST_CONTRACT.to_string(), "test".to_string(), args);
836        assert!(
837            result.is_ok(),
838            "Should successfully convert string u64/i64 to numbers"
839        );
840
841        // Test nested u128 parts
842        let u128_arg = vec![json!({"u128": {"hi": "100", "lo": "200"}})];
843        let result =
844            convert_invoke_contract(TEST_CONTRACT.to_string(), "test".to_string(), u128_arg);
845        assert!(result.is_ok(), "Should successfully convert u128 parts");
846
847        // Test nested i128 parts
848        let i128_arg = vec![json!({"i128": {"hi": "-100", "lo": "200"}})];
849        let result =
850            convert_invoke_contract(TEST_CONTRACT.to_string(), "test".to_string(), i128_arg);
851        assert!(result.is_ok(), "Should successfully convert i128 parts");
852
853        // Test nested u256 parts
854        let u256_arg =
855            vec![json!({"u256": {"hi_hi": "1", "hi_lo": "2", "lo_hi": "3", "lo_lo": "4"}})];
856        let result =
857            convert_invoke_contract(TEST_CONTRACT.to_string(), "test".to_string(), u256_arg);
858        assert!(result.is_ok(), "Should successfully convert u256 parts");
859
860        // Test nested i256 parts
861        let i256_arg =
862            vec![json!({"i256": {"hi_hi": "-1", "hi_lo": "2", "lo_hi": "3", "lo_lo": "4"}})];
863        let result =
864            convert_invoke_contract(TEST_CONTRACT.to_string(), "test".to_string(), i256_arg);
865        assert!(result.is_ok(), "Should successfully convert i256 parts");
866    }
867
868    #[test]
869    fn test_host_function_spec_json_format() {
870        // Test InvokeContract
871        let invoke = HostFunctionSpec::InvokeContract {
872            contract_address: TEST_CONTRACT.to_string(),
873            function_name: "test".to_string(),
874            args: vec![json!({"u64": 42})],
875        };
876        let invoke_json = serde_json::to_value(&invoke).unwrap();
877        assert_eq!(invoke_json["type"], "invoke_contract");
878        assert_eq!(invoke_json["contract_address"], TEST_CONTRACT);
879        assert_eq!(invoke_json["function_name"], "test");
880
881        // Test UploadWasm
882        let upload = HostFunctionSpec::UploadWasm {
883            wasm: WasmSource::Hex {
884                hex: "deadbeef".to_string(),
885            },
886        };
887        let upload_json = serde_json::to_value(&upload).unwrap();
888        assert_eq!(upload_json["type"], "upload_wasm");
889        assert!(upload_json["wasm"].is_object());
890
891        // Test CreateContract
892        let create = HostFunctionSpec::CreateContract {
893            source: ContractSource::Address {
894                address: TEST_PK.to_string(),
895            },
896            wasm_hash: "0000000000000000000000000000000000000000000000000000000000000001"
897                .to_string(),
898            salt: None,
899            constructor_args: None,
900        };
901        let create_json = serde_json::to_value(&create).unwrap();
902        assert_eq!(create_json["type"], "create_contract");
903        assert_eq!(create_json["source"]["from"], "address");
904        assert_eq!(create_json["source"]["address"], TEST_PK);
905    }
906}