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