quantus_cli/chain/
client.rs1use crate::{error::QuantusError, log_verbose};
7use jsonrpsee::ws_client::{WsClient, WsClientBuilder};
8use qp_dilithium_crypto::types::DilithiumSignatureScheme;
9use qp_poseidon::PoseidonHasher;
10use sp_core::{crypto::AccountId32, ByteArray};
11use sp_runtime::{traits::IdentifyAccount, MultiAddress};
12use std::{sync::Arc, time::Duration};
13use subxt::{
14 backend::rpc::RpcClient,
15 config::{substrate::SubstrateHeader, DefaultExtrinsicParams},
16 Config, OnlineClient,
17};
18use subxt_metadata::Metadata as SubxtMetadata;
19
20#[derive(Debug, Clone, Copy)]
21pub struct SubxtPoseidonHasher;
22
23impl subxt::config::Hasher for SubxtPoseidonHasher {
24 type Output = sp_core::H256;
25
26 fn new(_metadata: &SubxtMetadata) -> Self {
27 SubxtPoseidonHasher
28 }
29
30 fn hash(&self, bytes: &[u8]) -> Self::Output {
31 <PoseidonHasher as sp_runtime::traits::Hash>::hash(bytes)
32 }
33}
34
35pub enum ChainConfig {}
37impl Config for ChainConfig {
38 type AccountId = AccountId32;
39 type Address = MultiAddress<Self::AccountId, ()>;
40 type Signature = DilithiumSignatureScheme;
41 type Hasher = SubxtPoseidonHasher;
42 type Header = SubstrateHeader<u32, SubxtPoseidonHasher>;
43 type AssetId = u32;
44 type ExtrinsicParams = DefaultExtrinsicParams<Self>;
45}
46
47#[derive(Clone)]
49pub struct QuantusClient {
50 client: OnlineClient<ChainConfig>,
51 rpc_client: Arc<WsClient>,
52 node_url: String,
53}
54
55impl QuantusClient {
56 pub async fn new(node_url: &str) -> crate::error::Result<Self> {
58 log_verbose!("🔗 Connecting to Quantus node: {}", node_url);
59
60 if !node_url.starts_with("ws://") && !node_url.starts_with("wss://") {
62 return Err(QuantusError::NetworkError(format!(
63 "Invalid WebSocket URL: '{node_url}'. URL must start with 'ws://' (unsecured) or 'wss://' (secured)"
64 )));
65 }
66
67 if node_url.starts_with("ws://") &&
69 (node_url.contains("a.i.res.fm") || node_url.contains("a.t.res.fm"))
70 {
71 log_verbose!(
72 "💡 Hint: Remote nodes typically require secure WebSocket connections (wss://)"
73 );
74 }
75
76 let ws_client = WsClientBuilder::default()
78 .connection_timeout(Duration::from_secs(30))
81 .request_timeout(Duration::from_secs(30))
82 .build(node_url)
83 .await
84 .map_err(|e| {
85 let error_str = format!("{e:?}");
87 let error_msg = if error_str.contains("TimedOut") || error_str.contains("timed out") {
88 if node_url.starts_with("ws://") && (node_url.contains("a.i.res.fm") || node_url.contains("a.t.res.fm")) {
89 format!(
90 "Connection timed out. This remote node requires secure WebSocket connections (wss://). Try using 'wss://{}' instead of 'ws://{}'",
91 node_url.strip_prefix("ws://").unwrap_or(node_url),
92 node_url.strip_prefix("ws://").unwrap_or(node_url)
93 )
94 } else {
95 format!("Connection timed out. Please check if the node is running and accessible at: {node_url}")
96 }
97 } else if error_str.contains("HTTP") {
98 format!("HTTP error: {error_str}. This might indicate the node doesn't support WebSocket connections")
99 } else {
100 format!("Failed to create RPC client: {error_str}")
101 };
102 QuantusError::NetworkError(error_msg)
103 })?;
104
105 let ws_client = Arc::new(ws_client);
107
108 let rpc_client = RpcClient::new(ws_client.clone());
110
111 let client = OnlineClient::<ChainConfig>::from_rpc_client(rpc_client).await?;
113
114 log_verbose!("✅ Connected to Quantus node successfully!");
115
116 Ok(QuantusClient { client, rpc_client: ws_client, node_url: node_url.to_string() })
117 }
118
119 pub fn client(&self) -> &OnlineClient<ChainConfig> {
121 &self.client
122 }
123
124 pub fn node_url(&self) -> &str {
126 &self.node_url
127 }
128
129 pub fn rpc_client(&self) -> &WsClient {
131 &self.rpc_client
132 }
133
134 pub async fn get_latest_block(&self) -> crate::error::Result<subxt::utils::H256> {
137 log_verbose!("🔍 Fetching latest block hash via RPC...");
138
139 use jsonrpsee::core::client::ClientT;
141 let latest_hash: subxt::utils::H256 = self
142 .rpc_client
143 .request::<subxt::utils::H256, [(); 0]>("chain_getBlockHash", [])
144 .await
145 .map_err(|e| {
146 crate::error::QuantusError::NetworkError(format!(
147 "Failed to fetch latest block hash: {e:?}"
148 ))
149 })?;
150
151 log_verbose!("📦 Latest block hash: {:?}", latest_hash);
152 Ok(latest_hash)
153 }
154
155 pub async fn get_account_nonce_from_best_block(
158 &self,
159 account_id: &AccountId32,
160 ) -> crate::error::Result<u64> {
161 log_verbose!("🔍 Fetching account nonce from best block via RPC...");
162
163 let latest_block_hash = self.get_latest_block().await?;
165 log_verbose!("📦 Latest block hash for nonce query: {:?}", latest_block_hash);
166
167 let account_bytes: [u8; 32] = *account_id.as_ref();
169 let subxt_account_id = subxt::utils::AccountId32::from(account_bytes);
170
171 use crate::chain::quantus_subxt::api;
173 let storage_addr = api::storage().system().account(subxt_account_id);
174
175 let storage_at = self.client.storage().at(latest_block_hash);
176
177 let account_info = storage_at.fetch_or_default(&storage_addr).await?;
178
179 log_verbose!("✅ Nonce from best block: {}", account_info.nonce);
180 Ok(account_info.nonce as u64)
181 }
182
183 pub async fn get_genesis_hash(&self) -> crate::error::Result<subxt::utils::H256> {
185 log_verbose!("🔍 Fetching genesis hash via RPC...");
186
187 use jsonrpsee::core::client::ClientT;
188 let genesis_hash: subxt::utils::H256 = self
189 .rpc_client
190 .request::<subxt::utils::H256, [u32; 1]>("chain_getBlockHash", [0u32])
191 .await
192 .map_err(|e| {
193 crate::error::QuantusError::NetworkError(format!(
194 "Failed to fetch genesis hash: {e:?}"
195 ))
196 })?;
197
198 log_verbose!("🧬 Genesis hash: {:?}", genesis_hash);
199 Ok(genesis_hash)
200 }
201
202 pub async fn get_runtime_version(&self) -> crate::error::Result<(u32, u32)> {
204 log_verbose!("🔍 Fetching runtime version via RPC...");
205
206 use jsonrpsee::core::client::ClientT;
207 let runtime_version: serde_json::Value = self
208 .rpc_client
209 .request::<serde_json::Value, [(); 0]>("state_getRuntimeVersion", [])
210 .await
211 .map_err(|e| {
212 crate::error::QuantusError::NetworkError(format!(
213 "Failed to fetch runtime version: {e:?}"
214 ))
215 })?;
216
217 let spec_version = runtime_version["specVersion"].as_u64().ok_or_else(|| {
218 crate::error::QuantusError::NetworkError("Failed to parse spec version".to_string())
219 })? as u32;
220
221 let transaction_version =
222 runtime_version["transactionVersion"].as_u64().ok_or_else(|| {
223 crate::error::QuantusError::NetworkError(
224 "Failed to parse transaction version".to_string(),
225 )
226 })? as u32;
227
228 log_verbose!("🔧 Runtime version: spec={}, tx={}", spec_version, transaction_version);
229 Ok((spec_version, transaction_version))
230 }
231
232 pub async fn get_runtime_hash(&self) -> crate::error::Result<Option<String>> {
234 log_verbose!("🔍 Fetching runtime hash via RPC...");
235
236 use jsonrpsee::core::client::ClientT;
237
238 let possible_calls = ["state_getRuntimeHash", "state_getRuntime", "chain_getRuntimeHash"];
240
241 for call_name in &possible_calls {
242 match self.rpc_client.request::<serde_json::Value, [(); 0]>(call_name, []).await {
243 Ok(result) => {
244 log_verbose!("✅ Found runtime hash via {}", call_name);
245 if let Some(hash) = result.as_str() {
246 return Ok(Some(hash.to_string()));
247 } else if let Some(hash_obj) = result.get("hash") {
248 if let Some(hash) = hash_obj.as_str() {
249 return Ok(Some(hash.to_string()));
250 }
251 }
252 },
253 Err(_e) => {
254 log_verbose!("❌ {} failed: {:?}", call_name, _e);
255 },
256 }
257 }
258
259 log_verbose!("⚠️ No runtime hash RPC call available");
260 Ok(None)
261 }
262}
263
264impl subxt::tx::Signer<ChainConfig> for qp_dilithium_crypto::types::DilithiumPair {
266 fn account_id(&self) -> <ChainConfig as Config>::AccountId {
267 let resonance_public =
268 qp_dilithium_crypto::types::DilithiumPublic::from_slice(self.public.as_slice())
269 .expect("Invalid public key");
270 <qp_dilithium_crypto::types::DilithiumPublic as IdentifyAccount>::into_account(
271 resonance_public,
272 )
273 }
274
275 fn sign(&self, signer_payload: &[u8]) -> <ChainConfig as Config>::Signature {
276 let signature_with_public =
280 <qp_dilithium_crypto::types::DilithiumPair as sp_core::Pair>::sign(
281 self,
282 signer_payload,
283 );
284 qp_dilithium_crypto::types::DilithiumSignatureScheme::Dilithium(signature_with_public)
285 }
286}