فهرست منبع

lang: Associated program account attributes (#186)

Armani Ferrante 4 سال پیش
والد
کامیت
b498b99f96

+ 1 - 0
CHANGELOG.md

@@ -15,6 +15,7 @@ incremented for features.
 
 * lang: CPI clients for program state instructions ([#43](https://github.com/project-serum/anchor/pull/43)).
 * lang: Add `#[account(owner = <program>)]` constraint ([#178](https://github.com/project-serum/anchor/pull/178)).
+* lang, cli, ts: Add `#[account(associated = <target>)]` and `#[associated]` attributes for creating associated program accounts within programs. The TypeScript package can fetch these accounts with a new `<program>.account.<account-name>.associated` (and `associatedAddress`) method ([#186](https://github.com/project-serum/anchor/pull/186)).
 
 ## Fixes
 

+ 33 - 0
examples/misc/programs/misc/src/lib.rs

@@ -44,6 +44,14 @@ pub mod misc {
         let ctx = ctx.accounts.cpi_state.context(cpi_program, cpi_accounts);
         misc2::cpi::state::set_data(ctx, data)
     }
+
+    pub fn test_associated_account_creation(
+        ctx: Context<TestAssociatedAccount>,
+        data: u64,
+    ) -> ProgramResult {
+        ctx.accounts.my_account.data = data;
+        Ok(())
+    }
 }
 
 #[derive(Accounts)]
@@ -79,6 +87,31 @@ pub struct TestStateCpi<'info> {
     misc2_program: AccountInfo<'info>,
 }
 
+// `my_account` is the associated token account being created.
+// `authority` must be a signer since it will pay for the creation of the
+// associated token account. `state` is used as an association, i.e., one
+// can *optionally* identify targets to be used as seeds for the program
+// derived address by using `with` (and it doesn't have to be a state account).
+// For example, the SPL token program uses a `Mint` account. Lastly,
+// `rent` and `system_program` are *required* by convention, since the
+// accounts are needed when creating the associated program address within
+// the program.
+#[derive(Accounts)]
+pub struct TestAssociatedAccount<'info> {
+    #[account(associated = authority, with = state)]
+    my_account: ProgramAccount<'info, TestData>,
+    #[account(signer)]
+    authority: AccountInfo<'info>,
+    state: ProgramState<'info, MyState>,
+    rent: Sysvar<'info, Rent>,
+    system_program: AccountInfo<'info>,
+}
+
+#[associated]
+pub struct TestData {
+    data: u64,
+}
+
 #[account]
 pub struct Data {
     udata: u128,

+ 46 - 1
examples/misc/tests/misc.js

@@ -68,7 +68,7 @@ describe("misc", () => {
     );
   });
 
-  it("Can use the executable attribtue", async () => {
+  it("Can use the executable attribute", async () => {
     await program.rpc.testExecutable({
       accounts: {
         program: program.programId,
@@ -111,4 +111,49 @@ describe("misc", () => {
     assert.ok(stateAccount.data.eq(newData));
     assert.ok(stateAccount.auth.equals(program.provider.wallet.publicKey));
   });
+
+  it("Can create an associated program account", async () => {
+    const state = await program.state.address();
+
+    // Manual associated address calculation for test only. Clients should use
+    // the generated methods.
+    const [
+      associatedAccount,
+      nonce,
+    ] = await anchor.web3.PublicKey.findProgramAddress(
+      [
+        Buffer.from([97, 110, 99, 104, 111, 114]), // b"anchor".
+        program.provider.wallet.publicKey.toBuffer(),
+        state.toBuffer(),
+      ],
+      program.programId
+    );
+    await assert.rejects(
+      async () => {
+        await program.account.testData(associatedAccount);
+      },
+      (err) => {
+        assert.ok(
+          err.toString() ===
+            `Error: Account does not exist ${associatedAccount.toString()}`
+        );
+        return true;
+      }
+    );
+    await program.rpc.testAssociatedAccountCreation(new anchor.BN(1234), {
+      accounts: {
+        myAccount: associatedAccount,
+        authority: program.provider.wallet.publicKey,
+        state,
+        rent: anchor.web3.SYSVAR_RENT_PUBKEY,
+        systemProgram: anchor.web3.SystemProgram.programId,
+      },
+    });
+    // Try out the generated associated method.
+    const account = await program.account.testData.associated(
+      program.provider.wallet.publicKey,
+      state
+    );
+    assert.ok(account.data.toNumber() === 1234);
+  });
 });

+ 47 - 0
lang/attribute/account/src/lib.rs

@@ -90,3 +90,50 @@ pub fn account(
         #coder
     })
 }
+
+/// Extends the `#[account]` attribute to allow one to create associated token
+/// accounts. This includes a `Default` implementation, which means all fields
+/// in an `#[associated]` struct must implement `Default` and an
+/// `anchor_lang::Bump` trait implementation, which allows the account to be
+/// used as a program derived address.
+#[proc_macro_attribute]
+pub fn associated(
+    _args: proc_macro::TokenStream,
+    input: proc_macro::TokenStream,
+) -> proc_macro::TokenStream {
+    let mut account_strct = parse_macro_input!(input as syn::ItemStruct);
+
+    // Add a `__nonce: u8` field to the struct to hold the bump seed for
+    // the program dervied address.
+    match &mut account_strct.fields {
+        syn::Fields::Named(fields) => {
+            let mut segments = syn::punctuated::Punctuated::new();
+            segments.push(syn::PathSegment {
+                ident: syn::Ident::new("u8", proc_macro2::Span::call_site()),
+                arguments: syn::PathArguments::None,
+            });
+            fields.named.push(syn::Field {
+                attrs: Vec::new(),
+                vis: syn::Visibility::Inherited,
+                ident: Some(syn::Ident::new("__nonce", proc_macro2::Span::call_site())),
+                colon_token: Some(syn::token::Colon {
+                    spans: [proc_macro2::Span::call_site()],
+                }),
+                ty: syn::Type::Path(syn::TypePath {
+                    qself: None,
+                    path: syn::Path {
+                        leading_colon: None,
+                        segments,
+                    },
+                }),
+            });
+        }
+        _ => panic!("Fields must be named"),
+    }
+
+    proc_macro::TokenStream::from(quote! {
+        #[anchor_lang::account]
+        #[derive(Default)]
+        #account_strct
+    })
+}

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

@@ -40,7 +40,7 @@ use syn::parse_macro_input;
 /// |:--|:--|:--|
 /// | `#[account(signer)]` | On raw `AccountInfo` structs. | Checks the given account signed the transaction. |
 /// | `#[account(mut)]` | On `AccountInfo`, `ProgramAccount` or `CpiAccount` structs. | Marks the account as mutable and persists the state transition. |
-/// | `#[account(init)]` | On `ProgramAccount` structs. | Marks the account as being initialized, skipping the account discriminator check. |
+/// | `#[account(init)]` | On `ProgramAccount` structs. | Marks the account as being initialized, skipping the account discriminator check. When using `init`, a `rent` `Sysvar` must be present in the `Accounts` struct. |
 /// | `#[account(belongs_to = <target>)]` | On `ProgramAccount` or `CpiAccount` structs | Checks the `target` field on the account matches the `target` field in the struct deriving `Accounts`. |
 /// | `#[account(has_one = <target>)]` | On `ProgramAccount` or `CpiAccount` structs | Semantically different, but otherwise the same as `belongs_to`. |
 /// | `#[account(seeds = [<seeds>])]` | On `AccountInfo` structs | Seeds for the program derived address an `AccountInfo` struct represents. |
@@ -49,6 +49,9 @@ use syn::parse_macro_input;
 /// | `#[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. |
 /// | `#[account(owner = <target>)]` | On `CpiState`, `CpiAccount`, and `AccountInfo` | Checks the account owner matches the target. |
+/// | `#[account(associated = <target>, with? = <target>, payer? = <target>, space? = "<literal>")]` | On `ProgramAccount` | Creates an associated program account at a program derived address. `associated` is the SOL address to create the account for. `with` is an optional association, for example, a `Mint` account in the SPL token program. `payer` is an optional account to pay for the account creation, defaulting to the `associated` target if none is given. `space` is an optional literal specifying how large the account is, defaulting to the account's serialized `Default::default` size (+ 8 for the account discriminator) if none is given. When creating an associated account, a `rent` `Sysvar` and `system_program` `AccountInfo` must be present in the `Accounts` struct. |
+// TODO: How do we make the markdown render correctly without putting everything
+//       on absurdly long lines?
 #[proc_macro_derive(Accounts, attributes(account))]
 pub fn derive_anchor_deserialize(item: TokenStream) -> TokenStream {
     let strct = parse_macro_input!(item as syn::ItemStruct);

+ 11 - 5
lang/src/lib.rs

@@ -59,7 +59,7 @@ pub use crate::program_account::ProgramAccount;
 pub use crate::state::ProgramState;
 pub use crate::sysvar::Sysvar;
 pub use anchor_attribute_access_control::access_control;
-pub use anchor_attribute_account::account;
+pub use anchor_attribute_account::{account, associated};
 pub use anchor_attribute_error::error;
 pub use anchor_attribute_event::{emit, event};
 pub use anchor_attribute_interface::interface;
@@ -205,14 +205,20 @@ pub trait Discriminator {
     fn discriminator() -> [u8; 8];
 }
 
+/// Bump seed for program derived addresses.
+pub trait Bump {
+    fn seed(&self) -> u8;
+}
+
 /// The prelude contains all commonly used components of the crate.
 /// All programs should include it via `anchor_lang::prelude::*;`.
 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, CpiState, CpiStateContext, ProgramAccount, ProgramState,
-        Sysvar, ToAccountInfo, ToAccountInfos, ToAccountMetas,
+        access_control, account, associated, emit, error, event, interface, program, state,
+        AccountDeserialize, AccountSerialize, Accounts, AccountsExit, AccountsInit,
+        AnchorDeserialize, AnchorSerialize, Context, CpiAccount, CpiContext, CpiState,
+        CpiStateContext, ProgramAccount, ProgramState, Sysvar, ToAccountInfo, ToAccountInfos,
+        ToAccountMetas,
     };
 
     pub use borsh;

+ 2 - 0
lang/src/state.rs

@@ -66,11 +66,13 @@ where
         *accounts = &accounts[1..];
 
         if account.key != &Self::address(program_id) {
+            solana_program::msg!("Invalid state address");
             return Err(ProgramError::Custom(1)); // todo: proper error.
         }
 
         let pa = ProgramState::try_from(account)?;
         if pa.inner.info.owner != program_id {
+            solana_program::msg!("Invalid state owner");
             return Err(ProgramError::Custom(1)); // todo: proper error.
         }
         Ok(pa)

+ 198 - 25
lang/syn/src/codegen/accounts.rs

@@ -1,43 +1,85 @@
 use crate::{
-    AccountField, AccountsStruct, CompositeField, Constraint, ConstraintBelongsTo,
-    ConstraintExecutable, ConstraintLiteral, ConstraintOwner, ConstraintRentExempt,
-    ConstraintSeeds, ConstraintSigner, ConstraintState, Field, Ty,
+    AccountField, AccountsStruct, CompositeField, Constraint, ConstraintAssociated,
+    ConstraintBelongsTo, ConstraintExecutable, ConstraintLiteral, ConstraintOwner,
+    ConstraintRentExempt, ConstraintSeeds, ConstraintSigner, ConstraintState, Field, Ty,
 };
 use heck::SnakeCase;
 use quote::quote;
 
 pub fn generate(accs: AccountsStruct) -> proc_macro2::TokenStream {
-    // Deserialization for each field.
+    // All fields without an `#[account(associated)]` attribute.
+    let non_associated_fields: Vec<&AccountField> =
+        accs.fields.iter().filter(|af| !is_associated(af)).collect();
+
+    // Deserialization for each field
     let deser_fields: Vec<proc_macro2::TokenStream> = accs
         .fields
         .iter()
-        .map(|af: &AccountField| match af {
-            AccountField::AccountsStruct(s) => {
-                let name = &s.ident;
-                let ty = &s.raw_field.ty;
-                quote! {
-                    let #name: #ty = anchor_lang::Accounts::try_accounts(program_id, accounts)?;
+        .map(|af: &AccountField| {
+            match af {
+                AccountField::AccountsStruct(s) => {
+                    let name = &s.ident;
+                    let ty = &s.raw_field.ty;
+                    quote! {
+                        let #name: #ty = anchor_lang::Accounts::try_accounts(program_id, accounts)?;
+                    }
                 }
-            }
-            AccountField::Field(f) => {
-                let name = f.typed_ident();
-                match f.is_init {
-                    false => quote! {
-                        let #name = anchor_lang::Accounts::try_accounts(program_id, accounts)?;
-                    },
-                    true => quote! {
-                        let #name = anchor_lang::AccountsInit::try_accounts_init(program_id, accounts)?;
-                    },
+                AccountField::Field(f) => {
+                    // Associated fields are *first* deserialized into
+                    // AccountInfos, and then later deserialized into
+                    // ProgramAccounts in the "constraint check" phase.
+                    if is_associated(af) {
+                        let name = &f.ident;
+                        quote!{
+                            let #name = &accounts[0];
+                            *accounts = &accounts[1..];
+                        }
+                    } else {
+                        let name = &f.typed_ident();
+                        match f.is_init {
+                            false => quote! {
+                                let #name = anchor_lang::Accounts::try_accounts(program_id, accounts)?;
+                            },
+                            true => quote! {
+                                let #name = anchor_lang::AccountsInit::try_accounts_init(program_id, accounts)?;
+                            },
+                        }
+                    }
                 }
             }
         })
         .collect();
 
-    // Constraint checks for each account fields.
-    let access_checks: Vec<proc_macro2::TokenStream> = accs
+    // Deserialization for each *associated* field. This must be after
+    // the deser_fields.
+    let deser_associated_fields: Vec<proc_macro2::TokenStream> = accs
         .fields
         .iter()
-        .map(|af: &AccountField| {
+        .filter_map(|af| match af {
+            AccountField::AccountsStruct(_s) => None,
+            AccountField::Field(f) => match is_associated(af) {
+                false => None,
+                true => Some(f),
+            },
+        })
+        .map(|field: &Field| {
+            // TODO: the constraints should be sorted so that the associated
+            //       constraint comes first.
+            let checks = field
+                .constraints
+                .iter()
+                .map(|c| generate_field_constraint(&field, c))
+                .collect::<Vec<proc_macro2::TokenStream>>();
+            quote! {
+                #(#checks)*
+            }
+        })
+        .collect();
+
+    // Constraint checks for each account fields.
+    let access_checks: Vec<proc_macro2::TokenStream> = non_associated_fields
+        .iter()
+        .map(|af: &&AccountField| {
             let checks: Vec<proc_macro2::TokenStream> = match af {
                 AccountField::Field(f) => f
                     .constraints
@@ -265,10 +307,15 @@ pub fn generate(accs: AccountsStruct) -> proc_macro2::TokenStream {
             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)*
-
+                // Deserialize each associated account.
+                //
+                // Associated accounts are treated specially, because the fields
+                // do deserialization + constraint checks in a single go,
+                // whereas all other fields, i.e. the `deser_fields`, first
+                // deserialize, and then do constraint checks.
+                #(#deser_associated_fields)*
                 // Perform constraint checks on each account.
                 #(#access_checks)*
-
                 // Success. Return the validated accounts.
                 Ok(#name {
                     #(#return_tys),*
@@ -306,6 +353,22 @@ pub fn generate(accs: AccountsStruct) -> proc_macro2::TokenStream {
     }
 }
 
+// Returns true if the given AccountField has an associated constraint.
+fn is_associated(af: &AccountField) -> bool {
+    match af {
+        AccountField::AccountsStruct(_s) => false,
+        AccountField::Field(f) => f
+            .constraints
+            .iter()
+            .filter(|c| match c {
+                Constraint::Associated(_c) => true,
+                _ => false,
+            })
+            .next()
+            .is_some(),
+    }
+}
+
 pub fn generate_field_constraint(f: &Field, c: &Constraint) -> proc_macro2::TokenStream {
     match c {
         Constraint::BelongsTo(c) => generate_constraint_belongs_to(f, c),
@@ -316,6 +379,7 @@ pub fn generate_field_constraint(f: &Field, c: &Constraint) -> proc_macro2::Toke
         Constraint::Seeds(c) => generate_constraint_seeds(f, c),
         Constraint::Executable(c) => generate_constraint_executable(f, c),
         Constraint::State(c) => generate_constraint_state(f, c),
+        Constraint::Associated(c) => generate_constraint_associated(f, c),
     }
 }
 
@@ -446,3 +510,112 @@ pub fn generate_constraint_state(f: &Field, c: &ConstraintState) -> proc_macro2:
         }
     }
 }
+
+pub fn generate_constraint_associated(
+    f: &Field,
+    c: &ConstraintAssociated,
+) -> proc_macro2::TokenStream {
+    let associated_target = c.associated_target.clone();
+    let field = &f.ident;
+    let account_ty = match &f.ty {
+        Ty::ProgramAccount(ty) => &ty.account_ident,
+        _ => panic!("Invalid syntax"),
+    };
+
+    let space = match &f.space {
+        None => quote! {
+            let space = 8 + #account_ty::default().try_to_vec().unwrap().len();
+        },
+        Some(s) => quote! {
+            let space = #s;
+        },
+    };
+
+    let payer = match &f.payer {
+        None => quote! {
+            let payer = #associated_target.to_account_info();
+        },
+        Some(p) => quote! {
+            let payer = #p.to_account_info();
+        },
+    };
+
+    let seeds_no_nonce = match &f.associated_seed {
+        None => quote! {
+            [
+                &b"anchor"[..],
+                #associated_target.to_account_info().key.as_ref(),
+            ]
+        },
+        Some(seed) => quote! {
+            [
+                &b"anchor"[..],
+                #associated_target.to_account_info().key.as_ref(),
+                #seed.to_account_info().key.as_ref(),
+            ]
+        },
+    };
+    let seeds_with_nonce = match &f.associated_seed {
+        None => quote! {
+            [
+                &b"anchor"[..],
+                #associated_target.to_account_info().key.as_ref(),
+                &[nonce],
+            ]
+        },
+        Some(seed) => quote! {
+            [
+                &b"anchor"[..],
+                #associated_target.to_account_info().key.as_ref(),
+                #seed.to_account_info().key.as_ref(),
+                &[nonce],
+            ]
+        },
+    };
+
+    quote! {
+        let #field: anchor_lang::ProgramAccount<#account_ty> = {
+            #space
+            #payer
+
+            let (associated_field, nonce) = Pubkey::find_program_address(
+                &#seeds_no_nonce,
+                program_id,
+            );
+            if &associated_field != #field.key {
+                return Err(ProgramError::Custom(45)); // todo: proper error.
+            }
+            let lamports = rent.minimum_balance(space);
+            let ix = anchor_lang::solana_program::system_instruction::create_account(
+                payer.key,
+                #field.key,
+                lamports,
+                space as u64,
+                program_id,
+            );
+
+            let seeds = #seeds_with_nonce;
+            let signer = &[&seeds[..]];
+            anchor_lang::solana_program::program::invoke_signed(
+                &ix,
+                &[
+
+                    #field.clone(),
+                    payer.clone(),
+                    system_program.clone(),
+                ],
+                signer,
+            ).map_err(|e| {
+                anchor_lang::solana_program::msg!("Unable to create associated account");
+                e
+            })?;
+            // For now, we assume all accounts created with the `associated`
+            // attribute have a `nonce` field in their account.
+            let mut pa: anchor_lang::ProgramAccount<#account_ty> = anchor_lang::ProgramAccount::try_from_init(
+                &#field,
+            )?;
+            pa.__nonce = nonce;
+            pa
+        };
+    }
+}

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

@@ -170,6 +170,14 @@ pub struct Field {
     pub is_mut: bool,
     pub is_signer: bool,
     pub is_init: bool,
+    // TODO: move associated out of the constraints and put into tis own
+    //       field + struct.
+    // Used by the associated attribute only.
+    pub payer: Option<syn::Ident>,
+    // Used by the associated attribute only.
+    pub space: Option<proc_macro2::TokenStream>,
+    // Used by the associated attribute only.
+    pub associated_seed: Option<syn::Ident>,
 }
 
 impl Field {
@@ -285,6 +293,7 @@ pub enum Constraint {
     Seeds(ConstraintSeeds),
     Executable(ConstraintExecutable),
     State(ConstraintState),
+    Associated(ConstraintAssociated),
 }
 
 #[derive(Debug)]
@@ -324,6 +333,11 @@ pub struct ConstraintState {
     pub program_target: proc_macro2::Ident,
 }
 
+#[derive(Debug)]
+pub struct ConstraintAssociated {
+    pub associated_target: proc_macro2::Ident,
+}
+
 #[derive(Debug)]
 pub struct Error {
     pub name: String,

+ 100 - 8
lang/syn/src/parser/accounts.rs

@@ -1,8 +1,8 @@
 use crate::{
-    AccountField, AccountsStruct, CompositeField, Constraint, ConstraintBelongsTo,
-    ConstraintExecutable, ConstraintLiteral, ConstraintOwner, ConstraintRentExempt,
-    ConstraintSeeds, ConstraintSigner, ConstraintState, CpiAccountTy, CpiStateTy, Field,
-    ProgramAccountTy, ProgramStateTy, SysvarTy, Ty,
+    AccountField, AccountsStruct, CompositeField, Constraint, ConstraintAssociated,
+    ConstraintBelongsTo, ConstraintExecutable, ConstraintLiteral, ConstraintOwner,
+    ConstraintRentExempt, ConstraintSeeds, ConstraintSigner, ConstraintState, CpiAccountTy,
+    CpiStateTy, Field, ProgramAccountTy, ProgramStateTy, SysvarTy, Ty,
 };
 
 pub fn parse(strct: &syn::ItemStruct) -> AccountsStruct {
@@ -41,8 +41,8 @@ fn parse_account_attr(f: &syn::Field) -> Option<&syn::Attribute> {
 
 fn parse_field(f: &syn::Field, anchor: Option<&syn::Attribute>) -> AccountField {
     let ident = f.ident.clone().unwrap();
-    let (constraints, is_mut, is_signer, is_init) = match anchor {
-        None => (vec![], false, false, false),
+    let (constraints, is_mut, is_signer, is_init, payer, space, associated_seed) = match anchor {
+        None => (vec![], false, false, false, None, None, None),
         Some(anchor) => parse_constraints(anchor),
     };
     match is_field_primitive(f) {
@@ -55,6 +55,9 @@ fn parse_field(f: &syn::Field, anchor: Option<&syn::Attribute>) -> AccountField
                 is_mut,
                 is_signer,
                 is_init,
+                payer,
+                space,
+                associated_seed,
             })
         }
         false => AccountField::AccountsStruct(CompositeField {
@@ -174,7 +177,17 @@ fn parse_sysvar(path: &syn::Path) -> SysvarTy {
     }
 }
 
-fn parse_constraints(anchor: &syn::Attribute) -> (Vec<Constraint>, bool, bool, bool) {
+fn parse_constraints(
+    anchor: &syn::Attribute,
+) -> (
+    Vec<Constraint>,
+    bool,
+    bool,
+    bool,
+    Option<syn::Ident>,
+    Option<proc_macro2::TokenStream>,
+    Option<syn::Ident>,
+) {
     let mut tts = anchor.tokens.clone().into_iter();
     let g_stream = match tts.next().expect("Must have a token group") {
         proc_macro2::TokenTree::Group(g) => g.stream(),
@@ -186,6 +199,10 @@ fn parse_constraints(anchor: &syn::Attribute) -> (Vec<Constraint>, bool, bool, b
     let mut is_signer = false;
     let mut constraints = vec![];
     let mut is_rent_exempt = None;
+    let mut payer = None;
+    let mut space = None;
+    let mut is_associated = false;
+    let mut associated_seed = None;
 
     let mut inner_tts = g_stream.into_iter();
     while let Some(token) = inner_tts.next() {
@@ -290,6 +307,68 @@ fn parse_constraints(anchor: &syn::Attribute) -> (Vec<Constraint>, bool, bool, b
                     };
                     constraints.push(Constraint::State(ConstraintState { program_target }));
                 }
+                "associated" => {
+                    is_associated = true;
+                    is_mut = true;
+                    match inner_tts.next().unwrap() {
+                        proc_macro2::TokenTree::Punct(punct) => {
+                            assert!(punct.as_char() == '=');
+                            punct
+                        }
+                        _ => panic!("invalid syntax"),
+                    };
+                    let associated_target = match inner_tts.next().unwrap() {
+                        proc_macro2::TokenTree::Ident(ident) => ident,
+                        _ => panic!("invalid syntax"),
+                    };
+                    constraints.push(Constraint::Associated(ConstraintAssociated {
+                        associated_target,
+                    }));
+                }
+                "with" => {
+                    match inner_tts.next().unwrap() {
+                        proc_macro2::TokenTree::Punct(punct) => {
+                            assert!(punct.as_char() == '=');
+                            punct
+                        }
+                        _ => panic!("invalid syntax"),
+                    };
+                    associated_seed = match inner_tts.next().unwrap() {
+                        proc_macro2::TokenTree::Ident(ident) => Some(ident),
+                        _ => panic!("invalid syntax"),
+                    };
+                }
+                "payer" => {
+                    match inner_tts.next().unwrap() {
+                        proc_macro2::TokenTree::Punct(punct) => {
+                            assert!(punct.as_char() == '=');
+                            punct
+                        }
+                        _ => panic!("invalid syntax"),
+                    };
+                    let _payer = match inner_tts.next().unwrap() {
+                        proc_macro2::TokenTree::Ident(ident) => ident,
+                        _ => panic!("invalid syntax"),
+                    };
+                    payer = Some(_payer);
+                }
+                "space" => {
+                    match inner_tts.next().unwrap() {
+                        proc_macro2::TokenTree::Punct(punct) => {
+                            assert!(punct.as_char() == '=');
+                            punct
+                        }
+                        _ => panic!("invalid syntax"),
+                    };
+                    match inner_tts.next().unwrap() {
+                        proc_macro2::TokenTree::Literal(literal) => {
+                            let tokens: proc_macro2::TokenStream =
+                                literal.to_string().replace("\"", "").parse().unwrap();
+                            space = Some(tokens);
+                        }
+                        _ => panic!("invalid space"),
+                    }
+                }
                 _ => {
                     panic!("invalid syntax");
                 }
@@ -310,6 +389,11 @@ fn parse_constraints(anchor: &syn::Attribute) -> (Vec<Constraint>, bool, bool, b
         }
     }
 
+    // If `associated` is given, remove `init` since it's redundant.
+    if is_associated {
+        is_init = false;
+    }
+
     if let Some(is_re) = is_rent_exempt {
         match is_re {
             false => constraints.push(Constraint::RentExempt(ConstraintRentExempt::Skip)),
@@ -317,5 +401,13 @@ fn parse_constraints(anchor: &syn::Attribute) -> (Vec<Constraint>, bool, bool, b
         }
     }
 
-    (constraints, is_mut, is_signer, is_init)
+    (
+        constraints,
+        is_mut,
+        is_signer,
+        is_init,
+        payer,
+        space,
+        associated_seed,
+    )
 }

+ 1 - 1
lang/syn/src/parser/file.rs

@@ -323,7 +323,7 @@ fn parse_accounts(f: &syn::File) -> Vec<&syn::ItemStruct> {
                     .iter()
                     .filter(|attr| {
                         let segment = attr.path.segments.last().unwrap();
-                        segment.ident == "account"
+                        segment.ident == "account" || segment.ident == "associated"
                     })
                     .count();
                 match attrs_count {

+ 24 - 0
ts/src/rpc.ts

@@ -98,6 +98,8 @@ type AccountProps = {
   subscribe: (address: PublicKey, commitment?: Commitment) => EventEmitter;
   unsubscribe: (address: PublicKey) => void;
   createInstruction: (account: Account) => Promise<TransactionInstruction>;
+  associated: (...args: PublicKey[]) => Promise<any>;
+  associatedAddress: (...args: PublicKey[]) => Promise<PublicKey>;
 };
 
 /**
@@ -565,6 +567,28 @@ export class RpcFactory {
         );
       };
 
+      // Function returning the associated address. Args are keys to associate.
+      // Order matters.
+      accountsNamespace["associatedAddress"] = async (
+        ...args: PublicKey[]
+      ): Promise<PublicKey> => {
+        let seeds = [Buffer.from([97, 110, 99, 104, 111, 114])]; // b"anchor".
+        args.forEach((arg) => {
+          seeds.push(arg.toBuffer());
+        });
+        const [assoc] = await PublicKey.findProgramAddress(seeds, programId);
+        return assoc;
+      };
+
+      // Function returning the associated account. Args are keys to associate.
+      // Order matters.
+      accountsNamespace["associated"] = async (
+        ...args: PublicKey[]
+      ): Promise<any> => {
+        const addr = await accountsNamespace["associatedAddress"](...args);
+        return await accountsNamespace(addr);
+      };
+
       accountFns[name] = accountsNamespace;
     });