跳转到内容

合约

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

自引用

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

contract Example {
// persistent state variables
foo: Int;
init() {
self.foo = 42; // <- referencing variable foo through self.
}
}

结构

每份合约可包括:

继承trait, with

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

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

trait InheritMe {}
trait InheritMeToo {}
// A contract inheriting a single trait
contract Single with InheritMe {}
// A contract inheriting multiple traits
contract Plural with
InheritMe,
InheritMeToo, // trailing comma is allowed
{}

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

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

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

支持的接口,@interface(…)

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

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

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

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

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

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

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

要启用 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 仍然可以用于 Blueprint 项目。 除此之外,Blueprint 项目中仍可使用 tact.config.json。 在这种情况下,除非在 wrappers/ 中进行修改,否则 tact.config.json中指定的值将作为默认值。

:::

持久状态变量

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

contract Example {
// persistent state variables
val: Int; // Int
val32: Int as uint32; // Int serialized to an 32-bit unsigned
mapVal: map<Int, Int>; // Int keys to Int values
optVal: Int?; // Int or null
}

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

合约常量

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

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

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

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

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

// global constants are calculated in compile-time and cannot change
const GlobalConst1: Int = 1000 + ton("42") + pow(10, 9);
contract Example {
// contract constants are also calculated in compile-time and cannot change
const ContractConst1: Int = 2000 + ton("43") + pow(10, 9);
// contract constants can be an easy alternative to enums
const StateUnpaid: Int = 0;
const StatePaid: Int = 1;
const StateDelivered: Int = 2;
const StateDisputed: Int = 3;
get fun sum(): Int {
// access constants from anywhere
return GlobalConst1 + self.ContractConst1 + self.StatePaid;
}
}

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

构造函数 init()

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

如果合约有任何持久状态变量没有指定默认值,则必须在此函数中初始化它们。

contract Example {
// persistent state variables
var1: Int = 0; // initialized with default value 0
var2: Int; // must be initialized in the init() function
// constructor function
init() {
self.var2 = 42;
}
}

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

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

contract IamEmptyAndIKnowIt {}

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

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

获取器(getter)功能

获取(getter)函数 无法从其他合约访问,仅导出到链下世界

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

contract HelloWorld {
foo: Int;
init() {
self.foo = 0;
}
// getter function with return type Int
get fun foo(): Int {
return self.foo; // can't change self.foo here
}
}

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

接收器(Receiver)功能

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

  • receive(),用于接收内部消息(来自其他合约)。
  • bounced(),当该合约发出的消息被退回时会调用。
  • external(),没有发送者,世界上任何人都可以发送。
message CanBounce {
counter: Int;
}
contract HelloWorld {
counter: Int;
init() {
self.counter = 0;
}
get fun counter(): Int {
return self.counter;
}
// internal message receiver, which responds to a string message "increment"
receive("increment") {
self.counter += 1;
// sending the message back to the sender
send(SendParameters{
to: sender(),
value: 0,
mode: SendRemainingValue | SendIgnoreErrors,
body: CanBounce{counter: self.counter}.toCell(),
});
}
// bounced message receiver, which is called when the message bounces back to this contract
bounced(src: bounced<MsBounced>) {
self.counter = 0; // reset the counter in case message bounced
}
// external message receiver, which responds to off-chain message "hello, it's me"
external("hello, it's me") {
// can't be replied to as there's no sender!
self.counter = 0;
}
}

用下划线_命名接收函数的参数时,其值将被视为未使用的值并被丢弃。 这在您不需要检查收到的消息且只希望其传达特定操作码时非常有用:

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

内部函数

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

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

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

contract Functions {
val: Int = 0;
// this contract method can only be called from within this contract and access its variables
fun onlyZeros() {
require(self.val == 0, "Only zeros are permitted!");
}
// receiver function, which calls the internal function onlyZeros
receive("only zeros") {
self.onlyZeros();
}
}