Repository: scs/substrate-api-client (language: rust)

Issue:
Add possibility to include self-defined Jsonrpsee Client in Wrapper struct
E.g. add new function to create a `JsonrpseeClient` wrapper but with a user defined client here: https://github.com/scs/substrate-api-client/blob/3098812af70d80bdd63f82639f76e5c3e2c36312/src/rpc/jsonrpsee_client/mod.rs#L35-L50
Example:
```rust
pub async fn with_client(client: Arc<Client>) -> Self {
	Self { inner: client }
}
```
I'd also propose to make the inner client public - then the user may change settings any time.


Discussion / hints:
I don't think we should make the inner client public. We don't want anyone to change the client during the running program (what happens e. g. with subscriptions, when I exchange the client? What happens with running processes? We would have to handle all these cases.).

But adding a constructor which takes a custom JsonrpseeClient is fine. I don't see any problem with that.
That is true, but this way no one can access the inner convenience functions, such as `is_connected` (see here: https://github.com/paritytech/jsonrpsee/blob/master/core/src/client/async_client/mod.rs#L458-L490). We could also forward them, but then we'd need to make sure that we stay up-to-date with these functions. 

Fix patch:
diff --git a/Cargo.lock b/Cargo.lock
index 41afa87d7..b307668b1 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -122,6 +122,7 @@ name = "ac-testing-async"
 version = "0.4.0"
 dependencies = [
  "frame-support",
+ "jsonrpsee",
  "kitchensink-runtime",
  "pallet-balances",
  "pallet-staking",
@@ -2362,6 +2363,25 @@ dependencies = [
  "subtle",
 ]
 
+[[package]]
+name = "h2"
+version = "0.3.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9"
+dependencies = [
+ "bytes 1.5.0",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "futures-util",
+ "http 0.2.11",
+ "indexmap 2.2.3",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
 [[package]]
 name = "hash-db"
 version = "0.16.0"
@@ -2490,18 +2510,58 @@ dependencies = [
  "itoa",
 ]
 
+[[package]]
+name = "http-body"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2"
+dependencies = [
+ "bytes 1.5.0",
+ "http 0.2.11",
+ "pin-project-lite",
+]
+
 [[package]]
 name = "httparse"
 version = "1.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"
 
+[[package]]
+name = "httpdate"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
+
 [[package]]
 name = "humantime"
 version = "2.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
 
+[[package]]
+name = "hyper"
+version = "0.14.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80"
+dependencies = [
+ "bytes 1.5.0",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "h2",
+ "http 0.2.11",
+ "http-body",
+ "httparse",
+ "httpdate",
+ "itoa",
+ "pin-project-lite",
+ "tokio",
+ "tower-service",
+ "tracing",
+ "want",
+]
+
 [[package]]
 name = "iana-time-zone"
 version = "0.1.60"
@@ -2700,20 +2760,22 @@ dependencies = [
 
 [[package]]
 name = "jsonrpsee"
-version = "0.21.0"
+version = "0.22.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9579d0ca9fb30da026bac2f0f7d9576ec93489aeb7cd4971dd5b4617d82c79b2"
+checksum = "16fcc9dd231e72d22993f1643d5f7f0db785737dbe3c3d7ca222916ab4280795"
 dependencies = [
  "jsonrpsee-client-transport",
  "jsonrpsee-core",
+ "jsonrpsee-server",
  "jsonrpsee-types",
+ "tokio",
 ]
 
 [[package]]
 name = "jsonrpsee-client-transport"
-version = "0.21.0"
+version = "0.22.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3f9f9ed46590a8d5681975f126e22531698211b926129a40a2db47cbca429220"
+checksum = "0476c96eb741b40d39dcb39d0124e3b9be9840ec77653c42a0996563ae2a53f7"
 dependencies = [
  "futures-util",
  "http 0.2.11",
@@ -2732,9 +2794,9 @@ dependencies = [
 
 [[package]]
 name = "jsonrpsee-core"
-version = "0.21.0"
+version = "0.22.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "776d009e2f591b78c038e0d053a796f94575d66ca4e77dd84bfc5e81419e436c"
+checksum = "b974d8f6139efbe8425f32cb33302aba6d5e049556b5bfc067874e7a0da54a2e"
 dependencies = [
  "anyhow",
  "async-lock",
@@ -2742,8 +2804,11 @@ dependencies = [
  "beef",
  "futures-timer",
  "futures-util",
+ "hyper",
  "jsonrpsee-types",
+ "parking_lot",
  "pin-project",
+ "rand 0.8.5",
  "rustc-hash",
  "serde",
  "serde_json",
@@ -2753,11 +2818,35 @@ dependencies = [
  "tracing",
 ]
 
+[[package]]
+name = "jsonrpsee-server"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "344440ccd8492c1ca65f1391c5aa03f91189db38d602d189b9266a1a5c6a4d22"
+dependencies = [
+ "futures-util",
+ "http 0.2.11",
+ "hyper",
+ "jsonrpsee-core",
+ "jsonrpsee-types",
+ "pin-project",
+ "route-recognizer",
+ "serde",
+ "serde_json",
+ "soketto",
+ "thiserror",
+ "tokio",
+ "tokio-stream",
+ "tokio-util",
+ "tower",
+ "tracing",
+]
+
 [[package]]
 name = "jsonrpsee-types"
-version = "0.21.0"
+version = "0.22.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3266dfb045c9174b24c77c2dfe0084914bb23a6b2597d70c9dc6018392e1cd1b"
+checksum = "b13dac43c1a9fc2648b37f306b0a5b0e29b2a6e1c36a33b95c1948da2494e9c5"
 dependencies = [
  "anyhow",
  "beef",
@@ -5551,6 +5640,12 @@ dependencies = [
  "windows-sys 0.48.0",
 ]
 
+[[package]]
+name = "route-recognizer"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "afab94fb28594581f62d981211a9a4d53cc8130bbcbbb89a0440d9b8e81a7746"
+
 [[package]]
 name = "rustc-demangle"
 version = "0.1.23"
@@ -6202,6 +6297,7 @@ dependencies = [
  "base64 0.13.1",
  "bytes 1.5.0",
  "futures",
+ "http 0.2.11",
  "httparse",
  "log",
  "rand 0.8.5",
@@ -7422,6 +7518,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931"
 dependencies = [
  "backtrace",
+ "bytes 1.5.0",
  "libc",
  "mio 0.8.10",
  "num_cpus",
@@ -7462,6 +7559,7 @@ dependencies = [
  "futures-core",
  "pin-project-lite",
  "tokio",
+ "tokio-util",
 ]
 
 [[package]]
@@ -7476,6 +7574,7 @@ dependencies = [
  "futures-sink",
  "pin-project-lite",
  "tokio",
+ "tracing",
 ]
 
 [[package]]
@@ -7545,12 +7644,36 @@ dependencies = [
  "winnow 0.6.0",
 ]
 
+[[package]]
+name = "tower"
+version = "0.4.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
+dependencies = [
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "tower-layer"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0"
+
+[[package]]
+name = "tower-service"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"
+
 [[package]]
 name = "tracing"
 version = "0.1.40"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
 dependencies = [
+ "log",
  "pin-project-lite",
  "tracing-attributes",
  "tracing-core",
@@ -7642,6 +7765,12 @@ dependencies = [
  "hash-db",
 ]
 
+[[package]]
+name = "try-lock"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
+
 [[package]]
 name = "tt-call"
 version = "1.0.9"
@@ -7845,6 +7974,15 @@ dependencies = [
  "winapi-util",
 ]
 
+[[package]]
+name = "want"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
+dependencies = [
+ "try-lock",
+]
+
 [[package]]
 name = "wasi"
 version = "0.9.0+wasi-snapshot-preview1"
diff --git a/Cargo.toml b/Cargo.toml
index 734e93f24..2e1a5bdd7 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -42,7 +42,7 @@ serde_json = { version = "1.0.79", default-features = false }
 url = { version = "2.0.0", optional = true }
 
 # websocket dependent features
-jsonrpsee = { version = "0.21", optional = true, features = ["async-client", "client-ws-transport-native-tls", "jsonrpsee-types"] }
+jsonrpsee = { version = "0.22", optional = true, features = ["async-client", "client-ws-transport-native-tls", "jsonrpsee-types"] }
 tungstenite = { version = "0.21", optional = true, features = ["native-tls"] }
 ws = { version = "0.9.2", optional = true, features = ["ssl"] }
 
diff --git a/src/rpc/jsonrpsee_client/mod.rs b/src/rpc/jsonrpsee_client/mod.rs
index bdb822cf3..30b6f1018 100644
--- a/src/rpc/jsonrpsee_client/mod.rs
+++ b/src/rpc/jsonrpsee_client/mod.rs
@@ -15,7 +15,7 @@ use crate::rpc::{Error, Request, Result, RpcParams, Subscribe};
 use jsonrpsee::{
 	client_transport::ws::{Url, WsTransportClientBuilder},
 	core::{
-		client::{Client, ClientBuilder, ClientT, SubscriptionClientT},
+		client::{Client, ClientBuilder, ClientT, Error as JsonrpseeError, SubscriptionClientT},
 		traits::ToRpcParams,
 	},
 };
@@ -33,6 +33,7 @@ pub struct JsonrpseeClient {
 }
 
 impl JsonrpseeClient {
+	/// Create a new client to a local Substrate node with default port.
 	pub async fn with_default_url() -> Result<Self> {
 		Self::new("ws://127.0.0.1:9944").await
 	}
@@ -59,6 +60,42 @@ impl JsonrpseeClient {
 		let url = format!("{address}:{port:?}");
 		Self::new(&url).await
 	}
+
+	/// Create a new client with a user-generated Jsonrpsee Client.
+	pub fn new_with_client(client: Client) -> Self {
+		let inner = Arc::new(client);
+		Self { inner }
+	}
+}
+
+impl JsonrpseeClient {
+	/// Checks if the client is connected to the target.
+	pub fn is_connected(&self) -> bool {
+		self.inner.is_connected()
+	}
+
+	/// This is similar to [`Client::on_disconnect`] but it can be used to get
+	/// the reason why the client was disconnected but it's not cancel-safe.
+	///
+	/// The typical use-case is that this method will be called after
+	/// [`Client::on_disconnect`] has returned in a "select loop".
+	///
+	/// # Cancel-safety
+	///
+	/// This method is not cancel-safe
+	pub async fn disconnect_reason(&self) -> JsonrpseeError {
+		self.inner.disconnect_reason().await
+	}
+
+	/// Completes when the client is disconnected or the client's background task encountered an error.
+	/// If the client is already disconnected, the future produced by this method will complete immediately.
+	///
+	/// # Cancel safety
+	///
+	/// This method is cancel safe.
+	pub async fn on_disconnect(&self) {
+		self.inner.on_disconnect().await;
+	}
 }
 
 #[maybe_async::async_impl(?Send)]


Test patch:
diff --git a/testing/async/Cargo.toml b/testing/async/Cargo.toml
index fba3ca587..58820e8f3 100644
--- a/testing/async/Cargo.toml
+++ b/testing/async/Cargo.toml
@@ -8,6 +8,8 @@ edition = "2021"
 [dev-dependencies]
 codec = { package = "parity-scale-codec", version = "3.6.1", features = ['derive'] }
 tokio = { version = "1.24", features = ["rt-multi-thread", "macros", "time"] }
+jsonrpsee = { version = "0.22", features = ["async-client", "client-ws-transport-native-tls", "jsonrpsee-types", "server"] }
+
 
 # Substrate dependencies
 frame-support = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "master" }
diff --git a/testing/async/examples/jsonrpsee_tests.rs b/testing/async/examples/jsonrpsee_tests.rs
index b2c66de68..87c25a7ad 100644
--- a/testing/async/examples/jsonrpsee_tests.rs
+++ b/testing/async/examples/jsonrpsee_tests.rs
@@ -15,7 +15,15 @@
 
 //! Tests for the Jsonrpseeclient Wrapper. Should happen during runtime, otherwise no connection can be etablished.
 
+use core::panic;
+
+use jsonrpsee::{
+	client_transport::ws::{Url, WsTransportClientBuilder},
+	core::client::{Client, ClientBuilder},
+	server::{RpcModule, Server},
+};
 use substrate_api_client::rpc::JsonrpseeClient;
+use tokio::{sync::oneshot, task, time, time::Duration};
 
 #[tokio::main]
 async fn main() {
@@ -30,4 +38,51 @@ async fn main() {
 	assert!(client2.is_ok());
 	// No node running at this port - creation should fail.
 	assert!(client3.is_err());
+
+	// Test new_with_client and extra functionality of inner Jsonrpseee client.
+	let (tx, mut rx) = oneshot::channel();
+	let finish_message = "Finishing task";
+
+	// Start server.
+	let server = Server::builder().build("127.0.0.1:0").await.unwrap();
+	let addr = server.local_addr().unwrap();
+	let server_handle = server.start(RpcModule::new(()));
+
+	// Create client and connect.
+	let uri = Url::parse(&format!("ws://{}", addr)).unwrap();
+	let (tx1, rx1) = WsTransportClientBuilder::default().build(uri).await.unwrap();
+	let client: Client = ClientBuilder::default().build_with_tokio(tx1, rx1);
+	let api_rpsee_client = JsonrpseeClient::new_with_client(client);
+	assert!(api_rpsee_client.is_connected());
+
+	let client_handle = task::spawn(async move {
+		println!("Waiting for client disconnect");
+		api_rpsee_client.on_disconnect().await;
+		time::sleep(Duration::from_secs(2)).await;
+		println!("Disconnected due to: {:?}", api_rpsee_client.disconnect_reason().await);
+		tx.send(finish_message).unwrap();
+	});
+
+	// Drop server such that client gets a disconnect.
+	drop(server_handle);
+
+	// Wait for the disconnect message.
+	let timeout = 5;
+	let mut ctr = 0;
+	loop {
+		if let Ok(message) = rx.try_recv() {
+			assert_eq!(finish_message, message);
+			println!("{message}");
+			break;
+		} else {
+			ctr += 1;
+			if ctr == timeout {
+				panic!("Timeout");
+			}
+			time::sleep(Duration::from_secs(1)).await;
+			println!("sleeping..");
+		}
+	}
+
+	client_handle.await.unwrap();
 }