Browse Source

client: Add option to pass in mock rpc client (#3053)

cryptopapi997 1 year ago
parent
commit
f8d0f5208a
6 changed files with 100 additions and 48 deletions
  1. 5 3
      CHANGELOG.md
  2. 1 0
      client/Cargo.toml
  3. 3 3
      client/example/src/nonblocking.rs
  4. 35 2
      client/src/blocking.rs
  5. 26 39
      client/src/lib.rs
  6. 30 1
      client/src/nonblocking.rs

+ 5 - 3
CHANGELOG.md

@@ -17,6 +17,7 @@ The minor version will be incremented upon a breaking change and the patch versi
 - cli: Add checks for incorrect usage of `idl-build` feature ([#3061](https://github.com/coral-xyz/anchor/pull/3061)).
 - lang: Export `Discriminator` trait from `prelude` ([#3075](https://github.com/coral-xyz/anchor/pull/3075)).
 - lang: Add `Account` utility type to get accounts from bytes ([#3091](https://github.com/coral-xyz/anchor/pull/3091)).
+- client: Add option to pass in mock rpc client when using anchor_client ([#3053](https://github.com/coral-xyz/anchor/pull/3053)).
 
 ### Fixes
 
@@ -33,14 +34,15 @@ The minor version will be incremented upon a breaking change and the patch versi
 ### Breaking
 
 - syn: Remove `bpf` target support in `hash` feature ([#3078](https://github.com/coral-xyz/anchor/pull/3078)).
-- client: Add `tokio` support to `RequestBuilder` with `async` feature ([#3057](https://github.com/coral-xyz/anchor/pull/3057])).
-- lang: Remove `EventData` trait ([#3083](https://github.com/coral-xyz/anchor/pull/3083])).
+- client: Add `tokio` support to `RequestBuilder` with `async` feature ([#3057](https://github.com/coral-xyz/anchor/pull/3057)).
+- lang: Remove `EventData` trait ([#3083](https://github.com/coral-xyz/anchor/pull/3083)).
+- client: Remove `async_rpc` method ([#3053](https://github.com/coral-xyz/anchor/pull/3053)).
 
 ## [0.30.1] - 2024-06-20
 
 ### Features
 
-- idl: Allow overriding the idl build toolchain with the `RUSTUP_TOOLCHAIN` environment variable ([#2941](https://github.com/coral-xyz/anchor/pull/2941])).
+- idl: Allow overriding the idl build toolchain with the `RUSTUP_TOOLCHAIN` environment variable ([#2941](https://github.com/coral-xyz/anchor/pull/2941)).
 - avm: Support customizing the installation location using `AVM_HOME` environment variable ([#2917](https://github.com/coral-xyz/anchor/pull/2917)).
 - avm: Optimize `avm list` when GitHub API rate limits are reached ([#2962](https://github.com/coral-xyz/anchor/pull/2962))
 - idl, ts: Add accounts resolution for associated token accounts ([#2927](https://github.com/coral-xyz/anchor/pull/2927)).

+ 1 - 0
client/Cargo.toml

@@ -13,6 +13,7 @@ rustdoc-args = ["--cfg", "docsrs"]
 [features]
 async = []
 debug = []
+mock = []
 
 [dependencies]
 anchor-lang = { path = "../lang", version = "0.30.1" }

+ 3 - 3
client/example/src/nonblocking.rs

@@ -116,7 +116,7 @@ pub async fn composite<C: Deref<Target = impl Signer> + Clone>(
             &program.payer(),
             &dummy_a.pubkey(),
             program
-                .async_rpc()
+                .rpc()
                 .get_minimum_balance_for_rent_exemption(500)
                 .await?,
             500,
@@ -126,7 +126,7 @@ pub async fn composite<C: Deref<Target = impl Signer> + Clone>(
             &program.payer(),
             &dummy_b.pubkey(),
             program
-                .async_rpc()
+                .rpc()
                 .get_minimum_balance_for_rent_exemption(500)
                 .await?,
             500,
@@ -307,7 +307,7 @@ pub async fn optional<C: Deref<Target = impl Signer> + Clone>(
             &program.payer(),
             &required_keypair.pubkey(),
             program
-                .async_rpc()
+                .rpc()
                 .get_minimum_balance_for_rent_exemption(DataAccount::LEN)
                 .await?,
             DataAccount::LEN as u64,

+ 35 - 2
client/src/blocking.rs

@@ -3,7 +3,12 @@ use crate::{
     RequestBuilder,
 };
 use anchor_lang::{prelude::Pubkey, AccountDeserialize, Discriminator};
-use solana_client::{rpc_config::RpcSendTransactionConfig, rpc_filter::RpcFilterType};
+#[cfg(not(feature = "mock"))]
+use solana_client::rpc_client::RpcClient;
+use solana_client::{
+    nonblocking::rpc_client::RpcClient as AsyncRpcClient, rpc_config::RpcSendTransactionConfig,
+    rpc_filter::RpcFilterType,
+};
 use solana_sdk::{
     commitment_config::CommitmentConfig, signature::Signature, signer::Signer,
     transaction::Transaction,
@@ -22,17 +27,42 @@ impl<'a> EventUnsubscriber<'a> {
 }
 
 impl<C: Deref<Target = impl Signer> + Clone> Program<C> {
-    pub fn new(program_id: Pubkey, cfg: Config<C>) -> Result<Self, ClientError> {
+    pub fn new(
+        program_id: Pubkey,
+        cfg: Config<C>,
+        #[cfg(feature = "mock")] rpc_client: AsyncRpcClient,
+    ) -> Result<Self, ClientError> {
         let rt: tokio::runtime::Runtime = Builder::new_multi_thread().enable_all().build()?;
 
+        #[cfg(not(feature = "mock"))]
+        let rpc_client = {
+            let comm_config = cfg.options.unwrap_or_default();
+            let cluster_url = cfg.cluster.url().to_string();
+            AsyncRpcClient::new_with_commitment(cluster_url.clone(), comm_config)
+        };
+
         Ok(Self {
             program_id,
             cfg,
             sub_client: Arc::new(RwLock::new(None)),
+            internal_rpc_client: rpc_client,
             rt,
         })
     }
 
+    // We disable the `rpc` method for `mock` feature because otherwise we'd either have to
+    // return a new `RpcClient` instance (which is different to the one used internally)
+    // or require the user to pass another one in for blocking (since we use the non-blocking one under the hood).
+    // The former of these would be confusing and the latter would be very annoying, especially since a user
+    // using the mock feature likely already has a `RpcClient` instance at hand anyway.
+    #[cfg(not(feature = "mock"))]
+    pub fn rpc(&self) -> RpcClient {
+        RpcClient::new_with_commitment(
+            self.cfg.cluster.url().to_string(),
+            self.cfg.options.unwrap_or_default(),
+        )
+    }
+
     /// Returns a request builder.
     pub fn request(&self) -> RequestBuilder<'_, C, Box<dyn Signer + '_>> {
         RequestBuilder::from(
@@ -42,6 +72,7 @@ impl<C: Deref<Target = impl Signer> + Clone> Program<C> {
             self.cfg.options,
             #[cfg(not(feature = "async"))]
             self.rt.handle(),
+            &self.internal_rpc_client,
         )
     }
 
@@ -89,6 +120,7 @@ impl<'a, C: Deref<Target = impl Signer> + Clone> RequestBuilder<'a, C, Box<dyn S
         payer: C,
         options: Option<CommitmentConfig>,
         handle: &'a Handle,
+        rpc_client: &'a AsyncRpcClient,
     ) -> Self {
         Self {
             program_id,
@@ -100,6 +132,7 @@ impl<'a, C: Deref<Target = impl Signer> + Clone> RequestBuilder<'a, C, Box<dyn S
             instruction_data: None,
             signers: Vec::new(),
             handle,
+            internal_rpc_client: rpc_client,
             _phantom: PhantomData,
         }
     }

+ 26 - 39
client/src/lib.rs

@@ -66,6 +66,7 @@ use anchor_lang::{AccountDeserialize, Discriminator, InstructionData, ToAccountM
 use futures::{Future, StreamExt};
 use regex::Regex;
 use solana_account_decoder::UiAccountEncoding;
+use solana_client::nonblocking::rpc_client::RpcClient as AsyncRpcClient;
 use solana_client::rpc_config::{
     RpcAccountInfoConfig, RpcProgramAccountsConfig, RpcSendTransactionConfig,
     RpcTransactionLogsConfig, RpcTransactionLogsFilter,
@@ -73,11 +74,7 @@ use solana_client::rpc_config::{
 use solana_client::rpc_filter::{Memcmp, RpcFilterType};
 use solana_client::{
     client_error::ClientError as SolanaClientError,
-    nonblocking::{
-        pubsub_client::{PubsubClient, PubsubClientError},
-        rpc_client::RpcClient as AsyncRpcClient,
-    },
-    rpc_client::RpcClient,
+    nonblocking::pubsub_client::{PubsubClient, PubsubClientError},
     rpc_response::{Response as RpcResponse, RpcLogsResponse},
 };
 use solana_sdk::account::Account;
@@ -146,14 +143,23 @@ impl<C: Clone + Deref<Target = impl Signer>> Client<C> {
         }
     }
 
-    pub fn program(&self, program_id: Pubkey) -> Result<Program<C>, ClientError> {
+    pub fn program(
+        &self,
+        program_id: Pubkey,
+        #[cfg(feature = "mock")] rpc_client: AsyncRpcClient,
+    ) -> Result<Program<C>, ClientError> {
         let cfg = Config {
             cluster: self.cfg.cluster.clone(),
             options: self.cfg.options,
             payer: self.cfg.payer.clone(),
         };
 
-        Program::new(program_id, cfg)
+        Program::new(
+            program_id,
+            cfg,
+            #[cfg(feature = "mock")]
+            rpc_client,
+        )
     }
 }
 
@@ -220,6 +226,7 @@ pub struct Program<C> {
     sub_client: Arc<RwLock<Option<PubsubClient>>>,
     #[cfg(not(feature = "async"))]
     rt: tokio::runtime::Runtime,
+    internal_rpc_client: AsyncRpcClient,
 }
 
 impl<C: Deref<Target = impl Signer> + Clone> Program<C> {
@@ -231,29 +238,12 @@ impl<C: Deref<Target = impl Signer> + Clone> Program<C> {
         self.program_id
     }
 
-    pub fn rpc(&self) -> RpcClient {
-        RpcClient::new_with_commitment(
-            self.cfg.cluster.url().to_string(),
-            self.cfg.options.unwrap_or_default(),
-        )
-    }
-
-    pub fn async_rpc(&self) -> AsyncRpcClient {
-        AsyncRpcClient::new_with_commitment(
-            self.cfg.cluster.url().to_string(),
-            self.cfg.options.unwrap_or_default(),
-        )
-    }
-
     async fn account_internal<T: AccountDeserialize>(
         &self,
         address: Pubkey,
     ) -> Result<T, ClientError> {
-        let rpc_client = AsyncRpcClient::new_with_commitment(
-            self.cfg.cluster.url().to_string(),
-            self.cfg.options.unwrap_or_default(),
-        );
-        let account = rpc_client
+        let account = self
+            .internal_rpc_client
             .get_account_with_commitment(&address, CommitmentConfig::processed())
             .await?
             .value
@@ -276,9 +266,10 @@ impl<C: Deref<Target = impl Signer> + Clone> Program<C> {
             },
             ..RpcProgramAccountsConfig::default()
         };
+
         Ok(ProgramAccountsIterator {
             inner: self
-                .async_rpc()
+                .internal_rpc_client
                 .get_program_accounts_with_config(&self.id(), config)
                 .await?
                 .into_iter()
@@ -514,6 +505,7 @@ pub struct RequestBuilder<'a, C, S: 'a> {
     signers: Vec<S>,
     #[cfg(not(feature = "async"))]
     handle: &'a Handle,
+    internal_rpc_client: &'a AsyncRpcClient,
     _phantom: PhantomData<&'a ()>,
 }
 
@@ -631,21 +623,17 @@ impl<'a, C: Deref<Target = impl Signer> + Clone, S: AsSigner> RequestBuilder<'a,
     }
 
     async fn signed_transaction_internal(&self) -> Result<Transaction, ClientError> {
-        let latest_hash =
-            AsyncRpcClient::new_with_commitment(self.cluster.to_owned(), self.options)
-                .get_latest_blockhash()
-                .await?;
-        let tx = self.signed_transaction_with_blockhash(latest_hash)?;
+        let latest_hash = self.internal_rpc_client.get_latest_blockhash().await?;
 
+        let tx = self.signed_transaction_with_blockhash(latest_hash)?;
         Ok(tx)
     }
 
     async fn send_internal(&self) -> Result<Signature, ClientError> {
-        let rpc_client = AsyncRpcClient::new_with_commitment(self.cluster.to_owned(), self.options);
-        let latest_hash = rpc_client.get_latest_blockhash().await?;
+        let latest_hash = self.internal_rpc_client.get_latest_blockhash().await?;
         let tx = self.signed_transaction_with_blockhash(latest_hash)?;
 
-        rpc_client
+        self.internal_rpc_client
             .send_and_confirm_transaction(&tx)
             .await
             .map_err(Into::into)
@@ -655,14 +643,13 @@ impl<'a, C: Deref<Target = impl Signer> + Clone, S: AsSigner> RequestBuilder<'a,
         &self,
         config: RpcSendTransactionConfig,
     ) -> Result<Signature, ClientError> {
-        let rpc_client = AsyncRpcClient::new_with_commitment(self.cluster.to_owned(), self.options);
-        let latest_hash = rpc_client.get_latest_blockhash().await?;
+        let latest_hash = self.internal_rpc_client.get_latest_blockhash().await?;
         let tx = self.signed_transaction_with_blockhash(latest_hash)?;
 
-        rpc_client
+        self.internal_rpc_client
             .send_and_confirm_transaction_with_spinner_and_config(
                 &tx,
-                rpc_client.commitment(),
+                self.internal_rpc_client.commitment(),
                 config,
             )
             .await

+ 30 - 1
client/src/nonblocking.rs

@@ -3,6 +3,7 @@ use crate::{
     ProgramAccountsIterator, RequestBuilder,
 };
 use anchor_lang::{prelude::Pubkey, AccountDeserialize, Discriminator};
+use solana_client::nonblocking::rpc_client::RpcClient as AsyncRpcClient;
 use solana_client::{rpc_config::RpcSendTransactionConfig, rpc_filter::RpcFilterType};
 use solana_sdk::{
     commitment_config::CommitmentConfig, signature::Signature, signer::Signer,
@@ -35,14 +36,39 @@ impl AsSigner for Arc<dyn ThreadSafeSigner> {
 }
 
 impl<C: Deref<Target = impl Signer> + Clone> Program<C> {
-    pub fn new(program_id: Pubkey, cfg: Config<C>) -> Result<Self, ClientError> {
+    pub fn new(
+        program_id: Pubkey,
+        cfg: Config<C>,
+        #[cfg(feature = "mock")] rpc_client: AsyncRpcClient,
+    ) -> Result<Self, ClientError> {
+        #[cfg(not(feature = "mock"))]
+        let rpc_client = {
+            let comm_config = cfg.options.unwrap_or_default();
+            let cluster_url = cfg.cluster.url().to_string();
+            AsyncRpcClient::new_with_commitment(cluster_url.clone(), comm_config)
+        };
+
         Ok(Self {
             program_id,
             cfg,
             sub_client: Arc::new(RwLock::new(None)),
+            internal_rpc_client: rpc_client,
         })
     }
 
+    // We disable the `rpc` method for `mock` feature because otherwise we'd either have to
+    // return a new `RpcClient` instance (which is different to the one used internally)
+    // or require the user to pass another one in for blocking (since we use the non-blocking one under the hood).
+    // The former of these would be confusing and the latter would be very annoying, especially since a user
+    // using the mock feature likely already has a `RpcClient` instance at hand anyway.
+    #[cfg(not(feature = "mock"))]
+    pub fn rpc(&self) -> AsyncRpcClient {
+        AsyncRpcClient::new_with_commitment(
+            self.cfg.cluster.url().to_string(),
+            self.cfg.options.unwrap_or_default(),
+        )
+    }
+
     /// Returns a threadsafe request builder
     pub fn request(&self) -> RequestBuilder<'_, C, Arc<dyn ThreadSafeSigner>> {
         RequestBuilder::from(
@@ -50,6 +76,7 @@ impl<C: Deref<Target = impl Signer> + Clone> Program<C> {
             self.cfg.cluster.url(),
             self.cfg.payer.clone(),
             self.cfg.options,
+            &self.internal_rpc_client,
         )
     }
 
@@ -98,6 +125,7 @@ impl<'a, C: Deref<Target = impl Signer> + Clone> RequestBuilder<'a, C, Arc<dyn T
         cluster: &str,
         payer: C,
         options: Option<CommitmentConfig>,
+        rpc_client: &'a AsyncRpcClient,
     ) -> Self {
         Self {
             program_id,
@@ -108,6 +136,7 @@ impl<'a, C: Deref<Target = impl Signer> + Clone> RequestBuilder<'a, C, Arc<dyn T
             instructions: Vec::new(),
             instruction_data: None,
             signers: Vec::new(),
+            internal_rpc_client: rpc_client,
             _phantom: PhantomData,
         }
     }