Selaa lähdekoodia

Lockup and staking examples (#28)

Armani Ferrante 4 vuotta sitten
vanhempi
sitoutus
c9ae5eb0ef

+ 2 - 0
.travis.yml

@@ -43,8 +43,10 @@ jobs:
     - <<: *examples
       name: Runs the examples
       script:
+        - pushd examples/lockup && anchor test && popd
         - pushd examples/sysvars && anchor test && popd
         - pushd examples/composite && anchor test && popd
+        - pushd examples/errors && anchor test && popd
         - pushd examples/spl/token-proxy && anchor test && popd
         - pushd examples/tutorial/basic-0 && anchor test && popd
         - pushd examples/tutorial/basic-1 && anchor test && popd

+ 1 - 0
attribute/account/src/lib.rs

@@ -40,6 +40,7 @@ pub fn account(
         }
 
         impl anchor_lang::AccountDeserialize for #account_name {
+
             fn try_deserialize(buf: &mut &[u8]) -> Result<Self, ProgramError> {
                 let mut discriminator = [0u8; 8];
                 discriminator.copy_from_slice(

+ 2 - 0
cli/src/main.rs

@@ -287,6 +287,8 @@ fn test() -> Result<()> {
 
     // Run the tests.
     if let Err(e) = std::process::Command::new("mocha")
+        .arg("-t")
+        .arg("10000")
         .arg("tests/")
         .stdout(Stdio::inherit())
         .stderr(Stdio::inherit())

+ 1 - 1
examples/errors/Anchor.toml

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

+ 1 - 2
examples/errors/programs/errors/Cargo.toml

@@ -13,5 +13,4 @@ no-entrypoint = []
 cpi = ["no-entrypoint"]
 
 [dependencies]
-# anchor-lang = { git = "https://github.com/project-serum/anchor", features = ["derive"] }
-anchor-lang = { path = "/home/armaniferrante/Documents/code/src/github.com/project-serum/anchor", features = ["derive"] }
+anchor-lang = { git = "https://github.com/project-serum/anchor", features = ["derive"] }

+ 2 - 0
examples/lockup/Anchor.toml

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

+ 4 - 0
examples/lockup/Cargo.toml

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

+ 18 - 0
examples/lockup/programs/lockup/Cargo.toml

@@ -0,0 +1,18 @@
+[package]
+name = "serum-lockup"
+version = "0.1.0"
+description = "Created with Anchor"
+edition = "2018"
+
+[lib]
+crate-type = ["cdylib", "lib"]
+name = "serum_lockup"
+
+[features]
+no-entrypoint = []
+cpi = ["no-entrypoint"]
+
+[dependencies]
+anchor-lang = { git = "https://github.com/project-serum/anchor", features = ["derive"] }
+anchor-spl = { git = "https://github.com/project-serum/anchor" }
+bytemuck = "1.4.0"

+ 2 - 0
examples/lockup/programs/lockup/Xargo.toml

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

+ 83 - 0
examples/lockup/programs/lockup/src/calculator.rs

@@ -0,0 +1,83 @@
+//! Utility functions for calculating unlock schedules for a vesting account.
+
+use crate::Vesting;
+
+pub fn available_for_withdrawal(vesting: &Vesting, current_ts: i64) -> u64 {
+    std::cmp::min(outstanding_vested(vesting, current_ts), balance(vesting))
+}
+
+// The amount of funds currently in the vault.
+pub fn balance(vesting: &Vesting) -> u64 {
+    vesting
+        .outstanding
+        .checked_sub(vesting.whitelist_owned)
+        .unwrap()
+}
+
+// The amount of outstanding locked tokens vested. Note that these
+// tokens might have been transferred to whitelisted programs.
+fn outstanding_vested(vesting: &Vesting, current_ts: i64) -> u64 {
+    total_vested(vesting, current_ts)
+        .checked_sub(withdrawn_amount(vesting))
+        .unwrap()
+}
+
+// Returns the amount withdrawn from this vesting account.
+fn withdrawn_amount(vesting: &Vesting) -> u64 {
+    vesting
+        .start_balance
+        .checked_sub(vesting.outstanding)
+        .unwrap()
+}
+
+// Returns the total vested amount up to the given ts, assuming zero
+// withdrawals and zero funds sent to other programs.
+fn total_vested(vesting: &Vesting, current_ts: i64) -> u64 {
+    assert!(current_ts >= vesting.start_ts);
+
+    if current_ts >= vesting.end_ts {
+        return vesting.start_balance;
+    }
+    linear_unlock(vesting, current_ts).unwrap()
+}
+
+fn linear_unlock(vesting: &Vesting, current_ts: i64) -> Option<u64> {
+    // Signed division not supported.
+    let current_ts = current_ts as u64;
+    let start_ts = vesting.start_ts as u64;
+    let end_ts = vesting.end_ts as u64;
+
+    // If we can't perfectly partition the vesting window,
+    // push the start of the window back so that we can.
+    //
+    // This has the effect of making the first vesting period shorter
+    // than the rest.
+    let shifted_start_ts =
+        start_ts.checked_sub(end_ts.checked_sub(start_ts)? % vesting.period_count)?;
+
+    // Similarly, if we can't perfectly divide up the vesting rewards
+    // then make the first period act as a cliff, earning slightly more than
+    // subsequent periods.
+    let reward_overflow = vesting.start_balance % vesting.period_count;
+
+    // Reward per period ignoring the overflow.
+    let reward_per_period =
+        (vesting.start_balance.checked_sub(reward_overflow)?).checked_div(vesting.period_count)?;
+
+    // Number of vesting periods that have passed.
+    let current_period = {
+        let period_secs =
+            (end_ts.checked_sub(shifted_start_ts)?).checked_div(vesting.period_count)?;
+        let current_period_count =
+            (current_ts.checked_sub(shifted_start_ts)?).checked_div(period_secs)?;
+        std::cmp::min(current_period_count, vesting.period_count)
+    };
+
+    if current_period == 0 {
+        return Some(0);
+    }
+
+    current_period
+        .checked_mul(reward_per_period)?
+        .checked_add(reward_overflow)
+}

+ 557 - 0
examples/lockup/programs/lockup/src/lib.rs

@@ -0,0 +1,557 @@
+//! A relatively advanced example of a lockup program. If you're new to Anchor,
+//! it's suggested to start with the other examples.
+
+#![feature(proc_macro_hygiene)]
+
+use anchor_lang::prelude::*;
+use anchor_lang::solana_program;
+use anchor_lang::solana_program::instruction::Instruction;
+use anchor_spl::token::{self, TokenAccount, Transfer};
+
+mod calculator;
+
+#[program]
+mod lockup {
+    use super::*;
+    pub fn initialize(ctx: Context<Initialize>, authority: Pubkey) -> Result<(), Error> {
+        let safe = &mut ctx.accounts.safe;
+        let whitelist = &mut ctx.accounts.whitelist;
+
+        safe.authority = authority;
+        safe.whitelist = *whitelist.to_account_info().key;
+        whitelist.safe = *safe.to_account_info().key;
+
+        Ok(())
+    }
+
+    pub fn set_authority(ctx: Context<SetAuthority>, new_authority: Pubkey) -> Result<(), Error> {
+        let safe = &mut ctx.accounts.safe;
+        safe.authority = new_authority;
+        Ok(())
+    }
+
+    pub fn create_vesting(
+        ctx: Context<CreateVesting>,
+        beneficiary: Pubkey,
+        end_ts: i64,
+        period_count: u64,
+        deposit_amount: u64,
+        nonce: u8,
+    ) -> Result<(), Error> {
+        // Vesting scheudle.
+        if end_ts <= ctx.accounts.clock.unix_timestamp {
+            return Err(ErrorCode::InvalidTimestamp.into());
+        }
+        if period_count == 0 {
+            return Err(ErrorCode::InvalidPeriod.into());
+        }
+        if deposit_amount == 0 {
+            return Err(ErrorCode::InvalidDepositAmount.into());
+        }
+        // Vault.
+        let vault_authority = Pubkey::create_program_address(
+            &vault_signer_seeds(
+                ctx.accounts.safe.to_account_info().key,
+                &beneficiary,
+                &nonce,
+            ),
+            ctx.program_id,
+        )
+        .map_err(|_| ErrorCode::InvalidProgramAddress)?;
+        if ctx.accounts.vault.owner != vault_authority {
+            return Err(ErrorCode::InvalidVaultOwner)?;
+        }
+        if ctx.accounts.vault.amount != 0 {
+            return Err(ErrorCode::InvalidVaultAmount)?;
+        }
+
+        let vesting = &mut ctx.accounts.vesting;
+
+        vesting.safe = *ctx.accounts.safe.to_account_info().key;
+        vesting.beneficiary = beneficiary;
+        vesting.mint = ctx.accounts.vault.mint;
+        vesting.vault = *ctx.accounts.vault.to_account_info().key;
+        vesting.period_count = period_count;
+        vesting.start_balance = deposit_amount;
+        vesting.end_ts = end_ts;
+        vesting.start_ts = ctx.accounts.clock.unix_timestamp;
+        vesting.outstanding = deposit_amount;
+        vesting.whitelist_owned = 0;
+        vesting.grantor = *ctx.accounts.depositor_authority.key;
+        vesting.nonce = nonce;
+
+        token::transfer(ctx.accounts.into(), deposit_amount)?;
+
+        Ok(())
+    }
+
+    pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<(), Error> {
+        if amount == 0 {
+            return Err(ErrorCode::InvalidVaultAmount.into());
+        }
+        if amount
+            > calculator::available_for_withdrawal(
+                &ctx.accounts.vesting,
+                ctx.accounts.clock.unix_timestamp,
+            )
+        {
+            return Err(ErrorCode::InsufficienWithdrawalBalance.into());
+        }
+
+        let vesting = &mut ctx.accounts.vesting;
+        vesting.outstanding -= amount;
+
+        let nonce = ctx.accounts.vesting.nonce;
+        let signer = &[&vault_signer_seeds(
+            ctx.accounts.safe.to_account_info().key,
+            ctx.accounts.beneficiary.key,
+            &nonce,
+        )[..]];
+        let cpi_ctx = CpiContext::from(ctx.accounts).with_signer(signer);
+        token::transfer(cpi_ctx, amount)?;
+
+        Ok(())
+    }
+
+    pub fn whitelist_add(ctx: Context<WhitelistAdd>, entry: WhitelistEntry) -> Result<(), Error> {
+        if ctx.accounts.whitelist.entries.len() == 5 {
+            return Err(ErrorCode::WhitelistFull.into());
+        }
+        let entry_derived_address = entry.derived_address()?;
+        let mut items = ctx.accounts.whitelist.entries.iter().filter_map(|entry| {
+            let da = entry.derived_address().expect("always valid");
+            match da == entry_derived_address {
+                false => None,
+                true => Some(entry),
+            }
+        });
+        if items.next().is_some() {
+            return Err(ErrorCode::WhitelistEntryAlreadyExists.into());
+        }
+        ctx.accounts.whitelist.entries.push(entry);
+
+        Ok(())
+    }
+
+    pub fn whitelist_delete(
+        ctx: Context<WhitelistAdd>,
+        entry: WhitelistEntry,
+    ) -> Result<(), Error> {
+        let entry_derived_address = entry.derived_address()?;
+
+        let whitelist = &mut ctx.accounts.whitelist;
+        whitelist.entries = whitelist
+            .entries
+            .clone()
+            .into_iter()
+            .filter_map(|e: WhitelistEntry| {
+                if e.derived_address().expect("always valid") == entry_derived_address {
+                    None
+                } else {
+                    Some(e)
+                }
+            })
+            .collect::<Vec<WhitelistEntry>>();
+
+        Ok(())
+    }
+
+    // Sends funds from a whitelisted program back to the lockup program.
+    pub fn whitelist_deposit(
+        ctx: Context<WhitelistDeposit>,
+        instruction_data: Vec<u8>,
+    ) -> Result<(), Error> {
+        let accounts = ctx.accounts;
+
+        let before_amount = accounts.vault.amount;
+
+        // Invoke opaque relay.
+        {
+            let mut meta_accounts = vec![
+                AccountMeta::new_readonly(*accounts.vesting.to_account_info().key, false),
+                AccountMeta::new(*accounts.vault.to_account_info().key, false),
+                AccountMeta::new_readonly(*accounts.vault_authority.to_account_info().key, true),
+                AccountMeta::new_readonly(*accounts.token_program.to_account_info().key, false),
+                AccountMeta::new(*accounts.whitelisted_program.to_account_info().key, false),
+                AccountMeta::new_readonly(
+                    *accounts
+                        .whitelisted_program_vault_authority
+                        .to_account_info()
+                        .key,
+                    false,
+                ),
+            ];
+            meta_accounts.extend(ctx.remaining_accounts.iter().map(|a| {
+                if a.is_writable {
+                    AccountMeta::new(*a.key, a.is_signer)
+                } else {
+                    AccountMeta::new_readonly(*a.key, a.is_signer)
+                }
+            }));
+            let relay_instruction = Instruction {
+                program_id: *accounts.whitelisted_program.to_account_info().key,
+                accounts: meta_accounts,
+                data: instruction_data.to_vec(),
+            };
+
+            let signer_seeds = &[];
+            solana_program::program::invoke_signed(
+                &relay_instruction,
+                &accounts.to_account_infos(),
+                signer_seeds,
+            )?;
+        }
+
+        let after_amount = accounts.vault.reload()?.amount;
+
+        // Deposit safety checks.
+        let deposit_amount = after_amount - before_amount;
+        if deposit_amount <= 0 {
+            return Err(ErrorCode::InsufficientWhitelistDepositAmount)?;
+        }
+        if deposit_amount > accounts.vesting.whitelist_owned {
+            return Err(ErrorCode::WhitelistDepositOverflow)?;
+        }
+
+        // Bookkeeping.
+        accounts.vesting.whitelist_owned -= deposit_amount;
+
+        Ok(())
+    }
+
+    // Sends funds from the lockup program to a whitelisted program.
+    pub fn whitelist_withdraw(
+        ctx: Context<WhitelistWithdraw>,
+        instruction_data: Vec<u8>,
+        amount: u64,
+    ) -> Result<(), Error> {
+        let accounts = ctx.accounts;
+
+        let before_amount = accounts.vault.amount;
+
+        // Invoke opaque relay.
+        {
+            let mut meta_accounts = vec![
+                AccountMeta::new_readonly(*accounts.vesting.to_account_info().key, false),
+                AccountMeta::new(*accounts.vault.to_account_info().key, false),
+                AccountMeta::new_readonly(*accounts.vault_authority.to_account_info().key, true),
+                AccountMeta::new_readonly(*accounts.token_program.to_account_info().key, false),
+                AccountMeta::new(
+                    *accounts.whitelisted_program_vault.to_account_info().key,
+                    false,
+                ),
+                AccountMeta::new_readonly(
+                    *accounts
+                        .whitelisted_program_vault_authority
+                        .to_account_info()
+                        .key,
+                    false,
+                ),
+            ];
+            meta_accounts.extend(ctx.remaining_accounts.iter().map(|a| {
+                if a.is_writable {
+                    AccountMeta::new(*a.key, a.is_signer)
+                } else {
+                    AccountMeta::new_readonly(*a.key, a.is_signer)
+                }
+            }));
+            let relay_instruction = Instruction {
+                program_id: *accounts.whitelisted_program.to_account_info().key,
+                accounts: meta_accounts,
+                data: instruction_data.to_vec(),
+            };
+
+            let signer_seeds = &[];
+            solana_program::program::invoke_signed(
+                &relay_instruction,
+                &accounts.to_account_infos(),
+                signer_seeds,
+            )?;
+        }
+
+        let after_amount = accounts.vault.reload()?.amount;
+
+        // Withdrawal safety checks.
+        let amount_transferred = before_amount - after_amount;
+        if amount_transferred > amount {
+            return Err(ErrorCode::WhitelistWithdrawLimit)?;
+        }
+
+        // Bookeeping.
+        accounts.vesting.whitelist_owned += amount_transferred;
+
+        Ok(())
+    }
+
+    // Convenience function for UI's to calculate the withdrawalable amount.
+    pub fn available_for_withdrawal(ctx: Context<AvailableForWithdrawal>) -> Result<(), Error> {
+        let available = calculator::available_for_withdrawal(
+            &ctx.accounts.vesting,
+            ctx.accounts.clock.unix_timestamp,
+        );
+        // Log as string so that JS can read as a BN.
+        msg!(&format!("{{ \"result\": \"{}\" }}", available));
+        Ok(())
+    }
+}
+
+#[derive(Accounts)]
+pub struct Initialize<'info> {
+    #[account(init)]
+    safe: ProgramAccount<'info, Safe>,
+    #[account(init)]
+    whitelist: ProgramAccount<'info, Whitelist>,
+    rent: Sysvar<'info, Rent>,
+}
+
+#[derive(Accounts)]
+pub struct SetAuthority<'info> {
+    #[account(signer)]
+    authority: AccountInfo<'info>,
+    #[account(mut, "&safe.authority == authority.key")]
+    safe: ProgramAccount<'info, Safe>,
+}
+
+#[derive(Accounts)]
+pub struct CreateVesting<'info> {
+    #[account(init)]
+    vesting: ProgramAccount<'info, Vesting>,
+    safe: ProgramAccount<'info, Safe>,
+    #[account(mut)]
+    vault: CpiAccount<'info, TokenAccount>,
+    #[account(mut)]
+    depositor: AccountInfo<'info>,
+    #[account(signer)]
+    depositor_authority: AccountInfo<'info>,
+    token_program: AccountInfo<'info>,
+    rent: Sysvar<'info, Rent>,
+    clock: Sysvar<'info, Clock>,
+}
+
+#[derive(Accounts)]
+pub struct Withdraw<'info> {
+    safe: ProgramAccount<'info, Safe>,
+    #[account(mut, belongs_to = safe)]
+    vesting: ProgramAccount<'info, Vesting>,
+    #[account(signer, "beneficiary.key == &vesting.beneficiary")]
+    beneficiary: AccountInfo<'info>,
+    #[account(mut)]
+    token: CpiAccount<'info, TokenAccount>,
+    #[account(mut)]
+    vault: CpiAccount<'info, TokenAccount>,
+    vault_authority: AccountInfo<'info>,
+    token_program: AccountInfo<'info>,
+    clock: Sysvar<'info, Clock>,
+}
+
+#[derive(Accounts)]
+pub struct WhitelistAdd<'info> {
+    #[account(signer)]
+    authority: AccountInfo<'info>,
+    #[account("&safe.authority == authority.key")]
+    safe: ProgramAccount<'info, Safe>,
+    #[account(mut, belongs_to = safe)]
+    whitelist: ProgramAccount<'info, Whitelist>,
+}
+
+#[derive(Accounts)]
+pub struct WhitelistDelete<'info> {
+    #[account(signer)]
+    authority: AccountInfo<'info>,
+    #[account("&safe.authority == authority.key")]
+    safe: ProgramAccount<'info, Safe>,
+    #[account(mut, belongs_to = safe)]
+    whitelist: ProgramAccount<'info, Whitelist>,
+}
+
+#[derive(Accounts)]
+pub struct WhitelistDeposit<'info> {
+    #[account(signer)]
+    beneficiary: AccountInfo<'info>,
+    safe: ProgramAccount<'info, Safe>,
+    #[account(belongs_to = safe)]
+    whitelist: ProgramAccount<'info, Whitelist>,
+    whitelisted_program: AccountInfo<'info>,
+
+    // Whitelist interface.
+    #[account(
+				mut,
+				belongs_to = safe,
+				"&vesting.beneficiary == beneficiary.key",
+		)]
+    vesting: ProgramAccount<'info, Vesting>,
+    #[account(mut, "&vesting.vault == vault.to_account_info().key")]
+    vault: CpiAccount<'info, TokenAccount>,
+    vault_authority: AccountInfo<'info>,
+    token_program: AccountInfo<'info>,
+    #[account(mut)]
+    whitelisted_program_vault: AccountInfo<'info>,
+    whitelisted_program_vault_authority: AccountInfo<'info>,
+}
+
+#[derive(Accounts)]
+pub struct WhitelistWithdraw<'info> {
+    #[account(signer)]
+    beneficiary: AccountInfo<'info>,
+    safe: ProgramAccount<'info, Safe>,
+    #[account(belongs_to = safe)]
+    whitelist: ProgramAccount<'info, Whitelist>,
+    whitelisted_program: AccountInfo<'info>,
+
+    // Whitelist interface.
+    #[account(
+				mut,
+				belongs_to = safe,
+				"&vesting.beneficiary == beneficiary.key",
+		)]
+    vesting: ProgramAccount<'info, Vesting>,
+    #[account(mut, "&vesting.vault == vault.to_account_info().key")]
+    vault: CpiAccount<'info, TokenAccount>,
+    vault_authority: AccountInfo<'info>,
+    token_program: AccountInfo<'info>,
+    #[account(mut)]
+    whitelisted_program_vault: AccountInfo<'info>,
+    whitelisted_program_vault_authority: AccountInfo<'info>,
+}
+
+#[derive(Accounts)]
+pub struct AvailableForWithdrawal<'info> {
+    vesting: ProgramAccount<'info, Vesting>,
+    clock: Sysvar<'info, Clock>,
+}
+
+#[account]
+pub struct Safe {
+    /// The key with the ability to change the whitelist.
+    pub authority: Pubkey,
+    /// The whitelist of valid programs the Safe can relay transactions to.
+    pub whitelist: Pubkey,
+}
+
+#[account]
+pub struct Whitelist {
+    pub safe: Pubkey,
+    pub entries: Vec<WhitelistEntry>,
+}
+
+#[derive(AnchorSerialize, AnchorDeserialize, Default, Copy, Clone)]
+pub struct WhitelistEntry {
+    pub program_id: Pubkey,
+    pub instance: Option<Pubkey>,
+    pub nonce: u8,
+}
+
+impl WhitelistEntry {
+    pub fn derived_address(&self) -> Result<Pubkey, Error> {
+        let pk = {
+            if let Some(i) = self.instance {
+                Pubkey::create_program_address(&[i.as_ref(), &[self.nonce]], &self.program_id)
+            } else {
+                Pubkey::create_program_address(&[&[self.nonce]], &self.program_id)
+            }
+        };
+        pk.map_err(|_| ErrorCode::InvalidWhitelistEntry.into())
+    }
+}
+
+#[account]
+pub struct Vesting {
+    /// The Safe instance this account is associated with.
+    pub safe: Pubkey,
+    /// The owner of this Vesting account.
+    pub beneficiary: Pubkey,
+    /// The mint of the SPL token locked up.
+    pub mint: Pubkey,
+    /// Address of the account's token vault.
+    pub vault: Pubkey,
+    /// The owner of the token account funding this account.
+    pub grantor: Pubkey,
+    /// The outstanding SRM deposit backing this vesting account. All
+    /// withdrawals will deduct this balance.
+    pub outstanding: u64,
+    /// The starting balance of this vesting account, i.e., how much was
+    /// originally deposited.
+    pub start_balance: u64,
+    /// The unix timestamp at which this vesting account was created.
+    pub start_ts: i64,
+    /// The ts at which all the tokens associated with this account
+    /// should be vested.
+    pub end_ts: i64,
+    /// The number of times vesting will occur. For example, if vesting
+    /// is once a year over seven years, this will be 7.
+    pub period_count: u64,
+    /// The amount of tokens in custody of whitelisted programs.
+    pub whitelist_owned: u64,
+    /// Signer nonce.
+    pub nonce: u8,
+}
+
+#[error]
+pub enum ErrorCode {
+    #[msg("Vesting end must be greater than the current unix timestamp.")]
+    InvalidTimestamp,
+    #[msg("The number of vesting periods must be greater than zero.")]
+    InvalidPeriod,
+    #[msg("The vesting deposit amount must be greater than zero.")]
+    InvalidDepositAmount,
+    #[msg("The Whitelist entry is not a valid program address.")]
+    InvalidWhitelistEntry,
+    #[msg("Invalid program address. Did you provide the correct nonce?")]
+    InvalidProgramAddress,
+    #[msg("Invalid vault owner.")]
+    InvalidVaultOwner,
+    #[msg("Vault amount must be zero.")]
+    InvalidVaultAmount,
+    #[msg("Insufficient withdrawal balance.")]
+    InsufficienWithdrawalBalance,
+    #[msg("Whitelist is full")]
+    WhitelistFull,
+    #[msg("Whitelist entry already exists")]
+    WhitelistEntryAlreadyExists,
+    #[msg("Balance must go up when performing a whitelist deposit")]
+    InsufficientWhitelistDepositAmount,
+    #[msg("Cannot deposit more than withdrawn")]
+    WhitelistDepositOverflow,
+    #[msg("Tried to withdraw over the specified limit")]
+    WhitelistWithdrawLimit,
+}
+
+impl<'a, 'b, 'c, 'info> From<&mut CreateVesting<'info>>
+    for CpiContext<'a, 'b, 'c, 'info, Transfer<'info>>
+{
+    fn from(accounts: &mut CreateVesting<'info>) -> CpiContext<'a, 'b, 'c, 'info, Transfer<'info>> {
+        let cpi_accounts = Transfer {
+            from: accounts.depositor.clone(),
+            to: accounts.vault.to_account_info(),
+            authority: accounts.depositor_authority.clone(),
+        };
+        let cpi_program = accounts.token_program.clone();
+        CpiContext::new(cpi_program, cpi_accounts)
+    }
+}
+
+impl<'a, 'b, 'c, 'info> From<&mut Withdraw<'info>>
+    for CpiContext<'a, 'b, 'c, 'info, Transfer<'info>>
+{
+    fn from(accounts: &mut Withdraw<'info>) -> CpiContext<'a, 'b, 'c, 'info, Transfer<'info>> {
+        let cpi_accounts = Transfer {
+            from: accounts.vault.to_account_info(),
+            to: accounts.token.to_account_info(),
+            authority: accounts.vault_authority.to_account_info(),
+        };
+        let cpi_program = accounts.token_program.to_account_info();
+        CpiContext::new(cpi_program, cpi_accounts)
+    }
+}
+
+fn vault_signer_seeds<'a>(
+    safe: &'a Pubkey,
+    beneficiary: &'a Pubkey,
+    nonce: &'a u8,
+) -> [&'a [u8]; 3] {
+    [
+        safe.as_ref(),
+        beneficiary.as_ref(),
+        bytemuck::bytes_of(nonce),
+    ]
+}

+ 18 - 0
examples/lockup/programs/registry/Cargo.toml

@@ -0,0 +1,18 @@
+[package]
+name = "registry"
+version = "0.1.0"
+description = "Created with Anchor"
+edition = "2018"
+
+[lib]
+crate-type = ["cdylib", "lib"]
+name = "registry"
+
+[features]
+no-entrypoint = []
+cpi = ["no-entrypoint"]
+
+[dependencies]
+anchor-lang = { git = "https://github.com/project-serum/anchor", features = ["derive"] }
+anchor-spl = { git = "https://github.com/project-serum/anchor" }
+serum-lockup = { path = "../lockup", features = ["cpi"] }

+ 2 - 0
examples/lockup/programs/registry/Xargo.toml

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

+ 1234 - 0
examples/lockup/programs/registry/src/lib.rs

@@ -0,0 +1,1234 @@
+//! A relatively advanced example of a staking program. If you're new to Anchor,
+//! it's suggested to start with the other examples.
+
+#![feature(proc_macro_hygiene)]
+
+use anchor_lang::prelude::*;
+use anchor_lang::solana_program::program_option::COption;
+use anchor_spl::token::{self, Mint, TokenAccount, Transfer};
+use serum_lockup::CreateVesting;
+use std::convert::Into;
+
+#[program]
+mod registry {
+    use super::*;
+
+    pub fn initialize(
+        ctx: Context<Initialize>,
+        mint: Pubkey,
+        authority: Pubkey,
+        nonce: u8,
+        withdrawal_timelock: i64,
+        max_stake: u64,
+        stake_rate: u64,
+        reward_q_len: u32,
+    ) -> Result<(), Error> {
+        let vault_authority = Pubkey::create_program_address(
+            &[
+                ctx.accounts.registrar.to_account_info().key.as_ref(),
+                &[nonce],
+            ],
+            ctx.program_id,
+        )
+        .map_err(|_| ErrorCode::InvalidNonce)?;
+        if ctx.accounts.pool_mint.mint_authority != COption::Some(vault_authority) {
+            return Err(ErrorCode::InvalidPoolMintAuthority.into());
+        }
+
+        let registrar = &mut ctx.accounts.registrar;
+
+        registrar.authority = authority;
+        registrar.nonce = nonce;
+        registrar.mint = mint;
+        registrar.pool_mint = *ctx.accounts.pool_mint.to_account_info().key;
+        registrar.stake_rate = stake_rate;
+        registrar.reward_event_q = *ctx.accounts.reward_event_q.to_account_info().key;
+        registrar.withdrawal_timelock = withdrawal_timelock;
+        registrar.max_stake = max_stake;
+
+        let reward_q = &mut ctx.accounts.reward_event_q;
+        reward_q
+            .events
+            .resize(reward_q_len as usize, Default::default());
+
+        Ok(())
+    }
+
+    pub fn update_registrar(
+        ctx: Context<UpdateRegistrar>,
+        new_authority: Option<Pubkey>,
+        withdrawal_timelock: Option<i64>,
+        max_stake: Option<u64>,
+    ) -> Result<(), Error> {
+        let registrar = &mut ctx.accounts.registrar;
+
+        if let Some(new_authority) = new_authority {
+            registrar.authority = new_authority;
+        }
+
+        if let Some(withdrawal_timelock) = withdrawal_timelock {
+            registrar.withdrawal_timelock = withdrawal_timelock;
+        }
+
+        if let Some(max_stake) = max_stake {
+            registrar.max_stake = max_stake;
+        }
+
+        Ok(())
+    }
+
+    pub fn create_member(ctx: Context<CreateMember>, nonce: u8) -> Result<(), Error> {
+        let seeds = &[
+            ctx.accounts.registrar.to_account_info().key.as_ref(),
+            ctx.accounts.member.to_account_info().key.as_ref(),
+            &[nonce],
+        ];
+        let signer = &[&seeds[..]];
+
+        // Check the nonce + signer is correct.
+        let member_signer = Pubkey::create_program_address(seeds, ctx.program_id)
+            .map_err(|_| ErrorCode::InvalidNonce)?;
+        if &member_signer != ctx.accounts.member_signer.to_account_info().key {
+            return Err(ErrorCode::InvalidMemberSigner.into());
+        }
+
+        // Initialize member.
+        let member = &mut ctx.accounts.member;
+        member.registrar = *ctx.accounts.registrar.to_account_info().key;
+        member.beneficiary = *ctx.accounts.beneficiary.key;
+        member.balances = (&ctx.accounts.balances).into();
+        member.balances_locked = (&ctx.accounts.balances_locked).into();
+        member.nonce = nonce;
+
+        // Set delegate on staking tokens.
+        let (spt_approve, locked_spt_approve) = {
+            (
+                CpiContext::new_with_signer(
+                    ctx.accounts.token_program.clone(),
+                    token::Approve {
+                        to: ctx.accounts.balances.spt.to_account_info(),
+                        delegate: ctx.accounts.beneficiary.to_account_info(),
+                        authority: ctx.accounts.member_signer.to_account_info(),
+                    },
+                    signer,
+                ),
+                CpiContext::new_with_signer(
+                    ctx.accounts.token_program.clone(),
+                    token::Approve {
+                        to: ctx.accounts.balances_locked.spt.to_account_info(),
+                        delegate: ctx.accounts.beneficiary.to_account_info(),
+                        authority: ctx.accounts.member_signer.to_account_info(),
+                    },
+                    signer,
+                ),
+            )
+        };
+        token::approve(spt_approve, 0)?;
+        token::approve(locked_spt_approve, 0)?;
+
+        Ok(())
+    }
+
+    pub fn update_member(
+        ctx: Context<UpdateMember>,
+        metadata: Option<Pubkey>,
+    ) -> Result<(), Error> {
+        let member = &mut ctx.accounts.member;
+        if let Some(m) = metadata {
+            member.metadata = m;
+        }
+        Ok(())
+    }
+
+    pub fn deposit(ctx: Context<Deposit>, amount: u64) -> Result<(), Error> {
+        // Deposit authority *must*  match one of the balance ids.
+        // Similarly, the vault must match the balance id.
+        let vault = ctx.accounts.vault.to_account_info().key;
+
+        // Unlocked vault.
+        if ctx.accounts.depositor_authority.key == &ctx.accounts.member.balances.balance_id {
+            if vault != &ctx.accounts.member.balances.vault {
+                return Err(ErrorCode::InvalidVaultDeposit.into());
+            }
+        }
+        // Locked vault.
+        else if ctx.accounts.depositor_authority.key
+            == &ctx.accounts.member.balances_locked.balance_id
+        {
+            if vault != &ctx.accounts.member.balances_locked.vault {
+                return Err(ErrorCode::InvalidVaultDeposit.into());
+            }
+        }
+        // Unknown.
+        else {
+            return Err(ErrorCode::InvalidDepositor.into());
+        }
+
+        token::transfer(ctx.accounts.into(), amount).map_err(Into::into)
+    }
+
+    #[access_control(no_available_rewards(
+        &ctx.accounts.reward_event_q,
+        &ctx.accounts.member,
+        &ctx.accounts.balances,
+        &ctx.accounts.balances_locked,
+    ))]
+    pub fn stake(ctx: Context<Stake>, spt_amount: u64, balance_id: Pubkey) -> Result<(), Error> {
+        // Choose balances (locked or unlocked) based on balance_id.
+        let balances = {
+            if balance_id == ctx.accounts.member.beneficiary {
+                &ctx.accounts.balances
+            } else {
+                &ctx.accounts.balances_locked
+            }
+        };
+
+        // Transfer tokens into the stake vault.
+        {
+            // Convert from stake-token units to mint-token units.
+            let token_amount = spt_amount
+                .checked_mul(ctx.accounts.registrar.stake_rate)
+                .unwrap();
+
+            let seeds = &[
+                ctx.accounts.registrar.to_account_info().key.as_ref(),
+                ctx.accounts.member.to_account_info().key.as_ref(),
+                &[ctx.accounts.member.nonce],
+            ];
+            let member_signer = &[&seeds[..]];
+            let cpi_ctx = CpiContext::new_with_signer(
+                ctx.accounts.token_program.clone(),
+                token::Transfer {
+                    from: balances.vault.to_account_info(),
+                    to: balances.vault_stake.to_account_info(),
+                    authority: ctx.accounts.member_signer.to_account_info(),
+                },
+                member_signer,
+            );
+            token::transfer(cpi_ctx, token_amount)?;
+        }
+
+        // Mint pool tokens to the staker.
+        {
+            let seeds = &[
+                ctx.accounts.registrar.to_account_info().key.as_ref(),
+                &[ctx.accounts.registrar.nonce],
+            ];
+            let registrar_signer = &[&seeds[..]];
+
+            let cpi_ctx = CpiContext::new_with_signer(
+                ctx.accounts.token_program.clone(),
+                token::MintTo {
+                    mint: ctx.accounts.pool_mint.to_account_info(),
+                    to: balances.spt.to_account_info(),
+                    authority: ctx.accounts.registrar_signer.to_account_info(),
+                },
+                registrar_signer,
+            );
+            token::mint_to(cpi_ctx, spt_amount)?;
+        }
+
+        Ok(())
+    }
+
+    #[access_control(no_available_rewards(
+        &ctx.accounts.reward_event_q,
+        &ctx.accounts.member,
+        &ctx.accounts.balances,
+        &ctx.accounts.balances_locked,
+    ))]
+    pub fn start_unstake(
+        ctx: Context<StartUnstake>,
+        spt_amount: u64,
+        balance_id: Pubkey,
+    ) -> Result<(), Error> {
+        // Choose balances (locked or unlocked) based on balance_id.
+        let balances = {
+            if balance_id == ctx.accounts.member.beneficiary {
+                &ctx.accounts.balances
+            } else {
+                &ctx.accounts.balances_locked
+            }
+        };
+
+        // Program signer.
+        let seeds = &[
+            ctx.accounts.registrar.to_account_info().key.as_ref(),
+            ctx.accounts.member.to_account_info().key.as_ref(),
+            &[ctx.accounts.member.nonce],
+        ];
+        let member_signer = &[&seeds[..]];
+
+        // Burn pool tokens.
+        {
+            let cpi_ctx = CpiContext::new_with_signer(
+                ctx.accounts.token_program.clone(),
+                token::Burn {
+                    mint: ctx.accounts.pool_mint.to_account_info(),
+                    to: balances.spt.to_account_info(),
+                    authority: ctx.accounts.member_signer.to_account_info(),
+                },
+                member_signer,
+            );
+            token::burn(cpi_ctx, spt_amount)?;
+        }
+
+        // Convert from stake-token units to mint-token units.
+        let token_amount = spt_amount
+            .checked_mul(ctx.accounts.registrar.stake_rate)
+            .unwrap();
+
+        // Transfer tokens from the stake to pending vault.
+        {
+            let cpi_ctx = CpiContext::new_with_signer(
+                ctx.accounts.token_program.clone(),
+                token::Transfer {
+                    from: balances.vault_stake.to_account_info(),
+                    to: balances.vault_pw.to_account_info(),
+                    authority: ctx.accounts.member_signer.to_account_info(),
+                },
+                member_signer,
+            );
+            token::transfer(cpi_ctx, token_amount)?;
+        }
+
+        // Print receipt.
+        let pending_withdrawal = &mut ctx.accounts.pending_withdrawal;
+        pending_withdrawal.burned = false;
+        pending_withdrawal.member = *ctx.accounts.member.to_account_info().key;
+        pending_withdrawal.start_ts = ctx.accounts.clock.unix_timestamp;
+        pending_withdrawal.end_ts =
+            ctx.accounts.clock.unix_timestamp + ctx.accounts.registrar.withdrawal_timelock;
+        pending_withdrawal.amount = token_amount;
+        pending_withdrawal.pool = ctx.accounts.registrar.pool_mint;
+        pending_withdrawal.balance_id = balance_id;
+        pending_withdrawal.registrar = *ctx.accounts.registrar.to_account_info().key;
+
+        Ok(())
+    }
+
+    pub fn end_unstake(ctx: Context<EndUnstake>) -> Result<(), Error> {
+        if ctx.accounts.pending_withdrawal.end_ts > ctx.accounts.clock.unix_timestamp {
+            return Err(ErrorCode::UnstakeTimelock.into());
+        }
+
+        // Select which balance set this affects.
+        let balances = {
+            if ctx.accounts.pending_withdrawal.balance_id == ctx.accounts.member.balances.balance_id
+            {
+                &ctx.accounts.member.balances
+            } else if ctx.accounts.pending_withdrawal.balance_id
+                == ctx.accounts.member.balances_locked.balance_id
+            {
+                &ctx.accounts.member.balances_locked
+            } else {
+                return Err(ErrorCode::Unknown.into());
+            }
+        };
+        // Check the vaults given are corrrect.
+        if &balances.vault != ctx.accounts.vault.key {
+            return Err(ErrorCode::InvalidVault.into());
+        }
+        if &balances.vault_pw != ctx.accounts.vault_pw.key {
+            return Err(ErrorCode::InvalidVault.into());
+        }
+
+        // Transfer tokens between vaults.
+        {
+            let seeds = &[
+                ctx.accounts.registrar.to_account_info().key.as_ref(),
+                ctx.accounts.member.to_account_info().key.as_ref(),
+                &[ctx.accounts.member.nonce],
+            ];
+            let signer = &[&seeds[..]];
+            let cpi_ctx = CpiContext::new_with_signer(
+                ctx.accounts.token_program.clone(),
+                Transfer {
+                    from: ctx.accounts.vault_pw.to_account_info(),
+                    to: ctx.accounts.vault.to_account_info(),
+                    authority: ctx.accounts.member_signer.clone(),
+                },
+                signer,
+            );
+            token::transfer(cpi_ctx, ctx.accounts.pending_withdrawal.amount)?;
+        }
+
+        // Burn the pending withdrawal receipt.
+        let pending_withdrawal = &mut ctx.accounts.pending_withdrawal;
+        pending_withdrawal.burned = true;
+
+        Ok(())
+    }
+
+    pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<(), Error> {
+        // Deposit authority *must*  match one of the balance ids.
+        // Similarly, the vault must match the balance id.
+        let vault = ctx.accounts.vault.to_account_info().key;
+
+        // Unlocked vault.
+        if ctx.accounts.depositor_authority.key == &ctx.accounts.member.balances.balance_id {
+            if vault != &ctx.accounts.member.balances.vault {
+                return Err(ErrorCode::InvalidVaultDeposit.into());
+            }
+        }
+        // Locked vault.
+        else if ctx.accounts.depositor_authority.key
+            == &ctx.accounts.member.balances_locked.balance_id
+        {
+            if vault != &ctx.accounts.member.balances_locked.vault {
+                return Err(ErrorCode::InvalidVaultDeposit.into());
+            }
+        }
+        // Unknown.
+        else {
+            return Err(ErrorCode::InvalidDepositor.into());
+        }
+
+        let seeds = &[
+            ctx.accounts.registrar.to_account_info().key.as_ref(),
+            ctx.accounts.member.to_account_info().key.as_ref(),
+            &[ctx.accounts.member.nonce],
+        ];
+        let signer = &[&seeds[..]];
+        let cpi_accounts = Transfer {
+            from: ctx.accounts.vault.to_account_info(),
+            to: ctx.accounts.depositor.to_account_info(),
+            authority: ctx.accounts.member_signer.clone(),
+        };
+        let cpi_program = ctx.accounts.token_program.clone();
+        let cpi_ctx = CpiContext::new_with_signer(cpi_program, cpi_accounts, signer);
+
+        token::transfer(cpi_ctx, amount).map_err(Into::into)
+    }
+
+    pub fn drop_reward(
+        ctx: Context<DropReward>,
+        kind: RewardVendorKind,
+        total: u64,
+        expiry_ts: i64,
+        expiry_receiver: Pubkey,
+        nonce: u8,
+    ) -> Result<(), Error> {
+        // Validate args.
+        let vendor_signer = Pubkey::create_program_address(
+            &[
+                ctx.accounts.registrar.to_account_info().key.as_ref(),
+                ctx.accounts.vendor.to_account_info().key.as_ref(),
+                &[nonce],
+            ],
+            ctx.program_id,
+        )
+        .map_err(|_| ErrorCode::InvalidNonce)?;
+        if vendor_signer != ctx.accounts.vendor_vault.owner {
+            return Err(ErrorCode::InvalidVaultOwner.into());
+        }
+        if total < ctx.accounts.pool_mint.supply {
+            return Err(ErrorCode::InsufficientReward.into());
+        }
+        if ctx.accounts.clock.unix_timestamp >= expiry_ts {
+            return Err(ErrorCode::InvalidExpiry.into());
+        }
+
+        // Transfer funds into the vendor's vault.
+        token::transfer(ctx.accounts.into(), total)?;
+
+        // Add the event to the reward queue.
+        let reward_q = &mut ctx.accounts.reward_event_q;
+        let cursor = reward_q.append(RewardEvent {
+            vendor: *ctx.accounts.vendor.to_account_info().key,
+            ts: ctx.accounts.clock.unix_timestamp,
+            locked: kind != RewardVendorKind::Unlocked,
+        })?;
+
+        // Initialize the vendor.
+        let vendor = &mut ctx.accounts.vendor;
+        vendor.registrar = *ctx.accounts.registrar.to_account_info().key;
+        vendor.vault = *ctx.accounts.vendor_vault.to_account_info().key;
+        vendor.nonce = nonce;
+        vendor.pool_token_supply = ctx.accounts.pool_mint.supply;
+        vendor.reward_event_q_cursor = cursor;
+        vendor.start_ts = ctx.accounts.clock.unix_timestamp;
+        vendor.expiry_ts = expiry_ts;
+        vendor.expiry_receiver = expiry_receiver;
+        vendor.total = total;
+        vendor.expired = false;
+        vendor.kind = kind.clone();
+
+        Ok(())
+    }
+
+    #[access_control(reward_eligible(&ctx.accounts.cmn))]
+    pub fn claim_reward_unlocked(ctx: Context<ClaimRewardUnlocked>) -> Result<(), Error> {
+        if RewardVendorKind::Unlocked != ctx.accounts.cmn.vendor.kind {
+            return Err(ErrorCode::ExpectedUnlockedVendor.into());
+        }
+        // Reward to distribute.
+        let spt_total =
+            ctx.accounts.cmn.balances.spt.amount + ctx.accounts.cmn.balances_locked.spt.amount;
+        let reward_amount = spt_total
+            .checked_mul(ctx.accounts.cmn.vendor.total)
+            .unwrap()
+            .checked_div(ctx.accounts.cmn.vendor.pool_token_supply)
+            .unwrap();
+        assert!(reward_amount > 0);
+
+        // Vend reward to the member.
+        let seeds = &[
+            ctx.accounts.cmn.registrar.to_account_info().key.as_ref(),
+            ctx.accounts.cmn.vendor.to_account_info().key.as_ref(),
+            &[ctx.accounts.cmn.vendor.nonce],
+        ];
+        let signer = &[&seeds[..]];
+        let cpi_ctx = CpiContext::new_with_signer(
+            ctx.accounts.cmn.token_program.clone(),
+            token::Transfer {
+                to: ctx.accounts.token.to_account_info(),
+                from: ctx.accounts.cmn.vault.to_account_info(),
+                authority: ctx.accounts.cmn.vendor_signer.to_account_info(),
+            },
+            signer,
+        );
+        token::transfer(cpi_ctx, reward_amount)?;
+
+        // Update member as having processed the reward.
+        let member = &mut ctx.accounts.cmn.member;
+        member.rewards_cursor = ctx.accounts.cmn.vendor.reward_event_q_cursor + 1;
+
+        Ok(())
+    }
+
+    #[access_control(reward_eligible(&ctx.accounts.cmn))]
+    pub fn claim_reward_locked<'a, 'b, 'c, 'info>(
+        ctx: Context<'a, 'b, 'c, 'info, ClaimRewardLocked<'info>>,
+        nonce: u8,
+    ) -> Result<(), Error> {
+        let (end_ts, period_count) = match ctx.accounts.cmn.vendor.kind {
+            RewardVendorKind::Unlocked => return Err(ErrorCode::ExpectedLockedVendor.into()),
+            RewardVendorKind::Locked {
+                end_ts,
+                period_count,
+            } => (end_ts, period_count),
+        };
+        // 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,
+        };
+
+        // Calculate reward distribution.
+        let spt_total =
+            ctx.accounts.cmn.balances.spt.amount + ctx.accounts.cmn.balances_locked.spt.amount;
+        let reward_amount = spt_total
+            .checked_mul(ctx.accounts.cmn.vendor.total)
+            .unwrap()
+            .checked_div(ctx.accounts.cmn.vendor.pool_token_supply)
+            .unwrap();
+        assert!(reward_amount > 0);
+
+        // Vend reward to the member by creating a lockup account.
+        let seeds = &[
+            ctx.accounts.cmn.registrar.to_account_info().key.as_ref(),
+            ctx.accounts.cmn.vendor.to_account_info().key.as_ref(),
+            &[ctx.accounts.cmn.vendor.nonce],
+        ];
+        let signer = &[&seeds[..]];
+        let mut remaining_accounts: &[AccountInfo] = ctx.remaining_accounts;
+
+        let cpi_program = ctx.accounts.lockup_program.clone();
+        let cpi_accounts =
+            CreateVesting::try_accounts(ctx.accounts.lockup_program.key, &mut remaining_accounts)?;
+        let cpi_ctx = CpiContext::new_with_signer(cpi_program, cpi_accounts, signer);
+        serum_lockup::cpi::create_vesting(
+            cpi_ctx,
+            ctx.accounts.cmn.member.beneficiary,
+            end_ts,
+            period_count,
+            reward_amount,
+            nonce,
+        )?;
+
+        // Update the member account.
+        let member = &mut ctx.accounts.cmn.member;
+        member.rewards_cursor = ctx.accounts.cmn.vendor.reward_event_q_cursor + 1;
+
+        Ok(())
+    }
+
+    pub fn expire_reward(ctx: Context<ExpireReward>) -> Result<(), Error> {
+        if ctx.accounts.clock.unix_timestamp < ctx.accounts.vendor.expiry_ts {
+            return Err(ErrorCode::VendorNotYetExpired.into());
+        }
+
+        // Send all remaining funds to the expiry receiver.
+        let seeds = &[
+            ctx.accounts.registrar.to_account_info().key.as_ref(),
+            ctx.accounts.vendor.to_account_info().key.as_ref(),
+            &[ctx.accounts.vendor.nonce],
+        ];
+        let signer = &[&seeds[..]];
+        let cpi_ctx = CpiContext::new_with_signer(
+            ctx.accounts.token_program.clone(),
+            token::Transfer {
+                to: ctx.accounts.token.to_account_info(),
+                from: ctx.accounts.vault.to_account_info(),
+                authority: ctx.accounts.vendor_signer.to_account_info(),
+            },
+            signer,
+        );
+        token::transfer(cpi_ctx, ctx.accounts.vault.amount)?;
+
+        let vendor = &mut ctx.accounts.vendor;
+        vendor.expired = true;
+
+        Ok(())
+    }
+}
+
+fn reward_eligible(cmn: &ClaimRewardCommon) -> Result<(), Error> {
+    let vendor = &cmn.vendor;
+    let member = &cmn.member;
+    if vendor.expired {
+        return Err(ErrorCode::VendorExpired.into());
+    }
+    if member.rewards_cursor > vendor.reward_event_q_cursor {
+        return Err(ErrorCode::CursorAlreadyProcessed.into());
+    }
+    if member.last_stake_ts > vendor.start_ts {
+        return Err(ErrorCode::NotStakedDuringDrop.into());
+    }
+    Ok(())
+}
+
+// Asserts the user calling the `Stake` instruction has no rewards available
+// in the reward queue.
+pub fn no_available_rewards<'info>(
+    reward_q: &ProgramAccount<'info, RewardQueue>,
+    member: &ProgramAccount<'info, Member>,
+    balances: &BalanceSandboxAccounts<'info>,
+    balances_locked: &BalanceSandboxAccounts<'info>,
+) -> Result<(), Error> {
+    let mut cursor = member.rewards_cursor;
+
+    // If the member's cursor is less then the tail, then the ring buffer has
+    // overwritten those entries, so jump to the tail.
+    let tail = reward_q.tail();
+    if cursor < tail {
+        cursor = tail;
+    }
+
+    while cursor < reward_q.head() {
+        let r_event = reward_q.get(cursor);
+        if member.last_stake_ts < r_event.ts {
+            if balances.spt.amount > 0 || balances_locked.spt.amount > 0 {
+                return Err(ErrorCode::RewardsNeedsProcessing.into());
+            }
+        }
+        cursor += 1;
+    }
+
+    Ok(())
+}
+
+#[derive(Accounts)]
+pub struct Initialize<'info> {
+    #[account(init)]
+    registrar: ProgramAccount<'info, Registrar>,
+    pool_mint: CpiAccount<'info, Mint>,
+    #[account(init)]
+    reward_event_q: ProgramAccount<'info, RewardQueue>,
+    rent: Sysvar<'info, Rent>,
+}
+
+#[derive(Accounts)]
+pub struct UpdateRegistrar<'info> {
+    #[account(mut, has_one = authority)]
+    registrar: ProgramAccount<'info, Registrar>,
+    #[account(signer)]
+    authority: AccountInfo<'info>,
+}
+
+#[derive(Accounts)]
+pub struct CreateMember<'info> {
+    registrar: ProgramAccount<'info, Registrar>,
+    #[account(init)]
+    member: ProgramAccount<'info, Member>,
+    #[account(signer)]
+    beneficiary: AccountInfo<'info>,
+    // Must be verified against the user given nonce.
+    member_signer: AccountInfo<'info>,
+    #[account(
+        "balances.balance_id.key == beneficiary.key",
+        "&balances.spt.owner == member_signer.key",
+        "balances.spt.mint == registrar.pool_mint",
+        "balances.vault.mint == registrar.mint",
+        "balances.spt.delegate == COption::None"
+    )]
+    balances: BalanceSandboxAccounts<'info>,
+    #[account(
+        // Locked balance_id is unchecked; it's determined by the lockup program.
+        "&balances_locked.spt.owner == member_signer.key",
+        "balances_locked.spt.mint == registrar.pool_mint",
+        "balances_locked.vault.mint == registrar.mint",
+        "balances_locked.spt.delegate == COption::None"
+    )]
+    balances_locked: BalanceSandboxAccounts<'info>,
+    #[account("token_program.key == &token::ID")]
+    token_program: AccountInfo<'info>,
+    rent: Sysvar<'info, Rent>,
+}
+
+#[derive(Accounts, Clone)]
+pub struct BalanceSandboxAccounts<'info> {
+    balance_id: AccountInfo<'info>,
+    #[account(mut)]
+    spt: CpiAccount<'info, TokenAccount>,
+    #[account(mut, "vault.owner == spt.owner")]
+    vault: CpiAccount<'info, TokenAccount>,
+    #[account(
+        mut,
+        "vault_stake.owner == spt.owner",
+        "vault_stake.mint == vault.mint"
+    )]
+    vault_stake: CpiAccount<'info, TokenAccount>,
+    #[account(mut, "vault_pw.owner == spt.owner", "vault_pw.mint == vault.mint")]
+    vault_pw: CpiAccount<'info, TokenAccount>,
+}
+
+impl<'info> From<&BalanceSandboxAccounts<'info>> for BalanceSandbox {
+    fn from(accs: &BalanceSandboxAccounts<'info>) -> Self {
+        Self {
+            balance_id: *accs.balance_id.key,
+            spt: *accs.spt.to_account_info().key,
+            vault: *accs.vault.to_account_info().key,
+            vault_stake: *accs.vault_stake.to_account_info().key,
+            vault_pw: *accs.vault_pw.to_account_info().key,
+        }
+    }
+}
+
+#[derive(Accounts)]
+pub struct UpdateMember<'info> {
+    #[account(mut, has_one = beneficiary)]
+    member: ProgramAccount<'info, Member>,
+    #[account(signer)]
+    beneficiary: AccountInfo<'info>,
+}
+
+#[derive(Accounts)]
+pub struct Deposit<'info> {
+    // Lockup whitelist relay interface.
+    dummy_vesting: AccountInfo<'info>,
+    #[account(mut)]
+    depositor: AccountInfo<'info>,
+    #[account(signer)]
+    depositor_authority: AccountInfo<'info>,
+    #[account("token_program.key == &token::ID")]
+    token_program: AccountInfo<'info>,
+    #[account(mut, "&vault.owner == member_signer.key")]
+    vault: CpiAccount<'info, TokenAccount>,
+    #[account(
+        seeds = [
+            registrar.to_account_info().key.as_ref(),
+            member.to_account_info().key.as_ref(),
+            &[member.nonce],
+        ]
+    )]
+    member_signer: AccountInfo<'info>,
+
+    // Program specific.
+    registrar: ProgramAccount<'info, Registrar>,
+    #[account(signer)]
+    beneficiary: AccountInfo<'info>,
+    #[account(belongs_to = registrar, belongs_to = beneficiary)]
+    member: ProgramAccount<'info, Member>,
+}
+
+impl<'a, 'b, 'c, 'info> From<&mut Deposit<'info>>
+    for CpiContext<'a, 'b, 'c, 'info, Transfer<'info>>
+{
+    fn from(accounts: &mut Deposit<'info>) -> CpiContext<'a, 'b, 'c, 'info, Transfer<'info>> {
+        let cpi_accounts = Transfer {
+            from: accounts.depositor.clone(),
+            to: accounts.vault.to_account_info(),
+            authority: accounts.depositor_authority.clone(),
+        };
+        let cpi_program = accounts.token_program.clone();
+        CpiContext::new(cpi_program, cpi_accounts)
+    }
+}
+
+#[derive(Accounts)]
+pub struct Stake<'info> {
+    // Global accounts for the staking instance.
+    #[account(has_one = pool_mint, has_one = reward_event_q)]
+    registrar: ProgramAccount<'info, Registrar>,
+    reward_event_q: ProgramAccount<'info, RewardQueue>,
+    #[account(mut)]
+    pool_mint: CpiAccount<'info, Mint>,
+
+    // Member specific.
+    #[account(mut, has_one = beneficiary, belongs_to = registrar)]
+    member: ProgramAccount<'info, Member>,
+    #[account(signer)]
+    beneficiary: AccountInfo<'info>,
+    #[account("BalanceSandbox::from(&balances) == member.balances")]
+    balances: BalanceSandboxAccounts<'info>,
+    #[account("BalanceSandbox::from(&balances_locked) == member.balances_locked")]
+    balances_locked: BalanceSandboxAccounts<'info>,
+
+    // Programmatic signers.
+    #[account(
+        seeds = [
+            registrar.to_account_info().key.as_ref(),
+            member.to_account_info().key.as_ref(),
+            &[member.nonce],
+        ]
+    )]
+    member_signer: AccountInfo<'info>,
+    #[account(
+        seeds = [
+            registrar.to_account_info().key.as_ref(),
+            &[registrar.nonce],
+        ]
+    )]
+    registrar_signer: AccountInfo<'info>,
+
+    // Misc.
+    clock: Sysvar<'info, Clock>,
+    #[account("token_program.key == &token::ID")]
+    token_program: AccountInfo<'info>,
+}
+
+#[derive(Accounts)]
+pub struct StartUnstake<'info> {
+    // Stake instance globals.
+    registrar: ProgramAccount<'info, Registrar>,
+    reward_event_q: ProgramAccount<'info, RewardQueue>,
+    #[account(mut)]
+    pool_mint: AccountInfo<'info>,
+
+    // Member.
+    #[account(init)]
+    pending_withdrawal: ProgramAccount<'info, PendingWithdrawal>,
+    #[account(belongs_to = registrar)]
+    member: ProgramAccount<'info, Member>,
+    #[account(signer)]
+    beneficiary: AccountInfo<'info>,
+    #[account(
+        "&balances.spt.owner == member_signer.key",
+        "balances.spt.mint == registrar.pool_mint",
+        "balances.vault.mint == registrar.mint"
+    )]
+    balances: BalanceSandboxAccounts<'info>,
+    #[account(
+        "&balances_locked.spt.owner == member_signer.key",
+        "balances_locked.spt.mint == registrar.pool_mint",
+        "balances_locked.vault.mint == registrar.mint"
+    )]
+    balances_locked: BalanceSandboxAccounts<'info>,
+
+    // Programmatic signers.
+    #[account(
+        seeds = [
+            registrar.to_account_info().key.as_ref(),
+            member.to_account_info().key.as_ref(),
+            &[member.nonce],
+        ]
+    )]
+    member_signer: AccountInfo<'info>,
+
+    // Misc.
+    #[account("token_program.key == &token::ID")]
+    token_program: AccountInfo<'info>,
+    clock: Sysvar<'info, Clock>,
+    rent: Sysvar<'info, Rent>,
+}
+
+#[derive(Accounts)]
+pub struct EndUnstake<'info> {
+    registrar: ProgramAccount<'info, Registrar>,
+
+    #[account(belongs_to = registrar, has_one = beneficiary)]
+    member: ProgramAccount<'info, Member>,
+    #[account(signer)]
+    beneficiary: AccountInfo<'info>,
+    #[account(mut, belongs_to = registrar, belongs_to = member, "!pending_withdrawal.burned")]
+    pending_withdrawal: ProgramAccount<'info, PendingWithdrawal>,
+
+    // if we had ordered maps implementing Accounts we could do a constraint like
+    // balances.get(pending_withdrawal.balance_id).vault == vault.key
+    #[account(mut)]
+    vault: AccountInfo<'info>,
+    #[account(mut)]
+    vault_pw: AccountInfo<'info>,
+
+    #[account(
+        seeds = [
+            registrar.to_account_info().key.as_ref(),
+            member.to_account_info().key.as_ref(),
+            &[member.nonce],
+        ]
+    )]
+    member_signer: AccountInfo<'info>,
+
+    clock: Sysvar<'info, Clock>,
+    #[account("token_program.key == &token::ID")]
+    token_program: AccountInfo<'info>,
+}
+
+#[derive(Accounts)]
+pub struct Withdraw<'info> {
+    // Lockup whitelist relay interface.
+    dummy_vesting: AccountInfo<'info>,
+    #[account(mut)]
+    depositor: AccountInfo<'info>,
+    #[account(signer)]
+    depositor_authority: AccountInfo<'info>,
+    #[account("token_program.key == &token::ID")]
+    token_program: AccountInfo<'info>,
+    #[account(mut, "&vault.owner == member_signer.key")]
+    vault: CpiAccount<'info, TokenAccount>,
+    #[account(
+        seeds = [
+            registrar.to_account_info().key.as_ref(),
+            member.to_account_info().key.as_ref(),
+            &[member.nonce],
+        ]
+    )]
+    member_signer: AccountInfo<'info>,
+
+    // Program specific.
+    registrar: ProgramAccount<'info, Registrar>,
+    #[account(signer)]
+    beneficiary: AccountInfo<'info>,
+    #[account(belongs_to = registrar, belongs_to = beneficiary)]
+    member: ProgramAccount<'info, Member>,
+}
+
+#[derive(Accounts)]
+pub struct DropReward<'info> {
+    // Staking instance.
+    #[account(has_one = reward_event_q, has_one = pool_mint)]
+    registrar: ProgramAccount<'info, Registrar>,
+    #[account(mut)]
+    reward_event_q: ProgramAccount<'info, RewardQueue>,
+    pool_mint: CpiAccount<'info, Mint>,
+
+    // Vendor.
+    #[account(init)]
+    vendor: ProgramAccount<'info, RewardVendor>,
+    #[account(mut)]
+    vendor_vault: CpiAccount<'info, TokenAccount>,
+
+    // Depositor.
+    #[account(mut)]
+    depositor: AccountInfo<'info>,
+    #[account(signer)]
+    depositor_authority: AccountInfo<'info>,
+
+    // Misc.
+    #[account("token_program.key == &token::ID")]
+    token_program: AccountInfo<'info>,
+    clock: Sysvar<'info, Clock>,
+    rent: Sysvar<'info, Rent>,
+}
+
+impl<'a, 'b, 'c, 'info> From<&mut DropReward<'info>>
+    for CpiContext<'a, 'b, 'c, 'info, Transfer<'info>>
+{
+    fn from(accounts: &mut DropReward<'info>) -> CpiContext<'a, 'b, 'c, 'info, Transfer<'info>> {
+        let cpi_accounts = Transfer {
+            from: accounts.depositor.clone(),
+            to: accounts.vendor_vault.to_account_info(),
+            authority: accounts.depositor_authority.clone(),
+        };
+        let cpi_program = accounts.token_program.clone();
+        CpiContext::new(cpi_program, cpi_accounts)
+    }
+}
+
+#[derive(Accounts)]
+pub struct ClaimRewardUnlocked<'info> {
+    cmn: ClaimRewardCommon<'info>,
+    // Account to send reward to.
+    #[account(mut)]
+    token: AccountInfo<'info>,
+}
+
+#[derive(Accounts)]
+pub struct ClaimRewardLocked<'info> {
+    cmn: ClaimRewardCommon<'info>,
+    // TODO: assert on the lockup program id once deployed.
+    lockup_program: AccountInfo<'info>,
+}
+
+// Accounts common to both claim reward locked/unlocked instructions.
+#[derive(Accounts)]
+pub struct ClaimRewardCommon<'info> {
+    // Stake instance.
+    registrar: ProgramAccount<'info, Registrar>,
+
+    // Member.
+    #[account(mut, belongs_to = registrar)]
+    member: ProgramAccount<'info, Member>,
+    #[account(signer)]
+    beneficiary: AccountInfo<'info>,
+    #[account("BalanceSandbox::from(&balances) == member.balances")]
+    balances: BalanceSandboxAccounts<'info>,
+    #[account("BalanceSandbox::from(&balances_locked) == member.balances_locked")]
+    balances_locked: BalanceSandboxAccounts<'info>,
+
+    // Vendor.
+    #[account(belongs_to = registrar, has_one = vault)]
+    vendor: ProgramAccount<'info, RewardVendor>,
+    #[account(mut)]
+    vault: AccountInfo<'info>,
+    #[account(
+        seeds = [
+            registrar.to_account_info().key.as_ref(),
+            vendor.to_account_info().key.as_ref(),
+            &[vendor.nonce],
+        ]
+    )]
+    vendor_signer: AccountInfo<'info>,
+
+    // Misc.
+    #[account("token_program.key == &token::ID")]
+    token_program: AccountInfo<'info>,
+    clock: Sysvar<'info, Clock>,
+}
+
+#[derive(Accounts)]
+pub struct ExpireReward<'info> {
+    // Staking instance globals.
+    registrar: ProgramAccount<'info, Registrar>,
+
+    // Vendor.
+    #[account(mut, belongs_to = registrar, has_one = vault, has_one = expiry_receiver)]
+    vendor: ProgramAccount<'info, RewardVendor>,
+    #[account(mut)]
+    vault: CpiAccount<'info, TokenAccount>,
+    #[account(
+        seeds = [
+            registrar.to_account_info().key.as_ref(),
+            vendor.to_account_info().key.as_ref(),
+            &[vendor.nonce],
+        ]
+    )]
+    vendor_signer: AccountInfo<'info>,
+
+    // Receiver.
+    #[account(signer)]
+    expiry_receiver: AccountInfo<'info>,
+    #[account(mut)]
+    token: AccountInfo<'info>,
+
+    // Misc.
+    #[account("token_program.key == &token::ID")]
+    token_program: AccountInfo<'info>,
+    clock: Sysvar<'info, Clock>,
+}
+
+#[account]
+pub struct Registrar {
+    /// Priviledged account.
+    pub authority: Pubkey,
+    /// Nonce to derive the program-derived address owning the vaults.
+    pub nonce: u8,
+    /// The maximum stake per member, denominated in the mint.
+    pub max_stake: u64,
+    /// Number of seconds that must pass for a withdrawal to complete.
+    pub withdrawal_timelock: i64,
+    /// Global event queue for reward vendoring.
+    pub reward_event_q: Pubkey,
+    /// Mint of the tokens that can be staked.
+    pub mint: Pubkey,
+    /// Staking pool token mint.
+    pub pool_mint: Pubkey,
+    /// The amount of tokens (not decimal) that must be staked to get a single
+    /// staking pool token.
+    pub stake_rate: u64,
+}
+
+#[account]
+pub struct Member {
+    /// Registrar the member belongs to.
+    pub registrar: Pubkey,
+    /// The effective owner of the Member account.
+    pub beneficiary: Pubkey,
+    /// Arbitrary metadata account owned by any program.
+    pub metadata: Pubkey,
+    /// Sets of balances owned by the Member.
+    pub balances: BalanceSandbox,
+    /// Locked balances owned by the Member.
+    pub balances_locked: BalanceSandbox,
+    /// Next position in the rewards event queue to process.
+    pub rewards_cursor: u32,
+    /// The clock timestamp of the last time this account staked or switched
+    /// entities. Used as a proof to reward vendors that the Member account
+    /// was staked at a given point in time.
+    pub last_stake_ts: i64,
+    /// Signer nonce.
+    pub nonce: u8,
+}
+
+// BalanceSandbox defines isolated funds that can only be deposited/withdrawn
+// into the program if the `owner` signs off on the transaction.
+//
+// Once controlled by the program, the associated `Member` account's beneficiary
+// can send funds to/from any of the accounts within the sandbox, e.g., to
+// stake.
+#[derive(AnchorSerialize, AnchorDeserialize, Default, Debug, Clone, PartialEq)]
+pub struct BalanceSandbox {
+    pub balance_id: Pubkey,
+    // Staking pool token.
+    pub spt: Pubkey,
+    // Free balance (deposit) vaults.
+    pub vault: Pubkey,
+    // Stake vaults.
+    pub vault_stake: Pubkey,
+    // Pending withdrawal vaults.
+    pub vault_pw: Pubkey,
+}
+
+#[account]
+pub struct PendingWithdrawal {
+    /// Registrar this account belongs to.
+    pub registrar: Pubkey,
+    /// Member this account belongs to.
+    pub member: Pubkey,
+    /// One time token. True if the withdrawal has been completed.
+    pub burned: bool,
+    /// The pool being withdrawn from.
+    pub pool: Pubkey,
+    /// Unix timestamp when this account was initialized.
+    pub start_ts: i64,
+    /// Timestamp when the pending withdrawal completes.
+    pub end_ts: i64,
+    /// The number of tokens redeemed from the staking pool.
+    pub amount: u64,
+    /// The Member account's set of vaults this withdrawal belongs to.
+    pub balance_id: Pubkey,
+}
+
+#[account]
+pub struct RewardQueue {
+    // Invariant: index is position of the next available slot.
+    head: u32,
+    // Invariant: index is position of the first (oldest) taken slot.
+    // Invariant: head == tail => queue is initialized.
+    // Invariant: index_of(head + 1) == index_of(tail) => queue is full.
+    tail: u32,
+    // Although a vec is used, the size is immutable.
+    events: Vec<RewardEvent>,
+}
+
+impl RewardQueue {
+    pub fn append(&mut self, event: RewardEvent) -> Result<u32, Error> {
+        let cursor = self.head;
+
+        // Insert into next available slot.
+        let h_idx = self.index_of(self.head);
+        self.events[h_idx] = event;
+
+        // Update head and tail counters.
+        let is_full = self.index_of(self.head + 1) == self.index_of(self.tail);
+        if is_full {
+            self.tail += 1;
+        }
+        self.head += 1;
+
+        Ok(cursor)
+    }
+
+    pub fn index_of(&self, counter: u32) -> usize {
+        counter as usize % self.capacity()
+    }
+
+    pub fn capacity(&self) -> usize {
+        self.events.len()
+    }
+
+    pub fn get(&self, cursor: u32) -> &RewardEvent {
+        &self.events[cursor as usize % self.capacity()]
+    }
+
+    pub fn head(&self) -> u32 {
+        self.head
+    }
+
+    pub fn tail(&self) -> u32 {
+        self.tail
+    }
+}
+
+#[derive(Default, Clone, Copy, Debug, AnchorSerialize, AnchorDeserialize)]
+pub struct RewardEvent {
+    vendor: Pubkey,
+    ts: i64,
+    locked: bool,
+}
+
+#[account]
+pub struct RewardVendor {
+    pub registrar: Pubkey,
+    pub vault: Pubkey,
+    pub nonce: u8,
+    pub pool_token_supply: u64,
+    pub reward_event_q_cursor: u32,
+    pub start_ts: i64,
+    pub expiry_ts: i64,
+    pub expiry_receiver: Pubkey,
+    pub total: u64,
+    pub expired: bool,
+    pub kind: RewardVendorKind,
+}
+
+#[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq)]
+pub enum RewardVendorKind {
+    Unlocked,
+    Locked { end_ts: i64, period_count: u64 },
+}
+
+#[error]
+pub enum ErrorCode {
+    #[msg("The given reward queue has already been initialized.")]
+    RewardQAlreadyInitialized,
+    #[msg("The nonce given doesn't derive a valid program address.")]
+    InvalidNonce,
+    #[msg("Invalid pool mint authority")]
+    InvalidPoolMintAuthority,
+    #[msg("Member signer doesn't match the derived address.")]
+    InvalidMemberSigner,
+    #[msg("The given vault owner must match the signing depositor.")]
+    InvalidVaultDeposit,
+    #[msg("The signing depositor doesn't match either of the balance accounts")]
+    InvalidDepositor,
+    #[msg("The vault given does not match the vault expected.")]
+    InvalidVault,
+    #[msg("Invalid vault owner.")]
+    InvalidVaultOwner,
+    #[msg("An unknown error has occured.")]
+    Unknown,
+    #[msg("The unstake timelock has not yet expired.")]
+    UnstakeTimelock,
+    #[msg("Reward vendors must have at least one token unit per pool token")]
+    InsufficientReward,
+    #[msg("Reward expiry must be after the current clock timestamp.")]
+    InvalidExpiry,
+    #[msg("The reward vendor has been expired.")]
+    VendorExpired,
+    #[msg("This reward has already been processed.")]
+    CursorAlreadyProcessed,
+    #[msg("The account was not staked at the time of this reward.")]
+    NotStakedDuringDrop,
+    #[msg("The vendor is not yet eligible for expiry.")]
+    VendorNotYetExpired,
+    #[msg("Please collect your reward before otherwise using the program.")]
+    RewardsNeedsProcessing,
+    #[msg("Locked reward vendor expected but an unlocked vendor was given.")]
+    ExpectedLockedVendor,
+    #[msg("Unlocked reward vendor expected but a locked vendor was given.")]
+    ExpectedUnlockedVendor,
+}

+ 877 - 0
examples/lockup/tests/lockup.js

@@ -0,0 +1,877 @@
+const assert = require("assert");
+const anchor = require('@project-serum/anchor');
+const serumCmn = require("@project-serum/common");
+const TokenInstructions = require("@project-serum/serum").TokenInstructions;
+const utils = require("./utils");
+
+describe("Lockup and Registry", () => {
+  const provider = anchor.Provider.local();
+
+  // Configure the client to use the local cluster.
+  anchor.setProvider(provider);
+
+  const lockup = anchor.workspace.Lockup;
+  const registry = anchor.workspace.Registry;
+
+  const safe = new anchor.web3.Account();
+  const whitelist = new anchor.web3.Account();
+
+  let mint = null;
+  let god = null;
+
+  it("Sets up initial test state", async () => {
+    const [_mint, _god] = await serumCmn.createMintAndVault(
+      provider,
+      new anchor.BN(1000000)
+    );
+    mint = _mint;
+    god = _god;
+  });
+
+  it("Is initialized!", async () => {
+    await lockup.rpc.initialize(provider.wallet.publicKey, {
+      accounts: {
+        safe: safe.publicKey,
+        whitelist: whitelist.publicKey,
+        rent: anchor.web3.SYSVAR_RENT_PUBKEY,
+      },
+      signers: [safe, whitelist],
+      instructions: [
+        await lockup.account.safe.createInstruction(safe),
+        await lockup.account.whitelist.createInstruction(whitelist, 1000),
+      ],
+    });
+    const safeAccount = await lockup.account.safe(safe.publicKey);
+    const whitelistAccount = await lockup.account.whitelist(
+      whitelist.publicKey
+    );
+
+    assert.ok(safeAccount.authority.equals(provider.wallet.publicKey));
+    assert.ok(safeAccount.whitelist.equals(whitelist.publicKey));
+
+    assert.ok(whitelistAccount.safe.equals(safe.publicKey));
+    assert.ok(whitelistAccount.entries.length === 0);
+  });
+
+  it("Sets a new authority", async () => {
+    const newAuthority = new anchor.web3.Account();
+    await lockup.rpc.setAuthority(newAuthority.publicKey, {
+      accounts: {
+        authority: provider.wallet.publicKey,
+        safe: safe.publicKey,
+      },
+    });
+
+    let safeAccount = await lockup.account.safe(safe.publicKey);
+    assert.ok(safeAccount.authority.equals(newAuthority.publicKey));
+
+    await lockup.rpc.setAuthority(provider.wallet.publicKey, {
+      accounts: {
+        authority: newAuthority.publicKey,
+        safe: safe.publicKey,
+      },
+      signers: [newAuthority],
+    });
+
+    safeAccount = await lockup.account.safe(safe.publicKey);
+    assert.ok(safeAccount.authority.equals(provider.wallet.publicKey));
+  });
+
+  let e0 = null;
+  let e1 = null;
+  let e2 = null;
+  let e3 = null;
+  let e4 = null;
+
+  it("Adds to the whitelist", async () => {
+    const generateEntry = async () => {
+      let programId = new anchor.web3.Account().publicKey;
+      let instance = new anchor.web3.Account().publicKey;
+      let [_, nonce] = await anchor.web3.PublicKey.findProgramAddress(
+        [instance.toBuffer()],
+        programId
+      );
+      return {
+        programId,
+        instance,
+        nonce,
+      };
+    };
+    e0 = await generateEntry();
+    e1 = await generateEntry();
+    e2 = await generateEntry();
+    e3 = await generateEntry();
+    e4 = await generateEntry();
+    const e5 = await generateEntry();
+
+    const accounts = {
+      authority: provider.wallet.publicKey,
+      safe: safe.publicKey,
+      whitelist: whitelist.publicKey,
+    };
+
+    await lockup.rpc.whitelistAdd(e0, { accounts });
+
+    let whitelistAccount = await lockup.account.whitelist(whitelist.publicKey);
+
+    assert.ok(whitelistAccount.entries.length === 1);
+    assert.deepEqual(whitelistAccount.entries, [e0]);
+
+    await lockup.rpc.whitelistAdd(e1, { accounts });
+    await lockup.rpc.whitelistAdd(e2, { accounts });
+    await lockup.rpc.whitelistAdd(e3, { accounts });
+    await lockup.rpc.whitelistAdd(e4, { accounts });
+
+    whitelistAccount = await lockup.account.whitelist(whitelist.publicKey);
+
+    assert.deepEqual(whitelistAccount.entries, [e0, e1, e2, e3, e4]);
+
+    await assert.rejects(
+      async () => {
+        await lockup.rpc.whitelistAdd(e5, { accounts });
+      },
+      (err) => {
+        assert.equal(err.code, 108);
+        assert.equal(err.msg, "Whitelist is full");
+        return true;
+      }
+    );
+  });
+
+  it("Removes from the whitelist", async () => {
+    await lockup.rpc.whitelistDelete(e0, {
+      accounts: {
+        authority: provider.wallet.publicKey,
+        safe: safe.publicKey,
+        whitelist: whitelist.publicKey,
+      },
+    });
+    let whitelistAccount = await lockup.account.whitelist(whitelist.publicKey);
+    assert.deepEqual(whitelistAccount.entries, [e1, e2, e3, e4]);
+  });
+
+  const vesting = new anchor.web3.Account();
+  let vestingAccount = null;
+  let vaultAuthority = null;
+
+  it("Creates a vesting account", async () => {
+    const beneficiary = provider.wallet.publicKey;
+    const endTs = new anchor.BN(Date.now() / 1000 + 3);
+    const periodCount = new anchor.BN(5);
+    const depositAmount = new anchor.BN(100);
+
+    const vault = new anchor.web3.Account();
+    let [
+      _vaultAuthority,
+      nonce,
+    ] = await anchor.web3.PublicKey.findProgramAddress(
+      [safe.publicKey.toBuffer(), beneficiary.toBuffer()],
+      lockup.programId
+    );
+    vaultAuthority = _vaultAuthority;
+
+    await lockup.rpc.createVesting(
+      beneficiary,
+      endTs,
+      periodCount,
+      depositAmount,
+      nonce,
+      {
+        accounts: {
+          vesting: vesting.publicKey,
+          safe: safe.publicKey,
+          vault: vault.publicKey,
+          depositor: god,
+          depositorAuthority: provider.wallet.publicKey,
+          tokenProgram: TokenInstructions.TOKEN_PROGRAM_ID,
+          rent: anchor.web3.SYSVAR_RENT_PUBKEY,
+          clock: anchor.web3.SYSVAR_CLOCK_PUBKEY,
+        },
+        signers: [vesting, vault],
+        instructions: [
+          await lockup.account.vesting.createInstruction(vesting),
+          ...(await serumCmn.createTokenAccountInstrs(
+            provider,
+            vault.publicKey,
+            mint,
+            vaultAuthority
+          )),
+        ],
+      }
+    );
+
+    vestingAccount = await lockup.account.vesting(vesting.publicKey);
+
+    assert.ok(vestingAccount.safe.equals(safe.publicKey));
+    assert.ok(vestingAccount.beneficiary.equals(provider.wallet.publicKey));
+    assert.ok(vestingAccount.mint.equals(mint));
+    assert.ok(vestingAccount.grantor.equals(provider.wallet.publicKey));
+    assert.ok(vestingAccount.outstanding.eq(depositAmount));
+    assert.ok(vestingAccount.startBalance.eq(depositAmount));
+    assert.ok(vestingAccount.endTs.eq(endTs));
+    assert.ok(vestingAccount.periodCount.eq(periodCount));
+    assert.ok(vestingAccount.whitelistOwned.eq(new anchor.BN(0)));
+    assert.equal(vestingAccount.nonce, nonce);
+    assert.ok(endTs.gt(vestingAccount.startTs));
+  });
+
+  it("Fails to withdraw from a vesting account before vesting", async () => {
+    await assert.rejects(
+      async () => {
+        await lockup.rpc.withdraw(new anchor.BN(100), {
+          accounts: {
+            safe: safe.publicKey,
+            vesting: vesting.publicKey,
+            beneficiary: provider.wallet.publicKey,
+            token: god,
+            vault: vestingAccount.vault,
+            vaultAuthority: vaultAuthority,
+            tokenProgram: TokenInstructions.TOKEN_PROGRAM_ID,
+            clock: anchor.web3.SYSVAR_CLOCK_PUBKEY,
+          },
+        });
+      },
+      (err) => {
+        assert.equal(err.code, 107);
+        assert.equal(err.msg, "Insufficient withdrawal balance.");
+        return true;
+      }
+    );
+  });
+
+  it("Waits for a vesting period to pass", async () => {
+    await serumCmn.sleep(5 * 1000);
+  });
+
+  it("Withdraws from the vesting account", async () => {
+    const token = await serumCmn.createTokenAccount(
+      provider,
+      mint,
+      provider.wallet.publicKey
+    );
+
+    await lockup.rpc.withdraw(new anchor.BN(100), {
+      accounts: {
+        safe: safe.publicKey,
+        vesting: vesting.publicKey,
+        beneficiary: provider.wallet.publicKey,
+        token,
+        vault: vestingAccount.vault,
+        vaultAuthority: vaultAuthority,
+        tokenProgram: TokenInstructions.TOKEN_PROGRAM_ID,
+        clock: anchor.web3.SYSVAR_CLOCK_PUBKEY,
+      },
+    });
+
+    vestingAccount = await lockup.account.vesting(vesting.publicKey);
+    assert.ok(vestingAccount.outstanding.eq(new anchor.BN(0)));
+
+    const vaultAccount = await serumCmn.getTokenAccount(
+      provider,
+      vestingAccount.vault
+    );
+    assert.ok(vaultAccount.amount.eq(new anchor.BN(0)));
+
+    const tokenAccount = await serumCmn.getTokenAccount(provider, token);
+    assert.ok(tokenAccount.amount.eq(new anchor.BN(100)));
+  });
+
+  const registrar = new anchor.web3.Account();
+  const rewardQ = new anchor.web3.Account();
+  const withdrawalTimelock = new anchor.BN(5);
+  const maxStake = new anchor.BN("1000000000000000000");
+  const stakeRate = new anchor.BN(2);
+  const rewardQLen = 100;
+  let registrarAccount = null;
+  let registrarSigner = null;
+  let nonce = null;
+  let poolMint = null;
+
+  it("Creates registry genesis", async () => {
+    const [
+      _registrarSigner,
+      _nonce,
+    ] = await anchor.web3.PublicKey.findProgramAddress(
+      [registrar.publicKey.toBuffer()],
+      registry.programId
+    );
+    registrarSigner = _registrarSigner;
+    nonce = _nonce;
+    poolMint = await serumCmn.createMint(provider, registrarSigner);
+  });
+
+  it("Initializes the registrar", async () => {
+    await registry.rpc.initialize(
+      mint,
+      provider.wallet.publicKey,
+      nonce,
+      withdrawalTimelock,
+      maxStake,
+      stakeRate,
+      rewardQLen,
+      {
+        accounts: {
+          registrar: registrar.publicKey,
+          poolMint,
+          rewardEventQ: rewardQ.publicKey,
+          rent: anchor.web3.SYSVAR_RENT_PUBKEY,
+        },
+        signers: [registrar, rewardQ],
+        instructions: [
+          await registry.account.registrar.createInstruction(registrar),
+          await registry.account.rewardQueue.createInstruction(rewardQ, 8250),
+        ],
+      }
+    );
+
+    registrarAccount = await registry.account.registrar(registrar.publicKey);
+
+    assert.ok(registrarAccount.authority.equals(provider.wallet.publicKey));
+    assert.equal(registrarAccount.nonce, nonce);
+    assert.ok(registrarAccount.mint.equals(mint));
+    assert.ok(registrarAccount.poolMint.equals(poolMint));
+    assert.ok(registrarAccount.stakeRate.eq(stakeRate));
+    assert.ok(registrarAccount.rewardEventQ.equals(rewardQ.publicKey));
+    assert.ok(registrarAccount.withdrawalTimelock.eq(withdrawalTimelock));
+    assert.ok(registrarAccount.maxStake.eq(maxStake));
+  });
+
+  const member = new anchor.web3.Account();
+  let memberAccount = null;
+  let memberSigner = null;
+  let balances = null;
+  let balancesLocked = null;
+
+  it("Creates a member", async () => {
+    const [
+      _memberSigner,
+      nonce,
+    ] = await anchor.web3.PublicKey.findProgramAddress(
+      [registrar.publicKey.toBuffer(), member.publicKey.toBuffer()],
+      registry.programId
+    );
+    memberSigner = _memberSigner;
+
+    const [mainTx, _balances] = await utils.createBalanceSandbox(
+      provider,
+      registrarAccount,
+      memberSigner,
+      provider.wallet.publicKey // Beneficiary,
+    );
+    const [lockedTx, _balancesLocked] = await utils.createBalanceSandbox(
+      provider,
+      registrarAccount,
+      memberSigner,
+      vesting.publicKey // Lockup.
+    );
+
+    balances = _balances;
+    balancesLocked = _balancesLocked;
+
+    const tx = registry.transaction.createMember(nonce, {
+      accounts: {
+        registrar: registrar.publicKey,
+        member: member.publicKey,
+        beneficiary: provider.wallet.publicKey,
+        memberSigner,
+        balances,
+        balancesLocked,
+        tokenProgram: TokenInstructions.TOKEN_PROGRAM_ID,
+        rent: anchor.web3.SYSVAR_RENT_PUBKEY,
+      },
+      instructions: [await registry.account.member.createInstruction(member)],
+    });
+
+    const signers = [member, provider.wallet.payer];
+
+    const allTxs = [mainTx, lockedTx, { tx, signers }];
+
+    let txSigs = await provider.sendAll(allTxs);
+
+    memberAccount = await registry.account.member(member.publicKey);
+
+    assert.ok(memberAccount.registrar.equals(registrar.publicKey));
+    assert.ok(memberAccount.beneficiary.equals(provider.wallet.publicKey));
+    assert.ok(memberAccount.metadata.equals(new anchor.web3.PublicKey()));
+    assert.equal(
+      JSON.stringify(memberAccount.balances),
+      JSON.stringify(balances)
+    );
+    assert.equal(
+      JSON.stringify(memberAccount.balancesLocked),
+      JSON.stringify(balancesLocked)
+    );
+    assert.ok(memberAccount.rewardsCursor === 0);
+    assert.ok(memberAccount.lastStakeTs.eq(new anchor.BN(0)));
+  });
+
+  it("Deposits (unlocked) to a member", async () => {
+    const depositAmount = new anchor.BN(120);
+    await registry.rpc.deposit(depositAmount, {
+      accounts: {
+        // Whitelist relay.
+        dummyVesting: anchor.web3.SYSVAR_RENT_PUBKEY,
+        depositor: god,
+        depositorAuthority: provider.wallet.publicKey,
+        tokenProgram: TokenInstructions.TOKEN_PROGRAM_ID,
+        vault: memberAccount.balances.vault,
+        memberSigner,
+        // Program specific.
+        registrar: registrar.publicKey,
+        beneficiary: provider.wallet.publicKey,
+        member: member.publicKey,
+      },
+    });
+
+    const memberVault = await serumCmn.getTokenAccount(
+      provider,
+      memberAccount.balances.vault
+    );
+    assert.ok(memberVault.amount.eq(depositAmount));
+  });
+
+  it("Stakes to a member (unlocked)", async () => {
+    const stakeAmount = new anchor.BN(10);
+    await registry.rpc.stake(stakeAmount, provider.wallet.publicKey, {
+      accounts: {
+        // Stake instance.
+        registrar: registrar.publicKey,
+        rewardEventQ: rewardQ.publicKey,
+        poolMint,
+        // Member.
+        member: member.publicKey,
+        beneficiary: provider.wallet.publicKey,
+        balances,
+        balancesLocked,
+        // Program signers.
+        memberSigner,
+        registrarSigner,
+        // Misc.
+        clock: anchor.web3.SYSVAR_CLOCK_PUBKEY,
+        tokenProgram: TokenInstructions.TOKEN_PROGRAM_ID,
+      },
+    });
+
+    const vault = await serumCmn.getTokenAccount(
+      provider,
+      memberAccount.balances.vault
+    );
+    const vaultStake = await serumCmn.getTokenAccount(
+      provider,
+      memberAccount.balances.vaultStake
+    );
+    const spt = await serumCmn.getTokenAccount(
+      provider,
+      memberAccount.balances.spt
+    );
+
+    assert.ok(vault.amount.eq(new anchor.BN(100)));
+    assert.ok(vaultStake.amount.eq(new anchor.BN(20)));
+    assert.ok(spt.amount.eq(new anchor.BN(10)));
+  });
+
+  const unlockedVendor = new anchor.web3.Account();
+  const unlockedVendorVault = new anchor.web3.Account();
+  let unlockedVendorSigner = null;
+
+  it("Drops an unlocked reward", async () => {
+    const rewardKind = {
+      unlocked: {},
+    };
+    const rewardAmount = new anchor.BN(200);
+    const expiry = new anchor.BN(Date.now() / 1000 + 5);
+    const [
+      _vendorSigner,
+      nonce,
+    ] = await anchor.web3.PublicKey.findProgramAddress(
+      [registrar.publicKey.toBuffer(), unlockedVendor.publicKey.toBuffer()],
+      registry.programId
+    );
+    unlockedVendorSigner = _vendorSigner;
+
+    await registry.rpc.dropReward(
+      rewardKind,
+      rewardAmount,
+      expiry,
+      provider.wallet.publicKey,
+      nonce,
+      {
+        accounts: {
+          registrar: registrar.publicKey,
+          rewardEventQ: rewardQ.publicKey,
+          poolMint,
+
+          vendor: unlockedVendor.publicKey,
+          vendorVault: unlockedVendorVault.publicKey,
+
+          depositor: god,
+          depositorAuthority: provider.wallet.publicKey,
+
+          tokenProgram: TokenInstructions.TOKEN_PROGRAM_ID,
+          clock: anchor.web3.SYSVAR_CLOCK_PUBKEY,
+          rent: anchor.web3.SYSVAR_RENT_PUBKEY,
+        },
+        signers: [unlockedVendorVault, unlockedVendor],
+        instructions: [
+          ...(await serumCmn.createTokenAccountInstrs(
+            provider,
+            unlockedVendorVault.publicKey,
+            mint,
+            unlockedVendorSigner
+          )),
+          await registry.account.rewardVendor.createInstruction(unlockedVendor),
+        ],
+      }
+    );
+
+    const vendorAccount = await registry.account.rewardVendor(
+      unlockedVendor.publicKey
+    );
+
+    assert.ok(vendorAccount.registrar.equals(registrar.publicKey));
+    assert.ok(vendorAccount.vault.equals(unlockedVendorVault.publicKey));
+    assert.ok(vendorAccount.nonce === nonce);
+    assert.ok(vendorAccount.poolTokenSupply.eq(new anchor.BN(10)));
+    assert.ok(vendorAccount.expiryTs.eq(expiry));
+    assert.ok(vendorAccount.expiryReceiver.equals(provider.wallet.publicKey));
+    assert.ok(vendorAccount.total.eq(rewardAmount));
+    assert.ok(vendorAccount.expired === false);
+    assert.ok(vendorAccount.rewardEventQCursor === 0);
+    assert.deepEqual(vendorAccount.kind, rewardKind);
+
+    const rewardQAccount = await registry.account.rewardQueue(
+      rewardQ.publicKey
+    );
+    assert.ok(rewardQAccount.head === 1);
+    assert.ok(rewardQAccount.tail === 0);
+    const e = rewardQAccount.events[0];
+    assert.ok(e.vendor.equals(unlockedVendor.publicKey));
+    assert.equal(e.locked, false);
+  });
+
+  it("Collects an unlocked reward", async () => {
+    const token = await serumCmn.createTokenAccount(
+      provider,
+      mint,
+      provider.wallet.publicKey
+    );
+    await registry.rpc.claimRewardUnlocked({
+      accounts: {
+        token,
+        cmn: {
+          registrar: registrar.publicKey,
+
+          member: member.publicKey,
+          beneficiary: provider.wallet.publicKey,
+          balances,
+          balancesLocked,
+
+          vendor: unlockedVendor.publicKey,
+          vault: unlockedVendorVault.publicKey,
+          vendorSigner: unlockedVendorSigner,
+
+          tokenProgram: TokenInstructions.TOKEN_PROGRAM_ID,
+          clock: anchor.web3.SYSVAR_CLOCK_PUBKEY,
+        },
+      },
+    });
+
+    let tokenAccount = await serumCmn.getTokenAccount(provider, token);
+    assert.ok(tokenAccount.amount.eq(new anchor.BN(200)));
+
+    const memberAccount = await registry.account.member(member.publicKey);
+    assert.ok(memberAccount.rewardsCursor == 1);
+  });
+
+  const lockedVendor = new anchor.web3.Account();
+  const lockedVendorVault = new anchor.web3.Account();
+  let lockedVendorSigner = null;
+  let lockedRewardAmount = null;
+  let lockedRewardKind = null;
+
+  it("Drops a locked reward", async () => {
+    lockedRewardKind = {
+      locked: {
+        endTs: new anchor.BN(Date.now() / 1000 + 70),
+        periodCount: new anchor.BN(10),
+      },
+    };
+    lockedRewardAmount = new anchor.BN(200);
+    const expiry = new anchor.BN(Date.now() / 1000 + 5);
+    const [
+      _vendorSigner,
+      nonce,
+    ] = await anchor.web3.PublicKey.findProgramAddress(
+      [registrar.publicKey.toBuffer(), lockedVendor.publicKey.toBuffer()],
+      registry.programId
+    );
+    lockedVendorSigner = _vendorSigner;
+
+    await registry.rpc.dropReward(
+      lockedRewardKind,
+      lockedRewardAmount,
+      expiry,
+      provider.wallet.publicKey,
+      nonce,
+      {
+        accounts: {
+          registrar: registrar.publicKey,
+          rewardEventQ: rewardQ.publicKey,
+          poolMint,
+
+          vendor: lockedVendor.publicKey,
+          vendorVault: lockedVendorVault.publicKey,
+
+          depositor: god,
+          depositorAuthority: provider.wallet.publicKey,
+
+          tokenProgram: TokenInstructions.TOKEN_PROGRAM_ID,
+          clock: anchor.web3.SYSVAR_CLOCK_PUBKEY,
+          rent: anchor.web3.SYSVAR_RENT_PUBKEY,
+        },
+        signers: [lockedVendorVault, lockedVendor],
+        instructions: [
+          ...(await serumCmn.createTokenAccountInstrs(
+            provider,
+            lockedVendorVault.publicKey,
+            mint,
+            lockedVendorSigner
+          )),
+          await registry.account.rewardVendor.createInstruction(lockedVendor),
+        ],
+      }
+    );
+
+    const vendorAccount = await registry.account.rewardVendor(
+      lockedVendor.publicKey
+    );
+
+    assert.ok(vendorAccount.registrar.equals(registrar.publicKey));
+    assert.ok(vendorAccount.vault.equals(lockedVendorVault.publicKey));
+    assert.ok(vendorAccount.nonce === nonce);
+    assert.ok(vendorAccount.poolTokenSupply.eq(new anchor.BN(10)));
+    assert.ok(vendorAccount.expiryTs.eq(expiry));
+    assert.ok(vendorAccount.expiryReceiver.equals(provider.wallet.publicKey));
+    assert.ok(vendorAccount.total.eq(lockedRewardAmount));
+    assert.ok(vendorAccount.expired === false);
+    assert.ok(vendorAccount.rewardEventQCursor === 1);
+    assert.equal(
+      JSON.stringify(vendorAccount.kind),
+      JSON.stringify(lockedRewardKind)
+    );
+
+    const rewardQAccount = await registry.account.rewardQueue(
+      rewardQ.publicKey
+    );
+    assert.ok(rewardQAccount.head === 2);
+    assert.ok(rewardQAccount.tail === 0);
+    const e = rewardQAccount.events[1];
+    assert.ok(e.vendor.equals(lockedVendor.publicKey));
+    assert.ok(e.locked === true);
+  });
+
+  it("Collects a locked reward", async () => {
+    const vendoredVesting = new anchor.web3.Account();
+    const vendoredVestingVault = new anchor.web3.Account();
+    let [
+      vendoredVestingSigner,
+      nonce,
+    ] = await anchor.web3.PublicKey.findProgramAddress(
+      [safe.publicKey.toBuffer(), provider.wallet.publicKey.toBuffer()],
+      lockup.programId
+    );
+    const remainingAccounts = lockup.instruction.createVesting
+      .accounts({
+        vesting: vendoredVesting.publicKey,
+        safe: safe.publicKey,
+        vault: vendoredVestingVault.publicKey,
+        depositor: lockedVendorVault.publicKey,
+        depositorAuthority: lockedVendorSigner,
+        tokenProgram: TokenInstructions.TOKEN_PROGRAM_ID,
+        rent: anchor.web3.SYSVAR_RENT_PUBKEY,
+        clock: anchor.web3.SYSVAR_CLOCK_PUBKEY,
+      })
+      // Change the signer status on the vendor signer since it's signed by the program, not the
+      // client.
+      .map((meta) =>
+        meta.pubkey === lockedVendorSigner ? { ...meta, isSigner: false } : meta
+      );
+
+    await registry.rpc.claimRewardLocked(nonce, {
+      accounts: {
+        lockupProgram: lockup.programId,
+        cmn: {
+          registrar: registrar.publicKey,
+
+          member: member.publicKey,
+          beneficiary: provider.wallet.publicKey,
+          balances,
+          balancesLocked,
+
+          vendor: lockedVendor.publicKey,
+          vault: lockedVendorVault.publicKey,
+          vendorSigner: lockedVendorSigner,
+
+          tokenProgram: TokenInstructions.TOKEN_PROGRAM_ID,
+          clock: anchor.web3.SYSVAR_CLOCK_PUBKEY,
+        },
+      },
+      remainingAccounts,
+      signers: [vendoredVesting, vendoredVestingVault],
+      instructions: [
+        await lockup.account.vesting.createInstruction(vendoredVesting),
+        ...(await serumCmn.createTokenAccountInstrs(
+          provider,
+          vendoredVestingVault.publicKey,
+          mint,
+          vendoredVestingSigner
+        )),
+      ],
+    });
+
+    const lockupAccount = await lockup.account.vesting(
+      vendoredVesting.publicKey
+    );
+
+    assert.ok(lockupAccount.safe.equals(safe.publicKey));
+    assert.ok(lockupAccount.beneficiary.equals(provider.wallet.publicKey));
+    assert.ok(lockupAccount.mint.equals(mint));
+    assert.ok(lockupAccount.vault.equals(vendoredVestingVault.publicKey));
+    assert.ok(lockupAccount.outstanding.eq(lockedRewardAmount));
+    assert.ok(lockupAccount.startBalance.eq(lockedRewardAmount));
+    assert.ok(lockupAccount.endTs.eq(lockedRewardKind.locked.endTs));
+    assert.ok(
+      lockupAccount.periodCount.eq(lockedRewardKind.locked.periodCount)
+    );
+    assert.ok(lockupAccount.whitelistOwned.eq(new anchor.BN(0)));
+  });
+
+  const pendingWithdrawal = new anchor.web3.Account();
+
+  it("Unstakes", async () => {
+    const unstakeAmount = new anchor.BN(10);
+
+    await registry.rpc.startUnstake(unstakeAmount, provider.wallet.publicKey, {
+      accounts: {
+        registrar: registrar.publicKey,
+        rewardEventQ: rewardQ.publicKey,
+        poolMint,
+
+        pendingWithdrawal: pendingWithdrawal.publicKey,
+        member: member.publicKey,
+        beneficiary: provider.wallet.publicKey,
+        balances,
+        balancesLocked,
+
+        memberSigner,
+
+        tokenProgram: TokenInstructions.TOKEN_PROGRAM_ID,
+        clock: anchor.web3.SYSVAR_CLOCK_PUBKEY,
+        rent: anchor.web3.SYSVAR_RENT_PUBKEY,
+      },
+      signers: [pendingWithdrawal],
+      instructions: [
+        await registry.account.pendingWithdrawal.createInstruction(
+          pendingWithdrawal
+        ),
+      ],
+    });
+
+    const vaultPw = await serumCmn.getTokenAccount(
+      provider,
+      memberAccount.balances.vaultPw
+    );
+    const vaultStake = await serumCmn.getTokenAccount(
+      provider,
+      memberAccount.balances.vaultStake
+    );
+    const spt = await serumCmn.getTokenAccount(
+      provider,
+      memberAccount.balances.spt
+    );
+
+    assert.ok(vaultPw.amount.eq(new anchor.BN(20)));
+    assert.ok(vaultStake.amount.eq(new anchor.BN(0)));
+    assert.ok(spt.amount.eq(new anchor.BN(0)));
+  });
+
+  const tryEndUnstake = async () => {
+    await registry.rpc.endUnstake({
+      accounts: {
+        registrar: registrar.publicKey,
+
+        member: member.publicKey,
+        beneficiary: provider.wallet.publicKey,
+        pendingWithdrawal: pendingWithdrawal.publicKey,
+
+        vault: balances.vault,
+        vaultPw: balances.vaultPw,
+
+        memberSigner,
+
+        clock: anchor.web3.SYSVAR_CLOCK_PUBKEY,
+        tokenProgram: TokenInstructions.TOKEN_PROGRAM_ID,
+      },
+    });
+  };
+
+  it("Fails to end unstaking before timelock", async () => {
+    await assert.rejects(
+      async () => {
+        await tryEndUnstake();
+      },
+      (err) => {
+        assert.equal(err.code, 109);
+        assert.equal(err.msg, "The unstake timelock has not yet expired.");
+        return true;
+      }
+    );
+  });
+
+  it("Waits for the unstake period to end", async () => {
+    await serumCmn.sleep(5000);
+  });
+
+  it("Unstake finalizes (unlocked)", async () => {
+    await tryEndUnstake();
+
+    const vault = await serumCmn.getTokenAccount(
+      provider,
+      memberAccount.balances.vault
+    );
+    const vaultPw = await serumCmn.getTokenAccount(
+      provider,
+      memberAccount.balances.vaultPw
+    );
+
+    assert.ok(vault.amount.eq(new anchor.BN(120)));
+    assert.ok(vaultPw.amount.eq(new anchor.BN(0)));
+  });
+
+  it("Withdraws deposits (unlocked)", async () => {
+    const token = await serumCmn.createTokenAccount(
+      provider,
+      mint,
+      provider.wallet.publicKey
+    );
+    const withdrawAmount = new anchor.BN(100);
+    await registry.rpc.withdraw(withdrawAmount, {
+      accounts: {
+        // Whitelist relay.
+        dummyVesting: anchor.web3.SYSVAR_RENT_PUBKEY,
+        depositor: token,
+        depositorAuthority: provider.wallet.publicKey,
+        tokenProgram: TokenInstructions.TOKEN_PROGRAM_ID,
+        vault: memberAccount.balances.vault,
+        memberSigner,
+        // Program specific.
+        registrar: registrar.publicKey,
+        beneficiary: provider.wallet.publicKey,
+        member: member.publicKey,
+      },
+    });
+
+    const tokenAccount = await serumCmn.getTokenAccount(provider, token);
+    assert.ok(tokenAccount.amount.eq(withdrawAmount));
+  });
+});

+ 67 - 0
examples/lockup/tests/utils.js

@@ -0,0 +1,67 @@
+const anchor = require("@project-serum/anchor");
+const serumCmn = require("@project-serum/common");
+
+async function createBalanceSandbox(provider, r, registrySigner, owner) {
+  const spt = new anchor.web3.Account();
+  const vault = new anchor.web3.Account();
+  const vaultStake = new anchor.web3.Account();
+  const vaultPw = new anchor.web3.Account();
+
+  const lamports = await provider.connection.getMinimumBalanceForRentExemption(
+    165
+  );
+
+  const createSptIx = await serumCmn.createTokenAccountInstrs(
+    provider,
+    spt.publicKey,
+    r.poolMint,
+    registrySigner,
+    lamports
+  );
+  const createVaultIx = await serumCmn.createTokenAccountInstrs(
+    provider,
+    vault.publicKey,
+    r.mint,
+    registrySigner,
+    lamports
+  );
+  const createVaultStakeIx = await serumCmn.createTokenAccountInstrs(
+    provider,
+    vaultStake.publicKey,
+    r.mint,
+    registrySigner,
+    lamports
+  );
+  const createVaultPwIx = await serumCmn.createTokenAccountInstrs(
+    provider,
+    vaultPw.publicKey,
+    r.mint,
+    registrySigner,
+    lamports
+  );
+  let tx0 = new anchor.web3.Transaction();
+  tx0.add(
+    ...createSptIx,
+    ...createVaultIx,
+    ...createVaultStakeIx,
+    ...createVaultPwIx
+  );
+  let signers0 = [spt, vault, vaultStake, vaultPw];
+
+  const tx = { tx: tx0, signers: signers0 };
+
+  return [
+    tx,
+    {
+      balanceId: owner,
+      spt: spt.publicKey,
+      vault: vault.publicKey,
+      vaultStake: vaultStake.publicKey,
+      vaultPw: vaultPw.publicKey,
+    },
+  ];
+}
+
+module.exports = {
+  createBalanceSandbox,
+};

+ 79 - 1
spl/src/token.rs

@@ -1,7 +1,12 @@
 use anchor_lang::solana_program;
 use anchor_lang::solana_program::account_info::AccountInfo;
 use anchor_lang::solana_program::entrypoint::ProgramResult;
+use anchor_lang::solana_program::program_error::ProgramError;
+use anchor_lang::solana_program::program_pack::Pack;
 use anchor_lang::{Accounts, CpiContext};
+use std::ops::Deref;
+
+pub use spl_token::ID;
 
 pub fn transfer<'a, 'b, 'c, 'info>(
     ctx: CpiContext<'a, 'b, 'c, 'info, Transfer<'info>>,
@@ -42,8 +47,8 @@ pub fn mint_to<'a, 'b, 'c, 'info>(
     solana_program::program::invoke_signed(
         &ix,
         &[
-            ctx.accounts.mint.clone(),
             ctx.accounts.to.clone(),
+            ctx.accounts.mint.clone(),
             ctx.accounts.authority.clone(),
             ctx.program.clone(),
         ],
@@ -75,6 +80,30 @@ pub fn burn<'a, 'b, 'c, 'info>(
     )
 }
 
+pub fn approve<'a, 'b, 'c, 'info>(
+    ctx: CpiContext<'a, 'b, 'c, 'info, Approve<'info>>,
+    amount: u64,
+) -> ProgramResult {
+    let ix = spl_token::instruction::approve(
+        &spl_token::ID,
+        ctx.accounts.to.key,
+        ctx.accounts.delegate.key,
+        ctx.accounts.authority.key,
+        &[],
+        amount,
+    )?;
+    solana_program::program::invoke_signed(
+        &ix,
+        &[
+            ctx.accounts.to.clone(),
+            ctx.accounts.delegate.clone(),
+            ctx.accounts.authority.clone(),
+            ctx.program.clone(),
+        ],
+        ctx.signer_seeds,
+    )
+}
+
 #[derive(Accounts)]
 pub struct Transfer<'info> {
     pub from: AccountInfo<'info>,
@@ -95,3 +124,52 @@ pub struct Burn<'info> {
     pub to: AccountInfo<'info>,
     pub authority: AccountInfo<'info>,
 }
+
+#[derive(Accounts)]
+pub struct Approve<'info> {
+    pub to: AccountInfo<'info>,
+    pub delegate: AccountInfo<'info>,
+    pub authority: AccountInfo<'info>,
+}
+
+#[derive(Clone)]
+pub struct TokenAccount(spl_token::state::Account);
+
+impl anchor_lang::AccountDeserialize for TokenAccount {
+    fn try_deserialize(buf: &mut &[u8]) -> Result<Self, ProgramError> {
+        TokenAccount::try_deserialize_unchecked(buf)
+    }
+
+    fn try_deserialize_unchecked(buf: &mut &[u8]) -> Result<Self, ProgramError> {
+        spl_token::state::Account::unpack(buf).map(|a| TokenAccount(a))
+    }
+}
+
+impl Deref for TokenAccount {
+    type Target = spl_token::state::Account;
+
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
+#[derive(Clone)]
+pub struct Mint(spl_token::state::Mint);
+
+impl anchor_lang::AccountDeserialize for Mint {
+    fn try_deserialize(buf: &mut &[u8]) -> Result<Self, ProgramError> {
+        Mint::try_deserialize_unchecked(buf)
+    }
+
+    fn try_deserialize_unchecked(buf: &mut &[u8]) -> Result<Self, ProgramError> {
+        spl_token::state::Mint::unpack(buf).map(|a| Mint(a))
+    }
+}
+
+impl Deref for Mint {
+    type Target = spl_token::state::Mint;
+
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}

+ 13 - 4
src/account_info.rs

@@ -1,5 +1,6 @@
-use crate::{Accounts, ToAccountInfo, ToAccountInfos, ToAccountMetas};
+use crate::{Accounts, AccountsExit, ToAccountInfo, ToAccountInfos, ToAccountMetas};
 use solana_program::account_info::AccountInfo;
+use solana_program::entrypoint::ProgramResult;
 use solana_program::instruction::AccountMeta;
 use solana_program::program_error::ProgramError;
 use solana_program::pubkey::Pubkey;
@@ -19,10 +20,11 @@ impl<'info> Accounts<'info> for AccountInfo<'info> {
 }
 
 impl<'info> ToAccountMetas for AccountInfo<'info> {
-    fn to_account_metas(&self) -> Vec<AccountMeta> {
+    fn to_account_metas(&self, is_signer: Option<bool>) -> Vec<AccountMeta> {
+        let is_signer = is_signer.unwrap_or(self.is_signer);
         let meta = match self.is_writable {
-            false => AccountMeta::new_readonly(*self.key, self.is_signer),
-            true => AccountMeta::new(*self.key, self.is_signer),
+            false => AccountMeta::new_readonly(*self.key, is_signer),
+            true => AccountMeta::new(*self.key, is_signer),
         };
         vec![meta]
     }
@@ -39,3 +41,10 @@ impl<'info> ToAccountInfo<'info> for AccountInfo<'info> {
         self.clone()
     }
 }
+
+impl<'info> AccountsExit<'info> for AccountInfo<'info> {
+    fn exit(&self, _program_id: &Pubkey) -> ProgramResult {
+        // no-op
+        Ok(())
+    }
+}

+ 34 - 0
src/boxed.rs

@@ -0,0 +1,34 @@
+use crate::{Accounts, AccountsExit, ToAccountInfos, ToAccountMetas};
+use solana_program::account_info::AccountInfo;
+use solana_program::entrypoint::ProgramResult;
+use solana_program::instruction::AccountMeta;
+use solana_program::program_error::ProgramError;
+use solana_program::pubkey::Pubkey;
+use std::ops::Deref;
+
+impl<'info, T: Accounts<'info>> Accounts<'info> for Box<T> {
+    fn try_accounts(
+        program_id: &Pubkey,
+        accounts: &mut &[AccountInfo<'info>],
+    ) -> Result<Self, ProgramError> {
+        T::try_accounts(program_id, accounts).map(Box::new)
+    }
+}
+
+impl<'info, T: AccountsExit<'info>> AccountsExit<'info> for Box<T> {
+    fn exit(&self, program_id: &Pubkey) -> ProgramResult {
+        T::exit(Deref::deref(self), program_id)
+    }
+}
+
+impl<'info, T: ToAccountInfos<'info>> ToAccountInfos<'info> for Box<T> {
+    fn to_account_infos(&self) -> Vec<AccountInfo<'info>> {
+        T::to_account_infos(self)
+    }
+}
+
+impl<T: ToAccountMetas> ToAccountMetas for Box<T> {
+    fn to_account_metas(&self, is_signer: Option<bool>) -> Vec<AccountMeta> {
+        T::to_account_metas(self, is_signer)
+    }
+}

+ 7 - 2
src/context.rs

@@ -12,7 +12,7 @@ pub struct Context<'a, 'b, 'c, 'info, T> {
     pub remaining_accounts: &'c [AccountInfo<'info>],
 }
 
-impl<'a, 'b, 'c, 'info, T> Context<'a, 'b, 'c, 'info, T> {
+impl<'a, 'b, 'c, 'info, T: Accounts<'info>> Context<'a, 'b, 'c, 'info, T> {
     pub fn new(
         program_id: &'a Pubkey,
         accounts: &'b mut T,
@@ -43,8 +43,8 @@ impl<'a, 'b, 'c, 'info, T: Accounts<'info>> CpiContext<'a, 'b, 'c, 'info, T> {
     }
 
     pub fn new_with_signer(
-        accounts: T,
         program: AccountInfo<'info>,
+        accounts: T,
         signer_seeds: &'a [&'b [&'c [u8]]],
     ) -> Self {
         Self {
@@ -53,4 +53,9 @@ impl<'a, 'b, 'c, 'info, T: Accounts<'info>> CpiContext<'a, 'b, 'c, 'info, T> {
             signer_seeds,
         }
     }
+
+    pub fn with_signer(mut self, signer_seeds: &'a [&'b [&'c [u8]]]) -> Self {
+        self.signer_seeds = signer_seeds;
+        self
+    }
 }

+ 36 - 21
src/cpi_account.rs

@@ -1,7 +1,8 @@
 use crate::{
-    AccountDeserialize, AccountSerialize, Accounts, ToAccountInfo, ToAccountInfos, ToAccountMetas,
+    AccountDeserialize, Accounts, AccountsExit, ToAccountInfo, ToAccountInfos, ToAccountMetas,
 };
 use solana_program::account_info::AccountInfo;
+use solana_program::entrypoint::ProgramResult;
 use solana_program::instruction::AccountMeta;
 use solana_program::program_error::ProgramError;
 use solana_program::pubkey::Pubkey;
@@ -9,13 +10,13 @@ use std::ops::{Deref, DerefMut};
 
 /// Container for any account *not* owned by the current program.
 #[derive(Clone)]
-pub struct CpiAccount<'a, T: AccountSerialize + AccountDeserialize + Clone> {
+pub struct CpiAccount<'a, T: AccountDeserialize + Clone> {
     info: AccountInfo<'a>,
-    account: T,
+    account: Box<T>,
 }
 
-impl<'a, T: AccountSerialize + AccountDeserialize + Clone> CpiAccount<'a, T> {
-    pub fn new(info: AccountInfo<'a>, account: T) -> CpiAccount<'a, T> {
+impl<'a, T: AccountDeserialize + Clone> CpiAccount<'a, T> {
+    pub fn new(info: AccountInfo<'a>, account: Box<T>) -> CpiAccount<'a, T> {
         Self { info, account }
     }
 
@@ -24,15 +25,27 @@ impl<'a, T: AccountSerialize + AccountDeserialize + Clone> CpiAccount<'a, T> {
         let mut data: &[u8] = &info.try_borrow_data()?;
         Ok(CpiAccount::new(
             info.clone(),
-            T::try_deserialize(&mut data)?,
+            Box::new(T::try_deserialize(&mut data)?),
+        ))
+    }
+
+    /// Reloads the account from storage. This is useful, for example, when
+    /// observing side effects after CPI.
+    pub fn reload(&self) -> Result<CpiAccount<'a, T>, ProgramError> {
+        let info = self.to_account_info();
+        let mut data: &[u8] = &info.try_borrow_data()?;
+        Ok(CpiAccount::new(
+            info.clone(),
+            Box::new(T::try_deserialize(&mut data)?),
         ))
     }
 }
 
 impl<'info, T> Accounts<'info> for CpiAccount<'info, T>
 where
-    T: AccountSerialize + AccountDeserialize + Clone,
+    T: AccountDeserialize + Clone,
 {
+    #[inline(never)]
     fn try_accounts(
         _program_id: &Pubkey,
         accounts: &mut &[AccountInfo<'info>],
@@ -47,35 +60,30 @@ where
     }
 }
 
-impl<'info, T: AccountSerialize + AccountDeserialize + Clone> ToAccountMetas
-    for CpiAccount<'info, T>
-{
-    fn to_account_metas(&self) -> Vec<AccountMeta> {
+impl<'info, T: AccountDeserialize + Clone> ToAccountMetas for CpiAccount<'info, T> {
+    fn to_account_metas(&self, is_signer: Option<bool>) -> Vec<AccountMeta> {
+        let is_signer = is_signer.unwrap_or(self.info.is_signer);
         let meta = match self.info.is_writable {
-            false => AccountMeta::new_readonly(*self.info.key, self.info.is_signer),
-            true => AccountMeta::new(*self.info.key, self.info.is_signer),
+            false => AccountMeta::new_readonly(*self.info.key, is_signer),
+            true => AccountMeta::new(*self.info.key, is_signer),
         };
         vec![meta]
     }
 }
 
-impl<'info, T: AccountSerialize + AccountDeserialize + Clone> ToAccountInfos<'info>
-    for CpiAccount<'info, T>
-{
+impl<'info, T: AccountDeserialize + Clone> ToAccountInfos<'info> for CpiAccount<'info, T> {
     fn to_account_infos(&self) -> Vec<AccountInfo<'info>> {
         vec![self.info.clone()]
     }
 }
 
-impl<'info, T: AccountSerialize + AccountDeserialize + Clone> ToAccountInfo<'info>
-    for CpiAccount<'info, T>
-{
+impl<'info, T: AccountDeserialize + Clone> ToAccountInfo<'info> for CpiAccount<'info, T> {
     fn to_account_info(&self) -> AccountInfo<'info> {
         self.info.clone()
     }
 }
 
-impl<'a, T: AccountSerialize + AccountDeserialize + Clone> Deref for CpiAccount<'a, T> {
+impl<'a, T: AccountDeserialize + Clone> Deref for CpiAccount<'a, T> {
     type Target = T;
 
     fn deref(&self) -> &Self::Target {
@@ -83,8 +91,15 @@ impl<'a, T: AccountSerialize + AccountDeserialize + Clone> Deref for CpiAccount<
     }
 }
 
-impl<'a, T: AccountSerialize + AccountDeserialize + Clone> DerefMut for CpiAccount<'a, T> {
+impl<'a, T: AccountDeserialize + Clone> DerefMut for CpiAccount<'a, T> {
     fn deref_mut(&mut self) -> &mut Self::Target {
         &mut self.account
     }
 }
+
+impl<'info, T: AccountDeserialize + Clone> AccountsExit<'info> for CpiAccount<'info, T> {
+    fn exit(&self, _program_id: &Pubkey) -> ProgramResult {
+        // no-op
+        Ok(())
+    }
+}

+ 14 - 3
src/lib.rs

@@ -28,6 +28,7 @@ use solana_program::pubkey::Pubkey;
 use std::io::Write;
 
 mod account_info;
+mod boxed;
 mod context;
 mod cpi_account;
 mod error;
@@ -61,6 +62,11 @@ pub trait Accounts<'info>: ToAccountMetas + ToAccountInfos<'info> + Sized {
     ) -> Result<Self, ProgramError>;
 }
 
+/// The exit procedure for an accounts object.
+pub trait AccountsExit<'info>: ToAccountMetas + ToAccountInfos<'info> {
+    fn exit(&self, program_id: &Pubkey) -> solana_program::entrypoint::ProgramResult;
+}
+
 /// A data structure of accounts providing a one time deserialization upon
 /// initialization, i.e., when the data array for a given account is zeroed.
 /// For all subsequent deserializations, it's expected that
@@ -74,7 +80,12 @@ pub trait AccountsInit<'info>: ToAccountMetas + ToAccountInfos<'info> + Sized {
 
 /// Transformation to `AccountMeta` structs.
 pub trait ToAccountMetas {
-    fn to_account_metas(&self) -> Vec<AccountMeta>;
+    /// `is_signer` is given as an optional override for the signer meta field.
+    /// This covers the edge case when a program-derived-address needs to relay
+    /// a transaction from a client to another program but sign the transaction
+    /// before the relay. The client cannot mark the field as a signer, and so
+    /// we have to override the is_signer meta field given by the client.
+    fn to_account_metas(&self, is_signer: Option<bool>) -> Vec<AccountMeta>;
 }
 
 /// Transformation to `AccountInfo` structs.
@@ -119,8 +130,8 @@ pub trait AccountDeserialize: Sized {
 pub mod prelude {
     pub use super::{
         access_control, account, error, program, AccountDeserialize, AccountSerialize, Accounts,
-        AccountsInit, AnchorDeserialize, AnchorSerialize, Context, CpiAccount, CpiContext,
-        ProgramAccount, Sysvar, ToAccountInfo, ToAccountInfos, ToAccountMetas,
+        AccountsExit, AccountsInit, AnchorDeserialize, AnchorSerialize, Context, CpiAccount,
+        CpiContext, ProgramAccount, Sysvar, ToAccountInfo, ToAccountInfos, ToAccountMetas,
     };
 
     pub use borsh;

+ 45 - 20
src/program_account.rs

@@ -1,27 +1,36 @@
 use crate::{
-    AccountDeserialize, AccountSerialize, Accounts, AccountsInit, CpiAccount, ToAccountInfo,
-    ToAccountInfos, ToAccountMetas,
+    AccountDeserialize, AccountSerialize, Accounts, AccountsExit, AccountsInit, CpiAccount,
+    ToAccountInfo, ToAccountInfos, ToAccountMetas,
 };
 use solana_program::account_info::AccountInfo;
+use solana_program::entrypoint::ProgramResult;
 use solana_program::instruction::AccountMeta;
 use solana_program::program_error::ProgramError;
 use solana_program::pubkey::Pubkey;
 use std::ops::{Deref, DerefMut};
 
-/// Container for a serializable `account`. Use this to reference any account
-/// owned by the currently executing program.
+/// Boxed container for a deserialized `account`. Use this to reference any
+/// account owned by the currently executing program.
 #[derive(Clone)]
-pub struct ProgramAccount<'a, T: AccountSerialize + AccountDeserialize + Clone> {
-    info: AccountInfo<'a>,
+pub struct ProgramAccount<'info, T: AccountSerialize + AccountDeserialize + Clone> {
+    inner: Box<Inner<'info, T>>,
+}
+
+#[derive(Clone)]
+struct Inner<'info, T: AccountSerialize + AccountDeserialize + Clone> {
+    info: AccountInfo<'info>,
     account: T,
 }
 
 impl<'a, T: AccountSerialize + AccountDeserialize + Clone> ProgramAccount<'a, T> {
     pub fn new(info: AccountInfo<'a>, account: T) -> ProgramAccount<'a, T> {
-        Self { info, account }
+        Self {
+            inner: Box::new(Inner { info, account }),
+        }
     }
 
     /// Deserializes the given `info` into a `ProgramAccount`.
+    #[inline(never)]
     pub fn try_from(info: &AccountInfo<'a>) -> Result<ProgramAccount<'a, T>, ProgramError> {
         let mut data: &[u8] = &info.try_borrow_data()?;
         Ok(ProgramAccount::new(
@@ -34,6 +43,7 @@ impl<'a, T: AccountSerialize + AccountDeserialize + Clone> ProgramAccount<'a, T>
     /// checking the account type. This should only be used upon program account
     /// initialization (since the entire account data array is zeroed and thus
     /// no account type is set).
+    #[inline(never)]
     pub fn try_from_init(info: &AccountInfo<'a>) -> Result<ProgramAccount<'a, T>, ProgramError> {
         let mut data: &[u8] = &info.try_borrow_data()?;
 
@@ -56,6 +66,7 @@ impl<'info, T> Accounts<'info> for ProgramAccount<'info, T>
 where
     T: AccountSerialize + AccountDeserialize + Clone,
 {
+    #[inline(never)]
     fn try_accounts(
         program_id: &Pubkey,
         accounts: &mut &[AccountInfo<'info>],
@@ -66,7 +77,9 @@ where
         let account = &accounts[0];
         *accounts = &accounts[1..];
         let pa = ProgramAccount::try_from(account)?;
-        if pa.info.owner != program_id {}
+        if pa.inner.info.owner != program_id {
+            return Err(ProgramError::Custom(1)); // todo: proper error
+        }
         Ok(pa)
     }
 }
@@ -75,6 +88,7 @@ impl<'info, T> AccountsInit<'info> for ProgramAccount<'info, T>
 where
     T: AccountSerialize + AccountDeserialize + Clone,
 {
+    #[inline(never)]
     fn try_accounts_init(
         _program_id: &Pubkey,
         accounts: &mut &[AccountInfo<'info>],
@@ -88,13 +102,27 @@ where
     }
 }
 
+impl<'info, T: AccountSerialize + AccountDeserialize + Clone> AccountsExit<'info>
+    for ProgramAccount<'info, T>
+{
+    fn exit(&self, _program_id: &Pubkey) -> ProgramResult {
+        let info = self.to_account_info();
+        let mut data = info.try_borrow_mut_data()?;
+        let dst: &mut [u8] = &mut data;
+        let mut cursor = std::io::Cursor::new(dst);
+        self.inner.account.try_serialize(&mut cursor)?;
+        Ok(())
+    }
+}
+
 impl<'info, T: AccountSerialize + AccountDeserialize + Clone> ToAccountMetas
     for ProgramAccount<'info, T>
 {
-    fn to_account_metas(&self) -> Vec<AccountMeta> {
-        let meta = match self.info.is_writable {
-            false => AccountMeta::new_readonly(*self.info.key, self.info.is_signer),
-            true => AccountMeta::new(*self.info.key, self.info.is_signer),
+    fn to_account_metas(&self, is_signer: Option<bool>) -> Vec<AccountMeta> {
+        let is_signer = is_signer.unwrap_or(self.inner.info.is_signer);
+        let meta = match self.inner.info.is_writable {
+            false => AccountMeta::new_readonly(*self.inner.info.key, is_signer),
+            true => AccountMeta::new(*self.inner.info.key, is_signer),
         };
         vec![meta]
     }
@@ -104,7 +132,7 @@ impl<'info, T: AccountSerialize + AccountDeserialize + Clone> ToAccountInfos<'in
     for ProgramAccount<'info, T>
 {
     fn to_account_infos(&self) -> Vec<AccountInfo<'info>> {
-        vec![self.info.clone()]
+        vec![self.inner.info.clone()]
     }
 }
 
@@ -112,7 +140,7 @@ impl<'info, T: AccountSerialize + AccountDeserialize + Clone> ToAccountInfo<'inf
     for ProgramAccount<'info, T>
 {
     fn to_account_info(&self) -> AccountInfo<'info> {
-        self.info.clone()
+        self.inner.info.clone()
     }
 }
 
@@ -120,13 +148,13 @@ impl<'a, T: AccountSerialize + AccountDeserialize + Clone> Deref for ProgramAcco
     type Target = T;
 
     fn deref(&self) -> &Self::Target {
-        &self.account
+        &(*self.inner).account
     }
 }
 
 impl<'a, T: AccountSerialize + AccountDeserialize + Clone> DerefMut for ProgramAccount<'a, T> {
     fn deref_mut(&mut self) -> &mut Self::Target {
-        &mut self.account
+        &mut DerefMut::deref_mut(&mut self.inner).account
     }
 }
 
@@ -135,9 +163,6 @@ where
     T: AccountSerialize + AccountDeserialize + Clone,
 {
     fn from(a: CpiAccount<'info, T>) -> Self {
-        Self {
-            info: a.to_account_info(),
-            account: Deref::deref(&a).clone(),
-        }
+        Self::new(a.to_account_info(), Deref::deref(&a).clone())
     }
 }

+ 10 - 2
src/sysvar.rs

@@ -1,5 +1,6 @@
-use crate::{Accounts, ToAccountInfo, ToAccountInfos, ToAccountMetas};
+use crate::{Accounts, AccountsExit, ToAccountInfo, ToAccountInfos, ToAccountMetas};
 use solana_program::account_info::AccountInfo;
+use solana_program::entrypoint::ProgramResult;
 use solana_program::instruction::AccountMeta;
 use solana_program::program_error::ProgramError;
 use solana_program::pubkey::Pubkey;
@@ -37,7 +38,7 @@ impl<'info, T: solana_program::sysvar::Sysvar> Accounts<'info> for Sysvar<'info,
 }
 
 impl<'info, T: solana_program::sysvar::Sysvar> ToAccountMetas for Sysvar<'info, T> {
-    fn to_account_metas(&self) -> Vec<AccountMeta> {
+    fn to_account_metas(&self, is_mut_signer: Option<bool>) -> Vec<AccountMeta> {
         vec![AccountMeta::new_readonly(*self.info.key, false)]
     }
 }
@@ -67,3 +68,10 @@ impl<'info, T: solana_program::sysvar::Sysvar> ToAccountInfo<'info> for Sysvar<'
         self.info.clone()
     }
 }
+
+impl<'info, T: solana_program::sysvar::Sysvar> AccountsExit<'info> for Sysvar<'info, T> {
+    fn exit(&self, _program_id: &Pubkey) -> ProgramResult {
+        // no-op
+        Ok(())
+    }
+}

+ 82 - 55
syn/src/codegen/accounts.rs

@@ -1,6 +1,7 @@
 use crate::{
-    AccountField, AccountsStruct, Constraint, ConstraintBelongsTo, ConstraintLiteral,
-    ConstraintOwner, ConstraintRentExempt, ConstraintSigner, Field, Ty,
+    AccountField, AccountsStruct, CompositeField, Constraint, ConstraintBelongsTo,
+    ConstraintLiteral, ConstraintOwner, ConstraintRentExempt, ConstraintSeeds, ConstraintSigner,
+    Field, Ty,
 };
 use quote::quote;
 
@@ -12,8 +13,9 @@ pub fn generate(accs: AccountsStruct) -> proc_macro2::TokenStream {
         .map(|af: &AccountField| match af {
             AccountField::AccountsStruct(s) => {
                 let name = &s.ident;
+                let ty = &s.raw_field.ty;
                 quote! {
-                    let #name = Accounts::try_accounts(program_id, accounts)?;
+                    let #name: #ty = Accounts::try_accounts(program_id, accounts)?;
                 }
             }
             AccountField::Field(f) => {
@@ -34,17 +36,19 @@ pub fn generate(accs: AccountsStruct) -> proc_macro2::TokenStream {
     let access_checks: Vec<proc_macro2::TokenStream> = accs
         .fields
         .iter()
-        // TODO: allow constraints on composite fields.
-        .filter_map(|af: &AccountField| match af {
-            AccountField::AccountsStruct(_) => None,
-            AccountField::Field(f) => Some(f),
-        })
-        .map(|f: &Field| {
-            let checks: Vec<proc_macro2::TokenStream> = f
-                .constraints
-                .iter()
-                .map(|c| generate_constraint(&f, c))
-                .collect();
+        .map(|af: &AccountField| {
+            let checks: Vec<proc_macro2::TokenStream> = match af {
+                AccountField::Field(f) => f
+                    .constraints
+                    .iter()
+                    .map(|c| generate_field_constraint(&f, c))
+                    .collect(),
+                AccountField::AccountsStruct(s) => s
+                    .constraints
+                    .iter()
+                    .map(|c| generate_composite_constraint(&s, c))
+                    .collect(),
+            };
             quote! {
                 #(#checks)*
             }
@@ -70,36 +74,20 @@ pub fn generate(accs: AccountsStruct) -> proc_macro2::TokenStream {
     let on_save: Vec<proc_macro2::TokenStream> = accs
         .fields
         .iter()
-        .map(|af: &AccountField| {
-            match af {
-                AccountField::AccountsStruct(s) => {
-                    let name = &s.ident;
-                    quote! {
-                        self.#name.exit(program_id)?;
-                    }
+        .map(|af: &AccountField| match af {
+            AccountField::AccountsStruct(s) => {
+                let name = &s.ident;
+                quote! {
+                    anchor_lang::AccountsExit::exit(&self.#name, program_id)?;
                 }
-                AccountField::Field(f) => {
-                    let ident = &f.ident;
-                    let info = match f.ty {
-                        // Only ProgramAccounts are automatically saved (when
-                        // marked `#[account(mut)]`).
-                        Ty::ProgramAccount(_) => quote! { #ident.to_account_info() },
-                        _ => return quote! {},
-                    };
-                    match f.is_mut {
-                        false => quote! {},
-                        true => quote! {
-                            // Only persist the change if the account is owned by the
-                            // current program.
-                            if program_id == self.#info.owner  {
-                                let info = self.#info;
-                                let mut data = info.try_borrow_mut_data()?;
-                                let dst: &mut [u8] = &mut data;
-                                let mut cursor = std::io::Cursor::new(dst);
-                                self.#ident.try_serialize(&mut cursor)?;
-                            }
-                        },
-                    }
+            }
+            AccountField::Field(f) => {
+                let ident = &f.ident;
+                match f.is_mut {
+                    false => quote! {},
+                    true => quote! {
+                        anchor_lang::AccountsExit::exit(&self.#ident, program_id)?;
+                    },
                 }
             }
         })
@@ -125,12 +113,18 @@ pub fn generate(accs: AccountsStruct) -> proc_macro2::TokenStream {
         .fields
         .iter()
         .map(|f: &AccountField| {
-            let name = match f {
-                AccountField::AccountsStruct(s) => &s.ident,
-                AccountField::Field(f) => &f.ident,
+            let (name, is_signer) = match f {
+                AccountField::AccountsStruct(s) => (&s.ident, quote! {None}),
+                AccountField::Field(f) => {
+                    let is_signer = match f.is_signer {
+                        false => quote! {None},
+                        true => quote! {Some(true)},
+                    };
+                    (&f.ident, is_signer)
+                }
             };
             quote! {
-                account_metas.extend(self.#name.to_account_metas());
+                account_metas.extend(self.#name.to_account_metas(#is_signer));
             }
         })
         .collect();
@@ -146,6 +140,7 @@ pub fn generate(accs: AccountsStruct) -> proc_macro2::TokenStream {
 
     quote! {
         impl#combined_generics anchor_lang::Accounts#trait_generics for #name#strct_generics {
+            #[inline(never)]
             fn try_accounts(program_id: &anchor_lang::solana_program::pubkey::Pubkey, accounts: &mut &[anchor_lang::solana_program::account_info::AccountInfo<'info>]) -> Result<Self, anchor_lang::solana_program::program_error::ProgramError> {
                 // Deserialize each account.
                 #(#deser_fields)*
@@ -171,7 +166,7 @@ pub fn generate(accs: AccountsStruct) -> proc_macro2::TokenStream {
         }
 
         impl#combined_generics anchor_lang::ToAccountMetas for #name#strct_generics {
-            fn to_account_metas(&self) -> Vec<anchor_lang::solana_program::instruction::AccountMeta> {
+            fn to_account_metas(&self, is_signer: Option<bool>) -> Vec<anchor_lang::solana_program::instruction::AccountMeta> {
                 let mut account_metas = vec![];
 
                 #(#to_acc_metas)*
@@ -181,8 +176,8 @@ pub fn generate(accs: AccountsStruct) -> proc_macro2::TokenStream {
             }
         }
 
-        impl#strct_generics #name#strct_generics {
-            pub fn exit(&self, program_id: &anchor_lang::solana_program::pubkey::Pubkey) -> anchor_lang::solana_program::entrypoint::ProgramResult {
+        impl#combined_generics anchor_lang::AccountsExit#trait_generics for #name#strct_generics {
+            fn exit(&self, program_id: &anchor_lang::solana_program::pubkey::Pubkey) -> anchor_lang::solana_program::entrypoint::ProgramResult {
                 #(#on_save)*
                 Ok(())
             }
@@ -190,13 +185,24 @@ pub fn generate(accs: AccountsStruct) -> proc_macro2::TokenStream {
     }
 }
 
-pub fn generate_constraint(f: &Field, c: &Constraint) -> proc_macro2::TokenStream {
+pub fn generate_field_constraint(f: &Field, c: &Constraint) -> proc_macro2::TokenStream {
     match c {
         Constraint::BelongsTo(c) => generate_constraint_belongs_to(f, c),
         Constraint::Signer(c) => generate_constraint_signer(f, c),
-        Constraint::Literal(c) => generate_constraint_literal(f, c),
+        Constraint::Literal(c) => generate_constraint_literal(c),
         Constraint::Owner(c) => generate_constraint_owner(f, c),
         Constraint::RentExempt(c) => generate_constraint_rent_exempt(f, c),
+        Constraint::Seeds(c) => generate_constraint_seeds(f, c),
+    }
+}
+
+pub fn generate_composite_constraint(
+    _f: &CompositeField,
+    c: &Constraint,
+) -> proc_macro2::TokenStream {
+    match c {
+        Constraint::Literal(c) => generate_constraint_literal(c),
+        _ => panic!("Composite fields can only use literal constraints"),
     }
 }
 
@@ -224,13 +230,20 @@ pub fn generate_constraint_signer(f: &Field, _c: &ConstraintSigner) -> proc_macr
         _ => panic!("Invalid syntax: signer cannot be specified."),
     };
     quote! {
-        if !#info.is_signer {
-            return Err(ProgramError::MissingRequiredSignature);
+        // Don't enforce on CPI, since usually a program is signing and so
+        // the `try_accounts` deserializatoin will fail *if* the one
+        // tries to manually invoke it.
+        //
+        // This check will be performed on the other end of the invocation.
+        if cfg!(not(feature = "cpi")) {
+            if !#info.is_signer {
+                return Err(ProgramError::MissingRequiredSignature);
+            }
         }
     }
 }
 
-pub fn generate_constraint_literal(_f: &Field, c: &ConstraintLiteral) -> proc_macro2::TokenStream {
+pub fn generate_constraint_literal(c: &ConstraintLiteral) -> proc_macro2::TokenStream {
     let tokens = &c.tokens;
     quote! {
         if !(#tokens) {
@@ -275,3 +288,17 @@ pub fn generate_constraint_rent_exempt(
         },
     }
 }
+
+pub fn generate_constraint_seeds(f: &Field, c: &ConstraintSeeds) -> proc_macro2::TokenStream {
+    let name = &f.ident;
+    let seeds = &c.seeds;
+    quote! {
+        let program_signer = Pubkey::create_program_address(
+            &#seeds,
+            program_id,
+        ).map_err(|_| ProgramError::Custom(1))?; // todo
+        if #name.to_account_info().key != &program_signer {
+            return Err(ProgramError::Custom(1)); // todo
+        }
+    }
+}

+ 48 - 11
syn/src/codegen/program.rs

@@ -6,6 +6,7 @@ pub fn generate(program: Program) -> proc_macro2::TokenStream {
     let mod_name = &program.name;
     let instruction_name = instruction_enum_name(&program);
     let dispatch = generate_dispatch(&program);
+    let handlers_non_inlined = generate_non_inlined_handlers(&program);
     let methods = generate_methods(&program);
     let instruction = generate_instruction(&program);
     let cpi = generate_cpi(&program);
@@ -20,21 +21,27 @@ pub fn generate(program: Program) -> proc_macro2::TokenStream {
         #[cfg(not(feature = "no-entrypoint"))]
         fn entry(program_id: &Pubkey, accounts: &[AccountInfo], instruction_data: &[u8]) -> ProgramResult {
             let mut data: &[u8] = instruction_data;
-            let ix = instruction::#instruction_name::deserialize(&mut data)
+            let ix = __private::instruction::#instruction_name::deserialize(&mut data)
                 .map_err(|_| ProgramError::Custom(1))?; // todo: error code
 
                 #dispatch
         }
 
-        #methods
+        // Create a private module to not clutter the program's namespace.
+        mod __private {
+            use super::*;
+
+            #handlers_non_inlined
+
+            #instruction
+        }
 
-        #instruction
+        #methods
 
         #cpi
     }
 }
 pub fn generate_dispatch(program: &Program) -> proc_macro2::TokenStream {
-    let program_name = &program.name;
     let dispatch_arms: Vec<proc_macro2::TokenStream> = program
         .rpcs
         .iter()
@@ -42,10 +49,42 @@ pub fn generate_dispatch(program: &Program) -> proc_macro2::TokenStream {
             let rpc_arg_names: Vec<&syn::Ident> = rpc.args.iter().map(|arg| &arg.name).collect();
             let variant_arm = generate_ix_variant(program, rpc);
             let rpc_name = &rpc.raw_method.sig.ident;
+            quote! {
+                __private::instruction::#variant_arm => {
+                    __private::#rpc_name(program_id, accounts, #(#rpc_arg_names),*)
+                }
+            }
+        })
+        .collect();
+
+    quote! {
+        match ix {
+            #(#dispatch_arms),*
+        }
+    }
+}
+
+// Generate non-inlined wrappers for each instruction handler, since Solana's
+// BPF max stack size can't handle reasonable sized dispatch trees without doing
+// so.
+pub fn generate_non_inlined_handlers(program: &Program) -> proc_macro2::TokenStream {
+    let program_name = &program.name;
+    let non_inlined_handlers: Vec<proc_macro2::TokenStream> = program
+        .rpcs
+        .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 rpc_name = &rpc.raw_method.sig.ident;
             let anchor = &rpc.anchor_ident;
 
             quote! {
-                instruction::#variant_arm => {
+                #[inline(never)]
+                pub fn #rpc_name(
+                    program_id: &Pubkey,
+                    accounts: &[AccountInfo],
+                    #(#rpc_params),*
+                ) -> ProgramResult {
                     let mut remaining_accounts: &[AccountInfo] = accounts;
                     let mut accounts = #anchor::try_accounts(program_id, &mut remaining_accounts)?;
                     #program_name::#rpc_name(
@@ -59,9 +98,7 @@ pub fn generate_dispatch(program: &Program) -> proc_macro2::TokenStream {
         .collect();
 
     quote! {
-        match ix {
-            #(#dispatch_arms),*
-        }
+        #(#non_inlined_handlers)*
     }
 }
 
@@ -131,7 +168,7 @@ pub fn generate_instruction(program: &Program) -> proc_macro2::TokenStream {
 
 fn instruction_enum_name(program: &Program) -> proc_macro2::Ident {
     proc_macro2::Ident::new(
-        &format!("_{}Instruction", program.name.to_string().to_camel_case()),
+        &format!("{}Instruction", program.name.to_string().to_camel_case()),
         program.name.span(),
     )
 }
@@ -152,10 +189,10 @@ fn generate_cpi(program: &Program) -> proc_macro2::TokenStream {
                         #(#args),*
                     ) -> ProgramResult {
                         let ix = {
-                            let ix = instruction::#ix_variant;
+                            let ix = __private::instruction::#ix_variant;
                             let data = AnchorSerialize::try_to_vec(&ix)
                                 .map_err(|_| ProgramError::InvalidInstructionData)?;
-                            let accounts = ctx.accounts.to_account_metas();
+                            let accounts = ctx.accounts.to_account_metas(None);
                             anchor_lang::solana_program::instruction::Instruction {
                                 program_id: *ctx.program.key,
                                 accounts,

+ 17 - 1
syn/src/idl.rs

@@ -18,10 +18,26 @@ pub struct Idl {
 #[derive(Debug, Serialize, Deserialize)]
 pub struct IdlInstruction {
     pub name: String,
-    pub accounts: Vec<IdlAccount>,
+    pub accounts: Vec<IdlAccountItem>,
     pub args: Vec<IdlField>,
 }
 
+// A single struct deriving `Accounts`.
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct IdlAccounts {
+    pub name: String,
+    pub accounts: Vec<IdlAccountItem>,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(untagged)]
+pub enum IdlAccountItem {
+    IdlAccount(IdlAccount),
+    IdlAccounts(IdlAccounts),
+}
+
+// A single field in the accounts struct.
 #[derive(Debug, Serialize, Deserialize)]
 #[serde(rename_all = "camelCase")]
 pub struct IdlAccount {

+ 22 - 6
syn/src/lib.rs

@@ -1,7 +1,7 @@
 //! DSL syntax tokens.
 
 #[cfg(feature = "idl")]
-use crate::idl::IdlAccount;
+use crate::idl::{IdlAccount, IdlAccountItem, IdlAccounts};
 use anyhow::Result;
 #[cfg(feature = "idl")]
 use heck::MixedCase;
@@ -85,21 +85,28 @@ impl AccountsStruct {
     }
 
     #[cfg(feature = "idl")]
-    pub fn idl_accounts(&self, global_accs: &HashMap<String, AccountsStruct>) -> Vec<IdlAccount> {
+    pub fn idl_accounts(
+        &self,
+        global_accs: &HashMap<String, AccountsStruct>,
+    ) -> Vec<IdlAccountItem> {
         self.fields
             .iter()
-            .flat_map(|acc: &AccountField| match acc {
+            .map(|acc: &AccountField| match acc {
                 AccountField::AccountsStruct(comp_f) => {
                     let accs_strct = global_accs
                         .get(&comp_f.symbol)
                         .expect("Could not reslve Accounts symbol");
-                    accs_strct.idl_accounts(global_accs)
+                    let accounts = accs_strct.idl_accounts(global_accs);
+                    IdlAccountItem::IdlAccounts(IdlAccounts {
+                        name: comp_f.ident.to_string().to_mixed_case(),
+                        accounts,
+                    })
                 }
-                AccountField::Field(acc) => vec![IdlAccount {
+                AccountField::Field(acc) => IdlAccountItem::IdlAccount(IdlAccount {
                     name: acc.ident.to_string().to_mixed_case(),
                     is_mut: acc.is_mut,
                     is_signer: acc.is_signer,
-                }],
+                }),
             })
             .collect::<Vec<_>>()
     }
@@ -120,6 +127,8 @@ pub enum AccountField {
 pub struct CompositeField {
     pub ident: syn::Ident,
     pub symbol: String,
+    pub constraints: Vec<Constraint>,
+    pub raw_field: syn::Field,
 }
 
 // An account in the accounts struct.
@@ -219,6 +228,7 @@ pub enum Constraint {
     Literal(ConstraintLiteral),
     Owner(ConstraintOwner),
     RentExempt(ConstraintRentExempt),
+    Seeds(ConstraintSeeds),
 }
 
 #[derive(Debug)]
@@ -246,8 +256,14 @@ pub enum ConstraintRentExempt {
     Skip,
 }
 
+#[derive(Debug)]
+pub struct ConstraintSeeds {
+    pub seeds: proc_macro2::Group,
+}
+
 #[derive(Debug)]
 pub struct Error {
+    pub name: String,
     pub raw_enum: syn::ItemEnum,
     pub ident: syn::Ident,
     pub codes: Vec<ErrorCode>,

+ 25 - 16
syn/src/parser/accounts.rs

@@ -1,7 +1,7 @@
 use crate::{
     AccountField, AccountsStruct, CompositeField, Constraint, ConstraintBelongsTo,
-    ConstraintLiteral, ConstraintOwner, ConstraintRentExempt, ConstraintSigner, CpiAccountTy,
-    Field, ProgramAccountTy, SysvarTy, Ty,
+    ConstraintLiteral, ConstraintOwner, ConstraintRentExempt, ConstraintSeeds, ConstraintSigner,
+    CpiAccountTy, Field, ProgramAccountTy, SysvarTy, Ty,
 };
 
 pub fn parse(strct: &syn::ItemStruct) -> AccountsStruct {
@@ -43,13 +43,13 @@ pub fn parse(strct: &syn::ItemStruct) -> AccountsStruct {
 
 fn parse_field(f: &syn::Field, anchor: Option<&syn::Attribute>) -> AccountField {
     let ident = f.ident.clone().unwrap();
+    let (constraints, is_mut, is_signer, is_init) = match anchor {
+        None => (vec![], false, false, false),
+        Some(anchor) => parse_constraints(anchor),
+    };
     match is_field_primitive(f) {
         true => {
             let ty = parse_ty(f);
-            let (constraints, is_mut, is_signer, is_init) = match anchor {
-                None => (vec![], false, false, false),
-                Some(anchor) => parse_constraints(anchor, &ty),
-            };
             AccountField::Field(Field {
                 ident,
                 ty,
@@ -62,9 +62,12 @@ fn parse_field(f: &syn::Field, anchor: Option<&syn::Attribute>) -> AccountField
         false => AccountField::AccountsStruct(CompositeField {
             ident,
             symbol: ident_string(f),
+            constraints,
+            raw_field: f.clone(),
         }),
     }
 }
+
 fn is_field_primitive(f: &syn::Field) -> bool {
     match ident_string(f).as_str() {
         "ProgramAccount" | "CpiAccount" | "Sysvar" | "AccountInfo" => true,
@@ -167,7 +170,7 @@ fn parse_sysvar(path: &syn::Path) -> SysvarTy {
     }
 }
 
-fn parse_constraints(anchor: &syn::Attribute, ty: &Ty) -> (Vec<Constraint>, bool, bool, bool) {
+fn parse_constraints(anchor: &syn::Attribute) -> (Vec<Constraint>, bool, bool, bool) {
     let mut tts = anchor.tokens.clone().into_iter();
     let g_stream = match tts.next().expect("Must have a token group") {
         proc_macro2::TokenTree::Group(g) => g.stream(),
@@ -178,7 +181,6 @@ fn parse_constraints(anchor: &syn::Attribute, ty: &Ty) -> (Vec<Constraint>, bool
     let mut is_mut = false;
     let mut is_signer = false;
     let mut constraints = vec![];
-    let mut has_owner_constraint = false;
     let mut is_rent_exempt = None;
 
     let mut inner_tts = g_stream.into_iter();
@@ -201,7 +203,21 @@ fn parse_constraints(anchor: &syn::Attribute, ty: &Ty) -> (Vec<Constraint>, bool
                     is_signer = true;
                     constraints.push(Constraint::Signer(ConstraintSigner {}));
                 }
-                "belongs_to" => {
+                "seeds" => {
+                    match inner_tts.next().unwrap() {
+                        proc_macro2::TokenTree::Punct(punct) => {
+                            assert!(punct.as_char() == '=');
+                            punct
+                        }
+                        _ => panic!("invalid syntax"),
+                    };
+                    let seeds = match inner_tts.next().unwrap() {
+                        proc_macro2::TokenTree::Group(g) => g,
+                        _ => panic!("invalid syntax"),
+                    };
+                    constraints.push(Constraint::Seeds(ConstraintSeeds { seeds }))
+                }
+                "belongs_to" | "has_one" => {
                     match inner_tts.next().unwrap() {
                         proc_macro2::TokenTree::Punct(punct) => {
                             assert!(punct.as_char() == '=');
@@ -233,7 +249,6 @@ fn parse_constraints(anchor: &syn::Attribute, ty: &Ty) -> (Vec<Constraint>, bool
                         _ => panic!("invalid syntax"),
                     };
                     constraints.push(Constraint::Owner(constraint));
-                    has_owner_constraint = true;
                 }
                 "rent_exempt" => {
                     match inner_tts.next() {
@@ -279,12 +294,6 @@ fn parse_constraints(anchor: &syn::Attribute, ty: &Ty) -> (Vec<Constraint>, bool
         }
     }
 
-    if !has_owner_constraint {
-        if let Ty::ProgramAccount(_) = ty {
-            constraints.push(Constraint::Owner(ConstraintOwner::Program));
-        }
-    }
-
     if let Some(is_re) = is_rent_exempt {
         match is_re {
             false => constraints.push(Constraint::RentExempt(ConstraintRentExempt::Skip)),

+ 1 - 0
syn/src/parser/error.rs

@@ -32,6 +32,7 @@ pub fn parse(error_enum: &mut syn::ItemEnum) -> Error {
         .collect();
 
     Error {
+        name: error_enum.ident.to_string(),
         raw_enum: error_enum.clone(),
         ident,
         codes,

+ 61 - 11
syn/src/parser/file.rs

@@ -21,9 +21,10 @@ pub fn parse(filename: impl AsRef<Path>) -> Result<Idl> {
     let f = syn::parse_file(&src).expect("Unable to parse file");
 
     let p = program::parse(parse_program_mod(&f));
-    let errors = parse_error_enum(&f).map(|mut e| {
-        error::parse(&mut e)
-            .codes
+
+    let error = parse_error_enum(&f).map(|mut e| error::parse(&mut e));
+    let error_codes = error.as_ref().map(|e| {
+        e.codes
             .iter()
             .map(|code| IdlErrorCode {
                 code: 100 + code.id,
@@ -79,11 +80,17 @@ pub fn parse(filename: impl AsRef<Path>) -> Result<Idl> {
     let mut accounts = vec![];
     let mut types = vec![];
     let ty_defs = parse_ty_defs(&f)?;
+
+    let error_name = error.map(|e| e.name).unwrap_or("".to_string());
+
     for ty_def in ty_defs {
-        if acc_names.contains(&ty_def.name) {
-            accounts.push(ty_def);
-        } else {
-            types.push(ty_def);
+        // Don't add the error type to the types or accounts sections.
+        if ty_def.name != error_name {
+            if acc_names.contains(&ty_def.name) {
+                accounts.push(ty_def);
+            } else {
+                types.push(ty_def);
+            }
         }
     }
 
@@ -93,7 +100,7 @@ pub fn parse(filename: impl AsRef<Path>) -> Result<Idl> {
         instructions,
         types,
         accounts,
-        errors,
+        errors: error_codes,
         metadata: None,
     })
 }
@@ -117,14 +124,16 @@ fn parse_program_mod(f: &syn::File) -> syn::ItemMod {
                     })
                     .collect::<Vec<_>>();
                 if mods.len() != 1 {
-                    panic!("invalid program attribute");
+                    return None;
                 }
                 Some(item_mod)
             }
             _ => None,
         })
         .collect::<Vec<_>>();
-    assert!(mods.len() == 1);
+    if mods.len() != 1 {
+        panic!("Did not find program attribute");
+    }
     mods[0].clone()
 }
 
@@ -193,7 +202,7 @@ fn parse_ty_defs(f: &syn::File) -> Result<Vec<IdlTypeDef>> {
                         syn::Fields::Named(fields) => fields
                             .named
                             .iter()
-                            .map(|f| {
+                            .map(|f: &syn::Field| {
                                 let mut tts = proc_macro2::TokenStream::new();
                                 f.ty.to_tokens(&mut tts);
                                 Ok(IdlField {
@@ -212,7 +221,48 @@ fn parse_ty_defs(f: &syn::File) -> Result<Vec<IdlTypeDef>> {
                 }
                 None
             }
+            syn::Item::Enum(enm) => {
+                let name = enm.ident.to_string();
+                let variants = enm
+                    .variants
+                    .iter()
+                    .map(|variant: &syn::Variant| {
+                        let name = variant.ident.to_string();
+                        let fields = match &variant.fields {
+                            syn::Fields::Unit => None,
+                            syn::Fields::Unnamed(fields) => {
+                                let fields: Vec<IdlType> =
+                                    fields.unnamed.iter().map(to_idl_type).collect();
+                                Some(EnumFields::Tuple(fields))
+                            }
+                            syn::Fields::Named(fields) => {
+                                let fields: Vec<IdlField> = fields
+                                    .named
+                                    .iter()
+                                    .map(|f: &syn::Field| {
+                                        let name = f.ident.as_ref().unwrap().to_string();
+                                        let ty = to_idl_type(f);
+                                        IdlField { name, ty }
+                                    })
+                                    .collect();
+                                Some(EnumFields::Named(fields))
+                            }
+                        };
+                        EnumVariant { name, fields }
+                    })
+                    .collect::<Vec<EnumVariant>>();
+                Some(Ok(IdlTypeDef {
+                    name,
+                    ty: IdlTypeDefTy::Enum { variants },
+                }))
+            }
             _ => None,
         })
         .collect()
 }
+
+fn to_idl_type(f: &syn::Field) -> IdlType {
+    let mut tts = proc_macro2::TokenStream::new();
+    f.ty.to_tokens(&mut tts);
+    tts.to_string().parse().unwrap()
+}

+ 11 - 6
syn/src/parser/program.rs

@@ -66,13 +66,18 @@ fn extract_ident(path_ty: &syn::PatType) -> &proc_macro2::Ident {
         syn::PathArguments::AngleBracketed(args) => args,
         _ => panic!("invalid syntax"),
     };
-    let path = match &generic_args.args.first().unwrap() {
-        syn::GenericArgument::Type(ty) => match ty {
-            syn::Type::Path(ty_path) => &ty_path.path,
-            _ => panic!("invalid syntax"),
-        },
+    let generic_ty = generic_args
+        .args
+        .iter()
+        .filter_map(|arg| match arg {
+            syn::GenericArgument::Type(ty) => Some(ty),
+            _ => None,
+        })
+        .next()
+        .unwrap();
+    let path = match generic_ty {
+        syn::Type::Path(ty_path) => &ty_path.path,
         _ => panic!("invalid syntax"),
     };
-
     &path.segments[0].ident
 }

+ 74 - 8
ts/src/coder.ts

@@ -1,7 +1,7 @@
 import camelCase from "camelcase";
 import { Layout } from "buffer-layout";
 import * as borsh from "@project-serum/borsh";
-import { Idl, IdlField, IdlTypeDef } from "./idl";
+import { Idl, IdlField, IdlTypeDef, IdlEnumVariant, IdlType } from "./idl";
 import { IdlError } from "./error";
 
 /**
@@ -18,9 +18,15 @@ export default class Coder {
    */
   readonly accounts: AccountsCoder;
 
+  /**
+   * Types coder.
+   */
+  readonly types: TypesCoder;
+
   constructor(idl: Idl) {
     this.instruction = new InstructionCoder(idl);
     this.accounts = new AccountsCoder(idl);
+    this.types = new TypesCoder(idl);
   }
 }
 
@@ -49,7 +55,7 @@ class InstructionCoder<T = any> {
 
   private static parseIxLayout(idl: Idl): Layout {
     let ixLayouts = idl.instructions.map((ix) => {
-      let fieldLayouts = ix.args.map((arg) =>
+      let fieldLayouts = ix.args.map((arg: IdlField) =>
         IdlCoder.fieldLayout(arg, idl.types)
       );
       const name = camelCase(ix.name);
@@ -94,6 +100,41 @@ class AccountsCoder {
   }
 }
 
+/**
+ * Encodes and decodes user defined types.
+ */
+class TypesCoder {
+  /**
+   * Maps account type identifier to a layout.
+   */
+  private layouts: Map<string, Layout>;
+
+  public constructor(idl: Idl) {
+    if (idl.types === undefined) {
+      this.layouts = new Map();
+      return;
+    }
+    const layouts = idl.types.map((acc) => {
+      return [acc.name, IdlCoder.typeDefLayout(acc, idl.types)];
+    });
+
+    // @ts-ignore
+    this.layouts = new Map(layouts);
+  }
+
+  public encode<T = any>(accountName: string, account: T): Buffer {
+    const buffer = Buffer.alloc(1000); // TODO: use a tighter buffer.
+    const layout = this.layouts.get(accountName);
+    const len = layout.encode(account, buffer);
+    return buffer.slice(0, len);
+  }
+
+  public decode<T = any>(accountName: string, ix: Buffer): T {
+    const layout = this.layouts.get(accountName);
+    return layout.decode(ix);
+  }
+}
+
 class IdlCoder {
   public static fieldLayout(field: IdlField, types?: IdlTypeDef[]): Layout {
     const fieldName =
@@ -160,7 +201,7 @@ class IdlCoder {
           // @ts-ignore
           const filtered = types.filter((t) => t.name === field.type.defined);
           if (filtered.length !== 1) {
-            throw new IdlError("Type not found");
+            throw new IdlError(`Type not found: ${JSON.stringify(field)}`);
           }
           return IdlCoder.typeDefLayout(filtered[0], types, fieldName);
         } else {
@@ -176,13 +217,38 @@ class IdlCoder {
     name?: string
   ): Layout {
     if (typeDef.type.kind === "struct") {
-      const fieldLayouts = typeDef.type.fields.map((field) =>
-        IdlCoder.fieldLayout(field, types)
-      );
+      const fieldLayouts = typeDef.type.fields.map((field) => {
+        const x = IdlCoder.fieldLayout(field, types);
+        return x;
+      });
       return borsh.struct(fieldLayouts, name);
+    } else if (typeDef.type.kind === "enum") {
+      let variants = typeDef.type.variants.map((variant: IdlEnumVariant) => {
+        const name = camelCase(variant.name);
+        if (variant.fields === undefined) {
+          return borsh.struct([], name);
+        }
+        // @ts-ignore
+        const fieldLayouts = variant.fields.map((f: IdlField | IdlType) => {
+          // @ts-ignore
+          if (f.name === undefined) {
+            throw new Error("Tuple enum variants not yet implemented.");
+          }
+          // @ts-ignore
+          return IdlCoder.fieldLayout(f, types);
+        });
+        return borsh.struct(fieldLayouts, name);
+      });
+
+      if (name !== undefined) {
+        // Buffer-layout lib requires the name to be null (on construction)
+        // when used as a field.
+        return borsh.rustEnum(variants).replicate(name);
+      }
+
+      return borsh.rustEnum(variants, name);
     } else {
-      // TODO: enums
-      throw new Error("Enums not yet implemented");
+      throw new Error(`Unknown type kint: ${typeDef}`);
     }
   }
 }

+ 20 - 10
ts/src/idl.ts

@@ -9,16 +9,24 @@ export type Idl = {
 
 export type IdlInstruction = {
   name: string;
-  accounts: IdlAccount[];
+  accounts: IdlAccountItem[];
   args: IdlField[];
 };
 
+export type IdlAccountItem = IdlAccount | IdlAccounts;
+
 export type IdlAccount = {
   name: string;
   isMut: boolean;
   isSigner: boolean;
 };
 
+// A nested/recursive version of IdlAccount.
+export type IdlAccounts = {
+  name: string;
+  accounts: IdlAccountItem[];
+};
+
 export type IdlField = {
   name: string;
   type: IdlType;
@@ -32,17 +40,12 @@ export type IdlTypeDef = {
 type IdlTypeDefTy = {
   kind: "struct" | "enum";
   fields?: IdlTypeDefStruct;
-  variants?: IdlTypeDefEnum;
+  variants?: IdlEnumVariant[];
 };
 
 type IdlTypeDefStruct = Array<IdlField>;
 
-// TODO
-type IdlTypeDefEnum = {
-  variants: IdlEnumVariant;
-};
-
-type IdlType =
+export type IdlType =
   | "bool"
   | "u8"
   | "i8"
@@ -72,10 +75,17 @@ export type IdlTypeDefined = {
   defined: string;
 };
 
-type IdlEnumVariant = {
-  // todo
+export type IdlEnumVariant = {
+  name: string;
+  fields?: IdlEnumFields;
 };
 
+type IdlEnumFields = IdlEnumFieldsNamed | IdlEnumFieldsTuple;
+
+type IdlEnumFieldsNamed = IdlField[];
+
+type IdlEnumFieldsTuple = IdlType[];
+
 type IdlErrorCode = {
   code: number;
   name: string;

+ 8 - 2
ts/src/program.ts

@@ -2,7 +2,7 @@ import { PublicKey } from "@solana/web3.js";
 import { RpcFactory } from "./rpc";
 import { Idl } from "./idl";
 import Coder from "./coder";
-import { Rpcs, Ixs, Accounts } from "./rpc";
+import { Rpcs, Ixs, Txs, Accounts } from "./rpc";
 
 /**
  * Program is the IDL deserialized representation of a Solana program.
@@ -34,6 +34,11 @@ export class Program {
    */
   readonly instruction: Ixs;
 
+  /**
+   * Functions to build `Transaction` objects.
+   */
+  readonly transaction: Txs;
+
   /**
    * Coder for serializing rpc requests.
    */
@@ -47,9 +52,10 @@ export class Program {
     const coder = new Coder(idl);
 
     // Build the dynamic RPC functions.
-    const [rpcs, ixs, accounts] = RpcFactory.build(idl, coder, programId);
+    const [rpcs, ixs, txs, accounts] = RpcFactory.build(idl, coder, programId);
     this.rpc = rpcs;
     this.instruction = ixs;
+    this.transaction = txs;
     this.account = accounts;
     this.coder = coder;
   }

+ 251 - 61
ts/src/rpc.ts

@@ -1,18 +1,34 @@
 import camelCase from "camelcase";
 import {
   Account,
+  AccountMeta,
   PublicKey,
   ConfirmOptions,
+  SystemProgram,
   Transaction,
   TransactionSignature,
   TransactionInstruction,
 } from "@solana/web3.js";
 import { sha256 } from "crypto-hash";
-import { Idl, IdlInstruction } from "./idl";
+import {
+  Idl,
+  IdlAccount,
+  IdlInstruction,
+  IdlTypeDef,
+  IdlType,
+  IdlField,
+  IdlEnumVariant,
+  IdlAccountItem,
+} from "./idl";
 import { IdlError, ProgramError } from "./error";
 import Coder from "./coder";
 import { getProvider } from "./";
 
+/**
+ * Number of bytes of the account discriminator.
+ */
+const ACCOUNT_DISCRIMINATOR_SIZE = 8;
+
 /**
  * Rpcs is a dynamically generated object with rpc methods attached.
  */
@@ -27,6 +43,10 @@ export interface Ixs {
   [key: string]: IxFn;
 }
 
+export interface Txs {
+  [key: string]: TxFn;
+}
+
 /**
  * Accounts is a dynamically generated object to fetch any given account
  * of a program.
@@ -45,6 +65,11 @@ export type RpcFn = (...args: any[]) => Promise<TransactionSignature>;
  */
 export type IxFn = (...args: any[]) => TransactionInstruction;
 
+/**
+ * Tx is a function to create a `Transaction` generate from an IDL.
+ */
+export type TxFn = (...args: any[]) => Transaction;
+
 /**
  * Account is a function returning a deserialized account, given an address.
  */
@@ -62,12 +87,14 @@ export type RpcOptions = ConfirmOptions;
 type RpcContext = {
   // Accounts the instruction will use.
   accounts?: RpcAccounts;
+  remainingAccounts?: AccountMeta[];
   // Instructions to run *before* the specified rpc instruction.
   instructions?: TransactionInstruction[];
   // Accounts that must sign the transaction.
   signers?: Array<Account>;
   // RpcOptions.
   options?: RpcOptions;
+  __private?: { logAccounts: boolean };
 };
 
 /**
@@ -75,7 +102,7 @@ type RpcContext = {
  * The name of each key should match the name for that account in the IDL.
  */
 type RpcAccounts = {
-  [key: string]: PublicKey;
+  [key: string]: PublicKey | RpcAccounts;
 };
 
 /**
@@ -91,58 +118,32 @@ export class RpcFactory {
     idl: Idl,
     coder: Coder,
     programId: PublicKey
-  ): [Rpcs, Ixs, Accounts] {
+  ): [Rpcs, Ixs, Txs, Accounts] {
+    const idlErrors = parseIdlErrors(idl);
+
     const rpcs: Rpcs = {};
     const ixFns: Ixs = {};
-    const accountFns: Accounts = {};
-    const idlErrors = parseIdlErrors(idl);
+    const txFns: Txs = {};
+
     idl.instructions.forEach((idlIx) => {
       // Function to create a raw `TransactionInstruction`.
       const ix = RpcFactory.buildIx(idlIx, coder, programId);
+      // Ffnction to create a `Transaction`.
+      const tx = RpcFactory.buildTx(idlIx, ix);
       // Function to invoke an RPC against a cluster.
-      const rpc = RpcFactory.buildRpc(idlIx, ix, idlErrors);
+      const rpc = RpcFactory.buildRpc(idlIx, tx, idlErrors);
 
       const name = camelCase(idlIx.name);
       rpcs[name] = rpc;
       ixFns[name] = ix;
+      txFns[name] = tx;
     });
 
-    if (idl.accounts) {
-      idl.accounts.forEach((idlAccount) => {
-        const accountFn = async (address: PublicKey): Promise<any> => {
-          const provider = getProvider();
-          if (provider === null) {
-            throw new Error("Provider not set");
-          }
-          const accountInfo = await provider.connection.getAccountInfo(address);
-          if (accountInfo === null) {
-            throw new Error(`Entity does not exist ${address}`);
-          }
-
-          // Assert the account discriminator is correct.
-          const expectedDiscriminator = Buffer.from(
-            (
-              await sha256(`account:${idlAccount.name}`, {
-                outputFormat: "buffer",
-              })
-            ).slice(0, 8)
-          );
-          const discriminator = accountInfo.data.slice(0, 8);
-
-          if (expectedDiscriminator.compare(discriminator)) {
-            throw new Error("Invalid account discriminator");
-          }
-
-          // Chop off the discriminator before decoding.
-          const data = accountInfo.data.slice(8);
-          return coder.accounts.decode(idlAccount.name, data);
-        };
-        const name = camelCase(idlAccount.name);
-        accountFns[name] = accountFn;
-      });
-    }
+    const accountFns = idl.accounts
+      ? RpcFactory.buildAccounts(idl, coder, programId)
+      : {};
 
-    return [rpcs, ixFns, accountFns];
+    return [rpcs, ixFns, txFns, accountFns];
   }
 
   private static buildIx(
@@ -156,16 +157,18 @@ export class RpcFactory {
 
     const ix = (...args: any[]): TransactionInstruction => {
       const [ixArgs, ctx] = splitArgsAndCtx(idlIx, [...args]);
-      validateAccounts(idlIx, ctx.accounts);
+      validateAccounts(idlIx.accounts, ctx.accounts);
       validateInstruction(idlIx, ...args);
 
-      const keys = idlIx.accounts.map((acc) => {
-        return {
-          pubkey: ctx.accounts[acc.name],
-          isWritable: acc.isMut,
-          isSigner: acc.isSigner,
-        };
-      });
+      const keys = RpcFactory.accountsArray(ctx.accounts, idlIx.accounts);
+
+      if (ctx.remainingAccounts !== undefined) {
+        keys.push(...ctx.remainingAccounts);
+      }
+
+      if (ctx.__private && ctx.__private.logAccounts) {
+        console.log("Outoing account metas:", keys);
+      }
       return new TransactionInstruction({
         keys,
         programId,
@@ -173,21 +176,46 @@ export class RpcFactory {
       });
     };
 
+    // Utility fn for ordering the accounts for this instruction.
+    ix["accounts"] = (accs: RpcAccounts) => {
+      return RpcFactory.accountsArray(accs, idlIx.accounts);
+    };
+
     return ix;
   }
 
+  private static accountsArray(
+    ctx: RpcAccounts,
+    accounts: IdlAccountItem[]
+  ): any {
+    return accounts
+      .map((acc: IdlAccountItem) => {
+        // Nested accounts.
+        // @ts-ignore
+        const nestedAccounts: IdlAccountItem[] | undefined = acc.accounts;
+        if (nestedAccounts !== undefined) {
+          const rpcAccs = ctx[acc.name] as RpcAccounts;
+          return RpcFactory.accountsArray(rpcAccs, nestedAccounts).flat();
+        } else {
+          const account: IdlAccount = acc as IdlAccount;
+          return {
+            pubkey: ctx[acc.name],
+            isWritable: account.isMut,
+            isSigner: account.isSigner,
+          };
+        }
+      })
+      .flat();
+  }
+
   private static buildRpc(
     idlIx: IdlInstruction,
-    ixFn: IxFn,
+    txFn: TxFn,
     idlErrors: Map<number, string>
   ): RpcFn {
     const rpc = async (...args: any[]): Promise<TransactionSignature> => {
+      const tx = txFn(...args);
       const [_, ctx] = splitArgsAndCtx(idlIx, [...args]);
-      const tx = new Transaction();
-      if (ctx.instructions !== undefined) {
-        tx.add(...ctx.instructions);
-      }
-      tx.add(ixFn(...args));
       const provider = getProvider();
       if (provider === null) {
         throw new Error("Provider not found");
@@ -197,7 +225,7 @@ export class RpcFactory {
         return txSig;
       } catch (err) {
         let translatedErr = translateError(idlErrors, err);
-        if (err === null) {
+        if (translatedErr === null) {
           throw err;
         }
         throw translatedErr;
@@ -206,6 +234,80 @@ export class RpcFactory {
 
     return rpc;
   }
+
+  private static buildTx(idlIx: IdlInstruction, ixFn: IxFn): TxFn {
+    const txFn = (...args: any[]): Transaction => {
+      const [_, ctx] = splitArgsAndCtx(idlIx, [...args]);
+      const tx = new Transaction();
+      if (ctx.instructions !== undefined) {
+        tx.add(...ctx.instructions);
+      }
+      tx.add(ixFn(...args));
+      return tx;
+    };
+
+    return txFn;
+  }
+
+  private static buildAccounts(
+    idl: Idl,
+    coder: Coder,
+    programId: PublicKey
+  ): Accounts {
+    const accountFns: Accounts = {};
+    idl.accounts.forEach((idlAccount) => {
+      const accountFn = async (address: PublicKey): Promise<any> => {
+        const provider = getProvider();
+        if (provider === null) {
+          throw new Error("Provider not set");
+        }
+        const accountInfo = await provider.connection.getAccountInfo(address);
+        if (accountInfo === null) {
+          throw new Error(`Entity does not exist ${address}`);
+        }
+
+        // Assert the account discriminator is correct.
+        const expectedDiscriminator = Buffer.from(
+          (
+            await sha256(`account:${idlAccount.name}`, {
+              outputFormat: "buffer",
+            })
+          ).slice(0, 8)
+        );
+        const discriminator = accountInfo.data.slice(0, 8);
+
+        if (expectedDiscriminator.compare(discriminator)) {
+          throw new Error("Invalid account discriminator");
+        }
+
+        // Chop off the discriminator before decoding.
+        const data = accountInfo.data.slice(8);
+        return coder.accounts.decode(idlAccount.name, data);
+      };
+      const name = camelCase(idlAccount.name);
+      accountFns[name] = accountFn;
+      const size = ACCOUNT_DISCRIMINATOR_SIZE + accountSize(idl, idlAccount);
+      // @ts-ignore
+      accountFns[name]["size"] = size;
+      // @ts-ignore
+      accountFns[name]["createInstruction"] = async (
+        account: Account,
+        sizeOverride?: number
+      ): Promise<TransactionInstruction> => {
+        const provider = getProvider();
+        return SystemProgram.createAccount({
+          fromPubkey: provider.wallet.publicKey,
+          newAccountPubkey: account.publicKey,
+          space: sizeOverride ?? size,
+          lamports: await provider.connection.getMinimumBalanceForRentExemption(
+            sizeOverride ?? size
+          ),
+          programId,
+        });
+      };
+    });
+    return accountFns;
+  }
 }
 
 function translateError(
@@ -221,7 +323,7 @@ function translateError(
       let errorMsg = idlErrors.get(errorCode);
       if (errorMsg === undefined) {
         // Unexpected error code so just throw the untranslated error.
-        throw err;
+        return null;
       }
       return new ProgramError(errorCode, errorMsg);
     } catch (parseErr) {
@@ -279,10 +381,16 @@ function toInstruction(idlIx: IdlInstruction, ...args: any[]) {
 }
 
 // Throws error if any account required for the `ix` is not given.
-function validateAccounts(ix: IdlInstruction, accounts: RpcAccounts) {
-  ix.accounts.forEach((acc) => {
-    if (accounts[acc.name] === undefined) {
-      throw new Error(`Invalid arguments: ${acc.name} not provided.`);
+function validateAccounts(ixAccounts: IdlAccountItem[], accounts: RpcAccounts) {
+  ixAccounts.forEach((acc) => {
+    // @ts-ignore
+    if (acc.accounts !== undefined) {
+      // @ts-ignore
+      validateAccounts(acc.accounts, accounts[acc.name]);
+    } else {
+      if (accounts[acc.name] === undefined) {
+        throw new Error(`Invalid arguments: ${acc.name} not provided.`);
+      }
     }
   });
 }
@@ -291,3 +399,85 @@ function validateAccounts(ix: IdlInstruction, accounts: RpcAccounts) {
 function validateInstruction(ix: IdlInstruction, ...args: any[]) {
   // todo
 }
+
+function accountSize(idl: Idl, idlAccount: IdlTypeDef): number | undefined {
+  if (idlAccount.type.kind === "enum") {
+    let variantSizes = idlAccount.type.variants.map(
+      (variant: IdlEnumVariant) => {
+        if (variant.fields === undefined) {
+          return 0;
+        }
+        // @ts-ignore
+        return (
+          variant.fields
+            // @ts-ignore
+            .map((f: IdlField | IdlType) => {
+              // @ts-ignore
+              if (f.name === undefined) {
+                throw new Error("Tuple enum variants not yet implemented.");
+              }
+              // @ts-ignore
+              return typeSize(idl, f.type);
+            })
+            .reduce((a: number, b: number) => a + b)
+        );
+      }
+    );
+    return Math.max(...variantSizes) + 1;
+  }
+  if (idlAccount.type.fields === undefined) {
+    return 0;
+  }
+  return idlAccount.type.fields
+    .map((f) => typeSize(idl, f.type))
+    .reduce((a, b) => a + b);
+}
+
+// Returns the size of the type in bytes. For variable length types, just return
+// 1. Users should override this value in such cases.
+function typeSize(idl: Idl, ty: IdlType): number {
+  switch (ty) {
+    case "bool":
+      return 1;
+    case "u8":
+      return 1;
+    case "i8":
+      return 1;
+    case "u16":
+      return 2;
+    case "u32":
+      return 4;
+    case "u64":
+      return 8;
+    case "i64":
+      return 8;
+    case "bytes":
+      return 1;
+    case "string":
+      return 1;
+    case "publicKey":
+      return 32;
+    default:
+      // @ts-ignore
+      if (ty.vec !== undefined) {
+        return 1;
+      }
+      // @ts-ignore
+      if (ty.option !== undefined) {
+        // @ts-ignore
+        return 1 + typeSize(ty.option);
+      }
+      // @ts-ignore
+      if (ty.defined !== undefined) {
+        // @ts-ignore
+        const filtered = idl.types.filter((t) => t.name === ty.defined);
+        if (filtered.length !== 1) {
+          throw new IdlError(`Type not found: ${JSON.stringify(ty)}`);
+        }
+        let typeDef = filtered[0];
+
+        return accountSize(idl, typeDef);
+      }
+      throw new Error(`Invalid type ${JSON.stringify(ty)}`);
+  }
+}