/** * 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.network; import com.google.bitcoin.core.*; import com.google.bitcoin.core.Wallet.SendRequest; import com.google.bitcoin.crypto.KeyCrypterException; import com.google.bitcoin.net.discovery.DnsDiscovery; import com.google.bitcoin.net.discovery.IrcDiscovery; import com.google.bitcoin.store.BlockStore; import com.google.bitcoin.store.BlockStoreException; import com.google.bitcoin.store.SPVBlockStore; import com.google.common.util.concurrent.ListenableFuture; import org.bitcoinj.wallet.Protos.Wallet.EncryptionType; import org.multibit.ApplicationDataDirectoryLocator; import org.multibit.MultiBit; import org.multibit.controller.Controller; import org.multibit.controller.bitcoin.BitcoinController; import org.multibit.file.BackupManager; import org.multibit.file.FileHandlerException; 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.model.core.CoreModel; import org.multibit.model.core.StatusEnum; import org.multibit.store.MultiBitWalletVersion; import org.multibit.store.WalletVersionException; import org.multibit.viewsystem.swing.view.components.FeeSlider; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.spongycastle.crypto.params.KeyParameter; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.math.BigInteger; import java.net.InetAddress; import java.net.UnknownHostException; import java.security.SecureRandom; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.*; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; /** * <p> * MultiBitService encapsulates the interaction with the bitcoin netork * including: o Peers o Block chain download o sending / receiving bitcoins * <p/> * The testnet can be slow or flaky as it's a shared resource. You can use the * <a href="http://sourceforge * .net/projects/bitcoin/files/Bitcoin/testnet-in-a-box/">testnet in a box</a> * to do everything purely locally. * </p> */ public class MultiBitService { private static final String TESTNET3_GENESIS_HASH = "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943"; private static final Logger log = LoggerFactory.getLogger(MultiBitService.class); public static final String MULTIBIT_PREFIX = "multibit"; public static final String TESTNET_PREFIX = "testnet"; public static final String TESTNET3_PREFIX = "testnet3"; public static final String SEPARATOR = "-"; public static final String SPV_BLOCKCHAIN_SUFFIX = ".spvchain"; public static final String CHECKPOINTS_SUFFIX = ".checkpoints"; public static final String WALLET_SUFFIX = ".wallet"; public static final String IRC_CHANNEL_TEST = "#bitcoinTEST"; public static final String IRC_CHANNEL_TESTNET3 = "#bitcoinTEST3"; public Logger logger = LoggerFactory.getLogger(MultiBitService.class.getName()); private MultiBitPeerGroup peerGroup; private String blockchainFilename; private MultiBitBlockChain blockChain; private BlockStore blockStore; private final Controller controller; private final BitcoinController bitcoinController; private final NetworkParameters networkParameters; private SecureRandom secureRandom = new SecureRandom(); private MultiBitCheckpointManager checkpointManager; private String checkpointsFilename; public static Date genesisBlockCreationDate; static { try { java.text.SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); java.util.Calendar cal = Calendar.getInstance(new SimpleTimeZone(0, "GMT")); format.setCalendar(cal); genesisBlockCreationDate = format.parse("2009-01-03 18:15:05"); } catch (ParseException e) { // Will never happen. e.printStackTrace(); } } /** * @param bitcoinController BitcoinController */ public MultiBitService(BitcoinController bitcoinController) { this.bitcoinController = bitcoinController; this.controller = this.bitcoinController; if (controller == null) { throw new IllegalStateException("controller cannot be null"); } if (controller.getModel() == null) { throw new IllegalStateException("controller.getModel() cannot be null"); } if (controller.getApplicationDataDirectoryLocator() == null) { throw new IllegalStateException("controller.getApplicationDataDirectoryLocator() cannot be null"); } if (this.bitcoinController.getFileHandler() == null) { throw new IllegalStateException("controller.getFileHandler() cannot be null"); } networkParameters = this.bitcoinController.getModel().getNetworkParameters(); log.debug("Network parameters = " + networkParameters); try { // Load or create the blockStore.. log.debug("Loading/ creating blockstore ..."); blockStore = createBlockStore(null, false); log.debug("Blockstore is '" + blockStore + "'"); log.debug("Creating blockchain ..."); blockChain = new MultiBitBlockChain(networkParameters, blockStore); log.debug("Created blockchain '" + blockChain + "' with height " + blockChain.getBestChainHeight()); log.debug("Creating peergroup ..."); createNewPeerGroup(); log.debug("Created peergroup '" + peerGroup + "'"); log.debug("Starting peergroup ..."); peerGroup.start(); log.debug("Started peergroup."); } catch (BlockStoreException e) { handleError(e); } catch (FileHandlerException e) { handleError(e); } catch (Exception e) { handleError(e); } FileInputStream stream = null; try { stream = new FileInputStream(checkpointsFilename); checkpointManager = new MultiBitCheckpointManager(networkParameters, stream); } catch (IOException e) { log.error("Error creating checkpointManager " + e.getClass().getName() + " " + e.getMessage()); } finally { if (stream != null) { try { stream.close(); } catch (IOException e) { log.error("Error tidying up checkpointManager creation" + e.getClass().getName() + " " + e.getMessage()); } } } } private void handleError(Exception e) { controller.setOnlineStatus(StatusEnum.ERROR); MessageManager.INSTANCE.addMessage(new Message(controller.getLocaliser().getString( "multiBitService.couldNotLoadBlockchain", new Object[]{blockchainFilename, e.getClass().getName() + " " + e.getMessage()}))); log.error("Error creating MultiBitService " + e.getClass().getName() + " " + e.getMessage()); } private BlockStore createBlockStore(Date checkpointDate, boolean createNew) throws BlockStoreException, IOException { BlockStore blockStore = null; String filePrefix = getFilePrefix(); log.debug("filePrefix = " + filePrefix); if ("".equals(controller.getApplicationDataDirectoryLocator().getApplicationDataDirectory())) { blockchainFilename = filePrefix + SPV_BLOCKCHAIN_SUFFIX; checkpointsFilename = filePrefix + CHECKPOINTS_SUFFIX; } else { blockchainFilename = controller.getApplicationDataDirectoryLocator().getApplicationDataDirectory() + File.separator + filePrefix + SPV_BLOCKCHAIN_SUFFIX; checkpointsFilename = controller.getApplicationDataDirectoryLocator().getApplicationDataDirectory() + File.separator + filePrefix + CHECKPOINTS_SUFFIX; } File blockStoreFile = new File(blockchainFilename); boolean blockStoreCreatedNew = !blockStoreFile.exists(); // Ensure there is a checkpoints file. File checkpointsFile = new File(checkpointsFilename); if (!checkpointsFile.exists()) { bitcoinController.getFileHandler().copyCheckpointsFromInstallationDirectory(checkpointsFilename); } // Use the larger of the installed checkpoints file and the user data checkpoint file (larger = more recent). ApplicationDataDirectoryLocator applicationDataDirectoryLocator = new ApplicationDataDirectoryLocator(); String installedCheckpointsFilename = applicationDataDirectoryLocator.getInstallationDirectory() + File.separator + MultiBitService.getFilePrefix() + MultiBitService.CHECKPOINTS_SUFFIX; log.debug("Installed checkpoints file = '" + installedCheckpointsFilename + "'."); File installedCheckpointsFile = new File(installedCheckpointsFilename); long sizeOfUserDataCheckpointsFile = 0; if (checkpointsFile.exists()) { sizeOfUserDataCheckpointsFile = checkpointsFile.length(); } if (installedCheckpointsFile.exists() && installedCheckpointsFile.length() > sizeOfUserDataCheckpointsFile) { // The installed checkpoints file is longer (more checkpoints) so use that. checkpointsFilename = installedCheckpointsFilename; checkpointsFile = installedCheckpointsFile; log.debug("Using installed checkpoints file as it is longer than user data checkpoints - " + installedCheckpointsFile.length() + " bytes versus " + sizeOfUserDataCheckpointsFile + " bytes."); } else { log.debug("Using user data checkpoints file as it is longer/same size as installed checkpoints - " + sizeOfUserDataCheckpointsFile + " bytes versus " + installedCheckpointsFile.length() + " bytes."); } // If the spvBlockStore is to be created new // or its size is 0 bytes delete the file so that it is recreated fresh (fix for issue 165). if (createNew || blockStoreFile.length() == 0) { // Garbage collect any closed references to the blockchainFile. System.gc(); blockStoreFile.setWritable(true); boolean deletedOk = blockStoreFile.delete(); log.debug("Deleting SPV block store '{}' from disk.1", blockchainFilename + ", deletedOk = " + deletedOk); blockStoreCreatedNew = true; } log.debug("Opening / Creating SPV block store '{}' from disk", blockchainFilename); try { blockStore = new SPVBlockStore(networkParameters, blockStoreFile); } catch (BlockStoreException bse) { try { log.error("Failed to open/ create SPV block store '{}' from disk", blockchainFilename); // If the block store creation failed, delete the block store file and try again. // Garbage collect any closed references to the blockchainFile. System.gc(); blockStoreFile.setWritable(true); boolean deletedOk = blockStoreFile.delete(); log.debug("Deleting SPV block store '{}' from disk.2", blockchainFilename + ", deletedOk = " + deletedOk); blockStoreCreatedNew = true; blockStore = new SPVBlockStore(networkParameters, blockStoreFile); } catch (BlockStoreException bse2) { bse2.printStackTrace(); log.error("Unrecoverable failure in opening block store. This is bad."); // Throw the exception so that it is indicated on the UI. throw bse2; } } // Load the existing checkpoint file and checkpoint from today. if (blockStore != null && checkpointsFile.exists()) { FileInputStream stream = null; try { stream = new FileInputStream(checkpointsFile); if (checkpointDate == null) { if (blockStoreCreatedNew) { // Brand new block store - checkpoint from today. This // will go back to the last checkpoint. CheckpointManager.checkpoint(networkParameters, stream, blockStore, (new Date()).getTime() / 1000); } } else { // Use checkpoint date (block replay). CheckpointManager.checkpoint(networkParameters, stream, blockStore, checkpointDate.getTime() / 1000); } } finally { if (stream != null) { stream.close(); stream = null; } } } return blockStore; } public void createNewPeerGroup() { peerGroup = new MultiBitPeerGroup(bitcoinController, networkParameters, blockChain); peerGroup.setFastCatchupTimeSecs(0); // genesis block peerGroup.setUserAgent("MultiBit", controller.getLocaliser().getVersionNumber()); boolean peersSpecified = false; String singleNodeConnection = controller.getModel().getUserPreference(BitcoinModel.SINGLE_NODE_CONNECTION); String peers = controller.getModel().getUserPreference(BitcoinModel.PEERS); if (singleNodeConnection != null && !singleNodeConnection.equals("")) { try { peerGroup.addAddress(new PeerAddress(InetAddress.getByName(singleNodeConnection.trim()))); peerGroup.setMaxConnections(1); peersSpecified = true; } catch (UnknownHostException e) { log.error(e.getMessage(), e); } } else if (peers != null && !peers.equals("")) { // Split using commas. String[] peerList = peers.split(","); if (peerList != null) { int numberOfPeersAdded = 0; for (int i = 0; i < peerList.length; i++) { try { peerGroup.addAddress(new PeerAddress(InetAddress.getByName(peerList[i].trim()))); numberOfPeersAdded++; } catch (UnknownHostException e) { log.error(e.getMessage(), e); } } peerGroup.setMaxConnections(numberOfPeersAdded); peersSpecified = true; } } if (!peersSpecified) { // Use DNS for production, IRC for test. if (TESTNET3_GENESIS_HASH.equals(bitcoinController.getModel().getNetworkParameters().getGenesisBlock().getHashAsString())) { peerGroup.addPeerDiscovery(new IrcDiscovery(IRC_CHANNEL_TESTNET3)); } else if (NetworkParameters.testNet().equals(bitcoinController.getModel().getNetworkParameters())) { peerGroup.addPeerDiscovery(new IrcDiscovery(IRC_CHANNEL_TEST)); } else { peerGroup.addPeerDiscovery(new DnsDiscovery(networkParameters)); } } // Add the controller as a PeerEventListener. peerGroup.addEventListener(bitcoinController.getPeerEventListener()); // Add all existing wallets to the PeerGroup. if (controller != null && controller.getModel() != null) { List<WalletData> perWalletDataModels = bitcoinController.getModel().getPerWalletModelDataList(); if (perWalletDataModels != null) { Iterator<WalletData> iterator = perWalletDataModels.iterator(); if (iterator != null) { while (iterator.hasNext()) { WalletData perWalletModelData = iterator.next(); if (perWalletModelData != null && perWalletModelData.getWallet() != null) { peerGroup.addWallet(perWalletModelData.getWallet()); } } } } } } public void recalculateFastCatchupAndFilter() { if (peerGroup != null) { peerGroup.recalculateFastCatchupAndFilter(PeerGroup.FilterRecalculateMode.FORCE_SEND); } } public static String getFilePrefix() { BitcoinController bitcoinController = MultiBit.getBitcoinController(); // testnet3 if (TESTNET3_GENESIS_HASH.equals(bitcoinController.getModel().getNetworkParameters().getGenesisBlock().getHashAsString())) { return MULTIBIT_PREFIX + SEPARATOR + TESTNET3_PREFIX; } else if (NetworkParameters.testNet().equals(bitcoinController.getModel().getNetworkParameters())) { return MULTIBIT_PREFIX + SEPARATOR + TESTNET_PREFIX; } else { return MULTIBIT_PREFIX; } } /** * Initialize wallet from the wallet filename. * * @param walletFilename the wallet filename * @return perWalletModelData */ public WalletData addWalletFromFilename(String walletFilename) throws IOException { WalletData perWalletModelDataToReturn = null; Wallet wallet = null; File walletFile = null; boolean walletFileIsADirectory = false; boolean newWalletCreated = false; if (walletFilename != null) { walletFile = new File(walletFilename); if (walletFile.isDirectory()) { walletFileIsADirectory = true; } else { perWalletModelDataToReturn = bitcoinController.getFileHandler().loadFromFile(walletFile); if (perWalletModelDataToReturn != null) { wallet = perWalletModelDataToReturn.getWallet(); } } } if (walletFilename == null || walletFilename.equals("") || walletFileIsADirectory) { // Use default wallet name - create if does not exist. if ("".equals(controller.getApplicationDataDirectoryLocator().getApplicationDataDirectory())) { walletFilename = getFilePrefix() + WALLET_SUFFIX; } else { walletFilename = controller.getApplicationDataDirectoryLocator().getApplicationDataDirectory() + File.separator + getFilePrefix() + WALLET_SUFFIX; } walletFile = new File(walletFilename); if (walletFile.exists()) { // Wallet file exists with default name. perWalletModelDataToReturn = bitcoinController.getFileHandler().loadFromFile(walletFile); if (perWalletModelDataToReturn != null) { wallet = perWalletModelDataToReturn.getWallet(); newWalletCreated = true; } } else { // Create a brand new wallet - by default unencrypted. wallet = new Wallet(networkParameters); ECKey newKey = new ECKey(); wallet.addKey(newKey); perWalletModelDataToReturn = bitcoinController.getModel().addWallet(bitcoinController, wallet, walletFile.getAbsolutePath()); // Create a wallet info. WalletInfoData walletInfo = new WalletInfoData(walletFile.getAbsolutePath(), wallet, MultiBitWalletVersion.PROTOBUF); perWalletModelDataToReturn.setWalletInfo(walletInfo); // Set a default description. String defaultDescription = controller.getLocaliser().getString("createNewWalletSubmitAction.defaultDescription"); perWalletModelDataToReturn.setWalletDescription(defaultDescription); try { bitcoinController.getFileHandler().savePerWalletModelData(perWalletModelDataToReturn, true); newWalletCreated = true; // Backup the wallet and wallet info. BackupManager.INSTANCE.backupPerWalletModelData(bitcoinController.getFileHandler(), perWalletModelDataToReturn); } catch (WalletSaveException wse) { log.error(wse.getClass().getCanonicalName() + " " + wse.getMessage()); MessageManager.INSTANCE.addMessage(new Message(wse.getClass().getCanonicalName() + " " + wse.getMessage())); } catch (WalletVersionException wve) { log.error(wve.getClass().getCanonicalName() + " " + wve.getMessage()); MessageManager.INSTANCE.addMessage(new Message(wve.getClass().getCanonicalName() + " " + wve.getMessage())); } } } if (wallet != null) { // Add the keys for this wallet to the address book as receiving // addresses. List<ECKey> keys = wallet.getKeychain(); if (keys != null) { if (!newWalletCreated) { perWalletModelDataToReturn = bitcoinController.getModel().getPerWalletModelDataByWalletFilename(walletFilename); } if (perWalletModelDataToReturn != null) { WalletInfoData walletInfo = perWalletModelDataToReturn.getWalletInfo(); if (walletInfo != null) { for (ECKey key : keys) { if (key != null) { Address address = key.toAddress(networkParameters); walletInfo.addReceivingAddressOfKey(address); } } } } } // Add wallet to blockchain. if (blockChain != null) { blockChain.addWallet(wallet); } else { log.error("Could not add wallet '" + walletFilename + "' to the blockChain as the blockChain is missing.\n" + "This is bad. MultiBit is currently looking for a blockChain at '" + blockchainFilename + "'"); } // Add wallet to peergroup. if (peerGroup != null) { peerGroup.addWallet(wallet); peerGroup.addEventListener(bitcoinController.getPeerEventListener()); } else { log.error("Could not add wallet '" + walletFilename + "' to the peerGroup as the peerGroup is null. This is bad. "); } } return perWalletModelDataToReturn; } /** * Create a new block store. * * @param dateToReplayFrom The date to start the replay task from * @return height tof new block chain after truncate. * @throws IOException * @throws BlockStoreException */ public int createNewBlockStoreForReplay(Date dateToReplayFrom) throws IOException, BlockStoreException { log.debug("Loading/ creating blockstore ..."); if (blockStore != null) { try { blockStore.close(); blockStore = null; } catch (NullPointerException npe) { log.debug("NullPointerException on blockstore close"); } } // The CheckpointManager removes a week to cater for block header drift. // Any date before genesis + 1 week gets adjusted accordingly. Date genesisPlusOnwWeekAndASecond = new Date(MultiBitService.genesisBlockCreationDate.getTime() + (86400 * 7 + 1) * 1000); if (dateToReplayFrom != null) { if (dateToReplayFrom.getTime() < genesisPlusOnwWeekAndASecond.getTime()) { dateToReplayFrom = genesisPlusOnwWeekAndASecond; } blockStore = createBlockStore(dateToReplayFrom, true); } else { blockStore = createBlockStore(genesisPlusOnwWeekAndASecond, true); } log.debug("Blockstore is '" + blockStore + "'"); log.debug("Creating blockchain ..."); blockChain = new MultiBitBlockChain(bitcoinController.getModel().getNetworkParameters(), blockStore); log.debug("Created blockchain '" + blockChain + "'"); // Hook up the wallets to the new blockchain. if (blockChain != null) { List<WalletData> perWalletModelDataList = bitcoinController.getModel().getPerWalletModelDataList(); for (WalletData loopPerWalletModelData : perWalletModelDataList) { if (loopPerWalletModelData.getWallet() != null) { blockChain.addWallet(loopPerWalletModelData.getWallet()); } } } return blockChain.getBestChainHeight(); } /** * Send bitcoins from the active wallet. * * @return The sent transaction (may be null if there were insufficient * funds for send) * @throws KeyCrypterException * @throws IOException * @throws AddressFormatException */ public Transaction sendCoins(WalletData perWalletModelData, SendRequest sendRequest, CharSequence password) throws java.io.IOException, AddressFormatException, KeyCrypterException { // Ping the peers to check the bitcoin network connection List<Peer> connectedPeers = peerGroup.getConnectedPeers(); boolean atLeastOnePingWorked = false; if (connectedPeers != null) { for (Peer peer : connectedPeers) { log.debug("Ping: {}", peer.getAddress().toString()); try { ListenableFuture<Long> result = peer.ping(); result.get(4, TimeUnit.SECONDS); atLeastOnePingWorked = true; break; } catch (ProtocolException | InterruptedException | ExecutionException | TimeoutException e) { log.warn("Peer '" + peer.getAddress().toString() + "' failed ping test. Message was " + e.getMessage()); } } } if (!atLeastOnePingWorked) { throw new IllegalStateException("All peers failed ping test (check network)"); } // Send the coins log.debug("MultiBitService#sendCoins - Just about to send coins"); KeyParameter aesKey = null; if (perWalletModelData.getWallet().getEncryptionType() != EncryptionType.UNENCRYPTED) { aesKey = perWalletModelData.getWallet().getKeyCrypter().deriveKey(password); } sendRequest.aesKey = aesKey; sendRequest.fee = BigInteger.ZERO; // Work out fee per KB String unparsedFeePerKB = controller.getModel().getUserPreference(CoreModel.FEE_PER_KB); // Ensure the initialFeeValue is in range of the slider sendRequest.feePerKb = BigInteger.valueOf(FeeSlider.parseAndNormaliseFeePerKB(unparsedFeePerKB)); log.debug("Fee per KB set to {}", sendRequest.feePerKb); sendRequest.tx.getConfidence().addEventListener(perWalletModelData.getWallet().getTxConfidenceListener()); try { // The transaction is already added to the wallet (in SendBitcoinConfirmAction) so here we just need // to sign it, commit it and broadcast it. perWalletModelData.getWallet().sign(sendRequest); perWalletModelData.getWallet().commitTx(sendRequest.tx); // The tx has been committed to the pending pool by this point (via sendCoinsOffline -> commitTx), so it has // a txConfidenceListener registered. Once the tx is broadcast the peers will update the memory pool with the // count of seen peers, the memory pool will update the transaction confidence object, that will invoke the // txConfidenceListener which will in turn invoke the wallets event listener onTransactionConfidenceChanged // method. peerGroup.broadcastTransaction(sendRequest.tx); log.debug("Sending transaction '" + Utils.bytesToHexString(sendRequest.tx.bitcoinSerialize()) + "'"); } catch (VerificationException e1) { e1.printStackTrace(); } Transaction sendTransaction = sendRequest.tx; log.debug("MultiBitService#sendCoins - Sent coins has completed"); // We should never try to send more coins than we have! // throw an exception if sendTransaction is null - no money. if (sendTransaction != null) { log.debug("MultiBitService#sendCoins - Sent coins. Transaction hash is {}", sendTransaction.getHashAsString() + ", identityHashcode = " + System.identityHashCode(sendTransaction)); if (sendTransaction.getConfidence() != null) { log.debug("Added bitcoinController " + System.identityHashCode(bitcoinController) + " as listener to tx = " + sendTransaction.getHashAsString()); sendTransaction.getConfidence().addEventListener(bitcoinController); } else { log.debug("Cannot add bitcoinController as listener to tx = " + sendTransaction.getHashAsString() + " no transactionConfidence"); } try { bitcoinController.getFileHandler().savePerWalletModelData(perWalletModelData, false); } catch (WalletSaveException | WalletVersionException wse) { log.error(wse.getClass().getCanonicalName() + " " + wse.getMessage()); MessageManager.INSTANCE.addMessage(new Message(wse.getClass().getCanonicalName() + " " + wse.getMessage())); } try { // Notify other wallets of the send (it might be a send to or from them). List<WalletData> perWalletModelDataList = bitcoinController.getModel().getPerWalletModelDataList(); if (perWalletModelDataList != null) { for (WalletData loopPerWalletModelData : perWalletModelDataList) { if (!perWalletModelData.getWalletFilename().equals(loopPerWalletModelData.getWalletFilename())) { Wallet loopWallet = loopPerWalletModelData.getWallet(); if (loopWallet.isPendingTransactionRelevant(sendTransaction)) { // The loopPerWalletModelData is marked as dirty. if (loopPerWalletModelData.getWalletInfo() != null) { synchronized (loopPerWalletModelData.getWalletInfo()) { loopPerWalletModelData.setDirty(true); } } else { loopPerWalletModelData.setDirty(true); } if (loopWallet.getTransaction(sendTransaction.getHash()) == null) { log.debug("MultiBit adding a new pending transaction for the wallet '" + loopPerWalletModelData.getWalletDescription() + "'\n" + sendTransaction.toString()); loopWallet.receivePending(sendTransaction, null); } } } } } } catch (VerificationException e) { e.printStackTrace(); } } return sendTransaction; } public PeerGroup getPeerGroup() { return peerGroup; } public MultiBitBlockChain getChain() { return blockChain; } public BlockStore getBlockStore() { return blockStore; } public SecureRandom getSecureRandom() { return secureRandom; } ; public String getCheckpointsFilename() { return checkpointsFilename; } public MultiBitCheckpointManager getCheckpointManager() { return checkpointManager; } }