lang: Move program check to try_from (#660)
This commit is contained in:
parent
afa218f797
commit
f4f60d7fab
|
@ -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
|
||||
|
||||
|
|
|
@ -64,7 +64,6 @@ module.exports = {
|
|||
"/tutorials/tutorial-2",
|
||||
"/tutorials/tutorial-3",
|
||||
"/tutorials/tutorial-4",
|
||||
"/tutorials/tutorial-5",
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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<Hello>) -> 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 `<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).
|
||||
|
||||
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).
|
||||
|
|
|
@ -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<Hello>) -> 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).
|
|
@ -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(),
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
|
||||
|
|
|
@ -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]);
|
||||
|
@ -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()))
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<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(),
|
||||
|
@ -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<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(),
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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<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(),
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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(),
|
||||
)?
|
||||
},
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Reference in New Issue