/**
* Copyright 2011 multibit.org
*
* Licensed under the MIT license (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://opensource.org/licenses/mit-license.php
*
* 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 org.multibit.viewsystem.swing.action;
import com.google.bitcoin.core.StoredBlock;
import com.google.bitcoin.core.Transaction;
import com.google.bitcoin.core.TransactionConfidence;
import com.google.bitcoin.core.Wallet;
import com.google.bitcoin.crypto.KeyCrypterException;
import org.multibit.controller.Controller;
import org.multibit.controller.bitcoin.BitcoinController;
import org.multibit.file.BackupManager;
import org.multibit.file.FileHandler;
import org.multibit.file.WalletLoadException;
import org.multibit.file.WalletSaveException;
import org.multibit.message.Message;
import org.multibit.message.MessageManager;
import org.multibit.model.bitcoin.BitcoinModel;
import org.multibit.model.bitcoin.WalletData;
import org.multibit.model.bitcoin.WalletInfoData;
import org.multibit.network.MultiBitCheckpointManager;
import org.multibit.network.ReplayManager;
import org.multibit.network.ReplayTask;
import org.multibit.store.WalletVersionException;
import org.multibit.viewsystem.swing.MultiBitFrame;
import org.multibit.viewsystem.swing.view.WalletFileFilter;
import org.multibit.viewsystem.swing.view.components.FontSizer;
import org.multibit.viewsystem.swing.view.components.MultiBitLabel;
import org.multibit.viewsystem.swing.view.panels.HelpContentsPanel;
import org.multibit.viewsystem.swing.view.walletlist.SingleWalletPanel;
import org.multibit.viewsystem.swing.view.walletlist.WalletListPanel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.io.File;
import java.io.IOException;
import java.nio.CharBuffer;
import java.util.*;
import java.util.List;
/**
* This {@link Action} opens a wallet from a file.
*/
public class OpenWalletAction extends AbstractAction {
public Logger log = LoggerFactory.getLogger(OpenWalletAction.class.getName());
private static final long serialVersionUID = 1913592460523457705L;
private final Controller controller;
private final BitcoinController bitcoinController;
private MultiBitFrame mainFrame;
private JFileChooser fileChooser;
private Font adjustedFont;
/**
* Creates a new {@link OpenWalletAction}.
*/
public OpenWalletAction(BitcoinController bitcoinController, ImageIcon icon, MultiBitFrame mainFrame) {
super(bitcoinController.getLocaliser().getString("openWalletAction.text"), icon);
this.bitcoinController = bitcoinController;
this.controller = this.bitcoinController;
this.mainFrame = mainFrame;
MnemonicUtil mnemonicUtil = new MnemonicUtil(controller.getLocaliser());
putValue(SHORT_DESCRIPTION, HelpContentsPanel.createTooltipTextForMenuItem(controller.getLocaliser().getString("openWalletAction.tooltip")));
putValue(MNEMONIC_KEY, mnemonicUtil.getMnemonic("openWalletAction.mnemonicKey"));
}
/**
* Show open file chooser and load wallet.
*/
@Override
public void actionPerformed(ActionEvent e) {
mainFrame.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
setEnabled(false);
try {
if (fileChooser == null) {
JFileChooser.setDefaultLocale(controller.getLocaliser().getLocale());
fileChooser = new JFileChooser();
fileChooser.setLocale(controller.getLocaliser().getLocale());
fileChooser.setDialogTitle(controller.getLocaliser().getString("openWalletAction.tooltip"));
adjustedFont = FontSizer.INSTANCE.getAdjustedDefaultFont();
if (adjustedFont != null) {
setFileChooserFont(new Container[] {fileChooser});
}
fileChooser.applyComponentOrientation(ComponentOrientation.getOrientation(controller.getLocaliser().getLocale()));
if (controller.getModel() != null && this.bitcoinController.getModel().getActiveWalletFilename() != null) {
fileChooser.setCurrentDirectory(new File(this.bitcoinController.getModel().getActiveWalletFilename()));
}
fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
fileChooser.setFileFilter(new WalletFileFilter(controller));
}
fileChooser.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
int returnVal = fileChooser.showOpenDialog(mainFrame);
mainFrame.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
if (returnVal == JFileChooser.APPROVE_OPTION) {
File file = fileChooser.getSelectedFile();
if (file != null) {
if (!file.isDirectory()) {
String selectedWalletFilename = file.getAbsolutePath();
// See if the wallet is already open.
boolean walletIsAlreadyOpen = false;
if (controller != null && controller.getModel() != null) {
List<WalletData> perWalletDataModels = this.bitcoinController.getModel().getPerWalletModelDataList();
if (perWalletDataModels != null) {
Iterator<WalletData> iterator = perWalletDataModels.iterator();
while(iterator.hasNext()) {
WalletData perWalletModelData = iterator.next();
if (perWalletModelData != null && perWalletModelData.getWalletFilename() != null) {
if (perWalletModelData.getWalletFilename().equals(selectedWalletFilename)) {
walletIsAlreadyOpen = true;
this.bitcoinController.getModel().setActiveWalletByFilename(selectedWalletFilename);
controller.fireDataChangedUpdateNow();
break;
} else {
// Check if the file encrypted version of the wallet is already open - if so use it.
if ((perWalletModelData.getWalletFilename() + "." + BackupManager.FILE_ENCRYPTED_WALLET_SUFFIX).equals(selectedWalletFilename)) {
walletIsAlreadyOpen = true;
this.bitcoinController.getModel().setActiveWalletByFilename(perWalletModelData.getWalletFilename()); controller.fireDataChangedUpdateNow();
break;
}
}
}
}
}
}
if (!walletIsAlreadyOpen) {
// If the wallet is file encrypted, work out the name of the decrypted file and see if it exists.
if (selectedWalletFilename.matches(BackupManager.REGEX_FOR_TIMESTAMP_AND_WALLET_AND_CIPHER_SUFFIX)) {
String decryptedWalletFileName = selectedWalletFilename.substring(0, selectedWalletFilename.length() - ("." + BackupManager.FILE_ENCRYPTED_WALLET_SUFFIX).length());
// if this file already exists open it.
if ((new File(decryptedWalletFileName).exists())) {
selectedWalletFilename = decryptedWalletFileName;
} else {
// Ask the user for the wallet password.
CharSequence passwordToUse = getPasswordFromUser();
if (passwordToUse == null) {
return;
}
// Read in the encrypted file and decrypt it.
try {
byte [] walletBytes = BackupManager.INSTANCE.readFileAndDecrypt(new File(selectedWalletFilename), passwordToUse);
// Make a regular wallet file.
FileHandler.writeFile(walletBytes, new File(decryptedWalletFileName));
// Now just use the decrypted file and open it.
selectedWalletFilename = decryptedWalletFileName;
} catch (IOException e1) {
MessageManager.INSTANCE.addMessage(new Message(controller.getLocaliser().getString("openWalletSubmitAction.walletNotLoaded", new Object[]{selectedWalletFilename, e1.getMessage()})));
return;
} catch (KeyCrypterException e2) {
MessageManager.INSTANCE.addMessage(new Message(controller.getLocaliser().getString("openWalletSubmitAction.walletNotLoaded", new Object[]{selectedWalletFilename, e2.getMessage()})));
return;
}
}
}
Message openMessage = new Message(controller.getLocaliser().getString("multiBit.openingWallet", new Object[]{selectedWalletFilename}));
openMessage.setShowInStatusBar(false);
MessageManager.INSTANCE.addMessage(openMessage);
openWalletInBackground(selectedWalletFilename);
}
}
} else {
fileChooser = null;
}
} else {
fileChooser = null;
}
} finally {
setEnabled(true);
mainFrame.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
}
}
private CharSequence getPasswordFromUser() {
// Using a JPanel as the message for the JOptionPane
JPanel passwordPanel = new JPanel();
passwordPanel.setOpaque(false);
passwordPanel.setLayout(new GridBagLayout());
GridBagConstraints constraints = new GridBagConstraints();
FontMetrics fontMetrics = passwordPanel.getFontMetrics(FontSizer.INSTANCE.getAdjustedDefaultFont());
int minimumHeight = fontMetrics.getHeight() * 6;
int minimumWidth = fontMetrics.stringWidth(controller.getLocaliser().getString("openWalletAction.enterPassword.message")) + 50;
passwordPanel.setMinimumSize(new Dimension(minimumWidth, minimumHeight));
passwordPanel.setPreferredSize(new Dimension(minimumWidth, minimumHeight));
passwordPanel.setMaximumSize(new Dimension(minimumWidth, minimumHeight));
MultiBitLabel explainLabel = new MultiBitLabel("");
explainLabel.setText(controller.getLocaliser().getString("openWalletAction.enterPassword.message"));
constraints.fill = GridBagConstraints.HORIZONTAL;
constraints.gridx = 1;
constraints.gridy = 1;
constraints.weightx = 0.08;
constraints.weighty = 0.3;
constraints.gridwidth = 2;
constraints.gridheight = 1;
constraints.anchor = GridBagConstraints.LINE_START;
passwordPanel.add(explainLabel, constraints);
MultiBitLabel passwordLabel = new MultiBitLabel("");
passwordLabel.setText(controller.getLocaliser().getString("showExportPrivateKeysPanel.passwordPrompt") + " "); // Two spaces.
constraints.fill = GridBagConstraints.NONE;
constraints.gridx = 1;
constraints.gridy = 2;
constraints.weightx = 0.08;
constraints.weighty = 0.3;
constraints.gridwidth = 1;
constraints.gridheight = 1;
constraints.anchor = GridBagConstraints.LINE_END;
passwordPanel.add(passwordLabel, constraints);
JPasswordField passwordField = new JPasswordField();
constraints.fill = GridBagConstraints.HORIZONTAL;
constraints.gridx = 2;
constraints.gridy = 2;
constraints.weightx = 1;
constraints.weighty = 0.3;
constraints.gridwidth = 1;
constraints.gridheight = 1;
constraints.anchor = GridBagConstraints.LINE_START;
passwordPanel.add(passwordField, constraints);
int input = JOptionPane.showConfirmDialog(mainFrame, passwordPanel,
controller.getLocaliser().getString("showExportPrivateKeysAction.youMustEnterTheWalletPassword"), JOptionPane.OK_CANCEL_OPTION,
JOptionPane.PLAIN_MESSAGE);
if (input == 0) {
// Retrieve password.
return CharBuffer.wrap(passwordField.getPassword());
} else {
// Either the cancel button or the 'x' has been pressed.
return null;
}
}
/**
* Open a wallet in a background Swing worker thread.
* @param selectedWalletFilename Filename of wallet to open
*/
private void openWalletInBackground(String selectedWalletFilename) {
final String selectedWalletFilenameFinal = selectedWalletFilename;
SwingWorker<Boolean, Void> worker = new SwingWorker<Boolean, Void>() {
private String message = null;
@Override
protected Boolean doInBackground() throws Exception {
try {
log.debug("Opening wallet '" + selectedWalletFilenameFinal + "'.");
// Check if this is the first time this wallet has been opened post addition of data directories.
String topLevelWalletDirectory = BackupManager.INSTANCE.calculateTopLevelBackupDirectoryName(new File(selectedWalletFilenameFinal));
boolean firstUsageSinceWalletDirectoriesIntroduced = !(new File(topLevelWalletDirectory).exists());
WalletData perWalletModelData = bitcoinController.addWalletFromFilename(selectedWalletFilenameFinal);
log.debug("Setting active wallet for file '" + selectedWalletFilenameFinal + "'.");
bitcoinController.getModel().setActiveWalletByFilename(selectedWalletFilenameFinal);
// Save the user properties to disk.
log.debug("Writing user preferences. . .");
FileHandler.writeUserPreferences(bitcoinController);
log.debug("User preferences with new wallet written successfully");
// Clean out the "1Enjoy 1Sochi" spam - always do this (even if it has been done before)
// so that user can manually clean a wallet by closing it and then reopening it)
WalletInfoData walletInfo = perWalletModelData.getWalletInfo();
log.debug("Cleaning wallet '" + selectedWalletFilenameFinal + "' of spam ...");
perWalletModelData.getWallet().cleanup();
walletInfo.put(BitcoinModel.WALLET_CLEANED_OF_SPAM, Boolean.TRUE.toString());
bitcoinController.getFileHandler().savePerWalletModelData(perWalletModelData, false);
log.debug("done.");
// Backup the wallet and wallet info.
BackupManager.INSTANCE.backupPerWalletModelData(bitcoinController.getFileHandler(), perWalletModelData);
if (firstUsageSinceWalletDirectoriesIntroduced) {
// Move any timestamped key and wallet files into their appropriate directories
BackupManager.INSTANCE.moveSiblingTimestampedKeyAndWalletBackups(selectedWalletFilenameFinal);
}
message = controller.getLocaliser().getString("multiBit.openingWalletIsDone", new Object[]{selectedWalletFilenameFinal});
return Boolean.TRUE;
} catch (WalletLoadException e) {
message = controller.getLocaliser().getString("openWalletSubmitAction.walletNotLoaded", new Object[]{selectedWalletFilenameFinal, e.getMessage()});
return Boolean.FALSE;
} catch (WalletVersionException e) {
message = controller.getLocaliser().getString("openWalletSubmitAction.walletNotLoaded", new Object[]{selectedWalletFilenameFinal, e.getMessage()});
return Boolean.FALSE;
} catch (IOException e) {
message = controller.getLocaliser().getString("openWalletSubmitAction.walletNotLoaded", new Object[]{selectedWalletFilenameFinal, e.getMessage()});
return Boolean.FALSE;
} catch (WalletSaveException e) {
message = controller.getLocaliser().getString("openWalletSubmitAction.walletNotLoaded", new Object[]{selectedWalletFilenameFinal, e.getMessage()});
return Boolean.FALSE;
} catch (Exception e) {
message = controller.getLocaliser().getString("openWalletSubmitAction.walletNotLoaded", new Object[]{selectedWalletFilenameFinal, e.getMessage()});
return Boolean.FALSE;
}
}
@Override
protected void done() {
try {
Boolean wasSuccessful = get();
if (wasSuccessful) {
log.debug(message);
Message messageMessage = new Message(message);
messageMessage.setShowInStatusBar(false);
MessageManager.INSTANCE.addMessage(messageMessage);
// Work out the late date/ block the wallet saw to see if it needs syncing.
WalletData perWalletModelData = bitcoinController.getModel().getActivePerWalletModelData();
Wallet wallet = perWalletModelData.getWallet();
int lastBlockSeenHeight = wallet.getLastBlockSeenHeight();
log.debug("For wallet '" + perWalletModelData.getWalletFilename() + " the lastBlockSeenHeight was " + lastBlockSeenHeight);
// If there was no lastBlockSeenHeight, find the latest confirmed transaction height.
if (lastBlockSeenHeight <= 0) {
Set<Transaction> transactions = perWalletModelData.getWallet().getTransactions(true);
if (transactions != null) {
for (Transaction transaction : transactions) {
TransactionConfidence confidence = transaction.getConfidence();
if (confidence != null) {
if (TransactionConfidence.ConfidenceType.BUILDING.equals(confidence.getConfidenceType())) {
lastBlockSeenHeight = Math.max(lastBlockSeenHeight, confidence.getAppearedAtChainHeight());
}
}
}
}
}
log.debug("For wallet '" + perWalletModelData.getWalletFilename() + ", after transactions, the lastBlockSeenHeight was " + lastBlockSeenHeight);
int currentChainHeight = -1;
if (bitcoinController.getMultiBitService().getChain() != null) {
if (bitcoinController.getMultiBitService().getChain().getChainHead() != null) {
currentChainHeight = bitcoinController.getMultiBitService().getChain().getChainHead().getHeight();
}
}
log.debug("The current chain height is " + currentChainHeight);
boolean needToSync = false;
long requiredSyncTimeInSeconds = -1;
// Check if we have both the lastBlockSeenHeight and the currentChainHeight.
if (lastBlockSeenHeight > 0 && currentChainHeight > 0) {
if (lastBlockSeenHeight >= currentChainHeight) {
// Wallet is at or ahead of current chain - but check there isnt a replay at the moment.
// If there is, use the actual last chain height to check.
// (The user might have opened a wallet whilst a replay was occurring).
if (ReplayManager.INSTANCE.getCurrentReplayTask() != null) {
int actualLastChainHeight = ReplayManager.INSTANCE.getActualLastChainHeight();
if (lastBlockSeenHeight < actualLastChainHeight) {
needToSync = true;
}
}
} else {
// Wallet is behind the current chain - need to sync.
needToSync = true;
}
} else {
// If we still dont have a sync date/height, use the key birth date of the wallet
// and sync from that. This might be expensive, but should only be used for empty
// wallets.
requiredSyncTimeInSeconds = wallet.getEarliestKeyCreationTime();
needToSync = true;
log.debug("requiredSyncTimeInSeconds worked out from earliestKeyCreationTime = " + requiredSyncTimeInSeconds);
}
log.debug("needToSync = " + needToSync);
if (needToSync) {
StoredBlock syncFromStoredBlock = null;
MultiBitCheckpointManager checkpointManager = bitcoinController.getMultiBitService().getCheckpointManager();
if (checkpointManager != null) {
if (lastBlockSeenHeight > 0) {
syncFromStoredBlock = checkpointManager.getCheckpointBeforeOrAtHeight(lastBlockSeenHeight);
} else {
if (requiredSyncTimeInSeconds >= 0) {
syncFromStoredBlock = checkpointManager.getCheckpointBefore(requiredSyncTimeInSeconds);
}
}
}
log.debug("syncFromStoredBlock =" + syncFromStoredBlock);
// Initialise the message in the singleWalletPanel.
if (mainFrame != null) {
WalletListPanel walletListPanel = mainFrame.getWalletsView();
if (walletListPanel != null) {
SingleWalletPanel singleWalletPanel = walletListPanel.findWalletPanelByFilename(selectedWalletFilenameFinal);
if (singleWalletPanel != null) {
singleWalletPanel.setSyncMessage(controller.getLocaliser().getString("multiBitDownloadListener.downloadingTextShort"), Message.NOT_RELEVANT_PERCENTAGE_COMPLETE);
}
}
}
List<WalletData> perWalletModelDataList = new ArrayList<WalletData>();
perWalletModelDataList.add(perWalletModelData);
ReplayTask replayTask;
if (syncFromStoredBlock == null) {
// Sync from genesis block.
replayTask = new ReplayTask(perWalletModelDataList, null, 0);
} else {
Date syncDate = null;
if (syncFromStoredBlock.getHeader() != null) {
syncDate = new Date(syncFromStoredBlock.getHeader().getTimeSeconds() * 1000);
}
replayTask = new ReplayTask(perWalletModelDataList, syncDate, syncFromStoredBlock.getHeight());
}
ReplayManager.INSTANCE.offerReplayTask(replayTask);
}
controller.fireRecreateAllViews(true);
} else {
log.error(message);
MessageManager.INSTANCE.addMessage(new Message(message));
WalletData loopData = bitcoinController.getModel().getPerWalletModelDataByWalletFilename(selectedWalletFilenameFinal);
if (loopData != null) {
// Clear the backup wallet filename - this prevents it being automatically overwritten.
if (loopData.getWalletInfo() != null) {
loopData.getWalletInfo().put(BitcoinModel.WALLET_BACKUP_FILE, "");
}
}
}
} catch (Exception e) {
// Not really used but caught so that SwingWorker shuts down cleanly.
log.error(e.getClass() + " " + e.getMessage());
} finally {
controller.fireDataChangedUpdateNow();
setEnabled(true);
if (mainFrame != null) {
mainFrame.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
}
}
}
};
log.debug("Executing open of wallet '" + selectedWalletFilenameFinal + "'.");
worker.execute();
}
private void setFileChooserFont(Component[] comp) {
for (int x = 0; x < comp.length; x++) {
if (comp[x] instanceof Container) {
setFileChooserFont(((Container) comp[x]).getComponents());
}
try {
comp[x].setFont(adjustedFont);
} catch (Exception e) {
// Do nothing.
}
}
}
}