您正在查看: Surou 发布的文章

Solana基础 - Solidity开发者的Rust基础

本教程深入探讨了Solidity和Rust在控制流、数组、映射、结构体和常量等方面的语法对比,旨在帮助掌握Solidity的开发者快速上手Rust编程。

本教程介绍了在 Solidity 中最常用的语法,并展示了其在 Rust 中的等价实现。

如果你想了解 Rust 与 Solidity 之间的高层次差异,请查看链接的教程。本教程假设你已经了解 Solidity,假如你对 Solidity 不熟悉,请查看我们免费的 Solidity 教程

创建一个名为 tryrust 的新 Solana Anchor 项目并设置环境。

条件语句

我们可以说,开发人员在 Solidity 中可以根据特定条件控制执行流程的方式有 2 种:

  • If-Else 语句
  • 三元运算符

现在让我们看看上述内容在 Solidity 中的实现,以及它们在 Solana 中的翻译。

If-Else 语句

在 Solidity 中:

function ageChecker(uint256 age) public pure returns (string memory) {
    if (age >= 18) {
        return "你已满 18 岁或以上";
    } else {
        return "你未满 18 岁";
    }
}

在 Solana 中,在 lib.rs 中添加一个名为 age_checker 的新函数:

pub fn age_checker(ctx: Context<Initialize>, age: u64) -> Result<()> {
    if age >= 18 {
        msg!("你已满 18 岁或以上");
    } else {
        msg!("你未满 18 岁");
    }
    Ok(())
 }

请注意,条件 age >= 18 没有括号——这在 if 语句中是可选的。

要测试,请在 ./tests/tryrust.ts 中添加另一个 it 块:

it("年龄检查", async () => {
    // 在这里添加你的测试。
    const tx = await program.methods.ageChecker(new anchor.BN(35)).rpc();
    console.log("你的交易签名", tx);
});

运行测试后,我们应该得到以下日志:

Transaction executed in slot 77791:
  Signature: 2Av18ej2YjkRhzybbccPpwEtkw73VcBpDPZgC9iKrmf6mvwbqjA517garhrntWxKAM1ULL2eAv5vDWJ3SjnFZq6j
  Status: Ok
  Log Messages:
    Program 53hgft52DHUKMPHGu1kusuwxFGk2T8qngwSw2SyGRNrX invoke [1]
    Program log: Instruction: AgeChecker
    Program log: 你已满 18 岁或以上
    Program 53hgft52DHUKMPHGu1kusuwxFGk2T8qngwSw2SyGRNrX consumed 440 of 200000 compute units
    Program 53hgft52DHUKMPHGu1kusuwxFGk2T8qngwSw2SyGRNrX success

三元运算符

在 Solidity 中将 if-else 语句分配给变量:

function ageChecker(uint256 age) public pure returns (bool a) {
    a = age % 2 == 0 ? true : false;
}

在 Solana 中,我们基本上只是将一个 if-else 语句分配给一个变量。下面的 Solana 程序与上述相同:

pub fn age_checker(ctx: Context<Initialize>, age: u64) -> Result<()> {
    let result = if age >= 18 {"你已满 18 岁或以上"} else { "你未满 18 岁" };
    msg!("{:?}", result);
    Ok(())
}

请注意,在 Rust 的三元运算符示例中,if/else 块以分号结尾,因为这是分配给一个变量的。

还要注意,内部值没有以分号结尾,因为它作为返回值返回给变量,就像在表达式而不是语句的情况下不在 Ok(()) 之后加分号一样。

如果年龄是偶数,程序将输出 true,否则为 false:

Transaction executed in slot 102358:
  Signature: 2zohZKhY56rLb7myFs8kabdwULJALENyvyFS5LC6yLM264BnkwsThMnotHNAssJbQEzQpmK4yd3ozs3zhG3GH1Gx
  Status: Ok
  Log Messages:
    Program 53hgft52DHUKMPHGu1kusuwxFGk2T8qngwSw2SyGRNrX invoke [1]
    Program log: Instruction: AgeChecker
    Program log: true
    Program 53hgft52DHUKMPHGu1kusuwxFGk2T8qngwSw2SyGRNrX consumed 792 of 200000 compute units
    Program 53hgft52DHUKMPHGu1kusuwxFGk2T8qngwSw2SyGRNrX success

Rust 还有一个更强大的控制流结构叫做 match。让我们看看下面的使用 match 的示例:

pub fn age_checker(ctx: Context<Initialize>, age: u64) -> Result<()> {
    match age {
        1 => {
            // 如果年龄等于 1 则执行的代码块
            msg!("年龄是 1");
        },
        2 | 3 => {
            // 如果年龄等于 2 或 3 则执行的代码块
            msg!("年龄是 2 或 3");
        },
        4..=6 => {
            // 如果年龄在 4 到 6(包括)之间则执行的代码块
            msg!("年龄在 4 到 6 之间");
        },
        _ => {
            // 任何其他年龄的代码块
            msg!("年龄是其他值");
        }
    }
    Ok(())
}

For 循环

我们知道,for 循环允许遍历范围、集合和其他可迭代对象,在 Solidity 中是这样写的:

function loopOverSmth() public {
    for (uint256 i=0; i < 10; i++) {
        // 做一些事情...
    }
}

这在 Solana(Rust)中的等价实现为:

pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
    for i in 0..10 {
        // 做一些事情...
    }

    Ok(())
}

是的,简单如斯,但我们如何使用自定义步长遍历范围呢?以下是在 Solidity 中的预期行为:

function loopOverSmth() public {
    for (uint256 i=0; i < 10; i+=2) {
        // 做一些事情...

        // 将 i 增加 2
    }
}

在 Solana 中使用 step_by 的等价实现是:

pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
    for i in (0..10).step_by(2) {
        // 做一些事情...

        msg!("{}", i);
    }     

    Ok(())
}

运行测试后,我们应该获得以下日志:

Transaction executed in slot 126442:
  Signature: 3BSPA11TZVSbF8krjMnge1fgwNsL9odknD2twAsDeYEF39AzaJy1c5TmFCt6LEzLtvWnjzx7VyFKJ4VT1KQBpiwm
  Status: Ok
  Log Messages:
    Program 53hgft52DHUKMPHGu1kusuwxFGk2T8qngwSw2SyGRNrX invoke [1]
    Program log: Instruction: Initialize
    Program log: 0
    Program log: 2
    Program log: 4
    Program log: 6
    Program log: 8
    Program 53hgft52DHUKMPHGu1kusuwxFGk2T8qngwSw2SyGRNrX consumed 2830 of 200000 compute units
    Program 53hgft52DHUKMPHGu1kusuwxFGk2T8qngwSw2SyGRNrX success

数组和向量

Rust 和 Solidity 在数组支持上有所不同。虽然 Solidity 原生支持固定和动态数组,但 Rust 仅内置支持固定数组。如果你想要动态长度的列表,请使用向量。

现在,让我们看一些示例,演示如何声明和初始化固定和动态数组。

固定数组

pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
    // 声明一个固定大小为 5 的 u32 数组
    let my_array: [u32; 5] = [10, 20, 30, 40, 50];

    // 访问数组元素
    let first_element = my_array[0];
    let third_element = my_array[2];

    // 声明一个固定大小为 3 的可变 u32 数组
    let mut mutable_array: [u32; 3] = [100, 200, 300];

    // 将第二个元素从 200 修改为 250
    mutable_array[1] = 250;

    // 你程序的其余逻辑

    Ok(())
}

动态数组

在 Solana 中模拟动态数组的方法涉及使用来自 Rust 标准库的 Vec(向量)。以下是一个示例:

pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
    // 使用 Vec 声明一个类似动态数组的结构
    let mut dynamic_array: Vec<u32> = Vec::new();

    // 向动态数组添加元素
    dynamic_array.push(10);
    dynamic_array.push(20);
    dynamic_array.push(30);

    // 访问动态数组的元素
    let first_element = dynamic_array[0];
    let third_element = dynamic_array[2];

    // 你程序的其余逻辑
    msg!("第三个元素 = {}", third_element);

    Ok(())
}

dynamic_array 变量必须声明为可变(mut),以允许进行变更(推送、弹出、覆盖索引等)。

程序在运行测试后应该记录如下内容:

Transaction executed in slot 195373:
  Signature: 4113irrcBsFbNaiZia5c84yfJpS4Hn4H1QawfUSHYoPuuQPj22JnVFtDMHmZDFkQ3vK15SrDUSTakh5fT4N8UVRf
  Status: Ok
  Log Messages:
    Program 53hgft52DHUKMPHGu1kusuwxFGk2T8qngwSw2SyGRNrX invoke [1]
    Program log: Instruction: Initialize
    Program log: 第三个元素 = 30
    Program 53hgft52DHUKMPHGu1kusuwxFGk2T8qngwSw2SyGRNrX consumed 1010 of 200000 compute units
    Program 53hgft52DHUKMPHGu1kusuwxFGk2T8qngwSw2SyGRNrX success

映射

与 Solidity 不同,Solana 缺少内置的映射数据结构。然而,我们可以通过利用 Rust 标准库中的 HashMap 类型在 Solana 中复制键值映射的功能。与 EVM 链不同的是,我们在这里展示的映射是在内存中的,而不是存储中的。EVM 链没有内存哈希映射。我们将在稍后的教程中展示 Solana 中的存储映射。

让我们看看如何使用 HashMap 在 Solana 中创建一个映射。将提供的代码片段复制并粘贴到 lib.rs 文件中,并记得将程序 ID 替换为你自己的:

use anchor_lang::prelude::*;

declare_id!("53hgft52DHUKMPHGu1kusuwxFGk2T8qngwSw2SyGRNrX");

#[program]
pub mod tryrust {
    use super::*;
        // 导入 HashMap 库
    use std::collections::HashMap;

    pub fn initialize(ctx: Context<Initialize>, key: String, value: String) -> Result<()> {
        // 初始化映射
        let mut my_map = HashMap::new();

        // 向映射中添加键值对
        my_map.insert(key.to_string(), value.to_string());

        // 记录映射中与键对应的值
        msg!("我的名字是 {}", my_map[&key]);

        Ok(())
    }
}

my_map 变量也被声明为可变,以便进行编辑(即,添加/删除键→值对)。还注意我们是如何导入 HashMap 库的?

由于 initialize 函数接收两个参数,因此测试也需要进行更新:

it("已初始化!", async () => {
    // 在这里添加你的测试。
    const tx = await program.methods.initialize("name", "Bob").rpc();
    console.log("你的交易签名", tx);
});

当我们运行测试时,会看到以下日志:

Transaction executed in slot 216142:
  Signature: 5m4Cx26jaYT3c6YeJbLMDHppvki4Kmu3zTDMgk8Tao9v8b9sH7WgejETzymnHuUfr4hY25opptqniBuwDpncbnB9
  Status: Ok
  Log Messages:
    Program 53hgft52DHUKMPHGu1kusuwxFGk2T8qngwSw2SyGRNrX invoke [1]
    Program log: Instruction: Initialize
    Program log: 我叫 Bob
    Program 53hgft52DHUKMPHGu1kusuwxFGk2T8qngwSw2SyGRNrX consumed 2634 of 200000 compute units
    Program 53hgft52DHUKMPHGu1kusuwxFGk2T8qngwSw2SyGRNrX success

结构体

在 Solidity 和 Solana 中,结构体用于定义可以包含多个字段的自定义数据结构。让我们看看在 Solidity 和 Solana 中的结构体示例。

在 Solidity 中:

contract SolidityStructs {

    // 在 Solidity 中定义结构体
    struct Person {
        string my_name;
        uint256 my_age;
    }

    // 创建结构体的实例
    Person person1;

    function initPerson1(string memory name, uint256 age) public {
        // 访问和修改结构体字段
        person1.my_name = name;
        person1.my_age = age;
    }
}

在 Solana 中与之对应的实现:

pub fn initialize(_ctx: Context<Initialize>, name: String, age: u64) -> Result<()> {
    // 在 Solana 中定义结构体
    struct Person {
        my_name: String,
        my_age: u64,
    }

    // 创建结构体的实例
    let mut person1: Person = Person {
        my_name: name,
        my_age: age,
    };

    msg!("{} 的年龄为 {} 岁", person1.my_name, person1.my_age);

    // 访问和修改结构体字段
    person1.my_name = "Bob".to_string();
    person1.my_age = 18;

    msg!("{} 的年龄为 {} 岁", person1.my_name, person1.my_age);

    Ok(())
}

练习 : 更新测试文件以传递两个参数 Alice 和 20 给 initialize 函数并运行测试,你应该得到以下日志:

Transaction executed in slot 324406:
  Signature: 2XBQKJLpkJbVuuonqzirN9CK5dNKnuu5NqNCGTGgQovWBfrdjRcVeckDmqtzyEPe4PP8xSN8vf2STNxWygE4BPZN
  Status: Ok
  Log Messages:
    Program 53hgft52DHUKMPHGu1kusuwxFGk2T8qngwSw2SyGRNrX invoke [1]
    Program log: Instruction: Initialize
    Program log: Alice 的年龄为 20 岁
    Program log: Bob 的年龄为 18 岁
    Program 53hgft52DHUKMPHGu1kusuwxFGk2T8qngwSw2SyGRNrX consumed 2601 of 200000 compute units
    Program 53hgft52DHUKMPHGu1kusuwxFGk2T8qngwSw2SyGRNrX success

在提供的代码片段中,Solidity 实现将结构体的实例存储在存储中,而在 Solana 实现中,所有操作都发生在初始化函数中,且没有在链上存储。存储将在以后的教程中讨论。

Rust 中的常量

在 Rust 中声明常量变量非常简单。使用 const 关键字而不是 let 关键字。可以在 #[program] 块外声明这些常量。

use anchor_lang::prelude::*;

declare_id!("EiR8gcMCX11tYMRfoZ2vyheZsZ2NvdUTvYrRAUvTtYnL");

// *** 在这里声明常量 ***
const MEANING_OF_LIFE_AND_EXISTENCE: u64 = 42;

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

    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        msg!(&format!("终极问题的答案: {}", MEANING_OF_LIFE_AND_EXISTENCE)); // 这里的新行
        Ok(())
    }
}

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

usize 类型和类型转换

大多数时候我们可以假定在 Solana 中的无符号整数类型为 u64,但在测量列表的长度时,有一个例外:它将是 usize 类型。你需要将变量转换,如以下 Rust 代码所示:

use anchor_lang::prelude::*;

declare_id!("EiR8gcMCX11tYMRfoZ2vyheZsZ2NvdUTvYrRAUvTtYnL");

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

    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {

       let mut dynamic_array: Vec<u32> = Vec::from([1,2,3,4,5,6]);
       let len = dynamic_array.len(); // 这个类型为 usize

       let another_var: u64 = 5; // 这个类型为 u64

       let len_plus_another_var = len as u64 + another_var;

       msg!("结果是 {}", len_plus_another_var);

       Ok(())
    }
}

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

Try Catch

Rust 没有 try catch。故障预期会返回错误(就像我们在 Solana 回滚和错误的教程中所做的)或对于不可恢复性错误引发恐慌。

练习 : 编写一个 Solana / Rust 程序,接受一个 u64 向量,遍历它,将所有偶数推送到另一个向量中,然后打印新的向量。

转载:https://learnblockchain.cn/article/11406

Solana基础 - Solana程序是可升级的,并且没有构造函数

本文详细讲解了如何在Solana上使用Anchor框架部署程序,并解释了Solana程序的可变性和与Ethereum的差异。通过代码示例和命令行操作,展示了程序的部署、升级和测试过程。

在本教程中,我们将从幕后看看 Anchor 如何部署 Solana 程序。

让我们看看 Anchor 在我们运行 anchor init deploy_tutorial 时为我们创建的测试文件:

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

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

  it("Is initialized!", async () => {
    // 在此添加你的测试。
    const tx = await program.methods.initialize().rpc();
    console.log("你的交易签名", tx);
  });
});

它生成的启动程序应该很熟悉:

use anchor_lang::prelude::*;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

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

    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        Ok(())
    }
}

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

上述程序在哪里以及何时被部署?

合约唯一合理的部署地点是在测试文件中的这一行:

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

但这并没有意义,因为我们本该期望那是一个异步函数。

Anchor 在后台默默地部署程序。

Solana 程序默认没有构造函数

对于来自其他面向对象语言的人来说,这可能看起来不寻常。Rust 没有对象或类。

在以太坊智能合约中,构造函数可以配置存储或设置字节码和不可变变量。

那么“部署步骤”到底在哪里?

(如果你仍在运行 Solana 验证器和 Solana 日志,建议你重启并清除这两个终端)

让我们进行常规设置。创建一个名为 program-deploy 的新 Anchor 项目,并确保在其他终端中运行验证器和日志。

与其运行 anchor test,不如在终端中运行以下命令:

anchor deploy

Anchor 测试的截图

在上面的日志截图中,我们可以看到程序被部署的时刻。

现在,令人感兴趣的部分是。再次运行 anchor deploy

Anchor 测试升级后的截图

程序被部署到同一地址,但这次是 升级,而非重新部署。

程序 ID 没有改变,程序被覆盖了

Solana 程序默认是可变的

这可能对以太坊开发者来说是一个震惊,因为在以太坊中假设是不可变的。

如果作者可以随意更改程序,那么这个程序有什么意义?确实可以使 Solana 程序不可变。假设作者会先部署可变版本,随着时间的推移且未发现任何错误后,再将其重新部署为不可变。

在功能上,这与管理员控制的代理没有任何区别,后期所有者将所有权放弃给零地址。但可以说,Solana 模型更干净,因为以太坊代理可能会出问题。

另一个含义是:Solana 不需要 delegatecall,因为它不需要

在 Solidity 合约中,delegatecall 的主要用途是通过发出 delegatecalls 到新实现合约来升级代理合约的功能能力。然而,由于 Solana 中程序的字节码可以升级,因此无需调用实现合约。

另一个推论是:Solana 没有 Solidity 那种不可变变量(只能在构造函数中设置的变量)

在不重新部署程序的情况下运行测试

由于 Anchor 默认重新部署程序,让我们演示如何在不重新部署的情况下运行测试。

将测试更改为以下内容:

import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";

import fs from 'fs'
let idl = JSON.parse(fs.readFileSync('target/idl/deployed.json', 'utf-8'))

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

  // 更改为你的 programID
  const programID = "6p29sM4hEK8ZFT5AvsGJQG5nKUtHBKs13iVL6juo5Uqj";
  const program = new Program(idl, programID, anchor.getProvider());

  it("Is initialized!", async () => {
    // 在此添加你的测试。
    const tx = await program.methods.initialize().rpc();
    console.log("你的交易签名", tx);
  });
});

在你运行测试之前,建议清除 Solana 日志的终端并重启 solana-test-validator。

现在,使用以下测试命令:

anchor test --skip-local-validator --skip-deploy

现在查看日志终端:

跳过部署的 Anchor 测试截图

我们看到初始化指令已被执行,但程序既没有被部署也没有被升级,因为我们在 anchor test 中使用了 --skip-deploy 参数。

练习:要查看程序的字节码实际上发生了变化,部署两个打印不同 msg! 值的合约。具体步骤如下:

  1. lib.rs 中更新 initialize 函数,包括一个 msg! 语句,将字符串写入日志。
  2. anchor deploy
  3. anchor test --skip-local-validator --skip-deploy
  4. 检查日志以查看记录的消息
  5. 重复步骤 1 – 4,但更改 msg! 中的字符串
  6. 验证程序 ID 没有改变

你应该观察到消息字符串的变化,但程序 ID 保持不变。

总结

  • Solana 没有构造函数,程序“只是部署”。
  • Solana 没有不可变变量。
  • Solana 没有 delegatecall,程序可以“被更新”。

转载:https://learnblockchain.cn/article/11415

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

文章详细介绍了在 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);
    }
  });
});

练习

  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 中,这些是可选的。

转载:https://learnblockchain.cn/article/11401

Solana基础 - 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函数。

转载:https://learnblockchain.cn/article/11414

Solana基础 - Solana 和 Rust 中的算术与基本类型

本文详细介绍了如何在Solana平台上构建程序,其功能与Solidity合约类似,并探讨了Solana如何处理算术溢出问题。文章通过具体示例展示了如何在Rust中实现函数,处理数据类型,以及进行单元测试,同时强调了计算成本及浮点操作的性能限制。
今天我们将学习如何创建一个Solana程序,该程序实现与下面的Solidity合约相同的功能。我们还将学习Solana如何处理整数溢出等算术问题。

contract Day2 {

    event Result(uint256);
    event Who(string, address);

    function doSomeMath(uint256 a, uint256 b) public {
        uint256 result = a + b;
        emit Result(result);
    }

    function sayHelloToMe() public {
        emit Who("Hello World", msg.sender);
    }
}

让我们开始一个新项目。

anchor init day2
cd day2
anchor build
anchor keys sync

确保在一个终端内运行Solana测试验证器:

solana-test-validator

在另一个终端运行Solana日志:

solana logs

通过运行测试确保新搭建的程序正常工作

anchor test --skip-local-validator

提供函数参数

在我们进行任何数学计算之前,让我们将初始化函数更改为接收两个整数。以太坊使用uint256作为“标准”整数大小。在Solana上,它是u64——这相当于Solidity中的uint64。

传递无符号整数

默认的初始化函数将如下所示:

pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
    Ok(())
}

在lib.rs中修改initialize()函数,如下所示。

pub fn initialize(ctx: Context<Initialize>,
                  a: u64,
                  b: u64) -> Result<()> {
    msg!("你发送了 {} 和 {}", a, b);
    Ok(())
}

现在我们需要更改./tests/day2.ts中的测试。

it("初始化成功!", async () => {
  // 在这里添加你的测试。
  const tx = await program.methods
    .initialize(new anchor.BN(777), new anchor.BN(888)).rpc();
  console.log("你的交易签名", tx);
});

现在重新运行anchor test --skip-local-validator

当我们查看日志时,应该看到以下内容

Transaction executed in slot 367357:
  Signature: 54iJFbtEE61T9X2WCLbMe8Dq2YYBzCLYE4qW2DqTsA4gZRgootcubLgHc1MHYncbP63sxNxEY8tJfgfgsdt1Ch4g
  Status: Ok
  Log Messages:
    Program 8o3ehd3XnyDocd9hG1uz5trbmSRB7gaLaE9BCXDpEnMY invoke [1]
    Program log: Instruction: Initialize
    Program log: 你发送了 777 和 888
    Program 8o3ehd3XnyDocd9hG1uz5trbmSRB7gaLaE9BCXDpEnMY 消耗了 1116 的 200000 计算单位
    Program 8o3ehd3XnyDocd9hG1uz5trbmSRB7gaLaE9BCXDpEnMY 成功

传递字符串

现在让我们演示如何将字符串作为参数传递。

pub fn initialize(ctx: Context<Initialize>,
                  a: u64,
                  b: u64,
                  message: String) -> Result<()> {
    msg!("你说了 {:?}", message);
    msg!("你发送了 {} 和 {}", a, b);
    Ok(())
}

并更改测试。

it("初始化成功!", async () => {
  // 在这里添加你的测试。
  const tx = await program.methods
    .initialize(
       new anchor.BN(777), new anchor.BN(888), "hello").rpc();
    console.log("你的交易签名", tx);
});

当我们运行测试时,会看到新的日志。

数字数组

接下来,我们添加一个函数(和测试)来演示如何传递数字数组。在Rust中,“向量”或Vec就是Solidity所称的“数组”。

pub fn initialize(ctx: Context<Initialize>,
                  a: u64,
                  b: u64,
                  message: String) -> Result<()> {
    msg!("你说了 {:?}", message);
    msg!("你发送了 {} 和 {}", a, b);
    Ok(())
}

// 添加这个函数
pub fn array(ctx: Context<Initialize>,
             arr: Vec<u64>) -> Result<()> {
    msg!("你的数组 {:?}", arr);
    Ok(())
}

我们将单元测试更新如下

it("初始化成功!", async () => {
  // 在这里添加你的测试。
  const tx = await program.methods.initialize(new anchor.BN(777), new anchor.BN(888), "hello").rpc();
  console.log("你的交易签名", tx);
});

// 添加这个测试
it("数组测试", async () => {
  const tx = await program.methods.array([new anchor.BN(777), new anchor.BN(888)]).rpc();
  console.log("你的交易签名", tx);
});

然后我们再次运行测试并查看日志,以查看数组输出:

Transaction executed in slot 368489:
  Signature: 3TBzE3NddEY8KREv1FSXnieoyT6G6iNxF1n4hJHCeeWhAsUward3MEKm9WJHV4PMjPxeN2jRSRC9Rq8FUKjXoBQR
  Status: Ok
  Log Messages:
    Program 8o3ehd3XnyDocd9hG1uz5trbmSRB7gaLaE9BCXDpEnMY invoke [1]
    Program log: Instruction: Initialize
    Program log: 你说了 [777, 888]
    Program 8o3ehd3XnyDocd9hG1uz5trbmSRB7gaLaE9BCXDpEnMY 消耗了 1587 的 200000 计算单位
    Program 8o3ehd3XnyDocd9hG1uz5trbmSRB7gaLaE9BCXDpEnMY 成功

提示:如果你在Anchor测试中遇到问题,请尝试在Google中搜索与该错误相关的“Solana web3 js”。Anchor使用的TypeScript库是Solana Web3 JS库。

Solana中的数学

浮点数学

Solana有一些有限的原生支持浮点运算。

然而,最好避免浮点运算,因为它们计算消耗很大(稍后我们将看到这一点的例子)。请注意,Solidity对浮点运算没有原生支持。

关于使用浮点数的限制请阅读这里

算术溢出

算术溢出在Solidity 0.8.0版本之前是一种常见的攻击途径,该版本默认内建了溢出保护。在Solidity 0.8.0或更高版本中,溢出检查是默认进行的。由于这些检查会消耗gas,因此有时开发人员会故意通过“unchecked”块来禁用它们。

Solana如何防止算术溢出?

方法1:在Cargo.toml中设置overflow-checks = true

如果在Cargo.toml文件中将键overflow-checks设置为true,则Rust将在编译器级别添加溢出检查。请查看下一步Cargo.toml的截图:

cargo.toml中的overflow-checks键设置为true

如果Cargo.toml文件是这样配置的,你就不需要担心溢出。

然而,添加溢出检查会增加交易的计算成本(我们稍后将对此进行重访)。因此在一些计算成本问题影响下,你可能希望将overflow-checks设置为false。要战略性地检查溢出,可以使用Rust中的checked_*运算符。

方法2:使用checked_*运算符。

让我们看看如何在Rust自身的算术运算中应用溢出检查。考虑下面的Rust代码片段。

  • 在第1行,我们使用通常的+运算符进行算术运算,它会静默溢出。
  • 在第2行,我们使用.checked_add,如果发生溢出则会抛出错误。请注意,我们还可以对其他操作使用.checked_*,如checked_subchecked_mul
let x: u64 = y + z; // 将静默溢出
let xSafe: u64 = y.checked_add(z).unwrap(); // 如果发生溢出,将panic

// checked_sub、checked_mul等也可用

练习1:设置overflow-checks = true,创造一个测试案例,通过做0 - 1来使u64发生下溢。你需要将这些数字作为参数传递,否则代码将无法编译。会发生什么?

你会看到,当测试运行时交易失败(附带一个相当隐晦的错误信息),因为Anchor启动了溢出保护:

练习1错误信息

练习2:现在将overflow-checks设置为false,然后再次运行测试。你应该看到下溢值为18446744073709551615。

练习3:在Cargo.toml中禁止溢出保护,进行let result = a.checked_sub(b).unwrap();,同时让a = 0b = 1。会发生什么?

你是否应该将overflow-checks = true保持在Cargo.toml文件中用于你的Anchor项目?通常是的。但是如果你正在进行一些复杂计算,你可能想将overflow-checks设置为false,并在关键节点上战略性地防御溢出以节省计算成本,接下来我们将演示这一点。

Solana计算单位101

在以太坊中,交易运行直到消耗交易指定的“gas限额”。Solana称“gas”为“计算单位”。默认情况下,交易限制为200,000计算单位。如果消耗超过200,000计算单位,该交易会回滚。

确定Solana中交易的计算成本

与以太坊相比,Solana确实便宜使用,但这并不意味着你在以太坊开发中的优化技能毫无用处。让我们测量一下我们的数学函数需要多少计算单位。

Solana日志终端还显示所使用的计算单位。我们为checked和unchecked减法提供了基准测试。

禁用溢出保护时消耗824计算单位:

禁用溢出保护的Solana日志

启用溢出保护时消耗872计算单位:

启用溢出保护的Solana日志

正如你所看到的,进行一个简单的数学运算将使用近1000个单位。由于我们有20万单位,我们每笔交易只能够执行几百个简单的算术操作。因此,尽管Solana上的交易通常比以太坊便宜,但我们仍然受到相对较小的计算单位上限的限制,并且无法在Solana链上执行复杂的计算任务,比如流体动力学模拟。

我们稍后会重访交易成本。

Power不使用与Solidity相同的语法

在Solidity中,如果我们想将x提高到y的幂,我们这样写:

uint256 result = x ** y;

Rust不使用这种语法。相反地,它使用.pow

let x: u64 = 2; // 基数的数据类型必须明确
let y = 3; // 指数的数据类型可以推断
let result = x.pow(y);

如果你担心溢出,还有.checked_pow

浮点数

使用Rust进行智能合约开发的一个好处是,我们不必导入像Solmate或Solady这样的库来进行数学运算。Rust是一个相当复杂的语言,内置了许多操作,如果我们需要一些代码,我们可以在Solana生态系统外寻找一个Rust crate(这是Rust中库的称谓)来完成这个任务。

让我们计算50的立方根。浮点数的立方根函数已内置在Rust语言中,使用函数cbrt()

// 注意我们将`a`更改为f32(浮点32)
// 因为`cbrt()`对u64不可用
pub fn initialize(ctx: Context<Initialize>, a: f32) -> Result<()> {
  msg!("你说了 {:?}", a.cbrt());
  Ok(());
}

还记得我们在前面的部分提到过浮点数计算可能非常耗费计算资源吗?好吧,在这里,我们看到我们的立方根操作消耗的计算单位超过了简单无符号整数算术的5倍以上:

Transaction executed in slot unspecified:
  Signature: VfvySG5vvVSAnsYLCsvB9N6PsuGwL39kKd1fMsyvuB7y5DUHURwQVHU9rv3Xkz5NJqGHLSXoWoW92zJb5VKYCEF
  Status: Ok
  Log Messages:
    Program 8o3ehd3XnyDocd9hG1uz5trbmSRB7gaLaE9BCXDpEnMY invoke [1]
    Program log: Instruction: Initialize
    Program log: 试图开始函数,参数为50
    Program log: 结果 = 3.6840315
    Program 8o3ehd3XnyDocd9hG1uz5trbmSRB7gaLaE9BCXDpEnMY 消耗了 4860 的 200000 计算单位
    Program 8o3ehd3XnyDocd9hG1uz5trbmSRB7gaLaE9BCXDpEnMY 成功

练习4:构建一个实现+、-、×和÷运算的计算器,并包含sqrt和log10运算。

转载:https://learnblockchain.cn/article/11403