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 .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 .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}