com.ethercamp.harmony.jsonrpc.EthJsonRpcImpl.java Source code

Java tutorial

Introduction

Here is the source code for com.ethercamp.harmony.jsonrpc.EthJsonRpcImpl.java

Source

/*
 * Copyright 2015, 2016 Ether.Camp Inc. (US)
 * This file is part of Ethereum Harmony.
 *
 * Ethereum Harmony 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.
 *
 * Ethereum Harmony 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 Ethereum Harmony.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.ethercamp.harmony.jsonrpc;

import com.ethercamp.harmony.keystore.Keystore;
import com.ethercamp.harmony.util.ErrorCodes;
import com.ethercamp.harmony.util.exception.HarmonyException;
import com.google.common.collect.ImmutableMap;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.map.LRUMap;
import org.ethereum.config.CommonConfig;
import org.ethereum.config.SystemProperties;
import org.ethereum.core.*;
import org.ethereum.crypto.ECKey;
import org.ethereum.crypto.HashUtil;
import org.ethereum.db.BlockStore;
import org.ethereum.db.ByteArrayWrapper;
import org.ethereum.core.TransactionInfo;
import org.ethereum.db.TransactionStore;
import org.ethereum.facade.Ethereum;
import org.ethereum.listener.CompositeEthereumListener;
import org.ethereum.listener.EthereumListenerAdapter;
import org.ethereum.manager.WorldManager;
import org.ethereum.mine.BlockMiner;
import org.ethereum.mine.EthashAlgo;
import org.ethereum.mine.MinerIfc;
import org.ethereum.net.client.ConfigCapabilities;
import org.ethereum.net.rlpx.Node;
import org.ethereum.net.rlpx.discover.NodeManager;
import org.ethereum.net.server.ChannelManager;
import org.ethereum.net.server.PeerServer;
import org.ethereum.solidity.compiler.SolidityCompiler;
import org.ethereum.sync.SyncManager;
import org.ethereum.util.BuildInfo;
import org.ethereum.util.ByteUtil;
import org.ethereum.vm.DataWord;
import org.ethereum.vm.LogInfo;
import org.ethereum.vm.program.invoke.ProgramInvokeFactory;
import org.spongycastle.util.encoders.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import javax.annotation.PostConstruct;
import java.lang.reflect.Modifier;
import java.math.BigInteger;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import static com.ethercamp.harmony.jsonrpc.TypeConverter.*;
import static java.math.BigInteger.valueOf;
import static org.ethereum.crypto.HashUtil.sha3;
import static org.ethereum.util.ByteUtil.*;

/**
 * @author Anton Nashatyrev
 */
@Slf4j(topic = "jsonrpc")
@Service
// renamed to not conflict with class from core
// wait for core class to be removed
public class EthJsonRpcImpl implements JsonRpc {

    private static final String BLOCK_LATEST = "latest";

    private volatile String hashrate;

    public class BinaryCallArguments {
        public long nonce;
        public long gasPrice;
        public long gasLimit;
        public String toAddress;
        public String fromAddress;
        public long value;
        public byte[] data;

        public void setArguments(CallArguments args) throws Exception {
            nonce = 0;
            if (args.nonce != null && args.nonce.length() != 0)
                nonce = JSonHexToLong(args.nonce);

            gasPrice = 0;
            if (args.gasPrice != null && args.gasPrice.length() != 0)
                gasPrice = JSonHexToLong(args.gasPrice);

            gasLimit = 4_000_000;
            if (args.gas != null && args.gas.length() != 0)
                gasLimit = JSonHexToLong(args.gas);

            toAddress = null;
            if (args.to != null && !args.to.isEmpty())
                toAddress = JSonHexToHex(args.to);

            fromAddress = null;
            if (args.from != null && !args.from.isEmpty())
                fromAddress = JSonHexToHex(args.from);

            value = 0;
            if (args.value != null && args.value.length() != 0)
                value = JSonHexToLong(args.value);

            data = null;

            if (args.data != null && args.data.length() != 0)
                data = TypeConverter.StringHexToByteArray(args.data);
        }
    }

    @Autowired
    Keystore keystore;

    @Autowired
    public WorldManager worldManager;

    @Autowired
    public Repository repository;

    @Autowired
    BlockchainImpl blockchain;

    @Autowired
    Ethereum eth;

    @Autowired
    PeerServer peerServer;

    @Autowired
    SyncManager syncManager;

    @Autowired
    TransactionStore txStore;

    @Autowired
    ChannelManager channelManager;

    @Autowired
    NodeManager nodeManager;

    @Autowired
    CompositeEthereumListener compositeEthereumListener;

    @Autowired
    BlockMiner blockMiner;

    @Autowired
    TransactionStore transactionStore;

    @Autowired
    PendingStateImpl pendingState;

    @Autowired
    SystemProperties config;

    @Autowired
    ConfigCapabilities configCapabilities;

    @Autowired
    BlockStore blockStore;

    @Autowired
    ProgramInvokeFactory programInvokeFactory;

    @Autowired
    CommonConfig commonConfig = CommonConfig.getDefault();

    /**
     * Lowercase hex address as a key.
     */
    Map<String, Account> unlockedAccounts = new ConcurrentHashMap<>();

    /**
     * State fields
     */
    protected volatile long initialBlockNumber;

    AtomicInteger filterCounter = new AtomicInteger(1);
    Map<Integer, Filter> installedFilters = new Hashtable<>();
    Map<ByteArrayWrapper, TransactionReceipt> pendingReceipts = Collections.synchronizedMap(new LRUMap<>(1024));

    Map<ByteArrayWrapper, Block> miningBlocks = new ConcurrentHashMap<>();

    volatile Block miningBlock;

    volatile SettableFuture<MinerIfc.MiningResult> miningTask;

    final MinerIfc externalMiner = new MinerIfc() {
        @Override
        public ListenableFuture<MiningResult> mine(Block block) {
            miningBlock = block;
            miningTask = SettableFuture.create();
            return miningTask;
        }

        @Override
        public boolean validate(BlockHeader blockHeader) {
            return false;
        }
    };

    boolean minerInitialized = false;

    @PostConstruct
    private void init() {
        initialBlockNumber = blockchain.getBestBlock().getNumber();

        compositeEthereumListener.addListener(new EthereumListenerAdapter() {
            @Override
            public void onBlock(Block block, List<TransactionReceipt> receipts) {
                for (Filter filter : installedFilters.values()) {
                    filter.newBlockReceived(block);
                }
            }

            @Override
            public void onPendingTransactionsReceived(List<Transaction> transactions) {
                for (Filter filter : installedFilters.values()) {
                    for (Transaction tx : transactions) {
                        filter.newPendingTx(tx);
                    }
                }
            }

            @Override
            public void onPendingTransactionUpdate(TransactionReceipt txReceipt, PendingTransactionState state,
                    Block block) {
                ByteArrayWrapper txHashW = new ByteArrayWrapper(txReceipt.getTransaction().getHash());
                if (state.isPending() || state == PendingTransactionState.DROPPED) {
                    pendingReceipts.put(txHashW, txReceipt);
                } else {
                    pendingReceipts.remove(txHashW);
                }
            }
        });

    }

    private long JSonHexToLong(String x) throws Exception {
        if (!x.startsWith("0x"))
            throw new Exception("Incorrect hex syntax");
        x = x.substring(2);
        return Long.parseLong(x, 16);
    }

    private int JSonHexToInt(String x) throws Exception {
        if (!x.startsWith("0x"))
            throw new Exception("Incorrect hex syntax");
        x = x.substring(2);
        return Integer.parseInt(x, 16);
    }

    private String JSonHexToHex(String x) {
        if (!x.startsWith("0x"))
            throw new RuntimeException("Incorrect hex syntax");
        x = x.substring(2);
        return x;
    }

    private Block getBlockByJSonHash(String blockHash) throws Exception {
        byte[] bhash = TypeConverter.StringHexToByteArray(blockHash);
        return worldManager.getBlockchain().getBlockByHash(bhash);
    }

    private Block getByJsonBlockId(String id) {
        if ("earliest".equalsIgnoreCase(id)) {
            return blockchain.getBlockByNumber(0);
        } else if ("latest".equalsIgnoreCase(id)) {
            return blockchain.getBestBlock();
        } else if ("pending".equalsIgnoreCase(id)) {
            return null;
        } else {
            long blockNumber = StringHexToBigInteger(id).longValue();
            return blockchain.getBlockByNumber(blockNumber);
        }
    }

    private Repository getRepoByJsonBlockId(String id) {
        if ("pending".equalsIgnoreCase(id)) {
            return pendingState.getRepository();
        } else {
            Block block = getByJsonBlockId(id);
            return this.repository.getSnapshotTo(block.getStateRoot());
        }
    }

    private List<Transaction> getTransactionsByJsonBlockId(String id) {
        if ("pending".equalsIgnoreCase(id)) {
            return pendingState.getPendingTransactions();
        } else {
            Block block = getByJsonBlockId(id);
            return block != null ? block.getTransactionsList() : null;
        }
    }

    /**
     * @param address
     * @return unlocked account with private key ready for signing tx
     * @throws RuntimeException if account is not unlocked or not found in keystore
     */
    protected Account getAccountFromKeystore(String address) throws RuntimeException {
        if (address.indexOf("0x") == 0) {
            address = address.substring(2);
        }
        final Account account = unlockedAccounts.get(address.toLowerCase());
        if (account != null) {
            return account;
        }

        if (keystore.hasStoredKey(address)) {
            throw new HarmonyException("Unlocked account is required. Account: " + address,
                    ErrorCodes.ERROR__101_UNLOCK_ACCOUNT);
        } else {
            throw new HarmonyException("Key not found in keystore", ErrorCodes.ERROR__102_KEY_NOT_FOUND);
        }
    }

    protected Account importAccount(ECKey key, String password) {
        final Account account = new Account();
        account.init(key);

        keystore.storeKey(key, password);
        return account;
    }

    public String web3_clientVersion() {
        Pattern shortVersion = Pattern.compile("(\\d\\.\\d).*");
        Matcher matcher = shortVersion.matcher(System.getProperty("java.version"));
        matcher.matches();

        return Arrays
                .asList("Harmony", "v" + config.projectVersion(), System.getProperty("os.name"),
                        "Java" + matcher.group(1), config.projectVersionModifier() + "-" + BuildInfo.buildHash)
                .stream().collect(Collectors.joining("/"));
    }

    public String web3_sha3(String data) throws Exception {
        byte[] result = HashUtil.sha3(data.getBytes());
        return TypeConverter.toJsonHex(result);
    }

    public String net_version() {
        return eth_protocolVersion();
    }

    public String net_peerCount() {
        int size = channelManager.getActivePeers().size();
        return TypeConverter.toJsonHex(size);
    }

    public boolean net_listening() {
        return peerServer.isListening();
    }

    public String eth_protocolVersion() {
        return configCapabilities.getConfigCapabilities().stream().filter(p -> p.isEth())
                .map(p -> ((Byte) p.getVersion()).intValue()).reduce(0, (state, v) -> Math.max(state, v))
                .toString();
    }

    public Object eth_syncing() {
        if (syncManager.isSyncDone()) {
            return false;
        } else {
            return new SyncingResult(TypeConverter.toJsonHex(initialBlockNumber),
                    TypeConverter.toJsonHex(blockchain.getBestBlock().getNumber()),
                    TypeConverter.toJsonHex(syncManager.getLastKnownBlockNumber()));
        }
    }

    public String eth_coinbase() {
        return toJsonHex(blockchain.getMinerCoinbase());
    }

    public boolean eth_mining() {
        return blockMiner.isMining();
    }

    public String eth_hashrate() {
        return hashrate;
    }

    public String eth_gasPrice() {
        return TypeConverter.toJsonHex(eth.getGasPrice());
    }

    public String[] eth_accounts() {
        return personal_listAccounts();
    }

    public String eth_blockNumber() {
        return TypeConverter.toJsonHex(blockchain.getBestBlock().getNumber());
    }

    public String eth_getBalance(String address, String blockId) throws Exception {
        Objects.requireNonNull(address, "address is required");
        blockId = blockId == null ? BLOCK_LATEST : blockId;

        byte[] addressAsByteArray = TypeConverter.StringHexToByteArray(address);
        BigInteger balance = getRepoByJsonBlockId(blockId).getBalance(addressAsByteArray);
        return TypeConverter.toJsonHex(balance);
    }

    public String eth_getLastBalance(String address) throws Exception {
        return eth_getBalance(address, BLOCK_LATEST);
    }

    @Override
    public String eth_getStorageAt(String address, String storageIdx, String blockId) throws Exception {
        byte[] addressAsByteArray = StringHexToByteArray(address);
        DataWord storageValue = getRepoByJsonBlockId(blockId).getStorageValue(addressAsByteArray,
                new DataWord(StringHexToByteArray(storageIdx)));
        return storageValue != null ? TypeConverter.toJsonHex(storageValue.getData()) : null;
    }

    @Override
    public String eth_getTransactionCount(String address, String blockId) throws Exception {
        byte[] addressAsByteArray = TypeConverter.StringHexToByteArray(address);
        BigInteger nonce = getRepoByJsonBlockId(blockId).getNonce(addressAsByteArray);
        return TypeConverter.toJsonHex(nonce);
    }

    public String eth_getBlockTransactionCountByHash(String blockHash) throws Exception {
        Block b = getBlockByJSonHash(blockHash);
        if (b == null)
            return null;
        long n = b.getTransactionsList().size();
        return TypeConverter.toJsonHex(n);
    }

    public String eth_getBlockTransactionCountByNumber(String bnOrId) throws Exception {
        List<Transaction> list = getTransactionsByJsonBlockId(bnOrId);
        if (list == null)
            return null;
        long n = list.size();
        return TypeConverter.toJsonHex(n);
    }

    public String eth_getUncleCountByBlockHash(String blockHash) throws Exception {
        Block b = getBlockByJSonHash(blockHash);
        if (b == null)
            return null;
        long n = b.getUncleList().size();
        return TypeConverter.toJsonHex(n);
    }

    public String eth_getUncleCountByBlockNumber(String bnOrId) throws Exception {
        Block b = getByJsonBlockId(bnOrId);
        if (b == null)
            return null;
        long n = b.getUncleList().size();
        return TypeConverter.toJsonHex(n);
    }

    public String eth_getCode(String address, String blockId) throws Exception {
        byte[] addressAsByteArray = TypeConverter.StringHexToByteArray(address);
        byte[] code = getRepoByJsonBlockId(blockId).getCode(addressAsByteArray);
        return TypeConverter.toJsonHex(code);
    }

    /**
     * Sign message hash with key to produce Elliptic Curve Digital Signature (ECDSA) signature.
     *
     * Note: implementation may be different to other Ethereum node implementations.
     *
     * @param address - address to sign. Account must be unlocked
     * @param messageHash - sha3 of message
     * @return ECDSA signature (in hex)
     * @throws Exception
     */
    public String eth_sign(String address, String messageHash) throws Exception {
        String ha = JSonHexToHex(address);
        Account account = getAccountFromKeystore(ha);

        ECKey.ECDSASignature signature = account.getEcKey().sign(Hex.decode(JSonHexToHex(messageHash)));
        byte[] signatureBytes = toByteArray(signature);

        return TypeConverter.toJsonHex(signatureBytes);
    }

    private byte[] toByteArray(ECKey.ECDSASignature signature) {
        final byte fixedV = signature.v >= 27 ? (byte) (signature.v - 27) : signature.v;

        return ByteUtil.merge(ByteUtil.bigIntegerToBytes(signature.r), ByteUtil.bigIntegerToBytes(signature.s),
                new byte[] { fixedV });
    }

    public String eth_sendTransaction(CallArguments args) throws Exception {
        Account account = getAccountFromKeystore(JSonHexToHex(args.from));

        return sendTransaction(args, account);
    }

    private String sendTransaction(CallArguments args, Account account) {
        if (args.data != null && args.data.startsWith("0x"))
            args.data = args.data.substring(2);

        // convert zero to empty byte array
        // TEMP, until decide for better behavior
        final BigInteger valueBigInt = args.value != null ? StringHexToBigInteger(args.value) : BigInteger.ZERO;
        final byte[] value = !valueBigInt.equals(BigInteger.ZERO) ? ByteUtil.bigIntegerToBytes(valueBigInt)
                : EMPTY_BYTE_ARRAY;

        final Transaction tx = new Transaction(
                args.nonce != null ? StringHexToByteArray(args.nonce)
                        : bigIntegerToBytes(pendingState.getRepository().getNonce(account.getAddress())),
                args.gasPrice != null ? StringHexToByteArray(args.gasPrice)
                        : ByteUtil.longToBytesNoLeadZeroes(eth.getGasPrice()),
                args.gas != null ? StringHexToByteArray(args.gas) : ByteUtil.longToBytes(90_000),
                args.to != null ? StringHexToByteArray(args.to) : EMPTY_BYTE_ARRAY, value,
                args.data != null ? StringHexToByteArray(args.data) : EMPTY_BYTE_ARRAY);

        tx.sign(account.getEcKey());

        validateAndSubmit(tx);

        return TypeConverter.toJsonHex(tx.getHash());
    }

    public String eth_sendTransactionArgs(String from, String to, String gas, String gasPrice, String value,
            String data, String nonce) throws Exception {

        final Account account = getAccountFromKeystore(from);

        return sendTransaction(new CallArguments(from, to, gas, gasPrice, value, data, nonce), account);
    }

    public String eth_sendRawTransaction(String rawData) throws Exception {
        Transaction tx = new Transaction(StringHexToByteArray(rawData));

        tx.rlpParse();
        validateAndSubmit(tx);

        return TypeConverter.toJsonHex(tx.getHash());
    }

    protected void validateAndSubmit(Transaction tx) {
        if (tx.getValue().length > 0 && tx.getValue()[0] == 0) {
            // zero value should be sent as empty byte array
            // otherwise tx will be accepted by core, but never included in block
            //            throw new RuntimeException("Field 'value' should not have leading zero");
            log.warn("Transaction might use incorrect zero value");
        }

        eth.submitTransaction(tx);
    }

    protected TransactionReceipt createCallTxAndExecute(CallArguments args, Block block) throws Exception {
        Repository repository = ((Repository) worldManager.getRepository()).getSnapshotTo(block.getStateRoot())
                .startTracking();

        return createCallTxAndExecute(args, block, repository, worldManager.getBlockStore());
    }

    protected TransactionReceipt createCallTxAndExecute(CallArguments args, Block block, Repository repository,
            BlockStore blockStore) throws Exception {
        BinaryCallArguments bca = new BinaryCallArguments();
        bca.setArguments(args);
        Transaction rawTransaction = CallTransaction.createRawTransaction(0, bca.gasPrice, bca.gasLimit,
                bca.toAddress, bca.value, bca.data);
        LocalTransaction tx = new LocalTransaction(rawTransaction.getEncoded());

        // handle from address without signing
        if (args.from != null) {
            tx.setSender(hexStringToBytes(args.from));
        } else {
            // put mock signature if not present
            tx.sign(ECKey.fromPrivate(new byte[32]));
        }

        try {
            TransactionExecutor executor = commonConfig.transactionExecutor(tx, block.getCoinbase(), repository,
                    blockStore, programInvokeFactory, block, new EthereumListenerAdapter(), 0).setLocalCall(true);

            executor.init();
            executor.execute();
            executor.go();
            executor.finalization();

            return executor.getReceipt();
        } finally {
            repository.rollback();
        }
    }

    public String eth_call(CallArguments args, String bnOrId) throws Exception {
        TransactionReceipt res;
        if ("pending".equals(bnOrId)) {
            Block pendingBlock = blockchain.createNewBlock(blockchain.getBestBlock(),
                    pendingState.getPendingTransactions(), Collections.<BlockHeader>emptyList());
            res = createCallTxAndExecute(args, pendingBlock, pendingState.getRepository(),
                    worldManager.getBlockStore());
        } else {
            res = createCallTxAndExecute(args, getByJsonBlockId(bnOrId));
        }
        return TypeConverter.toJsonHex(res.getExecutionResult());
    }

    public String eth_estimateGas(CallArguments args) throws Exception {
        TransactionReceipt res = createCallTxAndExecute(args, blockchain.getBestBlock());
        return TypeConverter.toJsonHex(res.getGasUsed());
    }

    protected BlockResult getBlockResult(Block block, boolean fullTx) {
        if (block == null)
            return null;
        boolean isPending = ByteUtil.byteArrayToLong(block.getNonce()) == 0;
        BlockResult br = new BlockResult();
        br.number = isPending ? null : toJsonHex(block.getNumber());
        br.hash = isPending ? null : toJsonHex(block.getHash());
        br.parentHash = toJsonHex(block.getParentHash());
        br.nonce = isPending ? null : toJsonHex(block.getNonce());
        br.sha3Uncles = toJsonHex(block.getUnclesHash());
        br.logsBloom = isPending ? null : toJsonHex(block.getLogBloom());
        br.transactionsRoot = toJsonHex(block.getTxTrieRoot());
        br.stateRoot = toJsonHex(block.getStateRoot());
        br.receiptRoot = toJsonHex(block.getReceiptsRoot());
        br.miner = isPending ? null : toJsonHex(block.getCoinbase());
        br.difficulty = toJsonHex(block.getDifficultyBI());
        br.totalDifficulty = toJsonHex(blockStore.getTotalDifficultyForHash(block.getHash()));
        if (block.getExtraData() != null)
            br.extraData = toJsonHex(block.getExtraData());
        br.size = toJsonHex(block.getEncoded().length);
        br.gasLimit = toJsonHex(block.getGasLimit());
        br.gasUsed = toJsonHex(block.getGasUsed());
        br.timestamp = toJsonHex(block.getTimestamp());

        List<Object> txes = new ArrayList<>();
        if (fullTx) {
            for (int i = 0; i < block.getTransactionsList().size(); i++) {
                txes.add(new TransactionResultDTO(block, i, block.getTransactionsList().get(i)));
            }
        } else {
            for (Transaction tx : block.getTransactionsList()) {
                txes.add(toJsonHex(tx.getHash()));
            }
        }
        br.transactions = txes.toArray();

        List<String> ul = new ArrayList<>();
        for (BlockHeader header : block.getUncleList()) {
            ul.add(toJsonHex(header.getHash()));
        }
        br.uncles = ul.toArray(new String[ul.size()]);

        return br;
    }

    public BlockResult eth_getBlockByHash(String blockHash, Boolean fullTransactionObjects) throws Exception {
        final Block b = getBlockByJSonHash(blockHash);
        return getBlockResult(b, fullTransactionObjects);
    }

    public BlockResult eth_getBlockByNumber(String bnOrId, Boolean fullTransactionObjects) throws Exception {
        final Block b;
        if ("pending".equalsIgnoreCase(bnOrId)) {
            b = blockchain.createNewBlock(blockchain.getBestBlock(), pendingState.getPendingTransactions(),
                    Collections.<BlockHeader>emptyList());
        } else {
            b = getByJsonBlockId(bnOrId);
        }
        return (b == null ? null : getBlockResult(b, fullTransactionObjects));
    }

    public TransactionResultDTO eth_getTransactionByHash(String transactionHash) throws Exception {
        final byte[] txHash = StringHexToByteArray(transactionHash);

        final TransactionInfo txInfo = blockchain.getTransactionInfo(txHash);
        if (txInfo == null) {
            return null;
        }

        final Block block = blockchain.getBlockByHash(txInfo.getBlockHash());
        // need to return txes only from main chain
        final Block mainBlock = blockchain.getBlockByNumber(block.getNumber());
        if (!Arrays.equals(block.getHash(), mainBlock.getHash())) {
            return null;
        }
        txInfo.setTransaction(block.getTransactionsList().get(txInfo.getIndex()));

        return new TransactionResultDTO(block, txInfo.getIndex(), txInfo.getReceipt().getTransaction());
    }

    public TransactionResultDTO eth_getTransactionByBlockHashAndIndex(String blockHash, String index)
            throws Exception {
        Block b = getBlockByJSonHash(blockHash);
        if (b == null)
            return null;
        int idx = JSonHexToInt(index);
        if (idx >= b.getTransactionsList().size())
            return null;
        Transaction tx = b.getTransactionsList().get(idx);
        return new TransactionResultDTO(b, idx, tx);
    }

    public TransactionResultDTO eth_getTransactionByBlockNumberAndIndex(String bnOrId, String index)
            throws Exception {
        Block b = getByJsonBlockId(bnOrId);
        List<Transaction> txs = getTransactionsByJsonBlockId(bnOrId);
        if (txs == null)
            return null;
        int idx = JSonHexToInt(index);
        if (idx >= txs.size())
            return null;
        Transaction tx = txs.get(idx);
        return new TransactionResultDTO(b, idx, tx);
    }

    public TransactionReceiptDTO eth_getTransactionReceipt(String transactionHash) throws Exception {
        final byte[] hash = TypeConverter.StringHexToByteArray(transactionHash);

        final TransactionInfo txInfo = blockchain.getTransactionInfo(hash);

        if (txInfo == null)
            return null;

        final Block block = blockchain.getBlockByHash(txInfo.getBlockHash());
        final Block mainBlock = blockchain.getBlockByNumber(block.getNumber());

        // need to return txes only from main chain
        if (!Arrays.equals(block.getHash(), mainBlock.getHash())) {
            return null;
        }

        return new TransactionReceiptDTO(block, txInfo);
    }

    @Override
    public TransactionReceiptDTOExt ethj_getTransactionReceipt(String transactionHash) throws Exception {
        byte[] hash = TypeConverter.StringHexToByteArray(transactionHash);

        TransactionReceipt pendingReceipt = pendingReceipts.get(new ByteArrayWrapper(hash));

        TransactionInfo txInfo;
        Block block;

        if (pendingReceipt != null) {
            txInfo = new TransactionInfo(pendingReceipt);
            block = null;
        } else {
            txInfo = blockchain.getTransactionInfo(hash);

            if (txInfo == null)
                return null;

            block = blockchain.getBlockByHash(txInfo.getBlockHash());

            // need to return txes only from main chain
            Block mainBlock = blockchain.getBlockByNumber(block.getNumber());
            if (!Arrays.equals(block.getHash(), mainBlock.getHash())) {
                return null;
            }
        }

        return new TransactionReceiptDTOExt(block, txInfo);
    }

    @Override
    public BlockResult eth_getUncleByBlockHashAndIndex(String blockHash, String uncleIdx) throws Exception {
        Block block = blockchain.getBlockByHash(StringHexToByteArray(blockHash));
        if (block == null)
            return null;
        int idx = JSonHexToInt(uncleIdx);
        if (idx >= block.getUncleList().size())
            return null;
        BlockHeader uncleHeader = block.getUncleList().get(idx);
        Block uncle = blockchain.getBlockByHash(uncleHeader.getHash());
        if (uncle == null) {
            uncle = new Block(uncleHeader, Collections.<Transaction>emptyList(),
                    Collections.<BlockHeader>emptyList());
        }
        return getBlockResult(uncle, false);
    }

    @Override
    public BlockResult eth_getUncleByBlockNumberAndIndex(String blockId, String uncleIdx) throws Exception {
        Block block = getByJsonBlockId(blockId);
        return block == null ? null : eth_getUncleByBlockHashAndIndex(toJsonHex(block.getHash()), uncleIdx);
    }

    @Override
    public String[] eth_getCompilers() {
        return new String[] { "solidity" };
    }

    //    @Override
    //    public CompilationResult eth_compileLLL(String contract) {
    //        throw new UnsupportedOperationException("LLL compiler not supported");
    //    }

    @Override
    public CompilationResult eth_compileSolidity(String contract) throws Exception {
        SolidityCompiler.Result res = SolidityCompiler.compile(contract.getBytes(), true,
                SolidityCompiler.Options.ABI, SolidityCompiler.Options.BIN, SolidityCompiler.Options.INTERFACE);
        if (res.isFailed()) {
            throw new RuntimeException("Compilation error: " + res.errors);
        }
        org.ethereum.solidity.compiler.CompilationResult result = org.ethereum.solidity.compiler.CompilationResult
                .parse(res.output);
        CompilationResult ret = new CompilationResult();
        org.ethereum.solidity.compiler.CompilationResult.ContractMetadata contractMetadata = result.contracts
                .values().iterator().next();
        ret.code = toJsonHex(contractMetadata.bin);
        ret.info = new CompilationInfo();
        ret.info.source = contract;
        ret.info.language = "Solidity";
        ret.info.languageVersion = "0";
        ret.info.compilerVersion = result.version;
        ret.info.abiDefinition = new CallTransaction.Contract(contractMetadata.abi).functions;
        return ret;
    }

    //    @Override
    //    public CompilationResult eth_compileSerpent(String contract){
    //        throw new UnsupportedOperationException("Serpent compiler not supported");
    //    }
    //
    //    @Override
    //    public String eth_resend() {
    //        throw new UnsupportedOperationException("JSON RPC method eth_resend not implemented yet");
    //    }
    //
    //    @Override
    //    public String eth_pendingTransactions() {
    //        throw new UnsupportedOperationException("JSON RPC method eth_pendingTransactions not implemented yet");
    //    }

    static class Filter {
        static final int MAX_EVENT_COUNT = 1024; // prevent OOM when Filers are forgotten

        static abstract class FilterEvent {
            public abstract Object getJsonEventObject();
        }

        List<FilterEvent> events = new LinkedList<>();

        public synchronized boolean hasNew() {
            return !events.isEmpty();
        }

        public synchronized Object[] poll() {
            Object[] ret = new Object[events.size()];
            for (int i = 0; i < ret.length; i++) {
                ret[i] = events.get(i).getJsonEventObject();
            }
            this.events.clear();
            return ret;
        }

        protected synchronized void add(FilterEvent evt) {
            events.add(evt);
            if (events.size() > MAX_EVENT_COUNT)
                events.remove(0);
        }

        public void newBlockReceived(Block b) {
        }

        public void newPendingTx(Transaction tx) {
        }
    }

    static class NewBlockFilter extends Filter {
        class NewBlockFilterEvent extends FilterEvent {
            public final Block b;

            NewBlockFilterEvent(Block b) {
                this.b = b;
            }

            @Override
            public String getJsonEventObject() {
                return toJsonHex(b.getHash());
            }
        }

        public void newBlockReceived(Block b) {
            add(new NewBlockFilterEvent(b));
        }
    }

    static class PendingTransactionFilter extends Filter {
        class PendingTransactionFilterEvent extends FilterEvent {
            private final Transaction tx;

            PendingTransactionFilterEvent(Transaction tx) {
                this.tx = tx;
            }

            @Override
            public String getJsonEventObject() {
                return toJsonHex(tx.getHash());
            }
        }

        public void newPendingTx(Transaction tx) {
            add(new PendingTransactionFilterEvent(tx));
        }
    }

    class JsonLogFilter extends Filter {
        class LogFilterEvent extends FilterEvent {
            private final LogFilterElement el;

            LogFilterEvent(LogFilterElement el) {
                this.el = el;
            }

            @Override
            public LogFilterElement getJsonEventObject() {
                return el;
            }
        }

        LogFilter logFilter;
        boolean onNewBlock;
        boolean onPendingTx;

        public JsonLogFilter(LogFilter logFilter) {
            this.logFilter = logFilter;
        }

        void onLogMatch(LogInfo logInfo, Block b, int txIndex, Transaction tx, int logIdx) {
            add(new LogFilterEvent(new LogFilterElement(logInfo, b, txIndex, tx, logIdx)));
        }

        void onTransactionReceipt(TransactionReceipt receipt, Block b, int txIndex) {
            if (logFilter.matchBloom(receipt.getBloomFilter())) {
                int logIdx = 0;
                for (LogInfo logInfo : receipt.getLogInfoList()) {
                    if (logFilter.matchBloom(logInfo.getBloom()) && logFilter.matchesExactly(logInfo)) {
                        onLogMatch(logInfo, b, txIndex, receipt.getTransaction(), logIdx);
                    }
                    logIdx++;
                }
            }
        }

        void onTransaction(Transaction tx, Block b, int txIndex) {
            if (logFilter.matchesContractAddress(tx.getReceiveAddress())) {
                TransactionInfo txInfo = blockchain.getTransactionInfo(tx.getHash());
                onTransactionReceipt(txInfo.getReceipt(), b, txIndex);
            }
        }

        void onBlock(Block b) {
            if (logFilter.matchBloom(new Bloom(b.getLogBloom()))) {
                int txIdx = 0;
                for (Transaction tx : b.getTransactionsList()) {
                    onTransaction(tx, b, txIdx);
                    txIdx++;
                }
            }
        }

        @Override
        public void newBlockReceived(Block b) {
            if (onNewBlock)
                onBlock(b);
        }

        @Override
        public void newPendingTx(Transaction tx) {
            // TODO add TransactionReceipt for PendingTx
            //            if (onPendingTx)
        }
    }

    @Override
    public String eth_newFilter(FilterRequest fr) throws Exception {
        LogFilter logFilter = new LogFilter();

        if (fr.address instanceof String) {
            logFilter.withContractAddress(StringHexToByteArray((String) fr.address));
        } else if (fr.address instanceof String[]) {
            List<byte[]> addr = new ArrayList<>();
            for (String s : ((String[]) fr.address)) {
                addr.add(StringHexToByteArray(s));
            }
            logFilter.withContractAddress(addr.toArray(new byte[0][]));
        }

        if (fr.topics != null) {
            for (Object topic : fr.topics) {
                if (topic == null) {
                    logFilter.withTopic((byte[][]) null);
                } else if (topic instanceof String) {
                    logFilter.withTopic(new DataWord(StringHexToByteArray((String) topic)).getData());
                } else if (topic instanceof String[]) {
                    List<byte[]> t = new ArrayList<>();
                    for (String s : ((String[]) topic)) {
                        t.add(new DataWord(StringHexToByteArray(s)).getData());
                    }
                    logFilter.withTopic(t.toArray(new byte[0][]));
                }
            }
        }

        JsonLogFilter filter = new JsonLogFilter(logFilter);
        int id = filterCounter.getAndIncrement();
        installedFilters.put(id, filter);

        final Block blockFrom = fr.fromBlock == null ? null : getByJsonBlockId(fr.fromBlock);
        Block blockTo = fr.toBlock == null ? null : getByJsonBlockId(fr.toBlock);

        if (blockFrom != null) {
            // need to add historical data
            blockTo = blockTo == null ? blockchain.getBestBlock() : blockTo;
            for (long blockNum = blockFrom.getNumber(); blockNum <= blockTo.getNumber(); blockNum++) {
                filter.onBlock(blockchain.getBlockByNumber(blockNum));
            }
        }

        // the following is not precisely documented
        if ("pending".equalsIgnoreCase(fr.fromBlock) || "pending".equalsIgnoreCase(fr.toBlock)) {
            filter.onPendingTx = true;
        } else if (fr.toBlock == null || "latest".equalsIgnoreCase(fr.fromBlock)
                || "latest".equalsIgnoreCase(fr.toBlock)) {
            filter.onNewBlock = true;
        }

        return toJsonHex(id);
    }

    @Override
    public String eth_newBlockFilter() {
        int id = filterCounter.getAndIncrement();
        installedFilters.put(id, new NewBlockFilter());
        return toJsonHex(id);
    }

    @Override
    public String eth_newPendingTransactionFilter() {
        int id = filterCounter.getAndIncrement();
        installedFilters.put(id, new PendingTransactionFilter());
        return toJsonHex(id);
    }

    @Override
    public boolean eth_uninstallFilter(String id) {
        if (id == null)
            return false;
        return installedFilters.remove(StringHexToBigInteger(id).intValue()) != null;
    }

    @Override
    public Object[] eth_getFilterChanges(String id) {
        Filter filter = installedFilters.get(StringHexToBigInteger(id).intValue());
        if (filter == null)
            return null;
        return filter.poll();
    }

    @Override
    public Object[] eth_getFilterLogs(String id) {
        log.debug("eth_getFilterLogs ...");
        return eth_getFilterChanges(id);
    }

    @Override
    public Object[] eth_getLogs(FilterRequest filterRequest) throws Exception {
        log.debug("eth_getLogs ...");
        String id = eth_newFilter(filterRequest);
        Object[] ret = eth_getFilterChanges(id);
        eth_uninstallFilter(id);
        return ret;
    }

    @Override
    public List<Object> eth_getWork() {
        if (!minerInitialized) {
            minerInitialized = true;
            // this should initialize miningBlock
            blockMiner.setExternalMiner(externalMiner);
        }

        final Block block = miningBlock;

        if (block == null) {
            throw new RuntimeException("Mining block is not ready");
        }
        final EthashAlgo ethash = new EthashAlgo();
        final byte[] blockHash = sha3(block.getHeader().getEncodedWithoutNonce());
        final byte[] seedHash = ethash.getSeedHash(block.getNumber());
        final BigInteger target = valueOf(2).pow(256).divide(block.getDifficultyBI());

        miningBlocks.put(new ByteArrayWrapper(blockHash), block);

        return Arrays.asList(toJsonHex(blockHash), toJsonHex(seedHash), toJsonHex(target));
    }

    @Override
    public boolean eth_submitWork(String nonceHex, String headerHex, String digestHex) throws Exception {
        try {
            final long nonce = TypeConverter.HexToLong(nonceHex);
            final byte[] digest = TypeConverter.StringHexToByteArray(digestHex);
            final byte[] header = TypeConverter.StringHexToByteArray(headerHex);

            final Block block = miningBlocks.remove(new ByteArrayWrapper(header));

            if (block != null && miningTask != null) {
                block.setNonce(longToBytes(nonce));
                block.setMixHash(digest);

                miningTask.set(new MinerIfc.MiningResult(nonce, digest, block));
                miningTask = null;
                return true;
            } else {
                return false;
            }
        } catch (Exception e) {
            log.error("eth_submitWork", e);
            return false;
        }
    }

    @Override
    public boolean eth_submitHashrate(String hashrate, String id) {
        this.hashrate = hashrate;
        return true;
    }

    //
    //    @Override
    //    public String db_putString() {
    //        throw new UnsupportedOperationException("JSON RPC method db_putString not implemented yet");
    //    }
    //
    //    @Override
    //    public String db_getString() {
    //        throw new UnsupportedOperationException("JSON RPC method db_getString not implemented yet");
    //    }
    //
    //    @Override
    //    public String db_putHex() {
    //        throw new UnsupportedOperationException("JSON RPC method db_putHex not implemented yet");
    //    }
    //
    //    @Override
    //    public String db_getHex() {
    //        throw new UnsupportedOperationException("JSON RPC method db_getHex not implemented yet");
    //    }
    //
    //    @Override
    //    public String shh_post() {
    //        throw new UnsupportedOperationException("JSON RPC method shh_post not implemented yet");
    //    }
    //
    //    @Override
    //    public String shh_version() {
    //        throw new UnsupportedOperationException("JSON RPC method shh_version not implemented yet");
    //    }
    //
    //    @Override
    //    public String shh_newIdentity() {
    //        throw new UnsupportedOperationException("JSON RPC method shh_newIdentity not implemented yet");
    //    }
    //
    //    @Override
    //    public String shh_hasIdentity() {
    //        throw new UnsupportedOperationException("JSON RPC method shh_hasIdentity not implemented yet");
    //    }
    //
    //    @Override
    //    public String shh_newGroup() {
    //        throw new UnsupportedOperationException("JSON RPC method shh_newGroup not implemented yet");
    //    }
    //
    //    @Override
    //    public String shh_addToGroup() {
    //        throw new UnsupportedOperationException("JSON RPC method shh_addToGroup not implemented yet");
    //    }
    //
    //    @Override
    //    public String shh_newFilter() {
    //        throw new UnsupportedOperationException("JSON RPC method shh_newFilter not implemented yet");
    //    }
    //
    //    @Override
    //    public String shh_uninstallFilter() {
    //        throw new UnsupportedOperationException("JSON RPC method shh_uninstallFilter not implemented yet");
    //    }
    //
    //    @Override
    //    public String shh_getFilterChanges() {
    //        throw new UnsupportedOperationException("JSON RPC method shh_getFilterChanges not implemented yet");
    //    }
    //
    //    @Override
    //    public String shh_getMessages() {
    //        throw new UnsupportedOperationException("JSON RPC method shh_getMessages not implemented yet");
    //    }
    //
    @Override
    public boolean admin_addPeer(String enodeUrl) {
        final Node node = new Node(enodeUrl);
        eth.connect(node);
        nodeManager.getNodeHandler(node).getNodeStatistics().setPredefined(true);
        return true;
    }

    //
    //    @Override
    //    public String admin_exportChain() {
    //        throw new UnsupportedOperationException("JSON RPC method admin_exportChain not implemented yet");
    //    }
    //
    //    @Override
    //    public String admin_importChain() {
    //        throw new UnsupportedOperationException("JSON RPC method admin_importChain not implemented yet");
    //    }
    //
    //    @Override
    //    public String admin_sleepBlocks() {
    //        throw new UnsupportedOperationException("JSON RPC method admin_sleepBlocks not implemented yet");
    //    }
    //
    //    @Override
    //    public String admin_verbosity() {
    //        throw new UnsupportedOperationException("JSON RPC method admin_verbosity not implemented yet");
    //    }
    //
    //    @Override
    //    public String admin_setSolc() {
    //        throw new UnsupportedOperationException("JSON RPC method admin_setSolc not implemented yet");
    //    }
    //
    //    @Override
    //    public String admin_startRPC() {
    //        throw new UnsupportedOperationException("JSON RPC method admin_startRPC not implemented yet");
    //    }
    //
    //    @Override
    //    public String admin_stopRPC() {
    //        throw new UnsupportedOperationException("JSON RPC method admin_stopRPC not implemented yet");
    //    }
    //
    //    @Override
    //    public String admin_setGlobalRegistrar() {
    //        throw new UnsupportedOperationException("JSON RPC method admin_setGlobalRegistrar not implemented yet");
    //    }
    //
    //    @Override
    //    public String admin_setHashReg() {
    //        throw new UnsupportedOperationException("JSON RPC method admin_setHashReg not implemented yet");
    //    }
    //
    //    @Override
    //    public String admin_setUrlHint() {
    //        throw new UnsupportedOperationException("JSON RPC method admin_setUrlHint not implemented yet");
    //    }
    //
    //    @Override
    //    public String admin_saveInfo() {
    //        throw new UnsupportedOperationException("JSON RPC method admin_saveInfo not implemented yet");
    //    }
    //
    //    @Override
    //    public String admin_register() {
    //        throw new UnsupportedOperationException("JSON RPC method admin_register not implemented yet");
    //    }
    //
    //    @Override
    //    public String admin_registerUrl() {
    //        throw new UnsupportedOperationException("JSON RPC method admin_registerUrl not implemented yet");
    //    }
    //
    //    @Override
    //    public String admin_startNatSpec() {
    //        throw new UnsupportedOperationException("JSON RPC method admin_startNatSpec not implemented yet");
    //    }
    //
    //    @Override
    //    public String admin_stopNatSpec() {
    //        throw new UnsupportedOperationException("JSON RPC method admin_stopNatSpec not implemented yet");
    //    }
    //
    //    @Override
    //    public String admin_getContractInfo() {
    //        throw new UnsupportedOperationException("JSON RPC method admin_getContractInfo not implemented yet");
    //    }
    //
    //    @Override
    //    public String admin_httpGet() {
    //        throw new UnsupportedOperationException("JSON RPC method admin_httpGet not implemented yet");
    //    }
    //
    @Override
    public Map<String, ?> admin_nodeInfo() throws Exception {
        final String nodeId = Hex.toHexString(config.nodeId());
        final String listenAddr = config.bindIp() + ":" + config.listenPort();
        final BlockResult lastBlock = eth_getBlockByNumber("latest", false);

        final HashMap<String, Object> result = new HashMap<>();
        result.put("id", nodeId);
        result.put("name", web3_clientVersion());
        result.put("enode", "enode://" + nodeId + "@" + listenAddr);
        result.put("ip", config.bindIp());
        result.put("ports", ImmutableMap.of("discovery", config.listenPort(), "listener", config.listenPort()));
        result.put("listenAddr", listenAddr);
        result.put("protocols",
                ImmutableMap.of("eth",
                        ImmutableMap.of("network", config.getProperty("peer.networkId", "undefined"), "difficulty",
                                lastBlock.totalDifficulty, "genesis", toJsonHex(config.getGenesis().getHash()),
                                "head", lastBlock.hash)));
        return result;
    }

    @Override
    public List<Map<String, ?>> admin_peers() {
        return channelManager.getActivePeers().stream()
                .map(c -> ImmutableMap.of("id", toJsonHex(c.getNodeId()), "name",
                        toJsonHex(c.getNodeStatistics().getClientId()), "caps",
                        c.getNodeStatistics().capabilities.stream().filter(cap -> c != null)
                                .map(cap -> StringUtils.capitalize(cap.getName()) + "/" + cap.getVersion())
                                .collect(Collectors.toList()),
                        "network", ImmutableMap.of(
                                // TODO put local port which initiated connection
                                "localAddress", config.bindIp() + ":" + c.getInetSocketAddress().getPort(),
                                "remoteAddress", c.getNode().getHost() + ":" + c.getNode().getPort(), "hostname",
                                c.getInetSocketAddress().getHostName(), "protocols",
                                Optional.ofNullable(c.getEthHandler()).map(ethHandler -> ImmutableMap.of("eth",
                                        ImmutableMap.of("version", c.getEthVersion().getCode(), "difficulty",
                                                toJsonHex(ethHandler.getTotalDifficulty()), "head",
                                                Optional.ofNullable(ethHandler.getBestKnownBlock())
                                                        .map(block -> toJsonHex(
                                                                ethHandler.getBestKnownBlock().getHash()))
                                                        .orElse(null))))
                                        .orElse(null))))
                .collect(Collectors.toList());
    }
    //
    //    @Override
    //    public String admin_datadir() {
    //        throw new UnsupportedOperationException("JSON RPC method admin_datadir not implemented yet");
    //    }
    //
    //    @Override
    //    public String net_addPeer() {
    //        throw new UnsupportedOperationException("JSON RPC method net_addPeer not implemented yet");
    //    }

    @Override
    public boolean miner_start() {
        log.info("miner_start requested. MaxMemory: " + Runtime.getRuntime().maxMemory());
        final long requiredMemoryBytes = 2000L << 20; // ~2G
        if (config.isMineFullDataset() && Runtime.getRuntime().maxMemory() < requiredMemoryBytes) {
            final String errorMessage = "Not enough JVM heap (" + (Runtime.getRuntime().maxMemory() >> 20)
                    + "Mb) to generate DAG for mining (DAG requires min 1G). It is recommended to set -Xmx3G JVM option";
            log.error(errorMessage);
            throw new RuntimeException(errorMessage);
        }
        blockMiner.startMining();
        return true;
    }

    @Override
    public boolean miner_stop() {
        blockMiner.stopMining();
        return true;
    }

    @Override
    public boolean miner_setEtherbase(String coinBase) throws Exception {
        blockchain.setMinerCoinbase(TypeConverter.StringHexToByteArray(coinBase));
        return true;
    }

    @Override
    public boolean miner_setExtra(String data) throws Exception {
        blockchain.setMinerExtraData(TypeConverter.StringHexToByteArray(data));
        return true;
    }

    @Override
    public boolean miner_setGasPrice(String newMinGasPrice) {
        blockMiner.setMinGasPrice(TypeConverter.StringHexToBigInteger(newMinGasPrice));
        return true;
    }

    //    @Override
    //    public boolean miner_startAutoDAG() {
    //        return false;
    //    }
    //
    //    @Override
    //    public boolean miner_stopAutoDAG() {
    //        return false;
    //    }
    //
    //    @Override
    //    public boolean miner_makeDAG() {
    //        return false;
    //    }
    //
    //    @Override
    //    public String miner_hashrate() {
    //        return "0x01";
    //    }

    //    @Override
    //    public String debug_printBlock() {
    //        throw new UnsupportedOperationException("JSON RPC method debug_printBlock not implemented yet");
    //    }
    //
    //    @Override
    //    public String debug_getBlockRlp() {
    //        throw new UnsupportedOperationException("JSON RPC method debug_getBlockRlp not implemented yet");
    //    }
    //
    //    @Override
    //    public String debug_setHead() {
    //        throw new UnsupportedOperationException("JSON RPC method debug_setHead not implemented yet");
    //    }
    //
    //    @Override
    //    public String debug_processBlock() {
    //        throw new UnsupportedOperationException("JSON RPC method debug_processBlock not implemented yet");
    //    }

    //    @Override
    //    public String debug_seedHash() {
    //        throw new UnsupportedOperationException("JSON RPC method debug_seedHash not implemented yet");
    //    }
    //
    //    @Override
    //    public String debug_dumpBlock() {
    //        throw new UnsupportedOperationException("JSON RPC method debug_dumpBlock not implemented yet");
    //    }
    //
    //    @Override
    //    public String debug_metrics() {
    //        throw new UnsupportedOperationException("JSON RPC method debug_metrics not implemented yet");
    //    }

    @Override
    public String personal_newAccount(@NonNull String password) {
        log.debug("personal_newAccount(...)");
        // generate new private key
        ECKey key = new ECKey();
        Account account = importAccount(key, password);
        return toJsonHex(account.getAddress());
    }

    public String personal_importRawKey(String keydata, String passphrase) {
        log.debug("personal_importRawKey(...)");
        Objects.requireNonNull(keydata, "keydata is required");
        Objects.requireNonNull(passphrase, "passphrase is required");

        Account account = importAccount(ECKey.fromPrivate(Hex.decode(keydata)), passphrase);
        return toJsonHex(account.getAddress());
    }

    @Override
    public boolean personal_unlockAccount(String address, String password, String duration) {
        log.info("personal_unlockAccount(" + address + ", ...)");

        Objects.requireNonNull(address, "address is required");
        Objects.requireNonNull(password, "password is required");

        final ECKey key = keystore.loadStoredKey(JSonHexToHex(address).toLowerCase(), password);
        if (key != null) {
            log.info("Found key address is " + Hex.toHexString(key.getAddress()));
            final Account account = new Account();
            account.init(key);
            log.info("Found account address is " + Hex.toHexString(account.getAddress()));
            unlockedAccounts.put(Hex.toHexString(account.getAddress()).toLowerCase(), account);
            return true;
        } else {
            // we can return false or send description message with exception
            // prefer exception for now
            throw new RuntimeException("No key was found in keystore for account: " + address);
        }
    }

    @Override
    public boolean personal_lockAccount(String address) {
        Objects.requireNonNull(address, "address is required");

        unlockedAccounts.remove(address.toLowerCase());
        return true;
    }

    @Override
    public String[] personal_listAccounts() {
        return keystore.listStoredKeys();
    }

    @Override
    public String personal_signAndSendTransaction(CallArguments tx, String password) {
        final ECKey key = keystore.loadStoredKey(JSonHexToHex(tx.from).toLowerCase(), password);
        if (key != null) {
            final Account account = new Account();
            account.init(key);
            return sendTransaction(tx, account);
        } else {
            // we can return false or send description message with exception
            // prefer exception for now
            throw new RuntimeException("No key was found in keystore for account: " + JSonHexToHex(tx.from));
        }
    }

    /**
     * List method names for client side terminal competition.
     * @return array in format: `["methodName arg1 arg2", "methodName2"]`
     */
    @Override
    public String[] ethj_listAvailableMethods() {
        final Set<String> ignore = Arrays.asList(Object.class.getMethods()).stream().map(method -> method.getName())
                .collect(Collectors.toSet());

        return Arrays.asList(EthJsonRpcImpl.class.getMethods()).stream()
                .filter(method -> Modifier.isPublic(method.getModifiers()))
                .filter(method -> !ignore.contains(method.getName())).map(method -> {
                    List<String> params = Arrays.asList(method.getParameters()).stream()
                            .map(parameter -> parameter.isNamePresent() ? parameter.getName()
                                    : parameter.getType().getSimpleName())
                            .collect(Collectors.toList());
                    params.add(0, method.getName());
                    return params.stream().collect(Collectors.joining(" "));
                }).sorted(String::compareTo).toArray(size -> new String[size]);
    }
}