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_rpc_client::rpc_client::RpcClient,
12    solana_sdk::{commitment_config::CommitmentConfig, hash::Hash, pubkey::Pubkey},
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_decoder::{encode_ui_account, UiAccountEncoding},
118        solana_rpc_client_api::{
119            request::RpcRequest,
120            response::{Response, RpcBlockhash, RpcResponseContext},
121        },
122        solana_sdk::{
123            account::Account,
124            fee_calculator::FeeCalculator,
125            hash::hash,
126            nonce::{self, state::DurableNonce},
127            system_program,
128        },
129        std::collections::HashMap,
130    };
131
132    #[test]
133    fn test_blockhash_query_new_ok() {
134        let blockhash = hash(&[1u8]);
135        let nonce_pubkey = Pubkey::from([1u8; 32]);
136
137        assert_eq!(
138            BlockhashQuery::new(Some(blockhash), true, None),
139            BlockhashQuery::None(blockhash),
140        );
141        assert_eq!(
142            BlockhashQuery::new(Some(blockhash), false, None),
143            BlockhashQuery::FeeCalculator(blockhash_query::Source::Cluster, blockhash),
144        );
145        assert_eq!(
146            BlockhashQuery::new(None, false, None),
147            BlockhashQuery::All(blockhash_query::Source::Cluster)
148        );
149
150        assert_eq!(
151            BlockhashQuery::new(Some(blockhash), true, Some(nonce_pubkey)),
152            BlockhashQuery::None(blockhash),
153        );
154        assert_eq!(
155            BlockhashQuery::new(Some(blockhash), false, Some(nonce_pubkey)),
156            BlockhashQuery::FeeCalculator(
157                blockhash_query::Source::NonceAccount(nonce_pubkey),
158                blockhash
159            ),
160        );
161        assert_eq!(
162            BlockhashQuery::new(None, false, Some(nonce_pubkey)),
163            BlockhashQuery::All(blockhash_query::Source::NonceAccount(nonce_pubkey)),
164        );
165    }
166
167    #[test]
168    #[should_panic]
169    fn test_blockhash_query_new_no_nonce_fail() {
170        BlockhashQuery::new(None, true, None);
171    }
172
173    #[test]
174    #[should_panic]
175    fn test_blockhash_query_new_nonce_fail() {
176        let nonce_pubkey = Pubkey::from([1u8; 32]);
177        BlockhashQuery::new(None, true, Some(nonce_pubkey));
178    }
179
180    #[cfg(feature = "clap")]
181    #[test]
182    fn test_blockhash_query_new_from_matches_ok() {
183        let test_commands = App::new("blockhash_query_test")
184            .nonce_args(false)
185            .offline_args();
186        let blockhash = hash(&[1u8]);
187        let blockhash_string = blockhash.to_string();
188
189        let matches = test_commands.clone().get_matches_from(vec![
190            "blockhash_query_test",
191            "--blockhash",
192            &blockhash_string,
193            "--sign-only",
194        ]);
195        assert_eq!(
196            BlockhashQuery::new_from_matches(&matches),
197            BlockhashQuery::None(blockhash),
198        );
199
200        let matches = test_commands.clone().get_matches_from(vec![
201            "blockhash_query_test",
202            "--blockhash",
203            &blockhash_string,
204        ]);
205        assert_eq!(
206            BlockhashQuery::new_from_matches(&matches),
207            BlockhashQuery::FeeCalculator(blockhash_query::Source::Cluster, blockhash),
208        );
209
210        let matches = test_commands
211            .clone()
212            .get_matches_from(vec!["blockhash_query_test"]);
213        assert_eq!(
214            BlockhashQuery::new_from_matches(&matches),
215            BlockhashQuery::All(blockhash_query::Source::Cluster),
216        );
217
218        let nonce_pubkey = Pubkey::from([1u8; 32]);
219        let nonce_string = nonce_pubkey.to_string();
220        let matches = test_commands.clone().get_matches_from(vec![
221            "blockhash_query_test",
222            "--blockhash",
223            &blockhash_string,
224            "--sign-only",
225            "--nonce",
226            &nonce_string,
227        ]);
228        assert_eq!(
229            BlockhashQuery::new_from_matches(&matches),
230            BlockhashQuery::None(blockhash),
231        );
232
233        let matches = test_commands.clone().get_matches_from(vec![
234            "blockhash_query_test",
235            "--blockhash",
236            &blockhash_string,
237            "--nonce",
238            &nonce_string,
239        ]);
240        assert_eq!(
241            BlockhashQuery::new_from_matches(&matches),
242            BlockhashQuery::FeeCalculator(
243                blockhash_query::Source::NonceAccount(nonce_pubkey),
244                blockhash
245            ),
246        );
247    }
248
249    #[cfg(feature = "clap")]
250    #[test]
251    #[should_panic]
252    fn test_blockhash_query_new_from_matches_without_nonce_fail() {
253        let test_commands = App::new("blockhash_query_test")
254            .arg(blockhash_arg())
255            // We can really only hit this case if the arg requirements
256            // are broken, so unset the requires() to recreate that condition
257            .arg(sign_only_arg().requires(""));
258
259        let matches = test_commands.get_matches_from(vec!["blockhash_query_test", "--sign-only"]);
260        BlockhashQuery::new_from_matches(&matches);
261    }
262
263    #[cfg(feature = "clap")]
264    #[test]
265    #[should_panic]
266    fn test_blockhash_query_new_from_matches_with_nonce_fail() {
267        let test_commands = App::new("blockhash_query_test")
268            .arg(blockhash_arg())
269            // We can really only hit this case if the arg requirements
270            // are broken, so unset the requires() to recreate that condition
271            .arg(sign_only_arg().requires(""));
272        let nonce_pubkey = Pubkey::from([1u8; 32]);
273        let nonce_string = nonce_pubkey.to_string();
274
275        let matches = test_commands.get_matches_from(vec![
276            "blockhash_query_test",
277            "--sign-only",
278            "--nonce",
279            &nonce_string,
280        ]);
281        BlockhashQuery::new_from_matches(&matches);
282    }
283
284    #[test]
285    #[allow(deprecated)]
286    fn test_blockhash_query_get_blockhash() {
287        let test_blockhash = hash(&[0u8]);
288        let rpc_blockhash = hash(&[1u8]);
289        let get_latest_blockhash_response = json!(Response {
290            context: RpcResponseContext {
291                slot: 1,
292                api_version: None
293            },
294            value: json!(RpcBlockhash {
295                blockhash: rpc_blockhash.to_string(),
296                last_valid_block_height: 42,
297            }),
298        });
299        let is_blockhash_valid_response = json!(Response {
300            context: RpcResponseContext {
301                slot: 1,
302                api_version: None
303            },
304            value: true,
305        });
306        let mut mocks = HashMap::new();
307        mocks.insert(
308            RpcRequest::GetLatestBlockhash,
309            get_latest_blockhash_response.clone(),
310        );
311        let rpc_client = RpcClient::new_mock_with_mocks("".to_string(), mocks);
312        assert_eq!(
313            BlockhashQuery::default()
314                .get_blockhash(&rpc_client, CommitmentConfig::default())
315                .unwrap(),
316            rpc_blockhash,
317        );
318        let mut mocks = HashMap::new();
319        mocks.insert(
320            RpcRequest::IsBlockhashValid,
321            is_blockhash_valid_response.clone(),
322        );
323        let rpc_client = RpcClient::new_mock_with_mocks("".to_string(), mocks);
324        assert_eq!(
325            BlockhashQuery::FeeCalculator(Source::Cluster, test_blockhash)
326                .get_blockhash(&rpc_client, CommitmentConfig::default())
327                .unwrap(),
328            test_blockhash,
329        );
330        let mut mocks = HashMap::new();
331        mocks.insert(
332            RpcRequest::GetLatestBlockhash,
333            get_latest_blockhash_response,
334        );
335        let rpc_client = RpcClient::new_mock_with_mocks("".to_string(), mocks);
336        assert_eq!(
337            BlockhashQuery::None(test_blockhash)
338                .get_blockhash(&rpc_client, CommitmentConfig::default())
339                .unwrap(),
340            test_blockhash,
341        );
342        let rpc_client = RpcClient::new_mock("fails".to_string());
343        assert!(BlockhashQuery::default()
344            .get_blockhash(&rpc_client, CommitmentConfig::default())
345            .is_err());
346
347        let durable_nonce = DurableNonce::from_blockhash(&Hash::new(&[2u8; 32]));
348        let nonce_blockhash = *durable_nonce.as_hash();
349        let nonce_fee_calc = FeeCalculator::new(4242);
350        let data = nonce::state::Data {
351            authority: Pubkey::from([3u8; 32]),
352            durable_nonce,
353            fee_calculator: nonce_fee_calc,
354        };
355        let nonce_account = Account::new_data_with_space(
356            42,
357            &nonce::state::Versions::new(nonce::State::Initialized(data)),
358            nonce::State::size(),
359            &system_program::id(),
360        )
361        .unwrap();
362        let nonce_pubkey = Pubkey::from([4u8; 32]);
363        let rpc_nonce_account = encode_ui_account(
364            &nonce_pubkey,
365            &nonce_account,
366            UiAccountEncoding::Base64,
367            None,
368            None,
369        );
370        let get_account_response = json!(Response {
371            context: RpcResponseContext {
372                slot: 1,
373                api_version: None
374            },
375            value: json!(Some(rpc_nonce_account)),
376        });
377
378        let mut mocks = HashMap::new();
379        mocks.insert(RpcRequest::GetAccountInfo, get_account_response.clone());
380        let rpc_client = RpcClient::new_mock_with_mocks("".to_string(), mocks);
381        assert_eq!(
382            BlockhashQuery::All(Source::NonceAccount(nonce_pubkey))
383                .get_blockhash(&rpc_client, CommitmentConfig::default())
384                .unwrap(),
385            nonce_blockhash,
386        );
387        let mut mocks = HashMap::new();
388        mocks.insert(RpcRequest::GetAccountInfo, get_account_response.clone());
389        let rpc_client = RpcClient::new_mock_with_mocks("".to_string(), mocks);
390        assert_eq!(
391            BlockhashQuery::FeeCalculator(Source::NonceAccount(nonce_pubkey), nonce_blockhash)
392                .get_blockhash(&rpc_client, CommitmentConfig::default())
393                .unwrap(),
394            nonce_blockhash,
395        );
396        let mut mocks = HashMap::new();
397        mocks.insert(RpcRequest::GetAccountInfo, get_account_response);
398        let rpc_client = RpcClient::new_mock_with_mocks("".to_string(), mocks);
399        assert_eq!(
400            BlockhashQuery::None(nonce_blockhash)
401                .get_blockhash(&rpc_client, CommitmentConfig::default())
402                .unwrap(),
403            nonce_blockhash,
404        );
405
406        let rpc_client = RpcClient::new_mock("fails".to_string());
407        assert!(BlockhashQuery::All(Source::NonceAccount(nonce_pubkey))
408            .get_blockhash(&rpc_client, CommitmentConfig::default())
409            .is_err());
410    }
411}