Browse Source

Merge pull request #66 from project-serum/armani/realize

Program interfaces
Armani Ferrante 4 years ago
parent
commit
a903d48e1f

+ 1 - 0
.travis.yml

@@ -49,6 +49,7 @@ jobs:
         - pushd examples/errors && anchor test && popd
         - pushd examples/errors && anchor test && popd
         - pushd examples/spl/token-proxy && anchor test && popd
         - pushd examples/spl/token-proxy && anchor test && popd
         - pushd examples/multisig && 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-0 && anchor test && popd
         - pushd examples/tutorial/basic-1 && anchor test && popd
         - pushd examples/tutorial/basic-1 && anchor test && popd
         - pushd examples/tutorial/basic-2 && anchor test && popd
         - pushd examples/tutorial/basic-2 && anchor test && popd

+ 6 - 0
CHANGELOG.md

@@ -11,6 +11,12 @@ incremented for features.
 
 
 ## [Unreleased]
 ## [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).
 * 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
 ## [0.1.0] - 2021-01-31

+ 13 - 0
Cargo.lock

@@ -81,6 +81,18 @@ dependencies = [
  "syn 1.0.57",
  "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]]
 [[package]]
 name = "anchor-attribute-program"
 name = "anchor-attribute-program"
 version = "0.1.0"
 version = "0.1.0"
@@ -155,6 +167,7 @@ dependencies = [
  "anchor-attribute-access-control",
  "anchor-attribute-access-control",
  "anchor-attribute-account",
  "anchor-attribute-account",
  "anchor-attribute-error",
  "anchor-attribute-error",
+ "anchor-attribute-interface",
  "anchor-attribute-program",
  "anchor-attribute-program",
  "anchor-attribute-state",
  "anchor-attribute-state",
  "anchor-derive-accounts",
  "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)));
+  });
+});

+ 66 - 1
examples/lockup/programs/lockup/src/lib.rs

@@ -74,6 +74,7 @@ pub mod lockup {
         period_count: u64,
         period_count: u64,
         deposit_amount: u64,
         deposit_amount: u64,
         nonce: u8,
         nonce: u8,
+        realizor: Option<Realizor>,
     ) -> Result<()> {
     ) -> Result<()> {
         if end_ts <= ctx.accounts.clock.unix_timestamp {
         if end_ts <= ctx.accounts.clock.unix_timestamp {
             return Err(ErrorCode::InvalidTimestamp.into());
             return Err(ErrorCode::InvalidTimestamp.into());
@@ -100,12 +101,14 @@ pub mod lockup {
         vesting.whitelist_owned = 0;
         vesting.whitelist_owned = 0;
         vesting.grantor = *ctx.accounts.depositor_authority.key;
         vesting.grantor = *ctx.accounts.depositor_authority.key;
         vesting.nonce = nonce;
         vesting.nonce = nonce;
+        vesting.realizor = realizor;
 
 
         token::transfer(ctx.accounts.into(), deposit_amount)?;
         token::transfer(ctx.accounts.into(), deposit_amount)?;
 
 
         Ok(())
         Ok(())
     }
     }
 
 
+    #[access_control(is_realized(&ctx))]
     pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<()> {
     pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<()> {
         // Has the given amount vested?
         // Has the given amount vested?
         if amount
         if amount
@@ -187,7 +190,7 @@ pub mod lockup {
         Ok(())
         Ok(())
     }
     }
 
 
-    // Convenience function for UI's to calculate the withdrawalable amount.
+    // Convenience function for UI's to calculate the withdrawable amount.
     pub fn available_for_withdrawal(ctx: Context<AvailableForWithdrawal>) -> Result<()> {
     pub fn available_for_withdrawal(ctx: Context<AvailableForWithdrawal>) -> Result<()> {
         let available = calculator::available_for_withdrawal(
         let available = calculator::available_for_withdrawal(
             &ctx.accounts.vesting,
             &ctx.accounts.vesting,
@@ -242,6 +245,8 @@ impl<'info> CreateVesting<'info> {
     }
     }
 }
 }
 
 
+// All accounts not included here, i.e., the "remaining accounts" should be
+// ordered according to the realization interface.
 #[derive(Accounts)]
 #[derive(Accounts)]
 pub struct Withdraw<'info> {
 pub struct Withdraw<'info> {
     // Vesting.
     // Vesting.
@@ -327,6 +332,29 @@ pub struct Vesting {
     pub whitelist_owned: u64,
     pub whitelist_owned: u64,
     /// Signer nonce.
     /// Signer nonce.
     pub nonce: u8,
     pub nonce: u8,
+    /// The program that determines when the locked account is **realized**.
+    /// In addition to the lockup schedule, the program provides the ability
+    /// for applications to determine when locked tokens are considered earned.
+    /// For example, when earning locked tokens via the staking program, one
+    /// cannot receive the tokens until unstaking. As a result, if one never
+    /// unstakes, one would never actually receive the locked tokens.
+    pub realizor: Option<Realizor>,
+}
+
+#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)]
+pub struct Realizor {
+    /// Program to invoke to check a realization condition. This program must
+    /// implement the `RealizeLock` trait.
+    pub program: Pubkey,
+    /// Address of an arbitrary piece of metadata interpretable by the realizor
+    /// program. For example, when a vesting account is allocated, the program
+    /// can define its realization condition as a function of some account
+    /// state. The metadata is the address of that account.
+    ///
+    /// In the case of staking, the metadata is a `Member` account address. When
+    /// the realization condition is checked, the staking program will check the
+    /// `Member` account defined by the `metadata` has no staked tokens.
+    pub metadata: Pubkey,
 }
 }
 
 
 #[derive(AnchorSerialize, AnchorDeserialize, PartialEq, Default, Copy, Clone)]
 #[derive(AnchorSerialize, AnchorDeserialize, PartialEq, Default, Copy, Clone)]
@@ -366,6 +394,12 @@ pub enum ErrorCode {
     WhitelistEntryNotFound,
     WhitelistEntryNotFound,
     #[msg("You do not have sufficient permissions to perform this action.")]
     #[msg("You do not have sufficient permissions to perform this action.")]
     Unauthorized,
     Unauthorized,
+    #[msg("You are unable to realize projected rewards until unstaking.")]
+    UnableToWithdrawWhileStaked,
+    #[msg("The given lock realizor doesn't match the vesting account.")]
+    InvalidLockRealizor,
+    #[msg("You have not realized this vesting account.")]
+    UnrealizedVesting,
 }
 }
 
 
 impl<'a, 'b, 'c, 'info> From<&mut CreateVesting<'info>>
 impl<'a, 'b, 'c, 'info> From<&mut CreateVesting<'info>>
@@ -456,3 +490,34 @@ fn whitelist_auth(lockup: &Lockup, ctx: &Context<Auth>) -> Result<()> {
     }
     }
     Ok(())
     Ok(())
 }
 }
+
+// Returns Ok if the locked vesting account has been "realized". Realization
+// is application dependent. For example, in the case of staking, one must first
+// unstake before being able to earn locked tokens.
+fn is_realized<'info>(ctx: &Context<Withdraw>) -> Result<()> {
+    if let Some(realizor) = &ctx.accounts.vesting.realizor {
+        let cpi_program = {
+            let p = ctx.remaining_accounts[0].clone();
+            if p.key != &realizor.program {
+                return Err(ErrorCode::InvalidLockRealizor.into());
+            }
+            p
+        };
+        let cpi_accounts = ctx.remaining_accounts.to_vec()[1..].to_vec();
+        let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts);
+        let vesting = (*ctx.accounts.vesting).clone();
+        realize_lock::is_realized(cpi_ctx, vesting).map_err(|_| ErrorCode::UnrealizedVesting)?;
+    }
+    Ok(())
+}
+
+/// RealizeLock defines the interface an external program must implement if
+/// they want to define a "realization condition" on a locked vesting account.
+/// This condition must be satisfied *even if a vesting schedule has
+/// completed*. Otherwise the user can never earn the locked funds. For example,
+/// in the case of the staking program, one cannot received a locked reward
+/// until one has completely unstaked.
+#[interface]
+pub trait RealizeLock<'info, T: Accounts<'info>> {
+    fn is_realized(ctx: Context<T>, v: Vesting) -> ProgramResult;
+}

+ 56 - 8
examples/lockup/programs/registry/src/lib.rs

@@ -6,7 +6,7 @@
 use anchor_lang::prelude::*;
 use anchor_lang::prelude::*;
 use anchor_lang::solana_program::program_option::COption;
 use anchor_lang::solana_program::program_option::COption;
 use anchor_spl::token::{self, Mint, TokenAccount, Transfer};
 use anchor_spl::token::{self, Mint, TokenAccount, Transfer};
-use lockup::{CreateVesting, Vesting};
+use lockup::{CreateVesting, RealizeLock, Realizor, Vesting};
 use std::convert::Into;
 use std::convert::Into;
 
 
 #[program]
 #[program]
@@ -26,6 +26,23 @@ mod registry {
         }
         }
     }
     }
 
 
+    impl<'info> RealizeLock<'info, IsRealized<'info>> for Registry {
+        fn is_realized(ctx: Context<IsRealized>, v: Vesting) -> ProgramResult {
+            if let Some(realizor) = &v.realizor {
+                if &realizor.metadata != ctx.accounts.member.to_account_info().key {
+                    return Err(ErrorCode::InvalidRealizorMetadata.into());
+                }
+                assert!(ctx.accounts.member.beneficiary == v.beneficiary);
+                let total_staked =
+                    ctx.accounts.member_spt.amount + ctx.accounts.member_spt_locked.amount;
+                if total_staked != 0 {
+                    return Err(ErrorCode::UnrealizedReward.into());
+                }
+            }
+            Ok(())
+        }
+    }
+
     #[access_control(Initialize::accounts(&ctx, nonce))]
     #[access_control(Initialize::accounts(&ctx, nonce))]
     pub fn initialize(
     pub fn initialize(
         ctx: Context<Initialize>,
         ctx: Context<Initialize>,
@@ -435,14 +452,27 @@ mod registry {
             .unwrap();
             .unwrap();
         assert!(reward_amount > 0);
         assert!(reward_amount > 0);
 
 
-        // Lockup program requires the timestamp to be >= clock's timestamp.
-        // So update if the time has already passed. 60 seconds is arbitrary.
-        let end_ts = match end_ts > ctx.accounts.cmn.clock.unix_timestamp + 60 {
-            true => end_ts,
-            false => ctx.accounts.cmn.clock.unix_timestamp + 60,
+        // The lockup program requires the timestamp to be >= clock's timestamp.
+        // So update if the time has already passed.
+        //
+        // If the reward is within `period_count` seconds of fully vesting, then
+        // we bump the `end_ts` because, otherwise, the vesting account would
+        // fail to be created. Vesting must have no more frequently than the
+        // smallest unit of time, once per second, expressed as
+        // `period_count <= end_ts - start_ts`.
+        let end_ts = match end_ts < ctx.accounts.cmn.clock.unix_timestamp + period_count as i64 {
+            true => ctx.accounts.cmn.clock.unix_timestamp + period_count as i64,
+            false => end_ts,
         };
         };
 
 
-        // Create lockup account for the member's beneficiary.
+        // Specify the vesting account's realizor, so that unlocks can only
+        // execute once completely unstaked.
+        let realizor = Some(Realizor {
+            program: *ctx.program_id,
+            metadata: *ctx.accounts.cmn.member.to_account_info().key,
+        });
+
+        // CPI: Create lockup account for the member's beneficiary.
         let seeds = &[
         let seeds = &[
             ctx.accounts.cmn.registrar.to_account_info().key.as_ref(),
             ctx.accounts.cmn.registrar.to_account_info().key.as_ref(),
             ctx.accounts.cmn.vendor.to_account_info().key.as_ref(),
             ctx.accounts.cmn.vendor.to_account_info().key.as_ref(),
@@ -461,9 +491,10 @@ mod registry {
             period_count,
             period_count,
             reward_amount,
             reward_amount,
             nonce,
             nonce,
+            realizor,
         )?;
         )?;
 
 
-        // Update the member account.
+        // Make sure this reward can't be processed more than once.
         let member = &mut ctx.accounts.cmn.member;
         let member = &mut ctx.accounts.cmn.member;
         member.rewards_cursor = ctx.accounts.cmn.vendor.reward_event_q_cursor + 1;
         member.rewards_cursor = ctx.accounts.cmn.vendor.reward_event_q_cursor + 1;
 
 
@@ -609,6 +640,17 @@ pub struct Ctor<'info> {
     lockup_program: AccountInfo<'info>,
     lockup_program: AccountInfo<'info>,
 }
 }
 
 
+#[derive(Accounts)]
+pub struct IsRealized<'info> {
+    #[account(
+        "&member.balances.spt == member_spt.to_account_info().key",
+        "&member.balances_locked.spt == member_spt_locked.to_account_info().key"
+    )]
+    member: ProgramAccount<'info, Member>,
+    member_spt: CpiAccount<'info, TokenAccount>,
+    member_spt_locked: CpiAccount<'info, TokenAccount>,
+}
+
 #[derive(Accounts)]
 #[derive(Accounts)]
 pub struct UpdateMember<'info> {
 pub struct UpdateMember<'info> {
     #[account(mut, has_one = beneficiary)]
     #[account(mut, has_one = beneficiary)]
@@ -1168,6 +1210,12 @@ pub enum ErrorCode {
     ExpectedUnlockedVendor,
     ExpectedUnlockedVendor,
     #[msg("Locked deposit from an invalid deposit authority.")]
     #[msg("Locked deposit from an invalid deposit authority.")]
     InvalidVestingSigner,
     InvalidVestingSigner,
+    #[msg("Locked rewards cannot be realized until one unstaked all tokens.")]
+    UnrealizedReward,
+    #[msg("The beneficiary doesn't match.")]
+    InvalidBeneficiary,
+    #[msg("The given member account does not match the realizor metadata.")]
+    InvalidRealizorMetadata,
 }
 }
 
 
 impl<'a, 'b, 'c, 'info> From<&mut Deposit<'info>>
 impl<'a, 'b, 'c, 'info> From<&mut Deposit<'info>>

+ 89 - 6
examples/lockup/tests/lockup.js

@@ -159,6 +159,7 @@ describe("Lockup and Registry", () => {
       periodCount,
       periodCount,
       depositAmount,
       depositAmount,
       nonce,
       nonce,
+      null, // Lock realizor is None.
       {
       {
         accounts: {
         accounts: {
           vesting: vesting.publicKey,
           vesting: vesting.publicKey,
@@ -194,6 +195,7 @@ describe("Lockup and Registry", () => {
     assert.ok(vestingAccount.whitelistOwned.eq(new anchor.BN(0)));
     assert.ok(vestingAccount.whitelistOwned.eq(new anchor.BN(0)));
     assert.equal(vestingAccount.nonce, nonce);
     assert.equal(vestingAccount.nonce, nonce);
     assert.ok(endTs.gt(vestingAccount.startTs));
     assert.ok(endTs.gt(vestingAccount.startTs));
+    assert.ok(vestingAccount.realizor === null);
   });
   });
 
 
   it("Fails to withdraw from a vesting account before vesting", async () => {
   it("Fails to withdraw from a vesting account before vesting", async () => {
@@ -580,8 +582,8 @@ describe("Lockup and Registry", () => {
   it("Drops a locked reward", async () => {
   it("Drops a locked reward", async () => {
     lockedRewardKind = {
     lockedRewardKind = {
       locked: {
       locked: {
-        endTs: new anchor.BN(Date.now() / 1000 + 70),
-        periodCount: new anchor.BN(10),
+        endTs: new anchor.BN(Date.now() / 1000 + 5),
+        periodCount: new anchor.BN(3),
       },
       },
     };
     };
     lockedRewardAmount = new anchor.BN(200);
     lockedRewardAmount = new anchor.BN(200);
@@ -658,16 +660,21 @@ describe("Lockup and Registry", () => {
     assert.ok(e.locked === true);
     assert.ok(e.locked === true);
   });
   });
 
 
-  it("Collects a locked reward", async () => {
-    const vendoredVesting = new anchor.web3.Account();
-    const vendoredVestingVault = new anchor.web3.Account();
+  let vendoredVesting = null;
+  let vendoredVestingVault = null;
+  let vendoredVestingSigner = null;
+
+  it("Claims a locked reward", async () => {
+    vendoredVesting = new anchor.web3.Account();
+    vendoredVestingVault = new anchor.web3.Account();
     let [
     let [
-      vendoredVestingSigner,
+      _vendoredVestingSigner,
       nonce,
       nonce,
     ] = await anchor.web3.PublicKey.findProgramAddress(
     ] = await anchor.web3.PublicKey.findProgramAddress(
       [vendoredVesting.publicKey.toBuffer()],
       [vendoredVesting.publicKey.toBuffer()],
       lockup.programId
       lockup.programId
     );
     );
+    vendoredVestingSigner = _vendoredVestingSigner;
     const remainingAccounts = lockup.instruction.createVesting
     const remainingAccounts = lockup.instruction.createVesting
       .accounts({
       .accounts({
         vesting: vendoredVesting.publicKey,
         vesting: vendoredVesting.publicKey,
@@ -731,6 +738,51 @@ describe("Lockup and Registry", () => {
       lockupAccount.periodCount.eq(lockedRewardKind.locked.periodCount)
       lockupAccount.periodCount.eq(lockedRewardKind.locked.periodCount)
     );
     );
     assert.ok(lockupAccount.whitelistOwned.eq(new anchor.BN(0)));
     assert.ok(lockupAccount.whitelistOwned.eq(new anchor.BN(0)));
+    assert.ok(lockupAccount.realizor.program.equals(registry.programId));
+    assert.ok(lockupAccount.realizor.metadata.equals(member.publicKey));
+  });
+
+  it("Waits for the lockup period to pass", async () => {
+    await serumCmn.sleep(10 * 1000);
+  });
+
+  it("Should fail to unlock an unrealized lockup reward", async () => {
+    const token = await serumCmn.createTokenAccount(
+      provider,
+      mint,
+      provider.wallet.publicKey
+    );
+    await assert.rejects(
+      async () => {
+        const withdrawAmount = new anchor.BN(10);
+        await lockup.rpc.withdraw(withdrawAmount, {
+          accounts: {
+            vesting: vendoredVesting.publicKey,
+            beneficiary: provider.wallet.publicKey,
+            token,
+            vault: vendoredVestingVault.publicKey,
+            vestingSigner: vendoredVestingSigner,
+            tokenProgram: TokenInstructions.TOKEN_PROGRAM_ID,
+            clock: anchor.web3.SYSVAR_CLOCK_PUBKEY,
+          },
+          // TODO: trait methods generated on the client. Until then, we need to manually
+          //       specify the account metas here.
+          remainingAccounts: [
+            { pubkey: registry.programId, isWritable: false, isSigner: false },
+            { pubkey: member.publicKey, isWritable: false, isSigner: false },
+            { pubkey: balances.spt, isWritable: false, isSigner: false },
+            { pubkey: balancesLocked.spt, isWritable: false, isSigner: false },
+          ],
+        });
+      },
+      (err) => {
+        // Solana doesn't propagate errors across CPI. So we receive the registry's error code,
+        // not the lockup's.
+        const errorCode = "custom program error: 0x78";
+        assert.ok(err.toString().split(errorCode).length === 2);
+        return true;
+      }
+    );
   });
   });
 
 
   const pendingWithdrawal = new anchor.web3.Account();
   const pendingWithdrawal = new anchor.web3.Account();
@@ -857,4 +909,35 @@ describe("Lockup and Registry", () => {
     const tokenAccount = await serumCmn.getTokenAccount(provider, token);
     const tokenAccount = await serumCmn.getTokenAccount(provider, token);
     assert.ok(tokenAccount.amount.eq(withdrawAmount));
     assert.ok(tokenAccount.amount.eq(withdrawAmount));
   });
   });
+
+  it("Should succesfully unlock a locked reward after unstaking", async () => {
+    const token = await serumCmn.createTokenAccount(
+      provider,
+      mint,
+      provider.wallet.publicKey
+    );
+
+    const withdrawAmount = new anchor.BN(7);
+    await lockup.rpc.withdraw(withdrawAmount, {
+      accounts: {
+        vesting: vendoredVesting.publicKey,
+        beneficiary: provider.wallet.publicKey,
+        token,
+        vault: vendoredVestingVault.publicKey,
+        vestingSigner: vendoredVestingSigner,
+        tokenProgram: TokenInstructions.TOKEN_PROGRAM_ID,
+        clock: anchor.web3.SYSVAR_CLOCK_PUBKEY,
+      },
+      // TODO: trait methods generated on the client. Until then, we need to manually
+      //       specify the account metas here.
+      remainingAccounts: [
+        { pubkey: registry.programId, isWritable: false, isSigner: false },
+        { pubkey: member.publicKey, isWritable: false, isSigner: false },
+        { pubkey: balances.spt, isWritable: false, isSigner: false },
+        { pubkey: balancesLocked.spt, isWritable: false, isSigner: false },
+      ],
+    });
+    const tokenAccount = await serumCmn.getTokenAccount(provider, token);
+    assert.ok(tokenAccount.amount.eq(withdrawAmount));
+  });
 });
 });

+ 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-error = { path = "./attribute/error", version = "0.1.0" }
 anchor-attribute-program = { path = "./attribute/program", 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-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" }
 anchor-derive-accounts = { path = "./derive/accounts", version = "0.1.0" }
 serum-borsh = "0.8.1-serum.1"
 serum-borsh = "0.8.1-serum.1"
 solana-program = "=1.5.0"
 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 anchor_syn::parser::program as program_parser;
 use syn::parse_macro_input;
 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]
 #[proc_macro_attribute]
 pub fn program(
 pub fn program(
     _args: proc_macro::TokenStream,
     _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::account_info::AccountInfo;
 use solana_program::pubkey::Pubkey;
 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.
 /// 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 accounts: T,
     pub program: AccountInfo<'info>,
     pub program: AccountInfo<'info>,
     pub signer_seeds: &'a [&'b [&'c [u8]]],
     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 {
     pub fn new(program: AccountInfo<'info>, accounts: T) -> Self {
         Self {
         Self {
             accounts,
             accounts,

+ 7 - 5
lang/src/lib.rs

@@ -39,6 +39,7 @@ pub mod idl;
 mod program_account;
 mod program_account;
 mod state;
 mod state;
 mod sysvar;
 mod sysvar;
+mod vec;
 
 
 pub use crate::context::{Context, CpiContext};
 pub use crate::context::{Context, CpiContext};
 pub use crate::cpi_account::CpiAccount;
 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_access_control::access_control;
 pub use anchor_attribute_account::account;
 pub use anchor_attribute_account::account;
 pub use anchor_attribute_error::error;
 pub use anchor_attribute_error::error;
+pub use anchor_attribute_interface::interface;
 pub use anchor_attribute_program::program;
 pub use anchor_attribute_program::program;
 pub use anchor_attribute_state::state;
 pub use anchor_attribute_state::state;
 pub use anchor_derive_accounts::Accounts;
 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
     /// program dependent. However, users of these types should never have to
     /// worry about account substitution attacks. For example, if a program
     /// worry about account substitution attacks. For example, if a program
     /// expects a `Mint` account from the SPL token program  in a particular
     /// 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
     /// `program_id` is the currently executing program. `accounts` is the
     /// set of accounts to construct the type from. For every account used,
     /// 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::*;`.
 /// All programs should include it via `anchor_lang::prelude::*;`.
 pub mod prelude {
 pub mod prelude {
     pub use super::{
     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,
         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 cfg!(not(feature = "no-idl")) {
                 if sighash == anchor_lang::idl::IDL_IX_TAG.to_le_bytes() {
                 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 {
 pub fn generate_dispatch(program: &Program) -> proc_macro2::TokenStream {
+    // Dispatch the state constructor.
     let ctor_state_dispatch_arm = match &program.state {
     let ctor_state_dispatch_arm = match &program.state {
         None => quote! { /* no-op */ },
         None => quote! { /* no-op */ },
         Some(state) => {
         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 {
     let state_dispatch_arms: Vec<proc_macro2::TokenStream> = match &program.state {
         None => vec![],
         None => vec![],
         Some(s) => s
         Some(s) => s
@@ -112,6 +115,63 @@ pub fn generate_dispatch(program: &Program) -> proc_macro2::TokenStream {
             })
             })
             .collect(),
             .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
     let dispatch_arms: Vec<proc_macro2::TokenStream> = program
         .rpcs
         .rpcs
         .iter()
         .iter()
@@ -139,6 +199,7 @@ pub fn generate_dispatch(program: &Program) -> proc_macro2::TokenStream {
         match sighash {
         match sighash {
             #ctor_state_dispatch_arm
             #ctor_state_dispatch_arm
             #(#state_dispatch_arms)*
             #(#state_dispatch_arms)*
+            #(#trait_dispatch_arms)*
             #(#dispatch_arms)*
             #(#dispatch_arms)*
             _ => {
             _ => {
                 msg!("Fallback functions are not supported. If you have a use case, please file an issue.");
                 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 mut data: &[u8] = idl_ix_data;
 
 
                 let ix = anchor_lang::idl::IdlInstruction::deserialize(&mut data)
                 let ix = anchor_lang::idl::IdlInstruction::deserialize(&mut data)
-                    .map_err(|_| ProgramError::Custom(1))?; // todo
+                    .map_err(|_| ProgramError::Custom(2))?; // todo
 
 
                 match ix {
                 match ix {
                     anchor_lang::idl::IdlInstruction::Create { data_len } => {
                     anchor_lang::idl::IdlInstruction::Create { data_len } => {
@@ -419,6 +480,101 @@ pub fn generate_non_inlined_handlers(program: &Program) -> proc_macro2::TokenStr
             })
             })
             .collect(),
             .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
     let non_inlined_handlers: Vec<proc_macro2::TokenStream> = program
         .rpcs
         .rpcs
         .iter()
         .iter()
@@ -451,6 +607,7 @@ pub fn generate_non_inlined_handlers(program: &Program) -> proc_macro2::TokenStr
         #non_inlined_idl
         #non_inlined_idl
         #non_inlined_ctor
         #non_inlined_ctor
         #(#non_inlined_state_handlers)*
         #(#non_inlined_state_handlers)*
+        #(#non_inlined_state_trait_handlers)*
         #(#non_inlined_handlers)*
         #(#non_inlined_handlers)*
     }
     }
 }
 }
@@ -479,7 +636,14 @@ pub fn generate_ctor_typed_variant_with_semi(program: &Program) -> proc_macro2::
     match &program.state {
     match &program.state {
         None => quote! {},
         None => quote! {},
         Some(state) => {
         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 {
             if ctor_args.len() == 0 {
                 quote! {
                 quote! {
                     #[derive(AnchorSerialize, AnchorDeserialize)]
                     #[derive(AnchorSerialize, AnchorDeserialize)]
@@ -490,7 +654,7 @@ pub fn generate_ctor_typed_variant_with_semi(program: &Program) -> proc_macro2::
                     #[derive(AnchorSerialize, AnchorDeserialize)]
                     #[derive(AnchorSerialize, AnchorDeserialize)]
                     pub struct __Ctor {
                     pub struct __Ctor {
                         #(#ctor_args),*
                         #(#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.
 // 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
 // However, we do namespace methods in the preeimage so that we can use
 // different traits with the same method name.
 // 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 preimage = format!("{}::{}", namespace, name);
 
 
     let mut sighash = [0u8; 8];
     let mut sighash = [0u8; 8];

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

@@ -32,6 +32,7 @@ pub struct State {
     pub strct: syn::ItemStruct,
     pub strct: syn::ItemStruct,
     pub impl_block: syn::ItemImpl,
     pub impl_block: syn::ItemImpl,
     pub methods: Vec<StateRpc>,
     pub methods: Vec<StateRpc>,
+    pub interfaces: Vec<StateInterface>,
     pub ctor: syn::ImplItemMethod,
     pub ctor: syn::ImplItemMethod,
     pub ctor_anchor: syn::Ident, // TODO: consolidate this with ctor above.
     pub ctor_anchor: syn::Ident, // TODO: consolidate this with ctor above.
 }
 }
@@ -42,6 +43,14 @@ pub struct StateRpc {
     pub ident: syn::Ident,
     pub ident: syn::Ident,
     pub args: Vec<RpcArg>,
     pub args: Vec<RpcArg>,
     pub anchor_ident: syn::Ident,
     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)]
 #[derive(Debug)]

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

@@ -1,5 +1,5 @@
 use crate::parser;
 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 {
 pub fn parse(program_mod: syn::ItemMod) -> Program {
     let mod_ident = &program_mod.ident;
     let mod_ident = &program_mod.ident;
@@ -28,12 +28,15 @@ pub fn parse(program_mod: syn::ItemMod) -> Program {
             .next();
             .next();
 
 
         let impl_block: Option<&syn::ItemImpl> = strct.map(|strct| {
         let impl_block: Option<&syn::ItemImpl> = strct.map(|strct| {
-            let item_impl = mod_content
+            let item_impls = mod_content
                 .iter()
                 .iter()
                 .filter_map(|item| match item {
                 .filter_map(|item| match item {
                     syn::Item::Impl(item_impl) => {
                     syn::Item::Impl(item_impl) => {
                         let impl_ty_str = parser::tts_to_string(&item_impl.self_ty);
                         let impl_ty_str = parser::tts_to_string(&item_impl.self_ty);
                         let strct_name = strct.ident.to_string();
                         let strct_name = strct.ident.to_string();
+                        if item_impl.trait_.is_some() {
+                            return None;
+                        }
                         if strct_name != impl_ty_str {
                         if strct_name != impl_ty_str {
                             return None;
                             return None;
                         }
                         }
@@ -41,9 +44,39 @@ pub fn parse(program_mod: syn::ItemMod) -> Program {
                     }
                     }
                     _ => None,
                     _ => 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| {
         strct.map(|strct| {
@@ -112,6 +145,7 @@ pub fn parse(program_mod: syn::ItemMod) -> Program {
                                     ident: m.sig.ident.clone(),
                                     ident: m.sig.ident.clone(),
                                     args,
                                     args,
                                     anchor_ident,
                                     anchor_ident,
+                                    has_receiver: true,
                                 })
                                 })
                             }
                             }
                         },
                         },
@@ -122,6 +156,7 @@ pub fn parse(program_mod: syn::ItemMod) -> Program {
             State {
             State {
                 name: strct.ident.to_string(),
                 name: strct.ident.to_string(),
                 strct: strct.clone(),
                 strct: strct.clone(),
+                interfaces: trait_impls.expect("Some if state exists"),
                 impl_block,
                 impl_block,
                 ctor,
                 ctor,
                 ctor_anchor,
                 ctor_anchor,
@@ -206,3 +241,52 @@ fn extract_ident(path_ty: &syn::PatType) -> &proc_macro2::Ident {
     };
     };
     &path.segments[0].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",
   "name": "@project-serum/anchor",
-  "version": "0.1.0",
+  "version": "0.2.0-beta.1",
   "description": "Anchor client",
   "description": "Anchor client",
   "main": "dist/cjs/index.js",
   "main": "dist/cjs/index.js",
   "module": "dist/esm/index.js",
   "module": "dist/esm/index.js",
@@ -27,11 +27,13 @@
     "@solana/web3.js": "^0.90.4",
     "@solana/web3.js": "^0.90.4",
     "@types/bn.js": "^4.11.6",
     "@types/bn.js": "^4.11.6",
     "@types/bs58": "^4.0.1",
     "@types/bs58": "^4.0.1",
+    "@types/crypto-hash": "^1.1.2",
     "@types/pako": "^1.0.1",
     "@types/pako": "^1.0.1",
     "bn.js": "^5.1.2",
     "bn.js": "^5.1.2",
     "bs58": "^4.0.1",
     "bs58": "^4.0.1",
     "buffer-layout": "^1.2.0",
     "buffer-layout": "^1.2.0",
     "camelcase": "^5.3.1",
     "camelcase": "^5.3.1",
+    "crypto-hash": "^1.3.0",
     "eventemitter3": "^4.0.7",
     "eventemitter3": "^4.0.7",
     "find": "^0.3.0",
     "find": "^0.3.0",
     "js-sha256": "^0.9.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
 // Returns the size of the type in bytes. For variable length types, just return
 // 1. Users should override this value in such cases.
 // 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) {
   switch (ty) {
     case "bool":
     case "bool":
       return 1;
       return 1;
@@ -386,7 +386,7 @@ export function typeSize(idl: Idl, ty: IdlType): number {
       // @ts-ignore
       // @ts-ignore
       if (ty.option !== undefined) {
       if (ty.option !== undefined) {
         // @ts-ignore
         // @ts-ignore
-        return 1 + typeSize(ty.option);
+        return 1 + typeSize(idl, ty.option);
       }
       }
       // @ts-ignore
       // @ts-ignore
       if (ty.defined !== undefined) {
       if (ty.defined !== undefined) {

+ 8 - 1
ts/yarn.lock

@@ -753,6 +753,13 @@
   dependencies:
   dependencies:
     "@types/node" "*"
     "@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":
 "@types/express-serve-static-core@^4.17.9":
   version "4.17.18"
   version "4.17.18"
   resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.18.tgz#8371e260f40e0e1ca0c116a9afcd9426fa094c40"
   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"
     shebang-command "^2.0.0"
     which "^2.0.1"
     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"
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/crypto-hash/-/crypto-hash-1.3.0.tgz#b402cb08f4529e9f4f09346c3e275942f845e247"
   resolved "https://registry.yarnpkg.com/crypto-hash/-/crypto-hash-1.3.0.tgz#b402cb08f4529e9f4f09346c3e275942f845e247"
   integrity sha512-lyAZ0EMyjDkVvz8WOeVnuCPvKVBXcMv1l5SVqO1yC7PzTwrD/pPje/BIRbWhMoPe436U+Y2nD7f5bFx0kt+Sbg==
   integrity sha512-lyAZ0EMyjDkVvz8WOeVnuCPvKVBXcMv1l5SVqO1yC7PzTwrD/pPje/BIRbWhMoPe436U+Y2nD7f5bFx0kt+Sbg==