docs: public CompletableFuture<TransactionEvent> sendTransaction(Collection<? extends ProposalResponse> proposalResponses,
                                                                 TransactionOptions transactionOptions) {
          try {
  
              if (null == transactionOptions) {
                  throw new InvalidArgumentException("Parameter transactionOptions can't be null");
              }
              checkChannelState();
              User userContext = transactionOptions.userContext != null ? transactionOptions.userContext : client.getUserContext();
              userContextCheck(userContext);
              if (null == proposalResponses) {
                  throw new InvalidArgumentException("sendTransaction proposalResponses was null");
              }
  
              List<Orderer> orderers = transactionOptions.orderers != null ? transactionOptions.orderers :
                      new ArrayList<>(getOrderers());
  
              // make certain we have our own copy
              final List<Orderer> shuffeledOrderers = new ArrayList<>(orderers);
  
              if (transactionOptions.shuffleOrders) {
                  Collections.shuffle(shuffeledOrderers);
              }
  
              if (config.getProposalConsistencyValidation()) {
                  HashSet<ProposalResponse> invalid = new HashSet<>();
                  int consistencyGroups = SDKUtils.getProposalConsistencySets(proposalResponses, invalid).size();
  
                  if (consistencyGroups != 1 || !invalid.isEmpty()) {
                      throw new IllegalArgumentException(format(
                              "The proposal responses have %d inconsistent groups with %d that are invalid."
                                      + " Expected all to be consistent and none to be invalid.",
                              consistencyGroups, invalid.size()));
  
                  }
  
              }
  
              List<FabricProposalResponse.Endorsement> ed = new LinkedList<>();
              FabricProposal.Proposal proposal = null;
              ByteString proposalResponsePayload = null;
              String proposalTransactionID = null;
              TransactionContext transactionContext = null;
  
              for (ProposalResponse sdkProposalResponse : proposalResponses) {
                  ed.add(sdkProposalResponse.getProposalResponse().getEndorsement());
                  if (proposal == null) {
                      proposal = sdkProposalResponse.getProposal();
                      proposalTransactionID = sdkProposalResponse.getTransactionID();
                      if (proposalTransactionID == null) {
                          throw new InvalidArgumentException("Proposals with missing transaction ID");
                      }
                      proposalResponsePayload = sdkProposalResponse.getProposalResponse().getPayload();
                      if (proposalResponsePayload == null) {
                          throw new InvalidArgumentException("Proposals with missing payload.");
                      }
                      transactionContext = sdkProposalResponse.getTransactionContext();
                      if (transactionContext == null) {
                          throw new InvalidArgumentException("Proposals with missing transaction context.");
                      }
                  } else {
                      final String transactionID = sdkProposalResponse.getTransactionID();
                      if (transactionID == null) {
                          throw new InvalidArgumentException("Proposals with missing transaction id.");
                      }
                      if (!proposalTransactionID.equals(transactionID)) {
                          throw new InvalidArgumentException(format("Proposals with different transaction IDs %s,  and %s", proposalTransactionID, transactionID));
                      }
                  }
              }
  
              TransactionBuilder transactionBuilder = TransactionBuilder.newBuilder();
  
              Payload transactionPayload = transactionBuilder
                      .chaincodeProposal(proposal)
                      .endorsements(ed)
                      .proposalResponsePayload(proposalResponsePayload).build();
  
              Envelope transactionEnvelope = createTransactionEnvelope(transactionPayload, transactionContext);
  
              NOfEvents nOfEvents = transactionOptions.nOfEvents;
  
              if (nOfEvents == null) {
                  nOfEvents = NOfEvents.createNofEvents();
                  Collection<Peer> eventingPeers = getEventingPeers();
                  boolean anyAdded = false;
                  if (!eventingPeers.isEmpty()) {
                      anyAdded = true;
                      nOfEvents.addPeers(eventingPeers);
                  }
  
                  if (!anyAdded) {
                      nOfEvents = NOfEvents.createNoEvents();
                  }
  
              } else if (nOfEvents != NOfEvents.nofNoEvents) {
                  StringBuilder issues = new StringBuilder(100);
                  Collection<Peer> eventingPeers = getEventingPeers();
                  nOfEvents.unSeenPeers().forEach(peer -> {
                      if (peer.getChannel() != this) {
                          issues.append(format("Peer %s added to NOFEvents does not belong this channel. ", peer.getName()));
  
                      } else if (!eventingPeers.contains(peer)) {
                          issues.append(format("Peer %s added to NOFEvents is not a eventing Peer in this channel. ", peer.getName()));
                      }
  
                  });
  
                  if (nOfEvents.unSeenPeers().isEmpty()) {
                      issues.append("NofEvents had no added  Peer eventing services.");
                  }
                  String foundIssues = issues.toString();
                  if (!foundIssues.isEmpty()) {
                      throw new InvalidArgumentException(foundIssues);
                  }
              }
  
              final boolean replyonly = nOfEvents == NOfEvents.nofNoEvents || (getEventingPeers().isEmpty());
  
              CompletableFuture<TransactionEvent> sret;
              if (replyonly) { //If there are no eventsto complete the future, complete it
                  // immediately but give no transaction event
                  logger.debug(format("Completing transaction id %s immediately no peer eventing services found in channel %s.", proposalTransactionID, name));
                  sret = new CompletableFuture<>();
              } else {
                  sret = registerTxListener(proposalTransactionID, nOfEvents, transactionOptions.failFast);
              }
  
              logger.debug(format("Channel %s sending transaction to orderer(s) with TxID %s ", name, proposalTransactionID));
              boolean success = false;
              Exception lException = null; // Save last exception to report to user .. others are just logged.
  
              BroadcastResponse resp = null;
              Orderer failed = null;
              for (Orderer orderer : shuffeledOrderers) {
                  if (failed != null) {
                      logger.warn(format("Channel %s  %s failed. Now trying %s.", name, failed, orderer));
                  }
                  failed = orderer;
                  try {
  
                      if (null != diagnosticFileDumper) {
                          logger.trace(format("Sending to channel %s, orderer: %s, transaction: %s", name, orderer.getName(),
                                  diagnosticFileDumper.createDiagnosticProtobufFile(transactionEnvelope.toByteArray())));
                      }
  
                      resp = orderer.sendTransaction(transactionEnvelope);
                      lException = null; // no longer last exception .. maybe just failed.
                      if (resp.getStatus() == Status.SUCCESS) {
                          success = true;
                          break;
                      } else {
                          logger.warn(format("Channel %s %s failed. Status returned %s", name, orderer, getRespData(resp)));
                      }
                  } catch (Exception e) {
                      String emsg = format("Channel %s unsuccessful sendTransaction to orderer %s (%s)",
                              name, orderer.getName(), orderer.getUrl());
                      if (resp != null) {
  
                          emsg = format("Channel %s unsuccessful sendTransaction to orderer %s (%s).  %s",
                                  name, orderer.getName(), orderer.getUrl(), getRespData(resp));
                      }
  
                      logger.error(emsg);
                      lException = new Exception(emsg, e);
  
                  }
  
              }
  
              if (success) {
                  logger.debug(format("Channel %s successful sent to Orderer transaction id: %s",
                          name, proposalTransactionID));
                  if (replyonly) {
                      sret.complete(null); // just say we're done.
                  }
                  return sret;
              } else {
  
                  String emsg = format("Channel %s failed to place transaction %s on Orderer. Cause: UNSUCCESSFUL. %s",
                          name, proposalTransactionID, getRespData(resp));
  
                  unregisterTxListener(proposalTransactionID);
  
                  CompletableFuture<TransactionEvent> ret = new CompletableFuture<>();
                  ret.completeExceptionally(lException != null ? new Exception(emsg, lException) : new Exception(emsg));
                  return ret;
              }
          } catch (Exception e) {
  
              CompletableFuture<TransactionEvent> future = new CompletableFuture<>();
              future.completeExceptionally(e);
              return future;
  
          }
  
      }