Przeglądaj źródła

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

acheron 1 rok temu
rodzic
commit
0f6090950a
30 zmienionych plików z 1419 dodań i 8 usunięć
  1. 1 0
      CHANGELOG.md
  2. 5 0
      Cargo.lock
  3. 6 1
      lang/attribute/program/Cargo.toml
  4. 360 0
      lang/attribute/program/src/declare_program/common.rs
  5. 115 0
      lang/attribute/program/src/declare_program/mod.rs
  6. 118 0
      lang/attribute/program/src/declare_program/mods/accounts.rs
  7. 32 0
      lang/attribute/program/src/declare_program/mods/client.rs
  8. 29 0
      lang/attribute/program/src/declare_program/mods/constants.rs
  9. 116 0
      lang/attribute/program/src/declare_program/mods/cpi.rs
  10. 45 0
      lang/attribute/program/src/declare_program/mods/events.rs
  11. 166 0
      lang/attribute/program/src/declare_program/mods/internal.rs
  12. 10 0
      lang/attribute/program/src/declare_program/mods/mod.rs
  13. 25 0
      lang/attribute/program/src/declare_program/mods/program.rs
  14. 28 0
      lang/attribute/program/src/declare_program/mods/types.rs
  15. 37 0
      lang/attribute/program/src/lib.rs
  16. 5 4
      lang/src/lib.rs
  17. 2 2
      lang/syn/src/codegen/accounts/mod.rs
  18. 10 0
      tests/declare-program/Anchor.toml
  19. 14 0
      tests/declare-program/Cargo.toml
  20. 114 0
      tests/declare-program/idls/external.json
  21. 16 0
      tests/declare-program/package.json
  22. 20 0
      tests/declare-program/programs/declare-program/Cargo.toml
  23. 2 0
      tests/declare-program/programs/declare-program/Xargo.toml
  24. 39 0
      tests/declare-program/programs/declare-program/src/lib.rs
  25. 19 0
      tests/declare-program/programs/external/Cargo.toml
  26. 2 0
      tests/declare-program/programs/external/Xargo.toml
  27. 44 0
      tests/declare-program/programs/external/src/lib.rs
  28. 27 0
      tests/declare-program/tests/declare-program.ts
  29. 10 0
      tests/declare-program/tsconfig.json
  30. 2 1
      tests/package.json

+ 1 - 0
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
 

+ 5 - 0
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",
 ]
 

+ 6 - 1
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"] }

+ 360 - 0
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::<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(),
+    }
+}

+ 115 - 0
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<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,)*]);
+    }
+}

+ 118 - 0
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<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)*
+        }
+    }
+}

+ 32 - 0
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")
+}

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

+ 116 - 0
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<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")
+}

+ 45 - 0
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<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)*
+        }
+    }
+}

+ 166 - 0
lang/attribute/program/src/declare_program/mods/internal.rs

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

+ 10 - 0
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;

+ 25 - 0
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
+                }
+            }
+        }
+    }
+}

+ 28 - 0
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)*
+        }
+    }
+}

+ 37 - 0
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 (`<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.

+ 5 - 4
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,

+ 2 - 2
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;

+ 10 - 0
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"

+ 14 - 0
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

+ 114 - 0
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"
+          }
+        ]
+      }
+    }
+  ]
+}

+ 16 - 0
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"
+  }
+}

+ 20 - 0
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" }

+ 2 - 0
tests/declare-program/programs/declare-program/Xargo.toml

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

+ 39 - 0
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<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>,
+}

+ 19 - 0
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" }

+ 2 - 0
tests/declare-program/programs/external/Xargo.toml

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

+ 44 - 0
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<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,
+}

+ 27 - 0
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<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);
+  });
+});

+ 10 - 0
tests/declare-program/tsconfig.json

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

+ 2 - 1
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",