Skip to content
Snippets Groups Projects
Unverified Commit 76a489ea authored by Javier Viola's avatar Javier Viola Committed by GitHub
Browse files

feat: Add `pjs-rs` calls for `Network::Node` (#138)

- Add `pjs-rs` support and mimic the
[javascript](https://polkadot.js.org/apps/#/js) tab from polkadot.js
apps/
- Allow to use an external file or just an string with the `js/ts` code,

TODO: We should implement integration tests for this feature.
parent 07d86d08
Branches
No related merge requests found
......@@ -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" }
......
......@@ -11,3 +11,4 @@ tokio = { workspace = true }
futures = { workspace = true }
subxt = { workspace = true }
tracing-subscriber = "0.3"
serde_json = { workspace = true }
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(())
}
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
......@@ -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 }
......
......@@ -459,3 +459,4 @@ pub enum ZombieRole {
// re-export
pub use network::{AddCollatorOptions, AddNodeOptions};
pub use shared::types::PjsResult;
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
}
......@@ -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>;
......@@ -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};
......
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment