在 Ethereum 中,我们经常看到 require 语句用于限制函数参数的取值。看看下面的例子:
function foobar(uint256 x) public {
require(x < 100, "I'm not happy with the number you picked");
// rest of the function logic
}
在上面的代码中,如果给 foobar 传递的值大于或等于 100,交易将会 revert。
在 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", () => {
// Configure the client to use the local cluster.
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.Day4 as Program<Day4>;
it("Input test", async () => {
// Add your test here.
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);
}
});
});
练习:
- 你在错误代码(Error number)中注意到了什么规律?如果你在
enum MyError中改变错误的顺序,错误代码会发生什么变化? - 使用这个代码块,将新的
func和错误添加到现有代码中:
#[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(())
}
// NEW FUNCTION
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")] // NEW ERROR, what do you think the error code will be?
AlwaysErrors,
}
并添加以下测试:
it("Error test", async () => {
// Add your test here.
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);
}
});
在运行之前,你认为新的错误代码会是多少?
Ethereum 和 Solana 在如何通过无效参数中止交易方面的显著区别在于,Ethereum 会触发一个 revert,而 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(())
}
在 Ethereum 中,我们知道如果函数发生了 revert,将不会记录任何日志,即使 revert 发生在记录日志的操作之后。例如,调用下方合约中的 tryToLog 将不会记录任何内容,因为该函数会 revert:
contract DoesNotLog {
event SomeEvent(uint256);
function tryToLog() public {
emit SomeEvent(100);
require(false);
}
}
练习:如果你在一个 Solana 程序函数的返回错误语句之前放置一个 msg! 宏,会发生什么?如果你将 return err! 替换为 Ok(()),又会发生什么?下面我们有一个函数,先用 msg! 记录一些内容,然后返回一个错误。看看 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 不会中止执行,而是简单地返回一个不同的值。这类似于 Linux 根据成功与否返回 0 或 1。如果返回了 0(相当于返回 Ok(())),则说明一切顺利。
因此,Solana 程序应该始终返回点什么 —— 要么是 Ok(()),要么是 Error。
在 Anchor 中,错误是带有 #[error_code] 属性的 enum。
请注意,Solana 中的所有函数都有一个 Result<()> 的返回类型。一个 result 是一种可以为 Ok(()) 或错误的类型。
常见问答
为什么 Ok(()) 末尾没有分号?
如果你加上了它,你的代码将无法编译。在 Rust 中,如果最后的语句没有分号,那么该行上的值就会被作为返回值返回。
为什么 Ok(()) 多了一层括号?
() 在 Rust 中代表“单元”(unit),你可以把它看作是 C 语言中的 void 或是 Haskell 中的 Nothing。在这里,Ok 是一个包含了单元类型的 enum。这就是被返回的内容。在 Rust 中,没有返回值的函数会隐式地返回单元类型。没有分号的 Ok(()) 在语法上等同于 return Ok(());。请注意结尾的分号。
为什么上面的 if 语句没有括号?
在 Rust 中,这些是可选的。
在 RareSkills 学习更多内容
本教程是我们免费 Solana 课程的一部分。
首发于 2024 年 2 月 11 日