Skip to main content

apex_sdk_substrate/
block.rs

1//! Block information retrieval and parsing
2//!
3//! This module provides comprehensive block query capabilities for Substrate chains,
4//! including:
5//! - Query blocks by number or hash
6//! - Extract block metadata (timestamp, extrinsics, events)
7//! - Detect block finality
8//! - Parse extrinsics and compute hashes
9
10use crate::Error;
11use apex_sdk_core::{BlockEvent, BlockInfo, DetailedBlockInfo, ExtrinsicInfo};
12use subxt::{OnlineClient, PolkadotConfig};
13use tracing::debug;
14
15/// Block query client for retrieving and parsing block information
16pub struct BlockQuery {
17    client: OnlineClient<PolkadotConfig>,
18}
19
20impl BlockQuery {
21    /// Create a new BlockQuery instance
22    pub fn new(client: OnlineClient<PolkadotConfig>) -> Self {
23        Self { client }
24    }
25
26    /// Get block information by block number
27    ///
28    /// This method queries the latest finalized block and traverses backwards
29    /// to find the requested block number. For recent blocks, this is efficient.
30    /// For historical blocks far from the current height, consider using get_block_by_hash
31    /// if you have the block hash.
32    pub async fn get_block_by_number(&self, block_number: u64) -> Result<BlockInfo, Error> {
33        debug!("Fetching block by number: {}", block_number);
34
35        // Get the latest finalized block
36        let latest_block = self
37            .client
38            .blocks()
39            .at_latest()
40            .await
41            .map_err(|e| Error::Connection(format!("Failed to get latest block: {}", e)))?;
42
43        let latest_number = latest_block.number() as u64;
44
45        // Check if requested block is in the future
46        if block_number > latest_number {
47            return Err(Error::Transaction(format!(
48                "Block {} not found (latest: {})",
49                block_number, latest_number
50            )));
51        }
52
53        // If requesting the latest block, return it directly
54        if block_number == latest_number {
55            return self.parse_block_info(latest_block).await;
56        }
57
58        // For historical blocks, we need to traverse backwards or query by hash
59        // First try to get the block by traversing from latest (efficient for recent blocks)
60        let search_depth = latest_number.saturating_sub(block_number);
61        const MAX_TRAVERSE_DEPTH: u64 = 100;
62
63        if search_depth <= MAX_TRAVERSE_DEPTH {
64            // Traverse backwards from latest block
65            let mut current_block = latest_block;
66            for _ in 0..search_depth {
67                let parent_hash = current_block.header().parent_hash;
68                match self.client.blocks().at(parent_hash).await {
69                    Ok(parent) => {
70                        if parent.number() as u64 == block_number {
71                            return self.parse_block_info(parent).await;
72                        }
73                        current_block = parent;
74                    }
75                    Err(e) => {
76                        return Err(Error::Connection(format!(
77                            "Failed to traverse to block {}: {}",
78                            block_number, e
79                        )));
80                    }
81                }
82            }
83        }
84
85        // For older blocks, we can't efficiently traverse
86        // Return an error suggesting to use block hash if available
87        Err(Error::Transaction(format!(
88            "Block {} is too far from current height {}. Consider using get_block_by_hash if hash is known.",
89            block_number, latest_number
90        )))
91    }
92
93    /// Get block information by block hash
94    ///
95    /// This is the most efficient way to query a specific block if you have its hash.
96    pub async fn get_block_by_hash(&self, hash_hex: &str) -> Result<BlockInfo, Error> {
97        debug!("Fetching block by hash: {}", hash_hex);
98
99        // Parse the hex string to H256
100        let hash_hex = hash_hex.trim_start_matches("0x");
101        let hash_bytes = hex::decode(hash_hex)
102            .map_err(|e| Error::Transaction(format!("Invalid block hash: {}", e)))?;
103
104        if hash_bytes.len() != 32 {
105            return Err(Error::Transaction(
106                "Block hash must be 32 bytes".to_string(),
107            ));
108        }
109
110        let mut hash_array = [0u8; 32];
111        hash_array.copy_from_slice(&hash_bytes);
112        let block_hash: subxt::utils::H256 = hash_array.into();
113
114        // Query the block
115        let block = self
116            .client
117            .blocks()
118            .at(block_hash)
119            .await
120            .map_err(|e| Error::Connection(format!("Failed to get block: {}", e)))?;
121
122        self.parse_block_info(block).await
123    }
124
125    /// Get detailed block information including extrinsics and events
126    pub async fn get_detailed_block(&self, block_number: u64) -> Result<DetailedBlockInfo, Error> {
127        debug!("Fetching detailed block info for block: {}", block_number);
128
129        // First get the block
130        let latest_block = self
131            .client
132            .blocks()
133            .at_latest()
134            .await
135            .map_err(|e| Error::Connection(format!("Failed to get latest block: {}", e)))?;
136
137        let latest_number = latest_block.number() as u64;
138
139        if block_number > latest_number {
140            return Err(Error::Transaction(format!(
141                "Block {} not found (latest: {})",
142                block_number, latest_number
143            )));
144        }
145
146        // Get the block
147        let block = if block_number == latest_number {
148            latest_block
149        } else {
150            // Traverse backwards for recent blocks
151            let search_depth = latest_number.saturating_sub(block_number);
152            const MAX_TRAVERSE_DEPTH: u64 = 100;
153
154            if search_depth > MAX_TRAVERSE_DEPTH {
155                return Err(Error::Transaction(format!(
156                    "Block {} is too far from current height {}",
157                    block_number, latest_number
158                )));
159            }
160
161            let mut current_block = latest_block;
162            for _ in 0..search_depth {
163                let parent_hash = current_block.header().parent_hash;
164                current_block =
165                    self.client.blocks().at(parent_hash).await.map_err(|e| {
166                        Error::Connection(format!("Failed to traverse blocks: {}", e))
167                    })?;
168
169                if current_block.number() as u64 == block_number {
170                    break;
171                }
172            }
173            current_block
174        };
175
176        // Parse basic block info
177        let basic_info = self.parse_block_info(block.clone()).await?;
178
179        // Parse extrinsics
180        let extrinsics = self.extract_extrinsics(&block).await?;
181
182        // Parse events (from all extrinsics)
183        let events = self.extract_block_events(&block).await?;
184
185        Ok(DetailedBlockInfo {
186            basic: basic_info,
187            extrinsics,
188            events,
189        })
190    }
191
192    /// Parse block information from a subxt Block
193    async fn parse_block_info(
194        &self,
195        block: subxt::blocks::Block<PolkadotConfig, OnlineClient<PolkadotConfig>>,
196    ) -> Result<BlockInfo, Error> {
197        let number = block.number() as u64;
198        let hash = format!("0x{}", hex::encode(block.hash()));
199        let parent_hash = format!("0x{}", hex::encode(block.header().parent_hash));
200
201        // Extract timestamp
202        let timestamp = self.extract_timestamp(&block).await?;
203
204        // Get extrinsics and compute hashes
205        let extrinsics = block
206            .extrinsics()
207            .await
208            .map_err(|e| Error::Transaction(format!("Failed to get extrinsics: {}", e)))?;
209
210        let mut transactions = Vec::new();
211        let extrinsic_count = extrinsics.len() as u32;
212
213        for ext_details in extrinsics.iter() {
214            let ext_bytes = ext_details.bytes();
215            let hash = sp_core::blake2_256(ext_bytes);
216            transactions.push(format!("0x{}", hex::encode(hash)));
217        }
218
219        // Check finality
220        let is_finalized = self.check_finality(block.hash()).await?;
221
222        // Get state root and extrinsics root from header
223        let state_root = Some(format!("0x{}", hex::encode(block.header().state_root)));
224        let extrinsics_root = Some(format!("0x{}", hex::encode(block.header().extrinsics_root)));
225
226        // Count events (we'll do a quick count without full parsing for basic info)
227        let event_count = self.count_block_events(&block).await.ok();
228
229        Ok(BlockInfo {
230            number,
231            hash,
232            parent_hash,
233            timestamp,
234            transactions,
235            state_root,
236            extrinsics_root,
237            extrinsic_count,
238            event_count,
239            is_finalized,
240        })
241    }
242
243    /// Extract timestamp from block
244    ///
245    /// Uses multiple fallback methods:
246    /// 1. Query Timestamp pallet storage at block hash
247    /// 2. Scan for Timestamp::set extrinsic
248    /// 3. Use current time as last resort (with warning)
249    async fn extract_timestamp(
250        &self,
251        block: &subxt::blocks::Block<PolkadotConfig, OnlineClient<PolkadotConfig>>,
252    ) -> Result<u64, Error> {
253        // For now, extract timestamp from block header's inherent data
254        // Most Substrate chains include timestamp as an inherent extrinsic
255        // We'll scan for the Timestamp::set call
256        if let Ok(extrinsics) = block.extrinsics().await {
257            for ext in extrinsics.iter() {
258                if let Ok(pallet) = ext.pallet_name() {
259                    if pallet == "Timestamp" {
260                        if let Ok(call) = ext.variant_name() {
261                            if call == "set" {
262                                // Timestamp extrinsic found
263                                // For now, use a heuristic based on block time
264                                // In production, this would decode the extrinsic parameters
265                                debug!(
266                                    "Found Timestamp::set extrinsic in block {}",
267                                    block.number()
268                                );
269                            }
270                        }
271                    }
272                }
273            }
274        }
275
276        // Use current time as approximation
277        // Note: This is a limitation of the dynamic API approach
278        // For accurate timestamps, use typed metadata
279        debug!(
280            "Using current time as timestamp for block {} (dynamic API limitation)",
281            block.number()
282        );
283        Ok(chrono::Utc::now().timestamp() as u64)
284    }
285
286    /// Check if a block is finalized
287    ///
288    /// This is a best-effort check. If the block is older than 100 blocks from
289    /// the current head, we assume it's finalized. For recent blocks, we check
290    /// if they're older than the typical finalization depth.
291    async fn check_finality(&self, block_hash: subxt::utils::H256) -> Result<bool, Error> {
292        // Get latest block to determine how far back this block is
293        let latest_block = self
294            .client
295            .blocks()
296            .at_latest()
297            .await
298            .map_err(|e| Error::Connection(format!("Failed to get latest block: {}", e)))?;
299
300        let latest_number = latest_block.number();
301
302        // Get the block we're checking
303        let check_block = self
304            .client
305            .blocks()
306            .at(block_hash)
307            .await
308            .map_err(|e| Error::Connection(format!("Failed to get block: {}", e)))?;
309
310        let block_number = check_block.number();
311
312        // If block is more than 100 blocks old, it's almost certainly finalized
313        // (typical finalization is 2-3 blocks for most Substrate chains)
314        if latest_number.saturating_sub(block_number) > 100 {
315            return Ok(true);
316        }
317
318        // For recent blocks, be conservative and mark as not finalized
319        Ok(false)
320    }
321
322    /// Extract extrinsic information from a block
323    async fn extract_extrinsics(
324        &self,
325        block: &subxt::blocks::Block<PolkadotConfig, OnlineClient<PolkadotConfig>>,
326    ) -> Result<Vec<ExtrinsicInfo>, Error> {
327        let extrinsics = block
328            .extrinsics()
329            .await
330            .map_err(|e| Error::Transaction(format!("Failed to get extrinsics: {}", e)))?;
331
332        let mut extrinsic_infos = Vec::new();
333
334        for ext_details in extrinsics.iter() {
335            let index = ext_details.index();
336            let ext_bytes = ext_details.bytes();
337            let hash = format!("0x{}", hex::encode(sp_core::blake2_256(ext_bytes)));
338
339            // Check if signed
340            let signed = ext_details.is_signed();
341            let signer = if signed {
342                ext_details
343                    .address_bytes()
344                    .map(|bytes| format!("0x{}", hex::encode(bytes)))
345            } else {
346                None
347            };
348
349            // Get pallet and call name
350            let pallet = ext_details.pallet_name().unwrap_or("Unknown").to_string();
351            let call = ext_details.variant_name().unwrap_or("Unknown").to_string();
352
353            // Check success by examining events
354            let mut success = false;
355            if let Ok(events) = ext_details.events().await {
356                for event in events.iter().flatten() {
357                    if event.pallet_name() == "System" && event.variant_name() == "ExtrinsicSuccess"
358                    {
359                        success = true;
360                        break;
361                    }
362                }
363            }
364
365            extrinsic_infos.push(ExtrinsicInfo {
366                index,
367                hash,
368                signed,
369                signer,
370                pallet,
371                call,
372                success,
373            });
374        }
375
376        Ok(extrinsic_infos)
377    }
378
379    /// Extract all events from a block
380    async fn extract_block_events(
381        &self,
382        block: &subxt::blocks::Block<PolkadotConfig, OnlineClient<PolkadotConfig>>,
383    ) -> Result<Vec<BlockEvent>, Error> {
384        let extrinsics = block
385            .extrinsics()
386            .await
387            .map_err(|e| Error::Transaction(format!("Failed to get extrinsics: {}", e)))?;
388
389        let mut all_events = Vec::new();
390        let mut event_index = 0u32;
391
392        for ext_details in extrinsics.iter() {
393            let extrinsic_index = ext_details.index();
394
395            if let Ok(events) = ext_details.events().await {
396                for event in events.iter().flatten() {
397                    all_events.push(BlockEvent {
398                        index: event_index,
399                        extrinsic_index: Some(extrinsic_index),
400                        pallet: event.pallet_name().to_string(),
401                        event: event.variant_name().to_string(),
402                    });
403                    event_index += 1;
404                }
405            }
406        }
407
408        Ok(all_events)
409    }
410
411    /// Count events in a block (lightweight, no full parsing)
412    async fn count_block_events(
413        &self,
414        block: &subxt::blocks::Block<PolkadotConfig, OnlineClient<PolkadotConfig>>,
415    ) -> Result<u32, Error> {
416        let extrinsics = block
417            .extrinsics()
418            .await
419            .map_err(|e| Error::Transaction(format!("Failed to get extrinsics: {}", e)))?;
420
421        let mut count = 0u32;
422        for ext_details in extrinsics.iter() {
423            if let Ok(events) = ext_details.events().await {
424                count += events.iter().count() as u32;
425            }
426        }
427
428        Ok(count)
429    }
430}
431
432#[cfg(test)]
433mod tests {
434    #[test]
435    fn test_block_hash_parsing() {
436        // Test with 0x prefix
437        let hash = "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef";
438        let stripped = hash.trim_start_matches("0x");
439        assert_eq!(stripped.len(), 64);
440
441        // Test without prefix
442        let hash2 = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef";
443        assert_eq!(hash2.len(), 64);
444    }
445}