lang: Add `declare_program!` macro (#2857)

This commit is contained in:
acheron 2024-03-25 23:14:02 +01:00 committed by GitHub
parent 4393d73d3d
commit 0f6090950a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 1419 additions and 8 deletions

View File

@ -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

5
Cargo.lock generated
View File

@ -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",
]

View File

@ -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"] }

View File

@ -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::<Vec<_>>();
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<R>(
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(),
}
}

View File

@ -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<Self> {
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<Idl> {
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,)*]);
}
}

View File

@ -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<Self> {
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<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(())
}
}
impl anchor_lang::AccountDeserialize for #name {
#try_deserialize
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())
}
}
},
_ => {
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<Self> {
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)*
}
}
}

View File

@ -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")
}

View File

@ -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::<syn::Expr>(s).unwrap().to_token_stream()
}

View File

@ -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<Return::<#ty>> },
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<T> {
phantom: std::marker::PhantomData<T>
}
impl<T: AnchorDeserialize> Return<T> {
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")
}

View File

@ -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<u8> {
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)*
}
}
}

View File

@ -0,0 +1,166 @@
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);
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<AccountInfo #generics> })
.unwrap_or_else(|| quote! { AccountInfo #generics });
quote! {
#[account(#attrs)]
pub #name: #acc_expr
}
}
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)* }
}

View File

@ -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;

View File

@ -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
}
}
}
}
}

View File

@ -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)*
}
}
}

View File

@ -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 (`<PROGRAM_DIR>/idls`) and inside Anchor
/// workspace root directory (`<PROGRAM_DIR>/../../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.

View File

@ -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,

View File

@ -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;

View File

@ -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"

View File

@ -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

View File

@ -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"
}
]
}
}
]
}

View File

@ -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"
}
}

View File

@ -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" }

View File

@ -0,0 +1,2 @@
[target.bpfel-unknown-unknown.dependencies.std]
features = []

View File

@ -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<Cpi>, 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>,
}

View File

@ -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" }

View File

@ -0,0 +1,2 @@
[target.bpfel-unknown-unknown.dependencies.std]
features = []

View File

@ -0,0 +1,44 @@
use anchor_lang::prelude::*;
declare_id!("Externa111111111111111111111111111111111111");
#[program]
pub mod external {
use super::*;
pub fn init(_ctx: Context<Init>) -> Result<()> {
Ok(())
}
pub fn update(ctx: Context<Update>, 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,
}

View File

@ -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<DeclareProgram> =
anchor.workspace.declareProgram;
const externalProgram: anchor.Program<External> = 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);
});
});

View File

@ -0,0 +1,10 @@
{
"compilerOptions": {
"types": ["mocha", "chai"],
"lib": ["es2015"],
"module": "commonjs",
"target": "es6",
"esModuleInterop": true,
"strict": true
}
}

View File

@ -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",