/*
* This file is part of Bitsquare.
*
* Bitsquare is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bitsquare is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bitsquare. If not, see <http://www.gnu.org/licenses/>.
*/
package io.bitsquare.gui.main.funds.transactions;
import com.googlecode.jcsv.writer.CSVEntryConverter;
import de.jensd.fx.fontawesome.AwesomeIcon;
import io.bitsquare.arbitration.DisputeManager;
import io.bitsquare.btc.FeePolicy;
import io.bitsquare.btc.WalletService;
import io.bitsquare.common.util.Tuple2;
import io.bitsquare.common.util.Tuple4;
import io.bitsquare.common.util.Utilities;
import io.bitsquare.gui.common.view.ActivatableView;
import io.bitsquare.gui.common.view.FxmlView;
import io.bitsquare.gui.components.AddressWithIconAndDirection;
import io.bitsquare.gui.components.HyperlinkWithIcon;
import io.bitsquare.gui.main.overlays.popups.Popup;
import io.bitsquare.gui.main.overlays.windows.OfferDetailsWindow;
import io.bitsquare.gui.main.overlays.windows.TradeDetailsWindow;
import io.bitsquare.gui.util.BSFormatter;
import io.bitsquare.gui.util.GUIUtil;
import io.bitsquare.trade.Tradable;
import io.bitsquare.trade.Trade;
import io.bitsquare.trade.TradeManager;
import io.bitsquare.trade.closed.ClosedTradableManager;
import io.bitsquare.trade.failed.FailedTradesManager;
import io.bitsquare.trade.offer.OpenOffer;
import io.bitsquare.trade.offer.OpenOfferManager;
import io.bitsquare.user.Preferences;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.SortedList;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.util.Callback;
import org.bitcoinj.core.*;
import org.bitcoinj.script.Script;
import javax.annotation.Nullable;
import javax.inject.Inject;
import java.text.DateFormat;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@FxmlView
public class TransactionsView extends ActivatableView<VBox, Void> {
@FXML
TableView<TransactionsListItem> tableView;
@FXML
TableColumn<TransactionsListItem, TransactionsListItem> dateColumn, detailsColumn, addressColumn, transactionColumn, amountColumn, confidenceColumn, revertTxColumn;
@FXML
Button exportButton;
private final ObservableList<TransactionsListItem> observableList = FXCollections.observableArrayList();
private final SortedList<TransactionsListItem> sortedList = new SortedList<>(observableList);
private final WalletService walletService;
private final TradeManager tradeManager;
private final OpenOfferManager openOfferManager;
private final ClosedTradableManager closedTradableManager;
private final FailedTradesManager failedTradesManager;
private final BSFormatter formatter;
private final Preferences preferences;
private final TradeDetailsWindow tradeDetailsWindow;
private final DisputeManager disputeManager;
private Stage stage;
private final OfferDetailsWindow offerDetailsWindow;
private WalletEventListener walletEventListener;
private EventHandler<KeyEvent> keyEventEventHandler;
private Scene scene;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor, lifecycle
///////////////////////////////////////////////////////////////////////////////////////////
@Inject
private TransactionsView(WalletService walletService, TradeManager tradeManager, OpenOfferManager openOfferManager,
ClosedTradableManager closedTradableManager, FailedTradesManager failedTradesManager,
BSFormatter formatter, Preferences preferences, TradeDetailsWindow tradeDetailsWindow,
DisputeManager disputeManager, Stage stage,
OfferDetailsWindow offerDetailsWindow) {
this.walletService = walletService;
this.tradeManager = tradeManager;
this.openOfferManager = openOfferManager;
this.closedTradableManager = closedTradableManager;
this.failedTradesManager = failedTradesManager;
this.formatter = formatter;
this.preferences = preferences;
this.tradeDetailsWindow = tradeDetailsWindow;
this.disputeManager = disputeManager;
this.stage = stage;
this.offerDetailsWindow = offerDetailsWindow;
}
@Override
public void initialize() {
tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
tableView.setPlaceholder(new Label("No transactions available"));
setDateColumnCellFactory();
setDetailsColumnCellFactory();
setAddressColumnCellFactory();
setTransactionColumnCellFactory();
setAmountColumnCellFactory();
setConfidenceColumnCellFactory();
setRevertTxColumnCellFactory();
dateColumn.setComparator((o1, o2) -> o1.getDate().compareTo(o2.getDate()));
detailsColumn.setComparator((o1, o2) -> {
String id1 = o1.getTradable() != null ? o1.getTradable().getId() : o1.getDetails();
String id2 = o2.getTradable() != null ? o2.getTradable().getId() : o2.getDetails();
return id1.compareTo(id2);
});
addressColumn.setComparator((o1, o2) -> o1.getAddressString().compareTo(o2.getAddressString()));
transactionColumn.setComparator((o1, o2) -> o1.getTxId().compareTo(o2.getTxId()));
amountColumn.setComparator((o1, o2) -> o1.getAmountAsCoin().compareTo(o2.getAmountAsCoin()));
confidenceColumn.setComparator((o1, o2) -> Double.valueOf(o1.getTxConfidenceIndicator().getProgress())
.compareTo(o2.getTxConfidenceIndicator().getProgress()));
dateColumn.setSortType(TableColumn.SortType.DESCENDING);
tableView.getSortOrder().add(dateColumn);
walletEventListener = new WalletEventListener() {
@Override
public void onCoinsReceived(Wallet wallet, Transaction tx, Coin prevBalance, Coin newBalance) {
updateList();
}
@Override
public void onCoinsSent(Wallet wallet, Transaction tx, Coin prevBalance, Coin newBalance) {
updateList();
}
@Override
public void onReorganize(Wallet wallet) {
updateList();
}
@Override
public void onTransactionConfidenceChanged(Wallet wallet, Transaction tx) {
}
@Override
public void onWalletChanged(Wallet wallet) {
updateList();
}
@Override
public void onScriptsChanged(Wallet wallet, List<Script> scripts, boolean isAddingScripts) {
updateList();
}
@Override
public void onKeysAdded(List<ECKey> keys) {
updateList();
}
};
keyEventEventHandler = event -> {
if (new KeyCodeCombination(KeyCode.R, KeyCombination.ALT_DOWN).match(event))
revertTxColumn.setVisible(!revertTxColumn.isVisible());
else if (new KeyCodeCombination(KeyCode.A, KeyCombination.ALT_DOWN).match(event))
showStatisticsPopup();
};
exportButton.setText("Export to csv");
}
@Override
protected void activate() {
sortedList.comparatorProperty().bind(tableView.comparatorProperty());
tableView.setItems(sortedList);
updateList();
walletService.getWallet().addEventListener(walletEventListener);
scene = root.getScene();
if (scene != null)
scene.addEventHandler(KeyEvent.KEY_RELEASED, keyEventEventHandler);
exportButton.setOnAction(event -> {
final ObservableList<TableColumn<TransactionsListItem, ?>> tableColumns = tableView.getColumns();
CSVEntryConverter<TransactionsListItem> headerConverter = transactionsListItem -> {
String[] columns = new String[6];
for (int i = 0; i < columns.length; i++)
columns[i] = tableColumns.get(i).getText();
return columns;
};
CSVEntryConverter<TransactionsListItem> contentConverter = item -> {
String[] columns = new String[6];
columns[0] = item.getDateString();
columns[1] = item.getDetails();
columns[2] = item.getDirection() + " " + item.getAddressString();
columns[3] = item.getTxId();
columns[4] = item.getAmount();
columns[5] = item.getNumConfirmations();
return columns;
};
GUIUtil.exportCSV("transactions.csv", headerConverter, contentConverter,
new TransactionsListItem(), sortedList, stage);
});
}
@Override
protected void deactivate() {
sortedList.comparatorProperty().unbind();
observableList.forEach(TransactionsListItem::cleanup);
walletService.getWallet().removeEventListener(walletEventListener);
if (scene != null)
scene.removeEventHandler(KeyEvent.KEY_RELEASED, keyEventEventHandler);
exportButton.setOnAction(null);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Private
///////////////////////////////////////////////////////////////////////////////////////////
private void updateList() {
Stream<Tradable> concat1 = Stream.concat(openOfferManager.getOpenOffers().stream(), tradeManager.getTrades().stream());
Stream<Tradable> concat2 = Stream.concat(concat1, closedTradableManager.getClosedTrades().stream());
Stream<Tradable> concat3 = Stream.concat(concat2, failedTradesManager.getFailedTrades().stream());
Set<Tradable> all = concat3.collect(Collectors.toSet());
Set<Transaction> transactions = walletService.getWallet().getTransactions(true);
List<TransactionsListItem> transactionsListItems = transactions.stream()
.map(transaction -> {
Optional<Tradable> tradableOptional = all.stream()
.filter(tradable -> {
String txId = transaction.getHashAsString();
if (tradable instanceof OpenOffer)
return tradable.getOffer().getOfferFeePaymentTxID().equals(txId);
else if (tradable instanceof Trade) {
Trade trade = (Trade) tradable;
boolean isTakeOfferFeeTx = txId.equals(trade.getTakeOfferFeeTxId());
boolean isOfferFeeTx = trade.getOffer() != null &&
txId.equals(trade.getOffer().getOfferFeePaymentTxID());
boolean isDepositTx = trade.getDepositTx() != null &&
trade.getDepositTx().getHashAsString().equals(txId);
boolean isPayoutTx = trade.getPayoutTx() != null &&
trade.getPayoutTx().getHashAsString().equals(txId);
boolean isDisputedPayoutTx = disputeManager.getDisputesAsObservableList().stream()
.filter(dispute -> txId.equals(dispute.getDisputePayoutTxId()) &&
tradable.getId().equals(dispute.getTradeId()))
.findAny()
.isPresent();
return isTakeOfferFeeTx || isOfferFeeTx || isDepositTx || isPayoutTx || isDisputedPayoutTx;
} else
return false;
})
.findAny();
return new TransactionsListItem(transaction, walletService, tradableOptional, formatter);
})
.collect(Collectors.toList());
// are sorted by getRecentTransactions
observableList.forEach(TransactionsListItem::cleanup);
observableList.setAll(transactionsListItems);
}
private void openTxInBlockExplorer(TransactionsListItem item) {
if (item.getTxId() != null) {
try {
GUIUtil.openWebPage(preferences.getBlockChainExplorer().txUrl + item.getTxId());
} catch (Exception e) {
log.error(e.getMessage());
new Popup().warning("Opening browser failed. Please check your internet " +
"connection.").show();
}
}
}
private void openAddressInBlockExplorer(TransactionsListItem item) {
if (item.getAddressString() != null) {
try {
GUIUtil.openWebPage(preferences.getBlockChainExplorer().addressUrl + item.getAddressString());
} catch (Exception e) {
log.error(e.getMessage());
new Popup().warning("Opening browser failed. Please check your internet " +
"connection.").show();
}
}
}
private void openDetailPopup(TransactionsListItem item) {
if (item.getTradable() instanceof OpenOffer)
offerDetailsWindow.show(item.getTradable().getOffer());
else if (item.getTradable() instanceof Trade)
tradeDetailsWindow.show((Trade) item.getTradable());
}
///////////////////////////////////////////////////////////////////////////////////////////
// ColumnCellFactories
///////////////////////////////////////////////////////////////////////////////////////////
private void setDateColumnCellFactory() {
dateColumn.setCellValueFactory((addressListItem) ->
new ReadOnlyObjectWrapper<>(addressListItem.getValue()));
dateColumn.setCellFactory(
new Callback<TableColumn<TransactionsListItem, TransactionsListItem>, TableCell<TransactionsListItem,
TransactionsListItem>>() {
@Override
public TableCell<TransactionsListItem, TransactionsListItem> call(TableColumn<TransactionsListItem,
TransactionsListItem> column) {
return new TableCell<TransactionsListItem, TransactionsListItem>() {
@Override
public void updateItem(final TransactionsListItem item, boolean empty) {
super.updateItem(item, empty);
if (item != null && !empty) {
setText(item.getDateString());
} else {
setText("");
}
}
};
}
});
}
private void setDetailsColumnCellFactory() {
detailsColumn.setCellValueFactory((addressListItem) -> new ReadOnlyObjectWrapper<>(addressListItem.getValue()));
detailsColumn.setCellFactory(
new Callback<TableColumn<TransactionsListItem, TransactionsListItem>, TableCell<TransactionsListItem,
TransactionsListItem>>() {
@Override
public TableCell<TransactionsListItem, TransactionsListItem> call(TableColumn<TransactionsListItem,
TransactionsListItem> column) {
return new TableCell<TransactionsListItem, TransactionsListItem>() {
private HyperlinkWithIcon field;
@Override
public void updateItem(final TransactionsListItem item, boolean empty) {
super.updateItem(item, empty);
if (item != null && !empty) {
if (item.getDetailsAvailable()) {
field = new HyperlinkWithIcon(item.getDetails(), AwesomeIcon.INFO_SIGN);
field.setOnAction(event -> openDetailPopup(item));
field.setTooltip(new Tooltip("Open popup for details"));
setGraphic(field);
} else {
setGraphic(new Label(item.getDetails()));
}
} else {
setGraphic(null);
if (field != null)
field.setOnAction(null);
}
}
};
}
});
}
private void setAddressColumnCellFactory() {
addressColumn.setCellValueFactory((addressListItem) -> new ReadOnlyObjectWrapper<>(addressListItem.getValue()));
addressColumn.setCellFactory(
new Callback<TableColumn<TransactionsListItem, TransactionsListItem>, TableCell<TransactionsListItem,
TransactionsListItem>>() {
@Override
public TableCell<TransactionsListItem, TransactionsListItem> call(TableColumn<TransactionsListItem,
TransactionsListItem> column) {
return new TableCell<TransactionsListItem, TransactionsListItem>() {
private AddressWithIconAndDirection field;
@Override
public void updateItem(final TransactionsListItem item, boolean empty) {
super.updateItem(item, empty);
if (item != null && !empty) {
String addressString = item.getAddressString();
field = new AddressWithIconAndDirection(item.getDirection(), addressString,
AwesomeIcon.EXTERNAL_LINK, item.getReceived());
field.setOnAction(event -> openAddressInBlockExplorer(item));
field.setTooltip(new Tooltip("Open external blockchain explorer for " +
"address: " + addressString));
setGraphic(field);
} else {
setGraphic(null);
if (field != null)
field.setOnAction(null);
}
}
};
}
});
}
private void setTransactionColumnCellFactory() {
transactionColumn.setCellValueFactory((addressListItem) -> new ReadOnlyObjectWrapper<>(addressListItem.getValue()));
transactionColumn.setCellFactory(
new Callback<TableColumn<TransactionsListItem, TransactionsListItem>, TableCell<TransactionsListItem,
TransactionsListItem>>() {
@Override
public TableCell<TransactionsListItem, TransactionsListItem> call(TableColumn<TransactionsListItem,
TransactionsListItem> column) {
return new TableCell<TransactionsListItem, TransactionsListItem>() {
private HyperlinkWithIcon hyperlinkWithIcon;
@Override
public void updateItem(final TransactionsListItem item, boolean empty) {
super.updateItem(item, empty);
if (item != null && !empty) {
String transactionId = item.getTxId();
hyperlinkWithIcon = new HyperlinkWithIcon(transactionId, AwesomeIcon.EXTERNAL_LINK);
hyperlinkWithIcon.setOnAction(event -> openTxInBlockExplorer(item));
hyperlinkWithIcon.setTooltip(new Tooltip("Open external blockchain explorer for " +
"transaction: " + transactionId));
setGraphic(hyperlinkWithIcon);
} else {
setGraphic(null);
if (hyperlinkWithIcon != null)
hyperlinkWithIcon.setOnAction(null);
}
}
};
}
});
}
private void setAmountColumnCellFactory() {
amountColumn.setCellValueFactory((addressListItem) ->
new ReadOnlyObjectWrapper<>(addressListItem.getValue()));
amountColumn.setCellFactory(
new Callback<TableColumn<TransactionsListItem, TransactionsListItem>, TableCell<TransactionsListItem,
TransactionsListItem>>() {
@Override
public TableCell<TransactionsListItem, TransactionsListItem> call(TableColumn<TransactionsListItem,
TransactionsListItem> column) {
return new TableCell<TransactionsListItem, TransactionsListItem>() {
@Override
public void updateItem(final TransactionsListItem item, boolean empty) {
super.updateItem(item, empty);
if (item != null && !empty) {
setText(item.getAmount());
} else {
setText("");
}
}
};
}
});
}
private void setConfidenceColumnCellFactory() {
confidenceColumn.setCellValueFactory((addressListItem) ->
new ReadOnlyObjectWrapper<>(addressListItem.getValue()));
confidenceColumn.setCellFactory(
new Callback<TableColumn<TransactionsListItem, TransactionsListItem>, TableCell<TransactionsListItem,
TransactionsListItem>>() {
@Override
public TableCell<TransactionsListItem, TransactionsListItem> call(TableColumn<TransactionsListItem,
TransactionsListItem> column) {
return new TableCell<TransactionsListItem, TransactionsListItem>() {
@Override
public void updateItem(final TransactionsListItem item, boolean empty) {
super.updateItem(item, empty);
if (item != null && !empty) {
setGraphic(item.getTxConfidenceIndicator());
} else {
setGraphic(null);
}
}
};
}
});
}
private void setRevertTxColumnCellFactory() {
revertTxColumn.setCellValueFactory((addressListItem) ->
new ReadOnlyObjectWrapper<>(addressListItem.getValue()));
revertTxColumn.setCellFactory(
new Callback<TableColumn<TransactionsListItem, TransactionsListItem>, TableCell<TransactionsListItem,
TransactionsListItem>>() {
@Override
public TableCell<TransactionsListItem, TransactionsListItem> call(TableColumn<TransactionsListItem,
TransactionsListItem> column) {
return new TableCell<TransactionsListItem, TransactionsListItem>() {
Button button;
@Override
public void updateItem(final TransactionsListItem item, boolean empty) {
super.updateItem(item, empty);
if (item != null && !empty) {
if (walletService.getConfidenceForTxId(item.getTxId()).getConfidenceType() == TransactionConfidence.ConfidenceType.PENDING) {
if (button == null) {
button = new Button("Revert");
button.setOnAction(e -> revertTransaction(item.getTxId(), item.getTradable()));
setGraphic(button);
}
} else {
if (button != null) {
button.setOnAction(null);
button = null;
setGraphic(null);
}
}
} else {
setGraphic(null);
if (button != null) {
button.setOnAction(null);
button = null;
}
}
}
};
}
});
}
private void revertTransaction(String txId, @Nullable Tradable tradable) {
try {
walletService.doubleSpendTransaction(txId, () -> {
if (tradable != null)
walletService.swapAnyTradeEntryContextToAvailableEntry(tradable.getId());
new Popup().information("Transaction successfully sent to a new address in the local Bitsquare wallet.").show();
}, errorMessage -> {
new Popup().warning(errorMessage).show();
});
} catch (Throwable e) {
new Popup().warning(e.getMessage()).show();
}
}
private void showStatisticsPopup() {
Map<Long, List<Coin>> map = new HashMap<>();
Map<String, Tuple4<Date, Integer, Integer, Integer>> dataByDayMap = new HashMap<>();
observableList.stream().forEach(item -> {
Coin amountAsCoin = item.getAmountAsCoin();
List<Coin> amounts;
long key = amountAsCoin.getValue();
if (!map.containsKey(key)) {
amounts = new ArrayList<>();
map.put(key, amounts);
} else {
amounts = map.get(key);
}
amounts.add(amountAsCoin);
DateFormat dateFormatter = DateFormat.getDateInstance(DateFormat.DEFAULT, Locale.US);
String day = dateFormatter.format(item.getDate());
if (!dataByDayMap.containsKey(day)) {
int numOffers = 0;
int numTrades = 0;
if (amountAsCoin.compareTo(FeePolicy.getCreateOfferFee().subtract(FeePolicy.getFixedTxFeeForTrades())) == 0)
numOffers++;
else if (amountAsCoin.compareTo(FeePolicy.getTakeOfferFee().subtract(FeePolicy.getFixedTxFeeForTrades())) == 0)
numTrades++;
dataByDayMap.put(day, new Tuple4<>(item.getDate(), 1, numOffers, numTrades));
} else {
Tuple4<Date, Integer, Integer, Integer> tuple = dataByDayMap.get(day);
int prev = tuple.second;
int numOffers = tuple.third;
int numTrades = tuple.forth;
if (amountAsCoin.compareTo(FeePolicy.getCreateOfferFee().subtract(FeePolicy.getFixedTxFeeForTrades())) == 0)
numOffers++;
else if (amountAsCoin.compareTo(FeePolicy.getTakeOfferFee().subtract(FeePolicy.getFixedTxFeeForTrades())) == 0)
numTrades++;
dataByDayMap.put(day, new Tuple4<>(tuple.first, ++prev, numOffers, numTrades));
}
});
StringBuilder stringBuilder = new StringBuilder();
map.entrySet().stream().forEach(e -> {
stringBuilder.append("No. of transactions for amount ").
append(formatter.formatCoinWithCode(Coin.valueOf(e.getKey()))).
append(": ").
append(e.getValue().size()).
append("\n");
});
List<Tuple4<String, Date, Integer, Tuple2<Integer, Integer>>> sortedDataByDayList = dataByDayMap.entrySet().stream().
map(e -> {
Tuple4<Date, Integer, Integer, Integer> data = e.getValue();
return new Tuple4<>(e.getKey(), data.first, data.second, new Tuple2<>(data.third, data.forth));
}).
collect(Collectors.toList());
sortedDataByDayList.sort((o1, o2) -> o2.second.compareTo(o1.second));
StringBuilder transactionsByDayStringBuilder = new StringBuilder();
StringBuilder offersStringBuilder = new StringBuilder();
StringBuilder tradesStringBuilder = new StringBuilder();
StringBuilder allStringBuilder = new StringBuilder();
allStringBuilder.append("Date").append(";").append("Offers").append(";").append("Trades").append("\n");
sortedDataByDayList.stream().forEach(tuple4 -> {
offersStringBuilder.append(tuple4.forth.first).append(",");
tradesStringBuilder.append(tuple4.forth.second).append(",");
allStringBuilder.append(tuple4.first).append(";").append(tuple4.forth.first).append(";").append(tuple4.forth.second).append("\n");
transactionsByDayStringBuilder.append("\n").
append(tuple4.first).
append(": ").
append(tuple4.third).
append(" (Offers: ").
append(tuple4.forth.first).
append(" / Trades: ").
append(tuple4.forth.second).
append(")");
});
String message = stringBuilder.toString() + "\nNo. of transactions by day:" + transactionsByDayStringBuilder.toString();
new Popup().headLine("Statistical info")
.information(message)
.actionButtonText("Copy")
.onAction(() -> Utilities.copyToClipboard(message +
"\n\nCSV (Offers):\n" + offersStringBuilder.toString() +
"\n\nCSV (Trades):\n" + tradesStringBuilder.toString() +
"\n\nCSV (all):\n" + allStringBuilder.toString()))
.show();
}
}