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.2/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.2", 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
64//! [`RpcClient::new_mock`](solana_rpc_client::nonblocking::rpc_client::RpcClient::new_mock).
65
66#[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/// Specifies which transaction version to use when building transactions.
133#[derive(Debug, Clone, Default)]
134pub enum TxVersion<'a> {
135    /// Legacy transaction format.
136    #[default]
137    Legacy,
138    /// Versioned transaction format (v0) with optional address lookup tables.
139    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>;
151/// Client defines the base configuration for building RPC clients to
152/// communicate with Anchor programs running on a Solana cluster. It's
153/// primary use is to build a `Program` client via the `program` method.
154pub 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
199/// Auxiliary data structure to align the types of the Solana CLI utils with Anchor client.
200/// Client<C> implementation requires <C: Clone + Deref<Target = impl Signer>> which does not comply with Box<dyn Signer>
201/// that's used when loaded Signer from keypair file. This struct is used to wrap the usage.
202pub 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// Internal configuration for a client.
227#[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
252/// Program is the primary client handle to be used to build and send requests.
253pub 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
397/// Iterator with items of type (Pubkey, T). Used to lazily deserialize account structs.
398/// Wrapper type hides the inner type from usages so the implementation can be changed.
399pub struct ProgramAccountsIterator<T> {
400    inner: Map<IntoIter<(Pubkey, UiAccount)>, AccountConverterFunction<T>>,
401}
402
403/// Function type that accepts solana accounts and returns deserialized anchor accounts
404type 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    // Log emitted from the current program.
424    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    // System log.
448    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            // `Invoke [1]` instructions are pushed to the stack in `parse_logs`,
463            // so this ensures we only push CPIs to the stack at this stage
464        } else if invoke_match.get(2).unwrap().as_str() != "1" {
465            return (Some("cpi".to_string()), false); // Any string will do.
466        }
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    /// The program currently on top of the stack.
507    ///
508    /// # Panics
509    ///
510    /// Panics if the stack is empty. Prefer [`Execution::try_program`], which
511    /// returns `None` instead; the stack legitimately empties whenever a
512    /// top-level instruction returns, and more logs can still follow it.
513    pub fn program(&self) -> String {
514        assert!(!self.stack.is_empty());
515        self.stack[self.stack.len() - 1].clone()
516    }
517
518    /// The program currently on top of the stack, or `None` when no
519    /// instruction is in scope.
520    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    /// Pops the innermost program off the stack. A no-op when the stack is
529    /// already empty, which happens on a `Program <id> success` line that the
530    /// runtime emits without a matching tracked `invoke`.
531    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    /// Adding a new variant to [`ClientError`] is a breaking change in v1. To mitigate this issue,
564    /// use this helper method for all errors that cannot be precisely described by [`ClientError`].
565    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
583/// `RequestBuilder` provides a builder interface to create and send
584/// transactions to a cluster.
585pub 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
600// Shared implementation for all RequestBuilders
601impl<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    /// Set the accounts to pass to the instruction.
627    ///
628    /// `accounts` argument can be:
629    ///
630    /// - Any type that implements [`ToAccountMetas`] trait
631    /// - A vector of [`AccountMeta`]s (for remaining accounts)
632    ///
633    /// Note that the given accounts are appended to the previous list of accounts instead of
634    /// overriding the existing ones (if any).
635    ///
636    /// # Example
637    ///
638    /// ```ignore
639    /// program
640    ///     .request()
641    ///     // Regular accounts
642    ///     .accounts(accounts::Initialize {
643    ///         my_account: my_account_kp.pubkey(),
644    ///         payer: program.payer(),
645    ///         system_program: system_program::ID,
646    ///     })
647    ///     // Remaining accounts
648    ///     .accounts(vec![AccountMeta {
649    ///         pubkey: remaining,
650    ///         is_signer: true,
651    ///         is_writable: true,
652    ///     }])
653    ///     .args(instruction::Initialize { field: 42 })
654    ///     .send()?;
655    /// ```
656    #[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    /// Build the request into a transaction.
689    ///
690    /// Note: This will build a transaction with the legacy transaction format. If you'd like to use
691    /// a different transaction format, use [`transaction_versioned`].
692    pub fn transaction(&self) -> Transaction {
693        let instructions = &self.instructions();
694        Transaction::new_with_payer(instructions, Some(&self.payer.pubkey()))
695    }
696
697    /// Build an unsigned transaction.
698    ///
699    /// # Arguments
700    ///
701    /// * `version` - The transaction version to use ([`TxVersion::Legacy`] or [`TxVersion::V0`]).
702    /// * `recent_blockhash` - A recent blockhash to include in the transaction message.
703    ///
704    /// # Example
705    ///
706    /// ```no_run
707    /// use anchor_client::{Client, Cluster, TxVersion};
708    /// use anchor_lang::prelude::Pubkey;
709    /// use solana_signer::null_signer::NullSigner;
710    /// use anchor_client::AddressLookupTableAccount;
711    /// use anchor_client::Hash;
712    ///
713    /// let payer = NullSigner::new(&Pubkey::default());
714    /// let client = Client::new(Cluster::Localnet, std::rc::Rc::new(payer));
715    ///
716    /// let program = client.program(Pubkey::default()).unwrap();
717    /// // Dummy blockhash
718    /// let blockhash = Hash::from([0; 32]);
719    /// let lookup_table = AddressLookupTableAccount { key: Pubkey::default(), addresses: vec![] };
720    ///
721    /// let request = program.request();
722    /// // Legacy transaction
723    /// let tx = request.transaction_versioned(TxVersion::Legacy, blockhash).unwrap();
724    ///
725    /// // V0 transaction with address lookup tables
726    /// let tx = request.transaction_versioned(TxVersion::V0(&[lookup_table]), blockhash).unwrap();
727    ///
728    /// // V0 transaction without lookup tables
729    /// let tx = request.transaction_versioned(TxVersion::V0(&[]), blockhash).unwrap();
730    //// ```
731    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        // FIXME: Inline a no-spinner version of `RpcClient::send_and_confirm_transaction`
835        // that honors the configured commitment level (`self.options`). The built-in
836        // non-spinner methods ignore the commitment, and the only commitment-aware
837        // confirmation helper (`send_and_confirm_transaction_with_spinner_and_commitment`)
838        // forces a spinner onto callers. Replace this with the non-spinner,
839        // commitment-aware method once we upgrade to Solana 4.0, which adds it.
840        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                        // Block hash is not found by some reason
863                        break;
864                    } else if cfg!(not(test)) {
865                        // Retry twice a second
866                        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            // Create a new peekable iterator so that we can peek at the next log whilst iterating
910            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                // No instruction is in scope. This is reached whenever a
917                // top-level instruction has returned but the log stream has
918                // not ended -- most commonly the runtime's trailing
919                // `"Log truncated"` marker, which is appended after the final
920                // `success` when a transaction overruns the log buffer.
921                //
922                // Only a new top-level `invoke [1]` can re-enter a program
923                // context; anything else carries no events, so skip it rather
924                // than panicking in `Execution::program`.
925                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                // Parse the log.
935                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                // Emit the event.
944                if let Some(e) = event {
945                    events.push(e);
946                }
947                // Switch program context on CPI.
948                if let Some(new_program) = new_program {
949                    execution.push(new_program);
950                }
951                // Program returned.
952                if did_pop {
953                    execution.pop();
954
955                    // If the current iteration popped then it means there was a
956                    //`Program x success` log. If the next log in the iteration is
957                    // of depth [1] then we're not within a CPI and this is a new instruction.
958                    //
959                    // We need to ensure that the `Execution` instance is updated with
960                    // the next program ID, or else `execution.program()` will cause
961                    // a panic during the next iteration.
962                    //
963                    // Use the full regex match to gate this branch. A loose
964                    // `ends_with("invoke [1]")` check would also accept program-emitted
965                    // log lines that happen to end in that suffix (e.g.
966                    // `"Program log: ...invoke [1]"`), which then fail the strict
967                    // `^Program <pubkey> invoke [N]$` regex and panic on unwrap.
968                    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    // Creating a mock struct that implements `anchor_lang::events`
985    // for type inference in `test_logs`
986    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        // These logs are based on a Jupiter transaction.
1027        let logs = vec![
1028            "Program VeryCoolProgram invoke [1]", // Outer instruction #1 starts
1029            "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", // Outer instruction #1 ends
1037            "Program EvenCoolerProgram invoke [1]", // Outer instruction #2 starts
1038            "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", // Outer instruction #2 ends
1112            "Program ComputeBudget111111111111111111111111111111 invoke [1]",
1113            "Program ComputeBudget111111111111111111111111111111 success",
1114            "Program ComputeBudget111111111111111111111111111111 invoke [1]",
1115            "Program ComputeBudget111111111111111111111111111111 success",
1116        ];
1117
1118        // Convert to the owned strings received from RPC.
1119        let logs: Vec<String> = logs.iter().map(|&l| l.to_string()).collect();
1120
1121        let program_id_str = "VeryCoolProgram";
1122
1123        // No events returned here. Just ensuring that the function doesn't panic
1124        // due an incorrectly emptied stack.
1125        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            // Keep the websocket alive until the client or test runtime closes it.
1177            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        // Convert to the owned strings received from RPC.
1218        let logs: Vec<String> = logs.iter().map(|&l| l.to_string()).collect();
1219
1220        let program_id_str = "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb";
1221
1222        // No events returned here. Just ensuring that the function doesn't panic
1223        // due an incorrectly emptied stack.
1224        parse_logs::<MockEvent>(&logs, program_id_str).unwrap();
1225
1226        Ok(())
1227    }
1228
1229    /// Regression for #4461: a program-emitted `Program log:` line that ends
1230    /// with the literal `"invoke [1]"` (e.g. log content that happens to
1231    /// describe a CPI) used to satisfy the `ends_with` gate but fail the
1232    /// strict `^Program <pubkey> invoke [N]$` regex, panicking on
1233    /// `.captures(...).unwrap()` whenever it appeared right after a CPI pop.
1234    #[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-emitted log that happens to end with "invoke [1]"
1241            // immediately after a CPI returns. Pre-fix this would panic.
1242            "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        // A second pop with nothing left used to trip `assert!(!self.stack.is_empty())`.
1266        exe.pop();
1267        assert_eq!(exe.try_program(), None);
1268    }
1269
1270    /// Regression for #1941: the runtime appends a bare `"Log truncated"` line
1271    /// after the final `success` when a transaction overruns the log buffer.
1272    /// That line arrives with an empty stack, and `Execution::program` used to
1273    /// panic on it -- taking down the whole `logs_subscribe` thread rather than
1274    /// returning an error.
1275    #[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    /// The empty-stack guard must skip only the logs that carry no events -- a
1293    /// later top-level `invoke [1]` still has to re-enter the program context,
1294    /// or the fix would trade a panic for silently dropped events.
1295    #[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            // Empty stack from here until the next top-level invoke.
1308            "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        // Convert to the owned strings received from RPC.
1376        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    /// Regression test that registering multiple event listeners does not deadlock.
1388    #[test]
1389    fn multiple_listeners_no_deadlock() {
1390        // Spin up a tiny mock websocket server that responds to `logsSubscribe`
1391        // JSON-RPC requests with a valid subscription id.
1392        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                        // The PubsubClient sends sequential integer ids starting at 0.
1413                        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        // With the old RwLock-based code, the second call would deadlock.
1431        // Use a timeout to ensure the test fails instead of hanging forever.
1432        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        // If this times out, the deadlock is still present.
1468        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}