Cairo 编译器的第 2 版对 Starknet 语法进行了更改,使代码更加明确和安全。智能合约公共接口是使用特征定义的,并且对存储的访问是通过 ContractState 特征完成的。私有方法必须使用与公共接口不同的实现来定义。事件现在定义为枚举,其中每个变体都是同名的结构。
免责声明:此处使用的术语指的是 Cairo 编译器的不同版本,其语法是临时的,因为 Starknet 社区仍在讨论哪些是最好用的术语。一旦确定,本文将进行相应更新。
就在上周,Cairo 编译器的新的主要版本 2.0.0-rc0 在 Github 上发布。新的编译器对 Starknet 插件进行了重大改进,使我们的代码更安全、更明确、更可重复使用。请注意,Starknet 测试网或主网尚不支持这个新版本的编译器,因为它仍在集成环境中进行。
本文的目标是向您展示如何将为 Cairo 编译器版本 1.x 创建的 Starknet 智能合约重写为与编译器版本 2.x 兼容的智能合约。我们的起点是上一篇文章中创建的 Ownable
智能合约,它与 Cario 编译器版本 1.x 兼容。
#[contract]
mod Ownable {
use starknet::ContractAddress;
use starknet::get_caller_address;
#[event]
fn OwnershipTransferred(previous_owner: ContractAddress, new_owner: ContractAddress) {}
struct Storage {
owner: ContractAddress,
}
#[constructor]
fn constructor() {
let deployer = get_caller_address();
owner::write(deployer);
}
#[view]
fn get_owner() -> ContractAddress {
owner::read()
}
#[external]
fn transfer_ownership(new_owner: ContractAddress) {
only_owner();
let previous_owner = owner::read();
owner::write(new_owner);
OwnershipTransferred(previous_owner, new_owner);
}
fn only_owner() {
let caller = get_caller_address();
assert(caller == owner::read(), 'Caller is not the owner');
}
}
由于 Protostar 尚不支持编译器 v2,因此本文将依赖支持它的 Scarb 预发行版本(版本 0.5.0-alpha.1)。要安装该特定版本的 Scarb,您可以使用以下命令。
$ curl --proto '=https' --tlsv1.2 -sSf https://docs.swmansion.com/scarb/install.sh | bash -s -- -v 0.5.0-alpha.1
安装完成后,验证您是否获得了正确的版本。
$ scarb --version
>>>
scarb 0.5.0-alpha.1 (546dad33d 2023-06-19)
cairo: 2.0.0-rc3 (https://crates.io/crates/cairo-lang-compiler/2.0.0-rc3)
现在可以创建一个 Scarb 项目。
$ scarb new cairo1_v2
$ cd cairo1_v2
您应该得到如下所示的文件夹结构。
$ tree .
>>>
.
├── Scarb.toml
└── src
└── lib.cairo
为了让 Scarb 编译 Starknet 智能合约,需要启用 Starknet 插件作为依赖项。
// Scarb.toml
...
[dependencies]
starknet = "2.0.0-rc3"
设置完成后,我们可以前往 src/lib.cairo 开始编写智能合约。
在 Cairo 编译器的版本 2 中,智能合约仍然由带有 contract 属性注释的模块定义,只是这次该属性以定义它的插件的名称命名,在本例中为 starknet。
#[starknet::contract]
mod Ownable {}
内部存储仍然定义为一个必须称为 Storage
的结构,只是这次必须使用一个存储属性来注释它。
#[starknet::contract]
mod Ownable {
use super::ContractAddress;
#[storage]
struct Storage {
owner: ContractAddress,
}
}
为了定义构造函数,我们使用构造函数属性来注释函数,就像在 v1 中所做的那样,优点是现在函数可以具有任何名称,不需要像 v1 中那样被称为“构造函数”。尽管这不是必需的,但出于习惯,我仍然会将该函数称为“构造函数”,但您可以以不同的方式调用它。
另一个重要的变化是,现在构造函数会自动传递对 ContractState 的引用,该引用充当存储变量的中介,在本例中为“所有者”。
#[starknet::contract]
mod Ownable {
use super::ContractAddress;
#[storage]
struct Storage {
owner: ContractAddress,
}
#[constructor]
fn constructor(ref self: ContractState) {
let deployer = get_caller_address();
self.owner.write(deployer);
}
}
请注意,写入和读取存储的语法自 v1 以来已发生变化。之前我们执行owner::write()
,而现在执行self.owner.write()
。这同样适用于从存储中读取。
顺便说一下,ContractState 这个类型不需要手动进入作用域,它已包含在前奏中。
与 Cairo 编译器版本 1 的一个重要区别是,现在我们需要使用带有 starknet::interface
属性注释的特征来明确定义智能合约的公共接口。
use starknet::ContractAddress;
#[starknet::interface]
trait OwnableTrait<T> {
fn transfer_ownership(ref self: T, new_owner: ContractAddress);
fn get_owner(self: @T) -> ContractAddress;
}
#[starknet::contract]
mod Ownable {
...
}
如果您还记得 v1 中的原始代码,我们的智能合约有两个「公共」方法(get_owner 和 transfer_ownership)和一个「私有」方法(only_owner)。这一特征仅处理公共方法,而不依赖于「外部」或「视图」属性来表示哪个方法可以修改合约的状态,哪个方法不允许。相反,现在通过参数 self 的类型来明确这一点。
如果一个方法需要引用 ContractStorage(一旦实现,通用 T 就是这样),该方法就能够修改智能合约的内部状态。这就是我们过去所说的“外部”方法。另一方面,如果一个方法需要 ContractStorage 的快照,那么它只能读取它,而不能修改。这就是我们过去所说的“视图”方法。
现在,我们可以使用关键字 impl 为刚刚定义的特征创建一个实现。请记住,Cairo 与 Rust 的不同之处在于,实现是具备名称的。
use starknet::ContractAddress;
#[starknet::interface]
trait OwnableTrait<T> {
fn transfer_ownership(ref self: T, new_owner: ContractAddress);
fn get_owner(self: @T) -> ContractAddress;
}
#[starknet::contract]
mod Ownable {
...
#[external(v0)]
impl OwnableImpl of super::OwnableTrait<ContractState> {
fn transfer_ownership(ref self: ContractState, new_owner: ContractAddress) {
let prev_owner = self.owner.read();
self.owner.write(new_owner);
}
fn get_owner(self: @ContractState) -> ContractAddress {
self.owner.read()
}
}
}
我们在定义智能合约的模块内为我们的特征创建了一个实现,将类型 ContractState 作为通用类型 T 传递,这样就可以像构造函数那样访问存储。
我们的实现用属性 external(v0) 进行注释。属性中的版本 0 意味着选择器仅从方法名称派生,就像过去的情况一样。缺点是,如果您为您的智能合约定义了另一个不同特征的实现,并且两个特征碰巧对它其中一个方法使用相同的名称,则编译器会因为选择器的重复而抛出错误。
该属性的未来版本可能会添加一种新的方法来计算选择器,以防止冲突,但目前还不能使用。目前,我们只能使用外部属性的版本 0。
我们还需要为智能合约定义另一种方法,only_owner。此方法检查调用它的人是否应该是智能合约的所有者。
因为这是一个不允许从外部调用的私有方法,所以不能将其定义为 OwnableTrait(智能合约的公共接口)的一部分。相反,我们将使用 generate_trait 属性创建自动生成特征的新实现。
...
#[starknet::contract]
mod Ownable {
...
#[generate_trait]
impl PrivateMethods of PrivateMethodsTrait {
fn only_owner(self: @ContractState) {
let caller = get_caller_address();
assert(caller == self.owner.read(), 'Caller is not the owner');
}
}
}
现在可以通过在需要的地方调用 self.only_owner() 来使用 only_owner 方法。
#[starknet::contract]
mod Ownable {
...
#[external(v0)]
impl OwnableImpl of super::OwnableTrait<ContractState> {
fn transfer_ownership(ref self: ContractState, new_owner: ContractAddress) {
self.only_owner();
...
}
...
}
#[generate_trait]
impl PrivateMethods of PrivateMethodsTrait {
fn only_owner(self: @ContractState) {
...
}
}
}
在 Cairo v1 中,事件只是一个没有主体的函数,并用事件(event)属性进行注释,而在 v2 版本中,事件是一个用相同属性注释的枚举(enum),但现在使用派生(derive) 实现了一些附加特征。
...
#[starknet::contract]
mod Ownable {
...
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
OwnershipTransferred: OwnershipTransferred,
}
#[derive(Drop, starknet::Event)]
struct OwnershipTransferred {
#[key]
prev_owner: ContractAddress,
#[key]
new_owner: ContractAddress,
}
}
事件枚举的每个变体都必须是同名的结构体。在该结构中,使用可选的 key 属性定义想要发出的所有值,来通知系统我们希望 Starknet 索引哪些值,以便索引器更快地搜索和检索。在本例中,我们希望对两个值(prev_owner 和 new_owner)建立索引。
ContractState 特征定义了一个发出方法,可以用来发出事件。
...
#[starknet::contract]
mod Ownable {
...
#[external(v0)]
impl OwnableImpl of super::OwnableTrait<ContractState> {
fn transfer_ownership(ref self: ContractState, new_owner: ContractAddress) {
...
self.emit(Event::OwnershipTransferred(OwnershipTransferred {
prev_owner: prev_owner,
new_owner: new_owner,
}));
}
...
}
...
}
通过这个最终功能,我们已经完成了 Ownable 智能合约从 v1 到 v2 的迁移。完整代码如下所示。
use starknet::ContractAddress;
#[starknet::interface]
trait OwnableTrait<T> {
fn transfer_ownership(ref self: T, new_owner: ContractAddress);
fn get_owner(self: @T) -> ContractAddress;
}
#[starknet::contract]
mod Ownable {
use super::ContractAddress;
use starknet::get_caller_address;
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
OwnershipTransferred: OwnershipTransferred,
}
#[derive(Drop, starknet::Event)]
struct OwnershipTransferred {
#[key]
prev_owner: ContractAddress,
#[key]
new_owner: ContractAddress,
}
#[storage]
struct Storage {
owner: ContractAddress,
}
#[constructor]
fn constructor(ref self: ContractState) {
let deployer = get_caller_address();
self.owner.write(deployer);
}
#[external(v0)]
impl OwnableImpl of super::OwnableTrait<ContractState> {
fn transfer_ownership(ref self: ContractState, new_owner: ContractAddress) {
self.only_owner();
let prev_owner = self.owner.read();
self.owner.write(new_owner);
self.emit(Event::OwnershipTransferred(OwnershipTransferred {
prev_owner: prev_owner,
new_owner: new_owner,
}));
}
fn get_owner(self: @ContractState) -> ContractAddress {
self.owner.read()
}
}
#[generate_trait]
impl PrivateMethods of PrivateMethodsTrait {
fn only_owner(self: @ContractState) {
let caller = get_caller_address();
assert(caller == self.owner.read(), 'Caller is not the owner');
}
}
}
您也可以在 Github 上找到这段代码。
Cairo 编译器第 2 版为 Starknet 带来了新的语法,使智能合约代码看起来与 Cairo 本身更加一致,并且在扩展上更类似于 Rust。即使牺牲了更多繁琐的代码,安全方面的优势也值得权衡。
在本文中,我们没有触及关于新语法的所有内容,特别是如何与其他智能合约交互,但您可以阅读编译器的变更日志、阅读论坛上的这篇文章或观看 StarkWare 的 YouTube 频道上的视频来了解更多信息。
这个新版本的编译器将在几周内提供给 Starknet 的测试网,在几周后提供给主网,所以暂时不要尝试部署此代码,它还不能运行。
Cairo 一直在变得更好。