package org.ripple.power.ui.btc;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Toolkit;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.StringSelection;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.math.BigInteger;
import java.util.Arrays;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.ListSelectionModel;
import javax.swing.SwingConstants;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.TableRowSorter;
import org.ripple.power.config.LSystem;
import org.ripple.power.txns.btc.Address;
import org.ripple.power.txns.btc.BTCLoader;
import org.ripple.power.txns.btc.BlockStoreException;
import org.ripple.power.txns.btc.BlockTransaction;
import org.ripple.power.txns.btc.ECKey;
import org.ripple.power.txns.btc.ReceiveTransaction;
import org.ripple.power.txns.btc.SendTransaction;
import org.ripple.power.ui.UIConfig;
import org.ripple.power.ui.UIRes;
import org.ripple.power.ui.table.AddressTable;
import org.ripple.power.ui.view.ButtonPane;
import org.ripple.power.ui.view.log.ErrorLog;
public class TransactionPanel extends JPanel implements ActionListener {
/**
*
*/
private static final long serialVersionUID = 1L;
private static final Class<?>[] columnClasses = { Date.class, String.class,
String.class, String.class, BigInteger.class, BigInteger.class,
String.class, String.class };
private static final String[] columnNames = { "Date", "Transaction ID",
"Type", "Name/Address", "Amount", "Fee", "Location", "Status" };
private static final int[] columnTypes = { AddressTable.DATE,
AddressTable.ADDRESS, AddressTable.TYPE, AddressTable.ADDRESS,
AddressTable.AMOUNT, AddressTable.AMOUNT, AddressTable.STATUS,
AddressTable.STATUS };
private final JLabel walletLabel;
private final JLabel safeLabel;
private final JLabel blockLabel;
private final JScrollPane scrollPane;
private final JTable table;
private final TransactionTableModel tableModel;
private BigInteger safeBalance;
private BigInteger walletBalance;
public TransactionPanel(JDialog parentFrame) {
super();
setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
setOpaque(true);
setBorder(BorderFactory.createEmptyBorder(15, 15, 15, 15));
tableModel = new TransactionTableModel(columnNames, columnClasses);
table = new AddressTable(tableModel, columnTypes);
table.setRowSorter(new TableRowSorter<>(tableModel));
table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
int frameHeight = 1200;
if (LSystem.applicationMain != null) {
frameHeight = LSystem.applicationMain.getHeight() + 50;
}
table.setPreferredScrollableViewportSize(new Dimension(table
.getPreferredScrollableViewportSize().width,
(frameHeight / table.getRowHeight()) * table.getRowHeight()));
scrollPane = new JScrollPane(table);
JPanel statusPane = new JPanel();
statusPane.setLayout(new BoxLayout(statusPane, BoxLayout.X_AXIS));
statusPane.setBackground(Color.WHITE);
walletLabel = new JLabel(getWalletText(), SwingConstants.CENTER);
statusPane.add(walletLabel);
safeLabel = new JLabel(getSafeText(), SwingConstants.CENTER);
statusPane.add(safeLabel);
blockLabel = new JLabel(getBlockText(), SwingConstants.CENTER);
statusPane.add(blockLabel);
ButtonPane buttonPane = new ButtonPane(this, 20, new String[] {
"New Address", "new address" }, new String[] { "Copy TxID",
"copy txid" }, new String[] { "Move to Safe", "move to safe" },
new String[] { "Move to Wallet", "move to blockStore" });
buttonPane.setBackground(UIConfig.dialogbackground);
add(statusPane);
add(Box.createVerticalStrut(5));
add(scrollPane);
add(Box.createVerticalStrut(5));
add(buttonPane);
setBackground(UIConfig.dialogbackground);
}
@Override
public void actionPerformed(ActionEvent ae) {
try {
int row = table.getSelectedRow();
if (row < 0) {
UIRes.showErrorMessage(this, "Error", "No transaction selected");
} else {
row = table.convertRowIndexToModel(row);
String action = ae.getActionCommand();
switch (action) {
case "new address":
break;
case "copy txid":
String address = (String) tableModel.getValueAt(row, 1);
StringSelection sel = new StringSelection(address);
Clipboard cb = Toolkit.getDefaultToolkit()
.getSystemClipboard();
cb.setContents(sel, null);
break;
case "move to safe":
if (moveToSafe(row)) {
tableModel.fireTableRowsUpdated(row, row);
walletLabel.setText(getWalletText());
safeLabel.setText(getSafeText());
}
break;
case "move to blockStore":
if (moveToWallet(row)) {
tableModel.fireTableRowsUpdated(row, row);
walletLabel.setText(getWalletText());
safeLabel.setText(getSafeText());
}
break;
}
}
} catch (BlockStoreException exc) {
ErrorLog.get().logException("Unable to update blockStore", exc);
} catch (Exception exc) {
ErrorLog.get().logException("Exception while processing action event",
exc);
}
}
public void walletChanged() {
int row = table.getSelectedRow();
tableModel.walletChanged();
if (row >= 0 && row < table.getRowCount()) {
table.setRowSelectionInterval(row, row);
}
walletLabel.setText(getWalletText());
safeLabel.setText(getSafeText());
}
public void statusChanged() {
blockLabel.setText(getBlockText());
tableModel.fireTableDataChanged();
}
private boolean moveToSafe(int row) throws BlockStoreException {
BlockTransaction tx = tableModel.getTransaction(row);
if (!(tx instanceof ReceiveTransaction)) {
UIRes.showErrorMessage(this, "Error",
"The safe contains coins that you have received and not spent");
return false;
}
ReceiveTransaction rcvTx = (ReceiveTransaction) tx;
if (rcvTx.inSafe()) {
UIRes.showErrorMessage(this, "Error",
"The transaction is already in the safe");
return false;
}
if (rcvTx.isSpent()) {
UIRes.showErrorMessage(this, "Error",
"The coins have already been spent");
return false;
}
BTCLoader.blockStore.setTxSafe(rcvTx.getTxHash(), rcvTx.getTxIndex(),
true);
rcvTx.setSafe(true);
safeBalance = safeBalance.add(rcvTx.getValue());
walletBalance = walletBalance.subtract(rcvTx.getValue());
return true;
}
private boolean moveToWallet(int row) throws BlockStoreException {
BlockTransaction tx = tableModel.getTransaction(row);
if (!(tx instanceof ReceiveTransaction)) {
UIRes.showErrorMessage(this, "Error",
"The safe contains coins that you have received and not spent");
return false;
}
ReceiveTransaction rcvTx = (ReceiveTransaction) tx;
if (!rcvTx.inSafe()) {
UIRes.showErrorMessage(this, "Error",
"The transaction is not in the safe");
return false;
}
BTCLoader.blockStore.setTxSafe(rcvTx.getTxHash(), rcvTx.getTxIndex(),
false);
walletBalance = walletBalance.add(rcvTx.getValue());
safeBalance = safeBalance.subtract(rcvTx.getValue());
rcvTx.setSafe(false);
return true;
}
private String getWalletText() {
return String.format("<html><h2>Wallet %s BTC</h2></html>",
BTCLoader.satoshiToString(walletBalance));
}
private String getSafeText() {
return String.format("<html><h2>Safe %s BTC</h2></html>",
BTCLoader.satoshiToString(safeBalance));
}
private String getBlockText() {
return String.format("<html><h2>Block %d</h2></html>",
BTCLoader.blockStore.getChainHeight());
}
private class TransactionTableModel extends AbstractTableModel {
/**
*
*/
private static final long serialVersionUID = 1L;
private String[] columnNames;
private Class<?>[] columnClasses;
private final List<BlockTransaction> txList = new LinkedList<>();
public TransactionTableModel(String[] columnNames,
Class<?>[] columnClasses) {
super();
if (columnNames.length != columnClasses.length)
throw new IllegalArgumentException(
"Number of names not same as number of classes");
this.columnNames = columnNames;
this.columnClasses = columnClasses;
buildTxList();
}
private void buildTxList() {
txList.clear();
walletBalance = BigInteger.ZERO;
safeBalance = BigInteger.ZERO;
try {
List<SendTransaction> sendList = BTCLoader.blockStore
.getSendTxList();
for (SendTransaction sendTx : sendList) {
long txTime = sendTx.getTxTime();
walletBalance = walletBalance.subtract(sendTx.getValue())
.subtract(sendTx.getFee());
boolean added = false;
for (int i = 0; i < txList.size(); i++) {
if (txList.get(i).getTxTime() <= txTime) {
txList.add(i, sendTx);
added = true;
break;
}
}
if (!added)
txList.add(sendTx);
}
List<ReceiveTransaction> rcvList = BTCLoader.blockStore
.getReceiveTxList();
for (ReceiveTransaction rcvTx : rcvList) {
if (rcvTx.isChange())
continue;
if (rcvTx.inSafe())
safeBalance = safeBalance.add(rcvTx.getValue());
else
walletBalance = walletBalance.add(rcvTx.getValue());
long txTime = rcvTx.getTxTime();
boolean added = false;
for (int i = 0; i < txList.size(); i++) {
if (txList.get(i).getTxTime() <= txTime) {
txList.add(i, rcvTx);
added = true;
break;
}
}
if (!added) {
txList.add(rcvTx);
}
}
} catch (BlockStoreException exc) {
ErrorLog.get().logException("Unable to build transaction list", exc);
}
}
@Override
public int getColumnCount() {
return columnNames.length;
}
@Override
public Class<?> getColumnClass(int column) {
return columnClasses[column];
}
@Override
public String getColumnName(int column) {
return columnNames[column];
}
@Override
public int getRowCount() {
return txList.size();
}
@Override
public Object getValueAt(int row, int column) {
if (row >= txList.size()) {
throw new IndexOutOfBoundsException("Table row " + row
+ " is not valid");
}
Object value;
BlockTransaction tx = txList.get(row);
switch (column) {
case 0:
value = new Date(tx.getTxTime() * 1000);
break;
case 1:
value = tx.getTxHash().toString();
break;
case 2:
if (tx instanceof ReceiveTransaction) {
value = "Received with";
} else {
value = "Sent to";
}
break;
case 3:
value = null;
Address addr = tx.getAddress();
if (tx instanceof ReceiveTransaction) {
for (ECKey chkKey : BTCLoader.keys) {
if (Arrays.equals(chkKey.getPubKeyHash(),
addr.getHash())) {
if (chkKey.getLabel().length() > 0) {
value = chkKey.getLabel();
}
break;
}
}
} else {
for (Address chkAddr : BTCLoader.addresses) {
if (Arrays.equals(chkAddr.getHash(), addr.getHash())) {
if (chkAddr.getLabel().length() > 0) {
value = chkAddr.getLabel();
}
break;
}
}
}
if (value == null) {
value = addr.toString();
}
break;
case 4:
value = tx.getValue();
break;
case 5:
if (tx instanceof SendTransaction) {
value = ((SendTransaction) tx).getFee();
} else {
value = null;
}
break;
case 6:
if (tx instanceof ReceiveTransaction) {
if (((ReceiveTransaction) tx).inSafe()) {
value = "Safe";
} else {
value = "Wallet";
}
} else {
value = "";
}
break;
case 7: // Status
try {
if (tx instanceof ReceiveTransaction
&& ((ReceiveTransaction) tx).isSpent()) {
value = "Spent";
} else {
int depth = BTCLoader.blockStore.getTxDepth(tx
.getTxHash());
if ((tx instanceof ReceiveTransaction)
&& ((ReceiveTransaction) tx).isCoinBase()) {
if (depth == 0) {
value = "Pending";
} else if (depth < BTCLoader.COINBASE_MATURITY) {
value = "Immature";
} else {
value = "Mature";
}
} else if (depth == 0) {
value = "Pending";
} else if (depth < BTCLoader.TRANSACTION_CONFIRMED) {
value = "Building";
} else {
value = "Confirmed";
}
}
} catch (BlockStoreException exc) {
ErrorLog.get().logException("Unable to get transaction depth",
exc);
value = "Unknown";
}
break;
default:
throw new IndexOutOfBoundsException("Table column " + column
+ " is not valid");
}
return value;
}
public void walletChanged() {
buildTxList();
fireTableDataChanged();
}
public BlockTransaction getTransaction(int row) {
return txList.get(row);
}
}
}