Starknet 改进语法全解读

原文The Improved Starknet Syntax
翻译及校对
「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 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_ownertransfer_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 一直在变得更好。

资源

Subscribe to Starknet 中文
Receive the latest updates directly to your inbox.
Mint this entry as an NFT to add it to your collection.
Verification
This entry has been permanently stored onchain and signed by its creator.