Skip to content

Commit

Permalink
lang: Associated program account attributes (#186)
Browse files Browse the repository at this point in the history
  • Loading branch information
armaniferrante authored Apr 13, 2021
1 parent 3d661cd commit b498b99
Show file tree
Hide file tree
Showing 12 changed files with 481 additions and 41 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
33 changes: 33 additions & 0 deletions examples/misc/programs/misc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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,
Expand Down
47 changes: 46 additions & 1 deletion examples/misc/tests/misc.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 changes: 47 additions & 0 deletions lang/attribute/account/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
})
}
5 changes: 4 additions & 1 deletion lang/derive/accounts/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand All @@ -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);
Expand Down
16 changes: 11 additions & 5 deletions lang/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions lang/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading

0 comments on commit b498b99

Please sign in to comment.