diff --git a/src/main/java/com/nynja/walletservice/WalletServiceApplication.java b/src/main/java/com/nynja/walletservice/WalletServiceApplication.java index 633e4f60295261141466382226fda2e0347994fc..2416af03e9e33a1e6ef7730ea2c4c28a8cd9d155 100644 --- a/src/main/java/com/nynja/walletservice/WalletServiceApplication.java +++ b/src/main/java/com/nynja/walletservice/WalletServiceApplication.java @@ -1,12 +1,16 @@ package com.nynja.walletservice; +import com.nynja.walletservice.service.listener.TransferEventHandler; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.FilterType; +import org.web3j.spring.autoconfigure.Web3jAutoConfiguration; -@SpringBootApplication +@SpringBootApplication(exclude = Web3jAutoConfiguration.class) +@ComponentScan(excludeFilters=@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = TransferEventHandler.class)) public class WalletServiceApplication { - public static void main(String[] args) { SpringApplication.run(WalletServiceApplication.class, args); } diff --git a/src/main/java/com/nynja/walletservice/blockexplorer/EtherScanConsumer.java b/src/main/java/com/nynja/walletservice/blockexplorer/EtherScanConsumer.java index b030db340c6031b61f289c20c2a2939a39c70764..dd93ee9cd05943627484f65c1242855b2b3d362c 100644 --- a/src/main/java/com/nynja/walletservice/blockexplorer/EtherScanConsumer.java +++ b/src/main/java/com/nynja/walletservice/blockexplorer/EtherScanConsumer.java @@ -2,9 +2,10 @@ package com.nynja.walletservice.blockexplorer; import com.nynja.walletservice.blockexplorer.dto.Response; import com.nynja.walletservice.blockexplorer.dto.TxListItemDto; +import com.nynja.walletservice.config.ConfigValues; import com.nynja.walletservice.constant.enums.Sort; +import com.nynja.walletservice.model.network.NetworkProperties; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; @@ -18,13 +19,12 @@ import static org.apache.commons.lang3.StringUtils.isBlank; @Slf4j public class EtherScanConsumer { - @Value("${etherscan.url}") - private String etherScanUrl; + private ConfigValues conf; - @Value("${etherscan.apikey}") - private String apikey; + public EtherScanConsumer(ConfigValues conf) { + this.conf = conf; + } - //TODO Use ChainId to select etherscan urls public Response getTxHistory(String address, String chainId, String contractAddress, String page, String offset, Sort sort) { if (isBlank(contractAddress)) { return getTransactionsByAddress("txlist", address, chainId, page, offset, sort); @@ -51,15 +51,16 @@ public class EtherScanConsumer { return makeGetRequest(prepareBuilder(action, chainId, address, page, offset, sort)); } - //TODO Add differentiation between networks private UriComponentsBuilder prepareBuilder(String action, String chainId, String address, String page, String offset, Sort sort) { - UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(etherScanUrl) + NetworkProperties props = conf.getNetworks().get(chainId); + + UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(props.getBlockExplorerUrl()) .queryParam("module", "account") .queryParam("action", action) .queryParam("address", address) .queryParam("startblock", "0") .queryParam("endblock", "99999999999") - .queryParam("apikey", apikey) + .queryParam("apikey", props.getBlockExplorerApiKey()) .queryParam("sort", sort != null ? sort.getSort() : Sort.ASC.getSort()); if (!isBlank(page)) { diff --git a/src/main/java/com/nynja/walletservice/config/ConfigValues.java b/src/main/java/com/nynja/walletservice/config/ConfigValues.java index 072cc9687ad50164b8513b34e80c5d9edd9271f5..b0172b03410d47654435d08278cef55d3ebf22e0 100644 --- a/src/main/java/com/nynja/walletservice/config/ConfigValues.java +++ b/src/main/java/com/nynja/walletservice/config/ConfigValues.java @@ -1,29 +1,29 @@ package com.nynja.walletservice.config; +import com.nynja.walletservice.model.network.NetworkProperties; import lombok.Getter; +import lombok.Setter; import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Configuration; import java.math.BigInteger; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import static com.nynja.walletservice.constant.Constants.LogMessages.CHAIN_ID_NOT_FOUND; +import static com.nynja.walletservice.constant.Constants.newExceptionWithMessage; + +@ConfigurationProperties(prefix = "ethereum") +@EnableConfigurationProperties @Configuration +@Setter @Getter public class ConfigValues { - @Value("${ethereum.client.url}") - private String clientUrl; - - @Value("${ethereum.admin.address}") - private String adminAddress; - - @Value("${ethereum.admin.private-key}") - private String adminPrivateKey; - - @Value("${ethereum.contract.gas-price}") - private Long gasPrice; - - @Value("${ethereum.contract.gas-limit}") - private Long gasLimit; + private Map networks = new HashMap<>(); @Value("${web3j.attempts}") private Integer attempts; @@ -37,11 +37,9 @@ public class ConfigValues { @Value("${ethereum.event.margin}") private BigInteger blockSubscriptionMargin; - public BigInteger gasPrice() { - return BigInteger.valueOf(gasPrice); - } - - public BigInteger gasLimit() { - return BigInteger.valueOf(gasLimit); + public NetworkProperties getNetworkProps(String chainId) { + return Optional.ofNullable(networks.get(chainId)) + .orElseThrow(newExceptionWithMessage(CHAIN_ID_NOT_FOUND, chainId)); } } + diff --git a/src/main/java/com/nynja/walletservice/config/EthereumConfig.java b/src/main/java/com/nynja/walletservice/config/EthereumConfig.java deleted file mode 100644 index 8a14aa91e377c2b45a4d7343ff6dff1bfea90bb3..0000000000000000000000000000000000000000 --- a/src/main/java/com/nynja/walletservice/config/EthereumConfig.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.nynja.walletservice.config; - -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import okhttp3.OkHttpClient; -import org.springframework.context.ApplicationListener; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Scope; -import org.springframework.context.event.ContextRefreshedEvent; -import org.web3j.crypto.Credentials; -import org.web3j.protocol.Web3j; -import org.web3j.protocol.http.HttpService; -import org.web3j.utils.Async; - -@Configuration -@Getter -@Slf4j -public class EthereumConfig implements ApplicationListener { - - private ConfigValues conf; - private Credentials adminCredentials; - - public EthereumConfig(ConfigValues conf) { - this.conf = conf; - } - - @Override - public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) { - this.adminCredentials = Credentials.create(conf.getAdminPrivateKey()); - } - - @Bean - @Scope("singleton") - public Web3j web3j() { - log.info("BUILDING Web3j INSTANCE FOR URL: {}", conf.getClientUrl()); - - return Web3j.build(new HttpService( - conf.getClientUrl(), - new OkHttpClient.Builder().build(), - false - ), 1000L, Async.defaultExecutorService()); - } -} diff --git a/src/main/java/com/nynja/walletservice/config/Web3jWarmUp.java b/src/main/java/com/nynja/walletservice/config/Web3jWarmUp.java index c43cdf4739ea7e97b58de3ca33617b7b59ce3a60..984ddfc6d59aa64b9244aacc4461311be1743152 100644 --- a/src/main/java/com/nynja/walletservice/config/Web3jWarmUp.java +++ b/src/main/java/com/nynja/walletservice/config/Web3jWarmUp.java @@ -25,13 +25,13 @@ public class Web3jWarmUp implements ApplicationListener { @Override public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) { - log.info("Warming up the Web3j client"); + /*log.info("Warming up the Web3j client"); web3jProvider.get().ethGasPrice().sendAsync(); web3JService.getBalance(configValues.getAdminAddress()) .thenAcceptAsync(log::info) .exceptionally(ex -> { log.error(ex.getMessage(), ex); return null; - }); + });*/ } } diff --git a/src/main/java/com/nynja/walletservice/constant/Constants.java b/src/main/java/com/nynja/walletservice/constant/Constants.java index 1e720000a029b6b44adeb87eca5f50fd61e5d3bf..1ce15ee8850f904c7ac75e952d8df9c119307a3a 100644 --- a/src/main/java/com/nynja/walletservice/constant/Constants.java +++ b/src/main/java/com/nynja/walletservice/constant/Constants.java @@ -1,5 +1,7 @@ package com.nynja.walletservice.constant; +import java.util.function.Supplier; + /** * @author Oleg Zhymolokhov (oleg.zhimolokhov@dataart.com) */ @@ -44,6 +46,7 @@ public interface Constants { String SIGNED_TRANSACTION_SENT = "The signed transaction has been sent. Hash: {}"; String SIGNED_TRANSACTION_FAIL = "Failed to send signed transaction. Reason: %s"; String TRANSACTION_NOT_FOUND = "Transaction with the hash %s is not found"; + String CHAIN_ID_NOT_FOUND = "Wrong chainId (%s) or the configuration for this network is missing."; } interface ValidationMessages { @@ -52,4 +55,12 @@ public interface Constants { String HASH_VALIDATION_MESSAGE = "Hash should be correct"; } + static Supplier newExceptionWithMessage(String message, Object ... args) { + return () -> new RuntimeException(String.format(message, args)); + } + + static Supplier newExceptionWithMessage(String message) { + return () -> new RuntimeException(message); + } + } diff --git a/src/main/java/com/nynja/walletservice/controller/ContractController.java b/src/main/java/com/nynja/walletservice/controller/ContractController.java index 8382a69de1ba445be635ff9baa580e035fe228e9..e81d57be1a1d032246db85c37c09d006d6a5722b 100644 --- a/src/main/java/com/nynja/walletservice/controller/ContractController.java +++ b/src/main/java/com/nynja/walletservice/controller/ContractController.java @@ -5,8 +5,10 @@ import com.nynja.walletservice.provider.ContractProvider; import com.nynja.walletservice.service.TokenService; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiResponse; +import org.hibernate.validator.constraints.NotBlank; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; @@ -16,6 +18,7 @@ import java.util.concurrent.CompletableFuture; import static com.nynja.walletservice.constant.Constants.RestApi.API_VERSION; import static com.nynja.walletservice.constant.Constants.RestApi.RETRIEVED; +import static com.nynja.walletservice.constant.Constants.ValidationMessages.CHAIN_ID_VALIDATION_MESSAGE; import static com.nynja.walletservice.controller.ControllerHelper.handleAndRespondAsync; /** @@ -24,6 +27,7 @@ import static com.nynja.walletservice.controller.ControllerHelper.handleAndRespo @RestController @RequestMapping("/contract") +@Validated public class ContractController { private final TokenService tokenService; @@ -38,24 +42,28 @@ public class ContractController { @ApiOperation(value = "Endpoint for NynjaCoin contract deployment into Blockchain network", httpMethod = "GET") @ApiResponse(code = 200, message = RETRIEVED) @GetMapping(API_VERSION + "/deploy") - public CompletableFuture> deployNynjaCoin() { - return tokenService.deployTokenContract().thenApplyAsync(ResponseEntity::ok); + public CompletableFuture> deployNynjaCoin( + @NotBlank(message = CHAIN_ID_VALIDATION_MESSAGE) @RequestParam String chainId + ) { + return tokenService.deployTokenContract(chainId).thenApplyAsync(ResponseEntity::ok); } @ApiOperation(value = "Endpoint for NynjaCoin contract address obtaining", httpMethod = "GET") @ApiResponse(code = 200, message = RETRIEVED) @GetMapping(API_VERSION + "/nyn-address") - public CompletableFuture> getCurrentNynAddress() { - return handleAndRespondAsync(() -> NynContractAddressesDto.builder() - .runtimeAddress(contractProvider.getNynjaAddress()) - .dbAddress(contractProvider.getDBContractAddress()) - .build()); + public CompletableFuture> getCurrentNynAddress( + @NotBlank(message = CHAIN_ID_VALIDATION_MESSAGE) @RequestParam String chainId + ) { + return handleAndRespondAsync(() -> contractProvider.getCurrentContractAddresses(chainId)); } @ApiOperation(value = "Endpoint for NynjaCoin contract address updating", httpMethod = "GET") @ApiResponse(code = 200, message = RETRIEVED) @GetMapping(API_VERSION + "/update-nyn-address") - public CompletableFuture> updateNynAddress(@RequestParam String address) { - return handleAndRespondAsync(() -> contractProvider.updateContractAddress(address)); + public CompletableFuture> updateNynAddress( + @NotBlank(message = CHAIN_ID_VALIDATION_MESSAGE) @RequestParam String chainId, + @RequestParam String address + ) { + return handleAndRespondAsync(() -> contractProvider.updateContractAddress(chainId, address)); } } diff --git a/src/main/java/com/nynja/walletservice/controller/EthereumController.java b/src/main/java/com/nynja/walletservice/controller/EthereumController.java index f205f4e9ceb3404428d81dd7a17fe61fa8591b0f..3156f6ca0640b4b5356fa949d20307d801da2fb7 100644 --- a/src/main/java/com/nynja/walletservice/controller/EthereumController.java +++ b/src/main/java/com/nynja/walletservice/controller/EthereumController.java @@ -1,6 +1,6 @@ package com.nynja.walletservice.controller; -import com.nynja.walletservice.config.EthereumConfig; +import com.nynja.walletservice.config.ConfigValues; import com.nynja.walletservice.dto.TransactionResponseDto; import com.nynja.walletservice.service.Web3JService; import io.swagger.annotations.*; @@ -10,6 +10,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; +import org.web3j.crypto.Credentials; import org.web3j.utils.Convert; import java.math.BigDecimal; @@ -20,6 +21,7 @@ import static com.nynja.walletservice.constant.Constants.DataTypes.STRING; import static com.nynja.walletservice.constant.Constants.ParamTypes.Query; import static com.nynja.walletservice.constant.Constants.RestApi.*; import static com.nynja.walletservice.constant.Constants.ValidationMessages.ADDRESS_VALIDATION_MESSAGE; +import static com.nynja.walletservice.constant.Constants.ValidationMessages.CHAIN_ID_VALIDATION_MESSAGE; /** * @author Oleg Zhymolokhov (oleg.zhimolokhov@dataart.com) @@ -32,17 +34,19 @@ import static com.nynja.walletservice.constant.Constants.ValidationMessages.ADDR public class EthereumController { private final Web3JService web3JService; - private final EthereumConfig ethConfig; + private final ConfigValues conf; @Autowired - public EthereumController(Web3JService web3JService, EthereumConfig ethConfig) { + public EthereumController(Web3JService web3JService, ConfigValues conf) { this.web3JService = web3JService; - this.ethConfig = ethConfig; + this.conf = conf; } @GetMapping(API_VERSION + "/version") - public CompletableFuture> checkClientVersion() { - return web3JService.getClientVersion() + public CompletableFuture> checkClientVersion( + @NotBlank(message = CHAIN_ID_VALIDATION_MESSAGE) @RequestParam String chainId + ) { + return web3JService.getClientVersion(chainId) .thenApplyAsync(ResponseEntity::ok); } @@ -53,8 +57,11 @@ public class EthereumController { @ApiResponse(code = 400, message = REQUIRED_PARAM_MISSING) }) @GetMapping(API_VERSION + "/address-balance") - public CompletableFuture> getBalanceForAddress(@NotBlank(message = ADDRESS_VALIDATION_MESSAGE) @RequestParam String address) { - return web3JService.getBalance(address) + public CompletableFuture> getBalanceForAddress( + @NotBlank(message = CHAIN_ID_VALIDATION_MESSAGE) @RequestParam String chainId, + @NotBlank(message = ADDRESS_VALIDATION_MESSAGE) @RequestParam String address + ) { + return web3JService.getBalance(chainId, address) .thenApplyAsync(ResponseEntity::ok); } @@ -68,9 +75,14 @@ public class EthereumController { @ApiResponse(code = 400, message = REQUIRED_PARAM_MISSING) }) @GetMapping(API_VERSION + "/eth-send") - public CompletableFuture>> sendEthToAddress(@NotBlank(message = ADDRESS_VALIDATION_MESSAGE) @RequestParam String address, @RequestParam long amount) { + public CompletableFuture>> sendEthToAddress( + @NotBlank(message = CHAIN_ID_VALIDATION_MESSAGE) @RequestParam String chainId, + @NotBlank(message = ADDRESS_VALIDATION_MESSAGE) @RequestParam String address, + @RequestParam BigDecimal amount + ) { + Credentials adminCredentials = Credentials.create(conf.getNetworkProps(chainId).getAdminPrivateKey()); return web3JService - .sendEtherAsync(ethConfig.getAdminCredentials(), address, BigDecimal.valueOf(amount), Convert.Unit.ETHER) + .sendEtherAsync(chainId, adminCredentials, address, amount, Convert.Unit.ETHER) .thenApplyAsync(ResponseEntity::ok); } } diff --git a/src/main/java/com/nynja/walletservice/controller/TokenController.java b/src/main/java/com/nynja/walletservice/controller/TokenController.java index 57187054939b70c720e4d6c13d64016545d3be6b..fb89a38feb478af37bd942e51cc7e8d553a7a6ec 100644 --- a/src/main/java/com/nynja/walletservice/controller/TokenController.java +++ b/src/main/java/com/nynja/walletservice/controller/TokenController.java @@ -3,6 +3,7 @@ package com.nynja.walletservice.controller; import com.nynja.walletservice.dto.TransactionResponseDto; import com.nynja.walletservice.service.TokenService; import io.swagger.annotations.*; +import org.hibernate.validator.constraints.NotBlank; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -11,6 +12,9 @@ import java.util.concurrent.CompletableFuture; import static com.nynja.walletservice.constant.Constants.DataTypes.STRING; import static com.nynja.walletservice.constant.Constants.ParamTypes.Query; +import static com.nynja.walletservice.constant.Constants.RestApi.API_VERSION; +import static com.nynja.walletservice.constant.Constants.RestApi.REQUIRED_PARAM_MISSING; +import static com.nynja.walletservice.constant.Constants.ValidationMessages.CHAIN_ID_VALIDATION_MESSAGE; import static com.nynja.walletservice.constant.Constants.RestApi.*; /** @@ -38,8 +42,11 @@ public class TokenController { @ApiResponse(code = 400, message = REQUIRED_PARAM_MISSING) }) @GetMapping(API_VERSION + "/fund-address") - public CompletableFuture>> fundAddress(@RequestParam String address) { - return tokenService.handleAccountCreation(address) + public CompletableFuture>> fundAddress( + @NotBlank(message = CHAIN_ID_VALIDATION_MESSAGE) @RequestParam String chainId, + @RequestParam String address + ) { + return tokenService.handleAccountCreation(chainId, address) .thenApplyAsync(transactionResponseDto -> ResponseEntity.status(202).body(transactionResponseDto)); } } diff --git a/src/main/java/com/nynja/walletservice/controller/TransactionController.java b/src/main/java/com/nynja/walletservice/controller/TransactionController.java index 4e7587aa35fbf74f97f1474369cbeaeff7b178d2..cc6f5bb7cbc518d6f29bd741e227a9a9f35faa9a 100644 --- a/src/main/java/com/nynja/walletservice/controller/TransactionController.java +++ b/src/main/java/com/nynja/walletservice/controller/TransactionController.java @@ -52,29 +52,41 @@ public class TransactionController { @ApiResponse(code = 200, message = RETRIEVED) }) @GetMapping(API_VERSION + "/balance-for-address") - public CompletableFuture> getBalanceByAddress(@NotBlank(message = ADDRESS_VALIDATION_MESSAGE) @RequestParam String walletAddress) { - return tokenService.tokenBalanceByAddress(walletAddress).thenApplyAsync(ResponseEntity::ok); + public CompletableFuture> getBalanceByAddress( + @NotBlank(message = CHAIN_ID_VALIDATION_MESSAGE) @RequestParam String chainId, + @NotBlank(message = ADDRESS_VALIDATION_MESSAGE) @RequestParam String walletAddress + ) { + return tokenService.tokenBalanceByAddress(chainId, walletAddress).thenApplyAsync(ResponseEntity::ok); } @ApiOperation(value = "Endpoint for sending signed transactions", httpMethod = "POST") @ApiResponse(code = 200, message = RETRIEVED) @PostMapping(API_VERSION + "/signed-transaction-send") - public CompletableFuture>> sendSignedTransaction(@Valid @RequestBody SignedTransactionDto dto) { - return web3JService.sendSignedTransaction(dto.getSignedTransaction()).thenApplyAsync(ResponseEntity::ok); + public CompletableFuture>> sendSignedTransaction( + @NotBlank(message = CHAIN_ID_VALIDATION_MESSAGE) @RequestParam String chainId, + @Valid @RequestBody SignedTransactionDto dto + ) { + return web3JService.sendSignedTransaction(chainId, dto.getSignedTransaction()).thenApplyAsync(ResponseEntity::ok); } @ApiOperation(value = "Endpoint for getting transaction info by hash", httpMethod = "GET") @ApiResponse(code = 200, message = RETRIEVED) @GetMapping(API_VERSION + "/single-transaction-info") - public CompletableFuture> getSingleTransactionInfo(@NotBlank(message = HASH_VALIDATION_MESSAGE) @RequestParam String hash) { - return web3JService.getSingleTransactionInfo(hash).thenApplyAsync(ResponseEntity::ok); + public CompletableFuture> getSingleTransactionInfo( + @NotBlank(message = CHAIN_ID_VALIDATION_MESSAGE) @RequestParam String chainId, + @NotBlank(message = HASH_VALIDATION_MESSAGE) @RequestParam String hash + ) { + return web3JService.getSingleTransactionInfo(chainId, hash).thenApplyAsync(ResponseEntity::ok); } @ApiOperation(value = "Endpoint for obtaining the latest nonce", httpMethod = "GET") @ApiResponse(code = 200, message = RETRIEVED) @GetMapping(API_VERSION + "/get-transaction-count") - public CompletableFuture> getTransactionCount(@NotBlank(message = ADDRESS_VALIDATION_MESSAGE) @RequestParam String address) { - return web3JService.getTransactionCount(address).thenApplyAsync(ResponseEntity::ok); + public CompletableFuture> getTransactionCount( + @NotBlank(message = CHAIN_ID_VALIDATION_MESSAGE) @RequestParam String chainId, + @NotBlank(message = ADDRESS_VALIDATION_MESSAGE) @RequestParam String address + ) { + return web3JService.getTransactionCount(chainId, address).thenApplyAsync(ResponseEntity::ok); } @ApiOperation(value = "Endpoint for obtaining tx history by address. Internal transactions", httpMethod = "GET") @@ -104,7 +116,10 @@ public class TransactionController { @ApiOperation(value = "Endpoint for transaction gas price estimation", httpMethod = "POST") @ApiResponse(code = 200, message = RETRIEVED) @PostMapping(API_VERSION + "/estimate-gas-price") - public CompletableFuture> estimateGasPrice(@RequestBody EstimateGasRequestDto dto) { - return web3JService.estimateGasPrice(dto).thenApplyAsync(ResponseEntity::ok); + public CompletableFuture> estimateGasPrice( + @NotBlank(message = CHAIN_ID_VALIDATION_MESSAGE) @RequestParam String chainId, + @RequestBody EstimateGasRequestDto dto + ) { + return web3JService.estimateGasPrice(chainId, dto).thenApplyAsync(ResponseEntity::ok); } } diff --git a/src/main/java/com/nynja/walletservice/dto/AccountScanDto.java b/src/main/java/com/nynja/walletservice/dto/AccountScanDto.java index ddf9d393442cd4b7c5ce59e15015cd1cc407d8f6..9ca81d2e24bb0b8ca020aca386ccbefbc7bfd6b3 100644 --- a/src/main/java/com/nynja/walletservice/dto/AccountScanDto.java +++ b/src/main/java/com/nynja/walletservice/dto/AccountScanDto.java @@ -6,6 +6,8 @@ import lombok.Data; import lombok.NoArgsConstructor; import org.hibernate.validator.constraints.NotBlank; +import static com.nynja.walletservice.constant.Constants.ValidationMessages.CHAIN_ID_VALIDATION_MESSAGE; + @Data @NoArgsConstructor @AllArgsConstructor @@ -16,4 +18,6 @@ public class AccountScanDto { private String publicKey; @NotBlank(message = "ChainCode should be present.") private String chainCode; + @NotBlank(message = CHAIN_ID_VALIDATION_MESSAGE) + private String chainId; } diff --git a/src/main/java/com/nynja/walletservice/dto/EstimateGasRequestDto.java b/src/main/java/com/nynja/walletservice/dto/EstimateGasRequestDto.java index 78cdc337eacba67756d02ea4da449061f9635654..adfd2536988ae320b10f6cb0bd7280c6a1577c20 100644 --- a/src/main/java/com/nynja/walletservice/dto/EstimateGasRequestDto.java +++ b/src/main/java/com/nynja/walletservice/dto/EstimateGasRequestDto.java @@ -15,4 +15,5 @@ public class EstimateGasRequestDto { private BigInteger gas; private BigInteger value; private String data; + private String chainId; } diff --git a/src/main/java/com/nynja/walletservice/dto/SignedTransactionDto.java b/src/main/java/com/nynja/walletservice/dto/SignedTransactionDto.java index 01537d75ab5242fea5737e81dc6aaf236f51ae66..9fe0a6daff9f28d8c36a0e997b73eaf5f7780e48 100644 --- a/src/main/java/com/nynja/walletservice/dto/SignedTransactionDto.java +++ b/src/main/java/com/nynja/walletservice/dto/SignedTransactionDto.java @@ -16,4 +16,6 @@ public class SignedTransactionDto { @NotBlank private String signedTransaction; + @NotBlank + private String chainId; } diff --git a/src/main/java/com/nynja/walletservice/dto/WalletImportDto.java b/src/main/java/com/nynja/walletservice/dto/WalletImportDto.java index 58e417c494c308fa7e6dc2ce23d67621aa28564b..17ffbf09310886a54917c8793e9cacf9d730b3b8 100644 --- a/src/main/java/com/nynja/walletservice/dto/WalletImportDto.java +++ b/src/main/java/com/nynja/walletservice/dto/WalletImportDto.java @@ -9,6 +9,8 @@ import org.hibernate.validator.constraints.NotBlank; import javax.validation.constraints.NotNull; +import static com.nynja.walletservice.constant.Constants.ValidationMessages.CHAIN_ID_VALIDATION_MESSAGE; + @Data @NoArgsConstructor @AllArgsConstructor @@ -21,4 +23,7 @@ public class WalletImportDto { @NotNull(message = "Currency should be provided.") private Currency currency; + + @NotBlank(message = CHAIN_ID_VALIDATION_MESSAGE) + private String chainId; } diff --git a/src/main/java/com/nynja/walletservice/model/ContractData.java b/src/main/java/com/nynja/walletservice/model/ContractData.java index c4f2081b6d1bf7b2698f24b63a97db216abfcd50..3de11608195a996cf48c30e076209e59966eb497 100644 --- a/src/main/java/com/nynja/walletservice/model/ContractData.java +++ b/src/main/java/com/nynja/walletservice/model/ContractData.java @@ -12,5 +12,6 @@ import javax.persistence.Entity; @Builder public class ContractData extends MainEntity { + private String chainId; private String address; } diff --git a/src/main/java/com/nynja/walletservice/model/FundedAddress.java b/src/main/java/com/nynja/walletservice/model/FundedAddress.java index 5272637826f5fe1963ea2d312c8ddf7146a40a8c..8d8ba52cfc7256ff495b1957aaa3fb451d68e880 100644 --- a/src/main/java/com/nynja/walletservice/model/FundedAddress.java +++ b/src/main/java/com/nynja/walletservice/model/FundedAddress.java @@ -13,6 +13,7 @@ import javax.persistence.Entity; @AllArgsConstructor @NoArgsConstructor public class FundedAddress extends MainEntity { + private String chainId; private String address; private String amount; } diff --git a/src/main/java/com/nynja/walletservice/model/network/NetworkProperties.java b/src/main/java/com/nynja/walletservice/model/network/NetworkProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..3db862c80e7dfdfad880339b6e82f376f9fbb93d --- /dev/null +++ b/src/main/java/com/nynja/walletservice/model/network/NetworkProperties.java @@ -0,0 +1,18 @@ +package com.nynja.walletservice.model.network; + +import lombok.Data; + +import java.math.BigInteger; + +@Data +public class NetworkProperties { + private String url; + private String blockExplorerUrl; + private String blockExplorerApiKey; + private String adminAddress; + private String adminPrivateKey; + private String nynjaCoinAddress; + private boolean nynjaCoinListenEvents; + private BigInteger gasPrice; + private BigInteger gasLimit; +} diff --git a/src/main/java/com/nynja/walletservice/provider/ContractProvider.java b/src/main/java/com/nynja/walletservice/provider/ContractProvider.java index 2a4efd68a48b55801515a63b1cd504e945af9203..c646617df80d0ac1af4ce35c6ff5555892eb2271 100644 --- a/src/main/java/com/nynja/walletservice/provider/ContractProvider.java +++ b/src/main/java/com/nynja/walletservice/provider/ContractProvider.java @@ -1,13 +1,14 @@ package com.nynja.walletservice.provider; import com.nynja.walletservice.config.ConfigValues; +import com.nynja.walletservice.dto.NynContractAddressesDto; import com.nynja.walletservice.model.ContractData; +import com.nynja.walletservice.model.network.NetworkProperties; import com.nynja.walletservice.repository.ContractDataRepository; import com.nynja.walletservice.wrapper.NynjaCoin; -import lombok.Getter; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.web3j.crypto.Credentials; import org.web3j.protocol.Web3j; @@ -17,6 +18,8 @@ import org.web3j.tx.RawTransactionManager; import java.util.Optional; import java.util.concurrent.CompletableFuture; +import static com.nynja.walletservice.constant.Constants.newExceptionWithMessage; + /** * @author Oleg Zhymolokhov (oleg.zhimolokhov@dataart.com) */ @@ -25,10 +28,6 @@ import java.util.concurrent.CompletableFuture; @Slf4j public class ContractProvider { - @Value("${ethereum.contract.nynja-coin-address}") - @Getter - private String nynjaAddress; - private final ConfigValues conf; private final Web3jProvider web3jProvider; private final ContractDataRepository contractRepository; @@ -40,48 +39,54 @@ public class ContractProvider { this.contractRepository = contractRepository; } - private RawTransactionManager getTrManager(Credentials credentials) { - return new RawTransactionManager(web3jProvider.get(), credentials, conf.getAttempts(), conf.getInterval()); + private RawTransactionManager getTrManager(Web3j web3j, Credentials credentials) { + return new RawTransactionManager(web3j, credentials, conf.getAttempts(), conf.getInterval()); } - public CompletableFuture deployNynjaCoinContract() { - try { - return NynjaCoin.deploy( - web3jProvider.get(), - getTrManager(Credentials.create(conf.getAdminPrivateKey())), - conf.gasPrice(), - conf.gasLimit() - ).sendAsync() - .thenApplyAsync(Contract::getContractAddress) - .thenApplyAsync(this::persistContractAddress); - } catch (Exception e) { - throw new RuntimeException("Failed to deploy the Nynja contract.", e); - } + public CompletableFuture deployNynjaCoinContract(String chainId) { + NetworkProperties networkProperties = conf.getNetworkProps(chainId); + + return Optional.ofNullable(web3jProvider.getInstance(chainId)) + .map(web3j -> { + try { + return NynjaCoin.deploy( + web3j, + getTrManager(web3j, Credentials.create(networkProperties.getAdminPrivateKey())), + networkProperties.getGasPrice(), + networkProperties.getGasLimit() + ).sendAsync() + .thenApplyAsync(Contract::getContractAddress) + .thenApplyAsync(address -> { + networkProperties.setNynjaCoinAddress(address); + return persistContractAddress(chainId, address); + }); + } catch (Exception e) { + throw new RuntimeException("Failed to deploy the Nynja contract.", e); + } + }).orElseThrow(newExceptionWithMessage("Wrong value or the web3j is not configured for this chainId.")); } - private String persistContractAddress(String address) { - this.nynjaAddress = address; - - ContractData data = ContractData.builder().address(address).build(); - data.setId("1L"); - - return contractRepository.save(data).getAddress(); + private String persistContractAddress(String chainId, String address) { + return contractRepository.save(ContractData.builder().chainId(chainId).address(address).build()).getAddress(); } - public NynjaCoin getNynjaCoin() { - return getNynjaCoin(Credentials.create(conf.getAdminPrivateKey())); + public NynjaCoin getNynjaCoin(String chainId) { + return getNynjaCoin(chainId, Credentials.create(conf.getNetworkProps(chainId).getAdminPrivateKey())); } - public NynjaCoin getNynjaCoin(Credentials credentials) { - return loadNynjaCoin(web3jProvider.get(), credentials); + public NynjaCoin getNynjaCoin(String chainId, Credentials credentials) { + return loadNynjaCoin(web3jProvider.getInstance(chainId), chainId, credentials); } - private NynjaCoin loadNynjaCoin(Web3j web3j, Credentials credentials) { - String address = Optional.ofNullable(contractRepository.findOne("1L")) + private NynjaCoin loadNynjaCoin(Web3j web3j, String chainId, Credentials credentials) { + NetworkProperties networkProperties = conf.getNetworkProps(chainId); + + String address = contractRepository.findByChainId(chainId) .map(ContractData::getAddress) - .orElseGet(() -> Optional.of(nynjaAddress) - .filter(propertyAddress -> !propertyAddress.equals("")) - .map(this::persistContractAddress) + .orElseGet(() -> Optional.of(networkProperties) + .map(NetworkProperties::getNynjaCoinAddress) + .filter(StringUtils::isNotBlank) + .map(contractAddress -> persistContractAddress(chainId, contractAddress)) .orElseThrow(() -> { log.error("Deploy Nynja contract first and put it's address into application.yml"); return new RuntimeException("Nynja Contract is not deployed!"); @@ -90,21 +95,30 @@ public class ContractProvider { return NynjaCoin.load( address, web3j, - getTrManager(credentials), - conf.gasPrice(), - conf.gasLimit() + getTrManager(web3j, credentials), + networkProperties.getGasPrice(), + networkProperties.getGasLimit() ); } - public String getDBContractAddress() { - return contractRepository.findOne("1L").getAddress(); + public NynContractAddressesDto getCurrentContractAddresses(String chainId) { + return NynContractAddressesDto.builder() + .runtimeAddress(conf.getNetworkProps(chainId).getNynjaCoinAddress()) + .dbAddress(getDBContractAddress(chainId)) + .build(); + } + + public String getDBContractAddress(String chainId) { + return contractRepository.findByChainId(chainId) + .map(ContractData::getAddress) + .orElse(null); } - public String updateContractAddress(String nynjaAddress) { - return Optional.ofNullable(contractRepository.findOne("1L")) - .map(address -> { - address.setAddress(nynjaAddress); - return contractRepository.save(address).getAddress(); - }).orElseThrow(() -> new RuntimeException("There is no contract address in the DB")); + public String updateContractAddress(String chainId, String nynjaAddress) { + return contractRepository.findByChainId(chainId) + .map(contractData -> { + contractData.setAddress(nynjaAddress); + return contractRepository.save(contractData).getAddress(); + }).orElseThrow(newExceptionWithMessage("There is no contract address in the DB")); } } diff --git a/src/main/java/com/nynja/walletservice/provider/Web3jProvider.java b/src/main/java/com/nynja/walletservice/provider/Web3jProvider.java index 4c8de430cf615ea50c174f8361cc47d68881a691..29ff6773f0628863aea36f87c4c3e06ccd22d69b 100644 --- a/src/main/java/com/nynja/walletservice/provider/Web3jProvider.java +++ b/src/main/java/com/nynja/walletservice/provider/Web3jProvider.java @@ -1,12 +1,53 @@ package com.nynja.walletservice.provider; -import org.springframework.beans.factory.annotation.Lookup; +import com.nynja.walletservice.config.ConfigValues; +import lombok.extern.slf4j.Slf4j; +import okhttp3.OkHttpClient; +import org.springframework.context.ApplicationListener; +import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.stereotype.Component; import org.web3j.protocol.Web3j; +import org.web3j.protocol.http.HttpService; +import org.web3j.utils.Async; + +import java.util.HashMap; +import java.util.Map; + +import static org.apache.commons.lang3.StringUtils.isBlank; @Component -public abstract class Web3jProvider { +@Slf4j +public class Web3jProvider implements ApplicationListener { + + private Map instances; + + private ConfigValues conf; + + public Web3jProvider(ConfigValues conf) { + this.conf = conf; + } + + @Override + public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) { + instances = new HashMap<>(); + conf.getNetworks().forEach((k, v) -> instances.put(k, buildInstance(v.getUrl()))); + } + + private Web3j buildInstance(String clientUrl) { + log.info("BUILDING Web3j INSTANCE FOR URL: {}", clientUrl); + + return Web3j.build(new HttpService( + clientUrl, + new OkHttpClient.Builder().build(), + false + ), 1000L, Async.defaultExecutorService()); + } - @Lookup - public abstract Web3j get(); + public Web3j getInstance(String chainId) { + if (isBlank(chainId)) { + return instances.get("4"); + } else { + return instances.get(chainId); + } + } } diff --git a/src/main/java/com/nynja/walletservice/repository/ContractDataRepository.java b/src/main/java/com/nynja/walletservice/repository/ContractDataRepository.java index 81134ccdc0f648ed25ada1f48628080fbd76e0e5..8adf5d4d733ddeaebb97ac61d592617e5d777d00 100644 --- a/src/main/java/com/nynja/walletservice/repository/ContractDataRepository.java +++ b/src/main/java/com/nynja/walletservice/repository/ContractDataRepository.java @@ -3,5 +3,9 @@ package com.nynja.walletservice.repository; import com.nynja.walletservice.model.ContractData; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + public interface ContractDataRepository extends JpaRepository { + + Optional findByChainId(String chainId); } diff --git a/src/main/java/com/nynja/walletservice/repository/FundedAddressRepository.java b/src/main/java/com/nynja/walletservice/repository/FundedAddressRepository.java index 0ff2a780f285f661f8d78e7ec072cad7f5fa19d5..3746f723cd6b33cb416845a01e88af245e4c792e 100644 --- a/src/main/java/com/nynja/walletservice/repository/FundedAddressRepository.java +++ b/src/main/java/com/nynja/walletservice/repository/FundedAddressRepository.java @@ -5,5 +5,5 @@ import org.springframework.data.jpa.repository.JpaRepository; public interface FundedAddressRepository extends JpaRepository { - boolean existsByAddress(String address); + boolean existsByChainIdAndAddress(String chainId, String address); } diff --git a/src/main/java/com/nynja/walletservice/service/TokenService.java b/src/main/java/com/nynja/walletservice/service/TokenService.java index 9fbce37c1a1537ce7f484e1d126eb955bc966eae..88a40d3c8a9d22568e6420e524f6f735620951e5 100644 --- a/src/main/java/com/nynja/walletservice/service/TokenService.java +++ b/src/main/java/com/nynja/walletservice/service/TokenService.java @@ -1,14 +1,16 @@ package com.nynja.walletservice.service; -import com.nynja.walletservice.config.EthereumConfig; +import com.nynja.walletservice.config.ConfigValues; import com.nynja.walletservice.dto.TransactionResponseDto; import com.nynja.walletservice.model.FundedAddress; +import com.nynja.walletservice.model.network.NetworkProperties; import com.nynja.walletservice.provider.ContractProvider; import com.nynja.walletservice.repository.FundedAddressRepository; import com.nynja.walletservice.service.operation.TokenOperationFactory; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import org.web3j.crypto.Credentials; import org.web3j.protocol.core.methods.response.TransactionReceipt; import org.web3j.utils.Convert; @@ -32,29 +34,29 @@ public class TokenService { private final ContractProvider contractProvider; private final TokenOperationFactory operationFactory; - private final EthereumConfig ethConfig; private final Web3JService web3JService; private final FundedAddressRepository fundedAddressRepository; + private final ConfigValues conf; @Autowired public TokenService( - ContractProvider contractProvider, TokenOperationFactory operationFactory, - EthereumConfig ethConfig, Web3JService web3JService, FundedAddressRepository fundedAddressRepository + ContractProvider contractProvider, TokenOperationFactory operationFactory, Web3JService web3JService, + FundedAddressRepository fundedAddressRepository, ConfigValues conf ) { this.contractProvider = contractProvider; this.operationFactory = operationFactory; - this.ethConfig = ethConfig; this.web3JService = web3JService; this.fundedAddressRepository = fundedAddressRepository; + this.conf = conf; } - public CompletableFuture deployTokenContract() { - return contractProvider.deployNynjaCoinContract(); + public CompletableFuture deployTokenContract(String chainId) { + return contractProvider.deployNynjaCoinContract(chainId); } - public CompletableFuture tokenBalanceByAddress(String address) { - return operationFactory.balanceOperation(ethConfig.getAdminCredentials(), address) - .execute() + public CompletableFuture tokenBalanceByAddress(String chainId, String address) { + return operationFactory.balanceOperation(Credentials.create(conf.getNetworkProps(chainId).getAdminPrivateKey()), address) + .execute(chainId) .thenApplyAsync(balance -> { if(Objects.isNull(balance)) { throw new RuntimeException("SmartContract returned empty value"); @@ -65,21 +67,26 @@ public class TokenService { } //TODO For demo purposes. Remove after. - public CompletableFuture> handleAccountCreation(String accountAddress) { - checkIfFunded(accountAddress); + public CompletableFuture> handleAccountCreation(String chainId, String accountAddress) { + checkIfFunded(chainId, accountAddress); - mint(accountAddress, AMOUNT) + NetworkProperties networkProperties = conf.getNetworkProps(chainId); + + mint(chainId, Credentials.create(networkProperties.getAdminPrivateKey()), accountAddress, AMOUNT) .thenApplyAsync(transactionResponseDto -> { - fundedAddressRepository.save(new FundedAddress(accountAddress, transactionResponseDto.getValue())); - return transactionResponseDto; - }).thenAcceptAsync(transactionResponseDto -> { + fundedAddressRepository.save(new FundedAddress(chainId, accountAddress, transactionResponseDto.getValue())); + return transactionResponseDto; + } + ).thenAcceptAsync(transactionResponseDto -> { try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } + web3JService.sendEtherAsync( - ethConfig.getAdminCredentials(), + chainId, + Credentials.create(networkProperties.getAdminPrivateKey()), accountAddress, new BigDecimal(500), Convert.Unit.FINNEY @@ -89,18 +96,18 @@ public class TokenService { return CompletableFuture.completedFuture(new TransactionResponseDto<>("", AMOUNT)); } - private void checkIfFunded(String address) { - if (fundedAddressRepository.existsByAddress(address)) { + private void checkIfFunded(String chainId, String address) { + if (fundedAddressRepository.existsByChainIdAndAddress(chainId, address)) { throw new RuntimeException(String.format("The Address %s has already been funded", address)); } } - public CompletableFuture> mint(String address, String amount) { - return operationFactory.mintOperation(ethConfig.getAdminCredentials(), address, amount).execute() + public CompletableFuture> mint(String chainId, Credentials adminCredentials, String address, String amount) { + return operationFactory.mintOperation(adminCredentials, address, amount).execute(chainId) .thenApplyAsync(handleTransaction(amount, format("Error during token minting. Address %s", address))); } - public static Function> handleTransaction(T value, String errorMessage) { + static Function> handleTransaction(T value, String errorMessage) { return tR -> { if (!tR.getLogs().isEmpty()) return new TransactionResponseDto<>(tR.getTransactionHash(), value); diff --git a/src/main/java/com/nynja/walletservice/service/Web3JService.java b/src/main/java/com/nynja/walletservice/service/Web3JService.java index d24f1400a8073399928db5e096352abb157443d6..36b9f23757e76e3b9b7fc3a81687b15c31e9b051 100644 --- a/src/main/java/com/nynja/walletservice/service/Web3JService.java +++ b/src/main/java/com/nynja/walletservice/service/Web3JService.java @@ -6,6 +6,7 @@ import com.nynja.walletservice.dto.EstimateGasResponseDto; import com.nynja.walletservice.dto.TransactionResponseDto; import com.nynja.walletservice.exception.TransactionFailedException; import com.nynja.walletservice.exception.TransactionNotFoundException; +import com.nynja.walletservice.model.network.NetworkProperties; import com.nynja.walletservice.provider.Web3jProvider; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; @@ -43,17 +44,21 @@ public class Web3JService { this.conf = conf; } - public CompletableFuture getClientVersion() { - return web3j().web3ClientVersion() + public CompletableFuture getClientVersion(String chainId) { + return web3j(chainId).web3ClientVersion() .sendAsync() .thenApplyAsync(this::validateResponse) .thenApplyAsync(Web3ClientVersion::getWeb3ClientVersion); } - public CompletableFuture> sendEtherAsync(Credentials credentials, String toAddress, - BigDecimal value, Convert.Unit currency) { - return new Transfer(web3j(), new RawTransactionManager(web3j(), credentials, conf.getAttempts(), conf.getInterval())) - .sendFunds(toAddress, value, currency, conf.gasPrice(), conf.gasLimit()) + public CompletableFuture> sendEtherAsync( + String chainId, Credentials credentials, String toAddress, BigDecimal value, Convert.Unit currency + ) { + final Web3j web3j = web3j(chainId); + final NetworkProperties networkProperties = conf.getNetworkProps(chainId); + + return new Transfer(web3j, new RawTransactionManager(web3j, credentials, conf.getAttempts(), conf.getInterval())) + .sendFunds(toAddress, value, currency, networkProperties.getGasPrice(), networkProperties.getGasLimit()) .sendAsync() .thenApplyAsync(tr -> { log.info(ETH_TRANSFERRED, toAddress, value, currency); @@ -63,16 +68,16 @@ public class Web3JService { }); } - public CompletableFuture getBalance(String address) { - return web3j().ethGetBalance(address, DefaultBlockParameterName.LATEST) + public CompletableFuture getBalance(String chainId, String address) { + return web3j(chainId).ethGetBalance(address, DefaultBlockParameterName.LATEST) .sendAsync() .thenApplyAsync(this::validateResponse) .thenApplyAsync(EthGetBalance::getBalance) .thenApplyAsync(BigInteger::toString); } - public CompletableFuture> sendSignedTransaction(String transaction) { - return web3j().ethSendRawTransaction(transaction) + public CompletableFuture> sendSignedTransaction(String chainId, String transaction) { + return web3j(chainId).ethSendRawTransaction(transaction) .sendAsync() .thenApplyAsync(this::validateResponse) .thenApplyAsync(ethSendTransaction -> { @@ -83,23 +88,25 @@ public class Web3JService { }); } - public CompletableFuture getSingleTransactionInfo(String hash) { - return web3j().ethGetTransactionReceipt(hash) + public CompletableFuture getSingleTransactionInfo(String chainId, String hash) { + return web3j(chainId).ethGetTransactionReceipt(hash) .sendAsync() .thenApplyAsync(this::validateResponse) .thenApplyAsync(EthGetTransactionReceipt::getTransactionReceipt) .thenApplyAsync(trOpt -> trOpt.orElseThrow(() -> new TransactionNotFoundException(hash))); } - public CompletableFuture getTransactionCount(String address) { - return web3j().ethGetTransactionCount(address, PENDING) + public CompletableFuture getTransactionCount(String chainId, String address) { + return web3j(chainId).ethGetTransactionCount(address, PENDING) .sendAsync() .thenApplyAsync(this::validateResponse) .thenApplyAsync(EthGetTransactionCount::getTransactionCount); } - public CompletableFuture estimateGasPrice(EstimateGasRequestDto dto) { - return web3j().ethEstimateGas(new org.web3j.protocol.core.methods.request.Transaction( + public CompletableFuture estimateGasPrice(String chainId, EstimateGasRequestDto dto) { + final Web3j web3j = web3j(chainId); + + return web3j.ethEstimateGas(new org.web3j.protocol.core.methods.request.Transaction( dto.getFrom(), null, null, @@ -109,7 +116,7 @@ public class Web3JService { dto.getData() )).sendAsync() .thenApplyAsync(this::validateResponse) - .thenComposeAsync(ethEstimateGas -> web3j().ethGasPrice() + .thenComposeAsync(ethEstimateGas -> web3j.ethGasPrice() .sendAsync() .thenApplyAsync(this::validateResponse) .thenApplyAsync(ethGasPrice -> EstimateGasResponseDto.builder() @@ -119,14 +126,14 @@ public class Web3JService { )); } - public CompletableFuture getCurrentBlockNumber() { - return web3j().ethBlockNumber().sendAsync() + public CompletableFuture getCurrentBlockNumber(String chainId) { + return web3j(chainId).ethBlockNumber().sendAsync() .thenApplyAsync(this::validateResponse) .thenApplyAsync(EthBlockNumber::getBlockNumber); } - public CompletableFuture getCurrentBlockWithMargin() { - return getCurrentBlockNumber() + public CompletableFuture getCurrentBlockWithMargin(String chainId) { + return getCurrentBlockNumber(chainId) .thenApplyAsync(blockNumber -> blockNumber.compareTo(conf.getBlockSubscriptionMargin()) < 0 ? blockNumber : blockNumber.subtract(conf.getBlockSubscriptionMargin()) @@ -143,7 +150,7 @@ public class Web3JService { return response; } - public Web3j web3j() { - return web3jProvider.get(); + public Web3j web3j(String chainId) { + return web3jProvider.getInstance(chainId); } } diff --git a/src/main/java/com/nynja/walletservice/service/listener/EventListener.java b/src/main/java/com/nynja/walletservice/service/listener/EventListener.java index 8fa6f902d55dd04f26320c4abaca87ad2c4f40d8..d8e0c3045193f7024a9d08e4251d777f91ea4f60 100644 --- a/src/main/java/com/nynja/walletservice/service/listener/EventListener.java +++ b/src/main/java/com/nynja/walletservice/service/listener/EventListener.java @@ -1,22 +1,49 @@ package com.nynja.walletservice.service.listener; +import com.nynja.walletservice.config.ConfigValues; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationListener; import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.stereotype.Component; +import javax.annotation.PostConstruct; +import java.util.ArrayList; import java.util.List; @Component @Slf4j public class EventListener implements ApplicationListener { - private final List eventHandlers; + private List eventHandlers; + + private final ConfigValues conf; + private final ApplicationContext applicationContext; @Autowired - public EventListener(List eventHandlers) { - this.eventHandlers = eventHandlers; + public EventListener(ConfigValues conf, ApplicationContext applicationContext) { + this.conf = conf; + this.applicationContext = applicationContext; + } + + @PostConstruct + public void init() { + eventHandlers = new ArrayList<>(); + + AutowireCapableBeanFactory beanFactory = applicationContext.getAutowireCapableBeanFactory(); + conf.getNetworks().entrySet().stream() + .filter(e -> e.getValue().isNynjaCoinListenEvents()) + .forEach(e -> { + log.info("Creating {} bean for chainId: {}", TransferEventHandler.class.getCanonicalName(), e.getKey()); + + TransferEventHandler transferEventHandler = beanFactory.createBean(TransferEventHandler.class); + transferEventHandler.setChainId(e.getKey()); + transferEventHandler.setNetworkProperties(e.getValue()); + + eventHandlers.add(transferEventHandler); + }); } @Override diff --git a/src/main/java/com/nynja/walletservice/service/listener/MintEventHandler.java b/src/main/java/com/nynja/walletservice/service/listener/MintEventHandler.java index 166f4c304b1f8fe8ee7c4128731282f439d24321..22a401f15a63c83d83736cbbdb0a2ceb1ff7a176 100644 --- a/src/main/java/com/nynja/walletservice/service/listener/MintEventHandler.java +++ b/src/main/java/com/nynja/walletservice/service/listener/MintEventHandler.java @@ -3,6 +3,7 @@ package com.nynja.walletservice.service.listener; import com.nynja.walletservice.provider.ContractProvider; import com.nynja.walletservice.service.Web3JService; import com.nynja.walletservice.wrapper.NynjaCoin; +import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @@ -22,11 +23,14 @@ public class MintEventHandler extends EventHandler @Autowired private Web3JService web3JService; + @Setter + private String chainId; + @Override public void subscribe() { - web3JService.getCurrentBlockWithMargin() + web3JService.getCurrentBlockWithMargin(chainId) .thenAcceptAsync(blockNumber -> - provider.getNynjaCoin().mintEventObservable( + provider.getNynjaCoin(chainId).mintEventObservable( DefaultBlockParameter.valueOf(blockNumber), DefaultBlockParameterName.LATEST ).subscribe(this, ex -> log.error(ex.getMessage(), ex)) diff --git a/src/main/java/com/nynja/walletservice/service/listener/TransferEventHandler.java b/src/main/java/com/nynja/walletservice/service/listener/TransferEventHandler.java index 71907a9695cf8c2019593b6369888b0c2dcc0448..0eed9d0732d2c8d69e978809e1f29b5e1fc1696f 100644 --- a/src/main/java/com/nynja/walletservice/service/listener/TransferEventHandler.java +++ b/src/main/java/com/nynja/walletservice/service/listener/TransferEventHandler.java @@ -5,12 +5,12 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.nynja.walletservice.dto.messagebroker.MessageBrokerRequest; import com.nynja.walletservice.dto.messagebroker.MessageBrokerResponse; import com.nynja.walletservice.dto.messagebroker.TokenBalanceChangeDto; -import com.nynja.walletservice.provider.ContractProvider; +import com.nynja.walletservice.model.network.NetworkProperties; import com.nynja.walletservice.service.TokenService; import com.nynja.walletservice.service.Web3JService; import com.nynja.walletservice.wrapper.NynjaCoin; +import lombok.Setter; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Component; @@ -39,13 +39,19 @@ import static org.web3j.tx.Contract.staticExtractEventParameters; @Component public class TransferEventHandler extends EventHandler { - @Autowired - private ContractProvider provider; - @Autowired private TokenService tokenService; - @Autowired private Web3JService web3JService; + public TransferEventHandler(TokenService tokenService, Web3JService web3JService) { + this.tokenService = tokenService; + this.web3JService = web3JService; + } + + @Setter + private String chainId; + @Setter + private NetworkProperties networkProperties; + @Value("${message-broker.url}") private String messageBrokerUrl; @@ -56,7 +62,9 @@ public class TransferEventHandler extends EventHandler { @Override public void subscribe() { - web3JService.getCurrentBlockWithMargin() + log.info("Trying to subscribe to NynjaCoin for chainId: {}", chainId); + + web3JService.getCurrentBlockWithMargin(chainId) .thenAcceptAsync(blockNumber -> { event = new Event("Transfer", Arrays.asList(new TypeReference
() {}, new TypeReference
() {}), @@ -65,10 +73,10 @@ public class TransferEventHandler extends EventHandler { EthFilter filter = new EthFilter( DefaultBlockParameter.valueOf(blockNumber), DefaultBlockParameterName.LATEST, - provider.getNynjaAddress() + networkProperties.getNynjaCoinAddress() ); filter.addSingleTopic(EventEncoder.encode(event)); - web3JService.web3j().ethLogObservable(filter) + web3JService.web3j(chainId).ethLogObservable(filter) .subscribe(this, ex -> log.error(ex.getMessage(), ex)); } ); @@ -81,18 +89,14 @@ public class TransferEventHandler extends EventHandler { log.info("{} Nynja-coins has been transferred from {} to {}. Transaction hash: {}", typedResponse.value, typedResponse.from, typedResponse.to, response.getTransactionHash()); - tokenService.tokenBalanceByAddress(typedResponse.to).thenAcceptAsync(balance -> + tokenService.tokenBalanceByAddress(chainId, typedResponse.to).thenAcceptAsync(balance -> sendMessageToMessageBroker(typedResponse, balance, response.getTransactionHash())); } private NynjaCoin.TransferEventResponse extractEvent(Log response) { EventValues eventValues = staticExtractEventParameters(event, response); NynjaCoin.TransferEventResponse typedResponse = new NynjaCoin.TransferEventResponse(); - 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; + return NynjaCoin.getTransferEventResponse(eventValues, typedResponse); } private void sendMessageToMessageBroker(NynjaCoin.TransferEventResponse response, BigInteger balance, String txHash) { diff --git a/src/main/java/com/nynja/walletservice/service/operation/EthOperation.java b/src/main/java/com/nynja/walletservice/service/operation/EthOperation.java index 9e051ba9c34a48819bfc4ad66e360416fc0f2400..f37f171c458473ce7a30dca47c245b9c625a75ab 100644 --- a/src/main/java/com/nynja/walletservice/service/operation/EthOperation.java +++ b/src/main/java/com/nynja/walletservice/service/operation/EthOperation.java @@ -3,5 +3,5 @@ package com.nynja.walletservice.service.operation; import java.util.concurrent.CompletableFuture; public interface EthOperation { - CompletableFuture execute(); + CompletableFuture execute(String chainId); } diff --git a/src/main/java/com/nynja/walletservice/service/operation/TokenOperation.java b/src/main/java/com/nynja/walletservice/service/operation/TokenOperation.java index 6dd4225d9de2398f848d925d58d3511fb778ac13..44233a9047f1eeedc10dc9038b659a42b2b11c82 100644 --- a/src/main/java/com/nynja/walletservice/service/operation/TokenOperation.java +++ b/src/main/java/com/nynja/walletservice/service/operation/TokenOperation.java @@ -19,8 +19,8 @@ public abstract class TokenOperation implements EthOperation { this.senderCredentials = senderCredentials; } - public CompletableFuture execute() { - NynjaCoin token = contractProvider.getNynjaCoin(senderCredentials); + public CompletableFuture execute(String chainId) { + NynjaCoin token = contractProvider.getNynjaCoin(chainId, senderCredentials); return execute(token); } diff --git a/src/main/java/com/nynja/walletservice/wrapper/NynjaCoin.java b/src/main/java/com/nynja/walletservice/wrapper/NynjaCoin.java index 07855d9f210faad3ed7dc29a96bb1c1017b64c4b..12c7ec79388099d2f10c7e1c5e291b226ad72b26 100644 --- a/src/main/java/com/nynja/walletservice/wrapper/NynjaCoin.java +++ b/src/main/java/com/nynja/walletservice/wrapper/NynjaCoin.java @@ -181,33 +181,36 @@ public class NynjaCoin extends Contract { ArrayList responses = new ArrayList(valueList.size()); for (EventValues eventValues : valueList) { TransferEventResponse typedResponse = new TransferEventResponse(); - 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); + responses.add(getTransferEventResponse(eventValues, typedResponse)); } return responses; } public Observable transferEventObservable(DefaultBlockParameter startBlock, DefaultBlockParameter endBlock) { - final Event event = new Event("Transfer", + final Event event = new Event("Transfer", Arrays.>asList(new TypeReference
() {}, new TypeReference
() {}), Arrays.>asList(new TypeReference() {})); EthFilter filter = new EthFilter(startBlock, endBlock, getContractAddress()); filter.addSingleTopic(EventEncoder.encode(event)); - return web3j.ethLogObservable(filter).map(new Func1() { - @Override - public TransferEventResponse call(Log log) { - EventValues eventValues = extractEventParameters(event, log); - TransferEventResponse typedResponse = new TransferEventResponse(); - 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; - } + return web3j.ethLogObservable(filter).map(log -> { + EventValues eventValues = extractEventParameters(event, log); + TransferEventResponse typedResponse = new TransferEventResponse(); + return getTransferEventResponse(eventValues, typedResponse); }); } + /** + * This was not a part of generated Contract class. + * Add to the new NynjaCoin of there are any changes to the smart contract. + */ + public static NynjaCoin.TransferEventResponse getTransferEventResponse(EventValues eventValues, NynjaCoin.TransferEventResponse typedResponse) { + 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 RemoteCall mintingFinished() { Function function = new Function("mintingFinished", Arrays.asList(), diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 011ecdafe7942b61e70404c06718001cdb70f39c..d41b90a2e8172cac13c8f517e3bf3d040eb49404 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -18,13 +18,25 @@ web3j: attempts: 150 interval: 1000 ethereum: - client: - url: http://35.242.240.78:8545 - admin: - address: "0xB9BbCB84B654316D8E23383434c4EC66A97038ED" - private-key: "3e3a8b9c6e92808a1c683aa9458f90cfbcf5c19bed87e443b82fc7814a134796" - contract: - # First deploy a contract and then place/replace it's address into this property + networks: + 1: + url: http://35.242.240.78:8545 + block-explorer-url: http://api.etherscan.io/api + block-explorer-api-key: "K8J2W58EGI3EIR2FI8SYGW7FR1QKV5KDBM" + admin-address: "0xB9BbCB84B654316D8E23383434c4EC66A97038ED" + admin-private-key: "3e3a8b9c6e92808a1c683aa9458f90cfbcf5c19bed87e443b82fc7814a134796" + nynja-coin-listen-events: false + nynja-coin-address: "0xF36c7AdAd65c39A4848F3c85001F67f31d00d207" + gas-price: 41000000000 + gas-limit: 400000 + + 4: + url: http://35.242.240.78:8545 + block-explorer-url: http://api-rinkeby.etherscan.io/api + block-explorer-api-key: "K8J2W58EGI3EIR2FI8SYGW7FR1QKV5KDBM" + admin-address: "0xB9BbCB84B654316D8E23383434c4EC66A97038ED" + admin-private-key: "3e3a8b9c6e92808a1c683aa9458f90cfbcf5c19bed87e443b82fc7814a134796" + nynja-coin-listen-events: true nynja-coin-address: "0xF36c7AdAd65c39A4848F3c85001F67f31d00d207" gas-price: 41000000000 gas-limit: 400000 @@ -32,11 +44,6 @@ ethereum: margin: 20 wallet-discovery: search-job-pool-size: 2 -etherscan: - #url: http://api.etherscan.io/api - #url: http://api-ropsten.etherscan.io/api - url: http://api-rinkeby.etherscan.io/api - apikey: "K8J2W58EGI3EIR2FI8SYGW7FR1QKV5KDBM" message-broker: url: "http://dev2.ci.nynja.net/publish" topic: "/transfer-" diff --git a/src/test/java/com/nynja/walletservice/controller/EthereumControllerTest.java b/src/test/java/com/nynja/walletservice/controller/EthereumControllerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..1f9a51f925e0f707adeea7e9ccfb65cc9e83ed6a --- /dev/null +++ b/src/test/java/com/nynja/walletservice/controller/EthereumControllerTest.java @@ -0,0 +1,111 @@ +package com.nynja.walletservice.controller; + +import com.nynja.walletservice.config.ConfigValues; +import com.nynja.walletservice.dto.TransactionResponseDto; +import com.nynja.walletservice.model.network.NetworkProperties; +import com.nynja.walletservice.service.Web3JService; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; +import org.web3j.crypto.Credentials; +import org.web3j.utils.Convert; + +import java.math.BigDecimal; +import java.util.concurrent.CompletableFuture; + +import static com.nynja.walletservice.constant.Constants.RestApi.API_VERSION; +import static org.junit.Assert.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.request; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * @author Oleg Zhymolokhov (oleg.zhimolokhov@dataart.com) + */ +@SpringBootTest +@RunWith(SpringRunner.class) +public class EthereumControllerTest { + + static final String RINKEBY_CHAIN_ID = "4"; + static final String TEST_ADDRESS = "0x9028B59227c5EDdA388f04cef5e43020fEd1E73a"; + private static final String TEST_ADDRESS_PK = "3f5890282cc3b4eb8350823a1d1e950f7ba036b98cf074f299e1a9d09db196ca"; + + private MockMvc mvc; + private NetworkProperties networkProperties; + + @Autowired + private WebApplicationContext context; + @Autowired + private ConfigValues conf; + @Autowired + private Web3JService web3JService; + + + @Before + public void setup() { + mvc = MockMvcBuilders.webAppContextSetup(context).build(); + networkProperties = conf.getNetworkProps(RINKEBY_CHAIN_ID); + } + + @Test + public void testEthBalanceCheck() throws Exception { + MvcResult balanceResult = mvc.perform( + get("/ethereum" + API_VERSION + "address-balance") + .param("chainId", RINKEBY_CHAIN_ID) + .param("address", networkProperties.getAdminAddress()) + ).andExpect(request().asyncStarted()).andReturn(); + + mvc.perform(asyncDispatch(balanceResult)) + .andExpect(status().isOk()) + .andExpect(balance -> assertTrue( + new BigDecimal(balance.getResponse().getContentAsString()).compareTo(BigDecimal.ZERO) > 0 + )); + } + + @Test + public void testEthBalanceCheckBadRequest() throws Exception { + mvc.perform( + get("/ethereum" + API_VERSION + "address-balance") + .param("chainId", RINKEBY_CHAIN_ID) + .param("address", "") + ).andExpect(status().isBadRequest()); + } + + @Test + @SuppressWarnings("unchecked") + public void testEthSend() throws Exception { + String ethVal = "1"; + + MvcResult sendResult = mvc.perform( + get("/ethereum" + API_VERSION + "eth-send") + .param("chainId", RINKEBY_CHAIN_ID) + .param("address", TEST_ADDRESS) + .param("amount", ethVal) + ).andExpect(request().asyncStarted()).andReturn(); + + mvc.perform(asyncDispatch(sendResult)) + .andExpect(status().isOk()); + + TransactionResponseDto responseDto = ((ResponseEntity>) sendResult.getAsyncResult()).getBody(); + assertNotNull(responseDto.getHash()); + assertEquals(ethVal, responseDto.getValue()); + + //returning ether + CompletableFuture.supplyAsync(() -> web3JService.sendEtherAsync( + RINKEBY_CHAIN_ID, + Credentials.create(TEST_ADDRESS, TEST_ADDRESS_PK), + networkProperties.getAdminAddress(), + new BigDecimal("1"), + Convert.Unit.ETHER + )); + } +} diff --git a/src/test/java/com/nynja/walletservice/controller/TransactionControllerTest.java b/src/test/java/com/nynja/walletservice/controller/TransactionControllerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..d700cdf32fa0055ab0d2de6885d46b91115759d7 --- /dev/null +++ b/src/test/java/com/nynja/walletservice/controller/TransactionControllerTest.java @@ -0,0 +1,120 @@ +package com.nynja.walletservice.controller; + +import com.nynja.walletservice.blockexplorer.dto.Response; +import com.nynja.walletservice.blockexplorer.dto.TxListItemDto; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; +import org.web3j.protocol.core.methods.response.TransactionReceipt; + +import java.math.BigInteger; + +import static com.nynja.walletservice.constant.Constants.RestApi.API_VERSION; +import static com.nynja.walletservice.controller.EthereumControllerTest.RINKEBY_CHAIN_ID; +import static com.nynja.walletservice.controller.EthereumControllerTest.TEST_ADDRESS; +import static org.junit.Assert.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * @author Oleg Zhymolokhov (oleg.zhimolokhov@dataart.com) + */ +@SpringBootTest +@RunWith(SpringRunner.class) +public class TransactionControllerTest { + + private MockMvc mvc; + + @Autowired + private WebApplicationContext context; + + @Before + public void setup() { + mvc = MockMvcBuilders + .webAppContextSetup(context) + .build(); + } + + @Test + @SuppressWarnings("unchecked") + public void testTransactionHistory() throws Exception { + MvcResult result = mvc.perform( + get("/wallet-service" + API_VERSION + "tx-history") + .param("address", "0xef253203acde5813a82ce53a67b0f93adbeef74d") + .param("chainId", RINKEBY_CHAIN_ID) + .param("contractAddress", "0xF36c7AdAd65c39A4848F3c85001F67f31d00d207") + .param("page", "1") + .param("offset", "10") + ).andReturn(); + + mvc.perform(asyncDispatch(result)).andExpect(status().isOk()); + + Response responseDto = ((ResponseEntity>) result.getAsyncResult()).getBody(); + assertNotNull(responseDto); + assertEquals("1", responseDto.getStatus()); + assertFalse(responseDto.getResult().isEmpty()); + } + + @Test + public void testTransactionHistoryBadRequest() throws Exception { + mvc.perform( + get("/wallet-service" + API_VERSION + "tx-history") + .param("address", "") + .param("chainId", RINKEBY_CHAIN_ID) + .param("contractAddress", "0xF36c7AdAd65c39A4848F3c85001F67f31d00d207") + .param("page", "1") + .param("offset", "10") + ).andExpect(status().isBadRequest()); + } + + @Test + @SuppressWarnings("unchecked") + public void testSingleTransactionInfo() throws Exception { + final String testTransaction = "0x42165f05b29fab8170bdb3bd5e3b0b7beab27afe1472952b022b57ec9f2d4eb9"; + + MvcResult result = mvc.perform( + get("/wallet-service" + API_VERSION + "single-transaction-info") + .param("chainId", RINKEBY_CHAIN_ID) + .param("hash", testTransaction) + ).andReturn(); + + mvc.perform(asyncDispatch(result)).andExpect(status().isOk()); + + TransactionReceipt receipt = ((ResponseEntity) result.getAsyncResult()).getBody(); + assertNotNull(receipt); + assertEquals(testTransaction, receipt.getTransactionHash()); + } + + @Test + public void testSingleTransactionInfoBadRequest() throws Exception { + mvc.perform( + get("/wallet-service" + API_VERSION + "single-transaction-info") + .param("chainId", RINKEBY_CHAIN_ID) + .param("hash", "") + ).andExpect(status().isBadRequest()); + } + + @Test + @SuppressWarnings("unchecked") + public void testGetTransactionCount() throws Exception { + MvcResult result = mvc.perform( + get("/wallet-service" + API_VERSION + "get-transaction-count") + .param("chainId", RINKEBY_CHAIN_ID) + .param("address", TEST_ADDRESS) + ).andReturn(); + + mvc.perform(asyncDispatch(result)).andExpect(status().isOk()); + + BigInteger txCount = ((ResponseEntity) result.getAsyncResult()).getBody(); + assertNotNull(txCount); + } +}