一个简单的合约
本章将通过一个基本合约的例子向您介绍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() } } } }
注意:Starknet合约是在模块(modules)中被定义的。
这是什么合约?
在这个例子中,Storage
结构声明了一个名为 stored_data
的存储变量,类型为u128
(128位无符号整数)。
你可以将它想象成数据库中的一个单独槽位,通过调用管理数据库的代码的函数来查询和修改它。
该合约定义并公开了 set
和 get
函数,用于修改或检索该变量的值。
接口:合约的蓝图
#[starknet::interface]
trait ISimpleStorage<TContractState> {
fn set(ref self: TContractState, x: u128);
fn get(self: @TContractState) -> u128;
}
合约的接口代表了该合约向外界公开的函数。在这里,接口公开了两个函数:set
和 get
。通过利用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()
}
}
在接口中,请注意self
参数的通用类型TContractState
,它通过引用传递给set
函数。参数 self
代表合约状态。看到 self
参数传递给 set
告诉我们这个函数可能会访问合约的状态,因为它使我们能够访问合约的存储空间。ref
修饰符意味着 self
可以被修改,这意味着合约的存储变量可以在 set
函数中被修改。
另一方面,get
获取TContractState
的_snapshot_,这立即告诉我们它不会修改状态(事实上,如果我们试图在get
函数中修改存储变量,编译器会报错)。
在实现里定义public函数
在我们进一步探讨之前,让我们先定义一些术语。
-
在Starknet中,public function (公共函数)是一个对外公开的函数。在上面的例子中,
set
和get
是公共函数。公有函数可以被任何人调用,可以从合约外部调用,也可以从合约内部调用。在上面的示例中,set
和get
是公共函数。 -
我们所说的 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()
}
}
由于合约接口被定义为ISimpleStorage
trait,为了匹配接口,合约的外部函数必须在这个trait的实现中定义--这使我们能够确保合约的实现与其接口相匹配。
然而,仅仅在实现中定义函数是不够的。实现块必须标注上#[external(v0)]
属性。如果忘记添加该属性,您的函数将无法从外部调用。所有在标记为 #[external(v0)]
的代码块中定义的函数都是 public functions 。
当编写接口的实现时,在trait中对应于self
参数的泛型参数必须是ContractState
。ContractState
类型由编译器生成,它提供了对定义在 "Storage
结构中的存储变量的访问。
此外,ContractState
还提供了emit事件的能力。不要对ContractState
这个名字感到奇怪,因为它是合约状态的表示,也就是我们在合约接口trait中认为的 self
。
修改合约状态
正如你所注意到的,所有需要访问合约状态的函数都被定义在一个有TContractState
泛型参数的trait的实现下,并接受一个self:ContractState
参数。
这允许我们显式地将 self:ContractState
参数给函数,允许访问合约的存储变量。
要访问当前合约的存储变量,可以在存储变量名后添加 self
前缀,这样就可以使用 read
和 write
方法读取或写入存储变量的值。
fn set(ref self: ContractState, x: u128) {
self.stored_data.write(x);
}
注意:如果合约状态是作为snapshot而不是
ref
传递的,尝试修改它将导致编译错误。
除了允许任何人存储世界上任何人都可以访问的单个号码外,该合约没做其他事。任何人都可以用不同的值再次调用set
覆盖您的号码,但号码仍然存储在区块链的历史中。稍后,您将看到如何施加访问限制,以便只有您可以更改号码。