gt:             fee = 0; // fee doesn't affect recipient account
context: package nxt;
  
  import nxt.util.Convert;
  import nxt.util.Listener;
  import nxt.util.Logger;
  
  import java.io.BufferedWriter;
  import java.io.FileOutputStream;
  import java.io.IOException;
  import java.io.OutputStreamWriter;
  import java.io.PrintWriter;
  import java.math.BigInteger;
  import java.util.Collections;
  import java.util.HashMap;
  import java.util.HashSet;
  import java.util.List;
  import java.util.Map;
  import java.util.Set;
  
  public final class DebugTrace {
  
      static final String QUOTE = Nxt.getStringProperty("nxt.debugTraceQuote", "");
      static final String SEPARATOR = Nxt.getStringProperty("nxt.debugTraceSeparator", "\t");
      static final boolean LOG_UNCONFIRMED = Nxt.getBooleanProperty("nxt.debugLogUnconfirmed");
  
      static void init() {
          List<String> accountIds = Nxt.getStringListProperty("nxt.debugTraceAccounts");
          String logName = Nxt.getStringProperty("nxt.debugTraceLog");
          if (accountIds.isEmpty() || logName == null) {
              return;
          }
          addDebugTrace(accountIds, logName);
          Logger.logDebugMessage("Debug tracing of " + (accountIds.contains("*") ? "ALL"
                  : String.valueOf(accountIds.size())) + " accounts enabled");
      }
  
      public static void addDebugTrace(List<String> accountIds, String logName) {
          final DebugTrace debugTrace = new DebugTrace(accountIds, logName);
          final Map<String, String> headers = new HashMap<>();
          for (String entry : columns) {
              headers.put(entry, entry);
          }
          debugTrace.log(headers);
          Trade.addListener(new Listener<Trade>() {
              @Override
              public void notify(Trade trade) {
                  debugTrace.trace(trade);
              }
          }, Trade.Event.TRADE);
          Account.addListener(new Listener<Account>() {
              @Override
              public void notify(Account account) {
                  debugTrace.trace(account, false);
              }
          }, Account.Event.BALANCE);
          if (LOG_UNCONFIRMED) {
              Account.addListener(new Listener<Account>() {
                  @Override
                  public void notify(Account account) {
                      debugTrace.trace(account, true);
                  }
              }, Account.Event.UNCONFIRMED_BALANCE);
          }
          Account.addAssetListener(new Listener<Account.AccountAsset>() {
              @Override
              public void notify(Account.AccountAsset accountAsset) {
                  debugTrace.trace(accountAsset, false);
              }
          }, Account.Event.ASSET_BALANCE);
          if (LOG_UNCONFIRMED) {
              Account.addAssetListener(new Listener<Account.AccountAsset>() {
                  @Override
                  public void notify(Account.AccountAsset accountAsset) {
                      debugTrace.trace(accountAsset, true);
                  }
              }, Account.Event.UNCONFIRMED_ASSET_BALANCE);
          }
          Account.addLeaseListener(new Listener<Account.AccountLease>() {
              @Override
              public void notify(Account.AccountLease accountLease) {
                  debugTrace.trace(accountLease, true);
              }
          }, Account.Event.LEASE_STARTED);
          Account.addLeaseListener(new Listener<Account.AccountLease>() {
              @Override
              public void notify(Account.AccountLease accountLease) {
                  debugTrace.trace(accountLease, false);
              }
          }, Account.Event.LEASE_ENDED);
          Nxt.getBlockchainProcessor().addListener(new Listener<Block>() {
              @Override
              public void notify(Block block) {
                  debugTrace.resetLog();
                  debugTrace.log(headers);
              }
          }, BlockchainProcessor.Event.RESCAN_BEGIN);
          Nxt.getBlockchainProcessor().addListener(new Listener<Block>() {
              @Override
              public void notify(Block block) {
                  debugTrace.traceBeforeAccept(block, false);
              }
          }, BlockchainProcessor.Event.BEFORE_BLOCK_ACCEPT);
          Nxt.getBlockchainProcessor().addListener(new Listener<Block>() {
              @Override
              public void notify(Block block) {
                  debugTrace.trace(block, false);
              }
          }, BlockchainProcessor.Event.BEFORE_BLOCK_APPLY);
          Nxt.getBlockchainProcessor().addListener(new Listener<Block>() {
              @Override
              public void notify(Block block) {
                  debugTrace.trace(block, true);
              }
          }, BlockchainProcessor.Event.BEFORE_BLOCK_UNDO);
      }
  
      private static final String[] columns = {"height", "event", "account", "asset", "balance", "unconfirmed balance",
              "asset balance", "unconfirmed asset balance",
              "transaction amount", "transaction fee", "generation fee", "effective balance",
              "order", "order price", "order quantity", "order cost",
              "trade price", "trade quantity", "trade cost",
              "asset quantity", "transaction", "lessee", "lessor guaranteed balance",
              "purchase", "purchase price", "purchase quantity", "purchase cost", "discount", "refund",
              "sender", "recipient", "block", "timestamp"};
  
      private final Set<Long> accountIds;
      private final String logName;
      private PrintWriter log;
  
      private DebugTrace(List<String> accountIds, String logName) {
          this.accountIds = new HashSet<>();
          for (String accountId : accountIds) {
              if ("*".equals(accountId)) {
                  this.accountIds.clear();
                  break;
              }
              this.accountIds.add(Convert.parseUnsignedLong(accountId));
          }
          this.logName = logName;
          resetLog();
      }
  
      void resetLog() {
          if (log != null) {
              log.close();
          }
          try {
              log = new PrintWriter((new BufferedWriter(new OutputStreamWriter(new FileOutputStream(logName)))), true);
          } catch (IOException e) {
              Logger.logDebugMessage("Debug tracing to " + logName + " not possible", e);
              throw new RuntimeException(e);
          }
      }
  
      private boolean include(Long accountId) {
          return accountId != null && (accountIds.isEmpty() || accountIds.contains(accountId));
      }
  
      private boolean include(Attachment attachment) {
          if (attachment instanceof Attachment.DigitalGoodsPurchase) {
              Long sellerId = DigitalGoodsStore.getGoods(((Attachment.DigitalGoodsPurchase)attachment).getGoodsId()).getSellerId();
              return include(sellerId);
          } else if (attachment instanceof Attachment.DigitalGoodsDelivery) {
              Long buyerId = DigitalGoodsStore.getPurchase(((Attachment.DigitalGoodsDelivery)attachment).getPurchaseId()).getBuyerId();
              return include(buyerId);
          } else if (attachment instanceof Attachment.DigitalGoodsRefund) {
              Long buyerId = DigitalGoodsStore.getPurchase(((Attachment.DigitalGoodsRefund)attachment).getPurchaseId()).getBuyerId();
              return include(buyerId);
          }
          return false;
      }
  
      // Note: Trade events occur before the change in account balances
      private void trace(Trade trade) {
          Long askAccountId = Order.Ask.getAskOrder(trade.getAskOrderId()).getAccount().getId();
          Long bidAccountId = Order.Bid.getBidOrder(trade.getBidOrderId()).getAccount().getId();
          if (include(askAccountId)) {
              log(getValues(askAccountId, trade, true));
          }
          if (include(bidAccountId)) {
              log(getValues(bidAccountId, trade, false));
          }
      }
  
      private void trace(Account account, boolean unconfirmed) {
          if (include(account.getId())) {
              log(getValues(account.getId(), unconfirmed));
          }
      }
  
      private void trace(Account.AccountAsset accountAsset, boolean unconfirmed) {
          if (! include(accountAsset.accountId)) {
              return;
          }
          log(getValues(accountAsset.accountId, accountAsset, unconfirmed));
      }
  
      private void trace(Account.AccountLease accountLease, boolean start) {
          if (! include(accountLease.lesseeId) && ! include(accountLease.lessorId)) {
              return;
          }
          log(getValues(accountLease.lessorId, accountLease, start));
      }
  
      private void traceBeforeAccept(Block block, boolean isUndo) {
          Long generatorId = block.getGeneratorId();
          if (include(generatorId)) {
              log(getValues(generatorId, block, isUndo));
          }
          for (Long accountId : accountIds) {
              Account account = Account.getAccount(accountId);
              if (account != null) {
                  for (Long lessorId : account.getLessorIds()) {
                      log(lessorGuaranteedBalance(lessorId, accountId));
                  }
              }
          }
      }
  
      private void trace(Block block, boolean isUndo) {
          for (Transaction transaction : block.getTransactions()) {
              Long senderId = transaction.getSenderId();
              if (include(senderId)) {
                  log(getValues(senderId, transaction, false, isUndo));
                  log(getValues(senderId, transaction, transaction.getAttachment(), false, isUndo));
              }
              Long recipientId = transaction.getRecipientId();
              if (include(recipientId)) {
                  log(getValues(recipientId, transaction, true, isUndo));
                  log(getValues(recipientId, transaction, transaction.getAttachment(), true, isUndo));
              } else {
                  Attachment attachment = transaction.getAttachment();
                  if (include(attachment)) {
                      log(getValues(recipientId, transaction, transaction.getAttachment(), true, isUndo));
                  }
              }
          }
      }
  
      private Map<String,String> lessorGuaranteedBalance(Long accountId, Long lesseeId) {
          Map<String,String> map = new HashMap<>();
          map.put("account", Convert.toUnsignedLong(accountId));
          Account account = Account.getAccount(accountId);
          map.put("lessor guaranteed balance", String.valueOf(account.getGuaranteedBalanceNQT(1440)));
          map.put("lessee", Convert.toUnsignedLong(lesseeId));
          map.put("timestamp", String.valueOf(Nxt.getBlockchain().getLastBlock().getTimestamp()));
          map.put("height", String.valueOf(Nxt.getBlockchain().getLastBlock().getHeight()));
          map.put("event", "lessor guaranteed balance");
          return map;
      }
  
      private Map<String,String> getValues(Long accountId, boolean unconfirmed) {
          Map<String,String> map = new HashMap<>();
          map.put("account", Convert.toUnsignedLong(accountId));
          Account account = Account.getAccount(accountId);
          map.put("balance", String.valueOf(account != null ? account.getBalanceNQT() : 0));
          map.put("unconfirmed balance", String.valueOf(account != null ? account.getUnconfirmedBalanceNQT() : 0));
          map.put("timestamp", String.valueOf(Nxt.getBlockchain().getLastBlock().getTimestamp()));
          map.put("height", String.valueOf(Nxt.getBlockchain().getLastBlock().getHeight()));
          map.put("event", unconfirmed ? "unconfirmed balance" : "balance");
          return map;
      }
  
      private Map<String,String> getValues(Long accountId, Trade trade, boolean isAsk) {
          Map<String,String> map = getValues(accountId, false);
          map.put("asset", Convert.toUnsignedLong(trade.getAssetId()));
          map.put("trade quantity", String.valueOf(isAsk ? - trade.getQuantityQNT() : trade.getQuantityQNT()));
          map.put("trade price", String.valueOf(trade.getPriceNQT()));
          long tradeCost = Convert.safeMultiply(trade.getQuantityQNT(), trade.getPriceNQT());
          map.put("trade cost", String.valueOf((isAsk ? tradeCost : - tradeCost)));
          map.put("event", "trade");
          return map;
      }
  
      private Map<String,String> getValues(Long accountId, Transaction transaction, boolean isRecipient, boolean isUndo) {
          long amount = transaction.getAmountNQT();
          long fee = transaction.getFeeNQT();
          if (isRecipient) {
  