diff --git a/.github/workflows/cicd.yaml b/.github/workflows/cicd.yaml new file mode 100644 index 0000000..6251e27 --- /dev/null +++ b/.github/workflows/cicd.yaml @@ -0,0 +1,199 @@ +name: Create and publish a container image, update helm chart 'appVersion' + +on: + push: + branches: ["main", "develop"] + +############################################# +# +# Branch +# - develop > GitHub packages +# - main > Amazon ECR +# +############################################# + +jobs: + develop: + ### Reference + # https://docs.github.com/ko/actions/publishing-packages/publishing-docker-images#github-packages%EC%97%90-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EA%B2%8C%EC%8B%9C + ### + + if: github.ref == 'refs/heads/develop' + name: Build and Push Container Image to GitHub Container Registry + runs-on: ubuntu-latest + env: + REPOSITORY: admin + ENVIRONMENT: dev + permissions: + contents: read + packages: write + attestations: write + id-token: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Log in to the GitHub container registry + uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) for Container image + id: meta + uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7 + with: + images: ghcr.io/${{ github.repository }} + tags: type=sha + + - name: Set up JDK 21 + uses: actions/setup-java@v2 + with: + distribution: 'adopt' + java-version: '21' + + - name: Build JAR + run: ./gradlew clean build -x test + + - name: Build and push Docker image + id: push + uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + + # ### Error + # # [message] Failed to persist attestation + # # Feature not available for the NTF-marketplace organization. + # # To enable this feature, please upgrade the billing plan, or make this repository public. + # # https://docs.github.com/rest/repos/repos#create-an-attestation + # ### + # - name: Generate artifact attestation + # uses: actions/attest-build-provenance@v1 + # with: + # subject-name: ${{ env.REGISTRY }}/${{ github.repository }} + # subject-digest: ${{ steps.push.outputs.digest }} + # push-to-registry: true + + - name: Checkout Private Repository + uses: actions/checkout@v4 + with: + repository: NTF-marketplace/devops + fetch-depth: 0 + ref: develop + token: ${{ secrets.PAT }} + + - name: Replace image tag in helm values.yaml + uses: mikefarah/yq@master + env: + IMAGE_VERSION: ${{ steps.meta.outputs.version }} + with: + cmd: yq eval -i '.image.tag = env(IMAGE_VERSION)' 'chart/${{ env.REPOSITORY }}_${{ env.ENVIRONMENT }}/values.yaml' + + - name: Commit helm chart changes + env: + IMAGE_VERSION: ${{ steps.meta.outputs.version }} + run: | + cd chart/${{ env.REPOSITORY }}_${{ env.ENVIRONMENT }} + git config --global user.email "hun5879@naver.com" + git config --global user.name "dongdorrong" + + git add values.yaml + git commit --message "ci: update ${{ env.REPOSITORY }}_${{ env.ENVIRONMENT }} image tag to $IMAGE_VERSION" + + - name: Push commit + uses: ad-m/github-push-action@master + with: + github_token: ${{ secrets.PAT }} + repository: NTF-marketplace/devops + branch: develop + + main: + if: github.ref == 'refs/heads/main' + name: Build and Push Container Image to Amazon ECR + runs-on: ubuntu-latest + env: + REPOSITORY: admin + ENVIRONMENT: prod + permissions: + contents: read + packages: write + attestations: write + id-token: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ secrets.AWS_DEFAULT_REGION }} + + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v2 + + - name: Extract metadata (tags, labels) for Container image + id: meta + uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7 + with: + images: ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.${{ secrets.AWS_DEFAULT_REGION }}.amazonaws.com/${{ env.REPOSITORY }}_${{ env.ENVIRONMENT }} + tags: type=sha + + - name: Set up JDK 21 + uses: actions/setup-java@v2 + with: + distribution: 'adopt' + java-version: '21' + + - name: Build JAR + run: ./gradlew clean build -x test + + - name: Build and push Docker image + id: push + uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + + - name: Checkout Private Repository + uses: actions/checkout@v4 + with: + repository: NTF-marketplace/devops + fetch-depth: 0 + ref: develop + token: ${{ secrets.PAT }} + + - name: Replace image tag in helm values.yaml + uses: mikefarah/yq@master + env: + IMAGE_VERSION: ${{ steps.meta.outputs.version }} + with: + cmd: yq eval -i '.image.tag = env(IMAGE_VERSION)' 'chart/${{ env.REPOSITORY }}_${{ env.ENVIRONMENT }}/values.yaml' + + - name: Commit helm chart changes + env: + IMAGE_VERSION: ${{ steps.meta.outputs.version }} + run: | + cd chart/${{ env.REPOSITORY }}_${{ env.ENVIRONMENT }} + git config --global user.email "hun5879@naver.com" + git config --global user.name "dongdorrong" + + git add values.yaml + git commit --message "ci: update ${{ env.REPOSITORY }}_${{ env.ENVIRONMENT }} image tag to $IMAGE_VERSION" + + - name: Push commit + uses: ad-m/github-push-action@master + with: + github_token: ${{ secrets.PAT }} + repository: NTF-marketplace/devops + branch: develop \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1d0f2a8 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,9 @@ +FROM amazoncorretto:21-alpine + +COPY build/libs/*.jar /app.jar + +RUN apk update && apk upgrade && \ + # apk add --no-cache && \ + rm -rf /var/cache/apk/* + +ENTRYPOINT ["java", "-jar", "app.jar"] \ No newline at end of file diff --git a/Web3App/build.gradle b/Web3App/build.gradle new file mode 100644 index 0000000..2555420 --- /dev/null +++ b/Web3App/build.gradle @@ -0,0 +1,96 @@ +plugins { + id 'java' + id 'org.jetbrains.kotlin.jvm' version '1.8.10' + id 'application' + id "com.github.johnrengelman.shadow" version "5.2.0" + id 'org.web3j' version '4.11.2' +} + + +group 'org.web3j' +version '0.1.0' + +sourceCompatibility = 17 + +repositories { + mavenLocal() + mavenCentral() + maven { url "https://hyperledger.jfrog.io/hyperledger/besu-maven" } + maven { url "https://artifacts.consensys.net/public/maven/maven/" } + maven { url "https://splunk.jfrog.io/splunk/ext-releases-local" } +} + +web3j { + generatedPackageName = 'org.web3j.generated.contracts' + excludedContracts = ['Mortal'] +} + +node { + nodeProjectDir.set(file("$projectDir")) +} + +ext { + web3jVersion = '4.11.2' + logbackVersion = '1.4.14' + klaxonVersion = '5.5' + besuPluginVersion = '24.1.1' + besuInternalVersion = '24.1.1' + besuInternalCryptoVersion = '23.1.3' + besuCryptoDepVersion = '0.8.3' +} + +dependencies { + implementation "org.web3j:core:$web3jVersion", + "ch.qos.logback:logback-core:$logbackVersion", + "ch.qos.logback:logback-classic:$logbackVersion", + "com.beust:klaxon:$klaxonVersion" + implementation "org.web3j:web3j-unit:$web3jVersion" + implementation "org.web3j:web3j-evm:$web3jVersion" + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.3.1' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.3.1' + + implementation "org.hyperledger.besu:plugin-api:$besuPluginVersion" + implementation "org.hyperledger.besu.internal:besu:$besuInternalVersion" + implementation "org.hyperledger.besu.internal:api:$besuInternalVersion" + implementation "org.hyperledger.besu:evm:$besuPluginVersion" + implementation "org.hyperledger.besu.internal:config:$besuInternalVersion" + implementation "org.hyperledger.besu.internal:core:$besuInternalVersion" + implementation "org.hyperledger.besu.internal:crypto:$besuInternalCryptoVersion" + implementation "org.hyperledger.besu.internal:rlp:$besuInternalVersion" + implementation "org.hyperledger.besu.internal:kvstore:$besuInternalVersion" + implementation "org.hyperledger.besu.internal:metrics-core:$besuInternalVersion" + implementation "org.hyperledger.besu.internal:trie:$besuInternalVersion" + implementation "org.hyperledger.besu.internal:util:$besuInternalVersion" + implementation "org.hyperledger.besu:bls12-381:$besuCryptoDepVersion" + implementation "org.hyperledger.besu:secp256k1:$besuCryptoDepVersion" +} + +jar { + manifest { + attributes( + 'Main-Class': 'org.web3j.Web3App', + 'Multi-Release':'true' + ) + } +} + +application { + mainClassName = 'org.web3j.Web3App' +} + +test { + useJUnitPlatform() +} + +compileKotlin { + kotlinOptions.jvmTarget = "17" +} + +compileTestKotlin { + kotlinOptions.jvmTarget = "17" +} + +shadowJar { + zip64 = true +} diff --git a/Web3App/gradle/wrapper/gradle-wrapper.jar b/Web3App/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..5c2d1cf Binary files /dev/null and b/Web3App/gradle/wrapper/gradle-wrapper.jar differ diff --git a/Web3App/gradle/wrapper/gradle-wrapper.properties b/Web3App/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..070cb70 --- /dev/null +++ b/Web3App/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/Web3App/gradlew b/Web3App/gradlew new file mode 100755 index 0000000..f48d5d0 --- /dev/null +++ b/Web3App/gradlew @@ -0,0 +1,170 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/Web3App/gradlew.bat b/Web3App/gradlew.bat new file mode 100644 index 0000000..f955316 --- /dev/null +++ b/Web3App/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/Web3App/settings.gradle b/Web3App/settings.gradle new file mode 100644 index 0000000..a88c03a --- /dev/null +++ b/Web3App/settings.gradle @@ -0,0 +1,10 @@ +pluginManagement { + repositories { + mavenLocal() + gradlePluginPortal() + maven { url "https://hyperledger.jfrog.io/hyperledger/besu-maven" } + maven { url "https://artifacts.consensys.net/public/maven/maven/" } + maven { url "https://splunk.jfrog.io/splunk/ext-releases-local" } + } +} +rootProject.name = 'Web3App'; diff --git a/Web3App/src/main/java/org/web3j/Web3App.java b/Web3App/src/main/java/org/web3j/Web3App.java new file mode 100644 index 0000000..73a9270 --- /dev/null +++ b/Web3App/src/main/java/org/web3j/Web3App.java @@ -0,0 +1,31 @@ +package org.web3j; + +import org.web3j.crypto.Credentials; +import org.web3j.crypto.WalletUtils; +import org.web3j.protocol.Web3j; +import org.web3j.protocol.http.HttpService; +import org.web3j.tx.gas.DefaultGasProvider; + +import org.web3j.generated.contracts.HelloWorld; + +/** + *

This is the generated class for web3j new helloworld

+ *

It deploys the Hello World contract in src/main/solidity/ and prints its address

+ *

For more information on how to run this project, please refer to our documentation

+ */ +public class Web3App { + + private static final String nodeUrl = System.getenv().getOrDefault("WEB3J_NODE_URL", ""); + private static final String walletPassword = System.getenv().getOrDefault("WEB3J_WALLET_PASSWORD", ""); + private static final String walletPath = System.getenv().getOrDefault("WEB3J_WALLET_PATH", ""); + + public static void main(String[] args) throws Exception { + Credentials credentials = WalletUtils.loadCredentials(walletPassword, walletPath); + Web3j web3j = Web3j.build(new HttpService(nodeUrl)); + System.out.println("Deploying HelloWorld contract ..."); + HelloWorld helloWorld = HelloWorld.deploy(web3j, credentials, new DefaultGasProvider(), "Hello Blockchain World!").send(); + System.out.println("Contract address: " + helloWorld.getContractAddress()); + System.out.println("Greeting method result: " + helloWorld.greeting().send()); + } +} + diff --git a/Web3App/src/main/solidity/HelloWorld.sol b/Web3App/src/main/solidity/HelloWorld.sol new file mode 100644 index 0000000..91e4d74 --- /dev/null +++ b/Web3App/src/main/solidity/HelloWorld.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.7.0; + +// Modified Greeter contract. Based on example at https://www.ethereum.org/greeter. + +contract Mortal { + /* Define variable owner of the type address*/ + address owner; + + /* this function is executed at initialization and sets the owner of the contract */ + constructor () {owner = msg.sender;} + + modifier onlyOwner { + require( + msg.sender == owner, + "Only owner can call this function." + ); + _; + } + + /* Function to recover the funds on the contract */ + function kill() onlyOwner public {selfdestruct(msg.sender);} +} + +contract HelloWorld is Mortal { + /* define variable greeting of the type string */ + string greet; + + /* this runs when the contract is executed */ + constructor (string memory _greet) { + greet = _greet; + } + + function newGreeting(string memory _greet) onlyOwner public { + emit Modified(greet, _greet, greet, _greet); + greet = _greet; + } + + /* main function */ + function greeting() public view returns (string memory) { + return greet; + } + + event Modified( + string indexed oldGreetingIdx, string indexed newGreetingIdx, + string oldGreeting, string newGreeting); +} diff --git a/build.gradle.kts b/build.gradle.kts index 8a6de2f..b4f0256 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -11,14 +11,16 @@ group = "com.api" version = "0.0.1-SNAPSHOT" java { - sourceCompatibility = JavaVersion.VERSION_17 + sourceCompatibility = JavaVersion.VERSION_21 } repositories { mavenCentral() } +extra["springCloudVersion"] = "2023.0.2" dependencies { + implementation("org.springframework.cloud:spring-cloud-starter-config") implementation("org.springframework.boot:spring-boot-starter-data-r2dbc") implementation("org.springframework.boot:spring-boot-starter-webflux") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") @@ -40,10 +42,16 @@ dependencies { testRuntimeOnly("org.junit.platform:junit-platform-launcher") } +dependencyManagement { + imports { + mavenBom("org.springframework.cloud:spring-cloud-dependencies:${property("springCloudVersion")}") + } +} + tasks.withType { kotlinOptions { freeCompilerArgs += "-Xjsr305=strict" - jvmTarget = "17" + jvmTarget = "21" } } diff --git a/logs b/logs new file mode 100644 index 0000000..7e95676 --- /dev/null +++ b/logs @@ -0,0 +1,17 @@ +Downloading https://services.gradle.org/distributions/gradle-7.6-bin.zip +.................................................................................................................... + +FAILURE: Build failed with an exception. + +* What went wrong: +Could not open cp_settings generic class cache for settings file '/Users/odudeong/IdeaProjects/admin/Web3App/settings.gradle' (/Users/odudeong/.gradle/caches/7.6/scripts/eox0kpw4vi7cwl95dqm0jolbf). +> BUG! exception in phase 'semantic analysis' in source unit '_BuildScript_' Unsupported class file major version 65 + +* Try: +> Run with --stacktrace option to get the stack trace. +> Run with --info or --debug option to get more log output. +> Run with --scan to get full insights. + +* Get more help at https://help.gradle.org + +BUILD FAILED in 7s diff --git a/src/main/java/com/api/admin/wrapper/ERC20ABI.java b/src/main/java/com/api/admin/wrapper/ERC20ABI.java new file mode 100644 index 0000000..5c678e2 --- /dev/null +++ b/src/main/java/com/api/admin/wrapper/ERC20ABI.java @@ -0,0 +1,259 @@ +package com.api.admin.wrapper; + +import io.reactivex.Flowable; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.web3j.abi.EventEncoder; +import org.web3j.abi.TypeReference; +import org.web3j.abi.datatypes.Address; +import org.web3j.abi.datatypes.Event; +import org.web3j.abi.datatypes.Function; +import org.web3j.abi.datatypes.Type; +import org.web3j.abi.datatypes.Utf8String; +import org.web3j.abi.datatypes.generated.Uint256; +import org.web3j.abi.datatypes.generated.Uint8; +import org.web3j.crypto.Credentials; +import org.web3j.protocol.Web3j; +import org.web3j.protocol.core.DefaultBlockParameter; +import org.web3j.protocol.core.RemoteFunctionCall; +import org.web3j.protocol.core.methods.request.EthFilter; +import org.web3j.protocol.core.methods.response.BaseEventResponse; +import org.web3j.protocol.core.methods.response.Log; +import org.web3j.protocol.core.methods.response.TransactionReceipt; +import org.web3j.tx.Contract; +import org.web3j.tx.TransactionManager; +import org.web3j.tx.gas.ContractGasProvider; + +/** + *

Auto generated code. + *

Do not modify! + *

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

Generated with web3j version 1.5.3. + */ +@SuppressWarnings("rawtypes") +public class ERC20ABI extends Contract { + public static final String BINARY = "Bin file was not provided"; + + public static final String FUNC_NAME = "name"; + + public static final String FUNC_APPROVE = "approve"; + + public static final String FUNC_TOTALSUPPLY = "totalSupply"; + + public static final String FUNC_DECIMALS = "decimals"; + + public static final String FUNC_SYMBOL = "symbol"; + + public static final String FUNC_TRANSFER = "transfer"; + + public static final String FUNC_BALANCEOF = "balanceOf"; + + public static final String FUNC_TRANSFERFROM = "transferFrom"; + + public static final String FUNC_ALLOWANCE = "allowance"; + + public static final Event APPROVAL_EVENT = new Event("Approval", + Arrays.>asList(new TypeReference

(true) {}, new TypeReference
(true) {}, new TypeReference() {})); + ; + + public static final Event TRANSFER_EVENT = new Event("Transfer", + Arrays.>asList(new TypeReference
(true) {}, new TypeReference
(true) {}, new TypeReference() {})); + ; + + @Deprecated + protected ERC20ABI(String contractAddress, Web3j web3j, Credentials credentials, BigInteger gasPrice, BigInteger gasLimit) { + super(BINARY, contractAddress, web3j, credentials, gasPrice, gasLimit); + } + + protected ERC20ABI(String contractAddress, Web3j web3j, Credentials credentials, ContractGasProvider contractGasProvider) { + super(BINARY, contractAddress, web3j, credentials, contractGasProvider); + } + + @Deprecated + protected ERC20ABI(String contractAddress, Web3j web3j, TransactionManager transactionManager, BigInteger gasPrice, BigInteger gasLimit) { + super(BINARY, contractAddress, web3j, transactionManager, gasPrice, gasLimit); + } + + protected ERC20ABI(String contractAddress, Web3j web3j, TransactionManager transactionManager, ContractGasProvider contractGasProvider) { + super(BINARY, contractAddress, web3j, transactionManager, contractGasProvider); + } + + public RemoteFunctionCall name() { + final Function function = new Function(FUNC_NAME, + Arrays.asList(), + Arrays.>asList(new TypeReference() {})); + return executeRemoteCallSingleValueReturn(function, String.class); + } + + public RemoteFunctionCall approve(String _spender, BigInteger _value) { + final Function function = new Function( + FUNC_APPROVE, + Arrays.asList(new org.web3j.abi.datatypes.Address(160, _spender), + new org.web3j.abi.datatypes.generated.Uint256(_value)), + Collections.>emptyList()); + return executeRemoteCallTransaction(function); + } + + public RemoteFunctionCall totalSupply() { + final Function function = new Function(FUNC_TOTALSUPPLY, + Arrays.asList(), + Arrays.>asList(new TypeReference() {})); + return executeRemoteCallSingleValueReturn(function, BigInteger.class); + } + + public RemoteFunctionCall decimals() { + final Function function = new Function(FUNC_DECIMALS, + Arrays.asList(), + Arrays.>asList(new TypeReference() {})); + return executeRemoteCallSingleValueReturn(function, BigInteger.class); + } + + public RemoteFunctionCall symbol() { + final Function function = new Function(FUNC_SYMBOL, + Arrays.asList(), + Arrays.>asList(new TypeReference() {})); + return executeRemoteCallSingleValueReturn(function, String.class); + } + + public RemoteFunctionCall transfer(String _to, BigInteger _value) { + final Function function = new Function( + FUNC_TRANSFER, + Arrays.asList(new org.web3j.abi.datatypes.Address(160, _to), + new org.web3j.abi.datatypes.generated.Uint256(_value)), + Collections.>emptyList()); + return executeRemoteCallTransaction(function); + } + + public RemoteFunctionCall balanceOf(String _owner) { + final Function function = new Function(FUNC_BALANCEOF, + Arrays.asList(new org.web3j.abi.datatypes.Address(160, _owner)), + Arrays.>asList(new TypeReference() {})); + return executeRemoteCallSingleValueReturn(function, BigInteger.class); + } + + public RemoteFunctionCall transferFrom(String _from, String _to, BigInteger _value) { + final Function function = new Function( + FUNC_TRANSFERFROM, + Arrays.asList(new org.web3j.abi.datatypes.Address(160, _from), + new org.web3j.abi.datatypes.Address(160, _to), + new org.web3j.abi.datatypes.generated.Uint256(_value)), + Collections.>emptyList()); + return executeRemoteCallTransaction(function); + } + + public RemoteFunctionCall allowance(String _owner, String _spender) { + final Function function = new Function(FUNC_ALLOWANCE, + Arrays.asList(new org.web3j.abi.datatypes.Address(160, _owner), + new org.web3j.abi.datatypes.Address(160, _spender)), + Arrays.>asList(new TypeReference() {})); + return executeRemoteCallSingleValueReturn(function, BigInteger.class); + } + + public static List getApprovalEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(APPROVAL_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (Contract.EventValuesWithLog eventValues : valueList) { + ApprovalEventResponse typedResponse = new ApprovalEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.owner = (String) eventValues.getIndexedValues().get(0).getValue(); + typedResponse.spender = (String) eventValues.getIndexedValues().get(1).getValue(); + typedResponse.value = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); + responses.add(typedResponse); + } + return responses; + } + + public static ApprovalEventResponse getApprovalEventFromLog(Log log) { + Contract.EventValuesWithLog eventValues = staticExtractEventParametersWithLog(APPROVAL_EVENT, log); + ApprovalEventResponse typedResponse = new ApprovalEventResponse(); + typedResponse.log = log; + typedResponse.owner = (String) eventValues.getIndexedValues().get(0).getValue(); + typedResponse.spender = (String) eventValues.getIndexedValues().get(1).getValue(); + typedResponse.value = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); + return typedResponse; + } + + public Flowable approvalEventFlowable(EthFilter filter) { + return web3j.ethLogFlowable(filter).map(log -> getApprovalEventFromLog(log)); + } + + public Flowable approvalEventFlowable(DefaultBlockParameter startBlock, DefaultBlockParameter endBlock) { + EthFilter filter = new EthFilter(startBlock, endBlock, getContractAddress()); + filter.addSingleTopic(EventEncoder.encode(APPROVAL_EVENT)); + return approvalEventFlowable(filter); + } + + public static List getTransferEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(TRANSFER_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (Contract.EventValuesWithLog eventValues : valueList) { + TransferEventResponse typedResponse = new TransferEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.from = (String) eventValues.getIndexedValues().get(0).getValue(); + typedResponse.to = (String) eventValues.getIndexedValues().get(1).getValue(); + typedResponse.value = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); + responses.add(typedResponse); + } + return responses; + } + + public static TransferEventResponse getTransferEventFromLog(Log log) { + Contract.EventValuesWithLog eventValues = staticExtractEventParametersWithLog(TRANSFER_EVENT, log); + TransferEventResponse typedResponse = new TransferEventResponse(); + typedResponse.log = log; + typedResponse.from = (String) eventValues.getIndexedValues().get(0).getValue(); + typedResponse.to = (String) eventValues.getIndexedValues().get(1).getValue(); + typedResponse.value = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); + return typedResponse; + } + + public Flowable transferEventFlowable(EthFilter filter) { + return web3j.ethLogFlowable(filter).map(log -> getTransferEventFromLog(log)); + } + + public Flowable transferEventFlowable(DefaultBlockParameter startBlock, DefaultBlockParameter endBlock) { + EthFilter filter = new EthFilter(startBlock, endBlock, getContractAddress()); + filter.addSingleTopic(EventEncoder.encode(TRANSFER_EVENT)); + return transferEventFlowable(filter); + } + + @Deprecated + public static ERC20ABI load(String contractAddress, Web3j web3j, Credentials credentials, BigInteger gasPrice, BigInteger gasLimit) { + return new ERC20ABI(contractAddress, web3j, credentials, gasPrice, gasLimit); + } + + @Deprecated + public static ERC20ABI load(String contractAddress, Web3j web3j, TransactionManager transactionManager, BigInteger gasPrice, BigInteger gasLimit) { + return new ERC20ABI(contractAddress, web3j, transactionManager, gasPrice, gasLimit); + } + + public static ERC20ABI load(String contractAddress, Web3j web3j, Credentials credentials, ContractGasProvider contractGasProvider) { + return new ERC20ABI(contractAddress, web3j, credentials, contractGasProvider); + } + + public static ERC20ABI load(String contractAddress, Web3j web3j, TransactionManager transactionManager, ContractGasProvider contractGasProvider) { + return new ERC20ABI(contractAddress, web3j, transactionManager, contractGasProvider); + } + + public static class ApprovalEventResponse extends BaseEventResponse { + public String owner; + + public String spender; + + public BigInteger value; + } + + public static class TransferEventResponse extends BaseEventResponse { + public String from; + + public String to; + + public BigInteger value; + } +} diff --git a/src/main/kotlin/com/api/admin/AdminApplication.kt b/src/main/kotlin/com/api/admin/AdminApplication.kt index 728b526..071f1ee 100644 --- a/src/main/kotlin/com/api/admin/AdminApplication.kt +++ b/src/main/kotlin/com/api/admin/AdminApplication.kt @@ -1,9 +1,11 @@ package com.api.admin import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.context.properties.ConfigurationPropertiesScan import org.springframework.boot.runApplication @SpringBootApplication +@ConfigurationPropertiesScan class AdminApplication fun main(args: Array) { diff --git a/src/main/kotlin/com/api/admin/config/R2dbcConfig.kt b/src/main/kotlin/com/api/admin/config/R2dbcConfig.kt index 96a8339..355510d 100644 --- a/src/main/kotlin/com/api/admin/config/R2dbcConfig.kt +++ b/src/main/kotlin/com/api/admin/config/R2dbcConfig.kt @@ -1,15 +1,26 @@ package com.api.admin.config +import com.api.admin.enums.AccountType +import com.api.admin.enums.ChainType +import com.api.admin.enums.TransferType +import com.api.admin.util.AccountTypeConvert +import com.api.admin.util.ChainTypeConvert +import com.api.admin.util.StringToEnumConverter +import com.api.admin.util.TransferTypeConvert import io.r2dbc.postgresql.PostgresqlConnectionFactory import io.r2dbc.postgresql.PostgresqlConnectionConfiguration +import io.r2dbc.postgresql.codec.EnumCodec import io.r2dbc.spi.ConnectionFactory import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration +import org.springframework.core.convert.converter.Converter import org.springframework.data.r2dbc.config.AbstractR2dbcConfiguration +import org.springframework.data.r2dbc.convert.R2dbcCustomConversions import org.springframework.data.r2dbc.core.R2dbcEntityTemplate import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories import org.springframework.r2dbc.connection.R2dbcTransactionManager import org.springframework.transaction.ReactiveTransactionManager +import java.util.ArrayList @Configuration @EnableR2dbcRepositories @@ -22,10 +33,29 @@ class R2dbcConfig : AbstractR2dbcConfiguration() { .database("admin") .username("admin") .password("admin") + .codecRegistrar( + EnumCodec.builder() + .withEnum("chain_type", ChainType::class.java) + .withEnum("transfer_type", TransferType::class.java) + .withEnum("account_type", AccountType::class.java) + .build() + ) .build() return PostgresqlConnectionFactory(configuration) } + @Bean + override fun r2dbcCustomConversions(): R2dbcCustomConversions { + val converters: MutableList?> = ArrayList?>() + converters.add(ChainTypeConvert(ChainType::class.java)) + converters.add(StringToEnumConverter(ChainType::class.java)) + converters.add(AccountTypeConvert(AccountType::class.java)) + converters.add(StringToEnumConverter(AccountType::class.java)) + converters.add(TransferTypeConvert(TransferType::class.java)) + converters.add(StringToEnumConverter(TransferType::class.java)) + return R2dbcCustomConversions(storeConversions, converters) + } + @Bean fun transactionManager(connectionFactory: ConnectionFactory?): ReactiveTransactionManager { return R2dbcTransactionManager(connectionFactory!!) diff --git a/src/main/kotlin/com/api/admin/config/RabbitMQConfig.kt b/src/main/kotlin/com/api/admin/config/RabbitMQConfig.kt index 479f50f..7ae9e58 100644 --- a/src/main/kotlin/com/api/admin/config/RabbitMQConfig.kt +++ b/src/main/kotlin/com/api/admin/config/RabbitMQConfig.kt @@ -3,6 +3,7 @@ package com.api.admin.config import org.springframework.amqp.core.Binding import org.springframework.amqp.core.BindingBuilder import org.springframework.amqp.core.DirectExchange +import org.springframework.amqp.core.FanoutExchange import org.springframework.amqp.core.Queue import org.springframework.amqp.rabbit.connection.ConnectionFactory import org.springframework.amqp.rabbit.core.RabbitTemplate @@ -14,27 +15,23 @@ import org.springframework.context.annotation.Configuration class RabbitMQConfig { @Bean - fun nftQueue(): Queue { - return Queue("nftQueue", true) - } + fun jsonMessageConverter(): Jackson2JsonMessageConverter = Jackson2JsonMessageConverter() @Bean - fun nftExchange(): DirectExchange { - return DirectExchange("nftExchange") + fun rabbitTemplate(connectionFactory: ConnectionFactory, jsonMessageConverter: Jackson2JsonMessageConverter): RabbitTemplate { + val template = RabbitTemplate(connectionFactory) + template.messageConverter = jsonMessageConverter + return template } - @Bean - fun bindingNftQueue(nftQueue: Queue, nftExchange: DirectExchange): Binding { - return BindingBuilder.bind(nftQueue).to(nftExchange).with("nftRoutingKey") + private fun createExchange(name: String): FanoutExchange { + return FanoutExchange(name) } @Bean - fun jsonMessageConverter(): Jackson2JsonMessageConverter = Jackson2JsonMessageConverter() + fun nftExchange() = createExchange("nftExchange") @Bean - fun rabbitTemplate(connectionFactory: ConnectionFactory, jsonMessageConverter: Jackson2JsonMessageConverter): RabbitTemplate { - val template = RabbitTemplate(connectionFactory) - template.messageConverter = jsonMessageConverter - return template - } + fun transferExchange() = createExchange("transferExchange") + } \ No newline at end of file diff --git a/src/main/kotlin/com/api/admin/controller/AdminController.kt b/src/main/kotlin/com/api/admin/controller/AdminController.kt new file mode 100644 index 0000000..2c96ae1 --- /dev/null +++ b/src/main/kotlin/com/api/admin/controller/AdminController.kt @@ -0,0 +1,64 @@ +package com.api.admin.controller + +import com.api.admin.controller.dto.DepositRequest +import com.api.admin.controller.dto.WithdrawERC20Request +import com.api.admin.controller.dto.WithdrawERC721Request +import com.api.admin.enums.AccountType +import com.api.admin.service.TransferService +import com.api.admin.service.Web3jService +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.* +import reactor.core.publisher.Mono + +@RestController +@RequestMapping("/v1/admin") +class AdminController( + private val transferService: TransferService, + private val web3jService: Web3jService, +) { + + @PostMapping("/deposit") + fun deposit( + @RequestParam address: String, + @RequestBody request: DepositRequest, + ): Mono> { + return transferService.getTransferData(address, request.chainType, request.transactionHash, AccountType.DEPOSIT) + .then(Mono.just(ResponseEntity.ok().build())) + .onErrorResume { + Mono.just(ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build()) + } + } + + + @PostMapping("/withdraw/erc20") + fun withdrawERC20( + @RequestParam address: String, + @RequestBody request: WithdrawERC20Request, + ): Mono> { + println("Received withdraw request for address: $address with amount: ${request.amount}") + return web3jService.createTransactionERC20(address, request.amount, request.chainType) + .doOnSuccess { println("Transaction successful") } + .then(Mono.just(ResponseEntity.ok().build())) + .doOnError { e -> + println("Error in withdrawERC20: ${e.message}") + e.printStackTrace() + } + } + + + + @PostMapping("/withdraw/erc721") + fun withdrawERC721( + @RequestParam address: String, + @RequestBody request: WithdrawERC721Request, + ): Mono> { + return web3jService.createTransactionERC721(address, request.nftId) + .then(Mono.just(ResponseEntity.ok().build())) + .onErrorResume { + Mono.just(ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build()) + } + } + + +} \ No newline at end of file diff --git a/src/main/kotlin/com/api/admin/controller/dto/DepositRequest.kt b/src/main/kotlin/com/api/admin/controller/dto/DepositRequest.kt new file mode 100644 index 0000000..9ffe758 --- /dev/null +++ b/src/main/kotlin/com/api/admin/controller/dto/DepositRequest.kt @@ -0,0 +1,9 @@ +package com.api.admin.controller.dto + +import com.api.admin.enums.ChainType + +data class DepositRequest( + val chainType: ChainType, + val transactionHash: String, +) + diff --git a/src/main/kotlin/com/api/admin/controller/dto/WithdrawERC20Request.kt b/src/main/kotlin/com/api/admin/controller/dto/WithdrawERC20Request.kt new file mode 100644 index 0000000..9b26e87 --- /dev/null +++ b/src/main/kotlin/com/api/admin/controller/dto/WithdrawERC20Request.kt @@ -0,0 +1,10 @@ +package com.api.admin.controller.dto + +import com.api.admin.enums.ChainType +import java.math.BigDecimal +import java.math.BigInteger + +data class WithdrawERC20Request( + val chainType: ChainType, + val amount: BigDecimal, + ) diff --git a/src/main/kotlin/com/api/admin/controller/dto/WithdrawERC721Request.kt b/src/main/kotlin/com/api/admin/controller/dto/WithdrawERC721Request.kt new file mode 100644 index 0000000..a8879f0 --- /dev/null +++ b/src/main/kotlin/com/api/admin/controller/dto/WithdrawERC721Request.kt @@ -0,0 +1,5 @@ +package com.api.admin.controller.dto + +data class WithdrawERC721Request( + val nftId: Long, +) diff --git a/src/main/kotlin/com/api/admin/domain/nft/Nft.kt b/src/main/kotlin/com/api/admin/domain/nft/Nft.kt index e3a8070..6d2d674 100644 --- a/src/main/kotlin/com/api/admin/domain/nft/Nft.kt +++ b/src/main/kotlin/com/api/admin/domain/nft/Nft.kt @@ -1,15 +1,14 @@ package com.api.admin.domain.nft +import com.api.admin.enums.ChainType import org.springframework.data.annotation.Id import org.springframework.data.relational.core.mapping.Table @Table("nft") -class Nft( +data class Nft( @Id val id : Long, val tokenId: String, val tokenAddress: String, - val chainType: String, - val nftName: String, - val collectionName: String + val chainType: ChainType, ) { } \ No newline at end of file diff --git a/src/main/kotlin/com/api/admin/domain/nft/NftRepository.kt b/src/main/kotlin/com/api/admin/domain/nft/NftRepository.kt index 06e921c..bebb262 100644 --- a/src/main/kotlin/com/api/admin/domain/nft/NftRepository.kt +++ b/src/main/kotlin/com/api/admin/domain/nft/NftRepository.kt @@ -1,6 +1,11 @@ package com.api.admin.domain.nft +import com.api.admin.enums.ChainType import org.springframework.data.repository.reactive.ReactiveCrudRepository +import reactor.core.publisher.Mono -interface NftRepository : ReactiveCrudRepository{ +interface NftRepository : ReactiveCrudRepository, NftRepositorySupport{ + +// fun findByTokenAddressAndTokenId(address:String,tokenId:String): Mono + fun findByTokenAddressAndTokenIdAndChainType(address:String,tokenId:String,chainType: ChainType): Mono } \ No newline at end of file diff --git a/src/main/kotlin/com/api/admin/domain/transfer/Transfer.kt b/src/main/kotlin/com/api/admin/domain/transfer/Transfer.kt index c195f66..428e72e 100644 --- a/src/main/kotlin/com/api/admin/domain/transfer/Transfer.kt +++ b/src/main/kotlin/com/api/admin/domain/transfer/Transfer.kt @@ -1,15 +1,23 @@ package com.api.admin.domain.transfer +import com.api.admin.enums.AccountType +import com.api.admin.enums.ChainType +import com.api.admin.enums.TransferType import org.springframework.data.annotation.Id import org.springframework.data.relational.core.mapping.Table +import java.math.BigDecimal @Table("transfer") -class Transfer( +data class Transfer( @Id val id: Long?, - val nftId: Long, + val nftId: Long?, val wallet: String, val timestamp: Long, - val status: String + val accountType: AccountType, + val balance: BigDecimal?, + val transferType: TransferType, + val transactionHash: String, + val chainType: ChainType, ) { } \ No newline at end of file diff --git a/src/main/kotlin/com/api/admin/domain/transfer/TransferRepository.kt b/src/main/kotlin/com/api/admin/domain/transfer/TransferRepository.kt index e23a988..16d947f 100644 --- a/src/main/kotlin/com/api/admin/domain/transfer/TransferRepository.kt +++ b/src/main/kotlin/com/api/admin/domain/transfer/TransferRepository.kt @@ -1,6 +1,13 @@ package com.api.admin.domain.transfer +import com.api.admin.enums.AccountType import org.springframework.data.repository.reactive.ReactiveCrudRepository +import reactor.core.publisher.Mono interface TransferRepository : ReactiveCrudRepository { + fun findByWalletAndAccountTypeAndNftId(wallet:String, accountType:AccountType,nftId:Long) : Mono + fun existsByWalletAndAccountTypeAndTransactionHashAndTimestampAfter(wallet: String,accountType: AccountType,transactionHash: String,timestamp:Long) : Mono + + + fun existsByTransactionHash(transactionHash: String): Mono } \ No newline at end of file diff --git a/src/main/kotlin/com/api/admin/enums/Enum.kt b/src/main/kotlin/com/api/admin/enums/Enum.kt index f3ca104..efa0589 100644 --- a/src/main/kotlin/com/api/admin/enums/Enum.kt +++ b/src/main/kotlin/com/api/admin/enums/Enum.kt @@ -2,12 +2,27 @@ package com.api.admin.enums enum class ChainType{ ETHEREUM_MAINNET, + LINEA_MAINNET, + LINEA_SEPOLIA, POLYGON_MAINNET, - ETHREUM_GOERLI, - ETHREUM_SEPOLIA, - POLYGON_MUMBAI, + ETHEREUM_HOLESKY, + ETHEREUM_SEPOLIA, + POLYGON_AMOY, + } -enum class StatusType{ + +enum class AccountType{ WITHDRAW, DEPOSIT -} \ No newline at end of file +} + +enum class TransferType { + ERC20,ERC721,NATIVE +} + +enum class TokenType { + MATIC, ETH +} + + + diff --git a/src/main/kotlin/com/api/admin/properties/AdminInfoProperties.kt b/src/main/kotlin/com/api/admin/properties/AdminInfoProperties.kt new file mode 100644 index 0000000..447b332 --- /dev/null +++ b/src/main/kotlin/com/api/admin/properties/AdminInfoProperties.kt @@ -0,0 +1,10 @@ +package com.api.admin.properties + +import org.springframework.boot.context.properties.ConfigurationProperties + + +@ConfigurationProperties(prefix = "admin") +data class AdminInfoProperties( + val address: String, + val privatekey: String, +) diff --git a/src/main/kotlin/com/api/admin/properties/InfuraApiProperties.kt b/src/main/kotlin/com/api/admin/properties/InfuraApiProperties.kt new file mode 100644 index 0000000..d501670 --- /dev/null +++ b/src/main/kotlin/com/api/admin/properties/InfuraApiProperties.kt @@ -0,0 +1,8 @@ +package com.api.admin.properties + +import org.springframework.boot.context.properties.ConfigurationProperties + +@ConfigurationProperties(prefix = "apikey") +data class InfuraApiProperties( + val infura: String, +) diff --git a/src/main/kotlin/com/api/admin/properties/NftApiProperties.kt b/src/main/kotlin/com/api/admin/properties/NftApiProperties.kt new file mode 100644 index 0000000..ec26cc3 --- /dev/null +++ b/src/main/kotlin/com/api/admin/properties/NftApiProperties.kt @@ -0,0 +1,8 @@ +package com.api.admin.properties + +import org.springframework.boot.context.properties.ConfigurationProperties + +@ConfigurationProperties(prefix = "nft") +data class NftApiProperties( + val uri: String +) diff --git a/src/main/kotlin/com/api/admin/rabbitMQ/MessageReceiver.kt b/src/main/kotlin/com/api/admin/rabbitMQ/MessageReceiver.kt deleted file mode 100644 index 4303d1b..0000000 --- a/src/main/kotlin/com/api/admin/rabbitMQ/MessageReceiver.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.api.admin.rabbitMQ - -import com.api.admin.NftResponse -import com.api.admin.service.NftService -import org.springframework.amqp.rabbit.annotation.RabbitListener -import org.springframework.stereotype.Service - -@Service -class MessageReceiver( - private val nftService: NftService, -) { - @RabbitListener(queues = ["nftQueue"]) - fun receiveMessage(nft: NftResponse) { - nftService.save(nft) - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/api/admin/rabbitMQ/event/AdminEventListener.kt b/src/main/kotlin/com/api/admin/rabbitMQ/event/AdminEventListener.kt new file mode 100644 index 0000000..ce2a555 --- /dev/null +++ b/src/main/kotlin/com/api/admin/rabbitMQ/event/AdminEventListener.kt @@ -0,0 +1,17 @@ +package com.api.admin.rabbitMQ.event + +import com.api.admin.rabbitMQ.event.dto.AdminTransferCreatedEvent +import com.api.admin.rabbitMQ.sender.RabbitMQSender +import org.springframework.context.event.EventListener +import org.springframework.stereotype.Component + +@Component +class AdminEventListener( + private val provider : RabbitMQSender +) { + + @EventListener + fun onDepositSend(event: AdminTransferCreatedEvent) { + provider.transferSend(event.transfer) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/api/admin/rabbitMQ/event/dto/AdminTransferCreatedEvent.kt b/src/main/kotlin/com/api/admin/rabbitMQ/event/dto/AdminTransferCreatedEvent.kt new file mode 100644 index 0000000..eb19997 --- /dev/null +++ b/src/main/kotlin/com/api/admin/rabbitMQ/event/dto/AdminTransferCreatedEvent.kt @@ -0,0 +1,5 @@ +package com.api.admin.rabbitMQ.event.dto + +import org.springframework.context.ApplicationEvent + +data class AdminTransferCreatedEvent(val eventSource: Any, val transfer: AdminTransferResponse): ApplicationEvent(eventSource) diff --git a/src/main/kotlin/com/api/admin/rabbitMQ/event/dto/AdminTransferResponse.kt b/src/main/kotlin/com/api/admin/rabbitMQ/event/dto/AdminTransferResponse.kt new file mode 100644 index 0000000..5dc9251 --- /dev/null +++ b/src/main/kotlin/com/api/admin/rabbitMQ/event/dto/AdminTransferResponse.kt @@ -0,0 +1,18 @@ +package com.api.admin.rabbitMQ.event.dto + +import com.api.admin.enums.AccountType +import com.api.admin.enums.ChainType +import com.api.admin.enums.TransferType +import java.math.BigDecimal + + +data class AdminTransferResponse( + val id: Long, + val walletAddress: String, + val nftId: Long?, + val timestamp: Long, + val accountType: AccountType, + val transferType: TransferType, + val balance: BigDecimal?, + val chainType: ChainType, +) diff --git a/src/main/kotlin/com/api/admin/rabbitMQ/receiver/RabbitMQReceiver.kt b/src/main/kotlin/com/api/admin/rabbitMQ/receiver/RabbitMQReceiver.kt new file mode 100644 index 0000000..4e96328 --- /dev/null +++ b/src/main/kotlin/com/api/admin/rabbitMQ/receiver/RabbitMQReceiver.kt @@ -0,0 +1,25 @@ +package com.api.admin.rabbitMQ.receiver + +import com.api.admin.service.dto.NftResponse +import com.api.admin.service.NftService +import org.springframework.amqp.core.ExchangeTypes +import org.springframework.amqp.rabbit.annotation.Exchange +import org.springframework.amqp.rabbit.annotation.Queue +import org.springframework.amqp.rabbit.annotation.QueueBinding +import org.springframework.amqp.rabbit.annotation.RabbitListener +import org.springframework.stereotype.Service + +@Service +class RabbitMQReceiver( + private val nftService: NftService, +) { + @RabbitListener(bindings = [QueueBinding( + value = Queue(name = "", durable = "false", exclusive = "true", autoDelete = "true"), + exchange = Exchange(value = "nftExchange", type = ExchangeTypes.FANOUT) + )]) + fun nftMessage(nft: NftResponse) { + nftService.save(nft) + .subscribe() + + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/api/admin/rabbitMQ/sender/RabbitMQSender.kt b/src/main/kotlin/com/api/admin/rabbitMQ/sender/RabbitMQSender.kt new file mode 100644 index 0000000..87de049 --- /dev/null +++ b/src/main/kotlin/com/api/admin/rabbitMQ/sender/RabbitMQSender.kt @@ -0,0 +1,16 @@ +package com.api.admin.rabbitMQ.sender + +import com.api.admin.rabbitMQ.event.dto.AdminTransferResponse +import org.springframework.amqp.rabbit.core.RabbitTemplate +import org.springframework.stereotype.Service + +@Service +class RabbitMQSender( + private val rabbitTemplate: RabbitTemplate +) { + + fun transferSend(transfer: AdminTransferResponse) { + rabbitTemplate.convertAndSend("transferExchange", "", transfer) + } + +} \ No newline at end of file diff --git a/src/main/kotlin/com/api/admin/service/InfuraApiService.kt b/src/main/kotlin/com/api/admin/service/InfuraApiService.kt new file mode 100644 index 0000000..7fcac50 --- /dev/null +++ b/src/main/kotlin/com/api/admin/service/InfuraApiService.kt @@ -0,0 +1,99 @@ +package com.api.admin.service + +import com.api.admin.enums.ChainType +import com.api.admin.properties.InfuraApiProperties +import com.api.admin.service.dto.InfuraRequest +import com.api.admin.service.dto.InfuraResponse +import com.api.admin.service.dto.InfuraTransferResponse +import org.springframework.http.MediaType +import org.springframework.stereotype.Service +import org.springframework.web.reactive.function.client.WebClient +import reactor.core.publisher.Mono + +@Service +class InfuraApiService( + private val infuraApiProperties: InfuraApiProperties, +) { + fun urlByChain(chainType: ChainType) : WebClient { + val baseUrl = when (chainType) { + ChainType.ETHEREUM_MAINNET -> "https://mainnet.infura.io" + ChainType.POLYGON_MAINNET -> "https://polygon-mainnet.infura.io" + ChainType.LINEA_MAINNET -> "https://linea-mainnet.infura.io" + ChainType.LINEA_SEPOLIA -> "https://linea-sepolia.infura.io" + ChainType.ETHEREUM_HOLESKY -> "https://polygon-mumbai.infura.io" + ChainType.ETHEREUM_SEPOLIA -> "https://sepolia.infura.io" + ChainType.POLYGON_AMOY -> "https://polygon-amoy.infura.io" + } + return WebClient.builder() + .baseUrl(baseUrl) + .build() + } + + fun getTransferLog(chainType: ChainType, transactionHash: String): Mono { + val requestBody = InfuraRequest(method = "eth_getTransactionReceipt", params = listOf(transactionHash)) + val webClient = urlByChain(chainType) + + return webClient.post() + .uri("/v3/${infuraApiProperties.infura}") + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(requestBody) + .retrieve() + .bodyToMono(InfuraTransferResponse::class.java) + + } + + fun getSend(chainType: ChainType, signedTransactionData: String): Mono { + val requestBody = InfuraRequest(method = "eth_sendRawTransaction", params = listOf(signedTransactionData)) + val webClient = urlByChain(chainType) + + return webClient.post() + .uri("/v3/${infuraApiProperties.infura}") + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(requestBody) + .retrieve() + .bodyToMono(InfuraResponse::class.java) + .mapNotNull { it.result } + .onErrorMap { e -> NumberFormatException("Invalid response format for transaction count") } + } + + fun getTransactionCount(chainType: ChainType, address: String): Mono { + val requestBody = InfuraRequest(method = "eth_getTransactionCount", params = listOf(address, "latest")) + val webClient = urlByChain(chainType) + + return webClient.post() + .uri("/v3/${infuraApiProperties.infura}") + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(requestBody) + .retrieve() + .bodyToMono(InfuraResponse::class.java) + .mapNotNull { it.result } + .onErrorMap { e -> NumberFormatException("Invalid response format for transaction count") } + } + + fun getGasPrice(chainType: ChainType): Mono { + val requestBody = InfuraRequest(method = "eth_gasPrice", params = emptyList()) + val webClient = urlByChain(chainType) + + return webClient.post() + .uri("/v3/${infuraApiProperties.infura}") + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(requestBody) + .retrieve() + .bodyToMono(InfuraResponse::class.java) + .mapNotNull { it.result } + .onErrorMap { e -> NumberFormatException("Invalid response format for gas price") } + } + + fun getTransactionReceipt(chainType: ChainType, transactionHash: String): Mono { + val requestBody = InfuraRequest(method = "eth_getTransactionReceipt", params = listOf(transactionHash)) + val webClient = urlByChain(chainType) + + return webClient.post() + .uri("/v3/${infuraApiProperties.infura}") + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(requestBody) + .retrieve() + .bodyToMono(String::class.java) + } + +} \ No newline at end of file diff --git a/src/main/kotlin/com/api/admin/service/NftApiService.kt b/src/main/kotlin/com/api/admin/service/NftApiService.kt new file mode 100644 index 0000000..9333328 --- /dev/null +++ b/src/main/kotlin/com/api/admin/service/NftApiService.kt @@ -0,0 +1,29 @@ +package com.api.admin.service + +import com.api.admin.properties.NftApiProperties +import com.api.admin.service.dto.NftRequest +import com.api.admin.service.dto.NftResponse +import org.springframework.http.MediaType +import org.springframework.stereotype.Service +import org.springframework.web.reactive.function.client.WebClient +import reactor.core.publisher.Mono + +@Service +class NftApiService( + nftApiProperties: NftApiProperties, +) { + + + private val webClient = WebClient.builder() + .baseUrl(nftApiProperties.uri) + .build() + + fun getNftSave(request: NftRequest): Mono { + return webClient.post() + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(request) + .retrieve() + .bodyToMono(NftResponse::class.java) + } + +} \ No newline at end of file diff --git a/src/main/kotlin/com/api/admin/service/NftService.kt b/src/main/kotlin/com/api/admin/service/NftService.kt index cb5b97f..426eff3 100644 --- a/src/main/kotlin/com/api/admin/service/NftService.kt +++ b/src/main/kotlin/com/api/admin/service/NftService.kt @@ -1,18 +1,54 @@ package com.api.admin.service -import com.api.admin.NftResponse -import com.api.admin.NftResponse.Companion.toEntity +import com.api.admin.domain.nft.Nft +import com.api.admin.service.dto.NftResponse +import com.api.admin.service.dto.NftResponse.Companion.toEntity import com.api.admin.domain.nft.NftRepository +import com.api.admin.enums.ChainType +import com.api.admin.service.dto.NftRequest +import org.springframework.dao.DuplicateKeyException import org.springframework.stereotype.Service +import reactor.core.publisher.Mono +import reactor.kotlin.core.publisher.switchIfEmpty @Service class NftService( private val nftRepository: NftRepository, + private val nftApiService: NftApiService, ) { - fun save(response: NftResponse) { - nftRepository.findById(response.id).switchIfEmpty( - nftRepository.save(response.toEntity()) - ) + fun save(response: NftResponse): Mono { + return nftRepository.findById(response.id) + .hasElement() + .flatMap { exists -> + if (!exists) { + nftRepository.insert(response.toEntity()) + .onErrorResume(DuplicateKeyException::class.java) { + Mono.empty() + } + .then() + } else { + Mono.empty() + } + } } + + fun findByNft(address: String, tokenId: String, chainType: ChainType): Mono { + return nftRepository.findByTokenAddressAndTokenIdAndChainType(address, tokenId, chainType) + .switchIfEmpty( + nftApiService.getNftSave( + NftRequest( + address, + tokenId, + chainType + ) + ).flatMap { + nftRepository.insert(it.toEntity()) + .onErrorResume(DuplicateKeyException::class.java) { + Mono.empty() + } + } + ) + } + } \ No newline at end of file diff --git a/src/main/kotlin/com/api/admin/service/TransferService.kt b/src/main/kotlin/com/api/admin/service/TransferService.kt index 6b7d9a3..62cf074 100644 --- a/src/main/kotlin/com/api/admin/service/TransferService.kt +++ b/src/main/kotlin/com/api/admin/service/TransferService.kt @@ -1,85 +1,173 @@ package com.api.admin.service -import com.api.admin.controller.dto.ValidTransferRequest -import com.api.admin.domain.nft.NftRepository import com.api.admin.domain.transfer.Transfer import com.api.admin.domain.transfer.TransferRepository +import com.api.admin.enums.AccountType import com.api.admin.enums.ChainType -import com.api.admin.enums.StatusType +import com.api.admin.enums.TransferType +import com.api.admin.properties.AdminInfoProperties +import com.api.admin.rabbitMQ.event.dto.AdminTransferCreatedEvent +import com.api.admin.rabbitMQ.event.dto.AdminTransferResponse +import com.api.admin.service.dto.InfuraTransferDetail +import org.springframework.context.ApplicationEventPublisher import org.springframework.stereotype.Service -import org.web3j.protocol.core.methods.request.Transaction -import org.web3j.abi.FunctionEncoder -import org.web3j.abi.FunctionReturnDecoder -import org.web3j.abi.datatypes.Function -import org.web3j.abi.datatypes.generated.Uint256 -import org.web3j.abi.TypeReference -import org.web3j.abi.datatypes.Address -import org.web3j.protocol.Web3j -import org.web3j.protocol.core.DefaultBlockParameterName -import org.web3j.protocol.http.HttpService import reactor.core.publisher.Flux import reactor.core.publisher.Mono +import java.math.BigDecimal import java.math.BigInteger import java.time.Instant @Service class TransferService( - private val nftRepository: NftRepository, private val transferRepository: TransferRepository, + private val eventPublisher: ApplicationEventPublisher, + private val infuraApiService: InfuraApiService, + private val nftService: NftService, + private val adminInfoProperties: AdminInfoProperties, ) { - private val adminAddress = "0x01b72b4aa3f66f213d62d53e829bc172a6a72867" + private val transferEventSignature = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" + private val nativeTransferEventSignature = "0xe6497e3ee548a3372136af2fcb0696db31fc6cf20260707645068bd3fe97f3c4" - fun validTransfer(wallet: String, requests: List): Mono { - return Flux.fromIterable(requests) - .flatMap { request -> - nftRepository.findById(request.nftId) - .flatMap { nft -> - getNftOwner(request.chainType, nft.tokenAddress, nft.tokenId) - .filterWhen { address -> Mono.just(address == adminAddress) } - .flatMap { saveTransfer(nft.id!!, wallet) } - } + fun getTransferData( + wallet: String, + chainType: ChainType, + transactionHash: String, + accountType: AccountType, + ): Mono { + println("transactionHash : $transactionHash") + return transferRepository.existsByTransactionHash(transactionHash) + .flatMap { + if (it) { + Mono.error(IllegalStateException("Transaction already exists")) + } else { + saveTransfer(wallet, chainType, transactionHash, accountType) + .doOnNext { transfer -> + eventPublisher.publishEvent(AdminTransferCreatedEvent(this, transfer.toResponse())) + } + .then() + } } - .then() } - fun saveTransfer(nftId: Long, wallet: String): Mono { - val transfer = Transfer( - id = null, - wallet = wallet, - nftId = nftId, - timestamp = Instant.now().toEpochMilli(), - status = StatusType.DEPOSIT.toString() - ) - return transferRepository.save(transfer) + fun saveTransfer(wallet: String, chainType: ChainType, transactionHash: String, accountType: AccountType): Flux { + return infuraApiService.getTransferLog(chainType, transactionHash) + .flatMapMany { response -> + val result = response.result + if (result != null) { + Flux.fromIterable(result.logs) + .flatMap { it.toEntity(wallet, accountType, chainType) } + } else { + Flux.error(IllegalStateException("Transaction logs not found for transaction hash: $transactionHash")) + } + } + .flatMap { transfer -> transferRepository.save(transfer) } } - fun getNftOwner(chainType: ChainType, contractAddress: String, tokenId: String): Mono { - val web3 = Web3j.build(HttpService(chainType.baseUrl() + "/v3/98b672d2ce9a4089a3a5cb5081dde2fa")) - val function = Function( - "ownerOf", - listOf(Uint256(BigInteger(tokenId))), - listOf(object : TypeReference
() {}) - ) - - val encodedFunction = FunctionEncoder.encode(function) - val transaction = Transaction.createEthCallTransaction(null, contractAddress, encodedFunction) - - return Mono.fromCallable { - val ethCall = web3.ethCall(transaction, DefaultBlockParameterName.LATEST).send() - val decode = FunctionReturnDecoder.decode(ethCall.value, function.outputParameters) - if (decode.isEmpty()) null else decode[0].value as String - }.retry(3) + + private fun Transfer.toResponse() = AdminTransferResponse( + id = this.id!!, + walletAddress = this.wallet, + nftId = this.nftId, + timestamp = this.timestamp, + accountType = this.accountType, + transferType = this.transferType, + balance = this.balance, + chainType = this.chainType, + ) + + fun InfuraTransferDetail.toEntity(wallet: String, accountType: AccountType, chainType: ChainType): Mono { + return Mono.just(this) + .flatMap { log -> + println("log : " + log.toString()) + when { + log.topics[0] == nativeTransferEventSignature -> + handleERC20Transfer(log, wallet, accountType, chainType,TransferType.NATIVE) +// log.topics[0] == transferEventSignature && log.topics.size == 3 -> +// handleERC20Transfer(log, wallet, accountType, chainType, TransferType.ERC20) + log.topics[0] == transferEventSignature && log.topics.size == 4 -> + handleERC721Transfer(log, wallet, accountType, chainType) + else -> Mono.empty() + } + } } - fun ChainType.baseUrl(): String { - return when(this){ - ChainType.ETHEREUM_MAINNET -> "https://mainnet.infura.io" - ChainType.POLYGON_MAINNET -> "https://polygon-mainnet.infura.io" - ChainType.ETHREUM_GOERLI -> "https://goerli.infura.io" - ChainType.ETHREUM_SEPOLIA -> "https://sepolia.infura.io" - ChainType.POLYGON_MUMBAI -> "https://polygon-mumbai.infura.io" + private fun handleERC20Transfer(log: InfuraTransferDetail, wallet: String, accountType: AccountType, chainType: ChainType,transferType: TransferType): Mono { + val from = parseAddress(log.topics[2]) + val to = parseAddress(log.topics[3]) + val amount = when (transferType) { + TransferType.NATIVE -> parseNativeTransferAmount(log.data) +// TransferType.ERC20 -> toBigDecimal(log.data) + else -> BigDecimal.ZERO + } + + val isRelevantTransfer = when (accountType) { + AccountType.DEPOSIT -> from.equals(wallet, ignoreCase = true) && to.equals(adminInfoProperties.address, ignoreCase = true) + AccountType.WITHDRAW -> from.equals(adminInfoProperties.address, ignoreCase = true) && to.equals(wallet, ignoreCase = true) } + + return if (isRelevantTransfer) { + Mono.just( + Transfer( + id = null, + nftId = null, + wallet = wallet, + timestamp = Instant.now().toEpochMilli(), + accountType = accountType, + balance = amount, + transferType = TransferType.ERC20, + transactionHash = log.transactionHash, + chainType = chainType, + ) + ) + } else { + Mono.empty() + } + } + + + + private fun handleERC721Transfer(log: InfuraTransferDetail, wallet: String, accountType: AccountType, chainType: ChainType): Mono { + val from = parseAddress(log.topics[1]) + val to = parseAddress(log.topics[2]) + val tokenId = BigInteger(log.topics[3].removePrefix("0x"), 16).toString() + + val isRelevantTransfer = when (accountType) { + AccountType.DEPOSIT -> from.equals(wallet, ignoreCase = true) && to.equals(adminInfoProperties.address, ignoreCase = true) + AccountType.WITHDRAW -> from.equals(adminInfoProperties.address, ignoreCase = true) && to.equals(wallet, ignoreCase = true) + } + + return if (isRelevantTransfer) { + nftService.findByNft(log.address, tokenId, chainType) + .map { nft -> + Transfer( + id = null, + nftId = nft.id, + wallet = wallet, + timestamp = Instant.now().toEpochMilli(), + accountType = accountType, + balance = null, + transferType = TransferType.ERC721, + transactionHash = log.transactionHash, + chainType = chainType, + ) + } + } else { + Mono.empty() + } + } + + private fun parseAddress(address: String): String { + return "0x" + address.substring(26).padStart(40, '0').toLowerCase() + } + + private fun toBigDecimal(balance: String): BigDecimal = + BigInteger(balance.removePrefix("0x"), 16).toBigDecimal().divide(BigDecimal("1000000000000000000")) + + private fun parseNativeTransferAmount(data: String): BigDecimal { + val cleanData = data.removePrefix("0x") + val amountHex = cleanData.substring(0, 64) + return BigInteger(amountHex, 16).toBigDecimal().divide(BigDecimal("1000000000000000000")) } } \ No newline at end of file diff --git a/src/main/kotlin/com/api/admin/service/Web3jService.kt b/src/main/kotlin/com/api/admin/service/Web3jService.kt new file mode 100644 index 0000000..940b3fa --- /dev/null +++ b/src/main/kotlin/com/api/admin/service/Web3jService.kt @@ -0,0 +1,212 @@ +package com.api.admin.service + +import com.api.admin.domain.nft.NftRepository +import com.api.admin.enums.AccountType +import com.api.admin.enums.ChainType +import com.api.admin.properties.AdminInfoProperties +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.ObjectMapper +import org.springframework.stereotype.Service +import org.web3j.abi.FunctionEncoder +import org.web3j.abi.datatypes.Address +import org.web3j.abi.datatypes.Function +import org.web3j.abi.datatypes.generated.Uint256 +import org.web3j.crypto.Credentials +import org.web3j.crypto.RawTransaction +import org.web3j.crypto.TransactionEncoder +import org.web3j.utils.Numeric +import reactor.core.publisher.Mono +import java.math.BigDecimal +import java.math.BigInteger +import java.time.Duration + +@Service +class Web3jService( + private val infuraApiService: InfuraApiService, + private val transferService: TransferService, + private val nftRepository: NftRepository, + private val adminInfoProperties: AdminInfoProperties +) { + + // private val privateKey = "4ec9e64419547100af4f38d7ec57ba1de2d5c36a7dfb03f1a349b2c5b62ac0a9" + + private fun getChainId(chain: ChainType): Long { + val chain = when (chain) { + ChainType.ETHEREUM_MAINNET -> 1L + ChainType.POLYGON_MAINNET -> 137L + ChainType.LINEA_MAINNET -> 59144L + ChainType.LINEA_SEPOLIA -> 59140L + ChainType.ETHEREUM_HOLESKY -> 1L + ChainType.ETHEREUM_SEPOLIA -> 11155111L + ChainType.POLYGON_AMOY -> 80002L + } + return chain + } + + + fun createTransactionERC721( + toAddress: String, + nftId: Long, + ): Mono { + return nftRepository.findById(nftId).flatMap { nft-> + val credentials = Credentials.create(adminInfoProperties.privatekey) + createERC721TransactionData(credentials, nft.tokenAddress, toAddress, BigInteger(nft.tokenId), nft.chainType) + .flatMap { transactionHash -> + waitForTransactionReceipt(transactionHash, nft.chainType) + .flatMap { + transferService.getTransferData( + wallet = toAddress, + chainType = nft.chainType, + transactionHash = transactionHash, + accountType = AccountType.WITHDRAW + ) + } + } + .doOnError { e -> + println("Error in createTransactionERC20: ${e.message}") + e.printStackTrace() + } + .then() + } + } + + fun createERC721TransactionData( + credentials: Credentials, + contractAddress: String, + toAddress: String, + tokenId: BigInteger, + chainType: ChainType + ): Mono { + return infuraApiService.getTransactionCount(chainType, credentials.address) + .zipWith(infuraApiService.getGasPrice(chainType)) + .flatMap { tuple -> + val nonce = BigInteger(tuple.t1.removePrefix("0x"), 16) + val gasPrice = BigInteger(tuple.t2.removePrefix("0x"), 16) + val gasLimit = BigInteger.valueOf(100000) + val chainId = getChainId(chainType) + + val function = Function( + "safeTransferFrom", + listOf( + Address(credentials.address), + Address(toAddress), + Uint256(tokenId) + ), + emptyList() + ) + + val encodedFunction = FunctionEncoder.encode(function) + + val rawTransaction = RawTransaction.createTransaction( + nonce, gasPrice, gasLimit, contractAddress, encodedFunction + ) + + val signedMessage = TransactionEncoder.signMessage(rawTransaction, chainId, credentials) + val signedTransactionData = Numeric.toHexString(signedMessage) + + infuraApiService.getSend(chainType, signedTransactionData) + } + } + + + fun createTransactionERC20( + recipientAddress: String, + amount: BigDecimal, + chainType: ChainType + ): Mono { + val credentials = Credentials.create(adminInfoProperties.privatekey) + val weiAmount = amountToWei(amount) + return createERC20TransactionData(credentials, recipientAddress, weiAmount, chainType) + .flatMap { transactionHash -> + println("transactionLog: $transactionHash") + waitForTransactionReceipt(transactionHash, chainType) + .flatMap { + transferService.getTransferData(recipientAddress, chainType, transactionHash, AccountType.WITHDRAW) + } + } + .doOnError { e -> + println("Error in createTransactionERC20: ${e.message}") + e.printStackTrace() + } + .then() + } + + + fun createERC20TransactionData( + credentials: Credentials, + recipientAddress: String, + amount: BigInteger, + chainType: ChainType + ): Mono { + return infuraApiService.getTransactionCount(chainType, credentials.address) + .zipWith(infuraApiService.getGasPrice(chainType)) + .flatMap { tuple -> + val nonce = BigInteger(tuple.t1.removePrefix("0x"), 16) + val gasPrice = BigInteger(tuple.t2.removePrefix("0x"), 16) + val gasLimit = BigInteger.valueOf(100000) + val chainId = getChainId(chainType) + + val rawTransaction = RawTransaction.createEtherTransaction( + nonce, gasPrice, gasLimit, recipientAddress, amount + ) + + val signedMessage = TransactionEncoder.signMessage(rawTransaction, chainId, credentials) + val signedTransactionData = Numeric.toHexString(signedMessage) + + infuraApiService.getSend(chainType, signedTransactionData) + } + } + + fun amountToWei(amount: BigDecimal): BigInteger { + val weiPerMatic = BigDecimal("1000000000000000000") + return amount.multiply(weiPerMatic).toBigInteger() + } + + + + fun waitForTransactionReceipt(transactionHash: String, chainType: ChainType, maxAttempts: Int = 5, attempt: Int = 1): Mono { + + + val objectMapper = ObjectMapper() + println("Attempt $attempt for transaction $transactionHash") + return infuraApiService.getTransactionReceipt(chainType, transactionHash) + .flatMap { response -> + println("Transaction receipt response: $response") + val jsonNode: JsonNode = objectMapper.readTree(response) + val resultNode = jsonNode.get("result") + println("result Logic : $resultNode") + if (resultNode != null && resultNode.has("status")) { + val status = resultNode.get("status").asText() + println("Transaction status: $status") + if (status == "0x1") { + println("Transaction $transactionHash succeeded") + Mono.just(transactionHash) + } else if (status == "0x0") { + println("Transaction $transactionHash failed") + Mono.error(IllegalStateException("Transaction failed")) + } else { + println("Transaction $transactionHash is pending") + if (attempt >= maxAttempts) { + Mono.error(IllegalStateException("Transaction was not successful after $maxAttempts attempts")) + } else { + Mono.delay(Duration.ofSeconds(5)) + .flatMap { waitForTransactionReceipt(transactionHash, chainType, maxAttempts, attempt + 1) } + } + } + } else { + println("Transaction receipt for $transactionHash not found on attempt $attempt") + if (attempt >= maxAttempts) { + Mono.error(IllegalStateException("Transaction receipt not found after $maxAttempts attempts")) + } else { + Mono.delay(Duration.ofSeconds(5)) + .flatMap { waitForTransactionReceipt(transactionHash, chainType, maxAttempts, attempt + 1) } + } + } + } + .doOnError { e -> + println("Error while checking transaction receipt for $transactionHash: ${e.message}") + } + } + + +} \ No newline at end of file diff --git a/src/main/kotlin/com/api/admin/service/dto/InfuraRequest.kt b/src/main/kotlin/com/api/admin/service/dto/InfuraRequest.kt new file mode 100644 index 0000000..6c08245 --- /dev/null +++ b/src/main/kotlin/com/api/admin/service/dto/InfuraRequest.kt @@ -0,0 +1,9 @@ +package com.api.admin.service.dto + +data class InfuraRequest( + val jsonrpc: String = "2.0", + val method: String, + val params: List = emptyList(), + val id: Int = 1 +) + diff --git a/src/main/kotlin/com/api/admin/service/dto/InfuraResponse.kt b/src/main/kotlin/com/api/admin/service/dto/InfuraResponse.kt new file mode 100644 index 0000000..79bbcc3 --- /dev/null +++ b/src/main/kotlin/com/api/admin/service/dto/InfuraResponse.kt @@ -0,0 +1,3 @@ +package com.api.admin.service.dto + +data class InfuraResponse(val result: String) \ No newline at end of file diff --git a/src/main/kotlin/com/api/admin/service/dto/InfuraTransferResponse.kt b/src/main/kotlin/com/api/admin/service/dto/InfuraTransferResponse.kt new file mode 100644 index 0000000..9cec097 --- /dev/null +++ b/src/main/kotlin/com/api/admin/service/dto/InfuraTransferResponse.kt @@ -0,0 +1,40 @@ +package com.api.admin.service.dto + +import java.math.BigDecimal +import java.math.BigInteger + +data class InfuraTransferResponse( + val jsonrpc: String, + val id: String, + val result: InfuraTransferResult?, +) + +data class InfuraTransferResult( + val blockHash : String, + val blockNumber: String, + val contractAddress: String?, + val cumulativeGasUsed: String?, + val effectiveGasPrice: String?, + val from: String, + val gasUsed: String?, + val logs:List, + val logsBloom: String, + val status: String, + val to: String, + val transactionHash: String, + val transactionIndex: String, + val type: String, + +) + +data class InfuraTransferDetail( + val address: String, + val blockHash: String, + val blockNumber: String, + val data: String, + val logIndex: String, + val removed: Boolean, + val topics: List, + val transactionHash: String, + val transactionIndex: String +) \ No newline at end of file diff --git a/src/main/kotlin/com/api/admin/service/dto/NftRequest.kt b/src/main/kotlin/com/api/admin/service/dto/NftRequest.kt new file mode 100644 index 0000000..773e37c --- /dev/null +++ b/src/main/kotlin/com/api/admin/service/dto/NftRequest.kt @@ -0,0 +1,9 @@ +package com.api.admin.service.dto + +import com.api.admin.enums.ChainType + +data class NftRequest( + val tokenAddress: String, + val tokenId: String, + val chainType: ChainType +) diff --git a/src/main/kotlin/com/api/admin/NftResponse.kt b/src/main/kotlin/com/api/admin/service/dto/NftResponse.kt similarity index 66% rename from src/main/kotlin/com/api/admin/NftResponse.kt rename to src/main/kotlin/com/api/admin/service/dto/NftResponse.kt index 412859c..ac25b25 100644 --- a/src/main/kotlin/com/api/admin/NftResponse.kt +++ b/src/main/kotlin/com/api/admin/service/dto/NftResponse.kt @@ -1,14 +1,13 @@ -package com.api.admin +package com.api.admin.service.dto import com.api.admin.domain.nft.Nft +import com.api.admin.enums.ChainType data class NftResponse( val id : Long, val tokenId: String, val tokenAddress: String, - val chainType: String, - val nftName: String, - val collectionName: String + val chainType: ChainType, ){ companion object{ fun NftResponse.toEntity() = Nft( @@ -16,8 +15,6 @@ data class NftResponse( tokenId = this.tokenId, tokenAddress = this.tokenAddress, chainType = this.chainType, - nftName = this.nftName, - collectionName = this.collectionName ) } } diff --git a/src/main/kotlin/com/api/admin/util/ChainTypeConvert.kt b/src/main/kotlin/com/api/admin/util/ChainTypeConvert.kt new file mode 100644 index 0000000..3bce21d --- /dev/null +++ b/src/main/kotlin/com/api/admin/util/ChainTypeConvert.kt @@ -0,0 +1,12 @@ +package com.api.admin.util + +import com.api.admin.enums.AccountType +import com.api.admin.enums.ChainType +import com.api.admin.enums.TransferType +import org.springframework.data.r2dbc.convert.EnumWriteSupport + +data class ChainTypeConvert>(private val enumType: Class): EnumWriteSupport() + +data class TransferTypeConvert>(private val enumType: Class): EnumWriteSupport() + +data class AccountTypeConvert>(private val enumType: Class): EnumWriteSupport() diff --git a/src/main/kotlin/com/api/admin/util/StringToEnumConvert.kt b/src/main/kotlin/com/api/admin/util/StringToEnumConvert.kt new file mode 100644 index 0000000..8591771 --- /dev/null +++ b/src/main/kotlin/com/api/admin/util/StringToEnumConvert.kt @@ -0,0 +1,11 @@ +package com.api.admin.util + +import org.springframework.core.convert.converter.Converter +import org.springframework.data.convert.ReadingConverter + +@ReadingConverter +class StringToEnumConverter>(private val enumType: Class) : Converter { + override fun convert(source: String): T { + return java.lang.Enum.valueOf(enumType, source) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/api/admin/util/Util.kt b/src/main/kotlin/com/api/admin/util/Util.kt new file mode 100644 index 0000000..c39ed4e --- /dev/null +++ b/src/main/kotlin/com/api/admin/util/Util.kt @@ -0,0 +1,5 @@ +package com.api.admin.util + +object Util { + +} \ No newline at end of file diff --git a/src/main/kotlin/com/api/admin/wrapper/ERC1155ABI.json b/src/main/kotlin/com/api/admin/wrapper/ERC1155ABI.json new file mode 100644 index 0000000..211a562 --- /dev/null +++ b/src/main/kotlin/com/api/admin/wrapper/ERC1155ABI.json @@ -0,0 +1,314 @@ +[ + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "indexed": false, + "internalType": "bool", + "name": "approved", + "type": "bool" + } + ], + "name": "ApprovalForAll", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256[]", + "name": "ids", + "type": "uint256[]" + }, + { + "indexed": false, + "internalType": "uint256[]", + "name": "values", + "type": "uint256[]" + } + ], + "name": "TransferBatch", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "TransferSingle", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "string", + "name": "value", + "type": "string" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "name": "URI", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "accounts", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "ids", + "type": "uint256[]" + } + ], + "name": "balanceOfBatch", + "outputs": [ + { + "internalType": "uint256[]", + "name": "", + "type": "uint256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "address", + "name": "operator", + "type": "address" + } + ], + "name": "isApprovedForAll", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256[]", + "name": "ids", + "type": "uint256[]" + }, + { + "internalType": "uint256[]", + "name": "amounts", + "type": "uint256[]" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "safeBatchTransferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "safeTransferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "internalType": "bool", + "name": "approved", + "type": "bool" + } + ], + "name": "setApprovalForAll", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "interfaceId", + "type": "bytes4" + } + ], + "name": "supportsInterface", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "name": "uri", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + } +] \ No newline at end of file diff --git a/src/main/kotlin/com/api/admin/wrapper/ERC20ABI.json b/src/main/kotlin/com/api/admin/wrapper/ERC20ABI.json new file mode 100644 index 0000000..32a70ff --- /dev/null +++ b/src/main/kotlin/com/api/admin/wrapper/ERC20ABI.json @@ -0,0 +1,217 @@ +[ + { + "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": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "decimals", + "outputs": [ + { + "name": "", + "type": "uint8" + } + ], + "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": "", + "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": "_from", + "type": "address" + }, + { + "name": "_to", + "type": "address" + }, + { + "name": "_value", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "name": "", + "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": "owner", + "type": "address" + }, + { + "indexed": true, + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "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" + } +] diff --git a/src/main/kotlin/com/api/admin/wrapper/ERC721ABI.json b/src/main/kotlin/com/api/admin/wrapper/ERC721ABI.json new file mode 100644 index 0000000..d9d2991 --- /dev/null +++ b/src/main/kotlin/com/api/admin/wrapper/ERC721ABI.json @@ -0,0 +1,303 @@ +[ + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "mint", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "safeTransferFrom", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "bool", + "name": "approved", + "type": "bool" + } + ], + "name": "setApprovalForAll", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "approved", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "indexed": false, + "internalType": "bool", + "name": "approved", + "type": "bool" + } + ], + "name": "ApprovalForAll", + "type": "event" + }, + { + "constant": true, + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "getApproved", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "operator", + "type": "address" + } + ], + "name": "isApprovedForAll", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "ownerOf", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "internalType": "bytes4", + "name": "interfaceId", + "type": "bytes4" + } + ], + "name": "supportsInterface", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + } +] diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 4d0d09c..9999397 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,22 +1,9 @@ - spring: application: name: admin - datasource: - url: jdbc:postgresql://localhost:5435/admin - username: admin - password: admin - flyway: - locations: classpath:db/migration - r2dbc: - url: r2dbc:postgresql://localhost:5435/admin - username: admin - password: admin - rabbitmq: - host: localhost - port: 5672 - username: closeSea - password: closeSeaP@ssword - template: - routing-key: "nftRoutingKey" - exchange: "nftExchange" \ No newline at end of file + config: + import: "optional:configserver:http://localhost:9000" + cloud: + config: + fail-fast: true + diff --git a/src/main/resources/bootstrap.yaml b/src/main/resources/bootstrap.yaml new file mode 100644 index 0000000..9282f74 --- /dev/null +++ b/src/main/resources/bootstrap.yaml @@ -0,0 +1,9 @@ +spring: + application: + name: admin + cloud: + config: + uri: https://localhost:9000 + fail-fast: true + profiles: + active: local diff --git a/src/main/resources/db/migration/V1__Initial_schema.sql b/src/main/resources/db/migration/V1__Initial_schema.sql index 009c01b..863a35f 100644 --- a/src/main/resources/db/migration/V1__Initial_schema.sql +++ b/src/main/resources/db/migration/V1__Initial_schema.sql @@ -1,17 +1,38 @@ +CREATE TYPE chain_type AS ENUM ( + 'ETHEREUM_MAINNET', + 'LINEA_MAINNET', + 'LINEA_SEPOLIA', + 'POLYGON_MAINNET', + 'ETHEREUM_HOLESKY', + 'ETHEREUM_SEPOLIA', + 'POLYGON_AMOY' + ); + +CREATE TYPE account_type AS ENUM ( + 'WITHDRAW', 'DEPOSIT' +); + +CREATE TYPE transfer_type AS ENUM ( + 'ERC20', 'ERC721' + ); + + CREATE TABLE IF NOT EXISTS nft ( id BIGINT PRIMARY KEY, token_id VARCHAR(255) NOT NULL, token_address VARCHAR(255) NOT NULL, - chain_type varchar(100) NOT NULL, - nft_name varchar(255) NOT NULL, - collection_name varchar(500) + chain_type chain_type NOT NULL ); CREATE TABLE IF NOT EXISTS transfer ( - id BIGINT PRIMARY KEY, + id SERIAL PRIMARY KEY, wallet VARCHAR(255) NOT NULL, nft_id BIGINT REFERENCES nft(id), - timestamp bigint not null - status VARCHAR(255) NOT NULL -) + timestamp bigint not null, + account_type account_type NOT NULL, + balance DECIMAL(19, 4), + transfer_type transfer_type NOT NULL, + transaction_hash VARCHAR(255) NOT NULL, + chain_type chain_type NOT NULL +); diff --git a/src/test/kotlin/com/api/admin/AdminServiceTest.kt b/src/test/kotlin/com/api/admin/AdminServiceTest.kt index 06318ff..24bf486 100644 --- a/src/test/kotlin/com/api/admin/AdminServiceTest.kt +++ b/src/test/kotlin/com/api/admin/AdminServiceTest.kt @@ -1,19 +1,159 @@ package com.api.admin +import com.api.admin.enums.AccountType import com.api.admin.enums.ChainType +import com.api.admin.enums.TransferType +import com.api.admin.rabbitMQ.event.dto.AdminTransferResponse +import com.api.admin.rabbitMQ.sender.RabbitMQSender +import com.api.admin.service.InfuraApiService import com.api.admin.service.TransferService +import com.api.admin.service.Web3jService import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest +import java.math.BigDecimal +import java.math.BigInteger +import java.time.Instant @SpringBootTest class AdminServiceTest( @Autowired private val transferService: TransferService, + @Autowired private val rabbitMQSender: RabbitMQSender, + @Autowired private val infuraApiService: InfuraApiService, + @Autowired private val web3jService: Web3jService, ) { +// @Test +// fun test() { +// val res = transferService.getNftOwner(ChainType.POLYGON_MAINNET,"0xa3784fe9104fdc0b988769fba7459ece2fb36eea","0") +// println(res) +// } + @Test - fun test() { - val res = transferService.getNftOwner(ChainType.POLYGON_MAINNET,"0xa3784fe9104fdc0b988769fba7459ece2fb36eea","0") - println(res) + fun getNftTransferDetail() { + + val res = infuraApiService.getTransferLog(ChainType.POLYGON_MAINNET,"0xed96d307d81fd04a752d222b0cf06397dff8fc87245e8764b6641d97bc36081c").block() + println(res.toString()) } + + @Test + fun saveTransfer() { + transferService.saveTransfer("0x01b72b4aa3f66f213d62d53e829bc172a6a72867",ChainType.POLYGON_MAINNET,"0x55fa4495f983e9f162b39b3df4dec8ebcff9aa05daee7b051c680ccfb49422a6",AccountType.DEPOSIT).next().block() + } + + @Test + fun test1() { + val address = "0x0000000000000000000000009bdef468ae33b09b12a057b4c9211240d63bae65" + val result = parseAddress(address) + println(result) + println(result == "0x9bDeF468ae33b09b12a057B4c9211240D63BaE65") + + } + + @Test + fun deposit() { + val res = transferService.getTransferData("0x01b72b4aa3f66f213d62d53e829bc172a6a72867",ChainType.POLYGON_MAINNET,"0xed96d307d81fd04a752d222b0cf06397dff8fc87245e8764b6641d97bc36081c",AccountType.DEPOSIT) + .block() + + + println("res : " + res.toString()) + + } + + private fun parseAddress(address: String): String { + return "0x" + address.substring(26).padStart(40, '0') + } + + @Test + fun sendMessage() { + val response = AdminTransferResponse( + id= 1L, + walletAddress = "0x01b72b4aa3f66f213d62d53e829bc172a6a72867", + nftId = 3L, + timestamp = Instant.now().toEpochMilli(), + accountType = AccountType.DEPOSIT, + transferType = TransferType.ERC721, + balance = null, + chainType = ChainType.POLYGON_MAINNET + ) + rabbitMQSender.transferSend(response) + Thread.sleep(10000) + } + + @Test + fun sendMatic() { +// web3jService.createTransaction( +// "e9769d3c00032a83d703e03630edbfc3cb634b40b92e38ab2890d5e37f21bb15", +// "0x9bDeF468ae33b09b12a057B4c9211240D63BaE65", +// BigInteger("1000000000000000000") +// ) + + val transactionData = web3jService.createTransactionERC20( + "0x9bDeF468ae33b09b12a057B4c9211240D63BaE65", + BigDecimal("1000000000000000000"), + ChainType.POLYGON_MAINNET + ) +// val response = infuraApiService.getSend(ChainType.POLYGON_MAINNET,transactionData).block() +// println(response) + } + + @Test + fun infuraTest() { + val res =infuraApiService.getTransactionCount(ChainType.POLYGON_MAINNET,"0x01b72b4aa3f66f213d62d53e829bc172a6a72867").block() + println(res.toString()) + } + + + @Test + fun createTransactionERC721() { + // val res = web3jService.createTransactionERC721("0xbc0c96c8d12a149cac4f7688f740ef21b2c8fd23","0x01b72b4aa3f66f213d62d53e829bc172a6a72867", + // BigInteger("0"),ChainType.POLYGON_MAINNET).block() + // + // println(res.toString()) + + // 0x1b0f6c70528addc34a5f17d0c7df59d932c27e85d29af14a957567fff29ef267 + val res1 = transferService.getTransferData( + wallet = "0x01b72b4aa3f66f213d62d53e829bc172a6a72867", + transactionHash = "0x1b0f6c70528addc34a5f17d0c7df59d932c27e85d29af14a957567fff29ef267", + chainType = ChainType.POLYGON_MAINNET, + accountType = AccountType.WITHDRAW + ).block() + + } + + @Test + fun createTransactionERC20() { + val res = web3jService.createTransactionERC20("0x01b72b4aa3f66f213d62d53e829bc172a6a72867", amount = BigDecimal("1000000000000"),ChainType.POLYGON_AMOY).block() + println(res.toString()) + } + + @Test + fun test1sd() { + val res = transferService.getTransferData( + wallet = "0x01b72b4aa3f66f213d62d53e829bc172a6a72867", + chainType = ChainType.POLYGON_AMOY, + transactionHash = "0xe9d42662999109de038dc6701acf297ff7e693eaf241664323a44f71d626890e", + accountType = AccountType.WITHDRAW + ).block() + + Thread.sleep(50000) + } + + @Test + fun receipe() { + val res = infuraApiService.getTransactionReceipt(ChainType.POLYGON_AMOY,"0xe9d42662999109de038dc6701acf297ff7e693eaf241664323a44f71d626890e").block() + println("res" + res) + } + +// @Test +// fun twest1() { +// val res = web3jService.testcreateTransactionERC20(recipientAddress = "0x01b72b4aa3f66f213d62d53e829bc172a6a72867",ChainType.POLYGON_AMOY).block() +// } + + @Test + fun waitForTransaction() { + val res = web3jService.waitForTransactionReceipt("0x2c325130ac68f839cc0a049a53a756c9a86d6c430e3367b64b3ed11f524b57c3",ChainType.POLYGON_AMOY).block() + } + + } \ No newline at end of file