文章详细介绍了在 Solana 的 Anchor 框架中如何处理函数参数的限制,类似于以太坊中的 require
语句。通过代码示例展示了如何使用 require!
宏和错误处理机制来确保函数参数的有效性,并解释了 Solana 和以太坊在错误处理上的差异。
在以太坊中,我们经常会看到一个 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);
}
});
});
练习:
enum MyError
中错误的顺序,错误代码会发生什么?#[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!
宏,概念上与 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?
” 会被打印,而当你返回错误时则不会打印。
在 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 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!