示例#1
0
    def setupUi(self):
        ui_send_payout_dlg.Ui_SendPayoutDlg.setupUi(self, self)
        assert isinstance(self.tableView, QTableView)
        self.resize(cache.get_value('WndPayoutWidth', 800, int),
                    cache.get_value('WndPayoutHeight', 460, int))
        self.setWindowTitle('Transfer funds')
        self.closeEvent = self.closeEvent
        self.chbHideCollateralTx.setChecked(True)
        self.edtDestAddress.setText(
            cache.get_value('WndPayoutPaymentAddress', '', str))
        self.edtDestAddress.textChanged.connect(self.edtDestAddressChanged)
        self.setIcon(self.btnCheckAll, 'check.png')
        self.setIcon(self.btnUncheckAll, 'uncheck.png')

        self.table_model = PaymentTableModel(
            None, self.chbHideCollateralTx.isChecked(),
            self.onUtxoCheckChanged, self)
        self.tableView.setModel(self.table_model)
        self.tableView.horizontalHeader().resizeSection(0, 35)
        self.tableView.horizontalHeader().setSectionResizeMode(
            0, QHeaderView.Fixed)
        self.tableView.verticalHeader().setDefaultSectionSize(
            self.tableView.verticalHeader().fontMetrics().height() + 6)

        # set utxo table default column widths
        cws = cache.get_value('WndPayoutColWidths',
                              self.table_model.getDefaultColWidths(), list)
        for col, w in enumerate(cws):
            self.tableView.setColumnWidth(col, w)

        self.chbHideCollateralTx.toggled.connect(
            self.chbHideCollateralTxToggled)
        self.resizeEvent = self.resizeEvent

        if self.main_ui.config.hw_type == HWType.ledger_nano_s:
            self.pnlSourceAddress.setVisible(False)
            self.org_message = '<span style="color:red">Sending funds controlled by Ledger Nano S wallets is not ' \
                               'supported yet.</span>'
            self.setMessage(self.org_message)
            self.load_utxos()
            self.btnSend.setEnabled(False)
        elif len(self.utxos_source):
            self.pnlSourceAddress.setVisible(False)
            self.org_message = 'List of Unspent Transaction Outputs <i>(UTXOs)</i> for specified address(es). ' \
                               'Select checkboxes for the UTXOs you wish to transfer.'
            self.setMessage(self.org_message)
            self.load_utxos()
        else:
            self.setMessage("")
            self.source_address_mode = True
            self.edtSourceBip32Path.setText(
                cache.get_value('SourceBip32Path', '', str))
    def import_connections(self, in_conns, force_import):
        """
        Imports connections from a list. Used at the app's start to process default connections and/or from
          a configuration dialog, when user pastes from a clipboard a string, describing connections he 
          wants to add to the configuration. The latter feature is used for a convenience.
        :param in_conns: list of DashNetworkConnectionCfg objects.
        :returns: tuple (list_of_added_connections, list_of_updated_connections)
        """

        added_conns = []
        updated_conns = []
        if in_conns:
            for nc in in_conns:
                id = nc.get_conn_id()
                # check if new connection is in existing list
                conn = self.get_conn_cfg_by_id(id)
                if not conn:
                    if force_import or not cache.get_value('imported_default_conn_' + nc.get_conn_id(), False, bool):
                        # this new connection was not automatically imported before
                        self.dash_net_configs.append(nc)
                        added_conns.append(nc)
                        cache.set_value('imported_default_conn_' + nc.get_conn_id(), True)
                elif not conn.identical(nc) and force_import:
                    conn.copy_from(nc)
                    updated_conns.append(conn)
        return added_conns, updated_conns
示例#3
0
    def setupUi(self):
        Ui_TransactionDlg.setupUi(self, self)
        self.setWindowTitle('Transaction')
        self.chb_word_wrap.setChecked(
            app_cache.get_value(CACHE_ITEM_DETAILS_WORD_WRAP, False, bool))
        self.apply_word_wrap(self.chb_word_wrap.isChecked())
        self.edt_recipients.setOpenExternalLinks(True)
        self.edt_recipients.viewport().setAutoFillBackground(False)

        if sys.platform == 'win32':
            self.base_font_size = '8'
            self.title_font_size = '12'
        elif sys.platform == 'linux':
            self.base_font_size = '8'
            self.title_font_size = '14'
        else:  # mac
            self.base_font_size = '10'
            self.title_font_size = '17'

        self.edt_raw_transaction.setStyleSheet(
            f'font: {self.base_font_size}pt "Courier New";')
        doc = QTextDocument(self)
        doc.setDocumentMargin(0)
        doc.setHtml(
            f'<span style=" font-size:{self.title_font_size}pt;white-space:nowrap">AAAAAAAAAAAAAAAAAA'
        )
        default_width = int(doc.size().width()) * 3
        default_height = int(default_width / 2)

        app_cache.restore_window_size(self,
                                      default_width=default_width,
                                      default_height=default_height)
        self.prepare_tx_view()
 def setupUi(self):
     Ui_TransactionDlg.setupUi(self, self)
     self.setWindowTitle('Transaction')
     self.chb_word_wrap.setChecked(app_cache.get_value(CACHE_ITEM_DETAILS_WORD_WRAP, False, bool))
     self.apply_word_wrap(self.chb_word_wrap.isChecked())
     self.edt_recipients.setOpenExternalLinks(True)
     self.edt_recipients.viewport().setAutoFillBackground(False)
     self.prepare_tx_view()
    def setupUi(self):
        ui_send_payout_dlg.Ui_SendPayoutDlg.setupUi(self, self)
        assert isinstance(self.tableView, QTableView)
        self.resize(cache.get_value('WndPayoutWidth', 800, int),
                    cache.get_value('WndPayoutHeight', 460, int))
        self.setWindowTitle('Transfer funds')
        self.closeEvent = self.closeEvent
        self.chbHideCollateralTx.setChecked(True)
        self.btnClose.clicked.connect(self.btnCloseClick)
        self.btnSend.clicked.connect(self.btnSendClick)
        self.edtDestAddress.setText(
            cache.get_value('WndPayoutPaymentAddress', '', str))
        self.edtDestAddress.textChanged.connect(self.edtDestAddressChanged)
        self.setIcon(self.btnCheckAll, 'check.png')
        self.setIcon(self.btnUncheckAll, 'uncheck.png')

        self.table_model = PaymentTableModel(
            None, self.chbHideCollateralTx.isChecked(),
            self.onUtxoCheckChanged)
        self.tableView.setModel(self.table_model)
        self.tableView.horizontalHeader().resizeSection(0, 35)
        self.tableView.horizontalHeader().setSectionResizeMode(
            0, QHeaderView.Fixed)
        self.tableView.verticalHeader().setDefaultSectionSize(
            self.tableView.verticalHeader().fontMetrics().height() + 6)

        # set utxo table default column widths
        cws = cache.get_value('WndPayoutColWidths',
                              self.table_model.getDefaultColWidths(), list)
        for col, w in enumerate(cws):
            self.tableView.setColumnWidth(col, w)

        self.chbHideCollateralTx.toggled.connect(
            self.chbHideCollateralTxToggled)
        self.resizeEvent = self.resizeEvent

        self.threadFunctionDialog(self.load_utxos_thread, (),
                                  True,
                                  center_by_window=self.main_ui)
        self.table_model.setUtxos(self.utxos, self.masternodes)
示例#6
0
 def restore_col_defs(self, setting_name: str):
     cols = app_cache.get_value(setting_name, [], list)
     if cols:
         idx = 0
         for _c in cols:
             name = _c.get('name')
             c = self.col_by_name(name)
             if c:
                 c.visual_index = idx
                 c.visible = _c.get('visible', True)
                 c.initial_width = _c.get('width', c.initial_width)
                 idx += 1
         self._columns.sort(key=lambda x: x.visual_index)
         self._rebuild_column_index()
示例#7
0
 def restore_col_defs(self, setting_name: str) -> bool:
     """
     :return: True, if columns settings were found in cache, False otherwise
     """
     cols = app_cache.get_value(setting_name, [], list)
     if cols:
         idx = 0
         for _c in cols:
             name = _c.get('name')
             c = self.col_by_name(name)
             if c:
                 c.visual_index = idx
                 c.visible = _c.get('visible', True)
                 c.initial_width = _c.get('width', c.initial_width)
                 idx += 1
         self._columns.sort(key=lambda x: x.visual_index)
         self._rebuild_column_index()
         return True
     return False
    def init(self, app_path):
        """ Initialize configuration after openning the application. """
        self.app_path = app_path

        try:
            with open(os.path.join(app_path, 'version.txt')) as fptr:
                lines = fptr.read().splitlines()
                self.app_version = app_utils.extract_app_version(lines)
        except:
            pass

        parser = argparse.ArgumentParser()
        parser.add_argument('--config', help="Path to a configuration file", dest='config')
        parser.add_argument('--data-dir', help="Root directory for configuration file, cache and log dubdirs",
                            dest='data_dir')
        args = parser.parse_args()

        app_user_dir = ''
        if args.data_dir:
            if os.path.exists(args.data_dir):
                if os.path.isdir(args.data_dir):
                    app_user_dir = args.data_dir
                else:
                    WndUtils.errorMsg('--data-dir parameter doesn\'t point to a directory. Using the default '
                                      'data directory.')
            else:
                WndUtils.errorMsg('--data-dir parameter doesn\'t point to an existing directory. Using the default '
                                  'data directory.')

        if not app_user_dir:
            home_dir = expanduser('~')
            app_user_dir = os.path.join(home_dir, APP_NAME_SHORT)
            if not os.path.exists(app_user_dir):
                os.makedirs(app_user_dir)

        self.cache_dir = os.path.join(app_user_dir, 'cache')
        if not os.path.exists(self.cache_dir):
            os.makedirs(self.cache_dir)
        cache.init(self.cache_dir, self.app_version)
        self.app_last_version = cache.get_value('app_version', '', str)
        self.app_config_file_name = ''

        if args.config is not None:
            self.app_config_file_name = args.config
            if not os.path.exists(self.app_config_file_name):
                msg = 'Config file "%s" does not exist.' % self.app_config_file_name
                print(msg)
                raise Exception(msg)

        if not self.app_config_file_name:
            self.app_config_file_name = os.path.join(app_user_dir, 'config.ini')

        # setup logging
        self.log_dir = os.path.join(app_user_dir, 'logs')
        self.log_file = os.path.join(self.log_dir, 'dmt.log')
        if not os.path.exists(self.log_dir):
            os.makedirs(self.log_dir)

        self.log_level_str = 'INFO'
        log_exists = os.path.exists(self.log_file)
        handler = RotatingFileHandler(filename=self.log_file, mode='a', backupCount=30)
        logger = logging.getLogger()
        formatter = logging.Formatter(fmt='%(asctime)s %(levelname)s |%(threadName)s |%(filename)s |%(funcName)s '
                                          '|%(message)s', datefmt='%Y-%m-%d %H:%M:%S')
        handler.setFormatter(formatter)
        logger.addHandler(handler)
        logger.setLevel(self.log_level_str)
        if log_exists:
            handler.doRollover()
        logging.info('App started')

        # database (SQLITE) cache for caching bigger datasets:
        self.db_cache_file_name = os.path.join(self.cache_dir, 'dmt_cache.db')

        try:
            self.db_intf = DBCache(self.db_cache_file_name)
        except Exception as e:
            logging.exception('SQLite initialization error')

        # directory for configuration backups:
        self.cfg_backup_dir = os.path.join(app_user_dir, 'backup')
        if not os.path.exists(self.cfg_backup_dir):
            os.makedirs(self.cfg_backup_dir)

        try:
            # read configuration from a file
            self.read_from_file()
        except:
            pass

        if not self.app_last_version or \
           app_utils.version_str_to_number(self.app_last_version) < app_utils.version_str_to_number(self.app_version):
            cache.save_data()
        self.initialized = True
示例#9
0
 def restore_cache_settings(self):
     app_cache.restore_window_size(self)
     self.show_manual_commands = app_cache.get_value(
         CACHE_ITEM_SHOW_COMMANDS, False, bool)
    def restore_cache_settings(self):
        app_cache.restore_window_size(self)
        if self.initial_mn_sel is None:
            mode = app_cache.get_value(CACHE_ITEM_UTXO_SOURCE_MODE, 2, int)
            if mode in (1, 2, 3):
                self.utxo_src_mode = mode

        # activated bip32 account numbers:
        nrs = app_cache.get_value(CACHE_ITEM_HW_ACCOUNT_NUMBERS, [0, 1, 2, 3, 4], list)
        nrs.sort()
        self.hw_account_numbers.clear()
        self.hw_account_numbers.extend(nrs)

        # base bip32 path:
        path = app_cache.get_value(CACHE_ITEM_HW_ACCOUNT_BASE_PATH.replace('%NETWORK%', self.app_config.gobyte_network),
                                   gobyte_utils.get_default_bip32_base_path(self.app_config.gobyte_network), str)
        if gobyte_utils.validate_bip32_path(path):
            self.hw_account_base_bip32_path = path
        else:
            self.hw_account_base_bip32_path = gobyte_utils.get_default_bip32_base_path(self.app_config.gobyte_network)

        # account number:
        nr = app_cache.get_value(CACHE_ITEM_HW_ACCOUNT_NUMBER, 0, int)
        if nr < 0:
            nr = 0
        self.hw_account_number = nr

        # bip32 path (utxo_src_mode 3)
        path = app_cache.get_value(CACHE_ITEM_HW_SRC_BIP32_PATH.replace('%NETWORK%', self.app_config.gobyte_network),
                                   gobyte_utils.get_default_bip32_path(self.app_config.gobyte_network), str)
        if gobyte_utils.validate_bip32_path(path):
            self.hw_src_bip32_path = path
        else:
            self.hw_src_bip32_path = gobyte_utils.get_default_bip32_path(self.app_config.gobyte_network)

        self.grid_column_widths = app_cache.get_value(CACHE_ITEM_COL_WIDTHS, self.table_model.getDefaultColWidths(),
                                                      list)

        sel_nasternode = app_cache.get_value(
            CACHE_ITEM_UTXO_SRC_MASTRNODE.replace('%NETWORK%', self.app_config.gobyte_network), '', str)
        if sel_nasternode:
            if sel_nasternode == '<ALL>':
                self.mn_src_index = len(self.masternodes)
            else:
                for idx, mn in enumerate(self.masternodes):
                    if mn.name == sel_nasternode:
                        self.mn_src_index = idx
                        break

        # restore last list of used addresses
        enc_json_str = app_cache.get_value(CACHE_ITEM_LAST_RECIPIENTS.replace('%NETWORK%', self.app_config.gobyte_network), None, str)
        if enc_json_str:
            try:
                # hw encryption key may be not available so use the generated key to not save addresses as plain text
                self.encryption_key = base64.urlsafe_b64encode(self.app_config.hw_generated_key)
                fernet = Fernet(self.encryption_key)
                enc_json_str = bytes(enc_json_str, 'ascii')
                json_str = fernet.decrypt(enc_json_str)
                json_str = json_str.decode('ascii')
                self.recipient_list_from_cache = simplejson.loads(json_str)
            except Exception:
                logging.exception('Cannot restore data from cache.')
示例#11
0
    def setupUi(self):
        Ui_ConfigDlg.setupUi(self, self)
        self.resize(
            app_cache.get_value('ConfigDlg_Width',
                                self.size().width(), int),
            app_cache.get_value('ConfigDlg_Height',
                                self.size().height(), int))

        self.setWindowTitle("Configuration")
        self.splitter.setStretchFactor(0, 0)
        self.splitter.setStretchFactor(1, 1)
        self.accepted.connect(self.on_accepted)
        self.tabWidget.setCurrentIndex(0)

        self.disable_cfg_update = True

        layout_details = self.detailsFrame.layout()
        self.chbConnEnabled = QCheckBox("Enabled")
        self.chbConnEnabled.toggled.connect(self.on_chbConnEnabled_toggled)
        layout_details.addWidget(self.chbConnEnabled)
        self.chbUseSshTunnel = QCheckBox("Use SSH tunnel")
        self.chbUseSshTunnel.toggled.connect(self.on_chbUseSshTunnel_toggled)
        layout_details.addWidget(self.chbUseSshTunnel)
        self.ssh_tunnel_widget = SshConnectionWidget(self)
        layout_details.addWidget(self.ssh_tunnel_widget)

        # layout for button for reading RPC configuration from remote host over SSH:
        hl = QHBoxLayout()
        self.btnSshReadRpcConfig = QPushButton(
            "\u2193 Read RPC configuration from SSH host \u2193")
        self.btnSshReadRpcConfig.clicked.connect(
            self.on_btnSshReadRpcConfig_clicked)
        hl.addWidget(self.btnSshReadRpcConfig)
        hl.addStretch()
        layout_details.addLayout(hl)

        # add connection-editing controls widget:
        self.rpc_cfg_widget = RpcConnectionWidget(self.detailsFrame)
        layout_details.addWidget(self.rpc_cfg_widget)

        # layout for controls related to setting up an additional encryption
        hl = QHBoxLayout()
        self.btnEncryptionPublicKey = QPushButton("RPC encryption public key")
        self.btnEncryptionPublicKey.clicked.connect(
            self.on_btnEncryptionPublicKey_clicked)
        hl.addWidget(self.btnEncryptionPublicKey)
        self.lblEncryptionPublicKey = QLabel(self)
        self.lblEncryptionPublicKey.setText('')
        hl.addWidget(self.lblEncryptionPublicKey)
        hl.addStretch()
        layout_details.addLayout(hl)

        # layout for the 'test connection' button:
        hl = QHBoxLayout()
        self.btnTestConnection = QPushButton("\u2713 Test connection")
        self.btnTestConnection.clicked.connect(
            self.on_btnTestConnection_clicked)
        sp = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum,
                                   QtWidgets.QSizePolicy.Minimum)
        sp.setHorizontalStretch(0)
        sp.setVerticalStretch(0)
        sp.setHeightForWidth(
            self.btnTestConnection.sizePolicy().hasHeightForWidth())
        self.btnTestConnection.setSizePolicy(sp)
        hl.addWidget(self.btnTestConnection)
        hl.addStretch()
        layout_details.addLayout(hl)
        layout_details.addStretch()

        self.rpc_cfg_widget.edtRpcHost.textEdited.connect(
            self.on_edtRpcHost_textEdited)
        self.rpc_cfg_widget.edtRpcPort.textEdited.connect(
            self.on_edtRpcPort_textEdited)
        self.rpc_cfg_widget.edtRpcUsername.textEdited.connect(
            self.on_edtRpcUsername_textEdited)
        self.rpc_cfg_widget.edtRpcPassword.textEdited.connect(
            self.on_edtRpcPassword_textEdited)
        self.rpc_cfg_widget.chbRpcSSL.toggled.connect(self.chbRpcSSL_toggled)
        self.ssh_tunnel_widget.edtSshHost.textEdited.connect(
            self.on_edtSshHost_textEdited)
        self.ssh_tunnel_widget.edtSshPort.textEdited.connect(
            self.on_edtSshPort_textEdited)
        self.ssh_tunnel_widget.edtSshUsername.textEdited.connect(
            self.on_edtSshUsername_textEdited)
        self.ssh_tunnel_widget.cboAuthentication.currentIndexChanged.connect(
            self.on_cboSshAuthentication_currentIndexChanged)
        self.ssh_tunnel_widget.edtPrivateKeyPath.textChanged.connect(
            self.on_edtSshPrivateKeyPath_textChanged)

        self.lstConns.setContextMenuPolicy(Qt.CustomContextMenu)
        self.popMenu = QMenu(self)

        self.action_new_connection = self.popMenu.addAction(
            "Add new connection")
        self.action_new_connection.triggered.connect(
            self.on_action_new_connection_triggered)
        self.setIcon(self.action_new_connection, '*****@*****.**')
        self.btnNewConn.setDefaultAction(self.action_new_connection)

        self.action_delete_connections = self.popMenu.addAction(
            "Delete selected connection(s)")
        self.action_delete_connections.triggered.connect(
            self.on_action_delete_connections_triggered)
        self.setIcon(self.action_delete_connections, '*****@*****.**')
        self.btnDeleteConn.setDefaultAction(self.action_delete_connections)

        self.action_copy_connections = self.popMenu.addAction(
            "Copy connection(s) to clipboard",
            self.on_action_copy_connections_triggered, QKeySequence("Ctrl+C"))
        self.setIcon(self.action_copy_connections, '*****@*****.**')
        self.addAction(self.action_copy_connections)

        self.action_paste_connections = self.popMenu.addAction(
            "Paste connection(s) from clipboard",
            self.on_action_paste_connections_triggered, QKeySequence("Ctrl+V"))
        self.setIcon(self.action_paste_connections, '*****@*****.**')
        self.addAction(self.action_paste_connections)

        self.btnNewConn.setText("")
        self.btnDeleteConn.setText("")
        self.btnMoveDownConn.setText("")
        self.btnMoveUpConn.setText("")
        self.btnRestoreDefault.setText("")
        self.setIcon(self.btnMoveDownConn, "*****@*****.**")
        self.setIcon(self.btnMoveUpConn, "*****@*****.**", rotate=180)
        self.setIcon(self.btnRestoreDefault, "*****@*****.**")
        self.setIcon(self.rpc_cfg_widget.btnShowPassword, "*****@*****.**")

        self.rpc_cfg_widget.btnShowPassword.setText("")
        self.rpc_cfg_widget.btnShowPassword.pressed.connect(
            lambda: self.rpc_cfg_widget.edtRpcPassword.setEchoMode(QLineEdit.
                                                                   Normal))
        self.rpc_cfg_widget.btnShowPassword.released.connect(
            lambda: self.rpc_cfg_widget.edtRpcPassword.setEchoMode(QLineEdit.
                                                                   Password))

        if self.local_config.is_mainnet():
            self.cboDashNetwork.setCurrentIndex(0)
            self.connections_current = self.connections_mainnet
        else:
            self.cboDashNetwork.setCurrentIndex(1)
            self.connections_current = self.connections_testnet
        for cfg in self.local_config.dash_net_configs:
            if cfg.testnet:
                self.connections_testnet.append(cfg)
            else:
                self.connections_mainnet.append(cfg)

        if self.local_config.hw_type == HWType.trezor:
            self.chbHwTrezor.setChecked(True)
        elif self.local_config.hw_type == HWType.keepkey:
            self.chbHwKeepKey.setChecked(True)
        else:
            self.chbHwLedgerNanoS.setChecked(True)

        if self.local_config.hw_keepkey_psw_encoding == 'NFC':
            self.cboKeepkeyPassEncoding.setCurrentIndex(0)
        else:
            self.cboKeepkeyPassEncoding.setCurrentIndex(1)
        note_url = get_note_url('DMTN0001')
        self.lblKeepkeyPassEncoding.setText(
            f'KepKey passphrase encoding (<a href="{note_url}">see</a>)')

        self.chbCheckForUpdates.setChecked(self.local_config.check_for_updates)
        self.chbBackupConfigFile.setChecked(
            self.local_config.backup_config_file)
        self.chbDownloadProposalExternalData.setChecked(
            self.local_config.read_proposals_external_attributes)
        self.chbDontUseFileDialogs.setChecked(
            self.local_config.dont_use_file_dialogs)
        self.chbConfirmWhenVoting.setChecked(
            self.local_config.confirm_when_voting)
        self.chbAddRandomOffsetToVotingTime.setChecked(
            self.local_config.add_random_offset_to_vote_time)
        self.chbEncryptConfigFile.setChecked(
            self.local_config.encrypt_config_file)

        idx = {
            'CRITICAL': 0,
            'ERROR': 1,
            'WARNING': 2,
            'INFO': 3,
            'DEBUG': 4,
            'NOTSET': 5
        }.get(self.local_config.log_level_str, 2)
        self.cboLogLevel.setCurrentIndex(idx)

        self.display_connection_list()
        if len(self.local_config.dash_net_configs):
            self.lstConns.setCurrentRow(0)

        self.update_keepkey_pass_encoding_ui()
        self.update_connection_details_ui()
        self.disable_cfg_update = False
        self.splitter.setSizes(
            app_cache.get_value('ConfigDlg_ConnectionSplitter_Sizes',
                                [100, 100], list))
示例#12
0
 def get_cache_value(self, name, default_value, type):
     return app_cache.get_value(self.__class__.__name__ + '_' + name, default_value, type)
    def get_ghostnodelist(self, *args, data_max_age=MASTERNODES_CACHE_VALID_SECONDS):
        """
        Returns masternode list, read from the Dash network or from the internal cache.
        :param args: arguments passed to the 'ghostnodelist' RPC call
        :param data_max_age: maximum age (in seconds) of the cached masternode data to used; if the
            cache is older than 'data_max_age', then an RPC call is performed to load newer masternode data;
            value of 0 forces reading of the new data from the network
        :return: list of Masternode objects, matching the 'args' arguments
        """
        def parse_mns(mns_raw):
            """
            Parses dictionary of strings returned from the RPC to Masternode object list.
            :param mns_raw: Dict of masternodes in format of RPC ghostnodelist command
            :return: list of Masternode object
            """
            tm_begin = time.time()
            ret_list = []
            for mn_id in mns_raw.keys():
                mn_raw = mns_raw.get(mn_id)
                mn_raw = mn_raw.strip()
                elems = mn_raw.split()
                if len(elems) >= 8:
                    mn = Masternode()
                    # (status, protocol, payee, lastseen, activeseconds, lastpaidtime, pastpaidblock, ip)
                    mn.status, mn.protocol, mn.payee, mn.lastseen, mn.activeseconds, mn.lastpaidtime, \
                        mn.lastpaidblock, mn.ip = elems

                    mn.lastseen = int(mn.lastseen)
                    mn.activeseconds = int(mn.activeseconds)
                    mn.lastpaidtime = int(mn.lastpaidtime)
                    mn.lastpaidblock = int(mn.lastpaidblock)
                    mn.ident = mn_id
                    ret_list.append(mn)
            duration = time.time() - tm_begin
            logging.info('Parse ghostnodelist time: ' + str(duration))
            return ret_list

        def update_masternode_data(existing_mn, new_data, cursor):
            # update cached masternode's properties
            existing_mn.modified = False
            existing_mn.monitor_changes = True
            existing_mn.ident = new_data.ident
            existing_mn.status = new_data.status
            existing_mn.protocol = new_data.protocol
            existing_mn.payee = new_data.payee
            existing_mn.lastseen = new_data.lastseen
            existing_mn.activeseconds = new_data.activeseconds
            existing_mn.lastpaidtime = new_data.lastpaidtime
            existing_mn.lastpaidblock = new_data.lastpaidblock
            existing_mn.ip = new_data.ip

            # ... and finally update MN db record
            if cursor and existing_mn.modified:
                cursor.execute("UPDATE MASTERNODES set ident=?, status=?, protocol=?, payee=?,"
                               " last_seen=?, active_seconds=?, last_paid_time=?, "
                               " last_paid_block=?, ip=?"
                               "WHERE id=?",
                               (new_data.ident, new_data.status, new_data.protocol, new_data.payee,
                                new_data.lastseen, new_data.activeseconds, new_data.lastpaidtime,
                                new_data.lastpaidblock, new_data.ip, existing_mn.db_id))

        if self.open():

            if len(args) == 1 and args[0] == 'full':
                last_read_time = app_cache.get_value(f'MasternodesLastReadTime_{self.app_config.dash_network}', 0, int)
                logging.info("MasternodesLastReadTime: %d" % last_read_time)

                if self.masternodes and data_max_age > 0 and \
                   int(time.time()) - last_read_time < data_max_age:
                    logging.info('Using cached ghostnodelist (data age: %s)' % str(int(time.time()) - last_read_time))
                    return self.masternodes
                else:
                    logging.info('Loading masternode list from NIX daemon...')
                    mns = self.proxy.ghostnodelist(*args)
                    mns = parse_mns(mns)
                    logging.info('Finished loading masternode list')

                    # mark already cached masternodes to identify those to delete
                    for mn in self.masternodes:
                        mn.marker = False

                    # save masternodes to the db cache
                    db_modified = False
                    cur = None
                    try:
                        if self.db_intf.db_active:
                            cur = self.db_intf.get_cursor()

                        for mn in mns:
                            # check if newly-read masternode already exists in the cache
                            existing_mn = self.masternodes_by_ident.get(mn.ident)
                            if not existing_mn:
                                mn.marker = True
                                self.masternodes.append(mn)
                                self.masternodes_by_ident[mn.ident] = mn

                                if self.db_intf.db_active:
                                    cur.execute("INSERT INTO MASTERNODES(ident, status, protocol, payee, last_seen,"
                                            " active_seconds, last_paid_time, last_paid_block, ip, dmt_active,"
                                            " dmt_create_time) "
                                            "VALUES (?,?,?,?,?,?,?,?,?,?,?)",
                                            (mn.ident, mn.status, mn.protocol, mn.payee, mn.lastseen,
                                             mn.activeseconds, mn.lastpaidtime, mn.lastpaidblock, mn.ip, 1,
                                             datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')))
                                    mn.db_id = cur.lastrowid
                                    db_modified = True
                            else:
                                existing_mn.marker = True
                                update_masternode_data(existing_mn, mn, cur)
                                db_modified = True

                        # remove from the cache masternodes that no longer exist
                        for mn_index in reversed(range(len(self.masternodes))):
                            mn = self.masternodes[mn_index]

                            if not mn.marker:
                                if self.db_intf.db_active:
                                    cur.execute("UPDATE MASTERNODES set dmt_active=0, dmt_deactivation_time=?"
                                                "WHERE ID=?",
                                                (datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
                                                mn.db_id))
                                    db_modified = True
                                self.masternodes_by_ident.pop(mn.ident,0)
                                del self.masternodes[mn_index]

                        app_cache.set_value(f'MasternodesLastReadTime_{self.app_config.dash_network}', int(time.time()))
                        self.update_mn_queue_values()
                    finally:
                        if db_modified:
                            self.db_intf.commit()
                        if cur is not None:
                            self.db_intf.release_cursor()

                    return self.masternodes
            else:
                mns = self.proxy.ghostnodelist(*args)
                mns = parse_mns(mns)
                return mns
        else:
            raise Exception('Not connected')
 def restore_cache_settings(self):
     app_cache.restore_window_size(self)
     self.show_field_hinds = app_cache.get_value(
         CACHE_ITEM_SHOW_FIELD_HINTS, True, bool)
    def setupUi(self, Form):
        self.setSizePolicy(
            QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding,
                                  QtWidgets.QSizePolicy.MinimumExpanding))

        self.lay_main = QtWidgets.QVBoxLayout(Form)
        self.lay_main.setContentsMargins(6, 6, 6, 6)
        self.lay_main.setSpacing(3)

        # 'totals' area:
        self.lbl_totals = QLabel(Form)
        self.lbl_totals.setTextInteractionFlags(
            QtCore.Qt.LinksAccessibleByMouse | QtCore.Qt.TextSelectableByMouse)
        self.lay_main.addWidget(self.lbl_totals)

        # output definition data file labels:
        self.lay_data_file = QHBoxLayout()
        self.lay_data_file.setContentsMargins(0, 0, 0, 6)
        self.lay_main.addItem(self.lay_data_file)
        self.lbl_data_file_name = QLabel(Form)
        self.lay_data_file.addWidget(self.lbl_data_file_name)
        self.lbl_data_file_badge = QLabel(Form)
        self.lay_data_file.addWidget(self.lbl_data_file_badge)
        self.lbl_data_file_name.setTextInteractionFlags(
            QtCore.Qt.LinksAccessibleByMouse | QtCore.Qt.TextSelectableByMouse)
        self.lbl_data_file_badge.setTextInteractionFlags(
            QtCore.Qt.LinksAccessibleByMouse | QtCore.Qt.TextSelectableByMouse)
        self.lay_data_file.addStretch()

        # actions/options area:
        self.lay_actions = QHBoxLayout()
        self.lay_actions.setSpacing(6)
        self.lay_actions.setContentsMargins(0, 0, 0, 0)
        self.lay_main.addItem(self.lay_actions)
        self.btn_add_recipient = QPushButton(Form)
        self.btn_add_recipient.clicked.connect(
            partial(self.add_dest_address, 1))
        self.btn_add_recipient.setAutoDefault(False)
        self.btn_add_recipient.setText("Add recipient")
        self.lay_actions.addWidget(self.btn_add_recipient)
        #
        self.btn_actions = QPushButton(Form)
        self.btn_actions.clicked.connect(partial(self.add_dest_address, 1))
        self.btn_actions.setAutoDefault(False)
        self.btn_actions.setText("Actions")
        self.lay_actions.addWidget(self.btn_actions)

        # context menu for the 'Actions' button
        self.mnu_actions = QMenu()
        self.btn_actions.setMenu(self.mnu_actions)
        a = self.mnu_actions.addAction("Load from file...")
        a.triggered.connect(self.on_read_from_file_clicked)
        self.mnu_recent_files = self.mnu_actions.addMenu('Recent files')
        self.mnu_recent_files.setVisible(False)
        a = self.mnu_actions.addAction("Save to encrypted file...")
        a.triggered.connect(partial(self.save_to_file, True))
        a = self.mnu_actions.addAction("Save to plain CSV file...")
        a.triggered.connect(partial(self.save_to_file, False))
        a = self.mnu_actions.addAction("Clear recipients")
        a.triggered.connect(self.clear_outputs)

        self.lbl_output_unit = QLabel(Form)
        self.lbl_output_unit.setText('Values as')
        self.lay_actions.addWidget(self.lbl_output_unit)
        self.cbo_output_unit = QComboBox(Form)
        self.cbo_output_unit.addItems(['amount', 'percentage'])
        self.cbo_output_unit.setCurrentIndex(0)
        self.cbo_output_unit.currentIndexChanged.connect(
            self.on_cbo_output_unit_change)
        self.lay_actions.addWidget(self.cbo_output_unit)
        self.lay_actions.addStretch(0)

        # scroll area for send to (destination) addresses
        self.scroll_area = QtWidgets.QScrollArea()
        self.scroll_area.setWidgetResizable(True)
        self.scroll_area.setMinimumHeight(30)
        self.scroll_area.setSizePolicy(
            QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred,
                                  QtWidgets.QSizePolicy.MinimumExpanding))
        self.scroll_area.setFrameShape(QtWidgets.QFrame.NoFrame)
        self.lay_main.addWidget(self.scroll_area)

        self.scroll_area_widget = QtWidgets.QWidget()
        self.scroll_area_widget.setSizePolicy(
            QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred,
                                  QtWidgets.QSizePolicy.MinimumExpanding))
        self.lay_scroll_area = QtWidgets.QVBoxLayout()
        self.lay_scroll_area.setContentsMargins(0, 0, 0, 0)
        self.lay_scroll_area.setSpacing(0)
        self.scroll_area_widget.setLayout(self.lay_scroll_area)
        self.scroll_area.setWidget(self.scroll_area_widget)

        # grid layout for destination addresses and their corresponding controls:
        self.lay_addresses = QtWidgets.QGridLayout()
        self.lay_addresses.setSpacing(3)
        self.lay_addresses.setContentsMargins(0, 0, 0, 0)
        self.lay_scroll_area.addLayout(self.lay_addresses)
        self.lay_scroll_area.addStretch(0)

        # controls for the 'change' address/amount (it's placed in the last row of the addresses grid layout):
        self.lbl_change_address = QLabel(self.scroll_area_widget)
        self.lbl_change_address.setText('Change address')
        self.lbl_change_address.setAlignment(QtCore.Qt.AlignRight
                                             | QtCore.Qt.AlignTrailing
                                             | QtCore.Qt.AlignVCenter)
        self.lay_addresses.addWidget(self.lbl_change_address, 0, 0)
        # the 'change' address combobox:
        self.cbo_change_address = QtWidgets.QComboBox(self.scroll_area_widget)
        width = self.cbo_change_address.fontMetrics().width(
            'XvqNXF23dRBksxjW3VQGrBtJw7vkhWhenQ')
        self.address_widget_width = width + 40
        # combobox width on macos needs to be tweaked:
        self.cbo_change_address.setFixedWidth(
            self.address_widget_width + {'darwin': 5}.get(sys.platform, 0))
        self.lay_addresses.addWidget(self.cbo_change_address, 0, 1)
        self.lbl_change_amount = QLabel(self.scroll_area_widget)
        self.set_change_value_label()
        self.lay_addresses.addWidget(self.lbl_change_amount, 0, 2)
        # read only editbox for the amount of the change:
        self.edt_change_amount = QLineEdit(self.scroll_area_widget)
        self.edt_change_amount.setFixedWidth(100)
        self.edt_change_amount.setReadOnly(True)
        self.edt_change_amount.setStyleSheet('background-color:lightgray')
        self.lay_addresses.addWidget(self.edt_change_amount, 0, 3)
        # label dedicated to the second-unit value (e.g percentage if the main unit is set to (Dash) amount value)
        self.lbl_second_unit = QLabel(self.scroll_area_widget)
        self.lay_addresses.addWidget(self.lbl_second_unit, 0, 4)
        # spacer
        spacer = QtWidgets.QSpacerItem(20, 20, QtWidgets.QSizePolicy.Expanding,
                                       QtWidgets.QSizePolicy.Minimum)
        self.lay_addresses.addItem(spacer, 0, 5)

        # the last row of the grid layout is dedicated to 'fee' controls
        self.lbl_fee = QLabel(self.scroll_area_widget)
        self.lbl_fee.setText('Fee [Dash]')
        self.lbl_fee.setAlignment(QtCore.Qt.AlignRight
                                  | QtCore.Qt.AlignTrailing
                                  | QtCore.Qt.AlignVCenter)
        self.lay_addresses.addWidget(self.lbl_fee, 1, 0)

        # the fee value editbox with the 'use default' button:
        self.lay_fee_value = QHBoxLayout()
        self.lay_fee_value.setContentsMargins(0, 0, 0, 0)
        self.lay_fee_value.setSpacing(0)
        self.lay_addresses.addItem(self.lay_fee_value, 1, 1)
        self.edt_fee_value = QLineEdit(self.scroll_area_widget)
        self.edt_fee_value.setFixedWidth(100)
        self.edt_fee_value.textChanged.connect(self.on_edt_fee_value_changed)
        self.lay_fee_value.addWidget(self.edt_fee_value)
        self.btn_get_default_fee = QToolButton(self.scroll_area_widget)
        self.btn_get_default_fee.setText('\u2b06')
        self.btn_get_default_fee.setFixedSize(
            14,
            self.edt_fee_value.sizeHint().height())
        self.btn_get_default_fee.setToolTip('Use default fee')
        self.btn_get_default_fee.clicked.connect(
            self.on_btn_get_default_fee_clicked)
        self.lay_fee_value.addWidget(self.btn_get_default_fee)
        self.lay_fee_value.addStretch(0)

        # below the addresses grid place a label dedicated do display messages
        self.lbl_message = QLabel(Form)
        self.lbl_message.setTextInteractionFlags(
            QtCore.Qt.LinksAccessibleByMouse | QtCore.Qt.TextSelectableByMouse)
        self.lbl_message.setVisible(False)
        self.lay_main.addWidget(self.lbl_message)

        # add one 'send to' address row (in most cases it will bu sufficient)
        self.add_dest_address(1)

        # load last used file names from cache
        mru = app_cache.get_value(CACHE_ITEM_DATA_FILE_MRU_LIST,
                                  default_value=[],
                                  type=list)
        if isinstance(mru, list):
            for file_name in mru:
                if os.path.exists(file_name):
                    self.recent_data_files.append(file_name)
        self.update_mru_menu_items()

        self.retranslateUi(Form)