/******************************************************************************
* Copyright © 2013-2016 The Nxt Core Developers. *
* *
* See the AUTHORS.txt, DEVELOPER-AGREEMENT.txt and LICENSE.txt files at *
* the top-level directory of this distribution for the individual copyright *
* holder information and the developer policies on copyright and licensing. *
* *
* Unless otherwise agreed in a custom licensing agreement, no part of the *
* Nxt software, including this file, may be copied, modified, propagated, *
* or distributed except according to the terms contained in the LICENSE.txt *
* file. *
* *
* Removal or modification of this copyright notice is prohibited. *
* *
******************************************************************************/
package nxt.tools;
import nxt.Genesis;
import nxt.util.Convert;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
public final class VerifyTraceFile {
private static final List<String> balanceHeaders = Arrays.asList("balance", "unconfirmed balance");
private static final List<String> deltaHeaders = Arrays.asList("transaction amount", "transaction fee", "dividend",
"generation fee", "trade cost", "purchase cost", "discount", "refund", "exchange cost", "currency cost");
private static final List<String> assetQuantityHeaders = Arrays.asList("asset balance", "unconfirmed asset balance");
private static final List<String> deltaAssetQuantityHeaders = Arrays.asList("asset quantity", "trade quantity");
private static final List<String> currencyBalanceHeaders = Arrays.asList("currency balance", "unconfirmed currency balance");
private static final List<String> deltaCurrencyUnitHeaders = Arrays.asList("currency units", "exchange quantity");
private static boolean isBalance(String header) {
return balanceHeaders.contains(header);
}
private static boolean isDelta(String header) {
return deltaHeaders.contains(header);
}
private static boolean isAssetQuantity(String header) {
return assetQuantityHeaders.contains(header);
}
private static boolean isDeltaAssetQuantity(String header) {
return deltaAssetQuantityHeaders.contains(header);
}
private static boolean isCurrencyBalance(String header) {
return currencyBalanceHeaders.contains(header);
}
private static boolean isDeltaCurrencyUnits(String header) {
return deltaCurrencyUnitHeaders.contains(header);
}
public static void main(String[] args) {
String fileName = args.length == 1 ? args[0] : "nxt-trace.csv";
try (BufferedReader reader = new BufferedReader(new FileReader(fileName))) {
String line = reader.readLine();
String[] headers = unquote(line.split("\t"));
Map<String,Map<String,Long>> totals = new HashMap<>();
Map<String,Map<String,Map<String,Long>>> accountAssetTotals = new HashMap<>();
Map<String,Long> issuedAssetQuantities = new HashMap<>();
Map<String,Long> accountAssetQuantities = new HashMap<>();
Map<String,Map<String,Map<String,Long>>> accountCurrencyTotals = new HashMap<>();
Map<String,Long> issuedCurrencyUnits = new HashMap<>();
Map<String,Long> accountCurrencyUnits = new HashMap<>();
while ((line = reader.readLine()) != null) {
String[] values = unquote(line.split("\t"));
Map<String,String> valueMap = new HashMap<>();
for (int i = 0; i < headers.length; i++) {
valueMap.put(headers[i], values[i]);
}
String accountId = valueMap.get("account");
Map<String,Long> accountTotals = totals.get(accountId);
if (accountTotals == null) {
accountTotals = new HashMap<>();
totals.put(accountId, accountTotals);
}
Map<String,Map<String,Long>> accountAssetMap = accountAssetTotals.get(accountId);
if (accountAssetMap == null) {
accountAssetMap = new HashMap<>();
accountAssetTotals.put(accountId, accountAssetMap);
}
String event = valueMap.get("event");
if ("asset issuance".equals(event)) {
String assetId = valueMap.get("asset");
issuedAssetQuantities.put(assetId, Long.parseLong(valueMap.get("asset quantity")));
}
if ("asset transfer".equals(event) && Genesis.CREATOR_ID == Convert.parseUnsignedLong(accountId)) {
String assetId = valueMap.get("asset");
long deletedQuantity = Long.parseLong(valueMap.get("asset quantity"));
long currentQuantity = issuedAssetQuantities.get(assetId);
issuedAssetQuantities.put(assetId, currentQuantity - deletedQuantity);
}
if ("asset delete".equals(event)) {
String assetId = valueMap.get("asset");
long deletedQuantity = - Long.parseLong(valueMap.get("asset quantity"));
long currentQuantity = issuedAssetQuantities.get(assetId);
issuedAssetQuantities.put(assetId, currentQuantity - deletedQuantity);
}
Map<String,Map<String,Long>> accountCurrencyMap = accountCurrencyTotals.get(accountId);
if (accountCurrencyMap == null) {
accountCurrencyMap = new HashMap<>();
accountCurrencyTotals.put(accountId, accountCurrencyMap);
}
if ("currency issuance".equals(event)) {
String currencyId = valueMap.get("currency");
issuedCurrencyUnits.put(currencyId, Long.parseLong(valueMap.get("currency units")));
}
if ("crowdfunding".equals(event)) {
String currencyId = valueMap.get("currency");
issuedCurrencyUnits.put(currencyId, Long.parseLong(valueMap.get("crowdfunding")));
}
if ("currency mint".equals(event)) {
String currencyId = valueMap.get("currency");
issuedCurrencyUnits.put(currencyId, Math.addExact(nullToZero(issuedCurrencyUnits.get(currencyId)), Long.parseLong(valueMap.get("currency units"))));
}
if ("currency claim".equals(event)) {
String currencyId = valueMap.get("currency");
issuedCurrencyUnits.put(currencyId, Math.addExact(nullToZero(issuedCurrencyUnits.get(currencyId)), Long.parseLong(valueMap.get("currency units"))));
}
if ("currency delete".equals(event) || "undo crowdfunding".equals(event)) {
String currencyId = valueMap.get("currency");
issuedCurrencyUnits.put(currencyId, 0L);
}
for (Map.Entry<String,String> mapEntry : valueMap.entrySet()) {
String header = mapEntry.getKey();
String value = mapEntry.getValue();
if (value == null || "".equals(value.trim())) {
continue;
}
if (isBalance(header)) {
accountTotals.put(header, Long.parseLong(value));
} else if (isDelta(header)) {
long previousValue = nullToZero(accountTotals.get(header));
accountTotals.put(header, Math.addExact(previousValue, Long.parseLong(value)));
} else if (isAssetQuantity(header)) {
String assetId = valueMap.get("asset");
Map<String,Long> assetTotals = accountAssetMap.get(assetId);
if (assetTotals == null) {
assetTotals = new HashMap<>();
accountAssetMap.put(assetId, assetTotals);
}
assetTotals.put(header, Long.parseLong(value));
} else if (isDeltaAssetQuantity(header)) {
String assetId = valueMap.get("asset");
Map<String,Long> assetTotals = accountAssetMap.get(assetId);
if (assetTotals == null) {
assetTotals = new HashMap<>();
accountAssetMap.put(assetId, assetTotals);
}
long previousValue = nullToZero(assetTotals.get(header));
assetTotals.put(header, Math.addExact(previousValue, Long.parseLong(value)));
} else if (isCurrencyBalance(header)) {
String currencyId = valueMap.get("currency");
Map<String,Long> currencyTotals = accountCurrencyMap.get(currencyId);
if (currencyTotals == null) {
currencyTotals = new HashMap<>();
accountCurrencyMap.put(currencyId, currencyTotals);
}
currencyTotals.put(header, Long.parseLong(value));
} else if (isDeltaCurrencyUnits(header)) {
String currencyId = valueMap.get("currency");
Map<String,Long> currencyTotals = accountCurrencyMap.get(currencyId);
if (currencyTotals == null) {
currencyTotals = new HashMap<>();
accountCurrencyMap.put(currencyId, currencyTotals);
}
long previousValue = nullToZero(currencyTotals.get(header));
currencyTotals.put(header, Math.addExact(previousValue, Long.parseLong(value)));
}
}
}
Set<String> failed = new HashSet<>();
for (Map.Entry<String,Map<String,Long>> mapEntry : totals.entrySet()) {
String accountId = mapEntry.getKey();
Map<String,Long> accountValues = mapEntry.getValue();
System.out.println("account: " + accountId);
for (String balanceHeader : balanceHeaders) {
System.out.println(balanceHeader + ": " + nullToZero(accountValues.get(balanceHeader)));
}
System.out.println("totals:");
long totalDelta = 0;
for (String header : deltaHeaders) {
long delta = nullToZero(accountValues.get(header));
totalDelta = Math.addExact(totalDelta, delta);
System.out.println(header + ": " + delta);
}
System.out.println("total confirmed balance change: " + totalDelta);
long balance = nullToZero(accountValues.get("balance"));
if (balance != totalDelta) {
System.out.println("ERROR: balance doesn't match total change!!!");
failed.add(accountId);
}
Map<String,Map<String,Long>> accountAssetMap = accountAssetTotals.get(accountId);
for (Map.Entry<String,Map<String,Long>> assetMapEntry : accountAssetMap.entrySet()) {
String assetId = assetMapEntry.getKey();
Map<String,Long> assetValues = assetMapEntry.getValue();
System.out.println("asset: " + assetId);
for (Map.Entry<String,Long> assetValueEntry : assetValues.entrySet()) {
System.out.println(assetValueEntry.getKey() + ": " + assetValueEntry.getValue());
}
long totalAssetDelta = 0;
for (String header : deltaAssetQuantityHeaders) {
long delta = nullToZero(assetValues.get(header));
totalAssetDelta = Math.addExact(totalAssetDelta, delta);
}
System.out.println("total confirmed asset quantity change: " + totalAssetDelta);
long assetBalance = nullToZero(assetValues.get("asset balance"));
if (assetBalance != totalAssetDelta && (Genesis.CREATOR_ID != Convert.parseUnsignedLong(accountId) || assetBalance != 0)) {
System.out.println("ERROR: asset balance doesn't match total asset quantity change!!!");
failed.add(accountId);
}
long previousAssetQuantity = nullToZero(accountAssetQuantities.get(assetId));
accountAssetQuantities.put(assetId, Math.addExact(previousAssetQuantity, assetBalance));
}
Map<String,Map<String,Long>> accountCurrencyMap = accountCurrencyTotals.get(accountId);
for (Map.Entry<String,Map<String,Long>> currencyMapEntry : accountCurrencyMap.entrySet()) {
String currencyId = currencyMapEntry.getKey();
Map<String,Long> currencyValues = currencyMapEntry.getValue();
System.out.println("currency: " + currencyId);
for (Map.Entry<String,Long> currencyValueEntry : currencyValues.entrySet()) {
System.out.println(currencyValueEntry.getKey() + ": " + currencyValueEntry.getValue());
}
long totalCurrencyDelta = 0;
for (String header : deltaCurrencyUnitHeaders) {
long delta = nullToZero(currencyValues.get(header));
totalCurrencyDelta = Math.addExact(totalCurrencyDelta, delta);
}
System.out.println("total confirmed currency units change: " + totalCurrencyDelta);
long currencyBalance = nullToZero(currencyValues.get("currency balance"));
if (currencyBalance != totalCurrencyDelta) {
System.out.println("ERROR: currency balance doesn't match total currency units change!!!");
failed.add(accountId);
}
long previousCurrencyQuantity = nullToZero(accountCurrencyUnits.get(currencyId));
accountCurrencyUnits.put(currencyId, Math.addExact(previousCurrencyQuantity, currencyBalance));
}
System.out.println();
}
Set<String> failedAssets = new HashSet<>();
for (Map.Entry<String,Long> assetEntry : issuedAssetQuantities.entrySet()) {
String assetId = assetEntry.getKey();
long issuedAssetQuantity = assetEntry.getValue();
if (issuedAssetQuantity != nullToZero(accountAssetQuantities.get(assetId))) {
System.out.println("ERROR: asset " + assetId + " balances don't match, issued: "
+ issuedAssetQuantity
+ ", total of account balances: " + accountAssetQuantities.get(assetId));
failedAssets.add(assetId);
}
}
Set<String> failedCurrencies = new HashSet<>();
for (Map.Entry<String,Long> currencyEntry : issuedCurrencyUnits.entrySet()) {
String currencyId = currencyEntry.getKey();
long issuedCurrencyQuantity = currencyEntry.getValue();
if (issuedCurrencyQuantity != nullToZero(accountCurrencyUnits.get(currencyId))) {
System.out.println("ERROR: currency " + currencyId + " balances don't match, issued: "
+ issuedCurrencyQuantity
+ ", total of account balances: " + accountCurrencyUnits.get(currencyId));
failedCurrencies.add(currencyId);
}
}
if (failed.size() > 0) {
System.out.println("ERROR: " + failed.size() + " accounts have incorrect balances");
System.out.println(failed);
} else {
System.out.println("SUCCESS: all " + totals.size() + " account balances and asset balances match the transaction and trade totals!");
}
if (failedAssets.size() > 0) {
System.out.println("ERROR: " + failedAssets.size() + " assets have incorrect balances");
System.out.println(failedAssets);
} else {
System.out.println("SUCCESS: all " + issuedAssetQuantities.size() + " assets quantities are correct!");
}
if (failedCurrencies.size() > 0) {
System.out.println("ERROR: " + failedCurrencies.size() + " currencies have incorrect balances");
System.out.println(failedCurrencies);
} else {
System.out.println("SUCCESS: all " + issuedCurrencyUnits.size() + " currency units are correct!");
}
} catch (IOException e) {
System.out.println(e.toString());
throw new RuntimeException(e);
}
}
private static final String beginQuote = "^\"";
private static final String endQuote = "\"$";
private static String[] unquote(String[] values) {
String[] result = new String[values.length];
for (int i = 0; i < values.length; i++) {
result[i] = values[i].replaceFirst(beginQuote, "").replaceFirst(endQuote, "");
}
return result;
}
private static long nullToZero(Long l) {
return l == null ? 0 : l;
}
}