1use std::fs;
2use std::path::Path;
3use std::rc::Rc;
4use std::sync::Arc;
5
6use deno_ast::MediaType;
7use deno_ast::ParseParams;
8use deno_core::error::AnyError;
9use deno_core::serde_json;
10use deno_core::serde_json::json;
11use deno_core::serde_v8;
12use deno_core::url;
13use deno_core::v8;
14use deno_core::JsRuntime;
15
16mod perms;
17use perms::ZombiePermissions;
18
19mod ext;
24use ext::pjs_extension;
25
26type DynLoader = Rc<dyn deno_core::ModuleLoader + 'static>;
27
28#[derive(Debug, PartialEq)]
29pub enum ReturnValue {
32 Deserialized(serde_json::Value),
33 CantDeserialize(String),
34}
35
36pub fn create_runtime_with_loader(loader: Option<DynLoader>) -> JsRuntime {
40 deno_core::JsRuntime::new(deno_core::RuntimeOptions {
41 module_loader: if let Some(loader) = loader {
42 Some(loader)
43 } else {
44 Some(Rc::new(deno_core::NoopModuleLoader))
45 },
46 startup_snapshot: None,
47 extensions: vec![
48 deno_console::deno_console::init_ops_and_esm(),
49 deno_webidl::deno_webidl::init_ops_and_esm(),
50 deno_url::deno_url::init_ops_and_esm(),
51 deno_web::deno_web::init_ops_and_esm::<ZombiePermissions>(
52 Arc::new(deno_web::BlobStore::default()),
53 None,
54 ),
55 deno_crypto::deno_crypto::init_ops_and_esm(None),
56 deno_fetch::deno_fetch::init_ops_and_esm::<ZombiePermissions>(deno_fetch::Options {
57 user_agent: "zombienet-agent".to_string(),
58 ..Default::default()
59 }),
60 deno_net::deno_net::init_ops_and_esm::<ZombiePermissions>(None, None),
61 deno_websocket::deno_websocket::init_ops_and_esm::<ZombiePermissions>(
62 "zombienet-agent".to_string(),
63 None,
64 None,
65 ),
66 pjs_extension::init_ops_and_esm(),
67 ],
68 ..Default::default()
69 })
70}
71
72pub async fn run_file(
99 file_path: impl AsRef<Path>,
100 json_args: Option<Vec<serde_json::Value>>,
101) -> Result<ReturnValue, AnyError> {
102 let code_content = get_code(file_path).await?;
103 run_code(code_content, json_args, false).await
104}
105
106pub async fn run_file_with_wrap(
144 file_path: impl AsRef<Path>,
145 json_args: Option<Vec<serde_json::Value>>,
146) -> Result<ReturnValue, AnyError> {
147 let code_content = get_code(file_path).await?;
148 run_code(&code_content, json_args, true).await
149}
150
151pub async fn run_js_code(
152 code_content: impl Into<String>,
153 json_args: Option<Vec<serde_json::Value>>,
154) -> Result<ReturnValue, AnyError> {
155 run_code(code_content, json_args, false).await
156}
157
158pub async fn run_ts_code(
159 code_content: impl Into<String>,
160 json_args: Option<Vec<serde_json::Value>>,
161) -> Result<ReturnValue, AnyError> {
162 let transpiled = transpile(code_content)?;
163 run_code(transpiled, json_args, false).await
164}
165
166fn transpile(code: impl Into<String>) -> Result<String, AnyError> {
167 let parsed = deno_ast::parse_module(ParseParams {
168 specifier: url::Url::parse("file:///inner")?,
169 text: Arc::from(code.into()),
170 media_type: MediaType::TypeScript,
171 capture_tokens: false,
172 scope_analysis: false,
173 maybe_syntax: None,
174 })?;
175
176 let transpiled = parsed.transpile(
177 &Default::default(),
178 &Default::default(),
179 &Default::default(),
180 )?;
181 Ok(transpiled.into_source().text)
182}
183async fn get_code(file_path: impl AsRef<Path>) -> Result<String, AnyError> {
184 let content = fs::read_to_string(file_path.as_ref())?;
185
186 let code_content = if let MediaType::TypeScript = MediaType::from_path(file_path.as_ref()) {
188 transpile(&content)?
189 } else {
190 content
191 };
192
193 Ok(code_content)
194}
195
196async fn run_code(
197 code_content: impl Into<String>,
198 json_args: Option<Vec<serde_json::Value>>,
199 use_wrapper: bool,
200) -> Result<ReturnValue, AnyError> {
201 let code_content = code_content.into();
202 let bundle_util = include_str!("js/bundle-polkadot-util.js");
203 let bundle_util_crypto = include_str!("js/bundle-polkadot-util-crypto.js");
204 let bundle_keyring = include_str!("js/bundle-polkadot-keyring.js");
205 let bundle_types = include_str!("js/bundle-polkadot-types.js");
206 let bundle_api = include_str!("js/bundle-polkadot-api.js");
207 let code = if use_wrapper {
209 format!(
210 r#"
211 const {{ ApiPromise, WsProvider }} = pjs.api;
212 const {{ util, utilCrypto, keyring, types }} = pjs;
213 (async (arguments, ApiPromise, WsProvider, util, utilCrypto, keyring, types) => {{
214 {}
215 }})({}, ApiPromise, WsProvider, util, utilCrypto, keyring, types)"#,
216 &code_content,
217 json!(json_args.unwrap_or_default())
218 )
219 } else {
220 format!(
221 r#"
222 pjs.arguments = {};
223 {}
224 "#,
225 json!(json_args.unwrap_or_default()),
226 &code_content
227 )
228 };
229
230 log::trace!("code: \n{}", code);
231
232 let mut js_runtime = create_runtime_with_loader(None);
233 let with_bundle = format!(
234 "
235 {}
236 {}
237 {}
238 {}
239 {}
240
241 let pjs = {{
242 util: polkadotUtil,
243 utilCrypto: polkadotUtilCrypto,
244 keyring: polkadotKeyring,
245 types: polkadotTypes,
246 api: polkadotApi,
247 }};
248
249 {}
250 ",
251 bundle_util, bundle_util_crypto, bundle_keyring, bundle_types, bundle_api, code
252 );
253 log::trace!("full code: \n{}", with_bundle);
254 execute_script(&mut js_runtime, &with_bundle).await
255}
256async fn execute_script(
257 js_runtime: &mut JsRuntime,
258 code: impl Into<String>,
259) -> Result<ReturnValue, AnyError> {
260 let executed = js_runtime.execute_script("name", deno_core::FastString::from(code.into()))?;
262 let resolve = js_runtime.resolve(executed);
263 let resolved = js_runtime
264 .with_event_loop_promise(resolve, deno_core::PollEventLoopOptions::default())
265 .await;
266 match resolved {
267 Ok(global) => {
268 let scope = &mut js_runtime.handle_scope();
269 let local = v8::Local::new(scope, global);
270 let deserialized_value = serde_v8::from_v8::<serde_json::Value>(scope, local);
273
274 let resp = match deserialized_value {
275 Ok(value) => {
276 log::debug!("{:#?}", value);
277 ReturnValue::Deserialized(value)
278 }
279 Err(err) => {
280 log::warn!("{}", format!("Cannot deserialize value: {:?}", err));
281 ReturnValue::CantDeserialize(err.to_string())
282 }
283 };
284
285 Ok(resp)
286 }
287 Err(err) => {
288 log::error!("{}", format!("Evaling error: {:?}", err));
289 Err(err)
290 }
291 }
292}
293
294#[cfg(test)]
295mod tests {
296 use super::*;
297
298 #[tokio::test]
299 async fn query_parachains_works() {
300 let resp = run_file_with_wrap("./testing/query_parachains.ts", None)
301 .await
302 .unwrap();
303 if let ReturnValue::Deserialized(value) = resp {
304 let first_para_id = value.as_array().unwrap().first().unwrap().as_u64().unwrap();
305 assert_eq!(first_para_id, 1000_u64);
306 }
307 }
308
309 #[tokio::test]
310 async fn consts_works() {
311 let resp = run_file("./testing/epoch_duration_rococo.js", None)
312 .await
313 .unwrap();
314
315 println!("{:#?}", resp);
316 assert!(matches!(resp, ReturnValue::Deserialized { .. }));
317 if let ReturnValue::Deserialized(value) = resp {
318 assert_eq!(value, json!(600));
319 }
320 }
321
322 #[tokio::test]
323 async fn query_parachains_without_wrap_works() {
324 let resp = run_file("./testing/query_parachains_no_wrap.ts", None)
325 .await
326 .unwrap();
327 assert!(matches!(resp, ReturnValue::Deserialized { .. }));
328 if let ReturnValue::Deserialized(value) = resp {
329 println!("{:?}", value);
330 let first_para_id = value.as_array().unwrap().first().unwrap().as_u64().unwrap();
331 assert_eq!(first_para_id, 1000_u64);
332 }
333 }
334
335 #[tokio::test]
336 async fn query_parachains_from_ts_code_works() {
337 let ts_code = r#"
338 (async () => {
339 const api = await pjs.api.ApiPromise.create({ provider: new pjs.api.WsProvider('wss://rpc.polkadot.io') });
340 const parachains: number[] = (await api.query.paras.parachains()) || [];
341
342 return parachains.toJSON();
343 })();
344 "#;
345 let resp = run_ts_code(ts_code, None).await.unwrap();
346 assert!(matches!(resp, ReturnValue::Deserialized { .. }));
347 if let ReturnValue::Deserialized(value) = resp {
348 let first_para_id = value.as_array().unwrap().first().unwrap().as_u64().unwrap();
349 assert_eq!(first_para_id, 1000_u64);
350 }
351 }
352
353 #[tokio::test]
354 async fn query_parachains_from_js_code_works() {
355 let ts_code = r#"
356 (async () => {
357 const api = await pjs.api.ApiPromise.create({ provider: new pjs.api.WsProvider('wss://rpc.polkadot.io') });
358 const parachains = (await api.query.paras.parachains()) || [];
359
360 return parachains.toJSON();
361 })();
362 "#;
363 let resp = run_ts_code(ts_code, None).await.unwrap();
364 assert!(matches!(resp, ReturnValue::Deserialized { .. }));
365 if let ReturnValue::Deserialized(value) = resp {
366 let first_para_id = value.as_array().unwrap().first().unwrap().as_u64().unwrap();
367 assert_eq!(first_para_id, 1000_u64);
368 }
369 }
370
371 #[tokio::test]
372 async fn query_historic_data_rococo_works() {
373 run_file_with_wrap("./testing/query_historic_data.js", None)
374 .await
375 .unwrap();
376 }
377
378 #[tokio::test]
379 async fn query_chain_state_info_rococo_works() {
380 run_file_with_wrap(
381 "./testing/get_chain_state_info.js",
382 Some(vec![json!("wss://paseo-rpc.dwellir.com")]),
383 )
384 .await
385 .unwrap();
386 }
387
388 #[tokio::test]
389 async fn listen_new_head_works() {
390 let resp = run_file_with_wrap(
391 "./testing/rpc_listen_new_head.js",
392 Some(vec![json!("wss://paseo-rpc.dwellir.com")]),
393 )
394 .await
395 .unwrap();
396 assert_eq!(resp, ReturnValue::Deserialized(json!(5)));
397 }
398
399 #[tokio::test]
400 async fn transfer_works() {
401 let args = vec![json!("wss://paseo-rpc.dwellir.com"), json!("//Alice")];
402 let resp = run_file_with_wrap("./testing/transfer.js", Some(args.clone()))
403 .await
404 .unwrap();
405
406 assert!(matches!(resp, ReturnValue::Deserialized { .. }));
407
408 if let ReturnValue::Deserialized(value) = resp {
409 let amount = value.as_u64().unwrap();
410 println!("Returning {amount:?} to Bob");
411 let args = [args, vec![json!(amount)]].concat();
412 run_file_with_wrap("./testing/transfer.js", Some(args))
413 .await
414 .unwrap();
415 }
416 }
417}