diff --git a/test-network-k8s/kubectl b/test-network-k8s/kubectl
new file mode 100644
index 00000000..848c8ed6
Binary files /dev/null and b/test-network-k8s/kubectl differ
diff --git a/token-erc-20/chaincode-java/Dockerfile b/token-erc-20/chaincode-java/Dockerfile
new file mode 100755
index 00000000..55aa22ba
--- /dev/null
+++ b/token-erc-20/chaincode-java/Dockerfile
@@ -0,0 +1,32 @@
+# SPDX-License-Identifier: Apache-2.0
+# the first stage
+FROM gradle:jdk11 AS GRADLE_BUILD
+
+# copy the build.gradle and src code to the container
+COPY src/ src/
+COPY build.gradle ./
+
+# Build and package our code
+RUN gradle --no-daemon build shadowJar -x checkstyleMain -x checkstyleTest
+
+
+# the second stage of our build just needs the compiled files
+FROM openjdk:11-jre
+ARG CC_SERVER_PORT=9999
+
+# Setup tini to work better handle signals
+ENV TINI_VERSION v0.19.0
+ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini
+RUN chmod +x /tini
+
+RUN addgroup --system javauser && useradd -g javauser javauser
+
+# copy only the artifacts we need from the first stage and discard the rest
+COPY --chown=javauser:javauser --from=GRADLE_BUILD /home/gradle/build/libs/chaincode.jar /chaincode.jar
+COPY --chown=javauser:javauser docker/docker-entrypoint.sh /docker-entrypoint.sh
+RUN chmod +x /docker-entrypoint.sh
+ENV PORT $CC_SERVER_PORT
+EXPOSE $CC_SERVER_PORT
+
+USER javauser
+ENTRYPOINT [ "/tini", "--", "/docker-entrypoint.sh" ]
diff --git a/token-erc-20/chaincode-java/build.gradle b/token-erc-20/chaincode-java/build.gradle
new file mode 100755
index 00000000..518d6f8b
--- /dev/null
+++ b/token-erc-20/chaincode-java/build.gradle
@@ -0,0 +1,88 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+plugins {
+ id 'com.github.johnrengelman.shadow' version '5.1.0'
+ id 'application'
+ id 'checkstyle'
+ id 'jacoco'
+}
+
+group 'org.hyperledger.fabric.samples'
+version '1.0-SNAPSHOT'
+
+dependencies {
+
+ implementation 'org.hyperledger.fabric-chaincode-java:fabric-chaincode-shim:2.4.1'
+ implementation 'org.json:json:+'
+ implementation 'com.owlike:genson:1.5'
+ testImplementation 'org.hyperledger.fabric-chaincode-java:fabric-chaincode-shim:2.4.1'
+ testImplementation 'org.junit.jupiter:junit-jupiter:5.4.2'
+ testImplementation 'org.assertj:assertj-core:3.11.1'
+ testImplementation 'org.mockito:mockito-core:2.+'
+ testRuntimeOnly("net.bytebuddy:byte-buddy:1.10.6")
+
+}
+
+repositories {
+ mavenCentral()
+ 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
+}
+
+jacocoTestCoverageVerification {
+ violationRules {
+ rule {
+ limit {
+ minimum = 0.8
+ }
+ }
+ }
+
+ finalizedBy jacocoTestReport
+}
+
+test {
+ useJUnitPlatform()
+ testLogging {
+ events "passed", "skipped", "failed"
+ }
+}
+
+mainClassName = 'org.hyperledger.fabric.contract.ContractRouter'
+
+shadowJar {
+ baseName = 'chaincode'
+ version = null
+ classifier = null
+
+ manifest {
+ attributes 'Main-Class': 'org.hyperledger.fabric.contract.ContractRouter'
+ }
+}
+
+check.dependsOn jacocoTestCoverageVerification
+installDist.dependsOn check
diff --git a/token-erc-20/chaincode-java/config/checkstyle/checkstyle.xml b/token-erc-20/chaincode-java/config/checkstyle/checkstyle.xml
new file mode 100755
index 00000000..acd5df44
--- /dev/null
+++ b/token-erc-20/chaincode-java/config/checkstyle/checkstyle.xml
@@ -0,0 +1,171 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/token-erc-20/chaincode-java/config/checkstyle/suppressions.xml b/token-erc-20/chaincode-java/config/checkstyle/suppressions.xml
new file mode 100755
index 00000000..6a7dbbc2
--- /dev/null
+++ b/token-erc-20/chaincode-java/config/checkstyle/suppressions.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
diff --git a/token-erc-20/chaincode-java/docker/docker-entrypoint.sh b/token-erc-20/chaincode-java/docker/docker-entrypoint.sh
new file mode 100755
index 00000000..179818db
--- /dev/null
+++ b/token-erc-20/chaincode-java/docker/docker-entrypoint.sh
@@ -0,0 +1,16 @@
+#!/usr/bin/env bash
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+set -euo pipefail
+: ${CORE_PEER_TLS_ENABLED:="false"}
+: ${DEBUG:="false"}
+
+if [ "${DEBUG,,}" = "true" ]; then
+ exec java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=0.0.0.0:8000 -jar /chaincode.jar
+elif [ "${CORE_PEER_TLS_ENABLED,,}" = "true" ]; then
+ exec java -jar /chaincode.jar # todo
+else
+ exec java -jar /chaincode.jar
+fi
+
diff --git a/token-erc-20/chaincode-java/gradle/wrapper/gradle-wrapper.jar b/token-erc-20/chaincode-java/gradle/wrapper/gradle-wrapper.jar
new file mode 100755
index 00000000..5c2d1cf0
Binary files /dev/null and b/token-erc-20/chaincode-java/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/token-erc-20/chaincode-java/gradle/wrapper/gradle-wrapper.properties b/token-erc-20/chaincode-java/gradle/wrapper/gradle-wrapper.properties
new file mode 100755
index 00000000..bb8b2fc2
--- /dev/null
+++ b/token-erc-20/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/token-erc-20/chaincode-java/gradlew b/token-erc-20/chaincode-java/gradlew
new file mode 100755
index 00000000..83f2acfd
--- /dev/null
+++ b/token-erc-20/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/token-erc-20/chaincode-java/gradlew.bat b/token-erc-20/chaincode-java/gradlew.bat
new file mode 100755
index 00000000..24467a14
--- /dev/null
+++ b/token-erc-20/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/token-erc-20/chaincode-java/settings.gradle b/token-erc-20/chaincode-java/settings.gradle
new file mode 100755
index 00000000..2908c4a0
--- /dev/null
+++ b/token-erc-20/chaincode-java/settings.gradle
@@ -0,0 +1,5 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+rootProject.name = 'erc721'
diff --git a/token-erc-20/chaincode-java/src/main/java/org/hyperledger/fabric/samples/erc20/ContractConstants.java b/token-erc-20/chaincode-java/src/main/java/org/hyperledger/fabric/samples/erc20/ContractConstants.java
new file mode 100644
index 00000000..9c283143
--- /dev/null
+++ b/token-erc-20/chaincode-java/src/main/java/org/hyperledger/fabric/samples/erc20/ContractConstants.java
@@ -0,0 +1,28 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.hyperledger.fabric.samples.erc20;
+
+/** ERC20 constants for KEYS ,EVENTS and MSP */
+public enum ContractConstants {
+ BALANCE_PREFIX("balance"),
+ ALLOWANCE_PREFIX("allowance"),
+ NAME_KEY("name"),
+ SYMBOL_KEY("decimals"),
+ DECIMALS_KEY("symbolKey"),
+ TOTAL_SUPPLY_KEY("totalSupply"),
+ TRANSFER_EVENT("Transfer"),
+ MINTER_ORG_MSPID("Org1MSP"),
+ APPROVAL("Approval");
+
+ private final String prefix;
+
+ ContractConstants(final String value) {
+ this.prefix = value;
+ }
+
+ public String getValue() {
+ return prefix;
+ }
+}
diff --git a/token-erc-20/chaincode-java/src/main/java/org/hyperledger/fabric/samples/erc20/ContractErrors.java b/token-erc-20/chaincode-java/src/main/java/org/hyperledger/fabric/samples/erc20/ContractErrors.java
new file mode 100644
index 00000000..4a0a5959
--- /dev/null
+++ b/token-erc-20/chaincode-java/src/main/java/org/hyperledger/fabric/samples/erc20/ContractErrors.java
@@ -0,0 +1,19 @@
+
+/*
+ * SPDX-License-Identifier: Apache-2.0
+*/
+package org.hyperledger.fabric.samples.erc20;
+/**
+ * @author Renjith
+ * ERC20 constants for exceptions.
+ */
+
+public enum ContractErrors {
+ BALANCE_NOT_FOUND,
+ UNAUTHORIZED_SENDER,
+ INVALID_AMOUNT,
+ NOT_FOUND,
+ INVALID_TRANSFER,
+ INSUFFICIENT_FUND,
+ NO_ALLOWANCE_FOUND
+}
diff --git a/token-erc-20/chaincode-java/src/main/java/org/hyperledger/fabric/samples/erc20/ERC20TokenContract.java b/token-erc-20/chaincode-java/src/main/java/org/hyperledger/fabric/samples/erc20/ERC20TokenContract.java
new file mode 100644
index 00000000..a8ed75c6
--- /dev/null
+++ b/token-erc-20/chaincode-java/src/main/java/org/hyperledger/fabric/samples/erc20/ERC20TokenContract.java
@@ -0,0 +1,469 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package org.hyperledger.fabric.samples.erc20;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.hyperledger.fabric.samples.erc20.ContractConstants.ALLOWANCE_PREFIX;
+import static org.hyperledger.fabric.samples.erc20.ContractConstants.APPROVAL;
+import static org.hyperledger.fabric.samples.erc20.ContractConstants.BALANCE_PREFIX;
+import static org.hyperledger.fabric.samples.erc20.ContractConstants.DECIMALS_KEY;
+import static org.hyperledger.fabric.samples.erc20.ContractConstants.NAME_KEY;
+import static org.hyperledger.fabric.samples.erc20.ContractConstants.SYMBOL_KEY;
+import static org.hyperledger.fabric.samples.erc20.ContractConstants.TOTAL_SUPPLY_KEY;
+import static org.hyperledger.fabric.samples.erc20.ContractConstants.TRANSFER_EVENT;
+import static org.hyperledger.fabric.samples.erc20.ContractErrors.BALANCE_NOT_FOUND;
+import static org.hyperledger.fabric.samples.erc20.ContractErrors.INSUFFICIENT_FUND;
+import static org.hyperledger.fabric.samples.erc20.ContractErrors.INVALID_AMOUNT;
+import static org.hyperledger.fabric.samples.erc20.ContractErrors.INVALID_TRANSFER;
+import static org.hyperledger.fabric.samples.erc20.ContractErrors.NOT_FOUND;
+import static org.hyperledger.fabric.samples.erc20.ContractErrors.NO_ALLOWANCE_FOUND;
+import static org.hyperledger.fabric.samples.erc20.ContractErrors.UNAUTHORIZED_SENDER;
+import static org.hyperledger.fabric.samples.erc20.utils.ContractUtility.stringIsNullOrEmpty;
+
+import com.owlike.genson.Genson;
+import org.hyperledger.fabric.Logger;
+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.samples.erc20.model.Approval;
+import org.hyperledger.fabric.samples.erc20.model.Transfer;
+import org.hyperledger.fabric.shim.ChaincodeException;
+import org.hyperledger.fabric.shim.ChaincodeStub;
+import org.hyperledger.fabric.shim.ledger.CompositeKey;
+
+@Contract(
+ name = "erc20token",
+ info =
+ @Info(
+ title = "ERC20Token Contract",
+ description = "The erc20 fungible token implementation.",
+ version = "0.0.1-SNAPSHOT",
+ license =
+ @License(
+ name = "Apache 2.0 License",
+ url = "http://www.apache.org/licenses/LICENSE-2.0.html"),
+ contact =
+ @Contact(
+ email = "renjithkn@gmail.com",
+ name = "Renjith Narayanan",
+ url = "https://hyperledger.example.com")))
+@Default
+public final class ERC20TokenContract implements ContractInterface {
+
+ final Logger logger = Logger.getLogger(ERC20TokenContract.class);
+
+ /**
+ * Mint creates new tokens and adds them to minter's account balance. This function triggers a
+ * Transfer event.
+ *
+ * @param ctx the transaction context
+ * @param amount of tokens to be minted
+ */
+ @Transaction(intent = Transaction.TYPE.SUBMIT)
+ public void Mint(final Context ctx, final long amount) {
+
+ // Check minter authorization - this sample assumes Org1 is the central banker with privilege to
+ // mint new tokens
+ String clientMSPID = ctx.getClientIdentity().getMSPID();
+ ChaincodeStub stub = ctx.getStub();
+ if (!clientMSPID.equalsIgnoreCase(ContractConstants.MINTER_ORG_MSPID.getValue())) {
+ throw new ChaincodeException(
+ "Client is not authorized to mint new tokens", UNAUTHORIZED_SENDER.toString());
+ }
+ // Get ID of submitting client identity
+ String minter = ctx.getClientIdentity().getId();
+ if (amount <= 0) {
+ throw new ChaincodeException(
+ "Mint amount must be a positive integer", INVALID_AMOUNT.toString());
+ }
+ CompositeKey balanceKey = stub.createCompositeKey(BALANCE_PREFIX.getValue(), minter);
+ String currentBalanceStr = stub.getStringState(balanceKey.toString());
+ // If minter current balance doesn't yet exist, we'll create it with a current balance of 0
+ long currentBalance = 0;
+ if (!stringIsNullOrEmpty(currentBalanceStr)) {
+ currentBalance = Long.parseLong(currentBalanceStr);
+ }
+ // Used safe math .
+ long updatedBalance = Math.addExact(currentBalance, amount);
+ stub.putStringState(balanceKey.toString(), String.valueOf(updatedBalance));
+ // Increase totalSupply
+ String totalSupplyStr = stub.getStringState(TOTAL_SUPPLY_KEY.getValue());
+ long totalSupply = 0;
+ if (!stringIsNullOrEmpty(totalSupplyStr)) {
+ totalSupply = Long.parseLong(totalSupplyStr);
+ }
+ // Used safe math .
+ totalSupply = Math.addExact(totalSupply, amount);
+ stub.putStringState(TOTAL_SUPPLY_KEY.getValue(), String.valueOf(totalSupply));
+ Transfer transferEvent = new Transfer("0x0", minter, amount);
+ stub.setEvent(TRANSFER_EVENT.getValue(), this.marshal(transferEvent));
+ logger.info(
+ String.format(
+ "minter account %s balance updated from %d to %d",
+ minter, currentBalance, updatedBalance));
+ }
+
+ /**
+ * Burn redeems tokens the minter's account balance. This function triggers a Transfer event.
+ *
+ * @param ctx the transaction context
+ * @param amount amount of tokens to be burned
+ */
+ @Transaction(intent = Transaction.TYPE.SUBMIT)
+ public void Burn(final Context ctx, final long amount) {
+
+ // Check minter authorization - this sample assumes Org1 is the central banker with privilege to
+ // burn tokens
+
+ String clientMSPID = ctx.getClientIdentity().getMSPID();
+ ChaincodeStub stub = ctx.getStub();
+ if (!clientMSPID.equalsIgnoreCase(ContractConstants.MINTER_ORG_MSPID.getValue())) {
+ throw new ChaincodeException(
+ "Client is not authorized to burn tokens", UNAUTHORIZED_SENDER.toString());
+ }
+ String minter = ctx.getClientIdentity().getId();
+ if (amount <= 0) {
+ throw new ChaincodeException(
+ "Burn amount must be a positive integer", INVALID_AMOUNT.toString());
+ }
+ CompositeKey balanceKey = stub.createCompositeKey(BALANCE_PREFIX.getValue(), minter);
+ String currentBalanceStr = stub.getStringState(balanceKey.toString());
+ if (stringIsNullOrEmpty(currentBalanceStr)) {
+ throw new ChaincodeException("The balance does not exist", BALANCE_NOT_FOUND.toString());
+ }
+ long currentBalance = Long.parseLong(currentBalanceStr);
+ // Check if the sender has enough tokens to burn.
+
+ if (currentBalance < amount) {
+ String errorMessage = String.format("Client account %s has insufficient funds", minter);
+ throw new ChaincodeException(errorMessage, INSUFFICIENT_FUND.toString());
+ }
+ long updatedBalance = Math.subtractExact(currentBalance, amount);
+ stub.putStringState(balanceKey.toString(), String.valueOf(updatedBalance));
+ // Decrease totalSupply
+ String totalSupplyBytes = stub.getStringState(TOTAL_SUPPLY_KEY.getValue());
+ if (stringIsNullOrEmpty(totalSupplyBytes)) {
+ throw new ChaincodeException("TotalSupply does not exist", NOT_FOUND.toString());
+ }
+ long totalSupply = Math.subtractExact(Long.parseLong(totalSupplyBytes), amount);
+ stub.putStringState(TOTAL_SUPPLY_KEY.getValue(), String.valueOf(totalSupply));
+ // Emit the Transfer event
+ final Transfer transferEvent = new Transfer(minter, "0x0", amount);
+ stub.setEvent(TRANSFER_EVENT.getValue(), this.marshal(transferEvent));
+ logger.info(
+ String.format(
+ "minter account %s balance updated from %d to %d",
+ minter, currentBalance, updatedBalance));
+ }
+
+ /**
+ * Transfer transfers tokens from client account to recipient account. Recipient account must be a
+ * valid client Id as returned by the ClientID() function must be a valid clientID as returned by
+ * the ClientAccountID() function. This function triggers a Transfer event.
+ *
+ * @param ctx the transaction context
+ * @param to the recipient
+ * @param value the amount of token to be transferred
+ */
+ @Transaction(intent = Transaction.TYPE.SUBMIT)
+ public void Transfer(final Context ctx, final String to, final long value) {
+ String from = ctx.getClientIdentity().getId();
+ this.transferHelper(ctx, from, to, value);
+ final Transfer transferEvent = new Transfer(from, to, value);
+ ctx.getStub().setEvent(TRANSFER_EVENT.getValue(), this.marshal(transferEvent));
+ }
+
+ /**
+ * BalanceOf returns the balance of the given account.
+ *
+ * @param ctx the transaction context
+ * @param owner the owner from which the balance will be retrieved
+ * @return the account balance
+ */
+ @Transaction(intent = Transaction.TYPE.EVALUATE)
+ public long BalanceOf(final Context ctx, final String owner) {
+ ChaincodeStub stub = ctx.getStub();
+ CompositeKey balanceKey = stub.createCompositeKey(BALANCE_PREFIX.getValue(), owner);
+ String balance = stub.getStringState(balanceKey.toString());
+ if (stringIsNullOrEmpty(balance)) {
+ String errorMessage = String.format("Balance of the owner %s not exists", owner);
+ throw new ChaincodeException(errorMessage, NOT_FOUND.toString());
+ }
+ logger.info(String.format("%s has balance of %s tokens", owner, balance));
+ return Long.parseLong(balance);
+ }
+
+ /**
+ * ClientAccountBalance returns the balance of the requesting client's account.
+ *
+ * @param ctx the transaction context
+ * @return client the account balance
+ */
+ @Transaction(intent = Transaction.TYPE.EVALUATE)
+ public long ClientAccountBalance(final Context ctx) {
+ // Get ID of submitting client identity
+ ChaincodeStub stub = ctx.getStub();
+ String clientAccountID = ctx.getClientIdentity().getId();
+ CompositeKey balanceKey = stub.createCompositeKey(BALANCE_PREFIX.getValue(), clientAccountID);
+ String balanceBytes = stub.getStringState(balanceKey.toString());
+ if (stringIsNullOrEmpty(balanceBytes)) {
+ String errorMessage = String.format("The account %s does not exist", clientAccountID);
+ throw new ChaincodeException(errorMessage, NOT_FOUND.toString());
+ }
+ long balance = Long.parseLong(balanceBytes);
+ logger.info(String.format("%s has balance of %d tokens", clientAccountID, balance));
+ return balance;
+ }
+
+ /**
+ * ClientAccountID returns the id of the requesting client's account. In this implementation, the
+ * client account ID is the clientId itself. Users can use this function to get their own account
+ * id, which they can then give to others as the payment address.
+ *
+ * @return client account id .
+ */
+ @Transaction(intent = Transaction.TYPE.EVALUATE)
+ public String ClientAccountID(final Context ctx) {
+ // Get ID of submitting client identity
+ return ctx.getClientIdentity().getId();
+ }
+
+ /**
+ * Return the total token supply.
+ *
+ * @param ctx the transaction context
+ * @return the total token supply
+ */
+ @Transaction(intent = Transaction.TYPE.EVALUATE)
+ public long TotalSupply(final Context ctx) {
+ String totalSupply = ctx.getStub().getStringState(TOTAL_SUPPLY_KEY.getValue());
+ if (stringIsNullOrEmpty(totalSupply)) {
+ throw new ChaincodeException("Total Supply not found", NOT_FOUND.toString());
+ }
+ logger.info(String.format("TotalSupply: %s tokens", totalSupply));
+ return Long.parseLong(totalSupply);
+ }
+
+ /**
+ * Allows `spender` to spend `value` amount of tokens from the owner.
+ *
+ * @param ctx the transaction context
+ * @param spender The spender
+ * @param value The amount of tokens to be approved for transfer
+ */
+ @Transaction(intent = Transaction.TYPE.SUBMIT)
+ public void Approve(final Context ctx, final String spender, final long value) {
+ ChaincodeStub stub = ctx.getStub();
+ String owner = ctx.getClientIdentity().getId();
+ CompositeKey allowanceKey =
+ stub.createCompositeKey(ALLOWANCE_PREFIX.getValue(), owner, spender);
+ stub.putStringState(allowanceKey.toString(), String.valueOf(value));
+ Approval approval = new Approval(owner, spender, value);
+ stub.setEvent(APPROVAL.getValue(), this.marshal(approval));
+ logger.info(
+ String.format(
+ "client %s approved a withdrawal allowance of %d for spender %s",
+ owner, value, spender));
+ }
+
+ /**
+ * Returns the amount of tokens which `spender` is allowed to withdraw from `owner`.
+ *
+ * @param ctx the transaction context
+ * @param owner The owner of tokens
+ * @param spender The spender who are able to transfer the tokens
+ * @return the amount of remaining tokens allowed to spent
+ */
+ @Transaction(intent = Transaction.TYPE.SUBMIT)
+ public long Allowance(final Context ctx, final String owner, final String spender) {
+ ChaincodeStub stub = ctx.getStub();
+ CompositeKey allowanceKey =
+ stub.createCompositeKey(ALLOWANCE_PREFIX.getValue(), owner, spender);
+ String allowanceBytes = stub.getStringState(allowanceKey.toString());
+ long allowance = 0;
+ if (!stringIsNullOrEmpty(allowanceBytes)) {
+ allowance = Long.parseLong(allowanceBytes);
+ }
+ logger.info(
+ String.format(
+ "The allowance left for spender %s to withdraw from owner %s: %d",
+ spender, owner, allowance));
+ return allowance;
+ }
+
+ /**
+ * Transfer `value` amount of tokens from `from` to `to`.
+ *
+ * @param ctx the transaction context
+ * @param from The sender
+ * @param to The recipient
+ * @param value The amount of token to be transferred
+ */
+ @Transaction(intent = Transaction.TYPE.SUBMIT)
+ public void TransferFrom(
+ final Context ctx, final String from, final String to, final long value) {
+ String spender = ctx.getClientIdentity().getId();
+ ChaincodeStub stub = ctx.getStub();
+ // Retrieve the allowance of the spender
+ CompositeKey allowanceKey = stub.createCompositeKey(ALLOWANCE_PREFIX.getValue(), from, spender);
+ String currentAllowanceStr = stub.getStringState(allowanceKey.toString());
+ if (stringIsNullOrEmpty(currentAllowanceStr)) {
+ String errorMessage = String.format("Spender %s has no allowance from %s", spender, from);
+ throw new ChaincodeException(errorMessage, NO_ALLOWANCE_FOUND.toString());
+ }
+ long currentAllowance = Long.parseLong(currentAllowanceStr);
+ // Check if the transferred value is less than the allowance
+ if (currentAllowance < value) {
+ String errorMessage =
+ String.format("Spender %s does not have enough allowance to spend", spender);
+ throw new ChaincodeException(errorMessage, INSUFFICIENT_FUND.toString());
+ }
+ this.transferHelper(ctx, from, to, value);
+ // Decrease the allowance
+ long updatedAllowance = currentAllowance - value;
+ stub.putStringState(allowanceKey.toString(), String.valueOf(updatedAllowance));
+ final Transfer transferEvent = new Transfer(from, to, value);
+ stub.setEvent(TRANSFER_EVENT.getValue(), marshal(transferEvent));
+ logger.info(
+ String.format(
+ "spender %s allowance updated from %d to %d",
+ spender, currentAllowance, updatedAllowance));
+ }
+
+ /**
+ * This is a helper function function that transfers tokens from the "from" address to the "to"
+ * address. Dependent functions include Transfer and TransferFrom
+ *
+ * @param ctx the transaction context
+ * @param from the sender
+ * @param to the receiver
+ * @param value the amount.
+ */
+ private void transferHelper(
+ final Context ctx, final String from, final String to, final long value) {
+
+ if (from.equalsIgnoreCase(to)) {
+ throw new ChaincodeException(
+ "Cannot transfer to and from same client account", INVALID_TRANSFER.toString());
+ }
+ // transfer of 0 is allowed in ERC20, so just validate against negative amounts
+ if (value < 0) {
+ throw new ChaincodeException("Transfer amount cannot be negative", INVALID_AMOUNT.toString());
+ }
+ ChaincodeStub stub = ctx.getStub();
+ // Retrieve the current balance of the sender
+ CompositeKey fromBalanceKey = stub.createCompositeKey(BALANCE_PREFIX.getValue(), from);
+ String fromCurrentBalanceStr = stub.getStringState(fromBalanceKey.toString());
+ if (stringIsNullOrEmpty(fromCurrentBalanceStr)) {
+ String errorMessage = String.format("Client account %s has no balance", from);
+ throw new ChaincodeException(errorMessage, INSUFFICIENT_FUND.toString());
+ }
+ long fromCurrentBalance = Long.parseLong(fromCurrentBalanceStr);
+ // Check if the sender has enough tokens to spend.
+ if (fromCurrentBalance < value) {
+ String errorMessage = String.format("Client account %s has insufficient funds", from);
+ throw new ChaincodeException(errorMessage, INSUFFICIENT_FUND.toString());
+ }
+ // Retrieve the current balance of the recipient
+ CompositeKey toBalanceKey = stub.createCompositeKey(BALANCE_PREFIX.getValue(), to);
+ String toCurrentBalanceStr = stub.getStringState(toBalanceKey.toString());
+ long toCurrentBalance = 0;
+ // If recipient current balance doesn't yet exist, we'll create it with a
+ // current balance of 0
+ if (!stringIsNullOrEmpty(toCurrentBalanceStr)) {
+ toCurrentBalance = Long.parseLong(toCurrentBalanceStr.trim());
+ }
+ // Update the balance
+ long fromUpdatedBalance = Math.subtractExact(fromCurrentBalance, value);
+ long toUpdatedBalance = Math.addExact(toCurrentBalance, value);
+ stub.putStringState(fromBalanceKey.toString(), String.valueOf(fromUpdatedBalance));
+ stub.putStringState(toBalanceKey.toString(), String.valueOf(toUpdatedBalance));
+ logger.info(
+ String.format(
+ "client %s balance updated from %d to %d",
+ from, fromCurrentBalance, fromUpdatedBalance));
+ logger.info(
+ String.format(
+ "recipient %s balance updated from %d to %d", to, toCurrentBalance, toUpdatedBalance));
+ }
+
+ /**
+ * Set optional information for a token.
+ *
+ * @param ctx the transaction context
+ * @param name The name of the token
+ * @param symbol The symbol of the token
+ * @param decimals The decimals of the token
+ */
+ @Transaction(intent = Transaction.TYPE.SUBMIT)
+ public void SetOptions(
+ final Context ctx, final String name, final String symbol, final String decimals) {
+ ChaincodeStub stub = ctx.getStub();
+ stub.putStringState(NAME_KEY.getValue(), name);
+ stub.putStringState(SYMBOL_KEY.getValue(), symbol);
+ stub.putStringState(DECIMALS_KEY.getValue(), decimals);
+ }
+
+ /**
+ * Return the name of the token - e.g. "MyToken". The original function name is `name` in ERC20
+ * specification. However, 'name' conflicts with a parameter `name` in `Contract` class. As a work
+ * around, we use `TokenName` as an alternative function name.
+ *
+ * @param ctx the transaction context
+ * @return the name of the token
+ */
+ @Transaction(intent = Transaction.TYPE.EVALUATE)
+ public String TokenName(final Context ctx) {
+ String tokenName = ctx.getStub().getStringState(ContractConstants.NAME_KEY.getValue());
+ if (stringIsNullOrEmpty(tokenName)) {
+ throw new ChaincodeException("Token name not found", NOT_FOUND.toString());
+ }
+ return tokenName;
+ }
+
+ /**
+ * Return the symbol of the token.
+ *
+ * @param ctx the transaction context
+ * @return the symbol of the token
+ */
+ @Transaction(intent = Transaction.TYPE.EVALUATE)
+ public String TokenSymbol(final Context ctx) {
+ String tokenSymbol = ctx.getStub().getStringState(SYMBOL_KEY.getValue());
+ if (stringIsNullOrEmpty(tokenSymbol)) {
+ throw new ChaincodeException("Token symbol not found", NOT_FOUND.toString());
+ }
+ return tokenSymbol;
+ }
+
+ /**
+ * Return the number of decimals the token uses e.g. 8, means to divide the token amount by
+ * 100000000 to get its user representation.
+ *
+ * @param ctx the transaction context
+ * @return the number of decimals
+ */
+ @Transaction(intent = Transaction.TYPE.EVALUATE)
+ public int Decimals(final Context ctx) {
+ String decimals = ctx.getStub().getStringState(DECIMALS_KEY.getValue());
+ if (stringIsNullOrEmpty(decimals)) {
+ throw new ChaincodeException("Decimal not found", NOT_FOUND.toString());
+ }
+ return Integer.parseInt(decimals);
+ }
+ /**
+ * marshal the event data
+ *
+ * @param obj the object to marshal.
+ * @return marshalled object.
+ */
+ private byte[] marshal(final Object obj) {
+ return new Genson().serialize(obj).getBytes(UTF_8);
+ }
+}
diff --git a/token-erc-20/chaincode-java/src/main/java/org/hyperledger/fabric/samples/erc20/model/Approval.java b/token-erc-20/chaincode-java/src/main/java/org/hyperledger/fabric/samples/erc20/model/Approval.java
new file mode 100644
index 00000000..c8226475
--- /dev/null
+++ b/token-erc-20/chaincode-java/src/main/java/org/hyperledger/fabric/samples/erc20/model/Approval.java
@@ -0,0 +1,58 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package org.hyperledger.fabric.samples.erc20.model;
+
+import com.owlike.genson.annotation.JsonProperty;
+import org.hyperledger.fabric.contract.annotation.DataType;
+import org.hyperledger.fabric.contract.annotation.Property;
+
+@DataType()
+public final class Approval {
+
+ @Property()
+ @JsonProperty("owner")
+ private String owner;
+
+ @Property()
+ @JsonProperty("spender")
+ private String spender;
+
+ @Property()
+ @JsonProperty("value")
+ private long value;
+
+ /** Default constructor */
+ public Approval() {
+ super();
+ }
+
+ /**
+ * Constructor of the class
+ *
+ * @param owner token owner
+ * @param spender approved spender of the token
+ * @param value amount approved as allowance
+ */
+ public Approval(
+ @JsonProperty("owner") final String owner,
+ @JsonProperty("spender") final String spender,
+ @JsonProperty("value") final long value) {
+ super();
+ this.owner = owner;
+ this.spender = spender;
+ this.value = value;
+ }
+
+ public String getOwner() {
+ return owner;
+ }
+
+ public String getSpender() {
+ return spender;
+ }
+
+ public long getValue() {
+ return value;
+ }
+}
diff --git a/token-erc-20/chaincode-java/src/main/java/org/hyperledger/fabric/samples/erc20/model/Transfer.java b/token-erc-20/chaincode-java/src/main/java/org/hyperledger/fabric/samples/erc20/model/Transfer.java
new file mode 100644
index 00000000..0e7275c6
--- /dev/null
+++ b/token-erc-20/chaincode-java/src/main/java/org/hyperledger/fabric/samples/erc20/model/Transfer.java
@@ -0,0 +1,58 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package org.hyperledger.fabric.samples.erc20.model;
+
+import com.owlike.genson.annotation.JsonProperty;
+import org.hyperledger.fabric.contract.annotation.DataType;
+import org.hyperledger.fabric.contract.annotation.Property;
+
+@DataType()
+public final class Transfer {
+
+ @Property()
+ @JsonProperty("from")
+ private String from;
+
+ @Property()
+ @JsonProperty("to")
+ private String to;
+
+ @Property()
+ @JsonProperty("value")
+ private long value;
+
+ /** Default constructor */
+ public Transfer() {
+ super();
+ }
+
+ /**
+ * Constructor of the class
+ *
+ * @param from owner of the token
+ * @param to token receiver
+ * @param value amount to be transferred
+ */
+ public Transfer(
+ @JsonProperty("from") final String from,
+ @JsonProperty("to") final String to,
+ @JsonProperty("value") final long value) {
+ super();
+ this.from = from;
+ this.to = to;
+ this.value = value;
+ }
+
+ public String getFrom() {
+ return from;
+ }
+
+ public String getTo() {
+ return to;
+ }
+
+ public long getValue() {
+ return value;
+ }
+}
diff --git a/token-erc-20/chaincode-java/src/main/java/org/hyperledger/fabric/samples/erc20/utils/ContractUtility.java b/token-erc-20/chaincode-java/src/main/java/org/hyperledger/fabric/samples/erc20/utils/ContractUtility.java
new file mode 100644
index 00000000..bf952314
--- /dev/null
+++ b/token-erc-20/chaincode-java/src/main/java/org/hyperledger/fabric/samples/erc20/utils/ContractUtility.java
@@ -0,0 +1,19 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package org.hyperledger.fabric.samples.erc20.utils;
+/**
+ * @author Renjith
+ * @Desc Utility class
+ */
+
+public final class ContractUtility {
+
+ private ContractUtility() {
+ }
+
+
+ public static boolean stringIsNullOrEmpty(final String string) {
+ return string == null || string.isEmpty();
+ }
+}
diff --git a/token-erc-20/chaincode-java/src/test/java/org/hyperledger/fabric/samples/erc20/TokenERC20ContractTest.java b/token-erc-20/chaincode-java/src/test/java/org/hyperledger/fabric/samples/erc20/TokenERC20ContractTest.java
new file mode 100644
index 00000000..a38ac986
--- /dev/null
+++ b/token-erc-20/chaincode-java/src/test/java/org/hyperledger/fabric/samples/erc20/TokenERC20ContractTest.java
@@ -0,0 +1,565 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package org.hyperledger.fabric.samples.erc20;
+
+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;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.catchThrowable;
+import static org.hyperledger.fabric.samples.erc20.ContractConstants.ALLOWANCE_PREFIX;
+import static org.hyperledger.fabric.samples.erc20.ContractConstants.BALANCE_PREFIX;
+import static org.hyperledger.fabric.samples.erc20.ContractConstants.DECIMALS_KEY;
+import static org.hyperledger.fabric.samples.erc20.ContractConstants.MINTER_ORG_MSPID;
+import static org.hyperledger.fabric.samples.erc20.ContractConstants.NAME_KEY;
+import static org.hyperledger.fabric.samples.erc20.ContractConstants.SYMBOL_KEY;
+import static org.hyperledger.fabric.samples.erc20.ContractConstants.TOTAL_SUPPLY_KEY;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class TokenERC20ContractTest {
+
+ private final String org1UserId =
+ "x509::CN=User0@org1.example.com, L=San Francisco, ST=California,"
+ + " C=US::CN=ca.org2.example.com, O=org2.example.com, L=San Francisco, ST=California, C=US";
+ private final String spender =
+ "x509::CN=User1@org2.example.com, L=San Francisco, ST=California,"
+ + " C=US::CN=ca.org2.example.com, O=org2.example.com, L=San Francisco, ST=California, C=US";
+
+ @Nested
+ class InvokeQueryERC20TokenOptionsTransaction {
+
+ @Test
+ public void whenTokenNameExists() {
+ ERC20TokenContract contract = new ERC20TokenContract();
+ Context ctx = mock(Context.class);
+ ChaincodeStub stub = mock(ChaincodeStub.class);
+ when(ctx.getStub()).thenReturn(stub);
+ when(stub.getStringState(NAME_KEY.getValue())).thenReturn("ARBTToken");
+ String tokenName = contract.TokenName(ctx);
+ assertThat(tokenName).isEqualTo("ARBTToken");
+ }
+
+ @Test
+ public void whenTokenNameDoesNotExist() {
+ ERC20TokenContract contract = new ERC20TokenContract();
+ final Context ctx = mock(Context.class);
+ ChaincodeStub stub = mock(ChaincodeStub.class);
+ when(ctx.getStub()).thenReturn(stub);
+ when(stub.getStringState(NAME_KEY.getValue())).thenReturn("");
+ Throwable thrown = catchThrowable(() -> contract.TokenName(ctx));
+ assertThat(thrown)
+ .isInstanceOf(ChaincodeException.class)
+ .hasNoCause()
+ .hasMessage("Token name not found");
+ }
+
+ @Test
+ public void whenTokenSymbolExists() {
+ ERC20TokenContract contract = new ERC20TokenContract();
+ Context ctx = mock(Context.class);
+ ChaincodeStub stub = mock(ChaincodeStub.class);
+ when(ctx.getStub()).thenReturn(stub);
+ when(stub.getStringState(SYMBOL_KEY.getValue())).thenReturn("ARBT");
+ String toknName = contract.TokenSymbol(ctx);
+ assertThat(toknName).isEqualTo("ARBT");
+ }
+
+ @Test
+ public void whenTokenSymbolDoesNotExist() {
+ ERC20TokenContract contract = new ERC20TokenContract();
+ Context ctx = mock(Context.class);
+ ChaincodeStub stub = mock(ChaincodeStub.class);
+ when(ctx.getStub()).thenReturn(stub);
+ when(stub.getStringState(SYMBOL_KEY.getValue())).thenReturn("");
+ Throwable thrown = catchThrowable(() -> contract.TokenSymbol(ctx));
+ assertThat(thrown)
+ .isInstanceOf(ChaincodeException.class)
+ .hasNoCause()
+ .hasMessage("Token symbol not found");
+ }
+
+ @Test
+ public void whenTokenDecimalExists() {
+ ERC20TokenContract contract = new ERC20TokenContract();
+ Context ctx = mock(Context.class);
+ ChaincodeStub stub = mock(ChaincodeStub.class);
+ when(ctx.getStub()).thenReturn(stub);
+ when(stub.getStringState(DECIMALS_KEY.getValue())).thenReturn("18");
+ long decimal = contract.Decimals(ctx);
+ assertThat(decimal).isEqualTo(18);
+ }
+
+ @Test
+ public void whenTokenDecimalNotExists() {
+ ERC20TokenContract contract = new ERC20TokenContract();
+ Context ctx = mock(Context.class);
+ ChaincodeStub stub = mock(ChaincodeStub.class);
+ when(ctx.getStub()).thenReturn(stub);
+ when(stub.getStringState(DECIMALS_KEY.getValue())).thenReturn("");
+ Throwable thrown = catchThrowable(() -> contract.Decimals(ctx));
+ assertThat(thrown)
+ .isInstanceOf(ChaincodeException.class)
+ .hasNoCause()
+ .hasMessage("Decimal not found");
+ }
+
+ @Test
+ public void whenTokenTotalSupplyExists() {
+ ERC20TokenContract contract = new ERC20TokenContract();
+ Context ctx = mock(Context.class);
+ ChaincodeStub stub = mock(ChaincodeStub.class);
+ when(ctx.getStub()).thenReturn(stub);
+ when(stub.getStringState(TOTAL_SUPPLY_KEY.getValue())).thenReturn("222222222222");
+ long totalSupply = contract.TotalSupply(ctx);
+ assertThat(totalSupply).isEqualTo(222222222222L);
+ }
+
+ @Test
+ public void whenTokenTotalSupplyNotExists() {
+ ERC20TokenContract contract = new ERC20TokenContract();
+ Context ctx = mock(Context.class);
+ ChaincodeStub stub = mock(ChaincodeStub.class);
+ when(ctx.getStub()).thenReturn(stub);
+ when(stub.getStringState(TOTAL_SUPPLY_KEY.getValue())).thenReturn("");
+ Throwable thrown = catchThrowable(() -> contract.TotalSupply(ctx));
+ assertThat(thrown)
+ .isInstanceOf(ChaincodeException.class)
+ .hasNoCause()
+ .hasMessage("Total Supply not found");
+ }
+
+ @Test
+ public void ClientAccountIDTest() {
+ ERC20TokenContract contract = new ERC20TokenContract();
+ Context ctx = mock(Context.class);
+ ChaincodeStub stub = mock(ChaincodeStub.class);
+ ClientIdentity ci = mock(ClientIdentity.class);
+ when(ctx.getClientIdentity()).thenReturn(ci);
+ when(ci.getMSPID()).thenReturn(MINTER_ORG_MSPID.getValue());
+ when(ci.getId()).thenReturn(org1UserId);
+ when(ctx.getStub()).thenReturn(stub);
+ assertThat(ci.getMSPID()).isEqualTo(MINTER_ORG_MSPID.getValue());
+ String id = contract.ClientAccountID(ctx);
+ assertThat(id).isEqualTo(org1UserId);
+ }
+ }
+
+ @Nested
+ class TokenOperationsInvoke {
+
+ @Test
+ public void invokeSetOptionsTest() {
+ ERC20TokenContract contract = new ERC20TokenContract();
+ Context ctx = mock(Context.class);
+ ChaincodeStub stub = mock(ChaincodeStub.class);
+ when(ctx.getStub()).thenReturn(stub);
+ contract.SetOptions(ctx, "ARBTToken", "ARBT", "18");
+ verify(stub).putStringState(NAME_KEY.getValue(), "ARBTToken");
+ verify(stub).putStringState(SYMBOL_KEY.getValue(), "ARBT");
+ verify(stub).putStringState(DECIMALS_KEY.getValue(), "18");
+ }
+
+ @Test
+ public void invokeBalanceOfTest() {
+ ERC20TokenContract contract = new ERC20TokenContract();
+ Context ctx = mock(Context.class);
+ ChaincodeStub stub = mock(ChaincodeStub.class);
+ when(ctx.getStub()).thenReturn(stub);
+ ClientIdentity ci = mock(ClientIdentity.class);
+ when(ctx.getClientIdentity()).thenReturn(ci);
+ when(ci.getMSPID()).thenReturn(MINTER_ORG_MSPID.getValue());
+ when(ci.getId()).thenReturn(org1UserId);
+ when(ctx.getStub()).thenReturn(stub);
+ CompositeKey ck = mock(CompositeKey.class);
+ when(stub.createCompositeKey(BALANCE_PREFIX.getValue(), org1UserId)).thenReturn(ck);
+ when(ck.toString()).thenReturn(BALANCE_PREFIX.getValue() + org1UserId);
+ when(stub.getStringState(ck.toString())).thenReturn("1000");
+ long balance = contract.BalanceOf(ctx, org1UserId);
+ assertThat(balance).isEqualTo(1000);
+ }
+
+ @Test
+ public void invokeClientAccountBalanceTest() {
+ ERC20TokenContract contract = new ERC20TokenContract();
+ Context ctx = mock(Context.class);
+ ChaincodeStub stub = mock(ChaincodeStub.class);
+ when(ctx.getStub()).thenReturn(stub);
+ ClientIdentity ci = mock(ClientIdentity.class);
+ when(ctx.getClientIdentity()).thenReturn(ci);
+ when(ci.getMSPID()).thenReturn(MINTER_ORG_MSPID.getValue());
+ when(ci.getId()).thenReturn(org1UserId);
+ when(ctx.getStub()).thenReturn(stub);
+ CompositeKey ck = mock(CompositeKey.class);
+ when(stub.createCompositeKey(BALANCE_PREFIX.getValue(), org1UserId)).thenReturn(ck);
+ when(ck.toString()).thenReturn(BALANCE_PREFIX.getValue() + org1UserId);
+ when(stub.getStringState(ck.toString())).thenReturn("1000");
+ long balance = contract.ClientAccountBalance(ctx);
+ assertThat(balance).isEqualTo(1000);
+ }
+
+ @Test
+ public void invokeMintTokenTest() {
+
+ ERC20TokenContract contract = new ERC20TokenContract();
+ Context ctx = mock(Context.class);
+ ChaincodeStub stub = mock(ChaincodeStub.class);
+ ClientIdentity ci = mock(ClientIdentity.class);
+ when(ctx.getClientIdentity()).thenReturn(ci);
+ when(ci.getMSPID()).thenReturn(MINTER_ORG_MSPID.getValue());
+ when(ci.getId()).thenReturn(org1UserId);
+ when(ctx.getStub()).thenReturn(stub);
+ CompositeKey ck = mock(CompositeKey.class);
+ when(stub.createCompositeKey(BALANCE_PREFIX.getValue(), org1UserId)).thenReturn(ck);
+ when(ck.toString()).thenReturn(BALANCE_PREFIX.getValue() + org1UserId);
+ when(stub.getStringState(ck.toString())).thenReturn(null);
+ when(ctx.getStub()).thenReturn(stub);
+ contract.Mint(ctx, 1000);
+ verify(stub).putStringState(TOTAL_SUPPLY_KEY.getValue(), "1000");
+ verify(stub).putStringState(ck.toString(), "1000");
+ }
+
+ @Test
+ public void whenMintTokenUnAuthorized() {
+
+ ERC20TokenContract contract = new ERC20TokenContract();
+ Context ctx = mock(Context.class);
+ ChaincodeStub stub = mock(ChaincodeStub.class);
+ ClientIdentity ci = mock(ClientIdentity.class);
+ when(ctx.getClientIdentity()).thenReturn(ci);
+ when(ci.getMSPID()).thenReturn("Org2MSP");
+ when(ci.getId()).thenReturn(org1UserId);
+ when(ctx.getStub()).thenReturn(stub);
+ CompositeKey ck = mock(CompositeKey.class);
+ when(stub.createCompositeKey(BALANCE_PREFIX.getValue(), org1UserId)).thenReturn(ck);
+ when(ck.toString()).thenReturn(BALANCE_PREFIX.getValue() + org1UserId);
+ when(stub.getStringState(ck.toString())).thenReturn(null);
+ when(ctx.getStub()).thenReturn(stub);
+ Throwable thrown = catchThrowable(() -> contract.Mint(ctx, 1000));
+ assertThat(thrown)
+ .isInstanceOf(ChaincodeException.class)
+ .hasNoCause()
+ .hasMessage("Client is not authorized to mint new tokens");
+ }
+
+ @Test
+ public void invokeTokenTransferTest() {
+
+ ERC20TokenContract contract = new ERC20TokenContract();
+ Context ctx = mock(Context.class);
+ ChaincodeStub stub = mock(ChaincodeStub.class);
+ ClientIdentity ci = mock(ClientIdentity.class);
+ when(ctx.getClientIdentity()).thenReturn(ci);
+ when(ci.getMSPID()).thenReturn(MINTER_ORG_MSPID.getValue());
+ when(ci.getId()).thenReturn(org1UserId);
+ when(ctx.getStub()).thenReturn(stub);
+ String to =
+ "x509::CN=User2@org2.example.com, L=San Francisco, ST=California,"
+ + " C=US::CN=ca.org2.example.com, O=org2.example.com, L=San Francisco, ST=California, C=US";
+
+ CompositeKey ckFrom = mock(CompositeKey.class);
+ when(stub.createCompositeKey(BALANCE_PREFIX.getValue(), org1UserId)).thenReturn(ckFrom);
+ when(ckFrom.toString()).thenReturn(BALANCE_PREFIX.getValue() + org1UserId);
+ when(stub.getStringState(ckFrom.toString())).thenReturn("1000");
+ CompositeKey ckTo = mock(CompositeKey.class);
+ when(stub.createCompositeKey(BALANCE_PREFIX.getValue(), to)).thenReturn(ckTo);
+ when(ckTo.toString()).thenReturn(BALANCE_PREFIX.getValue() + to);
+ when(stub.getStringState(ckTo.toString())).thenReturn(null);
+ contract.Transfer(ctx, to, 100);
+ verify(stub).putStringState(ckTo.toString(), "100");
+ verify(stub).putStringState(ckFrom.toString(), "900");
+ }
+
+ @Test
+ public void whenZeroAmountTokenTransferTest() {
+
+ ERC20TokenContract contract = new ERC20TokenContract();
+ Context ctx = mock(Context.class);
+ ChaincodeStub stub = mock(ChaincodeStub.class);
+ ClientIdentity ci = mock(ClientIdentity.class);
+ when(ctx.getClientIdentity()).thenReturn(ci);
+ when(ci.getMSPID()).thenReturn(MINTER_ORG_MSPID.getValue());
+ when(ci.getId()).thenReturn(org1UserId);
+ when(ctx.getStub()).thenReturn(stub);
+ String to =
+ "x509::CN=User2@org2.example.com, L=San Francisco, ST=California,"
+ + " C=US::CN=ca.org2.example.com, O=org2.example.com, L=San Francisco, ST=California, C=US";
+
+ CompositeKey ckFrom = mock(CompositeKey.class);
+ when(stub.createCompositeKey(BALANCE_PREFIX.getValue(), org1UserId)).thenReturn(ckFrom);
+ when(ckFrom.toString()).thenReturn(BALANCE_PREFIX.getValue() + org1UserId);
+ when(stub.getStringState(ckFrom.toString())).thenReturn("1000");
+ CompositeKey ckTo = mock(CompositeKey.class);
+ when(stub.createCompositeKey(BALANCE_PREFIX.getValue(), to)).thenReturn(ckTo);
+ when(ckTo.toString()).thenReturn(BALANCE_PREFIX.getValue() + to);
+ when(stub.getStringState(ckTo.toString())).thenReturn(null);
+ contract.Transfer(ctx, to, 0);
+ verify(stub).putStringState(ckTo.toString(), "0");
+ verify(stub).putStringState(ckFrom.toString(), "1000");
+ }
+
+ @Test
+ public void whenTokenTransferNegativeAmount() {
+
+ ERC20TokenContract contract = new ERC20TokenContract();
+ Context ctx = mock(Context.class);
+ ChaincodeStub stub = mock(ChaincodeStub.class);
+ ClientIdentity ci = mock(ClientIdentity.class);
+ when(ctx.getClientIdentity()).thenReturn(ci);
+ when(ci.getMSPID()).thenReturn(MINTER_ORG_MSPID.getValue());
+ when(ci.getId()).thenReturn(org1UserId);
+ when(ctx.getStub()).thenReturn(stub);
+ String to =
+ "x509::CN=User2@org2.example.com, L=San Francisco, ST=California,"
+ + " C=US::CN=ca.org2.example.com, O=org2.example.com, L=San Francisco, ST=California, C=US";
+
+ CompositeKey ckFrom = mock(CompositeKey.class);
+ when(stub.createCompositeKey(BALANCE_PREFIX.getValue(), org1UserId)).thenReturn(ckFrom);
+ when(ckFrom.toString()).thenReturn(BALANCE_PREFIX.getValue() + org1UserId);
+ when(stub.getStringState(ckFrom.toString())).thenReturn("1000");
+ CompositeKey ckTo = mock(CompositeKey.class);
+ when(stub.createCompositeKey(BALANCE_PREFIX.getValue(), to)).thenReturn(ckTo);
+ when(ckTo.toString()).thenReturn(BALANCE_PREFIX.getValue() + to);
+ when(stub.getStringState(ckTo.toString())).thenReturn(null);
+
+ Throwable thrown = catchThrowable(() -> contract.Transfer(ctx, to, -1));
+ assertThat(thrown)
+ .isInstanceOf(ChaincodeException.class)
+ .hasNoCause()
+ .hasMessage("Transfer amount cannot be negative");
+ }
+
+ @Test
+ public void whenTokenTransferSameId() {
+
+ ERC20TokenContract contract = new ERC20TokenContract();
+ Context ctx = mock(Context.class);
+ ChaincodeStub stub = mock(ChaincodeStub.class);
+ ClientIdentity ci = mock(ClientIdentity.class);
+ when(ctx.getClientIdentity()).thenReturn(ci);
+ when(ci.getMSPID()).thenReturn(MINTER_ORG_MSPID.getValue());
+ when(ci.getId()).thenReturn(org1UserId);
+ when(ctx.getStub()).thenReturn(stub);
+ String to =
+ "x509::CN=User2@org2.example.com, L=San Francisco, ST=California,"
+ + " C=US::CN=ca.org2.example.com, O=org2.example.com, L=San Francisco, ST=California, C=US";
+
+ CompositeKey ckFrom = mock(CompositeKey.class);
+ when(stub.createCompositeKey(BALANCE_PREFIX.getValue(), org1UserId)).thenReturn(ckFrom);
+ when(ckFrom.toString()).thenReturn(BALANCE_PREFIX.getValue() + org1UserId);
+ when(stub.getStringState(ckFrom.toString())).thenReturn("1000");
+ CompositeKey ckTo = mock(CompositeKey.class);
+ when(stub.createCompositeKey(BALANCE_PREFIX.getValue(), to)).thenReturn(ckTo);
+ when(ckTo.toString()).thenReturn(BALANCE_PREFIX.getValue() + to);
+ when(stub.getStringState(ckTo.toString())).thenReturn(null);
+
+ Throwable thrown = catchThrowable(() -> contract.Transfer(ctx, org1UserId, 10));
+ assertThat(thrown)
+ .isInstanceOf(ChaincodeException.class)
+ .hasNoCause()
+ .hasMessage("Cannot transfer to and from same client account");
+ }
+
+ @Test
+ public void invokeTokenBurnTest() {
+
+ ERC20TokenContract contract = new ERC20TokenContract();
+ Context ctx = mock(Context.class);
+ ChaincodeStub stub = mock(ChaincodeStub.class);
+ ClientIdentity ci = mock(ClientIdentity.class);
+ when(ctx.getClientIdentity()).thenReturn(ci);
+ when(ci.getMSPID()).thenReturn(MINTER_ORG_MSPID.getValue());
+ when(ci.getId()).thenReturn(org1UserId);
+ CompositeKey ck = mock(CompositeKey.class);
+ when(stub.createCompositeKey(BALANCE_PREFIX.getValue(), org1UserId)).thenReturn(ck);
+ when(ck.toString()).thenReturn(BALANCE_PREFIX.getValue() + org1UserId);
+ when(stub.getStringState(ck.toString())).thenReturn(null);
+ when(ctx.getStub()).thenReturn(stub);
+ when(stub.getStringState(TOTAL_SUPPLY_KEY.getValue())).thenReturn("1000");
+ when(stub.getStringState(ck.toString())).thenReturn("1000");
+ contract.Burn(ctx, 100);
+ verify(stub).putStringState(TOTAL_SUPPLY_KEY.getValue(), "900");
+ }
+
+ @Test
+ public void whenTokenBurnUnAuthorizedTest() {
+ ERC20TokenContract contract = new ERC20TokenContract();
+ Context ctx = mock(Context.class);
+ ChaincodeStub stub = mock(ChaincodeStub.class);
+ ClientIdentity ci = mock(ClientIdentity.class);
+ when(ctx.getClientIdentity()).thenReturn(ci);
+ when(ci.getMSPID()).thenReturn("Org2MSP");
+ when(ci.getId()).thenReturn(spender);
+ CompositeKey ck = mock(CompositeKey.class);
+ when(stub.createCompositeKey(BALANCE_PREFIX.getValue(), spender)).thenReturn(ck);
+ when(ck.toString()).thenReturn(BALANCE_PREFIX.getValue() + spender);
+ when(stub.getStringState(ck.toString())).thenReturn(null);
+ when(ctx.getStub()).thenReturn(stub);
+ when(stub.getStringState(TOTAL_SUPPLY_KEY.getValue())).thenReturn("1000");
+ when(stub.getStringState(ck.toString())).thenReturn("1000");
+
+ Throwable thrown = catchThrowable(() -> contract.Burn(ctx, 100));
+ assertThat(thrown)
+ .isInstanceOf(ChaincodeException.class)
+ .hasNoCause()
+ .hasMessage("Client is not authorized to burn tokens");
+ }
+
+ @Test
+ public void whenTokenBurnNegativeAmountTest() {
+ ERC20TokenContract contract = new ERC20TokenContract();
+ Context ctx = mock(Context.class);
+ ChaincodeStub stub = mock(ChaincodeStub.class);
+ ClientIdentity ci = mock(ClientIdentity.class);
+ when(ctx.getClientIdentity()).thenReturn(ci);
+ when(ci.getMSPID()).thenReturn("Org1MSP");
+ when(ci.getId()).thenReturn(org1UserId);
+ CompositeKey ck = mock(CompositeKey.class);
+ when(stub.createCompositeKey(BALANCE_PREFIX.getValue(), org1UserId)).thenReturn(ck);
+ when(ck.toString()).thenReturn(BALANCE_PREFIX.getValue() + org1UserId);
+ when(stub.getStringState(ck.toString())).thenReturn(null);
+ when(ctx.getStub()).thenReturn(stub);
+ when(stub.getStringState(TOTAL_SUPPLY_KEY.getValue())).thenReturn("1000");
+ when(stub.getStringState(ck.toString())).thenReturn("1000");
+
+ Throwable thrown = catchThrowable(() -> contract.Burn(ctx, -100));
+ assertThat(thrown)
+ .isInstanceOf(ChaincodeException.class)
+ .hasNoCause()
+ .hasMessage("Burn amount must be a positive integer");
+ }
+ }
+
+ @Nested
+ class InvokeERC20AllowanceTransactions {
+
+ @Test
+ public void invokeAllowanceTest() {
+ ERC20TokenContract contract = new ERC20TokenContract();
+ Context ctx = mock(Context.class);
+ ChaincodeStub stub = mock(ChaincodeStub.class);
+ ClientIdentity ci = mock(ClientIdentity.class);
+ when(ctx.getClientIdentity()).thenReturn(ci);
+ when(ci.getMSPID()).thenReturn(MINTER_ORG_MSPID.getValue());
+ when(ci.getId()).thenReturn(org1UserId);
+ when(ctx.getStub()).thenReturn(stub);
+ CompositeKey ck = mock(CompositeKey.class);
+ when(stub.createCompositeKey(ALLOWANCE_PREFIX.getValue(), org1UserId, spender))
+ .thenReturn(ck);
+ when(ck.toString()).thenReturn(ALLOWANCE_PREFIX.getValue() + org1UserId + spender);
+ when(stub.getStringState(ck.toString())).thenReturn("100");
+ long allowance = contract.Allowance(ctx, org1UserId, spender);
+ assertThat(allowance).isEqualTo(100);
+ }
+
+ @Test
+ public void invokeApproveForTokenAllowanceTest() {
+ ERC20TokenContract contract = new ERC20TokenContract();
+ Context ctx = mock(Context.class);
+ ChaincodeStub stub = mock(ChaincodeStub.class);
+ ClientIdentity ci = mock(ClientIdentity.class);
+ when(ctx.getClientIdentity()).thenReturn(ci);
+ when(ci.getMSPID()).thenReturn(MINTER_ORG_MSPID.getValue());
+ when(ci.getId()).thenReturn(org1UserId);
+ when(ctx.getStub()).thenReturn(stub);
+ CompositeKey ck = mock(CompositeKey.class);
+ when(stub.createCompositeKey(ALLOWANCE_PREFIX.getValue(), org1UserId, spender))
+ .thenReturn(ck);
+ when(ck.toString()).thenReturn(ALLOWANCE_PREFIX.getValue() + org1UserId + spender);
+ contract.Approve(ctx, spender, 200);
+ verify(stub).putStringState(ck.toString(), String.valueOf(200));
+ }
+
+ @Test
+ public void invokeAllowanceTransferFromTest() {
+ ERC20TokenContract contract = new ERC20TokenContract();
+ Context ctx = mock(Context.class);
+ ChaincodeStub stub = mock(ChaincodeStub.class);
+ ClientIdentity ci;
+
+ String to =
+ "x509::CN=User3@org2.example.com, L=San Francisco, ST=California,"
+ + " C=US::CN=ca.org2.example.com, O=org2.example.com, L=San Francisco, ST=California, C=US";
+
+ CompositeKey ckFromBalance = mock(CompositeKey.class);
+ when(stub.createCompositeKey(BALANCE_PREFIX.toString(), org1UserId))
+ .thenReturn(ckFromBalance);
+ when(ckFromBalance.toString()).thenReturn(BALANCE_PREFIX.getValue() + org1UserId);
+ CompositeKey ckTOBalance = mock(CompositeKey.class);
+ when(stub.createCompositeKey(BALANCE_PREFIX.toString(), to)).thenReturn(ckTOBalance);
+ when(ckFromBalance.toString()).thenReturn(BALANCE_PREFIX.getValue() + to);
+ when(ctx.getStub()).thenReturn(stub);
+ ci = mock(ClientIdentity.class);
+ when(ctx.getClientIdentity()).thenReturn(ci);
+ when(ci.getMSPID()).thenReturn("Org2MSP");
+ when(ci.getId()).thenReturn(spender);
+ CompositeKey ckAllowance = mock(CompositeKey.class);
+ when(stub.createCompositeKey(ALLOWANCE_PREFIX.getValue(), org1UserId, spender))
+ .thenReturn(ckAllowance);
+ when(ckAllowance.toString()).thenReturn(ALLOWANCE_PREFIX.getValue() + org1UserId + spender);
+ when(stub.getStringState(ckAllowance.toString())).thenReturn("200");
+ CompositeKey ckFrom = mock(CompositeKey.class);
+ when(stub.createCompositeKey(BALANCE_PREFIX.getValue(), org1UserId)).thenReturn(ckFrom);
+ when(ckFrom.toString()).thenReturn(BALANCE_PREFIX.getValue() + org1UserId);
+ when(stub.getStringState(ckFrom.toString())).thenReturn("1000");
+ CompositeKey ckTo = mock(CompositeKey.class);
+ when(stub.createCompositeKey(BALANCE_PREFIX.getValue(), to)).thenReturn(ckTo);
+ when(ckTo.toString()).thenReturn(BALANCE_PREFIX.getValue() + to);
+ when(stub.getStringState(ckTo.toString())).thenReturn(null);
+ contract.TransferFrom(ctx, org1UserId, to, 100);
+ verify(stub).putStringState(ckTo.toString(), String.valueOf(100));
+ verify(stub).putStringState(ckFrom.toString(), String.valueOf(900));
+ }
+
+ @Test
+ public void whenClientSameAllowanceTransferFrom() {
+ ERC20TokenContract contract = new ERC20TokenContract();
+ Context ctx = mock(Context.class);
+ ChaincodeStub stub = mock(ChaincodeStub.class);
+ ClientIdentity ci;
+ String to =
+ "x509::CN=User3@org2.example.com, L=San Francisco, ST=California,"
+ + " C=US::CN=ca.org2.example.com, O=org2.example.com, L=San Francisco, ST=California, C=US";
+
+ CompositeKey ckFromBalance = mock(CompositeKey.class);
+ when(stub.createCompositeKey(BALANCE_PREFIX.getValue(), org1UserId))
+ .thenReturn(ckFromBalance);
+ when(ckFromBalance.toString()).thenReturn(BALANCE_PREFIX.getValue() + org1UserId);
+ CompositeKey ckTOBalance = mock(CompositeKey.class);
+ when(stub.createCompositeKey(BALANCE_PREFIX.getValue(), to)).thenReturn(ckTOBalance);
+ when(ckFromBalance.toString()).thenReturn(BALANCE_PREFIX.getValue() + to);
+ when(ctx.getStub()).thenReturn(stub);
+ ci = mock(ClientIdentity.class);
+ when(ctx.getClientIdentity()).thenReturn(ci);
+ when(ci.getMSPID()).thenReturn("Org2MSP");
+ when(ci.getId()).thenReturn(spender);
+ CompositeKey ckAllowance = mock(CompositeKey.class);
+ when(stub.createCompositeKey(ALLOWANCE_PREFIX.getValue(), org1UserId, spender))
+ .thenReturn(ckAllowance);
+ when(ckAllowance.toString()).thenReturn(ALLOWANCE_PREFIX.getValue() + org1UserId + spender);
+ when(stub.getStringState(ckAllowance.toString())).thenReturn("200");
+ CompositeKey ckFrom = mock(CompositeKey.class);
+ when(stub.createCompositeKey(BALANCE_PREFIX.getValue(), org1UserId)).thenReturn(ckFrom);
+ when(ckFrom.toString()).thenReturn(BALANCE_PREFIX.getValue() + org1UserId);
+ when(stub.getStringState(ckFrom.toString())).thenReturn("1000");
+ CompositeKey ckTo = mock(CompositeKey.class);
+ when(stub.createCompositeKey(BALANCE_PREFIX.getValue(), to)).thenReturn(ckTo);
+ when(ckTo.toString()).thenReturn(BALANCE_PREFIX.getValue() + to);
+ when(stub.getStringState(ckTo.toString())).thenReturn(null);
+
+ Throwable thrown =
+ catchThrowable(() -> contract.TransferFrom(ctx, org1UserId, org1UserId, 100));
+ assertThat(thrown)
+ .isInstanceOf(ChaincodeException.class)
+ .hasNoCause()
+ .hasMessage("Cannot transfer to and from same client account");
+ }
+ }
+}
diff --git a/token-erc-20/chaincode-java/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/token-erc-20/chaincode-java/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 00000000..ca6ee9ce
--- /dev/null
+++ b/token-erc-20/chaincode-java/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
@@ -0,0 +1 @@
+mock-maker-inline
\ No newline at end of file