1#![warn(missing_docs)]
19
20use serde::{Deserialize, Serialize};
21
22use mfm_machine::errors::{ErrorCategory, ErrorInfo, IoError};
23use mfm_machine::hashing::{artifact_id_for_json, CanonicalJsonError};
24use mfm_machine::ids::{ErrorCode, FactKey, StateId};
25use mfm_machine::io::{IoCall, IoProvider, IoResult};
26
27pub const NAMESPACE_EVM: &str = "evm";
29
30#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
32pub struct JsonRpcCall {
33 pub method: String,
35 pub params: serde_json::Value,
37}
38
39impl JsonRpcCall {
40 pub fn new(method: impl Into<String>, params: serde_json::Value) -> Self {
42 Self {
43 method: method.into(),
44 params,
45 }
46 }
47}
48
49#[derive(Clone, Debug, PartialEq, Eq)]
51pub enum FactKeyDerivationError {
52 NotCanonical(CanonicalJsonError),
54}
55
56impl std::fmt::Display for FactKeyDerivationError {
57 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58 match self {
59 FactKeyDerivationError::NotCanonical(e) => write!(f, "request not canonical: {e}"),
60 }
61 }
62}
63
64impl std::error::Error for FactKeyDerivationError {}
65
66pub fn fact_key_for_jsonrpc_call(
68 state_id: &StateId,
69 call: &JsonRpcCall,
70) -> Result<FactKey, FactKeyDerivationError> {
71 let request =
72 serde_json::to_value(call).expect("JsonRpcCall must be serializable to serde_json::Value");
73 let req_id = artifact_id_for_json(&request).map_err(FactKeyDerivationError::NotCanonical)?;
74 Ok(FactKey(format!(
75 "mfm:evm|state:{}|req:{}",
76 state_id.as_str(),
77 req_id.0
78 )))
79}
80
81pub fn evm_io_call(call: JsonRpcCall, fact_key: FactKey) -> IoCall {
83 let request =
84 serde_json::to_value(call).expect("JsonRpcCall must be serializable to serde_json::Value");
85 IoCall {
86 namespace: NAMESPACE_EVM.to_string(),
87 request,
88 fact_key: Some(fact_key),
89 }
90}
91
92#[derive(Clone, Debug, PartialEq, Eq)]
94pub enum ParseHexError {
95 MissingPrefix,
97 Empty,
99 Invalid,
101 Overflow,
103}
104
105impl std::fmt::Display for ParseHexError {
106 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
107 let s = match self {
108 ParseHexError::MissingPrefix => "missing 0x prefix",
109 ParseHexError::Empty => "empty hex string",
110 ParseHexError::Invalid => "invalid hex",
111 ParseHexError::Overflow => "hex value overflowed u64",
112 };
113 write!(f, "{s}")
114 }
115}
116
117impl std::error::Error for ParseHexError {}
118
119pub fn parse_u64_hex(s: &str) -> Result<u64, ParseHexError> {
121 let Some(rest) = s.strip_prefix("0x") else {
122 return Err(ParseHexError::MissingPrefix);
123 };
124 if rest.is_empty() {
125 return Err(ParseHexError::Empty);
126 }
127 if rest.len() > 16 {
128 return Err(ParseHexError::Overflow);
129 }
130 u64::from_str_radix(rest, 16).map_err(|_| ParseHexError::Invalid)
131}
132
133pub fn parse_u64_hex_value(v: &serde_json::Value) -> Result<u64, ParseHexError> {
135 let Some(s) = v.as_str() else {
136 return Err(ParseHexError::Invalid);
137 };
138 parse_u64_hex(s)
139}
140
141fn info(code: &'static str, category: ErrorCategory, message: &'static str) -> ErrorInfo {
142 ErrorInfo {
143 code: ErrorCode(code.to_string()),
144 category,
145 retryable: false,
146 message: message.to_string(),
147 details: None,
148 }
149}
150
151fn io_other(code: &'static str, category: ErrorCategory, message: &'static str) -> IoError {
152 IoError::Other(info(code, category, message))
153}
154
155pub struct EvmIoClient<'a> {
159 state_id: StateId,
160 io: &'a mut dyn IoProvider,
161}
162
163impl<'a> EvmIoClient<'a> {
164 pub fn new(state_id: StateId, io: &'a mut dyn IoProvider) -> Self {
166 Self { state_id, io }
167 }
168
169 pub fn state_id(&self) -> &StateId {
171 &self.state_id
172 }
173
174 pub fn io_mut(&mut self) -> &mut dyn IoProvider {
176 self.io
177 }
178
179 pub async fn call(&mut self, call: JsonRpcCall) -> Result<IoResult, IoError> {
181 let key = fact_key_for_jsonrpc_call(&self.state_id, &call).map_err(|e| match e {
182 FactKeyDerivationError::NotCanonical(CanonicalJsonError::FloatNotAllowed) => io_other(
183 "evm_request_not_canonical",
184 ErrorCategory::ParsingInput,
185 "evm request was not canonical-json-hashable (floats are forbidden)",
186 ),
187 FactKeyDerivationError::NotCanonical(CanonicalJsonError::SecretsNotAllowed) => {
188 io_other(
189 "secrets_detected",
190 ErrorCategory::Unknown,
191 "evm request contained secrets (policy forbids persisting secrets)",
192 )
193 }
194 })?;
195
196 self.io.call(evm_io_call(call, key)).await
197 }
198
199 pub async fn chain_id_u64(&mut self) -> Result<u64, IoError> {
201 let res = self
202 .call(JsonRpcCall::new("eth_chainId", serde_json::json!([])))
203 .await?;
204 parse_u64_hex_value(&res.response).map_err(|_| {
205 io_other(
206 "evm_response_invalid",
207 ErrorCategory::ParsingInput,
208 "evm response was not a hex u64",
209 )
210 })
211 }
212
213 pub async fn block_number_u64(&mut self) -> Result<u64, IoError> {
215 let res = self
216 .call(JsonRpcCall::new("eth_blockNumber", serde_json::json!([])))
217 .await?;
218 parse_u64_hex_value(&res.response).map_err(|_| {
219 io_other(
220 "evm_response_invalid",
221 ErrorCategory::ParsingInput,
222 "evm response was not a hex u64",
223 )
224 })
225 }
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231 use async_trait::async_trait;
232 use mfm_machine::ids::{ArtifactId, FactKey};
233
234 #[test]
235 fn fact_key_is_stable_for_same_call() {
236 let sid = StateId::must_new("m.main.chain_id".to_string());
237 let call = JsonRpcCall::new("eth_chainId", serde_json::json!([]));
238
239 let k1 = fact_key_for_jsonrpc_call(&sid, &call).expect("key");
240 let k2 = fact_key_for_jsonrpc_call(&sid, &call).expect("key");
241 assert_eq!(k1, k2);
242 }
243
244 #[test]
245 fn parse_u64_hex_basic() {
246 assert_eq!(parse_u64_hex("0x0").unwrap(), 0);
247 assert_eq!(parse_u64_hex("0x1").unwrap(), 1);
248 assert_eq!(parse_u64_hex("0x7b").unwrap(), 123);
249 }
250
251 #[test]
252 fn parse_u64_hex_rejects_bad_inputs() {
253 assert_eq!(
254 parse_u64_hex("1").unwrap_err(),
255 ParseHexError::MissingPrefix
256 );
257 assert_eq!(parse_u64_hex("0x").unwrap_err(), ParseHexError::Empty);
258 assert_eq!(parse_u64_hex("0xzz").unwrap_err(), ParseHexError::Invalid);
259 assert_eq!(
260 parse_u64_hex("0x0123456789abcdef0").unwrap_err(),
261 ParseHexError::Overflow
262 );
263 }
264
265 struct FixedIo {
266 response: serde_json::Value,
267 }
268
269 #[async_trait]
270 impl IoProvider for FixedIo {
271 async fn call(&mut self, _call: IoCall) -> Result<IoResult, IoError> {
272 Ok(IoResult {
273 response: self.response.clone(),
274 recorded_payload_id: None,
275 })
276 }
277
278 async fn record_value(
279 &mut self,
280 _key: FactKey,
281 _value: serde_json::Value,
282 ) -> Result<ArtifactId, IoError> {
283 Ok(ArtifactId("0".repeat(64)))
284 }
285
286 async fn get_recorded_fact(
287 &mut self,
288 _key: &FactKey,
289 ) -> Result<Option<ArtifactId>, IoError> {
290 Ok(None)
291 }
292
293 async fn now_millis(&mut self) -> Result<u64, IoError> {
294 Ok(0)
295 }
296
297 async fn random_bytes(&mut self, _n: usize) -> Result<Vec<u8>, IoError> {
298 Ok(Vec::new())
299 }
300 }
301
302 #[tokio::test]
303 async fn evm_client_parses_chain_id() {
304 let mut io = FixedIo {
305 response: serde_json::json!("0x1"),
306 };
307 let mut client =
308 EvmIoClient::new(StateId::must_new("m.main.chain_id".to_string()), &mut io);
309 let id = client.chain_id_u64().await.expect("chain id");
310 assert_eq!(id, 1);
311 }
312}