如何使用 Solidity 和 Solang 创建 Solana 程序

  • QuickNode
  • 发布于 2024-04-03 21:50
  • 阅读 10

本文介绍了如何使用 Solang 编译器在 Solana 上编写和部署 Solidity 智能合约,并详细讲解了如何创建一个计分板程序,包括初始化项目、编写合约、部署和测试程序。

概述

Solang 是一个用于 Solana 的 Solidity 编译器。它允许开发者使用 Solidity 编写智能合约并将其部署到 Solana。最近,Anchor(Solana 的用于构建 Solana 程序的框架)添加了对 Solang 的支持。本指南将展示如何使用 Solidity 和 Solang 创建并测试一个 Solana 程序。

你将做什么

  • 学习一些使用 Solang 构建的基础知识
  • 使用 Solidity 和 Solang 创建一个记分板程序
  • 使用本地网络将程序部署到 Solana
  • 运行测试以确保程序按预期工作

你需要什么

Anchor 安装

在撰写本文时(2023 年 7 月),Anchor 添加了一个新功能,启用了 Solang 的最新解析器,支持 Solidity 的新语法。尽管此功能已合并,但尚未发布。你必须从 main 分支安装 Anchor 才能使用此功能。为此,请运行以下命令:

cargo install --git https://github.com/coral-xyz/anchor anchor-cli --locked --force
依赖项 版本
node.js 18.16.1
anchor cli 0.28(来自 main 分支的最新版本)
solana cli 1.16.5
tsc 5.0.2

为了确保你已准备好开始,请验证你是否安装了 Solana 1.16+ 和 Anchor 0.28+。你可以通过运行以下命令来做到这一点:

solana --version
anchor --version

让我们开始吧!

Solang 基础知识

Solang 是一个编译器,允许开发者使用 Solidity 编写智能合约并将其部署到 Solana。尽管 Solang 正在工作并且最近已添加到 Anchor,但它仍在积极开发中,因此某些功能可能会发生变化。Solana GitHub 仓库可以在 这里 找到。Solang 与 Solidity 0.8 源代码兼容,但由于 Solana 的差异,有一些注意事项。让我们讨论 Solana 的一些关键特性以及它们在 Solang 中的实现:

账户

在 Solana 中,账户是存储和处理的基本单位。它们几乎就像文件系统中的文件。有两种类型的账户,程序(具有可执行代码的账户)和数据账户(管理状态的账户)。程序是无状态的,只能修改数据账户。数据账户是有状态的,只能由拥有它们的程序修改。对于使用 Solang 来说,这意味着 Solidity 合约将使用一个账户用于合约(程序)本身,并使用构造函数创建新的数据账户来管理状态。这些数据账户称为程序派生地址 (PDAs)。程序的地址通过在合约开始时声明来定义,如下所示:

@program_id("F1ipperKF9EfD821ZbbYjS319LXYiBmjhzkkf5a26rC")

程序派生地址 (PDAs)

PDAs 是 Solana 上重要的数据存储账户。它们由一个程序拥有,并且只能由该程序修改。PDAs 由程序创建,并从一组种子和程序的地址派生。你可以将种子视为数据集的唯一标识符。例如,如果你要创建一个 PDA 来存储用户的名称,你可能会使用单词 "name" 和用户的公钥作为种子。这将确保 PDA 的地址对该用户是唯一的。

在 Solang 中,种子在构造函数中使用注解定义:

  • @seed - 标识用于派生 PDA 的单个种子(可以有多个种子)
  • @bump - 用于从指定的种子派生 PDA(这是可选的)
  • @space - 为 PDA 分配的字节数(这是可选的)
  • @payer - 将支付 PDA 费用的账户(这是可选的)

示例:

@program_id("Foo5mMfYo5RhRcWa4NZ2bwFn4Kdhe8rNK5jchxsKrivA")
contract Foo {

    @space(500)
    @seed("Foo")
    @payer(payer)
    constructor(@seed bytes user_wallet, @bump bytes1 bump_val) {
        // ...
    }
}

当调用时,此构造函数将创建一个新的 PDA,该 PDA 对程序 "Foo" 和提供的 user_wallet 是唯一的。每个账户将初始化为 500 字节(由 payer 支付费用)。“bump” 实际上是我们用来确定性地找到 PDA 的 ed25519 椭圆曲线上的一组距离。要了解更多关于 PDAs 和 bumps 的信息,请查看我们的 指南:如何使用 PDAs

注意:如果合约没有构造函数,注解可以与空构造函数配对。

可以通过创建一个以 PDA 地址为参数的 getter 函数来访问存储在 PDA 中的值。例如:

    function getSomeValue() public view returns (uint64) {
        return accountData.someValue;
    }

在我们的客户端,我们可以使用 PDA 的地址来获取存储在 PDA 中的值。例如:

    const score = await program.methods.getSomeValue()
      .accounts({ dataAccount })
      .view();

接口定义语言 (IDL)

如果你习惯于在 EVM 上构建,你无疑熟悉 ABIs。Solana 使用类似的概念,称为接口定义语言 (IDL)。IDLs 用于定义程序与其客户端之间的接口。它们用于生成程序的客户端库。有关 IDLs 的更多信息,请查看我们的 指南:什么是 IDLs。对于本演示,重要的是要知道 Anchor 会在我们编译程序时从我们的程序生成一个 IDL。然后我们可以使用 IDL 为我们的程序创建一个客户端库。

尽管使用 Solang 还有其他细微差别(并且未来可能会进行更改),但这些基础知识将帮助你开始并编写你的第一个合约。现在,让我们构建一些东西!

创建记分板程序

初始化项目

让我们首先使用 Anchor 初始化一个新的 Solang 项目。从你想要保存项目的目录中,运行以下命令:

anchor init scoreboard --solidity

这将创建一个名为 scoreboard 的新目录,其中包含你开始所需的所有文件。--solidity 标志告诉 Anchor 我们要使用 Solang 编译我们的程序。导航到你的新项目并在你喜欢的代码编辑器中打开它。

cd scoreboard

你应该有一个名为 solidity 的文件夹,其中包含一个文件 scoreboard.sol。这是我们将编写合约的地方。 确保你已将纸质钱包保存到 Anchor.toml 中列出的目录(在项目的根目录中):

[provider]
cluster = "Localnet"
wallet = "/YOUR/PATH/TO/WALLET/id.json"

确保更新路径和文件名以反映你钱包的位置。你可以通过运行以下命令来确保你已在此处保存了一个钱包:

solana address -k /YOUR/PATH/TO/WALLET/id.json

如果没有返回地址,请通过输入以下命令生成一个:

solana-keygen new --no-bip39-passphrase -o /YOUR/PATH/TO/WALLET/id.json

确保你的 Solana CLI 设置为该钱包和本地网络,通过运行以下命令:

solana config set -u localhost -k /YOUR/PATH/TO/WALLET/id.json

运行 solana config get 应该显示与我们在 Anchor.toml 中概述的相同的钱包路径和 RPC URL: http://localhost:8899

在构建我们的程序之前,你应该能够对 Anchor 启动的示例合约运行测试,以确保你的环境已设置并正常工作。为此,请运行以下命令:

anchor test

这将编译程序并运行 tests 目录中的测试。你应该在你的本地集群上成功模拟该程序。如果你有任何问题,请检查并确保你没有错过任何步骤,或者在 Discord 上联系我们,看看我们是否能提供帮助。

创建记分板程序

我们将创建一个简单的程序,允许用户将他们的分数存储在区块链上。我们的合约应该能够:

  • 初始化一个 PDA 来存储用户的当前分数、个人最高分数和公钥(每个用户都应该有自己的 PDA)
  • 将 0-100 点添加到用户的当前分数(如果当前分数高于他们的最高分数,则更新他们的最高分数)
  • 将他们的当前分数重置为 0
  • 获取用户的当前分数和个人最高分数

让我们首先在你的代码编辑器中打开 /solidity/scoreboard.sol 并用以下内容替换默认的起始代码:

@program_id("F1ipperKF9EfD821ZbbYjS319LXYiBmjhzkkf5a26rC")
contract scoreboard {
    // 1 - 定义将存储在账户中的数据结构

    // 2 - 定义我们的账户初始化器
    constructor(

    ) {

    }

    // 3 - 添加与数据交互的函数

}

我们正在定义我们合约的程序 ID 并构建我们将遵循的三个步骤来构建这个项目。

步骤 1 - 定义数据结构

让我们创建一个新的私有变量 accountData 来存储我们所有的用户分数信息。我们将使用一个结构体来定义我们想要存储在账户中的数据。将以下代码添加到你的合约中:

    // 1 - 定义将存储在账户中的数据结构
    UserScore private accountData;

    struct UserScore {
        address player;
        uint64 currentScore;
        uint64 highestScore;
        bytes1 bump;
    }

我们在这里做的是:

  • 定义一个新的私有变量 accountData,类型为 UserScore
  • 定义一个新的结构体 UserScore,它将存储以下数据:

    • player - 用户的公钥(地址 是 base58 编码的公钥。)

    • currentScore - 用户的当前分数

    • highestScore - 用户的最高分数

    • bump - 用于派生 PDA 的 bump 种子(通常最好存储此值以确保 PDA 正确派生)

步骤 2 - 定义我们的账户初始化器

在 Solang 中,账户(PDA)的初始化通过合约的构造函数进行。正如我们之前讨论的,这是我们需要定义我们的支付者和 PDA 种子的地方。在你的 UserScore 枚举下方,添加以下代码:

    // 2 - 定义我们的账户初始化器
    @payer(payer)
    @seed("seed")
    constructor(
        @seed bytes payer,
        @bump bytes1 bump,
        address player
    ) {
        print("New QuickNode UserScore account initialized");
        accountData = UserScore (player, 0, 0, bump);
    }

让我们分解一下:

  1. 我们使用 @payer 注解来定义 PDA 的支付者。此账户将支付 PDA 的 租金
  2. 我们使用 @seed 注解来定义 PDA 的两个种子:
    • 单词 "seed"(这是第一个种子)
    • 用户的公钥(这是第二个种子,我们将作为参数传入)
  3. 我们使用 @bump 注解来定义 PDA 的 bump 种子。我们将在客户端生成它并将其作为参数传入
  4. 我们将玩家的公钥作为参数传入。
  5. 我们用玩家的公钥和 0 作为他们的当前和最高分数来初始化我们的 accountData 变量。

步骤 3 - 添加函数

让我们创建一些将与我们的账户数据交互的函数。

创建 setter 函数

让我们创建两个函数来设置我们的分数值:一个用于添加分数,另一个用于重置用户的当前分数。在你的构造函数之后,添加以下内容:

    // 3 - 添加与数据交互的函数
    function addPoints(uint8 numPoints) public {
        require(numPoints > 0 && numPoints < 100, 'INVALID_POINTS');
        accountData.currentScore += numPoints;
        if (accountData.currentScore > accountData.highestScore) {
            accountData.highestScore = accountData.currentScore;
        }
    }

    function resetScore() public {
        accountData.currentScore = 0;
    }

让我们看看我们在做什么:

  1. 我们创建一个名为 addPoints 的函数,它接受一个 uint8 作为参数。该函数检查接收到的参数是否大于 0 且小于 100。如果是,则将分数添加到用户的当前分数。如果用户的当前分数超过他们的最高分数,则更新他们的最高分数。
  2. 我们创建一个名为 resetScore 的函数,将用户的当前分数设置为 0,但不更新他们的最高分数。

创建 getter 函数

让我们添加两个函数,以便轻松获取用户的当前和最高分数。在你的 setter 函数之后,添加以下内容:

    function getCurrentScore() public view returns (uint64) {
        return accountData.currentScore;
    }

    function getHighScore() public view returns (uint64) {
        return accountData.highestScore;
    }

每个函数都不接受参数,并分别返回用户的当前和最高分数。

构建你的程序

完成你的合约后,继续运行以下命令以确保它编译:

anchor build

你应该会收到一个通知,表示你的 LLVM IR 和 IDL 文件已生成。干得好!让我们编写一些测试以确保我们的程序按预期工作。

测试你的程序

开箱即用,Anchor 安装了 Chai 断言库Mocha 测试框架。我们将使用这些来编写我们的测试。

打开 tests 目录和 scoreboard.ts 在你的代码编辑器中。你应该看到一个检查程序是否已正确初始化的测试。该测试是针对 Anchor 启动的默认合约的,因此你可能会看到一些错误。让我们删除此测试并编写我们自己的。将默认测试替换为以下内容:

import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { PublicKey } from "@solana/web3.js";
import { assert, expect } from "chai";
import { Scoreboard } from "../target/types/scoreboard";

function randomPointsGenerator(min = 1, max = 100) {
  return Math.floor(Math.random() * (max - min + 1) + min)
}

describe("Scoreboard", () => {
  const provider = anchor.AnchorProvider.env();
  anchor.setProvider(provider);
  const program = anchor.workspace.Scoreboard as Program<Scoreboard>;
  const wallet = provider.wallet;
  const walletSeed = wallet.publicKey.toBuffer();
  const [dataAccount, bump] = PublicKey.findProgramAddressSync(
    [\
      Buffer.from("seed"),\
      walletSeed\
    ],
    program.programId
  );

  it("Test 1 - Initializes a new account!", async () => {

  });

});

我们在这里做的是:

  1. 我们导入必要的依赖项,包括从我们编译程序时生成的 IDL 文件中导入的类型。
  2. 我们创建一个生成 1 到 100 之间随机数的函数。我们将使用它来向用户的分数添加分数。
  3. 我们使用 describe 创建一个名为 "Scoreboard" 的新测试套件。
  4. 我们定义一个 providerprogramwallet,这是与我们的程序交互所需的上下文。
  5. 我们使用 PublicKey.findProgramAddressSync 方法创建一个 dataAccountbump 种子(这需要我们传入我们在程序中定义的相同种子:单词 "seed" 和用户的公钥作为缓冲区)。我们将使用这些来创建用户分数数据的 PDA。
  6. 我们使用 it 创建一个名为 "Test 1 - Initializes a new account!" 的新测试。

测试账户初始化

将以下代码添加到你的第一个测试中:

  it("Test 1 - Is initialized!", async () => {
    const tx = await program.methods.new(walletSeed, [bump], dataAccount)
      .accounts({ dataAccount })
      .rpc();
    const score = await program.methods.getCurrentScore()
      .accounts({ dataAccount })
      .view();
    expect(score.toNumber()).to.equal(0);
  });

如果你习惯于在 Anchor 中构建程序,这应该看起来有些熟悉但略有不同。让我们分解一下:

  1. 我们使用 program.methods.new 命令初始化一个新的 PDA。此命令接受我们在构造函数中定义的参数(我们钱包的缓冲区、bump 和新 PDA 的 publicKey)并创建一个新的 PDA。请注意,对于 Solana,我们必须将我们将使用的账户传入交易上下文中——Anchor 允许我们使用 accounts 方法做到这一点。我们传入我们之前创建的 dataAccount
  2. 我们使用 rpc 方法发送并确认交易。
  3. 我们使用 program.methods.getCurrentScore 命令获取用户的当前分数。我们传入我们之前创建的 dataAccount。如果你想获取其他用户的分数,你将传入他们的 PDA 的地址。
  4. 我们使用 expect 方法断言用户的当前分数为 0。请注意,我们使用 toNumber 方法将分数转换为数字。这是因为分数作为 BN(大数字)对象返回。

你可以通过运行以下命令运行此测试:

anchor test

你应该在终端中看到以下输出:

  Scoreboard
    ✔ Test 1 - Is initialized! (413ms)

干得好!

测试添加分数

让我们添加一个测试以确保我们可以向用户的

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

0 条评论

请先 登录 后评论
QuickNode
QuickNode
江湖只有他的大名,没有他的介绍。