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,
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_id::SysvarId,
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>(&self) -> Result<T, BanksClientError>
187 where
188 T: wincode::DeserializeOwned<Dst = T> + SysvarId,
189 {
190 let sysvar = self
191 .get_account(T::id())
192 .await?
193 .ok_or(BanksClientError::ClientError("Sysvar not present"))?;
194 wincode::deserialize(&sysvar.data)
195 .map_err(|_| BanksClientError::ClientError("Failed to deserialize sysvar"))
196 }
197
198 pub async fn get_rent(&self) -> Result<Rent, BanksClientError> {
200 self.get_sysvar::<Rent>().await
201 }
202
203 pub async fn process_transaction_with_commitment(
206 &self,
207 transaction: impl Into<VersionedTransaction>,
208 commitment: CommitmentLevel,
209 ) -> Result<(), BanksClientError> {
210 let ctx = context::current();
211 match self
212 .process_transaction_with_commitment_and_context(ctx, transaction, commitment)
213 .await?
214 {
215 None => Err(BanksClientError::ClientError(
216 "invalid blockhash or fee-payer",
217 )),
218 Some(transaction_result) => Ok(transaction_result?),
219 }
220 }
221
222 pub async fn process_transaction_with_metadata(
224 &self,
225 transaction: impl Into<VersionedTransaction>,
226 ) -> Result<BanksTransactionResultWithMetadata, BanksClientError> {
227 let ctx = context::current();
228 self.process_transaction_with_metadata_and_context(ctx, transaction.into())
229 .await
230 }
231
232 pub async fn process_transaction_with_preflight_and_commitment(
235 &self,
236 transaction: impl Into<VersionedTransaction>,
237 commitment: CommitmentLevel,
238 ) -> Result<(), BanksClientError> {
239 let ctx = context::current();
240 match self
241 .process_transaction_with_preflight_and_commitment_and_context(
242 ctx,
243 transaction,
244 commitment,
245 )
246 .await?
247 {
248 BanksTransactionResultWithSimulation {
249 result: None,
250 simulation_details: _,
251 } => Err(BanksClientError::ClientError(
252 "invalid blockhash or fee-payer",
253 )),
254 BanksTransactionResultWithSimulation {
255 result: Some(Err(err)),
256 simulation_details: Some(simulation_details),
257 } => Err(BanksClientError::SimulationError {
258 err,
259 logs: simulation_details.logs,
260 units_consumed: simulation_details.units_consumed,
261 return_data: simulation_details.return_data,
262 }),
263 BanksTransactionResultWithSimulation {
264 result: Some(result),
265 simulation_details: _,
266 } => result.map_err(Into::into),
267 }
268 }
269
270 pub async fn process_transaction_with_preflight(
273 &self,
274 transaction: impl Into<VersionedTransaction>,
275 ) -> Result<(), BanksClientError> {
276 self.process_transaction_with_preflight_and_commitment(
277 transaction,
278 CommitmentLevel::default(),
279 )
280 .await
281 }
282
283 pub async fn process_transaction(
285 &self,
286 transaction: impl Into<VersionedTransaction>,
287 ) -> Result<(), BanksClientError> {
288 self.process_transaction_with_commitment(transaction, CommitmentLevel::default())
289 .await
290 }
291
292 pub async fn process_transactions_with_commitment<T: Into<VersionedTransaction>>(
293 &self,
294 transactions: Vec<T>,
295 commitment: CommitmentLevel,
296 ) -> Result<(), BanksClientError> {
297 let mut clients: Vec<_> = transactions.iter().map(|_| self.clone()).collect();
298 let futures = clients
299 .iter_mut()
300 .zip(transactions)
301 .map(|(client, transaction)| {
302 client.process_transaction_with_commitment(transaction, commitment)
303 });
304 let statuses = join_all(futures).await;
305 statuses.into_iter().collect() }
307
308 pub async fn process_transactions<'a, T: Into<VersionedTransaction> + 'a>(
310 &'a self,
311 transactions: Vec<T>,
312 ) -> Result<(), BanksClientError> {
313 self.process_transactions_with_commitment(transactions, CommitmentLevel::default())
314 .await
315 }
316
317 pub async fn simulate_transaction_with_commitment(
319 &self,
320 transaction: impl Into<VersionedTransaction>,
321 commitment: CommitmentLevel,
322 ) -> Result<BanksTransactionResultWithSimulation, BanksClientError> {
323 self.simulate_transaction_with_commitment_and_context(
324 context::current(),
325 transaction,
326 commitment,
327 )
328 .await
329 }
330
331 pub async fn simulate_transaction(
333 &self,
334 transaction: impl Into<VersionedTransaction>,
335 ) -> Result<BanksTransactionResultWithSimulation, BanksClientError> {
336 self.simulate_transaction_with_commitment(transaction, CommitmentLevel::default())
337 .await
338 }
339
340 pub async fn get_root_slot(&self) -> Result<Slot, BanksClientError> {
343 self.get_slot_with_context(context::current(), CommitmentLevel::default())
344 .await
345 }
346
347 pub async fn get_root_block_height(&self) -> Result<Slot, BanksClientError> {
350 self.get_block_height_with_context(context::current(), CommitmentLevel::default())
351 .await
352 }
353
354 pub async fn get_account_with_commitment(
357 &self,
358 address: Pubkey,
359 commitment: CommitmentLevel,
360 ) -> Result<Option<Account>, BanksClientError> {
361 self.get_account_with_commitment_and_context(context::current(), address, commitment)
362 .await
363 }
364
365 pub async fn get_account(&self, address: Pubkey) -> Result<Option<Account>, BanksClientError> {
368 self.get_account_with_commitment(address, CommitmentLevel::default())
369 .await
370 }
371
372 pub async fn get_packed_account_data<T: Pack>(
375 &self,
376 address: Pubkey,
377 ) -> Result<T, BanksClientError> {
378 let account = self
379 .get_account(address)
380 .await?
381 .ok_or(BanksClientError::ClientError("Account not found"))?;
382 T::unpack_from_slice(&account.data)
383 .map_err(|_| BanksClientError::ClientError("Failed to deserialize account"))
384 }
385
386 pub async fn get_account_data_with_borsh<T: BorshDeserialize>(
389 &self,
390 address: Pubkey,
391 ) -> Result<T, BanksClientError> {
392 let account = self
393 .get_account(address)
394 .await?
395 .ok_or(BanksClientError::ClientError("Account not found"))?;
396 T::try_from_slice(&account.data).map_err(Into::into)
397 }
398
399 pub async fn get_balance_with_commitment(
402 &self,
403 address: Pubkey,
404 commitment: CommitmentLevel,
405 ) -> Result<u64, BanksClientError> {
406 Ok(self
407 .get_account_with_commitment_and_context(context::current(), address, commitment)
408 .await?
409 .map(|x| x.lamports)
410 .unwrap_or(0))
411 }
412
413 pub async fn get_balance(&self, address: Pubkey) -> Result<u64, BanksClientError> {
416 self.get_balance_with_commitment(address, CommitmentLevel::default())
417 .await
418 }
419
420 pub async fn get_transaction_status(
426 &self,
427 signature: Signature,
428 ) -> Result<Option<TransactionStatus>, BanksClientError> {
429 self.get_transaction_status_with_context(context::current(), signature)
430 .await
431 }
432
433 pub async fn get_transaction_statuses(
435 &self,
436 signatures: Vec<Signature>,
437 ) -> Result<Vec<Option<TransactionStatus>>, BanksClientError> {
438 let mut clients_and_signatures: Vec<_> = signatures
440 .into_iter()
441 .map(|signature| (self.clone(), signature))
442 .collect();
443
444 let futs = clients_and_signatures
445 .iter_mut()
446 .map(|(client, signature)| client.get_transaction_status(*signature));
447
448 let statuses = join_all(futs).await;
449
450 statuses.into_iter().collect()
452 }
453
454 pub async fn get_latest_blockhash(&self) -> Result<Hash, BanksClientError> {
455 self.get_latest_blockhash_with_commitment(CommitmentLevel::default())
456 .await?
457 .map(|x| x.0)
458 .ok_or(BanksClientError::ClientError("valid blockhash not found"))
459 }
460
461 pub async fn get_latest_blockhash_with_commitment(
462 &self,
463 commitment: CommitmentLevel,
464 ) -> Result<Option<(Hash, u64)>, BanksClientError> {
465 self.get_latest_blockhash_with_commitment_and_context(context::current(), commitment)
466 .await
467 }
468
469 pub async fn get_latest_blockhash_with_commitment_and_context(
470 &self,
471 ctx: Context,
472 commitment: CommitmentLevel,
473 ) -> Result<Option<(Hash, u64)>, BanksClientError> {
474 self.inner
475 .get_latest_blockhash_with_commitment_and_context(ctx, commitment)
476 .await
477 .map_err(Into::into)
478 }
479
480 pub async fn get_fee_for_message(
481 &self,
482 message: Message,
483 ) -> Result<Option<u64>, BanksClientError> {
484 self.get_fee_for_message_with_commitment_and_context(
485 context::current(),
486 message,
487 CommitmentLevel::default(),
488 )
489 .await
490 }
491
492 pub async fn get_fee_for_message_with_commitment(
493 &self,
494 message: Message,
495 commitment: CommitmentLevel,
496 ) -> Result<Option<u64>, BanksClientError> {
497 self.get_fee_for_message_with_commitment_and_context(
498 context::current(),
499 message,
500 commitment,
501 )
502 .await
503 }
504
505 pub async fn get_fee_for_message_with_commitment_and_context(
506 &self,
507 ctx: Context,
508 message: Message,
509 commitment: CommitmentLevel,
510 ) -> Result<Option<u64>, BanksClientError> {
511 self.inner
512 .get_fee_for_message_with_commitment_and_context(ctx, message, commitment)
513 .await
514 .map_err(Into::into)
515 }
516}
517
518pub async fn start_client<C>(transport: C) -> Result<BanksClient, BanksClientError>
519where
520 C: Transport<ClientMessage<BanksRequest>, Response<BanksResponse>> + Send + 'static,
521{
522 Ok(BanksClient {
523 inner: TarpcClient::new(client::Config::default(), transport).spawn(),
524 })
525}
526
527pub async fn start_tcp_client<T: ToSocketAddrs>(addr: T) -> Result<BanksClient, BanksClientError> {
528 let transport = tcp::connect(addr, Bincode::default).await?;
529 Ok(BanksClient {
530 inner: TarpcClient::new(client::Config::default(), transport).spawn(),
531 })
532}
533
534#[cfg(test)]
535mod tests {
536 use {
537 super::*,
538 solana_banks_server::banks_server::start_local_server,
539 solana_runtime::{
540 bank::Bank, bank_forks::BankForks, commitment::BlockCommitmentCache,
541 genesis_utils::create_genesis_config,
542 },
543 solana_signer::Signer,
544 solana_system_interface::instruction as system_instruction,
545 solana_transaction::Transaction,
546 std::sync::{Arc, RwLock},
547 tarpc::transport,
548 tokio::{
549 runtime::Runtime,
550 time::{Duration, sleep},
551 },
552 };
553
554 #[test]
555 fn test_banks_client_new() {
556 let (client_transport, _server_transport) = transport::channel::unbounded();
557 BanksClient::new(client::Config::default(), client_transport);
558 }
559
560 #[test]
561 #[allow(clippy::result_large_err)]
562 fn test_banks_server_transfer_via_server() -> Result<(), BanksClientError> {
563 let genesis = create_genesis_config(10);
568 let bank = Bank::new_for_tests(&genesis.genesis_config);
569 let slot = bank.slot();
570 let block_commitment_cache = Arc::new(RwLock::new(
571 BlockCommitmentCache::new_for_tests_with_slots(slot, slot),
572 ));
573 let bank_forks = BankForks::new_rw_arc(bank);
574
575 let bob_pubkey = solana_pubkey::new_rand();
576 let mint_pubkey = genesis.mint_keypair.pubkey();
577 let instruction = system_instruction::transfer(&mint_pubkey, &bob_pubkey, 1);
578 let message = Message::new(&[instruction], Some(&mint_pubkey));
579
580 Runtime::new()?.block_on(async {
581 let client_transport =
582 start_local_server(bank_forks, block_commitment_cache, Duration::from_millis(1))
583 .await;
584 let banks_client = start_client(client_transport).await?;
585
586 let recent_blockhash = banks_client.get_latest_blockhash().await?;
587 let transaction = Transaction::new(&[&genesis.mint_keypair], message, recent_blockhash);
588 let simulation_result = banks_client
589 .simulate_transaction(transaction.clone())
590 .await
591 .unwrap();
592 assert!(simulation_result.result.unwrap().is_ok());
593 banks_client.process_transaction(transaction).await.unwrap();
594 assert_eq!(banks_client.get_balance(bob_pubkey).await?, 1);
595 Ok(())
596 })
597 }
598
599 #[test]
600 #[allow(clippy::result_large_err)]
601 fn test_banks_server_transfer_via_client() -> Result<(), BanksClientError> {
602 let genesis = create_genesis_config(10);
607 let bank = Bank::new_for_tests(&genesis.genesis_config);
608 let slot = bank.slot();
609 let block_commitment_cache = Arc::new(RwLock::new(
610 BlockCommitmentCache::new_for_tests_with_slots(slot, slot),
611 ));
612 let bank_forks = BankForks::new_rw_arc(bank);
613
614 let mint_pubkey = &genesis.mint_keypair.pubkey();
615 let bob_pubkey = solana_pubkey::new_rand();
616 let instruction = system_instruction::transfer(mint_pubkey, &bob_pubkey, 1);
617 let message = Message::new(&[instruction], Some(mint_pubkey));
618
619 Runtime::new()?.block_on(async {
620 let client_transport =
621 start_local_server(bank_forks, block_commitment_cache, Duration::from_millis(1))
622 .await;
623 let banks_client = start_client(client_transport).await?;
624 let (recent_blockhash, last_valid_block_height) = banks_client
625 .get_latest_blockhash_with_commitment(CommitmentLevel::default())
626 .await?
627 .unwrap();
628 let transaction = Transaction::new(&[&genesis.mint_keypair], message, recent_blockhash);
629 let signature = transaction.signatures[0];
630 banks_client.send_transaction(transaction).await?;
631
632 let mut status = banks_client.get_transaction_status(signature).await?;
633
634 while status.is_none() {
635 let root_block_height = banks_client.get_root_block_height().await?;
636 if root_block_height > last_valid_block_height {
637 break;
638 }
639 sleep(Duration::from_millis(100)).await;
640 status = banks_client.get_transaction_status(signature).await?;
641 }
642 assert!(status.unwrap().err.is_none());
643 assert_eq!(banks_client.get_balance(bob_pubkey).await?, 1);
644 Ok(())
645 })
646 }
647}