Program interfaces
This commit is contained in:
parent
c643438753
commit
1f31770c83
|
@ -49,6 +49,7 @@ jobs:
|
|||
- pushd examples/errors && anchor test && popd
|
||||
- pushd examples/spl/token-proxy && anchor test && popd
|
||||
- pushd examples/multisig && anchor test && popd
|
||||
- pushd examples/interface && anchor test && popd
|
||||
- pushd examples/tutorial/basic-0 && anchor test && popd
|
||||
- pushd examples/tutorial/basic-1 && anchor test && popd
|
||||
- pushd examples/tutorial/basic-2 && anchor test && popd
|
||||
|
|
|
@ -11,6 +11,12 @@ incremented for features.
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
### Features
|
||||
|
||||
* lang: Adds the ability to create and use CPI program interfaces [(#66)](https://github.com/project-serum/anchor/pull/66/files?file-filters%5B%5D=).
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
* lang, client, ts: Migrate from rust enum based method dispatch to a variant of sighash [(#64)](https://github.com/project-serum/anchor/pull/64).
|
||||
|
||||
## [0.1.0] - 2021-01-31
|
||||
|
|
|
@ -81,6 +81,18 @@ dependencies = [
|
|||
"syn 1.0.57",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anchor-attribute-interface"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anchor-syn",
|
||||
"anyhow",
|
||||
"heck",
|
||||
"proc-macro2 1.0.24",
|
||||
"quote 1.0.8",
|
||||
"syn 1.0.57",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anchor-attribute-program"
|
||||
version = "0.1.0"
|
||||
|
@ -155,6 +167,7 @@ dependencies = [
|
|||
"anchor-attribute-access-control",
|
||||
"anchor-attribute-account",
|
||||
"anchor-attribute-error",
|
||||
"anchor-attribute-interface",
|
||||
"anchor-attribute-program",
|
||||
"anchor-attribute-state",
|
||||
"anchor-derive-accounts",
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
cluster = "localnet"
|
||||
wallet = "~/.config/solana/id.json"
|
|
@ -0,0 +1,4 @@
|
|||
[workspace]
|
||||
members = [
|
||||
"programs/*"
|
||||
]
|
|
@ -0,0 +1,19 @@
|
|||
[package]
|
||||
name = "counter-auth"
|
||||
version = "0.1.0"
|
||||
description = "Created with Anchor"
|
||||
edition = "2018"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "lib"]
|
||||
name = "counter_auth"
|
||||
|
||||
[features]
|
||||
no-entrypoint = []
|
||||
no-idl = []
|
||||
cpi = ["no-entrypoint"]
|
||||
default = []
|
||||
|
||||
[dependencies]
|
||||
anchor-lang = { git = "https://github.com/project-serum/anchor" }
|
||||
counter = { path = "../counter", features = ["cpi"] }
|
|
@ -0,0 +1,2 @@
|
|||
[target.bpfel-unknown-unknown.dependencies.std]
|
||||
features = []
|
|
@ -0,0 +1,43 @@
|
|||
//! counter-auth is an example of a program *implementing* an external program
|
||||
//! interface. Here the `counter::Auth` trait, where we only allow a count
|
||||
//! to be incremented if it changes the counter from odd -> even or even -> odd.
|
||||
//! Creative, I know. :P.
|
||||
|
||||
#![feature(proc_macro_hygiene)]
|
||||
|
||||
use anchor_lang::prelude::*;
|
||||
use counter::Auth;
|
||||
|
||||
#[program]
|
||||
pub mod counter_auth {
|
||||
use super::*;
|
||||
|
||||
#[state]
|
||||
pub struct CounterAuth {}
|
||||
|
||||
// TODO: remove this impl block after addressing
|
||||
// https://github.com/project-serum/anchor/issues/71.
|
||||
impl CounterAuth {
|
||||
pub fn new(_ctx: Context<Empty>) -> Result<Self, ProgramError> {
|
||||
Ok(Self {})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'info> Auth<'info, Empty> for CounterAuth {
|
||||
fn is_authorized(_ctx: Context<Empty>, current: u64, new: u64) -> ProgramResult {
|
||||
if current % 2 == 0 {
|
||||
if new % 2 == 0 {
|
||||
return Err(ProgramError::Custom(50)); // Arbitrary error code.
|
||||
}
|
||||
} else {
|
||||
if new % 2 == 1 {
|
||||
return Err(ProgramError::Custom(60)); // Arbitrary error code.
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct Empty {}
|
|
@ -0,0 +1,18 @@
|
|||
[package]
|
||||
name = "counter"
|
||||
version = "0.1.0"
|
||||
description = "Created with Anchor"
|
||||
edition = "2018"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "lib"]
|
||||
name = "counter"
|
||||
|
||||
[features]
|
||||
no-entrypoint = []
|
||||
no-idl = []
|
||||
cpi = ["no-entrypoint"]
|
||||
default = []
|
||||
|
||||
[dependencies]
|
||||
anchor-lang = { git = "https://github.com/project-serum/anchor" }
|
|
@ -0,0 +1,2 @@
|
|||
[target.bpfel-unknown-unknown.dependencies.std]
|
||||
features = []
|
|
@ -0,0 +1,73 @@
|
|||
//! counter is an example program that depends on an external interface
|
||||
//! that another program must implement. This allows our program to depend
|
||||
//! on another program, without knowing anything about it other than the fact
|
||||
//! that it implements the `Auth` trait.
|
||||
//!
|
||||
//! Here, we have a counter, where, in order to set the count, the `Auth`
|
||||
//! program must first approve the transaction.
|
||||
|
||||
#![feature(proc_macro_hygiene)]
|
||||
|
||||
use anchor_lang::prelude::*;
|
||||
|
||||
#[program]
|
||||
pub mod counter {
|
||||
use super::*;
|
||||
|
||||
#[state]
|
||||
pub struct Counter {
|
||||
pub count: u64,
|
||||
pub auth_program: Pubkey,
|
||||
}
|
||||
|
||||
impl Counter {
|
||||
pub fn new(_ctx: Context<Empty>, auth_program: Pubkey) -> Result<Self> {
|
||||
Ok(Self {
|
||||
count: 0,
|
||||
auth_program,
|
||||
})
|
||||
}
|
||||
|
||||
#[access_control(SetCount::accounts(&self, &ctx))]
|
||||
pub fn set_count(&mut self, ctx: Context<SetCount>, new_count: u64) -> Result<()> {
|
||||
// Ask the auth program if we should approve the transaction.
|
||||
let cpi_program = ctx.accounts.auth_program.clone();
|
||||
let cpi_ctx = CpiContext::new(cpi_program, Empty {});
|
||||
auth::is_authorized(cpi_ctx, self.count, new_count)?;
|
||||
|
||||
// Approved, so update.
|
||||
self.count = new_count;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct Empty {}
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct SetCount<'info> {
|
||||
auth_program: AccountInfo<'info>,
|
||||
}
|
||||
|
||||
impl<'info> SetCount<'info> {
|
||||
// Auxiliary account validation requiring program inputs. As a convention,
|
||||
// we separate it from the business logic of the instruction handler itself.
|
||||
pub fn accounts(counter: &Counter, ctx: &Context<SetCount>) -> Result<()> {
|
||||
if ctx.accounts.auth_program.key != &counter.auth_program {
|
||||
return Err(ErrorCode::InvalidAuthProgram.into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[interface]
|
||||
pub trait Auth<'info, T: Accounts<'info>> {
|
||||
fn is_authorized(ctx: Context<T>, current: u64, new: u64) -> ProgramResult;
|
||||
}
|
||||
|
||||
#[error]
|
||||
pub enum ErrorCode {
|
||||
#[msg("Invalid auth program.")]
|
||||
InvalidAuthProgram,
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
const anchor = require('@project-serum/anchor');
|
||||
const assert = require("assert");
|
||||
|
||||
describe("interface", () => {
|
||||
// Configure the client to use the local cluster.
|
||||
anchor.setProvider(anchor.Provider.env());
|
||||
|
||||
const counter = anchor.workspace.Counter;
|
||||
const counterAuth = anchor.workspace.CounterAuth;
|
||||
it("Is initialized!", async () => {
|
||||
await counter.state.rpc.new(counterAuth.programId);
|
||||
|
||||
const stateAccount = await counter.state();
|
||||
assert.ok(stateAccount.count.eq(new anchor.BN(0)));
|
||||
assert.ok(stateAccount.authProgram.equals(counterAuth.programId));
|
||||
});
|
||||
|
||||
it("Should fail to go from even to event", async () => {
|
||||
await assert.rejects(
|
||||
async () => {
|
||||
await counter.state.rpc.setCount(new anchor.BN(4), {
|
||||
accounts: {
|
||||
authProgram: counterAuth.programId,
|
||||
},
|
||||
});
|
||||
},
|
||||
(err) => {
|
||||
if (err.toString().split("custom program error: 0x32").length !== 2) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it("Shold succeed to go from even to odd", async () => {
|
||||
await counter.state.rpc.setCount(new anchor.BN(3), {
|
||||
accounts: {
|
||||
authProgram: counterAuth.programId,
|
||||
},
|
||||
});
|
||||
const stateAccount = await counter.state();
|
||||
assert.ok(stateAccount.count.eq(new anchor.BN(3)));
|
||||
});
|
||||
});
|
|
@ -17,6 +17,7 @@ anchor-attribute-account = { path = "./attribute/account", version = "0.1.0" }
|
|||
anchor-attribute-error = { path = "./attribute/error", version = "0.1.0" }
|
||||
anchor-attribute-program = { path = "./attribute/program", version = "0.1.0" }
|
||||
anchor-attribute-state = { path = "./attribute/state", version = "0.1.0" }
|
||||
anchor-attribute-interface = { path = "./attribute/interface", version = "0.1.0" }
|
||||
anchor-derive-accounts = { path = "./derive/accounts", version = "0.1.0" }
|
||||
serum-borsh = "0.8.1-serum.1"
|
||||
solana-program = "=1.5.0"
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
[package]
|
||||
name = "anchor-attribute-interface"
|
||||
version = "0.1.0"
|
||||
authors = ["Serum Foundation <foundation@projectserum.com>"]
|
||||
repository = "https://github.com/project-serum/anchor"
|
||||
license = "Apache-2.0"
|
||||
description = "Attribute for defining a program interface trait"
|
||||
edition = "2018"
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
proc-macro2 = "1.0"
|
||||
quote = "1.0"
|
||||
syn = { version = "=1.0.57", features = ["full"] }
|
||||
anyhow = "1.0.32"
|
||||
anchor-syn = { path = "../../syn", version = "0.1.0" }
|
||||
heck = "0.3.2"
|
|
@ -0,0 +1,120 @@
|
|||
extern crate proc_macro;
|
||||
|
||||
use anchor_syn::parser;
|
||||
use heck::SnakeCase;
|
||||
use quote::quote;
|
||||
use syn::parse_macro_input;
|
||||
|
||||
#[proc_macro_attribute]
|
||||
pub fn interface(
|
||||
_args: proc_macro::TokenStream,
|
||||
input: proc_macro::TokenStream,
|
||||
) -> proc_macro::TokenStream {
|
||||
let item_trait = parse_macro_input!(input as syn::ItemTrait);
|
||||
|
||||
let trait_name = item_trait.ident.to_string();
|
||||
let mod_name: proc_macro2::TokenStream = item_trait
|
||||
.ident
|
||||
.to_string()
|
||||
.to_snake_case()
|
||||
.parse()
|
||||
.unwrap();
|
||||
|
||||
let methods: Vec<proc_macro2::TokenStream> = item_trait
|
||||
.items
|
||||
.iter()
|
||||
.filter_map(|trait_item: &syn::TraitItem| match trait_item {
|
||||
syn::TraitItem::Method(m) => Some(m),
|
||||
_ => None,
|
||||
})
|
||||
.map(|method: &syn::TraitItemMethod| {
|
||||
let method_name = &method.sig.ident;
|
||||
let args: Vec<&syn::PatType> = method
|
||||
.sig
|
||||
.inputs
|
||||
.iter()
|
||||
.filter_map(|arg: &syn::FnArg| match arg {
|
||||
syn::FnArg::Typed(pat_ty) => Some(pat_ty),
|
||||
// TODO: just map this to None once we allow this feature.
|
||||
_ => panic!("Invalid syntax. No self allowed."),
|
||||
})
|
||||
.filter_map(|pat_ty: &syn::PatType| {
|
||||
let mut ty = parser::tts_to_string(&pat_ty.ty);
|
||||
ty.retain(|s| !s.is_whitespace());
|
||||
if ty.starts_with("Context<") {
|
||||
None
|
||||
} else {
|
||||
Some(pat_ty)
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let args_no_tys: Vec<&Box<syn::Pat>> = args
|
||||
.iter()
|
||||
.map(|arg| {
|
||||
&arg.pat
|
||||
})
|
||||
.collect();
|
||||
let args_struct = {
|
||||
if args.len() == 0 {
|
||||
quote! {
|
||||
use anchor_lang::prelude::borsh;
|
||||
#[derive(anchor_lang::AnchorSerialize, anchor_lang::AnchorDeserialize)]
|
||||
struct Args;
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
use anchor_lang::prelude::borsh;
|
||||
#[derive(anchor_lang::AnchorSerialize, anchor_lang::AnchorDeserialize)]
|
||||
struct Args {
|
||||
#(#args),*
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let sighash_arr = anchor_syn::codegen::program::sighash(&trait_name, &method_name.to_string());
|
||||
let sighash_tts: proc_macro2::TokenStream =
|
||||
format!("{:?}", sighash_arr).parse().unwrap();
|
||||
quote! {
|
||||
pub fn #method_name<'a,'b, 'c, 'info, T: anchor_lang::ToAccountMetas + anchor_lang::ToAccountInfos<'info>>(
|
||||
ctx: anchor_lang::CpiContext<'a, 'b, 'c, 'info, T>,
|
||||
#(#args),*
|
||||
) -> anchor_lang::solana_program::entrypoint::ProgramResult {
|
||||
#args_struct
|
||||
|
||||
let ix = {
|
||||
let ix = Args {
|
||||
#(#args_no_tys),*
|
||||
};
|
||||
let mut ix_data = anchor_lang::AnchorSerialize::try_to_vec(&ix)
|
||||
.map_err(|_| anchor_lang::solana_program::program_error::ProgramError::InvalidInstructionData)?;
|
||||
let mut data = #sighash_tts.to_vec();
|
||||
data.append(&mut ix_data);
|
||||
let accounts = ctx.accounts.to_account_metas(None);
|
||||
anchor_lang::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());
|
||||
anchor_lang::solana_program::program::invoke_signed(
|
||||
&ix,
|
||||
&acc_infos,
|
||||
ctx.signer_seeds,
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
proc_macro::TokenStream::from(quote! {
|
||||
#item_trait
|
||||
|
||||
mod #mod_name {
|
||||
use super::*;
|
||||
#(#methods)*
|
||||
}
|
||||
})
|
||||
}
|
|
@ -4,8 +4,8 @@ use anchor_syn::codegen::program as program_codegen;
|
|||
use anchor_syn::parser::program as program_parser;
|
||||
use syn::parse_macro_input;
|
||||
|
||||
/// The module containing all instruction handlers defining all entries to the
|
||||
/// Solana program.
|
||||
/// The `#[program]` attribute defines the module containing all instruction
|
||||
/// handlers defining all entries into a Solana program.
|
||||
#[proc_macro_attribute]
|
||||
pub fn program(
|
||||
_args: proc_macro::TokenStream,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use crate::Accounts;
|
||||
use crate::{Accounts, ToAccountInfos, ToAccountMetas};
|
||||
use solana_program::account_info::AccountInfo;
|
||||
use solana_program::pubkey::Pubkey;
|
||||
|
||||
|
@ -27,13 +27,19 @@ impl<'a, 'b, 'c, 'info, T: Accounts<'info>> Context<'a, 'b, 'c, 'info, T> {
|
|||
}
|
||||
|
||||
/// Context speciying non-argument inputs for cross-program-invocations.
|
||||
pub struct CpiContext<'a, 'b, 'c, 'info, T: Accounts<'info>> {
|
||||
pub struct CpiContext<'a, 'b, 'c, 'info, T>
|
||||
where
|
||||
T: ToAccountMetas + ToAccountInfos<'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> {
|
||||
impl<'a, 'b, 'c, 'info, T> CpiContext<'a, 'b, 'c, 'info, T>
|
||||
where
|
||||
T: ToAccountMetas + ToAccountInfos<'info>,
|
||||
{
|
||||
pub fn new(program: AccountInfo<'info>, accounts: T) -> Self {
|
||||
Self {
|
||||
accounts,
|
||||
|
|
|
@ -39,6 +39,7 @@ pub mod idl;
|
|||
mod program_account;
|
||||
mod state;
|
||||
mod sysvar;
|
||||
mod vec;
|
||||
|
||||
pub use crate::context::{Context, CpiContext};
|
||||
pub use crate::cpi_account::CpiAccount;
|
||||
|
@ -49,6 +50,7 @@ pub use crate::sysvar::Sysvar;
|
|||
pub use anchor_attribute_access_control::access_control;
|
||||
pub use anchor_attribute_account::account;
|
||||
pub use anchor_attribute_error::error;
|
||||
pub use anchor_attribute_interface::interface;
|
||||
pub use anchor_attribute_program::program;
|
||||
pub use anchor_attribute_state::state;
|
||||
pub use anchor_derive_accounts::Accounts;
|
||||
|
@ -68,8 +70,8 @@ pub trait Accounts<'info>: ToAccountMetas + ToAccountInfos<'info> + Sized {
|
|||
/// program dependent. However, users of these types should never have to
|
||||
/// worry about account substitution attacks. For example, if a program
|
||||
/// expects a `Mint` account from the SPL token program in a particular
|
||||
/// field, then it should be impossible for this method to return `Ok` if any
|
||||
/// other account type is given--from the SPL token program or elsewhere.
|
||||
/// field, then it should be impossible for this method to return `Ok` if
|
||||
/// any other account type is given--from the SPL token program or elsewhere.
|
||||
///
|
||||
/// `program_id` is the currently executing program. `accounts` is the
|
||||
/// set of accounts to construct the type from. For every account used,
|
||||
|
@ -171,9 +173,9 @@ pub trait InstructionData: AnchorSerialize {
|
|||
/// All programs should include it via `anchor_lang::prelude::*;`.
|
||||
pub mod prelude {
|
||||
pub use super::{
|
||||
access_control, account, error, program, state, AccountDeserialize, AccountSerialize,
|
||||
Accounts, AccountsExit, AccountsInit, AnchorDeserialize, AnchorSerialize, Context,
|
||||
CpiAccount, CpiContext, Ctor, ProgramAccount, ProgramState, Sysvar, ToAccountInfo,
|
||||
access_control, account, error, interface, program, state, AccountDeserialize,
|
||||
AccountSerialize, Accounts, AccountsExit, AccountsInit, AnchorDeserialize, AnchorSerialize,
|
||||
Context, CpiAccount, CpiContext, Ctor, ProgramAccount, ProgramState, Sysvar, ToAccountInfo,
|
||||
ToAccountInfos, ToAccountMetas,
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
use crate::{ToAccountInfos, ToAccountMetas};
|
||||
use solana_program::account_info::AccountInfo;
|
||||
use solana_program::instruction::AccountMeta;
|
||||
|
||||
impl<'info, T: ToAccountInfos<'info>> ToAccountInfos<'info> for Vec<T> {
|
||||
fn to_account_infos(&self) -> Vec<AccountInfo<'info>> {
|
||||
self.iter()
|
||||
.flat_map(|item| item.to_account_infos())
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ToAccountMetas> ToAccountMetas for Vec<T> {
|
||||
fn to_account_metas(&self, is_signer: Option<bool>) -> Vec<AccountMeta> {
|
||||
self.iter()
|
||||
.flat_map(|item| (*item).to_account_metas(is_signer))
|
||||
.collect()
|
||||
}
|
||||
}
|
|
@ -37,5 +37,12 @@ pub fn generate(error: Error) -> proc_macro2::TokenStream {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::From<#enum_name> for ProgramError {
|
||||
fn from(e: #enum_name) -> ProgramError {
|
||||
let err: Error = e.into();
|
||||
err.into()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,7 +41,7 @@ pub fn generate(program: Program) -> proc_macro2::TokenStream {
|
|||
|
||||
if cfg!(not(feature = "no-idl")) {
|
||||
if sighash == anchor_lang::idl::IDL_IX_TAG.to_le_bytes() {
|
||||
return __private::__idl(program_id, accounts, &instruction_data[8..]);
|
||||
return __private::__idl(program_id, accounts, &instruction_data);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -66,6 +66,7 @@ pub fn generate(program: Program) -> proc_macro2::TokenStream {
|
|||
}
|
||||
|
||||
pub fn generate_dispatch(program: &Program) -> proc_macro2::TokenStream {
|
||||
// Dispatch the state constructor.
|
||||
let ctor_state_dispatch_arm = match &program.state {
|
||||
None => quote! { /* no-op */ },
|
||||
Some(state) => {
|
||||
|
@ -85,6 +86,8 @@ pub fn generate_dispatch(program: &Program) -> proc_macro2::TokenStream {
|
|||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Dispatch the state impl instructions.
|
||||
let state_dispatch_arms: Vec<proc_macro2::TokenStream> = match &program.state {
|
||||
None => vec![],
|
||||
Some(s) => s
|
||||
|
@ -112,6 +115,63 @@ pub fn generate_dispatch(program: &Program) -> proc_macro2::TokenStream {
|
|||
})
|
||||
.collect(),
|
||||
};
|
||||
|
||||
// Dispatch all trait interface implementations.
|
||||
let trait_dispatch_arms: Vec<proc_macro2::TokenStream> = match &program.state {
|
||||
None => vec![],
|
||||
Some(s) => s
|
||||
.interfaces
|
||||
.iter()
|
||||
.flat_map(|iface: &crate::StateInterface| {
|
||||
iface
|
||||
.methods
|
||||
.iter()
|
||||
.map(|m: &crate::StateRpc| {
|
||||
let rpc_arg_names: Vec<&syn::Ident> =
|
||||
m.args.iter().map(|arg| &arg.name).collect();
|
||||
let name = &m.raw_method.sig.ident.to_string();
|
||||
let rpc_name: proc_macro2::TokenStream = format!("__{}_{}", iface.trait_name, name).parse().unwrap();
|
||||
let raw_args: Vec<&syn::PatType> = m
|
||||
.args
|
||||
.iter()
|
||||
.map(|arg: &crate::RpcArg| &arg.raw_arg)
|
||||
.collect();
|
||||
let sighash_arr = sighash(&iface.trait_name, &m.ident.to_string());
|
||||
let sighash_tts: proc_macro2::TokenStream =
|
||||
format!("{:?}", sighash_arr).parse().unwrap();
|
||||
let args_struct = {
|
||||
if m.args.len() == 0 {
|
||||
quote! {
|
||||
#[derive(anchor_lang::AnchorSerialize, anchor_lang::AnchorDeserialize)]
|
||||
struct Args;
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
#[derive(anchor_lang::AnchorSerialize, anchor_lang::AnchorDeserialize)]
|
||||
struct Args {
|
||||
#(#raw_args),*
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
quote! {
|
||||
#sighash_tts => {
|
||||
#args_struct
|
||||
let ix = Args::deserialize(&mut instruction_data)
|
||||
.map_err(|_| ProgramError::Custom(1))?; // todo: error code
|
||||
let Args {
|
||||
#(#rpc_arg_names),*
|
||||
} = ix;
|
||||
__private::#rpc_name(program_id, accounts, #(#rpc_arg_names),*)
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect::<Vec<proc_macro2::TokenStream>>()
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
|
||||
// Dispatch all global instructions.
|
||||
let dispatch_arms: Vec<proc_macro2::TokenStream> = program
|
||||
.rpcs
|
||||
.iter()
|
||||
|
@ -139,6 +199,7 @@ pub fn generate_dispatch(program: &Program) -> proc_macro2::TokenStream {
|
|||
match sighash {
|
||||
#ctor_state_dispatch_arm
|
||||
#(#state_dispatch_arms)*
|
||||
#(#trait_dispatch_arms)*
|
||||
#(#dispatch_arms)*
|
||||
_ => {
|
||||
msg!("Fallback functions are not supported. If you have a use case, please file an issue.");
|
||||
|
@ -166,7 +227,7 @@ pub fn generate_non_inlined_handlers(program: &Program) -> proc_macro2::TokenStr
|
|||
let mut data: &[u8] = idl_ix_data;
|
||||
|
||||
let ix = anchor_lang::idl::IdlInstruction::deserialize(&mut data)
|
||||
.map_err(|_| ProgramError::Custom(1))?; // todo
|
||||
.map_err(|_| ProgramError::Custom(2))?; // todo
|
||||
|
||||
match ix {
|
||||
anchor_lang::idl::IdlInstruction::Create { data_len } => {
|
||||
|
@ -419,6 +480,101 @@ pub fn generate_non_inlined_handlers(program: &Program) -> proc_macro2::TokenStr
|
|||
})
|
||||
.collect(),
|
||||
};
|
||||
let non_inlined_state_trait_handlers: Vec<proc_macro2::TokenStream> = match &program.state {
|
||||
None => Vec::new(),
|
||||
Some(state) => state
|
||||
.interfaces
|
||||
.iter()
|
||||
.flat_map(|iface: &crate::StateInterface| {
|
||||
iface
|
||||
.methods
|
||||
.iter()
|
||||
.map(|rpc| {
|
||||
let rpc_params: Vec<_> = rpc.args.iter().map(|arg| &arg.raw_arg).collect();
|
||||
let rpc_arg_names: Vec<&syn::Ident> =
|
||||
rpc.args.iter().map(|arg| &arg.name).collect();
|
||||
let private_rpc_name: proc_macro2::TokenStream = {
|
||||
let n = format!("__{}_{}", iface.trait_name, &rpc.raw_method.sig.ident.to_string());
|
||||
n.parse().unwrap()
|
||||
};
|
||||
let rpc_name = &rpc.raw_method.sig.ident;
|
||||
let state_ty: proc_macro2::TokenStream = state.name.parse().unwrap();
|
||||
let anchor_ident = &rpc.anchor_ident;
|
||||
|
||||
if rpc.has_receiver {
|
||||
quote! {
|
||||
#[inline(never)]
|
||||
pub fn #private_rpc_name(
|
||||
program_id: &Pubkey,
|
||||
accounts: &[AccountInfo],
|
||||
#(#rpc_params),*
|
||||
) -> ProgramResult {
|
||||
|
||||
let mut remaining_accounts: &[AccountInfo] = accounts;
|
||||
if remaining_accounts.len() == 0 {
|
||||
return Err(ProgramError::Custom(1)); // todo
|
||||
}
|
||||
|
||||
// Deserialize the program state account.
|
||||
let state_account = &remaining_accounts[0];
|
||||
let mut state: #state_ty = {
|
||||
let data = state_account.try_borrow_data()?;
|
||||
let mut sliced: &[u8] = &data;
|
||||
anchor_lang::AccountDeserialize::try_deserialize(&mut sliced)?
|
||||
};
|
||||
|
||||
remaining_accounts = &remaining_accounts[1..];
|
||||
|
||||
// Deserialize the program's execution context.
|
||||
let mut accounts = #anchor_ident::try_accounts(
|
||||
program_id,
|
||||
&mut remaining_accounts,
|
||||
)?;
|
||||
let ctx = Context::new(program_id, &mut accounts, remaining_accounts);
|
||||
|
||||
// Execute user defined function.
|
||||
state.#rpc_name(
|
||||
ctx,
|
||||
#(#rpc_arg_names),*
|
||||
)?;
|
||||
|
||||
// Serialize the state and save it to storage.
|
||||
accounts.exit(program_id)?;
|
||||
let mut data = state_account.try_borrow_mut_data()?;
|
||||
let dst: &mut [u8] = &mut data;
|
||||
let mut cursor = std::io::Cursor::new(dst);
|
||||
state.try_serialize(&mut cursor)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let state_name: proc_macro2::TokenStream = state.name.parse().unwrap();
|
||||
quote! {
|
||||
#[inline(never)]
|
||||
pub fn #private_rpc_name(
|
||||
program_id: &Pubkey,
|
||||
accounts: &[AccountInfo],
|
||||
#(#rpc_params),*
|
||||
) -> ProgramResult {
|
||||
let mut remaining_accounts: &[AccountInfo] = accounts;
|
||||
let mut accounts = #anchor_ident::try_accounts(
|
||||
program_id,
|
||||
&mut remaining_accounts,
|
||||
)?;
|
||||
#state_name::#rpc_name(
|
||||
Context::new(program_id, &mut accounts, remaining_accounts),
|
||||
#(#rpc_arg_names),*
|
||||
)?;
|
||||
accounts.exit(program_id)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect::<Vec<proc_macro2::TokenStream>>()
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
let non_inlined_handlers: Vec<proc_macro2::TokenStream> = program
|
||||
.rpcs
|
||||
.iter()
|
||||
|
@ -451,6 +607,7 @@ pub fn generate_non_inlined_handlers(program: &Program) -> proc_macro2::TokenStr
|
|||
#non_inlined_idl
|
||||
#non_inlined_ctor
|
||||
#(#non_inlined_state_handlers)*
|
||||
#(#non_inlined_state_trait_handlers)*
|
||||
#(#non_inlined_handlers)*
|
||||
}
|
||||
}
|
||||
|
@ -479,7 +636,14 @@ pub fn generate_ctor_typed_variant_with_semi(program: &Program) -> proc_macro2::
|
|||
match &program.state {
|
||||
None => quote! {},
|
||||
Some(state) => {
|
||||
let ctor_args = generate_ctor_typed_args(state);
|
||||
let ctor_args: Vec<proc_macro2::TokenStream> = generate_ctor_typed_args(state)
|
||||
.iter()
|
||||
.map(|arg| {
|
||||
format!("pub {}", parser::tts_to_string(&arg))
|
||||
.parse()
|
||||
.unwrap()
|
||||
})
|
||||
.collect();
|
||||
if ctor_args.len() == 0 {
|
||||
quote! {
|
||||
#[derive(AnchorSerialize, AnchorDeserialize)]
|
||||
|
@ -490,7 +654,7 @@ pub fn generate_ctor_typed_variant_with_semi(program: &Program) -> proc_macro2::
|
|||
#[derive(AnchorSerialize, AnchorDeserialize)]
|
||||
pub struct __Ctor {
|
||||
#(#ctor_args),*
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -821,7 +985,7 @@ fn generate_cpi(program: &Program) -> proc_macro2::TokenStream {
|
|||
// Rust doesn't have method overloading so no need to use the arguments.
|
||||
// However, we do namespace methods in the preeimage so that we can use
|
||||
// different traits with the same method name.
|
||||
fn sighash(namespace: &str, name: &str) -> [u8; 8] {
|
||||
pub fn sighash(namespace: &str, name: &str) -> [u8; 8] {
|
||||
let preimage = format!("{}::{}", namespace, name);
|
||||
|
||||
let mut sighash = [0u8; 8];
|
||||
|
|
|
@ -32,6 +32,7 @@ pub struct State {
|
|||
pub strct: syn::ItemStruct,
|
||||
pub impl_block: syn::ItemImpl,
|
||||
pub methods: Vec<StateRpc>,
|
||||
pub interfaces: Vec<StateInterface>,
|
||||
pub ctor: syn::ImplItemMethod,
|
||||
pub ctor_anchor: syn::Ident, // TODO: consolidate this with ctor above.
|
||||
}
|
||||
|
@ -42,6 +43,14 @@ pub struct StateRpc {
|
|||
pub ident: syn::Ident,
|
||||
pub args: Vec<RpcArg>,
|
||||
pub anchor_ident: syn::Ident,
|
||||
// True if there exists a &self on the method.
|
||||
pub has_receiver: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct StateInterface {
|
||||
pub trait_name: String,
|
||||
pub methods: Vec<StateRpc>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use crate::parser;
|
||||
use crate::{Program, Rpc, RpcArg, State, StateRpc};
|
||||
use crate::{Program, Rpc, RpcArg, State, StateInterface, StateRpc};
|
||||
|
||||
pub fn parse(program_mod: syn::ItemMod) -> Program {
|
||||
let mod_ident = &program_mod.ident;
|
||||
|
@ -28,12 +28,15 @@ pub fn parse(program_mod: syn::ItemMod) -> Program {
|
|||
.next();
|
||||
|
||||
let impl_block: Option<&syn::ItemImpl> = strct.map(|strct| {
|
||||
let item_impl = mod_content
|
||||
let item_impls = mod_content
|
||||
.iter()
|
||||
.filter_map(|item| match item {
|
||||
syn::Item::Impl(item_impl) => {
|
||||
let impl_ty_str = parser::tts_to_string(&item_impl.self_ty);
|
||||
let strct_name = strct.ident.to_string();
|
||||
if item_impl.trait_.is_some() {
|
||||
return None;
|
||||
}
|
||||
if strct_name != impl_ty_str {
|
||||
return None;
|
||||
}
|
||||
|
@ -41,9 +44,39 @@ pub fn parse(program_mod: syn::ItemMod) -> Program {
|
|||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<&syn::ItemImpl>>();
|
||||
item_impls[0]
|
||||
});
|
||||
|
||||
// All program interface implementations.
|
||||
let trait_impls: Option<Vec<StateInterface>> = strct.map(|_strct| {
|
||||
mod_content
|
||||
.iter()
|
||||
.filter_map(|item| match item {
|
||||
syn::Item::Impl(item_impl) => {
|
||||
let trait_name = match &item_impl.trait_ {
|
||||
None => return None,
|
||||
Some((_, path, _)) => path
|
||||
.segments
|
||||
.iter()
|
||||
.next()
|
||||
.expect("Must provide an implementation");
|
||||
item_impl
|
||||
.expect("Must have one segmeent in a path")
|
||||
.ident
|
||||
.clone()
|
||||
.to_string(),
|
||||
};
|
||||
if item_impl.trait_.is_none() {
|
||||
return None;
|
||||
}
|
||||
let methods = parse_state_trait_methods(item_impl);
|
||||
Some(StateInterface {
|
||||
trait_name,
|
||||
methods,
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<StateInterface>>()
|
||||
});
|
||||
|
||||
strct.map(|strct| {
|
||||
|
@ -112,6 +145,7 @@ pub fn parse(program_mod: syn::ItemMod) -> Program {
|
|||
ident: m.sig.ident.clone(),
|
||||
args,
|
||||
anchor_ident,
|
||||
has_receiver: true,
|
||||
})
|
||||
}
|
||||
},
|
||||
|
@ -122,6 +156,7 @@ pub fn parse(program_mod: syn::ItemMod) -> Program {
|
|||
State {
|
||||
name: strct.ident.to_string(),
|
||||
strct: strct.clone(),
|
||||
interfaces: trait_impls.expect("Some if state exists"),
|
||||
impl_block,
|
||||
ctor,
|
||||
ctor_anchor,
|
||||
|
@ -206,3 +241,52 @@ fn extract_ident(path_ty: &syn::PatType) -> &proc_macro2::Ident {
|
|||
};
|
||||
&path.segments[0].ident
|
||||
}
|
||||
|
||||
fn parse_state_trait_methods(item_impl: &syn::ItemImpl) -> Vec<StateRpc> {
|
||||
item_impl
|
||||
.items
|
||||
.iter()
|
||||
.filter_map(|item: &syn::ImplItem| match item {
|
||||
syn::ImplItem::Method(m) => match m.sig.inputs.first() {
|
||||
None => None,
|
||||
Some(_arg) => {
|
||||
let mut has_receiver = false;
|
||||
let mut args = m
|
||||
.sig
|
||||
.inputs
|
||||
.iter()
|
||||
.filter_map(|arg| match arg {
|
||||
syn::FnArg::Receiver(_) => {
|
||||
has_receiver = true;
|
||||
None
|
||||
}
|
||||
syn::FnArg::Typed(arg) => Some(arg),
|
||||
})
|
||||
.map(|raw_arg| {
|
||||
let ident = match &*raw_arg.pat {
|
||||
syn::Pat::Ident(ident) => &ident.ident,
|
||||
_ => panic!("invalid syntax"),
|
||||
};
|
||||
RpcArg {
|
||||
name: ident.clone(),
|
||||
raw_arg: raw_arg.clone(),
|
||||
}
|
||||
})
|
||||
.collect::<Vec<RpcArg>>();
|
||||
// Remove the Anchor accounts argument
|
||||
let anchor = args.remove(0);
|
||||
let anchor_ident = extract_ident(&anchor.raw_arg).clone();
|
||||
|
||||
Some(StateRpc {
|
||||
raw_method: m.clone(),
|
||||
ident: m.sig.ident.clone(),
|
||||
args,
|
||||
anchor_ident,
|
||||
has_receiver,
|
||||
})
|
||||
}
|
||||
},
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@project-serum/anchor",
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.0-beta.1",
|
||||
"description": "Anchor client",
|
||||
"main": "dist/cjs/index.js",
|
||||
"module": "dist/esm/index.js",
|
||||
|
@ -27,11 +27,13 @@
|
|||
"@solana/web3.js": "^0.90.4",
|
||||
"@types/bn.js": "^4.11.6",
|
||||
"@types/bs58": "^4.0.1",
|
||||
"@types/crypto-hash": "^1.1.2",
|
||||
"@types/pako": "^1.0.1",
|
||||
"bn.js": "^5.1.2",
|
||||
"bs58": "^4.0.1",
|
||||
"buffer-layout": "^1.2.0",
|
||||
"camelcase": "^5.3.1",
|
||||
"crypto-hash": "^1.3.0",
|
||||
"eventemitter3": "^4.0.7",
|
||||
"find": "^0.3.0",
|
||||
"js-sha256": "^0.9.0",
|
||||
|
|
|
@ -356,7 +356,7 @@ export async function stateDiscriminator(name: string): Promise<Buffer> {
|
|||
|
||||
// Returns the size of the type in bytes. For variable length types, just return
|
||||
// 1. Users should override this value in such cases.
|
||||
export function typeSize(idl: Idl, ty: IdlType): number {
|
||||
function typeSize(idl: Idl, ty: IdlType): number {
|
||||
switch (ty) {
|
||||
case "bool":
|
||||
return 1;
|
||||
|
@ -386,7 +386,7 @@ export function typeSize(idl: Idl, ty: IdlType): number {
|
|||
// @ts-ignore
|
||||
if (ty.option !== undefined) {
|
||||
// @ts-ignore
|
||||
return 1 + typeSize(ty.option);
|
||||
return 1 + typeSize(idl, ty.option);
|
||||
}
|
||||
// @ts-ignore
|
||||
if (ty.defined !== undefined) {
|
||||
|
|
|
@ -753,6 +753,13 @@
|
|||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/crypto-hash@^1.1.2":
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/crypto-hash/-/crypto-hash-1.1.2.tgz#5a993deb0e6ba7c42f86eaa65d9bf563378f4569"
|
||||
integrity sha512-sOmi+4Go2XKodLV4+lfP+5QMQ+6ZYqRJhK8D/n6xsxIUvlerEulmU9S4Lo02pXCH3qPBeJXEy+g8ZERktDJLSg==
|
||||
dependencies:
|
||||
crypto-hash "*"
|
||||
|
||||
"@types/express-serve-static-core@^4.17.9":
|
||||
version "4.17.18"
|
||||
resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.18.tgz#8371e260f40e0e1ca0c116a9afcd9426fa094c40"
|
||||
|
@ -1687,7 +1694,7 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.2:
|
|||
shebang-command "^2.0.0"
|
||||
which "^2.0.1"
|
||||
|
||||
crypto-hash@^1.2.2:
|
||||
crypto-hash@*, crypto-hash@^1.2.2, crypto-hash@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/crypto-hash/-/crypto-hash-1.3.0.tgz#b402cb08f4529e9f4f09346c3e275942f845e247"
|
||||
integrity sha512-lyAZ0EMyjDkVvz8WOeVnuCPvKVBXcMv1l5SVqO1yC7PzTwrD/pPje/BIRbWhMoPe436U+Y2nD7f5bFx0kt+Sbg==
|
||||
|
|
Loading…
Reference in New Issue