Solana Anchor 程序 IDL

本文详细介绍了 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

它按预期运行。那么这个魔法是如何工作的呢?

测试如何知道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”的列表是该程序支持的公开函数,粗略上等同于以太坊合约中的外部和公共函数。Solana中的IDL文件在与合约的交互方式上,与Solidity中的ABI文件具有相似的角色。

我们之前看到函数不接受任何参数,这就是为什么args列表为空。稍后我们将解释“accounts”是什么。

一个显著的区别是:Rust中的函数是蛇形命名的,但Anchor在JavaScript中将它们格式化为驼峰命名。这是为了尊重这些语言的命名约定:Rust倾向于使用蛇形命名,而JavaScript通常使用驼峰命名。

这个JSON文件是“methods”对象知道支持哪些函数的方式。

当我们运行测试时,我们期望它通过,这意味着该测试正确地调用了Solana程序:

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

练习:muldivmodulo实现类似的函数,并编写单元测试以触发每一个。

那么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键以及它与程序中定义的结构体之间的关系。

Accounts IDL键

下面我们截图展示以上程序的IDL。所以我们可以看到Rust属性#[derive(Accounts)]中的“Accounts”和IDL中的“accounts”键之间的关系:

IDL的截图

在我们的例子中,上面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的类似物。

另一个程序和IDL示例。

为了总结我们到目前为止学到的一切,让我们构建另一个具有不同函数和账户结构体的程序。

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文件截图结果

你能看到IDL文件与上面的程序之间的关系吗?

函数function_a没有参数,这在IDL中显示为args键下的空数组。

它的Context采用NonEmptyAccountExample结构体。该结构体NonEmptyAccountExample有两个签名字段:signeranother_signer。请注意,这些在IDL的function_a的accounts键中重复作为元素。你可以看到Anchor将Rust的蛇形命名转换为IDL中的驼峰命名。

Anchor 0.30更新 Anchor不再自动执行此转换(发布说明)。

函数function_b接受一个u64参数。它的上下文结构体是空的,因此function_b在IDL中的accounts键是一个空数组。

通常,我们期望IDL的accounts键中的项数组与函数在其ctx参数中采用的账户结构体的键匹配。

总结

在本章中:

  • 我们了解到Solana使用IDL(接口定义语言)来显示如何与Solana程序交互以及IDL中出现的字段。
  • 我们介绍了通过#[derive(Accounts)]修改的结构体以及它如何与函数参数相关联。
  • Anchor将Rust中的snake_case函数转化为Typescript测试中的camelCase函数。

原文发表于2024年2月10日

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

0 条评论

请先 登录 后评论
RareSkills
RareSkills
https://www.rareskills.io/