本文介绍了如何使用 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
创建一个程序和客户端依赖 | 版本 |
---|---|
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 |
可以说,为 Solana 程序创建客户端的最简单方法是使用 create-solana-program
库。该库允许你使用 Shank 或 Anchor 创建 Solana 程序。它已经预先配置了 Codama。你只需创建新项目,构建程序,然后生成客户端:
创建一个新项目:
npm create solana-program@latest
你将被提示选择一个模板。选择你喜欢的模板(我们将使用 Anchor,但在本例中并不重要),并确保选择 JavaScript 客户端(你也可以包括 Rust,但 JavaScript 渲染器与新 Solana Web3.js 2.0 SDK 兼容)。
接下来,安装依赖:
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 来编写你的 Solana 程序,你也可以使用 Codama 生成与 Solana Web3.js 2.0 兼容的客户端。让我们通过一个简单的示例,使用 create-solana-program
库中的同一个计数器程序来展示。
首先,导航到 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
:
你现在可以使用这些客户端与你的程序交互!
由于我们现在拥有与新 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 使用 Kinobi 类。 Kinobi 类 包含一个 update
函数,允许你在生成客户端之前对 Visitor 进行修改。这是 Metaplex 实现的示例,用于元数据程序客户端生成器。
在本指南中,你创建了使用新的 Solana Web3.js 2.0 SDK 的自定义程序客户端。这样应使使用新库的构建更加高效和易于管理。如果你正在构建一个复杂的程序、为客户构建多个程序,或者只想节省时间,Codama 可以成为你工具箱中的一个宝贵工具。
这种方法具有高度的可定制性,能够无缝集成各种 QuickNode 插件和 API。我们希望你看到此过程可以带来很大的灵活性,因此请发挥你的创造力,构建一些了不起的东西!我们期待看到你的成就——在 QuickNode Discord 或 Twitter 上给我们发个消息,告诉我们你创建了什么!
让我们知道 如果你有任何反馈或新主题的请求。我们非常乐意听取你的意见。
- 原文链接: quicknode.com/guides/sol...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!