/**
* 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.ECKey;
import com.google.bitcoin.crypto.KeyCrypter;
import com.google.bitcoin.crypto.KeyCrypterException;
import org.bitcoinj.wallet.Protos.Wallet.EncryptionType;
import org.multibit.controller.bitcoin.BitcoinController;
import org.multibit.file.BackupManager;
import org.multibit.file.FileHandler;
import org.multibit.file.WalletSaveException;
import org.multibit.message.Message;
import org.multibit.message.MessageManager;
import org.multibit.model.bitcoin.*;
import org.multibit.store.MultiBitWalletVersion;
import org.multibit.utils.ImageLoader;
import org.multibit.viewsystem.swing.view.dialogs.CreateNewReceivingAddressDialog;
import org.multibit.viewsystem.swing.view.panels.CreateNewReceivingAddressPanel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.spongycastle.crypto.params.KeyParameter;
import javax.swing.*;
import java.awt.event.ActionEvent;
import java.io.File;
import java.io.IOException;
import java.nio.CharBuffer;
import java.util.ArrayList;
import java.util.List;
/**
* This {@link Action} represents an action to actually create receiving
* addresses.
*/
public class CreateNewReceivingAddressSubmitAction extends MultiBitSubmitAction implements WalletBusyListener {
private static Logger log = LoggerFactory.getLogger(CreateNewReceivingAddressSubmitAction.class);
private static final long serialVersionUID = 200152235465875405L;
private CreateNewReceivingAddressDialog createNewReceivingAddressDialog;
private CreateNewReceivingAddressPanel createNewReceivingAddressPanel;
private JPasswordField walletPassword;
/**
* The last private keys backup file used - used in testing.
*/
private File lastPrivateKeysBackupFile;
/**
* Creates a new {@link CreateNewReceivingAddressSubmitAction}.
*/
public CreateNewReceivingAddressSubmitAction(BitcoinController bitcoinController,
CreateNewReceivingAddressDialog createNewReceivingAddressDialog,
CreateNewReceivingAddressPanel createNewReceivingAddressPanel, JPasswordField walletPassword) {
super(bitcoinController, "createNewReceivingAddressSubmitAction.text", "createNewReceivingAddressSubmitAction.tooltip",
"createNewReceivingAddressSubmitAction.mnemonicKey", ImageLoader.createImageIcon(ImageLoader.ADD_ICON_FILE));
this.createNewReceivingAddressDialog = createNewReceivingAddressDialog;
this.createNewReceivingAddressPanel = createNewReceivingAddressPanel;
this.walletPassword = walletPassword;
this.lastPrivateKeysBackupFile = null;
// This action is a WalletBusyListener
super.bitcoinController.registerWalletBusyListener(this);
walletBusyChange(super.bitcoinController.getModel().getActivePerWalletModelData().isBusy());
}
/**
* Create new receiving addresses.
*/
@Override
public void actionPerformed(ActionEvent e) {
if (abort()) {
return;
}
WalletData perWalletModelData = super.bitcoinController.getModel().getActivePerWalletModelData();
boolean encryptNewKeys = false;
if (super.bitcoinController.getModel().getActiveWallet() != null
&& super.bitcoinController.getModel().getActiveWallet().getEncryptionType() == EncryptionType.ENCRYPTED_SCRYPT_AES) {
if (walletPassword.getPassword() == null || walletPassword.getPassword().length == 0) {
// User needs to enter password.
createNewReceivingAddressPanel.setMessageText(controller.getLocaliser().getString(
"showExportPrivateKeysAction.youMustEnterTheWalletPassword"));
return;
}
encryptNewKeys = true;
try {
if (!super.bitcoinController.getModel().getActiveWallet().checkPassword(CharBuffer.wrap(walletPassword.getPassword()))) {
// The password supplied is incorrect.
createNewReceivingAddressPanel.setMessageText(controller.getLocaliser().getString(
"createNewReceivingAddressSubmitAction.passwordIsIncorrect"));
return;
}
} catch (KeyCrypterException ede) {
log.debug(ede.getClass().getCanonicalName() + " " + ede.getMessage());
// The password supplied is probably incorrect.
createNewReceivingAddressPanel.setMessageText(controller.getLocaliser().getString(
"createNewReceivingAddressSubmitAction.passwordIsIncorrect"));
return;
}
}
WalletInfoData walletInfo = perWalletModelData.getWalletInfo();
if (walletInfo == null) {
walletInfo = new WalletInfoData(perWalletModelData.getWalletFilename(), perWalletModelData.getWallet(), MultiBitWalletVersion.PROTOBUF_ENCRYPTED);
perWalletModelData.setWalletInfo(walletInfo);
}
// Double check wallet is not busy then declare that the active wallet is busy with the addReceivingAddresses task
if (!perWalletModelData.isBusy()) {
perWalletModelData.setBusy(true);
perWalletModelData.setBusyTaskKey("createNewReceivingAddressSubmitAction.tooltip");
// Can no longer cancel as the task has started.
createNewReceivingAddressPanel.getCancelButton().setEnabled(false);
int numberOfAddressesToCreate = createNewReceivingAddressPanel.getNumberOfAddressesToCreate();
String walletDescription = super.bitcoinController.getModel().getActiveWalletWalletInfo().getProperty(WalletInfoData.DESCRIPTION_PROPERTY);
String shortMessage = controller.getLocaliser().getString("createNewReceivingAddressSubmitAction.creatingShort", new Object[] {numberOfAddressesToCreate});
String longMessage = controller.getLocaliser().getString("createNewReceivingAddressSubmitAction.creatingLong", new Object[] {numberOfAddressesToCreate, walletDescription});
createNewReceivingAddressPanel.setMessageText(shortMessage);
MessageManager.INSTANCE.addMessage(new Message(" "));
Message logMessage = new Message(longMessage);
logMessage.setShowInStatusBar(false);
MessageManager.INSTANCE.addMessage(logMessage);
super.bitcoinController.fireWalletBusyChange(true);
createNewReceivingAddressesInBackground(createNewReceivingAddressPanel.getNumberOfAddressesToCreate(), encryptNewKeys,
CharBuffer.wrap(walletPassword.getPassword()), this);
}
}
/**
* Create the new receiving addresses in a background Swing worker thread.
*/
private void createNewReceivingAddressesInBackground(final int numberOfAddressesToCreate, final boolean encryptNewKeys,
final CharSequence walletPassword, final CreateNewReceivingAddressSubmitAction thisAction) {
final WalletData finalPerWalletModelData = super.bitcoinController.getModel().getActivePerWalletModelData();
final BitcoinController finalController = super.bitcoinController;
SwingWorker<Boolean, Void> worker = new SwingWorker<Boolean, Void>() {
private String shortMessage = null;
private String longMessage = null;
private String lastAddressString = null;
private File privateKeysBackupFile = null;
@Override
protected Boolean doInBackground() throws Exception {
Boolean successMeasure = Boolean.FALSE;
privateKeysBackupFile = null;
final KeyCrypter walletKeyCrypter = finalPerWalletModelData.getWallet().getKeyCrypter();
try {
// Derive AES key to use outside of loop - it is the same for all keys in a single wallet.
KeyParameter aesKey = null;
if (encryptNewKeys) {
aesKey = walletKeyCrypter.deriveKey(walletPassword);
}
List<ECKey> newKeys = new ArrayList<ECKey>();
for (int i = 0; i < numberOfAddressesToCreate; i++) {
ECKey newKey;
if (encryptNewKeys) {
// Use the wallet KeyCrypter.
newKey = (new ECKey()).encrypt(walletKeyCrypter, aesKey);
} else {
newKey = new ECKey();
}
newKeys.add(newKey);
}
FileHandler fileHandler = finalController.getFileHandler();
synchronized (finalPerWalletModelData.getWallet()) {
finalPerWalletModelData.getWallet().addKeys(newKeys);
}
// Recalculate the bloom filter.
if (bitcoinController.getMultiBitService() != null) {
bitcoinController.getMultiBitService().recalculateFastCatchupAndFilter();
}
// Add keys to address book.
for (ECKey newKey : newKeys) {
lastAddressString = newKey.toAddress(finalController.getModel().getNetworkParameters()).toString();
finalPerWalletModelData.getWalletInfo().addReceivingAddress(new WalletAddressBookData("", lastAddressString),
false);
}
// Backup the private keys.
privateKeysBackupFile = fileHandler.backupPrivateKeys(CharBuffer.wrap(walletPassword));
thisAction.setLastPrivateKeysBackupFile(privateKeysBackupFile);
// Backup the wallet and wallet info.
BackupManager.INSTANCE.backupPerWalletModelData(fileHandler, finalPerWalletModelData);
successMeasure = Boolean.TRUE;
} catch (KeyCrypterException kce) {
logError(kce);
} catch (IOException io) {
logError(io);
} catch (Exception e) {
logError(e);
}
return successMeasure;
}
private void logError(Exception e) {
log.error(e.getClass().getName() + " " + e.getMessage());
e.printStackTrace();
shortMessage = controller.getLocaliser().getString("createNewReceivingAddressesSubmitAction.failure",
new Object[] { e.getMessage() });
longMessage = shortMessage;
}
@Override
protected void done() {
try {
Boolean wasSuccessful = get();
String walletDescription = "";
if (finalPerWalletModelData != null && finalPerWalletModelData.getWalletInfo() != null) {
walletDescription = finalPerWalletModelData.getWalletInfo().getProperty(WalletInfoData.DESCRIPTION_PROPERTY);
}
if (wasSuccessful) {
shortMessage = controller.getLocaliser().getString("createNewReceivingAddressSubmitAction.createdSuccessfullyShort", new Object[] {numberOfAddressesToCreate});
longMessage = controller.getLocaliser().getString("createNewReceivingAddressSubmitAction.createdSuccessfullyLong", new Object[] {numberOfAddressesToCreate, walletDescription});
if (privateKeysBackupFile != null) {
longMessage = longMessage + ".\n" + controller.getLocaliser().getString("changePasswordPanel.keysBackupSuccess", new Object[] { privateKeysBackupFile.getCanonicalPath() });
}
log.debug(longMessage);
if (createNewReceivingAddressPanel.getReceiveBitcoinPanel() != null) {
createNewReceivingAddressPanel.getReceiveBitcoinPanel().getAddressesTableModel().fireTableDataChanged();
createNewReceivingAddressPanel.getReceiveBitcoinPanel().selectRows();
}
finalPerWalletModelData.getWalletInfo().put(BitcoinModel.RECEIVE_ADDRESS, lastAddressString);
finalPerWalletModelData.getWalletInfo().put(BitcoinModel.RECEIVE_LABEL, "");
try {
finalController.getFileHandler().savePerWalletModelData(finalPerWalletModelData, false);
} catch (WalletSaveException wse) {
log.error(wse.getClass().getCanonicalName() + " " + wse.getMessage());
MessageManager.INSTANCE.addMessage(new Message(controller.getLocaliser().getString("createNewReceivingAddressesSubmitAction.failure",
new Object[] { wse.getClass().getCanonicalName() + " " + wse.getMessage() })));
}
} else {
log.error(longMessage);
}
if (shortMessage != null) {
createNewReceivingAddressPanel.setMessageText(shortMessage);
if (createNewReceivingAddressDialog != null && createNewReceivingAddressDialog.isVisible()) {
// Show short message in dialog, long in messages.
createNewReceivingAddressPanel.setMessageText(shortMessage);
Message logMessage = new Message(longMessage);
logMessage.setShowInStatusBar(false);
MessageManager.INSTANCE.addMessage(logMessage);
} else {
// Show long message on statusbar and in messages.
MessageManager.INSTANCE.addMessage(new Message(longMessage));
}
}
} catch (Exception e) {
// Not really used but caught so that SwingWorker shuts down cleanly.
log.error(e.getClass() + " " + e.getMessage());
} finally {
// Can now cancel the operation.
createNewReceivingAddressPanel.getCancelButton().setEnabled(true);
// Declare that wallet is no longer busy with the task.
finalPerWalletModelData.setBusyTaskKey(null);
finalPerWalletModelData.setBusy(false);
finalController.fireWalletBusyChange(false);
}
}
};
log.debug("Creating receive addresses in background SwingWorker thread");
worker.execute();
}
@Override
public void walletBusyChange(boolean newWalletIsBusy) {
// Update the enable status of the action to match the wallet busy status.
if (super.bitcoinController.getModel().getActivePerWalletModelData().isBusy()) {
// Wallet is busy with another operation that may change the private keys - Action is disabled.
putValue(SHORT_DESCRIPTION, this.bitcoinController.getLocaliser().getString("multiBitSubmitAction.walletIsBusy",
new Object[]{controller.getLocaliser().getString(this.bitcoinController.getModel().getActivePerWalletModelData().getBusyTaskKey())}));
setEnabled(false);
} else {
// Enable
putValue(SHORT_DESCRIPTION, this.bitcoinController.getLocaliser().getString("createNewReceivingAddressSubmitAction.tooltip"));
setEnabled(true);
// Make sure the cancel button is enabled.
createNewReceivingAddressPanel.getCancelButton().setEnabled(true);
}
}
public File getLastPrivateKeysBackupFile() {
return lastPrivateKeysBackupFile;
}
public void setLastPrivateKeysBackupFile(File lastPrivateKeysBackupFile) {
this.lastPrivateKeysBackupFile = lastPrivateKeysBackupFile;
}
}