Skip to main content

How to Create Anchor Program Clients using Kinobi

Created on
Updated on
Nov 26, 2024

11 min read

Overview

Kinobi is a set of libraries that provides tools for generating clients for Solana programs. It is a powerful tool that can be used to generate JavaScript, Umi (JavaScript), and Rust clients for existing Solana programs. Kinobi recently added support for generating clients from Anchor IDLs, so we can now use Kinobi to create clients for Anchor programs. This can save time when building and testing new programs with Anchor. This guide will show you how to use Kinobi to generate clients for your Anchor programs.

What You Will Do

  • Create a simple Anchor Program
  • Write a script that will generate a client for the program using Kinobi
  • Test the client

What You Need

This guide assumes you have a basic understanding of Solana Programming and Anchor:

Before you begin, make sure you have the following installed:

Check your Solana and Anchor Versions

This guide works with Solana CLI version 1.18.16 or later Anchor version 0.30.0 or later.

  • Check your Solana version by running solana --version in your terminal. Following the instructions here if you need to update.
  • Check your Anchor version by running anchor --version in your terminal. Follow the instructions here if you need to update.

Let's get started!

What is Kinobi?

Kinobi is a library created by the Metaplex Foundation designed to generate clients for Solana programs. Kinobi works by passing one or more programs' IDLs to generate a Kinobi, a tree of nodes that can be updated by Visitors. These visitors can update instructions or accounts as needed. Language-agnostic rendering Visitors can then generate clients in various languages so that you can manage client stack/dependencies.

How Kinobi Works

  1. Program Definition: You define your Solana programs and corresponding IDLs.
  2. Abstraction: Kinobi creates a language-agnostic tree of nodes (or client representation) that visitors can update.
  3. Client Generation: Kinobi's Visitors process the tree and generate language-specific clients.

source: Metaplex Developers

Key Components

  • Programs: Solana programs with associated IDLs.
  • IDLs: Describe the interface and functionality of Solana programs.
  • Kinobi Tree: Organizes IDLs and facilitates client generation.
  • Visitors: Modules that customize the client generation process for specific languages.
  • Dependencies: Includes necessary libraries and utilities like HTTP interfaces, RPC interfaces, and cryptographic tools.

Recently, Kinobi added support for generating clients from Anchor IDLs. This means you can now use Kinobi to generate clients for Anchor programs. Let's see how to do that.


Active Development

Kinobi support for Anchor is still very new and under active development. Code may change, and new features may be added. Please let us know if you encounter any issues on Discord.

Create an Anchor Program

First, let's create a simple Anchor program. We will create a program that has two instructions:

  1. initialize: Initializes a data account with a u64 value.
  2. set_data: Sets the value of the data account.

Initialize the Project

Create a new project directory and run the following commands to create a new Anchor program:

anchor init kinobi-test

Change into the project directory:

cd kinobi-test

Install Dependencies

After the project is initialized, you can run npm install to ensure the dependencies are installed. We will then install a few additional dependencies. In your terminal, run the following commands:

npm install @kinobi-so/nodes-from-anchor @kinobi-so/renderers @kinobi-so/visitors-core @metaplex-foundation/umi @metaplex-foundation/umi-bundle-defaults

This will install a few Kinobi packages, including the nodes-from-anchor package, which will help us generate the Kinobi tree from the Anchor IDL.

Update TSConfig

Add resolveJsonModule to your tsconfg.json to ensure we can load the IDL JSON object to generate the client and "DOM" to your lib array so we can run our script in Node.js. Update the tsconfig.json file in your project directory to look like this:

{
"compilerOptions": {
"types": ["mocha", "chai"],
"typeRoots": ["./node_modules/@types"],
"lib": ["ES2020", "DOM"],
"module": "commonjs",
"target": "es6",
"esModuleInterop": true,
"resolveJsonModule": true,
}
}

Write the Anchor Program

Let's write our Anchor program. Open your programs/kinobi-test/src/lib.rs file and replace the contents with the following code, being careful not to overwrite your declare_id! macro:

use anchor_lang::prelude::*;

declare_id!("YOUR_PROGRAM_ID_HERE"); // Replace with your program ID

#[program]
pub mod kinobi_test {
use super::*;

pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
ctx.accounts.pda.set_inner(ExampleStruct {
data: 0,
authority: *ctx.accounts.payer.key,
});
Ok(())
}

pub fn set_data(ctx: Context<SetData>, data: u32) -> Result<()> {
ctx.accounts.pda.data = data;
Ok(())
}
}

#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
payer: Signer<'info>,

#[account(
init,
payer = payer,
space = 45,
seeds = [b"example".as_ref(), payer.key().as_ref()],
bump
)]
pda: Account<'info, ExampleStruct>,

system_program: Program<'info, System>,
}


#[derive(Accounts)]
pub struct SetData<'info> {
#[account(mut)]
authority: Signer<'info>,

#[account(
mut,
has_one = authority,
seeds = [b"example".as_ref(), authority.key().as_ref()],
bump
)]
pda: Account<'info, ExampleStruct>,
}

#[account]
pub struct ExampleStruct {
pub data: u32,
pub authority: Pubkey,
}

This is a basic Anchor program that will allow a user to initialize an ExampleStruct (a PDA that holds u32 data and an authority PublicKey) and set the data value. The PDAs are seeded with the payer's key and the string "example". Feel free to use a different program, or modify this one as needed--it is just for demonstration purposes.

Build and Test the Program

Now that we have our program, we can build and test it. Run the following commands in your terminal:

anchor build

This might take a few minutes but should run without any errors. While it is running, let's write a simple test script. Open your anchor-generated test file, tests/kinobi-test.ts, and replace the contents with the following code:

import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { KinobiTest } from "../target/types/kinobi_test";

describe("kinobi-test", () => {
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.KinobiTest as Program<KinobiTest>;
const [pda] = anchor.web3.PublicKey.findProgramAddressSync(
[
Buffer.from("example"),
program.provider.publicKey.toBuffer()
],
program.programId
)
it("Is initialized!", async () => {
const tx = await program.methods
.initialize()
.accountsStrict({
payer: program.provider.publicKey,
pda,
systemProgram: anchor.web3.SystemProgram.programId
})
.rpc();
});
it("Can set data!", async () => {
const tx = await program.methods
.setData(10)
.accountsStrict({
authority: program.provider.publicKey,
pda
})
.rpc({skipPreflight: true});
});
});

This script will test the two instructions in our program. The first test will initialize the data account, and the second test will set the data value to 10. Go ahead and run the test script:

anchor test

You should see something like this:

  kinobi-test
✔ Is initialized! (450ms)
✔ Can set data! (463ms)


2 passing (916ms)

✨ Done in 2.80s.

Great job.

Generate a Client with Kinobi

Since your test has run successfully, Anchor should have auto-generated an IDL for you in target/idl/kinobi_test.json. Locate this file--we will use it in the next section (Note: if you used a different name for your Anchor project, this file path may vary slightly.). We can now use Kinobi to generate a client for this program.

From your root directory create a new folder, clients, and create two new files:

  1. generate-client.ts for the client generation script
  2. example.ts for trying the generated client

Generate the Client

Open generate-client.ts, and add the following code to the file:

import { AnchorIdl, rootNodeFromAnchorWithoutDefaultVisitor } from "@kinobi-so/nodes-from-anchor";
import { renderJavaScriptUmiVisitor, renderJavaScriptVisitor, renderRustVisitor } from "@kinobi-so/renderers";
import { visit } from "@kinobi-so/visitors-core";
import anchorIdl from "../target/idl/kinobi_test.json"; // Note: if you initiated your project with a different name, you may need to change this path

async function generateClients() {
const node = rootNodeFromAnchorWithoutDefaultVisitor(anchorIdl as AnchorIdl);

const clients = [
{ type: "JS", dir: "clients/generated/js/src", renderVisitor: renderJavaScriptVisitor },
{ type: "Umi", dir: "clients/generated/umi/src", renderVisitor: renderJavaScriptUmiVisitor },
{ type: "Rust", dir: "clients/generated/rust/src", renderVisitor: renderRustVisitor }
];

for (const client of clients) {
try {
await visit(
node,
await client.renderVisitor(client.dir)
); console.log(`✅ Successfully generated ${client.type} client for directory: ${client.dir}!`);
} catch (e) {
console.error(`Error in ${client.renderVisitor.name}:`, e);
throw e;
}
}
}

generateClients();

Let's break down what this script does:

  1. Imports the necessary Kinobi packages.
  2. Imports the IDL file generated by Anchor and creates a Kinobi tree from it (using the rootNodeFromAnchorWithoutDefaultVisitor function).
  3. Defines the clients to generate (JavaScript, Umi, and Rust) - feel free to comment out any you don't need and adjust the directories as needed.
  4. Iterates over the clients and generates the clients using the appropriate render visitor using the visit function.

Run the Script

Now that we have our script, we can run it to generate the clients. Run the following command in your terminal:

ts-node clients/generate-client.ts

You should see output similar to this:

ts-node clients/generate-client.ts
✅ Successfully generated JS client for directory: clients/generated/js/src!
✅ Successfully generated Umi client for directory: clients/generated/umi/src!
✅ Successfully generated Rust client for directory: clients/generated/rust/src!

You should now have clients generated for your program in the clients directory. Great job!

You can now use these clients to interact with your program.

Test the Client

Open the example.ts file you created earlier and add the following code:

import { createUmi } from '@metaplex-foundation/umi-bundle-defaults';
import { TransactionBuilderSendAndConfirmOptions, generateSigner, keypairIdentity, sol } from '@metaplex-foundation/umi';
import { publicKey as publicKeySerializer, string } from '@metaplex-foundation/umi/serializers';
import { getKinobiTestProgramId } from './generated/umi/src/programs/kinobiTest';
import { initialize, setData } from './generated/umi/src/instructions';

const umi = createUmi('http://127.0.0.1:8899', { commitment: 'processed' });
const creator = generateSigner(umi);
umi.use(keypairIdentity(creator));

const options: TransactionBuilderSendAndConfirmOptions = {
confirm: { commitment: 'processed' }
};

const pda = umi.eddsa.findPda(getKinobiTestProgramId(umi), [
string({ size: 'variable' }).serialize('example'),
publicKeySerializer().serialize(creator.publicKey),
]);

async function logPda() {
console.log(`PDA: ${pda.toString()}`);
}

async function airdropFunds() {
try {
await umi.rpc.airdrop(creator.publicKey, sol(100), options.confirm);
console.log(`1. ✅ - Airdropped 100 SOL to the ${creator.publicKey.toString()}`);
} catch (error) {
console.error('1. ❌ - Error airdropping SOL to the wallet.', error);
}
}

async function initializeAccount() {
try {
await initialize(umi, { pda, payer: creator }).sendAndConfirm(umi, options);
console.log('2. ✅ - Initialized the account.');
} catch (error) {
console.error('2. ❌ - Error initializing the account.', error);
}
}

async function setDataAccount(num: number, value: number) {
try {
await setData(umi, { authority: creator, pda, data: value }).sendAndConfirm(umi, options);
console.log(`${num}. ✅ - Set data to ${value}.`);
} catch (error) {
console.error(num, '. ❌ - Error setting data.', error);
}
}

async function main() {
await logPda();
await airdropFunds();
await initializeAccount();
await setDataAccount(3, 10);
await setDataAccount(4, 20);
await setDataAccount(5, 30);
await setDataAccount(6, 40);
}

main().then(() => {
console.log('🚀 - Done!');
}).catch((error) => {
console.error('❌ - Error:', error);
});

This script will:

  • Import necessary Umi packages and helpers
  • Import the generated client functions
  • Create a Umi instance
  • Fetch the PDA for our signer based on our program's seeds
  • Define functions to log the PDA, airdrop funds, initialize the account, and set the data
    • Note that Kinobi generated our initialize and setData functions. They take the Umi instance and the necessary arguments and return a function that can be used to send and confirm the transaction. Easy, right?
  • Finally, run the functions in order in our main function. We have included a few setData calls to demonstrate the functionality.

Run the Script

Now that we have our script, we can run it to interact with our program. Run the following command in your terminal:

ts-node clients/example.ts

I bet you got an error, didn't you? That's because our local validator is not running. Let's rerun our test with the --detach flag to keep it running in the background:

anchor test --detach

Now, in a separate terminal, run the example.ts script again:

ts-node clients/example.ts

Better luck this time? You should see output similar to this:

PDA: 5GawRMyhgw8uDxanKeZd89AMeteuHmuAhyb2NSN7YEgJ,255
1. ✅ - Airdropped 100 SOL to the 5L8siRBhjiAE4GJKZmZSSkfCYBSRZXMv44SVuoD888Yt
2. ✅ - Initialized the account.
3. ✅ - Set data to 10.
4. ✅ - Set data to 20.
5. ✅ - Set data to 30.
6. ✅ - Set data to 40.
🚀 - Done!

Congratulations! You have successfully generated a client for your Anchor program using Kinobi and interacted with it using Umi. You can now use this client to interact with your program in your applications.

Wrapping Up

Great job getting to this point. Here's a quick recap of what you have accomplished:

  1. Program Creation: You created a simple Anchor program with essential instructions.
  2. Testing: You wrote and executed tests to ensure your program works correctly.
  3. Client Generation: You used Kinobi to generate JavaScript, Umi, and Rust clients from your Anchor IDL.
  4. Client Interaction: You wrote a script to interact with your program using the generated clients and confirmed its functionality.

By leveraging Kinobi, you have streamlined the client generation process, making your development workflow more efficient. If you are building a complex program, building many programs for your customers, or just want to save time, Kinobi can be a valuable tool in your toolkit.

As Kinobi evolves, stay updated with its latest features and improvements. Don't hesitate to contribute to the project by reporting issues or submitting PRs.

If you are stuck or have questions, post them in our Discord. You can also stay up to date by following us on Twitter (@QuickNode) or our Telegram announcement channel.

We ❤️ Feedback!

Let us know if you have any feedback or requests for new topics. We'd love to hear from you.

Share this guide