pjs_rs/
lib.rs

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
19// Not used anymore
20// mod cert;
21// use cert::ValueRootCertStoreProvider;
22
23mod ext;
24use ext::pjs_extension;
25
26type DynLoader = Rc<dyn deno_core::ModuleLoader + 'static>;
27
28#[derive(Debug, PartialEq)]
29/// Js script return value, mapping types that can be deserialized as [serde_json::Value] or
30/// not, for the latest an string is returned witht the error message.
31pub enum ReturnValue {
32    Deserialized(serde_json::Value),
33    CantDeserialize(String),
34}
35
36/// Create a new runtime with the pjs extension built-in.
37/// Allow to pass a [ModuleLoader](deno_core::ModuleLoader) to use, by default
38/// [NoopModuleLoader](deno_core::NoopModuleLoader) is used.
39pub fn create_runtime_with_loader(loader: Option<DynLoader>) -> JsRuntime {
40    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_telemetry::deno_telemetry::init_ops_and_esm(),
49            deno_console::deno_console::init_ops_and_esm(),
50            deno_webidl::deno_webidl::init_ops_and_esm(),
51            deno_url::deno_url::init_ops_and_esm(),
52            deno_web::deno_web::init_ops_and_esm::<ZombiePermissions>(
53                Arc::new(deno_web::BlobStore::default()),
54                None,
55            ),
56            deno_crypto::deno_crypto::init_ops_and_esm(None),
57            deno_fetch::deno_fetch::init_ops_and_esm::<ZombiePermissions>(deno_fetch::Options {
58                user_agent: "zombienet-agent".to_string(),
59                ..Default::default()
60            }),
61            deno_net::deno_net::init_ops_and_esm::<ZombiePermissions>(None, None),
62            deno_websocket::deno_websocket::init_ops_and_esm::<ZombiePermissions>(
63                "zombienet-agent".to_string(),
64                None,
65                None,
66            ),
67            pjs_extension::init_ops_and_esm(),
68        ],
69        extension_transpiler: Some(Rc::new(|specifier, source| {
70            deno_runtime::transpile::maybe_transpile_source(specifier, source)
71        })),
72        ..Default::default()
73    })
74}
75
76/// Run a js/ts code from file in an isolated runtime with polkadotjs bundles embedded
77///
78/// The code runs without any wrapping clousure, and the arguments are binded to the `pjs` Object,
79/// available as `pjs.arguments`. If you want to to use top level `await` you will need to wrap your
80/// code like we do in [run_file_with_wrap].
81///
82/// # Example
83/// ## Javascript file example
84/// ```javascript
85/// return pjs.api.consts.babe.epochDuration;
86/// ```
87/// NOTE: To return a value you need to **explicit** call `return <value>`
88///
89///
90///
91/// ## Executing:
92/// ```rust
93/// # use pjs_rs::run_file;
94/// # use deno_core::error::AnyError;
95/// # async fn example() -> Result<(), AnyError> {
96/// let resp = run_file("./testing/epoch_duration_rococo.js", None).await?;
97/// # Ok(())
98/// # }
99/// ```
100///
101///
102pub async fn run_file(
103    file_path: impl AsRef<Path>,
104    json_args: Option<Vec<serde_json::Value>>,
105) -> Result<ReturnValue, AnyError> {
106    let code_content = get_code(file_path).await?;
107    run_code(code_content, json_args, false).await
108}
109
110/// Run a js/ts code from file in an isolated runtime with polkadotjs bundles embedded
111///
112/// The code runs in a closure where the `json_args` are passed as `arguments` array
113/// and the polkadotjs modules (util, utilCrypto, keyring, types) are exposed from the global `pjs` Object.
114/// `ApiPromise` and `WsProvider` are also availables to easy access.
115///
116/// All code is wrapped within an async closure,
117/// allowing access to api methods, keyring, types, util, utilCrypto.
118/// ```javascript
119/// (async (arguments, ApiPromise, WsProvider, util, utilCrypto, keyring, types) => {
120///   ... any user code is executed here ...
121/// })();
122/// ```
123///
124/// # Example
125/// ## Javascript file example
126/// ```javascript
127/// const api = await ApiPromise.create({ provider: new pjs.api.WsProvider('wss://rpc.polkadot.io') });
128/// const parachains = (await api.query.paras.parachains()) || [];
129/// console.log("parachain ids in polkadot:", parachains);
130/// return parachains.toJSON();
131/// ```
132/// NOTE: To return a value you need to **explicit** call `return <value>`
133///
134///
135///
136/// ## Executing:
137/// ```rust
138/// # use pjs_rs::run_file_with_wrap;
139/// # use deno_core::error::AnyError;
140/// # async fn example() -> Result<(), AnyError> {
141/// let resp = run_file_with_wrap("./testing/query_parachains.js", None).await?;
142/// # Ok(())
143/// # }
144/// ```
145///
146///
147pub async fn run_file_with_wrap(
148    file_path: impl AsRef<Path>,
149    json_args: Option<Vec<serde_json::Value>>,
150) -> Result<ReturnValue, AnyError> {
151    let code_content = get_code(file_path).await?;
152    run_code(&code_content, json_args, true).await
153}
154
155pub async fn run_js_code(
156    code_content: impl Into<String>,
157    json_args: Option<Vec<serde_json::Value>>,
158) -> Result<ReturnValue, AnyError> {
159    run_code(code_content, json_args, false).await
160}
161
162pub async fn run_ts_code(
163    code_content: impl Into<String>,
164    json_args: Option<Vec<serde_json::Value>>,
165) -> Result<ReturnValue, AnyError> {
166    let transpiled = transpile(code_content)?;
167    run_code(transpiled, json_args, false).await
168}
169
170fn transpile(code: impl Into<String>) -> Result<String, AnyError> {
171    let parsed = deno_ast::parse_module(ParseParams {
172        specifier: url::Url::parse("file:///inner")?,
173        text: Arc::from(code.into()),
174        media_type: MediaType::TypeScript,
175        capture_tokens: false,
176        scope_analysis: false,
177        maybe_syntax: None,
178    })?;
179
180    let transpiled = parsed.transpile(
181        &Default::default(),
182        &Default::default(),
183        &Default::default(),
184    )?;
185    Ok(transpiled.into_source().text)
186}
187async fn get_code(file_path: impl AsRef<Path>) -> Result<String, AnyError> {
188    let content = fs::read_to_string(file_path.as_ref())?;
189
190    // Check if we need to transpile (e.g .ts file)
191    let code_content = if let MediaType::TypeScript = MediaType::from_path(file_path.as_ref()) {
192        transpile(&content)?
193    } else {
194        content
195    };
196
197    Ok(code_content)
198}
199
200async fn run_code(
201    code_content: impl Into<String>,
202    json_args: Option<Vec<serde_json::Value>>,
203    use_wrapper: bool,
204) -> Result<ReturnValue, AnyError> {
205    let code_content = code_content.into();
206    let bundle_util = include_str!("js/bundle-polkadot-util.js");
207    let bundle_util_crypto = include_str!("js/bundle-polkadot-util-crypto.js");
208    let bundle_keyring = include_str!("js/bundle-polkadot-keyring.js");
209    let bundle_types = include_str!("js/bundle-polkadot-types.js");
210    let bundle_api = include_str!("js/bundle-polkadot-api.js");
211    // Code templates
212    let code = if use_wrapper {
213        format!(
214            r#"
215        const {{ ApiPromise, WsProvider }} = pjs.api;
216        const {{ util, utilCrypto, keyring, types }} = pjs;
217        (async (arguments, ApiPromise, WsProvider, util, utilCrypto, keyring, types) => {{
218            {}
219        }})({}, ApiPromise, WsProvider, util, utilCrypto, keyring, types)"#,
220            &code_content,
221            json!(json_args.unwrap_or_default())
222        )
223    } else {
224        format!(
225            r#"
226            pjs.arguments = {};
227            {}
228            "#,
229            json!(json_args.unwrap_or_default()),
230            &code_content
231        )
232    };
233
234    log::trace!("code: \n{}", code);
235
236    let mut js_runtime = create_runtime_with_loader(None);
237    let with_bundle = format!(
238        "
239    {}
240    {}
241    {}
242    {}
243    {}
244
245    let pjs = {{
246        util: polkadotUtil,
247        utilCrypto: polkadotUtilCrypto,
248        keyring: polkadotKeyring,
249        types: polkadotTypes,
250        api: polkadotApi,
251    }};
252
253    {}
254    ",
255        bundle_util, bundle_util_crypto, bundle_keyring, bundle_types, bundle_api, code
256    );
257    log::trace!("full code: \n{}", with_bundle);
258    execute_script(&mut js_runtime, &with_bundle).await
259}
260async fn execute_script(
261    js_runtime: &mut JsRuntime,
262    code: impl Into<String>,
263) -> Result<ReturnValue, AnyError> {
264    // Execution
265    let executed = js_runtime.execute_script("name", deno_core::FastString::from(code.into()))?;
266    let resolve = js_runtime.resolve(executed);
267    let resolved = js_runtime
268        .with_event_loop_promise(resolve, deno_core::PollEventLoopOptions::default())
269        .await;
270    match resolved {
271        Ok(global) => {
272            let scope = &mut js_runtime.handle_scope();
273            let local = v8::Local::new(scope, global);
274            // Deserialize a `v8` object into a Rust type using `serde_v8`,
275            // in this case deserialize to a JSON `Value`.
276            let deserialized_value = serde_v8::from_v8::<serde_json::Value>(scope, local);
277
278            let resp = match deserialized_value {
279                Ok(value) => {
280                    log::debug!("{:#?}", value);
281                    ReturnValue::Deserialized(value)
282                }
283                Err(err) => {
284                    log::warn!("{}", format!("Cannot deserialize value: {:?}", err));
285                    ReturnValue::CantDeserialize(err.to_string())
286                }
287            };
288
289            Ok(resp)
290        }
291        Err(err) => {
292            log::error!("{}", format!("Evaling error: {:?}", err));
293            Err(err.into())
294        }
295    }
296}
297
298#[cfg(test)]
299mod tests {
300    use super::*;
301
302    #[tokio::test]
303    async fn query_parachains_works() {
304        let resp = run_file_with_wrap("./testing/query_parachains.ts", None)
305            .await
306            .unwrap();
307        if let ReturnValue::Deserialized(value) = resp {
308            let first_para_id = value.as_array().unwrap().first().unwrap().as_u64().unwrap();
309            assert_eq!(first_para_id, 1000_u64);
310        }
311    }
312
313    #[tokio::test]
314    async fn consts_works() {
315        let resp = run_file("./testing/epoch_duration_rococo.js", None)
316            .await
317            .unwrap();
318
319        println!("{:#?}", resp);
320        assert!(matches!(resp, ReturnValue::Deserialized { .. }));
321        if let ReturnValue::Deserialized(value) = resp {
322            assert_eq!(value, json!(600));
323        }
324    }
325
326    #[tokio::test]
327    async fn query_parachains_without_wrap_works() {
328        let resp = run_file("./testing/query_parachains_no_wrap.ts", None)
329            .await
330            .unwrap();
331        assert!(matches!(resp, ReturnValue::Deserialized { .. }));
332        if let ReturnValue::Deserialized(value) = resp {
333            println!("{:?}", value);
334            let first_para_id = value.as_array().unwrap().first().unwrap().as_u64().unwrap();
335            assert_eq!(first_para_id, 1000_u64);
336        }
337    }
338
339    #[tokio::test]
340    async fn query_parachains_from_ts_code_works() {
341        let ts_code = r#"
342        (async () => {
343            const api = await pjs.api.ApiPromise.create({ provider: new pjs.api.WsProvider('wss://rpc.polkadot.io') });
344            const parachains: number[] = (await api.query.paras.parachains()) || [];
345
346            return parachains.toJSON();
347        })();
348        "#;
349        let resp = run_ts_code(ts_code, None).await.unwrap();
350        assert!(matches!(resp, ReturnValue::Deserialized { .. }));
351        if let ReturnValue::Deserialized(value) = resp {
352            let first_para_id = value.as_array().unwrap().first().unwrap().as_u64().unwrap();
353            assert_eq!(first_para_id, 1000_u64);
354        }
355    }
356
357    #[tokio::test]
358    async fn query_parachains_from_js_code_works() {
359        let ts_code = r#"
360        (async () => {
361            const api = await pjs.api.ApiPromise.create({ provider: new pjs.api.WsProvider('wss://rpc.polkadot.io') });
362            const parachains = (await api.query.paras.parachains()) || [];
363
364            return parachains.toJSON();
365        })();
366        "#;
367        let resp = run_ts_code(ts_code, None).await.unwrap();
368        assert!(matches!(resp, ReturnValue::Deserialized { .. }));
369        if let ReturnValue::Deserialized(value) = resp {
370            let first_para_id = value.as_array().unwrap().first().unwrap().as_u64().unwrap();
371            assert_eq!(first_para_id, 1000_u64);
372        }
373    }
374
375    #[tokio::test]
376    async fn query_historic_data_rococo_works() {
377        run_file_with_wrap("./testing/query_historic_data.js", None)
378            .await
379            .unwrap();
380    }
381
382    #[tokio::test]
383    async fn query_chain_state_info_rococo_works() {
384        run_file_with_wrap(
385            "./testing/get_chain_state_info.js",
386            Some(vec![json!("wss://paseo-rpc.dwellir.com")]),
387        )
388        .await
389        .unwrap();
390    }
391
392    #[tokio::test]
393    async fn listen_new_head_works() {
394        let resp = run_file_with_wrap(
395            "./testing/rpc_listen_new_head.js",
396            Some(vec![json!("wss://paseo-rpc.dwellir.com")]),
397        )
398        .await
399        .unwrap();
400        assert_eq!(resp, ReturnValue::Deserialized(json!(5)));
401    }
402
403    #[tokio::test]
404    async fn transfer_works() {
405        let args = vec![json!("wss://paseo-rpc.dwellir.com"), json!("//Alice")];
406        let resp = run_file_with_wrap("./testing/transfer.js", Some(args.clone()))
407            .await
408            .unwrap();
409
410        assert!(matches!(resp, ReturnValue::Deserialized { .. }));
411
412        if let ReturnValue::Deserialized(value) = resp {
413            let amount = value.as_u64().unwrap();
414            println!("Returning {amount:?} to Bob");
415            let args = [args, vec![json!(amount)]].concat();
416            run_file_with_wrap("./testing/transfer.js", Some(args))
417                .await
418                .unwrap();
419        }
420    }
421}