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