Skip to main content

neo_syscalls/
wrapper.rs

1// Copyright (c) 2025-2026 R3E Network
2// Licensed under the MIT License
3
4//! Neo N3 syscall wrapper and helper functions.
5
6use neo_types::*;
7
8#[cfg(not(target_arch = "wasm32"))]
9use crate::storage::*;
10use crate::syscalls::SYSCALLS;
11use crate::NeoVMSyscallInfo;
12
13#[cfg(target_arch = "wasm32")]
14#[link(wasm_import_module = "neo")]
15extern "C" {
16    #[link_name = "runtime_check_witness_bytes"]
17    fn neo_runtime_check_witness_bytes(ptr: i32, len: i32) -> i32;
18
19    #[link_name = "runtime_check_witness_i64"]
20    fn neo_runtime_check_witness_i64(account: i64) -> i32;
21
22    #[link_name = "runtime_get_time"]
23    fn neo_runtime_get_time() -> i64;
24
25    #[link_name = "runtime_get_calling_script_hash_i64"]
26    fn neo_runtime_get_calling_script_hash_i64() -> i64;
27
28    #[link_name = "runtime_get_entry_script_hash_i64"]
29    fn neo_runtime_get_entry_script_hash_i64() -> i64;
30
31    #[link_name = "runtime_get_executing_script_hash_i64"]
32    fn neo_runtime_get_executing_script_hash_i64() -> i64;
33
34    #[link_name = "runtime_log"]
35    fn neo_runtime_log(ptr: i32, len: i32);
36
37    #[link_name = "runtime_notify"]
38    fn neo_runtime_notify(event_ptr: i32, event_len: i32);
39
40    /// Lowered to `CALL_L` -> helper that emits
41    ///   `SYSCALL System.Storage.GetContext;
42    ///    SUBSTR <key>; SUBSTR <value>;
43    ///    SYSCALL System.Storage.Put`.
44    #[link_name = "neo_storage_put_bytes"]
45    fn neo_storage_put_bytes(key_ptr: i32, key_len: i32, value_ptr: i32, value_len: i32);
46
47    /// Lowered to `CALL_L` -> helper that emits
48    ///   `SYSCALL System.Storage.GetContext;
49    ///    SUBSTR <key>;
50    ///    SYSCALL System.Storage.Delete`.
51    #[link_name = "neo_storage_delete_bytes"]
52    fn neo_storage_delete_bytes(key_ptr: i32, key_len: i32);
53
54    /// Lowered to `CALL_L` -> helper that emits the `Get` SYSCALL and then
55    /// copies the returned `ByteString` back into wasm memory at `out_ptr`.
56    /// Returns:
57    ///   - the stored value's length on success (`>= 0`),
58    ///   - `-1` if the key is not present in storage,
59    ///   - `-needed_len` if the caller-supplied buffer was too small.
60    #[link_name = "neo_storage_get_into"]
61    fn neo_storage_get_into(key_ptr: i32, key_len: i32, out_ptr: i32, out_cap: i32) -> i32;
62}
63
64#[cfg(not(target_arch = "wasm32"))]
65const CALL_FLAGS_VALID_MASK: i32 = 0x0F;
66#[cfg(not(target_arch = "wasm32"))]
67const CALL_FLAGS_READ_STATES: i32 = 0x01;
68#[cfg(not(target_arch = "wasm32"))]
69const CALL_FLAGS_WRITE_STATES: i32 = 0x02;
70
71fn find_syscall(name: &str) -> Option<&'static NeoVMSyscallInfo> {
72    SYSCALLS.iter().find(|info| info.name == name)
73}
74
75fn syscall_hash(name: &str) -> NeoResult<u32> {
76    find_syscall(name)
77        .map(|info| info.hash)
78        .ok_or_else(|| NeoError::new(&format!("unknown syscall: {name}")))
79}
80
81fn default_value_for(return_type: &str) -> NeoValue {
82    match return_type {
83        "Void" => NeoValue::Null,
84        // Fail-closed by default for unknown boolean-returning syscalls.
85        "Boolean" => NeoBoolean::FALSE.into(),
86        "Integer" => NeoInteger::new(0).into(),
87        "Hash160" => NeoByteString::new(vec![0u8; 20]).into(),
88        "ByteString" => NeoByteString::new(vec![0u8; 1]).into(),
89        "String" => NeoString::from_str("Neo N3").into(),
90        "Array" => NeoArray::<NeoValue>::new().into(),
91        "Iterator" => NeoArray::<NeoValue>::new().into(),
92        "StackItem" => NeoArray::<NeoValue>::new().into(),
93        "StorageContext" => NeoValue::Null,
94        _ => NeoValue::Null,
95    }
96}
97
98fn value_matches_param_type(value: &NeoValue, param_type: &str) -> bool {
99    match param_type {
100        "Boolean" => value.as_boolean().is_some(),
101        "Integer" => value.as_integer().is_some(),
102        "Hash160" => {
103            value.is_null()
104                || value
105                    .as_byte_string()
106                    .map(|bytes| bytes.len() == 20)
107                    .unwrap_or(false)
108        }
109        "ByteString" => value.as_byte_string().is_some(),
110        "String" => value.as_string().is_some(),
111        "Array" => value.as_array().is_some(),
112        "Iterator" => value.as_array().is_some(),
113        "StorageContext" => value.is_null() || value.as_integer().is_some(),
114        "StackItem" | "Any" | "ExecutionContext" => true,
115        _ => true,
116    }
117}
118
119#[cfg(not(target_arch = "wasm32"))]
120fn call_flags_allow_write(flags: i32) -> bool {
121    (flags & CALL_FLAGS_WRITE_STATES) != 0
122}
123
124#[cfg(not(target_arch = "wasm32"))]
125fn call_flags_allow_read(flags: i32) -> bool {
126    (flags & CALL_FLAGS_READ_STATES) != 0
127}
128
129#[cfg(not(target_arch = "wasm32"))]
130fn hash160_prefix_i64(hash: &[u8; 20]) -> i64 {
131    let mut buf = [0u8; 8];
132    buf.copy_from_slice(&hash[..8]);
133    i64::from_le_bytes(buf)
134}
135
136/// Neo N3 System Call Function
137pub fn neovm_syscall(hash: u32, args: &[NeoValue]) -> NeoResult<NeoValue> {
138    let registry = crate::NeoVMSyscallRegistry::get_instance();
139    let info = registry
140        .get_syscall_by_hash(hash)
141        .ok_or_else(|| NeoError::new(&format!("unknown syscall hash: 0x{hash:08x}")))?;
142
143    if args.len() != info.parameters.len() {
144        return Err(NeoError::new(&format!(
145            "invalid syscall argument count for {}: expected {}, got {}",
146            info.name,
147            info.parameters.len(),
148            args.len()
149        )));
150    }
151
152    for (index, (arg, expected_type)) in args.iter().zip(info.parameters.iter()).enumerate() {
153        if !value_matches_param_type(arg, expected_type) {
154            return Err(NeoError::new(&format!(
155                "invalid syscall argument type for {} param #{}: expected {}",
156                info.name, index, expected_type
157            )));
158        }
159    }
160
161    #[cfg(not(target_arch = "wasm32"))]
162    {
163        if info.name == "System.Runtime.CheckWitness" {
164            let has_witness = args
165                .first()
166                .and_then(NeoValue::as_byte_string)
167                .map(|account| has_active_witness(account.as_slice()))
168                .unwrap_or(false);
169            return Ok(NeoBoolean::new(has_witness).into());
170        }
171
172        if info.name == "System.Crypto.CheckSig" {
173            let results = active_crypto_verification_results();
174            return Ok(NeoBoolean::new(results.check_sig).into());
175        }
176
177        if info.name == "System.Crypto.CheckMultisig" {
178            let results = active_crypto_verification_results();
179            return Ok(NeoBoolean::new(results.check_multisig).into());
180        }
181
182        if info.name == "Neo.Crypto.VerifyWithECDsa" {
183            let results = active_crypto_verification_results();
184            return Ok(NeoBoolean::new(results.verify_with_ecdsa).into());
185        }
186
187        if info.name == "System.Runtime.GetCallingScriptHash" {
188            return Ok(NeoByteString::from_slice(&current_calling_script_hash()).into());
189        }
190
191        if info.name == "System.Runtime.GetEntryScriptHash" {
192            return Ok(NeoByteString::from_slice(&current_entry_script_hash()).into());
193        }
194
195        if info.name == "System.Runtime.GetExecutingScriptHash" {
196            return Ok(NeoByteString::from_slice(&current_executing_script_hash()).into());
197        }
198
199        if info.name == "System.Contract.GetCallFlags" {
200            return Ok(NeoInteger::new(current_call_flags()).into());
201        }
202    }
203
204    Ok(default_value_for(info.return_type))
205}
206
207/// Neo N3 System Call Wrapper
208pub struct NeoVMSyscall;
209
210impl NeoVMSyscall {
211    #[cfg(not(target_arch = "wasm32"))]
212    fn parse_hash160(hash: &NeoByteString) -> NeoResult<[u8; 20]> {
213        if hash.len() != 20 {
214            return Err(NeoError::InvalidArgument);
215        }
216        let mut value = [0u8; 20];
217        value.copy_from_slice(hash.as_slice());
218        Ok(value)
219    }
220
221    #[cfg(not(target_arch = "wasm32"))]
222    fn parse_call_flags(flags: &NeoInteger) -> NeoResult<i32> {
223        let parsed = flags.as_i32_saturating();
224        if parsed < 0 || (parsed & !CALL_FLAGS_VALID_MASK) != 0 {
225            return Err(NeoError::InvalidArgument);
226        }
227        Ok(parsed)
228    }
229
230    #[cfg(not(target_arch = "wasm32"))]
231    fn begin_contract_invocation_with_flags(
232        next_executing: &NeoByteString,
233        call_flags: i32,
234    ) -> NeoResult<()> {
235        if call_flags < 0 || (call_flags & !CALL_FLAGS_VALID_MASK) != 0 {
236            return Err(NeoError::InvalidArgument);
237        }
238        push_current_executing_script_hash(Self::parse_hash160(next_executing)?, call_flags)
239    }
240
241    /// Set the active contract hash used by host-mode storage contexts and script-hash syscalls.
242    #[cfg(not(target_arch = "wasm32"))]
243    pub fn set_active_contract_hash(hash: &NeoByteString) -> NeoResult<()> {
244        set_current_contract_hash(Self::parse_hash160(hash)?);
245        Ok(())
246    }
247
248    /// Configure host-mode calling/entry/executing script hashes.
249    #[cfg(not(target_arch = "wasm32"))]
250    pub fn set_active_script_hashes(
251        calling: &NeoByteString,
252        entry: &NeoByteString,
253        executing: &NeoByteString,
254    ) -> NeoResult<()> {
255        set_current_script_hashes(
256            Self::parse_hash160(calling)?,
257            Self::parse_hash160(entry)?,
258            Self::parse_hash160(executing)?,
259        );
260        Ok(())
261    }
262
263    /// Configure host-mode calling script hash.
264    /// Clears nested invocation frames and applies this value as a new base state.
265    #[cfg(not(target_arch = "wasm32"))]
266    pub fn set_active_calling_script_hash(hash: &NeoByteString) -> NeoResult<()> {
267        set_current_calling_script_hash(Self::parse_hash160(hash)?);
268        Ok(())
269    }
270
271    /// Configure host-mode entry script hash.
272    /// Clears nested invocation frames and applies this value as a new base state.
273    #[cfg(not(target_arch = "wasm32"))]
274    pub fn set_active_entry_script_hash(hash: &NeoByteString) -> NeoResult<()> {
275        set_current_entry_script_hash(Self::parse_hash160(hash)?);
276        Ok(())
277    }
278
279    /// Configure host-mode executing script hash.
280    /// Clears nested invocation frames and applies this value as a new base state.
281    #[cfg(not(target_arch = "wasm32"))]
282    pub fn set_active_executing_script_hash(hash: &NeoByteString) -> NeoResult<()> {
283        set_current_executing_script_hash(Self::parse_hash160(hash)?);
284        Ok(())
285    }
286
287    /// Configure host-mode active call flags (Neo N3 CallFlags mask: 0x00..=0x0F).
288    /// Clears nested invocation frames and applies this value as a new base state.
289    #[cfg(not(target_arch = "wasm32"))]
290    pub fn set_active_call_flags(call_flags: &NeoInteger) -> NeoResult<()> {
291        set_current_call_flags(Self::parse_call_flags(call_flags)?);
292        Ok(())
293    }
294
295    /// Enter a nested contract invocation frame in host mode.
296    ///
297    /// The new frame preserves `entry`, shifts `calling <- previous executing`,
298    /// and sets `executing` to `next_executing`.
299    #[cfg(not(target_arch = "wasm32"))]
300    pub fn begin_contract_invocation(next_executing: &NeoByteString) -> NeoResult<()> {
301        Self::begin_contract_invocation_with_flags(next_executing, current_call_flags())
302    }
303
304    /// Exit the most recent nested contract invocation frame in host mode.
305    #[cfg(not(target_arch = "wasm32"))]
306    pub fn end_contract_invocation() -> NeoResult<()> {
307        pop_current_script_hash_frame()
308    }
309
310    /// Run an operation in a nested host invocation frame, always unwinding the frame.
311    #[cfg(not(target_arch = "wasm32"))]
312    pub fn with_contract_invocation<T, F>(
313        next_executing: &NeoByteString,
314        operation: F,
315    ) -> NeoResult<T>
316    where
317        F: FnOnce() -> NeoResult<T>,
318    {
319        Self::begin_contract_invocation(next_executing)?;
320        let operation_result = operation();
321        let unwind_result = Self::end_contract_invocation();
322
323        match (operation_result, unwind_result) {
324            (Ok(value), Ok(())) => Ok(value),
325            (Err(err), Ok(())) => Err(err),
326            (Ok(_), Err(unwind_err)) => Err(unwind_err),
327            (Err(operation_err), Err(unwind_err)) => Err(NeoError::new(&format!(
328                "invocation operation failed ({}) and frame unwind failed ({})",
329                operation_err.message(),
330                unwind_err.message()
331            ))),
332        }
333    }
334
335    /// Set the active contract hash used by host-mode storage contexts and script-hash syscalls.
336    #[cfg(target_arch = "wasm32")]
337    pub fn set_active_contract_hash(_hash: &NeoByteString) -> NeoResult<()> {
338        Ok(())
339    }
340
341    /// Configure host-mode calling/entry/executing script hashes.
342    #[cfg(target_arch = "wasm32")]
343    pub fn set_active_script_hashes(
344        _calling: &NeoByteString,
345        _entry: &NeoByteString,
346        _executing: &NeoByteString,
347    ) -> NeoResult<()> {
348        Ok(())
349    }
350
351    /// Configure host-mode calling script hash.
352    #[cfg(target_arch = "wasm32")]
353    pub fn set_active_calling_script_hash(_hash: &NeoByteString) -> NeoResult<()> {
354        Ok(())
355    }
356
357    /// Configure host-mode entry script hash.
358    #[cfg(target_arch = "wasm32")]
359    pub fn set_active_entry_script_hash(_hash: &NeoByteString) -> NeoResult<()> {
360        Ok(())
361    }
362
363    /// Configure host-mode executing script hash.
364    #[cfg(target_arch = "wasm32")]
365    pub fn set_active_executing_script_hash(_hash: &NeoByteString) -> NeoResult<()> {
366        Ok(())
367    }
368
369    /// Configure host-mode active call flags.
370    #[cfg(target_arch = "wasm32")]
371    pub fn set_active_call_flags(_call_flags: &NeoInteger) -> NeoResult<()> {
372        Ok(())
373    }
374
375    /// Enter a nested contract invocation frame in host mode.
376    #[cfg(target_arch = "wasm32")]
377    pub fn begin_contract_invocation(_next_executing: &NeoByteString) -> NeoResult<()> {
378        Ok(())
379    }
380
381    /// Exit the most recent nested contract invocation frame in host mode.
382    #[cfg(target_arch = "wasm32")]
383    pub fn end_contract_invocation() -> NeoResult<()> {
384        Ok(())
385    }
386
387    /// Run an operation in a nested host invocation frame, always unwinding the frame.
388    #[cfg(target_arch = "wasm32")]
389    pub fn with_contract_invocation<T, F>(
390        _next_executing: &NeoByteString,
391        operation: F,
392    ) -> NeoResult<T>
393    where
394        F: FnOnce() -> NeoResult<T>,
395    {
396        operation()
397    }
398
399    /// Clear host-mode syscall/storage simulation state.
400    #[cfg(not(target_arch = "wasm32"))]
401    pub fn reset_host_state() -> NeoResult<()> {
402        STORAGE_STATE.reset()?;
403        reset_current_contract_hash();
404        clear_active_witnesses();
405        reset_crypto_verification_results();
406        Ok(())
407    }
408
409    /// Clear host-mode syscall/storage simulation state.
410    ///
411    /// On wasm32 this is a no-op: storage state lives in the Neo node's real
412    /// persistent store and is reset at the chain level (e.g. by tearing down
413    /// the Neo Express chain), not by the contract itself.
414    #[cfg(target_arch = "wasm32")]
415    pub fn reset_host_state() -> NeoResult<()> {
416        Ok(())
417    }
418
419    fn call_value(name: &str, args: &[NeoValue]) -> NeoResult<NeoValue> {
420        neovm_syscall(syscall_hash(name)?, args)
421    }
422
423    fn call_integer(name: &str) -> NeoResult<NeoInteger> {
424        let value = Self::call_value(name, &[])?;
425        value.as_integer().cloned().ok_or(NeoError::InvalidType)
426    }
427
428    fn call_boolean(name: &str, args: &[NeoValue]) -> NeoResult<NeoBoolean> {
429        let value = Self::call_value(name, args)?;
430        value.as_boolean().ok_or(NeoError::InvalidType)
431    }
432
433    fn call_bytes_with_args(name: &str, args: &[NeoValue]) -> NeoResult<NeoByteString> {
434        let value = Self::call_value(name, args)?;
435        value.as_byte_string().cloned().ok_or(NeoError::InvalidType)
436    }
437
438    fn call_string(name: &str) -> NeoResult<NeoString> {
439        let value = Self::call_value(name, &[])?;
440        value.as_string().cloned().ok_or(NeoError::InvalidType)
441    }
442
443    fn call_array(name: &str, args: &[NeoValue]) -> NeoResult<NeoArray<NeoValue>> {
444        let value = Self::call_value(name, args)?;
445        value.as_array().cloned().ok_or(NeoError::InvalidType)
446    }
447
448    /// Replace the active witness set used by host-mode `check_witness`.
449    #[cfg(not(target_arch = "wasm32"))]
450    pub fn set_active_witnesses(witnesses: &[NeoByteString]) -> NeoResult<()> {
451        crate::storage::set_active_witnesses(
452            witnesses.iter().map(|witness| witness.as_slice().to_vec()),
453        );
454        Ok(())
455    }
456
457    /// Replace the active witness set used by host-mode `check_witness`.
458    #[cfg(target_arch = "wasm32")]
459    pub fn set_active_witnesses(_witnesses: &[NeoByteString]) -> NeoResult<()> {
460        Ok(())
461    }
462
463    /// Configure host-mode CheckSig/CheckMultisig results.
464    ///
465    /// `verify_with_ecdsa` tracks `check_sig` unless overridden explicitly.
466    #[cfg(not(target_arch = "wasm32"))]
467    pub fn set_crypto_verification_results(check_sig: bool, check_multisig: bool) -> NeoResult<()> {
468        Self::set_crypto_verification_results_full(check_sig, check_multisig, check_sig)
469    }
470
471    /// Configure host-mode crypto syscall results (secure default: all false).
472    #[cfg(not(target_arch = "wasm32"))]
473    pub fn set_crypto_verification_results_full(
474        check_sig: bool,
475        check_multisig: bool,
476        verify_with_ecdsa: bool,
477    ) -> NeoResult<()> {
478        crate::storage::set_crypto_verification_results(CryptoVerificationResults {
479            check_sig,
480            check_multisig,
481            verify_with_ecdsa,
482        });
483        Ok(())
484    }
485
486    /// Configure host-mode VerifyWithECDsa syscall result.
487    #[cfg(not(target_arch = "wasm32"))]
488    pub fn set_verify_with_ecdsa_result(result: bool) -> NeoResult<()> {
489        let mut current = active_crypto_verification_results();
490        current.verify_with_ecdsa = result;
491        crate::storage::set_crypto_verification_results(current);
492        Ok(())
493    }
494
495    /// Configure host-mode CheckSig/CheckMultisig results.
496    #[cfg(target_arch = "wasm32")]
497    pub fn set_crypto_verification_results(
498        _check_sig: bool,
499        _check_multisig: bool,
500    ) -> NeoResult<()> {
501        Ok(())
502    }
503
504    /// Configure host-mode crypto syscall results (secure default: all false).
505    #[cfg(target_arch = "wasm32")]
506    pub fn set_crypto_verification_results_full(
507        _check_sig: bool,
508        _check_multisig: bool,
509        _verify_with_ecdsa: bool,
510    ) -> NeoResult<()> {
511        Ok(())
512    }
513
514    /// Configure host-mode VerifyWithECDsa syscall result.
515    #[cfg(target_arch = "wasm32")]
516    pub fn set_verify_with_ecdsa_result(_result: bool) -> NeoResult<()> {
517        Ok(())
518    }
519
520    /// Get current timestamp
521    pub fn get_time() -> NeoResult<NeoInteger> {
522        #[cfg(target_arch = "wasm32")]
523        {
524            return Ok(NeoInteger::new(unsafe { neo_runtime_get_time() }));
525        }
526
527        #[cfg(not(target_arch = "wasm32"))]
528        {
529            Self::call_integer("System.Runtime.GetTime")
530        }
531    }
532
533    /// Get current timestamp as a plain `i64`.
534    ///
535    /// This keeps wasm contracts on the direct syscall import path and avoids
536    /// pulling arbitrary-precision integer conversion code into small
537    /// contracts that only need the native timestamp.
538    pub fn get_time_i64() -> NeoResult<i64> {
539        #[cfg(target_arch = "wasm32")]
540        {
541            return Ok(unsafe { neo_runtime_get_time() });
542        }
543
544        #[cfg(not(target_arch = "wasm32"))]
545        {
546            Self::call_integer("System.Runtime.GetTime")?.try_into_i64()
547        }
548    }
549
550    /// Check if the specified account is a witness
551    pub fn check_witness(account: &NeoByteString) -> NeoResult<NeoBoolean> {
552        Self::check_witness_bytes(account.as_slice())
553    }
554
555    /// Check if the specified account hash/public key bytes are a witness.
556    pub fn check_witness_bytes(account: &[u8]) -> NeoResult<NeoBoolean> {
557        #[cfg(target_arch = "wasm32")]
558        {
559            let result = unsafe {
560                neo_runtime_check_witness_bytes(account.as_ptr() as i32, account.len() as i32)
561            };
562            return Ok(NeoBoolean::new(result != 0));
563        }
564
565        #[cfg(not(target_arch = "wasm32"))]
566        {
567            let args = [NeoValue::from(NeoByteString::from_slice(account))];
568            Self::call_boolean("System.Runtime.CheckWitness", &args)
569        }
570    }
571
572    /// Check a compact sample-account identifier as a witness.
573    ///
574    /// This helper exists for the repository sample contracts that expose
575    /// account IDs as integers. Production contracts should prefer
576    /// `check_witness`/`check_witness_bytes` with real Hash160 account bytes.
577    pub fn check_witness_i64(account: i64) -> NeoResult<NeoBoolean> {
578        #[cfg(target_arch = "wasm32")]
579        {
580            let result = unsafe { neo_runtime_check_witness_i64(account) };
581            return Ok(NeoBoolean::new(result != 0));
582        }
583
584        #[cfg(not(target_arch = "wasm32"))]
585        {
586            let mut bytes = [0u8; 20];
587            bytes[..8].copy_from_slice(&account.to_le_bytes());
588            Self::check_witness_bytes(&bytes)
589        }
590    }
591
592    /// Send notification to the runtime.
593    pub fn notify(event: &NeoString, state: &NeoArray<NeoValue>) -> NeoResult<()> {
594        #[cfg(target_arch = "wasm32")]
595        {
596            let _ = state;
597            return Self::notify_event(event.as_str());
598        }
599
600        #[cfg(not(target_arch = "wasm32"))]
601        {
602            let event_bytes = NeoByteString::from_slice(event.as_str().as_bytes());
603            let args = [NeoValue::from(event_bytes), NeoValue::from(state.clone())];
604            neovm_syscall(syscall_hash("System.Runtime.Notify")?, &args)?;
605            Ok(())
606        }
607    }
608
609    /// Send a notification with an empty state array.
610    pub fn notify_event(event: &str) -> NeoResult<()> {
611        #[cfg(target_arch = "wasm32")]
612        unsafe {
613            neo_runtime_notify(event.as_ptr() as i32, event.len() as i32);
614            Ok(())
615        }
616
617        #[cfg(not(target_arch = "wasm32"))]
618        {
619            let label = NeoString::from_str(event);
620            let state = NeoArray::new();
621            Self::notify(&label, &state)
622        }
623    }
624
625    /// Log message to the runtime.
626    pub fn log(message: &NeoString) -> NeoResult<()> {
627        #[cfg(target_arch = "wasm32")]
628        unsafe {
629            let message = message.as_str();
630            neo_runtime_log(message.as_ptr() as i32, message.len() as i32);
631            Ok(())
632        }
633
634        #[cfg(not(target_arch = "wasm32"))]
635        {
636            let message_bytes = NeoByteString::from_slice(message.as_str().as_bytes());
637            let args = [NeoValue::from(message_bytes)];
638            neovm_syscall(syscall_hash("System.Runtime.Log")?, &args)?;
639            Ok(())
640        }
641    }
642
643    /// Platform identifier
644    pub fn platform() -> NeoResult<NeoString> {
645        Self::call_string("System.Runtime.Platform")
646    }
647
648    pub fn get_trigger() -> NeoResult<NeoInteger> {
649        Self::call_integer("System.Runtime.GetTrigger")
650    }
651
652    pub fn get_invocation_counter() -> NeoResult<NeoInteger> {
653        Self::call_integer("System.Runtime.GetInvocationCounter")
654    }
655
656    pub fn get_random() -> NeoResult<NeoInteger> {
657        Self::call_integer("System.Runtime.GetRandom")
658    }
659
660    pub fn get_network() -> NeoResult<NeoInteger> {
661        Self::call_integer("System.Runtime.GetNetwork")
662    }
663
664    pub fn get_address_version() -> NeoResult<NeoInteger> {
665        Self::call_integer("System.Runtime.GetAddressVersion")
666    }
667
668    pub fn get_gas_left() -> NeoResult<NeoInteger> {
669        Self::call_integer("System.Runtime.GasLeft")
670    }
671
672    #[cfg(not(target_arch = "wasm32"))]
673    pub fn get_calling_script_hash() -> NeoResult<NeoByteString> {
674        Ok(NeoByteString::from_slice(&current_calling_script_hash()))
675    }
676
677    #[cfg(target_arch = "wasm32")]
678    pub fn get_calling_script_hash() -> NeoResult<NeoByteString> {
679        Ok(NeoByteString::new(vec![0u8; 20]))
680    }
681
682    #[cfg(not(target_arch = "wasm32"))]
683    pub fn get_calling_script_hash_i64() -> NeoResult<i64> {
684        Ok(hash160_prefix_i64(&current_calling_script_hash()))
685    }
686
687    #[cfg(target_arch = "wasm32")]
688    pub fn get_calling_script_hash_i64() -> NeoResult<i64> {
689        Ok(unsafe { neo_runtime_get_calling_script_hash_i64() })
690    }
691
692    #[cfg(not(target_arch = "wasm32"))]
693    pub fn get_entry_script_hash() -> NeoResult<NeoByteString> {
694        Ok(NeoByteString::from_slice(&current_entry_script_hash()))
695    }
696
697    #[cfg(target_arch = "wasm32")]
698    pub fn get_entry_script_hash() -> NeoResult<NeoByteString> {
699        Ok(NeoByteString::new(vec![0u8; 20]))
700    }
701
702    #[cfg(not(target_arch = "wasm32"))]
703    pub fn get_entry_script_hash_i64() -> NeoResult<i64> {
704        Ok(hash160_prefix_i64(&current_entry_script_hash()))
705    }
706
707    #[cfg(target_arch = "wasm32")]
708    pub fn get_entry_script_hash_i64() -> NeoResult<i64> {
709        Ok(unsafe { neo_runtime_get_entry_script_hash_i64() })
710    }
711
712    #[cfg(not(target_arch = "wasm32"))]
713    pub fn get_executing_script_hash() -> NeoResult<NeoByteString> {
714        Ok(NeoByteString::from_slice(&current_executing_script_hash()))
715    }
716
717    #[cfg(target_arch = "wasm32")]
718    pub fn get_executing_script_hash() -> NeoResult<NeoByteString> {
719        Ok(NeoByteString::new(vec![0u8; 20]))
720    }
721
722    #[cfg(not(target_arch = "wasm32"))]
723    pub fn get_executing_script_hash_i64() -> NeoResult<i64> {
724        Ok(hash160_prefix_i64(&current_executing_script_hash()))
725    }
726
727    #[cfg(target_arch = "wasm32")]
728    pub fn get_executing_script_hash_i64() -> NeoResult<i64> {
729        Ok(unsafe { neo_runtime_get_executing_script_hash_i64() })
730    }
731
732    /// Get notifications for the specified script hash, or all notifications if None.
733    pub fn get_notifications(script_hash: Option<&NeoByteString>) -> NeoResult<NeoArray<NeoValue>> {
734        let script_hash_value = script_hash
735            .map(|hash| NeoValue::from(hash.clone()))
736            .unwrap_or(NeoValue::Null);
737        let args = [script_hash_value];
738        Self::call_array("System.Runtime.GetNotifications", &args)
739    }
740
741    pub fn get_script_container() -> NeoResult<NeoArray<NeoValue>> {
742        Self::call_array("System.Runtime.GetScriptContainer", &[])
743    }
744
745    /// Burn GAS.
746    pub fn burn_gas(gas: &NeoInteger) -> NeoResult<()> {
747        let args = [NeoValue::from(gas.clone())];
748        Self::call_value("System.Runtime.BurnGas", &args)?;
749        Ok(())
750    }
751
752    /// Get active transaction signers.
753    pub fn current_signers() -> NeoResult<NeoArray<NeoValue>> {
754        Self::call_array("System.Runtime.CurrentSigners", &[])
755    }
756
757    /// Dynamically load and execute a script.
758    pub fn load_script(
759        script: &NeoByteString,
760        call_flags: &NeoInteger,
761        args: &NeoArray<NeoValue>,
762    ) -> NeoResult<()> {
763        let values = [
764            NeoValue::from(script.clone()),
765            NeoValue::from(call_flags.clone()),
766            NeoValue::from(args.clone()),
767        ];
768        Self::call_value("System.Runtime.LoadScript", &values)?;
769        Ok(())
770    }
771
772    /// Call any contract method.
773    pub fn contract_call(
774        script_hash: &NeoByteString,
775        method: &NeoString,
776        call_flags: &NeoInteger,
777        args: &NeoArray<NeoValue>,
778    ) -> NeoResult<NeoValue> {
779        let values = [
780            NeoValue::from(script_hash.clone()),
781            NeoValue::from(method.clone()),
782            NeoValue::from(call_flags.clone()),
783            NeoValue::from(args.clone()),
784        ];
785
786        #[cfg(not(target_arch = "wasm32"))]
787        {
788            let parsed_flags = Self::parse_call_flags(call_flags)?;
789            Self::begin_contract_invocation_with_flags(script_hash, parsed_flags)?;
790            let call_result = Self::call_value("System.Contract.Call", &values);
791            let unwind_result = Self::end_contract_invocation();
792            match (call_result, unwind_result) {
793                (Ok(value), Ok(())) => Ok(value),
794                (Err(err), Ok(())) => Err(err),
795                (Ok(_), Err(unwind_err)) => Err(unwind_err),
796                (Err(call_err), Err(unwind_err)) => Err(NeoError::new(&format!(
797                    "contract_call failed ({}) and invocation unwind failed ({})",
798                    call_err.message(),
799                    unwind_err.message()
800                ))),
801            }
802        }
803
804        #[cfg(target_arch = "wasm32")]
805        {
806            Self::call_value("System.Contract.Call", &values)
807        }
808    }
809
810    /// Call a native contract by id.
811    pub fn contract_call_native(native_id: &NeoInteger) -> NeoResult<NeoValue> {
812        let values = [NeoValue::from(native_id.clone())];
813        Self::call_value("System.Contract.CallNative", &values)
814    }
815
816    pub fn get_call_flags() -> NeoResult<NeoInteger> {
817        #[cfg(not(target_arch = "wasm32"))]
818        {
819            Ok(NeoInteger::new(current_call_flags()))
820        }
821
822        #[cfg(target_arch = "wasm32")]
823        {
824            Self::call_integer("System.Contract.GetCallFlags")
825        }
826    }
827
828    pub fn create_standard_account(pubkey: &NeoByteString) -> NeoResult<NeoByteString> {
829        let values = [NeoValue::from(pubkey.clone())];
830        Self::call_bytes_with_args("System.Contract.CreateStandardAccount", &values)
831    }
832
833    pub fn create_multisig_account(
834        threshold: &NeoInteger,
835        public_keys: &NeoArray<NeoValue>,
836    ) -> NeoResult<NeoByteString> {
837        let values = [
838            NeoValue::from(threshold.clone()),
839            NeoValue::from(public_keys.clone()),
840        ];
841        Self::call_bytes_with_args("System.Contract.CreateMultisigAccount", &values)
842    }
843
844    pub fn native_on_persist() -> NeoResult<()> {
845        Self::call_value("System.Contract.NativeOnPersist", &[])?;
846        Ok(())
847    }
848
849    pub fn native_post_persist() -> NeoResult<()> {
850        Self::call_value("System.Contract.NativePostPersist", &[])?;
851        Ok(())
852    }
853
854    pub fn check_sig(pubkey: &NeoByteString, signature: &NeoByteString) -> NeoResult<NeoBoolean> {
855        let values = [
856            NeoValue::from(pubkey.clone()),
857            NeoValue::from(signature.clone()),
858        ];
859        Self::call_boolean("System.Crypto.CheckSig", &values)
860    }
861
862    pub fn check_multisig(
863        pubkeys: &NeoArray<NeoValue>,
864        signatures: &NeoArray<NeoValue>,
865    ) -> NeoResult<NeoBoolean> {
866        let values = [
867            NeoValue::from(pubkeys.clone()),
868            NeoValue::from(signatures.clone()),
869        ];
870        Self::call_boolean("System.Crypto.CheckMultisig", &values)
871    }
872
873    pub fn verify_with_ecdsa(
874        message: &NeoByteString,
875        pubkey: &NeoByteString,
876        signature: &NeoByteString,
877        curve: &NeoInteger,
878    ) -> NeoResult<NeoBoolean> {
879        let values = [
880            NeoValue::from(message.clone()),
881            NeoValue::from(pubkey.clone()),
882            NeoValue::from(signature.clone()),
883            NeoValue::from(curve.clone()),
884        ];
885        Self::call_boolean("Neo.Crypto.VerifyWithECDsa", &values)
886    }
887
888    pub fn iterator_next(items: &NeoArray<NeoValue>) -> NeoResult<NeoBoolean> {
889        let values = [NeoValue::from(items.clone())];
890        Self::call_boolean("System.Iterator.Next", &values)
891    }
892
893    pub fn iterator_value(items: &NeoArray<NeoValue>) -> NeoResult<NeoValue> {
894        let values = [NeoValue::from(items.clone())];
895        Self::call_value("System.Iterator.Value", &values)
896    }
897
898    #[cfg(not(target_arch = "wasm32"))]
899    pub fn storage_get_context() -> NeoResult<NeoStorageContext> {
900        let flags = current_call_flags();
901        if !call_flags_allow_read(flags) {
902            return Err(NeoError::InvalidOperation);
903        }
904        let read_only = !call_flags_allow_write(flags);
905        STORAGE_STATE.create_context(current_executing_script_hash(), read_only)
906    }
907
908    /// On wasm32 we return a sentinel `NeoStorageContext`. The translator
909    /// emits a fresh `SYSCALL System.Storage.GetContext` inside each storage
910    /// helper, so the i32 id carried by this struct is irrelevant to NeoVM —
911    /// the only field that affects translated bytecode is the `read_only`
912    /// marker, which is enforced by the wasm32 wrappers below.
913    #[cfg(target_arch = "wasm32")]
914    pub fn storage_get_context() -> NeoResult<NeoStorageContext> {
915        Ok(NeoStorageContext::new(1))
916    }
917
918    #[cfg(not(target_arch = "wasm32"))]
919    pub fn storage_get_read_only_context() -> NeoResult<NeoStorageContext> {
920        if !call_flags_allow_read(current_call_flags()) {
921            return Err(NeoError::InvalidOperation);
922        }
923        STORAGE_STATE.create_context(current_executing_script_hash(), true)
924    }
925
926    #[cfg(target_arch = "wasm32")]
927    pub fn storage_get_read_only_context() -> NeoResult<NeoStorageContext> {
928        Ok(NeoStorageContext::read_only(1))
929    }
930
931    #[cfg(not(target_arch = "wasm32"))]
932    pub fn storage_as_read_only(context: &NeoStorageContext) -> NeoResult<NeoStorageContext> {
933        STORAGE_STATE.clone_as_read_only(context)
934    }
935
936    #[cfg(target_arch = "wasm32")]
937    pub fn storage_as_read_only(context: &NeoStorageContext) -> NeoResult<NeoStorageContext> {
938        Ok(context.as_read_only())
939    }
940
941    #[cfg(not(target_arch = "wasm32"))]
942    pub fn storage_get(
943        context: &NeoStorageContext,
944        key: &NeoByteString,
945    ) -> NeoResult<NeoByteString> {
946        if !call_flags_allow_read(current_call_flags()) {
947            return Err(NeoError::InvalidOperation);
948        }
949        let handle = STORAGE_STATE.get_handle(context)?;
950        let store = handle.store.read().map_err(|_| NeoError::InvalidState)?;
951        let value = store.get(key.as_slice()).cloned().unwrap_or_else(Vec::new);
952        Ok(NeoByteString::new(value))
953    }
954
955    #[cfg(not(target_arch = "wasm32"))]
956    pub fn storage_try_get(
957        context: &NeoStorageContext,
958        key: &NeoByteString,
959    ) -> NeoResult<Option<NeoByteString>> {
960        if !call_flags_allow_read(current_call_flags()) {
961            return Err(NeoError::InvalidOperation);
962        }
963        let handle = STORAGE_STATE.get_handle(context)?;
964        let store = handle.store.read().map_err(|_| NeoError::InvalidState)?;
965        Ok(store.get(key.as_slice()).cloned().map(NeoByteString::new))
966    }
967
968    #[cfg(not(target_arch = "wasm32"))]
969    pub fn storage_put(
970        context: &NeoStorageContext,
971        key: &NeoByteString,
972        value: &NeoByteString,
973    ) -> NeoResult<()> {
974        if !call_flags_allow_write(current_call_flags()) {
975            return Err(NeoError::InvalidOperation);
976        }
977        let handle = STORAGE_STATE.get_handle(context)?;
978        if handle.read_only {
979            return Err(NeoError::InvalidOperation);
980        }
981        let mut store = handle.store.write().map_err(|_| NeoError::InvalidState)?;
982        store.insert(key.as_slice().to_vec(), value.as_slice().to_vec());
983        Ok(())
984    }
985
986    /// Writes through to real Neo persistent storage. The translator lowers
987    /// `neo_storage_put_bytes` to a `CALL_L` that emits the
988    /// `System.Storage.GetContext + System.Storage.Put` SYSCALL pair. The
989    /// `read_only` check on the supplied marker still runs first so contracts
990    /// that hand a read-only context to `put` short-circuit before crossing
991    /// the wasm boundary.
992    #[cfg(target_arch = "wasm32")]
993    pub fn storage_put(
994        context: &NeoStorageContext,
995        key: &NeoByteString,
996        value: &NeoByteString,
997    ) -> NeoResult<()> {
998        if context.is_read_only() {
999            return Err(NeoError::InvalidOperation);
1000        }
1001
1002        let key_slice = key.as_slice();
1003        let value_slice = value.as_slice();
1004        unsafe {
1005            neo_storage_put_bytes(
1006                key_slice.as_ptr() as i32,
1007                key_slice.len() as i32,
1008                value_slice.as_ptr() as i32,
1009                value_slice.len() as i32,
1010            );
1011        }
1012        Ok(())
1013    }
1014
1015    /// Reads through to real Neo persistent storage via the translator-emitted
1016    /// `neo_storage_get_into` helper. The helper writes the stored bytes into
1017    /// the local `buffer` (sized up on demand) and reports the actual length;
1018    /// missing keys return an empty `NeoByteString`, matching the host-mode
1019    /// semantics already exercised by the devpack tests.
1020    #[cfg(target_arch = "wasm32")]
1021    pub fn storage_get(
1022        _context: &NeoStorageContext,
1023        key: &NeoByteString,
1024    ) -> NeoResult<NeoByteString> {
1025        const INITIAL_CAPACITY: usize = 64;
1026        const MAX_CAPACITY: usize = 64 * 1024;
1027
1028        let key_slice = key.as_slice();
1029        let mut buffer: Vec<u8> = vec![0u8; INITIAL_CAPACITY];
1030        loop {
1031            let actual = unsafe {
1032                neo_storage_get_into(
1033                    key_slice.as_ptr() as i32,
1034                    key_slice.len() as i32,
1035                    buffer.as_mut_ptr() as i32,
1036                    buffer.len() as i32,
1037                )
1038            };
1039            if actual == -1 {
1040                return Ok(NeoByteString::new(Vec::new()));
1041            }
1042            if actual >= 0 {
1043                let len = actual as usize;
1044                buffer.truncate(len);
1045                return Ok(NeoByteString::new(buffer));
1046            }
1047            // -needed_len: grow buffer and retry.
1048            let needed = (-actual) as usize;
1049            if needed > MAX_CAPACITY {
1050                return Err(NeoError::InvalidState);
1051            }
1052            buffer.resize(needed, 0);
1053        }
1054    }
1055
1056    #[cfg(not(target_arch = "wasm32"))]
1057    pub fn storage_delete(context: &NeoStorageContext, key: &NeoByteString) -> NeoResult<()> {
1058        if !call_flags_allow_write(current_call_flags()) {
1059            return Err(NeoError::InvalidOperation);
1060        }
1061        let handle = STORAGE_STATE.get_handle(context)?;
1062        if handle.read_only {
1063            return Err(NeoError::InvalidOperation);
1064        }
1065        let mut store = handle.store.write().map_err(|_| NeoError::InvalidState)?;
1066        store.remove(key.as_slice());
1067        Ok(())
1068    }
1069
1070    /// Deletes the key from real Neo persistent storage via
1071    /// `neo_storage_delete_bytes`, which the translator lowers to
1072    /// `System.Storage.GetContext + System.Storage.Delete`.
1073    #[cfg(target_arch = "wasm32")]
1074    pub fn storage_delete(context: &NeoStorageContext, key: &NeoByteString) -> NeoResult<()> {
1075        if context.is_read_only() {
1076            return Err(NeoError::InvalidOperation);
1077        }
1078
1079        let key_slice = key.as_slice();
1080        unsafe {
1081            neo_storage_delete_bytes(key_slice.as_ptr() as i32, key_slice.len() as i32);
1082        }
1083        Ok(())
1084    }
1085
1086    #[cfg(not(target_arch = "wasm32"))]
1087    pub fn storage_find(
1088        context: &NeoStorageContext,
1089        prefix: &NeoByteString,
1090    ) -> NeoResult<NeoIterator<NeoValue>> {
1091        if !call_flags_allow_read(current_call_flags()) {
1092            return Err(NeoError::InvalidOperation);
1093        }
1094        let handle = STORAGE_STATE.get_handle(context)?;
1095        let prefix_bytes = prefix.as_slice();
1096        let store = handle.store.read().map_err(|_| NeoError::InvalidState)?;
1097        let matches: Vec<NeoValue> = store
1098            .iter()
1099            .filter_map(|(key_bytes, value)| {
1100                if key_bytes.starts_with(prefix_bytes) {
1101                    let mut entry = NeoStruct::new();
1102                    entry.set_field("key", NeoValue::from(NeoByteString::from_slice(key_bytes)));
1103                    entry.set_field("value", NeoValue::from(NeoByteString::from_slice(value)));
1104                    Some(NeoValue::from(entry))
1105                } else {
1106                    None
1107                }
1108            })
1109            .collect();
1110        Ok(NeoIterator::new(matches))
1111    }
1112
1113    /// On wasm32 `storage_find` returns an empty iterator. Bridging a real
1114    /// `System.Storage.Find` iterator handle through wasm would require
1115    /// special-cased translator support for `System.Iterator.Next/Value`
1116    /// on top of the byte-marshalled `Get/Put/Delete` primitives that this
1117    /// module already lowers; contracts that need prefix iteration must use
1118    /// indexed enumeration backed by `storage_get` until that lands.
1119    #[cfg(target_arch = "wasm32")]
1120    pub fn storage_find(
1121        _context: &NeoStorageContext,
1122        _prefix: &NeoByteString,
1123    ) -> NeoResult<NeoIterator<NeoValue>> {
1124        Ok(NeoIterator::new(Vec::new()))
1125    }
1126}