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: Move program check to try_from #660

Merged
merged 5 commits into from
Sep 1, 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 0 additions & 1 deletion docs/src/.vuepress/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ module.exports = {
"/tutorials/tutorial-2",
"/tutorials/tutorial-3",
"/tutorials/tutorial-4",
"/tutorials/tutorial-5",
],
},
{
Expand Down
113 changes: 45 additions & 68 deletions docs/src/tutorials/tutorial-4.md
Original file line number Diff line number Diff line change
@@ -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

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.

### How it works

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).
For example,

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.
```rust
use anchor_lang::prelude::*;

## Using the client
#[program]
mod errors {
use super::*;
pub fn hello(_ctx: Context<Hello>) -> Result<()> {
Err(ErrorCode::Hello.into())
}
}

### Invoke the constructor
#[derive(Accounts)]
pub struct Hello {}

To access the `#[state]` account and associated instructions, you can use the
`anchor.state` namespace on the client. For example, to invoke the constructor,

<<< @/../examples/tutorial/basic-4/tests/basic-4.js#ctor

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.

### Fetch the state

To fetch the state account,

<<< @/../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
#[error]
pub enum ErrorCode {
#[msg("This is an error message clients will automatically display")]
Hello,
}
```

## CPI
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.

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:
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`.

1. All the generated instructions are located under the `<my_program>::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).
::: 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.
:::

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).
## Using the Client

## Conclusion
When using the client, we get the error message.

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.
```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);
}
```

## Next Steps
It's that easy. :)

Next we'll discuss errors.
To run the full example, go [here](https://github.com/project-serum/anchor/tree/master/examples/errors).
61 changes: 0 additions & 61 deletions docs/src/tutorials/tutorial-5.md

This file was deleted.

12 changes: 6 additions & 6 deletions examples/cfo/programs/cfo/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand All @@ -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(),
Expand Down
2 changes: 1 addition & 1 deletion lang/src/cpi_account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>) -> CpiAccount<'a, T> {
fn new(info: AccountInfo<'a>, account: Box<T>) -> CpiAccount<'a, T> {
Self { info, account }
}

Expand Down
18 changes: 12 additions & 6 deletions lang/src/loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Loader<'info, T>, ProgramError> {
pub fn try_from(
program_id: &Pubkey,
acc_info: &AccountInfo<'info>,
) -> Result<Loader<'info, T>, 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]);
Expand All @@ -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<Loader<'info, T>, ProgramError> {
if acc_info.owner != program_id {
return Err(ErrorCode::AccountNotProgramOwned.into());
}
Ok(Loader::new(acc_info.clone()))
}

Expand Down Expand Up @@ -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)
}
}
Expand Down
27 changes: 16 additions & 11 deletions lang/src/program_account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,30 +24,39 @@ 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 }),
}
}

/// Deserializes the given `info` into a `ProgramAccount`.
#[inline(never)]
pub fn try_from(info: &AccountInfo<'a>) -> Result<ProgramAccount<'a, T>, ProgramError> {
pub fn try_from(
program_id: &Pubkey,
info: &AccountInfo<'a>,
) -> Result<ProgramAccount<'a, T>, ProgramError> {
if info.owner != program_id {
return Err(ErrorCode::AccountNotProgramOwned.into());
}
let mut data: &[u8] = &info.try_borrow_data()?;
Ok(ProgramAccount::new(
info.clone(),
T::try_deserialize(&mut data)?,
))
}

/// 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<ProgramAccount<'a, T>, ProgramError> {
if info.owner != program_id {
return Err(ErrorCode::AccountNotProgramOwned.into());
}
let mut data: &[u8] = &info.try_borrow_data()?;
Ok(ProgramAccount::new(
info.clone(),
Expand Down Expand Up @@ -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)
}
}

Expand Down
27 changes: 13 additions & 14 deletions lang/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,25 @@ 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 }),
}
}

/// Deserializes the given `info` into a `ProgramState`.
#[inline(never)]
pub fn try_from(info: &AccountInfo<'a>) -> Result<ProgramState<'a, T>, ProgramError> {
pub fn try_from(
program_id: &Pubkey,
info: &AccountInfo<'a>,
) -> Result<ProgramState<'a, T>, 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(),
Expand Down Expand Up @@ -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)
}
}

Expand Down
Loading