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