5. 使用Java进行智能合约开发
读者对象:本章节主要描述使用Java进行ChainMaker合约编写的方法,主要面向于使用Java进行ChainMaker的合约开发的开发者。
5.1. 环境依赖
软件依赖
当前合约执行环境为JDK 11,推荐使用JDK 11编写合约。
推荐使用 IDEA 或 vscode等IDE编写和编译Java合约。
长安链环境准备
准备一条支持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. 获取合约的调用参数
使用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();
使用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));
}