Solana中的Require、Revert和自定义错误

文章详细介绍了在 Solana 的 Anchor 框架中如何处理函数参数的限制,类似于以太坊中的 require 语句。通过代码示例展示了如何使用 require! 宏和错误处理机制来确保函数参数的有效性,并解释了 Solana 和以太坊在错误处理上的差异。

Hero Image showing Error code and Macro

在以太坊中,我们经常会看到一个 require 语句来限制函数参数可以接受的值。考虑以下示例:

function foobar(uint256 x) public {
    require(x < 100, "I'm not happy with the number you picked");
    // 其余的函数逻辑
}

在上面的代码中,如果 foobar 被传递一个 100 或更大的值,交易将会回退。

我们如何在 Solana 中实现这一点,或者更具体地说,在 Anchor 框架中实现?

Anchor 对 Solidity 的自定义错误和 require 语句有等效的实现。它们的 文档 在这一主题上相当不错,但我们也将解释如何在函数参数不符合预期时停止交易。

下面的 Solana 程序有一个 limit_range 函数,只接受 10 到 100 之间(包括)的值:

use anchor_lang::prelude::*;

declare_id!("8o3ehd3XnyDocd9hG1uz5trbmSRB7gaLaE9BCXDpEnMY");

#[program]
pub mod day4 {
    use super::*;

    pub fn limit_range(ctx: Context<LimitRange>, a: u64) -> Result<()> {
        if a < 10 {
            return err!(MyError::AisTooSmall);
        }
        if a > 100 {
            return err!(MyError::AisTooBig);
        }
        msg!("Result = {}", a);
        Ok(())
    }
}

#[derive(Accounts)]
pub struct LimitRange {}

#[error_code]
pub enum MyError {
    #[msg("a is too big")]
    AisTooBig,
    #[msg("a is too small")]
    AisTooSmall,
}

以下代码单元测试了上述程序:

import * as anchor from "@coral-xyz/anchor";
import { Program, AnchorError } from "@coral-xyz/anchor"
import { Day4 } from "../target/types/day4";
import { assert } from "chai";

describe("day4", () => {
  // 配置客户端以使用本地集群。
  anchor.setProvider(anchor.AnchorProvider.env());

  const program = anchor.workspace.Day4 as Program<Day4>;

  it("Input test", async () => {
    // 在此添加测试。
    try {
      const tx = await program.methods.limitRange(new anchor.BN(9)).rpc();
      console.log("Your transaction signature", tx);
    } catch (_err) {
      assert.isTrue(_err instanceof AnchorError);
      const err: AnchorError = _err;
      const errMsg =
        "a is too small";
      assert.strictEqual(err.error.errorMessage, errMsg);
      console.log("Error number:", err.error.errorCode.number);
    }

    try {
      const tx = await program.methods.limitRange(new anchor.BN(101)).rpc();
      console.log("Your transaction signature", tx);
    } catch (_err) {
      assert.isTrue(_err instanceof AnchorError);
      const err: AnchorError = _err;
      const errMsg =
        "a is too big";
      assert.strictEqual(err.error.errorMessage, errMsg);
      console.log("Error number:", err.error.errorCode.number);
    }
  });
});

练习

  1. 你注意到错误编号有什么模式?如果你改变 enum MyError 中错误的顺序,错误代码会发生什么?
  2. 使用这段代码块,将新函数和错误添加到现有代码中:
#[program]
pub mod day_4 {
    use super::*;

    pub fn limit_range(ctxThen : Context<LimitRange>, a: u64) -> Result<()> {
        require!(a >= 10, MyError::AisTooSmall);
        require!(a <= 100, MyError::AisTooBig);
        msg!("Result = {}", a);
        Ok(())
    }

    // 新函数
    pub fn func(ctx: Context<LimitRange>) -> Result<()> {
        msg!("Will this print?");
        return err!(MyError::AlwaysErrors);
    }
}

#[derive(Accounts)]
pub struct LimitRange {}

#[error_code]
pub enum MyError {
    #[msg("a is too small")]
    AisTooSmall,
    #[msg("a is too big")]
    AisTooBig,
    #[msg("Always errors")]  // 新错误,你认为错误代码是什么?
    AlwaysErrors,
}

并添加这个测试:

it("Error test", async () => {
    // 在此添加测试。
    try {
      const tx = await program.methods.func().rpc();
      console.log("Your transaction signature", tx);
    } catch (_err) {
      assert.isTrue(_err instanceof AnchorError);
      const err: AnchorError = _err;
      const errMsg =
        "Always errors";
      assert.strictEqual(err.error.errorMessage, errMsg);
      console.log("Error number:", err.error.errorCode.number);
    }
  });

在你运行这个之前,你认为新的错误代码将是什么?

以太坊和 Solana 在处理无效参数的交易停止方式的显著区别是,Ethereum 触发回退,而 Solana 返回一个错误。

使用 require 语句

有一个 require! 宏,概念上与 Solidity 的 require 相同,我们可以用它来简化我们的代码。将 if 检查(需要三行)切换到 require! 调用,之前的代码转换如下:

pub fn limit_range(ctx: Context<LimitRange>, a: u64) -> Result<()> {
    require!(a >= 10, Day4Error::AisTooSmall);
    require!(a <= 100, Day4Error::AisTooBig);

    msg!("Result = {}", a);
    Ok(())
}

在以太坊中,我们知道如果函数回退,则不会记日志,即使回退是在记录之后发生。例如,下面合约中的 tryToLog 调用不会记录任何内容,因为函数回退:

contract DoesNotLog {
    event SomeEvent(uint256);

    function tryToLog() public {
        emit SomeEvent(100);
        require(false);
    }
}

练习:如果你在 Solana 程序函数中返回错误语句之前放置 msg! 宏,结果会怎么样?如果你用 Ok(()) 替代 return err!,结果会怎么样?下面的函数在返回错误时先记录一些信息,看看 msg! 宏的内容是否被记录。

pub fn func(ctx: Context<ReturnError>) -> Result<()> {
    msg!("Will this print?");
    return err!(Day4Error::AlwaysErrors);
}

#[derive(Accounts)]
pub struct ReturnError {}

#[error_code]
pub enum Day4Error {
    #[msg("AlwaysErrors")]
    AlwaysErrors,
}

在底层,require! 宏与返回错误没有什么不同,它只是语法糖。

预期结果是,当你返回 Ok(()) 时,“Will this print?” 会被打印,而当你返回错误时则不会打印。

关于 Solana 和 Solidity 在处理错误方面的区别

在 Solidity 中,require 语句通过 revert 操作码停止执行。Solana 并不会停止执行,而是简单返回一个不同的值。这类似于操作系统如何在成功时返回 0 或 1。如果返回了 0(相当于返回 Ok(())),那么一切顺利。

因此,Solana 程序应该始终返回某个值——要么是 Ok(()) 或者是 Error

在 Anchor 中,错误是具有 #[error_code] 属性的枚举。

请注意,Solana 中的所有函数返回类型都是 Result<()>。一个 result 是一种可能是 Ok(()) 或错误的类型。

问题与解答

为什么 Ok(()) 结尾没有分号?

如果你添加它,你的代码将无法编译。如果 Rust 中最后一条语句没有分号,那么该行的值会被返回。

为什么 Ok(()) 有一对额外的括号?

() 在 Rust 中表示“单位”,你可以将其视为 C 中的 void 或 Haskell 中的 Nothing。这里,Ok 是包含单位类型的枚举。这就是返回的内容。在 Rust 中,不返回任何内容的函数隐式返回单位类型。没有分号的 Ok(())return Ok(()) 在语法上是等价的。请注意末尾的分号。

上述 if 语句为何缺少括号?

在 Rust 中,这些是可选的。

了解更多

本教程是 Solana 课程 的一部分。

最初发布于 2024 年 2 月 11 日

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

0 条评论

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