[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";
class HelloWorld extends Contract {
// 定义合约方法`hi`
@action
hi(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";
class HelloWorld extends Contract {
// 定义合约方法`hi`
@action
hi(msg: string): void {
// 打印日志
Prints(msg);
}
// 定义合约方法`transfer`
@action
transfer(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")
class Person implements Serializable {
@key
name: string
age: u32;
}Serializable是内置的序列化接口,所有需要持久化的数据类均须声明实现该接口,但无需开发者手动实现,编译器会帮助自动补全代码。
在Damoclis中,合约层的数据库模型如下:
- Contract
- table 1
- data1
- data2
- ...
- dataN
- table 2
- table 3
- ...
- table 1
@database("person")装饰器用于表明该数据类存储在person表内,编译器会识别并生成对应ABI。
@key装饰器用于表明该数据项的主键。数据库底层实际为kv存储,k为@key声明的主键,v为整个数据对象的序列化字符串。
然后,在合约类中添加下列方法:
@action
store(): void {
const person = new Person();
person.name = "bob";
person.age = 16;
// 创建一个数据项实例
// 第一个参数为数据库所在合约,第二个参数为数据表名称
const db = new Database<Person>(this.receiver, "person");
// 写操作只对合约本身的数据表生效
db.store(person);
}完整代码如下:
import { Contract, Database } from "../../src/database";
// 声明数据表person的数据类
@database("person")
class Person implements Serializable {
@key
name: string
age: u32;
}
class DBTest extends Contract {
_db: Database<Person>
constructor() {
// 初始化数据库实例
this._db = new Database<Person>(this.self, "person");
}
@action
store(): void {
const person = new Person();
person.name = "bob";
person.age = 16;
// 创建一个数据项实例
// 写操作只对合约本身的数据表生效
this._db.store(person);
}
}Damoclis设计了返回值机制,将合约的运行结果返回给区块链,并保存在交易回执中。
class ReturnTest extends Contract {
// 定义add方法,返回一个u64类型的值。
@action
add(a: u64, b: 64): u64{
return Safemath.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.ts
import { Contract, Action } from "dmc-lib";
class ContractA extends Contract {
@action
call(target: Address):void {
// 构建需要发送的内联action
const action = new Action(target, Asset.zero, "hi", [Builtin.fromString("hello")]);
// 发送action,调用对方合约方法
action.send();
}
}
// contractB.ts
import { Contract, Prints } from "dmc-lib";
class ContractB extends Contract {
@action
hi(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类,其构造函数提供两个入参:数量与单位。 |
const a1 = new Asset(1000, UNIT);
const a2 = new Asset(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接口的类,其编解码方式按照类内数据的定义顺序进行,例如:
class Person implements Serializable {
name: string;
age: u64;
constructor(name:string, age:u64){
this.name=name;
this.age=age;
}
}
let data=new Person("hello",1)data编码后的内容为: 0x 05 68 65 6c 6c 6f 01 00 00 00 00 00 00 00
readVarint32(): u32 {
var value: u32 = 0;
var shift: u32 = 0;
do {
var b = this.read<u8>();
value |= <u32>(b & 0x7f) << (7 * shift++);
} while (b & 0x80);
return value;
}
writeVarint32(value: u32): void {
do {
let b: u8 = <u8>value & <u8>0x7f;
value >>= 7;
b |= ((value > 0 ? 1 : 0) << 7);
this.write<u8>(b);
} while (value);
}