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