diff --git a/CHANGELOG.md b/CHANGELOG.md index 070c67392..56ad1f91e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,12 +14,14 @@ incremented for features. ### Features * lang: Add `seeds::program` constraint for specifying which program_id to use when deriving PDAs.([#1197](https://github.com/project-serum/anchor/pull/1197)) -* ts: Remove error logging in the event parser when log websocket encounters a program error. ([#1313](https://github.com/project-serum/anchor/pull/1313)) +* lang: `Context` now has a new `bumps: BTree` argument, mapping account name to bump seed "found" by the accounts context. This allows one to access bump seeds without having to pass them in from the client or recalculate them in the handler ([#1367](calculated )). +* ts: Remove error logging in the event parser when log websocket encounters a program error ([#1313](https://github.com/project-serum/anchor/pull/1313)). * ts: Add new `methods` namespace to the program client, introducing a more ergonomic builder API ([#1324](https://github.com/project-serum/anchor/pull/1324)). ### Breaking * lang: rename `loader_account` module to `account_loader` module ([#1279](https://github.com/project-serum/anchor/pull/1279)) +* lang: The `Accounts` trait's `try_accounts` method now has an additional `bumps: &mut BTreeMap` argument, which accumulates bump seeds ([#1367](https://github.com/project-serum/anchor/pull/1367)). * ts: `Coder` is now an interface and the existing class has been renamed to `BorshCoder`. This change allows the generation of Anchor clients for non anchor programs ([#1259](https://github.com/project-serum/anchor/pull/1259/files)). ## [0.20.1] - 2022-01-09 diff --git a/lang/src/accounts/account.rs b/lang/src/accounts/account.rs index ab357410f..cb2e3703a 100644 --- a/lang/src/accounts/account.rs +++ b/lang/src/accounts/account.rs @@ -7,6 +7,7 @@ use solana_program::entrypoint::ProgramResult; use solana_program::instruction::AccountMeta; use solana_program::program_error::ProgramError; use solana_program::pubkey::Pubkey; +use std::collections::BTreeMap; use std::fmt; use std::ops::{Deref, DerefMut}; @@ -314,6 +315,7 @@ where _program_id: &Pubkey, accounts: &mut &[AccountInfo<'info>], _ix_data: &[u8], + _bumps: &mut BTreeMap, ) -> Result { if accounts.is_empty() { return Err(ErrorCode::AccountNotEnoughKeys.into()); diff --git a/lang/src/accounts/account_info.rs b/lang/src/accounts/account_info.rs index 4de5be1ee..186c4848a 100644 --- a/lang/src/accounts/account_info.rs +++ b/lang/src/accounts/account_info.rs @@ -8,12 +8,14 @@ use solana_program::account_info::AccountInfo; use solana_program::instruction::AccountMeta; use solana_program::program_error::ProgramError; use solana_program::pubkey::Pubkey; +use std::collections::BTreeMap; impl<'info> Accounts<'info> for AccountInfo<'info> { fn try_accounts( _program_id: &Pubkey, accounts: &mut &[AccountInfo<'info>], _ix_data: &[u8], + _bumps: &mut BTreeMap, ) -> Result { if accounts.is_empty() { return Err(ErrorCode::AccountNotEnoughKeys.into()); diff --git a/lang/src/accounts/account_loader.rs b/lang/src/accounts/account_loader.rs index 4e4a8a7f1..4e2e81c44 100644 --- a/lang/src/accounts/account_loader.rs +++ b/lang/src/accounts/account_loader.rs @@ -12,6 +12,7 @@ use solana_program::instruction::AccountMeta; use solana_program::program_error::ProgramError; use solana_program::pubkey::Pubkey; use std::cell::{Ref, RefMut}; +use std::collections::BTreeMap; use std::fmt; use std::io::Write; use std::marker::PhantomData; @@ -211,6 +212,7 @@ impl<'info, T: ZeroCopy + Owner> Accounts<'info> for AccountLoader<'info, T> { _program_id: &Pubkey, accounts: &mut &[AccountInfo<'info>], _ix_data: &[u8], + _bumps: &mut BTreeMap, ) -> Result { if accounts.is_empty() { return Err(ErrorCode::AccountNotEnoughKeys.into()); diff --git a/lang/src/accounts/boxed.rs b/lang/src/accounts/boxed.rs index 7bfb5738b..5282fb19f 100644 --- a/lang/src/accounts/boxed.rs +++ b/lang/src/accounts/boxed.rs @@ -19,6 +19,7 @@ use solana_program::entrypoint::ProgramResult; use solana_program::instruction::AccountMeta; use solana_program::program_error::ProgramError; use solana_program::pubkey::Pubkey; +use std::collections::BTreeMap; use std::ops::Deref; impl<'info, T: Accounts<'info>> Accounts<'info> for Box { @@ -26,8 +27,9 @@ impl<'info, T: Accounts<'info>> Accounts<'info> for Box { program_id: &Pubkey, accounts: &mut &[AccountInfo<'info>], ix_data: &[u8], + bumps: &mut BTreeMap, ) -> Result { - T::try_accounts(program_id, accounts, ix_data).map(Box::new) + T::try_accounts(program_id, accounts, ix_data, bumps).map(Box::new) } } diff --git a/lang/src/accounts/cpi_account.rs b/lang/src/accounts/cpi_account.rs index 5efae03e6..eeff02a91 100644 --- a/lang/src/accounts/cpi_account.rs +++ b/lang/src/accounts/cpi_account.rs @@ -5,6 +5,7 @@ use solana_program::entrypoint::ProgramResult; use solana_program::instruction::AccountMeta; use solana_program::program_error::ProgramError; use solana_program::pubkey::Pubkey; +use std::collections::BTreeMap; use std::ops::{Deref, DerefMut}; /// Container for any account *not* owned by the current program. @@ -53,6 +54,7 @@ where _program_id: &Pubkey, accounts: &mut &[AccountInfo<'info>], _ix_data: &[u8], + _bumps: &mut BTreeMap, ) -> Result { if accounts.is_empty() { return Err(ErrorCode::AccountNotEnoughKeys.into()); diff --git a/lang/src/accounts/cpi_state.rs b/lang/src/accounts/cpi_state.rs index 38261a53b..3fba2284e 100644 --- a/lang/src/accounts/cpi_state.rs +++ b/lang/src/accounts/cpi_state.rs @@ -8,6 +8,7 @@ use solana_program::account_info::AccountInfo; use solana_program::instruction::AccountMeta; use solana_program::program_error::ProgramError; use solana_program::pubkey::Pubkey; +use std::collections::BTreeMap; use std::ops::{Deref, DerefMut}; /// Boxed container for the program state singleton, used when the state @@ -70,6 +71,7 @@ where _program_id: &Pubkey, accounts: &mut &[AccountInfo<'info>], _ix_data: &[u8], + _bumps: &mut BTreeMap, ) -> Result { if accounts.is_empty() { return Err(ErrorCode::AccountNotEnoughKeys.into()); diff --git a/lang/src/accounts/loader.rs b/lang/src/accounts/loader.rs index d436d5298..f60bbd71c 100644 --- a/lang/src/accounts/loader.rs +++ b/lang/src/accounts/loader.rs @@ -9,6 +9,7 @@ use solana_program::instruction::AccountMeta; use solana_program::program_error::ProgramError; use solana_program::pubkey::Pubkey; use std::cell::{Ref, RefMut}; +use std::collections::BTreeMap; use std::fmt; use std::io::Write; use std::marker::PhantomData; @@ -150,6 +151,7 @@ impl<'info, T: ZeroCopy> Accounts<'info> for Loader<'info, T> { program_id: &Pubkey, accounts: &mut &[AccountInfo<'info>], _ix_data: &[u8], + _bumps: &mut BTreeMap, ) -> Result { if accounts.is_empty() { return Err(ErrorCode::AccountNotEnoughKeys.into()); diff --git a/lang/src/accounts/program.rs b/lang/src/accounts/program.rs index 31b966355..f31b11a06 100644 --- a/lang/src/accounts/program.rs +++ b/lang/src/accounts/program.rs @@ -7,6 +7,7 @@ use solana_program::bpf_loader_upgradeable::{self, UpgradeableLoaderState}; use solana_program::instruction::AccountMeta; use solana_program::program_error::ProgramError; use solana_program::pubkey::Pubkey; +use std::collections::BTreeMap; use std::fmt; use std::marker::PhantomData; use std::ops::Deref; @@ -135,6 +136,7 @@ where _program_id: &Pubkey, accounts: &mut &[AccountInfo<'info>], _ix_data: &[u8], + _bumps: &mut BTreeMap, ) -> Result { if accounts.is_empty() { return Err(ErrorCode::AccountNotEnoughKeys.into()); diff --git a/lang/src/accounts/program_account.rs b/lang/src/accounts/program_account.rs index 67504f548..7a76770f7 100644 --- a/lang/src/accounts/program_account.rs +++ b/lang/src/accounts/program_account.rs @@ -10,6 +10,7 @@ use solana_program::entrypoint::ProgramResult; use solana_program::instruction::AccountMeta; use solana_program::program_error::ProgramError; use solana_program::pubkey::Pubkey; +use std::collections::BTreeMap; use std::ops::{Deref, DerefMut}; /// Boxed container for a deserialized `account`. Use this to reference any @@ -83,6 +84,7 @@ where program_id: &Pubkey, accounts: &mut &[AccountInfo<'info>], _ix_data: &[u8], + _bumps: &mut BTreeMap, ) -> Result { if accounts.is_empty() { return Err(ErrorCode::AccountNotEnoughKeys.into()); diff --git a/lang/src/accounts/signer.rs b/lang/src/accounts/signer.rs index f05b74799..fd05e7063 100644 --- a/lang/src/accounts/signer.rs +++ b/lang/src/accounts/signer.rs @@ -5,6 +5,7 @@ use solana_program::account_info::AccountInfo; use solana_program::instruction::AccountMeta; use solana_program::program_error::ProgramError; use solana_program::pubkey::Pubkey; +use std::collections::BTreeMap; use std::ops::Deref; /// Type validating that the account signed the transaction. No other ownership @@ -60,6 +61,7 @@ impl<'info> Accounts<'info> for Signer<'info> { _program_id: &Pubkey, accounts: &mut &[AccountInfo<'info>], _ix_data: &[u8], + _bumps: &mut BTreeMap, ) -> Result { if accounts.is_empty() { return Err(ErrorCode::AccountNotEnoughKeys.into()); diff --git a/lang/src/accounts/state.rs b/lang/src/accounts/state.rs index 010c15731..20dd35a19 100644 --- a/lang/src/accounts/state.rs +++ b/lang/src/accounts/state.rs @@ -10,6 +10,7 @@ use solana_program::entrypoint::ProgramResult; use solana_program::instruction::AccountMeta; use solana_program::program_error::ProgramError; use solana_program::pubkey::Pubkey; +use std::collections::BTreeMap; use std::ops::{Deref, DerefMut}; pub const PROGRAM_STATE_SEED: &str = "unversioned"; @@ -75,6 +76,7 @@ where program_id: &Pubkey, accounts: &mut &[AccountInfo<'info>], _ix_data: &[u8], + _bumps: &mut BTreeMap, ) -> Result { if accounts.is_empty() { return Err(ErrorCode::AccountNotEnoughKeys.into()); diff --git a/lang/src/accounts/system_account.rs b/lang/src/accounts/system_account.rs index 4e2d9415e..8495834bc 100644 --- a/lang/src/accounts/system_account.rs +++ b/lang/src/accounts/system_account.rs @@ -7,6 +7,7 @@ use solana_program::instruction::AccountMeta; use solana_program::program_error::ProgramError; use solana_program::pubkey::Pubkey; use solana_program::system_program; +use std::collections::BTreeMap; use std::ops::Deref; /// Type validating that the account is owned by the system program @@ -39,6 +40,7 @@ impl<'info> Accounts<'info> for SystemAccount<'info> { _program_id: &Pubkey, accounts: &mut &[AccountInfo<'info>], _ix_data: &[u8], + _bumps: &mut BTreeMap, ) -> Result { if accounts.is_empty() { return Err(ErrorCode::AccountNotEnoughKeys.into()); diff --git a/lang/src/accounts/sysvar.rs b/lang/src/accounts/sysvar.rs index 2cb730d18..024264a1e 100644 --- a/lang/src/accounts/sysvar.rs +++ b/lang/src/accounts/sysvar.rs @@ -6,6 +6,7 @@ use solana_program::account_info::AccountInfo; use solana_program::instruction::AccountMeta; use solana_program::program_error::ProgramError; use solana_program::pubkey::Pubkey; +use std::collections::BTreeMap; use std::fmt; use std::ops::{Deref, DerefMut}; @@ -69,6 +70,7 @@ impl<'info, T: solana_program::sysvar::Sysvar> Accounts<'info> for Sysvar<'info, _program_id: &Pubkey, accounts: &mut &[AccountInfo<'info>], _ix_data: &[u8], + _bumps: &mut BTreeMap, ) -> Result { if accounts.is_empty() { return Err(ErrorCode::AccountNotEnoughKeys.into()); diff --git a/lang/src/accounts/unchecked_account.rs b/lang/src/accounts/unchecked_account.rs index 1df81aeb8..7da7ea158 100644 --- a/lang/src/accounts/unchecked_account.rs +++ b/lang/src/accounts/unchecked_account.rs @@ -7,6 +7,7 @@ use solana_program::account_info::AccountInfo; use solana_program::instruction::AccountMeta; use solana_program::program_error::ProgramError; use solana_program::pubkey::Pubkey; +use std::collections::BTreeMap; use std::ops::Deref; /// Explicit wrapper for AccountInfo types to emphasize @@ -25,6 +26,7 @@ impl<'info> Accounts<'info> for UncheckedAccount<'info> { _program_id: &Pubkey, accounts: &mut &[AccountInfo<'info>], _ix_data: &[u8], + _bumps: &mut BTreeMap, ) -> Result { if accounts.is_empty() { return Err(ErrorCode::AccountNotEnoughKeys.into()); diff --git a/lang/src/context.rs b/lang/src/context.rs index ccf24e467..736f248c9 100644 --- a/lang/src/context.rs +++ b/lang/src/context.rs @@ -4,6 +4,7 @@ use crate::{Accounts, ToAccountInfos, ToAccountMetas}; use solana_program::account_info::AccountInfo; use solana_program::instruction::AccountMeta; use solana_program::pubkey::Pubkey; +use std::collections::BTreeMap; use std::fmt; /// Provides non-argument inputs to the program. @@ -29,6 +30,10 @@ pub struct Context<'a, 'b, 'c, 'info, T> { /// Remaining accounts given but not deserialized or validated. /// Be very careful when using this directly. pub remaining_accounts: &'c [AccountInfo<'info>], + /// Bump seeds found during constraint validation. This is provided as a + /// convenience so that handlers don't have to recalculate bump seeds or + /// pass them in as arguments. + pub bumps: BTreeMap, } impl<'a, 'b, 'c, 'info, T: fmt::Debug> fmt::Debug for Context<'a, 'b, 'c, 'info, T> { @@ -37,6 +42,7 @@ impl<'a, 'b, 'c, 'info, T: fmt::Debug> fmt::Debug for Context<'a, 'b, 'c, 'info, .field("program_id", &self.program_id) .field("accounts", &self.accounts) .field("remaining_accounts", &self.remaining_accounts) + .field("bumps", &self.bumps) .finish() } } @@ -46,11 +52,13 @@ impl<'a, 'b, 'c, 'info, T: Accounts<'info>> Context<'a, 'b, 'c, 'info, T> { program_id: &'a Pubkey, accounts: &'b mut T, remaining_accounts: &'c [AccountInfo<'info>], + bumps: BTreeMap, ) -> Self { Self { program_id, accounts, remaining_accounts, + bumps, } } } diff --git a/lang/src/lib.rs b/lang/src/lib.rs index ea79ad8dc..20e5497ae 100644 --- a/lang/src/lib.rs +++ b/lang/src/lib.rs @@ -29,6 +29,7 @@ use solana_program::entrypoint::ProgramResult; use solana_program::instruction::AccountMeta; use solana_program::program_error::ProgramError; use solana_program::pubkey::Pubkey; +use std::collections::BTreeMap; use std::io::Write; mod account_meta; @@ -80,6 +81,7 @@ pub trait Accounts<'info>: ToAccountMetas + ToAccountInfos<'info> + Sized { program_id: &Pubkey, accounts: &mut &[AccountInfo<'info>], ix_data: &[u8], + bumps: &mut BTreeMap, ) -> Result; } diff --git a/lang/src/vec.rs b/lang/src/vec.rs index 820c0c97a..c66f8f60f 100644 --- a/lang/src/vec.rs +++ b/lang/src/vec.rs @@ -3,6 +3,7 @@ use solana_program::account_info::AccountInfo; use solana_program::instruction::AccountMeta; use solana_program::program_error::ProgramError; use solana_program::pubkey::Pubkey; +use std::collections::BTreeMap; impl<'info, T: ToAccountInfos<'info>> ToAccountInfos<'info> for Vec { fn to_account_infos(&self) -> Vec> { @@ -25,9 +26,10 @@ impl<'info, T: Accounts<'info>> Accounts<'info> for Vec { program_id: &Pubkey, accounts: &mut &[AccountInfo<'info>], ix_data: &[u8], + bumps: &mut BTreeMap, ) -> Result { let mut vec: Vec = Vec::new(); - T::try_accounts(program_id, accounts, ix_data).map(|item| vec.push(item))?; + T::try_accounts(program_id, accounts, ix_data, bumps).map(|item| vec.push(item))?; Ok(vec) } } @@ -76,9 +78,10 @@ mod tests { false, Epoch::default(), ); - + let mut bumps = std::collections::BTreeMap::new(); let mut accounts = &[account1, account2][..]; - let parsed_accounts = Vec::::try_accounts(&program_id, &mut accounts, &[]).unwrap(); + let parsed_accounts = + Vec::::try_accounts(&program_id, &mut accounts, &[], &mut bumps).unwrap(); assert_eq!(accounts.len(), parsed_accounts.len()); } @@ -87,8 +90,8 @@ mod tests { #[should_panic] fn test_accounts_trait_for_vec_empty() { let program_id = Pubkey::default(); - + let mut bumps = std::collections::BTreeMap::new(); let mut accounts = &[][..]; - Vec::::try_accounts(&program_id, &mut accounts, &[]).unwrap(); + Vec::::try_accounts(&program_id, &mut accounts, &[], &mut bumps).unwrap(); } } diff --git a/lang/syn/src/codegen/accounts/constraints.rs b/lang/syn/src/codegen/accounts/constraints.rs index 473c31219..b8c1b6558 100644 --- a/lang/syn/src/codegen/accounts/constraints.rs +++ b/lang/syn/src/codegen/accounts/constraints.rs @@ -277,6 +277,16 @@ pub fn generate_constraint_rent_exempt( } fn generate_constraint_init_group(f: &Field, c: &ConstraintInitGroup) -> proc_macro2::TokenStream { + let field = &f.ident; + let ty_decl = f.ty_decl(); + let if_needed = if c.if_needed { + quote! {true} + } else { + quote! {false} + }; + let space = &c.space; + + // Payer for rent exemption. let payer = { let p = &c.payer; quote! { @@ -284,162 +294,56 @@ fn generate_constraint_init_group(f: &Field, c: &ConstraintInitGroup) -> proc_ma } }; - let seeds_with_nonce = match &c.seeds { - None => quote! {}, + // Convert from account info to account context wrapper type. + let from_account_info = f.from_account_info_unchecked(Some(&c.kind)); + + // PDA bump seeds. + let (find_pda, seeds_with_bump) = match &c.seeds { + None => (quote! {}, quote! {}), Some(c) => { - let s = &mut c.seeds.clone(); + let name_str = f.ident.to_string(); + let seeds = &mut c.seeds.clone(); + // If the seeds came with a trailing comma, we need to chop it off // before we interpolate them below. - if let Some(pair) = s.pop() { - s.push_value(pair.into_value()); + if let Some(pair) = seeds.pop() { + seeds.push_value(pair.into_value()); } - let maybe_seeds_plus_comma = (!s.is_empty()).then(|| { - quote! { #s, } + + let maybe_seeds_plus_comma = (!seeds.is_empty()).then(|| { + quote! { #seeds, } }); - let inner = match c.bump.as_ref() { - // Bump target not given. Use the canonical bump. - None => { - quote! { - [ - #maybe_seeds_plus_comma - &[ - Pubkey::find_program_address( - &[#s], - program_id, - ).1 - ][..] - ] - } - } - // Bump target given. Use it. - Some(b) => quote! { - [#maybe_seeds_plus_comma &[#b][..]] + + ( + quote! { + let (__pda_address, __bump) = Pubkey::find_program_address( + &[#maybe_seeds_plus_comma], + program_id, + ); + __bumps.insert(#name_str.to_string(), __bump); }, - }; - quote! { - &#inner[..] - } - } - }; - generate_init(f, c.if_needed, seeds_with_nonce, payer, &c.space, &c.kind) -} - -fn generate_constraint_seeds(f: &Field, c: &ConstraintSeedsGroup) -> proc_macro2::TokenStream { - let name = &f.ident; - let s = &mut c.seeds.clone(); - - let deriving_program_id = c - .program_seed - .clone() - // If they specified a seeds::program to use when deriving the PDA, use it. - .map(|program_id| quote! { #program_id }) - // Otherwise fall back to the current program's program_id. - .unwrap_or(quote! { program_id }); - - // If the seeds came with a trailing comma, we need to chop it off - // before we interpolate them below. - if let Some(pair) = s.pop() { - s.push_value(pair.into_value()); - } - - // If the bump is provided with init *and target*, then force it to be the - // canonical bump. - if c.is_init && c.bump.is_some() { - let b = c.bump.as_ref().unwrap(); - quote! { - let (__program_signer, __bump) = anchor_lang::solana_program::pubkey::Pubkey::find_program_address( - &[#s], - &#deriving_program_id, - ); - if #name.key() != __program_signer { - return Err(anchor_lang::__private::ErrorCode::ConstraintSeeds.into()); - } - if __bump != #b { - return Err(anchor_lang::__private::ErrorCode::ConstraintSeeds.into()); - } - } - } else { - let maybe_seeds_plus_comma = (!s.is_empty()).then(|| { - quote! { #s, } - }); - let seeds = match c.bump.as_ref() { - // Bump target not given. Find it. - None => { quote! { - [ + &[ #maybe_seeds_plus_comma - &[ - Pubkey::find_program_address( - &[#s], - &#deriving_program_id, - ).1 - ][..] - ] - } - } - // Bump target given. Use it. - Some(b) => { - quote! { - [#maybe_seeds_plus_comma &[#b][..]] - } - } - }; - quote! { - let __program_signer = Pubkey::create_program_address( - &#seeds[..], - &#deriving_program_id, - ).map_err(|_| anchor_lang::__private::ErrorCode::ConstraintSeeds)?; - if #name.key() != __program_signer { - return Err(anchor_lang::__private::ErrorCode::ConstraintSeeds.into()); - } + &[__bump][..] + ][..] + }, + ) } - } -} - -fn generate_constraint_associated_token( - f: &Field, - c: &ConstraintAssociatedToken, -) -> proc_macro2::TokenStream { - let name = &f.ident; - let wallet_address = &c.wallet; - let spl_token_mint_address = &c.mint; - quote! { - if #name.owner != #wallet_address.key() { - return Err(anchor_lang::__private::ErrorCode::ConstraintTokenOwner.into()); - } - let __associated_token_address = anchor_spl::associated_token::get_associated_token_address(&#wallet_address.key(), &#spl_token_mint_address.key()); - if #name.key() != __associated_token_address { - return Err(anchor_lang::__private::ErrorCode::ConstraintAssociated.into()); - } - } -} - -// `if_needed` is set if account allocation and initialization is optional. -pub fn generate_init( - f: &Field, - if_needed: bool, - seeds_with_nonce: proc_macro2::TokenStream, - payer: proc_macro2::TokenStream, - space: &Option, - kind: &InitKind, -) -> proc_macro2::TokenStream { - let field = &f.ident; - let ty_decl = f.ty_decl(); - let from_account_info = f.from_account_info_unchecked(Some(kind)); - let if_needed = if if_needed { - quote! {true} - } else { - quote! {false} }; - match kind { + + match &c.kind { InitKind::Token { owner, mint } => { let create_account = generate_create_account( field, quote! {anchor_spl::token::TokenAccount::LEN}, quote! {&token_program.key()}, - seeds_with_nonce, + seeds_with_bump, ); quote! { + // Define the bump and pda variable. + #find_pda + let #field: #ty_decl = { if !#if_needed || #field.as_ref().owner == &anchor_lang::solana_program::system_program::ID { // Define payer variable. @@ -475,6 +379,9 @@ pub fn generate_init( } InitKind::AssociatedToken { owner, mint } => { quote! { + // Define the bump and pda variable. + #find_pda + let #field: #ty_decl = { if !#if_needed || #field.as_ref().owner == &anchor_lang::solana_program::system_program::ID { #payer @@ -518,13 +425,16 @@ pub fn generate_init( field, quote! {anchor_spl::token::Mint::LEN}, quote! {&token_program.key()}, - seeds_with_nonce, + seeds_with_bump, ); let freeze_authority = match freeze_authority { Some(fa) => quote! { Option::<&anchor_lang::prelude::Pubkey>::Some(&#fa.key()) }, None => quote! { Option::<&anchor_lang::prelude::Pubkey>::None }, }; quote! { + // Define the bump and pda variable. + #find_pda + let #field: #ty_decl = { if !#if_needed || #field.as_ref().owner == &anchor_lang::solana_program::system_program::ID { // Define payer variable. @@ -562,6 +472,7 @@ pub fn generate_init( } } InitKind::Program { owner } => { + // Define the space variable. 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.) @@ -586,7 +497,7 @@ pub fn generate_init( }, }; - // Owner of the account being created. If not specified, + // Define the owner of the account being created. If not specified, // default to the currently executing program. let owner = match owner { None => quote! { @@ -596,31 +507,37 @@ pub fn generate_init( &#o }, }; - let pda_check = if !seeds_with_nonce.is_empty() { - quote! { - let expected_key = anchor_lang::prelude::Pubkey::create_program_address( - #seeds_with_nonce, - #owner - ).map_err(|_| anchor_lang::__private::ErrorCode::ConstraintSeeds)?; - if expected_key != #field.key() { - return Err(anchor_lang::__private::ErrorCode::ConstraintSeeds.into()); - } - } - } else { - quote! {} - }; + + // CPI to the system program to create the account. let create_account = - generate_create_account(field, quote! {space}, owner.clone(), seeds_with_nonce); + generate_create_account(field, quote! {space}, owner.clone(), seeds_with_bump); + + // Put it all together. quote! { + // Define the bump variable. + #find_pda + let #field = { let actual_field = #field.to_account_info(); let actual_owner = actual_field.owner; + + // Define the account space variable. #space + + // Create the account. Always do this in the event + // if needed is not specified or the system program is the owner. if !#if_needed || actual_owner == &anchor_lang::solana_program::system_program::ID { + // Define the payer variable. #payer + + // CPI to the system program to create. #create_account } + + // Convert from account info to account context wrapper type. let pa: #ty_decl = #from_account_info; + + // Assert the account was created correctly. if !(!#if_needed || actual_owner == &anchor_lang::solana_program::system_program::ID) { if space != actual_field.data_len() { return Err(anchor_lang::__private::ErrorCode::ConstraintSpace.into()); @@ -636,9 +553,9 @@ pub fn generate_init( return Err(anchor_lang::__private::ErrorCode::ConstraintRentExempt.into()); } } - - #pda_check } + + // Done. pa }; } @@ -646,6 +563,106 @@ pub fn generate_init( } } +fn generate_constraint_seeds(f: &Field, c: &ConstraintSeedsGroup) -> proc_macro2::TokenStream { + let name = &f.ident; + let name_str = name.to_string(); + + let s = &mut c.seeds.clone(); + + let deriving_program_id = c + .program_seed + .clone() + // If they specified a seeds::program to use when deriving the PDA, use it. + .map(|program_id| quote! { #program_id }) + // Otherwise fall back to the current program's program_id. + .unwrap_or(quote! { program_id }); + + // If the seeds came with a trailing comma, we need to chop it off + // before we interpolate them below. + if let Some(pair) = s.pop() { + s.push_value(pair.into_value()); + } + + // If the bump is provided with init *and target*, then force it to be the + // canonical bump. + // + // Note that for `#[account(init, seeds)]`, find_program_address has already + // been run in the init constraint. + if c.is_init && c.bump.is_some() { + let b = c.bump.as_ref().unwrap(); + quote! { + if #name.key() != __pda_address { + return Err(anchor_lang::__private::ErrorCode::ConstraintSeeds.into()); + } + if __bump != #b { + return Err(anchor_lang::__private::ErrorCode::ConstraintSeeds.into()); + } + } + } + // Init seeds but no bump. We already used the canonical to create bump so + // just check the address. + // + // Note that for `#[account(init, seeds)]`, find_program_address has already + // been run in the init constraint. + else if c.is_init { + quote! { + if #name.key() != __pda_address { + return Err(anchor_lang::__private::ErrorCode::ConstraintSeeds.into()); + } + } + } + // No init. So we just check the address. + else { + let maybe_seeds_plus_comma = (!s.is_empty()).then(|| { + quote! { #s, } + }); + let define_pda = match c.bump.as_ref() { + // Bump target not given. Find it. + None => quote! { + let (__pda_address, __bump) = Pubkey::find_program_address( + &[#maybe_seeds_plus_comma], + &#deriving_program_id, + ); + __bumps.insert(#name_str.to_string(), __bump); + }, + // Bump target given. Use it. + Some(b) => quote! { + let __pda_address = Pubkey::create_program_address( + &[#maybe_seeds_plus_comma &[#b][..]], + &#deriving_program_id, + ).map_err(|_| anchor_lang::__private::ErrorCode::ConstraintSeeds)?; + }, + }; + quote! { + // Define the PDA. + #define_pda + + // Check it. + if #name.key() != __pda_address { + return Err(anchor_lang::__private::ErrorCode::ConstraintSeeds.into()); + } + } + } +} + +fn generate_constraint_associated_token( + f: &Field, + c: &ConstraintAssociatedToken, +) -> proc_macro2::TokenStream { + let name = &f.ident; + let wallet_address = &c.wallet; + let spl_token_mint_address = &c.mint; + quote! { + if #name.owner != #wallet_address.key() { + return Err(anchor_lang::__private::ErrorCode::ConstraintTokenOwner.into()); + } + let __associated_token_address = anchor_spl::associated_token::get_associated_token_address(&#wallet_address.key(), &#spl_token_mint_address.key()); + if #name.key() != __associated_token_address { + return Err(anchor_lang::__private::ErrorCode::ConstraintAssociated.into()); + } + } +} + // Generated code to create an account with with system program with the // given `space` amount of data, owned by `owner`. // diff --git a/lang/syn/src/codegen/accounts/try_accounts.rs b/lang/syn/src/codegen/accounts/try_accounts.rs index b9c4e23e9..47d582e4b 100644 --- a/lang/syn/src/codegen/accounts/try_accounts.rs +++ b/lang/syn/src/codegen/accounts/try_accounts.rs @@ -25,7 +25,7 @@ pub fn generate(accs: &AccountsStruct) -> proc_macro2::TokenStream { quote! { #[cfg(feature = "anchor-debug")] ::solana_program::log::sol_log(stringify!(#name)); - let #name: #ty = anchor_lang::Accounts::try_accounts(program_id, accounts, ix_data)?; + let #name: #ty = anchor_lang::Accounts::try_accounts(program_id, accounts, ix_data, __bumps)?; } } AccountField::Field(f) => { @@ -43,7 +43,7 @@ pub fn generate(accs: &AccountsStruct) -> proc_macro2::TokenStream { quote! { #[cfg(feature = "anchor-debug")] ::solana_program::log::sol_log(stringify!(#name)); - let #name = anchor_lang::Accounts::try_accounts(program_id, accounts, ix_data)?; + let #name = anchor_lang::Accounts::try_accounts(program_id, accounts, ix_data, __bumps)?; } } } @@ -92,6 +92,7 @@ pub fn generate(accs: &AccountsStruct) -> proc_macro2::TokenStream { program_id: &anchor_lang::solana_program::pubkey::Pubkey, accounts: &mut &[anchor_lang::solana_program::account_info::AccountInfo<'info>], ix_data: &[u8], + __bumps: &mut std::collections::BTreeMap, ) -> std::result::Result { // Deserialize instruction, if declared. #ix_de diff --git a/lang/syn/src/codegen/program/handlers.rs b/lang/syn/src/codegen/program/handlers.rs index f622b6e13..7a23fcf8b 100644 --- a/lang/syn/src/codegen/program/handlers.rs +++ b/lang/syn/src/codegen/program/handlers.rs @@ -25,32 +25,37 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream { match ix { anchor_lang::idl::IdlInstruction::Create { data_len } => { + let mut bumps = std::collections::BTreeMap::new(); let mut accounts = - anchor_lang::idl::IdlCreateAccounts::try_accounts(program_id, &mut accounts, &[])?; + anchor_lang::idl::IdlCreateAccounts::try_accounts(program_id, &mut accounts, &[], &mut bumps)?; __idl_create_account(program_id, &mut accounts, data_len)?; accounts.exit(program_id)?; }, anchor_lang::idl::IdlInstruction::CreateBuffer => { + let mut bumps = std::collections::BTreeMap::new(); let mut accounts = - anchor_lang::idl::IdlCreateBuffer::try_accounts(program_id, &mut accounts, &[])?; + anchor_lang::idl::IdlCreateBuffer::try_accounts(program_id, &mut accounts, &[], &mut bumps)?; __idl_create_buffer(program_id, &mut accounts)?; accounts.exit(program_id)?; }, anchor_lang::idl::IdlInstruction::Write { data } => { + let mut bumps = std::collections::BTreeMap::new(); let mut accounts = - anchor_lang::idl::IdlAccounts::try_accounts(program_id, &mut accounts, &[])?; + anchor_lang::idl::IdlAccounts::try_accounts(program_id, &mut accounts, &[], &mut bumps)?; __idl_write(program_id, &mut accounts, data)?; accounts.exit(program_id)?; }, anchor_lang::idl::IdlInstruction::SetAuthority { new_authority } => { + let mut bumps = std::collections::BTreeMap::new(); let mut accounts = - anchor_lang::idl::IdlAccounts::try_accounts(program_id, &mut accounts, &[])?; + anchor_lang::idl::IdlAccounts::try_accounts(program_id, &mut accounts, &[], &mut bumps)?; __idl_set_authority(program_id, &mut accounts, new_authority)?; accounts.exit(program_id)?; }, anchor_lang::idl::IdlInstruction::SetBuffer => { + let mut bumps = std::collections::BTreeMap::new(); let mut accounts = - anchor_lang::idl::IdlSetBuffer::try_accounts(program_id, &mut accounts, &[])?; + anchor_lang::idl::IdlSetBuffer::try_accounts(program_id, &mut accounts, &[], &mut bumps)?; __idl_set_buffer(program_id, &mut accounts)?; accounts.exit(program_id)?; }, @@ -210,10 +215,14 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream { .map_err(|_| anchor_lang::__private::ErrorCode::InstructionDidNotDeserialize)?; let instruction::state::#variant_arm = ix; + let mut __bumps = std::collections::BTreeMap::new(); + // Deserialize accounts. let mut remaining_accounts: &[AccountInfo] = accounts; - let ctor_accounts = anchor_lang::__private::Ctor::try_accounts(program_id, &mut remaining_accounts, &[])?; - let mut ctor_user_def_accounts = #anchor_ident::try_accounts(program_id, &mut remaining_accounts, ix_data)?; + let ctor_accounts = + anchor_lang::__private::Ctor::try_accounts(program_id, &mut remaining_accounts, &[], &mut __bumps)?; + let mut ctor_user_def_accounts = + #anchor_ident::try_accounts(program_id, &mut remaining_accounts, ix_data, &mut __bumps)?; // Create the solana account for the ctor data. let from = ctor_accounts.from.key; @@ -258,6 +267,7 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream { program_id, &mut ctor_user_def_accounts, remaining_accounts, + __bumps, ), #(#ctor_untyped_args),* )?; @@ -284,10 +294,14 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream { .map_err(|_| anchor_lang::__private::ErrorCode::InstructionDidNotDeserialize)?; let instruction::state::#variant_arm = ix; + let mut __bumps = std::collections::BTreeMap::new(); + // Deserialize accounts. let mut remaining_accounts: &[AccountInfo] = accounts; - let ctor_accounts = anchor_lang::__private::Ctor::try_accounts(program_id, &mut remaining_accounts, &[])?; - let mut ctor_user_def_accounts = #anchor_ident::try_accounts(program_id, &mut remaining_accounts, ix_data)?; + let ctor_accounts = + anchor_lang::__private::Ctor::try_accounts(program_id, &mut remaining_accounts, &[], &mut __bumps)?; + let mut ctor_user_def_accounts = + #anchor_ident::try_accounts(program_id, &mut remaining_accounts, ix_data, &mut __bumps)?; // Invoke the ctor. let instance = #mod_name::#name::new( @@ -295,6 +309,7 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream { program_id, &mut ctor_user_def_accounts, remaining_accounts, + __bumps, ), #(#ctor_untyped_args),* )?; @@ -387,20 +402,30 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream { .map_err(|_| anchor_lang::__private::ErrorCode::InstructionDidNotDeserialize)?; let instruction::state::#variant_arm = ix; + // Bump collector. + let mut __bumps = std::collections::BTreeMap::new(); + // Load state. let mut remaining_accounts: &[AccountInfo] = accounts; if remaining_accounts.is_empty() { return Err(anchor_lang::__private::ErrorCode::AccountNotEnoughKeys.into()); } - let loader: anchor_lang::accounts::loader::Loader<#mod_name::#name> = anchor_lang::accounts::loader::Loader::try_accounts(program_id, &mut remaining_accounts, &[])?; + let loader: anchor_lang::accounts::loader::Loader<#mod_name::#name> = anchor_lang::accounts::loader::Loader::try_accounts(program_id, &mut remaining_accounts, &[], &mut __bumps)?; // Deserialize accounts. let mut accounts = #anchor_ident::try_accounts( program_id, &mut remaining_accounts, ix_data, + &mut __bumps, )?; - let ctx = anchor_lang::context::Context::new(program_id, &mut accounts, remaining_accounts); + let ctx = + anchor_lang::context::Context::new( + program_id, + &mut accounts, + remaining_accounts, + __bumps, + ); // Execute user defined function. { @@ -433,20 +458,35 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream { .map_err(|_| anchor_lang::__private::ErrorCode::InstructionDidNotDeserialize)?; let instruction::state::#variant_arm = ix; + // Bump collector. + let mut __bumps = std::collections::BTreeMap::new(); + // Load state. let mut remaining_accounts: &[AccountInfo] = accounts; if remaining_accounts.is_empty() { return Err(anchor_lang::__private::ErrorCode::AccountNotEnoughKeys.into()); } - let mut state: anchor_lang::accounts::state::ProgramState<#state_ty> = anchor_lang::accounts::state::ProgramState::try_accounts(program_id, &mut remaining_accounts, &[])?; + let mut state: anchor_lang::accounts::state::ProgramState<#state_ty> = anchor_lang::accounts::state::ProgramState::try_accounts( + program_id, + &mut remaining_accounts, + &[], + &mut __bumps, + )?; // Deserialize accounts. let mut accounts = #anchor_ident::try_accounts( program_id, &mut remaining_accounts, ix_data, + &mut __bumps, )?; - let ctx = anchor_lang::context::Context::new(program_id, &mut accounts, remaining_accounts); + let ctx = + anchor_lang::context::Context::new( + program_id, + &mut accounts, + remaining_accounts, + __bumps + ); // Execute user defined function. state.#ix_method_name( @@ -488,7 +528,7 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream { .map(|ix| { // Easy to implement. Just need to write a test. // Feel free to open a PR. - assert!(!state.is_zero_copy, "Trait implementations not yet implemented for zero copy state structs. Please file an issue."); + assert!(!state.is_zero_copy, "Trait implementations not yet implemented for zero copy state structs. Please file an issue."); let ix_arg_names: Vec<&syn::Ident> = ix.args.iter().map(|arg| &arg.name).collect(); @@ -546,20 +586,35 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream { // Deserialize instruction. #deserialize_instruction + // Bump collector. + let mut __bumps = std::collections::BTreeMap::new(); + // Deserialize the program state account. let mut remaining_accounts: &[AccountInfo] = accounts; if remaining_accounts.is_empty() { return Err(anchor_lang::__private::ErrorCode::AccountNotEnoughKeys.into()); } - let mut state: anchor_lang::accounts::state::ProgramState<#state_ty> = anchor_lang::accounts::state::ProgramState::try_accounts(program_id, &mut remaining_accounts, &[])?; + let mut state: anchor_lang::accounts::state::ProgramState<#state_ty> = anchor_lang::accounts::state::ProgramState::try_accounts( + program_id, + &mut remaining_accounts, + &[], + &mut __bumps, + )?; // Deserialize accounts. let mut accounts = #anchor_ident::try_accounts( program_id, &mut remaining_accounts, ix_data, + &mut __bumps, )?; - let ctx = anchor_lang::context::Context::new(program_id, &mut accounts, remaining_accounts); + let ctx = + anchor_lang::context::Context::new( + program_id, + &mut accounts, + remaining_accounts, + __bumps, + ); // Execute user defined function. state.#ix_method_name( @@ -593,17 +648,26 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream { // Deserialize instruction. #deserialize_instruction + // Bump collector. + let mut __bumps = std::collections::BTreeMap::new(); + // Deserialize accounts. let mut remaining_accounts: &[AccountInfo] = accounts; let mut accounts = #anchor_ident::try_accounts( program_id, &mut remaining_accounts, ix_data, + &mut __bumps, )?; // Execute user defined function. #state_name::#ix_method_name( - anchor_lang::context::Context::new(program_id, &mut accounts, remaining_accounts), + anchor_lang::context::Context::new( + program_id, + &mut accounts, + remaining_accounts, + __bumps + ), #(#ix_arg_names),* )?; @@ -644,17 +708,26 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream { .map_err(|_| anchor_lang::__private::ErrorCode::InstructionDidNotDeserialize)?; let instruction::#variant_arm = ix; + // Bump collector. + let mut __bumps = std::collections::BTreeMap::new(); + // Deserialize accounts. let mut remaining_accounts: &[AccountInfo] = accounts; let mut accounts = #anchor::try_accounts( program_id, &mut remaining_accounts, ix_data, + &mut __bumps, )?; // Invoke user defined handler. #program_name::#ix_method_name( - anchor_lang::context::Context::new(program_id, &mut accounts, remaining_accounts), + anchor_lang::context::Context::new( + program_id, + &mut accounts, + remaining_accounts, + __bumps, + ), #(#ix_arg_names),* )?; diff --git a/lang/syn/src/parser/accounts/constraints.rs b/lang/syn/src/parser/accounts/constraints.rs index 90fc98d8d..18d9b4f45 100644 --- a/lang/syn/src/parser/accounts/constraints.rs +++ b/lang/syn/src/parser/accounts/constraints.rs @@ -570,7 +570,7 @@ impl<'ty> ConstraintGroupBuilder<'ty> { }; Ok(ConstraintGroup { init: init.as_ref().map(|i| Ok(ConstraintInitGroup { - if_needed: i.if_needed, + if_needed: i.if_needed, seeds: seeds.clone(), payer: into_inner!(payer.clone()).map(|a| a.target), space: space.clone().map(|s| s.space.clone()), diff --git a/tests/auction-house b/tests/auction-house index fea2d89c2..09c911a53 160000 --- a/tests/auction-house +++ b/tests/auction-house @@ -1 +1 @@ -Subproject commit fea2d89c2b17ee39fcf0ebaadb0317b9e97206f4 +Subproject commit 09c911a53fd710d7d56972089c64de19d54ecf37 diff --git a/tests/cfo/deps/stake b/tests/cfo/deps/stake index bc73b5102..f04b2aaf8 160000 --- a/tests/cfo/deps/stake +++ b/tests/cfo/deps/stake @@ -1 +1 @@ -Subproject commit bc73b510285050f5a21da13c5bc3b8b4c959948f +Subproject commit f04b2aaf8817dac4c8d89e75eaaa0c099dfbf166