Rust client generation
This commit is contained in:
parent
8dc295fe67
commit
e42668279a
|
@ -120,6 +120,16 @@ dependencies = [
|
||||||
"toml",
|
"toml",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anchor-client"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anchor-lang",
|
||||||
|
"solana-client",
|
||||||
|
"solana-sdk",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anchor-derive-accounts"
|
name = "anchor-derive-accounts"
|
||||||
version = "0.0.0-alpha.0"
|
version = "0.0.0-alpha.0"
|
||||||
|
|
|
@ -25,6 +25,7 @@ thiserror = "1.0.20"
|
||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = [
|
||||||
"cli",
|
"cli",
|
||||||
|
"client",
|
||||||
"syn",
|
"syn",
|
||||||
"attribute/*",
|
"attribute/*",
|
||||||
"derive/*",
|
"derive/*",
|
||||||
|
|
|
@ -500,7 +500,9 @@ fn deploy(url: Option<String>, keypair: Option<String>) -> Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run migration script.
|
// Run migration script.
|
||||||
migrate(&url)?;
|
if Path::new("migrations/deploy.js").exists() {
|
||||||
|
migrate(&url)?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
|
@ -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"
|
|
@ -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(())
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,6 +3,7 @@ use crate::{
|
||||||
ConstraintLiteral, ConstraintOwner, ConstraintRentExempt, ConstraintSeeds, ConstraintSigner,
|
ConstraintLiteral, ConstraintOwner, ConstraintRentExempt, ConstraintSeeds, ConstraintSigner,
|
||||||
Field, Ty,
|
Field, Ty,
|
||||||
};
|
};
|
||||||
|
use heck::SnakeCase;
|
||||||
use quote::quote;
|
use quote::quote;
|
||||||
|
|
||||||
pub fn generate(accs: AccountsStruct) -> proc_macro2::TokenStream {
|
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! {
|
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 {
|
impl#combined_generics anchor_lang::Accounts#trait_generics for #name#strct_generics {
|
||||||
#[inline(never)]
|
#[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> {
|
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> {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use crate::parser;
|
use crate::parser;
|
||||||
use crate::{Program, RpcArg, State};
|
use crate::{Program, RpcArg, State};
|
||||||
use heck::CamelCase;
|
use heck::{CamelCase, SnakeCase};
|
||||||
use quote::quote;
|
use quote::quote;
|
||||||
|
|
||||||
pub fn generate(program: Program) -> proc_macro2::TokenStream {
|
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 methods = generate_methods(&program);
|
||||||
let instruction = generate_instruction(&program);
|
let instruction = generate_instruction(&program);
|
||||||
let cpi = generate_cpi(&program);
|
let cpi = generate_cpi(&program);
|
||||||
|
let accounts = generate_accounts(&program);
|
||||||
|
|
||||||
quote! {
|
quote! {
|
||||||
// Import everything in the mod, in case the user wants to put types
|
// TODO: remove once we allow segmented paths in `Accounts` structs.
|
||||||
// in there.
|
|
||||||
use #mod_name::*;
|
use #mod_name::*;
|
||||||
|
|
||||||
|
|
||||||
#[cfg(not(feature = "no-entrypoint"))]
|
#[cfg(not(feature = "no-entrypoint"))]
|
||||||
anchor_lang::solana_program::entrypoint!(entry);
|
anchor_lang::solana_program::entrypoint!(entry);
|
||||||
#[cfg(not(feature = "no-entrypoint"))]
|
#[cfg(not(feature = "no-entrypoint"))]
|
||||||
|
@ -29,10 +30,10 @@ pub fn generate(program: Program) -> proc_macro2::TokenStream {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let mut data: &[u8] = instruction_data;
|
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
|
.map_err(|_| ProgramError::Custom(1))?; // todo: error code
|
||||||
|
|
||||||
#dispatch
|
#dispatch
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a private module to not clutter the program's namespace.
|
// 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::*;
|
use super::*;
|
||||||
|
|
||||||
#handlers_non_inlined
|
#handlers_non_inlined
|
||||||
|
|
||||||
#instruction
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#accounts
|
||||||
|
|
||||||
|
#instruction
|
||||||
|
|
||||||
#methods
|
#methods
|
||||||
|
|
||||||
#cpi
|
#cpi
|
||||||
|
@ -57,7 +60,7 @@ pub fn generate_dispatch(program: &Program) -> proc_macro2::TokenStream {
|
||||||
let variant_arm = generate_ctor_variant(program, state);
|
let variant_arm = generate_ctor_variant(program, state);
|
||||||
let ctor_args = generate_ctor_args(state);
|
let ctor_args = generate_ctor_args(state);
|
||||||
quote! {
|
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()
|
format!("__{}", name).parse().unwrap()
|
||||||
};
|
};
|
||||||
quote! {
|
quote! {
|
||||||
__private::instruction::#variant_arm => {
|
instruction::#variant_arm => {
|
||||||
__private::#rpc_name(program_id, accounts, #(#rpc_arg_names),*)
|
__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;
|
let rpc_name = &rpc.raw_method.sig.ident;
|
||||||
quote! {
|
quote! {
|
||||||
__private::instruction::#variant_arm => {
|
instruction::#variant_arm => {
|
||||||
__private::#rpc_name(program_id, accounts, #(#rpc_arg_names),*)
|
__private::#rpc_name(program_id, accounts, #(#rpc_arg_names),*)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -594,6 +597,10 @@ pub fn generate_instruction(program: &Program) -> proc_macro2::TokenStream {
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
quote! {
|
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 {
|
pub mod instruction {
|
||||||
use super::*;
|
use super::*;
|
||||||
#[derive(AnchorSerialize, AnchorDeserialize)]
|
#[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 {
|
fn generate_cpi(program: &Program) -> proc_macro2::TokenStream {
|
||||||
let cpi_methods: Vec<proc_macro2::TokenStream> = program
|
let cpi_methods: Vec<proc_macro2::TokenStream> = program
|
||||||
.rpcs
|
.rpcs
|
||||||
|
@ -634,7 +692,7 @@ fn generate_cpi(program: &Program) -> proc_macro2::TokenStream {
|
||||||
#(#args),*
|
#(#args),*
|
||||||
) -> ProgramResult {
|
) -> ProgramResult {
|
||||||
let ix = {
|
let ix = {
|
||||||
let ix = __private::instruction::#ix_variant;
|
let ix = instruction::#ix_variant;
|
||||||
let data = AnchorSerialize::try_to_vec(&ix)
|
let data = AnchorSerialize::try_to_vec(&ix)
|
||||||
.map_err(|_| ProgramError::InvalidInstructionData)?;
|
.map_err(|_| ProgramError::InvalidInstructionData)?;
|
||||||
let accounts = ctx.accounts.to_account_metas(None);
|
let accounts = ctx.accounts.to_account_metas(None);
|
||||||
|
|
Loading…
Reference in New Issue