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