solana_rpc_client_nonce_utils/nonblocking/
blockhash_query.rs

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