Program
Cross-Program Invocation (CPI)
Cross-Program Invocation (CPI) lets one Arch program call another program within the same transaction. CPI enables composition, modularity, and reuse of on-chain logic.
When to use CPI
- Reuse core program logic (e.g., token transfers)
- Orchestrate multi-program workflows atomically
- Delegate permissions via PDAs instead of exposing private keys
Basic CPI
use arch_program::{
account_info::AccountInfo,
instruction::{AccountMeta, Instruction},
program::invoke,
pubkey::Pubkey,
};
pub fn transfer_tokens_cpi(
token_program_id: &Pubkey,
source: &AccountInfo,
destination: &AccountInfo,
authority: &AccountInfo,
amount: u64,
) -> arch_program::entrypoint::ProgramResult {
let accounts = vec![
AccountMeta::new(*source.key, false),
AccountMeta::new(*destination.key, false),
AccountMeta::new_readonly(*authority.key, true),
];
// Your program's instruction layout here
let instruction = Instruction::new_with_bytes(
*token_program_id,
&amount.to_le_bytes(),
accounts,
);
invoke(&instruction, &[source.clone(), destination.clone(), authority.clone()])
}
CPI with Program-Derived Address (PDA)
Use invoke_signed
when the "authority" is a PDA owned by your program.
use arch_program::{
account_info::AccountInfo,
program::invoke_signed,
pubkey::Pubkey,
system_instruction,
};
pub fn transfer_from_pda(
program_id: &Pubkey,
pda_account: &AccountInfo,
recipient: &AccountInfo,
lamports: u64,
seeds: &[&[u8]],
) -> arch_program::entrypoint::ProgramResult {
let ix = system_instruction::transfer(pda_account.key, recipient.key, lamports);
// PDA must be the signer via seeds
invoke_signed(
&ix,
&[pda_account.clone(), recipient.clone()],
&[seeds],
)
}
Deriving PDAs
use arch_program::pubkey::Pubkey;
pub fn user_pda(user: &Pubkey, program_id: &Pubkey) -> (Pubkey, u8) {
Pubkey::find_program_address(&[b"user", user.as_ref()], program_id)
}
Common Patterns
Initialize + Use Flow
use arch_program::{
account_info::{next_account_info, AccountInfo},
instruction::{AccountMeta, Instruction},
program::{invoke, invoke_signed},
pubkey::Pubkey,
entrypoint::ProgramResult,
};
pub fn process_initialize(
program_id: &Pubkey,
accounts: &[AccountInfo],
) -> ProgramResult {
let accounts_iter = &mut accounts.iter();
let payer = next_account_info(accounts_iter)?;
let pda_account = next_account_info(accounts_iter)?;
let system_program = next_account_info(accounts_iter)?;
// Derive PDA
let (pda, bump) = Pubkey::find_program_address(&[b"vault"], program_id);
if pda != *pda_account.key { return Err(arch_program::program_error::ProgramError::InvalidSeeds); }
// Create PDA account via CPI to system program
let rent = arch_program::sysvar::rent::Rent::get()?;
let lamports = rent.minimum_balance(0);
let create_ix = arch_program::system_instruction::create_account(
payer.key,
&pda,
lamports,
0,
program_id,
);
invoke_signed(
&create_ix,
&[payer.clone(), pda_account.clone(), system_program.clone()],
&[&[b"vault", &[bump]]],
)
}
Chaining Calls
You can call multiple programs in sequence within a single transaction. If any CPI fails, the entire transaction aborts.
Client-Side Considerations
- Build messages so that all required program accounts and PDAs are present
- Ensure PDA bumps and seeds are reproduced client-side for address derivation
- Keep instruction data small; respect transaction size limits
// TypeScript helper to derive the same PDA as on-chain
import { PublicKey } from '@solana/web3.js'; // replace with Arch TS when available
export function deriveVaultPda(programId: PublicKey) {
return PublicKey.findProgramAddressSync([Buffer.from('vault')], programId);
}
Security Best Practices
- Verify account ownership: check that accounts are owned by expected programs
- Validate signer/writable flags for all accounts used in CPI
- Never trust instruction data from clients; validate thoroughly
- Use PDAs to avoid holding private keys on-chain
Troubleshooting
- InvalidSeeds: PDA mismatch between client and program (check seeds/bump)
- MissingRequiredSignature: Caller did not provide required signer
- IncorrectProgramId: Account not owned by expected program
- Size/limit errors: Split flows into multiple transactions if needed