solana_banks_client/
lib.rs

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