Rust client generation

This commit is contained in:
Armani Ferrante 2021-01-29 08:02:34 -08:00
parent 8dc295fe67
commit e42668279a
No known key found for this signature in database
GPG Key ID: D597A80BCF8E12B7
9 changed files with 604 additions and 12 deletions

10
Cargo.lock generated
View File

@ -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"

View File

@ -25,6 +25,7 @@ thiserror = "1.0.20"
[workspace]
members = [
"cli",
"client",
"syn",
"attribute/*",
"derive/*",

View File

@ -500,7 +500,9 @@ fn deploy(url: Option<String>, keypair: Option<String>) -> Result<()> {
}
// Run migration script.
migrate(&url)?;
if Path::new("migrations/deploy.js").exists() {
migrate(&url)?;
}
Ok(())
}

11
client/Cargo.toml Normal file
View File

@ -0,0 +1,11 @@
[package]
name = "anchor-client"
version = "0.1.0"
authors = ["Armani Ferrante <armaniferrante@gmail.com>"]
edition = "2018"
[dependencies]
anchor-lang = { path = "../" }
solana-client = "1.5.0"
solana-sdk = "1.5.0"
thiserror = "1.0.20"

15
client/example/Cargo.toml Normal file
View File

@ -0,0 +1,15 @@
[package]
name = "example"
version = "0.1.0"
authors = ["Armani Ferrante <armaniferrante@gmail.com>"]
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"

158
client/example/src/main.rs Normal file
View File

@ -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(())
}

228
client/src/lib.rs Normal file
View File

@ -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<CommitmentConfig>,
}
/// 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<T: AccountDeserialize>(&self, address: Pubkey) -> Result<T, ClientError> {
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<AccountMeta>,
options: CommitmentConfig,
instructions: Vec<Instruction>,
payer: Keypair,
// Serialized instruction data for the target RPC.
instruction_data: Option<Vec<u8>>,
signers: Vec<&'a dyn Signer>,
}
impl<'a> RequestBuilder<'a> {
pub fn new(
program_id: Pubkey,
cluster: &str,
payer: Keypair,
options: Option<CommitmentConfig>,
) -> 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<Signature, ClientError> {
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)
}
}

View File

@ -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<proc_macro2::TokenStream> = 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<proc_macro2::TokenStream> = 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<proc_macro2::TokenStream> = 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<bool>) -> Vec<anchor_lang::solana_program::instruction::AccountMeta> {
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<Self, anchor_lang::solana_program::program_error::ProgramError> {

View File

@ -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<proc_macro2::TokenStream> = 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<proc_macro2::TokenStream> = 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);