From 333cfe4e4a08fb8c45973327370abb253a83c44b Mon Sep 17 00:00:00 2001 From: Blackmorse Date: Mon, 25 Sep 2023 00:02:09 +0400 Subject: [PATCH 01/18] Contract creation API --- .../api/etherscan/AccountAPIProvider.java | 6 +- .../goodforgod/api/etherscan/ContractAPI.java | 11 +++ .../api/etherscan/ContractAPIProvider.java | 30 +++++++ .../api/etherscan/model/ContractCreation.java | 80 +++++++++++++++++++ .../response/ContractCreationResponseTO.java | 4 + .../model/response/ContractCreationTO.java | 20 +++++ .../api/etherscan/util/BasicUtils.java | 4 + .../etherscan/contract/ContractApiTests.java | 46 +++++++++++ 8 files changed, 196 insertions(+), 5 deletions(-) create mode 100644 src/main/java/io/goodforgod/api/etherscan/model/ContractCreation.java create mode 100644 src/main/java/io/goodforgod/api/etherscan/model/response/ContractCreationResponseTO.java create mode 100644 src/main/java/io/goodforgod/api/etherscan/model/response/ContractCreationTO.java diff --git a/src/main/java/io/goodforgod/api/etherscan/AccountAPIProvider.java b/src/main/java/io/goodforgod/api/etherscan/AccountAPIProvider.java index 442edff..d36baf7 100644 --- a/src/main/java/io/goodforgod/api/etherscan/AccountAPIProvider.java +++ b/src/main/java/io/goodforgod/api/etherscan/AccountAPIProvider.java @@ -95,7 +95,7 @@ public List balances(@NotNull List addresses) throws EtherScanE final List> addressesAsBatches = BasicUtils.partition(addresses, 20); for (final List batch : addressesAsBatches) { - final String urlParams = ACT_BALANCE_MULTI_ACTION + TAG_LATEST_PARAM + ADDRESS_PARAM + toAddressParam(batch); + final String urlParams = ACT_BALANCE_MULTI_ACTION + TAG_LATEST_PARAM + ADDRESS_PARAM + BasicUtils.toAddressParam(batch); final BalanceResponseTO response = getRequest(urlParams, BalanceResponseTO.class); if (response.getStatus() != 1) { throw new EtherScanResponseException(response); @@ -111,10 +111,6 @@ public List balances(@NotNull List addresses) throws EtherScanE return balances; } - private String toAddressParam(List addresses) { - return String.join(",", addresses); - } - @NotNull @Override public List txs(@NotNull String address) throws EtherScanException { diff --git a/src/main/java/io/goodforgod/api/etherscan/ContractAPI.java b/src/main/java/io/goodforgod/api/etherscan/ContractAPI.java index af0852c..45ecb1e 100644 --- a/src/main/java/io/goodforgod/api/etherscan/ContractAPI.java +++ b/src/main/java/io/goodforgod/api/etherscan/ContractAPI.java @@ -2,8 +2,11 @@ import io.goodforgod.api.etherscan.error.EtherScanException; import io.goodforgod.api.etherscan.model.Abi; +import io.goodforgod.api.etherscan.model.ContractCreation; import org.jetbrains.annotations.NotNull; +import java.util.List; + /** * EtherScan - API Descriptions ... * @@ -21,4 +24,12 @@ public interface ContractAPI { */ @NotNull Abi contractAbi(@NotNull String address) throws EtherScanException; + + /** + * Returns a contract's deployer address and transaction hash it was created, up to 5 at a time. + * @param contractAddresses - list of addresses to fetch + * @throws EtherScanException parent exception class + */ + @NotNull + List contractCreation(@NotNull List contractAddresses) throws EtherScanException; } diff --git a/src/main/java/io/goodforgod/api/etherscan/ContractAPIProvider.java b/src/main/java/io/goodforgod/api/etherscan/ContractAPIProvider.java index 6b4404a..fda1b0d 100644 --- a/src/main/java/io/goodforgod/api/etherscan/ContractAPIProvider.java +++ b/src/main/java/io/goodforgod/api/etherscan/ContractAPIProvider.java @@ -5,10 +5,15 @@ import io.goodforgod.api.etherscan.http.EthHttpClient; import io.goodforgod.api.etherscan.manager.RequestQueueManager; import io.goodforgod.api.etherscan.model.Abi; +import io.goodforgod.api.etherscan.model.ContractCreation; +import io.goodforgod.api.etherscan.model.response.ContractCreationResponseTO; import io.goodforgod.api.etherscan.model.response.StringResponseTO; import io.goodforgod.api.etherscan.util.BasicUtils; import org.jetbrains.annotations.NotNull; +import java.util.List; +import java.util.stream.Collectors; + /** * Contract API Implementation * @@ -22,6 +27,12 @@ final class ContractAPIProvider extends BasicProvider implements ContractAPI { private static final String ADDRESS_PARAM = "&address="; + private static final String ACT_CONTRACT_CREATION_PARAM = "getcontractcreation"; + + private static final String ACT_CONTRACT_CREATION = ACT_PREFIX + ACT_CONTRACT_CREATION_PARAM; + + private static final String ACT_CONTRACT_ADDRESSES_PARAM = "&contractaddresses="; + ContractAPIProvider(RequestQueueManager requestQueueManager, String baseUrl, EthHttpClient executor, @@ -44,4 +55,23 @@ public Abi contractAbi(@NotNull String address) throws EtherScanException { ? Abi.nonVerified() : Abi.verified(response.getResult()); } + + @NotNull + @Override + public List contractCreation(@NotNull List contractAddresses) throws EtherScanException { + BasicUtils.validateAddresses(contractAddresses); + final String urlParam = ACT_CONTRACT_CREATION + ACT_CONTRACT_ADDRESSES_PARAM + BasicUtils.toAddressParam(contractAddresses); + final ContractCreationResponseTO response = getRequest(urlParam, ContractCreationResponseTO.class); + if (response.getStatus() != 1 && response.getMessage().startsWith("NOTOK")) { + throw new EtherScanResponseException(response); + } + + return response.getResult().stream() + .map(to -> ContractCreation.builder() + .withContractCreator(to.getContractCreator()) + .withContractAddress(to.getContractAddress()) + .withTxHash(to.getTxHash()) + .build() + ).collect(Collectors.toList()); + } } diff --git a/src/main/java/io/goodforgod/api/etherscan/model/ContractCreation.java b/src/main/java/io/goodforgod/api/etherscan/model/ContractCreation.java new file mode 100644 index 0000000..747aefb --- /dev/null +++ b/src/main/java/io/goodforgod/api/etherscan/model/ContractCreation.java @@ -0,0 +1,80 @@ +package io.goodforgod.api.etherscan.model; + +import java.util.Objects; + +public class ContractCreation { + private final String contractAddress; + private final String contractCreator; + private final String txHash; + + private ContractCreation(String contractAddress, String contractCreator, String txHash) { + this.contractAddress = contractAddress; + this.contractCreator = contractCreator; + this.txHash = txHash; + } + + public String getContractAddress() { + return contractAddress; + } + + public String getContractCreator() { + return contractCreator; + } + + public String getTxHash() { + return txHash; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ContractCreation that = (ContractCreation) o; + return Objects.equals(contractAddress, that.contractAddress) && Objects.equals(contractCreator, that.contractCreator) && Objects.equals(txHash, that.txHash); + } + + @Override + public int hashCode() { + return Objects.hash(contractAddress, contractCreator, txHash); + } + + @Override + public String toString() { + return "ContractCreation{" + + "contractAddress='" + contractAddress + '\'' + + ", contractCreator='" + contractCreator + '\'' + + ", txHash='" + txHash + '\'' + + '}'; + } + + public static ContractCreationBuilder builder() { + return new ContractCreationBuilder(); + } + + public static final class ContractCreationBuilder { + private String contractAddress; + private String contractCreator; + private String txHash; + + private ContractCreationBuilder() {} + + public ContractCreationBuilder withContractAddress(String contractAddress) { + this.contractAddress = contractAddress; + return this; + } + + public ContractCreationBuilder withContractCreator(String contractCreator) { + this.contractCreator = contractCreator; + return this; + } + + public ContractCreationBuilder withTxHash(String txHash) { + this.txHash = txHash; + return this; + } + + public ContractCreation build() { + return new ContractCreation(contractAddress, contractCreator, txHash); + } + } +} diff --git a/src/main/java/io/goodforgod/api/etherscan/model/response/ContractCreationResponseTO.java b/src/main/java/io/goodforgod/api/etherscan/model/response/ContractCreationResponseTO.java new file mode 100644 index 0000000..7cf28fc --- /dev/null +++ b/src/main/java/io/goodforgod/api/etherscan/model/response/ContractCreationResponseTO.java @@ -0,0 +1,4 @@ +package io.goodforgod.api.etherscan.model.response; + +public class ContractCreationResponseTO extends BaseListResponseTO { +} diff --git a/src/main/java/io/goodforgod/api/etherscan/model/response/ContractCreationTO.java b/src/main/java/io/goodforgod/api/etherscan/model/response/ContractCreationTO.java new file mode 100644 index 0000000..9e1551e --- /dev/null +++ b/src/main/java/io/goodforgod/api/etherscan/model/response/ContractCreationTO.java @@ -0,0 +1,20 @@ +package io.goodforgod.api.etherscan.model.response; + +public class ContractCreationTO { + + private String contractAddress; + private String contractCreator; + private String txHash; + + public String getContractAddress() { + return contractAddress; + } + + public String getContractCreator() { + return contractCreator; + } + + public String getTxHash() { + return txHash; + } +} diff --git a/src/main/java/io/goodforgod/api/etherscan/util/BasicUtils.java b/src/main/java/io/goodforgod/api/etherscan/util/BasicUtils.java index 216ab62..916d4ab 100644 --- a/src/main/java/io/goodforgod/api/etherscan/util/BasicUtils.java +++ b/src/main/java/io/goodforgod/api/etherscan/util/BasicUtils.java @@ -149,4 +149,8 @@ public static List> partition(List list, int pairSize) { return partitioned; } + + public static String toAddressParam(List addresses) { + return String.join(",", addresses); + } } diff --git a/src/test/java/io/goodforgod/api/etherscan/contract/ContractApiTests.java b/src/test/java/io/goodforgod/api/etherscan/contract/ContractApiTests.java index 4fd0fdb..49e8f07 100644 --- a/src/test/java/io/goodforgod/api/etherscan/contract/ContractApiTests.java +++ b/src/test/java/io/goodforgod/api/etherscan/contract/ContractApiTests.java @@ -3,8 +3,13 @@ import io.goodforgod.api.etherscan.ApiRunner; import io.goodforgod.api.etherscan.error.EtherScanInvalidAddressException; import io.goodforgod.api.etherscan.model.Abi; +import io.goodforgod.api.etherscan.model.ContractCreation; import org.junit.jupiter.api.Test; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + /** * @author GoodforGod * @since 03.11.2018 @@ -37,4 +42,45 @@ void correctParamWithEmptyExpectedResult() { assertNotNull(abi); assertTrue(abi.isVerified()); } + + @Test + void correctContractCreation() { + List contractCreations = + getApi().contract().contractCreation(Collections.singletonList("0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413")); + + assertEquals(1, contractCreations.size()); + ContractCreation contractCreation = contractCreations.get(0); + + assertEquals("0xbb9bc244d798123fde783fcc1c72d3bb8c189413", contractCreation.getContractAddress()); + assertEquals("0x793ea9692ada1900fbd0b80fffec6e431fe8b391", contractCreation.getContractCreator()); + assertEquals("0xe9ebfecc2fa10100db51a4408d18193b3ac504584b51a4e55bdef1318f0a30f9", contractCreation.getTxHash()); + } + + @Test + void correctMultipleContractCreation() { + List contractCreations = + getApi().contract().contractCreation(Arrays.asList("0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413", "0x5EaC95ad5b287cF44E058dCf694419333b796123")); + assertEquals(2, contractCreations.size()); + + ContractCreation contractCreation1 = ContractCreation.builder() + .withContractAddress("0xbb9bc244d798123fde783fcc1c72d3bb8c189413") + .withContractCreator("0x793ea9692ada1900fbd0b80fffec6e431fe8b391") + .withTxHash("0xe9ebfecc2fa10100db51a4408d18193b3ac504584b51a4e55bdef1318f0a30f9") + .build(); + + ContractCreation contractCreation2 = ContractCreation.builder() + .withContractAddress("0x5eac95ad5b287cf44e058dcf694419333b796123") + .withContractCreator("0x7c675b7450e878e5af8550b41df42d134674e61f") + .withTxHash("0x79cdfec19e5a86d9022680a4d1c86d3d8cd76c21c01903a2f02c127a0a7dbfb3") + .build(); + + assertTrue(contractCreations.contains(contractCreation1)); + assertTrue(contractCreations.contains(contractCreation2)); + } + + @Test + void contractCreationInvalidParamWithError() { + assertThrows(EtherScanInvalidAddressException.class, + () -> getApi().contract().contractCreation(Collections.singletonList("0xBBbc244D798123fDe783fCc1C72d3Bb8C189414"))); + } } From 234cce4cadb71e1dfd45969a7709b45533d2656f Mon Sep 17 00:00:00 2001 From: Blackmorse Date: Fri, 29 Sep 2023 12:51:00 +0400 Subject: [PATCH 02/18] Formatting --- .../api/etherscan/AccountAPIProvider.java | 3 ++- .../io/goodforgod/api/etherscan/ContractAPI.java | 4 ++-- .../api/etherscan/ContractAPIProvider.java | 10 +++++----- .../io/goodforgod/api/etherscan/StatisticAPI.java | 3 +++ .../api/etherscan/model/ContractCreation.java | 11 ++++++++--- .../model/response/ContractCreationResponseTO.java | 3 +-- .../api/etherscan/contract/ContractApiTests.java | 14 +++++++------- 7 files changed, 28 insertions(+), 20 deletions(-) diff --git a/src/main/java/io/goodforgod/api/etherscan/AccountAPIProvider.java b/src/main/java/io/goodforgod/api/etherscan/AccountAPIProvider.java index d36baf7..f968c1d 100644 --- a/src/main/java/io/goodforgod/api/etherscan/AccountAPIProvider.java +++ b/src/main/java/io/goodforgod/api/etherscan/AccountAPIProvider.java @@ -95,7 +95,8 @@ public List balances(@NotNull List addresses) throws EtherScanE final List> addressesAsBatches = BasicUtils.partition(addresses, 20); for (final List batch : addressesAsBatches) { - final String urlParams = ACT_BALANCE_MULTI_ACTION + TAG_LATEST_PARAM + ADDRESS_PARAM + BasicUtils.toAddressParam(batch); + final String urlParams = ACT_BALANCE_MULTI_ACTION + TAG_LATEST_PARAM + ADDRESS_PARAM + + BasicUtils.toAddressParam(batch); final BalanceResponseTO response = getRequest(urlParams, BalanceResponseTO.class); if (response.getStatus() != 1) { throw new EtherScanResponseException(response); diff --git a/src/main/java/io/goodforgod/api/etherscan/ContractAPI.java b/src/main/java/io/goodforgod/api/etherscan/ContractAPI.java index 45ecb1e..c076b74 100644 --- a/src/main/java/io/goodforgod/api/etherscan/ContractAPI.java +++ b/src/main/java/io/goodforgod/api/etherscan/ContractAPI.java @@ -3,9 +3,8 @@ import io.goodforgod.api.etherscan.error.EtherScanException; import io.goodforgod.api.etherscan.model.Abi; import io.goodforgod.api.etherscan.model.ContractCreation; -import org.jetbrains.annotations.NotNull; - import java.util.List; +import org.jetbrains.annotations.NotNull; /** * EtherScan - API Descriptions ... @@ -27,6 +26,7 @@ public interface ContractAPI { /** * Returns a contract's deployer address and transaction hash it was created, up to 5 at a time. + * * @param contractAddresses - list of addresses to fetch * @throws EtherScanException parent exception class */ diff --git a/src/main/java/io/goodforgod/api/etherscan/ContractAPIProvider.java b/src/main/java/io/goodforgod/api/etherscan/ContractAPIProvider.java index fda1b0d..0493f45 100644 --- a/src/main/java/io/goodforgod/api/etherscan/ContractAPIProvider.java +++ b/src/main/java/io/goodforgod/api/etherscan/ContractAPIProvider.java @@ -9,10 +9,9 @@ import io.goodforgod.api.etherscan.model.response.ContractCreationResponseTO; import io.goodforgod.api.etherscan.model.response.StringResponseTO; import io.goodforgod.api.etherscan.util.BasicUtils; -import org.jetbrains.annotations.NotNull; - import java.util.List; import java.util.stream.Collectors; +import org.jetbrains.annotations.NotNull; /** * Contract API Implementation @@ -60,7 +59,8 @@ public Abi contractAbi(@NotNull String address) throws EtherScanException { @Override public List contractCreation(@NotNull List contractAddresses) throws EtherScanException { BasicUtils.validateAddresses(contractAddresses); - final String urlParam = ACT_CONTRACT_CREATION + ACT_CONTRACT_ADDRESSES_PARAM + BasicUtils.toAddressParam(contractAddresses); + final String urlParam = ACT_CONTRACT_CREATION + ACT_CONTRACT_ADDRESSES_PARAM + + BasicUtils.toAddressParam(contractAddresses); final ContractCreationResponseTO response = getRequest(urlParam, ContractCreationResponseTO.class); if (response.getStatus() != 1 && response.getMessage().startsWith("NOTOK")) { throw new EtherScanResponseException(response); @@ -71,7 +71,7 @@ public List contractCreation(@NotNull List contractAdd .withContractCreator(to.getContractCreator()) .withContractAddress(to.getContractAddress()) .withTxHash(to.getTxHash()) - .build() - ).collect(Collectors.toList()); + .build()) + .collect(Collectors.toList()); } } diff --git a/src/main/java/io/goodforgod/api/etherscan/StatisticAPI.java b/src/main/java/io/goodforgod/api/etherscan/StatisticAPI.java index d7b48b8..b6db82e 100644 --- a/src/main/java/io/goodforgod/api/etherscan/StatisticAPI.java +++ b/src/main/java/io/goodforgod/api/etherscan/StatisticAPI.java @@ -15,6 +15,9 @@ public interface StatisticAPI { /** + * ERC20 token total Supply + * EtherScan * Returns the current amount of an ERC-20 token in circulation. * * @param contract contract address diff --git a/src/main/java/io/goodforgod/api/etherscan/model/ContractCreation.java b/src/main/java/io/goodforgod/api/etherscan/model/ContractCreation.java index 747aefb..0f3d822 100644 --- a/src/main/java/io/goodforgod/api/etherscan/model/ContractCreation.java +++ b/src/main/java/io/goodforgod/api/etherscan/model/ContractCreation.java @@ -3,6 +3,7 @@ import java.util.Objects; public class ContractCreation { + private final String contractAddress; private final String contractCreator; private final String txHash; @@ -27,10 +28,13 @@ public String getTxHash() { @Override public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; ContractCreation that = (ContractCreation) o; - return Objects.equals(contractAddress, that.contractAddress) && Objects.equals(contractCreator, that.contractCreator) && Objects.equals(txHash, that.txHash); + return Objects.equals(contractAddress, that.contractAddress) && Objects.equals(contractCreator, that.contractCreator) + && Objects.equals(txHash, that.txHash); } @Override @@ -52,6 +56,7 @@ public static ContractCreationBuilder builder() { } public static final class ContractCreationBuilder { + private String contractAddress; private String contractCreator; private String txHash; diff --git a/src/main/java/io/goodforgod/api/etherscan/model/response/ContractCreationResponseTO.java b/src/main/java/io/goodforgod/api/etherscan/model/response/ContractCreationResponseTO.java index 7cf28fc..e3766c3 100644 --- a/src/main/java/io/goodforgod/api/etherscan/model/response/ContractCreationResponseTO.java +++ b/src/main/java/io/goodforgod/api/etherscan/model/response/ContractCreationResponseTO.java @@ -1,4 +1,3 @@ package io.goodforgod.api.etherscan.model.response; -public class ContractCreationResponseTO extends BaseListResponseTO { -} +public class ContractCreationResponseTO extends BaseListResponseTO {} diff --git a/src/test/java/io/goodforgod/api/etherscan/contract/ContractApiTests.java b/src/test/java/io/goodforgod/api/etherscan/contract/ContractApiTests.java index 49e8f07..d1e4de4 100644 --- a/src/test/java/io/goodforgod/api/etherscan/contract/ContractApiTests.java +++ b/src/test/java/io/goodforgod/api/etherscan/contract/ContractApiTests.java @@ -4,11 +4,10 @@ import io.goodforgod.api.etherscan.error.EtherScanInvalidAddressException; import io.goodforgod.api.etherscan.model.Abi; import io.goodforgod.api.etherscan.model.ContractCreation; -import org.junit.jupiter.api.Test; - import java.util.Arrays; import java.util.Collections; import java.util.List; +import org.junit.jupiter.api.Test; /** * @author GoodforGod @@ -45,8 +44,8 @@ void correctParamWithEmptyExpectedResult() { @Test void correctContractCreation() { - List contractCreations = - getApi().contract().contractCreation(Collections.singletonList("0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413")); + List contractCreations = getApi().contract() + .contractCreation(Collections.singletonList("0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413")); assertEquals(1, contractCreations.size()); ContractCreation contractCreation = contractCreations.get(0); @@ -58,8 +57,8 @@ void correctContractCreation() { @Test void correctMultipleContractCreation() { - List contractCreations = - getApi().contract().contractCreation(Arrays.asList("0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413", "0x5EaC95ad5b287cF44E058dCf694419333b796123")); + List contractCreations = getApi().contract().contractCreation( + Arrays.asList("0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413", "0x5EaC95ad5b287cF44E058dCf694419333b796123")); assertEquals(2, contractCreations.size()); ContractCreation contractCreation1 = ContractCreation.builder() @@ -81,6 +80,7 @@ void correctMultipleContractCreation() { @Test void contractCreationInvalidParamWithError() { assertThrows(EtherScanInvalidAddressException.class, - () -> getApi().contract().contractCreation(Collections.singletonList("0xBBbc244D798123fDe783fCc1C72d3Bb8C189414"))); + () -> getApi().contract() + .contractCreation(Collections.singletonList("0xBBbc244D798123fDe783fCc1C72d3Bb8C189414"))); } } From 64540b8499b4cfdc284d04db01ecadbbc1a1e360 Mon Sep 17 00:00:00 2001 From: Blackmorse Date: Sun, 1 Oct 2023 16:59:32 +0400 Subject: [PATCH 03/18] filtering out empty env --- src/test/java/io/goodforgod/api/etherscan/ApiRunner.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/test/java/io/goodforgod/api/etherscan/ApiRunner.java b/src/test/java/io/goodforgod/api/etherscan/ApiRunner.java index 4b52c00..fd933c2 100644 --- a/src/test/java/io/goodforgod/api/etherscan/ApiRunner.java +++ b/src/test/java/io/goodforgod/api/etherscan/ApiRunner.java @@ -2,6 +2,8 @@ import io.goodforgod.api.etherscan.manager.RequestQueueManager; import java.util.Map; + +import io.goodforgod.api.etherscan.util.BasicUtils; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Assertions; @@ -15,6 +17,7 @@ public class ApiRunner extends Assertions { static { API_KEY = System.getenv().entrySet().stream() .filter(e -> e.getKey().startsWith("ETHERSCAN_API_KEY")) + .filter(e -> !BasicUtils.isBlank(e.getValue())) .map(Map.Entry::getValue) .findFirst() .orElse(DEFAULT_KEY); From 06464f87498713c03ab27b5232586057bd066138 Mon Sep 17 00:00:00 2001 From: Blackmorse Date: Thu, 5 Oct 2023 00:58:32 +0400 Subject: [PATCH 04/18] Fix codestyle --- src/test/java/io/goodforgod/api/etherscan/ApiRunner.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/test/java/io/goodforgod/api/etherscan/ApiRunner.java b/src/test/java/io/goodforgod/api/etherscan/ApiRunner.java index fd933c2..a6c43ac 100644 --- a/src/test/java/io/goodforgod/api/etherscan/ApiRunner.java +++ b/src/test/java/io/goodforgod/api/etherscan/ApiRunner.java @@ -1,9 +1,8 @@ package io.goodforgod.api.etherscan; import io.goodforgod.api.etherscan.manager.RequestQueueManager; -import java.util.Map; - import io.goodforgod.api.etherscan.util.BasicUtils; +import java.util.Map; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Assertions; From 3a87ca83cda95a61415bc5226651b5d18448900f Mon Sep 17 00:00:00 2001 From: Anton Kurako Date: Fri, 6 Oct 2023 02:10:05 +0300 Subject: [PATCH 05/18] [2.1.0-SNAPSHOT] EtherScanAPI.Builder#withRetryOnLimitReach added --- gradle.properties | 2 +- .../api/etherscan/AccountAPIProvider.java | 5 +- .../api/etherscan/BasicProvider.java | 51 ++++++++++++++++--- .../api/etherscan/BlockAPIProvider.java | 12 ++--- .../api/etherscan/ContractAPIProvider.java | 5 +- .../api/etherscan/EthScanAPIBuilder.java | 13 ++++- .../api/etherscan/EtherScanAPI.java | 11 ++++ .../api/etherscan/EtherScanAPIProvider.java | 19 +++---- .../api/etherscan/GasTrackerAPIProvider.java | 5 +- .../api/etherscan/LogsAPIProvider.java | 5 +- .../api/etherscan/ProxyAPIProvider.java | 5 +- .../api/etherscan/StatisticAPIProvider.java | 5 +- .../api/etherscan/TransactionAPIProvider.java | 5 +- .../manager/RequestQueueManager.java | 10 ++-- .../goodforgod/api/etherscan/model/Abi.java | 2 +- .../api/etherscan/model/Balance.java | 2 +- .../goodforgod/api/etherscan/model/Block.java | 2 +- .../api/etherscan/model/BlockUncle.java | 8 +-- .../api/etherscan/model/ContractCreation.java | 29 ++++++----- .../api/etherscan/model/EthSupply.java | 8 +-- .../goodforgod/api/etherscan/model/Log.java | 18 +++---- .../goodforgod/api/etherscan/model/Price.java | 4 +- .../api/etherscan/model/Status.java | 2 +- .../api/etherscan/model/TokenBalance.java | 2 +- .../io/goodforgod/api/etherscan/model/Tx.java | 18 +++---- .../api/etherscan/model/TxErc1155.java | 22 ++++---- .../api/etherscan/model/TxErc20.java | 20 ++++---- .../api/etherscan/model/TxErc721.java | 22 ++++---- .../api/etherscan/model/TxInternal.java | 18 +++---- .../api/etherscan/model/proxy/BlockProxy.java | 36 ++++++------- .../etherscan/model/proxy/ReceiptProxy.java | 22 ++++---- .../api/etherscan/model/proxy/TxProxy.java | 28 +++++----- .../goodforgod/api/etherscan/ApiRunner.java | 1 + 33 files changed, 242 insertions(+), 175 deletions(-) diff --git a/gradle.properties b/gradle.properties index 821da06..ee5fd3b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,6 @@ groupId=com.github.goodforgod artifactId=java-etherscan-api -artifactVersion=2.0.0 +artifactVersion=2.1.0-SNAPSHOT ##### GRADLE ##### diff --git a/src/main/java/io/goodforgod/api/etherscan/AccountAPIProvider.java b/src/main/java/io/goodforgod/api/etherscan/AccountAPIProvider.java index f968c1d..750d525 100644 --- a/src/main/java/io/goodforgod/api/etherscan/AccountAPIProvider.java +++ b/src/main/java/io/goodforgod/api/etherscan/AccountAPIProvider.java @@ -50,8 +50,9 @@ final class AccountAPIProvider extends BasicProvider implements AccountAPI { AccountAPIProvider(RequestQueueManager requestQueueManager, String baseUrl, EthHttpClient executor, - Converter converter) { - super(requestQueueManager, "account", baseUrl, executor, converter); + Converter converter, + int retryCount) { + super(requestQueueManager, "account", baseUrl, executor, converter, retryCount); } @NotNull diff --git a/src/main/java/io/goodforgod/api/etherscan/BasicProvider.java b/src/main/java/io/goodforgod/api/etherscan/BasicProvider.java index 5c61aad..41abd16 100644 --- a/src/main/java/io/goodforgod/api/etherscan/BasicProvider.java +++ b/src/main/java/io/goodforgod/api/etherscan/BasicProvider.java @@ -30,20 +30,23 @@ abstract class BasicProvider { private final EthHttpClient executor; private final RequestQueueManager queue; private final Converter converter; + private final int retryCountLimit; BasicProvider(RequestQueueManager requestQueueManager, String module, String baseUrl, EthHttpClient ethHttpClient, - Converter converter) { + Converter converter, + int retryCountLimit) { this.queue = requestQueueManager; this.module = "&module=" + module; this.baseUrl = baseUrl; this.executor = ethHttpClient; this.converter = converter; + this.retryCountLimit = retryCountLimit; } - T convert(byte[] json, Class tClass) { + private T convert(byte[] json, Class tClass) { try { final T t = converter.fromJson(json, tClass); if (t instanceof StringResponseTO && ((StringResponseTO) t).getResult().startsWith(MAX_RATE_LIMIT_REACHED)) { @@ -66,23 +69,59 @@ T convert(byte[] json, Class tClass) { } } - byte[] getRequest(String urlParameters) { + private byte[] getRequest(String urlParameters) { queue.takeTurn(); final URI uri = URI.create(baseUrl + module + urlParameters); return executor.get(uri); } - byte[] postRequest(String urlParameters, String dataToPost) { + private byte[] postRequest(String urlParameters, String dataToPost) { queue.takeTurn(); final URI uri = URI.create(baseUrl + module + urlParameters); return executor.post(uri, dataToPost.getBytes(StandardCharsets.UTF_8)); } T getRequest(String urlParameters, Class tClass) { - return convert(getRequest(urlParameters), tClass); + return getRequest(urlParameters, tClass, 0); + } + + private T getRequest(String urlParameters, Class tClass, int retryCount) { + try { + return convert(getRequest(urlParameters), tClass); + } catch (Exception e) { + if (retryCount < retryCountLimit) { + try { + Thread.sleep(1150); + } catch (InterruptedException ex) { + throw new IllegalStateException(ex); + } + + return getRequest(urlParameters, tClass, retryCount + 1); + } else { + throw e; + } + } } T postRequest(String urlParameters, String dataToPost, Class tClass) { - return convert(postRequest(urlParameters, dataToPost), tClass); + return postRequest(urlParameters, dataToPost, tClass, 0); + } + + private T postRequest(String urlParameters, String dataToPost, Class tClass, int retryCount) { + try { + return convert(postRequest(urlParameters, dataToPost), tClass); + } catch (EtherScanRateLimitException e) { + if (retryCount < retryCountLimit) { + try { + Thread.sleep(1150); + } catch (InterruptedException ex) { + throw new IllegalStateException(ex); + } + + return postRequest(urlParameters, dataToPost, tClass, retryCount + 1); + } else { + throw e; + } + } } } diff --git a/src/main/java/io/goodforgod/api/etherscan/BlockAPIProvider.java b/src/main/java/io/goodforgod/api/etherscan/BlockAPIProvider.java index 406ac19..b3604a7 100644 --- a/src/main/java/io/goodforgod/api/etherscan/BlockAPIProvider.java +++ b/src/main/java/io/goodforgod/api/etherscan/BlockAPIProvider.java @@ -26,21 +26,17 @@ final class BlockAPIProvider extends BasicProvider implements BlockAPI { BlockAPIProvider(RequestQueueManager requestQueueManager, String baseUrl, EthHttpClient executor, - Converter converter) { - super(requestQueueManager, "block", baseUrl, executor, converter); + Converter converter, + int retryCount) { + super(requestQueueManager, "block", baseUrl, executor, converter, retryCount); } @NotNull @Override public Optional uncles(long blockNumber) throws EtherScanException { final String urlParam = ACT_BLOCK_PARAM + BLOCKNO_PARAM + blockNumber; - final byte[] response = getRequest(urlParam); - if (response.length == 0) { - return Optional.empty(); - } - try { - final UncleBlockResponseTO responseTO = convert(response, UncleBlockResponseTO.class); + final UncleBlockResponseTO responseTO = getRequest(urlParam, UncleBlockResponseTO.class); if (responseTO.getMessage().startsWith("NOTOK")) { return Optional.empty(); } diff --git a/src/main/java/io/goodforgod/api/etherscan/ContractAPIProvider.java b/src/main/java/io/goodforgod/api/etherscan/ContractAPIProvider.java index 0493f45..898a7b7 100644 --- a/src/main/java/io/goodforgod/api/etherscan/ContractAPIProvider.java +++ b/src/main/java/io/goodforgod/api/etherscan/ContractAPIProvider.java @@ -35,8 +35,9 @@ final class ContractAPIProvider extends BasicProvider implements ContractAPI { ContractAPIProvider(RequestQueueManager requestQueueManager, String baseUrl, EthHttpClient executor, - Converter converter) { - super(requestQueueManager, "contract", baseUrl, executor, converter); + Converter converter, + int retryCount) { + super(requestQueueManager, "contract", baseUrl, executor, converter, retryCount); } @NotNull diff --git a/src/main/java/io/goodforgod/api/etherscan/EthScanAPIBuilder.java b/src/main/java/io/goodforgod/api/etherscan/EthScanAPIBuilder.java index dad9c50..70d9a01 100644 --- a/src/main/java/io/goodforgod/api/etherscan/EthScanAPIBuilder.java +++ b/src/main/java/io/goodforgod/api/etherscan/EthScanAPIBuilder.java @@ -26,6 +26,7 @@ final class EthScanAPIBuilder implements EtherScanAPI.Builder { private final Gson gson = new GsonConfiguration().builder().create(); + private int retryCountOnLimitReach = 0; private String apiKey = DEFAULT_KEY; private RequestQueueManager queueManager; private EthNetwork ethNetwork = EthNetworks.MAINNET; @@ -87,6 +88,16 @@ public EtherScanAPI.Builder withConverter(@NotNull Supplier converter return this; } + @NotNull + public EtherScanAPI.Builder withRetryOnLimitReach(int maxRetryCount) { + if (maxRetryCount < 0 || maxRetryCount > 20) { + throw new IllegalStateException("maxRetryCount value must be in range from 0 to 20, but was: " + maxRetryCount); + } + + this.retryCountOnLimitReach = maxRetryCount; + return this; + } + @Override public @NotNull EtherScanAPI build() { RequestQueueManager requestQueueManager; @@ -99,6 +110,6 @@ public EtherScanAPI.Builder withConverter(@NotNull Supplier converter } return new EtherScanAPIProvider(apiKey, ethNetwork, requestQueueManager, ethHttpClientSupplier.get(), - converterSupplier.get()); + converterSupplier.get(), retryCountOnLimitReach); } } diff --git a/src/main/java/io/goodforgod/api/etherscan/EtherScanAPI.java b/src/main/java/io/goodforgod/api/etherscan/EtherScanAPI.java index 6da3d8f..4e6bc57 100644 --- a/src/main/java/io/goodforgod/api/etherscan/EtherScanAPI.java +++ b/src/main/java/io/goodforgod/api/etherscan/EtherScanAPI.java @@ -1,9 +1,11 @@ package io.goodforgod.api.etherscan; +import io.goodforgod.api.etherscan.error.EtherScanRateLimitException; import io.goodforgod.api.etherscan.http.EthHttpClient; import io.goodforgod.api.etherscan.manager.RequestQueueManager; import java.util.function.Supplier; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Range; /** * EtherScan full API Description ... @@ -62,6 +64,15 @@ interface Builder { @NotNull Builder withConverter(@NotNull Supplier converterSupplier); + /** + * By default is disabled + * + * @param maxRetryCount to retry if {@link EtherScanRateLimitException} thrown + * @return self + */ + @NotNull + EtherScanAPI.Builder withRetryOnLimitReach(@Range(from = 0, to = 20) int maxRetryCount); + @NotNull EtherScanAPI build(); } diff --git a/src/main/java/io/goodforgod/api/etherscan/EtherScanAPIProvider.java b/src/main/java/io/goodforgod/api/etherscan/EtherScanAPIProvider.java index e698f45..ab6e863 100644 --- a/src/main/java/io/goodforgod/api/etherscan/EtherScanAPIProvider.java +++ b/src/main/java/io/goodforgod/api/etherscan/EtherScanAPIProvider.java @@ -26,19 +26,20 @@ final class EtherScanAPIProvider implements EtherScanAPI { EthNetwork network, RequestQueueManager queue, EthHttpClient ethHttpClient, - Converter converter) { + Converter converter, + int retryCount) { // EtherScan 1request\5sec limit support by queue manager final String baseUrl = network.domain() + "?apikey=" + apiKey; this.requestQueueManager = queue; - this.account = new AccountAPIProvider(queue, baseUrl, ethHttpClient, converter); - this.block = new BlockAPIProvider(queue, baseUrl, ethHttpClient, converter); - this.contract = new ContractAPIProvider(queue, baseUrl, ethHttpClient, converter); - this.logs = new LogsAPIProvider(queue, baseUrl, ethHttpClient, converter); - this.proxy = new ProxyAPIProvider(queue, baseUrl, ethHttpClient, converter); - this.stats = new StatisticAPIProvider(queue, baseUrl, ethHttpClient, converter); - this.txs = new TransactionAPIProvider(queue, baseUrl, ethHttpClient, converter); - this.gasTracker = new GasTrackerAPIProvider(queue, baseUrl, ethHttpClient, converter); + this.account = new AccountAPIProvider(queue, baseUrl, ethHttpClient, converter, retryCount); + this.block = new BlockAPIProvider(queue, baseUrl, ethHttpClient, converter, retryCount); + this.contract = new ContractAPIProvider(queue, baseUrl, ethHttpClient, converter, retryCount); + this.logs = new LogsAPIProvider(queue, baseUrl, ethHttpClient, converter, retryCount); + this.proxy = new ProxyAPIProvider(queue, baseUrl, ethHttpClient, converter, retryCount); + this.stats = new StatisticAPIProvider(queue, baseUrl, ethHttpClient, converter, retryCount); + this.txs = new TransactionAPIProvider(queue, baseUrl, ethHttpClient, converter, retryCount); + this.gasTracker = new GasTrackerAPIProvider(queue, baseUrl, ethHttpClient, converter, retryCount); } @NotNull diff --git a/src/main/java/io/goodforgod/api/etherscan/GasTrackerAPIProvider.java b/src/main/java/io/goodforgod/api/etherscan/GasTrackerAPIProvider.java index cbe0a75..ed717a9 100644 --- a/src/main/java/io/goodforgod/api/etherscan/GasTrackerAPIProvider.java +++ b/src/main/java/io/goodforgod/api/etherscan/GasTrackerAPIProvider.java @@ -28,8 +28,9 @@ final class GasTrackerAPIProvider extends BasicProvider implements GasTrackerAPI GasTrackerAPIProvider(RequestQueueManager queue, String baseUrl, EthHttpClient ethHttpClient, - Converter converter) { - super(queue, "gastracker", baseUrl, ethHttpClient, converter); + Converter converter, + int retryCount) { + super(queue, "gastracker", baseUrl, ethHttpClient, converter, retryCount); } @Override diff --git a/src/main/java/io/goodforgod/api/etherscan/LogsAPIProvider.java b/src/main/java/io/goodforgod/api/etherscan/LogsAPIProvider.java index d294fb5..237cafd 100644 --- a/src/main/java/io/goodforgod/api/etherscan/LogsAPIProvider.java +++ b/src/main/java/io/goodforgod/api/etherscan/LogsAPIProvider.java @@ -25,8 +25,9 @@ final class LogsAPIProvider extends BasicProvider implements LogsAPI { LogsAPIProvider(RequestQueueManager queue, String baseUrl, EthHttpClient executor, - Converter converter) { - super(queue, "logs", baseUrl, executor, converter); + Converter converter, + int retryCount) { + super(queue, "logs", baseUrl, executor, converter, retryCount); } @NotNull diff --git a/src/main/java/io/goodforgod/api/etherscan/ProxyAPIProvider.java b/src/main/java/io/goodforgod/api/etherscan/ProxyAPIProvider.java index 4dff589..428b48f 100644 --- a/src/main/java/io/goodforgod/api/etherscan/ProxyAPIProvider.java +++ b/src/main/java/io/goodforgod/api/etherscan/ProxyAPIProvider.java @@ -60,8 +60,9 @@ final class ProxyAPIProvider extends BasicProvider implements ProxyAPI { ProxyAPIProvider(RequestQueueManager queue, String baseUrl, EthHttpClient executor, - Converter converter) { - super(queue, "proxy", baseUrl, executor, converter); + Converter converter, + int retryCount) { + super(queue, "proxy", baseUrl, executor, converter, retryCount); } @Override diff --git a/src/main/java/io/goodforgod/api/etherscan/StatisticAPIProvider.java b/src/main/java/io/goodforgod/api/etherscan/StatisticAPIProvider.java index 131df71..a2bba16 100644 --- a/src/main/java/io/goodforgod/api/etherscan/StatisticAPIProvider.java +++ b/src/main/java/io/goodforgod/api/etherscan/StatisticAPIProvider.java @@ -33,8 +33,9 @@ final class StatisticAPIProvider extends BasicProvider implements StatisticAPI { StatisticAPIProvider(RequestQueueManager queue, String baseUrl, EthHttpClient executor, - Converter converter) { - super(queue, "stats", baseUrl, executor, converter); + Converter converter, + int retry) { + super(queue, "stats", baseUrl, executor, converter, retry); } @NotNull diff --git a/src/main/java/io/goodforgod/api/etherscan/TransactionAPIProvider.java b/src/main/java/io/goodforgod/api/etherscan/TransactionAPIProvider.java index da26b51..7374335 100644 --- a/src/main/java/io/goodforgod/api/etherscan/TransactionAPIProvider.java +++ b/src/main/java/io/goodforgod/api/etherscan/TransactionAPIProvider.java @@ -27,8 +27,9 @@ final class TransactionAPIProvider extends BasicProvider implements TransactionA TransactionAPIProvider(RequestQueueManager queue, String baseUrl, EthHttpClient executor, - Converter converter) { - super(queue, "transaction", baseUrl, executor, converter); + Converter converter, + int retryCount) { + super(queue, "transaction", baseUrl, executor, converter, retryCount); } @NotNull diff --git a/src/main/java/io/goodforgod/api/etherscan/manager/RequestQueueManager.java b/src/main/java/io/goodforgod/api/etherscan/manager/RequestQueueManager.java index 0f36b23..92875d0 100644 --- a/src/main/java/io/goodforgod/api/etherscan/manager/RequestQueueManager.java +++ b/src/main/java/io/goodforgod/api/etherscan/manager/RequestQueueManager.java @@ -17,7 +17,7 @@ public interface RequestQueueManager extends AutoCloseable { * Is used by default when no API KEY is provided */ static RequestQueueManager anonymous() { - return new SemaphoreRequestQueueManager(1, Duration.ofMillis(5015L)); + return new SemaphoreRequestQueueManager(1, Duration.ofMillis(5045L)); } /** @@ -25,19 +25,19 @@ static RequestQueueManager anonymous() { * Free API KEY */ static RequestQueueManager planFree() { - return new SemaphoreRequestQueueManager(5, Duration.ofMillis(1015L)); + return new SemaphoreRequestQueueManager(5, Duration.ofMillis(1045L)); } static RequestQueueManager planStandard() { - return new SemaphoreRequestQueueManager(10, Duration.ofMillis(1015L)); + return new SemaphoreRequestQueueManager(10, Duration.ofMillis(1045L)); } static RequestQueueManager planAdvanced() { - return new SemaphoreRequestQueueManager(20, Duration.ofMillis(1015L)); + return new SemaphoreRequestQueueManager(20, Duration.ofMillis(1045L)); } static RequestQueueManager planProfessional() { - return new SemaphoreRequestQueueManager(30, Duration.ofMillis(1015L)); + return new SemaphoreRequestQueueManager(30, Duration.ofMillis(1045L)); } static RequestQueueManager unlimited() { diff --git a/src/main/java/io/goodforgod/api/etherscan/model/Abi.java b/src/main/java/io/goodforgod/api/etherscan/model/Abi.java index 3536bf9..fbf71be 100644 --- a/src/main/java/io/goodforgod/api/etherscan/model/Abi.java +++ b/src/main/java/io/goodforgod/api/etherscan/model/Abi.java @@ -55,7 +55,7 @@ public int hashCode() { @Override public String toString() { return "Abi{" + - "contractAbi='" + contractAbi + '\'' + + "contractAbi=" + contractAbi + ", isVerified=" + isVerified + '}'; } diff --git a/src/main/java/io/goodforgod/api/etherscan/model/Balance.java b/src/main/java/io/goodforgod/api/etherscan/model/Balance.java index 079d4b6..1d2f743 100644 --- a/src/main/java/io/goodforgod/api/etherscan/model/Balance.java +++ b/src/main/java/io/goodforgod/api/etherscan/model/Balance.java @@ -45,7 +45,7 @@ public int hashCode() { @Override public String toString() { return "Balance{" + - "address='" + address + '\'' + + "address=" + address + ", balance=" + balance + '}'; } diff --git a/src/main/java/io/goodforgod/api/etherscan/model/Block.java b/src/main/java/io/goodforgod/api/etherscan/model/Block.java index 0550000..da1184b 100644 --- a/src/main/java/io/goodforgod/api/etherscan/model/Block.java +++ b/src/main/java/io/goodforgod/api/etherscan/model/Block.java @@ -58,7 +58,7 @@ public String toString() { return "Block{" + "blockNumber=" + blockNumber + ", blockReward=" + blockReward + - ", timeStamp='" + timeStamp + '\'' + + ", timeStamp=" + timeStamp + '}'; } diff --git a/src/main/java/io/goodforgod/api/etherscan/model/BlockUncle.java b/src/main/java/io/goodforgod/api/etherscan/model/BlockUncle.java index 9b110d9..961db7e 100644 --- a/src/main/java/io/goodforgod/api/etherscan/model/BlockUncle.java +++ b/src/main/java/io/goodforgod/api/etherscan/model/BlockUncle.java @@ -19,7 +19,7 @@ public static class Uncle { private BigInteger blockreward; private int unclePosition; - private Uncle() {} + protected Uncle() {} // public String getMiner() { @@ -54,7 +54,7 @@ public int hashCode() { @Override public String toString() { return "Uncle{" + - "miner='" + miner + '\'' + + "miner=" + miner + ", blockreward=" + blockreward + ", unclePosition=" + unclePosition + '}'; @@ -128,9 +128,9 @@ public String getUncleInclusionReward() { @Override public String toString() { return "UncleBlock{" + - "blockMiner='" + blockMiner + '\'' + + "blockMiner=" + blockMiner + ", uncles=" + uncles + - ", uncleInclusionReward='" + uncleInclusionReward + '\'' + + ", uncleInclusionReward=" + uncleInclusionReward + '}'; } diff --git a/src/main/java/io/goodforgod/api/etherscan/model/ContractCreation.java b/src/main/java/io/goodforgod/api/etherscan/model/ContractCreation.java index 0f3d822..2082883 100644 --- a/src/main/java/io/goodforgod/api/etherscan/model/ContractCreation.java +++ b/src/main/java/io/goodforgod/api/etherscan/model/ContractCreation.java @@ -4,15 +4,11 @@ public class ContractCreation { - private final String contractAddress; - private final String contractCreator; - private final String txHash; - - private ContractCreation(String contractAddress, String contractCreator, String txHash) { - this.contractAddress = contractAddress; - this.contractCreator = contractCreator; - this.txHash = txHash; - } + private String contractAddress; + private String contractCreator; + private String txHash; + + protected ContractCreation() {} public String getContractAddress() { return contractAddress; @@ -33,7 +29,8 @@ public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) return false; ContractCreation that = (ContractCreation) o; - return Objects.equals(contractAddress, that.contractAddress) && Objects.equals(contractCreator, that.contractCreator) + return Objects.equals(contractAddress, that.contractAddress) + && Objects.equals(contractCreator, that.contractCreator) && Objects.equals(txHash, that.txHash); } @@ -45,9 +42,9 @@ public int hashCode() { @Override public String toString() { return "ContractCreation{" + - "contractAddress='" + contractAddress + '\'' + - ", contractCreator='" + contractCreator + '\'' + - ", txHash='" + txHash + '\'' + + "contractAddress=" + contractAddress + + ", contractCreator=" + contractCreator + + ", txHash=" + txHash + '}'; } @@ -79,7 +76,11 @@ public ContractCreationBuilder withTxHash(String txHash) { } public ContractCreation build() { - return new ContractCreation(contractAddress, contractCreator, txHash); + ContractCreation contractCreation = new ContractCreation(); + contractCreation.contractAddress = contractAddress; + contractCreation.contractCreator = contractCreator; + contractCreation.txHash = txHash; + return contractCreation; } } } diff --git a/src/main/java/io/goodforgod/api/etherscan/model/EthSupply.java b/src/main/java/io/goodforgod/api/etherscan/model/EthSupply.java index 344e754..c626069 100644 --- a/src/main/java/io/goodforgod/api/etherscan/model/EthSupply.java +++ b/src/main/java/io/goodforgod/api/etherscan/model/EthSupply.java @@ -56,10 +56,10 @@ public int hashCode() { @Override public String toString() { return "EthSupply{" + - "EthSupply='" + EthSupply + '\'' + - ", Eth2Staking='" + Eth2Staking + '\'' + - ", BurntFees='" + BurntFees + '\'' + - ", WithdrawnTotal='" + WithdrawnTotal + '\'' + + "EthSupply=" + EthSupply + + ", Eth2Staking=" + Eth2Staking + + ", BurntFees=" + BurntFees + + ", WithdrawnTotal=" + WithdrawnTotal + '}'; } diff --git a/src/main/java/io/goodforgod/api/etherscan/model/Log.java b/src/main/java/io/goodforgod/api/etherscan/model/Log.java index da6c295..d54766c 100644 --- a/src/main/java/io/goodforgod/api/etherscan/model/Log.java +++ b/src/main/java/io/goodforgod/api/etherscan/model/Log.java @@ -138,16 +138,16 @@ public int hashCode() { @Override public String toString() { return "Log{" + - "blockNumber='" + blockNumber + '\'' + - ", address='" + address + '\'' + - ", transactionHash='" + transactionHash + '\'' + - ", transactionIndex='" + transactionIndex + '\'' + - ", timeStamp='" + timeStamp + '\'' + - ", data='" + data + '\'' + - ", gasPrice='" + gasPrice + '\'' + - ", gasUsed='" + gasUsed + '\'' + + "blockNumber=" + blockNumber + + ", address=" + address + + ", transactionHash=" + transactionHash + + ", transactionIndex=" + transactionIndex + + ", timeStamp=" + timeStamp + + ", data=" + data + + ", gasPrice=" + gasPrice + + ", gasUsed=" + gasUsed + ", topics=" + topics + - ", logIndex='" + logIndex + '\'' + + ", logIndex=" + logIndex + '}'; } diff --git a/src/main/java/io/goodforgod/api/etherscan/model/Price.java b/src/main/java/io/goodforgod/api/etherscan/model/Price.java index 565dbed..403b705 100644 --- a/src/main/java/io/goodforgod/api/etherscan/model/Price.java +++ b/src/main/java/io/goodforgod/api/etherscan/model/Price.java @@ -67,8 +67,8 @@ public String toString() { return "Price{" + "ethusd=" + ethusd + ", ethbtc=" + ethbtc + - ", ethusd_timestamp='" + ethusd_timestamp + '\'' + - ", ethbtc_timestamp='" + ethbtc_timestamp + '\'' + + ", ethusd_timestamp=" + ethusd_timestamp + + ", ethbtc_timestamp=" + ethbtc_timestamp + '}'; } diff --git a/src/main/java/io/goodforgod/api/etherscan/model/Status.java b/src/main/java/io/goodforgod/api/etherscan/model/Status.java index 052c187..41b598a 100644 --- a/src/main/java/io/goodforgod/api/etherscan/model/Status.java +++ b/src/main/java/io/goodforgod/api/etherscan/model/Status.java @@ -45,7 +45,7 @@ public int hashCode() { public String toString() { return "Status{" + "isError=" + isError + - ", errDescription='" + errDescription + '\'' + + ", errDescription=" + errDescription + '}'; } diff --git a/src/main/java/io/goodforgod/api/etherscan/model/TokenBalance.java b/src/main/java/io/goodforgod/api/etherscan/model/TokenBalance.java index bb40ee2..c257654 100644 --- a/src/main/java/io/goodforgod/api/etherscan/model/TokenBalance.java +++ b/src/main/java/io/goodforgod/api/etherscan/model/TokenBalance.java @@ -39,7 +39,7 @@ public int hashCode() { @Override public String toString() { return "TokenBalance{" + - "tokenContract='" + tokenContract + '\'' + + "tokenContract=" + tokenContract + '}'; } } diff --git a/src/main/java/io/goodforgod/api/etherscan/model/Tx.java b/src/main/java/io/goodforgod/api/etherscan/model/Tx.java index 7ef0e22..0a836d1 100644 --- a/src/main/java/io/goodforgod/api/etherscan/model/Tx.java +++ b/src/main/java/io/goodforgod/api/etherscan/model/Tx.java @@ -35,21 +35,21 @@ public String getTxReceiptStatus() { public String toString() { return "Tx{" + "value=" + value + - ", isError='" + isError + '\'' + - ", txreceipt_status='" + txreceipt_status + '\'' + + ", isError=" + isError + + ", txreceipt_status=" + txreceipt_status + ", nonce=" + nonce + - ", blockHash='" + blockHash + '\'' + + ", blockHash=" + blockHash + ", transactionIndex=" + transactionIndex + ", confirmations=" + confirmations + ", gasPrice=" + gasPrice + ", cumulativeGasUsed=" + cumulativeGasUsed + ", blockNumber=" + blockNumber + - ", timeStamp='" + timeStamp + '\'' + - ", hash='" + hash + '\'' + - ", from='" + from + '\'' + - ", to='" + to + '\'' + - ", contractAddress='" + contractAddress + '\'' + - ", input='" + input + '\'' + + ", timeStamp=" + timeStamp + + ", hash=" + hash + + ", from=" + from + + ", to=" + to + + ", contractAddress=" + contractAddress + + ", input=" + input + ", gas=" + gas + ", gasUsed=" + gasUsed + '}'; diff --git a/src/main/java/io/goodforgod/api/etherscan/model/TxErc1155.java b/src/main/java/io/goodforgod/api/etherscan/model/TxErc1155.java index 16d4457..f0b1ce4 100644 --- a/src/main/java/io/goodforgod/api/etherscan/model/TxErc1155.java +++ b/src/main/java/io/goodforgod/api/etherscan/model/TxErc1155.java @@ -56,23 +56,23 @@ public int hashCode() { @Override public String toString() { return "TxErc1155{" + - "tokenID='" + tokenID + '\'' + - ", tokenName='" + tokenName + '\'' + - ", tokenSymbol='" + tokenSymbol + '\'' + - ", tokenValue='" + tokenValue + '\'' + + "tokenID=" + tokenID + + ", tokenName=" + tokenName + + ", tokenSymbol=" + tokenSymbol + + ", tokenValue=" + tokenValue + ", nonce=" + nonce + - ", blockHash='" + blockHash + '\'' + + ", blockHash=" + blockHash + ", transactionIndex=" + transactionIndex + ", confirmations=" + confirmations + ", gasPrice=" + gasPrice + ", cumulativeGasUsed=" + cumulativeGasUsed + ", blockNumber=" + blockNumber + - ", timeStamp='" + timeStamp + '\'' + - ", hash='" + hash + '\'' + - ", from='" + from + '\'' + - ", to='" + to + '\'' + - ", contractAddress='" + contractAddress + '\'' + - ", input='" + input + '\'' + + ", timeStamp=" + timeStamp + + ", hash=" + hash + + ", from=" + from + + ", to=" + to + + ", contractAddress=" + contractAddress + + ", input=" + input + ", gas=" + gas + ", gasUsed=" + gasUsed + '}'; diff --git a/src/main/java/io/goodforgod/api/etherscan/model/TxErc20.java b/src/main/java/io/goodforgod/api/etherscan/model/TxErc20.java index 3dc22fd..1d6080e 100644 --- a/src/main/java/io/goodforgod/api/etherscan/model/TxErc20.java +++ b/src/main/java/io/goodforgod/api/etherscan/model/TxErc20.java @@ -58,22 +58,22 @@ public int hashCode() { public String toString() { return "TxErc20{" + "value=" + value + - ", tokenName='" + tokenName + '\'' + - ", tokenSymbol='" + tokenSymbol + '\'' + - ", tokenDecimal='" + tokenDecimal + '\'' + + ", tokenName=" + tokenName + + ", tokenSymbol=" + tokenSymbol + + ", tokenDecimal=" + tokenDecimal + ", nonce=" + nonce + - ", blockHash='" + blockHash + '\'' + + ", blockHash=" + blockHash + ", transactionIndex=" + transactionIndex + ", confirmations=" + confirmations + ", gasPrice=" + gasPrice + ", cumulativeGasUsed=" + cumulativeGasUsed + ", blockNumber=" + blockNumber + - ", timeStamp='" + timeStamp + '\'' + - ", hash='" + hash + '\'' + - ", from='" + from + '\'' + - ", to='" + to + '\'' + - ", contractAddress='" + contractAddress + '\'' + - ", input='" + input + '\'' + + ", timeStamp=" + timeStamp + + ", hash=" + hash + + ", from=" + from + + ", to=" + to + + ", contractAddress=" + contractAddress + + ", input=" + input + ", gas=" + gas + ", gasUsed=" + gasUsed + '}'; diff --git a/src/main/java/io/goodforgod/api/etherscan/model/TxErc721.java b/src/main/java/io/goodforgod/api/etherscan/model/TxErc721.java index 2180019..1ac49a0 100644 --- a/src/main/java/io/goodforgod/api/etherscan/model/TxErc721.java +++ b/src/main/java/io/goodforgod/api/etherscan/model/TxErc721.java @@ -56,23 +56,23 @@ public int hashCode() { @Override public String toString() { return "TxErc721{" + - "tokenID='" + tokenID + '\'' + - ", tokenName='" + tokenName + '\'' + - ", tokenSymbol='" + tokenSymbol + '\'' + - ", tokenDecimal='" + tokenDecimal + '\'' + + "tokenID=" + tokenID + + ", tokenName=" + tokenName + + ", tokenSymbol=" + tokenSymbol + + ", tokenDecimal=" + tokenDecimal + ", nonce=" + nonce + - ", blockHash='" + blockHash + '\'' + + ", blockHash=" + blockHash + ", transactionIndex=" + transactionIndex + ", confirmations=" + confirmations + ", gasPrice=" + gasPrice + ", cumulativeGasUsed=" + cumulativeGasUsed + ", blockNumber=" + blockNumber + - ", timeStamp='" + timeStamp + '\'' + - ", hash='" + hash + '\'' + - ", from='" + from + '\'' + - ", to='" + to + '\'' + - ", contractAddress='" + contractAddress + '\'' + - ", input='" + input + '\'' + + ", timeStamp=" + timeStamp + + ", hash=" + hash + + ", from=" + from + + ", to=" + to + + ", contractAddress=" + contractAddress + + ", input=" + input + ", gas=" + gas + ", gasUsed=" + gasUsed + '}'; diff --git a/src/main/java/io/goodforgod/api/etherscan/model/TxInternal.java b/src/main/java/io/goodforgod/api/etherscan/model/TxInternal.java index a61cf83..389f456 100644 --- a/src/main/java/io/goodforgod/api/etherscan/model/TxInternal.java +++ b/src/main/java/io/goodforgod/api/etherscan/model/TxInternal.java @@ -69,17 +69,17 @@ public int hashCode() { public String toString() { return "TxInternal{" + "value=" + value + - ", type='" + type + '\'' + - ", traceId='" + traceId + '\'' + + ", type=" + type + + ", traceId=" + traceId + ", isError=" + isError + - ", errCode='" + errCode + '\'' + + ", errCode=" + errCode + ", blockNumber=" + blockNumber + - ", timeStamp='" + timeStamp + '\'' + - ", hash='" + hash + '\'' + - ", from='" + from + '\'' + - ", to='" + to + '\'' + - ", contractAddress='" + contractAddress + '\'' + - ", input='" + input + '\'' + + ", timeStamp=" + timeStamp + + ", hash=" + hash + + ", from=" + from + + ", to=" + to + + ", contractAddress=" + contractAddress + + ", input=" + input + ", gas=" + gas + ", gasUsed=" + gasUsed + '}'; diff --git a/src/main/java/io/goodforgod/api/etherscan/model/proxy/BlockProxy.java b/src/main/java/io/goodforgod/api/etherscan/model/proxy/BlockProxy.java index 4a2b624..bee4d64 100644 --- a/src/main/java/io/goodforgod/api/etherscan/model/proxy/BlockProxy.java +++ b/src/main/java/io/goodforgod/api/etherscan/model/proxy/BlockProxy.java @@ -162,25 +162,25 @@ public int hashCode() { @Override public String toString() { return "BlockProxy{" + - "number='" + number + '\'' + - ", hash='" + hash + '\'' + - ", parentHash='" + parentHash + '\'' + - ", stateRoot='" + stateRoot + '\'' + - ", size='" + size + '\'' + - ", difficulty='" + difficulty + '\'' + - ", totalDifficulty='" + totalDifficulty + '\'' + - ", timestamp='" + timestamp + '\'' + - ", miner='" + miner + '\'' + - ", nonce='" + nonce + '\'' + - ", extraData='" + extraData + '\'' + - ", logsBloom='" + logsBloom + '\'' + - ", mixHash='" + mixHash + '\'' + - ", gasUsed='" + gasUsed + '\'' + - ", gasLimit='" + gasLimit + '\'' + - ", sha3Uncles='" + sha3Uncles + '\'' + + "number=" + number + + ", hash=" + hash + + ", parentHash=" + parentHash + + ", stateRoot=" + stateRoot + + ", size=" + size + + ", difficulty=" + difficulty + + ", totalDifficulty=" + totalDifficulty + + ", timestamp=" + timestamp + + ", miner=" + miner + + ", nonce=" + nonce + + ", extraData=" + extraData + + ", logsBloom=" + logsBloom + + ", mixHash=" + mixHash + + ", gasUsed=" + gasUsed + + ", gasLimit=" + gasLimit + + ", sha3Uncles=" + sha3Uncles + ", uncles=" + uncles + - ", receiptsRoot='" + receiptsRoot + '\'' + - ", transactionsRoot='" + transactionsRoot + '\'' + + ", receiptsRoot=" + receiptsRoot + + ", transactionsRoot=" + transactionsRoot + ", transactions=" + transactions + '}'; } diff --git a/src/main/java/io/goodforgod/api/etherscan/model/proxy/ReceiptProxy.java b/src/main/java/io/goodforgod/api/etherscan/model/proxy/ReceiptProxy.java index e6df01c..d88fd6d 100644 --- a/src/main/java/io/goodforgod/api/etherscan/model/proxy/ReceiptProxy.java +++ b/src/main/java/io/goodforgod/api/etherscan/model/proxy/ReceiptProxy.java @@ -115,18 +115,18 @@ public int hashCode() { @Override public String toString() { return "ReceiptProxy{" + - "root='" + root + '\'' + - ", from='" + from + '\'' + - ", to='" + to + '\'' + - ", blockNumber='" + blockNumber + '\'' + - ", blockHash='" + blockHash + '\'' + - ", transactionHash='" + transactionHash + '\'' + - ", transactionIndex='" + transactionIndex + '\'' + - ", gasUsed='" + gasUsed + '\'' + - ", cumulativeGasUsed='" + cumulativeGasUsed + '\'' + - ", contractAddress='" + contractAddress + '\'' + + "root=" + root + + ", from=" + from + + ", to=" + to + + ", blockNumber=" + blockNumber + + ", blockHash=" + blockHash + + ", transactionHash=" + transactionHash + + ", transactionIndex=" + transactionIndex + + ", gasUsed=" + gasUsed + + ", cumulativeGasUsed=" + cumulativeGasUsed + + ", contractAddress=" + contractAddress + ", logs=" + logs + - ", logsBloom='" + logsBloom + '\'' + + ", logsBloom=" + logsBloom + '}'; } diff --git a/src/main/java/io/goodforgod/api/etherscan/model/proxy/TxProxy.java b/src/main/java/io/goodforgod/api/etherscan/model/proxy/TxProxy.java index 70b4fd7..0a89921 100644 --- a/src/main/java/io/goodforgod/api/etherscan/model/proxy/TxProxy.java +++ b/src/main/java/io/goodforgod/api/etherscan/model/proxy/TxProxy.java @@ -127,20 +127,20 @@ public int hashCode() { @Override public String toString() { return "TxProxy{" + - "to='" + to + '\'' + - ", hash='" + hash + '\'' + - ", transactionIndex='" + transactionIndex + '\'' + - ", from='" + from + '\'' + - ", v='" + v + '\'' + - ", input='" + input + '\'' + - ", s='" + s + '\'' + - ", r='" + r + '\'' + - ", nonce='" + nonce + '\'' + - ", value='" + value + '\'' + - ", gas='" + gas + '\'' + - ", gasPrice='" + gasPrice + '\'' + - ", blockHash='" + blockHash + '\'' + - ", blockNumber='" + blockNumber + '\'' + + "to=" + to + + ", hash=" + hash + + ", transactionIndex=" + transactionIndex + + ", from=" + from + + ", v=" + v + + ", input=" + input + + ", s=" + s + + ", r=" + r + + ", nonce=" + nonce + + ", value=" + value + + ", gas=" + gas + + ", gasPrice=" + gasPrice + + ", blockHash=" + blockHash + + ", blockNumber=" + blockNumber + '}'; } diff --git a/src/test/java/io/goodforgod/api/etherscan/ApiRunner.java b/src/test/java/io/goodforgod/api/etherscan/ApiRunner.java index a6c43ac..72aeeff 100644 --- a/src/test/java/io/goodforgod/api/etherscan/ApiRunner.java +++ b/src/test/java/io/goodforgod/api/etherscan/ApiRunner.java @@ -29,6 +29,7 @@ public class ApiRunner extends Assertions { .withApiKey(ApiRunner.API_KEY) .withNetwork(EthNetworks.MAINNET) .withQueue(queueManager) + .withRetryOnLimitReach(5) .build(); } From 3405883f524a98b8c5c5848ddd862b140cac28aa Mon Sep 17 00:00:00 2001 From: Anton Kurako Date: Fri, 6 Oct 2023 02:10:53 +0300 Subject: [PATCH 06/18] [2.1.0-SNAPSHOT] build.gradle updated for new CI CONTRIBUTING.md added README.md updated --- CONTRIBUTING.md | 23 +++++++++++ README.md | 7 ++-- _config.yml | 1 - build.gradle | 48 +++++++++++++++++++---- gradle/wrapper/gradle-wrapper.jar | Bin 59536 -> 61574 bytes gradle/wrapper/gradle-wrapper.properties | 3 +- gradlew | 18 +++++++-- gradlew.bat | 15 ++++--- 8 files changed, 92 insertions(+), 23 deletions(-) create mode 100644 CONTRIBUTING.md delete mode 100644 _config.yml diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..5abd8dc --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,23 @@ +# Contributing Code or Documentation Guide + +## Running Tests + +The new code should contain tests that check new behavior. + +Run tests `./gradlew test` to check that code works as behavior. + +## Code Style + +The code base should remain clean, following industry best practices for organization, javadoc and style, as much as possible. + +To run the Code Style check use `./gradlew spotlessCheck`. + +If check found any errors, you can apply Code Style by running `./gradlew spotlessApply` + +## Creating a pull request + +Once you are satisfied with your changes: + +- Commit changes to the local branch you created. +- Push that branch with changes to the corresponding remote branch on GitHub +- Submit a [pull request](https://help.github.com/articles/creating-a-pull-request) to `dev` branch. \ No newline at end of file diff --git a/README.md b/README.md index dd244b5..c086a6b 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ # Java EtherScan API [![Minimum required Java version](https://img.shields.io/badge/Java-1.8%2B-blue?logo=openjdk)](https://openjdk.org/projects/jdk8/) -[![GitHub Action](https://github.com/goodforgod/java-etherscan-api/workflows/Java%20CI/badge.svg)](https://github.com/GoodforGod/java-etherscan-api/actions?query=workflow%3A%22Java+CI%22) +[![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.github.goodforgod/java-etherscan-api/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.github.goodforgod/java-etherscan-api) +[![Java CI](https://github.com/GoodforGod/java-etherscan-api/workflows/CI%20Master/badge.svg)](https://github.com/GoodforGod/java-etherscan-api/actions?query=workflow%3ACI+Master) [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=GoodforGod_java-etherscan-api&metric=coverage)](https://sonarcloud.io/dashboard?id=GoodforGod_java-etherscan-api) [![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=GoodforGod_java-etherscan-api&metric=sqale_rating)](https://sonarcloud.io/dashboard?id=GoodforGod_java-etherscan-api) [![Lines of Code](https://sonarcloud.io/api/project_badges/measure?project=GoodforGod_java-etherscan-api&metric=ncloc)](https://sonarcloud.io/dashboard?id=GoodforGod_java-etherscan-api) @@ -14,7 +15,7 @@ Library supports EtherScan *API* for all available *Ethereum Networks* for *ethe **Gradle** ```groovy -implementation "com.github.goodforgod:java-etherscan-api:2.0.0" +implementation "com.github.goodforgod:java-etherscan-api:2.1.0" ``` **Maven** @@ -22,7 +23,7 @@ implementation "com.github.goodforgod:java-etherscan-api:2.0.0" com.github.goodforgod java-etherscan-api - 2.0.0 + 2.1.0 ``` diff --git a/_config.yml b/_config.yml deleted file mode 100644 index 2f7efbe..0000000 --- a/_config.yml +++ /dev/null @@ -1 +0,0 @@ -theme: jekyll-theme-minimal \ No newline at end of file diff --git a/build.gradle b/build.gradle index 3d766c2..7dcf4c7 100644 --- a/build.gradle +++ b/build.gradle @@ -3,8 +3,9 @@ plugins { id "java-library" id "maven-publish" - id "org.sonarqube" version "3.3" - id "com.diffplug.spotless" version "6.12.0" + id "org.sonarqube" version "4.3.0.3225" + id "com.diffplug.spotless" version "6.19.0" + id "io.github.gradle-nexus.publish-plugin" version "1.3.0" } repositories { @@ -13,7 +14,8 @@ repositories { } group = groupId -version = artifactVersion +var ver = System.getenv().getOrDefault("RELEASE_VERSION", artifactVersion) +version = ver.startsWith("v") ? ver.substring(1) : ver sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 @@ -28,6 +30,7 @@ dependencies { } test { + failFast(false) useJUnitPlatform() testLogging { events("passed", "skipped", "failed") @@ -36,9 +39,13 @@ test { } reports { - html.enabled(false) - junitXml.enabled(false) + html.required = false + junitXml.required = false } + + environment([ + "": "", + ]) } spotless { @@ -46,7 +53,7 @@ spotless { encoding("UTF-8") importOrder() removeUnusedImports() - eclipse("4.21.0").configFile("${rootDir}/config/codestyle.xml") + eclipse("4.21").configFile("${rootDir}/config/codestyle.xml") } } @@ -58,6 +65,18 @@ sonarqube { } } +nexusPublishing { + packageGroup = groupId + repositories { + sonatype { + username = System.getenv("OSS_USERNAME") + password = System.getenv("OSS_PASSWORD") + nexusUrl.set(uri("https://oss.sonatype.org/service/local/")) + snapshotRepositoryUrl.set(uri("https://oss.sonatype.org/content/repositories/snapshots/")) + } + } +} + publishing { publications { mavenJava(MavenPublication) { @@ -99,6 +118,16 @@ publishing { password System.getenv("OSS_PASSWORD") } } + if (!version.endsWith("SNAPSHOT")) { + maven { + name = "GitHubPackages" + url = "https://maven.pkg.github.com/GoodforGod/$artifactId" + credentials { + username = System.getenv("GITHUB_ACTOR") + password = System.getenv("GITHUB_TOKEN") + } + } + } } } @@ -116,7 +145,7 @@ tasks.withType(JavaCompile) { check.dependsOn jacocoTestReport jacocoTestReport { reports { - xml.enabled true + xml.required = true html.destination file("${buildDir}/jacocoHtml") } } @@ -128,9 +157,12 @@ javadoc { } } -if (project.hasProperty("signing.keyId")) { +if (project.hasProperty("signingKey")) { apply plugin: "signing" signing { + def signingKey = findProperty("signingKey") + def signingPassword = findProperty("signingPassword") + useInMemoryPgpKeys(signingKey, signingPassword) sign publishing.publications.mavenJava } } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 7454180f2ae8848c63b8b4dea2cb829da983f2fa..943f0cbfa754578e88a3dae77fce6e3dea56edbf 100644 GIT binary patch delta 36900 zcmaI7V{m3&)UKP3ZQHh;j&0kvlMbHPwrx94Y}@X*V>{_2yT4s~SDp9Nsq=5uTw|_Z z*SyDA;~q0%0W54Etby(aY}o0VClxFRhyhkI3lkf_7jK2&%Ygpl=wU>3Rs~ZgXSj(C z9wu-Y1}5%m9g+euEqOU4N$)b6f%GhAiAKT7S{5tUZQ+O8qA*vXC@1j8=Hd@~>p~x- z&X>HDXCKd|8s~KfK;O~X@9)nS-#H{9?;Af5&gdstgNg%}?GllZ=%ag+j&895S#>oj zCkO*T+1@d%!}B4Af42&#LFvJYS1eKc>zxiny{a-5%Ej$3?^j5S_5)6c_G+!8pxufC zd9P-(56q5kbw)>3XQ7K853PQh24-~p}L;HQuyEO+s)M^Gk)Y#4fr1I*ySS6Z>g^ z3j2|yAwKXw?b#D4wNzK4zxeH;LuAJJct5s&k>(Qc2tH}2R3kpSJ)aaz!4*)5Vepww zWc0`u&~Lj*^{+V~D(lFTr?Eemqm3a{8wwF}l_dQsAQURmW$Bm$^?R10r)Xd_(HUYG zN)trq(ix@qb6alE>CCw@_H0*-r?5@|Fbx<6itm$^Qt~aj+h+Vd7l?ycraz%`lP%aB ziO6K|F?9|uUnx$T5aqKdAs74ED7SPSfzocG)~*66q;Yb=gB{=6k{ub6ho3Y`=;SnB z;W96mM@c5#(3(N~i_;u05{yUL8-BBVd|Z@8@(TO#gk&+1Ek#oDaZ?RNw{yG|z+^vm zz_8?GT|RX|oO;EH*3wMsfQTe(p6)G9a)6&yM+tYvZwg;#pZsdueT#%;G9gwXq%a(| zl*TBJYLyjOBS4he@nGA-CofFCVpGz!${(Qa{d?g*Yt zftsoLCHu-*AoZMC;gVx%qEKPVg@Ca2X(0LIQMr5^-B;1b)$5s^R@wa}C&FS9hr_0< zR(PnkT$}=;M;g}bw|7HERCSm?{<0JLnk{!U8*bbod@i#tj?Jr}|IcqMfaed&D?MHW zQQ>7BEPK-|c&@kx4femtLMpewFrq`MVIB%4e_8@IyFi9-$z0o48vnBWlh@E7Lz`C& z{~7u$g;@syjzMCZR|Nm+Jx^T!cp)q9$P*jxSQZ3le#HSIj=wN~)myB;srp0eMln_T z6?=}jUvU5_s4rEcO3k}*z#DQrR;TOvZGc03OR0)P5RI8M<#*B)8fYxxxX(I`Dks;X z_q5?sAs zMlaiDTP-1_XRMwL(q5h(W2yvr9HmtlnR);!9>U%TyViU)t#_5B#W0DnP!P#s!my-T zqbgQRIf%MWo*YUK2vXE8RIy;gJ8p^LU$c6POWt88``5^mIqohk~I!a zv-T{zI?eSLajm^r3>inooK|w$a_2H9J=;|sziKGRQ&FC5CWUF*#N6?n4rD-}S>Eg!tFkOpE7otS)$s3hyim=Ldy&-I$%Yra=M3xIOG{Jc zr8d_wbB301%Zy*8ILfeRiGfeQUIh2N3|41xAR|uvQ%?AIGUkdX*Ymgh z54d1)Igp9~)o7-h8AAH#6DzJ}UPh+srx=B^tGe~_(uwPoOov8sptn}$Rx@&$Ox^8H z!MND`vATA1%mR>+iCrV=b!*TSrj2TDv?Fnmj$=uw{JX1c$tt@zIC9gt)3Inpb+Q~= zh0Y@1o@R7|g+n0^b;v#5cc24{OYlnusF0tun^X?qHRYl#m%6UY?tK9vA zvtPnt7tgpi=qBIQ{v=D|p=4@{^E7)c3MLDCNMKPYec~o)VJ6zmZRE?UqXgYj7O~uG z^YQwQfQr>T!u&NaBfm|PW%g%cDoE8%t<-Ma$wIkMS{3sTS+aWpx=g7(+XtaLt9nqB zrLi<%uH29tuKZ6?`Ka5N0@G{F134GZ+6+RnA|Y+wCs~N*%N4CxyoB6?*{>AMy4w}` z@CMj>CaC}<;Y&#-a6~6AB=v2>)b=&t&D7SK6Vc4p+Tfg{AO(<+v?R1IsPA~@FvGJw z*d@a@6bydfT8{(k2N*D`FO@sUHbUIw4kQ(jrMPa2Mjc&~AK*xoe*c+VfsGx$cnzHQb4bSL2wJvVg>oYR*?s}CgoHMPLwA`Km%5LJm4a&OZ3QL*-+4G0t%;_ zS|DOILXL@I?hGl*3JvMq)Uq;%_B{$ipS*Qkn~F!-P^6Afg;Qf!n-zi$tpUjh9TEgk z$Em>`JJ(>S;8ZLM+$-RWUzFrR!@<;W=Y3ASjLR1`U zRnQ{ZU%JK?(2oo+c(5g;5Ez&I&5{C8{!I?aB34uFL`IQg#2z;=$Si?P0|qnfM1VdS zb6@5YL(+>w;EPEyeuX)yIA~VlFjk5^LQ^)aZ$<1LmDozK0cxH1z>q2*h5eR(*B8Pj6nS=K`)S3FLEV-S*4c;F0<9nRRu$YqiDCFaTc zU2LxT3wJJWeBb8}%B59!#)-W}_%?lSsy~vH3%oytE`j-^9*~SvMr-z3q=A7uy$?X& zf*Ky)z&7X0jy`YDtCs@NJw0+j_3CeDw_I25HR6CPV2t!asKPJV^R_r+u&LUxP)wtR zmFA-~HswLN)Ts=7{YPysG?DY))3+-L*En93o=+v+Kjw;_cUsONDZ!zzk{1O05Wm+3 z*2;}O&??lNOe-V{mDB}Gn<0_7H$ZCa5dWoq#}QCT(~h%=J=n@;@VXR52l^?vcj%GP zh7{kjosPu`1x+iQVU?(TJ^?xlT@AS>a?&FMQRTyRO?(2jczyS@T%&!d8mzxqO0r&;UjTNkbB)J1%*iB$McM0+stU%2(C}f0}_{G?dWaCGjmX7PnOq1 zdRr-MGfS#yqMH&mW5BiJE3#|^%`(niIKQ_BQ7xk`QFp50^I!yunb~0m24`10O=`w3 zc#^=Ae(B8CPKMDwLljERn*+I@7u8~-_2TPH`L# z=1~{&_1Fg{r>4*vu5rRTtDZ3}td&uZ)(p*OD4xfn01zzS+v3c_N~GkBgN$cm$Y%H} z1sPjxf=IxdrC~^)&Pvq1^e`~xXM2! zYU)LU02y$#S?v+CQ~GP{$|nR0d%`>hOlNwPU0Rr{E9ss;_>+ymGd10ASM{eJn+1RF zT}SD!JV-q&r|%0BQcGcRzR&sW)3v$3{tIN=O!JC~9!o8rOP6q=LW3BvlF$48 ziauC6R(9yToYA82viRfL#)tA@_TW;@)DcknleX^H4y+0kpRm zT&&(g50ZC+K(O0ZX6thiJEA8asDxF-J$*PytBYttTHI&)rXY!*0gdA9%@i#Sme5TY z(K6#6E@I~B?eoIu!{?l}dgxBz!rLS{3Q4PhpCSpxt4z#Yux6?y7~I=Yc?6P%bOq~j zI*D}tM^VMu{h6(>+IP|F8QYN`u{ziSK)DC*4*L>I4LoUwdEX_n{knkLwS`D-NRr>0 z&g8^|y3R$61{TgSK6)9&JZFhtApbp$KzF13WaC(QKwAZ|peA@Aol`&*>8RK(2|0%R zyo9nL{gtv}osWeNwLf@YG!wb9H2WRcYhg_DT60dzQGW(y7h7|4U*<;c*4N*sE2sdR zZRP^g;h(t0JLIuv)VNY6gZ)yUD)2d)p?eFznY8$~EZMYTiu%DF*7UeVQPV}h zF*|ls`|a+{u;cd>D@%~dRZBn~-Ac+m&Vg>P=3VY8+$<7Zi7p<~Nq zR^M^jl=zI!T`8H(gK0H945KY=N1J#Up`sWvfY$>1SGEfqEyKIokPVbexYnI`OXJF$ zkMS3dBE8RnB1dK)tJbNSu5Y&$IYBy38luzK-TGMpQcEojhte7Xff-zI50I2qM(i2F2)9DdagoKYlK zz%x8sxFf>5@1bI$-n*}N>o3o#^zP{$d7pf& zf*4SNbn9QDXDCVn;wo6|E0$(wBv*pgxHCA(S3lXJ4HMQW)rU}U7?F zxI}V}W~d>wx97Ozh+^glLBo{*j$o`=hK;idHhi4CG!_fG89V-Ew-^^hhMOWUdu-2< zd(t0O>8BgZ1N<2Xi1G3>r1@d)nBD*K3PsmP{s{&G;tmG_!k=7FNuKO+fCm`SxKP>B zK>mtj;Etn5J%mKvT;yE_zl8vk?q3f9hwea!Dt8yLUCgFO*BnS=YuY}-c!&0jb}J)D zV(s~BTYfVyXK<9y&hpVuS= zc!!wNsFjPgspRhCIw6}w^RvLX#?KnhpM(hB`U3x zg*!~MI$JfAFWhsN7xRdV^%0aygs+rZ;dpWzncKOTAa`0Xq7m(z zS_LwFYW$1KXsfgpFzlw7r#2KOQn(%ww?YQ$bT(GWx*gx2Bsny3J z!6UUPr8>TIGiK`%2m`PSS3Pd36m#OIl#SN?$h?mU25XXidM(*ZGBAelMO)H+;9Uw= z8`vjt5)+09c$b2FAWm3{jId9*ui3~Ihbw`9e-2;@?!T%Dqin&WFbQJt4_m@V=j9P* zbXi|lvH3x49-&)RB5c* zheg*i@5p((w*%DOB8-%Yv2P#-IHB%v>`Y&_9BR4)7ngJze2&>4c~NOkQnJ)jt+X$L z9`^6#2vV*K89hV$gu10|zu~;nKfa?ohox&sMS7NyTlMJCQAe^h{9nZwpoX?uy5xO? zW@PBU$b1{UOpv~AtZ#<+*z+(g?Fjwseh8lsxs5iozi*#gI!;qXBt)G~j z9v5n^MQKOT?2!Dj8;SOO0>6f3orwHJiOFK6`b<|b^4}5n{l-VQ?SoksHS=yv3$O(l zK4aL#0Zq4{g#z$jo$*dAJfuB~zb-n^5(3@{JHT~GGc;Ky(^y99NCxW2rZg%U^gIg; zJ%kBn@NxZn`e|BO6V4* z39i>kJU<7SyAHVHI%uKdcv|~U@W=4e@t=p!S?jnBEq^yQ2E14shzIlXKC?om(H84vN=o^2NtMBm7J~D=rmbm*NWjSVJeDEz-N5UmBk5`GjywWp zZ6s1IpXkUutr~lnCT>!2PPR9DIkuVbt|MCCR|#D(rD%~B zubEU^cc78hxs+x%Vg6$X@16i4ob@ek?PQijQzieZfi>E5NEg`76N6^2(v~ar1-yk2 z{{lAO$SjM{aof;NApyxnbEZnRO}8?!fT!U_<`21g+Y&qC_&99r6|*kDkDETgh-Blb z?9T7UIB}thISUzkw0O~5y~+>wtL{7Fc;gSldH8639yf31)qi4|Wq~g>_I0dfs^OGe z!K&|A^L|jeya>y7<>8(f3SXza9%^rl#3_31Neefn#Uk7*_^}IkM)e_&Fg~Ughu3}B zG0}?Kod{eb?94;$6dD4YV>n9mC5+Hy8M_h+bQmvUNvJ>0P#9a~pPDU9l#NrDP39Z> z7R3hA*IMVAod6Yl=s=BNyrblFv9ahxsA&Gst+0`2T@WSesGH1hRhw z#t7Smp){oxPiCm!XedMT9Xls`K+YKLV>+PC>98;G(5Lw*eBS5`f9B8Y2br|#y@jcz z`ddmVevy*mwN3@%YsE|Fsj!mu|5S)>5)wx;dbtMZ6Z1juCz$0kMS5-C{B5qnD{7ViiFNTv<&?w+5J7 zOvuImg^_o-ySHEQGAp-85!m8;Kjq_i-SzRFWcdAdj|VdIswTnUkggogN4`x{jEyG? zQ*_r9na<4wW8fySLr;PuoDVKKN@|y=99HWqBR+2kiH1prFkUgL{}*5_>twEG!W=|` z!(x}*NZ|P}Bf#p=-xK3y2>!x$6v(pYq)(6dQWk)$ZWSp%-^30dq``oVSfEWcTXE)1aMtpTQ;FW3e5ffMASm16(q#bJ}PAM2+l8m-{ z*nkDPH}ha-U3r{s>8XetSzpDN&nlc>|Er_gOMq?H8gtx5_)=$=rKn8D)UFKeitTF< zrA6>w`_sOEN&t!qEx|Pjw>cpv6y3zP58py3u%=88_f1w?Dh6qHi_=ps1{zKT3c+AJ z-CHtS&YwELV7i&XOXFt+doDFc=HdO@cjpeR_V#?~+=e|BdnS5C#8DCu@>*3!I9V9< zW8$!NLpp)$6Dt$s16B6U0ukr;dz~cWFIBq~D_Il@v4E@wH%Sf#P50K?&Z#GHc^JwQ5QyPaJatDTEbA97~OHLu)q6tU>srf)aJKx!w!`g-`+$hp=yl`47e};Vme|`Otn|zcuTh4TQZ6IKVT7?o{08_qzzuC#0N+` zUL{|(2B|=83J;W>uqDA61!wZ8=lN%B^2FGwkZO!2?1c;bDLELF1bQ^Y?Y+7uH}!W` z^`^=K4S@v^Hf0N&e`kde(pQ;BIt`1ze5~`Nn*fETHo^-|6KuqPj||YZ}sKX zV?ZxRbyMRcdpZnDH1-C5U5;4JguMyzlQm)=l~l=@z2)laaTx@kKq5APotoUE)xH#J z6)(ramD2fUHPdL793*l5S06`4Z3{&?tnR3xfYKS3B*A9}jW9$!H?R6_%7X{4+i!*D z*)40tp!3LCaUi_0jXN?z7Y6AEkZ^eIVyo1w;KO5iZg~7 zHCM5Jk&G}NQwK`~bXb=f#j!xIJJ#ETt7@1qhw9lR(hEuxbrv?Ct!{87z|%xN)YC*i zx*N?__cB*&7kQ_BKkH|g0C{L*XHjv2;aHF<^+m0ch@q*5qw}L{NLOF~Wij{R7GRxv zl5Ne^rT$D06;D(gWfiTsBRtZy(NY}48_YzA+&O?{^mT^%=g%f;Ze*H{?}d8=k;bAO*Q1?nvfP#$3|aI1lz{jcLWDIa9v7R}*UUhVLB> z?TDq)NCcJE9S%g0rVmhrf>=Nw6kt8m!lpu=;6aU-%{(-cj)pA`DiK5kE7&tX-cAxk zV7ZG}Y!Ot|OEx!qA%%(cHP{?eqT&8(26rmJ5#`!FG&0ynY|*(Kz?poEylYbT zipX*&ApQikP2)eD@Cw5>GKY=XH&1uQkIwKs&xAMXwn91ntk9#gnYz6e93PIWrmt>FDJ!k43qNZXPf6WzmzXnJHc=iBBr{8^QV3P3jBjzp1TS;KxA;CN~^( z+=W87)Xjkhvi+QF4Lx^aaWOqm(0Y9CO0GFZR8z&yMefP`|0m~2!!3xZ8Lm2Rvv@2r^&{YhR@ zw^UuX9c)b@B%u83iCNC~IC#%5yDEAF)=sG2Ixi3%m!~JwM$*P5x2h-9J*IpQSa~@J zrrr`+ovQAga*z#m7tsT{r|u?Zhxkhp{;cu*=@#(3`WZu}iQhp)>uS`C#CQB#V0r*V zTe2;aKaHbKz)(xpB<;4XJks+e6S0l-xv_|GDdg@Di2SHte&&#+NZ(2^BxzTs#s&{h zT+P^yaLR3Ngh&SYr_pGSlo1CA2wot^gmLX*Kry~2|D>4C=?)BOyuKoq!#CwNE>=xz z@B8_S`HEpn&6xHL%`uv=rD%h>RB_zhRU&TJz}mn5F1e&^ASo;(3ppRY={cnp``a?A zC0wiV5$%pZ!_*FuGrqYzT=2e770vS1j+=c~|zjkE7i4Y4E(NTKXd-je8>=6q<+#B7yc*NLp6Yi7`s>jG~xBpI-ljN3WLT@-~ z1>TEAk)dHU%i@jw-oY^D2AAb|%)}JjA7Bt{nKOF_Hp_!A9$XYm%X^ ztmK?aV&I-7@30n?X3rXfNuWHp0#VN~t=DRNoaeHi)w&{-K@k@5vgoq(MtF*-_fe2= zYChH0%?FP}6|_HapKK0kzEY{&1ar1-#X(o*HA;tY509Qp>zLBfP;v#}!^mV5J)dZ^ z>BgG%+gA^6~) zZIvs|p~pM!mkV)(Wj^@{;btztU>>X7r>wpDwmCLZ-ovAvPh4@D&-`&>!9aQ4ozB$& zp5iU5W6N}(oJL1>m258VY_?OHJtQ4roUQ9xnhBhaxRO?2T*pfCJ;?Y5nAyb%ZmWeQdtfRjFHZ{sZX3=>dcPZA7K6U&rrSMJ3 z23`Lst@rcgM;A*bOBZ7^yX5>5bBMmNiu{;nn9^8K@J#x?!{n@TH!x&BoMx1Y zpdS!C^i-FX$r+VWfUDF)D_ay~adG-ZLIz0`K#)}p3kzvR0rp=Om7M8tl78YAV0KgX{bGW4+cEG<+t|p2oXOxm#xNQfN z8f%1y6(O6G{7C}RnVfKJuiXZaj0W?HdU$68{-jOybhcswAmTI)jig>@#_t4FFbU=& z)3D3#bDeYZ26=;Z?rb?le{I}drsj^85p*AB*D=t(sbAMU^rLueRZ8e8j2qQV1~Fi> z8hYmusOb@gaqj3$`75=b|ETY1Q+Fq*KH$RLu8u@?^hVwkzBUu&NT}LcfTObO{CffG zsFXYPCekhefLbLr_#$o*i+-Y*PU)i`#x}$R}_=G*KKA8Od zg?&d1E5yBkIi!?6gDJR}d@@sZwG!db9)PIXWr=&{#YBo-o^KfC-w7L=Y$2_q5tA_s zd_)K$q}9eV8#$HB4v)xO`cRrV5M0lbBS^BQ?N_Uyj}uJ$8D))4`RzrAKn8@Bl20*K zK?_9(EL!7Tu@<%jia$Ut+x-QJbj1FEus=kWHhxabUvLKbdZYo9sf_2ZyUzTtQ`H9634fzfh{>IZs*n7#nJFjd~cRk}k{P;z%|sOnYp)rqs0 zMntK7EEh?ZW;Dj{ezME8Ko#w`;YZB7WQfu8Cl3?Ixic3l%&`v9SfHWm2pdd-N*w#6 z>pThQ1uF0rDpJ1vzbcK8Z)NAyf7p9L{2y_q0+dc+(u%0J1ZfqPj;s8HrXflA*Q%+? zSWY;#r_OEyUMB4@+!+QYb20UJ1&W~+YkpIj`Znt-)9V}-KKM^_-T2*HO#8n*e~|@< z*PKcjON29GAwVEB^Quix92bUpcgU|UHxv~9a~In6`L>OeU`GfbThFhw;fLI}TJzeF z0G!n|WK%ep~kHJws&s(en>DFZ0)ld zbX&L4=&DqT55oSDXVOUIOCNtJ?&o_+z|RdgGV~cu#bIU7P1)FXPox?Pt^Wzf#Uyju zHJ-wt;Q{pYCwybEi&h!8>!GxjB3=MYmJsd7{?h#Zb#sZQCgbR3-)Ak*c5Jng=kai# z@B_>mOjhgPQ7~?18moe?$->ieFbaQeT=5~Jd?z*=lLj*#XEpObnQ3^>$2tY5G-}a@ zEmSX?WSoC1&Qmzkw_{vO&V@N_n)R`16?m2h8z&f4!ZL=IT1Aj1)01Uq2tWZO5y$=s zaORP;**KR8NS$#Cee%5<5+F>(+o;+NQrr(r-VaWFBjbZZN76SSb_b1o zc^0aIX`Kg^LWGJ>O)L_3w-hi3`3e%|1sEYkdcfy++pC_P2+`cQV&+tAkLXej;;z$0P<*&mKBafg$S*@#Iivr!)FZxfykAAa& zl+J;luT&!5ym{m^r_*pS9j1jMnop!C&aB@CGMetbC}E6!cJ5#tE)p{Eerq_dc}p;( zrX=B=qAHr%w2o-7rgx<`E+s|9@rhVcgE~DvjDj#@ST0A8q{kD=UCuJ&zxFA}DVC+G za|Tc}KzT+i3WcdDzc_ZvU9+aGyS#D$I1Z}`a7V_(Oe4LSTyu*)ut(@ewfH*g6qn0b z5B!c7#hijdWXoSr@(n%%p}4>se!uezwv4nqN+dY#Aawu%=d-Rn+zkJ-QcHv4x~>H$ z;nl83-22HjF)2QMpNEM1ozq$th2#KRj5s^@lA)tHO0f36Asv{XHuEFwPv8h3aVTxQ z%oEW6IvV#QJ0B;vgw^Hp1Px?Mz2A(2dQ^;}4MsY<8eV>fzO;Af@2_ABvNCN&Vi@_$ zRA;E+5L+M~+U^kL3Cv6VGRI-YP4;A4S&FiV_IwHwRVdRsZgQhV)RgM4Ma^G}ULm!> z8q`CgL(VPvlGhnd4Y_Q(w#EU{=fE(mCcuyXqOz6x9k}xk63wR%n2?k=jbfx8KC{_QVW? z2ys94)HvxzFg3~`E+&TzC@%OAsX|h=**G(r1*OP#MUZ>t$ZBnnJ56m_n+*g-@o>wMN)L+r|C7%OU{k&i7w!T&(lEg>(Lm5?YI)Z zMu*56HN&c15ADmoxo6=V1AoJDxTx;8r_dWba= z34d+4zF0+J$*d`EgH=4aGD~iWMN?r-nPLgUypU3y7jqF-rKVVCMolJ?vXnQCHq3E? zygp@tR;A8@wwqP-$|X$GqUu>re>O?GO0#leqeF|PxrbFUnRX?&+9UTQ^-bmx!a%#? zHr;DWVKXE_Vk>kZU zv>7s5$dTD>2U*zg;YNegvp*xjy`Rq?-EF}S83Bmx;bgi)&qtF#*)1e44g-Oe6BOHb zLCMn`&=S1x^%&^OkftmS_H!DNy0tXtDm$oL#m`o9$?ic5tK&QaR`dqD8&VydP=hmO z4eNH1Vl)1SSv86{1;1>GZ7eRkgcGt^oM^b@+S81dqf)DFG?wjas_XRIoXwxA)TbD$ z&;YM#{~CaV6{j&!q8Q4}E87~4tjOhR`yD|jD7xz-`qG4CixswD1SJ!dNNr(YceB(S zdTBg-bN&brgS8l(!5vd%3#(D9Rs}p}8tkD#7%)3&P(x)5m)j6WJgmsD;%%#t?U^$$ zt}rR)lG=wjUkB3_m9)G?t6Pgk^z+!P)&Q}&ZX<4NL*j8pdJ{Kbnpl=Rg^*{}#rC$9 zgeHxM@YlVRDsc-hGD6kMZ~@(KO!AY7e3CkQJJ^eBC4qsB&hMFE~sc=K_u%p7dodffBw1U*#b6=_ylpuw)MUa&2g24IPnQkKD+p8Kjt| zBrA0e{WbCdZ9sUUwkn@$zfRSJdC;+_fgm}R!nrJph!|;r$;y6jNTv>VK%(mFIc71& zbYEKGXaibyqWmY@Tk{fC;#Flu0igd4Olz3+NBQp<*MZDTvWGBG8rigCLOH%o>>M6OIYwohsAYg2z8B&M~f7N=iLOPie+-I#!D&YrLJ#*|r zk`%QWr}mFM^d&^%W6EKt!Jense)RQoMqrAg_=q!e_ky9mt-vXrEWn`?scHMlBa@%fis_I33 zTO#Cq>!AB*P3)GH3GO0kE#&p6ALzGH1785t(r5xFj0@C83E@@HBtSSGZ|q#57SXzC zBcVYI{w#qZOiY|a25^Fdny!G``ENdD%DlS3Zk}KXPO%lG*^rJ-*YoTz0!5gcbUBIU zcxsp)g(jX$tR0mbI%5n51@)hFEWCS&4h~-C>z+e9XP2#9L=w6n0&{JJOi_tKFjBOmkydTxF?{=r~Z0SZ zQ!+?)lb|XW*a39dgeKjifBjqg6C6^fO>>mhlO5^a!?k@%Fm%OcR)0o}*qm6=$;a85F~$*LPd>M4+h=KK^p< zUTLr~iZCJ`#!sTSSP?A25d9$@jEe9}IiHO>I(cU!JV|?&>({{a8~_Oyc02#bw!fyZ z@HrqJOcWp<_mvL~UYdVG%AR6M@$eurF>ywq!qkU^T{D$%{9=rQK{Mr0e$Ev<4Z5_S zNnwMk`o5QFbqF(j*?kTXXP`Tk>0tE2420%Wbv=sgM}= zFD&odG<``_Nk$!;UUlNa@pUE;@K9l8cg(6Zp^76 zHSY4thE?HEz;V#!D}=e137fguh3sSu$@cn(U(I~bzJ+UcXJ=Q1O00`zY_m-#grEj4 zEGB@jzU304JM9hH$ewewKoi}a*G)7>aprL9L{@#&E63^!f5;GKKdIcz3u zIX?;8Hm+myU<%}TY{&)aehJtE{bUL5REqCLEv$}$XOuvB|LmWM={@UM30}Tc@D;(g zGwu3b=?d;_K`#|5(k3D+azz2#*`b*#(L%u7Pt3A#1qc<-_e7jCTL6jjvyRPZR?)zb zWgFrXi*Z})op{VWcX)K(M?p| z^}a9&&u8|iSNZT&G=-;Z1>0&GKleLMJk=huD4Vlz{zHe^OpLbVZE?7JHGRxRVhX@R zX#DjtFQ~S{-S678C8X4#M?IY@6Nj@YeQh)P53f_5{5@XcsQhQG$hZ}!=|IIsPG@-~ z_{~ws>hNg`<7R&15+VS9kG-XsFaWQ-qAIYaR{NtS)$_Kp8Ny;9bOV?yFjO|C|BAb1>)p63 z4?AKjs4JeWs^@~NgVY^gp5av^K1B~{YF7jfwz3uM!~O04tZ#R7eB-b!IWW%tVX4NF zZl~8XZhad1Tj?)(6C#PG6UgWf`0A^X+pq%_o&XegitvOnypX9A-jKwgoqIsk`7vDH zPz9}L=G;#3Lf5f!K3`t}l&J?TXKzH~Uzk?{5_k9H9xWw9crd@!v&1VY zsOuRn#7S^4j73)ETazCqI7bwNo$t{cZ&ry=x*Xgs76A|6USJp|n$Y_yB zDC2KGY3x!h=P8)>V7&ntYvVVK`hxw4Z_sN~Bp#BR6^2R37pGT z1Dj`(PM$x)t^Bc$%_kZgDbs?_&wIue+uUzpy}>uET;=1A)F*)A>Ata~GY4hAc!A?U z?{U63R0JMe536-g^k(*$`+N?+OJ(#XPk0Vrn^Rty$T*_`6p2GBZiWkJ{>w7+4g|H2 z4M328#NL_h?{$DR4^iA=7M|n{ahQctX<$tp*M$UZN+xz_oI{cx8*`dJ7 zuF=LPSVu%73wwaH{>HwHrblU4zy99llp3ScT+Mw7rR)7PJ^rA!wpR1f3=q)%h-?9K zK52(MxZVT~sZMJ~do{4JL-m{KI{J9x5!DKd$(}V4$Q5i);pa(WYKq|3lh&(wpC>*+ zMJlvE1NX)k5PT%eqpH=J7er0}#EOfJJqW;C+V(XcP_4kkIdOF!3{~9L+ z48Ix^+H}>9X`82&#cyS?k1$qbwT4ZbD>dvelVc$YL!v08DPS3-|GFX_@L!9d*r0D=CD`8m24nd4 zMFjft2!0|nj%z%!`PTgn`g{CLS1g*#*(w8|sFV~Bqc{^=k(H{#0Ah@*tQgwCd0N@ON!OYy9LF`#s=)zI0>F&P85;TXwk#VAWS+GnLle5w zSz<>g3hqrf#qGfiyY=*_G1~|k*h-g(AA+NbC~N@AVhf6A6qXmVY2Temx2|X$S0UFw z%*D3^qpS5e`ZtH#e-p_hv3bYtz!vUA56&MBhN4*snI=g8YNZ{TYX{~dPZ=Z_gk$3Z?0ZR{D-aliB#|SEnR`T;N3$!}02ZQ(F`K#y94FLke@r>i04JrfBacpWL!tC&p$j#%e~c zG0Oa(wM# zM(Mn!CQ&`w@usAmfZg29h)&o{r_NeX64w5N5WxG6q(-s6n3+LYQoV!fQdogT)Mf~f zrQ*(MSoLcIu2Zpl1bcHm-1-=no;nuG(Rr?&=9Dia+wfu8KmGNY@a~FBD`eM%#b5IC zn=aI`v<7i^08qgeb@EmZ1l73Fe^)VHH>vwnl#LfZYM}d!X*vZ=X-Kmm)|p~g8rR~7 zTHpjqRDXxKte4N;M7->5uZ?~X`;`Oeoq;87kGDaWGMa(5g9dgC3{EpOF1o}w3Ms0+ z270RrL{cUBU0=kwNClDNSwY!Lm!3n$dY&svjk#S0d>tPZn?&G%Bdtl_HV)BD3T&C$JTZ)yChEr+){ zP!q~(%s;6J22$ep1;aq;vT%}A@4H_e%j*18G#k|8R4HfuOLp~*H8ydsM!zd^J6-{I z0L19#cSH6Ztna?VS=NwT9B)9MqJAc(Hd_EwUk?-sA$*+!uqnSkia#g=*o}g> z+r%Me7rkks(=8I_1ku94GwiBA%18pKMzhP#Af0}Seaw|!n{!*P9TQbotzCQLm5EQN z>{zN@{lSM;n`U!Q*p-J1;p{VH`75=x^d=n#jJ1K1%%tgPj|GD0Xz zq9fV3Ma?HtM@!DivcDoBi|RXcCu&(8=pz_F%Qq#Kd@NT0|MtB&yqr?e&x3@7k^qX=q=oz=wvkChK5$_^jhq9 zhI+$s(bJ#2(25kdPfP>T<$A@3xOU9Xu;*O>W zPlGz<+y;?kBjzc;6Cx`rv_6DV)$7dgS>VSX3u8DBYT4@c~$tokVRZKT>AAJcn zM`3)eO!3jw64$ia2bI*ky%;JvZAew%gfzr@2z=cx-FW{@F2|Z2yJ)(40FvA_tyb$4 zHp-iN;@m7h0Wd7=&Re6T*H*wT&g*@8FgUyIHK5&0SUQ1)UCLemXi3}48~TLSgCCyk zrp@aYZmn?H^Jl<7jH)47mR8%{zw5cawx$r(oP>dTGqsxPPP=R8-^vbHS!I{bImH+d8&wJ9%Q;wmq?JKe27wwv&l7u{E(hv31^a>U`O|>aMzfL3gd{Uh8TtBa3!a zM{Iu}AI>-WSaizNSJ-FtewydP57^1>j^mNBnaaxoQn&p9y9&-_w4i7^xOT?7NKl?lKxm79T1T;#zGve! z^z&y}PFN96@n!`suxGzHHb%{=V`PLBTAb6YsDu-M5z|b*X1U-HtKvIeCp^%4PTA_v zr^@B{_qoGaW6!xov5Prol9ez6kdqH&(Vd~>o$?gruojX(F}osv#OuA9XCm{BA{HQ6 z7I#HXLktMs2!{a#?(wMAlBNdNxg}5ft0q4}Erg)PFo+~m7-_8kEk4%&n`n!qprR3_ zRKcyO67pN^HTAedB<#V{RM6J$?2A+0nwfZkx z)#H~>#TqYNMDy~b^!AI9>aavY_!YH!u%px+~ zAR_r);-C5#UfvaZNPmjHSuC39+iWbb>#uq)ntooMYNm#v%L5gx`qHNM^>O%V(&=$_ z)SkW9)C`tI#lQ5oYR4|5rnABn0GHiGa>kIEA)V)lr~lGU5$|u7S!kwV34&t z#Znst?`+H+{F>XL5Ihe`v2bcY2LZjt7?Bt^Q*1(5Xcp&jtGCX0X8@7GN*e>1pKz{? zTsY$-TL0JWaic5zP>F zBpD0yg8$LFD8iM^) zk-SPvJ|)^m$UbXDe<1>130Xcxq=9HeXVixa5li>o3bOiCmS8->t{1==s+|s)1#Fxf z`>r33c=P^?sE%sIN{nLrVKP2=8#A#L4aVF0&5hX+277!PfIi#w^-B=A(-v7xyZMmjc^*yX$#oLqK zZ9ANck>T6&l`fxVTgmj2FMyTGi}%N@9p_{)5@W~|eKY+}O(1Eb@~8MeO%U*3OJV&~O!Y|BfsbcWre3Qam04<^Ox8b7rmU*W?BC?5tQ&Maqv&(zE=o#*zFyM3A~aLQx(BIxtIGzX$s zVzx&kS;C&nIUnJf=0g?za@(IQ$b3sWi-$AZ35<7zDuzQDl|s$cdI)pS9|?_@L&YG= zTz1|NMy|(^-ZMSEMkmyA*Ec=8U#qiWonuyZ>vO5Uib@8!;^$YYmuBR+aS?1{mN|pv zw-8JT%`sus&h{q!ics^;33&wOgzyRooPenPBHseN0(uMGO0M=K4B# zfGQ7bWrup@w+0D8zuXDVG3`|9WQUIU2=lfs0}uW&$pO=+x%3;BTP?egh9}g!y|nxQ zF7c19A0dClYKuSr+0{^h;p=f9Z}r~jC}s(xg1yzB|3z2;`K_IX0kqq}KEYNiMmwrL zR11gCd%Misw-RpfU}^|g2}g%6#Etdt0G?#sN0(*BU)z~$KoK{Kq`9iHM72 zx#?+K`4Y8`;N;NJ+f!qAkK#UXrFMqzBWj;wJTv=9yxWXYj<=2W?S}YbPJurHi zQ($FF9S}jGm#Ch5G_{9=G&4K1rES6e)EtmgOi_(}8r`}~fLVtU&2@>eeNlYH>3oCK z-!_xrX%uzAB(J7fGqJ$WVfFlaX$_^-S(u6ywL|Ek8l5*sT z8D9aA(LyK~&|Ms@$?%C~OSUB8zJuyoz!y2nEHMk4VjBmJdxc06{ee>417r_Zx8M_f zQv&2&0cujOd<5@MSTY9gXQR_E^F$=~C=15`95Ht{YHmdLk$@3n#NUOMK$};s*lX~Z zj-hg?05PqDKaXM*=@C*FUgq$9FSP4gH_)(EMoJ6Vkgs{7exk&Q6_1EM;VrM=HLvKN zx7hNZad6+T$rH*0HD{xnW|(A;fL<{)@*L+A~DI2+a&j9;VV7>2~< zOwYgnm%NW?RDa+8Z;c&Dn}UQ!4V=-1_4~gI?EYyNM=CB-ToUF;W;(fN7&0R;6*M#$ zvq5<4o!#$u zL;H83)18fEmc^I%kG9Y0u2a8LzSGT&l-IvE1-?m<>GyN@RiOc=MG0pwK%(g}7UrlR z%-M&;96}o7L1r8apQ&v zS?_M`X_R4kkwW!jor7h&G=I3cyLo=WiDB0_Gi1V3Z<9=>`A-w>Q89bJ>Y)nS-T|=~ z@1h8-J2K?H;h0g6ESyOVVEyg9o<40j9gBKQkt9MJkx!1&%PpEAT{s(tVflR)k?!o2 z0mU~aI_52$;dv3)8$;S9zy4g!NYM&dv+h1r*xa)+IiI?ql;2upk;*aEok5LD%PUqS zz8;1l^|}F5xF(Ao%CIC$YgCZ|0wJ6yU9ZfstHAOwKs1ms4V(xMc;b-etG-ivj|D2A zWYxMR_SLI#Y)|w~S9~nxto669sc=HX zbX$_ZzOwkuE=C*zP%=)t7J$QsNW$t3`nShXVT*uu$f8k+iyTDp@_c=Lp{vaFBc^0&k4p3rk*Y7Zi_uzwrjSgca zMtjp&+ZrhxKyKW{K)&dq@Gfe!?G-`-PBLfo;s&_z5DRcM(+!N~fXTq|3O~PQbs=qA-pTg2l^u+d z%ds=eY1sNyehE&1F?Kp*1nt?h_p`OIU`aFI@{{AP0W(he39BQ}N&Fxr(_Nn9C@|Fv zF2CjVJpZj*KW06pkPfYefvVkXhPmEzhB0ZpvW78P+6b`(DXmx4XD$i@yG6uVoa7U_hH3k2Py`({xw)s6nAe(f(@W-J| zz@YAV6gVhtFUM>qy-n`}{EY%a%Z!g{Uc4KbHQ4Cysq(A?;rg&6Xew@Z;N+ZaVY|*= zY%CB8ewT@Az-G0c2It&IF33z$Exgk%iGnm9(StB(7KF?4q@06F#2&%w!1|s-vJ<$R z#XzNy)JYP=0BaD~u#sigQN$gNdTInmz#5sK4BSByfA_#G&)Zj<2A?Bk3$T_QnC;|2 z<0|qNBOdcGWX_efUbjcIbf9DLA2^E&r#fq>Gu)@g=vUoWqV-D~(xUfMfaCeY?ig%5 zNlo{2#2{?+Ykm2};*J1&Ep^Bz&WB;0YXN=I6)&JUITYUOUDcL5p;6b?izK++B7%r5 z9mr&h^fGbKR>>e`KebYXfs9w~PV?6xQw%lJOA*R&83!gvx2_G^Zzl1NjQ*&uWXlIJ zA5d%t%)`R6RVN`l7|hlJO0zti;vgD9yyKBh-oiXL(LgU}D{!LToK9roJSM_z=}gA@ zV0mkG5=+m9kztd>9U`MRFOYqw_R@@-88|~TY&n;wx0Y%6<;}H~Vhw9l)<<3|O$g znOS~HbBeb++hP5w^R9fzH*%%;O@OyRJ2HQ!`5r6TvCxLMt;lTth4BYout)}a_|rR1 zP|nlJjcdDbp~VeGki#sSoP(U~1 zzvfGSEi^1h$ayZla(pu`eFFiu-MqSdt8cz0qRmg++c}@ChaW9!{X)T1I}H&3h$C+b&J+B z&WGhay#y)vpbmts^9+1um2a^f=rUg9gc(vaIvdu9{ z=g~Ari+YZ*_9#%du+x0Tj|uG&ivk6<0W0(z->5&_@J!xrKJh+-N7(ay9KI1^9DKq1 z-`Q>5RXJWR>^gJg=ceSH1FhP&;-(b&yx3;%21tElpT5B-^B5lRW1stx=Lw@yl4K-H zH_&#(_w~Tx6OXfPTcCLo9$$?1c^Nx?=R`f{P#LiJu7|AN{H=1s9vgkea6`f*yNy6m zELFO8tlEHRx_O|Rftnf+yTTazHib2IaSS}hRg2p_EFj}MmiDQ$RqH#OP&*!>JX=+E zhHHTXEmdmJGX}fFret#wSWMoxwfs%78tQ;lJ+%#EPSxrJ1@y5{w3>3s`&VRTmheQ7 zm(`N@=UL#bJ3J63M84cI!+dq8*0Pa~cm)*vOH>96OZZ8rI+@#sxvX%J;j#2UyoI-P zoHw?w+>h2y0-i8E=E{R&#ky4YXy`dpzp?LN@i=(bZ>Ps)txu1NjX9j_ZqK;J7FkwVRy|k|*99~?Y z`*dy80oA`CJ_$tFQGtxLJfj|?%k{~!rK(wP%(jJ&e^AP#2mSmhEOc8GXcC^~u~)IG z&bB&9qn$v@0V@7Z+WqyCihnp!(NDz!v+(tZ6+efxni(EuvIZgq!%Q;IG-q zqF8&i9!)wS_%M!tY{yK|t}-+MVeB2X)^xwo4U+^n6ZT(3n^9s0^N~ZpVA-p-|=@^inh<~GA#G0Fb6cqg`G}K)*o{T5?_kIK6JI}m$v_ol&8oO4P_zX{TbEI^ zP4gy_X(a!@XOe=(Mp}U0!7ra+gbWnl2qGN(SI*+{5}&-NnMCpgbIjJJMM#>k=g30^ zDbJL&s-oi`3YUeZ9y-BZu65hbFPz;5@(6>;XEhacr$vW+pjdI#rGBriL|0cF)|$5S?ZhrZRY7Vy{kdqRI7&X0dtGtm6}Z)oRm-4;l8Ds`lB z1{;=7P~qZ2_n6wIDqX_QLr64UbcGnv7W5MkBQOQpPgUnUuZmy*Y1;{C(bD+H71WwI zFxkY4N6=#*ys|B0K*aJKZ-tf_Feu|x0wGE^{ za6HB=IjXDV7hj^UMqY@8D*!&A%+%g?A)#u;s#rUkuh7i!inq{PbR#Dr|8ZT+Wh(ZI z1r+upwLB#jrdiBGjm$~v%G;|eT(?4SqN&z(RF;+MW+&TN%T|}sR;8Dh>e|RrS`1xo z;obvgl5Z|wz0;94M2z-Y2WT6-(${?#QL}TPndp;hQjRZh6!1&D`+%7IvJc29LIBMq zvwi(+IZ(P1qKSTq#x08<=kru=S9oc!%gVY%A{T9{D%p8jSYCIzFy$TV^U4-RLFD+w zn77r`QwzNhX2Pbr7lOF`qlaW1HJk_R3Xg`iqZN?BZle86?}o%OyRW zEc|gt<9{tSk0Td&`c-N?)$%jzYaJhoOAjaF;6Z6r1}Rm!15{WMTw!4o5~)Fo-HoU_ z-&ujRx$TNix^SgDySgxKt>YCrB`EyID}h2#B6*Zab@La310Ghd_ma8AO#8-ulwSnj zZ<5BIUzZE;5*FP#&vkvaG!H~2tU$Jkd%gFw`T!S{2mp9?Vh1R?kv;~X`YAwb63>)? znkAD~i^l250{N2CJV<@SZeNTq!pqthV6F>e_QO<+Mykoxd5^JzHJaZeQZ zhJkUxQe7WRdWlz!MRJxF0W`KL@`p~)x5J(z5M;XocV_|rgnnd1%sW+|yq!Q`G&7GP zY07mPEwX@!LGr!_kNsDN#hMPL7#l zlc=pE5aWH28%^Dr5#obbnK@SMPeMr&YC`p^e?y)lV?@3LQVmf_yWw)b$Jl&Of#Rp# z&|KH+IbPYoU^~mj`IAFEK^Z{Gyzpb8*3I%bzXzl%M=>mC%Q2%)jr6JJ(KPB8q85*d zB`H_bk5V~4&VPE&gUAO>5~Zr82#kI9vNGHonE(8&8C(Hj-eU@GWQ@M~+4I^wF?8-BT6Km@x@%lir9`u3T}u<#oKmr!E| z2--yCX0m;Giv$T$>#E8290L1S=M=3CD`(J9s?1X>SX6lZ4GocaWFnHAC)t1T^hkf* zUD3KeM&diP@80N9p%T&fLe$oqvOhhZt`JxBO+^LSf?Q@z_`9Vr$Q6~<0L2-m>O(g4 zOan%-sNta~Xk*}&{@r#)usawmHs1u<1GjQ|b56{BDO&snX)z?_ zAankXRi*W~FHQC%{R2T17EVv=NN_~B7>6qS8-oRfDB^`%jRb@OLn=Vxce}tFY;7n@ zj#*voq%N#N>y$Y|*HtC2U!S=)^IxgQ0-7$v2yiqNXRM zwteC_-%jMY93pATf5JRZt)5Ay&cMar+UEM%P_tH6YH%!8xM83G_bjXj(q~&xt5EB% z3%t+9ys%^4AWWnRiJ*K6xjY*LNS|#O;pS)*K=AB^uJVW_JHF`#iYDK!(>=WUhh6%c zX>sTwaqCCJrW6nIY`0WWbIIb}bAzF+1oH!VTEEkh=Zo6npGn$x%=adz9iX3#tW4ZG zd<(6Uxn#z9!I5&G|DBlUn~4sC6q09u=rux4?hdLGj!_7Cw~W?;w)!zdM>lGL9?iJ}t$XPovsz-)cS-!LHv0ZC zb4AsYLrHn^FyZ^K^RfN==H_K5|Kmms8C*LII4c6rK%~mwn+cs0!Hx`!kJU7zAV@+T zY78x5H8b;aj{WU`xKGLdJJr*0Ydv@5KHQ6gH)}c2!V)JwlsWfdsGezcK zvNM+<{?KLS;}dCbka?fVSkA4*j<+1;zd^mMTl-!=UrG}%Dar#cYGiWKt*OnI2`}s& zKuJNJ^nn0>uh!6qs230jLkzPYLh2_ii7q$|O>AsUP2s0Lrn|+I5<#4D>kLax=_gwF z9%;kCQJZOVwWh{(5l+S2;i@c9Ea^@^d5H*?CXc?hq}byCKRwrA*C%v%mfkhaNtGo( z6ZP->A4&OCCWA#*#FO}#W|pFnPK7yjF|1x3zOLK4rW)-`{Id_xRgaYRE<$eQ5uvhX zwf1^~0@8-xJluw=SU}u}Dw6aJ;q1JO9ug~KY0 zc4j+Rx)`6g89&yl&N%L(+7`jSN#4N90mygg2v-%B)UllG#o_hk%4qb{}DFugg+wjSK#BF}Y6uqK(T} z?kzHTS{^k4!@fD4XcX#W(^8wah zxhMD99Ne&1gVtZZcgbC`hyPk0Duv+(pFsD@Nk!o&HRyRK5G1T7+eQevJC6LPk{?9c zQ-J=nD3qA?mBsZ7LMZK)4N_>F2_tu$3G)*!f%X;15m2(%QTyX5jbibaL(DZZ?^X)6 z6IQe1C)xidS(*m&S%Nxg6*Wvr#c_5a;M1(O#!UP zK|w*!f?nnepYPN2Q*1CL6QwdI+R$^%?Xi@THq}&u@#=_#DZffv#+TLtqCOXu9c<0O zBsjTGdF-y+Z@mK*MKeXymw+sY=m5iC_W;0f&xoJ>Z_(Nj$u*A&fs%=i& zXib;4XQuQ`Jk*=)+;=g|>19uWnY|Fm@!=U93(mB|GesI4Wr=-T+cXbcT)0}e zk9@N7!pP7X;)b3=9w&;zB8_zwDYIgysR+6MlJV2JZgTIABOgT$H7|24>D8+#;3xzh zyKY%iqA_a64CM6~S%7)I77x*&ho@z-+9T$)J3p7ZAAvXTlleQ)85O-Aovu)#(nBFp zlZv+~J@s!EXPC?AV2Qe2x8xWM@qgW+EK=kDvM;^m-$jX%#8X}}_^WbZAFz~n4^?Xl zj%R5)@O^*Xqwo3nF0=1jxhKO#Xm|5ZH%Ot*~o~Quw z_cI`0zS0)qV;eDMqE&yp@f(f!aI}g#JA3@l8p?CR&@Kv6EZIB?Qasr@Gt@Z{w77Nv z-U{;yNYdDIL049ee>V>Tr3Z~994}6y+LfVe( zL~*qRBcjeUeu*d3^?P%t9mHjZr3zcH#b1=(bHZuj@nb&CSkplmQTCO5-ncOKUr7>~ zXO}(#MI0}p_XUBw9Z{>_&I}hoUH;%ATm@}@Ytb5^tGOt&!%kKyT~|z0b_-_?RCARZ zLcxg9h%d{=k%-3K6b}W*odahEdv~P*`guGU=-EBpAXK}9hD!(mCb7CfG)h!eG^FI5 zd=4Io{XOpVr+hC9GHRYg2{EiG9pbO0{pc-`u!{CO2&6VBS#c?uQcF@Ge1pz8z`x7f zHE9T}UBeEQwl^S|gy7HSeu)=DMQEd|gKT=|>Z0d0x2Brl>e0Q*+NDE2Z%mv2r~4?* zs)BH22pO&FW692q$)y8BkuyA5=q{G1BlUhq1an)0@}`oN?EEaV#~%0orHAOc%vR{q z*;tAA6OP9cdMCD$ae+24Qm~2WV^os>Wz#8!J5r1cHjce&Nb+|lF^e;j^Bs&p-JGc~ zKav4|l*k}_e7EyWNLxyMK5|AW7)i^q2!*m2O?(+3 zqby+A^sT-jtH~dn3!P$OMc{Pqj?n#pg7Crsn{p4bJZ}i!``h8~b}(@ZpyEJ+ZW^DyE{7Z#gl4O)5m zjbk$DMFbl+chBv*PFd^V$J6J}hZ+3qBvi5k!tI_S>L$TzcJ^*G+St!ob6TYl)tfN? z;`rk9+C7v-`K&b^3?Dx02XH;WA*noz_@;rr@7b?!{e&;*zzHX(n!PtW~ul z&|=dUNrRvwc>mRXpQk5&-8k|D{su?2jk5!p^G#(vbx?!4tIQ>Il)tb9 znC3VL0&yIpl}_;L7*w91$b^Glb%SBKJYJjTcuN?=rjSt#n#loPeNN^GB|4QV6#|9A z))*lnJ%TH?o7n-B!{luw>GsRBh3~I*pndrHkLfbiN>UjYod}a51nzmD1+I0(7{u`r zlA9>4UXUc)z-!bi7JWd-w@wwKTI>{`9hR1r15}NZ1`EQ*5she490`UZDi{~)hLQAo zF@x+OMp^;QY=JO+x+2Qg;;>mIgf=Xmo^UY0Bv}V83(+id3?Mv1kz18z$0;fV^tm_A z!e*cJtvb-M`dwsOP$-dbF6uU5Yd&C02k~DDA0g?;H9dbopc?PCHW8bAv+1xXzXd!O z=bs!>6tU4sZ00nAP~*Y@frV6L2{yXW)wS2JPr{^!5n9UpOZ(@-%sgtOXPyQVQ0umj z#|bhR`~OAdK?1RqGv8gu00994KtM=RP(+H`^)6R6>^1s-x*RQ7 zWr)DO1*QM_-!NK!6}Zmzcz=fY-cT3weAX9u+-qCImEls)cv({&mB31~sTfkfRfSU9 z@{dXYKVzUjk4~#tJ(Jl*gbJoBq+P2EDx8xF>QB!Xr{_D@l}x+DS2Jw%PYzv#wr4Q$ z<{p>C>mQc{_~j%mrj`i2vup17g&@6~3r-)vgjQ}vy$vX4OsqwR&q%c1yrRY`CLUFV z{F5^#_Qw760bedcYqxO3Ym?KmN#AZdos&wy!>-x!nld4=Lmwf)5eFXEt2N8Iu~QxU zWhsx^S#3sLoZt=#IX=fu>74~JaBEzFwQ*Ew%DaZW;C2b#FMZ6?)-Rqv|FVK@{dUR5 zVYPEq$u{iW#^I@nmdSoGl-=QFN%G%3_toixR}MR>kbQbmWkLJB8S!{&f*kt2D|G?z z<}kD%#qQWOx+6xG&u@#;zXQfCXpHY`nN;(7PYJ1{<4tW*zw)l)3*&h1^^I(YQps}i zB8H=1{BZ7_mKGn)uj;B>p1prd=_Znix70hLVg6M%uEAvS(nMw|Qrw1jI^F()!-C3& zOp?`_DhrI>MoZJNcGqb(x_b=q@-iLhxTW0DzMt#9g0IPfxm;jr$3;gjS=-mVARB6W ztsy^bdmzeWVb4lNyELxF=1qS0?7=q3UL}}s)nKQDQ-|8(A~ke&#g3l#WP`@%Uw22? zB)w&2o_*2U=pf-^*y)C+Da9ck%PAFlPpgQ(dR#wP9%Z2=N0El$$fXrdZs87;i^-C& zXE6y+u3L-}y;k80%=MJv#%fPz%`^BU_3`hd8prA}Lr>|U+Oc7ct3@844p(p8khf!I zrX`B(z)4b&BxATa7wK3*4L_ygb7}WSJpTf~E;UYL?w5|XuB(L1cpyi#hi$6C4#SO` zYEZT>4d2N&MRgWadgfOhb;v4S%whUtMwPiTS75Z!$IWInA)SZHK%ixRWree_0x^?4tck^;}2eX5ll} zQ$3s;24vdFNEq!91S!!HNtcb#`rsV65H_yl+SsCNpV%AB9$hf^FcSg89XBzCduf8r zq7_K2+e^`mYkFJ|=V7htVLEbT;9K?W!9s=@*1EMVC&8$fB4t}SJcmER&6$rwdI6wI zp`@w+t>nlOd_al$CSHl!zWkvr`**OUFZ(yyQs=b=+16^F?cmcLccS|kNnHfpbz}y+ zV#VD(^0}rdw)0xQx65Nxyo*)MydMApuvD4itFO5-(yK$pMmDYQ5qC z>YI+^l$RA5o+1+kGO}l6qs*?<$W6-U5He|J;D}e}!K$EJcbA$rT4U13njeXmUWV04 zE*(&~v=J+wZ#wNB)meIcT;()U9*UkehG0O#b`t2MofG%By7p%!z8goIN;Qw!=U?(Z zXQIu)LM5u$=Q&UtL#ebx@zBKd?u#VPLds9n#p!FWEHr*k{0WtXAA}6?Sr9T{ntB zlb-DYLh__hEgQ+wY$KAZh& zt&aS4yp;Kg{@0JZhqpmXX%=86H-Ppe3S$=9LlRDkaf6p$%&H$n*X1D8<+2f>4syKQ zecCRqs12xWrI8C$2l&dto;YDkFnx%!xah6#`qIaO&!|S16m{T6l1s@JxC~txbpV#| zk}fu78*-_opFd&<)Ghrw*T^F(gm!-i?<-v*^%1X_TP))>kk2?ud zS>ABr25C^WWbW2A_G`(T>sQ0W+8b1yW9omVy?$VpN{_*i_DXgI#L9*`=02#eRg;M=HgS}J9^gh_9dw?cM2yCSonba zrkM9~Z@{}d^CI1%bV}4Oa%$+4biTEe);qYRO3qzE!$ZD~$CWauy#-f%&=%{&U^UX+ z!~hIB60(p$6*T*D_k~Bi{0173X#Ld0fwhJUOPakRaMlQ)3YkVBx# zg5knbl=(sY@Tiu8tx-ohlpN;g$h{F79#p!7C8)Le%inWP^DOB~p4DHV-J z%iRm{p|f<1+6U9e;@N};bY3A^C8fb2H*J%lU4r)6`S8^JoA7txgYiV(VZ=#hE3B;TL6vk(G(qY_W z!POO0YKZ-vI1SC)sYD#G;emLBMVFt4Ej(J~FvIPe{CDkLfm=Y>Pwm66S71Ztj`3Os z@9#@NqkqMB9WAzSs(>z(#CrZ*|UuT27M@1;t zZUYh8EeBojHewBZ)>j|%p+X5BY%J3l!Ume)@n*gy9%`4o$E1H2a8OZo{WZ-OPrsI5 zn;3l+TqmR$*P(Q;JJVe2Df%Se2%sR- zpqj9(xHtFlijQ#C#2pH2HE!G7y`#4H%Xsw=0o=d(?;->v=_AAEo%HI?v2MZNOLFm)M@RZds19xmfL+ z*|#nYtu=Hgcjw7Gy&}%1%S2>>v$8wAJ2R~+M-kNn21-)ocgfmrC-ArQ-Xh%l!S}+Nf=QLbte! zep3kGSahTxx~WCY-IbL{MyGt_qY%(_XX3GeEA)%;x8`3hU0@05AgN7g3Oy?a+V;Hg`*-ss>O+;-AIeMN=up-v9_UVbSd##|#j*F#DP!Td`gd@>xDb?WLvhVQ0Fq+?C?warby;8PufI~? z<-x`!=fDNS#g~QK#b*D~wDcQtN9$2Rye2K@SN^|IM-qJaeDu}~GeHQh)^sx^YSw}V zA^$P=sr-ZbrAzb0sWg?yH1d7Wy7Y0r&gI)2GCJvUs`81g$EIuze3XV*Y#w3&Y`S0VSRR_xr|q6*|QwRQZgI{ z9k@Jpq6J>dJD&D?SWbqg-67GR)r=H~73}CP%VZGiA^$CuoJsX3R?O#lvMJQVc==e} zg8@B@KFY}*)1dk5MQM1<=aMq$eXK5s7R3y`VZ4yjU*=^)`#4Wc#G3axQ-1-lGwk7V)I^lqBYBxsT0Kx2?zkRV8*_ar!tkJt z=|F*IsI*-eOxopCqFj4awt>@kgXY2S9RTy((EO7v<|`_58AtjJm`_I6+hS}M8iGyn z_x{c}*|HIA!gjiYJ7I&`Xc=AMJrz_UQUMCj9}(ZFV$nfn92bZ(o6+ZX!;3inf}!|B zw;Xg|HrIE>_rr^k*9sr|x^slE$-fv|GTpFfHzJBNIzcBecC?-;DJCA5;0Tmo0D zDkKj%y8mPQYnS+kI@VXwb6ni{3zyv0t0eB0oa3$Z$_+zzHe)BYf*-?J`G|k3dd)8> zI|o`Y-!iusuKN?Gv3E`4zo?xD(Dk6R9skkdGOaebO}zw}nI;!jpYJW8BOWZ)3Bj5e zx#CMhIEXnU~ZtFn%w%zMBj{~So6hLKHD34vBImBB6|rr=k_Ov9TDKb zjHv8x?aep|-NHo6bZw~E7&z;lfqdX7)6_9d!3T%O%i+h2Qy8eO#Jzu97y_0DR%Boi zZskbi)tz4_p5?G3RN}xVz)_VC7q~7k757;4Jkcm*1b>l{oR8B5A(n(aqU2MYFPpVB z6h&y5q*B8!@;^PIV@`WkEl>P_59)go7fUVT5s5G*^>im-k*|s-$5wkRp}EQ76+Ugj zIq!eLU!gEOZb?$hz0Nd=-2hv+OEaKb!CToAt`hn51=q`0DETbq)jvAF-4q1sk#2!_$hgUltLx=?;T2fk9Gvi^`h@3j zR&uPc^HEtoq0tCt$W$3NxBs3N*XP!q*QZ75Oa8EYU7qIO+Fg|}YnA-+Zm7E?he&Gn z(AN0GyFR}uX2}`m7h&ZmOt0-I_21pyb+NddB+Stfe7xs*vz#j`{sX^tCE}YRD%^E4 zBDjOl`FAUNnt63d#O!&I>x*cPXld<~b;(78#6_cVXV_SgKgMbR!m}^f z>2Zqo9XrXZ8r%X~!OMUxcEMkb4&r zAnz}M7jly&d4ZP}*|0Wqm5KCVeU^iDA?5RPpo+xYb z6%IN{rz>_6!{12CoCs)<+eX?XBJ8i zR`WZ_Fx(qnx%dyy(NMo?28O; z-Z+y)dMKc{Y(WBe0QS2<<+6vl>x$12LGh3Av;PrYZn-p;M6MM4hQ!pmLfci5##IU6 zs)BR1Xu&DENU7-N0JSwmYN5iL{aO^r^Ip>_oaH0nWGEizG-=y7Cz?v!P{V5jfANQF z4-avR%xP{HbGBg?@5|<0>Rq}g`@701KjGl;*CWuelQ!k)D(`1d(OH4R8inw#Y+>_e zi7c*o;0cv^4iPe|)so#OLYe%rSM2Slj9-JoEFm(^=!Nl%%U^sek|oG`!HP?^E1Y%R z!(|EVWzAaLJB)6RaozREJGc*39Tlm~n943AQZ} zxZ&%U!!a$wR#p0hG)dkF;NeG9AwCww8KmbS#%b09Y%L|}A!8ti-} zaK3ggH3Jg7HK+O&nyt|aYOmF+`N0s&Y~xbzzzLFjnPtxjQ=jm(yg5^D=vb+kTl=j>XHlhNK5n z2XGxTQ^(Nk(5Yn1$99jxX4jp^;DLcclXrG#h1(96y*!pJr@c3V8%vLKyT5*e8bLmb zqJ&d}@gokjki-s!gXDm&7f+qCn^~`8?Lp4)v0p7FqLVNQ2L);`F>Edas{wj!ZeS&4 zuE#B8m(>8`w3r+Svb-mQQB~NHt^DxfwPU!|N8ZgB#iltJ3ce0H%gM>VK4mKuBz_Bw z`qbSnzEXE1a>Ji)l^hx+=IA66VBY|RwJV08LAR64Kqkv&Wei5^?(SV1O^pZTDoz5D zLv?Ec`f|yFK7|7RavcaDE9G$Ql)G9Lhx*&1IwPaHTENXoZV_<#0-#nD_=>dOZFAaF zPo6y6h>h01UT)Rh6VW_|OaJ1JuH~`qiQVBfGvVgQH21epcy)N2(9(ymoY~oca|Kpis{4TTYxkX}3){rPMoy_j)Au0Fk}LiD`tK{%8G41l z!}o9ErvR}jd*hiP#QCVAKQO!%PM&!FmW^cH`A+y2Ea;{A53?yOOMep|!ABg|!UHT_ z%fq>&Z6dvcusl7km06wysty^a|6TcdtUeojF$w}dFcrb-B#B8p z33}B=f#s0%7e1>!8^mRd90+D`6`>IP@2@SiXhW7B0@pbRj%_5l)KC2IOGL#o1Lw%` z7fvSn1I{QN2sz;*lKw^lie-k)(IrSii!6Q;455=K!1zZ@P&yIPJ1(2cUwDi^QHp!O zFmb;D;SZM}wizbTOQ5{F{|KWrE=QUm$s=+IQSXV>>i?`G5s(h;T<=X-5Rh6-5D=RG zUq8?(3Jxg$aaA#nF@F@Ab2boCj5sM!V7g6G%{@t@RZvilVaz$ST433YauhjJ%*P9tfk zK~UTVHD+vRo2UoD@7{c&h}XTZPj7IwU7VpDFF&@M-Y`o?#C>~y!GVH~h+8D0-H9V; zZx8NJ&%0L?;11!CuNVLSY3t16q3RkqJ|?nOV;e?SmN7JzELqA{$U2m*tn(=QzLYGX zX+(N5QC-=xuaPZ-NGODalET;-G+EL-l~Ufk*F0@{-}Cv*=PdVowtLV0W9~io_iN3L z(+iVNTydGm*NiyQ@m23L>`pLAEm6ic7JK4cx`$NQ>LbJ+w~GY#)M-7XJ=CB}PgvbF zD^Bh>sGV?l%+8YiP)aY%Qupb+t9QNieMc<@i@oj9wD<2>^#MyorDx1al}A;YbeWKy5iM_g|DkJ`>%5{()W ztgM<67>~4rMx0%{Y9QGQh0$;`K*ejnhC2xoxOTIr zE>n|L)B8t1+1e-c)dqxim_-+#^r}1M{>Ge|>UBNi*2kJA0;P)PWB*km_{h^o**ou^ zsm$8btMa+AGb)RuvQw2QRW-Ue!jRmkq)wiTSytqmv0H;@Dp=vGF**qW8i#mqK`+t< zWTVK}i!*j(6$o89ZbtQ@_j|any;@#<^i6_QA^=$yjJ3vGv9uPIr&_t@75e1EUjQ{q z!J;nS`B7OlY$&_#Ap9-a5gh|5azpg8Z{^q*B{tYRd zD?aRkDFrotu<`BswHuCcX(V~Se6Nv$?BvD4;eEZ;&?}C1Y>pk()h|Dh%d$046jP&} zd6@mZLFBt<7RcsO^9w*-`Md;0Gj8nl_KV)sYMSp{^4gm__xT$u4PBC6X}|6h@Uj*e z;7B8zl~Y);4YI~wM_YXQa6LPn4vOJg3J>E?Cgp?}vAuNWhjkA^E}B6^A@yk{->SjMlvizuS|jYZcY{TyXS6c6|_`N|D0iu4K=6SU=P*Pu6_!MAp?HR-mCpfA#Z$F(s+k zHk&Fb0-?e=BZ|(6T*s}OJgy91-Ayu2*)6yD5QQY%y3!alN^w0sDmUIeG4_wL8Itb6 z-_o{ne4V%-6VHtzSktA}?K+&S*ZB!nbZE~}$D!lvoE{RsG(~itw0Hzpgm^V>@^yis zc5(4lMLm(Lf_6@geUdzGed3iNB~f+`ql-ZV%lu=Z@@HrdW8B^b`M2@}RI*M-cXuZT z{=H&mHyC>R>j}d(2egu=eDX_XZ<=$~OW%!-ndO0_{GZjTBwHZ6t@(MG%F;`oYxpOQ zSNR2mim^8%U)or^Oe8k&MDw0gtt2<*MBlSLaHKmMEO=fbY|zJDJln(>H*=wp&!hiv z5+SSFgy*l~B)_g_Ma+4|s|HJNc1J2|#VmRo>q=|ozGt!S9D;n`tLp|_;^mWH@K%>} zWu4|xH)Ayley*yIQL%33T+mmE40HHqorHuW$KX>UCLS@#B=-!bIe*OiO^)b>u;A5FUzxo?HC!@vPnv0m4=6-T>(jY$TEZ?c- zaL+ySPYp@I!u__#2rHI?qJ28{e!4q)FC?Rk^!DEtx)OV*m^)P`&{Ifd;94R_z2Aqk z1i=(%ji}?V5m}fVA4O|sAWqiv?_oaOPcDzRyyIF;rWAWnr3r;c4`&*TL*E6-q*%zg zz8qj{XGarHl)dXRsdryOJg}765&TI*w-69!d)`+vth~S;wvWjv5ZH0IJt)S7PW2># zs&Vg5Y6ijIJ9l1Ix>|%)j`s@F-eqO0K)9NWl?`4+9*ih=4!BDW%_WC&hwoL2jnC}G z^vz?U@Ags}Us4)Pm*mc_=JicfdtLLGiMv~6Snu9IO+V1+zNUO4BQnPK%9I!&1_~GZ z>THXu6y+SH?fPia({^+A%g&km=`+n7DK08=gDQL^mDG0orA~FAy*4IDE4Qq(jZmNP z?P365ABnrW&9j3{2c{RS1Ut?!DY~%YoIBF2FplG-(qguP^l0gPlcJVYWl7Hz5v31v z*BoN(^j&rztZjV1__D*^b_Z;J076Jr z!?xlt9mg1D17rC?N#-|P$z87Gql7!K9J6xnI_-s?*3yZB_q* zj}SE3mH1TO+{gHYmBriGr0N_yx!Ce7*BET(El)=y7a1aX4|ndUv)cRc4kF=HLAXL7 zS?!1!AfAv&!UK7xW)|bdU;3$?<WNZas@@+6uTG=e2qc>=e`PYj*jdmEs9{p4>F}mh@nn}D?EB(S+oig zq?=b0d#zNsAV%bc|1pFIn!dEAe1|7Bv_4ghNA3O4FAZwAx1JBPzyi zjK2(1(HMVfA^*#iRe2uHpW{CM^xlVNb4yy5(Jxju3WFBTTWryoaeWNpB~+zEhe zI*4KdF42ZUr8r=)zXV_~X-ItRM<^f)Gl4;}yTPduF<`V~UywX>WIyyn{~(~afJov5 zBPWi**Ezx7iQ{m6E>L1p10Ku;o|?qNH+Di13ZzUPg;(){xg`MjfFJ-mPD#TJ_!(Ir z8aKExxf8q`jo|vxY5}nb$vF6RN)^5YKuI*XahVmwPa~LVpS@bZplKw0NSIMxHZ2Wo zy0qs(ZUT~!P|D`;euM&Igct)#xXJ^@jUj+7_SiotC@vuSOEAEY85w|KjSIE50;xF} zY=Iu{Wk6FiDgeXabW^L18wS(b0tL%}iqvDk7Mr*&K%Nq#l@_WD^QQe4_?C)<=cqts zSjc-z68O{X=ttcGV&MTWXx8{&lcVNYB)nFGQE6jV3}DzCL1V6C`ST1^YeA3-WA?xN zWd0m;*o}mX7qQS~aZZMFFVBWNB0L|x-aJoLDJbr#3@XMXy zU)8!_W0f(6AaU^1yaK$>0VF;X2XU_z;G-^3avya05n$tMA^3(nIP}^bKHv!+qG>T! z!QnwJ@l8R!e**%xtW)Iuo8QxSdA-e*%aGUmg$@26?5EhCIgSa=w+&k0Y|sM(m=5eu zvAyrzLCav5&;R!JvzaZ@dz)tzlwtaP(f0d;#32XxP#_dxLDpdfxK0Rk`|yK-6gKe0 zupqESBkV_~P+UNi2>l6`uuFoy!w6uD`p*`)HsU9&xf2D-QxL!}eGwQ;YztgM_zoX{ zKfdv^UIRN464;i8*Mf{90!9?n9+8GWNQbiWVA==*`ZDA9sa?oqa9RgCQWg0XFHff%59CjAh5zR|&066m+{l``Lbm0wQbicUTBq8bttGcD?h``a_(MU|_#sz`#V)mi$T5NH3^>3e7!r0!_>>r|)?YmKbU>w3vD# z+xXyAnhfx^_WGpw_;OU35_JnyJxJTkechWP|00E6er64vrLE!^^HGR-RtB!-d{KP) zE#nm|yGjW@qX&7w^AM#?_i#V&xDVX)onHQ?0f0}~A%>SJ323qi_ zUW`-V&I%*7n^c=Qw>x~9I^J|gWMN33y3~i?&6N0$Ie8MCEi*wjr_1;druf($Jr;<= z16yD)wdSS&GJ39dF)J&gh>q4ev!sNPP!$wn!qc%a!REZ?DPT14#~;gBqYkPMA67ep z*yw3I_G+zm+dteG-Dzm(J{(y0y4n{QJ^l%NgDga7b&Q1?>_7`p0TwOdTad> zD$c+J)ihS1d%b-R1hNq_ZfQndv$=+CHwdaxP-5bc^V}|R)VV?sQ zG`MpON9^Y5sB&G@uWp8}YHprga>ERzXU9BnKh^Ve94m5f(oQ#Xr}q_owr7v3CY-az z+)VtLTWqS*nAQmYq*{+?7}0yH??dfumg4P|baz-_|G*zVa+qfC&9GJh*E<{0L~!JB zC?O)kPApy>p+iKk6NR|Z$(C9kfy)Ql&w6~(s^>nu&_xXUom17|NQJ zC!W#J`GShp z{)gR21Y#3FrI5xcJFz4~Y=Mo`#nr7e&&QLS!6V0^xW_}UrI5erSoP7xqV8g1sghvh zN-O20s{OXLL^}_k7@xYAN6%4T*3|WEN+;B5BHDZl~&} z^&cC!{>r83p4b2)mRfEWLm}E^u?J%nc?d{&FfdqHu>Up+SYc?xc1hZlzbNqAU0o9M z-<9H-q7yggm|Trc4LY0bHl^f8v1D<1vB{h1U~xP6c3#2b!QWjUck^@MBM!dY(m5WX zb3~Lmo?t$q7wwmQjM2^Q_O$W>O#bt0-o8Qir~EzMzUSqKq9AA&d@2ZOHv9@udx%hf z-A@kH{;21S$B+;d*YzRX2~QxO164DaRw#DAKbOVhkeu4XAhsBFxIA$d+RtTN1e}Dy zx#+CB_7Gn@YtTtE%{MZn^diIEQaRlrXZu#7g8au$c^~LkBW(i4ZT_*&mv7{-hO~uW z44Hw8d}>LR4X<18({b)2_E@eWLrkeXyuYkZ<_bZaDHizEyx;YY`4}K~keO(YJ>td> z@uT)orpYAEP7|Ga@BHk@2nN#|(0yyO7y$WIR0_^|;wn|HjQ1Vbr?{6FZIeh4n_(S$ zTkBJy{rWXRcX|@I=r#ixi#p}4xM39y{W4x#{$lLWwoi|@P{UI!37}Y22a*ZO}b((VF*`8paErO^WCTp%N z<>FN$pHBV+K8IX9p2Is6LJ}3&!_{Kncsy70KWeG#EZUoORe|!(^O}=NJ6_7o(DDOH zW9Ug28!xAm3HH&NtiRisRH{FCw96|_s%;`v`gN_(v~VoDV*I^t8ytiBA>=gx)7(}) z#l({u(KeWVjO}at0n5{~plTc`GD0_w)GhzVT^sy{s_Vj=YfjDjaXQU}RPuvdqJ{e3 z8I^kn%`FmyFMyM&p$|qO&G&Otxe9IgpO5e1ZE7+srpdb?A-_6Zfkr1ZSu&eHYN|AY zN?Uj%RL;~%!Irg)-2wts;VR0l=}%^XN{`mw$X-V^kqOIMPR zw+INRO)}`8{ZJkr@DrAif%1aH-(HSr54jVK%aMrk0PF9En zH%MNT!mPugh>L{*x{ijH)TKet#zMAshp#goVhm!_p0~i|d=b zKX7*^*a-1xuCQu`L9M{HiekBiSQ0yn`J$*EPfRJ5xty~Qm)yRw2Dbcz`oGhg0uX|1lABxTc^AgGQH#C~UWis6c^j@uoY% z5%W9q98fvVAT}DuiIJ>>vg{baVd$R_*It34ZyL{HL7T6j=ZXD zKGVCZcj{bZlHWA0wSDWvXs~uqKy|(%$5&z#$PrDdK2o&w5ts!UVaKN#7Ztt9Z`11g}{ zcd{hS(ApwuI{YHb3KQC~^mFnZ@0!Up62{`MAJ3d9HmhzD@kf^LL)2q)w%}XS*^~qS%%ns#qGIN=NbuLV#TR|pEGSRY(K;zUkUVM%e zd!=*>X#socMI;hG0N&8IDlSeAmvLz`KGE`M(?pj3nCq&ZQ1SginfsILm|eS zH@kIU+X7XJ-5G53@UV6*F_ZZ1hYCDC`*%TSH$F^~9sBIS6jh4C@9r~Uiy^MeGcH4g z?Kv`etoI%EL8;x-skig=DTOOurPqz}J`I$goshX~=SFDnq6`?7Z3u|C3if z-*`tqVlp!`ZkoQHn$!ajh*^DsADebD$yGPh2$f#y#BXWtF865&F`QwbsdD4=7O=$n zT=AhV>SpHUA$I}?!opy)s2EuKlWR(B{ASlW&pm68z_fhD?mXOEG`|*EE z8mqiOCkRh)+dW$P$&~q@%j&Djt3?&!hj6mpwNG&0&BO1N-jNMx9wt3F;sc>59P`X- zMVw!hBqY&r#{O5n=Rzd$eb<>an8LGvr?NvZ^y% z6U#A93?#Ue|GpZ|F98zK1+GjremNb1@6@cz z7V_ywkBWBAo1>I1)h&AV6h5MC_rVk-cUbkht>BYOwEBVkIp>4fUpez)BPtm14(Z#fEq|jjBK#7&zc4OF1<&#B8gHm3f~};t!6o*nbFq z3B@xY|0V_RD$!hrO8|zNzpW823?jnPp~tz8_>(T?O9T2ahz_ zec%rwzyE!9tR9p&hZzsOlF1 z1;Kz9-<+FbPv@}5xU;}3FJtCpVG#x&Lh&khYWz)?k-B@_E&+TC4M`La=?JOu`Rm%N zWamCs)eN`k)X;cwYcN9j3Anl}F&B`^p`!WCf8FIki?6h*HvytD0Nr8Ike3=J;yH0A zV+P5P8*ixF?qoy>YJQ-LAN{~DK=$ur#VVcTvGbd-zd_7Jt+|elsV|mkHc`5t%(NembP<$4=Gb1pKp5sg^O!rh**7qbcT&jeu;haDMQQE7iCS#+w6MCo znvrj`4uwQG2YaQluyN&~X;}bvxNl1qvXbgMzX+CEYX(pFTdGn=f=F(%kpGOi*`XBK zc873Gx75)Ar>HH*zo-dBMAQTdDZ{X3A31^gaSO!Ki^V@NR(plHRkt{Br8OU19Oh(M zbQK+PpsuC;XfnHm&>(36OT8cS)qs~W&NXI_mHZZ}=6c+9WVw(4{T?72(>Ai}A$JRO zDcD>=fBm(wgNJSH+;pO2NE^Jh7-*qv*$nj(^}JQKZX?NOO$Cc)aypmxVd)EDb$DtC zuuS3NuWXpkV!wJ7{5N`H5-;Om9KiD7ZHs1pnT^Na1IdWE?zfaaIK}8Cb~jrrx#q|L zQYtpP=ej12rIGe@j|H?Ok^hxMJ5@eZCnB2lh6o&0>7Sv#b)l=m1?FQfIX=ehys%Cb z%@F|bhsvi3!eMvT2opkg8j^c7Ms@f8eV^lD>Ops2(Eom?{v%#l8q6Aqev&V~B<1G4 zV`{27?tR11a0?|gKMIgy--}ugV_BBujMG~EJX_Pbd;}Au{Ril2Fn3vRV!)?Q6{-w} zbokVSg(mz8Y0>HN%{PEBKf11;PIgPxsBG*_)0jaWfF?p&l|Q;_Y!H^kKLqJTE-+Sd z_)HK{&Ep6ArOptwU!9HRY?&vYr{`*=yu7dJshy+i$z`oj+m$-mW$M8+zpLp<8J9Gb z!Z4lLKY9je{sD@eWgY~`snUNL>_KL6d83>Vj~fv10*XQriS&=ZAR9=l#FF$WBKkGR z`%>T->GNH5Fkb%2&*=*Ji23cy&a(0(APAAx*5Q@K=58Ho=&A$x0bD_+uDOPX-b6Hw zcvZX*9iHZ#&petTj)g8s;>2$OGE{aUaE--kz35JQ(tvw47OidBaeJX%jUj&V_!h-! zXK()YA4(-Ti<@YVyfZi$K1=1|Nvip>%@6NkTIP4gy^%%r$Mytj2z$uI*j($Fzz5~j zLCD6s^fD+nkKCC_TaXA+;c%SN5^owz4i)!xv1EHnZH+p;qht4o)|=}2d8(w5%An$; z!^7V+aiEd0X?E!Vv7oO(3YVT0&P3h?<+2^`lZlrHGxP=TEfMM9W~EKX*T89_9p+QP zi(`^lNA;t{5zE^>t?mi3AgkmdZ|Bfsc!-AyZ)ie((nhyyub||=OOdNL=pJ7SYQ|EG z-Gj@b#{+M0^OcPJbLAYims2u9t!>FA*z~=|4DbNqE1&B*pKq}b&Nf-u91rELq(<4E z!s%s{#9ddly6Oq;_xZ%H=hxmZFbUQ-{ng5tcGlJ0B-G>A^IH@zH=S{RDTJ{JDaW&) z-4CzTTdM7+IalL;(k613=lJR2aUiOo`IgJ!k+bKSt1-wRp0!a_S@?$7L0FMUE$P6c z1Za~xY`p4m{G?v!+TBPriv0eP!PfgnL*3VvEEe^EMffiwqfp##<#UL7Ko9y;V3GA~ z6I3t^s?SIPRXfsIFTTOHE!&lZ$Tj#$W0__-MYcD@Mi}fB>tAq32+sH%G!=4ANaLLL zET>Z1Rx844r6FtCF@yzNC4)x33V)^-;^poN@n4;5>qz6Wk zH1`8L-x!w%1NV|+Kl-MY$%&AOITrdB?mFEsUPT(%SA;$T`Nfbb%-k^>LP3H z@V%U>P^u|el)68Y zHRfPclv6g}53DhQBoxm_l%H|`5&{>5RZI{AyIXAV1*s)OB6zz7$&OAi$H?VN{1su6 zPr@WsK{-K`uNUXf`=|^z-7%g}b@F330#|bnnE9k?7V=0>XBUmaVXfyEO%Y0XTW?^t z?4+G!q<;dmt;?*z*wod9rM4S>iSlL71;;^=s^IR>E)ZYtM`%5OC4q@}^8$a)EdDx9 zQ#EE99N3izLyE{XzoEZT_LePFIFo^G)rUQO+(X&&3Xp*n~#pW5rDe*%X$V{*^!4s3IYyJvIFM!qv zl}{<`8bba7n}-Iuz{K;XL1t^jXk!TcVfb$HktTU5c<5dIF~4|D8vVuH#|83xr%hMs z?g!K-mER8;P9UOiXeuSYAxWn1ATmaNOZlv+q^#M6DMP`;KPsFJ{0yifhkjB36I>vK zgOnXlEh0PBk-^ST=V?>an#`_GY?jC(oM;=p?p^g@zCRNq5UqA|#8SkQ`>7Ah2iv!F1;=MSG_PjzE9Z@Ihk0{-CiM3(Nu|DR6MCsw1By)R$53g5 z#m^3N8fF;Z*7_=Hr-Ay~0=H~>f#@9mXu`@iaSds<-7JE>BOk!&@`3ImsZR_dc8>^O#aza>KF7OPJNFbBpU5oQa=xTw~Kg5qa`qDG5KVr;V zvd%Jb9y*iFOlpZgKfPB*<5G718R?Z1^ZpIAO_{Z2_zdgE^i*AjF25CL9Z}K~{}*1^ zCsqMe0xd+_(M{1ZzNNAeJE`5AH)e;WKn6k9(%|&do@&8Z!h$Rb##hJ^Z*>6ow|j)U zA9#dDd~zs#@&LmBlBTqe3;edj)H--16}R4;Iyf*eCTuV;`u}_=>@=ls_<#@QB-R&9 zL3`C&sat6bd66W447mcE&Il?Q9AyBh2)e{RSX_H5^0m|WE-{tTfk#!UR4h>y4vj0k zQhr)9_?VKn-_6?jkF*1xSLhm(1RfBp}!&W62uV{8+sIp^h(gXNbNw;NmE8IFLE*VeMV&tjeq3Dx7ySe(L!VuACxIEUqWVk3Eo5-ULbj0C!@Z#i2M1Uf$(|=WR$t2vLIm$kD|q+s&H&prb@UFUX*7CDW3j4iT&QwM;?T)`FVr zAoBOGzNR$$P+F!LGOwb9?YEqG^CLJb%N?gSu38#&M_^*#ivy3uri&3KI_G!iE?|}= zbU-;6+JsP#q)4<2uHL0&zxvm##w$;@ZqMZ*KxtT1p9zbdL_nfFr|M8uon)yQto?rO22a!{f)QsCJr5#CP%*YhG?2B^GG|4jGNjDN`v7jb<+0c*G1csqlK zwUNL+{l(bT9D;p}i0(oraA54VH;5(B2om-Y8wR-eC^6Z@F(gN-qRkZ3U1Fg&cts`b z*lC`q4!tO?EU@W}U$|818*Y(Sd=#ro6-?yoh?DZXT!xC%*dkefu`K?Ey@N;2)nZKm zWRszUd2Di8OoaVc*#u1?vse@vjSJGE3?~x_K0B#7+0<(pv?U^_=_NDB!E>vj)oY&K zU<@$YTr|;9pg8fll%FS* z$9!@7sPV^BRX#m>)njt7dzagyjHD$1?aH5uljSyD(qHcS2YT=QyB^FtnBIS z+4=Gab_OLJtsgl24Zgj*K2Hnvj!Ld3CB*EPmtJhnrG}VZ>Quikp*j`I=&fZMh8%)GX+z@gc?v?uzt*1tXSgn`q$APMC@hR2J&L~=;A9-S{ zu^m}+$E(|N8uZjPO2?jtRjc2DxbJn+dFMiif2iY?SD)JZ_Vr=umGD0aP)kBD-rW3f^0sdjmVw3&&0ZM#eGu|RmLzDDl6TbtXzLw3HSusL zciNsdFQ=E1jh=(|Ff00G&nqm4h|wo>&OesTO>4-`+=xM~Wp+0sD0)yT$H7fnvAm^c z2&}ecDki1fAmA4U#rPX;dmRbPj8yuP^N!3aotbk*sipoyd_rVJ1_S7Ch zq&?lb`Bkcx<$~;yrMIzcFJ7*+yMl?S1FE!&1Ng@9Ul3da2lBL64Djim&#&Nm-tZji zv_+KKGHw-=B)HO8-q5+R_OZvifAEdP;oEZMCRqDqYgA>J@Fod?);UE}BX}+@gPgsi z(^y~)7klb_q;e(0T<2%`dNtBv^;I1mQPe(eHyJA7c*0@z1;qm`c9PjNPo~;>D`uv$ z-vGw9#926x=z;YzLIzeGh8EbmX5zZ#5H83^YO|Kan*tk+Gb^Xvt4 z24bnYu-)i5RAdm~MH7(qYQ(1?A@7PN{lXQ7Ph4I;N?Tg^UUG=r^K?M@#wPMJ$<4_m z8I7&m9d=Zux-P?edKB@Pcgus2hW1LpF^+s9dW=XAoOP`aBHxf}FL#{9C0}ZVCoTd@Qscs~AwyA% zj&Wsh+!?kwBXwGNf{ttoeNW{X*X8mqw2FmmwEy6nZHiFf@%~%$Q5Wi56q=A!rZG%3 ztP~-q`HHQ`zjJB<1wmjj4Q z3n`=rbbJFay|Mm%wN5goeOplx!?DTJb8u$?(T9(UiLp7Nlahr)mKR(i=aIE>TwF4S z_^CKHNdLIV@GH`htoY?1wmk7JV*kT=S*t->@Pgz?T{6(wihJ`nBOP1O;@5)r=kEK! z^Sk20=V?jQxB3y`6H^FAr_`PPWP-drOzy;Z0K1%uFa>QSI=qbCqTJUlUb-vlmi*dy zj)4VqQn5pLdV-7x*RLSOZL~07@Zf@DG+fqa*^l02ma0ALgLDlC>QH#=MKxM%-6cIt z@WE*6?;(6XU{ZL|DjaAaRPFyk$krd0w~TsycKg7+8uxi5b#w7y zv!6u5nO68I0n|(mb!Aol_utq$>3N%PCR@u)Z5!V!vlZrJ9=*CSRxK5QljrMW@Ww{TK8JD2=pW2QKzZJL;Ipv&^+&dW*v}{*1 zSUzz-yK%XYM+8n8D!*HqqTM4Lc_-gI;eE7Rm!`_Tsd3LA9k5(^){8_@3QECWKC&h zCr@|mbxH@a?XoFck%y&nlL4g-@8)YcrGgjwG#%lq86u8o*|@sgwzrco{#xoL?kwCI z@w!7&z(9>{i$)%o8Ga@{#l*J}JvqVh4lHv;*LsU6F9{CVB##$(Wxgwd6y#E>Va-_arru~T^%DM0)SC}t=>%lJyH+;qKTSZHpLz?X%Wvr?H)0zy>%QPY(d&NOjBWY* z!SAuVhR-(dr(=O^vNf2cG^gWs?zx2CbWD9?xS(57MrT>>X}N(zZg#v#+wXXMt=Qt9 zHN4_l3L{lm0?}+x+pcM$iofbj5V#jd6W}||@3)SEPS0ppm=N{>keQg`9{PIR zX1NU};MSM|;cb{3)b={V);NP^*yVIJKQcQEp4>zcN3-h5moc59y zDtyQyVE~>TUaiI8I997TTcecMbun!xS8O*~s>BHw-pj>hnZrc+w<%zM5Of1yI8r{e zVteCRr6{dzqb|0o?GavZd34-H#bC=a5kHjC7Am#>CazJJfzyI7G`A{8PJt{x3jN3JZT(?OwH)DNXS<$3g9xJJe}mS&YG!ux)&++&B|Sh zZF711Zn8<8kus5sZs|RthJ7-I>&ECTyT6sIW;xg$lyy@+(I@lrbzH;*JYR>8NWmfpc zndd}Z7MjyZm(}f5ZF+q{wZti%EWL7arC9&9TkrQ>$VDJ)sSZaLQ%kjm2Kly>;%o5!S(7tXZ-*hlmEM zS!2UZ$Ey_eXDc0Z`)sdxqa6BW3i7;kXuosy_fDBd41q|)X`ku#o^>8u8RcdJq8t6a z+TyaUg^0!8G(dH=(|e0p5~V4TKQ*$v((Us0Jo@s#aW{WUaAz|q_IPF1B>Lg^A8DTP zUzrcz@B=z6pQ(POCcVhh`SL;$=nPN%d&j$qErsw*W#m$V(-JZ)Klvj$K+(@oB~JjN z(pb$>LYNYQWT1bcgH#!$+FlKtx;j@pdU|AZ^Y`Ok<}OVN;=c_zaH?7cn;}&N3=KbV zB@9P#Xa3+%?$;r_PwqD%z)YZ4Bfw0e))PcMf&r?TAS=7DF_ii-rk`5N__87}yg?IZJ;Aw%*omusSz3X32H#`< z{>9TsEX~1&Wbq@2qjvGN9)-kCB9|~+t69|%`^3Tvj|s9ZqG`VulKH~8egD3?BOGFB zI15O#3Dm*ORw>xrMSbe3nt^Lu$ucyNhfW|iQkNpu{+PGd3HSv-FW!+|K9?JAXSMl& zGwAL7K80_G90}p*Rx-iN^Y!>qd}>)urBhxWnI0bIp|F@+U+Url-VsRi#h;TwI91FX z=C>{_yyYNqPwc@N|ypzNQ7+oK4-KMcR&hx<(fw^s%CI|+S&gknxmwmJy^$_&m4`vP!{ z`xS}YLS%SA>JT^Ls_>R& z%Kd~Is;s8;H`Pmcx^dD7A4+y5=rP6do0KQ^JJ*5h<7(qjba$4Uz3?3|&htK)?&aue zDLTuLXsR1AQsWVrEd*xi^OF;Way8Jtg7^ylBnvBh76grOvM1xkD>kwZ#h8hjf$9(4 z5JkoLi2(DJ0IMoW@m&~>PopJch55RIh};Q3)QuBoRXRgnAgz$`ymDjs0l4EXRP8~V4a&p%-U<(H-UIN=o?l>H4#tha`*Nd``l?S%`?`+yAIv< zaD+y^u1o!Dbe?OqOh(@J?^e}8x@1(_ie-FTNO9jAbD3+d?!f+8<Idi}L_YObnei1w_ z%6Vp(8SI*>cT2f*=tNw^nod!}pxrxwnN~)jcE?OXi;oCds^ZgBf9M3g66ysV6E3qj zD&)!q&x@J6%QPdZIT(>~gdnbFfBUI0l9M}aMezuf(U4^NDwXwT%>fZl1iepidXMqU z5`Fzvef`wpw~U|W(ec9OY3A8wwci%uec4)x_%AMae~-tQ8o9{?;2_|PSycWDLBh6n zbq?m?%YO;-pX5Kdi8i2CqQ5iqZ|fVsWOr>|I}$|{%&36z zumlqfOq>Y}jP(D3&aWB*fSe35j{<#4?pKybi!3ZUVhDOBwBBDTUs)-uhk1guB}sj( ztj_iIl~_ZEhK$ZqtPDs+$%Zw(u5~A`wXMKaCu1Cay*J_Kc?Ife@u9s*mYw(AAE$-> zng4j7`}vhWpNGvQ+Oz-Rm;W%JoY!4ZNU7Axt%PT zu12AZaBQ105f_GeaxQ8#A|Lj1X!gjnhm)aPmp3u-t`=;=u3xWm1M-~cgBs6(VE>^U za8JJI78*igZ&NCF1~5ndiqeA~Ao@k$s1vxMZJ~^dUEPzlO!*O=QY$5M=SQsL7z5>l zyJlqSCbl_uiT8=V?b1OwBdG~?$+j`b2%r4MA5=W-nmvpV?G0vuUy&NnF{hBpi+GoE zLUD=e_mFE-Gv|=m?vX#dCVh61$dwOmSC@K%wB=StanX3o1~?hQ2u~$~(?kc-8^n}a znCL4Y0&*UIkgF6;e2V@-t9!cLb$#RxisHQa`C=#oFn@|WNO1ig7~28fVv91F90U3i)`7JUGYECJD=%M|GT{tFB=nuk}v)Yc{Fy)-)hPJ zSz^B@r;(q3Ao6h-d6v_`-H_6fqrq*>q-u4v#4zQ$-SSt8M1W_{;iF8clmmI=*;J7= zy|AO!5>Sn?t)KGL-tXL1s(?ZGH~sn0`}B2$;x{UTC+ zt$l}NA}#3lr>v1uHcMNV@!n}(#r|&W1Hc=Z*MBQ6SLka&`PDWatgpa;En7hejv7|h zBf1Pee9*qr4ME@LUT5pUH_d73O}*lU++=t07mmT|S10+cRLaK?&1RxRq4gY-me`70 zARoFXk8A3AeG4SJc_M7od{4Du!NZ{5GUjBa79U*MXd!F^JL;c=^XKhSIfI_>k1{fDe49P5NnAuUZ98$_|~)A3~OZ$+4;WtuH=92N+& z=4k85L+euotP<`#=H@EAlF(`5!D^_f`%#skcLZU;$U1R^h_c2dF=x8)39~_Wa?SSNfH~sIe?@qW#m*(1apk%K zjN@u4BcJIDa-d%M#_kz*J?j6AdET;*1BO}q*Bajfc1cU$22`Up>k<2nTi_t0^@XXb z!ZK z9IYToj^*N!N3dj7)1yP_rh>r}zgV=O@f5}Ukb~aSa#@kjP=4dQJ*jc|g@W(qH0jR= z+koyN#JyYG0?DcJ*@x^GBmlp-A^J{k`b1aYe5@=U5rC9JsmJ|OvrKR0l_P+FUGmGp z2sI4C<9PA@iVsM~RtXs~-viWKR2DoC*fVo@Ly1PW@l43U119 za+rmTrwJCCSVkV?)gML+;5e`nX)al347Q`kMy2{mEU*`j!jFca0MNwTH=<4q5Oevz z=FO-!fh`iF^s)=%;1vsrJu_wQ_OGJD1W~ zN89e%V0ZpSx`eC=U>nRyJ2!ioV(;tx_ z0k81pZJ1R!za3r2<~gcFdhqgCq@53987jvYmy^*_ohLPPD^mxB`6ivpbTrf^M*!BN z=8AoG)KH5Y`u&#{A620XeK%C84$mMxa#?j9QdXth;bu5KkojM1Cm)p0!p}Z#*>Dg4 zEBrzug2zhibn?XtQ*!iWD>rdFB|C?~i1KV8R?Up(eO)(mnT1a0bn;xXplHA8{G(hT zkO;ZFNJas2o8nG^5FxBeg)hJU5 zEU4C>cM8)D;O#HqEf}0$L@0BXeYirCJD!m&7^J|yixs4r8OWm|(0w}p5G2d{e9I`B zU^)8;{0dnRPT$dG|2}Dq%oU`2T6DMQ`2|%rvFcY)s&;A&+%k?P$0fU+p6|E5MhrnkB+8-t^Z@8R=|5C?~e)EG#;i8W+j@g8fF(0~euF=cv=^V^W&#KQG0XSUR+2V`9#FIs=@+d$Q)hv!-E&TO=#7`J6Ht%F(OG+}j$F`W7qLATqzZ7@_2+NT$sK#QX;( zEre^&v(sKXE#Q4BeXBZ-|1i>=hG&LJGNX2NodosFbjTW*#1ub$ofrDG~tPY zgl6;Pc+Ce_nfG(ea%MRB!qBLiaZjJZd71hNw?+|e)*(KZtsAO^mD%ZOGiPJ@Ynlob z>BQ}t=(9y|Vcy3ESJ#|*(C*$7Aab4bVuyYAbM4ReK)$MQBfnRT-c`)PSjF;TD1KH+ z+2P&qkzpp)7))wZ{p|1{dTSH$7yN;8^?v6C#pAQQ*nnF;5=#c(iItG2pp2Xv6h5J? zK}^Hm^fH{{U|4Yf< z;)h-X|1)jsc=#;pY!nyGHc>5^^UiJNoFvpUU}2G+fA zY{^l57)_9>phz1^s?kMORPsMi?Ki%@b$$s@rzl_5`l;?U%TrW8FzHklk#;UIrGIIB ze_h5|rG;P%;nDcK%E^3`*X|O0a*gw|<(I_1 zjZ81K4b{;riuTQeIVA3RX%n;J6*G+NP{(>1U(Pf`GU1F{C0DOH%S(-zJf0BYpA4GvS;qPdnqm+)!s=OYv@ zzG*}X%SwUVQ=mumb?6+EhtO{%W~0l2%mIn#;G$qpI$N5d^`>Q`1Ub%L?Xq{BviBIH zvds%FKJ*tB#fd&CQz4}XPCK83i6oa}FeIyDUvPmyasWyIIJ2(_3O?Z=DyEaP+>NU4 zpI2Y=OQ%m%I~L5Y5j*L@QeP{p55nqkht*P@_W*T zFw_Yik*HK3(=M~v7;f$-1O<0>^4~*2nIth`l4|WGK>L>Ryo$^^3ffPhLdG}Mg-J!( zSkp96hf4K}8~4Qig-0;OJs>0&lpx*?ud2;pYy0<`UYL_2Lc5U~(}Fk6rBV zhA}gqs#G-b&-zUF^jGk=Pr1iQ7l(ZB;Qpwn>hgxxv-vQMt{DBu>Vf%xs9f#7vFpPZ zk_orG27?2h$qU~1FVIJ>N5z#8?LpDsJCT;50LS}X0hv7LnhI>+Kn{l=P~RU>mh`vm zAe2>PWf->pjLFe1@rg9>r;v<~ZR;VgC`4T$3mla5$T<`J4_Dt5omtc^n~rVUwr$(C z)3Kc|wr$(CZL_0}(XpMIbH*L#-v7L>v7hE%HCN4=Rr%~#>ty)Q2i5bTmK>bDHK&&# zE(QIF+dz7(f*1s$>?4r%)>d8T_QJ@HhV4IeYM zOVDU~aP_BtoV2C2hOex@53IlsSTBcJf1hamKX7Mb?EmU|;P-!`tNTfKvO=|A4O>0n z9+SRE3w`st{VUMQ@5J?{FQ|F2RrGGy1$)qY!}oFKvoy%RHn9=leFy#&4ESuo1;S1C!d=IqLgWna1UnCfn3qH zeN$qFRONo5TnwPuRk2hEtJ5Gy3@N}gPJWs~eae1_V53PV0<1zs2KUu#{l$WQ43o)_ zVGSLki!mb0BqKt_U=p8Xz$X9*%eZVtB+p1@2Mp&xazB4*(JpFFDZ##9(!}Vw1cfq4 zlIok`9YWG@i7`%6DVS&RfOz_(^m9JRgPhZII4cAKUPlzS%Oq(MLWBaK#)dTd;SPHt z_9&Ybj6st3`D>8j=c7bTn0)aEYV+@4(kBel^S(h@fJnuoyXgrazY*|)!HEY^_pJ<+oq#-vC;*ov@jjQC3BDw zoOHe^=N&fMR}{4BOgw;xqSd4bFfYJz5{z2{JhnK&sSHAwQhzYrdbAU_6kPdRZSIkP z_ZHfp181Ym{iRxkjN0wSIiCEUGjjq(F-EqygO}=BmSN^hJMzyFeTg;I#akrzQV#Yc zh-B(~pPHVlrj?$9?(e+!I29%Y7(OZ>gAWQ47ZUXeq(U{-{R;p*tj4Tg%Lpu)@H$bz zCN2^y=NwZTIsI_t)&v(-Kdc7#&vm0;?vn`E*7^q@FoYe&cj2maA<#3z|73x_W{#X_ zfM$JFl@ok0XLaP>3``IMV&~HxHXE-%q%V?(yUH>jbYmFb(f7O&2Ecu6zCnrg9)la6X06HGjjM zAcmlx2l-`NmGM`1|C9Vinvegc+>;Eiu#=X&QIfK*V4Dd0IuM~N`6>|Vf2el>h@@)= zti&5^KunUY0*Vmgm_@25>Otp zd%PK7%nIYYWKHD*iQsdXm=Li99`Z#foVIBL0L9C2z;UWI#Ol*3_$tfxBiq#`Y@?Dw zRF_;;EL$7ZbI-{DQIN2ErQbNsJ^t0Xd{VM!3u6C3uEvJhQ_>uOewYFRwL9@-js4)e3o4G$RA5pFE zfC(!%UU}N^EW1AgZzV|<(q^w0Rt9$1^mt@QoT)~i!{ZvD4X)3cUk52yk+HB28!7w+79`(@vPSv<@9kn##{YP9ap zn*p3bB#9GWM5Xfmszx|ALSn-nd+`ZGep8n?_^pBaW=SmW8;t%|eZ#ePKZqfm2P}Rf z!4p`eH_h_EF_YInZSzevJZZ{HxhB+^F~<{^w1|7%Cu`4{$)# z4Z}Ib5^ozONB63POBWFQcH^g|2gTSAaK5$0#Mno>xGJ)9enWkLLFJp4&p(#uEWmV) zfI?m9nIA=2cSIv450a%8x*Fs|lavLgDjL1`C5#|~qd+ahie)Me%KUhx1l z0Ub|8Hl7d5Tn9>3Ap~v~FSbnks0cIx72k+VN)*Ja5t#lvJ{Yz!GP4Dr(DN5_4XD&4 zp&HpZ2%Drb_=ez27Cs@^FJ_eA=HI{mfA(GoNaCX$0qsYnjQd02Q~noupLhe2WV(b1 zcm|-HV14J(y&fKDGK1T|B8~dT+rWZC(iE?!@2`rq*n|_+aLHJ_3$9X?q5MV7Tv&7| zrm@Y8zjB$+NJqE9<|sh<<8s~eZgIHuS3;r0VH&nI0&A?yZr?!?oBJvi>>Lx~&^twDgWhr$a;3{wcX z!JW%H-eY0r#~D1)41k&b@&t1~fT`Zc@O&iG_vH$%tACqg8G>Oh_4Lb~P#A9qlpFH& zP9D}#Ngf~v>8mpaX@P0nJR<5R&)4_yaB99MV zYP%_sDAI$RigzX-O$zZ2(MgR2;7f+)B(uoi+HQp7V=$^H@)}@gzKq!Cs_4rfcI_XJ z|AN7lAF?^&b6hT-zDQ@HHxh}nifN0}(dI5{%WG`L-L@9En9d0-Gqh?oGCxz^PPa

yHlr~Qj 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 GTKgBj+ffbx diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 070cb70..a363877 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip +networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 1b6c787..65dcd68 100755 --- a/gradlew +++ b/gradlew @@ -55,7 +55,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -80,10 +80,10 @@ do esac done -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -APP_NAME="Gradle" +# This is normally unused +# shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' @@ -143,12 +143,16 @@ fi if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -205,6 +209,12 @@ set -- \ org.gradle.wrapper.GradleWrapperMain \ "$@" +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. diff --git a/gradlew.bat b/gradlew.bat index 107acd3..93e3f59 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -14,7 +14,7 @@ @rem limitations under the License. @rem -@if "%DEBUG%" == "" @echo off +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -25,7 +25,8 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -40,7 +41,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute +if %ERRORLEVEL% equ 0 goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -75,13 +76,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 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 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal From 40059f487c86ab9f10b13c7191556102760a2011 Mon Sep 17 00:00:00 2001 From: Anton Kurako Date: Fri, 6 Oct 2023 02:11:01 +0300 Subject: [PATCH 07/18] [2.1.0-SNAPSHOT] CI updated --- .github/workflows/publish-release.yml | 49 +++++++++++++++++++ .github/workflows/publish-snapshot.yml | 44 +++++++++++++++++ .../{gradle.yml => pull-request.yml} | 34 +++++++------ 3 files changed, 112 insertions(+), 15 deletions(-) create mode 100644 .github/workflows/publish-release.yml create mode 100644 .github/workflows/publish-snapshot.yml rename .github/workflows/{gradle.yml => pull-request.yml} (54%) diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml new file mode 100644 index 0000000..3aa7884 --- /dev/null +++ b/.github/workflows/publish-release.yml @@ -0,0 +1,49 @@ +name: CI Master + +on: + release: + types: [ published ] + +jobs: + publish-release: + runs-on: ubuntu-latest + name: Publish Release + + steps: + - uses: actions/checkout@v3 + - name: Set up JDK + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'adopt' + + - name: Build + run: './gradlew classes' + + - name: Test + run: './gradlew test jacocoTestReport' + env: + ETHERSCAN_API_KEY: ${{ secrets.ETHERSCAN_API_KEY_2 }} + + - name: SonarQube + run: './gradlew sonar --info' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + + - name: Publish Release to GitHub Packages + run: './gradlew publishMavenJavaPublicationToGitHubPackagesRepository' + env: + RELEASE_VERSION: ${{ github.ref_name }} + GITHUB_TOKEN: ${{ secrets.OSS_GITHUB_TOKEN }} + ORG_GRADLE_PROJECT_signingKey: ${{ secrets.OSS_SIGNING_KEY }} + ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.OSS_SIGNING_PASSWORD }} + + - name: Publish Release to OSSRH + run: './gradlew publishToSonatype closeAndReleaseSonatypeStagingRepository' + env: + RELEASE_VERSION: ${{ github.ref_name }} + OSS_USERNAME: ${{ secrets.OSS_USERNAME }} + OSS_PASSWORD: ${{ secrets.OSS_PASSWORD }} + ORG_GRADLE_PROJECT_signingKey: ${{ secrets.OSS_SIGNING_KEY }} + ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.OSS_SIGNING_PASSWORD }} diff --git a/.github/workflows/publish-snapshot.yml b/.github/workflows/publish-snapshot.yml new file mode 100644 index 0000000..d181a6c --- /dev/null +++ b/.github/workflows/publish-snapshot.yml @@ -0,0 +1,44 @@ +name: CI Dev + +on: + push: + paths: + - '**/workflows/*.yml' + - '**/java/**' + - '*.java' + - '*.gradle' + - '*.properties' + branches: + - dev + +jobs: + publish-snapshot: + runs-on: ubuntu-latest + name: Publish Snapshot + + steps: + - uses: actions/checkout@v3 + - name: Set up JDK + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'adopt' + + - name: Code Style + run: './gradlew spotlessCheck' + + - name: Build + run: './gradlew classes' + + - name: Test + run: './gradlew test jacocoTestReport' + env: + ETHERSCAN_API_KEY: ${{ secrets.ETHERSCAN_API_KEY_2 }} + + - name: Publish Snapshot + run: './gradlew publish' + env: + OSS_USERNAME: ${{ secrets.OSS_USERNAME }} + OSS_PASSWORD: ${{ secrets.OSS_PASSWORD }} + ORG_GRADLE_PROJECT_signingKey: ${{ secrets.OSS_SIGNING_KEY }} + ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.OSS_SIGNING_PASSWORD }} diff --git a/.github/workflows/gradle.yml b/.github/workflows/pull-request.yml similarity index 54% rename from .github/workflows/gradle.yml rename to .github/workflows/pull-request.yml index 31c42f0..570e503 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/pull-request.yml @@ -1,9 +1,6 @@ -name: Java CI +name: CI Pull Request on: - push: - branches: - - master pull_request: branches: - master @@ -15,37 +12,44 @@ jobs: strategy: matrix: java: [ '11', '17' ] - name: Java ${{ matrix.java }} setup + name: Java ${{ matrix.java }} Pull Request setup steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v3 - name: Set up JDK - uses: actions/setup-java@v1 - + uses: actions/setup-java@v3 with: java-version: ${{ matrix.java }} + distribution: 'adopt' - - name: Build - run: ./gradlew classes + - name: Code Style + run: './gradlew spotlessCheck' - - name: Codestyle - run: ./gradlew spotlessCheck + - name: Build + run: './gradlew classes' - name: Test if: matrix.java == '11' - run: ./gradlew test jacocoTestReport + run: './gradlew test jacocoTestReport' env: ETHERSCAN_API_KEY: ${{ secrets.ETHERSCAN_API_KEY_1 }} - name: Test if: matrix.java == '17' - run: ./gradlew test jacocoTestReport + run: './gradlew test jacocoTestReport' env: ETHERSCAN_API_KEY: ${{ secrets.ETHERSCAN_API_KEY_2 }} - name: SonarQube if: matrix.java == '17' - run: ./gradlew sonarqube + run: './gradlew sonar --info' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + + - name: Test Report + if: matrix.java == '17' + uses: EnricoMi/publish-unit-test-result-action@v2 + with: + files: | + **/test-results/**/*.xml From 519c26ae22fdc79e76aab423000b76be91bef339 Mon Sep 17 00:00:00 2001 From: Anton Kurako Date: Fri, 6 Oct 2023 02:15:47 +0300 Subject: [PATCH 08/18] [2.1.0-SNAPSHOT] Javadoc fixed --- src/main/java/io/goodforgod/api/etherscan/StatisticAPI.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/io/goodforgod/api/etherscan/StatisticAPI.java b/src/main/java/io/goodforgod/api/etherscan/StatisticAPI.java index b6db82e..3f48127 100644 --- a/src/main/java/io/goodforgod/api/etherscan/StatisticAPI.java +++ b/src/main/java/io/goodforgod/api/etherscan/StatisticAPI.java @@ -17,7 +17,7 @@ public interface StatisticAPI { /** * ERC20 token total Supply * EtherScan + * "https://docs.etherscan.io/api-endpoints/tokens#get-erc20-token-totalsupply-by-contractaddress">EtherScan * Returns the current amount of an ERC-20 token in circulation. * * @param contract contract address From b6e9ba5e988fefa805db78f10bdac90cb1b98c6e Mon Sep 17 00:00:00 2001 From: Anton Kurako Date: Fri, 6 Oct 2023 02:21:15 +0300 Subject: [PATCH 09/18] [2.1.0-SNAPSHOT] Method renamed --- .../java/io/goodforgod/api/etherscan/EthScanAPIBuilder.java | 2 +- src/main/java/io/goodforgod/api/etherscan/EtherScanAPI.java | 2 +- src/test/java/io/goodforgod/api/etherscan/ApiRunner.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/io/goodforgod/api/etherscan/EthScanAPIBuilder.java b/src/main/java/io/goodforgod/api/etherscan/EthScanAPIBuilder.java index 70d9a01..2b70711 100644 --- a/src/main/java/io/goodforgod/api/etherscan/EthScanAPIBuilder.java +++ b/src/main/java/io/goodforgod/api/etherscan/EthScanAPIBuilder.java @@ -89,7 +89,7 @@ public EtherScanAPI.Builder withConverter(@NotNull Supplier converter } @NotNull - public EtherScanAPI.Builder withRetryOnLimitReach(int maxRetryCount) { + public EtherScanAPI.Builder withRetryOnRateLimit(int maxRetryCount) { if (maxRetryCount < 0 || maxRetryCount > 20) { throw new IllegalStateException("maxRetryCount value must be in range from 0 to 20, but was: " + maxRetryCount); } diff --git a/src/main/java/io/goodforgod/api/etherscan/EtherScanAPI.java b/src/main/java/io/goodforgod/api/etherscan/EtherScanAPI.java index 4e6bc57..bae1902 100644 --- a/src/main/java/io/goodforgod/api/etherscan/EtherScanAPI.java +++ b/src/main/java/io/goodforgod/api/etherscan/EtherScanAPI.java @@ -71,7 +71,7 @@ interface Builder { * @return self */ @NotNull - EtherScanAPI.Builder withRetryOnLimitReach(@Range(from = 0, to = 20) int maxRetryCount); + EtherScanAPI.Builder withRetryOnRateLimit(@Range(from = 0, to = 20) int maxRetryCount); @NotNull EtherScanAPI build(); diff --git a/src/test/java/io/goodforgod/api/etherscan/ApiRunner.java b/src/test/java/io/goodforgod/api/etherscan/ApiRunner.java index 72aeeff..bc4f334 100644 --- a/src/test/java/io/goodforgod/api/etherscan/ApiRunner.java +++ b/src/test/java/io/goodforgod/api/etherscan/ApiRunner.java @@ -29,7 +29,7 @@ public class ApiRunner extends Assertions { .withApiKey(ApiRunner.API_KEY) .withNetwork(EthNetworks.MAINNET) .withQueue(queueManager) - .withRetryOnLimitReach(5) + .withRetryOnRateLimit(5) .build(); } From c855695fdc61fb978bf0c86669ee476c051915b9 Mon Sep 17 00:00:00 2001 From: Anton Kurako Date: Fri, 6 Oct 2023 02:21:55 +0300 Subject: [PATCH 10/18] [2.1.0-SNAPSHOT] Method renamed --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 7dcf4c7..7e28207 100644 --- a/build.gradle +++ b/build.gradle @@ -40,7 +40,7 @@ test { reports { html.required = false - junitXml.required = false + junitXml.required = true } environment([ From 3a252b4e32ebc5e4408ca25c903fb3776e5458ac Mon Sep 17 00:00:00 2001 From: Anton Kurako Date: Fri, 6 Oct 2023 02:22:51 +0300 Subject: [PATCH 11/18] [2.1.0-SNAPSHOT] CI updated --- .github/workflows/pull-request.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 570e503..0b49f50 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -40,16 +40,16 @@ jobs: env: ETHERSCAN_API_KEY: ${{ secrets.ETHERSCAN_API_KEY_2 }} - - name: SonarQube - if: matrix.java == '17' - run: './gradlew sonar --info' - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - - name: Test Report if: matrix.java == '17' uses: EnricoMi/publish-unit-test-result-action@v2 with: files: | **/test-results/**/*.xml + + - name: SonarQube + if: matrix.java == '17' + run: './gradlew sonar --info' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} From 3de7b242b9f65daaaedbec0ef67ea83fd45c0706 Mon Sep 17 00:00:00 2001 From: Anton Kurako Date: Mon, 15 Jan 2024 12:39:58 +0300 Subject: [PATCH 12/18] [2.1.0-SNAPSHOT] README.md updated --- README.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index c086a6b..0d06c99 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ API support all Ethereum [default networks](https://docs.etherscan.io/getting-st - [Sepolia](https://api-sepolia.etherscan.io/) ```java -EtherScanAPI api = EtherScanAPI.build(); +EtherScanAPI api = EtherScanAPI.builder().build(); EtherScanAPI apiGoerli = EtherScanAPI.builder().withNetwork(EthNetworks.GORLI).build(); EtherScanAPI apiSepolia = EtherScanAPI.builder().withNetwork(EthNetworks.SEPOLIA).build(); ``` @@ -97,7 +97,7 @@ Below are examples for each API category. **Get Ether Balance for a single Address** ```java -EtherScanAPI api = EtherScanAPI.build(); +EtherScanAPI api = EtherScanAPI.builder().build(); Balance balance = api.account().balance("0x8d4426f94e42f721C7116E81d6688cd935cB3b4F"); ``` @@ -105,14 +105,14 @@ Balance balance = api.account().balance("0x8d4426f94e42f721C7116E81d6688cd935cB3 **Get uncles block for block height** ```java -EtherScanAPI api = EtherScanAPI.build(); +EtherScanAPI api = EtherScanAPI.builder().build(); Optional uncles = api.block().uncles(200000); ``` ### Contract API **Request contract ABI from [verified codes](https://etherscan.io/contractsVerified)** ```java -EtherScanAPI api = EtherScanAPI.build(); +EtherScanAPI api = EtherScanAPI.builder().build(); Abi abi = api.contract().contractAbi("0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413"); ``` @@ -120,7 +120,7 @@ Abi abi = api.contract().contractAbi("0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413 **Get event logs for single topic** ```java -EtherScanAPI api = EtherScanAPI.build(); +EtherScanAPI api = EtherScanAPI.builder().build(); LogQuery query = LogQuery.builder("0x33990122638b9132ca29c723bdf037f1a891a70c") .withTopic("0xf63780e752c6a54a94fc52715dbc5518a3b4c3c2833d301a204226548a2a8545") .build(); @@ -129,7 +129,7 @@ List logs = api.logs().logs(query); **Get event logs for 3 topics with respectful operations** ```java -EtherScanAPI api = EtherScanAPI.build(); +EtherScanAPI api = EtherScanAPI.builder().build(); LogQuery query = LogQuery.builder("0x33990122638b9132ca29c723bdf037f1a891a70c") .withBlockFrom(379224) .withBlockTo(400000) @@ -148,13 +148,13 @@ List logs = api.logs().logs(query); **Get tx details with proxy endpoint** ```java -EtherScanAPI api = EtherScanAPI.build(); +EtherScanAPI api = EtherScanAPI.builder().build(); Optional tx = api.proxy().tx("0x1e2910a262b1008d0616a0beb24c1a491d78771baa54a33e66065e03b1f46bc1"); ``` **Get block info with proxy endpoint** ```java -EtherScanAPI api = EtherScanAPI.build(); +EtherScanAPI api = EtherScanAPI.builder().build(); Optional block = api.proxy().block(15215); ``` @@ -162,7 +162,7 @@ Optional block = api.proxy().block(15215); **Statistic about last price** ```java -EtherScanAPI api = EtherScanAPI.build(); +EtherScanAPI api = EtherScanAPI.builder().build(); Price price = api.stats().priceLast(); ``` @@ -170,7 +170,7 @@ Price price = api.stats().priceLast(); **Request receipt status for tx** ```java -EtherScanAPI api = EtherScanAPI.build(); +EtherScanAPI api = EtherScanAPI.builder().build(); Optional status = api.txs().receiptStatus("0x513c1ba0bebf66436b5fed86ab668452b7805593c05073eb2d51d3a52f480a76"); ``` From bb87f9014587b7e83a5b6c7ffa898d840b117587 Mon Sep 17 00:00:00 2001 From: Anton Kurako Date: Sun, 7 Sep 2025 15:09:54 +0300 Subject: [PATCH 13/18] [3.0.0-SNAPSHOT] Added JdkEthHttpClient All contracts open or protected Updated dependencies Updated Gradle --- README.md | 14 +- build.gradle | 16 +- gradle.properties | 2 +- gradle/wrapper/gradle-wrapper.jar | Bin 61574 -> 43764 bytes gradle/wrapper/gradle-wrapper.properties | 3 +- gradlew | 37 +++-- gradlew.bat | 26 +-- .../api/etherscan/AccountAPIProvider.java | 50 ++---- .../api/etherscan/BasicProvider.java | 81 +++++++--- .../api/etherscan/BlockAPIProvider.java | 14 +- .../api/etherscan/ContractAPIProvider.java | 16 +- .../api/etherscan/EthScanAPIBuilder.java | 6 +- .../api/etherscan/EtherScanAPIProvider.java | 14 +- .../api/etherscan/GasTrackerAPIProvider.java | 16 +- .../api/etherscan/LogsAPIProvider.java | 14 +- .../api/etherscan/ProxyAPIProvider.java | 38 ++--- .../api/etherscan/StatisticAPIProvider.java | 20 +-- .../api/etherscan/TransactionAPIProvider.java | 16 +- .../EtherScanConnectionTimeoutException.java | 12 ++ .../error/EtherScanTimeoutException.java | 12 -- .../api/etherscan/http/EthHttpClient.java | 4 +- .../api/etherscan/http/EthResponse.java | 30 ++++ .../api/etherscan/http/SimpleEthResponse.java | 64 ++++++++ .../etherscan/http/impl/JdkEthHttpClient.java | 148 ++++++++++++++++++ .../etherscan/http/impl/UrlEthHttpClient.java | 58 ++++--- .../manager/impl/FakeRequestQueueManager.java | 2 +- .../impl/SemaphoreRequestQueueManager.java | 2 +- .../goodforgod/api/etherscan/model/Abi.java | 2 +- .../api/etherscan/model/BaseTx.java | 2 +- .../api/etherscan/model/BlockTx.java | 2 +- .../api/etherscan/model/BlockUncle.java | 4 +- .../api/etherscan/model/ContractCreation.java | 2 +- .../api/etherscan/model/EthSupply.java | 2 +- .../api/etherscan/model/GasOracle.java | 2 +- .../goodforgod/api/etherscan/model/Log.java | 2 +- .../goodforgod/api/etherscan/model/Price.java | 2 +- .../api/etherscan/model/Status.java | 2 +- .../io/goodforgod/api/etherscan/model/Tx.java | 2 +- .../api/etherscan/model/TxErc1155.java | 2 +- .../api/etherscan/model/TxErc20.java | 2 +- .../api/etherscan/model/TxErc721.java | 2 +- .../api/etherscan/model/TxInternal.java | 2 +- .../api/etherscan/model/proxy/BlockProxy.java | 2 +- .../etherscan/model/proxy/ReceiptProxy.java | 2 +- .../api/etherscan/model/proxy/TxProxy.java | 2 +- .../model/proxy/utility/BaseProxyTO.java | 2 +- .../model/query/LogQueryBuilderImpl.java | 2 +- .../etherscan/model/query/LogQueryImpl.java | 2 +- .../etherscan/model/query/LogQueryParams.java | 2 +- .../etherscan/model/query/LogTopicQuadro.java | 2 +- .../etherscan/model/query/LogTopicSingle.java | 2 +- .../etherscan/model/query/LogTopicTriple.java | 2 +- .../etherscan/model/query/LogTopicTuple.java | 2 +- .../model/response/StringResponseTO.java | 2 +- .../api/etherscan/util/BasicUtils.java | 2 +- 55 files changed, 536 insertions(+), 237 deletions(-) create mode 100644 src/main/java/io/goodforgod/api/etherscan/error/EtherScanConnectionTimeoutException.java delete mode 100644 src/main/java/io/goodforgod/api/etherscan/error/EtherScanTimeoutException.java create mode 100644 src/main/java/io/goodforgod/api/etherscan/http/EthResponse.java create mode 100644 src/main/java/io/goodforgod/api/etherscan/http/SimpleEthResponse.java create mode 100644 src/main/java/io/goodforgod/api/etherscan/http/impl/JdkEthHttpClient.java diff --git a/README.md b/README.md index 0d06c99..160f8e4 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Library supports EtherScan *API* for all available *Ethereum Networks* for *ethe **Gradle** ```groovy -implementation "com.github.goodforgod:java-etherscan-api:2.1.0" +implementation "com.github.goodforgod:java-etherscan-api:3.0.0" ``` **Maven** @@ -23,7 +23,7 @@ implementation "com.github.goodforgod:java-etherscan-api:2.1.0" com.github.goodforgod java-etherscan-api - 2.1.0 + 3.0.0 ``` @@ -76,6 +76,14 @@ EtherScanAPI api = EtherScanAPI.builder() .build(); ``` +Also you can use Java 11+ HttpClient: +```java +Supplier ethHttpClientSupplier = () -> new JdkEthHttpClient(); +EtherScanAPI api = EtherScanAPI.builder() + .withHttpClient(supplier) + .build(); +``` + ## API Examples You can read about all API methods on [Etherscan](https://docs.etherscan.io/api-endpoints/accounts) @@ -149,7 +157,7 @@ List logs = api.logs().logs(query); **Get tx details with proxy endpoint** ```java EtherScanAPI api = EtherScanAPI.builder().build(); -Optional tx = api.proxy().tx("0x1e2910a262b1008d0616a0beb24c1a491d78771baa54a33e66065e03b1f46bc1"); +Optional tx = api.proxy().tx("0x1e2910a263.0.08d0616a0beb24c1a491d78771baa54a33e66065e03b1f46bc1"); ``` **Get block info with proxy endpoint** diff --git a/build.gradle b/build.gradle index 7e28207..f946f1d 100644 --- a/build.gradle +++ b/build.gradle @@ -3,9 +3,9 @@ plugins { id "java-library" id "maven-publish" - id "org.sonarqube" version "4.3.0.3225" + id "org.sonarqube" version "6.3.1.5724" id "com.diffplug.spotless" version "6.19.0" - id "io.github.gradle-nexus.publish-plugin" version "1.3.0" + id "io.github.gradle-nexus.publish-plugin" version "2.0.0" } repositories { @@ -17,16 +17,16 @@ group = groupId var ver = System.getenv().getOrDefault("RELEASE_VERSION", artifactVersion) version = ver.startsWith("v") ? ver.substring(1) : ver -sourceCompatibility = JavaVersion.VERSION_1_8 -targetCompatibility = JavaVersion.VERSION_1_8 +sourceCompatibility = JavaVersion.VERSION_11 +targetCompatibility = JavaVersion.VERSION_11 dependencies { - compileOnly "org.jetbrains:annotations:23.0.0" + compileOnly "org.jetbrains:annotations:24.0.1" implementation "io.goodforgod:gson-configuration:2.0.0" - testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:5.9.3" - testImplementation "org.junit.jupiter:junit-jupiter-api:5.9.3" - testImplementation "org.junit.jupiter:junit-jupiter-params:5.9.3" + testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:5.11.4" + testImplementation "org.junit.jupiter:junit-jupiter-api:5.11.4" + testImplementation "org.junit.jupiter:junit-jupiter-params:5.11.4" } test { diff --git a/gradle.properties b/gradle.properties index ee5fd3b..7b97567 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,6 @@ groupId=com.github.goodforgod artifactId=java-etherscan-api -artifactVersion=2.1.0-SNAPSHOT +artifactVersion=3.0.0-SNAPSHOT ##### GRADLE ##### diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 943f0cbfa754578e88a3dae77fce6e3dea56edbf..1b33c55baabb587c669f562ae36f953de2481846 100644 GIT binary patch literal 43764 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 ScNgau(U?ep-K_E5zy1%ZQTdPn diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a363877..d4081da 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 65dcd68..23d15a9 100755 --- a/gradlew +++ b/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -83,10 +85,8 @@ done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -114,7 +114,7 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar +CLASSPATH="\\\"\\\"" # Determine the Java command to use to start the JVM. @@ -133,10 +133,13 @@ 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. + if ! command -v java >/dev/null 2>&1 + then + 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 fi # Increase the maximum file descriptors if we can. @@ -144,7 +147,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac @@ -152,7 +155,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -197,16 +200,20 @@ if "$cygwin" || "$msys" ; then done fi -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. diff --git a/gradlew.bat b/gradlew.bat index 93e3f59..db3a6ac 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @@ -43,11 +45,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -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. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -57,22 +59,22 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -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. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail :execute @rem Setup the command line -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar +set CLASSPATH= @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell diff --git a/src/main/java/io/goodforgod/api/etherscan/AccountAPIProvider.java b/src/main/java/io/goodforgod/api/etherscan/AccountAPIProvider.java index 750d525..aceb9a2 100644 --- a/src/main/java/io/goodforgod/api/etherscan/AccountAPIProvider.java +++ b/src/main/java/io/goodforgod/api/etherscan/AccountAPIProvider.java @@ -21,9 +21,9 @@ * @author GoodforGod * @since 28.10.2018 */ -final class AccountAPIProvider extends BasicProvider implements AccountAPI { +public class AccountAPIProvider extends BasicProvider implements AccountAPI { - private static final int OFFSET_MAX = 10000; + private static final int OFFSET_MAX = 9999; private static final String ACT_BALANCE_ACTION = ACT_PREFIX + "balance"; private static final String ACT_TOKEN_BALANCE_PARAM = ACT_PREFIX + "tokenbalance"; @@ -47,11 +47,11 @@ final class AccountAPIProvider extends BasicProvider implements AccountAPI { private static final String OFFSET_PARAM = "&offset="; private static final String PAGE_PARAM = "&page="; - AccountAPIProvider(RequestQueueManager requestQueueManager, - String baseUrl, - EthHttpClient executor, - Converter converter, - int retryCount) { + public AccountAPIProvider(RequestQueueManager requestQueueManager, + String baseUrl, + EthHttpClient executor, + Converter converter, + int retryCount) { super(requestQueueManager, "account", baseUrl, executor, converter, retryCount); } @@ -61,7 +61,7 @@ public Balance balance(@NotNull String address) throws EtherScanException { BasicUtils.validateAddress(address); final String urlParams = ACT_BALANCE_ACTION + TAG_LATEST_PARAM + ADDRESS_PARAM + address; - final StringResponseTO response = getRequest(urlParams, StringResponseTO.class); + final StringResponseTO response = getResponse(urlParams, StringResponseTO.class); if (response.getStatus() != 1) throw new EtherScanResponseException(response); @@ -75,7 +75,7 @@ public TokenBalance balance(@NotNull String address, @NotNull String contract) t BasicUtils.validateAddress(contract); final String urlParams = ACT_TOKEN_BALANCE_PARAM + ADDRESS_PARAM + address + CONTRACT_PARAM + contract; - final StringResponseTO response = getRequest(urlParams, StringResponseTO.class); + final StringResponseTO response = getResponse(urlParams, StringResponseTO.class); if (response.getStatus() != 1) throw new EtherScanResponseException(response); @@ -98,7 +98,7 @@ public List balances(@NotNull List addresses) throws EtherScanE for (final List batch : addressesAsBatches) { final String urlParams = ACT_BALANCE_MULTI_ACTION + TAG_LATEST_PARAM + ADDRESS_PARAM + BasicUtils.toAddressParam(batch); - final BalanceResponseTO response = getRequest(urlParams, BalanceResponseTO.class); + final BalanceResponseTO response = getResponse(urlParams, BalanceResponseTO.class); if (response.getStatus() != 1) { throw new EtherScanResponseException(response); } @@ -139,34 +139,6 @@ public List txs(@NotNull String address, long startBlock, long endBlock) thr return getRequestUsingOffset(urlParams, TxResponseTO.class); } - /** - * Generic search for txs using offset api param To avoid 10k limit per response - * - * @param urlParams Url params for #getRequest() - * @param tClass responseListTO class - * @param responseTO list T type - * @param responseListTO type - * @return List of T values - */ - private > List getRequestUsingOffset(final String urlParams, Class tClass) - throws EtherScanException { - final List result = new ArrayList<>(); - int page = 1; - while (true) { - final String formattedUrl = String.format(urlParams, page++); - final R response = getRequest(formattedUrl, tClass); - BasicUtils.validateTxResponse(response); - if (BasicUtils.isEmpty(response.getResult())) - break; - - result.addAll(response.getResult()); - if (response.getResult().size() < OFFSET_MAX) - break; - } - - return result; - } - @NotNull @Override public List txsInternal(@NotNull String address) throws EtherScanException { @@ -200,7 +172,7 @@ public List txsInternalByHash(@NotNull String txhash) throws EtherSc BasicUtils.validateTxHash(txhash); final String urlParams = ACT_TX_INTERNAL_ACTION + TXHASH_PARAM + txhash; - final TxInternalResponseTO response = getRequest(urlParams, TxInternalResponseTO.class); + final TxInternalResponseTO response = getResponse(urlParams, TxInternalResponseTO.class); BasicUtils.validateTxResponse(response); return BasicUtils.isEmpty(response.getResult()) diff --git a/src/main/java/io/goodforgod/api/etherscan/BasicProvider.java b/src/main/java/io/goodforgod/api/etherscan/BasicProvider.java index 41abd16..2abb2e0 100644 --- a/src/main/java/io/goodforgod/api/etherscan/BasicProvider.java +++ b/src/main/java/io/goodforgod/api/etherscan/BasicProvider.java @@ -1,13 +1,21 @@ package io.goodforgod.api.etherscan; +import io.goodforgod.api.etherscan.error.EtherScanException; import io.goodforgod.api.etherscan.error.EtherScanParseException; import io.goodforgod.api.etherscan.error.EtherScanRateLimitException; import io.goodforgod.api.etherscan.error.EtherScanResponseException; import io.goodforgod.api.etherscan.http.EthHttpClient; +import io.goodforgod.api.etherscan.http.EthResponse; import io.goodforgod.api.etherscan.manager.RequestQueueManager; +import io.goodforgod.api.etherscan.model.response.BaseListResponseTO; import io.goodforgod.api.etherscan.model.response.StringResponseTO; import java.net.URI; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +import io.goodforgod.api.etherscan.util.BasicUtils; +import org.jetbrains.annotations.ApiStatus.Internal; /** * Base provider for API Implementations @@ -16,10 +24,13 @@ * @see EtherScanAPIProvider * @since 28.10.2018 */ -abstract class BasicProvider { +@Internal +public abstract class BasicProvider { private static final String MAX_RATE_LIMIT_REACHED = "Max rate limit reached"; + private static final int OFFSET_MAX = 9999; + static final int MAX_END_BLOCK = Integer.MAX_VALUE; static final int MIN_START_BLOCK = 0; @@ -32,12 +43,12 @@ abstract class BasicProvider { private final Converter converter; private final int retryCountLimit; - BasicProvider(RequestQueueManager requestQueueManager, - String module, - String baseUrl, - EthHttpClient ethHttpClient, - Converter converter, - int retryCountLimit) { + public BasicProvider(RequestQueueManager requestQueueManager, + String module, + String baseUrl, + EthHttpClient ethHttpClient, + Converter converter, + int retryCountLimit) { this.queue = requestQueueManager; this.module = "&module=" + module; this.baseUrl = baseUrl; @@ -46,7 +57,7 @@ abstract class BasicProvider { this.retryCountLimit = retryCountLimit; } - private T convert(byte[] json, Class tClass) { + protected T convert(byte[] json, Class tClass) { try { final T t = converter.fromJson(json, tClass); if (t instanceof StringResponseTO && ((StringResponseTO) t).getResult().startsWith(MAX_RATE_LIMIT_REACHED)) { @@ -69,25 +80,58 @@ private T convert(byte[] json, Class tClass) { } } - private byte[] getRequest(String urlParameters) { + protected int getMaximumOffset() { + return OFFSET_MAX; + } + + /** + * Generic search for txs using offset api param To avoid 10k limit per response + * + * @param urlParams Url params for #getRequest() + * @param tClass responseListTO class + * @param responseTO list T type + * @param responseListTO type + * @return List of T values + */ + protected > List getRequestUsingOffset(final String urlParams, Class tClass) + throws EtherScanException { + final List result = new ArrayList<>(); + int page = 1; + while (true) { + final String formattedUrl = String.format(urlParams, page++); + final R response = getResponse(formattedUrl, tClass); + BasicUtils.validateTxResponse(response); + if (BasicUtils.isEmpty(response.getResult())) + break; + + result.addAll(response.getResult()); + if (response.getResult().size() < getMaximumOffset()) + break; + } + + return result; + } + + protected EthResponse getResponse(String urlParameters) { queue.takeTurn(); final URI uri = URI.create(baseUrl + module + urlParameters); return executor.get(uri); } - private byte[] postRequest(String urlParameters, String dataToPost) { + protected EthResponse postRequest(String urlParameters, String dataToPost) { queue.takeTurn(); final URI uri = URI.create(baseUrl + module + urlParameters); return executor.post(uri, dataToPost.getBytes(StandardCharsets.UTF_8)); } - T getRequest(String urlParameters, Class tClass) { - return getRequest(urlParameters, tClass, 0); + protected T getResponse(String urlParameters, Class tClass) { + return getResponse(urlParameters, tClass, 0); } - private T getRequest(String urlParameters, Class tClass, int retryCount) { + protected T getResponse(String urlParameters, Class tClass, int retryCount) { try { - return convert(getRequest(urlParameters), tClass); + EthResponse response = getResponse(urlParameters); + return convert(response.body(), tClass); } catch (Exception e) { if (retryCount < retryCountLimit) { try { @@ -96,20 +140,21 @@ private T getRequest(String urlParameters, Class tClass, int retryCount) throw new IllegalStateException(ex); } - return getRequest(urlParameters, tClass, retryCount + 1); + return getResponse(urlParameters, tClass, retryCount + 1); } else { throw e; } } } - T postRequest(String urlParameters, String dataToPost, Class tClass) { + protected T postRequest(String urlParameters, String dataToPost, Class tClass) { return postRequest(urlParameters, dataToPost, tClass, 0); } - private T postRequest(String urlParameters, String dataToPost, Class tClass, int retryCount) { + protected T postRequest(String urlParameters, String dataToPost, Class tClass, int retryCount) { try { - return convert(postRequest(urlParameters, dataToPost), tClass); + EthResponse response = postRequest(urlParameters, dataToPost); + return convert(response.body(), tClass); } catch (EtherScanRateLimitException e) { if (retryCount < retryCountLimit) { try { diff --git a/src/main/java/io/goodforgod/api/etherscan/BlockAPIProvider.java b/src/main/java/io/goodforgod/api/etherscan/BlockAPIProvider.java index b3604a7..7228943 100644 --- a/src/main/java/io/goodforgod/api/etherscan/BlockAPIProvider.java +++ b/src/main/java/io/goodforgod/api/etherscan/BlockAPIProvider.java @@ -17,17 +17,17 @@ * @see BlockAPI * @since 28.10.2018 */ -final class BlockAPIProvider extends BasicProvider implements BlockAPI { +public class BlockAPIProvider extends BasicProvider implements BlockAPI { private static final String ACT_BLOCK_PARAM = ACT_PREFIX + "getblockreward"; private static final String BLOCKNO_PARAM = "&blockno="; - BlockAPIProvider(RequestQueueManager requestQueueManager, - String baseUrl, - EthHttpClient executor, - Converter converter, - int retryCount) { + public BlockAPIProvider(RequestQueueManager requestQueueManager, + String baseUrl, + EthHttpClient executor, + Converter converter, + int retryCount) { super(requestQueueManager, "block", baseUrl, executor, converter, retryCount); } @@ -36,7 +36,7 @@ final class BlockAPIProvider extends BasicProvider implements BlockAPI { public Optional uncles(long blockNumber) throws EtherScanException { final String urlParam = ACT_BLOCK_PARAM + BLOCKNO_PARAM + blockNumber; try { - final UncleBlockResponseTO responseTO = getRequest(urlParam, UncleBlockResponseTO.class); + final UncleBlockResponseTO responseTO = getResponse(urlParam, UncleBlockResponseTO.class); if (responseTO.getMessage().startsWith("NOTOK")) { return Optional.empty(); } diff --git a/src/main/java/io/goodforgod/api/etherscan/ContractAPIProvider.java b/src/main/java/io/goodforgod/api/etherscan/ContractAPIProvider.java index 898a7b7..0719569 100644 --- a/src/main/java/io/goodforgod/api/etherscan/ContractAPIProvider.java +++ b/src/main/java/io/goodforgod/api/etherscan/ContractAPIProvider.java @@ -20,7 +20,7 @@ * @author GoodforGod * @since 28.10.2018 */ -final class ContractAPIProvider extends BasicProvider implements ContractAPI { +public class ContractAPIProvider extends BasicProvider implements ContractAPI { private static final String ACT_ABI_PARAM = ACT_PREFIX + "getabi"; @@ -32,11 +32,11 @@ final class ContractAPIProvider extends BasicProvider implements ContractAPI { private static final String ACT_CONTRACT_ADDRESSES_PARAM = "&contractaddresses="; - ContractAPIProvider(RequestQueueManager requestQueueManager, - String baseUrl, - EthHttpClient executor, - Converter converter, - int retryCount) { + public ContractAPIProvider(RequestQueueManager requestQueueManager, + String baseUrl, + EthHttpClient executor, + Converter converter, + int retryCount) { super(requestQueueManager, "contract", baseUrl, executor, converter, retryCount); } @@ -46,7 +46,7 @@ public Abi contractAbi(@NotNull String address) throws EtherScanException { BasicUtils.validateAddress(address); final String urlParam = ACT_ABI_PARAM + ADDRESS_PARAM + address; - final StringResponseTO response = getRequest(urlParam, StringResponseTO.class); + final StringResponseTO response = getResponse(urlParam, StringResponseTO.class); if (response.getStatus() != 1 && response.getMessage().startsWith("NOTOK")) { throw new EtherScanResponseException(response); } @@ -62,7 +62,7 @@ public List contractCreation(@NotNull List contractAdd BasicUtils.validateAddresses(contractAddresses); final String urlParam = ACT_CONTRACT_CREATION + ACT_CONTRACT_ADDRESSES_PARAM + BasicUtils.toAddressParam(contractAddresses); - final ContractCreationResponseTO response = getRequest(urlParam, ContractCreationResponseTO.class); + final ContractCreationResponseTO response = getResponse(urlParam, ContractCreationResponseTO.class); if (response.getStatus() != 1 && response.getMessage().startsWith("NOTOK")) { throw new EtherScanResponseException(response); } diff --git a/src/main/java/io/goodforgod/api/etherscan/EthScanAPIBuilder.java b/src/main/java/io/goodforgod/api/etherscan/EthScanAPIBuilder.java index 2b70711..12bb9d3 100644 --- a/src/main/java/io/goodforgod/api/etherscan/EthScanAPIBuilder.java +++ b/src/main/java/io/goodforgod/api/etherscan/EthScanAPIBuilder.java @@ -4,7 +4,7 @@ import io.goodforgod.api.etherscan.error.EtherScanKeyException; import io.goodforgod.api.etherscan.error.EtherScanParseException; import io.goodforgod.api.etherscan.http.EthHttpClient; -import io.goodforgod.api.etherscan.http.impl.UrlEthHttpClient; +import io.goodforgod.api.etherscan.http.impl.JdkEthHttpClient; import io.goodforgod.api.etherscan.manager.RequestQueueManager; import io.goodforgod.api.etherscan.util.BasicUtils; import io.goodforgod.gson.configuration.GsonConfiguration; @@ -19,9 +19,9 @@ * @author Anton Kurako (GoodforGod) * @since 11.05.2023 */ -final class EthScanAPIBuilder implements EtherScanAPI.Builder { +public class EthScanAPIBuilder implements EtherScanAPI.Builder { - private static final Supplier DEFAULT_SUPPLIER = UrlEthHttpClient::new; + private static final Supplier DEFAULT_SUPPLIER = JdkEthHttpClient::new; private static final String DEFAULT_KEY = "YourApiKeyToken"; private final Gson gson = new GsonConfiguration().builder().create(); diff --git a/src/main/java/io/goodforgod/api/etherscan/EtherScanAPIProvider.java b/src/main/java/io/goodforgod/api/etherscan/EtherScanAPIProvider.java index ab6e863..daf1e4a 100644 --- a/src/main/java/io/goodforgod/api/etherscan/EtherScanAPIProvider.java +++ b/src/main/java/io/goodforgod/api/etherscan/EtherScanAPIProvider.java @@ -10,7 +10,7 @@ * @author GoodforGod * @since 28.10.2018 */ -final class EtherScanAPIProvider implements EtherScanAPI { +public class EtherScanAPIProvider implements EtherScanAPI { private final RequestQueueManager requestQueueManager; private final AccountAPI account; @@ -22,12 +22,12 @@ final class EtherScanAPIProvider implements EtherScanAPI { private final TransactionAPI txs; private final GasTrackerAPI gasTracker; - EtherScanAPIProvider(String apiKey, - EthNetwork network, - RequestQueueManager queue, - EthHttpClient ethHttpClient, - Converter converter, - int retryCount) { + public EtherScanAPIProvider(String apiKey, + EthNetwork network, + RequestQueueManager queue, + EthHttpClient ethHttpClient, + Converter converter, + int retryCount) { // EtherScan 1request\5sec limit support by queue manager final String baseUrl = network.domain() + "?apikey=" + apiKey; diff --git a/src/main/java/io/goodforgod/api/etherscan/GasTrackerAPIProvider.java b/src/main/java/io/goodforgod/api/etherscan/GasTrackerAPIProvider.java index ed717a9..e7ecb7c 100644 --- a/src/main/java/io/goodforgod/api/etherscan/GasTrackerAPIProvider.java +++ b/src/main/java/io/goodforgod/api/etherscan/GasTrackerAPIProvider.java @@ -18,25 +18,25 @@ * @author Abhay Gupta * @since 14.11.2022 */ -final class GasTrackerAPIProvider extends BasicProvider implements GasTrackerAPI { +public class GasTrackerAPIProvider extends BasicProvider implements GasTrackerAPI { private static final String ACT_GAS_ORACLE_PARAM = ACT_PREFIX + "gasoracle"; private static final String ACT_GAS_ESTIMATE_PARAM = ACT_PREFIX + "gasestimate"; private static final String GASPRICE_PARAM = "&gasprice="; - GasTrackerAPIProvider(RequestQueueManager queue, - String baseUrl, - EthHttpClient ethHttpClient, - Converter converter, - int retryCount) { + public GasTrackerAPIProvider(RequestQueueManager queue, + String baseUrl, + EthHttpClient ethHttpClient, + Converter converter, + int retryCount) { super(queue, "gastracker", baseUrl, ethHttpClient, converter, retryCount); } @Override public @NotNull Duration estimate(@NotNull Wei wei) throws EtherScanException { final String urlParams = ACT_GAS_ESTIMATE_PARAM + GASPRICE_PARAM + wei.asWei().toString(); - final GasEstimateResponseTO response = getRequest(urlParams, GasEstimateResponseTO.class); + final GasEstimateResponseTO response = getResponse(urlParams, GasEstimateResponseTO.class); if (response.getStatus() != 1) throw new EtherScanResponseException(response); @@ -46,7 +46,7 @@ final class GasTrackerAPIProvider extends BasicProvider implements GasTrackerAPI @NotNull @Override public GasOracle oracle() throws EtherScanException { - final GasOracleResponseTO response = getRequest(ACT_GAS_ORACLE_PARAM, GasOracleResponseTO.class); + final GasOracleResponseTO response = getResponse(ACT_GAS_ORACLE_PARAM, GasOracleResponseTO.class); if (response.getStatus() != 1) throw new EtherScanResponseException(response); diff --git a/src/main/java/io/goodforgod/api/etherscan/LogsAPIProvider.java b/src/main/java/io/goodforgod/api/etherscan/LogsAPIProvider.java index 237cafd..1002dc8 100644 --- a/src/main/java/io/goodforgod/api/etherscan/LogsAPIProvider.java +++ b/src/main/java/io/goodforgod/api/etherscan/LogsAPIProvider.java @@ -18,15 +18,15 @@ * @author GoodforGod * @since 28.10.2018 */ -final class LogsAPIProvider extends BasicProvider implements LogsAPI { +public class LogsAPIProvider extends BasicProvider implements LogsAPI { private static final String ACT_LOGS_PARAM = ACT_PREFIX + "getLogs"; - LogsAPIProvider(RequestQueueManager queue, - String baseUrl, - EthHttpClient executor, - Converter converter, - int retryCount) { + public LogsAPIProvider(RequestQueueManager queue, + String baseUrl, + EthHttpClient executor, + Converter converter, + int retryCount) { super(queue, "logs", baseUrl, executor, converter, retryCount); } @@ -34,7 +34,7 @@ final class LogsAPIProvider extends BasicProvider implements LogsAPI { @Override public List logs(@NotNull LogQuery query) throws EtherScanException { final String urlParams = ACT_LOGS_PARAM + query.params(); - final LogResponseTO response = getRequest(urlParams, LogResponseTO.class); + final LogResponseTO response = getResponse(urlParams, LogResponseTO.class); BasicUtils.validateTxResponse(response); return (BasicUtils.isEmpty(response.getResult())) diff --git a/src/main/java/io/goodforgod/api/etherscan/ProxyAPIProvider.java b/src/main/java/io/goodforgod/api/etherscan/ProxyAPIProvider.java index 428b48f..e35fd08 100644 --- a/src/main/java/io/goodforgod/api/etherscan/ProxyAPIProvider.java +++ b/src/main/java/io/goodforgod/api/etherscan/ProxyAPIProvider.java @@ -26,7 +26,7 @@ * @author GoodforGod * @since 28.10.2018 */ -final class ProxyAPIProvider extends BasicProvider implements ProxyAPI { +public class ProxyAPIProvider extends BasicProvider implements ProxyAPI { private static final String ACT_BLOCKNO_PARAM = ACT_PREFIX + "eth_blockNumber"; private static final String ACT_BY_BLOCKNO_PARAM = ACT_PREFIX + "eth_getBlockByNumber"; @@ -57,17 +57,17 @@ final class ProxyAPIProvider extends BasicProvider implements ProxyAPI { private static final Pattern EMPTY_HEX = Pattern.compile("0x0+"); - ProxyAPIProvider(RequestQueueManager queue, - String baseUrl, - EthHttpClient executor, - Converter converter, - int retryCount) { + public ProxyAPIProvider(RequestQueueManager queue, + String baseUrl, + EthHttpClient executor, + Converter converter, + int retryCount) { super(queue, "proxy", baseUrl, executor, converter, retryCount); } @Override public long blockNoLast() throws EtherScanException { - final StringProxyTO response = getRequest(ACT_BLOCKNO_PARAM, StringProxyTO.class); + final StringProxyTO response = getResponse(ACT_BLOCKNO_PARAM, StringProxyTO.class); return (BasicUtils.isEmpty(response.getResult())) ? -1 : BasicUtils.parseHex(response.getResult()).longValue(); @@ -79,7 +79,7 @@ public Optional block(long blockNo) throws EtherScanException { final long compBlockNo = BasicUtils.compensateMinBlock(blockNo); final String urlParams = ACT_BY_BLOCKNO_PARAM + TAG_PARAM + compBlockNo + BOOLEAN_PARAM; - final BlockProxyTO response = getRequest(urlParams, BlockProxyTO.class); + final BlockProxyTO response = getResponse(urlParams, BlockProxyTO.class); return Optional.ofNullable(response.getResult()); } @@ -91,7 +91,7 @@ public Optional blockUncle(long blockNo, long index) throws EtherSca final String urlParams = ACT_UNCLE_BY_BLOCKNOINDEX_PARAM + TAG_PARAM + "0x" + Long.toHexString(compBlockNo) + INDEX_PARAM + "0x" + Long.toHexString(compIndex); - final BlockProxyTO response = getRequest(urlParams, BlockProxyTO.class); + final BlockProxyTO response = getResponse(urlParams, BlockProxyTO.class); return Optional.ofNullable(response.getResult()); } @@ -101,7 +101,7 @@ public Optional tx(@NotNull String txhash) throws EtherScanException { BasicUtils.validateTxHash(txhash); final String urlParams = ACT_TX_BY_HASH_PARAM + TXHASH_PARAM + txhash; - final TxProxyTO response = getRequest(urlParams, TxProxyTO.class); + final TxProxyTO response = getResponse(urlParams, TxProxyTO.class); return Optional.ofNullable(response.getResult()); } @@ -115,7 +115,7 @@ public Optional tx(long blockNo, long index) throws EtherScanException final String urlParams = ACT_TX_BY_BLOCKNOINDEX_PARAM + TAG_PARAM + compBlockNo + INDEX_PARAM + "0x" + Long.toHexString(compIndex); - final TxProxyTO response = getRequest(urlParams, TxProxyTO.class); + final TxProxyTO response = getResponse(urlParams, TxProxyTO.class); return Optional.ofNullable(response.getResult()); } @@ -123,7 +123,7 @@ public Optional tx(long blockNo, long index) throws EtherScanException public int txCount(long blockNo) throws EtherScanException { final long compensatedBlockNo = BasicUtils.compensateMinBlock(blockNo); final String urlParams = ACT_BLOCKTX_COUNT_PARAM + TAG_PARAM + "0x" + Long.toHexString(compensatedBlockNo); - final StringProxyTO response = getRequest(urlParams, StringProxyTO.class); + final StringProxyTO response = getResponse(urlParams, StringProxyTO.class); return BasicUtils.parseHex(response.getResult()).intValue(); } @@ -132,7 +132,7 @@ public int txSendCount(@NotNull String address) throws EtherScanException { BasicUtils.validateAddress(address); final String urlParams = ACT_TX_COUNT_PARAM + ADDRESS_PARAM + address + TAG_LAST_PARAM; - final StringProxyTO response = getRequest(urlParams, StringProxyTO.class); + final StringProxyTO response = getResponse(urlParams, StringProxyTO.class); return BasicUtils.parseHex(response.getResult()).intValue(); } @@ -165,7 +165,7 @@ public Optional txReceipt(@NotNull String txhash) throws EtherScan BasicUtils.validateTxHash(txhash); final String urlParams = ACT_TX_RECEIPT_PARAM + TXHASH_PARAM + txhash; - final TxInfoProxyTO response = getRequest(urlParams, TxInfoProxyTO.class); + final TxInfoProxyTO response = getResponse(urlParams, TxInfoProxyTO.class); return Optional.ofNullable(response.getResult()); } @@ -177,7 +177,7 @@ public Optional call(@NotNull String address, @NotNull String data) thro throw new EtherScanInvalidDataHexException("Data is not hex encoded."); final String urlParams = ACT_CALL_PARAM + TO_PARAM + address + DATA_PARAM + data + TAG_LAST_PARAM; - final StringProxyTO response = getRequest(urlParams, StringProxyTO.class); + final StringProxyTO response = getResponse(urlParams, StringProxyTO.class); return Optional.ofNullable(response.getResult()); } @@ -187,7 +187,7 @@ public Optional code(@NotNull String address) throws EtherScanException BasicUtils.validateAddress(address); final String urlParams = ACT_CODE_PARAM + ADDRESS_PARAM + address + TAG_LAST_PARAM; - final StringProxyTO response = getRequest(urlParams, StringProxyTO.class); + final StringProxyTO response = getResponse(urlParams, StringProxyTO.class); return Optional.ofNullable(response.getResult()); } @@ -198,7 +198,7 @@ public Optional storageAt(@NotNull String address, long position) throws final long compPosition = BasicUtils.compensateMinBlock(position); final String urlParams = ACT_STORAGEAT_PARAM + ADDRESS_PARAM + address + POSITION_PARAM + compPosition + TAG_LAST_PARAM; - final StringProxyTO response = getRequest(urlParams, StringProxyTO.class); + final StringProxyTO response = getResponse(urlParams, StringProxyTO.class); return (BasicUtils.isEmpty(response.getResult()) || EMPTY_HEX.matcher(response.getResult()).matches()) ? Optional.empty() : Optional.of(response.getResult()); @@ -207,7 +207,7 @@ public Optional storageAt(@NotNull String address, long position) throws @NotNull @Override public Wei gasPrice() throws EtherScanException { - final StringProxyTO response = getRequest(ACT_GASPRICE_PARAM, StringProxyTO.class); + final StringProxyTO response = getResponse(ACT_GASPRICE_PARAM, StringProxyTO.class); return (BasicUtils.isEmpty(response.getResult())) ? Wei.ofWei(0) : Wei.ofWei(BasicUtils.parseHex(response.getResult())); @@ -226,7 +226,7 @@ public Wei gasEstimated(@NotNull String hexData) throws EtherScanException { throw new EtherScanInvalidDataHexException("Data is not in hex format."); final String urlParams = ACT_ESTIMATEGAS_PARAM + DATA_PARAM + hexData + GAS_PARAM + "2000000000000000"; - final StringProxyTO response = getRequest(urlParams, StringProxyTO.class); + final StringProxyTO response = getResponse(urlParams, StringProxyTO.class); return (BasicUtils.isEmpty(response.getResult())) ? Wei.ofWei(0) : Wei.ofWei(BasicUtils.parseHex(response.getResult())); diff --git a/src/main/java/io/goodforgod/api/etherscan/StatisticAPIProvider.java b/src/main/java/io/goodforgod/api/etherscan/StatisticAPIProvider.java index a2bba16..006017a 100644 --- a/src/main/java/io/goodforgod/api/etherscan/StatisticAPIProvider.java +++ b/src/main/java/io/goodforgod/api/etherscan/StatisticAPIProvider.java @@ -21,7 +21,7 @@ * @author GoodforGod * @since 28.10.2018 */ -final class StatisticAPIProvider extends BasicProvider implements StatisticAPI { +public class StatisticAPIProvider extends BasicProvider implements StatisticAPI { private static final String ACT_SUPPLY_PARAM = ACT_PREFIX + "ethsupply"; private static final String ACT_SUPPLY2_PARAM = ACT_PREFIX + "ethsupply2"; @@ -30,18 +30,18 @@ final class StatisticAPIProvider extends BasicProvider implements StatisticAPI { private static final String CONTRACT_ADDRESS_PARAM = "&contractaddress="; - StatisticAPIProvider(RequestQueueManager queue, - String baseUrl, - EthHttpClient executor, - Converter converter, - int retry) { + public StatisticAPIProvider(RequestQueueManager queue, + String baseUrl, + EthHttpClient executor, + Converter converter, + int retry) { super(queue, "stats", baseUrl, executor, converter, retry); } @NotNull @Override public Wei supply() throws EtherScanException { - final StringResponseTO response = getRequest(ACT_SUPPLY_PARAM, StringResponseTO.class); + final StringResponseTO response = getResponse(ACT_SUPPLY_PARAM, StringResponseTO.class); if (response.getStatus() != 1) throw new EtherScanResponseException(response); @@ -50,7 +50,7 @@ public Wei supply() throws EtherScanException { @Override public @NotNull EthSupply supplyTotal() throws EtherScanException { - final EthSupplyResponseTO response = getRequest(ACT_SUPPLY2_PARAM, EthSupplyResponseTO.class); + final EthSupplyResponseTO response = getResponse(ACT_SUPPLY2_PARAM, EthSupplyResponseTO.class); if (response.getStatus() != 1) throw new EtherScanResponseException(response); @@ -63,7 +63,7 @@ public Wei supply(@NotNull String contract) throws EtherScanException { BasicUtils.validateAddress(contract); final String urlParams = ACT_TOKEN_SUPPLY_PARAM + CONTRACT_ADDRESS_PARAM + contract; - final StringResponseTO response = getRequest(urlParams, StringResponseTO.class); + final StringResponseTO response = getResponse(urlParams, StringResponseTO.class); if (response.getStatus() != 1) throw new EtherScanResponseException(response); @@ -73,7 +73,7 @@ public Wei supply(@NotNull String contract) throws EtherScanException { @NotNull @Override public Price priceLast() throws EtherScanException { - final PriceResponseTO response = getRequest(ACT_LASTPRICE_PARAM, PriceResponseTO.class); + final PriceResponseTO response = getResponse(ACT_LASTPRICE_PARAM, PriceResponseTO.class); if (response.getStatus() != 1) throw new EtherScanResponseException(response); diff --git a/src/main/java/io/goodforgod/api/etherscan/TransactionAPIProvider.java b/src/main/java/io/goodforgod/api/etherscan/TransactionAPIProvider.java index 7374335..61f2484 100644 --- a/src/main/java/io/goodforgod/api/etherscan/TransactionAPIProvider.java +++ b/src/main/java/io/goodforgod/api/etherscan/TransactionAPIProvider.java @@ -17,18 +17,18 @@ * @see TransactionAPI * @since 28.10.2018 */ -final class TransactionAPIProvider extends BasicProvider implements TransactionAPI { +public class TransactionAPIProvider extends BasicProvider implements TransactionAPI { private static final String ACT_EXEC_STATUS_PARAM = ACT_PREFIX + "getstatus"; private static final String ACT_RECEIPT_STATUS_PARAM = ACT_PREFIX + "gettxreceiptstatus"; private static final String TXHASH_PARAM = "&txhash="; - TransactionAPIProvider(RequestQueueManager queue, - String baseUrl, - EthHttpClient executor, - Converter converter, - int retryCount) { + public TransactionAPIProvider(RequestQueueManager queue, + String baseUrl, + EthHttpClient executor, + Converter converter, + int retryCount) { super(queue, "transaction", baseUrl, executor, converter, retryCount); } @@ -38,7 +38,7 @@ public Optional statusExec(@NotNull String txhash) throws EtherScanExcep BasicUtils.validateTxHash(txhash); final String urlParams = ACT_EXEC_STATUS_PARAM + TXHASH_PARAM + txhash; - final StatusResponseTO response = getRequest(urlParams, StatusResponseTO.class); + final StatusResponseTO response = getResponse(urlParams, StatusResponseTO.class); BasicUtils.validateTxResponse(response); return Optional.ofNullable(response.getResult()); @@ -50,7 +50,7 @@ public Optional statusReceipt(@NotNull String txhash) throws EtherScanE BasicUtils.validateTxHash(txhash); final String urlParams = ACT_RECEIPT_STATUS_PARAM + TXHASH_PARAM + txhash; - final ReceiptStatusResponseTO response = getRequest(urlParams, ReceiptStatusResponseTO.class); + final ReceiptStatusResponseTO response = getResponse(urlParams, ReceiptStatusResponseTO.class); BasicUtils.validateTxResponse(response); return (response.getResult() == null || BasicUtils.isEmpty(response.getResult().getStatus())) diff --git a/src/main/java/io/goodforgod/api/etherscan/error/EtherScanConnectionTimeoutException.java b/src/main/java/io/goodforgod/api/etherscan/error/EtherScanConnectionTimeoutException.java new file mode 100644 index 0000000..1b2ff22 --- /dev/null +++ b/src/main/java/io/goodforgod/api/etherscan/error/EtherScanConnectionTimeoutException.java @@ -0,0 +1,12 @@ +package io.goodforgod.api.etherscan.error; + +/** + * @author GoodforGod + * @since 12.11.2018 + */ +public class EtherScanConnectionTimeoutException extends EtherScanConnectionException { + + public EtherScanConnectionTimeoutException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/io/goodforgod/api/etherscan/error/EtherScanTimeoutException.java b/src/main/java/io/goodforgod/api/etherscan/error/EtherScanTimeoutException.java deleted file mode 100644 index 7734139..0000000 --- a/src/main/java/io/goodforgod/api/etherscan/error/EtherScanTimeoutException.java +++ /dev/null @@ -1,12 +0,0 @@ -package io.goodforgod.api.etherscan.error; - -/** - * @author GoodforGod - * @since 12.11.2018 - */ -public class EtherScanTimeoutException extends EtherScanConnectionException { - - public EtherScanTimeoutException(String message, Throwable cause) { - super(message, cause); - } -} diff --git a/src/main/java/io/goodforgod/api/etherscan/http/EthHttpClient.java b/src/main/java/io/goodforgod/api/etherscan/http/EthHttpClient.java index bd01f83..28142b8 100644 --- a/src/main/java/io/goodforgod/api/etherscan/http/EthHttpClient.java +++ b/src/main/java/io/goodforgod/api/etherscan/http/EthHttpClient.java @@ -17,7 +17,7 @@ public interface EthHttpClient { * @param uri as string * @return result as string */ - byte[] get(@NotNull URI uri); + EthResponse get(@NotNull URI uri); /** * Performs a Http POST request @@ -26,5 +26,5 @@ public interface EthHttpClient { * @param body to post * @return result as string */ - byte[] post(@NotNull URI uri, byte[] body); + EthResponse post(@NotNull URI uri, byte[] body); } diff --git a/src/main/java/io/goodforgod/api/etherscan/http/EthResponse.java b/src/main/java/io/goodforgod/api/etherscan/http/EthResponse.java new file mode 100644 index 0000000..20b7d81 --- /dev/null +++ b/src/main/java/io/goodforgod/api/etherscan/http/EthResponse.java @@ -0,0 +1,30 @@ +package io.goodforgod.api.etherscan.http; + +import java.util.List; +import java.util.Map; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Anton Kurako (GoodforGod) + * + * @since 07.09.2025 + */ +public interface EthResponse { + + int statusCode(); + + @Nullable + byte[] body(); + + @NotNull + Map> headers(); + + static EthResponse of(int statusCode, @Nullable byte[] body) { + return new SimpleEthResponse(statusCode, body, null); + } + + static EthResponse of(int statusCode, @Nullable byte[] body, @Nullable Map> headers) { + return new SimpleEthResponse(statusCode, body, headers); + } +} diff --git a/src/main/java/io/goodforgod/api/etherscan/http/SimpleEthResponse.java b/src/main/java/io/goodforgod/api/etherscan/http/SimpleEthResponse.java new file mode 100644 index 0000000..2e9ebf4 --- /dev/null +++ b/src/main/java/io/goodforgod/api/etherscan/http/SimpleEthResponse.java @@ -0,0 +1,64 @@ +package io.goodforgod.api.etherscan.http; + +import java.util.*; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Anton Kurako (GoodforGod) + * + * @since 07.09.2025 + */ +class SimpleEthResponse implements EthResponse { + + private final int statusCode; + @Nullable + private final byte[] body; + private final Map> headers; + + SimpleEthResponse(int statusCode, @Nullable byte[] body, @Nullable Map> headers) { + this.statusCode = statusCode; + this.body = body; + this.headers = (headers == null) + ? Collections.emptyMap() + : headers; + } + + @Override + public int statusCode() { + return statusCode; + } + + @Nullable + @Override + public byte[] body() { + return body; + } + + @Override + public @NotNull Map> headers() { + return headers; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + SimpleEthResponse that = (SimpleEthResponse) o; + return statusCode == that.statusCode && Arrays.equals(body, that.body) && Objects.equals(headers, that.headers); + } + + @Override + public int hashCode() { + int result = Objects.hash(statusCode, headers); + result = 31 * result + Arrays.hashCode(body); + return result; + } + + @Override + public String toString() { + return "{\"statusCode\": " + statusCode + ", \"headers\": " + headers + ", \"body\": \"" + Arrays.toString(body) + "\"}"; + } +} diff --git a/src/main/java/io/goodforgod/api/etherscan/http/impl/JdkEthHttpClient.java b/src/main/java/io/goodforgod/api/etherscan/http/impl/JdkEthHttpClient.java new file mode 100644 index 0000000..df7a28b --- /dev/null +++ b/src/main/java/io/goodforgod/api/etherscan/http/impl/JdkEthHttpClient.java @@ -0,0 +1,148 @@ +package io.goodforgod.api.etherscan.http.impl; + +import io.goodforgod.api.etherscan.error.EtherScanConnectionException; +import io.goodforgod.api.etherscan.error.EtherScanConnectionTimeoutException; +import io.goodforgod.api.etherscan.http.EthHttpClient; +import io.goodforgod.api.etherscan.http.EthResponse; +import org.jetbrains.annotations.ApiStatus.Internal; +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpConnectTimeoutException; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Anton Kurako (GoodforGod) + * + * @since 07.09.2025 + */ +@Internal +public class JdkEthHttpClient implements EthHttpClient { + + private static final Map DEFAULT_HEADERS = new HashMap<>(); + + private static final Duration DEFAULT_CONNECT_TIMEOUT = Duration.ofSeconds(8); + private static final Duration DEFAULT_READ_TIMEOUT = Duration.ofMinutes(2); + + static { + DEFAULT_HEADERS.put("Accept-Language", "en"); + DEFAULT_HEADERS.put("Accept-Encoding", "deflate, gzip"); + DEFAULT_HEADERS.put("User-Agent", "Chrome/68.0.3440.106"); + DEFAULT_HEADERS.put("Accept-Charset", "UTF-8"); + } + + private final HttpClient httpClient; + private final Duration requestTimeout; + private final Map headers; + + public JdkEthHttpClient() { + this(HttpClient.newBuilder() + .connectTimeout(DEFAULT_CONNECT_TIMEOUT) + .followRedirects(HttpClient.Redirect.NORMAL) + .version(HttpClient.Version.HTTP_2) + .build(), + DEFAULT_READ_TIMEOUT, + DEFAULT_HEADERS); + } + + public JdkEthHttpClient(HttpClient httpClient) { + this(httpClient, DEFAULT_READ_TIMEOUT, DEFAULT_HEADERS); + } + + public JdkEthHttpClient(HttpClient httpClient, Duration requestTimeout) { + this(httpClient, requestTimeout, DEFAULT_HEADERS); + } + + public JdkEthHttpClient(HttpClient httpClient, Duration requestTimeout, Map headers) { + this.httpClient = httpClient; + this.requestTimeout = requestTimeout; + this.headers = headers; + } + + @Override + public EthResponse get(@NotNull URI uri) { + HttpRequest.Builder requestBuilder = HttpRequest.newBuilder() + .GET() + .uri(uri) + .timeout(requestTimeout); + + headers.forEach(requestBuilder::header); + + try { + HttpResponse response = httpClient.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofByteArray()); + return new EthResponse() { + + @Override + public int statusCode() { + return response.statusCode(); + } + + @Override + public byte[] body() { + return response.body(); + } + + @Override + public @NotNull Map> headers() { + return response.headers().map(); + } + }; + } catch (HttpConnectTimeoutException e) { + throw new EtherScanConnectionTimeoutException( + "Connection Timeout: Could not establish connection to Etherscan server for " + + httpClient.connectTimeout().orElse(DEFAULT_CONNECT_TIMEOUT) + " millis", + e); + } catch (IOException e) { + throw new EtherScanConnectionException("Etherscan HTTP server network error occurred: " + e.getMessage(), e); + } catch (InterruptedException e) { + throw new EtherScanConnectionException("Etherscan HTTP server interrupt exception occurred: " + e.getMessage(), e); + } + } + + @Override + public EthResponse post(@NotNull URI uri, byte[] body) { + HttpRequest.Builder requestBuilder = HttpRequest.newBuilder() + .POST(HttpRequest.BodyPublishers.ofByteArray(body)) + .uri(uri) + .timeout(requestTimeout); + + headers.forEach(requestBuilder::header); + + try { + HttpResponse response = httpClient.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofByteArray()); + return new EthResponse() { + + @Override + public int statusCode() { + return response.statusCode(); + } + + @Override + public byte[] body() { + return response.body(); + } + + @Override + public @NotNull Map> headers() { + return response.headers().map(); + } + }; + } catch (HttpConnectTimeoutException e) { + throw new EtherScanConnectionTimeoutException( + "Connection Timeout: Could not establish connection to Etherscan server for " + + httpClient.connectTimeout().orElse(DEFAULT_CONNECT_TIMEOUT) + " millis", + e); + } catch (IOException e) { + throw new EtherScanConnectionException("Etherscan HTTP server network error occurred: " + e.getMessage(), e); + } catch (InterruptedException e) { + throw new EtherScanConnectionException("Etherscan HTTP server interrupt exception occurred: " + e.getMessage(), e); + } + } +} diff --git a/src/main/java/io/goodforgod/api/etherscan/http/impl/UrlEthHttpClient.java b/src/main/java/io/goodforgod/api/etherscan/http/impl/UrlEthHttpClient.java index b298743..58f4658 100644 --- a/src/main/java/io/goodforgod/api/etherscan/http/impl/UrlEthHttpClient.java +++ b/src/main/java/io/goodforgod/api/etherscan/http/impl/UrlEthHttpClient.java @@ -3,20 +3,23 @@ import static java.net.HttpURLConnection.*; import io.goodforgod.api.etherscan.error.EtherScanConnectionException; -import io.goodforgod.api.etherscan.error.EtherScanTimeoutException; +import io.goodforgod.api.etherscan.error.EtherScanConnectionTimeoutException; import io.goodforgod.api.etherscan.http.EthHttpClient; -import java.io.*; -import java.net.HttpURLConnection; -import java.net.SocketTimeoutException; -import java.net.URI; -import java.net.URL; +import io.goodforgod.api.etherscan.http.EthResponse; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.*; import java.time.Duration; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.zip.GZIPInputStream; import java.util.zip.InflaterInputStream; +import org.jetbrains.annotations.ApiStatus.Internal; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; /** * Http client implementation @@ -25,12 +28,13 @@ * @see EthHttpClient * @since 28.10.2018 */ -public final class UrlEthHttpClient implements EthHttpClient { +@Internal +public class UrlEthHttpClient implements EthHttpClient { private static final Map DEFAULT_HEADERS = new HashMap<>(); private static final Duration DEFAULT_CONNECT_TIMEOUT = Duration.ofSeconds(8); - private static final Duration DEFAULT_READ_TIMEOUT = Duration.ZERO; + private static final Duration DEFAULT_READ_TIMEOUT = Duration.ofMinutes(2); static { DEFAULT_HEADERS.put("Accept-Language", "en"); @@ -39,6 +43,8 @@ public final class UrlEthHttpClient implements EthHttpClient { DEFAULT_HEADERS.put("Accept-Charset", "UTF-8"); } + @Nullable + private final Proxy proxy; private final Map headers; private final int connectTimeout; private final int readTimeout; @@ -48,11 +54,19 @@ public UrlEthHttpClient() { } public UrlEthHttpClient(Duration connectTimeout) { - this(connectTimeout, DEFAULT_READ_TIMEOUT); + this(connectTimeout, DEFAULT_READ_TIMEOUT, DEFAULT_HEADERS, null); } public UrlEthHttpClient(Duration connectTimeout, Duration readTimeout) { - this(connectTimeout, readTimeout, DEFAULT_HEADERS); + this(connectTimeout, readTimeout, DEFAULT_HEADERS, null); + } + + public UrlEthHttpClient(Duration connectTimeout, Duration readTimeout, Proxy proxy) { + this(connectTimeout, readTimeout, DEFAULT_HEADERS, null); + } + + public UrlEthHttpClient(Duration connectTimeout, Duration readTimeout, Map headers) { + this(connectTimeout, readTimeout, headers, null); } /** @@ -62,15 +76,19 @@ public UrlEthHttpClient(Duration connectTimeout, Duration readTimeout) { */ public UrlEthHttpClient(Duration connectTimeout, Duration readTimeout, - Map headers) { + Map headers, + @Nullable Proxy proxy) { this.connectTimeout = Math.toIntExact(connectTimeout.toMillis()); this.readTimeout = Math.toIntExact(readTimeout.toMillis()); this.headers = Collections.unmodifiableMap(headers); + this.proxy = proxy; } private HttpURLConnection buildConnection(URI uri, String method) throws IOException { final URL url = uri.toURL(); - final HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + final HttpURLConnection connection = (proxy == null) + ? (HttpURLConnection) url.openConnection() + : (HttpURLConnection) url.openConnection(proxy); connection.setRequestMethod(method); connection.setConnectTimeout(connectTimeout); connection.setReadTimeout(readTimeout); @@ -79,7 +97,7 @@ private HttpURLConnection buildConnection(URI uri, String method) throws IOExcep } @Override - public byte[] get(@NotNull URI uri) { + public EthResponse get(@NotNull URI uri) { try { final HttpURLConnection connection = buildConnection(uri, "GET"); final int status = connection.getResponseCode(); @@ -92,17 +110,19 @@ public byte[] get(@NotNull URI uri) { } final byte[] data = readData(connection); + EthResponse ethResponse = EthResponse.of(connection.getResponseCode(), data, connection.getHeaderFields()); connection.disconnect(); - return data; + return ethResponse; } catch (SocketTimeoutException e) { - throw new EtherScanTimeoutException("Timeout: Could not establish connection for " + connectTimeout + " millis", e); + throw new EtherScanConnectionTimeoutException( + "Timeout: Could not establish connection for " + connectTimeout + " millis", e); } catch (Exception e) { throw new EtherScanConnectionException(e.getMessage(), e); } } @Override - public byte[] post(@NotNull URI uri, byte[] body) { + public EthResponse post(@NotNull URI uri, byte[] body) { try { final HttpURLConnection connection = buildConnection(uri, "POST"); final int contentLength = body.length; @@ -126,10 +146,12 @@ public byte[] post(@NotNull URI uri, byte[] body) { } final byte[] data = readData(connection); + EthResponse ethResponse = EthResponse.of(connection.getResponseCode(), data, connection.getHeaderFields()); connection.disconnect(); - return data; + return ethResponse; } catch (SocketTimeoutException e) { - throw new EtherScanTimeoutException("Timeout: Could not establish connection for " + connectTimeout + " millis", e); + throw new EtherScanConnectionTimeoutException( + "Timeout: Could not establish connection for " + connectTimeout + " millis", e); } catch (Exception e) { throw new EtherScanConnectionException(e.getMessage(), e); } diff --git a/src/main/java/io/goodforgod/api/etherscan/manager/impl/FakeRequestQueueManager.java b/src/main/java/io/goodforgod/api/etherscan/manager/impl/FakeRequestQueueManager.java index 626b4c1..ed81b94 100644 --- a/src/main/java/io/goodforgod/api/etherscan/manager/impl/FakeRequestQueueManager.java +++ b/src/main/java/io/goodforgod/api/etherscan/manager/impl/FakeRequestQueueManager.java @@ -8,7 +8,7 @@ * @author GoodforGod * @since 03.11.2018 */ -public final class FakeRequestQueueManager implements RequestQueueManager { +public class FakeRequestQueueManager implements RequestQueueManager { @Override public void takeTurn() { diff --git a/src/main/java/io/goodforgod/api/etherscan/manager/impl/SemaphoreRequestQueueManager.java b/src/main/java/io/goodforgod/api/etherscan/manager/impl/SemaphoreRequestQueueManager.java index 2a3483c..44c6bd5 100644 --- a/src/main/java/io/goodforgod/api/etherscan/manager/impl/SemaphoreRequestQueueManager.java +++ b/src/main/java/io/goodforgod/api/etherscan/manager/impl/SemaphoreRequestQueueManager.java @@ -14,7 +14,7 @@ * @author GoodforGod * @since 30.10.2018 */ -public final class SemaphoreRequestQueueManager implements RequestQueueManager, AutoCloseable { +public class SemaphoreRequestQueueManager implements RequestQueueManager, AutoCloseable { private final ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor(); private final Semaphore semaphore; diff --git a/src/main/java/io/goodforgod/api/etherscan/model/Abi.java b/src/main/java/io/goodforgod/api/etherscan/model/Abi.java index fbf71be..21b6601 100644 --- a/src/main/java/io/goodforgod/api/etherscan/model/Abi.java +++ b/src/main/java/io/goodforgod/api/etherscan/model/Abi.java @@ -64,7 +64,7 @@ public static AbiBuilder builder() { return new AbiBuilder(); } - public static final class AbiBuilder { + public static class AbiBuilder { private String contractAbi; private boolean isVerified; diff --git a/src/main/java/io/goodforgod/api/etherscan/model/BaseTx.java b/src/main/java/io/goodforgod/api/etherscan/model/BaseTx.java index 8de679a..e0fc376 100644 --- a/src/main/java/io/goodforgod/api/etherscan/model/BaseTx.java +++ b/src/main/java/io/goodforgod/api/etherscan/model/BaseTx.java @@ -12,7 +12,7 @@ * @author GoodforGod * @since 28.10.2018 */ -abstract class BaseTx implements Comparable { +public abstract class BaseTx implements Comparable { long blockNumber; String timeStamp; diff --git a/src/main/java/io/goodforgod/api/etherscan/model/BlockTx.java b/src/main/java/io/goodforgod/api/etherscan/model/BlockTx.java index f3c4d67..f77e5d4 100644 --- a/src/main/java/io/goodforgod/api/etherscan/model/BlockTx.java +++ b/src/main/java/io/goodforgod/api/etherscan/model/BlockTx.java @@ -8,7 +8,7 @@ * @author Anton Kurako (GoodforGod) * @since 15.05.2023 */ -abstract class BlockTx extends BaseTx { +public abstract class BlockTx extends BaseTx { long nonce; String blockHash; diff --git a/src/main/java/io/goodforgod/api/etherscan/model/BlockUncle.java b/src/main/java/io/goodforgod/api/etherscan/model/BlockUncle.java index 961db7e..058e13b 100644 --- a/src/main/java/io/goodforgod/api/etherscan/model/BlockUncle.java +++ b/src/main/java/io/goodforgod/api/etherscan/model/BlockUncle.java @@ -64,7 +64,7 @@ public static UncleBuilder builder() { return new UncleBuilder(); } - public static final class UncleBuilder { + public static class UncleBuilder { private String miner; private BigInteger blockreward; @@ -138,7 +138,7 @@ public static BlockUncleBuilder builder() { return new BlockUncleBuilder(); } - public static final class BlockUncleBuilder extends Block.BlockBuilder { + public static class BlockUncleBuilder extends Block.BlockBuilder { private long blockNumber; private BigInteger blockReward; diff --git a/src/main/java/io/goodforgod/api/etherscan/model/ContractCreation.java b/src/main/java/io/goodforgod/api/etherscan/model/ContractCreation.java index 2082883..41e62c5 100644 --- a/src/main/java/io/goodforgod/api/etherscan/model/ContractCreation.java +++ b/src/main/java/io/goodforgod/api/etherscan/model/ContractCreation.java @@ -52,7 +52,7 @@ public static ContractCreationBuilder builder() { return new ContractCreationBuilder(); } - public static final class ContractCreationBuilder { + public static class ContractCreationBuilder { private String contractAddress; private String contractCreator; diff --git a/src/main/java/io/goodforgod/api/etherscan/model/EthSupply.java b/src/main/java/io/goodforgod/api/etherscan/model/EthSupply.java index c626069..4dbbce7 100644 --- a/src/main/java/io/goodforgod/api/etherscan/model/EthSupply.java +++ b/src/main/java/io/goodforgod/api/etherscan/model/EthSupply.java @@ -67,7 +67,7 @@ public static EthSupplyBuilder builder() { return new EthSupplyBuilder(); } - public static final class EthSupplyBuilder { + public static class EthSupplyBuilder { private Wei ethSupply; private Wei eth2Staking; diff --git a/src/main/java/io/goodforgod/api/etherscan/model/GasOracle.java b/src/main/java/io/goodforgod/api/etherscan/model/GasOracle.java index 6fe1231..7ee8b50 100644 --- a/src/main/java/io/goodforgod/api/etherscan/model/GasOracle.java +++ b/src/main/java/io/goodforgod/api/etherscan/model/GasOracle.java @@ -83,7 +83,7 @@ public static GasOracleBuilder builder() { return new GasOracleBuilder(); } - public static final class GasOracleBuilder { + public static class GasOracleBuilder { private Long lastBlock; private Wei safeGasPrice; diff --git a/src/main/java/io/goodforgod/api/etherscan/model/Log.java b/src/main/java/io/goodforgod/api/etherscan/model/Log.java index d54766c..d29db31 100644 --- a/src/main/java/io/goodforgod/api/etherscan/model/Log.java +++ b/src/main/java/io/goodforgod/api/etherscan/model/Log.java @@ -155,7 +155,7 @@ public static LogBuilder builder() { return new LogBuilder(); } - public static final class LogBuilder { + public static class LogBuilder { private Long blockNumber; private String address; diff --git a/src/main/java/io/goodforgod/api/etherscan/model/Price.java b/src/main/java/io/goodforgod/api/etherscan/model/Price.java index 403b705..0baa38a 100644 --- a/src/main/java/io/goodforgod/api/etherscan/model/Price.java +++ b/src/main/java/io/goodforgod/api/etherscan/model/Price.java @@ -76,7 +76,7 @@ public static PriceBuilder builder() { return new PriceBuilder(); } - public static final class PriceBuilder { + public static class PriceBuilder { private BigDecimal ethusd; private BigDecimal ethbtc; diff --git a/src/main/java/io/goodforgod/api/etherscan/model/Status.java b/src/main/java/io/goodforgod/api/etherscan/model/Status.java index 41b598a..f651b1f 100644 --- a/src/main/java/io/goodforgod/api/etherscan/model/Status.java +++ b/src/main/java/io/goodforgod/api/etherscan/model/Status.java @@ -53,7 +53,7 @@ public static StatusBuilder builder() { return new StatusBuilder(); } - public static final class StatusBuilder { + public static class StatusBuilder { private int isError; private String errDescription; diff --git a/src/main/java/io/goodforgod/api/etherscan/model/Tx.java b/src/main/java/io/goodforgod/api/etherscan/model/Tx.java index 0a836d1..8843bd6 100644 --- a/src/main/java/io/goodforgod/api/etherscan/model/Tx.java +++ b/src/main/java/io/goodforgod/api/etherscan/model/Tx.java @@ -59,7 +59,7 @@ public static TxBuilder builder() { return new TxBuilder(); } - public static final class TxBuilder { + public static class TxBuilder { private long blockNumber; private LocalDateTime timeStamp; diff --git a/src/main/java/io/goodforgod/api/etherscan/model/TxErc1155.java b/src/main/java/io/goodforgod/api/etherscan/model/TxErc1155.java index f0b1ce4..ed8754d 100644 --- a/src/main/java/io/goodforgod/api/etherscan/model/TxErc1155.java +++ b/src/main/java/io/goodforgod/api/etherscan/model/TxErc1155.java @@ -82,7 +82,7 @@ public static TxErc1155Builder builder() { return new TxErc1155Builder(); } - public static final class TxErc1155Builder { + public static class TxErc1155Builder { private long blockNumber; private LocalDateTime timeStamp; diff --git a/src/main/java/io/goodforgod/api/etherscan/model/TxErc20.java b/src/main/java/io/goodforgod/api/etherscan/model/TxErc20.java index 1d6080e..57d70cb 100644 --- a/src/main/java/io/goodforgod/api/etherscan/model/TxErc20.java +++ b/src/main/java/io/goodforgod/api/etherscan/model/TxErc20.java @@ -83,7 +83,7 @@ public static TxERC20Builder builder() { return new TxERC20Builder(); } - public static final class TxERC20Builder { + public static class TxERC20Builder { private long blockNumber; private LocalDateTime timeStamp; diff --git a/src/main/java/io/goodforgod/api/etherscan/model/TxErc721.java b/src/main/java/io/goodforgod/api/etherscan/model/TxErc721.java index 1ac49a0..64df779 100644 --- a/src/main/java/io/goodforgod/api/etherscan/model/TxErc721.java +++ b/src/main/java/io/goodforgod/api/etherscan/model/TxErc721.java @@ -82,7 +82,7 @@ public static TxERC721Builder builder() { return new TxERC721Builder(); } - public static final class TxERC721Builder { + public static class TxERC721Builder { private long blockNumber; private LocalDateTime timeStamp; diff --git a/src/main/java/io/goodforgod/api/etherscan/model/TxInternal.java b/src/main/java/io/goodforgod/api/etherscan/model/TxInternal.java index 389f456..91c2b27 100644 --- a/src/main/java/io/goodforgod/api/etherscan/model/TxInternal.java +++ b/src/main/java/io/goodforgod/api/etherscan/model/TxInternal.java @@ -89,7 +89,7 @@ public static TxInternalBuilder builder() { return new TxInternalBuilder(); } - public static final class TxInternalBuilder { + public static class TxInternalBuilder { private long blockNumber; private LocalDateTime timeStamp; diff --git a/src/main/java/io/goodforgod/api/etherscan/model/proxy/BlockProxy.java b/src/main/java/io/goodforgod/api/etherscan/model/proxy/BlockProxy.java index bee4d64..a16fb21 100644 --- a/src/main/java/io/goodforgod/api/etherscan/model/proxy/BlockProxy.java +++ b/src/main/java/io/goodforgod/api/etherscan/model/proxy/BlockProxy.java @@ -194,7 +194,7 @@ public static BlockProxyBuilder builder() { return new BlockProxyBuilder(); } - public static final class BlockProxyBuilder { + public static class BlockProxyBuilder { private Long number; private String hash; diff --git a/src/main/java/io/goodforgod/api/etherscan/model/proxy/ReceiptProxy.java b/src/main/java/io/goodforgod/api/etherscan/model/proxy/ReceiptProxy.java index d88fd6d..717b2e1 100644 --- a/src/main/java/io/goodforgod/api/etherscan/model/proxy/ReceiptProxy.java +++ b/src/main/java/io/goodforgod/api/etherscan/model/proxy/ReceiptProxy.java @@ -134,7 +134,7 @@ public static ReceiptProxyBuilder builder() { return new ReceiptProxyBuilder(); } - public static final class ReceiptProxyBuilder { + public static class ReceiptProxyBuilder { private String root; private String from; diff --git a/src/main/java/io/goodforgod/api/etherscan/model/proxy/TxProxy.java b/src/main/java/io/goodforgod/api/etherscan/model/proxy/TxProxy.java index 0a89921..1325cc4 100644 --- a/src/main/java/io/goodforgod/api/etherscan/model/proxy/TxProxy.java +++ b/src/main/java/io/goodforgod/api/etherscan/model/proxy/TxProxy.java @@ -156,7 +156,7 @@ public static TxProxyBuilder builder() { return new TxProxyBuilder(); } - public static final class TxProxyBuilder { + public static class TxProxyBuilder { private String to; private String hash; diff --git a/src/main/java/io/goodforgod/api/etherscan/model/proxy/utility/BaseProxyTO.java b/src/main/java/io/goodforgod/api/etherscan/model/proxy/utility/BaseProxyTO.java index ef57193..2960bfc 100644 --- a/src/main/java/io/goodforgod/api/etherscan/model/proxy/utility/BaseProxyTO.java +++ b/src/main/java/io/goodforgod/api/etherscan/model/proxy/utility/BaseProxyTO.java @@ -4,7 +4,7 @@ * @author GoodforGod * @since 31.10.2018 */ -abstract class BaseProxyTO { +public abstract class BaseProxyTO { private String id; private String jsonrpc; diff --git a/src/main/java/io/goodforgod/api/etherscan/model/query/LogQueryBuilderImpl.java b/src/main/java/io/goodforgod/api/etherscan/model/query/LogQueryBuilderImpl.java index 549bd47..f46a4e0 100644 --- a/src/main/java/io/goodforgod/api/etherscan/model/query/LogQueryBuilderImpl.java +++ b/src/main/java/io/goodforgod/api/etherscan/model/query/LogQueryBuilderImpl.java @@ -12,7 +12,7 @@ * @author GoodforGod * @since 31.10.2018 */ -final class LogQueryBuilderImpl implements LogQuery.Builder { +public class LogQueryBuilderImpl implements LogQuery.Builder { static final long MIN_BLOCK = 0; static final long MAX_BLOCK = 99999999999999999L; diff --git a/src/main/java/io/goodforgod/api/etherscan/model/query/LogQueryImpl.java b/src/main/java/io/goodforgod/api/etherscan/model/query/LogQueryImpl.java index ef66e72..6de9631 100644 --- a/src/main/java/io/goodforgod/api/etherscan/model/query/LogQueryImpl.java +++ b/src/main/java/io/goodforgod/api/etherscan/model/query/LogQueryImpl.java @@ -12,7 +12,7 @@ * @author GoodforGod * @since 31.10.2018 */ -final class LogQueryImpl implements LogQuery { +public class LogQueryImpl implements LogQuery { /** * Final request parameter for api call diff --git a/src/main/java/io/goodforgod/api/etherscan/model/query/LogQueryParams.java b/src/main/java/io/goodforgod/api/etherscan/model/query/LogQueryParams.java index ac77ae8..12fb6d0 100644 --- a/src/main/java/io/goodforgod/api/etherscan/model/query/LogQueryParams.java +++ b/src/main/java/io/goodforgod/api/etherscan/model/query/LogQueryParams.java @@ -10,7 +10,7 @@ * @author GoodforGod * @since 31.10.2018 */ -final class LogQueryParams { +public class LogQueryParams { private LogQueryParams() {} diff --git a/src/main/java/io/goodforgod/api/etherscan/model/query/LogTopicQuadro.java b/src/main/java/io/goodforgod/api/etherscan/model/query/LogTopicQuadro.java index 7fdd9db..f113a4f 100644 --- a/src/main/java/io/goodforgod/api/etherscan/model/query/LogTopicQuadro.java +++ b/src/main/java/io/goodforgod/api/etherscan/model/query/LogTopicQuadro.java @@ -14,7 +14,7 @@ * @author GoodforGod * @since 31.10.2018 */ -public final class LogTopicQuadro implements LogTopicBuilder { +public class LogTopicQuadro implements LogTopicBuilder { private final String address; private final long startBlock, endBlock; diff --git a/src/main/java/io/goodforgod/api/etherscan/model/query/LogTopicSingle.java b/src/main/java/io/goodforgod/api/etherscan/model/query/LogTopicSingle.java index a736ffa..b35c836 100644 --- a/src/main/java/io/goodforgod/api/etherscan/model/query/LogTopicSingle.java +++ b/src/main/java/io/goodforgod/api/etherscan/model/query/LogTopicSingle.java @@ -14,7 +14,7 @@ * @author GoodforGod * @since 31.10.2018 */ -public final class LogTopicSingle implements LogTopicBuilder { +public class LogTopicSingle implements LogTopicBuilder { private final String address; private final long startBlock, endBlock; diff --git a/src/main/java/io/goodforgod/api/etherscan/model/query/LogTopicTriple.java b/src/main/java/io/goodforgod/api/etherscan/model/query/LogTopicTriple.java index ac9efb8..d888a49 100644 --- a/src/main/java/io/goodforgod/api/etherscan/model/query/LogTopicTriple.java +++ b/src/main/java/io/goodforgod/api/etherscan/model/query/LogTopicTriple.java @@ -14,7 +14,7 @@ * @author GoodforGod * @since 31.10.2018 */ -public final class LogTopicTriple implements LogTopicBuilder { +public class LogTopicTriple implements LogTopicBuilder { private final String address; private final long startBlock, endBlock; diff --git a/src/main/java/io/goodforgod/api/etherscan/model/query/LogTopicTuple.java b/src/main/java/io/goodforgod/api/etherscan/model/query/LogTopicTuple.java index 2ef2bba..4a0218e 100644 --- a/src/main/java/io/goodforgod/api/etherscan/model/query/LogTopicTuple.java +++ b/src/main/java/io/goodforgod/api/etherscan/model/query/LogTopicTuple.java @@ -14,7 +14,7 @@ * @author GoodforGod * @since 31.10.2018 */ -public final class LogTopicTuple implements LogTopicBuilder { +public class LogTopicTuple implements LogTopicBuilder { private final String address; private final long startBlock, endBlock; diff --git a/src/main/java/io/goodforgod/api/etherscan/model/response/StringResponseTO.java b/src/main/java/io/goodforgod/api/etherscan/model/response/StringResponseTO.java index 19fa0a1..8b01b3d 100644 --- a/src/main/java/io/goodforgod/api/etherscan/model/response/StringResponseTO.java +++ b/src/main/java/io/goodforgod/api/etherscan/model/response/StringResponseTO.java @@ -16,7 +16,7 @@ public static StringResponseBuilder builder() { return new StringResponseBuilder(); } - public static final class StringResponseBuilder { + public static class StringResponseBuilder { private String status; private String message; diff --git a/src/main/java/io/goodforgod/api/etherscan/util/BasicUtils.java b/src/main/java/io/goodforgod/api/etherscan/util/BasicUtils.java index 916d4ab..db98bd4 100644 --- a/src/main/java/io/goodforgod/api/etherscan/util/BasicUtils.java +++ b/src/main/java/io/goodforgod/api/etherscan/util/BasicUtils.java @@ -17,7 +17,7 @@ * @author GoodforGod * @since 28.10.2018 */ -public final class BasicUtils { +public class BasicUtils { private BasicUtils() {} From 8dbcc82ccba7d82b711275e8df892916796f98ad Mon Sep 17 00:00:00 2001 From: Anton Kurako Date: Sun, 7 Sep 2025 15:11:35 +0300 Subject: [PATCH 14/18] [3.0.0-SNAPSHOT] Cleanup --- .../io/goodforgod/api/etherscan/BasicProvider.java | 3 +-- .../api/etherscan/http/impl/JdkEthHttpClient.java | 13 ++++++------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/main/java/io/goodforgod/api/etherscan/BasicProvider.java b/src/main/java/io/goodforgod/api/etherscan/BasicProvider.java index 2abb2e0..0ab4f20 100644 --- a/src/main/java/io/goodforgod/api/etherscan/BasicProvider.java +++ b/src/main/java/io/goodforgod/api/etherscan/BasicProvider.java @@ -9,12 +9,11 @@ import io.goodforgod.api.etherscan.manager.RequestQueueManager; import io.goodforgod.api.etherscan.model.response.BaseListResponseTO; import io.goodforgod.api.etherscan.model.response.StringResponseTO; +import io.goodforgod.api.etherscan.util.BasicUtils; import java.net.URI; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; - -import io.goodforgod.api.etherscan.util.BasicUtils; import org.jetbrains.annotations.ApiStatus.Internal; /** diff --git a/src/main/java/io/goodforgod/api/etherscan/http/impl/JdkEthHttpClient.java b/src/main/java/io/goodforgod/api/etherscan/http/impl/JdkEthHttpClient.java index df7a28b..eca88d8 100644 --- a/src/main/java/io/goodforgod/api/etherscan/http/impl/JdkEthHttpClient.java +++ b/src/main/java/io/goodforgod/api/etherscan/http/impl/JdkEthHttpClient.java @@ -4,9 +4,6 @@ import io.goodforgod.api.etherscan.error.EtherScanConnectionTimeoutException; import io.goodforgod.api.etherscan.http.EthHttpClient; import io.goodforgod.api.etherscan.http.EthResponse; -import org.jetbrains.annotations.ApiStatus.Internal; -import org.jetbrains.annotations.NotNull; - import java.io.IOException; import java.net.URI; import java.net.http.HttpClient; @@ -17,6 +14,8 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import org.jetbrains.annotations.ApiStatus.Internal; +import org.jetbrains.annotations.NotNull; /** * Anton Kurako (GoodforGod) @@ -44,10 +43,10 @@ public class JdkEthHttpClient implements EthHttpClient { public JdkEthHttpClient() { this(HttpClient.newBuilder() - .connectTimeout(DEFAULT_CONNECT_TIMEOUT) - .followRedirects(HttpClient.Redirect.NORMAL) - .version(HttpClient.Version.HTTP_2) - .build(), + .connectTimeout(DEFAULT_CONNECT_TIMEOUT) + .followRedirects(HttpClient.Redirect.NORMAL) + .version(HttpClient.Version.HTTP_2) + .build(), DEFAULT_READ_TIMEOUT, DEFAULT_HEADERS); } From 260899ba17f32b8fab05d5a477d518e450ee3422 Mon Sep 17 00:00:00 2001 From: Anton Kurako Date: Mon, 8 Sep 2025 00:23:30 +0300 Subject: [PATCH 15/18] [3.0.0-SNAPSHOT] Added JdkEthHttpClient gzip/deflate protocol support Added EthScanAPIBuilder key required --- .../api/etherscan/BasicProvider.java | 2 + .../api/etherscan/EthScanAPIBuilder.java | 16 +++---- .../api/etherscan/EtherScanAPI.java | 4 +- .../etherscan/http/impl/JdkEthHttpClient.java | 44 ++++++++++++++++--- .../goodforgod/api/etherscan/ApiRunner.java | 7 ++- .../api/etherscan/EtherScanAPITests.java | 11 ++--- 6 files changed, 59 insertions(+), 25 deletions(-) diff --git a/src/main/java/io/goodforgod/api/etherscan/BasicProvider.java b/src/main/java/io/goodforgod/api/etherscan/BasicProvider.java index 0ab4f20..4f809b5 100644 --- a/src/main/java/io/goodforgod/api/etherscan/BasicProvider.java +++ b/src/main/java/io/goodforgod/api/etherscan/BasicProvider.java @@ -129,7 +129,9 @@ protected T getResponse(String urlParameters, Class tClass) { protected T getResponse(String urlParameters, Class tClass, int retryCount) { try { + System.out.println("URL - " + URI.create(baseUrl + module + urlParameters)); EthResponse response = getResponse(urlParameters); + System.out.println("Response - " + new String(response.body(), StandardCharsets.UTF_8)); return convert(response.body(), tClass); } catch (Exception e) { if (retryCount < retryCountLimit) { diff --git a/src/main/java/io/goodforgod/api/etherscan/EthScanAPIBuilder.java b/src/main/java/io/goodforgod/api/etherscan/EthScanAPIBuilder.java index 12bb9d3..fa8af66 100644 --- a/src/main/java/io/goodforgod/api/etherscan/EthScanAPIBuilder.java +++ b/src/main/java/io/goodforgod/api/etherscan/EthScanAPIBuilder.java @@ -12,6 +12,7 @@ import java.io.IOException; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; +import java.util.Objects; import java.util.function.Supplier; import org.jetbrains.annotations.NotNull; @@ -22,12 +23,11 @@ public class EthScanAPIBuilder implements EtherScanAPI.Builder { private static final Supplier DEFAULT_SUPPLIER = JdkEthHttpClient::new; - private static final String DEFAULT_KEY = "YourApiKeyToken"; private final Gson gson = new GsonConfiguration().builder().create(); private int retryCountOnLimitReach = 0; - private String apiKey = DEFAULT_KEY; + private String apiKey; private RequestQueueManager queueManager; private EthNetwork ethNetwork = EthNetworks.MAINNET; private Supplier ethHttpClientSupplier = DEFAULT_SUPPLIER; @@ -43,6 +43,10 @@ public class EthScanAPIBuilder implements EtherScanAPI.Builder { } }; + public EthScanAPIBuilder(String apiKey) { + this.apiKey = apiKey; + } + @NotNull @Override public EtherScanAPI.Builder withApiKey(@NotNull String apiKey) { @@ -101,13 +105,7 @@ public EtherScanAPI.Builder withRetryOnRateLimit(int maxRetryCount) { @Override public @NotNull EtherScanAPI build() { RequestQueueManager requestQueueManager; - if (queueManager != null) { - requestQueueManager = queueManager; - } else if (DEFAULT_KEY.equals(apiKey)) { - requestQueueManager = RequestQueueManager.anonymous(); - } else { - requestQueueManager = RequestQueueManager.planFree(); - } + requestQueueManager = Objects.requireNonNullElseGet(queueManager, RequestQueueManager::planFree); return new EtherScanAPIProvider(apiKey, ethNetwork, requestQueueManager, ethHttpClientSupplier.get(), converterSupplier.get(), retryCountOnLimitReach); diff --git a/src/main/java/io/goodforgod/api/etherscan/EtherScanAPI.java b/src/main/java/io/goodforgod/api/etherscan/EtherScanAPI.java index bae1902..4c703bb 100644 --- a/src/main/java/io/goodforgod/api/etherscan/EtherScanAPI.java +++ b/src/main/java/io/goodforgod/api/etherscan/EtherScanAPI.java @@ -40,8 +40,8 @@ public interface EtherScanAPI extends AutoCloseable { GasTrackerAPI gasTracker(); @NotNull - static Builder builder() { - return new EthScanAPIBuilder(); + static Builder builder(@NotNull String key) { + return new EthScanAPIBuilder(key); } interface Builder { diff --git a/src/main/java/io/goodforgod/api/etherscan/http/impl/JdkEthHttpClient.java b/src/main/java/io/goodforgod/api/etherscan/http/impl/JdkEthHttpClient.java index eca88d8..fea2680 100644 --- a/src/main/java/io/goodforgod/api/etherscan/http/impl/JdkEthHttpClient.java +++ b/src/main/java/io/goodforgod/api/etherscan/http/impl/JdkEthHttpClient.java @@ -5,15 +5,17 @@ import io.goodforgod.api.etherscan.http.EthHttpClient; import io.goodforgod.api.etherscan.http.EthResponse; import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpConnectTimeoutException; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.time.Duration; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; +import java.util.zip.GZIPInputStream; +import java.util.zip.InflaterInputStream; import org.jetbrains.annotations.ApiStatus.Internal; import org.jetbrains.annotations.NotNull; @@ -75,7 +77,9 @@ public EthResponse get(@NotNull URI uri) { headers.forEach(requestBuilder::header); try { - HttpResponse response = httpClient.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofByteArray()); + HttpResponse response = httpClient.send(requestBuilder.build(), + HttpResponse.BodyHandlers.ofInputStream()); + byte[] bodyAsBytes = getDeflatedBytes(response); return new EthResponse() { @Override @@ -85,7 +89,7 @@ public int statusCode() { @Override public byte[] body() { - return response.body(); + return bodyAsBytes; } @Override @@ -115,7 +119,9 @@ public EthResponse post(@NotNull URI uri, byte[] body) { headers.forEach(requestBuilder::header); try { - HttpResponse response = httpClient.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofByteArray()); + HttpResponse response = httpClient.send(requestBuilder.build(), + HttpResponse.BodyHandlers.ofInputStream()); + byte[] bodyAsBytes = getDeflatedBytes(response); return new EthResponse() { @Override @@ -125,7 +131,7 @@ public int statusCode() { @Override public byte[] body() { - return response.body(); + return bodyAsBytes; } @Override @@ -144,4 +150,28 @@ public byte[] body() { throw new EtherScanConnectionException("Etherscan HTTP server interrupt exception occurred: " + e.getMessage(), e); } } + + private static byte[] getDeflatedBytes(HttpResponse response) { + try { + Optional encoding = response.headers().firstValue("content-encoding"); + if (encoding.isEmpty()) { + try (var is = response.body()) { + return is.readAllBytes(); + } + } + + switch (encoding.get().strip().toLowerCase(Locale.ROOT)) { + case "gzip": + return new GZIPInputStream(response.body()).readAllBytes(); + case "deflate": + return new InflaterInputStream(response.body()).readAllBytes(); + default: + try (var is = response.body()) { + return is.readAllBytes(); + } + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } } diff --git a/src/test/java/io/goodforgod/api/etherscan/ApiRunner.java b/src/test/java/io/goodforgod/api/etherscan/ApiRunner.java index bc4f334..23df478 100644 --- a/src/test/java/io/goodforgod/api/etherscan/ApiRunner.java +++ b/src/test/java/io/goodforgod/api/etherscan/ApiRunner.java @@ -25,8 +25,7 @@ public class ApiRunner extends Assertions { ? RequestQueueManager.anonymous() : RequestQueueManager.planFree(); - API = EtherScanAPI.builder() - .withApiKey(ApiRunner.API_KEY) + API = EtherScanAPI.builder(ApiRunner.API_KEY) .withNetwork(EthNetworks.MAINNET) .withQueue(queueManager) .withRetryOnRateLimit(5) @@ -37,6 +36,10 @@ public static EtherScanAPI getApi() { return API; } + public static String getKey() { + return API_KEY; + } + @AfterAll public static void cleanup() throws Exception { API.close(); diff --git a/src/test/java/io/goodforgod/api/etherscan/EtherScanAPITests.java b/src/test/java/io/goodforgod/api/etherscan/EtherScanAPITests.java index 36e23ec..d9e84f0 100644 --- a/src/test/java/io/goodforgod/api/etherscan/EtherScanAPITests.java +++ b/src/test/java/io/goodforgod/api/etherscan/EtherScanAPITests.java @@ -22,25 +22,26 @@ class EtherScanAPITests extends ApiRunner { @Test void validKey() { String validKey = "YourKey"; - EtherScanAPI api = EtherScanAPI.builder().withApiKey(validKey).withNetwork(network).build(); + EtherScanAPI api = EtherScanAPI.builder(validKey).withApiKey(validKey).withNetwork(network).build(); assertNotNull(api); } @Test void emptyKey() { - assertThrows(EtherScanKeyException.class, () -> EtherScanAPI.builder().withApiKey("").build()); + assertThrows(EtherScanKeyException.class, () -> EtherScanAPI.builder("someKey").withApiKey("").build()); } @Test void blankKey() { assertThrows(EtherScanKeyException.class, - () -> EtherScanAPI.builder().withApiKey(" ").withNetwork(network).build()); + () -> EtherScanAPI.builder("someKey").withApiKey(" ").withNetwork(network).build()); } @Test void noTimeoutOnRead() { Supplier supplier = () -> new UrlEthHttpClient(Duration.ofMillis(300)); - EtherScanAPI api = EtherScanAPI.builder().withNetwork(EthNetworks.MAINNET).withHttpClient(supplier).build(); + EtherScanAPI api = EtherScanAPI.builder(ApiRunner.getKey()).withNetwork(EthNetworks.MAINNET).withHttpClient(supplier) + .build(); Balance balance = api.account().balance("0xF318ABc9A5a92357c4Fea8d082dade4D43e780B7"); assertNotNull(balance); } @@ -67,7 +68,7 @@ void noTimeoutUnlimitedAwait() { void timeout() throws InterruptedException { TimeUnit.SECONDS.sleep(5); Supplier supplier = () -> new UrlEthHttpClient(Duration.ofMillis(300), Duration.ofMillis(300)); - EtherScanAPI api = EtherScanAPI.builder() + EtherScanAPI api = EtherScanAPI.builder(ApiRunner.getKey()) .withNetwork(() -> URI.create("https://api-unknown.etherscan.io/api")) .withHttpClient(supplier) .build(); From 7ea05262d05a803099e79e4f783b6bd926d9c9c6 Mon Sep 17 00:00:00 2001 From: Anton Kurako Date: Mon, 8 Sep 2025 01:08:49 +0300 Subject: [PATCH 16/18] [3.0.0-SNAPSHOT] Added JdkEthHttpClient gzip/deflate protocol support Added EthScanAPIBuilder key required Fixed JdkEthHttpClient fallback to UrlEthHttpClient cause content-length works incorrectly Updated test and fixed --- .github/workflows/master.yml | 37 +++++++++++++++++++ .github/workflows/publish-release.yml | 4 +- .github/workflows/publish-snapshot.yml | 4 +- .github/workflows/pull-request.yml | 18 +-------- build.gradle | 10 ++--- .../etherscan/http/impl/JdkEthHttpClient.java | 9 +++++ .../api/etherscan/model/GasOracle.java | 13 +++---- .../api/etherscan/EtherScanAPITests.java | 28 -------------- .../etherscan/account/AccountTxsTests.java | 4 +- .../etherscan/proxy/ProxyBlockApiTests.java | 1 - 10 files changed, 62 insertions(+), 66 deletions(-) create mode 100644 .github/workflows/master.yml diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml new file mode 100644 index 0000000..7e82142 --- /dev/null +++ b/.github/workflows/master.yml @@ -0,0 +1,37 @@ +name: CI Master + +on: + push: + branches: + - master + schedule: + - cron: 0 0 * * 0 + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + java: [ '11', '17' ] + name: Master Java ${{ matrix.java }} action + + steps: + - uses: actions/checkout@v3 + - name: Set up JDK + uses: actions/setup-java@v3 + with: + java-version: ${{ matrix.java }} + distribution: 'adopt' + + - name: Build + run: './gradlew classes' + + - name: Test + run: './gradlew test jacocoTestReport' + + - name: SonarQube + if: matrix.java == '17' + run: './gradlew sonar --info' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 3aa7884..6555593 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -1,4 +1,4 @@ -name: CI Master +name: Release on: release: @@ -22,8 +22,6 @@ jobs: - name: Test run: './gradlew test jacocoTestReport' - env: - ETHERSCAN_API_KEY: ${{ secrets.ETHERSCAN_API_KEY_2 }} - name: SonarQube run: './gradlew sonar --info' diff --git a/.github/workflows/publish-snapshot.yml b/.github/workflows/publish-snapshot.yml index d181a6c..cc95412 100644 --- a/.github/workflows/publish-snapshot.yml +++ b/.github/workflows/publish-snapshot.yml @@ -1,4 +1,4 @@ -name: CI Dev +name: Snapshot on: push: @@ -32,8 +32,6 @@ jobs: - name: Test run: './gradlew test jacocoTestReport' - env: - ETHERSCAN_API_KEY: ${{ secrets.ETHERSCAN_API_KEY_2 }} - name: Publish Snapshot run: './gradlew publish' diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 0b49f50..9da9b35 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -12,7 +12,7 @@ jobs: strategy: matrix: java: [ '11', '17' ] - name: Java ${{ matrix.java }} Pull Request setup + name: Pull Request Java ${{ matrix.java }} action steps: - uses: actions/checkout@v3 @@ -29,23 +29,7 @@ jobs: run: './gradlew classes' - name: Test - if: matrix.java == '11' run: './gradlew test jacocoTestReport' - env: - ETHERSCAN_API_KEY: ${{ secrets.ETHERSCAN_API_KEY_1 }} - - - name: Test - if: matrix.java == '17' - run: './gradlew test jacocoTestReport' - env: - ETHERSCAN_API_KEY: ${{ secrets.ETHERSCAN_API_KEY_2 }} - - - name: Test Report - if: matrix.java == '17' - uses: EnricoMi/publish-unit-test-result-action@v2 - with: - files: | - **/test-results/**/*.xml - name: SonarQube if: matrix.java == '17' diff --git a/build.gradle b/build.gradle index f946f1d..6fcbb8c 100644 --- a/build.gradle +++ b/build.gradle @@ -35,7 +35,7 @@ test { testLogging { events("passed", "skipped", "failed") exceptionFormat("full") - showStandardStreams(false) + showStandardStreams(true) } reports { @@ -71,8 +71,8 @@ nexusPublishing { sonatype { username = System.getenv("OSS_USERNAME") password = System.getenv("OSS_PASSWORD") - nexusUrl.set(uri("https://oss.sonatype.org/service/local/")) - snapshotRepositoryUrl.set(uri("https://oss.sonatype.org/content/repositories/snapshots/")) + nexusUrl.set(uri("https://ossrh-staging-api.central.sonatype.com/service/local/")) + snapshotRepositoryUrl.set(uri("https://central.sonatype.com/repository/maven-snapshots/")) } } } @@ -110,8 +110,8 @@ publishing { } repositories { maven { - def releasesRepoUrl = "https://oss.sonatype.org/service/local/staging/deploy/maven2" - def snapshotsRepoUrl = "https://oss.sonatype.org/content/repositories/snapshots/" + def releasesRepoUrl = "https://ossrh-staging-api.central.sonatype.com/service/local/" + def snapshotsRepoUrl = "https://central.sonatype.com/repository/maven-snapshots/" url = version.endsWith("SNAPSHOT") ? snapshotsRepoUrl : releasesRepoUrl credentials { username System.getenv("OSS_USERNAME") diff --git a/src/main/java/io/goodforgod/api/etherscan/http/impl/JdkEthHttpClient.java b/src/main/java/io/goodforgod/api/etherscan/http/impl/JdkEthHttpClient.java index fea2680..01580ce 100644 --- a/src/main/java/io/goodforgod/api/etherscan/http/impl/JdkEthHttpClient.java +++ b/src/main/java/io/goodforgod/api/etherscan/http/impl/JdkEthHttpClient.java @@ -42,6 +42,7 @@ public class JdkEthHttpClient implements EthHttpClient { private final HttpClient httpClient; private final Duration requestTimeout; private final Map headers; + private final UrlEthHttpClient urlEthHttpClient; public JdkEthHttpClient() { this(HttpClient.newBuilder() @@ -65,6 +66,7 @@ public JdkEthHttpClient(HttpClient httpClient, Duration requestTimeout, Map response = httpClient.send(requestBuilder.build(), diff --git a/src/main/java/io/goodforgod/api/etherscan/model/GasOracle.java b/src/main/java/io/goodforgod/api/etherscan/model/GasOracle.java index 7ee8b50..e7726f9 100644 --- a/src/main/java/io/goodforgod/api/etherscan/model/GasOracle.java +++ b/src/main/java/io/goodforgod/api/etherscan/model/GasOracle.java @@ -1,7 +1,6 @@ package io.goodforgod.api.etherscan.model; import java.math.BigDecimal; -import java.math.BigInteger; import java.util.Arrays; import java.util.List; import java.util.Objects; @@ -14,9 +13,9 @@ public class GasOracle { private Long LastBlock; - private BigInteger SafeGasPrice; - private BigInteger ProposeGasPrice; - private BigInteger FastGasPrice; + private BigDecimal SafeGasPrice; + private BigDecimal ProposeGasPrice; + private BigDecimal FastGasPrice; private BigDecimal suggestBaseFee; private String gasUsedRatio; @@ -129,13 +128,13 @@ public GasOracle build() { gasOracle.LastBlock = this.lastBlock; gasOracle.suggestBaseFee = this.suggestBaseFee; if (this.proposeGasPrice != null) { - gasOracle.ProposeGasPrice = this.proposeGasPrice.asGwei().toBigInteger(); + gasOracle.ProposeGasPrice = this.proposeGasPrice.asGwei(); } if (this.safeGasPrice != null) { - gasOracle.SafeGasPrice = this.safeGasPrice.asGwei().toBigInteger(); + gasOracle.SafeGasPrice = this.safeGasPrice.asGwei(); } if (this.fastGasPrice != null) { - gasOracle.FastGasPrice = this.fastGasPrice.asGwei().toBigInteger(); + gasOracle.FastGasPrice = this.fastGasPrice.asGwei(); } if (this.gasUsedRatio != null) { gasOracle.gasUsedRatio = this.gasUsedRatio.stream() diff --git a/src/test/java/io/goodforgod/api/etherscan/EtherScanAPITests.java b/src/test/java/io/goodforgod/api/etherscan/EtherScanAPITests.java index d9e84f0..636e752 100644 --- a/src/test/java/io/goodforgod/api/etherscan/EtherScanAPITests.java +++ b/src/test/java/io/goodforgod/api/etherscan/EtherScanAPITests.java @@ -4,7 +4,6 @@ import io.goodforgod.api.etherscan.error.EtherScanKeyException; import io.goodforgod.api.etherscan.http.EthHttpClient; import io.goodforgod.api.etherscan.http.impl.UrlEthHttpClient; -import io.goodforgod.api.etherscan.model.Balance; import java.net.URI; import java.time.Duration; import java.util.concurrent.TimeUnit; @@ -37,33 +36,6 @@ void blankKey() { () -> EtherScanAPI.builder("someKey").withApiKey(" ").withNetwork(network).build()); } - @Test - void noTimeoutOnRead() { - Supplier supplier = () -> new UrlEthHttpClient(Duration.ofMillis(300)); - EtherScanAPI api = EtherScanAPI.builder(ApiRunner.getKey()).withNetwork(EthNetworks.MAINNET).withHttpClient(supplier) - .build(); - Balance balance = api.account().balance("0xF318ABc9A5a92357c4Fea8d082dade4D43e780B7"); - assertNotNull(balance); - } - - @Test - void noTimeoutOnReadGroli() { - Balance balance = getApi().account().balance("0xF318ABc9A5a92357c4Fea8d082dade4D43e780B7"); - assertNotNull(balance); - } - - @Test - void noTimeoutOnReadTobalala() { - Balance balance = getApi().account().balance("0xF318ABc9A5a92357c4Fea8d082dade4D43e780B7"); - assertNotNull(balance); - } - - @Test - void noTimeoutUnlimitedAwait() { - Balance balance = getApi().account().balance("0xF318ABc9A5a92357c4Fea8d082dade4D43e780B7"); - assertNotNull(balance); - } - @Test void timeout() throws InterruptedException { TimeUnit.SECONDS.sleep(5); diff --git a/src/test/java/io/goodforgod/api/etherscan/account/AccountTxsTests.java b/src/test/java/io/goodforgod/api/etherscan/account/AccountTxsTests.java index 653f62a..42e89c4 100644 --- a/src/test/java/io/goodforgod/api/etherscan/account/AccountTxsTests.java +++ b/src/test/java/io/goodforgod/api/etherscan/account/AccountTxsTests.java @@ -16,7 +16,7 @@ class AccountTxsTests extends ApiRunner { void correct() { List txs = getApi().account().txs("0x9327cb34984c3992ec1EA0eAE98Ccf80A74f95B9"); assertNotNull(txs); - assertEquals(5, txs.size()); + assertEquals(6, txs.size()); assertTxs(txs); assertNotNull(txs.get(0).getTimeStamp()); assertNotNull(txs.get(0).getHash()); @@ -39,7 +39,7 @@ void correct() { void correctStartBlock() { List txs = getApi().account().txs("0x9327cb34984c3992ec1EA0eAE98Ccf80A74f95B9", 3892842); assertNotNull(txs); - assertEquals(4, txs.size()); + assertEquals(5, txs.size()); assertTxs(txs); } diff --git a/src/test/java/io/goodforgod/api/etherscan/proxy/ProxyBlockApiTests.java b/src/test/java/io/goodforgod/api/etherscan/proxy/ProxyBlockApiTests.java index 363d5a2..53ed4cd 100644 --- a/src/test/java/io/goodforgod/api/etherscan/proxy/ProxyBlockApiTests.java +++ b/src/test/java/io/goodforgod/api/etherscan/proxy/ProxyBlockApiTests.java @@ -22,7 +22,6 @@ void correct() { assertNotNull(proxy.getStateRoot()); assertNotNull(proxy.getSize()); assertNotNull(proxy.getDifficulty()); - assertNotNull(proxy.getTotalDifficulty()); assertNotNull(proxy.getTimeStamp()); assertNotNull(proxy.getMiner()); assertNotNull(proxy.getNonce()); From 7790bb7484bd36657c8e6a67de87bff941120599 Mon Sep 17 00:00:00 2001 From: Anton Kurako Date: Mon, 8 Sep 2025 01:26:05 +0300 Subject: [PATCH 17/18] [3.0.0-SNAPSHOT] Cleanup --- .github/workflows/master.yml | 2 +- .github/workflows/pull-request.yml | 2 +- src/main/java/io/goodforgod/api/etherscan/BasicProvider.java | 2 -- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index 7e82142..9892f9c 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - java: [ '11', '17' ] + java: [ '11' ] name: Master Java ${{ matrix.java }} action steps: diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 9da9b35..99940ee 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - java: [ '11', '17' ] + java: [ '11' ] name: Pull Request Java ${{ matrix.java }} action steps: diff --git a/src/main/java/io/goodforgod/api/etherscan/BasicProvider.java b/src/main/java/io/goodforgod/api/etherscan/BasicProvider.java index 4f809b5..0ab4f20 100644 --- a/src/main/java/io/goodforgod/api/etherscan/BasicProvider.java +++ b/src/main/java/io/goodforgod/api/etherscan/BasicProvider.java @@ -129,9 +129,7 @@ protected T getResponse(String urlParameters, Class tClass) { protected T getResponse(String urlParameters, Class tClass, int retryCount) { try { - System.out.println("URL - " + URI.create(baseUrl + module + urlParameters)); EthResponse response = getResponse(urlParameters); - System.out.println("Response - " + new String(response.body(), StandardCharsets.UTF_8)); return convert(response.body(), tClass); } catch (Exception e) { if (retryCount < retryCountLimit) { From 5c69eb92e32d337c2cdeb0d61598b185ee8eefd7 Mon Sep 17 00:00:00 2001 From: Anton Kurako Date: Mon, 8 Sep 2025 13:56:50 +0300 Subject: [PATCH 18/18] [3.0.0-SNAPSHOT] Updated CI with key Added more toString/equalsHashcode contracts --- .github/workflows/master.yml | 4 +- .github/workflows/publish-release.yml | 2 + .github/workflows/publish-snapshot.yml | 2 + .github/workflows/pull-request.yml | 4 +- README.md | 2 +- build.gradle | 2 +- .../api/etherscan/model/proxy/BlockProxy.java | 101 ++++++++++-------- .../etherscan/model/proxy/ReceiptProxy.java | 81 ++++++++------ .../api/etherscan/model/proxy/TxProxy.java | 86 ++++++++------- .../model/proxy/utility/BaseProxyTO.java | 26 +++++ .../model/proxy/utility/BlockProxyTO.java | 25 +++++ .../model/proxy/utility/ErrorProxyTO.java | 25 +++++ .../model/proxy/utility/StringProxyTO.java | 26 +++++ .../model/proxy/utility/TxInfoProxyTO.java | 25 +++++ .../model/proxy/utility/TxProxyTO.java | 25 +++++ .../etherscan/model/query/LogQueryImpl.java | 23 ++++ .../etherscan/model/query/LogTopicQuadro.java | 40 +++++++ .../etherscan/model/query/LogTopicSingle.java | 27 +++++ .../etherscan/model/query/LogTopicTriple.java | 34 ++++++ .../etherscan/model/query/LogTopicTuple.java | 30 ++++++ 20 files changed, 472 insertions(+), 118 deletions(-) diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index 9892f9c..32d0d9f 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -28,9 +28,11 @@ jobs: - name: Test run: './gradlew test jacocoTestReport' + env: + ETHERSCAN_API_KEY: ${{ secrets.ETHERSCAN_API_KEY }} - name: SonarQube - if: matrix.java == '17' + if: matrix.java == '11' run: './gradlew sonar --info' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 6555593..ed10cc4 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -22,6 +22,8 @@ jobs: - name: Test run: './gradlew test jacocoTestReport' + env: + ETHERSCAN_API_KEY: ${{ secrets.ETHERSCAN_API_KEY_PUBLISH }} - name: SonarQube run: './gradlew sonar --info' diff --git a/.github/workflows/publish-snapshot.yml b/.github/workflows/publish-snapshot.yml index cc95412..4a4d958 100644 --- a/.github/workflows/publish-snapshot.yml +++ b/.github/workflows/publish-snapshot.yml @@ -32,6 +32,8 @@ jobs: - name: Test run: './gradlew test jacocoTestReport' + env: + ETHERSCAN_API_KEY: ${{ secrets.ETHERSCAN_API_KEY_PUBLISH }} - name: Publish Snapshot run: './gradlew publish' diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 99940ee..c9c6a99 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -30,9 +30,11 @@ jobs: - name: Test run: './gradlew test jacocoTestReport' + env: + ETHERSCAN_API_KEY: ${{ secrets.ETHERSCAN_API_KEY }} - name: SonarQube - if: matrix.java == '17' + if: matrix.java == '11' run: './gradlew sonar --info' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/README.md b/README.md index 160f8e4..c09c885 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Java EtherScan API -[![Minimum required Java version](https://img.shields.io/badge/Java-1.8%2B-blue?logo=openjdk)](https://openjdk.org/projects/jdk8/) +[![Minimum required Java version](https://img.shields.io/badge/Java-11%2B-blue?logo=openjdk)](https://openjdk.org/projects/jdk/11/) [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.github.goodforgod/java-etherscan-api/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.github.goodforgod/java-etherscan-api) [![Java CI](https://github.com/GoodforGod/java-etherscan-api/workflows/CI%20Master/badge.svg)](https://github.com/GoodforGod/java-etherscan-api/actions?query=workflow%3ACI+Master) [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=GoodforGod_java-etherscan-api&metric=coverage)](https://sonarcloud.io/dashboard?id=GoodforGod_java-etherscan-api) diff --git a/build.gradle b/build.gradle index 6fcbb8c..b9be8cd 100644 --- a/build.gradle +++ b/build.gradle @@ -35,7 +35,7 @@ test { testLogging { events("passed", "skipped", "failed") exceptionFormat("full") - showStandardStreams(true) + showStandardStreams(false) } reports { diff --git a/src/main/java/io/goodforgod/api/etherscan/model/proxy/BlockProxy.java b/src/main/java/io/goodforgod/api/etherscan/model/proxy/BlockProxy.java index a16fb21..b138c14 100644 --- a/src/main/java/io/goodforgod/api/etherscan/model/proxy/BlockProxy.java +++ b/src/main/java/io/goodforgod/api/etherscan/model/proxy/BlockProxy.java @@ -143,48 +143,6 @@ public List getTransactions() { } // - @Override - public boolean equals(Object o) { - if (this == o) - return true; - if (!(o instanceof BlockProxy)) - return false; - BlockProxy that = (BlockProxy) o; - return Objects.equals(number, that.number) && Objects.equals(hash, that.hash) - && Objects.equals(parentHash, that.parentHash) && Objects.equals(nonce, that.nonce); - } - - @Override - public int hashCode() { - return Objects.hash(number, hash, parentHash, nonce); - } - - @Override - public String toString() { - return "BlockProxy{" + - "number=" + number + - ", hash=" + hash + - ", parentHash=" + parentHash + - ", stateRoot=" + stateRoot + - ", size=" + size + - ", difficulty=" + difficulty + - ", totalDifficulty=" + totalDifficulty + - ", timestamp=" + timestamp + - ", miner=" + miner + - ", nonce=" + nonce + - ", extraData=" + extraData + - ", logsBloom=" + logsBloom + - ", mixHash=" + mixHash + - ", gasUsed=" + gasUsed + - ", gasLimit=" + gasLimit + - ", sha3Uncles=" + sha3Uncles + - ", uncles=" + uncles + - ", receiptsRoot=" + receiptsRoot + - ", transactionsRoot=" + transactionsRoot + - ", transactions=" + transactions + - '}'; - } - @Override public int compareTo(@NotNull BlockProxy o) { return Long.compare(getNumber(), o.getNumber()); @@ -353,4 +311,63 @@ public BlockProxy build() { return blockProxy; } } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + BlockProxy that = (BlockProxy) o; + return Objects.equals(number, that.number) && Objects.equals(_number, that._number) && Objects.equals(hash, that.hash) + && Objects.equals(parentHash, that.parentHash) && Objects.equals(stateRoot, that.stateRoot) + && Objects.equals(size, that.size) && Objects.equals(_size, that._size) + && Objects.equals(difficulty, that.difficulty) && Objects.equals(totalDifficulty, that.totalDifficulty) + && Objects.equals(timestamp, that.timestamp) && Objects.equals(_timestamp, that._timestamp) + && Objects.equals(miner, that.miner) && Objects.equals(nonce, that.nonce) + && Objects.equals(extraData, that.extraData) && Objects.equals(logsBloom, that.logsBloom) + && Objects.equals(mixHash, that.mixHash) && Objects.equals(gasUsed, that.gasUsed) + && Objects.equals(_gasUsed, that._gasUsed) && Objects.equals(gasLimit, that.gasLimit) + && Objects.equals(_gasLimit, that._gasLimit) && Objects.equals(sha3Uncles, that.sha3Uncles) + && Objects.equals(uncles, that.uncles) && Objects.equals(receiptsRoot, that.receiptsRoot) + && Objects.equals(transactionsRoot, that.transactionsRoot) && Objects.equals(transactions, that.transactions); + } + + @Override + public int hashCode() { + return Objects.hash(number, number, hash, parentHash, stateRoot, size, size, difficulty, totalDifficulty, timestamp, + timestamp, miner, nonce, extraData, logsBloom, mixHash, gasUsed, gasUsed, gasLimit, gasLimit, sha3Uncles, uncles, + receiptsRoot, transactionsRoot, transactions); + } + + @Override + public String toString() { + return "BlockProxy{" + + "number=" + number + + ", number=" + _number + + ", hash=" + hash + + ", parentHash=" + parentHash + + ", stateRoot=" + stateRoot + + ", size=" + size + + ", size=" + _size + + ", difficulty=" + difficulty + + ", totalDifficulty=" + totalDifficulty + + ", timestamp=" + timestamp + + ", timestamp=" + _timestamp + + ", miner=" + miner + + ", nonce=" + nonce + + ", extraData=" + extraData + + ", logsBloom=" + logsBloom + + ", mixHash=" + mixHash + + ", gasUsed=" + gasUsed + + ", gasUsed=" + _gasUsed + + ", gasLimit=" + gasLimit + + ", gasLimit=" + _gasLimit + + ", sha3Uncles=" + sha3Uncles + + ", uncles=" + uncles + + ", receiptsRoot=" + receiptsRoot + + ", transactionsRoot=" + transactionsRoot + + ", transactions=" + transactions + + '}'; + } } diff --git a/src/main/java/io/goodforgod/api/etherscan/model/proxy/ReceiptProxy.java b/src/main/java/io/goodforgod/api/etherscan/model/proxy/ReceiptProxy.java index 717b2e1..6c933c5 100644 --- a/src/main/java/io/goodforgod/api/etherscan/model/proxy/ReceiptProxy.java +++ b/src/main/java/io/goodforgod/api/etherscan/model/proxy/ReceiptProxy.java @@ -95,41 +95,6 @@ public String getLogsBloom() { } // - @Override - public boolean equals(Object o) { - if (this == o) - return true; - if (!(o instanceof ReceiptProxy)) - return false; - ReceiptProxy that = (ReceiptProxy) o; - return Objects.equals(blockNumber, that.blockNumber) && Objects.equals(blockHash, that.blockHash) - && Objects.equals(transactionHash, that.transactionHash) - && Objects.equals(transactionIndex, that.transactionIndex); - } - - @Override - public int hashCode() { - return Objects.hash(blockNumber, blockHash, transactionHash, transactionIndex); - } - - @Override - public String toString() { - return "ReceiptProxy{" + - "root=" + root + - ", from=" + from + - ", to=" + to + - ", blockNumber=" + blockNumber + - ", blockHash=" + blockHash + - ", transactionHash=" + transactionHash + - ", transactionIndex=" + transactionIndex + - ", gasUsed=" + gasUsed + - ", cumulativeGasUsed=" + cumulativeGasUsed + - ", contractAddress=" + contractAddress + - ", logs=" + logs + - ", logsBloom=" + logsBloom + - '}'; - } - public static ReceiptProxyBuilder builder() { return new ReceiptProxyBuilder(); } @@ -234,4 +199,50 @@ public ReceiptProxy build() { return receiptProxy; } } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + ReceiptProxy that = (ReceiptProxy) o; + return Objects.equals(root, that.root) && Objects.equals(from, that.from) && Objects.equals(to, that.to) + && Objects.equals(blockNumber, that.blockNumber) && Objects.equals(_blockNumber, that._blockNumber) + && Objects.equals(blockHash, that.blockHash) && Objects.equals(transactionHash, that.transactionHash) + && Objects.equals(transactionIndex, that.transactionIndex) + && Objects.equals(_transactionIndex, that._transactionIndex) && Objects.equals(gasUsed, that.gasUsed) + && Objects.equals(_gasUsed, that._gasUsed) && Objects.equals(cumulativeGasUsed, that.cumulativeGasUsed) + && Objects.equals(_cumulativeGasUsed, that._cumulativeGasUsed) + && Objects.equals(contractAddress, that.contractAddress) && Objects.equals(logs, that.logs) + && Objects.equals(logsBloom, that.logsBloom); + } + + @Override + public int hashCode() { + return Objects.hash(root, from, to, blockNumber, blockNumber, blockHash, transactionHash, transactionIndex, + transactionIndex, gasUsed, gasUsed, cumulativeGasUsed, cumulativeGasUsed, contractAddress, logs, logsBloom); + } + + @Override + public String toString() { + return "ReceiptProxy{" + + "root=" + root + '\'' + + ", from=" + from + '\'' + + ", to=" + to + '\'' + + ", blockNumber=" + blockNumber + '\'' + + ", blockNumber=" + _blockNumber + + ", blockHash=" + blockHash + '\'' + + ", transactionHash=" + transactionHash + '\'' + + ", transactionIndex=" + transactionIndex + '\'' + + ", transactionIndex=" + _transactionIndex + + ", gasUsed=" + gasUsed + '\'' + + ", gasUsed=" + _gasUsed + + ", cumulativeGasUsed=" + cumulativeGasUsed + '\'' + + ", cumulativeGasUsed=" + _cumulativeGasUsed + + ", contractAddress=" + contractAddress + '\'' + + ", logs=" + logs + + ", logsBloom=" + logsBloom + '\'' + + '}'; + } } diff --git a/src/main/java/io/goodforgod/api/etherscan/model/proxy/TxProxy.java b/src/main/java/io/goodforgod/api/etherscan/model/proxy/TxProxy.java index 1325cc4..4372dde 100644 --- a/src/main/java/io/goodforgod/api/etherscan/model/proxy/TxProxy.java +++ b/src/main/java/io/goodforgod/api/etherscan/model/proxy/TxProxy.java @@ -107,43 +107,6 @@ public Long getBlockNumber() { } // - @Override - public boolean equals(Object o) { - if (this == o) - return true; - if (!(o instanceof TxProxy)) - return false; - TxProxy txProxy = (TxProxy) o; - return Objects.equals(hash, txProxy.hash) && Objects.equals(transactionIndex, txProxy.transactionIndex) - && Objects.equals(nonce, txProxy.nonce) && Objects.equals(blockHash, txProxy.blockHash) - && Objects.equals(blockNumber, txProxy.blockNumber); - } - - @Override - public int hashCode() { - return Objects.hash(hash, transactionIndex, nonce, blockHash, blockNumber); - } - - @Override - public String toString() { - return "TxProxy{" + - "to=" + to + - ", hash=" + hash + - ", transactionIndex=" + transactionIndex + - ", from=" + from + - ", v=" + v + - ", input=" + input + - ", s=" + s + - ", r=" + r + - ", nonce=" + nonce + - ", value=" + value + - ", gas=" + gas + - ", gasPrice=" + gasPrice + - ", blockHash=" + blockHash + - ", blockNumber=" + blockNumber + - '}'; - } - @Override public int compareTo(@NotNull TxProxy o) { final int firstCompare = Long.compare(getBlockNumber(), o.getBlockNumber()); @@ -271,4 +234,53 @@ public TxProxy build() { return txProxy; } } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + TxProxy txProxy = (TxProxy) o; + return Objects.equals(to, txProxy.to) && Objects.equals(hash, txProxy.hash) + && Objects.equals(transactionIndex, txProxy.transactionIndex) + && Objects.equals(_transactionIndex, txProxy._transactionIndex) && Objects.equals(from, txProxy.from) + && Objects.equals(v, txProxy.v) && Objects.equals(input, txProxy.input) && Objects.equals(s, txProxy.s) + && Objects.equals(r, txProxy.r) && Objects.equals(nonce, txProxy.nonce) && Objects.equals(_nonce, txProxy._nonce) + && Objects.equals(value, txProxy.value) && Objects.equals(gas, txProxy.gas) && Objects.equals(_gas, txProxy._gas) + && Objects.equals(gasPrice, txProxy.gasPrice) && Objects.equals(_gasPrice, txProxy._gasPrice) + && Objects.equals(blockHash, txProxy.blockHash) && Objects.equals(blockNumber, txProxy.blockNumber) + && Objects.equals(_blockNumber, txProxy._blockNumber); + } + + @Override + public int hashCode() { + return Objects.hash(to, hash, transactionIndex, transactionIndex, from, v, input, s, r, nonce, nonce, value, gas, gas, + gasPrice, gasPrice, blockHash, blockNumber, blockNumber); + } + + @Override + public String toString() { + return "TxProxy{" + + "to=" + to + '\'' + + ", hash=" + hash + '\'' + + ", transactionIndex=" + transactionIndex + '\'' + + ", transactionIndex=" + _transactionIndex + + ", from=" + from + '\'' + + ", v=" + v + '\'' + + ", input=" + input + '\'' + + ", s=" + s + '\'' + + ", r=" + r + '\'' + + ", nonce=" + nonce + '\'' + + ", nonce=" + _nonce + + ", value=" + value + '\'' + + ", gas=" + gas + '\'' + + ", gas=" + _gas + + ", gasPrice=" + gasPrice + '\'' + + ", gasPrice=" + _gasPrice + + ", blockHash=" + blockHash + '\'' + + ", blockNumber=" + blockNumber + '\'' + + ", blockNumber=" + _blockNumber + + '}'; + } } diff --git a/src/main/java/io/goodforgod/api/etherscan/model/proxy/utility/BaseProxyTO.java b/src/main/java/io/goodforgod/api/etherscan/model/proxy/utility/BaseProxyTO.java index 2960bfc..24588c7 100644 --- a/src/main/java/io/goodforgod/api/etherscan/model/proxy/utility/BaseProxyTO.java +++ b/src/main/java/io/goodforgod/api/etherscan/model/proxy/utility/BaseProxyTO.java @@ -1,5 +1,7 @@ package io.goodforgod.api.etherscan.model.proxy.utility; +import java.util.Objects; + /** * @author GoodforGod * @since 31.10.2018 @@ -21,4 +23,28 @@ public String getJsonrpc() { public ErrorProxyTO getError() { return error; } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + BaseProxyTO that = (BaseProxyTO) o; + return Objects.equals(id, that.id) && Objects.equals(jsonrpc, that.jsonrpc) && Objects.equals(error, that.error); + } + + @Override + public int hashCode() { + return Objects.hash(id, jsonrpc, error); + } + + @Override + public String toString() { + return "BaseProxyTO{" + + "id=" + id + + ", jsonrpc=" + jsonrpc + + ", error=" + error + + '}'; + } } diff --git a/src/main/java/io/goodforgod/api/etherscan/model/proxy/utility/BlockProxyTO.java b/src/main/java/io/goodforgod/api/etherscan/model/proxy/utility/BlockProxyTO.java index cf6c16b..b6bf1c1 100644 --- a/src/main/java/io/goodforgod/api/etherscan/model/proxy/utility/BlockProxyTO.java +++ b/src/main/java/io/goodforgod/api/etherscan/model/proxy/utility/BlockProxyTO.java @@ -1,6 +1,7 @@ package io.goodforgod.api.etherscan.model.proxy.utility; import io.goodforgod.api.etherscan.model.proxy.BlockProxy; +import java.util.Objects; /** * @author GoodforGod @@ -13,4 +14,28 @@ public class BlockProxyTO extends BaseProxyTO { public BlockProxy getResult() { return result; } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + if (!super.equals(o)) + return false; + BlockProxyTO that = (BlockProxyTO) o; + return Objects.equals(result, that.result); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), result); + } + + @Override + public String toString() { + return "BlockProxyTO{" + + "result=" + result + + '}'; + } } diff --git a/src/main/java/io/goodforgod/api/etherscan/model/proxy/utility/ErrorProxyTO.java b/src/main/java/io/goodforgod/api/etherscan/model/proxy/utility/ErrorProxyTO.java index 9b14cec..5aa1f0a 100644 --- a/src/main/java/io/goodforgod/api/etherscan/model/proxy/utility/ErrorProxyTO.java +++ b/src/main/java/io/goodforgod/api/etherscan/model/proxy/utility/ErrorProxyTO.java @@ -1,5 +1,7 @@ package io.goodforgod.api.etherscan.model.proxy.utility; +import java.util.Objects; + /** * @author GoodforGod * @since 03.11.2018 @@ -16,4 +18,27 @@ public String getMessage() { public String getCode() { return code; } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + ErrorProxyTO that = (ErrorProxyTO) o; + return Objects.equals(message, that.message) && Objects.equals(code, that.code); + } + + @Override + public int hashCode() { + return Objects.hash(message, code); + } + + @Override + public String toString() { + return "ErrorProxyTO{" + + "message=" + message + + ", code=" + code + + '}'; + } } diff --git a/src/main/java/io/goodforgod/api/etherscan/model/proxy/utility/StringProxyTO.java b/src/main/java/io/goodforgod/api/etherscan/model/proxy/utility/StringProxyTO.java index 489d87b..7afbf9c 100644 --- a/src/main/java/io/goodforgod/api/etherscan/model/proxy/utility/StringProxyTO.java +++ b/src/main/java/io/goodforgod/api/etherscan/model/proxy/utility/StringProxyTO.java @@ -1,5 +1,7 @@ package io.goodforgod.api.etherscan.model.proxy.utility; +import java.util.Objects; + /** * @author GoodforGod * @since 31.10.2018 @@ -11,4 +13,28 @@ public class StringProxyTO extends BaseProxyTO { public String getResult() { return result; } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + if (!super.equals(o)) + return false; + StringProxyTO that = (StringProxyTO) o; + return Objects.equals(result, that.result); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), result); + } + + @Override + public String toString() { + return "StringProxyTO{" + + "result=" + result + + '}'; + } } diff --git a/src/main/java/io/goodforgod/api/etherscan/model/proxy/utility/TxInfoProxyTO.java b/src/main/java/io/goodforgod/api/etherscan/model/proxy/utility/TxInfoProxyTO.java index 208cdbe..672585b 100644 --- a/src/main/java/io/goodforgod/api/etherscan/model/proxy/utility/TxInfoProxyTO.java +++ b/src/main/java/io/goodforgod/api/etherscan/model/proxy/utility/TxInfoProxyTO.java @@ -1,6 +1,7 @@ package io.goodforgod.api.etherscan.model.proxy.utility; import io.goodforgod.api.etherscan.model.proxy.ReceiptProxy; +import java.util.Objects; /** * @author GoodforGod @@ -13,4 +14,28 @@ public class TxInfoProxyTO extends BaseProxyTO { public ReceiptProxy getResult() { return result; } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + if (!super.equals(o)) + return false; + TxInfoProxyTO that = (TxInfoProxyTO) o; + return Objects.equals(result, that.result); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), result); + } + + @Override + public String toString() { + return "TxInfoProxyTO{" + + "result=" + result + + '}'; + } } diff --git a/src/main/java/io/goodforgod/api/etherscan/model/proxy/utility/TxProxyTO.java b/src/main/java/io/goodforgod/api/etherscan/model/proxy/utility/TxProxyTO.java index 0c084e7..45b885f 100644 --- a/src/main/java/io/goodforgod/api/etherscan/model/proxy/utility/TxProxyTO.java +++ b/src/main/java/io/goodforgod/api/etherscan/model/proxy/utility/TxProxyTO.java @@ -1,6 +1,7 @@ package io.goodforgod.api.etherscan.model.proxy.utility; import io.goodforgod.api.etherscan.model.proxy.TxProxy; +import java.util.Objects; /** * @author GoodforGod @@ -13,4 +14,28 @@ public class TxProxyTO extends BaseProxyTO { public TxProxy getResult() { return result; } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + if (!super.equals(o)) + return false; + TxProxyTO txProxyTO = (TxProxyTO) o; + return Objects.equals(result, txProxyTO.result); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), result); + } + + @Override + public String toString() { + return "TxProxyTO{" + + "result=" + result + + '}'; + } } diff --git a/src/main/java/io/goodforgod/api/etherscan/model/query/LogQueryImpl.java b/src/main/java/io/goodforgod/api/etherscan/model/query/LogQueryImpl.java index 6de9631..6229c76 100644 --- a/src/main/java/io/goodforgod/api/etherscan/model/query/LogQueryImpl.java +++ b/src/main/java/io/goodforgod/api/etherscan/model/query/LogQueryImpl.java @@ -1,6 +1,7 @@ package io.goodforgod.api.etherscan.model.query; import io.goodforgod.api.etherscan.LogsAPI; +import java.util.Objects; import org.jetbrains.annotations.NotNull; /** @@ -27,4 +28,26 @@ public class LogQueryImpl implements LogQuery { public @NotNull String params() { return params; } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + LogQueryImpl logQuery = (LogQueryImpl) o; + return Objects.equals(params, logQuery.params); + } + + @Override + public int hashCode() { + return Objects.hash(params); + } + + @Override + public String toString() { + return "LogQueryImpl{" + + "params=" + params + '\'' + + '}'; + } } diff --git a/src/main/java/io/goodforgod/api/etherscan/model/query/LogTopicQuadro.java b/src/main/java/io/goodforgod/api/etherscan/model/query/LogTopicQuadro.java index f113a4f..ff7bc8a 100644 --- a/src/main/java/io/goodforgod/api/etherscan/model/query/LogTopicQuadro.java +++ b/src/main/java/io/goodforgod/api/etherscan/model/query/LogTopicQuadro.java @@ -4,6 +4,7 @@ import io.goodforgod.api.etherscan.LogsAPI; import io.goodforgod.api.etherscan.error.EtherScanLogQueryException; +import java.util.Objects; import org.jetbrains.annotations.NotNull; /** @@ -96,4 +97,43 @@ public LogTopicQuadro setOpTopic1_3(LogOp topic1_3_opr) { + TOPIC_1_3_OPR_PARAM + topic1_2_opr.getOperation() + TOPIC_2_3_OPR_PARAM + topic0_2_opr.getOperation()); } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + LogTopicQuadro that = (LogTopicQuadro) o; + return startBlock == that.startBlock && endBlock == that.endBlock && Objects.equals(address, that.address) + && Objects.equals(topic0, that.topic0) && Objects.equals(topic1, that.topic1) + && Objects.equals(topic2, that.topic2) && Objects.equals(topic3, that.topic3) && topic0_1_opr == that.topic0_1_opr + && topic1_2_opr == that.topic1_2_opr && topic2_3_opr == that.topic2_3_opr && topic0_2_opr == that.topic0_2_opr + && topic0_3_opr == that.topic0_3_opr && topic1_3_opr == that.topic1_3_opr; + } + + @Override + public int hashCode() { + return Objects.hash(address, startBlock, endBlock, topic0, topic1, topic2, topic3, topic0_1_opr, topic1_2_opr, + topic2_3_opr, topic0_2_opr, topic0_3_opr, topic1_3_opr); + } + + @Override + public String toString() { + return "LogTopicQuadro{" + + "address=" + address + '\'' + + ", startBlock=" + startBlock + + ", endBlock=" + endBlock + + ", topic0=" + topic0 + '\'' + + ", topic1=" + topic1 + '\'' + + ", topic2=" + topic2 + '\'' + + ", topic3=" + topic3 + '\'' + + ", topic0_1_opr=" + topic0_1_opr + + ", topic1_2_opr=" + topic1_2_opr + + ", topic2_3_opr=" + topic2_3_opr + + ", topic0_2_opr=" + topic0_2_opr + + ", topic0_3_opr=" + topic0_3_opr + + ", topic1_3_opr=" + topic1_3_opr + + '}'; + } } diff --git a/src/main/java/io/goodforgod/api/etherscan/model/query/LogTopicSingle.java b/src/main/java/io/goodforgod/api/etherscan/model/query/LogTopicSingle.java index b35c836..58d7bda 100644 --- a/src/main/java/io/goodforgod/api/etherscan/model/query/LogTopicSingle.java +++ b/src/main/java/io/goodforgod/api/etherscan/model/query/LogTopicSingle.java @@ -4,6 +4,7 @@ import io.goodforgod.api.etherscan.LogsAPI; import io.goodforgod.api.etherscan.error.EtherScanLogQueryException; +import java.util.Objects; import org.jetbrains.annotations.NotNull; /** @@ -34,4 +35,30 @@ public class LogTopicSingle implements LogTopicBuilder { + FROM_BLOCK_PARAM + startBlock + TO_BLOCK_PARAM + endBlock + TOPIC_0_PARAM + topic0); } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + LogTopicSingle that = (LogTopicSingle) o; + return startBlock == that.startBlock && endBlock == that.endBlock && Objects.equals(address, that.address) + && Objects.equals(topic0, that.topic0); + } + + @Override + public int hashCode() { + return Objects.hash(address, startBlock, endBlock, topic0); + } + + @Override + public String toString() { + return "LogTopicSingle{" + + "address=" + address + '\'' + + ", startBlock=" + startBlock + + ", endBlock=" + endBlock + + ", topic0=" + topic0 + '\'' + + '}'; + } } diff --git a/src/main/java/io/goodforgod/api/etherscan/model/query/LogTopicTriple.java b/src/main/java/io/goodforgod/api/etherscan/model/query/LogTopicTriple.java index d888a49..b5a25fe 100644 --- a/src/main/java/io/goodforgod/api/etherscan/model/query/LogTopicTriple.java +++ b/src/main/java/io/goodforgod/api/etherscan/model/query/LogTopicTriple.java @@ -4,6 +4,7 @@ import io.goodforgod.api.etherscan.LogsAPI; import io.goodforgod.api.etherscan.error.EtherScanLogQueryException; +import java.util.Objects; import org.jetbrains.annotations.NotNull; /** @@ -69,4 +70,37 @@ public LogTopicTriple setOpTopic1_2(LogOp topic1_2_opr) { + TOPIC_1_2_OPR_PARAM + topic1_2_opr.getOperation() + TOPIC_0_2_OPR_PARAM + topic0_2_opr.getOperation()); } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + LogTopicTriple that = (LogTopicTriple) o; + return startBlock == that.startBlock && endBlock == that.endBlock && Objects.equals(address, that.address) + && Objects.equals(topic0, that.topic0) && Objects.equals(topic1, that.topic1) + && Objects.equals(topic2, that.topic2) && topic0_1_opr == that.topic0_1_opr && topic1_2_opr == that.topic1_2_opr + && topic0_2_opr == that.topic0_2_opr; + } + + @Override + public int hashCode() { + return Objects.hash(address, startBlock, endBlock, topic0, topic1, topic2, topic0_1_opr, topic1_2_opr, topic0_2_opr); + } + + @Override + public String toString() { + return "LogTopicTriple{" + + "address=" + address + '\'' + + ", startBlock=" + startBlock + + ", endBlock=" + endBlock + + ", topic0=" + topic0 + '\'' + + ", topic1=" + topic1 + '\'' + + ", topic2=" + topic2 + '\'' + + ", topic0_1_opr=" + topic0_1_opr + + ", topic1_2_opr=" + topic1_2_opr + + ", topic0_2_opr=" + topic0_2_opr + + '}'; + } } diff --git a/src/main/java/io/goodforgod/api/etherscan/model/query/LogTopicTuple.java b/src/main/java/io/goodforgod/api/etherscan/model/query/LogTopicTuple.java index 4a0218e..e396ace 100644 --- a/src/main/java/io/goodforgod/api/etherscan/model/query/LogTopicTuple.java +++ b/src/main/java/io/goodforgod/api/etherscan/model/query/LogTopicTuple.java @@ -4,6 +4,7 @@ import io.goodforgod.api.etherscan.LogsAPI; import io.goodforgod.api.etherscan.error.EtherScanLogQueryException; +import java.util.Objects; import org.jetbrains.annotations.NotNull; /** @@ -50,4 +51,33 @@ public LogTopicTuple setOpTopic0_1(LogOp topic0_1_opr) { + TOPIC_1_PARAM + topic1 + TOPIC_0_1_OPR_PARAM + topic0_1_opr.getOperation()); } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + LogTopicTuple that = (LogTopicTuple) o; + return startBlock == that.startBlock && endBlock == that.endBlock && Objects.equals(address, that.address) + && Objects.equals(topic0, that.topic0) && Objects.equals(topic1, that.topic1) + && topic0_1_opr == that.topic0_1_opr; + } + + @Override + public int hashCode() { + return Objects.hash(address, startBlock, endBlock, topic0, topic1, topic0_1_opr); + } + + @Override + public String toString() { + return "LogTopicTuple{" + + "address=" + address + '\'' + + ", startBlock=" + startBlock + + ", endBlock=" + endBlock + + ", topic0=" + topic0 + '\'' + + ", topic1=" + topic1 + '\'' + + ", topic0_1_opr=" + topic0_1_opr + + '}'; + } }