solana_rpc_client_nonce_utils/nonblocking/
blockhash_query.rs

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