Explorar o código

State struct cpi code generation (#43)

Armani Ferrante %!s(int64=4) %!d(string=hai) anos
pai
achega
c7c322a306

+ 4 - 0
CHANGELOG.md

@@ -11,6 +11,10 @@ incremented for features.
 
 ## [Unreleased]
 
+## Features
+
+* lang: CPI clients for program state instructions ([#43](https://github.com/project-serum/anchor/pull/43)).
+
 ## [0.4.2] - 2021-04-10
 
 ## Features

+ 1 - 0
examples/misc/programs/misc/Cargo.toml

@@ -16,3 +16,4 @@ default = []
 
 [dependencies]
 anchor-lang = { path = "../../../../lang" }
+misc2 = { path = "../misc2", features = ["cpi"] }

+ 22 - 1
examples/misc/programs/misc/src/lib.rs

@@ -2,6 +2,8 @@
 //! It's not too instructive/coherent by itself, so please see other examples.
 
 use anchor_lang::prelude::*;
+use misc2::misc2::MyState;
+use misc2::Auth;
 
 #[program]
 pub mod misc {
@@ -26,9 +28,18 @@ pub mod misc {
         Ok(())
     }
 
-    pub fn test_executable(ctx: Context<TestExecutable>) -> ProgramResult {
+    pub fn test_executable(_ctx: Context<TestExecutable>) -> ProgramResult {
         Ok(())
     }
+
+    pub fn test_state_cpi(ctx: Context<TestStateCpi>, data: u64) -> ProgramResult {
+        let cpi_program = ctx.accounts.misc2_program.clone();
+        let cpi_accounts = Auth {
+            authority: ctx.accounts.authority.clone(),
+        };
+        let ctx = ctx.accounts.cpi_state.context(cpi_program, cpi_accounts);
+        misc2::cpi::state::set_data(ctx, data)
+    }
 }
 
 #[derive(Accounts)]
@@ -47,6 +58,16 @@ pub struct TestExecutable<'info> {
     program: AccountInfo<'info>,
 }
 
+#[derive(Accounts)]
+pub struct TestStateCpi<'info> {
+    #[account(signer)]
+    authority: AccountInfo<'info>,
+    #[account(mut, state = misc2_program)]
+    cpi_state: CpiState<'info, MyState>,
+    #[account(executable)]
+    misc2_program: AccountInfo<'info>,
+}
+
 #[account]
 pub struct Data {
     udata: u128,

+ 18 - 0
examples/misc/programs/misc2/Cargo.toml

@@ -0,0 +1,18 @@
+[package]
+name = "misc2"
+version = "0.1.0"
+description = "Created with Anchor"
+edition = "2018"
+
+[lib]
+crate-type = ["cdylib", "lib"]
+name = "misc2"
+
+[features]
+no-entrypoint = []
+no-idl = []
+cpi = ["no-entrypoint"]
+default = []
+
+[dependencies]
+anchor-lang = { path = "../../../../lang" }

+ 2 - 0
examples/misc/programs/misc2/Xargo.toml

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

+ 35 - 0
examples/misc/programs/misc2/src/lib.rs

@@ -0,0 +1,35 @@
+use anchor_lang::prelude::*;
+
+#[program]
+pub mod misc2 {
+    use super::*;
+
+    #[state]
+    pub struct MyState {
+        pub data: u64,
+        pub auth: Pubkey,
+    }
+
+    impl MyState {
+        pub fn new(ctx: Context<Auth>) -> Result<Self, ProgramError> {
+            Ok(Self {
+                data: 0,
+                auth: *ctx.accounts.authority.key,
+            })
+        }
+
+        pub fn set_data(&mut self, ctx: Context<Auth>, data: u64) -> Result<(), ProgramError> {
+            if self.auth != *ctx.accounts.authority.key {
+                return Err(ProgramError::Custom(1234)); // Arbitrary error code.
+            }
+            self.data = data;
+            Ok(())
+        }
+    }
+}
+
+#[derive(Accounts)]
+pub struct Auth<'info> {
+    #[account(signer)]
+    pub authority: AccountInfo<'info>,
+}

+ 24 - 0
examples/misc/tests/misc.js

@@ -6,6 +6,7 @@ describe("misc", () => {
   // Configure the client to use the local cluster.
   anchor.setProvider(anchor.Provider.env());
   const program = anchor.workspace.Misc;
+  const misc2Program = anchor.workspace.Misc2;
 
   it("Can allocate extra space for a state constructor", async () => {
     const tx = await program.state.rpc.new();
@@ -63,4 +64,27 @@ describe("misc", () => {
       }
     );
   });
+
+  it("Can CPI to state instructions", async () => {
+    const oldData = new anchor.BN(0);
+    await misc2Program.state.rpc.new({
+      accounts: {
+        authority: program.provider.wallet.publicKey,
+      },
+    });
+    let stateAccount = await misc2Program.state();
+    assert.ok(stateAccount.data.eq(oldData));
+    assert.ok(stateAccount.auth.equals(program.provider.wallet.publicKey));
+    const newData = new anchor.BN(2134);
+    await program.rpc.testStateCpi(newData, {
+      accounts: {
+        authority: program.provider.wallet.publicKey,
+        cpiState: await misc2Program.state.address(),
+        misc2Program: misc2Program.programId,
+      },
+    });
+    stateAccount = await misc2Program.state();
+    assert.ok(stateAccount.data.eq(newData));
+    assert.ok(stateAccount.auth.equals(program.provider.wallet.publicKey));
+  });
 });

+ 1 - 0
lang/derive/accounts/src/lib.rs

@@ -47,6 +47,7 @@ use syn::parse_macro_input;
 /// | `#[account("<literal>")]` | On any type deriving `Accounts` | Executes the given code literal as a constraint. The literal should evaluate to a boolean. |
 /// | `#[account(rent_exempt = <skip>)]` | On `AccountInfo` or `ProgramAccount` structs | Optional attribute to skip the rent exemption check. By default, all accounts marked with `#[account(init)]` will be rent exempt, and so this should rarely (if ever) be used. Similarly, omitting `= skip` will mark the account rent exempt. |
 /// | `#[account(executable)]` | On `AccountInfo` structs | Checks the given account is an executable program. |
+/// | `#[account(state = <target>)]` | On `CpiState` structs | Checks the given state is the canonical state account for the target program. |
 #[proc_macro_derive(Accounts, attributes(account))]
 pub fn derive_anchor_deserialize(item: TokenStream) -> TokenStream {
     let strct = parse_macro_input!(item as syn::ItemStruct);

+ 76 - 1
lang/src/context.rs

@@ -1,5 +1,6 @@
 use crate::{Accounts, ToAccountInfos, ToAccountMetas};
 use solana_program::account_info::AccountInfo;
+use solana_program::instruction::AccountMeta;
 use solana_program::pubkey::Pubkey;
 
 /// Provides non-argument inputs to the program.
@@ -27,7 +28,7 @@ impl<'a, 'b, 'c, 'info, T: Accounts<'info>> Context<'a, 'b, 'c, 'info, T> {
     }
 }
 
-/// Context speciying non-argument inputs for cross-program-invocations.
+/// Context specifying non-argument inputs for cross-program-invocations.
 pub struct CpiContext<'a, 'b, 'c, 'info, T>
 where
     T: ToAccountMetas + ToAccountInfos<'info>,
@@ -66,3 +67,77 @@ where
         self
     }
 }
+
+/// Context specifying non-argument inputs for cross-program-invocations
+/// targeted at program state instructions.
+pub struct StateCpiContext<'a, 'b, 'c, 'info, T: Accounts<'info>> {
+    state: AccountInfo<'info>,
+    cpi_ctx: CpiContext<'a, 'b, 'c, 'info, T>,
+}
+
+impl<'a, 'b, 'c, 'info, T: Accounts<'info>> StateCpiContext<'a, 'b, 'c, 'info, T> {
+    pub fn new(program: AccountInfo<'info>, state: AccountInfo<'info>, accounts: T) -> Self {
+        Self {
+            state,
+            cpi_ctx: CpiContext {
+                accounts,
+                program,
+                signer_seeds: &[],
+            },
+        }
+    }
+
+    pub fn new_with_signer(
+        program: AccountInfo<'info>,
+        state: AccountInfo<'info>,
+        accounts: T,
+        signer_seeds: &'a [&'b [&'c [u8]]],
+    ) -> Self {
+        Self {
+            state,
+            cpi_ctx: CpiContext {
+                accounts,
+                program,
+                signer_seeds,
+            },
+        }
+    }
+
+    pub fn with_signer(mut self, signer_seeds: &'a [&'b [&'c [u8]]]) -> Self {
+        self.cpi_ctx = self.cpi_ctx.with_signer(signer_seeds);
+        self
+    }
+
+    pub fn program(&self) -> &AccountInfo<'info> {
+        &self.cpi_ctx.program
+    }
+
+    pub fn signer_seeds(&self) -> &[&[&[u8]]] {
+        self.cpi_ctx.signer_seeds
+    }
+}
+
+impl<'a, 'b, 'c, 'info, T: Accounts<'info>> ToAccountMetas
+    for StateCpiContext<'a, 'b, 'c, 'info, T>
+{
+    fn to_account_metas(&self, is_signer: Option<bool>) -> Vec<AccountMeta> {
+        // State account is always first for state instructions.
+        let mut metas = vec![match self.state.is_writable {
+            false => AccountMeta::new_readonly(*self.state.key, false),
+            true => AccountMeta::new(*self.state.key, false),
+        }];
+        metas.append(&mut self.cpi_ctx.accounts.to_account_metas(is_signer));
+        metas
+    }
+}
+
+impl<'a, 'b, 'c, 'info, T: Accounts<'info>> ToAccountInfos<'info>
+    for StateCpiContext<'a, 'b, 'c, 'info, T>
+{
+    fn to_account_infos(&self) -> Vec<AccountInfo<'info>> {
+        let mut infos = self.cpi_ctx.accounts.to_account_infos();
+        infos.push(self.state.clone());
+        infos.push(self.cpi_ctx.program.clone());
+        infos
+    }
+}

+ 1 - 0
lang/src/cpi_account.rs

@@ -55,6 +55,7 @@ where
         }
         let account = &accounts[0];
         *accounts = &accounts[1..];
+        // No owner check is done here.
         let pa = CpiAccount::try_from(account)?;
         Ok(pa)
     }

+ 132 - 0
lang/src/cpi_state.rs

@@ -0,0 +1,132 @@
+use crate::{
+    AccountDeserialize, AccountSerialize, Accounts, AccountsExit, ProgramState, StateCpiContext,
+    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};
+
+/// Boxed container for the program state singleton, used when the state
+/// is for a program not currently executing.
+#[derive(Clone)]
+pub struct CpiState<'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<'info, T: AccountSerialize + AccountDeserialize + Clone> CpiState<'info, T> {
+    pub fn new(i: AccountInfo<'info>, account: T) -> CpiState<'info, T> {
+        Self {
+            inner: Box::new(Inner { info: i, account }),
+        }
+    }
+
+    /// Deserializes the given `info` into a `CpiState`.
+    #[inline(never)]
+    pub fn try_from(info: &AccountInfo<'info>) -> Result<CpiState<'info, T>, ProgramError> {
+        let mut data: &[u8] = &info.try_borrow_data()?;
+        Ok(CpiState::new(info.clone(), T::try_deserialize(&mut data)?))
+    }
+
+    fn seed() -> &'static str {
+        ProgramState::<T>::seed()
+    }
+
+    pub fn address(program_id: &Pubkey) -> Pubkey {
+        let (base, _nonce) = Pubkey::find_program_address(&[], program_id);
+        let seed = Self::seed();
+        let owner = program_id;
+        Pubkey::create_with_seed(&base, seed, owner).unwrap()
+    }
+
+    /// Convenience api for creating a `StateCpiContext`.
+    pub fn context<'a, 'b, 'c, A: Accounts<'info>>(
+        &self,
+        program: AccountInfo<'info>,
+        accounts: A,
+    ) -> StateCpiContext<'a, 'b, 'c, 'info, A> {
+        StateCpiContext::new(program, self.inner.info.clone(), accounts)
+    }
+}
+
+impl<'info, T> Accounts<'info> for CpiState<'info, T>
+where
+    T: AccountSerialize + AccountDeserialize + Clone,
+{
+    #[inline(never)]
+    fn try_accounts(
+        _program_id: &Pubkey,
+        accounts: &mut &[AccountInfo<'info>],
+    ) -> Result<Self, ProgramError> {
+        if accounts.is_empty() {
+            return Err(ProgramError::NotEnoughAccountKeys);
+        }
+        let account = &accounts[0];
+        *accounts = &accounts[1..];
+
+        // No owner or address check is done here. One must use the
+        // #[account(state = <account-name>)] constraint.
+
+        CpiState::try_from(account)
+    }
+}
+
+impl<'info, T: AccountSerialize + AccountDeserialize + Clone> ToAccountMetas
+    for CpiState<'info, T>
+{
+    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]
+    }
+}
+
+impl<'info, T: AccountSerialize + AccountDeserialize + Clone> ToAccountInfos<'info>
+    for CpiState<'info, T>
+{
+    fn to_account_infos(&self) -> Vec<AccountInfo<'info>> {
+        vec![self.inner.info.clone()]
+    }
+}
+
+impl<'info, T: AccountSerialize + AccountDeserialize + Clone> ToAccountInfo<'info>
+    for CpiState<'info, T>
+{
+    fn to_account_info(&self) -> AccountInfo<'info> {
+        self.inner.info.clone()
+    }
+}
+
+impl<'info, T: AccountSerialize + AccountDeserialize + Clone> Deref for CpiState<'info, T> {
+    type Target = T;
+
+    fn deref(&self) -> &Self::Target {
+        &(*self.inner).account
+    }
+}
+
+impl<'info, T: AccountSerialize + AccountDeserialize + Clone> DerefMut for CpiState<'info, T> {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        &mut DerefMut::deref_mut(&mut self.inner).account
+    }
+}
+
+impl<'info, T: AccountSerialize + AccountDeserialize + Clone> AccountsExit<'info>
+    for CpiState<'info, T>
+{
+    fn exit(&self, _program_id: &Pubkey) -> ProgramResult {
+        // no-op
+        Ok(())
+    }
+}

+ 5 - 3
lang/src/lib.rs

@@ -33,6 +33,7 @@ mod account_info;
 mod boxed;
 mod context;
 mod cpi_account;
+mod cpi_state;
 mod ctor;
 mod error;
 #[doc(hidden)]
@@ -51,8 +52,9 @@ pub mod __private {
     pub use base64;
 }
 
-pub use crate::context::{Context, CpiContext};
+pub use crate::context::{Context, CpiContext, StateCpiContext};
 pub use crate::cpi_account::CpiAccount;
+pub use crate::cpi_state::CpiState;
 pub use crate::program_account::ProgramAccount;
 pub use crate::state::ProgramState;
 pub use crate::sysvar::Sysvar;
@@ -209,8 +211,8 @@ pub mod prelude {
     pub use super::{
         access_control, account, emit, error, event, interface, program, state, AccountDeserialize,
         AccountSerialize, Accounts, AccountsExit, AccountsInit, AnchorDeserialize, AnchorSerialize,
-        Context, CpiAccount, CpiContext, ProgramAccount, ProgramState, Sysvar, ToAccountInfo,
-        ToAccountInfos, ToAccountMetas,
+        Context, CpiAccount, CpiContext, CpiState, ProgramAccount, ProgramState, StateCpiContext,
+        Sysvar, ToAccountInfo, ToAccountInfos, ToAccountMetas,
     };
 
     pub use borsh;

+ 18 - 1
lang/syn/src/codegen/accounts.rs

@@ -1,7 +1,7 @@
 use crate::{
     AccountField, AccountsStruct, CompositeField, Constraint, ConstraintBelongsTo,
     ConstraintExecutable, ConstraintLiteral, ConstraintOwner, ConstraintRentExempt,
-    ConstraintSeeds, ConstraintSigner, Field, Ty,
+    ConstraintSeeds, ConstraintSigner, ConstraintState, Field, Ty,
 };
 use heck::SnakeCase;
 use quote::quote;
@@ -315,6 +315,7 @@ pub fn generate_field_constraint(f: &Field, c: &Constraint) -> proc_macro2::Toke
         Constraint::RentExempt(c) => generate_constraint_rent_exempt(f, c),
         Constraint::Seeds(c) => generate_constraint_seeds(f, c),
         Constraint::Executable(c) => generate_constraint_executable(f, c),
+        Constraint::State(c) => generate_constraint_state(f, c),
     }
 }
 
@@ -433,3 +434,19 @@ pub fn generate_constraint_executable(
         }
     }
 }
+
+pub fn generate_constraint_state(f: &Field, c: &ConstraintState) -> proc_macro2::TokenStream {
+    let program_target = c.program_target.clone();
+    let ident = &f.ident;
+    let account_ty = match &f.ty {
+        Ty::CpiState(ty) => &ty.account_ident,
+        _ => panic!("Invalid syntax"),
+    };
+    quote! {
+        // Checks the given state account is the canonical state account for
+        // the target program.
+        if #ident.to_account_info().key != &anchor_lang::CpiState::<#account_ty>::address(#program_target.to_account_info().key) {
+            return Err(ProgramError::Custom(1)); // todo: proper error.
+        }
+    }
+}

+ 61 - 3
lang/syn/src/codegen/program.rs

@@ -1,5 +1,5 @@
 use crate::parser;
-use crate::{IxArg, Program, State};
+use crate::{IxArg, Program, State, StateIx};
 use heck::{CamelCase, SnakeCase};
 use quote::quote;
 
@@ -1096,7 +1096,59 @@ fn generate_accounts(program: &Program) -> proc_macro2::TokenStream {
 }
 
 fn generate_cpi(program: &Program) -> proc_macro2::TokenStream {
-    let cpi_methods: Vec<proc_macro2::TokenStream> = program
+    // Generate cpi methods for the state struct.
+    // The Ctor is not exposed via CPI, since it is a one time use function.
+    let state_cpi_methods: Vec<proc_macro2::TokenStream> = program
+        .state
+        .as_ref()
+        .map(|state| {
+            state
+                .impl_block_and_methods
+                .as_ref()
+                .map(|(_, methods)| {
+                    methods
+                        .iter()
+                        .map(|method: &StateIx| {
+                            let accounts_ident = &method.anchor_ident;
+                            let ix_variant = generate_ix_variant(
+                                method.raw_method.sig.ident.to_string(),
+                                &method.args,
+                            );
+                            let method_name = &method.ident;
+                            let args: Vec<&syn::PatType> =
+                                method.args.iter().map(|arg| &arg.raw_arg).collect();
+
+                            quote! {
+                                pub fn #method_name<'a, 'b, 'c, 'info>(
+                                    ctx: StateCpiContext<'a, 'b, 'c, 'info, #accounts_ident<'info>>,
+                                    #(#args),*
+                                ) -> ProgramResult {
+                                    let ix = {
+                                        let ix = instruction::state::#ix_variant;
+                                        let data = anchor_lang::InstructionData::data(&ix);
+                                        let accounts = ctx.to_account_metas(None);
+                                        anchor_lang::solana_program::instruction::Instruction {
+                                            program_id: *ctx.program().key,
+                                            accounts,
+                                            data,
+                                        }
+                                    };
+                                    let mut acc_infos = ctx.to_account_infos();
+                                    anchor_lang::solana_program::program::invoke_signed(
+                                        &ix,
+                                        &acc_infos,
+                                        ctx.signer_seeds(),
+                                    )
+                                }
+                            }
+                        })
+                        .collect()
+                })
+                .unwrap_or(vec![])
+        })
+        .unwrap_or(vec![]);
+    // Generate cpi methods for global methods.
+    let global_cpi_methods: Vec<proc_macro2::TokenStream> = program
         .ixs
         .iter()
         .map(|ix| {
@@ -1146,7 +1198,13 @@ fn generate_cpi(program: &Program) -> proc_macro2::TokenStream {
         pub mod cpi {
             use super::*;
 
-            #(#cpi_methods)*
+            pub mod state {
+                use super::*;
+
+                #(#state_cpi_methods)*
+            }
+
+            #(#global_cpi_methods)*
         }
     }
 }

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

@@ -184,6 +184,12 @@ impl Field {
                     ProgramState<#account>
                 }
             }
+            Ty::CpiState(ty) => {
+                let account = &ty.account_ident;
+                quote! {
+                    CpiState<#account>
+                }
+            }
             Ty::ProgramAccount(ty) => {
                 let account = &ty.account_ident;
                 quote! {
@@ -226,6 +232,7 @@ impl Field {
 pub enum Ty {
     AccountInfo,
     ProgramState(ProgramStateTy),
+    CpiState(CpiStateTy),
     ProgramAccount(ProgramAccountTy),
     CpiAccount(CpiAccountTy),
     Sysvar(SysvarTy),
@@ -250,6 +257,11 @@ pub struct ProgramStateTy {
     pub account_ident: syn::Ident,
 }
 
+#[derive(Debug, PartialEq)]
+pub struct CpiStateTy {
+    pub account_ident: syn::Ident,
+}
+
 #[derive(Debug, PartialEq)]
 pub struct ProgramAccountTy {
     // The struct type of the account.
@@ -272,6 +284,7 @@ pub enum Constraint {
     RentExempt(ConstraintRentExempt),
     Seeds(ConstraintSeeds),
     Executable(ConstraintExecutable),
+    State(ConstraintState),
 }
 
 #[derive(Debug)]
@@ -307,6 +320,11 @@ pub struct ConstraintSeeds {
 #[derive(Debug)]
 pub struct ConstraintExecutable {}
 
+#[derive(Debug)]
+pub struct ConstraintState {
+    pub program_target: proc_macro2::Ident,
+}
+
 #[derive(Debug)]
 pub struct Error {
     pub name: String,

+ 24 - 3
lang/syn/src/parser/accounts.rs

@@ -1,8 +1,8 @@
 use crate::{
     AccountField, AccountsStruct, CompositeField, Constraint, ConstraintBelongsTo,
     ConstraintExecutable, ConstraintLiteral, ConstraintOwner, ConstraintRentExempt,
-    ConstraintSeeds, ConstraintSigner, CpiAccountTy, Field, ProgramAccountTy, ProgramStateTy,
-    SysvarTy, Ty,
+    ConstraintSeeds, ConstraintSigner, ConstraintState, CpiAccountTy, CpiStateTy, Field,
+    ProgramAccountTy, ProgramStateTy, SysvarTy, Ty,
 };
 
 pub fn parse(strct: &syn::ItemStruct) -> AccountsStruct {
@@ -68,7 +68,8 @@ fn parse_field(f: &syn::Field, anchor: Option<&syn::Attribute>) -> AccountField
 
 fn is_field_primitive(f: &syn::Field) -> bool {
     match ident_string(f).as_str() {
-        "ProgramState" | "ProgramAccount" | "CpiAccount" | "Sysvar" | "AccountInfo" => true,
+        "ProgramState" | "ProgramAccount" | "CpiAccount" | "Sysvar" | "AccountInfo"
+        | "CpiState" => true,
         _ => false,
     }
 }
@@ -80,6 +81,7 @@ fn parse_ty(f: &syn::Field) -> Ty {
     };
     match ident_string(f).as_str() {
         "ProgramState" => Ty::ProgramState(parse_program_state(&path)),
+        "CpiState" => Ty::CpiState(parse_cpi_state(&path)),
         "ProgramAccount" => Ty::ProgramAccount(parse_program_account(&path)),
         "CpiAccount" => Ty::CpiAccount(parse_cpi_account(&path)),
         "Sysvar" => Ty::Sysvar(parse_sysvar(&path)),
@@ -104,6 +106,11 @@ fn parse_program_state(path: &syn::Path) -> ProgramStateTy {
     ProgramStateTy { account_ident }
 }
 
+fn parse_cpi_state(path: &syn::Path) -> CpiStateTy {
+    let account_ident = parse_account(&path);
+    CpiStateTy { account_ident }
+}
+
 fn parse_cpi_account(path: &syn::Path) -> CpiAccountTy {
     let account_ident = parse_account(path);
     CpiAccountTy { account_ident }
@@ -274,6 +281,20 @@ fn parse_constraints(anchor: &syn::Attribute) -> (Vec<Constraint>, bool, bool, b
                 "executable" => {
                     constraints.push(Constraint::Executable(ConstraintExecutable {}));
                 }
+                "state" => {
+                    match inner_tts.next().unwrap() {
+                        proc_macro2::TokenTree::Punct(punct) => {
+                            assert!(punct.as_char() == '=');
+                            punct
+                        }
+                        _ => panic!("invalid syntax"),
+                    };
+                    let program_target = match inner_tts.next().unwrap() {
+                        proc_macro2::TokenTree::Ident(ident) => ident,
+                        _ => panic!("invalid syntax"),
+                    };
+                    constraints.push(Constraint::State(ConstraintState { program_target }));
+                }
                 _ => {
                     panic!("invalid syntax");
                 }