Patract完成Ask! v0.3开发:正式发布 ask 和 ask-cli 的 npm 库
3个月前,Patract 提交了 Kusama 国库的[第#81号]提案,关于 Ask! v0.3 的的实现目标,原理及过程。在那份提案中,我们将在 v0.3 版本中完成以下功能:
增加项目管理工具 ask-cli
性能优化
提供自定义 env 中系统参数的类型
单元测试和文档
我们已经实现的源码在 Ask! (https://github.com/patractlabs/ask)项目仓位中, 示例合约在 examples(https://github.com/patractlabs/ask/tree/master/examples)目录下,部分文档在 docs.patract.io,请在 v0.3-review 分支上 review,完成之后将合并到 main 分支。
Ask! v0.3 在 v0.2 版本的基础上,增加了 ask-cli 命令行工具,用于管理合约开发工作;优化了 Ask! 的性能;并提供了相关的使用文档。
增加项目管理工具 ask-cli
ask-cli 是 Ask! 的命令行管理工具,用来管理合约编译的生命周期。它提供了 init 和 compile 两个功能:
ask-cli init 命令:
init 命令用于初始化 Ask! 合约项目,它从 Dependencies(https://raw.githubusercontent.com/patractlabs/ask-cli/main/depens.json)列表中读取当前 Ask! 项目的最新配置信息,然后更新对应的 npm 包。然后,它将建立起本地文件目录结构:
.
├── build
├── contracts
├── node_modules
└── package.json
其中: contracts 目录下存放合约源码文件,build 目录在执行编译命令之后产生,里面存放生成的 Wasm 和 metadata.json 文件,以及如果是 --debug 模式下生成的其它文件。
ask-cli compile [--release|--debug] contracts/Hello.ts 命令:
compile 命令于编译指定的合约文件, 并将生成的 Wasm 和 metadata.json 文件存放于 build 目录下。
它有两种模式,--release 是默认选项,编译器将使用最高级别的优化和压缩;--debug 是调用模式,除了 Wasm 和 metadata.json 文件外,还会生成编译过程中产生的文件。
关于 ask-cli 的详细使用说明,请参考 QuickStart(https://github.com/patractlabs/ask/blob/v0.3-dev/docs/Quickstart.md)中对应的章节。
性能优化
2.1 合并 @storage 的功能到 @contract 中,减化状态变量定义的流程。
在 v0.3 之前的版本中,合约的状态变量是单独定义在由 @storage 注解的类里面。这种实现方式,不能很好的支持合约继承时,状态变量不便于统一安排存储位置。所以在 v0.3 版本中,我们将状态变量直接定义在合约中,并废除了 @storage 注解,同时引入了 @state 注解,用于标记合约中的成员变量是状态变量,未被 @state 标注的成员变量,则是普通的类成员变量。
为满足条款2.2的要求,当合约使用继承时,所有@state标注的状态变量,将按照它们的声明顺序,从基类到子类的顺序,进行排序,它们排序序号,将作为状态变量存储位置的编号。
编译器首先定位合约入口。
this.program.elementsByName.forEach((element, _) => {
let contractNum = 0;
if (ElementUtil.isTopContractClass(element)) {
contractNum++;
this.contract = new ContractInterpreter(<ClassPrototype>element);
}
});
然后递归父类把要存储的对象入栈。
private resolveBaseClass(classPrototype: ClassPrototype): void {
if (classPrototype.basePrototype) {
let basePrototype = classPrototype.basePrototype;
basePrototype.instanceMembers &&
basePrototype.instanceMembers.forEach((instance, _) => {
if (ElementUtil.isField(instance)) {
let fieldDef = new FieldDef(<FieldPrototype>instance);
if (!fieldDef.decorators.ignore) {
this.storeFields.push(fieldDef);
}
}
});
this.resolveBaseClass(basePrototype);
}
}
为满足条款2.3的要求,@state 中引入了选项 lazy,即: @state({"lazy": false}) 方式。
当 lazy 为 true 时,表示这个状态变量在同一次调用过程中,多次改变它的值时,只有最后一次的改变会同步到链上;lazy的默认值为true。
当 lazy 为 false 时,则表示每一次改变这个状态变量的值,都会立即同步到链上。
它的基本实现原理是:
对于每一个 lazy 为 true 的状态变量, 编译器给它们生成的 setter 方法, 将只更新内存中的值;同时,编译器将为合约生成一个__commit__方法,在这个方法中,这些状态变量的值如果曾经发生过改变,则将它们最新的值同步到链上。
而对于 lazy 为 false 的状态变量, 编译器生成的 setter 方法, 除了更新内存中的值,也会立即同步到链上。编译器会根据存储对象的状态,生成不同的 setter。
以 bool 类型举例, private vbool: bool;当 lazy 是 false 时, 其生成的扩展 set 方法如下;
set vbool(newvalue: bool) {
this._vbool = new Bool(newvalue);
const st = new Storage(new Hash("0x0000000000000000000000000000000000000000000000000000000000000001"));
st.store<Bool>(this._vbool!);
}
当 lazy 为 true 时,其生成的扩展方法如下:
set vbool(v: bool) {
this._vbool = new _lang.Bool(v);
}
__commit_storage__(): void {
if (this._vbool !== null) {
const st = new _lang.Storage(new _lang.Hash([0x0000000000000000000000000000000000000000000000000000000000000001]));
st.store<_lang.Bool>(this._vbool!);
}
}
2.3 在一个 message 调用过程中,多次改变状态变量的值时,减少seal_set_storage的调用次数。见条款 2.1.
2.4 定义 Map 和 Array 在 metadata.json 中导出格式。
Array 的的导出格式分成两部分,type 定义和 store 定义。在 Ask! 里面 Array 是默认变长数组,默认定义其结构为 SequenceDef 如下标记 array 为sequence,同时指定 Array 对象存储对象的类型,以及存储模式是 pack 还是 spread。对于 Array 还可以默认分配一些空间,他生成的 type 为 Arraydef 包含分配空间 capacity 的大小。
export interface Type extends ToMetadata {
typeKind(): TypeKind;
toMetadata(): ITypeDef;
}
export class SequenceDef implements Type {
constructor(public readonly type: number) {}
typeKind(): TypeKind {
return TypeKind.Sequence;
}
toMetadata(): ISequenceDef {
return {
def: {
sequence: {
type: this.type,
},
},
};
}
}
export class ArrayDef implements Type {
constructor(public readonly len: number, public readonly type: number) {}
typeKind(): TypeKind {
return TypeKind.Array;
}
toMetadata(): IArrayDef {
return {
def: {
array: {
len: this.len,
type: this.type,
},
},
};
}
}
SequenceDef 生成结构示例如下:
{
"def": {
"sequence": {
"type": 4
}
}
}
ArrayDef 生成结构示例如下:
{
"def": {
"array": {
"len": 32,
"type": 2
}
}
}
他们存储结构示例如下,
{
"name": "ages",
"layout": {
"struct": {
"fields": [
{
"name": "len",
"layout": {
"key": "0x0000000000000000000000000000000000000000000000000000000000000002",
"ty": 3
}
},
{
"name": "elems",
"layout": {
"offset": "0x0000000000000000000000000000000000000000000000000000000000000002",
"len": 0,
"cellsPerElem": 1,
"layout": {
"key": "0x0000000000000000000000000000000000000000000000000000000000000002",
"ty": 3
},
"storemode": "spread"
}
}
]
}
}
}
其中区别为:ArrayDef 包含 capacity 信息的 len 字段不为 0SequenceDef 的为0。
{
"name": "elems",
"layout": {
"offset": "0x0000000000000000000000000000000000000000000000000000000000000002",
"len": 0,
"cellsPerElem": 1,
"layout": {
"key": "0x0000000000000000000000000000000000000000000000000000000000000002",
"ty": 3
},
"storemode": "spread"
}
对于 map 存储对象解析,需要知道存储的对象 key 和 value 的类型,以及存储模式是 pack 和 spread。还有存储对象入口 key 的值。
export class CompositeDef implements Type {
constructor(public readonly fields: Array<Field>) {}
typeKind(): TypeKind {
return TypeKind.Composite;
}
toMetadata(): ICompositeDef {
return {
def: {
composite: {
fields: this.fields.map((f) => f.toMetadata()),
},
},
};
}
}
其生成结构实例如下:
{
"def": {
"composite": {
"fields": [
{
"name": "key_index",
"type": 2
},
{
"name": "value",
"type": 3
}
]
}
}
},
{
"def": {
"primitive": "u8"
}
},
{
"def": {
"primitive": "str"
}
}
其存储结构如下:
{
"name": "allowances",
"layout": {
"struct": {
"fields": [
{
"name": "key",
"layout": {
"offset": "0x0000000000000000000000000000000000000000000000000000000000000002",
"strategy": {
"hasher": "Blake2x256",
"prefix": "0x0000000000000000000000000000000000000000000000000000000000000002",
"postfix": ""
},
"layout": {
"key": "0x0000000000000000000000000000000000000000000000000000000000000002",
"ty": 3
},
"storemode": "spread"
}
},
{
"name": "values",
"layout": {
"offset": "0x0000000000000000000000000000000000000000000000000000000000000002",
"strategy": {
"hasher": "Blake2x256",
"prefix": "0x0000000000000000000000000000000000000000000000000000000000000002",
"postfix": ""
},
"layout": {
"key": "0x0000000000000000000000000000000000000000000000000000000000000002",
"ty": 6
},
"storemode": "spread"
}
}
]
}
}
}
2.5 使用 JSON 的方式代替()的注解方式
在 v0.3 之前的版本中,注解的选项,是以 @message(selector = '0x00001111') 的方式提供的,这种实现方式不是很直观,而且不符合编码习惯,所以在 v0.3 中,我们把它改成以下方式:
@message({"selector": "0x00001111"})
这种方式的可读性比较好,同时,也便于编译器解析处理。
先获取注解的参数部分比如“{"selector": "0x00001111"}”解析成 json 对象
export class DecoratorNodeDef {
jsonObj: any;
constructor(public decorator: DecoratorNode) {
this.jsonObj = this.parseToJson(decorator);
}
}
针对特定注解创建特定的注解类,并做特定的检查。
export class MessageDecoratorNodeDef extends DecoratorNodeDef {
constructor(decorator: DecoratorNode, public payable = false,
public mutates = true, public selector = "") {
super(decorator);
this.payable = this.getIfAbsent("payable", false, "boolean");
this.mutates = this.getIfAbsent('mutates', true, "boolean");
if (this.hasProperty('selector')) {
this.selector = this.getProperty('selector');
DecoratorUtil.checkSelector(decorator, this.selector);
}
if (this.payable && !this.mutates) {
throw new Error(`Decorator: ${decorator.name.range.toString()} arguments mutates and payable can only exist one. Trace: ${RangeUtil.location(decorator.range)} `);
}
}
}
2.6 增强 Event 语法
在 v0.2 中, 我们引入了 @event 注解,用来发出一个事件。但是在这个版本中,Event 不能被继承,而且定义 Event 就默认 emit 的实现方法,也不是很直观。所以在 v0.3 版本中,我们对Event做了以下增强和优化:
实现 @event 类时, 需要明确是从__lang.Event 继承, 或者继承于另一个 Event。
需要明确调用 emit() 方法来发出事件。
新的 Event 使用方法示例如下:
@event
class EventA extends __lang.Event {
@topic topicA: u8;
name: string;
constructor(t: u8, n: string) {
super();
this.topicA = t;
this.name = n;
}
}
@event
class EventB extends EventA {
@topic topicB: u8;
gender: string;
constructor(t: u8, g: string) {
super(t, g);
this.topicB = t;
this.gender = g;
}
}
@contract
export class EventEmitter {
count: i8;
constructor() {
}
@message
triggeEventA(): void {
let eventA = new EventA(100, "Elon");
eventA.emit();
}
@message
triggeEventB(): void {
let eventB = new EventB(<u8>300, "M");
eventB.emit();
}
}
2.7 增强注解的语法和参数检查
之前的版本,只会提示错误的注解,比如 @massage,会提示合约不支持注解@massge。
@massage({"mutates": false})
get(): bool {
return this.flag;
}
增强检查后,会通过匹配符算法,猜测用户要输入"@message"的注解,给出提示,如下:
Unsupported contract decorator @massage, do you mean '@message'? Check source text: @massage({"mutates": false}) in path:examples/flipper/flipper.ts lineAt: 24 columnAt: 5 range: (346 374).
还会检查 @message 标记的是不是 public 方法。
Decorator[@message] should mark on public method(Method: get isn't public method). Check source text: @message({"mutates": false})
@message({"mutates": false})
private get(): bool {
return this.flag;
} in path:examples/flipper/flipper.ts lineAt: 24 columnAt: 5 range: (346 432)..
会提示错误信息:
Decorator[@message] should mark on public method(Method: get isn't public method). Check source text: @message({"mutates": false})
private get(): bool {
return this.flag;
} in path:examples/flipper/flipper.ts lineAt: 24 columnAt: 5 range: (346 432)..
2.8 优化生成的 Wasm 文件大小
v0.3 版本中,ask-cli 的默认使用 --release 编译模式, 编译器将使用 -o3z 选
项来优化及压缩生成的 Wasm 文件;同时在 Framework 框架中,减少使用
字符串资源,以减小 Framework 的代码量。
2.9 升级 pallet-contract 中的 seal_xxx 方法
合约中的使用到的 seal_xxx 方法,已经更新到基于 Europa(https://github.com/patractlabs/europa)的最新 seal0 版本。
提供自定义 env 中系统参数的类型的能力。
在 Ask! 编程环境中,以下四个数据类型是可以根据运行的 FRAME 进行定制的: AccountId,Hash,Balance,BlockNumber。
它们的 Ask! 中的默认值分别为: Array<u8>(32), Array<u8>(32), UInt128,UInt32。
如果需要重新定义它们的实现方式,请在 Ask! 的项目路径 assembly/env/CustomTypes.ts 中, 重新实现即可。Ask! 对于它们的要求是: 它们都需要实现 Codec,此外没有其它的要求。
单元测试和文档
在 v0.3 版本中,我们提供了 examples/ 用来在线测试 Framework 的功能。提供了__tests__/ 用来测试编译器的功能。ts-package
下面有 ts-packages/contract-metadata/src/ 和ts-packages/transform/src/__tests__/ 目录用来放测试用例。
我们提供的文档,请参考使用 Ask! v0.3 章节。
Ask! v0.3 已经正式发布,请参考我们的 QuickStart(https://github.com/patractlabs/ask/blob/v0.3-dev/docs/Quickstart.md)。关于 Ask! 中各组件的使用文档,请参考 API 手册(https://github.com/patractlabs/ask/blob/v0.3-dev/docs/api/index.html)。
操作说明
现在我们可以使用 ask-cli 来编写 Ask! 智能合约了。
1.新建一个目录: mkdir erc20
2.进入新目录: cd erc20
3.新建一个 npm 项目: npm init -y
4.安装 pl-ask-cli: npm i pl-ask-cli
5.初始化项目: npx ask-cli init
6.拷贝 example/erc20(https://github.com/patractlabs/ask/tree/master/examples/erc20)下面的 index.ts,ERC20.ts 文件到 contracts/ 目录下。
7.编译: npx ask-cli compile contracts/index.ts
编译成功之后,我们就可以执行部署和调用的操作了。
使用 ERC20 合约
ERC20.ts 是一个符合 ERC20 标准的基类,它封装了可重复使用的 ERC20 接口,如 transfer,approve 等。定义了合约使用到的存储结构,以及事件 Transfer 和 Approval。在 Ask! v0.3中,我们已经按新的编程规范,重新实现了ERC20(https://github.com/patractlabs/ask/tree/master/examples/erc20),所以我们仍然可以这样写自己的 Token 合约:
import { Account, u128 } from "ask-lang";
import {ERC20} from "./ERC20";
@contract
@doc({"desc": "MyToken conract that extended erc20 contract"})
class MyToken extends ERC20 {
constructor() {
super();
}
@constructor
default(name: string = "", symbol: string = ""): void {
super.default(name, symbol);
}
@message
@doc({"desc": "Mint a token"})
mint(to: Account, amount: u128): void {
this._mint(to, amount);
}
@message
@doc({"desc": "burn the token"})
burn(from: Account, amount: u128): void {
this._burn(from, amount);
}
}
编译合约
使用以下的命令来编译我们的合约:
$ npx ask-cli compile contracts/index.ts
编译成功之后,将会在 examples/erc20/build/ 目录下生成 Wasm 和 metadata.json 文件。
部署和调用
我们在 Europa(https://github.com/patractlabs/europa)的 v3.0.0 分支沙盒环境中部署和测试合约功能,前端使用 polkadot-js(https://github.com/polkadot-js/apps),基于 master 分支,commit-id 11276477a0523348c7b143db566622aa32833296 代码基础,作为前端交互界面。
测试步骤如下:
1.首先我们按照 Europa 和 plokadot-js 的说明,启动节点和服务。
2.在 polkadot-js 的合约界面中,上传 build/ 下的 metadata.json 和 target.wasm 文件。
3.部署已经上传的合约,调用 default 方法发行 Token。
4.调用 mint, transfer, approve, burn 等方法,操作 ERC20 Token。
(视频链接)https://user-images.githubusercontent.com/2844215/120952438-9cc61a00-c77d-11eb-9745-454f977184be.gif
至此,在使用 ask-cli 和新的合约模型下,发行了 ERC20 通证。
正式发布 ask 和 ask-cli 的 npm 库。
用新的 contract 实现方式重新实现 examples 中的合约。
完整的合约开发教程。
完整的 API 文档。
往期精彩:2021 Web3.0 BootCamp|历经5个月,Patract所有产品线整体推进71%,继续砥砺深耕Wasm合约技术
2021 Sub0 Online精彩回顾|Patract Labs的Wasm智能合约
ink! 开发实践—组合范式(Metis) 之 ERC721及 ReceiverWasm合约测试网Jupiter已发布平行链版本Wasm合约生态的合约编程实践
Patract 为波卡 Wasm 合约生态的平行链和 DApp 开发提供解决方案。
How to join Patract官网|https://patract.ioElement|https://app.element.io/#/room/#PatractLabsDev:matrix.orgDiscord|https://discord.gg/znbmjYfvBRPatract 开放平台|https://open.patract.io
Telegram|https://t.me/patract
Twitter|https://twitter.com/PatractLabs
我们正招聘区块链开发工程师、前端/全栈开发工程师、云平台架构师、数据产品经理、产品经理等岗位,可以联系 sean@patract.io
扫码加入Patract微信开发群