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

Issue:
Pass adress and port independently to constructor.
Change the `Api::new(url: String)` to `Api::new(address: &str, port: &str)`. Practice shows that this is more convenient as all our CLI`s fetch the url and the port independently, which leads to boilerplate code outside the Api.

Note: `&str` suffices, as we will assemble the `String` internally with `format!`


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 74fbb8e2e..39621b6de 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -160,6 +160,7 @@ jobs:
           chain_tests,
           dispatch_errors_tests,
           frame_system_tests,
+          jsonrpsee_tests,
           keystore_tests,
           pallet_balances_tests,
           pallet_transaction_payment_tests,
diff --git a/src/rpc/jsonrpsee_client/mod.rs b/src/rpc/jsonrpsee_client/mod.rs
index be2bb50d6..bdb822cf3 100644
--- a/src/rpc/jsonrpsee_client/mod.rs
+++ b/src/rpc/jsonrpsee_client/mod.rs
@@ -37,6 +37,8 @@ impl JsonrpseeClient {
 		Self::new("ws://127.0.0.1:9944").await
 	}
 
+	/// Create a new client with the given url string.
+	/// Example url input: "ws://127.0.0.1:9944"
 	pub async fn new(url: &str) -> Result<Self> {
 		let parsed_url: Url = url.parse().map_err(|e| Error::Client(Box::new(e)))?;
 		let (tx, rx) = WsTransportClientBuilder::default()
@@ -48,6 +50,15 @@ impl JsonrpseeClient {
 			.build_with_tokio(tx, rx);
 		Ok(Self { inner: Arc::new(client) })
 	}
+
+	/// Create a new client with the given address, port and max number of reconnection attempts.
+	/// Example input:
+	/// - address: "ws://127.0.0.1"
+	/// - port: 9944
+	pub async fn new_with_port(address: &str, port: u32) -> Result<Self> {
+		let url = format!("{address}:{port:?}");
+		Self::new(&url).await
+	}
 }
 
 #[maybe_async::async_impl(?Send)]
diff --git a/src/rpc/tungstenite_client/client.rs b/src/rpc/tungstenite_client/client.rs
index 597f6baf7..99b7c1d0f 100644
--- a/src/rpc/tungstenite_client/client.rs
+++ b/src/rpc/tungstenite_client/client.rs
@@ -45,11 +45,25 @@ pub struct TungsteniteRpcClient {
 }
 
 impl TungsteniteRpcClient {
+	/// Create a new client with the given url string.
+	/// Example url input: "ws://127.0.0.1:9944"
 	pub fn new(url: &str, max_attempts: u8) -> Result<Self> {
-		Ok(Self { url: Url::parse(url)?, max_attempts })
+		let url: Url = Url::parse(url)?;
+		Ok(Self { url, max_attempts })
 	}
 
+	/// Create a new client with the given address, port and max number of reconnection attempts.
+	/// Example input:
+	/// - address: "ws://127.0.0.1"
+	/// - port: 9944
+	pub fn new_with_port(address: &str, port: u32, max_attempts: u8) -> Result<Self> {
+		let url = format!("{address}:{port:?}");
+		Self::new(&url, max_attempts)
+	}
+
+	/// Create a new client with a local address and default Substrate node port.
 	pub fn with_default_url(max_attempts: u8) -> Self {
+		// This unwrap is safe as is only regards the url parsing, which is tested.
 		Self::new("ws://127.0.0.1:9944", max_attempts).unwrap()
 	}
 }
@@ -210,3 +224,26 @@ fn read_until_text_message(socket: &mut MySocket) -> Result<String> {
 		}
 	}
 }
+
+#[cfg(test)]
+mod tests {
+	use super::*;
+
+	#[test]
+	fn client_new() {
+		let port = 9944;
+		let address = "ws://127.0.0.1";
+		let client = TungsteniteRpcClient::new_with_port(address, port, 1).unwrap();
+
+		let expected_url = Url::parse("ws://127.0.0.1:9944").unwrap();
+		assert_eq!(client.url, expected_url);
+	}
+
+	#[test]
+	fn client_with_default_url() {
+		let expected_url = Url::parse("ws://127.0.0.1:9944").unwrap();
+		let client = TungsteniteRpcClient::with_default_url(1);
+
+		assert_eq!(client.url, expected_url);
+	}
+}
diff --git a/src/rpc/ws_client/client.rs b/src/rpc/ws_client/client.rs
index bd7b53bcf..b3830e5c7 100644
--- a/src/rpc/ws_client/client.rs
+++ b/src/rpc/ws_client/client.rs
@@ -37,11 +37,25 @@ pub struct WsRpcClient {
 }
 
 impl WsRpcClient {
+	/// Create a new client with the given url string.
+	/// Example url input: "ws://127.0.0.1:9944"
 	pub fn new(url: &str) -> Result<Self> {
-		Ok(Self { url: Url::parse(url)? })
+		let url = Url::parse(url)?;
+		Ok(Self { url })
 	}
 
+	/// Create a new client with the given address and port.
+	/// Example input:
+	/// - address: "ws://127.0.0.1"
+	/// - port: 9944
+	pub fn new_with_port(address: &str, port: u32) -> Result<Self> {
+		let url = format!("{address}:{port:?}");
+		Self::new(&url)
+	}
+
+	/// Create a new client with a local address and default Substrate node port.
 	pub fn with_default_url() -> Self {
+		// This unwrap is safe as is only regards the url parsing, which is tested.
 		Self::new("ws://127.0.0.1:9944").unwrap()
 	}
 }
@@ -127,3 +141,26 @@ impl WsRpcClient {
 		Ok(result_out.recv()?)
 	}
 }
+
+#[cfg(test)]
+mod tests {
+	use super::*;
+
+	#[test]
+	fn client_new() {
+		let port = 9944;
+		let address = "ws://127.0.0.1";
+		let client = WsRpcClient::new_with_port(address, port).unwrap();
+
+		let expected_url = Url::parse("ws://127.0.0.1:9944").unwrap();
+		assert_eq!(client.url, expected_url);
+	}
+
+	#[test]
+	fn client_with_default_url() {
+		let expected_url = Url::parse("ws://127.0.0.1:9944").unwrap();
+		let client = WsRpcClient::with_default_url();
+
+		assert_eq!(client.url, expected_url);
+	}
+}


Test patch:
diff --git a/testing/async/examples/dump_metadata.rs b/testing/async/examples/dump_metadata.rs
index be488a882..ac7656d53 100644
--- a/testing/async/examples/dump_metadata.rs
+++ b/testing/async/examples/dump_metadata.rs
@@ -10,7 +10,9 @@ use substrate_api_client::{
 
 #[tokio::main]
 async fn main() {
-	let client = JsonrpseeClient::new("wss://kusama-rpc.polkadot.io:443").await.unwrap();
+	let client = JsonrpseeClient::new_with_port("wss://kusama-rpc.polkadot.io", 443)
+		.await
+		.unwrap();
 	let metadata_bytes: Bytes = client.request("state_getMetadata", rpc_params![]).await.unwrap();
 	let mut file = File::create("new_ksm_metadata.bin").unwrap();
 	file.write_all(&metadata_bytes.0).unwrap();
diff --git a/testing/async/examples/jsonrpsee_tests.rs b/testing/async/examples/jsonrpsee_tests.rs
new file mode 100644
index 000000000..b2c66de68
--- /dev/null
+++ b/testing/async/examples/jsonrpsee_tests.rs
@@ -0,0 +1,33 @@
+/*
+	Copyright 2024 Supercomputing Systems AG
+	Licensed under the Apache License, Version 2.0 (the "License");
+	you may not use this file except in compliance with the License.
+	You may obtain a copy of the License at
+
+		http://www.apache.org/licenses/LICENSE-2.0
+
+	Unless required by applicable law or agreed to in writing, software
+	distributed under the License is distributed on an "AS IS" BASIS,
+	WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+	See the License for the specific language governing permissions and
+	limitations under the License.
+*/
+
+//! Tests for the Jsonrpseeclient Wrapper. Should happen during runtime, otherwise no connection can be etablished.
+
+use substrate_api_client::rpc::JsonrpseeClient;
+
+#[tokio::main]
+async fn main() {
+	let port = 9944;
+	let address = "ws://127.0.0.1";
+
+	let client = JsonrpseeClient::with_default_url().await;
+	let client2 = JsonrpseeClient::new_with_port(address, port).await;
+	let client3 = JsonrpseeClient::new_with_port(address, 9994).await;
+
+	assert!(client.is_ok());
+	assert!(client2.is_ok());
+	// No node running at this port - creation should fail.
+	assert!(client3.is_err());
+}