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#[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
38const RETRY_LOCALNET_EVERY_MILLIS: u64 = 500;
42
43type Payer = Rc<Keypair>;
44
45pub struct Client {
47 payer: Keypair,
48 anchor_client: AnchorClient<Payer>,
49}
50
51impl Client {
52 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 pub fn payer(&self) -> &Keypair {
66 &self.payer
67 }
68
69 pub fn anchor_client(&self) -> &AnchorClient<Payer> {
71 &self.anchor_client
72 }
73
74 pub fn program(&self, program_id: Pubkey) -> Program<Payer> {
76 self.anchor_client.program(program_id)
77 }
78
79 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 #[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 #[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 #[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 #[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 #[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 #[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 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 #[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 #[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 #[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 #[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 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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
698pub trait PrintableTransaction {
700 fn print_named(&self, name: &str);
702
703 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}