safecoin_client/nonblocking/
blockhash_query.rs

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