diff --git a/.github/workflows/cicd.yaml b/.github/workflows/cicd.yaml new file mode 100644 index 0000000..c136146 --- /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: nft + 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: nft + 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/build.gradle.kts b/build.gradle.kts index 0428938..78deaab 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,24 +6,25 @@ plugins { id("org.flywaydb.flyway") version "9.21.1" kotlin("jvm") version "1.9.23" kotlin("plugin.spring") version "1.9.23" + kotlin("plugin.lombok") version "1.9.23" + id("io.freefair.lombok") version "8.1.0" } 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-batch") - implementation("org.springframework.boot:spring-boot-starter-jooq") - implementation("org.springframework.boot:spring-boot-starter-quartz") - implementation("org.springframework.boot:spring-boot-starter-data-r2dbc") implementation("org.postgresql:r2dbc-postgresql:1.0.4.RELEASE") implementation("org.postgresql:postgresql:42.7.3") @@ -36,19 +37,30 @@ dependencies { implementation("org.jetbrains.kotlin:kotlin-reflect") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor") -// implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.0") -// implementation("com.fasterxml.jackson.core:jackson-databind:2.13.0") + implementation("org.springframework.boot:spring-boot-starter-amqp") + + implementation("org.springframework.boot:spring-boot-starter-data-redis") + implementation("org.springframework:spring-context") + implementation("org.web3j:core:4.9.1") testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("io.projectreactor:reactor-test") - testImplementation("org.springframework.batch:spring-batch-test") + + implementation("io.netty:netty-resolver-dns-native-macos:4.1.68.Final:osx-aarch_64") + } +dependencyManagement { + imports { + mavenBom("org.springframework.cloud:spring-cloud-dependencies:${property("springCloudVersion")}") + } +} + tasks.withType { kotlinOptions { freeCompilerArgs += "-Xjsr305=strict" - jvmTarget = "17" + jvmTarget = "21" } } diff --git a/docker-compose.yaml b/docker-compose.yaml index dfa5a58..2f85329 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -3,12 +3,197 @@ version: "3.8" services: db: image: postgres:latest + ports: + - '5434:5432' + volumes: + - ./init.sql:/docker-entrypoint-initdb.d/init.sql + - ${HOME}/Library/CloudStorage/GoogleDrive-dongkim.chatgpt@gmail.com/내 드라이브/marketplace/db/nft:/var/lib/postgresql/data environment: POSTGRES_USER: nft POSTGRES_PASSWORD: nft POSTGRES_DB: nft + PGDATA: /var/lib/postgresql/data/pgdata + + rabbitmq: + image: rabbitmq:3-management ports: - - '5434:5432' + - '5672:5672' + - '15672:15672' volumes: - - ./init.sql:/docker-entrypoint-initdb.d/init.sql + - ./rabbitmq_data:/var/lib/rabbitmq + environment: + RABBITMQ_DEFAULT_USER: closeSea + RABBITMQ_DEFAULT_PASS: closeSeaP@ssword + + redis-node-0: + image: docker.io/bitnami/redis-cluster:7.2 + ports: + - 6379:6379 + volumes: + - redis-cluster_data-0:/bitnami/redis/data + environment: + - 'REDIS_PASSWORD=bitnami' + - 'REDIS_NODES=redis-node-0:6379 redis-node-1:6380 redis-node-2:6381 redis-node-3:6382 redis-node-4:6383 redis-node-5:6384 redis-node-6:6385 redis-node-7:6386 redis-node-8:6387 redis-node-9:6388' + - 'REDIS_CLUSTER_ANNOUNCE_HOSTNAME=localhost' + - 'REDIS_CLUSTER_PREFERRED_ENDPOINT_TYPE=hostname' + + redis-node-1: + image: docker.io/bitnami/redis-cluster:7.2 + ports: + - 6380:6380 + volumes: + - redis-cluster_data-1:/bitnami/redis/data + environment: + - 'REDIS_PASSWORD=bitnami' + - 'REDIS_NODES=redis-node-0:6379 redis-node-1:6380 redis-node-2:6381 redis-node-3:6382 redis-node-4:6383 redis-node-5:6384 redis-node-6:6385 redis-node-7:6386 redis-node-8:6387 redis-node-9:6388' + - 'REDIS_PORT_NUMBER=6380' + - 'REDIS_CLUSTER_ANNOUNCE_HOSTNAME=localhost' + - 'REDIS_CLUSTER_ANNOUNCE_PORT=6380' + - 'REDIS_CLUSTER_PREFERRED_ENDPOINT_TYPE=hostname' + + redis-node-2: + image: docker.io/bitnami/redis-cluster:7.2 + ports: + - 6381:6381 + volumes: + - redis-cluster_data-2:/bitnami/redis/data + environment: + - 'REDIS_PASSWORD=bitnami' + - 'REDIS_NODES=redis-node-0:6379 redis-node-1:6380 redis-node-2:6381 redis-node-3:6382 redis-node-4:6383 redis-node-5:6384 redis-node-6:6385 redis-node-7:6386 redis-node-8:6387 redis-node-9:6388' + - 'REDIS_PORT_NUMBER=6381' + - 'REDIS_CLUSTER_ANNOUNCE_HOSTNAME=localhost' + - 'REDIS_CLUSTER_ANNOUNCE_PORT=6381' + - 'REDIS_CLUSTER_PREFERRED_ENDPOINT_TYPE=hostname' + + redis-node-3: + image: docker.io/bitnami/redis-cluster:7.2 + ports: + - 6382:6382 + volumes: + - redis-cluster_data-3:/bitnami/redis/data + environment: + - 'REDIS_PASSWORD=bitnami' + - 'REDIS_NODES=redis-node-0:6379 redis-node-1:6380 redis-node-2:6381 redis-node-3:6382 redis-node-4:6383 redis-node-5:6384 redis-node-6:6385 redis-node-7:6386 redis-node-8:6387 redis-node-9:6388' + - 'REDIS_PORT_NUMBER=6382' + - 'REDIS_CLUSTER_ANNOUNCE_HOSTNAME=localhost' + - 'REDIS_CLUSTER_ANNOUNCE_PORT=6382' + - 'REDIS_CLUSTER_PREFERRED_ENDPOINT_TYPE=hostname' + + redis-node-4: + image: docker.io/bitnami/redis-cluster:7.2 + ports: + - 6383:6383 + volumes: + - redis-cluster_data-4:/bitnami/redis/data + environment: + - 'REDIS_PASSWORD=bitnami' + - 'REDIS_NODES=redis-node-0:6379 redis-node-1:6380 redis-node-2:6381 redis-node-3:6382 redis-node-4:6383 redis-node-5:6384 redis-node-6:6385 redis-node-7:6386 redis-node-8:6387 redis-node-9:6388' + - 'REDIS_PORT_NUMBER=6383' + - 'REDIS_CLUSTER_ANNOUNCE_HOSTNAME=localhost' + - 'REDIS_CLUSTER_ANNOUNCE_PORT=6383' + - 'REDIS_CLUSTER_PREFERRED_ENDPOINT_TYPE=hostname' + + redis-node-5: + image: docker.io/bitnami/redis-cluster:7.2 + ports: + - 6384:6384 + volumes: + - redis-cluster_data-5:/bitnami/redis/data + environment: + - 'REDIS_PASSWORD=bitnami' + - 'REDIS_NODES=redis-node-0:6379 redis-node-1:6380 redis-node-2:6381 redis-node-3:6382 redis-node-4:6383 redis-node-5:6384 redis-node-6:6385 redis-node-7:6386 redis-node-8:6387 redis-node-9:6388' + - 'REDIS_PORT_NUMBER=6384' + - 'REDIS_CLUSTER_ANNOUNCE_HOSTNAME=localhost' + - 'REDIS_CLUSTER_ANNOUNCE_PORT=6384' + - 'REDIS_CLUSTER_PREFERRED_ENDPOINT_TYPE=hostname' + + redis-node-6: + image: docker.io/bitnami/redis-cluster:7.2 + ports: + - 6385:6385 + volumes: + - redis-cluster_data-6:/bitnami/redis/data + environment: + - 'REDIS_PASSWORD=bitnami' + - 'REDIS_NODES=redis-node-0:6379 redis-node-1:6380 redis-node-2:6381 redis-node-3:6382 redis-node-4:6383 redis-node-5:6384 redis-node-6:6385 redis-node-7:6386 redis-node-8:6387 redis-node-9:6388' + - 'REDIS_PORT_NUMBER=6385' + - 'REDIS_CLUSTER_ANNOUNCE_HOSTNAME=localhost' + - 'REDIS_CLUSTER_ANNOUNCE_PORT=6385' + - 'REDIS_CLUSTER_PREFERRED_ENDPOINT_TYPE=hostname' + + redis-node-7: + image: docker.io/bitnami/redis-cluster:7.2 + ports: + - 6386:6386 + volumes: + - redis-cluster_data-7:/bitnami/redis/data + environment: + - 'REDIS_PASSWORD=bitnami' + - 'REDIS_NODES=redis-node-0:6379 redis-node-1:6380 redis-node-2:6381 redis-node-3:6382 redis-node-4:6383 redis-node-5:6384 redis-node-6:6385 redis-node-7:6386 redis-node-8:6387 redis-node-9:6388' + - 'REDIS_PORT_NUMBER=6386' + - 'REDIS_CLUSTER_ANNOUNCE_HOSTNAME=localhost' + - 'REDIS_CLUSTER_ANNOUNCE_PORT=6386' + - 'REDIS_CLUSTER_PREFERRED_ENDPOINT_TYPE=hostname' + + redis-node-8: + image: docker.io/bitnami/redis-cluster:7.2 + ports: + - 6387:6387 + volumes: + - redis-cluster_data-8:/bitnami/redis/data + environment: + - 'REDIS_PASSWORD=bitnami' + - 'REDIS_NODES=redis-node-0:6379 redis-node-1:6380 redis-node-2:6381 redis-node-3:6382 redis-node-4:6383 redis-node-5:6384 redis-node-6:6385 redis-node-7:6386 redis-node-8:6387 redis-node-9:6388' + - 'REDIS_PORT_NUMBER=6387' + - 'REDIS_CLUSTER_ANNOUNCE_HOSTNAME=localhost' + - 'REDIS_CLUSTER_ANNOUNCE_PORT=6387' + - 'REDIS_CLUSTER_PREFERRED_ENDPOINT_TYPE=hostname' + + redis-node-9: + image: docker.io/bitnami/redis-cluster:7.2 + ports: + - 6388:6388 + volumes: + - redis-cluster_data-9:/bitnami/redis/data + depends_on: + - redis-node-0 + - redis-node-1 + - redis-node-2 + - redis-node-3 + - redis-node-4 + - redis-node-5 + - redis-node-6 + - redis-node-7 + - redis-node-8 + environment: + - 'REDISCLI_AUTH=bitnami' + - 'REDIS_PASSWORD=bitnami' + - 'REDIS_NODES=redis-node-0:6379 redis-node-1:6380 redis-node-2:6381 redis-node-3:6382 redis-node-4:6383 redis-node-5:6384 redis-node-6:6385 redis-node-7:6386 redis-node-8:6387 redis-node-9:6388' + - 'REDIS_PORT_NUMBER=6388' + - 'REDIS_CLUSTER_ANNOUNCE_HOSTNAME=localhost' + - 'REDIS_CLUSTER_ANNOUNCE_PORT=6388' + - 'REDIS_CLUSTER_PREFERRED_ENDPOINT_TYPE=hostname' + - 'REDIS_CLUSTER_REPLICAS=2' + - 'REDIS_CLUSTER_CREATOR=yes' +volumes: + redis-cluster_data-0: + driver: local + redis-cluster_data-1: + driver: local + redis-cluster_data-2: + driver: local + redis-cluster_data-3: + driver: local + redis-cluster_data-4: + driver: local + redis-cluster_data-5: + driver: local + redis-cluster_data-6: + driver: local + redis-cluster_data-7: + driver: local + redis-cluster_data-8: + driver: local + redis-cluster_data-9: + driver: local diff --git a/redis.conf b/redis.conf new file mode 100644 index 0000000..461fa42 --- /dev/null +++ b/redis.conf @@ -0,0 +1,11 @@ +port 6379 +bind 0.0.0.0 +protected-mode no +cluster-enabled yes +cluster-config-file nodes.conf +cluster-node-timeout 5000 +appendonly yes +appendfilename "appendonly.aof" +cluster-announce-ip 10.10.148.169 +cluster-announce-port 6379 +cluster-announce-bus-port 16379 diff --git a/scan_all_nodes.sh b/scan_all_nodes.sh new file mode 100755 index 0000000..15c474e --- /dev/null +++ b/scan_all_nodes.sh @@ -0,0 +1,51 @@ +#!/bin/bash + +# Define an array of all Redis nodes +nodes=( + "172.24.0.2:6379" + "172.24.0.3:6379" + "172.24.0.4:6379" + "172.24.0.5:6379" + "172.24.0.6:6379" + "172.24.0.7:6379" + "172.24.0.8:6379" + "172.24.0.9:6379" + "172.24.0.10:6379" +) + +# Function to scan all keys in a Redis node +scan_keys() { + local host=$1 + local port=$2 + local cursor=0 + + echo "Scanning keys on node $host:$port" + + while : + do + # Use redis-cli to scan keys + result=$(docker exec -it nft-redis-1-1 redis-cli -h $host -p $port SCAN $cursor) + + # Extract the new cursor and keys + cursor=$(echo "$result" | head -n 1) + keys=$(echo "$result" | tail -n +2) + + # Print keys + if [ -n "$keys" ]; then + echo "$keys" + fi + + # Break if cursor is 0 + if [ "$cursor" == "0" ]; then + break + fi + done +} + +# Loop through each node and scan keys +for node in "${nodes[@]}" +do + host=$(echo $node | cut -d':' -f1) + port=$(echo $node | cut -d':' -f2) + scan_keys $host $port +done diff --git a/src/main/kotlin/com/api/nft/NftApplication.kt b/src/main/kotlin/com/api/nft/NftApplication.kt index 39cf3df..e037c97 100644 --- a/src/main/kotlin/com/api/nft/NftApplication.kt +++ b/src/main/kotlin/com/api/nft/NftApplication.kt @@ -1,9 +1,12 @@ package com.api.nft import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.context.properties.ConfigurationPropertiesScan import org.springframework.boot.runApplication @SpringBootApplication +@ConfigurationPropertiesScan +//@EnableScheduling class NftApplication fun main(args: Array) { diff --git a/src/main/kotlin/com/api/nft/config/R2dbcConfig.kt b/src/main/kotlin/com/api/nft/config/R2dbcConfig.kt index 3f76631..c1d7f43 100644 --- a/src/main/kotlin/com/api/nft/config/R2dbcConfig.kt +++ b/src/main/kotlin/com/api/nft/config/R2dbcConfig.kt @@ -1,15 +1,28 @@ package com.api.nft.config +import com.api.nft.enums.ChainType +import com.api.nft.enums.ContractType +import com.api.nft.enums.StatusType +import com.api.nft.enums.TokenType +import com.api.nft.util.ChainTypeConvert +import com.api.nft.util.ContractTypeConverter +import com.api.nft.util.StatusTypeConvert +import com.api.nft.util.StringToEnumConverter +import com.api.nft.util.TokenTypeConvert import io.r2dbc.postgresql.PostgresqlConnectionConfiguration import io.r2dbc.postgresql.PostgresqlConnectionFactory +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 @@ -24,10 +37,33 @@ class R2dbcConfig : AbstractR2dbcConfiguration() { .database("nft") .username("nft") .password("nft") + .codecRegistrar( + EnumCodec.builder() + .withEnum("contract_type", ContractType::class.java) + .withEnum("chain_type", ChainType::class.java) + .withEnum("token_type", TokenType::class.java) + .withEnum("status_type", StatusType::class.java) + .build() + ) .build() return PostgresqlConnectionFactory(configuration) } + @Bean + override fun r2dbcCustomConversions(): R2dbcCustomConversions { + val converters: MutableList?> = ArrayList?>() + converters.add(ContractTypeConverter(ContractType::class.java)) + converters.add(StringToEnumConverter(ContractType::class.java)) + converters.add(ChainTypeConvert(ChainType::class.java)) + converters.add(StringToEnumConverter(ChainType::class.java)) + converters.add(TokenTypeConvert(TokenType::class.java)) + converters.add(StringToEnumConverter(TokenType::class.java)) + converters.add(StatusTypeConvert(StatusType::class.java)) + converters.add(StringToEnumConverter(StatusType::class.java)) + return R2dbcCustomConversions(storeConversions, converters) + } + + @Bean fun transactionManager(connectionFactory: ConnectionFactory?): ReactiveTransactionManager { return R2dbcTransactionManager(connectionFactory!!) diff --git a/src/main/kotlin/com/api/nft/config/RabbitConfig.kt b/src/main/kotlin/com/api/nft/config/RabbitConfig.kt new file mode 100644 index 0000000..ef5b415 --- /dev/null +++ b/src/main/kotlin/com/api/nft/config/RabbitConfig.kt @@ -0,0 +1,40 @@ +package com.api.nft.config + +import org.springframework.amqp.core.* +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.amqp.rabbit.connection.ConnectionFactory +import org.springframework.amqp.rabbit.core.RabbitTemplate +import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter + +@Configuration +class RabbitConfig { + @Bean + fun jsonMessageConverter(): Jackson2JsonMessageConverter = Jackson2JsonMessageConverter() + + @Bean + fun rabbitTemplate( + connectionFactory: ConnectionFactory, + jsonMessageConverter: Jackson2JsonMessageConverter + ): RabbitTemplate { + val template = RabbitTemplate(connectionFactory) + template.messageConverter = jsonMessageConverter + return template + } + + + private fun createFanoutExchange(name: String): FanoutExchange { + return FanoutExchange(name) + } + + @Bean + fun nftExchange() = createFanoutExchange("nftExchange") + + + @Bean + fun listingExchange() = createFanoutExchange("listingExchange") + + + @Bean + fun auctionExchange() = createFanoutExchange("auctionExchange") +} \ No newline at end of file diff --git a/src/main/kotlin/com/api/nft/config/RedisConfig.kt b/src/main/kotlin/com/api/nft/config/RedisConfig.kt new file mode 100644 index 0000000..67f1409 --- /dev/null +++ b/src/main/kotlin/com/api/nft/config/RedisConfig.kt @@ -0,0 +1,67 @@ +package com.api.nft.config + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.data.redis.connection.RedisClusterConfiguration +import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory +import org.springframework.data.redis.connection.RedisPassword; +import io.lettuce.core.cluster.ClusterClientOptions +import org.springframework.data.redis.core.ReactiveRedisTemplate +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer +import org.springframework.data.redis.serializer.RedisSerializationContext +import org.springframework.data.redis.serializer.StringRedisSerializer +import java.time.Duration + + +@Configuration +class RedisConfig { + + @Bean + fun redisClusterConfiguration(): RedisClusterConfiguration { + val clusterNodes = listOf( + "localhost:6379", + "localhost:6380", + "localhost:6381", + "localhost:6382", + "localhost:6383", + "localhost:6384", + "localhost:6385", + "localhost:6386", + "localhost:6387", + "localhost:6388", + ) + return RedisClusterConfiguration(clusterNodes) + } + + @Bean + fun lettuceConnectionFactory(redisClusterConfiguration: RedisClusterConfiguration): LettuceConnectionFactory { + val clientConfig = LettuceClientConfiguration.builder() + .commandTimeout(Duration.ofSeconds(10)) + .clientOptions( + ClusterClientOptions.builder() + .autoReconnect(true) + .pingBeforeActivateConnection(true) + .build() + ) + .build() + redisClusterConfiguration.password = RedisPassword.of("bitnami") + redisClusterConfiguration.maxRedirects = 3 + return LettuceConnectionFactory(redisClusterConfiguration, clientConfig) + } + + @Bean + fun reactiveRedisTemplate(lettuceConnectionFactory: LettuceConnectionFactory): ReactiveRedisTemplate { + val keySerializer = StringRedisSerializer() + val valueSerializer = GenericJackson2JsonRedisSerializer() + + val serializationContext = RedisSerializationContext.newSerializationContext() + .key(keySerializer) + .value(valueSerializer) + .hashKey(keySerializer) + .hashValue(valueSerializer) + .build() + + return ReactiveRedisTemplate(lettuceConnectionFactory, serializationContext) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/api/nft/controller/NftController.kt b/src/main/kotlin/com/api/nft/controller/NftController.kt index 3bd1217..d649da6 100644 --- a/src/main/kotlin/com/api/nft/controller/NftController.kt +++ b/src/main/kotlin/com/api/nft/controller/NftController.kt @@ -1,10 +1,61 @@ package com.api.nft.controller +import com.api.nft.controller.dto.NftMetadataResponse +import com.api.nft.domain.trasfer.Transfer +import com.api.nft.enums.ChainType +import com.api.nft.event.dto.NftResponse +import com.api.nft.service.RedisService +import com.api.nft.service.api.NftService +import com.api.nft.service.api.TransferService +import com.api.nft.service.external.dto.NftRequest +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController +import reactor.core.publisher.Flux +import reactor.core.publisher.Mono +//TODO("responseEntity로 반환) @RestController @RequestMapping("/v1/nft") -class NftController { +class NftController( + private val nftService: NftService, + private val transferService: TransferService, +) { + + @GetMapping + fun getAllByIds(@RequestParam nftIds: List): Flux { + return nftService.findAllById(nftIds) + } + + @GetMapping("/{nftId}") + fun getOneById(@PathVariable nftId: Long): Mono { + return nftService.findById(nftId) + } + + @GetMapping("/transfer") + fun getTransfers(@RequestParam nftId: Long) : Mono>> { + return transferService.findOrUpdateByNftId(nftId) + .collectList() + .map { ResponseEntity.ok(it) } + .onErrorResume { Mono.just(ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build()) } + } + + @PostMapping + fun findOrCreate(@RequestBody request: NftRequest): Mono { + return nftService.findOrCreateNft(request.tokenAddress,request.tokenId,request.chainType) + } + + @GetMapping("/wallet/{chainType}") + fun getByWalletNft(@PathVariable chainType: ChainType, @RequestParam wallet: String) : Flux { + return nftService.getByWalletNft(wallet,chainType) + + } + } \ No newline at end of file diff --git a/src/main/kotlin/com/api/nft/controller/dto/NftBatchRequest.kt b/src/main/kotlin/com/api/nft/controller/dto/NftBatchRequest.kt new file mode 100644 index 0000000..7d0d6cf --- /dev/null +++ b/src/main/kotlin/com/api/nft/controller/dto/NftBatchRequest.kt @@ -0,0 +1,9 @@ +package com.api.nft.controller.dto + +import com.api.nft.enums.ChainType + +data class NftBatchRequest( + val tokenId: String, + val tokenAddress: String, + val chainType: ChainType, +) \ No newline at end of file diff --git a/src/main/kotlin/com/api/nft/controller/dto/NftMetadataResponse.kt b/src/main/kotlin/com/api/nft/controller/dto/NftMetadataResponse.kt new file mode 100644 index 0000000..42be222 --- /dev/null +++ b/src/main/kotlin/com/api/nft/controller/dto/NftMetadataResponse.kt @@ -0,0 +1,17 @@ +package com.api.nft.controller.dto + +import com.api.nft.enums.ChainType +import com.api.nft.enums.ContractType + + +data class NftMetadataResponse( + val id: Long, + val tokenId: String, + val tokenAddress: String, + val contractType: ContractType, + val chainType: ChainType, + val nftName: String, + val collectionName: String, + val image: String, + val lastPrice: Double?, +) diff --git a/src/main/kotlin/com/api/nft/controller/dto/NftResponse.kt b/src/main/kotlin/com/api/nft/controller/dto/NftResponse.kt new file mode 100644 index 0000000..f82587b --- /dev/null +++ b/src/main/kotlin/com/api/nft/controller/dto/NftResponse.kt @@ -0,0 +1,10 @@ +package com.api.nft.controller.dto + +data class NftResponse( + val id: Long, + val collectionName: String, + val tokenId: String, + val tokenAddress: String, + val image: String, // metadata + val value: String, // attribute +) diff --git a/src/main/kotlin/com/api/nft/domain/Collection.kt b/src/main/kotlin/com/api/nft/domain/Collection.kt deleted file mode 100644 index 8c9aca4..0000000 --- a/src/main/kotlin/com/api/nft/domain/Collection.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.api.nft.domain - -import org.springframework.data.annotation.Id -import org.springframework.data.relational.core.mapping.Table - -@Table("collection") -class Collection( - @Id val name: String -) { -} \ No newline at end of file diff --git a/src/main/kotlin/com/api/nft/domain/attribute/Attribute.kt b/src/main/kotlin/com/api/nft/domain/attribute/Attribute.kt new file mode 100644 index 0000000..e6052fc --- /dev/null +++ b/src/main/kotlin/com/api/nft/domain/attribute/Attribute.kt @@ -0,0 +1,16 @@ +package com.api.nft.domain.attribute + +import org.springframework.data.annotation.Id +import org.springframework.data.relational.core.mapping.Table +import lombok.AllArgsConstructor + +@Table("attribute") +@AllArgsConstructor +data class Attribute( + @Id val id: Long? = null, + val nftId : Long, + val traitType : String? = null, + val value: String? = null, +) { + +} \ No newline at end of file diff --git a/src/main/kotlin/com/api/nft/domain/attribute/AttributeRepository.kt b/src/main/kotlin/com/api/nft/domain/attribute/AttributeRepository.kt new file mode 100644 index 0000000..8542b5a --- /dev/null +++ b/src/main/kotlin/com/api/nft/domain/attribute/AttributeRepository.kt @@ -0,0 +1,6 @@ +package com.api.nft.domain.attribute + +import org.springframework.data.repository.reactive.ReactiveCrudRepository + +interface AttributeRepository : ReactiveCrudRepository { +} \ No newline at end of file diff --git a/src/main/kotlin/com/api/nft/domain/collection/Collection.kt b/src/main/kotlin/com/api/nft/domain/collection/Collection.kt new file mode 100644 index 0000000..6971a68 --- /dev/null +++ b/src/main/kotlin/com/api/nft/domain/collection/Collection.kt @@ -0,0 +1,16 @@ +package com.api.nft.domain.collection + +import org.springframework.data.annotation.Id +import org.springframework.data.relational.core.mapping.Table +import lombok.AllArgsConstructor +import org.springframework.data.relational.core.mapping.Column + +@Table("collection") +@AllArgsConstructor +data class Collection( + @Id val name: String, + val logo: String?, + @Column("banner_image")val bannerImage: String?, + val description : String?, +) { +} \ No newline at end of file diff --git a/src/main/kotlin/com/api/nft/domain/CollectionRepository.kt b/src/main/kotlin/com/api/nft/domain/collection/repository/CollectionRepository.kt similarity index 73% rename from src/main/kotlin/com/api/nft/domain/CollectionRepository.kt rename to src/main/kotlin/com/api/nft/domain/collection/repository/CollectionRepository.kt index 014d2db..8db6a3f 100644 --- a/src/main/kotlin/com/api/nft/domain/CollectionRepository.kt +++ b/src/main/kotlin/com/api/nft/domain/collection/repository/CollectionRepository.kt @@ -1,5 +1,6 @@ -package com.api.nft.domain +package com.api.nft.domain.collection.repository +import com.api.nft.domain.collection.Collection import org.springframework.data.repository.reactive.ReactiveCrudRepository import reactor.core.publisher.Mono diff --git a/src/main/kotlin/com/api/nft/domain/CollectionRepositorySupport.kt b/src/main/kotlin/com/api/nft/domain/collection/repository/CollectionRepositorySupport.kt similarity index 58% rename from src/main/kotlin/com/api/nft/domain/CollectionRepositorySupport.kt rename to src/main/kotlin/com/api/nft/domain/collection/repository/CollectionRepositorySupport.kt index 4744224..e5ef1b7 100644 --- a/src/main/kotlin/com/api/nft/domain/CollectionRepositorySupport.kt +++ b/src/main/kotlin/com/api/nft/domain/collection/repository/CollectionRepositorySupport.kt @@ -1,5 +1,6 @@ -package com.api.nft.domain +package com.api.nft.domain.collection.repository +import com.api.nft.domain.collection.Collection import reactor.core.publisher.Mono interface CollectionRepositorySupport { diff --git a/src/main/kotlin/com/api/nft/domain/CollectionRepositorySupportImpl.kt b/src/main/kotlin/com/api/nft/domain/collection/repository/CollectionRepositorySupportImpl.kt similarity index 78% rename from src/main/kotlin/com/api/nft/domain/CollectionRepositorySupportImpl.kt rename to src/main/kotlin/com/api/nft/domain/collection/repository/CollectionRepositorySupportImpl.kt index 2b68941..eace44a 100644 --- a/src/main/kotlin/com/api/nft/domain/CollectionRepositorySupportImpl.kt +++ b/src/main/kotlin/com/api/nft/domain/collection/repository/CollectionRepositorySupportImpl.kt @@ -1,5 +1,6 @@ -package com.api.nft.domain +package com.api.nft.domain.collection.repository +import com.api.nft.domain.collection.Collection import org.springframework.data.r2dbc.core.R2dbcEntityTemplate import reactor.core.publisher.Mono diff --git a/src/main/kotlin/com/api/nft/domain/Metadata.kt b/src/main/kotlin/com/api/nft/domain/metadata/Metadata.kt similarity index 81% rename from src/main/kotlin/com/api/nft/domain/Metadata.kt rename to src/main/kotlin/com/api/nft/domain/metadata/Metadata.kt index 59ca3e9..f881e9d 100644 --- a/src/main/kotlin/com/api/nft/domain/Metadata.kt +++ b/src/main/kotlin/com/api/nft/domain/metadata/Metadata.kt @@ -1,10 +1,12 @@ -package com.api.nft.domain +package com.api.nft.domain.metadata import org.springframework.data.annotation.Id import org.springframework.data.relational.core.mapping.Column import org.springframework.data.relational.core.mapping.Table +import lombok.AllArgsConstructor @Table("metadata") +@AllArgsConstructor class Metadata( @Id val id: Long? = null, @Column("nft_id") val nftId: Long, diff --git a/src/main/kotlin/com/api/nft/domain/MetadataRepository.kt b/src/main/kotlin/com/api/nft/domain/metadata/repository/MetadataRepository.kt similarity index 61% rename from src/main/kotlin/com/api/nft/domain/MetadataRepository.kt rename to src/main/kotlin/com/api/nft/domain/metadata/repository/MetadataRepository.kt index bcecb94..096ddad 100644 --- a/src/main/kotlin/com/api/nft/domain/MetadataRepository.kt +++ b/src/main/kotlin/com/api/nft/domain/metadata/repository/MetadataRepository.kt @@ -1,5 +1,6 @@ -package com.api.nft.domain +package com.api.nft.domain.metadata.repository +import com.api.nft.domain.metadata.Metadata import org.springframework.data.repository.reactive.ReactiveCrudRepository interface MetadataRepository: ReactiveCrudRepository { diff --git a/src/main/kotlin/com/api/nft/domain/Nft.kt b/src/main/kotlin/com/api/nft/domain/nft/Nft.kt similarity index 54% rename from src/main/kotlin/com/api/nft/domain/Nft.kt rename to src/main/kotlin/com/api/nft/domain/nft/Nft.kt index 61256d4..f21e2eb 100644 --- a/src/main/kotlin/com/api/nft/domain/Nft.kt +++ b/src/main/kotlin/com/api/nft/domain/nft/Nft.kt @@ -1,17 +1,22 @@ -package com.api.nft.domain +package com.api.nft.domain.nft +import com.api.nft.enums.ChainType +import com.api.nft.enums.ContractType +import lombok.AllArgsConstructor import org.springframework.data.annotation.Id import org.springframework.data.relational.core.mapping.Column import org.springframework.data.relational.core.mapping.Table +//TODO("캡슐화") @Table("nft") -class Nft( +@AllArgsConstructor +data class Nft( @Id val id: Long? = null, @Column("token_id") val tokenId: String, @Column("token_address") val tokenAddress: String, - @Column("chain_type") val chinType: String, - @Column("nft_name")val nftName: String, - @Column("owner_of") val ownerOf: String?, + @Column("chain_type") val chainType: ChainType, + @Column("contract_type") val contractType: ContractType, + @Column("nft_name")val nftName: String?, @Column("token_hash")val tokenHash: String?, val collectionName: String, val amount: Int, diff --git a/src/main/kotlin/com/api/nft/domain/nft/NftAuction.kt b/src/main/kotlin/com/api/nft/domain/nft/NftAuction.kt new file mode 100644 index 0000000..f91b032 --- /dev/null +++ b/src/main/kotlin/com/api/nft/domain/nft/NftAuction.kt @@ -0,0 +1,19 @@ +package com.api.nft.domain.nft + +import com.api.nft.enums.ChainType +import com.api.nft.enums.StatusType +import org.springframework.data.relational.core.mapping.Table +import java.math.BigDecimal + +@Table("nft_auction") +data class NftAuction( + val id: Long, + val startingPrice: BigDecimal, + val chainType: ChainType, + val nftId: Long, + val statusType: StatusType, + val createdDate: Long, + val endDate: Long, + +) { +} \ No newline at end of file diff --git a/src/main/kotlin/com/api/nft/domain/nft/NftListing.kt b/src/main/kotlin/com/api/nft/domain/nft/NftListing.kt new file mode 100644 index 0000000..fb4f214 --- /dev/null +++ b/src/main/kotlin/com/api/nft/domain/nft/NftListing.kt @@ -0,0 +1,22 @@ +package com.api.nft.domain.nft + +import com.api.nft.enums.ChainType +import com.api.nft.enums.StatusType +import com.api.nft.enums.TokenType +import org.springframework.data.relational.core.mapping.Table +import java.math.BigDecimal + +@Table("nft_listing") +data class NftListing( + val id: Long, + val price: BigDecimal, + val chainType: ChainType, + val nftId: Long, + val statusType: StatusType, + val createdDate: Long, + val endDate: Long, +){ + fun updateStatus(statusType: StatusType): NftListing { + return this.copy(statusType = statusType) + } +} diff --git a/src/main/kotlin/com/api/nft/domain/nft/repository/NftAuctionRepository.kt b/src/main/kotlin/com/api/nft/domain/nft/repository/NftAuctionRepository.kt new file mode 100644 index 0000000..f8f9606 --- /dev/null +++ b/src/main/kotlin/com/api/nft/domain/nft/repository/NftAuctionRepository.kt @@ -0,0 +1,17 @@ +package com.api.nft.domain.nft.repository + +import com.api.nft.domain.nft.NftAuction +import com.api.nft.enums.StatusType +import org.springframework.data.r2dbc.repository.Query +import org.springframework.data.repository.reactive.ReactiveCrudRepository +import reactor.core.publisher.Mono + +interface NftAuctionRepository : ReactiveCrudRepository { + fun findByNftId(nftId: Long) : Mono + + @Query("UPDATE nft_auction SET status_type = :statusType WHERE nft_id = :nftId") + fun updateAuction(nftId: Long, statusType: StatusType): Mono + + fun deleteByNftId(nftId: Long): Mono + +} \ No newline at end of file diff --git a/src/main/kotlin/com/api/nft/domain/nft/repository/NftListingRepository.kt b/src/main/kotlin/com/api/nft/domain/nft/repository/NftListingRepository.kt new file mode 100644 index 0000000..45bbfb7 --- /dev/null +++ b/src/main/kotlin/com/api/nft/domain/nft/repository/NftListingRepository.kt @@ -0,0 +1,21 @@ +package com.api.nft.domain.nft.repository + +import com.api.nft.domain.nft.NftListing +import com.api.nft.enums.StatusType +import com.api.nft.enums.TokenType +import org.springframework.data.r2dbc.repository.Query +import org.springframework.data.r2dbc.repository.R2dbcRepository +import org.springframework.data.repository.reactive.ReactiveCrudRepository +import reactor.core.publisher.Mono +import java.math.BigDecimal + +interface NftListingRepository : ReactiveCrudRepository { + + fun findByNftId(nftId: Long) : Mono + + @Query("UPDATE nft_listing SET status_type = :statusType WHERE nft_id = :nftId") + fun updateListing(nftId: Long, statusType: StatusType): Mono + + fun deleteByNftId(nftId: Long): Mono + +} \ No newline at end of file diff --git a/src/main/kotlin/com/api/nft/domain/nft/repository/NftRepository.kt b/src/main/kotlin/com/api/nft/domain/nft/repository/NftRepository.kt new file mode 100644 index 0000000..96b6c6a --- /dev/null +++ b/src/main/kotlin/com/api/nft/domain/nft/repository/NftRepository.kt @@ -0,0 +1,11 @@ +package com.api.nft.domain.nft.repository + +import com.api.nft.domain.nft.Nft +import com.api.nft.enums.ChainType +import org.springframework.data.repository.reactive.ReactiveCrudRepository +import reactor.core.publisher.Mono + +interface NftRepository : ReactiveCrudRepository, NftRepositorySupport { + + fun findByTokenAddressAndTokenIdAndChainType(tokenAddress: String, tokenId: String,chainType: ChainType): Mono +} \ No newline at end of file diff --git a/src/main/kotlin/com/api/nft/domain/nft/repository/NftRepositorySupport.kt b/src/main/kotlin/com/api/nft/domain/nft/repository/NftRepositorySupport.kt new file mode 100644 index 0000000..188b722 --- /dev/null +++ b/src/main/kotlin/com/api/nft/domain/nft/repository/NftRepositorySupport.kt @@ -0,0 +1,10 @@ +package com.api.nft.domain.nft.repository + +import com.api.nft.controller.dto.NftMetadataResponse +import reactor.core.publisher.Flux +import reactor.core.publisher.Mono + +interface NftRepositorySupport { + fun findByNftJoinMetadata(nftId: Long) : Mono + fun findAllByNftJoinMetadata(nftIds: List) : Flux +} \ No newline at end of file diff --git a/src/main/kotlin/com/api/nft/domain/nft/repository/NftRepositorySupportImpl.kt b/src/main/kotlin/com/api/nft/domain/nft/repository/NftRepositorySupportImpl.kt new file mode 100644 index 0000000..da84206 --- /dev/null +++ b/src/main/kotlin/com/api/nft/domain/nft/repository/NftRepositorySupportImpl.kt @@ -0,0 +1,90 @@ +package com.api.nft.domain.nft.repository + +import com.api.nft.controller.dto.NftMetadataResponse +import com.api.nft.enums.ChainType +import com.api.nft.enums.ContractType +import org.springframework.data.r2dbc.core.R2dbcEntityTemplate +import reactor.core.publisher.Flux +import reactor.core.publisher.Mono +import java.math.BigDecimal + +class NftRepositorySupportImpl( + private val r2dbcEntityTemplate: R2dbcEntityTemplate +): NftRepositorySupport { + override fun findByNftJoinMetadata(id: Long): Mono { + val query = """ + SELECT + n.id, + n.token_id AS tokenId, + n.token_address AS tokenAddress, + n.chain_type AS chainType, + n.nft_name AS nftName, + n.collection_name AS collectionName, + n.contract_type AS contractType, + m.image AS image, + nl.price AS lastPrice + FROM + nft n + LEFT JOIN + metadata m ON n.id = m.nft_id + LEFT JOIN + nft_listing nl ON n.id = nl.nft_id + WHERE + n.id = :$1 + """ + + return r2dbcEntityTemplate.databaseClient.sql(query) + .bind(0, id) + .map { row, data -> + NftMetadataResponse( + id = (row.get("id") as Number).toLong(), + tokenId = row.get("tokenId", String::class.java)!!, + tokenAddress = row.get("tokenAddress", String::class.java)!!, + chainType = row.get("chainType", ChainType::class.java)!!, + nftName = row.get("nftName", String::class.java)!!, + collectionName = row.get("collectionName", String::class.java)!!, + image = row.get("image", String::class.java) ?: "", + contractType = row.get("contractType", ContractType::class.java)!!, + lastPrice = row.get("lastPrice", BigDecimal::class.java)?.toDouble(), + ) + }.first() + } + + override fun findAllByNftJoinMetadata(ids: List): Flux { + val query = """ + SELECT + n.id, + n.token_id AS tokenId, + n.token_address AS tokenAddress, + n.chain_type AS chainType, + n.nft_name AS nftName, + n.collection_name AS collectionName, + n.contract_type AS contractType, + m.image AS image + FROM + nft n + LEFT JOIN + metadata m ON n.id = m.nft_id + LEFT JOIN nft_listing nl ON nft.id = nl.nft_id + WHERE + n.id IN (:$1) + """ + return r2dbcEntityTemplate.databaseClient.sql(query) + .bind(0, ids) + .map { row, metadata -> + NftMetadataResponse( + id = (row.get("id") as Number).toLong(), + tokenId = row.get("tokenId", String::class.java)!!, + tokenAddress = row.get("tokenAddress", String::class.java)!!, + chainType = row.get("chainType", ChainType::class.java)!!, + nftName = row.get("nftName", String::class.java)!!, + collectionName = row.get("collectionName", String::class.java)!!, + image = row.get("image", String::class.java) ?: "", + contractType = row.get("contractType", ContractType::class.java)!!, + lastPrice = row.get("lastPrice", BigDecimal::class.java)?.toDouble(), + ) + } + .all() + } + +} \ No newline at end of file diff --git a/src/main/kotlin/com/api/nft/domain/repository/NftRepository.kt b/src/main/kotlin/com/api/nft/domain/repository/NftRepository.kt deleted file mode 100644 index 01cfc33..0000000 --- a/src/main/kotlin/com/api/nft/domain/repository/NftRepository.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.api.nft.domain.repository - -import com.api.nft.domain.Nft -import org.springframework.data.repository.reactive.ReactiveCrudRepository -import reactor.core.publisher.Mono - -interface NftRepository : ReactiveCrudRepository { - - fun findByTokenAddressAndTokenId(tokenADdress: String, tokenId: String): Mono -} \ No newline at end of file diff --git a/src/main/kotlin/com/api/nft/domain/trasfer/Transfer.kt b/src/main/kotlin/com/api/nft/domain/trasfer/Transfer.kt new file mode 100644 index 0000000..7141e59 --- /dev/null +++ b/src/main/kotlin/com/api/nft/domain/trasfer/Transfer.kt @@ -0,0 +1,18 @@ +package com.api.nft.domain.trasfer + +import org.springframework.data.annotation.Id +import org.springframework.data.relational.core.mapping.Column +import org.springframework.data.relational.core.mapping.Table + +@Table("transfer") +data class Transfer( + @Id val id: Long? = null, + @Column("nft_id") val nftId: Long, + @Column("from_address") val fromAddress: String, + @Column("to_address") val toAddress: String, + @Column("block_number") val blockNumber: Long, + @Column("block_timestamp") val blockTimestamp: Long?, + ) { + + +} \ No newline at end of file diff --git a/src/main/kotlin/com/api/nft/domain/trasfer/TransferRepository.kt b/src/main/kotlin/com/api/nft/domain/trasfer/TransferRepository.kt new file mode 100644 index 0000000..458104b --- /dev/null +++ b/src/main/kotlin/com/api/nft/domain/trasfer/TransferRepository.kt @@ -0,0 +1,9 @@ +package com.api.nft.domain.trasfer + +import org.springframework.data.repository.reactive.ReactiveCrudRepository +import reactor.core.publisher.Flux + +interface TransferRepository: ReactiveCrudRepository { + + fun findByNftIdOrderByBlockTimestampDesc(nftId: Long) : Flux +} \ No newline at end of file diff --git a/src/main/kotlin/com/api/nft/enums/Enums.kt b/src/main/kotlin/com/api/nft/enums/Enums.kt index dfae135..7dde924 100644 --- a/src/main/kotlin/com/api/nft/enums/Enums.kt +++ b/src/main/kotlin/com/api/nft/enums/Enums.kt @@ -2,8 +2,27 @@ package com.api.nft.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 NetworkType{ + ETHEREUM, + POLYGON, +} + +enum class ContractType{ + ERC721, ERC1155 +} + +enum class TokenType { + SAND, MATIC, ETH, BTC +} + +enum class StatusType { RESERVATION, ACTIVED, RESERVATION_CANCEL, CANCEL, EXPIRED,LISTING, AUCTION } \ No newline at end of file diff --git a/src/main/kotlin/com/api/nft/event/NftEventListener.kt b/src/main/kotlin/com/api/nft/event/NftEventListener.kt new file mode 100644 index 0000000..8012940 --- /dev/null +++ b/src/main/kotlin/com/api/nft/event/NftEventListener.kt @@ -0,0 +1,17 @@ +package com.api.nft.event + +import com.api.nft.event.dto.NftCreatedEvent +import com.api.nft.rabbitMQ.RabbitMQSender +import org.springframework.context.event.EventListener +import org.springframework.stereotype.Component + +@Component +class NftEventListener( + private val provider: RabbitMQSender +){ + + @EventListener + fun onNftCreated(event: NftCreatedEvent) { + provider.nftSend(event.nft) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/api/nft/event/dto/NftCreatedEvent.kt b/src/main/kotlin/com/api/nft/event/dto/NftCreatedEvent.kt new file mode 100644 index 0000000..c9db6bd --- /dev/null +++ b/src/main/kotlin/com/api/nft/event/dto/NftCreatedEvent.kt @@ -0,0 +1,6 @@ +package com.api.nft.event.dto + +import org.springframework.context.ApplicationEvent + +data class NftCreatedEvent(val eventSource: Any, val nft: NftResponse): ApplicationEvent(eventSource) + diff --git a/src/main/kotlin/com/api/nft/event/dto/NftResponse.kt b/src/main/kotlin/com/api/nft/event/dto/NftResponse.kt new file mode 100644 index 0000000..e4df42a --- /dev/null +++ b/src/main/kotlin/com/api/nft/event/dto/NftResponse.kt @@ -0,0 +1,10 @@ +package com.api.nft.event.dto + +import com.api.nft.enums.ChainType + +data class NftResponse( + val id : Long, + val tokenId: String, + val tokenAddress: String, + val chainType: ChainType, +) diff --git a/src/main/kotlin/com/api/nft/properties/ApiKeysProperties.kt b/src/main/kotlin/com/api/nft/properties/ApiKeysProperties.kt new file mode 100644 index 0000000..b3e171e --- /dev/null +++ b/src/main/kotlin/com/api/nft/properties/ApiKeysProperties.kt @@ -0,0 +1,9 @@ +package com.api.nft.properties + +import org.springframework.boot.context.properties.ConfigurationProperties + +@ConfigurationProperties(prefix = "apikey") +data class ApiKeysProperties( + val infura: String, + val moralis: String, +) diff --git a/src/main/kotlin/com/api/nft/rabbitMQ/RabbitMQReceiver.kt b/src/main/kotlin/com/api/nft/rabbitMQ/RabbitMQReceiver.kt new file mode 100644 index 0000000..3dc84bf --- /dev/null +++ b/src/main/kotlin/com/api/nft/rabbitMQ/RabbitMQReceiver.kt @@ -0,0 +1,36 @@ +package com.api.nft.rabbitMQ + +import com.api.nft.service.api.NftAuctionService +import com.api.nft.service.api.NftListingService +import com.api.nft.service.dto.ListingResponse +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 +import com.api.nft.service.dto.AuctionResponse as AuctionResponse + +@Service +class RabbitMQReceiver( + private val nftListingService: NftListingService, + private val nftAuctionService: NftAuctionService +) { + + + @RabbitListener(bindings = [QueueBinding( + value = Queue(name = "", durable = "false", exclusive = "true", autoDelete = "true"), + exchange = Exchange(value = "listingExchange", type = ExchangeTypes.FANOUT) + )]) + fun listingMessage(listing: ListingResponse){ + nftListingService.update(listing).subscribe() + } + + @RabbitListener(bindings = [QueueBinding( + value = Queue(name = "", durable = "false", exclusive = "true", autoDelete = "true"), + exchange = Exchange(value = "auctionExchange", type = ExchangeTypes.FANOUT) + )]) + fun auctionMessage(auction: AuctionResponse){ + nftAuctionService.update(auction).subscribe() + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/api/nft/rabbitMQ/RabbitMQSender.kt b/src/main/kotlin/com/api/nft/rabbitMQ/RabbitMQSender.kt new file mode 100644 index 0000000..ff507a7 --- /dev/null +++ b/src/main/kotlin/com/api/nft/rabbitMQ/RabbitMQSender.kt @@ -0,0 +1,15 @@ +package com.api.nft.rabbitMQ + +import com.api.nft.event.dto.NftResponse +import org.springframework.amqp.rabbit.core.RabbitTemplate +import org.springframework.stereotype.Service + +@Service +class RabbitMQSender( + private val rabbitTemplate: RabbitTemplate +) { + + fun nftSend(nft: NftResponse) { + rabbitTemplate.convertAndSend("nftExchange", "", nft) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/api/nft/service/CollectionService.kt b/src/main/kotlin/com/api/nft/service/CollectionService.kt deleted file mode 100644 index 75850a9..0000000 --- a/src/main/kotlin/com/api/nft/service/CollectionService.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.api.nft.service - -import com.api.nft.domain.Collection -import com.api.nft.domain.CollectionRepository -import org.springframework.stereotype.Service -import reactor.core.publisher.Mono - -@Service -class CollectionService( - private val collectionRepository: CollectionRepository, -) { - - fun findOrCreate(name: String) : Mono { - return collectionRepository.findByName(name) - .switchIfEmpty( - collectionRepository.insert(Collection(name = name)) - ) - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/api/nft/service/MoralisApiService.kt b/src/main/kotlin/com/api/nft/service/MoralisApiService.kt deleted file mode 100644 index cadb2a3..0000000 --- a/src/main/kotlin/com/api/nft/service/MoralisApiService.kt +++ /dev/null @@ -1,48 +0,0 @@ -package com.api.nft.service - -import com.api.nft.enums.ChainType -import com.api.nft.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 MoralisApiService { - - private val webClient = WebClient.builder() - .baseUrl(baseUrl) - .build() - - private fun queryParamByChain(chain: ChainType): String? { - val chain = when (chain) { - ChainType.ETHEREUM_MAINNET -> "eth" - ChainType.POLYGON_MAINNET -> "polygon" - ChainType.ETHREUM_GOERLI -> "goerli" - ChainType.POLYGON_MUMBAI -> "munbai" - ChainType.ETHREUM_SEPOLIA -> "sepolia" - } - return chain - } - - fun getNft(tokenAddress: String,tokenId: String,chainType: ChainType): Mono { - val chain = queryParamByChain(chainType) - - return webClient.get() - .uri { - it.path("/v2.2/nft/${tokenAddress}/${tokenId}") - it.queryParam("chain", chain) - it.build() - } - .header("X-API-Key", apiKey) - .header("Accept", MediaType.APPLICATION_JSON_VALUE) - .retrieve() - .bodyToMono(NftResponse::class.java) - } - - companion object { - private val apiKey = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJub25jZSI6ImJiNmIxMWJmLWNmNzItNDg0OC04OGEyLTBjYTIwODRjN2VhMyIsIm9yZ0lkIjoiMzgzODQwIiwidXNlcklkIjoiMzk0NDAyIiwidHlwZUlkIjoiMGZlYWQ5NDctZjQwZS00MDkwLWFlNGUtOTA1ZTdmMjUxZTAzIiwidHlwZSI6IlBST0pFQ1QiLCJpYXQiOjE3MTA5NTIwMjYsImV4cCI6NDg2NjcxMjAyNn0.VQE60IPGiWxdp7jKLF0jzXnxrLjEpU56H4bnfhMt0Sw" - private val baseUrl = "https://deep-index.moralis.io/api" - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/api/nft/service/NftService.kt b/src/main/kotlin/com/api/nft/service/NftService.kt deleted file mode 100644 index 08285db..0000000 --- a/src/main/kotlin/com/api/nft/service/NftService.kt +++ /dev/null @@ -1,79 +0,0 @@ -package com.api.nft.service - -import com.api.nft.domain.Metadata -import com.api.nft.domain.MetadataRepository -import com.api.nft.domain.Nft -import com.api.nft.domain.repository.NftRepository -import com.api.nft.enums.ChainType -import com.api.nft.service.dto.AttributeResponse -import com.api.nft.service.dto.MetadataResponse -import com.api.nft.service.dto.NftResponse -import org.springframework.stereotype.Service -import reactor.core.publisher.Mono - -@Service -class NftService( - private val moralisApiService: MoralisApiService, - private val nftRepository: NftRepository, - private val collectionService: CollectionService, - private val metadataService: metadataService, -) { - -// fun findOrCreateNft(tokenId: String, tokenAddress: String, chainType: ChainType) { -// nftRepository.findByTokenAddressAndTokenId(tokenAddress,tokenId) -// .switchIfEmpty( -// createNftProcess() -//// getNftByMoralis(tokenId,tokenAddress,chainType) -//// // 이 후 nft외래키를 가지고 metadata 생성 -//// // 또 그 이후 nft외래키를 가지고 attribute 생성 -// ) -// } -// - fun createNftProcess(tokenId: String, - tokenAddress: String, - chainType: ChainType - ): Mono { - val response = getNftByMoralis(tokenId, tokenAddress, chainType) - return response.flatMap { - createNft(it.first,it.second,it.third,chainType) // 생성되고 밑에 flatMap 실행 async - .flatMap {nft -> - metadataService.createMetadata(nft.id!!,it.second) - } - } - } - - fun getNftByMoralis(tokenId: String, - tokenAddress: String, - chainType: ChainType - ) - : Mono>> { - return moralisApiService.getNft(tokenAddress,tokenId,chainType) - .filter { !it.possibleSpam } - .map { - val metadata = MetadataResponse.toMetadataResponse(it.metadata) - val attribute = AttributeResponse.toAttributeResponse(metadata.attributes) - Triple(it,metadata,attribute) - } - } - - fun createNft(nft: NftResponse, - metadata: MetadataResponse, - attribute: List, - chainType: ChainType - ): Mono { - return collectionService.findOrCreate(nft.name).flatMap { - nftRepository.save( - Nft( - tokenId = nft.tokenId, - tokenAddress = nft.tokenAddress, - chinType = chainType.toString(), - nftName = metadata.name, - collectionName = it.name, - ownerOf = nft.ownerOf, - tokenHash = nft.tokenHash, - amount = nft.amount.toInt() - ) - ) - } - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/api/nft/service/RedisService.kt b/src/main/kotlin/com/api/nft/service/RedisService.kt new file mode 100644 index 0000000..dcf0966 --- /dev/null +++ b/src/main/kotlin/com/api/nft/service/RedisService.kt @@ -0,0 +1,25 @@ +package com.api.nft.service + +import com.api.nft.domain.nft.repository.NftRepository +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.data.redis.core.ReactiveRedisTemplate +import org.springframework.stereotype.Service +import reactor.core.publisher.Mono + +@Service +class RedisService( + private val reactiveRedisTemplate: ReactiveRedisTemplate, + private val nftRepository: NftRepository, +) { + + private val logger: Logger = LoggerFactory.getLogger(RedisService::class.java) + + fun updateToRedis(nftId: Long): Mono { + return nftRepository.findByNftJoinMetadata(nftId) + .flatMap { data -> + println("data: " + data.id) + reactiveRedisTemplate.opsForValue().set("NFT:${nftId}", data).then() + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/api/nft/service/TransferResponse.kt b/src/main/kotlin/com/api/nft/service/TransferResponse.kt new file mode 100644 index 0000000..cb414bc --- /dev/null +++ b/src/main/kotlin/com/api/nft/service/TransferResponse.kt @@ -0,0 +1,9 @@ +package com.api.nft.service + +import java.math.BigInteger + +data class TransferResponse( + val from: String, + val to: String, + val tokenId: BigInteger +) diff --git a/src/main/kotlin/com/api/nft/service/api/AttributeService.kt b/src/main/kotlin/com/api/nft/service/api/AttributeService.kt new file mode 100644 index 0000000..ce2e57f --- /dev/null +++ b/src/main/kotlin/com/api/nft/service/api/AttributeService.kt @@ -0,0 +1,25 @@ +package com.api.nft.service.api + +import com.api.nft.domain.attribute.Attribute +import com.api.nft.domain.attribute.AttributeRepository +import com.api.nft.service.external.dto.NftAttribute +import org.springframework.stereotype.Service +import reactor.core.publisher.Flux + +@Service +class AttributeService( + private val attributeRepository: AttributeRepository, +) { + + fun createAttribute(nftId: Long, attributes: List): Flux { + return Flux.fromIterable(attributes).flatMap { + attributeRepository.save( + Attribute( + nftId = nftId, + traitType = it.traitType ?: null, + value = it.value ?: null + ) + ) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/api/nft/service/api/CollectionService.kt b/src/main/kotlin/com/api/nft/service/api/CollectionService.kt new file mode 100644 index 0000000..78b8c52 --- /dev/null +++ b/src/main/kotlin/com/api/nft/service/api/CollectionService.kt @@ -0,0 +1,36 @@ +package com.api.nft.service.api + +import com.api.nft.domain.collection.Collection +import com.api.nft.domain.collection.repository.CollectionRepository +import org.springframework.dao.DuplicateKeyException +import org.springframework.stereotype.Service +import reactor.core.publisher.Mono + +@Service +class CollectionService( + private val collectionRepository: CollectionRepository, +) { + + fun findOrCreate( + name: String, + logo: String?, + bannerImage: String?, + description: String?, + ) : Mono { + return collectionRepository.findByName(name) + .switchIfEmpty( + Mono.defer { + collectionRepository.insert( + Collection( + name = name, + logo = logo, + bannerImage = bannerImage, + description = description, + )) + .onErrorResume(DuplicateKeyException::class.java){ + collectionRepository.findByName(name) + } + } + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/api/nft/service/metadataService.kt b/src/main/kotlin/com/api/nft/service/api/MetadataService.kt similarity index 51% rename from src/main/kotlin/com/api/nft/service/metadataService.kt rename to src/main/kotlin/com/api/nft/service/api/MetadataService.kt index 09bda8c..10da639 100644 --- a/src/main/kotlin/com/api/nft/service/metadataService.kt +++ b/src/main/kotlin/com/api/nft/service/api/MetadataService.kt @@ -1,22 +1,22 @@ -package com.api.nft.service +package com.api.nft.service.api -import com.api.nft.domain.Metadata -import com.api.nft.domain.MetadataRepository -import com.api.nft.service.dto.MetadataResponse +import com.api.nft.domain.metadata.Metadata +import com.api.nft.domain.metadata.repository.MetadataRepository +import com.api.nft.service.external.dto.NftMetadata import org.springframework.stereotype.Service import reactor.core.publisher.Mono @Service -class metadataService( +class MetadataService( private val metadataRepository: MetadataRepository, ) { - fun createMetadata(nftId: Long, metadata: MetadataResponse): Mono { + fun createMetadata(nftId: Long, metadata: NftMetadata): Mono { return metadataRepository.save( Metadata( nftId = nftId, description = metadata.description, - image = MetadataResponse.parseImage(metadata.image), + image = NftMetadata.parseImage(metadata.image), animationUrl = metadata.animationUrl, ) ) diff --git a/src/main/kotlin/com/api/nft/service/api/NftAuctionService.kt b/src/main/kotlin/com/api/nft/service/api/NftAuctionService.kt new file mode 100644 index 0000000..78dae6d --- /dev/null +++ b/src/main/kotlin/com/api/nft/service/api/NftAuctionService.kt @@ -0,0 +1,49 @@ +package com.api.nft.service.api + +import com.api.nft.domain.nft.NftAuction +import com.api.nft.domain.nft.repository.NftAuctionRepository +import com.api.nft.enums.StatusType +import com.api.nft.service.dto.AuctionResponse +import org.springframework.stereotype.Service +import reactor.core.publisher.Mono + +@Service +class NftAuctionService( + private val nftAuctionRepository: NftAuctionRepository, +) { + + fun update(newAuction: AuctionResponse): Mono { + return when (newAuction.statusType) { + StatusType.RESERVATION -> { + save(newAuction) + } + StatusType.ACTIVED -> { + nftAuctionRepository.findByNftId(newAuction.nftId) + .flatMap { + nftAuctionRepository.updateAuction(nftId = newAuction.nftId, statusType = StatusType.AUCTION) + } + } + StatusType.RESERVATION_CANCEL, StatusType.CANCEL, StatusType.EXPIRED -> { + nftAuctionRepository.findByNftId(newAuction.nftId) + .flatMap { nftAuction -> + nftAuctionRepository.deleteByNftId(nftAuction.nftId) + } + + } + else -> Mono.empty() + } + } + fun save(auction: AuctionResponse) : Mono { + return nftAuctionRepository.save( + NftAuction( + id = auction.id, + nftId = auction.nftId, + startingPrice = auction.startingPrice, + chainType = auction.chainType, + statusType = auction.statusType, + createdDate = auction.createdDateTime, + endDate = auction.endDateTime, + ) + ).then() + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/api/nft/service/api/NftListingService.kt b/src/main/kotlin/com/api/nft/service/api/NftListingService.kt new file mode 100644 index 0000000..d40058c --- /dev/null +++ b/src/main/kotlin/com/api/nft/service/api/NftListingService.kt @@ -0,0 +1,54 @@ +package com.api.nft.service.api + +import com.api.nft.domain.nft.NftListing +import com.api.nft.domain.nft.repository.NftListingRepository +import com.api.nft.enums.StatusType +import com.api.nft.service.RedisService +import com.api.nft.service.dto.ListingResponse +import org.springframework.stereotype.Service +import reactor.core.publisher.Mono + +@Service +class NftListingService( + private val nftListingRepository: NftListingRepository, + private val redisService: RedisService, +) { + + fun update(newListing: ListingResponse): Mono { + return when (newListing.statusType) { + StatusType.RESERVATION -> { + save(newListing) + .then(redisService.updateToRedis(newListing.nftId)) + } + StatusType.ACTIVED -> { + nftListingRepository.findByNftId(newListing.nftId) + .flatMap { nftListing -> + nftListingRepository.updateListing(nftId = nftListing.nftId, statusType = StatusType.LISTING) + } + .then(redisService.updateToRedis(newListing.nftId)) + } + StatusType.RESERVATION_CANCEL, StatusType.CANCEL, StatusType.EXPIRED -> { + nftListingRepository.findByNftId(newListing.nftId) + .flatMap { nftListing -> + nftListingRepository.deleteByNftId(nftListing.nftId) + } + .then(redisService.updateToRedis(newListing.nftId)) + } + else -> Mono.empty() + } + } + fun save(listing: ListingResponse) : Mono { + return nftListingRepository.save( + NftListing( + id = listing.id, + nftId = listing.nftId, + price = listing.price, + chainType = listing.chainType, + statusType = listing.statusType, + createdDate = listing.createdDateTime, + endDate = listing.endDateTime, + ) + ) + } + +} \ No newline at end of file diff --git a/src/main/kotlin/com/api/nft/service/api/NftService.kt b/src/main/kotlin/com/api/nft/service/api/NftService.kt new file mode 100644 index 0000000..4f21745 --- /dev/null +++ b/src/main/kotlin/com/api/nft/service/api/NftService.kt @@ -0,0 +1,144 @@ +package com.api.nft.service.api + +import com.api.nft.controller.dto.NftMetadataResponse +import com.api.nft.domain.nft.Nft +import com.api.nft.domain.nft.repository.NftRepository +import com.api.nft.enums.ChainType +import com.api.nft.enums.ContractType +import com.api.nft.event.dto.NftCreatedEvent +import com.api.nft.event.dto.NftResponse +import com.api.nft.service.RedisService +import com.api.nft.service.external.dto.NftAttribute +import com.api.nft.service.external.dto.NftData +import com.api.nft.service.external.dto.NftMetadata +import com.api.nft.service.external.moralis.MoralisApiService +import org.springframework.context.ApplicationEventPublisher +import org.springframework.stereotype.Service +import reactor.core.publisher.Flux +import reactor.core.publisher.Mono + +@Service +class NftService( + private val nftRepository: NftRepository, + private val collectionService: CollectionService, + private val metadataService: MetadataService, + private val attributeService: AttributeService, + private val eventPublisher: ApplicationEventPublisher, + private val transferService: TransferService, + private val moralisApiService: MoralisApiService, + private val redisService: RedisService, +) { + + fun findAllById(ids: List): Flux { + return nftRepository.findAllByNftJoinMetadata(ids) + .flatMap { nft -> + redisService.updateToRedis(nft.id) + .thenReturn(nft) + } + } + + fun findById(id: Long): Mono { + return nftRepository.findByNftJoinMetadata(id) + .flatMap { nft -> + redisService.updateToRedis(nft.id) + .thenReturn(nft) + } + } + + + fun findOrCreateNft(tokenAddress: String,tokenId: String, chainType: ChainType): Mono { + return nftRepository.findByTokenAddressAndTokenIdAndChainType(tokenAddress,tokenId,chainType) + .switchIfEmpty( + moralisApiService.getNFTMetadata(tokenAddress,tokenId,chainType) + .flatMap { createNftProcess(it,chainType) } + ).map { it.toResponse() } + } + + fun getByWalletNft(wallet: String,chainType: ChainType): Flux { + return moralisApiService.getNFTsByAddress(wallet, chainType) + .flatMapMany { Flux.fromIterable(it.result) } + .filter { it.contractType == "ERC721"} + .flatMap { findOrCreateNft(it.tokenAddress, it.tokenId, chainType) } + } + + + fun createNftProcess(request: NftData, chainType: ChainType): Mono { + val response = getNftData(request, chainType) + return response + .flatMap { (nftData, metadataData, attributeDataList) -> + createMetadata(nftData, metadataData, attributeDataList ,chainType) } + .flatMap { createdNft -> + redisService.updateToRedis(createdNft.id!!).thenReturn(createdNft) } + // .flatMap { nft -> + // transferService.createTransfer(nft).thenReturn(nft) } + .doOnSuccess { + eventPublisher.publishEvent(NftCreatedEvent(this, it.toResponse())) + } + } + + + private fun createMetadata( + nftData: NftData, + metadataData: NftMetadata, + attributeDataList: List?, + chainType: ChainType + ): Mono { + return createNft(nftData, metadataData, chainType) + .flatMap { nft -> + metadataService.createMetadata(nft.id!!, metadataData) + .thenMany(attributeService.createAttribute(nft.id, attributeDataList ?: emptyList())) + .then(Mono.just(nft)) + } + } + + private fun Nft.toResponse() = NftResponse( + id = this.id!!, + tokenId = this.tokenId, + tokenAddress = this.tokenAddress, + chainType = this.chainType, + ) + + fun getNftData(request: NftData, chainType: ChainType): Mono?>> { + return Mono.fromCallable { + val metadata = NftMetadata.toMetadataResponse(request.metadata) + val attributes = metadata.attributes.let { + if (it.isNotEmpty()) NftAttribute.toAttributeResponse(it) else null + } + Triple(request, metadata, attributes) + } + } + + + fun createNft(nft: NftData, + metadata: NftMetadata?, + chainType: ChainType + ): Mono { + return collectionService.findOrCreate( + nft.name, + nft.collectionLogo ?: metadata?.image, + nft.collectionBannerImage ?: metadata?.image, + metadata?.description, + ).flatMap { + nftRepository.save( + Nft( + tokenId = nft.tokenId, + tokenAddress = nft.tokenAddress, + chainType = chainType, + nftName = metadata?.name, + collectionName = it.name, + tokenHash = nft.tokenHash, + amount = nft.amount?.toInt() ?: 0, + contractType = nft.contractType.toContractEnum(), + ) + ) + } + } + + fun String.toContractEnum() : ContractType { + return when(this) { + "ERC721" -> ContractType.ERC721 + "ERC1155" -> ContractType.ERC1155 + else -> throw IllegalArgumentException("not support contractType") + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/api/nft/service/api/TransferService.kt b/src/main/kotlin/com/api/nft/service/api/TransferService.kt new file mode 100644 index 0000000..dde0381 --- /dev/null +++ b/src/main/kotlin/com/api/nft/service/api/TransferService.kt @@ -0,0 +1,97 @@ +package com.api.nft.service.api + +import com.api.nft.domain.nft.Nft +import com.api.nft.domain.nft.repository.NftRepository +import com.api.nft.domain.trasfer.Transfer +import com.api.nft.domain.trasfer.TransferRepository +import com.api.nft.enums.ChainType +import com.api.nft.service.external.dto.EthLogResponse +import com.api.nft.service.external.dto.ResultData +import com.api.nft.service.external.infura.InfuraApiService +import com.api.nft.service.external.moralis.MoralisApiService +import com.api.nft.util.Util.convertNetworkTypeToChainType +import com.api.nft.util.Util.toEpochMilli +import org.springframework.stereotype.Service +import reactor.core.publisher.Flux +import reactor.core.publisher.Mono +import java.math.BigInteger + + +@Service +class TransferService( + private val moralisApiService: MoralisApiService, + private val transferRepository: TransferRepository, + private val nftRepository: NftRepository, + private val infuraApiService: InfuraApiService, +) { + + fun createTransfer(nft: Nft): Mono { + return moralisApiService.getNftTransfer( + nft.tokenAddress, + nft.tokenId, + nft.chainType + ).flatMapMany { + Flux.fromIterable(it.result) + }.map { + it.toEntity(nft.id!!) + }.flatMap { transfer -> + transferRepository.save(transfer) + }.doOnError { error -> + println("Error during transfer creation: $error") + }.then() + } + private fun ResultData.toEntity(nftId: Long): Transfer = + Transfer( + nftId = nftId, + fromAddress = this.fromAddress, + toAddress = this.toAddress, + blockNumber = this.blockNumber.toLong(), + blockTimestamp = this.blockTimestamp.toEpochMilli() + ) + + fun findOrUpdateByNftId(nftId: Long): Flux { + return nftRepository.findById(nftId).flatMapMany { nft -> + transferRepository.findByNftIdOrderByBlockTimestampDesc(nftId).next() + .flatMapMany { + updateByTransfer(it,nft) + .thenMany( transferRepository.findByNftIdOrderByBlockTimestampDesc(nftId)) + } + } + } + + fun updateByTransfer(transfer: Transfer, nft: Nft): Mono { + val fromBlock = transfer.blockNumber + 1 + return infuraApiService.getEthLogs( + fromBlock.toString(), + nft.chainType, + nft.tokenAddress, + nft.tokenId + ).flatMapMany { ethLogResponses -> + Flux.fromIterable(ethLogResponses) + }.flatMap { ethLogResponse -> + ethLogResponse.toTransferEntity(nft.id!!, nft.chainType) + .flatMap { + transferRepository.save(it) + } + }.then() + } + + + private fun EthLogResponse.toTransferEntity(nftId: Long, chainType: ChainType): Mono { + return infuraApiService.getBlockTimestamp(blockNumber, chainType) + .map { timestamp -> + Transfer( + toAddress = parseAddress(topics[2]), + fromAddress = parseAddress(topics[1]), blockNumber = BigInteger(blockNumber.substring(2), 16).toLong(), + nftId = nftId, + blockTimestamp = timestamp + ) + } + } + + private fun parseAddress(address: String): String { + return "0x" + address.substring(26).padStart(40, '0') + } + +} + diff --git a/src/main/kotlin/com/api/nft/service/dto/AuctionResponse.kt b/src/main/kotlin/com/api/nft/service/dto/AuctionResponse.kt new file mode 100644 index 0000000..b133eb0 --- /dev/null +++ b/src/main/kotlin/com/api/nft/service/dto/AuctionResponse.kt @@ -0,0 +1,16 @@ +package com.api.nft.service.dto + +import com.api.nft.enums.ChainType +import com.api.nft.enums.StatusType +import java.math.BigDecimal + +data class AuctionResponse( + val id : Long, + val nftId : Long, + val address: String, + val createdDateTime: Long, + val endDateTime: Long, + val statusType: StatusType, + val startingPrice: BigDecimal, + val chainType: ChainType +) diff --git a/src/main/kotlin/com/api/nft/service/dto/ListingResponse.kt b/src/main/kotlin/com/api/nft/service/dto/ListingResponse.kt new file mode 100644 index 0000000..4cc0def --- /dev/null +++ b/src/main/kotlin/com/api/nft/service/dto/ListingResponse.kt @@ -0,0 +1,16 @@ +package com.api.nft.service.dto + +import com.api.nft.enums.ChainType +import com.api.nft.enums.StatusType +import java.math.BigDecimal + +data class ListingResponse( + val id : Long, + val nftId : Long, + val address: String, + val createdDateTime: Long, + val endDateTime: Long, + val statusType: StatusType, + val price: BigDecimal, + val chainType: ChainType +) \ No newline at end of file diff --git a/src/main/kotlin/com/api/nft/service/dto/NftResponse.kt b/src/main/kotlin/com/api/nft/service/dto/NftResponse.kt deleted file mode 100644 index 9963c6e..0000000 --- a/src/main/kotlin/com/api/nft/service/dto/NftResponse.kt +++ /dev/null @@ -1,67 +0,0 @@ -package com.api.nft.service.dto - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties -import com.fasterxml.jackson.annotation.JsonProperty -import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper -import com.fasterxml.jackson.module.kotlin.readValue - -data class NftResponse( - val amount: String, - @JsonProperty("token_id")val tokenId: String, - @JsonProperty("token_address") val tokenAddress: String, - @JsonProperty("contract_type") val contractType: String?, - @JsonProperty("owner_of") val ownerOf: String?, - @JsonProperty("last_metadata_sync") val lastMetadataSync: String, - @JsonProperty("last_token_uri_sync") val lastTokenUriSync: String, - val metadata: String, - @JsonProperty("block_number") val blockNumber: String, - @JsonProperty("block_number_minted") val blockNumberMinted: String?, - val name: String, - val symbol: String, - @JsonProperty("token_hash") val tokenHash: String, - @JsonProperty("token_uri") val tokenUri: String, - @JsonProperty("minter_address") val minterAddress: String?, - @JsonProperty("verified_collection") val verifiedCollection: Boolean, - @JsonProperty("possible_spam") val possibleSpam: Boolean, - @JsonProperty("collection_logo") val collectionLogo: String?, - @JsonProperty("collection_banner_image") val collectionBannerImage: String?, -) - - -data class MetadataResponse( - val name: String, - val description: String, - val image: String, - @JsonProperty("animation_url") val animationUrl: String?, - val attributes: List>, - @JsonIgnoreProperties(ignoreUnknown = true) @JsonProperty("external_url") val externalUrl: String? = null -){ - companion object { - fun toMetadataResponse(metadata: String): MetadataResponse { - val mapper = jacksonObjectMapper() - return mapper.readValue(metadata, MetadataResponse::class.java) - } - - fun parseImage(image: String) : String { - return image.replace("ipfs://", "https://ipfs.io/ipfs/") - } - } -} - -data class AttributeResponse( - @JsonProperty("trait_type") val traitType: String?, - val value: String? -) { - companion object { - fun toAttributeResponse(attributes: List>): List { - return attributes.map { - AttributeResponse( - traitType = it["trait_type"], - value = it["value"] - ) - } - } - } -} - - diff --git a/src/main/kotlin/com/api/nft/service/external/binance/BinanceApiService.kt b/src/main/kotlin/com/api/nft/service/external/binance/BinanceApiService.kt new file mode 100644 index 0000000..ef6b266 --- /dev/null +++ b/src/main/kotlin/com/api/nft/service/external/binance/BinanceApiService.kt @@ -0,0 +1,33 @@ +package com.api.nft.service.external.binance + +import com.api.nft.enums.TokenType +import com.api.nft.service.external.dto.BinanceTickerPriceResponse +import org.springframework.stereotype.Service +import org.springframework.web.reactive.function.client.WebClient +import reactor.core.publisher.Mono + +@Service +class BinanceApiService { + + private val webClient = WebClient.builder() + .baseUrl(baseUrl) + .build() + + + fun getTickerPrice(tokenType: TokenType): Mono { + return webClient.get() + .uri{ + it.path("/api/v3/ticker/price") + it.queryParam("symbol","${tokenType}USDT") + it.build() + } + .retrieve() + .bodyToMono(BinanceTickerPriceResponse::class.java) + } + + + + companion object { + private val baseUrl = "https://api.binance.com" + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/api/nft/service/external/dto/BinanceTickerPriceResponse.kt b/src/main/kotlin/com/api/nft/service/external/dto/BinanceTickerPriceResponse.kt new file mode 100644 index 0000000..d9da1e1 --- /dev/null +++ b/src/main/kotlin/com/api/nft/service/external/dto/BinanceTickerPriceResponse.kt @@ -0,0 +1,8 @@ +package com.api.nft.service.external.dto + +import java.math.BigDecimal + +data class BinanceTickerPriceResponse( + val symbol : String, + val price: BigDecimal, +) diff --git a/src/main/kotlin/com/api/nft/service/external/dto/EthLogRequest.kt b/src/main/kotlin/com/api/nft/service/external/dto/EthLogRequest.kt new file mode 100644 index 0000000..f072acf --- /dev/null +++ b/src/main/kotlin/com/api/nft/service/external/dto/EthLogRequest.kt @@ -0,0 +1,9 @@ +package com.api.nft.service.external.dto + +data class EthLogRequest( + val fromBlock: String, + val toBlock: String, + val address: String, + val topics: List, +) + diff --git a/src/main/kotlin/com/api/nft/service/external/dto/EthLogResponse.kt b/src/main/kotlin/com/api/nft/service/external/dto/EthLogResponse.kt new file mode 100644 index 0000000..ce41176 --- /dev/null +++ b/src/main/kotlin/com/api/nft/service/external/dto/EthLogResponse.kt @@ -0,0 +1,36 @@ +package com.api.nft.service.external.dto + +import com.api.nft.domain.trasfer.Transfer +import java.math.BigInteger + +data class EthLogResponse( + val address: String, + val blockHash: String, + val blockNumber: String, + val data: String, + val logIndex: String, + val topics: List, + val transactionHash: String, + val transactionIndex: String, +){ + companion object{ + + fun toResponse(map: Map): EthLogResponse { + return EthLogResponse( + address = map["address"] as String, + blockHash = map["blockHash"] as String, + blockNumber = map["blockNumber"] as String, + data = map["data"] as String, + logIndex = map["logIndex"] as String, + topics = map["topics"] as List, + transactionHash = map["transactionHash"] as String, + transactionIndex = map["transactionIndex"] as String + ) + } + } + + private fun parseAddress(address: String): String { + return "0x" + address.substring(26).padStart(40, '0') + } +} + diff --git a/src/main/kotlin/com/api/nft/service/external/dto/InfuraRequest.kt b/src/main/kotlin/com/api/nft/service/external/dto/InfuraRequest.kt new file mode 100644 index 0000000..2ecfee0 --- /dev/null +++ b/src/main/kotlin/com/api/nft/service/external/dto/InfuraRequest.kt @@ -0,0 +1,8 @@ +package com.api.nft.service.external.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/nft/service/external/dto/InfuraResponse.kt b/src/main/kotlin/com/api/nft/service/external/dto/InfuraResponse.kt new file mode 100644 index 0000000..362e547 --- /dev/null +++ b/src/main/kotlin/com/api/nft/service/external/dto/InfuraResponse.kt @@ -0,0 +1,14 @@ +package com.api.nft.service.external.dto + +data class InfuraResponse( + val jsonrpc: String, + val id: Int, + val result: List>, +) { + + fun toEthLogResponses(): List { + return result.map { EthLogResponse.toResponse(it) } + } + +} + diff --git a/src/main/kotlin/com/api/nft/service/external/dto/NftData.kt b/src/main/kotlin/com/api/nft/service/external/dto/NftData.kt new file mode 100644 index 0000000..219a6ab --- /dev/null +++ b/src/main/kotlin/com/api/nft/service/external/dto/NftData.kt @@ -0,0 +1,76 @@ +package com.api.nft.service.external.dto + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper + +data class NFTByWalletResponse( + val status: String, + val page: Int, + @JsonProperty("page_size") val pageSize: Int, + val cursor: String?, + val result: List +) + +data class NftData( + @JsonProperty("token_address") val tokenAddress: String, + @JsonProperty("token_id") val tokenId: String, + @JsonProperty("contract_type") val contractType: String, + @JsonProperty("owner_of") val ownerOf: String, + @JsonProperty("block_number") val blockNumber: String, + @JsonProperty("block_number_minted") val blockNumberMinted: String?, + @JsonProperty("token_uri") val tokenUri: String?, + val metadata: String?, + @JsonProperty("normalized_metadata") val normalizedMetadata: String?, + val media: String?, + val amount: String?, + val name: String, + val symbol: String?, + @JsonProperty("token_hash") val tokenHash: String?, + @JsonProperty("last_token_uri_sync") val lastTokenUriSync: String?, + @JsonProperty("last_metadata_sync") val lastMetadataSync: String?, + @JsonProperty("possible_spam") val possibleSpam: Boolean?, + @JsonProperty("verified_collection") val verifiedCollection: Boolean?, + @JsonIgnoreProperties(ignoreUnknown = true) @JsonProperty("collection_logo") val collectionLogo: String?, + @JsonIgnoreProperties(ignoreUnknown = true) @JsonProperty("collection_banner_image") val collectionBannerImage: String?, + @JsonIgnoreProperties(ignoreUnknown = true) @JsonProperty("minter_address") val minterAddress: String? +) + +data class NftMetadata( + val name: String, + val description: String, + val image: String, + @JsonProperty("animation_url") val animationUrl: String?, + val attributes: List> = emptyList(), + @JsonIgnoreProperties(ignoreUnknown = true) @JsonProperty("external_url") val externalUrl: String? = null, + @JsonIgnoreProperties(ignoreUnknown = true) @JsonProperty("youtube_url") val youtube_url: String? = null +){ + companion object { + fun toMetadataResponse(metadata: String?): NftMetadata { + val mapper = jacksonObjectMapper() + return mapper.readValue(metadata, NftMetadata::class.java) + } + + fun parseImage(image: String) : String { + return image.replace("ipfs://", "https://ipfs.io/ipfs/") + } + } +} + +data class NftAttribute( + @JsonProperty("trait_type") val traitType: String?, + val value: String? +) { + companion object { + fun toAttributeResponse(attributes: List>): List { + return attributes.map { + NftAttribute( + traitType = it["trait_type"], + value = it["value"] + ) + } + } + } +} + + diff --git a/src/main/kotlin/com/api/nft/service/external/dto/NftRequest.kt b/src/main/kotlin/com/api/nft/service/external/dto/NftRequest.kt new file mode 100644 index 0000000..a5204e0 --- /dev/null +++ b/src/main/kotlin/com/api/nft/service/external/dto/NftRequest.kt @@ -0,0 +1,10 @@ +package com.api.nft.service.external.dto + +import com.api.nft.enums.ChainType + +data class NftRequest( + val id: Long, + val tokenAddress: String, + val tokenId: String, + val chainType: ChainType +) diff --git a/src/main/kotlin/com/api/nft/service/external/dto/NftTransferData.kt b/src/main/kotlin/com/api/nft/service/external/dto/NftTransferData.kt new file mode 100644 index 0000000..2ef6613 --- /dev/null +++ b/src/main/kotlin/com/api/nft/service/external/dto/NftTransferData.kt @@ -0,0 +1,36 @@ +package com.api.nft.service.external.dto + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties +import com.fasterxml.jackson.annotation.JsonProperty + +data class NftTransferData( + val page: String, + @JsonProperty("page_size")val pageSize: String?, + val cursor: String?, + val result: List, + @JsonProperty("block_exists") val blockExists: Boolean, + @JsonIgnoreProperties(ignoreUnknown = true) @JsonProperty("index_complete") val indexComplete: Boolean +) + +data class ResultData ( + @JsonProperty("token_address") val tokenAddress: String, + @JsonProperty("token_id") val tokenId: String, + @JsonProperty("from_address") val fromAddress: String, + @JsonProperty("from_address_label")val fromAddressLabel: String?, + @JsonProperty("to_address") val toAddress: String, + @JsonProperty("to_address_label") val toAddressLabel: String?, + val value: String, + val amount: String, + @JsonProperty("contract_type") val contractType: String, + @JsonProperty("block_number") val blockNumber: String, + @JsonProperty("block_timestamp") val blockTimestamp: String, + @JsonProperty("block_hash") val blockHash: String, + @JsonProperty("transaction_hash") val transactionHash: String, + @JsonProperty("transaction_type") val transactionType: String, + @JsonProperty("transaction_index") val transactionIndex: String, + @JsonProperty("log_index") val logIndex: String, + val operator: String?, + @JsonProperty("possible_spam") val possibleSpam: Boolean, + @JsonProperty("verified_collection") val verifiedCollection : String, + @JsonProperty("verified") val verified : Int, +) diff --git a/src/main/kotlin/com/api/nft/service/external/infura/InfuraApiService.kt b/src/main/kotlin/com/api/nft/service/external/infura/InfuraApiService.kt new file mode 100644 index 0000000..8be892a --- /dev/null +++ b/src/main/kotlin/com/api/nft/service/external/infura/InfuraApiService.kt @@ -0,0 +1,103 @@ +package com.api.nft.service.external.infura + +import com.api.nft.enums.ChainType +import com.api.nft.properties.ApiKeysProperties +import com.api.nft.service.external.dto.EthLogRequest +import com.api.nft.service.external.dto.EthLogResponse +import com.api.nft.service.external.dto.InfuraRequest +import com.api.nft.service.external.dto.InfuraResponse +import org.springframework.http.MediaType +import org.springframework.stereotype.Service +import org.springframework.web.reactive.function.client.WebClient +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.generated.Uint256 +import org.web3j.utils.Numeric +import reactor.core.publisher.Mono +import java.math.BigInteger +import java.time.Instant + + +@Service +class InfuraApiService( + private val apiKeysProperties: ApiKeysProperties +) { + + private 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 getEthLogs( + fromBlock: String, + chainType: ChainType, + tokenAddress: String, + tokenId: String, + ): Mono>{ + + val transferEvent = Event( + "Transfer", + listOf( + TypeReference.create(Address::class.java, true), + TypeReference.create(Address::class.java, true), + TypeReference.create(Uint256::class.java, true), + ) + ) + + val eventSignature = EventEncoder.encode(transferEvent) + val topicTokenId = Numeric.toHexStringWithPrefixZeroPadded(BigInteger(tokenId), 64) + val toHexFromBlock = Numeric.toHexStringWithPrefix(BigInteger(fromBlock)) + + + val requestBody = InfuraRequest( + method = "eth_getLogs", + params = listOf(EthLogRequest( + toHexFromBlock, + "latest", + tokenAddress, + listOf(eventSignature, null, null, topicTokenId))) + ) + val webClient = urlByChain(chainType) + + return webClient.post() + .uri("/v3/${apiKeysProperties.infura}") + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(requestBody) + .retrieve() + .bodyToMono(InfuraResponse::class.java) + .mapNotNull { it.toEthLogResponses() } + + } + + fun getBlockTimestamp(blockNumber: String, chainType: ChainType): Mono { + val webClient = urlByChain(chainType) + val requestBody = InfuraRequest( + method = "eth_getBlockByNumber", + params = listOf(blockNumber, false), + ) + + return webClient.post() + .uri("/v3/${apiKeysProperties.infura}") + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(requestBody) + .retrieve() + .bodyToMono(Map::class.java) + .mapNotNull { response -> + val result = response["result"] as Map + val timestamp = result["timestamp"].toString() + Instant.ofEpochSecond(Numeric.decodeQuantity(timestamp).longValueExact()).toEpochMilli() + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/api/nft/service/external/moralis/MoralisApiService.kt b/src/main/kotlin/com/api/nft/service/external/moralis/MoralisApiService.kt new file mode 100644 index 0000000..605e570 --- /dev/null +++ b/src/main/kotlin/com/api/nft/service/external/moralis/MoralisApiService.kt @@ -0,0 +1,87 @@ +package com.api.nft.service.external.moralis + +import com.api.nft.enums.ChainType +import com.api.nft.properties.ApiKeysProperties +import com.api.nft.service.external.dto.NFTByWalletResponse +import com.api.nft.service.external.dto.NftData +import com.api.nft.service.external.dto.NftTransferData +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 MoralisApiService( + private val apiKeysProperties: ApiKeysProperties, +) { + + private val webClient = WebClient.builder() + .baseUrl(baseUrl) + .build() + + private fun queryParamByChain(chain: ChainType): String { + val chain = when (chain) { + ChainType.ETHEREUM_MAINNET -> "0x1" + ChainType.POLYGON_MAINNET -> "0x89" + ChainType.LINEA_MAINNET -> "0xe708" + ChainType.LINEA_SEPOLIA -> "0xe705" + ChainType.ETHEREUM_HOLESKY -> "0x4268" + ChainType.ETHEREUM_SEPOLIA -> "0xaa36a7" + ChainType.POLYGON_AMOY -> "0xaa36a7" + } + return chain + } + + fun getNftTransfer(tokenAddress: String, + tokenId: String, + chainType: ChainType + ): Mono{ + val chain = queryParamByChain(chainType) + + return webClient.get() + .uri { + it.path("/v2.2/nft/${tokenAddress}/${tokenId}/transfers") + it.queryParam("chain", chain) + it.build() + } + .header("X-API-Key", apiKeysProperties.moralis) + .header("Accept", MediaType.APPLICATION_JSON_VALUE) + .retrieve() + .bodyToMono(NftTransferData::class.java) + } + + fun getNFTsByAddress(walletAddress: String,chainType: ChainType): Mono { + val chain = queryParamByChain(chainType) + return webClient.get() + .uri { + it.path("/v2.2/${walletAddress}/nft") + it.queryParam("chain", chain) + it.queryParam("exclude_spam", true) + it.build() + } + .header("X-API-Key", apiKeysProperties.moralis) + .header("Accept", MediaType.APPLICATION_JSON_VALUE) + .retrieve() + .bodyToMono(NFTByWalletResponse::class.java) + } + + fun getNFTMetadata(tokenAddress: String,tokenId:String,chainType: ChainType): Mono { + val chain = queryParamByChain(chainType) + return webClient.get() + .uri { + it.path("/v2.2/nft/${tokenAddress}/${tokenId}") + it.queryParam("chain", chain) + it.build() + } + .header("X-API-Key", apiKeysProperties.moralis) + .header("Accept", MediaType.APPLICATION_JSON_VALUE) + .retrieve() + .bodyToMono(NftData::class.java) + } + + + + companion object { + private val baseUrl = "https://deep-index.moralis.io/api" + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/api/nft/util/ChainTypeConvert.kt b/src/main/kotlin/com/api/nft/util/ChainTypeConvert.kt new file mode 100644 index 0000000..2abbd0d --- /dev/null +++ b/src/main/kotlin/com/api/nft/util/ChainTypeConvert.kt @@ -0,0 +1,12 @@ +package com.api.nft.util + +import com.api.nft.enums.ChainType +import com.api.nft.enums.ContractType +import com.api.nft.enums.StatusType +import com.api.nft.enums.TokenType +import org.springframework.data.r2dbc.convert.EnumWriteSupport + +data class ContractTypeConverter>(private val enumType: Class) : EnumWriteSupport() +data class ChainTypeConvert>(private val enumType: Class): EnumWriteSupport() +data class TokenTypeConvert>(private val enumType: Class): EnumWriteSupport() +data class StatusTypeConvert>(private val enumType: Class): EnumWriteSupport() diff --git a/src/main/kotlin/com/api/nft/util/StringToEnumConvert.kt b/src/main/kotlin/com/api/nft/util/StringToEnumConvert.kt new file mode 100644 index 0000000..a9cf2a5 --- /dev/null +++ b/src/main/kotlin/com/api/nft/util/StringToEnumConvert.kt @@ -0,0 +1,11 @@ +package com.api.nft.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/nft/util/Util.kt b/src/main/kotlin/com/api/nft/util/Util.kt new file mode 100644 index 0000000..657a855 --- /dev/null +++ b/src/main/kotlin/com/api/nft/util/Util.kt @@ -0,0 +1,27 @@ +package com.api.nft.util + +import com.api.nft.enums.ChainType +import java.time.Instant +import java.time.format.DateTimeFormatter + +object Util { + fun String.convertNetworkTypeToChainType(): ChainType { + return when (this) { + "ETHEREUM_MAINNET" -> ChainType.ETHEREUM_MAINNET + "POLYGON_MAINNET" -> ChainType.POLYGON_MAINNET + else -> throw IllegalArgumentException("Unknown network type: $this") + } + } + + fun timestampToString(timestamp: Long): String { + val instant = Instant.ofEpochMilli(timestamp) + return DateTimeFormatter.ISO_INSTANT.format(instant) + } + + fun Long.toIsoString(): String = DateTimeFormatter.ISO_INSTANT.format(Instant.ofEpochMilli(this)) + + fun String.toEpochMilli(): Long { + return Instant.parse(this).toEpochMilli() + } + +} \ No newline at end of file diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml new file mode 100644 index 0000000..c805f93 --- /dev/null +++ b/src/main/resources/application-local.yml @@ -0,0 +1,56 @@ +# +#spring: +# application: +# name: nft +# datasource: +# url: jdbc:postgresql://localhost:5434/nft +# username: nft +# password: nft +# flyway: +# locations: classpath:db/migration +# r2dbc: +# url: r2dbc:postgresql://localhost:5434/nft +# username: nft +# password: nft +# rabbitmq: +# host: localhost +# port: 5672 +# username: closeSea +# password: closeSeaP@ssword +# data: +# redis: +# cluster: +# nodes: localhost:6379,localhost:6380,localhost:6381,localhost:6382,localhost:6383,localhost:6384,localhost:6385,localhost:6386,localhost:6387,localhost:6388 +# max-redirects: 3 +# password: bitnami +# timeout: 500ms +# connect-ip: localhost +# lettuce: +# pool: +# max-active: 8 +# max-idle: 8 +# min-idle: 0 +# session: +# redis: +# flush-mode: on_save +# +#logging: +# level: +# org.springframework.r2dbc: debug +# org.springframework.data.redis: DEBUG +# redis.clients.jedis: DEBUG +# io.lettuce.core: DEBUG +# root: INFO +# +#server: +# port: 8082 +# + +spring: + application: + name: nft + config: + import: "optional:configserver:http://localhost:9000" + cloud: + config: + fail-fast: true diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml deleted file mode 100644 index ba73cda..0000000 --- a/src/main/resources/application.yml +++ /dev/null @@ -1,21 +0,0 @@ - -spring: - application: - name: nft - datasource: - url: jdbc:postgresql://localhost:5434/nft - username: nft - password: nft - flyway: - locations: classpath:db/migration - r2dbc: - url: r2dbc:postgresql://localhost:5434/nft - username: nft - password: nft - - -logging: - level: - org.springframework.r2dbc: debug - io.r2dbc.postgresql: DEBUG - root: info diff --git a/src/main/resources/bootstrap.yaml b/src/main/resources/bootstrap.yaml new file mode 100644 index 0000000..01f8804 --- /dev/null +++ b/src/main/resources/bootstrap.yaml @@ -0,0 +1,9 @@ +spring: + application: + name: nft + cloud: + config: + uri: https://localhost:9000 + fail-fast: true + profiles: + active: local diff --git a/src/main/resources/db/postgresql/migration/V1__Initial_schema.sql b/src/main/resources/db/postgresql/migration/V1__Initial_schema.sql index 77eef69..bb8cbbc 100644 --- a/src/main/resources/db/postgresql/migration/V1__Initial_schema.sql +++ b/src/main/resources/db/postgresql/migration/V1__Initial_schema.sql @@ -1,14 +1,48 @@ +CREATE TYPE contract_type AS ENUM( + 'ERC721', + 'ERC1155' + ); + +CREATE TYPE chain_type AS ENUM ( + 'ETHEREUM_MAINNET', + 'LINEA_MAINNET', + 'LINEA_SEPOLIA', + 'POLYGON_MAINNET', + 'ETHEREUM_HOLESKY', + 'ETHEREUM_SEPOLIA', + 'POLYGON_AMOY' + ); + +CREATE TYPE token_type AS ENUM ( + 'MATIC', + 'BTC', + 'ETH', + 'SAND' + ); + + +CREATE TYPE status_type AS ENUM ( + 'RESERVATION', + 'LISTING', + 'AUCTION' + ); + + + CREATE TABLE IF NOT EXISTS collection ( - name varchar(500) PRIMARY KEY + name varchar(500) PRIMARY KEY, + logo varchar(500), + banner_image varchar(500), + description varchar(1000) ); CREATE TABLE IF NOT EXISTS nft ( id SERIAL 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, - owner_of varchar(255), + chain_type chain_type NOT NULL, + nft_name varchar(255), + contract_type contract_type NOT NULL, token_hash varchar(300), amount INT, collection_name varchar(500) REFERENCES collection(name) @@ -24,6 +58,44 @@ CREATE TABLE IF NOT EXISTS metadata ( ); +CREATE TABLE IF NOT EXISTS attribute ( + id SERIAL PRIMARY KEY, + nft_id BIGINT REFERENCES nft(id), + trait_type varchar(255), + value varchar(255) +); + + +CREATE TABLE IF NOT EXISTS transfer ( + id SERIAL PRIMARY KEY, + nft_id BIGINT REFERENCES nft(id), + from_address varchar(255) not null, + to_address varchar(255) not null, + block_number bigint not null, + block_timestamp bigint not null +); + +CREATE TABLE IF NOT EXISTS nft_listing ( + id BIGINT PRIMARY KEY, + nft_id BIGINT REFERENCES nft(id), + price DECIMAL(19, 4) NOT NULL, + chain_type chain_type, + status_type status_type not null, + created_date BIGINT, + end_date BIGINT +); + + +CREATE TABLE IF NOT EXISTS nft_auction ( + id BIGINT PRIMARY KEY, + nft_id BIGINT REFERENCES nft(id), + starting_price DECIMAL(19, 4) NOT NULL, + chain_type chain_type, + status_type status_type not null, + created_date BIGINT, + end_date BIGINT +); + diff --git a/src/main/resources/http/test.http b/src/main/resources/http/test.http new file mode 100644 index 0000000..a00bafa --- /dev/null +++ b/src/main/resources/http/test.http @@ -0,0 +1,2 @@ +### +GET http://localhost:8082/v1/nft/redis \ No newline at end of file diff --git a/src/test/kotlin/com/api/nft/NftTest.kt b/src/test/kotlin/com/api/nft/NftTest.kt index 2f81ee6..f080130 100644 --- a/src/test/kotlin/com/api/nft/NftTest.kt +++ b/src/test/kotlin/com/api/nft/NftTest.kt @@ -1,46 +1,172 @@ package com.api.nft -import com.api.nft.domain.Collection -import com.api.nft.domain.CollectionRepository +import com.api.nft.domain.collection.repository.CollectionRepository +import com.api.nft.domain.nft.Nft +import com.api.nft.domain.nft.repository.NftListingRepository +import com.api.nft.domain.nft.repository.NftRepository import com.api.nft.enums.ChainType -import com.api.nft.service.MoralisApiService -import com.api.nft.service.NftService +import com.api.nft.enums.ContractType +import com.api.nft.enums.TokenType +import com.api.nft.event.dto.NftCreatedEvent +import com.api.nft.rabbitMQ.RabbitMQSender +import com.api.nft.service.external.moralis.MoralisApiService +import com.api.nft.service.api.NftService +import com.api.nft.service.api.TransferService +import com.api.nft.event.dto.NftResponse +import com.api.nft.service.RedisService +import com.api.nft.service.api.NftListingService +import com.api.nft.service.dto.ListingResponse +import com.api.nft.service.external.binance.BinanceApiService +import com.api.nft.service.external.infura.InfuraApiService import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest +import org.springframework.context.ApplicationEventPublisher +import reactor.test.StepVerifier +import java.math.BigDecimal @SpringBootTest class NftTest( @Autowired private val moralisApiService: MoralisApiService, @Autowired private val nftService: NftService, @Autowired private val collectionRepository: CollectionRepository, + @Autowired private val rabbitMQSender: RabbitMQSender, + @Autowired private val transferService: TransferService, + @Autowired private val nftRepository: NftRepository, + @Autowired private val infuraApiService: InfuraApiService, + @Autowired private val eventPublisher: ApplicationEventPublisher, + @Autowired private val binanceApiService: BinanceApiService, + @Autowired private val redisService: RedisService, + @Autowired private val nftListingService: NftListingService, + @Autowired private val nftListingRepository: NftListingRepository, ) { + + @Test + fun getEth() { + val res = infuraApiService.getEthLogs("56498172",ChainType.POLYGON_MAINNET,"0xa3784fe9104fdc0b988769fba7459ece2fb36eea","1").block() + println(res.toString()) + } + + + @Test + fun findOrUpdateByNftId() { + //56498172 + transferService.findOrUpdateByNftId(31).blockLast() + } + + @Test + fun rabbitMQTest() { + val nft = Nft( + id = 1, + tokenId = "hello", + tokenAddress = "helloAddress", + chainType = ChainType.POLYGON_MAINNET, + contractType = ContractType.ERC721, + nftName = "nftName", + tokenHash = null, + collectionName = "nftCollection", + amount = 3 + ) + eventPublisher.publishEvent(NftCreatedEvent(this,nft.toResponse())) + // rabbitMQSender.nftSend(nft.toResponse()) + // rabbitMQSender.nftSend1("asdasasdas") + } + + private fun Nft.toResponse() = NftResponse( + id = this.id!!, + tokenId = this.tokenId, + tokenAddress = this.tokenAddress, + chainType = this.chainType, + ) + + + @Test + fun getByWalletNft() { + val wallet = "0x01b72b4aa3f66f213d62d53e829bc172a6a72867" + val res= nftService.getByWalletNft(wallet,ChainType.POLYGON_MAINNET).blockLast() + println(res?.tokenId) + println(res?.tokenAddress) + + } + + @Test + fun binanceTest() { + val res = binanceApiService.getTickerPrice(TokenType.ETH).block() + println(res?.symbol) + println(res?.price) + } + + @Test + fun transferTest() { + val nft = nftRepository.findById(3L).block() + + transferService.createTransfer(nft!!).block() + + } + @Test - fun test(){ - val tokenAddress = "0xe7900239e9332060dc975ed6f0cc5f0129d924cf" - val tokenId = "3" - val res =moralisApiService.getNft(tokenAddress,tokenId, ChainType.POLYGON_MAINNET).block() - println(res) + fun trasfermoralist() { + val res =moralisApiService.getNftTransfer("0xa3784fe9104fdc0b988769fba7459ece2fb36eea","0",ChainType.POLYGON_MAINNET).block() + println(res.toString()) } @Test - fun test1() { - val tokenAddress = "0xe7900239e9332060dc975ed6f0cc5f0129d924cf" - val tokenId = "3" - val res =nftService.getNftByMoralis(tokenId,tokenAddress, ChainType.POLYGON_MAINNET).block() + fun testNftData() { + val res =nftRepository.findByNftJoinMetadata(3).block() + println(res.toString()) } + +// @Test +// fun nftListing() { +// val listing = ListingResponse( +// id = 1, +// nftId = 3L, +// address = "0x01b72b4aa3f66f213d62d53e829bc172a6a72867", +// createdDateTime = 1714662809000, +// endDateTime = 1714662809000, +// price = BigDecimal(0.23), +// tokenType = TokenType.ETH +// +// ) +// nftListingService.update(listing).block() +// } + @Test - fun test2() { - val tokenAddress = "0xe7900239e9332060dc975ed6f0cc5f0129d924cf" - val tokenId = "3" - nftService.createNftProcess(tokenId,tokenAddress, ChainType.POLYGON_MAINNET).block() + fun redisTest22( ){ + // val nftlisting = nftListingRepository.findByNftId(1L).block() + redisService.updateToRedis(1L).block() } @Test - fun test3() { - val tokenName = "asdasdasd" - collectionRepository.insert(Collection(tokenName)).block() + fun redisTest() { + val nft = nftRepository.findById(8).block() + redisService.updateToRedis(nft?.id!!).block() + // val res = nftRepository.findByNftJoinMetadata(1).block() + + } + + @Test + fun nftService() { + val nft = nftService.findOrCreateNft(tokenAddress = "0x524cAB2ec69124574082676e6F654a18df49A048", tokenId = "5430",ChainType.ETHEREUM_MAINNET).block() + + } + + @Test + fun asdasd() { + nftService.getByWalletNft("0x01b72b4aa3f66f213d62d53e829bc172a6a72867",ChainType.POLYGON_MAINNET).blockLast() + // Thread.sleep(10000) + + } + + @Test + fun test() { + val address = "0x01b72b4aa3f66f213d62d53e829bc172a6a72867" + + val res= moralisApiService.getNFTsByAddress(address,ChainType.POLYGON_MAINNET).block() + println(res.toString()) + } + } \ No newline at end of file