Skip to content

Commit

Permalink
error redesign (#1462)
Browse files Browse the repository at this point in the history
  • Loading branch information
paul-schaaf committed Feb 20, 2022
1 parent 093eaf0 commit 4059682
Show file tree
Hide file tree
Showing 82 changed files with 1,076 additions and 613 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/tests.yaml
Expand Up @@ -121,6 +121,7 @@ jobs:
name: Setup Client Example Test
runs-on: ubuntu-18.04
strategy:
fail-fast: false
matrix:
node:
- path: tests/events/
Expand Down Expand Up @@ -232,6 +233,7 @@ jobs:
name: Test ${{ matrix.node.path }}
runs-on: ubuntu-18.04
strategy:
fail-fast: false
matrix:
node:
- cmd: cd tests/sysvars && anchor test
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Expand Up @@ -25,6 +25,11 @@ incremented for features.
* lang: Enforce that the payer for an init-ed account be marked `mut` ([#1271](https://github.com/project-serum/anchor/pull/1271)).
* lang: All error-related code is now in the error module ([#1426](https://github.com/project-serum/anchor/pull/1426)).
* lang: Require doc comments when using AccountInfo or UncheckedAccount types ([#1452](https://github.com/project-serum/anchor/pull/1452)).
* lang: add [`error!`](https://docs.rs/anchor-lang/latest/anchor_lang/prelude/macro.error.html) and [`err!`](https://docs.rs/anchor-lang/latest/anchor_lang/prelude/macro.err.html) macro and `Result` type ([#1462](https://github.com/project-serum/anchor/pull/1462)).
This change will break most programs. Do the following to upgrade:
* change all `ProgramResult`'s to `Result<()>`
* change `#[error]` to `#[error_code]`
* change all `Err(MyError::SomeError.into())` to `Err(error!(MyError::SomeError))` and all `Err(ProgramError::SomeProgramError)` to `Err(ProgramError::SomeProgramError.into())` or `Err(Error::from(ProgramError::SomeProgramError).with_source(source!()))` to provide file and line source of the error (`with_source` is most useful with `ProgramError`s. `error!` already adds source information for custom and anchor internal errors).

## [0.21.0] - 2022-02-07

Expand Down
2 changes: 2 additions & 0 deletions client/src/lib.rs
Expand Up @@ -376,6 +376,8 @@ pub enum ClientError {
#[error("Account not found")]
AccountNotFound,
#[error("{0}")]
AnchorError(#[from] anchor_lang::error::Error),
#[error("{0}")]
ProgramError(#[from] ProgramError),
#[error("{0}")]
SolanaClientError(#[from] SolanaClientError),
Expand Down
21 changes: 8 additions & 13 deletions docs/src/tutorials/tutorial-4.md
@@ -1,8 +1,8 @@
# 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
either non existent or opaque error codes. Anchor attempts to address this by
providing the `#[error_code]` attribute, which can be used to create typed Errors with
descriptive messages that automatically propagate to the client.

## Defining a Program
Expand All @@ -16,31 +16,26 @@ use anchor_lang::prelude::*;
mod errors {
use super::*;
pub fn hello(_ctx: Context<Hello>) -> Result<()> {
Err(ErrorCode::Hello.into())
Err(error!(ErrorCode::Hello))
}
}

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

#[error]
#[error_code]
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.
Observe the [#[error_code]](https://docs.rs/anchor-lang/latest/anchor_lang/attr.error_code.html) attribute on the `ErrorCode` enum.
This macro generates internal anchor code that helps anchor turn the error code into an error and display it properly.

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 create an error, use the [`error!`](https://docs.rs/anchor-lang/latest/anchor_lang/prelude/macro.error.html) macro together with an error code. This macro creates an [`AnchorError`](https://docs.rs/anchor-lang/latest/anchor_lang/error/struct.AnchorError.html) that includes helpful information like the file and line the error was created in.

::: 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.
:::
To make writing errors even easier, anchor also provides the [`err!`](https://docs.rs/anchor-lang/latest/anchor_lang/prelude/macro.err.html and the [`require!`](https://docs.rs/anchor-lang/latest/anchor_lang/prelude/macro.require.html macros.

## Using the Client

Expand Down
2 changes: 1 addition & 1 deletion examples/tutorial/basic-0/programs/basic-0/src/lib.rs
Expand Up @@ -5,7 +5,7 @@ declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");
#[program]
mod basic_0 {
use super::*;
pub fn initialize(_ctx: Context<Initialize>) -> ProgramResult {
pub fn initialize(_ctx: Context<Initialize>) -> Result<()> {
Ok(())
}
}
Expand Down
4 changes: 2 additions & 2 deletions examples/tutorial/basic-1/programs/basic-1/src/lib.rs
Expand Up @@ -6,13 +6,13 @@ declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");
mod basic_1 {
use super::*;

pub fn initialize(ctx: Context<Initialize>, data: u64) -> ProgramResult {
pub fn initialize(ctx: Context<Initialize>, data: u64) -> Result<()> {
let my_account = &mut ctx.accounts.my_account;
my_account.data = data;
Ok(())
}

pub fn update(ctx: Context<Update>, data: u64) -> ProgramResult {
pub fn update(ctx: Context<Update>, data: u64) -> Result<()> {
let my_account = &mut ctx.accounts.my_account;
my_account.data = data;
Ok(())
Expand Down
4 changes: 2 additions & 2 deletions examples/tutorial/basic-2/programs/basic-2/src/lib.rs
Expand Up @@ -6,14 +6,14 @@ declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");
mod basic_2 {
use super::*;

pub fn create(ctx: Context<Create>, authority: Pubkey) -> ProgramResult {
pub fn create(ctx: Context<Create>, authority: Pubkey) -> Result<()> {
let counter = &mut ctx.accounts.counter;
counter.authority = authority;
counter.count = 0;
Ok(())
}

pub fn increment(ctx: Context<Increment>) -> ProgramResult {
pub fn increment(ctx: Context<Increment>) -> Result<()> {
let counter = &mut ctx.accounts.counter;
counter.count += 1;
Ok(())
Expand Down
Expand Up @@ -9,7 +9,7 @@ declare_id!("HmbTLCmaGvZhKnn1Zfa1JVnp7vkMV4DYVxPLWBVoN65L");
#[program]
mod puppet_master {
use super::*;
pub fn pull_strings(ctx: Context<PullStrings>, data: u64) -> ProgramResult {
pub fn pull_strings(ctx: Context<PullStrings>, data: u64) -> anchor_lang::Result<()> {
let cpi_program = ctx.accounts.puppet_program.to_account_info();
let cpi_accounts = SetData {
puppet: ctx.accounts.puppet.to_account_info(),
Expand Down
4 changes: 2 additions & 2 deletions examples/tutorial/basic-3/programs/puppet/src/lib.rs
Expand Up @@ -5,11 +5,11 @@ declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");
#[program]
pub mod puppet {
use super::*;
pub fn initialize(_ctx: Context<Initialize>) -> ProgramResult {
pub fn initialize(_ctx: Context<Initialize>) -> Result<()> {
Ok(())
}

pub fn set_data(ctx: Context<SetData>, data: u64) -> ProgramResult {
pub fn set_data(ctx: Context<SetData>, data: u64) -> Result<()> {
let puppet = &mut ctx.accounts.puppet;
puppet.data = data;
Ok(())
Expand Down
8 changes: 4 additions & 4 deletions examples/tutorial/basic-4/programs/basic-4/src/lib.rs
Expand Up @@ -14,16 +14,16 @@ pub mod basic_4 {
}

impl Counter {
pub fn new(ctx: Context<Auth>) -> Result<Self> {
pub fn new(ctx: Context<Auth>) -> anchor_lang::Result<Self> {
Ok(Self {
authority: *ctx.accounts.authority.key,
count: 0,
})
}

pub fn increment(&mut self, ctx: Context<Auth>) -> Result<()> {
pub fn increment(&mut self, ctx: Context<Auth>) -> anchor_lang::Result<()> {
if &self.authority != ctx.accounts.authority.key {
return Err(ErrorCode::Unauthorized.into());
return Err(error!(ErrorCode::Unauthorized));
}
self.count += 1;
Ok(())
Expand All @@ -37,7 +37,7 @@ pub struct Auth<'info> {
}
// #endregion code

#[error]
#[error_code]
pub enum ErrorCode {
#[msg("You are not authorized to perform this action.")]
Unauthorized,
Expand Down
23 changes: 12 additions & 11 deletions lang/attribute/account/src/lib.rs
Expand Up @@ -146,7 +146,7 @@ pub fn account(
// It's expected on-chain programs deserialize via zero-copy.
#[automatically_derived]
impl #impl_gen anchor_lang::AccountDeserialize for #account_name #type_gen #where_clause {
fn try_deserialize(buf: &mut &[u8]) -> std::result::Result<Self, ProgramError> {
fn try_deserialize(buf: &mut &[u8]) -> anchor_lang::Result<Self> {
if buf.len() < #discriminator.len() {
return Err(anchor_lang::error::ErrorCode::AccountDiscriminatorNotFound.into());
}
Expand All @@ -157,7 +157,7 @@ pub fn account(
Self::try_deserialize_unchecked(buf)
}

fn try_deserialize_unchecked(buf: &mut &[u8]) -> std::result::Result<Self, ProgramError> {
fn try_deserialize_unchecked(buf: &mut &[u8]) -> anchor_lang::Result<Self> {
let data: &[u8] = &buf[8..];
// Re-interpret raw bytes into the POD data structure.
let account = anchor_lang::__private::bytemuck::from_bytes(data);
Expand All @@ -175,20 +175,21 @@ pub fn account(

#[automatically_derived]
impl #impl_gen anchor_lang::AccountSerialize for #account_name #type_gen #where_clause {
fn try_serialize<W: std::io::Write>(&self, writer: &mut W) -> std::result::Result<(), ProgramError> {
writer.write_all(&#discriminator).map_err(|_| anchor_lang::error::ErrorCode::AccountDidNotSerialize)?;
AnchorSerialize::serialize(
self,
writer
)
.map_err(|_| anchor_lang::error::ErrorCode::AccountDidNotSerialize)?;
fn try_serialize<W: std::io::Write>(&self, writer: &mut W) -> anchor_lang::Result<()> {
if writer.write_all(&#discriminator).is_err() {
return Err(anchor_lang::error::ErrorCode::AccountDidNotSerialize.into());
}

if AnchorSerialize::serialize(self, writer).is_err() {
return Err(anchor_lang::error::ErrorCode::AccountDidNotSerialize.into());
}
Ok(())
}
}

#[automatically_derived]
impl #impl_gen anchor_lang::AccountDeserialize for #account_name #type_gen #where_clause {
fn try_deserialize(buf: &mut &[u8]) -> std::result::Result<Self, ProgramError> {
fn try_deserialize(buf: &mut &[u8]) -> anchor_lang::Result<Self> {
if buf.len() < #discriminator.len() {
return Err(anchor_lang::error::ErrorCode::AccountDiscriminatorNotFound.into());
}
Expand All @@ -199,7 +200,7 @@ pub fn account(
Self::try_deserialize_unchecked(buf)
}

fn try_deserialize_unchecked(buf: &mut &[u8]) -> std::result::Result<Self, ProgramError> {
fn try_deserialize_unchecked(buf: &mut &[u8]) -> anchor_lang::Result<Self> {
let mut data: &[u8] = &buf[8..];
AnchorDeserialize::deserialize(&mut data)
.map_err(|_| anchor_lang::error::ErrorCode::AccountDidNotDeserialize.into())
Expand Down
2 changes: 1 addition & 1 deletion lang/attribute/error/Cargo.toml
Expand Up @@ -17,4 +17,4 @@ anchor-debug = ["anchor-syn/anchor-debug"]
proc-macro2 = "1.0"
quote = "1.0"
syn = { version = "1.0.60", features = ["full"] }
anchor-syn = { path = "../../syn", version = "0.21.0" }
anchor-syn = { path = "../../syn", version = "0.21.0" }
82 changes: 74 additions & 8 deletions lang/attribute/error/src/lib.rs
@@ -1,9 +1,12 @@
extern crate proc_macro;

use anchor_syn::codegen::error as error_codegen;
use anchor_syn::parser::error as error_parser;
use proc_macro::TokenStream;
use quote::quote;

use anchor_syn::parser::error::{self as error_parser, ErrorWithAccountNameInput};
use anchor_syn::ErrorArgs;
use syn::parse_macro_input;
use anchor_syn::{codegen, parser::error::ErrorInput};
use syn::{parse_macro_input, Expr};

/// Generates `Error` and `type Result<T> = Result<T, Error>` types to be
/// used as return types from Anchor instruction handlers. Importantly, the
Expand All @@ -21,14 +24,14 @@ use syn::parse_macro_input;
/// mod errors {
/// use super::*;
/// pub fn hello(_ctx: Context<Hello>) -> Result<()> {
/// Err(MyError::Hello.into())
/// Err(error!(MyError::Hello))
/// }
/// }
///
/// #[derive(Accounts)]
/// pub struct Hello {}
///
/// #[error]
/// #[error_code]
/// pub enum MyError {
/// #[msg("This is an error message clients will automatically display")]
/// Hello,
Expand All @@ -40,14 +43,14 @@ use syn::parse_macro_input;
/// [`ProgramError`](../solana_program/enum.ProgramError.html), which is used
/// pervasively, throughout solana program crates. The generated `Error` type
/// should almost never be used directly, as the user defined error is
/// preferred. In the example above, `MyError::Hello.into()`.
/// preferred. In the example above, `error!(MyError::Hello)`.
///
/// # Msg
///
/// The `#[msg(..)]` attribute is inert, and is used only as a marker so that
/// parsers and IDLs can map error codes to error messages.
#[proc_macro_attribute]
pub fn error(
pub fn error_code(
args: proc_macro::TokenStream,
input: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
Expand All @@ -56,6 +59,69 @@ pub fn error(
false => Some(parse_macro_input!(args as ErrorArgs)),
};
let mut error_enum = parse_macro_input!(input as syn::ItemEnum);
let error = error_codegen::generate(error_parser::parse(&mut error_enum, args));
let error = codegen::error::generate(error_parser::parse(&mut error_enum, args));
proc_macro::TokenStream::from(error)
}

/// Generates an [`Error::AnchorError`](../../anchor_lang/error/enum.Error.html) that includes file and line information.
///
/// # Example
/// ```rust,ignore
/// #[program]
/// mod errors {
/// use super::*;
/// pub fn example(_ctx: Context<Example>) -> Result<()> {
/// Err(error!(MyError::Hello))
/// }
/// }
///
/// #[error_code]
/// pub enum MyError {
/// #[msg("This is an error message clients will automatically display")]
/// Hello,
/// }
/// ```
#[proc_macro]
pub fn error(ts: proc_macro::TokenStream) -> TokenStream {
let input = parse_macro_input!(ts as ErrorInput);
let error_code = input.error_code;
create_error(error_code, true, None)
}

#[proc_macro]
pub fn error_with_account_name(ts: proc_macro::TokenStream) -> TokenStream {
let input = parse_macro_input!(ts as ErrorWithAccountNameInput);
let error_code = input.error_code;
let account_name = input.account_name;
create_error(error_code, false, Some(account_name))
}

fn create_error(error_code: Expr, source: bool, account_name: Option<Expr>) -> TokenStream {
let source = if source {
quote! {
Some(anchor_lang::error::Source {
filename: file!(),
line: line!()
})
}
} else {
quote! {
None
}
};
let account_name = match account_name {
Some(_) => quote! { Some(#account_name.to_string()) },
None => quote! { None },
};
TokenStream::from(quote! {
anchor_lang::error::Error::from(
anchor_lang::error::AnchorError {
error_name: #error_code.name(),
error_code_number: #error_code.into(),
error_msg: #error_code.to_string(),
source: #source,
account_name: #account_name
}
)
})
}

0 comments on commit 4059682

Please sign in to comment.