查看原文
其他

Patract完成Ask! v0.2 的开发,实现可编写实用合约的功能

Patract Patract开放平台 2021-12-09



两个多月前,Patract 提交了 Kusama 国库的第#81号[1]提案,关于 Ask! v0.2 的实现目标,原理及过程。提案中,v0.2 的目标是完善并增强 Ask! 的功能,可以编写实用的合约。我们将在 v0.2 版本中完成以下功能:


  • 完善 @storage,@message 注解的子选项,增加 @event 注解。

  • 增加复合数据类型StorableMap,StorableArray

  • 实现合约继承。

  • 实现通过 @dynamic 注解解完成跨合约调用功能。

  • 提供  erc20erc721等示例合约。


我们已经实现的源码在 Ask![2] 项目仓位中, 示例合约在 examples[3]目录下,部分文档在 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

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.2
Ask!项目尚末发布,所以我们需要将源码 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 [4]沙盒环境中部署和测试合约功能, 前端使用 polkadot-js [5]作为交互界面。测试步骤如下:

1.首先我们按照 Europa 和 plokadot-js 的说明, 启动节点和服务。2.在 polkadot-js 的合约界面中, 上传 erc20/target 下的 metadata.json 和 target.wasm 文件。3.部署已经上传的合约,调用 default 方法发行 Token。4.调用 mint,transfer,approve,burn等方法,操作 ERC20 Token。


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

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

  • 完善@storage,@message注解的子选项,增加@event注解。

  • 增加复合数据类型 StorableMap ,StorableArray。

  • 实现合约继承。

  • 实现通过 @dynamic 注解完成跨合约调用功能。

  • 提供 erc20,erc721, crosscall等示例合约。


延伸阅读

[1]Kusama 国库的第#81号:
https://kusama.polkassembly.io/treasury/8
[2]Ask!源码:
https://github.com/patractlabs/ask
[3]示例合约 examples 目录:
https://github.com/patractlabs/ask/tree/master/examples
[4]Europa 源码:

https://github.com/patractlabs/europa

[5]polkadot-js源码:
https://github.com/polkadot-js/apps

About Patract


Patract 为波卡 Wasm 合约生态的平行链和 DApp 开发提供解决方案。我们帮助社区平行链设计和开发链上合约模块和 Runtime 支持,并且为 DApp 开发者提供覆盖开发、测试、调试、部署、监控、数据提供和前端开发等阶段的全栈工具和服务支持。


How to join Patract


1.对于合约开发者,可以访问官网(https://patract.io),熟悉测试链和工具套件。欢迎加入官方开发群:
Element(https://app.element.io/#/room/#PatractLabsDev:matrix.org)
Discord(https://discord.gg/wJ8TnTfjcq)
 
2.对于将要集成 Wasm 合约功能的平行链项目方,或者使用 Wasm 合约开发的 DApp 项目方,欢迎加入 Patract 开放平台:https://open.patract.io
 
3.对于用户,欢迎加入:
Telegram(https://t.me/patract)
Twitter(https://twitter.com/PatractLabs)
 
4.对于求职者,我们在招聘区块链开发工程师、前端/全栈开发工程师、产品经理等岗位,可以联系 sean@patract.io

扫码加入Patract 微信开发群


往期精彩:
// 平行链大考将至,Polkadot、Patract共话插槽拍卖
//Patract启动ink!和Ask!的标准库项目Metis,快速构建Wasm智能合约
// Patract 重要里程碑:Redspot v0.4完成主要开发计划


: . Video Mini Program Like ,轻点两下取消赞 Wow ,轻点两下取消在看

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存