Skip to content

Repository files navigation

Damoclis 智能合约开发文档

[TOC]

快速入门

安装

从Github拉取一份合约项目模板:

git clone https://github.com/damoclis/dmc-smart-contract-boilerplate

进入项目目录,安装依赖库:

npm install

全局安装dmcscript:

npm install dmcscript -g

编写Hello World合约

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.abi
  • wasm为合约的字节码文件,用于部署到区块链上。
  • 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
    • ...

@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/i64u32/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三个单位,换算公式如下:

SymbolValue
unit1
kunit$10^{3}$ unit
muint$10^{6}$ unit
dom$10^{8}$ unit
maximum$10^{10}$ dom
为了方便在合约中的使用,我们提供了Asset类,其构造函数提供两个入参:数量与单位。
consta1=newAsset(1000,UNIT);consta2=newAsset(1,KUNIT);a1==a2;// true

dmc-lib文档

Contract

Contract类

为合约编译的入口,编写的合约必须继承Contract类。

使用示例

import{Contract}from"dmc-lib"

类属性

sender: Address;

receiver: Address;

contract: Address;

actionName: string;

get访问器描述
get self(): Address返回receiver字段

主要类方法

Action

方法

HasAuth(addr: Address): bool

HasAuth方法用于确认该地址是否为合约的sender。

RequireAuth(addr: Address): void

HasAuth方法类似,但如果该地址不是合约的sender,则直接抛出异常。

Builtin类

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类中主要包含调用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

Address类

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

Asset类

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类支持的运算符操作包括:

  • 比较运算:>, >=, <, <=, ==, !=
  • 算数运算:+, -, *, /

Chain

Chain类

提供了一些获取区块链信息的方法。

使用示例

import{Chain}from"dmc-lib"

主要类方法

类方法描述
static getBlockHash(height: u64): Bytes获取区块哈希
static getBlockHeight(): u64获取区块高度

Database

Database类

用于数据持久化存储。存储内容为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删除对应的存储内容

Iterator类

用于对数据存储进行迭代。存储对象必须实现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判断是否迭代完毕

Print

方法

Prints(msg: string): void

打印出一串字符串。

Printi(val: i64): void

打印出一个i64类型的整数。

Printui(val: u64): void

打印出一个u64类型的整数。

PrintHex(data: Bytes): void

将字节串以16进制字符串的形式输出。

System

方法

Assert(test: bool, msg: string): void

断言,若test结果为false,会将msg作为错误信息报错。

AssertExit(test: bool, code: i32): void

若test结果为false,会以code作为错误码报错,并退出合约

Now(): u64

返回当前时间戳。

GenesisTime(): u64

返回创世时间戳。

Transaction

Transaction类

使用示例

import{Transaction}from"dmc-lib"

主要类方法

类方法描述
static getTxHash(): Bytes获取交易哈希
static getSignature(): Bytes获取交易签名

Crypro

Crypto类

使用示例

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

varuint32的编解码方式

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);}

gas扣除逻辑

gas扣除逻辑

About

A TypeScript to WebAssembly compiler 🚀

Resources

Code of conduct

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages