solana_rpc_client_nonce_utils/
blockhash_query.rs

1#[cfg(feature = "clap")]
2use {
3    clap::ArgMatches,
4    solana_clap_utils::{
5        input_parsers::{pubkey_of, value_of},
6        nonce::*,
7        offline::*,
8    },
9};
10use {
11    solana_commitment_config::CommitmentConfig, solana_hash::Hash, solana_pubkey::Pubkey,
12    solana_rpc_client::rpc_client::RpcClient,
13};
14
15#[derive(Debug, PartialEq, Eq)]
16pub enum Source {
17    Cluster,
18    NonceAccount(Pubkey),
19}
20
21impl Source {
22    pub fn get_blockhash(
23        &self,
24        rpc_client: &RpcClient,
25        commitment: CommitmentConfig,
26    ) -> Result<Hash, Box<dyn std::error::Error>> {
27        match self {
28            Self::Cluster => {
29                let (blockhash, _) = rpc_client.get_latest_blockhash_with_commitment(commitment)?;
30                Ok(blockhash)
31            }
32            Self::NonceAccount(ref pubkey) => {
33                let data = crate::get_account_with_commitment(rpc_client, pubkey, commitment)
34                    .and_then(|ref a| crate::data_from_account(a))?;
35                Ok(data.blockhash())
36            }
37        }
38    }
39
40    pub fn is_blockhash_valid(
41        &self,
42        rpc_client: &RpcClient,
43        blockhash: &Hash,
44        commitment: CommitmentConfig,
45    ) -> Result<bool, Box<dyn std::error::Error>> {
46        Ok(match self {
47            Self::Cluster => rpc_client.is_blockhash_valid(blockhash, commitment)?,
48            Self::NonceAccount(ref pubkey) => {
49                let _ = crate::get_account_with_commitment(rpc_client, pubkey, commitment)
50                    .and_then(|ref a| crate::data_from_account(a))?;
51                true
52            }
53        })
54    }
55}
56
57#[derive(Debug, PartialEq, Eq)]
58pub enum BlockhashQuery {
59    None(Hash),
60    FeeCalculator(Source, Hash),
61    All(Source),
62}
63
64impl BlockhashQuery {
65    pub fn new(blockhash: Option<Hash>, sign_only: bool, nonce_account: Option<Pubkey>) -> Self {
66        let source = nonce_account
67            .map(Source::NonceAccount)
68            .unwrap_or(Source::Cluster);
69        match blockhash {
70            Some(hash) if sign_only => Self::None(hash),
71            Some(hash) if !sign_only => Self::FeeCalculator(source, hash),
72            None if !sign_only => Self::All(source),
73            _ => panic!("Cannot resolve blockhash"),
74        }
75    }
76
77    #[cfg(feature = "clap")]
78    pub fn new_from_matches(matches: &ArgMatches<'_>) -> Self {
79        let blockhash = value_of(matches, BLOCKHASH_ARG.name);
80        let sign_only = matches.is_present(SIGN_ONLY_ARG.name);
81        let nonce_account = pubkey_of(matches, NONCE_ARG.name);
82        BlockhashQuery::new(blockhash, sign_only, nonce_account)
83    }
84
85    pub fn get_blockhash(
86        &self,
87        rpc_client: &RpcClient,
88        commitment: CommitmentConfig,
89    ) -> Result<Hash, Box<dyn std::error::Error>> {
90        match self {
91            BlockhashQuery::None(hash) => Ok(*hash),
92            BlockhashQuery::FeeCalculator(source, hash) => {
93                if !source.is_blockhash_valid(rpc_client, hash, commitment)? {
94                    return Err(format!("Hash has expired {hash:?}").into());
95                }
96                Ok(*hash)
97            }
98            BlockhashQuery::All(source) => source.get_blockhash(rpc_client, commitment),
99        }
100    }
101}
102
103impl Default for BlockhashQuery {
104    fn default() -> Self {
105        BlockhashQuery::All(Source::Cluster)
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    #[cfg(feature = "clap")]
112    use clap::App;
113    use {
114        super::*,
115        crate::blockhash_query,
116        serde_json::{self, json},
117        solana_account::Account,
118        solana_account_decoder::{encode_ui_account, UiAccountEncoding},
119        solana_fee_calculator::FeeCalculator,
120        solana_nonce::{self as nonce, state::DurableNonce},
121        solana_rpc_client_api::{
122            request::RpcRequest,
123            response::{Response, RpcBlockhash, RpcResponseContext},
124        },
125        solana_sha256_hasher::hash,
126        std::collections::HashMap,
127    };
128
129    #[test]
130    fn test_blockhash_query_new_ok() {
131        let blockhash = hash(&[1u8]);
132        let nonce_pubkey = Pubkey::from([1u8; 32]);
133
134        assert_eq!(
135            BlockhashQuery::new(Some(blockhash), true, None),
136            BlockhashQuery::None(blockhash),
137        );
138        assert_eq!(
139            BlockhashQuery::new(Some(blockhash), false, None),
140            BlockhashQuery::FeeCalculator(blockhash_query::Source::Cluster, blockhash),
141        );
142        assert_eq!(
143            BlockhashQuery::new(None, false, None),
144            BlockhashQuery::All(blockhash_query::Source::Cluster)
145        );
146
147        assert_eq!(
148            BlockhashQuery::new(Some(blockhash), true, Some(nonce_pubkey)),
149            BlockhashQuery::None(blockhash),
150        );
151        assert_eq!(
152            BlockhashQuery::new(Some(blockhash), false, Some(nonce_pubkey)),
153            BlockhashQuery::FeeCalculator(
154                blockhash_query::Source::NonceAccount(nonce_pubkey),
155                blockhash
156            ),
157        );
158        assert_eq!(
159            BlockhashQuery::new(None, false, Some(nonce_pubkey)),
160            BlockhashQuery::All(blockhash_query::Source::NonceAccount(nonce_pubkey)),
161        );
162    }
163
164    #[test]
165    #[should_panic]
166    fn test_blockhash_query_new_no_nonce_fail() {
167        BlockhashQuery::new(None, true, None);
168    }
169
170    #[test]
171    #[should_panic]
172    fn test_blockhash_query_new_nonce_fail() {
173        let nonce_pubkey = Pubkey::from([1u8; 32]);
174        BlockhashQuery::new(None, true, Some(nonce_pubkey));
175    }
176
177    #[cfg(feature = "clap")]
178    #[test]
179    fn test_blockhash_query_new_from_matches_ok() {
180        let test_commands = App::new("blockhash_query_test")
181            .nonce_args(false)
182            .offline_args();
183        let blockhash = hash(&[1u8]);
184        let blockhash_string = blockhash.to_string();
185
186        let matches = test_commands.clone().get_matches_from(vec![
187            "blockhash_query_test",
188            "--blockhash",
189            &blockhash_string,
190            "--sign-only",
191        ]);
192        assert_eq!(
193            BlockhashQuery::new_from_matches(&matches),
194            BlockhashQuery::None(blockhash),
195        );
196
197        let matches = test_commands.clone().get_matches_from(vec![
198            "blockhash_query_test",
199            "--blockhash",
200            &blockhash_string,
201        ]);
202        assert_eq!(
203            BlockhashQuery::new_from_matches(&matches),
204            BlockhashQuery::FeeCalculator(blockhash_query::Source::Cluster, blockhash),
205        );
206
207        let matches = test_commands
208            .clone()
209            .get_matches_from(vec!["blockhash_query_test"]);
210        assert_eq!(
211            BlockhashQuery::new_from_matches(&matches),
212            BlockhashQuery::All(blockhash_query::Source::Cluster),
213        );
214
215        let nonce_pubkey = Pubkey::from([1u8; 32]);
216        let nonce_string = nonce_pubkey.to_string();
217        let matches = test_commands.clone().get_matches_from(vec![
218            "blockhash_query_test",
219            "--blockhash",
220            &blockhash_string,
221            "--sign-only",
222            "--nonce",
223            &nonce_string,
224        ]);
225        assert_eq!(
226            BlockhashQuery::new_from_matches(&matches),
227            BlockhashQuery::None(blockhash),
228        );
229
230        let matches = test_commands.clone().get_matches_from(vec![
231            "blockhash_query_test",
232            "--blockhash",
233            &blockhash_string,
234            "--nonce",
235            &nonce_string,
236        ]);
237        assert_eq!(
238            BlockhashQuery::new_from_matches(&matches),
239            BlockhashQuery::FeeCalculator(
240                blockhash_query::Source::NonceAccount(nonce_pubkey),
241                blockhash
242            ),
243        );
244    }
245
246    #[cfg(feature = "clap")]
247    #[test]
248    #[should_panic]
249    fn test_blockhash_query_new_from_matches_without_nonce_fail() {
250        let test_commands = App::new("blockhash_query_test")
251            .arg(blockhash_arg())
252            // We can really only hit this case if the arg requirements
253            // are broken, so unset the requires() to recreate that condition
254            .arg(sign_only_arg().requires(""));
255
256        let matches = test_commands.get_matches_from(vec!["blockhash_query_test", "--sign-only"]);
257        BlockhashQuery::new_from_matches(&matches);
258    }
259
260    #[cfg(feature = "clap")]
261    #[test]
262    #[should_panic]
263    fn test_blockhash_query_new_from_matches_with_nonce_fail() {
264        let test_commands = App::new("blockhash_query_test")
265            .arg(blockhash_arg())
266            // We can really only hit this case if the arg requirements
267            // are broken, so unset the requires() to recreate that condition
268            .arg(sign_only_arg().requires(""));
269        let nonce_pubkey = Pubkey::from([1u8; 32]);
270        let nonce_string = nonce_pubkey.to_string();
271
272        let matches = test_commands.get_matches_from(vec![
273            "blockhash_query_test",
274            "--sign-only",
275            "--nonce",
276            &nonce_string,
277        ]);
278        BlockhashQuery::new_from_matches(&matches);
279    }
280
281    #[test]
282    #[allow(deprecated)]
283    fn test_blockhash_query_get_blockhash() {
284        let test_blockhash = hash(&[0u8]);
285        let rpc_blockhash = hash(&[1u8]);
286        let get_latest_blockhash_response = json!(Response {
287            context: RpcResponseContext {
288                slot: 1,
289                api_version: None
290            },
291            value: json!(RpcBlockhash {
292                blockhash: rpc_blockhash.to_string(),
293                last_valid_block_height: 42,
294            }),
295        });
296        let is_blockhash_valid_response = json!(Response {
297            context: RpcResponseContext {
298                slot: 1,
299                api_version: None
300            },
301            value: true,
302        });
303        let mut mocks = HashMap::new();
304        mocks.insert(
305            RpcRequest::GetLatestBlockhash,
306            get_latest_blockhash_response.clone(),
307        );
308        let rpc_client = RpcClient::new_mock_with_mocks("".to_string(), mocks);
309        assert_eq!(
310            BlockhashQuery::default()
311                .get_blockhash(&rpc_client, CommitmentConfig::default())
312                .unwrap(),
313            rpc_blockhash,
314        );
315        let mut mocks = HashMap::new();
316        mocks.insert(
317            RpcRequest::IsBlockhashValid,
318            is_blockhash_valid_response.clone(),
319        );
320        let rpc_client = RpcClient::new_mock_with_mocks("".to_string(), mocks);
321        assert_eq!(
322            BlockhashQuery::FeeCalculator(Source::Cluster, test_blockhash)
323                .get_blockhash(&rpc_client, CommitmentConfig::default())
324                .unwrap(),
325            test_blockhash,
326        );
327        let mut mocks = HashMap::new();
328        mocks.insert(
329            RpcRequest::GetLatestBlockhash,
330            get_latest_blockhash_response,
331        );
332        let rpc_client = RpcClient::new_mock_with_mocks("".to_string(), mocks);
333        assert_eq!(
334            BlockhashQuery::None(test_blockhash)
335                .get_blockhash(&rpc_client, CommitmentConfig::default())
336                .unwrap(),
337            test_blockhash,
338        );
339        let rpc_client = RpcClient::new_mock("fails".to_string());
340        assert!(BlockhashQuery::default()
341            .get_blockhash(&rpc_client, CommitmentConfig::default())
342            .is_err());
343
344        let durable_nonce = DurableNonce::from_blockhash(&Hash::new_from_array([2u8; 32]));
345        let nonce_blockhash = *durable_nonce.as_hash();
346        let nonce_fee_calc = FeeCalculator::new(4242);
347        let data = nonce::state::Data {
348            authority: Pubkey::from([3u8; 32]),
349            durable_nonce,
350            fee_calculator: nonce_fee_calc,
351        };
352        let nonce_account = Account::new_data_with_space(
353            42,
354            &nonce::versions::Versions::new(nonce::state::State::Initialized(data)),
355            nonce::state::State::size(),
356            &solana_sdk_ids::system_program::id(),
357        )
358        .unwrap();
359        let nonce_pubkey = Pubkey::from([4u8; 32]);
360        let rpc_nonce_account = encode_ui_account(
361            &nonce_pubkey,
362            &nonce_account,
363            UiAccountEncoding::Base64,
364            None,
365            None,
366        );
367        let get_account_response = json!(Response {
368            context: RpcResponseContext {
369                slot: 1,
370                api_version: None
371            },
372            value: json!(Some(rpc_nonce_account)),
373        });
374
375        let mut mocks = HashMap::new();
376        mocks.insert(RpcRequest::GetAccountInfo, get_account_response.clone());
377        let rpc_client = RpcClient::new_mock_with_mocks("".to_string(), mocks);
378        assert_eq!(
379            BlockhashQuery::All(Source::NonceAccount(nonce_pubkey))
380                .get_blockhash(&rpc_client, CommitmentConfig::default())
381                .unwrap(),
382            nonce_blockhash,
383        );
384        let mut mocks = HashMap::new();
385        mocks.insert(RpcRequest::GetAccountInfo, get_account_response.clone());
386        let rpc_client = RpcClient::new_mock_with_mocks("".to_string(), mocks);
387        assert_eq!(
388            BlockhashQuery::FeeCalculator(Source::NonceAccount(nonce_pubkey), nonce_blockhash)
389                .get_blockhash(&rpc_client, CommitmentConfig::default())
390                .unwrap(),
391            nonce_blockhash,
392        );
393        let mut mocks = HashMap::new();
394        mocks.insert(RpcRequest::GetAccountInfo, get_account_response);
395        let rpc_client = RpcClient::new_mock_with_mocks("".to_string(), mocks);
396        assert_eq!(
397            BlockhashQuery::None(nonce_blockhash)
398                .get_blockhash(&rpc_client, CommitmentConfig::default())
399                .unwrap(),
400            nonce_blockhash,
401        );
402
403        let rpc_client = RpcClient::new_mock("fails".to_string());
404        assert!(BlockhashQuery::All(Source::NonceAccount(nonce_pubkey))
405            .get_blockhash(&rpc_client, CommitmentConfig::default())
406            .is_err());
407    }
408}