智能合约语法使用的是 Typescript
语言的子集:
- 所有变量都有具体的类型,不使用
null
、any
、never
、object
、unknown
等类型 - 不支持
Symbol
- 不支持交叉类型(如
string & number
)和联合类型(如string | null
) - 不支持生成器和异步语法(不使用
Promise
、async/await
) - 不使用强制类型转换(不使用
<string>name
及name as string
) - 只能使用引擎内置的类型、对象、函数
- 变量、函数等必须有明确的类型或可推断出明确的类型,不可以是
any
- 简单类型:指
number
、string
、boolean
、bigint
之一 - 状态容器:指
Mapping<T>
或Vecotr<T>
- 常量声明
- 数据接口类型声明
- 自定义状态类型声明
- 合约类定义
一个合约文件中可以有多个常量声明,使用const
关键字声明,需要注意的是:常量只能使用四种简单类型(string
、number
、bigint
、boolean
),其他类型包括object
,any
等都不支持,例:
const DEFAULT_NAME = 'name'
const DEFAULT_INTEVEL = 3 * 1000
一个合约文件中可以有多个数据接口类型声明,数据接口类型用于公开方法的参数及返回值,使用interface
关键字声明,限制如下:
- 成员只能使用简单类型、
Array
、数据接口类型,成员可以是可选的(使用?
语法声明) - 不支持联合类型(如
string & number
和string | number
) - 如果成员是
Array
,必须指定泛型参数。泛型类型可以是简单类型、Array
、数据接口类型,泛型类型参数如果本身不是泛型推荐使用简写形式(如:names: string[]
) - 数据接口类型的嵌套深度不能超过 3
- 数据接口类型不可以使用泛型定义(不支持
inteface Data<T> {...}
) - 不支持只读成员(不支持
readonly
) - 不支持索引访问器
示例:
interface AddressInfo {
province: string
city: string
street: string
}
interface PersonInfo {
name: string
age?: number
sex: boolean
address: Address
}
interface PeopleResultInfo {
count: number
pepole: PersonInfo[]
}
一个合约文件中可以有多个状态类型声明,状态类型用于合约状态,类似于传统的POJO
使用class
关键字声明,限制如下:
- 成员只能属性定义或构造函数,属性可以使用简单类型、状态容器、状态类型
- 状态容器指
Mapping<T>
(类似于Map<stirng,T>
)或Vecotr<T>
(类似于Array<T>
),状态容器中的数据会自动持久化
- 状态容器指
- 成员可以是可选的(使用
?
语法定义),可以初始化默认值。除可选成员外的所有成员属性必须通过默认值或构造器初始化 - 只支持实例成员(不支持
static
)且可见性为公开(public
可省略),不支持private
,protected
- 如果成员是状态容器,必须指定泛型参数。泛型类型可以是简单类型、状态容器和状态类型
- 状态类型的嵌套深度不能超过 3
- 状态类不可以使用泛型定义(不支持
class StateData<T> {...}
) - 不能是抽象类(不支持
abstract
) - 不支持实现接口(不支持
implements
语法)和继承(不支持extends
语法) - 不支持索引访问器
- 不支持只读成员(不支持
readonly
) - 不支持
getter
和setter
- 所有非可选成员必须初始化,可以在声明时初始化或在构造函数中初始化
- 可以声明一个公开的构造函数,状态类构造函数不能产生异常 由于状态数据需要从数据库中加载,需要通过无参构造函数初始化(所有的参数都为
undefined
,随后再初始化各个成员属性)。引擎在调用构造函数时不能产生异常,否则会导致合约加载失败
示例:
class PayState {
payTimes: number
amount: bigint
constructor() {
this.payTimes = 0
this.amount = BigInt(0)
}
}
class PayStateDefault {
payTimes = 0
amount = BigInt(0)
}
class PayStateOptional {
payTimes = 0
amount?: bigint
}
一个合约文件中必须有且仅有一个合约类定义,使用class
关键字定义,合约类必须是AschContract
的子类。合约类只允许合约状态和方法两类成员,基本要求如下:
- 使用
export
关键字修饰 - 必须从
AschContract
直接继承,不支持多重继承 - 不能是泛型类(不能有泛型参数)
- 不能是抽象类(不支持
abstract
) - 不支持实现接口(不支持
implements
语法) - 不支持索引访问器
- 不支持
getter
和setter
- 只支持实例成员,不支持静态成员(不支持
static
)
下面来逐个介绍合约中两类成员的具体规范。
合约状态是可以自动进行持久化的合约成员属性。开发者只需要给合约的成员属性赋值,引擎会自动把这些状态持久化到区块链中,对于合约状态来说:
- 类型必须是简单类型、状态类型、状态容器之一,
- 如果成员是状态容器,必须指定泛型参数。泛型类型可以是简单类型、状态容器和状态类型
- 状态类型的嵌套深度不能超过 3
- 所有合约状态成员必须初始化,可以在声明时初始化或在构造函数中初始化
- 状态不可以是可选的(不可以是
undefined
) - 可见性为公开的状态可以通过HTTP接口查询其状态值(见本文后续介绍),非公开状态不可直接查询(可通过查询方法实现查询)
合约类中的方法都必须是成员方法(不支持static
),不支持异步语法(Promise
、async/await
)和生成器语法(generator
)。可分为以下几类
- 构造器
- 可调用方法(可见性为公开的普通方法)
- 资产接收方法(使用
payable
注解) - 查询方法(使用
constant
注解) - 内部方法(可见性为非公开的普通方法
private
或protected
)
一个合约只能有一个构造器,是合约类的初始化方法,名称必须为constructor
,仅在合约注册时执行一次,具体要求如下:
- 可见性必须是公开
- 签名必须是
constructor() {...}
,没有参数也没有返回值 - 可以访问
this.context
- 调用构造器不应产生异常,否则合约无法注册成功
- 不可以访问
this.transfer
,否则会产生异常导致合约无法注册(因为合约注册时,合约账户没有任何资产)
一个合约可以有多个可调用方法,是合约类中可见性为公开的,且没有注解修饰的成员方法,具体要求如下:
- 可见性必须是公开,否则外部不可访问
- 每个参数必须声明明确的类型,参数类型必须是简单类型、
Array
、数据接口类型之一 - 如果成员是
Array
,必须指定泛型参数。泛型类型可以是简单类型、Array
、数据接口类型,泛型类型参数如果本身不是泛型推荐使用简写形式(如:names: string[]
) - 不支持可选参数、不支持参数默认值,也不支持展开参数(
...args: string[]
) - 返回值类型同参数类型要求相同,必须明确声明返回值类型,否则返回值无法从外部获取
- 可以访问
this.context
和this.transfer
(如合约账户余额不足,则会失败)
一个合约可以多个资产接收方法,资产接收方法是使用payable
注解的公开方法,用于接收调用转入智能合约的资产,要求如下:
- 可见性必须是公开
- 必须有两个参数分别为金额与资产名称,一般采用 amount 和 currency 命名
- amount 类型为
bigint
- currency 类型必须为
string
- 可以不声明返回类型,如果声明了返回类型必须
void
- 可以访问
this.context
和this.transfer
(如合约账户余额不足,则会失败) payable
有一个可选参数,类型为{ isDefault?: boolean }
,用于表示是否是默认的资产接受方法(使用@payable({ isDefault: true })
注解)。一个合约中最多只能有一个默认资产接受方法
一个合约可以有多个查询方法,资产接收方法是使用constant
注解的公开方法,用于实现状态查询等只读状态的计算逻辑,具体要求:
- 可见性必须是公开
- 必须有返回类型,且必须是简单类型、
Array
、数据接口类型之一 - 不可访问
this.context
和this.transfer
,否则会失败 - 只能只读访问状态成员,不能修改状态。否则会失败
一个合约可以有多个内部方法,可见性为保护(protected
)或私有(private
,推荐),具体要求:
- 可见性必须是保护或私有
- 不可使用
constant
、payable
注解
智能合约语言是Typescript语言的子集,除上节描述的结构约定外,其他主要限制如下:
- 不可以使用引入第三方库
- 不支持
Symbol
- 不使用
null
、any
、never
、object
、unknown
等类型,undefined
可以使用 - 不使用交叉类型(如
string & number
)和联合类型(如string | null
) - 不支持生成器和异步语法(不使用
Promise
、async/await
) - 不使用强制类型转换(不使用
<string>name
及name as string
) - 一个合约文件只能有一个合约类,这个类必须从
AschContract
继承而来 - 不可以定义全局函数、静态函数
- 智能合约中只能使用合约引擎提供的内置类型、方法和对象,未提供的原Node.js内置的对象、函数或类型是不可用的(如
Function
、Date
都是不可用的) - 私有或保护方法的参数和返回值的定义比较灵活,但请谨慎使用。尽可能避免不确定性
- 合约中不允许使用
try...catch
语法,也不允许使用throw
语句。任何时候抛出异常(如使用assert
语句)即导致中止合约 - 可调用方法和查询方法参数和返回值的额外要求
- 由于合约调用时所有参数会被序列化为
JSON
传递,故只支持可序化的类型(可参考数据接口类的定义)基于效率考虑,全部参数或返回值序列化后的JSON
字符串长度应控制在32K
以内(length <= 32,767
) - 查询方法必须声明返回类型,对于可调用方法,如果未声明返回值类型,返回值将被丢弃(不作为调用结果返回)
- 由于合约调用时所有参数会被序列化为
- 状态类型和数据接口类嵌套深度不超过3
由于状态容器类型的值可以是状态容器类型或合约状态类型,而状态类型中也可以有状态类型或状态容器(数据接口类似)。基于代码可读性以及状态管理的性能考虑。嵌套的深度不应超过3,如
Mapping<bigint>
深度称为 1,Vector<Mapping<number>>
深度为2;简单自定义类型本身深度为1,包含一个深度为1的容器类型或自定义状态类型深度为2;以此类推 - 注意,与以太坊的solidity不同的是,在solidity中,给存储状态赋值会导致自动的复制。而在ASCH智能合约中,状态容器或自定义状态中使用的是对象的引用。这样的好处是性能更好、编程更灵活、更符合主流语言的习惯,但也会带来一个问题:当两个状态容器中保存相同的对象引用时,可能会导致误操作。合约引擎会自动检查这种情况的存在,当尝试把一个已经属于合约状态一部分的对象赋值给合约状态时,会抛出异常。