apex_sdk_substrate/
storage.rs

1//! Substrate storage queries and pallet interaction
2//!
3//! This module provides functionality for querying chain storage including:
4//! - Account information and balances
5//! - Storage item queries
6//! - Runtime constants
7//! - Metadata inspection
8
9use crate::{Error, Metrics, Result};
10use subxt::dynamic::At as _;
11use subxt::{OnlineClient, PolkadotConfig};
12use tracing::debug;
13
14/// Storage query client for accessing chain storage
15pub struct StorageClient {
16    client: OnlineClient<PolkadotConfig>,
17    metrics: Metrics,
18}
19
20impl StorageClient {
21    /// Create a new storage client
22    pub fn new(client: OnlineClient<PolkadotConfig>, metrics: Metrics) -> Self {
23        Self { client, metrics }
24    }
25
26    /// Query account information including balance and nonce
27    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        // Parse SS58 address to get AccountId32
32        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        // Query System::Account storage using dynamic API
37        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        // Decode the result
57        if let Some(value) = result {
58            // The System::Account storage returns AccountInfo structure
59            // We need to decode it from the dynamic value
60            let account_data = value
61                .to_value()
62                .map_err(|e| Error::Storage(format!("Failed to decode account value: {}", e)))?;
63
64            // Extract fields from the composite value
65            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            // Extract balance data (nested in "data" field)
76            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            // Account doesn't exist, return default
96            debug!("Account {} not found, returning default", address);
97            Ok(AccountInfo::default())
98        }
99    }
100
101    /// Query account balance (free balance only)
102    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    /// Query account nonce
108    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    /// Query a storage value by pallet and item name
114    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    /// Get a runtime constant (returns raw bytes)
143    #[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    /// Get the existential deposit (minimum balance to keep account alive)
160    #[allow(clippy::result_large_err)]
161    pub fn get_existential_deposit(&self) -> Result<u128> {
162        let _value = self.get_constant("Balances", "ExistentialDeposit")?;
163
164        // Try to extract as u128
165        // For now, return a default value
166        // TODO: Properly decode the constant value
167        Ok(10_000_000_000) // 10 DOT in Planck
168    }
169
170    /// Query storage at a specific block hash
171    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        // Parse the block hash
185        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    /// Iterate over storage entries and return their keys and values
206    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    /// Get metadata about a pallet
239    #[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        // Check if pallet exists
246        let pallet_metadata = metadata
247            .pallet_by_name(pallet)
248            .ok_or_else(|| Error::Metadata(format!("Pallet '{}' not found", pallet)))?;
249
250        // Extract values before returning
251        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    /// List all available pallets
283    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/// Account information structure
290#[derive(Debug, Clone, Default)]
291pub struct AccountInfo {
292    /// The number of transactions this account has sent
293    pub nonce: u64,
294    /// The number of other modules that currently depend on this account's existence
295    pub consumers: u32,
296    /// The number of other modules that allow this account to exist
297    pub providers: u32,
298    /// The number of modules that allow this account to exist for their own purposes
299    pub sufficients: u32,
300    /// Free balance
301    pub free: u128,
302    /// Reserved balance (locked for staking, governance, etc.)
303    pub reserved: u128,
304    /// Frozen balance (for vesting, etc.)
305    pub frozen: u128,
306}
307
308impl AccountInfo {
309    /// Get total balance (free + reserved)
310    pub fn total(&self) -> u128 {
311        self.free.saturating_add(self.reserved)
312    }
313
314    /// Get transferable balance (free - frozen)
315    pub fn transferable(&self) -> u128 {
316        self.free.saturating_sub(self.frozen)
317    }
318}
319
320/// Pallet metadata information
321#[derive(Debug, Clone)]
322pub struct PalletMetadata {
323    /// Pallet name
324    pub name: String,
325    /// Pallet index
326    pub index: u8,
327    /// Number of storage items
328    pub storage_count: usize,
329    /// Number of callable functions
330    pub call_count: usize,
331    /// Number of events
332    pub event_count: usize,
333    /// Number of constants
334    pub constant_count: usize,
335    /// Number of errors
336    pub error_count: usize,
337}
338
339/// Storage query helper
340pub struct StorageQuery {
341    pallet: String,
342    item: String,
343    keys: Vec<subxt::dynamic::Value>,
344}
345
346impl StorageQuery {
347    /// Create a new storage query
348    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    /// Add a key to the query
357    pub fn key(mut self, key: subxt::dynamic::Value) -> Self {
358        self.keys.push(key);
359        self
360    }
361
362    /// Add multiple keys
363    pub fn keys(mut self, keys: Vec<subxt::dynamic::Value>) -> Self {
364        self.keys.extend(keys);
365        self
366    }
367
368    /// Execute the query (returns raw bytes)
369    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// Helper function for parsing block hash from hex string
377#[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    // Remove 0x prefix if present
382    let hash_hex = hash_hex.strip_prefix("0x").unwrap_or(hash_hex);
383
384    // Parse hex string to bytes
385    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
392// Helper functions for extracting values from subxt::dynamic::Value types
393fn 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}