Skip to main content

neo_syscalls/
host_notifications.rs

1// Copyright (c) 2025-2026 R3E Network
2// Licensed under the MIT License
3
4//! Host-side notification recorder.
5//!
6//! When contracts call `NeoRuntime::notify(event, state)` on the host
7//! (i.e. not on the wasm32 path), the call is recorded here so tests
8//! can assert what was emitted. The wasm32 path emits to the Neo VM
9//! host directly via `runtime_notify_with_state`; for host tests we
10//! mirror the event into this recorder so the test surface is
11//! identical regardless of target.
12//!
13//! The `RecordedNotification` type is always defined (it's part of
14//! the public API surface used by tests), but the backing storage and
15//! the `record` function are host-only.
16
17use neo_types::{NeoArray, NeoString, NeoValue};
18
19#[derive(Debug, Clone)]
20pub struct RecordedNotification {
21    pub event: String,
22    pub state: Vec<NeoValue>,
23}
24
25#[cfg(not(target_arch = "wasm32"))]
26mod inner {
27    use super::*;
28    use once_cell::sync::Lazy;
29    use std::sync::Mutex;
30
31    static RECORDED: Lazy<Mutex<Vec<RecordedNotification>>> = Lazy::new(|| Mutex::new(Vec::new()));
32
33    pub fn record(event: &NeoString, state: &NeoArray<NeoValue>) {
34        let mut g = RECORDED.lock().expect("notification recorder poisoned");
35        g.push(RecordedNotification {
36            event: event.as_str().to_string(),
37            state: state.iter().cloned().collect(),
38        });
39    }
40
41    pub fn take() -> Vec<RecordedNotification> {
42        let mut g = RECORDED.lock().expect("notification recorder poisoned");
43        std::mem::take(&mut *g)
44    }
45
46    pub fn reset() {
47        let mut g = RECORDED.lock().expect("notification recorder poisoned");
48        g.clear();
49    }
50}
51
52/// Record a notification. Called from `NeoVMSyscall::notify` on
53/// both wasm32 and host paths so the recorder is consistent.
54#[cfg(not(target_arch = "wasm32"))]
55pub fn record(event: &NeoString, state: &NeoArray<NeoValue>) {
56    inner::record(event, state);
57}
58#[cfg(target_arch = "wasm32")]
59pub fn record(_event: &NeoString, _state: &NeoArray<NeoValue>) {
60    // No-op on wasm32: the VM itself records notifications in
61    // ApplicationEngine.cs::notifications.
62}
63
64/// Take all recorded notifications (drains the buffer).
65pub fn take() -> Vec<RecordedNotification> {
66    #[cfg(not(target_arch = "wasm32"))]
67    {
68        inner::take()
69    }
70    #[cfg(target_arch = "wasm32")]
71    {
72        Vec::new()
73    }
74}
75
76/// Clear the recorder without returning the contents.
77pub fn reset() {
78    #[cfg(not(target_arch = "wasm32"))]
79    {
80        inner::reset()
81    }
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87    use crate::NeoByteString;
88
89    #[test]
90    fn record_and_take_round_trip() {
91        reset();
92        let evt = NeoString::from_str("Transfer");
93        let mut state = NeoArray::new();
94        state.push(NeoValue::from(crate::NeoInteger::new(100i32)));
95        state.push(NeoValue::from(crate::NeoBoolean::new(true)));
96        record(&evt, &state);
97        let got = take();
98        assert_eq!(got.len(), 1);
99        assert_eq!(got[0].event, "Transfer");
100        assert_eq!(got[0].state.len(), 2);
101    }
102
103    #[test]
104    fn bytestring_value_round_trip() {
105        // smoke test for the stack_item serialiser via a single ByteString arg
106        let _ = NeoByteString::from_slice(b"hello");
107    }
108}