lang: Associated program account attributes (#186)
This commit is contained in:
parent
3d661cdc66
commit
b498b99f96
|
@ -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
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
|
||||
|
|
Loading…
Reference in New Issue