content: package org.aion.api.server.rpc;
  
  import static java.util.stream.Collectors.toList;
  import static org.aion.api.server.types.FltrLg.BLOCKS_QUERY_MAX;
  import static org.aion.util.bytes.ByteUtil.EMPTY_BYTE_ARRAY;
  import static org.aion.util.conversions.Hex.toHexString;
  import static org.aion.util.types.HexConvert.hexStringToBytes;
  
  import com.github.benmanes.caffeine.cache.CacheLoader;
  import com.github.benmanes.caffeine.cache.Caffeine;
  import com.github.benmanes.caffeine.cache.LoadingCache;
  import java.math.BigDecimal;
  import java.math.BigInteger;
  import java.math.RoundingMode;
  import java.time.Instant;
  import java.time.ZoneOffset;
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.Base64;
  import java.util.Collections;
  import java.util.HashMap;
  import java.util.LinkedList;
  import java.util.List;
  import java.util.ListIterator;
  import java.util.Map;
  import java.util.Map.Entry;
  import java.util.Optional;
  import java.util.concurrent.TimeUnit;
  import java.util.function.Function;
  import org.aion.api.server.ApiAion;
  import org.aion.api.server.ApiTxResponse;
  import org.aion.api.server.account.AccountManager;
  import org.aion.api.server.types.ArgFltr;
  import org.aion.api.server.types.ArgTxCall;
  import org.aion.api.server.types.Blk;
  import org.aion.api.server.types.CompiledContr;
  import org.aion.api.server.types.Evt;
  import org.aion.api.server.types.Fltr;
  import org.aion.api.server.types.FltrBlk;
  import org.aion.api.server.types.FltrLg;
  import org.aion.api.server.types.FltrTx;
  import org.aion.api.server.types.NumericalValue;
  import org.aion.api.server.types.SyncInfo;
  import org.aion.api.server.types.Tx;
  import org.aion.api.server.types.TxRecpt;
  import org.aion.base.AccountState;
  import org.aion.base.AionTransaction;
  import org.aion.base.AionTxReceipt;
  import org.aion.base.TransactionTypes;
  import org.aion.crypto.ECKey;
  import org.aion.crypto.HashUtil;
  import org.aion.evtmgr.IEventMgr;
  import org.aion.evtmgr.IHandler;
  import org.aion.evtmgr.impl.callback.EventCallback;
  import org.aion.zero.impl.types.Block;
  import org.aion.zero.impl.types.BlockHeader.Seal;
  import org.aion.types.AionAddress;
  import org.aion.types.Log;
  import org.aion.util.bytes.ByteUtil;
  import org.aion.util.string.StringUtils;
  import org.aion.util.types.AddressUtils;
  import org.aion.util.types.ByteArrayWrapper;
  import org.aion.util.types.DataWord;
  import org.aion.zero.impl.Version;
  import org.aion.zero.impl.blockchain.AionBlockchainImpl;
  import org.aion.zero.impl.blockchain.AionImpl;
  import org.aion.zero.impl.blockchain.IAionChain;
  import org.aion.zero.impl.config.CfgAion;
  import org.aion.zero.impl.config.CfgApi;
  import org.aion.zero.impl.config.CfgApiNrg;
  import org.aion.zero.impl.config.CfgApiRpc;
  import org.aion.zero.impl.config.CfgApiZmq;
  import org.aion.zero.impl.config.CfgConsensusUnity;
  import org.aion.zero.impl.config.CfgEnergyStrategy;
  import org.aion.zero.impl.config.CfgNet;
  import org.aion.zero.impl.config.CfgNetP2p;
  import org.aion.zero.impl.config.CfgSsl;
  import org.aion.zero.impl.config.CfgSync;
  import org.aion.zero.impl.config.CfgTx;
  import org.aion.zero.impl.core.ImportResult;
  import org.aion.zero.impl.keystore.Keystore;
  import org.aion.zero.impl.sync.NodeWrapper;
  import org.aion.zero.impl.types.AionBlockSummary;
  import org.aion.zero.impl.types.AionTxInfo;
  import org.aion.zero.impl.types.BlockContext;
  import org.aion.zero.impl.types.MiningBlock;
  import org.aion.zero.impl.types.MiningBlockHeader;
  import org.aion.zero.impl.types.StakingBlock;
  import org.aion.zero.impl.types.StakingBlockHeader;
  import org.apache.commons.collections4.map.LRUMap;
  import org.json.JSONArray;
  import org.json.JSONObject;
  
  /**
   * @author chris lin, ali sharif TODO: make implementation pass all spec tests:
   *     https://github.com/ethereum/rpc-tests
   */
  @SuppressWarnings({"Duplicates", "WeakerAccess"})
  public class ApiWeb3Aion extends ApiAion {
  
      private static final int SYNC_TOLERANCE = 1;
  
      private final int OPS_RECENT_ENTITY_COUNT = 32;
      private final int OPS_RECENT_ENTITY_CACHE_TIME_SECONDS = 4;
  
      private final int STRATUM_RECENT_BLK_COUNT = 128;
      private final int STRATUM_BLKTIME_INCLUDED_COUNT = 32;
      private final int STRATUM_CACHE_TIME_SECONDS = 15;
      private IEventMgr evtMgr;
      // doesn't need to be protected for concurrent access, since only one write in the constructor.
      private boolean isFilterEnabled;
  
      private boolean isSeedMode;
  
      private final long BEST_PENDING_BLOCK = -1L;
  
      private final LoadingCache<Integer, ChainHeadView> CachedRecentEntities;
      private final LoadingCache<String, MinerStatsView> MinerStats;
  
      protected void onBlock(AionBlockSummary cbs) {
          if (isFilterEnabled) {
              installedFilters
                      .keySet()
                      .forEach(
                              (k) -> {
                                  Fltr f = installedFilters.get(k);
                                  if (f.isExpired()) {
                                      LOG.debug("<Filter: expired, key={}>", k);
                                      installedFilters.remove(k);
                                  } else if (f.onBlock(cbs)) {
                                      LOG.debug(
                                              "<Filter: append, onBlock type={} blk#={}>",
                                              f.getType().name(),
                                              cbs.getBlock().getNumber());
                                  }
                              });
          }
      }
  
      protected void pendingTxReceived(AionTransaction _tx) {
          if (isFilterEnabled) {
              // not absolutely neccessary to do eviction on installedFilters here, since we're doing
              // it already
              // in the onBlock event. eviction done here "just in case ..."
              installedFilters
                      .keySet()
                      .forEach(
                              (k) -> {
                                  Fltr f = installedFilters.get(k);
                                  if (f.isExpired()) {
                                      LOG.debug("<filter expired, key={}>", k);
                                      installedFilters.remove(k);
                                  } else if (f.onTransaction(_tx)) {
                                      LOG.info(
                                              "<filter append, onPendingTransaction fltrSize={} type={} txHash={}>",
                                              f.getSize(),
                                              f.getType().name(),
                                              StringUtils.toJsonHex(_tx.getTransactionHash()));
                                  }
                              });
          }
      }
  
      @Override
      protected void pendingTxUpdate(AionTxReceipt _txRcpt, int _state) {
          // commenting this out because of lack support for old web3 client that we are using
          // TODO: re-enable this when we upgrade our web3 client
          /*
          if (isFilterEnabled) {
              ByteArrayWrapper txHashW = new ByteArrayWrapper(((AionTxReceipt) _txRcpt).getTransaction().getHash());
              if (_state.isPending() || _state == EventTx.STATE.DROPPED0) {
                  pendingReceipts.put(txHashW, (AionTxReceipt) _txRcpt);
              } else {
                  pendingReceipts.remove(txHashW);
              }
          }
          */
      }
  
      private final LoadingCache<ByteArrayWrapper, Block> blockCache;
      private static final int BLOCK_CACHE_SIZE = 1000;
  
      public ApiWeb3Aion(final IAionChain _ac, final AccountManager am) {
          super(_ac, am);
          pendingReceipts = Collections.synchronizedMap(new LRUMap<>(FLTRS_MAX, 100));
          isFilterEnabled = CfgAion.inst().getApi().getRpc().isFiltersEnabled();
          isSeedMode = CfgAion.inst().getTx().isSeedMode();
  
          initNrgOracle(_ac);
  
          if (isFilterEnabled) {
              evtMgr = this.ac.getAionHub().getEventMgr();
  
              startES("EpWeb3");
  
              // Fill data on block and transaction events into the filters and pending receipts
              IHandler blkHr = evtMgr.getHandler(IHandler.TYPE.BLOCK0.getValue());
              if (blkHr != null) {
                  blkHr.eventCallback(new EventCallback(ees, LOG));
              }
  
              IHandler txHr = evtMgr.getHandler(IHandler.TYPE.TX0.getValue());
              if (txHr != null) {
                  txHr.eventCallback(new EventCallback(ees, LOG));
              }
          }
  
          // ops-related endpoints
          CachedRecentEntities =
                  Caffeine.newBuilder()
                          .maximumSize(1)
                          .refreshAfterWrite(OPS_RECENT_ENTITY_CACHE_TIME_SECONDS, TimeUnit.SECONDS)
                          .build(
                                  new CacheLoader<>() {
                                      public ChainHeadView load(Integer key) { // no checked exception
                                          return new ChainHeadView(OPS_RECENT_ENTITY_COUNT).update();
                                      }
  
                                      // reload() executed asynchronously using
                                      // ForkJoinPool.commonPool()
                                      public ChainHeadView reload(
                                              final Integer key, ChainHeadView prev) {
                                          return new ChainHeadView(prev).update();
                                      }
                                  });
  
          //noinspection NullableProblems
          blockCache =
                  Caffeine.newBuilder()
                          .maximumSize(BLOCK_CACHE_SIZE)
                          .build(
                                  new CacheLoader<>() {
                                      public Block load(ByteArrayWrapper blockHash) {
                                          LOG.debug(
                                                  "<rpc-server blockCache miss for "
                                                          + blockHash.toString()
                                                          + " >");
                                          return getBlockByHash(blockHash.toBytes());
                                      }
                                  });
  
          MinerStats =
                  Caffeine.newBuilder()
                          .maximumSize(1)
                          .refreshAfterWrite(STRATUM_CACHE_TIME_SECONDS, TimeUnit.SECONDS)
                          .build(
                                  new CacheLoader<>() {
                                      public MinerStatsView load(String key) { // no checked exception
                                          AionAddress miner = AddressUtils.wrapAddress(key);
                                          return new MinerStatsView(
                                                          STRATUM_RECENT_BLK_COUNT,
                                                          miner.toByteArray())
                                                  .update();
                                      }
  
                                      // reload() executed asynchronously using
                                      // ForkJoinPool.commonPool()
                                      public MinerStatsView reload(
                                              final String key, MinerStatsView prev) {
                                          return new MinerStatsView(prev).update();
                                      }
                                  });
      }
  
      private void destroyCaches() {
          CachedRecentEntities.invalidateAll();
          MinerStats.invalidateAll();
          blockCache.invalidateAll();
      }
  
      // --------------------------------------------------------------------
      // Mining Pool
      // --------------------------------------------------------------------
  
      /* Return a reference to the AIONBlock without converting values to hex
       * Requied for the mining pool implementation
       */
      private Block getBlockRaw(int bn) {
          // long bn = this.parseBnOrId(_bnOrId);
          Block nb = this.ac.getBlockchain().getBlockByNumber(bn);
  
          if (nb == null) {
              if (LOG.isDebugEnabled()) {
                  LOG.debug("<get-block-raw bn={} err=not-found>", bn);
              }
              return null;
          } else {
              return nb;
          }
      }
  
      // --------------------------------------------------------------------
      // Ethereum-Compliant JSON RPC Specification Implementation
      // --------------------------------------------------------------------
  
      public RpcMsg web3_clientVersion() {
          return new RpcMsg(this.clientVersion);
      }
  
      public RpcMsg web3_sha3(Object _params) {
          String _data;
          if (_params instanceof JSONArray) {
              _data = ((JSONArray) _params).get(0) + "";
          } else if (_params instanceof JSONObject) {
              _data = ((JSONObject) _params).get("data") + "";
          } else {
              return new RpcMsg(null, RpcError.INVALID_PARAMS, "Invalid parameters");
          }
  
          return new RpcMsg(
                  StringUtils.toJsonHex(HashUtil.keccak256(ByteUtil.hexStringToBytes(_data))));
      }
  
      public RpcMsg net_version() {
          return new RpcMsg(chainId());
      }
  
      public RpcMsg net_peerCount() {
          return new RpcMsg(peerCount());
      }
  
      public RpcMsg net_listening() {
          // currently, p2p manager is always listening for peers and is active
          return new RpcMsg(true);
      }
  
      public RpcMsg eth_protocolVersion() {
          return new RpcMsg(p2pProtocolVersion());
      }
  
      /**
       * Returns an {@link RpcMsg} containing 'false' if the node is done syncing to the network
       * (since a node is never really 'done' syncing, we consider a node to be done if it is within
       * {@value SYNC_TOLERANCE} blocks of the network best block number).
       *
       * <p>Otherwise, if the sync is still syncing, a {@link RpcMsg} is returned with some basic
       * statistics relating to the sync: the block number the node was at when syncing began; the
       * current block number of the node; the current block number of the network.
       *
       * <p>If either the local block number or network block number cannot be determined, an {@link
       * RpcMsg} is returned with an {@link RpcError#INTERNAL_ERROR} code and its 'result' will be a
       * more descriptive string indicating what went wrong.
       *
       * @return the syncing statistics.
       */
      public RpcMsg eth_syncing() {
          Optional<Long> localBestBlockNumber = this.ac.getLocalBestBlockNumber();
          Optional<Long> networkBestBlockNumber = this.ac.getNetworkBestBlockNumber();
  
          // Check that we actually have real values in our hands.
          if (!localBestBlockNumber.isPresent()) {
              return new RpcMsg(
                      "Unable to determine the local node's best block number!",
                      RpcError.INTERNAL_ERROR);
          }
          if (!networkBestBlockNumber.isPresent()) {
              return new RpcMsg(
                      "Unable to determine the network's best block number!",
                      RpcError.INTERNAL_ERROR);
          }
  
          SyncInfo syncInfo = getSyncInfo(localBestBlockNumber.get(), networkBestBlockNumber.get());
  
          return (syncInfo.done) ? new RpcMsg(false) : new RpcMsg(syncInfoToJson(syncInfo));
      }
  
      private JSONObject syncInfoToJson(SyncInfo syncInfo) {
          JSONObject syncInfoAsJson = new JSONObject();
          syncInfoAsJson.put(
                  "startingBlock", new NumericalValue(syncInfo.chainStartingBlkNumber).toHexString());
          syncInfoAsJson.put(
                  "currentBlock", new NumericalValue(syncInfo.chainBestBlkNumber).toHexString());
          syncInfoAsJson.put(
                  "highestBlock", new NumericalValue(syncInfo.networkBestBlkNumber).toHexString());
          return syncInfoAsJson;
      }
  
      public RpcMsg eth_coinbase() {
          return new RpcMsg(getCoinbase());
      }
  
      public RpcMsg eth_mining() {
          return new RpcMsg(isMining());
      }
  
      public RpcMsg eth_hashrate() {
          return new RpcMsg(getHashrate());
      }
  
      public RpcMsg eth_submitHashrate(Object _params) {
          String _hashrate;
          String _clientId;
          if (_params instanceof JSONArray) {
              _hashrate = ((JSONArray) _params).get(0) + "";
              _clientId = ((JSONArray) _params).get(1) + "";
          } else if (_params instanceof JSONObject) {
              _hashrate = ((JSONObject) _params).get("hashrate") + "";
              _clientId = ((JSONObject) _params).get("clientId") + "";
          } else {
              return new RpcMsg(null, RpcError.INVALID_PARAMS, "Invalid parameters");
          }
  
          return new RpcMsg(setReportedHashrate(_hashrate, _clientId));
      }
  
      public RpcMsg eth_gasPrice() {
          return new RpcMsg(StringUtils.toJsonHex(getRecommendedNrgPrice()));
      }
  
      public RpcMsg eth_accounts() {
          return new RpcMsg(new JSONArray(getAccounts()));
      }
  
      public RpcMsg eth_blockNumber() {
          return new RpcMsg(getBestBlock().getNumber());
      }
  
      public RpcMsg eth_getBalance(Object _params) {
          String _address;
          Object _bnOrId;
  
          if (_params instanceof JSONArray) {
              _address = ((JSONArray) _params).get(0) + "";
              _bnOrId = ((JSONArray) _params).opt(1);
          } else if (_params instanceof JSONObject) {
              _address = ((JSONObject) _params).get("address") + "";
              _bnOrId = ((JSONObject) _params).opt("block") + "";
          } else {
              return new RpcMsg(null, RpcError.INVALID_PARAMS, "Invalid parameters");
          }
  
          String bnOrId = formalizeBlockNumberOrId(_bnOrId);
          Long bn = parseBnOrId(bnOrId);
          if (bn == null) {
              return new RpcMsg(null, RpcError.INVALID_PARAMS, "Invalid block number");
          } else {
              AionAddress address = AddressUtils.wrapAddress(_address);
              Optional<AccountState> accountState =
                      (bn == BEST_PENDING_BLOCK
                              ? pendingState.getAccountState(address)
                              : ac.getAccountState(address, bn));
  
              return accountState.map(state -> new RpcMsg(StringUtils.toJsonHex(state.getBalance())))
                  .orElseGet(() -> new RpcMsg(
                      null,
                      RpcError.EXECUTION_ERROR,
                      "Cannot find the account state:"
                          + address
                          + "in the pendingState!"
                          + "State may have been pruned; please check your db pruning settings in the configuration file."));
          }
      }
  
      private String formalizeBlockNumberOrId(Object bnOrId) {
          String result = "latest";
          if (!JSONObject.NULL.equals(bnOrId)) {
              result = bnOrId + "";
          }
  
          return result;
      }
  
      public RpcMsg eth_getStorageAt(Object _params) {
          String _address;
          String _index;
          Object _bnOrId;
          if (_params instanceof JSONArray) {
              _address = ((JSONArray) _params).get(0) + "";
              _index = ((JSONArray) _params).get(1) + "";
              _bnOrId = ((JSONArray) _params).opt(2);
          } else if (_params instanceof JSONObject) {
              _address = ((JSONObject) _params).get("address") + "";
              _index = ((JSONObject) _params).get("index") + "";
              _bnOrId = ((JSONObject) _params).opt("block");
          } else {
              return new RpcMsg(null, RpcError.INVALID_PARAMS, "Invalid parameters");
          }
  
          String bnOrId = formalizeBlockNumberOrId(_bnOrId);
          Long bn = parseBnOrId(bnOrId);
          if (bn == null) {
              return new RpcMsg(null, RpcError.INVALID_PARAMS, "Invalid block number");
          } else {
              DataWord key;
              try {
                  key = new DataWord(ByteUtil.hexStringToBytes(_index));
              } catch (Exception e) {
                  // invalid key
                  LOG.debug("eth_getStorageAt: invalid storageIndex. Must be <= 16 bytes.");
                  return new RpcMsg(
                      null, RpcError.INVALID_PARAMS, "Invalid storageIndex. Must be <= 16 bytes.");
              }
  
              AionAddress address = AddressUtils.wrapAddress(_address);
              Optional<ByteArrayWrapper> storageValue =
                      (bn == BEST_PENDING_BLOCK
                              ? pendingState.getStorageValue(address, key.toWrapper())
                              : ac.getStorageValue(address, key.toWrapper()));
  
              return storageValue.map(
                  byteArrayWrapper -> new RpcMsg(StringUtils.toJsonHex(byteArrayWrapper.toBytes())))
                  .orElseGet(
                      () -> new RpcMsg(null, RpcError.EXECUTION_ERROR, "Storage value not found"));
          }
      }
  
      public RpcMsg eth_getTransactionCount(Object _params) {
          String _address;
          Object _bnOrId;
          if (_params instanceof JSONArray) {
              _address = ((JSONArray) _params).get(0) + "";
              _bnOrId = ((JSONArray) _params).opt(1);
          } else if (_params instanceof JSONObject) {
              _address = ((JSONObject) _params).get("address") + "";
              _bnOrId = ((JSONObject) _params).opt("block");
          } else {
              return new RpcMsg(null, RpcError.INVALID_PARAMS, "Invalid parameters");
          }
  
          String bnOrId = formalizeBlockNumberOrId(_bnOrId);
          Long bn = parseBnOrId(bnOrId);
          if (bn == null) {
              return new RpcMsg(null, RpcError.INVALID_PARAMS, "Invalid block number");
          } else {
              AionAddress address = AddressUtils.wrapAddress(_address);
              Optional<AccountState> accountState = (bn == BEST_PENDING_BLOCK ? pendingState.getAccountState(address) : ac.getAccountState(address));
  
              return accountState.map(state -> new RpcMsg(StringUtils.toJsonHex(state.getNonce())))
                  .orElseGet(() -> new RpcMsg(null, RpcError.EXECUTION_ERROR,
                      "Cannot find the account:" + address + " state.\n"
                          + "State may have been pruned; please check your db pruning settings in the configuration file."));
          }
      }
  
      public RpcMsg eth_getBlockTransactionCountByHash(Object _params) {
          String _hash;
          if (_params instanceof JSONArray) {
              _hash = ((JSONArray) _params).get(0) + "";
          } else if (_params instanceof JSONObject) {
              _hash = ((JSONObject) _params).get("hash") + "";
          } else {
              return new RpcMsg(null, RpcError.INVALID_PARAMS, "Invalid parameters");
          }
  
          byte[] hash = ByteUtil.hexStringToBytes(_hash);
          Block b = this.ac.getBlockchain().getBlockByHash(hash);
          if (b == null) {
              return new RpcMsg(null, RpcError.EXECUTION_ERROR, "Block not found.");
          }
  
          long n = b.getTransactionsList().size();
          return new RpcMsg(StringUtils.toJsonHex(n));
      }
  
      public RpcMsg eth_getBlockTransactionCountByNumber(Object _params) {
          String _bnOrId;
          if (_params instanceof JSONArray) {
              _bnOrId = ((JSONArray) _params).get(0) + "";
          } else if (_params instanceof JSONObject) {
              _bnOrId = ((JSONObject) _params).get("block") + "";
          } else {
              return new RpcMsg(null, RpcError.INVALID_PARAMS, "Invalid parameters");
          }
  
          String bnOrId = formalizeBlockNumberOrId(_bnOrId);
          Long bn = parseBnOrId(bnOrId);
          if (bn == null) {
              return new RpcMsg(null, RpcError.INVALID_PARAMS, "Invalid block number");
          } else {
              // pending transactions
              if (bn == BEST_PENDING_BLOCK) {
                  long pendingTxCount = this.ac.getAionHub().getPendingState().getPendingTxSize();
                  return new RpcMsg(StringUtils.toJsonHex(pendingTxCount));
              }
  
              Block b = this.ac.getBlockchain().getBlockByNumber(bn);
              if (b == null) {
                  return new RpcMsg(null, RpcError.EXECUTION_ERROR, "Block not found.");
              }
  
              List<AionTransaction> list = b.getTransactionsList();
  
              long n = list.size();
              return new RpcMsg(StringUtils.toJsonHex(n));
          }
      }
  
      public RpcMsg eth_getCode(Object _params) {
          String _address;
          Object _bnOrId;
          if (_params instanceof JSONArray) {
              _address = ((JSONArray) _params).get(0) + "";
              _bnOrId = ((JSONArray) _params).opt(1);
          } else if (_params instanceof JSONObject) {
              _address = ((JSONObject) _params).get("address") + "";
              _bnOrId = ((JSONObject) _params).opt("block");
          } else {
              return new RpcMsg(null, RpcError.INVALID_PARAMS, "Invalid parameters");
          }
  
          String bnOrId = formalizeBlockNumberOrId(_bnOrId);
          Long bn = parseBnOrId(bnOrId);
          if (bn == null) {
              return new RpcMsg(null, RpcError.INVALID_PARAMS, "Invalid block number");
          } else {
              AionAddress address = AddressUtils.wrapAddress(_address);
              Optional<ByteArrayWrapper> code = (bn == BEST_PENDING_BLOCK ? pendingState.getCode(address) : ac.getCode(address));
  
              return code.map(byteArrayWrapper -> new RpcMsg(
                  StringUtils.toJsonHex(byteArrayWrapper.toBytes()))).orElseGet(
                  () -> new RpcMsg(null, RpcError.EXECUTION_ERROR,
                      "Cannot find the code from the account:" + address + " state.\n"
                          + "State may have been pruned; please check your db pruning settings in the configuration file."));
          }
      }
  
      public RpcMsg eth_sign(Object _params) {
          String _address;
          String _message;
          if (_params instanceof JSONArray) {
              _address = ((JSONArray) _params).get(0) + "";
              _message = ((JSONArray) _params).get(1) + "";
          } else if (_params instanceof JSONObject) {
              _address = ((JSONObject) _params).get("address") + "";
              _message = ((JSONObject) _params).get("message") + "";
          } else {
              return new RpcMsg(null, RpcError.INVALID_PARAMS, "Invalid parameters");
          }
  
          AionAddress address = AddressUtils.wrapAddress(_address);
          ECKey key = getAccountKey(address.toString());
          if (key == null) {
              return new RpcMsg(null, RpcError.NOT_ALLOWED, "Account not unlocked.");
          }
  
          // Message starts with Unicode Character 'END OF MEDIUM' (U+0019)
          String message = "\u0019Aion Signed Message:\n" + _message.length() + _message;
          byte[] messageHash = HashUtil.keccak256(message.getBytes());
  
          return new RpcMsg(StringUtils.toJsonHex(key.sign(messageHash).getSignature()));
      }
  
      /**
       * Sign a transaction. This account needs to be unlocked.
       *
       * @param _params
       * @return
       */
      public RpcMsg eth_signTransaction(Object _params) {
          JSONObject _tx;
          // Address to sign with
          String _address = null;
          if (_params instanceof JSONArray) {
              _tx = ((JSONArray) _params).getJSONObject(0);
  
              if (((JSONArray) _params).length() > 1) _address = ((JSONArray) _params).get(1) + "";
          } else if (_params instanceof JSONObject) {
              _tx = ((JSONObject) _params).getJSONObject("transaction");
              _address = ((JSONObject) _params).get("address") + "";
          } else {
              return new RpcMsg(null, RpcError.INVALID_PARAMS, "Invalid parameters");
          }
  
          ArgTxCall txParams = ArgTxCall.fromJSON(_tx, getRecommendedNrgPrice());
          if (txParams == null)
              return new RpcMsg(
                      null, RpcError.INVALID_PARAMS, "Please check your transaction object.");
  
          AionTransaction tx = signTransaction(txParams, _address);
          if (tx != null) {
              JSONObject obj = new JSONObject();
              obj.put("raw", StringUtils.toJsonHex(tx.getEncoded()));
  
              JSONObject txObj = new JSONObject();
              txObj.put("nonce", StringUtils.toJsonHex(tx.getNonce()));
              txObj.put("gasPrice", StringUtils.toJsonHex(tx.getEnergyPrice()));
              txObj.put("nrgPrice", StringUtils.toJsonHex(tx.getEnergyPrice()));
              txObj.put("gas", StringUtils.toJsonHex(tx.getEnergyLimit()));
              txObj.put("nrg", StringUtils.toJsonHex(tx.getEnergyLimit()));
              txObj.put(
                      "to",
                      StringUtils.toJsonHex(
                              tx.getDestinationAddress() == null
                                      ? EMPTY_BYTE_ARRAY
                                      : tx.getDestinationAddress().toByteArray()));
              txObj.put("value", StringUtils.toJsonHex(tx.getValue()));
              txObj.put("input", StringUtils.toJsonHex(tx.getData()));
              txObj.put("hash", StringUtils.toJsonHex(tx.getTransactionHash()));
  
              obj.put("tx", txObj);
              return new RpcMsg(obj);
          } else {
              if (LOG.isDebugEnabled()) LOG.debug("Transaction signing failed");
              return new RpcMsg(null, RpcError.INTERNAL_ERROR, "Error in signing the transaction.");
          }
      }
  
      private RpcMsg processTxResponse(ApiTxResponse rsp) {
          switch (rsp.getType()) {
              case SUCCESS:
              case ALREADY_CACHED:
              case CACHED_POOLMAX:
              case CACHED_NONCE:
              case ALREADY_SEALED:
              case REPAID:
              case CACHED_TIMEOUT:
              case TXPOOL_TIMEOUT:
              case CACHED_ACCOUNTMAX:
                  return new RpcMsg(StringUtils.toJsonHex(rsp.getTxHash()));
              case INVALID_TX:
              case INVALID_TX_NRG_PRICE:
              case INVALID_FROM:
              case INVALID_TX_NRG_LIMIT:
              case INVALID_TX_NONCE:
              case INVALID_TX_TIMESTAMP:
              case INVALID_TX_VALUE:
              case INVALID_TX_DATA:
              case INVALID_TX_HASH:
              case INVALID_TX_SIGNATURE:
              case INVALID_TX_TYPE:
              case INVALID_TX_BEACONHASH:
              case REPAYTX_LOWPRICE:
                  return new RpcMsg(null, RpcError.INVALID_PARAMS, rsp.getMessage());
              case INVALID_ACCOUNT:
                  return new RpcMsg(null, RpcError.NOT_ALLOWED, rsp.getMessage());
  
              case REPAYTX_BUFFER_FULL:
              case DROPPED:
              case EXCEPTION:
              default:
                  return new RpcMsg(null, RpcError.EXECUTION_ERROR, rsp.getMessage());
          }
      }
  
      public RpcMsg eth_sendTransaction(Object _params) {
          JSONObject _tx;
          if (_params instanceof JSONArray) {
              _tx = ((JSONArray) _params).getJSONObject(0);
          } else if (_params instanceof JSONObject) {
              _tx = ((JSONObject) _params).getJSONObject("transaction");
          } else {
              return new RpcMsg(null, RpcError.INVALID_PARAMS, "Invalid parameters");
          }
  
          ArgTxCall txParams = ArgTxCall.fromJSON(_tx, getRecommendedNrgPrice());
  
          ApiTxResponse response = sendTransaction(txParams);
  
          return processTxResponse(response);
      }
  
      public RpcMsg eth_sendRawTransaction(Object _params) {
          String _rawTx;
          if (_params instanceof JSONArray) {
              _rawTx = ((JSONArray) _params).get(0) + "";
          } else if (_params instanceof JSONObject) {
              _rawTx = ((JSONObject) _params).get("transaction") + "";
          } else {
              return new RpcMsg(null, RpcError.INVALID_PARAMS, "Invalid parameters");
          }
  
          if (_rawTx.equals("null")) {
              return new RpcMsg(null, RpcError.INVALID_PARAMS, "Null raw transaction provided.");
          }
  
          byte[] rawTransaction = ByteUtil.hexStringToBytes(_rawTx);
  
          ApiTxResponse response = sendTransaction(rawTransaction);
  
          return processTxResponse(response);
      }
  
      public RpcMsg eth_call(Object _params) {
          JSONObject _tx;
          Object _bnOrId;
          if (_params instanceof JSONArray) {
              _tx = ((JSONArray) _params).getJSONObject(0);
              _bnOrId = ((JSONArray) _params).opt(1);
          } else if (_params instanceof JSONObject) {
              _tx = ((JSONObject) _params).getJSONObject("transaction");
              _bnOrId = ((JSONObject) _params).opt("block");
          } else {
              return new RpcMsg(null, RpcError.INVALID_PARAMS, "Invalid parameters");
          }
  
          ArgTxCall txParams = ArgTxCall.fromJSONforCall(_tx, getRecommendedNrgPrice());
  
          if (txParams == null) {
              return new RpcMsg(
                      null, RpcError.INVALID_PARAMS, "Invalid transaction parameter provided");
          }
  
          String bnOrId = "latest";
          if (!JSONObject.NULL.equals(_bnOrId)) {
              bnOrId = _bnOrId + "";
          }
  
          Long bn = parseBnOrId(bnOrId);
          if (bn == null) {
              return new RpcMsg(null, RpcError.INVALID_PARAMS, "Invalid block number.");
          }
  
          Block b = getBlockByBN(bn);
  
          AionAddress sender = txParams.getFrom();
          if (sender == null) {
              sender = AddressUtils.ZERO_ADDRESS;
          }
  
          AionTransaction tx =
                  AionTransaction.createWithoutKey(
                          txParams.getNonce().toByteArray(),
                          sender,
                          txParams.getTo(),
                          txParams.getValue().toByteArray(),
                          txParams.getData(),
                          txParams.getNrg(),
                          txParams.getNrgPrice(),
                          TransactionTypes.DEFAULT,
                          null // TODO beacon hash goes here
                          );
  
          AionTxReceipt receipt = this.ac.callConstant(tx, b);
  
          return new RpcMsg(StringUtils.toJsonHex(receipt.getTransactionOutput()));
      }
  
      public RpcMsg eth_estimateGas(Object _params) {
          JSONObject _tx;
          if (_params instanceof JSONArray) {
              _tx = ((JSONArray) _params).getJSONObject(0);
          } else if (_params instanceof JSONObject) {
              _tx = ((JSONObject) _params).getJSONObject("transaction");
          } else {
              return new RpcMsg(null, RpcError.INVALID_PARAMS, "Invalid parameters");
          }
  
          ArgTxCall txParams = ArgTxCall.fromJSONforCall(_tx, getRecommendedNrgPrice());
  
          NumericalValue estimate = new NumericalValue(estimateNrg(txParams));
  
          return new RpcMsg(estimate.toHexString());
      }
  
      public RpcMsg eth_getBlockByHash(Object _params) {
          String _hash;
          boolean _fullTx;
          try {
              if (_params instanceof JSONArray) {
                  _hash = ((JSONArray) _params).get(0) + "";
                  _fullTx = ((JSONArray) _params).optBoolean(1, false);
              } else if (_params instanceof JSONObject) {
                  _hash = ((JSONObject) _params).get("block") + "";
                  _fullTx = ((JSONObject) _params).optBoolean("fullTransaction", false);
              } else {
                  throw new Exception("Invalid input object provided");
              }
          } catch (Exception e) {
              LOG.debug("Error processing json input arguments", e);
              return new RpcMsg(null, RpcError.INVALID_PARAMS, "Invalid parameters");
          }
  
          Block block = ac.getBlockchain().getBlockWithInfoByHash(ByteUtil.hexStringToBytes(_hash));
  
          if (block == null) {
              LOG.debug("<get-block hash={} err=not-found>", _hash);
              return new RpcMsg(JSONObject.NULL); // json rpc spec: 'or null when no block was found'
          }
  
          return new RpcMsg(Blk.AionBlockToJson(block, _fullTx));
      }
  
      public RpcMsg eth_getBlockByNumber(Object _params) {
          String _bnOrId;
          boolean _fullTx;
          try {
              if (_params instanceof JSONArray) {
                  _bnOrId = ((JSONArray) _params).get(0) + "";
                  _fullTx = ((JSONArray) _params).optBoolean(1, false);
              } else if (_params instanceof JSONObject) {
                  _bnOrId = ((JSONObject) _params).get("block") + "";
                  _fullTx = ((JSONObject) _params).optBoolean("fullTransaction", false);
              } else {
                  throw new Exception("Invalid input object provided");
              }
          } catch (Exception e) {
              LOG.debug("Error processing json input arguments", e);
              return new RpcMsg(null, RpcError.INVALID_PARAMS, "Invalid parameters");
          }
  
          Long bn = this.parseBnOrId(_bnOrId);
  
          if (bn == null) {
              return new RpcMsg(null, RpcError.INVALID_PARAMS, "Invalid block number.");
          }
  
          Block nb = getBlockByBN(bn);
  
          if (nb == null) {
              LOG.debug("<get-block bn={} err=not-found>", bn);
              return new RpcMsg(JSONObject.NULL); // json rpc spec: 'or null when no block was found'
          }
  
          // add main chain block to cache (currently only used by ops_getTransactionReceipt_*
          // functions)
          blockCache.put(nb.getHashWrapper(), nb);
          return new RpcMsg(Blk.AionBlockToJson(nb, _fullTx));
      }
  
      public RpcMsg eth_getTransactionByHash(Object _params) {
          String _hash;
          try {
              if (_params instanceof JSONArray) {
                  _hash = ((JSONArray) _params).get(0) + "";
              } else if (_params instanceof JSONObject) {
                  _hash = ((JSONObject) _params).get("transactionHash") + "";
              } else {
                  throw new Exception("Invalid input object provided");
              }
          } catch (Exception e) {
              LOG.debug("Error processing json input arguments", e);
              return new RpcMsg(null, RpcError.INVALID_PARAMS, "Invalid parameters");
          }
  
          byte[] txHash = ByteUtil.hexStringToBytes(_hash);
          if (_hash.equals("null") || txHash == null) {
              return null;
          }
  
          AionTxInfo txInfo = this.ac.getAionHub().getBlockchain().getTransactionInfo(txHash);
          if (txInfo == null) {
              return new RpcMsg(
                      JSONObject.NULL); // json rpc spec: 'or null when no transaction was found'
          }
  
          Block b = this.ac.getBlockchain().getBlockByHash(txInfo.getBlockHash());
          if (b == null) {
              return null; // this is actually an internal error
          }
  
          return new RpcMsg(Tx.InfoToJSON(txInfo, b));
      }
  
      public RpcMsg eth_getInternalTransactionsByHash(Object _params) {
          String _hash;
          try {
              if (_params instanceof JSONArray) {
                  _hash = ((JSONArray) _params).get(0) + "";
              } else if (_params instanceof JSONObject) {
                  _hash = ((JSONObject) _params).get("transactionHash") + "";
              } else {
                  throw new Exception("Invalid input object provided");
              }
          } catch (Exception e) {
              LOG.debug("Error processing json input arguments", e);
              return new RpcMsg(null, RpcError.INVALID_PARAMS, "Invalid parameters");
          }
  
          byte[] txHash = ByteUtil.hexStringToBytes(_hash);
          if (_hash.equals("null") || txHash == null) {
              return null;
          }
  
          AionTxInfo txInfo = this.ac.getAionHub().getBlockchain().getTransactionInfo(txHash);
          if (txInfo == null) {
              return new RpcMsg(JSONObject.NULL); // json rpc spec: 'or null when no transaction was found'
          }
  
          Block b = this.ac.getBlockchain().getBlockByHash(txInfo.getBlockHash());
          if (b == null) {
              return null; // this is actually an internal error
          }
  
          return new RpcMsg(Tx.internalTxsToJSON(txInfo.getInternalTransactions(), txHash, txInfo.isCreatedWithInternalTransactions()));
      }
  
      public RpcMsg eth_getTransactionByBlockHashAndIndex(Object _params) {
          String _hash;
          String _index;
          if (_params instanceof JSONArray) {
              _hash = ((JSONArray) _params).get(0) + "";
              _index = ((JSONArray) _params).get(1) + "";
          } else if (_params instanceof JSONObject) {
              _hash = ((JSONObject) _params).get("blockHash") + "";
              _index = ((JSONObject) _params).get("index") + "";
          } else {
              return new RpcMsg(null, RpcError.INVALID_PARAMS, "Invalid parameters");
          }
  
          byte[] hash = ByteUtil.hexStringToBytes(_hash);
          if (_hash.equals("null") || hash == null) {
              return null;
          }
  
          Block b = this.ac.getBlockchain().getBlockByHash(hash);
          if (b == null) {
              return new RpcMsg(
                      JSONObject.NULL); // json rpc spec: 'or null when no transaction was found'
          }
  
          List<AionTransaction> txs = b.getTransactionsList();
  
          int idx = Integer.decode(_index);
          if (idx >= txs.size()) {
              return new RpcMsg(
                      JSONObject.NULL); // json rpc spec: 'or null when no transaction was found'
          }
  
          return new RpcMsg(Tx.AionTransactionToJSON(txs.get(idx), b, idx));
      }
  
      public RpcMsg eth_getTransactionByBlockNumberAndIndex(Object _params) {
          String _bnOrId;
          String _index;
          if (_params instanceof JSONArray) {
              _bnOrId = ((JSONArray) _params).get(0) + "";
              _index = ((JSONArray) _params).get(1) + "";
          } else if (_params instanceof JSONObject) {
              _bnOrId = ((JSONObject) _params).get("block") + "";
              _index = ((JSONObject) _params).get("index") + "";
          } else {
              return new RpcMsg(null, RpcError.INVALID_PARAMS, "Invalid parameters");
          }
  
          Long bn = parseBnOrId(_bnOrId);
          if (bn == null) {
              return new RpcMsg(null, RpcError.INVALID_PARAMS, "Invalid block number.");
          }
  
          Block b = this.getBlockByBN(bn);
  
          if (b == null) {
              return new RpcMsg(
                      JSONObject.NULL); // json rpc spec: 'or null when no transaction was found'
          }
  
          List<AionTransaction> txs = b.getTransactionsList();
  
          int idx = Integer.decode(_index);
          if (idx >= txs.size()) {
              return new RpcMsg(
                      JSONObject.NULL); // json rpc spec: 'or null when no transaction was found'
          }
  
          return new RpcMsg(Tx.AionTransactionToJSON(txs.get(idx), b, idx));
      }
  
      public RpcMsg eth_getTransactionReceipt(Object _params) {
          String _hash;
          if (_params instanceof JSONArray) {
              _hash = ((JSONArray) _params).get(0) + "";
          } else if (_params instanceof JSONObject) {
              _hash = ((JSONObject) _params).get("hash") + "";
          } else {
              return new RpcMsg(null, RpcError.INVALID_PARAMS, "Invalid parameters");
          }
  
          byte[] txHash = StringUtils.StringHexToByteArray(_hash);
          TxRecpt r = getTransactionReceipt(txHash);
  
          // commenting this out because of lack support for old web3 client that we are using
          // TODO: re-enable this when we upgrade our web3 client
          /*
          // if we can't find the receipt on the mainchain, try looking for it in pending receipts cache
          /*
          if (r == null) {
              AionTxReceipt pendingReceipt = pendingReceipts.get(new ByteArrayWrapper(txHash));
              r = new TxRecpt(pendingReceipt, null, null, null, true);
          }
          */
  
          if (r == null) {
              return new RpcMsg(
                      JSONObject.NULL); // json rpc spec: 'or null when no receipt was found'
          }
  
          return new RpcMsg(r.toJson());
      }
  
      /* -------------------------------------------------------------------------
       * compiler
       */
  
      public RpcMsg eth_getCompilers() {
          return new RpcMsg(new JSONArray(this.compilers));
      }
  
      public RpcMsg eth_compileSolidity(Object _params) {
          String _contract;
          if (_params instanceof JSONArray) {
              _contract = ((JSONArray) _params).get(0) + "";
          } else if (_params instanceof JSONObject) {
              _contract = ((JSONObject) _params).get("contract") + "";
          } else {
              return new RpcMsg(null, RpcError.INVALID_PARAMS, "Invalid parameters");
          }
  
          @SuppressWarnings("unchecked")
          Map<String, CompiledContr> compiled = contract_compileSolidity(_contract);
          JSONObject obj = new JSONObject();
          for (String key : compiled.keySet()) {
              CompiledContr cc = compiled.get(key);
              obj.put(key, cc.toJSON());
          }
          return new RpcMsg(obj);
      }
  
      public RpcMsg eth_compileSolidityZip(Object _params) {
          String _zipfile, _entrypoint;
          if (_params instanceof JSONArray) {
              _zipfile = ((JSONArray) _params).get(0) + "";
              _entrypoint = ((JSONArray) _params).get(1) + "";
          } else if (_params instanceof JSONObject) {
              _zipfile = ((JSONObject) _params).get("zipfile") + "";
              _entrypoint = ((JSONObject) _params).get("entrypoint") + "";
          } else {
              return new RpcMsg(null, RpcError.INVALID_PARAMS, "Invalid parameters");
          }
  
          byte[] _zippedBytes = Base64.getDecoder().decode(_zipfile);
  
          @SuppressWarnings("unchecked")
          Map<String, CompiledContr> compiled =
                  contract_compileSolidityZip(_zippedBytes, _entrypoint);
          JSONObject obj = new JSONObject();
          for (String key : compiled.keySet()) {
              CompiledContr cc = compiled.get(key);
              obj.put(key, cc.toJSON());
          }
          return new RpcMsg(obj);
      }
  
      /* -------------------------------------------------------------------------
       * filters
       */
  
      /* Web3 Filters Support
       *
       * NOTE: newFilter behaviour is ill-defined in the JSON-rpc spec for the following scenarios:
       * (an explanation of how we resolved these ambiguities follows immediately after)
       *
       * newFilter is used to subscribe for filter on transaction logs for transactions with provided address and topics
       *
       * role of fromBlock, toBlock fields within context of newFilter, newBlockFilter, newPendingTransactionFilter
       * (they seem only more pertinent for getLogs)
       * how we resolve it: populate historical data (best-effort) in the filter response before "installing the filter"
       * onus on the user to flush the filter of the historical data, before depending on it for up-to-date values.
       * apart from loading historical data, fromBlock & toBlock are ignored when loading events on filter queue
       */
      private FltrLg createFilter(ArgFltr rf) {
          FltrLg filter = new FltrLg();
          filter.setTopics(rf.topics);
          filter.setContractAddress(rf.address);
  
          Long bnFrom = parseBnOrId(rf.fromBlock);
          Long bnTo = parseBnOrId(rf.toBlock);
  
          // we return an filter with the error message instead of return null.
          // The rpcServer can return the detailed error message to the client.
          if (bnFrom == null || bnTo == null) {
              LOG.debug("jsonrpc - eth_newFilter(): from, to block parse failed");
              filter.setFilterError("jsonrpc - eth_newFilter(): from, to block parse failed");
              return filter;
          }
  
          if (bnTo != BEST_PENDING_BLOCK && bnFrom > bnTo) {
              LOG.debug("jsonrpc - eth_newFilter(): from block is after to block");
              filter.setFilterError("jsonrpc - eth_newFilter(): from block is after to block");
              return filter;
          }
  
          if (bnTo >= (bnFrom + BLOCKS_QUERY_MAX)) {
              String errLog = "jsonrpc - eth_newFilter(): can't query more than " + BLOCKS_QUERY_MAX + " blocks";
              LOG.debug(errLog);
              filter.setFilterError(errLog);
              return filter;
          }
  
          Block fromBlock = this.getBlockByBN(bnFrom);
          Block toBlock = this.getBlockByBN(bnTo);
  
          if (fromBlock != null) {
              // need to add historical data
              // this is our own policy: what to do in this case is not defined in the spec
              //
              // policy: add data from earliest to latest, until we can't fill the queue anymore
              //
              // caveat: filling up the events-queue with historical data will cause the following
              // issue:
              // the user will miss all events generated between the first poll and filter
              // installation.
  
              toBlock = toBlock == null ? getBestBlock() : toBlock;
              for (long i = fromBlock.getNumber(); i <= toBlock.getNumber(); i++) {
                  if (filter.isFull()) {
                      break;
                  }
                  filter.onBlock(
                          this.ac.getBlockchain().getBlockByNumber(i),
                          this.ac.getAionHub().getBlockchain());
              }
          }
  
          return filter;
      }
  
      public RpcMsg eth_newFilter(Object _params) {
          if (!isFilterEnabled) {
              return new RpcMsg(null, RpcError.NOT_ALLOWED, "Filters over rpc disabled.");
          }
  
          JSONObject _filterObj;
          if (_params instanceof JSONArray) {
              _filterObj = ((JSONArray) _params).getJSONObject(0);
          } else if (_params instanceof JSONObject) {
              _filterObj = ((JSONObject) _params).getJSONObject("filter");
          } else {
              return new RpcMsg(null, RpcError.INVALID_PARAMS, "Invalid parameters");
          }
  
          ArgFltr rf = ArgFltr.fromJSON(_filterObj);
          if (rf == null) {
              return new RpcMsg(null, RpcError.INVALID_PARAMS, "Invalid filter object provided.");
          }
  
          FltrLg filter = createFilter(rf);
          if (filter.getFilterError() != null) {
              return new RpcMsg(null, RpcError.INVALID_PARAMS, "Invalid filter object provided. " + filter.getFilterError());
          }
  
          // "install" the filter after populating historical data;
          // rationale: until the user gets the id back, the user should not expect the filter to be
          // "installed" anyway.
          long id = fltrIndex.getAndIncrement();
          installedFilters.put(id, filter);
  
          return new RpcMsg(StringUtils.toJsonHex(id));
      }
  
      public RpcMsg eth_newBlockFilter() {
          if (!isFilterEnabled) {
              return new RpcMsg(null, RpcError.NOT_ALLOWED, "Filters over rpc disabled.");
          }
  
          long id = fltrIndex.getAndIncrement();
          installedFilters.put(id, new FltrBlk());
          return new RpcMsg(StringUtils.toJsonHex(id));
      }
  
      public RpcMsg eth_newPendingTransactionFilter() {
          if (!isFilterEnabled) {
              return new RpcMsg(null, RpcError.NOT_ALLOWED, "Filters over rpc disabled.");
          }
  
          long id = fltrIndex.getAndIncrement();
          installedFilters.put(id, new FltrTx());
          return new RpcMsg(StringUtils.toJsonHex(id));
      }
  
      public RpcMsg eth_uninstallFilter(Object _params) {
          if (!isFilterEnabled) {
              return new RpcMsg(null, RpcError.NOT_ALLOWED, "Filters over rpc disabled.");
          }
  
          String _id;
          if (_params instanceof JSONArray) {
              _id = ((JSONArray) _params).get(0) + "";
          } else if (_params instanceof JSONObject) {
              _id = ((JSONObject) _params).get("id") + "";
          } else {
              return new RpcMsg(null, RpcError.INVALID_PARAMS, "Invalid parameters");
          }
  
          return new RpcMsg(
                  installedFilters.remove(StringUtils.StringHexToBigInteger(_id).longValue())
                          != null);
      }
  
      private JSONArray buildFilterResponse(Fltr filter) {
          Object[] events = filter.poll();
          JSONArray response = new JSONArray();
          for (Object event : events) {
              if (event instanceof Evt) {
                  // put the Object we get out of the Evt object in here
                  response.put(((Evt) event).toJSON());
              }
          }
          return response;
      }
  
      public RpcMsg eth_getFilterChanges(Object _params) {
          if (!isFilterEnabled) {
              return new RpcMsg(null, RpcError.NOT_ALLOWED, "Filters over rpc disabled.");
          }
  
          String _id;
          if (_params instanceof JSONArray) {
              _id = ((JSONArray) _params).get(0) + "";
          } else if (_params instanceof JSONObject) {
              _id = ((JSONObject) _params).get("id") + "";
          } else {
              return new RpcMsg(null, RpcError.INVALID_PARAMS, "Invalid parameters");
          }
  
          long id = StringUtils.StringHexToBigInteger(_id).longValue();
          Fltr filter = installedFilters.get(id);
  
          if (filter == null) {
              return new RpcMsg(null, RpcError.EXECUTION_ERROR, "Filter not found.");
          }
  
          return new RpcMsg(buildFilterResponse(filter));
      }
  
      public RpcMsg eth_getLogs(Object _params) {
          JSONObject _filterObj;
          if (_params instanceof JSONArray) {
              _filterObj = ((JSONArray) _params).getJSONObject(0);
          } else if (_params instanceof JSONObject) {
              _filterObj = ((JSONObject) _params).getJSONObject("filter");
          } else {
              return new RpcMsg(null, RpcError.INVALID_PARAMS, "Invalid parameters");
          }
  
          ArgFltr rf = ArgFltr.fromJSON(_filterObj);
          if (rf == null) {
              return new RpcMsg(null, RpcError.INVALID_PARAMS, "Invalid filter object provided.");
          }
  
          FltrLg filter = createFilter(rf);
          if (filter.getFilterError() != null) {
              return new RpcMsg(null, RpcError.INVALID_PARAMS, "Invalid filter object provided. " + filter.getFilterError());
          }
  
          return new RpcMsg(buildFilterResponse(filter));
      }
  
      /* -------------------------------------------------------------------------
       * personal
       */
  
      public RpcMsg personal_unlockAccount(Object _params) {
          String _account;
          String _password;
          Object _duration;
          if (_params instanceof JSONArray) {
              _account = ((JSONArray) _params).get(0) + "";
              _password = ((JSONArray) _params).get(1) + "";
              _duration = ((JSONArray) _params).opt(2);
          } else if (_params instanceof JSONObject) {
              _account = ((JSONObject) _params).get("address") + "";
              _password = ((JSONObject) _params).get("password") + "";
              _duration = ((JSONObject) _params).opt("duration");
          } else {
              return new RpcMsg(null, RpcError.INVALID_PARAMS, "Invalid parameters");
          }
  
          int duration = 300;
          if (!JSONObject.NULL.equals(_duration)) {
              duration = new BigInteger(_duration + "").intValueExact();
          }
  
          return new RpcMsg(unlockAccount(_account, _password, duration));
      }
  
      public RpcMsg personal_lockAccount(Object _params) {
          String _account;
          String _password;
          if (_params instanceof JSONArray) {
              _account = ((JSONArray) _params).get(0) + "";
              _password = ((JSONArray) _params).get(1) + "";
          } else if (_params instanceof JSONObject) {
              _account = ((JSONObject) _params).get("address") + "";
              _password = ((JSONObject) _params).get("password") + "";
          } else {
              return new RpcMsg(null, RpcError.INVALID_PARAMS, "Invalid parameters");
          }
  
          return new RpcMsg(lockAccount(AddressUtils.wrapAddress(_account), _password));
      }
  
      public RpcMsg personal_newAccount(Object _params) {
          String _password;
          if (_params instanceof JSONArray) {
              _password = ((JSONArray) _params).get(0) + "";
          } else if (_params instanceof JSONObject) {
              _password = ((JSONObject) _params).get("password") + "";
          } else {
              return new RpcMsg(null, RpcError.INVALID_PARAMS, "Invalid parameters");
          }
  
          String address = Keystore.create(_password);
  
          return new RpcMsg(StringUtils.toJsonHex(address));
      }
  
      /* -------------------------------------------------------------------------
       * debug
       */
  
      public RpcMsg debug_getBlocksByNumber(Object _params) {
          String _bnOrId;
          boolean _fullTx;
          if (_params instanceof JSONArray) {
              _bnOrId = ((JSONArray) _params).get(0) + "";
              _fullTx = ((JSONArray) _params).optBoolean(1, false);
          } else if (_params instanceof JSONObject) {
              _bnOrId = ((JSONObject) _params).get("block") + "";
              _fullTx = ((JSONObject) _params).optBoolean("fullTransaction", false);
          } else {
              return new RpcMsg(null, RpcError.INVALID_PARAMS, "Invalid parameters");
          }
  
          Long bn = parseBnOrId(_bnOrId);
  
          if (bn == null || bn < 0) {
              return new RpcMsg(null, RpcError.INVALID_PARAMS, "Invalid block number.");
          }
  
          List<Block> blocks = this.ac.getAionHub().getBlockchain().getBlocksByNumber(bn);
          if (blocks == null) {
              return new RpcMsg(null, RpcError.EXECUTION_ERROR, "Blocks requested not found.");
          }
  
          JSONArray response = new JSONArray();
          for (Block block : blocks) {
              JSONObject b = (JSONObject) Blk.AionBlockToJson(block, _fullTx);
              b.put("mainchain", block.isMainChain());
              response.put(b);
          }
          return new RpcMsg(response);
      }
  
      /* -------------------------------------------------------------------------
       * private debugging APIs
       * Reasoning for not adding this to conventional web3 calls is so
       * we can freely change the responses without breaking compatibility
       */
      public RpcMsg priv_peers() {
          Map<Integer, NodeWrapper> activeNodes = this.ac.getAionHub().getActiveNodes();
  
          JSONArray peerList = new JSONArray();
  
          for (NodeWrapper node : activeNodes.values()) {
              JSONObject n = new JSONObject();
              n.put("idShort", node.getIdShort());
              n.put("id", new String(node.getId()));
              n.put("idHash", node.getIdHash());
              n.put("version", node.getBinaryVersion());
              n.put("blockNumber", node.getBestBlockNumber());
              n.put("totalDifficulty", node.getTotalDifficulty());
  
              JSONObject network = new JSONObject();
              network.put("remoteAddress", node.getIpStr() + ":" + node.getPort());
              n.put("network", network);
              n.put("latestTimestamp", node.getTimestamp());
  
              // generate a date corresponding to UTC date time (not local)
              String utcTimestampDate =
                      Instant.ofEpochMilli(node.getTimestamp()).atOffset(ZoneOffset.UTC).toString();
              n.put("latestTimestampUTC", utcTimestampDate);
              n.put("version", node.getBinaryVersion());
  
              peerList.put(n);
          }
          return new RpcMsg(peerList);
      }
  
      public RpcMsg priv_p2pConfig() {
          CfgNetP2p p2p = CfgAion.inst().getNet().getP2p();
  
          JSONObject obj = new JSONObject();
          obj.put("localBinding", p2p.getIp() + ":" + p2p.getPort());
          return new RpcMsg(obj);
      }
  
      // default block for pending transactions
      private static final MiningBlock defaultBlock = MiningBlock.newEmptyBlock();
  
      public RpcMsg priv_getPendingTransactions(Object _params) {
          boolean fullTx = ((JSONArray) _params).optBoolean(0, false);
          List<AionTransaction> transactions = this.ac.getPendingStateTransactions();
  
          JSONArray arr = new JSONArray();
          for (int i = 0; i < transactions.size(); i++) {
              if (fullTx) {
                  arr.put(Tx.AionTransactionToJSON(transactions.get(i), defaultBlock, i));
              } else {
                  arr.put(ByteUtil.toHexString(transactions.get(i).getTransactionHash()));
              }
          }
          return new RpcMsg(arr);
      }
  
      public RpcMsg priv_getPendingSize() {
          return new RpcMsg(this.ac.getPendingStateTransactions().size());
      }
  
      public RpcMsg priv_dumpTransaction(Object _params) {
          String transactionHash;
          if (_params instanceof JSONArray) {
              transactionHash = ((JSONArray) _params).get(0) + "";
          } else if (_params instanceof JSONObject) {
              transactionHash = ((JSONObject) _params).get("hash") + "";
          } else {
              return new RpcMsg(null, RpcError.INVALID_PARAMS, "Invalid parameters");
          }
  
          byte[] hash = ByteUtil.hexStringToBytes(transactionHash);
          if (hash == null) {
              return new RpcMsg(null, RpcError.INVALID_PARAMS, "Invalid transaction hash");
          }
  
          // begin output processing
          AionTxInfo transaction = this.ac.getAionHub().getBlockchain().getTransactionInfo(hash);
  
          if (transaction == null) {
              return new RpcMsg(JSONObject.NULL);
          }
  
          JSONObject tx =
                  Tx.InfoToJSON(
                          transaction,
                          this.ac.getBlockchain().getBlockByHash(transaction.getBlockHash()));
          String raw = ByteUtil.toHexString(transaction.getReceipt().getTransaction().getEncoded());
  
          JSONObject obj = new JSONObject();
          obj.put("transaction", tx);
          obj.put("raw", raw);
          return new RpcMsg(obj);
      }
  
      public RpcMsg priv_dumpBlockByHash(Object _params) {
          String hashString;
          if (_params instanceof JSONArray) {
              hashString = ((JSONArray) _params).get(0) + "";
          } else if (_params instanceof JSONObject) {
              hashString = ((JSONObject) _params).get("hash") + "";
          } else {
              return new RpcMsg(null, RpcError.INVALID_PARAMS, "Invalid parameters");
          }
  
          byte[] hash = ByteUtil.hexStringToBytes(hashString);
          if (hash == null) {
              return new RpcMsg(null, RpcError.INVALID_PARAMS, "Invalid block hash");
          }
  
          Block block = this.ac.getBlockchain().getBlockWithInfoByHash(hash);
  
          if (block == null) {
              return new RpcMsg(JSONObject.NULL);
          }
  
          return new RpcMsg(dumpBlock(block, false));
      }
  
      public RpcMsg priv_dumpBlockByNumber(Object _params) {
          String numberString;
          if (_params instanceof JSONArray) {
              numberString = ((JSONArray) _params).get(0) + "";
          } else if (_params instanceof JSONObject) {
              numberString = ((JSONObject) _params).get("number") + "";
          } else {
              return new RpcMsg(null, RpcError.INVALID_PARAMS, "Invalid parameters");
          }
  
          // TODO: parse hex
          long number;
          try {
              number = Long.parseLong(numberString);
          } catch (NumberFormatException e) {
              return new RpcMsg(null, RpcError.INVALID_PARAMS, "Unable to decode input number");
          }
          Block block = this.ac.getBlockchain().getBlockByNumber(number);
  
          if (block == null) {
              return new RpcMsg(JSONObject.NULL);
          }
  
          return new RpcMsg(dumpBlock(block, false));
      }
  
      private static JSONObject dumpBlock(Block block, boolean full) {
          JSONObject obj = new JSONObject();
          obj.put("block", Blk.AionBlockToJson(block, full));
          obj.put("raw", ByteUtil.toHexString(block.getEncoded()));
          return obj;
      }
  
      /**
       * Very short blurb generated about our most important stats, intended for quick digestion and
       * monitoring tool usage
       */
      // TODO
      public RpcMsg priv_shortStats() {
          Block block = this.ac.getBlockchain().getBestBlock();
          Map<Integer, NodeWrapper> peer = this.ac.getAionHub().getActiveNodes();
  
          // this could be optimized (cached)
          NodeWrapper maxPeer = null;
          for (NodeWrapper p : peer.values()) {
              if (maxPeer == null) {
                  maxPeer = p;
                  continue;
              }
  
              if (p.getTotalDifficulty().compareTo(maxPeer.getTotalDifficulty()) > 0) {
                  maxPeer = p;
              }
          }
  
          // basic local configuration
          CfgAion config = CfgAion.inst();
  
          JSONObject obj = new JSONObject();
          obj.put("id", config.getId());
          obj.put("genesisHash", ByteUtil.toHexString(config.getGenesis().getHash()));
          obj.put("version", Version.KERNEL_VERSION);
          obj.put("bootBlock", this.ac.getAionHub().getStartingBlock().getNumber());
  
          long time = System.currentTimeMillis();
          obj.put("timestamp", time);
          obj.put("timestampUTC", Instant.ofEpochMilli(time).atOffset(ZoneOffset.UTC).toString());
  
          // base.blockchain
          JSONObject blockchain = new JSONObject();
          blockchain.put("bestBlockhash", ByteUtil.toHexString(block.getHash()));
          blockchain.put("bestNumber", block.getNumber());
          blockchain.put(
                  "totalDifficulty",
                  this.ac.getBlockchain().getTotalDifficultyForHash(block.getHash()));
          // end
          obj.put("local", blockchain);
  
          // base.network
          JSONObject network = new JSONObject();
          // remote
          if (maxPeer != null) {
              // base.network.best
              JSONObject remote = new JSONObject();
              remote.put("id", new String(maxPeer.getId()));
              remote.put("totalDifficulty", maxPeer.getTotalDifficulty());
              remote.put("bestNumber", maxPeer.getBestBlockNumber());
              remote.put("version", maxPeer.getBinaryVersion());
              remote.put("timestamp", maxPeer.getTimestamp());
              remote.put(
                      "timestampUTC",
                      Instant.ofEpochMilli(maxPeer.getTimestamp())
                              .atOffset(ZoneOffset.UTC)
                              .toString());
              // end
              network.put("best", remote);
          }
  
          // end
          network.put("peerCount", peer.size());
          obj.put("network", network);
  
          return new RpcMsg(obj);
      }
  
      public RpcMsg priv_config() {
          JSONObject obj = new JSONObject();
  
          CfgAion config = CfgAion.inst();
  
          obj.put("id", config.getId());
          obj.put("basePath", config.getBasePath());
  
          obj.put("net", configNet());
          obj.put("consensus", configConsensus());
          obj.put("sync", configSync());
          obj.put("api", configApi());
          obj.put("db", configDb());
          obj.put("tx", configTx());
  
          return new RpcMsg(obj);
      }
  
      // TODO: we can refactor these in the future to be in
      // their respective classes, for now put the toJson here
  
      private static JSONObject configNet() {
          CfgNet config = CfgAion.inst().getNet();
          JSONObject obj = new JSONObject();
  
          // begin base.net.p2p
          CfgNetP2p configP2p = config.getP2p();
          JSONObject p2p = new JSONObject();
          p2p.put("ip", configP2p.getIp());
          p2p.put("port", configP2p.getPort());
          p2p.put("discover", configP2p.getDiscover());
          p2p.put("errorTolerance", configP2p.getErrorTolerance());
          p2p.put("maxActiveNodes", configP2p.getMaxActiveNodes());
          p2p.put("maxTempNodes", configP2p.getMaxTempNodes());
          p2p.put("clusterNodeMode", configP2p.inClusterNodeMode());
          p2p.put("syncOnlyMode", configP2p.inSyncOnlyMode());
  
          // end
          obj.put("p2p", p2p);
  
          // begin base.net.nodes[]
          JSONArray nodeArray = new JSONArray();
          for (String n : config.getNodes()) {
              nodeArray.put(n);
          }
  
          // end
          obj.put("nodes", nodeArray);
          // begin base
          obj.put("id", config.getId());
  
          // end
          return obj;
      }
  
      private static JSONObject configConsensus() {
          CfgConsensusUnity config = CfgAion.inst().getConsensus();
          JSONObject obj = new JSONObject();
          obj.put("mining", config.getMining());
          obj.put("minerAddress", config.getMinerAddress());
          obj.put("threads", config.getCpuMineThreads());
          obj.put("extraData", config.getExtraData());
  
          // base.consensus.energyStrategy
          CfgEnergyStrategy nrg = config.getEnergyStrategy();
          JSONObject nrgObj = new JSONObject();
          nrgObj.put("strategy", nrg.getStrategy());
          nrgObj.put("target", nrg.getTarget());
          nrgObj.put("upper", nrg.getUpperBound());
          nrgObj.put("lower", nrg.getLowerBound());
  
          // end
          obj.put("energyStrategy", nrgObj);
          return obj;
      }
  
      private static JSONObject configSync() {
          CfgSync config = CfgAion.inst().getSync();
          JSONObject obj = new JSONObject();
          obj.put("showStatus", config.getShowStatus());
          return obj;
      }
  
      private static JSONObject configApi() {
          CfgApi config = CfgAion.inst().getApi();
  
          JSONObject obj = new JSONObject();
  
          // base.api.rpc
          CfgApiRpc rpcConfig = config.getRpc();
          CfgSsl sslConfig = rpcConfig.getSsl();
          JSONObject rpc = new JSONObject();
          rpc.put("ip", rpcConfig.getIp());
          rpc.put("port", rpcConfig.getPort());
          rpc.put("corsEnabled", rpcConfig.isCorsEnabled());
          rpc.put("active", rpcConfig.isActive());
          rpc.put("maxThread", rpcConfig.getWorkerThreads());
          rpc.put("sslEnabled", sslConfig.getEnabled());
          rpc.put("sslCert", sslConfig.getCert());
          rpc.put("sslPass", sslConfig.getPass());
  
          // end
          obj.put("rpc", rpc);
  
          // base.api.zmq
          CfgApiZmq zmqConfig = config.getZmq();
          JSONObject zmq = new JSONObject();
  
          zmq.put("ip", zmqConfig.getIp());
          zmq.put("port", zmqConfig.getPort());
          zmq.put("active", zmqConfig.getActive());
  
          // end
          obj.put("zmq", zmq);
  
          // base.api.nrg
          CfgApiNrg nrgConfig = config.getNrg();
          JSONObject nrg = new JSONObject();
  
          nrg.put("defaultPrice", nrgConfig.getNrgPriceDefault());
          nrg.put("maxPrice", nrgConfig.getNrgPriceMax());
  
          // end
          obj.put("nrg", nrg);
          return obj;
      }
  
      // this is temporarily disabled until DB configuration changes come in
      private static JSONObject configDb() {
          return new JSONObject();
      }
  
      private static JSONObject configTx() {
          CfgTx config = CfgAion.inst().getTx();
          JSONObject obj = new JSONObject();
          obj.put("cacheMax", config.getCacheMax());
          obj.put("poolBackup", config.getPoolBackup());
          obj.put("poolDump", config.getPoolDump());
          obj.put("seedmode", config.isSeedMode());
          return obj;
      }
  
      /* -------------------------------------------------------------------------
       * operational api
       */
  
      // always gets the latest account state
      public RpcMsg ops_getAccountState(Object _params) {
          String _address;
          if (_params instanceof JSONArray) {
              _address = ((JSONArray) _params).get(0) + "";
          } else if (_params instanceof JSONObject) {
              _address = ((JSONObject) _params).get("address") + "";
          } else {
              return new RpcMsg(null, RpcError.INVALID_PARAMS, "Invalid parameters");
          }
  
          AionAddress address;
  
          try {
              address = AddressUtils.wrapAddress(_address);
          } catch (Exception e) {
              return new RpcMsg(null, RpcError.INVALID_PARAMS, "Invalid address provided.");
          }
  
          long latestBlkNum = this.getBestBlock().getNumber();
  
          Optional<AccountState> accountState = ac.getAccountState(address);
  
          BigInteger nonce = BigInteger.ZERO;
          BigInteger balance = BigInteger.ZERO;
  
          if (accountState.isPresent()) {
              nonce = accountState.get().getNonce();
              balance = accountState.get().getBalance();
          }
  
          JSONObject response = new JSONObject();
          response.put("address", address.toString());
          response.put("blockNumber", latestBlkNum);
          response.put("balance", StringUtils.toJsonHex(balance));
          response.put("nonce", StringUtils.toJsonHex(nonce));
  
          return new RpcMsg(response);
      }
  
      // always gets the latest 20 blocks and transactions
      private class ChainHeadView {
  
          LinkedList<byte[]> hashQueue; // more precisely a dequeue
          Map<byte[], JSONObject> blkList;
          Map<byte[], Block> blkObjList;
          Map<byte[], List<AionTransaction>> txnList;
  
          private JSONObject response;
  
          private int qSize;
  
          public ChainHeadView(ChainHeadView cv) {
              hashQueue = new LinkedList<>(cv.hashQueue);
              blkList = new HashMap<>(cv.blkList);
              blkObjList = new HashMap<>(cv.blkObjList);
              txnList = new HashMap<>(cv.txnList);
              response = new JSONObject(cv.response, JSONObject.getNames(cv.response));
              qSize = cv.qSize;
          }
  
          public ChainHeadView(int _qSize) {
              hashQueue = new LinkedList<>();
              blkList = new HashMap<>();
              blkObjList = new HashMap<>();
              txnList = new HashMap<>();
              response = new JSONObject();
              qSize = _qSize;
          }
  
          private JSONObject getJson(Block _b) {
              return Blk.AionBlockOnlyToJson(_b);
          }
  
          private JSONObject buildResponse() {
              JSONArray blks = new JSONArray();
              JSONArray txns = new JSONArray();
  
              // return qSize number of blocks and transactions as json
              for (int i = 0; i < hashQueue.size(); i++) {
                  byte[] hash = hashQueue.get(i);
                  JSONObject blk = blkList.get(hash);
                  if (i < hashQueue.size() - 1) {
                      Block blkThis = blkObjList.get(hash);
                      Block blkNext = blkObjList.get(hashQueue.get(i + 1));
                      blk.put("blockTime", blkThis.getTimestamp() - blkNext.getTimestamp());
                  }
                  blks.put(blk);
                  List<AionTransaction> t = txnList.get(hash);
  
                  for (int j = 0; (j < t.size() && txns.length() <= qSize); j++) {
                      txns.put(Tx.AionTransactionToJSON(t.get(j), blkObjList.get(hash), j));
                  }
              }
  
              JSONObject metrics;
              try {
                  metrics = computeMetrics();
              } catch (Exception e) {
                  LOG.error("failed to compute metrics.", e);
                  metrics = new JSONObject();
              }
  
              JSONObject o = new JSONObject();
              o.put("blocks", blks);
              o.put("transactions", txns);
              o.put("metrics", metrics);
              return o;
          }
  
          private JSONObject computeMetrics() {
              long blkTimeAccumulator = 0L;
              BigInteger lastDifficulty = null;
              BigInteger difficultyAccumulator = new BigInteger("0");
              BigInteger nrgConsumedAccumulator = new BigInteger("0");
              BigInteger nrgLimitAccumulator = new BigInteger("0");
              long txnCount = 0L;
  
              int count = blkObjList.size();
              Long lastBlkTimestamp = null;
              Block b = null;
              ListIterator li = hashQueue.listIterator(0);
              while (li.hasNext()) {
                  byte[] hash = (byte[]) li.next();
                  b = blkObjList.get(hash);
  
                  if (lastBlkTimestamp != null) {
                      blkTimeAccumulator += (lastBlkTimestamp - b.getTimestamp());
                  }
                  lastBlkTimestamp = b.getTimestamp();
  
                  difficultyAccumulator =
                          difficultyAccumulator.add(new BigInteger(b.getDifficulty()));
                  lastDifficulty = new BigInteger(b.getDifficulty());
  
                  nrgConsumedAccumulator =
                          nrgConsumedAccumulator.add(
                                  new BigInteger(Long.toString(b.getNrgConsumed())));
                  nrgLimitAccumulator =
                          nrgLimitAccumulator.add(new BigInteger(Long.toString(b.getNrgLimit())));
                  txnCount += b.getTransactionsList().size();
              }
              BigInteger lastBlkReward = (b == null ? null : ac.getBlockchain().calculateBlockRewards(b.getNumber()));
  
              double blkTime = 0;
              double hashRate = 0;
              double avgDifficulty = 0;
              double avgNrgConsumedPerBlock = 0;
              double avgNrgLimitPerBlock = 0;
              double txnPerSec = 0;
  
              if (count > 0 && blkTimeAccumulator > 0) {
                  blkTime = blkTimeAccumulator / (double) count;
                  hashRate = lastDifficulty.longValue() / blkTime;
                  avgDifficulty = difficultyAccumulator.longValue() / (double) count;
                  avgNrgConsumedPerBlock = nrgConsumedAccumulator.longValue() / (double) count;
                  avgNrgLimitPerBlock = nrgLimitAccumulator.longValue() / (double) count;
                  txnPerSec = txnCount / (double) blkTimeAccumulator;
              }
  
              long startBlock = 0;
              long endBlock = 0;
              long startTimestamp = 0;
              long endTimestamp = 0;
              long currentBlockchainHead = 0;
  
              if (hashQueue.size() > 0) {
                  Block startBlockObj = blkObjList.get(hashQueue.peekLast());
                  Block endBlockObj = blkObjList.get(hashQueue.peekFirst());
  
                  startBlock = startBlockObj.getNumber();
                  endBlock = endBlockObj.getNumber();
                  startTimestamp = startBlockObj.getTimestamp();
                  endTimestamp = endBlockObj.getTimestamp();
                  currentBlockchainHead = endBlock;
              }
  
              JSONObject metrics = new JSONObject();
              metrics.put("averageDifficulty", avgDifficulty);
              metrics.put("averageBlockTime", blkTime);
              metrics.put("hashRate", hashRate);
              metrics.put("transactionPerSecond", txnPerSec);
              metrics.put("lastBlockReward", (lastBlkReward == null ? "null" : lastBlkReward));
              metrics.put("targetBlockTime", 10);
              metrics.put("blockWindow", OPS_RECENT_ENTITY_COUNT);
  
              metrics.put("startBlock", startBlock);
              metrics.put("endBlock", endBlock);
              metrics.put("startTimestamp", startTimestamp);
              metrics.put("endTimestamp", endTimestamp);
              metrics.put("currentBlockchainHead", currentBlockchainHead);
  
              metrics.put("averageNrgConsumedPerBlock", avgNrgConsumedPerBlock);
              metrics.put("averageNrgLimitPerBlock", avgNrgLimitPerBlock);
  
              return metrics;
          }
  
          ChainHeadView update() {
              // get the latest head
              Block blk = ac.getBlockchain().getBestBlockWithInfo();
  
              if (Arrays.equals(hashQueue.peekFirst(), blk.getHash())) {
                  return this; // nothing to do
              }
  
              // evict data as necessary
              LinkedList<Map.Entry<byte[], Map.Entry<Block, JSONObject>>> tempStack =
                      new LinkedList<>();
              tempStack.push(Map.entry(blk.getHash(), Map.entry(blk, getJson(blk))));
              int itr = 1; // deliberately 1, since we've already added the 0th element to the stack
  
              /*
              if (hashQueue.peekFirst() != null) {
                  System.out.println("[" + 0 + "]: " + StringUtils.toJsonHex(hashQueue.peekFirst()) + " - " + blocks.get(hashQueue.peekFirst()).getNumber());
                  System.out.println("----------------------------------------------------------");
                  System.out.println("isParentHashMatch? " + FastByteComparisons.equal(hashQueue.peekFirst(), blk.getParentHash()));
                  System.out.println("blk.getNumber() " + blk.getNumber());
              }
              System.out.println("blkNum: " + blk.getNumber() +
                      " parentHash: " + StringUtils.toJsonHex(blk.getParentHash()) +
                      " blkHash: " + StringUtils.toJsonHex(blk.getHash()));
              */
  
              while (!Arrays.equals(hashQueue.peekFirst(), blk.getParentHash())
                      && itr < qSize
                      && blk.getNumber() > 2) {
  
                  blk = ac.getBlockchain().getBlockWithInfoByHash(blk.getParentHash());
                  tempStack.push(Map.entry(blk.getHash(), Map.entry(blk, getJson(blk))));
                  itr++;
                  /*
                  System.out.println("blkNum: " + blk.getNumber() +
                          " parentHash: " + StringUtils.toJsonHex(blk.getParentHash()) +
                          " blkHash: " + StringUtils.toJsonHex(blk.getHash()));
                  */
              }
  
              // evict out the right number of elements first
              for (int i = 0; i < tempStack.size(); i++) {
                  byte[] tailHash = hashQueue.pollLast();
                  if (tailHash != null) {
                      blkList.remove(tailHash);
                      blkObjList.remove(tailHash);
                      txnList.remove(tailHash);
                  }
              }
  
              // empty out the stack into the queue
              while (!tempStack.isEmpty()) {
                  // add to the queue
                  Map.Entry<byte[], Map.Entry<Block, JSONObject>> element = tempStack.pop();
                  byte[] hash = element.getKey();
                  Block blkObj = element.getValue().getKey();
                  JSONObject blkJson = element.getValue().getValue();
                  List<AionTransaction> txnJson = blkObj.getTransactionsList();
  
                  hashQueue.push(hash);
                  blkList.put(hash, blkJson);
                  blkObjList.put(hash, blkObj);
                  txnList.put(hash, txnJson);
              }
  
              /*
              System.out.println("[" + 0 + "]: " + StringUtils.toJsonHex(hashQueue.peekFirst()) + " - " + blocks.get(hashQueue.peekFirst()).getNumber());
              System.out.println("----------------------------------------------------------");
              for (int i = hashQueue.size() - 1; i >= 0; i--) {
                  System.out.println("[" + i + "]: " + StringUtils.toJsonHex(hashQueue.get(i)) + " - " + blocks.get(hashQueue.get(i)).getNumber());
              }
              */
              this.response = buildResponse();
  
              return this;
          }
  
          JSONObject getResponse() {
              return response;
          }
  
          long getViewBestBlock() {
              return blkObjList.get(hashQueue.peekFirst()).getNumber();
          }
      }
  
      private enum CachedResponseType {
          CHAIN_HEAD
      }
  
      public RpcMsg ops_getChainHeadView() {
          try {
              ChainHeadView v = CachedRecentEntities.get(CachedResponseType.CHAIN_HEAD.ordinal());
              return new RpcMsg(v.getResponse());
          } catch (Exception e) {
              LOG.error("<rpc-server - cannot get cached response for ops_getChainHeadView: ", e);
              return new RpcMsg(null, RpcError.EXECUTION_ERROR, "Cached response retrieve failed.");
          }
      }
  
      public RpcMsg ops_getChainHeadViewBestBlock() {
          try {
              ChainHeadView v = CachedRecentEntities.get(CachedResponseType.CHAIN_HEAD.ordinal());
              return new RpcMsg(v.getViewBestBlock());
          } catch (Exception e) {
              LOG.error("<rpc-server - cannot get cached response for ops_getChainHeadView: ", e);
              return new RpcMsg(null, RpcError.EXECUTION_ERROR, "Cached response retrieve failed.");
          }
      }
  
      // use a custom implementation to get a receipt with 2 db reads and a constant time op, as
      // opposed to
      // the getTransactionReceipt() in parent, which computes cumulativeNrg computatio for spec
      // compliance
      public RpcMsg ops_getTransaction(Object _params) {
          String _hash;
          if (_params instanceof JSONArray) {
              _hash = ((JSONArray) _params).get(0) + "";
          } else if (_params instanceof JSONObject) {
              _hash = ((JSONObject) _params).get("hash") + "";
          } else {
              return new RpcMsg(null, RpcError.INVALID_PARAMS, "Invalid parameters");
          }
  
          byte[] txHash = StringUtils.StringHexToByteArray(_hash);
  
          if (txHash == null) {
              return new RpcMsg(null, RpcError.INVALID_PARAMS, "Invalid parameters");
          }
  
          AionTxInfo txInfo = this.ac.getAionHub().getBlockchain().getTransactionInfo(txHash);
  
          if (txInfo == null) {
              return new RpcMsg(JSONObject.NULL);
          }
  
          Block block =
                  this.ac.getAionHub().getBlockchain().getBlockByHash(txInfo.getBlockHash());
  
          if (block == null) {
              return new RpcMsg(JSONObject.NULL);
          }
  
          AionTransaction tx = txInfo.getReceipt().getTransaction();
  
          if (tx == null) {
              return new RpcMsg(JSONObject.NULL);
          }
  
          JSONObject result = new JSONObject();
          result.put("timestampVal", block.getTimestamp());
          result.put("transactionHash", StringUtils.toJsonHex(tx.getTransactionHash()));
          result.put("blockNumber", block.getNumber());
          result.put("blockHash", StringUtils.toJsonHex(block.getHash()));
          result.put("nonce", StringUtils.toJsonHex(tx.getNonce()));
          result.put("fromAddr", StringUtils.toJsonHex(tx.getSenderAddress().toByteArray()));
          result.put(
                  "toAddr",
                  StringUtils.toJsonHex(
                          tx.getDestinationAddress() == null
                                  ? EMPTY_BYTE_ARRAY
                                  : tx.getDestinationAddress().toByteArray()));
          result.put("value", StringUtils.toJsonHex(tx.getValue()));
          result.put("nrgPrice", tx.getEnergyPrice());
          result.put("nrgConsumed", txInfo.getReceipt().getEnergyUsed());
          result.put("data", StringUtils.toJsonHex(tx.getData()));
          result.put("transactionIndex", txInfo.getIndex());
  
          JSONArray logs = new JSONArray();
          for (Log l : txInfo.getReceipt().getLogInfoList()) {
              JSONObject log = new JSONObject();
              AionAddress aionAddress = new AionAddress(l.copyOfAddress());
              log.put("address", aionAddress.toString());
              log.put("data", StringUtils.toJsonHex(l.copyOfData()));
              JSONArray topics = new JSONArray();
              for (byte[] topic : l.copyOfTopics()) {
                  topics.put(StringUtils.toJsonHex(topic));
              }
              log.put("topics", topics);
              logs.put(log);
          }
          result.put("logs", logs);
  
          return new RpcMsg(result);
      }
  
      public RpcMsg ops_getBlockDetailsByNumber(Object _params){
          String _blockNumber;
          if (_params instanceof JSONArray) {
              _blockNumber = ((JSONArray) _params).get(0) + "";
          } else if (_params instanceof JSONObject) {
              _blockNumber = ((JSONObject) _params).get("block") + "";
          } else {
              return new RpcMsg(null, RpcError.INVALID_PARAMS, "Invalid parameters");
          }
          Block block;
          Long bn = this.parseBnOrId(_blockNumber);
  
          // user passed a Long block number
          if (bn != null && bn >= 0) {
              block = this.ac.getBlockchain().getBlockByNumber(bn);
              if (block == null) {
                  return new RpcMsg(JSONObject.NULL);
              }
          }else {
              return new RpcMsg(JSONObject.NULL);
          }
  
          return rpcBlockDetailsFromBlock(block);
      }
  
  
      public RpcMsg ops_getBlockDetailsByHash(Object _params){
          String _blockHash;
          if (_params instanceof JSONArray) {
              _blockHash = ((JSONArray) _params).get(0) + "";
          } else if (_params instanceof JSONObject) {
              _blockHash = ((JSONObject) _params).get("block") + "";
          } else {
              return new RpcMsg(null, RpcError.INVALID_PARAMS, "Invalid parameters");
          }
  
          Block block = this.ac.getBlockchain().getBlockByHash(ByteUtil.hexStringToBytes(_blockHash));
  
          if (block == null) {
                  return new RpcMsg(JSONObject.NULL);
          }
  
          Block mainBlock = this.ac.getBlockchain().getBlockByNumber(block.getNumber());
  
          if (mainBlock == null || !Arrays.equals(block.getHash(), mainBlock.getHash())) {
              return new RpcMsg(JSONObject.NULL);
          }
  
          return rpcBlockDetailsFromBlock(block);
      }
  
      public RpcMsg ops_getBlock(Object _params) {
          String _bnOrHash;
          boolean _fullTx;
          if (_params instanceof JSONArray) {
              _bnOrHash = ((JSONArray) _params).get(0) + "";
              _fullTx = ((JSONArray) _params).optBoolean(1, false);
          } else if (_params instanceof JSONObject) {
              _bnOrHash = ((JSONObject) _params).get("block") + "";
              _fullTx = ((JSONObject) _params).optBoolean("fullTransaction", false);
          } else {
              return new RpcMsg(null, RpcError.INVALID_PARAMS, "Invalid parameters");
          }
  
          Block block = null;
  
          Long bn = this.parseBnOrId(_bnOrHash);
  
          AionBlockchainImpl blockchainImpl = (AionBlockchainImpl) this.ac.getBlockchain();
  
          // user passed a Long block number
          if (bn != null) {
              if (bn >= 0) {
                  block = blockchainImpl.getBlockByNumber(bn);
                  if (block == null) {
                      return new RpcMsg(JSONObject.NULL);
                  }
              } else {
                  return new RpcMsg(JSONObject.NULL);
              }
          }
  
          // see if the user passed in a hash
          if (block == null) {
              block = blockchainImpl.getBlockByHash(ByteUtil.hexStringToBytes(_bnOrHash));
              if (block == null) {
                  return new RpcMsg(JSONObject.NULL);
              }
          }
  
          Block mainBlock = blockchainImpl.getBlockByNumber(block.getNumber());
          if (mainBlock == null) {
              return new RpcMsg(JSONObject.NULL);
          }
  
          if (!Arrays.equals(block.getHash(), mainBlock.getHash())) {
              return new RpcMsg(JSONObject.NULL);
          }
  
          // ok so now we have a mainchain block
          BigInteger blkReward = blockchainImpl.calculateBlockRewards(block.getHeader().getNumber());
          BigInteger totalDiff =
                  this.ac.getAionHub().getTotalDifficultyForHash(block.getHash());
  
          JSONObject blk = new JSONObject();
          blk.put("timestampVal", block.getTimestamp());
          blk.put("blockNumber", block.getNumber());
          blk.put("numTransactions", block.getTransactionsList().size());
  
          blk.put("blockHash", StringUtils.toJsonHex(block.getHash()));
          blk.put("parentHash", StringUtils.toJsonHex(block.getParentHash()));
          blk.put("minerAddress", StringUtils.toJsonHex(block.getCoinbase().toByteArray()));
  
          blk.put("receiptTxRoot", StringUtils.toJsonHex(block.getReceiptsRoot()));
          blk.put("txTrieRoot", StringUtils.toJsonHex(block.getTxTrieRoot()));
          blk.put("stateRoot", StringUtils.toJsonHex(block.getStateRoot()));
  
          blk.put("difficulty", StringUtils.toJsonHex(block.getDifficulty()));
          blk.put("totalDifficulty", totalDiff.toString(16));
  
          blk.put("blockReward", blkReward);
          blk.put("nrgConsumed", block.getNrgConsumed());
          blk.put("nrgLimit", block.getNrgLimit());
  
          blk.put("size", block.size());
          blk.put("bloom", StringUtils.toJsonHex(block.getLogBloom()));
          blk.put("extraData", StringUtils.toJsonHex(block.getExtraData()));
          blk.put("sealType", StringUtils.toJsonHex(block.getHeader().getSealType().getSealId()));
  
          if (isPowBlock(block)) {
              MiningBlock powBlock = (MiningBlock) block;
              blk.put("nonce", StringUtils.toJsonHex(powBlock.getNonce()));
              blk.put("solution", StringUtils.toJsonHex(powBlock.getHeader().getSolution()));
          } else if(isPosBlock(block)){
              StakingBlock posBlock = (StakingBlock) block;
              byte[] seedOrProof = posBlock.getHeader().getSeedOrProof();
              if (seedOrProof.length == StakingBlockHeader.SEED_LENGTH) {
                  blk.put("seed", StringUtils.toJsonHex(seedOrProof));
              } else if (seedOrProof.length == StakingBlockHeader.PROOF_LENGTH) {
                  blk.put("proof", StringUtils.toJsonHex(seedOrProof));
              } else {
                  blk.put("seedOrProofError", StringUtils.toJsonHex(seedOrProof));
              }
  
              blk.put("publicKey", StringUtils.toJsonHex(posBlock.getHeader().getSigningPublicKey()));
              blk.put("signature", StringUtils.toJsonHex(posBlock.getHeader().getSignature()));
          }
          JSONObject result = new JSONObject();
          result.put("blk", blk);
  
          if (_fullTx) {
              JSONArray txn = new JSONArray();
              for (AionTransaction tx : block.getTransactionsList()) {
                  // transactionHash, fromAddr, toAddr, value, timestampVal, blockNumber, blockHash
                  JSONArray t = new JSONArray();
                  t.put(StringUtils.toJsonHex(tx.getTransactionHash()));
                  t.put(StringUtils.toJsonHex(tx.getSenderAddress().toByteArray()));
                  t.put(
                          StringUtils.toJsonHex(
                                  tx.getDestinationAddress() == null
                                          ? EMPTY_BYTE_ARRAY
                                          : tx.getDestinationAddress().toByteArray()));
                  t.put(StringUtils.toJsonHex(tx.getValue()));
                  t.put(block.getTimestamp());
                  t.put(block.getNumber());
  
                  txn.put(t);
              }
              result.put("txn", txn);
          }
  
          return new RpcMsg(result);
      }
  
      /**
       * This function runs in ~ 30ms Is an order of magnitude slower than
       * ops_getTransactionReceiptByTransactionAndBlockHash
       */
      public RpcMsg ops_getTransactionReceiptByTransactionHash(Object _params) {
          String _transactionHash;
          if (_params instanceof JSONArray) {
              _transactionHash = ((JSONArray) _params).get(0) + "";
          } else if (_params instanceof JSONObject) {
              _transactionHash = ((JSONObject) _params).get("transactionHash") + "";
          } else {
              return new RpcMsg(null, RpcError.INVALID_PARAMS, "Invalid parameters");
          }
  
          byte[] transactionHash = StringUtils.StringHexToByteArray(_transactionHash);
  
          AionTxInfo info = this.ac.getAionHub().getBlockchain().getTransactionInfo(transactionHash);
          if (info == null) {
              return new RpcMsg(JSONObject.NULL);
          }
  
          Block block = blockCache.get(ByteArrayWrapper.wrap(info.getBlockHash()));
  
          return new RpcMsg((new TxRecpt(block, info, 0L, true)).toJson());
      }
  
      /**
       * This function runs as fast as is possible with the on-disk data model Use this to retrieve
       * the Transaction Receipt if you know the block hash already
       */
      public RpcMsg ops_getTransactionReceiptByTransactionAndBlockHash(Object _params) {
          String _transactionHash;
          String _blockHash;
          if (_params instanceof JSONArray) {
              _transactionHash = ((JSONArray) _params).get(0) + "";
              _blockHash = ((JSONArray) _params).get(1) + "";
          } else if (_params instanceof JSONObject) {
              _transactionHash = ((JSONObject) _params).get("transactionHash") + "";
              _blockHash = ((JSONObject) _params).get("blockHash") + "";
          } else {
              return new RpcMsg(null, RpcError.INVALID_PARAMS, "Invalid parameters");
          }
  
          byte[] transactionHash = StringUtils.StringHexToByteArray(_transactionHash);
          byte[] blockHash = StringUtils.StringHexToByteArray(_blockHash);
  
          // cast will cause issues after the PoW refactor goes in
          AionBlockchainImpl chain = (AionBlockchainImpl) this.ac.getAionHub().getBlockchain();
  
          AionTxInfo info = chain.getTransactionInfoLite(transactionHash, blockHash);
          if (info == null) {
              return new RpcMsg(JSONObject.NULL);
          }
  
          Block block = blockCache.get(ByteArrayWrapper.wrap(blockHash));
  
          AionTransaction t = block.getTransactionsList().get(info.getIndex());
          if (Arrays.compare(t.getTransactionHash(), transactionHash) != 0) {
              LOG.error("INCONSISTENT STATE: transaction info's transaction index is wrong.");
              return new RpcMsg(null, RpcError.INTERNAL_ERROR, "Database Error");
          }
          info.setTransaction(t);
  
          return new RpcMsg((new TxRecpt(block, info, 0L, true)).toJson());
      }
  
      public RpcMsg ops_getTransactionReceiptListByBlockHash(Object _params) {
          String _blockHash;
          if (_params instanceof JSONArray) {
              _blockHash = ((JSONArray) _params).get(0) + "";
          } else if (_params instanceof JSONObject) {
              _blockHash = ((JSONObject) _params).get("blockHash") + "";
          } else {
              return new RpcMsg(null, RpcError.INVALID_PARAMS, "Invalid parameters");
          }
  
          byte[] blockHash = StringUtils.StringHexToByteArray(_blockHash);
  
          if (blockHash.length != 32) {
              return new RpcMsg(null, RpcError.INVALID_PARAMS, "Invalid parameters");
          }
  
          // ok to getUnchecked() since the load() implementation does not throw checked exceptions
          Block b;
          try {
              b = blockCache.get(ByteArrayWrapper.wrap(blockHash));
          } catch (Exception e) {
              // Catch errors if send an incorrect tx hash
              return new RpcMsg(
                      null,
                      RpcError.INVALID_REQUEST,
                      "Invalid Request " + e + " " + e.getMessage() != null ? e.getMessage() : "");
          }
  
          // cast will cause issues after the PoW refactor goes in
          AionBlockchainImpl chain = (AionBlockchainImpl) this.ac.getAionHub().getBlockchain();
  
          Function<AionTransaction, JSONObject> extractTxReceipt =
                  t -> {
                      AionTxInfo info =
                              chain.getTransactionInfoLite(t.getTransactionHash(), b.getHash());
                      info.setTransaction(t);
                      return ((new TxRecpt(b, info, 0L, true)).toJson());
                  };
  
          List<JSONObject> receipts;
          // use the fork-join pool to parallelize receipt retrieval if necessary
          int PARALLELIZE_RECEIPT_COUNT = 20;
          if (b.getTransactionsList().size() > PARALLELIZE_RECEIPT_COUNT) {
              receipts =
                      b.getTransactionsList()
                              .parallelStream()
                              .map(extractTxReceipt)
                              .collect(toList());
          } else {
              receipts = b.getTransactionsList().stream().map(extractTxReceipt).collect(toList());
          }
  
          return new RpcMsg(new JSONArray(receipts));
      }
  
      /* -------------------------------------------------------------------------
       * stratum pool
       */
  
      public RpcMsg stratum_getinfo() {
          JSONObject obj = new JSONObject();
  
          obj.put("balance", 0);
          obj.put("blocks", 0);
          obj.put("connections", peerCount());
          obj.put("proxy", "");
          obj.put("generate", true);
          obj.put("genproclimit", 100);
          obj.put("difficulty", 0);
  
          return new RpcMsg(obj);
      }
  
      public RpcMsg stratum_getwork() {
          if (isSeedMode) {
              return new RpcMsg(null, RpcError.NOT_ALLOWED, "SeedNodeIsOpened");
          }
  
          BlockContext bestBlock;
          try {
              bestBlock = getBlockTemplate();
              if (bestBlock == null) {
                  LOG.debug("Couldn't create a mining block template");
                  
                  JSONObject obj = new JSONObject();
                  obj.put("message", "no mining block template ");
                  obj.put("code", -1);
                  return new RpcMsg(obj);
              }
          } catch (Exception e) {
              LOG.error("get mining block template failed!", e);
  
              JSONObject obj = new JSONObject();
              obj.put("message", "failed: " + e.toString());
              obj.put("code", -1);
              return new RpcMsg(obj);
          }
          JSONObject obj = new JSONObject();
          obj.put("previousblockhash", toHexString(bestBlock.block.getParentHash()));
          obj.put("height", bestBlock.block.getNumber());
          obj.put("target", toHexString(bestBlock.block.getHeader().getPowBoundary()));
          obj.put("headerHash", toHexString(bestBlock.block.getHeader().getMineHash()));
          obj.put("blockBaseReward", toHexString(bestBlock.baseBlockReward.toByteArray()));
          obj.put("blockTxFee", toHexString(bestBlock.transactionFee.toByteArray()));
  
          return new RpcMsg(obj);
      }
  
      public RpcMsg stratum_dumpprivkey() {
          return new RpcMsg("");
      }
  
      public RpcMsg stratum_validateaddress(Object _params) {
          /*
           * "isvalid" : true|false, (boolean) If the address is valid or not.
           * "address", (string) The aion address validated to ensure address is valid
           * address "ismine" : true|false, (boolean) If the address is contained in the keystore.
           */
          String _address;
          if (_params instanceof JSONArray) {
              _address = ((JSONArray) _params).get(0) + "";
          } else if (_params instanceof JSONObject) {
              _address = ((JSONObject) _params).get("address") + "";
          } else {
              return new RpcMsg(null, RpcError.INVALID_PARAMS, "Invalid parameters");
          }
  
          JSONObject obj = new JSONObject();
  
          obj.put("isvalid", StringUtils.isValidAddress(_address));
          obj.put("address", _address + "");
          obj.put("ismine", Keystore.exist(_address));
          return new RpcMsg(obj);
      }
  
      public RpcMsg stratum_getdifficulty() {
          /*
           * Return the highest known difficulty
           */
          return new RpcMsg(getBestMiningBlock().getDifficultyBI().toString(16));
      }
  
      public RpcMsg stratum_getmininginfo() {
          Block bestBlock =  getBestMiningBlock();
  
          JSONObject obj = new JSONObject();
          obj.put("blocks", bestBlock.getNumber());
          obj.put("currentblocksize", bestBlock.size());
          obj.put("currentblocktx", bestBlock.getTransactionsList().size());
          obj.put("difficulty", bestBlock.getDifficultyBI().toString(16));
          obj.put("testnet", true);
  
          return new RpcMsg(obj);
      }
  
      public RpcMsg stratum_submitblock(Object _params) {
          Object nce;
          Object soln;
          Object hdrHash;
          if (_params instanceof JSONArray) {
              nce = ((JSONArray) _params).opt(0);
              soln = ((JSONArray) _params).opt(1);
              hdrHash = ((JSONArray) _params).opt(2);
          } else {
              return new RpcMsg(null, RpcError.INVALID_PARAMS, "Invalid parameters");
          }
  
          JSONObject obj = new JSONObject();
  
          if (!JSONObject.NULL.equals(nce)
                  && !JSONObject.NULL.equals(soln)
                  && !JSONObject.NULL.equals(hdrHash)) {
              // Grab copy of best block
              MiningBlock bestBlock = ac.getBlockchain().getCachingMiningBlockTemplate(hexStringToBytes((String) hdrHash));
              if (bestBlock != null && bestBlock.notSealed()) {
                  try {
                      bestBlock.seal(hexStringToBytes(nce + ""), hexStringToBytes(soln + ""));
                  } catch (Exception e) {
                      LOG.error("Finalize block failed!", e);
                      obj.put("message", "failed: " + e.toString());
                      obj.put("code", -1);
                      return new RpcMsg(obj);
                  }
  
                  // Directly submit to chain for new due to delays using event, explore event
                  // submission again
                  ImportResult importResult = AionImpl.inst().addNewBlock(bestBlock);
                  if (importResult.isSuccessful()) {
                      LOG.info(
                              "mining block submitted via api <num={}, hash={}, diff={}, tx={}>",
                              bestBlock.getNumber(),
                              bestBlock.getShortHash(), // LogUtil.toHexF8(newBlock.getHash()),
                              bestBlock.getHeader().getDifficultyBI().toString(),
                              bestBlock.getTransactionsList().size());
                  } else {
                      LOG.info(
                              "Unable to submit mining block via api <num={}, hash={}, diff={}, tx={}>",
                              bestBlock.getNumber(),
                              bestBlock.getShortHash(), // LogUtil.toHexF8(newBlock.getHash()),
                              bestBlock.getHeader().getDifficultyBI().toString(),
                              bestBlock.getTransactionsList().size());
                  }
              }
  
              // TODO: Simplified response for now, need to provide better feedback to caller in next
              // update
              obj.put("result", true);
          } else {
              obj.put("message", "success");
              obj.put("code", -1);
          }
  
          return new RpcMsg(obj);
      }
  
      public RpcMsg stratum_getHeaderByBlockNumber(Object _params) {
          Object _blockNum;
          if (_params instanceof JSONArray) {
              _blockNum = ((JSONArray) _params).opt(0);
          } else {
              return new RpcMsg(null, RpcError.INVALID_PARAMS, "Invalid parameters");
          }
  
          JSONObject obj = new JSONObject();
  
          if (!JSONObject.NULL.equals(_blockNum)) {
              String bnStr = _blockNum + "";
              try {
                  int bnInt = Integer.decode(bnStr);
                  Block block =  getBlockRaw(bnInt);
                  if (block != null && isPowBlock(block)) {
                      MiningBlockHeader header = (MiningBlockHeader) block.getHeader();
                      obj.put("code", 0); // 0 = success
                      obj.put("nonce", toHexString(header.getNonce()));
                      obj.put("solution", toHexString(header.getSolution()));
                      obj.put("headerHash", toHexString(header.getMineHash()));
                      obj.putOpt("blockHeader", header.toJSON());
                  } else {
                      obj.put("message", "Fail - Unable to find block" + bnStr);
                      obj.put("code", -2);
                  }
              } catch (Exception e) {
                  obj.put("message", bnStr + " must be an integer value");
                  obj.put("code", -3);
              }
          } else {
              obj.put("message", "Missing block number");
              obj.put("code", -1);
          }
  
          return new RpcMsg(obj);
      }
  
      // Always gets the latest N PoW blocks and N PoS blocks
      private class MinerStatsView {
  
          LinkedList<byte[]> hashQueue; // more precisely a dequeue
          Map<byte[], Block> blocks;
          private JSONObject response;
          private int qSize;
          private byte[] miner;
  
          MinerStatsView(MinerStatsView cv) {
              hashQueue = new LinkedList<>(cv.hashQueue);
              blocks = new HashMap<>(cv.blocks);
              response = new JSONObject(cv.response, JSONObject.getNames(cv.response));
              qSize = cv.qSize;
              miner = cv.miner;
          }
  
          MinerStatsView(int _qSize, byte[] _miner) {
              hashQueue = new LinkedList<>();
              blocks = new HashMap<>();
              response = new JSONObject();
              qSize = _qSize;
              miner = _miner;
          }
  
          private JSONObject buildResponse() {
              BigInteger lastDifficulty = BigInteger.ZERO;
              long blkTimeAccumulator = 0;
              long minedCount = 0L;
  
              int minedByMiner = 0;
  
              double minerHashrateShare = 0;
              BigDecimal minerHashrate = BigDecimal.ZERO;
              BigDecimal networkHashrate = BigDecimal.ZERO;
  
              int blkTimesAccumulated = 0;
              Long lastBlkTimestamp = null;
              Block b = null;
  
              try {
                  // index 0 = latest block
                  int i = 0;
                  ListIterator li = hashQueue.listIterator(0);
                  while (li.hasNext()) {
                      byte[] hash = (byte[]) li.next();
                      b = blocks.get(hash);
  
                      // only accumulate block times over the last STRATUM_BLKTIME_INCLUDED_COUNT blocks
                      if (i <= STRATUM_BLKTIME_INCLUDED_COUNT) {
                          if (b.getHeader().getSealType().equals(Seal.PROOF_OF_STAKE) && lastBlkTimestamp != null) {
                              //                            System.out.println("blocktime for [" +
                              // b.getNumber() + "] = " + (lastBlkTimestamp - b.getTimestamp()));
                              blkTimeAccumulator += lastBlkTimestamp - b.getTimestamp(); // delta = PoW block timestamp - PoS block timestamp
                              blkTimesAccumulated++;
                          }
  
                          if (b.getHeader().getSealType().equals(Seal.PROOF_OF_WORK)) {
                              lastBlkTimestamp = b.getTimestamp();
                          }
                      }
  
                      if (b.getHeader().getSealType().equals(Seal.PROOF_OF_WORK)) {
                          // Note latest PoW block's difficulty
                          if (lastDifficulty == BigInteger.ZERO) {
                              lastDifficulty = b.getDifficultyBI();
                          }
                          
                          // Count PoW blocks mined by this miner
                          if (Arrays.equals(b.getCoinbase().toByteArray(), miner)) {
                              minedByMiner++;
                          }
  
                          // Increment PoW index
                          i++;
                      }
                  }
  
                  double blkTime = 0L;
                  if (blkTimesAccumulated > 0) {
                      blkTime = blkTimeAccumulator / (double) blkTimesAccumulated;
                  }
  
                  if (blkTime > 0) {
                      networkHashrate =
                              (new BigDecimal(lastDifficulty))
                                      .divide(BigDecimal.valueOf(blkTime), 4, RoundingMode.HALF_UP);
                  }
  
                  if (i > 0) {
                      minerHashrateShare = minedByMiner / (double) i;
                  }
  
                  minerHashrate = BigDecimal.valueOf(minerHashrateShare).multiply(networkHashrate);
  
              } catch (Exception e) {
                  LOG.error("failed to compute miner metrics", e);
              }
  
              JSONObject o = new JSONObject();
              o.put("networkHashrate", networkHashrate.toString());
              o.put("minerHashrate", minerHashrate.toString());
              o.put("minerHashrateShare", minerHashrateShare);
              return o;
          }
  
          MinerStatsView update() {
              // get the latest head
              // blk is a temporary variable that will be used to iterate through blocks on the chain
              // therefore we use the abstract block type
              Block blk = getBestMiningBlock(); // getBestMiningBlock so that we do not need to check
                                                // check the block type
  
              if (blk == null) {
                  return this;
              }
  
              if (Arrays.equals(hashQueue.peekFirst(), blk.getHash())) {
                  return this; // nothing to do
              }
  
              // evict data as necessary
              LinkedList<Map.Entry<byte[], Block>> tempStack = new LinkedList<>();
              tempStack.push(Map.entry(blk.getHash(), blk));
              int itr = 1; // deliberately 1, since we've already added the 0th element to the stack
  
              /*
              if (hashQueue.peekFirst() != null) {
                  System.out.println("[" + 0 + "]: " + StringUtils.toJsonHex(hashQueue.peekFirst()) + " - " + blocks.get(hashQueue.peekFirst()).getNumber());
                  System.out.println("----------------------------------------------------------");
                  System.out.println("isParentHashMatch? " + FastByteComparisons.equal(hashQueue.peekFirst(), blk.getParentHash()));
                  System.out.println("blk.getNumber() " + blk.getNumber());
              }
              System.out.println("blkNum: " + blk.getNumber() +
                      " parentHash: " + StringUtils.toJsonHex(blk.getParentHash()) +
                      " blkHash: " + StringUtils.toJsonHex(blk.getHash()));
              */
  
              while (!Arrays.equals(hashQueue.peekFirst(), blk.getParentHash())
                      && itr < (qSize * 2) // Update (qSize) PoW blocks and (qSize) PoS blocks
                      && blk.getNumber() > 2) {
  
                  blk = getBlockByHash(blk.getParentHash());
                  tempStack.push(Map.entry(blk.getHash(), blk));
                  itr++;
                  /*
                  System.out.println("blkNum: " + blk.getNumber() +
                          " parentHash: " + StringUtils.toJsonHex(blk.getParentHash()) +
                          " blkHash: " + StringUtils.toJsonHex(blk.getHash()));
                  */
              }
  
              // evict out the right number of elements first
              for (int i = 0; i < tempStack.size(); i++) {
                  byte[] tailHash = hashQueue.pollLast();
                  if (tailHash != null) {
                      blocks.remove(tailHash);
                  } else {
                      break;
                  }
              }
  
              // empty out the stack into the queue
              while (!tempStack.isEmpty()) {
                  // add to the queue
                  Entry<byte[], Block> element = tempStack.pop();
                  byte[] hash = element.getKey();
                  Block blkObj = element.getValue();
  
                  hashQueue.push(hash);
                  blocks.put(hash, blkObj);
              }
  
              /*
              System.out.println("[" + 0 + "]: " + StringUtils.toJsonHex(hashQueue.peekFirst()) + " - " + blocks.get(hashQueue.peekFirst()).getNumber());
              System.out.println("----------------------------------------------------------");
              for (int i = hashQueue.size() - 1; i >= 0; i--) {
                  System.out.println("[" + i + "]: " + StringUtils.toJsonHex(hashQueue.get(i)) + " - " + blocks.get(hashQueue.get(i)).getNumber());
              }
              */
              this.response = buildResponse();
  
              return this;
          }
  
          JSONObject getResponse() {
              return response;
          }
      }
  
      public RpcMsg stratum_getMinerStats(Object _params) {
          String _address;
          if (_params instanceof JSONArray) {
              _address = ((JSONArray) _params).get(0) + "";
          } else if (_params instanceof JSONObject) {
              _address = ((JSONObject) _params).get("address") + "";
          } else {
              return new RpcMsg(null, RpcError.INVALID_PARAMS, "Invalid parameters");
          }
  
          try {
              MinerStatsView v = MinerStats.get(_address);
              return new RpcMsg(v.getResponse());
          } catch (Exception e) {
              LOG.error("<rpc-server - cannot get cached response for stratum_getMinerStats: ", e);
              return new RpcMsg(null, RpcError.EXECUTION_ERROR, "Cached response retrieve failed.");
          }
      }
  
      private Block getBlockByBN(long bn) {
          if (bn == BEST_PENDING_BLOCK) {
              return ac.getBlockchain().getBestBlockWithInfo();
          } else {
              return this.ac.getBlockchain().getBlockByNumber(bn);
          }
      }
  
      // Note: If return is null, caller sometimes assumes no blockNumber was passed in
  
      private Long parseBnOrId(String _bnOrId) {
          if (_bnOrId == null) {
              return null;
          }
  
          try {
              if ("earliest".equalsIgnoreCase(_bnOrId)) {
                  return 0L;
              } else if ("latest".equalsIgnoreCase(_bnOrId)) {
                  return getBestBlock().getNumber();
              } else if ("pending".equalsIgnoreCase(_bnOrId)) {
                  return BEST_PENDING_BLOCK;
              } else {
  
                  Long ret;
  
                  if (_bnOrId.startsWith("0x")) {
                      ret = StringUtils.StringHexToBigInteger(_bnOrId).longValue();
                  } else {
                      ret = Long.parseLong(_bnOrId);
                  }
                  if (ret < 0) {
                      LOG.debug("block number cannot be negative" + _bnOrId);
                      return null;
                  } else {
                      return ret;
                  }
              }
          } catch (Exception e) {
              LOG.debug("err on parsing block number #" + _bnOrId);
              return null;
          }
      }
  
  
      private RpcMsg rpcBlockDetailsFromBlock(Block block){
          BigInteger blkReward = ac.getBlockchain().calculateBlockRewards(block.getHeader().getNumber());
          BigInteger totalDiff =
              this.ac.getAionHub().getTotalDifficultyForHash(block.getHash());
  
          List<AionTxInfo> txInfoList = new ArrayList<>();
          for (AionTransaction transaction: block.getTransactionsList()){
              AionTxInfo txInfo = this.ac.getAionHub()
                  .getBlockchain()
                  .getTransactionInfo(transaction.getTransactionHash());
              txInfoList.add(txInfo);
          }
          Block previousBlock = this.ac.getBlockchain().getBlockByHash(block.getParentHash());
          // get the parent block
  
          Long previousTimestamp;
  
          if (previousBlock == null){
              previousTimestamp = null;
          }
          else {
              previousTimestamp = previousBlock.getTimestamp();
          }
  
          return new RpcMsg(
              Blk.aionBlockDetailsToJson(block, txInfoList, previousTimestamp, totalDiff, blkReward
              ));
      }
  
      private  boolean isPowBlock(Block block){
          return block.getHeader().getSealType().equals(Seal.PROOF_OF_WORK);
      }
  
      private boolean isPosBlock(Block block){
          return block.getHeader().getSealType().equals(Seal.PROOF_OF_STAKE);
      }
  
      public void shutdown() {
          destroyCaches();
          if (isFilterEnabled) {
              shutDownES();
          }
      }
  }
  