Patract完成Ask! v0.2 的开发,实现可编写实用合约的功能
两个多月前,Patract 提交了 Kusama 国库的第#81号[1]提案,关于 Ask! v0.2 的实现目标,原理及过程。提案中,v0.2 的目标是完善并增强 Ask! 的功能,可以编写实用的合约。我们将在 v0.2 版本中完成以下功能:
完善 @storage,@message 注解的子选项,增加 @event 注解。
增加复合数据类型
StorableMap,
StorableArray。实现合约继承。
实现通过 @dynamic 注解解完成跨合约调用功能。
提供 erc20
,
erc721
等示例合约。
我们已经实现的源码在 Ask![2] 项目仓位中, 示例合约在 examples[3]目录下,部分文档在 docs.patract.io,请在 v0.2-review 分支上 review,完成之后将合并到 main 分支。
注解功能完善
@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 方法发送到链上。
复合数据类型存取
复合数据类型支持 @spread 和 @packed 两种存储模式。
对于 @spread 存储模式,每一个存储单位都有自己的存储地址,只有在需要的时候才会载入。
对于 @packed 存储模式,需要将所有的存储单位序列化为一组数据流,存储在共享的地址。所有的存储单元一起存取. 这种模式不适合大数据存取。
StorableMap
KVStore 是一个具体存储的K/V值, 每一个 KVStore 除了保存 Key/Value 之外, 还保存了 next/prev 节点的 Hash。 如果它是一个尾节点,那么 next 的值是 NullHash,即(0x0000000000000000000000000000000000);如果它是一个头节点,那么 prev 的值是 NullHash。通过双向链表的方式,外部 Apps 可以迭代访问到所有的数据。
每一个 KVStore 的存储位置都由以下规则确定: Hash(prefix + key)。
StorableArray
SpreadStorableArray
和 PackedStorableArray 是 Array 类的封装,并添加了数据持久化功能,分别实现了 @spread 和 @packed 两种存储模式。
SpreadStorableArray 的存储结构如下:
每一个元素的存储位置都是通过 Hash(prefix + index) 的方式确定,并且在这个位置保存了元素序列化之后的数据。
结构化存储对象
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); } }
合约继承功能
对于 @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);
}
}
@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();
}
git clone https://github.com/patractlabs/ask
$ cd ask
$ yarn
编写合约
合约继承 合约中发送Event 使用复合存储类型: Map mutates = false
等其它注解
此处提供的 ERC20.ts 合约,仅仅用来展示 Ask! 的使用方式和能力,不能作为正式的 Token 合约使用。
ERC20.ts 是一个符合 ERC20 标准的基类, 它封装了可重复使用的 ERC20 接口,如 transfer,approve 等。定义了合约使用到的存储结构,以及事件Transfer 和 Approval。
@contract
export 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;
}
// .........
}
import { AccountId, u128 } from "ask-lang";
import {ERC20} from "./ERC20";
@contract
class 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
部署和调用
1.首先我们按照 Europa 和 plokadot-js 的说明, 启动节点和服务。2.在 polkadot-js 的合约界面中, 上传 erc20/target 下的 metadata.json 和 target.wasm 文件。3.部署已经上传的合约,调用 default 方法发行 Token。4.调用 mint,transfer,approve,burn等方法,操作 ERC20 Token。
完善@storage,@message注解的子选项,增加@event注解。
增加复合数据类型 StorableMap ,StorableArray。
实现合约继承。
实现通过 @dynamic 注解完成跨合约调用功能。
提供 erc20,erc721, crosscall等示例合约。
https://github.com/patractlabs/europa
About Patract
Patract 为波卡 Wasm 合约生态的平行链和 DApp 开发提供解决方案。我们帮助社区平行链设计和开发链上合约模块和 Runtime 支持,并且为 DApp 开发者提供覆盖开发、测试、调试、部署、监控、数据提供和前端开发等阶段的全栈工具和服务支持。