5. 使用Java进行智能合约开发

读者对象:本章节主要描述使用Java进行ChainMaker合约编写的方法,主要面向于使用Java进行ChainMaker的合约开发的开发者。

5.1. 环境依赖

  1. 软件依赖

    当前合约执行环境为JDK 11,推荐使用JDK 11编写合约。

    推荐使用 IDEA 或 vscode等IDE编写和编译Java合约。

  2. 长安链环境准备

    准备一条支持Docker_VM的长安链,以及长安链CMC工具,用于将写编写好的合约,部署到链上进行测试。相关安装教程请详见:

5.2. 编写Java合约

5.2.1. 引用合约sdk

5.2.1.1. 方式1

5.2.1.1.1. 在gradle项目中使用SDK

在build.gradle 中添加dependencies

implementation group: 'org.chainmaker', name: 'contracts-sdk-java', version: '1.0'
5.2.1.1.2. 在Maven项目中使用SDK
<dependency>
    <groupId>org.chainmaker</groupId>
    <artifactId>contracts-sdk-java</artifactId>
    <version>1.0</version>
</dependency>

5.2.1.2. 方式2

将项目引入到本地并编译安装到本地maven库:

git clone -b v3.0.0 https://git.chainmaker.org.cn/chainmaker/contract-sdk-java.git
cd contract-sdk-java
mvn clean install  '-Dmaven.test.skip=true'

然后通过方式1引入到合约开发项目。

5.2.2. 合约编写规则

5.2.2.1. 1. 合约必须实现接口 IContract

合约必须需要实现合约初始化方法(initContract) 和合约升级方法(upgradeContract)。

IContract 中定义了默认的 initContract 和 upgradeContract 的接口。默认这两个接口只返回成功的message。

合约可以根据需求自行实现这两个接口。

5.2.2.2. 2. 代码的执行入口

在合约类定义的main方法中,需要将合约实例作为参数传给sanbox.serve

public class fact implements IContract {

    // ... 

    public static void main(String[] args) {
        Sandbox.serve(args, new fact());
    }
}

5.2.2.3. 定义合约方法

在合约类中定义合约方法,方法权限必须是public,返回值为Response,并且使用@ContractMethod注解标识。在链上调用中,指定的合约方法名与定义的方法名同名(区分大小写)。

    @ContractMethod
    public Response save() throws IOException, ContractException {

        SDK.putState("key1", "field1", SDK.getParam("value"));

        return SDK.success("store success");
    }

5.2.2.4. 获取合约的调用参数

  1. 使用SDK.getArgs()获取参数map

     // 获取参数map
     Map<String, ByteString> args = SDK.getArgs();
     // 获取参数 key的值, 如果不存在则报错
     ByteString getKey = args.get("key");
     if (getKey == null) {
         return SDK.error("key not exist");
     }
     String key = getKey.toStringUtf8();
     // 获取参数 value 的值, 如果不存在使用空字符串
     ByteString getField = args.get("field");
     String field = getField == null ? "" : getField.toStringUtf8();
    
  2. 使用SDK.getParam() 和 SDK.getParamBytes()

    注意:getParam 和 getParamBytes 不做null值的判断,假如参数值没有传递,则分别返回空字符串和空字节数组

 String key = SDK.getParam("key");
 byte[] value = SDK.getParamBytes("value");

5.2.2.5. 错误捕获

合约报错中ContractException中包含了正常逻辑的报错,主要包括key field的合法检查,storemap的参数检查,以及链上接口返回的报错等。 在合约逻辑中,建议捕获ContractException,并做相应的处理。

5.2.3. 编译合约

合约需要编译成可独立运行的jar包(包含运行所需的依赖,也叫fat jar 或uber jar)。

5.2.3.1. gradle 打包方式

配置示例如下,执行./gradlew uberJar,默认生成的jar包在build/libs目录下,文件名以-uber.jar为结尾。

tasks.register('uberJar', Jar) {
    archiveClassifier = 'uber'
    duplicatesStrategy = DuplicatesStrategy.INCLUDE

    manifest {
        attributes "Main-Class": "org.example.fvt"
    }

    from sourceSets.main.output

    dependsOn configurations.runtimeClasspath
    from {
        configurations.runtimeClasspath.findAll { it.name.endsWith('jar') }.collect { zipTree(it) }
    }

    exclude 'META-INF/*.RSA'
    exclude 'META-INF/*.SF'
    exclude 'META-INF/*.DSA'

    with jar
}

5.2.3.2. maven 打包方式

使用maven-shade-plugin。配置示例如下。

执行 mvn package,默认生成的jar包在target目录下。

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-shade-plugin</artifactId>
    <version>3.4.0</version>
    <executions>
        <execution>
            <phase>package</phase>
            <goals>
                <goal>shade</goal>
            </goals>
            <configuration>
                <filters>
                    <filter>
                        <artifact>*:*</artifact>
                        <excludes>
                            <exclude>META-INF/*.SF</exclude>
                            <exclude>META-INF/*.DSA</exclude>
                            <exclude>META-INF/*.RSA</exclude>
                        </excludes>
                    </filter>
                </filters>

                <transformers>
                    <transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
                    <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                      <!-- 请根据合约修改mainClass -->  
                      <mainClass>org.chainmaker.examples.demo</mainClass>
                    </transformer>
                </transformers>
            </configuration>
        </execution>
    </executions>
</plugin>

5.2.4. 部署调用合约

部署合约的使用教程可详见:部署示例合约

5.3. 示例合约使用演示

5.3.1. 示例代码

package org.chainmaker.examples;

import com.google.protobuf.ByteString;
import org.chainmaker.contracts.docker.java.pb.proto.Response;
import org.chainmaker.contracts.docker.java.sandbox.IContract;
import org.chainmaker.contracts.docker.java.sandbox.ContractException;
import org.chainmaker.contracts.docker.java.sandbox.SDK;
import org.chainmaker.contracts.docker.java.sandbox.Sandbox;
import org.chainmaker.contracts.docker.java.sandbox.annotaion.ContractMethod;

import java.io.IOException;

// 实现智能合约接口 IContract
public class fact implements IContract {

    // 使用@ContractMethod注解定义合约方法:save 
    @ContractMethod
    public Response save() throws IOException, ContractException {
        // 获取参数:key
        String key = SDK.getParam("key");
        // 获取参数:value
        byte[] value = SDK.getParamBytes("value");

        // 将键值对存储到链上
        SDK.putState(key, value);

        // 返回成功信息
        return SDK.success("store success");
    }

    // 定义合约方法:get
    @ContractMethod
    public Response get() throws IOException, InterruptedException, ContractException {
        // 获取参数:key
        String key = SDK.getParam("key");
        // 从链上获取对应的值并返回
        return SDK.success(SDK.getState(key));
    }

    // 主函数,启动智能合约
    public static void main(String[] args) {
        Sandbox.serve(args, new fact());
    }

}

更多合约示例可以直接查看仓库的样例contracts-sdk-java

5.3.2. 部署调用示例合约

5.3.2.1. 使用cmc工具部署调用合约

## 创建合约
./cmc client contract user create \
--contract-name=fact \
--runtime-type=DOCKER_JAVA \
--byte-code-path=./testdata/docker-java-demo/fact.jar \
--version=1.0 \
--sdk-conf-path=./testdata/sdk_config.yml \
--admin-key-file-paths=./testdata/crypto-config/wx-org1.chainmaker.org/user/admin1/admin1.tls.key,./testdata/crypto-config/wx-org2.chainmaker.org/user/admin1/admin1.tls.key,./testdata/crypto-config/wx-org3.chainmaker.org/user/admin1/admin1.tls.key,./testdata/crypto-config/wx-org4.chainmaker.org/user/admin1/admin1.tls.key \
--admin-crt-file-paths=./testdata/crypto-config/wx-org1.chainmaker.org/user/admin1/admin1.tls.crt,./testdata/crypto-config/wx-org2.chainmaker.org/user/admin1/admin1.tls.crt,./testdata/crypto-config/wx-org3.chainmaker.org/user/admin1/admin1.tls.crt,./testdata/crypto-config/wx-org4.chainmaker.org/user/admin1/admin1.tls.crt \
--sync-result=true \
--params="{}"

## 调用合约
./cmc client contract user invoke \
--contract-name=fact \
--method=save \
--sdk-conf-path=./testdata/sdk_config.yml \
--params="{\"key\":\"key00\",\"value\":\"value00\"}" \
--sync-result=true

## 查询合约
./cmc client contract user get \
--contract-name=fact \
--method=get \
--sdk-conf-path=./testdata/sdk_config.yml \
--params="{\"key\":\"key00\"}"

5.4. 接口描述

用户与链交互接口

SDK:

        /**
     * get the transaction's tx id
     *
     * @return string
     */
    public static String getTxId() {}

    /**
     * get state by key and field
     *
     * @param key
     * @param field
     * @return value
     * @throws InterruptedException
     * @throws IOException
     * @throws ContractException
     */
    public static byte[] getState(String key, String field) throws InterruptedException, IOException, ContractException {}

    /**
     * get state by key
     *
     * @param key
     * @return value
     * @throws IOException
     * @throws ContractException
     * @throws InterruptedException
     */
    public static byte[] getState(String key) throws IOException, ContractException, InterruptedException {}


    /**
     * put state by key and field
     *
     * @param key
     * @param field
     * @param value
     * @throws IOException
     * @throws ContractException
     */
    public static void putState(String key, String field, byte[] value) throws IOException, ContractException {}


    /**
     * put state by key and field
     *
     * @param key
     * @param field
     * @param value
     * @throws IOException
     * @throws ContractException
     */
    public static void putState(String key, String field, String value) throws IOException, ContractException {}


    /**
     * put state by key
     *
     * @param key
     * @param value
     * @throws IOException
     * @throws ContractException
     */
    public static void putState(String key, String value) throws IOException, ContractException {}


    /**
     * put state by key
     *
     * @param key
     * @param value
     * @throws IOException
     * @throws ContractException
     */
    public static void putState(String key, byte[] value) throws IOException, ContractException {}


    /**
     * del state by key and field
     *
     * @param key
     * @param field
     * @throws ContractException
     * @throws IOException
     */
    public static void delState(String key, String field) throws ContractException, IOException {}


    /**
     * del state by key
     *
     * @param key
     * @throws ContractException
     * @throws IOException
     */
    public static void delState(String key) throws ContractException, IOException {}


    /**
     * cross call contract
     *
     * @param contractName
     * @param method
     * @param args
     * @return tx response
     */
    public static Response callContract(String contractName, String method, Map<String, ByteString> args) throws InterruptedException, InvalidProtocolBufferException {}

    /**
     * get param bytes by key
     *
     * @param key
     * @return value bytes
     */
    public static byte[] getParamBytes(String key) {}

    /**
     * get param string by key
     *
     * @param key
     * @return value string
     */
    public static String getParam(String key) {}


    /**
     * get args
     *
     * @return Map<String, ByteString>
     */
    public static Map<String, ByteString> getArgs() {}


    /**
     * emit event
     *
     * @param topic string
     * @param data  string[]
     */
    public static void emitEvent(String topic, String[] data) {}


    /**
     * new iterator
     *
     * @param startKey
     * @param limitKey
     * @return iterator
     * @throws ContractException
     * @throws InterruptedException
     */
    public static ResultSetKV newIterator(String startKey, String limitKey) throws ContractException, InterruptedException {}


    /**
     * new iterator with field
     *
     * @param key
     * @param startField
     * @param limitField
     * @return iterator
     * @throws ContractException
     * @throws InterruptedException
     */
    public static ResultSetKV newIteratorWithField(String key, String startField, String limitField) throws ContractException, InterruptedException {}


    /**
     * new iterator with prefix combined key and field
     *
     * @param startKey
     * @param startField
     * @return iterator
     * @throws ContractException
     * @throws InterruptedException
     */
    public static ResultSetKV newIteratorPrefixWithKeyField(String startKey, String startField) throws ContractException, InterruptedException {}

    /**
     * new iterator with prefix key
     *
     * @param key
     * @return iterator
     * @throws ContractException
     * @throws InterruptedException
     */
    public static ResultSetKV newIteratorPrefixWithKey(String key) throws ContractException, InterruptedException {}


    /**
     * new history iterator for key
     *
     * @param key
     * @param field
     * @return iterator
     * @throws ContractException
     * @throws InterruptedException
     */
    public static KeyHistoryKvIter newHistoryKvIterForKey(String key, String field) throws ContractException, InterruptedException {}


    /**
     * get batch state
     *
     * @param keys
     * @return values
     * @throws IOException
     * @throws ContractException
     * @throws InterruptedException
     */
    public static List<BatchKey> getBatchState(List<BatchKey> keys) throws IOException, ContractException, InterruptedException {}


    /**
     * origin is the identification of the tx origin sender(who sign the tx)
     *
     * @return origin
     * @throws InterruptedException
     * @throws ContractException
     */
    public static String origin() throws InterruptedException, ContractException {}


    /**
     * sender is the identification of the caller (who send the invoke request)
     * if it is a cross call, then the caller is the contract address who initiates the cross call。
     *
     * @return sender
     * @throws InterruptedException
     * @throws ContractException
     */
    public static String sender() throws InterruptedException, ContractException {}

    /**
     * log warn message string
     *
     * @param msg
     */
    public static void logW(String msg) {}

    /**
     * log error message string
     *
     * @param msg
     */
    public static void logE(String msg) {}

    /**
     * log info message string
     *
     * @param msg
     */
    public static void logI(String msg) {}

    /**
     * log debug message string
     *
     * @param msg
     */
    public static void logD(String msg) {}


    /**
     * get the contract creator org id
     *
     * @return org id
     */
    public static String getCreatorOrgId() {}

    /**
     * get the contract creator role
     *
     * @return role
     */
    public static String getCreatorRole() {}

    /**
     * get the contract creator public key
     *
     * @return public key
     */
    public static String getCreatorPk() {}

    /**
     * get the transaction sender org id
     *
     * @return org id
     */
    public static String getSenderOrgId() {}

    /**
     * get the transaction sender role
     *
     * @return role
     */
    public static String getSenderRole() {}

    /**
     * get the transaction sender public key
     *
     * @return public key
     */
    public static String getSenderPk() {}


    /**
     * get the current block height
     *
     * @return block height
     */
    public static int getBlockHeight() {
        return 0;}


    /**
     * get the transaction info by tx id
     *
     * @param txId
     * @return tx info
     * @throws InvalidProtocolBufferException
     * @throws InterruptedException
     */
    public static Response getTxInfo(String txId) throws InvalidProtocolBufferException, InterruptedException {}


    /**
     * get the transaction timestamp
     *
     * @return timestamp
     */
    public static String getTxTimestamp() {}


    /**
     * build the success response by string message
     *
     * @param resultMsg
     * @return response
     */
    public static Response success(String resultMsg) {}

    /**
     * build the success response by byte array message
     *
     * @param resultMsg
     * @return response
     */
    public static Response success(byte[] resultMsg) {}

    /**
     * build the success response by byte string message
     *
     * @param resultMsg
     * @return response
     */
    public static Response error(String resultMsg) {}

storemap

    /**
     * NewStoreMap create a new StoreMap with name and depth, 
     * if it exists, return the existing StoreMap
     * @param name
     * @param depth
     * @return storemap
     * @throws ContractException
     * @throws NoSuchAlgorithmException
     * @throws IOException
     * @throws InterruptedException
     */
    public static StoreMap NewStoreMap(String name, long depth) throws ContractException, NoSuchAlgorithmException, IOException, InterruptedException {}

    /**
     * NewStoreMap create a new StoreMap with name and depth and hash type, 
     * if it exists, return the existing StoreMap
     * @param name
     * @param depth
     * @return storemap
     * @throws ContractException
     * @throws NoSuchAlgorithmException
     * @throws IOException
     * @throws InterruptedException
     */
    public static StoreMap NewStoreMap(String name, long depth, HashTypeEnum hashType) throws ContractException, NoSuchAlgorithmException, IOException, InterruptedException {}


    /**
     * get the value of keys in the StoreMap
     * @param keys
     * @return value
     * @throws ContractException
     * @throws NoSuchAlgorithmException
     * @throws IOException
     * @throws InterruptedException
     */
    public byte[] get(String[] keys) throws ContractException, NoSuchAlgorithmException, IOException, InterruptedException {}

    /**
     * set the value of keys in the StoreMap
     * @param keys
     * @param value
     * @throws ContractException
     * @throws IOException
     * @throws NoSuchAlgorithmException
     */
    public void set(String[] keys, byte[] value) throws ContractException, IOException, NoSuchAlgorithmException {
        checkMapDepth(keys);
        KVResult kv = generateKey(keys);

        SDK.putState(kv.getKey(), kv.getField(), value);

    }

    /**
     * delete the keys in the StoreMap
     * @param keys
     * @throws ContractException
     * @throws IOException
     * @throws NoSuchAlgorithmException
     */
    public void del(String[] keys) throws ContractException, IOException, NoSuchAlgorithmException {
        checkMapDepth(keys);
        KVResult kv = generateKey(keys);

        SDK.delState(kv.getKey(), kv.getField());
    }

    /**
     * return whether the keys exist in the StoreMap
     * @param keys
     * @return isExist
     * @throws ContractException
     * @throws IOException
     * @throws NoSuchAlgorithmException
     * @throws InterruptedException
     */
    public boolean exist(String[] keys) throws ContractException, IOException, NoSuchAlgorithmException, InterruptedException {
        checkMapDepth(keys);
        KVResult kv = generateKey(keys);

        byte[] ret = SDK.getState(kv.getKey(), kv.getField());

        return ret.length != 0;
    }

    /**
     * create a iterator for the StoreMap
     * @param keys
     * @return iterator
     * @throws ContractException
     * @throws InterruptedException
     */
    public ResultSetKV newStoreMapIteratorPrefixWithKey(String[] keys) throws ContractException, InterruptedException {
        return SDK.newIteratorPrefixWithKey(name+String.join("",keys));
    }