From 69426657eb56c2f98e08fdfe61dc7650e42110d9 Mon Sep 17 00:00:00 2001 From: acheron Date: Tue, 19 Mar 2024 03:45:29 +0100 Subject: [PATCH 1/4] lang: Add `declare_program!` macro --- Cargo.lock | 5 + lang/attribute/program/Cargo.toml | 7 +- .../program/src/declare_program/common.rs | 360 ++++++++++++++++++ .../program/src/declare_program/mod.rs | 115 ++++++ .../src/declare_program/mods/accounts.rs | 118 ++++++ .../src/declare_program/mods/client.rs | 32 ++ .../src/declare_program/mods/constants.rs | 29 ++ .../program/src/declare_program/mods/cpi.rs | 116 ++++++ .../src/declare_program/mods/events.rs | 45 +++ .../src/declare_program/mods/internal.rs | 149 ++++++++ .../program/src/declare_program/mods/mod.rs | 10 + .../src/declare_program/mods/program.rs | 25 ++ .../program/src/declare_program/mods/types.rs | 28 ++ lang/attribute/program/src/lib.rs | 37 ++ lang/src/lib.rs | 9 +- lang/syn/src/codegen/accounts/mod.rs | 4 +- 16 files changed, 1082 insertions(+), 7 deletions(-) create mode 100644 lang/attribute/program/src/declare_program/common.rs create mode 100644 lang/attribute/program/src/declare_program/mod.rs create mode 100644 lang/attribute/program/src/declare_program/mods/accounts.rs create mode 100644 lang/attribute/program/src/declare_program/mods/client.rs create mode 100644 lang/attribute/program/src/declare_program/mods/constants.rs create mode 100644 lang/attribute/program/src/declare_program/mods/cpi.rs create mode 100644 lang/attribute/program/src/declare_program/mods/events.rs create mode 100644 lang/attribute/program/src/declare_program/mods/internal.rs create mode 100644 lang/attribute/program/src/declare_program/mods/mod.rs create mode 100644 lang/attribute/program/src/declare_program/mods/program.rs create mode 100644 lang/attribute/program/src/declare_program/mods/types.rs diff --git a/Cargo.lock b/Cargo.lock index c93eff4d5c..6563e19330 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -171,7 +171,12 @@ name = "anchor-attribute-program" version = "0.29.0" dependencies = [ "anchor-syn", + "anyhow", + "bs58 0.5.0", + "heck 0.3.3", + "proc-macro2", "quote", + "serde_json", "syn 1.0.109", ] diff --git a/lang/attribute/program/Cargo.toml b/lang/attribute/program/Cargo.toml index 3005195481..e72fec8221 100644 --- a/lang/attribute/program/Cargo.toml +++ b/lang/attribute/program/Cargo.toml @@ -17,6 +17,11 @@ idl-build = ["anchor-syn/idl-build"] interface-instructions = ["anchor-syn/interface-instructions"] [dependencies] -anchor-syn = { path = "../../syn", version = "0.29.0" } +anchor-syn = { path = "../../syn", version = "0.29.0", features = ["idl-types"] } +anyhow = "1" +bs58 = "0.5" +heck = "0.3" +proc-macro2 = "1" quote = "1" +serde_json = "1" syn = { version = "1", features = ["full"] } diff --git a/lang/attribute/program/src/declare_program/common.rs b/lang/attribute/program/src/declare_program/common.rs new file mode 100644 index 0000000000..2be500a535 --- /dev/null +++ b/lang/attribute/program/src/declare_program/common.rs @@ -0,0 +1,360 @@ +use anchor_syn::idl::types::{ + Idl, IdlArrayLen, IdlDefinedFields, IdlField, IdlGenericArg, IdlRepr, IdlSerialization, + IdlType, IdlTypeDef, IdlTypeDefGeneric, IdlTypeDefTy, +}; +use quote::{format_ident, quote}; + +/// This function should ideally return the absolute path to the declared program's id but because +/// `proc_macro2::Span::call_site().source_file().path()` is behind an unstable feature flag, we +/// are not able to reliably decide where the definition is. +pub fn get_canonical_program_id() -> proc_macro2::TokenStream { + quote! { super::__ID } +} + +pub fn gen_docs(docs: &[String]) -> proc_macro2::TokenStream { + let docs = docs + .iter() + .map(|doc| format!("{}{doc}", if doc.is_empty() { "" } else { " " })) + .map(|doc| quote! { #[doc = #doc] }); + quote! { #(#docs)* } +} + +pub fn gen_discriminator(disc: &[u8]) -> proc_macro2::TokenStream { + quote! { [#(#disc), *] } +} + +pub fn gen_accounts_common(idl: &Idl, prefix: &str) -> proc_macro2::TokenStream { + let re_exports = idl + .instructions + .iter() + .map(|ix| format_ident!("__{}_accounts_{}", prefix, ix.name)) + .map(|ident| quote! { pub use super::internal::#ident::*; }); + + quote! { + pub mod accounts { + #(#re_exports)* + } + } +} + +pub fn convert_idl_type_to_syn_type(ty: &IdlType) -> syn::Type { + syn::parse_str(&convert_idl_type_to_str(ty)).unwrap() +} + +// TODO: Impl `ToString` for `IdlType` +pub fn convert_idl_type_to_str(ty: &IdlType) -> String { + match ty { + IdlType::Bool => "bool".into(), + IdlType::U8 => "u8".into(), + IdlType::I8 => "i8".into(), + IdlType::U16 => "u16".into(), + IdlType::I16 => "i16".into(), + IdlType::U32 => "u32".into(), + IdlType::I32 => "i32".into(), + IdlType::F32 => "f32".into(), + IdlType::U64 => "u64".into(), + IdlType::I64 => "i64".into(), + IdlType::F64 => "f64".into(), + IdlType::U128 => "u128".into(), + IdlType::I128 => "i128".into(), + IdlType::U256 => "u256".into(), + IdlType::I256 => "i256".into(), + IdlType::Bytes => "bytes".into(), + IdlType::String => "String".into(), + IdlType::Pubkey => "Pubkey".into(), + IdlType::Option(ty) => format!("Option<{}>", convert_idl_type_to_str(ty)), + IdlType::Vec(ty) => format!("Vec<{}>", convert_idl_type_to_str(ty)), + IdlType::Array(ty, len) => format!( + "[{}; {}]", + convert_idl_type_to_str(ty), + match len { + IdlArrayLen::Generic(len) => len.into(), + IdlArrayLen::Value(len) => len.to_string(), + } + ), + IdlType::Defined { name, generics } => generics + .iter() + .map(|generic| match generic { + IdlGenericArg::Type { ty } => convert_idl_type_to_str(ty), + IdlGenericArg::Const { value } => value.into(), + }) + .reduce(|mut acc, cur| { + if !acc.is_empty() { + acc.push(','); + } + acc.push_str(&cur); + acc + }) + .map(|generics| format!("{name}<{generics}>")) + .unwrap_or(name.into()), + IdlType::Generic(ty) => ty.into(), + } +} + +pub fn convert_idl_type_def_to_ts( + ty_def: &IdlTypeDef, + ty_defs: &[IdlTypeDef], +) -> proc_macro2::TokenStream { + let name = format_ident!("{}", ty_def.name); + let docs = gen_docs(&ty_def.docs); + + let generics = { + let generics = ty_def + .generics + .iter() + .map(|generic| match generic { + IdlTypeDefGeneric::Type { name } => format_ident!("{name}"), + IdlTypeDefGeneric::Const { name, ty } => format_ident!("{name}: {ty}"), + }) + .collect::>(); + if generics.is_empty() { + quote!() + } else { + quote!(<#(#generics,)*>) + } + }; + + let attrs = { + let debug_attr = quote!(#[derive(Debug)]); + + let default_attr = can_derive_default(ty_def, ty_defs) + .then(|| quote!(#[derive(Default)])) + .unwrap_or_default(); + + let ser_attr = match &ty_def.serialization { + IdlSerialization::Borsh => quote!(#[derive(AnchorSerialize, AnchorDeserialize)]), + IdlSerialization::Bytemuck => quote!(#[zero_copy]), + IdlSerialization::BytemuckUnsafe => quote!(#[zero_copy(unsafe)]), + _ => unimplemented!("{:?}", ty_def.serialization), + }; + + let clone_attr = matches!(ty_def.serialization, IdlSerialization::Borsh) + .then(|| quote!(#[derive(Clone)])) + .unwrap_or_default(); + + let copy_attr = matches!(ty_def.serialization, IdlSerialization::Borsh) + .then(|| can_derive_copy(ty_def, ty_defs).then(|| quote!(#[derive(Copy)]))) + .flatten() + .unwrap_or_default(); + + quote! { + #debug_attr + #default_attr + #ser_attr + #clone_attr + #copy_attr + } + }; + + let repr = if let Some(repr) = &ty_def.repr { + let kind = match repr { + IdlRepr::Rust(_) => "Rust", + IdlRepr::C(_) => "C", + IdlRepr::Transparent => "transparent", + }; + let kind = format_ident!("{kind}"); + + let modifier = match repr { + IdlRepr::Rust(modifier) | IdlRepr::C(modifier) => { + let packed = modifier.packed.then(|| quote!(packed)).unwrap_or_default(); + let align = modifier + .align + .map(|align| quote!(align(#align))) + .unwrap_or_default(); + + if packed.is_empty() { + align + } else if align.is_empty() { + packed + } else { + quote! { #packed, #align } + } + } + _ => quote!(), + }; + let modifier = if modifier.is_empty() { + modifier + } else { + quote! { , #modifier } + }; + + quote! { #[repr(#kind #modifier)] } + } else { + quote!() + }; + + let ty = match &ty_def.ty { + IdlTypeDefTy::Struct { fields } => { + let declare_struct = quote! { pub struct #name #generics }; + handle_defined_fields( + fields.as_ref(), + || quote! { #declare_struct; }, + |fields| { + let fields = fields.iter().map(|field| { + let name = format_ident!("{}", field.name); + let ty = convert_idl_type_to_syn_type(&field.ty); + quote! { pub #name : #ty } + }); + quote! { + #declare_struct { + #(#fields,)* + } + } + }, + |tys| { + let tys = tys.iter().map(convert_idl_type_to_syn_type); + quote! { + #declare_struct (#(#tys,)*); + } + }, + ) + } + IdlTypeDefTy::Enum { variants } => { + let variants = variants.iter().map(|variant| { + let variant_name = format_ident!("{}", variant.name); + handle_defined_fields( + variant.fields.as_ref(), + || quote! { #variant_name }, + |fields| { + let fields = fields.iter().map(|field| { + let name = format_ident!("{}", field.name); + let ty = convert_idl_type_to_syn_type(&field.ty); + quote! { #name : #ty } + }); + quote! { + #variant_name { + #(#fields,)* + } + } + }, + |tys| { + let tys = tys.iter().map(convert_idl_type_to_syn_type); + quote! { + #variant_name (#(#tys,)*) + } + }, + ) + }); + + quote! { + pub enum #name #generics { + #(#variants,)* + } + } + } + IdlTypeDefTy::Type { alias } => { + let alias = convert_idl_type_to_syn_type(alias); + quote! { pub type #name = #alias; } + } + }; + + quote! { + #docs + #attrs + #repr + #ty + } +} + +fn can_derive_copy(ty_def: &IdlTypeDef, ty_defs: &[IdlTypeDef]) -> bool { + match &ty_def.ty { + IdlTypeDefTy::Struct { fields } => { + can_derive_common(fields.as_ref(), ty_defs, can_derive_copy_ty) + } + IdlTypeDefTy::Enum { variants } => variants + .iter() + .all(|variant| can_derive_common(variant.fields.as_ref(), ty_defs, can_derive_copy_ty)), + IdlTypeDefTy::Type { alias } => can_derive_copy_ty(alias, ty_defs), + } +} + +fn can_derive_default(ty_def: &IdlTypeDef, ty_defs: &[IdlTypeDef]) -> bool { + match &ty_def.ty { + IdlTypeDefTy::Struct { fields } => { + can_derive_common(fields.as_ref(), ty_defs, can_derive_default_ty) + } + // TODO: Consider storing the default enum variant in IDL + IdlTypeDefTy::Enum { .. } => false, + IdlTypeDefTy::Type { alias } => can_derive_default_ty(alias, ty_defs), + } +} + +fn can_derive_copy_ty(ty: &IdlType, ty_defs: &[IdlTypeDef]) -> bool { + match ty { + IdlType::Option(inner) => can_derive_copy_ty(inner, ty_defs), + IdlType::Array(inner, len) => { + if !can_derive_copy_ty(inner, ty_defs) { + return false; + } + + match len { + IdlArrayLen::Value(_) => true, + IdlArrayLen::Generic(_) => false, + } + } + IdlType::Defined { name, .. } => ty_defs + .iter() + .find(|ty_def| &ty_def.name == name) + .map(|ty_def| can_derive_copy(ty_def, ty_defs)) + .expect("Type def must exist"), + IdlType::Bytes | IdlType::String | IdlType::Vec(_) | IdlType::Generic(_) => false, + _ => true, + } +} + +fn can_derive_default_ty(ty: &IdlType, ty_defs: &[IdlTypeDef]) -> bool { + match ty { + IdlType::Option(inner) => can_derive_default_ty(inner, ty_defs), + IdlType::Vec(inner) => can_derive_default_ty(inner, ty_defs), + IdlType::Array(inner, len) => { + if !can_derive_default_ty(inner, ty_defs) { + return false; + } + + match len { + IdlArrayLen::Value(len) => *len <= 32, + IdlArrayLen::Generic(_) => false, + } + } + IdlType::Defined { name, .. } => ty_defs + .iter() + .find(|ty_def| &ty_def.name == name) + .map(|ty_def| can_derive_default(ty_def, ty_defs)) + .expect("Type def must exist"), + IdlType::Generic(_) => false, + _ => true, + } +} + +fn can_derive_common( + fields: Option<&IdlDefinedFields>, + ty_defs: &[IdlTypeDef], + can_derive_ty: fn(&IdlType, &[IdlTypeDef]) -> bool, +) -> bool { + handle_defined_fields( + fields, + || true, + |fields| { + fields + .iter() + .map(|field| &field.ty) + .all(|ty| can_derive_ty(ty, ty_defs)) + }, + |tys| tys.iter().all(|ty| can_derive_ty(ty, ty_defs)), + ) +} + +fn handle_defined_fields( + fields: Option<&IdlDefinedFields>, + unit_cb: impl Fn() -> R, + named_cb: impl Fn(&[IdlField]) -> R, + tuple_cb: impl Fn(&[IdlType]) -> R, +) -> R { + match fields { + Some(fields) => match fields { + IdlDefinedFields::Named(fields) => named_cb(fields), + IdlDefinedFields::Tuple(tys) => tuple_cb(tys), + }, + _ => unit_cb(), + } +} diff --git a/lang/attribute/program/src/declare_program/mod.rs b/lang/attribute/program/src/declare_program/mod.rs new file mode 100644 index 0000000000..ad809f291f --- /dev/null +++ b/lang/attribute/program/src/declare_program/mod.rs @@ -0,0 +1,115 @@ +mod common; +mod mods; + +use anchor_syn::idl::types::Idl; +use anyhow::anyhow; +use quote::{quote, ToTokens}; +use syn::parse::{Parse, ParseStream}; + +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, +}; + +pub struct DeclareProgram { + name: syn::Ident, + idl: Idl, +} + +impl Parse for DeclareProgram { + fn parse(input: ParseStream) -> syn::Result { + let name = input.parse()?; + let idl = get_idl(&name).map_err(|e| syn::Error::new(name.span(), e))?; + Ok(Self { name, idl }) + } +} + +impl ToTokens for DeclareProgram { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + let program = gen_program(&self.idl, &self.name); + tokens.extend(program) + } +} + +fn get_idl(name: &syn::Ident) -> anyhow::Result { + let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").expect("Failed to get manifest dir"); + let path = std::path::Path::new(&manifest_dir) + .ancestors() + .find_map(|ancestor| { + let idl_dir = ancestor.join("idls"); + std::fs::metadata(&idl_dir).map(|_| idl_dir).ok() + }) + .ok_or_else(|| anyhow!("`idls` directory not found")) + .map(|idl_dir| idl_dir.join(name.to_string()).with_extension("json"))?; + + std::fs::read(path) + .map_err(|e| anyhow!("Failed to read IDL: {e}")) + .map(|idl| serde_json::from_slice(&idl))? + .map_err(|e| anyhow!("Failed to parse IDL: {e}")) +} + +fn gen_program(idl: &Idl, name: &syn::Ident) -> proc_macro2::TokenStream { + let docs = gen_program_docs(idl); + let id = gen_id(idl); + let program_mod = gen_program_mod(&idl.metadata.name); + + // Defined + let constants_mod = gen_constants_mod(idl); + let accounts_mod = gen_accounts_mod(idl); + let events_mod = gen_events_mod(idl); + let types_mod = gen_types_mod(idl); + + // Clients + let cpi_mod = gen_cpi_mod(idl); + let client_mod = gen_client_mod(idl); + let internal_mod = gen_internal_mod(idl); + + quote! { + #docs + pub mod #name { + use anchor_lang::prelude::*; + + #id + #program_mod + + #constants_mod + #accounts_mod + #events_mod + #types_mod + + #cpi_mod + #client_mod + #internal_mod + } + } +} + +fn gen_program_docs(idl: &Idl) -> proc_macro2::TokenStream { + let docs: &[String] = &[ + format!( + "Generated external program declaration of program `{}`.", + idl.metadata.name + ), + String::default(), + ]; + let docs = [docs, &idl.docs].concat(); + gen_docs(&docs) +} + +fn gen_id(idl: &Idl) -> proc_macro2::TokenStream { + let address_bytes = bs58::decode(&idl.address) + .into_vec() + .expect("Invalid `idl.address`"); + let doc = format!("Program ID of program `{}`.", idl.metadata.name); + + quote! { + #[doc = #doc] + pub static ID: Pubkey = __ID; + + /// The name is intentionally prefixed with `__` in order to reduce to possibility of name + /// clashes with the crate's `ID`. + static __ID: Pubkey = Pubkey::new_from_array([#(#address_bytes,)*]); + } +} diff --git a/lang/attribute/program/src/declare_program/mods/accounts.rs b/lang/attribute/program/src/declare_program/mods/accounts.rs new file mode 100644 index 0000000000..111807e850 --- /dev/null +++ b/lang/attribute/program/src/declare_program/mods/accounts.rs @@ -0,0 +1,118 @@ +use anchor_syn::idl::types::{Idl, IdlSerialization}; +use quote::{format_ident, quote}; + +use super::common::{convert_idl_type_def_to_ts, gen_discriminator, get_canonical_program_id}; + +pub fn gen_accounts_mod(idl: &Idl) -> proc_macro2::TokenStream { + let accounts = idl.accounts.iter().map(|acc| { + let name = format_ident!("{}", acc.name); + let discriminator = gen_discriminator(&acc.discriminator); + + let ty_def = idl + .types + .iter() + .find(|ty| ty.name == acc.name) + .expect("Type must exist"); + + let impls = { + let try_deserialize = quote! { + fn try_deserialize(buf: &mut &[u8]) -> anchor_lang::Result { + if buf.len() < #discriminator.len() { + return Err(anchor_lang::error::ErrorCode::AccountDiscriminatorNotFound.into()); + } + + let given_disc = &buf[..8]; + if &#discriminator != given_disc { + return Err( + anchor_lang::error!(anchor_lang::error::ErrorCode::AccountDiscriminatorMismatch) + .with_account_name(stringify!(#name)) + ); + } + + Self::try_deserialize_unchecked(buf) + } + }; + match ty_def.serialization { + IdlSerialization::Borsh => quote! { + impl anchor_lang::AccountSerialize for #name { + fn try_serialize(&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(()) + } + } + + impl anchor_lang::AccountDeserialize for #name { + #try_deserialize + + fn try_deserialize_unchecked(buf: &mut &[u8]) -> anchor_lang::Result { + let mut data: &[u8] = &buf[8..]; + AnchorDeserialize::deserialize(&mut data) + .map_err(|_| anchor_lang::error::ErrorCode::AccountDidNotDeserialize.into()) + } + } + }, + _ => { + let unsafe_bytemuck_impl = + matches!(ty_def.serialization, IdlSerialization::BytemuckUnsafe) + .then(|| { + quote! { + unsafe impl anchor_lang::__private::Pod for #name {} + unsafe impl anchor_lang::__private::Zeroable for #name {} + } + }) + .unwrap_or_default(); + + quote! { + impl anchor_lang::ZeroCopy for #name {} + + impl anchor_lang::AccountDeserialize for #name { + #try_deserialize + + fn try_deserialize_unchecked(buf: &mut &[u8]) -> anchor_lang::Result { + let data: &[u8] = &buf[8..]; + let account = anchor_lang::__private::bytemuck::from_bytes(data); + Ok(*account) + } + } + + #unsafe_bytemuck_impl + } + } + } + }; + + let type_def_ts = convert_idl_type_def_to_ts(ty_def, &idl.types); + let program_id = get_canonical_program_id(); + + quote! { + #type_def_ts + + #impls + + impl anchor_lang::Discriminator for #name { + const DISCRIMINATOR: [u8; 8] = #discriminator; + } + + impl anchor_lang::Owner for #name { + fn owner() -> Pubkey { + #program_id + } + } + } + }); + + quote! { + /// Program account type definitions. + pub mod accounts { + use super::{*, types::*}; + + #(#accounts)* + } + } +} diff --git a/lang/attribute/program/src/declare_program/mods/client.rs b/lang/attribute/program/src/declare_program/mods/client.rs new file mode 100644 index 0000000000..840f1d18c0 --- /dev/null +++ b/lang/attribute/program/src/declare_program/mods/client.rs @@ -0,0 +1,32 @@ +use anchor_syn::idl::types::Idl; +use quote::quote; + +use super::common::gen_accounts_common; + +pub fn gen_client_mod(idl: &Idl) -> proc_macro2::TokenStream { + let client_args_mod = gen_client_args_mod(); + let client_accounts_mod = gen_client_accounts_mod(idl); + + quote! { + /// Off-chain client helpers. + pub mod client { + use super::*; + + #client_args_mod + #client_accounts_mod + } + } +} + +fn gen_client_args_mod() -> proc_macro2::TokenStream { + quote! { + /// Client args. + pub mod args { + pub use super::internal::args::*; + } + } +} + +fn gen_client_accounts_mod(idl: &Idl) -> proc_macro2::TokenStream { + gen_accounts_common(idl, "client") +} diff --git a/lang/attribute/program/src/declare_program/mods/constants.rs b/lang/attribute/program/src/declare_program/mods/constants.rs new file mode 100644 index 0000000000..c4fe8970e8 --- /dev/null +++ b/lang/attribute/program/src/declare_program/mods/constants.rs @@ -0,0 +1,29 @@ +use anchor_syn::idl::types::{Idl, IdlType}; +use quote::{format_ident, quote, ToTokens}; + +use super::common::convert_idl_type_to_str; + +pub fn gen_constants_mod(idl: &Idl) -> proc_macro2::TokenStream { + let constants = idl.constants.iter().map(|c| { + let name = format_ident!("{}", c.name); + let ty = match &c.ty { + IdlType::String => quote!(&str), + _ => parse_expr_ts(&convert_idl_type_to_str(&c.ty)), + }; + let val = parse_expr_ts(&c.value); + + // TODO: Docs + quote! { pub const #name: #ty = #val; } + }); + + quote! { + /// Program constants. + pub mod constants { + #(#constants)* + } + } +} + +fn parse_expr_ts(s: &str) -> proc_macro2::TokenStream { + syn::parse_str::(s).unwrap().to_token_stream() +} diff --git a/lang/attribute/program/src/declare_program/mods/cpi.rs b/lang/attribute/program/src/declare_program/mods/cpi.rs new file mode 100644 index 0000000000..fc14af2fc5 --- /dev/null +++ b/lang/attribute/program/src/declare_program/mods/cpi.rs @@ -0,0 +1,116 @@ +use anchor_syn::idl::types::Idl; +use heck::CamelCase; +use quote::{format_ident, quote}; + +use super::common::{convert_idl_type_to_syn_type, gen_accounts_common, gen_discriminator}; + +pub fn gen_cpi_mod(idl: &Idl) -> proc_macro2::TokenStream { + let cpi_instructions = gen_cpi_instructions(idl); + let cpi_return_type = gen_cpi_return_type(); + let cpi_accounts_mod = gen_cpi_accounts_mod(idl); + + quote! { + /// Cross program invocation (CPI) helpers. + pub mod cpi { + use super::*; + + #cpi_instructions + #cpi_return_type + #cpi_accounts_mod + } + } +} + +fn gen_cpi_instructions(idl: &Idl) -> proc_macro2::TokenStream { + let ixs = idl.instructions.iter().map(|ix| { + let method_name = format_ident!("{}", ix.name); + let accounts_ident = format_ident!("{}", ix.name.to_camel_case()); + + let args = ix.args.iter().map(|arg| { + let name = format_ident!("{}", arg.name); + let ty = convert_idl_type_to_syn_type(&arg.ty); + quote! { #name: #ty } + }); + + let arg_value = if ix.args.is_empty() { + quote! { #accounts_ident } + } else { + let fields= ix.args.iter().map(|arg| format_ident!("{}", arg.name)); + quote! { + #accounts_ident { + #(#fields),* + } + } + }; + + let discriminator = gen_discriminator(&ix.discriminator); + + let (ret_type, ret_value) = match ix.returns.as_ref() { + Some(ty) => { + let ty = convert_idl_type_to_syn_type(ty); + ( + quote! { anchor_lang::Result> }, + quote! { Ok(Return::<#ty> { phantom:: std::marker::PhantomData }) }, + ) + }, + None => ( + quote! { anchor_lang::Result<()> }, + quote! { Ok(()) }, + ) + }; + + quote! { + pub fn #method_name<'a, 'b, 'c, 'info>( + ctx: anchor_lang::context::CpiContext<'a, 'b, 'c, 'info, accounts::#accounts_ident<'info>>, + #(#args),* + ) -> #ret_type { + let ix = { + let mut data = Vec::with_capacity(256); + data.extend_from_slice(&#discriminator); + AnchorSerialize::serialize(&internal::args::#arg_value, &mut data) + .map_err(|_| anchor_lang::error::ErrorCode::InstructionDidNotSerialize)?; + + let accounts = ctx.to_account_metas(None); + anchor_lang::solana_program::instruction::Instruction { + program_id: ctx.program.key(), + accounts, + data, + } + }; + + let mut acc_infos = ctx.to_account_infos(); + anchor_lang::solana_program::program::invoke_signed( + &ix, + &acc_infos, + ctx.signer_seeds, + ).map_or_else( + |e| Err(Into::into(e)), + |_| { #ret_value } + ) + } + } + }); + + quote! { + #(#ixs)* + } +} + +fn gen_cpi_return_type() -> proc_macro2::TokenStream { + quote! { + pub struct Return { + phantom: std::marker::PhantomData + } + + impl Return { + pub fn get(&self) -> T { + let (_key, data) = anchor_lang::solana_program::program::get_return_data().unwrap(); + T::try_from_slice(&data).unwrap() + } + } + } +} + +fn gen_cpi_accounts_mod(idl: &Idl) -> proc_macro2::TokenStream { + gen_accounts_common(idl, "cpi_client") +} diff --git a/lang/attribute/program/src/declare_program/mods/events.rs b/lang/attribute/program/src/declare_program/mods/events.rs new file mode 100644 index 0000000000..f6730ee0f0 --- /dev/null +++ b/lang/attribute/program/src/declare_program/mods/events.rs @@ -0,0 +1,45 @@ +use anchor_syn::idl::types::Idl; +use quote::{format_ident, quote}; + +use super::common::{convert_idl_type_def_to_ts, gen_discriminator}; + +pub fn gen_events_mod(idl: &Idl) -> proc_macro2::TokenStream { + let events = idl.events.iter().map(|ev| { + let name = format_ident!("{}", ev.name); + let discriminator = gen_discriminator(&ev.discriminator); + + let ty_def = idl + .types + .iter() + .find(|ty| ty.name == ev.name) + .map(|ty| convert_idl_type_def_to_ts(ty, &idl.types)) + .expect("Type must exist"); + + quote! { + #[derive(anchor_lang::__private::EventIndex)] + #ty_def + + impl anchor_lang::Event for #name { + fn data(&self) -> Vec { + let mut data = Vec::with_capacity(256); + data.extend_from_slice(&#discriminator); + self.serialize(&mut data).unwrap(); + data + } + } + + impl anchor_lang::Discriminator for #name { + const DISCRIMINATOR: [u8; 8] = #discriminator; + } + } + }); + + quote! { + /// Program event type definitions. + pub mod events { + use super::{*, types::*}; + + #(#events)* + } + } +} diff --git a/lang/attribute/program/src/declare_program/mods/internal.rs b/lang/attribute/program/src/declare_program/mods/internal.rs new file mode 100644 index 0000000000..635615e02d --- /dev/null +++ b/lang/attribute/program/src/declare_program/mods/internal.rs @@ -0,0 +1,149 @@ +use anchor_syn::{ + codegen::accounts::{__client_accounts, __cpi_client_accounts}, + idl::types::{Idl, IdlInstructionAccountItem}, + parser::accounts, + AccountsStruct, +}; +use heck::CamelCase; +use quote::{format_ident, quote}; + +use super::common::{convert_idl_type_to_syn_type, gen_discriminator, get_canonical_program_id}; + +pub fn gen_internal_mod(idl: &Idl) -> proc_macro2::TokenStream { + let internal_args_mod = gen_internal_args_mod(idl); + let internal_accounts_mod = gen_internal_accounts(idl); + + quote! { + #[doc(hidden)] + mod internal { + use super::*; + + #internal_args_mod + #internal_accounts_mod + } + } +} + +fn gen_internal_args_mod(idl: &Idl) -> proc_macro2::TokenStream { + let ixs = idl.instructions.iter().map(|ix| { + let ix_struct_name = format_ident!("{}", ix.name.to_camel_case()); + + let fields = ix.args.iter().map(|arg| { + let name = format_ident!("{}", arg.name); + let ty = convert_idl_type_to_syn_type(&arg.ty); + quote! { pub #name: #ty } + }); + + let ix_struct = if ix.args.is_empty() { + quote! { + pub struct #ix_struct_name; + } + } else { + quote! { + pub struct #ix_struct_name { + #(#fields),* + } + } + }; + + let impl_discriminator = if ix.discriminator.len() == 8 { + let discriminator = gen_discriminator(&ix.discriminator); + quote! { + impl anchor_lang::Discriminator for #ix_struct_name { + const DISCRIMINATOR: [u8; 8] = #discriminator; + } + } + } else { + quote! {} + }; + + let impl_ix_data = quote! { + impl anchor_lang::InstructionData for #ix_struct_name {} + }; + + let program_id = get_canonical_program_id(); + let impl_owner = quote! { + impl anchor_lang::Owner for #ix_struct_name { + fn owner() -> Pubkey { + #program_id + } + } + }; + + quote! { + /// Instruction argument + #[derive(AnchorSerialize, AnchorDeserialize)] + #ix_struct + + #impl_discriminator + #impl_ix_data + #impl_owner + } + }); + + quote! { + /// An Anchor generated module containing the program's set of instructions, where each + /// method handler in the `#[program]` mod is associated with a struct defining the input + /// arguments to the method. These should be used directly, when one wants to serialize + /// Anchor instruction data, for example, when specifying instructions instructions on a + /// client. + pub mod args { + use super::*; + + #(#ixs)* + } + } +} + +fn gen_internal_accounts(idl: &Idl) -> proc_macro2::TokenStream { + let cpi_accounts = gen_internal_accounts_common(idl, __cpi_client_accounts::generate); + let client_accounts = gen_internal_accounts_common(idl, __client_accounts::generate); + + quote! { + #cpi_accounts + #client_accounts + } +} + +fn gen_internal_accounts_common( + idl: &Idl, + gen_accounts: impl Fn(&AccountsStruct) -> proc_macro2::TokenStream, +) -> proc_macro2::TokenStream { + let accounts = idl + .instructions + .iter() + .map(|ix| { + let ident = format_ident!("{}", ix.name.to_camel_case()); + let generics = if ix.accounts.is_empty() { + quote!() + } else { + quote!(<'info>) + }; + let accounts = ix.accounts.iter().map(|acc| match acc { + IdlInstructionAccountItem::Single(acc) => { + let name = format_ident!("{}", acc.name); + if acc.optional { + quote! { pub #name: Option } + } else { + quote! { pub #name: AccountInfo #generics } + } + } + IdlInstructionAccountItem::Composite(_accs) => todo!("Composite"), + }); + + quote! { + #[derive(Accounts)] + pub struct #ident #generics { + #(#accounts,)* + } + } + }) + .map(|accs_struct| { + let accs_struct = syn::parse2(accs_struct).expect("Failed to parse as syn::ItemStruct"); + let accs_struct = + accounts::parse(&accs_struct).expect("Failed to parse accounts struct"); + gen_accounts(&accs_struct) + }); + + quote! { #(#accounts)* } +} diff --git a/lang/attribute/program/src/declare_program/mods/mod.rs b/lang/attribute/program/src/declare_program/mods/mod.rs new file mode 100644 index 0000000000..4c0d3bbe48 --- /dev/null +++ b/lang/attribute/program/src/declare_program/mods/mod.rs @@ -0,0 +1,10 @@ +pub mod accounts; +pub mod client; +pub mod constants; +pub mod cpi; +pub mod events; +pub mod internal; +pub mod program; +pub mod types; + +use super::common; diff --git a/lang/attribute/program/src/declare_program/mods/program.rs b/lang/attribute/program/src/declare_program/mods/program.rs new file mode 100644 index 0000000000..46806e831d --- /dev/null +++ b/lang/attribute/program/src/declare_program/mods/program.rs @@ -0,0 +1,25 @@ +use heck::CamelCase; +use quote::{format_ident, quote}; + +use super::common::get_canonical_program_id; + +pub fn gen_program_mod(program_name: &str) -> proc_macro2::TokenStream { + let name = format_ident!("{}", program_name.to_camel_case()); + let id = get_canonical_program_id(); + quote! { + /// Program definition. + pub mod program { + use super::*; + + /// Program type + #[derive(Clone)] + pub struct #name; + + impl anchor_lang::Id for #name { + fn id() -> Pubkey { + #id + } + } + } + } +} diff --git a/lang/attribute/program/src/declare_program/mods/types.rs b/lang/attribute/program/src/declare_program/mods/types.rs new file mode 100644 index 0000000000..c2dd2970ac --- /dev/null +++ b/lang/attribute/program/src/declare_program/mods/types.rs @@ -0,0 +1,28 @@ +use anchor_syn::idl::types::Idl; +use quote::quote; + +use super::common::convert_idl_type_def_to_ts; + +pub fn gen_types_mod(idl: &Idl) -> proc_macro2::TokenStream { + let types = idl + .types + .iter() + .filter(|ty| { + // Skip accounts and events + !(idl.accounts.iter().any(|acc| acc.name == ty.name) + || idl.events.iter().any(|ev| ev.name == ty.name)) + }) + .map(|ty| convert_idl_type_def_to_ts(ty, &idl.types)); + + quote! { + /// Program type definitions. + /// + /// Note that account and event type definitions are not included in this module, as they + /// have their own dedicated modules. + pub mod types { + use super::*; + + #(#types)* + } + } +} diff --git a/lang/attribute/program/src/lib.rs b/lang/attribute/program/src/lib.rs index 7c506d447a..bdd77ac9cc 100644 --- a/lang/attribute/program/src/lib.rs +++ b/lang/attribute/program/src/lib.rs @@ -1,5 +1,8 @@ extern crate proc_macro; +mod declare_program; + +use declare_program::DeclareProgram; use quote::ToTokens; use syn::parse_macro_input; @@ -15,6 +18,40 @@ pub fn program( .into() } +/// Declare an external program based on its IDL. +/// +/// The IDL of the program must exist in a directory named `idls`. This directory can be at any +/// depth, e.g. both inside the program's directory (`/idls`) and inside Anchor +/// workspace root directory (`/../../idls`) are valid. +/// +/// # Usage +/// +/// ```rs +/// declare_program!(program_name); +/// ``` +/// +/// This generates a module named `external_program` that can be used to interact with the program +/// without having to add the program's crate as a dependency. +/// +/// Both on-chain and off-chain usage is supported. +/// +/// Use `cargo doc --open` to see the generated modules and their documentation. +/// +/// # Note +/// +/// Re-defining the same program to use the same definitions should be avoided since this results +/// in larger binary size. +/// +/// A program should only be defined once. If you have multiple programs that depend on the same +/// definition, you should consider creating a separate crate for the external program definition +/// and reuse it in your programs. +#[proc_macro] +pub fn declare_program(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + parse_macro_input!(input as DeclareProgram) + .to_token_stream() + .into() +} + /// The `#[interface]` attribute is used to mark an instruction as belonging /// to an interface implementation, thus transforming its discriminator to the /// proper bytes for that interface instruction. diff --git a/lang/src/lib.rs b/lang/src/lib.rs index 194646f944..15afd68673 100644 --- a/lang/src/lib.rs +++ b/lang/src/lib.rs @@ -51,7 +51,7 @@ pub use anchor_attribute_account::{account, declare_id, zero_copy}; pub use anchor_attribute_constant::constant; pub use anchor_attribute_error::*; pub use anchor_attribute_event::{emit, event}; -pub use anchor_attribute_program::program; +pub use anchor_attribute_program::{declare_program, program}; pub use anchor_derive_accounts::Accounts; pub use anchor_derive_serde::{AnchorDeserialize, AnchorSerialize}; pub use anchor_derive_space::InitSpace; @@ -392,9 +392,10 @@ pub mod prelude { accounts::interface_account::InterfaceAccount, accounts::program::Program, accounts::signer::Signer, accounts::system_account::SystemAccount, accounts::sysvar::Sysvar, accounts::unchecked_account::UncheckedAccount, constant, - context::Context, context::CpiContext, declare_id, emit, err, error, event, program, - require, require_eq, require_gt, require_gte, require_keys_eq, require_keys_neq, - require_neq, solana_program::bpf_loader_upgradeable::UpgradeableLoaderState, source, + context::Context, context::CpiContext, declare_id, declare_program, emit, err, error, + event, program, require, require_eq, require_gt, require_gte, require_keys_eq, + require_keys_neq, require_neq, + solana_program::bpf_loader_upgradeable::UpgradeableLoaderState, source, system_program::System, zero_copy, AccountDeserialize, AccountSerialize, Accounts, AccountsClose, AccountsExit, AnchorDeserialize, AnchorSerialize, Id, InitSpace, Key, Lamports, Owner, ProgramData, Result, Space, ToAccountInfo, ToAccountInfos, ToAccountMetas, diff --git a/lang/syn/src/codegen/accounts/mod.rs b/lang/syn/src/codegen/accounts/mod.rs index fec6265602..5789262e9b 100644 --- a/lang/syn/src/codegen/accounts/mod.rs +++ b/lang/syn/src/codegen/accounts/mod.rs @@ -5,8 +5,8 @@ use syn::punctuated::Punctuated; use syn::{ConstParam, LifetimeDef, Token, TypeParam}; use syn::{GenericParam, PredicateLifetime, WhereClause, WherePredicate}; -mod __client_accounts; -mod __cpi_client_accounts; +pub mod __client_accounts; +pub mod __cpi_client_accounts; mod bumps; mod constraints; mod exit; From 440424a8c8b19475d71f7189f329c19016089633 Mon Sep 17 00:00:00 2001 From: acheron Date: Wed, 20 Mar 2024 01:39:04 +0100 Subject: [PATCH 2/4] Add `signer` and `mut` attrs --- .../src/declare_program/mods/internal.rs | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/lang/attribute/program/src/declare_program/mods/internal.rs b/lang/attribute/program/src/declare_program/mods/internal.rs index 635615e02d..69e3c7e70a 100644 --- a/lang/attribute/program/src/declare_program/mods/internal.rs +++ b/lang/attribute/program/src/declare_program/mods/internal.rs @@ -122,10 +122,27 @@ fn gen_internal_accounts_common( let accounts = ix.accounts.iter().map(|acc| match acc { IdlInstructionAccountItem::Single(acc) => { let name = format_ident!("{}", acc.name); - if acc.optional { - quote! { pub #name: Option } - } else { - quote! { pub #name: AccountInfo #generics } + + let attrs = { + let signer = acc.signer.then(|| quote!(signer)).unwrap_or_default(); + let mt = acc.writable.then(|| quote!(mut)).unwrap_or_default(); + if signer.is_empty() { + mt + } else if mt.is_empty() { + signer + } else { + quote! { #signer, #mt } + } + }; + + let acc_expr = acc + .optional + .then(|| quote! { Option }) + .unwrap_or_else(|| quote! { AccountInfo #generics }); + + quote! { + #[account(#attrs)] + pub #name: #acc_expr } } IdlInstructionAccountItem::Composite(_accs) => todo!("Composite"), From 1c153b19c39f2d9e5bfa8efb583f5b59aa8f627a Mon Sep 17 00:00:00 2001 From: acheron Date: Mon, 25 Mar 2024 21:40:42 +0100 Subject: [PATCH 3/4] Add tests --- tests/declare-program/Anchor.toml | 10 ++ tests/declare-program/Cargo.toml | 14 +++ tests/declare-program/idls/external.json | 114 ++++++++++++++++++ tests/declare-program/package.json | 16 +++ .../programs/declare-program/Cargo.toml | 20 +++ .../programs/declare-program/Xargo.toml | 2 + .../programs/declare-program/src/lib.rs | 39 ++++++ .../programs/external/Cargo.toml | 19 +++ .../programs/external/Xargo.toml | 2 + .../programs/external/src/lib.rs | 44 +++++++ .../declare-program/tests/declare-program.ts | 27 +++++ tests/declare-program/tsconfig.json | 10 ++ tests/package.json | 3 +- 13 files changed, 319 insertions(+), 1 deletion(-) create mode 100644 tests/declare-program/Anchor.toml create mode 100644 tests/declare-program/Cargo.toml create mode 100644 tests/declare-program/idls/external.json create mode 100644 tests/declare-program/package.json create mode 100644 tests/declare-program/programs/declare-program/Cargo.toml create mode 100644 tests/declare-program/programs/declare-program/Xargo.toml create mode 100644 tests/declare-program/programs/declare-program/src/lib.rs create mode 100644 tests/declare-program/programs/external/Cargo.toml create mode 100644 tests/declare-program/programs/external/Xargo.toml create mode 100644 tests/declare-program/programs/external/src/lib.rs create mode 100644 tests/declare-program/tests/declare-program.ts create mode 100644 tests/declare-program/tsconfig.json diff --git a/tests/declare-program/Anchor.toml b/tests/declare-program/Anchor.toml new file mode 100644 index 0000000000..afa1b300bd --- /dev/null +++ b/tests/declare-program/Anchor.toml @@ -0,0 +1,10 @@ +[programs.localnet] +declare_program = "Dec1areProgram11111111111111111111111111111" +external = "Externa111111111111111111111111111111111111" + +[provider] +cluster = "localnet" +wallet = "~/.config/solana/id.json" + +[scripts] +test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts" diff --git a/tests/declare-program/Cargo.toml b/tests/declare-program/Cargo.toml new file mode 100644 index 0000000000..f397704811 --- /dev/null +++ b/tests/declare-program/Cargo.toml @@ -0,0 +1,14 @@ +[workspace] +members = [ + "programs/*" +] +resolver = "2" + +[profile.release] +overflow-checks = true +lto = "fat" +codegen-units = 1 +[profile.release.build-override] +opt-level = 3 +incremental = false +codegen-units = 1 diff --git a/tests/declare-program/idls/external.json b/tests/declare-program/idls/external.json new file mode 100644 index 0000000000..93418ecbfc --- /dev/null +++ b/tests/declare-program/idls/external.json @@ -0,0 +1,114 @@ +{ + "address": "Externa111111111111111111111111111111111111", + "metadata": { + "name": "external", + "version": "0.1.0", + "spec": "0.1.0", + "description": "Created with Anchor" + }, + "instructions": [ + { + "name": "init", + "discriminator": [ + 220, + 59, + 207, + 236, + 108, + 250, + 47, + 100 + ], + "accounts": [ + { + "name": "authority", + "writable": true, + "signer": true + }, + { + "name": "my_account", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "authority" + } + ] + } + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [] + }, + { + "name": "update", + "discriminator": [ + 219, + 200, + 88, + 176, + 158, + 63, + 253, + 127 + ], + "accounts": [ + { + "name": "authority", + "signer": true + }, + { + "name": "my_account", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "authority" + } + ] + } + } + ], + "args": [ + { + "name": "value", + "type": "u32" + } + ] + } + ], + "accounts": [ + { + "name": "MyAccount", + "discriminator": [ + 246, + 28, + 6, + 87, + 251, + 45, + 50, + 42 + ] + } + ], + "types": [ + { + "name": "MyAccount", + "type": { + "kind": "struct", + "fields": [ + { + "name": "field", + "type": "u32" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/tests/declare-program/package.json b/tests/declare-program/package.json new file mode 100644 index 0000000000..5218d2867c --- /dev/null +++ b/tests/declare-program/package.json @@ -0,0 +1,16 @@ +{ + "name": "declare-program", + "version": "0.29.0", + "license": "(MIT OR Apache-2.0)", + "homepage": "https://github.com/coral-xyz/anchor#readme", + "bugs": { + "url": "https://github.com/coral-xyz/anchor/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/coral-xyz/anchor.git" + }, + "engines": { + "node": ">=17" + } +} diff --git a/tests/declare-program/programs/declare-program/Cargo.toml b/tests/declare-program/programs/declare-program/Cargo.toml new file mode 100644 index 0000000000..5a8385e452 --- /dev/null +++ b/tests/declare-program/programs/declare-program/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "declare-program" +version = "0.1.0" +description = "Created with Anchor" +rust-version = "1.60" +edition = "2021" + +[lib] +crate-type = ["cdylib", "lib"] +name = "declare_program" + +[features] +no-entrypoint = [] +no-idl = [] +cpi = ["no-entrypoint"] +default = [] +idl-build = ["anchor-lang/idl-build"] + +[dependencies] +anchor-lang = { path = "../../../../lang" } diff --git a/tests/declare-program/programs/declare-program/Xargo.toml b/tests/declare-program/programs/declare-program/Xargo.toml new file mode 100644 index 0000000000..1744f098ae --- /dev/null +++ b/tests/declare-program/programs/declare-program/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] \ 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 new file mode 100644 index 0000000000..6cd5ff5f0a --- /dev/null +++ b/tests/declare-program/programs/declare-program/src/lib.rs @@ -0,0 +1,39 @@ +use anchor_lang::prelude::*; + +declare_id!("Dec1areProgram11111111111111111111111111111"); + +declare_program!(external); +use external::program::External; + +#[program] +pub mod declare_program { + use super::*; + + pub fn cpi(ctx: Context, value: u32) -> Result<()> { + let cpi_my_account = &mut ctx.accounts.cpi_my_account; + require_keys_eq!(external::accounts::MyAccount::owner(), external::ID); + require_eq!(cpi_my_account.field, 0); + + let cpi_ctx = CpiContext::new( + ctx.accounts.external_program.to_account_info(), + external::cpi::accounts::Update { + authority: ctx.accounts.authority.to_account_info(), + my_account: cpi_my_account.to_account_info(), + }, + ); + external::cpi::update(cpi_ctx, value)?; + + cpi_my_account.reload()?; + require_eq!(cpi_my_account.field, value); + + Ok(()) + } +} + +#[derive(Accounts)] +pub struct Cpi<'info> { + pub authority: Signer<'info>, + #[account(mut)] + pub cpi_my_account: Account<'info, external::accounts::MyAccount>, + pub external_program: Program<'info, External>, +} diff --git a/tests/declare-program/programs/external/Cargo.toml b/tests/declare-program/programs/external/Cargo.toml new file mode 100644 index 0000000000..d0418f87a9 --- /dev/null +++ b/tests/declare-program/programs/external/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "external" +version = "0.1.0" +description = "Created with Anchor" +rust-version = "1.60" +edition = "2021" + +[lib] +crate-type = ["cdylib", "lib"] + +[features] +no-entrypoint = [] +no-idl = [] +cpi = ["no-entrypoint"] +default = [] +idl-build = ["anchor-lang/idl-build"] + +[dependencies] +anchor-lang = { path = "../../../../lang" } diff --git a/tests/declare-program/programs/external/Xargo.toml b/tests/declare-program/programs/external/Xargo.toml new file mode 100644 index 0000000000..1744f098ae --- /dev/null +++ b/tests/declare-program/programs/external/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] \ No newline at end of file diff --git a/tests/declare-program/programs/external/src/lib.rs b/tests/declare-program/programs/external/src/lib.rs new file mode 100644 index 0000000000..06945fa297 --- /dev/null +++ b/tests/declare-program/programs/external/src/lib.rs @@ -0,0 +1,44 @@ +use anchor_lang::prelude::*; + +declare_id!("Externa111111111111111111111111111111111111"); + +#[program] +pub mod external { + use super::*; + + pub fn init(_ctx: Context) -> Result<()> { + Ok(()) + } + + pub fn update(ctx: Context, value: u32) -> Result<()> { + ctx.accounts.my_account.field = value; + Ok(()) + } +} + +#[derive(Accounts)] +pub struct Init<'info> { + #[account(mut)] + pub authority: Signer<'info>, + #[account( + init, + payer = authority, + space = 8 + 4, + seeds = [authority.key.as_ref()], + bump + )] + pub my_account: Account<'info, MyAccount>, + pub system_program: Program<'info, System>, +} + +#[derive(Accounts)] +pub struct Update<'info> { + pub authority: Signer<'info>, + #[account(mut, seeds = [authority.key.as_ref()], bump)] + pub my_account: Account<'info, MyAccount>, +} + +#[account] +pub struct MyAccount { + pub field: u32, +} diff --git a/tests/declare-program/tests/declare-program.ts b/tests/declare-program/tests/declare-program.ts new file mode 100644 index 0000000000..988153f281 --- /dev/null +++ b/tests/declare-program/tests/declare-program.ts @@ -0,0 +1,27 @@ +import * as anchor from "@coral-xyz/anchor"; +import assert from "assert"; + +import type { DeclareProgram } from "../target/types/declare_program"; +import type { External } from "../target/types/external"; + +describe("declare-program", () => { + anchor.setProvider(anchor.AnchorProvider.env()); + const program: anchor.Program = + anchor.workspace.declareProgram; + const externalProgram: anchor.Program = anchor.workspace.external; + + it("Can CPI", async () => { + const { pubkeys } = await externalProgram.methods.init().rpcAndKeys(); + + const value = 5; + await program.methods + .cpi(value) + .accounts({ cpiMyAccount: pubkeys.myAccount }) + .rpc(); + + const myAccount = await externalProgram.account.myAccount.fetch( + pubkeys.myAccount + ); + assert.strictEqual(myAccount.field, value); + }); +}); diff --git a/tests/declare-program/tsconfig.json b/tests/declare-program/tsconfig.json new file mode 100644 index 0000000000..feba6ca17f --- /dev/null +++ b/tests/declare-program/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "types": ["mocha", "chai"], + "lib": ["es2015"], + "module": "commonjs", + "target": "es6", + "esModuleInterop": true, + "strict": true + } +} diff --git a/tests/package.json b/tests/package.json index 7f2a29c2c8..49d90f9e4e 100644 --- a/tests/package.json +++ b/tests/package.json @@ -15,6 +15,8 @@ "chat", "composite", "custom-coder", + "declare-id", + "declare-program", "errors", "escrow", "events", @@ -42,7 +44,6 @@ "typescript", "validator-clone", "zero-copy", - "declare-id", "cpi-returns", "multiple-suites", "multiple-suites-run-single", From 823592f991670413edbe96a81ce2c0b90528994f Mon Sep 17 00:00:00 2001 From: acheron Date: Mon, 25 Mar 2024 22:42:50 +0100 Subject: [PATCH 4/4] Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c820d6e80..4dd181d31f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,7 @@ The minor version will be incremented upon a breaking change and the patch versi - cli: Add `--no-idl` flag to the `build` command ([#2847](https://github.com/coral-xyz/anchor/pull/2847)). - cli: Add priority fees to idl commands ([#2845](https://github.com/coral-xyz/anchor/pull/2845)). - ts: Add `prepend` option to MethodBuilder `preInstructions` method ([#2863](https://github.com/coral-xyz/anchor/pull/2863)). +- lang: Add `declare_program!` macro ([#2857](https://github.com/coral-xyz/anchor/pull/2857)). ### Fixes