[TOC]
从Github拉取一份合约项目模板:
git clone https://github.com/damoclis/dmc-smart-contract-boilerplate进入项目目录,安装依赖库:
npm install全局安装dmcscript:
npm install dmcscript -g在src下,创建helloworld目录,并在该合约目录下新建一个合约文件helloworld.ts。
编写一个简单的Hello World合约,示例如下:
import{Contract}from"dmc-lib";classHelloWorldextendsContract{// 定义合约方法`hi`
@actionhi(msg: string): void{// 打印日志Prints(msg);}}在上面的例子中,我们自定义了一个合约类HelloWorld,继承自内置合约类Contract。同时,声明了一个合约方法hi(msg:string):void。所有的合约方法均需要使用装饰器@action来声明,编译器将会解析并导出对应的abi。
Prints是内置API,作用是在运行节点的终端打印相关信息。
下面,我们使用dmcscript编译合约:
dsc src/helloworld/helloworld.ts -o build/helloworld/helloworld.wasm -t build/helloworld/helloworld.wat --abi build/helloworld/helloworld.abiwasm为合约的字节码文件,用于部署到区块链上。wat为合约字节码的文本描述文件。abi为合约的abi文件,用于部署到区块链上。
TODO 部署
我们尝试为上面的HelloWorld合约添加更丰富的功能,实现一个资产转移的例子:
import{Contract,RequireAuth,Prints,Address,Asset}from"dmc-lib";classHelloWorldextendsContract{// 定义合约方法`hi`
@actionhi(msg: string): void{// 打印日志Prints(msg);}// 定义合约方法`transfer`
@actiontransfer(from: Address,to: Address,val: Asset): void{// 判断当前合约发送方地址是否与`from`参数一致RequireAuth(from);// 从`from`向`to`转移资产`val`from.transfer(to,val);}}上面我们定义了一个transfer合约方法,用于将数值为val的资产从from转移到to。
这里我们涉及了两个重要的内置类型:
Address: 地址类型,用于表示账户地址并提供相关方法。Asset: 资产类型,用于表示原生资产并提供相关方法。
RequireAuth(addr: Address)常用于判断操作的权限,若入参地址与本次合约调用的发送方不一致,则会终止合约执行。类似API还有hasAuth(addr: Address): bool,但它返回bool值,不会终止合约执行。例如,Bob发送了一笔交易调用了transfer合约方法,若入参from的地址与Bob不一致,则RequireAuth(from)将失败,终止合约。
数据持久化是智能合约必不可少的功能。我们希望合约中对数据的持久化是显式调用的,并与传统的数据库交互方式类似。
首先我们定义一个数据表Person。它是数据持久化的基本单元:
// 声明数据表person,定义每列数据的字段。底层为k-v型存储。// 简单来说:在合约的数据表`person`中,存储若干数据格式为`Person`的记录。
@database("person")classPersonimplementsSerializable{
@keyname: stringage: u32;}Serializable是内置的序列化接口,所有需要持久化的数据类均须声明实现该接口,但无需开发者手动实现,编译器会帮助自动补全代码。
在Damoclis中,合约层的数据库模型如下:
- Contract
- table 1
- data1
- data2
- ...
- dataN
- table 2
- table 3
- ...
- table 1
@database("person")装饰器用于表明该数据类存储在person表内,编译器会识别并生成对应ABI。
@key装饰器用于表明该数据项的主键。数据库底层实际为kv存储,k为@key声明的主键,v为整个数据对象的序列化字符串。
然后,在合约类中添加下列方法:
@actionstore(): void{constperson=newPerson();person.name="bob";person.age=16;// 创建一个数据项实例// 第一个参数为数据库所在合约,第二个参数为数据表名称constdb=newDatabase<Person>(this.receiver,"person");// 写操作只对合约本身的数据表生效db.store(person);}完整代码如下:
import{Contract,Database}from"../../src/database";// 声明数据表person的数据类
@database("person")classPersonimplementsSerializable{
@keyname: stringage: u32;}classDBTestextendsContract{_db: Database<Person>constructor(){// 初始化数据库实例this._db=newDatabase<Person>(this.self,"person");}
@actionstore(): void{constperson=newPerson();person.name="bob";person.age=16;// 创建一个数据项实例// 写操作只对合约本身的数据表生效this._db.store(person);}}Damoclis设计了返回值机制,将合约的运行结果返回给区块链,并保存在交易回执中。
classReturnTestextendsContract{// 定义add方法,返回一个u64类型的值。
@actionadd(a: u64,b: 64): u64{returnSafemath.add(a,b);}}对于合约方法return的类型,我们支持以下三种:
- 整数类型:
i32/i64,u32/u64。 - 字符串类型:
string。 - 拥有
bytes成员变量的复杂类型。如Bytes,Address,Asset或其他自定义类型。 //目前暂时只支持内置类型的返回
返回值以Bytes数组的形式保存在交易回执中。其数据格式为:| 1 byte flag | bytes |。flag == 0表示bytes部分是普通的bytes或字符串;flag == 1表示bytes部分是整数的大端字节数组,可进行相应解码。
跨合约调用也是智能合约的必备功能。在Damoclis中,对合约的调用是以Action为单元。下面是一个调用其他合约的例子:
// contractA.tsimport{Contract,Action}from"dmc-lib";classContractAextendsContract{
@actioncall(target: Address):void{// 构建需要发送的内联actionconstaction=newAction(target,Asset.zero,"hi",[Builtin.fromString("hello")]);// 发送action,调用对方合约方法action.send();}}// contractB.tsimport{Contract,Prints}from"dmc-lib";classContractBextendsContract{
@actionhi(msg: string):void{Prints(msg);}}上面演示了contractA.call通过跨合约调用ContractB.hi的例子。这里我们提供了一个内置的Action类,用于表示一个action,其构造函数为Action(对方合约地址,发送资产数量,合约方法,[参数, ...]),详见Action一章。
Builtin是内置类型,用于创建可序列化的合约方法参数。参数数组仅支持一层的数组嵌套。详见Action一章。
Contract类初始化时会为三个成员变量复制:
sender- 交易发送方contract- 合约调用方receiver- 交易接收方
一次普通的交易调用合约,上述变量赋值按以下规则:
sender = 交易发送方
contract = receiver = ContractA的地址
在上面的例子中,跨合约调用将使对方合约的上下文更改如下:
sender = ContractA.sender
contract = ContractA的地址
receiver = ContractB的地址
在Damoclis中,所有账户的唯一标识是地址。地址由用户公钥取前20 Bytes生成。在合约中的权限检查本质上是比较地址是否一致。
在Damoclis中,调用合约方法的最小单元是Action。一笔交易中可包含多个Action,执行中若有某个Action执行失败,则回滚整个交易。
在一笔交易执行中,可能会触发多个跨合约调用,我们称为inline_action。它们也被看做是正常的Action,并会在交易执行结束后,收集并保存在交易的inlineActions字段中(该字段不参与交易哈希值计算,故不会改变交易的id)。这么做使得所有对合约的变更操作都留下日志,方便日后的追溯。
Damoclis中的原生代币dom,总量为100亿。它还有unit,munit,kunit三个单位,换算公式如下:
| Symbol | Value |
|---|---|
| unit | 1 |
| kunit | |
| muint | |
| dom | |
| maximum | |
为了方便在合约中的使用,我们提供了Asset类,其构造函数提供两个入参:数量与单位。 |
consta1=newAsset(1000,UNIT);consta2=newAsset(1,KUNIT);a1==a2;// true为合约编译的入口,编写的合约必须继承Contract类。
import{Contract}from"dmc-lib"sender: Address;
receiver: Address;
contract: Address;
actionName: string;
| get访问器 | 描述 |
|---|---|
| get self(): Address | 返回receiver字段 |
HasAuth(addr: Address): boolHasAuth方法用于确认该地址是否为合约的sender。
RequireAuth(addr: Address): void
和HasAuth方法类似,但如果该地址不是合约的sender,则直接抛出异常。
Builtin类主要用于action中的payload等,方便数据的序列化传输。实现了Serializable接口。
import{Builtin}from"dmc-lib"_val: DataStream;
| get访问器 | 描述 |
|---|---|
| get datastream(): DataStream | 返回类属性中的_val |
| get bytesLen(): u32 | 返回_val的长度 |
| 类方法 | 描述 |
|---|---|
| constructor(val: DataStream) | 根据指定的DataStream创建Builtin类 |
| static fromI32(val: i32): Builtin | 根据一个i32类型的值创建Builtin类 |
| static fromU32(val: u32): Builtin | 根据一个u32类型的值创建Builtin类 |
| static fromI64(val: i64): Builtin | 根据一个i64类型的值创建Builtin类 |
| static fromU64(val: u64): Builtin | 同上... |
| static fromI8(val: i8): Builtin | |
| static fromU8(val: u8): Builtin | |
| static fromI16(val: i16): Builtin | |
| static fromU16(val: u16): Builtin | |
| static fromVari32(value: i32): Builtin | |
| static fromVaru32(val: u32): Builtin | |
| static fromAddress(addr: Address): Builtin | |
| static fromString(str: string): Builtin | |
| static fromBytes(bytes: Bytes): Builtin | |
| static fromHash160(bytes: Bytes): Builtin | |
| static fromHash256(bytes: Bytes): Builtin | |
| static fromHash512(bytes: Bytes): Builtin | |
| static fromArray(params: Builtin[]): Builtin |
注意:调用fromArray时,不支持嵌套的数组。
Action类中主要包含调用action相关的方法。实现了Serializable接口。
import{Action}from"dmc-lib"_to: Address;
_value: Asset;
_method: string;
_payload: Builtin[];
_extra: string;
| 类方法 | 描述 |
|---|---|
| constructor(to: Address, value: Asset, method: string, payload: Builtin[] = [], extra: string = "") | 构造一个Action |
| static getValue(): Asset | 获取action的value字段 |
| send(): void | 发送当前action |
注意:调用send时,方法名不得为"__DEPLOY__"。
Address类主要是对于160位地址的封装。实现了Serializable接口。可以直接使用==与!=进行比较。
import{Address}from"dmc-lib"_value: Bytes;
_len: u32;
| get访问器 | 描述 |
|---|---|
| get bytes(): Bytes | 返回类的字节形式 |
| get buffer(): usize | 返回地址字节存储_value的缓冲区地址 |
| 类方法 | 描述 |
|---|---|
| static fromHex(hex: string): Address | 从字符串构造Address |
| static fromBytes(raw: Bytes): Address | 从字节数据构造Address |
| hex(): string | 返回地址的16进制字符串形式 |
| toString(): string | 返回地址的字符串形式 |
| getBalance(): Asset | 获取当前地址的余额 |
| transfer(to: Address, value: Asset): bool | 向to所指地址转账,返回转账是否成功 |
Asset类是对资产的封装,实现了Serializable接口。
import{Asset}from"dmc-lib"amount: u64
| get访问器 | 描述 |
|---|---|
| static get zero(): Asset | 返回数值为0的资产 |
| get bytes(): Bytes | 返回类的字节形式 |
注意: amount为uint单位下的货币数量
| 类方法 | 描述 |
|---|---|
| constructor(amt: u64 = 0, sy: u8 = UNIT) | 根据传入的货币单位与数量构造Asset类 |
此外,Asset类支持的运算符操作包括:
- 比较运算:>, >=, <, <=, ==, !=
- 算数运算:+, -, *, /
提供了一些获取区块链信息的方法。
import{Chain}from"dmc-lib"| 类方法 | 描述 |
|---|---|
| static getBlockHash(height: u64): Bytes | 获取区块哈希 |
| static getBlockHeight(): u64 | 获取区块高度 |
用于数据持久化存储。存储内容为key-value对。存储对象必须实现Serializable接口。
import{Database}from"dmc-lib"_contract: Address;
_table: string;
| 类方法 | 描述 |
|---|---|
| constructor(contract: Address, table: string) | 根据contract以及table构造一个Database类 |
| get(key: string): T | 根据key获取对应的value |
| store(obj: T): void | 存储obj对象 |
| update(obj: T): void | 更新存储内容 |
| exist(key: string): bool | 判断key对应的value是否存在 |
| iterator(): Iterator<T> | 获取迭代器 |
| remove(key: string): void | 根据key删除对应的存储内容 |
用于对数据存储进行迭代。存储对象必须实现Serializable接口。
import{Iterator}from"dmc-lib"_db: Database<T>;
_itr: i32;
| 类方法 | 描述 |
|---|---|
| constructor(db: Database<T>, itr: i32) | 构造一个迭代器。迭代器通常通过Databse里的iterator方法获得。 |
| get(): T | 获取存储对象 |
| next(): void | 将迭代器指向下一个对象地址 |
| end(): bool | 判断是否迭代完毕 |
Prints(msg: string): void
打印出一串字符串。
Printi(val: i64): void
打印出一个i64类型的整数。
Printui(val: u64): void
打印出一个u64类型的整数。
PrintHex(data: Bytes): void
将字节串以16进制字符串的形式输出。
Assert(test: bool, msg: string): void
断言,若test结果为false,会将msg作为错误信息报错。
AssertExit(test: bool, code: i32): void
若test结果为false,会以code作为错误码报错,并退出合约。
Now(): u64
返回当前时间戳。
GenesisTime(): u64
返回创世时间戳。
import{Transaction}from"dmc-lib"| 类方法 | 描述 |
|---|---|
| static getTxHash(): Bytes | 获取交易哈希 |
| static getSignature(): Bytes | 获取交易签名 |
import{Crypto}from"dmc-lib"| 类方法 | 描述 |
|---|---|
| static sha256(data: Bytes): Bytes | 将数据进行sha256加密 |
| static sha512(data: Bytes): Bytes | 将数据进行sha512加密 |
| static ripemd160(data: Bytes): Bytes | 将数据进行ripemd160加密 |
| static recoverKey(sign: Bytes): Bytes | 恢复公钥 |
| static assertSha256(origin: Bytes, hash: Bytes): void | 验证sha256加密结果 |
| static assertSha512(origin: Bytes, hash: Bytes): void | 验证sha512加密结果 |
| static assertRipemd160(origin: Bytes, hash: Bytes): void | 验证ripemd160加密结果 |
| static assertRecoverKey(sign: Bytes, pub: Bytes): void | 验证RecoverKey结果 |
以下均采用小端存储
i8、u8:一个字节
bool: 等同u8 一个字节
i16、u16:两个字节
i32、u32:四个字节
i64、u64:八个字节
Asset类型:等同u64 八个字节
Address:等同于20个字节的字节数组
Hash160: 等同于20个字节的字节数组
Hash256: 等同于32个字节的字节数组
Hash512: 等同于64个字节的字节数组
PublicKey: 等同于20个字节的字节数组
Signature: 等同于96个字节的字节数组
Bytes:字节数组
Bytes类型支持的类方法:
| 类方法名 | 描述 |
|---|---|
| static fromHex(hex:string): Bytes | 从16进制字符串生成字节数组 |
| static fromU8Array(arr: Array<u8>): Bytes | 从u8数组转为字节数组 |
| static fromString(str: string): Bytes | 从utf8字符串转为字节数组 |
| toHex(): string | 从字节数组转为16进制字符串 |
| swapEndian(): Bytes | 字节数组的大小端转换 |
| cloneBytes(): Bytes | 复制一份字节数组 |
| toU8Array(): Array<u8> | 从字节数组转为u8数组 |
| concat(b2: Bytes): Bytes | 将b2连接到该字节数组的后面 |
| wrapDataStream(): DataStream | 将该字节数组包装到一个数据流中(会在一开始加上数组长度) |
| toString(): string | 将字节数组转为utf8字符串 |
| ...... | 其他操作TypedArray的方法,请查看代码补全或源代码 |
DataStream:该类型为对Bytes数组的包装,实现了Serializable接口(该接口可以自动补全,参见:快速入门-数据持久化)的类序列化刀DataStream中,也可从DataStream中反序列化解析出类的内容。具体可以参见源码。
- 对于定长类型,比如u8、u16、i32等整数,以及Asset类型,直接按照小端存储。例如:
- u32:0x 01 00 00 00 表示32位无符号整数1
- Asset: 0x 00 04 00 00 00 00 00 00表示一个Asset类型,其内的token数量以最小单位表示为1024
- 对于不定长类型,包括string、Bytes等等,其编解码规则均为:先利用一个varUint32保存字节长度,再保存具体内容,varUint32的编解码方式见后。例如:
- string: 0x 05 68 65 6c 6c 6f 表示字符串"hello"
- Bytes: 0x 0A 00 00 00 00 1A 4D 01 50 3C 2B 表示一个字节数组,长度为10,内容为: 0x 00 00 00 00 1A 4D 01 50 3C 2B
- 对于Address、Signature等类型,其长度虽然固定,但其本质仍为字节数组,故其编解码方式与字节数组一致,例如:
- Address: 0x 14 10 11 12 13 14 15 16 17 18 19 00 00 00 00 00 00 00 00 00 00 表示一个地址,其内容为:10 11 12 13 14 15 16 17 18 19 00 00 00 00 00 00 00 00 00 00
- 对于实现了Serializable接口的类,其编解码方式按照类内数据的定义顺序进行,例如:
classPersonimplementsSerializable{name: string;age: u64;constructor(name:string,age:u64){this.name=name;this.age=age;}}letdata=newPerson("hello",1)data编码后的内容为: 0x 05 68 65 6c 6c 6f 01 00 00 00 00 00 00 00
readVarint32(): u32{varvalue: u32=0;varshift: u32=0;do{varb=this.read<u8>();value|=<u32>(b&0x7f)<<(7*shift++);}while(b&0x80);returnvalue;}writeVarint32(value: u32): void{do{letb: u8=<u8>value&<u8>0x7f;value>>=7;b|=((value>0 ? 1 : 0)<<7);this.write<u8>(b);}while(value);}