Skip to main content

anchor_client/
lib.rs

1#![cfg_attr(docsrs, feature(doc_cfg))]
2
3//! An RPC client to interact with Solana programs written in [`anchor_lang`].
4//!
5//! # Examples
6//!
7//! A simple example that creates a client, sends a transaction and fetches an account:
8//!
9//! ```ignore
10//! use std::rc::Rc;
11//!
12//! use anchor_client::{Client, Cluster, Signer};
13//! use my_program::{accounts, instruction, MyAccount};
14//! use solana_keypair::{read_keypair_file, Keypair};
15//! use solana_system_interface::program as system_program;
16//!
17//! fn main() -> Result<(), Box<dyn std::error::Error>> {
18//!     // Create client
19//!     let payer = read_keypair_file("keypair.json")?;
20//!     let client = Client::new(Cluster::Localnet, Rc::new(payer));
21//!
22//!     // Create program
23//!     let program = client.program(my_program::ID)?;
24//!
25//!     // Send transaction
26//!     let my_account_kp = Keypair::new();
27//!     program
28//!         .request()
29//!         .accounts(accounts::Initialize {
30//!             my_account: my_account_kp.pubkey(),
31//!             payer: program.payer(),
32//!             system_program: system_program::ID,
33//!         })
34//!         .args(instruction::Initialize { field: 42 })
35//!         .signer(&my_account_kp)
36//!         .send()?;
37//!
38//!     // Fetch account
39//!     let my_account: MyAccount = program.account(my_account_kp.pubkey())?;
40//!     assert_eq!(my_account.field, 42);
41//!
42//!     Ok(())
43//! }
44//! ```
45//!
46//! More examples can be found in [here].
47//!
48//! [here]: https://github.com/otter-sec/anchor/tree/v1.2.0/client/example/src
49//!
50//! # Features
51//!
52//! ## `async`
53//!
54//! The client is blocking by default. To enable asynchronous client, add `async` feature:
55//!
56//! ```toml
57//! anchor-client = { version = "1.2.0", features = ["async"] }
58//! ````
59//!
60//! ## `mock`
61//!
62//! This feature allows passing in a custom RPC client when creating program instances, which is
63//! useful for mocking RPC responses, e.g. via [`RpcClient::new_mock`].
64//!
65//! [`RpcClient::new_mock`]: https://docs.rs/solana-rpc-client/3.0.0/solana_rpc_client/rpc_client/struct.RpcClient.html#method.new_mock
66
67#[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/// Specifies which transaction version to use when building transactions.
129#[derive(Debug, Clone, Default)]
130pub enum TxVersion<'a> {
131    /// Legacy transaction format.
132    #[default]
133    Legacy,
134    /// Versioned transaction format (v0) with optional address lookup tables.
135    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>;
147/// Client defines the base configuration for building RPC clients to
148/// communicate with Anchor programs running on a Solana cluster. It's
149/// primary use is to build a `Program` client via the `program` method.
150pub 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
195/// Auxiliary data structure to align the types of the Solana CLI utils with Anchor client.
196/// Client<C> implementation requires <C: Clone + Deref<Target = impl Signer>> which does not comply with Box<dyn Signer>
197/// that's used when loaded Signer from keypair file. This struct is used to wrap the usage.
198pub 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// Internal configuration for a client.
223#[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
248/// Program is the primary client handle to be used to build and send requests.
249pub 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
389/// Iterator with items of type (Pubkey, T). Used to lazily deserialize account structs.
390/// Wrapper type hides the inner type from usages so the implementation can be changed.
391pub struct ProgramAccountsIterator<T> {
392    inner: Map<IntoIter<(Pubkey, UiAccount)>, AccountConverterFunction<T>>,
393}
394
395/// Function type that accepts solana accounts and returns deserialized anchor accounts
396type 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    // Log emitted from the current program.
416    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    // System log.
440    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            // `Invoke [1]` instructions are pushed to the stack in `parse_logs_response`,
455            // so this ensures we only push CPIs to the stack at this stage
456        } else if invoke_match.get(2).unwrap().as_str() != "1" {
457            return (Some("cpi".to_string()), false); // Any string will do.
458        }
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    /// The program currently on top of the stack.
499    ///
500    /// # Panics
501    ///
502    /// Panics if the stack is empty. Prefer [`Execution::try_program`], which
503    /// returns `None` instead; the stack legitimately empties whenever a
504    /// top-level instruction returns, and more logs can still follow it.
505    pub fn program(&self) -> String {
506        assert!(!self.stack.is_empty());
507        self.stack[self.stack.len() - 1].clone()
508    }
509
510    /// The program currently on top of the stack, or `None` when no
511    /// instruction is in scope.
512    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    /// Pops the innermost program off the stack. A no-op when the stack is
521    /// already empty, which happens on a `Program <id> success` line that the
522    /// runtime emits without a matching tracked `invoke`.
523    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    /// Adding a new variant to [`ClientError`] is a breaking change in v1. To mitigate this issue,
556    /// use this helper method for all errors that cannot be precisely described by [`ClientError`].
557    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
575/// `RequestBuilder` provides a builder interface to create and send
576/// transactions to a cluster.
577pub 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
592// Shared implementation for all RequestBuilders
593impl<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    /// Set the accounts to pass to the instruction.
619    ///
620    /// `accounts` argument can be:
621    ///
622    /// - Any type that implements [`ToAccountMetas`] trait
623    /// - A vector of [`AccountMeta`]s (for remaining accounts)
624    ///
625    /// Note that the given accounts are appended to the previous list of accounts instead of
626    /// overriding the existing ones (if any).
627    ///
628    /// # Example
629    ///
630    /// ```ignore
631    /// program
632    ///     .request()
633    ///     // Regular accounts
634    ///     .accounts(accounts::Initialize {
635    ///         my_account: my_account_kp.pubkey(),
636    ///         payer: program.payer(),
637    ///         system_program: system_program::ID,
638    ///     })
639    ///     // Remaining accounts
640    ///     .accounts(vec![AccountMeta {
641    ///         pubkey: remaining,
642    ///         is_signer: true,
643    ///         is_writable: true,
644    ///     }])
645    ///     .args(instruction::Initialize { field: 42 })
646    ///     .send()?;
647    /// ```
648    #[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    /// Build the request into a transaction.
681    ///
682    /// Note: This will build a transaction with the legacy transaction format. If you'd like to use
683    /// a different transaction format, use [`transaction_versioned`].
684    pub fn transaction(&self) -> Transaction {
685        let instructions = &self.instructions();
686        Transaction::new_with_payer(instructions, Some(&self.payer.pubkey()))
687    }
688
689    /// Build an unsigned transaction.
690    ///
691    /// # Arguments
692    ///
693    /// * `version` - The transaction version to use ([`TxVersion::Legacy`] or [`TxVersion::V0`]).
694    /// * `recent_blockhash` - A recent blockhash to include in the transaction message.
695    ///
696    /// # Example
697    ///
698    /// ```no_run
699    /// use anchor_client::{Client, Cluster, TxVersion};
700    /// use anchor_lang::prelude::Pubkey;
701    /// use solana_signer::null_signer::NullSigner;
702    /// use solana_message::AddressLookupTableAccount;
703    /// use solana_message::Hash;
704    ///
705    /// let payer = NullSigner::new(&Pubkey::default());
706    /// let client = Client::new(Cluster::Localnet, std::rc::Rc::new(payer));
707    ///
708    /// let program = client.program(Pubkey::default()).unwrap();
709    /// // Dummy blockhash
710    /// let blockhash = Hash::from([0; 32]);
711    /// let lookup_table = AddressLookupTableAccount { key: Pubkey::default(), addresses: vec![] };
712    ///
713    /// let request = program.request();
714    /// // Legacy transaction
715    /// let tx = request.transaction_versioned(TxVersion::Legacy, blockhash).unwrap();
716    ///
717    /// // V0 transaction with address lookup tables
718    /// let tx = request.transaction_versioned(TxVersion::V0(&[lookup_table]), blockhash).unwrap();
719    ///
720    /// // V0 transaction without lookup tables
721    /// let tx = request.transaction_versioned(TxVersion::V0(&[]), blockhash).unwrap();
722    //// ```
723    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        // FIXME: Inline a no-spinner version of `RpcClient::send_and_confirm_transaction`
827        // that honors the configured commitment level (`self.options`). The built-in
828        // non-spinner methods ignore the commitment, and the only commitment-aware
829        // confirmation helper (`send_and_confirm_transaction_with_spinner_and_commitment`)
830        // forces a spinner onto callers. Replace this with the non-spinner,
831        // commitment-aware method once we upgrade to Solana 4.0, which adds it.
832        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                        // Block hash is not found by some reason
855                        break;
856                    } else if cfg!(not(test)) {
857                        // Retry twice a second
858                        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            // Create a new peekable iterator so that we can peek at the next log whilst iterating
902            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                // No instruction is in scope. This is reached whenever a
909                // top-level instruction has returned but the log stream has
910                // not ended -- most commonly the runtime's trailing
911                // `"Log truncated"` marker, which is appended after the final
912                // `success` when a transaction overruns the log buffer.
913                //
914                // Only a new top-level `invoke [1]` can re-enter a program
915                // context; anything else carries no events, so skip it rather
916                // than panicking in `Execution::program`.
917                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                // Parse the log.
927                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                // Emit the event.
936                if let Some(e) = event {
937                    events.push(e);
938                }
939                // Switch program context on CPI.
940                if let Some(new_program) = new_program {
941                    execution.push(new_program);
942                }
943                // Program returned.
944                if did_pop {
945                    execution.pop();
946
947                    // If the current iteration popped then it means there was a
948                    //`Program x success` log. If the next log in the iteration is
949                    // of depth [1] then we're not within a CPI and this is a new instruction.
950                    //
951                    // We need to ensure that the `Execution` instance is updated with
952                    // the next program ID, or else `execution.program()` will cause
953                    // a panic during the next iteration.
954                    //
955                    // Use the full regex match to gate this branch. A loose
956                    // `ends_with("invoke [1]")` check would also accept program-emitted
957                    // log lines that happen to end in that suffix (e.g.
958                    // `"Program log: ...invoke [1]"`), which then fail the strict
959                    // `^Program <pubkey> invoke [N]$` regex and panic on unwrap.
960                    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    // Creating a mock struct that implements `anchor_lang::events`
977    // for type inference in `test_logs`
978    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        // Mock logs received within an `RpcResponse`. These are based on a Jupiter transaction.
1020        let logs = vec![
1021            "Program VeryCoolProgram invoke [1]", // Outer instruction #1 starts
1022            "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", // Outer instruction #1 ends
1030            "Program EvenCoolerProgram invoke [1]", // Outer instruction #2 starts
1031            "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", // Outer instruction #2 ends
1105            "Program ComputeBudget111111111111111111111111111111 invoke [1]",
1106            "Program ComputeBudget111111111111111111111111111111 success",
1107            "Program ComputeBudget111111111111111111111111111111 invoke [1]",
1108            "Program ComputeBudget111111111111111111111111111111 success",
1109        ];
1110
1111        // Converting to Vec<String> as expected in `RpcLogsResponse`
1112        let logs: Vec<String> = logs.iter().map(|&l| l.to_string()).collect();
1113
1114        let program_id_str = "VeryCoolProgram";
1115
1116        // No events returned here. Just ensuring that the function doesn't panic
1117        // due an incorrectly emptied stack.
1118        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        // Converting to Vec<String> as expected in `RpcLogsResponse`
1146        let logs: Vec<String> = logs.iter().map(|&l| l.to_string()).collect();
1147
1148        let program_id_str = "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb";
1149
1150        // No events returned here. Just ensuring that the function doesn't panic
1151        // due an incorrectly emptied stack.
1152        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    /// Regression for #4461: a program-emitted `Program log:` line that ends
1169    /// with the literal `"invoke [1]"` (e.g. log content that happens to
1170    /// describe a CPI) used to satisfy the `ends_with` gate but fail the
1171    /// strict `^Program <pubkey> invoke [N]$` regex, panicking on
1172    /// `.captures(...).unwrap()` whenever it appeared right after a CPI pop.
1173    #[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-emitted log that happens to end with "invoke [1]"
1180            // immediately after a CPI returns. Pre-fix this would panic.
1181            "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        // A second pop with nothing left used to trip `assert!(!self.stack.is_empty())`.
1216        exe.pop();
1217        assert_eq!(exe.try_program(), None);
1218    }
1219
1220    /// Regression for #1941: the runtime appends a bare `"Log truncated"` line
1221    /// after the final `success` when a transaction overruns the log buffer.
1222    /// That line arrives with an empty stack, and `Execution::program` used to
1223    /// panic on it -- taking down the whole `logs_subscribe` thread rather than
1224    /// returning an error.
1225    #[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    /// The empty-stack guard must skip only the logs that carry no events -- a
1253    /// later top-level `invoke [1]` still has to re-enter the program context,
1254    /// or the fix would trade a panic for silently dropped events.
1255    #[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            // Empty stack from here until the next top-level invoke.
1268            "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        // Converting to Vec<String> as expected in `RpcLogsResponse`
1346        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    /// Regression test that registering multiple event listeners does not deadlock.
1369    #[test]
1370    fn multiple_listeners_no_deadlock() {
1371        // Spin up a tiny mock websocket server that responds to `logsSubscribe`
1372        // JSON-RPC requests with a valid subscription id.
1373        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                        // The PubsubClient sends sequential integer ids starting at 0.
1394                        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        // With the old RwLock-based code, the second call would deadlock.
1412        // Use a timeout to ensure the test fails instead of hanging forever.
1413        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        // If this times out, the deadlock is still present.
1449        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}