Documentation

Cross Program Invocation (CPI) in Solana using Anchor

Learn how to use PDAs to invoke another Solana program through a Cross Program Invocation (CPI) in Anchor.

Banner

Introduction: What is a PDA?

In Solana, a Program Derived Address (PDA) is a special type of address generated using seeds and the Program ID. PDAs cannot be owned by a private key, which makes them ideal for use as program-owned accounts. They are deterministically derived, so you can always regenerate the same address given the same inputs.

What is a CPI?

A Cross Program Invocation (CPI) allows a Solana program to invoke another program from within its own instruction. This enables composability—where one program can build on top of or interact with others.

There are two types of CPI calls:

  • invoke: Used when all necessary signatures are already present.
  • invoke_signed: Used when a PDA must act as a signer, using seeds and bump.

When invoking another program, the original caller halts until the callee completes execution. This call stack can go up to 4 layers deep.


Overview

In this tutorial, we’ll walk through:

  • Creating two programs: program-a and program-b
  • Creating a PDA in program-a
  • Using that PDA as a signer to:
    • Transfer SOL from the PDA
    • Call program-b using CPI

We'll be using Anchor to scaffold and build the project.


Step 1: Scaffold Two Anchor Programs

Create the main program and a second one to be invoked.

anchor init program-a
cd program-a
anchor new program-b

Step 2: Configure program-a to Include program-b as Dependency

In programs/program-a/Cargo.toml:

[dependencies]
anchor-lang = "0.31.1"
program-b = { path = "../program-b/", features = ["cpi"] }

This enables CPI support and allows program-a to call program-b.


Step 3: Implement Program A

programs/program-a/src/lib.rs:

use anchor_lang::prelude::*;
use program_b::program::ProgramB;
declare_id!("7ijjWFV5ee5pSUChPMd4vYDMieYLXCyRzWMVRHiuKhXL");

#[program]
pub mod program_a {
    use anchor_lang::solana_program::{program::invoke_signed, system_instruction};

    use super::*;

    /*
    we get the pda account and the signer account.
    to invoke another solana program's instruction, we need to have correct seed of the PDA account.
    which allows us to sign the transaction. if the seed is not correct, the signature doesn't match
    and the cpi will fail.

    seeds and bumps need to be consistent and defined correctly to be able to invoke cpi correctly

    CpiContext::new_with_signer takes in the program and the instruction to invoke, and the signer's seed which in our case
    is b"BuildBear", signer's key and bump

    once you have the correct cpi_context you can use the context and invoke a call to program-b's initialize function
    */
    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        msg!("Greetings from: Program A");
        let pda_account = ctx.accounts.pda_account.key();
        let signer = ctx.accounts.signer.key();
        let bump = ctx.bumps.pda_account;
        let account_infos = [
            ctx.accounts.pda_account.to_account_info(),
            ctx.accounts.signer.to_account_info(),
            ctx.accounts.system_program.to_account_info(),
        ];
        let instruction = &system_instruction::transfer(&pda_account, &signer, 1_000_000_000);
        let signer_seeds: &[&[&[u8]]] = &[&[b"BuildBear", signer.as_ref(), &[bump]]];

        invoke_signed(instruction, &account_infos, signer_seeds)?;

        let cpi_context = CpiContext::new_with_signer(
            ctx.accounts.program_b.to_account_info(),
            program_b::cpi::accounts::Initialize {
                pda_account: ctx.accounts.pda_account.to_account_info(),
            },
            signer_seeds,
        );

        program_b::cpi::initialize(cpi_context)?;
        Ok(())
    }
}

/*
We define the pda_account as account info and pass in seeds and bump to be able to sign the transaction.
We also define the signer as a Signer type, which is required to sign the transaction and signer will invoke a call to program a which in turn invokes program b via cpi
*/
#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(
        mut,
        seeds=[b"BuildBear",signer.key().as_ref()],
        bump
    )]
    pub pda_account: AccountInfo<'info>,
    #[account(mut)]
    pub signer: Signer<'info>,
    pub system_program: Program<'info, System>,
    pub program_b: Program<'info, ProgramB>,
}

Step 4: Implement Program B

programs/program-b/src/lib.rs:

use anchor_lang::prelude::*;

declare_id!("3aLsvLgv2eCqDqAXvCRbLv2KEW73wUeRiCBPg2opSbV7");

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

    pub fn initialize(_ctx: Context<Initialize>) -> Result<()> {
        msg!("Greetings from: Program B");
        Ok(())
    }
}
/*
In Program B
We define pda (program-a) as the signer
like we mentioned in program-a, we can use the pda account as a signer, if you have the correct seeds and bump
This allows us to invoke the initialize function in program-b from program-a via cpi
*/
#[derive(Accounts)]
pub struct Initialize<'info> {
    pub pda_account: Signer<'info>,
}

Step 5: Set Up Tests for program-a

In tests/program-a.ts:

import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { ProgramA } from "../target/types/program_a";

describe("program-a", () => {
  // Configure the client to use the local cluster.
  // This will use the environment we set with solana config from before
  anchor.setProvider(anchor.AnchorProvider.env());

  // get the workspace for program-a and program-b
  const programA = anchor.workspace.programA as Program<ProgramA>;
  const programB = anchor.workspace.programB as Program<ProgramA>;

  // Generate a new wallet that will be used as signer
  let signerWallet = anchor.web3.Keypair.generate();

  it("Is initialized!", async () => {
    // Get the PDA address using the same bump and seeds as defined in the program.
    let [pda_address, bump] = anchor.web3.PublicKey.findProgramAddressSync(
      [Buffer.from("BuildBear"), signerWallet.publicKey.toBuffer()],
      programA.programId
    );

    // we get some SOL to send and cover gas
    await requestAirdrop(anchor.getProvider().connection, pda_address, 500);

    // invoke program-a's initialize function and pass in the context required
    const tx = await programA.methods
      .initialize()
      .accounts({
        pdaAccount: pda_address,
        signer: signerWallet.publicKey,
        systemProgram: anchor.web3.SystemProgram.programId,
        programB: programB.programId,
      })
      .signers([signerWallet]) // set the signer of tx to the new signer we created
      .rpc();

    console.log("Your transaction signature", tx); // use the tx signature with solana-test-validator
  });
});

// Helper function to request airdrop
export async function requestAirdrop(
  connection: any,
  address: any,
  amount: any = 500
) {
  await connection.confirmTransaction(
    await connection.requestAirdrop(
      address,
      amount * anchor.web3.LAMPORTS_PER_SOL
    ),
    "confirmed"
  );
}

Step 6: Build and Run

Build the project:

anchor build

Set Solana environment to localnet:

solana config set -u l
solana config get

Ensure RPC URL is http://localhost:8899.

Run tests:

anchor test


Step 7: View Transaction on Local Explorer

We can view the transaction details in the local Solana explorer. We need to run the Solana test validator first:

Run validator in background:

cd .anchor
solana-test-validator

After running the test, take the transaction signature from the terminal. Now go to Solana Localnet Explorer:

Once you've pasted the transaction signature and searched for it, you'll see the full transaction breakdown including:

Transaction Overview

  • Signature: The unique identifier of the transaction.
  • Status: Should show Success if everything executed properly.
  • Fee: Standard fee deducted for transaction processing.
  • Compute Units Consumed: Shows how much compute was used by both programs combined, etc.


Account Breakdown

Under Account Input(s), you can inspect:

  • The Fee Payer (your signer wallet),
  • The PDA used in the transaction,
  • The System Program for native SOL transfers,
  • Both Program A and Program B involved in the CPI call, etc.

This confirms that both programs participated in the same transaction and that the PDA was correctly used as a signer.


Instruction Logs

At the bottom of the explorer page, the Program Logs section shows the internal logs emitted during transaction execution.


Summary

In this tutorial, we learned:

  • What PDAs and CPIs are in Solana
  • How to use a PDA as a signer
  • How to call another Anchor program via CPI
  • How to test and verify the transaction on localnet

This is one of the foundational techniques for building modular, composable Solana programs.