package org.multibit.hd.ui.controllers; import com.google.common.base.Optional; import com.google.common.base.Preconditions; import com.google.common.eventbus.Subscribe; import com.google.common.util.concurrent.*; import org.bitcoinj.core.Coin; import org.bitcoinj.core.Transaction; import org.bitcoinj.core.Wallet; import org.joda.time.DateTime; import org.multibit.commons.concurrent.SafeExecutors; import org.multibit.commons.utils.Dates; import org.multibit.hd.core.atom.AscendingAtomEntryComparator; import org.multibit.hd.core.atom.AtomEntry; import org.multibit.hd.core.atom.AtomFeed; import org.multibit.hd.core.atom.AtomFeeds; import org.multibit.hd.core.config.BitcoinConfiguration; import org.multibit.hd.core.config.Configurations; import org.multibit.hd.core.dto.*; import org.multibit.hd.core.error_reporting.ExceptionHandler; import org.multibit.hd.core.events.*; import org.multibit.hd.core.exceptions.PaymentsSaveException; import org.multibit.hd.core.exchanges.ExchangeKey; import org.multibit.hd.core.managers.BackupManager; import org.multibit.hd.core.managers.InstallationManager; import org.multibit.hd.core.managers.WalletManager; import org.multibit.hd.core.services.*; import org.multibit.hd.core.store.TransactionInfo; import org.multibit.hd.hardware.core.HardwareWalletService; import org.multibit.hd.hardware.core.events.HardwareWalletEvent; import org.multibit.hd.hardware.core.events.HardwareWalletEventType; import org.multibit.hd.hardware.core.events.HardwareWalletEvents; import org.multibit.hd.hardware.core.messages.Features; import org.multibit.hd.hardware.core.messages.HardwareWalletMessage; import org.multibit.hd.ui.audio.Sounds; import org.multibit.hd.ui.events.controller.ControllerEvents; import org.multibit.hd.ui.events.view.ComponentChangedEvent; import org.multibit.hd.ui.events.view.SwitchWalletEvent; import org.multibit.hd.ui.events.view.ViewEvents; import org.multibit.hd.ui.events.view.WizardHideEvent; import org.multibit.hd.ui.languages.Languages; import org.multibit.hd.ui.languages.MessageKey; import org.multibit.hd.ui.models.AlertModel; import org.multibit.hd.ui.models.Models; import org.multibit.hd.ui.platform.listener.*; import org.multibit.hd.ui.services.ExternalDataListeningService; import org.multibit.hd.ui.utils.HtmlUtils; import org.multibit.hd.ui.utils.SafeDesktop; import org.multibit.hd.ui.views.MainView; import org.multibit.hd.ui.views.ViewKey; import org.multibit.hd.ui.views.components.Buttons; import org.multibit.hd.ui.views.components.Panels; import org.multibit.hd.ui.views.fonts.AwesomeIcon; import org.multibit.hd.ui.views.screens.Screen; import org.multibit.hd.ui.views.themes.Theme; import org.multibit.hd.ui.views.themes.ThemeKey; import org.multibit.hd.ui.views.themes.Themes; import org.multibit.hd.ui.views.wizards.Wizards; import org.multibit.hd.ui.views.wizards.buy_sell.BuySellState; import org.multibit.hd.ui.views.wizards.credentials.CredentialsRequestType; import org.multibit.hd.ui.views.wizards.credentials.CredentialsState; import org.multibit.hd.ui.views.wizards.credentials.CredentialsWizard; import org.multibit.hd.ui.views.wizards.edit_wallet.EditWalletState; import org.multibit.hd.ui.views.wizards.edit_wallet.EditWalletWizardModel; import org.multibit.hd.ui.views.wizards.exit.ExitState; import org.multibit.hd.ui.views.wizards.use_hardware_wallet.UseHardwareWalletState; import org.multibit.hd.ui.views.wizards.welcome.WelcomeWizard; import org.multibit.hd.ui.views.wizards.welcome.WelcomeWizardState; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nullable; import javax.swing.*; import java.awt.*; import java.awt.event.ActionEvent; import java.io.File; import java.net.URI; import java.util.*; import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; /** * <p>Controller for the main view </p> * <ul> * <li>Handles interaction between the model and the view</li> * </ul> * <p>To allow complete separation between Model, View and Controller all interactions are handled using application events</p> */ public class MainController extends AbstractController implements GenericOpenURIEventListener, GenericOpenFilesEventListener, GenericPreferencesEventListener, GenericAboutEventListener, GenericQuitEventListener { private static final Logger log = LoggerFactory.getLogger(MainController.class); private Optional<ExchangeTickerService> exchangeTickerService = Optional.absent(); private final ListeningExecutorService handoverExecutorService = SafeExecutors.newSingleThreadExecutor("wizard-handover"); // Keep a thread pool for transaction status checking private static final ListeningExecutorService transactionCheckingExecutorService = SafeExecutors.newFixedThreadPool(10, "transaction-checking"); // Provide a separate executor service for wallet operations private static final ListeningExecutorService walletExecutorService = SafeExecutors.newFixedThreadPool(10, "wallet-services"); private static final int NUMBER_OF_SECONDS_TO_WAIT_BEFORE_TRANSACTION_CHECKING = 60; // Keep track of other controllers for use after a preferences change private final HeaderController headerController; // Main view may be replaced during a soft shutdown private MainView mainView; // Start with the assumption that it is fine to avoid annoying "everything is OK" alert private RAGStatus lastExchangeSeverity = RAGStatus.GREEN; // Assume a password rather than a hardware wallet cipher key private CredentialsRequestType deferredCredentialsRequestType = CredentialsRequestType.PASSWORD; /** * The delay between a wipe and insertion of a new device */ private static final int HARDWARE_WALLET_WIPE_TIME_THRESHOLD = 4; /** * The last time a Trezor device was wiped (or yesterday as the default) */ private DateTime lastWipedHardwareWalletDateTime = Dates.nowUtc().minusDays(1); /** * Whether alerts should be fired when new transactions appear (true = fire alerts, false = suppress alerts) */ private static boolean fireTransactionAlerts = true; /** * @param headerController The header controller */ public MainController(HeaderController headerController) { super(); // MainController must also subscribe to ViewEvents ViewEvents.subscribe(this); Preconditions.checkNotNull(headerController, "'headerController' must be present"); this.headerController = headerController; } @Override public void unsubscribe() { super.unsubscribe(); ViewEvents.unsubscribe(this); } @Subscribe public void onShutdownEvent(ShutdownEvent shutdownEvent) { log.info("Received shutdown: {}", shutdownEvent.getShutdownType()); switch (shutdownEvent.getShutdownType()) { case HARD: case SOFT: // Unsubscribe views for events if (mainView != null) { mainView.unsubscribe(); } // Unregister controllers for events if (headerController != null) { headerController.unsubscribe(); } // Unregister this unsubscribe(); shutdownCurrentWallet(shutdownEvent.getShutdownType()); break; case SWITCH: // Do nothing - the wizard hide event triggers this process } } @Subscribe public void onWizardHideEvent(WizardHideEvent event) { Preconditions.checkState(SwingUtilities.isEventDispatchThread(), "This event should be running on the EDT."); log.debug("Wizard hide: '{}' Exit/Cancel: {}", event.getPanelName(), event.isExitCancel()); if (!event.isExitCancel()) { // Successful wizard interaction if (WelcomeWizardState.CREATE_WALLET_REPORT.name().equals(event.getPanelName()) || WelcomeWizardState.HARDWARE_CREATE_WALLET_REPORT.name().equals(event.getPanelName()) || WelcomeWizardState.RESTORE_WALLET_REPORT.name().equals(event.getPanelName()) || WelcomeWizardState.RESTORE_PASSWORD_REPORT.name().equals(event.getPanelName()) || WelcomeWizardState.WELCOME_SELECT_WALLET.name().equals(event.getPanelName()) ) { // We have just finished the welcome wizard and want the credentials screen handoverToCredentialsWizard(); } if (CredentialsState.CREDENTIALS_PRESS_CONFIRM_FOR_UNLOCK.name().equals(event.getPanelName()) || CredentialsState.CREDENTIALS_LOAD_WALLET_REPORT.name().equals(event.getPanelName()) ) { // We have just finished the credentials wizard and want the wallet details screen hideCredentialsWizard(); } if (CredentialsState.CREDENTIALS_RESTORE.name().equals(event.getPanelName())) { // We are exiting the credentials wizard via the restore button and want the welcome wizard handoverToWelcomeWizardRestore(); } if (CredentialsState.CREDENTIALS_CREATE.name().equals(event.getPanelName())) { // We are exiting the credentials wizard via the create button and want the welcome wizard handoverToWelcomeWizardCreate(); } if (CredentialsState.CREDENTIALS_REQUEST_CIPHER_KEY.name().equals(event.getPanelName()) || CredentialsState.CREDENTIALS_REQUEST_MASTER_PUBLIC_KEY.name().equals(event.getPanelName())) { // We are exiting the credentials wizard as the hardware wallet is uninitialised and want the welcome wizard handoverToWelcomeWizardCreateHardwareWallet(); } if (EditWalletState.EDIT_WALLET.name().equals(event.getPanelName())) { // We are exiting the edit wallet wizard and want the details screen to update with changes // Update the details screen (title etc) String walletName = ((EditWalletWizardModel) event.getWizardModel()).getWalletSummary().getName(); hideEditWalletWizard(walletName); } if (ExitState.SWITCH_WALLET.equals(event.getWizardModel().getState())) { // We have just finished with the exit wizard and want the credentials screen handleSwitchWallet(); } if (ExitState.CONFIRM_EXIT.equals(event.getWizardModel().getState())) { // We have just finished with the exit wizard and want to shut down CoreEvents.fireShutdownEvent(ShutdownEvent.ShutdownType.HARD); } // The buy/sell dialog requires the sidebar to regain focus for accessibility reasons if (BuySellState.SHOW_PARTNER_NOTES.name().equals(event.getPanelName())) { mainView.sidebarRequestFocus(); } // Do nothing other than usual wizard hide operations } else { // Shift focus depending on what was cancelled hideAsExitCancel(event.getPanelName()); } } @Subscribe public void onSwitchWalletEvent(SwitchWalletEvent event) { log.debug("Received 'switch wallet' event"); handleSwitchWallet(); } /** * <p>Update all views to use the current configuration</p> * * @param event The change configuration event */ @Subscribe public synchronized void onConfigurationChangedEvent(ConfigurationChangedEvent event) { log.debug("Received 'configuration changed' event"); Preconditions.checkNotNull(event, "'event' must be present"); if (mainView.isShowExitingWelcomeWizard()) { // Restarting the main view from a language change handleBasicMainViewRefresh(); } else { // Restarting the main view from a configuration change handleFullMainViewRefresh(); } } @Subscribe public void onBitcoinNetworkChangeEvent(BitcoinNetworkChangedEvent event) { log.trace("Received 'Bitcoin network changed' event"); Preconditions.checkNotNull(event, "'event' must be present"); Preconditions.checkNotNull(event.getSummary(), "'summary' must be present"); final BitcoinNetworkSummary summary = event.getSummary(); Preconditions.checkNotNull(summary.getSeverity(), "'severity' must be present"); Preconditions.checkNotNull(summary.getMessageKey(), "'errorKey' must be present"); Preconditions.checkNotNull(summary.getMessageData(), "'errorData' must be present"); if (BitcoinNetworkStatus.SYNCHRONIZED.equals(event.getSummary().getStatus())) { // Enable alerts for new transactions (suppressed on repair wallet for user simplicity) MainController.setFireTransactionAlerts(true); // Ensure that the header shows the header after a sync (if the configuration permits) final boolean viewHeader = Configurations.currentConfiguration.getAppearance().isShowBalance(); ViewEvents.fireViewChangedEvent(ViewKey.HEADER, viewHeader); // For Trezor hard wallets, get the date of the earliest transaction and use it to set the // earliestKeyCreationDate. This enables future repair wallets to be quicker if (WalletManager.INSTANCE.getCurrentWalletSummary().isPresent() && WalletManager.INSTANCE.getCurrentWalletSummary().get().getWalletType() == WalletType.TREZOR_HARD_WALLET) { // See if the synced wallet has transactions Wallet wallet = WalletManager.INSTANCE.getCurrentWalletSummary().get().getWallet(); java.util.List<Transaction> transactions = wallet.getTransactionsByTime(); if (!transactions.isEmpty()) { // Last is the oldest Date earliestTransactionDate = transactions.get(transactions.size() - 1).getUpdateTime(); if (earliestTransactionDate != null) { // Set the wallet key creation time to be the transaction date (minus one day to cater for blockchain forkiness) wallet.setEarliestKeyCreationTime(earliestTransactionDate.getTime() / 1000 - Dates.NUMBER_OF_SECONDS_IN_A_DAY); log.debug("Setting hardware wallet 'earliestKeyCreationDate' to one day before : {}", earliestTransactionDate); } } } } final String localisedMessage; if (summary.getMessageKey().isPresent() && summary.getMessageData().isPresent()) { // There is a message key with data localisedMessage = Languages.safeText(summary.getMessageKey().get(), summary.getMessageData().get()); } else if (summary.getMessageKey().isPresent()) { // There is a message key only localisedMessage = Languages.safeText(summary.getMessageKey().get()); } else { // There is no message key so use the status only localisedMessage = summary.getStatus().name(); } ViewEvents.fireProgressChangedEvent(localisedMessage, summary.getPercent()); // Ensure everyone is aware of the update ViewEvents.fireSystemStatusChangedEvent(localisedMessage, summary.getSeverity()); } @Subscribe public void onExchangeStatusChangeEvent(ExchangeStatusChangedEvent event) { log.trace("Received 'Exchange status changed' event"); Preconditions.checkNotNull(event, "'event' must be present"); Preconditions.checkNotNull(event.getSummary(), "'summary' must be present"); ExchangeSummary summary = event.getSummary(); if (!lastExchangeSeverity.equals(summary.getSeverity())) { log.debug("Event severity has changed"); Preconditions.checkNotNull(summary.getSeverity(), "'severity' must be present"); Preconditions.checkNotNull(summary.getMessageKey(), "'errorKey' must be present"); Preconditions.checkNotNull(summary.getMessageData(), "'errorData' must be present"); final String localisedMessage; if (summary.getMessageKey().isPresent() && summary.getMessageData().isPresent()) { // There is a message key with data localisedMessage = Languages.safeText(summary.getMessageKey().get(), summary.getMessageData().get()); } else if (summary.getMessageKey().isPresent()) { // There is a message key only localisedMessage = Languages.safeText(summary.getMessageKey().get()); } else { // There is no message key so use the status only localisedMessage = summary.getStatus().name(); } // Action to show the "exchange settings" wizard AbstractAction action = new AbstractAction() { @Override public void actionPerformed(ActionEvent e) { ControllerEvents.fireRemoveAlertEvent(); Panels.showLightBox(Wizards.newExchangeSettingsWizard().getWizardScreenHolder()); } }; JButton button = Buttons.newAlertPanelButton(action, MessageKey.SETTINGS, MessageKey.SETTINGS_TOOLTIP, AwesomeIcon.CHECK); // Provide the alert ControllerEvents.fireAddAlertEvent( Models.newAlertModel( localisedMessage, summary.getSeverity(), button) ); } } @Subscribe public void onEnvironmentEvent(final EnvironmentEvent event) { log.trace("Received 'environment' event"); Preconditions.checkNotNull(event, "'event' must be present"); Preconditions.checkNotNull(event.getSummary(), "'summary' must be present"); final EnvironmentSummary summary = event.getSummary(); Preconditions.checkNotNull(summary.getSeverity(), "'severity' must be present"); Preconditions.checkNotNull(summary.getMessageKey(), "'errorKey' must be present"); Preconditions.checkNotNull(summary.getMessageData(), "'errorData' must be present"); final String localisedMessage; if (summary.getMessageKey().isPresent() && summary.getMessageData().isPresent()) { // There is a message key with data localisedMessage = Languages.safeText(summary.getMessageKey().get(), summary.getMessageData().get()); } else if (summary.getMessageKey().isPresent()) { // There is a message key only localisedMessage = Languages.safeText(summary.getMessageKey().get()); } else { // There is no message key so use the status only localisedMessage = summary.getSeverity().name(); } switch (summary.getAlertType()) { case DEBUGGER_ATTACHED: case BACKUP_FAILED: // Append general security advice allowing for LTR/RTL ControllerEvents.fireAddAlertEvent( Models.newAlertModel( localisedMessage + " " + Languages.safeText(CoreMessageKey.SECURITY_ADVICE), summary.getSeverity()) ); break; case SYSTEM_TIME_DRIFT: // Present the localised message ControllerEvents.fireAddAlertEvent( Models.newAlertModel( localisedMessage, summary.getSeverity()) ); break; case ATOM_FEED_CHECK: // Check configuration allows alert if (!Configurations.currentConfiguration.getAppearance().isShowAtomFeedAlert()) { // Ignore return; } // Check for article URI if (!event.getSummary().getUri().isPresent()) { // Ignore return; } String eventUri = event.getSummary().getUri().get().toString(); String latestUri = Configurations.currentConfiguration.getAppearance().getLatestArticleUri(); // Check against local latest if (eventUri.equalsIgnoreCase(latestUri)) { // Ignore return; } // Persist the article URI even if the user subsequently ignores it to prevent repeat alerts Configurations.currentConfiguration.getAppearance().setLatestArticleUri(eventUri); Configurations.persistCurrentConfiguration(); // Creation of buttons must be on the EDT SwingUtilities.invokeLater(new Runnable() { @Override public void run() { JButton browserButton = Buttons.newAlertPanelButton( getShowAtomFeedArticleAction(event.getSummary().getUri().get()), MessageKey.YES, MessageKey.YES_TOOLTIP, AwesomeIcon.CHECK ); // Ensure we provide a button ControllerEvents.fireAddAlertEvent( Models.newAlertModel( localisedMessage, summary.getSeverity(), browserButton) ); } }); break; case CERTIFICATE_FAILED: // Creation of buttons must be on the EDT SwingUtilities.invokeLater(new Runnable() { @Override public void run() { // Create a button to the repair wallet tool JButton button = Buttons.newAlertPanelButton(getShowRepairWalletAction(), MessageKey.REPAIR, MessageKey.REPAIR_TOOLTIP, AwesomeIcon.MEDKIT); // Append general security advice allowing for LTR/RTL ControllerEvents.fireAddAlertEvent( Models.newAlertModel( localisedMessage + "\n" + Languages.safeText(CoreMessageKey.SECURITY_ADVICE), summary.getSeverity(), button) ); } }); break; case UNSUPPORTED_FIRMWARE_ATTACHED: case DEPRECATED_FIRMWARE_ATTACHED: case UNSUPPORTED_CONFIGURATION_ATTACHED: Optional<WalletSummary> walletSummary = WalletManager.INSTANCE.getCurrentWalletSummary(); if (walletSummary.isPresent() && !Panels.isLightBoxShowing()) { // Present the localised message as an alert bar since a popover won't appear any time soon ControllerEvents.fireAddAlertEvent( Models.newAlertModel( localisedMessage, summary.getSeverity()) ); } break; default: throw new IllegalStateException("Unknown alert type: " + summary.getAlertType()); } } @Subscribe public void onComponentChangedEvent(ComponentChangedEvent event) { // Check for specific component changes if (UseHardwareWalletState.CONFIRM_WIPE_DEVICE.name().equals(event.getPanelName())) { // The user has successfully completed wiping a Trezor device lastWipedHardwareWalletDateTime = (DateTime) event.getComponentModel().get(); } } /** * @param mainView The main view (the deferred credentials request type will also be set) */ public void setMainView(MainView mainView) { this.mainView = mainView; log.debug("Setting MainView credentials type: {}", deferredCredentialsRequestType.name()); mainView.setCredentialsRequestType(deferredCredentialsRequestType); } /** * <p>Complete tear down and rebuild of the detail screens comprising the main view</p> * <p>All services are restarted</p> * <p>The main view references remain intact</p> * <p/> * <p>This is not done through a simple SWITCH shutdown event being fired since the order * of shutdown is important and cannot be guaranteed otherwise.</p> */ private void handleSwitchWallet() { // Run this in a separate thread to ensure the original event returns promptly // and that the switch panel view is able to close before the MainView resets final ListenableFuture<Boolean> future = handoverExecutorService.submit( new Callable<Boolean>() { @Override public Boolean call() { log.trace("Using switch wallet view refresh"); // Sleep for a short time to reduce UI jolt Uninterruptibles.sleepUninterruptibly(200, TimeUnit.MILLISECONDS); // Hide the application frame to prevent user interacting with the detail // panels after the exit panel view has hidden // It is very tricky to get the timing right so hiding the UI is the safest // course of action here SwingUtilities.invokeLater( new Runnable() { @Override public void run() { // Dim the application using the glass pane Panels.getApplicationFrame().getRootPane().getGlassPane().setVisible(true); } }); // Sleep for a short time to allow UI events to occur Uninterruptibles.sleepUninterruptibly(100, TimeUnit.MILLISECONDS); try { // Close the supporting services // This can take some time shutdownCurrentWallet(ShutdownEvent.ShutdownType.SWITCH); } catch (Exception e) { log.error("Failed to shutdown current wallet", e); } // Avoiding repeating latest events which will leave traces of the earlier wallet // on the MainView during unlock mainView.setRepeatLatestEvents(false); log.debug("Find existing wallet directories"); // Check for any pre-existing wallets in the application directory File applicationDataDirectory = InstallationManager.getOrCreateApplicationDataDirectory(); java.util.List<File> walletDirectories = WalletManager.findWalletDirectories(applicationDataDirectory); if (walletDirectories.isEmpty()) { log.debug("No wallets in the directory - showing the welcome wizard"); mainView.setShowExitingWelcomeWizard(true); mainView.setShowExitingCredentialsWizard(false); } else { log.debug("Wallets are present - showing the credentials wizard"); mainView.setShowExitingCredentialsWizard(true); mainView.setShowExitingWelcomeWizard(false); } log.debug("Perform MainView refresh"); SwingUtilities.invokeLater( new Runnable() { @Override public void run() { // Switch off the background dimming and trigger the showing of the wizard Panels.getApplicationFrame().getRootPane().getGlassPane().setVisible(false); mainView.refresh(false); mainView.setRepeatLatestEvents(true); } }); // Sleep for a short time to allow UI events to occur Uninterruptibles.sleepUninterruptibly(100, TimeUnit.MILLISECONDS); return true; } }); Futures.addCallback( future, new FutureCallback<Boolean>() { @Override public void onSuccess(@Nullable Boolean result) { // We successfully switched the wallet } @Override public void onFailure(Throwable t) { // Show the application frame to provide some assistance to user SwingUtilities.invokeLater( new Runnable() { @Override public void run() { Panels.getApplicationFrame().setVisible(false); } }); // Sleep for a short time to allow UI events to occur Uninterruptibles.sleepUninterruptibly(100, TimeUnit.MILLISECONDS); // Use the generic handler since we're all over the place at this point ExceptionHandler.handleThrowable(t); } } ); } /** * Handles the process of shutting down the current wallet support services */ public void shutdownCurrentWallet(ShutdownEvent.ShutdownType shutdownType) { log.debug("Shutdown current wallet..."); if (!shutdownType.equals(ShutdownEvent.ShutdownType.SWITCH)) { // Hide the UI if not switching shutdownMainView(); } // Provide a graceful shutdown of the relevant core services in the correct order CoreServices.shutdownNow(shutdownType); // Close the current wallet WalletManager.INSTANCE.shutdownNow(shutdownType); // Close the backup manager for the wallet BackupManager.INSTANCE.shutdownNow(); // Close the installation manager InstallationManager.shutdownNow(shutdownType); } /** * Shutdown the MainView and dispose of the main application frame */ private void shutdownMainView() { // Dispose of the main view and all its attendant references log.debug("Disposing of MainView"); SwingUtilities.invokeLater( new Runnable() { @Override public void run() { Panels.hideLightBoxIfPresent(); Panels.getApplicationFrame().dispose(); } }); // Remove the reference mainView = null; // Sleep for a short time to allow UI events to occur Uninterruptibles.sleepUninterruptibly(100, TimeUnit.MILLISECONDS); } /** * <p>Complete tear down and rebuild of the detail screens comprising the main view</p> * <p>Non-Bitcoin services are restarted</p> * <p>The main view references remain intact</p> */ private void handleFullMainViewRefresh() { log.debug("Using full view refresh (configuration change)"); // Switch the exchange ticker service before the UI to ensure the // exchange rate provider is rendered correctly handleExchange(); // Ensure the Swing thread can perform a complete refresh SwingUtilities.invokeLater( new Runnable() { public void run() { // Switch the theme before any other UI building takes place handleTheme(); // Rebuild MainView contents handleLocale(); // Force a frame redraw Panels.getApplicationFrame().invalidate(); // Rebuild the detail views and alert panels mainView.refresh(false); // Show the current detail screen Screen screen = Screen.valueOf(Configurations.currentConfiguration.getAppearance().getCurrentScreen()); ViewEvents.fireShowDetailScreenEvent(screen); // Trigger the alert panels to refresh headerController.refresh(); } }); // Restart the hardware wallet service (devices may have changed) handleHardwareWallets(); // Check for system time drift (runs in the background) handleSystemTimeDrift(); // Check for Atom feed change (runs in the background) handleAtomFeedCheck(); } /** * <p>Partial rebuild of the detail screens comprising the main view</p> * <p>The main view references remain intact</p> */ private void handleBasicMainViewRefresh() { log.debug("Using simplified view refresh (language change)"); // Ensure the Swing thread can perform a complete refresh SwingUtilities.invokeLater( new Runnable() { public void run() { // Switch the theme before any other UI building takes place handleTheme(); // Rebuild MainView contents handleLocale(); // Force a frame redraw Panels.getApplicationFrame().invalidate(); // Rebuild the detail views and alert panels if (mainView != null) { mainView.refresh(true); } } }); } @Override public void onAboutEvent(GenericAboutEvent event) { if (WalletManager.INSTANCE.getCurrentWalletSummary().isPresent()) { // Show the Tools screen ViewEvents.fireShowDetailScreenEvent(Screen.TOOLS); // Show the About screen Panels.showLightBox(Wizards.newAboutWizard().getWizardScreenHolder()); } } @Subscribe @Override public void onOpenURIEvent(GenericOpenURIEvent event) { ExternalDataListeningService.parseRawData(event.getURI().toString()); // Now would be a good time to attempt to alert the user ExternalDataListeningService.purgeAlertModelQueue(); } @Override public void onOpenFilesEvent(GenericOpenFilesEvent event) { for (File file : event.getFiles()) { URI uri = file.toURI(); ExternalDataListeningService.parseRawData(uri.toString()); } // Now would be a good time to attempt to alert the user ExternalDataListeningService.purgeAlertModelQueue(); } @Override public void onPreferencesEvent(GenericPreferencesEvent event) { if (WalletManager.INSTANCE.getCurrentWalletSummary().isPresent()) { // Show the Preferences screen ViewEvents.fireShowDetailScreenEvent(Screen.SETTINGS); } } @Override public void onQuitEvent(GenericQuitEvent event, GenericQuitResponse response) { log.debug("Received quit event (close button). Initiating hard shutdown..."); CoreEvents.fireShutdownEvent(ShutdownEvent.ShutdownType.HARD); } /** * @param transactionSeenEvent The event (very high frequency during synchronisation) */ @Subscribe public void onTransactionSeenEvent(TransactionSeenEvent transactionSeenEvent) { if (transactionSeenEvent.isFirstAppearanceInWallet() && isFireTransactionAlerts()) { log.debug("Firing an alert for a new transaction"); transactionSeenEvent.setFirstAppearanceInWallet(false); Sounds.playPaymentReceived(Configurations.currentConfiguration.getSound()); AlertModel alertModel = Models.newPaymentReceivedAlertModel(transactionSeenEvent); ControllerEvents.fireAddAlertEvent(alertModel); } } /** * Make sure that when a transaction is successfully created its 'metadata' is stored in a transactionInfo * * @param transactionCreationEvent The transaction creation event from the EventBus */ @Subscribe public void onTransactionCreationEvent(TransactionCreationEvent transactionCreationEvent) { initiateDelayedTransactionStatusCheck(transactionCreationEvent); // Only store successful transactions if (!transactionCreationEvent.isTransactionCreationWasSuccessful()) { return; } // Create a transactionInfo to match the event created TransactionInfo transactionInfo = new TransactionInfo(); transactionInfo.setHash(transactionCreationEvent.getTransactionId()); String note = transactionCreationEvent.getNotes().or(""); transactionInfo.setNote(note); // Append miner's fee info log.debug("Transaction creation event with mining fee of {}", transactionCreationEvent.getMiningFeePaid()); transactionInfo.setMinerFee(transactionCreationEvent.getMiningFeePaid()); // Set the fiat payment amount transactionInfo.setAmountFiat(transactionCreationEvent.getFiatPayment().orNull()); // Set whether the transaction was created in this copy of MBHD // This is a copy of the TransactionConfidence.Source which does not survive a repair wallet. transactionInfo.setSentBySelf(transactionCreationEvent.isSentByMe()); WalletService walletService = CoreServices.getCurrentWalletService().get(); walletService.addTransactionInfo(transactionInfo); log.debug("Added transactionInfo {} to walletService {}", transactionInfo, walletService); try { CharSequence password = WalletManager.INSTANCE.getCurrentWalletSummary().get().getWalletPassword().getPassword(); if (password != null) { walletService.writePayments(password); } } catch (PaymentsSaveException pse) { ExceptionHandler.handleThrowable(pse); } } /** * Respond to a hardware wallet system event * * @param event The event */ @Subscribe public void onHardwareWalletEvent(final HardwareWalletEvent event) { log.debug("Received hardware event: '{}'", event.getEventType().name()); if (!ApplicationEventService.isHardwareWalletEventAllowed()) { log.debug("Ignoring hardware wallet event due to event threshold"); return; } // Check if CoreServices has identified a hardware wallet (could be just starting or in FEST mode) if (!CoreServices.getCurrentHardwareWalletService().isPresent()) { CoreServices.useFirstReadyHardwareWalletService(); } // Quick check for relevancy switch (event.getEventType()) { case SHOW_DEVICE_STOPPED: case SHOW_DEVICE_DETACHED: // Rely on the hardware wallet wizard to inform the user // An alert tends to stack and gets messy/irrelevant deferredCredentialsRequestType = CredentialsRequestType.PASSWORD; // Clear any alert-inducing events as the user has detached the device Optional<EnvironmentEvent> lastEnvironmentEventOptional = CoreServices.getApplicationEventService().getLatestEnvironmentEvent(); if (lastEnvironmentEventOptional.isPresent() && lastEnvironmentEventOptional.get().is(EnvironmentSummary.AlertType.UNSUPPORTED_FIRMWARE_ATTACHED)) { CoreServices.getApplicationEventService().onEnvironmentEvent(null); } if (lastEnvironmentEventOptional.isPresent() && lastEnvironmentEventOptional.get().is(EnvironmentSummary.AlertType.UNSUPPORTED_CONFIGURATION_ATTACHED)) { CoreServices.getApplicationEventService().onEnvironmentEvent(null); } if (lastEnvironmentEventOptional.isPresent() && lastEnvironmentEventOptional.get().is(EnvironmentSummary.AlertType.DEPRECATED_FIRMWARE_ATTACHED)) { CoreServices.getApplicationEventService().onEnvironmentEvent(null); } break; case SHOW_DEVICE_FAILED: handleShowDeviceFailed(event); break; case SHOW_DEVICE_READY: handleShowDeviceReady(event); break; default: // The AbstractHardwareWalletWizard handles everything when a wizard is showing return; } log.debug("Selected deferred credentials type: {}", deferredCredentialsRequestType); // Set the credentials immediately if possible so that MainView.refresh() works correctly if (mainView != null) { mainView.setCredentialsRequestType(deferredCredentialsRequestType); } } /** * <p>Handle the "show device failure" event</p> * <p>There are two conditions to check here: a failed device from a USB communication * problem, or one that has unsupported firmware (e.g. Trezor 1.2.x).</p> * <p>If the device is unsupported then two forms of alert are needed: a popover if a * light box is showing; an alert bar if a wallet is open.</p> * * @param event The hardware wallet event */ private void handleShowDeviceFailed(final HardwareWalletEvent event) { // Determine the nature of the failure if (!CoreServices.getCurrentHardwareWalletService().isPresent()) { // Use the alert bar mechanism SwingUtilities.invokeLater( new Runnable() { @Override public void run() { // Attempt to create a suitable alert model in addition to view event AlertModel alertModel = Models.newHardwareWalletAlertModel(event); ControllerEvents.fireAddAlertEvent(alertModel); } }); return; } // Optional<Features> featuresOptional = CoreServices.getCurrentHardwareWalletService().get().getContext().getFeatures(); boolean isUnsupportedFirmware = featuresOptional.isPresent() && !featuresOptional.get().isSupported(); boolean isUnsupportedConfigurationPassphrase = featuresOptional.isPresent() && featuresOptional.get().isSupported() && featuresOptional.get().hasPassphraseProtection(); log.warn("Hardware device failed. Unsupported firmware: {} Passphrase: {}", isUnsupportedFirmware, isUnsupportedConfigurationPassphrase); if (isUnsupportedFirmware) { // Show as a environment popover CoreEvents.fireEnvironmentEvent(EnvironmentSummary.newUnsupportedFirmware()); } else if (isUnsupportedConfigurationPassphrase) { // Show as an info popover CoreEvents.fireEnvironmentEvent(EnvironmentSummary.newUnsupportedConfigurationPassphrase()); } else { // Use the alert bar mechanism SwingUtilities.invokeLater( new Runnable() { @Override public void run() { // Attempt to create a suitable alert model in addition to view event AlertModel alertModel = Models.newHardwareWalletAlertModel(event); ControllerEvents.fireAddAlertEvent(alertModel); } }); } // Set the deferred credentials request type to be password since the device has failed deferredCredentialsRequestType = CredentialsRequestType.PASSWORD; } /** * <p>Handle the "show device ready" event</p> * <p>Show an alert if a hardware wallet connects when:</p> * <ul> * <li>there is a current wallet</li> * <li>the current wallet is not the same "hard" wallet as in the alert (different device)</li> * <li>there has not been a "wipe" in the last few seconds</li> * <li>the device has a supported configuration (e.g. not passphrase protected)</li> * </ul> * * @param event The hardware wallet event */ private void handleShowDeviceReady(final HardwareWalletEvent event) { // Configuration check if (event.getMessage().isPresent()) { final HardwareWalletMessage message = event.getMessage().get(); if (message instanceof Features) { Features features = (Features) message; if (features.hasPassphraseProtection()) { handleShowDeviceFailed(event); return; } // You would check for deprecated firmware versions here - there are none currently. // See git history for how to do it. } } Optional<WalletSummary> walletSummary = WalletManager.INSTANCE.getCurrentWalletSummary(); if (walletSummary.isPresent()) { Optional<HardwareWalletService> currentHardwareWalletService = CoreServices.getCurrentHardwareWalletService(); boolean showAlert = false; if (walletSummary.get().getWalletType() != WalletType.TREZOR_HARD_WALLET) { // Not currently using a hardware wallet so show the alert showAlert = true; } else { // Currently using a hardware wallet so check for a change in device if (currentHardwareWalletService.isPresent()) { // Get the current wallet name (label) String currentWalletName = walletSummary.get().getName(); // Get the current features Optional<Features> currentFeatures = currentHardwareWalletService.get().getContext().getFeatures(); // Check for a different device type String currentSource = currentHardwareWalletService.get().getContext().getClient().name(); String newSource = event.getSource(); if (!currentSource.equals(newSource)) { // Different device type (e.g. Trezor attached during KeepKey session) showAlert = true; } else { // Same device type (e.g. accidental detach of device or swap in advance of switch) // Check for a different device label if (currentFeatures.isPresent()) { // The current device is remains attached if (!currentFeatures.get().getLabel().equals(currentWalletName)) { showAlert = true; } } else { // The current device was detached so we can't tell if it is different showAlert = true; } } } } // Suppress alert if there has been a recent wipe if (currentHardwareWalletService.isPresent()) { if (lastWipedHardwareWalletDateTime .plusSeconds(HARDWARE_WALLET_WIPE_TIME_THRESHOLD) .isAfter(Dates.nowUtc())) { showAlert = false; } } if (showAlert) { // NOTE Currently getting a false positive due to FEST test use of WalletManager during fixture creation log.debug("Hardware wallet attached during an unlocked soft wallet session - showing alert"); SwingUtilities.invokeLater( new Runnable() { @Override public void run() { // Attempt to create a suitable alert model in addition to view event AlertModel alertModel = Models.newHardwareWalletAlertModel(event); ControllerEvents.fireAddAlertEvent(alertModel); } }); } } // Set the deferred credentials request type deferredCredentialsRequestType = CredentialsRequestType.HARDWARE; } /** * @return An action to show the "repair wallet" tool */ private AbstractAction getShowRepairWalletAction() { return new AbstractAction() { @Override public void actionPerformed(ActionEvent e) { Panels.showLightBox(Wizards.newRepairWalletWizard().getWizardScreenHolder()); } }; } /** * @return An action to show the help screen for 'spendable balance may be lower" */ private AbstractAction getShowHelpAction() { return new AbstractAction() { @Override public void actionPerformed(ActionEvent e) { // TODO show the 'spendable balance may be lower' help screen when it is written ViewEvents.fireShowDetailScreenEvent(Screen.HELP); } }; } /** * @return An action to show the Atom feed article */ private AbstractAction getShowAtomFeedArticleAction(final URI articleUri) { return new AbstractAction() { @Override public void actionPerformed(ActionEvent e) { SafeDesktop.browse(articleUri); ControllerEvents.fireRemoveAlertEvent(); } }; } /** * Handles the changes to the exchange ticker service */ private void handleExchange() { BitcoinConfiguration bitcoinConfiguration = Configurations.currentConfiguration.getBitcoin(); ExchangeKey exchangeKey; try { exchangeKey = ExchangeKey.valueOf(bitcoinConfiguration.getCurrentExchange()); } catch (IllegalArgumentException e) { // Exchange in configuration is not supported exchangeKey = ExchangeKey.NONE; Configurations.currentConfiguration.getBitcoin().setCurrentExchange(exchangeKey.name()); Configurations.persistCurrentConfiguration(); } if (ExchangeKey.OPEN_EXCHANGE_RATES.equals(exchangeKey)) { if (bitcoinConfiguration.getExchangeApiKeys().containsKey(ExchangeKey.OPEN_EXCHANGE_RATES.name())) { String apiKey = Configurations.currentConfiguration.getBitcoin().getExchangeApiKeys().get(ExchangeKey.OPEN_EXCHANGE_RATES.name()); exchangeKey.getExchange().get().getExchangeSpecification().setApiKey(apiKey); } } // Stop (with block) any existing exchange ticker service if (exchangeTickerService.isPresent()) { exchangeTickerService.get().shutdownNow(ShutdownEvent.ShutdownType.HARD); } // Create and start the exchange ticker service exchangeTickerService = Optional.of(CoreServices.createAndStartExchangeService(bitcoinConfiguration)); } /** * Handles the changes to the theme */ private void handleTheme() { Theme newTheme = ThemeKey.valueOf(Configurations.currentConfiguration.getAppearance().getCurrentTheme()).theme(); Themes.switchTheme(newTheme); } /** * Handles the changes to the locale (resource bundle change, frame locale, component orientation etc) */ private void handleLocale() { // Get the current locale Locale locale = Configurations.currentConfiguration.getLocale(); log.debug("Setting application frame to locale '{}'", locale); // Ensure the resource bundle is reset ResourceBundle.clearCache(); // Update the frame to allow for LTR or RTL transition Panels.getApplicationFrame().setLocale(locale); // Ensure LTR and RTL language formats are in place Panels.getApplicationFrame().applyComponentOrientation(ComponentOrientation.getOrientation(locale)); } /** * <p>Start the backup manager</p> */ private void handleBackupManager() { // Locate the installation directory File applicationDataDirectory = InstallationManager.getOrCreateApplicationDataDirectory(); // Initialise backup (must be before Bitcoin network starts and on the main thread) Optional<File> cloudBackupLocation = Optional.absent(); if (Configurations.currentConfiguration != null) { String cloudBackupLocationString = Configurations.currentConfiguration.getAppearance().getCloudBackupLocation(); if (cloudBackupLocationString != null && !"".equals(cloudBackupLocationString)) { File cloudBackupLocationAsFile = new File(cloudBackupLocationString); if (cloudBackupLocationAsFile.exists()) { cloudBackupLocation = Optional.of(cloudBackupLocationAsFile); } } } BackupManager.INSTANCE.initialise(applicationDataDirectory, cloudBackupLocation); BackupService backupService = CoreServices.getOrCreateBackupService(); backupService.start(); } /** * <p>Show any alerts coming about as part of the startup sequence (we have just unlocked a wallet).</p> * <p>See the ExternalDataListeningService for runtime handling</p> */ private void handleExternalDataAlerts() { // Now would be a good time to alert the user to any events ExternalDataListeningService.purgeAlertModelQueue(); } /** * <p>Restart the hardware wallet service if necessary and subscribe to hardware wallet events</p> */ public void handleHardwareWallets() { boolean isServiceAllowed = false; // Determine if at least one hardware wallet is selected if (Configurations.currentConfiguration.isTrezor()) { isServiceAllowed = true; } Optional<HardwareWalletService> currentHardwareWalletService = CoreServices.getCurrentHardwareWalletService(); // Check if the service is running and is allowed if (currentHardwareWalletService.isPresent() && !isServiceAllowed) { // Stop the service, all listeners and clear the CoreServices reference CoreServices.stopHardwareWalletServices(); return; } // Must require hardware wallet services to be here // Ensure we have initialised List<Optional<HardwareWalletService>> hardwareWalletServices = CoreServices.getOrCreateHardwareWalletServices(); // (Re)subscribe to hardware wallet events // This is required in case the user stops and starts the // hardware wallet service during a session HardwareWalletEvents.subscribe(this); // Start the services for (Optional<HardwareWalletService> hardwareWalletService : hardwareWalletServices) { if (hardwareWalletService.isPresent()) { log.info("Starting hardware wallet service: {}", hardwareWalletService.get().getContext().getClient().name()); hardwareWalletService.get().start(); } } } /** * <p>Performs a system time check against an internet time source over NTP * If the system time has drifted then blocks will be rejected and the * balance will be wrong</p> */ private void handleSystemTimeDrift() { // Check time is not more than 60 min off (60 x 60 x 1000) // in either direction final ListenableFuture<Integer> driftFuture = Dates.calculateDriftInMillis("pool.ntp.org"); Futures.addCallback( driftFuture, new FutureCallback<Integer>() { @Override public void onSuccess(@Nullable Integer result) { if (result != null && Math.abs(result) > 3_600_000) { log.warn("System time is adrift by: {} min(s)", result / 60_000); // Issue the info alert CoreEvents.fireEnvironmentEvent(EnvironmentSummary.newSystemTimeDrift()); } else { log.debug("System time drift is within limits"); } } @Override public void onFailure(Throwable t) { // Problem encountered - user won't be able to fix it log.warn("System drift check timed out: '{}'", t.getMessage()); } }); } /** * <p>Performs an Atom feed check against MultiBit.org over HTTPS * If a new article is present an alert is shown</p> */ private void handleAtomFeedCheck() { // Parse the MultiBit.org Atom feed asynchronously final ListenableFuture<AtomFeed> atomFeedFuture = AtomFeeds.parseMultiBitOrgFeed(); Futures.addCallback( atomFeedFuture, new FutureCallback<AtomFeed>() { @Override public void onSuccess(@Nullable AtomFeed result) { if (result != null && !result.getAtomEntries().isEmpty()) { // Sort the results in updated order Collections.sort(result.getAtomEntries(), new AscendingAtomEntryComparator()); // Check for Atom feed entries AtomEntry latestEntry = result.getAtomEntries().get(result.getAtomEntries().size() - 1); // Get the title text without HTML String title = HtmlUtils.stripHtml(latestEntry.getTitle()); // Issue the info alert URI atomEntryUri; try { atomEntryUri = URI.create(latestEntry.getLink().getHref()); } catch (IllegalArgumentException e) { // Silently fail (nothing the user can do about this so no point logging it) return; } CoreEvents.fireEnvironmentEvent(EnvironmentSummary.newAtomFeedCheck(title, atomEntryUri)); } else { log.debug("No Atom feed available."); } } @Override public void onFailure(Throwable t) { // Problem encountered - user won't be able to fix it log.warn("Atom feed check timed out: '{}'", t.getMessage()); } }); } private void hideAsExitCancel(String panelName) { // The exit dialog state is determined by the radio button selection if (ExitState.CONFIRM_EXIT.name().equals(panelName) || ExitState.SWITCH_WALLET.name().equals(panelName)) { mainView.sidebarRequestFocus(); } // The detail screens do not have an intuitive way to capture focus // we rely on CTRL+TAB to relocate the focus with keyboard } private void hideEditWalletWizard(String walletName) { mainView.sidebarWalletName(walletName); } /** * Welcome wizard has created a new wallet so hand over to the credentials wizard for access */ private void handoverToCredentialsWizard() { log.debug("Hand over to credentials wizard"); // Handover mainView.setShowExitingWelcomeWizard(false); mainView.setShowExitingCredentialsWizard(true); mainView.setCredentialsRequestType(deferredCredentialsRequestType); // Use a new thread to handle the new wizard so that the handover can complete // hiding the existing wizard before drawing the replacement handoverExecutorService.execute( new Runnable() { @Override public void run() { // Allow time for the other wizard to finish hiding (200ms is the minimum) Uninterruptibles.sleepUninterruptibly(200, TimeUnit.MILLISECONDS); // Must execute on the EDT SwingUtilities.invokeLater( new Runnable() { @Override public void run() { // Start building the wizard on the EDT to ensure darkened background remains final CredentialsWizard credentialsWizard = Wizards.newExitingCredentialsWizard(deferredCredentialsRequestType); log.debug("Forcing hide of existing light box"); Panels.hideLightBoxIfPresent(); log.debug("Showing exiting credentials wizard after handover"); Panels.showLightBox(credentialsWizard.getWizardScreenHolder()); } }); } }); } /** * Credentials wizard needs to perform a restore so hand over to the welcome wizard */ private void handoverToWelcomeWizardRestore() { log.debug("Hand over to welcome wizard (restore wallet)"); // Handover mainView.setShowExitingWelcomeWizard(true); mainView.setShowExitingCredentialsWizard(false); // Select the appropriate wallet mode final WalletMode walletMode; if (CredentialsRequestType.HARDWARE.equals(deferredCredentialsRequestType)) { Optional<HardwareWalletService> currentHardwareWalletService = CoreServices.getCurrentHardwareWalletService(); walletMode = WalletMode.of(currentHardwareWalletService); } else { walletMode = WalletMode.STANDARD; } // For soft wallets the restore goes to the select wallet screen, for Trezor hard wallets go directly to the restore final WelcomeWizardState initialState = (WalletMode.STANDARD == walletMode) ? WelcomeWizardState.WELCOME_SELECT_WALLET : WelcomeWizardState.RESTORE_WALLET_SELECT_BACKUP; // Start building the wizard on the EDT to prevent UI updates final WelcomeWizard welcomeWizard = Wizards.newExitingWelcomeWizard(initialState, walletMode); // Use a new thread to handle the new wizard so that the handover can complete handoverExecutorService.execute( new Runnable() { @Override public void run() { // Allow time for the other wizard to finish hiding (200ms is the minimum) Uninterruptibles.sleepUninterruptibly(200, TimeUnit.MILLISECONDS); // Must execute on the EDT SwingUtilities.invokeLater( new Runnable() { @Override public void run() { Panels.hideLightBoxIfPresent(); log.debug("Showing exiting welcome wizard after handover"); Panels.showLightBox(welcomeWizard.getWizardScreenHolder()); } }); } }); } /** * Credentials wizard needs to perform a create so hand over to the welcome wizard */ private void handoverToWelcomeWizardCreate() { log.debug("Hand over to welcome wizard (create wallet)"); // Handover mainView.setShowExitingWelcomeWizard(true); mainView.setShowExitingCredentialsWizard(false); // For soft wallets the create goes to the wallet preparation screen final WelcomeWizardState initialState = WelcomeWizardState.CREATE_WALLET_PREPARATION; // Start building the wizard on the EDT to prevent UI updates final WelcomeWizard welcomeWizard = Wizards.newExitingWelcomeWizard( initialState, WalletMode.STANDARD ); // Use a new thread to handle the new wizard so that the handover can complete handoverExecutorService.execute( new Runnable() { @Override public void run() { // Allow time for the other wizard to finish hiding (200ms is the minimum) Uninterruptibles.sleepUninterruptibly(200, TimeUnit.MILLISECONDS); // Must execute on the EDT SwingUtilities.invokeLater( new Runnable() { @Override public void run() { Panels.hideLightBoxIfPresent(); log.debug("Showing exiting welcome wizard after handover"); Panels.showLightBox(welcomeWizard.getWizardScreenHolder()); } }); } }); } /** * Credentials wizard needs to perform a create new Trezor wallet over to the welcome wizard */ private void handoverToWelcomeWizardCreateHardwareWallet() { log.debug("Hand over to welcome wizard (create Trezor wallet)"); // Handover mainView.setShowExitingWelcomeWizard(true); mainView.setShowExitingCredentialsWizard(false); // Get the wallet mode WalletMode walletMode = WalletMode.of(CoreServices.getCurrentHardwareWalletService()); // Start building the wizard on the EDT to prevent UI updates final WelcomeWizard welcomeWizard = Wizards.newExitingWelcomeWizard( WelcomeWizardState.HARDWARE_CREATE_WALLET_PREPARATION, walletMode ); // Use a new thread to handle the new wizard so that the handover can complete handoverExecutorService.execute( new Runnable() { @Override public void run() { // Allow time for the other wizard to finish hiding (200ms is the minimum) Uninterruptibles.sleepUninterruptibly(200, TimeUnit.MILLISECONDS); // Must execute on the EDT SwingUtilities.invokeLater( new Runnable() { @Override public void run() { Panels.hideLightBoxIfPresent(); log.debug("Showing exiting welcome wizard after handover"); Panels.showLightBox(welcomeWizard.getWizardScreenHolder()); } }); } }); } /** * Credentials wizard has hidden */ private void hideCredentialsWizard() { log.debug("Wallet unlocked. Starting services..."); // No wizards on further refreshes mainView.setShowExitingWelcomeWizard(false); mainView.setShowExitingCredentialsWizard(false); Optional<HardwareWalletEvent> lastHardwareWalletEvent = CoreServices.getApplicationEventService().getLatestHardwareWalletEvent(); // Refresh the main view mainView.refresh(false); if (lastHardwareWalletEvent.isPresent() && lastHardwareWalletEvent.get().getEventType() == HardwareWalletEventType.SHOW_DEVICE_READY) { // Make sure the 'DEVICE_READY' event is not lost HardwareWalletEvents.fireHardwareWalletEvent( lastHardwareWalletEvent.get().getEventType(), lastHardwareWalletEvent.get().getMessage().get(), lastHardwareWalletEvent.get().getSource() ); } // Don't hold up the UI thread with these background operations walletExecutorService.submit( new Runnable() { @Override public void run() { try { // Get a ticker going log.debug("Starting exchange..."); handleExchange(); // Check for external data (wants to be quick) log.debug("Check for external data..."); handleExternalDataAlerts(); // Check for system time drift (runs in the background) log.debug("Check for system time drift..."); handleSystemTimeDrift(); // Check for Atom feed from MultiBit.org log.debug("Check for MultiBit.org Atom feed update..."); handleAtomFeedCheck(); } catch (Exception e) { // TODO localise and put on UI via an alert log.error("Services did not start ok. Error was {}", e.getClass().getCanonicalName() + " " + e.getMessage(), e); } } }); // Start the backup manager log.debug("Starting backup manager..."); handleBackupManager(); // Get the current wallet summary Optional<WalletSummary> walletSummary = WalletManager.INSTANCE.getCurrentWalletSummary(); mainView.sidebarWalletName(walletSummary.get().getName()); // Start the wallet service log.debug("Starting wallet service..."); CoreServices.getOrCreateWalletService(walletSummary.get().getWalletId()); // Show the initial detail screen Screen screen = Screen.valueOf(Configurations.currentConfiguration.getAppearance().getCurrentScreen()); ViewEvents.fireShowDetailScreenEvent(screen); } /** * When a transaction is created, fire off a delayed check of the transaction confidence/ network status * * @param transactionCreationEvent The transaction creation event from the EventBus */ private void initiateDelayedTransactionStatusCheck(final TransactionCreationEvent transactionCreationEvent) { transactionCheckingExecutorService.submit( new Runnable() { @Override public void run() { log.debug("Performing delayed status check on transaction '" + transactionCreationEvent.getTransactionId() + "'"); // Wait for a while to let the Bitcoin network respond to the tx being sent Uninterruptibles.sleepUninterruptibly(NUMBER_OF_SECONDS_TO_WAIT_BEFORE_TRANSACTION_CHECKING, TimeUnit.SECONDS); // See if the transaction has a RAGStatus if red. // This could be the tx has not been transmitted ok or is only seen by zero or one peers. // In this case the user will not have access to the tx change and notify them with a warning alert WalletService currentWalletService = CoreServices.getCurrentWalletService().get(); if (currentWalletService != null) { TransactionData transactionData = currentWalletService.getTransactionDataByHash(transactionCreationEvent.getTransactionId()); if (transactionData != null) { PaymentStatus status = transactionData.getStatus(); if (status.getStatus().equals(RAGStatus.RED)) { // The transaction has not been sent correctly, or change is not spendable, throw a warning alert final AlertModel alertModel = Models.newAlertModel(Languages.safeText(MessageKey.SPENDABLE_BALANCE_IS_LOWER), RAGStatus.AMBER); SwingUtilities.invokeLater( new Runnable() { @Override public void run() { ControllerEvents.fireAddAlertEvent(alertModel); } }); } if (!status.getStatus().equals(RAGStatus.GREEN)) { // Ensure that there is a message that the spendable balance is lower - Fire a BitcoinSentEvent failure CoreEvents.fireBitcoinSentEvent( new BitcoinSentEvent( Optional.<Transaction>absent(), null, transactionData.getAmountCoin().orNull(), null, Optional.<Coin>absent(), false, CoreMessageKey.THE_ERROR_WAS, new String[]{Languages.safeText(MessageKey.SPENDABLE_BALANCE_IS_LOWER)} )); } } } } }); } public static boolean isFireTransactionAlerts() { return fireTransactionAlerts; } public static void setFireTransactionAlerts(boolean fireTransactionAlerts) { MainController.fireTransactionAlerts = fireTransactionAlerts; } }