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

Program interfaces
This commit is contained in:
Armani Ferrante 2021-02-08 12:56:22 +08:00 committed by GitHub
commit a903d48e1f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 903 additions and 39 deletions

View File

@ -49,6 +49,7 @@ jobs:
- pushd examples/errors && anchor test && popd
- pushd examples/spl/token-proxy && anchor test && popd
- pushd examples/multisig && anchor test && popd
- pushd examples/interface && anchor test && popd
- pushd examples/tutorial/basic-0 && anchor test && popd
- pushd examples/tutorial/basic-1 && anchor test && popd
- pushd examples/tutorial/basic-2 && anchor test && popd

View File

@ -11,6 +11,12 @@ incremented for features.
## [Unreleased]
### Features
* lang: Adds the ability to create and use CPI program interfaces [(#66)](https://github.com/project-serum/anchor/pull/66/files?file-filters%5B%5D=).
### Breaking Changes
* lang, client, ts: Migrate from rust enum based method dispatch to a variant of sighash [(#64)](https://github.com/project-serum/anchor/pull/64).
## [0.1.0] - 2021-01-31

13
Cargo.lock generated
View File

@ -81,6 +81,18 @@ dependencies = [
"syn 1.0.57",
]
[[package]]
name = "anchor-attribute-interface"
version = "0.1.0"
dependencies = [
"anchor-syn",
"anyhow",
"heck",
"proc-macro2 1.0.24",
"quote 1.0.8",
"syn 1.0.57",
]
[[package]]
name = "anchor-attribute-program"
version = "0.1.0"
@ -155,6 +167,7 @@ dependencies = [
"anchor-attribute-access-control",
"anchor-attribute-account",
"anchor-attribute-error",
"anchor-attribute-interface",
"anchor-attribute-program",
"anchor-attribute-state",
"anchor-derive-accounts",

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,19 @@
[package]
name = "counter-auth"
version = "0.1.0"
description = "Created with Anchor"
edition = "2018"
[lib]
crate-type = ["cdylib", "lib"]
name = "counter_auth"
[features]
no-entrypoint = []
no-idl = []
cpi = ["no-entrypoint"]
default = []
[dependencies]
anchor-lang = { git = "https://github.com/project-serum/anchor" }
counter = { path = "../counter", features = ["cpi"] }

View File

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

View File

@ -0,0 +1,43 @@
//! counter-auth is an example of a program *implementing* an external program
//! interface. Here the `counter::Auth` trait, where we only allow a count
//! to be incremented if it changes the counter from odd -> even or even -> odd.
//! Creative, I know. :P.
#![feature(proc_macro_hygiene)]
use anchor_lang::prelude::*;
use counter::Auth;
#[program]
pub mod counter_auth {
use super::*;
#[state]
pub struct CounterAuth {}
// TODO: remove this impl block after addressing
// https://github.com/project-serum/anchor/issues/71.
impl CounterAuth {
pub fn new(_ctx: Context<Empty>) -> Result<Self, ProgramError> {
Ok(Self {})
}
}
impl<'info> Auth<'info, Empty> for CounterAuth {
fn is_authorized(_ctx: Context<Empty>, current: u64, new: u64) -> ProgramResult {
if current % 2 == 0 {
if new % 2 == 0 {
return Err(ProgramError::Custom(50)); // Arbitrary error code.
}
} else {
if new % 2 == 1 {
return Err(ProgramError::Custom(60)); // Arbitrary error code.
}
}
Ok(())
}
}
}
#[derive(Accounts)]
pub struct Empty {}

View File

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

View File

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

View File

@ -0,0 +1,73 @@
//! counter is an example program that depends on an external interface
//! that another program must implement. This allows our program to depend
//! on another program, without knowing anything about it other than the fact
//! that it implements the `Auth` trait.
//!
//! Here, we have a counter, where, in order to set the count, the `Auth`
//! program must first approve the transaction.
#![feature(proc_macro_hygiene)]
use anchor_lang::prelude::*;
#[program]
pub mod counter {
use super::*;
#[state]
pub struct Counter {
pub count: u64,
pub auth_program: Pubkey,
}
impl Counter {
pub fn new(_ctx: Context<Empty>, auth_program: Pubkey) -> Result<Self> {
Ok(Self {
count: 0,
auth_program,
})
}
#[access_control(SetCount::accounts(&self, &ctx))]
pub fn set_count(&mut self, ctx: Context<SetCount>, new_count: u64) -> Result<()> {
// Ask the auth program if we should approve the transaction.
let cpi_program = ctx.accounts.auth_program.clone();
let cpi_ctx = CpiContext::new(cpi_program, Empty {});
auth::is_authorized(cpi_ctx, self.count, new_count)?;
// Approved, so update.
self.count = new_count;
Ok(())
}
}
}
#[derive(Accounts)]
pub struct Empty {}
#[derive(Accounts)]
pub struct SetCount<'info> {
auth_program: AccountInfo<'info>,
}
impl<'info> SetCount<'info> {
// Auxiliary account validation requiring program inputs. As a convention,
// we separate it from the business logic of the instruction handler itself.
pub fn accounts(counter: &Counter, ctx: &Context<SetCount>) -> Result<()> {
if ctx.accounts.auth_program.key != &counter.auth_program {
return Err(ErrorCode::InvalidAuthProgram.into());
}
Ok(())
}
}
#[interface]
pub trait Auth<'info, T: Accounts<'info>> {
fn is_authorized(ctx: Context<T>, current: u64, new: u64) -> ProgramResult;
}
#[error]
pub enum ErrorCode {
#[msg("Invalid auth program.")]
InvalidAuthProgram,
}

View File

@ -0,0 +1,45 @@
const anchor = require('@project-serum/anchor');
const assert = require("assert");
describe("interface", () => {
// Configure the client to use the local cluster.
anchor.setProvider(anchor.Provider.env());
const counter = anchor.workspace.Counter;
const counterAuth = anchor.workspace.CounterAuth;
it("Is initialized!", async () => {
await counter.state.rpc.new(counterAuth.programId);
const stateAccount = await counter.state();
assert.ok(stateAccount.count.eq(new anchor.BN(0)));
assert.ok(stateAccount.authProgram.equals(counterAuth.programId));
});
it("Should fail to go from even to event", async () => {
await assert.rejects(
async () => {
await counter.state.rpc.setCount(new anchor.BN(4), {
accounts: {
authProgram: counterAuth.programId,
},
});
},
(err) => {
if (err.toString().split("custom program error: 0x32").length !== 2) {
return false;
}
return true;
}
);
});
it("Shold succeed to go from even to odd", async () => {
await counter.state.rpc.setCount(new anchor.BN(3), {
accounts: {
authProgram: counterAuth.programId,
},
});
const stateAccount = await counter.state();
assert.ok(stateAccount.count.eq(new anchor.BN(3)));
});
});

View File

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

View File

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

View File

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

View File

@ -17,6 +17,7 @@ anchor-attribute-account = { path = "./attribute/account", version = "0.1.0" }
anchor-attribute-error = { path = "./attribute/error", version = "0.1.0" }
anchor-attribute-program = { path = "./attribute/program", version = "0.1.0" }
anchor-attribute-state = { path = "./attribute/state", version = "0.1.0" }
anchor-attribute-interface = { path = "./attribute/interface", version = "0.1.0" }
anchor-derive-accounts = { path = "./derive/accounts", version = "0.1.0" }
serum-borsh = "0.8.1-serum.1"
solana-program = "=1.5.0"

View File

@ -0,0 +1,19 @@
[package]
name = "anchor-attribute-interface"
version = "0.1.0"
authors = ["Serum Foundation <foundation@projectserum.com>"]
repository = "https://github.com/project-serum/anchor"
license = "Apache-2.0"
description = "Attribute for defining a program interface trait"
edition = "2018"
[lib]
proc-macro = true
[dependencies]
proc-macro2 = "1.0"
quote = "1.0"
syn = { version = "=1.0.57", features = ["full"] }
anyhow = "1.0.32"
anchor-syn = { path = "../../syn", version = "0.1.0" }
heck = "0.3.2"

View File

@ -0,0 +1,120 @@
extern crate proc_macro;
use anchor_syn::parser;
use heck::SnakeCase;
use quote::quote;
use syn::parse_macro_input;
#[proc_macro_attribute]
pub fn interface(
_args: proc_macro::TokenStream,
input: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
let item_trait = parse_macro_input!(input as syn::ItemTrait);
let trait_name = item_trait.ident.to_string();
let mod_name: proc_macro2::TokenStream = item_trait
.ident
.to_string()
.to_snake_case()
.parse()
.unwrap();
let methods: Vec<proc_macro2::TokenStream> = item_trait
.items
.iter()
.filter_map(|trait_item: &syn::TraitItem| match trait_item {
syn::TraitItem::Method(m) => Some(m),
_ => None,
})
.map(|method: &syn::TraitItemMethod| {
let method_name = &method.sig.ident;
let args: Vec<&syn::PatType> = method
.sig
.inputs
.iter()
.filter_map(|arg: &syn::FnArg| match arg {
syn::FnArg::Typed(pat_ty) => Some(pat_ty),
// TODO: just map this to None once we allow this feature.
_ => panic!("Invalid syntax. No self allowed."),
})
.filter_map(|pat_ty: &syn::PatType| {
let mut ty = parser::tts_to_string(&pat_ty.ty);
ty.retain(|s| !s.is_whitespace());
if ty.starts_with("Context<") {
None
} else {
Some(pat_ty)
}
})
.collect();
let args_no_tys: Vec<&Box<syn::Pat>> = args
.iter()
.map(|arg| {
&arg.pat
})
.collect();
let args_struct = {
if args.len() == 0 {
quote! {
use anchor_lang::prelude::borsh;
#[derive(anchor_lang::AnchorSerialize, anchor_lang::AnchorDeserialize)]
struct Args;
}
} else {
quote! {
use anchor_lang::prelude::borsh;
#[derive(anchor_lang::AnchorSerialize, anchor_lang::AnchorDeserialize)]
struct Args {
#(#args),*
}
}
}
};
let sighash_arr = anchor_syn::codegen::program::sighash(&trait_name, &method_name.to_string());
let sighash_tts: proc_macro2::TokenStream =
format!("{:?}", sighash_arr).parse().unwrap();
quote! {
pub fn #method_name<'a,'b, 'c, 'info, T: anchor_lang::ToAccountMetas + anchor_lang::ToAccountInfos<'info>>(
ctx: anchor_lang::CpiContext<'a, 'b, 'c, 'info, T>,
#(#args),*
) -> anchor_lang::solana_program::entrypoint::ProgramResult {
#args_struct
let ix = {
let ix = Args {
#(#args_no_tys),*
};
let mut ix_data = anchor_lang::AnchorSerialize::try_to_vec(&ix)
.map_err(|_| anchor_lang::solana_program::program_error::ProgramError::InvalidInstructionData)?;
let mut data = #sighash_tts.to_vec();
data.append(&mut ix_data);
let accounts = ctx.accounts.to_account_metas(None);
anchor_lang::solana_program::instruction::Instruction {
program_id: *ctx.program.key,
accounts,
data,
}
};
let mut acc_infos = ctx.accounts.to_account_infos();
acc_infos.push(ctx.program.clone());
anchor_lang::solana_program::program::invoke_signed(
&ix,
&acc_infos,
ctx.signer_seeds,
)
}
}
})
.collect();
proc_macro::TokenStream::from(quote! {
#item_trait
mod #mod_name {
use super::*;
#(#methods)*
}
})
}

View File

@ -4,8 +4,8 @@ use anchor_syn::codegen::program as program_codegen;
use anchor_syn::parser::program as program_parser;
use syn::parse_macro_input;
/// The module containing all instruction handlers defining all entries to the
/// Solana program.
/// The `#[program]` attribute defines the module containing all instruction
/// handlers defining all entries into a Solana program.
#[proc_macro_attribute]
pub fn program(
_args: proc_macro::TokenStream,

View File

@ -1,4 +1,4 @@
use crate::Accounts;
use crate::{Accounts, ToAccountInfos, ToAccountMetas};
use solana_program::account_info::AccountInfo;
use solana_program::pubkey::Pubkey;
@ -27,13 +27,19 @@ impl<'a, 'b, 'c, 'info, T: Accounts<'info>> Context<'a, 'b, 'c, 'info, T> {
}
/// Context speciying non-argument inputs for cross-program-invocations.
pub struct CpiContext<'a, 'b, 'c, 'info, T: Accounts<'info>> {
pub struct CpiContext<'a, 'b, 'c, 'info, T>
where
T: ToAccountMetas + ToAccountInfos<'info>,
{
pub accounts: T,
pub program: AccountInfo<'info>,
pub signer_seeds: &'a [&'b [&'c [u8]]],
}
impl<'a, 'b, 'c, 'info, T: Accounts<'info>> CpiContext<'a, 'b, 'c, 'info, T> {
impl<'a, 'b, 'c, 'info, T> CpiContext<'a, 'b, 'c, 'info, T>
where
T: ToAccountMetas + ToAccountInfos<'info>,
{
pub fn new(program: AccountInfo<'info>, accounts: T) -> Self {
Self {
accounts,

View File

@ -39,6 +39,7 @@ pub mod idl;
mod program_account;
mod state;
mod sysvar;
mod vec;
pub use crate::context::{Context, CpiContext};
pub use crate::cpi_account::CpiAccount;
@ -49,6 +50,7 @@ pub use crate::sysvar::Sysvar;
pub use anchor_attribute_access_control::access_control;
pub use anchor_attribute_account::account;
pub use anchor_attribute_error::error;
pub use anchor_attribute_interface::interface;
pub use anchor_attribute_program::program;
pub use anchor_attribute_state::state;
pub use anchor_derive_accounts::Accounts;
@ -68,8 +70,8 @@ pub trait Accounts<'info>: ToAccountMetas + ToAccountInfos<'info> + Sized {
/// program dependent. However, users of these types should never have to
/// worry about account substitution attacks. For example, if a program
/// expects a `Mint` account from the SPL token program in a particular
/// field, then it should be impossible for this method to return `Ok` if any
/// other account type is given--from the SPL token program or elsewhere.
/// field, then it should be impossible for this method to return `Ok` if
/// any other account type is given--from the SPL token program or elsewhere.
///
/// `program_id` is the currently executing program. `accounts` is the
/// set of accounts to construct the type from. For every account used,
@ -171,9 +173,9 @@ pub trait InstructionData: AnchorSerialize {
/// All programs should include it via `anchor_lang::prelude::*;`.
pub mod prelude {
pub use super::{
access_control, account, error, program, state, AccountDeserialize, AccountSerialize,
Accounts, AccountsExit, AccountsInit, AnchorDeserialize, AnchorSerialize, Context,
CpiAccount, CpiContext, Ctor, ProgramAccount, ProgramState, Sysvar, ToAccountInfo,
access_control, account, error, interface, program, state, AccountDeserialize,
AccountSerialize, Accounts, AccountsExit, AccountsInit, AnchorDeserialize, AnchorSerialize,
Context, CpiAccount, CpiContext, Ctor, ProgramAccount, ProgramState, Sysvar, ToAccountInfo,
ToAccountInfos, ToAccountMetas,
};

19
lang/src/vec.rs Normal file
View File

@ -0,0 +1,19 @@
use crate::{ToAccountInfos, ToAccountMetas};
use solana_program::account_info::AccountInfo;
use solana_program::instruction::AccountMeta;
impl<'info, T: ToAccountInfos<'info>> ToAccountInfos<'info> for Vec<T> {
fn to_account_infos(&self) -> Vec<AccountInfo<'info>> {
self.iter()
.flat_map(|item| item.to_account_infos())
.collect()
}
}
impl<T: ToAccountMetas> ToAccountMetas for Vec<T> {
fn to_account_metas(&self, is_signer: Option<bool>) -> Vec<AccountMeta> {
self.iter()
.flat_map(|item| (*item).to_account_metas(is_signer))
.collect()
}
}

View File

@ -37,5 +37,12 @@ pub fn generate(error: Error) -> proc_macro2::TokenStream {
}
}
}
impl std::convert::From<#enum_name> for ProgramError {
fn from(e: #enum_name) -> ProgramError {
let err: Error = e.into();
err.into()
}
}
}
}

View File

@ -41,7 +41,7 @@ pub fn generate(program: Program) -> proc_macro2::TokenStream {
if cfg!(not(feature = "no-idl")) {
if sighash == anchor_lang::idl::IDL_IX_TAG.to_le_bytes() {
return __private::__idl(program_id, accounts, &instruction_data[8..]);
return __private::__idl(program_id, accounts, &instruction_data);
}
}
@ -66,6 +66,7 @@ pub fn generate(program: Program) -> proc_macro2::TokenStream {
}
pub fn generate_dispatch(program: &Program) -> proc_macro2::TokenStream {
// Dispatch the state constructor.
let ctor_state_dispatch_arm = match &program.state {
None => quote! { /* no-op */ },
Some(state) => {
@ -85,6 +86,8 @@ pub fn generate_dispatch(program: &Program) -> proc_macro2::TokenStream {
}
}
};
// Dispatch the state impl instructions.
let state_dispatch_arms: Vec<proc_macro2::TokenStream> = match &program.state {
None => vec![],
Some(s) => s
@ -112,6 +115,63 @@ pub fn generate_dispatch(program: &Program) -> proc_macro2::TokenStream {
})
.collect(),
};
// Dispatch all trait interface implementations.
let trait_dispatch_arms: Vec<proc_macro2::TokenStream> = match &program.state {
None => vec![],
Some(s) => s
.interfaces
.iter()
.flat_map(|iface: &crate::StateInterface| {
iface
.methods
.iter()
.map(|m: &crate::StateRpc| {
let rpc_arg_names: Vec<&syn::Ident> =
m.args.iter().map(|arg| &arg.name).collect();
let name = &m.raw_method.sig.ident.to_string();
let rpc_name: proc_macro2::TokenStream = format!("__{}_{}", iface.trait_name, name).parse().unwrap();
let raw_args: Vec<&syn::PatType> = m
.args
.iter()
.map(|arg: &crate::RpcArg| &arg.raw_arg)
.collect();
let sighash_arr = sighash(&iface.trait_name, &m.ident.to_string());
let sighash_tts: proc_macro2::TokenStream =
format!("{:?}", sighash_arr).parse().unwrap();
let args_struct = {
if m.args.len() == 0 {
quote! {
#[derive(anchor_lang::AnchorSerialize, anchor_lang::AnchorDeserialize)]
struct Args;
}
} else {
quote! {
#[derive(anchor_lang::AnchorSerialize, anchor_lang::AnchorDeserialize)]
struct Args {
#(#raw_args),*
}
}
}
};
quote! {
#sighash_tts => {
#args_struct
let ix = Args::deserialize(&mut instruction_data)
.map_err(|_| ProgramError::Custom(1))?; // todo: error code
let Args {
#(#rpc_arg_names),*
} = ix;
__private::#rpc_name(program_id, accounts, #(#rpc_arg_names),*)
}
}
})
.collect::<Vec<proc_macro2::TokenStream>>()
})
.collect(),
};
// Dispatch all global instructions.
let dispatch_arms: Vec<proc_macro2::TokenStream> = program
.rpcs
.iter()
@ -139,6 +199,7 @@ pub fn generate_dispatch(program: &Program) -> proc_macro2::TokenStream {
match sighash {
#ctor_state_dispatch_arm
#(#state_dispatch_arms)*
#(#trait_dispatch_arms)*
#(#dispatch_arms)*
_ => {
msg!("Fallback functions are not supported. If you have a use case, please file an issue.");
@ -166,7 +227,7 @@ pub fn generate_non_inlined_handlers(program: &Program) -> proc_macro2::TokenStr
let mut data: &[u8] = idl_ix_data;
let ix = anchor_lang::idl::IdlInstruction::deserialize(&mut data)
.map_err(|_| ProgramError::Custom(1))?; // todo
.map_err(|_| ProgramError::Custom(2))?; // todo
match ix {
anchor_lang::idl::IdlInstruction::Create { data_len } => {
@ -419,6 +480,101 @@ pub fn generate_non_inlined_handlers(program: &Program) -> proc_macro2::TokenStr
})
.collect(),
};
let non_inlined_state_trait_handlers: Vec<proc_macro2::TokenStream> = match &program.state {
None => Vec::new(),
Some(state) => state
.interfaces
.iter()
.flat_map(|iface: &crate::StateInterface| {
iface
.methods
.iter()
.map(|rpc| {
let rpc_params: Vec<_> = rpc.args.iter().map(|arg| &arg.raw_arg).collect();
let rpc_arg_names: Vec<&syn::Ident> =
rpc.args.iter().map(|arg| &arg.name).collect();
let private_rpc_name: proc_macro2::TokenStream = {
let n = format!("__{}_{}", iface.trait_name, &rpc.raw_method.sig.ident.to_string());
n.parse().unwrap()
};
let rpc_name = &rpc.raw_method.sig.ident;
let state_ty: proc_macro2::TokenStream = state.name.parse().unwrap();
let anchor_ident = &rpc.anchor_ident;
if rpc.has_receiver {
quote! {
#[inline(never)]
pub fn #private_rpc_name(
program_id: &Pubkey,
accounts: &[AccountInfo],
#(#rpc_params),*
) -> ProgramResult {
let mut remaining_accounts: &[AccountInfo] = accounts;
if remaining_accounts.len() == 0 {
return Err(ProgramError::Custom(1)); // todo
}
// Deserialize the program state account.
let state_account = &remaining_accounts[0];
let mut state: #state_ty = {
let data = state_account.try_borrow_data()?;
let mut sliced: &[u8] = &data;
anchor_lang::AccountDeserialize::try_deserialize(&mut sliced)?
};
remaining_accounts = &remaining_accounts[1..];
// Deserialize the program's execution context.
let mut accounts = #anchor_ident::try_accounts(
program_id,
&mut remaining_accounts,
)?;
let ctx = Context::new(program_id, &mut accounts, remaining_accounts);
// Execute user defined function.
state.#rpc_name(
ctx,
#(#rpc_arg_names),*
)?;
// Serialize the state and save it to storage.
accounts.exit(program_id)?;
let mut data = state_account.try_borrow_mut_data()?;
let dst: &mut [u8] = &mut data;
let mut cursor = std::io::Cursor::new(dst);
state.try_serialize(&mut cursor)?;
Ok(())
}
}
} else {
let state_name: proc_macro2::TokenStream = state.name.parse().unwrap();
quote! {
#[inline(never)]
pub fn #private_rpc_name(
program_id: &Pubkey,
accounts: &[AccountInfo],
#(#rpc_params),*
) -> ProgramResult {
let mut remaining_accounts: &[AccountInfo] = accounts;
let mut accounts = #anchor_ident::try_accounts(
program_id,
&mut remaining_accounts,
)?;
#state_name::#rpc_name(
Context::new(program_id, &mut accounts, remaining_accounts),
#(#rpc_arg_names),*
)?;
accounts.exit(program_id)
}
}
}
})
.collect::<Vec<proc_macro2::TokenStream>>()
})
.collect(),
};
let non_inlined_handlers: Vec<proc_macro2::TokenStream> = program
.rpcs
.iter()
@ -451,6 +607,7 @@ pub fn generate_non_inlined_handlers(program: &Program) -> proc_macro2::TokenStr
#non_inlined_idl
#non_inlined_ctor
#(#non_inlined_state_handlers)*
#(#non_inlined_state_trait_handlers)*
#(#non_inlined_handlers)*
}
}
@ -479,7 +636,14 @@ pub fn generate_ctor_typed_variant_with_semi(program: &Program) -> proc_macro2::
match &program.state {
None => quote! {},
Some(state) => {
let ctor_args = generate_ctor_typed_args(state);
let ctor_args: Vec<proc_macro2::TokenStream> = generate_ctor_typed_args(state)
.iter()
.map(|arg| {
format!("pub {}", parser::tts_to_string(&arg))
.parse()
.unwrap()
})
.collect();
if ctor_args.len() == 0 {
quote! {
#[derive(AnchorSerialize, AnchorDeserialize)]
@ -490,7 +654,7 @@ pub fn generate_ctor_typed_variant_with_semi(program: &Program) -> proc_macro2::
#[derive(AnchorSerialize, AnchorDeserialize)]
pub struct __Ctor {
#(#ctor_args),*
};
}
}
}
}
@ -821,7 +985,7 @@ fn generate_cpi(program: &Program) -> proc_macro2::TokenStream {
// Rust doesn't have method overloading so no need to use the arguments.
// However, we do namespace methods in the preeimage so that we can use
// different traits with the same method name.
fn sighash(namespace: &str, name: &str) -> [u8; 8] {
pub fn sighash(namespace: &str, name: &str) -> [u8; 8] {
let preimage = format!("{}::{}", namespace, name);
let mut sighash = [0u8; 8];

View File

@ -32,6 +32,7 @@ pub struct State {
pub strct: syn::ItemStruct,
pub impl_block: syn::ItemImpl,
pub methods: Vec<StateRpc>,
pub interfaces: Vec<StateInterface>,
pub ctor: syn::ImplItemMethod,
pub ctor_anchor: syn::Ident, // TODO: consolidate this with ctor above.
}
@ -42,6 +43,14 @@ pub struct StateRpc {
pub ident: syn::Ident,
pub args: Vec<RpcArg>,
pub anchor_ident: syn::Ident,
// True if there exists a &self on the method.
pub has_receiver: bool,
}
#[derive(Debug)]
pub struct StateInterface {
pub trait_name: String,
pub methods: Vec<StateRpc>,
}
#[derive(Debug)]

View File

@ -1,5 +1,5 @@
use crate::parser;
use crate::{Program, Rpc, RpcArg, State, StateRpc};
use crate::{Program, Rpc, RpcArg, State, StateInterface, StateRpc};
pub fn parse(program_mod: syn::ItemMod) -> Program {
let mod_ident = &program_mod.ident;
@ -28,12 +28,15 @@ pub fn parse(program_mod: syn::ItemMod) -> Program {
.next();
let impl_block: Option<&syn::ItemImpl> = strct.map(|strct| {
let item_impl = mod_content
let item_impls = mod_content
.iter()
.filter_map(|item| match item {
syn::Item::Impl(item_impl) => {
let impl_ty_str = parser::tts_to_string(&item_impl.self_ty);
let strct_name = strct.ident.to_string();
if item_impl.trait_.is_some() {
return None;
}
if strct_name != impl_ty_str {
return None;
}
@ -41,9 +44,39 @@ pub fn parse(program_mod: syn::ItemMod) -> Program {
}
_ => None,
})
.next()
.expect("Must provide an implementation");
item_impl
.collect::<Vec<&syn::ItemImpl>>();
item_impls[0]
});
// All program interface implementations.
let trait_impls: Option<Vec<StateInterface>> = strct.map(|_strct| {
mod_content
.iter()
.filter_map(|item| match item {
syn::Item::Impl(item_impl) => {
let trait_name = match &item_impl.trait_ {
None => return None,
Some((_, path, _)) => path
.segments
.iter()
.next()
.expect("Must have one segmeent in a path")
.ident
.clone()
.to_string(),
};
if item_impl.trait_.is_none() {
return None;
}
let methods = parse_state_trait_methods(item_impl);
Some(StateInterface {
trait_name,
methods,
})
}
_ => None,
})
.collect::<Vec<StateInterface>>()
});
strct.map(|strct| {
@ -112,6 +145,7 @@ pub fn parse(program_mod: syn::ItemMod) -> Program {
ident: m.sig.ident.clone(),
args,
anchor_ident,
has_receiver: true,
})
}
},
@ -122,6 +156,7 @@ pub fn parse(program_mod: syn::ItemMod) -> Program {
State {
name: strct.ident.to_string(),
strct: strct.clone(),
interfaces: trait_impls.expect("Some if state exists"),
impl_block,
ctor,
ctor_anchor,
@ -206,3 +241,52 @@ fn extract_ident(path_ty: &syn::PatType) -> &proc_macro2::Ident {
};
&path.segments[0].ident
}
fn parse_state_trait_methods(item_impl: &syn::ItemImpl) -> Vec<StateRpc> {
item_impl
.items
.iter()
.filter_map(|item: &syn::ImplItem| match item {
syn::ImplItem::Method(m) => match m.sig.inputs.first() {
None => None,
Some(_arg) => {
let mut has_receiver = false;
let mut args = m
.sig
.inputs
.iter()
.filter_map(|arg| match arg {
syn::FnArg::Receiver(_) => {
has_receiver = true;
None
}
syn::FnArg::Typed(arg) => Some(arg),
})
.map(|raw_arg| {
let ident = match &*raw_arg.pat {
syn::Pat::Ident(ident) => &ident.ident,
_ => panic!("invalid syntax"),
};
RpcArg {
name: ident.clone(),
raw_arg: raw_arg.clone(),
}
})
.collect::<Vec<RpcArg>>();
// Remove the Anchor accounts argument
let anchor = args.remove(0);
let anchor_ident = extract_ident(&anchor.raw_arg).clone();
Some(StateRpc {
raw_method: m.clone(),
ident: m.sig.ident.clone(),
args,
anchor_ident,
has_receiver,
})
}
},
_ => None,
})
.collect()
}

View File

@ -1,6 +1,6 @@
{
"name": "@project-serum/anchor",
"version": "0.1.0",
"version": "0.2.0-beta.1",
"description": "Anchor client",
"main": "dist/cjs/index.js",
"module": "dist/esm/index.js",
@ -27,11 +27,13 @@
"@solana/web3.js": "^0.90.4",
"@types/bn.js": "^4.11.6",
"@types/bs58": "^4.0.1",
"@types/crypto-hash": "^1.1.2",
"@types/pako": "^1.0.1",
"bn.js": "^5.1.2",
"bs58": "^4.0.1",
"buffer-layout": "^1.2.0",
"camelcase": "^5.3.1",
"crypto-hash": "^1.3.0",
"eventemitter3": "^4.0.7",
"find": "^0.3.0",
"js-sha256": "^0.9.0",

View File

@ -356,7 +356,7 @@ export async function stateDiscriminator(name: string): Promise<Buffer> {
// Returns the size of the type in bytes. For variable length types, just return
// 1. Users should override this value in such cases.
export function typeSize(idl: Idl, ty: IdlType): number {
function typeSize(idl: Idl, ty: IdlType): number {
switch (ty) {
case "bool":
return 1;
@ -386,7 +386,7 @@ export function typeSize(idl: Idl, ty: IdlType): number {
// @ts-ignore
if (ty.option !== undefined) {
// @ts-ignore
return 1 + typeSize(ty.option);
return 1 + typeSize(idl, ty.option);
}
// @ts-ignore
if (ty.defined !== undefined) {

View File

@ -753,6 +753,13 @@
dependencies:
"@types/node" "*"
"@types/crypto-hash@^1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@types/crypto-hash/-/crypto-hash-1.1.2.tgz#5a993deb0e6ba7c42f86eaa65d9bf563378f4569"
integrity sha512-sOmi+4Go2XKodLV4+lfP+5QMQ+6ZYqRJhK8D/n6xsxIUvlerEulmU9S4Lo02pXCH3qPBeJXEy+g8ZERktDJLSg==
dependencies:
crypto-hash "*"
"@types/express-serve-static-core@^4.17.9":
version "4.17.18"
resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.18.tgz#8371e260f40e0e1ca0c116a9afcd9426fa094c40"
@ -1687,7 +1694,7 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.2:
shebang-command "^2.0.0"
which "^2.0.1"
crypto-hash@^1.2.2:
crypto-hash@*, crypto-hash@^1.2.2, crypto-hash@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/crypto-hash/-/crypto-hash-1.3.0.tgz#b402cb08f4529e9f4f09346c3e275942f845e247"
integrity sha512-lyAZ0EMyjDkVvz8WOeVnuCPvKVBXcMv1l5SVqO1yC7PzTwrD/pPje/BIRbWhMoPe436U+Y2nD7f5bFx0kt+Sbg==