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.
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.
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>,}
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 BWe define pda (program-a) as the signerlike we mentioned in program-a, we can use the pda account as a signer, if you have the correct seeds and bumpThis 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>,}
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 airdropexport async function requestAirdrop( connection: any, address: any, amount: any = 500) { await connection.confirmTransaction( await connection.requestAirdrop( address, amount * anchor.web3.LAMPORTS_PER_SOL ), "confirmed" );}