本文详细介绍了 Solana 如何使用 IDL(接口定义语言)来描述如何与 Solana 程序交互,并通过 Anchor 框架自动生成 IDL 文件。文章还展示了如何通过 Rust 编写 Solana 程序,并通过 TypeScript 单元测试进行验证。
IDL(接口定义语言)是一个描述如何与Solana程序交互的JSON文件。它是由Anchor框架自动生成的。
名为“initialize”的函数没有什么特别之处——这是Anchor选择的一个名称。在本教程中,我们将学习Typescript单元测试如何能够“找到”适当的函数。
让我们创建一个新的项目,名为anchor-function-tutorial
,并将initialize函数中的名称更改为boaty_mc_boatface
,保持其他一切不变。
pub fn boaty_mc_boatface(ctx: Context<Initialize>) -> Result<()> {
Ok(())
}
现在让我们将测试更改为如下:
it("调用boaty mcboatface", async () => {
// 在这里添加你的测试。
const tx = await program.methods.boatyMcBoatface().rpc();
console.log("你的交易签名", tx);
});
现在运行测试 anchor test --skip-local-validator
它按预期运行。那么这个魔法是如何工作的呢?
当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”的列表是该程序支持的公开函数,粗略上等同于以太坊合约中的外部和公共函数。Solana中的IDL文件在与合约的交互方式上,与Solidity中的ABI文件具有相似的角色。
我们之前看到函数不接受任何参数,这就是为什么
args
列表为空。稍后我们将解释“accounts”是什么。
一个显著的区别是:Rust中的函数是蛇形命名的,但Anchor在JavaScript中将它们格式化为驼峰命名。这是为了尊重这些语言的命名约定: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);
Ok(())
}
pub fn sub(ctx: Context<Initialize>, a: u64, b: u64) -> Result<()> {
let difference = a - b;
msg!("差是 {}", difference);
Ok(())
}
并将我们的单元测试更改为如下:
it("应该加法", async () => {
const tx = await program.methods.add(new anchor.BN(1), new anchor.BN(2)).rpc();
console.log("你的交易签名", tx);
});
it("应该减法", async () => {
const tx = await program.methods.sub(new anchor.BN(10), new anchor.BN(3)).rpc();
console.log("你的交易签名", tx);
});
练习: 为mul
、div
和modulo
实现类似的函数,并编写单元测试以触发每一个。
Initialize
结构体呢?现在还有一个狡猾的事情发生。我们已经保持Initialize
结构体不变,并在函数间重新使用它。同样,名称并不重要。让我们将结构体名称更改为Empty
并重新运行测试。
//...
// 在此处更改结构体名称
pub fn add(ctx: Context<Empty>, a: u64, b: u64) -> Result<()> {
let sum = a + b;
msg!("和是 {}", sum);
Ok(())
}
//...
// 在这里也更改结构体名称
#[derive(Accounts)]
pub struct Empty {}
再次强调,名称Empty
在这里完全是任意的。
练习: 将结构体名称Empty
更改为BoatyMcBoatface
并重新运行测试。
#[derive(Accounts)]
结构体?这个#
语法是Anchor框架定义的Rust属性。我们将在后面的教程中对此进行进一步说明。现在,我们想关注IDL中的accounts键以及它与程序中定义的结构体之间的关系。
下面我们截图展示以上程序的IDL。所以我们可以看到Rust属性#[derive(Accounts)]
中的“Accounts”和IDL中的“accounts”键之间的关系:
在我们的例子中,上面JSON IDL中的accounts
键(用紫色箭头标记)是空的。但对于大多数有用的Solana事务,这并不是这样,稍后我们将学习。
由于BoatyMcBoatface
的账户结构体为空,因此IDL中的账户列表也为空。
现在让我们看看当结构体非空时发生什么。复制下面的代码并替换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”的内容,但现在你可以将其视为与以太坊中的tx.origin
的类似物。
为了总结我们到目前为止学到的一切,让我们构建另一个具有不同函数和账户结构体的程序。
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
和another_signer
。请注意,这些在IDL的function_a
的accounts键中重复作为元素。你可以看到Anchor将Rust的蛇形命名转换为IDL中的驼峰命名。
Anchor 0.30更新 Anchor不再自动执行此转换(发布说明)。
函数function_b
接受一个u64参数。它的上下文结构体是空的,因此function_b
在IDL中的accounts
键是一个空数组。
通常,我们期望IDL的accounts
键中的项数组与函数在其ctx
参数中采用的账户结构体的键匹配。
在本章中:
#[derive(Accounts)]
修改的结构体以及它如何与函数参数相关联。原文发表于2024年2月10日
- 原文链接: rareskills.io/post/ancho...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!