1pub use {
9 crate::error::BanksClientError,
10 solana_banks_interface::{BanksClient as TarpcClient, TransactionStatus},
11};
12use {
13 borsh::BorshDeserialize,
14 futures::future::join_all,
15 solana_banks_interface::{
16 BanksRequest, BanksResponse, BanksTransactionResultWithMetadata,
17 BanksTransactionResultWithSimulation,
18 },
19 solana_program::{
20 clock::Slot, hash::Hash, program_pack::Pack, pubkey::Pubkey, rent::Rent, sysvar::Sysvar,
21 },
22 solana_sdk::{
23 account::{from_account, Account},
24 commitment_config::CommitmentLevel,
25 message::Message,
26 signature::Signature,
27 transaction::{self, VersionedTransaction},
28 },
29 tarpc::{
30 client::{self, NewClient, RequestDispatch},
31 context::{self, Context},
32 serde_transport::tcp,
33 ClientMessage, Response, Transport,
34 },
35 tokio::net::ToSocketAddrs,
36 tokio_serde::formats::Bincode,
37};
38
39mod error;
40
41pub trait BanksClientExt {}
43
44#[derive(Clone)]
45pub struct BanksClient {
46 inner: TarpcClient,
47}
48
49impl BanksClient {
50 #[allow(clippy::new_ret_no_self)]
51 pub fn new<C>(
52 config: client::Config,
53 transport: C,
54 ) -> NewClient<TarpcClient, RequestDispatch<BanksRequest, BanksResponse, C>>
55 where
56 C: Transport<ClientMessage<BanksRequest>, Response<BanksResponse>>,
57 {
58 TarpcClient::new(config, transport)
59 }
60
61 pub async fn send_transaction_with_context(
62 &self,
63 ctx: Context,
64 transaction: impl Into<VersionedTransaction>,
65 ) -> Result<(), BanksClientError> {
66 self.inner
67 .send_transaction_with_context(ctx, transaction.into())
68 .await
69 .map_err(Into::into)
70 }
71
72 pub async fn get_transaction_status_with_context(
73 &self,
74 ctx: Context,
75 signature: Signature,
76 ) -> Result<Option<TransactionStatus>, BanksClientError> {
77 self.inner
78 .get_transaction_status_with_context(ctx, signature)
79 .await
80 .map_err(Into::into)
81 }
82
83 pub async fn get_slot_with_context(
84 &self,
85 ctx: Context,
86 commitment: CommitmentLevel,
87 ) -> Result<Slot, BanksClientError> {
88 self.inner
89 .get_slot_with_context(ctx, commitment)
90 .await
91 .map_err(Into::into)
92 }
93
94 pub async fn get_block_height_with_context(
95 &self,
96 ctx: Context,
97 commitment: CommitmentLevel,
98 ) -> Result<Slot, BanksClientError> {
99 self.inner
100 .get_block_height_with_context(ctx, commitment)
101 .await
102 .map_err(Into::into)
103 }
104
105 pub async fn process_transaction_with_commitment_and_context(
106 &self,
107 ctx: Context,
108 transaction: impl Into<VersionedTransaction>,
109 commitment: CommitmentLevel,
110 ) -> Result<Option<transaction::Result<()>>, BanksClientError> {
111 self.inner
112 .process_transaction_with_commitment_and_context(ctx, transaction.into(), commitment)
113 .await
114 .map_err(Into::into)
115 }
116
117 pub async fn process_transaction_with_preflight_and_commitment_and_context(
118 &self,
119 ctx: Context,
120 transaction: impl Into<VersionedTransaction>,
121 commitment: CommitmentLevel,
122 ) -> Result<BanksTransactionResultWithSimulation, BanksClientError> {
123 self.inner
124 .process_transaction_with_preflight_and_commitment_and_context(
125 ctx,
126 transaction.into(),
127 commitment,
128 )
129 .await
130 .map_err(Into::into)
131 }
132
133 pub async fn process_transaction_with_metadata_and_context(
134 &self,
135 ctx: Context,
136 transaction: impl Into<VersionedTransaction>,
137 ) -> Result<BanksTransactionResultWithMetadata, BanksClientError> {
138 self.inner
139 .process_transaction_with_metadata_and_context(ctx, transaction.into())
140 .await
141 .map_err(Into::into)
142 }
143
144 pub async fn simulate_transaction_with_commitment_and_context(
145 &self,
146 ctx: Context,
147 transaction: impl Into<VersionedTransaction>,
148 commitment: CommitmentLevel,
149 ) -> Result<BanksTransactionResultWithSimulation, BanksClientError> {
150 self.inner
151 .simulate_transaction_with_commitment_and_context(ctx, transaction.into(), commitment)
152 .await
153 .map_err(Into::into)
154 }
155
156 pub async fn get_account_with_commitment_and_context(
157 &self,
158 ctx: Context,
159 address: Pubkey,
160 commitment: CommitmentLevel,
161 ) -> Result<Option<Account>, BanksClientError> {
162 self.inner
163 .get_account_with_commitment_and_context(ctx, address, commitment)
164 .await
165 .map_err(Into::into)
166 }
167
168 pub async fn send_transaction(
172 &self,
173 transaction: impl Into<VersionedTransaction>,
174 ) -> Result<(), BanksClientError> {
175 self.send_transaction_with_context(context::current(), transaction.into())
176 .await
177 }
178
179 pub async fn get_sysvar<T: Sysvar>(&self) -> Result<T, BanksClientError> {
181 let sysvar = self
182 .get_account(T::id())
183 .await?
184 .ok_or(BanksClientError::ClientError("Sysvar not present"))?;
185 from_account::<T, _>(&sysvar).ok_or(BanksClientError::ClientError(
186 "Failed to deserialize sysvar",
187 ))
188 }
189
190 pub async fn get_rent(&self) -> Result<Rent, BanksClientError> {
192 self.get_sysvar::<Rent>().await
193 }
194
195 pub async fn process_transaction_with_commitment(
198 &self,
199 transaction: impl Into<VersionedTransaction>,
200 commitment: CommitmentLevel,
201 ) -> Result<(), BanksClientError> {
202 let ctx = context::current();
203 match self
204 .process_transaction_with_commitment_and_context(ctx, transaction, commitment)
205 .await?
206 {
207 None => Err(BanksClientError::ClientError(
208 "invalid blockhash or fee-payer",
209 )),
210 Some(transaction_result) => Ok(transaction_result?),
211 }
212 }
213
214 pub async fn process_transaction_with_metadata(
216 &self,
217 transaction: impl Into<VersionedTransaction>,
218 ) -> Result<BanksTransactionResultWithMetadata, BanksClientError> {
219 let ctx = context::current();
220 self.process_transaction_with_metadata_and_context(ctx, transaction.into())
221 .await
222 }
223
224 pub async fn process_transaction_with_preflight_and_commitment(
227 &self,
228 transaction: impl Into<VersionedTransaction>,
229 commitment: CommitmentLevel,
230 ) -> Result<(), BanksClientError> {
231 let ctx = context::current();
232 match self
233 .process_transaction_with_preflight_and_commitment_and_context(
234 ctx,
235 transaction,
236 commitment,
237 )
238 .await?
239 {
240 BanksTransactionResultWithSimulation {
241 result: None,
242 simulation_details: _,
243 } => Err(BanksClientError::ClientError(
244 "invalid blockhash or fee-payer",
245 )),
246 BanksTransactionResultWithSimulation {
247 result: Some(Err(err)),
248 simulation_details: Some(simulation_details),
249 } => Err(BanksClientError::SimulationError {
250 err,
251 logs: simulation_details.logs,
252 units_consumed: simulation_details.units_consumed,
253 return_data: simulation_details.return_data,
254 }),
255 BanksTransactionResultWithSimulation {
256 result: Some(result),
257 simulation_details: _,
258 } => result.map_err(Into::into),
259 }
260 }
261
262 pub async fn process_transaction_with_preflight(
265 &self,
266 transaction: impl Into<VersionedTransaction>,
267 ) -> Result<(), BanksClientError> {
268 self.process_transaction_with_preflight_and_commitment(
269 transaction,
270 CommitmentLevel::default(),
271 )
272 .await
273 }
274
275 pub async fn process_transaction(
277 &self,
278 transaction: impl Into<VersionedTransaction>,
279 ) -> Result<(), BanksClientError> {
280 self.process_transaction_with_commitment(transaction, CommitmentLevel::default())
281 .await
282 }
283
284 pub async fn process_transactions_with_commitment<T: Into<VersionedTransaction>>(
285 &self,
286 transactions: Vec<T>,
287 commitment: CommitmentLevel,
288 ) -> Result<(), BanksClientError> {
289 let mut clients: Vec<_> = transactions.iter().map(|_| self.clone()).collect();
290 let futures = clients
291 .iter_mut()
292 .zip(transactions)
293 .map(|(client, transaction)| {
294 client.process_transaction_with_commitment(transaction, commitment)
295 });
296 let statuses = join_all(futures).await;
297 statuses.into_iter().collect() }
299
300 pub async fn process_transactions<'a, T: Into<VersionedTransaction> + 'a>(
302 &'a self,
303 transactions: Vec<T>,
304 ) -> Result<(), BanksClientError> {
305 self.process_transactions_with_commitment(transactions, CommitmentLevel::default())
306 .await
307 }
308
309 pub async fn simulate_transaction_with_commitment(
311 &self,
312 transaction: impl Into<VersionedTransaction>,
313 commitment: CommitmentLevel,
314 ) -> Result<BanksTransactionResultWithSimulation, BanksClientError> {
315 self.simulate_transaction_with_commitment_and_context(
316 context::current(),
317 transaction,
318 commitment,
319 )
320 .await
321 }
322
323 pub async fn simulate_transaction(
325 &self,
326 transaction: impl Into<VersionedTransaction>,
327 ) -> Result<BanksTransactionResultWithSimulation, BanksClientError> {
328 self.simulate_transaction_with_commitment(transaction, CommitmentLevel::default())
329 .await
330 }
331
332 pub async fn get_root_slot(&self) -> Result<Slot, BanksClientError> {
335 self.get_slot_with_context(context::current(), CommitmentLevel::default())
336 .await
337 }
338
339 pub async fn get_root_block_height(&self) -> Result<Slot, BanksClientError> {
342 self.get_block_height_with_context(context::current(), CommitmentLevel::default())
343 .await
344 }
345
346 pub async fn get_account_with_commitment(
349 &self,
350 address: Pubkey,
351 commitment: CommitmentLevel,
352 ) -> Result<Option<Account>, BanksClientError> {
353 self.get_account_with_commitment_and_context(context::current(), address, commitment)
354 .await
355 }
356
357 pub async fn get_account(&self, address: Pubkey) -> Result<Option<Account>, BanksClientError> {
360 self.get_account_with_commitment(address, CommitmentLevel::default())
361 .await
362 }
363
364 pub async fn get_packed_account_data<T: Pack>(
367 &self,
368 address: Pubkey,
369 ) -> Result<T, BanksClientError> {
370 let account = self
371 .get_account(address)
372 .await?
373 .ok_or(BanksClientError::ClientError("Account not found"))?;
374 T::unpack_from_slice(&account.data)
375 .map_err(|_| BanksClientError::ClientError("Failed to deserialize account"))
376 }
377
378 pub async fn get_account_data_with_borsh<T: BorshDeserialize>(
381 &self,
382 address: Pubkey,
383 ) -> Result<T, BanksClientError> {
384 let account = self
385 .get_account(address)
386 .await?
387 .ok_or(BanksClientError::ClientError("Account not found"))?;
388 T::try_from_slice(&account.data).map_err(Into::into)
389 }
390
391 pub async fn get_balance_with_commitment(
394 &self,
395 address: Pubkey,
396 commitment: CommitmentLevel,
397 ) -> Result<u64, BanksClientError> {
398 Ok(self
399 .get_account_with_commitment_and_context(context::current(), address, commitment)
400 .await?
401 .map(|x| x.lamports)
402 .unwrap_or(0))
403 }
404
405 pub async fn get_balance(&self, address: Pubkey) -> Result<u64, BanksClientError> {
408 self.get_balance_with_commitment(address, CommitmentLevel::default())
409 .await
410 }
411
412 pub async fn get_transaction_status(
418 &self,
419 signature: Signature,
420 ) -> Result<Option<TransactionStatus>, BanksClientError> {
421 self.get_transaction_status_with_context(context::current(), signature)
422 .await
423 }
424
425 pub async fn get_transaction_statuses(
427 &self,
428 signatures: Vec<Signature>,
429 ) -> Result<Vec<Option<TransactionStatus>>, BanksClientError> {
430 let mut clients_and_signatures: Vec<_> = signatures
432 .into_iter()
433 .map(|signature| (self.clone(), signature))
434 .collect();
435
436 let futs = clients_and_signatures
437 .iter_mut()
438 .map(|(client, signature)| client.get_transaction_status(*signature));
439
440 let statuses = join_all(futs).await;
441
442 statuses.into_iter().collect()
444 }
445
446 pub async fn get_latest_blockhash(&self) -> Result<Hash, BanksClientError> {
447 self.get_latest_blockhash_with_commitment(CommitmentLevel::default())
448 .await?
449 .map(|x| x.0)
450 .ok_or(BanksClientError::ClientError("valid blockhash not found"))
451 .map_err(Into::into)
452 }
453
454 pub async fn get_latest_blockhash_with_commitment(
455 &self,
456 commitment: CommitmentLevel,
457 ) -> Result<Option<(Hash, u64)>, BanksClientError> {
458 self.get_latest_blockhash_with_commitment_and_context(context::current(), commitment)
459 .await
460 }
461
462 pub async fn get_latest_blockhash_with_commitment_and_context(
463 &self,
464 ctx: Context,
465 commitment: CommitmentLevel,
466 ) -> Result<Option<(Hash, u64)>, BanksClientError> {
467 self.inner
468 .get_latest_blockhash_with_commitment_and_context(ctx, commitment)
469 .await
470 .map_err(Into::into)
471 }
472
473 pub async fn get_fee_for_message(
474 &self,
475 message: Message,
476 ) -> Result<Option<u64>, BanksClientError> {
477 self.get_fee_for_message_with_commitment_and_context(
478 context::current(),
479 message,
480 CommitmentLevel::default(),
481 )
482 .await
483 }
484
485 pub async fn get_fee_for_message_with_commitment(
486 &self,
487 message: Message,
488 commitment: CommitmentLevel,
489 ) -> Result<Option<u64>, BanksClientError> {
490 self.get_fee_for_message_with_commitment_and_context(
491 context::current(),
492 message,
493 commitment,
494 )
495 .await
496 }
497
498 pub async fn get_fee_for_message_with_commitment_and_context(
499 &self,
500 ctx: Context,
501 message: Message,
502 commitment: CommitmentLevel,
503 ) -> Result<Option<u64>, BanksClientError> {
504 self.inner
505 .get_fee_for_message_with_commitment_and_context(ctx, message, commitment)
506 .await
507 .map_err(Into::into)
508 }
509}
510
511pub async fn start_client<C>(transport: C) -> Result<BanksClient, BanksClientError>
512where
513 C: Transport<ClientMessage<BanksRequest>, Response<BanksResponse>> + Send + 'static,
514{
515 Ok(BanksClient {
516 inner: TarpcClient::new(client::Config::default(), transport).spawn(),
517 })
518}
519
520pub async fn start_tcp_client<T: ToSocketAddrs>(addr: T) -> Result<BanksClient, BanksClientError> {
521 let transport = tcp::connect(addr, Bincode::default).await?;
522 Ok(BanksClient {
523 inner: TarpcClient::new(client::Config::default(), transport).spawn(),
524 })
525}
526
527#[cfg(test)]
528mod tests {
529 use {
530 super::*,
531 solana_banks_server::banks_server::start_local_server,
532 solana_runtime::{
533 bank::Bank, bank_forks::BankForks, commitment::BlockCommitmentCache,
534 genesis_utils::create_genesis_config,
535 },
536 solana_sdk::{
537 message::Message, signature::Signer, system_instruction, transaction::Transaction,
538 },
539 std::sync::{Arc, RwLock},
540 tarpc::transport,
541 tokio::{
542 runtime::Runtime,
543 time::{sleep, Duration},
544 },
545 };
546
547 #[test]
548 fn test_banks_client_new() {
549 let (client_transport, _server_transport) = transport::channel::unbounded();
550 BanksClient::new(client::Config::default(), client_transport);
551 }
552
553 #[test]
554 #[allow(clippy::result_large_err)]
555 fn test_banks_server_transfer_via_server() -> Result<(), BanksClientError> {
556 let genesis = create_genesis_config(10);
561 let bank = Bank::new_for_tests(&genesis.genesis_config);
562 let slot = bank.slot();
563 let block_commitment_cache = Arc::new(RwLock::new(
564 BlockCommitmentCache::new_for_tests_with_slots(slot, slot),
565 ));
566 let bank_forks = BankForks::new_rw_arc(bank);
567
568 let bob_pubkey = solana_sdk::pubkey::new_rand();
569 let mint_pubkey = genesis.mint_keypair.pubkey();
570 let instruction = system_instruction::transfer(&mint_pubkey, &bob_pubkey, 1);
571 let message = Message::new(&[instruction], Some(&mint_pubkey));
572
573 Runtime::new()?.block_on(async {
574 let client_transport =
575 start_local_server(bank_forks, block_commitment_cache, Duration::from_millis(1))
576 .await;
577 let banks_client = start_client(client_transport).await?;
578
579 let recent_blockhash = banks_client.get_latest_blockhash().await?;
580 let transaction = Transaction::new(&[&genesis.mint_keypair], message, recent_blockhash);
581 let simulation_result = banks_client
582 .simulate_transaction(transaction.clone())
583 .await
584 .unwrap();
585 assert!(simulation_result.result.unwrap().is_ok());
586 banks_client.process_transaction(transaction).await.unwrap();
587 assert_eq!(banks_client.get_balance(bob_pubkey).await?, 1);
588 Ok(())
589 })
590 }
591
592 #[test]
593 #[allow(clippy::result_large_err)]
594 fn test_banks_server_transfer_via_client() -> Result<(), BanksClientError> {
595 let genesis = create_genesis_config(10);
600 let bank = Bank::new_for_tests(&genesis.genesis_config);
601 let slot = bank.slot();
602 let block_commitment_cache = Arc::new(RwLock::new(
603 BlockCommitmentCache::new_for_tests_with_slots(slot, slot),
604 ));
605 let bank_forks = BankForks::new_rw_arc(bank);
606
607 let mint_pubkey = &genesis.mint_keypair.pubkey();
608 let bob_pubkey = solana_sdk::pubkey::new_rand();
609 let instruction = system_instruction::transfer(mint_pubkey, &bob_pubkey, 1);
610 let message = Message::new(&[instruction], Some(mint_pubkey));
611
612 Runtime::new()?.block_on(async {
613 let client_transport =
614 start_local_server(bank_forks, block_commitment_cache, Duration::from_millis(1))
615 .await;
616 let banks_client = start_client(client_transport).await?;
617 let (recent_blockhash, last_valid_block_height) = banks_client
618 .get_latest_blockhash_with_commitment(CommitmentLevel::default())
619 .await?
620 .unwrap();
621 let transaction = Transaction::new(&[&genesis.mint_keypair], message, recent_blockhash);
622 let signature = transaction.signatures[0];
623 banks_client.send_transaction(transaction).await?;
624
625 let mut status = banks_client.get_transaction_status(signature).await?;
626
627 while status.is_none() {
628 let root_block_height = banks_client.get_root_block_height().await?;
629 if root_block_height > last_valid_block_height {
630 break;
631 }
632 sleep(Duration::from_millis(100)).await;
633 status = banks_client.get_transaction_status(signature).await?;
634 }
635 assert!(status.unwrap().err.is_none());
636 assert_eq!(banks_client.get_balance(bob_pubkey).await?, 1);
637 Ok(())
638 })
639 }
640}