Lockup and staking examples (#28)

This commit is contained in:
Armani Ferrante 2021-01-20 17:13:02 -08:00
parent e636cf9721
commit c9ae5eb0ef
36 changed files with 3729 additions and 243 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,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"

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@ -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
src/boxed.rs Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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::*;
#instruction
#handlers_non_inlined
#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,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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}`);
}
const accountFns = idl.accounts
? RpcFactory.buildAccounts(idl, coder, programId)
: {};
// 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;
});
}
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)}`);
}
}