Skip to content

Commit

Permalink
lang, spl, cli: Add associated_token keyword (#790)
Browse files Browse the repository at this point in the history
  • Loading branch information
armaniferrante authored Sep 24, 2021
1 parent 6583742 commit 2c827bc
Show file tree
Hide file tree
Showing 12 changed files with 276 additions and 19 deletions.
5 changes: 3 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@ incremented for features.

## [Unreleased]

## [0.16.1] - 2021-09-17

### Features

* lang: Add `--detach` flag to `anchor test` ([#770](https://github.com/project-serum/anchor/pull/770)).
* lang: Add `associated_token` keyword for initializing associated token accounts within `#[derive(Accounts)]` ([#790](https://github.com/project-serum/anchor/pull/790)).

## [0.16.1] - 2021-09-17

### Fixes

Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

28 changes: 16 additions & 12 deletions cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1370,18 +1370,10 @@ fn test(
.context(cmd)
};

match test_result {
Ok(exit) => {
if detach {
println!("Local validator still running. Press Ctrl + C quit.");
std::io::stdin().lock().lines().next().unwrap().unwrap();
} else if !exit.status.success() && !detach {
std::process::exit(exit.status.code().unwrap());
}
}
Err(err) => {
println!("Failed to run test: {:#}", err)
}
// Keep validator running if needed.
if test_result.is_ok() && detach {
println!("Local validator still running. Press Ctrl + C quit.");
std::io::stdin().lock().lines().next().unwrap().unwrap();
}

// Check all errors and shut down.
Expand All @@ -1396,6 +1388,18 @@ fn test(
}
}

// Must exist *after* shutting down the validator and log streams.
match test_result {
Ok(exit) => {
if !exit.status.success() {
std::process::exit(exit.status.code().unwrap());
}
}
Err(err) => {
println!("Failed to run test: {:#}", err)
}
}

Ok(())
})
}
Expand Down
22 changes: 22 additions & 0 deletions lang/syn/src/codegen/accounts/constraints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,28 @@ pub fn generate_init(
};
}
}
InitKind::AssociatedToken { owner, mint } => {
quote! {
let #field: #ty_decl = {
#payer

let cpi_program = associated_token_program.to_account_info();
let cpi_accounts = anchor_spl::associated_token::Create {
payer: payer.to_account_info(),
associated_token: #field.to_account_info(),
authority: #owner.to_account_info(),
mint: #mint.to_account_info(),
system_program: system_program.to_account_info(),
token_program: token_program.to_account_info(),
rent: rent.to_account_info(),
};
let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts);
anchor_spl::associated_token::create(cpi_ctx)?;
let pa: #ty_decl = #from_account_info;
pa
};
}
}
InitKind::Mint { owner, decimals } => {
let create_account = generate_create_account(
field,
Expand Down
10 changes: 10 additions & 0 deletions lang/syn/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,13 @@ impl Field {
}
}
}
Ty::CpiAccount(_) => {
quote! {
#container_ty::try_from_unchecked(
&#field,
)?
}
}
_ => {
let owner_addr = match &kind {
None => quote! { program_id },
Expand Down Expand Up @@ -554,6 +561,8 @@ pub enum ConstraintToken {
Address(Context<ConstraintAddress>),
TokenMint(Context<ConstraintTokenMint>),
TokenAuthority(Context<ConstraintTokenAuthority>),
AssociatedTokenMint(Context<ConstraintTokenMint>),
AssociatedTokenAuthority(Context<ConstraintTokenAuthority>),
MintAuthority(Context<ConstraintMintAuthority>),
MintDecimals(Context<ConstraintMintDecimals>),
Bump(Context<ConstraintTokenBump>),
Expand Down Expand Up @@ -653,6 +662,7 @@ pub enum InitKind {
// Owner for token and mint represents the authority. Not to be confused
// with the owner of the AccountInfo.
Token { owner: Expr, mint: Expr },
AssociatedToken { owner: Expr, mint: Expr },
Mint { owner: Expr, decimals: Expr },
}

Expand Down
101 changes: 100 additions & 1 deletion lang/syn/src/parser/accounts/constraints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,33 @@ pub fn parse_token(stream: ParseStream) -> ParseResult<ConstraintToken> {
_ => return Err(ParseError::new(ident.span(), "Invalid attribute")),
}
}
"associated_token" => {
stream.parse::<Token![:]>()?;
stream.parse::<Token![:]>()?;
let kw = stream.call(Ident::parse_any)?.to_string();
stream.parse::<Token![=]>()?;

let span = ident
.span()
.join(stream.span())
.unwrap_or_else(|| ident.span());

match kw.as_str() {
"mint" => ConstraintToken::AssociatedTokenMint(Context::new(
span,
ConstraintTokenMint {
mint: stream.parse()?,
},
)),
"authority" => ConstraintToken::AssociatedTokenAuthority(Context::new(
span,
ConstraintTokenAuthority {
auth: stream.parse()?,
},
)),
_ => return Err(ParseError::new(ident.span(), "Invalid attribute")),
}
}
"bump" => {
let bump = {
if stream.peek(Token![=]) {
Expand Down Expand Up @@ -246,6 +273,8 @@ pub struct ConstraintGroupBuilder<'ty> {
pub address: Option<Context<ConstraintAddress>>,
pub token_mint: Option<Context<ConstraintTokenMint>>,
pub token_authority: Option<Context<ConstraintTokenAuthority>>,
pub associated_token_mint: Option<Context<ConstraintTokenMint>>,
pub associated_token_authority: Option<Context<ConstraintTokenAuthority>>,
pub mint_authority: Option<Context<ConstraintMintAuthority>>,
pub mint_decimals: Option<Context<ConstraintMintDecimals>>,
pub bump: Option<Context<ConstraintTokenBump>>,
Expand Down Expand Up @@ -273,6 +302,8 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
address: None,
token_mint: None,
token_authority: None,
associated_token_mint: None,
associated_token_authority: None,
mint_authority: None,
mint_decimals: None,
bump: None,
Expand Down Expand Up @@ -307,7 +338,8 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
// When initializing a non-PDA account, the account being
// initialized must sign to invoke the system program's create
// account instruction.
if self.signer.is_none() && self.seeds.is_none() {
if self.signer.is_none() && self.seeds.is_none() && self.associated_token_mint.is_none()
{
self.signer
.replace(Context::new(i.span(), ConstraintSigner {}));
}
Expand Down Expand Up @@ -425,6 +457,8 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
address,
token_mint,
token_authority,
associated_token_mint,
associated_token_authority,
mint_authority,
mint_decimals,
bump,
Expand Down Expand Up @@ -469,6 +503,17 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
)),
},
}
} else if let Some(tm) = &associated_token_mint {
InitKind::AssociatedToken {
mint: tm.clone().into_inner().mint,
owner: match &associated_token_authority {
Some(a) => a.clone().into_inner().auth,
None => return Err(ParseError::new(
tm.span(),
"authority must be provided to initialize a token program derived address"
)),
},
}
} else if let Some(d) = &mint_decimals {
InitKind::Mint {
decimals: d.clone().into_inner().decimals,
Expand Down Expand Up @@ -522,6 +567,8 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
ConstraintToken::Address(c) => self.add_address(c),
ConstraintToken::TokenAuthority(c) => self.add_token_authority(c),
ConstraintToken::TokenMint(c) => self.add_token_mint(c),
ConstraintToken::AssociatedTokenAuthority(c) => self.add_associated_token_authority(c),
ConstraintToken::AssociatedTokenMint(c) => self.add_associated_token_mint(c),
ConstraintToken::MintAuthority(c) => self.add_mint_authority(c),
ConstraintToken::MintDecimals(c) => self.add_mint_decimals(c),
ConstraintToken::Bump(c) => self.add_bump(c),
Expand Down Expand Up @@ -585,6 +632,12 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
if self.token_mint.is_some() {
return Err(ParseError::new(c.span(), "token mint already provided"));
}
if self.associated_token_mint.is_some() {
return Err(ParseError::new(
c.span(),
"associated token mint already provided",
));
}
if self.init.is_none() {
return Err(ParseError::new(
c.span(),
Expand All @@ -595,6 +648,26 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
Ok(())
}

fn add_associated_token_mint(&mut self, c: Context<ConstraintTokenMint>) -> ParseResult<()> {
if self.associated_token_mint.is_some() {
return Err(ParseError::new(
c.span(),
"associated token mint already provided",
));
}
if self.token_mint.is_some() {
return Err(ParseError::new(c.span(), "token mint already provided"));
}
if self.init.is_none() {
return Err(ParseError::new(
c.span(),
"init must be provided before token",
));
}
self.associated_token_mint.replace(c);
Ok(())
}

fn add_bump(&mut self, c: Context<ConstraintTokenBump>) -> ParseResult<()> {
if self.bump.is_some() {
return Err(ParseError::new(c.span(), "bump already provided"));
Expand Down Expand Up @@ -626,6 +699,32 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
Ok(())
}

fn add_associated_token_authority(
&mut self,
c: Context<ConstraintTokenAuthority>,
) -> ParseResult<()> {
if self.associated_token_authority.is_some() {
return Err(ParseError::new(
c.span(),
"associated token authority already provided",
));
}
if self.token_authority.is_some() {
return Err(ParseError::new(
c.span(),
"token authority already provided",
));
}
if self.init.is_none() {
return Err(ParseError::new(
c.span(),
"init must be provided before token authority",
));
}
self.associated_token_authority.replace(c);
Ok(())
}

fn add_mint_authority(&mut self, c: Context<ConstraintMintAuthority>) -> ParseResult<()> {
if self.mint_authority.is_some() {
return Err(ParseError::new(c.span(), "mint authority already provided"));
Expand Down
1 change: 1 addition & 0 deletions spl/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ lazy_static = "1.4.0"
serum_dex = { git = "https://github.com/project-serum/serum-dex", rev = "1be91f2", version = "0.4.0", features = ["no-entrypoint"] }
solana-program = "=1.7.11"
spl-token = { version = "3.1.1", features = ["no-entrypoint"] }
spl-associated-token-account = { version = "1.0.3", features = ["no-entrypoint"] }
58 changes: 58 additions & 0 deletions spl/src/associated_token.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
use anchor_lang::solana_program::account_info::AccountInfo;
use anchor_lang::solana_program::entrypoint::ProgramResult;
use anchor_lang::solana_program::program_error::ProgramError;
use anchor_lang::solana_program::pubkey::Pubkey;
use anchor_lang::{Accounts, CpiContext};

pub use spl_associated_token_account::ID;

pub fn create<'info>(ctx: CpiContext<'_, '_, '_, 'info, Create<'info>>) -> ProgramResult {
let ix = spl_associated_token_account::create_associated_token_account(
ctx.accounts.payer.key,
ctx.accounts.authority.key,
ctx.accounts.mint.key,
);
solana_program::program::invoke_signed(
&ix,
&[
ctx.accounts.payer,
ctx.accounts.associated_token,
ctx.accounts.authority,
ctx.accounts.mint,
ctx.accounts.system_program,
ctx.accounts.token_program,
ctx.accounts.rent,
],
ctx.signer_seeds,
)
}

#[derive(Accounts)]
pub struct Create<'info> {
pub payer: AccountInfo<'info>,
pub associated_token: AccountInfo<'info>,
pub authority: AccountInfo<'info>,
pub mint: AccountInfo<'info>,
pub system_program: AccountInfo<'info>,
pub token_program: AccountInfo<'info>,
pub rent: AccountInfo<'info>,
}

#[derive(Clone)]
pub struct AssociatedToken;

impl anchor_lang::AccountDeserialize for AssociatedToken {
fn try_deserialize(buf: &mut &[u8]) -> Result<Self, ProgramError> {
AssociatedToken::try_deserialize_unchecked(buf)
}

fn try_deserialize_unchecked(_buf: &mut &[u8]) -> Result<Self, ProgramError> {
Ok(AssociatedToken)
}
}

impl anchor_lang::Id for AssociatedToken {
fn id() -> Pubkey {
ID
}
}
1 change: 1 addition & 0 deletions spl/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod associated_token;
pub mod dex;
pub mod mint;
pub mod shmem;
Expand Down
20 changes: 19 additions & 1 deletion tests/misc/programs/misc/src/context.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::account::*;
use anchor_lang::prelude::*;
use anchor_spl::token::{Mint, TokenAccount};
use anchor_spl::associated_token::AssociatedToken;
use anchor_spl::token::{Mint, Token, TokenAccount};
use misc2::misc2::MyState as Misc2State;
use std::mem::size_of;

Expand Down Expand Up @@ -31,6 +32,23 @@ pub struct TestTokenSeedsInit<'info> {
pub token_program: AccountInfo<'info>,
}

#[derive(Accounts)]
pub struct TestInitAssociatedToken<'info> {
#[account(
init,
payer = payer,
associated_token::mint = mint,
associated_token::authority = payer,
)]
pub token: Account<'info, TokenAccount>,
pub mint: Account<'info, Mint>,
pub payer: Signer<'info>,
pub rent: Sysvar<'info, Rent>,
pub system_program: Program<'info, System>,
pub token_program: Program<'info, Token>,
pub associated_token_program: Program<'info, AssociatedToken>,
}

#[derive(Accounts)]
#[instruction(nonce: u8)]
pub struct TestInstructionConstraint<'info> {
Expand Down
Loading

0 comments on commit 2c827bc

Please sign in to comment.