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