@@ -38,8 +38,38 @@ use std::{collections::HashMap, fs};
3838use rayon:: prelude:: * ;
3939use version:: VersionInfo ;
4040
41+ #[ cfg( feature = "history" ) ]
42+ const MAX_KEYS_PER_REQUEST : usize = 1 << 7 ;
4143#[ cfg( feature = "history" ) ]
4244type HistoricalMappingKey < N > = ( ProgramID < N > , Identifier < N > , Plaintext < N > , u32 ) ;
45+ #[ cfg( feature = "history" ) ]
46+ type HistoricalMappingRoute < N > = ( ProgramID < N > , Identifier < N > , u32 ) ;
47+
48+ #[ cfg( feature = "history" ) ]
49+ fn parse_historical_mapping_keys < N : Network > ( keys : & [ String ] ) -> Result < Vec < Plaintext < N > > , RestError > {
50+ // Retrieve the number of keys.
51+ let num_keys = keys. len ( ) ;
52+ // Return an error if no keys are provided.
53+ if num_keys == 0 {
54+ return Err ( RestError :: unprocessable_entity ( anyhow ! ( "No keys provided" ) ) ) ;
55+ }
56+ // Return an error if the number of keys exceeds the maximum allowed.
57+ if num_keys > MAX_KEYS_PER_REQUEST {
58+ return Err ( RestError :: unprocessable_entity ( anyhow ! (
59+ "Too many keys provided (max: {MAX_KEYS_PER_REQUEST}, got: {num_keys})"
60+ ) ) ) ;
61+ }
62+
63+ // Deserialize the keys from the query.
64+ keys. iter ( )
65+ . enumerate ( )
66+ . map ( |( index, key) | {
67+ key. parse :: < Plaintext < N > > ( ) . map_err ( |err| {
68+ RestError :: unprocessable_entity ( err. context ( format ! ( "Invalid key at index {index}: {key}" ) ) )
69+ } )
70+ } )
71+ . collect :: < Result < Vec < _ > , _ > > ( )
72+ }
4373
4474/// Deserialize a CSV string into a vector of strings.
4575fn de_csv < ' de , D > ( de : D ) -> std:: result:: Result < Vec < String > , D :: Error >
@@ -90,6 +120,14 @@ pub(crate) struct Commitments {
90120 commitments : Vec < String > ,
91121}
92122
123+ /// The query object for `get_history_batch`.
124+ #[ cfg( feature = "history" ) ]
125+ #[ derive( Clone , Deserialize , Serialize ) ]
126+ pub ( crate ) struct HistoricalKeys {
127+ #[ serde( deserialize_with = "de_csv" ) ]
128+ keys : Vec < String > ,
129+ }
130+
93131/// The return value for a transaction metadata query.
94132#[ skip_serializing_none]
95133#[ derive( Serialize ) ]
@@ -610,22 +648,31 @@ impl<N: Network, C: ConsensusStorage<N>, R: Routing<N>> Rest<N, C, R> {
610648 }
611649 // Return an error if the number of commitments exceeds the maximum allowed.
612650 if num_commitments > N :: MAX_INPUTS {
613- return Err ( RestError :: unprocessable_entity ( anyhow ! (
614- "Too many commitments provided (max: {}, got: {})" ,
615- N :: MAX_INPUTS ,
616- num_commitments
617- ) ) ) ;
651+ return Err ( RestError :: unprocessable_entity ( anyhow ! ( format!(
652+ "Too many commitments provided (max: {}, got: {num_commitments})" ,
653+ N :: MAX_INPUTS
654+ ) ) ) ) ;
618655 }
619656
620657 // Deserialize the commitments from the query.
621- let commitments = commitments
622- . commitments
623- . iter ( )
624- . map ( |s| {
625- s. parse :: < Field < N > > ( )
626- . map_err ( |err| RestError :: unprocessable_entity ( err. context ( format ! ( "Invalid commitment: {s}" ) ) ) )
627- } )
628- . collect :: < Result < Vec < _ > , _ > > ( ) ?;
658+ let commitments = match tokio:: task:: spawn_blocking ( move || {
659+ commitments
660+ . commitments
661+ . iter ( )
662+ . map ( |s| {
663+ s. parse :: < Field < N > > ( )
664+ . map_err ( |err| RestError :: unprocessable_entity ( err. context ( format ! ( "Invalid commitment: {s}" ) ) ) )
665+ } )
666+ . collect :: < Result < Vec < _ > , _ > > ( )
667+ } )
668+ . await
669+ {
670+ Ok ( Ok ( commitments) ) => commitments,
671+ Ok ( Err ( err) ) => {
672+ return Err ( RestError :: internal_server_error ( anyhow ! ( err) . context ( "Unable to parse commitments" ) ) ) ;
673+ }
674+ Err ( err) => return Err ( RestError :: internal_server_error ( anyhow ! ( err) . context ( "Tokio error" ) ) ) ,
675+ } ;
629676
630677 Ok ( ErasedJson :: pretty ( rest. ledger . get_state_paths_for_commitments ( & commitments) ?) )
631678 }
@@ -1051,6 +1098,42 @@ impl<N: Network, C: ConsensusStorage<N>, R: Routing<N>> Rest<N, C, R> {
10511098 Ok ( ( StatusCode :: OK , ErasedJson :: pretty ( value) ) )
10521099 }
10531100
1101+ /// GET /{network}/program/{id}/mapping/{name}/history/{height}?keys=key1,key2,...
1102+ #[ cfg( feature = "history" ) ]
1103+ pub ( crate ) async fn get_history_batch (
1104+ State ( rest) : State < Self > ,
1105+ Path ( ( program_id, mapping_name, height) ) : Path < HistoricalMappingRoute < N > > ,
1106+ Query ( historical_keys) : Query < HistoricalKeys > ,
1107+ ) -> Result < impl axum:: response:: IntoResponse , RestError > {
1108+ let mapping_keys = parse_historical_mapping_keys :: < N > ( & historical_keys. keys ) ?;
1109+
1110+ let values = match tokio:: task:: spawn_blocking ( move || cfg_into_iter ! ( historical_keys
1111+ . keys)
1112+ . zip ( mapping_keys)
1113+ . map ( |( key, mapping_key) | {
1114+ let value = rest
1115+ . ledger
1116+ . vm ( )
1117+ . finalize_store ( )
1118+ . get_historical_mapping_value ( program_id, mapping_name, mapping_key, height)
1119+ . map_err ( |err| {
1120+ RestError :: not_found ( err. context ( format ! (
1121+ "Could not load mapping '{mapping_name}/{key}' for program '{program_id}' from block '{height}'"
1122+ ) ) )
1123+ } ) ?;
1124+
1125+ Ok ( json ! ( { "key" : key, "value" : value } ) )
1126+ } )
1127+ . collect :: < Result < Vec < _ > , RestError > > ( ) )
1128+ . await {
1129+ Ok ( Ok ( values) ) => values,
1130+ Ok ( Err ( err) ) => return Err ( RestError :: internal_server_error ( anyhow ! ( err) . context ( "Unable to get historical mapping values" ) ) ) ,
1131+ Err ( err) => return Err ( RestError :: internal_server_error ( anyhow ! ( "Tokio error: {err}" ) ) ) ,
1132+ } ;
1133+
1134+ Ok ( ( StatusCode :: OK , ErasedJson :: pretty ( values) ) )
1135+ }
1136+
10541137 /// GET /{network}/staking/rewards/{address}/{height}
10551138 #[ cfg( feature = "history-staking-rewards" ) ]
10561139 pub ( crate ) async fn get_staking_reward (
@@ -1112,3 +1195,30 @@ impl<N: Network, C: ConsensusStorage<N>, R: Routing<N>> Rest<N, C, R> {
11121195 }
11131196 }
11141197}
1198+
1199+ #[ cfg( all( test, feature = "history" ) ) ]
1200+ mod tests {
1201+ use super :: * ;
1202+ use snarkvm:: prelude:: MainnetV0 ;
1203+
1204+ #[ test]
1205+ fn parse_historical_mapping_keys_rejects_empty ( ) {
1206+ let err = parse_historical_mapping_keys :: < MainnetV0 > ( & [ ] ) . unwrap_err ( ) ;
1207+ assert_eq ! ( err, StatusCode :: UNPROCESSABLE_ENTITY ) ;
1208+ }
1209+
1210+ #[ test]
1211+ fn parse_historical_mapping_keys_rejects_too_many ( ) {
1212+ let keys = vec ! [ String :: from( "1field" ) ; MAX_KEYS_PER_REQUEST + 1 ] ;
1213+ let err = parse_historical_mapping_keys :: < MainnetV0 > ( & keys) . unwrap_err ( ) ;
1214+ assert_eq ! ( err, StatusCode :: UNPROCESSABLE_ENTITY ) ;
1215+ }
1216+
1217+ #[ test]
1218+ fn parse_historical_mapping_keys_rejects_invalid_key_with_index ( ) {
1219+ let keys = vec ! [ String :: from( "1field" ) , String :: from( "not_a_plaintext" ) ] ;
1220+ let err = parse_historical_mapping_keys :: < MainnetV0 > ( & keys) . unwrap_err ( ) ;
1221+ assert_eq ! ( err, StatusCode :: UNPROCESSABLE_ENTITY ) ;
1222+ assert ! ( err. to_string( ) . contains( "Invalid key at index 1" ) ) ;
1223+ }
1224+ }
0 commit comments