Cross Program Invocation (CPI) es la terminología de Solana para un programa que llama a la función pública de otro programa.
Ya hemos realizado un CPI anteriormente cuando enviamos una transacción de transferencia de SOL al System Program. A modo de recordatorio, aquí está el fragmento de código relevante:
pub fn send_sol(ctx: Context<SendSol>, amount: u64) -> Result<()> {
let cpi_context = CpiContext::new(
ctx.accounts.system_program.to_account_info(),
system_program::Transfer {
from: ctx.accounts.signer.to_account_info(),
to: ctx.accounts.recipient.to_account_info(),
}
);
let res = system_program::transfer(cpi_context, amount);
if res.is_ok() {
return Ok(());
} else {
return err!(Errors::TransferFailed);
}
}
El Cpi en CpiContext literalmente significa “Cross program invocation”.
El flujo de trabajo para llamar a las funciones públicas de un programa que no sea el System Program no es muy diferente — y eso es lo que enseñaremos en este tutorial.
Este tutorial solo se enfoca en cómo llamar a otro programa de Solana que haya sido construido con Anchor. Si el otro programa fue desarrollado con Rust puro, entonces la siguiente guía no funcionará.
En nuestro ejemplo práctico, el programa Alice llamará a una función en el programa Bob.
El programa Bob
Comenzamos creando un nuevo proyecto usando la CLI de Anchor:
anchor init bob
Luego, copia y pega el código a continuación en bob/lib.rs. La cuenta tiene dos funciones: una para inicializar una cuenta de almacenamiento que contiene un u64 y una función add_and_store que toma dos variables u64, las suma y las almacena en la cuenta definida por el struct BobData.
use anchor_lang::prelude::*;
use std::mem::size_of;
// REPLACE WITH YOUR <PROGRAM_ID>declare_id!("8GYu5JYsvAYoinbFTvW4AACYB5GxGstz21FmZe3MNFn4");
#[program]
pub mod bob {
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
msg!("Data Account Initialized: {}", ctx.accounts.bob_data_account.key());
Ok(())
}
pub fn add_and_store(ctx: Context<BobAddOp>, a: u64, b: u64) -> Result<()> {
let result = a + b;
// MODIFY/UPDATE THE DATA ACCOUNT
ctx.accounts.bob_data_account.result = result;
Ok(())
}
}
#[account]
pub struct BobData {
pub result: u64,
}
#[derive(Accounts)]
pub struct BobAddOp<'info> {
#[account(mut)]
pub bob_data_account: Account<'info, BobData>,
}
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(init, payer = signer, space = size_of::<BobData>() + 8)]
pub bob_data_account: Account<'info, BobData>,
#[account(mut)]
pub signer: Signer<'info>,
pub system_program: Program<'info, System>,
}
El objetivo de este tutorial es crear otro programa alice que llame a bob.add_and_store.
Aún dentro del proyecto (bob), crea un nuevo programa usando el comando anchor new:
anchor new alice
La línea de comandos debería imprimir Created new program.
Antes de comenzar a escribir el programa para Alice, el fragmento de código a continuación debe agregarse a la sección [dependencies] del archivo Cargo.toml de Alice en programs/alice/Cargo.toml.
[dependencies]
bob = {path = "../bob", features = ["cpi"]}
Aquí, Anchor está haciendo una gran cantidad de trabajo en segundo plano. Alice ahora tiene acceso a la definición de las funciones públicas de Bob y a los structs de Bob. Puedes pensar en esto como algo análogo a importar una interfaz en Solidity para que sepamos cómo interactuar con otro contrato.
A continuación mostramos el programa Alice. En la parte superior, el programa Alice está importando el struct que contiene las cuentas para BobAddOp (que se usa para add_and_store). Presta atención a los comentarios en el código:
use anchor_lang::prelude::*;
// account struct for add_and_store
use bob::cpi::accounts::BobAddOp;
// The program definition for Bob
use bob::program::Bob;
// the account where Bob is storing the sum
use bob::BobData;
declare_id!("6wZDNWprmb9TAZYMAPpT23kHDPABvBLT8jbWQKLHEmBy");
#[program]
pub mod alice {
use super::*;
pub fn ask_bob_to_add(ctx: Context<AliceOp>, a: u64, b: u64) -> Result<()> {
let cpi_ctx = CpiContext::new(
ctx.accounts.bob_program.to_account_info(),
BobAddOp {
bob_data_account: ctx.accounts.bob_data_account.to_account_info(),
}
);
let res = bob::cpi::add_and_store(cpi_ctx, a, b);
// return an error if the CPI failed
if res.is_ok() {
return Ok(());
} else {
return err!(Errors::CPIToBobFailed);
}
}
}
#[error_code]
pub enum Errors {
#[msg("cpi to bob failed")]
CPIToBobFailed,
}
#[derive(Accounts)]
pub struct AliceOp<'info> {
#[account(mut)]
pub bob_data_account: Account<'info, BobData>,
pub bob_program: Program<'info, Bob>,
}
Si comparamos ask_bob_to_add con el fragmento de código en la parte superior de este artículo, donde mostramos cómo transferir SOL, vemos muchas similitudes.

Para realizar un CPI, se requiere lo siguiente:
- Una referencia al programa de destino (como un
AccountInfo) (recuadro rojo) - La lista de cuentas que necesita la función en el programa de destino para ejecutarse (el struct
ctxque contiene todas las cuentas) (recuadro verde) - Los argumentos que se pasarán a la función (recuadro naranja)
Probando el CPI
El siguiente código Typescript se puede usar para probar el CPI:
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { Bob } from "../target/types/bob";
import { Alice } from "../target/types/alice";
import { expect } from "chai";
describe("CPI from Alice to Bob", () => {
const provider = anchor.AnchorProvider.env();
// Configure the client to use the local cluster.
anchor.setProvider(provider);
const bobProgram = anchor.workspace.Bob as Program<Bob>;
const aliceProgram = anchor.workspace.Alice as Program<Alice>;
const dataAccountKeypair = anchor.web3.Keypair.generate();
it("Is initialized!", async () => {
// Add your test here.
const tx = await bobProgram.methods
.initialize()
.accounts({
bobDataAccount: dataAccountKeypair.publicKey,
signer: provider.wallet.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.signers([dataAccountKeypair])
.rpc();
});
it("Can add numbers then double!", async () => {
// Add your test here.
const tx = await aliceProgram.methods
.askBobToAdd(new anchor.BN(4), new anchor.BN(2))
.accounts({
bobDataAccount: dataAccountKeypair.publicKey,
bobProgram: bobProgram.programId,
})
.rpc();
});
it("Can assert value in Bob's data account equals 4 + 2", async () => {
const BobAccountValue = (
await bobProgram.account.bobData
.fetch(dataAccountKeypair.publicKey)
).result.toNumber();
expect(BobAccountValue).to.equal(6);
});
});
Haciendo el CPI en una línea
Debido a que la cuenta ctx pasada a Alice contiene una referencia a todas las cuentas que necesitamos para realizar la transacción, podemos crear una función dentro de un impl para ese struct que lleve a cabo el CPI. Recuerda que todo impl “adjunta” funciones a un struct y estas pueden utilizar los datos del struct. Dado que el struct ctx AliceOp ya contiene todas las cuentas que Bob necesita para la transacción, podemos mover todo el código del CPI:
let cpi_ctx = CpiContext::new(
ctx.accounts.bob_program.to_account_info(),
BobAddOp {
bob_data_account: ctx.accounts.bob_data_account.to_account_info(),
}
);
dentro de un impl de la siguiente manera:
let cpi_ctx = CpiContext::new(
ctx.accounts.bob_program.to_account_info(),
BobAddOp {
bob_data_account: ctx.accounts.bob_data_account.to_account_info(),
}
);
use anchor_lang::prelude::*;
use bob::cpi::accounts::BobAddOp;
use bob::program::Bob;
use bob::BobData;
// REPLACE WITTH YOUR <PROGRAM_ID>declare_id!("B2BNs2GecG8Ux5EchDDFZakRWX2NDfy1RDhPCTJuJtr5");
#[program]
pub mod alice {
use super::*;
pub fn ask_bob_to_add(ctx: Context<AliceOp>, a: u64, b: u64) -> Result<()> {
// Calls the `bob_add_operation` function in bob program
let res = bob::cpi::bob_add_operation(ctx.accounts.add_function_ctx(), a, b);
if res.is_ok() {
return Ok(());
} else {
return err!(Errors::CPIToBobFailed);
}
}
}
impl<'info> AliceOp<'info> {
pub fn add_function_ctx(&self) -> CpiContext<'_, '_, '_, 'info, BobAddOp<'info>> {
// The bob program we are interacting with
let cpi_program = self.bob_program.to_account_info();
// Passing the necessary account(s) to the `BobAddOp` account struct in Bob program
let cpi_account = BobAddOp {
bob_data_account: self.bob_data_account.to_account_info(),
};
// Creates a `CpiContext` object using the new method
CpiContext::new(cpi_program, cpi_account)
}
}
#[error_code]
pub enum Errors {
#[msg("cpi to bob failed")]
CPIToBobFailed,
}
#[derive(Accounts)]
pub struct AliceOp<'info> {
#[account(mut)]
pub bob_data_account: Account<'info, BobData>,
pub bob_program: Program<'info, Bob>,
}
Somos capaces de hacer una llamada CPI a Bob en “una línea”. Esto podría ser útil si otras partes del programa de Alice hicieran un CPI a Bob — mover el código al impl evitaría que tengamos que copiar y pegar el código para crear el CpiContext.
Aprende más con RareSkills
Este tutorial es parte de una serie sobre aprender desarrollo en Solana.
Publicado originalmente el 17 de mayo de 2024