/* * Copyright 2014 Christopher Mann * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package de.uni_bonn.bit; import ca.odell.glazedlists.BasicEventList; import ca.odell.glazedlists.gui.TableFormat; import ca.odell.glazedlists.swing.AdvancedTableModel; import ca.odell.glazedlists.swing.GlazedListsSwing; import org.bitcoinj.core.*; import org.bitcoinj.params.RegTestParams; import org.bitcoinj.store.*; import com.intellij.uiDesigner.core.GridConstraints; import com.intellij.uiDesigner.core.GridLayoutManager; import com.intellij.uiDesigner.core.Spacer; import org.apache.log4j.Level; import org.apache.log4j.Logger; import org.apache.log4j.PatternLayout; import org.apache.log4j.WriterAppender; import org.apache.log4j.spi.LoggingEvent; import org.bitcoinj.wallet.Protos; import javax.swing.*; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.Calendar; import java.util.concurrent.TimeUnit; import static org.bitcoinj.core.Wallet.BalanceType; /** * TThis class is the main window for the desktop part of the two-factor wallet. It displays the user's balance and a * list of the transactions. Most of the setup, include the pairing if the wallet has not been paired yet, is done * inside {@link de.uni_bonn.bit.MainWindow#startup()} */ public class MainWindow { JPanel panel1; private JLabel lblBalance; private JButton btnReceive; private JButton btnSend; private JTable tblTransactions; private JTextArea txtLog; private Wallet wallet; private BlockStore blockStore; private PeerGroup peerGroup; private final BasicEventList<Transaction> transactionList = new BasicEventList<>(); private final Logger log = Logger.getLogger(MainWindow.class); public MainWindow() { //setup logger TextAreaAppender textAreaAppender = new TextAreaAppender(); textAreaAppender.setThreshold(Level.INFO); textAreaAppender.setLayout(new PatternLayout("%d{HH:mm:ss,SS} %-5p: %m%n")); Logger.getRootLogger().addAppender(textAreaAppender); } /** * This method starts the desktop wallet. If the wallet has not yet been paired with a phone wallet, it will display * the pairing dialog. Afterwards, it loads the wallet file and the block store from disk and connects to the Bitcoin * network. */ public void startup() { wallet = new Wallet(TransactionHelper.netParams); KeyShareWalletExtension walletExtension = new KeyShareWalletExtension(); wallet.addExtension(walletExtension); File walletFile = new File("wallet.bin"); if (!walletFile.exists()) { //No wallet found -> no pairing established yet -> open pairing dialog PairingDialog pairingDialog = new PairingDialog(walletExtension); pairingDialog.setVisible(true); if (pairingDialog.getResult() == PairingDialog.Result.FAIL) { System.exit(0); } //Compute common public key and add it to the wallet ECKey commonPublicKey = BitcoinECMathHelper.convertPointToPubKEy( BitcoinECMathHelper.convertPubKeyToPoint(walletExtension.getOtherPublicKey()) .multiply(BitcoinECMathHelper.convertPrivKeyToBigInt(walletExtension.getPrivateKey()))); commonPublicKey.setCreationTimeSeconds(Calendar.getInstance().getTimeInMillis() / 1000); wallet.importKey(commonPublicKey); try { wallet.saveToFile(walletFile); } catch (IOException e) { log.error("Wallet could not be saved to file after pairing.", e); throw new RuntimeException(e); } } else { try { FileInputStream walletStream = new FileInputStream(walletFile); Protos.Wallet walletProto = WalletProtobufSerializer.parseToProto(walletStream); wallet = new WalletProtobufSerializer().readWallet(TransactionHelper.netParams, new WalletExtension[]{new KeyShareWalletExtension()}, walletProto); } catch (UnreadableWalletException e) { log.error("Wallet file could not be loaded.", e); throw new RuntimeException(e); } catch (FileNotFoundException e) { //Should never happen. We checked that the file exists. throw new RuntimeException(e); } catch (IOException e) { log.error("Wallet file could not be loaded.", e); throw new RuntimeException(e); } } wallet.autosaveToFile(walletFile, 1, TimeUnit.MINUTES, null); blockStore = null; try { blockStore = new SPVBlockStore(TransactionHelper.netParams, new File("blockstore.bin")); BlockChain chain = new BlockChain(TransactionHelper.netParams, wallet, blockStore); peerGroup = new PeerGroup(TransactionHelper.netParams, chain); peerGroup.addWallet(wallet); peerGroup.addAddress(new PeerAddress(InetAddress.getByName("127.0.0.1"), 9000)); peerGroup.startAsync(); peerGroup.startBlockChainDownload(null); } catch (BlockStoreException e) { log.error("Blockstore exception when creating a new block store.", e); throw new RuntimeException(e); } catch (UnknownHostException e) { throw new RuntimeException(e); } initializeGui(); } /** * This method correctly initializes the transaction table and the balance label. */ private void initializeGui() { AdvancedTableModel<Transaction> transactionTableModel = GlazedListsSwing.eventTableModelWithThreadProxyList(transactionList, new TransactionTableFormat()); tblTransactions.setModel(transactionTableModel); //hack to ensure correct column sizes. Real columns sizes are proportions of the preferred width. tblTransactions.getColumnModel().getColumn(0).setPreferredWidth(100); tblTransactions.getColumnModel().getColumn(1).setPreferredWidth(400); tblTransactions.getColumnModel().getColumn(2).setPreferredWidth(200); tblTransactions.getColumnModel().getColumn(3).setPreferredWidth(300); transactionList.addAll(wallet.getTransactions(false)); lblBalance.setText(wallet.getBalance(BalanceType.AVAILABLE).toFriendlyString()); btnSend.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent actionEvent) { new TransactionDialog(wallet, peerGroup).setVisible(true); } }); btnReceive.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent actionEvent) { new ReceivePaymentDialog(wallet.getImportedKeys().get(0).toAddress(RegTestParams.get()).toString()).setVisible(true); } }); wallet.addEventListener( new AbstractWalletEventListener() { @Override public void onWalletChanged(final Wallet wallet) { super.onWalletChanged(wallet); SwingUtilities.invokeLater(new Runnable() { @Override public void run() { lblBalance.setText(wallet.getBalance(BalanceType.AVAILABLE).toFriendlyString()); } }); } @Override public void onCoinsReceived(Wallet wallet, final Transaction tx, Coin prevBalance, Coin newBalance) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { if (transactionList.isEmpty() || !transactionList.get(0).equals(tx)) { transactionList.add(0, tx); } } }); } @Override public void onCoinsSent(Wallet wallet, final Transaction tx, Coin prevBalance, Coin newBalance) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { if (transactionList.isEmpty() || !transactionList.get(0).equals(tx)) { transactionList.add(0, tx); } } }); } @Override public void onTransactionConfidenceChanged(Wallet wallet, final Transaction tx) { super.onTransactionConfidenceChanged(wallet, tx); SwingUtilities.invokeLater(new Runnable() { @Override public void run() { //transactions are equal if their bitcoin serialization hash is equal. The hash stays the same //when the confidence changes int indexOfTx = transactionList.indexOf(tx); transactionList.set(indexOfTx, tx); } }); } } ); } public void onClose() { if (peerGroup != null) { peerGroup.stopAndWait(); } if (wallet != null) { try { wallet.saveToFile(new File("wallet.bin.tmp"), new File("wallet.bin")); } catch (IOException e) { log.error("The wallet could not be saved!", e); } } if (blockStore != null) { try { blockStore.close(); } catch (BlockStoreException e) { log.error("Exception while closing the block store.", e); } } } { // GUI initializer generated by IntelliJ IDEA GUI Designer // >>> IMPORTANT!! <<< // DO NOT EDIT OR ADD ANY CODE HERE! $$$setupUI$$$(); } /** * Method generated by IntelliJ IDEA GUI Designer * >>> IMPORTANT!! <<< * DO NOT edit this method OR call it in your code! * * @noinspection ALL */ private void $$$setupUI$$$() { panel1 = new JPanel(); panel1.setLayout(new GridLayoutManager(2, 1, new Insets(5, 5, 5, 5), -1, -1)); panel1.setMinimumSize(new Dimension(600, 500)); panel1.setPreferredSize(new Dimension(600, 500)); final JPanel panel2 = new JPanel(); panel2.setLayout(new GridLayoutManager(1, 6, new Insets(0, 0, 0, 0), -1, -1)); panel1.add(panel2, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, null, null, null, 0, false)); final JLabel label1 = new JLabel(); label1.setText(""); panel2.add(label1, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); final JLabel label2 = new JLabel(); label2.setText("Balance:"); panel2.add(label2, new GridConstraints(0, 1, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_VERTICAL, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); lblBalance = new JLabel(); lblBalance.setText("0 BTC"); panel2.add(lblBalance, new GridConstraints(0, 2, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_VERTICAL, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); final Spacer spacer1 = new Spacer(); panel2.add(spacer1, new GridConstraints(0, 3, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, 1, null, null, null, 0, false)); btnSend = new JButton(); btnSend.setText("Send"); panel2.add(btnSend, new GridConstraints(0, 4, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); btnReceive = new JButton(); btnReceive.setText("Receive"); panel2.add(btnReceive, new GridConstraints(0, 5, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); final JPanel panel3 = new JPanel(); panel3.setLayout(new GridLayoutManager(2, 1, new Insets(0, 0, 0, 0), -1, -1)); panel1.add(panel3, new GridConstraints(1, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, null, null, null, 0, false)); final JSplitPane splitPane1 = new JSplitPane(); splitPane1.setDividerLocation(200); splitPane1.setDividerSize(5); splitPane1.setOrientation(0); panel3.add(splitPane1, new GridConstraints(1, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, null, new Dimension(200, 200), null, 0, false)); final JScrollPane scrollPane1 = new JScrollPane(); splitPane1.setLeftComponent(scrollPane1); tblTransactions = new JTable(); scrollPane1.setViewportView(tblTransactions); final JScrollPane scrollPane2 = new JScrollPane(); splitPane1.setRightComponent(scrollPane2); txtLog = new JTextArea(); scrollPane2.setViewportView(txtLog); final JLabel label3 = new JLabel(); label3.setText("Transactions:"); panel3.add(label3, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); } /** * @noinspection ALL */ public JComponent $$$getRootComponent$$$() { return panel1; } public class TransactionTableFormat implements TableFormat<Transaction> { @Override public int getColumnCount() { return 4; } @Override public String getColumnName(int column) { switch (column) { case 0: return "In/Out"; case 1: return "Date"; case 2: return "Confidence"; case 3: return "Value"; } return ""; } @Override public Object getColumnValue(Transaction baseObject, int column) { switch (column) { case 0: // "In/Out" long valueSent = baseObject.getValueSentFromMe(wallet).getValue(); long valueReceived = baseObject.getValueSentToMe(wallet).getValue(); if (valueSent == valueReceived) { return "<-|>|"; } else if (valueSent > 0) { return "<-|-|"; } else if (valueReceived > 0) { return "--|>|"; } else { return " ??? "; } case 1: return baseObject.getUpdateTime().toString(); case 2: //Confirmation if (baseObject.hasConfidence()) { if (baseObject.getConfidence().getConfidenceType() == TransactionConfidence.ConfidenceType.BUILDING) { return " ✔ "; } else if (baseObject.getConfidence().getConfidenceType() == TransactionConfidence.ConfidenceType.PENDING) { return " ... "; } return "???"; } case 3: // Value return baseObject.getValue(wallet).toFriendlyString(); } return ""; } } public class TextAreaAppender extends WriterAppender { @Override public void append(LoggingEvent event) { final String messageString = this.layout.format(event); SwingUtilities.invokeLater(new Runnable() { @Override public void run() { txtLog.append(messageString); } }); } } }