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_rpc_client::rpc_client::RpcClient,
12 solana_sdk::{commitment_config::CommitmentConfig, hash::Hash, pubkey::Pubkey},
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_decoder::{encode_ui_account, UiAccountEncoding},
118 solana_rpc_client_api::{
119 request::RpcRequest,
120 response::{Response, RpcBlockhash, RpcResponseContext},
121 },
122 solana_sdk::{
123 account::Account,
124 fee_calculator::FeeCalculator,
125 hash::hash,
126 nonce::{self, state::DurableNonce},
127 system_program,
128 },
129 std::collections::HashMap,
130 };
131
132 #[test]
133 fn test_blockhash_query_new_ok() {
134 let blockhash = hash(&[1u8]);
135 let nonce_pubkey = Pubkey::from([1u8; 32]);
136
137 assert_eq!(
138 BlockhashQuery::new(Some(blockhash), true, None),
139 BlockhashQuery::None(blockhash),
140 );
141 assert_eq!(
142 BlockhashQuery::new(Some(blockhash), false, None),
143 BlockhashQuery::FeeCalculator(blockhash_query::Source::Cluster, blockhash),
144 );
145 assert_eq!(
146 BlockhashQuery::new(None, false, None),
147 BlockhashQuery::All(blockhash_query::Source::Cluster)
148 );
149
150 assert_eq!(
151 BlockhashQuery::new(Some(blockhash), true, Some(nonce_pubkey)),
152 BlockhashQuery::None(blockhash),
153 );
154 assert_eq!(
155 BlockhashQuery::new(Some(blockhash), false, Some(nonce_pubkey)),
156 BlockhashQuery::FeeCalculator(
157 blockhash_query::Source::NonceAccount(nonce_pubkey),
158 blockhash
159 ),
160 );
161 assert_eq!(
162 BlockhashQuery::new(None, false, Some(nonce_pubkey)),
163 BlockhashQuery::All(blockhash_query::Source::NonceAccount(nonce_pubkey)),
164 );
165 }
166
167 #[test]
168 #[should_panic]
169 fn test_blockhash_query_new_no_nonce_fail() {
170 BlockhashQuery::new(None, true, None);
171 }
172
173 #[test]
174 #[should_panic]
175 fn test_blockhash_query_new_nonce_fail() {
176 let nonce_pubkey = Pubkey::from([1u8; 32]);
177 BlockhashQuery::new(None, true, Some(nonce_pubkey));
178 }
179
180 #[cfg(feature = "clap")]
181 #[test]
182 fn test_blockhash_query_new_from_matches_ok() {
183 let test_commands = App::new("blockhash_query_test")
184 .nonce_args(false)
185 .offline_args();
186 let blockhash = hash(&[1u8]);
187 let blockhash_string = blockhash.to_string();
188
189 let matches = test_commands.clone().get_matches_from(vec![
190 "blockhash_query_test",
191 "--blockhash",
192 &blockhash_string,
193 "--sign-only",
194 ]);
195 assert_eq!(
196 BlockhashQuery::new_from_matches(&matches),
197 BlockhashQuery::None(blockhash),
198 );
199
200 let matches = test_commands.clone().get_matches_from(vec![
201 "blockhash_query_test",
202 "--blockhash",
203 &blockhash_string,
204 ]);
205 assert_eq!(
206 BlockhashQuery::new_from_matches(&matches),
207 BlockhashQuery::FeeCalculator(blockhash_query::Source::Cluster, blockhash),
208 );
209
210 let matches = test_commands
211 .clone()
212 .get_matches_from(vec!["blockhash_query_test"]);
213 assert_eq!(
214 BlockhashQuery::new_from_matches(&matches),
215 BlockhashQuery::All(blockhash_query::Source::Cluster),
216 );
217
218 let nonce_pubkey = Pubkey::from([1u8; 32]);
219 let nonce_string = nonce_pubkey.to_string();
220 let matches = test_commands.clone().get_matches_from(vec![
221 "blockhash_query_test",
222 "--blockhash",
223 &blockhash_string,
224 "--sign-only",
225 "--nonce",
226 &nonce_string,
227 ]);
228 assert_eq!(
229 BlockhashQuery::new_from_matches(&matches),
230 BlockhashQuery::None(blockhash),
231 );
232
233 let matches = test_commands.clone().get_matches_from(vec![
234 "blockhash_query_test",
235 "--blockhash",
236 &blockhash_string,
237 "--nonce",
238 &nonce_string,
239 ]);
240 assert_eq!(
241 BlockhashQuery::new_from_matches(&matches),
242 BlockhashQuery::FeeCalculator(
243 blockhash_query::Source::NonceAccount(nonce_pubkey),
244 blockhash
245 ),
246 );
247 }
248
249 #[cfg(feature = "clap")]
250 #[test]
251 #[should_panic]
252 fn test_blockhash_query_new_from_matches_without_nonce_fail() {
253 let test_commands = App::new("blockhash_query_test")
254 .arg(blockhash_arg())
255 .arg(sign_only_arg().requires(""));
258
259 let matches = test_commands.get_matches_from(vec!["blockhash_query_test", "--sign-only"]);
260 BlockhashQuery::new_from_matches(&matches);
261 }
262
263 #[cfg(feature = "clap")]
264 #[test]
265 #[should_panic]
266 fn test_blockhash_query_new_from_matches_with_nonce_fail() {
267 let test_commands = App::new("blockhash_query_test")
268 .arg(blockhash_arg())
269 .arg(sign_only_arg().requires(""));
272 let nonce_pubkey = Pubkey::from([1u8; 32]);
273 let nonce_string = nonce_pubkey.to_string();
274
275 let matches = test_commands.get_matches_from(vec![
276 "blockhash_query_test",
277 "--sign-only",
278 "--nonce",
279 &nonce_string,
280 ]);
281 BlockhashQuery::new_from_matches(&matches);
282 }
283
284 #[test]
285 #[allow(deprecated)]
286 fn test_blockhash_query_get_blockhash() {
287 let test_blockhash = hash(&[0u8]);
288 let rpc_blockhash = hash(&[1u8]);
289 let get_latest_blockhash_response = json!(Response {
290 context: RpcResponseContext {
291 slot: 1,
292 api_version: None
293 },
294 value: json!(RpcBlockhash {
295 blockhash: rpc_blockhash.to_string(),
296 last_valid_block_height: 42,
297 }),
298 });
299 let is_blockhash_valid_response = json!(Response {
300 context: RpcResponseContext {
301 slot: 1,
302 api_version: None
303 },
304 value: true,
305 });
306 let mut mocks = HashMap::new();
307 mocks.insert(
308 RpcRequest::GetLatestBlockhash,
309 get_latest_blockhash_response.clone(),
310 );
311 let rpc_client = RpcClient::new_mock_with_mocks("".to_string(), mocks);
312 assert_eq!(
313 BlockhashQuery::default()
314 .get_blockhash(&rpc_client, CommitmentConfig::default())
315 .unwrap(),
316 rpc_blockhash,
317 );
318 let mut mocks = HashMap::new();
319 mocks.insert(
320 RpcRequest::IsBlockhashValid,
321 is_blockhash_valid_response.clone(),
322 );
323 let rpc_client = RpcClient::new_mock_with_mocks("".to_string(), mocks);
324 assert_eq!(
325 BlockhashQuery::FeeCalculator(Source::Cluster, test_blockhash)
326 .get_blockhash(&rpc_client, CommitmentConfig::default())
327 .unwrap(),
328 test_blockhash,
329 );
330 let mut mocks = HashMap::new();
331 mocks.insert(
332 RpcRequest::GetLatestBlockhash,
333 get_latest_blockhash_response,
334 );
335 let rpc_client = RpcClient::new_mock_with_mocks("".to_string(), mocks);
336 assert_eq!(
337 BlockhashQuery::None(test_blockhash)
338 .get_blockhash(&rpc_client, CommitmentConfig::default())
339 .unwrap(),
340 test_blockhash,
341 );
342 let rpc_client = RpcClient::new_mock("fails".to_string());
343 assert!(BlockhashQuery::default()
344 .get_blockhash(&rpc_client, CommitmentConfig::default())
345 .is_err());
346
347 let durable_nonce = DurableNonce::from_blockhash(&Hash::new(&[2u8; 32]));
348 let nonce_blockhash = *durable_nonce.as_hash();
349 let nonce_fee_calc = FeeCalculator::new(4242);
350 let data = nonce::state::Data {
351 authority: Pubkey::from([3u8; 32]),
352 durable_nonce,
353 fee_calculator: nonce_fee_calc,
354 };
355 let nonce_account = Account::new_data_with_space(
356 42,
357 &nonce::state::Versions::new(nonce::State::Initialized(data)),
358 nonce::State::size(),
359 &system_program::id(),
360 )
361 .unwrap();
362 let nonce_pubkey = Pubkey::from([4u8; 32]);
363 let rpc_nonce_account = encode_ui_account(
364 &nonce_pubkey,
365 &nonce_account,
366 UiAccountEncoding::Base64,
367 None,
368 None,
369 );
370 let get_account_response = json!(Response {
371 context: RpcResponseContext {
372 slot: 1,
373 api_version: None
374 },
375 value: json!(Some(rpc_nonce_account)),
376 });
377
378 let mut mocks = HashMap::new();
379 mocks.insert(RpcRequest::GetAccountInfo, get_account_response.clone());
380 let rpc_client = RpcClient::new_mock_with_mocks("".to_string(), mocks);
381 assert_eq!(
382 BlockhashQuery::All(Source::NonceAccount(nonce_pubkey))
383 .get_blockhash(&rpc_client, CommitmentConfig::default())
384 .unwrap(),
385 nonce_blockhash,
386 );
387 let mut mocks = HashMap::new();
388 mocks.insert(RpcRequest::GetAccountInfo, get_account_response.clone());
389 let rpc_client = RpcClient::new_mock_with_mocks("".to_string(), mocks);
390 assert_eq!(
391 BlockhashQuery::FeeCalculator(Source::NonceAccount(nonce_pubkey), nonce_blockhash)
392 .get_blockhash(&rpc_client, CommitmentConfig::default())
393 .unwrap(),
394 nonce_blockhash,
395 );
396 let mut mocks = HashMap::new();
397 mocks.insert(RpcRequest::GetAccountInfo, get_account_response);
398 let rpc_client = RpcClient::new_mock_with_mocks("".to_string(), mocks);
399 assert_eq!(
400 BlockhashQuery::None(nonce_blockhash)
401 .get_blockhash(&rpc_client, CommitmentConfig::default())
402 .unwrap(),
403 nonce_blockhash,
404 );
405
406 let rpc_client = RpcClient::new_mock("fails".to_string());
407 assert!(BlockhashQuery::All(Source::NonceAccount(nonce_pubkey))
408 .get_blockhash(&rpc_client, CommitmentConfig::default())
409 .is_err());
410 }
411}