10 min read
Overview
Solana recently announced the release candidate for Solana Web3.js 2.0, a major update to their JavaScript library for interacting with the Solana blockchain. One exciting feature of the new SDK is that there is already a Kinobi JavaScript renderer that can be used to generate clients for the new Solana Web3.js 2.0 SDK with just an IDL file.
Kinboi 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, Rust, Umi (a JavaScript library maintained by Metaplex), and (in the future) other clients for existing 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.
Let's try it out!
What You Will Do
In this guide, you will:
- Create a program and client using
create-solana-program
- Create a program and client using Anchor
What You Will Need
Prerequisite Knowledge
- Knowledge of Solana basics (Guide: Introduction to Solana)
- Solana Programming Experience with Anchor
- Understanding of Solana IDLs
Dependencies Used in this Guide
Dependency | Version |
---|---|
create-solana-program | v0.3.12 |
anchor cli | 0.30.1 |
@solana/web3.js | ^2.0.0 |
@kinobi-so/nodes-from-anchor | ^0.21.2 |
@kinobi-so/renderers-js | ^0.21.8 |
kinobi | ^0.21.4 |
Create-Solana-Program
Arguably, the easiest way to create a client for a Solana program is to use the create-solana-program
library. This library allows you to create a Solana program using Shank or Anchor. It comes pre-configured with Kinobi. All you have to do is create a new project, build your program, and then generate the clients:
Create a new project:
npm create solana-program@latest
You will be prompted to select a template. Select your preferred template (we will be using Anchor, but it does not matter for this example), and make sure to select JavaScript Client (you can include Rust as well, but the JavaScript renderer is compatible with the new Solana Web3.js 2.0 SDK).
Next, install the dependencies:
npm install
The package comes with a pre-written counter program. Feel free to modify it in the programs/
directory. When ready, build the program:
npm run programs:build
After the program builds, you can run the generate
script to create IDLs and clients for the program:
npm run generate
If you are so inclined, go ahead and take a look at the generate-clients
script in scripts/generate-clients.mjs
. The script simply loads the IDL into a Kinboi tree and uses the JavaScript renderer to generate clients for the program. Pretty simple, right? We will recreate a similar script in an Anchor project next. But first, let's check out the generated clients!
Navigate to clients/js/src/index.ts
and browse the generated clients. You will see instructions, accounts, errors, etc. all generated from your IDL!
And without writing any code, you have a program and client ready to get deployed to Solana's mainnet! What if you are building in Anchor and you prefer not to use the create-solana-program
library? No problem! We will show you how to do that in the next section.
Anchor
If you are using Anchor to write your Solana programs, you can also use Kinobi to generate Solana Web3.js 2.0-compatible clients. Let's walk through a simple example using the same counter program from the create-solana-program
library.
Create a New Anchor Project
First, navigate out of the program-client-demo-anchor
directory and create a new project:
anchor init program-client-demo-anchor
Then, install the dependencies:
cd program-client-demo-anchor
Replace your programs/program-client-demo-anchor/src/lib.rs
file with the following code:
use anchor_lang::prelude::*;
declare_id!("YOUR_PROGRAM_ID_HERE");
#[program]
pub mod program_client_demo_anchor {
use super::*;
pub fn create(ctx: Context<Create>, authority: Pubkey) -> Result<()> {
let counter = &mut ctx.accounts.counter;
counter.authority = authority;
counter.count = 0;
Ok(())
}
pub fn increment(ctx: Context<Increment>) -> Result<()> {
let counter = &mut ctx.accounts.counter;
counter.count += 1;
Ok(())
}
}
#[derive(Accounts)]
pub struct Create<'info> {
#[account(init, payer = payer, space = 8 + 40)]
pub counter: Account<'info, Counter>,
#[account(mut)]
pub payer: Signer<'info>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(mut, has_one = authority @ ProgramClientDemoError::InvalidAuthority)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}
#[account]
pub struct Counter {
pub authority: Pubkey,
pub count: u64,
}
#[error_code]
pub enum ProgramClientDemoError {
#[msg("The provided authority doesn't match the counter account's authority")]
InvalidAuthority,
}
This is just the same program as the default in the create-solana-program
library. Feel free to modify it to suit your needs (though we will use this to write example tests in the next section).
Make sure to update your program ID in the declare_id!
macro or run anchor keys sync
to update the program ID. When ready, build the program:
anchor build
Set up Environment
We're going to need a few packages to get started. First, install Kinobi and the renderers we will use as well as the Solana Web3.js 2.0 SDK:
yarn add @solana/web3.js@2 kinobi @kinobi-so/renderers-js @kinobi-so/nodes-from-anchor @types/node
Next, update your tsconfig.json
file to include the following compiler options:
{
"compilerOptions": {
"types": ["mocha", "chai", "node"],
"typeRoots": ["./node_modules/@types"],
"lib": ["es2015"],
"module": "commonjs",
"target": "ESNext",
"esModuleInterop": true,
"resolveJsonModule": true
}
}
Finally, add the following script to your Anchor.toml
file in the [scripts]
section:
generate-clients = "yarn ts-node ./scripts/generate-clients.ts"
Generate Clients
Now that our environment is set up, let's create our clients. Create a new directory called scripts
and a new file in it called generate-clients.ts
. This file will contain the code to generate our client.
Add the following code to scripts/generate-clients.ts
:
import { createFromRoot } from 'kinobi';
import { rootNodeFromAnchor, AnchorIdl } from '@kinobi-so/nodes-from-anchor';
import { renderVisitor as renderJavaScriptVisitor } from "@kinobi-so/renderers-js";
import anchorIdl from '../target/idl/program_client_demo_anchor.json';
import path from 'path';
const kinobi = createFromRoot(rootNodeFromAnchor(anchorIdl as AnchorIdl));
const jsClient = path.join(__dirname, "..", "clients", "js");
kinobi.accept(
renderJavaScriptVisitor(path.join(jsClient, "src", "generated"))
);
This should look quite similar to the generate-clients
script in the create-solana-program
library. The main difference is that we are using the rootNodeFromAnchor
function to create a Kinobi tree from the Anchor IDL (which is a slightly different format). Then, we use the JavaScript renderer to generate clients for the tree.
Because we set up our Anchor.toml
, you should be able to run the following command to run the script and generate the program client:
anchor run generate-clients
You should notice a new clients/js/src/generated
directory with the generated clients:
You can now use these clients to interact with your program!
Test the Client
Since we now have a JS client compatible with the new Solana JS SDK, we can actually write our tests without using the Anchor library.
Navigate to your tests file (tests/program-client-demo-anchor.ts
), and clear out the contents of the existing file.
First, we will need to import the necessary dependencies from Solana Web3.js 2.0 and our new program client:
import {
appendTransactionMessageInstruction,
Commitment,
CompilableTransactionMessage,
TransactionMessageWithBlockhashLifetime,
Rpc,
RpcSubscriptions,
SolanaRpcApi,
SolanaRpcSubscriptionsApi,
TransactionSigner,
airdropFactory,
createSolanaRpc,
createSolanaRpcSubscriptions,
createTransactionMessage,
generateKeyPairSigner,
getSignatureFromTransaction,
lamports,
pipe,
sendAndConfirmTransactionFactory,
setTransactionMessageFeePayerSigner,
setTransactionMessageLifetimeUsingBlockhash,
signTransactionMessageWithSigners,
KeyPairSigner,
} from '@solana/web3.js';
import { assert } from 'chai';
import * as programClient from "../clients/js/src/generated";
Next, we will create a few helper functions to help us set up our test environment and handle transactions swiftly. We won't go into much detail on how these work here. If you need a refresher on transactions in the new Solana Web3.js 2.0 SDK, check out the Transfer Guide or Fungibles Guide.
type TestEnvironment = {
rpcClient: RpcClient;
authority: TransactionSigner;
counter: KeyPairSigner;
programClient: typeof programClient;
};
const createTestEnvironment = async (): Promise<TestEnvironment> => {
const rpcClient = createDefaultSolanaClient();
const authority = await generateKeyPairSignerWithSol(rpcClient);
const counter = await generateKeyPairSigner();
return { rpcClient, authority, counter, programClient };
};
type RpcClient = {
rpc: Rpc<SolanaRpcApi>;
rpcSubscriptions: RpcSubscriptions<SolanaRpcSubscriptionsApi>;
};
const createDefaultSolanaClient = (): RpcClient => {
const rpc = createSolanaRpc('http://127.0.0.1:8899');
const rpcSubscriptions = createSolanaRpcSubscriptions('ws://127.0.0.1:8900');
return { rpc, rpcSubscriptions };
};
const generateKeyPairSignerWithSol = async (
rpcClient: RpcClient,
putativeLamports: bigint = 1_000_000_000n
) => {
const signer = await generateKeyPairSigner();
await airdropFactory(rpcClient)({
recipientAddress: signer.address,
lamports: lamports(putativeLamports),
commitment: 'confirmed',
});
return signer;
};
const createDefaultTransaction = async (
testEnv: TestEnvironment
) => {
const { rpcClient, authority: feePayer } = testEnv;
const { value: latestBlockhash } = await rpcClient.rpc
.getLatestBlockhash()
.send();
return pipe(
createTransactionMessage({ version: 0 }),
(tx) => setTransactionMessageFeePayerSigner(feePayer, tx),
(tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx)
);
};
const signAndSendTransaction = async (
rpcClient: RpcClient,
transactionMessage: CompilableTransactionMessage &
TransactionMessageWithBlockhashLifetime,
commitment: Commitment = 'confirmed'
) => {
const signedTransaction =
await signTransactionMessageWithSigners(transactionMessage);
const signature = getSignatureFromTransaction(signedTransaction);
await sendAndConfirmTransactionFactory(rpcClient)(signedTransaction, {
commitment,
});
return signature;
};
Just take note that we are defining programClient
as an element of our TestEnvironment
type. We will use that in our tests below.
Go ahead and add two tests: one to initialize a new counter PDA and another to increment it:
describe('it creates a new counter account', () => {
let testEnv: TestEnvironment;
before(async () => {
testEnv = await createTestEnvironment();
})
it("Is created!", async () => {
const createIx = testEnv.programClient.getCreateInstruction({
authority: testEnv.authority.address,
counter: testEnv.counter,
payer: testEnv.authority,
});
await pipe(
await createDefaultTransaction(testEnv),
(tx) => appendTransactionMessageInstruction(createIx, tx),
(tx) => signAndSendTransaction(testEnv.rpcClient, tx)
);
let counterAccount = await testEnv.programClient.fetchCounter(testEnv.rpcClient.rpc, testEnv.counter.address);
assert.strictEqual(counterAccount.data.authority, testEnv.authority.address);
assert.strictEqual(counterAccount.data.count, 0n);
});
it("Is incremented!", async () => {
const incrementIx = testEnv.programClient.getIncrementInstruction({
authority: testEnv.authority,
counter: testEnv.counter.address
});
await pipe(
await createDefaultTransaction(testEnv),
(tx) => appendTransactionMessageInstruction(incrementIx, tx),
(tx) => signAndSendTransaction(testEnv.rpcClient, tx)
);
let counterAccount = await testEnv.programClient.fetchCounter(testEnv.rpcClient.rpc, testEnv.counter.address);
assert.strictEqual(counterAccount.data.authority, testEnv.authority.address);
assert.strictEqual(counterAccount.data.count, 1n);
});
});
As you can see the programClient
includes methods for creating instructions and fetching accounts associated with our program! Pretty handy. Navigate the Intellisense or browse the generated clients to see what else is available and expand your tests!
Finally, run your tests with the following command:
anchor test
You should see something like this:
it creates a new counter account
✔ Is created! (470ms)
✔ Is incremented! (487ms)
2 passing (1s)
Great job!
Customizing Kinobi
Though extremely powerful and handy for generating clients, you can imagine that you may run into some limitations for your specific needs. The Kinobi Class includes an update
function that allows you to make modifications to the Visitor before the client is generated. Here is an example of how this is implemented by Metaplex in the Metadata program client generator.
Wrap Up
In this guide, you created custom program clients that utilize the new Solana Web3.js 2.0 SDK. This should make building with the new library more efficient and easier to manage. 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.
This approach allows for high levels of customization to integrate various QuickNode Add-ons and APIs seamlessly. We hope you have seen that this process allows for a lot of flexibility, so bring your creativity and build something amazing! We are excited to see what you are up to—drop us a line in the QuickNode Discord or on Twitter, and let us know what you've built!
Resources
- Solana Web3.js 2.0 Blog
- Solana Web3.js 2.0 GitHub
- Solana Web3.js Pricples for a Rewrite
- 🎥 Introduction the New Solana Web3.js 2.0 SDK
- create-solana-program Library
- Kinobi Documentation
- Anchor Documentation
We ❤️ Feedback!
Let us know if you have any feedback or requests for new topics. We'd love to hear from you.