Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

lang, spl, cli: Add associated_token keyword #790

Merged
merged 4 commits into from
Sep 24, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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