Skip to main content

yevm_wasm/
lib.rs

1#[cfg(target_arch = "wasm32")]
2mod wasm {
3    use eyre::eyre;
4    use futures::StreamExt;
5    use futures::channel::mpsc;
6    use js_sys::{Function, JsString};
7    use wasm_bindgen::prelude::*;
8    use yevm_base::int;
9    use yevm_core::Int;
10    use yevm_core::cache::Cache;
11    use yevm_core::chain::{Chain, Fetched};
12    use yevm_core::exe::{CallResult, Executor};
13    use yevm_core::state::State;
14    use yevm_core::trace::Trace;
15    use yevm_core::{Call, Tx, call::TxCall, rpc::Rpc};
16
17    #[derive(Debug, thiserror::Error)]
18    #[error("{0}")]
19    pub struct Error(eyre::Report);
20
21    impl From<eyre::Report> for Error {
22        fn from(e: eyre::Report) -> Self {
23            Self(e)
24        }
25    }
26
27    impl From<yevm_core::Error> for Error {
28        fn from(e: yevm_core::Error) -> Self {
29            Self(e.into())
30        }
31    }
32
33    impl From<Error> for JsValue {
34        fn from(e: Error) -> Self {
35            JsValue::from(JsError::new(&e.0.to_string()))
36        }
37    }
38
39    #[wasm_bindgen]
40    pub struct FetchCache(Vec<Fetched>);
41
42    #[wasm_bindgen]
43    pub fn empty() -> FetchCache {
44        FetchCache(vec![])
45    }
46
47    #[wasm_bindgen]
48    pub fn hello(name: JsString) -> JsString {
49        JsString::from(format!("Hello, {name}").as_str())
50    }
51
52    #[wasm_bindgen]
53    pub fn call(json: JsValue) -> Result<JsValue, JsError> {
54        let call: Call = serde_wasm_bindgen::from_value(json)?;
55        Ok(serde_wasm_bindgen::to_value(&call)?)
56    }
57
58    #[wasm_bindgen]
59    pub struct Stream {
60        receiver: mpsc::Receiver<Trace>,
61        tx: Int,
62        rcpt_gas: Int,
63        yevm_gas: Int,
64        rcpt_status: Int,
65        yevm_status: Int,
66        fetches: Vec<Fetched>,
67    }
68
69    #[wasm_bindgen]
70    impl Stream {
71        pub async fn next(&mut self) -> JsValue {
72            release().await;
73            let result = match self.receiver.next().await {
74                Some(step) => {
75                    let value = serde_wasm_bindgen::to_value(&step).unwrap_or(JsValue::NULL);
76                    release().await;
77                    value
78                }
79                None => JsValue::NULL,
80            };
81            release().await;
82            result
83        }
84
85        pub fn fetches(&mut self) -> FetchCache {
86            FetchCache(std::mem::take(&mut self.fetches))
87        }
88
89        pub fn check(&self) -> JsString {
90            let gas_ok = self.yevm_gas == self.rcpt_gas;
91            let status_ok = self.yevm_status == self.rcpt_status;
92            [
93                format!("TX: {}", self.tx),
94                format!(
95                    "gas: yevm={} rcpt={}: OK={}",
96                    self.yevm_gas.as_u64(),
97                    self.rcpt_gas.as_u64(),
98                    gas_ok
99                ),
100                if self.rcpt_status > 2.into() {
101                    format!(
102                        "created: yevm={} rcpt={}: OK={}",
103                        self.yevm_status.to::<20>(),
104                        self.rcpt_status.to::<20>(),
105                        status_ok
106                    )
107                } else {
108                    format!(
109                        "status: yevm={} rcpt={}: OK={}",
110                        self.yevm_status.as_u8(),
111                        self.rcpt_status.as_u8(),
112                        status_ok
113                    )
114                },
115            ]
116            .join("\n")
117            .into()
118        }
119    }
120
121    #[wasm_bindgen]
122    pub async fn pick(url: JsString) -> Result<JsString, Error> {
123        let rpc = Rpc::latest(url.into()).await?;
124        let txs = &rpc.block(rpc.block_number).await?.txs;
125        let len = txs.len();
126        let idx = (js_sys::Math::random() * len as f64) as usize;
127        let hash = txs[idx].tx.hash.to_string();
128        Ok(hash.into())
129    }
130
131    #[wasm_bindgen]
132    pub async fn run(
133        url: JsString,
134        txn: JsString,
135        event_filter: u32,
136        fetches: FetchCache,
137    ) -> Result<Stream, Error> {
138        let mut rpc = Rpc::latest(url.into()).await?;
139        let chain_id = rpc.chain_id().await?;
140        let txn: String = txn.into();
141        let (block, index) = if let Some((left, right)) = txn.split_once(':') {
142            let block = left.trim().parse().map_err(|_| eyre!("invalid block"))?;
143            let index = right.trim().parse().map_err(|_| eyre!("invalid index"))?;
144            (block, index)
145        } else if txn.starts_with("0x") {
146            let hash = int(&txn);
147            let receipt = rpc.receipt(hash).await?;
148            let block = receipt.block_number.as_u64();
149            let index = receipt.transaction_index.as_usize();
150            (block, index)
151        } else {
152            return Err(eyre!("invalid tx selector").into());
153        };
154
155        let (call, tx, head, block_data) = {
156            let block = rpc.block(block).await?;
157            let tx = &block.txs[index];
158            let call = tx.call.clone().into();
159            (call, tx.tx.clone(), block.head.clone(), block)
160        };
161        let hash = tx.hash;
162        rpc.reset(head.number.as_u64() - 1, head.parent_hash);
163
164        let (ytx, yrx) = mpsc::channel(1024 * 1024);
165        let mut cache = Cache::with_sender(ytx, event_filter);
166        cache.set_chain_id(chain_id);
167
168        let FetchCache(fetched) = fetches;
169        if fetched.len() > 0 {
170            cache.prefetched(fetched);
171        } else {
172            cache.fetched.push(Fetched::ChainId(chain_id));
173            cache.fetched.push(Fetched::Block(block_data));
174        }
175
176        let mut exe = Executor::new(call);
177        let result = exe.run(tx, head, &mut cache, &rpc).await?;
178        let _ = cache.sender.take();
179
180        let (yevm_status, yevm_gas) = match result {
181            CallResult::Done {
182                status,
183                ret: _,
184                gas,
185            } => (status, gas.finalized.into()),
186            CallResult::Created { acc, code: _, gas } => (acc.to(), gas.finalized.into()),
187        };
188        let receipt = rpc.receipt(hash).await?;
189        let (rcpt_status, rcpt_gas) = (
190            if let Some(acc) = receipt.contract_address {
191                acc.to()
192            } else {
193                receipt.status
194            },
195            receipt.gas_used,
196        );
197        Ok(Stream {
198            receiver: yrx,
199            tx: hash,
200            rcpt_gas,
201            yevm_gas,
202            rcpt_status,
203            yevm_status,
204            fetches: cache.fetched,
205        })
206    }
207
208    #[wasm_bindgen]
209    pub async fn run_with_block(
210        url: JsString,
211        txn: JsString,
212        event_filter: u32,
213        fetches: FetchCache,
214        on_progress: Function,
215    ) -> Result<Stream, Error> {
216        let mut rpc = Rpc::latest(url.into()).await?;
217        let chain_id = rpc.chain_id().await?;
218        let txn: String = txn.into();
219
220        let (block, index) = if let Some((left, right)) = txn.split_once(':') {
221            let block = left.trim().parse().map_err(|_| eyre!("invalid block"))?;
222            let index = right.trim().parse().map_err(|_| eyre!("invalid index"))?;
223            (block, index)
224        } else if txn.starts_with("0x") {
225            let hash = int(&txn);
226            let receipt = rpc.receipt(hash).await?;
227            let block = receipt.block_number.as_u64();
228            let index = receipt.transaction_index.as_usize();
229            (block, index)
230        } else {
231            return Err(eyre!("invalid tx selector").into());
232        };
233
234        let (call, tx, head, prior_txs, block_data) = {
235            let b = rpc.block(block).await?;
236            let full = &b.txs[index];
237            let prior = b.txs[..index].to_vec();
238            (
239                full.call.clone().into(),
240                full.tx.clone(),
241                b.head.clone(),
242                prior,
243                b,
244            )
245        };
246        let hash = tx.hash;
247        rpc.reset(head.number.as_u64() - 1, head.parent_hash);
248
249        // Pre-execute prior txs: no tracing (filter=0, no sender), state accumulates.
250        let mut cache = Cache::new();
251        cache.set_chain_id(chain_id);
252
253        let FetchCache(fetched) = fetches;
254        if fetched.len() > 0 {
255            cache.prefetched(fetched);
256        } else {
257            cache.fetched.push(Fetched::ChainId(chain_id));
258            cache.fetched.push(Fetched::Block(block_data));
259        }
260
261        let total = prior_txs.len();
262        for (i, prior) in prior_txs.into_iter().enumerate() {
263            let prior_call: Call = prior.call.into();
264            let mut exe = Executor::new(prior_call);
265            let _ = exe.run(prior.tx, head.clone(), &mut cache, &rpc).await;
266            cache.reset();
267            release().await;
268            let _ = on_progress.call2(&JsValue::NULL, &JsValue::from(i + 1), &JsValue::from(total));
269        }
270
271        // Wire up tracing for the target tx, reuse accumulated state.
272        let (ytx, yrx) = mpsc::channel(1024 * 1024);
273        cache.sender = Some(ytx);
274        cache.filter = event_filter;
275        cache.reset(); // reset warm sets / transient for the target tx
276
277        let mut exe = Executor::new(call);
278        let result = exe.run(tx, head, &mut cache, &rpc).await?;
279        let _ = cache.sender.take();
280
281        let (yevm_status, yevm_gas) = match result {
282            CallResult::Done {
283                status,
284                ret: _,
285                gas,
286            } => (status, gas.finalized.into()),
287            CallResult::Created { acc, code: _, gas } => (acc.to(), gas.finalized.into()),
288        };
289        let receipt = rpc.receipt(hash).await?;
290        let (rcpt_status, rcpt_gas) = (
291            if let Some(acc) = receipt.contract_address {
292                acc.to()
293            } else {
294                receipt.status
295            },
296            receipt.gas_used,
297        );
298        Ok(Stream {
299            receiver: yrx,
300            tx: hash,
301            rcpt_gas,
302            yevm_gas,
303            rcpt_status,
304            yevm_status,
305            fetches: cache.fetched,
306        })
307    }
308
309    #[wasm_bindgen]
310    pub fn analyse(traces_js: JsValue) -> Result<JsValue, JsError> {
311        let traces: Vec<yevm_core::trace::Trace> = serde_wasm_bindgen::from_value(traces_js)?;
312        let alerts = yevm_lens::analyse(&traces);
313        Ok(serde_wasm_bindgen::to_value(&alerts)?)
314    }
315
316    #[wasm_bindgen]
317    pub async fn simulate(
318        url: JsString,
319        call_json: JsValue,
320        event_filter: u32,
321    ) -> Result<Stream, Error> {
322        let mut rpc = Rpc::latest(url.into()).await?;
323        let chain_id = rpc.chain_id().await?;
324        let head = rpc.block(rpc.block_number).await?.head;
325        rpc.reset(head.number.as_u64(), head.hash);
326
327        let call: Call = serde_wasm_bindgen::from_value::<TxCall>(call_json)
328            .map_err(|e| eyre!("{e}"))?
329            .into();
330
331        // Synthetic legacy tx: gas_price = base_fee satisfies the >= base_fee check.
332        // chain_id left at zero so exe.rs fills it from cache.
333        let tx = Tx {
334            chain_id: Default::default(),
335            nonce: Int::ZERO,
336            gas_price: head.base_fee,
337            max_fee_per_gas: Int::ZERO,
338            max_priority_fee_per_gas: Int::ZERO,
339            access_list: vec![],
340            authorization_list: vec![],
341            blob_versioned_hashes: vec![],
342            max_fee_per_blob_gas: None,
343            hash: Int::ZERO,
344            index: Int::ZERO,
345        };
346
347        let (ytx, yrx) = mpsc::channel(1024 * 1024);
348        let mut cache = Cache::with_sender(ytx, event_filter);
349        cache.set_chain_id(chain_id);
350
351        let mut exe = Executor::new(call);
352        let result = exe.run(tx, head, &mut cache, &rpc).await?;
353        let _ = cache.sender.take();
354
355        let (yevm_status, yevm_gas) = match result {
356            CallResult::Done {
357                status,
358                ret: _,
359                gas,
360            } => (status, gas.finalized.into()),
361            CallResult::Created { acc, code: _, gas } => (acc.to(), gas.finalized.into()),
362        };
363
364        Ok(Stream {
365            receiver: yrx,
366            tx: Int::ZERO,
367            rcpt_gas: yevm_gas,
368            yevm_gas,
369            rcpt_status: yevm_status,
370            yevm_status,
371            fetches: cache.fetched,
372        })
373    }
374
375    async fn release() {
376        let promise = js_sys::Promise::resolve(&JsValue::NULL);
377        let _ = wasm_bindgen_futures::JsFuture::from(promise).await;
378    }
379}
380
381#[cfg(target_arch = "wasm32")]
382pub use wasm::*;
383
384/*
385TODO:
386
387Call target: bring your own env (bytecode, call, storage, etc) + state overrides.
388(Fully reproducible hermetic execution environment, demo/PoC/etc)
389
390*/