diff --git a/Cargo.toml b/Cargo.toml index f873155c44d93df91b9dcc9811c6b7e151a5088b..5df2f40067df8d564599aa70333c8ff69af1e02d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,6 +45,7 @@ libp2p = { version = "0.52" } subxt = "0.32.0" subxt-signer = { version = "0.32.0", features = ["subxt"]} tracing = "0.1.35" +pjs-rs = "0.1.2" # Zombienet workspace crates: support = { package = "zombienet-support", version = "0.1.0-alpha.0", path = "crates/support" } diff --git a/crates/examples/Cargo.toml b/crates/examples/Cargo.toml index 0dd7f343b211fc1cae31666735c50c75406f156c..9c4d0c1fd8c69d10cb12a7e1311a3956d5705b13 100644 --- a/crates/examples/Cargo.toml +++ b/crates/examples/Cargo.toml @@ -11,3 +11,4 @@ tokio = { workspace = true } futures = { workspace = true } subxt = { workspace = true } tracing-subscriber = "0.3" +serde_json = { workspace = true } diff --git a/crates/examples/examples/pjs.rs b/crates/examples/examples/pjs.rs new file mode 100644 index 0000000000000000000000000000000000000000..1fd7f671c4b3bf3baea66907d1ff029a3eac6f76 --- /dev/null +++ b/crates/examples/examples/pjs.rs @@ -0,0 +1,53 @@ +use futures::stream::StreamExt; +use serde_json::json; +use zombienet_sdk::{NetworkConfigBuilder, NetworkConfigExt}; + +#[tokio::main] +async fn main() -> Result<(), Box<dyn std::error::Error>> { + tracing_subscriber::fmt::init(); + let network = NetworkConfigBuilder::new() + .with_relaychain(|r| { + r.with_chain("rococo-local") + .with_default_command("polkadot") + .with_node(|node| node.with_name("alice")) + .with_node(|node| node.with_name("bob")) + }) + .with_parachain(|p| { + p.with_id(100) + .cumulus_based(true) + .with_collator(|n| n.with_name("collator").with_command("polkadot-parachain")) + }) + .build() + .unwrap() + .spawn_native() + .await?; + + println!("🚀🚀🚀🚀 network deployed"); + + let alice = network.get_node("alice")?; + let client = alice.client::<subxt::PolkadotConfig>().await?; + + // wait 2 blocks + let mut blocks = client.blocks().subscribe_finalized().await?.take(2); + + while let Some(block) = blocks.next().await { + println!("Block #{}", block?.header().number); + } + + // run pjs with code + let query_paras = r#" + const parachains: number[] = (await api.query.paras.parachains()) || []; + return parachains.toJSON() + "#; + + let paras = alice.pjs(query_paras, vec![]).await??; + + println!("parachains registered: {:?}", paras); + + // run pjs with file + let _ = alice + .pjs_file("./examples/pjs_transfer.js", vec![json!("//Alice")]) + .await?; + + Ok(()) +} diff --git a/crates/examples/examples/pjs_transfer.js b/crates/examples/examples/pjs_transfer.js new file mode 100644 index 0000000000000000000000000000000000000000..ecc85f948ccc296780f7920ecdbde9fd949e659d --- /dev/null +++ b/crates/examples/examples/pjs_transfer.js @@ -0,0 +1,32 @@ +const seed = arguments[0]; + +await utilCrypto.cryptoWaitReady(); +const k = new keyring.Keyring({ type: "sr25519" }); +const signer = k.addFromUri(seed); + +// Make a transfer from Alice to Bob and listen to system events. +// You need to be connected to a development chain for this example to work. +const ALICE = '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY'; +const BOB = '5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty'; + +// Get a random number between 1 and 100000 +const randomAmount = Math.floor((Math.random() * 100000) + 1); + +// Create a extrinsic, transferring randomAmount units to Bob. +const transferAllowDeath = api.tx.balances.transferAllowDeath(BOB, randomAmount); + +return new Promise(async (resolve, _reject) => { + // Sign and Send the transaction + const unsub = await transferAllowDeath.signAndSend(signer, ({ events = [], status }) => { + if (status.isInBlock) { + console.log('Successful transfer of ' + randomAmount + ' with hash ' + status.asInBlock.toHex()); + return resolve(); + } else { + console.log('Status of transfer: ' + status.type); + } + + events.forEach(({ phase, event: { data, method, section } }) => { + console.log(phase.toString() + ' : ' + section + '.' + method + ' ' + data.toString()); + }); + }); +}); \ No newline at end of file diff --git a/crates/orchestrator/Cargo.toml b/crates/orchestrator/Cargo.toml index f8aff9d8aff967c61ad4b433941e77e3a08f5fc6..f6c28fac8f823506d99d8091f128597d513ca384 100644 --- a/crates/orchestrator/Cargo.toml +++ b/crates/orchestrator/Cargo.toml @@ -28,6 +28,7 @@ subxt = { workspace = true } subxt-signer = { workspace = true } reqwest = { workspace = true } tracing = { workspace = true } +pjs-rs = { workspace = true } # Zombienet deps configuration = { workspace = true } diff --git a/crates/orchestrator/src/lib.rs b/crates/orchestrator/src/lib.rs index b2d42c3b3dacd0580607fadb2ffa68a552ab4ee9..26f748f271d6cd75c2166e8a5111c92ab2e45d50 100644 --- a/crates/orchestrator/src/lib.rs +++ b/crates/orchestrator/src/lib.rs @@ -459,3 +459,4 @@ pub enum ZombieRole { // re-export pub use network::{AddCollatorOptions, AddNodeOptions}; +pub use shared::types::PjsResult; diff --git a/crates/orchestrator/src/network/node.rs b/crates/orchestrator/src/network/node.rs index 63ffaf36f80f67e7d941e59e5baddccec2043f02..a45439c3d24aadf2c51e8055aea0b653d7a70eeb 100644 --- a/crates/orchestrator/src/network/node.rs +++ b/crates/orchestrator/src/network/node.rs @@ -1,12 +1,14 @@ -use std::{sync::Arc, time::Duration}; +use std::{path::Path, sync::Arc, thread, time::Duration}; use anyhow::anyhow; +use pjs_rs::ReturnValue; use prom_metrics_parser::MetricMap; use provider::DynNode; +use serde_json::json; use subxt::{backend::rpc::RpcClient, OnlineClient}; use tokio::sync::RwLock; -use crate::network_spec::node::NodeSpec; +use crate::{network_spec::node::NodeSpec, shared::types::PjsResult}; #[derive(Clone)] pub struct NetworkNode { @@ -66,6 +68,51 @@ impl NetworkNode { OnlineClient::from_url(&self.ws_uri).await } + /// Execute js/ts code inside [pjs_rs] custom runtime. + /// + /// The code will be run in a wrapper similat to the `javascript` developer tab + /// of polkadot.js apps. The returning value is represented as [PjsResult] enum, to allow + /// to communicate that the execution was succeful but the returning value can be deserialized as [serde_json::Value]. + pub async fn pjs( + &self, + code: impl AsRef<str>, + args: Vec<serde_json::Value>, + ) -> Result<PjsResult, anyhow::Error> { + let code = pjs_build_template(self.ws_uri(), code.as_ref(), args); + let value = match thread::spawn(|| pjs_inner(code)) + .join() + .map_err(|_| anyhow!("[pjs] Thread panicked"))?? + { + ReturnValue::Deserialized(val) => Ok(val), + ReturnValue::CantDeserialize(msg) => Err(msg), + }; + + Ok(value) + } + + /// Execute js/ts file inside [pjs_rs] custom runtime. + /// + /// The content of the file will be run in a wrapper similat to the `javascript` developer tab + /// of polkadot.js apps. The returning value is represented as [PjsResult] enum, to allow + /// to communicate that the execution was succeful but the returning value can be deserialized as [serde_json::Value]. + pub async fn pjs_file( + &self, + file: impl AsRef<Path>, + args: Vec<serde_json::Value>, + ) -> Result<PjsResult, anyhow::Error> { + let content = std::fs::read_to_string(file)?; + let code = pjs_build_template(self.ws_uri(), content.as_ref(), args); + let value = match thread::spawn(|| pjs_inner(code)) + .join() + .map_err(|_| anyhow!("[pjs] Thread panicked"))?? + { + ReturnValue::Deserialized(val) => Ok(val), + ReturnValue::CantDeserialize(msg) => Err(msg), + }; + + Ok(value) + } + /// Resume the node, this is implemented by resuming the /// actual process (e.g polkadot) with sendig `SIGCONT` signal pub async fn resume(&self) -> Result<(), anyhow::Error> { @@ -152,3 +199,31 @@ impl std::fmt::Debug for NetworkNode { .finish() } } + +// Helper methods + +fn pjs_build_template(ws_uri: &str, content: &str, args: Vec<serde_json::Value>) -> String { + format!( + r#" + const {{ util, utilCrypto, keyring, types }} = pjs; + ( async () => {{ + const api = await pjs.api.ApiPromise.create({{ provider: new pjs.api.WsProvider('{}') }}); + const _run = async (api, hashing, keyring, types, util, arguments) => {{ + {} + }}; + return await _run(api, utilCrypto, keyring, types, util, {}); + }})() + "#, + ws_uri, + content, + json!(args) + ) +} + +// Since pjs-rs run a custom javascript runtime (using deno_core) we need to +// execute in an isolated thread. +#[tokio::main(flavor = "current_thread")] +async fn pjs_inner(code: String) -> Result<ReturnValue, anyhow::Error> { + // Arguments are already encoded in the code built from the template. + pjs_rs::run_ts_code(code, None).await +} diff --git a/crates/orchestrator/src/shared/types.rs b/crates/orchestrator/src/shared/types.rs index a0bc2b367175a05fa4b7bd1a7b3c57b6ebf24d93..015170f90b4584b4686d13b8d38f725002a6b24f 100644 --- a/crates/orchestrator/src/shared/types.rs +++ b/crates/orchestrator/src/shared/types.rs @@ -75,3 +75,12 @@ pub struct ParachainGenesisArgs { pub validation_code: String, pub parachain: bool, } + +/// pjs-rs success [Result] type +/// +/// Represent the possible states returned from a succefully call to pjs-rs +/// +/// Ok(value) -> Deserialized return value into a [serde_json::Value] +/// Err(msg) -> Execution of the script finish Ok, but the returned value +/// can't be deserialize into a [serde_json::Value] +pub type PjsResult = Result<serde_json::Value, String>; diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs index 867e483f84195ee2d80085c429573ab154ec0ae1..828a00ed761c6eea61b6ac467a9b8a8940dd78cd 100644 --- a/crates/sdk/src/lib.rs +++ b/crates/sdk/src/lib.rs @@ -2,6 +2,7 @@ use async_trait::async_trait; pub use configuration::{NetworkConfig, NetworkConfigBuilder, RegistrationStrategy}; pub use orchestrator::{ errors::OrchestratorError, network::Network, AddCollatorOptions, AddNodeOptions, Orchestrator, + PjsResult, }; use provider::NativeProvider; use support::{fs::local::LocalFileSystem, process::os::OsProcessManager};