原文:Improved Starknet Syntax

翻译及校对:「Starknet 中文社区」

Starknet改进语法全解读

概要

Cairo 编译器的第 2 版对 Starknet 语法进行了更改,使代码更加明确和安全。智能合约公共接口是使用特征定义的,并且对存储的访问是通过 ContractState 特征完成的。私有方法必须使用与公共接口不同的实现来定义。事件现在定义为枚举,其中每个变体都是同名的结构。

免责声明:此处使用的术语指的是 Cairo 编译器的不同版本,其语法是临时的,因为 Starknet 社区仍在讨论哪些是最好用的术语。一旦确定,本文将进行相应更新。

The Compiler v2

就在上周,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 | 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()

现在可以创建一个 Scarb 项目。

$ scarb new cairo1_v2$cdcairo1_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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 一直在变得更好。

资源

  • 合约语法——迁移指南
  • Cairo 1:合约语法在不断发展