Build CPI Program
Integrate Range verification into your own Solana program
This guide walks you through building a Solana program that integrates Range verification. You’ll learn two patterns: calling Range from your program, and receiving callbacks from Range.
Overview
Section titled “Overview”There are two integration patterns:
| Pattern | Use Case | Flow |
|---|---|---|
| Your Program → Range | Add verification to existing functionality | Your instruction calls Range via CPI |
| Range → Your Program | Atomic verification + action | Range verifies, then calls your callback |
Prerequisites
Section titled “Prerequisites”- Anchor framework installed
- Range program deployed (or use existing deployment)
- Basic understanding of Solana CPI
Pattern 1: Your Program Calls Range
Section titled “Pattern 1: Your Program Calls Range”Use this when you want to verify before performing an action.
-
Add Range as Dependency
In your
Cargo.toml:[dependencies]anchor-lang = "0.30.0"range = { path = "../codama-rust-range" }# Or if published:# range = { version = "0.1.0", features = ["cpi"] } -
Create Your Instruction
use anchor_lang::prelude::*;use range::program::Range;use range::cpi::accounts::VerifyRange;use range::cpi::verify_range;#[program]pub mod my_program {use super::*;pub fn verified_action(ctx: Context<VerifiedAction>,signature: [u8; 64],message: Vec<u8>,) -> Result<()> {// 1. Call Range for verificationlet cpi_accounts = VerifyRange {signer: ctx.accounts.signer.to_account_info(),settings: ctx.accounts.range_settings.to_account_info(),instructions_sysvar: ctx.accounts.instructions_sysvar.to_account_info(),};let cpi_ctx = CpiContext::new(ctx.accounts.range_program.to_account_info(),cpi_accounts);verify_range(cpi_ctx, signature, message)?;// 2. Verification passed - do your actionmsg!("Verification successful, performing action...");// Your business logic herectx.accounts.my_state.verified = true;ctx.accounts.my_state.last_verified = Clock::get()?.unix_timestamp;Ok(())}}#[derive(Accounts)]pub struct VerifiedAction<'info> {#[account(mut)]pub signer: Signer<'info>,/// Range programpub range_program: Program<'info, Range>,/// Range Settings account/// CHECK: Validated by Range programpub range_settings: AccountInfo<'info>,/// CHECK: Instructions sysvar#[account(address = anchor_lang::solana_program::sysvar::instructions::ID)]pub instructions_sysvar: AccountInfo<'info>,/// Your program's state#[account(mut)]pub my_state: Account<'info, MyState>,}#[account]pub struct MyState {pub verified: bool,pub last_verified: i64,} -
Client-Side Call
const [settingsPda] = PublicKey.findProgramAddressSync([Buffer.from('settings'), settingsAdmin.toBuffer()],RANGE_PROGRAM_ID);await myProgram.methods.verifiedAction(Array.from(signature), Buffer.from(message)).accounts({signer: user.publicKey,rangeProgram: RANGE_PROGRAM_ID,rangeSettings: settingsPda,instructionsSysvar: SYSVAR_INSTRUCTIONS_PUBKEY,myState: myStatePda,}).signers([user]).rpc();
Pattern 2: Range Calls Your Program (Callback)
Section titled “Pattern 2: Range Calls Your Program (Callback)”Use this for atomic operations where verification and action must both succeed.
-
Implement on_verify Callback
use anchor_lang::prelude::*;use anchor_lang::solana_program::sysvar::instructions::{get_instruction_relative,load_current_index_checked,};declare_id!("YourProgramId...");// Range program ID - update with actual IDconst RANGE_PROGRAM_ID: Pubkey = pubkey!("RangeProgramId...");#[program]pub mod my_callback_program {use super::*;pub fn on_verify(ctx: Context<OnVerify>, cpi_data: Vec<u8>) -> Result<()> {// 1. CRITICAL: Verify caller is Rangelet ix_sysvar = &ctx.accounts.instructions_sysvar;let current_index = load_current_index_checked(ix_sysvar).map_err(|_| ErrorCode::InvalidSysvar)?;require!(current_index > 0, ErrorCode::CpiOnly);let calling_ix = get_instruction_relative(-1, ix_sysvar).map_err(|_| ErrorCode::InvalidSysvar)?;require!(calling_ix.program_id == RANGE_PROGRAM_ID,ErrorCode::CpiOnly);// 2. Parse cpi_datalet params: CallbackParams = CallbackParams::try_from_slice(&cpi_data).map_err(|_| ErrorCode::InvalidCpiData)?;// 3. Execute callback logicmsg!("Callback received: action={}", params.action);match params.action {0 => perform_action_a(&ctx)?,1 => perform_action_b(&ctx)?,_ => return Err(ErrorCode::UnknownAction.into()),}Ok(())}}#[derive(Accounts)]pub struct OnVerify<'info> {pub signer: Signer<'info>,/// CHECK: Instructions sysvar for CPI verification#[account(address = anchor_lang::solana_program::sysvar::instructions::ID)]pub instructions_sysvar: AccountInfo<'info>,// Add your accounts here#[account(mut)]pub user_state: Account<'info, UserState>,}#[derive(BorshDeserialize)]pub struct CallbackParams {pub action: u8,pub amount: u64,}#[account]pub struct UserState {pub verified_count: u64,}fn perform_action_a(ctx: &Context<OnVerify>) -> Result<()> {ctx.accounts.user_state.verified_count += 1;Ok(())}fn perform_action_b(ctx: &Context<OnVerify>) -> Result<()> {// Different logicOk(())}#[error_code]pub enum ErrorCode {#[msg("This instruction can only be called via CPI from Range")]CpiOnly,#[msg("Invalid sysvar")]InvalidSysvar,#[msg("Invalid cpi_data")]InvalidCpiData,#[msg("Unknown action")]UnknownAction,} -
Client-Side Call with Callback
import * as borsh from 'borsh';// Encode callback paramsclass CallbackParams {action: number;amount: bigint;constructor(fields: { action: number; amount: bigint }) {this.action = fields.action;this.amount = fields.amount;}}const schema = new Map([[CallbackParams, { kind: 'struct', fields: [['action', 'u8'],['amount', 'u64'],]}],]);const params = new CallbackParams({ action: 0, amount: BigInt(100) });const cpiData = borsh.serialize(schema, params);// Call Range with callbackawait rangeProgram.methods.verifyRangeWithCallback(Array.from(signature),Buffer.from(message),Buffer.from(cpiData)).accounts({signer: user.publicKey,settings: settingsPda,targetProgram: MY_CALLBACK_PROGRAM_ID,instructionsSysvar: SYSVAR_INSTRUCTIONS_PUBKEY,}).remainingAccounts([{ pubkey: user.publicKey, isSigner: true, isWritable: false },{ pubkey: SYSVAR_INSTRUCTIONS_PUBKEY, isSigner: false, isWritable: false },{ pubkey: userStatePda, isSigner: false, isWritable: true },]).signers([user]).rpc();
Complete Example: Verified Token Gating
Section titled “Complete Example: Verified Token Gating”A full example combining both patterns for token-gated functionality:
use anchor_lang::prelude::*;use anchor_spl::token::{self, Token, TokenAccount, Transfer};
declare_id!("YourProgramId...");
const RANGE_PROGRAM_ID: Pubkey = pubkey!("RangeProgramId...");
#[program]pub mod verified_vault { use super::*;
/// Initialize a verified vault pub fn initialize_vault(ctx: Context<InitializeVault>) -> Result<()> { ctx.accounts.vault.admin = ctx.accounts.admin.key(); ctx.accounts.vault.total_deposited = 0; Ok(()) }
/// Callback from Range - deposit tokens after verification pub fn on_verify(ctx: Context<OnVerifyDeposit>, cpi_data: Vec<u8>) -> Result<()> { // Verify CPI caller verify_range_cpi(&ctx.accounts.instructions_sysvar)?;
// Parse deposit amount from cpi_data let amount = u64::from_le_bytes( cpi_data.try_into().map_err(|_| ErrorCode::InvalidCpiData)? );
// Transfer tokens to vault let transfer_accounts = Transfer { from: ctx.accounts.user_token.to_account_info(), to: ctx.accounts.vault_token.to_account_info(), authority: ctx.accounts.signer.to_account_info(), };
token::transfer( CpiContext::new( ctx.accounts.token_program.to_account_info(), transfer_accounts ), amount )?;
// Update vault state ctx.accounts.vault.total_deposited += amount;
msg!("Verified deposit of {} tokens", amount); Ok(()) }}
#[derive(Accounts)]pub struct InitializeVault<'info> { #[account(mut)] pub admin: Signer<'info>,
#[account( init, payer = admin, space = 8 + Vault::SIZE, seeds = [b"vault", admin.key().as_ref()], bump )] pub vault: Account<'info, Vault>,
pub system_program: Program<'info, System>,}
#[derive(Accounts)]pub struct OnVerifyDeposit<'info> { pub signer: Signer<'info>,
/// CHECK: Instructions sysvar #[account(address = anchor_lang::solana_program::sysvar::instructions::ID)] pub instructions_sysvar: AccountInfo<'info>,
#[account(mut, constraint = user_token.owner == signer.key())] pub user_token: Account<'info, TokenAccount>,
#[account(mut)] pub vault_token: Account<'info, TokenAccount>,
#[account(mut)] pub vault: Account<'info, Vault>,
pub token_program: Program<'info, Token>,}
#[account]pub struct Vault { pub admin: Pubkey, pub total_deposited: u64,}
impl Vault { const SIZE: usize = 32 + 8;}
fn verify_range_cpi(ix_sysvar: &AccountInfo) -> Result<()> { use anchor_lang::solana_program::sysvar::instructions::*;
let idx = load_current_index_checked(ix_sysvar) .map_err(|_| ErrorCode::InvalidSysvar)?;
require!(idx > 0, ErrorCode::CpiOnly);
let ix = get_instruction_relative(-1, ix_sysvar) .map_err(|_| ErrorCode::InvalidSysvar)?;
require!(ix.program_id == RANGE_PROGRAM_ID, ErrorCode::CpiOnly);
Ok(())}
#[error_code]pub enum ErrorCode { #[msg("CPI only")] CpiOnly, #[msg("Invalid sysvar")] InvalidSysvar, #[msg("Invalid cpi_data")] InvalidCpiData,}Testing Your Integration
Section titled “Testing Your Integration”describe('verified vault', () => { it('deposits after verification', async () => { // Setup: get signature from backend const amount = 1000; const cpiData = Buffer.alloc(8); cpiData.writeBigUInt64LE(BigInt(amount));
await rangeProgram.methods .verifyRangeWithCallback( Array.from(signature), Buffer.from(message), cpiData ) .accounts({ signer: user.publicKey, settings: settingsPda, targetProgram: verifiedVault.programId, instructionsSysvar: SYSVAR_INSTRUCTIONS_PUBKEY, }) .remainingAccounts([ { pubkey: user.publicKey, isSigner: true, isWritable: false }, { pubkey: SYSVAR_INSTRUCTIONS_PUBKEY, isSigner: false, isWritable: false }, { pubkey: userTokenAccount, isSigner: false, isWritable: true }, { pubkey: vaultTokenAccount, isSigner: false, isWritable: true }, { pubkey: vaultPda, isSigner: false, isWritable: true }, { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false }, ]) .signers([user]) .rpc();
// Verify deposit const vault = await verifiedVault.account.vault.fetch(vaultPda); expect(vault.totalDeposited.toNumber()).to.equal(amount); });});See Also
Section titled “See Also”- CPI Callbacks - Callback pattern details
- verify_via_range - CPI instruction reference
- on_verify - Callback instruction reference
- Testing Locally - Test your integration