开发手册

1. 简介

NULS智能合约使用Java进行开发,合约运行在NULS虚拟机中。合约开发不能使用所有的Java特性,在第3节列出具体限制。

2. 开发环境

2.1 安装NULS钱包

2.2 安装JDK 8

2.3 安装IntelliJ IDEA

Nuls智能合约使用的开发工具为IntelliJ IDEA。

2.4 NULS智能合约开发工具

NULS智能合约开发工具提供的主要功能:

  • 新建NULS智能合约Maven工程
  • 提供可视化页面来编译、打包、部署合约、调用合约、查询合约相关数据

构建NULS智能合约开发工具的说明文档

3. NULS智能合约规范与语法

Nuls智能合约语法是Java语法的一个子集,在Java语法上做了一些限制。

3.1 NULS智能合约规范

合约主类必须实现Contract接口,一个智能合约只能有一个类实现Contract接口,其他类和接口都是为这个合约提供功能的。

3.2 关键字

下面列出Java关键字,其中将标注NULS智能合约不支持的关键字

访问控制

  • public
  • protected
  • private

定义类、接口、抽象类和实现接口、继承类的关键字、实例化对象

  • class
  • interface
  • abstract
  • implements
  • extends
  • new

包的关键字

  • import
  • package

数据类型的关键字

  • byte
  • char
  • boolean
  • short
  • int
  • float
  • long
  • double
  • void
  • null
  • true
  • false

条件循环(流程控制)

  • if
  • else
  • while
  • for
  • switch
  • case
  • default
  • do
  • break
  • continue
  • return
  • instanceof

错误处理

  • catch
  • try
  • finally
  • throw
  • throws

修饰方法、类、属性和变量

  • static
  • final
  • super
  • this
  • native(不支持)
  • strictfp(不支持)
  • synchronized(不支持)
  • transient(不支持)
  • volatile(不支持)

其他

  • enum(不支持)
  • assert(不支持)

3.3 基本语法

下面的语法与Java相同,只是简单列出,具体可参考Java相关文档

  • 标识符:由字符、下划线、美元符或数字组成,以字符、下划线、美元符开头
  • 基本数据类型:byte short int long float double char boolean
  • 引用数据类型:类、接口、数组
  • 算术运算符:+ - * / % ++ --
  • 关系运算符:> < >= <= == !=
  • 逻辑运算符:! & | ^ && ||
  • 位运算符:& | ^ ~ >> << >>>
  • 赋值运算符:=
  • 拓展赋值运算符:+ = -= *= /=
  • 字符串链接运算符:+
  • 三目条件运算符 ? :
  • 流程控制语句(if,switch,for,while,do...while)

3.4 支持的类

Nuls智能合约只能使用下面的类进行开发

  • io.nuls.contract.sdk.Address
  • io.nuls.contract.sdk.Block
  • io.nuls.contract.sdk.BlockHeader
  • io.nuls.contract.sdk.Contract
  • io.nuls.contract.sdk.Event
  • io.nuls.contract.sdk.Msg
  • io.nuls.contract.sdk.Utils
  • io.nuls.contract.sdk.annotation.View
  • io.nuls.contract.sdk.annotation.Required
  • io.nuls.contract.sdk.annotation.Payable
  • java.lang.Boolean
  • java.lang.Byte
  • java.lang.Short
  • java.lang.Character
  • java.lang.Integer
  • java.lang.Long
  • java.lang.Float
  • java.lang.Double
  • java.lang.String
  • java.lang.StringBuilder
  • java.math.BigInteger
  • java.math.BigDecimal
  • java.util.Collection
  • java.util.List
  • java.util.ArrayList
  • java.util.LinkedList
  • java.util.Map
  • java.util.HashMap
  • java.util.LinkedHashMap
  • java.util.Set
  • java.util.HashSet

3.5 其他限制

  • 合约类只能有一个构造方法,其他类不限制
  • 执行一次合约方法最大的Gas消耗是1000万,请保证尽可能的优化合约代码
  • 执行一次@View类型的方法调用,最大的Gas消耗是1亿,请保证尽可能的优化合约代码

4. NULS智能合约简单示例

一个简单的合约

合约主类必须实现Contract接口,其他类和接口都是为这个合约提供功能的。

package contracts.examples;

import io.nuls.contract.sdk.Contract;
import io.nuls.contract.sdk.annotation.Payable;
import io.nuls.contract.sdk.annotation.Required;
import io.nuls.contract.sdk.annotation.View;

public class SimpleStorage implements Contract {

    private String storedData;

    /**
     * 调用后合约状态不会改变,可以通过这种方法查询合约状态
     */
    @View
    public String getStoredData() {
        return storedData;
    }

    /**
     * 标记@Payable的方法,才能在调用时候传入NULS金额
     */
    @Payable
    public void setStoredData(@Required String storedData) {
        this.storedData = storedData;
    }
    
    /**
     * 返回值会被VM自动JSON序列化,以JSON字符串的形式返回
     * 注意:对象层级不得超过3层,超过3层的部分会调用对象的toString方法,不会再继续序列化
     */
    @JSONSerializable
    public Map vJsonSerializableMap() {
        Map map = new HashMap();
        map.put("name", "nuls");
        map.put("url", "https://nuls.io");
        return map;
    }

}

合约写好后,编译打包,部署到NULS链上时候,虚拟机会执行合约的构造方法初始化这个合约,并把这个合约状态保存在链上,合约状态是合约类的所有成员变量。 合约部署好以后,合约类的所有public方法都是能调用的,通过调用这些方法读取或修改合约状态。

注解说明

@JSONSerializable 标记@JSONSerializable的方法,返回值会被VM自动JSON序列化,以JSON字符串的形式返回。

注意:对象层级不得超过3层,超过3层的部分会调用对象的toString方法,不会再继续序列化。

@View 标记@View的方法,调用后合约状态不会改变,可以通过这种方法查询合约状态。

@Payable 标记@Payable的方法,才能在调用时候传入NULS金额

@Required 标记@Required的参数,调用时候必须传入值,若不想传递未标记此注解的参数,需要填入0或者null占位

Github上里面有一些合约示例。

NULS合约示例收集

NULS合约示例 - NRC20

NULS合约示例 - NRC721

NULS合约示例 - POCM

5. NULS Contract SDK

合约SDK提供了几个类,方便合约开发:

io.nuls.contract.sdk.Address

public class Address {

    private final String address;

    public Address(String address) {
        valid(address);
        this.address = address;
    }

    /**
     * 获取该地址的可用余额
     *
     * @return BigInteger
     */
    public native BigInteger balance();

    /**
     * 获取该地址的总余额
     *
     * @return BigInteger
     */
    public native BigInteger totalBalance();

    /**
     * 合约向该地址转账
     *
     * @param value 转账金额(多少Na)
     */
    public native void transfer(BigInteger value);

    /**
     * 调用该地址的合约方法
     *
     * @param methodName 方法名
     * @param methodDesc 方法签名
     * @param args       参数
     * @param value      附带的货币量(多少Na)
     */
    public native void call(String methodName, String methodDesc, String[][] args, BigInteger value);

    /**
     * 调用该地址的合约方法并带有返回值(String)
     *
     * @param methodName 方法名
     * @param methodDesc 方法签名
     * @param args       参数
     * @param value      附带的货币量(多少Na)
     * @return 调用合约后的返回值
     */
    public native String callWithReturnValue(String methodName, String methodDesc, String[][] args, BigInteger value);

    /**
     * 验证地址
     *
     * @param address 地址
     */
    private native void valid(String address);

    /**
     * 验证地址是否是合约地址
     *
     */
    public native boolean isContract();

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        Address address1 = (Address) o;
        return address != null ? address.equals(address1.address) : address1.address == null;
    }

    @Override
    public int hashCode() {
        return address != null ? address.hashCode() : 0;
    }

    @Override
    public String toString() {
        return address;
    }

}

io.nuls.contract.sdk.Block

public class Block {

    /**
     * 给定块的区块头
     *
     * @param blockNumber 区块高度
     * @return 给定块的区块头
     */
    public static native BlockHeader getBlockHeader(long blockNumber);

    /**
     * 当前块的区块头
     *
     * @return 当前块的区块头
     */
    public static native BlockHeader currentBlockHeader();

    /**
     * 最新块的区块头
     *
     * @return 最新块的区块头
     */
    public static native BlockHeader newestBlockHeader();

    /**
     * 给定块的哈希值
     * hash of the given block
     *
     * @param blockNumber
     * @return 给定块的哈希值
     */
    public static String blockhash(long blockNumber) {
        return getBlockHeader(blockNumber).getHash();
    }

    /**
     * 当前块矿工地址
     * current block miner’s address
     *
     * @return 地址
     */
    public static Address coinbase() {
        return currentBlockHeader().getPackingAddress();
    }

    /**
     * 当前块编号
     * current block number
     *
     * @return number
     */
    public static long number() {
        return currentBlockHeader().getHeight();
    }

    /**
     * 当前块时间戳
     * current block timestamp
     *
     * @return timestamp
     */
    public static long timestamp() {
        return currentBlockHeader().getTime();
    }
    
}

io.nuls.contract.sdk.BlockHeader

public class BlockHeader {

    private String hash;
    private long time;
    private long height;
    private long txCount;
    private Address packingAddress;
    private String stateRoot;

    public String getHash() {
        return hash;
    }

    public long getTime() {
        return time;
    }

    public long getHeight() {
        return height;
    }

    public long getTxCount() {
        return txCount;
    }

    public Address getPackingAddress() {
        return packingAddress;
    }

    public String getStateRoot() {
        return stateRoot;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        BlockHeader that = (BlockHeader) o;

        if (time != that.time) return false;
        if (height != that.height) return false;
        if (txCount != that.txCount) return false;
        if (hash != null ? !hash.equals(that.hash) : that.hash != null) return false;
        if (packingAddress != null ? !packingAddress.equals(that.packingAddress) : that.packingAddress != null)
            return false;
        return stateRoot != null ? stateRoot.equals(that.stateRoot) : that.stateRoot == null;
    }

    @Override
    public int hashCode() {
        int result = hash != null ? hash.hashCode() : 0;
        result = 31 * result + (int) (time ^ (time >>> 32));
        result = 31 * result + (int) (height ^ (height >>> 32));
        result = 31 * result + (int) (txCount ^ (txCount >>> 32));
        result = 31 * result + (packingAddress != null ? packingAddress.hashCode() : 0);
        result = 31 * result + (stateRoot != null ? stateRoot.hashCode() : 0);
        return result;
    }

    @Override
    public String toString() {
        return "BlockHeader{" +
                "hash='" + hash + '\'' +
                ", time=" + time +
                ", height=" + height +
                ", txCount=" + txCount +
                ", packingAddress=" + packingAddress +
                ", stateRoot='" + stateRoot + '\'' +
                '}';
    }

}

io.nuls.contract.sdk.Contract

/**
 * 合约接口,合约类实现这个接口
 */
public interface Contract {

    /**
     * 直接向合约转账,会触发这个方法,默认不做任何操作,可以重载这个方法。
     * 前提: 需重载这个方法,并且标记`@Payable`注解
     */
    default void _payable() {
    }

    /**
     * 1. 当共识节点奖励地址是合约地址时,会触发这个方法,参数是区块奖励地址明细二维数组数据 eg. [[address, amount], [address, amount], ...]
     * 2. 当委托节点地址是合约地址时,会触发这个方法,参数是合约地址和奖励金额二维数组数据 eg. [[address, amount]]
     * 前提: 需重载这个方法,并且标记`@Payable`注解
     */
    default void _payable(String[][] args) {
    }

}

io.nuls.contract.sdk.Event

/**
* 事件接口,事件类实现这个接口
*/
public interface Event {
}

io.nuls.contract.sdk.Msg

public class Msg {

    /**
     * 剩余Gas
     * remaining gas
     *
     * @return 剩余gas
     */
    public static native long gasleft();

    /**
     * 合约发送者地址
     * sender of the contract
     *
     * @return 消息发送者地址
     */
    public static native Address sender();

    /**
     * 合约发送者地址公钥
     * sender public key of the contract
     *
     * @return 消息发送者地址公钥
     */
    public static native String senderPublicKey();

    /**
     * 合约发送者转入合约地址的Nuls数量,单位是Na,1Nuls=1亿Na
     * The number of Nuls transferred by the contract sender to the contract address, the unit is Na, 1Nuls = 1 billion Na
     *
     * @return
     */
    public static native BigInteger value();

    /**
     * Gas价格
     * gas price
     *
     * @return Gas价格
     */
    public static native long gasprice();

    /**
     * 合约地址
     * contract address
     *
     * @return 合约地址
     */
    public static native Address address();

}

io.nuls.contract.sdk.Utils

public class Utils {

    private Utils() {
    }

    /**
     * 检查条件,如果条件不满足则回滚
     *
     * @param expression
     */
    public static void require(boolean expression) {
        if (!expression) {
            revert();
        }
    }

    /**
     * 检查条件,如果条件不满足则回滚
     *
     * @param expression
     * @param errorMessage
     */
    public static void require(boolean expression, String errorMessage) {
        if (!expression) {
            revert(errorMessage);
        }
    }

    /**
     * 终止执行并还原改变的状态
     */
    public static void revert() {
        revert(null);
    }

    /**
     * 终止执行并还原改变的状态
     *
     * @param errorMessage
     */
    public static native void revert(String errorMessage);

    /**
     * 发送事件
     *
     * @param event
     */
    public static native void emit(Event event);

	/**
     * @param seed a private seed
     * @return pseudo random number (0 ~ 1)
     */
    public static float pseudoRandom(long seed) {
        int hash1 = Block.currentBlockHeader().getPackingAddress().toString().substring(2).hashCode();
        int hash2 = Msg.address().toString().substring(2).hashCode();
        int hash3 = Msg.sender() != null ? Msg.sender().toString().substring(2).hashCode() : 0;
        int hash4 = Long.valueOf(Block.timestamp()).toString().hashCode();

        long hash = seed ^ hash1 ^ hash2 ^ hash3 ^ hash4;

        seed = (hash * 0x5DEECE66DL + 0xBL) & ((1L << 48) - 1);
        return ((int) (seed >>> 24) / (float) (1 << 24));
    }

    /**
     * @return pseudo random number (0 ~ 1)
     */
    public static float pseudoRandom() {
        return pseudoRandom(0x5DEECE66DL);
    }

    /**
     *
     * Please note that this is the SHA-3 FIPS 202 standard, not Keccak-256.
     *
     * @param src source string (hex encoding string)
     * @return sha3-256 hash (hex encoding string)
     */
    public static native String sha3(String hexString);

    /**
     *
     * Please note that this is the SHA-3 FIPS 202 standard, not Keccak-256.
     *
     * @param bytes source byte array
     * @return sha3-256 hash (hex encoding string)
     */
    public static native String sha3(byte[] bytes);
    
    /**
     * [Testnet]verify signature data(ECDSA)
     *
     * @param data(hex encoding string)
     * @param signature(hex encoding string)
     * @param pubkey(hex encoding string)
     * @return verify result
     */
    public static native boolean verifySignatureData(String data, String signature, String pubkey);

    /**
     * [Testnet]根据截止高度和原始种子数量,用特定的算法生成一个随机种子
     *
     * @param endHeight 截止高度
     * @param seedCount 原始种子数量
     * @param algorithm hash算法标识
     * @return 原始种子字节数组合并后, 使用hash算法得到32位hash字节数组, 再转化为BigInteger(new BigInteger(byte[] bytes))
     */
    public static native BigInteger getRandomSeed(long endHeight, int seedCount, String algorithm);

    /**
     * [Testnet]根据截止高度和原始种子数量,用`SHA3-256`hash算法生成一个随机种子
     *
     * @param endHeight 截止高度
     * @param seedCount 原始种子数量
     * @return 原始种子字节数组合并后, 使用`SHA3-256`hash算法得到32位hash字节数组, 再转化为BigInteger(new BigInteger(byte[] bytes))
     */
    public static BigInteger getRandomSeed(long endHeight, int seedCount) {
        return getRandomSeed(endHeight, seedCount, "SHA3");
    }

    /**
     * [Testnet]根据高度范围,用特定的算法生成一个随机种子
     *
     * @param startHeight 起始高度
     * @param endHeight   截止高度
     * @param algorithm   hash算法标识
     * @return 原始种子字节数组合并后, 使用hash算法得到32位hash字节数组, 再转化为BigInteger(new BigInteger(byte[] bytes))
     */
    public static native BigInteger getRandomSeed(long startHeight, long endHeight, String algorithm);

    /**
     * [Testnet]根据高度范围,用`SHA3-256`hash算法生成一个随机种子
     *
     * @param startHeight 起始高度
     * @param endHeight   截止高度
     * @return 原始种子字节数组合并后, 使用`SHA3-256`hash算法得到32位hash字节数组, 再转化为BigInteger(new BigInteger(byte[] bytes))
     */
    public static BigInteger getRandomSeed(long startHeight, long endHeight){
        return getRandomSeed(startHeight, endHeight, "SHA3");
    }

    /**
     * [Testnet]根据截止高度和原始种子数量,获取原始种子的集合
     *
     * @param endHeight 截止高度
     * @param seedCount 原始种子数量
     * @return 返回原始种子的集合,元素是字节数组转化的BigInteger(new BigInteger(byte[] bytes))
     */
    public static native List<BigInteger> getRandomSeedList(long endHeight, int seedCount);

    /**
     * [Testnet]根据高度范围,获取原始种子的集合
     *
     * @param startHeight 起始高度
     * @param endHeight   截止高度
     * @return 返回原始种子的集合,元素是字节数组转化的BigInteger(new BigInteger(byte[] bytes))
     */
    public static native List<BigInteger> getRandomSeedList(long startHeight, long endHeight);
    
    /**
     * 调用链上其他模块的命令
     *
     * @see <a href="https://docs.nuls.io/zh/NULS2.0/vm-sdk.html">调用命令详细说明</a>
     * @param cmdName 命令名称
     * @param args 命令参数
     * @return 命令返回值(根据注册命令的返回类型可返回字符串,字符串数组,字符串二维数组)
     */
    public static native Object invokeExternalCmd(String cmdName, String[] args);
    
    /**
     * 把对象转换成json字符串
     * 注意:对象内如果包含复杂对象,序列化深度不得超过3级
     *
     * @param obj
     * @return json字符串
     */
    public static native String obj2Json(Object obj);
}

io.nuls.contract.sdk.annotation.Payable

@Payable 标记@Payable的方法,才能在调用时候转入NULS金额

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Payable {
}

io.nuls.contract.sdk.annotation.Required

@Required 标记@Required的参数,调用时候必须传入值, 未标记此注解的参数,若不想传递参数,需要填入0或者null占位

@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Required {
}

io.nuls.contract.sdk.annotation.View

@View 标记@View的方法,调用后合约状态不会改变,可以通过这种方法查询合约状态

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface View {
}

io.nuls.contract.sdk.annotation.JSONSerializable

@JSONSerializable 标记@JSONSerializable的方法,返回值会被VM自动JSON序列化,以JSON字符串的形式返回。

注意:对象层级不得超过3层,超过3层的部分会调用对象的toString方法,不会再继续序列化。

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface JSONSerializable {
}

6. 智能合约主要的API

在NULS2.0模块NULS-API中,我们提供了大部分常用的API,请参考该文档中智能合约部分。

NULS-API_JSONRPC

NULS-API_RESTFUL

7. 智能合约方法参数传递的一些说明

智能合约的方法中如果有数组类型的参数,请使用如下方式传递参数

参考投票合约代码中的create方法

{
  "sender": "NsdtydTVWskMc7GkZzbsq2FoChqKFwMf",
  "password": "",
  "contractAddress": "NseLt14NacjTDhXaTXUdrk6VF7aEwtW4",
  "gasLimit": 200000,
  "price": 1,
  "value": 10000000000,
  "methodName": "create",
  "methodDesc": "",
  "remark": "",
  "args": [
    "测试投票1",
    "第一个投票合约",
    [
      "第一个选项",
      "第二个选项",
      "第三个选项"
    ],
    1536044066056, 1536184066056, false, 300, false
  ]
}
Last Updated: 2019-9-10 17:23:55