diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fce777476..f1b24e5722 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,7 @@ The minor version will be incremented upon a breaking change and the patch versi - cli: Add `deactivate_feature` flag to `solana-test-validator` config in Anchor.toml ([#2872](https://github.com/coral-xyz/anchor/pull/2872)). - idl: Add `docs` field for constants ([#2887](https://github.com/coral-xyz/anchor/pull/2887)). - idl: Store deployment addresses for other clusters ([#2892](https://github.com/coral-xyz/anchor/pull/2892)). +- lang: Add `Event` utility type to get events from bytes ([#2897](https://github.com/coral-xyz/anchor/pull/2897)). ### Fixes diff --git a/lang/attribute/program/src/declare_program/mod.rs b/lang/attribute/program/src/declare_program/mod.rs index ec60ad4468..a8fdf19d5a 100644 --- a/lang/attribute/program/src/declare_program/mod.rs +++ b/lang/attribute/program/src/declare_program/mod.rs @@ -10,7 +10,7 @@ use common::gen_docs; use mods::{ accounts::gen_accounts_mod, client::gen_client_mod, constants::gen_constants_mod, cpi::gen_cpi_mod, events::gen_events_mod, internal::gen_internal_mod, program::gen_program_mod, - types::gen_types_mod, + types::gen_types_mod, utils::gen_utils_mod, }; pub struct DeclareProgram { @@ -66,6 +66,9 @@ fn gen_program(idl: &Idl, name: &syn::Ident) -> proc_macro2::TokenStream { let client_mod = gen_client_mod(idl); let internal_mod = gen_internal_mod(idl); + // Utils + let utils_mod = gen_utils_mod(idl); + quote! { #docs pub mod #name { @@ -82,6 +85,8 @@ fn gen_program(idl: &Idl, name: &syn::Ident) -> proc_macro2::TokenStream { #cpi_mod #client_mod #internal_mod + + #utils_mod } } } diff --git a/lang/attribute/program/src/declare_program/mods/mod.rs b/lang/attribute/program/src/declare_program/mods/mod.rs index 4c0d3bbe48..8c9c58a45c 100644 --- a/lang/attribute/program/src/declare_program/mods/mod.rs +++ b/lang/attribute/program/src/declare_program/mods/mod.rs @@ -6,5 +6,6 @@ pub mod events; pub mod internal; pub mod program; pub mod types; +pub mod utils; use super::common; diff --git a/lang/attribute/program/src/declare_program/mods/utils.rs b/lang/attribute/program/src/declare_program/mods/utils.rs new file mode 100644 index 0000000000..a73502f39e --- /dev/null +++ b/lang/attribute/program/src/declare_program/mods/utils.rs @@ -0,0 +1,69 @@ +use anchor_idl::types::Idl; +use quote::{format_ident, quote}; + +use super::common::gen_discriminator; + +pub fn gen_utils_mod(idl: &Idl) -> proc_macro2::TokenStream { + let event = gen_event(idl); + + quote! { + /// Program utilities. + pub mod utils { + #event + } + } +} + +fn gen_event(idl: &Idl) -> proc_macro2::TokenStream { + let variants = idl + .events + .iter() + .map(|ev| format_ident!("{}", ev.name)) + .map(|name| quote! { #name(#name) }); + let match_arms = idl.events.iter().map(|ev| { + let disc = gen_discriminator(&ev.discriminator); + let name = format_ident!("{}", ev.name); + let event = quote! { + #name::try_from_slice(&value[8..]) + .map(Self::#name) + .map_err(Into::into) + }; + quote! { #disc => #event } + }); + + quote! { + use super::{*, events::*}; + + /// An enum that includes all events of the declared program as a tuple variant. + /// + /// See [`Self::try_from_bytes`] to create an instance from bytes. + pub enum Event { + #(#variants,)* + } + + impl Event { + /// Try to create an event based on the given bytes. + /// + /// This method returns an error if the discriminator of the given bytes don't match + /// with any of the existing events, or if the deserialization fails. + pub fn try_from_bytes(bytes: &[u8]) -> Result { + Self::try_from(bytes) + } + } + + impl TryFrom<&[u8]> for Event { + type Error = anchor_lang::error::Error; + + fn try_from(value: &[u8]) -> Result { + if value.len() < 8 { + return Err(ProgramError::InvalidArgument.into()); + } + + match &value[..8] { + #(#match_arms,)* + _ => Err(ProgramError::InvalidArgument.into()), + } + } + } + } +} diff --git a/tests/declare-program/idls/external.json b/tests/declare-program/idls/external.json index 60a9553061..7329c7c558 100644 --- a/tests/declare-program/idls/external.json +++ b/tests/declare-program/idls/external.json @@ -139,6 +139,21 @@ ] } ], + "events": [ + { + "name": "MyEvent", + "discriminator": [ + 96, + 184, + 197, + 243, + 139, + 2, + 90, + 148 + ] + } + ], "types": [ { "name": "MyAccount", @@ -151,6 +166,18 @@ } ] } + }, + { + "name": "MyEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "value", + "type": "u32" + } + ] + } } ] } \ No newline at end of file diff --git a/tests/declare-program/programs/declare-program/src/lib.rs b/tests/declare-program/programs/declare-program/src/lib.rs index dff1fcee7c..5272dea559 100644 --- a/tests/declare-program/programs/declare-program/src/lib.rs +++ b/tests/declare-program/programs/declare-program/src/lib.rs @@ -48,6 +48,31 @@ pub mod declare_program { Ok(()) } + + pub fn event_utils(_ctx: Context) -> Result<()> { + use external::utils::Event; + + // Empty + if Event::try_from_bytes(&[]).is_ok() { + return Err(ProgramError::Custom(0).into()); + } + + const DISC: &[u8] = + &::DISCRIMINATOR; + + // Correct discriminator but invalid data + if Event::try_from_bytes(DISC).is_ok() { + return Err(ProgramError::Custom(1).into()); + }; + + // Correct discriminator and valid data + match Event::try_from_bytes(&[DISC, &[1, 0, 0, 0]].concat()) { + Ok(Event::MyEvent(my_event)) => require_eq!(my_event.value, 1), + Err(e) => return Err(e.into()), + } + + Ok(()) + } } #[derive(Accounts)] @@ -57,3 +82,8 @@ pub struct Cpi<'info> { pub cpi_my_account: Account<'info, external::accounts::MyAccount>, pub external_program: Program<'info, External>, } + +#[derive(Accounts)] +pub struct Utils<'info> { + pub authority: Signer<'info>, +} diff --git a/tests/declare-program/programs/external/src/lib.rs b/tests/declare-program/programs/external/src/lib.rs index 6768569a32..4897ba8ad2 100644 --- a/tests/declare-program/programs/external/src/lib.rs +++ b/tests/declare-program/programs/external/src/lib.rs @@ -52,3 +52,8 @@ pub struct UpdateComposite<'info> { pub struct MyAccount { pub field: u32, } + +#[event] +pub struct MyEvent { + pub value: u32, +} diff --git a/tests/declare-program/tests/declare-program.ts b/tests/declare-program/tests/declare-program.ts index a2bf63a080..de73ab25ed 100644 --- a/tests/declare-program/tests/declare-program.ts +++ b/tests/declare-program/tests/declare-program.ts @@ -46,4 +46,8 @@ describe("declare-program", () => { ); assert.strictEqual(myAccount.field, value); }); + + it("Can use event utils", async () => { + await program.methods.eventUtils().rpc(); + }); });