From cc66f5cd408ae9f4d8f2c651c75164707ae425f6 Mon Sep 17 00:00:00 2001
From: PG Herveou <pgherveou@gmail.com>
Date: Tue, 24 Oct 2023 14:24:19 +0200
Subject: [PATCH] Expose subxt onclineclient / rpc (#125)

just wanted to throw a quick implementation that exposes the
OnclineClient on the node.
This would make the sdk super useful to enable cross chain e2e tests in
ink!

---------

Co-authored-by: Javier Viola <pepoviola@gmail.com>
---
 crates/configuration/src/global_settings.rs   |  8 +++----
 crates/configuration/src/network.rs           | 10 ++++----
 crates/configuration/src/parachain.rs         | 20 ++++++++--------
 crates/configuration/src/relaychain.rs        | 14 +++++------
 crates/configuration/src/shared/node.rs       | 24 +++++++++----------
 crates/configuration/src/shared/resources.rs  | 10 ++++----
 crates/examples/Cargo.toml                    |  2 ++
 .../examples/simple_network_example.rs        | 18 ++++++++++----
 crates/orchestrator/src/network/node.rs       | 13 ++++++++++
 crates/orchestrator/src/network_spec.rs       |  4 ++--
 crates/orchestrator/src/spawner.rs            | 14 +++++------
 11 files changed, 80 insertions(+), 57 deletions(-)

diff --git a/crates/configuration/src/global_settings.rs b/crates/configuration/src/global_settings.rs
index f6e7302..36a7600 100644
--- a/crates/configuration/src/global_settings.rs
+++ b/crates/configuration/src/global_settings.rs
@@ -235,7 +235,7 @@ mod tests {
 
         assert_eq!(errors.len(), 1);
         assert_eq!(
-            errors.get(0).unwrap().to_string(),
+            errors.first().unwrap().to_string(),
             "global_settings.bootnodes_addresses[0]: '/ip4//tcp/45421' failed to parse: invalid IPv4 address syntax"
         );
     }
@@ -250,7 +250,7 @@ mod tests {
 
         assert_eq!(errors.len(), 2);
         assert_eq!(
-            errors.get(0).unwrap().to_string(),
+            errors.first().unwrap().to_string(),
             "global_settings.bootnodes_addresses[0]: '/ip4//tcp/45421' failed to parse: invalid IPv4 address syntax"
         );
         assert_eq!(
@@ -268,7 +268,7 @@ mod tests {
 
         assert_eq!(errors.len(), 1);
         assert_eq!(
-            errors.get(0).unwrap().to_string(),
+            errors.first().unwrap().to_string(),
             "global_settings.local_ip: invalid IP address syntax"
         );
     }
@@ -284,7 +284,7 @@ mod tests {
 
         assert_eq!(errors.len(), 3);
         assert_eq!(
-            errors.get(0).unwrap().to_string(),
+            errors.first().unwrap().to_string(),
             "global_settings.bootnodes_addresses[0]: '/ip4//tcp/45421' failed to parse: invalid IPv4 address syntax"
         );
         assert_eq!(
diff --git a/crates/configuration/src/network.rs b/crates/configuration/src/network.rs
index ee67b19..cba24ca 100644
--- a/crates/configuration/src/network.rs
+++ b/crates/configuration/src/network.rs
@@ -582,7 +582,7 @@ mod tests {
 
         assert_eq!(errors.len(), 2);
         assert_eq!(
-            errors.get(0).unwrap().to_string(),
+            errors.first().unwrap().to_string(),
             "relaychain.default_image: 'invalid.image' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'"
         );
         assert_eq!(
@@ -622,7 +622,7 @@ mod tests {
 
         assert_eq!(errors.len(), 2);
         assert_eq!(
-            errors.get(0).unwrap().to_string(),
+            errors.first().unwrap().to_string(),
             "parachain[1000].collators['collator1'].command: 'invalid command' shouldn't contains whitespace"
         );
         assert_eq!(
@@ -679,7 +679,7 @@ mod tests {
 
         assert_eq!(errors.len(), 2);
         assert_eq!(
-            errors.get(0).unwrap().to_string(),
+            errors.first().unwrap().to_string(),
             "parachain[1000].collators['collator1'].command: 'invalid command' shouldn't contains whitespace"
         );
         assert_eq!(
@@ -724,7 +724,7 @@ mod tests {
 
         assert_eq!(errors.len(), 2);
         assert_eq!(
-            errors.get(0).unwrap().to_string(),
+            errors.first().unwrap().to_string(),
             "global_settings.local_ip: invalid IP address syntax"
         );
         assert_eq!(
@@ -766,7 +766,7 @@ mod tests {
 
         assert_eq!(errors.len(), 3);
         assert_eq!(
-            errors.get(0).unwrap().to_string(),
+            errors.first().unwrap().to_string(),
             "relaychain.nodes['node'].command: 'invalid command' shouldn't contains whitespace"
         );
         assert_eq!(
diff --git a/crates/configuration/src/parachain.rs b/crates/configuration/src/parachain.rs
index 58cd23c..5f3aa08 100644
--- a/crates/configuration/src/parachain.rs
+++ b/crates/configuration/src/parachain.rs
@@ -831,7 +831,7 @@ mod tests {
 
         assert_eq!(errors.len(), 1);
         assert_eq!(
-            errors.get(0).unwrap().to_string(),
+            errors.first().unwrap().to_string(),
             "parachain[1000].chain: 'invalid chain' shouldn't contains whitespace"
         );
     }
@@ -853,7 +853,7 @@ mod tests {
 
         assert_eq!(errors.len(), 1);
         assert_eq!(
-            errors.get(0).unwrap().to_string(),
+            errors.first().unwrap().to_string(),
             "parachain[1000].default_command: 'invalid command' shouldn't contains whitespace"
         );
     }
@@ -875,7 +875,7 @@ mod tests {
 
         assert_eq!(errors.len(), 1);
         assert_eq!(
-            errors.get(0).unwrap().to_string(),
+            errors.first().unwrap().to_string(),
             r"parachain[1000].default_image: 'invalid image' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'"
         );
     }
@@ -902,7 +902,7 @@ mod tests {
 
         assert_eq!(errors.len(), 1);
         assert_eq!(
-            errors.get(0).unwrap().to_string(),
+            errors.first().unwrap().to_string(),
             r"parachain[1000].default_resources.request_cpu: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
         );
     }
@@ -925,7 +925,7 @@ mod tests {
 
         assert_eq!(errors.len(), 1);
         assert_eq!(
-            errors.get(0).unwrap().to_string(),
+            errors.first().unwrap().to_string(),
             "parachain[2000].genesis_wasm_generator: 'invalid command' shouldn't contains whitespace"
         );
     }
@@ -948,7 +948,7 @@ mod tests {
 
         assert_eq!(errors.len(), 1);
         assert_eq!(
-            errors.get(0).unwrap().to_string(),
+            errors.first().unwrap().to_string(),
             "parachain[1000].genesis_state_generator: 'invalid command' shouldn't contains whitespace"
         );
     }
@@ -971,7 +971,7 @@ mod tests {
 
         assert_eq!(errors.len(), 2);
         assert_eq!(
-            errors.get(0).unwrap().to_string(),
+            errors.first().unwrap().to_string(),
             "parachain[2000].bootnodes_addresses[0]: '/ip4//tcp/45421' failed to parse: invalid IPv4 address syntax"
         );
         assert_eq!(
@@ -995,7 +995,7 @@ mod tests {
 
         assert_eq!(errors.len(), 1);
         assert_eq!(
-            errors.get(0).unwrap().to_string(),
+            errors.first().unwrap().to_string(),
             "parachain[1000].collators['collator'].command: 'invalid command' shouldn't contains whitespace"
         );
     }
@@ -1024,7 +1024,7 @@ mod tests {
 
         assert_eq!(errors.len(), 1);
         assert_eq!(
-            errors.get(0).unwrap().to_string(),
+            errors.first().unwrap().to_string(),
             "parachain[2000].collators['collator2'].command: 'invalid command' shouldn't contains whitespace"
         );
     }
@@ -1060,7 +1060,7 @@ mod tests {
 
         assert_eq!(errors.len(), 5);
         assert_eq!(
-            errors.get(0).unwrap().to_string(),
+            errors.first().unwrap().to_string(),
             "parachain[2000].bootnodes_addresses[0]: '/ip4//tcp/45421' failed to parse: invalid IPv4 address syntax"
         );
         assert_eq!(
diff --git a/crates/configuration/src/relaychain.rs b/crates/configuration/src/relaychain.rs
index a3782cd..9d33d5f 100644
--- a/crates/configuration/src/relaychain.rs
+++ b/crates/configuration/src/relaychain.rs
@@ -511,7 +511,7 @@ mod tests {
 
         assert_eq!(errors.len(), 1);
         assert_eq!(
-            errors.get(0).unwrap().to_string(),
+            errors.first().unwrap().to_string(),
             "relaychain.chain: 'invalid chain' shouldn't contains whitespace"
         );
     }
@@ -531,7 +531,7 @@ mod tests {
 
         assert_eq!(errors.len(), 1);
         assert_eq!(
-            errors.get(0).unwrap().to_string(),
+            errors.first().unwrap().to_string(),
             "relaychain.default_command: 'invalid command' shouldn't contains whitespace"
         );
     }
@@ -551,7 +551,7 @@ mod tests {
 
         assert_eq!(errors.len(), 1);
         assert_eq!(
-            errors.get(0).unwrap().to_string(),
+            errors.first().unwrap().to_string(),
             r"relaychain.default_image: 'invalid image' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'"
         );
     }
@@ -576,7 +576,7 @@ mod tests {
 
         assert_eq!(errors.len(), 1);
         assert_eq!(
-            errors.get(0).unwrap().to_string(),
+            errors.first().unwrap().to_string(),
             r"relaychain.default_resources.request_cpu: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
         );
     }
@@ -595,7 +595,7 @@ mod tests {
 
         assert_eq!(errors.len(), 1);
         assert_eq!(
-            errors.get(0).unwrap().to_string(),
+            errors.first().unwrap().to_string(),
             "relaychain.nodes['node'].command: 'invalid command' shouldn't contains whitespace"
         );
     }
@@ -620,7 +620,7 @@ mod tests {
 
         assert_eq!(errors.len(), 1);
         assert_eq!(
-            errors.get(0).unwrap().to_string(),
+            errors.first().unwrap().to_string(),
             "relaychain.nodes['node2'].command: 'invalid command' shouldn't contains whitespace"
         );
     }
@@ -646,7 +646,7 @@ mod tests {
 
         assert_eq!(errors.len(), 2);
         assert_eq!(
-            errors.get(0).unwrap().to_string(),
+            errors.first().unwrap().to_string(),
             "relaychain.default_resources.limit_cpu: 'invalid' doesn't match regex '^\\d+(.\\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
         );
         assert_eq!(
diff --git a/crates/configuration/src/shared/node.rs b/crates/configuration/src/shared/node.rs
index 16b3905..85ff15f 100644
--- a/crates/configuration/src/shared/node.rs
+++ b/crates/configuration/src/shared/node.rs
@@ -717,7 +717,7 @@ mod tests {
         assert_eq!(node_name, "node");
         assert_eq!(errors.len(), 1);
         assert_eq!(
-            errors.get(0).unwrap().to_string(),
+            errors.first().unwrap().to_string(),
             "command: 'invalid command' shouldn't contains whitespace"
         );
     }
@@ -734,7 +734,7 @@ mod tests {
         assert_eq!(node_name, "node");
         assert_eq!(errors.len(), 1);
         assert_eq!(
-            errors.get(0).unwrap().to_string(),
+            errors.first().unwrap().to_string(),
             "image: 'myinvalid.image' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'"
         );
     }
@@ -752,7 +752,7 @@ mod tests {
         assert_eq!(node_name, "node");
         assert_eq!(errors.len(), 1);
         assert_eq!(
-            errors.get(0).unwrap().to_string(),
+            errors.first().unwrap().to_string(),
             "bootnodes_addresses[0]: '/ip4//tcp/45421' failed to parse: invalid IPv4 address syntax"
         );
     }
@@ -770,7 +770,7 @@ mod tests {
         assert_eq!(node_name, "node");
         assert_eq!(errors.len(), 2);
         assert_eq!(
-            errors.get(0).unwrap().to_string(),
+            errors.first().unwrap().to_string(),
             "bootnodes_addresses[0]: '/ip4//tcp/45421' failed to parse: invalid IPv4 address syntax"
         );
         assert_eq!(
@@ -792,7 +792,7 @@ mod tests {
         assert_eq!(node_name, "node");
         assert_eq!(errors.len(), 1);
         assert_eq!(
-            errors.get(0).unwrap().to_string(),
+            errors.first().unwrap().to_string(),
             r"resources.limit_cpu: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
         );
     }
@@ -814,7 +814,7 @@ mod tests {
         assert_eq!(node_name, "node");
         assert_eq!(errors.len(), 2);
         assert_eq!(
-            errors.get(0).unwrap().to_string(),
+            errors.first().unwrap().to_string(),
             r"resources.limit_cpu: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
         );
         assert_eq!(
@@ -842,7 +842,7 @@ mod tests {
         assert_eq!(node_name, "node");
         assert_eq!(errors.len(), 4);
         assert_eq!(
-            errors.get(0).unwrap().to_string(),
+            errors.first().unwrap().to_string(),
             "command: 'invalid command' shouldn't contains whitespace"
         );
         assert_eq!(
@@ -875,7 +875,7 @@ mod tests {
         assert_eq!(node_name, "mynode");
         assert_eq!(errors.len(), 1);
         assert_eq!(
-            errors.get(0).unwrap().to_string(),
+            errors.first().unwrap().to_string(),
             "name: 'mynode' is already used across config"
         );
     }
@@ -897,7 +897,7 @@ mod tests {
         assert_eq!(node_name, "node");
         assert_eq!(errors.len(), 1);
         assert_eq!(
-            errors.get(0).unwrap().to_string(),
+            errors.first().unwrap().to_string(),
             "ws_port: '30333' is already used across config"
         );
     }
@@ -919,7 +919,7 @@ mod tests {
         assert_eq!(node_name, "node");
         assert_eq!(errors.len(), 1);
         assert_eq!(
-            errors.get(0).unwrap().to_string(),
+            errors.first().unwrap().to_string(),
             "rpc_port: '4444' is already used across config"
         );
     }
@@ -941,7 +941,7 @@ mod tests {
         assert_eq!(node_name, "node");
         assert_eq!(errors.len(), 1);
         assert_eq!(
-            errors.get(0).unwrap().to_string(),
+            errors.first().unwrap().to_string(),
             "prometheus_port: '9089' is already used across config"
         );
     }
@@ -963,7 +963,7 @@ mod tests {
         assert_eq!(node_name, "node");
         assert_eq!(errors.len(), 1);
         assert_eq!(
-            errors.get(0).unwrap().to_string(),
+            errors.first().unwrap().to_string(),
             "p2p_port: '45093' is already used across config"
         );
     }
diff --git a/crates/configuration/src/shared/resources.rs b/crates/configuration/src/shared/resources.rs
index 8537b18..425831d 100644
--- a/crates/configuration/src/shared/resources.rs
+++ b/crates/configuration/src/shared/resources.rs
@@ -423,7 +423,7 @@ mod tests {
 
         assert_eq!(errors.len(), 1);
         assert_eq!(
-            errors.get(0).unwrap().to_string(),
+            errors.first().unwrap().to_string(),
             r"request_memory: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
         );
     }
@@ -436,7 +436,7 @@ mod tests {
 
         assert_eq!(errors.len(), 1);
         assert_eq!(
-            errors.get(0).unwrap().to_string(),
+            errors.first().unwrap().to_string(),
             r"request_cpu: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
         );
     }
@@ -449,7 +449,7 @@ mod tests {
 
         assert_eq!(errors.len(), 1);
         assert_eq!(
-            errors.get(0).unwrap().to_string(),
+            errors.first().unwrap().to_string(),
             r"limit_memory: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
         );
     }
@@ -462,7 +462,7 @@ mod tests {
 
         assert_eq!(errors.len(), 1);
         assert_eq!(
-            errors.get(0).unwrap().to_string(),
+            errors.first().unwrap().to_string(),
             r"limit_cpu: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
         );
     }
@@ -478,7 +478,7 @@ mod tests {
 
         assert_eq!(errors.len(), 2);
         assert_eq!(
-            errors.get(0).unwrap().to_string(),
+            errors.first().unwrap().to_string(),
             r"limit_cpu: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
         );
         assert_eq!(
diff --git a/crates/examples/Cargo.toml b/crates/examples/Cargo.toml
index dce89cf..a39fd0e 100644
--- a/crates/examples/Cargo.toml
+++ b/crates/examples/Cargo.toml
@@ -13,3 +13,5 @@ provider = { path = "../provider" }
 # to review the exports for neeeded types
 support = { path = "../support" }
 tokio = { workspace = true }
+futures = { workspace = true }
+subxt = { workspace = true }
diff --git a/crates/examples/examples/simple_network_example.rs b/crates/examples/examples/simple_network_example.rs
index fb42ef2..a8731cc 100644
--- a/crates/examples/examples/simple_network_example.rs
+++ b/crates/examples/examples/simple_network_example.rs
@@ -1,6 +1,7 @@
 // use std::time::Duration;
 
 use configuration::NetworkConfig;
+use futures::stream::StreamExt;
 use orchestrator::Orchestrator;
 use provider::NativeProvider;
 use support::{fs::local::LocalFileSystem, process::os::OsProcessManager};
@@ -14,11 +15,18 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
     let pm = OsProcessManager;
     let provider = NativeProvider::new(fs.clone(), pm);
     let orchestrator = Orchestrator::new(fs, provider);
-    orchestrator.spawn(config).await?;
+    let network = orchestrator.spawn(config).await?;
     println!("🚀🚀🚀🚀 network deployed");
-    // For now let just loop....
-    #[allow(clippy::empty_loop)]
-    loop {}
 
-    // Ok(())
+    let client = network
+        .get_node("alice")?
+        .client::<subxt::PolkadotConfig>()
+        .await?;
+    let mut blocks = client.blocks().subscribe_finalized().await?.take(3);
+
+    while let Some(block) = blocks.next().await {
+        println!("Block #{}", block?.header().number);
+    }
+
+    Ok(())
 }
diff --git a/crates/orchestrator/src/network/node.rs b/crates/orchestrator/src/network/node.rs
index 0be0b88..904f96f 100644
--- a/crates/orchestrator/src/network/node.rs
+++ b/crates/orchestrator/src/network/node.rs
@@ -3,6 +3,7 @@ use std::{sync::Arc, time::Duration};
 use anyhow::anyhow;
 use prom_metrics_parser::MetricMap;
 use provider::DynNode;
+use subxt::{backend::rpc::RpcClient, OnlineClient};
 use tokio::sync::RwLock;
 
 use crate::network_spec::node::NodeSpec;
@@ -45,6 +46,18 @@ impl NetworkNode {
         Ok(())
     }
 
+    /// Get the rpc client for the node
+    pub async fn rpc(&self) -> Result<RpcClient, subxt::Error> {
+        RpcClient::from_url(&self.ws_uri).await
+    }
+
+    /// Get the online client for the node
+    pub async fn client<Config: subxt::Config>(
+        &self,
+    ) -> Result<OnlineClient<Config>, subxt::Error> {
+        OnlineClient::from_url(&self.ws_uri).await
+    }
+
     /// 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> {
diff --git a/crates/orchestrator/src/network_spec.rs b/crates/orchestrator/src/network_spec.rs
index 13f49b8..a38a4f2 100644
--- a/crates/orchestrator/src/network_spec.rs
+++ b/crates/orchestrator/src/network_spec.rs
@@ -81,7 +81,7 @@ mod tests {
             .unwrap();
 
         let network_spec = NetworkSpec::from_config(&config).await.unwrap();
-        let alice = network_spec.relaychain.nodes.get(0).unwrap();
+        let alice = network_spec.relaychain.nodes.first().unwrap();
         let bob = network_spec.relaychain.nodes.get(1).unwrap();
         assert_eq!(alice.command.as_str(), "polkadot");
         assert_eq!(bob.command.as_str(), "polkadot1");
@@ -90,7 +90,7 @@ mod tests {
 
         // paras
         assert_eq!(network_spec.parachains.len(), 1);
-        let para_100 = network_spec.parachains.get(0).unwrap();
+        let para_100 = network_spec.parachains.first().unwrap();
         assert_eq!(para_100.id, 100);
     }
 }
diff --git a/crates/orchestrator/src/spawner.rs b/crates/orchestrator/src/spawner.rs
index 0ff0e71..18db51f 100644
--- a/crates/orchestrator/src/spawner.rs
+++ b/crates/orchestrator/src/spawner.rs
@@ -108,20 +108,20 @@ where
     let (program, args) = match ctx.role {
         // Collator should be `non-cumulus` one (e.g adder/undying)
         ZombieRole::Node | ZombieRole::Collator => {
-
             let maybe_para_id = ctx.parachain.map(|para| para.id);
 
             generators::generate_node_command(node, gen_opts, maybe_para_id)
         },
         ZombieRole::CumulusCollator => {
-            let para = ctx.parachain.expect("parachain must be part of the context, this is a bug");
+            let para = ctx
+                .parachain
+                .expect("parachain must be part of the context, this is a bug");
             let full_p2p = generators::generate_node_port(None)?;
             generators::generate_node_command_cumulus(node, gen_opts, para.id, full_p2p.0)
-        }
-        _ => unreachable!()
-        // TODO: do we need those?
-        // ZombieRole::Bootnode => todo!(),
-        // ZombieRole::Companion => todo!(),
+        },
+        _ => unreachable!(), /* TODO: do we need those?
+                              * ZombieRole::Bootnode => todo!(),
+                              * ZombieRole::Companion => todo!(), */
     };
 
     println!("\n");
-- 
GitLab