├── .gitignore ├── README.md ├── build.gradle ├── docker └── Dockerfile ├── generate.sh ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── images └── full-swagger-ui.png ├── 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 │ ├── HumanStandardToken.bin │ ├── StandardToken.abi │ ├── StandardToken.bin │ ├── Token.abi │ └── Token.bin └── test ├── java └── io │ └── blk │ └── erc20 │ └── ControllerIT.java └── resources └── logback-test.xml /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Java template 3 | *.class 4 | 5 | # Mobile Tools for Java (J2ME) 6 | .mtj.tmp/ 7 | 8 | # Package Files # 9 | *.jar 10 | *.war 11 | *.ear 12 | 13 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 14 | hs_err_pid* 15 | ### Gradle template 16 | .gradle 17 | /build 18 | src/main/resources/gradle.properties 19 | 20 | # Ignore Gradle GUI config 21 | gradle-app.setting 22 | 23 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 24 | !gradle-wrapper.jar 25 | 26 | # Cache of project 27 | .gradletasknamecache 28 | 29 | # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 30 | # gradle/wrapper/gradle-wrapper.properties 31 | 32 | .idea 33 | *.iml 34 | 35 | # OS X 36 | .DS_Store 37 | 38 | # log files 39 | *.log 40 | logs 41 | /out 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ERC-20 RESTful service 2 | 3 | This application provides a RESTful service for creating and managing 4 | [ERC-20 tokens](https://github.com/ethereum/EIPs/issues/20). 5 | It has been built using [Spring Boot](https://projects.spring.io/spring-boot/), and 6 | [web3j](https://web3j.io). 7 | 8 | It works with both [Geth](https://github.com/ethereum/go-ethereum), 9 | [Parity](https://github.com/paritytech/parity), and 10 | [Quorum](https://github.com/jpmorganchase/quorum). 11 | 12 | For Quorum, the RESTful semantics are identical, with the exception that if you wish to create 13 | a private transaction, you populate a HTTP header name *privateFor* with a comma-separated 14 | list of public keys 15 | 16 | 17 | ## Build 18 | 19 | To build a runnable jar file: 20 | 21 | ```bash 22 | ./gradlew clean build 23 | ``` 24 | 25 | ## Run 26 | 27 | Using Java 1.8+: 28 | 29 | ```bash 30 | java -jar build/libs/azure-demo-0.1.jar 31 | ``` 32 | 33 | By default the application will log to a file named erc20-web3j.log. 34 | 35 | 36 | ## Configuration 37 | 38 | The following default properties are used in the application: 39 | 40 | ```properties 41 | # Port for service to bind to 42 | port=8080 43 | # Log file path and name 44 | logging.file=logs/erc20-rest-service.log 45 | 46 | # Endpoint of an Ethereum or Quorum node we wish to use. 47 | # To use IPC simply provide a file path to the socket, such as /path/to/geth.ipc 48 | nodeEndpoint=http://localhost:22000 49 | # The Ethereum or Quorum address we wish to use when transacting. 50 | # Note - this address must be already unlocked in the client 51 | fromAddress=0xed9d02e382b34818e88b88a309c7fe71e65f419d 52 | ``` 53 | 54 | You can override any of these properties by creating a file name 55 | *application.properties* in the root directory of your application, or in 56 | *config/application.properties* relative to your root. If you'd rather use yaml, 57 | simply change the filename to *application.yml*. 58 | 59 | 60 | ## Usage 61 | 62 | All available application endpoints are documented using [Swagger](http://swagger.io/). 63 | 64 | You can view the Swagger UI at http://localhost:8080/swagger-ui.html. From here you 65 | can perform all POST and GET requests easily to facilitate deployment of, transacting 66 | with, and querying state of ERC-20 tokens. 67 | 68 | ![alt text](https://github.com/blk-io/erc20-rest-service/raw/master/images/full-swagger-ui.png "Swagger UI screen capture") 69 | 70 | 71 | ## Docker 72 | 73 | We can use [Docker](https://www.docker.com/) to easily spin up a arbritrary instance 74 | of our service connecting to an already running Ethereum or Quorum network. 75 | 76 | All you need to do is build the Dockerfile: 77 | 78 | ```docker 79 | docker build -f docker/Dockerfile -t blk-io/erc20-service . 80 | ``` 81 | 82 | Then either run it with default configuration: 83 | ```docker 84 | docker run -p 8080:8080 -v "$PWD/logs":/logs blk-io/erc20-service 85 | ``` 86 | 87 | Or with a custom configuration: 88 | 89 | ```docker 90 | export PORT=8081 91 | docker run -p ${PORT}:${PORT} -v "$PWD/logs":/logs \ 92 | -e ENDPOINT="http://localhost:22001" \ 93 | -e FROMADDR="0xca843569e3427144cead5e4d5999a3d0ccf92b8e" \ 94 | -e PORT="$PORT" \ 95 | blk-io/erc20-service 96 | ``` 97 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | id 'idea' 4 | id 'eclipse' 5 | id 'application' 6 | id 'org.springframework.boot' version '2.1.7.RELEASE' 7 | } 8 | 9 | mainClassName = 'io.blk.erc20.Application' 10 | 11 | group 'io.blk' 12 | version '0.1.0' 13 | 14 | sourceCompatibility = 1.8 15 | 16 | repositories { 17 | mavenCentral() 18 | } 19 | 20 | dependencies { 21 | compile 'org.springframework.boot:spring-boot-starter-web:2.1.7.RELEASE', 22 | 'io.springfox:springfox-swagger2:2.7.0', 23 | 'io.springfox:springfox-swagger-ui:2.7.0', 24 | 'org.projectlombok:lombok:1.16.16', 25 | 'org.web3j:quorum:4.+', 26 | 'org.web3j:core:4.+', 27 | 'io.reactivex.rxjava2:rxjava:2.2.0', 28 | 'com.fasterxml.jackson:jackson-bom:2.9.4', 29 | 'org.apache.httpcomponents:httpclient:4.5.3' 30 | 31 | testCompile 'junit:junit:4.12', 32 | 'org.springframework.boot:spring-boot-starter-test:2.1.7.RELEASE', 33 | 34 | 35 | implementation("org.web3j:contracts:4.2.0") { exclude group: 'org.web3j' } 36 | } 37 | 38 | run { 39 | /* Can pass all the properties: */ 40 | systemProperties System.getProperties() 41 | 42 | /* Or just each by name: */ 43 | systemProperty "nodeEndpoint", System.getProperty("nodeEndpoint") 44 | systemProperty "fromAddress", System.getProperty("fromAddress") 45 | 46 | /* Need to split the space-delimited value in the exec.args */ 47 | args System.getProperty("exec.args", "").split() 48 | } 49 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM frolvlad/alpine-java 2 | 3 | RUN apk update && apk upgrade && \ 4 | apk add --no-cache bash git openssh 5 | 6 | RUN mkdir -p /app/erc20-rest-service 7 | RUN git clone https://github.com/blk-io/erc20-rest-service.git 8 | 9 | # We exclude running tests as we need a Ethereum/Quorum network to be running 10 | RUN cd erc20-rest-service \ 11 | && ./gradlew build -x test \ 12 | && cp build/libs/erc20-rest-service-0.1.0.jar /app/erc20-rest-service 13 | 14 | ENV PORT=8080 15 | ENV ENDPOINT="http://localhost:22000" 16 | ENV FROMADDR="0xed9d02e382b34818e88b88a309c7fe71e65f419d" 17 | 18 | ENTRYPOINT ["/usr/bin/java"] 19 | # We can define an environment variable to interpolate these values, as Docker does not support 20 | # this functionality, hence we have to do it in the command. 21 | CMD ["-jar", "/app/erc20-rest-service/erc20-rest-service-0.1.0.jar", \ 22 | "--spring.application.json={\"nodeEndpoint\":\"${ENDPOINT}\",\"fromAddress\":\"${FROMADDR}\"}"] 23 | -------------------------------------------------------------------------------- /generate.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | cd src/main/resources/solidity/contract/ && \ 4 | solc --bin --abi --optimize --overwrite HumanStandardToken.sol -o build/ && \ 5 | web3j solidity generate \ 6 | --binFile build/HumanStandardToken.bin \ 7 | --abiFile build/HumanStandardToken.abi \ 8 | -p io.blk.erc20.generated \ 9 | -o ../../../java/ 10 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/web3labs/erc20-rest-service/b7e1933e6c21699e16e2cfdff5f25b4adb1d9a89/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Nov 18 15:02:41 GMT 2019 2 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip 3 | distributionBase=GRADLE_USER_HOME 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | # Determine the Java command to use to start the JVM. 86 | if [ -n "$JAVA_HOME" ] ; then 87 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 88 | # IBM's JDK on AIX uses strange locations for the executables 89 | JAVACMD="$JAVA_HOME/jre/sh/java" 90 | else 91 | JAVACMD="$JAVA_HOME/bin/java" 92 | fi 93 | if [ ! -x "$JAVACMD" ] ; then 94 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 95 | 96 | Please set the JAVA_HOME variable in your environment to match the 97 | location of your Java installation." 98 | fi 99 | else 100 | JAVACMD="java" 101 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 102 | 103 | Please set the JAVA_HOME variable in your environment to match the 104 | location of your Java installation." 105 | fi 106 | 107 | # Increase the maximum file descriptors if we can. 108 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 109 | MAX_FD_LIMIT=`ulimit -H -n` 110 | if [ $? -eq 0 ] ; then 111 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 112 | MAX_FD="$MAX_FD_LIMIT" 113 | fi 114 | ulimit -n $MAX_FD 115 | if [ $? -ne 0 ] ; then 116 | warn "Could not set maximum file descriptor limit: $MAX_FD" 117 | fi 118 | else 119 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 120 | fi 121 | fi 122 | 123 | # For Darwin, add options to specify how the application appears in the dock 124 | if $darwin; then 125 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 126 | fi 127 | 128 | # For Cygwin or MSYS, switch paths to Windows format before running java 129 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 130 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 131 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 132 | JAVACMD=`cygpath --unix "$JAVACMD"` 133 | 134 | # We build the pattern for arguments to be converted via cygpath 135 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 136 | SEP="" 137 | for dir in $ROOTDIRSRAW ; do 138 | ROOTDIRS="$ROOTDIRS$SEP$dir" 139 | SEP="|" 140 | done 141 | OURCYGPATTERN="(^($ROOTDIRS))" 142 | # Add a user-defined pattern to the cygpath arguments 143 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 144 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 145 | fi 146 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 147 | i=0 148 | for arg in "$@" ; do 149 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 150 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 151 | 152 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 153 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 154 | else 155 | eval `echo args$i`="\"$arg\"" 156 | fi 157 | i=$((i+1)) 158 | done 159 | case $i in 160 | (0) set -- ;; 161 | (1) set -- "$args0" ;; 162 | (2) set -- "$args0" "$args1" ;; 163 | (3) set -- "$args0" "$args1" "$args2" ;; 164 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 165 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 166 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 167 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 168 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 169 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 170 | esac 171 | fi 172 | 173 | # Escape application args 174 | save () { 175 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 176 | echo " " 177 | } 178 | APP_ARGS=$(save "$@") 179 | 180 | # Collect all arguments for the java command, following the shell quoting and substitution rules 181 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 182 | 183 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 184 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 185 | cd "$(dirname "$0")" 186 | fi 187 | 188 | exec "$JAVACMD" "$@" 189 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 33 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 34 | 35 | @rem Find java.exe 36 | if defined JAVA_HOME goto findJavaFromJavaHome 37 | 38 | set JAVA_EXE=java.exe 39 | %JAVA_EXE% -version >NUL 2>&1 40 | if "%ERRORLEVEL%" == "0" goto init 41 | 42 | echo. 43 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 44 | echo. 45 | echo Please set the JAVA_HOME variable in your environment to match the 46 | echo location of your Java installation. 47 | 48 | goto fail 49 | 50 | :findJavaFromJavaHome 51 | set JAVA_HOME=%JAVA_HOME:"=% 52 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 53 | 54 | if exist "%JAVA_EXE%" goto init 55 | 56 | echo. 57 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 58 | echo. 59 | echo Please set the JAVA_HOME variable in your environment to match the 60 | echo location of your Java installation. 61 | 62 | goto fail 63 | 64 | :init 65 | @rem Get command-line arguments, handling Windows variants 66 | 67 | if not "%OS%" == "Windows_NT" goto win9xME_args 68 | 69 | :win9xME_args 70 | @rem Slurp the command line arguments. 71 | set CMD_LINE_ARGS= 72 | set _SKIP=2 73 | 74 | :win9xME_args_slurp 75 | if "x%~1" == "x" goto execute 76 | 77 | set CMD_LINE_ARGS=%* 78 | 79 | :execute 80 | @rem Setup the command line 81 | 82 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 83 | 84 | @rem Execute Gradle 85 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 86 | 87 | :end 88 | @rem End local scope for the variables with windows NT shell 89 | if "%ERRORLEVEL%"=="0" goto mainEnd 90 | 91 | :fail 92 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 93 | rem the _cmd.exe /c_ return code! 94 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 95 | exit /b 1 96 | 97 | :mainEnd 98 | if "%OS%"=="Windows_NT" endlocal 99 | 100 | :omega 101 | -------------------------------------------------------------------------------- /images/full-swagger-ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/web3labs/erc20-rest-service/b7e1933e6c21699e16e2cfdff5f25b4adb1d9a89/images/full-swagger-ui.png -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'erc20-rest-service' 2 | -------------------------------------------------------------------------------- /src/main/java/io/blk/erc20/Application.java: -------------------------------------------------------------------------------- 1 | package io.blk.erc20; 2 | 3 | import com.google.common.base.Predicates; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.boot.Banner; 6 | import org.springframework.boot.SpringApplication; 7 | import org.springframework.boot.autoconfigure.SpringBootApplication; 8 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 9 | import org.springframework.context.annotation.Bean; 10 | import org.web3j.protocol.Web3jService; 11 | import org.web3j.protocol.http.HttpService; 12 | import org.web3j.protocol.ipc.UnixIpcService; 13 | import org.web3j.protocol.ipc.WindowsIpcService; 14 | import org.web3j.quorum.Quorum; 15 | import springfox.documentation.builders.ApiInfoBuilder; 16 | import springfox.documentation.builders.RequestHandlerSelectors; 17 | import springfox.documentation.service.ApiInfo; 18 | import springfox.documentation.spi.DocumentationType; 19 | import springfox.documentation.spring.web.plugins.Docket; 20 | import springfox.documentation.swagger2.annotations.EnableSwagger2; 21 | 22 | /** 23 | * Our main application class. 24 | */ 25 | @EnableSwagger2 26 | @SpringBootApplication 27 | public class Application { 28 | 29 | public static void main(String[] args) { 30 | SpringApplication application = new SpringApplication(Application.class); 31 | application.setBannerMode(Banner.Mode.OFF); 32 | application.run(args); 33 | } 34 | 35 | @Autowired 36 | NodeConfiguration nodeConfiguration; 37 | 38 | @Bean 39 | Quorum quorum() { 40 | String nodeEndpoint = nodeConfiguration.getNodeEndpoint(); 41 | Web3jService web3jService; 42 | if (nodeEndpoint == null || nodeEndpoint.equals("")) { 43 | web3jService = new HttpService(); 44 | } else if (nodeEndpoint.startsWith("http")) { 45 | web3jService = new HttpService(nodeEndpoint); 46 | } else if (System.getProperty("os.name").toLowerCase().startsWith("win")) { 47 | web3jService = new WindowsIpcService(nodeEndpoint); 48 | } else { 49 | web3jService = new UnixIpcService(nodeEndpoint); 50 | } 51 | return Quorum.build(web3jService); 52 | } 53 | 54 | @Bean 55 | public Docket lenderApi() { 56 | return new Docket(DocumentationType.SWAGGER_2) 57 | .apiInfo(apiInfo()) 58 | .select() 59 | // see https://github.com/springfox/springfox/issues/631 60 | .apis(Predicates.not( 61 | RequestHandlerSelectors.basePackage("org.springframework.boot"))) 62 | .build(); 63 | } 64 | 65 | private ApiInfo apiInfo() { 66 | return new ApiInfoBuilder() 67 | .title("ERC-20 API") 68 | .description("ERC-20 token standard RESTful service") 69 | .build(); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/io/blk/erc20/ContractService.java: -------------------------------------------------------------------------------- 1 | package io.blk.erc20; 2 | 3 | import java.math.BigInteger; 4 | import java.util.Collections; 5 | import java.util.List; 6 | import java.util.concurrent.ExecutionException; 7 | import java.util.function.Function; 8 | 9 | import io.blk.erc20.generated.HumanStandardToken; 10 | import io.reactivex.annotations.Nullable; 11 | import lombok.Getter; 12 | import lombok.Setter; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | 15 | import org.springframework.stereotype.Service; 16 | 17 | import org.web3j.protocol.core.methods.response.TransactionReceipt; 18 | import org.web3j.quorum.Quorum; 19 | import org.web3j.quorum.tx.ClientTransactionManager; 20 | import org.web3j.tx.TransactionManager; 21 | 22 | import static org.web3j.tx.Contract.GAS_LIMIT; 23 | import static org.web3j.tx.ManagedTransaction.GAS_PRICE; 24 | 25 | /** 26 | * Our smart contract service. 27 | */ 28 | @Service 29 | public class ContractService { 30 | 31 | private final Quorum quorum; 32 | 33 | private final NodeConfiguration nodeConfiguration; 34 | 35 | @Autowired 36 | public ContractService(Quorum quorum, NodeConfiguration nodeConfiguration) { 37 | this.quorum = quorum; 38 | this.nodeConfiguration = nodeConfiguration; 39 | } 40 | 41 | public NodeConfiguration getConfig() { 42 | return nodeConfiguration; 43 | } 44 | 45 | public String deploy( 46 | List privateFor, BigInteger initialAmount, String tokenName, BigInteger decimalUnits, 47 | String tokenSymbol) throws Exception { 48 | try { 49 | TransactionManager transactionManager = new ClientTransactionManager( 50 | quorum, nodeConfiguration.getFromAddress(), privateFor); 51 | HumanStandardToken humanStandardToken = HumanStandardToken.deploy( 52 | quorum, transactionManager, GAS_PRICE, GAS_LIMIT, 53 | initialAmount, tokenName, decimalUnits, 54 | tokenSymbol).send(); 55 | return humanStandardToken.getContractAddress(); 56 | } catch (InterruptedException | ExecutionException e) { 57 | throw new RuntimeException(e); 58 | } 59 | } 60 | 61 | public String name(String contractAddress) throws Exception { 62 | HumanStandardToken humanStandardToken = load(contractAddress); 63 | try { 64 | return humanStandardToken.name().send(); 65 | } catch (InterruptedException | ExecutionException e) { 66 | throw new RuntimeException(e); 67 | } 68 | } 69 | 70 | public TransactionResponse approve( 71 | List privateFor, String contractAddress, String spender, BigInteger value) throws Exception { 72 | HumanStandardToken humanStandardToken = load(contractAddress, privateFor); 73 | try { 74 | TransactionReceipt transactionReceipt = humanStandardToken 75 | .approve(spender, value).send(); 76 | return processApprovalEventResponse(humanStandardToken, transactionReceipt); 77 | } catch (InterruptedException | ExecutionException e) { 78 | throw new RuntimeException(e); 79 | } 80 | } 81 | 82 | public String totalSupply(String contractAddress) throws Exception { 83 | HumanStandardToken humanStandardToken = load(contractAddress); 84 | try { 85 | return humanStandardToken.totalSupply().send().toString(); 86 | } catch (InterruptedException | ExecutionException e) { 87 | throw new RuntimeException(e); 88 | } 89 | } 90 | 91 | public TransactionResponse transferFrom( 92 | List privateFor, String contractAddress, String from, String to, BigInteger value) throws Exception { 93 | HumanStandardToken humanStandardToken = load(contractAddress, privateFor); 94 | try { 95 | TransactionReceipt transactionReceipt = humanStandardToken 96 | .transferFrom(from, to, value).send(); 97 | return processTransferEventsResponse(humanStandardToken, transactionReceipt); 98 | } catch (InterruptedException | ExecutionException e) { 99 | throw new RuntimeException(e); 100 | } 101 | } 102 | 103 | public String decimals(String contractAddress) throws Exception { 104 | HumanStandardToken humanStandardToken = load(contractAddress); 105 | try { 106 | return humanStandardToken.decimals().send().toString(); 107 | } catch (InterruptedException | ExecutionException e) { 108 | throw new RuntimeException(e); 109 | } 110 | } 111 | 112 | public String version(String contractAddress) throws Exception { 113 | HumanStandardToken humanStandardToken = load(contractAddress); 114 | try { 115 | return humanStandardToken.version().send(); 116 | } catch (InterruptedException | ExecutionException e) { 117 | throw new RuntimeException(e); 118 | } 119 | } 120 | 121 | public String balanceOf(String contractAddress, String ownerAddress) throws Exception { 122 | HumanStandardToken humanStandardToken = load(contractAddress); 123 | try { 124 | return humanStandardToken.balanceOf(ownerAddress).send().toString(); 125 | } catch (InterruptedException | ExecutionException e) { 126 | throw new RuntimeException(e); 127 | } 128 | } 129 | 130 | public String symbol(String contractAddress) throws Exception { 131 | HumanStandardToken humanStandardToken = load(contractAddress); 132 | try { 133 | return humanStandardToken.symbol().send(); 134 | } catch (InterruptedException | ExecutionException e) { 135 | throw new RuntimeException(e); 136 | } 137 | } 138 | 139 | public TransactionResponse transfer( 140 | List privateFor, String contractAddress, String to, BigInteger value) throws Exception { 141 | HumanStandardToken humanStandardToken = load(contractAddress, privateFor); 142 | try { 143 | TransactionReceipt transactionReceipt = humanStandardToken 144 | .transfer(to, value).send(); 145 | return processTransferEventsResponse(humanStandardToken, transactionReceipt); 146 | } catch (InterruptedException | ExecutionException e) { 147 | throw new RuntimeException(e); 148 | } 149 | } 150 | 151 | public TransactionResponse approveAndCall( 152 | @Nullable List privateFor, String contractAddress, String spender, BigInteger value, 153 | String extraData) throws Exception { 154 | HumanStandardToken humanStandardToken = load(contractAddress, privateFor); 155 | try { 156 | TransactionReceipt transactionReceipt = humanStandardToken 157 | .approveAndCall( 158 | spender, value, 159 | extraData.getBytes()) 160 | .send(); 161 | return processApprovalEventResponse(humanStandardToken, transactionReceipt); 162 | } catch (InterruptedException | ExecutionException e) { 163 | throw new RuntimeException(e); 164 | } 165 | } 166 | 167 | public String allowance(String contractAddress, String ownerAddress, String spenderAddress) throws Exception { 168 | HumanStandardToken humanStandardToken = load(contractAddress); 169 | try { 170 | return humanStandardToken.allowance( 171 | ownerAddress, spenderAddress) 172 | .send().toString(); 173 | } catch (InterruptedException | ExecutionException e) { 174 | throw new RuntimeException(e); 175 | } 176 | } 177 | 178 | private HumanStandardToken load(String contractAddress, List privateFor) { 179 | TransactionManager transactionManager = new ClientTransactionManager( 180 | quorum, nodeConfiguration.getFromAddress(), privateFor); 181 | return HumanStandardToken.load( 182 | contractAddress, quorum, transactionManager, GAS_PRICE, GAS_LIMIT); 183 | } 184 | 185 | private HumanStandardToken load(String contractAddress) { 186 | TransactionManager transactionManager = new ClientTransactionManager( 187 | quorum, nodeConfiguration.getFromAddress(), Collections.emptyList()); 188 | return HumanStandardToken.load( 189 | contractAddress, quorum, transactionManager, GAS_PRICE, GAS_LIMIT); 190 | } 191 | 192 | private TransactionResponse 193 | processApprovalEventResponse( 194 | HumanStandardToken humanStandardToken, 195 | TransactionReceipt transactionReceipt) { 196 | 197 | return processEventResponse( 198 | humanStandardToken.getApprovalEvents(transactionReceipt), 199 | transactionReceipt, 200 | ApprovalEventResponse::new); 201 | } 202 | 203 | private TransactionResponse 204 | processTransferEventsResponse( 205 | HumanStandardToken humanStandardToken, 206 | TransactionReceipt transactionReceipt) { 207 | 208 | return processEventResponse( 209 | humanStandardToken.getTransferEvents(transactionReceipt), 210 | transactionReceipt, 211 | TransferEventResponse::new); 212 | } 213 | 214 | private TransactionResponse processEventResponse( 215 | List eventResponses, TransactionReceipt transactionReceipt, Function map) { 216 | if (!eventResponses.isEmpty()) { 217 | return new TransactionResponse<>( 218 | transactionReceipt.getTransactionHash(), 219 | map.apply(eventResponses.get(0))); 220 | } else { 221 | return new TransactionResponse<>( 222 | transactionReceipt.getTransactionHash()); 223 | } 224 | } 225 | 226 | @Getter 227 | @Setter 228 | public static class TransferEventResponse { 229 | private String from; 230 | private String to; 231 | private long value; 232 | 233 | public TransferEventResponse() { } 234 | 235 | public TransferEventResponse( 236 | HumanStandardToken.TransferEventResponse transferEventResponse) { 237 | this.from = transferEventResponse._from; 238 | this.to = transferEventResponse._to; 239 | this.value = transferEventResponse._value.longValueExact(); 240 | } 241 | 242 | public String getFrom() { 243 | return from; 244 | } 245 | 246 | public void setFrom(String from) { 247 | this.from = from; 248 | } 249 | 250 | public String getTo() { 251 | return to; 252 | } 253 | 254 | public void setTo(String to) { 255 | this.to = to; 256 | } 257 | 258 | public long getValue() { 259 | return value; 260 | } 261 | 262 | public void setValue(long value) { 263 | this.value = value; 264 | } 265 | } 266 | 267 | @Getter 268 | @Setter 269 | public static class ApprovalEventResponse { 270 | private String owner; 271 | private String spender; 272 | private long value; 273 | 274 | public ApprovalEventResponse() { } 275 | 276 | public ApprovalEventResponse( 277 | HumanStandardToken.ApprovalEventResponse approvalEventResponse) { 278 | this.owner = approvalEventResponse._owner; 279 | this.spender = approvalEventResponse._spender; 280 | this.value = approvalEventResponse._value.longValueExact(); 281 | } 282 | 283 | public String getOwner() { 284 | return owner; 285 | } 286 | 287 | public void setOwner(String owner) { 288 | this.owner = owner; 289 | } 290 | 291 | public String getSpender() { 292 | return spender; 293 | } 294 | 295 | public void setSpender(String spender) { 296 | this.spender = spender; 297 | } 298 | 299 | public long getValue() { 300 | return value; 301 | } 302 | 303 | public void setValue(long value) { 304 | this.value = value; 305 | } 306 | } 307 | } 308 | -------------------------------------------------------------------------------- /src/main/java/io/blk/erc20/Controller.java: -------------------------------------------------------------------------------- 1 | package io.blk.erc20; 2 | 3 | import java.math.BigInteger; 4 | import java.util.Arrays; 5 | import java.util.Collections; 6 | import java.util.List; 7 | import javax.servlet.http.HttpServletRequest; 8 | 9 | import io.reactivex.annotations.Nullable; 10 | import io.swagger.annotations.Api; 11 | import io.swagger.annotations.ApiImplicitParam; 12 | import io.swagger.annotations.ApiOperation; 13 | import lombok.Data; 14 | import org.springframework.beans.factory.annotation.Autowired; 15 | import org.springframework.http.MediaType; 16 | import org.springframework.web.bind.annotation.PathVariable; 17 | import org.springframework.web.bind.annotation.RequestBody; 18 | import org.springframework.web.bind.annotation.RequestMapping; 19 | import org.springframework.web.bind.annotation.RequestMethod; 20 | import org.springframework.web.bind.annotation.RequestParam; 21 | import org.springframework.web.bind.annotation.RestController; 22 | 23 | /** 24 | * Controller for our ERC-20 contract API. 25 | */ 26 | @Api("ERC-20 token standard API") 27 | @RestController 28 | public class Controller { 29 | 30 | private final ContractService ContractService; 31 | 32 | @Autowired 33 | public Controller(ContractService ContractService) { 34 | this.ContractService = ContractService; 35 | } 36 | 37 | @ApiOperation("Application configuration") 38 | @RequestMapping(value = "/config", method = RequestMethod.GET, 39 | produces = MediaType.APPLICATION_JSON_UTF8_VALUE) 40 | NodeConfiguration config() { 41 | return ContractService.getConfig(); 42 | } 43 | 44 | @ApiOperation( 45 | value = "Deploy new ERC-20 token", 46 | notes = "Returns hex encoded contract address") 47 | @ApiImplicitParam(name = "privateFor", 48 | value = "Comma separated list of public keys of enclave nodes that transaction is " 49 | + "private for", 50 | paramType = "header", 51 | dataType = "string") 52 | @RequestMapping(value = "/deploy", method = RequestMethod.POST) 53 | String deploy( 54 | HttpServletRequest request, 55 | @RequestBody ContractSpecification contractSpecification) throws Exception { 56 | 57 | return ContractService.deploy( 58 | extractPrivateFor(request), 59 | contractSpecification.getInitialAmount(), 60 | contractSpecification.getTokenName(), 61 | contractSpecification.getDecimalUnits(), 62 | contractSpecification.getTokenSymbol()); 63 | } 64 | 65 | @ApiOperation("Get token name") 66 | @RequestMapping(value = "/{contractAddress}/name", method = RequestMethod.GET) 67 | String name(@PathVariable String contractAddress) throws Exception { 68 | return ContractService.name(contractAddress); 69 | } 70 | 71 | @ApiOperation( 72 | value = "Approve transfers by a specific address up to the provided total quantity", 73 | notes = "Returns hex encoded transaction hash, and Approval event if called") 74 | @ApiImplicitParam(name = "privateFor", 75 | value = "Comma separated list of public keys of enclave nodes that transaction is " 76 | + "private for", 77 | paramType = "header", 78 | dataType = "string") 79 | @RequestMapping(value = "/{contractAddress}/approve", method = RequestMethod.POST) 80 | TransactionResponse approve( 81 | HttpServletRequest request, 82 | @PathVariable String contractAddress, 83 | @RequestBody ApproveRequest approveRequest) throws Exception { 84 | return ContractService.approve( 85 | extractPrivateFor(request), 86 | contractAddress, 87 | approveRequest.getSpender(), 88 | approveRequest.getValue()); 89 | } 90 | 91 | @ApiOperation("Get total supply of tokens") 92 | @RequestMapping(value = "/{contractAddress}/totalSupply", method = RequestMethod.GET) 93 | String totalSupply(@PathVariable String contractAddress) throws Exception { 94 | return ContractService.totalSupply(contractAddress); 95 | } 96 | 97 | @ApiOperation( 98 | value = "Transfer tokens between addresses (must already be approved)", 99 | notes = "Returns hex encoded transaction hash, and Transfer event if called") 100 | @ApiImplicitParam(name = "privateFor", 101 | value = "Comma separated list of public keys of enclave nodes that transaction is " 102 | + "private for", 103 | paramType = "header", 104 | dataType = "string") 105 | @RequestMapping(value = "/{contractAddress}/transferFrom", method = RequestMethod.POST) 106 | TransactionResponse transferFrom( 107 | HttpServletRequest request, 108 | @PathVariable String contractAddress, 109 | @RequestBody TransferFromRequest transferFromRequest) throws Exception { 110 | return ContractService.transferFrom( 111 | extractPrivateFor(request), 112 | contractAddress, 113 | transferFromRequest.getFrom(), 114 | transferFromRequest.getTo(), 115 | transferFromRequest.getValue()); 116 | } 117 | 118 | @ApiOperation("Get decimal precision of tokens") 119 | @RequestMapping(value = "/{contractAddress}/decimals", method = RequestMethod.GET) 120 | String decimals(@PathVariable String contractAddress) throws Exception { 121 | return ContractService.decimals(contractAddress); 122 | } 123 | 124 | @ApiOperation("Get contract version") 125 | @RequestMapping(value = "/{contractAddress}/version", method = RequestMethod.GET) 126 | String version(@PathVariable String contractAddress) throws Exception { 127 | return ContractService.version(contractAddress); 128 | } 129 | 130 | @ApiOperation("Get token balance for address") 131 | @RequestMapping( 132 | value = "/{contractAddress}/balanceOf/{ownerAddress}", method = RequestMethod.GET) 133 | String balanceOf( 134 | @PathVariable String contractAddress, 135 | @PathVariable String ownerAddress) throws Exception { 136 | return ContractService.balanceOf(contractAddress, ownerAddress); 137 | } 138 | 139 | @ApiOperation("Get token symbol") 140 | @RequestMapping(value = "/{contractAddress}/symbol", method = RequestMethod.GET) 141 | String symbol(@PathVariable String contractAddress) throws Exception { 142 | return ContractService.symbol(contractAddress); 143 | } 144 | 145 | @ApiOperation( 146 | value = "Transfer tokens you own to another address", 147 | notes = "Returns hex encoded transaction hash, and Transfer event if called") 148 | @ApiImplicitParam(name = "privateFor", 149 | value = "Comma separated list of public keys of enclave nodes that transaction is " 150 | + "private for", 151 | paramType = "header", 152 | dataType = "string") 153 | @RequestMapping(value = "/{contractAddress}/transfer", method = RequestMethod.POST) 154 | TransactionResponse transfer( 155 | HttpServletRequest request, 156 | @PathVariable String contractAddress, 157 | @RequestBody TransferRequest transferRequest) throws Exception { 158 | return ContractService.transfer( 159 | extractPrivateFor(request), 160 | contractAddress, 161 | transferRequest.getTo(), 162 | transferRequest.getValue()); 163 | } 164 | 165 | @ApiOperation( 166 | value = "Approve transfers by a specific contract address up to the provided total " 167 | + "quantity, and notify that contract address of the approval", 168 | notes = "Returns hex encoded transaction hash, and Approval event if called") 169 | @ApiImplicitParam(name = "privateFor", 170 | value = "Comma separated list of public keys of enclave nodes that transaction is " 171 | + "private for", 172 | paramType = "header", 173 | dataType = "string") 174 | @RequestMapping(value = "/{contractAddress}/approveAndCall", method = RequestMethod.POST) 175 | TransactionResponse approveAndCall( 176 | HttpServletRequest request, 177 | @PathVariable String contractAddress, 178 | @RequestBody ApproveAndCallRequest approveAndCallRequest) throws Exception { 179 | return ContractService.approveAndCall( 180 | extractPrivateFor(request), 181 | contractAddress, 182 | approveAndCallRequest.getSpender(), 183 | approveAndCallRequest.getValue(), 184 | approveAndCallRequest.getExtraData()); 185 | } 186 | 187 | @ApiOperation("Get quantity of tokens you can transfer on another token holder's behalf") 188 | @RequestMapping(value = "/{contractAddress}/allowance", method = RequestMethod.GET) 189 | String allowance( 190 | @PathVariable String contractAddress, 191 | @RequestParam String ownerAddress, 192 | @RequestParam String spenderAddress) throws Exception { 193 | return ContractService.allowance( 194 | contractAddress, ownerAddress, spenderAddress); 195 | } 196 | 197 | private static @Nullable List extractPrivateFor(HttpServletRequest request) { 198 | String privateFor = request.getHeader("privateFor"); 199 | if (privateFor == null) { 200 | return null; 201 | } else { 202 | return Arrays.asList(privateFor.split(",")); 203 | } 204 | } 205 | 206 | @Data 207 | static class ContractSpecification { 208 | private BigInteger initialAmount; 209 | private String tokenName; 210 | private BigInteger decimalUnits; 211 | private String tokenSymbol; 212 | 213 | ContractSpecification() { 214 | } 215 | 216 | ContractSpecification(BigInteger initialAmount, String tokenName, BigInteger decimalUnits, String tokenSymbol) { 217 | this.initialAmount = initialAmount; 218 | this.tokenName = tokenName; 219 | this.decimalUnits = decimalUnits; 220 | this.tokenSymbol = tokenSymbol; 221 | } 222 | 223 | public BigInteger getDecimalUnits() { 224 | return decimalUnits; 225 | } 226 | 227 | public BigInteger getInitialAmount() { 228 | return initialAmount; 229 | } 230 | 231 | public String getTokenName() { 232 | return tokenName; 233 | } 234 | 235 | public String getTokenSymbol() { 236 | return tokenSymbol; 237 | } 238 | } 239 | 240 | @Data 241 | static class ApproveRequest { 242 | private String spender; 243 | private BigInteger value; 244 | 245 | ApproveRequest() {} 246 | 247 | ApproveRequest(String spender, BigInteger value) { 248 | this.spender = spender; 249 | this.value = value; 250 | } 251 | 252 | public String getSpender() { 253 | return spender; 254 | } 255 | 256 | public BigInteger getValue() { 257 | return value; 258 | } 259 | } 260 | 261 | @Data 262 | static class TransferFromRequest { 263 | private String from; 264 | private String to; 265 | private BigInteger value; 266 | 267 | TransferFromRequest() {} 268 | 269 | TransferFromRequest(String from, String to, BigInteger value) { 270 | this.from = from; 271 | this.to = to; 272 | this.value = value; 273 | } 274 | 275 | 276 | public String getFrom() { 277 | return from; 278 | } 279 | 280 | BigInteger getValue() { 281 | return value; 282 | } 283 | 284 | public String getTo() { 285 | return to; 286 | } 287 | } 288 | 289 | @Data 290 | static class TransferRequest { 291 | private String to; 292 | private BigInteger value; 293 | 294 | TransferRequest(String to, BigInteger value) { 295 | this.to = to; 296 | this.value = value; 297 | } 298 | 299 | TransferRequest() {} 300 | 301 | public String getTo() { 302 | return to; 303 | } 304 | 305 | public BigInteger getValue() { 306 | return value; 307 | } 308 | } 309 | 310 | @Data 311 | static class ApproveAndCallRequest { 312 | private String spender; 313 | private BigInteger value; 314 | private String extraData; 315 | 316 | ApproveAndCallRequest() {} 317 | 318 | ApproveAndCallRequest(String spender, BigInteger value, String extraData) { 319 | this.spender = spender; 320 | this.value = value; 321 | this.extraData = extraData; 322 | } 323 | 324 | String getSpender() { 325 | return spender; 326 | } 327 | 328 | BigInteger getValue() { 329 | return value; 330 | } 331 | 332 | String getExtraData() { 333 | return extraData; 334 | } 335 | } 336 | 337 | @Data 338 | static class AllowanceRequest { 339 | private String ownerAddress; 340 | private String spenderAddress; 341 | 342 | AllowanceRequest() {} 343 | 344 | AllowanceRequest(String ownerAddress, String spenderAddress) { 345 | this.ownerAddress = ownerAddress; 346 | this.spenderAddress = spenderAddress; 347 | } 348 | 349 | public String getOwnerAddress() { 350 | return ownerAddress; 351 | } 352 | 353 | public void setOwnerAddress(String ownerAddress) { 354 | this.ownerAddress = ownerAddress; 355 | } 356 | 357 | public String getSpenderAddress() { 358 | return spenderAddress; 359 | } 360 | 361 | public void setSpenderAddress(String spenderAddress) { 362 | this.spenderAddress = spenderAddress; 363 | } 364 | } 365 | } 366 | -------------------------------------------------------------------------------- /src/main/java/io/blk/erc20/NodeConfiguration.java: -------------------------------------------------------------------------------- 1 | package io.blk.erc20; 2 | 3 | import lombok.Data; 4 | import org.springframework.boot.context.properties.ConfigurationProperties; 5 | import org.springframework.stereotype.Component; 6 | 7 | /** 8 | * Node configuration bean. 9 | */ 10 | @Data 11 | @ConfigurationProperties("io.blk.erc20") 12 | @Component 13 | public class NodeConfiguration { 14 | 15 | private String nodeEndpoint = System.getProperty("nodeEndpoint"); 16 | private String fromAddress = System.getProperty("fromAddress"); 17 | 18 | public String getNodeEndpoint() { 19 | return nodeEndpoint; 20 | } 21 | 22 | public void setNodeEndpoint(String nodeEndpoint) { 23 | this.nodeEndpoint = nodeEndpoint; 24 | } 25 | 26 | public String getFromAddress() { 27 | return fromAddress; 28 | } 29 | 30 | public void setFromAddress(String fromAddress) { 31 | this.fromAddress = fromAddress; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/io/blk/erc20/TransactionResponse.java: -------------------------------------------------------------------------------- 1 | package io.blk.erc20; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | /** 7 | * TransactionResponse wrapper. 8 | */ 9 | @Getter 10 | @Setter 11 | public class TransactionResponse { 12 | 13 | private String transactionHash; 14 | private T event; 15 | 16 | TransactionResponse() { } 17 | 18 | public TransactionResponse(String transactionHash) { 19 | this(transactionHash, null); 20 | } 21 | 22 | public TransactionResponse(String transactionHash, T event) { 23 | this.transactionHash = transactionHash; 24 | this.event = event; 25 | } 26 | 27 | public String getTransactionHash() { 28 | return transactionHash; 29 | } 30 | 31 | public void setTransactionHash(String transactionHash) { 32 | this.transactionHash = transactionHash; 33 | } 34 | 35 | public T getEvent() { 36 | return event; 37 | } 38 | 39 | public void setEvent(T event) { 40 | this.event = event; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/io/blk/erc20/generated/HumanStandardToken.java: -------------------------------------------------------------------------------- 1 | package io.blk.erc20.generated; 2 | 3 | import io.reactivex.Flowable; 4 | import java.math.BigInteger; 5 | import java.util.ArrayList; 6 | import java.util.Arrays; 7 | import java.util.Collections; 8 | import java.util.List; 9 | import org.web3j.abi.EventEncoder; 10 | import org.web3j.abi.FunctionEncoder; 11 | import org.web3j.abi.TypeReference; 12 | import org.web3j.abi.datatypes.Address; 13 | import org.web3j.abi.datatypes.Event; 14 | import org.web3j.abi.datatypes.Function; 15 | import org.web3j.abi.datatypes.Type; 16 | import org.web3j.abi.datatypes.Utf8String; 17 | import org.web3j.abi.datatypes.generated.Uint256; 18 | import org.web3j.abi.datatypes.generated.Uint8; 19 | import org.web3j.crypto.Credentials; 20 | import org.web3j.protocol.Web3j; 21 | import org.web3j.protocol.core.DefaultBlockParameter; 22 | import org.web3j.protocol.core.RemoteCall; 23 | import org.web3j.protocol.core.methods.request.EthFilter; 24 | import org.web3j.protocol.core.methods.response.Log; 25 | import org.web3j.protocol.core.methods.response.TransactionReceipt; 26 | import org.web3j.tx.Contract; 27 | import org.web3j.tx.TransactionManager; 28 | import org.web3j.tx.gas.ContractGasProvider; 29 | 30 | /** 31 | *

Auto generated code. 32 | *

Do not modify! 33 | *

Please use the web3j command line tools, 34 | * or the org.web3j.codegen.SolidityFunctionWrapperGenerator in the 35 | * codegen module to update. 36 | * 37 | *

Generated with web3j version 4.3.0. 38 | */ 39 | public class HumanStandardToken extends Contract { 40 | private static final String BINARY = "60c0604052600460808190527f48302e310000000000000000000000000000000000000000000000000000000060a090815261003e916006919061016b565b5034801561004b57600080fd5b50604051610a4f380380610a4f8339810180604052608081101561006e57600080fd5b81516020830180519193928301929164010000000081111561008f57600080fd5b820160208101848111156100a257600080fd5b81516401000000008111828201871017156100bc57600080fd5b505060208201516040909201805191949293916401000000008111156100e157600080fd5b820160208101848111156100f457600080fd5b815164010000000081118282018710171561010e57600080fd5b5050336000908152600160209081526040822089905590889055865191945061013e93506003925086019061016b565b506004805460ff191660ff8416179055805161016190600590602084019061016b565b5050505050610206565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f106101ac57805160ff19168380011785556101d9565b828001600101855582156101d9579182015b828111156101d95782518255916020019190600101906101be565b506101e59291506101e9565b5090565b61020391905b808211156101e557600081556001016101ef565b90565b61083a806102156000396000f3fe608060405234801561001057600080fd5b50600436106100c6576000357c01000000000000000000000000000000000000000000000000000000009004806354fd4d501161008e57806354fd4d50146101f657806370a08231146101fe57806395d89b4114610224578063a9059cbb1461022c578063cae9ca5114610258578063dd62ed3e146102dd576100c6565b806306fdde03146100cb578063095ea7b31461014857806318160ddd1461018857806323b872dd146101a2578063313ce567146101d8575b600080fd5b6100d361030b565b6040805160208082528351818301528351919283929083019185019080838360005b8381101561010d5781810151838201526020016100f5565b50505050905090810190601f16801561013a5780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b6101746004803603604081101561015e57600080fd5b50600160a060020a038135169060200135610399565b604080519115158252519081900360200190f35b610190610400565b60408051918252519081900360200190f35b610174600480360360608110156101b857600080fd5b50600160a060020a03813581169160208101359091169060400135610406565b6101e06104f3565b6040805160ff9092168252519081900360200190f35b6100d36104fc565b6101906004803603602081101561021457600080fd5b5035600160a060020a0316610557565b6100d3610572565b6101746004803603604081101561024257600080fd5b50600160a060020a0381351690602001356105cd565b6101746004803603606081101561026e57600080fd5b600160a060020a038235169160208101359181019060608101604082013564010000000081111561029e57600080fd5b8201836020820111156102b057600080fd5b803590602001918460018302840111640100000000831117156102d257600080fd5b509092509050610666565b610190600480360360408110156102f357600080fd5b50600160a060020a03813581169160200135166107b5565b6003805460408051602060026001851615610100026000190190941693909304601f810184900484028201840190925281815292918301828280156103915780601f1061036657610100808354040283529160200191610391565b820191906000526020600020905b81548152906001019060200180831161037457829003601f168201915b505050505081565b336000818152600260209081526040808320600160a060020a038716808552908352818420869055815186815291519394909390927f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925928290030190a35060015b92915050565b60005481565b600160a060020a03831660009081526001602052604081205482118015906104515750600160a060020a03841660009081526002602090815260408083203384529091529020548211155b801561045d5750600082115b156104e857600160a060020a03808416600081815260016020908152604080832080548801905593881680835284832080548890039055600282528483203384528252918490208054879003905583518681529351929391927fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef9281900390910190a35060016104ec565b5060005b9392505050565b60045460ff1681565b6006805460408051602060026001851615610100026000190190941693909304601f810184900484028201840190925281815292918301828280156103915780601f1061036657610100808354040283529160200191610391565b600160a060020a031660009081526001602052604090205490565b6005805460408051602060026001851615610100026000190190941693909304601f810184900484028201840190925281815292918301828280156103915780601f1061036657610100808354040283529160200191610391565b3360009081526001602052604081205482118015906105ec5750600082115b1561065e5733600081815260016020908152604080832080548790039055600160a060020a03871680845292819020805487019055805186815290519293927fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef929181900390910190a35060016103fa565b5060006103fa565b336000818152600260209081526040808320600160a060020a038916808552908352818420889055815188815291519394909390927f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925928290030190a3600085600160a060020a031660405160200180806020018281038252602e8152602001806107e1602e91396040019150506040516020818303038152906040526040518082805190602001908083835b602083106107325780518252601f199092019160209182019101610713565b6001836020036101000a0380198251168184511680821785525050505050509050019150506000604051808303816000865af19150503d8060008114610794576040519150601f19603f3d011682016040523d82523d6000602084013e610799565b606091505b505090508015156107a957600080fd5b50600195945050505050565b600160a060020a0391821660009081526002602090815260408083209390941682529190915220549056fe72656365697665417070726f76616c28616464726573732c75696e743235362c616464726573732c627974657329a165627a7a72305820d15a070a95051e159632a5f42da17cdc0b4e940c8c7574a86370e2f434405abd0029"; 41 | 42 | public static final String FUNC_NAME = "name"; 43 | 44 | public static final String FUNC_APPROVE = "approve"; 45 | 46 | public static final String FUNC_TOTALSUPPLY = "totalSupply"; 47 | 48 | public static final String FUNC_TRANSFERFROM = "transferFrom"; 49 | 50 | public static final String FUNC_DECIMALS = "decimals"; 51 | 52 | public static final String FUNC_VERSION = "version"; 53 | 54 | public static final String FUNC_BALANCEOF = "balanceOf"; 55 | 56 | public static final String FUNC_SYMBOL = "symbol"; 57 | 58 | public static final String FUNC_TRANSFER = "transfer"; 59 | 60 | public static final String FUNC_APPROVEANDCALL = "approveAndCall"; 61 | 62 | public static final String FUNC_ALLOWANCE = "allowance"; 63 | 64 | public static final Event TRANSFER_EVENT = new Event("Transfer", 65 | Arrays.>asList(new TypeReference

(true) {}, new TypeReference
(true) {}, new TypeReference() {})); 66 | ; 67 | 68 | public static final Event APPROVAL_EVENT = new Event("Approval", 69 | Arrays.>asList(new TypeReference
(true) {}, new TypeReference
(true) {}, new TypeReference() {})); 70 | ; 71 | 72 | @Deprecated 73 | protected HumanStandardToken(String contractAddress, Web3j web3j, Credentials credentials, BigInteger gasPrice, BigInteger gasLimit) { 74 | super(BINARY, contractAddress, web3j, credentials, gasPrice, gasLimit); 75 | } 76 | 77 | protected HumanStandardToken(String contractAddress, Web3j web3j, Credentials credentials, ContractGasProvider contractGasProvider) { 78 | super(BINARY, contractAddress, web3j, credentials, contractGasProvider); 79 | } 80 | 81 | @Deprecated 82 | protected HumanStandardToken(String contractAddress, Web3j web3j, TransactionManager transactionManager, BigInteger gasPrice, BigInteger gasLimit) { 83 | super(BINARY, contractAddress, web3j, transactionManager, gasPrice, gasLimit); 84 | } 85 | 86 | protected HumanStandardToken(String contractAddress, Web3j web3j, TransactionManager transactionManager, ContractGasProvider contractGasProvider) { 87 | super(BINARY, contractAddress, web3j, transactionManager, contractGasProvider); 88 | } 89 | 90 | public RemoteCall name() { 91 | final Function function = new Function(FUNC_NAME, 92 | Arrays.asList(), 93 | Arrays.>asList(new TypeReference() {})); 94 | return executeRemoteCallSingleValueReturn(function, String.class); 95 | } 96 | 97 | public RemoteCall approve(String _spender, BigInteger _value) { 98 | final Function function = new Function( 99 | FUNC_APPROVE, 100 | Arrays.asList(new org.web3j.abi.datatypes.Address(_spender), 101 | new org.web3j.abi.datatypes.generated.Uint256(_value)), 102 | Collections.>emptyList()); 103 | return executeRemoteCallTransaction(function); 104 | } 105 | 106 | public RemoteCall totalSupply() { 107 | final Function function = new Function(FUNC_TOTALSUPPLY, 108 | Arrays.asList(), 109 | Arrays.>asList(new TypeReference() {})); 110 | return executeRemoteCallSingleValueReturn(function, BigInteger.class); 111 | } 112 | 113 | public RemoteCall transferFrom(String _from, String _to, BigInteger _value) { 114 | final Function function = new Function( 115 | FUNC_TRANSFERFROM, 116 | Arrays.asList(new org.web3j.abi.datatypes.Address(_from), 117 | new org.web3j.abi.datatypes.Address(_to), 118 | new org.web3j.abi.datatypes.generated.Uint256(_value)), 119 | Collections.>emptyList()); 120 | return executeRemoteCallTransaction(function); 121 | } 122 | 123 | public RemoteCall decimals() { 124 | final Function function = new Function(FUNC_DECIMALS, 125 | Arrays.asList(), 126 | Arrays.>asList(new TypeReference() {})); 127 | return executeRemoteCallSingleValueReturn(function, BigInteger.class); 128 | } 129 | 130 | public RemoteCall version() { 131 | final Function function = new Function(FUNC_VERSION, 132 | Arrays.asList(), 133 | Arrays.>asList(new TypeReference() {})); 134 | return executeRemoteCallSingleValueReturn(function, String.class); 135 | } 136 | 137 | public RemoteCall balanceOf(String _owner) { 138 | final Function function = new Function(FUNC_BALANCEOF, 139 | Arrays.asList(new org.web3j.abi.datatypes.Address(_owner)), 140 | Arrays.>asList(new TypeReference() {})); 141 | return executeRemoteCallSingleValueReturn(function, BigInteger.class); 142 | } 143 | 144 | public RemoteCall symbol() { 145 | final Function function = new Function(FUNC_SYMBOL, 146 | Arrays.asList(), 147 | Arrays.>asList(new TypeReference() {})); 148 | return executeRemoteCallSingleValueReturn(function, String.class); 149 | } 150 | 151 | public RemoteCall transfer(String _to, BigInteger _value) { 152 | final Function function = new Function( 153 | FUNC_TRANSFER, 154 | Arrays.asList(new org.web3j.abi.datatypes.Address(_to), 155 | new org.web3j.abi.datatypes.generated.Uint256(_value)), 156 | Collections.>emptyList()); 157 | return executeRemoteCallTransaction(function); 158 | } 159 | 160 | public RemoteCall approveAndCall(String _spender, BigInteger _value, byte[] _extraData) { 161 | final Function function = new Function( 162 | FUNC_APPROVEANDCALL, 163 | Arrays.asList(new org.web3j.abi.datatypes.Address(_spender), 164 | new org.web3j.abi.datatypes.generated.Uint256(_value), 165 | new org.web3j.abi.datatypes.DynamicBytes(_extraData)), 166 | Collections.>emptyList()); 167 | return executeRemoteCallTransaction(function); 168 | } 169 | 170 | public RemoteCall allowance(String _owner, String _spender) { 171 | final Function function = new Function(FUNC_ALLOWANCE, 172 | Arrays.asList(new org.web3j.abi.datatypes.Address(_owner), 173 | new org.web3j.abi.datatypes.Address(_spender)), 174 | Arrays.>asList(new TypeReference() {})); 175 | return executeRemoteCallSingleValueReturn(function, BigInteger.class); 176 | } 177 | 178 | public List getTransferEvents(TransactionReceipt transactionReceipt) { 179 | List valueList = extractEventParametersWithLog(TRANSFER_EVENT, transactionReceipt); 180 | ArrayList responses = new ArrayList(valueList.size()); 181 | for (Contract.EventValuesWithLog eventValues : valueList) { 182 | TransferEventResponse typedResponse = new TransferEventResponse(); 183 | typedResponse.log = eventValues.getLog(); 184 | typedResponse._from = (String) eventValues.getIndexedValues().get(0).getValue(); 185 | typedResponse._to = (String) eventValues.getIndexedValues().get(1).getValue(); 186 | typedResponse._value = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); 187 | responses.add(typedResponse); 188 | } 189 | return responses; 190 | } 191 | 192 | public Flowable transferEventFlowable(EthFilter filter) { 193 | return web3j.ethLogFlowable(filter).map(new io.reactivex.functions.Function() { 194 | @Override 195 | public TransferEventResponse apply(Log log) { 196 | Contract.EventValuesWithLog eventValues = extractEventParametersWithLog(TRANSFER_EVENT, log); 197 | TransferEventResponse typedResponse = new TransferEventResponse(); 198 | typedResponse.log = log; 199 | typedResponse._from = (String) eventValues.getIndexedValues().get(0).getValue(); 200 | typedResponse._to = (String) eventValues.getIndexedValues().get(1).getValue(); 201 | typedResponse._value = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); 202 | return typedResponse; 203 | } 204 | }); 205 | } 206 | 207 | public Flowable transferEventFlowable(DefaultBlockParameter startBlock, DefaultBlockParameter endBlock) { 208 | EthFilter filter = new EthFilter(startBlock, endBlock, getContractAddress()); 209 | filter.addSingleTopic(EventEncoder.encode(TRANSFER_EVENT)); 210 | return transferEventFlowable(filter); 211 | } 212 | 213 | public List getApprovalEvents(TransactionReceipt transactionReceipt) { 214 | List valueList = extractEventParametersWithLog(APPROVAL_EVENT, transactionReceipt); 215 | ArrayList responses = new ArrayList(valueList.size()); 216 | for (Contract.EventValuesWithLog eventValues : valueList) { 217 | ApprovalEventResponse typedResponse = new ApprovalEventResponse(); 218 | typedResponse.log = eventValues.getLog(); 219 | typedResponse._owner = (String) eventValues.getIndexedValues().get(0).getValue(); 220 | typedResponse._spender = (String) eventValues.getIndexedValues().get(1).getValue(); 221 | typedResponse._value = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); 222 | responses.add(typedResponse); 223 | } 224 | return responses; 225 | } 226 | 227 | public Flowable approvalEventFlowable(EthFilter filter) { 228 | return web3j.ethLogFlowable(filter).map(new io.reactivex.functions.Function() { 229 | @Override 230 | public ApprovalEventResponse apply(Log log) { 231 | Contract.EventValuesWithLog eventValues = extractEventParametersWithLog(APPROVAL_EVENT, log); 232 | ApprovalEventResponse typedResponse = new ApprovalEventResponse(); 233 | typedResponse.log = log; 234 | typedResponse._owner = (String) eventValues.getIndexedValues().get(0).getValue(); 235 | typedResponse._spender = (String) eventValues.getIndexedValues().get(1).getValue(); 236 | typedResponse._value = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); 237 | return typedResponse; 238 | } 239 | }); 240 | } 241 | 242 | public Flowable approvalEventFlowable(DefaultBlockParameter startBlock, DefaultBlockParameter endBlock) { 243 | EthFilter filter = new EthFilter(startBlock, endBlock, getContractAddress()); 244 | filter.addSingleTopic(EventEncoder.encode(APPROVAL_EVENT)); 245 | return approvalEventFlowable(filter); 246 | } 247 | 248 | @Deprecated 249 | public static HumanStandardToken load(String contractAddress, Web3j web3j, Credentials credentials, BigInteger gasPrice, BigInteger gasLimit) { 250 | return new HumanStandardToken(contractAddress, web3j, credentials, gasPrice, gasLimit); 251 | } 252 | 253 | @Deprecated 254 | public static HumanStandardToken load(String contractAddress, Web3j web3j, TransactionManager transactionManager, BigInteger gasPrice, BigInteger gasLimit) { 255 | return new HumanStandardToken(contractAddress, web3j, transactionManager, gasPrice, gasLimit); 256 | } 257 | 258 | public static HumanStandardToken load(String contractAddress, Web3j web3j, Credentials credentials, ContractGasProvider contractGasProvider) { 259 | return new HumanStandardToken(contractAddress, web3j, credentials, contractGasProvider); 260 | } 261 | 262 | public static HumanStandardToken load(String contractAddress, Web3j web3j, TransactionManager transactionManager, ContractGasProvider contractGasProvider) { 263 | return new HumanStandardToken(contractAddress, web3j, transactionManager, contractGasProvider); 264 | } 265 | 266 | public static RemoteCall deploy(Web3j web3j, Credentials credentials, ContractGasProvider contractGasProvider, BigInteger _initialAmount, String _tokenName, BigInteger _decimalUnits, String _tokenSymbol) { 267 | String encodedConstructor = FunctionEncoder.encodeConstructor(Arrays.asList(new org.web3j.abi.datatypes.generated.Uint256(_initialAmount), 268 | new org.web3j.abi.datatypes.Utf8String(_tokenName), 269 | new org.web3j.abi.datatypes.generated.Uint8(_decimalUnits), 270 | new org.web3j.abi.datatypes.Utf8String(_tokenSymbol))); 271 | return deployRemoteCall(HumanStandardToken.class, web3j, credentials, contractGasProvider, BINARY, encodedConstructor); 272 | } 273 | 274 | public static RemoteCall deploy(Web3j web3j, TransactionManager transactionManager, ContractGasProvider contractGasProvider, BigInteger _initialAmount, String _tokenName, BigInteger _decimalUnits, String _tokenSymbol) { 275 | String encodedConstructor = FunctionEncoder.encodeConstructor(Arrays.asList(new org.web3j.abi.datatypes.generated.Uint256(_initialAmount), 276 | new org.web3j.abi.datatypes.Utf8String(_tokenName), 277 | new org.web3j.abi.datatypes.generated.Uint8(_decimalUnits), 278 | new org.web3j.abi.datatypes.Utf8String(_tokenSymbol))); 279 | return deployRemoteCall(HumanStandardToken.class, web3j, transactionManager, contractGasProvider, BINARY, encodedConstructor); 280 | } 281 | 282 | @Deprecated 283 | public static RemoteCall deploy(Web3j web3j, Credentials credentials, BigInteger gasPrice, BigInteger gasLimit, BigInteger _initialAmount, String _tokenName, BigInteger _decimalUnits, String _tokenSymbol) { 284 | String encodedConstructor = FunctionEncoder.encodeConstructor(Arrays.asList(new org.web3j.abi.datatypes.generated.Uint256(_initialAmount), 285 | new org.web3j.abi.datatypes.Utf8String(_tokenName), 286 | new org.web3j.abi.datatypes.generated.Uint8(_decimalUnits), 287 | new org.web3j.abi.datatypes.Utf8String(_tokenSymbol))); 288 | return deployRemoteCall(HumanStandardToken.class, web3j, credentials, gasPrice, gasLimit, BINARY, encodedConstructor); 289 | } 290 | 291 | @Deprecated 292 | public static RemoteCall deploy(Web3j web3j, TransactionManager transactionManager, BigInteger gasPrice, BigInteger gasLimit, BigInteger _initialAmount, String _tokenName, BigInteger _decimalUnits, String _tokenSymbol) { 293 | String encodedConstructor = FunctionEncoder.encodeConstructor(Arrays.asList(new org.web3j.abi.datatypes.generated.Uint256(_initialAmount), 294 | new org.web3j.abi.datatypes.Utf8String(_tokenName), 295 | new org.web3j.abi.datatypes.generated.Uint8(_decimalUnits), 296 | new org.web3j.abi.datatypes.Utf8String(_tokenSymbol))); 297 | return deployRemoteCall(HumanStandardToken.class, web3j, transactionManager, gasPrice, gasLimit, BINARY, encodedConstructor); 298 | } 299 | 300 | public static class TransferEventResponse { 301 | public Log log; 302 | 303 | public String _from; 304 | 305 | public String _to; 306 | 307 | public BigInteger _value; 308 | } 309 | 310 | public static class ApprovalEventResponse { 311 | public Log log; 312 | 313 | public String _owner; 314 | 315 | public String _spender; 316 | 317 | public BigInteger _value; 318 | } 319 | } 320 | -------------------------------------------------------------------------------- /src/main/resources/config/application.yml: -------------------------------------------------------------------------------- 1 | # Port to run on 2 | server: 3 | port: ${port:8081} 4 | 5 | # Our log file path and name 6 | logging: 7 | file: logs/erc20-rest-service.log 8 | 9 | # Endpoint of an Ethereum or Quorum node we wish to use. 10 | # To use IPC simply provide a file path to the socket, such as /path/to/geth.ipc 11 | nodeEndpoint: http://localhost:22000 12 | 13 | # The Ethereum or Quorum address we wish to use when transacting. 14 | # Note - this address must be already unlocked in the client 15 | fromAddress: "0xed9d02e382b34818e88b88a309c7fe71e65f419d" 16 | -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 33 | 34 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/main/resources/solidity/contract/HumanStandardToken.sol: -------------------------------------------------------------------------------- 1 | /* 2 | 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. 3 | 4 | In other words. This is intended for deployment in something like a Token Factory or Mist wallet, and then used by humans. 5 | Imagine coins, currencies, shares, voting weight, etc. 6 | Machine-based, rapid creation of many tokens would not necessarily need these extra features or will be minted in other manners. 7 | 8 | 1) Initial Finite Supply (upon creation one specifies how much is minted). 9 | 2) In the absence of a token registry: Optional Decimal, Symbol & Name. 10 | 3) Optional approveAndCall() functionality to notify a contract if an approval() has occurred. 11 | 12 | .*/ 13 | 14 | import "./StandardToken.sol"; 15 | 16 | pragma solidity ^0.5.2; 17 | 18 | contract HumanStandardToken is StandardToken { 19 | 20 | function () external { 21 | //if ether is sent to this address, send it back. 22 | revert(); 23 | } 24 | 25 | /* Public variables of the token */ 26 | 27 | /* 28 | NOTE: 29 | The following variables are OPTIONAL vanities. One does not have to include them. 30 | They allow one to customise the token contract & in no way influences the core functionality. 31 | Some wallets/interfaces might not even bother to look at this information. 32 | */ 33 | string public name; //fancy name: eg Simon Bucks 34 | 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. 35 | string public symbol; //An identifier: eg SBX 36 | string public version = 'H0.1'; //human 0.1 standard. Just an arbitrary versioning scheme. 37 | 38 | constructor( 39 | uint256 _initialAmount, 40 | string memory _tokenName, 41 | uint8 _decimalUnits, 42 | string memory _tokenSymbol 43 | ) public { 44 | balances[msg.sender] = _initialAmount; // Give the creator all initial tokens 45 | totalSupply = _initialAmount; // Update total supply 46 | name = _tokenName; // Set the name for display purposes 47 | decimals = _decimalUnits; // Amount of decimals for display purposes 48 | symbol = _tokenSymbol; // Set the symbol for display purposes 49 | } 50 | 51 | /* Approves and then calls the receiving contract */ 52 | function approveAndCall(address _spender, uint256 _value, bytes calldata _extraData) external returns (bool success) { 53 | allowed[msg.sender][_spender] = _value; 54 | emit Approval(msg.sender, _spender, _value); 55 | 56 | //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. 57 | //receiveApproval(address _from, uint256 _value, address _tokenContract, bytes _extraData) 58 | //it is assumed that when does this that the call *should* succeed, otherwise one would use vanilla approve instead. 59 | (bool success, ) = _spender.call(abi.encode("receiveApproval(address,uint256,address,bytes)")); 60 | 61 | if(!success) { revert(); } 62 | return true; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main/resources/solidity/contract/HumanStandardTokenFactory.sol: -------------------------------------------------------------------------------- 1 | import "./HumanStandardToken.sol"; 2 | 3 | pragma solidity ^0.5.2; 4 | 5 | contract HumanStandardTokenFactory { 6 | 7 | mapping(address => address[]) public created; 8 | mapping(address => bool) public isHumanToken; //verify without having to do a bytecode check. 9 | bytes public humanStandardByteCode; 10 | 11 | function HumanStandardTokenFactory() { 12 | //upon creation of the factory, deploy a HumanStandardToken (parameters are meaningless) and store the bytecode provably. 13 | address verifiedToken = createHumanStandardToken(10000, "Verify Token", 3, "VTX"); 14 | humanStandardByteCode = codeAt(verifiedToken); 15 | } 16 | 17 | //verifies if a contract that has been deployed is a Human Standard Token. 18 | //NOTE: This is a very expensive function, and should only be used in an eth_call. ~800k gas 19 | function verifyHumanStandardToken(address _tokenContract) returns (bool) { 20 | bytes memory fetchedTokenByteCode = codeAt(_tokenContract); 21 | 22 | if (fetchedTokenByteCode.length != humanStandardByteCode.length) { 23 | return false; //clear mismatch 24 | } 25 | 26 | //starting iterating through it if lengths match 27 | for (uint i = 0; i < fetchedTokenByteCode.length; i ++) { 28 | if (fetchedTokenByteCode[i] != humanStandardByteCode[i]) { 29 | return false; 30 | } 31 | } 32 | 33 | return true; 34 | } 35 | 36 | //for now, keeping this internal. Ideally there should also be a live version of this that any contract can use, lib-style. 37 | //retrieves the bytecode at a specific address. 38 | function codeAt(address _addr) internal returns (bytes o_code) { 39 | assembly { 40 | // retrieve the size of the code, this needs assembly 41 | let size := extcodesize(_addr) 42 | // allocate output byte array - this could also be done without assembly 43 | // by using o_code = new bytes(size) 44 | o_code := mload(0x40) 45 | // new "memory end" including padding 46 | mstore(0x40, add(o_code, and(add(add(size, 0x20), 0x1f), not(0x1f)))) 47 | // store length in memory 48 | mstore(o_code, size) 49 | // actually retrieve the code, this needs assembly 50 | extcodecopy(_addr, add(o_code, 0x20), 0, size) 51 | } 52 | } 53 | 54 | function createHumanStandardToken(uint256 _initialAmount, string _name, uint8 _decimals, string _symbol) returns (address) { 55 | 56 | HumanStandardToken newToken = (new HumanStandardToken(_initialAmount, _name, _decimals, _symbol)); 57 | created[msg.sender].push(address(newToken)); 58 | isHumanToken[address(newToken)] = true; 59 | newToken.transfer(msg.sender, _initialAmount); //the factory will own the created tokens. You must transfer them. 60 | return address(newToken); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/resources/solidity/contract/StandardToken.sol: -------------------------------------------------------------------------------- 1 | /* 2 | You should inherit from StandardToken or, for a token like you would want to 3 | deploy in something like Mist, see HumanStandardToken.sol. 4 | (This implements ONLY the standard functions and NOTHING else. 5 | If you deploy this, you won't have anything useful.) 6 | 7 | Implements ERC 20 Token standard: https://github.com/ethereum/EIPs/issues/20 8 | .*/ 9 | pragma solidity ^0.5.2; 10 | 11 | import "./Token.sol"; 12 | 13 | contract StandardToken is Token { 14 | 15 | function transfer(address _to, uint256 _value) public returns (bool success) { 16 | //Default assumes totalSupply can't be over max (2^256 - 1). 17 | //If your token leaves out totalSupply and can issue more tokens as time goes on, you need to check if it doesn't wrap. 18 | //Replace the if with this one instead. 19 | //if (balances[msg.sender] >= _value && balances[_to] + _value > balances[_to]) { 20 | if (balances[msg.sender] >= _value && _value > 0) { 21 | balances[msg.sender] -= _value; 22 | balances[_to] += _value; 23 | emit Transfer(msg.sender, _to, _value); 24 | return true; 25 | } else { return false; } 26 | } 27 | 28 | function transferFrom(address _from, address _to, uint256 _value) public returns (bool success) { 29 | //same as above. Replace this line with the following if you want to protect against wrapping uints. 30 | //if (balances[_from] >= _value && allowed[_from][msg.sender] >= _value && balances[_to] + _value > balances[_to]) { 31 | if (balances[_from] >= _value && allowed[_from][msg.sender] >= _value && _value > 0) { 32 | balances[_to] += _value; 33 | balances[_from] -= _value; 34 | allowed[_from][msg.sender] -= _value; 35 | emit Transfer(_from, _to, _value); 36 | return true; 37 | } else { return false; } 38 | } 39 | 40 | function balanceOf(address _owner) public view returns (uint256 balance) { 41 | return balances[_owner]; 42 | } 43 | 44 | function approve(address _spender, uint256 _value) public returns (bool success) { 45 | allowed[msg.sender][_spender] = _value; 46 | emit Approval(msg.sender, _spender, _value); 47 | return true; 48 | } 49 | 50 | function allowance(address _owner, address _spender) public view returns (uint256 remaining) { 51 | return allowed[_owner][_spender]; 52 | } 53 | 54 | mapping (address => uint256) balances; 55 | mapping (address => mapping (address => uint256)) allowed; 56 | } 57 | -------------------------------------------------------------------------------- /src/main/resources/solidity/contract/Token.sol: -------------------------------------------------------------------------------- 1 | // Abstract contract for the full ERC 20 Token standard 2 | // https://github.com/ethereum/EIPs/issues/20 3 | pragma solidity ^0.5.2; 4 | 5 | contract Token { 6 | /* This is a slight change to the ERC20 base standard. 7 | function totalSupply() constant returns (uint256 supply); 8 | is replaced with: 9 | uint256 public totalSupply; 10 | This automatically creates a getter function for the totalSupply. 11 | This is moved to the base contract since public getter functions are not 12 | currently recognised as an implementation of the matching abstract 13 | function by the compiler. 14 | */ 15 | /// total amount of tokens 16 | uint256 public totalSupply; 17 | 18 | /// @param _owner The address from which the balance will be retrieved 19 | /// @return The balance 20 | function balanceOf(address _owner) public view returns (uint256 balance); 21 | 22 | /// @notice send `_value` token to `_to` from `msg.sender` 23 | /// @param _to The address of the recipient 24 | /// @param _value The amount of token to be transferred 25 | /// @return Whether the transfer was successful or not 26 | function transfer(address _to, uint256 _value) public returns (bool success); 27 | 28 | /// @notice send `_value` token to `_to` from `_from` on the condition it is approved by `_from` 29 | /// @param _from The address of the sender 30 | /// @param _to The address of the recipient 31 | /// @param _value The amount of token to be transferred 32 | /// @return Whether the transfer was successful or not 33 | function transferFrom(address _from, address _to, uint256 _value) public returns (bool success); 34 | 35 | /// @notice `msg.sender` approves `_spender` to spend `_value` tokens 36 | /// @param _spender The address of the account able to transfer the tokens 37 | /// @param _value The amount of tokens to be approved for transfer 38 | /// @return Whether the approval was successful or not 39 | function approve(address _spender, uint256 _value) public returns (bool success); 40 | 41 | /// @param _owner The address of the account owning tokens 42 | /// @param _spender The address of the account able to transfer the tokens 43 | /// @return Amount of remaining tokens allowed to spent 44 | function allowance(address _owner, address _spender) public view returns (uint256 remaining); 45 | 46 | event Transfer(address indexed _from, address indexed _to, uint256 _value); 47 | event Approval(address indexed _owner, address indexed _spender, uint256 _value); 48 | } 49 | -------------------------------------------------------------------------------- /src/main/resources/solidity/contract/build/HumanStandardToken.abi: -------------------------------------------------------------------------------- 1 | [{"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"}] -------------------------------------------------------------------------------- /src/main/resources/solidity/contract/build/HumanStandardToken.bin: -------------------------------------------------------------------------------- 1 | 60c0604052600460808190527f48302e310000000000000000000000000000000000000000000000000000000060a090815261003e916006919061016b565b5034801561004b57600080fd5b50604051610a4f380380610a4f8339810180604052608081101561006e57600080fd5b81516020830180519193928301929164010000000081111561008f57600080fd5b820160208101848111156100a257600080fd5b81516401000000008111828201871017156100bc57600080fd5b505060208201516040909201805191949293916401000000008111156100e157600080fd5b820160208101848111156100f457600080fd5b815164010000000081118282018710171561010e57600080fd5b5050336000908152600160209081526040822089905590889055865191945061013e93506003925086019061016b565b506004805460ff191660ff8416179055805161016190600590602084019061016b565b5050505050610206565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f106101ac57805160ff19168380011785556101d9565b828001600101855582156101d9579182015b828111156101d95782518255916020019190600101906101be565b506101e59291506101e9565b5090565b61020391905b808211156101e557600081556001016101ef565b90565b61083a806102156000396000f3fe608060405234801561001057600080fd5b50600436106100c6576000357c01000000000000000000000000000000000000000000000000000000009004806354fd4d501161008e57806354fd4d50146101f657806370a08231146101fe57806395d89b4114610224578063a9059cbb1461022c578063cae9ca5114610258578063dd62ed3e146102dd576100c6565b806306fdde03146100cb578063095ea7b31461014857806318160ddd1461018857806323b872dd146101a2578063313ce567146101d8575b600080fd5b6100d361030b565b6040805160208082528351818301528351919283929083019185019080838360005b8381101561010d5781810151838201526020016100f5565b50505050905090810190601f16801561013a5780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b6101746004803603604081101561015e57600080fd5b50600160a060020a038135169060200135610399565b604080519115158252519081900360200190f35b610190610400565b60408051918252519081900360200190f35b610174600480360360608110156101b857600080fd5b50600160a060020a03813581169160208101359091169060400135610406565b6101e06104f3565b6040805160ff9092168252519081900360200190f35b6100d36104fc565b6101906004803603602081101561021457600080fd5b5035600160a060020a0316610557565b6100d3610572565b6101746004803603604081101561024257600080fd5b50600160a060020a0381351690602001356105cd565b6101746004803603606081101561026e57600080fd5b600160a060020a038235169160208101359181019060608101604082013564010000000081111561029e57600080fd5b8201836020820111156102b057600080fd5b803590602001918460018302840111640100000000831117156102d257600080fd5b509092509050610666565b610190600480360360408110156102f357600080fd5b50600160a060020a03813581169160200135166107b5565b6003805460408051602060026001851615610100026000190190941693909304601f810184900484028201840190925281815292918301828280156103915780601f1061036657610100808354040283529160200191610391565b820191906000526020600020905b81548152906001019060200180831161037457829003601f168201915b505050505081565b336000818152600260209081526040808320600160a060020a038716808552908352818420869055815186815291519394909390927f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925928290030190a35060015b92915050565b60005481565b600160a060020a03831660009081526001602052604081205482118015906104515750600160a060020a03841660009081526002602090815260408083203384529091529020548211155b801561045d5750600082115b156104e857600160a060020a03808416600081815260016020908152604080832080548801905593881680835284832080548890039055600282528483203384528252918490208054879003905583518681529351929391927fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef9281900390910190a35060016104ec565b5060005b9392505050565b60045460ff1681565b6006805460408051602060026001851615610100026000190190941693909304601f810184900484028201840190925281815292918301828280156103915780601f1061036657610100808354040283529160200191610391565b600160a060020a031660009081526001602052604090205490565b6005805460408051602060026001851615610100026000190190941693909304601f810184900484028201840190925281815292918301828280156103915780601f1061036657610100808354040283529160200191610391565b3360009081526001602052604081205482118015906105ec5750600082115b1561065e5733600081815260016020908152604080832080548790039055600160a060020a03871680845292819020805487019055805186815290519293927fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef929181900390910190a35060016103fa565b5060006103fa565b336000818152600260209081526040808320600160a060020a038916808552908352818420889055815188815291519394909390927f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925928290030190a3600085600160a060020a031660405160200180806020018281038252602e8152602001806107e1602e91396040019150506040516020818303038152906040526040518082805190602001908083835b602083106107325780518252601f199092019160209182019101610713565b6001836020036101000a0380198251168184511680821785525050505050509050019150506000604051808303816000865af19150503d8060008114610794576040519150601f19603f3d011682016040523d82523d6000602084013e610799565b606091505b505090508015156107a957600080fd5b50600195945050505050565b600160a060020a0391821660009081526002602090815260408083209390941682529190915220549056fe72656365697665417070726f76616c28616464726573732c75696e743235362c616464726573732c627974657329a165627a7a72305820d15a070a95051e159632a5f42da17cdc0b4e940c8c7574a86370e2f434405abd0029 -------------------------------------------------------------------------------- /src/main/resources/solidity/contract/build/StandardToken.abi: -------------------------------------------------------------------------------- 1 | [{"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"}] -------------------------------------------------------------------------------- /src/main/resources/solidity/contract/build/StandardToken.bin: -------------------------------------------------------------------------------- 1 | 608060405234801561001057600080fd5b506103f8806100206000396000f3fe608060405234801561001057600080fd5b506004361061007e577c01000000000000000000000000000000000000000000000000000000006000350463095ea7b3811461008357806318160ddd146100c357806323b872dd146100dd57806370a0823114610113578063a9059cbb14610139578063dd62ed3e14610165575b600080fd5b6100af6004803603604081101561009957600080fd5b50600160a060020a038135169060200135610193565b604080519115158252519081900360200190f35b6100cb6101fa565b60408051918252519081900360200190f35b6100af600480360360608110156100f357600080fd5b50600160a060020a03813581169160208101359091169060400135610200565b6100cb6004803603602081101561012957600080fd5b5035600160a060020a03166102ed565b6100af6004803603604081101561014f57600080fd5b50600160a060020a038135169060200135610308565b6100cb6004803603604081101561017b57600080fd5b50600160a060020a03813581169160200135166103a1565b336000818152600260209081526040808320600160a060020a038716808552908352818420869055815186815291519394909390927f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925928290030190a35060015b92915050565b60005481565b600160a060020a038316600090815260016020526040812054821180159061024b5750600160a060020a03841660009081526002602090815260408083203384529091529020548211155b80156102575750600082115b156102e257600160a060020a03808416600081815260016020908152604080832080548801905593881680835284832080548890039055600282528483203384528252918490208054879003905583518681529351929391927fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef9281900390910190a35060016102e6565b5060005b9392505050565b600160a060020a031660009081526001602052604090205490565b3360009081526001602052604081205482118015906103275750600082115b156103995733600081815260016020908152604080832080548790039055600160a060020a03871680845292819020805487019055805186815290519293927fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef929181900390910190a35060016101f4565b5060006101f4565b600160a060020a0391821660009081526002602090815260408083209390941682529190915220549056fea165627a7a723058205e019d486f4295d7ad9c656e9eaaf88e838fde74f0d8a64e8481887eb9585e8c0029 -------------------------------------------------------------------------------- /src/main/resources/solidity/contract/build/Token.abi: -------------------------------------------------------------------------------- 1 | [{"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"}] -------------------------------------------------------------------------------- /src/main/resources/solidity/contract/build/Token.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/web3labs/erc20-rest-service/b7e1933e6c21699e16e2cfdff5f25b4adb1d9a89/src/main/resources/solidity/contract/build/Token.bin -------------------------------------------------------------------------------- /src/test/java/io/blk/erc20/ControllerIT.java: -------------------------------------------------------------------------------- 1 | package io.blk.erc20; 2 | 3 | import java.math.BigInteger; 4 | import java.util.Arrays; 5 | 6 | import org.junit.Ignore; 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.boot.test.context.SpringBootTest; 11 | import org.springframework.boot.test.web.client.TestRestTemplate; 12 | import org.springframework.http.HttpEntity; 13 | import org.springframework.http.HttpHeaders; 14 | import org.springframework.http.HttpStatus; 15 | import org.springframework.http.MediaType; 16 | import org.springframework.http.ResponseEntity; 17 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 18 | 19 | import static junit.framework.TestCase.assertNull; 20 | import static org.hamcrest.CoreMatchers.is; 21 | import static org.junit.Assert.assertFalse; 22 | import static org.junit.Assert.assertNotNull; 23 | import static org.junit.Assert.assertThat; 24 | import static org.junit.Assert.assertTrue; 25 | 26 | @RunWith(SpringJUnit4ClassRunner.class) 27 | @SpringBootTest(webEnvironment= SpringBootTest.WebEnvironment.RANDOM_PORT) 28 | public class ControllerIT { 29 | 30 | // key2 31 | private static final String OTHER_ACCOUNT = "ca843569e3427144cead5e4d5999a3d0ccf92b8e"; 32 | // Transaction manager 2 33 | private static final String PRIVATE_FOR = "QfeDAys9MPDs2XHExtc84jKGHxZg/aj52DTh0vtA3Xc="; 34 | 35 | @Autowired 36 | private NodeConfiguration nodeConfiguration; 37 | 38 | @Autowired 39 | private TestRestTemplate restTemplate; 40 | 41 | 42 | 43 | @Test 44 | public void testConfig() { 45 | ResponseEntity responseEntity = 46 | this.restTemplate.getForEntity("/config", NodeConfiguration.class); 47 | verifyHttpStatus(responseEntity); 48 | assertNotNull(responseEntity.getBody()); 49 | } 50 | 51 | @Ignore 52 | @Test 53 | public void testLifeCycle() { 54 | Controller.ContractSpecification contractSpecification = 55 | new Controller.ContractSpecification( 56 | BigInteger.valueOf(1000000), "Quorum Token", BigInteger.valueOf(25), "QT"); 57 | 58 | String contractAddress = deploy(contractSpecification); 59 | 60 | verifyName(contractAddress, contractSpecification.getTokenName()); 61 | verifySymbol(contractAddress, contractSpecification.getTokenSymbol()); 62 | verifyDecimals(contractAddress, contractSpecification.getDecimalUnits()); 63 | verifyVersion(contractAddress, "H0.1"); 64 | 65 | verifyTotalSupply(contractAddress, contractSpecification.getInitialAmount()); 66 | verifyBalanceOf(contractAddress, 67 | nodeConfiguration.getFromAddress(), contractSpecification.getInitialAmount()); 68 | 69 | Controller.ApproveRequest approveRequest = new Controller.ApproveRequest( 70 | OTHER_ACCOUNT, BigInteger.valueOf(10000)); 71 | verifyApproveTx(contractAddress, approveRequest); 72 | 73 | verifyAllowance( 74 | contractAddress, nodeConfiguration.getFromAddress(), OTHER_ACCOUNT, 75 | approveRequest.getValue()); 76 | 77 | Controller.TransferRequest transferRequest = new Controller.TransferRequest( 78 | OTHER_ACCOUNT, BigInteger.valueOf(10000)); 79 | verifyTransferTx(contractAddress, transferRequest); 80 | verifyBalanceOf( 81 | contractAddress, 82 | transferRequest.getTo(), 83 | transferRequest.getValue()); 84 | verifyBalanceOf( 85 | contractAddress, 86 | nodeConfiguration.getFromAddress(), 87 | contractSpecification.getInitialAmount().subtract(transferRequest.getValue())); 88 | 89 | // Needs to be performed by another account, hence this will fail 90 | Controller.TransferFromRequest transferFromRequest = 91 | new Controller.TransferFromRequest( 92 | nodeConfiguration.getFromAddress(), OTHER_ACCOUNT, BigInteger.valueOf(1000)); 93 | verifyTransferFromTxFailure(contractAddress, transferFromRequest); 94 | // Therefore our balance remains the same 95 | verifyBalanceOf( 96 | contractAddress, 97 | transferFromRequest.getFrom(), 98 | contractSpecification.getInitialAmount().subtract(transferRequest.getValue())); 99 | } 100 | 101 | private String deploy( 102 | Controller.ContractSpecification contractSpecification) { 103 | 104 | ResponseEntity responseEntity = 105 | this.restTemplate.postForEntity( 106 | "/deploy", buildEntity(contractSpecification), String.class); 107 | verifyHttpStatus(responseEntity); 108 | 109 | String contractAddress = responseEntity.getBody(); 110 | assertFalse(contractAddress.isEmpty()); 111 | return contractAddress; 112 | } 113 | 114 | private void verifyName(String contractAddress, String name) { 115 | ResponseEntity responseEntity = 116 | this.restTemplate.getForEntity( 117 | "/" + contractAddress + "" + "/name", String.class); 118 | verifyHttpStatus(responseEntity); 119 | assertThat(responseEntity.getBody(), is(name)); 120 | } 121 | 122 | private void verifyTotalSupply(String contractAddress, BigInteger totalSupply) { 123 | ResponseEntity responseEntity = 124 | this.restTemplate.getForEntity( 125 | "/" + contractAddress + "" + "/totalSupply", BigInteger.class); 126 | verifyHttpStatus(responseEntity); 127 | assertThat(responseEntity.getBody(), is(totalSupply)); 128 | } 129 | 130 | private void verifyDecimals(String contractAddress, BigInteger decimals) { 131 | ResponseEntity responseEntity = 132 | this.restTemplate.getForEntity( 133 | "/" + contractAddress + "" + "/decimals", BigInteger.class); 134 | verifyHttpStatus(responseEntity); 135 | assertThat(responseEntity.getBody(), is(decimals)); 136 | } 137 | 138 | private void verifyVersion(String contractAddress, String version) { 139 | ResponseEntity responseEntity = 140 | this.restTemplate.getForEntity( 141 | "/" + contractAddress + "" + "/version", String.class); 142 | verifyHttpStatus(responseEntity); 143 | assertThat(responseEntity.getBody(), is(version)); 144 | } 145 | 146 | private void verifyBalanceOf(String contractAddress, String ownerAddress, BigInteger balance) { 147 | ResponseEntity responseEntity = 148 | this.restTemplate.getForEntity( 149 | "/" + contractAddress + "" + "/balanceOf/" + ownerAddress, 150 | BigInteger.class); 151 | verifyHttpStatus(responseEntity); 152 | assertThat(responseEntity.getBody(), is(balance)); 153 | } 154 | 155 | private void verifySymbol(String contractAddress, String symbol) { 156 | ResponseEntity responseEntity = 157 | this.restTemplate.getForEntity( 158 | "/" + contractAddress + "" + "/symbol", String.class); 159 | verifyHttpStatus(responseEntity); 160 | assertThat(responseEntity.getBody(), is(symbol)); 161 | } 162 | 163 | private void verifyAllowance( 164 | String contractAddress, 165 | String ownerAddress, 166 | String spenderAddress, 167 | BigInteger expected) { 168 | ResponseEntity responseEntity = 169 | this.restTemplate.getForEntity( 170 | "/" + contractAddress 171 | + "/allowance?" 172 | + "ownerAddress={ownerAddress}" 173 | + "&spenderAddress={spenderAddress}", 174 | BigInteger.class, 175 | ownerAddress, 176 | spenderAddress); 177 | verifyHttpStatus(responseEntity); 178 | assertThat(responseEntity.getBody(), is(expected)); 179 | } 180 | 181 | private void verifyTransferFromTxFailure( 182 | String contractAddress, Controller.TransferFromRequest transferFromRequest) { 183 | ResponseEntity responseEntity = 184 | this.restTemplate.postForEntity( 185 | "/" + contractAddress + "/transferFrom", 186 | buildEntity(transferFromRequest), 187 | TransactionResponse.class); 188 | verifyPostResponseFailure(responseEntity); 189 | } 190 | 191 | private void verifyApproveTx( 192 | String contractAddress, Controller.ApproveRequest approveRequest) { 193 | ResponseEntity responseEntity = 194 | this.restTemplate.postForEntity( 195 | "/" + contractAddress + "/approve", 196 | buildEntity(approveRequest), 197 | TransactionResponse.class); 198 | verifyPostResponse(responseEntity); 199 | } 200 | 201 | private void verifyTransferTx( 202 | String contractAddress, Controller.TransferRequest transferRequest) { 203 | ResponseEntity responseEntity = 204 | this.restTemplate.postForEntity( 205 | "/" + contractAddress + "/transfer", 206 | buildEntity(transferRequest), 207 | TransactionResponse.class); 208 | verifyPostResponse(responseEntity); 209 | } 210 | 211 | private HttpEntity buildEntity(T body) { 212 | HttpHeaders headers = new HttpHeaders(); 213 | headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON)); 214 | headers.setContentType(MediaType.APPLICATION_JSON); 215 | headers.add("privateFor", PRIVATE_FOR); 216 | 217 | return new HttpEntity<>(body, headers); 218 | } 219 | 220 | private void verifyHttpStatus(ResponseEntity responseEntity) { 221 | assertThat(responseEntity.getStatusCode(), is(HttpStatus.OK)); 222 | } 223 | 224 | private void verifyPostResponse(ResponseEntity responseEntity) { 225 | verifyPostResponseBody(responseEntity); 226 | assertNotNull(responseEntity.getBody().getEvent()); 227 | } 228 | 229 | private void verifyPostResponseFailure(ResponseEntity responseEntity) { 230 | assertNull(responseEntity.getBody().getEvent()); 231 | } 232 | 233 | private void verifyPostResponseBody(ResponseEntity responseEntity) { 234 | verifyHttpStatus(responseEntity); 235 | TransactionResponse body = responseEntity.getBody(); 236 | assertNotNull(body); 237 | String transactionHash = body.getTransactionHash(); 238 | assertTrue(transactionHash.startsWith("0x")); 239 | assertThat(transactionHash.length(), is(66)); 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | --------------------------------------------------------------------------------