Skip to main content

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, payableselector选项.
    一个完整的@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
    SpreadStorableMapPackedStorableMap是Map的封装类, 并添加了数据持久化功能. 分别实现了@spread@packed两种存储模式.
    SpreadStorableMap的存储结构如下:
    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的存储结构如下:
PackedStorableMap

Packed存储模式与Spread不同, 它的所有数据都是一次性全部加载/存储的.
MapEntry的使用与Spread模式一样.
它的所有所有数据, 都通过u8[]的方式, 存储在固定位置Hash(prefix + ".value")下面.

  • StorableArray
    SpreadStorableArrayPackedStorableArray是Array类的封装, 并添加了数据持久化功能, 分别实现了@spread@packed两种存储模式.
    SpreadStorableArray的存储结构如下:
    SpreadStorableArray

ArrayEntry保存了这个Array的元素个数size以及序列化之后的bytes的数量rawBytesCount(Spread模式下这个值是0). 它本身的存储位置在Hash(prefix), 并且这个存储位置将会被导出到metadata.json中, 供外部Apps访问.
每一个元素的存储位置都是通过Hash(prefix + index)的方式确定, 并且在这个位置保存了元素序列化之后的数据.

PackedStorableArray的存储结构如下:
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.2#

Ask!项目尚末发布, 所以我们需要将源码clone到本地.
git clone https://github.com/patractlabs/ask

clone完成之后, 请执行以下步骤:

$ cd ask$ yarn

在v0.2项目中, 我们已经在examples目录下, 提供了erc20erc721两个项目. 下面我们用erc20项目来说明v0.2新增功能如何使用.

编写合约#

在示例的erc20合约中, 我们使用到了v0.2版本中的以下特性:

  • 合约继承
  • 合约中发送Event
  • 使用复合存储类型: Map
  • mutates = false等其它注解

此处提供的ERC20.ts合约, 仅仅用来展示Ask!的使用方式和能力, 不能作为正式的Token合约使用.

ERC20合约#

ERC20.ts是一个符合ERC20标准的基类, 它封装了可重复使用的ERC20接口, 如transfer, approve等. 定义了合约使用到的存储结构, 以及事件TransferApproval.


@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.wasmmetadata.json文件.

部署和调用#

我们在Europa沙盒环境中部署和测试合约功能, 前端使用polkadot-js作为交互界面.
测试步骤如下:

  1. 首先我们按照Europaplokadot-js的说明, 启动节点和服务.

  2. polkadot-js的合约界面中, 上传erc20/target下的metadata.jsontarget.wasm文件.

  3. 部署已经上传的合约, 调用default方法发行Token.

  4. 调用mint, transfer, approve, burn等方法, 操作ERC20 Token.

至此, 我们通过继承的方式, 成功的发行了ERC20通证.

Ask! v0.2已经实现的内容#

  • 完善@storage, @message注解的子选项, 增加@event注解.
  • 增加复合数据类型StorableMap, StorableArray.
  • 实现合约继承.
  • 实现通过@dynamic注解完成跨合约调用功能.
  • 提供erc20, erc721, crosscall等示例合约.