如何使用 Codama 在 Solana Web3.js 2.0 中创建自定义程序客户端

  • QuickNode
  • 发布于 2025-01-30 18:18
  • 阅读 18

本文介绍了如何使用 Solana Web3.js 2.0 SDK 和 Codama 创建自定义程序客户端,包括环境设置、依赖项和测试。文章展示了如何利用 create-solana-program 和 Anchor 等工具生成客户端,并附带了详细的代码示例和步骤说明,使得复杂的 Solana 程序更易于构建和管理。

概述

Solana 最近宣布了 Solana Web3.js 2.0 的发布候选版本,这是针对其与 Solana 区块链交互的 JavaScript 库的重大更新。新 SDK 的一个令人兴奋的特点是,已经有一个 Codama JavaScript 渲染器,可以只通过一个 IDL 文件生成新 Solana Web3.js 2.0 SDK 的客户端。

Kinboi 是一组库,提供生成 Solana 程序客户端的工具。它是一个强大的工具,可以用于生成 JavaScript、Rust、Umi(由 Metaplex 维护的 JavaScript 库)以及(将来)其他现有 Solana 程序的客户端。Codama 的工作原理是将一个或多个程序的 IDL 传递给它,以生成一个可以由 Visitors 更新的节点树。这些 visitors 可以根据需要更新指令或账户。语言无关的渲染 Visitors 之后可以生成多种语言的客户端,以便你可以管理客户端栈/依赖关系。

让我们试试吧!

你将要做的事情

在本指南中,你将:

  • 使用 create-solana-program 创建一个程序和客户端
  • 使用 Anchor 创建一个程序和客户端

你将需要的东西

  • 安装 Node.js(版本 19 或更高)
  • TypeScript 经验以及安装 ts-node

先决知识

本指南中使用的依赖

依赖 版本
create-solana-program v0.3.12
anchor cli 0.30.1
@solana/web3.js ^2.0.0
@codama/nodes-from-anchor ^0.21.2
@codama/renderers-js ^0.21.8
codama ^0.21.4

Create-Solana-Program

可以说,为 Solana 程序创建客户端的最简单方法是使用 create-solana-program。该库允许你使用 ShankAnchor 创建 Solana 程序。它已经预先配置了 Codama。你只需创建新项目,构建程序,然后生成客户端:

创建一个新项目:

npm create solana-program@latest

你将被提示选择一个模板。选择你喜欢的模板(我们将使用 Anchor,但在本例中并不重要),并确保选择 JavaScript 客户端(你也可以包括 Rust,但 JavaScript 渲染器与新 Solana Web3.js 2.0 SDK 兼容)。

Create Solana Program

接下来,安装依赖:

npm install

该包附带了一个预写的计数器程序。请随意在 programs/ 目录下修改它。当准备好后,构建程序:

npm run programs:build

程序构建完成后,你可以运行 generate 脚本以创建程序的 IDLs 和客户端:

npm run generate

如果你有兴趣,可以查看 scripts/generate-clients.mjs 中的 generate-clients 脚本。该脚本只是将 IDL 加载到一个 Kinboi 树中,并使用 JavaScript 渲染器为程序生成客户端。相当简单,对吧?接下来我们将在 Anchor 项目中重建一个类似的脚本。但首先,让我们查看生成的客户端!

导航到 clients/js/src/index.ts 并浏览生成的客户端。你将看到从 IDL 生成的指令、账户、错误等!

在没有编写任何代码的情况下,你已有一个程序和客户端,可以部署到 Solana 的主网!如果你正在使用 Anchor 并且不想使用 create-solana-program 库怎么办?没问题!我们将在下一部分中向你展示如何做到这一点。

Anchor

如果你使用 Anchor 来编写你的 Solana 程序,你也可以使用 Codama 生成与 Solana Web3.js 2.0 兼容的客户端。让我们通过一个简单的示例,使用 create-solana-program 库中的同一个计数器程序来展示。

创建一个新的 Anchor 项目

首先,导航到 program-client-demo-anchor 目录外并创建一个新项目:

anchor init program-client-demo-anchor

然后,安装依赖:

cd program-client-demo-anchor

用以下代码替换你的 programs/program-client-demo-anchor/src/lib.rs 文件:

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,
}

这正是 create-solana-program 库中的默认程序。请随意修改以满足你的需求(虽然我们将在下一部分中使用它来编写示例测试)。

确保在 declare_id! 宏中更新你的程序 ID,或者运行 anchor keys sync 来更新程序 ID。当准备就绪时,构建程序:

anchor build

设置环境

我们将需要一些包来开始。首先,安装 Codama 和我们将使用的渲染器,以及 Solana Web3.js 2.0 SDK:

yarn add @solana/web3.js@2 codama @codama/renderers-js @codama/nodes-from-anchor @types/node

接下来,更新你的 tsconfig.json 文件以包含以下编译器选项:

{
  "compilerOptions": {
    "types": ["mocha", "chai", "node"],
    "typeRoots": ["./node_modules/@types"],
    "lib": ["es2015"],
    "module": "commonjs",
    "target": "ESNext",
    "esModuleInterop": true,
    "resolveJsonModule": true
  }
}

最后,将以下脚本添加到你 Anchor.toml 文件的 [scripts] 部分:

generate-clients = "yarn ts-node ./scripts/generate-clients.ts"

生成客户端

既然我们的环境已设置好,让我们创建客户端。创建一个名为 scripts 的新目录,并在其中创建一个名为 generate-clients.ts 的新文件。该文件将包含生成客户端的代码。

将以下代码添加到 scripts/generate-clients.ts

import { createFromRoot } from 'codama';
import { rootNodeFromAnchor, AnchorIdl } from '@codama/nodes-from-anchor';
import { renderVisitor as renderJavaScriptVisitor } from "@codama/renderers-js";
import anchorIdl from '../target/idl/program_client_demo_anchor.json';
import path from 'path';

const codama = createFromRoot(rootNodeFromAnchor(anchorIdl as AnchorIdl));

const jsClient = path.join(__dirname, "..", "clients", "js");
codama.accept(
  renderJavaScriptVisitor(path.join(jsClient, "src", "generated"))
);

这看起来与 create-solana-program 库中的 generate-clients 脚本非常相似。主要区别在于我们使用 rootNodeFromAnchor 函数从 Anchor IDL(格式稍有不同)创建了一个 Codama 树。然后,我们使用 JavaScript 渲染器为该树生成客户端。

由于我们设置了 Anchor.toml,你应该能够运行以下命令来运行该脚本并生成程序客户端:

anchor run generate-clients

你应该注意到生成的客户端目录中有一个新目录 clients/js/src/generated

Generated Clients

你现在可以使用这些客户端与你的程序交互!

测试客户端

由于我们现在拥有与新 Solana JS SDK 兼容的 JS 客户端,我们实际上可以在不使用 Anchor 库的情况下编写我们的测试。

导航到你的测试文件(tests/program-client-demo-anchor.ts),并清空现有文件的内容。

首先,我们将需要从 Solana Web3.js 2.0 和我们的新程序客户端导入必要的依赖:

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";

接下来,我们将创建几个帮助函数,以帮助我们快速设置测试环境和处理交易。我们在这里不会详细讲解这些工作原理。如果你需要关于新 Solana Web3.js 2.0 SDK 中交易的复习,请查阅 转账指南可替代资产指南

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;
};

请注意,我们将 programClient 定义为 TestEnvironment 类型的一个元素。我们将在下面的测试中使用它。

继续添加两个测试:一个用于初始化新的计数器 PDA,另一个用于递增它:

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);
  });
});

如你所见,programClient 包含用于创建指令和获取与我们程序关联的账户的方法!相当方便。浏览 Intellisense 或查看生成的客户端,了解其他可用内容,并扩展你的测试!

最后,运行以下命令测试你的代码:

anchor test

你将看到类似以下的输出:

  it creates a new counter account
    ✔ Is created! (470ms)
    ✔ Is incremented! (487ms)
  2 passing (1s)

干得好!

自定义 Codama

虽然 Codama 在生成客户端方面非常强大和方便,但你也可以想象,你可能会遇到某些特定需求的限制。Codama 使用 Kinobi 类。 Kinobi 类 包含一个 update 函数,允许你在生成客户端之前对 Visitor 进行修改。这是 Metaplex 实现的示例,用于元数据程序客户端生成器。

总结

在本指南中,你创建了使用新的 Solana Web3.js 2.0 SDK 的自定义程序客户端。这样应使使用新库的构建更加高效和易于管理。如果你正在构建一个复杂的程序、为客户构建多个程序,或者只想节省时间,Codama 可以成为你工具箱中的一个宝贵工具。

这种方法具有高度的可定制性,能够无缝集成各种 QuickNode 插件和 API。我们希望你看到此过程可以带来很大的灵活性,因此请发挥你的创造力,构建一些了不起的东西!我们期待看到你的成就——在 QuickNode DiscordTwitter 上给我们发个消息,告诉我们你创建了什么!

资源

我们 ❤️ 意见反馈!

让我们知道 如果你有任何反馈或新主题的请求。我们非常乐意听取你的意见。

  • 原文链接: quicknode.com/guides/sol...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
QuickNode
QuickNode
江湖只有他的大名,没有他的介绍。