# 使用Java进行智能合约开发 读者对象:本章节主要描述使用Java进行ChainMaker合约编写的方法,主要面向于使用Java进行ChainMaker的合约开发的开发者。 ## 环境依赖 1. 软件依赖 当前合约执行环境为JDK 11,推荐使用JDK 11编写合约。 推荐使用 IDEA 或 vscode等IDE编写和编译Java合约。 2. 长安链环境准备 准备一条支持Docker_VM的长安链,以及长安链CMC工具,用于将写编写好的合约,部署到链上进行测试。相关安装教程请详见: - [部署支持Docker_VM的长安链教程。](./启动支持Docker_VM的链.md) - [部署长安链CMC工具的教程。](../dev/命令行工具.md) ## 编写Java合约 ### 引用合约sdk #### 方式1 ##### 在gradle项目中使用SDK 在build.gradle 中添加dependencies ```groovy implementation group: 'org.chainmaker', name: 'contracts-sdk-java', version: '1.0' ``` ##### 在Maven项目中使用SDK ```xml org.chainmaker contracts-sdk-java 1.0 ``` #### 方式2 将项目引入到本地并编译安装到本地maven库: ```shell git clone -b v2.4.0 https://git.chainmaker.org.cn/chainmaker/contract-sdk-java.git cd contract-sdk-java mvn clean install '-Dmaven.test.skip=true' ``` 然后通过**方式1**引入到合约开发项目。 ### 合约编写规则 #### 1. 合约必须实现接口 IContract 合约必须需要实现合约初始化方法(initContract) 和合约升级方法(upgradeContract)。 IContract 中定义了默认的 initContract 和 upgradeContract 的接口。默认这两个接口只返回成功的message。 合约可以根据需求自行实现这两个接口。 #### 2. 代码的执行入口 在合约类定义的main方法中,需要将合约实例作为参数传给sanbox.serve ```java public class fact implements IContract { // ... public static void main(String[] args) { Sandbox.serve(args, new fact()); } } ``` #### 定义合约方法 在合约类中定义合约方法,方法权限必须是public,返回值为Response,并且使用@ContractMethod注解标识。在链上调用中,指定的合约方法名与定义的方法名同名(区分大小写)。 ```java @ContractMethod public Response save() throws IOException, ContractException { SDK.putState("key1", "field1", SDK.getParam("value")); return SDK.success("store success"); } ``` #### 获取合约的调用参数 1. 使用SDK.getArgs()获取参数map ```java // 获取参数map Map 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值的判断,假如参数值没有传递,则分别返回空字符串和空字节数组 ```java String key = SDK.getParam("key"); byte[] value = SDK.getParamBytes("value"); ``` #### 错误捕获 合约报错中ContractException中包含了正常逻辑的报错,主要包括key field的合法检查,storemap的参数检查,以及链上接口返回的报错等。 在合约逻辑中,建议捕获ContractException,并做相应的处理。 ### 编译合约 合约需要编译成可独立运行的jar包(包含运行所需的依赖,也叫fat jar 或uber jar)。 #### gradle 打包方式 配置示例如下,执行`./gradlew uberJar`,默认生成的jar包在build/libs目录下,文件名以-uber.jar为结尾。 ```groovy 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 } ``` #### maven 打包方式 使用maven-shade-plugin。配置示例如下。 执行 `mvn package`,默认生成的jar包在target目录下。 ```xml org.apache.maven.plugins maven-shade-plugin 3.4.0 package shade *:* META-INF/*.SF META-INF/*.DSA META-INF/*.RSA org.chainmaker.examples.demo ``` ### 部署调用合约 部署合约的使用教程可详见:[部署示例合约](./部署示例合约.md)。 ## 示例合约使用演示 ### 示例代码 ```java 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](https://git.chainmaker.org.cn/chainmaker/contract-sdk-java/-/tree/v2.4.0/src/main/java/org/chainmaker/examples) ### 部署调用示例合约 #### 使用cmc工具部署调用合约 ```shell ## 创建合约 ./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\"}" ``` ## 接口描述 用户与链交互接口 SDK: ```java /** * 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 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 */ public static Map 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 getBatchState(List 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 ```java /** * 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)); } ```