编写智能合约

Nebulas实现了NVM虚拟机来运行智能合约,NVM的实现使用了JavaScript V8引擎,所以当前的开发版,我们可以使用JavaScript、TypeScript来编写智能合约。

编写智能合约的简要规范

  1. 智能合约代码必须是一个Prototype的对象;
  2. 智能合约代码必须有一个init()的方法,这个方法只会在部署的时候被执行一次;
  3. 智能合约里面的私有方法是以_开头的方法,私有方法不能被外部直接调用;

智能合约存储区

星云链智能合约(smart contract)提供了链上数据存储功能。类似于传统的key-value存储系统(eg:redis),可以付费(消耗gas)将数据存储到星云链上。

星云链的智能合约运行环境内置了存储对象==LocalContractStorage==,可以存储数字,字符串,JavaScript对象,存储数据只能在智能合约内使用,其他合约不能读取存储的内容。

基础用法

LocalContractStorage的简单接口包括set,get,del接口,实现了存储,读取,删除数据功能。存储可以是数字,字符串,对象。

LocalContractStorage存储数据

// 存储数据,数据会被json序列化成字符串保存
LocalContractStorage.put(key, value);
// 或者
LocalContractStorage.set(key, value);

LocalContractStorage读取数据

// 获取数据
LocalContractStorage.get(key);

LocalContractStorage删除数据

// 删除数据, 数据删除后无法读取
LocalContractStorage.del(key);
// 或者
LocalContractStorage.delete(key);

高级用法

LocalContractStorage除了基本的set,get,del方法,还提供方法来绑定合约属性。对绑定过的合约属性的读写将直接在LocalContractStorage上读写,而无需调用get和set方法。

绑定属性

在绑定一个合约属性时,需要提供对象实例,属性名和序列化方法。

// SampleContract的`size`属性为存储属性,对`size`的读写会存储到链上,
// 此处的`descriptor`设置为null,将使用默认的JSON.stringify()和JSON.parse()
LocalContractStorage.defineProperty(this, "size", null);

// SampleContract的`value`属性为存储属性,对`value`的读写会存储到链上,
// 此处的`descriptor`自定义实现,存储时直接转为字符串,读取时获得Bignumber对象
LocalContractStorage.defineProperty(this, "value", { // 提供自定义的序列化和反序列化方法
    stringify: function (obj) { // 序列化方法
        return obj.toString();
    },
    parse: function (str) { //反序列化方法
        return new BigNumber(str);
    }
});
// SampleContract的多个属性批量设置为存储属性,对应的descriptor默认使用JSON序列化
LocalContractStorage.defineProperties(this, {
    name: null,
    count: null
});

然后,我们可以如下在合约里直接读写这些属性。

SampleContract.prototype = {
    // 合约部署时调用,部署后无法二次调用
    init: function (name, count, size, value) {
        // 在部署合约时将数据存储到链上
        this.name = name;
        this.count = count;
        this.size = size;
        this.value = value;
    },
    testStorage: function (balance) {
        // 使用value时会从存储中读取链上数据,并根据descriptor设置自动转换为Bignumber
        var amount = this.value.plus(new BigNumber(2));
        if (amount.lessThan(new BigNumber(balance))) {
            return 0
        }
    }
};

绑定Map属性

'use strict';

var SampleContract = function () {
    // 为`SampleContract`定义`userMap`的属性集合,数据可以通过`userMap`存储到链上
    LocalContractStorage.defineMapProperty(this, "userMap");

    // 为`SampleContract`定义`userBalanceMap`的属性集合,并且存储和读取序列化方法自定义
    LocalContractStorage.defineMapProperty(this, "userBalanceMap", {
        stringify: function (obj) {
            return obj.toString();
        },
        parse: function (str) {
            return new BigNumber(str);
        }
    });

    // 为`SampleContract`定义多个集合
    LocalContractStorage.defineMapProperties(this,{
        key1Map: null,
        key2Map: null
    });
};

SampleContract.prototype = {
    init: function () {
    },
    testStorage: function () {
        // 将数据存储到userMap中,并序列化到链上
        this.userMap.set("robin","1");
        // 将数据存储到userBalanceMap中,使用自定义序列化函数,保存到链上
        this.userBalanceMap.set("robin",new BigNumber(1));
    },
    testRead: function () {
        //读取存储数据
        var balance = this.userBalanceMap.get("robin");
        this.key1Map.set("robin", balance.toString());
        this.key2Map.set("robin", balance.toString());
    }
};

module.exports = SampleContract;

Map数据遍历

在智能合约中如果需要遍历map集合,可以采用如下方式:定义两个map,分别是arrayMap,dataMap,arrayMap采用严格递增的计数器作为key,dataMap采用data的key作为key,详细参见set方法。遍历实现参见forEach,先遍历arrayMap,得到dataKey,再对dataMap遍历。Tip:由于Map遍历性能开销比较大,不建议对大数据量map进行遍历,建议按照limit,offset形式进行遍历,否者可能会由于数据过多,导致调用超时。

"use strict";

var SampleContract = function () {
   LocalContractStorage.defineMapProperty(this, "arrayMap");
   LocalContractStorage.defineMapProperty(this, "dataMap");
   LocalContractStorage.defineProperty(this, "size");
};

SampleContract.prototype = {
    init: function () {
        this.size = 0;
    },

    set: function (key, value) {
        var index = this.size;
        this.arrayMap.set(index, key);
        this.dataMap.set(key, value);
        this.size +=1;
    },

    get: function (key) {
        return this.dataMap.get(key);
    },

    len:function(){
      return this.size;
    },

    forEach: function(limit, offset){
        limit = parseInt(limit);
        offset = parseInt(offset);
        if(offset>this.size){
           throw new Error("offset is not valid");
        }
        var number = offset+limit;
        if(number > this.size){
          number = this.size;
        }
        var result  = "";
        for(var i=offset;i

智能合约实例

下面通过实现一个非常简单的功能,来说明智能合约的开发。

功能简要说明

  1. 提交文字信息存储到星云链,包括:标题和内容
  2. 根据作者地址查看提交的信息

    智能合约代码

'use strict';

// 定义信息类
var Info = function (text) {
    if (text) {
        var obj = JSON.parse(text); // 如果传入的内容不为空将字符串解析成json对象
        this.title = obj.title;                     // 标题
        this.content = obj.content;                 // 内容
        this.author = obj.author;                  // 作者
        this.timestamp = obj.timestamp;            // 时间戳
    } else {
        this.title = "";
        this.content = "";
        this.author = "";
        this.timestamp = 0;
    }
};

// 将信息类对象转成字符串
Info.prototype.toString = function () {
    return JSON.stringify(this)
};

// 定义智能合约
var InfoContract = function () {
    // 使用内置的LocalContractStorage绑定一个map,名称为infoMap
    // 这里不使用prototype是保证每布署一次该合约此处的infoMap都是独立的
    LocalContractStorage.defineMapProperty(this, "infoMap", {
        // 从infoMap中读取,反序列化
        parse: function (text) {
            return new Info(text);
        },
        // 存入infoMap,序列化
        stringify: function (o) {
            return o.toString();
        }
    });
};

// 定义合约的原型对象
InfoContract.prototype = {
    // init是星云链智能合约中必须定义的方法,只在布署时执行一次
    init : function () {

    },
    // 提交信息到星云链保存,传入标题和内容
    save : function (title, content) {
        title = title.trim();
        content = content.trim();

        if (title === "" || content === "") {
            throw new Error("标题或内容为空!");
        }

        if (title.length > 64) {
            throw new Error("标题长度超过64个字符!");
        }

        if (content.length > 256) {
            throw new Error("内容长度超过256个字符!");
        }
        // 使用内置对象Blockchain获取提交内容的作者钱包地址
        var from = Blockchain.transaction.from;
        // 此处调用前面定义的反序列方法parse,从存储区中读取内容
        var existInfo = this.infoMap.get(from);
        if (existInfo) {
            throw new Error("您已经发布过内容!");
        }

        var info = new Info();
        info.title = title;
        info.content = content;
        info.timestamp = new Date().getTime();
        info.author = from;

        // 此处调用前面定义的序列化方法stringify,将Info对象存储到存储区
        this.infoMap.put(from, info);
    },
    // 根据作者的钱包地址从存储区读取内容,返回Info对象
    read : function (author) {
        author = author.trim();
        if (author === "") {
            throw new Error("地址为空!");
        }
        // 验证地址
        if (!this.verifyAddress(author)) {
            throw new Error("输入的地址不存在!");
        }
        var existInfo = this.infoMap.get(author);
        return existInfo;
    },
    // 验证地址是否合法
    verifyAddress: function (address) {
        // 1-valid, 0-invalid
        var result = Blockchain.verifyAddress(address);
        return {
            valid: result == 0 ? false : true
        };
    }
};
// 导出代码,标示智能合约入口
module.exports = InfoContract;

部署智能合约

可以在命令行下进行部署,本文介绍在web钱包下的部署方法,操作更简单。

第一步

星云链智能合约开发(六):智能合约开发与部署_第1张图片

第二步

星云链智能合约开发(六):智能合约开发与部署_第2张图片

第三步

星云链智能合约开发(六):智能合约开发与部署_第3张图片

执行智能合约方法

执行save方法,发布信息

星云链智能合约开发(六):智能合约开发与部署_第4张图片

星云链智能合约开发(六):智能合约开发与部署_第5张图片

注意:目的地址为合约地址

执行read方法,查看信息

星云链智能合约开发(六):智能合约开发与部署_第6张图片

星云链智能合约开发(六):智能合约开发与部署_第7张图片