diff --git a/token-erc-1155/chaincode-java/Dockerfile b/token-erc-1155/chaincode-java/Dockerfile new file mode 100755 index 00000000..f46f5b49 --- /dev/null +++ b/token-erc-1155/chaincode-java/Dockerfile @@ -0,0 +1,31 @@ +#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-1155/chaincode-java/build.gradle b/token-erc-1155/chaincode-java/build.gradle new file mode 100755 index 00000000..dd0f5c41 --- /dev/null +++ b/token-erc-1155/chaincode-java/build.gradle @@ -0,0 +1,91 @@ +/* + * 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.23.4' + 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 { + showStandardStreams = true + } + 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-1155/chaincode-java/config/checkstyle/checkstyle.xml b/token-erc-1155/chaincode-java/config/checkstyle/checkstyle.xml new file mode 100755 index 00000000..deafca76 --- /dev/null +++ b/token-erc-1155/chaincode-java/config/checkstyle/checkstyle.xml @@ -0,0 +1,174 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/token-erc-1155/chaincode-java/config/checkstyle/suppressions.xml b/token-erc-1155/chaincode-java/config/checkstyle/suppressions.xml new file mode 100755 index 00000000..f3b0387f --- /dev/null +++ b/token-erc-1155/chaincode-java/config/checkstyle/suppressions.xml @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/token-erc-1155/chaincode-java/docker/docker-entrypoint.sh b/token-erc-1155/chaincode-java/docker/docker-entrypoint.sh new file mode 100755 index 00000000..1ff8e07e --- /dev/null +++ b/token-erc-1155/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-1155/chaincode-java/gradle/wrapper/gradle-wrapper.jar b/token-erc-1155/chaincode-java/gradle/wrapper/gradle-wrapper.jar new file mode 100755 index 00000000..7454180f Binary files /dev/null and b/token-erc-1155/chaincode-java/gradle/wrapper/gradle-wrapper.jar differ diff --git a/token-erc-1155/chaincode-java/gradle/wrapper/gradle-wrapper.properties b/token-erc-1155/chaincode-java/gradle/wrapper/gradle-wrapper.properties new file mode 100755 index 00000000..2e6e5897 --- /dev/null +++ b/token-erc-1155/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-7.3.3-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/token-erc-1155/chaincode-java/gradlew b/token-erc-1155/chaincode-java/gradlew new file mode 100755 index 00000000..1b6c7873 --- /dev/null +++ b/token-erc-1155/chaincode-java/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original 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 POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${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 "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# 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 ;; #( + MSYS* | 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" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/token-erc-1155/chaincode-java/gradlew.bat b/token-erc-1155/chaincode-java/gradlew.bat new file mode 100755 index 00000000..9618d8d9 --- /dev/null +++ b/token-erc-1155/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-1155/chaincode-java/settings.gradle b/token-erc-1155/chaincode-java/settings.gradle new file mode 100755 index 00000000..ba793b99 --- /dev/null +++ b/token-erc-1155/chaincode-java/settings.gradle @@ -0,0 +1,5 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +rootProject.name = 'erc1155' diff --git a/token-erc-1155/chaincode-java/src/main/java/org/hyperledger/fabric/samples/erc1155/ContractConstants.java b/token-erc-1155/chaincode-java/src/main/java/org/hyperledger/fabric/samples/erc1155/ContractConstants.java new file mode 100755 index 00000000..30a53182 --- /dev/null +++ b/token-erc-1155/chaincode-java/src/main/java/org/hyperledger/fabric/samples/erc1155/ContractConstants.java @@ -0,0 +1,31 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.hyperledger.fabric.samples.erc1155; + +public enum ContractConstants { + URI_KEY("uri"), + BALANCE_PREFIX("account~tokenId~sender"), + APPROVAL_PREFIX("account~operator"), + NAME_KEY("name"), + SYMBOL_KEY("symbol"), + APPROVE_FOR_ALL("ApproveForAll"), + TRANSFER("Transfer"), + MINTER_ORG_MSP("Org1MSP"), + ZERO_ADDRESS("0x0"), + TRANSFER_SINGLE_EVENT("TransferSingle"), + TRANSFER_BATCH_EVENT("TransferBatch"), + TRANSFER_BATCH_MULTI_RECIPIENT_EVENT("TransferBatchMultiRecipient"), + APPROVE_FOR_ALL_EVENT("ApprovalForAll"); + + private final String value; + + ContractConstants(final String value) { + this.value = value; + } + + public String getValue() { + return this.value; + } +} diff --git a/token-erc-1155/chaincode-java/src/main/java/org/hyperledger/fabric/samples/erc1155/ContractErrors.java b/token-erc-1155/chaincode-java/src/main/java/org/hyperledger/fabric/samples/erc1155/ContractErrors.java new file mode 100755 index 00000000..b8c41f71 --- /dev/null +++ b/token-erc-1155/chaincode-java/src/main/java/org/hyperledger/fabric/samples/erc1155/ContractErrors.java @@ -0,0 +1,19 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.fabric.samples.erc1155; + +public enum ContractErrors { + TOKEN_NOT_FOUND, + TOKEN_ALREADY_EXITS, + NO_OWNER_ASSIGNED, + UNAUTHORIZED_SENDER, + INVALID_TOKEN_OWNER, + NOT_FOUND, + INVALID_ADDRESS, + INVALID_AMOUNT, + INVALID_ARGUMENTS, + INSUFFICIENT_FUND, + INVALID_URI, + INVALID_INPUT +} diff --git a/token-erc-1155/chaincode-java/src/main/java/org/hyperledger/fabric/samples/erc1155/ERC1155Contract.java b/token-erc-1155/chaincode-java/src/main/java/org/hyperledger/fabric/samples/erc1155/ERC1155Contract.java new file mode 100755 index 00000000..1e0572f3 --- /dev/null +++ b/token-erc-1155/chaincode-java/src/main/java/org/hyperledger/fabric/samples/erc1155/ERC1155Contract.java @@ -0,0 +1,993 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.hyperledger.fabric.samples.erc1155; + +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.erc1155.models.ApprovalForAll; +import org.hyperledger.fabric.samples.erc1155.models.ToID; +import org.hyperledger.fabric.samples.erc1155.models.TransferBatch; +import org.hyperledger.fabric.samples.erc1155.models.TransferBatchMultiRecipient; +import org.hyperledger.fabric.samples.erc1155.models.TransferSingle; +import org.hyperledger.fabric.shim.ChaincodeException; +import org.hyperledger.fabric.shim.ChaincodeStub; +import org.hyperledger.fabric.shim.ledger.CompositeKey; +import org.hyperledger.fabric.shim.ledger.KeyValue; +import org.hyperledger.fabric.shim.ledger.QueryResultsIterator; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.hyperledger.fabric.samples.erc1155.utils.ContractUtility.stringIsNullOrEmpty; + +@Contract( + name = "erc1155token", + info = + @Info( + title = "ERC1155Token Contract", + description = "The erc1155 multi token standard 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 = "renjith.narayanan@gmail.com", + name = "Renjith Narayanan", + url = "https://hyperledger.example.com"))) +@Default +public class ERC1155Contract implements ContractInterface { + + final Logger logger = Logger.getLogger(ERC1155Contract.class); + + /** + * Mint a new non-fungible token + * + * @param ctx the transaction context + * @param tokenId Unique ID of the non-fungible token to be minted + * @param tokenURI URI containing metadata of the minted non-fungible token + * @return Return the non-fungible token object + */ + @Transaction(intent = Transaction.TYPE.SUBMIT) + public void Mint(final Context ctx, final String account, final long id, final long amount) { + + // check if contract has been initialized first + this.checkInitialized(ctx); + // Check minter authorization - this sample assumes Org1 is the central banker with privilege to + // mint new tokens + this.authorizationHelper(ctx); + // Get ID of submitting client identity + String operator = ctx.getClientIdentity().getId(); + // Mint tokens + this.mintHelper(ctx, operator, account, id, amount); + // Emit TransferSingle event + TransferSingle transferSingleEvent = + new TransferSingle( + operator, ContractConstants.ZERO_ADDRESS.getValue(), account, id, amount); + this.emitTransferSingle(ctx, transferSingleEvent); + } + + /** + * MintBatch creates amount tokens for each token type id and assigns them to account This + * function emits a TransferBatch event + * + * @param ctx the transaction context + * @param tokenId Unique ID of the non-fungible token to be minted + * @param tokenURI URI containing metadata of the minted non-fungible token + * @return Return the non-fungible token object + */ + @Transaction(intent = Transaction.TYPE.SUBMIT) + public void MintBatch( + final Context ctx, final String account, final long[] ids, final long[] amounts) { + + // check if contract has been initialized first + this.checkInitialized(ctx); + + if (ids.length != amounts.length) { + + throw new ChaincodeException( + "ids and amounts must have the same length", ContractErrors.INVALID_ARGUMENTS.toString()); + } + + // Check minter authorization - this sample assumes Org1 is the central banker with privilege to + // mint new tokens + this.authorizationHelper(ctx); + + // Get ID of submitting client identity + String operator = ctx.getClientIdentity().getId(); + + // Group amount by token id because we can only send token to a recipient only one time in a + // block. This prevents key conflicts + Map amountToSend = new HashMap<>(); // token id => amount + + for (int i = 0; i < amounts.length; i++) { + if (amountToSend.containsKey(ids[i])) { + amountToSend.put(ids[i], Math.addExact(amountToSend.get(ids[i]).longValue(), amounts[i])); + } else { + amountToSend.put(ids[i], amounts[i]); + } + } + + // Copy the map keys and sort it. This is necessary because iterating maps in Go is not + // deterministic + List amountToSendKeys = this.sortedKeys(amountToSend); + + // Mint tokens + for (long id : amountToSendKeys) { + long amount = amountToSend.get(id); + this.mintHelper(ctx, operator, account, id, amount); + } + + // Emit TransferBatch event + TransferBatch transferBatchEvent = + new TransferBatch( + operator, ContractConstants.ZERO_ADDRESS.getValue(), account, ids, amounts); + this.emitTransferBatch(ctx, transferBatchEvent); + } + + /** + * Burn destroys amount tokens of token type id from account. This function triggers a + * TransferSingle event. + * + * @param ctx the transaction context + * @param account the account from where to burn token + * @param id token id + * @param amount amount of tokens to burn for the given token id. + */ + @Transaction(intent = Transaction.TYPE.SUBMIT) + public void Burn(final Context ctx, final String account, final long id, final long amount) { + // check if contract has been initialized first + this.checkInitialized(ctx); + if (account.equalsIgnoreCase(ContractConstants.ZERO_ADDRESS.getValue())) { + throw new ChaincodeException( + "burn to the zero address", ContractErrors.INVALID_ADDRESS.toString()); + } + // Check minter authorization - this sample assumes Org1 is the central banker with privilege to + // burn new tokens + this.authorizationHelper(ctx); + // Get ID of submitting client identity + String operator = ctx.getClientIdentity().getId(); + // Burn tokens + this.removeBalance(ctx, account, new long[] {id}, new long[] {amount}); + TransferSingle transferSingleEvent = + new TransferSingle( + operator, account, ContractConstants.ZERO_ADDRESS.getValue(), id, amount); + this.emitTransferSingle(ctx, transferSingleEvent); + } + + /** + * Batched version of Burn. + * + * @param ctx the transaction context + * @param account the account from where to burn token + * @param ids array of token ids + * @param amounts array of amount to burn corresponds to the token ids + */ + @Transaction(intent = Transaction.TYPE.SUBMIT) + public void BurnBatch( + final Context ctx, final String account, final long[] ids, final long[] amounts) { + + // check if contract has been initialized first + this.checkInitialized(ctx); + if (account.equalsIgnoreCase(ContractConstants.ZERO_ADDRESS.getValue())) { + throw new ChaincodeException( + "burn to the zero address", ContractErrors.INVALID_ADDRESS.toString()); + } + if (ids.length != amounts.length) { + throw new ChaincodeException( + "ids and amounts must have the same length", ContractErrors.INVALID_INPUT.toString()); + } + + // Check minter authorization - this sample assumes Org1 is the central banker with privilege to + // burn new tokens + this.authorizationHelper(ctx); + + // Get ID of submitting client identity + String operator = ctx.getClientIdentity().getId(); + this.removeBalance(ctx, account, ids, amounts); + TransferBatch transferBatchEvent = + new TransferBatch( + operator, account, ContractConstants.ZERO_ADDRESS.getValue(), ids, amounts); + this.emitTransferBatch(ctx, transferBatchEvent); + } + + /** + * TransferFrom transfers tokens from sender account to recipient account. recipient account must + * be a valid clientID as returned by the ClientID() function This function triggers a + * TransferSingle event + * + * @param ctx + * @param sender + * @param recipient + * @param id + * @param amount + */ + @Transaction(intent = Transaction.TYPE.SUBMIT) + public void TransferFrom( + final Context ctx, + final String sender, + final String recipient, + final long id, + final long amount) { + + // check if contract has been initialized first + this.checkInitialized(ctx); + + if (sender.equalsIgnoreCase(recipient)) { + + throw new ChaincodeException("transfer to self", ContractErrors.INVALID_INPUT.toString()); + } + + if (recipient.equalsIgnoreCase(ContractConstants.ZERO_ADDRESS.getValue())) { + + throw new ChaincodeException( + "transfer to the zero address", ContractErrors.INVALID_ADDRESS.toString()); + } + + // Get ID of submitting client identity + String operator = ctx.getClientIdentity().getId(); + + // Check whether operator is owner or approved + if (!operator.equalsIgnoreCase(sender)) { + boolean approved = _isApprovedForAll(ctx, sender, operator); + + if (!approved) { + throw new ChaincodeException( + "caller is not owner nor is approved", ContractErrors.UNAUTHORIZED_SENDER.toString()); + } + } + + // Withdraw the funds from the sender address + this.removeBalance(ctx, sender, new long[] {id}, new long[] {amount}); + + // Deposit the fund to the recipient address + this.addBalance(ctx, sender, recipient, id, amount); + + // Emit TransferSingle event + TransferSingle transferSingleEvent = + new TransferSingle(operator, sender, recipient, id, amount); + this.emitTransferSingle(ctx, transferSingleEvent); + } + + /** + * BatchTransferFrom transfers multiple tokens from sender account to recipient account recipient + * account must be a valid clientID as returned by the ClientID() function This function triggers + * a TransferBatch event + * + * @param ctx the transaction context + * @param sender the sender account id + * @param recipient account id of the recipient + * @param ids token ids + * @param amounts amount to transfer for different token ids + */ + @Transaction(intent = Transaction.TYPE.SUBMIT) + public void BatchTransferFrom( + final Context ctx, + final String sender, + final String recipient, + final long[] ids, + final long[] amounts) { + + this.checkInitialized(ctx); // check if contract has been initialized first + if (sender.equalsIgnoreCase(recipient)) { + throw new ChaincodeException("transfer to self", ContractErrors.INVALID_INPUT.toString()); + } + if (ids.length != amounts.length) { + + throw new ChaincodeException( + "ids and amounts must have the same length", ContractErrors.INVALID_INPUT.toString()); + } + + String operator = ctx.getClientIdentity().getId(); // Get ID of submitting client identity + + // Check whether operator is owner or approved + if (!operator.equalsIgnoreCase(sender)) { + boolean approved = _isApprovedForAll(ctx, sender, operator); + + if (!approved) { + + throw new ChaincodeException( + "caller is not owner nor is approved", ContractErrors.UNAUTHORIZED_SENDER.toString()); + } + } + + if (recipient.equalsIgnoreCase(ContractConstants.ZERO_ADDRESS.getValue())) { + throw new ChaincodeException( + "transfer to the zero address", ContractErrors.INVALID_ADDRESS.toString()); + } + this.removeBalance(ctx, sender, ids, amounts); // Withdraw the funds from the sender address + + // Group amount by token id because we can only send token to a recipient only one time in a + // block. This prevents key conflicts + Map amountToSend = new HashMap<>(); // token id => amount + for (int i = 0; i < amounts.length; i++) { + amountToSend.put(ids[i], amountToSend.getOrDefault(ids[i], 0L) + amounts[i]); + } + // Copy the map keys and sort it. This is necessary because iterating maps in Go is not + // deterministic + List amountToSendKeys = sortedKeys(amountToSend); + // Deposit the funds to the recipient address + + for (Long id : amountToSendKeys) { + long amount = amountToSend.get(id); + this.addBalance(ctx, sender, recipient, id, amount); + } + + TransferBatch transferBatchEvent = new TransferBatch(operator, sender, recipient, ids, amounts); + this.emitTransferBatch(ctx, transferBatchEvent); + } + + /** + * BatchTransferFromMultiRecipient transfers multiple tokens from sender account to multiple + * recipient accounts recipient account must be a valid clientID as returned by the ClientID() + * function This function triggers a TransferBatchMultiRecipient event + * + * @param ctx the transaction context + * @param sender account id of the sender + * @param recipients account id of the all the recipients + * @param ids all the token Ids + * @param amounts amount of token for each token id in the id array. + */ + @Transaction(intent = Transaction.TYPE.SUBMIT) + public void BatchTransferFromMultiRecipient( + final Context ctx, + final String sender, + final String[] recipients, + final long[] ids, + final long[] amounts) { + + // check if contract has been initialized first + this.checkInitialized(ctx); + + if ((recipients.length != ids.length) || (ids.length != amounts.length)) { + throw new ChaincodeException( + "recipients, ids, and amounts must have the same length", + ContractErrors.INVALID_INPUT.toString()); + } + + for (String recipient : recipients) { + if (sender.equalsIgnoreCase(recipient)) { + throw new ChaincodeException("transfer to self", ContractErrors.INVALID_ADDRESS.toString()); + } + } + + // Get ID of submitting client identity + String operator = ctx.getClientIdentity().getId(); + + // Check whether operator is owner or approved + if (!operator.equalsIgnoreCase(sender)) { + boolean approved = _isApprovedForAll(ctx, sender, operator); + + if (!approved) { + throw new ChaincodeException( + "caller is not owner nor is approved", ContractErrors.INVALID_ADDRESS.toString()); + } + } + + // Withdraw the funds from the sender address + this.removeBalance(ctx, sender, ids, amounts); + + // Group amount by (recipient, id ) pair because we can only send token to a recipient only one + // time in a block. This prevents key conflicts + Map amountToSend = new HashMap<>(); // (recipient, id ) => amount + for (int i = 0; i < amounts.length; i++) { + ToID key = new ToID(recipients[i], ids[i]); + amountToSend.put(key, amountToSend.getOrDefault(key, 0L) + amounts[i]); + } + + // Copy the map keys and sort it. This is necessary because iterating maps in Go is not + // deterministic + List amountToSendKeys = sortedKeysToID(amountToSend); + + // Deposit the funds to the recipient addresses + for (ToID amountoSendKey : amountToSendKeys) { + if (amountoSendKey.getTo().equalsIgnoreCase(ContractConstants.ZERO_ADDRESS.getValue())) { + + throw new ChaincodeException( + "transfer to the zero address", ContractErrors.INVALID_ADDRESS.toString()); + } + + long amount = amountToSend.get(amountoSendKey); + + this.addBalance(ctx, sender, amountoSendKey.getTo(), amountoSendKey.getId(), amount); + } + + // Emit TransferBatchMultiRecipient event + TransferBatchMultiRecipient transferBatchMultiRecipientEvent = + new TransferBatchMultiRecipient(operator, sender, recipients, ids, amounts); + this.emitTransferBatchMultiRecipient(ctx, transferBatchMultiRecipientEvent); + } + + /** + * IsApprovedForAll returns true if operator is approved to transfer account's tokens. + * + * @param ctx the transaction context + * @param account account id from where token transferred + * @param operator the operator of the account + * @return + */ + @Transaction(intent = Transaction.TYPE.EVALUATE) + public boolean IsApprovedForAll(final Context ctx, final String account, final String operator) { + return this._isApprovedForAll(ctx, account, operator); + } + + /** + * Helper function for IsApprovedForAll + * + * @param ctx the transaction context + * @param account account id from where token transferred + * @param operator the operator of the account + * @return + */ + private boolean _isApprovedForAll( + final Context ctx, final String account, final String operator) { + + // check if contract has been initialized first + this.checkInitialized(ctx); + CompositeKey approvalKey = + ctx.getStub() + .createCompositeKey(ContractConstants.APPROVAL_PREFIX.getValue(), account, operator); + + String approved = ctx.getStub().getStringState(approvalKey.toString()); + + if (stringIsNullOrEmpty(approved)) { + return false; + } + + return new Boolean(approved); + } + + /** + * SetApprovalForAll returns true if operator is approved to transfer account's tokens. Emits an + * ApprovalForAll event. + * + * @param ctx the transaction context + * @param operator get the permission to transfer account's tokens. + * @param approved + */ + @Transaction(intent = Transaction.TYPE.SUBMIT) + public void SetApprovalForAll(final Context ctx, final String operator, final boolean approved) { + + // check if contract has been initialized first + this.checkInitialized(ctx); + + // Get ID of submitting client identity + String account = ctx.getClientIdentity().getId(); + if (account.equalsIgnoreCase(operator)) { + throw new ChaincodeException( + "setting approval status for self", ContractErrors.INVALID_INPUT.toString()); + } + + CompositeKey approvalKey = + ctx.getStub() + .createCompositeKey(ContractConstants.APPROVAL_PREFIX.getValue(), account, operator); + ctx.getStub().putStringState(approvalKey.toString(), String.valueOf(approved)); + ApprovalForAll approvalForAllEvent = new ApprovalForAll(account, operator, approved); + ctx.getStub() + .setEvent( + ContractConstants.APPROVE_FOR_ALL_EVENT.getValue(), this.marshal(approvalForAllEvent)); + } + + /** + * BalanceOf returns the balance of the given account + * + * @param ctx the transaction context + * @param account the account id + * @param id the given token id + * @return + */ + @Transaction(intent = Transaction.TYPE.EVALUATE) + public long BalanceOf(final Context ctx, final String account, final long id) { + + // check if contract has been initialized first + this.checkInitialized(ctx); + return balanceOfHelper(ctx, account, id); + } + + /** + * BalanceOfBatch returns the balance of multiple account/token pairs + * + * @param ctx the transaction context + * @param accounts the account ids + * @param ids the given token ids + * @return + */ + @Transaction(intent = Transaction.TYPE.EVALUATE) + public long[] BalanceOfBatch(final Context ctx, final String[] accounts, final long[] ids) { + + // check if contract has been initialized first + this.checkInitialized(ctx); + if (accounts.length != ids.length) { + throw new ChaincodeException( + "ids and amounts must have the same length", ContractErrors.INVALID_INPUT.toString()); + } + + long[] balances = new long[accounts.length]; + + for (int i = 0; i < accounts.length; i++) { + + balances[i] = balanceOfHelper(ctx, accounts[i], ids[i]); + } + + return balances; + } + + /** + * ClientAccountBalance returns the balance of the requesting client's account + * + * @param ctx the transaction context + * @param id token id + * @return + */ + @Transaction(intent = Transaction.TYPE.EVALUATE) + public long ClientAccountBalance(final Context ctx, final long id) { + + // check if contract has been initialized first + this.checkInitialized(ctx); + + // Get ID of submitting client identity + String clientID = ctx.getClientIdentity().getId(); + + return balanceOfHelper(ctx, clientID, id); + } + + /** + * 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. + * + * @param ctx the transaction context + * @return + */ + @Transaction(intent = Transaction.TYPE.EVALUATE) + public String ClientAccountID(final Context ctx) { + // check if contract has been initialized first + this.checkInitialized(ctx); + // Get ID of submitting client identity + String clientAccountID = ctx.getClientIdentity().getId(); + return clientAccountID; + } + + /** + * SetURI set the URI value. This function triggers URI event for each token id .This + * implementation returns the same URI for all token types. It relies on the token type ID + * substitution mechanism . + * + * @param ctx the transaction context + * @param uri the token URI + */ + @Transaction(intent = Transaction.TYPE.SUBMIT) + public void SetURI(final Context ctx, final String uri) { + + // check if contract has been initialized first + this.checkInitialized(ctx); + + // Check minter authorization - this sample assumes Org1 is the central banker with privilege to + // mint new tokens + this.authorizationHelper(ctx); + if (uri.indexOf("{id}") < 0) { + throw new ChaincodeException( + "failed to set uri, uri should contain '{id}'", ContractErrors.INVALID_URI.toString()); + } + + ctx.getStub().putStringState(ContractConstants.URI_KEY.getValue(), uri); + } + + /** + * Returns the URI.This implementation returns the same URI for *all* token types. It relies on + * the token type ID substitution mechanism. Clients calling this function must replace the + * `\{id\}` substring with the actual token type ID. + * + * @param ctx the transaction context + * @param id tokenid + * @return + */ + @Transaction(intent = Transaction.TYPE.EVALUATE) + public String URI(final Context ctx, final long id) { + // check if contract has been initialized first + final ChaincodeStub stub = ctx.getStub(); + String uri = stub.getStringState(ContractConstants.URI_KEY.getValue()); + if (stringIsNullOrEmpty(uri)) { + throw new ChaincodeException("no uri is set", ContractErrors.NOT_FOUND.toString()); + } + return uri; + } + + /** + * @param ctx + * @param id + */ + @Transaction(intent = Transaction.TYPE.EVALUATE) + public void BroadcastTokenExistance(final Context ctx, final long id) { + // check if contract has been initialized first + this.checkInitialized(ctx); + // Check minter authorization - this sample assumes Org1 is the central banker with privilege to + // mint new tokens + this.authorizationHelper(ctx); + String operator = ctx.getClientIdentity().getId(); + TransferSingle transferSingleEvent = + new TransferSingle( + operator, + ContractConstants.ZERO_ADDRESS.getValue(), + ContractConstants.ZERO_ADDRESS.getValue(), + id, + 0); + this.emitTransferSingle(ctx, transferSingleEvent); + } + + /** + * Returns a descriptive name for a collection of non-fungible tokens in this contract + * + * @param ctx the transaction context + * @return name of the token. + */ + @Transaction(intent = Transaction.TYPE.EVALUATE) + public String Name(final Context ctx) { + // check contract options are already set first to execute the function + this.checkInitialized(ctx); + return ctx.getStub().getStringState(ContractConstants.NAME_KEY.getValue()); + } + + /** + * Returns an abbreviated symbol for non-fungible tokens in this contract. + * + * @param ctx the transaction context + * @return token symbol. + */ + @Transaction(intent = Transaction.TYPE.EVALUATE) + public String Symbol(final Context ctx) { + // check contract options are already set first to execute the function + this.checkInitialized(ctx); + return ctx.getStub().getStringState(ContractConstants.SYMBOL_KEY.getValue()); + } + + /** + * Initialize the token with name and symbol. + * + * @param ctx the transaction context + * @param name token name + * @param symbol token symbol + */ + @Transaction(intent = Transaction.TYPE.SUBMIT) + public void Initialize(final Context ctx, final String name, final String symbol) { + + final ChaincodeStub stub = ctx.getStub(); + final String clientMSPID = ctx.getClientIdentity().getMSPID(); + + // Check minter authorization - this sample assumes Org1 is the issuer with privilege to set the + // name and symbol + if (!clientMSPID.equalsIgnoreCase(ContractConstants.MINTER_ORG_MSP.getValue())) { + throw new ChaincodeException( + "Client is not authorized to initialize the contract", + ContractErrors.UNAUTHORIZED_SENDER.toString()); + } + + // check contract options are not already set, client is not authorized to change them once + // initialized + String tokenName = stub.getStringState(ContractConstants.NAME_KEY.getValue()); + if (!stringIsNullOrEmpty(tokenName)) { + throw new ChaincodeException( + "contract options are already set, client is not authorized to change them", + ContractErrors.UNAUTHORIZED_SENDER.toString()); + } + stub.putStringState(ContractConstants.NAME_KEY.getValue(), name); + stub.putStringState(ContractConstants.SYMBOL_KEY.getValue(), symbol); + } + + /** + * This helper function checks minter authorization - this sample assumes Org1 is the central + * banker with privilege to mint new tokens Throw exception if not initialized. + * + * @param ctx the transaction context + * @return + */ + private void authorizationHelper(final Context ctx) { + String clientMSPID = ctx.getClientIdentity().getMSPID(); + if (!clientMSPID.equalsIgnoreCase(ContractConstants.MINTER_ORG_MSP.getValue())) { + throw new ChaincodeException( + "Client is not authorized to set the name and symbol of the token", + ContractErrors.UNAUTHORIZED_SENDER.toString()); + } + } + + /** + * Update the balance of account for the given token id. + * + * @param ctx + * @param account + * @param id + * @return + */ + private long balanceOfHelper(final Context ctx, final String account, final long id) { + + if (account.equalsIgnoreCase(ContractConstants.ZERO_ADDRESS.getValue())) { + throw new ChaincodeException( + "balance query for the zero address", ContractErrors.INVALID_ADDRESS.toString()); + } + + // Convert id to string + String idString = String.valueOf(id); + long balance = 0; + final QueryResultsIterator balanceIterator = + ctx.getStub() + .getStateByPartialCompositeKey( + ContractConstants.BALANCE_PREFIX.getValue(), account, idString); + for (KeyValue keyVal : balanceIterator) { + long balAmount = Long.parseLong(keyVal.getStringValue()); + balance = Math.addExact(balance, balAmount); + } + return balance; + } + + /** + * Helper function to add balance of recipient account + * + * @param ctx the transaction context + * @param sender the sender account id + * @param recipient the token recipient account id + * @param id the token id + * @param amount the amount of token + */ + private void addBalance( + final Context ctx, + final String sender, + final String recipient, + final long id, + final long amount) { + ChaincodeStub stub = ctx.getStub(); + String idString = String.valueOf(id); // Convert id to string + CompositeKey balanceKey = + stub.createCompositeKey( + ContractConstants.BALANCE_PREFIX.getValue(), recipient, idString, sender); + String balanceStr = stub.getStringState(balanceKey.toString()); + long balance = 0; + if (!stringIsNullOrEmpty(balanceStr)) { + + balance = Long.parseLong(balanceStr); + } + balance = Math.addExact(balance, amount); + stub.putStringState(balanceKey.toString(), String.valueOf(balance)); + logger.info(String.format("account %s balance updated to %d", recipient, balance)); + } + + /** + * Remove Balance of the sender for each given token id and amounts. + * + * @param ctx the transaction context + * @param sender the owner of the token + * @param ids each token ids + * @param amounts the amount of the token to transfered for each id + */ + private void removeBalance( + final Context ctx, final String sender, final long[] ids, final long[] amounts) { + + // Calculate the total amount of each token to withdraw + Map necessaryFunds = new HashMap(); // token id -> necessary amount + for (int i = 0; i < amounts.length; i++) { + necessaryFunds.put(ids[i], necessaryFunds.getOrDefault(i, 0L) + amounts[i]); + } + ChaincodeStub stub = ctx.getStub(); + // Copy the map keys and sort it. This is necessary because iterating maps in Go is not + // deterministic + List necessaryFundsKeys = sortedKeys(necessaryFunds); + // Check whether the sender has the necessary funds and withdraw them from the account + for (long tokenId : necessaryFundsKeys) { + long neededAmount = necessaryFunds.get(tokenId); + String idString = String.valueOf(tokenId); + long partialBalance = 0; + boolean selfRecipientKeyNeedsToBeRemoved = false; + String selfRecipientKey = ""; + final QueryResultsIterator balanceResulutIterator = + ctx.getStub() + .getStateByPartialCompositeKey( + ContractConstants.BALANCE_PREFIX.getValue(), sender, idString); + Iterator balanceIterator = balanceResulutIterator.iterator(); + while (balanceIterator.hasNext() && partialBalance < neededAmount) { + KeyValue queryResponse = balanceIterator.next(); + long partBalAmount = Long.valueOf(queryResponse.getStringValue()); + partialBalance = Math.addExact(partialBalance, partBalAmount); + CompositeKey compositeKeyParts = stub.splitCompositeKey(queryResponse.getKey()); + if (compositeKeyParts.getAttributes().contains(sender)) { + selfRecipientKeyNeedsToBeRemoved = true; + selfRecipientKey = queryResponse.getKey(); + } else { + ctx.getStub().delState(queryResponse.getKey()); + } + } + if (partialBalance < neededAmount) { + String error = + String.format( + "sender has insufficient funds for token %s, needed funds: %d, available fund: %d", + tokenId, neededAmount, partialBalance); + throw new ChaincodeException(error, ContractErrors.INSUFFICIENT_FUND.toString()); + + } else if (partialBalance > neededAmount) { + + // Send the remainder back to the sender + long remainder = Math.subtractExact(partialBalance, neededAmount); + + if (selfRecipientKeyNeedsToBeRemoved) { + + // Set balance for the key that has the same address for sender and recipient + this.setBalance(ctx, sender, sender, tokenId, remainder); + + } else { + + this.addBalance(ctx, sender, sender, tokenId, remainder); + } + + } else { + + // Delete self recipient key + stub.delState(selfRecipientKey); + } + } + } + + /** + * Helper function for token mint call. + * + * @param ctx the transaction context + * @param operator the approved operator of the token + * @param account account id updated with token + * @param id the token id + * @param amount quantity of token to be minted + */ + private void mintHelper( + final Context ctx, + final String operator, + final String account, + final long id, + final long amount) { + + if (account.equalsIgnoreCase(ContractConstants.ZERO_ADDRESS.getValue())) { + + throw new ChaincodeException( + "mint to the zero address", ContractErrors.INVALID_ADDRESS.toString()); + } + + if (amount <= 0) { + + throw new ChaincodeException( + "Mint amount must be a positive integer", ContractErrors.INVALID_AMOUNT.toString()); + } + + this.addBalance(ctx, operator, account, id, amount); + } + + private void setBalance( + final Context ctx, + final String sender, + final String recipient, + final long id, + final long amount) { + // Convert id to string + String idString = String.valueOf(id); + CompositeKey balanceKey = + ctx.getStub() + .createCompositeKey( + ContractConstants.BALANCE_PREFIX.getValue(), recipient, idString, sender); + ctx.getStub().putStringState(balanceKey.toString(), String.valueOf(amount)); + } + + /** + * Emit when single recipient token transfer occur. + * + * @param ctx + * @param transferSingleEvent + */ + private void emitTransferSingle(final Context ctx, final TransferSingle transferSingleEvent) { + + ctx.getStub() + .setEvent( + ContractConstants.TRANSFER_SINGLE_EVENT.getValue(), this.marshal(transferSingleEvent)); + } + + /** + * Emit transfer batch event. + * + * @param ctx the transaction context + * @param transferBatchEvent BatchEvent details + */ + private void emitTransferBatch(final Context ctx, final TransferBatch transferBatchEvent) { + + ctx.getStub() + .setEvent( + ContractConstants.TRANSFER_BATCH_EVENT.getValue(), this.marshal(transferBatchEvent)); + } + + /** + * EmitTransferBatchMultiRecipient when multi recipient token batch transfer occurs. + * + * @param ctx the transaction context + * @param transferBatchMutipleRecipientEvent event details + */ + private void emitTransferBatchMultiRecipient( + final Context ctx, final TransferBatchMultiRecipient transferBatchMutipleRecipientEvent) { + + ctx.getStub() + .setEvent( + ContractConstants.TRANSFER_BATCH_MULTI_RECIPIENT_EVENT.getValue(), + this.marshal(transferBatchMutipleRecipientEvent)); + } + + /** + * Returns the sorted array from the keys of map + * + * @param map the keys of map data to be sorted + * @return + */ + private List sortedKeys(final Map map) { + List sortedKeys = new ArrayList<>(); + sortedKeys.addAll(map.keySet()); + Collections.sort(sortedKeys); + return sortedKeys; + } + + /** + * Returns the sorted slice ([]ToID) copied from the keys of map[ToID]uint64 + * + * @param the keys of map data to be sorted + * @return + */ + private List sortedKeysToID(final Map map) { + + List sortedKeys = new ArrayList<>(); + sortedKeys.addAll(map.keySet()); + Collections.sort( + sortedKeys, + new Comparator() { + public int compare(final ToID toId1, final ToID toId2) { + if (toId1.getId() != toId2.getId()) { + return toId1.getTo().compareTo(toId2.getTo()); + } + + return new Long(toId1.getId()).compareTo(toId2.getId()); + } + }); + + return sortedKeys; + } + + /** + * Checks that contract options have been already initialized Throw exception if not initialized. + * + * @param ctx the transaction context + * @return + */ + private void checkInitialized(final Context ctx) { + String tokenName = ctx.getStub().getStringState(ContractConstants.NAME_KEY.getValue()); + if (stringIsNullOrEmpty(tokenName)) { + throw new ChaincodeException( + "Contract options need to be set before calling any function, call Initialize() to initialize contract", + ContractErrors.TOKEN_NOT_FOUND.toString()); + } + } + + /** + * 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-1155/chaincode-java/src/main/java/org/hyperledger/fabric/samples/erc1155/models/ApprovalForAll.java b/token-erc-1155/chaincode-java/src/main/java/org/hyperledger/fabric/samples/erc1155/models/ApprovalForAll.java new file mode 100755 index 00000000..642bc704 --- /dev/null +++ b/token-erc-1155/chaincode-java/src/main/java/org/hyperledger/fabric/samples/erc1155/models/ApprovalForAll.java @@ -0,0 +1,68 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.fabric.samples.erc1155.models; + +import com.owlike.genson.annotation.JsonProperty; +import org.hyperledger.fabric.contract.annotation.Property; + +public class ApprovalForAll { + + @Property() + @JsonProperty("owner") + private String owner; + + @Property() + @JsonProperty("operator") + private String operator; + + @Property() + @JsonProperty("approved") + private boolean approved; + + /** Default constructor */ + public ApprovalForAll() { + super(); + } + + /** + * Constructor of the class + * + * @param owner + * @param operator + * @param approved + */ + public ApprovalForAll(final String owner, final String operator, final boolean approved) { + super(); + this.owner = owner; + this.operator = operator; + this.approved = approved; + } + + /** + * getter function for the owner + * + * @return the token owner account + */ + public String getOwner() { + return owner; + } + + /** + * getter function for operator + * + * @return the token approved operator + */ + public String getOperator() { + return operator; + } + + /** + * getter function for the approved + * + * @return is operator is approved one or not + */ + public boolean getApproved() { + return approved; + } +} diff --git a/token-erc-1155/chaincode-java/src/main/java/org/hyperledger/fabric/samples/erc1155/models/Event.java b/token-erc-1155/chaincode-java/src/main/java/org/hyperledger/fabric/samples/erc1155/models/Event.java new file mode 100755 index 00000000..112b4e4d --- /dev/null +++ b/token-erc-1155/chaincode-java/src/main/java/org/hyperledger/fabric/samples/erc1155/models/Event.java @@ -0,0 +1,68 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.fabric.samples.erc1155.models; + +import com.owlike.genson.annotation.JsonProperty; +import org.hyperledger.fabric.contract.annotation.Property; + +public abstract class Event { + + @Property() + @JsonProperty("operator") + private String operator; + + @Property() + @JsonProperty("from") + private String from; + + @Property() + @JsonProperty("to") + private String to; + + /** Default constructor */ + public Event() { + super(); + } + + /** + * Constructor of the class + * + * @param operator + * @param from + * @param to + */ + public Event(final String operator, final String from, final String to) { + super(); + this.operator = operator; + this.from = from; + this.to = to; + } + + /** + * getter function for the operator + * + * @return the token operator + */ + public String getOperator() { + return operator; + } + + /** + * getter function for the from + * + * @return the sender account + */ + public String getFrom() { + return from; + } + + /** + * getter function for the to + * + * @return the recipient account + */ + public String getTo() { + return to; + } +} diff --git a/token-erc-1155/chaincode-java/src/main/java/org/hyperledger/fabric/samples/erc1155/models/ToID.java b/token-erc-1155/chaincode-java/src/main/java/org/hyperledger/fabric/samples/erc1155/models/ToID.java new file mode 100755 index 00000000..dc4a35f9 --- /dev/null +++ b/token-erc-1155/chaincode-java/src/main/java/org/hyperledger/fabric/samples/erc1155/models/ToID.java @@ -0,0 +1,98 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.fabric.samples.erc1155.models; + +import org.hyperledger.fabric.contract.annotation.Property; + +import com.owlike.genson.annotation.JsonProperty; + +public class ToID { + + @Property() + @JsonProperty("id") + private long id; + + @Property() + @JsonProperty("to") + private String to; + + /** Default constructor */ + public ToID() { + super(); + } + + /** + * Constructor of the class + * + * @param id tokenId + * @param to the recipient account + */ + public ToID(final String to, final long id) { + super(); + this.to = to; + this.id = id; + } + + /** + * The getter function for the Id + * + * @return the tokenId + */ + public long getId() { + return id; + } + + /** + * The getter function for the To. + * + * @return the recipient account id + */ + public String getTo() { + return to; + } + + /** + * + * @param obj + * @return + */ + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + ToID other = (ToID) obj; + if (id != other.id) { + return false; + } + if (to == null) { + if (other.to != null) { + return false; + } + } else if (!to.equalsIgnoreCase(other.to)) { + return false; + } + return true; + } + + /** + * + * @return the hashcode + */ + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + (int) (id ^ (id >>> 32)); + result = prime * result + ((to == null) ? 0 : to.hashCode()); + return result; + } +} diff --git a/token-erc-1155/chaincode-java/src/main/java/org/hyperledger/fabric/samples/erc1155/models/TransferBatch.java b/token-erc-1155/chaincode-java/src/main/java/org/hyperledger/fabric/samples/erc1155/models/TransferBatch.java new file mode 100755 index 00000000..bb288104 --- /dev/null +++ b/token-erc-1155/chaincode-java/src/main/java/org/hyperledger/fabric/samples/erc1155/models/TransferBatch.java @@ -0,0 +1,61 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.fabric.samples.erc1155.models; + +import com.owlike.genson.annotation.JsonProperty; +import org.hyperledger.fabric.contract.annotation.Property; + +public class TransferBatch extends Event { + + @Property() + @JsonProperty("ids") + private long[] ids; + + @Property() + @JsonProperty("values") + private long[] values; + + /** Default constructor */ + public TransferBatch() { + super(); + } + + /** + * Constructor of the class + * + * @param operator + * @param from + * @param to + * @param ids + * @param values + */ + public TransferBatch( + final String operator, + final String from, + final String to, + final long[] ids, + final long[] values) { + super(operator, from, to); + this.ids = ids; + this.values = values; + } + + /** + * Getter function for Ids + * + * @return all token Ids + */ + public long[] getIds() { + return ids; + } + + /** + * Getter function for the values + * + * @return get the amount or value of all the tokens + */ + public long[] getValues() { + return values; + } +} diff --git a/token-erc-1155/chaincode-java/src/main/java/org/hyperledger/fabric/samples/erc1155/models/TransferBatchMultiRecipient.java b/token-erc-1155/chaincode-java/src/main/java/org/hyperledger/fabric/samples/erc1155/models/TransferBatchMultiRecipient.java new file mode 100755 index 00000000..337084bd --- /dev/null +++ b/token-erc-1155/chaincode-java/src/main/java/org/hyperledger/fabric/samples/erc1155/models/TransferBatchMultiRecipient.java @@ -0,0 +1,103 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.fabric.samples.erc1155.models; + +import com.owlike.genson.annotation.JsonProperty; +import org.hyperledger.fabric.contract.annotation.Property; + +public class TransferBatchMultiRecipient { + + @Property() + @JsonProperty("operator") + private String operator; + + @Property() + @JsonProperty("from") + private String from; + + @Property() + @JsonProperty("to") + private String[] to; + + @Property() + @JsonProperty("ids") + private long[] ids; + + @Property() + @JsonProperty("values") + private long[] values; + + /** Default constructor */ + public TransferBatchMultiRecipient() { + super(); + } + + /** + * Constructor of the class + * + * @param operator + * @param from + * @param to + * @param ids + * @param values + */ + public TransferBatchMultiRecipient( + final String operator, + final String from, + final String[] to, + final long[] ids, + final long[] values) { + super(); + this.operator = operator; + this.from = from; + this.to = to; + this.ids = ids; + this.values = values; + } + + /** + * getter function for from account + * + * @return get the from account + */ + public String getFrom() { + return from; + } + + /** + * getter function for Ids + * + * @return get all token ids + */ + public long[] getIds() { + return ids; + } + + /** + * getter function for the operator + * + * @return gett all aproved token operators + */ + public String getOperator() { + return operator; + } + + /** + * getter function for recipients + * + * @return all the recipients + */ + public String[] getTo() { + return to; + } + + /** + * getter function for all the values + * + * @return get all the token values or ammount + */ + public long[] getValues() { + return values; + } +} diff --git a/token-erc-1155/chaincode-java/src/main/java/org/hyperledger/fabric/samples/erc1155/models/TransferSingle.java b/token-erc-1155/chaincode-java/src/main/java/org/hyperledger/fabric/samples/erc1155/models/TransferSingle.java new file mode 100755 index 00000000..60c1475c --- /dev/null +++ b/token-erc-1155/chaincode-java/src/main/java/org/hyperledger/fabric/samples/erc1155/models/TransferSingle.java @@ -0,0 +1,57 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.fabric.samples.erc1155.models; + +import org.hyperledger.fabric.contract.annotation.Property; + +import com.owlike.genson.annotation.JsonProperty; + +public class TransferSingle extends Event { + + @Property() + @JsonProperty("id") + private long id; + + @Property() + @JsonProperty("value") + private long value; + + /** Default constructor */ + public TransferSingle() { + super(); + } + + /** + * Constructor of the class + * + * @param operator + * @param from + * @param to + * @param id + * @param value + */ + public TransferSingle(final String operator, final String from, final String to, final long id, final long value) { + super(operator, from, to); + this.id = id; + this.value = value; + } + + /** + * gettter function for id + * + * @return get the token id + */ + public long getId() { + return id; + } + + /** + * getter function for value + * + * @return the value or amount of the token id + */ + public long getValue() { + return value; + } +} diff --git a/token-erc-1155/chaincode-java/src/main/java/org/hyperledger/fabric/samples/erc1155/models/URI.java b/token-erc-1155/chaincode-java/src/main/java/org/hyperledger/fabric/samples/erc1155/models/URI.java new file mode 100755 index 00000000..a8692f43 --- /dev/null +++ b/token-erc-1155/chaincode-java/src/main/java/org/hyperledger/fabric/samples/erc1155/models/URI.java @@ -0,0 +1,53 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.fabric.samples.erc1155.models; + +import com.owlike.genson.annotation.JsonProperty; +import org.hyperledger.fabric.contract.annotation.Property; + +public class URI { + + @Property() + @JsonProperty("id") + private long id; + + @Property() + @JsonProperty("value") + private long value; + + /** Default constructor */ + public URI() { + super(); + } + + /** + * Constructor of the class + * + * @param id + * @param value + */ + public URI(final long id, final long value) { + super(); + this.id = id; + this.value = value; + } + + /** + * getter function for the id + * + * @return token id + */ + public long getId() { + return id; + } + + /** + * getter function for the value + * + * @return + */ + public long getValue() { + return value; + } +} diff --git a/token-erc-1155/chaincode-java/src/main/java/org/hyperledger/fabric/samples/erc1155/utils/ContractUtility.java b/token-erc-1155/chaincode-java/src/main/java/org/hyperledger/fabric/samples/erc1155/utils/ContractUtility.java new file mode 100755 index 00000000..0e50a0e0 --- /dev/null +++ b/token-erc-1155/chaincode-java/src/main/java/org/hyperledger/fabric/samples/erc1155/utils/ContractUtility.java @@ -0,0 +1,14 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.fabric.samples.erc1155.utils; + +public final class ContractUtility { + + private ContractUtility() { + } + + public static boolean stringIsNullOrEmpty(final String string) { + return string == null || string.isEmpty(); + } +} diff --git a/token-erc-1155/chaincode-java/src/test/java/org/hyperledger/fabric/samples/erc1155/CommonUtils.java b/token-erc-1155/chaincode-java/src/test/java/org/hyperledger/fabric/samples/erc1155/CommonUtils.java new file mode 100755 index 00000000..3bbb5999 --- /dev/null +++ b/token-erc-1155/chaincode-java/src/test/java/org/hyperledger/fabric/samples/erc1155/CommonUtils.java @@ -0,0 +1,151 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.hyperledger.fabric.samples.erc1155; + +import org.hyperledger.fabric.contract.ClientIdentity; +import org.hyperledger.fabric.shim.ChaincodeStub; +import org.hyperledger.fabric.shim.ledger.CompositeKey; +import org.hyperledger.fabric.shim.ledger.KeyValue; +import org.hyperledger.fabric.shim.ledger.QueryResultsIterator; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import static org.hyperledger.fabric.samples.erc1155.Constants.TOKEN_NAME; +import static org.hyperledger.fabric.samples.erc1155.Constants.TOKEN_SYMBOL; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class CommonUtils { + + /** Default constructor */ + public CommonUtils() { + super(); + // TODO Auto-generated constructor stub + } + + /** + * Set token name and symbol + * + * @param stub + */ + protected void setTokenNameAndSysmbol(final ChaincodeStub stub) { + when(stub.getStringState(ContractConstants.NAME_KEY.getValue())).thenReturn(TOKEN_NAME); + when(stub.getStringState(ContractConstants.SYMBOL_KEY.getValue())).thenReturn(TOKEN_SYMBOL); + } + + /** + * Set MSPID + * + * @param ci + */ + protected void setOrg1MspId(final ClientIdentity ci) { + when(ci.getMSPID()).thenReturn("Org1MSP"); + } + + /** + * Set org2 msp id + * + * @param ci + */ + protected void setOrg2MspId(final ClientIdentity ci) { + when(ci.getMSPID()).thenReturn("Org2MSP"); + } + + /** + * Create composite key + * + * @param stub + * @param returnValue + * @param firstKey + * @param keys + * @return + */ + protected CompositeKey createCompositeKey( + final ChaincodeStub stub, + final String returnValue, + final ContractConstants firstKey, + final String... keys) { + CompositeKey ck = mock(CompositeKey.class); + StringBuilder keyCombined = new StringBuilder(); + keyCombined.append(ContractConstants.BALANCE_PREFIX.getValue()); + for (String key : keys) { + keyCombined.append(key); + } + when(stub.createCompositeKey(firstKey.getValue(), keys)).thenReturn(ck); + when(ck.toString()).thenReturn(keyCombined.toString()); + when(stub.getStringState(ck.toString())).thenReturn(returnValue); + return ck; + } + + /** + * Set iterator for the key + * + * @param stub + * @param firstKey + * @param keys + * @param values + */ + protected void setResultsIterator( + final ChaincodeStub stub, + final ContractConstants firstKey, + final String[] keys, + final MockKeyValue... values) { + List list = new ArrayList(); + for (MockKeyValue value : values) { + list.add(value); + } + when(stub.getStateByPartialCompositeKey(firstKey.getValue(), keys)) + .thenReturn(new MockAssetResultsIterator(list)); + } + + final class MockKeyValue implements KeyValue { + + private final String key; + private final String value; + + MockKeyValue(final String key, final String value) { + super(); + this.key = key; + this.value = value; + } + + @Override + public String getKey() { + return this.key; + } + + @Override + public String getStringValue() { + return this.value; + } + + @Override + public byte[] getValue() { + return this.value.getBytes(); + } + } + + final class MockAssetResultsIterator implements QueryResultsIterator { + + private final List assetList; + + MockAssetResultsIterator(final List list) { + super(); + this.assetList = list; + } + + @Override + public Iterator iterator() { + return assetList.iterator(); + } + + @Override + public void close() throws Exception { + // do nothing + } + } +} diff --git a/token-erc-1155/chaincode-java/src/test/java/org/hyperledger/fabric/samples/erc1155/Constants.java b/token-erc-1155/chaincode-java/src/test/java/org/hyperledger/fabric/samples/erc1155/Constants.java new file mode 100755 index 00000000..73219da8 --- /dev/null +++ b/token-erc-1155/chaincode-java/src/test/java/org/hyperledger/fabric/samples/erc1155/Constants.java @@ -0,0 +1,25 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.hyperledger.fabric.samples.erc1155; + +public final class Constants { + + /** Default constructor */ + private Constants() { + super(); + } + + public static final String ORG1_USER_ID = + "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"; + public static final String ORG2_USER_ID = + "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"; + public static final String ORG3_USER_ID = + "x509::CN=User1@org3.example.com, L=San Francisco, ST=California," + + " C=US::CN=ca.org3.example.com, O=org3.example.com, L=San Francisco, ST=California, C=US"; + public static final String TOKEN_NAME = "AirlineToke"; + public static final String TOKEN_SYMBOL = "ART"; +} diff --git a/token-erc-1155/chaincode-java/src/test/java/org/hyperledger/fabric/samples/erc1155/ERC1155ContractTest.java b/token-erc-1155/chaincode-java/src/test/java/org/hyperledger/fabric/samples/erc1155/ERC1155ContractTest.java new file mode 100755 index 00000000..067cdcc8 --- /dev/null +++ b/token-erc-1155/chaincode-java/src/test/java/org/hyperledger/fabric/samples/erc1155/ERC1155ContractTest.java @@ -0,0 +1,1320 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.hyperledger.fabric.samples.erc1155; + +import com.owlike.genson.Genson; +import org.hyperledger.fabric.contract.ClientIdentity; +import org.hyperledger.fabric.contract.Context; +import org.hyperledger.fabric.samples.erc1155.models.ApprovalForAll; +import org.hyperledger.fabric.samples.erc1155.models.TransferBatch; +import org.hyperledger.fabric.samples.erc1155.models.TransferBatchMultiRecipient; +import org.hyperledger.fabric.samples.erc1155.models.TransferSingle; +import org.hyperledger.fabric.shim.ChaincodeException; +import org.hyperledger.fabric.shim.ChaincodeStub; +import org.hyperledger.fabric.shim.ledger.CompositeKey; +import org.hyperledger.fabric.shim.ledger.KeyValue; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.ThrowableAssert.catchThrowable; +import static org.hyperledger.fabric.samples.erc1155.Constants.ORG1_USER_ID; +import static org.hyperledger.fabric.samples.erc1155.Constants.ORG2_USER_ID; +import static org.hyperledger.fabric.samples.erc1155.Constants.ORG3_USER_ID; +import static org.hyperledger.fabric.samples.erc1155.Constants.TOKEN_NAME; +import static org.hyperledger.fabric.samples.erc1155.Constants.TOKEN_SYMBOL; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class ERC1155ContractTest extends CommonUtils { + + @Nested + class ContractInitalizeFunctionsTest { + + private ERC1155Contract contract; + private Context ctx; + private ChaincodeStub stub; + private ClientIdentity ci = null; + + @BeforeEach + public void initialize() { + this.contract = new ERC1155Contract(); + this.ctx = mock(Context.class); + ci = mock(ClientIdentity.class); + this.stub = mock(ChaincodeStub.class); + when(this.ctx.getStub()).thenReturn(this.stub); + when(this.ctx.getClientIdentity()).thenReturn(ci); + } + + @Test + public void invokeInitializeTest() { + setOrg1MspId(this.ci); + this.contract.Initialize(this.ctx, TOKEN_NAME, TOKEN_SYMBOL); + verify(this.stub).putStringState(ContractConstants.NAME_KEY.getValue(), TOKEN_NAME); + verify(this.stub).putStringState(ContractConstants.SYMBOL_KEY.getValue(), TOKEN_SYMBOL); + } + + @Test + public void unAuthorizedOrgInitializeTest() { + setOrg2MspId(this.ci); + Throwable thrown = + catchThrowable( + () -> { + this.contract.Initialize(ctx, TOKEN_NAME, TOKEN_SYMBOL); + }); + assertThat(thrown) + .isInstanceOf(ChaincodeException.class) + .hasNoCause() + .hasMessage("Client is not authorized to initialize the contract"); + } + + @Test + public void queryNameTest() { + setOrg1MspId(this.ci); + setTokenNameAndSysmbol(stub); + String name = this.contract.Name(ctx); + assertThat(name).isEqualTo(TOKEN_NAME); + } + + @Test + public void querySymbolTest() { + setOrg1MspId(this.ci); + setTokenNameAndSysmbol(stub); + String name = this.contract.Symbol(this.ctx); + assertThat(name).isEqualTo(TOKEN_SYMBOL); + } + } + + @Nested + class ContractMintFunctionsTest { + + private ERC1155Contract contract; + private Context ctx; + private ChaincodeStub stub; + private ClientIdentity ci = null; + + @BeforeEach + public void initialize() { + this.contract = new ERC1155Contract(); + this.ctx = mock(Context.class); + this.stub = mock(ChaincodeStub.class); + this.ci = mock(ClientIdentity.class); + when(this.ctx.getStub()).thenReturn(this.stub); + when(this.ctx.getClientIdentity()).thenReturn(this.ci); + } + + @Test + public void invokeMintWithoutInitializeTest() { + setOrg1MspId(this.ci); + when(this.ci.getId()).thenReturn(ORG1_USER_ID); + Throwable thrown = + catchThrowable( + () -> { + this.contract.Mint(this.ctx, ORG1_USER_ID, 1, 1); + }); + assertThat(thrown) + .isInstanceOf(ChaincodeException.class) + .hasNoCause() + .hasMessage( + "Contract options need to be set before calling any function, call Initialize() to initialize contract"); + } + + @Test + public void unAuthorizedInvokeMintTest() { + setOrg2MspId(this.ci); + when(ci.getId()).thenReturn(ORG2_USER_ID); + setTokenNameAndSysmbol(stub); + Throwable thrown = + catchThrowable( + () -> { + this.contract.Mint(this.ctx, ORG2_USER_ID, 1, 1); + }); + assertThat(thrown) + .isInstanceOf(ChaincodeException.class) + .hasNoCause() + .hasMessage("Client is not authorized to set the name and symbol of the token"); + } + + @Test + public void invalidAmountInvokeMintTest() { + setOrg1MspId(ci); + when(this.ci.getId()).thenReturn(ORG1_USER_ID); + setTokenNameAndSysmbol(stub); + Throwable thrown = + catchThrowable( + () -> { + this.contract.Mint(this.ctx, ORG2_USER_ID, 1, 0); + }); + assertThat(thrown) + .isInstanceOf(ChaincodeException.class) + .hasNoCause() + .hasMessage("Mint amount must be a positive integer"); + } + + @Test + public void invalidAccountInvokeMintTest() { + setOrg1MspId(this.ci); + when(this.ci.getId()).thenReturn(ORG1_USER_ID); + setTokenNameAndSysmbol(stub); + Throwable thrown = + catchThrowable( + () -> { + this.contract.Mint(ctx, "0x0", 1, 1); + }); + assertThat(thrown) + .isInstanceOf(ChaincodeException.class) + .hasNoCause() + .hasMessage("mint to the zero address"); + } + + @Test + public void invokeMintTest() { + + setOrg1MspId(ci); + when(this.ci.getId()).thenReturn(ORG1_USER_ID); + setTokenNameAndSysmbol(stub); + CompositeKey ck = + createCompositeKey( + stub, "", ContractConstants.BALANCE_PREFIX, ORG1_USER_ID, "1", ORG1_USER_ID); + this.contract.Mint(this.ctx, ORG1_USER_ID, 1, 1); + verify(this.stub).putStringState(ck.toString(), "1"); + TransferSingle transferSingleEvent = + new TransferSingle(ORG1_USER_ID, "0x0", ORG1_USER_ID, 1, 1); + verify(this.stub) + .setEvent("TransferSingle", new Genson().serialize(transferSingleEvent).getBytes(UTF_8)); + } + + @Test + public void invokeBatchMintWithoutInitializeTest() { + setOrg1MspId(this.ci); + when(ci.getId()).thenReturn(ORG1_USER_ID); + Throwable thrown = + catchThrowable( + () -> { + this.contract.MintBatch(this.ctx, ORG1_USER_ID, new long[] {1}, new long[] {1}); + }); + assertThat(thrown) + .isInstanceOf(ChaincodeException.class) + .hasNoCause() + .hasMessage( + "Contract options need to be set before calling any function, call Initialize() to initialize contract"); + } + + @Test + public void misMatchedTokenIdsAndAmountsInputBatchMintTest() { + setOrg1MspId(this.ci); + when(ci.getId()).thenReturn(ORG1_USER_ID); + setTokenNameAndSysmbol(stub); + Throwable thrown = + catchThrowable( + () -> { + this.contract.MintBatch(ctx, ORG1_USER_ID, new long[] {1, 2}, new long[] {1}); + }); + assertThat(thrown) + .isInstanceOf(ChaincodeException.class) + .hasNoCause() + .hasMessage("ids and amounts must have the same length"); + } + + @Test + public void unAuthorizedBatchMintTest() { + setOrg2MspId(this.ci); + when(ci.getId()).thenReturn(ORG2_USER_ID); + setTokenNameAndSysmbol(stub); + Throwable thrown = + catchThrowable( + () -> { + this.contract.MintBatch(ctx, ORG2_USER_ID, new long[] {1}, new long[] {1}); + }); + assertThat(thrown) + .isInstanceOf(ChaincodeException.class) + .hasNoCause() + .hasMessage("Client is not authorized to set the name and symbol of the token"); + } + + @Test + public void invalidAmountInvokeMintBatchTest() { + setOrg1MspId(this.ci); + when(ci.getId()).thenReturn(ORG1_USER_ID); + setTokenNameAndSysmbol(stub); + Throwable thrown = + catchThrowable( + () -> { + this.contract.MintBatch(this.ctx, ORG2_USER_ID, new long[] {1}, new long[] {0}); + }); + assertThat(thrown) + .isInstanceOf(ChaincodeException.class) + .hasNoCause() + .hasMessage("Mint amount must be a positive integer"); + } + + @Test + public void invalidAccountInvokeMintBatchTest() { + setOrg1MspId(this.ci); + when(ci.getId()).thenReturn(ORG1_USER_ID); + setTokenNameAndSysmbol(stub); + Throwable thrown = + catchThrowable( + () -> { + this.contract.MintBatch(this.ctx, "0x0", new long[] {1}, new long[] {0}); + }); + assertThat(thrown) + .isInstanceOf(ChaincodeException.class) + .hasNoCause() + .hasMessage("mint to the zero address"); + } + + @Test + public void invokeBatchMintTest() { + setOrg1MspId(this.ci); + when(ci.getId()).thenReturn(ORG1_USER_ID); + CompositeKey ck1 = + createCompositeKey( + stub, "", ContractConstants.BALANCE_PREFIX, ORG1_USER_ID, "1", ORG1_USER_ID); + CompositeKey ck2 = + createCompositeKey( + stub, "", ContractConstants.BALANCE_PREFIX, ORG1_USER_ID, "2", ORG1_USER_ID); + setTokenNameAndSysmbol(stub); + this.contract.MintBatch(ctx, ORG1_USER_ID, new long[] {1, 2}, new long[] {1, 1}); + verify(this.stub).putStringState(ck1.toString(), "1"); + verify(this.stub).putStringState(ck2.toString(), "1"); + TransferBatch transferBatchEvent = + new TransferBatch( + ORG1_USER_ID, "0x0", ORG1_USER_ID, new long[] {1, 2}, new long[] {1, 1}); + verify(this.stub) + .setEvent("TransferBatch", new Genson().serialize(transferBatchEvent).getBytes(UTF_8)); + verify(this.stub) + .putStringState( + ContractConstants.BALANCE_PREFIX.getValue() + ORG1_USER_ID + "2" + ORG1_USER_ID, "1"); + } + } + + @Nested + class ContractBurnFunctionsTest { + + private ERC1155Contract contract; + private Context ctx; + private ChaincodeStub stub; + private CompositeKey ck1; + private ClientIdentity ci = null; + + @BeforeEach + public void initialize() { + this.contract = new ERC1155Contract(); + this.ctx = mock(Context.class); + this.stub = mock(ChaincodeStub.class); + when(this.ctx.getStub()).thenReturn(this.stub); + ci = mock(ClientIdentity.class); + ck1 = mock(CompositeKey.class); + when(this.ctx.getClientIdentity()).thenReturn(ci); + when(ci.getId()).thenReturn(ORG1_USER_ID); + when(ck1.toString()) + .thenReturn( + ContractConstants.BALANCE_PREFIX.getValue() + ORG1_USER_ID + "1" + ORG1_USER_ID); + when(this.stub.createCompositeKey( + ContractConstants.BALANCE_PREFIX.getValue(), ORG1_USER_ID, "1", ORG1_USER_ID)) + .thenReturn(ck1); + when(stub.getStringState(ck1.toString())).thenReturn(""); + } + + @Test + public void invokeTokenBurnTest() { + + setOrg1MspId(this.ci); + createCompositeKey( + stub, "", ContractConstants.BALANCE_PREFIX, ORG1_USER_ID, "2", ORG1_USER_ID); + setTokenNameAndSysmbol(stub); + List list = new ArrayList(); + list.add(new MockKeyValue("1", "10")); + list.add(new MockKeyValue("2", "10")); + when(stub.getStateByPartialCompositeKey( + ContractConstants.BALANCE_PREFIX.getValue(), ORG1_USER_ID, "1")) + .thenReturn(new MockAssetResultsIterator(list)); + when(stub.splitCompositeKey("1")).thenReturn(ck1); + this.contract.Burn(ctx, ORG1_USER_ID, 1, 1); + verify(this.stub).putStringState(ck1.toString(), "9"); + } + + @Test + public void invokeBurnWithoutInitializeTest() { + setOrg1MspId(this.ci); + Throwable thrown = + catchThrowable( + () -> { + this.contract.Burn(ctx, ORG1_USER_ID, 1, 1); + }); + assertThat(thrown) + .isInstanceOf(ChaincodeException.class) + .hasNoCause() + .hasMessage( + "Contract options need to be set before calling any function, call Initialize() to initialize contract"); + } + + @Test + public void invokeWithInSufficientFundTokenBurnTest() { + + setOrg1MspId(this.ci); + setTokenNameAndSysmbol(stub); + createCompositeKey( + stub, "", ContractConstants.BALANCE_PREFIX, ORG1_USER_ID, "2", ORG1_USER_ID); + setResultsIterator( + stub, + ContractConstants.BALANCE_PREFIX, + new String[] {ORG1_USER_ID, "1"}, + new MockKeyValue("1", "10")); + when(stub.splitCompositeKey("1")).thenReturn(ck1); + Throwable thrown = + catchThrowable( + () -> { + this.contract.Burn(ctx, ORG1_USER_ID, 1, 12); + }); + assertThat(thrown) + .isInstanceOf(ChaincodeException.class) + .hasNoCause() + .hasMessage( + String.format( + "sender has insufficient funds for token %s, needed funds: %d, available fund: %d", + "1", 12, 10)); + } + + @Test + public void invokeWithZeroAddressTokenBurnTest() { + + setOrg1MspId(this.ci); + setTokenNameAndSysmbol(stub); + createCompositeKey( + stub, "", ContractConstants.BALANCE_PREFIX, ORG1_USER_ID, "2", ORG1_USER_ID); + setResultsIterator( + stub, + ContractConstants.BALANCE_PREFIX, + new String[] {ORG1_USER_ID, "1"}, + new MockKeyValue("1", "10")); + when(stub.splitCompositeKey("1")).thenReturn(ck1); + + Throwable thrown = + catchThrowable( + () -> { + this.contract.Burn(ctx, "0x0", 1, 12); + }); + + assertThat(thrown) + .isInstanceOf(ChaincodeException.class) + .hasNoCause() + .hasMessage("burn to the zero address"); + } + + @Test + public void invokeTokenBatchBurnTest() { + + setTokenNameAndSysmbol(stub); + setOrg1MspId(this.ci); + CompositeKey ck2 = + createCompositeKey( + stub, "", ContractConstants.BALANCE_PREFIX, ORG1_USER_ID, "2", ORG1_USER_ID); + setResultsIterator( + stub, + ContractConstants.BALANCE_PREFIX, + new String[] {ORG1_USER_ID, "1"}, + new MockKeyValue("1", "10")); + setResultsIterator( + stub, + ContractConstants.BALANCE_PREFIX, + new String[] {ORG1_USER_ID, "2"}, + new MockKeyValue("2", "10")); + when(stub.splitCompositeKey("1")).thenReturn(ck1); + when(stub.splitCompositeKey("2")).thenReturn(ck2); + this.contract.BurnBatch(ctx, ORG1_USER_ID, new long[] {1, 2}, new long[] {1, 1}); + verify(this.stub).putStringState(ck1.toString(), "9"); + verify(this.stub).putStringState(ck2.toString(), "9"); + } + + @Test + public void invokeBurnBatchWithoutInitializeTest() { + setOrg1MspId(this.ci); + when(this.ctx.getClientIdentity()).thenReturn(ci); + Throwable thrown = + catchThrowable( + () -> { + this.contract.BurnBatch(ctx, ORG1_USER_ID, new long[] {1, 2}, new long[] {1, 1}); + }); + assertThat(thrown) + .isInstanceOf(ChaincodeException.class) + .hasNoCause() + .hasMessage( + "Contract options need to be set before calling any function, call Initialize() to initialize contract"); + } + + @Test + public void invokeInsufficientFundTokenBatchBurnTest() { + setTokenNameAndSysmbol(this.stub); + setOrg1MspId(this.ci); + CompositeKey ck2 = + createCompositeKey( + stub, "", ContractConstants.BALANCE_PREFIX, ORG1_USER_ID, "2", ORG1_USER_ID); + setResultsIterator( + stub, + ContractConstants.BALANCE_PREFIX, + new String[] {ORG1_USER_ID, "1"}, + new MockKeyValue("1", "10")); + setResultsIterator( + stub, + ContractConstants.BALANCE_PREFIX, + new String[] {ORG1_USER_ID, "2"}, + new MockKeyValue("2", "10")); + when(stub.splitCompositeKey("1")).thenReturn(ck1); + when(stub.splitCompositeKey("2")).thenReturn(ck2); + + Throwable thrown = + catchThrowable( + () -> { + this.contract.BurnBatch(ctx, ORG1_USER_ID, new long[] {1, 2}, new long[] {12, 12}); + }); + + String error = + String.format( + "sender has insufficient funds for token %s, needed funds: %d, available fund: %d", + "1", 12, 10); + assertThat(thrown).isInstanceOf(ChaincodeException.class).hasNoCause().hasMessage(error); + } + } + + @Nested + class ContractTransferFunctionsTest { + + private ERC1155Contract contract; + private Context ctx; + private ChaincodeStub stub; + private CompositeKey ck1; + private ClientIdentity ci = null; + + @BeforeEach + public void initialize() { + this.contract = new ERC1155Contract(); + this.ctx = mock(Context.class); + this.stub = mock(ChaincodeStub.class); + when(this.ctx.getStub()).thenReturn(this.stub); + ck1 = + createCompositeKey( + stub, "", ContractConstants.BALANCE_PREFIX, ORG1_USER_ID, "1", ORG1_USER_ID); + ci = mock(ClientIdentity.class); + when(ci.getMSPID()).thenReturn("Org1MSP"); + when(ci.getId()).thenReturn(ORG1_USER_ID); + when(this.ctx.getClientIdentity()).thenReturn(ci); + when(stub.splitCompositeKey("1")).thenReturn(ck1); + } + + @Test + public void invokeTokenTransferTest() { + CompositeKey org2Balance = + createCompositeKey( + stub, "", ContractConstants.BALANCE_PREFIX, ORG2_USER_ID, "1", ORG1_USER_ID); + setTokenNameAndSysmbol(this.stub); + setResultsIterator( + stub, + ContractConstants.BALANCE_PREFIX, + new String[] {ORG1_USER_ID, "1"}, + new MockKeyValue("1", "10")); + this.contract.TransferFrom(ctx, ORG1_USER_ID, ORG2_USER_ID, 1, 2); + verify(this.stub).putStringState(ck1.toString(), "8"); + verify(this.stub).putStringState(org2Balance.toString(), "2"); + TransferSingle transferSingleEvent = + new TransferSingle(ORG1_USER_ID, ORG1_USER_ID, ORG2_USER_ID, 1, 2); + verify(this.stub) + .setEvent("TransferSingle", new Genson().serialize(transferSingleEvent).getBytes(UTF_8)); + } + + @Test + public void invokeTokenTransferByOperatorTest() { + when(this.ci.getMSPID()).thenReturn("Org3MSP"); + when(this.ci.getId()).thenReturn(ORG3_USER_ID); + CompositeKey org2Balance = + createCompositeKey( + stub, "", ContractConstants.BALANCE_PREFIX, ORG2_USER_ID, "1", ORG1_USER_ID); + createCompositeKey( + stub, "true", ContractConstants.APPROVAL_PREFIX, ORG1_USER_ID, ORG3_USER_ID); + setTokenNameAndSysmbol(this.stub); + setResultsIterator( + stub, + ContractConstants.BALANCE_PREFIX, + new String[] {ORG1_USER_ID, "1"}, + new MockKeyValue("1", "10")); + this.contract.TransferFrom(ctx, ORG1_USER_ID, ORG2_USER_ID, 1, 2); + verify(this.stub).putStringState(ck1.toString(), "8"); + verify(this.stub).putStringState(org2Balance.toString(), "2"); + TransferSingle transferSingleEvent = + new TransferSingle(ORG3_USER_ID, ORG1_USER_ID, ORG2_USER_ID, 1, 2); + verify(this.stub) + .setEvent("TransferSingle", new Genson().serialize(transferSingleEvent).getBytes(UTF_8)); + } + + @Test + public void invokeTokenTransferWithUnApprovedOperatorTest() { + + setTokenNameAndSysmbol(this.stub); + when(ci.getMSPID()).thenReturn("Org3MSP"); + when(ci.getId()).thenReturn(ORG3_USER_ID); + createCompositeKey( + stub, "", ContractConstants.BALANCE_PREFIX, ORG2_USER_ID, "1", ORG1_USER_ID); + createCompositeKey(stub, "", ContractConstants.APPROVAL_PREFIX, ORG1_USER_ID, ORG3_USER_ID); + setResultsIterator( + stub, + ContractConstants.BALANCE_PREFIX, + new String[] {ORG1_USER_ID, "1"}, + new MockKeyValue("1", "10")); + Throwable thrown = + catchThrowable( + () -> { + this.contract.TransferFrom(ctx, ORG1_USER_ID, ORG2_USER_ID, 1, 2); + }); + assertThat(thrown) + .isInstanceOf(ChaincodeException.class) + .hasNoCause() + .hasMessage("caller is not owner nor is approved"); + } + + @Test + public void invokeTokenTransferWithZeroAddressReceiverTest() { + setTokenNameAndSysmbol(this.stub); + when(ci.getMSPID()).thenReturn("Org3MSP"); + when(ci.getId()).thenReturn(ORG3_USER_ID); + createCompositeKey( + stub, "", ContractConstants.BALANCE_PREFIX, ORG2_USER_ID, "1", ORG1_USER_ID); + createCompositeKey(stub, "", ContractConstants.APPROVAL_PREFIX, ORG1_USER_ID, ORG3_USER_ID); + setResultsIterator( + stub, + ContractConstants.BALANCE_PREFIX, + new String[] {ORG1_USER_ID, "1"}, + new MockKeyValue("1", "10")); + + Throwable thrown = + catchThrowable( + () -> { + this.contract.TransferFrom(ctx, ORG1_USER_ID, "0x0", 1, 2); + }); + assertThat(thrown) + .isInstanceOf(ChaincodeException.class) + .hasNoCause() + .hasMessage("transfer to the zero address"); + } + + @Test + public void invokeTokenTransferWithInsufficientBalanceTest() { + + setTokenNameAndSysmbol(this.stub); + when(ci.getMSPID()).thenReturn("Org3MSP"); + when(ci.getId()).thenReturn(ORG3_USER_ID); + createCompositeKey( + stub, "", ContractConstants.BALANCE_PREFIX, ORG2_USER_ID, "1", ORG1_USER_ID); + createCompositeKey( + stub, "true", ContractConstants.APPROVAL_PREFIX, ORG1_USER_ID, ORG3_USER_ID); + setResultsIterator( + stub, + ContractConstants.BALANCE_PREFIX, + new String[] {ORG1_USER_ID, "1"}, + new MockKeyValue("1", "10")); + + Throwable thrown = + catchThrowable( + () -> { + this.contract.TransferFrom(ctx, ORG1_USER_ID, ORG2_USER_ID, 1, 12); + }); + String error = + String.format( + "sender has insufficient funds for token %s, needed funds: %d, available fund: %d", + "1", 12, 10); + assertThat(thrown).isInstanceOf(ChaincodeException.class).hasNoCause().hasMessage(error); + } + + @Test + public void invokeTokenTransferBySelfTest() { + setTokenNameAndSysmbol(this.stub); + + createCompositeKey( + stub, "", ContractConstants.BALANCE_PREFIX, ORG2_USER_ID, "1", ORG1_USER_ID); + createCompositeKey( + stub, "true", ContractConstants.APPROVAL_PREFIX, ORG1_USER_ID, ORG3_USER_ID); + setResultsIterator( + stub, + ContractConstants.BALANCE_PREFIX, + new String[] {ORG1_USER_ID, "1"}, + new MockKeyValue("1", "10")); + + Throwable thrown = + catchThrowable( + () -> { + this.contract.TransferFrom(ctx, ORG1_USER_ID, ORG1_USER_ID, 1, 12); + }); + String error = "transfer to self"; + assertThat(thrown).isInstanceOf(ChaincodeException.class).hasNoCause().hasMessage(error); + } + + @Test + public void invokeTokenBatchTransferTest() { + setTokenNameAndSysmbol(this.stub); + + CompositeKey org1BalancePrefix = + createCompositeKey( + stub, "", ContractConstants.BALANCE_PREFIX, ORG1_USER_ID, "2", ORG1_USER_ID); + + CompositeKey org2Token1BalancePrefix = + createCompositeKey( + stub, "", ContractConstants.BALANCE_PREFIX, ORG2_USER_ID, "1", ORG1_USER_ID); + + CompositeKey org2Token2BalancePrefix = + createCompositeKey( + stub, "", ContractConstants.BALANCE_PREFIX, ORG2_USER_ID, "2", ORG1_USER_ID); + + setResultsIterator( + stub, + ContractConstants.BALANCE_PREFIX, + new String[] {ORG1_USER_ID, "1"}, + new MockKeyValue("1", "10"), + new MockKeyValue("1", "10")); + setResultsIterator( + stub, + ContractConstants.BALANCE_PREFIX, + new String[] {ORG1_USER_ID, "2"}, + new MockKeyValue("2", "10"), + new MockKeyValue("2", "10")); + + when(stub.splitCompositeKey("1")).thenReturn(org1BalancePrefix); + when(stub.splitCompositeKey("2")).thenReturn(org2Token1BalancePrefix); + this.contract.BatchTransferFrom( + ctx, ORG1_USER_ID, ORG2_USER_ID, new long[] {1, 2}, new long[] {1, 1}); + verify(this.stub).putStringState(ck1.toString(), "9"); + verify(this.stub).putStringState(org2Token1BalancePrefix.toString(), "1"); + verify(this.stub).putStringState(ck1.toString(), "9"); + verify(this.stub).putStringState(org2Token2BalancePrefix.toString(), "1"); + + TransferBatch transferBatchEvent = + new TransferBatch( + ORG1_USER_ID, ORG1_USER_ID, ORG2_USER_ID, new long[] {1, 2}, new long[] {1, 1}); + verify(this.stub) + .setEvent("TransferBatch", new Genson().serialize(transferBatchEvent).getBytes(UTF_8)); + } + + @Test + public void invokeTokenBatchTransferByOperatorTest() { + + setTokenNameAndSysmbol(this.stub); + when(ci.getMSPID()).thenReturn("Org3MSP"); + when(ci.getId()).thenReturn(ORG3_USER_ID); + createCompositeKey( + stub, "", ContractConstants.BALANCE_PREFIX, ORG1_USER_ID, "2", ORG1_USER_ID); + CompositeKey org2Token1BalancePrefix = + createCompositeKey( + stub, "", ContractConstants.BALANCE_PREFIX, ORG2_USER_ID, "1", ORG1_USER_ID); + CompositeKey org2Token2BalancePrefix = + createCompositeKey( + stub, "", ContractConstants.BALANCE_PREFIX, ORG2_USER_ID, "2", ORG1_USER_ID); + createCompositeKey( + stub, "true", ContractConstants.APPROVAL_PREFIX, ORG1_USER_ID, ORG3_USER_ID); + setResultsIterator( + stub, + ContractConstants.BALANCE_PREFIX, + new String[] {ORG1_USER_ID, "1"}, + new MockKeyValue("1", "10"), + new MockKeyValue("1", "10")); + setResultsIterator( + stub, + ContractConstants.BALANCE_PREFIX, + new String[] {ORG1_USER_ID, "2"}, + new MockKeyValue("2", "10"), + new MockKeyValue("2", "10")); + when(stub.splitCompositeKey("1")).thenReturn(ck1); + when(stub.splitCompositeKey("2")).thenReturn(org2Token1BalancePrefix); + this.contract.BatchTransferFrom( + ctx, ORG1_USER_ID, ORG2_USER_ID, new long[] {1, 2}, new long[] {1, 1}); + verify(this.stub).putStringState(ck1.toString(), "9"); + verify(this.stub).putStringState(org2Token1BalancePrefix.toString(), "1"); + verify(this.stub).putStringState(ck1.toString(), "9"); + verify(this.stub).putStringState(org2Token2BalancePrefix.toString(), "1"); + TransferBatch transferBatchEvent = + new TransferBatch( + ORG3_USER_ID, ORG1_USER_ID, ORG2_USER_ID, new long[] {1, 2}, new long[] {1, 1}); + verify(this.stub) + .setEvent("TransferBatch", new Genson().serialize(transferBatchEvent).getBytes(UTF_8)); + } + + @Test + public void invokeTokenBatchTransferByUnApprovedOperatorTest() { + setTokenNameAndSysmbol(this.stub); + when(ci.getMSPID()).thenReturn("Org3MSP"); + when(ci.getId()).thenReturn(ORG3_USER_ID); + createCompositeKey( + stub, "", ContractConstants.BALANCE_PREFIX, ORG2_USER_ID, "1", ORG1_USER_ID); + createCompositeKey( + stub, "", ContractConstants.BALANCE_PREFIX, ORG2_USER_ID, "2", ORG1_USER_ID); + createCompositeKey(stub, "", ContractConstants.APPROVAL_PREFIX, ORG1_USER_ID, ORG3_USER_ID); + setResultsIterator( + stub, + ContractConstants.BALANCE_PREFIX, + new String[] {ORG1_USER_ID, "1"}, + new MockKeyValue("1", "10"), + new MockKeyValue("1", "10")); + setResultsIterator( + stub, + ContractConstants.BALANCE_PREFIX, + new String[] {ORG1_USER_ID, "2"}, + new MockKeyValue("2", "10"), + new MockKeyValue("2", "10")); + Throwable thrown = + catchThrowable( + () -> { + this.contract.BatchTransferFrom( + ctx, ORG1_USER_ID, ORG2_USER_ID, new long[] {1, 2}, new long[] {1, 1}); + }); + String error = "caller is not owner nor is approved"; + assertThat(thrown).isInstanceOf(ChaincodeException.class).hasNoCause().hasMessage(error); + } + + @Test + public void invokeTokenBatchTransferWithMismatchedIdsAndAmountInputTest() { + setTokenNameAndSysmbol(this.stub); + when(ci.getMSPID()).thenReturn("Org3MSP"); + when(ci.getId()).thenReturn(ORG3_USER_ID); + createCompositeKey( + stub, "", ContractConstants.BALANCE_PREFIX, ORG2_USER_ID, "1", ORG1_USER_ID); + createCompositeKey( + stub, "", ContractConstants.BALANCE_PREFIX, ORG2_USER_ID, "2", ORG1_USER_ID); + createCompositeKey(stub, "", ContractConstants.APPROVAL_PREFIX, ORG1_USER_ID, ORG3_USER_ID); + setResultsIterator( + stub, + ContractConstants.BALANCE_PREFIX, + new String[] {ORG1_USER_ID, "1"}, + new MockKeyValue("1", "10"), + new MockKeyValue("1", "10")); + setResultsIterator( + stub, + ContractConstants.BALANCE_PREFIX, + new String[] {ORG1_USER_ID, "2"}, + new MockKeyValue("2", "10"), + new MockKeyValue("2", "10")); + when(stub.splitCompositeKey("1")).thenReturn(ck1); + Throwable thrown = + catchThrowable( + () -> { + this.contract.BatchTransferFrom( + ctx, ORG1_USER_ID, ORG2_USER_ID, new long[] {1}, new long[] {1, 1}); + }); + String error = "ids and amounts must have the same length"; + assertThat(thrown).isInstanceOf(ChaincodeException.class).hasNoCause().hasMessage(error); + } + + @Test + public void invokeTokenBatchTransferMultiReceipentByOperatorTest() { + + setTokenNameAndSysmbol(this.stub); + when(ci.getMSPID()).thenReturn("Org3MSP"); + when(ci.getId()).thenReturn(ORG3_USER_ID); + CompositeKey ck2 = + createCompositeKey( + stub, "", ContractConstants.BALANCE_PREFIX, ORG1_USER_ID, "2", ORG1_USER_ID); + CompositeKey ck3 = + createCompositeKey( + stub, "", ContractConstants.BALANCE_PREFIX, ORG2_USER_ID, "1", ORG1_USER_ID); + createCompositeKey( + stub, "true", ContractConstants.APPROVAL_PREFIX, ORG1_USER_ID, ORG3_USER_ID); + createCompositeKey( + stub, "", ContractConstants.BALANCE_PREFIX, ORG2_USER_ID, "2", ORG1_USER_ID); + setResultsIterator( + stub, + ContractConstants.BALANCE_PREFIX, + new String[] {ORG1_USER_ID, "1"}, + new MockKeyValue("1", "10"), + new MockKeyValue("1", "10")); + setResultsIterator( + stub, + ContractConstants.BALANCE_PREFIX, + new String[] {ORG1_USER_ID, "2"}, + new MockKeyValue("2", "10"), + new MockKeyValue("2", "10")); + when(stub.splitCompositeKey("1")).thenReturn(ck1); + when(stub.splitCompositeKey("2")).thenReturn(ck2); + this.contract.BatchTransferFromMultiRecipient( + ctx, + ORG1_USER_ID, + new String[] {ORG2_USER_ID, ORG2_USER_ID}, + new long[] {1, 2}, + new long[] {1, 1}); + verify(this.stub).putStringState(ck1.toString(), "9"); + verify(this.stub).putStringState(ck3.toString(), "1"); + + verify(this.stub).putStringState(ck1.toString(), "9"); + verify(this.stub).putStringState(ck3.toString(), "1"); + TransferBatchMultiRecipient transferBatchMultiRecipientEvent = + new TransferBatchMultiRecipient( + ORG3_USER_ID, + ORG1_USER_ID, + new String[] {ORG2_USER_ID, ORG2_USER_ID}, + new long[] {1, 2}, + new long[] {1, 1}); + + verify(this.stub) + .setEvent( + "TransferBatchMultiRecipient", + new Genson().serialize(transferBatchMultiRecipientEvent).getBytes(UTF_8)); + } + + @Test + public void invokeTokenBatchTransferMultiReceipentTest() { + + setTokenNameAndSysmbol(this.stub); + CompositeKey ck2 = + createCompositeKey( + stub, "", ContractConstants.BALANCE_PREFIX, ORG1_USER_ID, "2", ORG1_USER_ID); + CompositeKey ck3 = + createCompositeKey( + stub, "", ContractConstants.BALANCE_PREFIX, ORG2_USER_ID, "1", ORG1_USER_ID); + createCompositeKey( + stub, "", ContractConstants.BALANCE_PREFIX, ORG3_USER_ID, "2", ORG1_USER_ID); + setResultsIterator( + stub, + ContractConstants.BALANCE_PREFIX, + new String[] {ORG1_USER_ID, "1"}, + new MockKeyValue("1", "10"), + new MockKeyValue("1", "10")); + setResultsIterator( + stub, + ContractConstants.BALANCE_PREFIX, + new String[] {ORG1_USER_ID, "2"}, + new MockKeyValue("2", "10"), + new MockKeyValue("2", "10")); + when(stub.splitCompositeKey("1")).thenReturn(ck1); + when(stub.splitCompositeKey("2")).thenReturn(ck2); + this.contract.BatchTransferFromMultiRecipient( + ctx, + ORG1_USER_ID, + new String[] {ORG2_USER_ID, ORG3_USER_ID}, + new long[] {1, 2}, + new long[] {1, 1}); + verify(this.stub).putStringState(ck1.toString(), "9"); + verify(this.stub).putStringState(ck3.toString(), "1"); + verify(this.stub).putStringState(ck1.toString(), "9"); + verify(this.stub).putStringState(ck3.toString(), "1"); + TransferBatchMultiRecipient transferBatchMultiRecipientEvent = + new TransferBatchMultiRecipient( + ORG1_USER_ID, + ORG1_USER_ID, + new String[] {ORG2_USER_ID, ORG3_USER_ID}, + new long[] {1, 2}, + new long[] {1, 1}); + verify(this.stub) + .setEvent( + "TransferBatchMultiRecipient", + new Genson().serialize(transferBatchMultiRecipientEvent).getBytes(UTF_8)); + } + + @Test + public void invokeTokenBatchTransferMultiReceipentByUnAutorizedOperatorTest() { + + setTokenNameAndSysmbol(this.stub); + when(ci.getMSPID()).thenReturn("Org3MSP"); + when(ci.getId()).thenReturn(ORG3_USER_ID); + CompositeKey ck2 = + createCompositeKey( + stub, "", ContractConstants.BALANCE_PREFIX, ORG1_USER_ID, "2", ORG1_USER_ID); + createCompositeKey( + stub, "", ContractConstants.BALANCE_PREFIX, ORG2_USER_ID, "1", ORG1_USER_ID); + createCompositeKey( + stub, "false", ContractConstants.APPROVAL_PREFIX, ORG1_USER_ID, ORG3_USER_ID); + createCompositeKey( + stub, "", ContractConstants.BALANCE_PREFIX, ORG2_USER_ID, "2", ORG1_USER_ID); + setResultsIterator( + stub, + ContractConstants.BALANCE_PREFIX, + new String[] {ORG1_USER_ID, "1"}, + new MockKeyValue("1", "10"), + new MockKeyValue("1", "10")); + setResultsIterator( + stub, + ContractConstants.BALANCE_PREFIX, + new String[] {ORG1_USER_ID, "2"}, + new MockKeyValue("2", "10"), + new MockKeyValue("2", "10")); + when(stub.splitCompositeKey("1")).thenReturn(ck1); + when(stub.splitCompositeKey("2")).thenReturn(ck2); + Throwable thrown = + catchThrowable( + () -> { + this.contract.BatchTransferFromMultiRecipient( + ctx, + ORG1_USER_ID, + new String[] {ORG2_USER_ID, ORG2_USER_ID}, + new long[] {1, 2}, + new long[] {1, 1}); + }); + assertThat(thrown) + .isInstanceOf(ChaincodeException.class) + .hasNoCause() + .hasMessage("caller is not owner nor is approved"); + } + + @Test + public void invokeTokenBatchTransferMultiReceipentWithMisMatchedRecipientsAndIdsTest() { + + setTokenNameAndSysmbol(this.stub); + when(ci.getMSPID()).thenReturn("Org3MSP"); + when(ci.getId()).thenReturn(ORG3_USER_ID); + CompositeKey ck2 = + createCompositeKey( + stub, "", ContractConstants.BALANCE_PREFIX, ORG1_USER_ID, "2", ORG1_USER_ID); + createCompositeKey( + stub, "", ContractConstants.BALANCE_PREFIX, ORG2_USER_ID, "1", ORG1_USER_ID); + createCompositeKey( + stub, "false", ContractConstants.APPROVAL_PREFIX, ORG1_USER_ID, ORG3_USER_ID); + createCompositeKey( + stub, "", ContractConstants.BALANCE_PREFIX, ORG2_USER_ID, "2", ORG1_USER_ID); + setResultsIterator( + stub, + ContractConstants.BALANCE_PREFIX, + new String[] {ORG1_USER_ID, "1"}, + new MockKeyValue("1", "10"), + new MockKeyValue("1", "10")); + setResultsIterator( + stub, + ContractConstants.BALANCE_PREFIX, + new String[] {ORG1_USER_ID, "2"}, + new MockKeyValue("2", "10"), + new MockKeyValue("2", "10")); + when(stub.splitCompositeKey("1")).thenReturn(ck1); + when(stub.splitCompositeKey("2")).thenReturn(ck2); + Throwable thrown = + catchThrowable( + () -> { + this.contract.BatchTransferFromMultiRecipient( + ctx, + ORG1_USER_ID, + new String[] {ORG2_USER_ID}, + new long[] {1, 2}, + new long[] {1, 1}); + }); + assertThat(thrown) + .isInstanceOf(ChaincodeException.class) + .hasNoCause() + .hasMessage("recipients, ids, and amounts must have the same length"); + } + + @Test + public void invokeTokenBatchTransferMultiReceipentWithMisMatchedAmountsAndIdsTest() { + + setTokenNameAndSysmbol(this.stub); + when(ci.getMSPID()).thenReturn("Org3MSP"); + when(ci.getId()).thenReturn(ORG3_USER_ID); + CompositeKey ck2 = + createCompositeKey( + stub, "", ContractConstants.BALANCE_PREFIX, ORG1_USER_ID, "2", ORG1_USER_ID); + createCompositeKey( + stub, "", ContractConstants.BALANCE_PREFIX, ORG2_USER_ID, "1", ORG1_USER_ID); + createCompositeKey( + stub, "false", ContractConstants.APPROVAL_PREFIX, ORG1_USER_ID, ORG3_USER_ID); + createCompositeKey( + stub, "", ContractConstants.BALANCE_PREFIX, ORG2_USER_ID, "2", ORG1_USER_ID); + setResultsIterator( + stub, + ContractConstants.BALANCE_PREFIX, + new String[] {ORG1_USER_ID, "1"}, + new MockKeyValue("1", "10"), + new MockKeyValue("1", "10")); + setResultsIterator( + stub, + ContractConstants.BALANCE_PREFIX, + new String[] {ORG1_USER_ID, "2"}, + new MockKeyValue("2", "10"), + new MockKeyValue("2", "10")); + when(stub.splitCompositeKey("1")).thenReturn(ck1); + when(stub.splitCompositeKey("2")).thenReturn(ck2); + Throwable thrown = + catchThrowable( + () -> { + this.contract.BatchTransferFromMultiRecipient( + ctx, + ORG1_USER_ID, + new String[] {ORG2_USER_ID}, + new long[] {1, 2}, + new long[] {1, 1}); + }); + assertThat(thrown) + .isInstanceOf(ChaincodeException.class) + .hasNoCause() + .hasMessage("recipients, ids, and amounts must have the same length"); + } + + @Test + public void invokeTokenBatchTransferMultiReceipentWithInsufficientFundTest() { + + setTokenNameAndSysmbol(this.stub); + CompositeKey ck2 = + createCompositeKey( + stub, "", ContractConstants.BALANCE_PREFIX, ORG1_USER_ID, "2", ORG1_USER_ID); + createCompositeKey( + stub, "", ContractConstants.BALANCE_PREFIX, ORG2_USER_ID, "1", ORG1_USER_ID); + createCompositeKey( + stub, "", ContractConstants.BALANCE_PREFIX, ORG3_USER_ID, "2", ORG1_USER_ID); + setResultsIterator( + stub, + ContractConstants.BALANCE_PREFIX, + new String[] {ORG1_USER_ID, "1"}, + new MockKeyValue("1", "10"), + new MockKeyValue("1", "10")); + setResultsIterator( + stub, + ContractConstants.BALANCE_PREFIX, + new String[] {ORG1_USER_ID, "2"}, + new MockKeyValue("2", "10"), + new MockKeyValue("2", "10")); + + when(stub.splitCompositeKey("1")).thenReturn(ck1); + when(stub.splitCompositeKey("2")).thenReturn(ck2); + + Throwable thrown = + catchThrowable( + () -> { + this.contract.BatchTransferFromMultiRecipient( + ctx, + ORG1_USER_ID, + new String[] {ORG2_USER_ID, ORG3_USER_ID}, + new long[] {1, 2}, + new long[] {31, 9}); + }); + assertThat(thrown) + .isInstanceOf(ChaincodeException.class) + .hasNoCause() + .hasMessage( + "sender has insufficient funds for token 1, needed funds: 31, available fund: 20"); + } + } + + @Nested + class ContractUriFunctionsTest { + + private ERC1155Contract contract; + private Context ctx; + private ChaincodeStub stub; + private ClientIdentity ci = null; + + @BeforeEach + public void initialize() { + this.contract = new ERC1155Contract(); + this.ctx = mock(Context.class); + ci = mock(ClientIdentity.class); + this.stub = mock(ChaincodeStub.class); + when(this.ctx.getStub()).thenReturn(this.stub); + when(this.ctx.getClientIdentity()).thenReturn(ci); + } + + @Test + public void setUriTest() { + setTokenNameAndSysmbol(this.stub); + setOrg1MspId(this.ci); + when(ci.getId()).thenReturn(ORG1_USER_ID); + this.contract.SetURI(ctx, "http://ree/{id}.json"); + verify(this.stub) + .putStringState(ContractConstants.URI_KEY.getValue(), "http://ree/{id}.json"); + } + + @Test + public void setUriWithoutIdTest() { + setTokenNameAndSysmbol(this.stub); + setOrg1MspId(this.ci); + when(ci.getId()).thenReturn(ORG1_USER_ID); + this.contract.SetURI(ctx, "http://ree/{id}.json"); + verify(this.stub) + .putStringState(ContractConstants.URI_KEY.getValue(), "http://ree/{id}.json"); + Throwable thrown = + catchThrowable( + () -> { + this.contract.SetURI(ctx, "http://ree/2.json"); + }); + assertThat(thrown) + .isInstanceOf(ChaincodeException.class) + .hasNoCause() + .hasMessage("failed to set uri, uri should contain '{id}'"); + } + + @Test + public void invokeGetApproveAllWithoutInitializeTest() { + setOrg1MspId(this.ci); + when(ci.getId()).thenReturn(ORG1_USER_ID); + Throwable thrown = + catchThrowable( + () -> { + this.contract.SetURI(ctx, "http://ree/2.json"); + }); + assertThat(thrown) + .isInstanceOf(ChaincodeException.class) + .hasNoCause() + .hasMessage( + "Contract options need to be set before calling any function, call Initialize() to initialize contract"); + } + + @Test + public void getUriTest() { + setTokenNameAndSysmbol(this.stub); + setOrg1MspId(this.ci); + when(ci.getId()).thenReturn(ORG1_USER_ID); + when(stub.getStringState(ContractConstants.URI_KEY.getValue())) + .thenReturn("http://ree/{id}.json"); + String value = this.contract.URI(ctx, 1); + assertThat(value).isEqualTo("http://ree/{id}.json"); + } + } + + @Nested + class ContractApproveAndBalanceFunctionsTest { + + private ERC1155Contract contract; + private Context ctx; + private ChaincodeStub stub; + private ClientIdentity ci = null; + + @BeforeEach + public void initialize() { + this.contract = new ERC1155Contract(); + this.ctx = mock(Context.class); + ci = mock(ClientIdentity.class); + this.stub = mock(ChaincodeStub.class); + when(this.ctx.getStub()).thenReturn(this.stub); + when(this.ctx.getClientIdentity()).thenReturn(ci); + } + + @Test + public void setApproveForAllTest() { + setTokenNameAndSysmbol(this.stub); + setOrg1MspId(this.ci); + when(ci.getId()).thenReturn(ORG1_USER_ID); + CompositeKey ck = + createCompositeKey( + stub, "", ContractConstants.APPROVAL_PREFIX, ORG1_USER_ID, ORG2_USER_ID); + this.contract.SetApprovalForAll(ctx, ORG2_USER_ID, true); + verify(this.stub).putStringState(ck.toString(), "true"); + ApprovalForAll approvalForAllEvent = new ApprovalForAll(ORG1_USER_ID, ORG2_USER_ID, true); + verify(this.stub) + .setEvent("ApprovalForAll", new Genson().serialize(approvalForAllEvent).getBytes(UTF_8)); + } + + @Test + public void invokeSetApproveAllWithoutInitializeTest() { + setOrg1MspId(this.ci); + when(this.ci.getId()).thenReturn(ORG1_USER_ID); + Throwable thrown = + catchThrowable( + () -> { + this.contract.SetApprovalForAll(ctx, ORG2_USER_ID, true); + }); + assertThat(thrown) + .isInstanceOf(ChaincodeException.class) + .hasNoCause() + .hasMessage( + "Contract options need to be set before calling any function, call Initialize() to initialize contract"); + } + + @Test + public void isApproveForAllTest() { + setTokenNameAndSysmbol(this.stub); + setOrg1MspId(this.ci); + when(ci.getId()).thenReturn(ORG1_USER_ID); + CompositeKey ck = + createCompositeKey( + stub, "", ContractConstants.APPROVAL_PREFIX, ORG1_USER_ID, ORG2_USER_ID); + when(stub.getStringState(ck.toString())).thenReturn("true"); + boolean value = this.contract.IsApprovedForAll(ctx, ORG1_USER_ID, ORG2_USER_ID); + assertThat(value).isEqualTo(true); + } + + @Test + public void invokeGetApproveAllWithoutInitializeTest() { + setOrg1MspId(this.ci); + when(this.ci.getId()).thenReturn(ORG1_USER_ID); + Throwable thrown = + catchThrowable( + () -> { + this.contract.IsApprovedForAll(ctx, ORG1_USER_ID, ORG2_USER_ID); + }); + assertThat(thrown) + .isInstanceOf(ChaincodeException.class) + .hasNoCause() + .hasMessage( + "Contract options need to be set before calling any function, call Initialize() to initialize contract"); + } + + @Test + public void invokeBroadcastTokenExistanceTest() { + setTokenNameAndSysmbol(this.stub); + setOrg1MspId(this.ci); + when(ci.getId()).thenReturn(ORG1_USER_ID); + CompositeKey ck = + createCompositeKey( + stub, "", ContractConstants.APPROVAL_PREFIX, ORG1_USER_ID, ORG2_USER_ID); + when(stub.getStringState(ck.toString())).thenReturn("true"); + this.contract.BroadcastTokenExistance(ctx, 1); + TransferSingle transferSingleEvent = new TransferSingle(ORG1_USER_ID, "0x0", "0x0", 1, 0); + verify(this.stub) + .setEvent("TransferSingle", new Genson().serialize(transferSingleEvent).getBytes(UTF_8)); + } + + @Test + public void getBalanceOfTest() { + setTokenNameAndSysmbol(this.stub); + setOrg1MspId(this.ci); + when(ci.getId()).thenReturn(ORG1_USER_ID); + createCompositeKey(stub, "", ContractConstants.BALANCE_PREFIX, ORG1_USER_ID, "1"); + setResultsIterator( + stub, + ContractConstants.BALANCE_PREFIX, + new String[] {ORG1_USER_ID, "1"}, + new MockKeyValue("1", "10")); + long balance = this.contract.BalanceOf(ctx, ORG1_USER_ID, 1); + assertThat(balance).isEqualTo(10); + } + + @Test + public void getClientAccountBalanceTest() { + setTokenNameAndSysmbol(this.stub); + setOrg1MspId(this.ci); + when(ci.getId()).thenReturn(ORG1_USER_ID); + createCompositeKey(stub, "", ContractConstants.BALANCE_PREFIX, ORG1_USER_ID, "1"); + setResultsIterator( + stub, + ContractConstants.BALANCE_PREFIX, + new String[] {ORG1_USER_ID, "1"}, + new MockKeyValue("1", "10")); + long balance = this.contract.ClientAccountBalance(ctx, 1); + assertThat(balance).isEqualTo(10); + } + + @Test + public void getBalanceOfBatchTest() { + + setTokenNameAndSysmbol(this.stub); + setOrg1MspId(this.ci); + when(ci.getId()).thenReturn(ORG1_USER_ID); + createCompositeKey(stub, "", ContractConstants.BALANCE_PREFIX, ORG1_USER_ID, "1"); + createCompositeKey(stub, "", ContractConstants.BALANCE_PREFIX, ORG2_USER_ID, "2"); + setResultsIterator( + stub, + ContractConstants.BALANCE_PREFIX, + new String[] {ORG1_USER_ID, "1"}, + new MockKeyValue("1", "10")); + setResultsIterator( + stub, + ContractConstants.BALANCE_PREFIX, + new String[] {ORG2_USER_ID, "2"}, + new MockKeyValue("2", "20")); + long[] balance = + this.contract.BalanceOfBatch( + ctx, new String[] {ORG1_USER_ID, ORG2_USER_ID}, new long[] {1, 2}); + assertThat(balance.length).isEqualTo(2); + assertThat(balance[0]).isEqualTo(10); + assertThat(balance[1]).isEqualTo(20); + } + } +} diff --git a/token-erc-1155/chaincode-java/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/token-erc-1155/chaincode-java/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100755 index 00000000..ca6ee9ce --- /dev/null +++ b/token-erc-1155/chaincode-java/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline \ No newline at end of file diff --git a/token-erc-721/chaincode-java/config/checkstyle/checkstyle.xml b/token-erc-721/chaincode-java/config/checkstyle/checkstyle.xml old mode 100644 new mode 100755 diff --git a/token-erc-721/chaincode-java/config/checkstyle/suppressions.xml b/token-erc-721/chaincode-java/config/checkstyle/suppressions.xml old mode 100644 new mode 100755 diff --git a/token-erc-721/chaincode-java/gradle/wrapper/gradle-wrapper.jar b/token-erc-721/chaincode-java/gradle/wrapper/gradle-wrapper.jar old mode 100644 new mode 100755 diff --git a/token-erc-721/chaincode-java/gradle/wrapper/gradle-wrapper.properties b/token-erc-721/chaincode-java/gradle/wrapper/gradle-wrapper.properties old mode 100644 new mode 100755