@Override
  public void catchUp(List<BID> inventory, int limit, boolean headers, final TrunkListener listener)
      throws BCSAPIException {
    log.trace("catchUp");
    ConnectorMessage m;
    try (ConnectorSession session = connection.createSession()) {
      ConnectorProducer transactionRequestProducer =
          session.createProducer(session.createQueue("catchUpRequest"));

      m = session.createMessage();
      BCSAPIMessage.CatchUpRequest.Builder builder = BCSAPIMessage.CatchUpRequest.newBuilder();
      builder.setLimit(limit);
      builder.setHeaders(true);
      for (BID hash : inventory) {
        builder.addInventory(ByteString.copyFrom(hash.unsafeGetArray()));
      }
      m.setPayload(builder.build().toByteArray());
      byte[] response = synchronousRequest(session, transactionRequestProducer, m);
      if (response != null) {
        BCSAPIMessage.TrunkUpdate blockMessage = BCSAPIMessage.TrunkUpdate.parseFrom(response);
        List<APIBlock> blockList = new ArrayList<>();
        for (BCSAPIMessage.BLK b : blockMessage.getAddedList()) {
          blockList.add(APIBlock.fromProtobuf(b));
        }
        listener.trunkUpdate(blockList);
      }
    } catch (ConnectorException | HyperLedgerException | InvalidProtocolBufferException e) {
      throw new BCSAPIException(e);
    }
  }
  @Override
  public APIBlockIdList getBlockIds(BID blockId, int count) throws BCSAPIException {
    try (ConnectorSession session = connection.createSession()) {
      log.trace("get " + count + " block ids from " + blockId);

      ConnectorProducer blockIdsRequestProducer =
          session.createProducer(session.createQueue("blockIdsRequest"));

      ConnectorMessage m = session.createMessage();
      BCSAPIMessage.BLKIDSREQ.Builder builder = BCSAPIMessage.BLKIDSREQ.newBuilder();
      if (blockId != null) {
        builder.setBlockHash(ByteString.copyFrom(blockId.unsafeGetArray()));
      }
      if (count <= 0) count = 20;
      builder.setCount(count);
      m.setPayload(builder.build().toByteArray());
      byte[] response = synchronousRequest(session, blockIdsRequestProducer, m);
      if (response != null) {
        BCSAPIMessage.BLKIDS message = BCSAPIMessage.BLKIDS.parseFrom(response);
        List<ByteString> blockIdsList = message.getBlockIdsList();
        List<BID> blockIds =
            blockIdsList.stream().map(bs -> new BID(bs.toByteArray())).collect(Collectors.toList());
        return new APIBlockIdList(
            blockIds,
            message.getHeight(),
            message.hasPreviousBlockId()
                ? new BID(message.getPreviousBlockId().toByteArray())
                : null);
      }
    } catch (ConnectorException | InvalidProtocolBufferException e) {
      throw new BCSAPIException(e);
    }

    return null;
  }
  @Override
  public List<APITransaction> getInputTransactions(TID txId) throws BCSAPIException {
    log.trace("get input transactions " + txId);
    ConnectorMessage m;
    try (ConnectorSession session = connection.createSession()) {
      ConnectorProducer transactionRequestProducer =
          session.createProducer(session.createQueue("inputTransactionsRequest"));

      m = session.createMessage();
      BCSAPIMessage.Hash.Builder builder = BCSAPIMessage.Hash.newBuilder();
      builder.addHash(ByteString.copyFrom(txId.unsafeGetArray()));
      m.setPayload(builder.build().toByteArray());
      byte[] response = synchronousRequest(session, transactionRequestProducer, m);
      if (response != null) {
        List<BCSAPIMessage.OPTIONAL_TX> txsList =
            BCSAPIMessage.TXS.parseFrom(response).getTxsList();
        List<APITransaction> txs = new ArrayList<>(txsList.size());
        for (BCSAPIMessage.OPTIONAL_TX tx : txsList) {
          if (tx.getIsNull()) {
            txs.add(null);
          } else {
            txs.add(APITransaction.fromProtobuf(tx.getTransaction()));
          }
        }
        return txs;
      }
    } catch (ConnectorException | HyperLedgerException | InvalidProtocolBufferException e) {
      throw new BCSAPIException(e);
    }

    return null;
  }
  private byte[] synchronousRequest(
      ConnectorSession session, ConnectorProducer producer, ConnectorMessage m)
      throws BCSAPIException {
    ConnectorTemporaryQueue answerQueue = null;
    try {
      answerQueue = session.createTemporaryQueue();
      m.setReplyTo(answerQueue);

      try (ConnectorConsumer consumer = session.createConsumer(answerQueue)) {
        producer.send(m);
        ConnectorMessage reply = consumer.receive(clientSettings.getTimeout());
        if (reply == null) {
          throw new BCSAPIException("timeout");
        }
        return reply.getPayload();
      }
    } catch (ConnectorException e) {
      throw new BCSAPIException(e);
    } finally {
      try {
        if (answerQueue != null) {
          answerQueue.delete();
        }
      } catch (ConnectorException e) {
      }
    }
  }
 @SuppressWarnings("unchecked")
 private <T> void addTopicListener(
     String topic, Object inner, ByteArrayConverter<T> converter, TypedListener<T> listener)
     throws ConnectorException {
   synchronized (messageDispatcher) {
     MessageDispatcher<T> dispatcher = messageDispatcher.get(topic);
     if (dispatcher == null) {
       ConnectorSession session = connection.createSession();
       ConnectorConsumer consumer = session.createConsumer(session.createTopic(topic));
       messageDispatcher.put(topic, dispatcher = new MessageDispatcher<T>(consumer, converter));
     }
     dispatcher.addListener(inner, listener);
   }
 }
  @Override
  public int getChainHeight() throws BCSAPIException {
    try (ConnectorSession session = connection.createSession()) {
      log.trace("get chain height");

      ConnectorProducer heightRequestProducer =
          session.createProducer(session.createQueue("chainHeightRequest"));

      ConnectorMessage m = session.createMessage();
      byte[] response = synchronousRequest(session, heightRequestProducer, m);
      if (response != null) {
        return BCSAPIMessage.HEIGHT.parseFrom(response).getHeight();
      }
    } catch (ConnectorException | InvalidProtocolBufferException e) {
      throw new BCSAPIException(e);
    }
    return -1;
  }
  @Override
  public void sendBlock(Block block) throws BCSAPIException {
    try (ConnectorSession session = connection.createSession()) {
      log.trace("send block " + block.getID());
      ConnectorProducer blockProducer = session.createProducer(session.createTopic("newBlock"));

      ConnectorMessage m = session.createMessage();
      m.setPayload(block.toBCSAPIMessage().toByteArray());
      byte[] reply = synchronousRequest(session, blockProducer, m);
      if (reply != null) {
        try {
          BCSAPIMessage.ExceptionMessage em = BCSAPIMessage.ExceptionMessage.parseFrom(reply);
          throw new BCSAPIException(em.getMessage(0));
        } catch (InvalidProtocolBufferException e) {
          throw new BCSAPIException("Invalid response", e);
        }
      }
    } catch (ConnectorException e) {
      throw new BCSAPIException(e);
    }
  }
  @Override
  public APIHeader mine(Address address) throws BCSAPIException {
    try (ConnectorSession session = connection.createSession()) {
      log.trace("mine to " + address);

      ConnectorProducer producer = session.createProducer(session.createTopic("mine"));

      ConnectorMessage m = session.createMessage();
      BCSAPIMessage.Script.Builder builder =
          BCSAPIMessage.Script.newBuilder()
              .setScript(ByteString.copyFrom(address.getAddressScript().toByteArray()));
      m.setPayload(builder.build().toByteArray());
      byte[] reply = synchronousRequest(session, producer, m);
      if (reply != null) {
        return APIHeader.fromProtobuf(BCSAPIMessage.BLK.parseFrom(reply));
      }
    } catch (ConnectorException | InvalidProtocolBufferException | HyperLedgerException e) {
      throw new BCSAPIException(e);
    }
    return null;
  }
  @Override
  public APIHeader getBlockHeader(BID hash) throws BCSAPIException {
    try (ConnectorSession session = connection.createSession()) {
      log.trace("get block header" + hash);

      ConnectorProducer blockHeaderRequestProducer =
          session.createProducer(session.createQueue("headerRequest"));

      ConnectorMessage m = session.createMessage();
      BCSAPIMessage.Hash.Builder builder = BCSAPIMessage.Hash.newBuilder();
      builder.addHash(ByteString.copyFrom(hash.unsafeGetArray()));
      m.setPayload(builder.build().toByteArray());
      byte[] response = synchronousRequest(session, blockHeaderRequestProducer, m);
      if (response != null) {
        return APIHeader.fromProtobuf(BCSAPIMessage.BLK.parseFrom(response));
      }
    } catch (ConnectorException | InvalidProtocolBufferException e) {
      throw new BCSAPIException(e);
    }

    return null;
  }
  private BCSAPIMessage.Ping handshake(long nonce) throws BCSAPIException {
    try (ConnectorSession session = connection.createSession()) {
      log.trace("ping " + nonce);

      ConnectorMessage m = session.createMessage();
      BCSAPIMessage.Ping.Builder builder = BCSAPIMessage.Ping.newBuilder();
      builder.setNonce(nonce);
      builder.setClientVersion(getClientVersion());
      m.setPayload(builder.build().toByteArray());
      ConnectorProducer pingProducer = session.createProducer(session.createQueue("ping"));
      byte[] response = synchronousRequest(session, pingProducer, m);
      if (response != null) {
        BCSAPIMessage.Ping echo = BCSAPIMessage.Ping.parseFrom(response);
        if (echo.getNonce() != nonce) {
          throw new BCSAPIException("Incorrect echo nonce from ping");
        }
        return echo;
      }
      throw new BCSAPIException("no reply");
    } catch (ConnectorException | InvalidProtocolBufferException e) {
      throw new BCSAPIException(e);
    }
  }
  @Override
  public APITransaction getTransaction(TID hash) throws BCSAPIException {
    log.trace("get transaction " + hash);
    ConnectorMessage m;
    try (ConnectorSession session = connection.createSession()) {
      ConnectorProducer transactionRequestProducer =
          session.createProducer(session.createQueue("transactionRequest"));

      m = session.createMessage();
      BCSAPIMessage.Hash.Builder builder = BCSAPIMessage.Hash.newBuilder();
      builder.addHash(ByteString.copyFrom(hash.unsafeGetArray()));
      m.setPayload(builder.build().toByteArray());
      byte[] response = synchronousRequest(session, transactionRequestProducer, m);
      if (response != null) {
        APITransaction t;
        t = APITransaction.fromProtobuf(BCSAPIMessage.TX.parseFrom(response));
        return t;
      }
    } catch (ConnectorException | HyperLedgerException | InvalidProtocolBufferException e) {
      throw new BCSAPIException(e);
    }

    return null;
  }
  @Override
  public void spendingTransactions(List<TID> tids, final TransactionListener listener)
      throws BCSAPIException {
    try (ConnectorSession session = connection.createSession()) {
      ConnectorMessage m = session.createMessage();

      ConnectorProducer scanAccountProducer =
          session.createProducer(session.createQueue("spendingTransactions"));
      BCSAPIMessage.Hash.Builder builder = BCSAPIMessage.Hash.newBuilder();
      for (TID tid : tids) {
        builder.addHash(ByteString.copyFrom(tid.unsafeGetArray()));
      }
      m.setPayload(builder.build().toByteArray());

      final ConnectorTemporaryQueue answerQueue = session.createTemporaryQueue();
      final ConnectorConsumer consumer = session.createConsumer(answerQueue);
      m.setReplyTo(answerQueue);
      final Semaphore ready = new Semaphore(0);
      consumer.setMessageListener(
          message -> {
            try {
              byte[] body = message.getPayload();
              if (body != null) {
                APITransaction t = APITransaction.fromProtobuf(BCSAPIMessage.TX.parseFrom(body));
                listener.process(t);
              } else {
                consumer.close();
                answerQueue.delete();
                ready.release();
              }
            } catch (ConnectorException | HyperLedgerException | InvalidProtocolBufferException e) {
              log.error("Malformed message received for spendingt ransactions request", e);
            }
          });

      scanAccountProducer.send(m);
      ready.acquireUninterruptibly();
    } catch (ConnectorException e) {
      throw new BCSAPIException(e);
    }
  }
  private void scanRequest(
      Collection<Script> match, final TransactionListener listener, String requestQueue)
      throws BCSAPIException {
    try (ConnectorSession session = connection.createSession()) {
      ConnectorMessage m = session.createMessage();

      ConnectorProducer exactMatchProducer =
          session.createProducer(session.createQueue(requestQueue));
      BCSAPIMessage.ExactMatchRequest.Builder builder =
          BCSAPIMessage.ExactMatchRequest.newBuilder();
      for (Script d : match) {
        builder.addMatch(ByteString.copyFrom(d.toByteArray()));
      }
      m.setPayload(builder.build().toByteArray());
      final ConnectorTemporaryQueue answerQueue = session.createTemporaryQueue();
      final ConnectorConsumer consumer = session.createConsumer(answerQueue);
      m.setReplyTo(answerQueue);
      final Semaphore ready = new Semaphore(0);
      consumer.setMessageListener(
          message -> {
            try {
              byte[] body = message.getPayload();
              if (body != null) {
                APITransaction t = APITransaction.fromProtobuf(BCSAPIMessage.TX.parseFrom(body));
                listener.process(t);
              } else {
                consumer.close();
                answerQueue.delete();
                ready.release();
              }
            } catch (ConnectorException | HyperLedgerException | InvalidProtocolBufferException e) {
              log.error("Malformed message received for scan matching transactions", e);
            }
          });

      exactMatchProducer.send(m);
      ready.acquireUninterruptibly();
    } catch (ConnectorException e) {
      throw new BCSAPIException(e);
    }
  }
  private void scanRequest(
      MasterPublicKey master, int lookAhead, final TransactionListener listener, String request)
      throws BCSAPIException {
    try (ConnectorSession session = connection.createSession()) {
      ConnectorMessage m = session.createMessage();

      ConnectorProducer scanAccountProducer = session.createProducer(session.createQueue(request));
      BCSAPIMessage.AccountRequest.Builder builder = BCSAPIMessage.AccountRequest.newBuilder();
      builder.setPublicKey(master.serialize(true));
      builder.setLookAhead(lookAhead);
      m.setPayload(builder.build().toByteArray());

      final ConnectorTemporaryQueue answerQueue = session.createTemporaryQueue();
      final ConnectorConsumer consumer = session.createConsumer(answerQueue);
      m.setReplyTo(answerQueue);
      final Semaphore ready = new Semaphore(0);
      consumer.setMessageListener(
          message -> {
            try {
              byte[] body = message.getPayload();
              if (body != null) {
                APITransaction t = APITransaction.fromProtobuf(BCSAPIMessage.TX.parseFrom(body));
                listener.process(t);
              } else {
                consumer.close();
                answerQueue.delete();
                ready.release();
              }
            } catch (ConnectorException | HyperLedgerException | InvalidProtocolBufferException e) {
              log.error("Malformed message received for account scan transactions", e);
            }
          });

      scanAccountProducer.send(m);
      ready.acquireUninterruptibly();
    } catch (ConnectorException e) {
      throw new BCSAPIException(e);
    }
  }