1#![cfg_attr(docsrs, feature(doc_cfg))]
2
3#[cfg(feature = "async")]
68pub use nonblocking::ThreadSafeSigner;
69pub use {
70 anchor_lang,
71 cluster::Cluster,
72 solana_commitment_config::CommitmentConfig,
73 solana_hash::Hash,
74 solana_instruction::Instruction,
75 solana_message::AddressLookupTableAccount,
76 solana_pubsub_client::nonblocking::pubsub_client::PubsubClientError,
77 solana_rpc_client_api::{
78 client_error::{Error as SolanaClientError, ErrorKind as SolanaClientErrorKind},
79 config::RpcSendTransactionConfig,
80 filter::RpcFilterType,
81 },
82 solana_signer::{Signer, SignerError},
83 solana_transaction::{versioned::VersionedTransaction, Transaction},
84};
85use {
86 anchor_lang::{
87 solana_program::{program_error::ProgramError, pubkey::Pubkey},
88 AccountDeserialize, Discriminator, InstructionData, ToAccountMetas,
89 },
90 futures::{Future, StreamExt},
91 regex::Regex,
92 solana_account_decoder::{UiAccount, UiAccountEncoding},
93 solana_instruction::AccountMeta,
94 solana_message::v0,
95 solana_pubsub_client::nonblocking::pubsub_client::PubsubClient,
96 solana_rpc_client::nonblocking::rpc_client::RpcClient as AsyncRpcClient,
97 solana_rpc_client_api::{
98 config::{
99 RpcAccountInfoConfig, RpcProgramAccountsConfig, RpcTransactionLogsConfig,
100 RpcTransactionLogsFilter,
101 },
102 filter::Memcmp,
103 request::RpcError,
104 response::{Response as RpcResponse, RpcLogsResponse},
105 },
106 solana_signature::Signature,
107 std::{
108 iter::Map,
109 marker::PhantomData,
110 ops::Deref,
111 pin::Pin,
112 sync::{Arc, LazyLock},
113 vec::IntoIter,
114 },
115 thiserror::Error,
116 tokio::{
117 runtime::Handle,
118 sync::{
119 mpsc::{unbounded_channel, UnboundedReceiver},
120 OnceCell,
121 },
122 task::JoinHandle,
123 },
124};
125
126mod cluster;
127
128#[derive(Debug, Clone, Default)]
130pub enum TxVersion<'a> {
131 #[default]
133 Legacy,
134 V0(&'a [AddressLookupTableAccount]),
136}
137
138#[cfg(not(feature = "async"))]
139mod blocking;
140#[cfg(feature = "async")]
141mod nonblocking;
142
143const PROGRAM_LOG: &str = "Program log: ";
144const PROGRAM_DATA: &str = "Program data: ";
145
146type UnsubscribeFn = Box<dyn FnOnce() -> Pin<Box<dyn Future<Output = ()> + Send>> + Send>;
147pub struct Client<C> {
151 cfg: Config<C>,
152}
153
154impl<C: Clone + Deref<Target = impl Signer>> Client<C> {
155 pub fn new(cluster: Cluster, payer: C) -> Self {
156 Self {
157 cfg: Config {
158 cluster,
159 payer,
160 options: None,
161 },
162 }
163 }
164
165 pub fn new_with_options(cluster: Cluster, payer: C, options: CommitmentConfig) -> Self {
166 Self {
167 cfg: Config {
168 cluster,
169 payer,
170 options: Some(options),
171 },
172 }
173 }
174
175 pub fn program(
176 &self,
177 program_id: Pubkey,
178 #[cfg(feature = "mock")] rpc_client: AsyncRpcClient,
179 ) -> Result<Program<C>, ClientError> {
180 let cfg = Config {
181 cluster: self.cfg.cluster.clone(),
182 options: self.cfg.options,
183 payer: self.cfg.payer.clone(),
184 };
185
186 Program::new(
187 program_id,
188 cfg,
189 #[cfg(feature = "mock")]
190 rpc_client,
191 )
192 }
193}
194
195pub struct DynSigner(pub Arc<dyn Signer>);
199
200impl Signer for DynSigner {
201 fn pubkey(&self) -> Pubkey {
202 self.0.pubkey()
203 }
204
205 fn try_pubkey(&self) -> Result<Pubkey, SignerError> {
206 self.0.try_pubkey()
207 }
208
209 fn sign_message(&self, message: &[u8]) -> Signature {
210 self.0.sign_message(message)
211 }
212
213 fn try_sign_message(&self, message: &[u8]) -> Result<Signature, SignerError> {
214 self.0.try_sign_message(message)
215 }
216
217 fn is_interactive(&self) -> bool {
218 self.0.is_interactive()
219 }
220}
221
222#[derive(Debug)]
224pub struct Config<C> {
225 cluster: Cluster,
226 payer: C,
227 options: Option<CommitmentConfig>,
228}
229
230pub struct EventUnsubscriber<'a> {
231 handle: JoinHandle<Result<(), ClientError>>,
232 rx: UnboundedReceiver<UnsubscribeFn>,
233 #[cfg(not(feature = "async"))]
234 runtime_handle: &'a Handle,
235 _lifetime_marker: PhantomData<&'a Handle>,
236}
237
238impl EventUnsubscriber<'_> {
239 async fn unsubscribe_internal(mut self) {
240 if let Some(unsubscribe) = self.rx.recv().await {
241 unsubscribe().await;
242 }
243
244 let _ = self.handle.await;
245 }
246}
247
248pub struct Program<C> {
250 program_id: Pubkey,
251 cfg: Config<C>,
252 sub_client: OnceCell<Arc<PubsubClient>>,
253 #[cfg(not(feature = "async"))]
254 rt: tokio::runtime::Runtime,
255 internal_rpc_client: AsyncRpcClient,
256}
257
258impl<C: Deref<Target = impl Signer> + Clone> Program<C> {
259 pub fn payer(&self) -> Pubkey {
260 self.cfg.payer.pubkey()
261 }
262
263 pub fn id(&self) -> Pubkey {
264 self.program_id
265 }
266
267 #[cfg(feature = "mock")]
268 pub fn internal_rpc(&self) -> &AsyncRpcClient {
269 &self.internal_rpc_client
270 }
271
272 async fn account_internal<T: AccountDeserialize>(
273 &self,
274 address: Pubkey,
275 ) -> Result<T, ClientError> {
276 let account = self
277 .internal_rpc_client
278 .get_account_with_commitment(&address, self.internal_rpc_client.commitment())
279 .await
280 .map_err(Box::new)?
281 .value
282 .ok_or(ClientError::AccountNotFound)?;
283 let mut data: &[u8] = &account.data;
284 T::try_deserialize(&mut data).map_err(Into::into)
285 }
286
287 async fn accounts_lazy_internal<T: AccountDeserialize + Discriminator>(
288 &self,
289 filters: Vec<RpcFilterType>,
290 ) -> Result<ProgramAccountsIterator<T>, ClientError> {
291 let account_type_filter =
292 RpcFilterType::Memcmp(Memcmp::new_base58_encoded(0, T::DISCRIMINATOR));
293 let config = RpcProgramAccountsConfig {
294 filters: Some([vec![account_type_filter], filters].concat()),
295 account_config: RpcAccountInfoConfig {
296 encoding: Some(UiAccountEncoding::Base64),
297 ..RpcAccountInfoConfig::default()
298 },
299 ..RpcProgramAccountsConfig::default()
300 };
301
302 Ok(ProgramAccountsIterator {
303 inner: self
304 .internal_rpc_client
305 .get_program_ui_accounts_with_config(&self.id(), config)
306 .await
307 .map_err(Box::new)?
308 .into_iter()
309 .map(|(key, account)| {
310 let data = account.data.decode().ok_or_else(|| {
311 ClientError::SolanaClientError(Box::new(
312 SolanaClientError::new_with_request(
313 SolanaClientErrorKind::Custom(
314 "Failed to decode account data".to_string(),
315 ),
316 solana_rpc_client_api::request::RpcRequest::GetProgramAccounts,
317 ),
318 ))
319 })?;
320 Ok((key, T::try_deserialize(&mut data.as_slice())?))
321 }),
322 })
323 }
324
325 async fn on_internal<T: anchor_lang::Event + anchor_lang::AnchorDeserialize>(
326 &self,
327 mut f: impl FnMut(&EventContext, T) + Send + 'static,
328 ) -> Result<
329 (
330 JoinHandle<Result<(), ClientError>>,
331 UnboundedReceiver<UnsubscribeFn>,
332 ),
333 ClientError,
334 > {
335 let client = self
336 .sub_client
337 .get_or_try_init(|| async {
338 PubsubClient::new(self.cfg.cluster.ws_url())
339 .await
340 .map(Arc::new)
341 .map_err(|e| ClientError::SolanaClientPubsubError(Box::new(e)))
342 })
343 .await?
344 .clone();
345
346 let (tx, rx) = unbounded_channel::<_>();
347 let config = RpcTransactionLogsConfig {
348 commitment: self.cfg.options,
349 };
350 let program_id_str = self.program_id.to_string();
351 let filter = RpcTransactionLogsFilter::Mentions(vec![program_id_str.clone()]);
352
353 let handle = tokio::spawn(async move {
354 let (mut notifications, unsubscribe) = client
355 .logs_subscribe(filter, config)
356 .await
357 .map_err(Box::new)?;
358
359 tx.send(unsubscribe).map_err(|e| {
360 ClientError::SolanaClientPubsubError(Box::new(PubsubClientError::RequestFailed {
361 message: "Unsubscribe failed".to_string(),
362 reason: e.to_string(),
363 }))
364 })?;
365
366 while let Some(logs) = notifications.next().await {
367 let signature: Signature = logs.value.signature.parse().map_err(|e| {
368 ClientError::LogParseError(format!(
369 "Invalid signature '{}': {e}",
370 logs.value.signature
371 ))
372 })?;
373 let ctx = EventContext {
374 signature,
375 slot: logs.context.slot,
376 };
377 let events = parse_logs_response(logs, &program_id_str)?;
378 for e in events {
379 f(&ctx, e);
380 }
381 }
382 Ok::<(), ClientError>(())
383 });
384
385 Ok((handle, rx))
386 }
387}
388
389pub struct ProgramAccountsIterator<T> {
392 inner: Map<IntoIter<(Pubkey, UiAccount)>, AccountConverterFunction<T>>,
393}
394
395type AccountConverterFunction<T> = fn((Pubkey, UiAccount)) -> Result<(Pubkey, T), ClientError>;
397
398impl<T> Iterator for ProgramAccountsIterator<T> {
399 type Item = Result<(Pubkey, T), ClientError>;
400
401 fn next(&mut self) -> Option<Self::Item> {
402 self.inner.next()
403 }
404}
405
406pub fn handle_program_log<T: anchor_lang::Event + anchor_lang::AnchorDeserialize>(
407 self_program_str: &str,
408 l: &str,
409) -> Result<(Option<T>, Option<String>, bool), ClientError> {
410 use {
411 anchor_lang::__private::base64,
412 base64::{engine::general_purpose::STANDARD, Engine},
413 };
414
415 if let Some(log) = l
417 .strip_prefix(PROGRAM_LOG)
418 .or_else(|| l.strip_prefix(PROGRAM_DATA))
419 {
420 let log_bytes = match STANDARD.decode(log) {
421 Ok(log_bytes) => log_bytes,
422 _ => {
423 #[cfg(feature = "debug")]
424 println!("Could not base64 decode log: {}", log);
425 return Ok((None, None, false));
426 }
427 };
428
429 let event = log_bytes
430 .starts_with(T::DISCRIMINATOR)
431 .then(|| {
432 let mut data = &log_bytes[T::DISCRIMINATOR.len()..];
433 T::deserialize(&mut data).map_err(|e| ClientError::LogParseError(e.to_string()))
434 })
435 .transpose()?;
436
437 Ok((event, None, false))
438 }
439 else {
441 let (program, did_pop) = handle_system_log(self_program_str, l);
442 Ok((None, program, did_pop))
443 }
444}
445
446pub fn handle_system_log(this_program_str: &str, log: &str) -> (Option<String>, bool) {
447 static INVOKE_RE: LazyLock<Regex> = LazyLock::new(|| {
448 Regex::new(r"^Program ([1-9A-HJ-NP-Za-km-z]+) invoke \[([\d]+)\]$").unwrap()
449 });
450 if let Some(invoke_match) = INVOKE_RE.captures(log) {
451 if invoke_match.get(1).unwrap().as_str() == this_program_str {
452 return (Some(this_program_str.to_string()), false);
453
454 } else if invoke_match.get(2).unwrap().as_str() != "1" {
457 return (Some("cpi".to_string()), false); }
459 }
460
461 if log.starts_with(&format!("Program {this_program_str} log:")) {
462 (Some(this_program_str.to_string()), false)
463 } else {
464 static SUCCESS_RE: LazyLock<Regex> =
465 LazyLock::new(|| Regex::new(r"^Program ([1-9A-HJ-NP-Za-km-z]+) success$").unwrap());
466 if SUCCESS_RE.is_match(log) {
467 (None, true)
468 } else {
469 (None, false)
470 }
471 }
472}
473
474pub struct Execution {
475 stack: Vec<String>,
476}
477
478impl Execution {
479 pub fn new(logs: &mut &[String]) -> Result<Self, ClientError> {
480 let l = &logs[0];
481 *logs = &logs[1..];
482 static RE: LazyLock<Regex> = LazyLock::new(|| {
483 Regex::new(r"^Program ([1-9A-HJ-NP-Za-km-z]+) invoke \[[\d]+\]$").unwrap()
484 });
485 let c = RE
486 .captures(l)
487 .ok_or_else(|| ClientError::LogParseError(l.to_string()))?;
488 let program = c
489 .get(1)
490 .ok_or_else(|| ClientError::LogParseError(l.to_string()))?
491 .as_str()
492 .to_string();
493 Ok(Self {
494 stack: vec![program],
495 })
496 }
497
498 pub fn program(&self) -> String {
506 assert!(!self.stack.is_empty());
507 self.stack[self.stack.len() - 1].clone()
508 }
509
510 pub fn try_program(&self) -> Option<String> {
513 self.stack.last().cloned()
514 }
515
516 pub fn push(&mut self, new_program: String) {
517 self.stack.push(new_program);
518 }
519
520 pub fn pop(&mut self) {
524 self.stack.pop();
525 }
526}
527
528#[derive(Debug)]
529pub struct EventContext {
530 pub signature: Signature,
531 pub slot: u64,
532}
533
534#[derive(Debug, Error)]
535pub enum ClientError {
536 #[error("Account not found")]
537 AccountNotFound,
538 #[error("{0}")]
539 AnchorError(#[from] anchor_lang::error::Error),
540 #[error("{0}")]
541 ProgramError(#[from] ProgramError),
542 #[error("{0}")]
543 SolanaClientError(#[from] Box<SolanaClientError>),
544 #[error("{0}")]
545 SolanaClientPubsubError(#[from] Box<PubsubClientError>),
546 #[error("Unable to parse log: {0}")]
547 LogParseError(String),
548 #[error(transparent)]
549 IOError(#[from] std::io::Error),
550 #[error("{0}")]
551 SignerError(#[from] SignerError),
552}
553
554impl ClientError {
555 fn other<E>(e: E) -> Self
558 where
559 E: Into<Box<dyn std::error::Error + Send + Sync>>,
560 {
561 Self::IOError(std::io::Error::other(e))
562 }
563}
564
565pub trait AsSigner {
566 fn as_signer(&self) -> &dyn Signer;
567}
568
569impl AsSigner for Box<dyn Signer + '_> {
570 fn as_signer(&self) -> &dyn Signer {
571 self.as_ref()
572 }
573}
574
575pub struct RequestBuilder<'a, C, S: 'a> {
578 cluster: String,
579 program_id: Pubkey,
580 accounts: Vec<AccountMeta>,
581 options: CommitmentConfig,
582 instructions: Vec<Instruction>,
583 payer: C,
584 instruction_data: Option<Vec<u8>>,
585 signers: Vec<S>,
586 #[cfg(not(feature = "async"))]
587 handle: &'a Handle,
588 internal_rpc_client: &'a AsyncRpcClient,
589 _phantom: PhantomData<&'a ()>,
590}
591
592impl<C: Deref<Target = impl Signer> + Clone, S: AsSigner> RequestBuilder<'_, C, S> {
594 #[must_use]
595 pub fn payer(mut self, payer: C) -> Self {
596 self.payer = payer;
597 self
598 }
599
600 #[must_use]
601 pub fn cluster(mut self, url: &str) -> Self {
602 self.cluster = url.to_string();
603 self
604 }
605
606 #[must_use]
607 pub fn instruction(mut self, ix: Instruction) -> Self {
608 self.instructions.push(ix);
609 self
610 }
611
612 #[must_use]
613 pub fn program(mut self, program_id: Pubkey) -> Self {
614 self.program_id = program_id;
615 self
616 }
617
618 #[must_use]
649 pub fn accounts(mut self, accounts: impl ToAccountMetas) -> Self {
650 let mut metas = accounts.to_account_metas(None);
651 self.accounts.append(&mut metas);
652 self
653 }
654
655 #[must_use]
656 pub fn options(mut self, options: CommitmentConfig) -> Self {
657 self.options = options;
658 self
659 }
660
661 #[must_use]
662 pub fn args(mut self, args: impl InstructionData) -> Self {
663 self.instruction_data = Some(args.data());
664 self
665 }
666
667 pub fn instructions(&self) -> Vec<Instruction> {
668 let mut instructions = self.instructions.clone();
669 if let Some(ix_data) = &self.instruction_data {
670 instructions.push(Instruction {
671 program_id: self.program_id,
672 data: ix_data.clone(),
673 accounts: self.accounts.clone(),
674 });
675 }
676
677 instructions
678 }
679
680 pub fn transaction(&self) -> Transaction {
685 let instructions = &self.instructions();
686 Transaction::new_with_payer(instructions, Some(&self.payer.pubkey()))
687 }
688
689 pub fn transaction_versioned(
724 &self,
725 version: TxVersion<'_>,
726 recent_blockhash: Hash,
727 ) -> Result<solana_transaction::versioned::VersionedTransaction, ClientError> {
728 let instructions = self.instructions();
729 let payer = self.payer.pubkey();
730
731 match version {
732 TxVersion::Legacy => {
733 let message = solana_message::legacy::Message::new_with_blockhash(
734 &instructions,
735 Some(&payer),
736 &recent_blockhash,
737 );
738 Ok(solana_transaction::versioned::VersionedTransaction {
739 signatures: vec![
740 solana_signature::Signature::default();
741 message.header.num_required_signatures as usize
742 ],
743 message: solana_message::VersionedMessage::Legacy(message),
744 })
745 }
746 TxVersion::V0(address_lookup_table_accounts) => {
747 let message = v0::Message::try_compile(
748 &payer,
749 &instructions,
750 address_lookup_table_accounts,
751 recent_blockhash,
752 )
753 .map_err(ClientError::other)?;
754 Ok(solana_transaction::versioned::VersionedTransaction {
755 signatures: vec![
756 solana_signature::Signature::default();
757 message.header.num_required_signatures as usize
758 ],
759 message: solana_message::VersionedMessage::V0(message),
760 })
761 }
762 }
763 }
764
765 fn signed_transaction_with_blockhash_versioned(
766 &self,
767 version: TxVersion<'_>,
768 latest_hash: Hash,
769 ) -> Result<solana_transaction::versioned::VersionedTransaction, ClientError> {
770 let signers: Vec<&dyn Signer> = self.signers.iter().map(|s| s.as_signer()).collect();
771 let mut all_signers = signers;
772 all_signers.push(&*self.payer);
773
774 let instructions = self.instructions();
775 let payer = self.payer.pubkey();
776
777 let message = match version {
778 TxVersion::Legacy => {
779 let msg = solana_message::legacy::Message::new_with_blockhash(
780 &instructions,
781 Some(&payer),
782 &latest_hash,
783 );
784 solana_message::VersionedMessage::Legacy(msg)
785 }
786 TxVersion::V0(address_lookup_table_accounts) => {
787 let msg = v0::Message::try_compile(
788 &payer,
789 &instructions,
790 address_lookup_table_accounts,
791 latest_hash,
792 )
793 .map_err(ClientError::other)?;
794 solana_message::VersionedMessage::V0(msg)
795 }
796 };
797
798 let tx =
799 solana_transaction::versioned::VersionedTransaction::try_new(message, &all_signers)?;
800
801 Ok(tx)
802 }
803
804 async fn signed_transaction_internal(
805 &self,
806 version: TxVersion<'_>,
807 ) -> Result<solana_transaction::versioned::VersionedTransaction, ClientError> {
808 let latest_hash = self
809 .internal_rpc_client
810 .get_latest_blockhash_with_commitment(self.options)
811 .await
812 .map_err(Box::new)?
813 .0;
814
815 self.signed_transaction_with_blockhash_versioned(version, latest_hash)
816 }
817
818 async fn send_internal(&self, version: TxVersion<'_>) -> Result<Signature, ClientError> {
819 let (latest_hash, _) = self
820 .internal_rpc_client
821 .get_latest_blockhash_with_commitment(self.options)
822 .await
823 .map_err(Box::new)?;
824 let tx = self.signed_transaction_with_blockhash_versioned(version, latest_hash)?;
825
826 let signature = self
833 .internal_rpc_client
834 .send_transaction(&tx)
835 .await
836 .map_err(Box::new)?;
837
838 loop {
839 match self
840 .internal_rpc_client
841 .get_signature_status_with_commitment(&signature, self.options)
842 .await
843 .map_err(Box::new)?
844 {
845 Some(Ok(())) => return Ok(signature),
846 Some(Err(e)) => return Err(ClientError::SolanaClientError(Box::new(e.into()))),
847 None => {
848 if !self
849 .internal_rpc_client
850 .is_blockhash_valid(&latest_hash, CommitmentConfig::processed())
851 .await
852 .map_err(Box::new)?
853 {
854 break;
856 } else if cfg!(not(test)) {
857 tokio::time::sleep(std::time::Duration::from_millis(500)).await;
859 }
860 }
861 }
862 }
863
864 Err(ClientError::SolanaClientError(Box::new(
865 RpcError::ForUser(
866 "unable to confirm transaction. This can happen in situations such as transaction \
867 expiration and insufficient fee-payer funds"
868 .to_string(),
869 )
870 .into(),
871 )))
872 }
873
874 async fn send_with_spinner_and_config_internal(
875 &self,
876 version: TxVersion<'_>,
877 config: RpcSendTransactionConfig,
878 ) -> Result<Signature, ClientError> {
879 let (latest_hash, _) = self
880 .internal_rpc_client
881 .get_latest_blockhash_with_commitment(self.options)
882 .await
883 .map_err(Box::new)?;
884 let tx = self.signed_transaction_with_blockhash_versioned(version, latest_hash)?;
885
886 self.internal_rpc_client
887 .send_and_confirm_transaction_with_spinner_and_config(&tx, self.options, config)
888 .await
889 .map_err(|e| Box::new(e).into())
890 }
891}
892
893fn parse_logs_response<T: anchor_lang::Event + anchor_lang::AnchorDeserialize>(
894 logs: RpcResponse<RpcLogsResponse>,
895 program_id_str: &str,
896) -> Result<Vec<T>, ClientError> {
897 let mut logs = &logs.value.logs[..];
898 let mut events: Vec<T> = Vec::new();
899 if !logs.is_empty() {
900 if let Ok(mut execution) = Execution::new(&mut logs) {
901 let mut logs_iter = logs.iter().peekable();
903 static RE: LazyLock<Regex> = LazyLock::new(|| {
904 Regex::new(r"^Program ([1-9A-HJ-NP-Za-km-z]+) invoke \[(\d+)\]$").unwrap()
905 });
906
907 while let Some(l) = logs_iter.next() {
908 let Some(current_program) = execution.try_program() else {
918 if let Some(caps) = RE.captures(l) {
919 if &caps[2] == "1" {
920 execution.push(caps[1].to_string());
921 }
922 }
923 continue;
924 };
925
926 let (event, new_program, did_pop) = {
928 if program_id_str == current_program {
929 handle_program_log(program_id_str, l)?
930 } else {
931 let (program, did_pop) = handle_system_log(program_id_str, l);
932 (None, program, did_pop)
933 }
934 };
935 if let Some(e) = event {
937 events.push(e);
938 }
939 if let Some(new_program) = new_program {
941 execution.push(new_program);
942 }
943 if did_pop {
945 execution.pop();
946
947 if let Some(&next_log) = logs_iter.peek() {
961 if let Some(caps) = RE.captures(next_log) {
962 if &caps[2] == "1" {
963 execution.push(caps[1].to_string());
964 }
965 }
966 };
967 }
968 }
969 }
970 }
971 Ok(events)
972}
973
974#[cfg(test)]
975mod tests {
976 use {
979 anchor_lang::{prelude::*, Event},
980 futures::{SinkExt, StreamExt},
981 solana_rpc_client_api::response::RpcResponseContext,
982 std::sync::atomic::{AtomicU64, Ordering},
983 tokio_tungstenite::tungstenite::Message,
984 };
985 #[derive(Debug, Clone, Copy)]
986 #[event]
987 pub struct MockEvent {}
988
989 use super::*;
990 #[test]
991 fn new_execution() {
992 let mut logs: &[String] =
993 &["Program 7Y8VDzehoewALqJfyxZYMgYCnMTCDhWuGfJKUvjYWATw invoke [1]".to_string()];
994 let exe = Execution::new(&mut logs).unwrap();
995 assert_eq!(
996 exe.stack[0],
997 "7Y8VDzehoewALqJfyxZYMgYCnMTCDhWuGfJKUvjYWATw".to_string()
998 );
999 }
1000
1001 #[test]
1002 fn handle_system_log_pop() {
1003 let log = "Program 7Y8VDzehoewALqJfyxZYMgYCnMTCDhWuGfJKUvjYWATw success";
1004 let (program, did_pop) = handle_system_log("asdf", log);
1005 assert_eq!(program, None);
1006 assert!(did_pop);
1007 }
1008
1009 #[test]
1010 fn handle_system_log_no_pop() {
1011 let log = "Program 7swsTUiQ6KUK4uFYquQKg4epFRsBnvbrTf2fZQCa2sTJ qwer";
1012 let (program, did_pop) = handle_system_log("asdf", log);
1013 assert_eq!(program, None);
1014 assert!(!did_pop);
1015 }
1016
1017 #[test]
1018 fn test_parse_logs_response() -> Result<()> {
1019 let logs = vec![
1021 "Program VeryCoolProgram invoke [1]", "Program log: Instruction: VeryCoolEvent",
1023 "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]",
1024 "Program log: Instruction: Transfer",
1025 "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4645 of 664387 compute \
1026 units",
1027 "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success",
1028 "Program VeryCoolProgram consumed 42417 of 700000 compute units",
1029 "Program VeryCoolProgram success", "Program EvenCoolerProgram invoke [1]", "Program log: Instruction: EvenCoolerEvent",
1032 "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]",
1033 "Program log: Instruction: TransferChecked",
1034 "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 6200 of 630919 compute \
1035 units",
1036 "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success",
1037 "Program HyaB3W9q6XdA5xwpU4XnSZV94htfmbmqJXZcEbRaJutt invoke [2]",
1038 "Program log: Instruction: Swap",
1039 "Program log: INVARIANT: SWAP",
1040 "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [3]",
1041 "Program log: Instruction: Transfer",
1042 "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4736 of 539321 compute \
1043 units",
1044 "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success",
1045 "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [3]",
1046 "Program log: Instruction: Transfer",
1047 "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4645 of 531933 compute \
1048 units",
1049 "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success",
1050 "Program HyaB3W9q6XdA5xwpU4XnSZV94htfmbmqJXZcEbRaJutt consumed 84670 of 610768 \
1051 compute units",
1052 "Program HyaB3W9q6XdA5xwpU4XnSZV94htfmbmqJXZcEbRaJutt success",
1053 "Program EvenCoolerProgram invoke [2]",
1054 "Program EvenCoolerProgram consumed 2021 of 523272 compute units",
1055 "Program EvenCoolerProgram success",
1056 "Program HyaB3W9q6XdA5xwpU4XnSZV94htfmbmqJXZcEbRaJutt invoke [2]",
1057 "Program log: Instruction: Swap",
1058 "Program log: INVARIANT: SWAP",
1059 "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [3]",
1060 "Program log: Instruction: Transfer",
1061 "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4736 of 418618 compute \
1062 units",
1063 "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success",
1064 "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [3]",
1065 "Program log: Instruction: Transfer",
1066 "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4645 of 411230 compute \
1067 units",
1068 "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success",
1069 "Program HyaB3W9q6XdA5xwpU4XnSZV94htfmbmqJXZcEbRaJutt consumed 102212 of 507607 \
1070 compute units",
1071 "Program HyaB3W9q6XdA5xwpU4XnSZV94htfmbmqJXZcEbRaJutt success",
1072 "Program EvenCoolerProgram invoke [2]",
1073 "Program EvenCoolerProgram consumed 2021 of 402569 compute units",
1074 "Program EvenCoolerProgram success",
1075 "Program 9W959DqEETiGZocYWCQPaJ6sBmUzgfxXfqGeTEdp3aQP invoke [2]",
1076 "Program log: Instruction: Swap",
1077 "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [3]",
1078 "Program log: Instruction: Transfer",
1079 "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4736 of 371140 compute \
1080 units",
1081 "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success",
1082 "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [3]",
1083 "Program log: Instruction: MintTo",
1084 "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4492 of 341800 compute \
1085 units",
1086 "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success",
1087 "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [3]",
1088 "Program log: Instruction: Transfer",
1089 "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4645 of 334370 compute \
1090 units",
1091 "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success",
1092 "Program 9W959DqEETiGZocYWCQPaJ6sBmUzgfxXfqGeTEdp3aQP consumed 57610 of 386812 \
1093 compute units",
1094 "Program 9W959DqEETiGZocYWCQPaJ6sBmUzgfxXfqGeTEdp3aQP success",
1095 "Program EvenCoolerProgram invoke [2]",
1096 "Program EvenCoolerProgram consumed 2021 of 326438 compute units",
1097 "Program EvenCoolerProgram success",
1098 "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]",
1099 "Program log: Instruction: TransferChecked",
1100 "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 6173 of 319725 compute \
1101 units",
1102 "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success",
1103 "Program EvenCoolerProgram consumed 345969 of 657583 compute units",
1104 "Program EvenCoolerProgram success", "Program ComputeBudget111111111111111111111111111111 invoke [1]",
1106 "Program ComputeBudget111111111111111111111111111111 success",
1107 "Program ComputeBudget111111111111111111111111111111 invoke [1]",
1108 "Program ComputeBudget111111111111111111111111111111 success",
1109 ];
1110
1111 let logs: Vec<String> = logs.iter().map(|&l| l.to_string()).collect();
1113
1114 let program_id_str = "VeryCoolProgram";
1115
1116 parse_logs_response::<MockEvent>(
1119 RpcResponse {
1120 context: RpcResponseContext::new(0),
1121 value: RpcLogsResponse {
1122 signature: "".to_string(),
1123 err: None,
1124 logs: logs.to_vec(),
1125 },
1126 },
1127 program_id_str,
1128 )
1129 .unwrap();
1130
1131 Ok(())
1132 }
1133
1134 #[test]
1135 fn test_parse_logs_response_fake_pop() -> Result<()> {
1136 let logs = [
1137 "Program fake111111111111111111111111111111111111112 invoke [1]",
1138 "Program log: i logged success",
1139 "Program log: i logged success",
1140 "Program fake111111111111111111111111111111111111112 consumed 1411 of 200000 compute \
1141 units",
1142 "Program fake111111111111111111111111111111111111112 success",
1143 ];
1144
1145 let logs: Vec<String> = logs.iter().map(|&l| l.to_string()).collect();
1147
1148 let program_id_str = "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb";
1149
1150 parse_logs_response::<MockEvent>(
1153 RpcResponse {
1154 context: RpcResponseContext::new(0),
1155 value: RpcLogsResponse {
1156 signature: "".to_string(),
1157 err: None,
1158 logs: logs.to_vec(),
1159 },
1160 },
1161 program_id_str,
1162 )
1163 .unwrap();
1164
1165 Ok(())
1166 }
1167
1168 #[test]
1174 fn test_parse_logs_response_log_line_ends_with_invoke_1() -> Result<()> {
1175 let logs = [
1176 "Program VeryCoolProgram invoke [1]",
1177 "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]",
1178 "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success",
1179 "Program log: forwarded inner instruction invoke [1]",
1182 "Program VeryCoolProgram success",
1183 ];
1184 let logs: Vec<String> = logs.iter().map(|&l| l.to_string()).collect();
1185
1186 parse_logs_response::<MockEvent>(
1187 RpcResponse {
1188 context: RpcResponseContext::new(0),
1189 value: RpcLogsResponse {
1190 signature: "".to_string(),
1191 err: None,
1192 logs,
1193 },
1194 },
1195 "VeryCoolProgram",
1196 )
1197 .unwrap();
1198
1199 Ok(())
1200 }
1201
1202 #[test]
1203 fn execution_pop_past_empty_is_not_a_panic() {
1204 let mut logs: &[String] =
1205 &["Program term9YPb9mzAsABaqN71A4xdbxHmpBNZavpBiQKZzN3 invoke [1]".to_string()];
1206 let mut exe = Execution::new(&mut logs).unwrap();
1207 assert_eq!(
1208 exe.try_program().as_deref(),
1209 Some("term9YPb9mzAsABaqN71A4xdbxHmpBNZavpBiQKZzN3")
1210 );
1211
1212 exe.pop();
1213 assert_eq!(exe.try_program(), None);
1214
1215 exe.pop();
1217 assert_eq!(exe.try_program(), None);
1218 }
1219
1220 #[test]
1226 fn test_parse_logs_response_trailing_log_after_last_instruction() -> Result<()> {
1227 let logs = [
1228 "Program ComputeBudget111111111111111111111111111111 invoke [1]",
1229 "Program ComputeBudget111111111111111111111111111111 success",
1230 "Log truncated",
1231 ];
1232 let logs: Vec<String> = logs.iter().map(|&l| l.to_string()).collect();
1233
1234 let events = parse_logs_response::<MockEvent>(
1235 RpcResponse {
1236 context: RpcResponseContext::new(0),
1237 value: RpcLogsResponse {
1238 signature: "".to_string(),
1239 err: None,
1240 logs,
1241 },
1242 },
1243 "term9YPb9mzAsABaqN71A4xdbxHmpBNZavpBiQKZzN3",
1244 )
1245 .unwrap();
1246
1247 assert!(events.is_empty());
1248
1249 Ok(())
1250 }
1251
1252 #[test]
1256 fn test_parse_logs_response_event_after_trailing_log() -> Result<()> {
1257 use {
1258 anchor_lang::__private::base64,
1259 base64::{engine::general_purpose::STANDARD, Engine},
1260 };
1261
1262 let program_data_log = format!("Program data: {}", STANDARD.encode(MockEvent {}.data()));
1263
1264 let logs = vec![
1265 "Program ComputeBudget111111111111111111111111111111 invoke [1]".to_string(),
1266 "Program ComputeBudget111111111111111111111111111111 success".to_string(),
1267 "Log truncated".to_string(),
1269 "Program term9YPb9mzAsABaqN71A4xdbxHmpBNZavpBiQKZzN3 invoke [1]".to_string(),
1270 program_data_log,
1271 "Program term9YPb9mzAsABaqN71A4xdbxHmpBNZavpBiQKZzN3 success".to_string(),
1272 ];
1273
1274 let events = parse_logs_response::<MockEvent>(
1275 RpcResponse {
1276 context: RpcResponseContext::new(0),
1277 value: RpcLogsResponse {
1278 signature: "".to_string(),
1279 err: None,
1280 logs,
1281 },
1282 },
1283 "term9YPb9mzAsABaqN71A4xdbxHmpBNZavpBiQKZzN3",
1284 )
1285 .unwrap();
1286
1287 assert_eq!(events.len(), 1);
1288
1289 Ok(())
1290 }
1291
1292 #[test]
1293 fn test_parse_log_response_inner_events() -> Result<()> {
1294 use {
1295 anchor_lang::__private::base64,
1296 base64::{engine::general_purpose::STANDARD, Engine},
1297 };
1298
1299 let mock_event = MockEvent {};
1300 let program_data_log = format!("Program data: {}", STANDARD.encode(mock_event.data()));
1301
1302 let logs = vec![
1303 "Program ComputeBudget111111111111111111111111111111 invoke [1]",
1304 "Program ComputeBudget111111111111111111111111111111 success",
1305 "Program ComputeBudget111111111111111111111111111111 invoke [1]",
1306 "Program ComputeBudget111111111111111111111111111111 success",
1307 "Program term9YPb9mzAsABaqN71A4xdbxHmpBNZavpBiQKZzN3 invoke [1]",
1308 "Program log: Instruction: ValidateNonce",
1309 "Program term9YPb9mzAsABaqN71A4xdbxHmpBNZavpBiQKZzN3 consumed 4839 of 239700 compute \
1310 units",
1311 "Program term9YPb9mzAsABaqN71A4xdbxHmpBNZavpBiQKZzN3 success",
1312 "Program term9YPb9mzAsABaqN71A4xdbxHmpBNZavpBiQKZzN3 invoke [1]",
1313 "Program log: Instruction: SellExactInPumpFunV3",
1314 "Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P invoke [2]",
1315 "Program log: Instruction: Sell",
1316 "Program pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ invoke [3]",
1317 "Program log: Instruction: GetFees",
1318 "Program pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ consumed 3136 of 187774 compute \
1319 units",
1320 "Program return: pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ \
1321 AAAAAAAAAABfAAAAAAAAAB4AAAAAAAAA",
1322 "Program pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ success",
1323 "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [3]",
1324 "Program log: Instruction: TransferChecked",
1325 "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 2475 of 180928 compute \
1326 units",
1327 "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success",
1328 &program_data_log,
1329 "Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P invoke [3]",
1330 "Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P consumed 2060 of 166037 compute \
1331 units",
1332 "Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P success",
1333 "Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P consumed 60634 of 223605 compute \
1334 units",
1335 "Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P success",
1336 "Program term9YPb9mzAsABaqN71A4xdbxHmpBNZavpBiQKZzN3 consumed 72662 of 234861 compute \
1337 units",
1338 "Program term9YPb9mzAsABaqN71A4xdbxHmpBNZavpBiQKZzN3 success",
1339 "Program 11111111111111111111111111111111 invoke [1]",
1340 "Program 11111111111111111111111111111111 success",
1341 "Program 11111111111111111111111111111111 invoke [1]",
1342 "Program 11111111111111111111111111111111 success",
1343 ];
1344
1345 let logs: Vec<String> = logs.iter().map(|&l| l.to_string()).collect();
1347
1348 let program_id_str = "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P";
1349
1350 let events = parse_logs_response::<MockEvent>(
1351 RpcResponse {
1352 context: RpcResponseContext::new(0),
1353 value: RpcLogsResponse {
1354 signature: "".to_string(),
1355 err: None,
1356 logs: logs.to_vec(),
1357 },
1358 },
1359 program_id_str,
1360 )
1361 .unwrap();
1362
1363 assert_eq!(events.len(), 1);
1364
1365 Ok(())
1366 }
1367
1368 #[test]
1370 fn multiple_listeners_no_deadlock() {
1371 let rt = tokio::runtime::Builder::new_multi_thread()
1374 .enable_all()
1375 .build()
1376 .unwrap();
1377
1378 let (addr_tx, addr_rx) = std::sync::mpsc::channel();
1379
1380 rt.spawn(async move {
1381 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1382 let addr = listener.local_addr().unwrap();
1383 addr_tx.send(addr).unwrap();
1384
1385 static SUB_ID: AtomicU64 = AtomicU64::new(0);
1386
1387 loop {
1388 let (stream, _) = listener.accept().await.unwrap();
1389 tokio::spawn(async move {
1390 let mut ws = tokio_tungstenite::accept_async(stream).await.unwrap();
1391 while let Some(Ok(Message::Text(_))) = ws.next().await {
1392 let sub_id = SUB_ID.fetch_add(1, Ordering::Relaxed);
1393 let resp =
1395 format!(r#"{{"jsonrpc":"2.0","result":{sub_id},"id":{sub_id}}}"#);
1396 ws.send(Message::Text(resp.into())).await.unwrap();
1397 }
1398 });
1399 }
1400 });
1401
1402 let addr = addr_rx.recv().unwrap();
1403 let ws_url = format!("ws://{}", addr);
1404
1405 let client = super::Client::new(
1406 super::Cluster::Custom(ws_url.clone(), ws_url),
1407 std::sync::Arc::new(solana_keypair::Keypair::new()),
1408 );
1409 let program = client.program(Pubkey::new_unique()).unwrap();
1410
1411 let (done_tx, done_rx) = std::sync::mpsc::channel();
1414 let handle = std::thread::spawn(move || {
1415 #[cfg(not(feature = "async"))]
1416 {
1417 let _listener1 = program
1418 .on::<MockEvent>(|_ctx, _event| {})
1419 .expect("first listener");
1420
1421 let _listener2 = program
1422 .on::<MockEvent>(|_ctx, _event| {})
1423 .expect("second listener");
1424 }
1425
1426 #[cfg(feature = "async")]
1427 {
1428 let rt = tokio::runtime::Builder::new_current_thread()
1429 .enable_all()
1430 .build()
1431 .unwrap();
1432 rt.block_on(async {
1433 let _listener1 = program
1434 .on::<MockEvent>(|_ctx, _event| {})
1435 .await
1436 .expect("first listener");
1437
1438 let _listener2 = program
1439 .on::<MockEvent>(|_ctx, _event| {})
1440 .await
1441 .expect("second listener");
1442 });
1443 }
1444
1445 let _ = done_tx.send(());
1446 });
1447
1448 done_rx
1450 .recv_timeout(std::time::Duration::from_secs(5))
1451 .expect("registering two listeners should not deadlock");
1452
1453 handle.join().unwrap();
1454 }
1455}