trdelnik_sandbox_client/
client.rs

1use crate::{config::CONFIG, Reader, TempClone};
2use anchor_client::{
3    anchor_lang::{
4        prelude::System, solana_program::program_pack::Pack, AccountDeserialize, Id,
5        InstructionData, ToAccountMetas,
6    },
7    solana_client::{client_error::ClientErrorKind, rpc_config::RpcTransactionConfig},
8    solana_sdk::{
9        account::Account,
10        bpf_loader,
11        commitment_config::CommitmentConfig,
12        instruction::Instruction,
13        loader_instruction,
14        pubkey::Pubkey,
15        signer::{keypair::Keypair, Signer},
16        system_instruction,
17        transaction::Transaction,
18    },
19    Client as AnchorClient, ClientError as Error, Cluster, Program,
20};
21
22use borsh::BorshDeserialize;
23use fehler::{throw, throws};
24use futures::stream::{self, StreamExt};
25use log::debug;
26use serde::de::DeserializeOwned;
27use solana_account_decoder::parse_token::UiTokenAmount;
28use solana_cli_output::display::println_transaction;
29use solana_transaction_status::{EncodedConfirmedTransactionWithStatusMeta, UiTransactionEncoding};
30use spl_associated_token_account::{
31    get_associated_token_address, instruction::create_associated_token_account,
32};
33use std::{mem, rc::Rc};
34use std::{thread::sleep, time::Duration};
35use tokio::task;
36
37// @TODO: Make compatible with the latest Anchor deps.
38// https://github.com/project-serum/anchor/pull/1307#issuecomment-1022592683
39
40const RETRY_LOCALNET_EVERY_MILLIS: u64 = 500;
41
42/// `Client` allows you to send typed RPC requests to a Solana cluster.
43pub struct Client {
44    payer: Keypair,
45    anchor_client: AnchorClient,
46}
47
48impl Client {
49    /// Creates a new `Client` instance.
50    pub fn new(payer: Keypair) -> Self {
51        Self {
52            payer: payer.clone(),
53            anchor_client: AnchorClient::new_with_options(
54                Cluster::Localnet,
55                Rc::new(payer),
56                CommitmentConfig::confirmed(),
57            ),
58        }
59    }
60
61    /// Gets client's payer.
62    pub fn payer(&self) -> &Keypair {
63        &self.payer
64    }
65
66    /// Gets the internal Anchor client to call Anchor client's methods directly.
67    pub fn anchor_client(&self) -> &AnchorClient {
68        &self.anchor_client
69    }
70
71    /// Creates [Program] instance to communicate with the selected program.
72    pub fn program(&self, program_id: Pubkey) -> Program {
73        self.anchor_client.program(program_id)
74    }
75
76    /// Finds out if the Solana localnet is running.
77    ///
78    /// Set `retry` to `true` when you want to wait for up to 15 seconds until
79    /// the localnet is running (until 30 retries with 500ms delays are performed).
80    pub async fn is_localnet_running(&self, retry: bool) -> bool {
81        let dummy_pubkey = Pubkey::new_from_array([0; 32]);
82        let rpc_client = self.anchor_client.program(dummy_pubkey).rpc();
83        task::spawn_blocking(move || {
84            for _ in 0..(if retry {
85                CONFIG.test.validator_startup_timeout / RETRY_LOCALNET_EVERY_MILLIS
86            } else {
87                1
88            }) {
89                if rpc_client.get_health().is_ok() {
90                    return true;
91                }
92                if retry {
93                    sleep(Duration::from_millis(RETRY_LOCALNET_EVERY_MILLIS));
94                }
95            }
96            false
97        })
98        .await
99        .expect("is_localnet_running task failed")
100    }
101
102    /// Gets deserialized data from the chosen account serialized with Anchor
103    ///
104    /// # Errors
105    ///
106    /// It fails when:
107    /// - the account does not exist.
108    /// - the Solana cluster is not running.
109    /// - deserialization failed.
110    #[throws]
111    pub async fn account_data<T>(&self, account: Pubkey) -> T
112    where
113        T: AccountDeserialize + Send + 'static,
114    {
115        task::spawn_blocking(move || {
116            let dummy_keypair = Keypair::new();
117            let dummy_program_id = Pubkey::new_from_array([0; 32]);
118            let program = Client::new(dummy_keypair).program(dummy_program_id);
119            program.account::<T>(account)
120        })
121        .await
122        .expect("account_data task failed")?
123    }
124
125    /// Gets deserialized data from the chosen account serialized with Bincode
126    ///
127    /// # Errors
128    ///
129    /// It fails when:
130    /// - the account does not exist.
131    /// - the Solana cluster is not running.
132    /// - deserialization failed.
133    #[throws]
134    pub async fn account_data_bincode<T>(&self, account: Pubkey) -> T
135    where
136        T: DeserializeOwned + Send + 'static,
137    {
138        let account = self
139            .get_account(account)
140            .await?
141            .ok_or(Error::AccountNotFound)?;
142
143        bincode::deserialize(&account.data)
144            .map_err(|_| Error::LogParseError("Bincode deserialization failed".to_string()))?
145    }
146
147    /// Gets deserialized data from the chosen account serialized with Borsh
148    ///
149    /// # Errors
150    ///
151    /// It fails when:
152    /// - the account does not exist.
153    /// - the Solana cluster is not running.
154    /// - deserialization failed.
155    #[throws]
156    pub async fn account_data_borsh<T>(&self, account: Pubkey) -> T
157    where
158        T: BorshDeserialize + Send + 'static,
159    {
160        let account = self
161            .get_account(account)
162            .await?
163            .ok_or(Error::AccountNotFound)?;
164
165        T::try_from_slice(&account.data)
166            .map_err(|_| Error::LogParseError("Bincode deserialization failed".to_string()))?
167    }
168
169    /// Returns all information associated with the account of the provided [Pubkey].
170    ///
171    /// # Errors
172    ///
173    /// It fails when the Solana cluster is not running.
174    #[throws]
175    pub async fn get_account(&self, account: Pubkey) -> Option<Account> {
176        let rpc_client = self.anchor_client.program(System::id()).rpc();
177        task::spawn_blocking(move || {
178            rpc_client.get_account_with_commitment(&account, rpc_client.commitment())
179        })
180        .await
181        .expect("get_account task failed")?
182        .value
183    }
184
185    /// Sends the Anchor instruction with associated accounts and signers.
186    ///
187    /// # Example
188    ///
189    /// ```rust,ignore
190    /// use trdelnik_client::*;
191    ///
192    /// pub async fn initialize(
193    ///     client: &Client,
194    ///     state: Pubkey,
195    ///     user: Pubkey,
196    ///     system_program: Pubkey,
197    ///     signers: impl IntoIterator<Item = Keypair> + Send + 'static,
198    /// ) -> Result<EncodedConfirmedTransaction, ClientError> {
199    ///     Ok(client
200    ///         .send_instruction(
201    ///             PROGRAM_ID,
202    ///             turnstile::instruction::Initialize {},
203    ///             turnstile::accounts::Initialize {
204    ///                 state: a_state,
205    ///                 user: a_user,
206    ///                 system_program: a_system_program,
207    ///             },
208    ///             signers,
209    ///         )
210    ///         .await?)
211    /// }
212    /// ```
213    #[throws]
214    pub async fn send_instruction(
215        &self,
216        program: Pubkey,
217        instruction: impl InstructionData + Send + 'static,
218        accounts: impl ToAccountMetas + Send + 'static,
219        signers: impl IntoIterator<Item = Keypair> + Send + 'static,
220    ) -> EncodedConfirmedTransactionWithStatusMeta {
221        let payer = self.payer().clone();
222        let signature = task::spawn_blocking(move || {
223            let program = Client::new(payer).program(program);
224            let mut request = program.request().args(instruction).accounts(accounts);
225            let signers = signers.into_iter().collect::<Vec<_>>();
226            for signer in &signers {
227                request = request.signer(signer);
228            }
229            request.send()
230        })
231        .await
232        .expect("send instruction task failed")?;
233
234        let rpc_client = self.anchor_client.program(System::id()).rpc();
235        task::spawn_blocking(move || {
236            rpc_client.get_transaction_with_config(
237                &signature,
238                RpcTransactionConfig {
239                    encoding: Some(UiTransactionEncoding::Binary),
240                    commitment: Some(CommitmentConfig::confirmed()),
241                    max_supported_transaction_version: None,
242                },
243            )
244        })
245        .await
246        .expect("get transaction task failed")?
247    }
248
249    /// Sends the transaction with associated instructions and signers.
250    ///
251    /// # Example
252    ///
253    /// ```rust,ignore
254    /// #[throws]
255    /// pub async fn create_account(
256    ///     &self,
257    ///     keypair: &Keypair,
258    ///     lamports: u64,
259    ///     space: u64,
260    ///     owner: &Pubkey,
261    /// ) -> EncodedConfirmedTransaction {
262    ///     self.send_transaction(
263    ///         &[system_instruction::create_account(
264    ///             &self.payer().pubkey(),
265    ///             &keypair.pubkey(),
266    ///             lamports,
267    ///             space,
268    ///             owner,
269    ///         )],
270    ///         [keypair],
271    ///     )
272    ///     .await?
273    /// }
274    /// ```
275    #[throws]
276    pub async fn send_transaction(
277        &self,
278        instructions: &[Instruction],
279        signers: impl IntoIterator<Item = &Keypair> + Send,
280    ) -> EncodedConfirmedTransactionWithStatusMeta {
281        let rpc_client = self.anchor_client.program(System::id()).rpc();
282        let mut signers = signers.into_iter().collect::<Vec<_>>();
283        signers.push(self.payer());
284
285        let tx = &Transaction::new_signed_with_payer(
286            instructions,
287            Some(&self.payer.pubkey()),
288            &signers,
289            rpc_client
290                .get_latest_blockhash()
291                .expect("Error while getting recent blockhash"),
292        );
293        // @TODO make this call async with task::spawn_blocking
294        let signature = rpc_client.send_and_confirm_transaction(tx)?;
295        let transaction = task::spawn_blocking(move || {
296            rpc_client.get_transaction_with_config(
297                &signature,
298                RpcTransactionConfig {
299                    encoding: Some(UiTransactionEncoding::Binary),
300                    commitment: Some(CommitmentConfig::confirmed()),
301                    max_supported_transaction_version: None,
302                },
303            )
304        })
305        .await
306        .expect("get transaction task failed")?;
307
308        transaction
309    }
310
311    /// Airdrops lamports to the chosen account.
312    #[throws]
313    pub async fn airdrop(&self, address: Pubkey, lamports: u64) {
314        let rpc_client = self.anchor_client.program(System::id()).rpc();
315        task::spawn_blocking(move || -> Result<(), Error> {
316            let signature = rpc_client.request_airdrop(&address, lamports)?;
317            for _ in 0..5 {
318                match rpc_client.get_signature_status(&signature)? {
319                    Some(Ok(_)) => {
320                        debug!("{} lamports airdropped", lamports);
321                        return Ok(());
322                    }
323                    Some(Err(transaction_error)) => {
324                        throw!(Error::SolanaClientError(transaction_error.into()));
325                    }
326                    None => sleep(Duration::from_millis(500)),
327                }
328            }
329            throw!(Error::SolanaClientError(
330                ClientErrorKind::Custom(
331                    "Airdrop transaction has not been processed yet".to_owned(),
332                )
333                .into(),
334            ));
335        })
336        .await
337        .expect("airdrop task failed")?
338    }
339
340    /// Get balance of an account
341    #[throws]
342    pub async fn get_balance(&mut self, address: Pubkey) -> u64 {
343        let rpc_client = self.anchor_client.program(System::id()).rpc();
344        task::spawn_blocking(move || rpc_client.get_balance(&address))
345            .await
346            .expect("get_balance task failed")?
347    }
348
349    /// Get token balance of an token account
350    #[throws]
351    pub async fn get_token_balance(&mut self, address: Pubkey) -> UiTokenAmount {
352        let rpc_client = self.anchor_client.program(System::id()).rpc();
353        task::spawn_blocking(move || rpc_client.get_token_account_balance(&address))
354            .await
355            .expect("get_token_balance task failed")?
356    }
357
358    /// Deploys a program based on it's name.
359    /// This function wraps boilerplate code required for the successful deployment of a program,
360    /// i.e. SOLs airdrop etc.
361    ///
362    /// # Arguments
363    ///
364    /// * `program_keypair` - [Keypair] used for the program
365    /// * `program_name` - Name of the program to be deployed
366    ///
367    /// # Example:
368    ///
369    /// *Project structure*
370    ///
371    /// ```text
372    /// project/
373    /// - programs/
374    ///   - awesome_contract/
375    ///     - ...
376    ///     - Cargo.toml
377    ///   - turnstile/
378    ///     - ...
379    ///     - Cargo.toml
380    /// - ...
381    /// - Cargo.toml
382    /// ```
383    ///
384    /// *Code*
385    ///
386    /// ```rust,ignore
387    /// client.deploy_program(program_keypair(0), "awesome_contract");
388    /// client.deploy_program(program_keypair(1), "turnstile");
389    /// ```
390    #[throws]
391    pub async fn deploy_by_name(&self, program_keypair: &Keypair, program_name: &str) {
392        debug!("reading program data");
393
394        let reader = Reader::new();
395        let mut program_data = reader
396            .program_data(program_name)
397            .await
398            .expect("reading program data failed");
399
400        debug!("airdropping the minimum balance required to deploy the program");
401
402        // TODO: This will fail on devnet where airdrops are limited to 1 SOL
403        self.airdrop(self.payer().pubkey(), 5_000_000_000)
404            .await
405            .expect("airdropping for deployment failed");
406
407        debug!("deploying program");
408
409        self.deploy(program_keypair.clone(), mem::take(&mut program_data))
410            .await
411            .expect("deploying program failed");
412    }
413
414    /// Deploys the program.
415    #[throws]
416    async fn deploy(&self, program_keypair: Keypair, program_data: Vec<u8>) {
417        const PROGRAM_DATA_CHUNK_SIZE: usize = 900;
418
419        let program_pubkey = program_keypair.pubkey();
420        let system_program = self.anchor_client.program(System::id());
421
422        let program_data_len = program_data.len();
423        debug!("program_data_len: {}", program_data_len);
424
425        debug!("create program account");
426
427        let rpc_client = system_program.rpc();
428        let min_balance_for_rent_exemption = task::spawn_blocking(move || {
429            rpc_client.get_minimum_balance_for_rent_exemption(program_data_len)
430        })
431        .await
432        .expect("crate program account task failed")?;
433
434        let create_account_ix = system_instruction::create_account(
435            &system_program.payer(),
436            &program_pubkey,
437            min_balance_for_rent_exemption,
438            program_data_len as u64,
439            &bpf_loader::id(),
440        );
441        {
442            let program_keypair = Keypair::from_bytes(&program_keypair.to_bytes()).unwrap();
443            let payer = self.payer().clone();
444            task::spawn_blocking(move || {
445                let system_program = Client::new(payer).program(System::id());
446                system_program
447                    .request()
448                    .instruction(create_account_ix)
449                    .signer(&program_keypair)
450                    .send()
451            })
452            .await
453            .expect("create program account task failed")?;
454        }
455
456        debug!("write program data");
457
458        let mut offset = 0usize;
459        let mut futures = Vec::new();
460        for chunk in program_data.chunks(PROGRAM_DATA_CHUNK_SIZE) {
461            let program_keypair = Keypair::from_bytes(&program_keypair.to_bytes()).unwrap();
462            let loader_write_ix = loader_instruction::write(
463                &program_pubkey,
464                &bpf_loader::id(),
465                offset as u32,
466                chunk.to_vec(),
467            );
468            let payer = self.payer().clone();
469
470            futures.push(async move {
471                task::spawn_blocking(move || {
472                    let system_program = Client::new(payer).program(System::id());
473                    system_program
474                        .request()
475                        .instruction(loader_write_ix)
476                        .signer(&program_keypair)
477                        .send()
478                })
479                .await
480                .expect("write program data task failed")
481            });
482            offset += chunk.len();
483        }
484        stream::iter(futures)
485            .buffer_unordered(100)
486            .collect::<Vec<_>>()
487            .await;
488
489        debug!("finalize program");
490
491        let loader_finalize_ix = loader_instruction::finalize(&program_pubkey, &bpf_loader::id());
492        let payer = self.payer().clone();
493        task::spawn_blocking(move || {
494            let system_program = Client::new(payer).program(System::id());
495            system_program
496                .request()
497                .instruction(loader_finalize_ix)
498                .signer(&program_keypair)
499                .send()
500        })
501        .await
502        .expect("finalize program account task failed")?;
503
504        debug!("program deployed");
505    }
506
507    /// Creates accounts.
508    #[throws]
509    pub async fn create_account(
510        &self,
511        keypair: &Keypair,
512        lamports: u64,
513        space: u64,
514        owner: &Pubkey,
515    ) -> EncodedConfirmedTransactionWithStatusMeta {
516        self.send_transaction(
517            &[system_instruction::create_account(
518                &self.payer().pubkey(),
519                &keypair.pubkey(),
520                lamports,
521                space,
522                owner,
523            )],
524            [keypair],
525        )
526        .await?
527    }
528
529    /// Creates rent exempt account.
530    #[throws]
531    pub async fn create_account_rent_exempt(
532        &mut self,
533        keypair: &Keypair,
534        space: u64,
535        owner: &Pubkey,
536    ) -> EncodedConfirmedTransactionWithStatusMeta {
537        let rpc_client = self.anchor_client.program(System::id()).rpc();
538        self.send_transaction(
539            &[system_instruction::create_account(
540                &self.payer().pubkey(),
541                &keypair.pubkey(),
542                rpc_client.get_minimum_balance_for_rent_exemption(space as usize)?,
543                space,
544                owner,
545            )],
546            [keypair],
547        )
548        .await?
549    }
550
551    /// Executes a transaction constructing a token mint.
552    #[throws]
553    pub async fn create_token_mint(
554        &self,
555        mint: &Keypair,
556        authority: Pubkey,
557        freeze_authority: Option<Pubkey>,
558        decimals: u8,
559    ) -> EncodedConfirmedTransactionWithStatusMeta {
560        let rpc_client = self.anchor_client.program(System::id()).rpc();
561        self.send_transaction(
562            &[
563                system_instruction::create_account(
564                    &self.payer().pubkey(),
565                    &mint.pubkey(),
566                    rpc_client
567                        .get_minimum_balance_for_rent_exemption(spl_token::state::Mint::LEN)?,
568                    spl_token::state::Mint::LEN as u64,
569                    &spl_token::ID,
570                ),
571                spl_token::instruction::initialize_mint(
572                    &spl_token::ID,
573                    &mint.pubkey(),
574                    &authority,
575                    freeze_authority.as_ref(),
576                    decimals,
577                )
578                .unwrap(),
579            ],
580            [mint],
581        )
582        .await?
583    }
584
585    /// Executes a transaction that mints tokens from a mint to an account belonging to that mint.
586    #[throws]
587    pub async fn mint_tokens(
588        &self,
589        mint: Pubkey,
590        authority: &Keypair,
591        account: Pubkey,
592        amount: u64,
593    ) -> EncodedConfirmedTransactionWithStatusMeta {
594        self.send_transaction(
595            &[spl_token::instruction::mint_to(
596                &spl_token::ID,
597                &mint,
598                &account,
599                &authority.pubkey(),
600                &[],
601                amount,
602            )
603            .unwrap()],
604            [authority],
605        )
606        .await?
607    }
608
609    /// Executes a transaction constructing a token account of the specified mint. The account needs to be empty and belong to system for this to work.
610    /// Prefer to use [create_associated_token_account] if you don't need the provided account to contain the token account.
611    #[throws]
612    pub async fn create_token_account(
613        &self,
614        account: &Keypair,
615        mint: &Pubkey,
616        owner: &Pubkey,
617    ) -> EncodedConfirmedTransactionWithStatusMeta {
618        let rpc_client = self.anchor_client.program(System::id()).rpc();
619        self.send_transaction(
620            &[
621                system_instruction::create_account(
622                    &self.payer().pubkey(),
623                    &account.pubkey(),
624                    rpc_client
625                        .get_minimum_balance_for_rent_exemption(spl_token::state::Account::LEN)?,
626                    spl_token::state::Account::LEN as u64,
627                    &spl_token::ID,
628                ),
629                spl_token::instruction::initialize_account(
630                    &spl_token::ID,
631                    &account.pubkey(),
632                    mint,
633                    owner,
634                )
635                .unwrap(),
636            ],
637            [account],
638        )
639        .await?
640    }
641
642    /// Executes a transaction constructing the associated token account of the specified mint belonging to the owner. This will fail if the account already exists.
643    #[throws]
644    pub async fn create_associated_token_account(&self, owner: &Keypair, mint: Pubkey) -> Pubkey {
645        self.send_transaction(
646            &[create_associated_token_account(
647                &self.payer().pubkey(),
648                &owner.pubkey(),
649                &mint,
650            )],
651            &[],
652        )
653        .await?;
654        get_associated_token_address(&owner.pubkey(), &mint)
655    }
656
657    /// Executes a transaction creating and filling the given account with the given data.
658    /// The account is required to be empty and will be owned by bpf_loader afterwards.
659    #[throws]
660    pub async fn create_account_with_data(&self, account: &Keypair, data: Vec<u8>) {
661        const DATA_CHUNK_SIZE: usize = 900;
662
663        let rpc_client = self.anchor_client.program(System::id()).rpc();
664        self.send_transaction(
665            &[system_instruction::create_account(
666                &self.payer().pubkey(),
667                &account.pubkey(),
668                rpc_client.get_minimum_balance_for_rent_exemption(data.len())?,
669                data.len() as u64,
670                &bpf_loader::id(),
671            )],
672            [account],
673        )
674        .await?;
675
676        let mut offset = 0usize;
677        for chunk in data.chunks(DATA_CHUNK_SIZE) {
678            debug!("writing bytes {} to {}", offset, offset + chunk.len());
679            self.send_transaction(
680                &[loader_instruction::write(
681                    &account.pubkey(),
682                    &bpf_loader::id(),
683                    offset as u32,
684                    chunk.to_vec(),
685                )],
686                [account],
687            )
688            .await?;
689            offset += chunk.len();
690        }
691    }
692}
693
694/// Utility trait for printing transaction results.
695pub trait PrintableTransaction {
696    /// Pretty print the transaction results, tagged with the given name for distinguishability.
697    fn print_named(&self, name: &str);
698
699    /// Pretty print the transaction results.
700    fn print(&self) {
701        self.print_named("");
702    }
703}
704
705impl PrintableTransaction for EncodedConfirmedTransactionWithStatusMeta {
706    fn print_named(&self, name: &str) {
707        let tx = self.transaction.transaction.decode().unwrap();
708        debug!("EXECUTE {} (slot {})", name, self.slot);
709        match self.transaction.meta.clone() {
710            Some(meta) => println_transaction(&tx, Some(&meta), "  ", None, None),
711            _ => println_transaction(&tx, None, "  ", None, None),
712        }
713    }
714}