org.duniter.core.client.service.bma.BlockchainRemoteServiceImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.duniter.core.client.service.bma.BlockchainRemoteServiceImpl.java

Source

package org.duniter.core.client.service.bma;

/*
 * #%L
 * UCoin Java :: Core Client API
 * %%
 * Copyright (C) 2014 - 2016 EIS
 * %%
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as
 * published by the Free Software Foundation, either version 3 of the 
 * License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public 
 * License along with this program.  If not, see
 * <http://www.gnu.org/licenses/gpl-3.0.html>.
 * #L%
 */

import org.apache.http.NameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.message.BasicNameValuePair;
import org.duniter.core.client.config.Configuration;
import org.duniter.core.client.model.bma.*;
import org.duniter.core.client.model.local.Identity;
import org.duniter.core.client.model.local.Peer;
import org.duniter.core.client.model.local.Wallet;
import org.duniter.core.client.service.ServiceLocator;
import org.duniter.core.client.service.exception.*;
import org.duniter.core.exception.TechnicalException;
import org.duniter.core.service.CryptoService;
import org.duniter.core.util.Preconditions;
import org.duniter.core.util.StringUtils;
import org.duniter.core.util.cache.Cache;
import org.duniter.core.util.cache.SimpleCache;
import org.duniter.core.util.crypto.CryptoUtils;
import org.duniter.core.util.json.JsonArrayParser;
import org.duniter.core.util.websocket.WebsocketClientEndpoint;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.util.*;

public class BlockchainRemoteServiceImpl extends BaseRemoteServiceImpl implements BlockchainRemoteService {

    private static final Logger log = LoggerFactory.getLogger(BlockchainRemoteServiceImpl.class);

    private static final String JSON_DIVIDEND_ATTR = "\"dividend\":";

    public static final String URL_BASE = "/blockchain";

    public static final String URL_PARAMETERS = URL_BASE + "/parameters";

    public static final String URL_BLOCK = URL_BASE + "/block/%s";

    public static final String URL_BLOCKS_FROM = URL_BASE + "/blocks/%s/%s";

    public static final String URL_BLOCK_CURRENT = URL_BASE + "/current";

    public static final String URL_BLOCK_WITH_TX = URL_BASE + "/with/tx";

    public static final String URL_BLOCK_WITH_UD = URL_BASE + "/with/ud";

    public static final String URL_MEMBERSHIP = URL_BASE + "/membership";

    public static final String URL_MEMBERSHIP_SEARCH = URL_BASE + "/memberships/%s";

    public static final String URL_WS_BLOCK = "/ws/block";

    private Configuration config;

    // Cache need for wallet refresh : iteration on wallet should not
    // execute a download of the current block
    private Cache<String, BlockchainBlock> mCurrentBlockCache;

    // Cache on blockchain parameters
    private Cache<String, BlockchainParameters> mParametersCache;

    private Map<URI, WebsocketClientEndpoint> wsEndPoints = new HashMap<>();

    public BlockchainRemoteServiceImpl() {
        super();
    }

    @Override
    public void afterPropertiesSet() {
        super.afterPropertiesSet();
        config = Configuration.instance();

        // Initialize caches
        initCaches();
    }

    @Override
    public void close() throws IOException {
        super.close();

        if (wsEndPoints.size() != 0) {
            for (WebsocketClientEndpoint clientEndPoint : wsEndPoints.values()) {
                clientEndPoint.close();
            }
            wsEndPoints.clear();
        }
    }

    @Override
    public BlockchainParameters getParameters(String currencyId, boolean useCache) {
        if (!useCache) {
            return getParameters(currencyId);
        } else {
            return mParametersCache.get(currencyId);
        }
    }

    @Override
    public BlockchainParameters getParameters(String currencyId) {
        // get blockchain parameter
        BlockchainParameters result = executeRequest(currencyId, URL_PARAMETERS, BlockchainParameters.class);
        return result;
    }

    @Override
    public BlockchainParameters getParameters(Peer peer) {
        // get blockchain parameter
        BlockchainParameters result = executeRequest(peer, URL_PARAMETERS, BlockchainParameters.class);
        return result;
    }

    @Override
    public BlockchainBlock getBlock(String currencyId, long number) throws BlockNotFoundException {
        String path = String.format(URL_BLOCK, number);
        try {
            return executeRequest(currencyId, path, BlockchainBlock.class);
        } catch (HttpNotFoundException e) {
            throw new BlockNotFoundException(String.format("Block #%s not found", number));
        }
    }

    @Override
    public Long getBlockDividend(String currencyId, long number) throws BlockNotFoundException {
        String path = String.format(URL_BLOCK, number);
        try {
            String json = executeRequest(currencyId, path, String.class);
            return getDividendFromBlockJson(json);
        } catch (HttpNotFoundException e) {
            throw new BlockNotFoundException(String.format("Block #%s not found", number));
        }
    }

    @Override
    public BlockchainBlock getBlock(Peer peer, long number) throws BlockNotFoundException {
        // Get block from number
        String path = String.format(URL_BLOCK, number);
        try {
            return executeRequest(peer, path, BlockchainBlock.class);
        } catch (HttpNotFoundException e) {
            throw new BlockNotFoundException(String.format("Block #%s not found on peer [%s]", number, peer));
        }
    }

    @Override
    public long[] getBlocksWithTx(Peer peer) {
        try {
            Blocks blocks = executeRequest(peer, URL_BLOCK_WITH_TX, Blocks.class);
            return (blocks == null || blocks.getResult() == null) ? new long[0] : blocks.getResult().getBlocks();
        } catch (HttpNotFoundException e) {
            throw new TechnicalException(String.format("Error while getting blocks with TX on peer [%s]", peer));
        }
    }

    @Override
    public String getBlockAsJson(Peer peer, long number) {
        // get blockchain parameter
        String path = String.format(URL_BLOCK, number);
        try {
            return executeRequest(peer, path, String.class);
        } catch (HttpNotFoundException e) {
            throw new BlockNotFoundException(String.format("Block #%s not found on peer [%s]", number, peer));
        }
    }

    @Override
    public String[] getBlocksAsJson(Peer peer, int count, int from) {
        // get blockchain parameter
        String path = String.format(URL_BLOCKS_FROM, count, from);
        String jsonBlocksStr = executeRequest(peer, path, String.class);

        // Parse only array content, but deserialize array item
        JsonArrayParser parser = new JsonArrayParser();
        return parser.getValuesAsArray(jsonBlocksStr);
    }

    /**
     * Retrieve the current block (with short cache)
     *
     * @return
     */
    public BlockchainBlock getCurrentBlock(String currencyId, boolean useCache) {
        if (!useCache) {
            return getCurrentBlock(currencyId);
        } else {
            return mCurrentBlockCache.get(currencyId);
        }
    }

    @Override
    public BlockchainBlock getCurrentBlock(String currencyId) {
        // get blockchain parameter
        BlockchainBlock result = executeRequest(currencyId, URL_BLOCK_CURRENT, BlockchainBlock.class);
        return result;
    }

    @Override
    public BlockchainBlock getCurrentBlock(Peer peer) {
        // get blockchain parameter
        BlockchainBlock result = executeRequest(peer, URL_BLOCK_CURRENT, BlockchainBlock.class);
        return result;
    }

    @Override
    public org.duniter.core.client.model.local.Currency getCurrencyFromPeer(Peer peer) {
        BlockchainParameters parameter = getParameters(peer);
        BlockchainBlock firstBlock = getBlock(peer, 0);
        BlockchainBlock lastBlock = getCurrentBlock(peer);

        org.duniter.core.client.model.local.Currency result = new org.duniter.core.client.model.local.Currency();
        result.setCurrencyName(parameter.getCurrency());
        result.setFirstBlockSignature(firstBlock.getSignature());
        result.setMembersCount(lastBlock.getMembersCount());
        result.setLastUD(parameter.getUd0());

        return result;
    }

    @Override
    public BlockchainParameters getBlockchainParametersFromPeer(Peer peer) {
        return getParameters(peer);
    }

    @Override
    public long getLastUD(String currencyId) {
        // get block number with UD
        String blocksWithUdResponse = executeRequest(currencyId, URL_BLOCK_WITH_UD, String.class);
        Integer blockNumber = getLastBlockNumberFromJson(blocksWithUdResponse);

        // If no result (this could happen when no UD has been send
        if (blockNumber == null) {
            // get the first UD from currency parameter
            BlockchainParameters parameter = getParameters(currencyId);
            return parameter.getUd0();
        }

        // Get the UD from the last block with UD
        Long lastUD = getBlockDividend(currencyId, blockNumber);

        // Check not null (should never append)
        if (lastUD == null) {
            throw new TechnicalException("Unable to get last UD from server");
        }
        return lastUD.longValue();
    }

    @Override
    public long getLastUD(Peer peer) {
        // get block number with UD
        String blocksWithUdResponse = executeRequest(peer, URL_BLOCK_WITH_UD, String.class);

        int[] blocksWithUD = getBlockNumbersFromJson(blocksWithUdResponse);

        // If no result (this could happen when no UD has been send
        if (blocksWithUD != null && blocksWithUD.length > 0) {

            int index = blocksWithUD.length - 1;
            while (index >= 0) {

                try {
                    // Get the UD from the last block with UD
                    String path = String.format(URL_BLOCK, blocksWithUD[index]);
                    String json = executeRequest(peer, path, String.class);
                    Long lastUD = getDividendFromBlockJson(json);

                    // Check not null (should never append)
                    if (lastUD == null) {
                        throw new TechnicalException("Unable to get last UD from server");
                    }
                    return lastUD.longValue();
                } catch (HttpNotFoundException e) {
                    index--; // Can occur something (observed in Duniter 0.50.0)
                }
            }
        }

        // get the first UD from currency parameter
        BlockchainParameters parameter = getParameters(peer);
        return parameter.getUd0();
    }

    /**
     * Check is a identity is not already used by a existing member
     *
     * @param peer
     * @param identity
     * @throws UidAlreadyUsedException    if UID already used by another member
     * @throws PubkeyAlreadyUsedException if pubkey already used by another member
     */
    public void checkNotMemberIdentity(Peer peer, Identity identity)
            throws UidAlreadyUsedException, PubkeyAlreadyUsedException {
        Preconditions.checkNotNull(peer);
        Preconditions.checkNotNull(identity);
        Preconditions.checkArgument(StringUtils.isNotBlank(identity.getUid()));
        Preconditions.checkArgument(StringUtils.isNotBlank(identity.getPubkey()));

        // Read membership data from the UID
        BlockchainMemberships result = getMembershipByPubkeyOrUid(peer, identity.getUid());

        // uid already used by another member
        if (result != null) {
            throw new UidAlreadyUsedException(
                    String.format("User identifier '%s' is already used by another member", identity.getUid()));
        }

        result = getMembershipByPubkeyOrUid(peer, identity.getPubkey());

        // pubkey already used by another member
        if (result != null) {
            throw new PubkeyAlreadyUsedException(
                    String.format("Pubkey key '%s' is already used by another member", identity.getPubkey()));
        }
    }

    /**
     * Check is a wallet is a member, and load its attribute isMember and certTimestamp
     *
     * @param wallet
     * @throws UidMatchAnotherPubkeyException is uid already used by another pubkey
     */
    public void loadAndCheckMembership(Peer peer, Wallet wallet) throws UidMatchAnotherPubkeyException {
        Preconditions.checkNotNull(wallet);

        // Load membership data
        loadMembership(null, peer, wallet.getIdentity(), true);

        // Something wrong on pubkey : uid already used by another pubkey !
        if (wallet.getIdentity().getIsMember() == null) {
            throw new UidMatchAnotherPubkeyException(wallet.getPubKeyHash());
        }
    }

    /**
     * Load identity attribute isMember and timestamp
     *
     * @param identity
     */
    public void loadMembership(String currencyId, Identity identity, boolean checkLookupForNonMember) {
        loadMembership(currencyId, null, identity, checkLookupForNonMember);
    }

    public BlockchainMemberships getMembershipByUid(String currencyId, String uid) {
        Preconditions.checkArgument(StringUtils.isNotBlank(uid));

        BlockchainMemberships result = getMembershipByPubkeyOrUid(currencyId, uid);
        if (result == null || !uid.equals(result.getUid())) {
            return null;
        }
        return result;
    }

    public BlockchainMemberships getMembershipByPublicKey(String currencyId, String pubkey) {
        Preconditions.checkArgument(StringUtils.isNotBlank(pubkey));

        BlockchainMemberships result = getMembershipByPubkeyOrUid(currencyId, pubkey);
        if (result == null || !pubkey.equals(result.getPubkey())) {
            return null;
        }
        return result;
    }

    /**
     * Request to integrate the wot
     */
    public void requestMembership(Wallet wallet) {
        Preconditions.checkNotNull(wallet);
        Preconditions.checkNotNull(wallet.getCurrencyId());
        Preconditions.checkNotNull(wallet.getCertTimestamp());

        BlockchainBlock block = getCurrentBlock(wallet.getCurrencyId());

        // Compute membership document
        String membership = getMembership(wallet, block, true /*side in*/);

        if (log.isDebugEnabled()) {
            log.debug(String.format("Will send membership document: \n------\n%s------", membership));
        }

        List<NameValuePair> urlParameters = new ArrayList<NameValuePair>();
        urlParameters.add(new BasicNameValuePair("membership", membership));

        HttpPost httpPost = new HttpPost(getPath(wallet.getCurrencyId(), URL_MEMBERSHIP));
        try {
            httpPost.setEntity(new UrlEncodedFormEntity(urlParameters));
        } catch (UnsupportedEncodingException e) {
            throw new TechnicalException(e);
        }

        String membershipResult = executeRequest(httpPost, String.class);
        if (log.isDebugEnabled()) {
            log.debug("received from /tx/process: " + membershipResult);
        }

        executeRequest(httpPost, String.class);
    }

    public void requestMembership(Peer peer, String currency, byte[] pubKey, byte[] secKey, String uid,
            String membershipBlockUid, String selfBlockUid) {
        // http post /blockchain/membership
        HttpPost httpPost = new HttpPost(getPath(peer, URL_MEMBERSHIP));

        // compute the self-certification
        String membership = getSignedMembership(currency, pubKey, secKey, uid, membershipBlockUid, selfBlockUid,
                true/*side in*/);

        List<NameValuePair> urlParameters = new ArrayList<NameValuePair>();
        urlParameters.add(new BasicNameValuePair("membership", membership));

        try {
            httpPost.setEntity(new UrlEncodedFormEntity(urlParameters));
        } catch (UnsupportedEncodingException e) {
            throw new TechnicalException(e);
        }

        // Execute the request
        executeRequest(httpPost, String.class);
    }

    public BlockchainMemberships getMembershipByPubkeyOrUid(String currencyId, String uidOrPubkey) {
        String path = String.format(URL_MEMBERSHIP_SEARCH, uidOrPubkey);

        // search blockchain membership
        try {
            return executeRequest(currencyId, path, BlockchainMemberships.class);
        } catch (HttpBadRequestException e) {
            log.debug("No member matching this pubkey or uid: " + uidOrPubkey);
            return null;
        }
    }

    public BlockchainMemberships getMembershipByPubkeyOrUid(Peer peer, String uidOrPubkey) {
        String path = String.format(URL_MEMBERSHIP_SEARCH, uidOrPubkey);

        // search blockchain membership
        try {
            BlockchainMemberships result = executeRequest(peer, path, BlockchainMemberships.class);
            return result;
        } catch (HttpBadRequestException e) {
            log.debug("No member matching this pubkey or uid: " + uidOrPubkey);
            return null;
        }
    }

    public String getMembership(Wallet wallet, BlockchainBlock block, boolean sideIn) {

        // Create the member ship document
        String membership = getUnsignedMembership(wallet.getCurrency(), wallet.getPubKeyHash(), wallet.getUid(),
                block.getNumber() + '-' + block.getHash(), wallet.getCertTimestamp(), sideIn);

        // Add signature
        CryptoService cryptoService = ServiceLocator.instance().getCryptoService();
        String signature = cryptoService.sign(membership, wallet.getSecKey());

        return new StringBuilder().append(membership).append(signature).append('\n').toString();
    }

    /**
     * Get UD, by block number
     *
     * @param currencyId
     * @param startOffset
     * @return
     */
    public Map<Integer, Long> getUDs(String currencyId, long startOffset) {
        log.debug(String.format("Getting block's UD from block [%s]", startOffset));

        int[] blockNumbersWithUD = getBlocksWithUD(currencyId);

        Map<Integer, Long> result = new LinkedHashMap<Integer, Long>();

        //         Insert the UD0 (if need)
        //        if (startOffset <= 0) {
        //            BlockchainParameters parameters = getParameters(currencyId, true/*with cache*/);
        //            result.put(0, parameters.getUd0());
        //        }

        boolean previousBlockInsert = false;
        if (blockNumbersWithUD != null && blockNumbersWithUD.length != 0) {
            Integer previousBlockNumberWithUd = null;
            for (Integer blockNumber : blockNumbersWithUD) {
                if (blockNumber >= startOffset) {
                    if (!previousBlockInsert) {
                        Long previousUd = getParameters(currencyId, true/*with cache*/).getUd0();
                        Integer previousBlockNumber = 0;
                        if (previousBlockNumberWithUd != null) {
                            previousUd = getBlockDividend(currencyId, previousBlockNumberWithUd);
                            if (previousUd == null) {
                                throw new TechnicalException(String.format(
                                        "Unable to get UD from server block [%s]", previousBlockNumberWithUd));
                            }
                            previousBlockNumber = previousBlockNumberWithUd;
                        }
                        result.put(previousBlockNumber, previousUd);
                        previousBlockInsert = true;
                    }
                    Long ud = getBlockDividend(currencyId, blockNumber);
                    // Check not null (should never append)
                    if (ud == null) {
                        throw new TechnicalException(
                                String.format("Unable to get UD from server block [%s]", blockNumber));
                    }
                    result.put(blockNumber, ud);
                } else {
                    previousBlockNumberWithUd = blockNumber;
                }
            }
        } else {
            result.put(0, getParameters(currencyId, true/*with cache*/).getUd0());
        }

        return result;
    }

    @Override
    public WebsocketClientEndpoint addBlockListener(String currencyId,
            WebsocketClientEndpoint.MessageListener listener, boolean autoReconnect) {
        return addBlockListener(peerService.getActivePeerByCurrencyId(currencyId), listener, autoReconnect);
    }

    @Override
    public WebsocketClientEndpoint addBlockListener(Peer peer, WebsocketClientEndpoint.MessageListener listener,
            boolean autoReconnect) {
        Preconditions.checkNotNull(peer);
        Preconditions.checkNotNull(listener);

        // Get (or create) the websocket endpoint
        WebsocketClientEndpoint wsClientEndPoint = getWebsocketClientEndpoint(peer, URL_WS_BLOCK, autoReconnect);

        // add listener
        wsClientEndPoint.registerListener(listener);

        return wsClientEndPoint;
    }

    /* -- Internal methods -- */

    /**
     * Initialize caches
     */
    protected void initCaches() {
        int cacheTimeInMillis = config.getNetworkCacheTimeInMillis();

        mCurrentBlockCache = new SimpleCache<String, BlockchainBlock>(cacheTimeInMillis) {
            @Override
            public BlockchainBlock load(String currencyId) {
                return getCurrentBlock(currencyId);
            }
        };

        mParametersCache = new SimpleCache<String, BlockchainParameters>(/*eternal cache*/) {
            @Override
            public BlockchainParameters load(String currencyId) {
                return getParameters(currencyId);
            }
        };
    }

    protected void loadMembership(String currencyId, Peer peer, Identity identity,
            boolean checkLookupForNonMember) {
        Preconditions.checkNotNull(identity);
        Preconditions.checkArgument(StringUtils.isNotBlank(identity.getUid()));
        Preconditions.checkArgument(StringUtils.isNotBlank(identity.getPubkey()));
        Preconditions.checkArgument(peer != null || currencyId != null);

        // Read membership data from the UID
        BlockchainMemberships result = peer != null ? getMembershipByPubkeyOrUid(peer, identity.getUid())
                : getMembershipByPubkeyOrUid(currencyId, identity.getUid());

        // uid not used = not was member
        if (result == null) {
            identity.setMember(false);

            if (checkLookupForNonMember) {
                WotRemoteService wotService = ServiceLocator.instance().getWotRemoteService();
                Identity lookupIdentity = peer != null
                        ? wotService.getIdentity(peer, identity.getUid(), identity.getPubkey())
                        : wotService.getIdentity(currencyId, identity.getUid(), identity.getPubkey());

                // Self certification exists, update the cert timestamp
                if (lookupIdentity != null) {
                    identity.setTimestamp(lookupIdentity.getTimestamp());
                }

                // Self certification not exists: make sure the cert time is cleaning
                else {
                    identity.setTimestamp(null);
                }
            }
        }

        // UID and pubkey is a member: fine
        else if (identity.getPubkey().equals(result.getPubkey())) {
            identity.setMember(true);
            //FIXME identity.setTimestamp(result.getSigDate());
        }

        // Something wrong on pubkey : uid already used by anither pubkey !
        else {
            identity.setMember(null);
        }

    }

    private int[] getBlocksWithUD(String currencyId) {
        log.debug("Getting blocks with UD");

        String json = executeRequest(currencyId, URL_BLOCK_WITH_UD, String.class);

        int startIndex = json.indexOf("[");
        int endIndex = json.lastIndexOf(']');

        if (startIndex == -1 || endIndex == -1) {
            return null;
        }

        String blockNumbersStr = json.substring(startIndex + 1, endIndex).trim();

        if (StringUtils.isBlank(blockNumbersStr)) {
            return null;
        }

        String[] blockNumbers = blockNumbersStr.split(",");
        int[] result = new int[blockNumbers.length];
        try {
            int i = 0;
            for (String blockNumber : blockNumbers) {
                result[i++] = Integer.parseInt(blockNumber.trim());
            }
        } catch (NumberFormatException e) {
            if (log.isDebugEnabled()) {
                log.debug(String.format("Bad format of the response '%s'.", URL_BLOCK_WITH_UD));
            }
            throw new TechnicalException("Unable to read block with UD numbers: " + e.getMessage(), e);
        }

        return result;
    }

    protected String getSignedMembership(String currency, byte[] pubKey, byte[] secKey, String userId,
            String membershipBlockUid, String selfBlockUid, boolean sideIn) {
        // Compute the pub key hash
        String pubKeyHash = CryptoUtils.encodeBase58(pubKey);

        // Create the member ship document
        String membership = getUnsignedMembership(currency, pubKeyHash, userId, membershipBlockUid, selfBlockUid,
                sideIn);

        // Add signature
        CryptoService cryptoService = ServiceLocator.instance().getCryptoService();
        String signature = cryptoService.sign(membership, secKey);

        return new StringBuilder().append(membership).append(signature).append('\n').toString();
    }

    protected String getUnsignedMembership(String currency, String pubkey, String userId, String membershipBlockUid,
            String selfBlockUid, boolean sideIn) {
        // see https://github.com/ucoin-io/ucoin/blob/master/doc/Protocol.md#membership
        return new StringBuilder().append("Version: ").append(Protocol.VERSION).append("\nType: ")
                .append(Protocol.TYPE_MEMBERSHIP).append("\nCurrency: ").append(currency).append("\nIssuer: ")
                .append(pubkey).append("\nBlock: ").append(membershipBlockUid).append("\nMembership: ")
                .append(sideIn ? "IN" : "OUT").append("\nUserID: ").append(userId).append("\nCertTS: ")
                .append(selfBlockUid).append("\n").toString();
    }

    private Integer getLastBlockNumberFromJson(final String json) {
        int[] numbers = getBlockNumbersFromJson(json);
        if (numbers == null || numbers.length == 0) {
            return null;
        }
        return numbers[numbers.length - 1];
    }

    private int[] getBlockNumbersFromJson(final String json) {

        String arrayPrefix = "\"blocks\": [";
        int startIndex = json.indexOf(arrayPrefix);
        int endIndex = json.lastIndexOf(']');
        if (startIndex == -1 || endIndex == -1) {
            return null;
        }

        String jsonArrayContent = json.substring(startIndex + arrayPrefix.length(), endIndex).trim();
        if (jsonArrayContent.length() == 0) {
            return null;
        }

        String[] blockNumbers = jsonArrayContent.split(",");

        try {
            int[] result = new int[blockNumbers.length];
            int index = 0;
            for (String blockNumber : blockNumbers) {
                result[index++] = Integer.parseInt(blockNumber.trim());
            }
            return result;
        } catch (NumberFormatException e) {
            if (log.isDebugEnabled()) {
                log.debug("Could not parse JSON (block numbers)");
            }
            throw new TechnicalException("Could not parse server response");
        }
    }

    protected Long getDividendFromBlockJson(String blockJson) {

        int startIndex = blockJson.indexOf(JSON_DIVIDEND_ATTR);
        if (startIndex == -1) {
            return null;
        }
        startIndex += JSON_DIVIDEND_ATTR.length();
        int endIndex = blockJson.indexOf(',', startIndex);
        if (endIndex == -1) {
            return null;
        }

        String dividendStr = blockJson.substring(startIndex, endIndex).trim();
        if (dividendStr.length() == 0 || "null".equals(dividendStr)) {
            return null;
        }

        return Long.parseLong(dividendStr);
    }
}