#!/usr/bin/env python3

'''
Joinmarket GUI using PyQt for doing coinjoins.
Some widgets copied and modified from https://github.com/spesmilo/electrum


    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
'''

import sys, datetime, os, logging
import platform, json, threading, time
from optparse import OptionParser
from typing import Optional, Tuple

from PySide2 import QtCore

from PySide2.QtGui import *

from PySide2.QtWidgets import *

if platform.system() == 'Windows':
    MONOSPACE_FONT = 'Lucida Console'
elif platform.system() == 'Darwin':
    MONOSPACE_FONT = 'Monaco'
else:
    MONOSPACE_FONT = 'monospace'

# This is required to change the decimal separator
# to '.' regardless of the locale; TODO don't require
# this, but will require other edits for parsing amounts.
curL = QtCore.QLocale("en_US")
QtCore.QLocale.setDefault(curL)

app = QApplication(sys.argv)
if 'twisted.internet.reactor' in sys.modules:
    del sys.modules['twisted.internet.reactor']

import qt5reactor
qt5reactor.install()

#Version of this Qt script specifically
JM_GUI_VERSION = '34'

from jmbase import get_log, stop_reactor, set_custom_stop_reactor
from jmbase.support import EXIT_FAILURE, utxo_to_utxostr,\
    hextobin, JM_CORE_VERSION
import jmbitcoin as btc
from jmclient import load_program_config, get_network, update_persist_config,\
    open_test_wallet_maybe, get_wallet_path,\
    jm_single, validate_address, fidelity_bond_weighted_order_choose, Taker,\
    JMClientProtocolFactory, start_reactor, get_schedule, schedule_to_text,\
    get_blockchain_interface_instance, direct_send, WalletService,\
    RegtestBitcoinCoreInterface, tumbler_taker_finished_update,\
    get_tumble_log, restart_wait, tumbler_filter_orders_callback,\
    wallet_generate_recover_bip39, wallet_display, get_utxos_enabled_disabled,\
    NO_ROUNDING, get_max_cj_fee_values, get_default_max_absolute_fee, \
    get_default_max_relative_fee, RetryableStorageError, add_base_options, \
    BTCEngine, FidelityBondMixin, wallet_change_passphrase, \
    parse_payjoin_setup, send_payjoin, JMBIP78ReceiverManager, \
    detect_script_type, general_custom_change_warning, \
    nonwallet_custom_change_warning, sweep_custom_change_warning, EngineError,\
    TYPE_P2WPKH, check_and_start_tor, is_extended_public_key, \
    ScheduleGenerationErrorNoFunds, Storage
from jmclient.wallet import BaseWallet

from qtsupport import ScheduleWizard, TumbleRestartWizard, config_tips,\
    config_types, QtHandler, XStream, Buttons, OkButton, CancelButton,\
    PasswordDialog, MyTreeWidget, JMQtMessageBox, BLUE_FG,\
    donation_more_message, BitcoinAmountEdit, JMIntValidator,\
    ReceiveBIP78Dialog, QRCodePopup

from twisted.internet import task

log = get_log()

def update_config_for_gui():
    '''The default joinmarket config does not contain these GUI settings
    (they are generally set by command line flags or not needed).
    If they are set in the file, use them, else set the defaults.
    '''
    gui_config_names = ['gaplimit', 'history_file', 'check_high_fee',
                        'max_mix_depth', 'order_wait_time', 'checktx']
    gui_config_default_vals = ['6', 'jm-tx-history.txt', '2', '5', '30',
                               'true']
    if "GUI" not in jm_single().config.sections():
        jm_single().config.add_section("GUI")
    gui_items = jm_single().config.items("GUI")
    for gcn, gcv in zip(gui_config_names, gui_config_default_vals):
        if gcn not in [_[0] for _ in gui_items]:
            jm_single().config.set("GUI", gcn, gcv)



handler = QtHandler()
handler.setFormatter(logging.Formatter("%(levelname)s:%(message)s"))
log.addHandler(handler)


from jmqtui import Ui_OpenWalletDialog


class JMOpenWalletDialog(QDialog, Ui_OpenWalletDialog):
    DEFAULT_WALLET_FILE_TEXT = "wallet.jmdat"

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.setupUi(self)
        self.passphraseEdit.setFocus()

        self.chooseWalletButton.clicked.connect(self.chooseWalletFile)

    def chooseWalletFile(self, error_text: str = ""):
        (filename, _) = QFileDialog.getOpenFileName(
            self,
            "Choose Wallet File",
            self._get_wallets_path(),
            options=QFileDialog.DontUseNativeDialog,
        )
        if filename:
            self.walletFileEdit.setText(filename)
            self.passphraseEdit.setFocus()
            self.errorMessageLabel.setText(self.verify_lock(filename))

    @staticmethod
    def _get_wallets_path() -> str:
        """Return wallets path"""
        return os.path.join(jm_single().datadir, "wallets")

    @classmethod
    def verify_lock(cls, filename: Optional[str] = None) -> str:
        """Return an error text if wallet is locked, empty string otherwise"""
        if filename is None:
            filename = os.path.join(
                cls._get_wallets_path(), cls.DEFAULT_WALLET_FILE_TEXT
            )
        try:
            Storage.verify_lock(filename)
            return ""
        except Exception as e:
            return str(e)


class HelpLabel(QLabel):

    def __init__(self, text, help_text, wtitle):
        QLabel.__init__(self, text)
        self.help_text = help_text
        self.wtitle = wtitle
        self.font = QFont()
        self.setStyleSheet(BLUE_FG)

    def mouseReleaseEvent(self, x):
        QMessageBox.information(mainWindow, self.wtitle, self.help_text)

    def enterEvent(self, event):
        self.font.setUnderline(True)
        self.setFont(self.font)
        app.setOverrideCursor(QCursor(QtCore.Qt.PointingHandCursor))
        return QLabel.enterEvent(self, event)

    def leaveEvent(self, event):
        self.font.setUnderline(False)
        self.setFont(self.font)
        app.setOverrideCursor(QCursor(QtCore.Qt.ArrowCursor))
        return QLabel.leaveEvent(self, event)

class SettingsTab(QDialog):

    def __init__(self):
        super().__init__()
        self.initUI()

    def initUI(self):
        outerGrid = QGridLayout()
        sA = QScrollArea()
        sA.setWidgetResizable(True)
        frame = QFrame()
        grid = QGridLayout()
        self.settingsFields = []
        j = 0
        for i, section in enumerate(jm_single().config.sections()):
            pairs = jm_single().config.items(section)
            #an awkward design element from the core code: maker_timeout_sec
            #is set outside the config, if it doesn't exist in the config.
            #Add it here and it will be in the newly updated config file.
            if section == 'TIMEOUT' and 'maker_timeout_sec' not in [
                    _[0] for _ in pairs
            ]:
                jm_single().config.set(section, 'maker_timeout_sec', '60')
                pairs = jm_single().config.items(section)
            newSettingsFields = self.getSettingsFields(section,
                                                       [_[0] for _ in pairs])
            self.settingsFields.extend(newSettingsFields)
            sL = QLabel(section)
            sL.setStyleSheet("QLabel {color: blue;}")
            grid.addWidget(sL)
            j += 1
            for k, ns in enumerate(newSettingsFields):
                grid.addWidget(ns[0], j, 0)
                #try to find the tooltip for this label from config tips;
                #it might not be there
                if str(ns[0].text()) in config_tips:
                    ttS = config_tips[str(ns[0].text())]
                    ns[0].setToolTip(ttS)
                grid.addWidget(ns[1], j, 1)
                sfindex = len(self.settingsFields) - len(newSettingsFields) + k
                if isinstance(ns[1], QCheckBox):
                    ns[1].toggled.connect(lambda checked, s=section,
                                          q=sfindex: self.handleEdit(
                                    s, self.settingsFields[q], checked))
                else:
                    ns[1].editingFinished.connect(
                    lambda q=sfindex, s=section: self.handleEdit(s,
                                                      self.settingsFields[q]))
                j += 1
        outerGrid.addWidget(sA)
        sA.setWidget(frame)
        frame.setLayout(grid)
        frame.adjustSize()
        self.setLayout(outerGrid)
        self.show()

    def handleEdit(self, section, t, checked=None):
        if isinstance(t[1], QCheckBox):
            if str(t[0].text()) == 'Testnet':
                oname = 'network'
                oval = 'testnet' if checked else 'mainnet'
                add = '' if not checked else ' - Testnet'
                mainWindow.setWindowTitle(appWindowTitle + add)
            else:
                oname = str(t[0].text())
                oval = 'true' if checked else 'false'
            log.debug('setting section: ' + section + ' and name: ' + oname +
                      ' to: ' + oval)
            if not update_persist_config(section, oname, oval):
                log.warn("Unable to persist config change to file: " + str(section) + str(oname) + str(oval))

        else:  #currently there is only QLineEdit
            log.debug('setting section: ' + section + ' and name: ' + str(t[
                0].text()) + ' to: ' + str(t[1].text()))
            if not update_persist_config(section, str(t[0].text()), str(t[1].text())):
                # we don't include GUI as it's not required to be persisted:
                if not section == "GUI":
                    log.warn("Unable to persist config change to file: " + str(
                        section) + str(t[0].text()) + str(t[1].text()))
            if str(t[0].text()) == 'blockchain_source':
                jm_single().bc_interface = get_blockchain_interface_instance(
                    jm_single().config)
            if str(t[0].text()) == 'maker_timeout_sec':
                jm_single().maker_timeout_sec = int(t[1].text())
                log.debug("Set maker timeout sec to : " + str(jm_single().maker_timeout_sec))

    def getSettingsFields(self, section, names):
        results = []
        for name in names:
            val = jm_single().config.get(section, name)
            if name in config_types:
                t = config_types[name]
                if t == bool:
                    sf = QCheckBox()
                    if val == 'testnet' or val.lower() == 'true':
                        sf.setChecked(True)
                elif t == 'amount':
                    sf = BitcoinAmountEdit(val)
                elif not t:
                    continue
                else:
                    sf = QLineEdit(val)
                    if t == int:
                        if name in ["port", "rpc_port", "socks5_port",
                                    "daemon_port"]:
                            sf.setValidator(JMIntValidator(1, 65535))
                        elif name == "tx_fees":
                            # must account for both tx_fees settings type,
                            # and we set upper limit well above default absurd
                            # check just in case a high value is needed:
                            sf.setValidator(JMIntValidator(1, 1000000))
            else:
                sf = QLineEdit(val)
            label = 'Testnet' if name == 'network' else name
            results.append((QLabel(label), sf))
        return results

class SpendStateMgr(object):
    """A primitive class keep track of the mode
    in which the spend tab is being run
    """
    def __init__(self, updatecallback):
        self.reset_vars()
        self.updatecallback = updatecallback

    def updateType(self, t):
        self.typestate = t
        self.updatecallback()

    def updateRun(self, r):
        self.runstate = r
        self.updatecallback()

    def reset_vars(self):
        self.typestate = 'single'
        self.runstate = 'ready'
        self.schedule_name = None
        self.loaded_schedule = None

    def reset(self):
        self.reset_vars()
        self.updatecallback()

class SpendTab(QWidget):

    def __init__(self):
        super().__init__()
        self.initUI()
        self.taker = None
        self.filter_offers_response = None
        self.clientfactory = None
        self.tumbler_options = None
        #timer for waiting for confirmation on restart
        self.restartTimer = QtCore.QTimer()
        #timer for wait for next transaction
        self.nextTxTimer = None
        #tracks which mode the spend tab is run in
        self.spendstate = SpendStateMgr(self.toggleButtons)
        self.spendstate.reset() #trigger callback to 'ready' state
        # needed to be saved for parse_payjoin_setup()
        self.bip21_uri = None
        # avoid re-starting BIP78 daemon unnecessarily:
        self.bip78_daemon_started = False

    def switchToBIP78Payjoin(self, endpoint_url):
        self.numCPLabel.setVisible(False)
        self.numCPInput.setVisible(False)
        self.pjEndpointInput.setText(endpoint_url)
        self.pjEndpointLabel.setVisible(True)
        self.pjEndpointInput.setVisible(True)

        # while user is attempting a payjoin, address/change
        # cannot be edited; to back out, they hit Abort.
        self.addressInput.setEnabled(False)
        self.changeInput.setEnabled(False)
        self.changeInput.clear()
        self.abortButton.setEnabled(True)

    def switchToJoinmarket(self):
        self.pjEndpointLabel.setVisible(False)
        self.pjEndpointInput.setVisible(False)
        self.pjEndpointInput.setText('')
        self.numCPLabel.setVisible(True)
        self.numCPInput.setVisible(True)

    def clearFields(self, ignored):
        self.switchToJoinmarket()
        self.addressInput.setText('')
        self.amountInput.setText('')
        self.changeInput.setText('')
        self.addressInput.setEnabled(True)
        self.pjEndpointInput.setEnabled(True)
        self.mixdepthInput.setEnabled(True)
        self.amountInput.setEnabled(True)
        self.changeInput.setEnabled(True)
        self.startButton.setEnabled(True)
        self.abortButton.setEnabled(False)

    def checkAddress(self, addr):
        valid, errmsg = validate_address(str(addr))
        if not valid and len(addr) > 0:
            JMQtMessageBox(self,
                       "Bitcoin address not valid.\n" + errmsg,
                       mbtype='warn',
                       title="Error")

    def parseURIAndValidateAddress(self, addr):
        addr = addr.strip()
        if btc.is_bip21_uri(addr):
            try:
                parsed = btc.decode_bip21_uri(addr)
            except ValueError as e:
                JMQtMessageBox(self,
                    "Bitcoin URI not valid.\n" + str(e),
                    mbtype='warn',
                    title="Error")
                return
            self.bip21_uri = addr
            addr = parsed['address']
            if 'amount' in parsed:
                self.amountInput.setText(parsed['amount'])
            if 'pj' in parsed:
                self.switchToBIP78Payjoin(parsed['pj'])
            else:
                self.switchToJoinmarket()
        else:
            self.bip21_uri = None

        self.addressInput.setText(addr)
        self.checkAddress(addr)

    def checkAmount(self, amount_str):
        if not amount_str:
            return False
        try:
            amount_sat = btc.amount_to_sat(amount_str)
        except ValueError as e:
            JMQtMessageBox(self, e.args[0], title="Error", mbtype="warn")
            return False
        if amount_sat < jm_single().DUST_THRESHOLD:
            JMQtMessageBox(self,
                       "Amount " + btc.amount_to_str(amount_sat) +
                       " is below dust threshold " +
                       btc.amount_to_str(jm_single().DUST_THRESHOLD) + ".",
                       mbtype='warn',
                       title="Error")
            return False
        return True

    def generateTumbleSchedule(self):
        if not mainWindow.wallet_service:
            JMQtMessageBox(self, "Cannot start without a loaded wallet.",
                           mbtype="crit", title="Error")
            return
        #needs a set of tumbler options and destination addresses, so needs
        #a wizard
        wizard = ScheduleWizard()
        wizard_return = wizard.exec_()
        if wizard_return == QDialog.Rejected:
            return
        try:
            self.spendstate.loaded_schedule = wizard.get_schedule(
            mainWindow.wallet_service.get_balance_by_mixdepth(),
            mainWindow.wallet_service.mixdepth)
        except ScheduleGenerationErrorNoFunds:
            JMQtMessageBox(self,
                           "Failed to start tumbler; no funds available.",
                           title="Tumbler start failed.")
            return
        self.spendstate.schedule_name = wizard.get_name()
        self.updateSchedView()
        self.tumbler_options = wizard.opts
        self.tumbler_destaddrs = wizard.get_destaddrs()
        #tumbler may require more mixdepths; update the wallet
        required_mixdepths = max([tx[0] for tx in self.spendstate.loaded_schedule])
        if required_mixdepths > jm_single().config.getint("GUI", "max_mix_depth"):
            jm_single().config.set("GUI", "max_mix_depth", str(required_mixdepths))
            #recreate wallet and sync again; needed due to cache.
            JMQtMessageBox(self,
            "Max mixdepth has been reset to: " + str(required_mixdepths) + ".\n" +
            "Please choose 'Load' from the Wallet menu and resync before running.",
            title='Wallet resync required')
            return
        self.sch_startButton.setEnabled(True)

    def selectSchedule(self):
        current_path = os.path.dirname(os.path.realpath(__file__))
        firstarg = QFileDialog.getOpenFileName(self,
                                               'Choose Schedule File',
                                               directory=current_path,
                                               options=QFileDialog.DontUseNativeDialog)
        #TODO validate the schedule
        log.debug('Looking for schedule in: ' + str(firstarg))
        if not firstarg:
            return
        #extract raw text before processing
        with open(firstarg[0], 'rb') as f:
            rawsched = f.read()

        res, schedule = get_schedule(firstarg[0])
        if not res:
            JMQtMessageBox(self, "Not a valid JM schedule file", mbtype='crit',
                           title='Error')
        else:
            mainWindow.statusBar().showMessage("Schedule loaded OK.")
            self.spendstate.loaded_schedule = schedule
            self.spendstate.schedule_name = os.path.basename(str(firstarg[0]))
            self.updateSchedView()
            if self.spendstate.schedule_name == "TUMBLE.schedule":
                reply = JMQtMessageBox(self, "An incomplete tumble run detected. "
                                       "\nDo you want to restart?",
                                       title="Restart detected", mbtype='question')
                if reply != QMessageBox.Yes:
                    self.giveUp()
                    return
                self.tumbler_options = True

    def updateSchedView(self):
        self.sch_label2.setText(self.spendstate.schedule_name)
        self.sched_view.setText(schedule_to_text(self.spendstate.loaded_schedule).decode('utf-8'))

    def getDonateLayout(self):
        donateLayout = QHBoxLayout()
        self.donateCheckBox = QCheckBox()
        self.donateCheckBox.setChecked(False)
        #Temporarily disabled
        self.donateCheckBox.setEnabled(False)
        self.donateCheckBox.setMaximumWidth(30)
        self.donateLimitBox = QDoubleSpinBox()
        self.donateLimitBox.setMinimum(0.001)
        self.donateLimitBox.setMaximum(0.100)
        self.donateLimitBox.setSingleStep(0.001)
        self.donateLimitBox.setDecimals(3)
        self.donateLimitBox.setValue(0.010)
        self.donateLimitBox.setMaximumWidth(100)
        self.donateLimitBox.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
        donateLayout.addWidget(self.donateCheckBox)
        label1 = QLabel("Check to send change lower than: ")
        label1.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
        donateLayout.addWidget(label1)
        donateLayout.setAlignment(label1, QtCore.Qt.AlignLeft)
        donateLayout.addWidget(self.donateLimitBox)
        donateLayout.setAlignment(self.donateLimitBox, QtCore.Qt.AlignLeft)
        label2 = QLabel(" BTC as a donation.")
        donateLayout.addWidget(label2)
        label2.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
        donateLayout.setAlignment(label2, QtCore.Qt.AlignLeft)
        label3 = HelpLabel('More', donation_more_message,
                           'About the donation feature')
        label3.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
        donateLayout.setAlignment(label3, QtCore.Qt.AlignLeft)
        donateLayout.addWidget(label3)
        donateLayout.addStretch(1)
        return donateLayout

    def initUI(self):
        vbox = QVBoxLayout(self)
        top = QFrame()
        top.setFrameShape(QFrame.StyledPanel)
        topLayout = QGridLayout()
        top.setLayout(topLayout)
        sA = QScrollArea()
        sA.setWidgetResizable(True)
        topLayout.addWidget(sA)
        self.qtw = QTabWidget()
        sA.setWidget(self.qtw)
        self.single_join_tab = QWidget()
        self.schedule_tab = QWidget()
        self.qtw.addTab(self.single_join_tab, "Single Join")
        self.qtw.addTab(self.schedule_tab, "Multiple Join")

        #construct layout for scheduler
        sch_layout = QGridLayout()
        sch_layout.setSpacing(4)
        self.schedule_tab.setLayout(sch_layout)
        current_schedule_layout = QVBoxLayout()
        sch_label1=QLabel("Current schedule: ")
        sch_label1.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
        self.sch_label2 = QLabel("None")
        current_schedule_layout.addWidget(sch_label1)
        current_schedule_layout.addWidget(self.sch_label2)
        self.sched_view = QTextEdit()
        self.sched_view.setReadOnly(True)
        self.sched_view.setLineWrapMode(QTextEdit.NoWrap)
        current_schedule_layout.addWidget(self.sched_view)
        sch_layout.addLayout(current_schedule_layout, 0, 0, 1, 1)
        self.schedule_set_button = QPushButton('Choose schedule file')
        self.schedule_set_button.clicked.connect(self.selectSchedule)
        self.schedule_generate_button = QPushButton('Generate tumble schedule')
        self.schedule_generate_button.clicked.connect(self.generateTumbleSchedule)
        self.sch_startButton = QPushButton('Run schedule')
        self.sch_startButton.setEnabled(False) #not runnable until schedule chosen
        self.sch_startButton.clicked.connect(self.startMultiple)
        self.sch_abortButton = QPushButton('Abort')
        self.sch_abortButton.setEnabled(False)
        self.sch_abortButton.clicked.connect(self.abortTransactions)

        sch_buttons_box = QGroupBox("Actions")
        sch_buttons_layout = QVBoxLayout()
        sch_buttons_layout.addWidget(self.schedule_set_button)
        sch_buttons_layout.addWidget(self.schedule_generate_button)
        sch_buttons_layout.addWidget(self.sch_startButton)
        sch_buttons_layout.addWidget(self.sch_abortButton)
        sch_buttons_box.setLayout(sch_buttons_layout)
        sch_layout.addWidget(sch_buttons_box, 0, 1, 1, 1)

        #construct layout for single joins
        innerTopLayout = QGridLayout()
        innerTopLayout.setSpacing(4)
        self.single_join_tab.setLayout(innerTopLayout)

        #Temporarily disabled
        # donateLayout = self.getDonateLayout()
        # innerTopLayout.addLayout(donateLayout, 0, 0, 1, 2)

        recipientLabel = QLabel('Recipient address / URI')
        recipientLabel.setToolTip(
            'The address or bitcoin: URI you want to send the payment to')
        self.addressInput = QLineEdit()
        self.addressInput.editingFinished.connect(
            lambda: self.parseURIAndValidateAddress(self.addressInput.text()))
        innerTopLayout.addWidget(recipientLabel, 1, 0)
        innerTopLayout.addWidget(self.addressInput, 1, 1, 1, 2)

        self.numCPLabel = QLabel('Number of counterparties')
        self.numCPLabel.setToolTip(
            'How many other parties to send to; if you enter 4\n' +
            ', there will be 5 participants, including you.\n' +
            'Enter 0 to send direct without coinjoin.')
        self.numCPInput = QLineEdit('9')
        self.numCPInput.setValidator(QIntValidator(0, 20))
        innerTopLayout.addWidget(self.numCPLabel, 2, 0)
        innerTopLayout.addWidget(self.numCPInput, 2, 1, 1, 2)

        self.pjEndpointLabel = QLabel('PayJoin endpoint')
        self.pjEndpointLabel.setVisible(False)
        self.pjEndpointInput = QLineEdit()
        self.pjEndpointInput.setVisible(False)
        innerTopLayout.addWidget(self.pjEndpointLabel, 2, 0)
        innerTopLayout.addWidget(self.pjEndpointInput, 2, 1, 1, 2)

        mixdepthLabel = QLabel('Mixdepth')
        mixdepthLabel.setToolTip(
            'The mixdepth of the wallet to send the payment from')
        self.mixdepthInput = QLineEdit('0')
        self.mixdepthInput.setValidator(QIntValidator(
            0, jm_single().config.getint("GUI", "max_mix_depth") - 1))
        innerTopLayout.addWidget(mixdepthLabel, 3, 0)
        innerTopLayout.addWidget(self.mixdepthInput, 3, 1, 1, 2)

        amountLabel = QLabel('Amount')
        amountLabel.setToolTip(
            'The amount to send.\n' +
            'If you enter 0, a SWEEP transaction\nwill be performed,' +
            ' spending all the coins \nin the given mixdepth.')
        self.amountInput = BitcoinAmountEdit('')
        innerTopLayout.addWidget(amountLabel, 4, 0)
        innerTopLayout.addWidget(self.amountInput, 4, 1, 1, 2)

        changeLabel = QLabel('Custom change address')
        changeLabel.setToolTip(
            'Specify an address to receive change, rather ' +
            'than sending it to the internal wallet.')
        self.changeInput = QLineEdit()
        self.changeInput.editingFinished.connect(
            lambda: self.checkAddress(self.changeInput.text().strip()))
        self.changeInput.setPlaceholderText("(optional)")
        innerTopLayout.addWidget(changeLabel, 5, 0)
        innerTopLayout.addWidget(self.changeInput, 5, 1, 1, 2)

        self.startButton = QPushButton('Start')
        self.startButton.setToolTip(
            'If "checktx" is selected in the Settings, you will be \n'
            'prompted to decide whether to accept\n'
            'the transaction after connecting, and shown the\n'
            'fees to pay; you can cancel at that point, or by \n'
             'pressing "Abort".')
        self.startButton.clicked.connect(self.startSingle)
        self.abortButton = QPushButton('Abort')
        self.abortButton.setEnabled(False)
        buttons = QHBoxLayout()
        buttons.addStretch(1)
        buttons.addWidget(self.startButton)
        buttons.addWidget(self.abortButton)
        self.abortButton.clicked.connect(self.abortTransactions)
        innerTopLayout.addLayout(buttons, 6, 0, 1, 2)
        splitter1 = QSplitter(QtCore.Qt.Vertical)
        self.textedit = QTextEdit()
        self.textedit.verticalScrollBar().rangeChanged.connect(
            self.resizeScroll)
        XStream.stdout().messageWritten.connect(self.updateConsoleText)
        XStream.stderr().messageWritten.connect(self.updateConsoleText)
        splitter1.addWidget(top)
        splitter1.addWidget(self.textedit)
        splitter1.setSizes([400, 200])
        self.setLayout(vbox)
        vbox.addWidget(splitter1)
        self.show()

    def updateConsoleText(self, txt):
        #these alerts are a bit suboptimal;
        #colored is better
        #However, the transaction confirmation dialog
        #will at least show both in RED and BOLD, and they will be more prominent.
        #TODO in new daemon this is not accessible? Or?
        """
        if joinmarket_alert[0]:
            mainWindow.statusBar().showMessage("JOINMARKET ALERT: " + joinmarket_alert[
                0])
        """
        self.textedit.insertPlainText(txt)

    def resizeScroll(self, mini, maxi):
        self.textedit.verticalScrollBar().setValue(maxi)

    def restartWaitWrap(self):
        if restart_wait(self.waitingtxid):
            self.restartTimer.stop()
            self.waitingtxid = None
            mainWindow.statusBar().showMessage("Transaction in a block, now continuing.")
            self.startJoin()

    def startMultiple(self):
        if jm_single().bc_interface is None:
            log.info("Cannot start join, blockchain source not available.")
            return
        if not self.spendstate.runstate == 'ready':
            log.info("Cannot start join, already running.")
            return
        if not self.spendstate.loaded_schedule:
            log.info("Cannot start, no schedule loaded.")
            return
        self.spendstate.updateType('multiple')
        self.spendstate.updateRun('running')

        if self.tumbler_options:
            #Uses the flag 'True' value from selectSchedule to recognize a restart,
            #which needs new dynamic option values. The rationale for using input
            #is in case the user can increase success probability by changing them.
            if self.tumbler_options == True:
                wizard = TumbleRestartWizard()
                wizard_return = wizard.exec_()
                if wizard_return == QDialog.Rejected:
                    return
                self.tumbler_options = wizard.getOptions()
            #check for a partially-complete schedule; if so,
            #follow restart logic
            #1. filter out complete:
            self.spendstate.loaded_schedule = [
                s for s in self.spendstate.loaded_schedule if s[-1] != 1]
            #reload destination addresses
            self.tumbler_destaddrs = [x[3] for x in self.spendstate.loaded_schedule
                                     if x not in ["INTERNAL", "addrask"]]
            #2 Check for unconfirmed
            if isinstance(self.spendstate.loaded_schedule[0][-1], str) and len(
                self.spendstate.loaded_schedule[0][-1]) == 64:
                #ensure last transaction is confirmed before restart
                tumble_log.info("WAITING TO RESTART...")
                mainWindow.statusBar().showMessage("Waiting for confirmation to restart..")
                txid = self.spendstate.loaded_schedule[0][-1]
                #remove the already-done entry (this connects to the other TODO,
                #probably better *not* to truncate the done-already txs from file,
                #but simplest for now.
                self.spendstate.loaded_schedule = self.spendstate.loaded_schedule[1:]
                #defers startJoin() call until tx seen on network. Note that
                #since we already updated state to running, user cannot
                #start another transactions while waiting. Also, use :0 because
                #it always exists
                self.waitingtxid=txid
                self.restartTimer.timeout.connect(self.restartWaitWrap)
                self.restartTimer.start(5000)
                self.updateSchedView()
                return
            self.updateSchedView()
        self.startJoin()

    def checkDirectSend(self, dtx, destaddr, amount, fee, custom_change_addr):
        """Give user info to decide whether to accept a direct send;
        note the callback includes the full prettified transaction,
        but currently not printing it for space reasons.
        """
        mbinfo = ["Sending " + btc.amount_to_str(amount) + ",",
                  "to: " + destaddr + ","]

        if custom_change_addr:
            mbinfo.append("change to: " + custom_change_addr + ",")

        mbinfo += ["fee: " + btc.amount_to_str(fee) + ".",
                  "Accept?"]
        reply = JMQtMessageBox(self, '\n'.join([m + '<p>' for m in mbinfo]),
                               mbtype='question', title="Direct send")
        if reply == QMessageBox.Yes:
            self.direct_send_amount = amount
            return True
        else:
            return False

    def infoDirectSend(self, msg):
        self.clearFields(None)
        JMQtMessageBox(self, msg, title="Success")

    def errorDirectSend(self, msg):
        JMQtMessageBox(self, msg, mbtype="warn", title="Error")

    def startSingle(self):
        if not self.spendstate.runstate == 'ready':
            log.info("Cannot start join, already running.")
        if not self.validateSingleSend():
            return

        destaddr = str(self.addressInput.text().strip())
        try:
            amount = btc.amount_to_sat(self.amountInput.text())
        except ValueError as e:
            JMQtMessageBox(self, e.args[0], title="Error", mbtype="warn")
            return
        makercount = int(self.numCPInput.text())
        mixdepth = int(self.mixdepthInput.text())
        bip78url = self.pjEndpointInput.text()

        if makercount == 0 and not bip78url:
            custom_change = None
            if len(self.changeInput.text().strip()) > 0:
                custom_change = str(self.changeInput.text().strip())
            try:
                txid = direct_send(mainWindow.wallet_service, mixdepth,
                                   [(destaddr, amount)],
                                   accept_callback=self.checkDirectSend,
                                   info_callback=self.infoDirectSend,
                                   error_callback=self.errorDirectSend,
                                   custom_change_addr=custom_change)
            except Exception as e:
                JMQtMessageBox(self, e.args[0], title="Error", mbtype="warn")
                return
            if not txid:
                self.giveUp()
            else:
                # since direct_send() assumes a one-shot processing, it does
                # not add a callback for confirmation, so that event could
                # get lost; we do that here to ensure that the confirmation
                # event is noticed:
                def qt_directsend_callback(rtxd, rtxid, confs):
                    if rtxid == txid:
                        return True
                    return False
                mainWindow.wallet_service.active_txids.append(txid)
                mainWindow.wallet_service.register_callbacks([qt_directsend_callback],
                                                    txid, cb_type="confirmed")
                self.persistTxToHistory(destaddr, self.direct_send_amount, txid)
                self.cleanUp()
            return

        if bip78url:
            manager = parse_payjoin_setup(self.bip21_uri,
                    mainWindow.wallet_service, mixdepth, "joinmarket-qt")
            # start BIP78 AMP protocol if not yet up:
            if not self.bip78_daemon_started:
                daemon = jm_single().config.getint("DAEMON", "no_daemon")
                daemon = True if daemon == 1 else False
                start_reactor(jm_single().config.get("DAEMON", "daemon_host"),
                       jm_single().config.getint("DAEMON", "daemon_port"),
                       bip78=True, jm_coinjoin=False,
                       ish=False,
                       daemon=daemon,
                       gui=True)
                self.bip78_daemon_started = True
            # disable form fields until payment is done
            self.addressInput.setEnabled(False)
            self.pjEndpointInput.setEnabled(False)
            self.mixdepthInput.setEnabled(False)
            self.amountInput.setEnabled(False)
            self.changeInput.setEnabled(False)
            self.startButton.setEnabled(False)
            d = task.deferLater(reactor, 0.0, send_payjoin, manager,
                    accept_callback=self.checkDirectSend,
                    info_callback=self.infoDirectSend)
            d.addCallback(self.clearFields)
            return

        # for coinjoin sends no point to send below dust threshold, likely
        # there will be no makers for such amount.
        if amount != 0 and makercount > 0 and not self.checkAmount(amount):
            return

        if makercount < jm_single().config.getint(
            "POLICY", "minimum_makers"):
            JMQtMessageBox(self, "Number of counterparties (" + str(
                makercount) + ") below minimum_makers (" + str(
                jm_single().config.getint("POLICY", "minimum_makers")) +
                ") in configuration.",
                title="Error", mbtype="warn")
            return

        #note 'amount' is integer, so not interpreted as fraction
        #see notes in sample testnet schedule for format
        self.spendstate.loaded_schedule = [[mixdepth, amount, makercount,
                                            destaddr, 0, NO_ROUNDING, 0]]
        self.spendstate.updateType('single')
        self.spendstate.updateRun('running')
        self.startJoin()

    def getMaxCJFees(self, relfee, absfee):
        """ Used as a callback to decide relative and absolute
        maximum fees for coinjoins, in cases where the user has not
        set these values in the config (which is the default)."""
        if relfee is None:
            relfee = get_default_max_relative_fee()
        if absfee is None:
            absfee = get_default_max_absolute_fee()
        msg = ("Your maximum absolute fee in from one counterparty has been "
              "set to: " + str(absfee) + " satoshis.\n"
              "Your maximum relative fee from one counterparty has been set "
              "to: " + str(relfee) + ".\n"
              "To change these, please edit the config file and change the "
              "settings:\n"
              "max_cj_fee_abs = your-value-in-satoshis\n"
              "max_cj_fee_rel = your-value-as-decimal\n"
              "in the [POLICY] section.\n"
              "Note: If you don't do this, this dialog will interrupt the tumbler.")
        JMQtMessageBox(self, msg, mbtype="info", title="Setting fee limits.")
        return relfee, absfee

    def startJoin(self):
        if not mainWindow.wallet_service:
            JMQtMessageBox(self, "Cannot start without a loaded wallet.",
                           mbtype="crit", title="Error")
            return
        log.debug('starting coinjoin ..')
        #Decide whether to interrupt processing to sanity check the fees
        if self.tumbler_options:
            check_offers_callback = self.checkOffersTumbler
        elif jm_single().config.get("GUI", "checktx") == "true":
            check_offers_callback = self.checkOffers
        else:
            check_offers_callback = None

        destaddrs = self.tumbler_destaddrs if self.tumbler_options else []
        custom_change = None
        if len(self.changeInput.text().strip()) > 0:
            custom_change = str(self.changeInput.text().strip())
        maxcjfee = get_max_cj_fee_values(jm_single().config, None,
                                         user_callback=self.getMaxCJFees)
        log.info("Using maximum coinjoin fee limits per maker of {:.4%}, {} "
                     "".format(maxcjfee[0], btc.amount_to_str(maxcjfee[1])))
        self.taker = Taker(mainWindow.wallet_service,
                           self.spendstate.loaded_schedule,
                           maxcjfee,
                           order_chooser=fidelity_bond_weighted_order_choose,
                           callbacks=[check_offers_callback,
                                      self.takerInfo,
                                      self.takerFinished],
                           tdestaddrs=destaddrs,
                           custom_change_address=custom_change,
                           ignored_makers=ignored_makers)
        if not self.clientfactory:
            #First run means we need to start: create clientfactory
            #and start reactor connections
            self.clientfactory = JMClientProtocolFactory(self.taker)
            daemon = jm_single().config.getint("DAEMON", "no_daemon")
            daemon = True if daemon == 1 else False
            start_reactor(jm_single().config.get("DAEMON", "daemon_host"),
                   jm_single().config.getint("DAEMON", "daemon_port"),
                   self.clientfactory,
                   ish=False,
                   daemon=daemon,
                   gui=True)
        else:
            #This will re-use message channels in background (daemon), no restart
            self.clientfactory.getClient().client = self.taker
            self.clientfactory.getClient().clientStart()
        mainWindow.statusBar().showMessage("Connecting to message channels ...")

    def takerInfo(self, infotype, infomsg):
        if infotype == "INFO":
            #use of a dialog interrupts processing?, investigate.
            if len(infomsg) > 200:
                log.info("INFO: " + infomsg)
            else:
                mainWindow.statusBar().showMessage(infomsg)
        elif infotype == "ABORT":
            JMQtMessageBox(self, infomsg,
                           mbtype='warn')
            #Abort signal explicitly means this transaction will not continue.
            self.abortTransactions()
        else:
            raise NotImplementedError

    def checkOffersTumbler(self, offers_fees, cjamount):
        return tumbler_filter_orders_callback(offers_fees, cjamount,
                                              self.taker)

    def checkOffers(self, offers_fee, cjamount):
        """Parse offers and total fee from client protocol,
        allow the user to agree or decide.
        """
        if self.taker.aborted:
            log.debug("Not processing offers, user has aborted.")
            return False

        if not offers_fee:
            JMQtMessageBox(self,
                           "Not enough matching offers found.",
                           mbtype='warn',
                           title="Error")
            self.giveUp()
            return
        offers, total_cj_fee = offers_fee
        total_fee_pc = 1.0 * total_cj_fee / self.taker.cjamount

        mbinfo = []
        mbinfo.append("Sending amount: " + btc.amount_to_str(self.taker.cjamount))
        mbinfo.append("to address: " + self.taker.my_cj_addr)
        mbinfo.append(" ")
        mbinfo.append("Counterparties chosen:")
        mbinfo.append('Name,     Order id, Coinjoin fee (sat.)')
        for k, o in offers.items():
            if o['ordertype'] in ['sw0reloffer', 'swreloffer', 'reloffer']:
                display_fee = int(self.taker.cjamount *
                                  float(o['cjfee'])) - int(o['txfee'])
            elif o['ordertype'] in ['sw0absoffer', 'swabsoffer', 'absoffer']:
                display_fee = int(o['cjfee']) - int(o['txfee'])
            else:
                log.debug("Unsupported order type: " + str(o['ordertype']) +
                          ", aborting.")
                self.giveUp()
                return False
            mbinfo.append(k + ', ' + str(o['oid']) + ',         ' + str(
                display_fee))
        mbinfo.append('Total coinjoin fee = ' + btc.amount_to_str(total_cj_fee) +
                      ', or ' + str(float('%.3g' % (
                          100.0 * total_fee_pc))) + '%')
        title = 'Check Transaction'
        if total_fee_pc * 100 > jm_single().config.getint("GUI",
                                                          "check_high_fee"):
            title += ': WARNING: Fee is HIGH!!'
        reply = JMQtMessageBox(self,
                               '\n'.join([m + '<p>' for m in mbinfo]),
                               mbtype='question',
                               title=title)
        if reply == QMessageBox.Yes:
            #amount is now accepted;
            #The user is now committed to the transaction
            self.abortButton.setEnabled(False)
            return True
        else:
            self.filter_offers_response = "REJECT"
            self.giveUp()
            return False

    def startNextTransaction(self):
        self.clientfactory.getClient().clientStart()

    def takerFinished(self, res, fromtx=False, waittime=0.0, txdetails=None):
        """Callback (after pass-through signal) for jmclient.Taker
        on completion of each join transaction.
        """
        #non-GUI-specific state updates first:
        if self.tumbler_options:
            sfile = os.path.join(logsdir, 'TUMBLE.schedule')
            tumbler_taker_finished_update(self.taker, sfile, tumble_log,
                                      self.tumbler_options, res,
                                      fromtx,
                                      waittime,
                                      txdetails)

        self.spendstate.loaded_schedule = self.taker.schedule
        #Shows the schedule updates in the GUI; TODO make this more visual
        if self.spendstate.typestate == 'multiple':
            self.updateSchedView()

        #GUI-specific updates; QTimer.singleShot serves the role
        #of reactor.callLater
        if fromtx == "unconfirmed":
            mainWindow.statusBar().showMessage(
                "Transaction seen on network: " + self.taker.txid)
            if self.spendstate.typestate == 'single':
                self.clearFields(None)
                JMQtMessageBox(self, "Transaction broadcast OK. You can safely \n"
                               "shut down if you don't want to wait.",
                               title="Success")
            #TODO: theoretically possible to miss this if confirmed event
            #seen before unconfirmed.
            self.persistTxToHistory(self.taker.my_cj_addr, self.taker.cjamount,
                                                        self.taker.txid)

            #TODO prob best to completely fold multiple and tumble to reduce
            #complexity/duplication
            if self.spendstate.typestate == 'multiple' and not self.tumbler_options:
                self.taker.wallet_service.save_wallet()
            return
        if fromtx:
            if res:
                mainWindow.statusBar().showMessage("Transaction confirmed: " + self.taker.txid)
                #singleShot argument is in milliseconds
                if self.nextTxTimer:
                    self.nextTxTimer.stop()
                self.nextTxTimer = QtCore.QTimer()
                self.nextTxTimer.setSingleShot(True)
                self.nextTxTimer.timeout.connect(self.startNextTransaction)
                self.nextTxTimer.start(int(waittime*60*1000))
                #QtCore.QTimer.singleShot(int(self.taker_finished_waittime*60*1000),
                #                         self.startNextTransaction)
            else:
                if self.tumbler_options:
                    mainWindow.statusBar().showMessage("Transaction failed, trying again...")
                    QtCore.QTimer.singleShot(0, self.startNextTransaction)
                else:
                    #currently does not continue for non-tumble schedules
                    self.giveUp()
        else:
            if res:
                mainWindow.statusBar().showMessage("All transaction(s) completed successfully.")
                if len(self.taker.schedule) == 1:
                    msg = "Transaction has been confirmed.\n" + "Txid: " + \
                                           str(self.taker.txid)
                else:
                    msg = "All transactions have been confirmed."
                JMQtMessageBox(self, msg, title="Success")
                self.cleanUp()
            else:
                self.giveUp()

    def persistTxToHistory(self, addr, amt, txid):
        #persist the transaction to history
        with open(jm_single().config.get("GUI", "history_file"), 'ab') as f:
            f.write((','.join([addr, btc.amount_to_btc_str(amt), txid,
                              datetime.datetime.now(
                                  ).strftime("%Y/%m/%d %H:%M:%S")])).encode('utf-8'))
            f.write(b'\n')  #TODO: Windows
        #update the TxHistory tab
        txhist = mainWindow.centralWidget().widget(3)
        txhist.updateTxInfo()

    def toggleButtons(self):
        """Refreshes accessibility of buttons in the (single, multiple) join
        tabs based on the current state as defined by the SpendStateMgr instance.
        Thus, should always be called on any update to that instance.
        """
        #The first two buttons are for the single join tab; the remaining 4
        #are for the multijoin tab.
        btns = (self.startButton, self.abortButton,
                self.schedule_set_button, self.schedule_generate_button,
                self.sch_startButton, self.sch_abortButton)
        if self.spendstate.runstate == 'ready':
            btnsettings = (True, False, True, True, True, False)
        elif self.spendstate.runstate == 'running':
            if self.spendstate.typestate == 'single':
                #can only abort current run, nothing else
                btnsettings = (False, True, False, False, False, False)
            elif self.spendstate.typestate == 'multiple':
                btnsettings = (False, False, False, False, False, True)
            else:
                assert False
        else:
            assert False

        for b, s in zip(btns, btnsettings):
            b.setEnabled(s)

    def abortTransactions(self):
        if self.pjEndpointInput.isVisible():
            self.clearFields(None)
        else:
            self.taker.aborted = True
            self.giveUp()

    def giveUp(self):
        """Inform the user that the transaction failed, then reset state.
        """
        log.debug("Transaction aborted.")
        mainWindow.statusBar().showMessage("Transaction aborted.")
        if self.taker and len(self.taker.ignored_makers) > 0:
            JMQtMessageBox(self, "These Makers did not respond, and will be \n"
                           "ignored in future: \n" + str(
                            ','.join(self.taker.ignored_makers)),
                           title="Transaction aborted")
            ignored_makers.extend(self.taker.ignored_makers)
        self.cleanUp()

    def cleanUp(self):
        """Reset state to 'ready'
        """
        #Qt specific: because schedules can restart in same app instance,
        #we must clean up any existing delayed actions via singleShot.
        #Currently this should only happen via self.abortTransactions.
        if self.nextTxTimer:
            self.nextTxTimer.stop()
        self.spendstate.reset()
        self.tumbler_options = None
        self.tumbler_destaddrs = None

    def validateSingleSend(self):
        if not mainWindow.wallet_service:
            JMQtMessageBox(self,
                           "There is no wallet loaded.",
                           mbtype='warn',
                           title="Error")
            return False
        if jm_single().bc_interface is None:
            JMQtMessageBox(
                self,
                "Sending coins not possible without blockchain source.",
                mbtype='warn', title="Error")
            return False
        if len(self.addressInput.text()) == 0:
            JMQtMessageBox(
                self,
                "Recipient address or BIP21 bitcoin: payment URI must be provided.",
                mbtype='warn', title="Error")
            return False
        valid, errmsg = validate_address(
            str(self.addressInput.text().strip()))
        if not valid:
            JMQtMessageBox(self, errmsg, mbtype='warn', title="Error")
            return False
        if len(self.numCPInput.text()) == 0:
            JMQtMessageBox(
                self,
                "Number of counterparties must be provided. Enter '0' to do a direct send instead of a CoinJoin.",
                mbtype='warn', title="Error")
            return False
        if len(self.mixdepthInput.text()) == 0:
            JMQtMessageBox(
                self,
                "Mixdepth must be chosen.",
                mbtype='warn', title="Error")
            return False
        if len(self.amountInput.text()) == 0:
            JMQtMessageBox(
                self,
                "Amount must be provided.",
                mbtype='warn', title="Error")
            return False
        if len(self.changeInput.text().strip()) != 0:
            dest_addr = str(self.addressInput.text().strip())
            change_addr = str(self.changeInput.text().strip())
            makercount = int(self.numCPInput.text())
            try:
                amount = btc.amount_to_sat(self.amountInput.text())
            except ValueError as e:
                JMQtMessageBox(self, e.args[0], title="Error", mbtype="warn")
                return False
            valid, errmsg = validate_address(change_addr)
            if not valid:
                JMQtMessageBox(self,
                               "Custom change address is invalid: \"%s\"" % errmsg,
                               mbtype='warn', title="Error")
                return False

            if change_addr == dest_addr:
                msg = ''.join(["Custom change address cannot be the ",
                               "same as the recipient address."])
                JMQtMessageBox(self,
                               msg,
                               mbtype='warn', title="Error")
                return False
            if amount == 0:
                JMQtMessageBox(self, sweep_custom_change_warning,
                               mbtype='warn', title="Error")
                return False
            if makercount > 0:
                reply = JMQtMessageBox(self, general_custom_change_warning,
                                       mbtype='question', title="Warning")
                if reply == QMessageBox.No:
                    return False

            if makercount > 0:
                engine_recognized = True
                try:
                    change_addr_type = mainWindow.wallet_service.get_outtype(
                        change_addr)
                except EngineError:
                    engine_recognized = False
                wallet_type = mainWindow.wallet_service.get_txtype()
                if not engine_recognized or change_addr_type != wallet_type:
                    reply = JMQtMessageBox(self,
                                       nonwallet_custom_change_warning,
                                       mbtype='question',
                                       title="Warning")
                    if reply == QMessageBox.No:
                        return False

        return True

class TxHistoryTab(QWidget):

    def __init__(self):
        super().__init__()
        self.initUI()

    def initUI(self):
        self.tHTW = MyTreeWidget(self, self.create_menu, self.getHeaders())
        self.tHTW.setSelectionMode(QAbstractItemView.ExtendedSelection)
        self.tHTW.header().setSectionResizeMode(QHeaderView.Interactive)
        self.tHTW.header().setStretchLastSection(False)
        self.tHTW.on_update = self.updateTxInfo
        vbox = QVBoxLayout()
        self.setLayout(vbox)
        vbox.setContentsMargins(0,0,0,0)
        vbox.setSpacing(0)
        vbox.addWidget(self.tHTW)
        self.updateTxInfo()
        self.show()

    def getHeaders(self):
        '''Function included in case dynamic in future'''
        return ['Receiving address', 'Amount in BTC', 'Transaction id', 'Date']

    def updateTxInfo(self, txinfo=None):
        self.tHTW.clear()
        if not txinfo:
            txinfo = self.getTxInfoFromFile()
        for t in txinfo:
            t_item = QTreeWidgetItem(t)
            self.tHTW.addChild(t_item)
        for i in range(4):
            self.tHTW.resizeColumnToContents(i)

    def getTxInfoFromFile(self):
        hf = jm_single().config.get("GUI", "history_file")
        if not os.path.isfile(hf):
            if mainWindow:
                mainWindow.statusBar().showMessage("No transaction history found.")
            return []
        txhist = []
        with open(hf, 'rb') as f:
            txlines = f.readlines()
            for tl in txlines:
                txhist.append(tl.decode('utf-8').strip().split(','))
                if not len(txhist[-1]) == 4:
                    JMQtMessageBox(self,
                                   "Incorrectedly formatted file " + hf,
                                   mbtype='warn',
                                   title="Error")
                    mainWindow.statusBar().showMessage("No transaction history found.")
                    return []
        return txhist[::-1
                     ]  #appended to file in date order, window shows reverse

    def create_menu(self, position):
        item = self.tHTW.currentItem()
        if not item:
            return
        address_valid = False
        if item:
            address = str(item.text(0))
            address_valid = validate_address(address)

        menu = QMenu()
        if address_valid:
            menu.addAction("Copy address to clipboard",
                           lambda: app.clipboard().setText(address))
        menu.addAction("Copy transaction id to clipboard",
                       lambda: app.clipboard().setText(str(item.text(2))))
        menu.addAction("Copy full transaction info to clipboard",
                       lambda: app.clipboard().setText(
                           ','.join([str(item.text(_)) for _ in range(4)])))
        menu.exec_(self.tHTW.viewport().mapToGlobal(position))

class CoinsTab(QWidget):

    def __init__(self):
        super().__init__()
        self.initUI()

    def initUI(self):
        self.cTW = MyTreeWidget(self, self.create_menu, self.getHeaders())
        self.cTW.setSelectionMode(QAbstractItemView.ExtendedSelection)
        self.cTW.header().setSectionResizeMode(QHeaderView.Interactive)
        self.cTW.header().setStretchLastSection(False)
        self.cTW.on_update = self.updateUtxos

        vbox = QVBoxLayout()
        self.setLayout(vbox)
        vbox.setContentsMargins(0,0,0,0)
        vbox.setSpacing(0)
        vbox.addWidget(self.cTW)
        self.updateUtxos()
        self.show()

    def getHeaders(self):
        '''Function included in case dynamic in future'''
        return ['Txid:n', 'Amount in BTC', 'Address', 'Label']

    def updateUtxos(self):
        """ Note that this refresh of the display only accesses in-process
        utxo database (no sync e.g.) so can be immediate.
        """
        self.cTW.clear()
        def show_blank():
            m_item = QTreeWidgetItem(["No coins", "", "", ""])
            self.cTW.addChild(m_item)
            self.cTW.show()

        if not mainWindow.wallet_service:
            show_blank()
            return
        utxos_enabled = {}
        utxos_disabled = {}
        for i in range(jm_single().config.getint("GUI", "max_mix_depth")):
            utxos_e, utxos_d = get_utxos_enabled_disabled(mainWindow.wallet_service, i)
            if utxos_e != {}:
                utxos_enabled[i] = utxos_e
            if utxos_d != {}:
                utxos_disabled[i] = utxos_d
        if utxos_enabled == {} and utxos_disabled == {}:
            show_blank()
            return

        for i in range(jm_single().config.getint("GUI", "max_mix_depth")):
            uem = utxos_enabled.get(i)
            udm = utxos_disabled.get(i)
            m_item = QTreeWidgetItem(["Mixdepth " + str(i), '', '', ''])
            self.cTW.addChild(m_item)
            for heading in ["NOT FROZEN", "FROZEN"]:
                um = uem if heading == "NOT FROZEN" else udm
                seq_item = QTreeWidgetItem([heading, '', '', ''])
                m_item.addChild(seq_item)
                seq_item.setExpanded(True)
                if um is None:
                    item = QTreeWidgetItem(['None', '', '', ''])
                    seq_item.addChild(item)
                else:
                    for k, v in um.items():
                        # txid:index, btc, address
                        success, t = utxo_to_utxostr(k)
                        # keys must be utxo format else a coding error:
                        assert success
                        s = "{0:.08f}".format(v['value']/1e8)
                        a = mainWindow.wallet_service.script_to_addr(v["script"])
                        item = QTreeWidgetItem([t, s, a, v["label"]])
                        item.setFont(0, QFont(MONOSPACE_FONT))
                        #if rows[i][forchange][j][3] != 'new':
                        #    item.setForeground(3, QBrush(QColor('red')))
                        seq_item.addChild(item)
                    m_item.setExpanded(True)

    def toggle_utxo_disable(self, txids, idxs):
        for i in range(0, len(txids)):
            txid = txids[i]
            txid_bytes = hextobin(txid)
            mainWindow.wallet_service.toggle_disable_utxo(txid_bytes, idxs[i])
        self.updateUtxos()

    def create_menu(self, position):
        # all selected items
        selected_items = self.cTW.selectedItems()
        txids = []
        idxs = []
        if len(selected_items) == 0:
            return
        try:
            for item in selected_items:
                txid, idx = item.text(0).split(":")
                assert len(txid) == 64
                idx = int(idx)
                assert idx >= 0
                txids.append(txid)
                idxs.append(idx)
        except Exception as e:
            log.error("Error retrieving txids in Coins tab: " + repr(e))
            return
        # current item
        item = self.cTW.currentItem()
        txid, idx = item.text(0).split(":")

        menu = QMenu()
        menu.addAction("Freeze/un-freeze utxo(s) (toggle)",
                           lambda: self.toggle_utxo_disable(txids, idxs))
        menu.addAction("Copy transaction id to clipboard",
                       lambda: app.clipboard().setText(txid))
        menu.exec_(self.cTW.viewport().mapToGlobal(position))


class JMWalletTab(QWidget):

    def __init__(self):
        super().__init__()
        self.wallet_name = 'NONE'
        self.initUI()

    def initUI(self):
        self.label1 = QLabel(
            'No wallet loaded. Use "Wallet > Load" to load existing wallet ' +
            'or "Wallet > Generate" to create a new wallet.',
            self)
        self.label1.setAlignment(QtCore.Qt.AlignTop | QtCore.Qt.AlignLeft)
        v = MyTreeWidget(self, self.create_menu, self.getHeaders())
        v.header().resizeSection(0, 400)    # size of "Address" column
        v.header().resizeSection(1, 130)    # size of "Index" column
        v.setSelectionMode(QAbstractItemView.ExtendedSelection)
        v.on_update = self.updateWalletInfo
        v.hide()
        self.walletTree = v
        vbox = QVBoxLayout()
        self.setLayout(vbox)
        vbox.setContentsMargins(0,0,0,0)
        vbox.setSpacing(0)
        vbox.addWidget(self.label1)
        vbox.addWidget(v)
        buttons = QWidget()
        vbox.addWidget(buttons)
        self.updateWalletInfo()
        self.show()

    def getHeaders(self):
        '''Function included in case dynamic in future'''
        return ['Address', 'Index', 'Balance', 'Status', 'Label']

    def create_menu(self, position):
        item = self.walletTree.currentItem()
        address_valid = False
        xpub_exists = False
        if item:
            txt = str(item.text(0))
            if validate_address(txt)[0]:
                address_valid = True

            parsed = txt.split()
            if len(parsed) > 1:
                if is_extended_public_key(parsed[-1]):
                    xpub = parsed[-1]
                    xpub_exists = True

        menu = QMenu()
        if address_valid:
            menu.addAction("Copy address to clipboard",
                           lambda: app.clipboard().setText(txt),
                           shortcut=QKeySequence(QKeySequence.Copy))
            if item.text(4):
                menu.addAction("Copy label to clipboard",
                            lambda: app.clipboard().setText(item.text(4)))
            # Show QR code option only for new addresses to avoid address reuse
            if item.text(3) == "new":
                menu.addAction("Show QR code",
                               lambda: self.openAddressQRCodePopup(txt))
        if xpub_exists:
            menu.addAction("Copy extended public key to clipboard",
                           lambda: app.clipboard().setText(xpub),
                           shortcut=QKeySequence(QKeySequence.Copy))
            menu.addAction("Show QR code",
                               lambda: self.openQRCodePopup(xpub, xpub))
        menu.addAction("Refresh wallet",
                       lambda: mainWindow.updateWalletInfo(None, "all"),
                       shortcut=QKeySequence(QKeySequence.Refresh))

        #TODO add more items to context menu
        menu.exec_(self.walletTree.viewport().mapToGlobal(position))

    def openQRCodePopup(self, title, data):
        popup = QRCodePopup(self, title, data)
        popup.show()

    def openAddressQRCodePopup(self, address):
        bip21_uri = btc.encode_bip21_uri(address, {})
        # From BIP173 (bech32) spec:
        #   For presentation, lowercase is usually preferable, but inside
        #   QR codes uppercase SHOULD be used, as those permit the use of
        #   alphanumeric mode, which is 45% more compact than the normal
        #   byte mode.
        # So we keep case for QR popup title, but convert BIP21 URI to be
        # encoded in QR code to uppercase, if possible.
        if detect_script_type(mainWindow.wallet_service.addr_to_script(address)) == TYPE_P2WPKH:
            bip21_uri = bip21_uri.upper()
        self.openQRCodePopup(address, bip21_uri)

    def updateWalletInfo(self, walletinfo=None):
        max_mixdepth_count = jm_single().config.getint("GUI", "max_mix_depth")

        previous_expand_states = []
        # before deleting, note whether items were expanded
        for i in range(self.walletTree.topLevelItemCount()):
            tli = self.walletTree.invisibleRootItem().child(i)
            # expandedness is a list beginning with the top level expand state,
            # followed by the expand state of its children
            expandedness = [tli.isExpanded()]
            for j in range(tli.childCount()):
                expandedness.append(tli.child(j).isExpanded())
            previous_expand_states.append(expandedness)
        self.walletTree.clear()

        # Skip the remaining of this method if wallet info doesn't exist
        if not walletinfo:
            return

        rows, mbalances, xpubs, total_bal = walletinfo
        if jm_single().config.get("BLOCKCHAIN", "blockchain_source") == "regtest":
            self.wallet_name = mainWindow.testwalletname
        else:
            self.wallet_name = os.path.basename(
                mainWindow.wallet_service.get_storage_location())
        if total_bal is None:
            if jm_single().bc_interface is not None:
                total_bal = " (syncing..)"
            else:
                total_bal = " (unknown, no blockchain source available)"
        self.label1.setText("CURRENT WALLET: " + self.wallet_name +
                            ', total balance: ' + total_bal)
        self.walletTree.show()

        if jm_single().bc_interface is None and self.wallet_name != 'NONE':
            return

        for mixdepth in range(max_mixdepth_count):
            mdbalance = mbalances[mixdepth]
            account_xpub = xpubs[mixdepth][-1]

            m_item = QTreeWidgetItem(["Mixdepth " + str(mixdepth) + " , balance: " +
                                      mdbalance + ", " + account_xpub, '', '', '', ''])
            self.walletTree.addChild(m_item)

            # if expansion states existed, reinstate them:
            if len(previous_expand_states) == max_mixdepth_count:
                m_item.setExpanded(previous_expand_states[mixdepth][0])
            # we expand at the mixdepth level by default
            else:
                m_item.setExpanded(True)

            for address_type in [
                BaseWallet.ADDRESS_TYPE_EXTERNAL,
                BaseWallet.ADDRESS_TYPE_INTERNAL,
                FidelityBondMixin.BIP32_TIMELOCK_ID]:
                if address_type == FidelityBondMixin.BIP32_TIMELOCK_ID \
                    and (mixdepth != FidelityBondMixin.FIDELITY_BOND_MIXDEPTH
                         or not isinstance(mainWindow.wallet_service.wallet, FidelityBondMixin)):
                    continue

                if address_type == BaseWallet.ADDRESS_TYPE_EXTERNAL:
                    heading = "EXTERNAL " + xpubs[mixdepth][address_type]
                elif address_type == BaseWallet.ADDRESS_TYPE_INTERNAL:
                    heading = "INTERNAL"
                elif address_type == FidelityBondMixin.BIP32_TIMELOCK_ID:
                    heading = "TIMELOCK"
                else:
                    heading = ""

                seq_item = QTreeWidgetItem([heading, '', '', '', ''])
                m_item.addChild(seq_item)

                # by default, the external addresses of mixdepth 0 is expanded
                should_expand = mixdepth == 0 and address_type == BaseWallet.ADDRESS_TYPE_EXTERNAL
                for address_index in range(len(rows[mixdepth][address_type])):
                    item = QTreeWidgetItem(rows[mixdepth][address_type][address_index])
                    item.setFont(0, QFont(MONOSPACE_FONT))
                    if rows[mixdepth][address_type][address_index][3] != 'new':
                        item.setForeground(3, QBrush(QColor('red')))
                    # by default, if the balance is non zero, it is also expanded
                    if float(rows[mixdepth][address_type][address_index][2]) > 0:
                        should_expand = True
                    seq_item.addChild(item)
                # Remember user choice, if expansion states existed, reinstate them:
                if len(previous_expand_states) == max_mixdepth_count:
                    should_expand = previous_expand_states[mixdepth][address_type+1]
                seq_item.setExpanded(should_expand)


class JMMainWindow(QMainWindow):

    computing_privkeys_signal = QtCore.Signal()
    show_privkeys_signal = QtCore.Signal()

    def __init__(self, reactor):
        super().__init__()
        # the wallet service that encapsulates
        # the wallet we will interact with
        self.wallet_service = None

        # keep track of whether wallet sync message
        # was already shown
        self.syncmsg = ""

        # loop used for initial sync:
        self.walletRefresh = None
        self.update_registered = False

        # BIP 78 Receiver manager object, only
        # created when user starts a payjoin event:
        self.backend_receiver = None
        # Keep track of whether the BIP78 daemon has
        # been started, to avoid unnecessary duplication:
        self.bip78daemon = False

        self.reactor = reactor
        self.initUI()

        # a flag to indicate that shutdown should not
        # depend on user input
        self.unconditional_shutdown = False

    def closeEvent(self, event):
        if self.unconditional_shutdown:
            JMQtMessageBox(self,
                           "RPC connection is lost; shutting down.",
                           mbtype='crit',
                           title="Error")
            reply = QMessageBox.Yes
        else:
            quit_msg = "Are you sure you want to quit?"
            reply = JMQtMessageBox(self, quit_msg, mbtype='question')
        if reply == QMessageBox.Yes:
            event.accept()
            if self.reactor.threadpool is not None:
                self.reactor.threadpool.stop()
            stop_reactor()
        else:
            event.ignore()

    def initUI(self):
        self.statusBar().showMessage("Ready")
        self.setGeometry(300, 300, 250, 150)
        openWalletAction = QAction('&Open...', self)
        openWalletAction.setStatusTip('Open JoinMarket wallet file')
        openWalletAction.setShortcut(QKeySequence.Open)
        openWalletAction.triggered.connect(self.openWallet)
        generateAction = QAction('&Generate...', self)
        generateAction.setStatusTip('Generate new wallet')
        generateAction.triggered.connect(self.generateWallet)
        recoverAction = QAction('&Recover...', self)
        recoverAction.setStatusTip('Recover wallet from seed phrase')
        recoverAction.triggered.connect(self.recoverWallet)
        showSeedAction = QAction('&Show seed', self)
        showSeedAction.setStatusTip('Show wallet seed phrase')
        showSeedAction.triggered.connect(self.showSeedDialog)
        exportPrivAction = QAction('&Export keys', self)
        exportPrivAction.setStatusTip('Export all private keys to a  file')
        exportPrivAction.triggered.connect(self.exportPrivkeysJson)
        changePassAction = QAction('&Change passphrase...', self)
        changePassAction.setStatusTip('Change wallet encryption passphrase')
        changePassAction.triggered.connect(self.changePassphrase)
        receivePayjoinAction = QAction('Receive &payjoin...', self)
        receivePayjoinAction.setStatusTip('Receive BIP78 style payment')
        receivePayjoinAction.triggered.connect(self.receiver_bip78_init)
        quitAction = QAction(QIcon('exit.png'), '&Quit', self)
        quitAction.setShortcut(QKeySequence.Quit)
        quitAction.setStatusTip('Quit application')
        quitAction.triggered.connect(qApp.quit)

        aboutAction = QAction('About Joinmarket', self)
        aboutAction.triggered.connect(self.showAboutDialog)

        menubar = self.menuBar()
        walletMenu = menubar.addMenu('&Wallet')
        walletMenu.addAction(openWalletAction)
        walletMenu.addAction(generateAction)
        walletMenu.addAction(recoverAction)
        walletMenu.addAction(showSeedAction)
        walletMenu.addAction(exportPrivAction)
        walletMenu.addAction(changePassAction)
        walletMenu.addAction(receivePayjoinAction)
        walletMenu.addAction(quitAction)
        aboutMenu = menubar.addMenu('&About')
        aboutMenu.addAction(aboutAction)

        self.show()


    def receiver_bip78_init(self):
        """ Initializes BIP78 workflow with modal dialog.
        """
        if not self.wallet_service:
            JMQtMessageBox(self,
                           "No wallet loaded.",
                           mbtype='crit',
                           title="Error")
            return
        self.receiver_bip78_dialog = ReceiveBIP78Dialog(
            self.startReceiver, self.stopReceiver)

    def startReceiver(self):
        """ Initializes BIP78 Receiving object and
        starts the setup of onion service to serve
        request.
        Returns False in case we are not able to start
        due to bad parameters, otherwise True (not affected
        by success of whole request generation process).
        """
        assert self.receiver_bip78_dialog
        amount = btc.amount_to_sat(
            self.receiver_bip78_dialog.get_amount_text())
        mixdepth = self.receiver_bip78_dialog.get_mixdepth()
        if mixdepth > self.wallet_service.mixdepth:
            JMQtMessageBox(self,
                           "Wallet does not have mixdepth " + str(mixdepth),
                           mbtype='crit', title="Error")
            return False
        if self.wallet_service.get_balance_by_mixdepth(minconfs=1)[mixdepth] == 0:
            JMQtMessageBox(self, "Mixdepth " + str(mixdepth) + \
                           " has no confirmed coins.",
                           mbtype='crit', title="Error")
            return False
        self.backend_receiver = JMBIP78ReceiverManager(self.wallet_service,
            mixdepth, amount, 80, self.receiver_bip78_dialog.info_update,
            uri_created_callback=self.receiver_bip78_dialog.update_uri,
            shutdown_callback=self.receiver_bip78_dialog.process_complete,
            mode="gui")
        if not self.bip78daemon:
            #First run means we need to start: create daemon;
            # the client and its connection are created in the .initiate()
            # call.
            daemon = jm_single().config.getint("DAEMON", "no_daemon")
            if daemon:
                # this call not needed if daemon is external.
                start_reactor(jm_single().config.get("DAEMON", "daemon_host"),
                       jm_single().config.getint("DAEMON", "daemon_port"),
                       jm_coinjoin=False, bip78=True, daemon=True,
                       gui=True, rs=False)
                self.bip78daemon = True
        self.backend_receiver.initiate()
        return True

    def stopReceiver(self):
        if self.backend_receiver is None:
            return
        self.backend_receiver.shutdown()

    def showAboutDialog(self):
        msgbox = QDialog(self)
        lyt = QVBoxLayout(msgbox)
        msgbox.setWindowTitle(appWindowTitle)
        about_text_label = QLabel()
        about_text_label.setText(
            "<a href=" + "'https://github.com/joinmarket-org/joinmarket-clientserver/'>"
            + "Read more about Joinmarket</a><p>" + "<p>".join(
                ["Joinmarket core software version: " + JM_CORE_VERSION + "<br/>JoinmarketQt version: "
                    + JM_GUI_VERSION + "<br/>Messaging protocol version:" + " %s" % (
                     str(jm_single().JM_VERSION)
                 )]))
        about_text_label.setWordWrap(True)
        for l in [about_text_label]:
            l.setTextFormat(QtCore.Qt.RichText)
            l.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction)
            l.setOpenExternalLinks(True)
        lyt.addWidget(about_text_label)
        btnbox = QDialogButtonBox(msgbox)
        btnbox.setStandardButtons(QDialogButtonBox.Ok)
        btnbox.accepted.connect(msgbox.accept)
        lyt.addWidget(btnbox)
        msgbox.exec_()

    def exportPrivkeysJson(self):
        if not self.wallet_service:
            JMQtMessageBox(self,
                           "No wallet loaded.",
                           mbtype='crit',
                           title="Error")
            return
        #TODO add password protection; too critical
        d = QDialog(self)
        d.setWindowTitle('Private keys')
        d.setMinimumSize(850, 300)
        vbox = QVBoxLayout(d)

        msg = "%s\n%s\n%s" % (
            "WARNING: ALL your private keys are secret.",
            "Exposing a single private key can compromise your entire wallet!",
            "In particular, DO NOT use 'redeem private key' services proposed by third parties."
        )
        vbox.addWidget(QLabel(msg))
        e = QTextEdit()
        e.setReadOnly(True)
        vbox.addWidget(e)
        b = OkButton(d, 'Export')
        b.setEnabled(False)
        vbox.addLayout(Buttons(CancelButton(d), b))
        private_keys = {}
        #prepare list of addresses with non-zero balance
        #TODO: consider adding a 'export insanely huge amount'
        #option for anyone with gaplimit troubles, although
        #that is a complete mess for a user, mostly changing
        #the gaplimit in the Settings tab should address it.
        rows = get_wallet_printout(self.wallet_service)
        addresses = []
        for forchange in rows[0]:
            for mixdepth in forchange:
                for addr_info in mixdepth:
                    if float(addr_info[2]) > 0:
                        addresses.append(addr_info[0])
        done = False

        def privkeys_thread():
            # To explain this (given setting was already done in
            # load_program_config), see:
            # https://github.com/Simplexum/python-bitcointx/blob/9f1fa67a5445f8c187ef31015a4008bc5a048eea/bitcointx/__init__.py#L242-L243
            # note, we ignore the return value as we only want to apply
            # the chainparams setting logic:
            get_blockchain_interface_instance(jm_single().config)
            for addr in addresses:
                time.sleep(0.1)
                if done:
                    break
                priv = self.wallet_service.get_key_from_addr(addr)
                private_keys[addr] = BTCEngine.privkey_to_wif(priv)
                self.computing_privkeys_signal.emit()
            self.show_privkeys_signal.emit()

        def show_privkeys():
            s = "\n".join(map(lambda x: x[0] + "\t" + x[1], private_keys.items(
            )))
            e.setText(s)
            b.setEnabled(True)

        self.computing_privkeys_signal.connect(lambda: e.setText(
            "Please wait... %d/%d" % (len(private_keys), len(addresses))))
        self.show_privkeys_signal.connect(show_privkeys)

        threading.Thread(target=privkeys_thread).start()
        if not d.exec_():
            done = True
            return
        privkeys_fn_base = 'joinmarket-private-keys'
        i = 0
        privkeys_fn = privkeys_fn_base
        # Updated to use json format, simply because L1354 writer
        # has some extremely weird behaviour cross Py2/Py3
        while os.path.isfile(os.path.join(jm_single().datadir,
                                          privkeys_fn + '.json')):
            i += 1
            privkeys_fn = privkeys_fn_base + str(i)
        try:
            with open(os.path.join(jm_single().datadir,
                                   privkeys_fn + '.json'), "wb") as f:
                for addr, pk in private_keys.items():
                    # sanity check
                    rawpriv, _ = BTCEngine.wif_to_privkey(pk)
                    if not addr == self.wallet_service._ENGINE.privkey_to_address(rawpriv):
                        JMQtMessageBox(None, "Failed to create privkey export -" +\
                                       " critical error in key parsing.",
                                       mbtype='crit')
                        return
                f.write(json.dumps(private_keys, indent=4).encode('utf-8'))
        except (IOError, os.error) as reason:
            export_error_label = "JoinmarketQt was unable to produce a private key-export."
            JMQtMessageBox(None,
                           export_error_label + "\n" + str(reason),
                           mbtype='crit',
                           title="Unable to create json file")

        except Exception as er:
            JMQtMessageBox(self, str(er), mbtype='crit', title="Error")
            return

        JMQtMessageBox(self,
                       "Private keys exported to: " + os.path.join(jm_single().datadir,
                        privkeys_fn) + '.json', title="Success")

    def seedEntry(self) -> Tuple[Optional[str], Optional[str]]:
        d = QDialog(self)
        d.setModal(1)
        d.setWindowTitle('Recover from mnemonic phrase')
        layout = QGridLayout(d)
        message_e = QTextEdit()
        layout.addWidget(QLabel('Enter 12 words'), 0, 0)
        layout.addWidget(message_e, 1, 0)

        pp_hbox = QHBoxLayout()
        pp_field = QLineEdit()
        pp_field.setEnabled(False)
        use_pp = QCheckBox('Input Mnemonic Extension', self)
        use_pp.setCheckState(QtCore.Qt.CheckState(False))
        use_pp.stateChanged.connect(lambda state: pp_field.setEnabled(state
            == QtCore.Qt.Checked))
        pp_hbox.addWidget(use_pp)
        pp_hbox.addWidget(pp_field)

        hbox = QHBoxLayout()
        buttonBox = QDialogButtonBox(self)
        buttonBox.setStandardButtons(QDialogButtonBox.Ok |
                                     QDialogButtonBox.Cancel)
        buttonBox.button(QDialogButtonBox.Ok).clicked.connect(d.accept)
        buttonBox.button(QDialogButtonBox.Cancel).clicked.connect(d.reject)
        hbox.addWidget(buttonBox)
        layout.addLayout(hbox, 4, 0)
        layout.addLayout(pp_hbox, 3, 0)
        result = d.exec_()
        if result != QDialog.Accepted:
            return None, None
        mn_extension = None
        if use_pp.checkState() == QtCore.Qt.Checked:
            mn_extension = pp_field.text()
        return message_e.toPlainText(), mn_extension

    def autofreeze_warning_cb(self, utxo):
        """ Handles coins sent to reused addresses,
        preventing forced address reuse, according to value of
        POLICY setting `max_sats_freeze_reuse` (see
        WalletService.check_for_reuse()).
        """
        success, utxostr = utxo_to_utxostr(utxo)
        assert success, "Autofreeze warning cb called with invalid utxo."
        msg = "New utxo has been automatically " +\
             "frozen to prevent forced address reuse:\n" + utxostr +\
             "\n You can unfreeze this utxo via the Coins tab."
        JMQtMessageBox(self, msg, mbtype='info',
                       title="New utxo frozen")

    def restartWithMsg(self, msg):
        JMQtMessageBox(self, msg, mbtype='info',
                       title="Restart")
        self.close()

    def recoverWallet(self):
        try:
            success = wallet_generate_recover_bip39(
                "recover", os.path.join(jm_single().datadir, 'wallets'),
                "wallet.jmdat",
                display_seed_callback=None,
                enter_seed_callback=self.seedEntry,
                enter_wallet_password_callback=self.getPassword,
                enter_wallet_file_name_callback=self.getWalletFileName,
                enter_if_use_seed_extension=None,
                enter_seed_extension_callback=None,
                enter_do_support_fidelity_bonds=lambda: False)
            if not success:
                JMQtMessageBox(self,
                           "Failed to recover wallet.",
                           mbtype='warn',
                           title="Error")
                return
        except Exception as e:
            JMQtMessageBox(self, e.args[0], title="Error", mbtype="warn")
            return

        JMQtMessageBox(self, 'Wallet saved to ' + self.walletname,
                                   title="Wallet created")
        self.initWallet(seed=self.walletname)

    def openWallet(self):
        wallet_loaded = False
        error_text = ""

        while not wallet_loaded:
            openWalletDialog = JMOpenWalletDialog()
            # Set default wallet file name and verify its lock status
            openWalletDialog.walletFileEdit.setText(openWalletDialog.DEFAULT_WALLET_FILE_TEXT)
            openWalletDialog.errorMessageLabel.setText(openWalletDialog.verify_lock())

            if openWalletDialog.exec_() == QDialog.Accepted:
                wallet_file_text = openWalletDialog.walletFileEdit.text()
                wallet_path = wallet_file_text
                if not os.path.isabs(wallet_path):
                    wallet_path = os.path.join(jm_single().datadir, 'wallets', wallet_path)

                try:
                    wallet_loaded = mainWindow.loadWalletFromBlockchain(wallet_path, openWalletDialog.passphraseEdit.text(), rethrow=True)
                except Exception as e:
                    error_text = str(e)
            else:
                break

    def selectWallet(self, testnet_seed=None):
        if jm_single().config.get("BLOCKCHAIN", "blockchain_source") != "regtest":
            # guaranteed to exist as load_program_config was called on startup:
            wallets_path = os.path.join(jm_single().datadir, 'wallets')
            firstarg = QFileDialog.getOpenFileName(self,
                                                   'Choose Wallet File',
                                                   wallets_path,
                                                   options=QFileDialog.DontUseNativeDialog)
            #TODO validate the file looks vaguely like a wallet file
            log.debug('Looking for wallet in: ' + str(firstarg))
            if not firstarg or not firstarg[0]:
                return
            decrypted = False
            while not decrypted:
                text, ok = QInputDialog.getText(self,
                                                'Decrypt wallet',
                                                'Enter your password:',
                                                echo=QLineEdit.Password)
                if not ok:
                    return
                pwd = str(text).strip()
                try:
                    decrypted = self.loadWalletFromBlockchain(firstarg[0], pwd)
                except Exception as e:
                    JMQtMessageBox(self,
                               str(e),
                               mbtype='warn',
                               title="Error")
                    return
                if decrypted == "error":
                    # special case, not a failure to decrypt the file but
                    # a failure of wallet loading, give up:
                    self.close()
        else:
            if not testnet_seed:
                testnet_seed, ok = QInputDialog.getText(self,
                                                        'Load Testnet wallet',
                                                        'Enter a testnet seed:',
                                                        QLineEdit.Normal)
                if not ok:
                    return
            firstarg = str(testnet_seed)
            pwd = None
            #ignore return value as there is no decryption failure possible
            self.loadWalletFromBlockchain(firstarg, pwd)

    def loadWalletFromBlockchain(self, firstarg=None, pwd=None, rethrow=False):
        if firstarg:
            wallet_path = get_wallet_path(str(firstarg), None)
            try:
                wallet = open_test_wallet_maybe(wallet_path, str(firstarg),
                        None, ask_for_password=False, password=pwd.encode('utf-8') if pwd else None,
                        gap_limit=jm_single().config.getint("GUI", "gaplimit"))
            except RetryableStorageError as e:
                if rethrow:
                    raise e
                else:
                    JMQtMessageBox(self,
                                str(e),
                                mbtype='warn',
                                title="Error")
                    return False
            # only used for GUI display on regtest:
            self.testwalletname = wallet.seed = str(firstarg)
        if 'listunspent_args' not in jm_single().config.options('POLICY'):
            jm_single().config.set('POLICY', 'listunspent_args', '[0]')
        assert wallet, "No wallet loaded"

        # shut down any existing wallet service
        # monitoring loops
        if self.wallet_service is not None:
            if self.wallet_service.isRunning():
                self.wallet_service.stopService()
        if self.walletRefresh is not None:
            self.walletRefresh.stop()

        self.wallet_service = WalletService(wallet)

        # in case an RPC error occurs in the constructor:
        if self.wallet_service.rpc_error:
            JMQtMessageBox(self,self.wallet_service.rpc_error,
                           mbtype='warn',title="Error")
            return "error"

        if jm_single().bc_interface is None:
            self.centralWidget().widget(0).updateWalletInfo(
                get_wallet_printout(self.wallet_service))
            return True

        # add information callbacks:
        self.wallet_service.add_restart_callback(self.restartWithMsg)
        self.wallet_service.autofreeze_warning_cb = self.autofreeze_warning_cb
        self.wallet_service.startService()
        self.syncmsg = ""
        self.walletRefresh = task.LoopingCall(self.updateWalletInfo, None, None)
        self.walletRefresh.start(5.0)

        self.statusBar().showMessage("Reading wallet from blockchain ...")
        return True

    def updateWalletInfo(self, txd, txid):
        """ TODO: see use of `jmclient.BaseWallet.process_new_tx` in
        `jmclient.WalletService.transaction_monitor`;
        we could similarly find the exact utxos to update in the view,
        though it's not entirely trivial, using the provided arguments.
        For now we can just recreate the entire view, as this event is rare.
        """
        t = self.centralWidget().widget(0)
        if not self.wallet_service:  #failure to sync in constructor means object is not created
            newsyncmsg = "Unable to sync wallet - see error in console."
        elif not self.wallet_service.isRunning():
            JMQtMessageBox(self,
                           "The Joinmarket wallet service has stopped; this is usually caused "
                           "by a Bitcoin Core RPC connection failure. Is your node running?",
                           mbtype='crit',
                           title="Error")
            qApp.exit(EXIT_FAILURE)
            return
        elif not self.wallet_service.synced:
            return
        else:
            # sync phase is finished; we now wait for callbacks:
            if self.walletRefresh and self.walletRefresh.running:
                self.walletRefresh.stop()
                self.walletRefresh = None
            # the callback allows us to update our view only on changes:
            if not self.update_registered:
                self.wallet_service.register_callbacks(
                    [self.updateWalletInfo], None, "all")
                self.update_registered = True
            try:
                t.updateWalletInfo(get_wallet_printout(self.wallet_service))
            except Exception:
                # this is very likely to happen in case Core RPC connection goes
                # down (but, order of events means it is not deterministic).
                log.debug("Failed to get wallet information, is there a problem with "
                          "the blockchain interface?")
                return
            newsyncmsg = "Wallet synced successfully."
        if newsyncmsg != self.syncmsg:
            self.syncmsg = newsyncmsg
            self.statusBar().showMessage(self.syncmsg)

    def generateWallet(self):
        log.debug('generating wallet')
        if jm_single().config.get("BLOCKCHAIN", "blockchain_source") == "regtest":
            seed = self.getTestnetSeed()
            self.selectWallet(testnet_seed=seed)
        else:
            self.initWallet()

    def checkPassphrase(self):
        match = False
        while not match:
            text, ok = QInputDialog.getText(self, 'Passphrase check',
                                            'Enter your passphrase:',
                                            echo=QLineEdit.Password)
            if not ok:
                return False
            pwd = str(text).strip().encode('utf-8')
            match = self.wallet_service.check_wallet_passphrase(pwd)
            if not match:
                JMQtMessageBox(self,
                               "Wrong passphrase.", mbtype='warn', title="Error")
        return True

    def changePassphrase(self):
        if not self.wallet_service:
            JMQtMessageBox(self, "Cannot change passphrase without loaded wallet.",
                           mbtype="crit", title="Error")
            return
        if not (self.checkPassphrase()
                and wallet_change_passphrase(self.wallet_service, self.getPassword)):
            JMQtMessageBox(self, "Failed to change passphrase.",
                           title="Error", mbtype="warn")
            return
        JMQtMessageBox(self, "Passphrase changed successfully.",
                       title="Passphrase changed")

    def getTestnetSeed(self):
        text, ok = QInputDialog.getText(
            self, 'Testnet seed', 'Enter a 32 char hex string as seed:')
        if not ok or not text:
            JMQtMessageBox(self,
                           "No seed entered, aborting",
                           mbtype='warn',
                           title="Error")
            return
        return str(text).strip()

    def showSeedDialog(self):
        if not self.wallet_service:
            JMQtMessageBox(self,
                           "No wallet loaded.",
                           mbtype='crit',
                           title="Error")
            return
        try:
            self.displayWords(*self.wallet_service.get_mnemonic_words())
        except NotImplementedError:
            JMQtMessageBox(self,
                           "Wallet does not support seed phrases",
                           mbtype='info',
                           title="Error")

    def getPassword(self) -> str:
        pd = PasswordDialog()
        while True:
            for child in pd.findChildren(QLineEdit):
                child.clear()
            pd.findChild(QLineEdit).setFocus()
            pd_return = pd.exec_()
            if pd_return == QDialog.Rejected:
                return None
            elif pd.new_pw.text() != pd.conf_pw.text():
                JMQtMessageBox(self,
                               "Passphrases don't match.",
                               mbtype='warn',
                               title="Error")
                continue
            elif pd.new_pw.text() == "":
                JMQtMessageBox(self,
                               "Passphrase must not be empty.",
                               mbtype='warn',
                               title="Error")
                continue
            break
        self.textpassword = str(pd.new_pw.text())
        return self.textpassword.encode('utf-8')

    def getWalletFileName(self) -> str:
        walletname, ok = QInputDialog.getText(self, 'Choose wallet name',
                                              'Enter wallet file name:',
                                              QLineEdit.Normal, "wallet.jmdat")
        if not ok:
            JMQtMessageBox(self, "Create wallet aborted", mbtype='warn')
            # cannot use None for a 'fail' condition, as this is used
            # for the case where the default wallet name is to be used in non-Qt.
            return "cancelled"
        self.walletname = str(walletname)
        return self.walletname

    def displayWords(self, words: str, mnemonic_extension: str) -> None:
        mb = QMessageBox(self)
        seed_recovery_warning = [
            "WRITE DOWN THIS WALLET RECOVERY SEED.",
            "If you fail to do this, your funds are",
            "at risk. Do NOT ignore this step!!!"
        ]
        mb.setText("<br/>".join(seed_recovery_warning))
        text = "<strong>" + words + "</strong>"
        if mnemonic_extension:
            text += "<br/><br/>Seed extension: <strong>" + mnemonic_extension.decode('utf-8') + "</strong>"
        mb.setInformativeText(text)
        mb.setStandardButtons(QMessageBox.Ok)
        ret = mb.exec_()

    def promptUseMnemonicExtension(self) -> bool:
        msg = "Would you like to use a two-factor mnemonic recovery phrase?\nIf you don\'t know what this is press No."
        reply = QMessageBox.question(self, 'Use mnemonic extension?',
                    msg, QMessageBox.Yes, QMessageBox.No)
        return reply == QMessageBox.Yes

    def promptInputMnemonicExtension(self) -> Optional[str]:
        mnemonic_extension, ok = QInputDialog.getText(self,
                                     'Input Mnemonic Extension',
                                     'Enter mnemonic Extension:',
                                     QLineEdit.Normal, "")
        if not ok:
            return None
        return str(mnemonic_extension)

    def initWallet(self, seed=None):
        '''Creates a new wallet if seed not provided.
        Initializes by syncing.
        '''
        if not seed:
            try:
                # guaranteed to exist as load_program_config was called on startup:
                wallets_path = os.path.join(jm_single().datadir, 'wallets')
                success = wallet_generate_recover_bip39(
                    "generate", wallets_path, "wallet.jmdat",
                    display_seed_callback=self.displayWords,
                    enter_seed_callback=None,
                    enter_wallet_password_callback=self.getPassword,
                    enter_wallet_file_name_callback=self.getWalletFileName,
                    enter_if_use_seed_extension=self.promptUseMnemonicExtension,
                    enter_seed_extension_callback=self.promptInputMnemonicExtension,
                    enter_do_support_fidelity_bonds=lambda: False)

                if not success:
                    JMQtMessageBox(self, "Failed to create new wallet file.",
                                   title="Error", mbtype="warn")
                    return
            except Exception as e:
                JMQtMessageBox(self, e.args[0], title="Error", mbtype="warn")
                return

            JMQtMessageBox(self, 'Wallet saved to ' + self.walletname,
                           title="Wallet created")
        self.loadWalletFromBlockchain(self.walletname, pwd=self.textpassword)

def get_wallet_printout(wallet_service):
    """Given a WalletService object, retrieve the list of
    addresses and corresponding balances to be displayed.
    We retrieve a WalletView abstraction, and iterate over
    sub-objects to arrange the per-mixdepth and per-address lists.
    The format of the returned data is:
    rows: is of format [[[addr,index,bal,status,label],[addr,...]]*5,
    [[addr, index,..], [addr, index..]]*5]
    mbalances: is a simple array of 5 mixdepth balances
    xpubs: [[xpubext, xpubint], ...]
    Bitcoin amounts returned are in btc, not satoshis
    """
    walletview = wallet_display(wallet_service, False, serialized=False)
    rows = []
    mbalances = []
    xpubs = []
    for j, acct in enumerate(walletview.children):
        mbalances.append(acct.get_fmt_balance())
        rows.append([])
        xpubs.append([])
        for i, branch in enumerate(acct.children):
            xpubs[j].append(branch.xpub)
            rows[j].append([])
            for entry in branch.children:
                rows[-1][i].append([entry.serialize_address(),
                                    entry.serialize_wallet_position(),
                                    entry.serialize_amounts(),
                                    entry.serialize_status(),
                                    entry.serialize_label()])
        # Append the account xpub to the end of the list
        FBONDS_PUBKEY_PREFIX = "fbonds-mpk-"
        account_xpub = acct.xpub
        if account_xpub.startswith(FBONDS_PUBKEY_PREFIX):
            account_xpub = account_xpub[len(FBONDS_PUBKEY_PREFIX):]
        xpubs[j].append(account_xpub)
    # in case the wallet is not yet synced, don't return an incorrect
    # 0 balance, but signal incompleteness:
    total_bal = walletview.get_fmt_balance() if wallet_service.synced else None
    return (rows, mbalances, xpubs, total_bal)

################################

parser = OptionParser(usage='usage: %prog [options]')
add_base_options(parser)
# wallet related base options are not applicable:
parser.remove_option("--recoversync")
parser.remove_option("--wallet-password-stdin")
(options, args) = parser.parse_args()

config_load_error = False
try:
    load_program_config(config_path=options.datadir)
except Exception as e:
    config_load_error = "Failed to setup joinmarket: "+repr(e)
    if "RPC" in repr(e):
        config_load_error += '\n'*3 + ''.join(
            ["Errors about failed RPC connections usually mean an incorrectly ",
             "configured instance of Bitcoin Core (e.g. it hasn't been started ",
             "or the rpc ports are not correct in your joinmarket.cfg or your ",
             "bitcoin.conf file)."
             ])
    JMQtMessageBox(None, config_load_error, mbtype='crit', title='failed to load')
    sys.exit(EXIT_FAILURE)
# Only partial functionality (see wallet info, change config) is possible
# without a blockchain interface.
if jm_single().bc_interface is None:
    blockchain_warning = ''.join([
        "No blockchain source currently configured. ",
        "You will be able to see wallet information and change configuration ",
        "but other functionality will be limited. ",
        "Go to the 'Settings' tab and configure blockchain settings there."])
    JMQtMessageBox(None, blockchain_warning, mbtype='warn',
        title='No blockchain source')
#refuse to load non-segwit wallet (needs extra work in wallet-utils).
if not jm_single().config.get("POLICY", "segwit") == "true":
    wallet_load_error = ''.join(["Joinmarket-Qt only supports segwit based wallets, ",
                                 "please edit the config file and remove any setting ",
                                 "of the field `segwit` in the `POLICY` section."])
    JMQtMessageBox(None, wallet_load_error, mbtype='crit',
                   title='Incompatible wallet type')
    sys.exit(EXIT_FAILURE)

update_config_for_gui()

check_and_start_tor()

def onTabChange(i):
    """ Respond to change of tab.
    """
    # TODO: hardcoded literal;
    # note that this is needed for an auto-update
    # of utxos on the Coins tab only atm.
    if i == 2:
        tabWidget.widget(2).updateUtxos()

#to allow testing of confirm/unconfirm callback for multiple txs
if isinstance(jm_single().bc_interface, RegtestBitcoinCoreInterface):
    jm_single().bc_interface.tick_forward_chain_interval = 10
    jm_single().bc_interface.simulating = True
    jm_single().maker_timeout_sec = 15
    #trigger start with a fake tx
    jm_single().bc_interface.pushtx(b"\x00"*20)

logsdir = os.path.join(os.path.dirname(jm_single().config_location), "logs")
#tumble log will not always be used, but is made available anyway:
tumble_log = get_tumble_log(logsdir)
#ignored makers list persisted across entire app run
ignored_makers = []
appWindowTitle = 'JoinMarketQt'
from twisted.internet import reactor
mainWindow = JMMainWindow(reactor)
tabWidget = QTabWidget(mainWindow)
tabWidget.addTab(JMWalletTab(), "JM Wallet")
tabWidget.addTab(SpendTab(), "Coinjoins")
tabWidget.addTab(CoinsTab(), "Coins")
tabWidget.addTab(TxHistoryTab(), "Tx History")
settingsTab = SettingsTab()
tabWidget.addTab(settingsTab, "Settings")

mainWindow.resize(600, 500)
if get_network() == 'testnet':
    suffix = ' - Testnet'
elif get_network() == 'signet':
    suffix = ' - Signet'
else:
    suffix = ''
mainWindow.setWindowTitle(appWindowTitle + suffix)
tabWidget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
mainWindow.setCentralWidget(tabWidget)
tabWidget.currentChanged.connect(onTabChange)

mainWindow.show()
reactor.runReturn()

# Qt does not stop automatically when we stop the qt5reactor, and
# also we don't want to close without warning the user;
# patch our stop_reactor method to include the necessary cleanup:
def qt_shutdown():
    # checking ensures we only fire the close
    # event once even if stop_reactor is called
    # multiple times (which it often is):
    if mainWindow.isVisible():
        mainWindow.unconditional_shutdown = True
        mainWindow.close()
set_custom_stop_reactor(qt_shutdown)

# Upon launching the app, ask the user to choose a wallet to open
mainWindow.openWallet()

sys.exit(app.exec_())
