package io.goodforgod.api.etherscan; import io.goodforgod.api.etherscan.error.EtherScanException; import io.goodforgod.api.etherscan.error.EtherScanResponseException; import io.goodforgod.api.etherscan.http.EthHttpClient; import io.goodforgod.api.etherscan.manager.RequestQueueManager; import io.goodforgod.api.etherscan.model.*; import io.goodforgod.api.etherscan.model.response.*; import io.goodforgod.api.etherscan.util.BasicUtils; import java.math.BigInteger; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.stream.Collectors; import org.jetbrains.annotations.NotNull; /** * Account API Implementation * * @see AccountAPI * @author GoodforGod * @since 28.10.2018 */ final class AccountAPIProvider extends BasicProvider implements AccountAPI { 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_ERC20_ACTION = ACT_PREFIX + "tokentx"; private static final String ACT_TX_ERC721_ACTION = ACT_PREFIX + "tokennfttx"; private static final String ACT_TX_ERC1155_ACTION = ACT_PREFIX + "token1155tx"; 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(RequestQueueManager requestQueueManager, String baseUrl, EthHttpClient executor, Converter converter) { super(requestQueueManager, "account", baseUrl, executor, converter); } @NotNull @Override public Balance balance(@NotNull String address) throws EtherScanException { BasicUtils.validateAddress(address); final String urlParams = ACT_BALANCE_ACTION + TAG_LATEST_PARAM + ADDRESS_PARAM + address; final StringResponseTO response = getRequest(urlParams, StringResponseTO.class); if (response.getStatus() != 1) throw new EtherScanResponseException(response); return new Balance(address, Wei.ofWei(new BigInteger(response.getResult()))); } @NotNull @Override public TokenBalance balance(@NotNull String address, @NotNull String contract) throws EtherScanException { 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 EtherScanResponseException(response); return new TokenBalance(address, Wei.ofWei(new BigInteger(response.getResult())), contract); } @NotNull @Override public List<Balance> balances(@NotNull List<String> addresses) throws EtherScanException { if (BasicUtils.isEmpty(addresses)) { return Collections.emptyList(); } BasicUtils.validateAddresses(addresses); // Maximum addresses in batch request - 20 final List<Balance> balances = new ArrayList<>(); final List<List<String>> addressesAsBatches = BasicUtils.partition(addresses, 20); for (final List<String> batch : addressesAsBatches) { final String urlParams = ACT_BALANCE_MULTI_ACTION + TAG_LATEST_PARAM + ADDRESS_PARAM + BasicUtils.toAddressParam(batch); final BalanceResponseTO response = getRequest(urlParams, BalanceResponseTO.class); if (response.getStatus() != 1) { throw new EtherScanResponseException(response); } if (!BasicUtils.isEmpty(response.getResult())) { balances.addAll(response.getResult().stream() .map(r -> new Balance(r.getAccount(), Wei.ofWei(new BigInteger(r.getBalance())))) .collect(Collectors.toList())); } } return balances; } @NotNull @Override public List<Tx> txs(@NotNull String address) throws EtherScanException { return txs(address, MIN_START_BLOCK); } @NotNull @Override public List<Tx> txs(@NotNull String address, long startBlock) throws EtherScanException { return txs(address, startBlock, MAX_END_BLOCK); } @NotNull @Override public List<Tx> txs(@NotNull String address, long startBlock, long endBlock) throws EtherScanException { BasicUtils.validateAddress(address); final BlockParam blocks = BasicUtils.compensateBlocks(startBlock, endBlock); final String urlParams = ACT_TX_ACTION + PAGE_PARAM + "%s" + OFFSET_PARAM + OFFSET_MAX + ADDRESS_PARAM + address + START_BLOCK_PARAM + blocks.start() + END_BLOCK_PARAM + blocks.end() + 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 <T> responseTO list T type * @param <R> responseListTO type * @return List of T values */ private <T, R extends BaseListResponseTO<T>> List<T> getRequestUsingOffset(final String urlParams, Class<R> tClass) throws EtherScanException { final List<T> 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<TxInternal> txsInternal(@NotNull String address) throws EtherScanException { return txsInternal(address, MIN_START_BLOCK); } @NotNull @Override public List<TxInternal> txsInternal(@NotNull String address, long startBlock) throws EtherScanException { return txsInternal(address, startBlock, MAX_END_BLOCK); } @NotNull @Override public List<TxInternal> txsInternal(@NotNull String address, long startBlock, long endBlock) throws EtherScanException { BasicUtils.validateAddress(address); final BlockParam blocks = BasicUtils.compensateBlocks(startBlock, endBlock); final String urlParams = ACT_TX_INTERNAL_ACTION + PAGE_PARAM + "%s" + OFFSET_PARAM + OFFSET_MAX + ADDRESS_PARAM + address + START_BLOCK_PARAM + blocks.start() + END_BLOCK_PARAM + blocks.end() + SORT_ASC_PARAM; return getRequestUsingOffset(urlParams, TxInternalResponseTO.class); } @NotNull @Override public List<TxInternal> txsInternalByHash(@NotNull String txhash) throws EtherScanException { 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<TxErc20> txsErc20(@NotNull String address) throws EtherScanException { return txsErc20(address, MIN_START_BLOCK); } @NotNull @Override public List<TxErc20> txsErc20(@NotNull String address, long startBlock) throws EtherScanException { return txsErc20(address, startBlock, MAX_END_BLOCK); } @NotNull @Override public List<TxErc20> txsErc20(@NotNull String address, long startBlock, long endBlock) throws EtherScanException { BasicUtils.validateAddress(address); final BlockParam blocks = BasicUtils.compensateBlocks(startBlock, endBlock); final String urlParams = ACT_TX_ERC20_ACTION + PAGE_PARAM + "%s" + OFFSET_PARAM + OFFSET_MAX + ADDRESS_PARAM + address + START_BLOCK_PARAM + blocks.start() + END_BLOCK_PARAM + blocks.end() + SORT_ASC_PARAM; return getRequestUsingOffset(urlParams, TxErc20ResponseTO.class); } @NotNull @Override public List<TxErc20> txsErc20(@NotNull String address, @NotNull String contractAddress) throws EtherScanException { return txsErc20(address, contractAddress, MIN_START_BLOCK); } @NotNull @Override public List<TxErc20> txsErc20(@NotNull String address, @NotNull String contractAddress, long startBlock) throws EtherScanException { return txsErc20(address, contractAddress, startBlock, MAX_END_BLOCK); } @NotNull @Override public List<TxErc20> txsErc20(@NotNull String address, @NotNull String contractAddress, long startBlock, long endBlock) throws EtherScanException { 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_ERC20_ACTION + offsetParam + ADDRESS_PARAM + address + CONTRACT_PARAM + contractAddress + blockParam + SORT_ASC_PARAM; return getRequestUsingOffset(urlParams, TxErc20ResponseTO.class); } @NotNull @Override public List<TxErc721> txsErc721(@NotNull String address) throws EtherScanException { return txsErc721(address, MIN_START_BLOCK); } @NotNull @Override public List<TxErc721> txsErc721(@NotNull String address, long startBlock) throws EtherScanException { return txsErc721(address, startBlock, MAX_END_BLOCK); } @NotNull @Override public List<TxErc721> txsErc721(@NotNull String address, long startBlock, long endBlock) throws EtherScanException { BasicUtils.validateAddress(address); final BlockParam blocks = BasicUtils.compensateBlocks(startBlock, endBlock); final String urlParams = ACT_TX_ERC721_ACTION + PAGE_PARAM + "%s" + OFFSET_PARAM + OFFSET_MAX + ADDRESS_PARAM + address + START_BLOCK_PARAM + blocks.start() + END_BLOCK_PARAM + blocks.end() + SORT_ASC_PARAM; return getRequestUsingOffset(urlParams, TxErc721ResponseTO.class); } @Override public @NotNull List<TxErc721> txsErc721(@NotNull String address, @NotNull String contractAddress, long startBlock, long endBlock) throws EtherScanException { 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_ERC721_ACTION + offsetParam + ADDRESS_PARAM + address + CONTRACT_PARAM + contractAddress + blockParam + SORT_ASC_PARAM; return getRequestUsingOffset(urlParams, TxErc721ResponseTO.class); } @Override public @NotNull List<TxErc721> txsErc721(@NotNull String address, @NotNull String contractAddress, long startBlock) throws EtherScanException { return txsErc721(address, contractAddress, startBlock, MAX_END_BLOCK); } @Override public @NotNull List<TxErc721> txsErc721(@NotNull String address, @NotNull String contractAddress) throws EtherScanException { return txsErc721(address, contractAddress, MIN_START_BLOCK); } @Override public @NotNull List<TxErc1155> txsErc1155(@NotNull String address, long startBlock, long endBlock) throws EtherScanException { BasicUtils.validateAddress(address); final BlockParam blocks = BasicUtils.compensateBlocks(startBlock, endBlock); final String urlParams = ACT_TX_ERC1155_ACTION + PAGE_PARAM + "%s" + OFFSET_PARAM + OFFSET_MAX + ADDRESS_PARAM + address + START_BLOCK_PARAM + blocks.start() + END_BLOCK_PARAM + blocks.end() + SORT_ASC_PARAM; return getRequestUsingOffset(urlParams, TxErc1155ResponseTO.class); } @Override public @NotNull List<TxErc1155> txsErc1155(@NotNull String address, long startBlock) throws EtherScanException { return txsErc1155(address, startBlock, MAX_END_BLOCK); } @Override public @NotNull List<TxErc1155> txsErc1155(@NotNull String address) throws EtherScanException { return txsErc1155(address, MIN_START_BLOCK); } @Override public @NotNull List<TxErc1155> txsErc1155(@NotNull String address, @NotNull String contractAddress, long startBlock, long endBlock) throws EtherScanException { 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_ERC1155_ACTION + offsetParam + ADDRESS_PARAM + address + CONTRACT_PARAM + contractAddress + blockParam + SORT_ASC_PARAM; return getRequestUsingOffset(urlParams, TxErc1155ResponseTO.class); } @Override public @NotNull List<TxErc1155> txsErc1155(@NotNull String address, @NotNull String contractAddress, long startBlock) throws EtherScanException { return txsErc1155(address, contractAddress, startBlock, MAX_END_BLOCK); } @Override public @NotNull List<TxErc1155> txsErc1155(@NotNull String address, @NotNull String contractAddress) throws EtherScanException { return txsErc1155(address, contractAddress, MIN_START_BLOCK); } @NotNull @Override public List<Block> blocksMined(@NotNull String address) throws EtherScanException { BasicUtils.validateAddress(address); final String urlParams = ACT_MINED_ACTION + PAGE_PARAM + "%s" + OFFSET_PARAM + OFFSET_MAX + BLOCK_TYPE_PARAM + ADDRESS_PARAM + address; return getRequestUsingOffset(urlParams, BlockResponseTO.class); } }