本文介绍了如何使用 Solang 编译器在 Solana 上编写和部署 Solidity 智能合约,并详细讲解了如何创建一个计分板程序,包括初始化项目、编写合约、部署和测试程序。
Solang 是一个用于 Solana 的 Solidity 编译器。它允许开发者使用 Solidity 编写智能合约并将其部署到 Solana。最近,Anchor(Solana 的用于构建 Solana 程序的框架)添加了对 Solang 的支持。本指南将展示如何使用 Solidity 和 Solang 创建并测试一个 Solana 程序。
在撰写本文时(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 是一个编译器,允许开发者使用 Solidity 编写智能合约并将其部署到 Solana。尽管 Solang 正在工作并且最近已添加到 Anchor,但它仍在积极开发中,因此某些功能可能会发生变化。Solana GitHub 仓库可以在 这里 找到。Solang 与 Solidity 0.8 源代码兼容,但由于 Solana 的差异,有一些注意事项。让我们讨论 Solana 的一些关键特性以及它们在 Solang 中的实现:
在 Solana 中,账户是存储和处理的基本单位。它们几乎就像文件系统中的文件。有两种类型的账户,程序(具有可执行代码的账户)和数据账户(管理状态的账户)。程序是无状态的,只能修改数据账户。数据账户是有状态的,只能由拥有它们的程序修改。对于使用 Solang 来说,这意味着 Solidity 合约将使用一个账户用于合约(程序)本身,并使用构造函数创建新的数据账户来管理状态。这些数据账户称为程序派生地址 (PDAs)。程序的地址通过在合约开始时声明来定义,如下所示:
@program_id("F1ipperKF9EfD821ZbbYjS319LXYiBmjhzkkf5a26rC")
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();
如果你习惯于在 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 上联系我们,看看我们是否能提供帮助。
我们将创建一个简单的程序,允许用户将他们的分数存储在区块链上。我们的合约应该能够:
让我们首先在你的代码编辑器中打开 /solidity/scoreboard.sol
并用以下内容替换默认的起始代码:
@program_id("F1ipperKF9EfD821ZbbYjS319LXYiBmjhzkkf5a26rC")
contract scoreboard {
// 1 - 定义将存储在账户中的数据结构
// 2 - 定义我们的账户初始化器
constructor(
) {
}
// 3 - 添加与数据交互的函数
}
我们正在定义我们合约的程序 ID 并构建我们将遵循的三个步骤来构建这个项目。
让我们创建一个新的私有变量 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 正确派生)
在 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);
}
让我们分解一下:
@payer
注解来定义 PDA 的支付者。此账户将支付 PDA 的 租金。@seed
注解来定义 PDA 的两个种子:
@bump
注解来定义 PDA 的 bump 种子。我们将在客户端生成它并将其作为参数传入accountData
变量。让我们创建一些将与我们的账户数据交互的函数。
让我们创建两个函数来设置我们的分数值:一个用于添加分数,另一个用于重置用户的当前分数。在你的构造函数之后,添加以下内容:
// 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;
}
让我们看看我们在做什么:
addPoints
的函数,它接受一个 uint8
作为参数。该函数检查接收到的参数是否大于 0 且小于 100。如果是,则将分数添加到用户的当前分数。如果用户的当前分数超过他们的最高分数,则更新他们的最高分数。resetScore
的函数,将用户的当前分数设置为 0,但不更新他们的最高分数。让我们添加两个函数,以便轻松获取用户的当前和最高分数。在你的 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 () => {
});
});
我们在这里做的是:
describe
创建一个名为 "Scoreboard" 的新测试套件。provider
、program
和 wallet
,这是与我们的程序交互所需的上下文。PublicKey.findProgramAddressSync
方法创建一个 dataAccount
和 bump
种子(这需要我们传入我们在程序中定义的相同种子:单词 "seed" 和用户的公钥作为缓冲区)。我们将使用这些来创建用户分数数据的 PDA。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 中构建程序,这应该看起来有些熟悉但略有不同。让我们分解一下:
program.methods.new
命令初始化一个新的 PDA。此命令接受我们在构造函数中定义的参数(我们钱包的缓冲区、bump 和新 PDA 的 publicKey)并创建一个新的 PDA。请注意,对于 Solana,我们必须将我们将使用的账户传入交易上下文中——Anchor 允许我们使用 accounts
方法做到这一点。我们传入我们之前创建的 dataAccount
。rpc
方法发送并确认交易。program.methods.getCurrentScore
命令获取用户的当前分数。我们传入我们之前创建的 dataAccount
。如果你想获取其他用户的分数,你将传入他们的 PDA 的地址。expect
方法断言用户的当前分数为 0。请注意,我们使用 toNumber
方法将分数转换为数字。这是因为分数作为 BN
(大数字)对象返回。你可以通过运行以下命令运行此测试:
anchor test
你应该在终端中看到以下输出:
Scoreboard
✔ Test 1 - Is initialized! (413ms)
干得好!
让我们添加一个测试以确保我们可以向用户的
- 原文链接: quicknode.com/guides/sol...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!