diff --git a/asset-transfer-private-data/chaincode-java/.gitattributes b/asset-transfer-private-data/chaincode-java/.gitattributes
new file mode 100644
index 00000000..00a51aff
--- /dev/null
+++ b/asset-transfer-private-data/chaincode-java/.gitattributes
@@ -0,0 +1,6 @@
+#
+# https://help.github.com/articles/dealing-with-line-endings/
+#
+# These are explicitly windows files and should use crlf
+*.bat text eol=crlf
+
diff --git a/asset-transfer-private-data/chaincode-java/META-INF/statedb/couchdb/collections/assetCollection/indexes/indexOwner.json b/asset-transfer-private-data/chaincode-java/META-INF/statedb/couchdb/collections/assetCollection/indexes/indexOwner.json
new file mode 100644
index 00000000..2e2a5c6d
--- /dev/null
+++ b/asset-transfer-private-data/chaincode-java/META-INF/statedb/couchdb/collections/assetCollection/indexes/indexOwner.json
@@ -0,0 +1,11 @@
+{
+ "index": {
+ "fields": [
+ "objectType",
+ "owner"
+ ]
+ },
+ "ddoc": "indexOwnerDoc",
+ "name": "indexOwner",
+ "type": "json"
+}
diff --git a/asset-transfer-private-data/chaincode-java/build.gradle b/asset-transfer-private-data/chaincode-java/build.gradle
new file mode 100644
index 00000000..bb48d5b6
--- /dev/null
+++ b/asset-transfer-private-data/chaincode-java/build.gradle
@@ -0,0 +1,62 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+plugins {
+ id 'application'
+ id 'checkstyle'
+ id 'jacoco'
+}
+
+group 'org.hyperledger.fabric.samples'
+version '1.0-SNAPSHOT'
+
+dependencies {
+
+ compileOnly 'org.hyperledger.fabric-chaincode-java:fabric-chaincode-shim:2.+'
+
+ testImplementation 'org.hyperledger.fabric-chaincode-java:fabric-chaincode-shim:2.+'
+ testImplementation 'org.junit.jupiter:junit-jupiter:5.4.2'
+ testImplementation 'org.assertj:assertj-core:3.11.1'
+ testImplementation 'org.mockito:mockito-core:2.+'
+}
+
+repositories {
+ maven {
+ url "https://hyperledger.jfrog.io/hyperledger/fabric-maven"
+ }
+ jcenter()
+ maven {
+ url 'https://jitpack.io'
+ }
+}
+
+application {
+ mainClass = 'org.hyperledger.fabric.contract.ContractRouter'
+}
+
+checkstyle {
+ toolVersion '8.21'
+ configFile file("config/checkstyle/checkstyle.xml")
+}
+
+checkstyleMain {
+ source ='src/main/java'
+}
+
+checkstyleTest {
+ source ='src/test/java'
+}
+
+jacocoTestReport {
+ dependsOn test
+}
+
+test {
+ useJUnitPlatform()
+ testLogging {
+ events "passed", "skipped", "failed"
+ }
+}
+
+installDist.dependsOn check
\ No newline at end of file
diff --git a/asset-transfer-private-data/chaincode-java/collections_config.json b/asset-transfer-private-data/chaincode-java/collections_config.json
new file mode 100644
index 00000000..cb3729aa
--- /dev/null
+++ b/asset-transfer-private-data/chaincode-java/collections_config.json
@@ -0,0 +1,35 @@
+[
+ {
+ "name": "assetCollection",
+ "policy": "OR('Org1MSP.member', 'Org2MSP.member')",
+ "requiredPeerCount": 1,
+ "maxPeerCount": 1,
+ "blockToLive":1000000,
+ "memberOnlyRead": true,
+ "memberOnlyWrite": true
+},
+ {
+ "name": "Org1MSPPrivateCollection",
+ "policy": "OR('Org1MSP.member')",
+ "requiredPeerCount": 0,
+ "maxPeerCount": 1,
+ "blockToLive":3,
+ "memberOnlyRead": true,
+ "memberOnlyWrite": false,
+ "endorsementPolicy": {
+ "signaturePolicy": "OR('Org1MSP.member')"
+ }
+ },
+ {
+ "name": "Org2MSPPrivateCollection",
+ "policy": "OR('Org2MSP.member')",
+ "requiredPeerCount": 0,
+ "maxPeerCount": 1,
+ "blockToLive":3,
+ "memberOnlyRead": true,
+ "memberOnlyWrite": false,
+ "endorsementPolicy": {
+ "signaturePolicy": "OR('Org2MSP.member')"
+ }
+ }
+]
diff --git a/asset-transfer-private-data/chaincode-java/config/checkstyle/checkstyle.xml b/asset-transfer-private-data/chaincode-java/config/checkstyle/checkstyle.xml
new file mode 100644
index 00000000..acd5df44
--- /dev/null
+++ b/asset-transfer-private-data/chaincode-java/config/checkstyle/checkstyle.xml
@@ -0,0 +1,171 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/asset-transfer-private-data/chaincode-java/config/checkstyle/suppressions.xml b/asset-transfer-private-data/chaincode-java/config/checkstyle/suppressions.xml
new file mode 100644
index 00000000..33dda041
--- /dev/null
+++ b/asset-transfer-private-data/chaincode-java/config/checkstyle/suppressions.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/asset-transfer-private-data/chaincode-java/gradle/wrapper/gradle-wrapper.jar b/asset-transfer-private-data/chaincode-java/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 00000000..5c2d1cf0
Binary files /dev/null and b/asset-transfer-private-data/chaincode-java/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/asset-transfer-private-data/chaincode-java/gradle/wrapper/gradle-wrapper.properties b/asset-transfer-private-data/chaincode-java/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 00000000..bb8b2fc2
--- /dev/null
+++ b/asset-transfer-private-data/chaincode-java/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.5.1-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/asset-transfer-private-data/chaincode-java/gradlew b/asset-transfer-private-data/chaincode-java/gradlew
new file mode 100755
index 00000000..83f2acfd
--- /dev/null
+++ b/asset-transfer-private-data/chaincode-java/gradlew
@@ -0,0 +1,188 @@
+#!/usr/bin/env sh
+
+#
+# Copyright 2015 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+ cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
diff --git a/asset-transfer-private-data/chaincode-java/gradlew.bat b/asset-transfer-private-data/chaincode-java/gradlew.bat
new file mode 100644
index 00000000..9618d8d9
--- /dev/null
+++ b/asset-transfer-private-data/chaincode-java/gradlew.bat
@@ -0,0 +1,100 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/asset-transfer-private-data/chaincode-java/settings.gradle b/asset-transfer-private-data/chaincode-java/settings.gradle
new file mode 100644
index 00000000..59cbbbca
--- /dev/null
+++ b/asset-transfer-private-data/chaincode-java/settings.gradle
@@ -0,0 +1,5 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+rootProject.name = 'private'
diff --git a/asset-transfer-private-data/chaincode-java/src/main/java/org/hyperledger/fabric/samples/privatedata/Asset.java b/asset-transfer-private-data/chaincode-java/src/main/java/org/hyperledger/fabric/samples/privatedata/Asset.java
new file mode 100644
index 00000000..11b29101
--- /dev/null
+++ b/asset-transfer-private-data/chaincode-java/src/main/java/org/hyperledger/fabric/samples/privatedata/Asset.java
@@ -0,0 +1,126 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.hyperledger.fabric.samples.privatedata;
+
+import java.util.Objects;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import org.hyperledger.fabric.contract.annotation.DataType;
+import org.hyperledger.fabric.contract.annotation.Property;
+
+import org.hyperledger.fabric.shim.ChaincodeException;
+import org.json.JSONObject;
+
+@DataType()
+public final class Asset {
+
+ @Property()
+ private final String assetID;
+
+ @Property()
+ private final String objectType;
+
+ @Property()
+ private final String color;
+
+ @Property()
+ private final int size;
+
+ @Property()
+ private String owner;
+
+ public String getAssetID() {
+ return assetID;
+ }
+
+ public String getColor() {
+ return color;
+ }
+
+ public int getSize() {
+ return size;
+ }
+
+ public String getOwner() {
+ return owner;
+ }
+
+ public String getObjectType() {
+ return objectType;
+ }
+
+ public void setOwner(final String newowner) {
+ owner = newowner;
+ }
+
+ public Asset(final String type,
+ final String assetID, final String color,
+ final int size, final String owner) {
+ this.objectType = type;
+ this.assetID = assetID;
+ this.color = color;
+ this.size = size;
+ this.owner = owner;
+ }
+
+ public byte[] serialize() {
+ String jsonStr = new JSONObject(this).toString();
+ return jsonStr.getBytes(UTF_8);
+ }
+
+ public static Asset deserialize(final byte[] assetJSON) {
+ return deserialize(new String(assetJSON, UTF_8));
+ }
+
+ public static Asset deserialize(final String assetJSON) {
+ try {
+ JSONObject json = new JSONObject(assetJSON);
+ final String id = json.getString("assetID");
+ final String type = json.getString("objectType");
+ final String color = json.getString("color");
+ final String owner = json.getString("owner");
+ final int size = json.getInt("size");
+ return new Asset(type, id, color, size, owner);
+ } catch (Exception e) {
+ throw new ChaincodeException("Deserialize error: " + e.getMessage(), "DATA_ERROR");
+ }
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) {
+ return true;
+ }
+
+ if ((obj == null) || (getClass() != obj.getClass())) {
+ return false;
+ }
+
+ Asset other = (Asset) obj;
+
+ return Objects.deepEquals(
+ new String[]{getAssetID(), getColor(), getOwner()},
+ new String[]{other.getAssetID(), other.getColor(), other.getOwner()})
+ &&
+ Objects.deepEquals(
+ new int[]{getSize()},
+ new int[]{other.getSize()});
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(getObjectType(), getAssetID(), getColor(), getSize(), getOwner());
+ }
+
+ @Override
+ public String toString() {
+ return this.getClass().getSimpleName() + "@" + Integer.toHexString(hashCode())
+ + " [assetID=" + assetID + ", type=" + objectType + ", color="
+ + color + ", size=" + size + ", owner=" + owner + "]";
+ }
+
+
+}
diff --git a/asset-transfer-private-data/chaincode-java/src/main/java/org/hyperledger/fabric/samples/privatedata/AssetPrivateDetails.java b/asset-transfer-private-data/chaincode-java/src/main/java/org/hyperledger/fabric/samples/privatedata/AssetPrivateDetails.java
new file mode 100644
index 00000000..da1c6e24
--- /dev/null
+++ b/asset-transfer-private-data/chaincode-java/src/main/java/org/hyperledger/fabric/samples/privatedata/AssetPrivateDetails.java
@@ -0,0 +1,51 @@
+package org.hyperledger.fabric.samples.privatedata;
+
+import org.hyperledger.fabric.contract.annotation.DataType;
+import org.hyperledger.fabric.contract.annotation.Property;
+
+import org.hyperledger.fabric.shim.ChaincodeException;
+import org.json.JSONObject;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+@DataType()
+public final class AssetPrivateDetails {
+
+ @Property()
+ private final String assetID;
+
+ @Property()
+ private int appraisedValue;
+
+ public String getAssetID() {
+ return assetID;
+ }
+
+ public int getAppraisedValue() {
+ return appraisedValue;
+ }
+
+ public AssetPrivateDetails(final String assetID,
+ final int appraisedValue) {
+ this.assetID = assetID;
+ this.appraisedValue = appraisedValue;
+ }
+
+ public byte[] serialize() {
+ String jsonStr = new JSONObject(this).toString();
+ return jsonStr.getBytes(UTF_8);
+ }
+
+ public static AssetPrivateDetails deserialize(final byte[] assetJSON) {
+ try {
+ JSONObject json = new JSONObject(new String(assetJSON, UTF_8));
+ final String id = json.getString("assetID");
+ final int appraisedValue = json.getInt("appraisedValue");
+ return new AssetPrivateDetails(id, appraisedValue);
+ } catch (Exception e) {
+ throw new ChaincodeException("Deserialize error: " + e.getMessage(), "DATA_ERROR");
+ }
+ }
+
+
+}
diff --git a/asset-transfer-private-data/chaincode-java/src/main/java/org/hyperledger/fabric/samples/privatedata/AssetTransfer.java b/asset-transfer-private-data/chaincode-java/src/main/java/org/hyperledger/fabric/samples/privatedata/AssetTransfer.java
new file mode 100644
index 00000000..c8c569f8
--- /dev/null
+++ b/asset-transfer-private-data/chaincode-java/src/main/java/org/hyperledger/fabric/samples/privatedata/AssetTransfer.java
@@ -0,0 +1,593 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.hyperledger.fabric.samples.privatedata;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import org.hyperledger.fabric.contract.Context;
+import org.hyperledger.fabric.contract.ContractInterface;
+import org.hyperledger.fabric.contract.annotation.Contact;
+import org.hyperledger.fabric.contract.annotation.Contract;
+import org.hyperledger.fabric.contract.annotation.Default;
+import org.hyperledger.fabric.contract.annotation.Info;
+import org.hyperledger.fabric.contract.annotation.License;
+import org.hyperledger.fabric.contract.annotation.Transaction;
+import org.hyperledger.fabric.shim.ChaincodeException;
+import org.hyperledger.fabric.shim.ChaincodeStub;
+import org.hyperledger.fabric.shim.ledger.CompositeKey;
+
+import org.hyperledger.fabric.shim.ledger.KeyValue;
+import org.hyperledger.fabric.shim.ledger.QueryResultsIterator;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Main Chaincode class. A ContractInterface gets converted to Chaincode internally.
+ * @see org.hyperledger.fabric.shim.Chaincode
+ *
+ * Each chaincode transaction function must take, Context as first parameter.
+ * Unless specified otherwise via annotation (@Contract or @Transaction), the contract name
+ * is the class name (without package)
+ * and the transaction name is the method name.
+ *
+ * To create fabric test-network
+ * cd fabric-samples/test-network
+ * ./network.sh up createChannel -ca -s couchdb
+ * To deploy this chaincode to test-network, use the collection config as described in
+ * See queryResults = new ArrayList<>();
+ // retrieve asset with keys between startKey (inclusive) and endKey(exclusive) in lexical order.
+ try (QueryResultsIterator results = stub.getPrivateDataByRange(ASSET_COLLECTION_NAME, startKey, endKey)) {
+ for (KeyValue result : results) {
+ if (result.getStringValue() == null || result.getStringValue().length() == 0) {
+ System.err.printf("Invalid Asset json: %s\n", result.getStringValue());
+ continue;
+ }
+ Asset asset = Asset.deserialize(result.getStringValue());
+ queryResults.add(asset);
+ System.out.println("QueryResult: " + asset.toString());
+ }
+ }
+ return queryResults.toArray(new Asset[0]);
+ }
+
+ // =======Rich queries =========================================================================
+ // Two examples of rich queries are provided below (parameterized query and ad hoc query).
+ // Rich queries pass a query string to the state database.
+ // Rich queries are only supported by state database implementations
+ // that support rich query (e.g. CouchDB).
+ // The query string is in the syntax of the underlying state database.
+ // With rich queries there is no guarantee that the result set hasn't changed between
+ // endorsement time and commit time, aka 'phantom reads'.
+ // Therefore, rich queries should not be used in update transactions, unless the
+ // application handles the possibility of result set changes between endorsement and commit time.
+ // Rich queries can be used for point-in-time queries against a peer.
+ // ============================================================================================
+
+ /**
+ * QueryAssetByOwner queries for assets based on assetType, owner.
+ * This is an example of a parameterized query where the query logic is baked into the chaincode,
+ * and accepting a single query parameter (owner).
+ *
+ * @param ctx the transaction context
+ * @param assetType type to query for
+ * @param owner asset owner to query for
+ * @return the asset found on the ledger if there was one
+ */
+ @Transaction(intent = Transaction.TYPE.EVALUATE)
+ public Asset[] QueryAssetByOwner(final Context ctx, final String assetType, final String owner) throws Exception {
+ String queryString = String.format("{\"selector\":{\"objectType\":\"%s\",\"owner\":\"%s\"}}", assetType, owner);
+ return getQueryResult(ctx, queryString);
+ }
+
+ /**
+ * QueryAssets uses a query string to perform a query for assets.
+ * Query string matching state database syntax is passed in and executed as is.
+ * Supports ad hoc queries that can be defined at runtime by the client.
+ *
+ * @param ctx the transaction context
+ * @param queryString query string matching state database syntax
+ * @return the asset found on the ledger if there was one
+ */
+ @Transaction(intent = Transaction.TYPE.EVALUATE)
+ public Asset[] QueryAssets(final Context ctx, final String queryString) throws Exception {
+ return getQueryResult(ctx, queryString);
+ }
+
+ private Asset[] getQueryResult(final Context ctx, final String queryString) throws Exception {
+ ChaincodeStub stub = ctx.getStub();
+ System.out.printf("QueryAssets: %s\n", queryString);
+
+ List queryResults = new ArrayList();
+ // retrieve asset with keys between startKey (inclusive) and endKey(exclusive) in lexical order.
+ try (QueryResultsIterator results = stub.getPrivateDataQueryResult(ASSET_COLLECTION_NAME, queryString)) {
+ for (KeyValue result : results) {
+ if (result.getStringValue() == null || result.getStringValue().length() == 0) {
+ System.err.printf("Invalid Asset json: %s\n", result.getStringValue());
+ continue;
+ }
+ Asset asset = Asset.deserialize(result.getStringValue());
+ queryResults.add(asset);
+ System.out.println("QueryResult: " + asset.toString());
+ }
+ }
+ return queryResults.toArray(new Asset[0]);
+ }
+
+
+ /**
+ * Creates a new asset on the ledger from asset properties passed in as transient map.
+ * Asset owner will be inferred from the ClientId via stub api
+ *
+ * @param ctx the transaction context
+ * Transient map with asset_properties key with asset json as value
+ * @return the created asset
+ */
+ @Transaction(intent = Transaction.TYPE.SUBMIT)
+ public Asset CreateAsset(final Context ctx) {
+ ChaincodeStub stub = ctx.getStub();
+ Map transientMap = ctx.getStub().getTransient();
+ if (!transientMap.containsKey("asset_properties")) {
+ String errorMessage = String.format("CreateAsset call must specify asset_properties in Transient map input");
+ System.err.println(errorMessage);
+ throw new ChaincodeException(errorMessage, AssetTransferErrors.INCOMPLETE_INPUT.toString());
+ }
+
+ byte[] transientAssetJSON = transientMap.get("asset_properties");
+ final String assetID;
+ final String type;
+ final String color;
+ int appraisedValue = 0;
+ int size = 0;
+ try {
+ JSONObject json = new JSONObject(new String(transientAssetJSON, UTF_8));
+ Map tMap = json.toMap();
+
+ type = (String) tMap.get("objectType");
+ assetID = (String) tMap.get("assetID");
+ color = (String) tMap.get("color");
+ if (tMap.containsKey("size")) {
+ size = (Integer) tMap.get("size");
+ }
+ if (tMap.containsKey("appraisedValue")) {
+ appraisedValue = (Integer) tMap.get("appraisedValue");
+ }
+ } catch (Exception err) {
+ String errorMessage = String.format("TransientMap deserialized error: %s ", err);
+ System.err.println(errorMessage);
+ throw new ChaincodeException(errorMessage, AssetTransferErrors.INCOMPLETE_INPUT.toString());
+ }
+
+ //input validations
+ String errorMessage = null;
+ if (assetID.equals("")) {
+ errorMessage = String.format("Empty input in Transient map: assetID");
+ }
+ if (type.equals("")) {
+ errorMessage = String.format("Empty input in Transient map: objectType");
+ }
+ if (color.equals("")) {
+ errorMessage = String.format("Empty input in Transient map: color");
+ }
+ if (size <= 0) {
+ errorMessage = String.format("Empty input in Transient map: size");
+ }
+ if (appraisedValue <= 0) {
+ errorMessage = String.format("Empty input in Transient map: appraisedValue");
+ }
+
+ if (errorMessage != null) {
+ System.err.println(errorMessage);
+ throw new ChaincodeException(errorMessage, AssetTransferErrors.INCOMPLETE_INPUT.toString());
+ }
+
+ Asset asset = new Asset(type, assetID, color, size, "");
+ // Check if asset already exists
+ byte[] assetJSON = ctx.getStub().getPrivateData(ASSET_COLLECTION_NAME, assetID);
+ if (assetJSON != null && assetJSON.length > 0) {
+ errorMessage = String.format("Asset %s already exists", assetID);
+ System.err.println(errorMessage);
+ throw new ChaincodeException(errorMessage, AssetTransferErrors.ASSET_ALREADY_EXISTS.toString());
+ }
+
+ // Get ID of submitting client identity
+ String clientID = ctx.getClientIdentity().getId();
+
+ // Verify that the client is submitting request to peer in their organization
+ // This is to ensure that a client from another org doesn't attempt to read or
+ // write private data from this peer.
+ verifyClientOrgMatchesPeerOrg(ctx);
+
+ //Make submitting client the owner
+ asset.setOwner(clientID);
+ System.out.printf("CreateAsset Put: collection %s, ID %s\n", ASSET_COLLECTION_NAME, assetID);
+ System.out.printf("Put: collection %s, ID %s\n", ASSET_COLLECTION_NAME, new String(asset.serialize()));
+ stub.putPrivateData(ASSET_COLLECTION_NAME, assetID, asset.serialize());
+
+ // Get collection name for this organization.
+ String orgCollectionName = getCollectionName(ctx);
+
+ //Save AssetPrivateDetails to org collection
+ AssetPrivateDetails assetPriv = new AssetPrivateDetails(assetID, appraisedValue);
+ System.out.printf("Put AssetPrivateDetails: collection %s, ID %s\n", orgCollectionName, assetID);
+ stub.putPrivateData(orgCollectionName, assetID, assetPriv.serialize());
+
+ return asset;
+ }
+
+ /**
+ * AgreeToTransfer is used by the potential buyer of the asset to agree to the
+ * asset value. The agreed to appraisal value is stored in the buying orgs
+ * org specifc collection, while the the buyer client ID is stored in the asset collection
+ * using a composite key
+ * Uses transient map with key asset_value
+ *
+ * @param ctx the transaction context
+ */
+ @Transaction(intent = Transaction.TYPE.SUBMIT)
+ public void AgreeToTransfer(final Context ctx) {
+ ChaincodeStub stub = ctx.getStub();
+ Map transientMap = ctx.getStub().getTransient();
+ if (!transientMap.containsKey("asset_value")) {
+ String errorMessage = String.format("AgreeToTransfer call must specify \"asset_value\" in Transient map input");
+ System.err.println(errorMessage);
+ throw new ChaincodeException(errorMessage, AssetTransferErrors.INCOMPLETE_INPUT.toString());
+ }
+
+ byte[] transientAssetJSON = transientMap.get("asset_value");
+ AssetPrivateDetails assetPriv;
+ String assetID;
+ try {
+ JSONObject json = new JSONObject(new String(transientAssetJSON, UTF_8));
+ assetID = json.getString("assetID");
+ final int appraisedValue = json.getInt("appraisedValue");
+
+ assetPriv = new AssetPrivateDetails(assetID, appraisedValue);
+ } catch (Exception err) {
+ String errorMessage = String.format("TransientMap deserialized error %s ", err);
+ System.err.println(errorMessage);
+ throw new ChaincodeException(errorMessage, AssetTransferErrors.INCOMPLETE_INPUT.toString());
+ }
+
+ if (assetID.equals("")) {
+ String errorMessage = String.format("Invalid input in Transient map: assetID");
+ System.err.println(errorMessage);
+ throw new ChaincodeException(errorMessage, AssetTransferErrors.INCOMPLETE_INPUT.toString());
+ }
+ if (assetPriv.getAppraisedValue() <= 0) { // appraisedValue field must be a positive integer
+ String errorMessage = String.format("Input must be positive integer: appraisedValue");
+ System.err.println(errorMessage);
+ throw new ChaincodeException(errorMessage, AssetTransferErrors.INCOMPLETE_INPUT.toString());
+ }
+ System.out.printf("AgreeToTransfer: verify asset %s exists\n", assetID);
+ Asset existing = ReadAsset(ctx, assetID);
+ if (existing == null) {
+ String errorMessage = String.format("Asset does not exist in the collection: ", assetID);
+ System.err.println(errorMessage);
+ throw new ChaincodeException(errorMessage, AssetTransferErrors.INCOMPLETE_INPUT.toString());
+ }
+ // Get collection name for this organization.
+ String orgCollectionName = getCollectionName(ctx);
+
+ verifyClientOrgMatchesPeerOrg(ctx);
+
+ //Save AssetPrivateDetails to org collection
+ System.out.printf("Put AssetPrivateDetails: collection %s, ID %s\n", orgCollectionName, assetID);
+ stub.putPrivateData(orgCollectionName, assetID, assetPriv.serialize());
+
+ String clientID = ctx.getClientIdentity().getId();
+ //Write the AgreeToTransfer key in assetCollection
+ CompositeKey aggKey = stub.createCompositeKey(AGREEMENT_KEYPREFIX, assetID);
+ System.out.printf("AgreeToTransfer Put: collection %s, ID %s, Key %s\n", ASSET_COLLECTION_NAME, assetID, aggKey);
+ stub.putPrivateData(ASSET_COLLECTION_NAME, aggKey.toString(), clientID);
+ }
+
+ /**
+ * TransferAsset transfers the asset to the new owner by setting a new owner ID based on
+ * AgreeToTransfer data
+ *
+ * @param ctx the transaction context
+ * @return none
+ */
+ @Transaction(intent = Transaction.TYPE.SUBMIT)
+ public void TransferAsset(final Context ctx) {
+ ChaincodeStub stub = ctx.getStub();
+ Map transientMap = ctx.getStub().getTransient();
+ if (!transientMap.containsKey("asset_owner")) {
+ String errorMessage = "TransferAsset call must specify \"asset_owner\" in Transient map input";
+ System.err.println(errorMessage);
+ throw new ChaincodeException(errorMessage, AssetTransferErrors.INCOMPLETE_INPUT.toString());
+ }
+
+ byte[] transientAssetJSON = transientMap.get("asset_owner");
+ final String assetID;
+ final String buyerMSP;
+ try {
+ JSONObject json = new JSONObject(new String(transientAssetJSON, UTF_8));
+ assetID = json.getString("assetID");
+ buyerMSP = json.getString("buyerMSP");
+
+ } catch (Exception err) {
+ String errorMessage = String.format("TransientMap deserialized error %s ", err);
+ System.err.println(errorMessage);
+ throw new ChaincodeException(errorMessage, AssetTransferErrors.INCOMPLETE_INPUT.toString());
+ }
+
+ if (assetID.equals("")) {
+ String errorMessage = String.format("Invalid input in Transient map: " + "assetID");
+ System.err.println(errorMessage);
+ throw new ChaincodeException(errorMessage, AssetTransferErrors.INCOMPLETE_INPUT.toString());
+ }
+ if (buyerMSP.equals("")) {
+ String errorMessage = String.format("Invalid input in Transient map: " + "buyerMSP");
+ System.err.println(errorMessage);
+ throw new ChaincodeException(errorMessage, AssetTransferErrors.INCOMPLETE_INPUT.toString());
+ }
+
+ System.out.printf("TransferAsset: verify asset %s exists\n", assetID);
+ byte[] assetJSON = stub.getPrivateData(ASSET_COLLECTION_NAME, assetID);
+
+ if (assetJSON == null || assetJSON.length == 0) {
+ String errorMessage = String.format("Asset %s does not exist in the collection", assetID);
+ System.err.println(errorMessage);
+ throw new ChaincodeException(errorMessage, AssetTransferErrors.INCOMPLETE_INPUT.toString());
+ }
+
+ verifyClientOrgMatchesPeerOrg(ctx);
+ Asset thisAsset = Asset.deserialize(assetJSON);
+ // Verify transfer details and transfer owner
+ verifyAgreement(ctx, assetID, thisAsset.getOwner(), buyerMSP);
+
+ TransferAgreement transferAgreement = ReadTransferAgreement(ctx, assetID);
+ if (transferAgreement == null) {
+ String errorMessage = String.format("TransferAgreement does not exist for asset: %s", assetID);
+ System.err.println(errorMessage);
+ throw new ChaincodeException(errorMessage, AssetTransferErrors.INCOMPLETE_INPUT.toString());
+ }
+
+ // Transfer asset in private data collection to new owner
+ String newOwner = transferAgreement.getBuyerID();
+ thisAsset.setOwner(newOwner);
+
+ //Save updated Asset to collection
+ System.out.printf("Transfer Asset: collection %s, ID %s to owner %s\n", ASSET_COLLECTION_NAME, assetID, newOwner);
+ stub.putPrivateData(ASSET_COLLECTION_NAME, assetID, thisAsset.serialize());
+
+ // delete the key from owners collection
+ String ownersCollectionName = getCollectionName(ctx);
+ stub.delPrivateData(ownersCollectionName, assetID);
+
+ //Delete the transfer agreement from the asset collection
+ CompositeKey aggKey = stub.createCompositeKey(AGREEMENT_KEYPREFIX, assetID);
+ System.out.printf("AgreeToTransfer deleteKey: collection %s, ID %s, Key %s\n", ASSET_COLLECTION_NAME, assetID, aggKey);
+ stub.delPrivateData(ASSET_COLLECTION_NAME, aggKey.toString());
+ }
+
+ /**
+ * Deletes a asset & related details from the ledger.
+ * Input in transient map: asset_delete
+ *
+ * @param ctx the transaction context
+ */
+ @Transaction(intent = Transaction.TYPE.SUBMIT)
+ public void DeleteAsset(final Context ctx) {
+ ChaincodeStub stub = ctx.getStub();
+ Map transientMap = ctx.getStub().getTransient();
+ if (!transientMap.containsKey("asset_delete")) {
+ String errorMessage = String.format("DeleteAsset call must specify 'asset_delete' in Transient map input");
+ System.err.println(errorMessage);
+ throw new ChaincodeException(errorMessage, AssetTransferErrors.INCOMPLETE_INPUT.toString());
+ }
+
+ byte[] transientAssetJSON = transientMap.get("asset_delete");
+ final String assetID;
+
+ try {
+ JSONObject json = new JSONObject(new String(transientAssetJSON, UTF_8));
+ assetID = json.getString("assetID");
+
+ } catch (Exception err) {
+ String errorMessage = String.format("TransientMap deserialized error: %s ", err);
+ System.err.println(errorMessage);
+ throw new ChaincodeException(errorMessage, AssetTransferErrors.INCOMPLETE_INPUT.toString());
+ }
+
+ System.out.printf("DeleteAsset: verify asset %s exists\n", assetID);
+ byte[] assetJSON = stub.getPrivateData(ASSET_COLLECTION_NAME, assetID);
+
+ if (assetJSON == null || assetJSON.length == 0) {
+ String errorMessage = String.format("Asset %s does not exist", assetID);
+ System.err.println(errorMessage);
+ throw new ChaincodeException(errorMessage, AssetTransferErrors.ASSET_NOT_FOUND.toString());
+ }
+ String ownersCollectionName = getCollectionName(ctx);
+ byte[] apdJSON = stub.getPrivateData(ownersCollectionName, assetID);
+
+ if (apdJSON == null || apdJSON.length == 0) {
+ String errorMessage = String.format("Failed to read asset from owner's Collection %s", ownersCollectionName);
+ System.err.println(errorMessage);
+ throw new ChaincodeException(errorMessage, AssetTransferErrors.ASSET_NOT_FOUND.toString());
+ }
+ verifyClientOrgMatchesPeerOrg(ctx);
+
+ // delete the key from asset collection
+ System.out.printf("DeleteAsset: collection %s, ID %s\n", ASSET_COLLECTION_NAME, assetID);
+ stub.delPrivateData(ASSET_COLLECTION_NAME, assetID);
+
+ // Finally, delete private details of asset
+ stub.delPrivateData(ownersCollectionName, assetID);
+ }
+
+ // Used by TransferAsset to verify that the transfer is being initiated by the owner and that
+ // the buyer has agreed to the same appraisal value as the owner
+ private void verifyAgreement(final Context ctx, final String assetID, final String owner, final String buyerMSP) {
+ String clienID = ctx.getClientIdentity().getId();
+
+ // Check 1: verify that the transfer is being initiatied by the owner
+ if (!clienID.equals(owner)) {
+ throw new ChaincodeException("Submitting client identity does not own the asset", AssetTransferErrors.INVALID_ACCESS.toString());
+ }
+
+ // Check 2: verify that the buyer has agreed to the appraised value
+ String collectionOwner = getCollectionName(ctx); // get owner collection from caller identity
+ String collectionBuyer = buyerMSP + "PrivateCollection";
+
+ // Get hash of owners agreed to value
+ byte[] ownerAppraisedValueHash = ctx.getStub().getPrivateDataHash(collectionOwner, assetID);
+ if (ownerAppraisedValueHash == null) {
+ throw new ChaincodeException(String.format("Hash of appraised value for %s does not exist in collection %s", assetID, collectionOwner));
+ }
+
+ // Get hash of buyers agreed to value
+ byte[] buyerAppraisedValueHash = ctx.getStub().getPrivateDataHash(collectionBuyer, assetID);
+ if (buyerAppraisedValueHash == null) {
+ throw new ChaincodeException(String.format("Hash of appraised value for %s does not exist in collection %s. AgreeToTransfer must be called by the buyer first.", assetID, collectionBuyer));
+ }
+
+ // Verify that the two hashes match
+ if (!Arrays.equals(ownerAppraisedValueHash, buyerAppraisedValueHash)) {
+ throw new ChaincodeException(String.format("Hash for appraised value for owner %x does not match value for seller %x", ownerAppraisedValueHash, buyerAppraisedValueHash));
+ }
+ }
+
+ private void verifyClientOrgMatchesPeerOrg(final Context ctx) {
+ String clientMSPID = ctx.getClientIdentity().getMSPID();
+ String peerMSPID = ctx.getStub().getMspId();
+
+ if (!peerMSPID.equals(clientMSPID)) {
+ String errorMessage = String.format("Client from org %s is not authorized to read or write private data from an org %s peer", clientMSPID, peerMSPID);
+ System.err.println(errorMessage);
+ throw new ChaincodeException(errorMessage, AssetTransferErrors.INVALID_ACCESS.toString());
+ }
+ }
+
+ private String getCollectionName(final Context ctx) {
+
+ // Get the MSP ID of submitting client identity
+ String clientMSPID = ctx.getClientIdentity().getMSPID();
+ // Create the collection name
+ return clientMSPID + "PrivateCollection";
+ }
+
+}
diff --git a/asset-transfer-private-data/chaincode-java/src/main/java/org/hyperledger/fabric/samples/privatedata/TransferAgreement.java b/asset-transfer-private-data/chaincode-java/src/main/java/org/hyperledger/fabric/samples/privatedata/TransferAgreement.java
new file mode 100644
index 00000000..2082d978
--- /dev/null
+++ b/asset-transfer-private-data/chaincode-java/src/main/java/org/hyperledger/fabric/samples/privatedata/TransferAgreement.java
@@ -0,0 +1,51 @@
+package org.hyperledger.fabric.samples.privatedata;
+
+import org.hyperledger.fabric.contract.annotation.DataType;
+import org.hyperledger.fabric.contract.annotation.Property;
+import org.hyperledger.fabric.shim.ChaincodeException;
+import org.json.JSONObject;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+@DataType()
+public final class TransferAgreement {
+
+ @Property()
+ private final String assetID;
+
+
+ @Property()
+ private String buyerID;
+
+ public String getAssetID() {
+ return assetID;
+ }
+
+ public String getBuyerID() {
+ return buyerID;
+ }
+
+ public TransferAgreement(final String assetID,
+ final String buyer) {
+ this.assetID = assetID;
+ this.buyerID = buyer;
+ }
+
+ public byte[] serialize() {
+ String jsonStr = new JSONObject(this).toString();
+ return jsonStr.getBytes(UTF_8);
+ }
+
+ public static TransferAgreement deserialize(final byte[] assetJSON) {
+ try {
+ JSONObject json = new JSONObject(new String(assetJSON, UTF_8));
+ final String id = json.getString("assetID");
+ final String buyerID = json.getString("buyerID");
+ return new TransferAgreement(id, buyerID);
+ } catch (Exception e) {
+ throw new ChaincodeException("Deserialize error: " + e.getMessage(), "DATA_ERROR");
+ }
+ }
+
+
+}
diff --git a/asset-transfer-private-data/chaincode-java/src/test/java/org/hyperledger/fabric/samples/privatedata/AssetTransferTest.java b/asset-transfer-private-data/chaincode-java/src/test/java/org/hyperledger/fabric/samples/privatedata/AssetTransferTest.java
new file mode 100644
index 00000000..a11a5f0b
--- /dev/null
+++ b/asset-transfer-private-data/chaincode-java/src/test/java/org/hyperledger/fabric/samples/privatedata/AssetTransferTest.java
@@ -0,0 +1,170 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.hyperledger.fabric.samples.privatedata;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.ThrowableAssert.catchThrowable;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.hyperledger.fabric.samples.privatedata.AssetTransfer.AGREEMENT_KEYPREFIX;
+import static org.hyperledger.fabric.samples.privatedata.AssetTransfer.ASSET_COLLECTION_NAME;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+import java.io.IOException;
+import java.security.cert.CertificateException;
+import java.util.HashMap;
+import java.util.Map;
+import org.hyperledger.fabric.contract.ClientIdentity;
+import org.hyperledger.fabric.contract.Context;
+import org.hyperledger.fabric.shim.ChaincodeException;
+import org.hyperledger.fabric.shim.ChaincodeStub;
+import org.hyperledger.fabric.shim.ledger.CompositeKey;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+public final class AssetTransferTest {
+
+ @Nested
+ class InvokeWriteTransaction {
+
+ @Test
+ public void createAssetWhenAssetExists() {
+ AssetTransfer contract = new AssetTransfer();
+ Context ctx = mock(Context.class);
+ ChaincodeStub stub = mock(ChaincodeStub.class);
+ when(ctx.getStub()).thenReturn(stub);
+ Map m = new HashMap();
+ m.put("asset_properties", dataAsset1Bytes);
+ when(ctx.getStub().getTransient()).thenReturn(m);
+ when(stub.getPrivateData(ASSET_COLLECTION_NAME, testAsset1ID))
+ .thenReturn(dataAsset1Bytes);
+
+ Throwable thrown = catchThrowable(() -> {
+ contract.CreateAsset(ctx);
+ });
+
+ assertThat(thrown).isInstanceOf(ChaincodeException.class).hasNoCause()
+ .hasMessage("Asset asset1 already exists");
+ assertThat(((ChaincodeException) thrown).getPayload()).isEqualTo("ASSET_ALREADY_EXISTS".getBytes());
+ }
+
+ @Test
+ public void createAssetWhenNewAssetIsCreated() throws CertificateException, IOException {
+ AssetTransfer contract = new AssetTransfer();
+ Context ctx = mock(Context.class);
+ ChaincodeStub stub = mock(ChaincodeStub.class);
+ when(ctx.getStub()).thenReturn(stub);
+ when(stub.getMspId()).thenReturn(testOrgOneMSP);
+ ClientIdentity ci = mock(ClientIdentity.class);
+ when(ci.getId()).thenReturn(testOrg1Client);
+ when(ci.getMSPID()).thenReturn(testOrgOneMSP);
+ when(ctx.getClientIdentity()).thenReturn(ci);
+
+ Map m = new HashMap();
+ m.put("asset_properties", dataAsset1Bytes);
+ when(ctx.getStub().getTransient()).thenReturn(m);
+
+ when(stub.getPrivateData(ASSET_COLLECTION_NAME, testAsset1ID))
+ .thenReturn(new byte[0]);
+
+ Asset created = contract.CreateAsset(ctx);
+ assertThat(created).isEqualTo(testAsset1);
+
+ verify(stub).putPrivateData(ASSET_COLLECTION_NAME, testAsset1ID, created.serialize());
+ }
+
+ @Test
+ public void transferAssetWhenExistingAssetIsTransferred() throws CertificateException, IOException {
+ AssetTransfer contract = new AssetTransfer();
+ Context ctx = mock(Context.class);
+ ChaincodeStub stub = mock(ChaincodeStub.class);
+ when(ctx.getStub()).thenReturn(stub);
+ when(stub.getMspId()).thenReturn(testOrgOneMSP);
+ ClientIdentity ci = mock(ClientIdentity.class);
+ when(ci.getId()).thenReturn(testOrg1Client);
+ when(ctx.getClientIdentity()).thenReturn(ci);
+ when(ci.getMSPID()).thenReturn(testOrgOneMSP);
+ final String recipientOrgMsp = "TestOrg2";
+ final String buyerIdentity = "TestOrg2User";
+ Map m = new HashMap();
+ m.put("asset_owner", ("{ \"buyerMSP\": \"" + recipientOrgMsp + "\", \"assetID\": \"" + testAsset1ID + "\" }").getBytes());
+ when(ctx.getStub().getTransient()).thenReturn(m);
+
+ when(stub.getPrivateDataHash(anyString(), anyString())).thenReturn("TestHashValue".getBytes());
+ when(stub.getPrivateData(ASSET_COLLECTION_NAME, testAsset1ID))
+ .thenReturn(dataAsset1Bytes);
+ CompositeKey ck = mock(CompositeKey.class);
+ when(ck.toString()).thenReturn(AGREEMENT_KEYPREFIX + testAsset1ID);
+ when(stub.createCompositeKey(AGREEMENT_KEYPREFIX, testAsset1ID)).thenReturn(ck);
+ when(stub.getPrivateData(ASSET_COLLECTION_NAME, AGREEMENT_KEYPREFIX + testAsset1ID)).thenReturn(buyerIdentity.getBytes(UTF_8));
+ contract.TransferAsset(ctx);
+
+ Asset exptectedAfterTransfer = Asset.deserialize("{ \"objectType\": \"testasset\", \"assetID\": \"asset1\", \"color\": \"blue\", \"size\": 5, \"owner\": \"" + buyerIdentity + "\", \"appraisedValue\": 300 }");
+
+ verify(stub).putPrivateData(ASSET_COLLECTION_NAME, testAsset1ID, exptectedAfterTransfer.serialize());
+ String collectionOwner = testOrgOneMSP + "PrivateCollection";
+ verify(stub).delPrivateData(collectionOwner, testAsset1ID);
+ verify(stub).delPrivateData(ASSET_COLLECTION_NAME, AGREEMENT_KEYPREFIX + testAsset1ID);
+ }
+ }
+
+ @Nested
+ class QueryReadAssetTransaction {
+
+ @Test
+ public void whenAssetExists() {
+ AssetTransfer contract = new AssetTransfer();
+ Context ctx = mock(Context.class);
+ ChaincodeStub stub = mock(ChaincodeStub.class);
+ when(ctx.getStub()).thenReturn(stub);
+ when(stub.getPrivateData(ASSET_COLLECTION_NAME, testAsset1ID))
+ .thenReturn(dataAsset1Bytes);
+
+ Asset asset = contract.ReadAsset(ctx, testAsset1ID);
+
+ assertThat(asset).isEqualTo(testAsset1);
+ }
+
+ @Test
+ public void whenAssetDoesNotExist() {
+ AssetTransfer contract = new AssetTransfer();
+ Context ctx = mock(Context.class);
+ ChaincodeStub stub = mock(ChaincodeStub.class);
+ when(ctx.getStub()).thenReturn(stub);
+ when(stub.getStringState(testAsset1ID)).thenReturn(null);
+
+ Asset asset = contract.ReadAsset(ctx, testAsset1ID);
+ assertThat(asset).isNull();
+ }
+
+ @Test
+ public void invokeUnknownTransaction() {
+ AssetTransfer contract = new AssetTransfer();
+ Context ctx = mock(Context.class);
+
+ Throwable thrown = catchThrowable(() -> {
+ contract.unknownTransaction(ctx);
+ });
+
+ assertThat(thrown).isInstanceOf(ChaincodeException.class).hasNoCause()
+ .hasMessage("Undefined contract method called");
+ assertThat(((ChaincodeException) thrown).getPayload()).isEqualTo(null);
+
+ verifyZeroInteractions(ctx);
+ }
+
+ }
+
+ private static String testOrgOneMSP = "TestOrg1";
+ private static String testOrg1Client = "testOrg1User";
+
+ private static String testAsset1ID = "asset1";
+ private static Asset testAsset1 = new Asset("testasset", "asset1", "blue", 5, testOrg1Client);
+ private static byte[] dataAsset1Bytes = "{ \"objectType\": \"testasset\", \"assetID\": \"asset1\", \"color\": \"blue\", \"size\": 5, \"owner\": \"testOrg1User\", \"appraisedValue\": 300 }".getBytes();
+
+}
diff --git a/asset-transfer-private-data/chaincode-java/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/asset-transfer-private-data/chaincode-java/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 00000000..1f0955d4
--- /dev/null
+++ b/asset-transfer-private-data/chaincode-java/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
@@ -0,0 +1 @@
+mock-maker-inline