From e42668279a2e679f2f7e657ecac4d5b20d4dad92 Mon Sep 17 00:00:00 2001 From: Armani Ferrante Date: Fri, 29 Jan 2021 08:02:34 -0800 Subject: [PATCH] Rust client generation --- Cargo.lock | 10 ++ Cargo.toml | 1 + cli/src/main.rs | 4 +- client/Cargo.toml | 11 ++ client/example/Cargo.toml | 15 +++ client/example/src/main.rs | 158 +++++++++++++++++++++++++ client/src/lib.rs | 228 ++++++++++++++++++++++++++++++++++++ syn/src/codegen/accounts.rs | 109 +++++++++++++++++ syn/src/codegen/program.rs | 80 +++++++++++-- 9 files changed, 604 insertions(+), 12 deletions(-) create mode 100644 client/Cargo.toml create mode 100644 client/example/Cargo.toml create mode 100644 client/example/src/main.rs create mode 100644 client/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index c30662cf..74f4fd11 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -120,6 +120,16 @@ dependencies = [ "toml", ] +[[package]] +name = "anchor-client" +version = "0.1.0" +dependencies = [ + "anchor-lang", + "solana-client", + "solana-sdk", + "thiserror", +] + [[package]] name = "anchor-derive-accounts" version = "0.0.0-alpha.0" diff --git a/Cargo.toml b/Cargo.toml index 380fae67..a67f6bd2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ thiserror = "1.0.20" [workspace] members = [ "cli", + "client", "syn", "attribute/*", "derive/*", diff --git a/cli/src/main.rs b/cli/src/main.rs index 02b07648..9122b47a 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -500,7 +500,9 @@ fn deploy(url: Option, keypair: Option) -> Result<()> { } // Run migration script. - migrate(&url)?; + if Path::new("migrations/deploy.js").exists() { + migrate(&url)?; + } Ok(()) } diff --git a/client/Cargo.toml b/client/Cargo.toml new file mode 100644 index 00000000..9d7105c0 --- /dev/null +++ b/client/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "anchor-client" +version = "0.1.0" +authors = ["Armani Ferrante "] +edition = "2018" + +[dependencies] +anchor-lang = { path = "../" } +solana-client = "1.5.0" +solana-sdk = "1.5.0" +thiserror = "1.0.20" \ No newline at end of file diff --git a/client/example/Cargo.toml b/client/example/Cargo.toml new file mode 100644 index 00000000..87a7b1d4 --- /dev/null +++ b/client/example/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "example" +version = "0.1.0" +authors = ["Armani Ferrante "] +edition = "2018" + +[workspace] + +[dependencies] +anchor-client = { path = "../" } +basic-2 = { path = "../../examples/tutorial/basic-2/programs/basic-2", features = ["no-entrypoint"] } +composite = { path = "../../examples/composite/programs/composite", features = ["no-entrypoint"] } +shellexpand = "2.1.0" +anyhow = "1.0.32" +rand = "0.7.3" \ No newline at end of file diff --git a/client/example/src/main.rs b/client/example/src/main.rs new file mode 100644 index 00000000..6ed2a7a0 --- /dev/null +++ b/client/example/src/main.rs @@ -0,0 +1,158 @@ +use anchor_client::solana_sdk::commitment_config::CommitmentConfig; +use anchor_client::solana_sdk::signature::read_keypair_file; +use anchor_client::solana_sdk::signature::{Keypair, Signer}; +use anchor_client::solana_sdk::system_instruction; +use anchor_client::solana_sdk::sysvar; +use anchor_client::Client; +use anyhow::Result; +// The `accounts` and `instructions` modules are generated by the framework. +use basic_2::accounts::CreateAuthor; +use basic_2::instruction::Basic2Instruction; +use basic_2::Author; +// The `accounts` and `instructions` modules are generated by the framework. +use composite::accounts::{Bar, CompositeUpdate, Foo, Initialize}; +use composite::instruction::CompositeInstruction; +use composite::{DummyA, DummyB}; + +use rand::rngs::OsRng; + +fn main() -> Result<()> { + // Wallet and cluster params. + let payer = read_keypair_file(&shellexpand::tilde("~/.config/solana/id.json")) + .expect("Example requires a keypair file"); + let url = "http://localhost:8899"; + let opts = CommitmentConfig::recent(); + + // Client. + let client = Client::new_with_options(url, payer, opts); + + // Run tests. + composite(&client)?; + basic_2(&client)?; + + // Success. + Ok(()) +} + +// Runs a client for examples/tutorial/composite. +// +// Make sure to run a localnet with the program deploy to run this example. +fn composite(client: &Client) -> Result<()> { + // Deployed program to execute. + let pid = "75TykCe6b1oBa8JWVvfkXsFbZydgqi3QfRjgBEJJwy2g" + .parse() + .unwrap(); + + // Program client. + let program = client.program(pid); + + // `Initialize` parameters. + let dummy_a = Keypair::generate(&mut OsRng); + let dummy_b = Keypair::generate(&mut OsRng); + + // Build and send a transaction. + program + .request() + .instruction(system_instruction::create_account( + &program.payer(), + &dummy_a.pubkey(), + program.rpc().get_minimum_balance_for_rent_exemption(500)?, + 500, + &program.id(), + )) + .instruction(system_instruction::create_account( + &program.payer(), + &dummy_b.pubkey(), + program.rpc().get_minimum_balance_for_rent_exemption(500)?, + 500, + &program.id(), + )) + .signer(&dummy_a) + .signer(&dummy_b) + .accounts(Initialize { + dummy_a: dummy_a.pubkey(), + dummy_b: dummy_b.pubkey(), + rent: sysvar::rent::ID, + }) + .args(CompositeInstruction::Initialize) + .send()?; + + // Assert the transaction worked. + let dummy_a_account: DummyA = program.account(dummy_a.pubkey())?; + let dummy_b_account: DummyB = program.account(dummy_b.pubkey())?; + assert_eq!(dummy_a_account.data, 0); + assert_eq!(dummy_b_account.data, 0); + + // Build and send another transaction, using composite account parameters. + program + .request() + .accounts(CompositeUpdate { + foo: Foo { + dummy_a: dummy_a.pubkey(), + }, + bar: Bar { + dummy_b: dummy_b.pubkey(), + }, + }) + .args(CompositeInstruction::CompositeUpdate { + dummy_a: 1234, + dummy_b: 4321, + }) + .send()?; + + // Assert the transaction worked. + let dummy_a_account: DummyA = program.account(dummy_a.pubkey())?; + let dummy_b_account: DummyB = program.account(dummy_b.pubkey())?; + assert_eq!(dummy_a_account.data, 1234); + assert_eq!(dummy_b_account.data, 4321); + + println!("Success!"); + + Ok(()) +} + +// Runs a client for examples/tutorial/basic-2. +// +// Make sure to run a localnet with the program deploy to run this example. +fn basic_2(client: &Client) -> Result<()> { + // Deployed program to execute. + let program_id = "FU3yvTEGTFUdMa6qAjVyKfNcDU6hb4yXbPhz8f5iFyvE" + .parse() + .unwrap(); + + let program = client.program(program_id); + + // `CreateAuthor` parameters. + let author = Keypair::generate(&mut OsRng); + let authority = program.payer(); + + // Build and send a transaction. + program + .request() + .instruction(system_instruction::create_account( + &authority, + &author.pubkey(), + program.rpc().get_minimum_balance_for_rent_exemption(500)?, + 500, + &program_id, + )) + .signer(&author) + .accounts(CreateAuthor { + author: author.pubkey(), + rent: sysvar::rent::ID, + }) + .args(Basic2Instruction::CreateAuthor { + authority, + name: "My Book Name".to_string(), + }) + .send()?; + + let author_account: Author = program.account(author.pubkey())?; + + assert_eq!(author_account.authority, authority); + assert_eq!(author_account.name, "My Book Name".to_string()); + + println!("Success!"); + + Ok(()) +} diff --git a/client/src/lib.rs b/client/src/lib.rs new file mode 100644 index 00000000..ac1a1962 --- /dev/null +++ b/client/src/lib.rs @@ -0,0 +1,228 @@ +//! `anchor_client` provides an RPC client to send transactions and fetch +//! deserialized accounts from Solana programs written in `anchor_lang`. + +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, AnchorSerialize, ToAccountMetas}; +use solana_client::client_error::ClientError as SolanaClientError; +use solana_client::rpc_client::RpcClient; +use solana_sdk::commitment_config::CommitmentConfig; +use solana_sdk::signature::{Keypair, Signature, Signer}; +use solana_sdk::transaction::Transaction; +use std::convert::Into; +use thiserror::Error; + +pub use anchor_lang; +pub use solana_client; +pub use solana_sdk; + +/// Client defines the base configuration for building RPC clients to +/// communitcate with Anchor programs running on a Solana cluster. It's +/// primary use is to build a `Program` client via the `program` method. +pub struct Client { + cfg: Config, +} + +impl Client { + pub fn new(cluster: &str, payer: Keypair) -> Self { + Self { + cfg: Config { + cluster: cluster.to_string(), + payer, + options: None, + }, + } + } + + pub fn new_with_options(cluster: &str, payer: Keypair, options: CommitmentConfig) -> Self { + Self { + cfg: Config { + cluster: cluster.to_string(), + payer, + options: Some(options), + }, + } + } + + pub fn program(&self, program_id: Pubkey) -> Program { + Program { + program_id, + cfg: Config { + cluster: self.cfg.cluster.clone(), + options: self.cfg.options.clone(), + payer: Keypair::from_bytes(&self.cfg.payer.to_bytes()).unwrap(), + }, + } + } +} + +// Internal configuration for a client. +struct Config { + cluster: String, + payer: Keypair, + options: Option, +} + +/// Program is the primary client handle to be used to build and send requests. +pub struct Program { + program_id: Pubkey, + cfg: Config, +} + +impl Program { + pub fn payer(&self) -> Pubkey { + self.cfg.payer.pubkey() + } + + /// Returns a request builder. + pub fn request(&self) -> RequestBuilder { + RequestBuilder::new( + self.program_id, + &self.cfg.cluster, + Keypair::from_bytes(&self.cfg.payer.to_bytes()).unwrap(), + self.cfg.options.clone(), + ) + } + + /// Returns the account at the given address. + pub fn account(&self, address: Pubkey) -> Result { + let rpc_client = RpcClient::new_with_commitment( + self.cfg.cluster.clone(), + self.cfg.options.unwrap_or(Default::default()), + ); + let account = rpc_client + .get_account_with_commitment(&address, CommitmentConfig::recent())? + .value + .ok_or(ClientError::AccountNotFound)?; + let mut data: &[u8] = &account.data; + T::try_deserialize(&mut data).map_err(Into::into) + } + + pub fn rpc(&self) -> RpcClient { + RpcClient::new_with_commitment( + self.cfg.cluster.clone(), + self.cfg.options.unwrap_or(Default::default()), + ) + } + + pub fn id(&self) -> Pubkey { + self.program_id + } +} + +#[derive(Debug, Error)] +pub enum ClientError { + #[error("Account not found")] + AccountNotFound, + #[error("{0}")] + ProgramError(#[from] ProgramError), + #[error("{0}")] + SolanaClientError(#[from] SolanaClientError), +} + +/// `RequestBuilder` provides a builder interface to create and send +/// transactions to a cluster. +pub struct RequestBuilder<'a> { + cluster: String, + program_id: Pubkey, + accounts: Vec, + options: CommitmentConfig, + instructions: Vec, + payer: Keypair, + // Serialized instruction data for the target RPC. + instruction_data: Option>, + signers: Vec<&'a dyn Signer>, +} + +impl<'a> RequestBuilder<'a> { + pub fn new( + program_id: Pubkey, + cluster: &str, + payer: Keypair, + options: Option, + ) -> Self { + Self { + program_id, + payer, + cluster: cluster.to_string(), + accounts: Vec::new(), + options: options.unwrap_or(Default::default()), + instructions: Vec::new(), + instruction_data: None, + signers: Vec::new(), + } + } + + pub fn payer(mut self, payer: Keypair) -> Self { + self.payer = payer; + self + } + + pub fn cluster(mut self, url: &str) -> Self { + self.cluster = url.to_string(); + self + } + + pub fn instruction(mut self, ix: Instruction) -> Self { + self.instructions.push(ix); + self + } + + pub fn program(mut self, program_id: Pubkey) -> Self { + self.program_id = program_id; + self + } + + pub fn accounts(mut self, accounts: impl ToAccountMetas) -> Self { + let mut metas = accounts.to_account_metas(None); + self.accounts.append(&mut metas); + self + } + + pub fn options(mut self, options: CommitmentConfig) -> Self { + self.options = options; + self + } + + pub fn args(mut self, args: impl AnchorSerialize) -> Self { + let data = args.try_to_vec().expect("Should always serialize"); + self.instruction_data = Some(data); + self + } + + pub fn signer(mut self, signer: &'a dyn Signer) -> Self { + self.signers.push(signer); + self + } + + pub fn send(self) -> Result { + let mut instructions = self.instructions; + if let Some(ix_data) = self.instruction_data { + instructions.push(Instruction { + program_id: self.program_id, + data: ix_data, + accounts: self.accounts, + }); + } + + let mut signers = self.signers; + signers.push(&self.payer); + + let rpc_client = RpcClient::new_with_commitment(self.cluster, self.options); + + let tx = { + let (recent_hash, _fee_calc) = rpc_client.get_recent_blockhash()?; + Transaction::new_signed_with_payer( + &instructions, + Some(&self.payer.pubkey()), + &signers, + recent_hash, + ) + }; + + rpc_client + .send_and_confirm_transaction(&tx) + .map_err(Into::into) + } +} diff --git a/syn/src/codegen/accounts.rs b/syn/src/codegen/accounts.rs index aee9e858..976ef2a5 100644 --- a/syn/src/codegen/accounts.rs +++ b/syn/src/codegen/accounts.rs @@ -3,6 +3,7 @@ use crate::{ ConstraintLiteral, ConstraintOwner, ConstraintRentExempt, ConstraintSeeds, ConstraintSigner, Field, Ty, }; +use heck::SnakeCase; use quote::quote; pub fn generate(accs: AccountsStruct) -> proc_macro2::TokenStream { @@ -138,7 +139,115 @@ pub fn generate(accs: AccountsStruct) -> proc_macro2::TokenStream { } }; + let account_mod_name: proc_macro2::TokenStream = format!( + "__client_accounts_{}", + accs.ident.to_string().to_snake_case() + ) + .parse() + .unwrap(); + + let account_struct_fields: Vec = accs + .fields + .iter() + .map(|f: &AccountField| match f { + AccountField::AccountsStruct(s) => { + let name = &s.ident; + let symbol: proc_macro2::TokenStream = format!( + "__client_accounts_{0}::{1}", + s.symbol.to_snake_case(), + s.symbol, + ) + .parse() + .unwrap(); + quote! { + pub #name: #symbol + } + } + AccountField::Field(f) => { + let name = &f.ident; + quote! { + pub #name: anchor_lang::solana_program::pubkey::Pubkey + } + } + }) + .collect(); + + let account_struct_metas: Vec = accs + .fields + .iter() + .map(|f: &AccountField| match f { + AccountField::AccountsStruct(s) => { + let name = &s.ident; + quote! { + account_metas.extend(self.#name.to_account_metas(None)); + } + } + AccountField::Field(f) => { + let is_signer = match f.is_signer { + false => quote! {false}, + true => quote! {true}, + }; + let meta = match f.is_mut { + false => quote! { anchor_lang::solana_program::instruction::AccountMeta::new_readonly }, + true => quote! { anchor_lang::solana_program::instruction::AccountMeta::new }, + }; + let name = &f.ident; + quote! { + account_metas.push(#meta(self.#name, #is_signer)); + } + } + }) + .collect(); + + // Re-export all composite account structs (i.e. other structs deriving + // accounts embedded into this struct. Required because, these embedded + // structs are *not* visible from the #[program] macro, which is responsible + // for generating the `accounts` mod, which aggregates all the the generated + // accounts used for structs. + let re_exports: Vec = accs + .fields + .iter() + .filter_map(|f: &AccountField| match f { + AccountField::AccountsStruct(s) => Some(s), + AccountField::Field(_) => None, + }) + .map(|f: &CompositeField| { + let symbol: proc_macro2::TokenStream = format!( + "__client_accounts_{0}::{1}", + f.symbol.to_snake_case(), + f.symbol, + ) + .parse() + .unwrap(); + quote! { + pub use #symbol; + } + }) + .collect(); + quote! { + + mod #account_mod_name { + use super::*; + use anchor_lang::prelude::borsh; + #(#re_exports)* + + #[derive(anchor_lang::AnchorSerialize)] + pub struct #name { + #(#account_struct_fields),* + } + + impl anchor_lang::ToAccountMetas for #name { + fn to_account_metas(&self, is_signer: Option) -> Vec { + let mut account_metas = vec![]; + + #(#account_struct_metas)* + + account_metas + } + } + } + impl#combined_generics anchor_lang::Accounts#trait_generics for #name#strct_generics { #[inline(never)] fn try_accounts(program_id: &anchor_lang::solana_program::pubkey::Pubkey, accounts: &mut &[anchor_lang::solana_program::account_info::AccountInfo<'info>]) -> std::result::Result { diff --git a/syn/src/codegen/program.rs b/syn/src/codegen/program.rs index 8e3dd13f..fd19af08 100644 --- a/syn/src/codegen/program.rs +++ b/syn/src/codegen/program.rs @@ -1,6 +1,6 @@ use crate::parser; use crate::{Program, RpcArg, State}; -use heck::CamelCase; +use heck::{CamelCase, SnakeCase}; use quote::quote; pub fn generate(program: Program) -> proc_macro2::TokenStream { @@ -11,12 +11,13 @@ pub fn generate(program: Program) -> proc_macro2::TokenStream { let methods = generate_methods(&program); let instruction = generate_instruction(&program); let cpi = generate_cpi(&program); + let accounts = generate_accounts(&program); quote! { - // Import everything in the mod, in case the user wants to put types - // in there. + // TODO: remove once we allow segmented paths in `Accounts` structs. use #mod_name::*; + #[cfg(not(feature = "no-entrypoint"))] anchor_lang::solana_program::entrypoint!(entry); #[cfg(not(feature = "no-entrypoint"))] @@ -29,10 +30,10 @@ pub fn generate(program: Program) -> proc_macro2::TokenStream { } } let mut data: &[u8] = instruction_data; - let ix = __private::instruction::#instruction_name::deserialize(&mut data) + let ix = instruction::#instruction_name::deserialize(&mut data) .map_err(|_| ProgramError::Custom(1))?; // todo: error code - #dispatch + #dispatch } // Create a private module to not clutter the program's namespace. @@ -40,10 +41,12 @@ pub fn generate(program: Program) -> proc_macro2::TokenStream { use super::*; #handlers_non_inlined - - #instruction } + #accounts + + #instruction + #methods #cpi @@ -57,7 +60,7 @@ pub fn generate_dispatch(program: &Program) -> proc_macro2::TokenStream { let variant_arm = generate_ctor_variant(program, state); let ctor_args = generate_ctor_args(state); quote! { - __private::instruction::#variant_arm => __private::__ctor(program_id, accounts, #(#ctor_args),*), + instruction::#variant_arm => __private::__ctor(program_id, accounts, #(#ctor_args),*), } } }; @@ -80,7 +83,7 @@ pub fn generate_dispatch(program: &Program) -> proc_macro2::TokenStream { format!("__{}", name).parse().unwrap() }; quote! { - __private::instruction::#variant_arm => { + instruction::#variant_arm => { __private::#rpc_name(program_id, accounts, #(#rpc_arg_names),*) } } @@ -100,7 +103,7 @@ pub fn generate_dispatch(program: &Program) -> proc_macro2::TokenStream { ); let rpc_name = &rpc.raw_method.sig.ident; quote! { - __private::instruction::#variant_arm => { + instruction::#variant_arm => { __private::#rpc_name(program_id, accounts, #(#rpc_arg_names),*) } } @@ -594,6 +597,10 @@ pub fn generate_instruction(program: &Program) -> proc_macro2::TokenStream { .collect(); quote! { + /// `instruction` is a macro generated module containing the program's + /// instruction enum, where each variant is created from each method + /// handler in the `#[program]` mod. These should be used directly, when + /// specifying instructions on a client. pub mod instruction { use super::*; #[derive(AnchorSerialize, AnchorDeserialize)] @@ -613,6 +620,57 @@ fn instruction_enum_name(program: &Program) -> proc_macro2::Ident { ) } +fn generate_accounts(program: &Program) -> proc_macro2::TokenStream { + let mut accounts = std::collections::HashSet::new(); + + // Got through state accounts. + if let Some(state) = &program.state { + for rpc in &state.methods { + let anchor_ident = &rpc.anchor_ident; + // TODO: move to fn and share with accounts.rs. + let macro_name = format!( + "__client_accounts_{}", + anchor_ident.to_string().to_snake_case() + ); + accounts.insert(macro_name); + } + } + + // Go through instruction accounts. + for rpc in &program.rpcs { + let anchor_ident = &rpc.anchor_ident; + // TODO: move to fn and share with accounts.rs. + let macro_name = format!( + "__client_accounts_{}", + anchor_ident.to_string().to_snake_case() + ); + accounts.insert(macro_name); + } + + // Build the tokens from all accounts + let account_structs: Vec = accounts + .iter() + .map(|macro_name: &String| { + let macro_name: proc_macro2::TokenStream = macro_name.parse().unwrap(); + quote! { + pub use crate::#macro_name::*; + } + }) + .collect(); + + // TODO: calculate the account size and add it as a constant field to + // each struct here. This is convenient for Rust clients. + + quote! { + /// `accounts` is a macro generated module, providing a set of structs + /// mirroring the structs deriving `Accounts`, where each field is + /// a `Pubkey`. This is useful for specifying accounts for a client. + pub mod accounts { + #(#account_structs)* + } + } +} + fn generate_cpi(program: &Program) -> proc_macro2::TokenStream { let cpi_methods: Vec = program .rpcs @@ -634,7 +692,7 @@ fn generate_cpi(program: &Program) -> proc_macro2::TokenStream { #(#args),* ) -> ProgramResult { let ix = { - let ix = __private::instruction::#ix_variant; + let ix = instruction::#ix_variant; let data = AnchorSerialize::try_to_vec(&ix) .map_err(|_| ProgramError::InvalidInstructionData)?; let accounts = ctx.accounts.to_account_metas(None);