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