浏览代码

Program interfaces

Armani Ferrante 4 年之前
父节点
当前提交
1f31770c83

+ 1 - 0
.travis.yml

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

+ 6 - 0
CHANGELOG.md

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

+ 13 - 0
Cargo.lock

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

+ 2 - 0
examples/interface/Anchor.toml

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

+ 4 - 0
examples/interface/Cargo.toml

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

+ 19 - 0
examples/interface/programs/counter-auth/Cargo.toml

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

+ 2 - 0
examples/interface/programs/counter-auth/Xargo.toml

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

+ 43 - 0
examples/interface/programs/counter-auth/src/lib.rs

@@ -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 {}

+ 18 - 0
examples/interface/programs/counter/Cargo.toml

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

+ 2 - 0
examples/interface/programs/counter/Xargo.toml

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

+ 73 - 0
examples/interface/programs/counter/src/lib.rs

@@ -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,
+}

+ 45 - 0
examples/interface/tests/interface.js

@@ -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)));
+  });
+});

+ 1 - 0
lang/Cargo.toml

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

+ 19 - 0
lang/attribute/interface/Cargo.toml

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

+ 120 - 0
lang/attribute/interface/src/lib.rs

@@ -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)*
+        }
+    })
+}

+ 2 - 2
lang/attribute/program/src/lib.rs

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

+ 9 - 3
lang/src/context.rs

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

+ 7 - 5
lang/src/lib.rs

@@ -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,
     };
 

+ 19 - 0
lang/src/vec.rs

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

+ 7 - 0
lang/syn/src/codegen/error.rs

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

+ 169 - 5
lang/syn/src/codegen/program.rs

@@ -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];

+ 9 - 0
lang/syn/src/lib.rs

@@ -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)]

+ 89 - 5
lang/syn/src/parser/program.rs

@@ -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,
                 })
-                .next()
-                .expect("Must provide an implementation");
-            item_impl
+                .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 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()
+}

+ 3 - 1
ts/package.json

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

+ 2 - 2
ts/src/coder.ts

@@ -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) {

+ 8 - 1
ts/yarn.lock

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