CPI client generation (#19)

This commit is contained in:
Armani Ferrante 2021-01-14 15:16:27 -08:00 committed by GitHub
parent 830c187279
commit 92b5f74eea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 675 additions and 147 deletions

View File

@ -67,7 +67,7 @@ pub fn account(
};
proc_macro::TokenStream::from(quote! {
#[derive(AnchorSerialize, AnchorDeserialize)]
#[derive(AnchorSerialize, AnchorDeserialize, Clone)]
#account_strct
#coder

View File

@ -19,9 +19,12 @@ description = "Created with Anchor"
edition = "2018"
[lib]
crate-type = ["cdylib"]
crate-type = ["cdylib", "lib"]
name = "{1}"
[features]
no-entrypoint = []
[dependencies]
borsh = {{ git = "https://github.com/project-serum/borsh", branch = "serum", features = ["serum-program"] }}
solana-program = "1.4.3"

View File

@ -5,6 +5,8 @@ use anchor_syn::parser::accounts as accounts_parser;
use proc_macro::TokenStream;
use syn::parse_macro_input;
/// Implements an `Accounts` deserializer on the given struct, applying any
/// constraints specified via `#[account]` attributes.
#[proc_macro_derive(Accounts, attributes(account))]
pub fn derive_anchor_deserialize(item: TokenStream) -> TokenStream {
let strct = parse_macro_input!(item as syn::ItemStruct);

View File

@ -50,10 +50,11 @@ module.exports = {
collapsable: false,
title: "Tutorials",
children: [
"/tutorials/tutorial-0",
"/tutorials/tutorial-1",
"/tutorials/tutorial-2",
],
"/tutorials/tutorial-0",
"/tutorials/tutorial-1",
"/tutorials/tutorial-2",
"/tutorials/tutorial-3",
],
},
],

View File

@ -33,3 +33,9 @@ cd anchor/examples/tutorial/basic-2
For now see the [source](https://github.com/project-serum/anchor/tree/master/examples/basic-2).
TODO
## Next Steps
We've covered the basics for writing a single program using Anchor on Solana. But the power of
blockchains come not from a single program, but from combining multiple *composable* programs
(buzzword alert!). Next, we'll see how to call one program from another.

View File

@ -0,0 +1,74 @@
# Tutorial 3: Cross Program Invocations (CPI)
This tutorial covers how to call one program from another, a process known as
*cross-program-invocation* (CPI).
## Clone the Repo
To get started, clone the repo.
```bash
git clone https://github.com/project-serum/anchor
```
And change directories to the [example](https://github.com/project-serum/anchor/tree/master/examples/tutorial/basic-2).
```bash
cd anchor/examples/tutorial/basic-3
```
## Defining a Puppet Program
We start with the program that will be called by another program, the puppet.
<<< @/../examples/tutorial/basic-3/programs/puppet/src/lib.rs
If you've followed along the other tutorials, this should be straight forward. We have
a program with two instructions, `initialize`, which does nothing other than the
initialization of the account (remember, the program *transparently* prepends a unique 8
byte discriminator the first time an account is used). and `set_data`, which takes a previously
initialized account, and sets its data field.
Now, suppose we wanted to call `set_data` from another program.
## Defining a Puppet Master Program
We define a new `puppet-master` crate, which successfully executes the Puppet program's `set_data`
instruction via CPI.
<<< @/../examples/tutorial/basic-3/programs/puppet-master/src/lib.rs#core
Things to notice
* We create a `CpiContext` object with the target instruction's accounts and program,
here `SetData` and `puppet_program`.
* To invoke an instruction on another program, just use the `cpi` module on the crate, here, `puppet::cpi::set_data`.
* Our `Accounts` struct has a new type, `CpiAccount`, containing the target program's `Puppet`
account. Think of `CpiAccount` exactly like `ProgramAccount`, except used for accounts *not*
owned by the current program.
::: details
When adding another Anchor program to your crate's `Cargo.toml`, make sure to specify the `no-entrypoint`
feature. If you look at the `Cargo.toml` for this example, you'll see
`puppet = { path = "../puppet", features = ["no-entrypoint"] }`.
:::
## Signer Seeds
Often it's useful for a program to sign instructions. For example, if a program controls a token
account and wants to send tokens to another account, it must sign. In Solana, this is done by specifying
"signer seeds" on CPI (TODO: add link to docs). To do this using the example above, simply change
`CpiContext::new(cpi_accounts, cpi_program)` to
`CpiContext::new_with_signer(cpi_accounts, cpi_program, signer_seeds)`.
## Return values
Solana currently has no way to return values from CPI, alas. However, one can approximate this
by having the callee write return values to an account and the caller read that account to
retrieve the return value. In future work, Anchor should do this transparently.
## Conclusion
That's it for the introductory tutorials for Anchor. For more, see the `examples/` directory in the
[repository](https://github.com/project-serum/anchor). For feature requests or bugs, GitHub issues
are appreciated. For direct questions or support, join the [Serum Discord](https://discord.com/channels/739225212658122886/752530209848295555).

View File

@ -11,8 +11,8 @@ mod sysvars {
}
#[derive(Accounts)]
pub struct Sysvars {
pub clock: Clock,
pub rent: Rent,
pub stake_history: StakeHistory,
pub struct Sysvars<'info> {
pub clock: Sysvar<'info, Clock>,
pub rent: Sysvar<'info, Rent>,
pub stake_history: Sysvar<'info, StakeHistory>,
}

View File

@ -23,7 +23,7 @@ mod basic_1 {
pub struct Initialize<'info> {
#[account(init)]
pub my_account: ProgramAccount<'info, MyAccount>,
pub rent: Rent,
pub rent: Sysvar<'info, Rent>,
}
#[derive(Accounts)]

View File

@ -13,7 +13,6 @@ mod basic_2 {
let root = &mut ctx.accounts.root;
root.authority = authority;
root.data = data;
root.initialized = true;
Ok(())
}
@ -25,8 +24,7 @@ mod basic_2 {
pub fn create_leaf(ctx: Context<CreateLeaf>, data: u64, custom: MyCustomType) -> ProgramResult {
let leaf = &mut ctx.accounts.leaf;
leaf.initialized = true;
leaf.root = *ctx.accounts.root.info.key;
leaf.root = *ctx.accounts.root.to_account_info().key;
leaf.data = data;
leaf.custom = custom;
Ok(())
@ -52,7 +50,7 @@ mod basic_2 {
pub struct CreateRoot<'info> {
#[account(init)]
pub root: ProgramAccount<'info, Root>,
pub rent: Rent,
pub rent: Sysvar<'info, Rent>,
}
#[derive(Accounts)]
@ -68,7 +66,7 @@ pub struct CreateLeaf<'info> {
pub root: ProgramAccount<'info, Root>,
#[account(init)]
pub leaf: ProgramAccount<'info, Leaf>,
pub rent: Rent,
pub rent: Sysvar<'info, Rent>,
}
#[derive(Accounts)]
@ -85,14 +83,12 @@ pub struct UpdateLeaf<'info> {
#[account]
pub struct Root {
pub initialized: bool,
pub authority: Pubkey,
pub data: u64,
}
#[account]
pub struct Leaf {
pub initialized: bool,
pub root: Pubkey,
pub data: u64,
pub custom: MyCustomType,
@ -100,7 +96,7 @@ pub struct Leaf {
// Define custom types.
#[derive(AnchorSerialize, AnchorDeserialize)]
#[derive(AnchorSerialize, AnchorDeserialize, Clone)]
pub struct MyCustomType {
pub my_data: u64,
pub key: Pubkey,

View File

@ -0,0 +1,2 @@
cluster = "localnet"
wallet = "~/.config/solana/id.json"

View File

@ -0,0 +1,4 @@
[workspace]
members = [
"programs/*"
]

View File

@ -0,0 +1,20 @@
[package]
name = "puppet-master"
version = "0.1.0"
description = "Created with Anchor"
edition = "2018"
[lib]
crate-type = ["cdylib", "lib"]
name = "puppet_master"
[features]
no-entrypoint = []
[dependencies]
borsh = { git = "https://github.com/project-serum/borsh", branch = "serum", features = ["serum-program"] }
solana-program = "1.4.3"
solana-sdk = { version = "1.3.14", default-features = false, features = ["program"] }
# anchor = { git = "https://github.com/project-serum/anchor", features = ["derive"] }
anchor = { path = "/home/armaniferrante/Documents/code/src/github.com/project-serum/anchor", features = ["derive"] }
puppet = { path = "../puppet", features = ["no-entrypoint"] }

View File

@ -0,0 +1,2 @@
[target.bpfel-unknown-unknown.dependencies.std]
features = []

View File

@ -0,0 +1,26 @@
#![feature(proc_macro_hygiene)]
// #region core
use anchor::prelude::*;
use puppet::{Puppet, SetData};
#[program]
mod puppet_master {
use super::*;
pub fn pull_strings(ctx: Context<PullStrings>, data: u64) -> ProgramResult {
let cpi_program = ctx.accounts.puppet_program.clone();
let cpi_accounts = SetData {
puppet: ctx.accounts.puppet.clone(),
};
let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts);
puppet::cpi::set_data(cpi_ctx, data)
}
}
#[derive(Accounts)]
pub struct PullStrings<'info> {
#[account(mut)]
pub puppet: CpiAccount<'info, Puppet>,
pub puppet_program: AccountInfo<'info>,
}
// #endregion core

View File

@ -0,0 +1,19 @@
[package]
name = "puppet"
version = "0.1.0"
description = "Created with Anchor"
edition = "2018"
[lib]
crate-type = ["cdylib", "lib"]
name = "puppet"
[features]
no-entrypoint = []
[dependencies]
borsh = { git = "https://github.com/project-serum/borsh", branch = "serum", features = ["serum-program"] }
solana-program = "1.4.3"
solana-sdk = { version = "1.3.14", default-features = false, features = ["program"] }
# anchor = { git = "https://github.com/project-serum/anchor", features = ["derive"] }
anchor = { path = "/home/armaniferrante/Documents/code/src/github.com/project-serum/anchor", features = ["derive"] }

View File

@ -0,0 +1,2 @@
[target.bpfel-unknown-unknown.dependencies.std]
features = []

View File

@ -0,0 +1,35 @@
#![feature(proc_macro_hygiene)]
use anchor::prelude::*;
#[program]
mod puppet {
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> ProgramResult {
Ok(())
}
pub fn set_data(ctx: Context<SetData>, data: u64) -> ProgramResult {
let puppet = &mut ctx.accounts.puppet;
puppet.data = data;
Ok(())
}
}
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(init)]
pub puppet: ProgramAccount<'info, Puppet>,
pub rent: Sysvar<'info, Rent>,
}
#[derive(Accounts)]
pub struct SetData<'info> {
#[account(mut)]
pub puppet: ProgramAccount<'info, Puppet>,
}
#[account]
pub struct Puppet {
pub data: u64,
}

View File

@ -0,0 +1,47 @@
const assert = require("assert");
const anchor = require("@project-serum/anchor");
describe("basic-3", () => {
const provider = anchor.Provider.local();
// Configure the client to use the local cluster.
anchor.setProvider(provider);
it("Performs CPI from puppet master to puppet", async () => {
const puppetMaster = anchor.workspace.PuppetMaster;
const puppet = anchor.workspace.Puppet;
// Initialize a new puppet account.
const newPuppetAccount = new anchor.web3.Account();
const tx = await puppet.rpc.initialize({
accounts: {
puppet: newPuppetAccount.publicKey,
rent: anchor.web3.SYSVAR_RENT_PUBKEY,
},
signers: [newPuppetAccount],
instructions: [
anchor.web3.SystemProgram.createAccount({
fromPubkey: provider.wallet.publicKey,
newAccountPubkey: newPuppetAccount.publicKey,
space: 8 + 8, // Add 8 for the account discriminator.
lamports: await provider.connection.getMinimumBalanceForRentExemption(
8 + 8
),
programId: puppet.programId,
}),
],
});
// Invoke the puppet master to perform a CPI to the puppet.
await puppetMaster.rpc.pullStrings(new anchor.BN(111), {
accounts: {
puppet: newPuppetAccount.publicKey,
puppetProgram: puppet.programId,
},
});
// Check the state updated.
puppetAccount = await puppet.account.puppet(newPuppetAccount.publicKey);
assert.ok(puppetAccount.data.eq(new anchor.BN(111)));
});
});

View File

@ -1,4 +1,5 @@
use solana_sdk::account_info::AccountInfo;
use solana_sdk::instruction::AccountMeta;
use solana_sdk::program_error::ProgramError;
use solana_sdk::pubkey::Pubkey;
use std::io::Write;
@ -8,6 +9,7 @@ pub use anchor_attribute_access_control::access_control;
pub use anchor_attribute_account::account;
pub use anchor_attribute_program::program;
pub use anchor_derive_accounts::Accounts;
/// Default serialization format for anchor instructions and accounts.
pub use borsh::{BorshDeserialize as AnchorDeserialize, BorshSerialize as AnchorSerialize};
/// A data structure of Solana accounts that can be deserialized from the input
@ -16,9 +18,26 @@ pub use borsh::{BorshDeserialize as AnchorDeserialize, BorshSerialize as AnchorS
/// (in addition to any done within `AccountDeserialize`) on accounts to ensure
/// the accounts maintain any invariants required for the program to run
/// securely.
pub trait Accounts<'info>: Sized {
fn try_accounts(program_id: &Pubkey, from: &[AccountInfo<'info>])
-> Result<Self, ProgramError>;
pub trait Accounts<'info>: ToAccountMetas + ToAccountInfos<'info> + Sized {
fn try_accounts(
program_id: &Pubkey,
from: &mut &[AccountInfo<'info>],
) -> Result<Self, ProgramError>;
}
/// Transformation to `AccountMeta` structs.
pub trait ToAccountMetas {
fn to_account_metas(&self) -> Vec<AccountMeta>;
}
/// Transformation to `AccountInfo` structs.
pub trait ToAccountInfos<'info> {
fn to_account_infos(&self) -> Vec<AccountInfo<'info>>;
}
/// Transformation to an `AccountInfo` struct.
pub trait ToAccountInfo<'info> {
fn to_account_info(&self) -> AccountInfo<'info>;
}
/// A data structure that can be serialized and stored in an `AccountInfo` data
@ -48,16 +67,15 @@ pub trait AccountDeserialize: Sized {
fn try_deserialize_unchecked(buf: &mut &[u8]) -> Result<Self, ProgramError>;
}
/// A container for a deserialized `account` and raw `AccountInfo` object.
///
/// Using this within a data structure deriving `Accounts` will ensure the
/// account is owned by the currently executing program.
pub struct ProgramAccount<'a, T: AccountSerialize + AccountDeserialize> {
pub info: AccountInfo<'a>,
pub account: T,
/// Container for a serializable `account`. Use this to reference any account
/// owned by the currently executing program.
#[derive(Clone)]
pub struct ProgramAccount<'a, T: AccountSerialize + AccountDeserialize + Clone> {
info: AccountInfo<'a>,
account: T,
}
impl<'a, T: AccountSerialize + AccountDeserialize> ProgramAccount<'a, T> {
impl<'a, T: AccountSerialize + AccountDeserialize + Clone> ProgramAccount<'a, T> {
pub fn new(info: AccountInfo<'a>, account: T) -> ProgramAccount<'a, T> {
Self { info, account }
}
@ -93,7 +111,15 @@ impl<'a, T: AccountSerialize + AccountDeserialize> ProgramAccount<'a, T> {
}
}
impl<'a, T: AccountSerialize + AccountDeserialize> Deref for ProgramAccount<'a, T> {
impl<'info, T: AccountSerialize + AccountDeserialize + Clone> ToAccountInfo<'info>
for ProgramAccount<'info, T>
{
fn to_account_info(&self) -> AccountInfo<'info> {
self.info.clone()
}
}
impl<'a, T: AccountSerialize + AccountDeserialize + Clone> Deref for ProgramAccount<'a, T> {
type Target = T;
fn deref(&self) -> &Self::Target {
@ -101,30 +127,123 @@ impl<'a, T: AccountSerialize + AccountDeserialize> Deref for ProgramAccount<'a,
}
}
impl<'a, T: AccountSerialize + AccountDeserialize> DerefMut for ProgramAccount<'a, T> {
impl<'a, T: AccountSerialize + AccountDeserialize + Clone> DerefMut for ProgramAccount<'a, T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.account
}
}
/// A data structure providing non-argument inputs to the Solana program, namely
/// the currently executing program's ID and the set of validated, deserialized
/// accounts.
pub struct Context<'a, 'b, T> {
/// Similar to `ProgramAccount`, but to reference any account *not* owned by
/// the current program.
pub type CpiAccount<'a, T> = ProgramAccount<'a, T>;
/// Container for a Solana sysvar.
pub struct Sysvar<'info, T: solana_sdk::sysvar::Sysvar> {
info: AccountInfo<'info>,
account: T,
}
impl<'info, T: solana_sdk::sysvar::Sysvar> Sysvar<'info, T> {
pub fn from_account_info(
acc_info: &AccountInfo<'info>,
) -> Result<Sysvar<'info, T>, ProgramError> {
Ok(Sysvar {
info: acc_info.clone(),
account: T::from_account_info(&acc_info)?,
})
}
}
impl<'a, T: solana_sdk::sysvar::Sysvar> Deref for Sysvar<'a, T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.account
}
}
impl<'a, T: solana_sdk::sysvar::Sysvar> DerefMut for Sysvar<'a, T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.account
}
}
impl<'info, T: solana_sdk::sysvar::Sysvar> ToAccountInfo<'info> for Sysvar<'info, T> {
fn to_account_info(&self) -> AccountInfo<'info> {
self.info.clone()
}
}
impl<'info> ToAccountInfo<'info> for AccountInfo<'info> {
fn to_account_info(&self) -> AccountInfo<'info> {
self.clone()
}
}
/// Provides non-argument inputs to the program.
pub struct Context<'a, 'b, 'c, 'info, T> {
/// Deserialized accounts.
pub accounts: &'a mut T,
/// Currently executing program id.
pub program_id: &'b Pubkey,
/// Remaining accounts given but not deserialized or validated.
pub remaining_accounts: &'c [AccountInfo<'info>],
}
impl<'a, 'b, 'c, 'info, T> Context<'a, 'b, 'c, 'info, T> {
pub fn new(
accounts: &'a mut T,
program_id: &'b Pubkey,
remaining_accounts: &'c [AccountInfo<'info>],
) -> Self {
Self {
accounts,
program_id,
remaining_accounts,
}
}
}
/// Context speciying non-argument inputs for cross-program-invocations.
pub struct CpiContext<'a, 'b, 'c, 'info, T: Accounts<'info>> {
pub accounts: T,
pub program: AccountInfo<'info>,
pub signer_seeds: &'a [&'b [&'c [u8]]],
}
impl<'a, 'b, 'c, 'info, T: Accounts<'info>> CpiContext<'a, 'b, 'c, 'info, T> {
pub fn new(program: AccountInfo<'info>, accounts: T) -> Self {
Self {
accounts,
program,
signer_seeds: &[],
}
}
pub fn new_with_signer(
accounts: T,
program: AccountInfo<'info>,
signer_seeds: &'a [&'b [&'c [u8]]],
) -> Self {
Self {
accounts,
program,
signer_seeds,
}
}
}
pub mod prelude {
pub use super::{
access_control, account, program, AccountDeserialize, AccountSerialize, Accounts,
AnchorDeserialize, AnchorSerialize, Context, ProgramAccount,
AnchorDeserialize, AnchorSerialize, Context, CpiAccount, CpiContext, ProgramAccount,
Sysvar, ToAccountInfo, ToAccountInfos, ToAccountMetas,
};
pub use solana_program::msg;
pub use solana_sdk::account_info::next_account_info;
pub use solana_sdk::account_info::AccountInfo;
pub use solana_sdk::account_info::{next_account_info, AccountInfo};
pub use solana_sdk::entrypoint::ProgramResult;
pub use solana_sdk::instruction::AccountMeta;
pub use solana_sdk::program_error::ProgramError;
pub use solana_sdk::pubkey::Pubkey;
pub use solana_sdk::sysvar::clock::Clock;
@ -137,5 +256,5 @@ pub mod prelude {
pub use solana_sdk::sysvar::slot_hashes::SlotHashes;
pub use solana_sdk::sysvar::slot_history::SlotHistory;
pub use solana_sdk::sysvar::stake_history::StakeHistory;
pub use solana_sdk::sysvar::Sysvar;
pub use solana_sdk::sysvar::Sysvar as SolanaSysvar;
}

View File

@ -5,6 +5,7 @@ use crate::{
use quote::quote;
pub fn generate(accs: AccountsStruct) -> proc_macro2::TokenStream {
// Extract out each account info.
let acc_infos: Vec<proc_macro2::TokenStream> = accs
.fields
.iter()
@ -15,44 +16,49 @@ pub fn generate(accs: AccountsStruct) -> proc_macro2::TokenStream {
}
})
.collect();
let (deser_fields, access_checks, return_tys) = {
// Deserialization for each field.
let deser_fields: Vec<proc_macro2::TokenStream> = accs
.fields
.iter()
.map(generate_field_deserialization)
.collect();
// Constraint checks for each account fields.
let access_checks: Vec<proc_macro2::TokenStream> = accs
.fields
.iter()
.map(|f: &Field| {
let checks: Vec<proc_macro2::TokenStream> = f
.constraints
.iter()
.map(|c| generate_constraint(&f, c))
.collect();
quote! {
#(#checks)*
}
})
.collect();
// Each field in the final deserialized accounts struct.
let return_tys: Vec<proc_macro2::TokenStream> = accs
.fields
.iter()
.map(|f: &Field| {
let name = &f.ident;
quote! {
#name
}
})
.collect();
(deser_fields, access_checks, return_tys)
let acc_infos_len = {
let acc_infos_len = acc_infos.len();
quote! {
#acc_infos_len
}
};
// Deserialization for each field.
let deser_fields: Vec<proc_macro2::TokenStream> = accs
.fields
.iter()
.map(generate_field_deserialization)
.collect();
// Constraint checks for each account fields.
let access_checks: Vec<proc_macro2::TokenStream> = accs
.fields
.iter()
.map(|f: &Field| {
let checks: Vec<proc_macro2::TokenStream> = f
.constraints
.iter()
.map(|c| generate_constraint(&f, c))
.collect();
quote! {
#(#checks)*
}
})
.collect();
// Each field in the final deserialized accounts struct.
let return_tys: Vec<proc_macro2::TokenStream> = accs
.fields
.iter()
.map(|f: &Field| {
let name = &f.ident;
quote! {
#name
}
})
.collect();
// Exit program code-blocks for each account.
let on_save: Vec<proc_macro2::TokenStream> = accs
.fields
.iter()
@ -60,16 +66,54 @@ pub fn generate(accs: AccountsStruct) -> proc_macro2::TokenStream {
let ident = &f.ident;
let info = match f.ty {
Ty::AccountInfo => quote! { #ident },
Ty::ProgramAccount(_) => quote! { #ident.info },
Ty::ProgramAccount(_) => quote! { #ident.to_account_info() },
_ => return quote! {},
};
match f.is_mut {
false => quote! {},
true => quote! {
let mut data = self.#info.try_borrow_mut_data()?;
let dst: &mut [u8] = &mut data;
let mut cursor = std::io::Cursor::new(dst);
self.#ident.account.try_serialize(&mut cursor)?;
// Only persist the change if the account is owned by the
// current program.
if program_id == self.#info.owner {
let info = self.#info;
let mut data = info.try_borrow_mut_data()?;
let dst: &mut [u8] = &mut data;
let mut cursor = std::io::Cursor::new(dst);
self.#ident.try_serialize(&mut cursor)?;
}
},
}
})
.collect();
// Implementation for `ToAccountInfos` trait.
let to_acc_infos: Vec<proc_macro2::TokenStream> = accs
.fields
.iter()
.map(|f: &Field| {
let name = &f.ident;
quote! {
self.#name.to_account_info()
}
})
.collect();
// Implementation for `ToAccountMetas` trait.
let to_acc_metas: Vec<proc_macro2::TokenStream> = accs
.fields
.iter()
.map(|f: &Field| {
let name = &f.ident;
let is_signer = match f.is_signer {
false => quote! { false },
true => quote! { true },
};
match f.is_mut {
false => quote! {
AccountMeta::new_readonly(*self.#name.to_account_info().key, #is_signer)
},
true => quote! {
AccountMeta::new(*self.#name.to_account_info().key, #is_signer)
},
}
})
@ -86,12 +130,15 @@ pub fn generate(accs: AccountsStruct) -> proc_macro2::TokenStream {
quote! {
impl#combined_generics Accounts#trait_generics for #name#strct_generics {
fn try_accounts(program_id: &Pubkey, accounts: &[AccountInfo<'info>]) -> Result<Self, ProgramError> {
let acc_infos = &mut accounts.iter();
fn try_accounts(program_id: &Pubkey, remaining_accounts: &mut &[AccountInfo<'info>]) -> Result<Self, ProgramError> {
let acc_infos = &mut remaining_accounts.iter();
// Pull out each account info from the `accounts` slice.
#(#acc_infos)*
// Move the remaining_accounts cursor to the iterator end.
*remaining_accounts = &remaining_accounts[#acc_infos_len..];
// Deserialize each account.
#(#deser_fields)*
@ -105,8 +152,24 @@ pub fn generate(accs: AccountsStruct) -> proc_macro2::TokenStream {
}
}
impl#combined_generics ToAccountInfos#trait_generics for #name#strct_generics {
fn to_account_infos(&self) -> Vec<AccountInfo<'info>> {
vec![
#(#to_acc_infos),*
]
}
}
impl#combined_generics ToAccountMetas for #name#strct_generics {
fn to_account_metas(&self) -> Vec<AccountMeta> {
vec![
#(#to_acc_metas),*
]
}
}
impl#strct_generics #name#strct_generics {
pub fn exit(&self) -> ProgramResult {
pub fn exit(&self, program_id: &Pubkey) -> ProgramResult {
#(#on_save)*
Ok(())
}
@ -131,36 +194,47 @@ pub fn generate_field_deserialization(f: &Field) -> proc_macro2::TokenStream {
},
}
}
Ty::CpiAccount(acc) => {
let account_struct = &acc.account_ident;
match f.is_init {
false => quote! {
let #ident: CpiAccount<#account_struct> = CpiAccount::try_from(#ident)?;
},
true => quote! {
let #ident: CpiAccount<#account_struct> = CpiAccount::try_from_init(#ident)?;
},
}
}
Ty::Sysvar(sysvar) => match sysvar {
SysvarTy::Clock => quote! {
let #ident = Clock::from_account_info(#ident)?;
let #ident: Sysvar<Clock> = Sysvar::from_account_info(#ident)?;
},
SysvarTy::Rent => quote! {
let #ident = Rent::from_account_info(#ident)?;
let #ident: Sysvar<Rent> = Sysvar::from_account_info(#ident)?;
},
SysvarTy::EpochSchedule => quote! {
let #ident = EpochSchedule::from_account_info(#ident)?;
let #ident: Sysvar<EpochSchedule> = Sysvar::from_account_info(#ident)?;
},
SysvarTy::Fees => quote! {
let #ident = Fees::from_account_info(#ident)?;
let #ident: Sysvar<Fees> = Sysvar::from_account_info(#ident)?;
},
SysvarTy::RecentBlockHashes => quote! {
let #ident = RecentBlockhashes::from_account_info(#ident)?;
let #ident: Sysvar<RecentBlockhashes> = Sysvar::from_account_info(#ident)?;
},
SysvarTy::SlotHashes => quote! {
let #ident = SlotHashes::from_account_info(#ident)?;
let #ident: Sysvar<SlotHashes> = Sysvar::from_account_info(#ident)?;
},
SysvarTy::SlotHistory => quote! {
let #ident = SlotHistory::from_account_info(#ident)?;
let #ident: Sysvar<SlotHistory> = Sysvar::from_account_info(#ident)?;
},
SysvarTy::StakeHistory => quote! {
let #ident = StakeHistory::from_account_info(#ident)?;
let #ident: Sysvar<StakeHistory> = Sysvar::from_account_info(#ident)?;
},
SysvarTy::Instructions => quote! {
let #ident = Instructions::from_account_info(#ident)?;
let #ident: Sysvar<Instructions> = Sysvar::from_account_info(#ident)?;
},
SysvarTy::Rewards => quote! {
let #ident = Rewards::from_account_info(#ident)?;
let #ident: Sysvar<Rewards> = Sysvar::from_account_info(#ident)?;
},
},
};
@ -190,7 +264,7 @@ pub fn generate_constraint_belongs_to(
let ident = &f.ident;
// todo: would be nice if target could be an account info object.
quote! {
if &#ident.#target != #target.info.key {
if &#ident.#target != #target.to_account_info().key {
return Err(ProgramError::Custom(1)); // todo: error codes
}
}
@ -200,7 +274,7 @@ pub fn generate_constraint_signer(f: &Field, _c: &ConstraintSigner) -> proc_macr
let ident = &f.ident;
let info = match f.ty {
Ty::AccountInfo => quote! { #ident },
Ty::ProgramAccount(_) => quote! { #ident.info },
Ty::ProgramAccount(_) => quote! { #ident.to_account_info() },
_ => panic!("Invalid syntax: signer cannot be specified."),
};
quote! {
@ -223,7 +297,7 @@ pub fn generate_constraint_owner(f: &Field, c: &ConstraintOwner) -> proc_macro2:
let ident = &f.ident;
let info = match f.ty {
Ty::AccountInfo => quote! { #ident },
Ty::ProgramAccount(_) => quote! { #ident.info },
Ty::ProgramAccount(_) => quote! { #ident.to_account_info() },
_ => panic!("Invalid syntax: owner cannot be specified."),
};
match c {
@ -243,7 +317,7 @@ pub fn generate_constraint_rent_exempt(
let ident = &f.ident;
let info = match f.ty {
Ty::AccountInfo => quote! { #ident },
Ty::ProgramAccount(_) => quote! { #ident.info },
Ty::ProgramAccount(_) => quote! { #ident.to_account_info() },
_ => panic!("Invalid syntax: rent exemption cannot be specified."),
};
match c {

View File

@ -1,4 +1,4 @@
use crate::Program;
use crate::{Program, Rpc};
use heck::CamelCase;
use quote::quote;
@ -8,13 +8,16 @@ pub fn generate(program: Program) -> proc_macro2::TokenStream {
let dispatch = generate_dispatch(&program);
let methods = generate_methods(&program);
let instruction = generate_instruction(&program);
let cpi = generate_cpi(&program);
quote! {
// Import everything in the mod, in case the user wants to put types
// in there.
use #mod_name::*;
#[cfg(not(feature = "no-entrypoint"))]
solana_program::entrypoint!(entry);
#[cfg(not(feature = "no-entrypoint"))]
fn entry(program_id: &Pubkey, accounts: &[AccountInfo], instruction_data: &[u8]) -> ProgramResult {
let mut data: &[u8] = instruction_data;
let ix = instruction::#instruction_name::deserialize(&mut data)
@ -26,50 +29,30 @@ pub fn generate(program: Program) -> proc_macro2::TokenStream {
#methods
#instruction
#cpi
}
}
pub fn generate_dispatch(program: &Program) -> proc_macro2::TokenStream {
let program_name = &program.name;
let enum_name = instruction_enum_name(program);
let dispatch_arms: Vec<proc_macro2::TokenStream> = program
.rpcs
.iter()
.map(|rpc| {
let rpc_arg_names: Vec<&syn::Ident> = rpc.args.iter().map(|arg| &arg.name).collect();
let variant_arm = {
let rpc_name_camel = proc_macro2::Ident::new(
&rpc.raw_method.sig.ident.to_string().to_camel_case(),
rpc.raw_method.sig.ident.span(),
);
// If no args, output a "unit" variant instead of a struct variant.
if rpc.args.len() == 0 {
quote! {
#enum_name::#rpc_name_camel
}
} else {
quote! {
#enum_name::#rpc_name_camel {
#(#rpc_arg_names),*
}
}
}
};
let variant_arm = generate_ix_variant(program, rpc);
let rpc_name = &rpc.raw_method.sig.ident;
let anchor = &rpc.anchor_ident;
quote! {
instruction::#variant_arm => {
let mut accounts = #anchor::try_accounts(program_id, accounts)?;
let mut remaining_accounts: &[AccountInfo] = accounts;
let mut accounts = #anchor::try_accounts(program_id, &mut remaining_accounts)?;
#program_name::#rpc_name(
Context {
accounts: &mut accounts,
program_id,
},
Context::new(&mut accounts, program_id, remaining_accounts),
#(#rpc_arg_names),*
)?;
accounts.exit()
accounts.exit(program_id)
}
}
})
@ -82,6 +65,26 @@ pub fn generate_dispatch(program: &Program) -> proc_macro2::TokenStream {
}
}
pub fn generate_ix_variant(program: &Program, rpc: &Rpc) -> proc_macro2::TokenStream {
let enum_name = instruction_enum_name(program);
let rpc_arg_names: Vec<&syn::Ident> = rpc.args.iter().map(|arg| &arg.name).collect();
let rpc_name_camel = proc_macro2::Ident::new(
&rpc.raw_method.sig.ident.to_string().to_camel_case(),
rpc.raw_method.sig.ident.span(),
);
if rpc.args.len() == 0 {
quote! {
#enum_name::#rpc_name_camel
}
} else {
quote! {
#enum_name::#rpc_name_camel {
#(#rpc_arg_names),*
}
}
}
}
pub fn generate_methods(program: &Program) -> proc_macro2::TokenStream {
let program_mod = &program.program_mod;
quote! {
@ -128,7 +131,56 @@ pub fn generate_instruction(program: &Program) -> proc_macro2::TokenStream {
fn instruction_enum_name(program: &Program) -> proc_macro2::Ident {
proc_macro2::Ident::new(
&program.name.to_string().to_camel_case(),
&format!("_{}Instruction", program.name.to_string().to_camel_case()),
program.name.span(),
)
}
fn generate_cpi(program: &Program) -> proc_macro2::TokenStream {
let cpi_methods: Vec<proc_macro2::TokenStream> = program
.rpcs
.iter()
.map(|rpc| {
let accounts_ident = &rpc.anchor_ident;
let cpi_method = {
let ix_variant = generate_ix_variant(program, rpc);
let method_name = &rpc.ident;
let args: Vec<&syn::PatType> = rpc.args.iter().map(|arg| &arg.raw_arg).collect();
quote! {
pub fn #method_name<'a, 'b, 'c, 'info>(
ctx: CpiContext<'a, 'b, 'c, 'info, #accounts_ident<'info>>,
#(#args),*
) -> ProgramResult {
let ix = {
let ix = instruction::#ix_variant;
let data = AnchorSerialize::try_to_vec(&ix)
.map_err(|_| ProgramError::InvalidInstructionData)?;
let accounts = ctx.accounts.to_account_metas();
solana_program::instruction::Instruction {
program_id: *ctx.program.key,
accounts,
data,
}
};
let mut acc_infos = ctx.accounts.to_account_infos();
acc_infos.push(ctx.program.clone());
solana_sdk::program::invoke_signed(
&ix,
&acc_infos,
ctx.signer_seeds,
)
}
}
};
cpi_method
})
.collect();
quote! {
pub mod cpi {
use super::*;
#(#cpi_methods)*
}
}
}

View File

@ -17,6 +17,7 @@ pub struct Rpc {
pub raw_method: syn::ItemFn,
pub ident: syn::Ident,
pub args: Vec<RpcArg>,
// The ident for the struct deriving Accounts.
pub anchor_ident: syn::Ident,
}
@ -46,6 +47,7 @@ impl AccountsStruct {
}
}
// Returns all program owned accounts in the Accounts struct.
pub fn account_tys(&self) -> Vec<String> {
self.fields
.iter()
@ -73,6 +75,7 @@ pub enum Ty {
AccountInfo,
ProgramAccount(ProgramAccountTy),
Sysvar(SysvarTy),
CpiAccount(CpiAccountTy),
}
#[derive(PartialEq)]
@ -95,6 +98,12 @@ pub struct ProgramAccountTy {
pub account_ident: syn::Ident,
}
#[derive(PartialEq)]
pub struct CpiAccountTy {
// The struct type of the account.
pub account_ident: syn::Ident,
}
// An access control constraint for an account.
pub enum Constraint {
Signer(ConstraintSigner),

View File

@ -1,6 +1,6 @@
use crate::{
AccountsStruct, Constraint, ConstraintBelongsTo, ConstraintLiteral, ConstraintOwner,
ConstraintRentExempt, ConstraintSigner, Field, ProgramAccountTy, SysvarTy, Ty,
ConstraintRentExempt, ConstraintSigner, CpiAccountTy, Field, ProgramAccountTy, SysvarTy, Ty,
};
pub fn parse(strct: &syn::ItemStruct) -> AccountsStruct {
@ -68,22 +68,24 @@ fn parse_ty(f: &syn::Field) -> Ty {
let segments = &path.segments[0];
match segments.ident.to_string().as_str() {
"ProgramAccount" => Ty::ProgramAccount(parse_program_account(&path)),
"CpiAccount" => Ty::CpiAccount(parse_cpi_account(&path)),
"Sysvar" => Ty::Sysvar(parse_sysvar(&path)),
"AccountInfo" => Ty::AccountInfo,
"Clock" => Ty::Sysvar(SysvarTy::Clock),
"Rent" => Ty::Sysvar(SysvarTy::Rent),
"EpochSchedule" => Ty::Sysvar(SysvarTy::EpochSchedule),
"Fees" => Ty::Sysvar(SysvarTy::Fees),
"RecentBlockhashes" => Ty::Sysvar(SysvarTy::RecentBlockHashes),
"SlotHashes" => Ty::Sysvar(SysvarTy::SlotHashes),
"SlotHistory" => Ty::Sysvar(SysvarTy::SlotHistory),
"StakeHistory" => Ty::Sysvar(SysvarTy::StakeHistory),
"Instructions" => Ty::Sysvar(SysvarTy::Instructions),
"Rewards" => Ty::Sysvar(SysvarTy::Rewards),
_ => panic!("invalid account type"),
}
}
fn parse_cpi_account(path: &syn::Path) -> CpiAccountTy {
let account_ident = parse_account(path);
CpiAccountTy { account_ident }
}
fn parse_program_account(path: &syn::Path) -> ProgramAccountTy {
let account_ident = parse_account(path);
ProgramAccountTy { account_ident }
}
fn parse_account(path: &syn::Path) -> syn::Ident {
let segments = &path.segments[0];
let account_ident = match &segments.arguments {
syn::PathArguments::AngleBracketed(args) => {
@ -104,7 +106,43 @@ fn parse_program_account(path: &syn::Path) -> ProgramAccountTy {
}
_ => panic!("Invalid ProgramAccount"),
};
ProgramAccountTy { account_ident }
account_ident
}
fn parse_sysvar(path: &syn::Path) -> SysvarTy {
let segments = &path.segments[0];
let account_ident = match &segments.arguments {
syn::PathArguments::AngleBracketed(args) => {
// Expected: <'info, MyType>.
assert!(args.args.len() == 2);
match &args.args[1] {
syn::GenericArgument::Type(ty) => match ty {
syn::Type::Path(ty_path) => {
// TODO: allow segmented paths.
assert!(ty_path.path.segments.len() == 1);
let path_segment = &ty_path.path.segments[0];
path_segment.ident.clone()
}
_ => panic!("Invalid Sysvar"),
},
_ => panic!("Invalid Sysvar"),
}
}
_ => panic!("Invalid Sysvar"),
};
match account_ident.to_string().as_str() {
"Clock" => SysvarTy::Clock,
"Rent" => SysvarTy::Rent,
"EpochSchedule" => SysvarTy::EpochSchedule,
"Fees" => SysvarTy::Fees,
"RecentBlockhashes" => SysvarTy::RecentBlockHashes,
"SlotHashes" => SysvarTy::SlotHashes,
"SlotHistory" => SysvarTy::SlotHistory,
"StakeHistory" => SysvarTy::StakeHistory,
"Instructions" => SysvarTy::Instructions,
"Rewards" => SysvarTy::Rewards,
_ => panic!("Invalid Sysvar"),
}
}
fn parse_constraints(anchor: &syn::Attribute, ty: &Ty) -> (Vec<Constraint>, bool, bool, bool) {
@ -220,19 +258,16 @@ fn parse_constraints(anchor: &syn::Attribute, ty: &Ty) -> (Vec<Constraint>, bool
}
if !has_owner_constraint {
if ty == &Ty::AccountInfo {
constraints.push(Constraint::Owner(ConstraintOwner::Skip));
} else {
if let Ty::ProgramAccount(_) = ty {
constraints.push(Constraint::Owner(ConstraintOwner::Program));
}
}
match is_rent_exempt {
None => constraints.push(Constraint::RentExempt(ConstraintRentExempt::Skip)),
Some(is_re) => match is_re {
if let Some(is_re) = is_rent_exempt {
match is_re {
false => constraints.push(Constraint::RentExempt(ConstraintRentExempt::Skip)),
true => constraints.push(Constraint::RentExempt(ConstraintRentExempt::Enforce)),
},
}
}
(constraints, is_mut, is_signer, is_init)

View File

@ -1,6 +1,6 @@
{
"name": "@project-serum/anchor",
"version": "0.0.0-alpha.2",
"version": "0.0.0-alpha.3",
"description": "Anchor client",
"main": "dist/cjs/index.js",
"module": "dist/esm/index.js",