From f677742a978ffdf7bc321746b4119394f6654b7c Mon Sep 17 00:00:00 2001 From: cryptopapi997 <38372048+cryptopapi997@users.noreply.github.com> Date: Fri, 12 Jul 2024 07:32:55 +0200 Subject: [PATCH] client: Add `tokio` support to `RequestBuilder` with `async` feature (#3057) Co-authored-by: acheron <98934430+acheroncrypto@users.noreply.github.com> --- .gitignore | 1 + CHANGELOG.md | 1 + client/example/src/nonblocking.rs | 61 +++++++++++++++++++++++++------ client/src/blocking.rs | 21 ++++++++++- client/src/lib.rs | 48 +++++++++++------------- client/src/nonblocking.rs | 39 ++++++++++++++++++-- 6 files changed, 128 insertions(+), 43 deletions(-) diff --git a/.gitignore b/.gitignore index d41f3a725..186717407 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ target/ test-ledger examples/*/Cargo.lock examples/**/Cargo.lock +*/example/Cargo.lock tests/*/Cargo.lock tests/**/Cargo.lock tests/*/yarn.lock diff --git a/CHANGELOG.md b/CHANGELOG.md index 23d67e4a0..8fc408df2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ 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])). ## [0.30.1] - 2024-06-20 diff --git a/client/example/src/nonblocking.rs b/client/example/src/nonblocking.rs index eeee5d4f1..0f558699e 100644 --- a/client/example/src/nonblocking.rs +++ b/client/example/src/nonblocking.rs @@ -26,7 +26,7 @@ use composite::instruction as composite_instruction; use composite::{DummyA, DummyB}; use optional::account::{DataAccount, DataPda}; use std::ops::Deref; -use std::rc::Rc; +use std::sync::Arc; use std::time::Duration; use tokio::sync::mpsc; use tokio::time::sleep; @@ -43,7 +43,7 @@ pub async fn main() -> Result<()> { ); // Client. - let payer = Rc::new(payer); + let payer = Arc::new(payer); let client = Client::new_with_options(url.clone(), payer.clone(), CommitmentConfig::processed()); @@ -51,6 +51,7 @@ pub async fn main() -> Result<()> { composite(&client, opts.composite_pid).await?; basic_2(&client, opts.basic_2_pid).await?; basic_4(&client, opts.basic_4_pid).await?; + test_tokio(client, opts.basic_2_pid).await?; // Can also use references, since they deref to a signer let payer: &Keypair = &payer; @@ -61,6 +62,42 @@ pub async fn main() -> Result<()> { Ok(()) } +pub async fn test_tokio(client: Client>, pid: Pubkey) -> Result<()> { + tokio::spawn(async move { + let program = client.program(pid).unwrap(); + + // `Create` parameters. + let counter = Arc::new(Keypair::new()); + let counter_pubkey = counter.pubkey(); + let authority = program.payer(); + + // Build and send a transaction. + program + .request() + .signer(counter) + .accounts(basic_2_accounts::Create { + counter: counter_pubkey, + user: authority, + system_program: system_program::ID, + }) + .args(basic_2_instruction::Create { authority }) + .send() + .await + .unwrap(); + + let counter_account: Counter = program.account(counter_pubkey).await.unwrap(); + + assert_eq!(counter_account.authority, authority); + assert_eq!(counter_account.count, 0); + }) + .await + .unwrap(); + + println!("Tokio success!"); + + Ok(()) +} + pub async fn composite + Clone>( client: &Client, pid: Pubkey, @@ -69,8 +106,8 @@ pub async fn composite + Clone>( let program = client.program(pid)?; // `Initialize` parameters. - let dummy_a = Keypair::new(); - let dummy_b = Keypair::new(); + let dummy_a = Arc::new(Keypair::new()); + let dummy_b = Arc::new(Keypair::new()); // Build and send a transaction. program @@ -95,8 +132,8 @@ pub async fn composite + Clone>( 500, &program.id(), )) - .signer(&dummy_a) - .signer(&dummy_b) + .signer(dummy_a.clone()) + .signer(dummy_b.clone()) .accounts(Initialize { dummy_a: dummy_a.pubkey(), dummy_b: dummy_b.pubkey(), @@ -147,13 +184,13 @@ pub async fn basic_2 + Clone>( let program = client.program(pid)?; // `Create` parameters. - let counter = Keypair::new(); + let counter = Arc::new(Keypair::new()); let authority = program.payer(); // Build and send a transaction. program .request() - .signer(&counter) + .signer(counter.clone()) .accounts(basic_2_accounts::Create { counter: counter.pubkey(), user: authority, @@ -253,13 +290,13 @@ pub async fn optional + Clone>( let program = client.program(pid)?; // `Initialize` parameters. - let data_account_keypair = Keypair::new(); + let data_account_keypair = Arc::new(Keypair::new()); let data_account_key = data_account_keypair.pubkey(); let data_pda_seeds = &[DataPda::PREFIX.as_ref(), data_account_key.as_ref()]; let data_pda_key = Pubkey::find_program_address(data_pda_seeds, &pid).0; - let required_keypair = Keypair::new(); + let required_keypair = Arc::new(Keypair::new()); let value: u64 = 10; // Build and send a transaction. @@ -276,8 +313,8 @@ pub async fn optional + Clone>( DataAccount::LEN as u64, &program.id(), )) - .signer(&data_account_keypair) - .signer(&required_keypair) + .signer(data_account_keypair.clone()) + .signer(required_keypair.clone()) .accounts(OptionalInitialize { payer: Some(program.payer()), required: required_keypair.pubkey(), diff --git a/client/src/blocking.rs b/client/src/blocking.rs index 492729d63..541af4cff 100644 --- a/client/src/blocking.rs +++ b/client/src/blocking.rs @@ -33,6 +33,18 @@ impl + Clone> Program { }) } + /// Returns a request builder. + pub fn request(&self) -> RequestBuilder<'_, C, Box> { + RequestBuilder::from( + self.program_id, + self.cfg.cluster.url(), + self.cfg.payer.clone(), + self.cfg.options, + #[cfg(not(feature = "async"))] + self.rt.handle(), + ) + } + /// Returns the account at the given address. pub fn account(&self, address: Pubkey) -> Result { self.rt.block_on(self.account_internal(address)) @@ -70,7 +82,7 @@ impl + Clone> Program { } } -impl<'a, C: Deref + Clone> RequestBuilder<'a, C> { +impl<'a, C: Deref + Clone> RequestBuilder<'a, C, Box> { pub fn from( program_id: Pubkey, cluster: &str, @@ -88,9 +100,16 @@ impl<'a, C: Deref + Clone> RequestBuilder<'a, C> { instruction_data: None, signers: Vec::new(), handle, + _phantom: PhantomData, } } + #[must_use] + pub fn signer(mut self, signer: T) -> Self { + self.signers.push(Box::new(signer)); + self + } + pub fn signed_transaction(&self) -> Result { self.handle.block_on(self.signed_transaction_internal()) } diff --git a/client/src/lib.rs b/client/src/lib.rs index cd1dd534a..0c2495402 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -60,8 +60,6 @@ //! anchor-client = { version = "0.30.1 ", features = ["async"] } //! ```` -use anchor_lang::solana_program::hash::Hash; -use anchor_lang::solana_program::instruction::{AccountMeta, Instruction}; use anchor_lang::solana_program::program_error::ProgramError; use anchor_lang::solana_program::pubkey::Pubkey; use anchor_lang::{AccountDeserialize, Discriminator, InstructionData, ToAccountMetas}; @@ -84,6 +82,8 @@ use solana_client::{ }; use solana_sdk::account::Account; use solana_sdk::commitment_config::CommitmentConfig; +use solana_sdk::hash::Hash; +use solana_sdk::instruction::{AccountMeta, Instruction}; use solana_sdk::signature::{Signature, Signer}; use solana_sdk::transaction::Transaction; use std::iter::Map; @@ -227,18 +227,6 @@ impl + Clone> Program { self.cfg.payer.pubkey() } - /// Returns a request builder. - pub fn request(&self) -> RequestBuilder { - RequestBuilder::from( - self.program_id, - self.cfg.cluster.url(), - self.cfg.payer.clone(), - self.cfg.options, - #[cfg(not(feature = "async"))] - self.rt.handle(), - ) - } - pub fn id(&self) -> Pubkey { self.program_id } @@ -503,23 +491,34 @@ pub enum ClientError { IOError(#[from] std::io::Error), } +pub trait AsSigner { + fn as_signer(&self) -> &dyn Signer; +} + +impl<'a> AsSigner for Box { + fn as_signer(&self) -> &dyn Signer { + self.as_ref() + } +} + /// `RequestBuilder` provides a builder interface to create and send /// transactions to a cluster. -pub struct RequestBuilder<'a, C> { +pub struct RequestBuilder<'a, C, S: 'a> { cluster: String, program_id: Pubkey, accounts: Vec, options: CommitmentConfig, instructions: Vec, payer: C, - // Serialized instruction data for the target RPC. instruction_data: Option>, - signers: Vec<&'a dyn Signer>, + signers: Vec, #[cfg(not(feature = "async"))] handle: &'a Handle, + _phantom: PhantomData<&'a ()>, } -impl<'a, C: Deref + Clone> RequestBuilder<'a, C> { +// Shared implementation for all RequestBuilders +impl<'a, C: Deref + Clone, S: AsSigner> RequestBuilder<'a, C, S> { #[must_use] pub fn payer(mut self, payer: C) -> Self { self.payer = payer; @@ -593,12 +592,6 @@ impl<'a, C: Deref + Clone> RequestBuilder<'a, C> { self } - #[must_use] - pub fn signer(mut self, signer: &'a dyn Signer) -> Self { - self.signers.push(signer); - self - } - pub fn instructions(&self) -> Result, ClientError> { let mut instructions = self.instructions.clone(); if let Some(ix_data) = &self.instruction_data { @@ -617,13 +610,14 @@ impl<'a, C: Deref + Clone> RequestBuilder<'a, C> { latest_hash: Hash, ) -> Result { let instructions = self.instructions()?; - let mut signers = self.signers.clone(); - signers.push(&*self.payer); + let signers: Vec<&dyn Signer> = self.signers.iter().map(|s| s.as_signer()).collect(); + let mut all_signers = signers; + all_signers.push(&*self.payer); let tx = Transaction::new_signed_with_payer( &instructions, Some(&self.payer.pubkey()), - &signers, + &all_signers, latest_hash, ); diff --git a/client/src/nonblocking.rs b/client/src/nonblocking.rs index 93f06c114..a45153465 100644 --- a/client/src/nonblocking.rs +++ b/client/src/nonblocking.rs @@ -1,6 +1,6 @@ use crate::{ - ClientError, Config, EventContext, EventUnsubscriber, Program, ProgramAccountsIterator, - RequestBuilder, + AsSigner, ClientError, Config, EventContext, EventUnsubscriber, Program, + ProgramAccountsIterator, RequestBuilder, }; use anchor_lang::{prelude::Pubkey, AccountDeserialize, Discriminator}; use solana_client::{rpc_config::RpcSendTransactionConfig, rpc_filter::RpcFilterType}; @@ -18,6 +18,22 @@ impl<'a> EventUnsubscriber<'a> { } } +pub trait ThreadSafeSigner: Signer + Send + Sync + 'static { + fn to_signer(&self) -> &dyn Signer; +} + +impl ThreadSafeSigner for T { + fn to_signer(&self) -> &dyn Signer { + self + } +} + +impl AsSigner for Arc { + fn as_signer(&self) -> &dyn Signer { + self.to_signer() + } +} + impl + Clone> Program { pub fn new(program_id: Pubkey, cfg: Config) -> Result { Ok(Self { @@ -27,6 +43,16 @@ impl + Clone> Program { }) } + /// Returns a threadsafe request builder + pub fn request(&self) -> RequestBuilder<'_, C, Arc> { + RequestBuilder::from( + self.program_id, + self.cfg.cluster.url(), + self.cfg.payer.clone(), + self.cfg.options, + ) + } + /// Returns the account at the given address. pub async fn account(&self, address: Pubkey) -> Result { self.account_internal(address).await @@ -66,7 +92,7 @@ impl + Clone> Program { } } -impl<'a, C: Deref + Clone> RequestBuilder<'a, C> { +impl<'a, C: Deref + Clone> RequestBuilder<'a, C, Arc> { pub fn from( program_id: Pubkey, cluster: &str, @@ -82,9 +108,16 @@ impl<'a, C: Deref + Clone> RequestBuilder<'a, C> { instructions: Vec::new(), instruction_data: None, signers: Vec::new(), + _phantom: PhantomData, } } + #[must_use] + pub fn signer(mut self, signer: T) -> Self { + self.signers.push(Arc::new(signer)); + self + } + pub async fn signed_transaction(&self) -> Result { self.signed_transaction_internal().await }