Cross Program Invocation

In this section, we'll update the CRUD program from the previous PDA section by adding Cross Program Invocations (CPIs), a feature that enables Solana programs to invoke each other.

We'll also demonstrate how programs can "sign" for Program Derived Addresses (PDAs) when making Cross Program Invocations.

We'll modify the update and delete instructions to handle SOL transfers between accounts by invoking the System Program.

The purpose of this section is to walk through the process of implementing CPIs in a Solana program using the Anchor framework, building upon the PDA concepts we explored in the previous section. For more details, refer to the Cross Program Invocation page.

For reference, here is the final code after completing both the PDA and CPI sections.

Here is the starter code for this section, with just the PDA section completed.

Modify Update Instruction

First, we'll implement a simple "pay-to-update" mechanism by modifying the Update struct and update function.

Begin by updating the lib.rs file to bring into scope items from the system_program module.

lib.rs
use anchor_lang::system_program::{transfer, Transfer};

Next, update the Update struct to include an additional account called vault_account. This account, controlled by our program, will receive SOL from a user when they update their message account.

lib.rs
#[account(
    mut,
    seeds = [b"vault", user.key().as_ref()],
    bump,
)]
pub vault_account: SystemAccount<'info>,

Next, implement the CPI logic in the update instruction to transfer 0.001 SOL from the user's account to the vault account.

lib.rs
let transfer_accounts = Transfer {
    from: ctx.accounts.user.to_account_info(),
    to: ctx.accounts.vault_account.to_account_info(),
};
let cpi_context = CpiContext::new(
    ctx.accounts.system_program.to_account_info(),
    transfer_accounts,
);
transfer(cpi_context, 1_000_000)?;

Rebuild the program.

Terminal
build

Modify Delete Instruction

We'll now implement a "refund on delete" mechanism by modifying the Delete struct and delete function.

First, update the Delete struct to include the vault_account. This allows us to transfer any SOL in the vault back to the user when they close their message account.

lib.rs
#[account(
    mut,
    seeds = [b"vault", user.key().as_ref()],
    bump,
)]
pub vault_account: SystemAccount<'info>,

Also add the system_program as the CPI for the transfer requires invoking the System Program.

lib.rs
pub system_program: Program<'info, System>,

Next, implement the CPI logic in the delete instruction to transfer SOL from the vault account back to the user's account.

lib.rs
let user_key = ctx.accounts.user.key();
let signer_seeds: &[&[&[u8]]] =
    &[&[b"vault", user_key.as_ref(), &[ctx.bumps.vault_account]]];
 
let transfer_accounts = Transfer {
    from: ctx.accounts.vault_account.to_account_info(),
    to: ctx.accounts.user.to_account_info(),
};
let cpi_context = CpiContext::new(
    ctx.accounts.system_program.to_account_info(),
    transfer_accounts,
).with_signer(signer_seeds);
transfer(cpi_context, ctx.accounts.vault_account.lamports())?;

Note that we updated _ctx: Context<Delete> to ctx: Context<Delete> as we'll be using the context in the body of the function.

Rebuild the program.

Terminal
build

Redeploy Program

After making these changes, we need to redeploy our updated program. This ensures that our modified program is available for testing. On Solana, updating a program simply requires deploying the program at the same program ID.

Info

Ensure your Playground wallet has devnet SOL. Get devnet SOL from the Solana Faucet.

Terminal
deploy

Update Test File

Next, we'll update our anchor.test.ts file to include the new vault account in our instructions. This requires deriving the vault PDA and including it in our update and delete instruction calls.

Derive Vault PDA

First, add the vault PDA derivation:

anchor.test.ts
const [vaultPda, vaultBump] = PublicKey.findProgramAddressSync(
  [Buffer.from("vault"), wallet.publicKey.toBuffer()],
  program.programId,
);

Modify Update Test

Then, update the update instruction to include the vaultAccount.

anchor.test.ts
const transactionSignature = await program.methods
  .update(message)
  .accounts({
    messageAccount: messagePda,
    vaultAccount: vaultPda,
  })
  .rpc({ commitment: "confirmed" });

Modify Delete Test

Then, update the delete instruction to include the vaultAccount.

anchor.test.ts
const transactionSignature = await program.methods
  .delete()
  .accounts({
    messageAccount: messagePda,
    vaultAccount: vaultPda,
  })
  .rpc({ commitment: "confirmed" });

Rerun Test

After making these changes, run the tests to ensure everything is working as expected:

Terminal
test

You can then inspect the SolanaFM links to view the transaction details, where you'll find the CPIs for the transfer instructions within the update and delete instructions.

Update CPIUpdate CPI

Delete CPIDelete CPI

If you encounter any errors, you can reference the final code.

Next Steps

Congratulations on completing the Solana Quickstart guide! You've gained hands-on experience with key Solana concepts including:

  • Fetching and reading data from accounts
  • Building and sending transactions
  • Deploying and updating Solana programs
  • Working with Program Derived Addresses (PDAs)
  • Making Cross-Program Invocations (CPIs)

To deepen your understanding of these concepts, check out the Core Concepts documentation which provides detailed explanations of the topics covered in this guide.

Explore More Examples

If you prefer learning by example, check out the Program Examples Repository for a variety of example programs.

Solana Playground offers a convenient feature allowing you to import or view projects using their GitHub links. For example, open this Solana Playground link to view the Anchor project from this Github repo.

Click the Import button and enter a project name to add it to your list of projects in Solana Playground. Once a project is imported, all changes are automatically saved and persisted.

Last updated on

目次

ページを編集