Skip to main content
The declare_program!() macro enables dependency-free interaction with Anchor programs by generating Rust modules from a program’s IDL file. This eliminates the need to add the external program as a crate dependency.

Overview

Traditionally, to interact with an external program, you would add it as a dependency in Cargo.toml. The declare_program!() macro provides an alternative approach: Traditional Approach:
[dependencies]
external_program = { version = "0.1.0", features = ["cpi"] }
With declare_program!:
declare_program!(external_program);
The macro generates all necessary modules for both on-chain (CPI) and off-chain (client) interactions.

Generated Modules

The declare_program!() macro generates the following modules:
ModuleDescriptionUse Case
cpiCross-program invocation helpersMaking CPIs from on-chain programs
clientClient instruction buildersBuilding transactions from off-chain clients
accountsAccount data typesAccessing program state
programProgram ID constantIdentifying the program
constantsProgram constantsUsing program-defined constants
eventsProgram eventsListening to program events
typesCustom typesUsing program-defined structs/enums
errorsProgram errorsHandling program-specific errors

Setup

1. Obtain the IDL File

You need the target program’s IDL file (JSON format). Place it in an /idls directory anywhere in your project structure:
project/
├── idls/
│   └── external_program.json
├── programs/
│   └── your_program/
│       └── src/
│           └── lib.rs
└── Cargo.toml
The /idls directory can be at any level in your project.

2. Invoke the Macro

In your program, invoke the macro with the IDL filename (without extension):
use anchor_lang::prelude::*;

declare_program!(external_program);  // Looks for idls/external_program.json

On-Chain Usage (CPI)

Use the generated cpi module to make cross-program invocations.

Complete Example

Let’s say you want to call an external counter program: Target Program (external_program):
use anchor_lang::prelude::*;

declare_id!("ExternalProgram11111111111111111111111111111");

#[program]
pub mod external_program {
    use super::*;

    pub fn increment(ctx: Context<Increment>) -> Result<()> {
        let counter = &mut ctx.accounts.counter;
        counter.count += 1;
        Ok(())
    }
}

#[derive(Accounts)]
pub struct Increment<'info> {
    #[account(mut)]
    pub counter: Account<'info, Counter>,
}

#[account]
pub struct Counter {
    pub count: u64,
}
Your Program (caller):
use anchor_lang::prelude::*;

declare_id!("YourProgram111111111111111111111111111111111");

// Generate modules from IDL
declare_program!(external_program);

// Import generated types
use external_program::{
    accounts::Counter,
    cpi::{self, accounts::Increment},
    program::ExternalProgram,
};

#[program]
pub mod your_program {
    use super::*;

    pub fn call_external_increment(ctx: Context<CallExternal>) -> Result<()> {
        // Create CPI context
        let cpi_ctx = CpiContext::new(
            ctx.accounts.external_program.to_account_info(),
            Increment {
                counter: ctx.accounts.counter.to_account_info(),
            },
        );

        // Make CPI call
        cpi::increment(cpi_ctx)?;
        Ok(())
    }
}

#[derive(Accounts)]
pub struct CallExternal<'info> {
    #[account(mut)]
    pub counter: Account<'info, Counter>,
    pub external_program: Program<'info, ExternalProgram>,
}

CPI with Seeds (PDA Signing)

When your program needs to sign the CPI as a PDA:
pub fn call_with_seeds(ctx: Context<CallWithSeeds>) -> Result<()> {
    let seeds = &[
        b"authority",
        &[ctx.bumps.authority],
    ];
    let signer_seeds = &[&seeds[..]];

    let cpi_ctx = CpiContext::new_with_signer(
        ctx.accounts.external_program.to_account_info(),
        Increment {
            counter: ctx.accounts.counter.to_account_info(),
        },
        signer_seeds,
    );

    cpi::increment(cpi_ctx)?;
    Ok(())
}

#[derive(Accounts)]
pub struct CallWithSeeds<'info> {
    #[account(
        mut,
        seeds = [b"authority"],
        bump,
    )]
    pub authority: SystemAccount<'info>,
    #[account(mut)]
    pub counter: Account<'info, Counter>,
    pub external_program: Program<'info, ExternalProgram>,
}

Off-Chain Usage (Client)

Use the generated client module to build instructions from your off-chain client.

Rust Client Example

use anchor_client::{
    solana_client::rpc_client::RpcClient,
    solana_sdk::{
        commitment_config::CommitmentConfig,
        signature::Keypair,
        signer::Signer,
    },
    Client, Cluster,
};
use anchor_lang::prelude::*;
use std::rc::Rc;

// Generate modules from IDL
declare_program!(external_program);

use external_program::{
    accounts::Counter,
    client::{accounts, args},
};

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let payer = Keypair::new();
    let counter = Keypair::new();

    // Create client
    let client = Client::new_with_options(
        Cluster::Localnet,
        Rc::new(payer),
        CommitmentConfig::confirmed(),
    );
    let program = client.program(external_program::ID)?;

    // Build increment instruction
    let increment_ix = program
        .request()
        .accounts(accounts::Increment {
            counter: counter.pubkey(),
        })
        .args(args::Increment)
        .instructions()?
        .remove(0);

    // Send transaction
    let signature = program
        .request()
        .instruction(increment_ix)
        .send()
        .await?;

    println!("Transaction signature: {}", signature);

    // Fetch account data
    let counter_account: Counter = program
        .account::<Counter>(counter.pubkey())
        .await?;
    println!("Counter: {}", counter_account.count);

    Ok(())
}

TypeScript Client

For TypeScript clients, use the generated IDL types:
import * as anchor from "@anchor-lang/core";
import { Program } from "@anchor-lang/core";
import { ExternalProgram } from "./target/types/external_program";

const program = anchor.workspace.ExternalProgram as Program<ExternalProgram>;

const counterKeypair = anchor.web3.Keypair.generate();

// Call increment instruction
const tx = await program.methods
  .increment()
  .accounts({
    counter: counterKeypair.publicKey,
  })
  .rpc();

console.log("Transaction signature:", tx);

// Fetch account
const counter = await program.account.counter.fetch(
  counterKeypair.publicKey
);
console.log("Count:", counter.count.toString());

Working with Events

If the external program emits events, use the generated events module:
use external_program::events::CounterUpdated;

pub fn handle_event(ctx: Context<HandleEvent>) -> Result<()> {
    // Events are typically read by off-chain listeners
    // but you can access event types for type safety
    Ok(())
}
Off-chain event listening (Rust):
let mut event_subscriber = program.events()?;

loop {
    let event = event_subscriber.next().await?;
    match event {
        external_program::events::CounterUpdated { old_count, new_count } => {
            println!("Counter updated: {} -> {}", old_count, new_count);
        }
        _ => {}
    }
}

Handling Errors

Access program-specific errors through the generated errors module:
use external_program::errors::ErrorCode;

pub fn handle_error(ctx: Context<HandleError>) -> Result<()> {
    // Check for specific error conditions
    if some_condition {
        return Err(ErrorCode::CustomError.into());
    }
    Ok(())
}

Working with Custom Types

Use the generated types module to access program-defined structs and enums:
use external_program::types::{CustomStruct, CustomEnum};

pub fn use_custom_types(
    ctx: Context<UseCustom>,
    data: CustomStruct,
    variant: CustomEnum,
) -> Result<()> {
    // Use the custom types
    msg!("Received custom data");
    Ok(())
}

Multiple Programs

Declare multiple external programs in the same file:
use anchor_lang::prelude::*;

// Declare multiple programs
declare_program!(program_a);
declare_program!(program_b);
declare_program!(program_c);

use program_a::cpi as program_a_cpi;
use program_b::cpi as program_b_cpi;
use program_c::cpi as program_c_cpi;

#[program]
pub mod your_program {
    use super::*;

    pub fn call_multiple(ctx: Context<CallMultiple>) -> Result<()> {
        // Call program A
        program_a_cpi::instruction_a(
            CpiContext::new(/* ... */),
        )?;

        // Call program B
        program_b_cpi::instruction_b(
            CpiContext::new(/* ... */),
        )?;

        Ok(())
    }
}

Advantages

Zero Dependencies

No need to add external programs as crate dependencies, reducing compilation time and dependency conflicts.

Version Flexibility

Work with different versions of external programs by simply swapping IDL files.

Unified Interface

Single macro generates both on-chain (CPI) and off-chain (client) code.

Type Safety

Fully typed interfaces generated from IDL ensure compile-time safety.

Limitations

Account Validation: The declare_program!() macro generates type definitions but doesn’t include the constraint validation logic from the original program. Always validate account relationships in your program.
IDL Availability: Requires the external program to publish its IDL. Programs without IDLs cannot be used with this macro.
Custom Types: Complex types that don’t derive AnchorSerialize/AnchorDeserialize may require manual handling.

Best Practices

Commit IDL files to version control to ensure reproducible builds:
# .gitignore
# Don't ignore IDL files
!idls/*.json
Always verify the program ID matches the expected address:
require_keys_eq!(
    ctx.accounts.external_program.key(),
    external_program::ID,
    ErrorCode::InvalidProgram
);
Document where IDL files come from:
// IDL from: https://github.com/project/external-program/releases/tag/v1.0.0
declare_program!(external_program);
Thoroughly test all CPI interactions:
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_external_cpi() {
        // Test CPI calls
    }
}

Troubleshooting

IDL Not Found

Error:
error: could not find IDL file for program 'external_program'
Solution: Ensure the IDL file exists in an /idls directory:
project/
└── idls/
    └── external_program.json

Module Not Found

Error:
error[E0433]: failed to resolve: use of undeclared crate or module `external_program`
Solution: Add the declare_program!() invocation:
declare_program!(external_program);

Type Mismatch

Error:
expected struct `Account<'_, Counter>`, found `InterfaceAccount<'_, ...>`
Solution: Use the generated account types:
use external_program::accounts::Counter;

pub counter: Account<'info, Counter>,

Resources

Build docs developers (and LLMs) love