Merge pull request #41 from project-serum/armani/stateix

This commit is contained in:
Armani Ferrante 2021-01-23 07:43:48 -08:00 committed by GitHub
commit b87def6f8c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 567 additions and 218 deletions

View File

@ -8,7 +8,7 @@
Anchor is a framework for Solana's [Sealevel](https://medium.com/solana-labs/sealevel-parallel-processing-thousands-of-smart-contracts-d814b378192) runtime providing several convenient developer tools.
- Rust DSL for writing Solana programs
- Rust eDSL for writing Solana programs
- [IDL](https://en.wikipedia.org/wiki/Interface_description_language) specification
- TypeScript package for generating clients from IDL
- CLI and workspace management for developing complete applications
@ -25,7 +25,55 @@ To jump straight to examples, go [here](https://github.com/project-serum/anchor/
* **Anchor is in active development, so all APIs are subject to change.**
* **This code is unaudited. Use at your own risk.**
## Example
## Examples
Build stateful programs on Solana by defining a state struct with associated
methods. Here's a classic counter example, where only the designated `authority`
can increment the count.
```rust
#[program]
mod counter {
#[state]
pub struct Counter {
authority: Pubkey,
count: u64,
}
pub fn new(ctx: Context<Auth>) -> Result<Self> {
Ok(Self {
auth: *auth.accounts.authority.key
})
}
pub fn increment(&mut self, ctx: Context<Auth>) -> Result<()> {
if &self.authority != ctx.accounts.authority.key {
return Err(ErrorCode::Unauthorized.into());
}
self.count += 1;
Ok(())
}
}
#[derive(Accounts)]
pub struct Auth<'info> {
#[account(signer)]
authority: AccountInfo<'info>,
}
#[error]
pub enum ErrorCode {
#[msg("You are not authorized to perform this action.")]
Unauthorized,
}
```
Additionally, one can utilize the full power of Solana's parallel execution model by
keeping the program stateless and working with accounts directly. The above example
can be rewritten as follows.
```rust
use anchor::prelude::*;
@ -33,18 +81,23 @@ use anchor::prelude::*;
// Define instruction handlers.
#[program]
mod example {
mod counter {
use super::*;
pub fn initialize(ctx: Context<Initialize>, authority: Pubkey) -> ProgramResult {
let my_account = &mut ctx.accounts.my_account;
my_account.authority = authority;
let counter = &mut ctx.accounts.counter;
counter.authority = authority;
counter.count = 0;
Ok(())
}
pub fn update(ctx: Context<Update>, data: u64) -> ProgramResult {
let my_account = &mut ctx.accounts.my_account;
my_account.data = data;
pub fn increment(ctx: Context<Update>) -> ProgramResult {
let counter = &mut ctx.accounts.counter;
counter += 1;
Ok(())
}
}
@ -54,14 +107,14 @@ mod example {
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(init)]
pub my_account: ProgramAccount<'info, MyAccount>,
pub counter: ProgramAccount<'info, Counter>,
pub rent: Sysvar<'info, Rent>,
}
#[derive(Accounts)]
pub struct Update<'info> {
pub struct Increment<'info> {
#[account(mut, has_one = authority)]
pub my_account: ProgramAccount<'info, MyAccount>,
pub counter: ProgramAccount<'info, Counter>,
#[account(signer)]
pub authority: AccountInfo<'info>,
}
@ -69,12 +122,17 @@ pub struct Update<'info> {
// Define program owned accounts.
#[account]
pub struct MyAccount {
pub struct Counter {
pub authority: Pubkey,
pub data: u64,
pub count: u64,
}
```
Due to the fact that account sizes on Solana are fixed, some combination of
the above is often required. For example, one can store store global state
associated with the entire program in the `#[state]` struct and local
state assocated with each user in individual `#[account]` structs.
## Accounts attribute syntax.
There are several inert attributes (attributes that are consumed only for the

View File

@ -25,7 +25,7 @@ pub fn account(
let coder = quote! {
impl anchor_lang::AccountSerialize for #account_name {
fn try_serialize<W: std::io::Write>(&self, writer: &mut W) -> Result<(), ProgramError> {
fn try_serialize<W: std::io::Write>(&self, writer: &mut W) -> std::result::Result<(), ProgramError> {
// TODO: we shouldn't have to hash at runtime. However, rust
// is not happy when trying to include solana-sdk from
// the proc-macro crate.
@ -48,7 +48,7 @@ pub fn account(
impl anchor_lang::AccountDeserialize for #account_name {
fn try_deserialize(buf: &mut &[u8]) -> Result<Self, ProgramError> {
fn try_deserialize(buf: &mut &[u8]) -> std::result::Result<Self, ProgramError> {
let mut discriminator = [0u8; 8];
discriminator.copy_from_slice(
&anchor_lang::solana_program::hash::hash(
@ -66,7 +66,7 @@ pub fn account(
Self::try_deserialize_unchecked(buf)
}
fn try_deserialize_unchecked(buf: &mut &[u8]) -> Result<Self, ProgramError> {
fn try_deserialize_unchecked(buf: &mut &[u8]) -> std::result::Result<Self, ProgramError> {
let mut data: &[u8] = &buf[8..];
AnchorDeserialize::deserialize(&mut data)
.map_err(|_| ProgramError::InvalidAccountData)

View File

@ -15,3 +15,11 @@ pub fn state(
#item_struct
})
}
/*
impl<'a, 'b, 'c, 'info> anchor_lang::ProgramStateContext<'a, 'b, 'c, 'info> for #strct {
fn context() -> Context<'a, 'b, 'c, 'info, ProgramStateAccounts<'info>> {
// todo
}
}
*/

View File

@ -47,7 +47,7 @@ pub fn lib_rs(name: &str) -> String {
use anchor_lang::prelude::*;
#[program]
mod {} {{
pub mod {} {{
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> ProgramResult {{
Ok(())

View File

@ -10,8 +10,10 @@ use anchor_spl::token::{self, TokenAccount, Transfer};
mod calculator;
type Result<T> = std::result::Result<T, Error>;
#[program]
mod lockup {
pub mod lockup {
use super::*;
#[state]
@ -25,20 +27,45 @@ mod lockup {
impl Lockup {
pub const WHITELIST_SIZE: usize = 5;
pub fn new(authority: Pubkey) -> Result<Self, Error> {
pub fn new(ctx: Context<Auth>) -> Result<Self> {
let mut whitelist = vec![];
whitelist.resize(Self::WHITELIST_SIZE, Default::default());
Ok(Lockup {
authority,
authority: *ctx.accounts.authority.key,
whitelist,
})
}
}
pub fn set_authority(ctx: Context<SetAuthority>, new_authority: Pubkey) -> Result<(), Error> {
let lockup = &mut ctx.accounts.lockup;
lockup.authority = new_authority;
Ok(())
#[access_control(whitelist_auth(self, &ctx))]
pub fn whitelist_add(&mut self, ctx: Context<Auth>, entry: WhitelistEntry) -> Result<()> {
if self.whitelist.len() == Self::WHITELIST_SIZE {
return Err(ErrorCode::WhitelistFull.into());
}
if self.whitelist.contains(&entry) {
return Err(ErrorCode::WhitelistEntryAlreadyExists.into());
}
self.whitelist.push(entry);
Ok(())
}
#[access_control(whitelist_auth(self, &ctx))]
pub fn whitelist_delete(
&mut self,
ctx: Context<Auth>,
entry: WhitelistEntry,
) -> Result<()> {
if !self.whitelist.contains(&entry) {
return Err(ErrorCode::WhitelistEntryNotFound.into());
}
self.whitelist.retain(|e| e != &entry);
Ok(())
}
#[access_control(whitelist_auth(self, &ctx))]
pub fn set_authority(&mut self, ctx: Context<Auth>, new_authority: Pubkey) -> Result<()> {
self.authority = new_authority;
Ok(())
}
}
#[access_control(CreateVesting::accounts(&ctx, nonce))]
@ -49,7 +76,7 @@ mod lockup {
period_count: u64,
deposit_amount: u64,
nonce: u8,
) -> Result<(), Error> {
) -> Result<()> {
if end_ts <= ctx.accounts.clock.unix_timestamp {
return Err(ErrorCode::InvalidTimestamp.into());
}
@ -78,7 +105,7 @@ mod lockup {
Ok(())
}
pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<(), Error> {
pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<()> {
// Has the given amount vested?
if amount
> calculator::available_for_withdrawal(
@ -105,32 +132,12 @@ mod lockup {
Ok(())
}
#[access_control(whitelist_has_capacity(&ctx))]
pub fn whitelist_add(ctx: Context<WhitelistAdd>, entry: WhitelistEntry) -> Result<(), Error> {
if ctx.accounts.lockup.whitelist.contains(&entry) {
return Err(ErrorCode::WhitelistEntryAlreadyExists.into());
}
ctx.accounts.lockup.whitelist.push(entry);
Ok(())
}
pub fn whitelist_delete(
ctx: Context<WhitelistAdd>,
entry: WhitelistEntry,
) -> Result<(), Error> {
if !ctx.accounts.lockup.whitelist.contains(&entry) {
return Err(ErrorCode::WhitelistEntryNotFound.into());
}
ctx.accounts.lockup.whitelist.retain(|e| e != &entry);
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> {
) -> Result<()> {
let before_amount = ctx.accounts.transfer.vault.amount;
whitelist_relay_cpi(
&ctx.accounts.transfer,
@ -155,7 +162,7 @@ mod lockup {
pub fn whitelist_deposit(
ctx: Context<WhitelistDeposit>,
instruction_data: Vec<u8>,
) -> Result<(), Error> {
) -> Result<()> {
let before_amount = ctx.accounts.transfer.vault.amount;
whitelist_relay_cpi(
&ctx.accounts.transfer,
@ -180,7 +187,7 @@ mod lockup {
}
// Convenience function for UI's to calculate the withdrawalable amount.
pub fn available_for_withdrawal(ctx: Context<AvailableForWithdrawal>) -> Result<(), Error> {
pub fn available_for_withdrawal(ctx: Context<AvailableForWithdrawal>) -> Result<()> {
let available = calculator::available_for_withdrawal(
&ctx.accounts.vesting,
ctx.accounts.clock.unix_timestamp,
@ -192,9 +199,7 @@ mod lockup {
}
#[derive(Accounts)]
pub struct SetAuthority<'info> {
#[account(mut, has_one = authority)]
lockup: ProgramState<'info, Lockup>,
pub struct Auth<'info> {
#[account(signer)]
authority: AccountInfo<'info>,
}
@ -219,7 +224,7 @@ pub struct CreateVesting<'info> {
}
impl<'info> CreateVesting<'info> {
fn accounts(ctx: &Context<CreateVesting>, nonce: u8) -> Result<(), Error> {
fn accounts(ctx: &Context<CreateVesting>, nonce: u8) -> Result<()> {
let vault_authority = Pubkey::create_program_address(
&[
ctx.accounts.vesting.to_account_info().key.as_ref(),
@ -256,22 +261,6 @@ pub struct Withdraw<'info> {
clock: Sysvar<'info, Clock>,
}
#[derive(Accounts)]
pub struct WhitelistAdd<'info> {
#[account(mut, has_one = authority)]
lockup: ProgramState<'info, Lockup>,
#[account(signer)]
authority: AccountInfo<'info>,
}
#[derive(Accounts)]
pub struct WhitelistDelete<'info> {
#[account(mut, has_one = authority)]
lockup: ProgramState<'info, Lockup>,
#[account(signer)]
authority: AccountInfo<'info>,
}
#[derive(Accounts)]
pub struct WhitelistWithdraw<'info> {
transfer: WhitelistTransfer<'info>,
@ -374,6 +363,8 @@ pub enum ErrorCode {
WhitelistWithdrawLimit,
#[msg("Whitelist entry not found.")]
WhitelistEntryNotFound,
#[msg("You do not have sufficient permissions to perform this action.")]
Unauthorized,
}
impl<'a, 'b, 'c, 'info> From<&mut CreateVesting<'info>>
@ -402,19 +393,12 @@ impl<'a, 'b, 'c, 'info> From<&Withdraw<'info>> for CpiContext<'a, 'b, 'c, 'info,
}
}
fn whitelist_has_capacity(ctx: &Context<WhitelistAdd>) -> Result<(), Error> {
if ctx.accounts.lockup.whitelist.len() == lockup::Lockup::WHITELIST_SIZE {
return Err(ErrorCode::WhitelistFull.into());
}
Ok(())
}
#[access_control(is_whitelisted(transfer))]
pub fn whitelist_relay_cpi<'info>(
transfer: &WhitelistTransfer,
remaining_accounts: &[AccountInfo<'info>],
instruction_data: Vec<u8>,
) -> Result<(), Error> {
) -> Result<()> {
let mut meta_accounts = vec![
AccountMeta::new_readonly(*transfer.vesting.to_account_info().key, false),
AccountMeta::new(*transfer.vault.to_account_info().key, false),
@ -454,7 +438,7 @@ pub fn whitelist_relay_cpi<'info>(
.map_err(Into::into)
}
pub fn is_whitelisted<'info>(transfer: &WhitelistTransfer<'info>) -> Result<(), Error> {
pub fn is_whitelisted<'info>(transfer: &WhitelistTransfer<'info>) -> Result<()> {
if !transfer.lockup.whitelist.contains(&WhitelistEntry {
program_id: *transfer.whitelisted_program.key,
}) {
@ -462,3 +446,10 @@ pub fn is_whitelisted<'info>(transfer: &WhitelistTransfer<'info>) -> Result<(),
}
Ok(())
}
fn whitelist_auth(lockup: &Lockup, ctx: &Context<Auth>) -> Result<()> {
if &lockup.authority != ctx.accounts.authority.key {
return Err(ErrorCode::Unauthorized.into());
}
Ok(())
}

View File

@ -19,8 +19,10 @@ mod registry {
}
impl Registry {
pub fn new<'info>(lockup_program: Pubkey) -> Result<Self, Error> {
Ok(Registry { lockup_program })
pub fn new<'info>(ctx: Context<Ctor>) -> Result<Self, Error> {
Ok(Registry {
lockup_program: *ctx.accounts.lockup_program.key,
})
}
}
@ -598,6 +600,11 @@ pub struct BalanceSandboxAccounts<'info> {
vault_pw: CpiAccount<'info, TokenAccount>,
}
#[derive(Accounts)]
pub struct Ctor<'info> {
lockup_program: AccountInfo<'info>,
}
#[derive(Accounts)]
pub struct UpdateMember<'info> {
#[account(mut, has_one = beneficiary)]

View File

@ -28,7 +28,11 @@ describe("Lockup and Registry", () => {
});
it("Is initialized!", async () => {
await lockup.state.rpc.new(provider.wallet.publicKey);
await lockup.state.rpc.new({
accounts: {
authority: provider.wallet.publicKey,
},
});
lockupAddress = await lockup.state.address();
const lockupAccount = await lockup.state();
@ -42,30 +46,27 @@ describe("Lockup and Registry", () => {
it("Deletes the default whitelisted addresses", async () => {
const defaultEntry = { programId: new anchor.web3.PublicKey() };
await lockup.rpc.whitelistDelete(defaultEntry, {
await lockup.state.rpc.whitelistDelete(defaultEntry, {
accounts: {
authority: provider.wallet.publicKey,
lockup: lockupAddress,
},
});
});
it("Sets a new authority", async () => {
const newAuthority = new anchor.web3.Account();
await lockup.rpc.setAuthority(newAuthority.publicKey, {
await lockup.state.rpc.setAuthority(newAuthority.publicKey, {
accounts: {
authority: provider.wallet.publicKey,
lockup: lockupAddress,
},
});
let lockupAccount = await lockup.state();
assert.ok(lockupAccount.authority.equals(newAuthority.publicKey));
await lockup.rpc.setAuthority(provider.wallet.publicKey, {
await lockup.state.rpc.setAuthority(provider.wallet.publicKey, {
accounts: {
authority: newAuthority.publicKey,
lockup: lockupAddress,
},
signers: [newAuthority],
});
@ -96,20 +97,19 @@ describe("Lockup and Registry", () => {
const accounts = {
authority: provider.wallet.publicKey,
lockup: lockupAddress,
};
await lockup.rpc.whitelistAdd(e0, { accounts });
await lockup.state.rpc.whitelistAdd(e0, { accounts });
let lockupAccount = await lockup.state();
assert.ok(lockupAccount.whitelist.length === 1);
assert.deepEqual(lockupAccount.whitelist, [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 });
await lockup.state.rpc.whitelistAdd(e1, { accounts });
await lockup.state.rpc.whitelistAdd(e2, { accounts });
await lockup.state.rpc.whitelistAdd(e3, { accounts });
await lockup.state.rpc.whitelistAdd(e4, { accounts });
lockupAccount = await lockup.state();
@ -117,7 +117,7 @@ describe("Lockup and Registry", () => {
await assert.rejects(
async () => {
await lockup.rpc.whitelistAdd(e5, { accounts });
await lockup.state.rpc.whitelistAdd(e5, { accounts });
},
(err) => {
assert.equal(err.code, 108);
@ -128,10 +128,9 @@ describe("Lockup and Registry", () => {
});
it("Removes from the whitelist", async () => {
await lockup.rpc.whitelistDelete(e0, {
await lockup.state.rpc.whitelistDelete(e0, {
accounts: {
authority: provider.wallet.publicKey,
lockup: lockupAddress,
},
});
let lockupAccount = await lockup.state();
@ -284,7 +283,9 @@ describe("Lockup and Registry", () => {
});
it("Initializes registry's global state", async () => {
await registry.state.rpc.new(lockup.programId);
await registry.state.rpc.new({
accounts: { lockupProgram: lockup.programId },
});
const state = await registry.state();
assert.ok(state.lockupProgram.equals(lockup.programId));

View File

@ -4,7 +4,7 @@
use anchor_lang::prelude::*;
#[program]
mod basic_4 {
pub mod basic_4 {
use super::*;
#[state]
@ -13,9 +13,12 @@ mod basic_4 {
}
impl MyProgram {
pub fn new(data: u64) -> Result<Self, ProgramError> {
pub fn new(ctx: Context<Ctor>, data: u64) -> Result<Self, ProgramError> {
Ok(Self { data })
}
}
}
#[derive(Accounts)]
pub struct Ctor {}
// #endregion code

View File

@ -41,7 +41,7 @@ pub use crate::context::{Context, CpiContext};
pub use crate::cpi_account::CpiAccount;
pub use crate::ctor::Ctor;
pub use crate::program_account::ProgramAccount;
pub use crate::state::ProgramState;
pub use crate::state::{ProgramState, ProgramStateAccounts};
pub use crate::sysvar::Sysvar;
pub use anchor_attribute_access_control::access_control;
pub use anchor_attribute_account::account;

View File

@ -1,6 +1,6 @@
use crate::{
AccountDeserialize, AccountSerialize, Accounts, AccountsExit, CpiAccount, ToAccountInfo,
ToAccountInfos, ToAccountMetas,
self as anchor_lang, AccountDeserialize, AccountSerialize, Accounts, AccountsExit, Context,
CpiAccount, ToAccountInfo, ToAccountInfos, ToAccountMetas,
};
use solana_program::account_info::AccountInfo;
use solana_program::entrypoint::ProgramResult;
@ -9,6 +9,20 @@ use solana_program::program_error::ProgramError;
use solana_program::pubkey::Pubkey;
use std::ops::{Deref, DerefMut};
/// Trait implemented by program state structs, providing the execution context
/// for the currently executing program.
pub trait ProgramStateContext<'a, 'b, 'c, 'info> {
fn context() -> Context<'a, 'b, 'c, 'info, ProgramStateAccounts<'info>>;
}
/// Accounts given to instructions defined on a state struct, excluding the
/// state account itself.
#[derive(Accounts)]
pub struct ProgramStateAccounts<'info> {
#[account(signer)]
pub payer: AccountInfo<'info>,
}
/// Boxed container for the program state singleton.
#[derive(Clone)]
pub struct ProgramState<'info, T: AccountSerialize + AccountDeserialize + Clone> {

View File

@ -38,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, is_mut_signer: Option<bool>) -> Vec<AccountMeta> {
fn to_account_metas(&self, _is_signer: Option<bool>) -> Vec<AccountMeta> {
vec![AccountMeta::new_readonly(*self.info.key, false)]
}
}

View File

@ -141,7 +141,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> {
fn try_accounts(program_id: &anchor_lang::solana_program::pubkey::Pubkey, accounts: &mut &[anchor_lang::solana_program::account_info::AccountInfo<'info>]) -> std::result::Result<Self, anchor_lang::solana_program::program_error::ProgramError> {
// Deserialize each account.
#(#deser_fields)*

View File

@ -18,7 +18,7 @@ pub fn generate(error: Error) -> proc_macro2::TokenStream {
#error_enum
impl std::fmt::Display for #enum_name {
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
<Self as std::fmt::Debug>::fmt(self, fmt)
}
}

View File

@ -1,5 +1,5 @@
use crate::parser;
use crate::{Program, Rpc, State};
use crate::{Program, RpcArg, State};
use heck::CamelCase;
use quote::quote;
@ -42,6 +42,7 @@ pub fn generate(program: Program) -> proc_macro2::TokenStream {
#cpi
}
}
pub fn generate_dispatch(program: &Program) -> proc_macro2::TokenStream {
let ctor_state_dispatch_arm = match &program.state {
None => quote! { /* no-op */ },
@ -49,21 +50,33 @@ pub fn generate_dispatch(program: &Program) -> proc_macro2::TokenStream {
let variant_arm = generate_ctor_variant(program, state);
let ctor_args = generate_ctor_args(state);
quote! {
__private::instruction::#variant_arm => {
__private::__ctor(program_id, accounts, #(#ctor_args),*)
}
__private::instruction::#variant_arm => __private::__ctor(program_id, accounts, #(#ctor_args),*),
}
}
};
let state_dispatch_arms: Vec<proc_macro2::TokenStream> = match &program.state {
None => vec![quote! { /* no-op */}],
None => vec![],
Some(s) => s
.methods
.iter()
.map(|m| {
.map(|rpc: &crate::StateRpc| {
let rpc_arg_names: Vec<&syn::Ident> =
rpc.args.iter().map(|arg| &arg.name).collect();
let variant_arm: proc_macro2::TokenStream = generate_ix_variant(
program,
rpc.raw_method.sig.ident.to_string(),
&rpc.args,
true,
);
let rpc_name: proc_macro2::TokenStream = {
let name = &rpc.raw_method.sig.ident.to_string();
format!("__{}", name).parse().unwrap()
};
quote! {
// todo
}
__private::instruction::#variant_arm => {
__private::#rpc_name(program_id, accounts, #(#rpc_arg_names),*)
}
}
})
.collect(),
};
@ -72,7 +85,12 @@ pub fn generate_dispatch(program: &Program) -> proc_macro2::TokenStream {
.iter()
.map(|rpc| {
let rpc_arg_names: Vec<&syn::Ident> = rpc.args.iter().map(|arg| &arg.name).collect();
let variant_arm = generate_ix_variant(program, rpc);
let variant_arm = generate_ix_variant(
program,
rpc.raw_method.sig.ident.to_string(),
&rpc.args,
false,
);
let rpc_name = &rpc.raw_method.sig.ident;
quote! {
__private::instruction::#variant_arm => {
@ -85,9 +103,7 @@ pub fn generate_dispatch(program: &Program) -> proc_macro2::TokenStream {
quote! {
match ix {
#ctor_state_dispatch_arm
#(#state_dispatch_arms),*
#(#dispatch_arms),*
}
}
@ -105,14 +121,27 @@ pub fn generate_non_inlined_handlers(program: &Program) -> proc_macro2::TokenStr
let ctor_untyped_args = generate_ctor_args(state);
let name = &state.strct.ident;
let mod_name = &program.name;
let anchor_ident = &state.ctor_anchor;
quote! {
#[inline(never)]
pub fn __ctor(program_id: &Pubkey, accounts: &[AccountInfo], #(#ctor_typed_args),*) -> ProgramResult {
let mut accounts: &[AccountInfo] = accounts;
let ctor_accounts = anchor_lang::Ctor::try_accounts(program_id, &mut accounts)?;
let mut remaining_accounts: &[AccountInfo] = accounts;
let instance = #mod_name::#name::new(#(#ctor_untyped_args),*)?;
// Deserialize accounts.
let ctor_accounts = anchor_lang::Ctor::try_accounts(program_id, &mut remaining_accounts)?;
let mut ctor_user_def_accounts = #anchor_ident::try_accounts(program_id, &mut remaining_accounts)?;
// Invoke the ctor.
let instance = #mod_name::#name::new(
anchor_lang::Context::new(
program_id,
&mut ctor_user_def_accounts,
remaining_accounts,
),
#(#ctor_untyped_args),*
)?;
// Create the solana account for the ctor data.
let from = ctor_accounts.from.key;
let (base, nonce) = Pubkey::find_program_address(&[], ctor_accounts.program.key);
let seed = anchor_lang::ProgramState::<#name>::seed();
@ -122,8 +151,6 @@ pub fn generate_non_inlined_handlers(program: &Program) -> proc_macro2::TokenStr
let space = 8 + instance.try_to_vec().map_err(|_| ProgramError::Custom(1))?.len();
let lamports = ctor_accounts.rent.minimum_balance(space);
let seeds = &[&[nonce][..]];
// Create the new program owned account (from within the program).
let ix = anchor_lang::solana_program::system_instruction::create_account_with_seed(
from,
&to,
@ -145,6 +172,7 @@ pub fn generate_non_inlined_handlers(program: &Program) -> proc_macro2::TokenStr
)?;
// Serialize the state and save it to storage.
ctor_user_def_accounts.exit(program_id)?;
let mut data = ctor_accounts.to.try_borrow_mut_data()?;
let dst: &mut [u8] = &mut data;
let mut cursor = std::io::Cursor::new(dst);
@ -155,6 +183,71 @@ pub fn generate_non_inlined_handlers(program: &Program) -> proc_macro2::TokenStr
}
}
};
let non_inlined_state_handlers: Vec<proc_macro2::TokenStream> = match &program.state {
None => vec![],
Some(state) => state
.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!("__{}", &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;
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(())
}
}
})
.collect(),
};
let non_inlined_handlers: Vec<proc_macro2::TokenStream> = program
.rpcs
.iter()
@ -185,7 +278,7 @@ pub fn generate_non_inlined_handlers(program: &Program) -> proc_macro2::TokenStr
quote! {
#non_inlined_ctor
#(#non_inlined_state_handlers)*
#(#non_inlined_handlers)*
}
}
@ -213,7 +306,7 @@ pub fn generate_ctor_typed_variant_with_comma(program: &Program) -> proc_macro2:
let ctor_args = generate_ctor_typed_args(state);
if ctor_args.len() == 0 {
quote! {
__Ctor
__Ctor,
}
} else {
quote! {
@ -232,9 +325,16 @@ fn generate_ctor_typed_args(state: &State) -> Vec<syn::PatType> {
.sig
.inputs
.iter()
.map(|arg: &syn::FnArg| match arg {
syn::FnArg::Typed(pat_ty) => pat_ty.clone(),
_ => panic!(""),
.filter_map(|arg: &syn::FnArg| match arg {
syn::FnArg::Typed(pat_ty) => {
let mut arg_str = parser::tts_to_string(&pat_ty.ty);
arg_str.retain(|c| !c.is_whitespace());
if arg_str.starts_with("Context<") {
return None;
}
Some(pat_ty.clone())
}
_ => panic!("Invalid syntaxe,"),
})
.collect()
}
@ -245,21 +345,38 @@ fn generate_ctor_args(state: &State) -> Vec<Box<syn::Pat>> {
.sig
.inputs
.iter()
.map(|arg: &syn::FnArg| match arg {
syn::FnArg::Typed(pat_ty) => pat_ty.pat.clone(),
.filter_map(|arg: &syn::FnArg| match arg {
syn::FnArg::Typed(pat_ty) => {
let mut arg_str = parser::tts_to_string(&pat_ty.ty);
arg_str.retain(|c| !c.is_whitespace());
if arg_str.starts_with("Context<") {
return None;
}
Some(pat_ty.pat.clone())
}
_ => panic!(""),
})
.collect()
}
pub fn generate_ix_variant(program: &Program, rpc: &Rpc) -> proc_macro2::TokenStream {
pub fn generate_ix_variant(
program: &Program,
name: String,
args: &[RpcArg],
underscore: bool,
) -> proc_macro2::TokenStream {
let enum_name = instruction_enum_name(program);
let rpc_arg_names: Vec<&syn::Ident> = rpc.args.iter().map(|arg| &arg.name).collect();
let rpc_name_camel = proc_macro2::Ident::new(
&rpc.raw_method.sig.ident.to_string().to_camel_case(),
rpc.raw_method.sig.ident.span(),
);
if rpc.args.len() == 0 {
let rpc_arg_names: Vec<&syn::Ident> = args.iter().map(|arg| &arg.name).collect();
let rpc_name_camel: proc_macro2::TokenStream = {
let n = name.to_camel_case();
if underscore {
format!("__{}", n).parse().unwrap()
} else {
n.parse().unwrap()
}
};
if args.len() == 0 {
quote! {
#enum_name::#rpc_name_camel
}
@ -282,6 +399,36 @@ pub fn generate_methods(program: &Program) -> proc_macro2::TokenStream {
pub fn generate_instruction(program: &Program) -> proc_macro2::TokenStream {
let enum_name = instruction_enum_name(program);
let ctor_variant = generate_ctor_typed_variant_with_comma(program);
let state_method_variants: Vec<proc_macro2::TokenStream> = match &program.state {
None => vec![],
Some(state) => state
.methods
.iter()
.map(|method| {
let rpc_name_camel: proc_macro2::TokenStream = {
let name = format!(
"__{}",
&method.raw_method.sig.ident.to_string().to_camel_case(),
);
name.parse().unwrap()
};
let raw_args: Vec<&syn::PatType> =
method.args.iter().map(|arg| &arg.raw_arg).collect();
// If no args, output a "unit" variant instead of a struct variant.
if method.args.len() == 0 {
quote! {
#rpc_name_camel,
}
} else {
quote! {
#rpc_name_camel {
#(#raw_args),*
},
}
}
})
.collect(),
};
let variants: Vec<proc_macro2::TokenStream> = program
.rpcs
.iter()
@ -312,6 +459,7 @@ pub fn generate_instruction(program: &Program) -> proc_macro2::TokenStream {
#[derive(AnchorSerialize, AnchorDeserialize)]
pub enum #enum_name {
#ctor_variant
#(#state_method_variants)*
#(#variants),*
}
}
@ -332,7 +480,12 @@ fn generate_cpi(program: &Program) -> proc_macro2::TokenStream {
.map(|rpc| {
let accounts_ident = &rpc.anchor_ident;
let cpi_method = {
let ix_variant = generate_ix_variant(program, rpc);
let ix_variant = generate_ix_variant(
program,
rpc.raw_method.sig.ident.to_string(),
&rpc.args,
false,
);
let method_name = &rpc.ident;
let args: Vec<&syn::PatType> = rpc.args.iter().map(|arg| &arg.raw_arg).collect();
quote! {

View File

@ -24,13 +24,7 @@ pub struct IdlState {
pub methods: Vec<IdlStateMethod>,
}
// IdlStateMethods are similar to instructions, except they only allow
// for a single account, the state account.
#[derive(Debug, Serialize, Deserialize)]
pub struct IdlStateMethod {
pub name: String,
pub args: Vec<IdlField>,
}
pub type IdlStateMethod = IdlInstruction;
#[derive(Debug, Serialize, Deserialize)]
pub struct IdlInstruction {

View File

@ -27,8 +27,17 @@ pub struct State {
pub name: String,
pub strct: syn::ItemStruct,
pub impl_block: syn::ItemImpl,
pub methods: Vec<Rpc>,
pub methods: Vec<StateRpc>,
pub ctor: syn::ImplItemMethod,
pub ctor_anchor: syn::Ident, // TODO: consolidate this with ctor above.
}
#[derive(Debug)]
pub struct StateRpc {
pub raw_method: syn::ImplItemMethod,
pub ident: syn::Ident,
pub args: Vec<RpcArg>,
pub anchor_ident: syn::Ident,
}
#[derive(Debug)]

View File

@ -1,6 +1,6 @@
use crate::idl::*;
use crate::parser::{self, accounts, error, program};
use crate::{AccountsStruct, Rpc};
use crate::{AccountsStruct, StateRpc};
use anyhow::Result;
use heck::MixedCase;
use quote::ToTokens;
@ -22,12 +22,23 @@ pub fn parse(filename: impl AsRef<Path>) -> Result<Idl> {
let p = program::parse(parse_program_mod(&f));
let accs = parse_accounts(&f);
let acc_names = {
let mut acc_names = HashSet::new();
for accs_strct in accs.values() {
for a in accs_strct.account_tys(&accs)? {
acc_names.insert(a);
}
}
acc_names
};
let state = p.state.map(|state| {
let mut methods = state
.methods
.iter()
.map(|method: &Rpc| {
let name = method.ident.to_string();
.map(|method: &StateRpc| {
let name = method.ident.to_string().to_mixed_case();
let args = method
.args
.iter()
@ -41,7 +52,13 @@ pub fn parse(filename: impl AsRef<Path>) -> Result<Idl> {
}
})
.collect::<Vec<_>>();
IdlStateMethod { name, args }
let accounts_strct = accs.get(&method.anchor_ident.to_string()).unwrap();
let accounts = accounts_strct.idl_accounts(&accs);
IdlStateMethod {
name,
args,
accounts,
}
})
.collect::<Vec<_>>();
let ctor = {
@ -51,6 +68,18 @@ pub fn parse(filename: impl AsRef<Path>) -> Result<Idl> {
.sig
.inputs
.iter()
.filter_map(|arg: &syn::FnArg| match arg {
syn::FnArg::Typed(pat_ty) => {
// TODO: this filtering should be donein the parser.
let mut arg_str = parser::tts_to_string(&pat_ty.ty);
arg_str.retain(|c| !c.is_whitespace());
if arg_str.starts_with("Context<") {
return None;
}
Some(arg)
}
_ => None,
})
.map(|arg: &syn::FnArg| match arg {
syn::FnArg::Typed(arg_typed) => {
let mut tts = proc_macro2::TokenStream::new();
@ -64,7 +93,13 @@ pub fn parse(filename: impl AsRef<Path>) -> Result<Idl> {
_ => panic!("Invalid syntax"),
})
.collect();
IdlStateMethod { name, args }
let accounts_strct = accs.get(&state.ctor_anchor.to_string()).unwrap();
let accounts = accounts_strct.idl_accounts(&accs);
IdlStateMethod {
name,
args,
accounts,
}
};
methods.insert(0, ctor);
@ -106,20 +141,6 @@ pub fn parse(filename: impl AsRef<Path>) -> Result<Idl> {
.collect::<Vec<IdlErrorCode>>()
});
let accs = parse_accounts(&f);
let acc_names = {
let mut acc_names = HashSet::new();
for accs_strct in accs.values() {
for a in accs_strct.account_tys(&accs)? {
acc_names.insert(a);
}
}
acc_names
};
let instructions = p
.rpcs
.iter()

View File

@ -1,5 +1,5 @@
use crate::parser;
use crate::{Program, Rpc, RpcArg, State};
use crate::{Program, Rpc, RpcArg, State, StateRpc};
pub fn parse(program_mod: syn::ItemMod) -> Program {
let mod_ident = &program_mod.ident;
@ -52,13 +52,19 @@ pub fn parse(program_mod: syn::ItemMod) -> Program {
strct.attrs = vec![];
let impl_block = impl_block.expect("Must exist if struct exists").clone();
let ctor = impl_block
let (ctor, ctor_anchor) = impl_block
.items
.iter()
.filter_map(|item: &syn::ImplItem| match item {
syn::ImplItem::Method(m) => {
if m.sig.ident.to_string() == "new" {
Some(m)
let ctx_arg = m.sig.inputs.first().unwrap(); // todo: unwrap.
match ctx_arg {
syn::FnArg::Receiver(_) => panic!("invalid syntax"),
syn::FnArg::Typed(arg) => {
Some((m.clone(), extract_ident(&arg).clone()))
}
}
} else {
None
}
@ -68,12 +74,58 @@ pub fn parse(program_mod: syn::ItemMod) -> Program {
.next()
.expect("Must exist if struct exists")
.clone();
let methods: Vec<StateRpc> = impl_block
.items
.iter()
.filter_map(|item: &syn::ImplItem| match item {
syn::ImplItem::Method(m) => match m.sig.inputs.first() {
None => None,
Some(arg) => match arg {
syn::FnArg::Typed(_) => None,
syn::FnArg::Receiver(_) => {
let mut args = m
.sig
.inputs
.iter()
.filter_map(|arg| match arg {
syn::FnArg::Receiver(_) => 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,
})
}
},
},
_ => None,
})
.collect();
State {
name: strct.ident.to_string(),
strct: strct.clone(),
impl_block,
ctor,
methods: vec![], // todo
ctor_anchor,
methods,
}
})
};

View File

@ -1,6 +1,6 @@
{
"name": "@project-serum/anchor",
"version": "0.0.0-alpha.6",
"version": "0.0.0-alpha.7",
"description": "Anchor client",
"main": "dist/cjs/index.js",
"module": "dist/esm/index.js",

View File

@ -21,10 +21,7 @@ export type IdlState = {
methods: IdlStateMethod[];
};
export type IdlStateMethod = {
name: string;
args: IdlField[];
};
export type IdlStateMethod = IdlInstruction;
export type IdlAccountItem = IdlAccount | IdlAccounts;

View File

@ -131,7 +131,7 @@ export class RpcFactory {
const rpcs: Rpcs = {};
const ixFns: Ixs = {};
const txFns: Txs = {};
const state = RpcFactory.buildState(idl, coder, programId);
const state = RpcFactory.buildState(idl, coder, programId, idlErrors);
idl.instructions.forEach((idlIx) => {
// Function to create a raw `TransactionInstruction`.
@ -157,7 +157,8 @@ export class RpcFactory {
private static buildState(
idl: Idl,
coder: Coder,
programId: PublicKey
programId: PublicKey,
idlErrors: Map<number, string>
): State | undefined {
if (idl.state === undefined) {
return undefined;
@ -172,52 +173,92 @@ export class RpcFactory {
const rpc: Rpcs = {};
idl.state.methods.forEach((m: IdlStateMethod) => {
if (m.name !== "new") {
throw new Error("State struct mutatation not yet implemented.");
}
// Ctor `new` method.
rpc[m.name] = async (...args: any[]): Promise<TransactionSignature> => {
const tx = new Transaction();
const [programSigner, _nonce] = await PublicKey.findProgramAddress(
[],
programId
);
const ix = new TransactionInstruction({
keys: [
{
pubkey: getProvider().wallet.publicKey,
isWritable: false,
isSigner: true,
},
if (m.name === "new") {
// Ctor `new` method.
rpc[m.name] = async (...args: any[]): Promise<TransactionSignature> => {
const [ixArgs, ctx] = splitArgsAndCtx(m, [...args]);
const tx = new Transaction();
const [programSigner, _nonce] = await PublicKey.findProgramAddress(
[],
programId
);
const ix = new TransactionInstruction({
keys: [
{
pubkey: getProvider().wallet.publicKey,
isWritable: false,
isSigner: true,
},
{ pubkey: await address(), isWritable: true, isSigner: false },
{ pubkey: programSigner, isWritable: false, isSigner: false },
{
pubkey: SystemProgram.programId,
isWritable: false,
isSigner: false,
},
{ pubkey: programId, isWritable: false, isSigner: false },
{
pubkey: SYSVAR_RENT_PUBKEY,
isWritable: false,
isSigner: false,
},
].concat(RpcFactory.accountsArray(ctx.accounts, m.accounts)),
programId,
data: coder.instruction.encode(toInstruction(m, ...ixArgs)),
});
tx.add(ix);
const provider = getProvider();
if (provider === null) {
throw new Error("Provider not found");
}
try {
const txSig = await provider.send(tx, ctx.signers, ctx.options);
return txSig;
} catch (err) {
let translatedErr = translateError(idlErrors, err);
if (translatedErr === null) {
throw err;
}
throw translatedErr;
}
};
} else {
rpc[m.name] = async (...args: any[]): Promise<TransactionSignature> => {
const [ixArgs, ctx] = splitArgsAndCtx(m, [...args]);
validateAccounts(m.accounts, ctx.accounts);
const tx = new Transaction();
const keys = [
{ pubkey: await address(), isWritable: true, isSigner: false },
{ pubkey: programSigner, isWritable: false, isSigner: false },
{
pubkey: SystemProgram.programId,
isWritable: false,
isSigner: false,
},
].concat(RpcFactory.accountsArray(ctx.accounts, m.accounts));
{ pubkey: programId, isWritable: false, isSigner: false },
{ pubkey: SYSVAR_RENT_PUBKEY, isWritable: false, isSigner: false },
],
programId,
data: coder.instruction.encode(toInstruction(m, ...args)),
});
tx.add(
new TransactionInstruction({
keys,
programId,
data: coder.instruction.encode(toInstruction(m, ...ixArgs)),
})
);
tx.add(ix);
const provider = getProvider();
if (provider === null) {
throw new Error("Provider not found");
}
try {
const txSig = await provider.send(tx);
return txSig;
} catch (err) {
// TODO: translate error.
throw err;
}
};
const provider = getProvider();
if (provider === null) {
throw new Error("Provider not found");
}
try {
const txSig = await provider.send(tx, ctx.signers, ctx.options);
return txSig;
} catch (err) {
let translatedErr = translateError(idlErrors, err);
if (translatedErr === null) {
throw err;
}
throw translatedErr;
}
};
}
});
// Fetches the state object from the blockchain.