/*
* Copyright 2011 Google Inc.
*
* 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 com.google.devcoin.examples.toywallet;
import com.google.devcoin.core.*;
import com.google.devcoin.discovery.DnsDiscovery;
import com.google.devcoin.params.MainNetParams;
import com.google.devcoin.params.TestNet3Params;
import com.google.devcoin.store.H2FullPrunedBlockStore;
import com.google.devcoin.store.SPVBlockStore;
import com.google.devcoin.store.UnreadableWalletException;
import com.google.devcoin.utils.BriefLogFormatter;
import com.google.common.collect.Lists;
import org.spongycastle.util.encoders.Hex;
import javax.swing.*;
import javax.swing.table.AbstractTableModel;
import java.awt.*;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.StringSelection;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.File;
import java.math.BigInteger;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* A GUI demo that lets you watch received transactions as they accumulate confidence.
*/
public class ToyWallet {
private NetworkParameters params;
private Wallet wallet;
private PeerGroup peerGroup;
private AbstractBlockChain chain;
private JLabel networkStats;
private File walletFile;
private JScrollPane txScrollPane;
private JTable txTable;
private TransactionTableModel txTableModel;
public static void main(String[] args) throws Exception {
BriefLogFormatter.init();
new ToyWallet(true, true, args);
}
// Converts the contents of the wallet to a table for the GUI.
public class TransactionTableModel extends AbstractTableModel {
private List<Transaction> transactions = Lists.newLinkedList();
public int getRowCount() {
return transactions.size();
}
@Override
public String getColumnName(int i) {
switch (i) {
case 0: return "Confidence";
case 1: return "Description";
case 2: return "Value";
default: throw new RuntimeException("Unreachable");
}
}
public int getColumnCount() {
// Column 1: confidence
// Column 2: description
// Column 3: balance adjustment (+ve or -ve)
return 3;
}
public Object getValueAt(int row, int col) {
Transaction tx = transactions.get(row);
switch (col) {
case 0:
TransactionConfidence conf = tx.getConfidence();
return conf.toString();
case 1:
return String.format("TX with %d inputs and %d outputs",
tx.getInputs().size(), tx.getOutputs().size());
case 2:
try {
BigInteger val = tx.getValue(wallet);
return Utils.bitcoinValueToFriendlyString(val);
} catch (ScriptException e) {
throw new RuntimeException(e);
}
default:
throw new RuntimeException("Unreachable");
}
}
public void setTransactions(List<Transaction> txns) {
transactions = txns;
fireTableDataChanged();
}
}
public ToyWallet(boolean testnet, boolean fullChain, String[] args) throws Exception {
// Set up a Bitcoin connection + empty wallet. TODO: Simplify the setup for this use case.
if (testnet) {
params = TestNet3Params.get();
} else {
params = MainNetParams.get();
}
// Try to read the wallet from storage, create a new one if not possible.
boolean freshWallet = false;
walletFile = new File("toy.wallet");
try {
wallet = Wallet.loadFromFile(walletFile);
} catch (UnreadableWalletException e) {
wallet = new Wallet(params);
// Allow user to specify the first key on the command line as:
// hex-encoded-key:creation-time-seconds
ECKey key;
if (args.length > 0) {
try {
String[] parts = args[0].split(":");
byte[] pubKey = Hex.decode(parts[0]);
key = new ECKey(null, pubKey);
long creationTimeSeconds = Long.parseLong(parts[1]);
key.setCreationTimeSeconds(creationTimeSeconds);
System.out.println(String.format("Using address from command line %s, created on %s",
key.toAddress(params).toString(), new Date(creationTimeSeconds*1000).toString()));
} catch (Exception e2) {
System.err.println("Could not understand argument. Try a hex encoded pub key with a creation " +
"time in seconds appended with a colon in between: " + e2.toString());
return;
}
} else {
key = new ECKey(); // Generate a fresh key.
}
wallet.addKey(key);
wallet.saveToFile(walletFile);
freshWallet = true;
}
System.out.println("Send to: " + wallet.getKeys().get(0).toAddress(params));
System.out.println(wallet);
wallet.autosaveToFile(walletFile, 500, TimeUnit.MILLISECONDS, null);
File blockChainFile = new File("toy.blockchain");
if (!blockChainFile.exists() && !freshWallet) {
// No block chain, but we had a wallet. So empty out the transactions in the wallet so when we rescan
// the blocks there are no problems (wallets don't support replays without being emptied).
wallet.clearTransactions(0);
}
if (fullChain) {
H2FullPrunedBlockStore store = new H2FullPrunedBlockStore(params, blockChainFile.getName(), 100);
chain = new FullPrunedBlockChain(params, wallet, store);
} else {
chain = new BlockChain(params, wallet, new SPVBlockStore(params, blockChainFile));
}
peerGroup = new PeerGroup(params, chain);
peerGroup.setUserAgent("ToyWallet", "1.0");
peerGroup.addPeerDiscovery(new DnsDiscovery(params));
peerGroup.addWallet(wallet);
// Watch for peers coming and going so we can update the UI.
peerGroup.addEventListener(new AbstractPeerEventListener() {
@Override
public void onPeerConnected(Peer peer, int peerCount) {
super.onPeerConnected(peer, peerCount);
triggerNetworkStatsUpdate();
}
@Override
public void onPeerDisconnected(Peer peer, int peerCount) {
super.onPeerDisconnected(peer, peerCount);
triggerNetworkStatsUpdate();
}
@Override
public void onBlocksDownloaded(Peer peer, Block block, int blocksLeft) {
super.onBlocksDownloaded(peer, block, blocksLeft);
triggerNetworkStatsUpdate();
}
});
wallet.addEventListener(new AbstractWalletEventListener() {
@Override
public void onWalletChanged(Wallet wallet) {
// MUST BE THREAD SAFE.
final List<Transaction> txns = wallet.getTransactionsByTime();
SwingUtilities.invokeLater(new Runnable() {
public void run() {
txTableModel.setTransactions(txns);
}
});
}
});
// Create the GUI.
JFrame window = new JFrame("Toy wallet");
window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setupWindow(window);
window.pack();
window.setSize(640, 480);
txTableModel.setTransactions(wallet.getTransactionsByTime());
// Go!
window.setVisible(true);
peerGroup.start();
peerGroup.downloadBlockChain();
}
private void triggerNetworkStatsUpdate() {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
int numPeers = peerGroup.numConnectedPeers();
StoredBlock chainHead = chain.getChainHead();
String date = chainHead.getHeader().getTime().toString();
String status = String.format("%d peer(s) connected. %d blocks: %s",
numPeers, chainHead.getHeight(), date);
networkStats.setText(status);
}
});
}
private void setupWindow(JFrame window) {
final Address address = wallet.getKeys().get(0).toAddress(params);
JLabel instructions = new JLabel(
"<html>Broadcast transactions appear below. Watch them gain confidence.<br>" +
"Send coins to: <b>" + address + "</b> <i>(click to place on clipboard)</i>");
// Just make the label clickable so it puts the address in the clipboard.
instructions.addMouseListener(new MouseAdapter() {
public void mouseClicked(MouseEvent mouseEvent) {
Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
StringSelection sel = new StringSelection(address.toString());
clipboard.setContents(sel, sel);
}
});
window.getContentPane().add(instructions, BorderLayout.NORTH);
txTableModel = new TransactionTableModel();
txTableModel.transactions = new LinkedList<Transaction>();
txTable = new JTable(txTableModel);
// The list of transactions.
txScrollPane = new JScrollPane(txTable);
window.getContentPane().add(txScrollPane, BorderLayout.CENTER);
networkStats = new JLabel("Connecting to the Bitcoin network ...");
window.getContentPane().add(networkStats, BorderLayout.SOUTH);
}
}