use crate::{
Blockchain,
rpc_server::{
RpcServerError,
types::{SyncState, SystemHealth},
},
strings::rpc_server::{storage, system},
};
use jsonrpsee::{core::RpcResult, proc_macros::rpc};
use sp_core::crypto::{AccountId32, Ss58Codec};
use std::sync::Arc;
#[rpc(server, namespace = "system")]
pub trait SystemApi {
#[method(name = "chain")]
async fn chain(&self) -> RpcResult<String>;
#[method(name = "name")]
async fn name(&self) -> RpcResult<String>;
#[method(name = "version")]
async fn version(&self) -> RpcResult<String>;
#[method(name = "health")]
async fn health(&self) -> RpcResult<SystemHealth>;
#[method(name = "properties")]
async fn properties(&self) -> RpcResult<Option<serde_json::Value>>;
#[method(name = "localPeerId")]
fn local_peer_id(&self) -> RpcResult<String>;
#[method(name = "nodeRoles")]
fn node_roles(&self) -> RpcResult<Vec<String>>;
#[method(name = "localListenAddresses")]
fn local_listen_addresses(&self) -> RpcResult<Vec<String>>;
#[method(name = "chainType")]
fn chain_type(&self) -> RpcResult<String>;
#[method(name = "syncState")]
async fn sync_state(&self) -> RpcResult<SyncState>;
#[method(name = "accountNextIndex")]
async fn account_next_index(&self, account: String) -> RpcResult<u32>;
}
pub struct SystemApi {
blockchain: Arc<Blockchain>,
}
impl SystemApi {
pub fn new(blockchain: Arc<Blockchain>) -> Self {
Self { blockchain }
}
}
#[async_trait::async_trait]
impl SystemApiServer for SystemApi {
async fn chain(&self) -> RpcResult<String> {
Ok(self.blockchain.chain_name().to_string())
}
async fn name(&self) -> RpcResult<String> {
Ok(system::NODE_NAME.to_string())
}
async fn version(&self) -> RpcResult<String> {
Ok(system::NODE_VERSION.to_string())
}
async fn health(&self) -> RpcResult<SystemHealth> {
Ok(SystemHealth::default())
}
async fn properties(&self) -> RpcResult<Option<serde_json::Value>> {
Ok(self.blockchain.chain_properties().await)
}
fn local_peer_id(&self) -> RpcResult<String> {
Ok(system::MOCK_PEER_ID.to_string())
}
fn node_roles(&self) -> RpcResult<Vec<String>> {
Ok(vec![system::NODE_ROLE_FULL.to_string()])
}
fn local_listen_addresses(&self) -> RpcResult<Vec<String>> {
Ok(vec![])
}
fn chain_type(&self) -> RpcResult<String> {
Ok(system::CHAIN_TYPE_DEVELOPMENT.to_string())
}
async fn sync_state(&self) -> RpcResult<SyncState> {
let head = self.blockchain.head_number().await;
Ok(SyncState { starting_block: 0, current_block: head, highest_block: head })
}
async fn account_next_index(&self, account: String) -> RpcResult<u32> {
let account_id = AccountId32::from_ss58check(&account).map_err(|_| {
RpcServerError::InvalidParam(format!("Invalid SS58 address: {}", account))
})?;
let account_bytes: [u8; 32] = account_id.into();
let mut key = Vec::new();
key.extend(sp_core::twox_128(storage::SYSTEM_PALLET));
key.extend(sp_core::twox_128(storage::ACCOUNT_STORAGE));
key.extend(sp_core::blake2_128(&account_bytes));
key.extend(&account_bytes);
let block_number = self.blockchain.head_number().await;
match self.blockchain.storage_at(block_number, &key).await {
Ok(Some(data)) if data.len() >= storage::NONCE_SIZE => {
let nonce = u32::from_le_bytes(
data[..storage::NONCE_SIZE].try_into().unwrap_or([0; storage::NONCE_SIZE]),
);
Ok(nonce)
},
_ => Ok(0), }
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::testing::TestContext;
use jsonrpsee::{core::client::ClientT, rpc_params, ws_client::WsClientBuilder};
#[tokio::test(flavor = "multi_thread")]
async fn chain_works() {
let ctx = TestContext::for_rpc_server().await;
let client = WsClientBuilder::default()
.build(&ctx.ws_url())
.await
.expect("Failed to connect");
let name: String =
client.request("system_chain", rpc_params![]).await.expect("RPC call failed");
assert!(!name.is_empty(), "Chain name should not be empty");
assert_eq!(name, "ink-node");
}
#[tokio::test(flavor = "multi_thread")]
async fn name_works() {
let ctx = TestContext::for_rpc_server().await;
let client = WsClientBuilder::default()
.build(&ctx.ws_url())
.await
.expect("Failed to connect");
let name: String =
client.request("system_name", rpc_params![]).await.expect("RPC call failed");
assert!(!name.is_empty(), "Chain name should not be empty");
assert_eq!(name, "pop-fork");
}
#[tokio::test(flavor = "multi_thread")]
async fn version_works() {
let ctx = TestContext::for_rpc_server().await;
let client = WsClientBuilder::default()
.build(&ctx.ws_url())
.await
.expect("Failed to connect");
let version: String =
client.request("system_version", rpc_params![]).await.expect("RPC call failed");
assert_eq!(version, "1.0.0");
}
#[tokio::test(flavor = "multi_thread")]
async fn health_works() {
let ctx = TestContext::for_rpc_server().await;
let client = WsClientBuilder::default()
.build(&ctx.ws_url())
.await
.expect("Failed to connect");
let health: SystemHealth =
client.request("system_health", rpc_params![]).await.expect("RPC call failed");
assert_eq!(health, SystemHealth::default());
}
#[tokio::test(flavor = "multi_thread")]
async fn chain_spec_chain_name() {
let ctx = TestContext::for_rpc_server().await;
let client = WsClientBuilder::default()
.build(&ctx.ws_url())
.await
.expect("Failed to connect");
let name: String =
client.request("system_chain", rpc_params![]).await.expect("RPC call failed");
assert!(!name.is_empty(), "Chain name should not be empty");
assert_eq!(name, "ink-node");
}
#[tokio::test(flavor = "multi_thread")]
async fn properties_returns_json_or_null() {
let ctx = TestContext::for_rpc_server().await;
let client = WsClientBuilder::default()
.build(&ctx.ws_url())
.await
.expect("Failed to connect");
let properties: Option<serde_json::Value> = client
.request("system_properties", rpc_params![])
.await
.expect("RPC call failed");
if let Some(props) = &properties {
assert!(props.is_object(), "Properties should be a JSON object");
}
}
const ALICE_SS58: &str = "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY";
#[tokio::test(flavor = "multi_thread")]
async fn account_next_index_returns_nonce() {
let ctx = TestContext::for_rpc_server().await;
let client = WsClientBuilder::default()
.build(&ctx.ws_url())
.await
.expect("Failed to connect");
let nonce: u32 = client
.request("system_accountNextIndex", rpc_params![ALICE_SS58])
.await
.expect("RPC call failed");
assert!(nonce < u32::MAX, "Nonce should be a valid value");
}
#[tokio::test(flavor = "multi_thread")]
async fn account_next_index_returns_zero_for_nonexistent() {
let ctx = TestContext::for_rpc_server().await;
let client = WsClientBuilder::default()
.build(&ctx.ws_url())
.await
.expect("Failed to connect");
let nonexistent_account = "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM";
let nonce: u32 = client
.request("system_accountNextIndex", rpc_params![nonexistent_account])
.await
.expect("RPC call failed");
assert_eq!(nonce, 0, "Nonexistent account should have nonce 0");
}
#[tokio::test(flavor = "multi_thread")]
async fn account_next_index_invalid_address_returns_error() {
let ctx = TestContext::for_rpc_server().await;
let client = WsClientBuilder::default()
.build(&ctx.ws_url())
.await
.expect("Failed to connect");
let result: Result<u32, _> = client
.request("system_accountNextIndex", rpc_params!["not_a_valid_address"])
.await;
assert!(result.is_err(), "Invalid SS58 address should return an error");
}
}