v0.2Report
#
Patract's treasury report for Ask! v0.2 (ink! in AssemblyScript)12 weeks ago, Patract submitted Kusama Treasury’s #81 proposal regarding Ask! v0.2, including the implementation of the goal, principle and process. In that proposal, we will complete the following functions in the v0.2:
V0.2 Goal: Improve and enhance the function of Ask!. You can write practical contracts
- Improve the sub-options of
@storage
,@message
annotations, and add@event
annotations.- Add composite data types
StorableMap
,StorableArray
.- Implement contract inheritance.
- Implement the cross-contract call function through the
@dynamic
annotation.- Provide example contracts such as
ERC20
,ERC721
, etc.
Now the source code we have implemented is in the Ask! project repo, and the example contract is in examples directory, Please review at v0.2-review branch, then we will merge into master.
#
Design and ImplementationAsk! v0.2 follows the annotation parsing and compilation method used in v0.1, adding new functions.
#
Improve Annotation functionThe
@storage
annotation works on the class, and provides the@packed
and@ignore
sub-options.- The
@packed
annotation is used for data type about Map and Array . The data marked as@packed
will be stored and accessed as a whole. Its implementation theory will be described in detail in the following chapters. - Members of classe marked with
@ignore
annotations are only saved in memory and will not be saved on the chain. After the execution environment exits, they are destroyed.
- The
The
@message
annotation works on the methods of the class, and provides themutates
,payable
andselector
options. A complete@message
annotation is like:@message(payable, mutates = false, selector = "0xabcdef12")
- The
payable
option indicates that the method can accept value, but it is not accepted by default. It is implemented by inserting a piece of logic before executing the method to determine whether the value is sent when the method is called. If the value is not 0, and there is no annotation as payable, When the method is executed, it will exit through the assert method. - The
mutates
option indicates whether the method can change the value of the state variable. The default value of mutates is true, and can be omitted. Its implementation is that ifmutates = false
is specified, it will be executed an assert method in theseal_set_storage
, which is not allowed to write data to the chain in such a method. - The
selector
option is used to indicate that this method uses a fixed value as the selector, and does not need to be calculated and generated based on the real method name. It is used to generate theselector
of this method in metadata.json, and to call the contract entry methodcall
, it is also used as a judgment condition for method dispatch. In their implementation, conditional checks can only be checked at runtime, and cannot be checked at compile time for the time being.
- The
Added
@event
annotation to support the event function. The @event annotation is applied on the class, and the preprocessor needs to generate logic that meets the requirements for this class.- The
@topic
sub-annotation acts on a member variable of the class, which means that this variable can be filtered out on the chain. Its implementation is to store the hash of the topic variable in the topic buffer, and store all the variables in the data buffer, The value is then sent to the chain through theseal_deposit_event
method.
- The
#
Composite data type store and readWe support StorableMap
, StorableArray
about composite data types and custom class objects in v0.2 version (need to implement Codec
interface). The composite data type supports two storage modes, @spread
and @packed
. For the @spread
storage mode, each storage unit has its own storage address, and will only be loaded when needed. For the @packed
storage mode, all storage units need to be serialized into a set of data streams and stored in a shared address. All storage units are accessed together. This mode is not suitable for large data access.
StorableMap
:SpreadStorableMap
andPackedStorableMap
are encapsulated classes of Map, and add data persistence function. Two storage modes of@spread
and@packed
are implemented respectively. The storage structure ofSpreadStorableMap
is as follows:
The number of data stored in this Map and the Hash of the first storage location are saved in MapEntry
. Its storage location is in Hash(prefix)
, and this storage location will be exported to metadata.json for access of external apps.
KVStore
is a specific stored K/V value. In addition to storing Key/Value, each KVStore also stores the hash of the next/prev node. If it is a tail node, then the value of next
is NullHash
, that is (0x0000000000000000000000000000000000
); if it is a head node, then the value of prev
is NullHash
. Through a doubly linked list, external Apps can iteratively access all data. The storage location of each KVStore
is determined by the following rules: Hash(prefix + key)
. The storage structure of PackedStorableMap
is as follows:
The storage model of Packed is different from Spread, all its data is loaded/stored all at once. The usage of MapEntry
is the same as the Spread model. All its data is stored in a fixed location under Hash(prefix + ".value")
through the method of u8[]
.
StorableArray
:SpreadStorableArray
andPackedStorableArray
are the encapsulation of the Array class, and added data persistence function, respectively implementing the two storage modes of@spread
and@packed
.
The storage structure of SpreadStorableArray
is as follows:
ArrayEntry
saves the number of elements of this Array size
and the number of bytes after serialization rawBytesCount
(this value is 0
in Spread model). Its storage location is in Hash(prefix)
, And this storage location will be exported to metadata.json for external apps to access. The storage location of each element is determined by the method of Hash(prefix + index)
, and the serialized data of the element is stored in this location.
The storage structure of PackedStorableArray
is as follows:
ArrayEntry
stores the number of elements in this Array size
and the number of bytes after serialization rawBytesCount
. In this storage mode, all elements are stored under the same address Hash(prefix + ".values ")
.
Composite object
Composite object
is a serializable class, that is, a class that implements theCodec
interface, which can be stored on the chain. For example, the following 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
can be used in the storage class annotated by @storage
to save a set of related information.
#
Contract inheritance functionThe inheritance function makes contract reuse possible. The contract inheritance of v0.2 follows the following basic principles:
- For the
@constructor
method, use the@constructor
method defined in the subclass contract. If it is not provided in the subclass, then the final generated contract will not provide@constructor
, even if it is already defined in the parent class. The parent class cannot know the member variables in the subclass, and cannot completely initialize the contract correctly. - For the
@message
method, use the union of all messages in the parent class and the child class. - For the
@storage
class, no additional processing is done, and the developer decides how to use it.
The realization theory of inheritance function
- The sub-contract must be located in the compiled entry file. The main contract entry is determined by analyzing the description information of the class marked with @contract annotations. It should mention that the every entry function can only have one contract with @contract.
clzPrototype.declaration.range.source.sourceKind == SourceKind.USER_ENTRY && AstUtil.hasSpecifyDecorator(clzPrototype.declaration, ContractDecoratorKind.CONTRACT);
- After locating the main contract class, analyze the inheritance relationship of the contract class, parse the parent class to obtain @message, and then perform this operation recursively through the contract method message everywhere.
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); }}
- The generation methods of @message and @storage refer to the single contract.
#
The role and implementation of @dynamic annotationThe @dynamic annotation is used to describe the message information of a contract, which has been deployed and instantiated. Other contracts can interact with this contract through @dynamic declarations. The @dynamic annotation acts on the class, The pre-compiler will generate cross-contract call logic for the @dynamic class.
the implementation theory of @dynamic
- Find the corresponding interface class through the @dynamic annotation
if (ElementUtil.isDynamicClassPrototype(element)) { let dynamicInterpreter = new DynamicIntercepter(<ClassPrototype>element); this.dynamics.push(dynamicInterpreter);}
- Then analyze the interface class, and then generate the implementation call method for each method. The template generated by the implementation call class is as follows. Where addr is the address of the contract being called.
export const dynamicTpl = `class {{className}} { addr: AccountId; constructor(addr: AccountId) { this.addr = addr; } {{#each functions}} {{#generateFunction .}}{{/generateFunction}} {{/each}}}`;
- The most important one is to implement calling classes for methods. Generated by the generateFunction method. generateFunction analyzes the parameters of the method, and then converts the parameters to the codec type. Then do cross-contract call through Abi.encode.
If the original interface method
transfer(recipient: AccountId, amount: u128): bool { return true;}
The generated call method
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();}
- Set the contract address for the contract, and then implement the call through Abi.encode.
#
How to use Ask! v0.2The Ask! project is not yet released, so we need to clone the source code locally.
git clone https://github.com/patractlabs/ask
After the clone is completed, please perform the following steps:
$ cd ask$ yarn
In the v0.2 project, we have provided two projects erc20
and erc721
in the examples directory. Below we use the erc20
project to illustrate how to use the new features of v0.2.
#
Write a contractIn the example erc20 contract, we used the following features in the v0.2 version:
- Contract inheritance
- Event sent in contract
- Use composite storage type: Map
mutates = false
and other annotations
The ERC20.ts contract provided here is only used to demonstrate the use and capabilities of Ask! and cannot be used as a formal Token contract.
#
ERC20 contractERC20.ts
is a base class that fit to the ERC20 standard. It encapsulates reusable ERC20 interfaces, such as transfer
, approve
, etc. It defines the storage structure used by the contract, as well as the events Transfer
and 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; }// .........}
If we already have an ERC20 contract, it will be very simple for us to issue new Tokens, such as the MyToken
issued in the index.ts
contract (just to demonstrate how to use Ask! to issue ERC20 Tokens, without permission control logic):
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); }}
#
Compile the contractUse the following command to compile our contract:
$ npx ask examples/erc20/index.ts
After the compilation is successful, the target.wasm
and metadata.json
files will be generated in the examples/erc20/target/
directory.
#
Deployment and invocationWe deploy and test contract functions in the Europa sandbox environment, using polkadot-js on the front end as an interactive interface. The test steps are as follows:
First, we follow the instructions of
Europa
andplokadot-js
to start nodes and services.In the contract interface of
polkadot-js
, upload themetadata.json
andtarget.wasm
files undererc20/target
.Deploy the uploaded contract and call the
default
method to issue tokens.Call
mint
,transfer
,approve
,burn
and other methods to operate ERC20 Token.
So far, we have successfully issued ERC20 tokens through inheritance.
#
What has been implemented in Ask! v0.2- Improve the sub-options of
@storage
,@message
annotations, and add@event
annotations. - Add composite data types
StorableMap
,StorableArray
. - Implement contract inheritance.
- Implement the cross-contract call function through the
@dynamic
annotation. - Provide example contracts such as
erc20
,erc721
,crosscall
, etc.