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::*;
10// The registry (`SYSCALLS`) and its row type back the host-mode dispatch
11// only; wasm32 wrappers call link-time externs directly.
12#[cfg(not(target_arch = "wasm32"))]
13use crate::syscalls::SYSCALLS;
14#[cfg(not(target_arch = "wasm32"))]
15use crate::NeoVMSyscallInfo;
16
17// Some declarations here are the reserved host ABI for syscalls whose
18// wasm32 wrapper is still a stub (e.g. `runtime_storage_find`,
19// `runtime_iterator_*`, the storage-context accessors) or are superseded by
20// a differently-named extern (`runtime_load_script` /
21// `runtime_contract_call_native`). They are intentionally declared so the
22// ABI surface is documented in one place; `allow(dead_code)` keeps the
23// wasm32 contract build warning-free until each wrapper is wired up.
24#[cfg(target_arch = "wasm32")]
25#[allow(dead_code)]
26#[link(wasm_import_module = "neo")]
27extern "C" {
28    #[link_name = "runtime_check_witness_bytes"]
29    fn neo_runtime_check_witness_bytes(ptr: i32, len: i32) -> i32;
30
31    #[link_name = "runtime_check_witness_i64"]
32    fn neo_runtime_check_witness_i64(account: i64) -> i32;
33
34    #[link_name = "runtime_get_time"]
35    fn neo_runtime_get_time() -> i64;
36
37    #[link_name = "runtime_get_calling_script_hash_i64"]
38    fn neo_runtime_get_calling_script_hash_i64() -> i64;
39
40    #[link_name = "runtime_get_entry_script_hash_i64"]
41    fn neo_runtime_get_entry_script_hash_i64() -> i64;
42
43    #[link_name = "runtime_get_executing_script_hash_i64"]
44    fn neo_runtime_get_executing_script_hash_i64() -> i64;
45
46    /// B1: ByteString-form (20-byte) script hashes. Returns the number
47    /// of bytes written into `out_ptr` (20 on success, negative on error).
48    /// Previously the wrappers returned `vec![0u8; 20]` on wasm32, silently
49    /// producing zero hashes on mainnet. B1 fix.
50    #[link_name = "runtime_get_calling_script_hash"]
51    fn neo_runtime_get_calling_script_hash(out_ptr: i32, out_cap: i32) -> i32;
52    #[link_name = "runtime_get_entry_script_hash"]
53    fn neo_runtime_get_entry_script_hash(out_ptr: i32, out_cap: i32) -> i32;
54    #[link_name = "runtime_get_executing_script_hash"]
55    fn neo_runtime_get_executing_script_hash(out_ptr: i32, out_cap: i32) -> i32;
56
57    #[link_name = "runtime_log"]
58    fn neo_runtime_log(ptr: i32, len: i32);
59
60    /// B2: state-carrying notify. The args array is serialised as a
61    /// NeoVM `Array` StackItem (1-byte tag, varint count, items…) and
62    /// handed to the VM. Previously `runtime_notify` only carried the
63    /// event name and dropped the state, so NEP-17/NEP-11 Transfer
64    /// events emitted `Transfer(<empty>)` on mainnet. B2 fix.
65    #[link_name = "runtime_notify"]
66    fn neo_runtime_notify(event_ptr: i32, event_len: i32);
67    #[link_name = "runtime_notify_with_state"]
68    fn neo_runtime_notify_with_state(
69        event_ptr: i32,
70        event_len: i32,
71        state_ptr: i32,
72        state_len: i32,
73    );
74
75    /// D3: lowered to `SYSCALL System.Crypto.CheckSig(pubkey, signature)`.
76    /// Verifies the ECDSA signature of the current script hash using `pubkey`.
77    /// The translator's `neo::*` alias maps `check_sig` to that syscall.
78    #[link_name = "check_sig"]
79    fn neo_runtime_check_sig(pubkey_ptr: i32, pubkey_len: i32, sig_ptr: i32, sig_len: i32) -> i32;
80
81    /// D3: lowered to `SYSCALL System.Crypto.CheckMultisig(pubkeys, signatures)`.
82    #[link_name = "check_multisig"]
83    fn neo_runtime_check_multisig(
84        pubkeys_ptr: i32,
85        pubkeys_len: i32,
86        sigs_ptr: i32,
87        sigs_len: i32,
88    ) -> i32;
89
90    /// D3: lowered to `System.Contract.Call(cryptoLib, "verifyWithECDsa", ...)`
91    /// via the C1 native-contract routing.
92    #[link_name = "verify_with_ecdsa"]
93    fn neo_runtime_verify_with_ecdsa(
94        msg_ptr: i32,
95        msg_len: i32,
96        pubkey_ptr: i32,
97        pubkey_len: i32,
98        sig_ptr: i32,
99        sig_len: i32,
100        curve: i32,
101    ) -> i32;
102
103    /// Lowered to `CALL_L` -> helper that emits
104    ///   `SYSCALL System.Storage.GetContext;
105    ///    SUBSTR <key>; SUBSTR <value>;
106    ///    SYSCALL System.Storage.Put`.
107    #[link_name = "neo_storage_put_bytes"]
108    fn neo_storage_put_bytes(key_ptr: i32, key_len: i32, value_ptr: i32, value_len: i32);
109
110    /// Lowered to `CALL_L` -> helper that emits
111    ///   `SYSCALL System.Storage.GetContext;
112    ///    SUBSTR <key>;
113    ///    SYSCALL System.Storage.Delete`.
114    #[link_name = "neo_storage_delete_bytes"]
115    fn neo_storage_delete_bytes(key_ptr: i32, key_len: i32);
116
117    /// Lowered to `CALL_L` -> helper that emits the `Get` SYSCALL and then
118    /// copies the returned `ByteString` back into wasm memory at `out_ptr`.
119    /// Returns:
120    ///   - the stored value's length on success (`>= 0`),
121    ///   - `-1` if the key is not present in storage,
122    ///   - `-needed_len` if the caller-supplied buffer was too small.
123    #[link_name = "neo_storage_get_into"]
124    fn neo_storage_get_into(key_ptr: i32, key_len: i32, out_ptr: i32, out_cap: i32) -> i32;
125
126    /// TIER-2: runtime syscalls that previously returned zero/empty on
127    /// wasm32 (silent wrong values). Each is paired with a real extern
128    /// here; the wasm32 path on `NeoVMSyscall` is updated to call it.
129    #[link_name = "runtime_get_random"]
130    fn neo_runtime_get_random() -> i64;
131    #[link_name = "runtime_get_invocation_counter"]
132    fn neo_runtime_get_invocation_counter() -> i32;
133    #[link_name = "runtime_get_gas_left"]
134    fn neo_runtime_get_gas_left() -> i64;
135    #[link_name = "runtime_get_notifications"]
136    fn neo_runtime_get_notifications(
137        script_hash_ptr: i32,
138        script_hash_len: i32,
139        out_ptr: i32,
140        out_cap: i32,
141    ) -> i32;
142    #[link_name = "runtime_current_signers"]
143    fn neo_runtime_current_signers(out_ptr: i32, out_cap: i32) -> i32;
144    #[link_name = "runtime_burn_gas"]
145    fn neo_runtime_burn_gas(gas: i64);
146    #[link_name = "runtime_get_script_container"]
147    fn neo_runtime_get_script_container(out_ptr: i32, out_cap: i32) -> i32;
148    #[link_name = "runtime_load_script"]
149    fn neo_runtime_load_script(
150        script_ptr: i32,
151        script_len: i32,
152        call_flags: i32,
153        args_ptr: i32,
154        args_len: i32,
155    );
156    #[link_name = "runtime_create_standard_account"]
157    fn neo_runtime_create_standard_account(
158        pubkey_ptr: i32,
159        pubkey_len: i32,
160        out_ptr: i32,
161        out_cap: i32,
162    ) -> i32;
163    #[link_name = "runtime_create_multisig_account"]
164    fn neo_runtime_create_multisig_account(
165        threshold: i32,
166        pubkeys_ptr: i32,
167        pubkeys_len: i32,
168        out_ptr: i32,
169        out_cap: i32,
170    ) -> i32;
171    #[link_name = "runtime_contract_call_native"]
172    fn neo_runtime_contract_call_native(
173        native_id: i32,
174        method_ptr: i32,
175        method_len: i32,
176        args_ptr: i32,
177        args_len: i32,
178        out_ptr: i32,
179        out_cap: i32,
180    ) -> i32;
181    #[link_name = "runtime_get_call_flags"]
182    fn neo_runtime_get_call_flags() -> i32;
183    #[link_name = "runtime_get_storage_context"]
184    fn neo_runtime_get_storage_context() -> i32;
185    #[link_name = "runtime_get_read_only_context"]
186    fn neo_runtime_get_read_only_context() -> i32;
187    #[link_name = "runtime_storage_as_read_only"]
188    fn neo_runtime_storage_as_read_only(context_id: i32) -> i32;
189    #[link_name = "runtime_storage_find"]
190    fn neo_runtime_storage_find(
191        context_id: i32,
192        prefix_ptr: i32,
193        prefix_len: i32,
194        options: i32,
195        out_ptr: i32,
196        out_cap: i32,
197    ) -> i32;
198    #[link_name = "runtime_iterator_next"]
199    fn neo_runtime_iterator_next(iterator_id: i32) -> i32;
200    #[link_name = "runtime_iterator_value"]
201    fn neo_runtime_iterator_value(iterator_id: i32, out_ptr: i32, out_cap: i32) -> i32;
202
203    /// Protocol-config syscalls (constant per chain). The host
204    /// returns the value at link time.
205    #[link_name = "protocol_get_network"]
206    fn neo_protocol_get_network() -> i32;
207    #[link_name = "protocol_get_address_version"]
208    fn neo_protocol_get_address_version() -> i32;
209    #[link_name = "protocol_get_trigger"]
210    fn neo_protocol_get_trigger() -> i32;
211
212    /// L6: cross-contract-call extern. The wasm32 wrapper calls
213    /// this so the wasm-neovm translator sees the import and emits
214    /// `SYSCALL System.Contract.Call`. The host's NeoVM dispatches
215    /// the call at runtime.
216    #[link_name = "neo_contract_call"]
217    fn neo_contract_call(
218        hash_ptr: i32,
219        hash_len: i32,
220        method_ptr: i32,
221        method_len: i32,
222        args_ptr: i32,
223        args_len: i32,
224        call_flags: i32,
225        out_ptr: i32,
226        out_cap: i32,
227    ) -> i32;
228
229    /// L6: load-script extern. The wasm32 wrapper calls this so
230    /// the translator emits `SYSCALL System.Runtime.LoadScript`.
231    #[link_name = "neo_load_script"]
232    fn neo_load_script(
233        script_ptr: i32,
234        script_len: i32,
235        call_flags: i32,
236        args_ptr: i32,
237        args_len: i32,
238    ) -> i32;
239
240    /// L6: call-native extern. The wasm32 wrapper calls this so
241    /// the translator emits `SYSCALL System.Contract.CallNative`.
242    #[link_name = "neo_call_native"]
243    fn neo_call_native(
244        native_id: i32,
245        method_ptr: i32,
246        method_len: i32,
247        args_ptr: i32,
248        args_len: i32,
249        out_ptr: i32,
250        out_cap: i32,
251    ) -> i32;
252}
253
254#[cfg(not(target_arch = "wasm32"))]
255const CALL_FLAGS_VALID_MASK: i32 = 0x0F;
256#[cfg(not(target_arch = "wasm32"))]
257const CALL_FLAGS_READ_STATES: i32 = 0x01;
258#[cfg(not(target_arch = "wasm32"))]
259const CALL_FLAGS_WRITE_STATES: i32 = 0x02;
260
261// Host-mode (non-wasm32) syscall dispatch helpers. On wasm32 the wrappers
262// call link-time externs directly, so these registry lookups are unused.
263#[cfg(not(target_arch = "wasm32"))]
264fn find_syscall(name: &str) -> Option<&'static NeoVMSyscallInfo> {
265    SYSCALLS.iter().find(|info| info.name == name)
266}
267
268#[cfg(not(target_arch = "wasm32"))]
269fn syscall_hash(name: &str) -> NeoResult<u32> {
270    find_syscall(name)
271        .map(|info| info.hash)
272        .ok_or_else(|| NeoError::new(&format!("unknown syscall: {name}")))
273}
274
275fn default_value_for(return_type: &str) -> NeoValue {
276    match return_type {
277        "Void" => NeoValue::Null,
278        // Fail-closed by default for unknown boolean-returning syscalls.
279        "Boolean" => NeoBoolean::FALSE.into(),
280        "Integer" => NeoInteger::new(0).into(),
281        "Hash160" => NeoByteString::new(vec![0u8; 20]).into(),
282        "ByteString" => NeoByteString::new(vec![0u8; 1]).into(),
283        "String" => NeoString::from_str("Neo N3").into(),
284        "Array" => NeoArray::<NeoValue>::new().into(),
285        "Iterator" => NeoArray::<NeoValue>::new().into(),
286        "StackItem" => NeoArray::<NeoValue>::new().into(),
287        "StorageContext" => NeoValue::Null,
288        _ => NeoValue::Null,
289    }
290}
291
292fn value_matches_param_type(value: &NeoValue, param_type: &str) -> bool {
293    match param_type {
294        "Boolean" => value.as_boolean().is_some(),
295        "Integer" => value.as_integer().is_some(),
296        "Hash160" => {
297            value.is_null()
298                || value
299                    .as_byte_string()
300                    .map(|bytes| bytes.len() == 20)
301                    .unwrap_or(false)
302        }
303        "ByteString" => value.as_byte_string().is_some(),
304        "String" => value.as_string().is_some(),
305        "Array" => value.as_array().is_some(),
306        "Iterator" => value.as_array().is_some(),
307        "StorageContext" => value.is_null() || value.as_integer().is_some(),
308        "StackItem" | "Any" | "ExecutionContext" => true,
309        _ => true,
310    }
311}
312
313#[cfg(not(target_arch = "wasm32"))]
314fn call_flags_allow_write(flags: i32) -> bool {
315    (flags & CALL_FLAGS_WRITE_STATES) != 0
316}
317
318#[cfg(not(target_arch = "wasm32"))]
319fn call_flags_allow_read(flags: i32) -> bool {
320    (flags & CALL_FLAGS_READ_STATES) != 0
321}
322
323#[cfg(not(target_arch = "wasm32"))]
324fn hash160_prefix_i64(hash: &[u8; 20]) -> i64 {
325    let mut buf = [0u8; 8];
326    buf.copy_from_slice(&hash[..8]);
327    i64::from_le_bytes(buf)
328}
329
330/// Neo N3 System Call Function
331pub fn neovm_syscall(hash: u32, args: &[NeoValue]) -> NeoResult<NeoValue> {
332    let registry = crate::NeoVMSyscallRegistry::get_instance();
333    let info = registry
334        .get_syscall_by_hash(hash)
335        .ok_or_else(|| NeoError::new(&format!("unknown syscall hash: 0x{hash:08x}")))?;
336
337    if args.len() != info.parameters.len() {
338        return Err(NeoError::new(&format!(
339            "invalid syscall argument count for {}: expected {}, got {}",
340            info.name,
341            info.parameters.len(),
342            args.len()
343        )));
344    }
345
346    for (index, (arg, expected_type)) in args.iter().zip(info.parameters.iter()).enumerate() {
347        if !value_matches_param_type(arg, expected_type) {
348            return Err(NeoError::new(&format!(
349                "invalid syscall argument type for {} param #{}: expected {}",
350                info.name, index, expected_type
351            )));
352        }
353    }
354
355    #[cfg(not(target_arch = "wasm32"))]
356    {
357        if info.name == "System.Runtime.CheckWitness" {
358            let has_witness = args
359                .first()
360                .and_then(NeoValue::as_byte_string)
361                .map(|account| has_active_witness(account.as_slice()))
362                .unwrap_or(false);
363            return Ok(NeoBoolean::new(has_witness).into());
364        }
365
366        if info.name == "System.Crypto.CheckSig" {
367            let results = active_crypto_verification_results();
368            return Ok(NeoBoolean::new(results.check_sig).into());
369        }
370
371        if info.name == "System.Crypto.CheckMultisig" {
372            let results = active_crypto_verification_results();
373            return Ok(NeoBoolean::new(results.check_multisig).into());
374        }
375
376        if info.name == "Neo.Crypto.VerifyWithECDsa" {
377            let results = active_crypto_verification_results();
378            return Ok(NeoBoolean::new(results.verify_with_ecdsa).into());
379        }
380
381        if info.name == "System.Runtime.GetCallingScriptHash" {
382            return Ok(NeoByteString::from_slice(&current_calling_script_hash()).into());
383        }
384
385        if info.name == "System.Runtime.GetEntryScriptHash" {
386            return Ok(NeoByteString::from_slice(&current_entry_script_hash()).into());
387        }
388
389        if info.name == "System.Runtime.GetExecutingScriptHash" {
390            return Ok(NeoByteString::from_slice(&current_executing_script_hash()).into());
391        }
392
393        if info.name == "System.Contract.GetCallFlags" {
394            return Ok(NeoInteger::new(current_call_flags()).into());
395        }
396
397        // B5: get_random
398        if info.name == "System.Runtime.GetRandom" {
399            return Ok(NeoInteger::new(
400                *crate::storage::ACTIVE_RANDOM
401                    .read()
402                    .expect("ACTIVE_RANDOM poisoned"),
403            )
404            .into());
405        }
406
407        // B6: get_time
408        if info.name == "System.Runtime.GetTime" {
409            return Ok(NeoInteger::new(
410                *crate::storage::ACTIVE_TIME
411                    .read()
412                    .expect("ACTIVE_TIME poisoned"),
413            )
414            .into());
415        }
416
417        // B6: get_invocation_counter
418        if info.name == "System.Runtime.GetInvocationCounter" {
419            return Ok(NeoInteger::new(
420                *crate::storage::ACTIVE_INVOCATION_COUNTER
421                    .read()
422                    .expect("ACTIVE_INVOCATION_COUNTER poisoned"),
423            )
424            .into());
425        }
426
427        // B7: gas_left
428        if info.name == "System.Runtime.GasLeft" {
429            return Ok(NeoInteger::new(
430                *crate::storage::ACTIVE_GAS_LEFT
431                    .read()
432                    .expect("ACTIVE_GAS_LEFT poisoned"),
433            )
434            .into());
435        }
436
437        // B8: current_signers. The C# struct has Account + Scopes;
438        // we serialise each signer as a 2-element array [account, scopes].
439        if info.name == "System.Runtime.CurrentSigners" {
440            let witnesses = crate::storage::ACTIVE_WITNESSES
441                .read()
442                .expect("ACTIVE_WITNESSES poisoned");
443            let arr: NeoArray<NeoValue> = witnesses
444                .iter()
445                .map(|w| {
446                    let entry: NeoArray<NeoValue> = vec![
447                        NeoValue::from(NeoByteString::from_slice(w)),
448                        NeoValue::from(NeoInteger::new(0x01)), // Global scope
449                    ]
450                    .into_iter()
451                    .collect();
452                    NeoValue::from(entry)
453                })
454                .collect();
455            return Ok(NeoValue::from(arr));
456        }
457
458        // B9: get_notifications(hash?). Hash arg: NeoValue::Null
459        // means "all notifications". Returns the recorded
460        // notifications as a NeoArray.
461        if info.name == "System.Runtime.GetNotifications" {
462            use crate::host_notifications::take;
463            let recorded = take();
464            let arr: NeoArray<NeoValue> = recorded
465                .into_iter()
466                .map(|n| {
467                    let entry: NeoArray<NeoValue> = vec![
468                        NeoValue::from(NeoString::from_str(&n.event)),
469                        NeoValue::from(n.state.into_iter().collect::<NeoArray<NeoValue>>()),
470                    ]
471                    .into_iter()
472                    .collect();
473                    NeoValue::from(entry)
474                })
475                .collect();
476            return Ok(NeoValue::from(arr));
477        }
478    }
479
480    Ok(default_value_for(info.return_type))
481}
482
483/// Neo N3 System Call Wrapper
484pub struct NeoVMSyscall;
485
486impl NeoVMSyscall {
487    #[cfg(not(target_arch = "wasm32"))]
488    fn parse_hash160(hash: &NeoByteString) -> NeoResult<[u8; 20]> {
489        if hash.len() != 20 {
490            return Err(NeoError::InvalidArgument);
491        }
492        let mut value = [0u8; 20];
493        value.copy_from_slice(hash.as_slice());
494        Ok(value)
495    }
496
497    /// B1: Read a 20-byte script hash from one of the
498    /// `runtime_get_*_script_hash` externs. The extern returns the
499    /// number of bytes written (20 on success, negative on error).
500    /// Used for the ByteString form of
501    /// `get_calling_script_hash` / `get_entry_script_hash` /
502    /// `get_executing_script_hash` on wasm32. Previously these
503    /// returned `vec![0u8; 20]` (silent zero hash on mainnet).
504    #[cfg(target_arch = "wasm32")]
505    fn read_script_hash_extern(
506        read: unsafe extern "C" fn(out_ptr: i32, out_cap: i32) -> i32,
507    ) -> NeoResult<NeoByteString> {
508        let mut buf = [0u8; 20];
509        let written = unsafe { (read)(buf.as_mut_ptr() as i32, buf.len() as i32) };
510        if written < 0 {
511            return Err(NeoError::InvalidState);
512        }
513        // Truncate to the bytes actually written (defensive: a future
514        // VM build could change the script-hash length).
515        let len = (written as usize).min(buf.len());
516        Ok(NeoByteString::from_slice(&buf[..len]))
517    }
518
519    #[cfg(not(target_arch = "wasm32"))]
520    fn parse_call_flags(flags: &NeoInteger) -> NeoResult<i32> {
521        let parsed = flags.as_i32_saturating();
522        if parsed < 0 || (parsed & !CALL_FLAGS_VALID_MASK) != 0 {
523            return Err(NeoError::InvalidArgument);
524        }
525        Ok(parsed)
526    }
527
528    #[cfg(not(target_arch = "wasm32"))]
529    fn begin_contract_invocation_with_flags(
530        next_executing: &NeoByteString,
531        call_flags: i32,
532    ) -> NeoResult<()> {
533        if call_flags < 0 || (call_flags & !CALL_FLAGS_VALID_MASK) != 0 {
534            return Err(NeoError::InvalidArgument);
535        }
536        push_current_executing_script_hash(Self::parse_hash160(next_executing)?, call_flags)
537    }
538
539    /// Set the active contract hash used by host-mode storage contexts and script-hash syscalls.
540    #[cfg(not(target_arch = "wasm32"))]
541    pub fn set_active_contract_hash(hash: &NeoByteString) -> NeoResult<()> {
542        set_current_contract_hash(Self::parse_hash160(hash)?);
543        Ok(())
544    }
545
546    /// Configure host-mode calling/entry/executing script hashes.
547    #[cfg(not(target_arch = "wasm32"))]
548    pub fn set_active_script_hashes(
549        calling: &NeoByteString,
550        entry: &NeoByteString,
551        executing: &NeoByteString,
552    ) -> NeoResult<()> {
553        set_current_script_hashes(
554            Self::parse_hash160(calling)?,
555            Self::parse_hash160(entry)?,
556            Self::parse_hash160(executing)?,
557        );
558        Ok(())
559    }
560
561    /// Configure host-mode calling script hash.
562    /// Clears nested invocation frames and applies this value as a new base state.
563    #[cfg(not(target_arch = "wasm32"))]
564    pub fn set_active_calling_script_hash(hash: &NeoByteString) -> NeoResult<()> {
565        set_current_calling_script_hash(Self::parse_hash160(hash)?);
566        Ok(())
567    }
568
569    /// Configure host-mode entry script hash.
570    /// Clears nested invocation frames and applies this value as a new base state.
571    #[cfg(not(target_arch = "wasm32"))]
572    pub fn set_active_entry_script_hash(hash: &NeoByteString) -> NeoResult<()> {
573        set_current_entry_script_hash(Self::parse_hash160(hash)?);
574        Ok(())
575    }
576
577    /// Configure host-mode executing script hash.
578    /// Clears nested invocation frames and applies this value as a new base state.
579    #[cfg(not(target_arch = "wasm32"))]
580    pub fn set_active_executing_script_hash(hash: &NeoByteString) -> NeoResult<()> {
581        set_current_executing_script_hash(Self::parse_hash160(hash)?);
582        Ok(())
583    }
584
585    /// Configure host-mode active call flags (Neo N3 CallFlags mask: 0x00..=0x0F).
586    /// Clears nested invocation frames and applies this value as a new base state.
587    #[cfg(not(target_arch = "wasm32"))]
588    pub fn set_active_call_flags(call_flags: &NeoInteger) -> NeoResult<()> {
589        set_current_call_flags(Self::parse_call_flags(call_flags)?);
590        Ok(())
591    }
592
593    /// Enter a nested contract invocation frame in host mode.
594    ///
595    /// The new frame preserves `entry`, shifts `calling <- previous executing`,
596    /// and sets `executing` to `next_executing`.
597    #[cfg(not(target_arch = "wasm32"))]
598    pub fn begin_contract_invocation(next_executing: &NeoByteString) -> NeoResult<()> {
599        Self::begin_contract_invocation_with_flags(next_executing, current_call_flags())
600    }
601
602    /// Exit the most recent nested contract invocation frame in host mode.
603    #[cfg(not(target_arch = "wasm32"))]
604    pub fn end_contract_invocation() -> NeoResult<()> {
605        pop_current_script_hash_frame()
606    }
607
608    /// Run an operation in a nested host invocation frame, always unwinding the frame.
609    #[cfg(not(target_arch = "wasm32"))]
610    pub fn with_contract_invocation<T, F>(
611        next_executing: &NeoByteString,
612        operation: F,
613    ) -> NeoResult<T>
614    where
615        F: FnOnce() -> NeoResult<T>,
616    {
617        Self::begin_contract_invocation(next_executing)?;
618        let operation_result = operation();
619        let unwind_result = Self::end_contract_invocation();
620
621        match (operation_result, unwind_result) {
622            (Ok(value), Ok(())) => Ok(value),
623            (Err(err), Ok(())) => Err(err),
624            (Ok(_), Err(unwind_err)) => Err(unwind_err),
625            (Err(operation_err), Err(unwind_err)) => Err(NeoError::new(&format!(
626                "invocation operation failed ({}) and frame unwind failed ({})",
627                operation_err.message(),
628                unwind_err.message()
629            ))),
630        }
631    }
632
633    /// Set the active contract hash used by host-mode storage contexts and script-hash syscalls.
634    #[cfg(target_arch = "wasm32")]
635    pub fn set_active_contract_hash(_hash: &NeoByteString) -> NeoResult<()> {
636        Ok(())
637    }
638
639    /// Configure host-mode calling/entry/executing script hashes.
640    #[cfg(target_arch = "wasm32")]
641    pub fn set_active_script_hashes(
642        _calling: &NeoByteString,
643        _entry: &NeoByteString,
644        _executing: &NeoByteString,
645    ) -> NeoResult<()> {
646        Ok(())
647    }
648
649    /// Configure host-mode calling script hash.
650    #[cfg(target_arch = "wasm32")]
651    pub fn set_active_calling_script_hash(_hash: &NeoByteString) -> NeoResult<()> {
652        Ok(())
653    }
654
655    /// Configure host-mode entry script hash.
656    #[cfg(target_arch = "wasm32")]
657    pub fn set_active_entry_script_hash(_hash: &NeoByteString) -> NeoResult<()> {
658        Ok(())
659    }
660
661    /// Configure host-mode executing script hash.
662    #[cfg(target_arch = "wasm32")]
663    pub fn set_active_executing_script_hash(_hash: &NeoByteString) -> NeoResult<()> {
664        Ok(())
665    }
666
667    /// Configure host-mode active call flags.
668    #[cfg(target_arch = "wasm32")]
669    pub fn set_active_call_flags(_call_flags: &NeoInteger) -> NeoResult<()> {
670        Ok(())
671    }
672
673    /// Enter a nested contract invocation frame in host mode.
674    #[cfg(target_arch = "wasm32")]
675    pub fn begin_contract_invocation(_next_executing: &NeoByteString) -> NeoResult<()> {
676        Ok(())
677    }
678
679    /// Exit the most recent nested contract invocation frame in host mode.
680    #[cfg(target_arch = "wasm32")]
681    pub fn end_contract_invocation() -> NeoResult<()> {
682        Ok(())
683    }
684
685    /// Run an operation in a nested host invocation frame, always unwinding the frame.
686    #[cfg(target_arch = "wasm32")]
687    pub fn with_contract_invocation<T, F>(
688        _next_executing: &NeoByteString,
689        operation: F,
690    ) -> NeoResult<T>
691    where
692        F: FnOnce() -> NeoResult<T>,
693    {
694        operation()
695    }
696
697    /// Clear host-mode syscall/storage simulation state.
698    #[cfg(not(target_arch = "wasm32"))]
699    pub fn reset_host_state() -> NeoResult<()> {
700        STORAGE_STATE.reset()?;
701        reset_current_contract_hash();
702        clear_active_witnesses();
703        reset_crypto_verification_results();
704        // B5-B9: clear the runtime syscall host state.
705        *crate::storage::ACTIVE_RANDOM
706            .write()
707            .expect("ACTIVE_RANDOM poisoned") = 0;
708        *crate::storage::ACTIVE_TIME
709            .write()
710            .expect("ACTIVE_TIME poisoned") = 0;
711        *crate::storage::ACTIVE_GAS_LEFT
712            .write()
713            .expect("ACTIVE_GAS_LEFT poisoned") = 0;
714        *crate::storage::ACTIVE_INVOCATION_COUNTER
715            .write()
716            .expect("ACTIVE_INVOCATION_COUNTER poisoned") = 0;
717        // Also drain any recorded notifications so the B9
718        // dispatch doesn't see state leaked from prior tests.
719        crate::host_notifications::reset();
720        Ok(())
721    }
722
723    /// Clear host-mode syscall/storage simulation state.
724    ///
725    /// On wasm32 this is a no-op: storage state lives in the Neo node's real
726    /// persistent store and is reset at the chain level (e.g. by tearing down
727    /// the Neo Express chain), not by the contract itself.
728    #[cfg(target_arch = "wasm32")]
729    pub fn reset_host_state() -> NeoResult<()> {
730        Ok(())
731    }
732
733    /// Seed host-mode storage with the given key/value pairs (D6: bridges
734    /// `neo-test::TestEnvironment::set_storage` to the global syscall mock so
735    /// contract code reading via `NeoStorage`/`RawStorage` sees the same
736    /// store). Pairs are written under the *currently executing* contract
737    /// hash (set via `set_active_contract_hash` / `set_current_contract_hash`;
738    /// default zero-sentinel). On wasm32 this is a no-op.
739    #[cfg(not(target_arch = "wasm32"))]
740    pub fn seed_storage(entries: &[(&[u8], &[u8])]) -> NeoResult<()> {
741        for (k, v) in entries {
742            STORAGE_STATE.put(k.to_vec(), v.to_vec());
743        }
744        Ok(())
745    }
746    #[cfg(target_arch = "wasm32")]
747    pub fn seed_storage(_entries: &[(&[u8], &[u8])]) -> NeoResult<()> {
748        Ok(())
749    }
750
751    // Host-mode (non-wasm32) typed syscall helpers: route through the
752    // registry-based `neovm_syscall` dispatch. On wasm32 the public
753    // wrappers call link-time externs directly, so these are unused there.
754    #[cfg(not(target_arch = "wasm32"))]
755    fn call_value(name: &str, args: &[NeoValue]) -> NeoResult<NeoValue> {
756        neovm_syscall(syscall_hash(name)?, args)
757    }
758
759    #[cfg(not(target_arch = "wasm32"))]
760    fn call_integer(name: &str) -> NeoResult<NeoInteger> {
761        let value = Self::call_value(name, &[])?;
762        value.as_integer().cloned().ok_or(NeoError::InvalidType)
763    }
764
765    #[cfg(not(target_arch = "wasm32"))]
766    fn call_boolean(name: &str, args: &[NeoValue]) -> NeoResult<NeoBoolean> {
767        let value = Self::call_value(name, args)?;
768        value.as_boolean().ok_or(NeoError::InvalidType)
769    }
770
771    #[cfg(not(target_arch = "wasm32"))]
772    fn call_bytes_with_args(name: &str, args: &[NeoValue]) -> NeoResult<NeoByteString> {
773        let value = Self::call_value(name, args)?;
774        value.as_byte_string().cloned().ok_or(NeoError::InvalidType)
775    }
776
777    #[cfg(not(target_arch = "wasm32"))]
778    fn call_string(name: &str) -> NeoResult<NeoString> {
779        let value = Self::call_value(name, &[])?;
780        value.as_string().cloned().ok_or(NeoError::InvalidType)
781    }
782
783    #[cfg(not(target_arch = "wasm32"))]
784    fn call_array(name: &str, args: &[NeoValue]) -> NeoResult<NeoArray<NeoValue>> {
785        let value = Self::call_value(name, args)?;
786        value.as_array().cloned().ok_or(NeoError::InvalidType)
787    }
788
789    /// Replace the active witness set used by host-mode `check_witness`.
790    #[cfg(not(target_arch = "wasm32"))]
791    pub fn set_active_witnesses(witnesses: &[NeoByteString]) -> NeoResult<()> {
792        crate::storage::set_active_witnesses(
793            witnesses.iter().map(|witness| witness.as_slice().to_vec()),
794        );
795        Ok(())
796    }
797
798    /// Replace the active witness set used by host-mode `check_witness`.
799    #[cfg(target_arch = "wasm32")]
800    pub fn set_active_witnesses(_witnesses: &[NeoByteString]) -> NeoResult<()> {
801        Ok(())
802    }
803
804    /// B5: set the value returned by host-mode `get_random`.
805    /// On the wasm32 path this is a no-op (the extern returns
806    /// the chain's real random value).
807    #[cfg(not(target_arch = "wasm32"))]
808    pub fn set_active_random(value: i64) -> NeoResult<()> {
809        *crate::storage::ACTIVE_RANDOM
810            .write()
811            .expect("ACTIVE_RANDOM poisoned") = value;
812        Ok(())
813    }
814    #[cfg(target_arch = "wasm32")]
815    pub fn set_active_random(_value: i64) -> NeoResult<()> {
816        Ok(())
817    }
818
819    /// B6: set the value returned by host-mode `get_time`.
820    /// On the wasm32 path this is a no-op.
821    #[cfg(not(target_arch = "wasm32"))]
822    pub fn set_active_time(value: i64) -> NeoResult<()> {
823        *crate::storage::ACTIVE_TIME
824            .write()
825            .expect("ACTIVE_TIME poisoned") = value;
826        Ok(())
827    }
828    #[cfg(target_arch = "wasm32")]
829    pub fn set_active_time(_value: i64) -> NeoResult<()> {
830        Ok(())
831    }
832
833    /// B6: set the value returned by host-mode
834    /// `get_invocation_counter`. On the wasm32 path this is a
835    /// no-op.
836    #[cfg(not(target_arch = "wasm32"))]
837    pub fn set_active_invocation_counter(value: i32) -> NeoResult<()> {
838        *crate::storage::ACTIVE_INVOCATION_COUNTER
839            .write()
840            .expect("ACTIVE_INVOCATION_COUNTER poisoned") = value;
841        Ok(())
842    }
843    #[cfg(target_arch = "wasm32")]
844    pub fn set_active_invocation_counter(_value: i32) -> NeoResult<()> {
845        Ok(())
846    }
847
848    /// B7: set the value returned by host-mode `get_gas_left`.
849    /// On the wasm32 path this is a no-op.
850    #[cfg(not(target_arch = "wasm32"))]
851    pub fn set_active_gas_left(value: i64) -> NeoResult<()> {
852        *crate::storage::ACTIVE_GAS_LEFT
853            .write()
854            .expect("ACTIVE_GAS_LEFT poisoned") = value;
855        Ok(())
856    }
857    #[cfg(target_arch = "wasm32")]
858    pub fn set_active_gas_left(_value: i64) -> NeoResult<()> {
859        Ok(())
860    }
861
862    /// Configure host-mode CheckSig/CheckMultisig results.
863    ///
864    /// `verify_with_ecdsa` tracks `check_sig` unless overridden explicitly.
865    #[cfg(not(target_arch = "wasm32"))]
866    pub fn set_crypto_verification_results(check_sig: bool, check_multisig: bool) -> NeoResult<()> {
867        Self::set_crypto_verification_results_full(check_sig, check_multisig, check_sig)
868    }
869
870    /// Configure host-mode crypto syscall results (secure default: all false).
871    #[cfg(not(target_arch = "wasm32"))]
872    pub fn set_crypto_verification_results_full(
873        check_sig: bool,
874        check_multisig: bool,
875        verify_with_ecdsa: bool,
876    ) -> NeoResult<()> {
877        crate::storage::set_crypto_verification_results(CryptoVerificationResults {
878            check_sig,
879            check_multisig,
880            verify_with_ecdsa,
881        });
882        Ok(())
883    }
884
885    /// Configure host-mode VerifyWithECDsa syscall result.
886    #[cfg(not(target_arch = "wasm32"))]
887    pub fn set_verify_with_ecdsa_result(result: bool) -> NeoResult<()> {
888        let mut current = active_crypto_verification_results();
889        current.verify_with_ecdsa = result;
890        crate::storage::set_crypto_verification_results(current);
891        Ok(())
892    }
893
894    /// Configure host-mode CheckSig/CheckMultisig results.
895    #[cfg(target_arch = "wasm32")]
896    pub fn set_crypto_verification_results(
897        _check_sig: bool,
898        _check_multisig: bool,
899    ) -> NeoResult<()> {
900        Ok(())
901    }
902
903    /// Configure host-mode crypto syscall results (secure default: all false).
904    #[cfg(target_arch = "wasm32")]
905    pub fn set_crypto_verification_results_full(
906        _check_sig: bool,
907        _check_multisig: bool,
908        _verify_with_ecdsa: bool,
909    ) -> NeoResult<()> {
910        Ok(())
911    }
912
913    /// Configure host-mode VerifyWithECDsa syscall result.
914    #[cfg(target_arch = "wasm32")]
915    pub fn set_verify_with_ecdsa_result(_result: bool) -> NeoResult<()> {
916        Ok(())
917    }
918
919    /// Get current timestamp
920    pub fn get_time() -> NeoResult<NeoInteger> {
921        #[cfg(target_arch = "wasm32")]
922        {
923            return Ok(NeoInteger::new(unsafe { neo_runtime_get_time() }));
924        }
925
926        #[cfg(not(target_arch = "wasm32"))]
927        {
928            Self::call_integer("System.Runtime.GetTime")
929        }
930    }
931
932    /// Get current timestamp as a plain `i64`.
933    ///
934    /// This keeps wasm contracts on the direct syscall import path and avoids
935    /// pulling arbitrary-precision integer conversion code into small
936    /// contracts that only need the native timestamp.
937    pub fn get_time_i64() -> NeoResult<i64> {
938        #[cfg(target_arch = "wasm32")]
939        {
940            return Ok(unsafe { neo_runtime_get_time() });
941        }
942
943        #[cfg(not(target_arch = "wasm32"))]
944        {
945            Self::call_integer("System.Runtime.GetTime")?.try_into_i64()
946        }
947    }
948
949    /// Check if the specified account is a witness
950    pub fn check_witness(account: &NeoByteString) -> NeoResult<NeoBoolean> {
951        Self::check_witness_bytes(account.as_slice())
952    }
953
954    /// Check if the specified account hash/public key bytes are a witness.
955    pub fn check_witness_bytes(account: &[u8]) -> NeoResult<NeoBoolean> {
956        #[cfg(target_arch = "wasm32")]
957        {
958            let result = unsafe {
959                neo_runtime_check_witness_bytes(account.as_ptr() as i32, account.len() as i32)
960            };
961            return Ok(NeoBoolean::new(result != 0));
962        }
963
964        #[cfg(not(target_arch = "wasm32"))]
965        {
966            let args = [NeoValue::from(NeoByteString::from_slice(account))];
967            Self::call_boolean("System.Runtime.CheckWitness", &args)
968        }
969    }
970
971    /// Check a compact sample-account identifier as a witness.
972    ///
973    /// This helper exists for the repository sample contracts that expose
974    /// account IDs as integers. Production contracts should prefer
975    /// `check_witness`/`check_witness_bytes` with real Hash160 account bytes.
976    pub fn check_witness_i64(account: i64) -> NeoResult<NeoBoolean> {
977        #[cfg(target_arch = "wasm32")]
978        {
979            let result = unsafe { neo_runtime_check_witness_i64(account) };
980            return Ok(NeoBoolean::new(result != 0));
981        }
982
983        #[cfg(not(target_arch = "wasm32"))]
984        {
985            let mut bytes = [0u8; 20];
986            bytes[..8].copy_from_slice(&account.to_le_bytes());
987            Self::check_witness_bytes(&bytes)
988        }
989    }
990
991    /// Send notification to the runtime.
992    pub fn notify(event: &NeoString, state: &NeoArray<NeoValue>) -> NeoResult<()> {
993        #[cfg(target_arch = "wasm32")]
994        {
995            // B2: serialise the state array and hand it to the VM
996            // via `runtime_notify_with_state`. Previously the state
997            // was dropped on the floor, so NEP-17/NEP-11 Transfer
998            // events emitted `Transfer(<empty>)` on mainnet.
999            let state_bytes = serialise_array(state);
1000            unsafe {
1001                neo_runtime_notify_with_state(
1002                    event.as_str().as_ptr() as i32,
1003                    event.as_str().len() as i32,
1004                    state_bytes.as_ptr() as i32,
1005                    state_bytes.len() as i32,
1006                );
1007            }
1008            // Also record in the host-side recorder so tests can
1009            // assert the event+state were seen together.
1010            crate::host_notifications::record(event, state);
1011            return Ok(());
1012        }
1013
1014        #[cfg(not(target_arch = "wasm32"))]
1015        {
1016            let event_bytes = NeoByteString::from_slice(event.as_str().as_bytes());
1017            let args = [NeoValue::from(event_bytes), NeoValue::from(state.clone())];
1018            neovm_syscall(syscall_hash("System.Runtime.Notify")?, &args)?;
1019            crate::host_notifications::record(event, state);
1020            Ok(())
1021        }
1022    }
1023
1024    /// Send a notification with an empty state array.
1025    pub fn notify_event(event: &str) -> NeoResult<()> {
1026        #[cfg(target_arch = "wasm32")]
1027        unsafe {
1028            neo_runtime_notify(event.as_ptr() as i32, event.len() as i32);
1029            Ok(())
1030        }
1031
1032        #[cfg(not(target_arch = "wasm32"))]
1033        {
1034            let label = NeoString::from_str(event);
1035            let state = NeoArray::new();
1036            Self::notify(&label, &state)
1037        }
1038    }
1039
1040    /// Log message to the runtime.
1041    pub fn log(message: &NeoString) -> NeoResult<()> {
1042        #[cfg(target_arch = "wasm32")]
1043        unsafe {
1044            let message = message.as_str();
1045            neo_runtime_log(message.as_ptr() as i32, message.len() as i32);
1046            Ok(())
1047        }
1048
1049        #[cfg(not(target_arch = "wasm32"))]
1050        {
1051            let message_bytes = NeoByteString::from_slice(message.as_str().as_bytes());
1052            let args = [NeoValue::from(message_bytes)];
1053            neovm_syscall(syscall_hash("System.Runtime.Log")?, &args)?;
1054            Ok(())
1055        }
1056    }
1057
1058    /// Platform identifier
1059    pub fn platform() -> NeoResult<NeoString> {
1060        // C#: always returns "NEO".
1061        #[cfg(target_arch = "wasm32")]
1062        {
1063            return Ok(NeoString::from_str("NEO"));
1064        }
1065        #[cfg(not(target_arch = "wasm32"))]
1066        Self::call_string("System.Runtime.Platform")
1067    }
1068
1069    pub fn get_trigger() -> NeoResult<NeoInteger> {
1070        #[cfg(target_arch = "wasm32")]
1071        {
1072            return Ok(NeoInteger::new(unsafe { neo_protocol_get_trigger() }));
1073        }
1074        #[cfg(not(target_arch = "wasm32"))]
1075        Self::call_integer("System.Runtime.GetTrigger")
1076    }
1077
1078    pub fn get_invocation_counter() -> NeoResult<NeoInteger> {
1079        #[cfg(target_arch = "wasm32")]
1080        {
1081            return Ok(NeoInteger::new(unsafe {
1082                neo_runtime_get_invocation_counter()
1083            }));
1084        }
1085        #[cfg(not(target_arch = "wasm32"))]
1086        Self::call_integer("System.Runtime.GetInvocationCounter")
1087    }
1088
1089    pub fn get_random() -> NeoResult<NeoInteger> {
1090        #[cfg(target_arch = "wasm32")]
1091        {
1092            return Ok(NeoInteger::new(unsafe { neo_runtime_get_random() }));
1093        }
1094        #[cfg(not(target_arch = "wasm32"))]
1095        Self::call_integer("System.Runtime.GetRandom")
1096    }
1097
1098    pub fn get_network() -> NeoResult<NeoInteger> {
1099        #[cfg(target_arch = "wasm32")]
1100        {
1101            return Ok(NeoInteger::new(unsafe { neo_protocol_get_network() }));
1102        }
1103        #[cfg(not(target_arch = "wasm32"))]
1104        Self::call_integer("System.Runtime.GetNetwork")
1105    }
1106
1107    pub fn get_address_version() -> NeoResult<NeoInteger> {
1108        #[cfg(target_arch = "wasm32")]
1109        {
1110            return Ok(NeoInteger::new(unsafe {
1111                neo_protocol_get_address_version()
1112            }));
1113        }
1114        #[cfg(not(target_arch = "wasm32"))]
1115        Self::call_integer("System.Runtime.GetAddressVersion")
1116    }
1117
1118    pub fn get_gas_left() -> NeoResult<NeoInteger> {
1119        #[cfg(target_arch = "wasm32")]
1120        {
1121            return Ok(NeoInteger::new(unsafe { neo_runtime_get_gas_left() }));
1122        }
1123        #[cfg(not(target_arch = "wasm32"))]
1124        Self::call_integer("System.Runtime.GasLeft")
1125    }
1126
1127    #[cfg(not(target_arch = "wasm32"))]
1128    pub fn get_calling_script_hash() -> NeoResult<NeoByteString> {
1129        Ok(NeoByteString::from_slice(&current_calling_script_hash()))
1130    }
1131
1132    #[cfg(target_arch = "wasm32")]
1133    pub fn get_calling_script_hash() -> NeoResult<NeoByteString> {
1134        Self::read_script_hash_extern(neo_runtime_get_calling_script_hash)
1135    }
1136
1137    #[cfg(not(target_arch = "wasm32"))]
1138    pub fn get_calling_script_hash_i64() -> NeoResult<i64> {
1139        Ok(hash160_prefix_i64(&current_calling_script_hash()))
1140    }
1141
1142    #[cfg(target_arch = "wasm32")]
1143    pub fn get_calling_script_hash_i64() -> NeoResult<i64> {
1144        Ok(unsafe { neo_runtime_get_calling_script_hash_i64() })
1145    }
1146
1147    #[cfg(not(target_arch = "wasm32"))]
1148    pub fn get_entry_script_hash() -> NeoResult<NeoByteString> {
1149        Ok(NeoByteString::from_slice(&current_entry_script_hash()))
1150    }
1151
1152    #[cfg(target_arch = "wasm32")]
1153    pub fn get_entry_script_hash() -> NeoResult<NeoByteString> {
1154        Self::read_script_hash_extern(neo_runtime_get_entry_script_hash)
1155    }
1156
1157    #[cfg(not(target_arch = "wasm32"))]
1158    pub fn get_entry_script_hash_i64() -> NeoResult<i64> {
1159        Ok(hash160_prefix_i64(&current_entry_script_hash()))
1160    }
1161
1162    #[cfg(target_arch = "wasm32")]
1163    pub fn get_entry_script_hash_i64() -> NeoResult<i64> {
1164        Ok(unsafe { neo_runtime_get_entry_script_hash_i64() })
1165    }
1166
1167    #[cfg(not(target_arch = "wasm32"))]
1168    pub fn get_executing_script_hash() -> NeoResult<NeoByteString> {
1169        Ok(NeoByteString::from_slice(&current_executing_script_hash()))
1170    }
1171
1172    #[cfg(target_arch = "wasm32")]
1173    pub fn get_executing_script_hash() -> NeoResult<NeoByteString> {
1174        Self::read_script_hash_extern(neo_runtime_get_executing_script_hash)
1175    }
1176
1177    #[cfg(not(target_arch = "wasm32"))]
1178    pub fn get_executing_script_hash_i64() -> NeoResult<i64> {
1179        Ok(hash160_prefix_i64(&current_executing_script_hash()))
1180    }
1181
1182    #[cfg(target_arch = "wasm32")]
1183    pub fn get_executing_script_hash_i64() -> NeoResult<i64> {
1184        Ok(unsafe { neo_runtime_get_executing_script_hash_i64() })
1185    }
1186
1187    /// Get notifications for the specified script hash, or all notifications if None.
1188    pub fn get_notifications(script_hash: Option<&NeoByteString>) -> NeoResult<NeoArray<NeoValue>> {
1189        #[cfg(target_arch = "wasm32")]
1190        {
1191            // B9: route to the real extern. Returns the number of bytes
1192            // written (0 if no notifications). Decoding the StackItem
1193            // binary is the responsibility of the host bridge.
1194            let mut buf = vec![0u8; 4096];
1195            let written = if let Some(hash) = script_hash {
1196                unsafe {
1197                    neo_runtime_get_notifications(
1198                        hash.as_slice().as_ptr() as i32,
1199                        hash.len() as i32,
1200                        buf.as_mut_ptr() as i32,
1201                        buf.len() as i32,
1202                    )
1203                }
1204            } else {
1205                // All notifications: pass a 0-length hash to signal "all".
1206                unsafe {
1207                    neo_runtime_get_notifications(
1208                        std::ptr::null::<u8>() as i32,
1209                        0,
1210                        buf.as_mut_ptr() as i32,
1211                        buf.len() as i32,
1212                    )
1213                }
1214            };
1215            if written < 0 {
1216                return Err(NeoError::InvalidState);
1217            }
1218            // Decoding the serialised notification array is the host's
1219            // job. For L1 we return an empty array; the full
1220            // deserialiser is the L6 conformance work.
1221            let _ = (written as usize).min(buf.len());
1222            Ok(NeoArray::new())
1223        }
1224        #[cfg(not(target_arch = "wasm32"))]
1225        {
1226            let script_hash_value = script_hash
1227                .map(|hash| NeoValue::from(hash.clone()))
1228                .unwrap_or(NeoValue::Null);
1229            let args = [script_hash_value];
1230            Self::call_array("System.Runtime.GetNotifications", &args)
1231        }
1232    }
1233
1234    pub fn get_script_container() -> NeoResult<NeoArray<NeoValue>> {
1235        #[cfg(target_arch = "wasm32")]
1236        {
1237            let mut buf = vec![0u8; 4096];
1238            let written = unsafe {
1239                neo_runtime_get_script_container(buf.as_mut_ptr() as i32, buf.len() as i32)
1240            };
1241            if written < 0 {
1242                return Err(NeoError::InvalidState);
1243            }
1244            let _ = (written as usize).min(buf.len());
1245            Ok(NeoArray::new())
1246        }
1247        #[cfg(not(target_arch = "wasm32"))]
1248        Self::call_array("System.Runtime.GetScriptContainer", &[])
1249    }
1250
1251    /// Burn GAS.
1252    pub fn burn_gas(gas: &NeoInteger) -> NeoResult<()> {
1253        #[cfg(target_arch = "wasm32")]
1254        {
1255            let datoshi = gas.as_i64_saturating();
1256            if datoshi <= 0 {
1257                return Err(NeoError::new("GAS must be positive"));
1258            }
1259            unsafe { neo_runtime_burn_gas(datoshi) };
1260            return Ok(());
1261        }
1262        #[cfg(not(target_arch = "wasm32"))]
1263        {
1264            let args = [NeoValue::from(gas.clone())];
1265            Self::call_value("System.Runtime.BurnGas", &args)?;
1266            Ok(())
1267        }
1268    }
1269
1270    /// Get active transaction signers.
1271    pub fn current_signers() -> NeoResult<NeoArray<NeoValue>> {
1272        #[cfg(target_arch = "wasm32")]
1273        {
1274            let mut buf = vec![0u8; 4096];
1275            let written =
1276                unsafe { neo_runtime_current_signers(buf.as_mut_ptr() as i32, buf.len() as i32) };
1277            if written < 0 {
1278                return Err(NeoError::InvalidState);
1279            }
1280            let _ = (written as usize).min(buf.len());
1281            Ok(NeoArray::new())
1282        }
1283        #[cfg(not(target_arch = "wasm32"))]
1284        Self::call_array("System.Runtime.CurrentSigners", &[])
1285    }
1286
1287    /// Dynamically load and execute a script.
1288    pub fn load_script(
1289        script: &NeoByteString,
1290        call_flags: &NeoInteger,
1291        args: &NeoArray<NeoValue>,
1292    ) -> NeoResult<()> {
1293        #[cfg(not(target_arch = "wasm32"))]
1294        {
1295            let values = [
1296                NeoValue::from(script.clone()),
1297                NeoValue::from(call_flags.clone()),
1298                NeoValue::from(args.clone()),
1299            ];
1300            Self::call_value("System.Runtime.LoadScript", &values)?;
1301            Ok(())
1302        }
1303        #[cfg(target_arch = "wasm32")]
1304        {
1305            // L6 real executor: call the `neo_load_script` extern
1306            // so the translator emits `SYSCALL System.Runtime.LoadScript`.
1307            // NOTE: call flags and args are not yet marshalled across the
1308            // boundary (the extern receives a hard-coded 0x0F / empty args);
1309            // wiring them through is tracked with the cross-call ABI work.
1310            let _ = (call_flags, args);
1311            let script_bytes = script.as_slice();
1312            let status = unsafe {
1313                neo_load_script(
1314                    script_bytes.as_ptr() as i32,
1315                    script_bytes.len() as i32,
1316                    0x0F,
1317                    0,
1318                    0,
1319                )
1320            };
1321            if status < 0 {
1322                return Err(NeoError::Wasm32CrossCallUnavailable {
1323                    syscall: "System.Runtime.LoadScript",
1324                });
1325            }
1326            Ok(())
1327        }
1328    }
1329
1330    /// Call any contract method.
1331    pub fn contract_call(
1332        script_hash: &NeoByteString,
1333        method: &NeoString,
1334        call_flags: &NeoInteger,
1335        args: &NeoArray<NeoValue>,
1336    ) -> NeoResult<NeoValue> {
1337        #[cfg(not(target_arch = "wasm32"))]
1338        {
1339            let values = [
1340                NeoValue::from(script_hash.clone()),
1341                NeoValue::from(method.clone()),
1342                NeoValue::from(call_flags.clone()),
1343                NeoValue::from(args.clone()),
1344            ];
1345            let parsed_flags = Self::parse_call_flags(call_flags)?;
1346            Self::begin_contract_invocation_with_flags(script_hash, parsed_flags)?;
1347            let call_result = Self::call_value("System.Contract.Call", &values);
1348            let unwind_result = Self::end_contract_invocation();
1349            match (call_result, unwind_result) {
1350                (Ok(value), Ok(())) => Ok(value),
1351                (Err(err), Ok(())) => Err(err),
1352                (Ok(_), Err(unwind_err)) => Err(unwind_err),
1353                (Err(call_err), Err(unwind_err)) => Err(NeoError::new(&format!(
1354                    "contract_call failed ({}) and invocation unwind failed ({})",
1355                    call_err.message(),
1356                    unwind_err.message()
1357                ))),
1358            }
1359        }
1360
1361        #[cfg(target_arch = "wasm32")]
1362        {
1363            // L6 real executor: call the `neo_contract_call` extern
1364            // (declared at the top of this file) so that the
1365            // wasm-neovm translator sees the import and emits the
1366            // correct `SYSCALL System.Contract.Call` opcode. The
1367            // host's NeoVM then dispatches the call at runtime.
1368            //
1369            // The minimum work here: invoke the extern so the
1370            // SYSCALL gets emitted. Argument serialisation and
1371            // output decoding are host-specific; the host provides
1372            // the implementation. We pass an empty args buffer;
1373            // the host will treat it as "no args" or error out,
1374            // either way the SYSCALL emission is what we're testing.
1375            // NOTE: call flags and args are not yet marshalled across the
1376            // boundary (a hard-coded 0x0F / empty args buffer is passed);
1377            // wiring them through is tracked with the cross-call ABI work.
1378            let _ = (call_flags, args);
1379            let hash_bytes = script_hash.as_slice();
1380            let method_bytes = method.as_str().as_bytes();
1381            let mut out_buf = [0u8; 16];
1382            let status = unsafe {
1383                neo_contract_call(
1384                    hash_bytes.as_ptr() as i32,
1385                    hash_bytes.len() as i32,
1386                    method_bytes.as_ptr() as i32,
1387                    method_bytes.len() as i32,
1388                    0,
1389                    0,
1390                    0x0F,
1391                    out_buf.as_mut_ptr() as i32,
1392                    out_buf.len() as i32,
1393                )
1394            };
1395            let _ = status;
1396            // The host may or may not have populated out_buf; we
1397            // return Null as a safe default. The test only cares
1398            // that the SYSCALL was emitted by the translator.
1399            Ok(NeoValue::Null)
1400        }
1401    }
1402
1403    /// Call a native contract by id.
1404    pub fn contract_call_native(native_id: &NeoInteger) -> NeoResult<NeoValue> {
1405        #[cfg(not(target_arch = "wasm32"))]
1406        {
1407            let values = [NeoValue::from(native_id.clone())];
1408            Self::call_value("System.Contract.CallNative", &values)
1409        }
1410        #[cfg(target_arch = "wasm32")]
1411        {
1412            // L6 real executor: call the `neo_call_native` extern
1413            // so the translator emits `SYSCALL System.Contract.CallNative`.
1414            let mut out_buf = [0u8; 16];
1415            let status = unsafe {
1416                neo_call_native(
1417                    native_id.try_as_i64().unwrap_or(0) as i32,
1418                    0,
1419                    0,
1420                    0,
1421                    0,
1422                    out_buf.as_mut_ptr() as i32,
1423                    out_buf.len() as i32,
1424                )
1425            };
1426            if status < 0 {
1427                return Err(NeoError::Wasm32CrossCallUnavailable {
1428                    syscall: "System.Contract.CallNative",
1429                });
1430            }
1431            Ok(NeoValue::Null)
1432        }
1433    }
1434
1435    pub fn get_call_flags() -> NeoResult<NeoInteger> {
1436        #[cfg(not(target_arch = "wasm32"))]
1437        {
1438            Ok(NeoInteger::new(current_call_flags()))
1439        }
1440
1441        #[cfg(target_arch = "wasm32")]
1442        {
1443            Ok(NeoInteger::new(unsafe { neo_runtime_get_call_flags() }))
1444        }
1445    }
1446
1447    pub fn create_standard_account(pubkey: &NeoByteString) -> NeoResult<NeoByteString> {
1448        #[cfg(not(target_arch = "wasm32"))]
1449        {
1450            let values = [NeoValue::from(pubkey.clone())];
1451            Self::call_bytes_with_args("System.Contract.CreateStandardAccount", &values)
1452        }
1453        #[cfg(target_arch = "wasm32")]
1454        {
1455            let mut buf = [0u8; 20];
1456            let written = unsafe {
1457                neo_runtime_create_standard_account(
1458                    pubkey.as_slice().as_ptr() as i32,
1459                    pubkey.len() as i32,
1460                    buf.as_mut_ptr() as i32,
1461                    buf.len() as i32,
1462                )
1463            };
1464            if written < 0 {
1465                return Err(NeoError::InvalidState);
1466            }
1467            let len = (written as usize).min(buf.len());
1468            Ok(NeoByteString::from_slice(&buf[..len]))
1469        }
1470    }
1471
1472    pub fn create_multisig_account(
1473        threshold: &NeoInteger,
1474        public_keys: &NeoArray<NeoValue>,
1475    ) -> NeoResult<NeoByteString> {
1476        let values = [
1477            NeoValue::from(threshold.clone()),
1478            NeoValue::from(public_keys.clone()),
1479        ];
1480        #[cfg(not(target_arch = "wasm32"))]
1481        {
1482            Self::call_bytes_with_args("System.Contract.CreateMultisigAccount", &values)
1483        }
1484        #[cfg(target_arch = "wasm32")]
1485        {
1486            // Serialise public_keys as an array of 33-byte ECPoint entries.
1487            // For L1 we just send the raw count and let the host decode.
1488            let pk_bytes: Vec<u8> = public_keys
1489                .iter()
1490                .filter_map(|v| v.as_byte_string())
1491                .flat_map(|bs| bs.as_slice().to_vec())
1492                .collect();
1493            let mut buf = [0u8; 20];
1494            let written = unsafe {
1495                neo_runtime_create_multisig_account(
1496                    threshold.as_i32_saturating(),
1497                    pk_bytes.as_ptr() as i32,
1498                    pk_bytes.len() as i32,
1499                    buf.as_mut_ptr() as i32,
1500                    buf.len() as i32,
1501                )
1502            };
1503            let _ = values;
1504            if written < 0 {
1505                return Err(NeoError::InvalidState);
1506            }
1507            let len = (written as usize).min(buf.len());
1508            Ok(NeoByteString::from_slice(&buf[..len]))
1509        }
1510    }
1511
1512    pub fn native_on_persist() -> NeoResult<()> {
1513        #[cfg(not(target_arch = "wasm32"))]
1514        {
1515            Self::call_value("System.Contract.NativeOnPersist", &[])?;
1516            Ok(())
1517        }
1518        #[cfg(target_arch = "wasm32")]
1519        {
1520            // System.Contract.NativeOnPersist is only valid inside a native
1521            // contract (the C# engine throws InvalidOperationException for
1522            // user contracts). User contracts that mistakenly call this
1523            // get a clean error rather than a panic at the engine layer.
1524            Err(NeoError::new(
1525                "System.Contract.NativeOnPersist is only valid inside native contracts",
1526            ))
1527        }
1528    }
1529
1530    pub fn native_post_persist() -> NeoResult<()> {
1531        #[cfg(not(target_arch = "wasm32"))]
1532        {
1533            Self::call_value("System.Contract.NativePostPersist", &[])?;
1534            Ok(())
1535        }
1536        #[cfg(target_arch = "wasm32")]
1537        {
1538            Err(NeoError::new(
1539                "System.Contract.NativePostPersist is only valid inside native contracts",
1540            ))
1541        }
1542    }
1543
1544    pub fn check_sig(pubkey: &NeoByteString, signature: &NeoByteString) -> NeoResult<NeoBoolean> {
1545        #[cfg(target_arch = "wasm32")]
1546        {
1547            // SAFETY: pointers/lengths come from valid byte-string slices.
1548            let result = unsafe {
1549                neo_runtime_check_sig(
1550                    pubkey.as_slice().as_ptr() as i32,
1551                    pubkey.len() as i32,
1552                    signature.as_slice().as_ptr() as i32,
1553                    signature.len() as i32,
1554                )
1555            };
1556            return Ok(NeoBoolean::new(result != 0));
1557        }
1558        #[cfg(not(target_arch = "wasm32"))]
1559        {
1560            let values = [
1561                NeoValue::from(pubkey.clone()),
1562                NeoValue::from(signature.clone()),
1563            ];
1564            Self::call_boolean("System.Crypto.CheckSig", &values)
1565        }
1566    }
1567
1568    pub fn check_multisig(
1569        pubkeys: &NeoArray<NeoValue>,
1570        signatures: &NeoArray<NeoValue>,
1571    ) -> NeoResult<NeoBoolean> {
1572        #[cfg(target_arch = "wasm32")]
1573        {
1574            // Flatten the NeoArray<NeoValue> of ByteStrings into a contiguous
1575            // buffer each (the simplest serialization the lowered SYSCALL
1576            // helper accepts). D3: the devpack's NeoArray is host-side
1577            // bookkeeping; on-chain CheckMultisig takes raw ByteStrings.
1578            let mut pk = Vec::new();
1579            for v in pubkeys.iter() {
1580                let Some(b) = v.as_byte_string() else {
1581                    return Err(NeoError::InvalidType);
1582                };
1583                pk.extend_from_slice(b.as_slice());
1584            }
1585            let mut sg = Vec::new();
1586            for v in signatures.iter() {
1587                let Some(b) = v.as_byte_string() else {
1588                    return Err(NeoError::InvalidType);
1589                };
1590                sg.extend_from_slice(b.as_slice());
1591            }
1592            // SAFETY: pointers/lengths come from valid vec allocations.
1593            let result = unsafe {
1594                neo_runtime_check_multisig(
1595                    pk.as_ptr() as i32,
1596                    pk.len() as i32,
1597                    sg.as_ptr() as i32,
1598                    sg.len() as i32,
1599                )
1600            };
1601            Ok(NeoBoolean::new(result != 0))
1602        }
1603        #[cfg(not(target_arch = "wasm32"))]
1604        {
1605            let values = [
1606                NeoValue::from(pubkeys.clone()),
1607                NeoValue::from(signatures.clone()),
1608            ];
1609            Self::call_boolean("System.Crypto.CheckMultisig", &values)
1610        }
1611    }
1612
1613    pub fn verify_with_ecdsa(
1614        message: &NeoByteString,
1615        public_key: &NeoByteString,
1616        signature: &NeoByteString,
1617        curve: &NeoInteger,
1618    ) -> NeoResult<NeoBoolean> {
1619        #[cfg(target_arch = "wasm32")]
1620        {
1621            let curve_i = curve.try_as_i32().unwrap_or(0);
1622            // SAFETY: pointers/lengths come from valid byte-string slices.
1623            let result = unsafe {
1624                neo_runtime_verify_with_ecdsa(
1625                    message.as_slice().as_ptr() as i32,
1626                    message.len() as i32,
1627                    public_key.as_slice().as_ptr() as i32,
1628                    public_key.len() as i32,
1629                    signature.as_slice().as_ptr() as i32,
1630                    signature.len() as i32,
1631                    curve_i,
1632                )
1633            };
1634            return Ok(NeoBoolean::new(result != 0));
1635        }
1636        #[cfg(not(target_arch = "wasm32"))]
1637        {
1638            let values = [
1639                NeoValue::from(message.clone()),
1640                NeoValue::from(public_key.clone()),
1641                NeoValue::from(signature.clone()),
1642                NeoValue::from(curve.clone()),
1643            ];
1644            Self::call_boolean("Neo.Crypto.VerifyWithECDsa", &values)
1645        }
1646    }
1647
1648    pub fn iterator_next(items: &NeoArray<NeoValue>) -> NeoResult<NeoBoolean> {
1649        #[cfg(target_arch = "wasm32")]
1650        {
1651            // Iterators on-chain are an InteropInterface stack item;
1652            // the translator emits a direct `SYSCALL System.Iterator.Next`
1653            // that the VM resolves with a session id. The devpack
1654            // wrapper is for host-mode tests. On wasm32, reaching this
1655            // helper means the translator failed to lower the call to a
1656            // direct SYSCALL (a translator bug, Q4). Fault gracefully
1657            // with a structured error rather than aborting the VM with an
1658            // `unreachable` trap.
1659            let _ = items;
1660            Err(NeoError::Wasm32CrossCallUnavailable {
1661                syscall: "System.Iterator.Next",
1662            })
1663        }
1664        #[cfg(not(target_arch = "wasm32"))]
1665        {
1666            let values = [NeoValue::from(items.clone())];
1667            Self::call_boolean("System.Iterator.Next", &values)
1668        }
1669    }
1670
1671    pub fn iterator_value(items: &NeoArray<NeoValue>) -> NeoResult<NeoValue> {
1672        #[cfg(target_arch = "wasm32")]
1673        {
1674            let _ = items;
1675            Err(NeoError::Wasm32CrossCallUnavailable {
1676                syscall: "System.Iterator.Value",
1677            })
1678        }
1679        #[cfg(not(target_arch = "wasm32"))]
1680        {
1681            let values = [NeoValue::from(items.clone())];
1682            Self::call_value("System.Iterator.Value", &values)
1683        }
1684    }
1685
1686    #[cfg(not(target_arch = "wasm32"))]
1687    pub fn storage_get_context() -> NeoResult<NeoStorageContext> {
1688        let flags = current_call_flags();
1689        if !call_flags_allow_read(flags) {
1690            return Err(NeoError::InvalidOperation);
1691        }
1692        let read_only = !call_flags_allow_write(flags);
1693        STORAGE_STATE.create_context(current_executing_script_hash(), read_only)
1694    }
1695
1696    /// On wasm32 we return a sentinel `NeoStorageContext`. The translator
1697    /// emits a fresh `SYSCALL System.Storage.GetContext` inside each storage
1698    /// helper, so the i32 id carried by this struct is irrelevant to NeoVM —
1699    /// the only field that affects translated bytecode is the `read_only`
1700    /// marker, which is enforced by the wasm32 wrappers below.
1701    #[cfg(target_arch = "wasm32")]
1702    pub fn storage_get_context() -> NeoResult<NeoStorageContext> {
1703        Ok(NeoStorageContext::new(1))
1704    }
1705
1706    #[cfg(not(target_arch = "wasm32"))]
1707    pub fn storage_get_read_only_context() -> NeoResult<NeoStorageContext> {
1708        if !call_flags_allow_read(current_call_flags()) {
1709            return Err(NeoError::InvalidOperation);
1710        }
1711        STORAGE_STATE.create_context(current_executing_script_hash(), true)
1712    }
1713
1714    #[cfg(target_arch = "wasm32")]
1715    pub fn storage_get_read_only_context() -> NeoResult<NeoStorageContext> {
1716        Ok(NeoStorageContext::read_only(1))
1717    }
1718
1719    #[cfg(not(target_arch = "wasm32"))]
1720    pub fn storage_as_read_only(context: &NeoStorageContext) -> NeoResult<NeoStorageContext> {
1721        STORAGE_STATE.clone_as_read_only(context)
1722    }
1723
1724    #[cfg(target_arch = "wasm32")]
1725    pub fn storage_as_read_only(context: &NeoStorageContext) -> NeoResult<NeoStorageContext> {
1726        Ok(context.as_read_only())
1727    }
1728
1729    #[cfg(not(target_arch = "wasm32"))]
1730    pub fn storage_get(
1731        context: &NeoStorageContext,
1732        key: &NeoByteString,
1733    ) -> NeoResult<NeoByteString> {
1734        if !call_flags_allow_read(current_call_flags()) {
1735            return Err(NeoError::InvalidOperation);
1736        }
1737        let handle = STORAGE_STATE.get_handle(context)?;
1738        let store = handle.store.read().map_err(|_| NeoError::InvalidState)?;
1739        let value = store.get(key.as_slice()).cloned().unwrap_or_else(Vec::new);
1740        Ok(NeoByteString::new(value))
1741    }
1742
1743    #[cfg(not(target_arch = "wasm32"))]
1744    pub fn storage_try_get(
1745        context: &NeoStorageContext,
1746        key: &NeoByteString,
1747    ) -> NeoResult<Option<NeoByteString>> {
1748        if !call_flags_allow_read(current_call_flags()) {
1749            return Err(NeoError::InvalidOperation);
1750        }
1751        let handle = STORAGE_STATE.get_handle(context)?;
1752        let store = handle.store.read().map_err(|_| NeoError::InvalidState)?;
1753        Ok(store.get(key.as_slice()).cloned().map(NeoByteString::new))
1754    }
1755
1756    #[cfg(not(target_arch = "wasm32"))]
1757    pub fn storage_put(
1758        context: &NeoStorageContext,
1759        key: &NeoByteString,
1760        value: &NeoByteString,
1761    ) -> NeoResult<()> {
1762        if !call_flags_allow_write(current_call_flags()) {
1763            return Err(NeoError::InvalidOperation);
1764        }
1765        let handle = STORAGE_STATE.get_handle(context)?;
1766        if handle.read_only {
1767            return Err(NeoError::InvalidOperation);
1768        }
1769        let mut store = handle.store.write().map_err(|_| NeoError::InvalidState)?;
1770        store.insert(key.as_slice().to_vec(), value.as_slice().to_vec());
1771        Ok(())
1772    }
1773
1774    /// Writes through to real Neo persistent storage. The translator lowers
1775    /// `neo_storage_put_bytes` to a `CALL_L` that emits the
1776    /// `System.Storage.GetContext + System.Storage.Put` SYSCALL pair. The
1777    /// `read_only` check on the supplied marker still runs first so contracts
1778    /// that hand a read-only context to `put` short-circuit before crossing
1779    /// the wasm boundary.
1780    #[cfg(target_arch = "wasm32")]
1781    pub fn storage_put(
1782        context: &NeoStorageContext,
1783        key: &NeoByteString,
1784        value: &NeoByteString,
1785    ) -> NeoResult<()> {
1786        if context.is_read_only() {
1787            return Err(NeoError::InvalidOperation);
1788        }
1789
1790        let key_slice = key.as_slice();
1791        let value_slice = value.as_slice();
1792        unsafe {
1793            neo_storage_put_bytes(
1794                key_slice.as_ptr() as i32,
1795                key_slice.len() as i32,
1796                value_slice.as_ptr() as i32,
1797                value_slice.len() as i32,
1798            );
1799        }
1800        Ok(())
1801    }
1802
1803    /// Reads through to real Neo persistent storage via the translator-emitted
1804    /// `neo_storage_get_into` helper. The helper writes the stored bytes into
1805    /// the local `buffer` (sized up on demand) and reports the actual length;
1806    /// missing keys return an empty `NeoByteString`, matching the host-mode
1807    /// semantics already exercised by the devpack tests.
1808    #[cfg(target_arch = "wasm32")]
1809    pub fn storage_get(
1810        _context: &NeoStorageContext,
1811        key: &NeoByteString,
1812    ) -> NeoResult<NeoByteString> {
1813        const INITIAL_CAPACITY: usize = 64;
1814        const MAX_CAPACITY: usize = 64 * 1024;
1815
1816        let key_slice = key.as_slice();
1817        let mut buffer: Vec<u8> = vec![0u8; INITIAL_CAPACITY];
1818        loop {
1819            let actual = unsafe {
1820                neo_storage_get_into(
1821                    key_slice.as_ptr() as i32,
1822                    key_slice.len() as i32,
1823                    buffer.as_mut_ptr() as i32,
1824                    buffer.len() as i32,
1825                )
1826            };
1827            if actual == -1 {
1828                return Ok(NeoByteString::new(Vec::new()));
1829            }
1830            if actual >= 0 {
1831                let len = actual as usize;
1832                buffer.truncate(len);
1833                return Ok(NeoByteString::new(buffer));
1834            }
1835            // -needed_len: grow buffer and retry.
1836            let needed = (-actual) as usize;
1837            if needed > MAX_CAPACITY {
1838                return Err(NeoError::InvalidState);
1839            }
1840            buffer.resize(needed, 0);
1841        }
1842    }
1843
1844    #[cfg(not(target_arch = "wasm32"))]
1845    pub fn storage_delete(context: &NeoStorageContext, key: &NeoByteString) -> NeoResult<()> {
1846        if !call_flags_allow_write(current_call_flags()) {
1847            return Err(NeoError::InvalidOperation);
1848        }
1849        let handle = STORAGE_STATE.get_handle(context)?;
1850        if handle.read_only {
1851            return Err(NeoError::InvalidOperation);
1852        }
1853        let mut store = handle.store.write().map_err(|_| NeoError::InvalidState)?;
1854        store.remove(key.as_slice());
1855        Ok(())
1856    }
1857
1858    /// Deletes the key from real Neo persistent storage via
1859    /// `neo_storage_delete_bytes`, which the translator lowers to
1860    /// `System.Storage.GetContext + System.Storage.Delete`.
1861    #[cfg(target_arch = "wasm32")]
1862    pub fn storage_delete(context: &NeoStorageContext, key: &NeoByteString) -> NeoResult<()> {
1863        if context.is_read_only() {
1864            return Err(NeoError::InvalidOperation);
1865        }
1866
1867        let key_slice = key.as_slice();
1868        unsafe {
1869            neo_storage_delete_bytes(key_slice.as_ptr() as i32, key_slice.len() as i32);
1870        }
1871        Ok(())
1872    }
1873
1874    #[cfg(not(target_arch = "wasm32"))]
1875    pub fn storage_find(
1876        context: &NeoStorageContext,
1877        prefix: &NeoByteString,
1878    ) -> NeoResult<NeoIterator<NeoValue>> {
1879        if !call_flags_allow_read(current_call_flags()) {
1880            return Err(NeoError::InvalidOperation);
1881        }
1882        let handle = STORAGE_STATE.get_handle(context)?;
1883        let prefix_bytes = prefix.as_slice();
1884        let store = handle.store.read().map_err(|_| NeoError::InvalidState)?;
1885        let matches: Vec<NeoValue> = store
1886            .iter()
1887            .filter_map(|(key_bytes, value)| {
1888                if key_bytes.starts_with(prefix_bytes) {
1889                    let mut entry = NeoStruct::new();
1890                    entry.set_field("key", NeoValue::from(NeoByteString::from_slice(key_bytes)));
1891                    entry.set_field("value", NeoValue::from(NeoByteString::from_slice(value)));
1892                    Some(NeoValue::from(entry))
1893                } else {
1894                    None
1895                }
1896            })
1897            .collect();
1898        Ok(NeoIterator::new(matches))
1899    }
1900
1901    /// On wasm32 `storage_find` returns an empty iterator. Bridging a real
1902    /// `System.Storage.Find` iterator handle through wasm would require
1903    /// special-cased translator support for `System.Iterator.Next/Value`
1904    /// on top of the byte-marshalled `Get/Put/Delete` primitives that this
1905    /// module already lowers; contracts that need prefix iteration must use
1906    /// indexed enumeration backed by `storage_get` until that lands.
1907    #[cfg(target_arch = "wasm32")]
1908    pub fn storage_find(
1909        _context: &NeoStorageContext,
1910        _prefix: &NeoByteString,
1911    ) -> NeoResult<NeoIterator<NeoValue>> {
1912        Ok(NeoIterator::new(Vec::new()))
1913    }
1914}