diff --git a/CHANGELOG.md b/CHANGELOG.md index 947cd0033..42f0f6f78 100644 --- a/CHANGELOG.md +++ b/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 = )]` constraint ([#178](https://github.com/project-serum/anchor/pull/178)). +* lang, cli, ts: Add `#[account(associated = )]` and `#[associated]` attributes for creating associated program accounts within programs. The TypeScript package can fetch these accounts with a new `.account..associated` (and `associatedAddress`) method ([#186](https://github.com/project-serum/anchor/pull/186)). ## Fixes diff --git a/examples/misc/programs/misc/src/lib.rs b/examples/misc/programs/misc/src/lib.rs index 012d6c7b1..0facae183 100644 --- a/examples/misc/programs/misc/src/lib.rs +++ b/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, + 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, diff --git a/examples/misc/tests/misc.js b/examples/misc/tests/misc.js index f37f0c3b6..3169a4ae1 100644 --- a/examples/misc/tests/misc.js +++ b/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); + }); }); diff --git a/lang/attribute/account/src/lib.rs b/lang/attribute/account/src/lib.rs index 2ff9289b6..f87805e28 100644 --- a/lang/attribute/account/src/lib.rs +++ b/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 + }) +} diff --git a/lang/derive/accounts/src/lib.rs b/lang/derive/accounts/src/lib.rs index f37f3ddc0..46f69dfd0 100644 --- a/lang/derive/accounts/src/lib.rs +++ b/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 = )]` | On `ProgramAccount` or `CpiAccount` structs | Checks the `target` field on the account matches the `target` field in the struct deriving `Accounts`. | /// | `#[account(has_one = )]` | On `ProgramAccount` or `CpiAccount` structs | Semantically different, but otherwise the same as `belongs_to`. | /// | `#[account(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 = )]` | On `CpiState` structs | Checks the given state is the canonical state account for the target program. | /// | `#[account(owner = )]` | On `CpiState`, `CpiAccount`, and `AccountInfo` | Checks the account owner matches the target. | +/// | `#[account(associated = , with? = , payer? = , space? = "")]` | 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); diff --git a/lang/src/lib.rs b/lang/src/lib.rs index 548d21d65..5af4faddd 100644 --- a/lang/src/lib.rs +++ b/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; diff --git a/lang/src/state.rs b/lang/src/state.rs index 6b519b19a..b4630d0b1 100644 --- a/lang/src/state.rs +++ b/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) diff --git a/lang/syn/src/codegen/accounts.rs b/lang/syn/src/codegen/accounts.rs index 488a0fdfa..50aad280a 100644 --- a/lang/syn/src/codegen/accounts.rs +++ b/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 = 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 = accs + // Deserialization for each *associated* field. This must be after + // the deser_fields. + let deser_associated_fields: Vec = 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::>(); + quote! { + #(#checks)* + } + }) + .collect(); + + // Constraint checks for each account fields. + let access_checks: Vec = non_associated_fields + .iter() + .map(|af: &&AccountField| { let checks: Vec = 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 { // 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 + }; + } +} diff --git a/lang/syn/src/lib.rs b/lang/syn/src/lib.rs index b806ff36e..bf306dc43 100644 --- a/lang/syn/src/lib.rs +++ b/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, + // Used by the associated attribute only. + pub space: Option, + // Used by the associated attribute only. + pub associated_seed: Option, } 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, diff --git a/lang/syn/src/parser/accounts.rs b/lang/syn/src/parser/accounts.rs index 683bbdc36..308670c59 100644 --- a/lang/syn/src/parser/accounts.rs +++ b/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, bool, bool, bool) { +fn parse_constraints( + anchor: &syn::Attribute, +) -> ( + Vec, + bool, + bool, + bool, + Option, + Option, + Option, +) { 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, 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, 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, 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, bool, bool, b } } - (constraints, is_mut, is_signer, is_init) + ( + constraints, + is_mut, + is_signer, + is_init, + payer, + space, + associated_seed, + ) } diff --git a/lang/syn/src/parser/file.rs b/lang/syn/src/parser/file.rs index e78a75ac2..b5562024d 100644 --- a/lang/syn/src/parser/file.rs +++ b/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 { diff --git a/ts/src/rpc.ts b/ts/src/rpc.ts index 7e5953678..91c3188a4 100644 --- a/ts/src/rpc.ts +++ b/ts/src/rpc.ts @@ -98,6 +98,8 @@ type AccountProps = { subscribe: (address: PublicKey, commitment?: Commitment) => EventEmitter; unsubscribe: (address: PublicKey) => void; createInstruction: (account: Account) => Promise; + associated: (...args: PublicKey[]) => Promise; + associatedAddress: (...args: PublicKey[]) => Promise; }; /** @@ -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 => { + 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 => { + const addr = await accountsNamespace["associatedAddress"](...args); + return await accountsNamespace(addr); + }; + accountFns[name] = accountsNamespace; });