Skip to main content

neo_test/
mock_runtime.rs

1// Copyright (c) 2025-2026 R3E Network
2// Licensed under the MIT License
3
4//! Mock Runtime for Testing Neo N3 Smart Contracts
5//!
6//! This module provides a comprehensive mock runtime environment for testing
7//! Neo N3 smart contracts without requiring a full Neo node.
8
9use neo_types::*;
10use std::collections::HashMap;
11
12/// Mock storage that simulates Neo blockchain storage
13#[derive(Debug, Clone, Default)]
14pub struct MockStorage {
15    data: HashMap<Vec<u8>, Vec<u8>>,
16}
17
18impl MockStorage {
19    pub fn new() -> Self {
20        Self::default()
21    }
22
23    pub fn get(&self, key: &[u8]) -> Option<Vec<u8>> {
24        self.data.get(key).cloned()
25    }
26
27    pub fn put(&mut self, key: &[u8], value: &[u8]) {
28        self.data.insert(key.to_vec(), value.to_vec());
29    }
30
31    pub fn delete(&mut self, key: &[u8]) {
32        self.data.remove(key);
33    }
34
35    pub fn contains(&self, key: &[u8]) -> bool {
36        self.data.contains_key(key)
37    }
38
39    pub fn clear(&mut self) {
40        self.data.clear();
41    }
42
43    pub fn len(&self) -> usize {
44        self.data.len()
45    }
46
47    pub fn is_empty(&self) -> bool {
48        self.data.is_empty()
49    }
50
51    pub fn keys(&self) -> Vec<Vec<u8>> {
52        self.data.keys().cloned().collect()
53    }
54
55    pub fn find(&self, prefix: &[u8]) -> Vec<(Vec<u8>, Vec<u8>)> {
56        self.data
57            .iter()
58            .filter(|(k, _)| k.starts_with(prefix))
59            .map(|(k, v)| (k.clone(), v.clone()))
60            .collect()
61    }
62}
63
64/// Mock storage context for simulating storage operations
65#[derive(Debug, Clone)]
66pub struct MockStorageContext {
67    pub id: u32,
68    is_read_only: bool,
69}
70
71impl MockStorageContext {
72    pub fn new(id: u32) -> Self {
73        Self {
74            id,
75            is_read_only: false,
76        }
77    }
78
79    pub fn read_only(id: u32) -> Self {
80        Self {
81            id,
82            is_read_only: true,
83        }
84    }
85
86    pub fn is_read_only(&self) -> bool {
87        self.is_read_only
88    }
89}
90
91/// Mock runtime for testing contract execution
92///
93/// This provides a complete simulation of the Neo N3 runtime environment
94/// for testing smart contracts.
95#[derive(Debug, Clone)]
96pub struct MockRuntime {
97    pub storage: MockStorage,
98    pub trigger: i32,
99    pub time: i64,
100    pub network: i64,
101    pub address_version: i32,
102    witnesses: Vec<NeoByteString>,
103    pub notifications: Vec<(NeoString, NeoArray<NeoValue>)>,
104    pub logs: Vec<String>,
105    pub script_container: Option<NeoArray<NeoValue>>,
106    pub calling_script_hash: Option<NeoByteString>,
107    pub executing_script_hash: Option<NeoByteString>,
108    pub entry_script_hash: Option<NeoByteString>,
109    pub gas_left: i64,
110    pub invocation_counter: i32,
111    storage_contexts: Vec<MockStorageContext>,
112}
113
114impl Default for MockRuntime {
115    fn default() -> Self {
116        Self::new()
117    }
118}
119
120impl MockRuntime {
121    pub fn new() -> Self {
122        Self {
123            storage: MockStorage::new(),
124            trigger: 0,
125            time: 0,
126            network: 860905102, // MainNet
127            address_version: 53,
128            witnesses: Vec::new(),
129            notifications: Vec::new(),
130            logs: Vec::new(),
131            script_container: None,
132            calling_script_hash: None,
133            executing_script_hash: None,
134            entry_script_hash: None,
135            gas_left: 100_000_000, // 100 GAS
136            invocation_counter: 0,
137            storage_contexts: vec![MockStorageContext::new(0)],
138        }
139    }
140
141    pub fn with_storage(mut self, storage: MockStorage) -> Self {
142        self.storage = storage;
143        self
144    }
145
146    pub fn with_trigger(mut self, trigger: i32) -> Self {
147        self.trigger = trigger;
148        self
149    }
150
151    pub fn with_time(mut self, time: i64) -> Self {
152        self.time = time;
153        self
154    }
155
156    pub fn with_network(mut self, network: i64) -> Self {
157        self.network = network;
158        self
159    }
160
161    pub fn with_witness(mut self, address: &[u8]) -> Self {
162        self.witnesses.push(NeoByteString::from_slice(address));
163        self
164    }
165
166    pub fn with_script_container(mut self, container: NeoArray<NeoValue>) -> Self {
167        self.script_container = Some(container);
168        self
169    }
170
171    pub fn with_calling_script_hash(mut self, hash: &[u8]) -> Self {
172        self.calling_script_hash = Some(NeoByteString::from_slice(hash));
173        self
174    }
175
176    pub fn with_executing_script_hash(mut self, hash: &[u8]) -> Self {
177        self.executing_script_hash = Some(NeoByteString::from_slice(hash));
178        self
179    }
180
181    pub fn with_entry_script_hash(mut self, hash: &[u8]) -> Self {
182        self.entry_script_hash = Some(NeoByteString::from_slice(hash));
183        self
184    }
185
186    pub fn with_gas_left(mut self, gas: i64) -> Self {
187        self.gas_left = gas;
188        self
189    }
190
191    pub fn storage_ref(&self) -> &MockStorage {
192        &self.storage
193    }
194
195    pub fn storage_mut(&mut self) -> &mut MockStorage {
196        &mut self.storage
197    }
198
199    pub fn trigger_value(&self) -> i32 {
200        self.trigger
201    }
202
203    pub fn time_value(&self) -> i64 {
204        self.time
205    }
206
207    pub fn network_value(&self) -> i64 {
208        self.network
209    }
210
211    pub fn address_version_value(&self) -> i32 {
212        self.address_version
213    }
214
215    pub fn witnesses(&self) -> &[NeoByteString] {
216        &self.witnesses
217    }
218
219    pub fn witnesses_mut(&mut self) -> &mut Vec<NeoByteString> {
220        &mut self.witnesses
221    }
222
223    pub fn notifications(&self) -> &[(NeoString, NeoArray<NeoValue>)] {
224        &self.notifications
225    }
226
227    pub fn logs(&self) -> &[String] {
228        &self.logs
229    }
230
231    pub fn clear_notifications(&mut self) {
232        self.notifications.clear();
233    }
234
235    pub fn clear_logs(&mut self) {
236        self.logs.clear();
237    }
238
239    pub fn add_log(&mut self, message: &str) {
240        self.logs.push(message.to_string());
241    }
242
243    pub fn add_notification(&mut self, event: NeoString, state: NeoArray<NeoValue>) {
244        self.notifications.push((event, state));
245    }
246
247    pub fn check_witness(&self, hash: &[u8]) -> bool {
248        self.witnesses.iter().any(|w| w.as_slice() == hash)
249    }
250
251    pub fn calling_script_hash(&self) -> Option<&NeoByteString> {
252        self.calling_script_hash.as_ref()
253    }
254
255    pub fn executing_script_hash(&self) -> Option<&NeoByteString> {
256        self.executing_script_hash.as_ref()
257    }
258
259    pub fn entry_script_hash(&self) -> Option<&NeoByteString> {
260        self.entry_script_hash.as_ref()
261    }
262
263    pub fn gas_left(&self) -> i64 {
264        self.gas_left
265    }
266
267    pub fn consume_gas(&mut self, amount: i64) {
268        if amount <= 0 {
269            return;
270        }
271
272        self.gas_left = self.gas_left.checked_sub(amount).unwrap_or(0).max(0);
273    }
274
275    pub fn invocation_counter(&self) -> i32 {
276        self.invocation_counter
277    }
278
279    pub fn increment_invocation_counter(&mut self) {
280        self.invocation_counter += 1;
281    }
282
283    pub fn get_storage_context(&mut self) -> MockStorageContext {
284        let id = self.storage_contexts.len() as u32;
285        self.storage_contexts.push(MockStorageContext::new(id));
286        MockStorageContext::new(id)
287    }
288
289    pub fn get_read_only_storage_context(&mut self) -> MockStorageContext {
290        let id = self.storage_contexts.len() as u32;
291        self.storage_contexts
292            .push(MockStorageContext::read_only(id));
293        MockStorageContext::read_only(id)
294    }
295
296    /// Simulate storage get operation
297    pub fn storage_get(&self, key: &[u8]) -> Option<Vec<u8>> {
298        self.storage.get(key)
299    }
300
301    /// Simulate storage put operation
302    pub fn storage_put(&mut self, key: &[u8], value: &[u8]) {
303        self.storage.put(key, value);
304    }
305
306    /// Simulate storage put operation with storage context validation.
307    pub fn storage_put_with_context(
308        &mut self,
309        context: &MockStorageContext,
310        key: &[u8],
311        value: &[u8],
312    ) -> NeoResult<()> {
313        self.ensure_context_valid(context)?;
314        if context.is_read_only() {
315            return Err(NeoError::InvalidOperation);
316        }
317
318        self.storage.put(key, value);
319        Ok(())
320    }
321
322    /// Simulate storage delete operation
323    pub fn storage_delete(&mut self, key: &[u8]) {
324        self.storage.delete(key);
325    }
326
327    /// Simulate storage delete operation with storage context validation.
328    pub fn storage_delete_with_context(
329        &mut self,
330        context: &MockStorageContext,
331        key: &[u8],
332    ) -> NeoResult<()> {
333        self.ensure_context_valid(context)?;
334        if context.is_read_only() {
335            return Err(NeoError::InvalidOperation);
336        }
337
338        self.storage.delete(key);
339        Ok(())
340    }
341
342    /// Simulate storage find operation
343    pub fn storage_find(&self, prefix: &[u8]) -> Vec<(Vec<u8>, Vec<u8>)> {
344        self.storage.find(prefix)
345    }
346
347    /// Reset the runtime state
348    pub fn reset(&mut self) {
349        self.notifications.clear();
350        self.logs.clear();
351        self.invocation_counter = 0;
352    }
353
354    /// Reset known storage contexts to the runtime default context.
355    pub fn clear_storage_contexts(&mut self) {
356        self.storage_contexts.clear();
357        self.storage_contexts.push(MockStorageContext::new(0));
358    }
359
360    fn ensure_context_valid(&self, context: &MockStorageContext) -> NeoResult<()> {
361        let is_known_context = self
362            .storage_contexts
363            .iter()
364            .any(|ctx| ctx.id == context.id && ctx.is_read_only == context.is_read_only);
365        if !is_known_context {
366            return Err(NeoError::InvalidArgument);
367        }
368
369        Ok(())
370    }
371}
372
373/// Builder for creating mock runtime
374pub struct MockRuntimeBuilder {
375    runtime: MockRuntime,
376}
377
378impl MockRuntimeBuilder {
379    pub fn new() -> Self {
380        Self {
381            runtime: MockRuntime::new(),
382        }
383    }
384
385    pub fn storage(mut self, storage: MockStorage) -> Self {
386        self.runtime.storage = storage;
387        self
388    }
389
390    pub fn trigger(mut self, trigger: i32) -> Self {
391        self.runtime.trigger = trigger;
392        self
393    }
394
395    pub fn time(mut self, time: i64) -> Self {
396        self.runtime.time = time;
397        self
398    }
399
400    pub fn network(mut self, network: i64) -> Self {
401        self.runtime.network = network;
402        self
403    }
404
405    pub fn witness(mut self, address: &[u8]) -> Self {
406        self.runtime
407            .witnesses
408            .push(NeoByteString::from_slice(address));
409        self
410    }
411
412    pub fn script_hash(mut self, hash: &[u8]) -> Self {
413        self.runtime.executing_script_hash = Some(NeoByteString::from_slice(hash));
414        self
415    }
416
417    pub fn gas(mut self, gas: i64) -> Self {
418        self.runtime.gas_left = gas;
419        self
420    }
421
422    pub fn build(self) -> MockRuntime {
423        self.runtime
424    }
425}
426
427impl Default for MockRuntimeBuilder {
428    fn default() -> Self {
429        Self::new()
430    }
431}