diff --git a/pom.xml b/pom.xml
index 974a13c1698182312d3eb64dd07e64db7fc08d84..1488f1a9d46a8a40328f8c72a55ace44ed94d6d4 100644
--- a/pom.xml
+++ b/pom.xml
@@ -91,6 +91,14 @@
h2
1.4.196
+
+
+
+ org.apache.commons
+ commons-lang3
+ 3.8.1
+
+
diff --git a/src/main/java/com/nynja/walletservice/blockexplorer/EtherScanConsumer.java b/src/main/java/com/nynja/walletservice/blockexplorer/EtherScanConsumer.java
index c8ecdb8511c7ecff6c1d94e94af7a4ff3776bef4..b030db340c6031b61f289c20c2a2939a39c70764 100644
--- a/src/main/java/com/nynja/walletservice/blockexplorer/EtherScanConsumer.java
+++ b/src/main/java/com/nynja/walletservice/blockexplorer/EtherScanConsumer.java
@@ -12,6 +12,8 @@ import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
+import static org.apache.commons.lang3.StringUtils.isBlank;
+
@Component
@Slf4j
public class EtherScanConsumer {
@@ -22,39 +24,60 @@ public class EtherScanConsumer {
@Value("${etherscan.apikey}")
private String apikey;
- public Response getNormalTransactionsByAddress(String address) {
- return getTransactionsByAddress("txlist", address, "0", "9999999999", null);
+ //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);
+ } else {
+ return getERC20Transactions(address, chainId, contractAddress, page, offset, sort);
+ }
+ }
+
+ public Response getInternalTransactions(String address, String chainId, String page, String offset, Sort sort) {
+ return getTransactionsByAddress("txlistinternal", chainId, address, page, offset, sort);
}
- public Response getNormalTransactionsByAddress(String address, String startBlock, String endBlock, Sort sort) {
- return getTransactionsByAddress("txlist", address, startBlock, endBlock, sort);
+ private Response getERC20Transactions(String address, String chainId, String contractAddress, String page, String offset, Sort sort) {
+ UriComponentsBuilder builder = prepareBuilder("tokentx", chainId, address, page, offset, sort);
+
+ if (!isBlank(contractAddress)) {
+ builder.queryParam("contractaddress", contractAddress);
+ }
+
+ return makeGetRequest(builder);
}
- public Response getInternalTransactionsByAddress(String address, String startBlock, String endBlock, Sort sort) {
- return getTransactionsByAddress("txlistinternal", address, startBlock, endBlock, sort);
+ private Response getTransactionsByAddress(String action, String address, String chainId, String page, String offset, Sort sort) {
+ return makeGetRequest(prepareBuilder(action, chainId, address, page, offset, sort));
}
- private Response getTransactionsByAddress(String action, String address, String startBlock, String endBlock, Sort 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)
.queryParam("module", "account")
.queryParam("action", action)
.queryParam("address", address)
- .queryParam("sort", sort != null ? sort.getSort() : Sort.ASC.getSort())
- .queryParam("apikey", apikey);
-
- if (startBlock != null) {
- builder.queryParam("startblock", startBlock);
+ .queryParam("startblock", "0")
+ .queryParam("endblock", "99999999999")
+ .queryParam("apikey", apikey)
+ .queryParam("sort", sort != null ? sort.getSort() : Sort.ASC.getSort());
- if (endBlock != null) {
- builder.queryParam("endblock", endBlock);
- }
+ if (!isBlank(page)) {
+ builder.queryParam("page", page);
}
+ if (!isBlank(offset)) {
+ builder.queryParam("offset", offset);
+ }
+
+ return builder;
+ }
+ private Response makeGetRequest(UriComponentsBuilder builder) {
return handleEtherScanResponse(new RestTemplate().exchange(
builder.toUriString(),
HttpMethod.GET,
null,
- new ParameterizedTypeReference>() {}
+ new ParameterizedTypeReference>() {}
));
}
diff --git a/src/main/java/com/nynja/walletservice/constant/Constants.java b/src/main/java/com/nynja/walletservice/constant/Constants.java
index 94601a8ba3a2d6c98964426c392798ff2acf760f..6313d29ead4265c023349a651a0cae3dc23eac72 100644
--- a/src/main/java/com/nynja/walletservice/constant/Constants.java
+++ b/src/main/java/com/nynja/walletservice/constant/Constants.java
@@ -47,6 +47,7 @@ public interface Constants {
interface ValidationMessages {
String ADDRESS_VALIDATION_MESSAGE = "Address should be correct";
+ String CHAIN_ID_VALIDATION_MESSAGE = "Chain ID should not be empty";
String HASH_VALIDATION_MESSAGE = "Hash should be correct";
}
diff --git a/src/main/java/com/nynja/walletservice/controller/TransactionController.java b/src/main/java/com/nynja/walletservice/controller/TransactionController.java
index a7fe092f08a7376167aca5fb5f4714fd93e06637..49ae3ed6f09c7cd752a99d0e79e8de8d53cdb8f1 100644
--- a/src/main/java/com/nynja/walletservice/controller/TransactionController.java
+++ b/src/main/java/com/nynja/walletservice/controller/TransactionController.java
@@ -4,7 +4,8 @@ import com.nynja.walletservice.blockexplorer.EtherScanConsumer;
import com.nynja.walletservice.blockexplorer.dto.Response;
import com.nynja.walletservice.blockexplorer.dto.TxListItemDto;
import com.nynja.walletservice.constant.enums.Sort;
-import com.nynja.walletservice.dto.EstimateGasDto;
+import com.nynja.walletservice.dto.EstimateGasRequestDto;
+import com.nynja.walletservice.dto.EstimateGasResponseDto;
import com.nynja.walletservice.dto.SignedTransactionDto;
import com.nynja.walletservice.dto.TransactionResponseDto;
import com.nynja.walletservice.service.TokenService;
@@ -26,6 +27,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.ADDRESS_VALIDATION_MESSAGE;
+import static com.nynja.walletservice.constant.Constants.ValidationMessages.CHAIN_ID_VALIDATION_MESSAGE;
import static com.nynja.walletservice.constant.Constants.ValidationMessages.HASH_VALIDATION_MESSAGE;
import static com.nynja.walletservice.controller.ControllerHelper.handleAndRespondAsync;
@@ -75,36 +77,34 @@ public class TransactionController {
return web3JService.getTransactionCount(address).thenApplyAsync(ResponseEntity::ok);
}
- @ApiOperation(value = "Endpoint for obtaining tx history by address. External(normal) transactions", httpMethod = "GET")
+ @ApiOperation(value = "Endpoint for obtaining tx history by address. Internal transactions", httpMethod = "GET")
@ApiResponse(code = 200, message = RETRIEVED)
- @GetMapping(API_VERSION + "/normal-tx-history")
- public CompletableFuture>> getNormalTransactionsHistory(
+ @GetMapping(API_VERSION + "/internal-tx-history")
+ public CompletableFuture>> getInternalTransactionsHistory(
@NotBlank(message = ADDRESS_VALIDATION_MESSAGE) @RequestParam String address,
- @RequestParam(required = false) String startBlock, @RequestParam(required = false) String endBlock,
+ @NotBlank(message = CHAIN_ID_VALIDATION_MESSAGE) @RequestParam String chainId,
+ @RequestParam(required = false) String page, @RequestParam(required = false) String offset,
@RequestParam(required = false) Sort sort
) {
- return handleAndRespondAsync(() -> etherScanConsumer.getNormalTransactionsByAddress(
- address, startBlock, endBlock, sort
- ));
+ return handleAndRespondAsync(() -> etherScanConsumer.getInternalTransactions(address, chainId, page, offset, sort));
}
- @ApiOperation(value = "Endpoint for obtaining tx history by address. Internal transactions", httpMethod = "GET")
+ @ApiOperation(value = "Endpoint for obtaining tx history by address and/or contract address. External(normal)/ERC20 transactions", httpMethod = "GET")
@ApiResponse(code = 200, message = RETRIEVED)
- @GetMapping(API_VERSION + "/internal-tx-history")
- public CompletableFuture>> getInternalTransactionsHistory(
+ @GetMapping(API_VERSION + "/tx-history")
+ public CompletableFuture>> getTransactionHistory(
@NotBlank(message = ADDRESS_VALIDATION_MESSAGE) @RequestParam String address,
- @RequestParam(required = false) String startBlock, @RequestParam(required = false) String endBlock,
- @RequestParam(required = false) Sort sort
+ @NotBlank(message = CHAIN_ID_VALIDATION_MESSAGE) @RequestParam String chainId,
+ @RequestParam(required = false) String contractAddress, @RequestParam(required = false) String page,
+ @RequestParam(required = false) String offset, @RequestParam(required = false) Sort sort
) {
- return handleAndRespondAsync(() -> etherScanConsumer.getInternalTransactionsByAddress(
- address, startBlock, endBlock, sort
- ));
+ return handleAndRespondAsync(() -> etherScanConsumer.getTxHistory(address, chainId, contractAddress, page, offset, sort));
}
@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 EstimateGasDto dto) {
+ public CompletableFuture> estimateGasPrice(@RequestBody EstimateGasRequestDto dto) {
return web3JService.estimateGasPrice(dto).thenApplyAsync(ResponseEntity::ok);
}
}
diff --git a/src/main/java/com/nynja/walletservice/dto/EstimateGasDto.java b/src/main/java/com/nynja/walletservice/dto/EstimateGasRequestDto.java
similarity index 90%
rename from src/main/java/com/nynja/walletservice/dto/EstimateGasDto.java
rename to src/main/java/com/nynja/walletservice/dto/EstimateGasRequestDto.java
index 3eed0d582f96562b6c743f0d0126a32e32d2b817..78cdc337eacba67756d02ea4da449061f9635654 100644
--- a/src/main/java/com/nynja/walletservice/dto/EstimateGasDto.java
+++ b/src/main/java/com/nynja/walletservice/dto/EstimateGasRequestDto.java
@@ -9,7 +9,7 @@ import java.math.BigInteger;
@Data
@NoArgsConstructor
@AllArgsConstructor
-public class EstimateGasDto {
+public class EstimateGasRequestDto {
private String from;
private String to;
private BigInteger gas;
diff --git a/src/main/java/com/nynja/walletservice/dto/EstimateGasResponseDto.java b/src/main/java/com/nynja/walletservice/dto/EstimateGasResponseDto.java
new file mode 100644
index 0000000000000000000000000000000000000000..540745e73641557f3d14b5e4062261c47a85b0a3
--- /dev/null
+++ b/src/main/java/com/nynja/walletservice/dto/EstimateGasResponseDto.java
@@ -0,0 +1,17 @@
+package com.nynja.walletservice.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.math.BigInteger;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class EstimateGasResponseDto {
+ private BigInteger gasConsumed;
+ private BigInteger gasPrice;
+}
diff --git a/src/main/java/com/nynja/walletservice/exception/GlobalExceptionHandler.java b/src/main/java/com/nynja/walletservice/exception/GlobalExceptionHandler.java
index 1e81b3c589b32d484787788aab80e5aaf7e9b009..4cf5524c33d5fc5cfed6be59acf3467213516672 100644
--- a/src/main/java/com/nynja/walletservice/exception/GlobalExceptionHandler.java
+++ b/src/main/java/com/nynja/walletservice/exception/GlobalExceptionHandler.java
@@ -4,6 +4,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.async.AsyncRequestTimeoutException;
@@ -48,6 +49,12 @@ public class GlobalExceptionHandler {
return compose(HttpStatus.BAD_REQUEST, errorMessage);
}
+ @ExceptionHandler(MissingServletRequestParameterException.class)
+ public ResponseEntity handle(MissingServletRequestParameterException e) {
+ log.error(e.getMessage(), e);
+ return compose(HttpStatus.BAD_REQUEST, e.getMessage());
+ }
+
@ExceptionHandler(RuntimeException.class)
public ResponseEntity handle(RuntimeException e) {
return handleException(e);
diff --git a/src/main/java/com/nynja/walletservice/service/WalletService.java b/src/main/java/com/nynja/walletservice/service/WalletService.java
index 354834dab5efa12b57cacce08ea801099612064b..e4f413fa60abff7cca32f8ced8ba3e3431a4a624 100644
--- a/src/main/java/com/nynja/walletservice/service/WalletService.java
+++ b/src/main/java/com/nynja/walletservice/service/WalletService.java
@@ -139,7 +139,8 @@ public class WalletService {
return new ScanJobResult(
childIndex,
address,
- etherScanConsumer.getNormalTransactionsByAddress(address).getResult()
+ //TODO Handle chainId
+ etherScanConsumer.getTxHistory(address, "", null, "1", "1", null).getResult()
);
};
}
diff --git a/src/main/java/com/nynja/walletservice/service/Web3JService.java b/src/main/java/com/nynja/walletservice/service/Web3JService.java
index 9ad63f3821bba4fb036b2685bf7ed0916da59c07..e74b9bf6164f0c3dded835f141cbc79de1de691d 100644
--- a/src/main/java/com/nynja/walletservice/service/Web3JService.java
+++ b/src/main/java/com/nynja/walletservice/service/Web3JService.java
@@ -1,7 +1,8 @@
package com.nynja.walletservice.service;
import com.nynja.walletservice.config.EthereumConfig;
-import com.nynja.walletservice.dto.EstimateGasDto;
+import com.nynja.walletservice.dto.EstimateGasRequestDto;
+import com.nynja.walletservice.dto.EstimateGasResponseDto;
import com.nynja.walletservice.dto.TransactionResponseDto;
import com.nynja.walletservice.exception.TransactionFailedException;
import com.nynja.walletservice.exception.TransactionNotFoundException;
@@ -104,7 +105,7 @@ public class Web3JService {
.thenApplyAsync(EthGetTransactionCount::getTransactionCount);
}
- public CompletableFuture estimateGasPrice(EstimateGasDto dto) {
+ public CompletableFuture estimateGasPrice(EstimateGasRequestDto dto) {
return web3j().ethEstimateGas(new org.web3j.protocol.core.methods.request.Transaction(
dto.getFrom(),
null,
@@ -113,10 +114,13 @@ public class Web3JService {
dto.getTo(),
dto.getValue(),
dto.getData()
- )).sendAsync().thenComposeAsync(ethEstimateGas -> web3j().ethGasPrice().sendAsync()
- .thenApplyAsync(ethGasPrice ->
- ethGasPrice.getGasPrice().multiply(ethEstimateGas.getAmountUsed()))
- );
+ )).sendAsync().thenComposeAsync(ethEstimateGas -> web3j().ethGasPrice()
+ .sendAsync()
+ .thenApplyAsync(ethGasPrice -> EstimateGasResponseDto.builder()
+ .gasConsumed(ethEstimateGas.getAmountUsed())
+ .gasPrice(ethGasPrice.getGasPrice())
+ .build()
+ ));
}
private Web3j web3j() {
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index 3d76581b40930b338b3a0b2a8b48ffa865693f84..ab056887f5ac54c95bd3d4a9fd0725f7f9998622 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -25,14 +25,15 @@ ethereum:
private-key: "960d1cad3ad7c42251f79c31e15636915f953cbfff1770cc4483efa44bf4ffc9"
contract:
# First deploy a contract and then place/replace it's address into this property
- nynja-coin-address: "0x5c015d5b7490cc329b96aebfb9d8e5ebd78e2bf6"
- gas-price: 2000000000000
- gas-limit: 4700000
+ nynja-coin-address: "0xF36c7AdAd65c39A4848F3c85001F67f31d00d207"
+ gas-price: 41000000000
+ gas-limit: 400000
wallet-discovery:
search-job-pool-size: 2
etherscan:
- url: http://api.etherscan.io/api
+ #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"
diff --git a/src/test/java/com/nynja/walletservice/service/WalletServiceTest.java b/src/test/java/com/nynja/walletservice/service/WalletServiceTest.java
index 9cfe6424c13b920c39155257a2a0218ca99318d9..464caee3d14988f33c85b3247beac509c997e473 100644
--- a/src/test/java/com/nynja/walletservice/service/WalletServiceTest.java
+++ b/src/test/java/com/nynja/walletservice/service/WalletServiceTest.java
@@ -20,6 +20,7 @@ import static com.nynja.walletservice.service.WalletService.BIP44_GAP;
import static org.junit.Assert.assertEquals;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
+import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.times;
@@ -66,7 +67,7 @@ public class WalletServiceTest {
@Test
public void testSingleAccountScan() {
- given(etherScanConsumer.getNormalTransactionsByAddress(anyString()))
+ given(etherScanConsumer.getTxHistory(anyString(), anyString(), anyString(), anyString(), anyString(), any()))
.will(invocation -> {
String address = invocation.getArgumentAt(0, String.class);
return FIRST_ADDRESS.equals(address) || SECOND_ADDRESS.equals(address) ? positiveResponse : emptyResponse;
@@ -77,19 +78,22 @@ public class WalletServiceTest {
//The test data implies that there will be transaction history only for 0th and 2th addresses
//This means that there will be 23 requests to the block-explorer to reach the BIP-44 gap and stop the discovery
- then(etherScanConsumer).should(times(BIP44_GAP + 3)).getNormalTransactionsByAddress(anyString());
+ then(etherScanConsumer).should(times(BIP44_GAP + 3))
+ .getTxHistory(anyString(), anyString(), anyString(), anyString(), anyString(), any());
assertEquals(2, result.size());
}
@Test
public void testNoResults() {
- given(etherScanConsumer.getNormalTransactionsByAddress(anyString())).willReturn(emptyResponse);
+ given(etherScanConsumer.getTxHistory(anyString(), anyString(), anyString(), anyString(), anyString(), any()))
+ .willReturn(emptyResponse);
//when
List result = walletService.scanSingleAccount(PUB, CHAIN_CODE);
- then(etherScanConsumer).should(times(BIP44_GAP)).getNormalTransactionsByAddress(anyString());
+ then(etherScanConsumer).should(times(BIP44_GAP))
+ .getTxHistory(anyString(), anyString(), anyString(), anyString(), anyString(), any());
assertEquals(0, result.size());
}
}