def on_update(self) -> None: if self._send_view._account_id is None: return self._stop_timer() current_id = None if self._send_view._payment_request is not None: current_id = self._send_view._payment_request.get_id() if current_id is None: current_item = self.currentItem() current_id = current_item.data( COL_RECEIVED, Qt.UserRole) if current_item else None self.clear() current_item = None current_time = time.time() nearest_expiry_time = float("inf") for row in self._send_view._account.invoices.get_invoices(): flags = row.flags & PaymentFlag.STATE_MASK if flags & PaymentFlag.UNPAID and row.date_expires: if row.date_expires <= current_time + 5: flags = (row.flags & ~PaymentFlag.UNPAID) | PaymentFlag.EXPIRED else: nearest_expiry_time = min(nearest_expiry_time, row.date_expires) requestor_uri = urllib.parse.urlparse(row.payment_uri) requestor_text = requestor_uri.netloc received_text = format_time(row.date_created, _("Unknown")) expires_text = format_time( row.date_expires, _("Unknown") if row.date_expires else _('Never')) item = QTreeWidgetItem([ received_text, expires_text, requestor_text, row.description, app_state.format_amount(row.value, whitespaces=True), # The tooltip text should be None to ensure the icon does not have extra RHS space. pr_tooltips.get(flags, None) ]) icon_entry = pr_icons.get(flags) if icon_entry: item.setIcon(COL_STATUS, read_QIcon(icon_entry)) if row.invoice_id == current_id: current_item = item item.setData(COL_RECEIVED, Qt.UserRole, row.invoice_id) item.setFont(COL_DESCRIPTION, self._monospace_font) item.setFont(COL_AMOUNT, self._monospace_font) self.addTopLevelItem(item) if current_item is not None: self.setCurrentItem(current_item) if nearest_expiry_time != float("inf"): self._start_timer(nearest_expiry_time)
def _on_update_utxo_list(self): if self._account_id is None: return prev_selection = self.get_selected( ) # cache previous selection, if any self.clear() for utxo in self._account.get_utxos(): metadata = self._account.get_transaction_metadata(utxo.tx_hash) prevout_str = utxo.key_str() prevout_str = prevout_str[0:10] + '...' + prevout_str[-2:] label = self._wallet.get_transaction_label(utxo.tx_hash) amount = app_state.format_amount(utxo.value, whitespaces=True) utxo_item = SortableTreeWidgetItem( [prevout_str, label, amount, str(metadata.height)]) # set this here to avoid sorting based on Qt.UserRole+1 utxo_item.DataRole = Qt.UserRole + 100 for col in (0, 2): utxo_item.setFont(col, self._monospace_font) utxo_item.setData(0, Qt.UserRole + 2, utxo) if self._account.is_frozen_utxo(utxo): utxo_item.setBackground(0, ColorScheme.BLUE.as_color(True)) self.addChild(utxo_item) if utxo in prev_selection: # NB: This needs to be here after the item is added to the widget. See #979. utxo_item.setSelected(True) # restore previous selection
def _update_transactions_tab_summary(self) -> None: local_count = 0 local_value = 0 if self._account_id is not None: wallet = self._account.get_wallet() with wallet.get_transaction_delta_table() as table: _account_id, local_value, local_count = table.read_balance( self._account_id, mask=TxFlags.STATE_UNCLEARED_MASK) if local_count == 0: self._local_summary_label.setVisible(False) return value_text = app_state.format_amount( local_value) + " " + app_state.base_unit() if local_count == 1: text = _( "The Transactions tab has <b>1</b> transaction containing <b>{balance}</b> " "in allocated coins.").format(balance=value_text) else: text = _( "The Transactions tab has <b>{count}</b> transactions containing " "<b>{balance}</b> in allocated coins.").format( count=local_count, balance=value_text) self._local_summary_label.setText(text) self._local_summary_label.setVisible(True)
def set_content(self, address_text, amount, message, url): self._address_edit.setPlainText(address_text) if amount: amount_text = '{} {}'.format(app_state.format_amount(amount), app_state.base_unit()) else: amount_text = '' self._amount_edit.setText(amount_text) self._message_edit.setPlainText(message) self.qrw.setData(url)
def redraw_from_list(self) -> None: self._from_list.clear() self._from_label.setHidden(len(self.pay_from) == 0) self._from_list.setHidden(len(self.pay_from) == 0) def format_utxo(utxo: UTXO) -> str: h = hash_to_hex_str(utxo.tx_hash) return '{}...{}:{:d}\t{}'.format(h[0:10], h[-10:], utxo.out_index, utxo.address) for utxo in self.pay_from: self._from_list.addTopLevelItem( QTreeWidgetItem( [format_utxo(utxo), app_state.format_amount(utxo.value)])) update_fixed_tree_height(self._from_list)
def _on_entry_changed(self) -> None: text = "" if self._not_enough_funds: amt_color = ColorScheme.RED text = _("Not enough funds") c, u, x = self._account.get_frozen_balance() if c + u + x: text += (' (' + app_state.format_amount(c + u + x).strip() + ' ' + app_state.base_unit() + ' ' + _("are frozen") + ')') if self.amount_e.isModified(): amt_color = ColorScheme.DEFAULT else: amt_color = ColorScheme.BLUE self._main_window.statusBar().showMessage(text) self.amount_e.setStyleSheet(amt_color.as_stylesheet())
def on_update(self) -> None: if self._account_id is None: return wallet = self._account._wallet with wallet.get_payment_request_table() as table: rows = table.read(self._account_id, flags=PaymentFlag.NONE, mask=PaymentFlag.ARCHIVED) # update the receive address if necessary current_key_id = self._receive_view.get_receive_key_id() if current_key_id is None: return keyinstance = None if self._account.is_deterministic(): keyinstance = self._account.get_fresh_keys(RECEIVING_SUBPATH, 1)[0] if keyinstance is not None: self._receive_view.set_receive_key(keyinstance) self._receive_view.set_new_button_enabled(current_key_id != keyinstance.keyinstance_id) # clear the list and fill it again self.clear() for row in rows: date = format_time(row.date_created, _("Unknown")) amount_str = app_state.format_amount(row.value, whitespaces=True) if row.value else "" script_template = self._account.get_script_template_for_id(row.keyinstance_id) address_text = script_template_to_string(script_template) state = row.state & sum(pr_icons.keys()) item = QTreeWidgetItem([date, address_text, '', row.description or "", amount_str, pr_tooltips.get(state,'')]) item.setData(0, Qt.UserRole, row.paymentrequest_id) if state != PaymentFlag.UNKNOWN: icon_name = pr_icons.get(state) if icon_name is not None: item.setIcon(6, read_QIcon(icon_name)) item.setFont(4, self._monospace_font) self.addTopLevelItem(item)
def _update_io(self, i_table: MyTreeWidget, o_table: MyTreeWidget) -> None: def get_xtxoutput_account( output: XTxOutput) -> Tuple[Optional[AbstractAccount], int]: if output.x_pubkeys: for x_pubkey in output.x_pubkeys: result = self._main_window._wallet.resolve_xpubkey( x_pubkey) if result is not None: account, keyinstance_id = result if account.get_script_for_id( keyinstance_id) == output.script_pubkey: return account, keyinstance_id # TODO: Document when this happens break return None, -1 def get_keyinstance_id(account: AbstractAccount, txo_key: TxoKeyType) -> Optional[int]: utxo = account._utxos.get(txo_key) if utxo is not None: return utxo.keyinstance_id stxo_keyinstance_id = account._stxos.get(txo_key) if stxo_keyinstance_id is not None: return stxo_keyinstance_id return None def compare_key_path(account: AbstractAccount, keyinstance_id: int, leading_path: Sequence[int]) -> bool: key_path = account.get_derivation_path(keyinstance_id) return key_path is not None and key_path[:len(leading_path )] == leading_path def name_for_account(account: AbstractAccount) -> str: name = account.display_name() return f"{account.get_id()}: {name}" is_tx_complete = self.tx.is_complete() is_tx_known = self._account and self._account.have_transaction_data( self._tx_hash) prev_txos = self._coin_service.get_outputs([ TxoKeyType(txin.prev_hash, txin.prev_idx) for txin in self.tx.inputs ]) prev_txo_dict = { TxoKeyType(r.tx_hash, r.tx_index): r for r in prev_txos } self._spent_value_label.setText( _("Spent input value") + ": " + app_state.format_amount(sum(r.value for r in prev_txos))) for tx_index, txin in enumerate(self.tx.inputs): account_name = "" source_text = "" amount_text = "" is_receiving = is_change = is_broken = False txo_key = TxoKeyType(txin.prev_hash, txin.prev_idx) if txin.is_coinbase(): source_text = "<coinbase>" else: prev_hash_hex = hash_to_hex_str(txin.prev_hash) source_text = f"{prev_hash_hex}:{txin.prev_idx}" # There are only certain kinds of transactions that have values on the inputs, # likely deserialised incomplete transactions from cosigners. Others? value = txin.value if self._account is not None: keyinstance_id = get_keyinstance_id(self._account, txo_key) is_receiving = compare_key_path(self._account, keyinstance_id, RECEIVING_SUBPATH) is_change = compare_key_path(self._account, keyinstance_id, CHANGE_SUBPATH) account_name = name_for_account(self._account) prev_txo = prev_txo_dict.get(txo_key, None) if prev_txo is not None and is_tx_complete: value = prev_txo.value if is_tx_known: # The transaction has been added to the account. is_broken = (prev_txo.flags & TransactionOutputFlag.IS_SPENT) == 0 else: # The transaction was most likely loaded from external source and is # being viewed but has not been added to the account. is_broken = (prev_txo.flags & TransactionOutputFlag.IS_SPENT) != 0 amount_text = app_state.format_amount(value, whitespaces=True) item = QTreeWidgetItem( [str(tx_index), account_name, source_text, amount_text]) item.setData(InputColumns.INDEX, Roles.TX_HASH, txin.prev_hash) item.setData(InputColumns.INDEX, Roles.IS_MINE, is_change or is_receiving) if is_receiving: item.setBackground(InputColumns.SOURCE, self._receiving_brush) if is_change: item.setBackground(InputColumns.SOURCE, self._change_brush) if is_broken: item.setBackground(InputColumns.SOURCE, self._broken_brush) item.setTextAlignment(InputColumns.AMOUNT, Qt.AlignRight | Qt.AlignVCenter) item.setFont(InputColumns.AMOUNT, self._monospace_font) i_table.addTopLevelItem(item) # TODO: Rewrite this to be lot simpler when we have better TXO management. At this time # we do not track UTXOs except when a transaction is known to the network as we rely on # the server state to map key usage to transactions or something. We need to completely # rewrite that and then rewrite this. Anyway, that is why signed tx outputs do not get # identified and colourised. received_value = 0 for tx_index, tx_output in enumerate(self.tx.outputs): text, _kind = tx_output_to_display_text(tx_output) if isinstance(_kind, Unknown_Output): text = script_bytes_to_asm(tx_output.script_pubkey) # In the longer run we will have some form of abstraction for incomplete transactions # that maps where the keys come from, but for now we manually map them to the limited # key hierarchy that currently exists. xtxo_account, xtxo_keyinstance_id = get_xtxoutput_account( tx_output) accounts: List[AbstractAccount] = [] if xtxo_account is not None: accounts.append(xtxo_account) if self._account is not None and xtxo_account is not self._account: accounts.append(self._account) account_id: Optional[int] = None account_name = "" keyinstance_id: Optional[int] = None is_receiving = is_change = False txo_key = TxoKeyType(self._tx_hash, tx_index) for account in accounts: if is_tx_complete: keyinstance_id = get_keyinstance_id(account, txo_key) elif account is xtxo_account and xtxo_keyinstance_id != -1: keyinstance_id = xtxo_keyinstance_id if keyinstance_id is not None: account_id = account.get_id() is_receiving = compare_key_path(account, keyinstance_id, RECEIVING_SUBPATH) is_change = compare_key_path(account, keyinstance_id, CHANGE_SUBPATH) account_name = name_for_account(account) received_value += tx_output.value break amount_text = app_state.format_amount(tx_output.value, whitespaces=True) item = QTreeWidgetItem( [str(tx_index), account_name, text, amount_text]) item.setData(OutputColumns.INDEX, Roles.IS_MINE, is_change or is_receiving) item.setData(OutputColumns.INDEX, Roles.ACCOUNT_ID, account_id) item.setData(OutputColumns.INDEX, Roles.KEY_ID, keyinstance_id) if is_receiving: item.setBackground(OutputColumns.DESTINATION, self._receiving_brush) if is_change: item.setBackground(OutputColumns.DESTINATION, self._change_brush) item.setTextAlignment(OutputColumns.AMOUNT, Qt.AlignRight | Qt.AlignVCenter) item.setFont(OutputColumns.AMOUNT, self._monospace_font) o_table.addTopLevelItem(item) self._received_value_label.setText( _("Received output value") + ": " + app_state.format_amount(received_value))
def __init__(self, main_window: ElectrumWindow) -> None: super().__init__(main_window) self._logger = logs.get_logger("key-view") self._main_window = weakref.proxy(main_window) self._account: AbstractAccount = None self._account_id: Optional[int] = None self._update_lock = threading.Lock() self._headers = COLUMN_NAMES self.verticalHeader().setVisible(False) self.setAlternatingRowColors(True) self._pending_state: Dict[int, EventFlags] = {} self._pending_actions = set([ListActions.RESET]) self._main_window.keys_created_signal.connect(self._on_keys_created) self._main_window.keys_updated_signal.connect(self._on_keys_updated) self._main_window.account_change_signal.connect( self._on_account_change) model = _ItemModel(self, self._headers) model.set_data(self._account_id, []) self._base_model = model # If the underlying model changes, observe it in the sort. self._proxy_model = proxy_model = _SortFilterProxyModel() proxy_model.setDynamicSortFilter(True) proxy_model.setSortRole(QT_SORT_ROLE) proxy_model.setSourceModel(model) self.setModel(proxy_model) fx = app_state.fx self._set_fiat_columns_enabled(fx and fx.get_fiat_address_config()) # Sort by type then by index, by making sure the initial sort is our type column. self.sortByColumn(BALANCE_COLUMN, Qt.DescendingOrder) self.setSortingEnabled(True) defaultFontMetrics = QFontMetrics(app_state.app.font()) def fw(s: str) -> int: return defaultFontMetrics.boundingRect(s).width() + 10 self._monospace_font = QFont(platform.monospace_font) monospaceFontMetrics = QFontMetrics(self._monospace_font) def mw(s: str) -> int: return monospaceFontMetrics.boundingRect(s).width() + 10 # We set the columm widths so that rendering is instant rather than taking a second or two # because ResizeToContents does not scale for thousands of rows. horizontalHeader = self.horizontalHeader() horizontalHeader.setMinimumSectionSize(20) horizontalHeader.resizeSection(TYPE_COLUMN, fw(COLUMN_NAMES[TYPE_COLUMN])) horizontalHeader.resizeSection(STATE_COLUMN, fw(COLUMN_NAMES[STATE_COLUMN])) horizontalHeader.resizeSection(KEY_COLUMN, fw("1442:01:m/000/1392")) horizontalHeader.resizeSection(SCRIPT_COLUMN, fw("MULTISIG_ACCUMULATOR")) horizontalHeader.setSectionResizeMode(LABEL_COLUMN, QHeaderView.Stretch) horizontalHeader.resizeSection(USAGES_COLUMN, fw(COLUMN_NAMES[USAGES_COLUMN])) balance_width = mw(app_state.format_amount(1.2, whitespaces=True)) horizontalHeader.resizeSection(BALANCE_COLUMN, balance_width) verticalHeader = self.verticalHeader() verticalHeader.setSectionResizeMode(QHeaderView.Fixed) # This value will get pushed out if the contents are larger, so it does not have to be # correct, it just has to be minimal. lineHeight = defaultFontMetrics.height() verticalHeader.setDefaultSectionSize(lineHeight) self.setVerticalScrollMode(QAbstractItemView.ScrollPerPixel) self.setSelectionBehavior(QAbstractItemView.SelectRows) # New selections clear existing selections, unless the user holds down control. self.setSelectionMode(QAbstractItemView.ExtendedSelection) self.setContextMenuPolicy(Qt.CustomContextMenu) self.customContextMenuRequested.connect(self._event_create_menu) app_state.app.base_unit_changed.connect( self._on_balance_display_change) app_state.app.fiat_balance_changed.connect( self._on_fiat_balance_display_change) app_state.app.fiat_ccy_changed.connect( self._on_fiat_balance_display_change) app_state.app.labels_changed_signal.connect(self.update_labels) app_state.app.num_zeros_changed.connect( self._on_balance_display_change) self.setEditTriggers(QAbstractItemView.DoubleClicked) self.doubleClicked.connect(self._event_double_clicked) self._last_not_synced = 0 self._timer = QTimer(self) self._timer.setSingleShot(False) self._timer.setInterval(1000) self._timer.timeout.connect(self._on_update_check)
def data(self, model_index: QModelIndex, role: int) -> QVariant: if self._view._account_id != self._account_id: return None row = model_index.row() column = model_index.column() if row >= len(self._data): return None if column >= len(self._column_names): return None if model_index.isValid(): line = self._data[row] # First check the custom sort role. if role == QT_SORT_ROLE: if column == TYPE_COLUMN: return line.script_type elif column == STATE_COLUMN: return line.flags elif column == KEY_COLUMN: return get_key_text(line) elif column == SCRIPT_COLUMN: return ScriptType(line.script_type).name elif column == LABEL_COLUMN: return self._view._account.get_keyinstance_label( line.keyinstance_id) elif column == USAGES_COLUMN: return line.match_count elif column in (BALANCE_COLUMN, FIAT_BALANCE_COLUMN): if column == BALANCE_COLUMN: return line.total_value elif column == FIAT_BALANCE_COLUMN: fx = app_state.fx rate = fx.exchange_rate() return fx.value_str(line.total_value, rate) elif role == QT_FILTER_ROLE: if column == KEY_COLUMN: return line elif role == Qt.DecorationRole: if column == TYPE_COLUMN: # TODO(rt12) BACKLOG Need to add variation in icons. return self._receive_icon elif role == Qt.DisplayRole: if column == TYPE_COLUMN: return None elif column == STATE_COLUMN: state_text = "" if line.flags & KeyInstanceFlag.ALLOCATED_MASK: state_text += "A" if line.flags & KeyInstanceFlag.IS_PAYMENT_REQUEST: state_text += "R" if line.flags & KeyInstanceFlag.IS_INVOICE: state_text += "I" if state_text: return state_text elif column == KEY_COLUMN: return get_key_text(line) elif column == SCRIPT_COLUMN: return ScriptType(line.script_type).name elif column == LABEL_COLUMN: return self._view._account.get_keyinstance_label( line.keyinstance_id) elif column == USAGES_COLUMN: return line.match_count elif column == BALANCE_COLUMN: return app_state.format_amount(line.total_value, whitespaces=True) elif column == FIAT_BALANCE_COLUMN: fx = app_state.fx rate = fx.exchange_rate() return fx.value_str(line.total_value, rate) elif role == Qt.FontRole: if column in (BALANCE_COLUMN, FIAT_BALANCE_COLUMN): return self._view._monospace_font elif role == Qt.BackgroundRole: # This does not work because the CSS overrides it. pass elif role == Qt.TextAlignmentRole: if column in (TYPE_COLUMN, STATE_COLUMN): return Qt.AlignCenter elif column in (BALANCE_COLUMN, FIAT_BALANCE_COLUMN, USAGES_COLUMN): return Qt.AlignRight | Qt.AlignVCenter return Qt.AlignVCenter elif role == Qt.ToolTipRole: if column == TYPE_COLUMN: return _("Key") elif column == STATE_COLUMN: if line.flags & KeyInstanceFlag.ALLOCATED_MASK: return _("This is an allocated address") elif not line.flags & KeyInstanceFlag.IS_ACTIVE: return _("This is an inactive address") elif column == KEY_COLUMN: key_id = line.keyinstance_id masterkey_id = line.masterkey_id derivation_text = self._view._account.get_derivation_path_text( key_id) return "\n".join([ f"Key instance id: {key_id}", f"Master key id: {masterkey_id}", f"Derivation path {derivation_text}", ]) elif role == Qt.EditRole: if column == LABEL_COLUMN: return self._view._account.get_keyinstance_label( line.keyinstance_id)
def data(self, model_index: QModelIndex, role: int) -> QVariant: row = model_index.row() column = model_index.column() if row >= len(self._data): return None if column >= len(self._column_names): return None if model_index.isValid(): line = self._data[row] # First check the custom sort role. if role == QT_SORT_ROLE: if column == DATE_ADDED_COLUMN: return line.date_added elif column == DATE_UPDATED_COLUMN: return line.date_updated elif column == STATE_COLUMN: if line.flags & TxFlags.StateDispatched: return 0 elif line.flags & TxFlags.StateReceived: return 2 elif line.flags & TxFlags.StateSigned: return 1 else: return 3 elif column == LABEL_COLUMN: return self._view._wallet.get_transaction_label(line.hash) elif column in (VALUE_COLUMN, FIAT_VALUE_COLUMN): return line.value elif role == Qt.DecorationRole: if column == LABEL_COLUMN and line.flags & TxFlags.PaysInvoice: return self._view._invoice_icon elif role == Qt.DisplayRole: if column == DATE_ADDED_COLUMN: return (format_time(line.date_added, _("unknown")) if line.date_added else _("unknown")) elif column == DATE_UPDATED_COLUMN: return (format_time(line.date_updated, _("unknown")) if line.date_updated else _("unknown")) elif column == STATE_COLUMN: if line.flags & TxFlags.StateDispatched: return _("Dispatched") elif line.flags & TxFlags.StateReceived: return _("Received") elif line.flags & TxFlags.StateSigned: return _("Signed") return line.flags elif column == LABEL_COLUMN: return self._view._wallet.get_transaction_label(line.hash) elif column == VALUE_COLUMN: return app_state.format_amount(line.value, whitespaces=True) elif column == FIAT_VALUE_COLUMN: fx = app_state.fx rate = fx.exchange_rate() return fx.value_str(line.value, rate) elif role == Qt.FontRole: if column in (VALUE_COLUMN, FIAT_VALUE_COLUMN): return self._monospace_font elif role == Qt.TextAlignmentRole: if column in (VALUE_COLUMN, FIAT_VALUE_COLUMN): return Qt.AlignRight | Qt.AlignVCenter return Qt.AlignVCenter elif role == Qt.ToolTipRole: if column == LABEL_COLUMN: if line.flags & TxFlags.PaysInvoice: return _("This transaction is associated with an invoice.") elif column == STATE_COLUMN: if line.flags & TxFlags.StateDispatched: return _("This transaction has been sent to the network, but has not " "cleared yet.") elif line.flags & TxFlags.StateReceived: return _("This transaction has been received from another party, but " "has not been broadcast yet.") elif line.flags & TxFlags.StateSigned: return _("This transaction has been signed, but has not been broadcast " "yet.") elif role == Qt.EditRole: if column == LABEL_COLUMN: return self._view._wallet.get_transaction_label(line.hash)
def get_base_amount(self, sv_value: int) -> str: return app_state.format_amount(sv_value)
def __init__(self, main_window: 'ElectrumWindow', row: InvoiceRow) -> None: super().__init__(main_window, _("Invoice")) self.setMinimumWidth(400) self._main_window = weakref.proxy(main_window) self._pr = pr = PaymentRequest.from_json(row.invoice_data) state = row.flags & PaymentFlag.STATE_MASK if state & PaymentFlag.UNPAID and has_expired(row.date_expires): state = PaymentFlag.EXPIRED total_amount = 0 for output in pr.outputs: total_amount += output.amount vbox = QVBoxLayout(self) form = FormSectionWidget(minimum_label_width=120) form.add_row(_('Type'), QLabel(_("BIP270"))) form.add_row(_("State"), QLabel(pr_tooltips.get(state, _("Unknown")))) form.add_row( _('Amount'), QLabel( app_state.format_amount(output.amount) + " " + app_state.base_unit())) form.add_row(_('Memo'), QLabel(row.description)) form.add_row(_('Date Created'), QLabel(format_time(pr.creation_timestamp, _("Unknown")))) form.add_row(_('Date Received'), QLabel(format_time(row.date_created, _("Unknown")))) if row.date_expires: form.add_row(_('Date Expires'), QLabel(format_time(row.date_expires, _("Unknown")))) vbox.addWidget(form) self._table = table = ButtonsTableWidget() table.setSelectionBehavior(QAbstractItemView.SelectRows) table.setSelectionMode(QAbstractItemView.SingleSelection) vh = table.verticalHeader() vh.setSectionResizeMode(QHeaderView.ResizeToContents) vh.hide() table.setColumnCount(3) table.setContextMenuPolicy(Qt.CustomContextMenu) table.customContextMenuRequested.connect(self._on_table_menu) table.setHorizontalHeaderLabels(self._table_column_names) table.setRowCount(len(pr.outputs)) # table.addButton("icons8-copy-to-clipboard-32.png", f, # _("Copy all listed destinations to the clipboard")) # table.addButton("icons8-save-as-32-windows.png", f, # _("Save the listed destinations to a file")) hh = table.horizontalHeader() hh.setStretchLastSection(True) for row, output in enumerate(pr.outputs): label = QLabel( app_state.format_amount(output.amount) + " " + app_state.base_unit()) table.setCellWidget(row, 0, label) table.setCellWidget(row, 1, QLabel(output.description)) kind = classify_output_script(output.script, Net.COIN) text = script_to_display_text(output.script, kind) table.setCellWidget(row, 2, QLabel(text)) vbox.addWidget(table, 1) def do_export(): fn = self._main_window.getSaveFileName(_("Export invoice to file"), "*.bip270.json") if not fn: return with open(fn, 'w') as f: data = f.write(row.invoice_data) self._main_window.show_message(_('Invoice saved as' + ' ' + fn)) exportButton = EnterButton(_('Export'), do_export) def do_delete(): if self.question( _('Are you sure you want to delete this invoice?'), title=_("Delete invoice"), icon=QMessageBox.Warning): self._main_window._send_view._invoice_list._delete_invoice( row.invoice_id) self.close() deleteButton = EnterButton(_('Delete'), do_delete) vbox.addLayout(Buttons(exportButton, deleteButton, CloseButton(self)))
def _update_io(self, i_table: MyTreeWidget, o_table: MyTreeWidget) -> None: def get_xtxoutput_account( output: XTxOutput) -> Optional[AbstractAccount]: if not output.x_pubkeys: return None for x_pubkey in output.x_pubkeys: result = self._main_window._wallet.resolve_xpubkey(x_pubkey) if result is not None: account, keyinstance_id = result if account.get_script_for_id( keyinstance_id) == output.script_pubkey: return account return None return None known_txos: Set[Tuple[bytes, int]] = set() if self._account is not None: known_txos = set(self._account._utxos) | set(self._account._stxos) def compare_key_path(account: AbstractAccount, txo_key: TxoKeyType, leading_path: Sequence[int]) -> bool: utxo = account._utxos.get(txo_key) if utxo is not None: key_path = account.get_derivation_path(utxo.keyinstance_id) if key_path is not None and key_path[:len(leading_path )] == leading_path: return True stxo_keyinstance_id = account._stxos.get(txo_key) if stxo_keyinstance_id is not None: key_path = account.get_derivation_path(stxo_keyinstance_id) if key_path is not None and key_path[:len(leading_path )] == leading_path: return True return False def name_for_account(account: AbstractAccount) -> str: name = account.display_name() return f"{account.get_id()}: {name}" for tx_index, txin in enumerate(self.tx.inputs): account_name = "" source_text = "" amount_text = "" is_receiving = is_change = False if txin.is_coinbase(): source_text = "<coinbase>" else: prev_hash_hex = hash_to_hex_str(txin.prev_hash) source_text = f"{prev_hash_hex}:{txin.prev_idx}" if self._account is not None: txo_key = TxoKeyType(txin.prev_hash, txin.prev_idx) is_receiving = compare_key_path(self._account, txo_key, RECEIVING_SUBPATH) is_change = compare_key_path(self._account, txo_key, CHANGE_SUBPATH) account_name = name_for_account(self._account) # TODO(rt12): When does a txin have a value? Loaded incomplete transactions only? if txin.value is not None: amount_text = app_state.format_amount(txin.value, whitespaces=True) item = QTreeWidgetItem( [str(tx_index), account_name, source_text, amount_text]) # item.setData(0, Qt.UserRole, row.paymentrequest_id) if is_receiving: item.setBackground(2, self._receiving_brush) if is_change: item.setBackground(2, self._change_brush) item.setTextAlignment(3, Qt.AlignRight | Qt.AlignVCenter) item.setFont(3, self._monospace_font) i_table.addTopLevelItem(item) for tx_index, tx_output in enumerate(self.tx.outputs): text, _kind = tx_output_to_display_text(tx_output) if isinstance(_kind, Unknown_Output): text = script_bytes_to_asm(tx_output.script_pubkey) account = get_xtxoutput_account(tx_output) accounts: List[AbstractAccount] = [] if account is not None: accounts.append(account) if self._account is not None and account is not self._account: accounts.append(self._account) account_name = "" is_receiving = is_change = False txo_key = TxoKeyType(self._tx_hash, tx_index) for account in accounts: if txo_key in account._stxos or txo_key in account._utxos: is_receiving = compare_key_path(account, txo_key, RECEIVING_SUBPATH) is_change = compare_key_path(account, txo_key, CHANGE_SUBPATH) account_name = name_for_account(account) break amount_text = app_state.format_amount(tx_output.value, whitespaces=True) item = QTreeWidgetItem( [str(tx_index), account_name, text, amount_text]) # item.setData(0, Qt.UserRole, row.paymentrequest_id) if is_receiving: item.setBackground(2, self._receiving_brush) if is_change: item.setBackground(2, self._change_brush) item.setTextAlignment(3, Qt.AlignRight | Qt.AlignVCenter) item.setFont(3, self._monospace_font) o_table.addTopLevelItem(item)
def _on_update_history_list(self) -> None: item = self.currentItem() current_tx_hash = item.data(Columns.STATUS, self.TX_ROLE) if item else None self.clear() if self._account is None: return fx = app_state.fx if fx: fx.history_used_spot = False local_height = self._wallet.get_local_height() server_height = self._main_window.network.get_server_height() if self._main_window.network \ else 0 header_at_height = app_state.headers.header_at_height chain = app_state.headers.longest_chain() missing_header_heights = [] items = [] for line, balance in self._account.get_history(self.get_domain()): tx_id = hash_to_hex_str(line.tx_hash) conf = 0 if line.height <= 0 else max( local_height - line.height + 1, 0) timestamp = False if line.height > 0: try: timestamp = header_at_height(chain, line.height).timestamp except MissingHeader: if line.height <= server_height: missing_header_heights.append(line.height) else: logger.debug("Unable to backfill header at %d (> %d)", line.height, server_height) status = get_tx_status(self._account, line.tx_hash, line.height, conf, timestamp) status_str = get_tx_desc(status, timestamp) v_str = app_state.format_amount(line.value_delta, True, whitespaces=True) balance_str = app_state.format_amount(balance, whitespaces=True) label = self._wallet.get_transaction_label(line.tx_hash) entry = [None, tx_id, status_str, label, v_str, balance_str] if fx and fx.show_history(): date = timestamp_to_datetime( time.time() if conf <= 0 else timestamp) for amount in [line.value_delta, balance]: text = fx.historical_value_str(amount, date) entry.append(text) item = SortableTreeWidgetItem(entry) # If there is no text, item.setIcon(Columns.STATUS, get_tx_icon(status)) item.setToolTip(Columns.STATUS, get_tx_tooltip(status, conf)) if line.tx_flags & TxFlags.PaysInvoice: item.setIcon(Columns.DESCRIPTION, self.invoiceIcon) for i in range(len(entry)): if i > Columns.DESCRIPTION: item.setTextAlignment(i, Qt.AlignRight | Qt.AlignVCenter) else: item.setTextAlignment(i, Qt.AlignLeft | Qt.AlignVCenter) if i != Columns.DATE: item.setFont(i, self.monospace_font) if line.value_delta and line.value_delta < 0: item.setForeground(Columns.DESCRIPTION, self.withdrawalBrush) item.setForeground(Columns.AMOUNT, self.withdrawalBrush) item.setData(Columns.STATUS, SortableTreeWidgetItem.DataRole, line.sort_key) item.setData(Columns.DATE, SortableTreeWidgetItem.DataRole, line.sort_key) item.setData(Columns.STATUS, self.ACCOUNT_ROLE, self._account_id) item.setData(Columns.STATUS, self.TX_ROLE, line.tx_hash) # self.insertTopLevelItem(0, item) if current_tx_hash == line.tx_hash: self.setCurrentItem(item) items.append(item) self.addTopLevelItems(items) if len(missing_header_heights) and self._main_window.network: self._main_window.network.backfill_headers_at_heights( missing_header_heights)