v0.2Report
#
Ask! v0.2 报告12 weeks ago, Patract提交了Kusama国库的第#81号提案,关于Ask! v0.2的的实现目标, 原理及过程。在那份提案中, 我们将在v0.2版本中完成以下功能:
v0.2目标: 完善并增强Ask!的功能, 可以编写实用的合约
- 完善
@storage
,@message
注解的子选项, 增加@event
注解.- 增加复合数据类型
StorableMap
,StorableArray
.- 实现合约继承.
- 实现通过
@dynamic
注解完成跨合约调用功能.- 提供
erc20
,erc721
等示例合约.
我们已经实现的源码在Ask!项目仓位中, 示例合约在examples目录下,部分文档在https://docs.patract.io, 请在v0.2-review分支上review, 完成之后将合并到main分支.
#
设计与实现Ask! v0.2沿用v0.1中使用的注解解析和编译方式, 添加新的功能.
#
注解功能完善@storage
注解作用于class, 提供了@packed
和@ignore
子选项.@packed
注解使用于Map和Array类型的数据. 标记为@packed
的数据,会作为一个整体存取. 它的实现原理将在#复合类型数据存取章节详细描述.@ignore
注解标记的类, 只保存在memory中, 不会保存到链上, 执行环境退出之后, 即销毁.
@message
注解作用于类的方法, 提供了mutates
,payable
和selector
选项.
一个完整的@message
注解如:@message(payable, mutates = false, selector = "0xabcdef12")
* `payable`选项表明方法可以接受value, 默认不接受. 它的实现方式是, 在执行方法前插入一段逻辑, 判断调用方法时是否有value发送. 如果value不为0, 又没有注解为payable, 则方法执行时会通过assert方法退出. * `mutates`选项表明方法是否能够改变状态变量的值. mutates的默认值为true, 并且可以省略. 它的实现方式是, 如果指定了`mutates = false`, 那么会在`seal_set_storage`方法中执行一个assert, 不允许在这样的方法中写入数据到链上. * `selector`选项用于表明这个方法使用固定的值作为selector, 不用根据真实的方法名计算生成. 它既用来生成metadata.json中这个方法的`selector`, 同时在调用合约入口方法`call`时, 也使用它来作为方法dispatch的判断条件.
在它们的实现方式中, 条件检查只能在运行时检查, 暂时还不能在编译时检查.
增加
@event
注解, 支持发出event功能.
@event
注解作用于class上, 预处理器需要为这个类生成符合要求的逻辑.* `@topic`子注解作用于类上的一个成员变量, 表示这个变量可以在链上被过滤出来. 它的实现方式是, 在topic buffer中存放topic变量的hash, 在data buffer中存放所有变量的值, 然后通过`seal_deposit_event`方法发送到链上.
#
复合数据类型存取复合数据类型在v0.2版本中支持了StorableMap
, StorableArray
以及自定义的class对象(需要实现Codec
接口).
复合数据类型支持@spread
和@packed
两种存储模式.
对于@spread
存储模式, 每一个存储单位都有自己的存储地址, 只有在需要的时候才会载入.
对于@packed
存储模式, 需要将所有的存储单位序列化为一组数据流, 存储在共享的地址. 所有的存储单元一起存取. 这种模式不适合大数据存取.
StorableMap
SpreadStorableMap
和PackedStorableMap
是Map的封装类, 并添加了数据持久化功能. 分别实现了@spread
和@packed
两种存储模式.SpreadStorableMap
的存储结构如下:
MapEntry
中保存了这个Map所存储数据的数量以及第一个存储位置的Hash. 它本身的存储位置在Hash(prefix)
, 并且这个存储位置将会被导出到metadata.json中, 供外部Apps访问.
KVStore
是一个具体存储的K/V值, 每一个KVStore除了保存Key/Value之外, 还保存了next/prev节点的Hash. 如果它是一个尾节点, 那么next
的值是NullHash
, 即(0x0000000000000000000000000000000000
); 如果它是一个头节点, 那么prev
的值是NullHash
. 通过双向链表的方式, 外部Apps可以迭代访问到所有的数据.
每一个KVStore
的存储位置都由以下规则确定: Hash(prefix + key)
PackedStorableMap
的存储结构如下:
Packed存储模式与Spread不同, 它的所有数据都是一次性全部加载/存储的.
MapEntry
的使用与Spread模式一样.
它的所有所有数据, 都通过u8[]
的方式, 存储在固定位置Hash(prefix + ".value")
下面.
StorableArray
SpreadStorableArray
和PackedStorableArray
是Array类的封装, 并添加了数据持久化功能, 分别实现了@spread
和@packed
两种存储模式.SpreadStorableArray
的存储结构如下:
ArrayEntry
保存了这个Array的元素个数size
以及序列化之后的bytes的数量rawBytesCount
(Spread模式下这个值是0
). 它本身的存储位置在Hash(prefix)
, 并且这个存储位置将会被导出到metadata.json中, 供外部Apps访问.
每一个元素的存储位置都是通过Hash(prefix + index)
的方式确定, 并且在这个位置保存了元素序列化之后的数据.
PackedStorableArray
的存储结构如下:
ArrayEntry
保存了这个Array的元素个数size
以及序列化之后的bytes的数量rawBytesCount
.
在这个存储模式下, 所有的元素都保存在同一个地址下Hash(prefix + ".values")
.
结构化存储对象
结构化存储对象
是一个可序列化的类, 即实现了Codec
接口的类, 均可以存储到链上.
例如下的class:
class EmbedObj implements Codec {
a: i8; b: string; c: u128;
constructor(a: i8 = 0, b: string = "", c: u128 = u128.Zero) { this.a = a; this.b = b; this.c = c; }
toU8a(): u8[] { let bytes = new Array<u8>(); let aWrap = new Int8(this.a); let bWrap = new ScaleString(this.b); let cWrap = new UInt128(this.c);
bytes = bytes.concat(aWrap.toU8a()) .concat(bWrap.toU8a()) .concat(cWrap.toU8a()); return bytes; }
encodedLength(): i32 { let aWrap = new Int8(this.a); let bWrap = new ScaleString(this.b); let cWrap = new UInt128(this.c);
return aWrap.encodedLength() + bWrap.encodedLength() + cWrap.encodedLength(); }
populateFromBytes(bytes: u8[], index: i32 = 0): void { let aWrap = new Int8(); aWrap.populateFromBytes(bytes, index); index += aWrap.encodedLength();
let bWrap = new ScaleString(); bWrap.populateFromBytes(bytes, index); index += bWrap.encodedLength();
let cWrap = new UInt128(); cWrap.populateFromBytes(bytes, index);
this.a = aWrap.unwrap(); this.b = bWrap.toString(); this.c = cWrap.unwrap(); }
eq(other: EmbedObj): bool { return this.a == other.a && this.b == other.b && this.c == other.c; }
notEq(other: EmbedObj): bool { return !this.eq(other); } }
EmbedObj
可以用在@storage
注解的存储类中, 保存一组相关联的信息.
#
合约继承功能继承功能使合约复用成为了可能.
v0.2的合约继承遵循以下基本原则:
- 对于
@constructor
方法, 使用子类合约中的定义的@constructor
方法. 如果子类中没有提供, 则最终生成的合约中不提供@constructor
, 即便父类中已经定义. 因为父类无法得知子类中成员变量情况, 不能够完全正确初始化合约. - 对于
@message
方法, 使用父类和子类中所有message的并集. - 对于
@storage
类, 不做额外处理, 由开发者决定如何使用.
继承功能实现原理
- 子合约必须位于编译的入口文件中。通过对标记有@contract注解类描述信息分析,确定主合约入口。说明,入口函数只能有一个@contract合约。
clzPrototype.declaration.range.source.sourceKind == SourceKind.USER_ENTRY && AstUtil.hasSpecifyDecorator(clzPrototype.declaration, ContractDecoratorKind.CONTRACT);
- 定位到主合约类之后,分析合约类的继承关系,对父类解析获取@message, 然后到处合约方法message,递归执行这个操作。
public resolveContractClass(): void { this.classPrototype.instanceMembers && this.classPrototype.instanceMembers.forEach((instance, _) => { if (ElementUtil.isCntrFuncPrototype(instance)) { this.cntrFuncDefs.push(new ConstructorDef(<FunctionPrototype>instance)); } if (ElementUtil.isMessageFuncPrototype(instance)) { let msgFunc = new MessageFunctionDef(<FunctionPrototype>instance); this.msgFuncDefs.push(msgFunc); } }); this.resolveBaseClass(this.classPrototype);}
private resolveBaseClass(sonClassPrototype: ClassPrototype): void { if (sonClassPrototype.basePrototype) { let basePrototype = sonClassPrototype.basePrototype; basePrototype.instanceMembers && basePrototype.instanceMembers.forEach((instance, _) => { if (ElementUtil.isMessageFuncPrototype(instance)) { let msgFunc = new MessageFunctionDef(<FunctionPrototype>instance); this.msgFuncDefs.push(msgFunc); } }); this.resolveBaseClass(basePrototype); }}
- @message和@storage的生成方式,参考单合约。
#
@dynamic注解的作用与实现@dynamic
注解用来描述一个合约的message信息, 这个合约已经部署并完成了实例化. 其它合约可以通过@dynamic声明, 与这个合约进行跨合约交互.
@dynamic
注解作用于类上面, 预编译器将对@dynamic的类生成跨合约调用的逻辑.
@dynamic实现原理
- 通过@dynamic注解找到对应的接口类
if (ElementUtil.isDynamicClassPrototype(element)) { let dynamicInterpreter = new DynamicIntercepter(<ClassPrototype>element); this.dynamics.push(dynamicInterpreter);}
- 然后对接口类分析,然后对每个方法生成实现调用方法, 实现调用类生成的模板如下。其中addr是被调用的合约地址。
export const dynamicTpl = `class {{className}} { addr: AccountId; constructor(addr: AccountId) { this.addr = addr; } {{#each functions}} {{#generateFunction .}}{{/generateFunction}} {{/each}}}`;
- 其中最主要的是对方法实现调用类。通过generateFunction方法来生成。generateFunction通过分析方法的参数,然后对参数进行转换,转换到codec类型。然后通过Abi.encode编码进行跨合约调用。
如果原始接口方式
transfer(recipient: AccountId, amount: u128): bool { return true;}
则生成的调用方法
transfer(p0: AccountId,p1: u128): bool { let data = Abi.encode("transfer", [p0,new UInt128(p1)]); let rs = this.addr.call(data); return BytesReader.decodeInto<Bool>(rs).unwrap();}
- 通过对合约设置合约地址,然后通过Abi.encode实现调用。
#
使用Ask! v0.2Ask!项目尚末发布, 所以我们需要将源码clone到本地.
git clone https://github.com/patractlabs/ask
clone完成之后, 请执行以下步骤:
$ cd ask$ yarn
在v0.2项目中, 我们已经在examples目录下, 提供了erc20
和erc721
两个项目. 下面我们用erc20
项目来说明v0.2新增功能如何使用.
#
编写合约在示例的erc20合约中, 我们使用到了v0.2版本中的以下特性:
- 合约继承
- 合约中发送Event
- 使用复合存储类型: Map
mutates = false
等其它注解
此处提供的ERC20.ts合约, 仅仅用来展示Ask!的使用方式和能力, 不能作为正式的Token合约使用.
#
ERC20合约ERC20.ts
是一个符合ERC20标准的基类, 它封装了可重复使用的ERC20接口, 如transfer
, approve
等. 定义了合约使用到的存储结构, 以及事件Transfer
和Approval
.
@contractexport class ERC20 { private storage: ERC20Storage;
constructor() { this.storage = new ERC20Storage(); }
@constructor default(name: string = "", symbol: string = ""): void { this.storage.name = name; this.storage.symbol = symbol; this.storage.decimal = 18; this.storage.totalSupply = u128.Zero; }
@message(mutates = false) name(): string { return this.storage.name; }
@message(mutates = false) symbol(): string { return this.storage.symbol; }
@message(mutates = false) decimal(): u8 { return this.storage.decimal; }
@message(mutates = false) totalSupply(): u128 { return this.storage.totalSupply; }
@message(mutates = false) balanceOf(account: AccountId): u128 { return this.storage.balances.get(account).unwrap(); }
@message transfer(recipient: AccountId, amount: u128): bool { let from = msg.sender; this._transfer(from, recipient, amount); return true; }// .........}
在已经拥有了ERC20合约的情况下, 我们发行新的Token就会非常的简单, 例如index.ts
合约中发行的MyToken
(只为了演示如何使用Ask!发行ERC20 Token, 未加权限控制逻辑):
import { AccountId, u128 } from "ask-lang";import {ERC20} from "./ERC20";
@contractclass MyToken extends ERC20 {
constructor() { super(); }
@constructor default(name: string = "", symbol: string = ""): void { super.default(name, symbol); }
@message mint(to: AccountId, amount: u128): void { this._mint(to, amount); }
@message burn(from: AccountId, amount: u128): void { this._burn(from, amount); }}
#
编译合约使用以下的命令来编译我们的合约:
$ npx ask examples/erc20/index.ts
编译成功之后, 将会在examples/erc20/target/
目录下生成target.wasm
和metadata.json
文件.
#
部署和调用我们在Europa沙盒环境中部署和测试合约功能, 前端使用polkadot-js作为交互界面.
测试步骤如下:
首先我们按照
Europa
和plokadot-js
的说明, 启动节点和服务.在
polkadot-js
的合约界面中, 上传erc20/target
下的metadata.json
和target.wasm
文件.部署已经上传的合约, 调用
default
方法发行Token.调用
mint
,transfer
,approve
,burn
等方法, 操作ERC20 Token.
至此, 我们通过继承的方式, 成功的发行了ERC20通证.
#
Ask! v0.2已经实现的内容- 完善
@storage
,@message
注解的子选项, 增加@event
注解. - 增加复合数据类型
StorableMap
,StorableArray
. - 实现合约继承.
- 实现通过
@dynamic
注解完成跨合约调用功能. - 提供
erc20
,erc721
,crosscall
等示例合约.