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