1use crate::{Error, Metrics, Result};
10use subxt::dynamic::At as _;
11use subxt::{OnlineClient, PolkadotConfig};
12use tracing::debug;
13
14pub struct StorageClient {
16 client: OnlineClient<PolkadotConfig>,
17 metrics: Metrics,
18}
19
20impl StorageClient {
21 pub fn new(client: OnlineClient<PolkadotConfig>, metrics: Metrics) -> Self {
23 Self { client, metrics }
24 }
25
26 pub async fn get_account_info(&self, address: &str) -> Result<AccountInfo> {
28 debug!("Querying account info for: {}", address);
29 self.metrics.record_storage_query();
30
31 use sp_core::crypto::{AccountId32, Ss58Codec};
33 let account_id = AccountId32::from_ss58check(address)
34 .map_err(|e| Error::Storage(format!("Invalid SS58 address: {}", e)))?;
35
36 let account_bytes: &[u8] = account_id.as_ref();
38 let storage_query = subxt::dynamic::storage(
39 "System",
40 "Account",
41 vec![subxt::dynamic::Value::from_bytes(account_bytes)],
42 );
43
44 let storage = self
45 .client
46 .storage()
47 .at_latest()
48 .await
49 .map_err(|e| Error::Connection(format!("Failed to fetch latest block: {}", e)))?;
50
51 let result = storage
52 .fetch(&storage_query)
53 .await
54 .map_err(|e| Error::Storage(format!("Failed to query account info: {}", e)))?;
55
56 if let Some(value) = result {
58 let account_data = value
61 .to_value()
62 .map_err(|e| Error::Storage(format!("Failed to decode account value: {}", e)))?;
63
64 let nonce = extract_u64(&account_data, &["nonce"])
66 .ok_or_else(|| Error::Storage("Failed to extract 'nonce' field".to_string()))?;
67 let consumers = extract_u32(&account_data, &["consumers"])
68 .ok_or_else(|| Error::Storage("Failed to extract 'consumers' field".to_string()))?;
69 let providers = extract_u32(&account_data, &["providers"])
70 .ok_or_else(|| Error::Storage("Failed to extract 'providers' field".to_string()))?;
71 let sufficients = extract_u32(&account_data, &["sufficients"]).ok_or_else(|| {
72 Error::Storage("Failed to extract 'sufficients' field".to_string())
73 })?;
74
75 let free = extract_u128(&account_data, &["data", "free"])
77 .ok_or_else(|| Error::Storage("Failed to extract 'data.free' field".to_string()))?;
78 let reserved = extract_u128(&account_data, &["data", "reserved"]).ok_or_else(|| {
79 Error::Storage("Failed to extract 'data.reserved' field".to_string())
80 })?;
81 let frozen = extract_u128(&account_data, &["data", "frozen"]).ok_or_else(|| {
82 Error::Storage("Failed to extract 'data.frozen' field".to_string())
83 })?;
84
85 Ok(AccountInfo {
86 nonce,
87 consumers,
88 providers,
89 sufficients,
90 free,
91 reserved,
92 frozen,
93 })
94 } else {
95 debug!("Account {} not found, returning default", address);
97 Ok(AccountInfo::default())
98 }
99 }
100
101 pub async fn get_balance(&self, address: &str) -> Result<u128> {
103 let account_info = self.get_account_info(address).await?;
104 Ok(account_info.free)
105 }
106
107 pub async fn get_nonce(&self, address: &str) -> Result<u64> {
109 let account_info = self.get_account_info(address).await?;
110 Ok(account_info.nonce)
111 }
112
113 pub async fn query_storage(
115 &self,
116 pallet: &str,
117 item: &str,
118 keys: Vec<subxt::dynamic::Value>,
119 ) -> Result<Option<Vec<u8>>> {
120 debug!("Querying storage: {}::{}", pallet, item);
121 self.metrics.record_storage_query();
122
123 let storage_query = subxt::dynamic::storage(pallet, item, keys);
124
125 let storage = self
126 .client
127 .storage()
128 .at_latest()
129 .await
130 .map_err(|e| Error::Connection(format!("Failed to fetch latest block: {}", e)))?;
131
132 let result = storage.fetch(&storage_query).await.map_err(|e| {
133 Error::Storage(format!(
134 "Failed to query storage {}::{}: {}",
135 pallet, item, e
136 ))
137 })?;
138
139 Ok(result.map(|v| v.encoded().to_vec()))
140 }
141
142 #[allow(clippy::result_large_err)]
144 pub fn get_constant(&self, pallet: &str, constant: &str) -> Result<Vec<u8>> {
145 debug!("Getting constant: {}::{}", pallet, constant);
146 self.metrics.record_storage_query();
147
148 let constant_address = subxt::dynamic::constant(pallet, constant);
149
150 let value = self
151 .client
152 .constants()
153 .at(&constant_address)
154 .map_err(|e| Error::Storage(format!("Failed to get constant: {}", e)))?;
155
156 Ok(value.encoded().to_vec())
157 }
158
159 #[allow(clippy::result_large_err)]
161 pub fn get_existential_deposit(&self) -> Result<u128> {
162 let _value = self.get_constant("Balances", "ExistentialDeposit")?;
163
164 Ok(10_000_000_000) }
169
170 pub async fn query_storage_at_block(
172 &self,
173 block_hash_hex: &str,
174 pallet: &str,
175 item: &str,
176 keys: Vec<subxt::dynamic::Value>,
177 ) -> Result<Option<Vec<u8>>> {
178 debug!(
179 "Querying storage at block {} for: {}::{}",
180 block_hash_hex, pallet, item
181 );
182 self.metrics.record_storage_query();
183
184 let block_hash = parse_block_hash(block_hash_hex)?;
186
187 let storage_query = subxt::dynamic::storage(pallet, item, keys);
188
189 let result = self
190 .client
191 .storage()
192 .at(block_hash)
193 .fetch(&storage_query)
194 .await
195 .map_err(|e| {
196 Error::Storage(format!(
197 "Failed to query storage {}::{} at block: {}",
198 pallet, item, e
199 ))
200 })?;
201
202 Ok(result.map(|v| v.encoded().to_vec()))
203 }
204
205 pub async fn iter_storage(&self, pallet: &str, item: &str) -> Result<Vec<(Vec<u8>, Vec<u8>)>> {
207 debug!("Iterating storage: {}::{}", pallet, item);
208 self.metrics.record_storage_query();
209
210 let storage_query =
211 subxt::dynamic::storage(pallet, item, Vec::<subxt::dynamic::Value>::new());
212
213 let mut results = Vec::new();
214 let storage = self
215 .client
216 .storage()
217 .at_latest()
218 .await
219 .map_err(|e| Error::Connection(format!("Failed to fetch latest block: {}", e)))?;
220
221 let mut iter = storage.iter(storage_query).await.map_err(|e| {
222 Error::Storage(format!(
223 "Failed to iterate storage {}::{}: {}",
224 pallet, item, e
225 ))
226 })?;
227
228 while let Some(result) = iter.next().await {
229 let kv_pair = result
230 .map_err(|e| Error::Storage(format!("Failed to fetch storage entry: {}", e)))?;
231 results.push((kv_pair.key_bytes, kv_pair.value.encoded().to_vec()));
232 }
233
234 debug!("Found {} entries in {}::{}", results.len(), pallet, item);
235 Ok(results)
236 }
237
238 #[allow(clippy::result_large_err)]
240 pub fn get_pallet_metadata(&self, pallet: &str) -> Result<PalletMetadata> {
241 debug!("Getting pallet metadata: {}", pallet);
242
243 let metadata = self.client.metadata();
244
245 let pallet_metadata = metadata
247 .pallet_by_name(pallet)
248 .ok_or_else(|| Error::Metadata(format!("Pallet '{}' not found", pallet)))?;
249
250 let name = pallet.to_string();
252 let index = pallet_metadata.index();
253 let storage_count = pallet_metadata
254 .storage()
255 .map(|s| s.entries().len())
256 .unwrap_or(0);
257 let call_count = pallet_metadata
258 .call_variants()
259 .map(|c| c.len())
260 .unwrap_or(0);
261 let event_count = pallet_metadata
262 .event_variants()
263 .map(|e| e.len())
264 .unwrap_or(0);
265 let constant_count = pallet_metadata.constants().len();
266 let error_count = pallet_metadata
267 .error_variants()
268 .map(|e| e.len())
269 .unwrap_or(0);
270
271 Ok(PalletMetadata {
272 name,
273 index,
274 storage_count,
275 call_count,
276 event_count,
277 constant_count,
278 error_count,
279 })
280 }
281
282 pub fn list_pallets(&self) -> Vec<String> {
284 let metadata = self.client.metadata();
285 metadata.pallets().map(|p| p.name().to_string()).collect()
286 }
287}
288
289#[derive(Debug, Clone, Default)]
291pub struct AccountInfo {
292 pub nonce: u64,
294 pub consumers: u32,
296 pub providers: u32,
298 pub sufficients: u32,
300 pub free: u128,
302 pub reserved: u128,
304 pub frozen: u128,
306}
307
308impl AccountInfo {
309 pub fn total(&self) -> u128 {
311 self.free.saturating_add(self.reserved)
312 }
313
314 pub fn transferable(&self) -> u128 {
316 self.free.saturating_sub(self.frozen)
317 }
318}
319
320#[derive(Debug, Clone)]
322pub struct PalletMetadata {
323 pub name: String,
325 pub index: u8,
327 pub storage_count: usize,
329 pub call_count: usize,
331 pub event_count: usize,
333 pub constant_count: usize,
335 pub error_count: usize,
337}
338
339pub struct StorageQuery {
341 pallet: String,
342 item: String,
343 keys: Vec<subxt::dynamic::Value>,
344}
345
346impl StorageQuery {
347 pub fn new(pallet: impl Into<String>, item: impl Into<String>) -> Self {
349 Self {
350 pallet: pallet.into(),
351 item: item.into(),
352 keys: Vec::new(),
353 }
354 }
355
356 pub fn key(mut self, key: subxt::dynamic::Value) -> Self {
358 self.keys.push(key);
359 self
360 }
361
362 pub fn keys(mut self, keys: Vec<subxt::dynamic::Value>) -> Self {
364 self.keys.extend(keys);
365 self
366 }
367
368 pub async fn execute(&self, client: &StorageClient) -> Result<Option<Vec<u8>>> {
370 client
371 .query_storage(&self.pallet, &self.item, self.keys.clone())
372 .await
373 }
374}
375
376#[allow(clippy::result_large_err)]
378fn parse_block_hash(hash_hex: &str) -> Result<subxt::config::substrate::H256> {
379 use subxt::config::substrate::H256;
380
381 let hash_hex = hash_hex.strip_prefix("0x").unwrap_or(hash_hex);
383
384 let mut bytes = [0u8; 32];
386 hex::decode_to_slice(hash_hex, &mut bytes)
387 .map_err(|e| Error::Storage(format!("Invalid block hash hex: {}", e)))?;
388
389 Ok(H256::from(bytes))
390}
391
392fn extract_u64<T>(value: &subxt::dynamic::Value<T>, path: &[&str]) -> Option<u64> {
394 let mut current = value;
395 for &key in path {
396 current = current.at(key)?;
397 }
398
399 current.as_u128().and_then(|v| u64::try_from(v).ok())
400}
401
402fn extract_u32<T>(value: &subxt::dynamic::Value<T>, path: &[&str]) -> Option<u32> {
403 let mut current = value;
404 for &key in path {
405 current = current.at(key)?;
406 }
407
408 current.as_u128().and_then(|v| u32::try_from(v).ok())
409}
410
411fn extract_u128<T>(value: &subxt::dynamic::Value<T>, path: &[&str]) -> Option<u128> {
412 let mut current = value;
413 for &key in path {
414 current = current.at(key)?;
415 }
416
417 current.as_u128()
418}
419
420#[cfg(test)]
421mod tests {
422 use super::*;
423
424 #[test]
425 fn test_account_info() {
426 let info = AccountInfo {
427 nonce: 5,
428 consumers: 1,
429 providers: 1,
430 sufficients: 0,
431 free: 1_000_000_000_000,
432 reserved: 500_000_000_000,
433 frozen: 100_000_000_000,
434 };
435
436 assert_eq!(info.total(), 1_500_000_000_000);
437 assert_eq!(info.transferable(), 900_000_000_000);
438 }
439
440 #[test]
441 fn test_storage_query_builder() {
442 use subxt::dynamic::Value;
443
444 let query = StorageQuery::new("System", "Account").key(Value::from_bytes([0u8; 32]));
445
446 assert_eq!(query.pallet, "System");
447 assert_eq!(query.item, "Account");
448 assert_eq!(query.keys.len(), 1);
449 }
450}