lang: Associated program account attributes (#186)

This commit is contained in:
Armani Ferrante 2021-04-14 02:47:54 +08:00 committed by GitHub
parent 3d661cdc66
commit b498b99f96
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 481 additions and 41 deletions

View File

@ -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

View File

@ -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,

View File

@ -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);
});
});

View File

@ -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
})
}

View File

@ -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);

View File

@ -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;

View File

@ -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)

View File

@ -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
};
}
}

View File

@ -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,

View File

@ -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,
)
}

View File

@ -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 {

View File

@ -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;
});