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