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