diff --git a/CHANGELOG.md b/CHANGELOG.md index 009d9175f1..8f783a469f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,11 @@ incremented for features. ### Features * lang: Ignore `Unnamed` structs instead of panic ([#605](https://github.com/project-serum/anchor/pull/605)). +* lang: Add constraints for initializing mint accounts as pdas, `#[account(init, seeds = [...], mint::decimals = , mint::authority = )]` ([#](https://github.com/project-serum/anchor/pull/562)). + +### Breaking Changes + +* lang: Change `#[account(init, seeds = [...], token = , authority = )]` to `#[account(init, token::mint = token::authority = )]` ([#](https://github.com/project-serum/anchor/pull/562)). ## [0.13.2] - 2021-08-11 diff --git a/Cargo.lock b/Cargo.lock index b1ae461856..7414ec9493 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2863,7 +2863,7 @@ dependencies = [ [[package]] name = "serum-common" version = "0.1.0" -source = "git+https://github.com/project-serum/serum-dex#576e5d2ef2a1669fbd889f13b97b4f552554e334" +source = "git+https://github.com/project-serum/serum-dex#b977df6c9c89d0600396fe0c9244ea0ee6fafb5a" dependencies = [ "anyhow", "arrayref", @@ -2881,7 +2881,7 @@ dependencies = [ [[package]] name = "serum_dex" version = "0.4.0" -source = "git+https://github.com/project-serum/serum-dex#9f776c45fa37ec80109ea1f46461a75125f3f334" +source = "git+https://github.com/project-serum/serum-dex#b977df6c9c89d0600396fe0c9244ea0ee6fafb5a" dependencies = [ "arrayref", "bincode", diff --git a/examples/cfo/programs/cfo/src/lib.rs b/examples/cfo/programs/cfo/src/lib.rs index d3befcd216..93f9dabcde 100644 --- a/examples/cfo/programs/cfo/src/lib.rs +++ b/examples/cfo/programs/cfo/src/lib.rs @@ -292,15 +292,14 @@ pub struct CreateOfficer<'info> { officer: ProgramAccount<'info, Officer>, #[account( init, - token = mint, + token::mint = mint, associated = officer, with = b"vault", - space = TokenAccount::LEN, payer = authority, )] srm_vault: CpiAccount<'info, TokenAccount>, #[account( init, - token = mint, + token::mint = mint, associated = officer, with = b"stake", space = TokenAccount::LEN, payer = authority, @@ -308,9 +307,8 @@ pub struct CreateOfficer<'info> { stake: CpiAccount<'info, TokenAccount>, #[account( init, - token = mint, + token::mint = mint, associated = officer, with = b"treasury", - space = TokenAccount::LEN, payer = authority, )] treasury: CpiAccount<'info, TokenAccount>, @@ -337,7 +335,7 @@ pub struct CreateOfficerToken<'info> { officer: ProgramAccount<'info, Officer>, #[account( init, - token = mint, + token::mint = mint, associated = officer, with = mint, space = TokenAccount::LEN, payer = payer, diff --git a/examples/misc/package.json b/examples/misc/package.json new file mode 100644 index 0000000000..4d29034cff --- /dev/null +++ b/examples/misc/package.json @@ -0,0 +1,7 @@ +{ + "dependencies": { + "@project-serum/anchor": "^0.11.1", + "@solana/spl-token": "^0.1.6", + "mocha": "^9.0.3" + } +} diff --git a/examples/misc/programs/misc/src/context.rs b/examples/misc/programs/misc/src/context.rs index da7f9dad90..1c788e6d33 100644 --- a/examples/misc/programs/misc/src/context.rs +++ b/examples/misc/programs/misc/src/context.rs @@ -5,18 +5,24 @@ use anchor_spl::token::{Mint, TokenAccount}; use misc2::misc2::MyState as Misc2State; #[derive(Accounts)] -#[instruction(nonce: u8)] +#[instruction(token_bump: u8, mint_bump: u8)] pub struct TestTokenSeedsInit<'info> { #[account( init, - token = mint, - authority = authority, - seeds = [b"my-token-seed".as_ref(), &[nonce]], + seeds = [b"my-mint-seed".as_ref(), &[mint_bump]], payer = authority, - space = TokenAccount::LEN, + mint::decimals = 6, + mint::authority = authority, )] - pub my_pda: CpiAccount<'info, TokenAccount>, pub mint: CpiAccount<'info, Mint>, + #[account( + init, + seeds = [b"my-token-seed".as_ref(), &[token_bump]], + payer = authority, + token::mint = mint, + token::authority = authority, + )] + pub my_pda: CpiAccount<'info, TokenAccount>, pub authority: AccountInfo<'info>, pub system_program: AccountInfo<'info>, pub rent: Sysvar<'info, Rent>, diff --git a/examples/misc/programs/misc/src/lib.rs b/examples/misc/programs/misc/src/lib.rs index f1662b5ae3..d2058a3392 100644 --- a/examples/misc/programs/misc/src/lib.rs +++ b/examples/misc/programs/misc/src/lib.rs @@ -143,7 +143,11 @@ pub mod misc { Ok(()) } - pub fn test_token_seeds_init(_ctx: Context, _nonce: u8) -> ProgramResult { + pub fn test_token_seeds_init( + _ctx: Context, + _token_bump: u8, + _mint_bump: u8, + ) -> ProgramResult { Ok(()) } diff --git a/examples/misc/tests/misc.js b/examples/misc/tests/misc.js index 21e4735f89..beba049893 100644 --- a/examples/misc/tests/misc.js +++ b/examples/misc/tests/misc.js @@ -458,22 +458,18 @@ describe("misc", () => { }); it("Can create a token account from seeds pda", async () => { - const mint = await Token.createMint( - program.provider.connection, - program.provider.wallet.payer, - program.provider.wallet.publicKey, - null, - 0, - TOKEN_PROGRAM_ID + const [mint, mint_bump] = await PublicKey.findProgramAddress( + [Buffer.from(anchor.utils.bytes.utf8.encode("my-mint-seed"))], + program.programId ); - const [myPda, bump] = await PublicKey.findProgramAddress( + const [myPda, token_bump] = await PublicKey.findProgramAddress( [Buffer.from(anchor.utils.bytes.utf8.encode("my-token-seed"))], program.programId ); - await program.rpc.testTokenSeedsInit(bump, { + await program.rpc.testTokenSeedsInit(token_bump, mint_bump, { accounts: { myPda, - mint: mint.publicKey, + mint, authority: program.provider.wallet.publicKey, systemProgram: anchor.web3.SystemProgram.programId, rent: anchor.web3.SYSVAR_RENT_PUBKEY, @@ -481,12 +477,18 @@ describe("misc", () => { }, }); - const account = await mint.getAccountInfo(myPda); + const mintAccount = new Token( + program.provider.connection, + mint, + TOKEN_PROGRAM_ID, + program.provider.wallet.payer + ); + const account = await mintAccount.getAccountInfo(myPda); assert.ok(account.state === 1); assert.ok(account.amount.toNumber() === 0); assert.ok(account.isInitialized); assert.ok(account.owner.equals(program.provider.wallet.publicKey)); - assert.ok(account.mint.equals(mint.publicKey)); + assert.ok(account.mint.equals(mint)); }); it("Can execute a fallback function", async () => { diff --git a/lang/syn/src/codegen/accounts/constraints.rs b/lang/syn/src/codegen/accounts/constraints.rs index 10b1aeefbd..dc1432580f 100644 --- a/lang/syn/src/codegen/accounts/constraints.rs +++ b/lang/syn/src/codegen/accounts/constraints.rs @@ -464,27 +464,6 @@ pub fn generate_pda( let field = &f.ident; let (account_ty, account_wrapper_ty, is_zero_copy) = parse_ty(f); - let space = match space { - // If no explicit space param was given, serialize the type to bytes - // and take the length (with +8 for the discriminator.) - None => match is_zero_copy { - false => { - quote! { - let space = 8 + #account_ty::default().try_to_vec().unwrap().len(); - } - } - true => { - quote! { - let space = 8 + anchor_lang::__private::bytemuck::bytes_of(&#account_ty::default()).len(); - } - } - }, - // Explicit account size given. Use it. - Some(s) => quote! { - let space = #s; - }, - }; - let nonce_assignment = match assign_nonce { false => quote! {}, true => match &f.ty { @@ -525,7 +504,6 @@ pub fn generate_pda( match kind { PdaKind::Token { owner, mint } => quote! { let #field: #combined_account_ty = { - #space #payer #seeds_constraint @@ -534,64 +512,102 @@ pub fn generate_pda( .minimum_balance(anchor_spl::token::TokenAccount::LEN) .max(1) .saturating_sub(#field.to_account_info().lamports()); - if required_lamports > 0 { - anchor_lang::solana_program::program::invoke( - &anchor_lang::solana_program::system_instruction::transfer( - payer.to_account_info().key, - #field.to_account_info().key, - required_lamports, - ), - &[ - payer.to_account_info(), - #field.to_account_info(), - system_program.to_account_info().clone(), - ], - )?; - } - // Allocate space. + // Create the token account with right amount of lamports and space, and the correct owner. anchor_lang::solana_program::program::invoke_signed( - &anchor_lang::solana_program::system_instruction::allocate( + &anchor_lang::solana_program::system_instruction::create_account( + payer.to_account_info().key, #field.to_account_info().key, + required_lamports, anchor_spl::token::TokenAccount::LEN as u64, + token_program.to_account_info().key, ), &[ + payer.to_account_info(), #field.to_account_info(), - system_program.clone(), + system_program.to_account_info().clone(), ], &[&#seeds_with_nonce[..]], )?; - // Assign to the spl token program. - let __ix = anchor_lang::solana_program::system_instruction::assign( - #field.to_account_info().key, - token_program.to_account_info().key, - ); + // Initialize the token account. + let cpi_program = token_program.to_account_info(); + let accounts = anchor_spl::token::InitializeAccount { + account: #field.to_account_info(), + mint: #mint.to_account_info(), + authority: #owner.to_account_info(), + rent: rent.to_account_info(), + }; + let cpi_ctx = CpiContext::new(cpi_program, accounts); + anchor_spl::token::initialize_account(cpi_ctx)?; + anchor_lang::CpiAccount::try_from_init( + &#field.to_account_info(), + )? + }; + }, + PdaKind::Mint { owner, decimals } => quote! { + let #field: #combined_account_ty = { + #payer + #seeds_constraint + + // Fund the account for rent exemption. + let required_lamports = rent + .minimum_balance(anchor_spl::token::Mint::LEN) + .max(1) + .saturating_sub(#field.to_account_info().lamports()); + + // Create the token account with right amount of lamports and space, and the correct owner. anchor_lang::solana_program::program::invoke_signed( - &__ix, + &anchor_lang::solana_program::system_instruction::create_account( + payer.to_account_info().key, + #field.to_account_info().key, + required_lamports, + anchor_spl::token::Mint::LEN as u64, + token_program.to_account_info().key, + ), &[ + payer.to_account_info(), #field.to_account_info(), - system_program.to_account_info(), + system_program.to_account_info().clone(), ], &[&#seeds_with_nonce[..]], )?; - // Initialize the token account. + // Initialize the mint account. let cpi_program = token_program.to_account_info(); - let accounts = anchor_spl::token::InitializeAccount { - account: #field.to_account_info(), - mint: #mint.to_account_info(), - authority: #owner.to_account_info(), + let accounts = anchor_spl::token::InitializeMint { + mint: #field.to_account_info(), rent: rent.to_account_info(), }; let cpi_ctx = CpiContext::new(cpi_program, accounts); - anchor_spl::token::initialize_account(cpi_ctx)?; + anchor_spl::token::initialize_mint(cpi_ctx, #decimals, &#owner.to_account_info().key, None)?; anchor_lang::CpiAccount::try_from_init( &#field.to_account_info(), )? }; }, PdaKind::Program { owner } => { + let space = match space { + // If no explicit space param was given, serialize the type to bytes + // and take the length (with +8 for the discriminator.) + None => match is_zero_copy { + false => { + quote! { + let space = 8 + #account_ty::default().try_to_vec().unwrap().len(); + } + } + true => { + quote! { + let space = 8 + anchor_lang::__private::bytemuck::bytes_of(&#account_ty::default()).len(); + } + } + }, + // Explicit account size given. Use it. + Some(s) => quote! { + let space = #s; + }, + }; + // Owner of the account being created. If not specified, // default to the currently executing program. let owner = match owner { diff --git a/lang/syn/src/lib.rs b/lang/syn/src/lib.rs index ae354a85dc..d5a0a0f916 100644 --- a/lang/syn/src/lib.rs +++ b/lang/syn/src/lib.rs @@ -341,6 +341,8 @@ pub enum ConstraintToken { Address(Context), TokenMint(Context), TokenAuthority(Context), + MintAuthority(Context), + MintDecimals(Context), Bump(Context), } @@ -448,6 +450,7 @@ pub struct ConstraintAssociatedSpace { pub enum PdaKind { Program { owner: Option }, Token { owner: Expr, mint: Expr }, + Mint { owner: Expr, decimals: Expr }, } #[derive(Debug, Clone)] @@ -465,6 +468,16 @@ pub struct ConstraintTokenAuthority { auth: Expr, } +#[derive(Debug, Clone)] +pub struct ConstraintMintAuthority { + mint_auth: Expr, +} + +#[derive(Debug, Clone)] +pub struct ConstraintMintDecimals { + decimals: Expr, +} + #[derive(Debug, Clone)] pub struct ConstraintTokenBump { bump: Expr, diff --git a/lang/syn/src/parser/accounts/constraints.rs b/lang/syn/src/parser/accounts/constraints.rs index 06ad6a7ef1..2ed41a3ff2 100644 --- a/lang/syn/src/parser/accounts/constraints.rs +++ b/lang/syn/src/parser/accounts/constraints.rs @@ -67,6 +67,60 @@ pub fn parse_token(stream: ParseStream) -> ParseResult { "executable" => { ConstraintToken::Executable(Context::new(ident.span(), ConstraintExecutable {})) } + "mint" => { + stream.parse::()?; + stream.parse::()?; + let kw = stream.call(Ident::parse_any)?.to_string(); + stream.parse::()?; + + let span = ident + .span() + .join(stream.span()) + .unwrap_or_else(|| ident.span()); + + match kw.as_str() { + "authority" => ConstraintToken::MintAuthority(Context::new( + span, + ConstraintMintAuthority { + mint_auth: stream.parse()?, + }, + )), + "decimals" => ConstraintToken::MintDecimals(Context::new( + span, + ConstraintMintDecimals { + decimals: stream.parse()?, + }, + )), + _ => return Err(ParseError::new(ident.span(), "Invalid attribute")), + } + } + "token" => { + stream.parse::()?; + stream.parse::()?; + let kw = stream.call(Ident::parse_any)?.to_string(); + stream.parse::()?; + + let span = ident + .span() + .join(stream.span()) + .unwrap_or_else(|| ident.span()); + + match kw.as_str() { + "mint" => ConstraintToken::TokenMint(Context::new( + span, + ConstraintTokenMint { + mint: stream.parse()?, + }, + )), + "authority" => ConstraintToken::TokenAuthority(Context::new( + span, + ConstraintTokenAuthority { + auth: stream.parse()?, + }, + )), + _ => return Err(ParseError::new(ident.span(), "Invalid attribute")), + } + } _ => { stream.parse::()?; let span = ident @@ -164,18 +218,6 @@ pub fn parse_token(stream: ParseStream) -> ParseResult { address: stream.parse()?, }, )), - "token" => ConstraintToken::TokenMint(Context::new( - ident.span(), - ConstraintTokenMint { - mint: stream.parse()?, - }, - )), - "authority" => ConstraintToken::TokenAuthority(Context::new( - ident.span(), - ConstraintTokenAuthority { - auth: stream.parse()?, - }, - )), "bump" => ConstraintToken::Bump(Context::new( ident.span(), ConstraintTokenBump { @@ -212,6 +254,8 @@ pub struct ConstraintGroupBuilder<'ty> { pub address: Option>, pub token_mint: Option>, pub token_authority: Option>, + pub mint_authority: Option>, + pub mint_decimals: Option>, pub bump: Option>, } @@ -238,6 +282,8 @@ impl<'ty> ConstraintGroupBuilder<'ty> { address: None, token_mint: None, token_authority: None, + mint_authority: None, + mint_decimals: None, bump: None, } } @@ -260,6 +306,8 @@ impl<'ty> ConstraintGroupBuilder<'ty> { .replace(Context::new(i.span(), ConstraintRentExempt::Enforce)); } } + + // Seeds. if let Some(i) = &self.seeds { if self.init.is_some() && self.associated_payer.is_none() { return Err(ParseError::new( @@ -269,7 +317,15 @@ impl<'ty> ConstraintGroupBuilder<'ty> { } } + // Token. if let Some(token_mint) = &self.token_mint { + if self.token_authority.is_none() { + return Err(ParseError::new( + token_mint.span(), + "token authority must be provided if token mint is", + )); + } + if self.init.is_none() || (self.associated.is_none() && self.seeds.is_none()) { return Err(ParseError::new( token_mint.span(), @@ -277,6 +333,45 @@ impl<'ty> ConstraintGroupBuilder<'ty> { )); } } + if let Some(token_authority) = &self.token_authority { + if self.token_mint.is_none() { + return Err(ParseError::new( + token_authority.span(), + "token authority must be provided if token mint is", + )); + } + } + + // Mint. + if let Some(mint_decimals) = &self.mint_decimals { + if self.mint_authority.is_none() { + return Err(ParseError::new( + mint_decimals.span(), + "mint authority must be provided if mint decimals is", + )); + } + } + if let Some(mint_authority) = &self.mint_authority { + if self.mint_decimals.is_none() { + return Err(ParseError::new( + mint_authority.span(), + "mint decimals must be provided if mint authority is", + )); + } + } + + // SPL Space. + if self.init.is_some() + && self.seeds.is_some() + && self.token_mint.is_some() + && (self.mint_authority.is_some() || self.token_authority.is_some()) + && self.associated_space.is_some() + { + return Err(ParseError::new( + self.associated_space.as_ref().unwrap().span(), + "space is not required for initializing an spl account", + )); + } let ConstraintGroupBuilder { f_ty: _, @@ -299,6 +394,8 @@ impl<'ty> ConstraintGroupBuilder<'ty> { address, token_mint, token_authority, + mint_authority, + mint_decimals, bump, } = self; @@ -343,21 +440,34 @@ impl<'ty> ConstraintGroupBuilder<'ty> { seeds: c.into_inner().seeds, payer: into_inner!(associated_payer.clone()).map(|a| a.target), space: associated_space.clone().map(|s| s.space.clone()), - kind: match &token_mint { - None => PdaKind::Program { - owner: pda_owner.clone(), - }, - Some(tm) => PdaKind::Token { - mint: tm.clone().into_inner().mint, - owner: match &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" - )), + kind: if let Some(tm) = &token_mint { + PdaKind::Token { + mint: tm.clone().into_inner().mint, + owner: match &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 { + PdaKind::Mint { + decimals: d.clone().into_inner().decimals, + owner: match &mint_authority { + Some(a) => a.clone().into_inner().mint_auth, + None => return Err(ParseError::new( + d.span(), + "authority must be provided to initialize a mint program derived address" + )) + + } + } + } else { + PdaKind::Program { + owner: pda_owner.clone(), + } }, - }, - }, bump: into_inner!(bump).map(|b| b.bump), }) }) @@ -407,6 +517,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::MintAuthority(c) => self.add_mint_authority(c), + ConstraintToken::MintDecimals(c) => self.add_mint_decimals(c), ConstraintToken::Bump(c) => self.add_bump(c), } } @@ -484,16 +596,44 @@ impl<'ty> ConstraintGroupBuilder<'ty> { "token authority already provided", )); } - if self.token_mint.is_none() { + if self.init.is_none() { return Err(ParseError::new( c.span(), - "token must bne provided before authority", + "init must be provided before token authority", )); } self.token_authority.replace(c); Ok(()) } + fn add_mint_authority(&mut self, c: Context) -> ParseResult<()> { + if self.mint_authority.is_some() { + return Err(ParseError::new(c.span(), "mint authority already provided")); + } + if self.init.is_none() { + return Err(ParseError::new( + c.span(), + "init must be provided before mint authority", + )); + } + self.mint_authority.replace(c); + Ok(()) + } + + fn add_mint_decimals(&mut self, c: Context) -> ParseResult<()> { + if self.mint_decimals.is_some() { + return Err(ParseError::new(c.span(), "mint decimals already provided")); + } + if self.init.is_none() { + return Err(ParseError::new( + c.span(), + "init must be provided before mint decimals", + )); + } + self.mint_decimals.replace(c); + Ok(()) + } + fn add_mut(&mut self, c: Context) -> ParseResult<()> { if self.mutable.is_some() { return Err(ParseError::new(c.span(), "mut already provided")); diff --git a/spl/src/token.rs b/spl/src/token.rs index 3abd4fdab6..e1fabdaaf0 100644 --- a/spl/src/token.rs +++ b/spl/src/token.rs @@ -256,6 +256,10 @@ impl Deref for TokenAccount { #[derive(Clone)] pub struct Mint(spl_token::state::Mint); +impl Mint { + pub const LEN: usize = spl_token::state::Mint::LEN; +} + impl anchor_lang::AccountDeserialize for Mint { fn try_deserialize(buf: &mut &[u8]) -> Result { Mint::try_deserialize_unchecked(buf)