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}