1+ use std:: collections:: HashMap ;
2+
13use anyhow:: { Context , Result } ;
24use clap:: { Args , Subcommand } ;
35use katana_primitives:: block:: { BlockHash , BlockIdOrTag , BlockNumber , ConfirmedBlockIdOrTag } ;
@@ -7,6 +9,7 @@ use katana_primitives::execution::{EntryPointSelector, FunctionCall};
79use katana_primitives:: transaction:: TxHash ;
810use katana_primitives:: { ContractAddress , Felt } ;
911use katana_rpc_types:: event:: { EventFilter , EventFilterWithPage , ResultPageRequest } ;
12+ use katana_rpc_types:: trie:: ContractStorageKeys ;
1013
1114use super :: client:: Client ;
1215
@@ -91,6 +94,10 @@ pub enum StarknetCommands {
9194 /// Get execution traces for all transactions in a block
9295 #[ command( name = "block-traces" ) ]
9396 TraceBlockTransactions ( TraceBlockTransactionsArg ) ,
97+
98+ /// Get storage proofs for classes, contracts, and storage keys
99+ #[ command( name = "proof" ) ]
100+ GetStorageProof ( GetStorageProofArgs ) ,
94101}
95102
96103#[ derive( Debug , Args ) ]
@@ -267,17 +274,21 @@ pub struct GetNonceArgs {
267274#[ derive( Debug , Args ) ]
268275#[ cfg_attr( test, derive( PartialEq , Eq ) ) ]
269276pub struct GetStorageProofArgs {
270- /// Class hashes JSON array
271- #[ arg( long) ]
272- class_hashes : Option < String > ,
277+ /// Class hashes to get storage proofs for
278+ /// Example: --classes 0x1 0x2 0x3
279+ #[ arg( long, num_args = 0 ..) ]
280+ classes : Vec < String > ,
273281
274- /// Contract addresses JSON array
275- #[ arg( long) ]
276- contract_addresses : Option < String > ,
282+ /// Contract addresses to get storage proofs for
283+ /// Example: --contracts 0x1 0x2 0x3
284+ #[ arg( long, num_args = 0 ..) ]
285+ contracts : Vec < String > ,
277286
278- /// Contract storage keys JSON
279- #[ arg( long) ]
280- contracts_storage_keys : Option < String > ,
287+ /// Contract storage keys in format: address,key address,key
288+ /// Multiple keys for same address are supported: 0x123,0x1 0x123,0x2
289+ /// Example: --storages 0x1234,0xabc 0x5678,0xdef
290+ #[ arg( long, num_args = 0 ..) ]
291+ storages : Vec < String > ,
281292
282293 /// Block ID (number, hash, 'latest', or 'pending'). Defaults to 'latest'
283294 #[ arg( long) ]
@@ -455,6 +466,64 @@ impl StarknetCommands {
455466 let result = client. trace_block_transactions ( block_id. 0 ) . await ?;
456467 println ! ( "{}" , colored_json:: to_colored_json_auto( & result) ?) ;
457468 }
469+
470+ StarknetCommands :: GetStorageProof ( args) => {
471+ let block_id = args. block . 0 ;
472+
473+ // Parse class_hashes if provided
474+ let class_hashes: Option < Vec < ClassHash > > = if !args. classes . is_empty ( ) {
475+ Some (
476+ args. classes
477+ . iter ( )
478+ . enumerate ( )
479+ . map ( |( i, s) | {
480+ s. trim ( ) . parse :: < ClassHash > ( ) . with_context ( || {
481+ format ! ( "Invalid class hash at position {}: '{}'" , i, s)
482+ } )
483+ } )
484+ . collect :: < Result < Vec < _ > > > ( ) ?,
485+ )
486+ } else {
487+ None
488+ } ;
489+
490+ // Parse contract_addresses if provided
491+ let contract_addresses: Option < Vec < ContractAddress > > = if !args. contracts . is_empty ( )
492+ {
493+ Some (
494+ args. contracts
495+ . iter ( )
496+ . enumerate ( )
497+ . map ( |( i, s) | {
498+ s. trim ( ) . parse :: < ContractAddress > ( ) . with_context ( || {
499+ format ! ( "Invalid contract address at position {}: '{}'" , i, s)
500+ } )
501+ } )
502+ . collect :: < Result < Vec < _ > > > ( ) ?,
503+ )
504+ } else {
505+ None
506+ } ;
507+
508+ // Parse contracts_storage_keys if provided
509+ let contracts_storage_keys: Option < Vec < ContractStorageKeys > > =
510+ if !args. storages . is_empty ( ) {
511+ Some ( parse_contract_storage_keys ( & args. storages ) ?)
512+ } else {
513+ None
514+ } ;
515+
516+ let result = client
517+ . get_storage_proof (
518+ block_id,
519+ class_hashes,
520+ contract_addresses,
521+ contracts_storage_keys,
522+ )
523+ . await ?;
524+
525+ println ! ( "{}" , colored_json:: to_colored_json_auto( & result) ?) ;
526+ }
458527 }
459528 Ok ( ( ) )
460529 }
@@ -545,6 +614,42 @@ fn parse_event_keys(keys: &[String]) -> Result<Vec<Vec<Felt>>> {
545614 . collect ( )
546615}
547616
617+ /// Parses contract storage keys from CLI arguments.
618+ ///
619+ /// Format: Each argument is "address,key" where address is a contract address and key is a storage
620+ /// key. Multiple entries with the same address will be grouped together.
621+ /// Example: ["0x123,0x1", "0x123,0x2", "0x456,0x3"] =>
622+ /// [ContractStorageKeys { address: 0x123, keys: [0x1, 0x2] },
623+ /// ContractStorageKeys { address: 0x456, keys: [0x3] }]
624+ fn parse_contract_storage_keys ( storages : & [ String ] ) -> Result < Vec < ContractStorageKeys > > {
625+ let mut map: HashMap < ContractAddress , Vec < StorageKey > > = HashMap :: new ( ) ;
626+
627+ for ( i, pair) in storages. iter ( ) . enumerate ( ) {
628+ let parts: Vec < & str > = pair. split ( ',' ) . collect ( ) ;
629+
630+ if parts. len ( ) != 2 {
631+ anyhow:: bail!(
632+ "invalid storage format at position {}: '{}'. Expected 'address,key'" ,
633+ i,
634+ pair
635+ ) ;
636+ }
637+
638+ let address = parts[ 0 ] . trim ( ) . parse :: < ContractAddress > ( ) . with_context ( || {
639+ format ! ( "invalid contract address at position {}: '{}'" , i, parts[ 0 ] )
640+ } ) ?;
641+
642+ let key = parts[ 1 ]
643+ . trim ( )
644+ . parse :: < StorageKey > ( )
645+ . with_context ( || format ! ( "invalid storage key at position {}: '{}'" , i, parts[ 1 ] ) ) ?;
646+
647+ map. entry ( address) . or_default ( ) . push ( key) ;
648+ }
649+
650+ Ok ( map. into_iter ( ) . map ( |( address, keys) | ContractStorageKeys { address, keys } ) . collect ( ) )
651+ }
652+
548653#[ cfg( test) ]
549654mod tests {
550655 use std:: str:: FromStr ;
@@ -554,6 +659,7 @@ mod tests {
554659 use katana_primitives:: felt;
555660
556661 use super :: { BlockIdArg , ConfirmedBlockIdArg } ;
662+ use crate :: cli:: rpc:: starknet:: GetStorageProofArgs ;
557663
558664 #[ test]
559665 fn block_id_arg_from_str ( ) {
@@ -623,18 +729,20 @@ mod tests {
623729 }
624730
625731 use clap:: Parser ;
732+ use katana_primitives:: contract:: StorageKey ;
733+ use katana_primitives:: ContractAddress ;
626734
627- use super :: { parse_event_keys, GetEventsArgs } ;
735+ use super :: { parse_contract_storage_keys , parse_event_keys, GetEventsArgs } ;
628736
629737 #[ derive( Debug , Parser ) ]
630- struct TestCli {
738+ struct TestEventCli {
631739 #[ command( flatten) ]
632740 args : GetEventsArgs ,
633741 }
634742
635743 #[ test]
636744 fn get_events_args_single_keys ( ) {
637- let args = TestCli :: try_parse_from ( [ "test" , "--keys" , "0x1" , "0x2" , "0x3" ] ) . unwrap ( ) ;
745+ let args = TestEventCli :: try_parse_from ( [ "test" , "--keys" , "0x1" , "0x2" , "0x3" ] ) . unwrap ( ) ;
638746
639747 let keys = parse_event_keys ( & args. args . keys ) . unwrap ( ) ;
640748 assert_eq ! ( keys. len( ) , 3 ) ;
@@ -646,7 +754,8 @@ mod tests {
646754 #[ test]
647755 fn get_events_args_multiple_keys ( ) {
648756 let args =
649- TestCli :: try_parse_from ( [ "test" , "--keys" , "0x9" , "0x1,0x2,0x3" , "0x4,0x5" ] ) . unwrap ( ) ;
757+ TestEventCli :: try_parse_from ( [ "test" , "--keys" , "0x9" , "0x1,0x2,0x3" , "0x4,0x5" ] )
758+ . unwrap ( ) ;
650759
651760 let keys = parse_event_keys ( & args. args . keys ) . unwrap ( ) ;
652761 assert_eq ! ( keys. len( ) , 3 ) ;
@@ -657,7 +766,7 @@ mod tests {
657766
658767 #[ test]
659768 fn get_events_args_keys_with_whitespace ( ) {
660- let args = TestCli :: try_parse_from ( [ "test" , "--keys" , "0x1, 0x2 , 0x3" ] ) . unwrap ( ) ;
769+ let args = TestEventCli :: try_parse_from ( [ "test" , "--keys" , "0x1, 0x2 , 0x3" ] ) . unwrap ( ) ;
661770
662771 let keys = parse_event_keys ( & args. args . keys ) . unwrap ( ) ;
663772 assert_eq ! ( keys. len( ) , 1 ) ;
@@ -666,7 +775,7 @@ mod tests {
666775
667776 #[ test]
668777 fn get_events_args_invalid_felt ( ) {
669- let args = TestCli :: try_parse_from ( [ "test" , "--keys" , "0x1" , "invalid" ] ) . unwrap ( ) ;
778+ let args = TestEventCli :: try_parse_from ( [ "test" , "--keys" , "0x1" , "invalid" ] ) . unwrap ( ) ;
670779
671780 let result = parse_event_keys ( & args. args . keys ) ;
672781 assert ! ( result. is_err( ) ) ;
@@ -675,10 +784,111 @@ mod tests {
675784
676785 #[ test]
677786 fn get_events_args_invalid_hex ( ) {
678- let args = TestCli :: try_parse_from ( [ "test" , "--keys" , "0x1,0xGGG" ] ) . unwrap ( ) ;
787+ let args = TestEventCli :: try_parse_from ( [ "test" , "--keys" , "0x1,0xGGG" ] ) . unwrap ( ) ;
679788
680789 let result = parse_event_keys ( & args. args . keys ) ;
681790 assert ! ( result. is_err( ) ) ;
682791 assert ! ( result. unwrap_err( ) . to_string( ) . contains( "invalid felt in key group 0" ) ) ;
683792 }
793+
794+ #[ derive( Debug , Parser ) ]
795+ struct TestProofCli {
796+ #[ command( flatten) ]
797+ args : GetStorageProofArgs ,
798+ }
799+
800+ #[ test]
801+ fn get_contract_storage_proof_keys_single_pair ( ) {
802+ let args = TestProofCli :: try_parse_from ( [ "test" , "--storages" , "0x123,0xabc" ] ) . unwrap ( ) ;
803+ let result = parse_contract_storage_keys ( & args. args . storages ) . unwrap ( ) ;
804+
805+ assert_eq ! ( result. len( ) , 1 ) ;
806+ assert_eq ! ( result[ 0 ] . address, ContractAddress :: from( felt!( "0x123" ) ) ) ;
807+ assert_eq ! ( result[ 0 ] . keys. len( ) , 1 ) ;
808+ assert_eq ! ( result[ 0 ] . keys[ 0 ] , StorageKey :: from( felt!( "0xabc" ) ) ) ;
809+ }
810+
811+ #[ test]
812+ fn get_contract_storage_proof_keys_multiple_pairs_same_address ( ) {
813+ let args = TestProofCli :: try_parse_from ( [
814+ "test" ,
815+ "--storages" ,
816+ "0x123,0x1" ,
817+ "0x123,0x2" ,
818+ "0x123,0x3" ,
819+ ] )
820+ . unwrap ( ) ;
821+ let result = parse_contract_storage_keys ( & args. args . storages ) . unwrap ( ) ;
822+
823+ assert_eq ! ( result. len( ) , 1 ) ;
824+ assert_eq ! ( result[ 0 ] . address, ContractAddress :: from( felt!( "0x123" ) ) ) ;
825+ assert_eq ! ( result[ 0 ] . keys. len( ) , 3 ) ;
826+ assert ! ( result[ 0 ] . keys. contains( & StorageKey :: from( felt!( "0x1" ) ) ) ) ;
827+ assert ! ( result[ 0 ] . keys. contains( & StorageKey :: from( felt!( "0x2" ) ) ) ) ;
828+ assert ! ( result[ 0 ] . keys. contains( & StorageKey :: from( felt!( "0x3" ) ) ) ) ;
829+ }
830+
831+ #[ test]
832+ fn get_contract_storage_proof_keys_multiple_addresses ( ) {
833+ let args = TestProofCli :: try_parse_from ( [
834+ "test" ,
835+ "--storages" ,
836+ "0x123,0xabc" ,
837+ "0x456,0xdef" ,
838+ "0x789,0x111" ,
839+ ] )
840+ . unwrap ( ) ;
841+ let result = parse_contract_storage_keys ( & args. args . storages ) . unwrap ( ) ;
842+
843+ assert_eq ! ( result. len( ) , 3 ) ;
844+
845+ let addr_123 =
846+ result. iter ( ) . find ( |r| r. address == ContractAddress :: from ( felt ! ( "0x123" ) ) ) . unwrap ( ) ;
847+ assert_eq ! ( addr_123. keys. len( ) , 1 ) ;
848+ assert_eq ! ( addr_123. keys[ 0 ] , StorageKey :: from( felt!( "0xabc" ) ) ) ;
849+
850+ let addr_456 =
851+ result. iter ( ) . find ( |r| r. address == ContractAddress :: from ( felt ! ( "0x456" ) ) ) . unwrap ( ) ;
852+ assert_eq ! ( addr_456. keys. len( ) , 1 ) ;
853+ assert_eq ! ( addr_456. keys[ 0 ] , StorageKey :: from( felt!( "0xdef" ) ) ) ;
854+
855+ let addr_789 =
856+ result. iter ( ) . find ( |r| r. address == ContractAddress :: from ( felt ! ( "0x789" ) ) ) . unwrap ( ) ;
857+ assert_eq ! ( addr_789. keys. len( ) , 1 ) ;
858+ assert_eq ! ( addr_789. keys[ 0 ] , StorageKey :: from( felt!( "0x111" ) ) ) ;
859+ }
860+
861+ #[ test]
862+ fn parse_contract_storage_keys_with_whitespace ( ) {
863+ let args = TestProofCli :: try_parse_from ( [ "test" , "--storages" , " 0x123 , 0xabc " ] ) . unwrap ( ) ;
864+ let result = parse_contract_storage_keys ( & args. args . storages ) . unwrap ( ) ;
865+
866+ assert_eq ! ( result. len( ) , 1 ) ;
867+ assert_eq ! ( result[ 0 ] . address, ContractAddress :: from( felt!( "0x123" ) ) ) ;
868+ assert_eq ! ( result[ 0 ] . keys[ 0 ] , StorageKey :: from( felt!( "0xabc" ) ) ) ;
869+ }
870+
871+ #[ test]
872+ fn parse_contract_storage_keys_invalid_format ( ) {
873+ let input = vec ! [ "0x123" . to_string( ) ] ;
874+ let result = parse_contract_storage_keys ( & input) ;
875+ assert ! ( result. is_err( ) ) ;
876+ assert ! ( result. unwrap_err( ) . to_string( ) . contains( "invalid storage format" ) ) ;
877+ }
878+
879+ #[ test]
880+ fn parse_contract_storage_keys_invalid_address ( ) {
881+ let input = vec ! [ "invalid,0xabc" . to_string( ) ] ;
882+ let result = parse_contract_storage_keys ( & input) ;
883+ assert ! ( result. is_err( ) ) ;
884+ assert ! ( result. unwrap_err( ) . to_string( ) . contains( "invalid contract address" ) ) ;
885+ }
886+
887+ #[ test]
888+ fn parse_contract_storage_keys_invalid_key ( ) {
889+ let input = vec ! [ "0x123,invalid" . to_string( ) ] ;
890+ let result = parse_contract_storage_keys ( & input) ;
891+ assert ! ( result. is_err( ) ) ;
892+ assert ! ( result. unwrap_err( ) . to_string( ) . contains( "invalid storage key" ) ) ;
893+ }
684894}
0 commit comments