diff --git a/CHANGELOG.md b/CHANGELOG.md index ce2e81616..8982de8fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ incremented for features. * lang: `bump` must be provided when using the `seeds` constraint. This has been added as an extra safety constraint to ensure that whenever a PDA is initialized via a constraint the bump used is the one created by `Pubkey::find_program_address` ([#641](https://github.com/project-serum/anchor/pull/641)). * lang: `try_from_init` has been removed from `Loader`, `ProgramAccount`, and `CpiAccount` and replaced with `try_from_unchecked` ([#641](https://github.com/project-serum/anchor/pull/641)). * lang: Remove `AccountsInit` trait ([#641](https://github.com/project-serum/anchor/pull/641)). +* lang: `try_from` methods for `ProgramAccount`, `Loader`, and `ProgramState` now take in an additional `program_id: &Pubkey` parameter ([#660](https://github.com/project-serum/anchor/pull/660)). ## [0.13.2] - 2021-08-11 diff --git a/docs/src/.vuepress/config.js b/docs/src/.vuepress/config.js index 1f1cdea24..802569fac 100755 --- a/docs/src/.vuepress/config.js +++ b/docs/src/.vuepress/config.js @@ -64,7 +64,6 @@ module.exports = { "/tutorials/tutorial-2", "/tutorials/tutorial-3", "/tutorials/tutorial-4", - "/tutorials/tutorial-5", ], }, { diff --git a/docs/src/tutorials/tutorial-4.md b/docs/src/tutorials/tutorial-4.md index 7ee836eb5..14b3fb45f 100644 --- a/docs/src/tutorials/tutorial-4.md +++ b/docs/src/tutorials/tutorial-4.md @@ -1,84 +1,61 @@ -# State structs +# Errors -Up until now, we've treated programs on Solana as stateless, using accounts to persist -state between instruction invocations. In this tutorial, we'll give Solana programs the -illusion of state by introducing state structs, which define program account -singletons that can be operated over like any other account. - -## Clone the Repo - -To get started, clone the repo. - -```bash -git clone https://github.com/project-serum/anchor -``` - -And change directories to the [example](https://github.com/project-serum/anchor/tree/master/examples/tutorial/basic-4). - -```bash -cd anchor/examples/tutorial/basic-4 -``` +If you've ever programmed on a blockchain, you've probably been frustrated by +either non existant or opaque error codes. Anchor attempts to address this by +providing the `#[error]` attribute, which can be used to create typed Errors with +descriptive messages that automatically propagate to the client. ## Defining a Program -<<< @/../examples/tutorial/basic-4/programs/basic-4/src/lib.rs#code +For example, -Unlike the previous examples, all the instructions here not only take in an `Accounts` -struct, but they also operate over a mutable, global account marked by the `#[state]` -attribute. Every instruction defined in the corresponding `impl` block will have access -to this account, making it a great place to store global program state. +```rust +use anchor_lang::prelude::*; -### How it works +#[program] +mod errors { + use super::*; + pub fn hello(_ctx: Context) -> Result<()> { + Err(ErrorCode::Hello.into()) + } +} -We are able to give a program the illusion of state by adopting conventions in the framework. When invoking the `new` constructor, Anchor will automatically create a -program-owned account inside the program itself, invoking the system program's [create_account_with_seed](https://docs.rs/solana-program/1.5.5/solana_program/system_instruction/fn.create_account_with_seed.html) instruction, using `Pubkey::find_program_address(&[], program_id)` as the **base** and a deterministic string as the **seed** (the string doesn't -matter, as long as the framework is consistent). +#[derive(Accounts)] +pub struct Hello {} -This all has the effect of -giving the `#[state]` account a deterministic address, and so as long as all clients -and programs adopt this convention, programs can have the illusion of state in addition -to the full power of the lower level Solana accounts API. Of course, Anchor will handle this all for you, so you never have to worry about these details. +#[error] +pub enum ErrorCode { + #[msg("This is an error message clients will automatically display")] + Hello, +} +``` -## Using the client +Observe the [#[error]](https://docs.rs/anchor-lang/latest/anchor_lang/attr.error.html) attribute on the `ErrorCode` enum. This macro generates two types: an `Error` and a `Result`, both of which can be used when returning from your program. -### Invoke the constructor +To use the `Error`, you can simply use the user defined `ErrorCode` with Rust's [From](https://doc.rust-lang.org/std/convert/trait.From.html) trait. If you're unfamiliar with `From`, no worries. Just know that you need to either call +`.into()` when using your `ErrorCode`. Or use Rust's `?` operator, when returning an error. +Both of these will automatically convert *into* the correct `Error`. -To access the `#[state]` account and associated instructions, you can use the -`anchor.state` namespace on the client. For example, to invoke the constructor, +::: details +What's the deal with this From stuff? Well, because the Solana runtime expects a [ProgramError](https://docs.rs/solana-program/1.5.5/solana_program/program_error/enum.ProgramError.html) in the return value. The framework needs to wrap the user defined error code into a +`ProgramError::Code` variant, before returning. The alternative would be to use the +`ProgramError` directly. +::: -<<< @/../examples/tutorial/basic-4/tests/basic-4.js#ctor +## Using the Client -Note that the constructor can only be invoked once per program. All subsequent calls -to it will fail, since, as explained above, an account at a deterministic address -will be created. +When using the client, we get the error message. -### Fetch the state +```javascript +try { + const tx = await program.rpc.hello(); + assert.ok(false); +} catch (err) { + const errMsg = "This is an error message clients will automatically display"; + assert.equal(err.toString(), errMsg); +} +``` -To fetch the state account, +It's that easy. :) -<<< @/../examples/tutorial/basic-4/tests/basic-4.js#accessor - -### Invoke an instruction - -To invoke an instruction, - -<<< @/../examples/tutorial/basic-4/tests/basic-4.js#instruction - -## CPI - -Performing CPI from one Anchor program to another's state methods is very similar to performing CPI to normal Anchor instructions, except for two differences: - -1. All the generated instructions are located under the `::cpi::state` module. -2. You must use a [CpiStateContext](https://docs.rs/anchor-lang/latest/anchor_lang/struct.CpiStateContext.html), instead of a `[CpiContext](https://docs.rs/anchor-lang/latest/anchor_lang/struct.CpiContext.html). - -For a full example, see the `test_state_cpi` instruction, [here](https://github.com/project-serum/anchor/blob/master/examples/misc/programs/misc/src/lib.rs#L39). - -## Conclusion - -Using state structs is intuitive. However, due to the fact that accounts -on Solana have a fixed size, applications often need to use accounts -directly in addition to `#[state]` stucts. - -## Next Steps - -Next we'll discuss errors. +To run the full example, go [here](https://github.com/project-serum/anchor/tree/master/examples/errors). diff --git a/docs/src/tutorials/tutorial-5.md b/docs/src/tutorials/tutorial-5.md deleted file mode 100644 index 14b3fb45f..000000000 --- a/docs/src/tutorials/tutorial-5.md +++ /dev/null @@ -1,61 +0,0 @@ -# Errors - -If you've ever programmed on a blockchain, you've probably been frustrated by -either non existant or opaque error codes. Anchor attempts to address this by -providing the `#[error]` attribute, which can be used to create typed Errors with -descriptive messages that automatically propagate to the client. - -## Defining a Program - -For example, - -```rust -use anchor_lang::prelude::*; - -#[program] -mod errors { - use super::*; - pub fn hello(_ctx: Context) -> Result<()> { - Err(ErrorCode::Hello.into()) - } -} - -#[derive(Accounts)] -pub struct Hello {} - -#[error] -pub enum ErrorCode { - #[msg("This is an error message clients will automatically display")] - Hello, -} -``` - -Observe the [#[error]](https://docs.rs/anchor-lang/latest/anchor_lang/attr.error.html) attribute on the `ErrorCode` enum. This macro generates two types: an `Error` and a `Result`, both of which can be used when returning from your program. - -To use the `Error`, you can simply use the user defined `ErrorCode` with Rust's [From](https://doc.rust-lang.org/std/convert/trait.From.html) trait. If you're unfamiliar with `From`, no worries. Just know that you need to either call -`.into()` when using your `ErrorCode`. Or use Rust's `?` operator, when returning an error. -Both of these will automatically convert *into* the correct `Error`. - -::: details -What's the deal with this From stuff? Well, because the Solana runtime expects a [ProgramError](https://docs.rs/solana-program/1.5.5/solana_program/program_error/enum.ProgramError.html) in the return value. The framework needs to wrap the user defined error code into a -`ProgramError::Code` variant, before returning. The alternative would be to use the -`ProgramError` directly. -::: - -## Using the Client - -When using the client, we get the error message. - -```javascript -try { - const tx = await program.rpc.hello(); - assert.ok(false); -} catch (err) { - const errMsg = "This is an error message clients will automatically display"; - assert.equal(err.toString(), errMsg); -} -``` - -It's that easy. :) - -To run the full example, go [here](https://github.com/project-serum/anchor/tree/master/examples/errors). diff --git a/examples/cfo/programs/cfo/src/lib.rs b/examples/cfo/programs/cfo/src/lib.rs index b67ccada8..26a3e8202 100644 --- a/examples/cfo/programs/cfo/src/lib.rs +++ b/examples/cfo/programs/cfo/src/lib.rs @@ -703,10 +703,10 @@ impl<'info> DropStakeReward<'info> { fn into_srm_reward(&self) -> CpiContext<'_, '_, '_, 'info, registry::DropReward<'info>> { let program = self.registry_program.clone(); let accounts = registry::DropReward { - registrar: ProgramAccount::try_from(&self.srm.registrar).unwrap(), - reward_event_q: ProgramAccount::try_from(&self.srm.reward_event_q).unwrap(), + registrar: ProgramAccount::try_from(program.key, &self.srm.registrar).unwrap(), + reward_event_q: ProgramAccount::try_from(program.key, &self.srm.reward_event_q).unwrap(), pool_mint: self.srm.pool_mint.clone(), - vendor: ProgramAccount::try_from(&self.srm.vendor).unwrap(), + vendor: ProgramAccount::try_from(program.key, &self.srm.vendor).unwrap(), vendor_vault: CpiAccount::try_from(&self.srm.vendor_vault).unwrap(), depositor: self.stake.to_account_info(), depositor_authority: self.officer.to_account_info(), @@ -720,10 +720,10 @@ impl<'info> DropStakeReward<'info> { fn into_msrm_reward(&self) -> CpiContext<'_, '_, '_, 'info, registry::DropReward<'info>> { let program = self.registry_program.clone(); let accounts = registry::DropReward { - registrar: ProgramAccount::try_from(&self.msrm.registrar).unwrap(), - reward_event_q: ProgramAccount::try_from(&self.msrm.reward_event_q).unwrap(), + registrar: ProgramAccount::try_from(program.key, &self.msrm.registrar).unwrap(), + reward_event_q: ProgramAccount::try_from(program.key, &self.msrm.reward_event_q).unwrap(), pool_mint: self.msrm.pool_mint.clone(), - vendor: ProgramAccount::try_from(&self.msrm.vendor).unwrap(), + vendor: ProgramAccount::try_from(program.key, &self.msrm.vendor).unwrap(), vendor_vault: CpiAccount::try_from(&self.msrm.vendor_vault).unwrap(), depositor: self.stake.to_account_info(), depositor_authority: self.officer.to_account_info(), diff --git a/lang/src/cpi_account.rs b/lang/src/cpi_account.rs index c7b313742..d0a880400 100644 --- a/lang/src/cpi_account.rs +++ b/lang/src/cpi_account.rs @@ -17,7 +17,7 @@ pub struct CpiAccount<'a, T: AccountDeserialize + Clone> { } impl<'a, T: AccountDeserialize + Clone> CpiAccount<'a, T> { - pub fn new(info: AccountInfo<'a>, account: Box) -> CpiAccount<'a, T> { + fn new(info: AccountInfo<'a>, account: Box) -> CpiAccount<'a, T> { Self { info, account } } diff --git a/lang/src/loader.rs b/lang/src/loader.rs index 19f034276..2b3285ac6 100644 --- a/lang/src/loader.rs +++ b/lang/src/loader.rs @@ -39,9 +39,14 @@ impl<'info, T: ZeroCopy> Loader<'info, T> { /// Constructs a new `Loader` from a previously initialized account. #[inline(never)] - pub fn try_from(acc_info: &AccountInfo<'info>) -> Result, ProgramError> { + pub fn try_from( + program_id: &Pubkey, + acc_info: &AccountInfo<'info>, + ) -> Result, ProgramError> { + if acc_info.owner != program_id { + return Err(ErrorCode::AccountNotProgramOwned.into()); + } let data: &[u8] = &acc_info.try_borrow_data()?; - // Discriminator must match. let mut disc_bytes = [0u8; 8]; disc_bytes.copy_from_slice(&data[..8]); @@ -55,8 +60,12 @@ impl<'info, T: ZeroCopy> Loader<'info, T> { /// Constructs a new `Loader` from an uninitialized account. #[inline(never)] pub fn try_from_unchecked( + program_id: &Pubkey, acc_info: &AccountInfo<'info>, ) -> Result, ProgramError> { + if acc_info.owner != program_id { + return Err(ErrorCode::AccountNotProgramOwned.into()); + } Ok(Loader::new(acc_info.clone())) } @@ -131,10 +140,7 @@ impl<'info, T: ZeroCopy> Accounts<'info> for Loader<'info, T> { } let account = &accounts[0]; *accounts = &accounts[1..]; - let l = Loader::try_from(account)?; - if l.acc_info.owner != program_id { - return Err(ErrorCode::AccountNotProgramOwned.into()); - } + let l = Loader::try_from(program_id, account)?; Ok(l) } } diff --git a/lang/src/program_account.rs b/lang/src/program_account.rs index 98b6abf82..885157f2f 100644 --- a/lang/src/program_account.rs +++ b/lang/src/program_account.rs @@ -24,7 +24,7 @@ struct Inner<'info, T: AccountSerialize + AccountDeserialize + Clone> { } impl<'a, T: AccountSerialize + AccountDeserialize + Clone> ProgramAccount<'a, T> { - pub fn new(info: AccountInfo<'a>, account: T) -> ProgramAccount<'a, T> { + fn new(info: AccountInfo<'a>, account: T) -> ProgramAccount<'a, T> { Self { inner: Box::new(Inner { info, account }), } @@ -32,7 +32,13 @@ impl<'a, T: AccountSerialize + AccountDeserialize + Clone> ProgramAccount<'a, T> /// Deserializes the given `info` into a `ProgramAccount`. #[inline(never)] - pub fn try_from(info: &AccountInfo<'a>) -> Result, ProgramError> { + pub fn try_from( + program_id: &Pubkey, + info: &AccountInfo<'a>, + ) -> Result, ProgramError> { + if info.owner != program_id { + return Err(ErrorCode::AccountNotProgramOwned.into()); + } let mut data: &[u8] = &info.try_borrow_data()?; Ok(ProgramAccount::new( info.clone(), @@ -40,14 +46,17 @@ impl<'a, T: AccountSerialize + AccountDeserialize + Clone> ProgramAccount<'a, T> )) } - /// Deserializes the zero-initialized `info` into a `ProgramAccount` without - /// checking the account type. This should only be used upon program account - /// initialization (since the entire account data array is zeroed and thus - /// no account type is set). + /// Deserializes the given `info` into a `ProgramAccount` without checking + /// the account discriminator. Be careful when using this and avoid it if + /// possible. #[inline(never)] pub fn try_from_unchecked( + program_id: &Pubkey, info: &AccountInfo<'a>, ) -> Result, ProgramError> { + if info.owner != program_id { + return Err(ErrorCode::AccountNotProgramOwned.into()); + } let mut data: &[u8] = &info.try_borrow_data()?; Ok(ProgramAccount::new( info.clone(), @@ -75,11 +84,7 @@ where } let account = &accounts[0]; *accounts = &accounts[1..]; - let pa = ProgramAccount::try_from(account)?; - if pa.inner.info.owner != program_id { - return Err(ErrorCode::AccountNotProgramOwned.into()); - } - Ok(pa) + ProgramAccount::try_from(program_id, account) } } diff --git a/lang/src/state.rs b/lang/src/state.rs index 4eee0964a..e29872760 100644 --- a/lang/src/state.rs +++ b/lang/src/state.rs @@ -25,7 +25,7 @@ struct Inner<'info, T: AccountSerialize + AccountDeserialize + Clone> { } impl<'a, T: AccountSerialize + AccountDeserialize + Clone> ProgramState<'a, T> { - pub fn new(info: AccountInfo<'a>, account: T) -> ProgramState<'a, T> { + fn new(info: AccountInfo<'a>, account: T) -> ProgramState<'a, T> { Self { inner: Box::new(Inner { info, account }), } @@ -33,7 +33,17 @@ impl<'a, T: AccountSerialize + AccountDeserialize + Clone> ProgramState<'a, T> { /// Deserializes the given `info` into a `ProgramState`. #[inline(never)] - pub fn try_from(info: &AccountInfo<'a>) -> Result, ProgramError> { + pub fn try_from( + program_id: &Pubkey, + info: &AccountInfo<'a>, + ) -> Result, ProgramError> { + if info.owner != program_id { + return Err(ErrorCode::AccountNotProgramOwned.into()); + } + if info.key != &Self::address(program_id) { + solana_program::msg!("Invalid state address"); + return Err(ErrorCode::StateInvalidAddress.into()); + } let mut data: &[u8] = &info.try_borrow_data()?; Ok(ProgramState::new( info.clone(), @@ -65,18 +75,7 @@ where } let account = &accounts[0]; *accounts = &accounts[1..]; - - if account.key != &Self::address(program_id) { - solana_program::msg!("Invalid state address"); - return Err(ErrorCode::StateInvalidAddress.into()); - } - - let pa = ProgramState::try_from(account)?; - if pa.inner.info.owner != program_id { - solana_program::msg!("Invalid state owner"); - return Err(ErrorCode::AccountNotProgramOwned.into()); - } - Ok(pa) + ProgramState::try_from(program_id, account) } } diff --git a/lang/syn/src/codegen/accounts/constraints.rs b/lang/syn/src/codegen/accounts/constraints.rs index b8ff696ca..497fb7a37 100644 --- a/lang/syn/src/codegen/accounts/constraints.rs +++ b/lang/syn/src/codegen/accounts/constraints.rs @@ -153,6 +153,7 @@ pub fn generate_constraint_zeroed(f: &Field, _c: &ConstraintZeroed) -> proc_macr return Err(anchor_lang::__private::ErrorCode::ConstraintZero.into()); } #account_wrapper_ty::try_from_unchecked( + program_id, &#field, )? }; @@ -439,6 +440,7 @@ pub fn generate_pda( }, quote! { #account_wrapper_ty::try_from_unchecked( + program_id, &#field.to_account_info(), )? }, diff --git a/lang/syn/src/codegen/program/handlers.rs b/lang/syn/src/codegen/program/handlers.rs index f36a30336..606cd72cf 100644 --- a/lang/syn/src/codegen/program/handlers.rs +++ b/lang/syn/src/codegen/program/handlers.rs @@ -227,7 +227,7 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream { )?; // Zero copy deserialize. - let loader: anchor_lang::Loader<#mod_name::#name> = anchor_lang::Loader::try_from_unchecked(&ctor_accounts.to)?; + let loader: anchor_lang::Loader<#mod_name::#name> = anchor_lang::Loader::try_from_unchecked(program_id, &ctor_accounts.to)?; // Invoke the ctor in a new lexical scope so that // the zero-copy RefMut gets dropped. Required @@ -367,7 +367,7 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream { return Err(anchor_lang::__private::ErrorCode::AccountNotEnoughKeys.into()); } let state_account = &remaining_accounts[0]; - let loader: anchor_lang::Loader<#mod_name::#name> = anchor_lang::Loader::try_from(&state_account)?; + let loader: anchor_lang::Loader<#mod_name::#name> = anchor_lang::Loader::try_from(program_id, &state_account)?; remaining_accounts = &remaining_accounts[1..]; // Deserialize accounts.