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_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 一直在变得更好。

来源:Starknet 中文社区,本站:/zixun/2023-07-12/416.html

生成海报
收藏
考拉

考拉

还没有填写个人资料!会员中心-修改资料-个人介绍填写!

相关推荐

StarkNet 如何彻底改变加密签名

淘汰复杂密钥,采用人脸识别。关于帐户抽象的案例研究

一旦可以使用人脸识别等熟悉的方式来签署加密交易,我们能预见自托管的蓬勃发展。众所周知,自行运行管理钱包是最初的加密 ...

0 条评论

微信扫一扫,分享到朋友圈

QQ QQ

客服 工作时间:周一至周六 9:30-22:00 QQ:670088886(点击咨询) 直奔主题,别问在不在,谢谢!

热线 热线

13888888888

微信 微信
微信
公众号 公众号
公众号