/**
* 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.model.bitcoin;
import com.google.bitcoin.core.*;
import com.google.bitcoin.core.Wallet.BalanceType;
import com.google.bitcoin.store.BlockStoreException;
import org.multibit.controller.Controller;
import org.multibit.controller.bitcoin.BitcoinController;
import org.multibit.model.AbstractModel;
import org.multibit.model.ModelEnum;
import org.multibit.model.core.CoreModel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.math.BigInteger;
import java.util.*;
/**
* Model containing the MultiBit data.
*
* Most of the methods act on the single, active wallet in the model.
*
* @author jim
*
*/
public class BitcoinModel extends AbstractModel<CoreModel> {
private static final Logger log = LoggerFactory.getLogger(BitcoinModel.class);
// Constants used in the multibit.properties.
// MultiBit start up.
public static final String TEST_OR_PRODUCTION_NETWORK = "testOrProductionNetwork";
public static final String TEST_NETWORK_VALUE = "test";
public static final String TESTNET3_VALUE = "testnet3";
public static final String PRODUCTION_NETWORK_VALUE = "production";
public static final String WALLET_FILENAME = "walletFilename";
// Wallets, open wallet and save wallet as dialog.
public static final String GRAB_FOCUS_FOR_ACTIVE_WALLET = "grabFocusForActiveWallet";
public static final String ACTIVE_WALLET_FILENAME = "selectedWalletFilename";
public static final String WALLET_DESCRIPTION_PREFIX = "walletDescription.";
public static final String SHOW_DELETE_WALLET = "showDeleteWallet";
// The number of serialised and protobuf2 wallets in the multibit.properties.
public static final String EARLY_WALLET_FILENAME_PREFIX = "walletFilename.";
public static final String NUMBER_OF_EARLY_WALLETS = "numberOfWallets";
public static final String PROTOBUF3_WALLET_FILENAME_PREFIX = "protobuf3WalletFilename.";
public static final String NUMBER_OF_PROTOBUF3_WALLETS = "numberfProtobuf3Wallets";
public static final String WALLET_ORDER_TOTAL = "walletOrderTotal";
public static final String WALLET_ORDER_PREFIX = "walletOrder.";
public static final String WALLET_CLEANED_OF_SPAM = "walletCleanedOfSpam";
// Send bitcoin and send bitcoin confirm.
public static final String SEND_ADDRESS = "sendAddress";
public static final String SEND_LABEL = "sendLabel";
public static final String SEND_AMOUNT = "sendAmount";
public static final String SEND_FEE = "sendFee";
public static final String SEND_PERFORM_PASTE_NOW = "sendPerformPasteNow";
public static final String SHOW_SIDE_PANEL = "showSidePanel";
public static final String DISPLAY_AS_SWATCH = "displayAsSwatch";
public static final String DISPLAY_AS_QR_CODE = "displayAsQRcode";
public static final int MINIMUM_NUMBER_OF_CONNECTED_PEERS_BEFORE_SEND_IS_ENABLED = 2;
// Open bitcoin URI.
public static final String OPEN_URI_SHOW_DIALOG = "openUriShowDialog";
public static final String OPEN_URI_USE_URI = "openUriUseUri";
public static final String OPEN_URI_ADDRESS = "openUriAddress";
public static final String OPEN_URI_LABEL = "openUriLabel";
public static final String OPEN_URI_AMOUNT = "openUriAmount";
public static final String BRING_TO_FRONT = "bringToFront";
// Receive bitcoin.
public static final String IS_RECEIVE_BITCOIN = "isReceiveBitcoin";
public static final String RECEIVE_ADDRESS = "receiveAddress";
public static final String RECEIVE_LABEL = "receiveLabel";
public static final String RECEIVE_AMOUNT = "receiveAmount";
public static final String RECEIVE_NEW_KEY = "receiveNewKey"; // to delete
// Validation.
public static final String VALIDATION_ADDRESS_IS_INVALID = "validationAddressIsInvalid";
public static final String VALIDATION_AMOUNT_IS_INVALID = "validationAmountIsInvalid";
public static final String VALIDATION_AMOUNT_IS_MISSING = "validationAmountIsMissing";
public static final String VALIDATION_AMOUNT_IS_NEGATIVE_OR_ZERO = "validationAmountIsNegativeOrZero";
public static final String VALIDATION_AMOUNT_IS_TOO_SMALL = "validationAmountIsTooSmall";
public static final String VALIDATION_NOT_ENOUGH_FUNDS = "validationNotEnoughFunds";
public static final String VALIDATION_ADDRESS_VALUE = "validationAddressValue";
public static final String VALIDATION_AMOUNT_VALUE = "validationAmountValue";
public static final String WALLET_FILE_EXTENSION = "wallet";
public static final String CSV_FILE_EXTENSION = "csv";
// Private key import and export.
public static final String PRIVATE_KEY_FILE_EXTENSION = "key";
public static final String PRIVATE_KEY_FILENAME = "privateKeyFilename";
// Blockchain.info support.
public static final String BLOCKCHAIN_WALLET_ENCRYPTED_SUFFIX = "aes.json";
public static final String BLOCKCHAIN_WALLET_PLAIN_SUFFIX = "json";
// Connect to nodes.
@Deprecated
public static final String SINGLE_NODE_CONNECTION = "singleNodeConnection";
public static final String PEERS = "peers";
// User preferences undo.
public static final String PREVIOUS_OPEN_URI_SHOW_DIALOG = "previousOpenUriShowDialog";
public static final String PREVIOUS_OPEN_URI_USE_URI = "previousOpenUriUseUri";
public static final String PREVIOUS_SEND_FEE = "previousSendFee";
// Wallet backup.
public static final String WALLET_BACKUP_FILE = "walletBackupFile";
// AlertManager and versions
public static final String ALERT_MANAGER_NEW_VERSION_VALUE = "alertManagerNewVersionValue";
public static final String ALERT_MANAGER_NEW_VERSION_SEEN_COUNT = "alertManagerNewVersionSeenCount";
/**
* List of each wallet's total model data.
*/
private List<WalletData> perWalletModelDataList;
/**
* The current active wallet.
*/
private WalletData activeWalletModelData;
public static final int UNKNOWN_NUMBER_OF_CONNECTD_PEERS = -1;
/**
* The number of peers connected.
*/
private int numberOfConnectedPeers = UNKNOWN_NUMBER_OF_CONNECTD_PEERS;
/**
* Used to enable/ disable blinking of the SingleWalletPanels when language changes etc.
*/
private boolean blinkEnabled = true;
@SuppressWarnings("deprecation")
public BitcoinModel(CoreModel coreModel) {
super(coreModel);
perWalletModelDataList = new LinkedList<WalletData>();
activeWalletModelData = new WalletData();
perWalletModelDataList.add(activeWalletModelData);
}
public WalletData getActivePerWalletModelData() {
return activeWalletModelData;
}
/**
* Get a wallet preference from the active wallet.
*
* @param key String key of property
* @return String property value
*/
public String getActiveWalletPreference(String key) {
if (activeWalletModelData.getWalletInfo() != null) {
return activeWalletModelData.getWalletInfo().getProperty(key);
} else {
return null;
}
}
/**
* Set a wallet preference from the active wallet.
*
*/
public void setActiveWalletPreference(String key, String value) {
if (BitcoinModel.SEND_AMOUNT.equals(key)) {
if (value.contains(",")) {
boolean bad = true;
bad = !bad;
}
}
if (activeWalletModelData.getWalletInfo() != null && value != null) {
activeWalletModelData.getWalletInfo().put(key, value);
activeWalletModelData.setDirty(true);
}
}
/**
* Get the estimated balance of the active wallet.
*
* @return The estimated balance
*/
public BigInteger getActiveWalletEstimatedBalance() {
if (activeWalletModelData.getWallet() == null) {
return BigInteger.ZERO;
} else {
return activeWalletModelData.getWallet().getBalance(BalanceType.ESTIMATED);
}
}
/**
* Get the available balance (plus boomeranged change) of the active wallet.
*
* @return the available balance
*/
public BigInteger getActiveWalletAvailableBalance() {
if (activeWalletModelData.getWallet() == null) {
return BigInteger.ZERO;
} else {
return activeWalletModelData.getWallet().getBalance(BalanceType.AVAILABLE);
}
}
/**
* Get the wallet data for the active wallet.
*
* @return the table data list
*/
public List<WalletTableData> getActiveWalletWalletData() {
return activeWalletModelData.getWalletTableDataList();
}
/**
* @return the wallet info for the active wallet
*/
public WalletInfoData getActiveWalletWalletInfo() {
return activeWalletModelData.getWalletInfo();
}
/**
* @return the active wallet
*/
public Wallet getActiveWallet() {
return activeWalletModelData.getWallet();
}
/**
* Set the active wallet, given a wallet filename.
*
* @param walletFilename the wallet filename
*/
public void setActiveWalletByFilename(String walletFilename) {
if (walletFilename == null) {
return;
}
if (perWalletModelDataList != null) {
for (WalletData loopPerWalletModelData : perWalletModelDataList) {
if (walletFilename.equals(loopPerWalletModelData.getWalletFilename())) {
activeWalletModelData = loopPerWalletModelData;
break;
}
}
}
}
/**
* Remove the specified perWalletModelData. Note that this does not remove
* any backing wallet or wallet info files.
*
* Removal is determined by matching the wallet filename. Use FileHandler to
* do that.
*
* @param perWalletModelDataToRemove The wallet data
*/
public void remove(WalletData perWalletModelDataToRemove) {
if (perWalletModelDataToRemove == null) {
return;
}
if (perWalletModelDataList != null) {
for (WalletData loopPerWalletModelData : perWalletModelDataList) {
if (perWalletModelDataToRemove.getWalletFilename().equals(loopPerWalletModelData.getWalletFilename())) {
perWalletModelDataList.remove(loopPerWalletModelData);
break;
}
}
}
// If there are no wallets, clear the activeWalletModelData.
activeWalletModelData = new WalletData();
}
/**
* Set a wallet description, given a wallet filename.
*
* @param walletFilename The wallet file name
* @param walletDescription The wallet description
*/
public void setWalletDescriptionByFilename(String walletFilename, String walletDescription) {
if (walletFilename == null) {
return;
}
if (perWalletModelDataList != null) {
for (WalletData loopPerWalletModelData : perWalletModelDataList) {
if (walletFilename.equals(loopPerWalletModelData.getWalletFilename())) {
loopPerWalletModelData.setWalletDescription(walletDescription);
loopPerWalletModelData.setDirty(true);
break;
}
}
}
}
/**
* Add a new wallet to the list of managed wallets.
*/
public WalletData addWallet(final BitcoinController bitcoinController, Wallet wallet, String walletFilename) {
if (walletFilename == null) {
return null;
}
// Check to see if it is already in the managed list - no need to add it
// again if so.
for (WalletData loopModelData : perWalletModelDataList) {
if (walletFilename.equals(loopModelData.getWalletFilename())) {
return loopModelData;
}
}
WalletData newPerWalletModelData = new WalletData();
newPerWalletModelData.setWallet(wallet);
newPerWalletModelData.setWalletFilename(walletFilename);
// Table row data used in displaying transactions - initially empty
newPerWalletModelData.setWalletTableDataList(new ArrayList<WalletTableData>());
// If it is the initial empty activeWalletModelData remove it.
if (thereIsNoActiveWallet()) {
perWalletModelDataList.remove(activeWalletModelData);
activeWalletModelData = newPerWalletModelData;
}
perWalletModelDataList.add(newPerWalletModelData);
// Wire up the controller as a wallet event listener.
if (wallet != null) {
wallet.addEventListener(bitcoinController);
}
createWalletTableData(bitcoinController, walletFilename);
createAddressBookReceivingAddresses(walletFilename);
return newPerWalletModelData;
}
/**
* Get the active wallet filename.
*
*
* @return
*/
public String getActiveWalletFilename() {
return activeWalletModelData.getWalletFilename();
}
/**
* Convert the active wallet info into walletdata records as they are easier
* to show to the user in tabular form.
*/
public ArrayList<WalletTableData> createActiveWalletData(final BitcoinController bitcoinController) {
return createWalletTableData(bitcoinController, this.getActivePerWalletModelData());
}
/**
* Convert the wallet info into walletdata records as they are easier
* to show to the user in tabular form.
*/
public ArrayList<WalletTableData> createWalletTableData(final BitcoinController bitcoinController, String walletFilename) {
ArrayList<WalletTableData> walletData = new ArrayList<WalletTableData>();
if (walletFilename == null) {
return walletData;
}
WalletData perWalletModelData = null;
if (perWalletModelDataList != null) {
for (WalletData loopPerWalletModelData : perWalletModelDataList) {
if (walletFilename.equals(loopPerWalletModelData.getWalletFilename())) {
perWalletModelData = loopPerWalletModelData;
break;
}
}
}
return createWalletTableData(bitcoinController, perWalletModelData);
}
public ArrayList<WalletTableData> createWalletTableData(final BitcoinController bitcoinController, WalletData perWalletModelData) {
ArrayList<WalletTableData> walletData = new ArrayList<WalletTableData>();
if (perWalletModelData == null || perWalletModelData.getWallet() == null) {
return walletData;
}
Set<Transaction> transactions = perWalletModelData.getWallet().getTransactions(false);
if (transactions != null) {
for (Transaction loopTransaction : transactions) {
WalletTableData walletDataRow = new WalletTableData(loopTransaction);
walletData.add(walletDataRow);
walletDataRow.setCredit(loopTransaction.getValueSentToMe(perWalletModelData.getWallet()));
try {
walletDataRow.setDebit(loopTransaction.getValueSentFromMe(perWalletModelData.getWallet()));
} catch (ScriptException e) {
log.error(e.getMessage(), e);
}
List<TransactionInput> transactionInputs = loopTransaction.getInputs();
List<TransactionOutput> transactionOutputs = loopTransaction.getOutputs();
if (transactionInputs != null) {
TransactionInput firstInput = transactionInputs.get(0);
if (firstInput != null) {
walletDataRow.setDescription(createDescription(bitcoinController, perWalletModelData.getWallet(), transactionInputs,
transactionOutputs, walletDataRow.getCredit(), walletDataRow.getDebit()));
}
}
walletDataRow.setDate(createDate(bitcoinController, loopTransaction));
walletDataRow.setHeight(workOutHeight(loopTransaction));
}
}
// Run through all the walletdata to see if both credit and debit are
// set (this means change was received).
for (WalletTableData walletDataRow : walletData) {
if (walletDataRow.getCredit() != null && (walletDataRow.getCredit().compareTo(BigInteger.ZERO) > 0)
&& (walletDataRow.getDebit() != null) && walletDataRow.getDebit().compareTo(BigInteger.ZERO) > 0) {
BigInteger net = walletDataRow.getCredit().subtract(walletDataRow.getDebit());
if (net.compareTo(BigInteger.ZERO) >= 0) {
walletDataRow.setCredit(net);
walletDataRow.setDebit(BigInteger.ZERO);
} else {
walletDataRow.setCredit(BigInteger.ZERO);
walletDataRow.setDebit(net.negate());
}
}
}
return walletData;
}
/**
* Add the receiving addresses of all the keys of the specified wallet.
*/
public void createAddressBookReceivingAddresses(String walletFilename) {
if (walletFilename == null) {
return;
}
WalletData perWalletModelData = null;
if (perWalletModelDataList != null) {
for (WalletData loopPerWalletModelData : perWalletModelDataList) {
if (walletFilename.equals(loopPerWalletModelData.getWalletFilename())) {
perWalletModelData = loopPerWalletModelData;
break;
}
}
}
if (!(perWalletModelData == null)) {
List<ECKey> keyChain = perWalletModelData.getWallet().getKeychain();
if (keyChain != null) {
NetworkParameters networkParameters = getNetworkParameters();
if (networkParameters != null) {
if (perWalletModelData.getWalletInfo() != null) {
// Keep a copy of the existing receiving addresses - labels will be recycled.
List<WalletAddressBookData> currentReceivingAddresses = perWalletModelData.getWalletInfo().getReceivingAddresses();
// Clear the existing receiving addresses.
ArrayList<WalletAddressBookData> newReceivingAddresses = new ArrayList<WalletAddressBookData>();
perWalletModelData.getWalletInfo().setReceivingAddresses(newReceivingAddresses);
// Add the new receiving addresses from the keys, checking if there is an old label.
for (ECKey key : keyChain) {
Address address = key.toAddress(getNetworkParameters());
String addressString = address.toString();
WalletAddressBookData addressBookData = new WalletAddressBookData(null, addressString);
for (WalletAddressBookData loopAddressBookData : currentReceivingAddresses) {
if (loopAddressBookData.getAddress().equals(addressString)) {
// Recycle label.
addressBookData.setLabel(loopAddressBookData.getLabel());
break;
}
}
perWalletModelData.getWalletInfo().addReceivingAddress(addressBookData, false);
}
}
}
}
}
}
/**
* Create a description for a transaction.
*
* @param transactionInputs
* @param transactionOutputs
* @param credit
* @param debit
* @return A description of the transaction
*/
public String createDescription(final Controller controller, Wallet wallet, List<TransactionInput> transactionInputs,
List<TransactionOutput> transactionOutputs, BigInteger credit, BigInteger debit) {
String toReturn = "";
WalletData perWalletModelData = null;
if (perWalletModelDataList != null) {
for (WalletData loopPerWalletModelData : perWalletModelDataList) {
if (wallet.equals(loopPerWalletModelData.getWallet())) {
perWalletModelData = loopPerWalletModelData;
break;
}
}
}
if (perWalletModelData == null) {
return toReturn;
}
TransactionOutput myOutput = null;
TransactionOutput theirOutput = null;
if (transactionOutputs != null) {
for (TransactionOutput transactionOutput : transactionOutputs) {
if (transactionOutput != null && transactionOutput.isMine(perWalletModelData.getWallet())) {
myOutput = transactionOutput;
}
if (transactionOutput != null && !transactionOutput.isMine(perWalletModelData.getWallet())) {
theirOutput = transactionOutput;
}
}
}
if (credit != null && credit.compareTo(BigInteger.ZERO) > 0) {
// Credit.
try {
String addressString = "";
if (myOutput != null) {
Address toAddress = new Address(getNetworkParameters(), myOutput
.getScriptPubKey().getPubKeyHash());
addressString = toAddress.toString();
}
String label = null;
if (perWalletModelData.getWalletInfo() != null) {
label = perWalletModelData.getWalletInfo().lookupLabelForReceivingAddress(addressString);
}
if (label != null && !label.equals("")) {
toReturn = controller.getLocaliser().getString("multiBitModel.creditDescriptionWithLabel",
new Object[]{addressString, label});
} else {
toReturn = controller.getLocaliser().getString("multiBitModel.creditDescription",
new Object[]{addressString});
}
} catch (ScriptException e) {
log.error(e.getMessage(), e);
}
}
if (debit != null && debit.compareTo(BigInteger.ZERO) > 0) {
// Debit.
try {
// See if the address is a known sending address.
if (theirOutput != null) {
String addressString = theirOutput.getScriptPubKey().getToAddress(getNetworkParameters()).toString();
String label = null;
if (perWalletModelData.getWalletInfo() != null) {
label = perWalletModelData.getWalletInfo().lookupLabelForSendingAddress(addressString);
}
if (label != null && !label.equals("")) {
toReturn = controller.getLocaliser().getString("multiBitModel.debitDescriptionWithLabel",
new Object[]{addressString, label});
} else {
toReturn = controller.getLocaliser().getString("multiBitModel.debitDescription",
new Object[]{addressString});
}
}
} catch (ScriptException e) {
log.error(e.getMessage(), e);
}
}
return toReturn;
}
/**
* Work out the transaction date.
*
* @param transaction
* @return Date date of transaction
*/
private Date createDate(final BitcoinController bitcoinController, Transaction transaction) {
// If transaction has altered date - return that.
if (transaction.getUpdateTime() != null) {
return transaction.getUpdateTime();
}
// Other wise return the date of the block it first appeared in.
Map<Sha256Hash, Integer> appearsIn = transaction.getAppearsInHashes();
if (appearsIn != null) {
if (!appearsIn.isEmpty()) {
Iterator<Sha256Hash> iterator = appearsIn.keySet().iterator();
// just take the first i.e. ignore impact of side chains
if (iterator.hasNext()) {
Sha256Hash appearsInHash = iterator.next();
StoredBlock appearsInStoredBlock;
try {
if (bitcoinController != null && bitcoinController.getMultiBitService() != null
&& bitcoinController.getMultiBitService().getBlockStore() != null) {
appearsInStoredBlock = bitcoinController.getMultiBitService().getBlockStore().get(appearsInHash);
Block appearsInBlock = appearsInStoredBlock.getHeader();
// Set the time of the block to be the time of the
// transaction - TODO get transaction time.
return new Date(appearsInBlock.getTimeSeconds() * 1000);
}
} catch (BlockStoreException e) {
e.printStackTrace();
}
}
}
}
return null;
}
/**
* Work out the height of the block chain in which the transaction appears.
*
* @param transaction
* @return
*/
private int workOutHeight(Transaction transaction) {
return -1; // -1 = we do not know. TODO probably needs replacing by height on TransactionConfidence.
}
public void setActiveWalletInfo(WalletInfoData walletInfo) {
activeWalletModelData.setWalletInfo(walletInfo);
}
public List<WalletData> getPerWalletModelDataList() {
return perWalletModelDataList;
}
public WalletData getPerWalletModelDataByWalletFilename(String walletFilename) {
if (walletFilename == null) {
return null;
}
if (perWalletModelDataList != null) {
for (WalletData loopPerWalletModelData : perWalletModelDataList) {
if (walletFilename.equals(loopPerWalletModelData.getWalletFilename())) {
return loopPerWalletModelData;
}
}
}
return null;
}
public NetworkParameters getNetworkParameters() {
// If test or production is not specified, default to production.
String testOrProduction = super.getUserPreference(BitcoinModel.TEST_OR_PRODUCTION_NETWORK);
if (testOrProduction == null) {
testOrProduction = BitcoinModel.PRODUCTION_NETWORK_VALUE;
super.setUserPreference(BitcoinModel.TEST_OR_PRODUCTION_NETWORK, testOrProduction);
}
if (BitcoinModel.TEST_NETWORK_VALUE.equalsIgnoreCase(testOrProduction)) {
return NetworkParameters.testNet2();
} else if (BitcoinModel.TESTNET3_VALUE.equalsIgnoreCase(testOrProduction)) {
return NetworkParameters.testNet();
} else {
return NetworkParameters.prodNet();
}
}
public boolean thereIsNoActiveWallet() {
return activeWalletModelData == null
|| "".equals(activeWalletModelData.getWalletFilename()) || activeWalletModelData.getWalletFilename() == null;
}
public int getNumberOfConnectedPeers() {
return numberOfConnectedPeers;
}
public void setNumberOfConnectedPeers(int numberOfConnectedPeers) {
this.numberOfConnectedPeers = numberOfConnectedPeers;
}
public boolean isBlinkEnabled() {
return blinkEnabled;
}
public void setBlinkEnabled(boolean blinkEnabled) {
this.blinkEnabled = blinkEnabled;
}
@Override
public ModelEnum getModelEnum() {
return ModelEnum.BITCOIN;
}
}