From d27d01a51d0d2bab653ee0f11b37bce7282ea55d Mon Sep 17 00:00:00 2001 From: iSnow <139699+iSnow@users.noreply.github.com> Date: Thu, 9 Jul 2020 22:01:21 +0200 Subject: [PATCH 01/17] Method to get an event time stamp as milliseconds since the Unix epoch to avoid time zone calculations --- .gitignore | 32 ++++++++++++++++++- src/main/java/io/api/etherscan/model/Log.java | 15 ++++++++- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 1062418..719a4de 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,32 @@ -.idea/ +# Compiled class files +*.class + +# Log file +*.log +**/.log + +# IntelliJ *.iml +/.idea + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +# other +/bin/ +/.classpath +/.project +/target/ +/out/ +/.DS_Store +/.settings/ + diff --git a/src/main/java/io/api/etherscan/model/Log.java b/src/main/java/io/api/etherscan/model/Log.java index cf485fd..f1535b3 100644 --- a/src/main/java/io/api/etherscan/model/Log.java +++ b/src/main/java/io/api/etherscan/model/Log.java @@ -60,12 +60,25 @@ public LocalDateTime getTimeStamp() { if(_timeStamp == null && !BasicUtils.isEmpty(timeStamp)) { long formatted = (timeStamp.charAt(0) == '0' && timeStamp.charAt(1) == 'x') ? BasicUtils.parseHex(timeStamp).longValue() - : Long.valueOf(timeStamp); + : Long.parseLong(timeStamp); _timeStamp = LocalDateTime.ofEpochSecond(formatted, 0, ZoneOffset.UTC); } return _timeStamp; } + /** + * + * @return + */ + public Long getTimeStampAsMillis() { + if (BasicUtils.isEmpty(timeStamp)) { + return null; + } + return (timeStamp.charAt(0) == '0' && timeStamp.charAt(1) == 'x') + ? BasicUtils.parseHex(timeStamp).longValue() + : Long.parseLong(timeStamp) * 1000; + } + public String getData() { return data; } From cf2d3861ad38b6a390b369f35c0cabcea281f837 Mon Sep 17 00:00:00 2001 From: Johannes Jander <139699+iSnow@users.noreply.github.com> Date: Thu, 9 Jul 2020 22:04:03 +0200 Subject: [PATCH 02/17] Added Jitpack link for snapshots --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index f62f766..a7f90b1 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ ![travis](https://travis-ci.org/GoodforGod/java-etherscan-api.svg?branch=master) [![Maintainability](https://api.codeclimate.com/v1/badges/808997be2e69ff1ae8fe/maintainability)](https://codeclimate.com/github/GoodforGod/java-etherscan-api/maintainability) [![codecov](https://codecov.io/gh/GoodforGod/java-etherscan-api/branch/master/graph/badge.svg)](https://codecov.io/gh/GoodforGod/java-etherscan-api) +[![](https://jitpack.io/v/iSnow/java-etherscan-api.svg)](https://jitpack.io/#iSnow/java-etherscan-api) [Etherscan](https://etherscan.io/apis) Java API implementation. From 8de0601e788773696f583bbe4467c938ac494e73 Mon Sep 17 00:00:00 2001 From: iSnow <139699+iSnow@users.noreply.github.com> Date: Thu, 9 Jul 2020 22:11:13 +0200 Subject: [PATCH 03/17] Method to get an event time stamp as milliseconds since the Unix epoch to avoid time zone calculations --- src/main/java/io/api/etherscan/model/Log.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/main/java/io/api/etherscan/model/Log.java b/src/main/java/io/api/etherscan/model/Log.java index f1535b3..2fa58ee 100644 --- a/src/main/java/io/api/etherscan/model/Log.java +++ b/src/main/java/io/api/etherscan/model/Log.java @@ -67,16 +67,18 @@ public LocalDateTime getTimeStamp() { } /** - * - * @return + * Return the "timeStamp" field of the event record as a long-int representing the milliseconds + * since the Unix epoch (1970-01-01 00:00:00). + * @return milliseconds between Unix epoch and `timeStamp`. If field is empty or null, returns null */ public Long getTimeStampAsMillis() { if (BasicUtils.isEmpty(timeStamp)) { return null; } - return (timeStamp.charAt(0) == '0' && timeStamp.charAt(1) == 'x') + long tsSecs = (timeStamp.charAt(0) == '0' && timeStamp.charAt(1) == 'x') ? BasicUtils.parseHex(timeStamp).longValue() - : Long.parseLong(timeStamp) * 1000; + : Long.parseLong(timeStamp); + return tsSecs * 1000; } public String getData() { From 235be0d2021420d920a44b60979c7143e5e9d60c Mon Sep 17 00:00:00 2001 From: iSnow <139699+iSnow@users.noreply.github.com> Date: Tue, 14 Jul 2020 15:07:56 +0200 Subject: [PATCH 04/17] Method to get all events without a topic filter --- .../api/etherscan/model/query/impl/LogQueryBuilder.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/io/api/etherscan/model/query/impl/LogQueryBuilder.java b/src/main/java/io/api/etherscan/model/query/impl/LogQueryBuilder.java index d4e10be..1397f15 100644 --- a/src/main/java/io/api/etherscan/model/query/impl/LogQueryBuilder.java +++ b/src/main/java/io/api/etherscan/model/query/impl/LogQueryBuilder.java @@ -2,6 +2,7 @@ import io.api.etherscan.core.ILogsApi; import io.api.etherscan.error.LogQueryException; +import io.api.etherscan.model.query.IQueryBuilder; import io.api.etherscan.util.BasicUtils; /** @@ -12,7 +13,7 @@ * @author GoodforGod * @since 31.10.2018 */ -public class LogQueryBuilder { +public class LogQueryBuilder implements IQueryBuilder { private static final long MIN_BLOCK = 0; private static final long MAX_BLOCK = 99999999999L; @@ -75,4 +76,9 @@ public LogTopicQuadro topic(String topic0, String topic1, String topic2, String return new LogTopicQuadro(address, startBlock, endBlock, topic0, topic1, topic2, topic3); } + + @Override + public LogQuery build() throws LogQueryException { + return new LogQuery("&address=" + this.address + "&fromBlock=" + this.startBlock + "&toBlock=" + this.endBlock); + } } From ec0cf80dc4d79f47f638ea5d56e793ee27b6f358 Mon Sep 17 00:00:00 2001 From: iSnow <139699+iSnow@users.noreply.github.com> Date: Tue, 14 Jul 2020 22:05:33 +0200 Subject: [PATCH 05/17] Higher-level API to read well-known ETH events as polymorphic classes --- .../io/api/etherscan/core/IEventsApi.java | 28 ++++++++++ .../core/impl/EventsApiProvider.java | 55 +++++++++++++++++++ .../etherscan/error/EventModelException.java | 11 ++++ .../io/api/etherscan/model/event/IEvent.java | 32 +++++++++++ .../model/event/impl/ApprovalEvent.java | 10 ++++ .../model/event/impl/DepositEvent.java | 10 ++++ .../api/etherscan/model/event/impl/Event.java | 42 ++++++++++++++ .../etherscan/model/event/impl/MintEvent.java | 11 ++++ .../etherscan/model/event/impl/SyncEvent.java | 10 ++++ .../model/event/impl/TransferErc20Event.java | 31 +++++++++++ .../model/event/impl/WithdrawEvent.java | 10 ++++ 11 files changed, 250 insertions(+) create mode 100644 src/main/java/io/api/etherscan/core/IEventsApi.java create mode 100644 src/main/java/io/api/etherscan/core/impl/EventsApiProvider.java create mode 100644 src/main/java/io/api/etherscan/error/EventModelException.java create mode 100644 src/main/java/io/api/etherscan/model/event/IEvent.java create mode 100644 src/main/java/io/api/etherscan/model/event/impl/ApprovalEvent.java create mode 100644 src/main/java/io/api/etherscan/model/event/impl/DepositEvent.java create mode 100644 src/main/java/io/api/etherscan/model/event/impl/Event.java create mode 100644 src/main/java/io/api/etherscan/model/event/impl/MintEvent.java create mode 100644 src/main/java/io/api/etherscan/model/event/impl/SyncEvent.java create mode 100644 src/main/java/io/api/etherscan/model/event/impl/TransferErc20Event.java create mode 100644 src/main/java/io/api/etherscan/model/event/impl/WithdrawEvent.java diff --git a/src/main/java/io/api/etherscan/core/IEventsApi.java b/src/main/java/io/api/etherscan/core/IEventsApi.java new file mode 100644 index 0000000..c5f3665 --- /dev/null +++ b/src/main/java/io/api/etherscan/core/IEventsApi.java @@ -0,0 +1,28 @@ +package io.api.etherscan.core; + +import io.api.etherscan.error.ApiException; +import io.api.etherscan.model.Log; +import io.api.etherscan.model.event.IEvent; +import io.api.etherscan.model.query.impl.LogQuery; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +/** + * EtherScan - API Descriptions + * https://etherscan.io/apis#logs + */ +public interface IEventsApi { + + /** + * This is a high-level alternative to the ILogsApi and an alternative to the native eth_getLogs + * Read at EtherScan API description for full info! + * @param query build log query + * @return logs according to query + * @throws ApiException parent exception class + * + * @see io.api.etherscan.model.query.impl.LogQueryBuilder + */ + @NotNull + List events(LogQuery query) throws ApiException; +} diff --git a/src/main/java/io/api/etherscan/core/impl/EventsApiProvider.java b/src/main/java/io/api/etherscan/core/impl/EventsApiProvider.java new file mode 100644 index 0000000..b99a934 --- /dev/null +++ b/src/main/java/io/api/etherscan/core/impl/EventsApiProvider.java @@ -0,0 +1,55 @@ +package io.api.etherscan.core.impl; + +import io.api.etherscan.core.IEventsApi; +import io.api.etherscan.core.ILogsApi; +import io.api.etherscan.error.ApiException; +import io.api.etherscan.executor.IHttpExecutor; +import io.api.etherscan.manager.IQueueManager; +import io.api.etherscan.model.Log; +import io.api.etherscan.model.event.IEvent; +import io.api.etherscan.model.event.impl.Event; +import io.api.etherscan.model.query.impl.LogQuery; +import io.api.etherscan.model.utility.LogResponseTO; +import io.api.etherscan.util.BasicUtils; +import org.jetbrains.annotations.NotNull; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Logs API Implementation + * + * @see IEventsApi + * + */ +public class EventsApiProvider extends BasicProvider implements IEventsApi { + + private static final String ACT_LOGS_PARAM = ACT_PREFIX + "getLogs"; + + EventsApiProvider(final IQueueManager queue, + final String baseUrl, + final IHttpExecutor executor) { + super(queue, "logs", baseUrl, executor); + } + + @NotNull + @Override + public List events(final LogQuery query) throws ApiException { + final String urlParams = ACT_LOGS_PARAM + query.getParams(); + final LogResponseTO response = getRequest(urlParams, LogResponseTO.class); + BasicUtils.validateTxResponse(response); + + if (BasicUtils.isEmpty(response.getResult())) { + return Collections.emptyList(); + }; + return response + .getResult() + .stream() + .map((log) -> { + String eventTypeHash = log.getTopics().get(0); + return IEvent.createEvent(eventTypeHash, log); + }) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/io/api/etherscan/error/EventModelException.java b/src/main/java/io/api/etherscan/error/EventModelException.java new file mode 100644 index 0000000..5c3e17e --- /dev/null +++ b/src/main/java/io/api/etherscan/error/EventModelException.java @@ -0,0 +1,11 @@ +package io.api.etherscan.error; + +public class EventModelException extends ApiException { + public EventModelException(String message) { + super(message); + } + + public EventModelException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/io/api/etherscan/model/event/IEvent.java b/src/main/java/io/api/etherscan/model/event/IEvent.java new file mode 100644 index 0000000..0fb0e65 --- /dev/null +++ b/src/main/java/io/api/etherscan/model/event/IEvent.java @@ -0,0 +1,32 @@ +package io.api.etherscan.model.event; + +import io.api.etherscan.error.ApiException; +import io.api.etherscan.error.EventModelException; +import io.api.etherscan.model.Log; + +import java.util.HashMap; +import java.util.Map; + +public interface IEvent { + static final Map> subTypes = new HashMap<>(); + + void setLog(Log log); + + static void registerEventType(String typeHash, Class clazz) { + subTypes.put(typeHash, clazz); + } + + static IEvent createEvent(String typeHash, Log log) { + if (null == typeHash) { + throw new EventModelException("Event type hash cannot be null"); + } + Class clazz = subTypes.get(typeHash); + try { + IEvent evt = (IEvent) clazz.newInstance(); + evt.setLog(log); + return evt; + } catch (InstantiationException | IllegalAccessException ex) { + throw new ApiException("Client-side error instantiating Event object", ex); + } + } +} diff --git a/src/main/java/io/api/etherscan/model/event/impl/ApprovalEvent.java b/src/main/java/io/api/etherscan/model/event/impl/ApprovalEvent.java new file mode 100644 index 0000000..915b99c --- /dev/null +++ b/src/main/java/io/api/etherscan/model/event/impl/ApprovalEvent.java @@ -0,0 +1,10 @@ +package io.api.etherscan.model.event.impl; + +import io.api.etherscan.model.event.IEvent; + +public class ApprovalEvent extends Event { + static final String eventTypeHash = "0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925"; + static { + IEvent.registerEventType(ApprovalEvent.eventTypeHash, ApprovalEvent.class); + } +} diff --git a/src/main/java/io/api/etherscan/model/event/impl/DepositEvent.java b/src/main/java/io/api/etherscan/model/event/impl/DepositEvent.java new file mode 100644 index 0000000..31343a5 --- /dev/null +++ b/src/main/java/io/api/etherscan/model/event/impl/DepositEvent.java @@ -0,0 +1,10 @@ +package io.api.etherscan.model.event.impl; + +import io.api.etherscan.model.event.IEvent; + +public class DepositEvent extends Event { + static final String eventTypeHash = "0xe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c"; + static { + IEvent.registerEventType(DepositEvent.eventTypeHash, DepositEvent.class); + } +} diff --git a/src/main/java/io/api/etherscan/model/event/impl/Event.java b/src/main/java/io/api/etherscan/model/event/impl/Event.java new file mode 100644 index 0000000..dbcffcd --- /dev/null +++ b/src/main/java/io/api/etherscan/model/event/impl/Event.java @@ -0,0 +1,42 @@ +package io.api.etherscan.model.event.impl; + +import io.api.etherscan.error.ApiException; +import io.api.etherscan.error.EventModelException; +import io.api.etherscan.model.Log; +import io.api.etherscan.model.event.IEvent; + +import java.util.HashMap; +import java.util.Map; + +/** + * Base class for a higher-level API on top of {@link Log}. Each Event class has an identifying hash + */ +public class Event implements IEvent { + + static String eventTypeHash; + + private Log log; + + String address; + + public static String getEventTypeHash() { + return eventTypeHash; + } + + public Log getLog() { + return log; + } + + public String getAddress() { + return address; + } + + public void setLog(Log log) { + this.log = log; + } + + public void setAddress(String address) { + this.address = address; + } + +} diff --git a/src/main/java/io/api/etherscan/model/event/impl/MintEvent.java b/src/main/java/io/api/etherscan/model/event/impl/MintEvent.java new file mode 100644 index 0000000..597dd7e --- /dev/null +++ b/src/main/java/io/api/etherscan/model/event/impl/MintEvent.java @@ -0,0 +1,11 @@ +package io.api.etherscan.model.event.impl; + +import io.api.etherscan.model.event.IEvent; + +public class MintEvent extends Event { + static final String eventTypeHash = "0x4c209b5fc8ad50758f13e2e1088ba56a560dff690a1c6fef26394f4c03821c4f"; + static { + IEvent.registerEventType(MintEvent.eventTypeHash, MintEvent.class); + } + +} diff --git a/src/main/java/io/api/etherscan/model/event/impl/SyncEvent.java b/src/main/java/io/api/etherscan/model/event/impl/SyncEvent.java new file mode 100644 index 0000000..ff566f4 --- /dev/null +++ b/src/main/java/io/api/etherscan/model/event/impl/SyncEvent.java @@ -0,0 +1,10 @@ +package io.api.etherscan.model.event.impl; + +import io.api.etherscan.model.event.IEvent; + +public class SyncEvent extends Event { + static final String eventTypeHash = "0x1c411e9a96e071241c2f21f7726b17ae89e3cab4c78be50e062b03a9fffbbad1"; + static { + IEvent.registerEventType(SyncEvent.eventTypeHash, SyncEvent.class); + } +} diff --git a/src/main/java/io/api/etherscan/model/event/impl/TransferErc20Event.java b/src/main/java/io/api/etherscan/model/event/impl/TransferErc20Event.java new file mode 100644 index 0000000..a1a8be0 --- /dev/null +++ b/src/main/java/io/api/etherscan/model/event/impl/TransferErc20Event.java @@ -0,0 +1,31 @@ +package io.api.etherscan.model.event.impl; + +import io.api.etherscan.model.event.IEvent; + +public class TransferErc20Event extends Event { + static final String eventTypeHash = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"; + static { + IEvent.registerEventType(TransferErc20Event.eventTypeHash, TransferErc20Event.class); + } + + String fromAddress; + + String toAddress; + + public String getFromAddress() { + return fromAddress; + } + + public void setFromAddress(String fromAddress) { + this.fromAddress = fromAddress; + } + + public String getToAddress() { + return toAddress; + } + + public void setToAddress(String toAddress) { + this.toAddress = toAddress; + } + +} diff --git a/src/main/java/io/api/etherscan/model/event/impl/WithdrawEvent.java b/src/main/java/io/api/etherscan/model/event/impl/WithdrawEvent.java new file mode 100644 index 0000000..3fa442b --- /dev/null +++ b/src/main/java/io/api/etherscan/model/event/impl/WithdrawEvent.java @@ -0,0 +1,10 @@ +package io.api.etherscan.model.event.impl; + +import io.api.etherscan.model.event.IEvent; + +public class WithdrawEvent extends Event { + static final String eventTypeHash = "0x7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65"; + static { + IEvent.registerEventType(WithdrawEvent.eventTypeHash, WithdrawEvent.class); + } +} From 28932837fc84a959e071cc2a264fc70894ea031a Mon Sep 17 00:00:00 2001 From: iSnow <139699+iSnow@users.noreply.github.com> Date: Mon, 5 Oct 2020 17:01:10 +0200 Subject: [PATCH 06/17] Added better handling of communication errors --- .../java/io/api/etherscan/error/ConnectionException.java | 4 ++++ .../java/io/api/etherscan/executor/impl/HttpExecutor.java | 7 +++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/main/java/io/api/etherscan/error/ConnectionException.java b/src/main/java/io/api/etherscan/error/ConnectionException.java index c22955c..410c0ac 100644 --- a/src/main/java/io/api/etherscan/error/ConnectionException.java +++ b/src/main/java/io/api/etherscan/error/ConnectionException.java @@ -8,6 +8,10 @@ */ public class ConnectionException extends ApiException { + public ConnectionException(String message) { + super(message); + } + public ConnectionException(String message, Throwable cause) { super(message, cause); } diff --git a/src/main/java/io/api/etherscan/executor/impl/HttpExecutor.java b/src/main/java/io/api/etherscan/executor/impl/HttpExecutor.java index 3a33515..c059d27 100644 --- a/src/main/java/io/api/etherscan/executor/impl/HttpExecutor.java +++ b/src/main/java/io/api/etherscan/executor/impl/HttpExecutor.java @@ -18,8 +18,7 @@ import java.util.zip.GZIPInputStream; import java.util.zip.InflaterInputStream; -import static java.net.HttpURLConnection.HTTP_MOVED_PERM; -import static java.net.HttpURLConnection.HTTP_MOVED_TEMP; +import static java.net.HttpURLConnection.*; /** * Http client implementation @@ -88,6 +87,10 @@ public String get(final String urlAsString) { final int status = connection.getResponseCode(); if (status == HTTP_MOVED_TEMP || status == HTTP_MOVED_PERM) { return get(connection.getHeaderField("Location")); + } else if ((status >= HTTP_BAD_REQUEST) && (status < HTTP_INTERNAL_ERROR)) { + throw new ConnectionException("Protocol error: "+connection.getResponseMessage()); + } else if (status >= HTTP_INTERNAL_ERROR) { + throw new ConnectionException("Server error: "+connection.getResponseMessage()); } final String data = readData(connection); From 6494764af52c83b24c76e665a1a3aa052b2eef95 Mon Sep 17 00:00:00 2001 From: iSnow <139699+iSnow@users.noreply.github.com> Date: Mon, 5 Oct 2020 17:05:16 +0200 Subject: [PATCH 07/17] Removed events stuff again, moved to different library --- .../io/api/etherscan/core/IEventsApi.java | 28 -- .../core/impl/AccountApiProvider.java | 243 ------------------ .../etherscan/core/impl/BasicProvider.java | 72 ------ .../etherscan/core/impl/BlockApiProvider.java | 48 ---- .../core/impl/ContractApiProvider.java | 47 ---- .../api/etherscan/core/impl/EtherScanApi.java | 120 --------- .../core/impl/EventsApiProvider.java | 55 ---- .../etherscan/core/impl/LogsApiProvider.java | 45 ---- .../etherscan/core/impl/ProxyApiProvider.java | 224 ---------------- .../core/impl/StatisticApiProvider.java | 71 ----- .../core/impl/TransactionApiProvider.java | 60 ----- .../etherscan/error/EventModelException.java | 11 - .../io/api/etherscan/model/event/IEvent.java | 32 --- .../model/event/impl/ApprovalEvent.java | 10 - .../model/event/impl/DepositEvent.java | 10 - .../api/etherscan/model/event/impl/Event.java | 42 --- .../etherscan/model/event/impl/MintEvent.java | 11 - .../etherscan/model/event/impl/SyncEvent.java | 10 - .../model/event/impl/TransferErc20Event.java | 31 --- .../model/event/impl/WithdrawEvent.java | 10 - 20 files changed, 1180 deletions(-) delete mode 100644 src/main/java/io/api/etherscan/core/IEventsApi.java delete mode 100644 src/main/java/io/api/etherscan/core/impl/AccountApiProvider.java delete mode 100644 src/main/java/io/api/etherscan/core/impl/BasicProvider.java delete mode 100644 src/main/java/io/api/etherscan/core/impl/BlockApiProvider.java delete mode 100644 src/main/java/io/api/etherscan/core/impl/ContractApiProvider.java delete mode 100644 src/main/java/io/api/etherscan/core/impl/EtherScanApi.java delete mode 100644 src/main/java/io/api/etherscan/core/impl/EventsApiProvider.java delete mode 100644 src/main/java/io/api/etherscan/core/impl/LogsApiProvider.java delete mode 100644 src/main/java/io/api/etherscan/core/impl/ProxyApiProvider.java delete mode 100644 src/main/java/io/api/etherscan/core/impl/StatisticApiProvider.java delete mode 100644 src/main/java/io/api/etherscan/core/impl/TransactionApiProvider.java delete mode 100644 src/main/java/io/api/etherscan/error/EventModelException.java delete mode 100644 src/main/java/io/api/etherscan/model/event/IEvent.java delete mode 100644 src/main/java/io/api/etherscan/model/event/impl/ApprovalEvent.java delete mode 100644 src/main/java/io/api/etherscan/model/event/impl/DepositEvent.java delete mode 100644 src/main/java/io/api/etherscan/model/event/impl/Event.java delete mode 100644 src/main/java/io/api/etherscan/model/event/impl/MintEvent.java delete mode 100644 src/main/java/io/api/etherscan/model/event/impl/SyncEvent.java delete mode 100644 src/main/java/io/api/etherscan/model/event/impl/TransferErc20Event.java delete mode 100644 src/main/java/io/api/etherscan/model/event/impl/WithdrawEvent.java diff --git a/src/main/java/io/api/etherscan/core/IEventsApi.java b/src/main/java/io/api/etherscan/core/IEventsApi.java deleted file mode 100644 index c5f3665..0000000 --- a/src/main/java/io/api/etherscan/core/IEventsApi.java +++ /dev/null @@ -1,28 +0,0 @@ -package io.api.etherscan.core; - -import io.api.etherscan.error.ApiException; -import io.api.etherscan.model.Log; -import io.api.etherscan.model.event.IEvent; -import io.api.etherscan.model.query.impl.LogQuery; -import org.jetbrains.annotations.NotNull; - -import java.util.List; - -/** - * EtherScan - API Descriptions - * https://etherscan.io/apis#logs - */ -public interface IEventsApi { - - /** - * This is a high-level alternative to the ILogsApi and an alternative to the native eth_getLogs - * Read at EtherScan API description for full info! - * @param query build log query - * @return logs according to query - * @throws ApiException parent exception class - * - * @see io.api.etherscan.model.query.impl.LogQueryBuilder - */ - @NotNull - List events(LogQuery query) throws ApiException; -} diff --git a/src/main/java/io/api/etherscan/core/impl/AccountApiProvider.java b/src/main/java/io/api/etherscan/core/impl/AccountApiProvider.java deleted file mode 100644 index 195d0f0..0000000 --- a/src/main/java/io/api/etherscan/core/impl/AccountApiProvider.java +++ /dev/null @@ -1,243 +0,0 @@ -package io.api.etherscan.core.impl; - -import io.api.etherscan.core.IAccountApi; -import io.api.etherscan.error.ApiException; -import io.api.etherscan.error.EtherScanException; -import io.api.etherscan.executor.IHttpExecutor; -import io.api.etherscan.manager.IQueueManager; -import io.api.etherscan.model.*; -import io.api.etherscan.model.utility.*; -import io.api.etherscan.util.BasicUtils; -import org.jetbrains.annotations.NotNull; - -import java.math.BigInteger; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.stream.Collectors; - -/** - * Account API Implementation - * - * @see IAccountApi - * - * @author GoodforGod - * @since 28.10.2018 - */ -public class AccountApiProvider extends BasicProvider implements IAccountApi { - - private static final int OFFSET_MAX = 10000; - - private static final String ACT_BALANCE_ACTION = ACT_PREFIX + "balance"; - private static final String ACT_TOKEN_BALANCE_PARAM = ACT_PREFIX + "tokenbalance"; - private static final String ACT_BALANCE_MULTI_ACTION = ACT_PREFIX + "balancemulti"; - private static final String ACT_TX_ACTION = ACT_PREFIX + "txlist"; - private static final String ACT_TX_INTERNAL_ACTION = ACT_PREFIX + "txlistinternal"; - private static final String ACT_TX_TOKEN_ACTION = ACT_PREFIX + "tokentx"; - private static final String ACT_MINED_ACTION = ACT_PREFIX + "getminedblocks"; - - private static final String BLOCK_TYPE_PARAM = "&blocktype=blocks"; - private static final String CONTRACT_PARAM = "&contractaddress="; - private static final String START_BLOCK_PARAM = "&startblock="; - private static final String TAG_LATEST_PARAM = "&tag=latest"; - private static final String END_BLOCK_PARAM = "&endblock="; - private static final String SORT_DESC_PARAM = "&sort=desc"; - private static final String SORT_ASC_PARAM = "&sort=asc"; - private static final String ADDRESS_PARAM = "&address="; - private static final String TXHASH_PARAM = "&txhash="; - private static final String OFFSET_PARAM = "&offset="; - private static final String PAGE_PARAM = "&page="; - - AccountApiProvider(final IQueueManager queueManager, - final String baseUrl, - final IHttpExecutor executor) { - super(queueManager, "account", baseUrl, executor); - } - - @NotNull - @Override - public Balance balance(final String address) throws ApiException { - BasicUtils.validateAddress(address); - - final String urlParams = ACT_BALANCE_ACTION + TAG_LATEST_PARAM + ADDRESS_PARAM + address; - final StringResponseTO response = getRequest(urlParams, StringResponseTO.class); - if (response.getStatus() != 1) - throw new EtherScanException(response.getMessage() + ", with status " + response.getStatus()); - - return new Balance(address, new BigInteger(response.getResult())); - } - - @NotNull - @Override - public TokenBalance balance(final String address, final String contract) throws ApiException { - BasicUtils.validateAddress(address); - BasicUtils.validateAddress(contract); - - final String urlParams = ACT_TOKEN_BALANCE_PARAM + ADDRESS_PARAM + address + CONTRACT_PARAM + contract; - final StringResponseTO response = getRequest(urlParams, StringResponseTO.class); - if (response.getStatus() != 1) - throw new EtherScanException(response.getMessage() + ", with status " + response.getStatus()); - - return new TokenBalance(address, new BigInteger(response.getResult()), contract); - } - - @NotNull - @Override - public List balances(final List addresses) throws ApiException { - if (BasicUtils.isEmpty(addresses)) - return Collections.emptyList(); - - BasicUtils.validateAddresses(addresses); - - // Maximum addresses in batch request - 20 - final List balances = new ArrayList<>(); - 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 BalanceResponseTO response = getRequest(urlParams, BalanceResponseTO.class); - if (response.getStatus() != 1) - throw new EtherScanException(response.getMessage() + ", with status " + response.getStatus()); - - if (!BasicUtils.isEmpty(response.getResult())) - balances.addAll(response.getResult().stream() - .map(Balance::of) - .collect(Collectors.toList())); - } - - return balances; - } - - private String toAddressParam(final List addresses) { - return addresses.stream().collect(Collectors.joining(",")); - } - - @NotNull - @Override - public List txs(final String address) throws ApiException { - return txs(address, MIN_START_BLOCK); - } - - @NotNull - @Override - public List txs(final String address, final long startBlock) throws ApiException { - return txs(address, startBlock, MAX_END_BLOCK); - } - - @NotNull - @Override - public List txs(final String address, final long startBlock, final long endBlock) throws ApiException { - BasicUtils.validateAddress(address); - final BlockParam blocks = BasicUtils.compensateBlocks(startBlock, endBlock); - - final String offsetParam = PAGE_PARAM + "%s" + OFFSET_PARAM + OFFSET_MAX; - final String blockParam = START_BLOCK_PARAM + blocks.start() + END_BLOCK_PARAM + blocks.end(); - final String urlParams = ACT_TX_ACTION + offsetParam + ADDRESS_PARAM + address + blockParam + SORT_ASC_PARAM; - - 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 ApiException { - 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(final String address) throws ApiException { - return txsInternal(address, MIN_START_BLOCK); - } - - @NotNull - @Override - public List txsInternal(final String address, final long startBlock) throws ApiException { - return txsInternal(address, startBlock, MAX_END_BLOCK); - } - - @NotNull - @Override - public List txsInternal(final String address, final long startBlock, final long endBlock) throws ApiException { - BasicUtils.validateAddress(address); - final BlockParam blocks = BasicUtils.compensateBlocks(startBlock, endBlock); - - final String offsetParam = PAGE_PARAM + "%s" + OFFSET_PARAM + OFFSET_MAX; - final String blockParam = START_BLOCK_PARAM + blocks.start() + END_BLOCK_PARAM + blocks.end(); - final String urlParams = ACT_TX_INTERNAL_ACTION + offsetParam + ADDRESS_PARAM + address + blockParam + SORT_ASC_PARAM; - - return getRequestUsingOffset(urlParams, TxInternalResponseTO.class); - } - - @NotNull - @Override - public List txsInternalByHash(final String txhash) throws ApiException { - BasicUtils.validateTxHash(txhash); - - final String urlParams = ACT_TX_INTERNAL_ACTION + TXHASH_PARAM + txhash; - final TxInternalResponseTO response = getRequest(urlParams, TxInternalResponseTO.class); - BasicUtils.validateTxResponse(response); - - return BasicUtils.isEmpty(response.getResult()) - ? Collections.emptyList() - : response.getResult(); - } - - @NotNull - @Override - public List txsToken(final String address) throws ApiException { - return txsToken(address, MIN_START_BLOCK); - } - - @NotNull - @Override - public List txsToken(final String address, final long startBlock) throws ApiException { - return txsToken(address, startBlock, MAX_END_BLOCK); - } - - @NotNull - @Override - public List txsToken(final String address, final long startBlock, final long endBlock) throws ApiException { - BasicUtils.validateAddress(address); - final BlockParam blocks = BasicUtils.compensateBlocks(startBlock, endBlock); - - final String offsetParam = PAGE_PARAM + "%s" + OFFSET_PARAM + OFFSET_MAX; - final String blockParam = START_BLOCK_PARAM + blocks.start() + END_BLOCK_PARAM + blocks.end(); - final String urlParams = ACT_TX_TOKEN_ACTION + offsetParam + ADDRESS_PARAM + address + blockParam + SORT_ASC_PARAM; - - return getRequestUsingOffset(urlParams, TxTokenResponseTO.class); - } - - @NotNull - @Override - public List minedBlocks(final String address) throws ApiException { - BasicUtils.validateAddress(address); - - final String offsetParam = PAGE_PARAM + "%s" + OFFSET_PARAM + OFFSET_MAX; - final String urlParams = ACT_MINED_ACTION + offsetParam + BLOCK_TYPE_PARAM + ADDRESS_PARAM + address; - - return getRequestUsingOffset(urlParams, BlockResponseTO.class); - } -} diff --git a/src/main/java/io/api/etherscan/core/impl/BasicProvider.java b/src/main/java/io/api/etherscan/core/impl/BasicProvider.java deleted file mode 100644 index 5b95f68..0000000 --- a/src/main/java/io/api/etherscan/core/impl/BasicProvider.java +++ /dev/null @@ -1,72 +0,0 @@ -package io.api.etherscan.core.impl; - -import com.google.gson.Gson; -import io.api.etherscan.error.EtherScanException; -import io.api.etherscan.error.ParseException; -import io.api.etherscan.executor.IHttpExecutor; -import io.api.etherscan.manager.IQueueManager; -import io.api.etherscan.util.BasicUtils; - -/** - * Base provider for API Implementations - * - * @author GoodforGod - * @see EtherScanApi - * @since 28.10.2018 - */ -abstract class BasicProvider { - - static final int MAX_END_BLOCK = 999999999; - static final int MIN_START_BLOCK = 0; - - static final String ACT_PREFIX = "&action="; - - private final String module; - private final String baseUrl; - private final IHttpExecutor executor; - private final IQueueManager queue; - private final Gson gson; - - BasicProvider(final IQueueManager queue, - final String module, - final String baseUrl, - final IHttpExecutor executor) { - this.queue = queue; - this.module = "&module=" + module; - this.baseUrl = baseUrl; - this.executor = executor; - this.gson = new Gson(); - } - - T convert(final String json, final Class tClass) { - try { - return gson.fromJson(json, tClass); - } catch (Exception e) { - throw new ParseException(e.getMessage(), e.getCause()); - } - } - - String getRequest(final String urlParameters) { - queue.takeTurn(); - final String url = baseUrl + module + urlParameters; - final String result = executor.get(url); - if (BasicUtils.isEmpty(result)) - throw new EtherScanException("Server returned null value for GET request at URL - " + url); - - return result; - } - - String postRequest(final String urlParameters, final String dataToPost) { - queue.takeTurn(); - final String url = baseUrl + module + urlParameters; - return executor.post(url, dataToPost); - } - - T getRequest(final String urlParameters, final Class tClass) { - return convert(getRequest(urlParameters), tClass); - } - - T postRequest(final String urlParameters, final String dataToPost, final Class tClass) { - return convert(postRequest(urlParameters, dataToPost), tClass); - } -} diff --git a/src/main/java/io/api/etherscan/core/impl/BlockApiProvider.java b/src/main/java/io/api/etherscan/core/impl/BlockApiProvider.java deleted file mode 100644 index d076e18..0000000 --- a/src/main/java/io/api/etherscan/core/impl/BlockApiProvider.java +++ /dev/null @@ -1,48 +0,0 @@ -package io.api.etherscan.core.impl; - -import io.api.etherscan.core.IBlockApi; -import io.api.etherscan.error.ApiException; -import io.api.etherscan.executor.IHttpExecutor; -import io.api.etherscan.manager.IQueueManager; -import io.api.etherscan.model.UncleBlock; -import io.api.etherscan.model.utility.UncleBlockResponseTO; -import io.api.etherscan.util.BasicUtils; -import org.jetbrains.annotations.NotNull; - -import java.util.Optional; - -/** - * Block API Implementation - * - * @see IBlockApi - * - * @author GoodforGod - * @since 28.10.2018 - */ -public class BlockApiProvider extends BasicProvider implements IBlockApi { - - private static final String ACT_BLOCK_PARAM = ACT_PREFIX + "getblockreward"; - - private static final String BLOCKNO_PARAM = "&blockno="; - - BlockApiProvider(final IQueueManager queueManager, - final String baseUrl, - final IHttpExecutor executor) { - super(queueManager, "block", baseUrl, executor); - } - - @NotNull - @Override - public Optional uncles(long blockNumber) throws ApiException { - final String urlParam = ACT_BLOCK_PARAM + BLOCKNO_PARAM + blockNumber; - final String response = getRequest(urlParam); - if(BasicUtils.isEmpty(response) || response.contains("NOTOK")) - return Optional.empty(); - - final UncleBlockResponseTO responseTO = convert(response, UncleBlockResponseTO.class); - BasicUtils.validateTxResponse(responseTO); - return (responseTO.getResult() == null || responseTO.getResult().isEmpty()) - ? Optional.empty() - : Optional.of(responseTO.getResult()); - } -} diff --git a/src/main/java/io/api/etherscan/core/impl/ContractApiProvider.java b/src/main/java/io/api/etherscan/core/impl/ContractApiProvider.java deleted file mode 100644 index 83a6e0a..0000000 --- a/src/main/java/io/api/etherscan/core/impl/ContractApiProvider.java +++ /dev/null @@ -1,47 +0,0 @@ -package io.api.etherscan.core.impl; - -import io.api.etherscan.core.IContractApi; -import io.api.etherscan.error.ApiException; -import io.api.etherscan.error.EtherScanException; -import io.api.etherscan.executor.IHttpExecutor; -import io.api.etherscan.manager.IQueueManager; -import io.api.etherscan.model.Abi; -import io.api.etherscan.model.utility.StringResponseTO; -import io.api.etherscan.util.BasicUtils; -import org.jetbrains.annotations.NotNull; - -/** - * Contract API Implementation - * - * @see IContractApi - * - * @author GoodforGod - * @since 28.10.2018 - */ -public class ContractApiProvider extends BasicProvider implements IContractApi { - - private static final String ACT_ABI_PARAM = ACT_PREFIX + "getabi"; - - private static final String ADDRESS_PARAM = "&address="; - - ContractApiProvider(final IQueueManager queueManager, - final String baseUrl, - final IHttpExecutor executor) { - super(queueManager, "contract", baseUrl, executor); - } - - @NotNull - @Override - public Abi contractAbi(final String address) throws ApiException { - BasicUtils.validateAddress(address); - - final String urlParam = ACT_ABI_PARAM + ADDRESS_PARAM + address; - final StringResponseTO response = getRequest(urlParam, StringResponseTO.class); - if (response.getStatus() != 1 && !"NOTOK".equals(response.getMessage())) - throw new EtherScanException(response.getMessage() + ", with status " + response.getStatus()); - - return (response.getResult().startsWith("Contract sou")) - ? Abi.nonVerified() - : Abi.verified(response.getResult()); - } -} diff --git a/src/main/java/io/api/etherscan/core/impl/EtherScanApi.java b/src/main/java/io/api/etherscan/core/impl/EtherScanApi.java deleted file mode 100644 index 1f4d0b4..0000000 --- a/src/main/java/io/api/etherscan/core/impl/EtherScanApi.java +++ /dev/null @@ -1,120 +0,0 @@ -package io.api.etherscan.core.impl; - -import io.api.etherscan.core.*; -import io.api.etherscan.error.ApiException; -import io.api.etherscan.error.ApiKeyException; -import io.api.etherscan.executor.IHttpExecutor; -import io.api.etherscan.executor.impl.HttpExecutor; -import io.api.etherscan.manager.IQueueManager; -import io.api.etherscan.manager.impl.FakeQueueManager; -import io.api.etherscan.manager.impl.QueueManager; -import io.api.etherscan.model.EthNetwork; -import io.api.etherscan.util.BasicUtils; -import org.jetbrains.annotations.NotNull; - -import java.util.function.Supplier; - -/** - * EtherScan full API Description - * https://etherscan.io/apis - * - * @author GoodforGod - * @since 28.10.2018 - */ -public class EtherScanApi { - - private static final Supplier DEFAULT_SUPPLIER = HttpExecutor::new; - - private final IAccountApi account; - private final IBlockApi block; - private final IContractApi contract; - private final ILogsApi logs; - private final IProxyApi proxy; - private final IStatisticApi stats; - private final ITransactionApi txs; - - public EtherScanApi() { - this("YourApiKeyToken", EthNetwork.MAINNET); - } - - public EtherScanApi(final EthNetwork network) { - this("YourApiKeyToken", network); - } - - public EtherScanApi(final String apiKey) { - this(apiKey, EthNetwork.MAINNET); - } - - public EtherScanApi(final EthNetwork network, - final Supplier executorSupplier) { - this("YourApiKeyToken", network, executorSupplier); - } - - public EtherScanApi(final String apiKey, - final EthNetwork network) { - this(apiKey, network, DEFAULT_SUPPLIER); - } - - public EtherScanApi(final String apiKey, - final EthNetwork network, - final Supplier executorSupplier) { - if (BasicUtils.isBlank(apiKey)) - throw new ApiKeyException("API key can not be null or empty"); - - if(network == null) - throw new ApiException("Ethereum Network is set to NULL value"); - - // EtherScan 5request\sec limit support by queue manager - final IQueueManager masterQueue = (apiKey.equals("YourApiKeyToken")) - ? new FakeQueueManager() - : new QueueManager(5, 1); - - final IHttpExecutor executor = executorSupplier.get(); - - final String ending = (EthNetwork.TOBALABA.equals(network)) ? "com" : "io"; - final String baseUrl = "https://" + network.getDomain() + ".etherscan." + ending + "/api" + "?apikey=" + apiKey; - - this.account = new AccountApiProvider(masterQueue, baseUrl, executor); - this.block = new BlockApiProvider(masterQueue, baseUrl, executor); - this.contract = new ContractApiProvider(masterQueue, baseUrl, executor); - this.logs = new LogsApiProvider(masterQueue, baseUrl, executor); - this.proxy = new ProxyApiProvider(masterQueue, baseUrl, executor); - this.stats = new StatisticApiProvider(masterQueue, baseUrl, executor); - this.txs = new TransactionApiProvider(masterQueue, baseUrl, executor); - } - - @NotNull - public IAccountApi account() { - return account; - } - - @NotNull - public IContractApi contract() { - return contract; - } - - @NotNull - public ITransactionApi txs() { - return txs; - } - - @NotNull - public IBlockApi block() { - return block; - } - - @NotNull - public ILogsApi logs() { - return logs; - } - - @NotNull - public IProxyApi proxy() { - return proxy; - } - - @NotNull - public IStatisticApi stats() { - return stats; - } -} diff --git a/src/main/java/io/api/etherscan/core/impl/EventsApiProvider.java b/src/main/java/io/api/etherscan/core/impl/EventsApiProvider.java deleted file mode 100644 index b99a934..0000000 --- a/src/main/java/io/api/etherscan/core/impl/EventsApiProvider.java +++ /dev/null @@ -1,55 +0,0 @@ -package io.api.etherscan.core.impl; - -import io.api.etherscan.core.IEventsApi; -import io.api.etherscan.core.ILogsApi; -import io.api.etherscan.error.ApiException; -import io.api.etherscan.executor.IHttpExecutor; -import io.api.etherscan.manager.IQueueManager; -import io.api.etherscan.model.Log; -import io.api.etherscan.model.event.IEvent; -import io.api.etherscan.model.event.impl.Event; -import io.api.etherscan.model.query.impl.LogQuery; -import io.api.etherscan.model.utility.LogResponseTO; -import io.api.etherscan.util.BasicUtils; -import org.jetbrains.annotations.NotNull; - -import java.util.Collections; -import java.util.List; -import java.util.stream.Collectors; - -/** - * Logs API Implementation - * - * @see IEventsApi - * - */ -public class EventsApiProvider extends BasicProvider implements IEventsApi { - - private static final String ACT_LOGS_PARAM = ACT_PREFIX + "getLogs"; - - EventsApiProvider(final IQueueManager queue, - final String baseUrl, - final IHttpExecutor executor) { - super(queue, "logs", baseUrl, executor); - } - - @NotNull - @Override - public List events(final LogQuery query) throws ApiException { - final String urlParams = ACT_LOGS_PARAM + query.getParams(); - final LogResponseTO response = getRequest(urlParams, LogResponseTO.class); - BasicUtils.validateTxResponse(response); - - if (BasicUtils.isEmpty(response.getResult())) { - return Collections.emptyList(); - }; - return response - .getResult() - .stream() - .map((log) -> { - String eventTypeHash = log.getTopics().get(0); - return IEvent.createEvent(eventTypeHash, log); - }) - .collect(Collectors.toList()); - } -} diff --git a/src/main/java/io/api/etherscan/core/impl/LogsApiProvider.java b/src/main/java/io/api/etherscan/core/impl/LogsApiProvider.java deleted file mode 100644 index 6086869..0000000 --- a/src/main/java/io/api/etherscan/core/impl/LogsApiProvider.java +++ /dev/null @@ -1,45 +0,0 @@ -package io.api.etherscan.core.impl; - -import io.api.etherscan.core.ILogsApi; -import io.api.etherscan.error.ApiException; -import io.api.etherscan.executor.IHttpExecutor; -import io.api.etherscan.manager.IQueueManager; -import io.api.etherscan.model.Log; -import io.api.etherscan.model.query.impl.LogQuery; -import io.api.etherscan.model.utility.LogResponseTO; -import io.api.etherscan.util.BasicUtils; -import org.jetbrains.annotations.NotNull; - -import java.util.Collections; -import java.util.List; - -/** - * Logs API Implementation - * - * @see ILogsApi - * - * @author GoodforGod - * @since 28.10.2018 - */ -public class LogsApiProvider extends BasicProvider implements ILogsApi { - - private static final String ACT_LOGS_PARAM = ACT_PREFIX + "getLogs"; - - LogsApiProvider(final IQueueManager queue, - final String baseUrl, - final IHttpExecutor executor) { - super(queue, "logs", baseUrl, executor); - } - - @NotNull - @Override - public List logs(final LogQuery query) throws ApiException { - final String urlParams = ACT_LOGS_PARAM + query.getParams(); - final LogResponseTO response = getRequest(urlParams, LogResponseTO.class); - BasicUtils.validateTxResponse(response); - - return (BasicUtils.isEmpty(response.getResult())) - ? Collections.emptyList() - : response.getResult(); - } -} diff --git a/src/main/java/io/api/etherscan/core/impl/ProxyApiProvider.java b/src/main/java/io/api/etherscan/core/impl/ProxyApiProvider.java deleted file mode 100644 index f2376d6..0000000 --- a/src/main/java/io/api/etherscan/core/impl/ProxyApiProvider.java +++ /dev/null @@ -1,224 +0,0 @@ -package io.api.etherscan.core.impl; - -import io.api.etherscan.core.IProxyApi; -import io.api.etherscan.error.ApiException; -import io.api.etherscan.error.EtherScanException; -import io.api.etherscan.error.InvalidDataHexException; -import io.api.etherscan.executor.IHttpExecutor; -import io.api.etherscan.manager.IQueueManager; -import io.api.etherscan.model.proxy.BlockProxy; -import io.api.etherscan.model.proxy.ReceiptProxy; -import io.api.etherscan.model.proxy.TxProxy; -import io.api.etherscan.model.proxy.utility.BlockProxyTO; -import io.api.etherscan.model.proxy.utility.StringProxyTO; -import io.api.etherscan.model.proxy.utility.TxInfoProxyTO; -import io.api.etherscan.model.proxy.utility.TxProxyTO; -import io.api.etherscan.util.BasicUtils; -import org.jetbrains.annotations.NotNull; - -import java.math.BigInteger; -import java.util.Optional; -import java.util.regex.Pattern; - -/** - * Proxy API Implementation - * - * @see IProxyApi - * - * @author GoodforGod - * @since 28.10.2018 - */ -public class ProxyApiProvider extends BasicProvider implements IProxyApi { - - private static final String ACT_BLOCKNO_PARAM = ACT_PREFIX + "eth_blockNumber"; - private static final String ACT_BY_BLOCKNO_PARAM = ACT_PREFIX + "eth_getBlockByNumber"; - private static final String ACT_UNCLE_BY_BLOCKNOINDEX_PARAM = ACT_PREFIX + "eth_getUncleByBlockNumberAndIndex"; - private static final String ACT_BLOCKTX_COUNT_PARAM = ACT_PREFIX + "eth_getBlockTransactionCountByNumber"; - private static final String ACT_TX_BY_HASH_PARAM = ACT_PREFIX + "eth_getTransactionByHash"; - private static final String ACT_TX_BY_BLOCKNOINDEX_PARAM = ACT_PREFIX + "eth_getTransactionByBlockNumberAndIndex"; - private static final String ACT_TX_COUNT_PARAM = ACT_PREFIX + "eth_getTransactionCount"; - private static final String ACT_SEND_RAW_TX_PARAM = ACT_PREFIX + "eth_sendRawTransaction"; - private static final String ACT_TX_RECEIPT_PARAM = ACT_PREFIX + "eth_getTransactionReceipt"; - private static final String ACT_CALL_PARAM = ACT_PREFIX + "eth_call"; - private static final String ACT_CODE_PARAM = ACT_PREFIX + "eth_getCode"; - private static final String ACT_STORAGEAT_PARAM = ACT_PREFIX + "eth_getStorageAt"; - private static final String ACT_GASPRICE_PARAM = ACT_PREFIX + "eth_gasPrice"; - private static final String ACT_ESTIMATEGAS_PARAM = ACT_PREFIX + "eth_estimateGas"; - - private static final String BOOLEAN_PARAM = "&boolean=true"; - private static final String TAG_LAST_PARAM = "&tag=latest"; - private static final String POSITION_PARAM = "&position="; - private static final String ADDRESS_PARAM = "&address="; - private static final String TXHASH_PARAM = "&txhash="; - private static final String INDEX_PARAM = "&index="; - private static final String DATA_PARAM = "&data="; - private static final String GAS_PARAM = "&gas="; - private static final String TAG_PARAM = "&tag="; - private static final String HEX_PARAM = "&hex="; - private static final String TO_PARAM = "&to="; - - private static final Pattern EMPTY_HEX = Pattern.compile("0x0+"); - - ProxyApiProvider(final IQueueManager queue, - final String baseUrl, - final IHttpExecutor executor) { - super(queue, "proxy", baseUrl,executor); - } - - @Override - public long blockNoLast() throws ApiException { - final StringProxyTO response = getRequest(ACT_BLOCKNO_PARAM, StringProxyTO.class); - return (BasicUtils.isEmpty(response.getResult())) - ? -1 - : BasicUtils.parseHex(response.getResult()).longValue(); - } - - @NotNull - @Override - public Optional block(final long blockNo) throws ApiException { - 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); - return Optional.ofNullable(response.getResult()); - } - - @NotNull - @Override - public Optional blockUncle(final long blockNo, final long index) throws ApiException { - final long compBlockNo = BasicUtils.compensateMinBlock(blockNo); - final long compIndex = BasicUtils.compensateMinBlock(index); - - 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); - return Optional.ofNullable(response.getResult()); - } - - @NotNull - @Override - public Optional tx(final String txhash) throws ApiException { - BasicUtils.validateTxHash(txhash); - - final String urlParams = ACT_TX_BY_HASH_PARAM + TXHASH_PARAM + txhash; - final TxProxyTO response = getRequest(urlParams, TxProxyTO.class); - return Optional.ofNullable(response.getResult()); - } - - @NotNull - @Override - public Optional tx(final long blockNo, final long index) throws ApiException { - final long compBlockNo = BasicUtils.compensateMinBlock(blockNo); - final long compIndex = (index < 1) ? 1 : index; - - final String urlParams = ACT_TX_BY_BLOCKNOINDEX_PARAM + TAG_PARAM + compBlockNo + INDEX_PARAM + "0x" + Long.toHexString(compIndex); - final TxProxyTO response = getRequest(urlParams, TxProxyTO.class); - return Optional.ofNullable(response.getResult()); - } - - @Override - public int txCount(final long blockNo) throws ApiException { - 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); - return BasicUtils.parseHex(response.getResult()).intValue(); - } - - @Override - public int txSendCount(final String address) throws ApiException { - BasicUtils.validateAddress(address); - - final String urlParams = ACT_TX_COUNT_PARAM + ADDRESS_PARAM + address + TAG_LAST_PARAM; - final StringProxyTO response = getRequest(urlParams, StringProxyTO.class); - return BasicUtils.parseHex(response.getResult()).intValue(); - } - - @Override - @NotNull - public Optional txSendRaw(final String hexEncodedTx) throws ApiException { - if(BasicUtils.isNotHex(hexEncodedTx)) - throw new InvalidDataHexException("Data is not encoded in hex format - " + hexEncodedTx); - - final String urlParams = ACT_SEND_RAW_TX_PARAM + HEX_PARAM + hexEncodedTx; - final StringProxyTO response = postRequest(urlParams, "", StringProxyTO.class); - if(response.getError() != null) - throw new EtherScanException("Error occurred with code " + response.getError().getCode() - + " with message " + response.getError().getMessage() - + ", error id " + response.getId() + ", jsonRPC " + response.getJsonrpc()); - - return Optional.ofNullable(response.getResult()); - } - - @NotNull - @Override - public Optional txReceipt(final String txhash) throws ApiException { - BasicUtils.validateTxHash(txhash); - - final String urlParams = ACT_TX_RECEIPT_PARAM + TXHASH_PARAM + txhash; - final TxInfoProxyTO response = getRequest(urlParams, TxInfoProxyTO.class); - return Optional.ofNullable(response.getResult()); - } - - @NotNull - @Override - public Optional call(final String address, final String data) throws ApiException { - BasicUtils.validateAddress(address); - if(BasicUtils.isNotHex(data)) - throw new InvalidDataHexException("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); - return Optional.ofNullable (response.getResult()); - } - - @NotNull - @Override - public Optional code(final String address) throws ApiException { - BasicUtils.validateAddress(address); - - final String urlParams = ACT_CODE_PARAM + ADDRESS_PARAM + address + TAG_LAST_PARAM; - final StringProxyTO response = getRequest(urlParams, StringProxyTO.class); - return Optional.ofNullable(response.getResult()); - } - - @NotNull - @Override - public Optional storageAt(final String address, final long position) throws ApiException { - BasicUtils.validateAddress(address); - 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); - return (BasicUtils.isEmpty(response.getResult()) || EMPTY_HEX.matcher(response.getResult()).matches()) - ? Optional.empty() - : Optional.of(response.getResult()); - } - - @NotNull - @Override - public BigInteger gasPrice() throws ApiException { - final StringProxyTO response = getRequest(ACT_GASPRICE_PARAM, StringProxyTO.class); - return (BasicUtils.isEmpty(response.getResult())) - ? BigInteger.valueOf(-1) - : BasicUtils.parseHex(response.getResult()); - } - - @NotNull - @Override - public BigInteger gasEstimated() throws ApiException { - return gasEstimated("606060405260728060106000396000f360606040526000"); - } - - @NotNull - @Override - public BigInteger gasEstimated(final String hexData) throws ApiException { - if(!BasicUtils.isEmpty(hexData) && BasicUtils.isNotHex(hexData)) - throw new InvalidDataHexException("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); - return (BasicUtils.isEmpty(response.getResult())) - ? BigInteger.valueOf(-1) - : BasicUtils.parseHex(response.getResult()); - } -} diff --git a/src/main/java/io/api/etherscan/core/impl/StatisticApiProvider.java b/src/main/java/io/api/etherscan/core/impl/StatisticApiProvider.java deleted file mode 100644 index 0125850..0000000 --- a/src/main/java/io/api/etherscan/core/impl/StatisticApiProvider.java +++ /dev/null @@ -1,71 +0,0 @@ -package io.api.etherscan.core.impl; - -import io.api.etherscan.core.IStatisticApi; -import io.api.etherscan.error.ApiException; -import io.api.etherscan.error.EtherScanException; -import io.api.etherscan.executor.IHttpExecutor; -import io.api.etherscan.manager.IQueueManager; -import io.api.etherscan.model.Price; -import io.api.etherscan.model.Supply; -import io.api.etherscan.model.utility.PriceResponseTO; -import io.api.etherscan.model.utility.StringResponseTO; -import io.api.etherscan.util.BasicUtils; -import org.jetbrains.annotations.NotNull; - -import java.math.BigInteger; - -/** - * Statistic API Implementation - * - * @see IStatisticApi - * - * @author GoodforGod - * @since 28.10.2018 - */ -public class StatisticApiProvider extends BasicProvider implements IStatisticApi { - - private static final String ACT_SUPPLY_PARAM = ACT_PREFIX + "ethsupply"; - private static final String ACT_TOKEN_SUPPLY_PARAM = ACT_PREFIX + "tokensupply"; - private static final String ACT_LASTPRICE_PARAM = ACT_PREFIX + "ethprice"; - - private static final String CONTRACT_ADDRESS_PARAM = "&contractaddress="; - - StatisticApiProvider(final IQueueManager queue, - final String baseUrl, - final IHttpExecutor executor) { - super(queue, "stats", baseUrl, executor); - } - - @NotNull - @Override - public Supply supply() throws ApiException { - final StringResponseTO response = getRequest(ACT_SUPPLY_PARAM, StringResponseTO.class); - if (response.getStatus() != 1) - throw new EtherScanException(response.getMessage() + ", with status " + response.getStatus()); - - return new Supply(new BigInteger(response.getResult())); - } - - @NotNull - @Override - public BigInteger supply(final String contract) throws ApiException { - BasicUtils.validateAddress(contract); - - final String urlParams = ACT_TOKEN_SUPPLY_PARAM + CONTRACT_ADDRESS_PARAM + contract; - final StringResponseTO response = getRequest(urlParams, StringResponseTO.class); - if (response.getStatus() != 1) - throw new EtherScanException(response.getMessage() + ", with status " + response.getStatus()); - - return new BigInteger(response.getResult()); - } - - @NotNull - @Override - public Price lastPrice() throws ApiException { - final PriceResponseTO response = getRequest(ACT_LASTPRICE_PARAM, PriceResponseTO.class); - if (response.getStatus() != 1) - throw new EtherScanException(response.getMessage() + ", with status " + response.getStatus()); - - return response.getResult(); - } -} diff --git a/src/main/java/io/api/etherscan/core/impl/TransactionApiProvider.java b/src/main/java/io/api/etherscan/core/impl/TransactionApiProvider.java deleted file mode 100644 index 82eb467..0000000 --- a/src/main/java/io/api/etherscan/core/impl/TransactionApiProvider.java +++ /dev/null @@ -1,60 +0,0 @@ -package io.api.etherscan.core.impl; - -import io.api.etherscan.core.ITransactionApi; -import io.api.etherscan.error.ApiException; -import io.api.etherscan.executor.IHttpExecutor; -import io.api.etherscan.manager.IQueueManager; -import io.api.etherscan.model.Status; -import io.api.etherscan.model.utility.ReceiptStatusResponseTO; -import io.api.etherscan.model.utility.StatusResponseTO; -import io.api.etherscan.util.BasicUtils; -import org.jetbrains.annotations.NotNull; - -import java.util.Optional; - -/** - * Transaction API Implementation - * - * @author GoodforGod - * @see ITransactionApi - * @since 28.10.2018 - */ -public class TransactionApiProvider extends BasicProvider implements ITransactionApi { - - 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(final IQueueManager queue, - final String baseUrl, - final IHttpExecutor executor) { - super(queue, "transaction", baseUrl, executor); - } - - @NotNull - @Override - public Optional execStatus(final String txhash) throws ApiException { - BasicUtils.validateTxHash(txhash); - - final String urlParams = ACT_EXEC_STATUS_PARAM + TXHASH_PARAM + txhash; - final StatusResponseTO response = getRequest(urlParams, StatusResponseTO.class); - BasicUtils.validateTxResponse(response); - - return Optional.ofNullable(response.getResult()); - } - - @NotNull - @Override - public Optional receiptStatus(final String txhash) throws ApiException { - BasicUtils.validateTxHash(txhash); - - final String urlParams = ACT_RECEIPT_STATUS_PARAM + TXHASH_PARAM + txhash; - final ReceiptStatusResponseTO response = getRequest(urlParams, ReceiptStatusResponseTO.class); - BasicUtils.validateTxResponse(response); - - return (response.getResult() == null || BasicUtils.isEmpty(response.getResult().getStatus())) - ? Optional.empty() - : Optional.of(response.getResult().getStatus().contains("1")); - } -} diff --git a/src/main/java/io/api/etherscan/error/EventModelException.java b/src/main/java/io/api/etherscan/error/EventModelException.java deleted file mode 100644 index 5c3e17e..0000000 --- a/src/main/java/io/api/etherscan/error/EventModelException.java +++ /dev/null @@ -1,11 +0,0 @@ -package io.api.etherscan.error; - -public class EventModelException extends ApiException { - public EventModelException(String message) { - super(message); - } - - public EventModelException(String message, Throwable cause) { - super(message, cause); - } -} diff --git a/src/main/java/io/api/etherscan/model/event/IEvent.java b/src/main/java/io/api/etherscan/model/event/IEvent.java deleted file mode 100644 index 0fb0e65..0000000 --- a/src/main/java/io/api/etherscan/model/event/IEvent.java +++ /dev/null @@ -1,32 +0,0 @@ -package io.api.etherscan.model.event; - -import io.api.etherscan.error.ApiException; -import io.api.etherscan.error.EventModelException; -import io.api.etherscan.model.Log; - -import java.util.HashMap; -import java.util.Map; - -public interface IEvent { - static final Map> subTypes = new HashMap<>(); - - void setLog(Log log); - - static void registerEventType(String typeHash, Class clazz) { - subTypes.put(typeHash, clazz); - } - - static IEvent createEvent(String typeHash, Log log) { - if (null == typeHash) { - throw new EventModelException("Event type hash cannot be null"); - } - Class clazz = subTypes.get(typeHash); - try { - IEvent evt = (IEvent) clazz.newInstance(); - evt.setLog(log); - return evt; - } catch (InstantiationException | IllegalAccessException ex) { - throw new ApiException("Client-side error instantiating Event object", ex); - } - } -} diff --git a/src/main/java/io/api/etherscan/model/event/impl/ApprovalEvent.java b/src/main/java/io/api/etherscan/model/event/impl/ApprovalEvent.java deleted file mode 100644 index 915b99c..0000000 --- a/src/main/java/io/api/etherscan/model/event/impl/ApprovalEvent.java +++ /dev/null @@ -1,10 +0,0 @@ -package io.api.etherscan.model.event.impl; - -import io.api.etherscan.model.event.IEvent; - -public class ApprovalEvent extends Event { - static final String eventTypeHash = "0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925"; - static { - IEvent.registerEventType(ApprovalEvent.eventTypeHash, ApprovalEvent.class); - } -} diff --git a/src/main/java/io/api/etherscan/model/event/impl/DepositEvent.java b/src/main/java/io/api/etherscan/model/event/impl/DepositEvent.java deleted file mode 100644 index 31343a5..0000000 --- a/src/main/java/io/api/etherscan/model/event/impl/DepositEvent.java +++ /dev/null @@ -1,10 +0,0 @@ -package io.api.etherscan.model.event.impl; - -import io.api.etherscan.model.event.IEvent; - -public class DepositEvent extends Event { - static final String eventTypeHash = "0xe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c"; - static { - IEvent.registerEventType(DepositEvent.eventTypeHash, DepositEvent.class); - } -} diff --git a/src/main/java/io/api/etherscan/model/event/impl/Event.java b/src/main/java/io/api/etherscan/model/event/impl/Event.java deleted file mode 100644 index dbcffcd..0000000 --- a/src/main/java/io/api/etherscan/model/event/impl/Event.java +++ /dev/null @@ -1,42 +0,0 @@ -package io.api.etherscan.model.event.impl; - -import io.api.etherscan.error.ApiException; -import io.api.etherscan.error.EventModelException; -import io.api.etherscan.model.Log; -import io.api.etherscan.model.event.IEvent; - -import java.util.HashMap; -import java.util.Map; - -/** - * Base class for a higher-level API on top of {@link Log}. Each Event class has an identifying hash - */ -public class Event implements IEvent { - - static String eventTypeHash; - - private Log log; - - String address; - - public static String getEventTypeHash() { - return eventTypeHash; - } - - public Log getLog() { - return log; - } - - public String getAddress() { - return address; - } - - public void setLog(Log log) { - this.log = log; - } - - public void setAddress(String address) { - this.address = address; - } - -} diff --git a/src/main/java/io/api/etherscan/model/event/impl/MintEvent.java b/src/main/java/io/api/etherscan/model/event/impl/MintEvent.java deleted file mode 100644 index 597dd7e..0000000 --- a/src/main/java/io/api/etherscan/model/event/impl/MintEvent.java +++ /dev/null @@ -1,11 +0,0 @@ -package io.api.etherscan.model.event.impl; - -import io.api.etherscan.model.event.IEvent; - -public class MintEvent extends Event { - static final String eventTypeHash = "0x4c209b5fc8ad50758f13e2e1088ba56a560dff690a1c6fef26394f4c03821c4f"; - static { - IEvent.registerEventType(MintEvent.eventTypeHash, MintEvent.class); - } - -} diff --git a/src/main/java/io/api/etherscan/model/event/impl/SyncEvent.java b/src/main/java/io/api/etherscan/model/event/impl/SyncEvent.java deleted file mode 100644 index ff566f4..0000000 --- a/src/main/java/io/api/etherscan/model/event/impl/SyncEvent.java +++ /dev/null @@ -1,10 +0,0 @@ -package io.api.etherscan.model.event.impl; - -import io.api.etherscan.model.event.IEvent; - -public class SyncEvent extends Event { - static final String eventTypeHash = "0x1c411e9a96e071241c2f21f7726b17ae89e3cab4c78be50e062b03a9fffbbad1"; - static { - IEvent.registerEventType(SyncEvent.eventTypeHash, SyncEvent.class); - } -} diff --git a/src/main/java/io/api/etherscan/model/event/impl/TransferErc20Event.java b/src/main/java/io/api/etherscan/model/event/impl/TransferErc20Event.java deleted file mode 100644 index a1a8be0..0000000 --- a/src/main/java/io/api/etherscan/model/event/impl/TransferErc20Event.java +++ /dev/null @@ -1,31 +0,0 @@ -package io.api.etherscan.model.event.impl; - -import io.api.etherscan.model.event.IEvent; - -public class TransferErc20Event extends Event { - static final String eventTypeHash = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"; - static { - IEvent.registerEventType(TransferErc20Event.eventTypeHash, TransferErc20Event.class); - } - - String fromAddress; - - String toAddress; - - public String getFromAddress() { - return fromAddress; - } - - public void setFromAddress(String fromAddress) { - this.fromAddress = fromAddress; - } - - public String getToAddress() { - return toAddress; - } - - public void setToAddress(String toAddress) { - this.toAddress = toAddress; - } - -} diff --git a/src/main/java/io/api/etherscan/model/event/impl/WithdrawEvent.java b/src/main/java/io/api/etherscan/model/event/impl/WithdrawEvent.java deleted file mode 100644 index 3fa442b..0000000 --- a/src/main/java/io/api/etherscan/model/event/impl/WithdrawEvent.java +++ /dev/null @@ -1,10 +0,0 @@ -package io.api.etherscan.model.event.impl; - -import io.api.etherscan.model.event.IEvent; - -public class WithdrawEvent extends Event { - static final String eventTypeHash = "0x7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65"; - static { - IEvent.registerEventType(WithdrawEvent.eventTypeHash, WithdrawEvent.class); - } -} From 67b695d4882be23629a2dbd7e3b4da644e7a4ff0 Mon Sep 17 00:00:00 2001 From: iSnow <139699+iSnow@users.noreply.github.com> Date: Mon, 5 Oct 2020 17:11:44 +0200 Subject: [PATCH 08/17] Removed events stuff again, moved to different library --- .../core/impl/AccountApiProvider.java | 243 ++++++++++++++++++ .../etherscan/core/impl/BasicProvider.java | 72 ++++++ .../etherscan/core/impl/BlockApiProvider.java | 48 ++++ .../core/impl/ContractApiProvider.java | 47 ++++ .../api/etherscan/core/impl/EtherScanApi.java | 120 +++++++++ .../etherscan/core/impl/LogsApiProvider.java | 45 ++++ .../etherscan/core/impl/ProxyApiProvider.java | 224 ++++++++++++++++ .../core/impl/StatisticApiProvider.java | 71 +++++ .../core/impl/TransactionApiProvider.java | 60 +++++ 9 files changed, 930 insertions(+) create mode 100644 src/main/java/io/api/etherscan/core/impl/AccountApiProvider.java create mode 100644 src/main/java/io/api/etherscan/core/impl/BasicProvider.java create mode 100644 src/main/java/io/api/etherscan/core/impl/BlockApiProvider.java create mode 100644 src/main/java/io/api/etherscan/core/impl/ContractApiProvider.java create mode 100644 src/main/java/io/api/etherscan/core/impl/EtherScanApi.java create mode 100644 src/main/java/io/api/etherscan/core/impl/LogsApiProvider.java create mode 100644 src/main/java/io/api/etherscan/core/impl/ProxyApiProvider.java create mode 100644 src/main/java/io/api/etherscan/core/impl/StatisticApiProvider.java create mode 100644 src/main/java/io/api/etherscan/core/impl/TransactionApiProvider.java diff --git a/src/main/java/io/api/etherscan/core/impl/AccountApiProvider.java b/src/main/java/io/api/etherscan/core/impl/AccountApiProvider.java new file mode 100644 index 0000000..195d0f0 --- /dev/null +++ b/src/main/java/io/api/etherscan/core/impl/AccountApiProvider.java @@ -0,0 +1,243 @@ +package io.api.etherscan.core.impl; + +import io.api.etherscan.core.IAccountApi; +import io.api.etherscan.error.ApiException; +import io.api.etherscan.error.EtherScanException; +import io.api.etherscan.executor.IHttpExecutor; +import io.api.etherscan.manager.IQueueManager; +import io.api.etherscan.model.*; +import io.api.etherscan.model.utility.*; +import io.api.etherscan.util.BasicUtils; +import org.jetbrains.annotations.NotNull; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Account API Implementation + * + * @see IAccountApi + * + * @author GoodforGod + * @since 28.10.2018 + */ +public class AccountApiProvider extends BasicProvider implements IAccountApi { + + private static final int OFFSET_MAX = 10000; + + private static final String ACT_BALANCE_ACTION = ACT_PREFIX + "balance"; + private static final String ACT_TOKEN_BALANCE_PARAM = ACT_PREFIX + "tokenbalance"; + private static final String ACT_BALANCE_MULTI_ACTION = ACT_PREFIX + "balancemulti"; + private static final String ACT_TX_ACTION = ACT_PREFIX + "txlist"; + private static final String ACT_TX_INTERNAL_ACTION = ACT_PREFIX + "txlistinternal"; + private static final String ACT_TX_TOKEN_ACTION = ACT_PREFIX + "tokentx"; + private static final String ACT_MINED_ACTION = ACT_PREFIX + "getminedblocks"; + + private static final String BLOCK_TYPE_PARAM = "&blocktype=blocks"; + private static final String CONTRACT_PARAM = "&contractaddress="; + private static final String START_BLOCK_PARAM = "&startblock="; + private static final String TAG_LATEST_PARAM = "&tag=latest"; + private static final String END_BLOCK_PARAM = "&endblock="; + private static final String SORT_DESC_PARAM = "&sort=desc"; + private static final String SORT_ASC_PARAM = "&sort=asc"; + private static final String ADDRESS_PARAM = "&address="; + private static final String TXHASH_PARAM = "&txhash="; + private static final String OFFSET_PARAM = "&offset="; + private static final String PAGE_PARAM = "&page="; + + AccountApiProvider(final IQueueManager queueManager, + final String baseUrl, + final IHttpExecutor executor) { + super(queueManager, "account", baseUrl, executor); + } + + @NotNull + @Override + public Balance balance(final String address) throws ApiException { + BasicUtils.validateAddress(address); + + final String urlParams = ACT_BALANCE_ACTION + TAG_LATEST_PARAM + ADDRESS_PARAM + address; + final StringResponseTO response = getRequest(urlParams, StringResponseTO.class); + if (response.getStatus() != 1) + throw new EtherScanException(response.getMessage() + ", with status " + response.getStatus()); + + return new Balance(address, new BigInteger(response.getResult())); + } + + @NotNull + @Override + public TokenBalance balance(final String address, final String contract) throws ApiException { + BasicUtils.validateAddress(address); + BasicUtils.validateAddress(contract); + + final String urlParams = ACT_TOKEN_BALANCE_PARAM + ADDRESS_PARAM + address + CONTRACT_PARAM + contract; + final StringResponseTO response = getRequest(urlParams, StringResponseTO.class); + if (response.getStatus() != 1) + throw new EtherScanException(response.getMessage() + ", with status " + response.getStatus()); + + return new TokenBalance(address, new BigInteger(response.getResult()), contract); + } + + @NotNull + @Override + public List balances(final List addresses) throws ApiException { + if (BasicUtils.isEmpty(addresses)) + return Collections.emptyList(); + + BasicUtils.validateAddresses(addresses); + + // Maximum addresses in batch request - 20 + final List balances = new ArrayList<>(); + 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 BalanceResponseTO response = getRequest(urlParams, BalanceResponseTO.class); + if (response.getStatus() != 1) + throw new EtherScanException(response.getMessage() + ", with status " + response.getStatus()); + + if (!BasicUtils.isEmpty(response.getResult())) + balances.addAll(response.getResult().stream() + .map(Balance::of) + .collect(Collectors.toList())); + } + + return balances; + } + + private String toAddressParam(final List addresses) { + return addresses.stream().collect(Collectors.joining(",")); + } + + @NotNull + @Override + public List txs(final String address) throws ApiException { + return txs(address, MIN_START_BLOCK); + } + + @NotNull + @Override + public List txs(final String address, final long startBlock) throws ApiException { + return txs(address, startBlock, MAX_END_BLOCK); + } + + @NotNull + @Override + public List txs(final String address, final long startBlock, final long endBlock) throws ApiException { + BasicUtils.validateAddress(address); + final BlockParam blocks = BasicUtils.compensateBlocks(startBlock, endBlock); + + final String offsetParam = PAGE_PARAM + "%s" + OFFSET_PARAM + OFFSET_MAX; + final String blockParam = START_BLOCK_PARAM + blocks.start() + END_BLOCK_PARAM + blocks.end(); + final String urlParams = ACT_TX_ACTION + offsetParam + ADDRESS_PARAM + address + blockParam + SORT_ASC_PARAM; + + 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 ApiException { + 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(final String address) throws ApiException { + return txsInternal(address, MIN_START_BLOCK); + } + + @NotNull + @Override + public List txsInternal(final String address, final long startBlock) throws ApiException { + return txsInternal(address, startBlock, MAX_END_BLOCK); + } + + @NotNull + @Override + public List txsInternal(final String address, final long startBlock, final long endBlock) throws ApiException { + BasicUtils.validateAddress(address); + final BlockParam blocks = BasicUtils.compensateBlocks(startBlock, endBlock); + + final String offsetParam = PAGE_PARAM + "%s" + OFFSET_PARAM + OFFSET_MAX; + final String blockParam = START_BLOCK_PARAM + blocks.start() + END_BLOCK_PARAM + blocks.end(); + final String urlParams = ACT_TX_INTERNAL_ACTION + offsetParam + ADDRESS_PARAM + address + blockParam + SORT_ASC_PARAM; + + return getRequestUsingOffset(urlParams, TxInternalResponseTO.class); + } + + @NotNull + @Override + public List txsInternalByHash(final String txhash) throws ApiException { + BasicUtils.validateTxHash(txhash); + + final String urlParams = ACT_TX_INTERNAL_ACTION + TXHASH_PARAM + txhash; + final TxInternalResponseTO response = getRequest(urlParams, TxInternalResponseTO.class); + BasicUtils.validateTxResponse(response); + + return BasicUtils.isEmpty(response.getResult()) + ? Collections.emptyList() + : response.getResult(); + } + + @NotNull + @Override + public List txsToken(final String address) throws ApiException { + return txsToken(address, MIN_START_BLOCK); + } + + @NotNull + @Override + public List txsToken(final String address, final long startBlock) throws ApiException { + return txsToken(address, startBlock, MAX_END_BLOCK); + } + + @NotNull + @Override + public List txsToken(final String address, final long startBlock, final long endBlock) throws ApiException { + BasicUtils.validateAddress(address); + final BlockParam blocks = BasicUtils.compensateBlocks(startBlock, endBlock); + + final String offsetParam = PAGE_PARAM + "%s" + OFFSET_PARAM + OFFSET_MAX; + final String blockParam = START_BLOCK_PARAM + blocks.start() + END_BLOCK_PARAM + blocks.end(); + final String urlParams = ACT_TX_TOKEN_ACTION + offsetParam + ADDRESS_PARAM + address + blockParam + SORT_ASC_PARAM; + + return getRequestUsingOffset(urlParams, TxTokenResponseTO.class); + } + + @NotNull + @Override + public List minedBlocks(final String address) throws ApiException { + BasicUtils.validateAddress(address); + + final String offsetParam = PAGE_PARAM + "%s" + OFFSET_PARAM + OFFSET_MAX; + final String urlParams = ACT_MINED_ACTION + offsetParam + BLOCK_TYPE_PARAM + ADDRESS_PARAM + address; + + return getRequestUsingOffset(urlParams, BlockResponseTO.class); + } +} diff --git a/src/main/java/io/api/etherscan/core/impl/BasicProvider.java b/src/main/java/io/api/etherscan/core/impl/BasicProvider.java new file mode 100644 index 0000000..5b95f68 --- /dev/null +++ b/src/main/java/io/api/etherscan/core/impl/BasicProvider.java @@ -0,0 +1,72 @@ +package io.api.etherscan.core.impl; + +import com.google.gson.Gson; +import io.api.etherscan.error.EtherScanException; +import io.api.etherscan.error.ParseException; +import io.api.etherscan.executor.IHttpExecutor; +import io.api.etherscan.manager.IQueueManager; +import io.api.etherscan.util.BasicUtils; + +/** + * Base provider for API Implementations + * + * @author GoodforGod + * @see EtherScanApi + * @since 28.10.2018 + */ +abstract class BasicProvider { + + static final int MAX_END_BLOCK = 999999999; + static final int MIN_START_BLOCK = 0; + + static final String ACT_PREFIX = "&action="; + + private final String module; + private final String baseUrl; + private final IHttpExecutor executor; + private final IQueueManager queue; + private final Gson gson; + + BasicProvider(final IQueueManager queue, + final String module, + final String baseUrl, + final IHttpExecutor executor) { + this.queue = queue; + this.module = "&module=" + module; + this.baseUrl = baseUrl; + this.executor = executor; + this.gson = new Gson(); + } + + T convert(final String json, final Class tClass) { + try { + return gson.fromJson(json, tClass); + } catch (Exception e) { + throw new ParseException(e.getMessage(), e.getCause()); + } + } + + String getRequest(final String urlParameters) { + queue.takeTurn(); + final String url = baseUrl + module + urlParameters; + final String result = executor.get(url); + if (BasicUtils.isEmpty(result)) + throw new EtherScanException("Server returned null value for GET request at URL - " + url); + + return result; + } + + String postRequest(final String urlParameters, final String dataToPost) { + queue.takeTurn(); + final String url = baseUrl + module + urlParameters; + return executor.post(url, dataToPost); + } + + T getRequest(final String urlParameters, final Class tClass) { + return convert(getRequest(urlParameters), tClass); + } + + T postRequest(final String urlParameters, final String dataToPost, final Class tClass) { + return convert(postRequest(urlParameters, dataToPost), tClass); + } +} diff --git a/src/main/java/io/api/etherscan/core/impl/BlockApiProvider.java b/src/main/java/io/api/etherscan/core/impl/BlockApiProvider.java new file mode 100644 index 0000000..d076e18 --- /dev/null +++ b/src/main/java/io/api/etherscan/core/impl/BlockApiProvider.java @@ -0,0 +1,48 @@ +package io.api.etherscan.core.impl; + +import io.api.etherscan.core.IBlockApi; +import io.api.etherscan.error.ApiException; +import io.api.etherscan.executor.IHttpExecutor; +import io.api.etherscan.manager.IQueueManager; +import io.api.etherscan.model.UncleBlock; +import io.api.etherscan.model.utility.UncleBlockResponseTO; +import io.api.etherscan.util.BasicUtils; +import org.jetbrains.annotations.NotNull; + +import java.util.Optional; + +/** + * Block API Implementation + * + * @see IBlockApi + * + * @author GoodforGod + * @since 28.10.2018 + */ +public class BlockApiProvider extends BasicProvider implements IBlockApi { + + private static final String ACT_BLOCK_PARAM = ACT_PREFIX + "getblockreward"; + + private static final String BLOCKNO_PARAM = "&blockno="; + + BlockApiProvider(final IQueueManager queueManager, + final String baseUrl, + final IHttpExecutor executor) { + super(queueManager, "block", baseUrl, executor); + } + + @NotNull + @Override + public Optional uncles(long blockNumber) throws ApiException { + final String urlParam = ACT_BLOCK_PARAM + BLOCKNO_PARAM + blockNumber; + final String response = getRequest(urlParam); + if(BasicUtils.isEmpty(response) || response.contains("NOTOK")) + return Optional.empty(); + + final UncleBlockResponseTO responseTO = convert(response, UncleBlockResponseTO.class); + BasicUtils.validateTxResponse(responseTO); + return (responseTO.getResult() == null || responseTO.getResult().isEmpty()) + ? Optional.empty() + : Optional.of(responseTO.getResult()); + } +} diff --git a/src/main/java/io/api/etherscan/core/impl/ContractApiProvider.java b/src/main/java/io/api/etherscan/core/impl/ContractApiProvider.java new file mode 100644 index 0000000..83a6e0a --- /dev/null +++ b/src/main/java/io/api/etherscan/core/impl/ContractApiProvider.java @@ -0,0 +1,47 @@ +package io.api.etherscan.core.impl; + +import io.api.etherscan.core.IContractApi; +import io.api.etherscan.error.ApiException; +import io.api.etherscan.error.EtherScanException; +import io.api.etherscan.executor.IHttpExecutor; +import io.api.etherscan.manager.IQueueManager; +import io.api.etherscan.model.Abi; +import io.api.etherscan.model.utility.StringResponseTO; +import io.api.etherscan.util.BasicUtils; +import org.jetbrains.annotations.NotNull; + +/** + * Contract API Implementation + * + * @see IContractApi + * + * @author GoodforGod + * @since 28.10.2018 + */ +public class ContractApiProvider extends BasicProvider implements IContractApi { + + private static final String ACT_ABI_PARAM = ACT_PREFIX + "getabi"; + + private static final String ADDRESS_PARAM = "&address="; + + ContractApiProvider(final IQueueManager queueManager, + final String baseUrl, + final IHttpExecutor executor) { + super(queueManager, "contract", baseUrl, executor); + } + + @NotNull + @Override + public Abi contractAbi(final String address) throws ApiException { + BasicUtils.validateAddress(address); + + final String urlParam = ACT_ABI_PARAM + ADDRESS_PARAM + address; + final StringResponseTO response = getRequest(urlParam, StringResponseTO.class); + if (response.getStatus() != 1 && !"NOTOK".equals(response.getMessage())) + throw new EtherScanException(response.getMessage() + ", with status " + response.getStatus()); + + return (response.getResult().startsWith("Contract sou")) + ? Abi.nonVerified() + : Abi.verified(response.getResult()); + } +} diff --git a/src/main/java/io/api/etherscan/core/impl/EtherScanApi.java b/src/main/java/io/api/etherscan/core/impl/EtherScanApi.java new file mode 100644 index 0000000..1f4d0b4 --- /dev/null +++ b/src/main/java/io/api/etherscan/core/impl/EtherScanApi.java @@ -0,0 +1,120 @@ +package io.api.etherscan.core.impl; + +import io.api.etherscan.core.*; +import io.api.etherscan.error.ApiException; +import io.api.etherscan.error.ApiKeyException; +import io.api.etherscan.executor.IHttpExecutor; +import io.api.etherscan.executor.impl.HttpExecutor; +import io.api.etherscan.manager.IQueueManager; +import io.api.etherscan.manager.impl.FakeQueueManager; +import io.api.etherscan.manager.impl.QueueManager; +import io.api.etherscan.model.EthNetwork; +import io.api.etherscan.util.BasicUtils; +import org.jetbrains.annotations.NotNull; + +import java.util.function.Supplier; + +/** + * EtherScan full API Description + * https://etherscan.io/apis + * + * @author GoodforGod + * @since 28.10.2018 + */ +public class EtherScanApi { + + private static final Supplier DEFAULT_SUPPLIER = HttpExecutor::new; + + private final IAccountApi account; + private final IBlockApi block; + private final IContractApi contract; + private final ILogsApi logs; + private final IProxyApi proxy; + private final IStatisticApi stats; + private final ITransactionApi txs; + + public EtherScanApi() { + this("YourApiKeyToken", EthNetwork.MAINNET); + } + + public EtherScanApi(final EthNetwork network) { + this("YourApiKeyToken", network); + } + + public EtherScanApi(final String apiKey) { + this(apiKey, EthNetwork.MAINNET); + } + + public EtherScanApi(final EthNetwork network, + final Supplier executorSupplier) { + this("YourApiKeyToken", network, executorSupplier); + } + + public EtherScanApi(final String apiKey, + final EthNetwork network) { + this(apiKey, network, DEFAULT_SUPPLIER); + } + + public EtherScanApi(final String apiKey, + final EthNetwork network, + final Supplier executorSupplier) { + if (BasicUtils.isBlank(apiKey)) + throw new ApiKeyException("API key can not be null or empty"); + + if(network == null) + throw new ApiException("Ethereum Network is set to NULL value"); + + // EtherScan 5request\sec limit support by queue manager + final IQueueManager masterQueue = (apiKey.equals("YourApiKeyToken")) + ? new FakeQueueManager() + : new QueueManager(5, 1); + + final IHttpExecutor executor = executorSupplier.get(); + + final String ending = (EthNetwork.TOBALABA.equals(network)) ? "com" : "io"; + final String baseUrl = "https://" + network.getDomain() + ".etherscan." + ending + "/api" + "?apikey=" + apiKey; + + this.account = new AccountApiProvider(masterQueue, baseUrl, executor); + this.block = new BlockApiProvider(masterQueue, baseUrl, executor); + this.contract = new ContractApiProvider(masterQueue, baseUrl, executor); + this.logs = new LogsApiProvider(masterQueue, baseUrl, executor); + this.proxy = new ProxyApiProvider(masterQueue, baseUrl, executor); + this.stats = new StatisticApiProvider(masterQueue, baseUrl, executor); + this.txs = new TransactionApiProvider(masterQueue, baseUrl, executor); + } + + @NotNull + public IAccountApi account() { + return account; + } + + @NotNull + public IContractApi contract() { + return contract; + } + + @NotNull + public ITransactionApi txs() { + return txs; + } + + @NotNull + public IBlockApi block() { + return block; + } + + @NotNull + public ILogsApi logs() { + return logs; + } + + @NotNull + public IProxyApi proxy() { + return proxy; + } + + @NotNull + public IStatisticApi stats() { + return stats; + } +} diff --git a/src/main/java/io/api/etherscan/core/impl/LogsApiProvider.java b/src/main/java/io/api/etherscan/core/impl/LogsApiProvider.java new file mode 100644 index 0000000..6086869 --- /dev/null +++ b/src/main/java/io/api/etherscan/core/impl/LogsApiProvider.java @@ -0,0 +1,45 @@ +package io.api.etherscan.core.impl; + +import io.api.etherscan.core.ILogsApi; +import io.api.etherscan.error.ApiException; +import io.api.etherscan.executor.IHttpExecutor; +import io.api.etherscan.manager.IQueueManager; +import io.api.etherscan.model.Log; +import io.api.etherscan.model.query.impl.LogQuery; +import io.api.etherscan.model.utility.LogResponseTO; +import io.api.etherscan.util.BasicUtils; +import org.jetbrains.annotations.NotNull; + +import java.util.Collections; +import java.util.List; + +/** + * Logs API Implementation + * + * @see ILogsApi + * + * @author GoodforGod + * @since 28.10.2018 + */ +public class LogsApiProvider extends BasicProvider implements ILogsApi { + + private static final String ACT_LOGS_PARAM = ACT_PREFIX + "getLogs"; + + LogsApiProvider(final IQueueManager queue, + final String baseUrl, + final IHttpExecutor executor) { + super(queue, "logs", baseUrl, executor); + } + + @NotNull + @Override + public List logs(final LogQuery query) throws ApiException { + final String urlParams = ACT_LOGS_PARAM + query.getParams(); + final LogResponseTO response = getRequest(urlParams, LogResponseTO.class); + BasicUtils.validateTxResponse(response); + + return (BasicUtils.isEmpty(response.getResult())) + ? Collections.emptyList() + : response.getResult(); + } +} diff --git a/src/main/java/io/api/etherscan/core/impl/ProxyApiProvider.java b/src/main/java/io/api/etherscan/core/impl/ProxyApiProvider.java new file mode 100644 index 0000000..f2376d6 --- /dev/null +++ b/src/main/java/io/api/etherscan/core/impl/ProxyApiProvider.java @@ -0,0 +1,224 @@ +package io.api.etherscan.core.impl; + +import io.api.etherscan.core.IProxyApi; +import io.api.etherscan.error.ApiException; +import io.api.etherscan.error.EtherScanException; +import io.api.etherscan.error.InvalidDataHexException; +import io.api.etherscan.executor.IHttpExecutor; +import io.api.etherscan.manager.IQueueManager; +import io.api.etherscan.model.proxy.BlockProxy; +import io.api.etherscan.model.proxy.ReceiptProxy; +import io.api.etherscan.model.proxy.TxProxy; +import io.api.etherscan.model.proxy.utility.BlockProxyTO; +import io.api.etherscan.model.proxy.utility.StringProxyTO; +import io.api.etherscan.model.proxy.utility.TxInfoProxyTO; +import io.api.etherscan.model.proxy.utility.TxProxyTO; +import io.api.etherscan.util.BasicUtils; +import org.jetbrains.annotations.NotNull; + +import java.math.BigInteger; +import java.util.Optional; +import java.util.regex.Pattern; + +/** + * Proxy API Implementation + * + * @see IProxyApi + * + * @author GoodforGod + * @since 28.10.2018 + */ +public class ProxyApiProvider extends BasicProvider implements IProxyApi { + + private static final String ACT_BLOCKNO_PARAM = ACT_PREFIX + "eth_blockNumber"; + private static final String ACT_BY_BLOCKNO_PARAM = ACT_PREFIX + "eth_getBlockByNumber"; + private static final String ACT_UNCLE_BY_BLOCKNOINDEX_PARAM = ACT_PREFIX + "eth_getUncleByBlockNumberAndIndex"; + private static final String ACT_BLOCKTX_COUNT_PARAM = ACT_PREFIX + "eth_getBlockTransactionCountByNumber"; + private static final String ACT_TX_BY_HASH_PARAM = ACT_PREFIX + "eth_getTransactionByHash"; + private static final String ACT_TX_BY_BLOCKNOINDEX_PARAM = ACT_PREFIX + "eth_getTransactionByBlockNumberAndIndex"; + private static final String ACT_TX_COUNT_PARAM = ACT_PREFIX + "eth_getTransactionCount"; + private static final String ACT_SEND_RAW_TX_PARAM = ACT_PREFIX + "eth_sendRawTransaction"; + private static final String ACT_TX_RECEIPT_PARAM = ACT_PREFIX + "eth_getTransactionReceipt"; + private static final String ACT_CALL_PARAM = ACT_PREFIX + "eth_call"; + private static final String ACT_CODE_PARAM = ACT_PREFIX + "eth_getCode"; + private static final String ACT_STORAGEAT_PARAM = ACT_PREFIX + "eth_getStorageAt"; + private static final String ACT_GASPRICE_PARAM = ACT_PREFIX + "eth_gasPrice"; + private static final String ACT_ESTIMATEGAS_PARAM = ACT_PREFIX + "eth_estimateGas"; + + private static final String BOOLEAN_PARAM = "&boolean=true"; + private static final String TAG_LAST_PARAM = "&tag=latest"; + private static final String POSITION_PARAM = "&position="; + private static final String ADDRESS_PARAM = "&address="; + private static final String TXHASH_PARAM = "&txhash="; + private static final String INDEX_PARAM = "&index="; + private static final String DATA_PARAM = "&data="; + private static final String GAS_PARAM = "&gas="; + private static final String TAG_PARAM = "&tag="; + private static final String HEX_PARAM = "&hex="; + private static final String TO_PARAM = "&to="; + + private static final Pattern EMPTY_HEX = Pattern.compile("0x0+"); + + ProxyApiProvider(final IQueueManager queue, + final String baseUrl, + final IHttpExecutor executor) { + super(queue, "proxy", baseUrl,executor); + } + + @Override + public long blockNoLast() throws ApiException { + final StringProxyTO response = getRequest(ACT_BLOCKNO_PARAM, StringProxyTO.class); + return (BasicUtils.isEmpty(response.getResult())) + ? -1 + : BasicUtils.parseHex(response.getResult()).longValue(); + } + + @NotNull + @Override + public Optional block(final long blockNo) throws ApiException { + 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); + return Optional.ofNullable(response.getResult()); + } + + @NotNull + @Override + public Optional blockUncle(final long blockNo, final long index) throws ApiException { + final long compBlockNo = BasicUtils.compensateMinBlock(blockNo); + final long compIndex = BasicUtils.compensateMinBlock(index); + + 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); + return Optional.ofNullable(response.getResult()); + } + + @NotNull + @Override + public Optional tx(final String txhash) throws ApiException { + BasicUtils.validateTxHash(txhash); + + final String urlParams = ACT_TX_BY_HASH_PARAM + TXHASH_PARAM + txhash; + final TxProxyTO response = getRequest(urlParams, TxProxyTO.class); + return Optional.ofNullable(response.getResult()); + } + + @NotNull + @Override + public Optional tx(final long blockNo, final long index) throws ApiException { + final long compBlockNo = BasicUtils.compensateMinBlock(blockNo); + final long compIndex = (index < 1) ? 1 : index; + + final String urlParams = ACT_TX_BY_BLOCKNOINDEX_PARAM + TAG_PARAM + compBlockNo + INDEX_PARAM + "0x" + Long.toHexString(compIndex); + final TxProxyTO response = getRequest(urlParams, TxProxyTO.class); + return Optional.ofNullable(response.getResult()); + } + + @Override + public int txCount(final long blockNo) throws ApiException { + 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); + return BasicUtils.parseHex(response.getResult()).intValue(); + } + + @Override + public int txSendCount(final String address) throws ApiException { + BasicUtils.validateAddress(address); + + final String urlParams = ACT_TX_COUNT_PARAM + ADDRESS_PARAM + address + TAG_LAST_PARAM; + final StringProxyTO response = getRequest(urlParams, StringProxyTO.class); + return BasicUtils.parseHex(response.getResult()).intValue(); + } + + @Override + @NotNull + public Optional txSendRaw(final String hexEncodedTx) throws ApiException { + if(BasicUtils.isNotHex(hexEncodedTx)) + throw new InvalidDataHexException("Data is not encoded in hex format - " + hexEncodedTx); + + final String urlParams = ACT_SEND_RAW_TX_PARAM + HEX_PARAM + hexEncodedTx; + final StringProxyTO response = postRequest(urlParams, "", StringProxyTO.class); + if(response.getError() != null) + throw new EtherScanException("Error occurred with code " + response.getError().getCode() + + " with message " + response.getError().getMessage() + + ", error id " + response.getId() + ", jsonRPC " + response.getJsonrpc()); + + return Optional.ofNullable(response.getResult()); + } + + @NotNull + @Override + public Optional txReceipt(final String txhash) throws ApiException { + BasicUtils.validateTxHash(txhash); + + final String urlParams = ACT_TX_RECEIPT_PARAM + TXHASH_PARAM + txhash; + final TxInfoProxyTO response = getRequest(urlParams, TxInfoProxyTO.class); + return Optional.ofNullable(response.getResult()); + } + + @NotNull + @Override + public Optional call(final String address, final String data) throws ApiException { + BasicUtils.validateAddress(address); + if(BasicUtils.isNotHex(data)) + throw new InvalidDataHexException("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); + return Optional.ofNullable (response.getResult()); + } + + @NotNull + @Override + public Optional code(final String address) throws ApiException { + BasicUtils.validateAddress(address); + + final String urlParams = ACT_CODE_PARAM + ADDRESS_PARAM + address + TAG_LAST_PARAM; + final StringProxyTO response = getRequest(urlParams, StringProxyTO.class); + return Optional.ofNullable(response.getResult()); + } + + @NotNull + @Override + public Optional storageAt(final String address, final long position) throws ApiException { + BasicUtils.validateAddress(address); + 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); + return (BasicUtils.isEmpty(response.getResult()) || EMPTY_HEX.matcher(response.getResult()).matches()) + ? Optional.empty() + : Optional.of(response.getResult()); + } + + @NotNull + @Override + public BigInteger gasPrice() throws ApiException { + final StringProxyTO response = getRequest(ACT_GASPRICE_PARAM, StringProxyTO.class); + return (BasicUtils.isEmpty(response.getResult())) + ? BigInteger.valueOf(-1) + : BasicUtils.parseHex(response.getResult()); + } + + @NotNull + @Override + public BigInteger gasEstimated() throws ApiException { + return gasEstimated("606060405260728060106000396000f360606040526000"); + } + + @NotNull + @Override + public BigInteger gasEstimated(final String hexData) throws ApiException { + if(!BasicUtils.isEmpty(hexData) && BasicUtils.isNotHex(hexData)) + throw new InvalidDataHexException("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); + return (BasicUtils.isEmpty(response.getResult())) + ? BigInteger.valueOf(-1) + : BasicUtils.parseHex(response.getResult()); + } +} diff --git a/src/main/java/io/api/etherscan/core/impl/StatisticApiProvider.java b/src/main/java/io/api/etherscan/core/impl/StatisticApiProvider.java new file mode 100644 index 0000000..0125850 --- /dev/null +++ b/src/main/java/io/api/etherscan/core/impl/StatisticApiProvider.java @@ -0,0 +1,71 @@ +package io.api.etherscan.core.impl; + +import io.api.etherscan.core.IStatisticApi; +import io.api.etherscan.error.ApiException; +import io.api.etherscan.error.EtherScanException; +import io.api.etherscan.executor.IHttpExecutor; +import io.api.etherscan.manager.IQueueManager; +import io.api.etherscan.model.Price; +import io.api.etherscan.model.Supply; +import io.api.etherscan.model.utility.PriceResponseTO; +import io.api.etherscan.model.utility.StringResponseTO; +import io.api.etherscan.util.BasicUtils; +import org.jetbrains.annotations.NotNull; + +import java.math.BigInteger; + +/** + * Statistic API Implementation + * + * @see IStatisticApi + * + * @author GoodforGod + * @since 28.10.2018 + */ +public class StatisticApiProvider extends BasicProvider implements IStatisticApi { + + private static final String ACT_SUPPLY_PARAM = ACT_PREFIX + "ethsupply"; + private static final String ACT_TOKEN_SUPPLY_PARAM = ACT_PREFIX + "tokensupply"; + private static final String ACT_LASTPRICE_PARAM = ACT_PREFIX + "ethprice"; + + private static final String CONTRACT_ADDRESS_PARAM = "&contractaddress="; + + StatisticApiProvider(final IQueueManager queue, + final String baseUrl, + final IHttpExecutor executor) { + super(queue, "stats", baseUrl, executor); + } + + @NotNull + @Override + public Supply supply() throws ApiException { + final StringResponseTO response = getRequest(ACT_SUPPLY_PARAM, StringResponseTO.class); + if (response.getStatus() != 1) + throw new EtherScanException(response.getMessage() + ", with status " + response.getStatus()); + + return new Supply(new BigInteger(response.getResult())); + } + + @NotNull + @Override + public BigInteger supply(final String contract) throws ApiException { + BasicUtils.validateAddress(contract); + + final String urlParams = ACT_TOKEN_SUPPLY_PARAM + CONTRACT_ADDRESS_PARAM + contract; + final StringResponseTO response = getRequest(urlParams, StringResponseTO.class); + if (response.getStatus() != 1) + throw new EtherScanException(response.getMessage() + ", with status " + response.getStatus()); + + return new BigInteger(response.getResult()); + } + + @NotNull + @Override + public Price lastPrice() throws ApiException { + final PriceResponseTO response = getRequest(ACT_LASTPRICE_PARAM, PriceResponseTO.class); + if (response.getStatus() != 1) + throw new EtherScanException(response.getMessage() + ", with status " + response.getStatus()); + + return response.getResult(); + } +} diff --git a/src/main/java/io/api/etherscan/core/impl/TransactionApiProvider.java b/src/main/java/io/api/etherscan/core/impl/TransactionApiProvider.java new file mode 100644 index 0000000..82eb467 --- /dev/null +++ b/src/main/java/io/api/etherscan/core/impl/TransactionApiProvider.java @@ -0,0 +1,60 @@ +package io.api.etherscan.core.impl; + +import io.api.etherscan.core.ITransactionApi; +import io.api.etherscan.error.ApiException; +import io.api.etherscan.executor.IHttpExecutor; +import io.api.etherscan.manager.IQueueManager; +import io.api.etherscan.model.Status; +import io.api.etherscan.model.utility.ReceiptStatusResponseTO; +import io.api.etherscan.model.utility.StatusResponseTO; +import io.api.etherscan.util.BasicUtils; +import org.jetbrains.annotations.NotNull; + +import java.util.Optional; + +/** + * Transaction API Implementation + * + * @author GoodforGod + * @see ITransactionApi + * @since 28.10.2018 + */ +public class TransactionApiProvider extends BasicProvider implements ITransactionApi { + + 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(final IQueueManager queue, + final String baseUrl, + final IHttpExecutor executor) { + super(queue, "transaction", baseUrl, executor); + } + + @NotNull + @Override + public Optional execStatus(final String txhash) throws ApiException { + BasicUtils.validateTxHash(txhash); + + final String urlParams = ACT_EXEC_STATUS_PARAM + TXHASH_PARAM + txhash; + final StatusResponseTO response = getRequest(urlParams, StatusResponseTO.class); + BasicUtils.validateTxResponse(response); + + return Optional.ofNullable(response.getResult()); + } + + @NotNull + @Override + public Optional receiptStatus(final String txhash) throws ApiException { + BasicUtils.validateTxHash(txhash); + + final String urlParams = ACT_RECEIPT_STATUS_PARAM + TXHASH_PARAM + txhash; + final ReceiptStatusResponseTO response = getRequest(urlParams, ReceiptStatusResponseTO.class); + BasicUtils.validateTxResponse(response); + + return (response.getResult() == null || BasicUtils.isEmpty(response.getResult().getStatus())) + ? Optional.empty() + : Optional.of(response.getResult().getStatus().contains("1")); + } +} From 278396ab2f61d4cee151fd2c6e43110e9959d205 Mon Sep 17 00:00:00 2001 From: Johannes Jander <139699+iSnow@users.noreply.github.com> Date: Mon, 5 Oct 2020 17:18:33 +0200 Subject: [PATCH 09/17] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a7f90b1..db6e614 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ ![travis](https://travis-ci.org/GoodforGod/java-etherscan-api.svg?branch=master) [![Maintainability](https://api.codeclimate.com/v1/badges/808997be2e69ff1ae8fe/maintainability)](https://codeclimate.com/github/GoodforGod/java-etherscan-api/maintainability) [![codecov](https://codecov.io/gh/GoodforGod/java-etherscan-api/branch/master/graph/badge.svg)](https://codecov.io/gh/GoodforGod/java-etherscan-api) -[![](https://jitpack.io/v/iSnow/java-etherscan-api.svg)](https://jitpack.io/#iSnow/java-etherscan-api) +[![](https://jitpack.io/v/GoodforGod/java-etherscan-api.svg)](https://jitpack.io/#GoodforGod/java-etherscan-api) [Etherscan](https://etherscan.io/apis) Java API implementation. From ccda63141802f4ac86e3f29884efa1b7924ccafc Mon Sep 17 00:00:00 2001 From: Johannes Jander <139699+iSnow@users.noreply.github.com> Date: Mon, 5 Oct 2020 17:19:43 +0200 Subject: [PATCH 10/17] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index db6e614..3692f81 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ ![travis](https://travis-ci.org/GoodforGod/java-etherscan-api.svg?branch=master) [![Maintainability](https://api.codeclimate.com/v1/badges/808997be2e69ff1ae8fe/maintainability)](https://codeclimate.com/github/GoodforGod/java-etherscan-api/maintainability) [![codecov](https://codecov.io/gh/GoodforGod/java-etherscan-api/branch/master/graph/badge.svg)](https://codecov.io/gh/GoodforGod/java-etherscan-api) -[![](https://jitpack.io/v/GoodforGod/java-etherscan-api.svg)](https://jitpack.io/#GoodforGod/java-etherscan-api) +[![Jitpack](https://jitpack.io/v/GoodforGod/java-etherscan-api.svg)](https://jitpack.io/#GoodforGod/java-etherscan-api) [Etherscan](https://etherscan.io/apis) Java API implementation. From dffeda728ae75ba1e4562a9d311e3fe65aecf709 Mon Sep 17 00:00:00 2001 From: Johannes Jander <139699+iSnow@users.noreply.github.com> Date: Mon, 5 Oct 2020 17:23:30 +0200 Subject: [PATCH 11/17] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3692f81..f0857ff 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # java-etherscan-api -![travis](https://travis-ci.org/GoodforGod/java-etherscan-api.svg?branch=master) +[![travis](https://travis-ci.org/GoodforGod/java-etherscan-api.svg?branch=master)](https://travis-ci.com/GoodforGod/java-etherscan-api) [![Maintainability](https://api.codeclimate.com/v1/badges/808997be2e69ff1ae8fe/maintainability)](https://codeclimate.com/github/GoodforGod/java-etherscan-api/maintainability) [![codecov](https://codecov.io/gh/GoodforGod/java-etherscan-api/branch/master/graph/badge.svg)](https://codecov.io/gh/GoodforGod/java-etherscan-api) [![Jitpack](https://jitpack.io/v/GoodforGod/java-etherscan-api.svg)](https://jitpack.io/#GoodforGod/java-etherscan-api) From 577c38d76ab8e38937dd884c63bc6c3d5dedf64e Mon Sep 17 00:00:00 2001 From: Johannes Jander <139699+iSnow@users.noreply.github.com> Date: Mon, 5 Oct 2020 17:29:37 +0200 Subject: [PATCH 12/17] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f0857ff..b19a717 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # java-etherscan-api -[![travis](https://travis-ci.org/GoodforGod/java-etherscan-api.svg?branch=master)](https://travis-ci.com/GoodforGod/java-etherscan-api) +[![travis](https://travis-ci.org/GoodforGod/java-etherscan-api.svg?branch=master)](https://travis-ci.com/iSnow/java-etherscan-api) [![Maintainability](https://api.codeclimate.com/v1/badges/808997be2e69ff1ae8fe/maintainability)](https://codeclimate.com/github/GoodforGod/java-etherscan-api/maintainability) [![codecov](https://codecov.io/gh/GoodforGod/java-etherscan-api/branch/master/graph/badge.svg)](https://codecov.io/gh/GoodforGod/java-etherscan-api) -[![Jitpack](https://jitpack.io/v/GoodforGod/java-etherscan-api.svg)](https://jitpack.io/#GoodforGod/java-etherscan-api) +[![Jitpack](https://jitpack.io/v/iSnow/java-etherscan-api.svg)](https://jitpack.io/#iSnow/java-etherscan-api) [Etherscan](https://etherscan.io/apis) Java API implementation. From 5c79d665b63806935f6a89e70aefb6fdf28c3f84 Mon Sep 17 00:00:00 2001 From: iSnow <139699+iSnow@users.noreply.github.com> Date: Tue, 6 Oct 2020 09:27:35 +0200 Subject: [PATCH 13/17] Removed events stuff again, moved to different library --- src/main/java/io/api/etherscan/core/impl/BasicProvider.java | 2 +- src/main/java/io/api/etherscan/error/ParseException.java | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/io/api/etherscan/core/impl/BasicProvider.java b/src/main/java/io/api/etherscan/core/impl/BasicProvider.java index 5b95f68..67c790b 100644 --- a/src/main/java/io/api/etherscan/core/impl/BasicProvider.java +++ b/src/main/java/io/api/etherscan/core/impl/BasicProvider.java @@ -42,7 +42,7 @@ T convert(final String json, final Class tClass) { try { return gson.fromJson(json, tClass); } catch (Exception e) { - throw new ParseException(e.getMessage(), e.getCause()); + throw new ParseException(e.getMessage(), e.getCause(), json); } } diff --git a/src/main/java/io/api/etherscan/error/ParseException.java b/src/main/java/io/api/etherscan/error/ParseException.java index 81974df..f279fda 100644 --- a/src/main/java/io/api/etherscan/error/ParseException.java +++ b/src/main/java/io/api/etherscan/error/ParseException.java @@ -7,8 +7,10 @@ * @since 29.10.2018 */ public class ParseException extends ApiException { + String json; - public ParseException(String message, Throwable cause) { + public ParseException(String message, Throwable cause, String json) { super(message, cause); + this.json = json; } } From 50d5799439967f0bb7a21c98d211cde5c604b1d0 Mon Sep 17 00:00:00 2001 From: iSnow <139699+iSnow@users.noreply.github.com> Date: Tue, 6 Oct 2020 09:45:28 +0200 Subject: [PATCH 14/17] Removed events stuff again, moved to different library --- src/test/java/io/api/util/BasicUtilsTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/io/api/util/BasicUtilsTests.java b/src/test/java/io/api/util/BasicUtilsTests.java index 1b1753e..2c5ad71 100644 --- a/src/test/java/io/api/util/BasicUtilsTests.java +++ b/src/test/java/io/api/util/BasicUtilsTests.java @@ -98,6 +98,6 @@ public void isResponseNullThrows() { @Test(expected = ParseException.class) public void isThrowParseException() { - throw new ParseException("Test", null); + throw new ParseException("Test", null, null); } } From 120ba0fc8712f42e0492b451a8db8243fddece52 Mon Sep 17 00:00:00 2001 From: iSnow <139699+iSnow@users.noreply.github.com> Date: Tue, 6 Oct 2020 11:23:03 +0200 Subject: [PATCH 15/17] Added support for rate limiting by Etherscan: throw RateLimitException --- .../api/etherscan/core/impl/BasicProvider.java | 17 +++++++++++++++++ .../api/etherscan/error/RateLimitException.java | 15 +++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 src/main/java/io/api/etherscan/error/RateLimitException.java diff --git a/src/main/java/io/api/etherscan/core/impl/BasicProvider.java b/src/main/java/io/api/etherscan/core/impl/BasicProvider.java index 67c790b..d242a76 100644 --- a/src/main/java/io/api/etherscan/core/impl/BasicProvider.java +++ b/src/main/java/io/api/etherscan/core/impl/BasicProvider.java @@ -1,12 +1,16 @@ package io.api.etherscan.core.impl; import com.google.gson.Gson; +import com.google.gson.JsonSyntaxException; import io.api.etherscan.error.EtherScanException; import io.api.etherscan.error.ParseException; +import io.api.etherscan.error.RateLimitException; import io.api.etherscan.executor.IHttpExecutor; import io.api.etherscan.manager.IQueueManager; import io.api.etherscan.util.BasicUtils; +import java.util.Map; + /** * Base provider for API Implementations * @@ -42,6 +46,19 @@ T convert(final String json, final Class tClass) { try { return gson.fromJson(json, tClass); } catch (Exception e) { + if (e instanceof JsonSyntaxException) { + Map map = gson.fromJson(json, Map.class); + Object statusCode = map.get("status"); + if ((statusCode instanceof String) && (statusCode.equals("0"))) { + Object message = map.get("message"); + if ((message instanceof String) && (message.equals("NOTOK"))) { + Object result = map.get("result"); + if ((result instanceof String) && (result.equals("Max rate limit reached"))) { + throw new RateLimitException ("Max rate limit reached"); + } + } + } + } throw new ParseException(e.getMessage(), e.getCause(), json); } } diff --git a/src/main/java/io/api/etherscan/error/RateLimitException.java b/src/main/java/io/api/etherscan/error/RateLimitException.java new file mode 100644 index 0000000..2562342 --- /dev/null +++ b/src/main/java/io/api/etherscan/error/RateLimitException.java @@ -0,0 +1,15 @@ +package io.api.etherscan.error; + +/** + * ! NO DESCRIPTION ! + * + * @author iSnow + * @since 2020-10-06 + */ +public class RateLimitException extends ApiException { + + public RateLimitException(String message) { + super(message); + } + +} From f7bb00c7abe884c38c2e6d8cbc49787f5cd532d9 Mon Sep 17 00:00:00 2001 From: iSnow <139699+iSnow@users.noreply.github.com> Date: Tue, 6 Oct 2020 12:37:08 +0200 Subject: [PATCH 16/17] added getter for the JSON string to ParseException --- src/main/java/io/api/etherscan/error/ParseException.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/io/api/etherscan/error/ParseException.java b/src/main/java/io/api/etherscan/error/ParseException.java index f279fda..dc3952c 100644 --- a/src/main/java/io/api/etherscan/error/ParseException.java +++ b/src/main/java/io/api/etherscan/error/ParseException.java @@ -13,4 +13,8 @@ public ParseException(String message, Throwable cause, String json) { super(message, cause); this.json = json; } + + public String getJson() { + return json; + } } From 77ee6ace1161070dcce5fe60e6d6b672b87ace22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=B4=80=C9=B4=E1=B4=9B=E1=B4=8F=C9=B4=CA=8F?= Date: Sun, 18 Oct 2020 13:42:40 +0300 Subject: [PATCH 17/17] Licence added