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
37const RETRY_LOCALNET_EVERY_MILLIS: u64 = 500;
41
42pub struct Client {
44 payer: Keypair,
45 anchor_client: AnchorClient,
46}
47
48impl Client {
49 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 pub fn payer(&self) -> &Keypair {
63 &self.payer
64 }
65
66 pub fn anchor_client(&self) -> &AnchorClient {
68 &self.anchor_client
69 }
70
71 pub fn program(&self, program_id: Pubkey) -> Program {
73 self.anchor_client.program(program_id)
74 }
75
76 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 #[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 #[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 #[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 #[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 #[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 #[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 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 #[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 #[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 #[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 #[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 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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
694pub trait PrintableTransaction {
696 fn print_named(&self, name: &str);
698
699 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}