solana_banks_client/
lib.rs

1//! A client for the ledger state, from the perspective of an arbitrary validator.
2//!
3//! Use start_tcp_client() to create a client and then import BanksClientExt to
4//! access its methods. Additional "*_with_context" methods are also available,
5//! but they are undocumented, may change over time, and are generally more
6//! cumbersome to use.
7
8pub 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_account::{from_account, Account},
16    solana_banks_interface::{
17        BanksRequest, BanksResponse, BanksTransactionResultWithMetadata,
18        BanksTransactionResultWithSimulation,
19    },
20    solana_clock::Slot,
21    solana_commitment_config::CommitmentLevel,
22    solana_hash::Hash,
23    solana_message::Message,
24    solana_program_pack::Pack,
25    solana_pubkey::Pubkey,
26    solana_rent::Rent,
27    solana_signature::Signature,
28    solana_sysvar::SysvarSerialize,
29    solana_transaction::versioned::VersionedTransaction,
30    tarpc::{
31        client::{self, NewClient, RequestDispatch},
32        context::{self, Context},
33        serde_transport::tcp,
34        ClientMessage, Response, Transport,
35    },
36    tokio::net::ToSocketAddrs,
37    tokio_serde::formats::Bincode,
38};
39
40mod error;
41
42mod transaction {
43    pub use solana_transaction_error::TransactionResult as Result;
44}
45
46// This exists only for backward compatibility
47pub trait BanksClientExt {}
48
49#[derive(Clone)]
50pub struct BanksClient {
51    inner: TarpcClient,
52}
53
54impl BanksClient {
55    #[allow(clippy::new_ret_no_self)]
56    pub fn new<C>(
57        config: client::Config,
58        transport: C,
59    ) -> NewClient<TarpcClient, RequestDispatch<BanksRequest, BanksResponse, C>>
60    where
61        C: Transport<ClientMessage<BanksRequest>, Response<BanksResponse>>,
62    {
63        TarpcClient::new(config, transport)
64    }
65
66    pub async fn send_transaction_with_context(
67        &self,
68        ctx: Context,
69        transaction: impl Into<VersionedTransaction>,
70    ) -> Result<(), BanksClientError> {
71        self.inner
72            .send_transaction_with_context(ctx, transaction.into())
73            .await
74            .map_err(Into::into)
75    }
76
77    pub async fn get_transaction_status_with_context(
78        &self,
79        ctx: Context,
80        signature: Signature,
81    ) -> Result<Option<TransactionStatus>, BanksClientError> {
82        self.inner
83            .get_transaction_status_with_context(ctx, signature)
84            .await
85            .map_err(Into::into)
86    }
87
88    pub async fn get_slot_with_context(
89        &self,
90        ctx: Context,
91        commitment: CommitmentLevel,
92    ) -> Result<Slot, BanksClientError> {
93        self.inner
94            .get_slot_with_context(ctx, commitment)
95            .await
96            .map_err(Into::into)
97    }
98
99    pub async fn get_block_height_with_context(
100        &self,
101        ctx: Context,
102        commitment: CommitmentLevel,
103    ) -> Result<Slot, BanksClientError> {
104        self.inner
105            .get_block_height_with_context(ctx, commitment)
106            .await
107            .map_err(Into::into)
108    }
109
110    pub async fn process_transaction_with_commitment_and_context(
111        &self,
112        ctx: Context,
113        transaction: impl Into<VersionedTransaction>,
114        commitment: CommitmentLevel,
115    ) -> Result<Option<transaction::Result<()>>, BanksClientError> {
116        self.inner
117            .process_transaction_with_commitment_and_context(ctx, transaction.into(), commitment)
118            .await
119            .map_err(Into::into)
120    }
121
122    pub async fn process_transaction_with_preflight_and_commitment_and_context(
123        &self,
124        ctx: Context,
125        transaction: impl Into<VersionedTransaction>,
126        commitment: CommitmentLevel,
127    ) -> Result<BanksTransactionResultWithSimulation, BanksClientError> {
128        self.inner
129            .process_transaction_with_preflight_and_commitment_and_context(
130                ctx,
131                transaction.into(),
132                commitment,
133            )
134            .await
135            .map_err(Into::into)
136    }
137
138    pub async fn process_transaction_with_metadata_and_context(
139        &self,
140        ctx: Context,
141        transaction: impl Into<VersionedTransaction>,
142    ) -> Result<BanksTransactionResultWithMetadata, BanksClientError> {
143        self.inner
144            .process_transaction_with_metadata_and_context(ctx, transaction.into())
145            .await
146            .map_err(Into::into)
147    }
148
149    pub async fn simulate_transaction_with_commitment_and_context(
150        &self,
151        ctx: Context,
152        transaction: impl Into<VersionedTransaction>,
153        commitment: CommitmentLevel,
154    ) -> Result<BanksTransactionResultWithSimulation, BanksClientError> {
155        self.inner
156            .simulate_transaction_with_commitment_and_context(ctx, transaction.into(), commitment)
157            .await
158            .map_err(Into::into)
159    }
160
161    pub async fn get_account_with_commitment_and_context(
162        &self,
163        ctx: Context,
164        address: Pubkey,
165        commitment: CommitmentLevel,
166    ) -> Result<Option<Account>, BanksClientError> {
167        self.inner
168            .get_account_with_commitment_and_context(ctx, address, commitment)
169            .await
170            .map_err(Into::into)
171    }
172
173    /// Send a transaction and return immediately. The server will resend the
174    /// transaction until either it is accepted by the cluster or the transaction's
175    /// blockhash expires.
176    pub async fn send_transaction(
177        &self,
178        transaction: impl Into<VersionedTransaction>,
179    ) -> Result<(), BanksClientError> {
180        self.send_transaction_with_context(context::current(), transaction.into())
181            .await
182    }
183
184    /// Return the cluster Sysvar
185    pub async fn get_sysvar<T: SysvarSerialize>(&self) -> Result<T, BanksClientError> {
186        let sysvar = self
187            .get_account(T::id())
188            .await?
189            .ok_or(BanksClientError::ClientError("Sysvar not present"))?;
190        from_account::<T, _>(&sysvar).ok_or(BanksClientError::ClientError(
191            "Failed to deserialize sysvar",
192        ))
193    }
194
195    /// Return the cluster rent
196    pub async fn get_rent(&self) -> Result<Rent, BanksClientError> {
197        self.get_sysvar::<Rent>().await
198    }
199
200    /// Send a transaction and return after the transaction has been rejected or
201    /// reached the given level of commitment.
202    pub async fn process_transaction_with_commitment(
203        &self,
204        transaction: impl Into<VersionedTransaction>,
205        commitment: CommitmentLevel,
206    ) -> Result<(), BanksClientError> {
207        let ctx = context::current();
208        match self
209            .process_transaction_with_commitment_and_context(ctx, transaction, commitment)
210            .await?
211        {
212            None => Err(BanksClientError::ClientError(
213                "invalid blockhash or fee-payer",
214            )),
215            Some(transaction_result) => Ok(transaction_result?),
216        }
217    }
218
219    /// Process a transaction and return the result with metadata.
220    pub async fn process_transaction_with_metadata(
221        &self,
222        transaction: impl Into<VersionedTransaction>,
223    ) -> Result<BanksTransactionResultWithMetadata, BanksClientError> {
224        let ctx = context::current();
225        self.process_transaction_with_metadata_and_context(ctx, transaction.into())
226            .await
227    }
228
229    /// Send a transaction and return any preflight (sanitization or simulation) errors, or return
230    /// after the transaction has been rejected or reached the given level of commitment.
231    pub async fn process_transaction_with_preflight_and_commitment(
232        &self,
233        transaction: impl Into<VersionedTransaction>,
234        commitment: CommitmentLevel,
235    ) -> Result<(), BanksClientError> {
236        let ctx = context::current();
237        match self
238            .process_transaction_with_preflight_and_commitment_and_context(
239                ctx,
240                transaction,
241                commitment,
242            )
243            .await?
244        {
245            BanksTransactionResultWithSimulation {
246                result: None,
247                simulation_details: _,
248            } => Err(BanksClientError::ClientError(
249                "invalid blockhash or fee-payer",
250            )),
251            BanksTransactionResultWithSimulation {
252                result: Some(Err(err)),
253                simulation_details: Some(simulation_details),
254            } => Err(BanksClientError::SimulationError {
255                err,
256                logs: simulation_details.logs,
257                units_consumed: simulation_details.units_consumed,
258                return_data: simulation_details.return_data,
259            }),
260            BanksTransactionResultWithSimulation {
261                result: Some(result),
262                simulation_details: _,
263            } => result.map_err(Into::into),
264        }
265    }
266
267    /// Send a transaction and return any preflight (sanitization or simulation) errors, or return
268    /// after the transaction has been finalized or rejected.
269    pub async fn process_transaction_with_preflight(
270        &self,
271        transaction: impl Into<VersionedTransaction>,
272    ) -> Result<(), BanksClientError> {
273        self.process_transaction_with_preflight_and_commitment(
274            transaction,
275            CommitmentLevel::default(),
276        )
277        .await
278    }
279
280    /// Send a transaction and return until the transaction has been finalized or rejected.
281    pub async fn process_transaction(
282        &self,
283        transaction: impl Into<VersionedTransaction>,
284    ) -> Result<(), BanksClientError> {
285        self.process_transaction_with_commitment(transaction, CommitmentLevel::default())
286            .await
287    }
288
289    pub async fn process_transactions_with_commitment<T: Into<VersionedTransaction>>(
290        &self,
291        transactions: Vec<T>,
292        commitment: CommitmentLevel,
293    ) -> Result<(), BanksClientError> {
294        let mut clients: Vec<_> = transactions.iter().map(|_| self.clone()).collect();
295        let futures = clients
296            .iter_mut()
297            .zip(transactions)
298            .map(|(client, transaction)| {
299                client.process_transaction_with_commitment(transaction, commitment)
300            });
301        let statuses = join_all(futures).await;
302        statuses.into_iter().collect() // Convert Vec<Result<_, _>> to Result<Vec<_>>
303    }
304
305    /// Send transactions and return until the transaction has been finalized or rejected.
306    pub async fn process_transactions<'a, T: Into<VersionedTransaction> + 'a>(
307        &'a self,
308        transactions: Vec<T>,
309    ) -> Result<(), BanksClientError> {
310        self.process_transactions_with_commitment(transactions, CommitmentLevel::default())
311            .await
312    }
313
314    /// Simulate a transaction at the given commitment level
315    pub async fn simulate_transaction_with_commitment(
316        &self,
317        transaction: impl Into<VersionedTransaction>,
318        commitment: CommitmentLevel,
319    ) -> Result<BanksTransactionResultWithSimulation, BanksClientError> {
320        self.simulate_transaction_with_commitment_and_context(
321            context::current(),
322            transaction,
323            commitment,
324        )
325        .await
326    }
327
328    /// Simulate a transaction at the default commitment level
329    pub async fn simulate_transaction(
330        &self,
331        transaction: impl Into<VersionedTransaction>,
332    ) -> Result<BanksTransactionResultWithSimulation, BanksClientError> {
333        self.simulate_transaction_with_commitment(transaction, CommitmentLevel::default())
334            .await
335    }
336
337    /// Return the most recent rooted slot. All transactions at or below this slot
338    /// are said to be finalized. The cluster will not fork to a higher slot.
339    pub async fn get_root_slot(&self) -> Result<Slot, BanksClientError> {
340        self.get_slot_with_context(context::current(), CommitmentLevel::default())
341            .await
342    }
343
344    /// Return the most recent rooted block height. All transactions at or below this height
345    /// are said to be finalized. The cluster will not fork to a higher block height.
346    pub async fn get_root_block_height(&self) -> Result<Slot, BanksClientError> {
347        self.get_block_height_with_context(context::current(), CommitmentLevel::default())
348            .await
349    }
350
351    /// Return the account at the given address at the slot corresponding to the given
352    /// commitment level. If the account is not found, None is returned.
353    pub async fn get_account_with_commitment(
354        &self,
355        address: Pubkey,
356        commitment: CommitmentLevel,
357    ) -> Result<Option<Account>, BanksClientError> {
358        self.get_account_with_commitment_and_context(context::current(), address, commitment)
359            .await
360    }
361
362    /// Return the account at the given address at the time of the most recent root slot.
363    /// If the account is not found, None is returned.
364    pub async fn get_account(&self, address: Pubkey) -> Result<Option<Account>, BanksClientError> {
365        self.get_account_with_commitment(address, CommitmentLevel::default())
366            .await
367    }
368
369    /// Return the unpacked account data at the given address
370    /// If the account is not found, an error is returned
371    pub async fn get_packed_account_data<T: Pack>(
372        &self,
373        address: Pubkey,
374    ) -> Result<T, BanksClientError> {
375        let account = self
376            .get_account(address)
377            .await?
378            .ok_or(BanksClientError::ClientError("Account not found"))?;
379        T::unpack_from_slice(&account.data)
380            .map_err(|_| BanksClientError::ClientError("Failed to deserialize account"))
381    }
382
383    /// Return the unpacked account data at the given address
384    /// If the account is not found, an error is returned
385    pub async fn get_account_data_with_borsh<T: BorshDeserialize>(
386        &self,
387        address: Pubkey,
388    ) -> Result<T, BanksClientError> {
389        let account = self
390            .get_account(address)
391            .await?
392            .ok_or(BanksClientError::ClientError("Account not found"))?;
393        T::try_from_slice(&account.data).map_err(Into::into)
394    }
395
396    /// Return the balance in lamports of an account at the given address at the slot
397    /// corresponding to the given commitment level.
398    pub async fn get_balance_with_commitment(
399        &self,
400        address: Pubkey,
401        commitment: CommitmentLevel,
402    ) -> Result<u64, BanksClientError> {
403        Ok(self
404            .get_account_with_commitment_and_context(context::current(), address, commitment)
405            .await?
406            .map(|x| x.lamports)
407            .unwrap_or(0))
408    }
409
410    /// Return the balance in lamports of an account at the given address at the time
411    /// of the most recent root slot.
412    pub async fn get_balance(&self, address: Pubkey) -> Result<u64, BanksClientError> {
413        self.get_balance_with_commitment(address, CommitmentLevel::default())
414            .await
415    }
416
417    /// Return the status of a transaction with a signature matching the transaction's first
418    /// signature. Return None if the transaction is not found, which may be because the
419    /// blockhash was expired or the fee-paying account had insufficient funds to pay the
420    /// transaction fee. Note that servers rarely store the full transaction history. This
421    /// method may return None if the transaction status has been discarded.
422    pub async fn get_transaction_status(
423        &self,
424        signature: Signature,
425    ) -> Result<Option<TransactionStatus>, BanksClientError> {
426        self.get_transaction_status_with_context(context::current(), signature)
427            .await
428    }
429
430    /// Same as get_transaction_status, but for multiple transactions.
431    pub async fn get_transaction_statuses(
432        &self,
433        signatures: Vec<Signature>,
434    ) -> Result<Vec<Option<TransactionStatus>>, BanksClientError> {
435        // tarpc futures oddly hold a mutable reference back to the client so clone the client upfront
436        let mut clients_and_signatures: Vec<_> = signatures
437            .into_iter()
438            .map(|signature| (self.clone(), signature))
439            .collect();
440
441        let futs = clients_and_signatures
442            .iter_mut()
443            .map(|(client, signature)| client.get_transaction_status(*signature));
444
445        let statuses = join_all(futs).await;
446
447        // Convert Vec<Result<_, _>> to Result<Vec<_>>
448        statuses.into_iter().collect()
449    }
450
451    pub async fn get_latest_blockhash(&self) -> Result<Hash, BanksClientError> {
452        self.get_latest_blockhash_with_commitment(CommitmentLevel::default())
453            .await?
454            .map(|x| x.0)
455            .ok_or(BanksClientError::ClientError("valid blockhash not found"))
456    }
457
458    pub async fn get_latest_blockhash_with_commitment(
459        &self,
460        commitment: CommitmentLevel,
461    ) -> Result<Option<(Hash, u64)>, BanksClientError> {
462        self.get_latest_blockhash_with_commitment_and_context(context::current(), commitment)
463            .await
464    }
465
466    pub async fn get_latest_blockhash_with_commitment_and_context(
467        &self,
468        ctx: Context,
469        commitment: CommitmentLevel,
470    ) -> Result<Option<(Hash, u64)>, BanksClientError> {
471        self.inner
472            .get_latest_blockhash_with_commitment_and_context(ctx, commitment)
473            .await
474            .map_err(Into::into)
475    }
476
477    pub async fn get_fee_for_message(
478        &self,
479        message: Message,
480    ) -> Result<Option<u64>, BanksClientError> {
481        self.get_fee_for_message_with_commitment_and_context(
482            context::current(),
483            message,
484            CommitmentLevel::default(),
485        )
486        .await
487    }
488
489    pub async fn get_fee_for_message_with_commitment(
490        &self,
491        message: Message,
492        commitment: CommitmentLevel,
493    ) -> Result<Option<u64>, BanksClientError> {
494        self.get_fee_for_message_with_commitment_and_context(
495            context::current(),
496            message,
497            commitment,
498        )
499        .await
500    }
501
502    pub async fn get_fee_for_message_with_commitment_and_context(
503        &self,
504        ctx: Context,
505        message: Message,
506        commitment: CommitmentLevel,
507    ) -> Result<Option<u64>, BanksClientError> {
508        self.inner
509            .get_fee_for_message_with_commitment_and_context(ctx, message, commitment)
510            .await
511            .map_err(Into::into)
512    }
513}
514
515pub async fn start_client<C>(transport: C) -> Result<BanksClient, BanksClientError>
516where
517    C: Transport<ClientMessage<BanksRequest>, Response<BanksResponse>> + Send + 'static,
518{
519    Ok(BanksClient {
520        inner: TarpcClient::new(client::Config::default(), transport).spawn(),
521    })
522}
523
524pub async fn start_tcp_client<T: ToSocketAddrs>(addr: T) -> Result<BanksClient, BanksClientError> {
525    let transport = tcp::connect(addr, Bincode::default).await?;
526    Ok(BanksClient {
527        inner: TarpcClient::new(client::Config::default(), transport).spawn(),
528    })
529}
530
531#[cfg(test)]
532mod tests {
533    use {
534        super::*,
535        solana_banks_server::banks_server::start_local_server,
536        solana_runtime::{
537            bank::Bank, bank_forks::BankForks, commitment::BlockCommitmentCache,
538            genesis_utils::create_genesis_config,
539        },
540        solana_signer::Signer,
541        solana_system_interface::instruction as system_instruction,
542        solana_transaction::Transaction,
543        std::sync::{Arc, RwLock},
544        tarpc::transport,
545        tokio::{
546            runtime::Runtime,
547            time::{sleep, Duration},
548        },
549    };
550
551    #[test]
552    fn test_banks_client_new() {
553        let (client_transport, _server_transport) = transport::channel::unbounded();
554        BanksClient::new(client::Config::default(), client_transport);
555    }
556
557    #[test]
558    #[allow(clippy::result_large_err)]
559    fn test_banks_server_transfer_via_server() -> Result<(), BanksClientError> {
560        // This test shows the preferred way to interact with BanksServer.
561        // It creates a runtime explicitly (no globals via tokio macros) and calls
562        // `runtime.block_on()` just once, to run all the async code.
563
564        let genesis = create_genesis_config(10);
565        let bank = Bank::new_for_tests(&genesis.genesis_config);
566        let slot = bank.slot();
567        let block_commitment_cache = Arc::new(RwLock::new(
568            BlockCommitmentCache::new_for_tests_with_slots(slot, slot),
569        ));
570        let bank_forks = BankForks::new_rw_arc(bank);
571
572        let bob_pubkey = solana_pubkey::new_rand();
573        let mint_pubkey = genesis.mint_keypair.pubkey();
574        let instruction = system_instruction::transfer(&mint_pubkey, &bob_pubkey, 1);
575        let message = Message::new(&[instruction], Some(&mint_pubkey));
576
577        Runtime::new()?.block_on(async {
578            let client_transport =
579                start_local_server(bank_forks, block_commitment_cache, Duration::from_millis(1))
580                    .await;
581            let banks_client = start_client(client_transport).await?;
582
583            let recent_blockhash = banks_client.get_latest_blockhash().await?;
584            let transaction = Transaction::new(&[&genesis.mint_keypair], message, recent_blockhash);
585            let simulation_result = banks_client
586                .simulate_transaction(transaction.clone())
587                .await
588                .unwrap();
589            assert!(simulation_result.result.unwrap().is_ok());
590            banks_client.process_transaction(transaction).await.unwrap();
591            assert_eq!(banks_client.get_balance(bob_pubkey).await?, 1);
592            Ok(())
593        })
594    }
595
596    #[test]
597    #[allow(clippy::result_large_err)]
598    fn test_banks_server_transfer_via_client() -> Result<(), BanksClientError> {
599        // The caller may not want to hold the connection open until the transaction
600        // is processed (or blockhash expires). In this test, we verify the
601        // server-side functionality is available to the client.
602
603        let genesis = create_genesis_config(10);
604        let bank = Bank::new_for_tests(&genesis.genesis_config);
605        let slot = bank.slot();
606        let block_commitment_cache = Arc::new(RwLock::new(
607            BlockCommitmentCache::new_for_tests_with_slots(slot, slot),
608        ));
609        let bank_forks = BankForks::new_rw_arc(bank);
610
611        let mint_pubkey = &genesis.mint_keypair.pubkey();
612        let bob_pubkey = solana_pubkey::new_rand();
613        let instruction = system_instruction::transfer(mint_pubkey, &bob_pubkey, 1);
614        let message = Message::new(&[instruction], Some(mint_pubkey));
615
616        Runtime::new()?.block_on(async {
617            let client_transport =
618                start_local_server(bank_forks, block_commitment_cache, Duration::from_millis(1))
619                    .await;
620            let banks_client = start_client(client_transport).await?;
621            let (recent_blockhash, last_valid_block_height) = banks_client
622                .get_latest_blockhash_with_commitment(CommitmentLevel::default())
623                .await?
624                .unwrap();
625            let transaction = Transaction::new(&[&genesis.mint_keypair], message, recent_blockhash);
626            let signature = transaction.signatures[0];
627            banks_client.send_transaction(transaction).await?;
628
629            let mut status = banks_client.get_transaction_status(signature).await?;
630
631            while status.is_none() {
632                let root_block_height = banks_client.get_root_block_height().await?;
633                if root_block_height > last_valid_block_height {
634                    break;
635                }
636                sleep(Duration::from_millis(100)).await;
637                status = banks_client.get_transaction_status(signature).await?;
638            }
639            assert!(status.unwrap().err.is_none());
640            assert_eq!(banks_client.get_balance(bob_pubkey).await?, 1);
641            Ok(())
642        })
643    }
644}