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    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
72/// Run a js/ts code from file in an isolated runtime with polkadotjs bundles embedded
73///
74/// The code runs without any wrapping clousure, and the arguments are binded to the `pjs` Object,
75/// available as `pjs.arguments`. If you want to to use top level `await` you will need to wrap your
76/// code like we do in [run_file_with_wrap].
77///
78/// # Example
79/// ## Javascript file example
80/// ```javascript
81/// return pjs.api.consts.babe.epochDuration;
82/// ```
83/// NOTE: To return a value you need to **explicit** call `return <value>`
84///
85///
86///
87/// ## Executing:
88/// ```rust
89/// # use pjs_rs::run_file;
90/// # use deno_core::error::AnyError;
91/// # async fn example() -> Result<(), AnyError> {
92/// let resp = run_file("./testing/epoch_duration_rococo.js", None).await?;
93/// # Ok(())
94/// # }
95/// ```
96///
97///
98pub 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
106/// Run a js/ts code from file in an isolated runtime with polkadotjs bundles embedded
107///
108/// The code runs in a closure where the `json_args` are passed as `arguments` array
109/// and the polkadotjs modules (util, utilCrypto, keyring, types) are exposed from the global `pjs` Object.
110/// `ApiPromise` and `WsProvider` are also availables to easy access.
111///
112/// All code is wrapped within an async closure,
113/// allowing access to api methods, keyring, types, util, utilCrypto.
114/// ```javascript
115/// (async (arguments, ApiPromise, WsProvider, util, utilCrypto, keyring, types) => {
116///   ... any user code is executed here ...
117/// })();
118/// ```
119///
120/// # Example
121/// ## Javascript file example
122/// ```javascript
123/// const api = await ApiPromise.create({ provider: new pjs.api.WsProvider('wss://rpc.polkadot.io') });
124/// const parachains = (await api.query.paras.parachains()) || [];
125/// console.log("parachain ids in polkadot:", parachains);
126/// return parachains.toJSON();
127/// ```
128/// NOTE: To return a value you need to **explicit** call `return <value>`
129///
130///
131///
132/// ## Executing:
133/// ```rust
134/// # use pjs_rs::run_file_with_wrap;
135/// # use deno_core::error::AnyError;
136/// # async fn example() -> Result<(), AnyError> {
137/// let resp = run_file_with_wrap("./testing/query_parachains.js", None).await?;
138/// # Ok(())
139/// # }
140/// ```
141///
142///
143pub 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    // Check if we need to transpile (e.g .ts file)
187    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    // Code templates
208    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    // Execution
261    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            // Deserialize a `v8` object into a Rust type using `serde_v8`,
271            // in this case deserialize to a JSON `Value`.
272            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}