From a2380eeff13cfbed42ec224b0368ebdf1187e92b Mon Sep 17 00:00:00 2001 From: min-96 Date: Mon, 20 May 2024 01:11:14 +0900 Subject: [PATCH 01/24] feat: send depositNftData to api-wallet --- .../com/api/admin/config/RabbitMQConfig.kt | 43 +++++++++++++------ .../rabbitMQ/event/AdminEventListener.kt | 16 +++++++ .../RabbitMQReceiver.kt} | 6 +-- .../admin/rabbitMQ/sender/RabbitMQSender.kt | 15 +++++++ .../com/api/admin/service/TransferService.kt | 4 +- src/main/resources/application.yml | 6 +-- .../kotlin/com/api/admin/AdminServiceTest.kt | 7 +++ 7 files changed, 75 insertions(+), 22 deletions(-) create mode 100644 src/main/kotlin/com/api/admin/rabbitMQ/event/AdminEventListener.kt rename src/main/kotlin/com/api/admin/rabbitMQ/{MessageReceiver.kt => receiver/RabbitMQReceiver.kt} (75%) create mode 100644 src/main/kotlin/com/api/admin/rabbitMQ/sender/RabbitMQSender.kt diff --git a/src/main/kotlin/com/api/admin/config/RabbitMQConfig.kt b/src/main/kotlin/com/api/admin/config/RabbitMQConfig.kt index 479f50f..595c122 100644 --- a/src/main/kotlin/com/api/admin/config/RabbitMQConfig.kt +++ b/src/main/kotlin/com/api/admin/config/RabbitMQConfig.kt @@ -14,27 +14,42 @@ import org.springframework.context.annotation.Configuration class RabbitMQConfig { @Bean - fun nftQueue(): Queue { - return Queue("nftQueue", true) - } + fun jsonMessageConverter(): Jackson2JsonMessageConverter = Jackson2JsonMessageConverter() @Bean - fun nftExchange(): DirectExchange { - return DirectExchange("nftExchange") + fun rabbitTemplate(connectionFactory: ConnectionFactory, jsonMessageConverter: Jackson2JsonMessageConverter): RabbitTemplate { + val template = RabbitTemplate(connectionFactory) + template.messageConverter = jsonMessageConverter + return template } - @Bean - fun bindingNftQueue(nftQueue: Queue, nftExchange: DirectExchange): Binding { - return BindingBuilder.bind(nftQueue).to(nftExchange).with("nftRoutingKey") + private fun createQueue(name: String, durable: Boolean = true): Queue { + return Queue(name, durable) + } + + private fun createExchange(name: String): DirectExchange { + return DirectExchange(name) + } + + private fun createBinding(queue: Queue, exchange: DirectExchange, routingKey: String): Binding { + return BindingBuilder.bind(queue).to(exchange).with(routingKey) } @Bean - fun jsonMessageConverter(): Jackson2JsonMessageConverter = Jackson2JsonMessageConverter() + fun nftQueue() = createQueue("nftQueue") @Bean - fun rabbitTemplate(connectionFactory: ConnectionFactory, jsonMessageConverter: Jackson2JsonMessageConverter): RabbitTemplate { - val template = RabbitTemplate(connectionFactory) - template.messageConverter = jsonMessageConverter - return template - } + fun nftExchange() = createExchange("nftExchange") + + @Bean + fun bindingNftQueue(nftQueue: Queue, nftExchange: DirectExchange) = createBinding(nftQueue, nftExchange, "nftRoutingKey") + + @Bean + fun depositQueue() = createQueue("depositQueue") + + @Bean + fun depositExchange() = createExchange("depositExchange") + + @Bean + fun bindingDepositQueue(depositQueue: Queue, depositExchange: DirectExchange) = createBinding(depositQueue, depositExchange, "depositRoutingKey") } \ No newline at end of file diff --git a/src/main/kotlin/com/api/admin/rabbitMQ/event/AdminEventListener.kt b/src/main/kotlin/com/api/admin/rabbitMQ/event/AdminEventListener.kt new file mode 100644 index 0000000..191fbcb --- /dev/null +++ b/src/main/kotlin/com/api/admin/rabbitMQ/event/AdminEventListener.kt @@ -0,0 +1,16 @@ +package com.api.admin.rabbitMQ.event + +import com.api.admin.rabbitMQ.sender.RabbitMQSender +import org.springframework.context.event.EventListener +import org.springframework.stereotype.Component + +@Component +class AdminEventListener( + private val provider : RabbitMQSender +) { + + @EventListener + fun onDepositSend(event: Long) { + provider.depositSend(event) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/api/admin/rabbitMQ/MessageReceiver.kt b/src/main/kotlin/com/api/admin/rabbitMQ/receiver/RabbitMQReceiver.kt similarity index 75% rename from src/main/kotlin/com/api/admin/rabbitMQ/MessageReceiver.kt rename to src/main/kotlin/com/api/admin/rabbitMQ/receiver/RabbitMQReceiver.kt index 4303d1b..9c06ead 100644 --- a/src/main/kotlin/com/api/admin/rabbitMQ/MessageReceiver.kt +++ b/src/main/kotlin/com/api/admin/rabbitMQ/receiver/RabbitMQReceiver.kt @@ -1,4 +1,4 @@ -package com.api.admin.rabbitMQ +package com.api.admin.rabbitMQ.receiver import com.api.admin.NftResponse import com.api.admin.service.NftService @@ -6,11 +6,11 @@ import org.springframework.amqp.rabbit.annotation.RabbitListener import org.springframework.stereotype.Service @Service -class MessageReceiver( +class RabbitMQReceiver( private val nftService: NftService, ) { @RabbitListener(queues = ["nftQueue"]) - fun receiveMessage(nft: NftResponse) { + fun nftMessage(nft: NftResponse) { nftService.save(nft) } } \ No newline at end of file diff --git a/src/main/kotlin/com/api/admin/rabbitMQ/sender/RabbitMQSender.kt b/src/main/kotlin/com/api/admin/rabbitMQ/sender/RabbitMQSender.kt new file mode 100644 index 0000000..a0d7cd1 --- /dev/null +++ b/src/main/kotlin/com/api/admin/rabbitMQ/sender/RabbitMQSender.kt @@ -0,0 +1,15 @@ +package com.api.admin.rabbitMQ.sender + +import org.springframework.amqp.rabbit.core.RabbitTemplate +import org.springframework.stereotype.Service + +@Service +class RabbitMQSender( + private val rabbitTemplate: RabbitTemplate +) { + + fun depositSend(deposit: Long) { + rabbitTemplate.convertAndSend("depositExchange", "depositRoutingKey", deposit) + } + +} \ No newline at end of file diff --git a/src/main/kotlin/com/api/admin/service/TransferService.kt b/src/main/kotlin/com/api/admin/service/TransferService.kt index 6b7d9a3..1d152ef 100644 --- a/src/main/kotlin/com/api/admin/service/TransferService.kt +++ b/src/main/kotlin/com/api/admin/service/TransferService.kt @@ -38,7 +38,8 @@ class TransferService( .flatMap { nft -> getNftOwner(request.chainType, nft.tokenAddress, nft.tokenId) .filterWhen { address -> Mono.just(address == adminAddress) } - .flatMap { saveTransfer(nft.id!!, wallet) } + .flatMap { saveTransfer(nft.id, wallet) } + // 이벤트로? } } .then() @@ -55,6 +56,7 @@ class TransferService( return transferRepository.save(transfer) } + //TODO("apiKey 캡슐화") fun getNftOwner(chainType: ChainType, contractAddress: String, tokenId: String): Mono { val web3 = Web3j.build(HttpService(chainType.baseUrl() + "/v3/98b672d2ce9a4089a3a5cb5081dde2fa")) val function = Function( diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 4d0d09c..f35b490 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,4 +1,5 @@ - +server: + port: 8084 spring: application: name: admin @@ -17,6 +18,3 @@ spring: port: 5672 username: closeSea password: closeSeaP@ssword - template: - routing-key: "nftRoutingKey" - exchange: "nftExchange" \ No newline at end of file diff --git a/src/test/kotlin/com/api/admin/AdminServiceTest.kt b/src/test/kotlin/com/api/admin/AdminServiceTest.kt index 06318ff..c15b6eb 100644 --- a/src/test/kotlin/com/api/admin/AdminServiceTest.kt +++ b/src/test/kotlin/com/api/admin/AdminServiceTest.kt @@ -1,6 +1,7 @@ package com.api.admin import com.api.admin.enums.ChainType +import com.api.admin.rabbitMQ.sender.RabbitMQSender import com.api.admin.service.TransferService import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired @@ -9,6 +10,7 @@ import org.springframework.boot.test.context.SpringBootTest @SpringBootTest class AdminServiceTest( @Autowired private val transferService: TransferService, + @Autowired private val rabbitMQSender: RabbitMQSender, ) { @Test @@ -16,4 +18,9 @@ class AdminServiceTest( val res = transferService.getNftOwner(ChainType.POLYGON_MAINNET,"0xa3784fe9104fdc0b988769fba7459ece2fb36eea","0") println(res) } + + @Test + fun rabbitMqTest() { + rabbitMQSender.depositSend(1) + } } \ No newline at end of file From 550decabe1f61cc8f0598de6249617549b48b1fc Mon Sep 17 00:00:00 2001 From: min-96 Date: Mon, 20 May 2024 18:06:17 +0900 Subject: [PATCH 02/24] feat: refactor --- .../com/api/admin/config/RabbitMQConfig.kt | 6 ++--- .../com/api/admin/domain/transfer/Transfer.kt | 3 ++- src/main/kotlin/com/api/admin/enums/Enum.kt | 7 +++++- .../rabbitMQ/event/AdminEventListener.kt | 5 +++-- .../event/dto/AdminTransferCreatedEvent.kt | 5 +++++ .../event/dto/AdminTransferResponse.kt | 10 +++++++++ .../admin/rabbitMQ/sender/RabbitMQSender.kt | 5 +++-- .../com/api/admin/service/TransferService.kt | 22 ++++++++++++++----- .../db/migration/V1__Initial_schema.sql | 6 ++--- 9 files changed, 52 insertions(+), 17 deletions(-) create mode 100644 src/main/kotlin/com/api/admin/rabbitMQ/event/dto/AdminTransferCreatedEvent.kt create mode 100644 src/main/kotlin/com/api/admin/rabbitMQ/event/dto/AdminTransferResponse.kt diff --git a/src/main/kotlin/com/api/admin/config/RabbitMQConfig.kt b/src/main/kotlin/com/api/admin/config/RabbitMQConfig.kt index 595c122..3df9362 100644 --- a/src/main/kotlin/com/api/admin/config/RabbitMQConfig.kt +++ b/src/main/kotlin/com/api/admin/config/RabbitMQConfig.kt @@ -45,11 +45,11 @@ class RabbitMQConfig { fun bindingNftQueue(nftQueue: Queue, nftExchange: DirectExchange) = createBinding(nftQueue, nftExchange, "nftRoutingKey") @Bean - fun depositQueue() = createQueue("depositQueue") + fun transferQueue() = createQueue("transferQueue") @Bean - fun depositExchange() = createExchange("depositExchange") + fun transferExchange() = createExchange("transferExchange") @Bean - fun bindingDepositQueue(depositQueue: Queue, depositExchange: DirectExchange) = createBinding(depositQueue, depositExchange, "depositRoutingKey") + fun bindingTransferQueue(transferQueue: Queue, transferExchange: DirectExchange) = createBinding(transferQueue, transferExchange, "transferRoutingKey") } \ No newline at end of file diff --git a/src/main/kotlin/com/api/admin/domain/transfer/Transfer.kt b/src/main/kotlin/com/api/admin/domain/transfer/Transfer.kt index c195f66..220e287 100644 --- a/src/main/kotlin/com/api/admin/domain/transfer/Transfer.kt +++ b/src/main/kotlin/com/api/admin/domain/transfer/Transfer.kt @@ -1,5 +1,6 @@ package com.api.admin.domain.transfer +import com.api.admin.enums.AccountType import org.springframework.data.annotation.Id import org.springframework.data.relational.core.mapping.Table @@ -9,7 +10,7 @@ class Transfer( val nftId: Long, val wallet: String, val timestamp: Long, - val status: String + val accountType: String ) { } \ No newline at end of file diff --git a/src/main/kotlin/com/api/admin/enums/Enum.kt b/src/main/kotlin/com/api/admin/enums/Enum.kt index f3ca104..4abd8ee 100644 --- a/src/main/kotlin/com/api/admin/enums/Enum.kt +++ b/src/main/kotlin/com/api/admin/enums/Enum.kt @@ -9,5 +9,10 @@ enum class ChainType{ } enum class StatusType{ + ACTIVE, DEACTIVE +} + +enum class AccountType{ WITHDRAW, DEPOSIT -} \ No newline at end of file +} + diff --git a/src/main/kotlin/com/api/admin/rabbitMQ/event/AdminEventListener.kt b/src/main/kotlin/com/api/admin/rabbitMQ/event/AdminEventListener.kt index 191fbcb..bfefe55 100644 --- a/src/main/kotlin/com/api/admin/rabbitMQ/event/AdminEventListener.kt +++ b/src/main/kotlin/com/api/admin/rabbitMQ/event/AdminEventListener.kt @@ -1,5 +1,6 @@ package com.api.admin.rabbitMQ.event +import com.api.admin.rabbitMQ.event.dto.AdminTransferResponse import com.api.admin.rabbitMQ.sender.RabbitMQSender import org.springframework.context.event.EventListener import org.springframework.stereotype.Component @@ -10,7 +11,7 @@ class AdminEventListener( ) { @EventListener - fun onDepositSend(event: Long) { - provider.depositSend(event) + fun onDepositSend(event: AdminTransferResponse) { + provider.transferSend(event) } } \ No newline at end of file diff --git a/src/main/kotlin/com/api/admin/rabbitMQ/event/dto/AdminTransferCreatedEvent.kt b/src/main/kotlin/com/api/admin/rabbitMQ/event/dto/AdminTransferCreatedEvent.kt new file mode 100644 index 0000000..eb19997 --- /dev/null +++ b/src/main/kotlin/com/api/admin/rabbitMQ/event/dto/AdminTransferCreatedEvent.kt @@ -0,0 +1,5 @@ +package com.api.admin.rabbitMQ.event.dto + +import org.springframework.context.ApplicationEvent + +data class AdminTransferCreatedEvent(val eventSource: Any, val transfer: AdminTransferResponse): ApplicationEvent(eventSource) diff --git a/src/main/kotlin/com/api/admin/rabbitMQ/event/dto/AdminTransferResponse.kt b/src/main/kotlin/com/api/admin/rabbitMQ/event/dto/AdminTransferResponse.kt new file mode 100644 index 0000000..0f6dcd6 --- /dev/null +++ b/src/main/kotlin/com/api/admin/rabbitMQ/event/dto/AdminTransferResponse.kt @@ -0,0 +1,10 @@ +package com.api.admin.rabbitMQ.event.dto + + +data class AdminTransferResponse( + val id: Long, + val walletAddress: String, + val nftId: Long, + val timestamp: Long, + val accountType: String +) diff --git a/src/main/kotlin/com/api/admin/rabbitMQ/sender/RabbitMQSender.kt b/src/main/kotlin/com/api/admin/rabbitMQ/sender/RabbitMQSender.kt index a0d7cd1..5a1cb0b 100644 --- a/src/main/kotlin/com/api/admin/rabbitMQ/sender/RabbitMQSender.kt +++ b/src/main/kotlin/com/api/admin/rabbitMQ/sender/RabbitMQSender.kt @@ -1,5 +1,6 @@ package com.api.admin.rabbitMQ.sender +import com.api.admin.rabbitMQ.event.dto.AdminTransferResponse import org.springframework.amqp.rabbit.core.RabbitTemplate import org.springframework.stereotype.Service @@ -8,8 +9,8 @@ class RabbitMQSender( private val rabbitTemplate: RabbitTemplate ) { - fun depositSend(deposit: Long) { - rabbitTemplate.convertAndSend("depositExchange", "depositRoutingKey", deposit) + fun transferSend(transfer: AdminTransferResponse) { + rabbitTemplate.convertAndSend("depositExchange", "depositRoutingKey", transfer) } } \ No newline at end of file diff --git a/src/main/kotlin/com/api/admin/service/TransferService.kt b/src/main/kotlin/com/api/admin/service/TransferService.kt index 1d152ef..5fe4142 100644 --- a/src/main/kotlin/com/api/admin/service/TransferService.kt +++ b/src/main/kotlin/com/api/admin/service/TransferService.kt @@ -4,8 +4,11 @@ import com.api.admin.controller.dto.ValidTransferRequest import com.api.admin.domain.nft.NftRepository import com.api.admin.domain.transfer.Transfer import com.api.admin.domain.transfer.TransferRepository +import com.api.admin.enums.AccountType import com.api.admin.enums.ChainType -import com.api.admin.enums.StatusType +import com.api.admin.rabbitMQ.event.dto.AdminTransferCreatedEvent +import com.api.admin.rabbitMQ.event.dto.AdminTransferResponse +import org.springframework.context.ApplicationEventPublisher import org.springframework.stereotype.Service import org.web3j.protocol.core.methods.request.Transaction import org.web3j.abi.FunctionEncoder @@ -26,6 +29,7 @@ import java.time.Instant class TransferService( private val nftRepository: NftRepository, private val transferRepository: TransferRepository, + private val eventPublisher: ApplicationEventPublisher, ) { private val adminAddress = "0x01b72b4aa3f66f213d62d53e829bc172a6a72867" @@ -38,20 +42,28 @@ class TransferService( .flatMap { nft -> getNftOwner(request.chainType, nft.tokenAddress, nft.tokenId) .filterWhen { address -> Mono.just(address == adminAddress) } - .flatMap { saveTransfer(nft.id, wallet) } - // 이벤트로? + .flatMap { saveTransfer(nft.id, wallet, AccountType.DEPOSIT) } + .doOnSuccess { eventPublisher.publishEvent(AdminTransferCreatedEvent(this, it.toResponse())) } } } .then() } - fun saveTransfer(nftId: Long, wallet: String): Mono { + private fun Transfer.toResponse( ) = AdminTransferResponse( + id = this.id!!, + walletAddress = this.wallet, + nftId = this.nftId, + timestamp = this.timestamp, + accountType = this.accountType + ) + + fun saveTransfer(nftId: Long, wallet: String,accountType: AccountType): Mono { val transfer = Transfer( id = null, wallet = wallet, nftId = nftId, timestamp = Instant.now().toEpochMilli(), - status = StatusType.DEPOSIT.toString() + accountType = accountType.toString() ) return transferRepository.save(transfer) } diff --git a/src/main/resources/db/migration/V1__Initial_schema.sql b/src/main/resources/db/migration/V1__Initial_schema.sql index 009c01b..7cbaa9b 100644 --- a/src/main/resources/db/migration/V1__Initial_schema.sql +++ b/src/main/resources/db/migration/V1__Initial_schema.sql @@ -12,6 +12,6 @@ CREATE TABLE IF NOT EXISTS transfer ( id BIGINT PRIMARY KEY, wallet VARCHAR(255) NOT NULL, nft_id BIGINT REFERENCES nft(id), - timestamp bigint not null - status VARCHAR(255) NOT NULL -) + timestamp bigint not null, + account_type VARCHAR(255) NOT NULL +); From e3f021086ee5e05f6a7968d7ea0cfc14d5d37557 Mon Sep 17 00:00:00 2001 From: min-96 Date: Fri, 24 May 2024 03:13:02 +0900 Subject: [PATCH 03/24] refactor: deposit transfer(erc721,erc20) --- .../com/api/admin/domain/nft/NftRepository.kt | 3 + .../com/api/admin/domain/transfer/Transfer.kt | 9 +- src/main/kotlin/com/api/admin/enums/Enum.kt | 7 +- .../event/dto/AdminTransferResponse.kt | 2 +- .../com/api/admin/service/InfuraApiService.kt | 41 ++++++ .../com/api/admin/service/TransferService.kt | 135 +++++++++++------- .../api/admin/service/dto/InfuraRequest.kt | 9 ++ .../service/dto/InfuraTransferResponse.kt | 40 ++++++ .../db/migration/V1__Initial_schema.sql | 7 +- .../kotlin/com/api/admin/AdminServiceTest.kt | 34 ++++- 10 files changed, 221 insertions(+), 66 deletions(-) create mode 100644 src/main/kotlin/com/api/admin/service/InfuraApiService.kt create mode 100644 src/main/kotlin/com/api/admin/service/dto/InfuraRequest.kt create mode 100644 src/main/kotlin/com/api/admin/service/dto/InfuraTransferResponse.kt diff --git a/src/main/kotlin/com/api/admin/domain/nft/NftRepository.kt b/src/main/kotlin/com/api/admin/domain/nft/NftRepository.kt index 06e921c..ce881ce 100644 --- a/src/main/kotlin/com/api/admin/domain/nft/NftRepository.kt +++ b/src/main/kotlin/com/api/admin/domain/nft/NftRepository.kt @@ -1,6 +1,9 @@ package com.api.admin.domain.nft import org.springframework.data.repository.reactive.ReactiveCrudRepository +import reactor.core.publisher.Mono interface NftRepository : ReactiveCrudRepository{ + + fun findByTokenAddressAndTokenId(address:String,tokenId:String): Mono } \ No newline at end of file diff --git a/src/main/kotlin/com/api/admin/domain/transfer/Transfer.kt b/src/main/kotlin/com/api/admin/domain/transfer/Transfer.kt index 220e287..2ad674f 100644 --- a/src/main/kotlin/com/api/admin/domain/transfer/Transfer.kt +++ b/src/main/kotlin/com/api/admin/domain/transfer/Transfer.kt @@ -1,16 +1,19 @@ package com.api.admin.domain.transfer -import com.api.admin.enums.AccountType import org.springframework.data.annotation.Id import org.springframework.data.relational.core.mapping.Table +import java.math.BigDecimal @Table("transfer") class Transfer( @Id val id: Long?, - val nftId: Long, + val nftId: Long?, val wallet: String, val timestamp: Long, - val accountType: String + val accountType: String, + val balance: BigDecimal?, + val transferType: String, + val transactionHash: String?, ) { } \ No newline at end of file diff --git a/src/main/kotlin/com/api/admin/enums/Enum.kt b/src/main/kotlin/com/api/admin/enums/Enum.kt index 4abd8ee..60aa385 100644 --- a/src/main/kotlin/com/api/admin/enums/Enum.kt +++ b/src/main/kotlin/com/api/admin/enums/Enum.kt @@ -8,11 +8,12 @@ enum class ChainType{ POLYGON_MUMBAI, } -enum class StatusType{ - ACTIVE, DEACTIVE -} enum class AccountType{ WITHDRAW, DEPOSIT } +enum class TransferType { + ERC20,ERC721 +} + diff --git a/src/main/kotlin/com/api/admin/rabbitMQ/event/dto/AdminTransferResponse.kt b/src/main/kotlin/com/api/admin/rabbitMQ/event/dto/AdminTransferResponse.kt index 0f6dcd6..3c3217d 100644 --- a/src/main/kotlin/com/api/admin/rabbitMQ/event/dto/AdminTransferResponse.kt +++ b/src/main/kotlin/com/api/admin/rabbitMQ/event/dto/AdminTransferResponse.kt @@ -4,7 +4,7 @@ package com.api.admin.rabbitMQ.event.dto data class AdminTransferResponse( val id: Long, val walletAddress: String, - val nftId: Long, + val nftId: Long?, val timestamp: Long, val accountType: String ) diff --git a/src/main/kotlin/com/api/admin/service/InfuraApiService.kt b/src/main/kotlin/com/api/admin/service/InfuraApiService.kt new file mode 100644 index 0000000..78e2775 --- /dev/null +++ b/src/main/kotlin/com/api/admin/service/InfuraApiService.kt @@ -0,0 +1,41 @@ +package com.api.admin.service + +import com.api.admin.enums.ChainType +import com.api.admin.service.dto.InfuraRequest +import com.api.admin.service.dto.InfuraTransferDetail +import com.api.admin.service.dto.InfuraTransferResponse +import com.fasterxml.jackson.databind.ObjectMapper +import org.springframework.http.MediaType +import org.springframework.stereotype.Service +import org.springframework.web.reactive.function.client.WebClient +import reactor.core.publisher.Mono + +@Service +class InfuraApiService { + + private 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.ETHREUM_GOERLI -> "https://goerli.infura.io" + ChainType.ETHREUM_SEPOLIA -> "https://sepolia.infura.io" + ChainType.POLYGON_MUMBAI -> "https://polygon-mumbai.infura.io" + } + return WebClient.builder() + .baseUrl(baseUrl) + .build() + } + + fun getNftTransfer(chainType: ChainType, transactionHash: String): Mono { + val requestBody = InfuraRequest(method = "eth_getTransactionReceipt", params = listOf(transactionHash)) + val webClient = urlByChain(chainType) + + return webClient.post() + .uri("/v3/98b672d2ce9a4089a3a5cb5081dde2fa") + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(requestBody) + .retrieve() + .bodyToMono(InfuraTransferResponse::class.java) + + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/api/admin/service/TransferService.kt b/src/main/kotlin/com/api/admin/service/TransferService.kt index 5fe4142..d0acb25 100644 --- a/src/main/kotlin/com/api/admin/service/TransferService.kt +++ b/src/main/kotlin/com/api/admin/service/TransferService.kt @@ -8,6 +8,7 @@ import com.api.admin.enums.AccountType import com.api.admin.enums.ChainType import com.api.admin.rabbitMQ.event.dto.AdminTransferCreatedEvent import com.api.admin.rabbitMQ.event.dto.AdminTransferResponse +import com.api.admin.service.dto.InfuraTransferDetail import org.springframework.context.ApplicationEventPublisher import org.springframework.stereotype.Service import org.web3j.protocol.core.methods.request.Transaction @@ -22,6 +23,7 @@ import org.web3j.protocol.core.DefaultBlockParameterName import org.web3j.protocol.http.HttpService import reactor.core.publisher.Flux import reactor.core.publisher.Mono +import java.math.BigDecimal import java.math.BigInteger import java.time.Instant @@ -30,70 +32,99 @@ class TransferService( private val nftRepository: NftRepository, private val transferRepository: TransferRepository, private val eventPublisher: ApplicationEventPublisher, + private val infuraApiService: InfuraApiService, ) { - private val adminAddress = "0x01b72b4aa3f66f213d62d53e829bc172a6a72867" + private val adminAddress = "0x9bDeF468ae33b09b12a057B4c9211240D63BaE65" + private val transferEventSignature = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" - fun validTransfer(wallet: String, requests: List): Mono { - return Flux.fromIterable(requests) - .flatMap { request -> - nftRepository.findById(request.nftId) - .flatMap { nft -> - getNftOwner(request.chainType, nft.tokenAddress, nft.tokenId) - .filterWhen { address -> Mono.just(address == adminAddress) } - .flatMap { saveTransfer(nft.id, wallet, AccountType.DEPOSIT) } - .doOnSuccess { eventPublisher.publishEvent(AdminTransferCreatedEvent(this, it.toResponse())) } - } +// fun deposit(wallet: String, chainType: ChainType,transactionHash: String): Mono { +// return Flux.fromIterable(requests) +// .flatMap { request -> +// nftRepository.findById(request.nftId) +// .flatMap { nft -> +// getNftOwner(request.chainType, nft.tokenAddress, nft.tokenId) +// .filterWhen { address -> Mono.just(address == adminAddress) } +// .flatMap { saveTransfer(nft.id, wallet, AccountType.DEPOSIT) } +// .doOnSuccess { eventPublisher.publishEvent(AdminTransferCreatedEvent(this, it.toResponse())) } +// } +// } +// .then() +// } + +// private fun Transfer.toResponse( ) = AdminTransferResponse( +// id = this.id!!, +// walletAddress = this.wallet, +// nftId = this.nftId , +// timestamp = this.timestamp, +// accountType = this.accountType +// ) + + fun saveTransfer(wallet: String, chainType: ChainType, transactionHash: String): Mono { + return infuraApiService.getNftTransfer(chainType, transactionHash) + .flatMapMany { response -> + println(response.toString()) + Flux.fromIterable(response.result.logs) + .flatMap { it.toEntity(wallet) } + } + .flatMap { transfer -> + transferRepository.save(transfer) } .then() } - private fun Transfer.toResponse( ) = AdminTransferResponse( - id = this.id!!, - walletAddress = this.wallet, - nftId = this.nftId, - timestamp = this.timestamp, - accountType = this.accountType - ) - - fun saveTransfer(nftId: Long, wallet: String,accountType: AccountType): Mono { - val transfer = Transfer( - id = null, - wallet = wallet, - nftId = nftId, - timestamp = Instant.now().toEpochMilli(), - accountType = accountType.toString() - ) - return transferRepository.save(transfer) - } - //TODO("apiKey 캡슐화") - fun getNftOwner(chainType: ChainType, contractAddress: String, tokenId: String): Mono { - val web3 = Web3j.build(HttpService(chainType.baseUrl() + "/v3/98b672d2ce9a4089a3a5cb5081dde2fa")) - val function = Function( - "ownerOf", - listOf(Uint256(BigInteger(tokenId))), - listOf(object : TypeReference
() {}) - ) - val encodedFunction = FunctionEncoder.encode(function) - val transaction = Transaction.createEthCallTransaction(null, contractAddress, encodedFunction) + fun InfuraTransferDetail.toEntity(wallet: String): Mono { + return Mono.just(this) + .filter { it.topics.isNotEmpty() && it.topics[0] == transferEventSignature } + .filter { it.topics.size >= 3 && parseAddress(it.topics[2]) == adminAddress.lowercase() && parseAddress(it.topics[1]) == wallet.lowercase() } + .flatMap { log -> + println("log: " + log.toString()) + val transferType = if (log.topics.size > 3) "ERC721" else "ERC20" - return Mono.fromCallable { - val ethCall = web3.ethCall(transaction, DefaultBlockParameterName.LATEST).send() - val decode = FunctionReturnDecoder.decode(ethCall.value, function.outputParameters) - if (decode.isEmpty()) null else decode[0].value as String - }.retry(3) + when (transferType) { + "ERC721" -> { + val tokenId = BigInteger(log.topics[3].removePrefix("0x"), 16).toString() + nftRepository.findByTokenAddressAndTokenId(log.address, tokenId) // 없으면 nft서버가서 저장해와 ㅋ + .mapNotNull { nft -> + Transfer( + id = null, + nftId = nft.id, + wallet = wallet, + timestamp = System.currentTimeMillis(), + accountType = "DEPOSIT", + balance = null, + transferType = transferType, + transactionHash = log.transactionHash + ) + } + } + else -> { + val balance = toBigDecimal(log.data) + Mono.just( + Transfer( + id = null, + nftId = null, + wallet = wallet, + timestamp = System.currentTimeMillis(), + accountType = "DEPOSIT", + balance = balance, + transferType = transferType, + transactionHash = log.transactionHash + ) + ) + } + } + } } - fun ChainType.baseUrl(): String { - return when(this){ - ChainType.ETHEREUM_MAINNET -> "https://mainnet.infura.io" - ChainType.POLYGON_MAINNET -> "https://polygon-mainnet.infura.io" - ChainType.ETHREUM_GOERLI -> "https://goerli.infura.io" - ChainType.ETHREUM_SEPOLIA -> "https://sepolia.infura.io" - ChainType.POLYGON_MUMBAI -> "https://polygon-mumbai.infura.io" - } + private fun parseAddress(address: String): String { + return "0x" + address.substring(26).padStart(40, '0') } + + private fun toBigDecimal(balance: String): BigDecimal = + BigInteger(balance.removePrefix("0x"), 16).toBigDecimal().divide(BigDecimal("1000000000000000000")) + } \ No newline at end of file diff --git a/src/main/kotlin/com/api/admin/service/dto/InfuraRequest.kt b/src/main/kotlin/com/api/admin/service/dto/InfuraRequest.kt new file mode 100644 index 0000000..6c08245 --- /dev/null +++ b/src/main/kotlin/com/api/admin/service/dto/InfuraRequest.kt @@ -0,0 +1,9 @@ +package com.api.admin.service.dto + +data class InfuraRequest( + val jsonrpc: String = "2.0", + val method: String, + val params: List = emptyList(), + val id: Int = 1 +) + diff --git a/src/main/kotlin/com/api/admin/service/dto/InfuraTransferResponse.kt b/src/main/kotlin/com/api/admin/service/dto/InfuraTransferResponse.kt new file mode 100644 index 0000000..0394d4e --- /dev/null +++ b/src/main/kotlin/com/api/admin/service/dto/InfuraTransferResponse.kt @@ -0,0 +1,40 @@ +package com.api.admin.service.dto + +import java.math.BigDecimal +import java.math.BigInteger + +data class InfuraTransferResponse( + val jsonrpc: String, + val id: String, + val result: InfuraTransferResult, +) + +data class InfuraTransferResult( + val blockHash : String, + val blockNumber: String, + val contractAddress: String?, + val cumulativeGasUsed: String?, + val effectiveGasPrice: String?, + val from: String, + val gasUsed: String?, + val logs:List, + val logsBloom: String, + val status: String, + val to: String, + val transactionHash: String, + val transactionIndex: String, + val type: String, + +) + +data class InfuraTransferDetail( + val address: String, + val blockHash: String, + val blockNumber: String, + val data: String, + val logIndex: String, + val removed: Boolean, + val topics: List, + val transactionHash: String, + val transactionIndex: String +) \ No newline at end of file diff --git a/src/main/resources/db/migration/V1__Initial_schema.sql b/src/main/resources/db/migration/V1__Initial_schema.sql index 7cbaa9b..707e9a3 100644 --- a/src/main/resources/db/migration/V1__Initial_schema.sql +++ b/src/main/resources/db/migration/V1__Initial_schema.sql @@ -9,9 +9,12 @@ CREATE TABLE IF NOT EXISTS nft ( CREATE TABLE IF NOT EXISTS transfer ( - id BIGINT PRIMARY KEY, + id SERIAL PRIMARY KEY, wallet VARCHAR(255) NOT NULL, nft_id BIGINT REFERENCES nft(id), timestamp bigint not null, - account_type VARCHAR(255) NOT NULL + account_type VARCHAR(255) NOT NULL, + balance DECIMAL(19, 4), + transfer_type VARCHAR(255) NOT NULL, + transaction_hash VARCHAR(255) NOT NULL ); diff --git a/src/test/kotlin/com/api/admin/AdminServiceTest.kt b/src/test/kotlin/com/api/admin/AdminServiceTest.kt index c15b6eb..dbff483 100644 --- a/src/test/kotlin/com/api/admin/AdminServiceTest.kt +++ b/src/test/kotlin/com/api/admin/AdminServiceTest.kt @@ -2,6 +2,7 @@ package com.api.admin import com.api.admin.enums.ChainType import com.api.admin.rabbitMQ.sender.RabbitMQSender +import com.api.admin.service.InfuraApiService import com.api.admin.service.TransferService import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired @@ -11,16 +12,39 @@ import org.springframework.boot.test.context.SpringBootTest class AdminServiceTest( @Autowired private val transferService: TransferService, @Autowired private val rabbitMQSender: RabbitMQSender, + @Autowired private val infuraApiService: InfuraApiService, ) { +// @Test +// fun test() { +// val res = transferService.getNftOwner(ChainType.POLYGON_MAINNET,"0xa3784fe9104fdc0b988769fba7459ece2fb36eea","0") +// println(res) +// } + + @Test + fun getNftTransferDetail() { + + val res = infuraApiService.getNftTransfer(ChainType.POLYGON_MAINNET,"0x55fa4495f983e9f162b39b3df4dec8ebcff9aa05daee7b051c680ccfb49422a6").block() + println(res.toString()) + } + @Test - fun test() { - val res = transferService.getNftOwner(ChainType.POLYGON_MAINNET,"0xa3784fe9104fdc0b988769fba7459ece2fb36eea","0") - println(res) + fun saveTransfer() { + transferService.saveTransfer("0x01b72b4aa3f66f213d62d53e829bc172a6a72867",ChainType.POLYGON_MAINNET,"0x55fa4495f983e9f162b39b3df4dec8ebcff9aa05daee7b051c680ccfb49422a6").block() } @Test - fun rabbitMqTest() { - rabbitMQSender.depositSend(1) + fun test1() { + val address = "0x0000000000000000000000009bdef468ae33b09b12a057b4c9211240d63bae65" + val result = parseAddress(address) + println(result) + println(result == "0x9bDeF468ae33b09b12a057B4c9211240D63BaE65") + + } + + private fun parseAddress(address: String): String { + return "0x" + address.substring(26).padStart(40, '0') } + + } \ No newline at end of file From 32d0f15475afb93772fe58c12533de418a9a4223 Mon Sep 17 00:00:00 2001 From: min-96 Date: Fri, 24 May 2024 17:11:41 +0900 Subject: [PATCH 04/24] refactor: send deposit --- .../com/api/admin/domain/transfer/Transfer.kt | 2 +- .../domain/transfer/TransferRepository.kt | 3 + .../rabbitMQ/event/AdminEventListener.kt | 5 +- .../event/dto/AdminTransferResponse.kt | 6 +- .../admin/rabbitMQ/sender/RabbitMQSender.kt | 2 +- .../com/api/admin/service/TransferService.kt | 100 +++++++++--------- .../kotlin/com/api/admin/AdminServiceTest.kt | 30 +++++- 7 files changed, 91 insertions(+), 57 deletions(-) diff --git a/src/main/kotlin/com/api/admin/domain/transfer/Transfer.kt b/src/main/kotlin/com/api/admin/domain/transfer/Transfer.kt index 2ad674f..e670903 100644 --- a/src/main/kotlin/com/api/admin/domain/transfer/Transfer.kt +++ b/src/main/kotlin/com/api/admin/domain/transfer/Transfer.kt @@ -13,7 +13,7 @@ class Transfer( val accountType: String, val balance: BigDecimal?, val transferType: String, - val transactionHash: String?, + val transactionHash: String, ) { } \ No newline at end of file diff --git a/src/main/kotlin/com/api/admin/domain/transfer/TransferRepository.kt b/src/main/kotlin/com/api/admin/domain/transfer/TransferRepository.kt index e23a988..c19e5e1 100644 --- a/src/main/kotlin/com/api/admin/domain/transfer/TransferRepository.kt +++ b/src/main/kotlin/com/api/admin/domain/transfer/TransferRepository.kt @@ -1,6 +1,9 @@ package com.api.admin.domain.transfer import org.springframework.data.repository.reactive.ReactiveCrudRepository +import reactor.core.publisher.Mono interface TransferRepository : ReactiveCrudRepository { + fun findByWalletAndAccountTypeAndNftId(wallet:String, accountType:String,nftId:Long) : Mono + fun existsByWalletAndAccountTypeAndTransactionHashAndTimestampAfter(wallet: String,accountType: String,transactionHash: String,timestamp:Long) : Mono } \ No newline at end of file diff --git a/src/main/kotlin/com/api/admin/rabbitMQ/event/AdminEventListener.kt b/src/main/kotlin/com/api/admin/rabbitMQ/event/AdminEventListener.kt index bfefe55..36b4870 100644 --- a/src/main/kotlin/com/api/admin/rabbitMQ/event/AdminEventListener.kt +++ b/src/main/kotlin/com/api/admin/rabbitMQ/event/AdminEventListener.kt @@ -1,5 +1,6 @@ package com.api.admin.rabbitMQ.event +import com.api.admin.rabbitMQ.event.dto.AdminTransferCreatedEvent import com.api.admin.rabbitMQ.event.dto.AdminTransferResponse import com.api.admin.rabbitMQ.sender.RabbitMQSender import org.springframework.context.event.EventListener @@ -11,7 +12,7 @@ class AdminEventListener( ) { @EventListener - fun onDepositSend(event: AdminTransferResponse) { - provider.transferSend(event) + fun onDepositSend(event: AdminTransferCreatedEvent) { + provider.transferSend(event.transfer) } } \ No newline at end of file diff --git a/src/main/kotlin/com/api/admin/rabbitMQ/event/dto/AdminTransferResponse.kt b/src/main/kotlin/com/api/admin/rabbitMQ/event/dto/AdminTransferResponse.kt index 3c3217d..1c2d244 100644 --- a/src/main/kotlin/com/api/admin/rabbitMQ/event/dto/AdminTransferResponse.kt +++ b/src/main/kotlin/com/api/admin/rabbitMQ/event/dto/AdminTransferResponse.kt @@ -1,10 +1,14 @@ package com.api.admin.rabbitMQ.event.dto +import java.math.BigDecimal + data class AdminTransferResponse( val id: Long, val walletAddress: String, val nftId: Long?, val timestamp: Long, - val accountType: String + val accountType: String, + val transferType: String, + val balance: BigDecimal?, ) diff --git a/src/main/kotlin/com/api/admin/rabbitMQ/sender/RabbitMQSender.kt b/src/main/kotlin/com/api/admin/rabbitMQ/sender/RabbitMQSender.kt index 5a1cb0b..a042818 100644 --- a/src/main/kotlin/com/api/admin/rabbitMQ/sender/RabbitMQSender.kt +++ b/src/main/kotlin/com/api/admin/rabbitMQ/sender/RabbitMQSender.kt @@ -10,7 +10,7 @@ class RabbitMQSender( ) { fun transferSend(transfer: AdminTransferResponse) { - rabbitTemplate.convertAndSend("depositExchange", "depositRoutingKey", transfer) + rabbitTemplate.convertAndSend("transferExchange", "transferRoutingKey", transfer) } } \ No newline at end of file diff --git a/src/main/kotlin/com/api/admin/service/TransferService.kt b/src/main/kotlin/com/api/admin/service/TransferService.kt index d0acb25..7921fec 100644 --- a/src/main/kotlin/com/api/admin/service/TransferService.kt +++ b/src/main/kotlin/com/api/admin/service/TransferService.kt @@ -1,26 +1,16 @@ package com.api.admin.service -import com.api.admin.controller.dto.ValidTransferRequest import com.api.admin.domain.nft.NftRepository import com.api.admin.domain.transfer.Transfer import com.api.admin.domain.transfer.TransferRepository import com.api.admin.enums.AccountType import com.api.admin.enums.ChainType +import com.api.admin.enums.TransferType import com.api.admin.rabbitMQ.event.dto.AdminTransferCreatedEvent import com.api.admin.rabbitMQ.event.dto.AdminTransferResponse import com.api.admin.service.dto.InfuraTransferDetail import org.springframework.context.ApplicationEventPublisher import org.springframework.stereotype.Service -import org.web3j.protocol.core.methods.request.Transaction -import org.web3j.abi.FunctionEncoder -import org.web3j.abi.FunctionReturnDecoder -import org.web3j.abi.datatypes.Function -import org.web3j.abi.datatypes.generated.Uint256 -import org.web3j.abi.TypeReference -import org.web3j.abi.datatypes.Address -import org.web3j.protocol.Web3j -import org.web3j.protocol.core.DefaultBlockParameterName -import org.web3j.protocol.http.HttpService import reactor.core.publisher.Flux import reactor.core.publisher.Mono import java.math.BigDecimal @@ -38,65 +28,73 @@ class TransferService( private val adminAddress = "0x9bDeF468ae33b09b12a057B4c9211240D63BaE65" private val transferEventSignature = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" - -// fun deposit(wallet: String, chainType: ChainType,transactionHash: String): Mono { -// return Flux.fromIterable(requests) -// .flatMap { request -> -// nftRepository.findById(request.nftId) -// .flatMap { nft -> -// getNftOwner(request.chainType, nft.tokenAddress, nft.tokenId) -// .filterWhen { address -> Mono.just(address == adminAddress) } -// .flatMap { saveTransfer(nft.id, wallet, AccountType.DEPOSIT) } -// .doOnSuccess { eventPublisher.publishEvent(AdminTransferCreatedEvent(this, it.toResponse())) } -// } -// } -// .then() -// } - -// private fun Transfer.toResponse( ) = AdminTransferResponse( -// id = this.id!!, -// walletAddress = this.wallet, -// nftId = this.nftId , -// timestamp = this.timestamp, -// accountType = this.accountType -// ) - - fun saveTransfer(wallet: String, chainType: ChainType, transactionHash: String): Mono { + fun deposit(wallet: String, chainType: ChainType, transactionHash: String): Mono { + return saveTransfer(wallet, chainType, transactionHash) + .doOnNext { transfer -> + eventPublisher.publishEvent(AdminTransferCreatedEvent(this, transfer.toResponse())) + } + .then() + } + fun saveTransfer(wallet: String, chainType: ChainType, transactionHash: String): Flux { return infuraApiService.getNftTransfer(chainType, transactionHash) .flatMapMany { response -> - println(response.toString()) Flux.fromIterable(response.result.logs) - .flatMap { it.toEntity(wallet) } + .flatMap { it.toEntity(wallet, AccountType.DEPOSIT) } } .flatMap { transfer -> - transferRepository.save(transfer) + checkTransferExistenceAndSave(transfer) } - .then() } + private fun checkTransferExistenceAndSave(transfer: Transfer): Mono { + return transferRepository.findByWalletAndAccountTypeAndNftId(transfer.wallet, transfer.accountType, transfer.nftId!!) + .flatMap { existingTransfer -> + if (existingTransfer != null) { + Mono.empty() + } else { + transferRepository.existsByWalletAndAccountTypeAndTransactionHashAndTimestampAfter( + transfer.wallet, AccountType.WITHDRAW.toString(), transfer.transactionHash, transfer.timestamp + ).flatMap { withdrawalExists -> + if (withdrawalExists) { + Mono.empty() + } else { + transferRepository.save(transfer) + } + } + } + } + } + + private fun Transfer.toResponse() = AdminTransferResponse( + id = this.id!!, + walletAddress = this.wallet, + nftId = this.nftId, + timestamp =this.timestamp, + accountType = this.accountType, + transferType = this.transferType, + balance = this.balance + ) - fun InfuraTransferDetail.toEntity(wallet: String): Mono { + fun InfuraTransferDetail.toEntity(wallet: String,accountType: AccountType): Mono { return Mono.just(this) .filter { it.topics.isNotEmpty() && it.topics[0] == transferEventSignature } .filter { it.topics.size >= 3 && parseAddress(it.topics[2]) == adminAddress.lowercase() && parseAddress(it.topics[1]) == wallet.lowercase() } .flatMap { log -> - println("log: " + log.toString()) - val transferType = if (log.topics.size > 3) "ERC721" else "ERC20" - + val transferType = if (log.topics.size > 3) TransferType.ERC721 else TransferType.ERC20 when (transferType) { - "ERC721" -> { + TransferType.ERC721 -> { val tokenId = BigInteger(log.topics[3].removePrefix("0x"), 16).toString() nftRepository.findByTokenAddressAndTokenId(log.address, tokenId) // 없으면 nft서버가서 저장해와 ㅋ - .mapNotNull { nft -> + .map { nft -> Transfer( id = null, nftId = nft.id, wallet = wallet, - timestamp = System.currentTimeMillis(), - accountType = "DEPOSIT", + timestamp = Instant.now().toEpochMilli(), + accountType = accountType.toString(), balance = null, - transferType = transferType, + transferType = transferType.toString(), transactionHash = log.transactionHash ) } @@ -108,10 +106,10 @@ class TransferService( id = null, nftId = null, wallet = wallet, - timestamp = System.currentTimeMillis(), - accountType = "DEPOSIT", + timestamp = Instant.now().toEpochMilli(), + accountType = accountType.toString(), balance = balance, - transferType = transferType, + transferType = transferType.toString(), transactionHash = log.transactionHash ) ) diff --git a/src/test/kotlin/com/api/admin/AdminServiceTest.kt b/src/test/kotlin/com/api/admin/AdminServiceTest.kt index dbff483..af05d6c 100644 --- a/src/test/kotlin/com/api/admin/AdminServiceTest.kt +++ b/src/test/kotlin/com/api/admin/AdminServiceTest.kt @@ -1,12 +1,17 @@ package com.api.admin +import com.api.admin.domain.transfer.Transfer +import com.api.admin.enums.AccountType import com.api.admin.enums.ChainType +import com.api.admin.enums.TransferType +import com.api.admin.rabbitMQ.event.dto.AdminTransferResponse import com.api.admin.rabbitMQ.sender.RabbitMQSender import com.api.admin.service.InfuraApiService import com.api.admin.service.TransferService import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest +import java.time.Instant @SpringBootTest class AdminServiceTest( @@ -30,7 +35,7 @@ class AdminServiceTest( @Test fun saveTransfer() { - transferService.saveTransfer("0x01b72b4aa3f66f213d62d53e829bc172a6a72867",ChainType.POLYGON_MAINNET,"0x55fa4495f983e9f162b39b3df4dec8ebcff9aa05daee7b051c680ccfb49422a6").block() + transferService.saveTransfer("0x01b72b4aa3f66f213d62d53e829bc172a6a72867",ChainType.POLYGON_MAINNET,"0x55fa4495f983e9f162b39b3df4dec8ebcff9aa05daee7b051c680ccfb49422a6").next().block() } @Test @@ -42,9 +47,32 @@ class AdminServiceTest( } + @Test + fun deposit() { + transferService.deposit("0x01b72b4aa3f66f213d62d53e829bc172a6a72867",ChainType.POLYGON_MAINNET,"0x55fa4495f983e9f162b39b3df4dec8ebcff9aa05daee7b051c680ccfb49422a6") + .block() + + Thread.sleep(100000) + } + private fun parseAddress(address: String): String { return "0x" + address.substring(26).padStart(40, '0') } + @Test + fun sendMessage() { + val response = AdminTransferResponse( + id= 1L, + walletAddress = "0x01b72b4aa3f66f213d62d53e829bc172a6a72867", + nftId = 1L, + timestamp = Instant.now().toEpochMilli(), + accountType = AccountType.DEPOSIT.toString(), + transferType = TransferType.ERC721.toString(), + balance = null + ) + rabbitMQSender.transferSend(response) + Thread.sleep(10000) + } + } \ No newline at end of file From c6296ca0d8b6c19b2499ddbadf043d1f1f810e6d Mon Sep 17 00:00:00 2001 From: min-96 Date: Fri, 24 May 2024 23:06:40 +0900 Subject: [PATCH 05/24] feat: create wrapper(erc20) --- src/main/kotlin/com/api/admin/enums/Enum.kt | 8 + .../com/api/admin/service/InfuraApiService.kt | 15 +- .../com/api/admin/service/TransferService.kt | 2 + .../com/api/admin/service/Web3jService.kt | 67 ++++++ .../com/api/admin/wrapper/ERC20ABI.json | 217 ++++++++++++++++++ .../kotlin/com/api/admin/AdminServiceTest.kt | 21 ++ 6 files changed, 328 insertions(+), 2 deletions(-) create mode 100644 src/main/kotlin/com/api/admin/service/Web3jService.kt create mode 100644 src/main/kotlin/com/api/admin/wrapper/ERC20ABI.json diff --git a/src/main/kotlin/com/api/admin/enums/Enum.kt b/src/main/kotlin/com/api/admin/enums/Enum.kt index 60aa385..f7fd4c2 100644 --- a/src/main/kotlin/com/api/admin/enums/Enum.kt +++ b/src/main/kotlin/com/api/admin/enums/Enum.kt @@ -17,3 +17,11 @@ enum class TransferType { ERC20,ERC721 } +//enum class ChainType(val chainId: Long, val baseUrl: String) { +// ETHEREUM_MAINNET(1L, "https://mainnet.infura.io/v3/YOUR_INFURA_PROJECT_ID"), +// POLYGON_MAINNET(137L, "https://polygon-mainnet.infura.io/v3/YOUR_INFURA_PROJECT_ID"), +// ETHEREUM_GOERLI(5L, "https://goerli.infura.io/v3/YOUR_INFURA_PROJECT_ID"), +// ETHEREUM_SEPOLIA(11155111L, "https://sepolia.infura.io/v3/YOUR_INFURA_PROJECT_ID"), +// POLYGON_MUMBAI(80001L, "https://polygon-mumbai.infura.io/v3/YOUR_INFURA_PROJECT_ID") +//} + diff --git a/src/main/kotlin/com/api/admin/service/InfuraApiService.kt b/src/main/kotlin/com/api/admin/service/InfuraApiService.kt index 78e2775..55f473a 100644 --- a/src/main/kotlin/com/api/admin/service/InfuraApiService.kt +++ b/src/main/kotlin/com/api/admin/service/InfuraApiService.kt @@ -2,9 +2,7 @@ package com.api.admin.service import com.api.admin.enums.ChainType import com.api.admin.service.dto.InfuraRequest -import com.api.admin.service.dto.InfuraTransferDetail import com.api.admin.service.dto.InfuraTransferResponse -import com.fasterxml.jackson.databind.ObjectMapper import org.springframework.http.MediaType import org.springframework.stereotype.Service import org.springframework.web.reactive.function.client.WebClient @@ -38,4 +36,17 @@ class InfuraApiService { .bodyToMono(InfuraTransferResponse::class.java) } + + fun getSend(chainType: ChainType,signedTransactionData: String): Mono { + val requestBody = InfuraRequest(method = "eth_sendRawTransaction", params = listOf(signedTransactionData)) + val webClient = urlByChain(chainType) + + return webClient.post() + .uri("/v3/98b672d2ce9a4089a3a5cb5081dde2fa") + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(requestBody) + .retrieve() + .bodyToMono(String::class.java) + + } } \ No newline at end of file diff --git a/src/main/kotlin/com/api/admin/service/TransferService.kt b/src/main/kotlin/com/api/admin/service/TransferService.kt index 7921fec..6d21152 100644 --- a/src/main/kotlin/com/api/admin/service/TransferService.kt +++ b/src/main/kotlin/com/api/admin/service/TransferService.kt @@ -28,6 +28,8 @@ class TransferService( private val adminAddress = "0x9bDeF468ae33b09b12a057B4c9211240D63BaE65" private val transferEventSignature = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" + + fun deposit(wallet: String, chainType: ChainType, transactionHash: String): Mono { return saveTransfer(wallet, chainType, transactionHash) .doOnNext { transfer -> diff --git a/src/main/kotlin/com/api/admin/service/Web3jService.kt b/src/main/kotlin/com/api/admin/service/Web3jService.kt new file mode 100644 index 0000000..ead20b5 --- /dev/null +++ b/src/main/kotlin/com/api/admin/service/Web3jService.kt @@ -0,0 +1,67 @@ +package com.api.admin.service + +import com.api.admin.enums.ChainType +import com.api.admin.wrapper.ERC20ABI +import org.springframework.stereotype.Service +import org.web3j.crypto.Credentials +import org.web3j.crypto.RawTransaction +import org.web3j.crypto.TransactionEncoder +import org.web3j.protocol.Web3j +import org.web3j.protocol.core.DefaultBlockParameterName +import org.web3j.protocol.core.methods.response.EthSendTransaction +import org.web3j.protocol.http.HttpService +import org.web3j.tx.ClientTransactionManager +import org.web3j.tx.gas.DefaultGasProvider +import org.web3j.utils.Numeric +import java.math.BigInteger + +@Service +class Web3jService( + private val infuraApiService: InfuraApiService, +) { + + + fun sendMatic() { + val web3j = Web3j.build(HttpService("https://polygon-mainnet.infura.io/v3/98b672d2ce9a4089a3a5cb5081dde2fa")) + val credentials = Credentials.create("e9769d3c00032a83d703e03630edbfc3cb634b40b92e38ab2890d5e37f21bb15") + + val nonce = web3j.ethGetTransactionCount(credentials.address, DefaultBlockParameterName.LATEST) + .send().transactionCount + val gasPrice = web3j.ethGasPrice().send().gasPrice + val gasLimit = BigInteger.valueOf(21000) + val recipientAddress = "0x9bDeF468ae33b09b12a057B4c9211240D63BaE65" + val amount = BigInteger("1000000000000000000") + + val rawTransaction = RawTransaction.createEtherTransaction( + nonce, gasPrice, gasLimit, recipientAddress, amount + ) + + val signedMessage = TransactionEncoder.signMessage(rawTransaction, credentials) + val hexValue = Numeric.toHexString(signedMessage) + + val ethSendTransaction = web3j.ethSendRawTransaction(hexValue).send() + val transactionHash = ethSendTransaction.transactionHash + + println("Transaction hash: $transactionHash") + } + + fun createTransaction(privateKey:String,recipientAddress: String, amount: BigInteger): String{ + val web3j = Web3j.build(HttpService("https://polygon-mainnet.infura.io/v3/98b672d2ce9a4089a3a5cb5081dde2fa")) + val credentials = Credentials.create(privateKey) + return createTransactionData(web3j, credentials, recipientAddress, amount) + + } + + fun createTransactionData(web3j: Web3j, credentials: Credentials, recipientAddress: String, amountInWei: BigInteger): String { + val nonce = web3j.ethGetTransactionCount(credentials.address, DefaultBlockParameterName.LATEST).send().transactionCount + val gasPrice = web3j.ethGasPrice().send().gasPrice + val gasLimit = BigInteger.valueOf(21000) + val chainId = 137L // 이더리움메인넷 1L + val rawTransaction = RawTransaction.createEtherTransaction( + nonce, gasPrice, gasLimit, recipientAddress, amountInWei + ) + + val signedMessage = TransactionEncoder.signMessage(rawTransaction, chainId, credentials) + return Numeric.toHexString(signedMessage) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/api/admin/wrapper/ERC20ABI.json b/src/main/kotlin/com/api/admin/wrapper/ERC20ABI.json new file mode 100644 index 0000000..32a70ff --- /dev/null +++ b/src/main/kotlin/com/api/admin/wrapper/ERC20ABI.json @@ -0,0 +1,217 @@ +[ + { + "constant": true, + "inputs": [], + "name": "name", + "outputs": [ + { + "name": "", + "type": "string" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_spender", + "type": "address" + }, + { + "name": "_value", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "decimals", + "outputs": [ + { + "name": "", + "type": "uint8" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "symbol", + "outputs": [ + { + "name": "", + "type": "string" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_to", + "type": "address" + }, + { + "name": "_value", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_owner", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "name": "balance", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_from", + "type": "address" + }, + { + "name": "_to", + "type": "address" + }, + { + "name": "_value", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_owner", + "type": "address" + }, + { + "name": "_spender", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "name": "remaining", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "from", + "type": "address" + }, + { + "indexed": true, + "name": "to", + "type": "address" + }, + { + "indexed": false, + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + } +] diff --git a/src/test/kotlin/com/api/admin/AdminServiceTest.kt b/src/test/kotlin/com/api/admin/AdminServiceTest.kt index af05d6c..65a3e7a 100644 --- a/src/test/kotlin/com/api/admin/AdminServiceTest.kt +++ b/src/test/kotlin/com/api/admin/AdminServiceTest.kt @@ -8,9 +8,11 @@ import com.api.admin.rabbitMQ.event.dto.AdminTransferResponse import com.api.admin.rabbitMQ.sender.RabbitMQSender import com.api.admin.service.InfuraApiService import com.api.admin.service.TransferService +import com.api.admin.service.Web3jService import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest +import java.math.BigInteger import java.time.Instant @SpringBootTest @@ -18,6 +20,7 @@ class AdminServiceTest( @Autowired private val transferService: TransferService, @Autowired private val rabbitMQSender: RabbitMQSender, @Autowired private val infuraApiService: InfuraApiService, + @Autowired private val web3jService: Web3jService, ) { // @Test @@ -74,5 +77,23 @@ class AdminServiceTest( Thread.sleep(10000) } + @Test + fun sendMatic() { +// web3jService.createTransaction( +// "e9769d3c00032a83d703e03630edbfc3cb634b40b92e38ab2890d5e37f21bb15", +// "0x9bDeF468ae33b09b12a057B4c9211240D63BaE65", +// BigInteger("1000000000000000000") +// ) + + val transactionData = web3jService.createTransaction("e9769d3c00032a83d703e03630edbfc3cb634b40b92e38ab2890d5e37f21bb15", + "0x9bDeF468ae33b09b12a057B4c9211240D63BaE65", + BigInteger("1000000000000000000")) + + + + val response = infuraApiService.getSend(ChainType.POLYGON_MAINNET,transactionData).block() + println(response) + } + } \ No newline at end of file From 0ef25f3957da199fcbe2d2c3a29235274dbb8a37 Mon Sep 17 00:00:00 2001 From: min-96 Date: Sun, 26 May 2024 02:09:47 +0900 Subject: [PATCH 06/24] feat: create wrapper(erc721) --- .../com/api/admin/domain/transfer/Transfer.kt | 2 +- src/main/kotlin/com/api/admin/enums/Enum.kt | 2 + .../com/api/admin/service/InfuraApiService.kt | 19 +- .../com/api/admin/service/Web3jService.kt | 67 ++-- .../com/api/admin/wrapper/ERC721ABI.json | 296 ++++++++++++++++++ .../kotlin/com/api/admin/AdminServiceTest.kt | 6 + 6 files changed, 356 insertions(+), 36 deletions(-) create mode 100644 src/main/kotlin/com/api/admin/wrapper/ERC721ABI.json diff --git a/src/main/kotlin/com/api/admin/domain/transfer/Transfer.kt b/src/main/kotlin/com/api/admin/domain/transfer/Transfer.kt index e670903..b631259 100644 --- a/src/main/kotlin/com/api/admin/domain/transfer/Transfer.kt +++ b/src/main/kotlin/com/api/admin/domain/transfer/Transfer.kt @@ -5,7 +5,7 @@ import org.springframework.data.relational.core.mapping.Table import java.math.BigDecimal @Table("transfer") -class Transfer( +data class Transfer( @Id val id: Long?, val nftId: Long?, val wallet: String, diff --git a/src/main/kotlin/com/api/admin/enums/Enum.kt b/src/main/kotlin/com/api/admin/enums/Enum.kt index f7fd4c2..89dd2ab 100644 --- a/src/main/kotlin/com/api/admin/enums/Enum.kt +++ b/src/main/kotlin/com/api/admin/enums/Enum.kt @@ -25,3 +25,5 @@ enum class TransferType { // POLYGON_MUMBAI(80001L, "https://polygon-mumbai.infura.io/v3/YOUR_INFURA_PROJECT_ID") //} + + diff --git a/src/main/kotlin/com/api/admin/service/InfuraApiService.kt b/src/main/kotlin/com/api/admin/service/InfuraApiService.kt index 55f473a..6e9aa23 100644 --- a/src/main/kotlin/com/api/admin/service/InfuraApiService.kt +++ b/src/main/kotlin/com/api/admin/service/InfuraApiService.kt @@ -11,6 +11,8 @@ import reactor.core.publisher.Mono @Service class InfuraApiService { + private val apiKey = "98b672d2ce9a4089a3a5cb5081dde2fa" + private fun urlByChain(chainType: ChainType) : WebClient { val baseUrl = when (chainType) { ChainType.ETHEREUM_MAINNET -> "https://mainnet.infura.io" @@ -29,7 +31,7 @@ class InfuraApiService { val webClient = urlByChain(chainType) return webClient.post() - .uri("/v3/98b672d2ce9a4089a3a5cb5081dde2fa") + .uri("/v3/$apiKey") .contentType(MediaType.APPLICATION_JSON) .bodyValue(requestBody) .retrieve() @@ -37,16 +39,27 @@ class InfuraApiService { } - fun getSend(chainType: ChainType,signedTransactionData: String): Mono { + fun getSend(chainType: ChainType, signedTransactionData: String): Mono { val requestBody = InfuraRequest(method = "eth_sendRawTransaction", params = listOf(signedTransactionData)) val webClient = urlByChain(chainType) return webClient.post() - .uri("/v3/98b672d2ce9a4089a3a5cb5081dde2fa") + .uri("/v3/$apiKey") .contentType(MediaType.APPLICATION_JSON) .bodyValue(requestBody) .retrieve() .bodyToMono(String::class.java) + } + fun getTransactionCount(chainType: ChainType,address: String) : Mono { + val requestBody = InfuraRequest(method = "eth_getTransactionCount", params = listOf(address,"latest")) + val webClient = urlByChain(chainType) + + return webClient.post() + .uri("/v3/$apiKey") + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(requestBody) + .retrieve() + .bodyToMono(String::class.java) } } \ No newline at end of file diff --git a/src/main/kotlin/com/api/admin/service/Web3jService.kt b/src/main/kotlin/com/api/admin/service/Web3jService.kt index ead20b5..7df05f4 100644 --- a/src/main/kotlin/com/api/admin/service/Web3jService.kt +++ b/src/main/kotlin/com/api/admin/service/Web3jService.kt @@ -1,17 +1,16 @@ package com.api.admin.service -import com.api.admin.enums.ChainType -import com.api.admin.wrapper.ERC20ABI import org.springframework.stereotype.Service +import org.web3j.abi.FunctionEncoder +import org.web3j.abi.datatypes.Address +import org.web3j.abi.datatypes.Function +import org.web3j.abi.datatypes.generated.Uint256 import org.web3j.crypto.Credentials import org.web3j.crypto.RawTransaction import org.web3j.crypto.TransactionEncoder import org.web3j.protocol.Web3j import org.web3j.protocol.core.DefaultBlockParameterName -import org.web3j.protocol.core.methods.response.EthSendTransaction import org.web3j.protocol.http.HttpService -import org.web3j.tx.ClientTransactionManager -import org.web3j.tx.gas.DefaultGasProvider import org.web3j.utils.Numeric import java.math.BigInteger @@ -20,33 +19,11 @@ class Web3jService( private val infuraApiService: InfuraApiService, ) { + private val apiKey = "98b672d2ce9a4089a3a5cb5081dde2fa" + private val privateKey = "e9769d3c00032a83d703e03630edbfc3cb634b40b92e38ab2890d5e37f21bb15" - fun sendMatic() { - val web3j = Web3j.build(HttpService("https://polygon-mainnet.infura.io/v3/98b672d2ce9a4089a3a5cb5081dde2fa")) - val credentials = Credentials.create("e9769d3c00032a83d703e03630edbfc3cb634b40b92e38ab2890d5e37f21bb15") - - val nonce = web3j.ethGetTransactionCount(credentials.address, DefaultBlockParameterName.LATEST) - .send().transactionCount - val gasPrice = web3j.ethGasPrice().send().gasPrice - val gasLimit = BigInteger.valueOf(21000) - val recipientAddress = "0x9bDeF468ae33b09b12a057B4c9211240D63BaE65" - val amount = BigInteger("1000000000000000000") - - val rawTransaction = RawTransaction.createEtherTransaction( - nonce, gasPrice, gasLimit, recipientAddress, amount - ) - - val signedMessage = TransactionEncoder.signMessage(rawTransaction, credentials) - val hexValue = Numeric.toHexString(signedMessage) - - val ethSendTransaction = web3j.ethSendRawTransaction(hexValue).send() - val transactionHash = ethSendTransaction.transactionHash - - println("Transaction hash: $transactionHash") - } - - fun createTransaction(privateKey:String,recipientAddress: String, amount: BigInteger): String{ - val web3j = Web3j.build(HttpService("https://polygon-mainnet.infura.io/v3/98b672d2ce9a4089a3a5cb5081dde2fa")) + fun createTransaction(privateKey:String,recipientAddress: String, amount: BigInteger): String { + val web3j = Web3j.build(HttpService("https://polygon-mainnet.infura.io/v3/$apiKey")) val credentials = Credentials.create(privateKey) return createTransactionData(web3j, credentials, recipientAddress, amount) @@ -56,7 +33,7 @@ class Web3jService( val nonce = web3j.ethGetTransactionCount(credentials.address, DefaultBlockParameterName.LATEST).send().transactionCount val gasPrice = web3j.ethGasPrice().send().gasPrice val gasLimit = BigInteger.valueOf(21000) - val chainId = 137L // 이더리움메인넷 1L + val chainId = 137L val rawTransaction = RawTransaction.createEtherTransaction( nonce, gasPrice, gasLimit, recipientAddress, amountInWei ) @@ -64,4 +41,30 @@ class Web3jService( val signedMessage = TransactionEncoder.signMessage(rawTransaction, chainId, credentials) return Numeric.toHexString(signedMessage) } + + fun createERC721TransactionData(web3j: Web3j, credentials: Credentials, contractAddress: String, fromAddress: String, toAddress: String, tokenId: BigInteger): String { + val nonce = web3j.ethGetTransactionCount(credentials.address, DefaultBlockParameterName.LATEST).send().transactionCount + val gasPrice = web3j.ethGasPrice().send().gasPrice + val gasLimit = BigInteger.valueOf(60000) + val chainId = 137L + + val function = Function( + "safeTransferFrom", + listOf( + Address(fromAddress), + Address(toAddress), + Uint256(tokenId) + ), + emptyList() + ) + + val encodedFunction = FunctionEncoder.encode(function) + + val rawTransaction = RawTransaction.createTransaction( + nonce, gasPrice, gasLimit, contractAddress, encodedFunction + ) + + val signedMessage = TransactionEncoder.signMessage(rawTransaction, chainId, credentials) + return Numeric.toHexString(signedMessage) + } } \ No newline at end of file diff --git a/src/main/kotlin/com/api/admin/wrapper/ERC721ABI.json b/src/main/kotlin/com/api/admin/wrapper/ERC721ABI.json new file mode 100644 index 0000000..8c9174b --- /dev/null +++ b/src/main/kotlin/com/api/admin/wrapper/ERC721ABI.json @@ -0,0 +1,296 @@ +[ + { + "constant": true, + "inputs": [], + "name": "name", + "outputs": [ + { + "name": "", + "type": "string" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_to", + "type": "address" + }, + { + "name": "_tokenId", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "symbol", + "outputs": [ + { + "name": "", + "type": "string" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_from", + "type": "address" + }, + { + "name": "_to", + "type": "address" + }, + { + "name": "_tokenId", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_owner", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "name": "balance", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_tokenId", + "type": "uint256" + } + ], + "name": "ownerOf", + "outputs": [ + { + "name": "owner", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_from", + "type": "address" + }, + { + "name": "_to", + "type": "address" + }, + { + "name": "_tokenId", + "type": "uint256" + } + ], + "name": "safeTransferFrom", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_from", + "type": "address" + }, + { + "name": "_to", + "type": "address" + }, + { + "name": "_tokenId", + "type": "uint256" + }, + { + "name": "_data", + "type": "bytes" + } + ], + "name": "safeTransferFrom", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_operator", + "type": "address" + }, + { + "name": "_approved", + "type": "bool" + } + ], + "name": "setApprovalForAll", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_owner", + "type": "address" + }, + { + "name": "_operator", + "type": "address" + } + ], + "name": "isApprovedForAll", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_tokenId", + "type": "uint256" + } + ], + "name": "getApproved", + "outputs": [ + { + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "from", + "type": "address" + }, + { + "indexed": true, + "name": "to", + "type": "address" + }, + { + "indexed": true, + "name": "tokenId", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "name": "approved", + "type": "address" + }, + { + "indexed": true, + "name": "tokenId", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "name": "operator", + "type": "address" + }, + { + "indexed": false, + "name": "approved", + "type": "bool" + } + ], + "name": "ApprovalForAll", + "type": "event" + } +] diff --git a/src/test/kotlin/com/api/admin/AdminServiceTest.kt b/src/test/kotlin/com/api/admin/AdminServiceTest.kt index 65a3e7a..646db1e 100644 --- a/src/test/kotlin/com/api/admin/AdminServiceTest.kt +++ b/src/test/kotlin/com/api/admin/AdminServiceTest.kt @@ -95,5 +95,11 @@ class AdminServiceTest( println(response) } + @Test + fun infuraTest() { + val res =infuraApiService.getTransactionCount(ChainType.POLYGON_MAINNET,"0x01b72b4aa3f66f213d62d53e829bc172a6a72867").block() + println(res.toString()) + } + } \ No newline at end of file From 6f890927b7c321fb5e238aec735969f4dbbe3307 Mon Sep 17 00:00:00 2001 From: min-96 Date: Mon, 27 May 2024 18:15:06 +0900 Subject: [PATCH 07/24] refactor: transferService --- src/main/kotlin/com/api/admin/NftResponse.kt | 3 +- .../com/api/admin/config/R2dbcConfig.kt | 30 +++++++ .../kotlin/com/api/admin/domain/nft/Nft.kt | 4 +- .../com/api/admin/domain/nft/NftRepository.kt | 2 +- .../com/api/admin/domain/transfer/Transfer.kt | 6 +- .../domain/transfer/TransferRepository.kt | 8 +- src/main/kotlin/com/api/admin/enums/Enum.kt | 9 +- .../event/dto/AdminTransferResponse.kt | 6 +- .../rabbitMQ/receiver/RabbitMQReceiver.kt | 2 + .../com/api/admin/service/InfuraApiService.kt | 8 +- .../com/api/admin/service/NftService.kt | 16 ++-- .../com/api/admin/service/TransferService.kt | 86 +++++++++---------- .../com/api/admin/service/Web3jService.kt | 43 ++++++++-- .../com/api/admin/util/ChainTypeConvert.kt | 12 +++ .../com/api/admin/util/StringToEnumConvert.kt | 11 +++ .../db/migration/V1__Initial_schema.sql | 25 +++++- .../kotlin/com/api/admin/AdminServiceTest.kt | 17 ++-- 17 files changed, 202 insertions(+), 86 deletions(-) create mode 100644 src/main/kotlin/com/api/admin/util/ChainTypeConvert.kt create mode 100644 src/main/kotlin/com/api/admin/util/StringToEnumConvert.kt diff --git a/src/main/kotlin/com/api/admin/NftResponse.kt b/src/main/kotlin/com/api/admin/NftResponse.kt index 412859c..34b2b00 100644 --- a/src/main/kotlin/com/api/admin/NftResponse.kt +++ b/src/main/kotlin/com/api/admin/NftResponse.kt @@ -1,12 +1,13 @@ package com.api.admin import com.api.admin.domain.nft.Nft +import com.api.admin.enums.ChainType data class NftResponse( val id : Long, val tokenId: String, val tokenAddress: String, - val chainType: String, + val chainType: ChainType, val nftName: String, val collectionName: String ){ diff --git a/src/main/kotlin/com/api/admin/config/R2dbcConfig.kt b/src/main/kotlin/com/api/admin/config/R2dbcConfig.kt index 96a8339..355510d 100644 --- a/src/main/kotlin/com/api/admin/config/R2dbcConfig.kt +++ b/src/main/kotlin/com/api/admin/config/R2dbcConfig.kt @@ -1,15 +1,26 @@ package com.api.admin.config +import com.api.admin.enums.AccountType +import com.api.admin.enums.ChainType +import com.api.admin.enums.TransferType +import com.api.admin.util.AccountTypeConvert +import com.api.admin.util.ChainTypeConvert +import com.api.admin.util.StringToEnumConverter +import com.api.admin.util.TransferTypeConvert import io.r2dbc.postgresql.PostgresqlConnectionFactory import io.r2dbc.postgresql.PostgresqlConnectionConfiguration +import io.r2dbc.postgresql.codec.EnumCodec import io.r2dbc.spi.ConnectionFactory import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration +import org.springframework.core.convert.converter.Converter import org.springframework.data.r2dbc.config.AbstractR2dbcConfiguration +import org.springframework.data.r2dbc.convert.R2dbcCustomConversions import org.springframework.data.r2dbc.core.R2dbcEntityTemplate import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories import org.springframework.r2dbc.connection.R2dbcTransactionManager import org.springframework.transaction.ReactiveTransactionManager +import java.util.ArrayList @Configuration @EnableR2dbcRepositories @@ -22,10 +33,29 @@ class R2dbcConfig : AbstractR2dbcConfiguration() { .database("admin") .username("admin") .password("admin") + .codecRegistrar( + EnumCodec.builder() + .withEnum("chain_type", ChainType::class.java) + .withEnum("transfer_type", TransferType::class.java) + .withEnum("account_type", AccountType::class.java) + .build() + ) .build() return PostgresqlConnectionFactory(configuration) } + @Bean + override fun r2dbcCustomConversions(): R2dbcCustomConversions { + val converters: MutableList?> = ArrayList?>() + converters.add(ChainTypeConvert(ChainType::class.java)) + converters.add(StringToEnumConverter(ChainType::class.java)) + converters.add(AccountTypeConvert(AccountType::class.java)) + converters.add(StringToEnumConverter(AccountType::class.java)) + converters.add(TransferTypeConvert(TransferType::class.java)) + converters.add(StringToEnumConverter(TransferType::class.java)) + return R2dbcCustomConversions(storeConversions, converters) + } + @Bean fun transactionManager(connectionFactory: ConnectionFactory?): ReactiveTransactionManager { return R2dbcTransactionManager(connectionFactory!!) diff --git a/src/main/kotlin/com/api/admin/domain/nft/Nft.kt b/src/main/kotlin/com/api/admin/domain/nft/Nft.kt index e3a8070..0a928b4 100644 --- a/src/main/kotlin/com/api/admin/domain/nft/Nft.kt +++ b/src/main/kotlin/com/api/admin/domain/nft/Nft.kt @@ -1,5 +1,7 @@ package com.api.admin.domain.nft +import com.api.admin.enums.ChainType +import org.springframework.boot.autoconfigure.web.WebProperties.Resources.Chain import org.springframework.data.annotation.Id import org.springframework.data.relational.core.mapping.Table @@ -8,7 +10,7 @@ class Nft( @Id val id : Long, val tokenId: String, val tokenAddress: String, - val chainType: String, + val chainType: ChainType, val nftName: String, val collectionName: String ) { diff --git a/src/main/kotlin/com/api/admin/domain/nft/NftRepository.kt b/src/main/kotlin/com/api/admin/domain/nft/NftRepository.kt index ce881ce..9bb9082 100644 --- a/src/main/kotlin/com/api/admin/domain/nft/NftRepository.kt +++ b/src/main/kotlin/com/api/admin/domain/nft/NftRepository.kt @@ -3,7 +3,7 @@ package com.api.admin.domain.nft import org.springframework.data.repository.reactive.ReactiveCrudRepository import reactor.core.publisher.Mono -interface NftRepository : ReactiveCrudRepository{ +interface NftRepository : ReactiveCrudRepository, NftRepositorySupport{ fun findByTokenAddressAndTokenId(address:String,tokenId:String): Mono } \ No newline at end of file diff --git a/src/main/kotlin/com/api/admin/domain/transfer/Transfer.kt b/src/main/kotlin/com/api/admin/domain/transfer/Transfer.kt index b631259..0d6c85d 100644 --- a/src/main/kotlin/com/api/admin/domain/transfer/Transfer.kt +++ b/src/main/kotlin/com/api/admin/domain/transfer/Transfer.kt @@ -1,5 +1,7 @@ package com.api.admin.domain.transfer +import com.api.admin.enums.AccountType +import com.api.admin.enums.TransferType import org.springframework.data.annotation.Id import org.springframework.data.relational.core.mapping.Table import java.math.BigDecimal @@ -10,9 +12,9 @@ data class Transfer( val nftId: Long?, val wallet: String, val timestamp: Long, - val accountType: String, + val accountType: AccountType, val balance: BigDecimal?, - val transferType: String, + val transferType: TransferType, val transactionHash: String, ) { diff --git a/src/main/kotlin/com/api/admin/domain/transfer/TransferRepository.kt b/src/main/kotlin/com/api/admin/domain/transfer/TransferRepository.kt index c19e5e1..16d947f 100644 --- a/src/main/kotlin/com/api/admin/domain/transfer/TransferRepository.kt +++ b/src/main/kotlin/com/api/admin/domain/transfer/TransferRepository.kt @@ -1,9 +1,13 @@ package com.api.admin.domain.transfer +import com.api.admin.enums.AccountType import org.springframework.data.repository.reactive.ReactiveCrudRepository import reactor.core.publisher.Mono interface TransferRepository : ReactiveCrudRepository { - fun findByWalletAndAccountTypeAndNftId(wallet:String, accountType:String,nftId:Long) : Mono - fun existsByWalletAndAccountTypeAndTransactionHashAndTimestampAfter(wallet: String,accountType: String,transactionHash: String,timestamp:Long) : Mono + fun findByWalletAndAccountTypeAndNftId(wallet:String, accountType:AccountType,nftId:Long) : Mono + fun existsByWalletAndAccountTypeAndTransactionHashAndTimestampAfter(wallet: String,accountType: AccountType,transactionHash: String,timestamp:Long) : Mono + + + fun existsByTransactionHash(transactionHash: String): Mono } \ No newline at end of file diff --git a/src/main/kotlin/com/api/admin/enums/Enum.kt b/src/main/kotlin/com/api/admin/enums/Enum.kt index 89dd2ab..916f98e 100644 --- a/src/main/kotlin/com/api/admin/enums/Enum.kt +++ b/src/main/kotlin/com/api/admin/enums/Enum.kt @@ -2,10 +2,13 @@ package com.api.admin.enums enum class ChainType{ ETHEREUM_MAINNET, + LINEA_MAINNET, + LINEA_SEPOLIA, POLYGON_MAINNET, - ETHREUM_GOERLI, - ETHREUM_SEPOLIA, - POLYGON_MUMBAI, + ETHEREUM_HOLESKY, + ETHEREUM_SEPOLIA, + POLYGON_AMOY, + } diff --git a/src/main/kotlin/com/api/admin/rabbitMQ/event/dto/AdminTransferResponse.kt b/src/main/kotlin/com/api/admin/rabbitMQ/event/dto/AdminTransferResponse.kt index 1c2d244..0e09c1c 100644 --- a/src/main/kotlin/com/api/admin/rabbitMQ/event/dto/AdminTransferResponse.kt +++ b/src/main/kotlin/com/api/admin/rabbitMQ/event/dto/AdminTransferResponse.kt @@ -1,5 +1,7 @@ package com.api.admin.rabbitMQ.event.dto +import com.api.admin.enums.AccountType +import com.api.admin.enums.TransferType import java.math.BigDecimal @@ -8,7 +10,7 @@ data class AdminTransferResponse( val walletAddress: String, val nftId: Long?, val timestamp: Long, - val accountType: String, - val transferType: String, + val accountType: AccountType, + val transferType: TransferType, val balance: BigDecimal?, ) diff --git a/src/main/kotlin/com/api/admin/rabbitMQ/receiver/RabbitMQReceiver.kt b/src/main/kotlin/com/api/admin/rabbitMQ/receiver/RabbitMQReceiver.kt index 9c06ead..1fba199 100644 --- a/src/main/kotlin/com/api/admin/rabbitMQ/receiver/RabbitMQReceiver.kt +++ b/src/main/kotlin/com/api/admin/rabbitMQ/receiver/RabbitMQReceiver.kt @@ -12,5 +12,7 @@ class RabbitMQReceiver( @RabbitListener(queues = ["nftQueue"]) fun nftMessage(nft: NftResponse) { nftService.save(nft) + .subscribe() + } } \ No newline at end of file diff --git a/src/main/kotlin/com/api/admin/service/InfuraApiService.kt b/src/main/kotlin/com/api/admin/service/InfuraApiService.kt index 6e9aa23..86ce91a 100644 --- a/src/main/kotlin/com/api/admin/service/InfuraApiService.kt +++ b/src/main/kotlin/com/api/admin/service/InfuraApiService.kt @@ -17,9 +17,11 @@ class InfuraApiService { val baseUrl = when (chainType) { ChainType.ETHEREUM_MAINNET -> "https://mainnet.infura.io" ChainType.POLYGON_MAINNET -> "https://polygon-mainnet.infura.io" - ChainType.ETHREUM_GOERLI -> "https://goerli.infura.io" - ChainType.ETHREUM_SEPOLIA -> "https://sepolia.infura.io" - ChainType.POLYGON_MUMBAI -> "https://polygon-mumbai.infura.io" + 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) diff --git a/src/main/kotlin/com/api/admin/service/NftService.kt b/src/main/kotlin/com/api/admin/service/NftService.kt index cb5b97f..695c3e3 100644 --- a/src/main/kotlin/com/api/admin/service/NftService.kt +++ b/src/main/kotlin/com/api/admin/service/NftService.kt @@ -4,15 +4,21 @@ import com.api.admin.NftResponse import com.api.admin.NftResponse.Companion.toEntity import com.api.admin.domain.nft.NftRepository import org.springframework.stereotype.Service +import reactor.core.publisher.Mono @Service class NftService( private val nftRepository: NftRepository, ) { - - fun save(response: NftResponse) { - nftRepository.findById(response.id).switchIfEmpty( - nftRepository.save(response.toEntity()) - ) + fun save(response: NftResponse): Mono { + return nftRepository.findById(response.id) + .flatMap { + Mono.empty() + } + .switchIfEmpty( + nftRepository.insert(response.toEntity()).then() + ) } + + } \ No newline at end of file diff --git a/src/main/kotlin/com/api/admin/service/TransferService.kt b/src/main/kotlin/com/api/admin/service/TransferService.kt index 6d21152..a9c1ffc 100644 --- a/src/main/kotlin/com/api/admin/service/TransferService.kt +++ b/src/main/kotlin/com/api/admin/service/TransferService.kt @@ -25,82 +25,77 @@ class TransferService( private val infuraApiService: InfuraApiService, ) { - private val adminAddress = "0x9bDeF468ae33b09b12a057B4c9211240D63BaE65" - private val transferEventSignature = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" + private val adminAddress = "0x9bDeF468ae33b09b12a057B4c9211240D63BaE65" + private val transferEventSignature = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" - - - fun deposit(wallet: String, chainType: ChainType, transactionHash: String): Mono { - return saveTransfer(wallet, chainType, transactionHash) - .doOnNext { transfer -> - eventPublisher.publishEvent(AdminTransferCreatedEvent(this, transfer.toResponse())) - } - .then() - } - fun saveTransfer(wallet: String, chainType: ChainType, transactionHash: String): Flux { - return infuraApiService.getNftTransfer(chainType, transactionHash) - .flatMapMany { response -> - Flux.fromIterable(response.result.logs) - .flatMap { it.toEntity(wallet, AccountType.DEPOSIT) } - } - .flatMap { transfer -> - checkTransferExistenceAndSave(transfer) - } - } - - private fun checkTransferExistenceAndSave(transfer: Transfer): Mono { - return transferRepository.findByWalletAndAccountTypeAndNftId(transfer.wallet, transfer.accountType, transfer.nftId!!) - .flatMap { existingTransfer -> - if (existingTransfer != null) { + fun getTransferData( + wallet: String, + chainType: ChainType, + transactionHash: String, + accountType: AccountType + ): Mono { + return transferRepository.existsByTransactionHash(transactionHash) + .flatMap { + if (it) { Mono.empty() } else { - transferRepository.existsByWalletAndAccountTypeAndTransactionHashAndTimestampAfter( - transfer.wallet, AccountType.WITHDRAW.toString(), transfer.transactionHash, transfer.timestamp - ).flatMap { withdrawalExists -> - if (withdrawalExists) { - Mono.empty() - } else { - transferRepository.save(transfer) + saveTransfer(wallet, chainType, transactionHash, AccountType.DEPOSIT) + .doOnNext { transfer -> + eventPublisher.publishEvent(AdminTransferCreatedEvent(this, transfer.toResponse())) } - } + .then() } } } + fun saveTransfer(wallet: String, chainType: ChainType, transactionHash: String, accountType: AccountType): Flux { + return infuraApiService.getNftTransfer(chainType, transactionHash) + .flatMapMany { response -> + Flux.fromIterable(response.result.logs) + .flatMap { it.toEntity(wallet, accountType) } + } + .flatMap { transfer -> transferRepository.save(transfer) } + } private fun Transfer.toResponse() = AdminTransferResponse( id = this.id!!, walletAddress = this.wallet, nftId = this.nftId, - timestamp =this.timestamp, + timestamp = this.timestamp, accountType = this.accountType, transferType = this.transferType, balance = this.balance ) - - fun InfuraTransferDetail.toEntity(wallet: String,accountType: AccountType): Mono { + fun InfuraTransferDetail.toEntity(wallet: String, accountType: AccountType): Mono { return Mono.just(this) .filter { it.topics.isNotEmpty() && it.topics[0] == transferEventSignature } - .filter { it.topics.size >= 3 && parseAddress(it.topics[2]) == adminAddress.lowercase() && parseAddress(it.topics[1]) == wallet.lowercase() } + .filter { + if (accountType == AccountType.DEPOSIT) { + it.topics.size >= 3 && parseAddress(it.topics[2]) == adminAddress.lowercase() && parseAddress(it.topics[1]) == wallet.lowercase() + } else { + it.topics.size >= 3 && parseAddress(it.topics[2]) == wallet.lowercase() && parseAddress(it.topics[1]) == adminAddress.lowercase() + } + } .flatMap { log -> val transferType = if (log.topics.size > 3) TransferType.ERC721 else TransferType.ERC20 when (transferType) { TransferType.ERC721 -> { val tokenId = BigInteger(log.topics[3].removePrefix("0x"), 16).toString() - nftRepository.findByTokenAddressAndTokenId(log.address, tokenId) // 없으면 nft서버가서 저장해와 ㅋ + nftRepository.findByTokenAddressAndTokenId(log.address, tokenId) .map { nft -> Transfer( id = null, nftId = nft.id, wallet = wallet, - timestamp = Instant.now().toEpochMilli(), - accountType = accountType.toString(), + timestamp = Instant.now().toEpochMilli(), + accountType = accountType, balance = null, - transferType = transferType.toString(), + transferType = transferType, transactionHash = log.transactionHash ) } } + else -> { val balance = toBigDecimal(log.data) Mono.just( @@ -108,10 +103,10 @@ class TransferService( id = null, nftId = null, wallet = wallet, - timestamp = Instant.now().toEpochMilli(), - accountType = accountType.toString(), + timestamp = Instant.now().toEpochMilli(), + accountType = accountType, balance = balance, - transferType = transferType.toString(), + transferType = transferType, transactionHash = log.transactionHash ) ) @@ -119,7 +114,6 @@ class TransferService( } } } - private fun parseAddress(address: String): String { return "0x" + address.substring(26).padStart(40, '0') } diff --git a/src/main/kotlin/com/api/admin/service/Web3jService.kt b/src/main/kotlin/com/api/admin/service/Web3jService.kt index 7df05f4..ba16558 100644 --- a/src/main/kotlin/com/api/admin/service/Web3jService.kt +++ b/src/main/kotlin/com/api/admin/service/Web3jService.kt @@ -1,5 +1,6 @@ package com.api.admin.service +import com.api.admin.enums.ChainType import org.springframework.stereotype.Service import org.web3j.abi.FunctionEncoder import org.web3j.abi.datatypes.Address @@ -22,18 +23,37 @@ class Web3jService( private val apiKey = "98b672d2ce9a4089a3a5cb5081dde2fa" private val privateKey = "e9769d3c00032a83d703e03630edbfc3cb634b40b92e38ab2890d5e37f21bb15" - fun createTransaction(privateKey:String,recipientAddress: String, amount: BigInteger): String { + + private fun getChainId(chain: ChainType): Long { + val chain = when (chain) { + ChainType.ETHEREUM_MAINNET -> 1L + ChainType.POLYGON_MAINNET -> 137L + ChainType.LINEA_MAINNET -> 59144L + ChainType.LINEA_SEPOLIA -> 59140L + ChainType.ETHEREUM_HOLESKY -> 1L + ChainType.ETHEREUM_SEPOLIA -> 11155111L + ChainType.POLYGON_AMOY -> 80002L + } + return chain + } + + fun createTransaction(privateKey:String,recipientAddress: String, amount: BigInteger,chainType: ChainType): String { val web3j = Web3j.build(HttpService("https://polygon-mainnet.infura.io/v3/$apiKey")) val credentials = Credentials.create(privateKey) - return createTransactionData(web3j, credentials, recipientAddress, amount) + return createERC20TransactionData(web3j, credentials, recipientAddress, amount,chainType) } - fun createTransactionData(web3j: Web3j, credentials: Credentials, recipientAddress: String, amountInWei: BigInteger): String { + fun createERC20TransactionData(web3j: Web3j, + credentials: Credentials, + recipientAddress: String, + amountInWei: BigInteger, + chainType: ChainType + ): String { val nonce = web3j.ethGetTransactionCount(credentials.address, DefaultBlockParameterName.LATEST).send().transactionCount val gasPrice = web3j.ethGasPrice().send().gasPrice - val gasLimit = BigInteger.valueOf(21000) - val chainId = 137L + val gasLimit = BigInteger.valueOf(15000) + val chainId = getChainId(chainType) val rawTransaction = RawTransaction.createEtherTransaction( nonce, gasPrice, gasLimit, recipientAddress, amountInWei ) @@ -42,11 +62,18 @@ class Web3jService( return Numeric.toHexString(signedMessage) } - fun createERC721TransactionData(web3j: Web3j, credentials: Credentials, contractAddress: String, fromAddress: String, toAddress: String, tokenId: BigInteger): String { + fun createERC721TransactionData(web3j: Web3j, + credentials: Credentials, + contractAddress: String, + fromAddress: String, + toAddress: String, + tokenId: BigInteger, + chainType: ChainType, + ): String { val nonce = web3j.ethGetTransactionCount(credentials.address, DefaultBlockParameterName.LATEST).send().transactionCount val gasPrice = web3j.ethGasPrice().send().gasPrice - val gasLimit = BigInteger.valueOf(60000) - val chainId = 137L + val gasLimit = BigInteger.valueOf(15000) + val chainId = getChainId(chainType) val function = Function( "safeTransferFrom", diff --git a/src/main/kotlin/com/api/admin/util/ChainTypeConvert.kt b/src/main/kotlin/com/api/admin/util/ChainTypeConvert.kt new file mode 100644 index 0000000..3bce21d --- /dev/null +++ b/src/main/kotlin/com/api/admin/util/ChainTypeConvert.kt @@ -0,0 +1,12 @@ +package com.api.admin.util + +import com.api.admin.enums.AccountType +import com.api.admin.enums.ChainType +import com.api.admin.enums.TransferType +import org.springframework.data.r2dbc.convert.EnumWriteSupport + +data class ChainTypeConvert>(private val enumType: Class): EnumWriteSupport() + +data class TransferTypeConvert>(private val enumType: Class): EnumWriteSupport() + +data class AccountTypeConvert>(private val enumType: Class): EnumWriteSupport() diff --git a/src/main/kotlin/com/api/admin/util/StringToEnumConvert.kt b/src/main/kotlin/com/api/admin/util/StringToEnumConvert.kt new file mode 100644 index 0000000..8591771 --- /dev/null +++ b/src/main/kotlin/com/api/admin/util/StringToEnumConvert.kt @@ -0,0 +1,11 @@ +package com.api.admin.util + +import org.springframework.core.convert.converter.Converter +import org.springframework.data.convert.ReadingConverter + +@ReadingConverter +class StringToEnumConverter>(private val enumType: Class) : Converter { + override fun convert(source: String): T { + return java.lang.Enum.valueOf(enumType, source) + } +} \ No newline at end of file diff --git a/src/main/resources/db/migration/V1__Initial_schema.sql b/src/main/resources/db/migration/V1__Initial_schema.sql index 707e9a3..5b3f461 100644 --- a/src/main/resources/db/migration/V1__Initial_schema.sql +++ b/src/main/resources/db/migration/V1__Initial_schema.sql @@ -1,8 +1,27 @@ +CREATE TYPE chain_type AS ENUM ( + 'ETHEREUM_MAINNET', + 'LINEA_MAINNET', + 'LINEA_SEPOLIA', + 'POLYGON_MAINNET', + 'ETHEREUM_HOLESKY', + 'ETHEREUM_SEPOLIA', + 'POLYGON_AMOY' + ); + +CREATE TYPE account_type AS ENUM ( + 'WITHDRAW', 'DEPOSIT' +); + +CREATE TYPE transfer_type AS ENUM ( + 'ERC20', 'ERC721' + ); + + CREATE TABLE IF NOT EXISTS nft ( id BIGINT PRIMARY KEY, token_id VARCHAR(255) NOT NULL, token_address VARCHAR(255) NOT NULL, - chain_type varchar(100) NOT NULL, + chain_type chain_type NOT NULL, nft_name varchar(255) NOT NULL, collection_name varchar(500) ); @@ -13,8 +32,8 @@ CREATE TABLE IF NOT EXISTS transfer ( wallet VARCHAR(255) NOT NULL, nft_id BIGINT REFERENCES nft(id), timestamp bigint not null, - account_type VARCHAR(255) NOT NULL, + account_type account_type NOT NULL, balance DECIMAL(19, 4), - transfer_type VARCHAR(255) NOT NULL, + transfer_type transfer_type NOT NULL, transaction_hash VARCHAR(255) NOT NULL ); diff --git a/src/test/kotlin/com/api/admin/AdminServiceTest.kt b/src/test/kotlin/com/api/admin/AdminServiceTest.kt index 646db1e..ab9090c 100644 --- a/src/test/kotlin/com/api/admin/AdminServiceTest.kt +++ b/src/test/kotlin/com/api/admin/AdminServiceTest.kt @@ -1,6 +1,5 @@ package com.api.admin -import com.api.admin.domain.transfer.Transfer import com.api.admin.enums.AccountType import com.api.admin.enums.ChainType import com.api.admin.enums.TransferType @@ -38,7 +37,7 @@ class AdminServiceTest( @Test fun saveTransfer() { - transferService.saveTransfer("0x01b72b4aa3f66f213d62d53e829bc172a6a72867",ChainType.POLYGON_MAINNET,"0x55fa4495f983e9f162b39b3df4dec8ebcff9aa05daee7b051c680ccfb49422a6").next().block() + transferService.saveTransfer("0x01b72b4aa3f66f213d62d53e829bc172a6a72867",ChainType.POLYGON_MAINNET,"0x55fa4495f983e9f162b39b3df4dec8ebcff9aa05daee7b051c680ccfb49422a6",AccountType.DEPOSIT).next().block() } @Test @@ -52,7 +51,7 @@ class AdminServiceTest( @Test fun deposit() { - transferService.deposit("0x01b72b4aa3f66f213d62d53e829bc172a6a72867",ChainType.POLYGON_MAINNET,"0x55fa4495f983e9f162b39b3df4dec8ebcff9aa05daee7b051c680ccfb49422a6") + transferService.getTransferData("0x01b72b4aa3f66f213d62d53e829bc172a6a72867",ChainType.POLYGON_MAINNET,"0x55fa4495f983e9f162b39b3df4dec8ebcff9aa05daee7b051c680ccfb49422a6",AccountType.DEPOSIT) .block() Thread.sleep(100000) @@ -69,8 +68,8 @@ class AdminServiceTest( walletAddress = "0x01b72b4aa3f66f213d62d53e829bc172a6a72867", nftId = 1L, timestamp = Instant.now().toEpochMilli(), - accountType = AccountType.DEPOSIT.toString(), - transferType = TransferType.ERC721.toString(), + accountType = AccountType.DEPOSIT, + transferType = TransferType.ERC721, balance = null ) rabbitMQSender.transferSend(response) @@ -87,10 +86,9 @@ class AdminServiceTest( val transactionData = web3jService.createTransaction("e9769d3c00032a83d703e03630edbfc3cb634b40b92e38ab2890d5e37f21bb15", "0x9bDeF468ae33b09b12a057B4c9211240D63BaE65", - BigInteger("1000000000000000000")) - - - + BigInteger("1000000000000000000"), + ChainType.POLYGON_MAINNET + ) val response = infuraApiService.getSend(ChainType.POLYGON_MAINNET,transactionData).block() println(response) } @@ -102,4 +100,5 @@ class AdminServiceTest( } + } \ No newline at end of file From 70111823dce1c2c80679e35b43ee369a4f0488c8 Mon Sep 17 00:00:00 2001 From: min-96 Date: Wed, 29 May 2024 21:41:53 +0900 Subject: [PATCH 08/24] feat: add ABI --- Web3App/build.gradle | 96 +++++ Web3App/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 55616 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 + Web3App/gradlew | 170 +++++++++ Web3App/gradlew.bat | 84 +++++ Web3App/settings.gradle | 10 + Web3App/src/main/java/org/web3j/Web3App.java | 31 ++ Web3App/src/main/solidity/HelloWorld.sol | 47 +++ logs | 17 + .../java/com/api/admin/wrapper/ERC20ABI.java | 259 ++++++++++++++ .../java/com/api/admin/wrapper/ERC721ABI.java | 337 ++++++++++++++++++ 11 files changed, 1056 insertions(+) create mode 100644 Web3App/build.gradle create mode 100644 Web3App/gradle/wrapper/gradle-wrapper.jar create mode 100644 Web3App/gradle/wrapper/gradle-wrapper.properties create mode 100755 Web3App/gradlew create mode 100644 Web3App/gradlew.bat create mode 100644 Web3App/settings.gradle create mode 100644 Web3App/src/main/java/org/web3j/Web3App.java create mode 100644 Web3App/src/main/solidity/HelloWorld.sol create mode 100644 logs create mode 100644 src/main/java/com/api/admin/wrapper/ERC20ABI.java create mode 100644 src/main/java/com/api/admin/wrapper/ERC721ABI.java diff --git a/Web3App/build.gradle b/Web3App/build.gradle new file mode 100644 index 0000000..2555420 --- /dev/null +++ b/Web3App/build.gradle @@ -0,0 +1,96 @@ +plugins { + id 'java' + id 'org.jetbrains.kotlin.jvm' version '1.8.10' + id 'application' + id "com.github.johnrengelman.shadow" version "5.2.0" + id 'org.web3j' version '4.11.2' +} + + +group 'org.web3j' +version '0.1.0' + +sourceCompatibility = 17 + +repositories { + mavenLocal() + mavenCentral() + maven { url "https://hyperledger.jfrog.io/hyperledger/besu-maven" } + maven { url "https://artifacts.consensys.net/public/maven/maven/" } + maven { url "https://splunk.jfrog.io/splunk/ext-releases-local" } +} + +web3j { + generatedPackageName = 'org.web3j.generated.contracts' + excludedContracts = ['Mortal'] +} + +node { + nodeProjectDir.set(file("$projectDir")) +} + +ext { + web3jVersion = '4.11.2' + logbackVersion = '1.4.14' + klaxonVersion = '5.5' + besuPluginVersion = '24.1.1' + besuInternalVersion = '24.1.1' + besuInternalCryptoVersion = '23.1.3' + besuCryptoDepVersion = '0.8.3' +} + +dependencies { + implementation "org.web3j:core:$web3jVersion", + "ch.qos.logback:logback-core:$logbackVersion", + "ch.qos.logback:logback-classic:$logbackVersion", + "com.beust:klaxon:$klaxonVersion" + implementation "org.web3j:web3j-unit:$web3jVersion" + implementation "org.web3j:web3j-evm:$web3jVersion" + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.3.1' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.3.1' + + implementation "org.hyperledger.besu:plugin-api:$besuPluginVersion" + implementation "org.hyperledger.besu.internal:besu:$besuInternalVersion" + implementation "org.hyperledger.besu.internal:api:$besuInternalVersion" + implementation "org.hyperledger.besu:evm:$besuPluginVersion" + implementation "org.hyperledger.besu.internal:config:$besuInternalVersion" + implementation "org.hyperledger.besu.internal:core:$besuInternalVersion" + implementation "org.hyperledger.besu.internal:crypto:$besuInternalCryptoVersion" + implementation "org.hyperledger.besu.internal:rlp:$besuInternalVersion" + implementation "org.hyperledger.besu.internal:kvstore:$besuInternalVersion" + implementation "org.hyperledger.besu.internal:metrics-core:$besuInternalVersion" + implementation "org.hyperledger.besu.internal:trie:$besuInternalVersion" + implementation "org.hyperledger.besu.internal:util:$besuInternalVersion" + implementation "org.hyperledger.besu:bls12-381:$besuCryptoDepVersion" + implementation "org.hyperledger.besu:secp256k1:$besuCryptoDepVersion" +} + +jar { + manifest { + attributes( + 'Main-Class': 'org.web3j.Web3App', + 'Multi-Release':'true' + ) + } +} + +application { + mainClassName = 'org.web3j.Web3App' +} + +test { + useJUnitPlatform() +} + +compileKotlin { + kotlinOptions.jvmTarget = "17" +} + +compileTestKotlin { + kotlinOptions.jvmTarget = "17" +} + +shadowJar { + zip64 = true +} diff --git a/Web3App/gradle/wrapper/gradle-wrapper.jar b/Web3App/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..5c2d1cf016b3885f6930543d57b744ea8c220a1a GIT binary patch literal 55616 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 \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/Web3App/gradlew.bat b/Web3App/gradlew.bat new file mode 100644 index 0000000..f955316 --- /dev/null +++ b/Web3App/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/Web3App/settings.gradle b/Web3App/settings.gradle new file mode 100644 index 0000000..a88c03a --- /dev/null +++ b/Web3App/settings.gradle @@ -0,0 +1,10 @@ +pluginManagement { + repositories { + mavenLocal() + gradlePluginPortal() + maven { url "https://hyperledger.jfrog.io/hyperledger/besu-maven" } + maven { url "https://artifacts.consensys.net/public/maven/maven/" } + maven { url "https://splunk.jfrog.io/splunk/ext-releases-local" } + } +} +rootProject.name = 'Web3App'; diff --git a/Web3App/src/main/java/org/web3j/Web3App.java b/Web3App/src/main/java/org/web3j/Web3App.java new file mode 100644 index 0000000..73a9270 --- /dev/null +++ b/Web3App/src/main/java/org/web3j/Web3App.java @@ -0,0 +1,31 @@ +package org.web3j; + +import org.web3j.crypto.Credentials; +import org.web3j.crypto.WalletUtils; +import org.web3j.protocol.Web3j; +import org.web3j.protocol.http.HttpService; +import org.web3j.tx.gas.DefaultGasProvider; + +import org.web3j.generated.contracts.HelloWorld; + +/** + *

This is the generated class for web3j new helloworld

+ *

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

+ *

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

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

Auto generated code. + *

Do not modify! + *

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

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

(true) {}, new TypeReference
(true) {}, new TypeReference() {})); + ; + + public static final Event TRANSFER_EVENT = new Event("Transfer", + Arrays.>asList(new TypeReference
(true) {}, new TypeReference
(true) {}, new TypeReference() {})); + ; + + @Deprecated + protected ERC20ABI(String contractAddress, Web3j web3j, Credentials credentials, BigInteger gasPrice, BigInteger gasLimit) { + super(BINARY, contractAddress, web3j, credentials, gasPrice, gasLimit); + } + + protected ERC20ABI(String contractAddress, Web3j web3j, Credentials credentials, ContractGasProvider contractGasProvider) { + super(BINARY, contractAddress, web3j, credentials, contractGasProvider); + } + + @Deprecated + protected ERC20ABI(String contractAddress, Web3j web3j, TransactionManager transactionManager, BigInteger gasPrice, BigInteger gasLimit) { + super(BINARY, contractAddress, web3j, transactionManager, gasPrice, gasLimit); + } + + protected ERC20ABI(String contractAddress, Web3j web3j, TransactionManager transactionManager, ContractGasProvider contractGasProvider) { + super(BINARY, contractAddress, web3j, transactionManager, contractGasProvider); + } + + public RemoteFunctionCall name() { + final Function function = new Function(FUNC_NAME, + Arrays.asList(), + Arrays.>asList(new TypeReference() {})); + return executeRemoteCallSingleValueReturn(function, String.class); + } + + public RemoteFunctionCall approve(String _spender, BigInteger _value) { + final Function function = new Function( + FUNC_APPROVE, + Arrays.asList(new org.web3j.abi.datatypes.Address(160, _spender), + new org.web3j.abi.datatypes.generated.Uint256(_value)), + Collections.>emptyList()); + return executeRemoteCallTransaction(function); + } + + public RemoteFunctionCall totalSupply() { + final Function function = new Function(FUNC_TOTALSUPPLY, + Arrays.asList(), + Arrays.>asList(new TypeReference() {})); + return executeRemoteCallSingleValueReturn(function, BigInteger.class); + } + + public RemoteFunctionCall decimals() { + final Function function = new Function(FUNC_DECIMALS, + Arrays.asList(), + Arrays.>asList(new TypeReference() {})); + return executeRemoteCallSingleValueReturn(function, BigInteger.class); + } + + public RemoteFunctionCall symbol() { + final Function function = new Function(FUNC_SYMBOL, + Arrays.asList(), + Arrays.>asList(new TypeReference() {})); + return executeRemoteCallSingleValueReturn(function, String.class); + } + + public RemoteFunctionCall transfer(String _to, BigInteger _value) { + final Function function = new Function( + FUNC_TRANSFER, + Arrays.asList(new org.web3j.abi.datatypes.Address(160, _to), + new org.web3j.abi.datatypes.generated.Uint256(_value)), + Collections.>emptyList()); + return executeRemoteCallTransaction(function); + } + + public RemoteFunctionCall balanceOf(String _owner) { + final Function function = new Function(FUNC_BALANCEOF, + Arrays.asList(new org.web3j.abi.datatypes.Address(160, _owner)), + Arrays.>asList(new TypeReference() {})); + return executeRemoteCallSingleValueReturn(function, BigInteger.class); + } + + public RemoteFunctionCall transferFrom(String _from, String _to, BigInteger _value) { + final Function function = new Function( + FUNC_TRANSFERFROM, + Arrays.asList(new org.web3j.abi.datatypes.Address(160, _from), + new org.web3j.abi.datatypes.Address(160, _to), + new org.web3j.abi.datatypes.generated.Uint256(_value)), + Collections.>emptyList()); + return executeRemoteCallTransaction(function); + } + + public RemoteFunctionCall allowance(String _owner, String _spender) { + final Function function = new Function(FUNC_ALLOWANCE, + Arrays.asList(new org.web3j.abi.datatypes.Address(160, _owner), + new org.web3j.abi.datatypes.Address(160, _spender)), + Arrays.>asList(new TypeReference() {})); + return executeRemoteCallSingleValueReturn(function, BigInteger.class); + } + + public static List getApprovalEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(APPROVAL_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (Contract.EventValuesWithLog eventValues : valueList) { + ApprovalEventResponse typedResponse = new ApprovalEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.owner = (String) eventValues.getIndexedValues().get(0).getValue(); + typedResponse.spender = (String) eventValues.getIndexedValues().get(1).getValue(); + typedResponse.value = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); + responses.add(typedResponse); + } + return responses; + } + + public static ApprovalEventResponse getApprovalEventFromLog(Log log) { + Contract.EventValuesWithLog eventValues = staticExtractEventParametersWithLog(APPROVAL_EVENT, log); + ApprovalEventResponse typedResponse = new ApprovalEventResponse(); + typedResponse.log = log; + typedResponse.owner = (String) eventValues.getIndexedValues().get(0).getValue(); + typedResponse.spender = (String) eventValues.getIndexedValues().get(1).getValue(); + typedResponse.value = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); + return typedResponse; + } + + public Flowable approvalEventFlowable(EthFilter filter) { + return web3j.ethLogFlowable(filter).map(log -> getApprovalEventFromLog(log)); + } + + public Flowable approvalEventFlowable(DefaultBlockParameter startBlock, DefaultBlockParameter endBlock) { + EthFilter filter = new EthFilter(startBlock, endBlock, getContractAddress()); + filter.addSingleTopic(EventEncoder.encode(APPROVAL_EVENT)); + return approvalEventFlowable(filter); + } + + public static List getTransferEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(TRANSFER_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (Contract.EventValuesWithLog eventValues : valueList) { + TransferEventResponse typedResponse = new TransferEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.from = (String) eventValues.getIndexedValues().get(0).getValue(); + typedResponse.to = (String) eventValues.getIndexedValues().get(1).getValue(); + typedResponse.value = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); + responses.add(typedResponse); + } + return responses; + } + + public static TransferEventResponse getTransferEventFromLog(Log log) { + Contract.EventValuesWithLog eventValues = staticExtractEventParametersWithLog(TRANSFER_EVENT, log); + TransferEventResponse typedResponse = new TransferEventResponse(); + typedResponse.log = log; + typedResponse.from = (String) eventValues.getIndexedValues().get(0).getValue(); + typedResponse.to = (String) eventValues.getIndexedValues().get(1).getValue(); + typedResponse.value = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); + return typedResponse; + } + + public Flowable transferEventFlowable(EthFilter filter) { + return web3j.ethLogFlowable(filter).map(log -> getTransferEventFromLog(log)); + } + + public Flowable transferEventFlowable(DefaultBlockParameter startBlock, DefaultBlockParameter endBlock) { + EthFilter filter = new EthFilter(startBlock, endBlock, getContractAddress()); + filter.addSingleTopic(EventEncoder.encode(TRANSFER_EVENT)); + return transferEventFlowable(filter); + } + + @Deprecated + public static ERC20ABI load(String contractAddress, Web3j web3j, Credentials credentials, BigInteger gasPrice, BigInteger gasLimit) { + return new ERC20ABI(contractAddress, web3j, credentials, gasPrice, gasLimit); + } + + @Deprecated + public static ERC20ABI load(String contractAddress, Web3j web3j, TransactionManager transactionManager, BigInteger gasPrice, BigInteger gasLimit) { + return new ERC20ABI(contractAddress, web3j, transactionManager, gasPrice, gasLimit); + } + + public static ERC20ABI load(String contractAddress, Web3j web3j, Credentials credentials, ContractGasProvider contractGasProvider) { + return new ERC20ABI(contractAddress, web3j, credentials, contractGasProvider); + } + + public static ERC20ABI load(String contractAddress, Web3j web3j, TransactionManager transactionManager, ContractGasProvider contractGasProvider) { + return new ERC20ABI(contractAddress, web3j, transactionManager, contractGasProvider); + } + + public static class ApprovalEventResponse extends BaseEventResponse { + public String owner; + + public String spender; + + public BigInteger value; + } + + public static class TransferEventResponse extends BaseEventResponse { + public String from; + + public String to; + + public BigInteger value; + } +} diff --git a/src/main/java/com/api/admin/wrapper/ERC721ABI.java b/src/main/java/com/api/admin/wrapper/ERC721ABI.java new file mode 100644 index 0000000..4fd0850 --- /dev/null +++ b/src/main/java/com/api/admin/wrapper/ERC721ABI.java @@ -0,0 +1,337 @@ +package com.api.admin.wrapper; + +import io.reactivex.Flowable; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.web3j.abi.EventEncoder; +import org.web3j.abi.TypeReference; +import org.web3j.abi.datatypes.Address; +import org.web3j.abi.datatypes.Bool; +import org.web3j.abi.datatypes.Event; +import org.web3j.abi.datatypes.Function; +import org.web3j.abi.datatypes.Type; +import org.web3j.abi.datatypes.Utf8String; +import org.web3j.abi.datatypes.generated.Uint256; +import org.web3j.crypto.Credentials; +import org.web3j.protocol.Web3j; +import org.web3j.protocol.core.DefaultBlockParameter; +import org.web3j.protocol.core.RemoteFunctionCall; +import org.web3j.protocol.core.methods.request.EthFilter; +import org.web3j.protocol.core.methods.response.BaseEventResponse; +import org.web3j.protocol.core.methods.response.Log; +import org.web3j.protocol.core.methods.response.TransactionReceipt; +import org.web3j.tx.Contract; +import org.web3j.tx.TransactionManager; +import org.web3j.tx.gas.ContractGasProvider; + +/** + *

Auto generated code. + *

Do not modify! + *

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

Generated with web3j version 1.5.3. + */ +@SuppressWarnings("rawtypes") +public class ERC721ABI extends Contract { + public static final String BINARY = "Bin file was not provided"; + + public static final String FUNC_NAME = "name"; + + public static final String FUNC_APPROVE = "approve"; + + public static final String FUNC_TOTALSUPPLY = "totalSupply"; + + public static final String FUNC_SYMBOL = "symbol"; + + public static final String FUNC_TRANSFERFROM = "transferFrom"; + + public static final String FUNC_BALANCEOF = "balanceOf"; + + public static final String FUNC_OWNEROF = "ownerOf"; + + public static final String FUNC_safeTransferFrom = "safeTransferFrom"; + + public static final String FUNC_SETAPPROVALFORALL = "setApprovalForAll"; + + public static final String FUNC_ISAPPROVEDFORALL = "isApprovedForAll"; + + public static final String FUNC_GETAPPROVED = "getApproved"; + + public static final Event TRANSFER_EVENT = new Event("Transfer", + Arrays.>asList(new TypeReference

(true) {}, new TypeReference
(true) {}, new TypeReference(true) {})); + ; + + public static final Event APPROVAL_EVENT = new Event("Approval", + Arrays.>asList(new TypeReference
(true) {}, new TypeReference
(true) {}, new TypeReference(true) {})); + ; + + public static final Event APPROVALFORALL_EVENT = new Event("ApprovalForAll", + Arrays.>asList(new TypeReference
(true) {}, new TypeReference
(true) {}, new TypeReference() {})); + ; + + @Deprecated + protected ERC721ABI(String contractAddress, Web3j web3j, Credentials credentials, BigInteger gasPrice, BigInteger gasLimit) { + super(BINARY, contractAddress, web3j, credentials, gasPrice, gasLimit); + } + + protected ERC721ABI(String contractAddress, Web3j web3j, Credentials credentials, ContractGasProvider contractGasProvider) { + super(BINARY, contractAddress, web3j, credentials, contractGasProvider); + } + + @Deprecated + protected ERC721ABI(String contractAddress, Web3j web3j, TransactionManager transactionManager, BigInteger gasPrice, BigInteger gasLimit) { + super(BINARY, contractAddress, web3j, transactionManager, gasPrice, gasLimit); + } + + protected ERC721ABI(String contractAddress, Web3j web3j, TransactionManager transactionManager, ContractGasProvider contractGasProvider) { + super(BINARY, contractAddress, web3j, transactionManager, contractGasProvider); + } + + public RemoteFunctionCall name() { + final Function function = new Function(FUNC_NAME, + Arrays.asList(), + Arrays.>asList(new TypeReference() {})); + return executeRemoteCallSingleValueReturn(function, String.class); + } + + public RemoteFunctionCall approve(String _to, BigInteger _tokenId) { + final Function function = new Function( + FUNC_APPROVE, + Arrays.asList(new org.web3j.abi.datatypes.Address(160, _to), + new org.web3j.abi.datatypes.generated.Uint256(_tokenId)), + Collections.>emptyList()); + return executeRemoteCallTransaction(function); + } + + public RemoteFunctionCall totalSupply() { + final Function function = new Function(FUNC_TOTALSUPPLY, + Arrays.asList(), + Arrays.>asList(new TypeReference() {})); + return executeRemoteCallSingleValueReturn(function, BigInteger.class); + } + + public RemoteFunctionCall symbol() { + final Function function = new Function(FUNC_SYMBOL, + Arrays.asList(), + Arrays.>asList(new TypeReference() {})); + return executeRemoteCallSingleValueReturn(function, String.class); + } + + public RemoteFunctionCall transferFrom(String _from, String _to, BigInteger _tokenId) { + final Function function = new Function( + FUNC_TRANSFERFROM, + Arrays.asList(new org.web3j.abi.datatypes.Address(160, _from), + new org.web3j.abi.datatypes.Address(160, _to), + new org.web3j.abi.datatypes.generated.Uint256(_tokenId)), + Collections.>emptyList()); + return executeRemoteCallTransaction(function); + } + + public RemoteFunctionCall balanceOf(String _owner) { + final Function function = new Function(FUNC_BALANCEOF, + Arrays.asList(new org.web3j.abi.datatypes.Address(160, _owner)), + Arrays.>asList(new TypeReference() {})); + return executeRemoteCallSingleValueReturn(function, BigInteger.class); + } + + public RemoteFunctionCall ownerOf(BigInteger _tokenId) { + final Function function = new Function(FUNC_OWNEROF, + Arrays.asList(new org.web3j.abi.datatypes.generated.Uint256(_tokenId)), + Arrays.>asList(new TypeReference
() {})); + return executeRemoteCallSingleValueReturn(function, String.class); + } + + public RemoteFunctionCall safeTransferFrom(String _from, String _to, BigInteger _tokenId) { + final Function function = new Function( + FUNC_safeTransferFrom, + Arrays.asList(new org.web3j.abi.datatypes.Address(160, _from), + new org.web3j.abi.datatypes.Address(160, _to), + new org.web3j.abi.datatypes.generated.Uint256(_tokenId)), + Collections.>emptyList()); + return executeRemoteCallTransaction(function); + } + + public RemoteFunctionCall safeTransferFrom(String _from, String _to, BigInteger _tokenId, byte[] _data) { + final Function function = new Function( + FUNC_safeTransferFrom, + Arrays.asList(new org.web3j.abi.datatypes.Address(160, _from), + new org.web3j.abi.datatypes.Address(160, _to), + new org.web3j.abi.datatypes.generated.Uint256(_tokenId), + new org.web3j.abi.datatypes.DynamicBytes(_data)), + Collections.>emptyList()); + return executeRemoteCallTransaction(function); + } + + public RemoteFunctionCall setApprovalForAll(String _operator, Boolean _approved) { + final Function function = new Function( + FUNC_SETAPPROVALFORALL, + Arrays.asList(new org.web3j.abi.datatypes.Address(160, _operator), + new org.web3j.abi.datatypes.Bool(_approved)), + Collections.>emptyList()); + return executeRemoteCallTransaction(function); + } + + public RemoteFunctionCall isApprovedForAll(String _owner, String _operator) { + final Function function = new Function(FUNC_ISAPPROVEDFORALL, + Arrays.asList(new org.web3j.abi.datatypes.Address(160, _owner), + new org.web3j.abi.datatypes.Address(160, _operator)), + Arrays.>asList(new TypeReference() {})); + return executeRemoteCallSingleValueReturn(function, Boolean.class); + } + + public RemoteFunctionCall getApproved(BigInteger _tokenId) { + final Function function = new Function(FUNC_GETAPPROVED, + Arrays.asList(new org.web3j.abi.datatypes.generated.Uint256(_tokenId)), + Arrays.>asList(new TypeReference
() {})); + return executeRemoteCallSingleValueReturn(function, String.class); + } + + public static List getTransferEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(TRANSFER_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (Contract.EventValuesWithLog eventValues : valueList) { + TransferEventResponse typedResponse = new TransferEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.from = (String) eventValues.getIndexedValues().get(0).getValue(); + typedResponse.to = (String) eventValues.getIndexedValues().get(1).getValue(); + typedResponse.tokenId = (BigInteger) eventValues.getIndexedValues().get(2).getValue(); + responses.add(typedResponse); + } + return responses; + } + + public static TransferEventResponse getTransferEventFromLog(Log log) { + Contract.EventValuesWithLog eventValues = staticExtractEventParametersWithLog(TRANSFER_EVENT, log); + TransferEventResponse typedResponse = new TransferEventResponse(); + typedResponse.log = log; + typedResponse.from = (String) eventValues.getIndexedValues().get(0).getValue(); + typedResponse.to = (String) eventValues.getIndexedValues().get(1).getValue(); + typedResponse.tokenId = (BigInteger) eventValues.getIndexedValues().get(2).getValue(); + return typedResponse; + } + + public Flowable transferEventFlowable(EthFilter filter) { + return web3j.ethLogFlowable(filter).map(log -> getTransferEventFromLog(log)); + } + + public Flowable transferEventFlowable(DefaultBlockParameter startBlock, DefaultBlockParameter endBlock) { + EthFilter filter = new EthFilter(startBlock, endBlock, getContractAddress()); + filter.addSingleTopic(EventEncoder.encode(TRANSFER_EVENT)); + return transferEventFlowable(filter); + } + + public static List getApprovalEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(APPROVAL_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (Contract.EventValuesWithLog eventValues : valueList) { + ApprovalEventResponse typedResponse = new ApprovalEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.owner = (String) eventValues.getIndexedValues().get(0).getValue(); + typedResponse.approved = (String) eventValues.getIndexedValues().get(1).getValue(); + typedResponse.tokenId = (BigInteger) eventValues.getIndexedValues().get(2).getValue(); + responses.add(typedResponse); + } + return responses; + } + + public static ApprovalEventResponse getApprovalEventFromLog(Log log) { + Contract.EventValuesWithLog eventValues = staticExtractEventParametersWithLog(APPROVAL_EVENT, log); + ApprovalEventResponse typedResponse = new ApprovalEventResponse(); + typedResponse.log = log; + typedResponse.owner = (String) eventValues.getIndexedValues().get(0).getValue(); + typedResponse.approved = (String) eventValues.getIndexedValues().get(1).getValue(); + typedResponse.tokenId = (BigInteger) eventValues.getIndexedValues().get(2).getValue(); + return typedResponse; + } + + public Flowable approvalEventFlowable(EthFilter filter) { + return web3j.ethLogFlowable(filter).map(log -> getApprovalEventFromLog(log)); + } + + public Flowable approvalEventFlowable(DefaultBlockParameter startBlock, DefaultBlockParameter endBlock) { + EthFilter filter = new EthFilter(startBlock, endBlock, getContractAddress()); + filter.addSingleTopic(EventEncoder.encode(APPROVAL_EVENT)); + return approvalEventFlowable(filter); + } + + public static List getApprovalForAllEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(APPROVALFORALL_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (Contract.EventValuesWithLog eventValues : valueList) { + ApprovalForAllEventResponse typedResponse = new ApprovalForAllEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.owner = (String) eventValues.getIndexedValues().get(0).getValue(); + typedResponse.operator = (String) eventValues.getIndexedValues().get(1).getValue(); + typedResponse.approved = (Boolean) eventValues.getNonIndexedValues().get(0).getValue(); + responses.add(typedResponse); + } + return responses; + } + + public static ApprovalForAllEventResponse getApprovalForAllEventFromLog(Log log) { + Contract.EventValuesWithLog eventValues = staticExtractEventParametersWithLog(APPROVALFORALL_EVENT, log); + ApprovalForAllEventResponse typedResponse = new ApprovalForAllEventResponse(); + typedResponse.log = log; + typedResponse.owner = (String) eventValues.getIndexedValues().get(0).getValue(); + typedResponse.operator = (String) eventValues.getIndexedValues().get(1).getValue(); + typedResponse.approved = (Boolean) eventValues.getNonIndexedValues().get(0).getValue(); + return typedResponse; + } + + public Flowable approvalForAllEventFlowable(EthFilter filter) { + return web3j.ethLogFlowable(filter).map(log -> getApprovalForAllEventFromLog(log)); + } + + public Flowable approvalForAllEventFlowable(DefaultBlockParameter startBlock, DefaultBlockParameter endBlock) { + EthFilter filter = new EthFilter(startBlock, endBlock, getContractAddress()); + filter.addSingleTopic(EventEncoder.encode(APPROVALFORALL_EVENT)); + return approvalForAllEventFlowable(filter); + } + + @Deprecated + public static ERC721ABI load(String contractAddress, Web3j web3j, Credentials credentials, BigInteger gasPrice, BigInteger gasLimit) { + return new ERC721ABI(contractAddress, web3j, credentials, gasPrice, gasLimit); + } + + @Deprecated + public static ERC721ABI load(String contractAddress, Web3j web3j, TransactionManager transactionManager, BigInteger gasPrice, BigInteger gasLimit) { + return new ERC721ABI(contractAddress, web3j, transactionManager, gasPrice, gasLimit); + } + + public static ERC721ABI load(String contractAddress, Web3j web3j, Credentials credentials, ContractGasProvider contractGasProvider) { + return new ERC721ABI(contractAddress, web3j, credentials, contractGasProvider); + } + + public static ERC721ABI load(String contractAddress, Web3j web3j, TransactionManager transactionManager, ContractGasProvider contractGasProvider) { + return new ERC721ABI(contractAddress, web3j, transactionManager, contractGasProvider); + } + + public static class TransferEventResponse extends BaseEventResponse { + public String from; + + public String to; + + public BigInteger tokenId; + } + + public static class ApprovalEventResponse extends BaseEventResponse { + public String owner; + + public String approved; + + public BigInteger tokenId; + } + + public static class ApprovalForAllEventResponse extends BaseEventResponse { + public String owner; + + public String operator; + + public Boolean approved; + } +} From a243759910e9cdaa0a3cf62b27d22de6e7b661d1 Mon Sep 17 00:00:00 2001 From: min-96 Date: Tue, 4 Jun 2024 21:11:31 +0900 Subject: [PATCH 09/24] refactor: nftResponse --- src/main/kotlin/com/api/admin/NftResponse.kt | 4 ---- src/main/kotlin/com/api/admin/domain/nft/Nft.kt | 3 --- src/main/resources/db/migration/V1__Initial_schema.sql | 4 +--- 3 files changed, 1 insertion(+), 10 deletions(-) diff --git a/src/main/kotlin/com/api/admin/NftResponse.kt b/src/main/kotlin/com/api/admin/NftResponse.kt index 34b2b00..5d47aa9 100644 --- a/src/main/kotlin/com/api/admin/NftResponse.kt +++ b/src/main/kotlin/com/api/admin/NftResponse.kt @@ -8,8 +8,6 @@ data class NftResponse( val tokenId: String, val tokenAddress: String, val chainType: ChainType, - val nftName: String, - val collectionName: String ){ companion object{ fun NftResponse.toEntity() = Nft( @@ -17,8 +15,6 @@ data class NftResponse( tokenId = this.tokenId, tokenAddress = this.tokenAddress, chainType = this.chainType, - nftName = this.nftName, - collectionName = this.collectionName ) } } diff --git a/src/main/kotlin/com/api/admin/domain/nft/Nft.kt b/src/main/kotlin/com/api/admin/domain/nft/Nft.kt index 0a928b4..54b1c65 100644 --- a/src/main/kotlin/com/api/admin/domain/nft/Nft.kt +++ b/src/main/kotlin/com/api/admin/domain/nft/Nft.kt @@ -1,7 +1,6 @@ package com.api.admin.domain.nft import com.api.admin.enums.ChainType -import org.springframework.boot.autoconfigure.web.WebProperties.Resources.Chain import org.springframework.data.annotation.Id import org.springframework.data.relational.core.mapping.Table @@ -11,7 +10,5 @@ class Nft( val tokenId: String, val tokenAddress: String, val chainType: ChainType, - val nftName: String, - val collectionName: String ) { } \ No newline at end of file diff --git a/src/main/resources/db/migration/V1__Initial_schema.sql b/src/main/resources/db/migration/V1__Initial_schema.sql index 5b3f461..067ed37 100644 --- a/src/main/resources/db/migration/V1__Initial_schema.sql +++ b/src/main/resources/db/migration/V1__Initial_schema.sql @@ -21,9 +21,7 @@ CREATE TABLE IF NOT EXISTS nft ( id BIGINT PRIMARY KEY, token_id VARCHAR(255) NOT NULL, token_address VARCHAR(255) NOT NULL, - chain_type chain_type NOT NULL, - nft_name varchar(255) NOT NULL, - collection_name varchar(500) + chain_type chain_type NOT NULL ); From 4551ff58446aed87b575d9877a8b4d44d6a2f9c6 Mon Sep 17 00:00:00 2001 From: min-96 Date: Tue, 11 Jun 2024 01:15:04 +0900 Subject: [PATCH 10/24] fix: add chainType of transfer --- .../com/api/admin/domain/transfer/Transfer.kt | 2 ++ .../rabbitMQ/event/dto/AdminTransferResponse.kt | 2 ++ .../kotlin/com/api/admin/service/TransferService.kt | 13 ++++++++----- .../resources/db/migration/V1__Initial_schema.sql | 3 ++- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/com/api/admin/domain/transfer/Transfer.kt b/src/main/kotlin/com/api/admin/domain/transfer/Transfer.kt index 0d6c85d..428e72e 100644 --- a/src/main/kotlin/com/api/admin/domain/transfer/Transfer.kt +++ b/src/main/kotlin/com/api/admin/domain/transfer/Transfer.kt @@ -1,6 +1,7 @@ package com.api.admin.domain.transfer import com.api.admin.enums.AccountType +import com.api.admin.enums.ChainType import com.api.admin.enums.TransferType import org.springframework.data.annotation.Id import org.springframework.data.relational.core.mapping.Table @@ -16,6 +17,7 @@ data class Transfer( val balance: BigDecimal?, val transferType: TransferType, val transactionHash: String, + val chainType: ChainType, ) { } \ No newline at end of file diff --git a/src/main/kotlin/com/api/admin/rabbitMQ/event/dto/AdminTransferResponse.kt b/src/main/kotlin/com/api/admin/rabbitMQ/event/dto/AdminTransferResponse.kt index 0e09c1c..5dc9251 100644 --- a/src/main/kotlin/com/api/admin/rabbitMQ/event/dto/AdminTransferResponse.kt +++ b/src/main/kotlin/com/api/admin/rabbitMQ/event/dto/AdminTransferResponse.kt @@ -1,6 +1,7 @@ package com.api.admin.rabbitMQ.event.dto import com.api.admin.enums.AccountType +import com.api.admin.enums.ChainType import com.api.admin.enums.TransferType import java.math.BigDecimal @@ -13,4 +14,5 @@ data class AdminTransferResponse( val accountType: AccountType, val transferType: TransferType, val balance: BigDecimal?, + val chainType: ChainType, ) diff --git a/src/main/kotlin/com/api/admin/service/TransferService.kt b/src/main/kotlin/com/api/admin/service/TransferService.kt index a9c1ffc..d5cea1f 100644 --- a/src/main/kotlin/com/api/admin/service/TransferService.kt +++ b/src/main/kotlin/com/api/admin/service/TransferService.kt @@ -52,7 +52,7 @@ class TransferService( return infuraApiService.getNftTransfer(chainType, transactionHash) .flatMapMany { response -> Flux.fromIterable(response.result.logs) - .flatMap { it.toEntity(wallet, accountType) } + .flatMap { it.toEntity(wallet, accountType,chainType) } } .flatMap { transfer -> transferRepository.save(transfer) } } @@ -63,10 +63,11 @@ class TransferService( timestamp = this.timestamp, accountType = this.accountType, transferType = this.transferType, - balance = this.balance + balance = this.balance, + chainType = this.chainType, ) - fun InfuraTransferDetail.toEntity(wallet: String, accountType: AccountType): Mono { + fun InfuraTransferDetail.toEntity(wallet: String, accountType: AccountType,chainType: ChainType): Mono { return Mono.just(this) .filter { it.topics.isNotEmpty() && it.topics[0] == transferEventSignature } .filter { @@ -91,7 +92,8 @@ class TransferService( accountType = accountType, balance = null, transferType = transferType, - transactionHash = log.transactionHash + transactionHash = log.transactionHash, + chainType = chainType, ) } } @@ -107,7 +109,8 @@ class TransferService( accountType = accountType, balance = balance, transferType = transferType, - transactionHash = log.transactionHash + transactionHash = log.transactionHash, + chainType = chainType, ) ) } diff --git a/src/main/resources/db/migration/V1__Initial_schema.sql b/src/main/resources/db/migration/V1__Initial_schema.sql index 067ed37..295062e 100644 --- a/src/main/resources/db/migration/V1__Initial_schema.sql +++ b/src/main/resources/db/migration/V1__Initial_schema.sql @@ -33,5 +33,6 @@ CREATE TABLE IF NOT EXISTS transfer ( account_type account_type NOT NULL, balance DECIMAL(19, 4), transfer_type transfer_type NOT NULL, - transaction_hash VARCHAR(255) NOT NULL + transaction_hash VARCHAR(255) NOT NULL, + chain_type chain_type ); From 055d00bac429d1a324ea031b30afdffd79034965 Mon Sep 17 00:00:00 2001 From: min-96 Date: Fri, 28 Jun 2024 17:29:37 +0900 Subject: [PATCH 11/24] refactor: nftResponse --- .../kotlin/com/api/admin/domain/nft/Nft.kt | 2 +- .../rabbitMQ/receiver/RabbitMQReceiver.kt | 2 +- .../com/api/admin/service/NftService.kt | 4 +- .../com/api/admin/service/Web3jService.kt | 40 ++- .../admin/{ => service/dto}/NftResponse.kt | 2 +- .../com/api/admin/wrapper/ERC1155ABI.json | 314 ++++++++++++++++++ .../kotlin/com/api/admin/AdminServiceTest.kt | 36 +- 7 files changed, 392 insertions(+), 8 deletions(-) rename src/main/kotlin/com/api/admin/{ => service/dto}/NftResponse.kt (92%) create mode 100644 src/main/kotlin/com/api/admin/wrapper/ERC1155ABI.json diff --git a/src/main/kotlin/com/api/admin/domain/nft/Nft.kt b/src/main/kotlin/com/api/admin/domain/nft/Nft.kt index 54b1c65..6d2d674 100644 --- a/src/main/kotlin/com/api/admin/domain/nft/Nft.kt +++ b/src/main/kotlin/com/api/admin/domain/nft/Nft.kt @@ -5,7 +5,7 @@ import org.springframework.data.annotation.Id import org.springframework.data.relational.core.mapping.Table @Table("nft") -class Nft( +data class Nft( @Id val id : Long, val tokenId: String, val tokenAddress: String, diff --git a/src/main/kotlin/com/api/admin/rabbitMQ/receiver/RabbitMQReceiver.kt b/src/main/kotlin/com/api/admin/rabbitMQ/receiver/RabbitMQReceiver.kt index 1fba199..3796aec 100644 --- a/src/main/kotlin/com/api/admin/rabbitMQ/receiver/RabbitMQReceiver.kt +++ b/src/main/kotlin/com/api/admin/rabbitMQ/receiver/RabbitMQReceiver.kt @@ -1,6 +1,6 @@ package com.api.admin.rabbitMQ.receiver -import com.api.admin.NftResponse +import com.api.admin.service.dto.NftResponse import com.api.admin.service.NftService import org.springframework.amqp.rabbit.annotation.RabbitListener import org.springframework.stereotype.Service diff --git a/src/main/kotlin/com/api/admin/service/NftService.kt b/src/main/kotlin/com/api/admin/service/NftService.kt index 695c3e3..9d851a5 100644 --- a/src/main/kotlin/com/api/admin/service/NftService.kt +++ b/src/main/kotlin/com/api/admin/service/NftService.kt @@ -1,7 +1,7 @@ package com.api.admin.service -import com.api.admin.NftResponse -import com.api.admin.NftResponse.Companion.toEntity +import com.api.admin.service.dto.NftResponse +import com.api.admin.service.dto.NftResponse.Companion.toEntity import com.api.admin.domain.nft.NftRepository import org.springframework.stereotype.Service import reactor.core.publisher.Mono diff --git a/src/main/kotlin/com/api/admin/service/Web3jService.kt b/src/main/kotlin/com/api/admin/service/Web3jService.kt index ba16558..ab01f94 100644 --- a/src/main/kotlin/com/api/admin/service/Web3jService.kt +++ b/src/main/kotlin/com/api/admin/service/Web3jService.kt @@ -5,6 +5,7 @@ import org.springframework.stereotype.Service import org.web3j.abi.FunctionEncoder import org.web3j.abi.datatypes.Address import org.web3j.abi.datatypes.Function +import org.web3j.abi.datatypes.Utf8String import org.web3j.abi.datatypes.generated.Uint256 import org.web3j.crypto.Credentials import org.web3j.crypto.RawTransaction @@ -22,8 +23,6 @@ class Web3jService( private val apiKey = "98b672d2ce9a4089a3a5cb5081dde2fa" private val privateKey = "e9769d3c00032a83d703e03630edbfc3cb634b40b92e38ab2890d5e37f21bb15" - - private fun getChainId(chain: ChainType): Long { val chain = when (chain) { ChainType.ETHEREUM_MAINNET -> 1L @@ -94,4 +93,41 @@ class Web3jService( val signedMessage = TransactionEncoder.signMessage(rawTransaction, chainId, credentials) return Numeric.toHexString(signedMessage) } + + fun createERC1155TransactionData( + web3j: Web3j, + credentials: Credentials, + contractAddress: String, + fromAddress: String, + toAddress: String, + tokenId: BigInteger, + amount: BigInteger, + chainType: ChainType + ): String { + val nonce = web3j.ethGetTransactionCount(credentials.address, DefaultBlockParameterName.LATEST).send().transactionCount + val gasPrice = web3j.ethGasPrice().send().gasPrice + val gasLimit = BigInteger.valueOf(200000) + val chainId = getChainId(chainType) + + val function = Function( + "safeTransferFrom", + listOf( + Address(credentials.address), + Address(toAddress), + Uint256(tokenId), + Uint256(amount), + Utf8String("") // empty data + ), + emptyList() + ) + + val encodedFunction = FunctionEncoder.encode(function) + + val rawTransaction = RawTransaction.createTransaction( + nonce, gasPrice, gasLimit, contractAddress, encodedFunction + ) + + val signedMessage = TransactionEncoder.signMessage(rawTransaction, chainId, credentials) + return Numeric.toHexString(signedMessage) + } } \ No newline at end of file diff --git a/src/main/kotlin/com/api/admin/NftResponse.kt b/src/main/kotlin/com/api/admin/service/dto/NftResponse.kt similarity index 92% rename from src/main/kotlin/com/api/admin/NftResponse.kt rename to src/main/kotlin/com/api/admin/service/dto/NftResponse.kt index 5d47aa9..ac25b25 100644 --- a/src/main/kotlin/com/api/admin/NftResponse.kt +++ b/src/main/kotlin/com/api/admin/service/dto/NftResponse.kt @@ -1,4 +1,4 @@ -package com.api.admin +package com.api.admin.service.dto import com.api.admin.domain.nft.Nft import com.api.admin.enums.ChainType diff --git a/src/main/kotlin/com/api/admin/wrapper/ERC1155ABI.json b/src/main/kotlin/com/api/admin/wrapper/ERC1155ABI.json new file mode 100644 index 0000000..211a562 --- /dev/null +++ b/src/main/kotlin/com/api/admin/wrapper/ERC1155ABI.json @@ -0,0 +1,314 @@ +[ + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "indexed": false, + "internalType": "bool", + "name": "approved", + "type": "bool" + } + ], + "name": "ApprovalForAll", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256[]", + "name": "ids", + "type": "uint256[]" + }, + { + "indexed": false, + "internalType": "uint256[]", + "name": "values", + "type": "uint256[]" + } + ], + "name": "TransferBatch", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "TransferSingle", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "string", + "name": "value", + "type": "string" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "name": "URI", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "accounts", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "ids", + "type": "uint256[]" + } + ], + "name": "balanceOfBatch", + "outputs": [ + { + "internalType": "uint256[]", + "name": "", + "type": "uint256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "address", + "name": "operator", + "type": "address" + } + ], + "name": "isApprovedForAll", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256[]", + "name": "ids", + "type": "uint256[]" + }, + { + "internalType": "uint256[]", + "name": "amounts", + "type": "uint256[]" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "safeBatchTransferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "safeTransferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "internalType": "bool", + "name": "approved", + "type": "bool" + } + ], + "name": "setApprovalForAll", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "interfaceId", + "type": "bytes4" + } + ], + "name": "supportsInterface", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "name": "uri", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + } +] \ No newline at end of file diff --git a/src/test/kotlin/com/api/admin/AdminServiceTest.kt b/src/test/kotlin/com/api/admin/AdminServiceTest.kt index ab9090c..7213a74 100644 --- a/src/test/kotlin/com/api/admin/AdminServiceTest.kt +++ b/src/test/kotlin/com/api/admin/AdminServiceTest.kt @@ -11,6 +11,9 @@ import com.api.admin.service.Web3jService import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest +import org.web3j.crypto.Credentials +import org.web3j.protocol.Web3j +import org.web3j.protocol.http.HttpService import java.math.BigInteger import java.time.Instant @@ -70,7 +73,8 @@ class AdminServiceTest( timestamp = Instant.now().toEpochMilli(), accountType = AccountType.DEPOSIT, transferType = TransferType.ERC721, - balance = null + balance = null, + chainType = ChainType.POLYGON_MAINNET ) rabbitMQSender.transferSend(response) Thread.sleep(10000) @@ -100,5 +104,35 @@ class AdminServiceTest( } + @Test + fun erc1155Test() { + // val apiKey = "98b672d2ce9a4089a3a5cb5081dde2fa" + // val privateKey = "e9769d3c00032a83d703e03630edbfc3cb634b40b92e38ab2890d5e37f21bb15" + // val web3j = Web3j.build(HttpService("https://polygon-mainnet.infura.io/v3/$apiKey")) + // val credentials = Credentials.create(privateKey) + // + // val contractAddress = "0xe7900239E9332060dC975ED6F0cc5F0129D924cf" + // val fromAddress = "0x01b72b4aa3f66f213d62d53e829bc172a6a72867" + // val toAddress = "0x9bDeF468ae33b09b12a057B4c9211240D63BaE65" + // val tokenId = BigInteger("5") + // val amount =BigInteger("1") + // val chainType = ChainType.POLYGON_MAINNET + // val res = web3jService.createERC1155TransactionData( + // web3j, + // credentials, + // contractAddress, + // fromAddress, + // toAddress, + // tokenId, + // amount, + // chainType + // ) + // + // println("transaction DAta : " + res) + val res = "0xf9012c1d8506fc23ac2983030d4094e7900239e9332060dc975ed6f0cc5f0129d924cf80b8c4de6c65ff00000000000000000000000001b72b4aa3f66f213d62d53e829bc172a6a728670000000000000000000000009bdef468ae33b09b12a057b4c9211240d63bae650000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000820136a0c09f6b44990413c3a81950b014780bd9b2c480f65319922959aeb38df13f2105a014b6764cc0cf8baa3ffb7d82930a1887aa2b014cb4d77bf846e04084583ce138" + val response = infuraApiService.getSend(ChainType.POLYGON_MAINNET,res).block() + println(response) + } + } \ No newline at end of file From b01048a7103069d32beff82cff3c65840fa26288 Mon Sep 17 00:00:00 2001 From: min-96 Date: Tue, 2 Jul 2024 00:44:05 +0900 Subject: [PATCH 12/24] fix: sendTransaction logic --- .../java/com/api/admin/wrapper/ERC721ABI.java | 337 ------------------ .../com/api/admin/domain/nft/NftRepository.kt | 2 + src/main/kotlin/com/api/admin/enums/Enum.kt | 10 +- .../com/api/admin/service/InfuraApiService.kt | 27 +- .../com/api/admin/service/NftApiService.kt | 30 ++ .../com/api/admin/service/NftService.kt | 38 +- .../com/api/admin/service/TransferService.kt | 133 ++++--- .../com/api/admin/service/Web3jService.kt | 160 ++++----- .../api/admin/service/dto/InfuraResponse.kt | 3 + .../com/api/admin/service/dto/NftRequest.kt | 9 + .../com/api/admin/wrapper/ERC721ABI.json | 277 +++++++------- .../kotlin/com/api/admin/AdminServiceTest.kt | 53 ++- 12 files changed, 425 insertions(+), 654 deletions(-) delete mode 100644 src/main/java/com/api/admin/wrapper/ERC721ABI.java create mode 100644 src/main/kotlin/com/api/admin/service/NftApiService.kt create mode 100644 src/main/kotlin/com/api/admin/service/dto/InfuraResponse.kt create mode 100644 src/main/kotlin/com/api/admin/service/dto/NftRequest.kt diff --git a/src/main/java/com/api/admin/wrapper/ERC721ABI.java b/src/main/java/com/api/admin/wrapper/ERC721ABI.java deleted file mode 100644 index 4fd0850..0000000 --- a/src/main/java/com/api/admin/wrapper/ERC721ABI.java +++ /dev/null @@ -1,337 +0,0 @@ -package com.api.admin.wrapper; - -import io.reactivex.Flowable; -import java.math.BigInteger; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import org.web3j.abi.EventEncoder; -import org.web3j.abi.TypeReference; -import org.web3j.abi.datatypes.Address; -import org.web3j.abi.datatypes.Bool; -import org.web3j.abi.datatypes.Event; -import org.web3j.abi.datatypes.Function; -import org.web3j.abi.datatypes.Type; -import org.web3j.abi.datatypes.Utf8String; -import org.web3j.abi.datatypes.generated.Uint256; -import org.web3j.crypto.Credentials; -import org.web3j.protocol.Web3j; -import org.web3j.protocol.core.DefaultBlockParameter; -import org.web3j.protocol.core.RemoteFunctionCall; -import org.web3j.protocol.core.methods.request.EthFilter; -import org.web3j.protocol.core.methods.response.BaseEventResponse; -import org.web3j.protocol.core.methods.response.Log; -import org.web3j.protocol.core.methods.response.TransactionReceipt; -import org.web3j.tx.Contract; -import org.web3j.tx.TransactionManager; -import org.web3j.tx.gas.ContractGasProvider; - -/** - *

Auto generated code. - *

Do not modify! - *

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

Generated with web3j version 1.5.3. - */ -@SuppressWarnings("rawtypes") -public class ERC721ABI extends Contract { - public static final String BINARY = "Bin file was not provided"; - - public static final String FUNC_NAME = "name"; - - public static final String FUNC_APPROVE = "approve"; - - public static final String FUNC_TOTALSUPPLY = "totalSupply"; - - public static final String FUNC_SYMBOL = "symbol"; - - public static final String FUNC_TRANSFERFROM = "transferFrom"; - - public static final String FUNC_BALANCEOF = "balanceOf"; - - public static final String FUNC_OWNEROF = "ownerOf"; - - public static final String FUNC_safeTransferFrom = "safeTransferFrom"; - - public static final String FUNC_SETAPPROVALFORALL = "setApprovalForAll"; - - public static final String FUNC_ISAPPROVEDFORALL = "isApprovedForAll"; - - public static final String FUNC_GETAPPROVED = "getApproved"; - - public static final Event TRANSFER_EVENT = new Event("Transfer", - Arrays.>asList(new TypeReference

(true) {}, new TypeReference
(true) {}, new TypeReference(true) {})); - ; - - public static final Event APPROVAL_EVENT = new Event("Approval", - Arrays.>asList(new TypeReference
(true) {}, new TypeReference
(true) {}, new TypeReference(true) {})); - ; - - public static final Event APPROVALFORALL_EVENT = new Event("ApprovalForAll", - Arrays.>asList(new TypeReference
(true) {}, new TypeReference
(true) {}, new TypeReference() {})); - ; - - @Deprecated - protected ERC721ABI(String contractAddress, Web3j web3j, Credentials credentials, BigInteger gasPrice, BigInteger gasLimit) { - super(BINARY, contractAddress, web3j, credentials, gasPrice, gasLimit); - } - - protected ERC721ABI(String contractAddress, Web3j web3j, Credentials credentials, ContractGasProvider contractGasProvider) { - super(BINARY, contractAddress, web3j, credentials, contractGasProvider); - } - - @Deprecated - protected ERC721ABI(String contractAddress, Web3j web3j, TransactionManager transactionManager, BigInteger gasPrice, BigInteger gasLimit) { - super(BINARY, contractAddress, web3j, transactionManager, gasPrice, gasLimit); - } - - protected ERC721ABI(String contractAddress, Web3j web3j, TransactionManager transactionManager, ContractGasProvider contractGasProvider) { - super(BINARY, contractAddress, web3j, transactionManager, contractGasProvider); - } - - public RemoteFunctionCall name() { - final Function function = new Function(FUNC_NAME, - Arrays.asList(), - Arrays.>asList(new TypeReference() {})); - return executeRemoteCallSingleValueReturn(function, String.class); - } - - public RemoteFunctionCall approve(String _to, BigInteger _tokenId) { - final Function function = new Function( - FUNC_APPROVE, - Arrays.asList(new org.web3j.abi.datatypes.Address(160, _to), - new org.web3j.abi.datatypes.generated.Uint256(_tokenId)), - Collections.>emptyList()); - return executeRemoteCallTransaction(function); - } - - public RemoteFunctionCall totalSupply() { - final Function function = new Function(FUNC_TOTALSUPPLY, - Arrays.asList(), - Arrays.>asList(new TypeReference() {})); - return executeRemoteCallSingleValueReturn(function, BigInteger.class); - } - - public RemoteFunctionCall symbol() { - final Function function = new Function(FUNC_SYMBOL, - Arrays.asList(), - Arrays.>asList(new TypeReference() {})); - return executeRemoteCallSingleValueReturn(function, String.class); - } - - public RemoteFunctionCall transferFrom(String _from, String _to, BigInteger _tokenId) { - final Function function = new Function( - FUNC_TRANSFERFROM, - Arrays.asList(new org.web3j.abi.datatypes.Address(160, _from), - new org.web3j.abi.datatypes.Address(160, _to), - new org.web3j.abi.datatypes.generated.Uint256(_tokenId)), - Collections.>emptyList()); - return executeRemoteCallTransaction(function); - } - - public RemoteFunctionCall balanceOf(String _owner) { - final Function function = new Function(FUNC_BALANCEOF, - Arrays.asList(new org.web3j.abi.datatypes.Address(160, _owner)), - Arrays.>asList(new TypeReference() {})); - return executeRemoteCallSingleValueReturn(function, BigInteger.class); - } - - public RemoteFunctionCall ownerOf(BigInteger _tokenId) { - final Function function = new Function(FUNC_OWNEROF, - Arrays.asList(new org.web3j.abi.datatypes.generated.Uint256(_tokenId)), - Arrays.>asList(new TypeReference
() {})); - return executeRemoteCallSingleValueReturn(function, String.class); - } - - public RemoteFunctionCall safeTransferFrom(String _from, String _to, BigInteger _tokenId) { - final Function function = new Function( - FUNC_safeTransferFrom, - Arrays.asList(new org.web3j.abi.datatypes.Address(160, _from), - new org.web3j.abi.datatypes.Address(160, _to), - new org.web3j.abi.datatypes.generated.Uint256(_tokenId)), - Collections.>emptyList()); - return executeRemoteCallTransaction(function); - } - - public RemoteFunctionCall safeTransferFrom(String _from, String _to, BigInteger _tokenId, byte[] _data) { - final Function function = new Function( - FUNC_safeTransferFrom, - Arrays.asList(new org.web3j.abi.datatypes.Address(160, _from), - new org.web3j.abi.datatypes.Address(160, _to), - new org.web3j.abi.datatypes.generated.Uint256(_tokenId), - new org.web3j.abi.datatypes.DynamicBytes(_data)), - Collections.>emptyList()); - return executeRemoteCallTransaction(function); - } - - public RemoteFunctionCall setApprovalForAll(String _operator, Boolean _approved) { - final Function function = new Function( - FUNC_SETAPPROVALFORALL, - Arrays.asList(new org.web3j.abi.datatypes.Address(160, _operator), - new org.web3j.abi.datatypes.Bool(_approved)), - Collections.>emptyList()); - return executeRemoteCallTransaction(function); - } - - public RemoteFunctionCall isApprovedForAll(String _owner, String _operator) { - final Function function = new Function(FUNC_ISAPPROVEDFORALL, - Arrays.asList(new org.web3j.abi.datatypes.Address(160, _owner), - new org.web3j.abi.datatypes.Address(160, _operator)), - Arrays.>asList(new TypeReference() {})); - return executeRemoteCallSingleValueReturn(function, Boolean.class); - } - - public RemoteFunctionCall getApproved(BigInteger _tokenId) { - final Function function = new Function(FUNC_GETAPPROVED, - Arrays.asList(new org.web3j.abi.datatypes.generated.Uint256(_tokenId)), - Arrays.>asList(new TypeReference
() {})); - return executeRemoteCallSingleValueReturn(function, String.class); - } - - public static List getTransferEvents(TransactionReceipt transactionReceipt) { - List valueList = staticExtractEventParametersWithLog(TRANSFER_EVENT, transactionReceipt); - ArrayList responses = new ArrayList(valueList.size()); - for (Contract.EventValuesWithLog eventValues : valueList) { - TransferEventResponse typedResponse = new TransferEventResponse(); - typedResponse.log = eventValues.getLog(); - typedResponse.from = (String) eventValues.getIndexedValues().get(0).getValue(); - typedResponse.to = (String) eventValues.getIndexedValues().get(1).getValue(); - typedResponse.tokenId = (BigInteger) eventValues.getIndexedValues().get(2).getValue(); - responses.add(typedResponse); - } - return responses; - } - - public static TransferEventResponse getTransferEventFromLog(Log log) { - Contract.EventValuesWithLog eventValues = staticExtractEventParametersWithLog(TRANSFER_EVENT, log); - TransferEventResponse typedResponse = new TransferEventResponse(); - typedResponse.log = log; - typedResponse.from = (String) eventValues.getIndexedValues().get(0).getValue(); - typedResponse.to = (String) eventValues.getIndexedValues().get(1).getValue(); - typedResponse.tokenId = (BigInteger) eventValues.getIndexedValues().get(2).getValue(); - return typedResponse; - } - - public Flowable transferEventFlowable(EthFilter filter) { - return web3j.ethLogFlowable(filter).map(log -> getTransferEventFromLog(log)); - } - - public Flowable transferEventFlowable(DefaultBlockParameter startBlock, DefaultBlockParameter endBlock) { - EthFilter filter = new EthFilter(startBlock, endBlock, getContractAddress()); - filter.addSingleTopic(EventEncoder.encode(TRANSFER_EVENT)); - return transferEventFlowable(filter); - } - - public static List getApprovalEvents(TransactionReceipt transactionReceipt) { - List valueList = staticExtractEventParametersWithLog(APPROVAL_EVENT, transactionReceipt); - ArrayList responses = new ArrayList(valueList.size()); - for (Contract.EventValuesWithLog eventValues : valueList) { - ApprovalEventResponse typedResponse = new ApprovalEventResponse(); - typedResponse.log = eventValues.getLog(); - typedResponse.owner = (String) eventValues.getIndexedValues().get(0).getValue(); - typedResponse.approved = (String) eventValues.getIndexedValues().get(1).getValue(); - typedResponse.tokenId = (BigInteger) eventValues.getIndexedValues().get(2).getValue(); - responses.add(typedResponse); - } - return responses; - } - - public static ApprovalEventResponse getApprovalEventFromLog(Log log) { - Contract.EventValuesWithLog eventValues = staticExtractEventParametersWithLog(APPROVAL_EVENT, log); - ApprovalEventResponse typedResponse = new ApprovalEventResponse(); - typedResponse.log = log; - typedResponse.owner = (String) eventValues.getIndexedValues().get(0).getValue(); - typedResponse.approved = (String) eventValues.getIndexedValues().get(1).getValue(); - typedResponse.tokenId = (BigInteger) eventValues.getIndexedValues().get(2).getValue(); - return typedResponse; - } - - public Flowable approvalEventFlowable(EthFilter filter) { - return web3j.ethLogFlowable(filter).map(log -> getApprovalEventFromLog(log)); - } - - public Flowable approvalEventFlowable(DefaultBlockParameter startBlock, DefaultBlockParameter endBlock) { - EthFilter filter = new EthFilter(startBlock, endBlock, getContractAddress()); - filter.addSingleTopic(EventEncoder.encode(APPROVAL_EVENT)); - return approvalEventFlowable(filter); - } - - public static List getApprovalForAllEvents(TransactionReceipt transactionReceipt) { - List valueList = staticExtractEventParametersWithLog(APPROVALFORALL_EVENT, transactionReceipt); - ArrayList responses = new ArrayList(valueList.size()); - for (Contract.EventValuesWithLog eventValues : valueList) { - ApprovalForAllEventResponse typedResponse = new ApprovalForAllEventResponse(); - typedResponse.log = eventValues.getLog(); - typedResponse.owner = (String) eventValues.getIndexedValues().get(0).getValue(); - typedResponse.operator = (String) eventValues.getIndexedValues().get(1).getValue(); - typedResponse.approved = (Boolean) eventValues.getNonIndexedValues().get(0).getValue(); - responses.add(typedResponse); - } - return responses; - } - - public static ApprovalForAllEventResponse getApprovalForAllEventFromLog(Log log) { - Contract.EventValuesWithLog eventValues = staticExtractEventParametersWithLog(APPROVALFORALL_EVENT, log); - ApprovalForAllEventResponse typedResponse = new ApprovalForAllEventResponse(); - typedResponse.log = log; - typedResponse.owner = (String) eventValues.getIndexedValues().get(0).getValue(); - typedResponse.operator = (String) eventValues.getIndexedValues().get(1).getValue(); - typedResponse.approved = (Boolean) eventValues.getNonIndexedValues().get(0).getValue(); - return typedResponse; - } - - public Flowable approvalForAllEventFlowable(EthFilter filter) { - return web3j.ethLogFlowable(filter).map(log -> getApprovalForAllEventFromLog(log)); - } - - public Flowable approvalForAllEventFlowable(DefaultBlockParameter startBlock, DefaultBlockParameter endBlock) { - EthFilter filter = new EthFilter(startBlock, endBlock, getContractAddress()); - filter.addSingleTopic(EventEncoder.encode(APPROVALFORALL_EVENT)); - return approvalForAllEventFlowable(filter); - } - - @Deprecated - public static ERC721ABI load(String contractAddress, Web3j web3j, Credentials credentials, BigInteger gasPrice, BigInteger gasLimit) { - return new ERC721ABI(contractAddress, web3j, credentials, gasPrice, gasLimit); - } - - @Deprecated - public static ERC721ABI load(String contractAddress, Web3j web3j, TransactionManager transactionManager, BigInteger gasPrice, BigInteger gasLimit) { - return new ERC721ABI(contractAddress, web3j, transactionManager, gasPrice, gasLimit); - } - - public static ERC721ABI load(String contractAddress, Web3j web3j, Credentials credentials, ContractGasProvider contractGasProvider) { - return new ERC721ABI(contractAddress, web3j, credentials, contractGasProvider); - } - - public static ERC721ABI load(String contractAddress, Web3j web3j, TransactionManager transactionManager, ContractGasProvider contractGasProvider) { - return new ERC721ABI(contractAddress, web3j, transactionManager, contractGasProvider); - } - - public static class TransferEventResponse extends BaseEventResponse { - public String from; - - public String to; - - public BigInteger tokenId; - } - - public static class ApprovalEventResponse extends BaseEventResponse { - public String owner; - - public String approved; - - public BigInteger tokenId; - } - - public static class ApprovalForAllEventResponse extends BaseEventResponse { - public String owner; - - public String operator; - - public Boolean approved; - } -} diff --git a/src/main/kotlin/com/api/admin/domain/nft/NftRepository.kt b/src/main/kotlin/com/api/admin/domain/nft/NftRepository.kt index 9bb9082..b79c2b6 100644 --- a/src/main/kotlin/com/api/admin/domain/nft/NftRepository.kt +++ b/src/main/kotlin/com/api/admin/domain/nft/NftRepository.kt @@ -1,9 +1,11 @@ package com.api.admin.domain.nft +import com.api.admin.enums.ChainType import org.springframework.data.repository.reactive.ReactiveCrudRepository import reactor.core.publisher.Mono interface NftRepository : ReactiveCrudRepository, NftRepositorySupport{ fun findByTokenAddressAndTokenId(address:String,tokenId:String): Mono + fun findByTokenAddressAndTokenIdAndChainType(address:String,tokenId:String,chainType: ChainType): Mono } \ No newline at end of file diff --git a/src/main/kotlin/com/api/admin/enums/Enum.kt b/src/main/kotlin/com/api/admin/enums/Enum.kt index 916f98e..c02b7b9 100644 --- a/src/main/kotlin/com/api/admin/enums/Enum.kt +++ b/src/main/kotlin/com/api/admin/enums/Enum.kt @@ -17,16 +17,8 @@ enum class AccountType{ } enum class TransferType { - ERC20,ERC721 + ERC20,ERC721,NATIVE } -//enum class ChainType(val chainId: Long, val baseUrl: String) { -// ETHEREUM_MAINNET(1L, "https://mainnet.infura.io/v3/YOUR_INFURA_PROJECT_ID"), -// POLYGON_MAINNET(137L, "https://polygon-mainnet.infura.io/v3/YOUR_INFURA_PROJECT_ID"), -// ETHEREUM_GOERLI(5L, "https://goerli.infura.io/v3/YOUR_INFURA_PROJECT_ID"), -// ETHEREUM_SEPOLIA(11155111L, "https://sepolia.infura.io/v3/YOUR_INFURA_PROJECT_ID"), -// POLYGON_MUMBAI(80001L, "https://polygon-mumbai.infura.io/v3/YOUR_INFURA_PROJECT_ID") -//} - diff --git a/src/main/kotlin/com/api/admin/service/InfuraApiService.kt b/src/main/kotlin/com/api/admin/service/InfuraApiService.kt index 86ce91a..304fe1e 100644 --- a/src/main/kotlin/com/api/admin/service/InfuraApiService.kt +++ b/src/main/kotlin/com/api/admin/service/InfuraApiService.kt @@ -2,6 +2,7 @@ package com.api.admin.service import com.api.admin.enums.ChainType import com.api.admin.service.dto.InfuraRequest +import com.api.admin.service.dto.InfuraResponse import com.api.admin.service.dto.InfuraTransferResponse import org.springframework.http.MediaType import org.springframework.stereotype.Service @@ -13,7 +14,7 @@ class InfuraApiService { private val apiKey = "98b672d2ce9a4089a3a5cb5081dde2fa" - private fun urlByChain(chainType: ChainType) : WebClient { + fun urlByChain(chainType: ChainType) : WebClient { val baseUrl = when (chainType) { ChainType.ETHEREUM_MAINNET -> "https://mainnet.infura.io" ChainType.POLYGON_MAINNET -> "https://polygon-mainnet.infura.io" @@ -28,7 +29,7 @@ class InfuraApiService { .build() } - fun getNftTransfer(chainType: ChainType, transactionHash: String): Mono { + fun getTransferLog(chainType: ChainType, transactionHash: String): Mono { val requestBody = InfuraRequest(method = "eth_getTransactionReceipt", params = listOf(transactionHash)) val webClient = urlByChain(chainType) @@ -53,8 +54,8 @@ class InfuraApiService { .bodyToMono(String::class.java) } - fun getTransactionCount(chainType: ChainType,address: String) : Mono { - val requestBody = InfuraRequest(method = "eth_getTransactionCount", params = listOf(address,"latest")) + fun getTransactionCount(chainType: ChainType, address: String): Mono { + val requestBody = InfuraRequest(method = "eth_getTransactionCount", params = listOf(address, "latest")) val webClient = urlByChain(chainType) return webClient.post() @@ -62,6 +63,22 @@ class InfuraApiService { .contentType(MediaType.APPLICATION_JSON) .bodyValue(requestBody) .retrieve() - .bodyToMono(String::class.java) + .bodyToMono(InfuraResponse::class.java) + .mapNotNull { it.result } + .onErrorMap { e -> NumberFormatException("Invalid response format for transaction count") } + } + + fun getGasPrice(chainType: ChainType): Mono { + val requestBody = InfuraRequest(method = "eth_gasPrice", params = emptyList()) + val webClient = urlByChain(chainType) + + return webClient.post() + .uri("/v3/$apiKey") + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(requestBody) + .retrieve() + .bodyToMono(InfuraResponse::class.java) + .mapNotNull { it.result } + .onErrorMap { e -> NumberFormatException("Invalid response format for gas price") } } } \ No newline at end of file diff --git a/src/main/kotlin/com/api/admin/service/NftApiService.kt b/src/main/kotlin/com/api/admin/service/NftApiService.kt new file mode 100644 index 0000000..85c76bc --- /dev/null +++ b/src/main/kotlin/com/api/admin/service/NftApiService.kt @@ -0,0 +1,30 @@ +package com.api.admin.service + +import com.api.admin.service.dto.NftRequest +import com.api.admin.service.dto.NftResponse +import org.springframework.http.MediaType +import org.springframework.stereotype.Service +import org.springframework.web.reactive.function.client.WebClient +import reactor.core.publisher.Mono + +@Service +class NftApiService { + + + private val webClient = WebClient.builder() + .baseUrl(uri) + .build() + + fun getNftSave(request: NftRequest): Mono { + return webClient.post() + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(request) + .retrieve() + .bodyToMono(NftResponse::class.java) + } + + + companion object { + val uri = "http://localhost:8082/v1/nft" + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/api/admin/service/NftService.kt b/src/main/kotlin/com/api/admin/service/NftService.kt index 9d851a5..e4a942f 100644 --- a/src/main/kotlin/com/api/admin/service/NftService.kt +++ b/src/main/kotlin/com/api/admin/service/NftService.kt @@ -1,24 +1,54 @@ package com.api.admin.service +import com.api.admin.domain.nft.Nft import com.api.admin.service.dto.NftResponse import com.api.admin.service.dto.NftResponse.Companion.toEntity import com.api.admin.domain.nft.NftRepository +import com.api.admin.enums.ChainType +import com.api.admin.service.dto.NftRequest +import org.springframework.dao.DuplicateKeyException import org.springframework.stereotype.Service import reactor.core.publisher.Mono +import reactor.kotlin.core.publisher.switchIfEmpty @Service class NftService( private val nftRepository: NftRepository, + private val nftApiService: NftApiService, ) { + fun save(response: NftResponse): Mono { return nftRepository.findById(response.id) - .flatMap { - Mono.empty() + .hasElement() + .flatMap { exists -> + if (!exists) { + nftRepository.insert(response.toEntity()) + .onErrorResume(DuplicateKeyException::class.java) { + Mono.empty() + } + .then() + } else { + Mono.empty() + } } + } + + fun findByNft(address: String, tokenId: String, chainType: ChainType): Mono { + return nftRepository.findByTokenAddressAndTokenIdAndChainType(address, tokenId, chainType) .switchIfEmpty( - nftRepository.insert(response.toEntity()).then() + nftApiService.getNftSave( + NftRequest( + address, + tokenId, + chainType + ) + ).flatMap { + nftRepository.insert(it.toEntity()) + .onErrorResume(DuplicateKeyException::class.java) { + nftRepository.findByTokenAddressAndTokenIdAndChainType(address, tokenId, chainType) + } + } ) } - } \ No newline at end of file diff --git a/src/main/kotlin/com/api/admin/service/TransferService.kt b/src/main/kotlin/com/api/admin/service/TransferService.kt index d5cea1f..c1512fc 100644 --- a/src/main/kotlin/com/api/admin/service/TransferService.kt +++ b/src/main/kotlin/com/api/admin/service/TransferService.kt @@ -23,10 +23,13 @@ class TransferService( private val transferRepository: TransferRepository, private val eventPublisher: ApplicationEventPublisher, private val infuraApiService: InfuraApiService, + private val nftService: NftService, + ) { private val adminAddress = "0x9bDeF468ae33b09b12a057B4c9211240D63BaE65" private val transferEventSignature = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" + private val nativeTransferEventSignature = "0xe6497e3ee548a3372136af2fcb0696db31fc6cf20260707645068bd3fe97f3c4" fun getTransferData( wallet: String, @@ -37,7 +40,7 @@ class TransferService( return transferRepository.existsByTransactionHash(transactionHash) .flatMap { if (it) { - Mono.empty() + Mono.error(IllegalStateException("Transaction already exists")) } else { saveTransfer(wallet, chainType, transactionHash, AccountType.DEPOSIT) .doOnNext { transfer -> @@ -49,13 +52,14 @@ class TransferService( } fun saveTransfer(wallet: String, chainType: ChainType, transactionHash: String, accountType: AccountType): Flux { - return infuraApiService.getNftTransfer(chainType, transactionHash) + return infuraApiService.getTransferLog(chainType, transactionHash) .flatMapMany { response -> Flux.fromIterable(response.result.logs) .flatMap { it.toEntity(wallet, accountType,chainType) } } .flatMap { transfer -> transferRepository.save(transfer) } } + private fun Transfer.toResponse() = AdminTransferResponse( id = this.id!!, walletAddress = this.wallet, @@ -67,56 +71,86 @@ class TransferService( chainType = this.chainType, ) - fun InfuraTransferDetail.toEntity(wallet: String, accountType: AccountType,chainType: ChainType): Mono { + fun InfuraTransferDetail.toEntity(wallet: String, accountType: AccountType, chainType: ChainType): Mono { return Mono.just(this) - .filter { it.topics.isNotEmpty() && it.topics[0] == transferEventSignature } - .filter { - if (accountType == AccountType.DEPOSIT) { - it.topics.size >= 3 && parseAddress(it.topics[2]) == adminAddress.lowercase() && parseAddress(it.topics[1]) == wallet.lowercase() - } else { - it.topics.size >= 3 && parseAddress(it.topics[2]) == wallet.lowercase() && parseAddress(it.topics[1]) == adminAddress.lowercase() - } - } .flatMap { log -> - val transferType = if (log.topics.size > 3) TransferType.ERC721 else TransferType.ERC20 - when (transferType) { - TransferType.ERC721 -> { - val tokenId = BigInteger(log.topics[3].removePrefix("0x"), 16).toString() - nftRepository.findByTokenAddressAndTokenId(log.address, tokenId) - .map { nft -> - Transfer( - id = null, - nftId = nft.id, - wallet = wallet, - timestamp = Instant.now().toEpochMilli(), - accountType = accountType, - balance = null, - transferType = transferType, - transactionHash = log.transactionHash, - chainType = chainType, - ) - } - } - - else -> { - val balance = toBigDecimal(log.data) - Mono.just( - Transfer( - id = null, - nftId = null, - wallet = wallet, - timestamp = Instant.now().toEpochMilli(), - accountType = accountType, - balance = balance, - transferType = transferType, - transactionHash = log.transactionHash, - chainType = chainType, - ) - ) - } + when { + log.topics[0] == nativeTransferEventSignature -> + handleERC20Transfer(log, wallet, accountType, chainType,TransferType.NATIVE) + log.topics[0] == transferEventSignature && log.topics.size == 3 -> + handleERC20Transfer(log, wallet, accountType, chainType, TransferType.ERC20) + log.topics[0] == transferEventSignature && log.topics.size == 4 -> + handleERC721Transfer(log, wallet, accountType, chainType) + else -> Mono.empty() } } } + + private fun handleERC20Transfer(log: InfuraTransferDetail, wallet: String, accountType: AccountType, chainType: ChainType,transferType: TransferType): Mono { + val from = parseAddress(log.topics[2]) + val to = parseAddress(log.topics[3]) + val amount = when (transferType) { + TransferType.NATIVE -> parseNativeTransferAmount(log.data) + TransferType.ERC20 -> toBigDecimal(log.data) + else -> BigDecimal.ZERO + } + + val isRelevantTransfer = when (accountType) { + AccountType.DEPOSIT -> from.equals(wallet, ignoreCase = true) && to.equals(adminAddress, ignoreCase = true) + AccountType.WITHDRAW -> from.equals(adminAddress, ignoreCase = true) && to.equals(wallet, ignoreCase = true) + } + + return if (isRelevantTransfer) { + Mono.just( + Transfer( + id = null, + nftId = null, + wallet = wallet, + timestamp = Instant.now().toEpochMilli(), + accountType = accountType, + balance = amount, + transferType = TransferType.ERC20, + transactionHash = log.transactionHash, + chainType = chainType, + ) + ) + } else { + Mono.empty() + } + } + + + + private fun handleERC721Transfer(log: InfuraTransferDetail, wallet: String, accountType: AccountType, chainType: ChainType): Mono { + val from = parseAddress(log.topics[1]) + val to = parseAddress(log.topics[2]) + val tokenId = BigInteger(log.topics[3].removePrefix("0x"), 16).toString() + + val isRelevantTransfer = when (accountType) { + AccountType.DEPOSIT -> from.equals(wallet, ignoreCase = true) && to.equals(adminAddress, ignoreCase = true) + AccountType.WITHDRAW -> from.equals(adminAddress, ignoreCase = true) && to.equals(wallet, ignoreCase = true) + } + + return if (isRelevantTransfer) { + nftService.findByNft(log.address, tokenId, chainType) + .map { nft -> + Transfer( + id = null, + nftId = nft.id, + wallet = wallet, + timestamp = Instant.now().toEpochMilli(), + accountType = accountType, + balance = null, + transferType = TransferType.ERC721, + transactionHash = log.transactionHash, + chainType = chainType, + ) + } + } else { + Mono.empty() + } + } + private fun parseAddress(address: String): String { return "0x" + address.substring(26).padStart(40, '0') } @@ -124,4 +158,9 @@ class TransferService( private fun toBigDecimal(balance: String): BigDecimal = BigInteger(balance.removePrefix("0x"), 16).toBigDecimal().divide(BigDecimal("1000000000000000000")) + private fun parseNativeTransferAmount(data: String): BigDecimal { + val cleanData = data.removePrefix("0x") + val amountHex = cleanData.substring(0, 64) + return BigInteger(amountHex, 16).toBigDecimal().divide(BigDecimal("1000000000000000000")) + } } \ No newline at end of file diff --git a/src/main/kotlin/com/api/admin/service/Web3jService.kt b/src/main/kotlin/com/api/admin/service/Web3jService.kt index ab01f94..460a9e3 100644 --- a/src/main/kotlin/com/api/admin/service/Web3jService.kt +++ b/src/main/kotlin/com/api/admin/service/Web3jService.kt @@ -14,6 +14,7 @@ import org.web3j.protocol.Web3j import org.web3j.protocol.core.DefaultBlockParameterName import org.web3j.protocol.http.HttpService import org.web3j.utils.Numeric +import reactor.core.publisher.Mono import java.math.BigInteger @Service @@ -22,7 +23,9 @@ class Web3jService( ) { private val apiKey = "98b672d2ce9a4089a3a5cb5081dde2fa" - private val privateKey = "e9769d3c00032a83d703e03630edbfc3cb634b40b92e38ab2890d5e37f21bb15" + private val privateKey = "4ec9e64419547100af4f38d7ec57ba1de2d5c36a7dfb03f1a349b2c5b62ac0a9" + private val adminAddress = "0x9bDeF468ae33b09b12a057B4c9211240D63BaE65" + private fun getChainId(chain: ChainType): Long { val chain = when (chain) { ChainType.ETHEREUM_MAINNET -> 1L @@ -36,98 +39,87 @@ class Web3jService( return chain } - fun createTransaction(privateKey:String,recipientAddress: String, amount: BigInteger,chainType: ChainType): String { - val web3j = Web3j.build(HttpService("https://polygon-mainnet.infura.io/v3/$apiKey")) - val credentials = Credentials.create(privateKey) - return createERC20TransactionData(web3j, credentials, recipientAddress, amount,chainType) + fun createTransactionERC721( + contractAddress: String, + toAddress: String, + tokenId: BigInteger, + chainType: ChainType + ): Mono { + val credentials = Credentials.create(privateKey) + return createERC721TransactionData(credentials, contractAddress, toAddress, tokenId, chainType) } - fun createERC20TransactionData(web3j: Web3j, - credentials: Credentials, - recipientAddress: String, - amountInWei: BigInteger, - chainType: ChainType - ): String { - val nonce = web3j.ethGetTransactionCount(credentials.address, DefaultBlockParameterName.LATEST).send().transactionCount - val gasPrice = web3j.ethGasPrice().send().gasPrice - val gasLimit = BigInteger.valueOf(15000) - val chainId = getChainId(chainType) - val rawTransaction = RawTransaction.createEtherTransaction( - nonce, gasPrice, gasLimit, recipientAddress, amountInWei - ) - - val signedMessage = TransactionEncoder.signMessage(rawTransaction, chainId, credentials) - return Numeric.toHexString(signedMessage) + fun createERC721TransactionData( + credentials: Credentials, + contractAddress: String, + toAddress: String, + tokenId: BigInteger, + chainType: ChainType + ): Mono { + return infuraApiService.getTransactionCount(chainType, credentials.address) + .zipWith(infuraApiService.getGasPrice(chainType)) + .flatMap { tuple -> + val nonce = BigInteger(tuple.t1.removePrefix("0x"), 16) + val gasPrice = BigInteger(tuple.t2.removePrefix("0x"), 16) + val gasLimit = BigInteger.valueOf(21940) + val chainId = getChainId(chainType) + + val function = Function( + "safeTransferFrom", + listOf( + Address(credentials.address), + Address(toAddress), + Uint256(tokenId) + ), + emptyList() + ) + + val encodedFunction = FunctionEncoder.encode(function) + + val rawTransaction = RawTransaction.createTransaction( + nonce, gasPrice, gasLimit, contractAddress, encodedFunction + ) + + val signedMessage = TransactionEncoder.signMessage(rawTransaction, chainId, credentials) + val signedTransactionData = Numeric.toHexString(signedMessage) + + infuraApiService.getSend(chainType, signedTransactionData) + } } - fun createERC721TransactionData(web3j: Web3j, - credentials: Credentials, - contractAddress: String, - fromAddress: String, - toAddress: String, - tokenId: BigInteger, - chainType: ChainType, - ): String { - val nonce = web3j.ethGetTransactionCount(credentials.address, DefaultBlockParameterName.LATEST).send().transactionCount - val gasPrice = web3j.ethGasPrice().send().gasPrice - val gasLimit = BigInteger.valueOf(15000) - val chainId = getChainId(chainType) - - val function = Function( - "safeTransferFrom", - listOf( - Address(fromAddress), - Address(toAddress), - Uint256(tokenId) - ), - emptyList() - ) - - val encodedFunction = FunctionEncoder.encode(function) - - val rawTransaction = RawTransaction.createTransaction( - nonce, gasPrice, gasLimit, contractAddress, encodedFunction - ) - - val signedMessage = TransactionEncoder.signMessage(rawTransaction, chainId, credentials) - return Numeric.toHexString(signedMessage) + fun createTransactionERC20( + recipientAddress: String, + amount: BigInteger, + chainType: ChainType + ): Mono { + val credentials = Credentials.create(privateKey) + return createERC20TransactionData(credentials, recipientAddress, amount, chainType) } - fun createERC1155TransactionData( - web3j: Web3j, + fun createERC20TransactionData( credentials: Credentials, - contractAddress: String, - fromAddress: String, - toAddress: String, - tokenId: BigInteger, + recipientAddress: String, amount: BigInteger, chainType: ChainType - ): String { - val nonce = web3j.ethGetTransactionCount(credentials.address, DefaultBlockParameterName.LATEST).send().transactionCount - val gasPrice = web3j.ethGasPrice().send().gasPrice - val gasLimit = BigInteger.valueOf(200000) - val chainId = getChainId(chainType) - - val function = Function( - "safeTransferFrom", - listOf( - Address(credentials.address), - Address(toAddress), - Uint256(tokenId), - Uint256(amount), - Utf8String("") // empty data - ), - emptyList() - ) - - val encodedFunction = FunctionEncoder.encode(function) - - val rawTransaction = RawTransaction.createTransaction( - nonce, gasPrice, gasLimit, contractAddress, encodedFunction - ) - - val signedMessage = TransactionEncoder.signMessage(rawTransaction, chainId, credentials) - return Numeric.toHexString(signedMessage) + ): Mono { + return infuraApiService.getTransactionCount(chainType, credentials.address) + .zipWith(infuraApiService.getGasPrice(chainType)) + .flatMap { tuple -> + val nonce = BigInteger(tuple.t1.removePrefix("0x"), 16) + val gasPrice = BigInteger(tuple.t2.removePrefix("0x"), 16) + val gasLimit = BigInteger.valueOf(30000) + val chainId = getChainId(chainType) + + val rawTransaction = RawTransaction.createEtherTransaction( + nonce, gasPrice, gasLimit, recipientAddress, amount + ) + + val signedMessage = TransactionEncoder.signMessage(rawTransaction, chainId, credentials) + val signedTransactionData = Numeric.toHexString(signedMessage) + + infuraApiService.getSend(chainType, signedTransactionData) + } } + } \ No newline at end of file diff --git a/src/main/kotlin/com/api/admin/service/dto/InfuraResponse.kt b/src/main/kotlin/com/api/admin/service/dto/InfuraResponse.kt new file mode 100644 index 0000000..79bbcc3 --- /dev/null +++ b/src/main/kotlin/com/api/admin/service/dto/InfuraResponse.kt @@ -0,0 +1,3 @@ +package com.api.admin.service.dto + +data class InfuraResponse(val result: String) \ No newline at end of file diff --git a/src/main/kotlin/com/api/admin/service/dto/NftRequest.kt b/src/main/kotlin/com/api/admin/service/dto/NftRequest.kt new file mode 100644 index 0000000..773e37c --- /dev/null +++ b/src/main/kotlin/com/api/admin/service/dto/NftRequest.kt @@ -0,0 +1,9 @@ +package com.api.admin.service.dto + +import com.api.admin.enums.ChainType + +data class NftRequest( + val tokenAddress: String, + val tokenId: String, + val chainType: ChainType +) diff --git a/src/main/kotlin/com/api/admin/wrapper/ERC721ABI.json b/src/main/kotlin/com/api/admin/wrapper/ERC721ABI.json index 8c9174b..d9d2991 100644 --- a/src/main/kotlin/com/api/admin/wrapper/ERC721ABI.json +++ b/src/main/kotlin/com/api/admin/wrapper/ERC721ABI.json @@ -1,27 +1,15 @@ [ - { - "constant": true, - "inputs": [], - "name": "name", - "outputs": [ - { - "name": "", - "type": "string" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, { "constant": false, "inputs": [ { - "name": "_to", + "internalType": "address", + "name": "to", "type": "address" }, { - "name": "_tokenId", + "internalType": "uint256", + "name": "tokenId", "type": "uint256" } ], @@ -32,176 +20,191 @@ "type": "function" }, { - "constant": true, - "inputs": [], - "name": "totalSupply", - "outputs": [ + "constant": false, + "inputs": [ { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "symbol", - "outputs": [ + "internalType": "address", + "name": "to", + "type": "address" + }, { - "name": "", - "type": "string" + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" } ], + "name": "mint", + "outputs": [], "payable": false, - "stateMutability": "view", + "stateMutability": "nonpayable", "type": "function" }, { "constant": false, "inputs": [ { - "name": "_from", + "internalType": "address", + "name": "from", "type": "address" }, { - "name": "_to", + "internalType": "address", + "name": "to", "type": "address" }, { - "name": "_tokenId", + "internalType": "uint256", + "name": "tokenId", "type": "uint256" } ], - "name": "transferFrom", + "name": "safeTransferFrom", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { - "constant": true, + "constant": false, "inputs": [ { - "name": "_owner", + "internalType": "address", + "name": "to", "type": "address" - } - ], - "name": "balanceOf", - "outputs": [ + }, { - "name": "balance", - "type": "uint256" + "internalType": "bool", + "name": "approved", + "type": "bool" } ], + "name": "setApprovalForAll", + "outputs": [], "payable": false, - "stateMutability": "view", + "stateMutability": "nonpayable", "type": "function" }, { - "constant": true, + "constant": false, "inputs": [ { - "name": "_tokenId", - "type": "uint256" - } - ], - "name": "ownerOf", - "outputs": [ + "internalType": "address", + "name": "from", + "type": "address" + }, { - "name": "owner", + "internalType": "address", + "name": "to", "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" } ], + "name": "transferFrom", + "outputs": [], "payable": false, - "stateMutability": "view", + "stateMutability": "nonpayable", "type": "function" }, { - "constant": false, + "inputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, "inputs": [ { - "name": "_from", + "indexed": true, + "internalType": "address", + "name": "from", "type": "address" }, { - "name": "_to", + "indexed": true, + "internalType": "address", + "name": "to", "type": "address" }, { - "name": "_tokenId", + "indexed": true, + "internalType": "uint256", + "name": "tokenId", "type": "uint256" } ], - "name": "safeTransferFrom", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" + "name": "Transfer", + "type": "event" }, { - "constant": false, + "anonymous": false, "inputs": [ { - "name": "_from", + "indexed": true, + "internalType": "address", + "name": "owner", "type": "address" }, { - "name": "_to", + "indexed": true, + "internalType": "address", + "name": "approved", "type": "address" }, { - "name": "_tokenId", + "indexed": true, + "internalType": "uint256", + "name": "tokenId", "type": "uint256" - }, - { - "name": "_data", - "type": "bytes" } ], - "name": "safeTransferFrom", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" + "name": "Approval", + "type": "event" }, { - "constant": false, + "anonymous": false, "inputs": [ { - "name": "_operator", + "indexed": true, + "internalType": "address", + "name": "owner", "type": "address" }, { - "name": "_approved", + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "indexed": false, + "internalType": "bool", + "name": "approved", "type": "bool" } ], - "name": "setApprovalForAll", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" + "name": "ApprovalForAll", + "type": "event" }, { "constant": true, "inputs": [ { - "name": "_owner", - "type": "address" - }, - { - "name": "_operator", + "internalType": "address", + "name": "owner", "type": "address" } ], - "name": "isApprovedForAll", + "name": "balanceOf", "outputs": [ { + "internalType": "uint256", "name": "", - "type": "bool" + "type": "uint256" } ], "payable": false, @@ -212,13 +215,15 @@ "constant": true, "inputs": [ { - "name": "_tokenId", + "internalType": "uint256", + "name": "tokenId", "type": "uint256" } ], "name": "getApproved", "outputs": [ { + "internalType": "address", "name": "", "type": "address" } @@ -228,69 +233,71 @@ "type": "function" }, { - "anonymous": false, + "constant": true, "inputs": [ { - "indexed": true, - "name": "from", + "internalType": "address", + "name": "owner", "type": "address" }, { - "indexed": true, - "name": "to", + "internalType": "address", + "name": "operator", "type": "address" - }, + } + ], + "name": "isApprovedForAll", + "outputs": [ { - "indexed": true, - "name": "tokenId", - "type": "uint256" + "internalType": "bool", + "name": "", + "type": "bool" } ], - "name": "Transfer", - "type": "event" + "payable": false, + "stateMutability": "view", + "type": "function" }, { - "anonymous": false, + "constant": true, "inputs": [ { - "indexed": true, - "name": "owner", - "type": "address" - }, - { - "indexed": true, - "name": "approved", - "type": "address" - }, - { - "indexed": true, + "internalType": "uint256", "name": "tokenId", "type": "uint256" } ], - "name": "Approval", - "type": "event" + "name": "ownerOf", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" }, { - "anonymous": false, + "constant": true, "inputs": [ { - "indexed": true, - "name": "owner", - "type": "address" - }, - { - "indexed": true, - "name": "operator", - "type": "address" - }, + "internalType": "bytes4", + "name": "interfaceId", + "type": "bytes4" + } + ], + "name": "supportsInterface", + "outputs": [ { - "indexed": false, - "name": "approved", + "internalType": "bool", + "name": "", "type": "bool" } ], - "name": "ApprovalForAll", - "type": "event" + "payable": false, + "stateMutability": "view", + "type": "function" } ] diff --git a/src/test/kotlin/com/api/admin/AdminServiceTest.kt b/src/test/kotlin/com/api/admin/AdminServiceTest.kt index 7213a74..9267c78 100644 --- a/src/test/kotlin/com/api/admin/AdminServiceTest.kt +++ b/src/test/kotlin/com/api/admin/AdminServiceTest.kt @@ -34,7 +34,7 @@ class AdminServiceTest( @Test fun getNftTransferDetail() { - val res = infuraApiService.getNftTransfer(ChainType.POLYGON_MAINNET,"0x55fa4495f983e9f162b39b3df4dec8ebcff9aa05daee7b051c680ccfb49422a6").block() + val res = infuraApiService.getTransferLog(ChainType.POLYGON_MAINNET,"0xed96d307d81fd04a752d222b0cf06397dff8fc87245e8764b6641d97bc36081c").block() println(res.toString()) } @@ -54,10 +54,12 @@ class AdminServiceTest( @Test fun deposit() { - transferService.getTransferData("0x01b72b4aa3f66f213d62d53e829bc172a6a72867",ChainType.POLYGON_MAINNET,"0x55fa4495f983e9f162b39b3df4dec8ebcff9aa05daee7b051c680ccfb49422a6",AccountType.DEPOSIT) + val res = transferService.getTransferData("0x01b72b4aa3f66f213d62d53e829bc172a6a72867",ChainType.POLYGON_MAINNET,"0xed96d307d81fd04a752d222b0cf06397dff8fc87245e8764b6641d97bc36081c",AccountType.DEPOSIT) .block() - Thread.sleep(100000) + + println("res : " + res.toString()) + } private fun parseAddress(address: String): String { @@ -88,13 +90,13 @@ class AdminServiceTest( // BigInteger("1000000000000000000") // ) - val transactionData = web3jService.createTransaction("e9769d3c00032a83d703e03630edbfc3cb634b40b92e38ab2890d5e37f21bb15", + val transactionData = web3jService.createTransactionERC20( "0x9bDeF468ae33b09b12a057B4c9211240D63BaE65", BigInteger("1000000000000000000"), ChainType.POLYGON_MAINNET ) - val response = infuraApiService.getSend(ChainType.POLYGON_MAINNET,transactionData).block() - println(response) +// val response = infuraApiService.getSend(ChainType.POLYGON_MAINNET,transactionData).block() +// println(response) } @Test @@ -105,34 +107,19 @@ class AdminServiceTest( @Test - fun erc1155Test() { - // val apiKey = "98b672d2ce9a4089a3a5cb5081dde2fa" - // val privateKey = "e9769d3c00032a83d703e03630edbfc3cb634b40b92e38ab2890d5e37f21bb15" - // val web3j = Web3j.build(HttpService("https://polygon-mainnet.infura.io/v3/$apiKey")) - // val credentials = Credentials.create(privateKey) - // - // val contractAddress = "0xe7900239E9332060dC975ED6F0cc5F0129D924cf" - // val fromAddress = "0x01b72b4aa3f66f213d62d53e829bc172a6a72867" - // val toAddress = "0x9bDeF468ae33b09b12a057B4c9211240D63BaE65" - // val tokenId = BigInteger("5") - // val amount =BigInteger("1") - // val chainType = ChainType.POLYGON_MAINNET - // val res = web3jService.createERC1155TransactionData( - // web3j, - // credentials, - // contractAddress, - // fromAddress, - // toAddress, - // tokenId, - // amount, - // chainType - // ) - // - // println("transaction DAta : " + res) - val res = "0xf9012c1d8506fc23ac2983030d4094e7900239e9332060dc975ed6f0cc5f0129d924cf80b8c4de6c65ff00000000000000000000000001b72b4aa3f66f213d62d53e829bc172a6a728670000000000000000000000009bdef468ae33b09b12a057b4c9211240d63bae650000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000820136a0c09f6b44990413c3a81950b014780bd9b2c480f65319922959aeb38df13f2105a014b6764cc0cf8baa3ffb7d82930a1887aa2b014cb4d77bf846e04084583ce138" - val response = infuraApiService.getSend(ChainType.POLYGON_MAINNET,res).block() - println(response) + fun createTransactionERC721() { + val res = web3jService.createTransactionERC721("0x0277AD67A866C0a306D0A16f58C68425f5F216A6","0x01b72b4aa3f66f213d62d53e829bc172a6a72867", + BigInteger("3"),ChainType.POLYGON_AMOY).block() + + println(res.toString()) } + @Test + fun createTransactionERC20() { + val res = web3jService.createTransactionERC20("0x01b72b4aa3f66f213d62d53e829bc172a6a72867", amount = BigInteger("1000000000000"),ChainType.POLYGON_AMOY).block() + println(res.toString()) + } + + } \ No newline at end of file From d35b9ef687d176ee029b89b112cc72c639a323db Mon Sep 17 00:00:00 2001 From: min-96 Date: Sat, 13 Jul 2024 22:05:15 +0900 Subject: [PATCH 13/24] faet: api account of withdraw and deposit --- .../api/admin/controller/AdminController.kt | 35 +++++++++++++++++++ .../admin/controller/dto/DepositRequest.kt | 9 +++++ .../com/api/admin/service/TransferService.kt | 22 ++++++++++-- .../com/api/admin/service/Web3jService.kt | 7 ++-- .../db/migration/V1__Initial_schema.sql | 2 +- .../kotlin/com/api/admin/AdminServiceTest.kt | 32 +++++++++++++---- 6 files changed, 91 insertions(+), 16 deletions(-) create mode 100644 src/main/kotlin/com/api/admin/controller/AdminController.kt create mode 100644 src/main/kotlin/com/api/admin/controller/dto/DepositRequest.kt diff --git a/src/main/kotlin/com/api/admin/controller/AdminController.kt b/src/main/kotlin/com/api/admin/controller/AdminController.kt new file mode 100644 index 0000000..428a44f --- /dev/null +++ b/src/main/kotlin/com/api/admin/controller/AdminController.kt @@ -0,0 +1,35 @@ +package com.api.admin.controller + +import com.api.admin.controller.dto.DepositRequest +import com.api.admin.enums.AccountType +import com.api.admin.service.TransferService +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.GetMapping +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.Mono + +@RestController +@RequestMapping("/v1/admin") +class AdminController( + private val transferService: TransferService, +) { + + @PostMapping("/deposit") + fun deposit( + @RequestParam address: String, + @RequestBody request: DepositRequest, + ): Mono> { + return transferService.getTransferData(address, request.chainType, request.transactionHash, AccountType.DEPOSIT) + .then(Mono.just(ResponseEntity.ok().build())) + .onErrorResume { + Mono.just(ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build()) + } + } + + +} \ No newline at end of file diff --git a/src/main/kotlin/com/api/admin/controller/dto/DepositRequest.kt b/src/main/kotlin/com/api/admin/controller/dto/DepositRequest.kt new file mode 100644 index 0000000..9ffe758 --- /dev/null +++ b/src/main/kotlin/com/api/admin/controller/dto/DepositRequest.kt @@ -0,0 +1,9 @@ +package com.api.admin.controller.dto + +import com.api.admin.enums.ChainType + +data class DepositRequest( + val chainType: ChainType, + val transactionHash: String, +) + diff --git a/src/main/kotlin/com/api/admin/service/TransferService.kt b/src/main/kotlin/com/api/admin/service/TransferService.kt index c1512fc..0a5a2ab 100644 --- a/src/main/kotlin/com/api/admin/service/TransferService.kt +++ b/src/main/kotlin/com/api/admin/service/TransferService.kt @@ -24,6 +24,7 @@ class TransferService( private val eventPublisher: ApplicationEventPublisher, private val infuraApiService: InfuraApiService, private val nftService: NftService, + private val web3jService: Web3jService, ) { @@ -31,18 +32,32 @@ class TransferService( private val transferEventSignature = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" private val nativeTransferEventSignature = "0xe6497e3ee548a3372136af2fcb0696db31fc6cf20260707645068bd3fe97f3c4" + + // fun accountTransfer( + // wallet: String, + // chainType: ChainType, + // transactionHash: String, + // accountType: AccountType, + // transferType: TransferType, + // ) { + // return when(accountType) { + // AccountType.DEPOSIT -> getTransferData(wallet,chainType,transactionHash, accountType) + // + // } + // } + fun getTransferData( wallet: String, chainType: ChainType, transactionHash: String, - accountType: AccountType + accountType: AccountType, ): Mono { return transferRepository.existsByTransactionHash(transactionHash) .flatMap { if (it) { Mono.error(IllegalStateException("Transaction already exists")) } else { - saveTransfer(wallet, chainType, transactionHash, AccountType.DEPOSIT) + saveTransfer(wallet, chainType, transactionHash, accountType) .doOnNext { transfer -> eventPublisher.publishEvent(AdminTransferCreatedEvent(this, transfer.toResponse())) } @@ -74,6 +89,7 @@ class TransferService( fun InfuraTransferDetail.toEntity(wallet: String, accountType: AccountType, chainType: ChainType): Mono { return Mono.just(this) .flatMap { log -> + println("log : " + log.toString()) when { log.topics[0] == nativeTransferEventSignature -> handleERC20Transfer(log, wallet, accountType, chainType,TransferType.NATIVE) @@ -152,7 +168,7 @@ class TransferService( } private fun parseAddress(address: String): String { - return "0x" + address.substring(26).padStart(40, '0') + return "0x" + address.substring(26).padStart(40, '0').toLowerCase() } private fun toBigDecimal(balance: String): BigDecimal = diff --git a/src/main/kotlin/com/api/admin/service/Web3jService.kt b/src/main/kotlin/com/api/admin/service/Web3jService.kt index 460a9e3..663ccfe 100644 --- a/src/main/kotlin/com/api/admin/service/Web3jService.kt +++ b/src/main/kotlin/com/api/admin/service/Web3jService.kt @@ -5,15 +5,12 @@ import org.springframework.stereotype.Service import org.web3j.abi.FunctionEncoder import org.web3j.abi.datatypes.Address import org.web3j.abi.datatypes.Function -import org.web3j.abi.datatypes.Utf8String import org.web3j.abi.datatypes.generated.Uint256 import org.web3j.crypto.Credentials import org.web3j.crypto.RawTransaction import org.web3j.crypto.TransactionEncoder -import org.web3j.protocol.Web3j -import org.web3j.protocol.core.DefaultBlockParameterName -import org.web3j.protocol.http.HttpService import org.web3j.utils.Numeric +import reactor.core.publisher.Flux import reactor.core.publisher.Mono import java.math.BigInteger @@ -62,7 +59,7 @@ class Web3jService( .flatMap { tuple -> val nonce = BigInteger(tuple.t1.removePrefix("0x"), 16) val gasPrice = BigInteger(tuple.t2.removePrefix("0x"), 16) - val gasLimit = BigInteger.valueOf(21940) + val gasLimit = BigInteger.valueOf(100000) val chainId = getChainId(chainType) val function = Function( diff --git a/src/main/resources/db/migration/V1__Initial_schema.sql b/src/main/resources/db/migration/V1__Initial_schema.sql index 295062e..863a35f 100644 --- a/src/main/resources/db/migration/V1__Initial_schema.sql +++ b/src/main/resources/db/migration/V1__Initial_schema.sql @@ -34,5 +34,5 @@ CREATE TABLE IF NOT EXISTS transfer ( balance DECIMAL(19, 4), transfer_type transfer_type NOT NULL, transaction_hash VARCHAR(255) NOT NULL, - chain_type chain_type + chain_type chain_type NOT NULL ); diff --git a/src/test/kotlin/com/api/admin/AdminServiceTest.kt b/src/test/kotlin/com/api/admin/AdminServiceTest.kt index 9267c78..83931a6 100644 --- a/src/test/kotlin/com/api/admin/AdminServiceTest.kt +++ b/src/test/kotlin/com/api/admin/AdminServiceTest.kt @@ -11,9 +11,6 @@ import com.api.admin.service.Web3jService import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest -import org.web3j.crypto.Credentials -import org.web3j.protocol.Web3j -import org.web3j.protocol.http.HttpService import java.math.BigInteger import java.time.Instant @@ -71,7 +68,7 @@ class AdminServiceTest( val response = AdminTransferResponse( id= 1L, walletAddress = "0x01b72b4aa3f66f213d62d53e829bc172a6a72867", - nftId = 1L, + nftId = 3L, timestamp = Instant.now().toEpochMilli(), accountType = AccountType.DEPOSIT, transferType = TransferType.ERC721, @@ -108,10 +105,19 @@ class AdminServiceTest( @Test fun createTransactionERC721() { - val res = web3jService.createTransactionERC721("0x0277AD67A866C0a306D0A16f58C68425f5F216A6","0x01b72b4aa3f66f213d62d53e829bc172a6a72867", - BigInteger("3"),ChainType.POLYGON_AMOY).block() + // val res = web3jService.createTransactionERC721("0xbc0c96c8d12a149cac4f7688f740ef21b2c8fd23","0x01b72b4aa3f66f213d62d53e829bc172a6a72867", + // BigInteger("0"),ChainType.POLYGON_MAINNET).block() + // + // println(res.toString()) + + // 0x1b0f6c70528addc34a5f17d0c7df59d932c27e85d29af14a957567fff29ef267 + val res1 = transferService.getTransferData( + wallet = "0x01b72b4aa3f66f213d62d53e829bc172a6a72867", + transactionHash = "0x1b0f6c70528addc34a5f17d0c7df59d932c27e85d29af14a957567fff29ef267", + chainType = ChainType.POLYGON_MAINNET, + accountType = AccountType.WITHDRAW + ).block() - println(res.toString()) } @Test @@ -120,6 +126,18 @@ class AdminServiceTest( println(res.toString()) } + @Test + fun test1sd() { + val res = transferService.getTransferData( + wallet = "0x01b72b4aa3f66f213d62d53e829bc172a6a72867", + chainType = ChainType.POLYGON_MAINNET, + transactionHash = "0x0b79d621e6b6652b98e4f45e604985d7f0de6530d19ee0f62b89f5f870951814", + accountType = AccountType.DEPOSIT + ).block() + + Thread.sleep(50000) + } + } \ No newline at end of file From 50bfccc1df0a54bf1cebcf102dd23a36fd4eb87e Mon Sep 17 00:00:00 2001 From: min-96 Date: Sun, 14 Jul 2024 16:26:09 +0900 Subject: [PATCH 14/24] WIP --- .../api/admin/controller/AdminController.kt | 35 +++++++++++++++---- .../controller/dto/WithdrawERC20Request.kt | 9 +++++ .../controller/dto/WithdrawERC721Request.kt | 5 +++ src/main/kotlin/com/api/admin/enums/Enum.kt | 4 +++ .../com/api/admin/service/TransferService.kt | 2 -- .../com/api/admin/service/Web3jService.kt | 13 ++++--- src/main/kotlin/com/api/admin/util/Utils.kt | 5 +++ 7 files changed, 61 insertions(+), 12 deletions(-) create mode 100644 src/main/kotlin/com/api/admin/controller/dto/WithdrawERC20Request.kt create mode 100644 src/main/kotlin/com/api/admin/controller/dto/WithdrawERC721Request.kt create mode 100644 src/main/kotlin/com/api/admin/util/Utils.kt diff --git a/src/main/kotlin/com/api/admin/controller/AdminController.kt b/src/main/kotlin/com/api/admin/controller/AdminController.kt index 428a44f..4a6b93f 100644 --- a/src/main/kotlin/com/api/admin/controller/AdminController.kt +++ b/src/main/kotlin/com/api/admin/controller/AdminController.kt @@ -1,22 +1,20 @@ package com.api.admin.controller import com.api.admin.controller.dto.DepositRequest +import com.api.admin.controller.dto.WithdrawERC20Request import com.api.admin.enums.AccountType import com.api.admin.service.TransferService +import com.api.admin.service.Web3jService import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity -import org.springframework.web.bind.annotation.GetMapping -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 org.springframework.web.bind.annotation.* import reactor.core.publisher.Mono @RestController @RequestMapping("/v1/admin") class AdminController( private val transferService: TransferService, + private val web3jService: Web3jService, ) { @PostMapping("/deposit") @@ -31,5 +29,30 @@ class AdminController( } } + @PostMapping("/withdraw/erc20") + fun withdrawERC20( + @RequestParam address: String, + @RequestBody request: WithdrawERC20Request, + ): Mono> { + return web3jService.createTransactionERC20(address,request.amount,request.chainType) + .then(Mono.just(ResponseEntity.ok().build())) + .onErrorResume { + Mono.just(ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build()) + } + } + + +// @PostMapping("/withdraw/erc721") +// fun withdrawERC721( +// @RequestParam address: String, +// @RequestBody request: WithdrawERC721Request, +// ): Mono> { +// return transferService.getTransferData(address, request) +// .then(Mono.just(ResponseEntity.ok().build())) +// .onErrorResume { +// Mono.just(ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build()) +// } +// } + } \ No newline at end of file diff --git a/src/main/kotlin/com/api/admin/controller/dto/WithdrawERC20Request.kt b/src/main/kotlin/com/api/admin/controller/dto/WithdrawERC20Request.kt new file mode 100644 index 0000000..0bb482d --- /dev/null +++ b/src/main/kotlin/com/api/admin/controller/dto/WithdrawERC20Request.kt @@ -0,0 +1,9 @@ +package com.api.admin.controller.dto + +import com.api.admin.enums.ChainType +import java.math.BigInteger + +data class WithdrawERC20Request( + val chainType: ChainType, + val amount: BigInteger, + ) diff --git a/src/main/kotlin/com/api/admin/controller/dto/WithdrawERC721Request.kt b/src/main/kotlin/com/api/admin/controller/dto/WithdrawERC721Request.kt new file mode 100644 index 0000000..a8879f0 --- /dev/null +++ b/src/main/kotlin/com/api/admin/controller/dto/WithdrawERC721Request.kt @@ -0,0 +1,5 @@ +package com.api.admin.controller.dto + +data class WithdrawERC721Request( + val nftId: Long, +) diff --git a/src/main/kotlin/com/api/admin/enums/Enum.kt b/src/main/kotlin/com/api/admin/enums/Enum.kt index c02b7b9..efa0589 100644 --- a/src/main/kotlin/com/api/admin/enums/Enum.kt +++ b/src/main/kotlin/com/api/admin/enums/Enum.kt @@ -20,5 +20,9 @@ enum class TransferType { ERC20,ERC721,NATIVE } +enum class TokenType { + MATIC, ETH +} + diff --git a/src/main/kotlin/com/api/admin/service/TransferService.kt b/src/main/kotlin/com/api/admin/service/TransferService.kt index 0a5a2ab..5737ef3 100644 --- a/src/main/kotlin/com/api/admin/service/TransferService.kt +++ b/src/main/kotlin/com/api/admin/service/TransferService.kt @@ -24,8 +24,6 @@ class TransferService( private val eventPublisher: ApplicationEventPublisher, private val infuraApiService: InfuraApiService, private val nftService: NftService, - private val web3jService: Web3jService, - ) { private val adminAddress = "0x9bDeF468ae33b09b12a057B4c9211240D63BaE65" diff --git a/src/main/kotlin/com/api/admin/service/Web3jService.kt b/src/main/kotlin/com/api/admin/service/Web3jService.kt index 663ccfe..c5ebae9 100644 --- a/src/main/kotlin/com/api/admin/service/Web3jService.kt +++ b/src/main/kotlin/com/api/admin/service/Web3jService.kt @@ -1,5 +1,6 @@ package com.api.admin.service +import com.api.admin.enums.AccountType import com.api.admin.enums.ChainType import org.springframework.stereotype.Service import org.web3j.abi.FunctionEncoder @@ -17,11 +18,10 @@ import java.math.BigInteger @Service class Web3jService( private val infuraApiService: InfuraApiService, + private val transferService: TransferService, ) { - private val apiKey = "98b672d2ce9a4089a3a5cb5081dde2fa" private val privateKey = "4ec9e64419547100af4f38d7ec57ba1de2d5c36a7dfb03f1a349b2c5b62ac0a9" - private val adminAddress = "0x9bDeF468ae33b09b12a057B4c9211240D63BaE65" private fun getChainId(chain: ChainType): Long { val chain = when (chain) { @@ -43,6 +43,7 @@ class Web3jService( tokenId: BigInteger, chainType: ChainType ): Mono { + // nftId로 들어오면,해당 tokenId와 contractAddress, chainType으로 가져오기 val credentials = Credentials.create(privateKey) return createERC721TransactionData(credentials, contractAddress, toAddress, tokenId, chainType) } @@ -89,9 +90,13 @@ class Web3jService( recipientAddress: String, amount: BigInteger, chainType: ChainType - ): Mono { + ): Mono { val credentials = Credentials.create(privateKey) return createERC20TransactionData(credentials, recipientAddress, amount, chainType) + .flatMap { + transferService.getTransferData(recipientAddress,chainType,it,AccountType.WITHDRAW) + } + .then() } fun createERC20TransactionData( @@ -105,7 +110,7 @@ class Web3jService( .flatMap { tuple -> val nonce = BigInteger(tuple.t1.removePrefix("0x"), 16) val gasPrice = BigInteger(tuple.t2.removePrefix("0x"), 16) - val gasLimit = BigInteger.valueOf(30000) + val gasLimit = BigInteger.valueOf(100000) val chainId = getChainId(chainType) val rawTransaction = RawTransaction.createEtherTransaction( diff --git a/src/main/kotlin/com/api/admin/util/Utils.kt b/src/main/kotlin/com/api/admin/util/Utils.kt new file mode 100644 index 0000000..17fae8b --- /dev/null +++ b/src/main/kotlin/com/api/admin/util/Utils.kt @@ -0,0 +1,5 @@ +package com.api.admin.util + +object Utils { + +} \ No newline at end of file From 1a5641cfb2dcba8125b503d032d55fe397cfe1ba Mon Sep 17 00:00:00 2001 From: min-96 Date: Mon, 15 Jul 2024 01:27:43 +0900 Subject: [PATCH 15/24] success ERC20 of withdraw --- .../api/admin/controller/AdminController.kt | 34 +++--- .../controller/dto/WithdrawERC20Request.kt | 3 +- .../com/api/admin/domain/nft/NftRepository.kt | 2 +- .../com/api/admin/service/InfuraApiService.kt | 18 ++- .../com/api/admin/service/TransferService.kt | 14 ++- .../com/api/admin/service/Web3jService.kt | 108 ++++++++++++++++-- .../service/dto/InfuraTransferResponse.kt | 2 +- .../com/api/admin/util/{Utils.kt => Util.kt} | 2 +- .../kotlin/com/api/admin/AdminServiceTest.kt | 26 ++++- 9 files changed, 171 insertions(+), 38 deletions(-) rename src/main/kotlin/com/api/admin/util/{Utils.kt => Util.kt} (66%) diff --git a/src/main/kotlin/com/api/admin/controller/AdminController.kt b/src/main/kotlin/com/api/admin/controller/AdminController.kt index 4a6b93f..2c96ae1 100644 --- a/src/main/kotlin/com/api/admin/controller/AdminController.kt +++ b/src/main/kotlin/com/api/admin/controller/AdminController.kt @@ -2,6 +2,7 @@ package com.api.admin.controller import com.api.admin.controller.dto.DepositRequest import com.api.admin.controller.dto.WithdrawERC20Request +import com.api.admin.controller.dto.WithdrawERC721Request import com.api.admin.enums.AccountType import com.api.admin.service.TransferService import com.api.admin.service.Web3jService @@ -29,12 +30,30 @@ class AdminController( } } + @PostMapping("/withdraw/erc20") fun withdrawERC20( @RequestParam address: String, @RequestBody request: WithdrawERC20Request, ): Mono> { - return web3jService.createTransactionERC20(address,request.amount,request.chainType) + println("Received withdraw request for address: $address with amount: ${request.amount}") + return web3jService.createTransactionERC20(address, request.amount, request.chainType) + .doOnSuccess { println("Transaction successful") } + .then(Mono.just(ResponseEntity.ok().build())) + .doOnError { e -> + println("Error in withdrawERC20: ${e.message}") + e.printStackTrace() + } + } + + + + @PostMapping("/withdraw/erc721") + fun withdrawERC721( + @RequestParam address: String, + @RequestBody request: WithdrawERC721Request, + ): Mono> { + return web3jService.createTransactionERC721(address, request.nftId) .then(Mono.just(ResponseEntity.ok().build())) .onErrorResume { Mono.just(ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build()) @@ -42,17 +61,4 @@ class AdminController( } -// @PostMapping("/withdraw/erc721") -// fun withdrawERC721( -// @RequestParam address: String, -// @RequestBody request: WithdrawERC721Request, -// ): Mono> { -// return transferService.getTransferData(address, request) -// .then(Mono.just(ResponseEntity.ok().build())) -// .onErrorResume { -// Mono.just(ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build()) -// } -// } - - } \ No newline at end of file diff --git a/src/main/kotlin/com/api/admin/controller/dto/WithdrawERC20Request.kt b/src/main/kotlin/com/api/admin/controller/dto/WithdrawERC20Request.kt index 0bb482d..9b26e87 100644 --- a/src/main/kotlin/com/api/admin/controller/dto/WithdrawERC20Request.kt +++ b/src/main/kotlin/com/api/admin/controller/dto/WithdrawERC20Request.kt @@ -1,9 +1,10 @@ package com.api.admin.controller.dto import com.api.admin.enums.ChainType +import java.math.BigDecimal import java.math.BigInteger data class WithdrawERC20Request( val chainType: ChainType, - val amount: BigInteger, + val amount: BigDecimal, ) diff --git a/src/main/kotlin/com/api/admin/domain/nft/NftRepository.kt b/src/main/kotlin/com/api/admin/domain/nft/NftRepository.kt index b79c2b6..bebb262 100644 --- a/src/main/kotlin/com/api/admin/domain/nft/NftRepository.kt +++ b/src/main/kotlin/com/api/admin/domain/nft/NftRepository.kt @@ -6,6 +6,6 @@ import reactor.core.publisher.Mono interface NftRepository : ReactiveCrudRepository, NftRepositorySupport{ - fun findByTokenAddressAndTokenId(address:String,tokenId:String): Mono +// fun findByTokenAddressAndTokenId(address:String,tokenId:String): Mono fun findByTokenAddressAndTokenIdAndChainType(address:String,tokenId:String,chainType: ChainType): Mono } \ No newline at end of file diff --git a/src/main/kotlin/com/api/admin/service/InfuraApiService.kt b/src/main/kotlin/com/api/admin/service/InfuraApiService.kt index 304fe1e..b396414 100644 --- a/src/main/kotlin/com/api/admin/service/InfuraApiService.kt +++ b/src/main/kotlin/com/api/admin/service/InfuraApiService.kt @@ -7,6 +7,7 @@ import com.api.admin.service.dto.InfuraTransferResponse import org.springframework.http.MediaType import org.springframework.stereotype.Service import org.springframework.web.reactive.function.client.WebClient +import org.web3j.protocol.core.methods.response.TransactionReceipt import reactor.core.publisher.Mono @Service @@ -51,7 +52,9 @@ class InfuraApiService { .contentType(MediaType.APPLICATION_JSON) .bodyValue(requestBody) .retrieve() - .bodyToMono(String::class.java) + .bodyToMono(InfuraResponse::class.java) + .mapNotNull { it.result } + .onErrorMap { e -> NumberFormatException("Invalid response format for transaction count") } } fun getTransactionCount(chainType: ChainType, address: String): Mono { @@ -81,4 +84,17 @@ class InfuraApiService { .mapNotNull { it.result } .onErrorMap { e -> NumberFormatException("Invalid response format for gas price") } } + + fun getTransactionReceipt(chainType: ChainType, transactionHash: String): Mono { + val requestBody = InfuraRequest(method = "eth_getTransactionReceipt", params = listOf(transactionHash)) + val webClient = urlByChain(chainType) + + return webClient.post() + .uri("/v3/$apiKey") + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(requestBody) + .retrieve() + .bodyToMono(String::class.java) + } + } \ No newline at end of file diff --git a/src/main/kotlin/com/api/admin/service/TransferService.kt b/src/main/kotlin/com/api/admin/service/TransferService.kt index 5737ef3..0831c33 100644 --- a/src/main/kotlin/com/api/admin/service/TransferService.kt +++ b/src/main/kotlin/com/api/admin/service/TransferService.kt @@ -50,6 +50,8 @@ class TransferService( transactionHash: String, accountType: AccountType, ): Mono { + println("transactionHash : $transactionHash") + println("여기까지는 와야됨 최소한") return transferRepository.existsByTransactionHash(transactionHash) .flatMap { if (it) { @@ -67,12 +69,18 @@ class TransferService( fun saveTransfer(wallet: String, chainType: ChainType, transactionHash: String, accountType: AccountType): Flux { return infuraApiService.getTransferLog(chainType, transactionHash) .flatMapMany { response -> - Flux.fromIterable(response.result.logs) - .flatMap { it.toEntity(wallet, accountType,chainType) } + val result = response.result + if (result != null) { + Flux.fromIterable(result.logs) + .flatMap { it.toEntity(wallet, accountType, chainType) } + } else { + Flux.error(IllegalStateException("Transaction logs not found for transaction hash: $transactionHash")) + } } .flatMap { transfer -> transferRepository.save(transfer) } } + private fun Transfer.toResponse() = AdminTransferResponse( id = this.id!!, walletAddress = this.wallet, @@ -109,6 +117,8 @@ class TransferService( else -> BigDecimal.ZERO } + println("amount : " + amount) + val isRelevantTransfer = when (accountType) { AccountType.DEPOSIT -> from.equals(wallet, ignoreCase = true) && to.equals(adminAddress, ignoreCase = true) AccountType.WITHDRAW -> from.equals(adminAddress, ignoreCase = true) && to.equals(wallet, ignoreCase = true) diff --git a/src/main/kotlin/com/api/admin/service/Web3jService.kt b/src/main/kotlin/com/api/admin/service/Web3jService.kt index c5ebae9..fbf3b75 100644 --- a/src/main/kotlin/com/api/admin/service/Web3jService.kt +++ b/src/main/kotlin/com/api/admin/service/Web3jService.kt @@ -1,7 +1,10 @@ package com.api.admin.service +import com.api.admin.domain.nft.NftRepository import com.api.admin.enums.AccountType import com.api.admin.enums.ChainType +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.ObjectMapper import org.springframework.stereotype.Service import org.web3j.abi.FunctionEncoder import org.web3j.abi.datatypes.Address @@ -10,15 +13,18 @@ import org.web3j.abi.datatypes.generated.Uint256 import org.web3j.crypto.Credentials import org.web3j.crypto.RawTransaction import org.web3j.crypto.TransactionEncoder +import org.web3j.protocol.core.methods.response.TransactionReceipt import org.web3j.utils.Numeric -import reactor.core.publisher.Flux import reactor.core.publisher.Mono +import java.math.BigDecimal import java.math.BigInteger +import java.time.Duration @Service class Web3jService( private val infuraApiService: InfuraApiService, private val transferService: TransferService, + private val nftRepository: NftRepository, ) { private val privateKey = "4ec9e64419547100af4f38d7ec57ba1de2d5c36a7dfb03f1a349b2c5b62ac0a9" @@ -38,14 +44,16 @@ class Web3jService( fun createTransactionERC721( - contractAddress: String, toAddress: String, - tokenId: BigInteger, - chainType: ChainType - ): Mono { - // nftId로 들어오면,해당 tokenId와 contractAddress, chainType으로 가져오기 - val credentials = Credentials.create(privateKey) - return createERC721TransactionData(credentials, contractAddress, toAddress, tokenId, chainType) + nftId: Long, + ): Mono { + return nftRepository.findById(nftId).flatMap { + val credentials = Credentials.create(privateKey) + createERC721TransactionData(credentials, it.tokenAddress, toAddress, BigInteger(it.tokenId), it.chainType) + .flatMap { data -> + transferService.getTransferData(wallet = toAddress, chainType = it.chainType, transactionHash = data, accountType = AccountType.WITHDRAW) + } + }.then() } fun createERC721TransactionData( @@ -86,19 +94,43 @@ class Web3jService( } } +// fun createTransactionERC20( +// recipientAddress: String, +// amount: BigInteger, +// chainType: ChainType +// ): Mono { +// val credentials = Credentials.create(privateKey) +// return createERC20TransactionData(credentials, recipientAddress, amountToWei(BigDecimal(amount)), chainType) +// .flatMap { +// println("transactionLog :" + it) +// transferService.getTransferData(recipientAddress,chainType,it,AccountType.WITHDRAW) +// } +// .then() +// } + fun createTransactionERC20( recipientAddress: String, - amount: BigInteger, + amount: BigDecimal, chainType: ChainType ): Mono { val credentials = Credentials.create(privateKey) - return createERC20TransactionData(credentials, recipientAddress, amount, chainType) - .flatMap { - transferService.getTransferData(recipientAddress,chainType,it,AccountType.WITHDRAW) + val weiAmount = amountToWei(amount) + return createERC20TransactionData(credentials, recipientAddress, weiAmount, chainType) + .flatMap { transactionHash -> + println("transactionLog: $transactionHash") + waitForTransactionReceipt(transactionHash, chainType) + .flatMap { + transferService.getTransferData(recipientAddress, chainType, transactionHash, AccountType.WITHDRAW) + } + } + .doOnError { e -> + println("Error in createTransactionERC20: ${e.message}") + e.printStackTrace() } .then() } + fun createERC20TransactionData( credentials: Credentials, recipientAddress: String, @@ -124,4 +156,56 @@ class Web3jService( } } + fun amountToWei(amount: BigDecimal): BigInteger { + val weiPerMatic = BigDecimal("1000000000000000000") + return amount.multiply(weiPerMatic).toBigInteger() + } + + + + fun waitForTransactionReceipt(transactionHash: String, chainType: ChainType, maxAttempts: Int = 5, attempt: Int = 1): Mono { + + + val objectMapper = ObjectMapper() + println("Attempt $attempt for transaction $transactionHash") + return infuraApiService.getTransactionReceipt(chainType, transactionHash) + .flatMap { response -> + println("Transaction receipt response: $response") + val jsonNode: JsonNode = objectMapper.readTree(response) + val resultNode = jsonNode.get("result") + println("result Logic : $resultNode") + if (resultNode != null && resultNode.has("status")) { + val status = resultNode.get("status").asText() + println("Transaction status: $status") + if (status == "0x1") { + println("Transaction $transactionHash succeeded") + Mono.just(transactionHash) + } else if (status == "0x0") { + println("Transaction $transactionHash failed") + Mono.error(IllegalStateException("Transaction failed")) + } else { + println("Transaction $transactionHash is pending") + if (attempt >= maxAttempts) { + Mono.error(IllegalStateException("Transaction was not successful after $maxAttempts attempts")) + } else { + Mono.delay(Duration.ofSeconds(5)) + .flatMap { waitForTransactionReceipt(transactionHash, chainType, maxAttempts, attempt + 1) } + } + } + } else { + println("Transaction receipt for $transactionHash not found on attempt $attempt") + if (attempt >= maxAttempts) { + Mono.error(IllegalStateException("Transaction receipt not found after $maxAttempts attempts")) + } else { + Mono.delay(Duration.ofSeconds(5)) + .flatMap { waitForTransactionReceipt(transactionHash, chainType, maxAttempts, attempt + 1) } + } + } + } + .doOnError { e -> + println("Error while checking transaction receipt for $transactionHash: ${e.message}") + } + } + + } \ No newline at end of file diff --git a/src/main/kotlin/com/api/admin/service/dto/InfuraTransferResponse.kt b/src/main/kotlin/com/api/admin/service/dto/InfuraTransferResponse.kt index 0394d4e..9cec097 100644 --- a/src/main/kotlin/com/api/admin/service/dto/InfuraTransferResponse.kt +++ b/src/main/kotlin/com/api/admin/service/dto/InfuraTransferResponse.kt @@ -6,7 +6,7 @@ import java.math.BigInteger data class InfuraTransferResponse( val jsonrpc: String, val id: String, - val result: InfuraTransferResult, + val result: InfuraTransferResult?, ) data class InfuraTransferResult( diff --git a/src/main/kotlin/com/api/admin/util/Utils.kt b/src/main/kotlin/com/api/admin/util/Util.kt similarity index 66% rename from src/main/kotlin/com/api/admin/util/Utils.kt rename to src/main/kotlin/com/api/admin/util/Util.kt index 17fae8b..c39ed4e 100644 --- a/src/main/kotlin/com/api/admin/util/Utils.kt +++ b/src/main/kotlin/com/api/admin/util/Util.kt @@ -1,5 +1,5 @@ package com.api.admin.util -object Utils { +object Util { } \ No newline at end of file diff --git a/src/test/kotlin/com/api/admin/AdminServiceTest.kt b/src/test/kotlin/com/api/admin/AdminServiceTest.kt index 83931a6..24bf486 100644 --- a/src/test/kotlin/com/api/admin/AdminServiceTest.kt +++ b/src/test/kotlin/com/api/admin/AdminServiceTest.kt @@ -11,6 +11,7 @@ import com.api.admin.service.Web3jService import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest +import java.math.BigDecimal import java.math.BigInteger import java.time.Instant @@ -89,7 +90,7 @@ class AdminServiceTest( val transactionData = web3jService.createTransactionERC20( "0x9bDeF468ae33b09b12a057B4c9211240D63BaE65", - BigInteger("1000000000000000000"), + BigDecimal("1000000000000000000"), ChainType.POLYGON_MAINNET ) // val response = infuraApiService.getSend(ChainType.POLYGON_MAINNET,transactionData).block() @@ -122,7 +123,7 @@ class AdminServiceTest( @Test fun createTransactionERC20() { - val res = web3jService.createTransactionERC20("0x01b72b4aa3f66f213d62d53e829bc172a6a72867", amount = BigInteger("1000000000000"),ChainType.POLYGON_AMOY).block() + val res = web3jService.createTransactionERC20("0x01b72b4aa3f66f213d62d53e829bc172a6a72867", amount = BigDecimal("1000000000000"),ChainType.POLYGON_AMOY).block() println(res.toString()) } @@ -130,14 +131,29 @@ class AdminServiceTest( fun test1sd() { val res = transferService.getTransferData( wallet = "0x01b72b4aa3f66f213d62d53e829bc172a6a72867", - chainType = ChainType.POLYGON_MAINNET, - transactionHash = "0x0b79d621e6b6652b98e4f45e604985d7f0de6530d19ee0f62b89f5f870951814", - accountType = AccountType.DEPOSIT + chainType = ChainType.POLYGON_AMOY, + transactionHash = "0xe9d42662999109de038dc6701acf297ff7e693eaf241664323a44f71d626890e", + accountType = AccountType.WITHDRAW ).block() Thread.sleep(50000) } + @Test + fun receipe() { + val res = infuraApiService.getTransactionReceipt(ChainType.POLYGON_AMOY,"0xe9d42662999109de038dc6701acf297ff7e693eaf241664323a44f71d626890e").block() + println("res" + res) + } + +// @Test +// fun twest1() { +// val res = web3jService.testcreateTransactionERC20(recipientAddress = "0x01b72b4aa3f66f213d62d53e829bc172a6a72867",ChainType.POLYGON_AMOY).block() +// } + + @Test + fun waitForTransaction() { + val res = web3jService.waitForTransactionReceipt("0x2c325130ac68f839cc0a049a53a756c9a86d6c430e3367b64b3ed11f524b57c3",ChainType.POLYGON_AMOY).block() + } } \ No newline at end of file From 4d44ee2ab02b3177ef93aff50e86091b11e914e5 Mon Sep 17 00:00:00 2001 From: min-96 Date: Mon, 15 Jul 2024 19:45:50 +0900 Subject: [PATCH 16/24] WIP --- .../com/api/admin/service/Web3jService.kt | 39 +++++++++---------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/src/main/kotlin/com/api/admin/service/Web3jService.kt b/src/main/kotlin/com/api/admin/service/Web3jService.kt index fbf3b75..441580f 100644 --- a/src/main/kotlin/com/api/admin/service/Web3jService.kt +++ b/src/main/kotlin/com/api/admin/service/Web3jService.kt @@ -13,7 +13,6 @@ import org.web3j.abi.datatypes.generated.Uint256 import org.web3j.crypto.Credentials import org.web3j.crypto.RawTransaction import org.web3j.crypto.TransactionEncoder -import org.web3j.protocol.core.methods.response.TransactionReceipt import org.web3j.utils.Numeric import reactor.core.publisher.Mono import java.math.BigDecimal @@ -47,13 +46,26 @@ class Web3jService( toAddress: String, nftId: Long, ): Mono { - return nftRepository.findById(nftId).flatMap { + return nftRepository.findById(nftId).flatMap { nft-> val credentials = Credentials.create(privateKey) - createERC721TransactionData(credentials, it.tokenAddress, toAddress, BigInteger(it.tokenId), it.chainType) - .flatMap { data -> - transferService.getTransferData(wallet = toAddress, chainType = it.chainType, transactionHash = data, accountType = AccountType.WITHDRAW) - } - }.then() + createERC721TransactionData(credentials, nft.tokenAddress, toAddress, BigInteger(nft.tokenId), nft.chainType) + .flatMap { transactionHash -> + waitForTransactionReceipt(transactionHash, nft.chainType) + .flatMap { + transferService.getTransferData( + wallet = toAddress, + chainType = nft.chainType, + transactionHash = transactionHash, + accountType = AccountType.WITHDRAW + ) + } + } + .doOnError { e -> + println("Error in createTransactionERC20: ${e.message}") + e.printStackTrace() + } + .then() + } } fun createERC721TransactionData( @@ -94,19 +106,6 @@ class Web3jService( } } -// fun createTransactionERC20( -// recipientAddress: String, -// amount: BigInteger, -// chainType: ChainType -// ): Mono { -// val credentials = Credentials.create(privateKey) -// return createERC20TransactionData(credentials, recipientAddress, amountToWei(BigDecimal(amount)), chainType) -// .flatMap { -// println("transactionLog :" + it) -// transferService.getTransferData(recipientAddress,chainType,it,AccountType.WITHDRAW) -// } -// .then() -// } fun createTransactionERC20( recipientAddress: String, From e8a01402116507ca7815203cfaa217164a6322b8 Mon Sep 17 00:00:00 2001 From: min96 Date: Mon, 22 Jul 2024 20:00:34 +0900 Subject: [PATCH 17/24] refactor: version up and change to FanoutExchange --- build.gradle.kts | 4 ++-- .../com/api/admin/config/RabbitMQConfig.kt | 24 +++---------------- .../rabbitMQ/receiver/RabbitMQReceiver.kt | 9 ++++++- .../admin/rabbitMQ/sender/RabbitMQSender.kt | 2 +- 4 files changed, 14 insertions(+), 25 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 8a6de2f..dabda35 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -11,7 +11,7 @@ group = "com.api" version = "0.0.1-SNAPSHOT" java { - sourceCompatibility = JavaVersion.VERSION_17 + sourceCompatibility = JavaVersion.VERSION_21 } repositories { @@ -43,7 +43,7 @@ dependencies { tasks.withType { kotlinOptions { freeCompilerArgs += "-Xjsr305=strict" - jvmTarget = "17" + jvmTarget = "21" } } diff --git a/src/main/kotlin/com/api/admin/config/RabbitMQConfig.kt b/src/main/kotlin/com/api/admin/config/RabbitMQConfig.kt index 3df9362..7ae9e58 100644 --- a/src/main/kotlin/com/api/admin/config/RabbitMQConfig.kt +++ b/src/main/kotlin/com/api/admin/config/RabbitMQConfig.kt @@ -3,6 +3,7 @@ package com.api.admin.config import org.springframework.amqp.core.Binding import org.springframework.amqp.core.BindingBuilder import org.springframework.amqp.core.DirectExchange +import org.springframework.amqp.core.FanoutExchange import org.springframework.amqp.core.Queue import org.springframework.amqp.rabbit.connection.ConnectionFactory import org.springframework.amqp.rabbit.core.RabbitTemplate @@ -23,33 +24,14 @@ class RabbitMQConfig { return template } - private fun createQueue(name: String, durable: Boolean = true): Queue { - return Queue(name, durable) + private fun createExchange(name: String): FanoutExchange { + return FanoutExchange(name) } - private fun createExchange(name: String): DirectExchange { - return DirectExchange(name) - } - - private fun createBinding(queue: Queue, exchange: DirectExchange, routingKey: String): Binding { - return BindingBuilder.bind(queue).to(exchange).with(routingKey) - } - - @Bean - fun nftQueue() = createQueue("nftQueue") - @Bean fun nftExchange() = createExchange("nftExchange") - @Bean - fun bindingNftQueue(nftQueue: Queue, nftExchange: DirectExchange) = createBinding(nftQueue, nftExchange, "nftRoutingKey") - - @Bean - fun transferQueue() = createQueue("transferQueue") - @Bean fun transferExchange() = createExchange("transferExchange") - @Bean - fun bindingTransferQueue(transferQueue: Queue, transferExchange: DirectExchange) = createBinding(transferQueue, transferExchange, "transferRoutingKey") } \ No newline at end of file diff --git a/src/main/kotlin/com/api/admin/rabbitMQ/receiver/RabbitMQReceiver.kt b/src/main/kotlin/com/api/admin/rabbitMQ/receiver/RabbitMQReceiver.kt index 3796aec..4e96328 100644 --- a/src/main/kotlin/com/api/admin/rabbitMQ/receiver/RabbitMQReceiver.kt +++ b/src/main/kotlin/com/api/admin/rabbitMQ/receiver/RabbitMQReceiver.kt @@ -2,6 +2,10 @@ package com.api.admin.rabbitMQ.receiver import com.api.admin.service.dto.NftResponse import com.api.admin.service.NftService +import org.springframework.amqp.core.ExchangeTypes +import org.springframework.amqp.rabbit.annotation.Exchange +import org.springframework.amqp.rabbit.annotation.Queue +import org.springframework.amqp.rabbit.annotation.QueueBinding import org.springframework.amqp.rabbit.annotation.RabbitListener import org.springframework.stereotype.Service @@ -9,7 +13,10 @@ import org.springframework.stereotype.Service class RabbitMQReceiver( private val nftService: NftService, ) { - @RabbitListener(queues = ["nftQueue"]) + @RabbitListener(bindings = [QueueBinding( + value = Queue(name = "", durable = "false", exclusive = "true", autoDelete = "true"), + exchange = Exchange(value = "nftExchange", type = ExchangeTypes.FANOUT) + )]) fun nftMessage(nft: NftResponse) { nftService.save(nft) .subscribe() diff --git a/src/main/kotlin/com/api/admin/rabbitMQ/sender/RabbitMQSender.kt b/src/main/kotlin/com/api/admin/rabbitMQ/sender/RabbitMQSender.kt index a042818..87de049 100644 --- a/src/main/kotlin/com/api/admin/rabbitMQ/sender/RabbitMQSender.kt +++ b/src/main/kotlin/com/api/admin/rabbitMQ/sender/RabbitMQSender.kt @@ -10,7 +10,7 @@ class RabbitMQSender( ) { fun transferSend(transfer: AdminTransferResponse) { - rabbitTemplate.convertAndSend("transferExchange", "transferRoutingKey", transfer) + rabbitTemplate.convertAndSend("transferExchange", "", transfer) } } \ No newline at end of file From 906e0099af268d958c7e6f9d285d8b46b2fbe5be Mon Sep 17 00:00:00 2001 From: min96 Date: Tue, 23 Jul 2024 00:42:28 +0900 Subject: [PATCH 18/24] fix: nftService of findByNft --- src/main/kotlin/com/api/admin/service/NftService.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/com/api/admin/service/NftService.kt b/src/main/kotlin/com/api/admin/service/NftService.kt index e4a942f..426eff3 100644 --- a/src/main/kotlin/com/api/admin/service/NftService.kt +++ b/src/main/kotlin/com/api/admin/service/NftService.kt @@ -45,7 +45,7 @@ class NftService( ).flatMap { nftRepository.insert(it.toEntity()) .onErrorResume(DuplicateKeyException::class.java) { - nftRepository.findByTokenAddressAndTokenIdAndChainType(address, tokenId, chainType) + Mono.empty() } } ) From 2fb2bacd6814992db4dbb9bfaaba60f2b304e77e Mon Sep 17 00:00:00 2001 From: min-96 Date: Tue, 23 Jul 2024 16:11:26 +0900 Subject: [PATCH 19/24] apply config --- build.gradle.kts | 8 ++++++ .../kotlin/com/api/admin/AdminApplication.kt | 2 ++ .../admin/properties/AdminInfoProperties.kt | 10 +++++++ .../admin/properties/InfuraApiProperties.kt | 8 ++++++ .../api/admin/properties/NftApiProperties.kt | 8 ++++++ .../rabbitMQ/event/AdminEventListener.kt | 1 - .../com/api/admin/service/InfuraApiService.kt | 19 +++++++------ .../com/api/admin/service/NftApiService.kt | 11 ++++---- .../com/api/admin/service/TransferService.kt | 27 +++++-------------- .../com/api/admin/service/Web3jService.kt | 8 +++--- src/main/resources/application.yml | 23 +++++----------- src/main/resources/bootstrap.yaml | 9 +++++++ 12 files changed, 76 insertions(+), 58 deletions(-) create mode 100644 src/main/kotlin/com/api/admin/properties/AdminInfoProperties.kt create mode 100644 src/main/kotlin/com/api/admin/properties/InfuraApiProperties.kt create mode 100644 src/main/kotlin/com/api/admin/properties/NftApiProperties.kt create mode 100644 src/main/resources/bootstrap.yaml diff --git a/build.gradle.kts b/build.gradle.kts index dabda35..b4f0256 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -17,8 +17,10 @@ java { repositories { mavenCentral() } +extra["springCloudVersion"] = "2023.0.2" dependencies { + implementation("org.springframework.cloud:spring-cloud-starter-config") implementation("org.springframework.boot:spring-boot-starter-data-r2dbc") implementation("org.springframework.boot:spring-boot-starter-webflux") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") @@ -40,6 +42,12 @@ dependencies { testRuntimeOnly("org.junit.platform:junit-platform-launcher") } +dependencyManagement { + imports { + mavenBom("org.springframework.cloud:spring-cloud-dependencies:${property("springCloudVersion")}") + } +} + tasks.withType { kotlinOptions { freeCompilerArgs += "-Xjsr305=strict" diff --git a/src/main/kotlin/com/api/admin/AdminApplication.kt b/src/main/kotlin/com/api/admin/AdminApplication.kt index 728b526..071f1ee 100644 --- a/src/main/kotlin/com/api/admin/AdminApplication.kt +++ b/src/main/kotlin/com/api/admin/AdminApplication.kt @@ -1,9 +1,11 @@ package com.api.admin import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.context.properties.ConfigurationPropertiesScan import org.springframework.boot.runApplication @SpringBootApplication +@ConfigurationPropertiesScan class AdminApplication fun main(args: Array) { diff --git a/src/main/kotlin/com/api/admin/properties/AdminInfoProperties.kt b/src/main/kotlin/com/api/admin/properties/AdminInfoProperties.kt new file mode 100644 index 0000000..447b332 --- /dev/null +++ b/src/main/kotlin/com/api/admin/properties/AdminInfoProperties.kt @@ -0,0 +1,10 @@ +package com.api.admin.properties + +import org.springframework.boot.context.properties.ConfigurationProperties + + +@ConfigurationProperties(prefix = "admin") +data class AdminInfoProperties( + val address: String, + val privatekey: String, +) diff --git a/src/main/kotlin/com/api/admin/properties/InfuraApiProperties.kt b/src/main/kotlin/com/api/admin/properties/InfuraApiProperties.kt new file mode 100644 index 0000000..d501670 --- /dev/null +++ b/src/main/kotlin/com/api/admin/properties/InfuraApiProperties.kt @@ -0,0 +1,8 @@ +package com.api.admin.properties + +import org.springframework.boot.context.properties.ConfigurationProperties + +@ConfigurationProperties(prefix = "apikey") +data class InfuraApiProperties( + val infura: String, +) diff --git a/src/main/kotlin/com/api/admin/properties/NftApiProperties.kt b/src/main/kotlin/com/api/admin/properties/NftApiProperties.kt new file mode 100644 index 0000000..ec26cc3 --- /dev/null +++ b/src/main/kotlin/com/api/admin/properties/NftApiProperties.kt @@ -0,0 +1,8 @@ +package com.api.admin.properties + +import org.springframework.boot.context.properties.ConfigurationProperties + +@ConfigurationProperties(prefix = "nft") +data class NftApiProperties( + val uri: String +) diff --git a/src/main/kotlin/com/api/admin/rabbitMQ/event/AdminEventListener.kt b/src/main/kotlin/com/api/admin/rabbitMQ/event/AdminEventListener.kt index 36b4870..ce2a555 100644 --- a/src/main/kotlin/com/api/admin/rabbitMQ/event/AdminEventListener.kt +++ b/src/main/kotlin/com/api/admin/rabbitMQ/event/AdminEventListener.kt @@ -1,7 +1,6 @@ package com.api.admin.rabbitMQ.event import com.api.admin.rabbitMQ.event.dto.AdminTransferCreatedEvent -import com.api.admin.rabbitMQ.event.dto.AdminTransferResponse import com.api.admin.rabbitMQ.sender.RabbitMQSender import org.springframework.context.event.EventListener import org.springframework.stereotype.Component diff --git a/src/main/kotlin/com/api/admin/service/InfuraApiService.kt b/src/main/kotlin/com/api/admin/service/InfuraApiService.kt index b396414..7fcac50 100644 --- a/src/main/kotlin/com/api/admin/service/InfuraApiService.kt +++ b/src/main/kotlin/com/api/admin/service/InfuraApiService.kt @@ -1,20 +1,19 @@ package com.api.admin.service import com.api.admin.enums.ChainType +import com.api.admin.properties.InfuraApiProperties import com.api.admin.service.dto.InfuraRequest import com.api.admin.service.dto.InfuraResponse import com.api.admin.service.dto.InfuraTransferResponse import org.springframework.http.MediaType import org.springframework.stereotype.Service import org.springframework.web.reactive.function.client.WebClient -import org.web3j.protocol.core.methods.response.TransactionReceipt import reactor.core.publisher.Mono @Service -class InfuraApiService { - - private val apiKey = "98b672d2ce9a4089a3a5cb5081dde2fa" - +class InfuraApiService( + private val infuraApiProperties: InfuraApiProperties, +) { fun urlByChain(chainType: ChainType) : WebClient { val baseUrl = when (chainType) { ChainType.ETHEREUM_MAINNET -> "https://mainnet.infura.io" @@ -35,7 +34,7 @@ class InfuraApiService { val webClient = urlByChain(chainType) return webClient.post() - .uri("/v3/$apiKey") + .uri("/v3/${infuraApiProperties.infura}") .contentType(MediaType.APPLICATION_JSON) .bodyValue(requestBody) .retrieve() @@ -48,7 +47,7 @@ class InfuraApiService { val webClient = urlByChain(chainType) return webClient.post() - .uri("/v3/$apiKey") + .uri("/v3/${infuraApiProperties.infura}") .contentType(MediaType.APPLICATION_JSON) .bodyValue(requestBody) .retrieve() @@ -62,7 +61,7 @@ class InfuraApiService { val webClient = urlByChain(chainType) return webClient.post() - .uri("/v3/$apiKey") + .uri("/v3/${infuraApiProperties.infura}") .contentType(MediaType.APPLICATION_JSON) .bodyValue(requestBody) .retrieve() @@ -76,7 +75,7 @@ class InfuraApiService { val webClient = urlByChain(chainType) return webClient.post() - .uri("/v3/$apiKey") + .uri("/v3/${infuraApiProperties.infura}") .contentType(MediaType.APPLICATION_JSON) .bodyValue(requestBody) .retrieve() @@ -90,7 +89,7 @@ class InfuraApiService { val webClient = urlByChain(chainType) return webClient.post() - .uri("/v3/$apiKey") + .uri("/v3/${infuraApiProperties.infura}") .contentType(MediaType.APPLICATION_JSON) .bodyValue(requestBody) .retrieve() diff --git a/src/main/kotlin/com/api/admin/service/NftApiService.kt b/src/main/kotlin/com/api/admin/service/NftApiService.kt index 85c76bc..9333328 100644 --- a/src/main/kotlin/com/api/admin/service/NftApiService.kt +++ b/src/main/kotlin/com/api/admin/service/NftApiService.kt @@ -1,5 +1,6 @@ package com.api.admin.service +import com.api.admin.properties.NftApiProperties import com.api.admin.service.dto.NftRequest import com.api.admin.service.dto.NftResponse import org.springframework.http.MediaType @@ -8,11 +9,13 @@ import org.springframework.web.reactive.function.client.WebClient import reactor.core.publisher.Mono @Service -class NftApiService { +class NftApiService( + nftApiProperties: NftApiProperties, +) { private val webClient = WebClient.builder() - .baseUrl(uri) + .baseUrl(nftApiProperties.uri) .build() fun getNftSave(request: NftRequest): Mono { @@ -23,8 +26,4 @@ class NftApiService { .bodyToMono(NftResponse::class.java) } - - companion object { - val uri = "http://localhost:8082/v1/nft" - } } \ No newline at end of file diff --git a/src/main/kotlin/com/api/admin/service/TransferService.kt b/src/main/kotlin/com/api/admin/service/TransferService.kt index 0831c33..000b1df 100644 --- a/src/main/kotlin/com/api/admin/service/TransferService.kt +++ b/src/main/kotlin/com/api/admin/service/TransferService.kt @@ -1,11 +1,11 @@ package com.api.admin.service -import com.api.admin.domain.nft.NftRepository import com.api.admin.domain.transfer.Transfer import com.api.admin.domain.transfer.TransferRepository import com.api.admin.enums.AccountType import com.api.admin.enums.ChainType import com.api.admin.enums.TransferType +import com.api.admin.properties.AdminInfoProperties import com.api.admin.rabbitMQ.event.dto.AdminTransferCreatedEvent import com.api.admin.rabbitMQ.event.dto.AdminTransferResponse import com.api.admin.service.dto.InfuraTransferDetail @@ -19,31 +19,17 @@ import java.time.Instant @Service class TransferService( - private val nftRepository: NftRepository, private val transferRepository: TransferRepository, private val eventPublisher: ApplicationEventPublisher, private val infuraApiService: InfuraApiService, private val nftService: NftService, + private val adminInfoProperties: AdminInfoProperties, ) { - private val adminAddress = "0x9bDeF468ae33b09b12a057B4c9211240D63BaE65" private val transferEventSignature = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" private val nativeTransferEventSignature = "0xe6497e3ee548a3372136af2fcb0696db31fc6cf20260707645068bd3fe97f3c4" - // fun accountTransfer( - // wallet: String, - // chainType: ChainType, - // transactionHash: String, - // accountType: AccountType, - // transferType: TransferType, - // ) { - // return when(accountType) { - // AccountType.DEPOSIT -> getTransferData(wallet,chainType,transactionHash, accountType) - // - // } - // } - fun getTransferData( wallet: String, chainType: ChainType, @@ -51,7 +37,6 @@ class TransferService( accountType: AccountType, ): Mono { println("transactionHash : $transactionHash") - println("여기까지는 와야됨 최소한") return transferRepository.existsByTransactionHash(transactionHash) .flatMap { if (it) { @@ -120,8 +105,8 @@ class TransferService( println("amount : " + amount) val isRelevantTransfer = when (accountType) { - AccountType.DEPOSIT -> from.equals(wallet, ignoreCase = true) && to.equals(adminAddress, ignoreCase = true) - AccountType.WITHDRAW -> from.equals(adminAddress, ignoreCase = true) && to.equals(wallet, ignoreCase = true) + AccountType.DEPOSIT -> from.equals(wallet, ignoreCase = true) && to.equals(adminInfoProperties.address, ignoreCase = true) + AccountType.WITHDRAW -> from.equals(adminInfoProperties.address, ignoreCase = true) && to.equals(wallet, ignoreCase = true) } return if (isRelevantTransfer) { @@ -151,8 +136,8 @@ class TransferService( val tokenId = BigInteger(log.topics[3].removePrefix("0x"), 16).toString() val isRelevantTransfer = when (accountType) { - AccountType.DEPOSIT -> from.equals(wallet, ignoreCase = true) && to.equals(adminAddress, ignoreCase = true) - AccountType.WITHDRAW -> from.equals(adminAddress, ignoreCase = true) && to.equals(wallet, ignoreCase = true) + AccountType.DEPOSIT -> from.equals(wallet, ignoreCase = true) && to.equals(adminInfoProperties.address, ignoreCase = true) + AccountType.WITHDRAW -> from.equals(adminInfoProperties.address, ignoreCase = true) && to.equals(wallet, ignoreCase = true) } return if (isRelevantTransfer) { diff --git a/src/main/kotlin/com/api/admin/service/Web3jService.kt b/src/main/kotlin/com/api/admin/service/Web3jService.kt index 441580f..940b3fa 100644 --- a/src/main/kotlin/com/api/admin/service/Web3jService.kt +++ b/src/main/kotlin/com/api/admin/service/Web3jService.kt @@ -3,6 +3,7 @@ package com.api.admin.service import com.api.admin.domain.nft.NftRepository import com.api.admin.enums.AccountType import com.api.admin.enums.ChainType +import com.api.admin.properties.AdminInfoProperties import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.ObjectMapper import org.springframework.stereotype.Service @@ -24,9 +25,10 @@ class Web3jService( private val infuraApiService: InfuraApiService, private val transferService: TransferService, private val nftRepository: NftRepository, + private val adminInfoProperties: AdminInfoProperties ) { - private val privateKey = "4ec9e64419547100af4f38d7ec57ba1de2d5c36a7dfb03f1a349b2c5b62ac0a9" + // private val privateKey = "4ec9e64419547100af4f38d7ec57ba1de2d5c36a7dfb03f1a349b2c5b62ac0a9" private fun getChainId(chain: ChainType): Long { val chain = when (chain) { @@ -47,7 +49,7 @@ class Web3jService( nftId: Long, ): Mono { return nftRepository.findById(nftId).flatMap { nft-> - val credentials = Credentials.create(privateKey) + val credentials = Credentials.create(adminInfoProperties.privatekey) createERC721TransactionData(credentials, nft.tokenAddress, toAddress, BigInteger(nft.tokenId), nft.chainType) .flatMap { transactionHash -> waitForTransactionReceipt(transactionHash, nft.chainType) @@ -112,7 +114,7 @@ class Web3jService( amount: BigDecimal, chainType: ChainType ): Mono { - val credentials = Credentials.create(privateKey) + val credentials = Credentials.create(adminInfoProperties.privatekey) val weiAmount = amountToWei(amount) return createERC20TransactionData(credentials, recipientAddress, weiAmount, chainType) .flatMap { transactionHash -> diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index f35b490..9999397 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,20 +1,9 @@ -server: - port: 8084 spring: application: name: admin - datasource: - url: jdbc:postgresql://localhost:5435/admin - username: admin - password: admin - flyway: - locations: classpath:db/migration - r2dbc: - url: r2dbc:postgresql://localhost:5435/admin - username: admin - password: admin - rabbitmq: - host: localhost - port: 5672 - username: closeSea - password: closeSeaP@ssword + config: + import: "optional:configserver:http://localhost:9000" + cloud: + config: + fail-fast: true + diff --git a/src/main/resources/bootstrap.yaml b/src/main/resources/bootstrap.yaml new file mode 100644 index 0000000..9282f74 --- /dev/null +++ b/src/main/resources/bootstrap.yaml @@ -0,0 +1,9 @@ +spring: + application: + name: admin + cloud: + config: + uri: https://localhost:9000 + fail-fast: true + profiles: + active: local From e5b24f0c4e51743966967699cab9a13438ef1d40 Mon Sep 17 00:00:00 2001 From: Donghyeon Im Date: Thu, 25 Jul 2024 11:40:45 +0900 Subject: [PATCH 20/24] feat: test cicd --- .github/workflows/cicd.yaml | 200 ++++++++++++++++++++++++++++++++++++ Dockerfile | 9 ++ 2 files changed, 209 insertions(+) create mode 100644 .github/workflows/cicd.yaml create mode 100644 Dockerfile diff --git a/.github/workflows/cicd.yaml b/.github/workflows/cicd.yaml new file mode 100644 index 0000000..ef5c798 --- /dev/null +++ b/.github/workflows/cicd.yaml @@ -0,0 +1,200 @@ +name: Create and publish a container image, update helm chart 'appVersion' + +on: + push: + branches: ["main", "develop", "feat/cicd"] + +############################################# +# +# 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' + if: github.ref == 'refs/heads/feat/cicd' + name: Build and Push Container Image to GitHub Container Registry + runs-on: ubuntu-latest + env: + REPOSITORY: admin + ENVIRONMENT: dev + permissions: + contents: read + packages: write + attestations: write + id-token: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Log in to the GitHub container registry + uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) for Container image + id: meta + uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7 + with: + images: ghcr.io/${{ github.repository }} + tags: type=sha + + - name: Set up JDK 21 + uses: actions/setup-java@v2 + with: + distribution: 'adopt' + java-version: '21' + + - name: Build JAR + run: ./gradlew clean build -x test + + - name: Build and push Docker image + id: push + uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + + # ### Error + # # [message] Failed to persist attestation + # # Feature not available for the NTF-marketplace organization. + # # To enable this feature, please upgrade the billing plan, or make this repository public. + # # https://docs.github.com/rest/repos/repos#create-an-attestation + # ### + # - name: Generate artifact attestation + # uses: actions/attest-build-provenance@v1 + # with: + # subject-name: ${{ env.REGISTRY }}/${{ github.repository }} + # subject-digest: ${{ steps.push.outputs.digest }} + # push-to-registry: true + + - name: Checkout Private Repository + uses: actions/checkout@v4 + with: + repository: NTF-marketplace/devops + fetch-depth: 0 + ref: develop + token: ${{ secrets.PAT }} + + - name: Replace image tag in helm values.yaml + uses: mikefarah/yq@master + env: + IMAGE_VERSION: ${{ steps.meta.outputs.version }} + with: + cmd: yq eval -i '.image.tag = env(IMAGE_VERSION)' 'chart/${{ env.REPOSITORY }}_${{ env.ENVIRONMENT }}/values.yaml' + + - name: Commit helm chart changes + env: + IMAGE_VERSION: ${{ steps.meta.outputs.version }} + run: | + cd chart/${{ env.REPOSITORY }}_${{ env.ENVIRONMENT }} + git config --global user.email "hun5879@naver.com" + git config --global user.name "dongdorrong" + + git add values.yaml + git commit --message "ci: update ${{ env.REPOSITORY }}_${{ env.ENVIRONMENT }} image tag to $IMAGE_VERSION" + + - name: Push commit + uses: ad-m/github-push-action@master + with: + github_token: ${{ secrets.PAT }} + repository: NTF-marketplace/devops + branch: develop + + main: + if: github.ref == 'refs/heads/main' + name: Build and Push Container Image to Amazon ECR + runs-on: ubuntu-latest + env: + REPOSITORY: admin + ENVIRONMENT: prod + permissions: + contents: read + packages: write + attestations: write + id-token: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ secrets.AWS_DEFAULT_REGION }} + + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v2 + + - name: Extract metadata (tags, labels) for Container image + id: meta + uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7 + with: + images: ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.${{ secrets.AWS_DEFAULT_REGION }}.amazonaws.com/${{ env.REPOSITORY }}_${{ env.ENVIRONMENT }} + tags: type=sha + + - name: Set up JDK 21 + uses: actions/setup-java@v2 + with: + distribution: 'adopt' + java-version: '21' + + - name: Build JAR + run: ./gradlew clean build -x test + + - name: Build and push Docker image + id: push + uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + + - name: Checkout Private Repository + uses: actions/checkout@v4 + with: + repository: NTF-marketplace/devops + fetch-depth: 0 + ref: develop + token: ${{ secrets.PAT }} + + - name: Replace image tag in helm values.yaml + uses: mikefarah/yq@master + env: + IMAGE_VERSION: ${{ steps.meta.outputs.version }} + with: + cmd: yq eval -i '.image.tag = env(IMAGE_VERSION)' 'chart/${{ env.REPOSITORY }}_${{ env.ENVIRONMENT }}/values.yaml' + + - name: Commit helm chart changes + env: + IMAGE_VERSION: ${{ steps.meta.outputs.version }} + run: | + cd chart/${{ env.REPOSITORY }}_${{ env.ENVIRONMENT }} + git config --global user.email "hun5879@naver.com" + git config --global user.name "dongdorrong" + + git add values.yaml + git commit --message "ci: update ${{ env.REPOSITORY }}_${{ env.ENVIRONMENT }} image tag to $IMAGE_VERSION" + + - name: Push commit + uses: ad-m/github-push-action@master + with: + github_token: ${{ secrets.PAT }} + repository: NTF-marketplace/devops + branch: develop \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1d0f2a8 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,9 @@ +FROM amazoncorretto:21-alpine + +COPY build/libs/*.jar /app.jar + +RUN apk update && apk upgrade && \ + # apk add --no-cache && \ + rm -rf /var/cache/apk/* + +ENTRYPOINT ["java", "-jar", "app.jar"] \ No newline at end of file From 07282d38ee0c1606ffa7ea9f3097021df219ba39 Mon Sep 17 00:00:00 2001 From: Donghyeon Im Date: Thu, 25 Jul 2024 11:46:47 +0900 Subject: [PATCH 21/24] feat: test cicd --- .github/workflows/cicd.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/cicd.yaml b/.github/workflows/cicd.yaml index ef5c798..05d02b0 100644 --- a/.github/workflows/cicd.yaml +++ b/.github/workflows/cicd.yaml @@ -18,8 +18,7 @@ jobs: # 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' - if: github.ref == 'refs/heads/feat/cicd' + if: github.ref == 'refs/heads/develop' name: Build and Push Container Image to GitHub Container Registry runs-on: ubuntu-latest env: @@ -114,7 +113,8 @@ jobs: branch: develop main: - if: github.ref == 'refs/heads/main' + # if: github.ref == 'refs/heads/main' + if: github.ref == 'refs/heads/feat/cicd' name: Build and Push Container Image to Amazon ECR runs-on: ubuntu-latest env: From 0b3e3fde30835e16ba58f85f5b6cf048a599a67e Mon Sep 17 00:00:00 2001 From: Donghyeon Im Date: Thu, 25 Jul 2024 11:51:50 +0900 Subject: [PATCH 22/24] refactor: organize cicd --- .github/workflows/cicd.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/cicd.yaml b/.github/workflows/cicd.yaml index 05d02b0..02f8183 100644 --- a/.github/workflows/cicd.yaml +++ b/.github/workflows/cicd.yaml @@ -113,8 +113,7 @@ jobs: branch: develop main: - # if: github.ref == 'refs/heads/main' - if: github.ref == 'refs/heads/feat/cicd' + if: github.ref == 'refs/heads/main' name: Build and Push Container Image to Amazon ECR runs-on: ubuntu-latest env: From 57e2fd52d5385a0f0167e2932c3d08e8638332bd Mon Sep 17 00:00:00 2001 From: Donghyeon Im Date: Thu, 25 Jul 2024 11:57:11 +0900 Subject: [PATCH 23/24] fix: fix cicd --- .github/workflows/cicd.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cicd.yaml b/.github/workflows/cicd.yaml index 02f8183..6251e27 100644 --- a/.github/workflows/cicd.yaml +++ b/.github/workflows/cicd.yaml @@ -2,7 +2,7 @@ name: Create and publish a container image, update helm chart 'appVersion' on: push: - branches: ["main", "develop", "feat/cicd"] + branches: ["main", "develop"] ############################################# # From bed431ac01eca9985d572d946da80481bf010319 Mon Sep 17 00:00:00 2001 From: min-96 Date: Mon, 29 Jul 2024 23:00:52 +0900 Subject: [PATCH 24/24] fix: infuraTransfer logic --- src/main/kotlin/com/api/admin/service/TransferService.kt | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/com/api/admin/service/TransferService.kt b/src/main/kotlin/com/api/admin/service/TransferService.kt index 000b1df..62cf074 100644 --- a/src/main/kotlin/com/api/admin/service/TransferService.kt +++ b/src/main/kotlin/com/api/admin/service/TransferService.kt @@ -84,8 +84,8 @@ class TransferService( when { log.topics[0] == nativeTransferEventSignature -> handleERC20Transfer(log, wallet, accountType, chainType,TransferType.NATIVE) - log.topics[0] == transferEventSignature && log.topics.size == 3 -> - handleERC20Transfer(log, wallet, accountType, chainType, TransferType.ERC20) +// log.topics[0] == transferEventSignature && log.topics.size == 3 -> +// handleERC20Transfer(log, wallet, accountType, chainType, TransferType.ERC20) log.topics[0] == transferEventSignature && log.topics.size == 4 -> handleERC721Transfer(log, wallet, accountType, chainType) else -> Mono.empty() @@ -98,12 +98,10 @@ class TransferService( val to = parseAddress(log.topics[3]) val amount = when (transferType) { TransferType.NATIVE -> parseNativeTransferAmount(log.data) - TransferType.ERC20 -> toBigDecimal(log.data) +// TransferType.ERC20 -> toBigDecimal(log.data) else -> BigDecimal.ZERO } - println("amount : " + amount) - val isRelevantTransfer = when (accountType) { AccountType.DEPOSIT -> from.equals(wallet, ignoreCase = true) && to.equals(adminInfoProperties.address, ignoreCase = true) AccountType.WITHDRAW -> from.equals(adminInfoProperties.address, ignoreCase = true) && to.equals(wallet, ignoreCase = true)