一个简单的合约

本章将通过一个基本合约的例子向您介绍Starknet合约的基础知识。您将学习如何编写一个在区块链上存储单个数字的简单合约。

一个简单Starknet合约剖析

让我们通过下面的合约来了解Starknet合约的基本内容。可能一下子很难理解,但是我们将一步一步地进行讲解:

#![allow(unused)]
fn main() {
#[starknet::interface]
trait ISimpleStorage<TContractState> {
    fn set(ref self: TContractState, x: u128);
    fn get(self: @TContractState) -> u128;
}

#[starknet::contract]
mod SimpleStorage {
    use starknet::get_caller_address;
    use starknet::ContractAddress;

    #[storage]
    struct Storage {
        stored_data: u128
    }

    #[external(v0)]
    impl SimpleStorage of super::ISimpleStorage<ContractState> {
        fn set(ref self: ContractState, x: u128) {
            self.stored_data.write(x);
        }
        fn get(self: @ContractState) -> u128 {
            self.stored_data.read()
        }
    }
}
}

示例99-1:一个简单的存储合约

注意:Starknet合约是在模块(modules)中被定义的。

这是什么合约?

在这个例子中,Storage 结构声明了一个名为 stored_data 的存储变量,类型为u128(128位无符号整数)。 你可以将它想象成数据库中的一个单独槽位,通过调用管理数据库的代码的函数来查询和修改它。 该合约定义并公开了 setget 函数,用于修改或检索该变量的值。

接口:合约的蓝图

#[starknet::interface]
trait ISimpleStorage<TContractState> {
    fn set(ref self: TContractState, x: u128);
    fn get(self: @TContractState) -> u128;
}

合约的接口代表了该合约向外界公开的函数。在这里,接口公开了两个函数:setget 。通过利用Cairo的traits & impls 机制,我们可以确保合约的实际实现与其接口匹配。事实上,如果你的合约与声明的接口不符合,将会得到编译错误。

    #[external(v0)]
    impl SimpleStorage of super::ISimpleStorage<ContractState> {
        fn set(ref self: ContractState) {}
        fn get(self: @ContractState) -> u128 {
            self.stored_data.read()
        }
    }

示例 99-1-bis: 合约接口的错误实现。无法编译。

在接口中,请注意self参数的通用类型TContractState,它通过引用传递给set函数。参数 self 代表合约状态。看到 self 参数传递给 set 告诉我们这个函数可能会访问合约的状态,因为它使我们能够访问合约的存储空间。ref 修饰符意味着 self 可以被修改,这意味着合约的存储变量可以在 set 函数中被修改。

另一方面,get获取TContractState的_snapshot_,这立即告诉我们它不会修改状态(事实上,如果我们试图在get函数中修改存储变量,编译器会报错)。

在实现里定义public函数

在我们进一步探讨之前,让我们先定义一些术语。

  • 在Starknet中,public function (公共函数)是一个对外公开的函数。在上面的例子中,setget是公共函数。公有函数可以被任何人调用,可以从合约外部调用,也可以从合约内部调用。在上面的示例中,setget是公共函数。

  • 我们所说的 external 函数是通过交易唤起的公共函数,它可以改变合约的状态。set就是一个外部函数。

  • 一个 view 函数是一个公共函数,它可以从合约外部调用,但不能改变合约的状态。get是一个视图函数。

    #[external(v0)]
    impl SimpleStorage of super::ISimpleStorage<ContractState> {
        fn set(ref self: ContractState, x: u128) {
            self.stored_data.write(x);
        }
        fn get(self: @ContractState) -> u128 {
            self.stored_data.read()
        }
    }

由于合约接口被定义为ISimpleStoragetrait,为了匹配接口,合约的外部函数必须在这个trait的实现中定义--这使我们能够确保合约的实现与其接口相匹配。

然而,仅仅在实现中定义函数是不够的。实现块必须标注上#[external(v0)]属性。如果忘记添加该属性,您的函数将无法从外部调用。所有在标记为 #[external(v0)]的代码块中定义的函数都是 public functions

当编写接口的实现时,在trait中对应于self参数的泛型参数必须是ContractStateContractState类型由编译器生成,它提供了对定义在 "Storage结构中的存储变量的访问。 此外,ContractState还提供了emit事件的能力。不要对ContractState 这个名字感到奇怪,因为它是合约状态的表示,也就是我们在合约接口trait中认为的 self

修改合约状态

正如你所注意到的,所有需要访问合约状态的函数都被定义在一个有TContractState泛型参数的trait的实现下,并接受一个self:ContractState参数。 这允许我们显式地将 self:ContractState参数给函数,允许访问合约的存储变量。 要访问当前合约的存储变量,可以在存储变量名后添加 self 前缀,这样就可以使用 readwrite 方法读取或写入存储变量的值。

        fn set(ref self: ContractState, x: u128) {
            self.stored_data.write(x);
        }

使用 selfwrite 方法修改存储变量的值

注意:如果合约状态是作为snapshot而不是ref传递的,尝试修改它将导致编译错误。

除了允许任何人存储世界上任何人都可以访问的单个号码外,该合约没做其他事。任何人都可以用不同的值再次调用set覆盖您的号码,但号码仍然存储在区块链的历史中。稍后,您将看到如何施加访问限制,以便只有您可以更改号码。

Last change: 2023-11-21, commit: 2fbb62a