IDL(Interface Definition Language,接口定义语言)是一个 JSON 文件,用于描述如何与 Solana 程序进行交互。它是由 Anchor 框架自动生成的。
名为 initialize 的函数并没有什么特别之处——它只是 Anchor 选择的一个名称。本教程中我们将学习的是 Typescript 单元测试如何“找到”合适的函数。
让我们创建一个名为 anchor-function-tutorial 的新项目,并将 initialize 函数的名称更改为 boaty_mc_boatface,其余部分保持不变。
pub fn boaty_mc_boatface(ctx: Context<Initialize>) -> Result<()> {
Ok(())
}
现在让我们将测试代码更改如下:
it("Call boaty mcboatface", async () => {
// Add your test here.
const tx = await program.methods.boatyMcBoatface().rpc();
console.log("Your transaction signature", tx);
});
现在使用 anchor test --skip-local-validator 运行测试。
它如预期般运行了。那么这种神奇的魔法是如何生效的呢?
测试是如何知道 initialize 函数的?
当 Anchor 构建 Solana 程序时,它会创建一个 IDL(接口定义语言)。
这个文件存储在 target/idl/anchor_function_tutorial.json 中。该文件之所以命名为 anchor_function_tutorial.json,是因为 anchor_function_tutorial 是程序的名称。请注意,Anchor 将破折号转换为了下划线!
让我们打开它。
{
"version": "0.1.0",
"name": "anchor_function_tutorial",
"instructions": [
{
"name": "boatyMcBoatface",
"accounts": [],
"args": []
}
]
}
instructions 列表是该程序支持的对外公开函数,大致相当于 Ethereum 合约中的 external 和 public 函数。Solana 中的 IDL 文件扮演着与 Solidity 中的 ABI 文件类似的角色,它指定了如何与程序/合约进行交互。
前面我们看到我们的函数没有接收任何参数,这就是为什么
args列表为空的原因。我们稍后会解释什么是accounts。
有一点非常引人注目:Rust 中的函数使用的是 snake_case(蛇形命名法),但在 JavaScript 环境中,Anchor 将它们格式化为了 camelCase(驼峰命名法)。这是为了遵循各语言的约定:Rust 倾向于使用蛇形命名法,而 JavaScript 通常使用驼峰命名法。
这个 JSON 文件就是 methods 对象能够知道支持哪些函数的原因。
当我们运行测试时,我们期望它能够通过,这意味着测试正在正确地调用 Solana 程序:

练习: 为 boaty_mc_boatface 函数添加一个参数以接收一个 u64 值。再次运行 anchor build。然后再次打开 target/idl/anchor_function_tutorial.json 文件。它发生了什么变化?
现在,让我们开始创建一个 Solana 程序,该程序包含用于基础加法和减法运算的函数,并打印结果。Solana 函数不能像 Solidity 那样直接返回值,因此我们必须将它们打印出来。(Solana 有其他传递值的方式,我们将在以后讨论)。让我们像下面这样创建两个函数:
pub fn add(ctx: Context<Initialize>, a: u64, b: u64) -> Result<()> {
let sum = a + b;
msg!("Sum is {}", sum);
Ok(())
}
pub fn sub(ctx: Context<Initialize>, a: u64, b: u64) -> Result<()> {
let difference = a - b;
msg!("Difference is {}", difference);
Ok(())
}
并将我们的单元测试更改如下:
it("Should add", async () => {
const tx = await program.methods.add(new anchor.BN(1), new anchor.BN(2)).rpc();
console.log("Your transaction signature", tx);
});
it("Should sub", async () => {
const tx = await program.methods.sub( new anchor.BN(10), new anchor.BN(3)).rpc();
console.log("Your transaction signature", tx);
});
练习: 为 mul、div 和 modulo 实现类似的函数,并编写一个单元测试来分别触发它们。
那么 Initialize 结构体呢?
现在这里还发生了另一件暗藏玄机的事情。我们保留了 Initialize 结构体没有对其做任何修改,并在各个函数之间复用它。再说一次,名称并不重要。让我们将结构体名称更改为 Empty,然后重新运行测试。
//...
// Change struct name here
pub fn add(ctx: Context<Empty>, a: u64, b: u64) -> Result<()> {
let sum = a + b;
msg!("Sum is {}", sum);
Ok(())
}
//...
// Change struct name here too
#[derive(Accounts)]
pub struct Empty {}
同样,这里的名称 Empty 是完全任意的。
练习: 将结构体名称 Empty 更改为 BoatyMcBoatface,然后重新运行测试。
#[derive(Accounts)] 结构体是什么?
这个 # 语法是由 Anchor 框架定义的一个 Rust attribute(Rust 属性)。我们将在以后的教程中进一步解释这一点。现在,我们需要关注的是 IDL 中的 accounts 键,以及它与程序中定义的结构体之间的关系。
Accounts IDL 键
下面是我们上述程序 IDL 的截图。因此,我们可以看到 Rust 属性 #[derive(Accounts)] 中的 “Accounts” 与 IDL 中的 accounts 键之间的关系:

在我们的例子中,上方 JSON IDL 中用 紫色箭头 标记的 accounts 键是空的。但正如我们稍后将学到的,对于大多数有实际用途的 Solana 交易来说,情况并非如此。
因为我们为 BoatyMcBoatface 定义的账户结构体是空的,所以 IDL 中的 accounts 列表同样也是空的。
现在让我们看看当结构体非空时会发生什么。复制下方代码并替换 lib.rs 的内容。
use anchor_lang::prelude::*;
declare_id!("8PSAL9t1RMb7BcewhsSFrRQDq61Y7YXC5kHUxMk5b39Z");
#[program]
pub mod anchor_function_tutorial {
use super::*;
pub fn non_empty_account_example(ctx: Context<NonEmptyAccountExample>) -> Result<()> {
Ok(())
}
}
#[derive(Accounts)]
pub struct NonEmptyAccountExample<'info> {
signer: Signer<'info>,
another_signer: Signer<'info>,
}
现在运行 anchor build - 让我们看看新的 IDL 返回了什么。
{
"version": "0.1.0",
"name": "anchor_function_tutorial",
"instructions": [
{
"name": "nonEmptyAccountExample",
"accounts": [
{
"name": "signer",
"isMut": false,
"isSigner": true
},
{
"name": "anotherSigner",
"isMut": false,
"isSigner": true
}
],
"args": []
}
],
"metadata": {
"address": "8PSAL9t1RMb7BcewhsSFrRQDq61Y7YXC5kHUxMk5b39Z"
}
}
注意,accounts 不再为空,并且被填充了结构体中的字段:signer 和 anotherSigner(请注意,another_signer 已从蛇形命名法转换为驼峰命名法)。IDL 已更新以匹配我们刚刚修改的结构体,特别是与我们添加的账户数量保持一致。
我们将在接下来的教程中深入探讨 Signer,但目前你可以将其视作类似于 Ethereum 中的 tx.origin。
关于程序和 IDL 的第二个示例。
为了总结到目前为止我们学到的所有内容,让我们构建另一个带有不同函数和 Account 结构体的程序。
use anchor_lang::prelude::*;
declare_id!("8PSAL9t1RMb7BcewhsSFrRQDq61Y7YXC5kHUxMk5b39Z");
#[program]
pub mod anchor_function_tutorial {
use super::*;
pub fn function_a(ctx: Context<NonEmptyAccountExample>) -> Result<()> {
Ok(())
}
pub fn function_b(ctx: Context<Empty>, firstArg: u64) -> Result<()> {
Ok(())
}
}
#[derive(Accounts)]
pub struct NonEmptyAccountExample<'info> {
signer: Signer<'info>,
another_signer: Signer<'info>,
}
#[derive(Accounts)]
pub struct Empty {}
现在使用 anchor build 进行构建。
让我们再看一下 IDL 文件 target/idl/anchor_function_tutorial.json,并将这些文件并排放在一起:

你能看出上方 IDL 文件和程序之间的关系吗?
函数 function_a 没有参数,这在 IDL 中显示为 args 键下的一个空数组。
其 Context 接收 NonEmptyAccountExample 结构体。这个 NonEmptyAccountExample 结构体有两个签名者(signer)字段:signer 和 another_signer。请注意,这些在 IDL 中作为 function_a 的 account 键内的元素被逐一罗列出来。你可以看到,Anchor 在 IDL 中把 Rust 的蛇形命名法转换为了驼峰命名法。
Anchor 0.30 更新 Anchor 不再自动执行此转换(发布说明)。
函数 function_b 接收一个 u64 参数。它的上下文结构体是空的,所以 IDL 中 function_b 的 accounts 键是一个空数组。
总的来说,我们期望 IDL 中 accounts 键内的项数组,能够与该函数在其 ctx 参数中接收的账户结构体的键相匹配。
总结
在本章中:
- 我们学习了 Solana 使用 IDL(接口定义语言)来显示如何与 Solana 程序进行交互,以及 IDL 中会出现哪些字段。
- 我们介绍了由
#[derive(Accounts)]修饰的结构体,以及它与函数参数之间的关系。 - Anchor 在 Typescript 测试中会将 Rust 的 snake_case 函数解释为 camelCase 函数。
原载于 2024 年 2 月 10 日