Skip to main content

solana_banks_client/
lib.rs

1#![cfg(feature = "agave-unstable-api")]
2//! A client for the ledger state, from the perspective of an arbitrary validator.
3//!
4//! Use start_tcp_client() to create a client and then import BanksClientExt to
5//! access its methods. Additional "*_with_context" methods are also available,
6//! but they are undocumented, may change over time, and are generally more
7//! cumbersome to use.
8
9pub 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
47// This exists only for backward compatibility
48pub 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    /// Send a transaction and return immediately. The server will resend the
175    /// transaction until either it is accepted by the cluster or the transaction's
176    /// blockhash expires.
177    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    /// Return the cluster Sysvar
186    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    /// Return the cluster rent
197    pub async fn get_rent(&self) -> Result<Rent, BanksClientError> {
198        self.get_sysvar::<Rent>().await
199    }
200
201    /// Send a transaction and return after the transaction has been rejected or
202    /// reached the given level of commitment.
203    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    /// Process a transaction and return the result with metadata.
221    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    /// Send a transaction and return any preflight (sanitization or simulation) errors, or return
231    /// after the transaction has been rejected or reached the given level of commitment.
232    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    /// Send a transaction and return any preflight (sanitization or simulation) errors, or return
269    /// after the transaction has been finalized or rejected.
270    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    /// Send a transaction and return until the transaction has been finalized or rejected.
282    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() // Convert Vec<Result<_, _>> to Result<Vec<_>>
304    }
305
306    /// Send transactions and return until the transaction has been finalized or rejected.
307    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    /// Simulate a transaction at the given commitment level
316    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    /// Simulate a transaction at the default commitment level
330    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    /// Return the most recent rooted slot. All transactions at or below this slot
339    /// are said to be finalized. The cluster will not fork to a higher slot.
340    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    /// Return the most recent rooted block height. All transactions at or below this height
346    /// are said to be finalized. The cluster will not fork to a higher block height.
347    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    /// Return the account at the given address at the slot corresponding to the given
353    /// commitment level. If the account is not found, None is returned.
354    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    /// Return the account at the given address at the time of the most recent root slot.
364    /// If the account is not found, None is returned.
365    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    /// Return the unpacked account data at the given address
371    /// If the account is not found, an error is returned
372    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    /// Return the unpacked account data at the given address
385    /// If the account is not found, an error is returned
386    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    /// Return the balance in lamports of an account at the given address at the slot
398    /// corresponding to the given commitment level.
399    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    /// Return the balance in lamports of an account at the given address at the time
412    /// of the most recent root slot.
413    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    /// Return the status of a transaction with a signature matching the transaction's first
419    /// signature. Return None if the transaction is not found, which may be because the
420    /// blockhash was expired or the fee-paying account had insufficient funds to pay the
421    /// transaction fee. Note that servers rarely store the full transaction history. This
422    /// method may return None if the transaction status has been discarded.
423    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    /// Same as get_transaction_status, but for multiple transactions.
432    pub async fn get_transaction_statuses(
433        &self,
434        signatures: Vec<Signature>,
435    ) -> Result<Vec<Option<TransactionStatus>>, BanksClientError> {
436        // tarpc futures oddly hold a mutable reference back to the client so clone the client upfront
437        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        // Convert Vec<Result<_, _>> to Result<Vec<_>>
449        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        // This test shows the preferred way to interact with BanksServer.
562        // It creates a runtime explicitly (no globals via tokio macros) and calls
563        // `runtime.block_on()` just once, to run all the async code.
564
565        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        // The caller may not want to hold the connection open until the transaction
601        // is processed (or blockhash expires). In this test, we verify the
602        // server-side functionality is available to the client.
603
604        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}