Skip to main content

reth_cli_commands/db/
get.rs

1use alloy_primitives::{hex, Address, BlockHash, B256};
2use clap::Parser;
3use reth_db::{
4    static_file::{
5        AccountChangesetMask, ColumnSelectorOne, ColumnSelectorTwo, HeaderWithHashMask,
6        ReceiptMask, TransactionMask, TransactionSenderMask,
7    },
8    RawDupSort,
9};
10use reth_db_api::{
11    cursor::{DbCursorRO, DbDupCursorRO},
12    database::Database,
13    models::{storage_sharded_key::StorageShardedKey, ShardedKey},
14    table::{Compress, Decompress, DupSort, Table},
15    tables,
16    transaction::DbTx,
17    RawKey, RawTable, TableViewer,
18};
19use reth_db_common::DbTool;
20use reth_node_api::{HeaderTy, ReceiptTy, TxTy};
21use reth_node_builder::NodeTypesWithDB;
22use reth_primitives_traits::ValueWithSubKey;
23use reth_provider::{
24    providers::ProviderNodeTypes, ChangeSetReader, RocksDBProviderFactory,
25    StaticFileProviderFactory,
26};
27use reth_static_file_types::StaticFileSegment;
28use reth_storage_api::StorageChangeSetReader;
29use tracing::error;
30
31/// The arguments for the `reth db get` command
32#[derive(Parser, Debug)]
33pub struct Command {
34    #[command(subcommand)]
35    subcommand: Subcommand,
36}
37
38#[derive(clap::Subcommand, Debug)]
39enum Subcommand {
40    /// Gets the content of a database table for the given key
41    Mdbx {
42        table: tables::Tables,
43
44        /// The key to get content for
45        #[arg(value_parser = maybe_json_value_parser)]
46        key: String,
47
48        /// The subkey to get content for
49        #[arg(value_parser = maybe_json_value_parser)]
50        subkey: Option<String>,
51
52        /// Optional end key for range query (exclusive upper bound)
53        #[arg(value_parser = maybe_json_value_parser)]
54        end_key: Option<String>,
55
56        /// Optional end subkey for range query (exclusive upper bound)
57        #[arg(value_parser = maybe_json_value_parser)]
58        end_subkey: Option<String>,
59
60        /// Output bytes instead of human-readable decoded value
61        #[arg(long)]
62        raw: bool,
63    },
64    /// Gets the content of a static file segment for the given key
65    StaticFile {
66        segment: StaticFileSegment,
67
68        /// The key to get content for
69        #[arg(value_parser = maybe_json_value_parser)]
70        key: String,
71
72        /// The subkey to get content for, for example address in changeset
73        #[arg(value_parser = maybe_json_value_parser)]
74        subkey: Option<String>,
75
76        /// Output bytes instead of human-readable decoded value
77        #[arg(long)]
78        raw: bool,
79    },
80    /// Gets the content of a RocksDB table for the given key
81    ///
82    /// For history tables (accounts-history, storages-history), you can pass a plain address
83    /// instead of a full JSON ShardedKey. Use --block to query a specific block number
84    /// (seeks to the shard containing that block), or --all-shards to list all shards for
85    /// the address.
86    ///
87    /// Examples:
88    ///   reth db get rocksdb accounts-history 0xdBBE3D8c2d2b22A2611c5A94A9a12C2fCD49Eb29
89    ///   reth db get rocksdb accounts-history 0xdBBE...Eb29 --block 1000000
90    ///   reth db get rocksdb accounts-history 0xdBBE...Eb29 --all-shards
91    ///   reth db get rocksdb storages-history 0xdBBE...Eb29 --storage-key 0x0000...0003
92    Rocksdb {
93        /// The RocksDB table
94        #[arg(value_enum)]
95        table: RocksDbTable,
96
97        /// The key to get content for. For history tables, this can be a plain address.
98        #[arg(value_parser = maybe_json_value_parser)]
99        key: String,
100
101        /// Target block number for history tables. Seeks to the shard containing this block.
102        /// Defaults to the latest shard if not specified.
103        #[arg(long)]
104        block: Option<u64>,
105
106        /// Storage key for storages-history table lookups.
107        #[arg(long)]
108        storage_key: Option<String>,
109
110        /// List all shards for the given key (history tables only).
111        #[arg(long)]
112        all_shards: bool,
113
114        /// Output bytes instead of human-readable decoded value
115        #[arg(long)]
116        raw: bool,
117    },
118}
119
120/// RocksDB tables that can be queried.
121#[derive(Debug, Clone, Copy, clap::ValueEnum)]
122pub enum RocksDbTable {
123    /// Transaction hash to transaction number mapping
124    TransactionHashNumbers,
125    /// Account history indices
126    AccountsHistory,
127    /// Storage history indices
128    StoragesHistory,
129}
130
131impl Command {
132    /// Execute `db get` command
133    pub fn execute<N: ProviderNodeTypes>(self, tool: &DbTool<N>) -> eyre::Result<()> {
134        match self.subcommand {
135            Subcommand::Mdbx { table, key, subkey, end_key, end_subkey, raw } => {
136                table.view(&GetValueViewer { tool, key, subkey, end_key, end_subkey, raw })?
137            }
138            Subcommand::Rocksdb { table, key, block, storage_key, all_shards, raw } => {
139                get_rocksdb(tool, table, &key, block, storage_key.as_deref(), all_shards, raw)?;
140            }
141            Subcommand::StaticFile { segment, key, subkey, raw } => {
142                if let StaticFileSegment::StorageChangeSets = segment {
143                    let storage_key =
144                        table_subkey::<tables::StorageChangeSets>(subkey.as_deref()).ok();
145                    let key = table_key::<tables::StorageChangeSets>(&key)?;
146
147                    let provider = tool.provider_factory.static_file_provider();
148
149                    if let Some(storage_key) = storage_key {
150                        let entry = provider.get_storage_before_block(
151                            key.block_number(),
152                            key.address(),
153                            storage_key,
154                        )?;
155
156                        if let Some(entry) = entry {
157                            let se: reth_primitives_traits::StorageEntry = entry;
158                            println!("{}", serde_json::to_string_pretty(&se)?);
159                        } else {
160                            error!(target: "reth::cli", "No content for the given table key.");
161                        }
162                        return Ok(());
163                    }
164
165                    let changesets = provider.storage_changeset(key.block_number())?;
166                    let serializable: Vec<_> = changesets
167                        .into_iter()
168                        .map(|(addr, entry)| {
169                            let se: reth_primitives_traits::StorageEntry = entry;
170                            (addr, se)
171                        })
172                        .collect();
173                    println!("{}", serde_json::to_string_pretty(&serializable)?);
174                    return Ok(());
175                }
176
177                let (key, subkey, mask): (u64, _, _) = match segment {
178                    StaticFileSegment::Headers => (
179                        table_key::<tables::Headers>(&key)?,
180                        None,
181                        <HeaderWithHashMask<HeaderTy<N>>>::MASK,
182                    ),
183                    StaticFileSegment::Transactions => (
184                        table_key::<tables::Transactions>(&key)?,
185                        None,
186                        <TransactionMask<TxTy<N>>>::MASK,
187                    ),
188                    StaticFileSegment::Receipts => (
189                        table_key::<tables::Receipts>(&key)?,
190                        None,
191                        <ReceiptMask<ReceiptTy<N>>>::MASK,
192                    ),
193                    StaticFileSegment::TransactionSenders => (
194                        table_key::<tables::TransactionSenders>(&key)?,
195                        None,
196                        TransactionSenderMask::MASK,
197                    ),
198                    StaticFileSegment::AccountChangeSets => {
199                        let subkey =
200                            table_subkey::<tables::AccountChangeSets>(subkey.as_deref()).ok();
201                        (
202                            table_key::<tables::AccountChangeSets>(&key)?,
203                            subkey,
204                            AccountChangesetMask::MASK,
205                        )
206                    }
207                    StaticFileSegment::StorageChangeSets => {
208                        unreachable!("storage changesets handled above");
209                    }
210                };
211
212                // handle account changesets differently if a subkey is provided.
213                if let StaticFileSegment::AccountChangeSets = segment {
214                    let Some(subkey) = subkey else {
215                        // get all changesets for the block
216                        let changesets = tool
217                            .provider_factory
218                            .static_file_provider()
219                            .account_block_changeset(key)?;
220
221                        println!("{}", serde_json::to_string_pretty(&changesets)?);
222                        return Ok(())
223                    };
224
225                    let account = tool
226                        .provider_factory
227                        .static_file_provider()
228                        .get_account_before_block(key, subkey)?;
229
230                    if let Some(account) = account {
231                        println!("{}", serde_json::to_string_pretty(&account)?);
232                    } else {
233                        error!(target: "reth::cli", "No content for the given table key.");
234                    }
235
236                    return Ok(())
237                }
238
239                let content = tool.provider_factory.static_file_provider().find_static_file(
240                    segment,
241                    |provider| {
242                        let mut cursor = provider.cursor()?;
243                        cursor.get(key.into(), mask).map(|result| {
244                            result.map(|vec| {
245                                vec.iter().map(|slice| slice.to_vec()).collect::<Vec<_>>()
246                            })
247                        })
248                    },
249                )?;
250
251                match content {
252                    Some(content) => {
253                        if raw {
254                            println!("{}", hex::encode_prefixed(&content[0]));
255                        } else {
256                            match segment {
257                                StaticFileSegment::Headers => {
258                                    let header = HeaderTy::<N>::decompress(content[0].as_slice())?;
259                                    let block_hash = BlockHash::decompress(content[1].as_slice())?;
260                                    println!(
261                                        "Header\n{}\n\nBlockHash\n{}",
262                                        serde_json::to_string_pretty(&header)?,
263                                        serde_json::to_string_pretty(&block_hash)?
264                                    );
265                                }
266                                StaticFileSegment::Transactions => {
267                                    let transaction = TxTy::<N>::decompress(content[0].as_slice())?;
268                                    println!("{}", serde_json::to_string_pretty(&transaction)?);
269                                }
270                                StaticFileSegment::Receipts => {
271                                    let receipt =
272                                        ReceiptTy::<N>::decompress(content[0].as_slice())?;
273                                    println!("{}", serde_json::to_string_pretty(&receipt)?);
274                                }
275                                StaticFileSegment::TransactionSenders => {
276                                    let sender =
277                                        <<tables::TransactionSenders as Table>::Value>::decompress(
278                                            content[0].as_slice(),
279                                        )?;
280                                    println!("{}", serde_json::to_string_pretty(&sender)?);
281                                }
282                                StaticFileSegment::AccountChangeSets => {
283                                    unreachable!("account changeset static files are special cased before this match")
284                                }
285                                StaticFileSegment::StorageChangeSets => {
286                                    unreachable!("storage changeset static files are special cased before this match")
287                                }
288                            }
289                        }
290                    }
291                    None => {
292                        error!(target: "reth::cli", "No content for the given table key.");
293                    }
294                };
295            }
296        }
297
298        Ok(())
299    }
300}
301
302/// Gets a value from a RocksDB table by key.
303fn get_rocksdb<N: ProviderNodeTypes>(
304    tool: &DbTool<N>,
305    table: RocksDbTable,
306    key: &str,
307    block: Option<u64>,
308    storage_key: Option<&str>,
309    all_shards: bool,
310    raw: bool,
311) -> eyre::Result<()> {
312    let rocksdb = tool.provider_factory.rocksdb_provider();
313
314    match table {
315        RocksDbTable::TransactionHashNumbers => {
316            if block.is_some() || all_shards || storage_key.is_some() {
317                return Err(eyre::eyre!(
318                    "--block, --all-shards, and --storage-key are only supported for history tables"
319                ));
320            }
321            get_rocksdb_table::<tables::TransactionHashNumbers>(&rocksdb, key, raw)
322        }
323        RocksDbTable::AccountsHistory => {
324            if storage_key.is_some() {
325                return Err(eyre::eyre!("--storage-key is only supported for storages-history"));
326            }
327            get_rocksdb_account_history(&rocksdb, key, block, all_shards, raw)
328        }
329        RocksDbTable::StoragesHistory => {
330            get_rocksdb_storage_history(&rocksdb, key, storage_key, block, all_shards, raw)
331        }
332    }
333}
334
335/// Try to parse a key string as a plain address, falling back to JSON `ShardedKey` parsing.
336fn parse_address(key: &str) -> eyre::Result<Address> {
337    // Strip surrounding quotes that `maybe_json_value_parser` may have added
338    let stripped = key.trim_matches('"');
339    stripped.parse::<Address>().map_err(|e| eyre::eyre!("failed to parse address: {e}"))
340}
341
342/// Gets account history from RocksDB with ergonomic key parsing.
343///
344/// Accepts a plain address and uses seek to find the relevant shard.
345fn get_rocksdb_account_history(
346    rocksdb: &reth_provider::providers::RocksDBProvider,
347    key: &str,
348    block: Option<u64>,
349    all_shards: bool,
350    raw: bool,
351) -> eyre::Result<()> {
352    // Try parsing as a plain address first, fall back to full JSON ShardedKey
353    match parse_address(key) {
354        Ok(address) => {
355            let block_number = block.unwrap_or(u64::MAX);
356            let seek_key = ShardedKey::new(address, block_number);
357
358            if all_shards {
359                // Iterate all shards: seek from (address, 0) until address changes
360                let start = ShardedKey::new(address, 0);
361                let iter = rocksdb.iter_from::<tables::AccountsHistory>(start)?;
362                for result in iter {
363                    let (k, v) = result?;
364                    if k.key != address {
365                        break;
366                    }
367                    println!(
368                        "{}",
369                        serde_json::to_string_pretty(&serde_json::json!({
370                            "highest_block_number": k.highest_block_number,
371                            "value": v,
372                        }))?
373                    );
374                }
375            } else {
376                // Seek to the first shard with highest_block_number >= target
377                let mut iter = rocksdb.iter_from::<tables::AccountsHistory>(seek_key)?;
378                match iter.next() {
379                    Some(Ok((k, v))) if k.key == address => {
380                        if raw {
381                            let raw_val = rocksdb.get_raw::<tables::AccountsHistory>(k)?;
382                            if let Some(bytes) = raw_val {
383                                println!("{}", hex::encode_prefixed(&bytes));
384                            }
385                        } else {
386                            println!("{}", serde_json::to_string_pretty(&v)?);
387                        }
388                    }
389                    _ => {
390                        error!(target: "reth::cli", "No content for the given table key.");
391                    }
392                }
393            }
394            Ok(())
395        }
396        Err(_) => {
397            // Fall back to full JSON key parsing (e.g.
398            // `{"key":"0x...","highest_block_number":...}`)
399            if all_shards || block.is_some() {
400                return Err(eyre::eyre!(
401                    "--block and --all-shards require a plain address, not a JSON key"
402                ));
403            }
404            get_rocksdb_table::<tables::AccountsHistory>(rocksdb, key, raw)
405        }
406    }
407}
408
409/// Gets storage history from RocksDB with ergonomic key parsing.
410///
411/// Accepts a plain address + optional `--storage-key` and uses seek.
412fn get_rocksdb_storage_history(
413    rocksdb: &reth_provider::providers::RocksDBProvider,
414    key: &str,
415    storage_key: Option<&str>,
416    block: Option<u64>,
417    all_shards: bool,
418    raw: bool,
419) -> eyre::Result<()> {
420    match parse_address(key) {
421        Ok(address) => {
422            let storage_key = storage_key
423                .map(|s| s.trim_matches('"').parse::<B256>())
424                .transpose()
425                .map_err(|e| eyre::eyre!("failed to parse storage key: {e}"))?
426                .unwrap_or_default();
427            let block_number = block.unwrap_or(u64::MAX);
428            let seek_key = StorageShardedKey::new(address, storage_key, block_number);
429
430            if all_shards {
431                let start = StorageShardedKey::new(address, storage_key, 0);
432                let iter = rocksdb.iter_from::<tables::StoragesHistory>(start)?;
433                for result in iter {
434                    let (k, v) = result?;
435                    if k.address != address || k.sharded_key.key != storage_key {
436                        break;
437                    }
438                    println!(
439                        "{}",
440                        serde_json::to_string_pretty(&serde_json::json!({
441                            "highest_block_number": k.sharded_key.highest_block_number,
442                            "value": v,
443                        }))?
444                    );
445                }
446            } else {
447                let mut iter = rocksdb.iter_from::<tables::StoragesHistory>(seek_key)?;
448                match iter.next() {
449                    Some(Ok((k, v)))
450                        if k.address == address && k.sharded_key.key == storage_key =>
451                    {
452                        if raw {
453                            let raw_val = rocksdb.get_raw::<tables::StoragesHistory>(k)?;
454                            if let Some(bytes) = raw_val {
455                                println!("{}", hex::encode_prefixed(&bytes));
456                            }
457                        } else {
458                            println!("{}", serde_json::to_string_pretty(&v)?);
459                        }
460                    }
461                    _ => {
462                        error!(target: "reth::cli", "No content for the given table key.");
463                    }
464                }
465            }
466            Ok(())
467        }
468        Err(_) => {
469            if all_shards || block.is_some() || storage_key.is_some() {
470                return Err(eyre::eyre!(
471                    "--block, --all-shards, and --storage-key require a plain address, not a JSON key"
472                ));
473            }
474            get_rocksdb_table::<tables::StoragesHistory>(rocksdb, key, raw)
475        }
476    }
477}
478
479/// Gets a value from a specific RocksDB table by exact key and prints it.
480fn get_rocksdb_table<T: Table>(
481    rocksdb: &reth_provider::providers::RocksDBProvider,
482    key_str: &str,
483    raw: bool,
484) -> eyre::Result<()> {
485    let key = table_key::<T>(key_str)?;
486
487    if raw {
488        let content = rocksdb.get_raw::<T>(key)?;
489        match content {
490            Some(bytes) => println!("{}", hex::encode_prefixed(&bytes)),
491            None => error!(target: "reth::cli", "No content for the given table key."),
492        }
493    } else {
494        let content = rocksdb.get::<T>(key)?;
495        match content {
496            Some(value) => println!("{}", serde_json::to_string_pretty(&value)?),
497            None => error!(target: "reth::cli", "No content for the given table key."),
498        }
499    }
500
501    Ok(())
502}
503
504/// Get an instance of key for given table
505pub(crate) fn table_key<T: Table>(key: &str) -> Result<T::Key, eyre::Error> {
506    serde_json::from_str(key).map_err(|e| eyre::eyre!(e))
507}
508
509/// Get an instance of subkey for given dupsort table
510fn table_subkey<T: DupSort>(subkey: Option<&str>) -> Result<T::SubKey, eyre::Error> {
511    serde_json::from_str(subkey.unwrap_or_default()).map_err(|e| eyre::eyre!(e))
512}
513
514struct GetValueViewer<'a, N: NodeTypesWithDB> {
515    tool: &'a DbTool<N>,
516    key: String,
517    subkey: Option<String>,
518    end_key: Option<String>,
519    end_subkey: Option<String>,
520    raw: bool,
521}
522
523impl<N: ProviderNodeTypes> TableViewer<()> for GetValueViewer<'_, N> {
524    type Error = eyre::Report;
525
526    fn view<T: Table>(&self) -> Result<(), Self::Error> {
527        let key = table_key::<T>(&self.key)?;
528
529        // A non-dupsort table cannot have subkeys. The `subkey` arg becomes the `end_key`. First we
530        // check that `end_key` and `end_subkey` weren't previously given, as that wouldn't be
531        // valid.
532        if self.end_key.is_some() || self.end_subkey.is_some() {
533            return Err(eyre::eyre!("Only END_KEY can be given for non-DUPSORT tables"));
534        }
535
536        let end_key = self.subkey.clone();
537
538        // Check if we're doing a range query
539        if let Some(ref end_key_str) = end_key {
540            let end_key = table_key::<T>(end_key_str)?;
541
542            // Use walk_range to iterate over the range
543            self.tool.provider_factory.db_ref().view(|tx| {
544                let mut cursor = tx.cursor_read::<T>()?;
545                let walker = cursor.walk_range(key..end_key)?;
546
547                for result in walker {
548                    let (k, v) = result?;
549                    let json_val = if self.raw {
550                        let raw_key = RawKey::from(k);
551                        serde_json::json!({
552                            "key": hex::encode_prefixed(raw_key.raw_key()),
553                            "val": hex::encode_prefixed(v.compress().as_ref()),
554                        })
555                    } else {
556                        serde_json::json!({
557                            "key": &k,
558                            "val": &v,
559                        })
560                    };
561
562                    println!("{}", serde_json::to_string_pretty(&json_val)?);
563                }
564
565                Ok::<_, eyre::Report>(())
566            })??;
567        } else {
568            // Single key lookup
569            let content = if self.raw {
570                self.tool
571                    .get::<RawTable<T>>(RawKey::from(key))?
572                    .map(|content| hex::encode_prefixed(content.raw_value()))
573            } else {
574                self.tool.get::<T>(key)?.as_ref().map(serde_json::to_string_pretty).transpose()?
575            };
576
577            match content {
578                Some(content) => {
579                    println!("{content}");
580                }
581                None => {
582                    error!(target: "reth::cli", "No content for the given table key.");
583                }
584            };
585        }
586
587        Ok(())
588    }
589
590    fn view_dupsort<T: DupSort>(&self) -> Result<(), Self::Error>
591    where
592        T::Value: reth_primitives_traits::ValueWithSubKey<SubKey = T::SubKey>,
593    {
594        // get a key for given table
595        let key = table_key::<T>(&self.key)?;
596
597        // Check if we're doing a range query
598        if let Some(ref end_key_str) = self.end_key {
599            let end_key = table_key::<T>(end_key_str)?;
600            let start_subkey = table_subkey::<T>(Some(
601                self.subkey.as_ref().expect("must have been given if end_key is given").as_str(),
602            ))?;
603            let end_subkey_parsed = self
604                .end_subkey
605                .as_ref()
606                .map(|s| table_subkey::<T>(Some(s.as_str())))
607                .transpose()?;
608
609            self.tool.provider_factory.db_ref().view(|tx| {
610                let mut cursor = tx.cursor_dup_read::<T>()?;
611
612                // Seek to the starting key. If there is actually a key at the starting key then
613                // seek to the subkey within it.
614                if let Some((decoded_key, _)) = cursor.seek(key.clone())? &&
615                    decoded_key == key
616                {
617                    cursor.seek_by_key_subkey(key.clone(), start_subkey.clone())?;
618                }
619
620                // Get the current position to start iteration
621                let mut current = cursor.current()?;
622
623                while let Some((decoded_key, decoded_value)) = current {
624                    // Extract the subkey using the ValueWithSubKey trait
625                    let decoded_subkey = decoded_value.get_subkey();
626
627                    // Check if we've reached the end (exclusive)
628                    if (&decoded_key, Some(&decoded_subkey)) >=
629                        (&end_key, end_subkey_parsed.as_ref())
630                    {
631                        break;
632                    }
633
634                    // Output the entry with both key and subkey
635                    let json_val = if self.raw {
636                        let raw_key = RawKey::from(decoded_key.clone());
637                        serde_json::json!({
638                            "key": hex::encode_prefixed(raw_key.raw_key()),
639                            "val": hex::encode_prefixed(decoded_value.compress().as_ref()),
640                        })
641                    } else {
642                        serde_json::json!({
643                            "key": &decoded_key,
644                            "val": &decoded_value,
645                        })
646                    };
647
648                    println!("{}", serde_json::to_string_pretty(&json_val)?);
649
650                    // Move to next entry
651                    current = cursor.next()?;
652                }
653
654                Ok::<_, eyre::Report>(())
655            })??;
656        } else {
657            // Single key/subkey lookup
658            let subkey = table_subkey::<T>(self.subkey.as_deref())?;
659
660            let content = if self.raw {
661                self.tool
662                    .get_dup::<RawDupSort<T>>(RawKey::from(key), RawKey::from(subkey))?
663                    .map(|content| hex::encode_prefixed(content.raw_value()))
664            } else {
665                self.tool
666                    .get_dup::<T>(key, subkey)?
667                    .as_ref()
668                    .map(serde_json::to_string_pretty)
669                    .transpose()?
670            };
671
672            match content {
673                Some(content) => {
674                    println!("{content}");
675                }
676                None => {
677                    error!(target: "reth::cli", "No content for the given table subkey.");
678                }
679            };
680        }
681        Ok(())
682    }
683}
684
685/// Map the user input value to json
686pub(crate) fn maybe_json_value_parser(value: &str) -> Result<String, eyre::Error> {
687    if serde_json::from_str::<serde::de::IgnoredAny>(value).is_ok() {
688        Ok(value.to_string())
689    } else {
690        serde_json::to_string(&value).map_err(|e| eyre::eyre!(e))
691    }
692}
693
694#[cfg(test)]
695mod tests {
696    use super::*;
697    use alloy_primitives::{address, B256};
698    use clap::{Args, Parser};
699    use reth_db_api::{
700        models::{storage_sharded_key::StorageShardedKey, ShardedKey},
701        AccountsHistory, HashedAccounts, Headers, StageCheckpoints, StoragesHistory,
702    };
703    use std::str::FromStr;
704
705    /// A helper type to parse Args more easily
706    #[derive(Parser)]
707    struct CommandParser<T: Args> {
708        #[command(flatten)]
709        args: T,
710    }
711
712    #[test]
713    fn parse_numeric_key_args() {
714        assert_eq!(table_key::<Headers>("123").unwrap(), 123);
715        assert_eq!(
716            table_key::<HashedAccounts>(
717                "\"0x0ac361fe774b78f8fc4e86c1916930d150865c3fc2e21dca2e58833557608bac\""
718            )
719            .unwrap(),
720            B256::from_str("0x0ac361fe774b78f8fc4e86c1916930d150865c3fc2e21dca2e58833557608bac")
721                .unwrap()
722        );
723    }
724
725    #[test]
726    fn parse_string_key_args() {
727        assert_eq!(
728            table_key::<StageCheckpoints>("\"MerkleExecution\"").unwrap(),
729            "MerkleExecution"
730        );
731    }
732
733    #[test]
734    fn parse_json_key_args() {
735        assert_eq!(
736            table_key::<StoragesHistory>(r#"{ "address": "0x01957911244e546ce519fbac6f798958fafadb41", "sharded_key": { "key": "0x0000000000000000000000000000000000000000000000000000000000000003", "highest_block_number": 18446744073709551615 } }"#).unwrap(),
737            StorageShardedKey::new(
738                address!("0x01957911244e546ce519fbac6f798958fafadb41"),
739                B256::from_str(
740                    "0x0000000000000000000000000000000000000000000000000000000000000003"
741                )
742                .unwrap(),
743                18446744073709551615
744            )
745        );
746    }
747
748    #[test]
749    fn parse_json_key_for_account_history() {
750        assert_eq!(
751            table_key::<AccountsHistory>(r#"{ "key": "0x4448e1273fd5a8bfdb9ed111e96889c960eee145", "highest_block_number": 18446744073709551615 }"#).unwrap(),
752            ShardedKey::new(
753                address!("0x4448e1273fd5a8bfdb9ed111e96889c960eee145"),
754                18446744073709551615
755            )
756        );
757    }
758}