Skip to main content

neo_runtime/
contract_caller.rs

1// Copyright (c) 2025-2026 R3E Network
2// Licensed under the MIT License
3
4//! L9: typed cross-contract calls via the `ContractCaller` trait.
5//!
6//! The `FromNeoValue` + `ContractCaller::call_typed<T>` pair
7//! mirrors the C# devpack's `IInteroperable.FromStackItem` +
8//! `Contract.Call<T>` pattern. The default `DefaultContractCaller`
9//! routes through `NeoVMSyscall::contract_call`; the L6
10//! cross-call executor will add a real wasm32 implementation
11//! that doesn't panic (B4 fix).
12
13use neo_syscalls::NeoVMSyscall;
14use neo_types::{
15    ContractCaller, NeoArray, NeoByteString, NeoError, NeoInteger, NeoResult, NeoString, NeoValue,
16};
17
18/// The default `ContractCaller` impl: routes `call_raw` to
19/// `NeoVMSyscall::contract_call`. Used by host-mode tests and
20/// by production code that needs the L6 cross-call upgrade
21/// (tracked in the audit).
22pub struct DefaultContractCaller;
23
24impl ContractCaller for DefaultContractCaller {
25    fn call_raw(
26        &self,
27        script_hash: &NeoByteString,
28        method: &str,
29        args: &[NeoValue],
30        call_flags: &NeoInteger,
31    ) -> NeoResult<NeoValue> {
32        // We pass the method as a NeoString here. The syscall
33        // signature in neo-syscalls takes &NeoString; we
34        // construct it from the &str method name. The user
35        // should not call this directly; `call_typed<T>` is
36        // the public API.
37        let method_str = NeoString::from_str(method);
38        let args_array: NeoArray<NeoValue> = args.iter().cloned().collect();
39        NeoVMSyscall::contract_call(script_hash, &method_str, call_flags, &args_array)
40    }
41}
42
43/// Helper: typed cross-contract call with the default caller.
44/// Mirrors the C# `Contract.Call<T>` API.
45pub fn call_typed<T: neo_types::FromNeoValue>(
46    script_hash: &NeoByteString,
47    method: &str,
48    args: &[NeoValue],
49    call_flags: &NeoInteger,
50) -> NeoResult<T> {
51    DefaultContractCaller.call_typed(script_hash, method, args, call_flags)
52}
53
54/// Error type for the L9 typed-call helpers. Reserved for
55/// future use (the L6 cross-call executor will return this
56/// rather than panicking with "see L6 design").
57#[derive(Debug, Clone, PartialEq, Eq)]
58pub enum ContractCallError {
59    /// The call returned no value.
60    NoReturn,
61    /// The call returned a value of the wrong type.
62    TypeMismatch(String),
63    /// The call panicked (L6: cross-call executor will replace
64    /// this with a proper Result).
65    Panicked(String),
66    /// L6 minimal: the wasm32 cross-call stub (System.Contract.Call
67    /// / System.Runtime.LoadScript / System.Contract.CallNative)
68    /// was invoked, but a real wasm32 cross-call executor is not
69    /// yet implemented. The contract author can `match` on this
70    /// variant and degrade gracefully (e.g. return a default
71    /// value) instead of crashing the VM.
72    Wasm32CrossCallUnavailable { syscall: &'static str },
73    /// Other (delegated from `NeoError`).
74    Other(NeoError),
75}
76
77impl std::fmt::Display for ContractCallError {
78    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79        match self {
80            ContractCallError::NoReturn => write!(f, "contract call returned no value"),
81            ContractCallError::TypeMismatch(s) => write!(f, "type mismatch: {s}"),
82            ContractCallError::Panicked(s) => write!(f, "contract call panicked: {s}"),
83            ContractCallError::Wasm32CrossCallUnavailable { syscall } => {
84                write!(f, "wasm32 cross-call unavailable: {syscall}")
85            }
86            ContractCallError::Other(e) => write!(f, "{e}"),
87        }
88    }
89}
90
91impl std::error::Error for ContractCallError {}
92
93impl From<NeoError> for ContractCallError {
94    fn from(e: NeoError) -> Self {
95        match e {
96            NeoError::Wasm32CrossCallUnavailable { syscall } => {
97                ContractCallError::Wasm32CrossCallUnavailable { syscall }
98            }
99            other => ContractCallError::Other(other),
100        }
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107    use neo_types::{NeoByteString, NeoInteger, NeoValue};
108
109    #[test]
110    fn default_caller_returns_null_for_known_contract() {
111        // The DefaultContractCaller uses NeoVMSyscall::contract_call
112        // which on the host path uses the host-mode dispatcher.
113        // The dispatcher may return either Null (B4 fallback) or
114        // an error; we just assert the call doesn't panic and
115        // either returns Ok or Err.
116        let script_hash = NeoByteString::from_slice(&[1u8; 20]);
117        let call_flags = NeoInteger::new(0x0F);
118        let result = DefaultContractCaller.call_raw(&script_hash, "method", &[], &call_flags);
119        // Acceptable: Ok with any value, or an Err (B4 silent
120        // fallback) — the L6 cross-call upgrade will replace
121        // this with a real Result.
122        let _ = result;
123    }
124
125    #[test]
126    fn call_typed_returns_null_for_non_typed_target() {
127        // call_typed<NeoValue> should always succeed if the
128        // raw call succeeded, regardless of the return value.
129        let script_hash = NeoByteString::from_slice(&[1u8; 20]);
130        let call_flags = NeoInteger::new(0x0F);
131        let _result: Result<NeoValue, _> = call_typed(&script_hash, "method", &[], &call_flags);
132    }
133}