safecoin_client/
blockhash_query.rs

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