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