跳转到内容

合同

Tact 中的合约类似于流行的面向对象语言中的类,只是它们的实例部署在区块链上,不能像 Structs and Messages 那样被传递。

Self-references

契约和[特质][trait]有一个内置的标识符 self,用于引用它们的字段(持久状态变量常量)和方法(内部函数):

contract Example {
// 持久状态变量
fooInt;
init() {
self.foo = 42; // <- 通过 self 引用变量 foo。
}
}

Structure

每份合同可包括:

Inherited traits, with

契约可以继承[traits][trait]的所有声明和定义,并覆盖它们的某些默认行为。除此之外,每个契约和特质都隐式继承了特殊的BaseTrait trait

要继承[trait][trait],请在合约声明中的关键字with后指定其名称。要同时继承多个特质,请在逗号分隔的列表中指定它们的名称,并在后面加上逗号。

trait InheritMe {}
trait InheritMeToo {}
// 继承单个特质的合约
合约 Single with InheritMe {}
// 继承多个特质的合约
合约 Plural with
InheritMe,
InheritMeToo, // 允许使用逗号结尾
{}

由于[traits][trait]不允许有init()函数,因此继承了声明了任何持久状态变量的trait的合约必须通过提供自己的init()函数来初始化这些变量。

trait Supe { omelander: Bool }
contract Vot with Supe {
init() {
self.omelander = true;
}
}

如果在特质中声明或定义了内部函数和常量,则可将其标记为虚拟或抽象,并在从特质继承的合约中重写。

Supported interfaces, @interface(…)

如果不查看源代码,就很难弄清一个合约是做什么的,有哪些接收器获取器。有时,无法获得或无法访问源代码,剩下的办法就是尝试拆解合同,并以这种方式对其进行反省,这是一种非常混乱且容易出错的方法,其收益会递减,而且没有真正的可重复性。

为了解决这个问题,创建了OTP-001:支持的接口。据此,Tact 合约可以报告支持的接口列表,作为特殊的supported_interfaces getter的返回值。使用任何 TON 区块链浏览器都可以在链外访问该获取器—只需指定”supported_interfaces”作为要执行的方法,即可获得十六进制值列表。

这些十六进制值被截断为受支持接口的原始String值的SHA-256哈希值的前 128 位。该列表中的第一个值必须等于0x5cec3d5d2cae7b1e84ec39d64a851b66\mathrm{0x5cec3d5d2cae7b1e84ec39d64a851b66}十六进制符号),即"org.ton.introspection.v0"的 SHA-256 哈希值的前半部分。如果第一个值是错误的,就必须停止反省合同,因为它不符合支持的接口建议。

要声明支持某个接口,可在 contract 和[trait][trait]声明之前添加一个或多个@interface("…")属性:

@interface("His name is")
@interface("John")
contract SeeNah with Misc {
// ...
}
@interface("name_of_your_org - miscellaneous")
trait Misc {
// ...
}

Tact 有一小套在特定条件下提供的接口:

标准库中的一些[traits][trait]也定义了它们的接口:

要启用 supported_interfaces getter 生成并在 Tact 合约中使用 @interface() 属性,请修改项目根目录下的 tact.config.json 文件(如果该文件不存在,则创建该文件),并 interfacesGetter 属性设置为 true

如果您的项目基于 Blueprint,您可以在合约的编译配置中启用supported_interfaces,这些配置位于名为wrappers/的目录中:

wrappers/YourContractName.compile.ts
import { CompilerConfig } from '@ton/blueprint';
export const compile:CompilerConfig = {
lang: 'tact',
target: 'contracts/your_contract_name.tact',
options:{
interfacesGetter: true, // ← that's the stuff!
}
};

除此之外,蓝图 项目中仍可使用 tact.config.json。 在这种情况下,除非在 wrappers/ 中进行修改,否则 tact.config.json中指定的值将作为默认值。

Persistent state variables

合约可以定义在合约调用之间持续存在的状态变量。 TON 中的合约支付租金 与它们消耗的持久空间成正比,因此鼓励通过序列化进行紧凑表示

contract Example {
// 持久状态变量
val: Int; // Int
val32: Int as uint32; // Int 序列化为 32 位无符号
mapVal: map<Int, Int>; // Int keys to Int values
optVal: Int?; // Int or null
}

状态变量必须有默认值或在 init() 函数中初始化,该函数在部署合约时运行。唯一的例外是 map<K, V> 类型的持久状态变量,因为它们默认初始化为空。

Contract constants

变量 不同,常量不能更改。 它们的值是在编译时计算的,在执行过程中不会改变。

在合约外定义的常量(全局常量)和在合约内定义的常量(合约常量)没有太大区别。 项目中的其他合同可以使用外部定义的常量。

常量初始化必须相对简单,并且只依赖于编译时已知的值。 例如,如果您将两个数字相加,编译器会在编译过程中计算出结果,并将结果放入已编译的代码中。

您可以在 接收器获取器 中读取常量。

合约变量不同,合约常量不会占用持久状态的空间。 它们的值直接存储在合约的代码 单元格中。

// 全局常量在编译时计算,不能更改
const GlobalConst1Int = 1000 + ton("42") + pow(10, 9);
contract Example {
// 合同常量也是在编译时计算的,不能更改
const ContractConst1Int = 2000 + ton("43") + pow(10, 9);
// 合同常量可以轻松替代枚举
const StateUnpaidInt = 0;
const StatePaid: Int = 1;
const StateDeliveredInt = 2;
const StateDisputedInt = 3;
get fun sum():Int {
// access constants from anywhere
return GlobalConst1 + self.ContractConst1 + self.StatePaid;
}
}

有关常量的更多信息,请访问其专门页面:常量

Constructor function init()

在部署合约时,会运行构造函数 init()

如果合约有任何未指定默认值的持久状态变量,则必须在此函数中对其进行初始化。

contract Example {
// 持久状态变量
var1: Int = 0; // 以默认值 0 初始化
var2: Int; // 必须在 init() 函数中初始化
// 构造函数
init() {
self.var2 = 42;
}
}

如果合约没有任何持久状态变量,或所有持久状态变量都指定了默认值,则可以完全省略 init() 函数声明。 这是因为除非明确声明,否则所有合约中都默认存在空的 init() 函数。

下面是一个有效空合同的示例:

合同 IamEmptyAndIKnowIt {}

为方便起见,init() 的参数列表可以使用逗号:

contract TheySeeMeTrailing {
init(
param1: Int,
param2: Int, // trailing comma is allowed
){
// ...
}
}

Getter functions

获取函数 不能从其他合约访问,只能导出到链外世界

此外,获取器不能修改合约的状态变量,只能读取它们的值并在表达式中使用。

contract HelloWorld {
fooInt;
init() {
self.foo = 0;
}
// 返回类型为 Int 的 getter 函数
get fun foo():Int {
return self.foo; // 这里不能更改 self.foo
}
}

请在其专门章节中阅读更多相关信息:获取函数

Receiver functions

Tact 中的接收器函数可以是以下三种之一:

  • receive(),用于接收内部消息(来自其他合约)。
  • bounced(),当该合约发出的消息被退回时会调用。
  • external(),没有发送者,世界上任何人都可以发送。
message CanBounce {
counterInt;
}
contract HelloWorld {
counterInt;
init() {
self.counter = 0;
}
get fun counter():Int {
return self.counter;
}
// 内部消息接收器,响应字符串消息 "increment"
receive("increment") {
self.counter += 1;
// 发送消息回发送者
send(SendParameters{
to: sender(),
value: 0,
modeSendRemainingValue | SendIgnoreErrors,
bodyCanBounce{counter: self.counter}.toCell(),
});
}
// 被弹回的消息接收器,当消息弹回本合约时被调用
bounced(src: bounced<MsBounced>) {
self.counter = 0; // 重置计数器以防消息弹回
}
// 外部消息接收器,用于回复链外消息 "你好,是我"
external("hello, it's me") {
// 无法回复,因为没有发件人!
self.counter = 0;
}
}

用下划线_命名接收函数的参数时,其值将被视为未使用的值并被丢弃。当您不需要检查接收到的信息,而只想让它传达特定的操作码时,这就很有用了:

message(42) UniverseCalls {}
contract Example {
receive(_: UniverseCalls) {
// Got a Message with opcode 42
}
}

Internal functions

这些函数的行为类似于流行的面向对象语言中的私有方法——它们是合约的内部函数,可以通过在其前缀添加特殊的标识符 self来调用。因此,内部函数有时也被称为”契约方法”。

内部函数可以访问合约的持久状态变量常量

它们只能从接收器获取器和其他内部函数中调用,而不能从其他合约或init()中调用。

contract Functions {
val: Int = 0;
// 该契约方法只能在该契约内部调用,并访问其变量
fun onlyZeros() {
require(self.val == 0, "Only zero are permitted!");
}
// 接收函数,调用内部函数 onlyZeros
receive("only zeros") {
self.onlyZeros();
}
}