In this part of Module 3, you'll delve into the practical aspects of coding a flip program that leverages Cross-Program Invocation (CPI) techniques in Solana using Solidity. You will start with creating an interface in Solidity and then move on to writing tests for the program using `TypeScript`, followed by building, deploying, and testing the flip program.
illustration
b
Creating Tests for CPI Flip Program

In this lesson, you will:

  • Set up a testing environment for Solang.
  • Write test cases for the Solidity code.

This lesson guides writing tests for a Solana program that incorporates cross-program invocation (CPI). It begins with setting up the testing environment and generating key pairs for data accounts. The tests involve initializing a data account for the lever program, executing a virtual lever pull-through CPI, and verifying the updated state value. These tests aim to ensure the program's functionality.

To start, clear the existing code in the test file and replace it with the following code block:

import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { Lever } from "../target/types/lever";
import { Hand } from "../target/types/hand";

describe("cross-program-invocation", () => {
  // Configuration for the local cluster.
  const provider = anchor.AnchorProvider.env();
  anchor.setProvider(provider);

  // Generate keypairs for data accounts of each program.
  const dataAccountLever = anchor.web3.Keypair.generate();
  const dataAccountHand = anchor.web3.Keypair.generate();
  const wallet = provider.wallet;

  // Lever and Hand programs
   const leverProgram = anchor.workspace.Lever as Program<Lever>;
   const handProgram = anchor.workspace.Hand as Program<Hand>;

  it("Initialize the lever!", async () => {
    // Initialize data account for lever program
    const tx = await leverProgram.methods
      .new()
      .accounts({ dataAccount: dataAccountLever.publicKey })
      .signers([dataAccountLever])
      .rpc();
    console.log("Your transaction signature", tx);

    // Fetch and display the state of the data account
    const val = await leverProgram.methods
      .get()
      .accounts({ dataAccount: dataAccountLever.publicKey })
      .view();
    console.log("State:", val);
  });

  it("Pull the lever!", async () => {
    // Initialize data account for hand program
    const tx = await handProgram.methods
      .new()
      .accounts({ dataAccount: dataAccountHand.publicKey })
      .signers([dataAccountHand])
      .rpc();
    console.log("Your transaction signature", tx);

    // Invoke pullLever in hand program, calling lever program via CPI
    const tx2 = await handProgram.methods
      .pullLever(dataAccountLever.publicKey, "Chris")
      .accounts({ dataAccount: dataAccountHand.publicKey })
      .remainingAccounts([
        // Lever program's data account
        {
          pubkey: dataAccountLever.publicKey,
          isWritable: true,
          isSigner: false,
        },
        // Lever program's ID
        {
          pubkey: leverProgram.programId,
          isWritable: false,
          isSigner: false,
        },
      ])
      .rpc({ skipPreflight: true });
    console.log("Your transaction signature", tx2);

    // Fetch and display updated state
    const val = await leverProgram.methods
      .get()
      .accounts({ dataAccount: dataAccountLever.publicKey })
      .view();
    console.log("State:", val);
  });

  it("Pull it again!", async () => {
    // Repeat lever pull with different arguments
    const tx = await handProgram.methods
      .pullLever(dataAccountLever.publicKey, "Ashley")
      .accounts({ dataAccount: dataAccountHand.publicKey })
      .remainingAccounts(
        // Lever program's data account
        {
          pubkey: dataAccountLever.publicKey,
          isWritable: true,
          isSigner: false,
        },
        // Lever program's ID
        {
          pubkey: leverProgram.programId,
          isWritable: false,
          isSigner: false,
        }
      )
      .rpc({ skipPreflight: true });

    console.log("Your transaction signature", tx);

    // Fetch and display updated state
    const val = await leverProgram.methods
      .get()
      .accounts({ dataAccount: dataAccountLever.publicKey })
      .view();
    console.log("State:", val);
  });
});

In this code:

  • We import necessary dependencies and types for our programs, enabling the testing of the switch state toggle via CPI.
  • The describe block outlines our test suite for the CPI program.
  • We configure the testing environment, generate keypairs for data accounts, and initialize program instances.
describe("cross-program-invocation", () => {
})

In this part, we'll talk about and test how the "CPI" program works. We'll write different test cases, make assertions, and set expectations to make sure the program behaves correctly. For this, you need to first set the Requirements for the test:

// Configure the client to use the local cluster.
 const provider = anchor.AnchorProvider.env();
 anchor.setProvider(provider);

  // Generate new keypairs for data accounts
 const dataAccountLever = anchor.web3.Keypair.generate();
 const dataAccountHand = anchor.web3.Keypair.generate();
 const wallet = provider.wallet;

  // Initialize Lever and Hand programs
 const leverProgram = anchor.workspace.Lever as Program<Lever>;
 const handProgram = anchor.workspace.Hand as Program<Hand>;

Here, we establish the testing setup by connecting to a local cluster, generating keypairs for data accounts, and initializing our Lever and Hand programs. Now, let's initialize the data account for the lever program:

it("Initialize the lever!", async () => {
    // Initialize data account for the lever program
    const tx = await leverProgram.methods
      .new()
      .accounts({ dataAccount: dataAccountLever.publicKey })
      .signers([dataAccountLever])
      .rpc();
   console.log("Your transaction signature", tx);

    // Fetch the state of the data account
    const val = await leverProgram.methods
      .get()
      .accounts({ dataAccount: dataAccountLever.publicKey })
      .view();

 console.log("State:", val);
  });

In this test case, we initialize a data account for the lever program and fetch its state.

it("Pull the lever!", async () => {
  // Initialize data account for the hand program
  // This is required by Solang, but the account is not used
  const tx = await handProgram.methods
    .new()
    .accounts({ dataAccount: dataAccountHand.publicKey })
    .signers([dataAccountHand])
    .rpc();
  console.log("Your transaction signature", tx);

  // Call the pullLever instruction on the hand program, which invokes the lever program via CPI
  const tx2 = await handProgram.methods
    .pullLever(dataAccountLever.publicKey, "Chris")
    .accounts({ dataAccount: dataAccountHand.publicKey })
    .remainingAccounts([
      {
        pubkey: dataAccountLever.publicKey, // The lever program's data account, which stores the state
        isWritable: true,
        isSigner: false,
      },
      {
        pubkey: leverProgram.programId, // The lever program's program ID
        isWritable: false,
        isSigner: false,
      },
    ])
    .rpc({ skipPreflight: true });
  console.log("Your transaction signature", tx2);

  // Fetch the state of the data account
  const val = await leverProgram.methods
    .get()
    .accounts({ dataAccount: dataAccountLever.publicKey })
    .view();

  console.log("State:", val);
});

This test case focuses on the functionality of pulling the lever, where the hand program calls the lever program via CPI. We check the updated state post-CPI execution.

 it("Pull it again!", async () => {
  // Repeat lever pull with different arguments
  const tx = await handProgram.methods
    .pullLever(dataAccountLever.publicKey, "Ashley")
    .accounts({ dataAccount: dataAccountHand.publicKey })
    .remainingAccounts([
      // Account configurations
    ])
    .rpc({ skipPreflight: true });

  console.log("Your transaction signature", tx);

  // Fetch the state of the data account
  const val = await leverProgram.methods
    .get()
    .accounts({ dataAccount: dataAccountLever.publicKey })
    .view();
  console.log("State:", val);
});

Here, we perform another lever pull with varying arguments to ensure the program responds correctly to different inputs.

In the next lesson, we will explore the process of building and deploying our Solana program to the blockchain, further advancing our understanding of Solana development.