Repository: blk-io/erc20-rest-service Branch: master Commit: b7e1933e6c21 Files: 27 Total size: 93.0 KB Directory structure: gitextract_nychjct6/ ├── .gitignore ├── README.md ├── build.gradle ├── docker/ │ └── Dockerfile ├── generate.sh ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src/ ├── main/ │ ├── java/ │ │ └── io/ │ │ └── blk/ │ │ └── erc20/ │ │ ├── Application.java │ │ ├── ContractService.java │ │ ├── Controller.java │ │ ├── NodeConfiguration.java │ │ ├── TransactionResponse.java │ │ └── generated/ │ │ └── HumanStandardToken.java │ └── resources/ │ ├── config/ │ │ └── application.yml │ ├── logback.xml │ └── solidity/ │ └── contract/ │ ├── HumanStandardToken.sol │ ├── HumanStandardTokenFactory.sol │ ├── StandardToken.sol │ ├── Token.sol │ └── build/ │ ├── HumanStandardToken.abi │ ├── StandardToken.abi │ └── Token.abi └── test/ ├── java/ │ └── io/ │ └── blk/ │ └── erc20/ │ └── ControllerIT.java └── resources/ └── logback-test.xml ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # Created by .ignore support plugin (hsz.mobi) ### Java template *.class # Mobile Tools for Java (J2ME) .mtj.tmp/ # Package Files # *.jar *.war *.ear # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* ### Gradle template .gradle /build src/main/resources/gradle.properties # Ignore Gradle GUI config gradle-app.setting # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) !gradle-wrapper.jar # Cache of project .gradletasknamecache # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 # gradle/wrapper/gradle-wrapper.properties .idea *.iml # OS X .DS_Store # log files *.log logs /out ================================================ FILE: README.md ================================================ # ERC-20 RESTful service This application provides a RESTful service for creating and managing [ERC-20 tokens](https://github.com/ethereum/EIPs/issues/20). It has been built using [Spring Boot](https://projects.spring.io/spring-boot/), and [web3j](https://web3j.io). It works with both [Geth](https://github.com/ethereum/go-ethereum), [Parity](https://github.com/paritytech/parity), and [Quorum](https://github.com/jpmorganchase/quorum). For Quorum, the RESTful semantics are identical, with the exception that if you wish to create a private transaction, you populate a HTTP header name *privateFor* with a comma-separated list of public keys ## Build To build a runnable jar file: ```bash ./gradlew clean build ``` ## Run Using Java 1.8+: ```bash java -jar build/libs/azure-demo-0.1.jar ``` By default the application will log to a file named erc20-web3j.log. ## Configuration The following default properties are used in the application: ```properties # Port for service to bind to port=8080 # Log file path and name logging.file=logs/erc20-rest-service.log # Endpoint of an Ethereum or Quorum node we wish to use. # To use IPC simply provide a file path to the socket, such as /path/to/geth.ipc nodeEndpoint=http://localhost:22000 # The Ethereum or Quorum address we wish to use when transacting. # Note - this address must be already unlocked in the client fromAddress=0xed9d02e382b34818e88b88a309c7fe71e65f419d ``` You can override any of these properties by creating a file name *application.properties* in the root directory of your application, or in *config/application.properties* relative to your root. If you'd rather use yaml, simply change the filename to *application.yml*. ## Usage All available application endpoints are documented using [Swagger](http://swagger.io/). You can view the Swagger UI at http://localhost:8080/swagger-ui.html. From here you can perform all POST and GET requests easily to facilitate deployment of, transacting with, and querying state of ERC-20 tokens. ![alt text](https://github.com/blk-io/erc20-rest-service/raw/master/images/full-swagger-ui.png "Swagger UI screen capture") ## Docker We can use [Docker](https://www.docker.com/) to easily spin up a arbritrary instance of our service connecting to an already running Ethereum or Quorum network. All you need to do is build the Dockerfile: ```docker docker build -f docker/Dockerfile -t blk-io/erc20-service . ``` Then either run it with default configuration: ```docker docker run -p 8080:8080 -v "$PWD/logs":/logs blk-io/erc20-service ``` Or with a custom configuration: ```docker export PORT=8081 docker run -p ${PORT}:${PORT} -v "$PWD/logs":/logs \ -e ENDPOINT="http://localhost:22001" \ -e FROMADDR="0xca843569e3427144cead5e4d5999a3d0ccf92b8e" \ -e PORT="$PORT" \ blk-io/erc20-service ``` ================================================ FILE: build.gradle ================================================ plugins { id 'java' id 'idea' id 'eclipse' id 'application' id 'org.springframework.boot' version '2.1.7.RELEASE' } mainClassName = 'io.blk.erc20.Application' group 'io.blk' version '0.1.0' sourceCompatibility = 1.8 repositories { mavenCentral() } dependencies { compile 'org.springframework.boot:spring-boot-starter-web:2.1.7.RELEASE', 'io.springfox:springfox-swagger2:2.7.0', 'io.springfox:springfox-swagger-ui:2.7.0', 'org.projectlombok:lombok:1.16.16', 'org.web3j:quorum:4.+', 'org.web3j:core:4.+', 'io.reactivex.rxjava2:rxjava:2.2.0', 'com.fasterxml.jackson:jackson-bom:2.9.4', 'org.apache.httpcomponents:httpclient:4.5.3' testCompile 'junit:junit:4.12', 'org.springframework.boot:spring-boot-starter-test:2.1.7.RELEASE', implementation("org.web3j:contracts:4.2.0") { exclude group: 'org.web3j' } } run { /* Can pass all the properties: */ systemProperties System.getProperties() /* Or just each by name: */ systemProperty "nodeEndpoint", System.getProperty("nodeEndpoint") systemProperty "fromAddress", System.getProperty("fromAddress") /* Need to split the space-delimited value in the exec.args */ args System.getProperty("exec.args", "").split() } ================================================ FILE: docker/Dockerfile ================================================ FROM frolvlad/alpine-java RUN apk update && apk upgrade && \ apk add --no-cache bash git openssh RUN mkdir -p /app/erc20-rest-service RUN git clone https://github.com/blk-io/erc20-rest-service.git # We exclude running tests as we need a Ethereum/Quorum network to be running RUN cd erc20-rest-service \ && ./gradlew build -x test \ && cp build/libs/erc20-rest-service-0.1.0.jar /app/erc20-rest-service ENV PORT=8080 ENV ENDPOINT="http://localhost:22000" ENV FROMADDR="0xed9d02e382b34818e88b88a309c7fe71e65f419d" ENTRYPOINT ["/usr/bin/java"] # We can define an environment variable to interpolate these values, as Docker does not support # this functionality, hence we have to do it in the command. CMD ["-jar", "/app/erc20-rest-service/erc20-rest-service-0.1.0.jar", \ "--spring.application.json={\"nodeEndpoint\":\"${ENDPOINT}\",\"fromAddress\":\"${FROMADDR}\"}"] ================================================ FILE: generate.sh ================================================ #!/usr/bin/env bash cd src/main/resources/solidity/contract/ && \ solc --bin --abi --optimize --overwrite HumanStandardToken.sol -o build/ && \ web3j solidity generate \ --binFile build/HumanStandardToken.bin \ --abiFile build/HumanStandardToken.abi \ -p io.blk.erc20.generated \ -o ../../../java/ ================================================ FILE: gradle/wrapper/gradle-wrapper.properties ================================================ #Mon Nov 18 15:02:41 GMT 2019 distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME ================================================ FILE: gradlew ================================================ #!/usr/bin/env sh # # Copyright 2015 the original author or authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ############################################################################## ## ## Gradle start up script for UN*X ## ############################################################################## # Attempt to set APP_HOME # Resolve links: $0 may be a link PRG="$0" # Need this for relative symlinks. while [ -h "$PRG" ] ; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` if expr "$link" : '/.*' > /dev/null; then PRG="$link" else PRG=`dirname "$PRG"`"/$link" fi done SAVED="`pwd`" cd "`dirname \"$PRG\"`/" >/dev/null APP_HOME="`pwd -P`" cd "$SAVED" >/dev/null APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" warn () { echo "$*" } die () { echo echo "$*" echo exit 1 } # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false case "`uname`" in CYGWIN* ) cygwin=true ;; Darwin* ) darwin=true ;; MINGW* ) msys=true ;; NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" else JAVACMD="$JAVA_HOME/bin/java" fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD="java" which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi # Increase the maximum file descriptors if we can. if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then MAX_FD="$MAX_FD_LIMIT" fi ulimit -n $MAX_FD if [ $? -ne 0 ] ; then warn "Could not set maximum file descriptor limit: $MAX_FD" fi else warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" fi fi # For Darwin, add options to specify how the application appears in the dock if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi # For Cygwin or MSYS, switch paths to Windows format before running java if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` SEP="" for dir in $ROOTDIRSRAW ; do ROOTDIRS="$ROOTDIRS$SEP$dir" SEP="|" done OURCYGPATTERN="(^($ROOTDIRS))" # Add a user-defined pattern to the cygpath arguments if [ "$GRADLE_CYGPATTERN" != "" ] ; then OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" fi # Now convert the arguments - kludge to limit ourselves to /bin/sh i=0 for arg in "$@" ; do CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` else eval `echo args$i`="\"$arg\"" fi i=$((i+1)) done case $i in (0) set -- ;; (1) set -- "$args0" ;; (2) set -- "$args0" "$args1" ;; (3) set -- "$args0" "$args1" "$args2" ;; (4) set -- "$args0" "$args1" "$args2" "$args3" ;; (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi # Escape application args save () { for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done echo " " } APP_ARGS=$(save "$@") # Collect all arguments for the java command, following the shell quoting and substitution rules eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then cd "$(dirname "$0")" fi exec "$JAVACMD" "$@" ================================================ FILE: gradlew.bat ================================================ @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 ================================================ FILE: settings.gradle ================================================ rootProject.name = 'erc20-rest-service' ================================================ FILE: src/main/java/io/blk/erc20/Application.java ================================================ package io.blk.erc20; import com.google.common.base.Predicates; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.Banner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.web3j.protocol.Web3jService; import org.web3j.protocol.http.HttpService; import org.web3j.protocol.ipc.UnixIpcService; import org.web3j.protocol.ipc.WindowsIpcService; import org.web3j.quorum.Quorum; import springfox.documentation.builders.ApiInfoBuilder; import springfox.documentation.builders.RequestHandlerSelectors; import springfox.documentation.service.ApiInfo; import springfox.documentation.spi.DocumentationType; import springfox.documentation.spring.web.plugins.Docket; import springfox.documentation.swagger2.annotations.EnableSwagger2; /** * Our main application class. */ @EnableSwagger2 @SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication application = new SpringApplication(Application.class); application.setBannerMode(Banner.Mode.OFF); application.run(args); } @Autowired NodeConfiguration nodeConfiguration; @Bean Quorum quorum() { String nodeEndpoint = nodeConfiguration.getNodeEndpoint(); Web3jService web3jService; if (nodeEndpoint == null || nodeEndpoint.equals("")) { web3jService = new HttpService(); } else if (nodeEndpoint.startsWith("http")) { web3jService = new HttpService(nodeEndpoint); } else if (System.getProperty("os.name").toLowerCase().startsWith("win")) { web3jService = new WindowsIpcService(nodeEndpoint); } else { web3jService = new UnixIpcService(nodeEndpoint); } return Quorum.build(web3jService); } @Bean public Docket lenderApi() { return new Docket(DocumentationType.SWAGGER_2) .apiInfo(apiInfo()) .select() // see https://github.com/springfox/springfox/issues/631 .apis(Predicates.not( RequestHandlerSelectors.basePackage("org.springframework.boot"))) .build(); } private ApiInfo apiInfo() { return new ApiInfoBuilder() .title("ERC-20 API") .description("ERC-20 token standard RESTful service") .build(); } } ================================================ FILE: src/main/java/io/blk/erc20/ContractService.java ================================================ package io.blk.erc20; import java.math.BigInteger; import java.util.Collections; import java.util.List; import java.util.concurrent.ExecutionException; import java.util.function.Function; import io.blk.erc20.generated.HumanStandardToken; import io.reactivex.annotations.Nullable; import lombok.Getter; import lombok.Setter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.web3j.protocol.core.methods.response.TransactionReceipt; import org.web3j.quorum.Quorum; import org.web3j.quorum.tx.ClientTransactionManager; import org.web3j.tx.TransactionManager; import static org.web3j.tx.Contract.GAS_LIMIT; import static org.web3j.tx.ManagedTransaction.GAS_PRICE; /** * Our smart contract service. */ @Service public class ContractService { private final Quorum quorum; private final NodeConfiguration nodeConfiguration; @Autowired public ContractService(Quorum quorum, NodeConfiguration nodeConfiguration) { this.quorum = quorum; this.nodeConfiguration = nodeConfiguration; } public NodeConfiguration getConfig() { return nodeConfiguration; } public String deploy( List privateFor, BigInteger initialAmount, String tokenName, BigInteger decimalUnits, String tokenSymbol) throws Exception { try { TransactionManager transactionManager = new ClientTransactionManager( quorum, nodeConfiguration.getFromAddress(), privateFor); HumanStandardToken humanStandardToken = HumanStandardToken.deploy( quorum, transactionManager, GAS_PRICE, GAS_LIMIT, initialAmount, tokenName, decimalUnits, tokenSymbol).send(); return humanStandardToken.getContractAddress(); } catch (InterruptedException | ExecutionException e) { throw new RuntimeException(e); } } public String name(String contractAddress) throws Exception { HumanStandardToken humanStandardToken = load(contractAddress); try { return humanStandardToken.name().send(); } catch (InterruptedException | ExecutionException e) { throw new RuntimeException(e); } } public TransactionResponse approve( List privateFor, String contractAddress, String spender, BigInteger value) throws Exception { HumanStandardToken humanStandardToken = load(contractAddress, privateFor); try { TransactionReceipt transactionReceipt = humanStandardToken .approve(spender, value).send(); return processApprovalEventResponse(humanStandardToken, transactionReceipt); } catch (InterruptedException | ExecutionException e) { throw new RuntimeException(e); } } public String totalSupply(String contractAddress) throws Exception { HumanStandardToken humanStandardToken = load(contractAddress); try { return humanStandardToken.totalSupply().send().toString(); } catch (InterruptedException | ExecutionException e) { throw new RuntimeException(e); } } public TransactionResponse transferFrom( List privateFor, String contractAddress, String from, String to, BigInteger value) throws Exception { HumanStandardToken humanStandardToken = load(contractAddress, privateFor); try { TransactionReceipt transactionReceipt = humanStandardToken .transferFrom(from, to, value).send(); return processTransferEventsResponse(humanStandardToken, transactionReceipt); } catch (InterruptedException | ExecutionException e) { throw new RuntimeException(e); } } public String decimals(String contractAddress) throws Exception { HumanStandardToken humanStandardToken = load(contractAddress); try { return humanStandardToken.decimals().send().toString(); } catch (InterruptedException | ExecutionException e) { throw new RuntimeException(e); } } public String version(String contractAddress) throws Exception { HumanStandardToken humanStandardToken = load(contractAddress); try { return humanStandardToken.version().send(); } catch (InterruptedException | ExecutionException e) { throw new RuntimeException(e); } } public String balanceOf(String contractAddress, String ownerAddress) throws Exception { HumanStandardToken humanStandardToken = load(contractAddress); try { return humanStandardToken.balanceOf(ownerAddress).send().toString(); } catch (InterruptedException | ExecutionException e) { throw new RuntimeException(e); } } public String symbol(String contractAddress) throws Exception { HumanStandardToken humanStandardToken = load(contractAddress); try { return humanStandardToken.symbol().send(); } catch (InterruptedException | ExecutionException e) { throw new RuntimeException(e); } } public TransactionResponse transfer( List privateFor, String contractAddress, String to, BigInteger value) throws Exception { HumanStandardToken humanStandardToken = load(contractAddress, privateFor); try { TransactionReceipt transactionReceipt = humanStandardToken .transfer(to, value).send(); return processTransferEventsResponse(humanStandardToken, transactionReceipt); } catch (InterruptedException | ExecutionException e) { throw new RuntimeException(e); } } public TransactionResponse approveAndCall( @Nullable List privateFor, String contractAddress, String spender, BigInteger value, String extraData) throws Exception { HumanStandardToken humanStandardToken = load(contractAddress, privateFor); try { TransactionReceipt transactionReceipt = humanStandardToken .approveAndCall( spender, value, extraData.getBytes()) .send(); return processApprovalEventResponse(humanStandardToken, transactionReceipt); } catch (InterruptedException | ExecutionException e) { throw new RuntimeException(e); } } public String allowance(String contractAddress, String ownerAddress, String spenderAddress) throws Exception { HumanStandardToken humanStandardToken = load(contractAddress); try { return humanStandardToken.allowance( ownerAddress, spenderAddress) .send().toString(); } catch (InterruptedException | ExecutionException e) { throw new RuntimeException(e); } } private HumanStandardToken load(String contractAddress, List privateFor) { TransactionManager transactionManager = new ClientTransactionManager( quorum, nodeConfiguration.getFromAddress(), privateFor); return HumanStandardToken.load( contractAddress, quorum, transactionManager, GAS_PRICE, GAS_LIMIT); } private HumanStandardToken load(String contractAddress) { TransactionManager transactionManager = new ClientTransactionManager( quorum, nodeConfiguration.getFromAddress(), Collections.emptyList()); return HumanStandardToken.load( contractAddress, quorum, transactionManager, GAS_PRICE, GAS_LIMIT); } private TransactionResponse processApprovalEventResponse( HumanStandardToken humanStandardToken, TransactionReceipt transactionReceipt) { return processEventResponse( humanStandardToken.getApprovalEvents(transactionReceipt), transactionReceipt, ApprovalEventResponse::new); } private TransactionResponse processTransferEventsResponse( HumanStandardToken humanStandardToken, TransactionReceipt transactionReceipt) { return processEventResponse( humanStandardToken.getTransferEvents(transactionReceipt), transactionReceipt, TransferEventResponse::new); } private TransactionResponse processEventResponse( List eventResponses, TransactionReceipt transactionReceipt, Function map) { if (!eventResponses.isEmpty()) { return new TransactionResponse<>( transactionReceipt.getTransactionHash(), map.apply(eventResponses.get(0))); } else { return new TransactionResponse<>( transactionReceipt.getTransactionHash()); } } @Getter @Setter public static class TransferEventResponse { private String from; private String to; private long value; public TransferEventResponse() { } public TransferEventResponse( HumanStandardToken.TransferEventResponse transferEventResponse) { this.from = transferEventResponse._from; this.to = transferEventResponse._to; this.value = transferEventResponse._value.longValueExact(); } public String getFrom() { return from; } public void setFrom(String from) { this.from = from; } public String getTo() { return to; } public void setTo(String to) { this.to = to; } public long getValue() { return value; } public void setValue(long value) { this.value = value; } } @Getter @Setter public static class ApprovalEventResponse { private String owner; private String spender; private long value; public ApprovalEventResponse() { } public ApprovalEventResponse( HumanStandardToken.ApprovalEventResponse approvalEventResponse) { this.owner = approvalEventResponse._owner; this.spender = approvalEventResponse._spender; this.value = approvalEventResponse._value.longValueExact(); } public String getOwner() { return owner; } public void setOwner(String owner) { this.owner = owner; } public String getSpender() { return spender; } public void setSpender(String spender) { this.spender = spender; } public long getValue() { return value; } public void setValue(long value) { this.value = value; } } } ================================================ FILE: src/main/java/io/blk/erc20/Controller.java ================================================ package io.blk.erc20; import java.math.BigInteger; import java.util.Arrays; import java.util.Collections; import java.util.List; import javax.servlet.http.HttpServletRequest; import io.reactivex.annotations.Nullable; import io.swagger.annotations.Api; import io.swagger.annotations.ApiImplicitParam; import io.swagger.annotations.ApiOperation; import lombok.Data; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; /** * Controller for our ERC-20 contract API. */ @Api("ERC-20 token standard API") @RestController public class Controller { private final ContractService ContractService; @Autowired public Controller(ContractService ContractService) { this.ContractService = ContractService; } @ApiOperation("Application configuration") @RequestMapping(value = "/config", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) NodeConfiguration config() { return ContractService.getConfig(); } @ApiOperation( value = "Deploy new ERC-20 token", notes = "Returns hex encoded contract address") @ApiImplicitParam(name = "privateFor", value = "Comma separated list of public keys of enclave nodes that transaction is " + "private for", paramType = "header", dataType = "string") @RequestMapping(value = "/deploy", method = RequestMethod.POST) String deploy( HttpServletRequest request, @RequestBody ContractSpecification contractSpecification) throws Exception { return ContractService.deploy( extractPrivateFor(request), contractSpecification.getInitialAmount(), contractSpecification.getTokenName(), contractSpecification.getDecimalUnits(), contractSpecification.getTokenSymbol()); } @ApiOperation("Get token name") @RequestMapping(value = "/{contractAddress}/name", method = RequestMethod.GET) String name(@PathVariable String contractAddress) throws Exception { return ContractService.name(contractAddress); } @ApiOperation( value = "Approve transfers by a specific address up to the provided total quantity", notes = "Returns hex encoded transaction hash, and Approval event if called") @ApiImplicitParam(name = "privateFor", value = "Comma separated list of public keys of enclave nodes that transaction is " + "private for", paramType = "header", dataType = "string") @RequestMapping(value = "/{contractAddress}/approve", method = RequestMethod.POST) TransactionResponse approve( HttpServletRequest request, @PathVariable String contractAddress, @RequestBody ApproveRequest approveRequest) throws Exception { return ContractService.approve( extractPrivateFor(request), contractAddress, approveRequest.getSpender(), approveRequest.getValue()); } @ApiOperation("Get total supply of tokens") @RequestMapping(value = "/{contractAddress}/totalSupply", method = RequestMethod.GET) String totalSupply(@PathVariable String contractAddress) throws Exception { return ContractService.totalSupply(contractAddress); } @ApiOperation( value = "Transfer tokens between addresses (must already be approved)", notes = "Returns hex encoded transaction hash, and Transfer event if called") @ApiImplicitParam(name = "privateFor", value = "Comma separated list of public keys of enclave nodes that transaction is " + "private for", paramType = "header", dataType = "string") @RequestMapping(value = "/{contractAddress}/transferFrom", method = RequestMethod.POST) TransactionResponse transferFrom( HttpServletRequest request, @PathVariable String contractAddress, @RequestBody TransferFromRequest transferFromRequest) throws Exception { return ContractService.transferFrom( extractPrivateFor(request), contractAddress, transferFromRequest.getFrom(), transferFromRequest.getTo(), transferFromRequest.getValue()); } @ApiOperation("Get decimal precision of tokens") @RequestMapping(value = "/{contractAddress}/decimals", method = RequestMethod.GET) String decimals(@PathVariable String contractAddress) throws Exception { return ContractService.decimals(contractAddress); } @ApiOperation("Get contract version") @RequestMapping(value = "/{contractAddress}/version", method = RequestMethod.GET) String version(@PathVariable String contractAddress) throws Exception { return ContractService.version(contractAddress); } @ApiOperation("Get token balance for address") @RequestMapping( value = "/{contractAddress}/balanceOf/{ownerAddress}", method = RequestMethod.GET) String balanceOf( @PathVariable String contractAddress, @PathVariable String ownerAddress) throws Exception { return ContractService.balanceOf(contractAddress, ownerAddress); } @ApiOperation("Get token symbol") @RequestMapping(value = "/{contractAddress}/symbol", method = RequestMethod.GET) String symbol(@PathVariable String contractAddress) throws Exception { return ContractService.symbol(contractAddress); } @ApiOperation( value = "Transfer tokens you own to another address", notes = "Returns hex encoded transaction hash, and Transfer event if called") @ApiImplicitParam(name = "privateFor", value = "Comma separated list of public keys of enclave nodes that transaction is " + "private for", paramType = "header", dataType = "string") @RequestMapping(value = "/{contractAddress}/transfer", method = RequestMethod.POST) TransactionResponse transfer( HttpServletRequest request, @PathVariable String contractAddress, @RequestBody TransferRequest transferRequest) throws Exception { return ContractService.transfer( extractPrivateFor(request), contractAddress, transferRequest.getTo(), transferRequest.getValue()); } @ApiOperation( value = "Approve transfers by a specific contract address up to the provided total " + "quantity, and notify that contract address of the approval", notes = "Returns hex encoded transaction hash, and Approval event if called") @ApiImplicitParam(name = "privateFor", value = "Comma separated list of public keys of enclave nodes that transaction is " + "private for", paramType = "header", dataType = "string") @RequestMapping(value = "/{contractAddress}/approveAndCall", method = RequestMethod.POST) TransactionResponse approveAndCall( HttpServletRequest request, @PathVariable String contractAddress, @RequestBody ApproveAndCallRequest approveAndCallRequest) throws Exception { return ContractService.approveAndCall( extractPrivateFor(request), contractAddress, approveAndCallRequest.getSpender(), approveAndCallRequest.getValue(), approveAndCallRequest.getExtraData()); } @ApiOperation("Get quantity of tokens you can transfer on another token holder's behalf") @RequestMapping(value = "/{contractAddress}/allowance", method = RequestMethod.GET) String allowance( @PathVariable String contractAddress, @RequestParam String ownerAddress, @RequestParam String spenderAddress) throws Exception { return ContractService.allowance( contractAddress, ownerAddress, spenderAddress); } private static @Nullable List extractPrivateFor(HttpServletRequest request) { String privateFor = request.getHeader("privateFor"); if (privateFor == null) { return null; } else { return Arrays.asList(privateFor.split(",")); } } @Data static class ContractSpecification { private BigInteger initialAmount; private String tokenName; private BigInteger decimalUnits; private String tokenSymbol; ContractSpecification() { } ContractSpecification(BigInteger initialAmount, String tokenName, BigInteger decimalUnits, String tokenSymbol) { this.initialAmount = initialAmount; this.tokenName = tokenName; this.decimalUnits = decimalUnits; this.tokenSymbol = tokenSymbol; } public BigInteger getDecimalUnits() { return decimalUnits; } public BigInteger getInitialAmount() { return initialAmount; } public String getTokenName() { return tokenName; } public String getTokenSymbol() { return tokenSymbol; } } @Data static class ApproveRequest { private String spender; private BigInteger value; ApproveRequest() {} ApproveRequest(String spender, BigInteger value) { this.spender = spender; this.value = value; } public String getSpender() { return spender; } public BigInteger getValue() { return value; } } @Data static class TransferFromRequest { private String from; private String to; private BigInteger value; TransferFromRequest() {} TransferFromRequest(String from, String to, BigInteger value) { this.from = from; this.to = to; this.value = value; } public String getFrom() { return from; } BigInteger getValue() { return value; } public String getTo() { return to; } } @Data static class TransferRequest { private String to; private BigInteger value; TransferRequest(String to, BigInteger value) { this.to = to; this.value = value; } TransferRequest() {} public String getTo() { return to; } public BigInteger getValue() { return value; } } @Data static class ApproveAndCallRequest { private String spender; private BigInteger value; private String extraData; ApproveAndCallRequest() {} ApproveAndCallRequest(String spender, BigInteger value, String extraData) { this.spender = spender; this.value = value; this.extraData = extraData; } String getSpender() { return spender; } BigInteger getValue() { return value; } String getExtraData() { return extraData; } } @Data static class AllowanceRequest { private String ownerAddress; private String spenderAddress; AllowanceRequest() {} AllowanceRequest(String ownerAddress, String spenderAddress) { this.ownerAddress = ownerAddress; this.spenderAddress = spenderAddress; } public String getOwnerAddress() { return ownerAddress; } public void setOwnerAddress(String ownerAddress) { this.ownerAddress = ownerAddress; } public String getSpenderAddress() { return spenderAddress; } public void setSpenderAddress(String spenderAddress) { this.spenderAddress = spenderAddress; } } } ================================================ FILE: src/main/java/io/blk/erc20/NodeConfiguration.java ================================================ package io.blk.erc20; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; /** * Node configuration bean. */ @Data @ConfigurationProperties("io.blk.erc20") @Component public class NodeConfiguration { private String nodeEndpoint = System.getProperty("nodeEndpoint"); private String fromAddress = System.getProperty("fromAddress"); public String getNodeEndpoint() { return nodeEndpoint; } public void setNodeEndpoint(String nodeEndpoint) { this.nodeEndpoint = nodeEndpoint; } public String getFromAddress() { return fromAddress; } public void setFromAddress(String fromAddress) { this.fromAddress = fromAddress; } } ================================================ FILE: src/main/java/io/blk/erc20/TransactionResponse.java ================================================ package io.blk.erc20; import lombok.Getter; import lombok.Setter; /** * TransactionResponse wrapper. */ @Getter @Setter public class TransactionResponse { private String transactionHash; private T event; TransactionResponse() { } public TransactionResponse(String transactionHash) { this(transactionHash, null); } public TransactionResponse(String transactionHash, T event) { this.transactionHash = transactionHash; this.event = event; } public String getTransactionHash() { return transactionHash; } public void setTransactionHash(String transactionHash) { this.transactionHash = transactionHash; } public T getEvent() { return event; } public void setEvent(T event) { this.event = event; } } ================================================ FILE: src/main/java/io/blk/erc20/generated/HumanStandardToken.java ================================================ package io.blk.erc20.generated; import io.reactivex.Flowable; import java.math.BigInteger; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import org.web3j.abi.EventEncoder; import org.web3j.abi.FunctionEncoder; import org.web3j.abi.TypeReference; import org.web3j.abi.datatypes.Address; import org.web3j.abi.datatypes.Event; import org.web3j.abi.datatypes.Function; import org.web3j.abi.datatypes.Type; import org.web3j.abi.datatypes.Utf8String; import org.web3j.abi.datatypes.generated.Uint256; import org.web3j.abi.datatypes.generated.Uint8; import org.web3j.crypto.Credentials; import org.web3j.protocol.Web3j; import org.web3j.protocol.core.DefaultBlockParameter; import org.web3j.protocol.core.RemoteCall; import org.web3j.protocol.core.methods.request.EthFilter; import org.web3j.protocol.core.methods.response.Log; import org.web3j.protocol.core.methods.response.TransactionReceipt; import org.web3j.tx.Contract; import org.web3j.tx.TransactionManager; import org.web3j.tx.gas.ContractGasProvider; /** *

Auto generated code. *

Do not modify! *

Please use the web3j command line tools, * or the org.web3j.codegen.SolidityFunctionWrapperGenerator in the * codegen module to update. * *

Generated with web3j version 4.3.0. */ public class HumanStandardToken extends Contract { private static final String BINARY = "60c0604052600460808190527f48302e310000000000000000000000000000000000000000000000000000000060a090815261003e916006919061016b565b5034801561004b57600080fd5b50604051610a4f380380610a4f8339810180604052608081101561006e57600080fd5b81516020830180519193928301929164010000000081111561008f57600080fd5b820160208101848111156100a257600080fd5b81516401000000008111828201871017156100bc57600080fd5b505060208201516040909201805191949293916401000000008111156100e157600080fd5b820160208101848111156100f457600080fd5b815164010000000081118282018710171561010e57600080fd5b5050336000908152600160209081526040822089905590889055865191945061013e93506003925086019061016b565b506004805460ff191660ff8416179055805161016190600590602084019061016b565b5050505050610206565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f106101ac57805160ff19168380011785556101d9565b828001600101855582156101d9579182015b828111156101d95782518255916020019190600101906101be565b506101e59291506101e9565b5090565b61020391905b808211156101e557600081556001016101ef565b90565b61083a806102156000396000f3fe608060405234801561001057600080fd5b50600436106100c6576000357c01000000000000000000000000000000000000000000000000000000009004806354fd4d501161008e57806354fd4d50146101f657806370a08231146101fe57806395d89b4114610224578063a9059cbb1461022c578063cae9ca5114610258578063dd62ed3e146102dd576100c6565b806306fdde03146100cb578063095ea7b31461014857806318160ddd1461018857806323b872dd146101a2578063313ce567146101d8575b600080fd5b6100d361030b565b6040805160208082528351818301528351919283929083019185019080838360005b8381101561010d5781810151838201526020016100f5565b50505050905090810190601f16801561013a5780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b6101746004803603604081101561015e57600080fd5b50600160a060020a038135169060200135610399565b604080519115158252519081900360200190f35b610190610400565b60408051918252519081900360200190f35b610174600480360360608110156101b857600080fd5b50600160a060020a03813581169160208101359091169060400135610406565b6101e06104f3565b6040805160ff9092168252519081900360200190f35b6100d36104fc565b6101906004803603602081101561021457600080fd5b5035600160a060020a0316610557565b6100d3610572565b6101746004803603604081101561024257600080fd5b50600160a060020a0381351690602001356105cd565b6101746004803603606081101561026e57600080fd5b600160a060020a038235169160208101359181019060608101604082013564010000000081111561029e57600080fd5b8201836020820111156102b057600080fd5b803590602001918460018302840111640100000000831117156102d257600080fd5b509092509050610666565b610190600480360360408110156102f357600080fd5b50600160a060020a03813581169160200135166107b5565b6003805460408051602060026001851615610100026000190190941693909304601f810184900484028201840190925281815292918301828280156103915780601f1061036657610100808354040283529160200191610391565b820191906000526020600020905b81548152906001019060200180831161037457829003601f168201915b505050505081565b336000818152600260209081526040808320600160a060020a038716808552908352818420869055815186815291519394909390927f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925928290030190a35060015b92915050565b60005481565b600160a060020a03831660009081526001602052604081205482118015906104515750600160a060020a03841660009081526002602090815260408083203384529091529020548211155b801561045d5750600082115b156104e857600160a060020a03808416600081815260016020908152604080832080548801905593881680835284832080548890039055600282528483203384528252918490208054879003905583518681529351929391927fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef9281900390910190a35060016104ec565b5060005b9392505050565b60045460ff1681565b6006805460408051602060026001851615610100026000190190941693909304601f810184900484028201840190925281815292918301828280156103915780601f1061036657610100808354040283529160200191610391565b600160a060020a031660009081526001602052604090205490565b6005805460408051602060026001851615610100026000190190941693909304601f810184900484028201840190925281815292918301828280156103915780601f1061036657610100808354040283529160200191610391565b3360009081526001602052604081205482118015906105ec5750600082115b1561065e5733600081815260016020908152604080832080548790039055600160a060020a03871680845292819020805487019055805186815290519293927fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef929181900390910190a35060016103fa565b5060006103fa565b336000818152600260209081526040808320600160a060020a038916808552908352818420889055815188815291519394909390927f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925928290030190a3600085600160a060020a031660405160200180806020018281038252602e8152602001806107e1602e91396040019150506040516020818303038152906040526040518082805190602001908083835b602083106107325780518252601f199092019160209182019101610713565b6001836020036101000a0380198251168184511680821785525050505050509050019150506000604051808303816000865af19150503d8060008114610794576040519150601f19603f3d011682016040523d82523d6000602084013e610799565b606091505b505090508015156107a957600080fd5b50600195945050505050565b600160a060020a0391821660009081526002602090815260408083209390941682529190915220549056fe72656365697665417070726f76616c28616464726573732c75696e743235362c616464726573732c627974657329a165627a7a72305820d15a070a95051e159632a5f42da17cdc0b4e940c8c7574a86370e2f434405abd0029"; public static final String FUNC_NAME = "name"; public static final String FUNC_APPROVE = "approve"; public static final String FUNC_TOTALSUPPLY = "totalSupply"; public static final String FUNC_TRANSFERFROM = "transferFrom"; public static final String FUNC_DECIMALS = "decimals"; public static final String FUNC_VERSION = "version"; public static final String FUNC_BALANCEOF = "balanceOf"; public static final String FUNC_SYMBOL = "symbol"; public static final String FUNC_TRANSFER = "transfer"; public static final String FUNC_APPROVEANDCALL = "approveAndCall"; public static final String FUNC_ALLOWANCE = "allowance"; public static final Event TRANSFER_EVENT = new Event("Transfer", Arrays.>asList(new TypeReference

(true) {}, new TypeReference
(true) {}, new TypeReference() {})); ; public static final Event APPROVAL_EVENT = new Event("Approval", Arrays.>asList(new TypeReference
(true) {}, new TypeReference
(true) {}, new TypeReference() {})); ; @Deprecated protected HumanStandardToken(String contractAddress, Web3j web3j, Credentials credentials, BigInteger gasPrice, BigInteger gasLimit) { super(BINARY, contractAddress, web3j, credentials, gasPrice, gasLimit); } protected HumanStandardToken(String contractAddress, Web3j web3j, Credentials credentials, ContractGasProvider contractGasProvider) { super(BINARY, contractAddress, web3j, credentials, contractGasProvider); } @Deprecated protected HumanStandardToken(String contractAddress, Web3j web3j, TransactionManager transactionManager, BigInteger gasPrice, BigInteger gasLimit) { super(BINARY, contractAddress, web3j, transactionManager, gasPrice, gasLimit); } protected HumanStandardToken(String contractAddress, Web3j web3j, TransactionManager transactionManager, ContractGasProvider contractGasProvider) { super(BINARY, contractAddress, web3j, transactionManager, contractGasProvider); } public RemoteCall name() { final Function function = new Function(FUNC_NAME, Arrays.asList(), Arrays.>asList(new TypeReference() {})); return executeRemoteCallSingleValueReturn(function, String.class); } public RemoteCall approve(String _spender, BigInteger _value) { final Function function = new Function( FUNC_APPROVE, Arrays.asList(new org.web3j.abi.datatypes.Address(_spender), new org.web3j.abi.datatypes.generated.Uint256(_value)), Collections.>emptyList()); return executeRemoteCallTransaction(function); } public RemoteCall totalSupply() { final Function function = new Function(FUNC_TOTALSUPPLY, Arrays.asList(), Arrays.>asList(new TypeReference() {})); return executeRemoteCallSingleValueReturn(function, BigInteger.class); } public RemoteCall transferFrom(String _from, String _to, BigInteger _value) { final Function function = new Function( FUNC_TRANSFERFROM, Arrays.asList(new org.web3j.abi.datatypes.Address(_from), new org.web3j.abi.datatypes.Address(_to), new org.web3j.abi.datatypes.generated.Uint256(_value)), Collections.>emptyList()); return executeRemoteCallTransaction(function); } public RemoteCall decimals() { final Function function = new Function(FUNC_DECIMALS, Arrays.asList(), Arrays.>asList(new TypeReference() {})); return executeRemoteCallSingleValueReturn(function, BigInteger.class); } public RemoteCall version() { final Function function = new Function(FUNC_VERSION, Arrays.asList(), Arrays.>asList(new TypeReference() {})); return executeRemoteCallSingleValueReturn(function, String.class); } public RemoteCall balanceOf(String _owner) { final Function function = new Function(FUNC_BALANCEOF, Arrays.asList(new org.web3j.abi.datatypes.Address(_owner)), Arrays.>asList(new TypeReference() {})); return executeRemoteCallSingleValueReturn(function, BigInteger.class); } public RemoteCall symbol() { final Function function = new Function(FUNC_SYMBOL, Arrays.asList(), Arrays.>asList(new TypeReference() {})); return executeRemoteCallSingleValueReturn(function, String.class); } public RemoteCall transfer(String _to, BigInteger _value) { final Function function = new Function( FUNC_TRANSFER, Arrays.asList(new org.web3j.abi.datatypes.Address(_to), new org.web3j.abi.datatypes.generated.Uint256(_value)), Collections.>emptyList()); return executeRemoteCallTransaction(function); } public RemoteCall approveAndCall(String _spender, BigInteger _value, byte[] _extraData) { final Function function = new Function( FUNC_APPROVEANDCALL, Arrays.asList(new org.web3j.abi.datatypes.Address(_spender), new org.web3j.abi.datatypes.generated.Uint256(_value), new org.web3j.abi.datatypes.DynamicBytes(_extraData)), Collections.>emptyList()); return executeRemoteCallTransaction(function); } public RemoteCall allowance(String _owner, String _spender) { final Function function = new Function(FUNC_ALLOWANCE, Arrays.asList(new org.web3j.abi.datatypes.Address(_owner), new org.web3j.abi.datatypes.Address(_spender)), Arrays.>asList(new TypeReference() {})); return executeRemoteCallSingleValueReturn(function, BigInteger.class); } public List getTransferEvents(TransactionReceipt transactionReceipt) { List valueList = extractEventParametersWithLog(TRANSFER_EVENT, transactionReceipt); ArrayList responses = new ArrayList(valueList.size()); for (Contract.EventValuesWithLog eventValues : valueList) { TransferEventResponse typedResponse = new TransferEventResponse(); typedResponse.log = eventValues.getLog(); typedResponse._from = (String) eventValues.getIndexedValues().get(0).getValue(); typedResponse._to = (String) eventValues.getIndexedValues().get(1).getValue(); typedResponse._value = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); responses.add(typedResponse); } return responses; } public Flowable transferEventFlowable(EthFilter filter) { return web3j.ethLogFlowable(filter).map(new io.reactivex.functions.Function() { @Override public TransferEventResponse apply(Log log) { Contract.EventValuesWithLog eventValues = extractEventParametersWithLog(TRANSFER_EVENT, log); TransferEventResponse typedResponse = new TransferEventResponse(); typedResponse.log = log; typedResponse._from = (String) eventValues.getIndexedValues().get(0).getValue(); typedResponse._to = (String) eventValues.getIndexedValues().get(1).getValue(); typedResponse._value = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); return typedResponse; } }); } public Flowable transferEventFlowable(DefaultBlockParameter startBlock, DefaultBlockParameter endBlock) { EthFilter filter = new EthFilter(startBlock, endBlock, getContractAddress()); filter.addSingleTopic(EventEncoder.encode(TRANSFER_EVENT)); return transferEventFlowable(filter); } public List getApprovalEvents(TransactionReceipt transactionReceipt) { List valueList = extractEventParametersWithLog(APPROVAL_EVENT, transactionReceipt); ArrayList responses = new ArrayList(valueList.size()); for (Contract.EventValuesWithLog eventValues : valueList) { ApprovalEventResponse typedResponse = new ApprovalEventResponse(); typedResponse.log = eventValues.getLog(); typedResponse._owner = (String) eventValues.getIndexedValues().get(0).getValue(); typedResponse._spender = (String) eventValues.getIndexedValues().get(1).getValue(); typedResponse._value = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); responses.add(typedResponse); } return responses; } public Flowable approvalEventFlowable(EthFilter filter) { return web3j.ethLogFlowable(filter).map(new io.reactivex.functions.Function() { @Override public ApprovalEventResponse apply(Log log) { Contract.EventValuesWithLog eventValues = extractEventParametersWithLog(APPROVAL_EVENT, log); ApprovalEventResponse typedResponse = new ApprovalEventResponse(); typedResponse.log = log; typedResponse._owner = (String) eventValues.getIndexedValues().get(0).getValue(); typedResponse._spender = (String) eventValues.getIndexedValues().get(1).getValue(); typedResponse._value = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); return typedResponse; } }); } public Flowable approvalEventFlowable(DefaultBlockParameter startBlock, DefaultBlockParameter endBlock) { EthFilter filter = new EthFilter(startBlock, endBlock, getContractAddress()); filter.addSingleTopic(EventEncoder.encode(APPROVAL_EVENT)); return approvalEventFlowable(filter); } @Deprecated public static HumanStandardToken load(String contractAddress, Web3j web3j, Credentials credentials, BigInteger gasPrice, BigInteger gasLimit) { return new HumanStandardToken(contractAddress, web3j, credentials, gasPrice, gasLimit); } @Deprecated public static HumanStandardToken load(String contractAddress, Web3j web3j, TransactionManager transactionManager, BigInteger gasPrice, BigInteger gasLimit) { return new HumanStandardToken(contractAddress, web3j, transactionManager, gasPrice, gasLimit); } public static HumanStandardToken load(String contractAddress, Web3j web3j, Credentials credentials, ContractGasProvider contractGasProvider) { return new HumanStandardToken(contractAddress, web3j, credentials, contractGasProvider); } public static HumanStandardToken load(String contractAddress, Web3j web3j, TransactionManager transactionManager, ContractGasProvider contractGasProvider) { return new HumanStandardToken(contractAddress, web3j, transactionManager, contractGasProvider); } public static RemoteCall deploy(Web3j web3j, Credentials credentials, ContractGasProvider contractGasProvider, BigInteger _initialAmount, String _tokenName, BigInteger _decimalUnits, String _tokenSymbol) { String encodedConstructor = FunctionEncoder.encodeConstructor(Arrays.asList(new org.web3j.abi.datatypes.generated.Uint256(_initialAmount), new org.web3j.abi.datatypes.Utf8String(_tokenName), new org.web3j.abi.datatypes.generated.Uint8(_decimalUnits), new org.web3j.abi.datatypes.Utf8String(_tokenSymbol))); return deployRemoteCall(HumanStandardToken.class, web3j, credentials, contractGasProvider, BINARY, encodedConstructor); } public static RemoteCall deploy(Web3j web3j, TransactionManager transactionManager, ContractGasProvider contractGasProvider, BigInteger _initialAmount, String _tokenName, BigInteger _decimalUnits, String _tokenSymbol) { String encodedConstructor = FunctionEncoder.encodeConstructor(Arrays.asList(new org.web3j.abi.datatypes.generated.Uint256(_initialAmount), new org.web3j.abi.datatypes.Utf8String(_tokenName), new org.web3j.abi.datatypes.generated.Uint8(_decimalUnits), new org.web3j.abi.datatypes.Utf8String(_tokenSymbol))); return deployRemoteCall(HumanStandardToken.class, web3j, transactionManager, contractGasProvider, BINARY, encodedConstructor); } @Deprecated public static RemoteCall deploy(Web3j web3j, Credentials credentials, BigInteger gasPrice, BigInteger gasLimit, BigInteger _initialAmount, String _tokenName, BigInteger _decimalUnits, String _tokenSymbol) { String encodedConstructor = FunctionEncoder.encodeConstructor(Arrays.asList(new org.web3j.abi.datatypes.generated.Uint256(_initialAmount), new org.web3j.abi.datatypes.Utf8String(_tokenName), new org.web3j.abi.datatypes.generated.Uint8(_decimalUnits), new org.web3j.abi.datatypes.Utf8String(_tokenSymbol))); return deployRemoteCall(HumanStandardToken.class, web3j, credentials, gasPrice, gasLimit, BINARY, encodedConstructor); } @Deprecated public static RemoteCall deploy(Web3j web3j, TransactionManager transactionManager, BigInteger gasPrice, BigInteger gasLimit, BigInteger _initialAmount, String _tokenName, BigInteger _decimalUnits, String _tokenSymbol) { String encodedConstructor = FunctionEncoder.encodeConstructor(Arrays.asList(new org.web3j.abi.datatypes.generated.Uint256(_initialAmount), new org.web3j.abi.datatypes.Utf8String(_tokenName), new org.web3j.abi.datatypes.generated.Uint8(_decimalUnits), new org.web3j.abi.datatypes.Utf8String(_tokenSymbol))); return deployRemoteCall(HumanStandardToken.class, web3j, transactionManager, gasPrice, gasLimit, BINARY, encodedConstructor); } public static class TransferEventResponse { public Log log; public String _from; public String _to; public BigInteger _value; } public static class ApprovalEventResponse { public Log log; public String _owner; public String _spender; public BigInteger _value; } } ================================================ FILE: src/main/resources/config/application.yml ================================================ # Port to run on server: port: ${port:8081} # Our log file path and name logging: file: logs/erc20-rest-service.log # Endpoint of an Ethereum or Quorum node we wish to use. # To use IPC simply provide a file path to the socket, such as /path/to/geth.ipc nodeEndpoint: http://localhost:22000 # The Ethereum or Quorum address we wish to use when transacting. # Note - this address must be already unlocked in the client fromAddress: "0xed9d02e382b34818e88b88a309c7fe71e65f419d" ================================================ FILE: src/main/resources/logback.xml ================================================ %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n ================================================ FILE: src/main/resources/solidity/contract/HumanStandardToken.sol ================================================ /* This Token Contract implements the standard token functionality (https://github.com/ethereum/EIPs/issues/20) as well as the following OPTIONAL extras intended for use by humans. In other words. This is intended for deployment in something like a Token Factory or Mist wallet, and then used by humans. Imagine coins, currencies, shares, voting weight, etc. Machine-based, rapid creation of many tokens would not necessarily need these extra features or will be minted in other manners. 1) Initial Finite Supply (upon creation one specifies how much is minted). 2) In the absence of a token registry: Optional Decimal, Symbol & Name. 3) Optional approveAndCall() functionality to notify a contract if an approval() has occurred. .*/ import "./StandardToken.sol"; pragma solidity ^0.5.2; contract HumanStandardToken is StandardToken { function () external { //if ether is sent to this address, send it back. revert(); } /* Public variables of the token */ /* NOTE: The following variables are OPTIONAL vanities. One does not have to include them. They allow one to customise the token contract & in no way influences the core functionality. Some wallets/interfaces might not even bother to look at this information. */ string public name; //fancy name: eg Simon Bucks uint8 public decimals; //How many decimals to show. ie. There could 1000 base units with 3 decimals. Meaning 0.980 SBX = 980 base units. It's like comparing 1 wei to 1 ether. string public symbol; //An identifier: eg SBX string public version = 'H0.1'; //human 0.1 standard. Just an arbitrary versioning scheme. constructor( uint256 _initialAmount, string memory _tokenName, uint8 _decimalUnits, string memory _tokenSymbol ) public { balances[msg.sender] = _initialAmount; // Give the creator all initial tokens totalSupply = _initialAmount; // Update total supply name = _tokenName; // Set the name for display purposes decimals = _decimalUnits; // Amount of decimals for display purposes symbol = _tokenSymbol; // Set the symbol for display purposes } /* Approves and then calls the receiving contract */ function approveAndCall(address _spender, uint256 _value, bytes calldata _extraData) external returns (bool success) { allowed[msg.sender][_spender] = _value; emit Approval(msg.sender, _spender, _value); //call the receiveApproval function on the contract you want to be notified. This crafts the function signature manually so one doesn't have to include a contract in here just for this. //receiveApproval(address _from, uint256 _value, address _tokenContract, bytes _extraData) //it is assumed that when does this that the call *should* succeed, otherwise one would use vanilla approve instead. (bool success, ) = _spender.call(abi.encode("receiveApproval(address,uint256,address,bytes)")); if(!success) { revert(); } return true; } } ================================================ FILE: src/main/resources/solidity/contract/HumanStandardTokenFactory.sol ================================================ import "./HumanStandardToken.sol"; pragma solidity ^0.5.2; contract HumanStandardTokenFactory { mapping(address => address[]) public created; mapping(address => bool) public isHumanToken; //verify without having to do a bytecode check. bytes public humanStandardByteCode; function HumanStandardTokenFactory() { //upon creation of the factory, deploy a HumanStandardToken (parameters are meaningless) and store the bytecode provably. address verifiedToken = createHumanStandardToken(10000, "Verify Token", 3, "VTX"); humanStandardByteCode = codeAt(verifiedToken); } //verifies if a contract that has been deployed is a Human Standard Token. //NOTE: This is a very expensive function, and should only be used in an eth_call. ~800k gas function verifyHumanStandardToken(address _tokenContract) returns (bool) { bytes memory fetchedTokenByteCode = codeAt(_tokenContract); if (fetchedTokenByteCode.length != humanStandardByteCode.length) { return false; //clear mismatch } //starting iterating through it if lengths match for (uint i = 0; i < fetchedTokenByteCode.length; i ++) { if (fetchedTokenByteCode[i] != humanStandardByteCode[i]) { return false; } } return true; } //for now, keeping this internal. Ideally there should also be a live version of this that any contract can use, lib-style. //retrieves the bytecode at a specific address. function codeAt(address _addr) internal returns (bytes o_code) { assembly { // retrieve the size of the code, this needs assembly let size := extcodesize(_addr) // allocate output byte array - this could also be done without assembly // by using o_code = new bytes(size) o_code := mload(0x40) // new "memory end" including padding mstore(0x40, add(o_code, and(add(add(size, 0x20), 0x1f), not(0x1f)))) // store length in memory mstore(o_code, size) // actually retrieve the code, this needs assembly extcodecopy(_addr, add(o_code, 0x20), 0, size) } } function createHumanStandardToken(uint256 _initialAmount, string _name, uint8 _decimals, string _symbol) returns (address) { HumanStandardToken newToken = (new HumanStandardToken(_initialAmount, _name, _decimals, _symbol)); created[msg.sender].push(address(newToken)); isHumanToken[address(newToken)] = true; newToken.transfer(msg.sender, _initialAmount); //the factory will own the created tokens. You must transfer them. return address(newToken); } } ================================================ FILE: src/main/resources/solidity/contract/StandardToken.sol ================================================ /* You should inherit from StandardToken or, for a token like you would want to deploy in something like Mist, see HumanStandardToken.sol. (This implements ONLY the standard functions and NOTHING else. If you deploy this, you won't have anything useful.) Implements ERC 20 Token standard: https://github.com/ethereum/EIPs/issues/20 .*/ pragma solidity ^0.5.2; import "./Token.sol"; contract StandardToken is Token { function transfer(address _to, uint256 _value) public returns (bool success) { //Default assumes totalSupply can't be over max (2^256 - 1). //If your token leaves out totalSupply and can issue more tokens as time goes on, you need to check if it doesn't wrap. //Replace the if with this one instead. //if (balances[msg.sender] >= _value && balances[_to] + _value > balances[_to]) { if (balances[msg.sender] >= _value && _value > 0) { balances[msg.sender] -= _value; balances[_to] += _value; emit Transfer(msg.sender, _to, _value); return true; } else { return false; } } function transferFrom(address _from, address _to, uint256 _value) public returns (bool success) { //same as above. Replace this line with the following if you want to protect against wrapping uints. //if (balances[_from] >= _value && allowed[_from][msg.sender] >= _value && balances[_to] + _value > balances[_to]) { if (balances[_from] >= _value && allowed[_from][msg.sender] >= _value && _value > 0) { balances[_to] += _value; balances[_from] -= _value; allowed[_from][msg.sender] -= _value; emit Transfer(_from, _to, _value); return true; } else { return false; } } function balanceOf(address _owner) public view returns (uint256 balance) { return balances[_owner]; } function approve(address _spender, uint256 _value) public returns (bool success) { allowed[msg.sender][_spender] = _value; emit Approval(msg.sender, _spender, _value); return true; } function allowance(address _owner, address _spender) public view returns (uint256 remaining) { return allowed[_owner][_spender]; } mapping (address => uint256) balances; mapping (address => mapping (address => uint256)) allowed; } ================================================ FILE: src/main/resources/solidity/contract/Token.sol ================================================ // Abstract contract for the full ERC 20 Token standard // https://github.com/ethereum/EIPs/issues/20 pragma solidity ^0.5.2; contract Token { /* This is a slight change to the ERC20 base standard. function totalSupply() constant returns (uint256 supply); is replaced with: uint256 public totalSupply; This automatically creates a getter function for the totalSupply. This is moved to the base contract since public getter functions are not currently recognised as an implementation of the matching abstract function by the compiler. */ /// total amount of tokens uint256 public totalSupply; /// @param _owner The address from which the balance will be retrieved /// @return The balance function balanceOf(address _owner) public view returns (uint256 balance); /// @notice send `_value` token to `_to` from `msg.sender` /// @param _to The address of the recipient /// @param _value The amount of token to be transferred /// @return Whether the transfer was successful or not function transfer(address _to, uint256 _value) public returns (bool success); /// @notice send `_value` token to `_to` from `_from` on the condition it is approved by `_from` /// @param _from The address of the sender /// @param _to The address of the recipient /// @param _value The amount of token to be transferred /// @return Whether the transfer was successful or not function transferFrom(address _from, address _to, uint256 _value) public returns (bool success); /// @notice `msg.sender` approves `_spender` to spend `_value` tokens /// @param _spender The address of the account able to transfer the tokens /// @param _value The amount of tokens to be approved for transfer /// @return Whether the approval was successful or not function approve(address _spender, uint256 _value) public returns (bool success); /// @param _owner The address of the account owning tokens /// @param _spender The address of the account able to transfer the tokens /// @return Amount of remaining tokens allowed to spent function allowance(address _owner, address _spender) public view returns (uint256 remaining); event Transfer(address indexed _from, address indexed _to, uint256 _value); event Approval(address indexed _owner, address indexed _spender, uint256 _value); } ================================================ FILE: src/main/resources/solidity/contract/build/HumanStandardToken.abi ================================================ [{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_spender","type":"address"},{"name":"_value","type":"uint256"}],"name":"approve","outputs":[{"name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_from","type":"address"},{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transferFrom","outputs":[{"name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"decimals","outputs":[{"name":"","type":"uint8"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"version","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_owner","type":"address"}],"name":"balanceOf","outputs":[{"name":"balance","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transfer","outputs":[{"name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_spender","type":"address"},{"name":"_value","type":"uint256"},{"name":"_extraData","type":"bytes"}],"name":"approveAndCall","outputs":[{"name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"_owner","type":"address"},{"name":"_spender","type":"address"}],"name":"allowance","outputs":[{"name":"remaining","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"inputs":[{"name":"_initialAmount","type":"uint256"},{"name":"_tokenName","type":"string"},{"name":"_decimalUnits","type":"uint8"},{"name":"_tokenSymbol","type":"string"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"payable":false,"stateMutability":"nonpayable","type":"fallback"},{"anonymous":false,"inputs":[{"indexed":true,"name":"_from","type":"address"},{"indexed":true,"name":"_to","type":"address"},{"indexed":false,"name":"_value","type":"uint256"}],"name":"Transfer","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"_owner","type":"address"},{"indexed":true,"name":"_spender","type":"address"},{"indexed":false,"name":"_value","type":"uint256"}],"name":"Approval","type":"event"}] ================================================ FILE: src/main/resources/solidity/contract/build/StandardToken.abi ================================================ [{"constant":false,"inputs":[{"name":"_spender","type":"address"},{"name":"_value","type":"uint256"}],"name":"approve","outputs":[{"name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_from","type":"address"},{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transferFrom","outputs":[{"name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"_owner","type":"address"}],"name":"balanceOf","outputs":[{"name":"balance","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transfer","outputs":[{"name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"_owner","type":"address"},{"name":"_spender","type":"address"}],"name":"allowance","outputs":[{"name":"remaining","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"anonymous":false,"inputs":[{"indexed":true,"name":"_from","type":"address"},{"indexed":true,"name":"_to","type":"address"},{"indexed":false,"name":"_value","type":"uint256"}],"name":"Transfer","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"_owner","type":"address"},{"indexed":true,"name":"_spender","type":"address"},{"indexed":false,"name":"_value","type":"uint256"}],"name":"Approval","type":"event"}] ================================================ FILE: src/main/resources/solidity/contract/build/Token.abi ================================================ [{"constant":false,"inputs":[{"name":"_spender","type":"address"},{"name":"_value","type":"uint256"}],"name":"approve","outputs":[{"name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_from","type":"address"},{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transferFrom","outputs":[{"name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"_owner","type":"address"}],"name":"balanceOf","outputs":[{"name":"balance","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transfer","outputs":[{"name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"_owner","type":"address"},{"name":"_spender","type":"address"}],"name":"allowance","outputs":[{"name":"remaining","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"anonymous":false,"inputs":[{"indexed":true,"name":"_from","type":"address"},{"indexed":true,"name":"_to","type":"address"},{"indexed":false,"name":"_value","type":"uint256"}],"name":"Transfer","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"_owner","type":"address"},{"indexed":true,"name":"_spender","type":"address"},{"indexed":false,"name":"_value","type":"uint256"}],"name":"Approval","type":"event"}] ================================================ FILE: src/test/java/io/blk/erc20/ControllerIT.java ================================================ package io.blk.erc20; import java.math.BigInteger; import java.util.Arrays; import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import static junit.framework.TestCase.assertNull; import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; @RunWith(SpringJUnit4ClassRunner.class) @SpringBootTest(webEnvironment= SpringBootTest.WebEnvironment.RANDOM_PORT) public class ControllerIT { // key2 private static final String OTHER_ACCOUNT = "ca843569e3427144cead5e4d5999a3d0ccf92b8e"; // Transaction manager 2 private static final String PRIVATE_FOR = "QfeDAys9MPDs2XHExtc84jKGHxZg/aj52DTh0vtA3Xc="; @Autowired private NodeConfiguration nodeConfiguration; @Autowired private TestRestTemplate restTemplate; @Test public void testConfig() { ResponseEntity responseEntity = this.restTemplate.getForEntity("/config", NodeConfiguration.class); verifyHttpStatus(responseEntity); assertNotNull(responseEntity.getBody()); } @Ignore @Test public void testLifeCycle() { Controller.ContractSpecification contractSpecification = new Controller.ContractSpecification( BigInteger.valueOf(1000000), "Quorum Token", BigInteger.valueOf(25), "QT"); String contractAddress = deploy(contractSpecification); verifyName(contractAddress, contractSpecification.getTokenName()); verifySymbol(contractAddress, contractSpecification.getTokenSymbol()); verifyDecimals(contractAddress, contractSpecification.getDecimalUnits()); verifyVersion(contractAddress, "H0.1"); verifyTotalSupply(contractAddress, contractSpecification.getInitialAmount()); verifyBalanceOf(contractAddress, nodeConfiguration.getFromAddress(), contractSpecification.getInitialAmount()); Controller.ApproveRequest approveRequest = new Controller.ApproveRequest( OTHER_ACCOUNT, BigInteger.valueOf(10000)); verifyApproveTx(contractAddress, approveRequest); verifyAllowance( contractAddress, nodeConfiguration.getFromAddress(), OTHER_ACCOUNT, approveRequest.getValue()); Controller.TransferRequest transferRequest = new Controller.TransferRequest( OTHER_ACCOUNT, BigInteger.valueOf(10000)); verifyTransferTx(contractAddress, transferRequest); verifyBalanceOf( contractAddress, transferRequest.getTo(), transferRequest.getValue()); verifyBalanceOf( contractAddress, nodeConfiguration.getFromAddress(), contractSpecification.getInitialAmount().subtract(transferRequest.getValue())); // Needs to be performed by another account, hence this will fail Controller.TransferFromRequest transferFromRequest = new Controller.TransferFromRequest( nodeConfiguration.getFromAddress(), OTHER_ACCOUNT, BigInteger.valueOf(1000)); verifyTransferFromTxFailure(contractAddress, transferFromRequest); // Therefore our balance remains the same verifyBalanceOf( contractAddress, transferFromRequest.getFrom(), contractSpecification.getInitialAmount().subtract(transferRequest.getValue())); } private String deploy( Controller.ContractSpecification contractSpecification) { ResponseEntity responseEntity = this.restTemplate.postForEntity( "/deploy", buildEntity(contractSpecification), String.class); verifyHttpStatus(responseEntity); String contractAddress = responseEntity.getBody(); assertFalse(contractAddress.isEmpty()); return contractAddress; } private void verifyName(String contractAddress, String name) { ResponseEntity responseEntity = this.restTemplate.getForEntity( "/" + contractAddress + "" + "/name", String.class); verifyHttpStatus(responseEntity); assertThat(responseEntity.getBody(), is(name)); } private void verifyTotalSupply(String contractAddress, BigInteger totalSupply) { ResponseEntity responseEntity = this.restTemplate.getForEntity( "/" + contractAddress + "" + "/totalSupply", BigInteger.class); verifyHttpStatus(responseEntity); assertThat(responseEntity.getBody(), is(totalSupply)); } private void verifyDecimals(String contractAddress, BigInteger decimals) { ResponseEntity responseEntity = this.restTemplate.getForEntity( "/" + contractAddress + "" + "/decimals", BigInteger.class); verifyHttpStatus(responseEntity); assertThat(responseEntity.getBody(), is(decimals)); } private void verifyVersion(String contractAddress, String version) { ResponseEntity responseEntity = this.restTemplate.getForEntity( "/" + contractAddress + "" + "/version", String.class); verifyHttpStatus(responseEntity); assertThat(responseEntity.getBody(), is(version)); } private void verifyBalanceOf(String contractAddress, String ownerAddress, BigInteger balance) { ResponseEntity responseEntity = this.restTemplate.getForEntity( "/" + contractAddress + "" + "/balanceOf/" + ownerAddress, BigInteger.class); verifyHttpStatus(responseEntity); assertThat(responseEntity.getBody(), is(balance)); } private void verifySymbol(String contractAddress, String symbol) { ResponseEntity responseEntity = this.restTemplate.getForEntity( "/" + contractAddress + "" + "/symbol", String.class); verifyHttpStatus(responseEntity); assertThat(responseEntity.getBody(), is(symbol)); } private void verifyAllowance( String contractAddress, String ownerAddress, String spenderAddress, BigInteger expected) { ResponseEntity responseEntity = this.restTemplate.getForEntity( "/" + contractAddress + "/allowance?" + "ownerAddress={ownerAddress}" + "&spenderAddress={spenderAddress}", BigInteger.class, ownerAddress, spenderAddress); verifyHttpStatus(responseEntity); assertThat(responseEntity.getBody(), is(expected)); } private void verifyTransferFromTxFailure( String contractAddress, Controller.TransferFromRequest transferFromRequest) { ResponseEntity responseEntity = this.restTemplate.postForEntity( "/" + contractAddress + "/transferFrom", buildEntity(transferFromRequest), TransactionResponse.class); verifyPostResponseFailure(responseEntity); } private void verifyApproveTx( String contractAddress, Controller.ApproveRequest approveRequest) { ResponseEntity responseEntity = this.restTemplate.postForEntity( "/" + contractAddress + "/approve", buildEntity(approveRequest), TransactionResponse.class); verifyPostResponse(responseEntity); } private void verifyTransferTx( String contractAddress, Controller.TransferRequest transferRequest) { ResponseEntity responseEntity = this.restTemplate.postForEntity( "/" + contractAddress + "/transfer", buildEntity(transferRequest), TransactionResponse.class); verifyPostResponse(responseEntity); } private HttpEntity buildEntity(T body) { HttpHeaders headers = new HttpHeaders(); headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON)); headers.setContentType(MediaType.APPLICATION_JSON); headers.add("privateFor", PRIVATE_FOR); return new HttpEntity<>(body, headers); } private void verifyHttpStatus(ResponseEntity responseEntity) { assertThat(responseEntity.getStatusCode(), is(HttpStatus.OK)); } private void verifyPostResponse(ResponseEntity responseEntity) { verifyPostResponseBody(responseEntity); assertNotNull(responseEntity.getBody().getEvent()); } private void verifyPostResponseFailure(ResponseEntity responseEntity) { assertNull(responseEntity.getBody().getEvent()); } private void verifyPostResponseBody(ResponseEntity responseEntity) { verifyHttpStatus(responseEntity); TransactionResponse body = responseEntity.getBody(); assertNotNull(body); String transactionHash = body.getTransactionHash(); assertTrue(transactionHash.startsWith("0x")); assertThat(transactionHash.length(), is(66)); } } ================================================ FILE: src/test/resources/logback-test.xml ================================================